construe

Crates.ioconstrue
lib.rsconstrue
version0.0.3
sourcesrc
created_at2023-07-24 14:12:41.701989
updated_at2023-08-09 16:36:02.079388
descriptionCompile-Time Growable Array: Vec & String for const!
homepage
repositoryhttps://git.sr.ht/~xaos/construe
max_upload_size
id924628
size68,852
(itsxaos)

documentation

https://docs.rs/construe

README

construe

Compile-Time Growable Array: Vec & String for const!

Construe & StrConstrue allow you to write const functions that behave as though though they can collect items into a Vec or &strs into a String, returning an array or &str, whose length does not need to be known before the function is called.

The one caveat is that this is only possible for deterministic functions which, given the same inputs, will always return the same amount of items. This is because they will be invoked twice: once to determine the size of the buffer needed and once to collect its contents.

Some other restrictions (currently) apply:

  • can't overwrite previous items or assign directly to an index
  • items can only be added to the end (only .push())
  • can't remove items from the buffer (e.g. no .pop())
  • it's not possible to inspect the buffer during construction
  • no fallback mode for use outside of const contexts

Examples

Simple compile-time &str concatenation using StrConstrue:

use construe::{StrConstrue, construe};

/// Concatenate all `&str`s into a single `&str`
const fn concat<const N: usize>(mut slice: &[&str]) -> StrConstrue<N> {
    let mut strc = StrConstrue::new();
    // no `for` in const
    while let [s, rest @ ..] = slice {
        slice = rest;
        // by-value since there's no `&mut` in const
        strc = strc.push_str(s);
    }
    strc
}

construe!(const HELLO_WORLD: &str = concat(&["Hello", " ", "World", "!"]));

assert_eq!(HELLO_WORLD, "Hello World!");

And a slightly more complicated example, using Construe:

use construe::{Construe, construe};

/// Multiply each item in `slice` `x` times
const fn multiply<const N: usize, T: Copy>(mut slice: &[T], x: usize)
    -> Construe<T, N>
{
    let mut c = Construe::new();
    while let [item, rest @ ..] = slice {
        slice = rest;
        let mut i = 0;
        while i < x {
            // see Construe::push docs on why we need `.0` at the end
            c = c.push(*item).0;
            i += 1;
        }
    }
    c
}

// as slice:
construe!(const NUMBERS: &[u8] = multiply(&[1, 2, 3], 2));
assert_eq!(NUMBERS, [1, 1, 2, 2, 3, 3]);

// as array:
construe!(const NUMBERS_ARRAY: [u8; _] = multiply(&[1, 2, 3], 2));
assert_eq!(NUMBERS, &NUMBERS_ARRAY);

// or with `&str`s:
construe!(const WORDS: &[&str] = multiply(&["hey", "you"], 2));
assert_eq!(WORDS, &["hey", "hey", "you", "you"]);

Possible Improvements

Some of the restrictions mentioned above may or may not be remedied in future versions (roughly in the order given).

However, if a specialized version of these types is required, it would be easiest for you to just write it yourself. For instance, if your function will need to inspect, say, the last 4 items of the buffer during construction, it would be fairly easy to write a Construe that holds a buffer of size 4 for the first run too.
Implementing pop() where you don't need the result would be trivial, and if you do, it would be analogous to the previous case, as long as you can guarantee that a buffer of size N can keep up (e.g. "pop() won't be called more than once between push()es"). If you can't make this guarantee it might still be possible to implement: you could run your function thrice, once to determine the size of the look-back buffer, then twice like a normal Construe.

Generally, I think the method used by this crate can be extended to make any computation work, given that you could implement an allocator: it would abort the computation prematurely if it runs out of space and ask for twice as much. Then you need to invoke the computation at most log2(available_memory) times.
Probably better to just wait until const support is better though.

Commit count: 0

cargo fmt