How to format a const str using const generic?

Minimal example:

trait SQLDataType {
    const NAME: &'static str;
}

pub struct VarChar<const LEN: usize>;

impl<const LEN: usize> SQLDataType for VarChar<LEN> {
    const NAME: &'static str = const_format::formatcp!("VARCHAR({})", LEN);
}

The crate const_format mentioned in the code.

Compiler error message:

❯ cargo build
   Compiling milang v0.1.0
error[E0401]: can't use generic parameters from outer item
  --> src/main.rs:16:71
   |
15 | impl<const LEN: usize> SQLDataType for VarChar<LEN> {
   |            --- const parameter from outer item
16 |     const NAME: &'static str = const_format::formatcp!("VARCHAR({})", LEN);
   |                                                                       ^^^ use of generic parameter from outer item
   |
   = note: a `const` is a separate item from the item that contains it

Is it possible to achieve this (generate SQL data type declaration string)?

The compiler more-or-less tells you what to do. When you put const or fn inside of your function it actually acts as a namespace: there are only one such const and thus it couldn't doesn't of LEN.

To make it possible to depend on LEN you have to put it into some place where LEN can be used.

impl<const LEN: usize> for VarChar<LEN> {
    …
}

should do the trick. In the impl block you are allowed to introduce items that depend on LEN.

Could you show me a complete version of your solution? I am not sure I understand your solution. Do you mean I can only do this when implement inherent methods but not implementing traits?

that's not true, whether it is an inherent impl block or trait impl block, it is an associated item nevertheless. the limitation is probably because of how the macro formatcp is implemented, e.g. it likely expands to a block expression (so it won't use conflicted identifiers, as procedural macros is not hygienic). while the const variable NAME itself is an associated item, the const variables inside the block are not. here's an example to demonstrate what I was describing:

struct Foo<const LEN: usize>;
impl<const LEN: usize> Foo<LEN> {
    const X: usize = LEN: //<--- this is ok
    const Y: usize = { LEN }; //<--- this is also ok
    const Z: usize = {
        const TMP: usize = LEN; //<--- this is NOT ok
        TMP
    };
}

I didn't check the source code of the implementation, but if my guess was right, what you want to do is not possible with the current version of const_format crate, no matter it's in an inherent impl or trait impl.

1 Like

They document the limitation.

  • The formatting macros that expand to &'static strs can only use constants from concrete types, so while a Type::<u8>::FOO argument would be fine, Type::<T>::FOO would not be (T being a type parameter).

Is it possible to do without const_format?

there's no support in the standard library. it is "possible", in the sense that the language does have the machenism, macros, to implement such features, but it is NOT EASY to implement, especially in a very flexible and robust manner. however, for some specific requirements, it might be reasonably and practically doable.

if the NAME constant is only used as some unique key, or will only show up in diagnostic messages (and you are willing to use unstable features), you can use an alternative API std::any::type_name()

#![feature(const_type_name)]
impl<const LEN: usize> SQLDataType for VarChar<LEN> {
    const NAME: &'static str = std::any::type_name::<Self>();
}

Thx for the suggestion. Unfortunately, I would like to have the control of NAME so it is not an option.

You mentioned macros is sufficient for this, even though it is difficult, but I cannot understand how it could possibly be achieved. Using macros will still have to refer to LEN pretty much. Since its value is not available yet, the only way I can see is for the macro to eventually evaluated into a block, so I would be faced with the same problem as using const_format. Could you enlighten me?

still, chances are you don't need the full formatting capabilities, so you might get away with your own (greatly simplified) implementation.

for example, the following example is supported by const-format:

const MESSAGE: &str = formatcp!("{greet}, {name}, the answer is {:#?}", 42usize, greet = "hello", name = "world");

but I would imagine most use cases don't need the "advanced" formatting syntax and probably looks like this:

const MESSAGE: &str = formatcp!("{} + {} = {}", 1usize, 2usize, 1usize + 2usize);

it really depends on the situations. block expressions alone are fine, it's the constants defined inside the block that is problematic, since they are themselves items.

if you can limit the problem domain for some specific use cases instead of creating a flexible and robust library for general uses, I think there's room for potential optimization/simplifications.

for example, if you can implement the macro with only let bindings instead of intermediate const items, block expressions would just work fine.

another possibility I would guess is, the macro could define const functions (with potential const generics), but depending on the use case, the user might be required to use certain syntax conventions (e.g.use ALL_UPPER_CASE or some DSL marker for named const) so the macro can generate the function signatures right.

#![feature(generic_const_exprs)]
#![allow(incomplete_features)]
#![allow(dead_code)]

fn main() {}

trait T {
    const S: &'static str;
}

pub struct A<const N: usize>;

const fn number_of_digits<const N: usize>() -> usize {
    if N > 0 {
        N.ilog10() as usize + 1usize
    } else {
        1usize
    }
}

const USIZE_MAX_LEN: usize = number_of_digits::<{ usize::MAX }>();

macro_rules! digit {
    (
        $n: expr,
        $p: expr$(,)?
    ) => {
        (($n / $p / 10usize > 0) as u8) * (b'0' + ($n / $p / 10usize % 10) as u8)
            + ((!($n / $p / 10usize > 0)) as u8) * b' '
    };
}

const EXTRA_LEN: usize = "VARCHAR()".len();

const fn usize_to_u8<const N: usize>() -> &'static [u8; USIZE_MAX_LEN + EXTRA_LEN] {
    &[
        b'V',
        b'A',
        b'R',
        b'C',
        b'H',
        b'A',
        b'R',
        b'(',
        digit!(N, 1000000000000000000usize),
        digit!(N, 100000000000000000usize),
        digit!(N, 10000000000000000usize),
        digit!(N, 1000000000000000usize),
        digit!(N, 100000000000000usize),
        digit!(N, 10000000000000usize),
        digit!(N, 1000000000000usize),
        digit!(N, 100000000000usize),
        digit!(N, 10000000000usize),
        digit!(N, 1000000000usize),
        digit!(N, 100000000usize),
        digit!(N, 10000000usize),
        digit!(N, 1000000usize),
        digit!(N, 100000usize),
        digit!(N, 10000usize),
        digit!(N, 1000usize),
        digit!(N, 100usize),
        digit!(N, 10usize),
        digit!(N, 1usize),
        b'0' + (N % 10) as u8,
        b')',
    ]
}

const fn usize_to_varchar_str<const N: usize>() -> &'static str {
    unsafe { core::str::from_utf8_unchecked(usize_to_u8::<N>().trim_ascii()) }
}

impl<const LEN: usize> T for A<LEN> {
    const S: &'static str = usize_to_varchar_str::<LEN>();
}

fn test_case<const N: usize>()
where
    [u8; USIZE_MAX_LEN - number_of_digits::<N>()]: Sized,
{
    let n_string: String = N.to_string();
    assert!(n_string.len() <= USIZE_MAX_LEN);
    let spaces =
        core::str::from_utf8(&[b' '; USIZE_MAX_LEN - number_of_digits::<N>()]).expect("impossible");
    let result = format!("VARCHAR({}{})", spaces, n_string,);
    assert_eq!(<A<N> as T>::S, result);
}

#[test]
fn test() {
    test_case::<0>();
    test_case::<{ usize::MAX }>();
    test_case::<10>();
}

Playground

Come up with a very ugly solution. Very. But I don't see how to avoid writing down the whole array without naming variables or creating temporary values, without this.

EDIT:

My use case is much more specific than writing a const_format. In fact, I just want to implement a function of signature const fn<const N: usize>() -> &'static [u8] where the return is something plus the string of the number N. Unfortunately, due to my limited scope of knowledge, I find myself crippled by the fact that I can only use const function without defining any const item via N, without defining runtime variables since it is a const fn to be defined. Please tell me if there is anything I can use to just not do this.

This solution is so frustrating that just removing the leading spaces of the digits (Now I have VARCHAR( 0) would require thinking about how to do the arithmetic correctly to compute the bytes, which I shall defer into future work.

Here's a way to convert a number in a const generic parameter to a &'static str on stable. This can be extended to support arbitrary formatting.

The key point here is to

  • figure out an upper bound on the size of the buffer
  • write to the buffer
  • slice off any parts you didn't write to

And depending on how you arrange the const blocks, this can all be done in a single function

edit: note that &const { ... } is relying on const-promotion, which is guaranteed to work with const { ... } blocks. This way we can compute something at compile time, then take a reference to that computed work, and the Rust compiler will figure out how to work out the lifetimes.

2 Likes