Skip to content

Conversation

@tdelabro
Copy link
Collaborator

@tdelabro tdelabro commented Nov 5, 2025

Adding a felt! macro that allow for easy initialization of Felt from different types

use syn::{Expr, Lit, parse_macro_input};

#[proc_macro]
pub fn felt(input: TokenStream) -> TokenStream {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, I’m one of the maintainers of starknet-foundry. I believe we could really benefit from const support for numeric and decimal string literals. Would you be interested in adding this capability?

Here is some code that enables this functionality, including support for const expressions using numeric and decimal string literals.

use lambdaworks_math::field::element::FieldElement;
use lambdaworks_math::field::fields::fft_friendly::stark_252_prime_field::Stark252PrimeField;
use lambdaworks_math::unsigned_integer::element::UnsignedInteger;
use proc_macro::TokenStream;
use quote::quote;
use syn::{Expr, ExprLit, Lit, UnOp, parse_macro_input};

/// Used in macro as we can't import `Felt` form `starknet-types-core`
/// into the macro scope because of circular dependencies
type Felt = FieldElement<Stark252PrimeField>;

/// Create a [`Felt`] from an [`i128`] value
fn felt_from_i128(value: i128) -> Felt {
    let mut res = FieldElement::from(&UnsignedInteger::from(value.unsigned_abs()));
    if value.is_negative() {
        res = -res;
    }
    res
}

#[proc_macro]
pub fn felt(input: TokenStream) -> TokenStream {
    let expr = parse_macro_input!(input as Expr);
    match parse_felt_input(&expr) {
        Ok(ir) => generate_felt_code(ir),
        Err(e) => e.to_compile_error().into(),
    }
}

/// Intermediate representation for valid felt values
enum FeltInput {
    /// A value that can be fully computed at macro expansion
    Const(Felt),
    /// A value that must be evaluated at runtime
    ExprOnly(Expr),
}

/// Try to convert syn::Expr into a FeltInput
fn parse_felt_input(expr: &Expr) -> syn::Result<FeltInput> {
    match expr {
        // Handle literal: bool
        Expr::Lit(ExprLit {
            lit: Lit::Bool(b), ..
        }) => Ok(FeltInput::Const(if b.value {
            Felt::from(1)
        } else {
            Felt::from(0)
        })),

        // Handle literal: integer
        Expr::Lit(ExprLit {
            lit: Lit::Int(i), ..
        }) => {
            let value = i.base10_parse::<i128>()?;
            Ok(FeltInput::Const(felt_from_i128(value)))
        }

        // Handle literal: string ("42", "-1", "0x42")
        Expr::Lit(ExprLit {
            lit: Lit::Str(s), ..
        }) => {
            let value = s.value();
            if value.starts_with("0x") || value.starts_with("0X") {
                Ok(FeltInput::Const(Felt::from_hex_unchecked(&value)))
            } else if let Ok(parsed) = value.parse::<i128>() {
                Ok(FeltInput::Const(felt_from_i128(parsed)))
            } else {
                Err(syn::Error::new_spanned(s, "Invalid decimal string literal"))
            }
        }

        // Handle unary negation of integer: -42
        Expr::Unary(unary) if matches!(unary.op, UnOp::Neg(_)) => {
            if let Expr::Lit(ExprLit {
                lit: Lit::Int(i), ..
            }) = &*unary.expr
            {
                let val = i.base10_parse::<i128>()?;
                Ok(FeltInput::Const(felt_from_i128(-val)))
            } else {
                Ok(FeltInput::ExprOnly(expr.clone()))
            }
        }

        // Fallback to runtime expression
        _ => Ok(FeltInput::ExprOnly(expr.clone())),
    }
}

/// Generate code from [`FeltInput`].
fn generate_felt_code(input: FeltInput) -> TokenStream {
    match input {
        FeltInput::Const(felt) => {
            let raw = felt.to_raw().limbs;
            let r0 = raw[0];
            let r1 = raw[1];
            let r2 = raw[2];
            let r3 = raw[3];
            quote! {
                Felt::from_raw([#r0, #r1, #r2, #r3])
            }
        }

        FeltInput::ExprOnly(expr) => {
            quote! {
                match Felt::try_from(#expr) {
                    Ok(f) => f,
                    Err(_) => panic!("Invalid Felt value at runtime"),
                }
            }
        }
    }
    .into()
}

Feel free to use or adapt it as needed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @ksew1!
Thanks a lot! It would indeed be great to have this additional support. I didn't include it initially coz it required extra development. But if you already have all this ready, I will include it in the PR asap

Thanks!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ksew1 Ok I just pushed a new commit with basically every possible literal type handled at compile time.

// Bools
assert_eq!(felt!(false), Felt::ZERO);
assert_eq!(felt!(true), Felt::ONE);
assert_eq!(felt!(-true), Felt::from(-1));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this assert is repeated in line 66

Comment on lines +64 to +65
assert_eq!(felt!(-"42"), Felt::ZERO - Felt::from(42));
assert_eq!(felt!(-"-42"), Felt::from(42));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Rust doesn't allow this syntax then I think it may be a bit confusing allowing this and causing divergence in what does this macro allow vs what does Rust allow

@@ -1,4 +1,4 @@
[toolchain]
channel = "1.87.0"
channel = "1.89.0"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this bump necessary?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants