From 735b912eca9d8f9e0542d68f5bbc1eff40d6b997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Tue, 4 Nov 2025 20:57:24 -0300 Subject: [PATCH 01/10] feat: add macro --- Cargo.toml | 1 + crates/felt-macro/Cargo.toml | 12 +++ crates/felt-macro/src/lib.rs | 101 +++++++++++++++++++++ crates/starknet-types-core/Cargo.toml | 3 + crates/starknet-types-core/src/felt/mod.rs | 86 ++++++++++++++++++ rust-toolchain.toml | 2 +- 6 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 crates/felt-macro/Cargo.toml create mode 100644 crates/felt-macro/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index e65651f0..e3be8438 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "crates/felt-macro", "crates/starknet-types-core", ] diff --git a/crates/felt-macro/Cargo.toml b/crates/felt-macro/Cargo.toml new file mode 100644 index 00000000..892e132c --- /dev/null +++ b/crates/felt-macro/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "felt-macro" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" diff --git a/crates/felt-macro/src/lib.rs b/crates/felt-macro/src/lib.rs new file mode 100644 index 00000000..d406b807 --- /dev/null +++ b/crates/felt-macro/src/lib.rs @@ -0,0 +1,101 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{Expr, Lit, parse_macro_input}; + +#[proc_macro] +pub fn felt(input: TokenStream) -> TokenStream { + let expr = parse_macro_input!(input as Expr); + + match &expr { + Expr::Lit(expr_lit) => match &expr_lit.lit { + Lit::Str(lit_str) => { + let value = lit_str.value(); + + // Check if it's a hex string (starts with 0x or 0X) + if value.starts_with("0x") || value.starts_with("0X") { + // Hex string: use const fn for compile-time validation + quote! { + { + const __FELT_VALUE: Felt = Felt::from_hex_unwrap(#lit_str); + __FELT_VALUE + } + } + .into() + } else { + // Check for valid decimal format (optional leading minus, then digits) + let is_valid = if let Some(stripped) = value.strip_prefix('-') { + !stripped.is_empty() && stripped.chars().all(|c| c.is_ascii_digit()) + } else { + !value.is_empty() && value.chars().all(|c| c.is_ascii_digit()) + }; + + if !is_valid { + return syn::Error::new_spanned( + lit_str, + format!("Invalid Felt decimal string literal: '{}'. Expected decimal digits (0-9), optionally prefixed with '-'.", value) + ) + .to_compile_error() + .into(); + } + + // Valid format, generate runtime parsing code + quote! { + match ::from_str(#lit_str) { + Ok(f) => f, + Err(_) => panic!(concat!("Invalid Felt decimal string literal: ", #lit_str)), + } + } + .into() + } + } + + Lit::Bool(lit_bool) => quote! { + match #lit_bool { + true => Felt::ONE, + false => Felt::ZERO, + } + } + .into(), + + Lit::Int(lit_int) => quote! { + Felt::from(#lit_int) + } + .into(), + + _ => panic!("Unsupported literal type for felt! macro"), + }, + + // Handle negative integer literals: -42, -123, etc. + Expr::Unary(expr_unary) if matches!(expr_unary.op, syn::UnOp::Neg(_)) => { + if let Expr::Lit(syn::ExprLit { + lit: Lit::Int(lit_int), + .. + }) = &*expr_unary.expr + { + // Negative integer literal + quote! { + Felt::from(-#lit_int) + } + .into() + } else { + // Some other unary negation, treat as expression + quote! { + match ::from_str(&#expr) { + Ok(f) => f, + Err(_) => panic!("Invalid Felt value"), + } + } + .into() + } + } + + // Anything else is handled as a string and will fail if it is not one + _ => quote! { + match Felt::try_from(#expr) { + Ok(f) => f, + Err(_) => panic!("Invalid Felt value"), + } + } + .into(), + } +} diff --git a/crates/starknet-types-core/Cargo.toml b/crates/starknet-types-core/Cargo.toml index 1fb1294d..adde1aed 100644 --- a/crates/starknet-types-core/Cargo.toml +++ b/crates/starknet-types-core/Cargo.toml @@ -33,6 +33,9 @@ rand = { version = "0.9.2", default-features = false, optional = true } # TODO: in the future remove this dependency to allow upstream crates to use version 1.X.X of generic-array generic-array = { version = ">=0.14.0, <=0.14.7", default-features = false } +# macro +felt-macro = { path = "../felt-macro" } + [features] default = ["std", "serde", "curve", "num-traits"] std = [ diff --git a/crates/starknet-types-core/src/felt/mod.rs b/crates/starknet-types-core/src/felt/mod.rs index bb3e689c..cf3fd680 100644 --- a/crates/starknet-types-core/src/felt/mod.rs +++ b/crates/starknet-types-core/src/felt/mod.rs @@ -21,6 +21,27 @@ pub mod secret_felt; #[cfg(feature = "serde")] mod serde; +/// Handy macro to initialize `Felt` +// +/// Accepts: +/// - booleans +/// - positive and negative number literals (eg. `5`, `12u8`, `-77`, `-2007i32`) +/// - positive and negative decimal string literals (eg. `"5"`, `"-12"`) +/// - positive hexadecimal string literal (eg. `"0x0"`, `"0x42"`) +/// - variables of any type that implements `TryFrom for Felt` (eg. `u32`, `i128`, `&str`, `String`) +/// - functions and closure which return type implements `TryFrom for Felt` (eg. `|x| x * 42`, `fn ret42() -> u32 { 42 }` ) +/// - code block (eg. `{40 + 2}`) and more generaly any expression that returns as types that implements `TryFrom for Felt` +/// +/// Use in `const` expression is only possible using literal `bool` and literal hex string +/// because the other types rely on non-`const` function for conversion (eg. `From::from` for numbers). +#[macro_export] +macro_rules! felt { + ($($tt:tt)*) => {{ + let felt: Felt = felt_macro::felt!($($tt)*); + felt + }}; +} + use lambdaworks_math::errors::CreationError; pub use non_zero::{FeltIsZeroError, NonZeroFelt}; @@ -455,6 +476,23 @@ impl FromStr for Felt { } } +#[cfg(feature = "alloc")] +impl TryFrom for Felt { + type Error = FromStrError; + + fn try_from(value: alloc::string::String) -> Result { + Felt::from_str(&value) + } +} + +impl TryFrom<&str> for Felt { + type Error = FromStrError; + + fn try_from(value: &str) -> Result { + Felt::from_str(value) + } +} + impl Add<&Felt> for u64 { type Output = Option; @@ -1347,4 +1385,52 @@ mod test { ); assert!(Felt::from_hex(&format!("0x{}", "f".repeat(63))).is_err()); } + + #[test] + fn felt_macro() { + // Bools + assert_eq!(felt!(false), Felt::ZERO); + assert_eq!(felt!(true), Felt::ONE); + + // Primitive numbers + assert_eq!(felt!(42), Felt::from(42)); + assert_eq!(felt!(42u8), Felt::from(42)); + assert_eq!(felt!(42i8), Felt::from(42)); + assert_eq!(felt!(42u128), Felt::from(42)); + assert_eq!(felt!(-42), Felt::ZERO - Felt::from(42)); + assert_eq!(felt!(-42i8), Felt::ZERO - Felt::from(42)); + + // Statis &str + assert_eq!(felt!("42"), Felt::from(42)); + assert_eq!(felt!("-42"), Felt::ZERO - Felt::from(42)); + assert_eq!(felt!("0x42"), Felt::from_hex_unwrap("0x42")); + + // Variables + let x = true; + assert_eq!(felt!(x), Felt::ONE); + let x = "42"; + assert_eq!(felt!(x), Felt::from(42)); + let x = String::from("42"); + assert_eq!(felt!(x), Felt::from(42)); + let x = 42u32; + assert_eq!(felt!(x), Felt::from(42)); + let x = -42; + assert_eq!(felt!(x), Felt::ZERO - Felt::from(42)); + + // Expresions + let double_closure = |x| x * 2; + assert_eq!(felt!(double_closure(5)), Felt::from(10)); + assert_eq!(felt!({ 40 + 2 }), Felt::from(42)); + + // Constants + const X: &str = "42"; + assert_eq!(felt!(X), Felt::from(42)); + const Y: u32 = 42; + assert_eq!(felt!(Y), Felt::from(42)); + + // Use in const expressions + const _: Felt = felt!("0x42"); + const _: Felt = felt!(true); + const _: Felt = felt!(false); + } } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index c2cd5f89..62bcfba9 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.87.0" +channel = "1.89.0" components = ["rustfmt", "clippy", "rust-analyzer"] profile = "minimal" From ae50e10c9bed14467b1ac599a08e547467606aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Wed, 5 Nov 2025 12:44:18 -0300 Subject: [PATCH 02/10] chore: move macro to own file add typo check to CI --- .github/workflows/typo.yml | 14 ++++ .../src/felt/macro_impl.rs | 73 +++++++++++++++++++ crates/starknet-types-core/src/felt/mod.rs | 70 +----------------- 3 files changed, 88 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/typo.yml create mode 100644 crates/starknet-types-core/src/felt/macro_impl.rs diff --git a/.github/workflows/typo.yml b/.github/workflows/typo.yml new file mode 100644 index 00000000..efa57519 --- /dev/null +++ b/.github/workflows/typo.yml @@ -0,0 +1,14 @@ +name: Typo Check + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + typos: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: crate-ci/typos@master diff --git a/crates/starknet-types-core/src/felt/macro_impl.rs b/crates/starknet-types-core/src/felt/macro_impl.rs new file mode 100644 index 00000000..b44001c1 --- /dev/null +++ b/crates/starknet-types-core/src/felt/macro_impl.rs @@ -0,0 +1,73 @@ +/// Handy macro to initialize `Felt` +// +/// Accepts: +/// - booleans +/// - positive and negative number literals (eg. `5`, `12u8`, `-77`, `-2007i32`) +/// - positive and negative decimal string literals (eg. `"5"`, `"-12"`) +/// - positive hexadecimal string literal (eg. `"0x0"`, `"0x42"`) +/// - variables of any type that implements `TryFrom for Felt` (eg. `u32`, `i128`, `&str`, `String`) +/// - functions and closure which return type implements `TryFrom for Felt` (eg. `|x| x * 42`, `fn ret42() -> u32 { 42 }` ) +/// - code block (eg. `{40 + 2}`) and more generaly any expression that returns as types that implements `TryFrom for Felt` +/// +/// Use in `const` expression is only possible using literal `bool` and literal hex string +/// because the other types rely on non-`const` function for conversion (eg. `From::from` for numbers). +#[macro_export] +macro_rules! felt { + ($($tt:tt)*) => {{ + let felt: $crate::felt::Felt = felt_macro::felt!($($tt)*); + felt + }}; +} + +#[cfg(test)] +mod tests { + use crate::felt::Felt; + + #[test] + fn felt_macro() { + // Bools + assert_eq!(felt!(false), Felt::ZERO); + assert_eq!(felt!(true), Felt::ONE); + + // Primitive numbers + assert_eq!(felt!(42), Felt::from(42)); + assert_eq!(felt!(42u8), Felt::from(42)); + assert_eq!(felt!(42i8), Felt::from(42)); + assert_eq!(felt!(42u128), Felt::from(42)); + assert_eq!(felt!(-42), Felt::ZERO - Felt::from(42)); + assert_eq!(felt!(-42i8), Felt::ZERO - Felt::from(42)); + + // Statis &str + assert_eq!(felt!("42"), Felt::from(42)); + assert_eq!(felt!("-42"), Felt::ZERO - Felt::from(42)); + assert_eq!(felt!("0x42"), Felt::from_hex_unwrap("0x42")); + + // Variables + let x = true; + assert_eq!(felt!(x), Felt::ONE); + let x = "42"; + assert_eq!(felt!(x), Felt::from(42)); + let x = String::from("42"); + assert_eq!(felt!(x), Felt::from(42)); + let x = 42u32; + assert_eq!(felt!(x), Felt::from(42)); + let x = -42; + assert_eq!(felt!(x), Felt::ZERO - Felt::from(42)); + + // Expresions + let double_closure = |x| x * 2; + assert_eq!(felt!(double_closure(5)), Felt::from(10)); + assert_eq!(felt!({ 40 + 2 }), Felt::from(42)); + + // Constants + const X: &str = "42"; + assert_eq!(felt!(X), Felt::from(42)); + const Y: u32 = 42; + assert_eq!(felt!(Y), Felt::from(42)); + + // Use in const expressions + const _: Felt = felt!("0x42"); + const _: Felt = felt!(true); + const _: Felt = felt!(false); + } +} diff --git a/crates/starknet-types-core/src/felt/mod.rs b/crates/starknet-types-core/src/felt/mod.rs index cf3fd680..115f3347 100644 --- a/crates/starknet-types-core/src/felt/mod.rs +++ b/crates/starknet-types-core/src/felt/mod.rs @@ -8,6 +8,7 @@ mod apollo_serialization; mod arbitrary; #[cfg(test)] mod felt_arbitrary; +mod macro_impl; mod non_zero; #[cfg(feature = "num-traits")] mod num_traits_impl; @@ -21,27 +22,6 @@ pub mod secret_felt; #[cfg(feature = "serde")] mod serde; -/// Handy macro to initialize `Felt` -// -/// Accepts: -/// - booleans -/// - positive and negative number literals (eg. `5`, `12u8`, `-77`, `-2007i32`) -/// - positive and negative decimal string literals (eg. `"5"`, `"-12"`) -/// - positive hexadecimal string literal (eg. `"0x0"`, `"0x42"`) -/// - variables of any type that implements `TryFrom for Felt` (eg. `u32`, `i128`, `&str`, `String`) -/// - functions and closure which return type implements `TryFrom for Felt` (eg. `|x| x * 42`, `fn ret42() -> u32 { 42 }` ) -/// - code block (eg. `{40 + 2}`) and more generaly any expression that returns as types that implements `TryFrom for Felt` -/// -/// Use in `const` expression is only possible using literal `bool` and literal hex string -/// because the other types rely on non-`const` function for conversion (eg. `From::from` for numbers). -#[macro_export] -macro_rules! felt { - ($($tt:tt)*) => {{ - let felt: Felt = felt_macro::felt!($($tt)*); - felt - }}; -} - use lambdaworks_math::errors::CreationError; pub use non_zero::{FeltIsZeroError, NonZeroFelt}; @@ -1385,52 +1365,4 @@ mod test { ); assert!(Felt::from_hex(&format!("0x{}", "f".repeat(63))).is_err()); } - - #[test] - fn felt_macro() { - // Bools - assert_eq!(felt!(false), Felt::ZERO); - assert_eq!(felt!(true), Felt::ONE); - - // Primitive numbers - assert_eq!(felt!(42), Felt::from(42)); - assert_eq!(felt!(42u8), Felt::from(42)); - assert_eq!(felt!(42i8), Felt::from(42)); - assert_eq!(felt!(42u128), Felt::from(42)); - assert_eq!(felt!(-42), Felt::ZERO - Felt::from(42)); - assert_eq!(felt!(-42i8), Felt::ZERO - Felt::from(42)); - - // Statis &str - assert_eq!(felt!("42"), Felt::from(42)); - assert_eq!(felt!("-42"), Felt::ZERO - Felt::from(42)); - assert_eq!(felt!("0x42"), Felt::from_hex_unwrap("0x42")); - - // Variables - let x = true; - assert_eq!(felt!(x), Felt::ONE); - let x = "42"; - assert_eq!(felt!(x), Felt::from(42)); - let x = String::from("42"); - assert_eq!(felt!(x), Felt::from(42)); - let x = 42u32; - assert_eq!(felt!(x), Felt::from(42)); - let x = -42; - assert_eq!(felt!(x), Felt::ZERO - Felt::from(42)); - - // Expresions - let double_closure = |x| x * 2; - assert_eq!(felt!(double_closure(5)), Felt::from(10)); - assert_eq!(felt!({ 40 + 2 }), Felt::from(42)); - - // Constants - const X: &str = "42"; - assert_eq!(felt!(X), Felt::from(42)); - const Y: u32 = 42; - assert_eq!(felt!(Y), Felt::from(42)); - - // Use in const expressions - const _: Felt = felt!("0x42"); - const _: Felt = felt!(true); - const _: Felt = felt!(false); - } } From 9465192b806376cac976106ce7c42b30f80dad8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Wed, 5 Nov 2025 12:48:06 -0300 Subject: [PATCH 03/10] typos --- crates/starknet-types-core/src/felt/alloc_impls.rs | 2 +- crates/starknet-types-core/src/felt/macro_impl.rs | 6 +++--- crates/starknet-types-core/src/felt/mod.rs | 10 +++++----- .../starknet-types-core/src/felt/parity_scale_codec.rs | 2 +- crates/starknet-types-core/src/short_string/mod.rs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/starknet-types-core/src/felt/alloc_impls.rs b/crates/starknet-types-core/src/felt/alloc_impls.rs index 8394de30..03ee300d 100644 --- a/crates/starknet-types-core/src/felt/alloc_impls.rs +++ b/crates/starknet-types-core/src/felt/alloc_impls.rs @@ -18,7 +18,7 @@ impl Felt { /// 2. an amount of padding zeros so that the resulting string length is fixed (This amount may be 0), /// 3. the felt value represented in hexadecimal /// - /// The resulting string is guaranted to be 66 chars long, which is enough to represent `Felt::MAX`: + /// The resulting string is guaranteed to be 66 chars long, which is enough to represent `Felt::MAX`: /// 2 chars for the `0x` prefix and 64 chars for the padded hexadecimal felt value. pub fn to_fixed_hex_string(&self) -> alloc::string::String { alloc::format!("{self:#066x}") diff --git a/crates/starknet-types-core/src/felt/macro_impl.rs b/crates/starknet-types-core/src/felt/macro_impl.rs index b44001c1..245265a0 100644 --- a/crates/starknet-types-core/src/felt/macro_impl.rs +++ b/crates/starknet-types-core/src/felt/macro_impl.rs @@ -7,7 +7,7 @@ /// - positive hexadecimal string literal (eg. `"0x0"`, `"0x42"`) /// - variables of any type that implements `TryFrom for Felt` (eg. `u32`, `i128`, `&str`, `String`) /// - functions and closure which return type implements `TryFrom for Felt` (eg. `|x| x * 42`, `fn ret42() -> u32 { 42 }` ) -/// - code block (eg. `{40 + 2}`) and more generaly any expression that returns as types that implements `TryFrom for Felt` +/// - code block (eg. `{40 + 2}`) and more generally any expression that returns as types that implements `TryFrom for Felt` /// /// Use in `const` expression is only possible using literal `bool` and literal hex string /// because the other types rely on non-`const` function for conversion (eg. `From::from` for numbers). @@ -37,7 +37,7 @@ mod tests { assert_eq!(felt!(-42), Felt::ZERO - Felt::from(42)); assert_eq!(felt!(-42i8), Felt::ZERO - Felt::from(42)); - // Statis &str + // Static &str assert_eq!(felt!("42"), Felt::from(42)); assert_eq!(felt!("-42"), Felt::ZERO - Felt::from(42)); assert_eq!(felt!("0x42"), Felt::from_hex_unwrap("0x42")); @@ -54,7 +54,7 @@ mod tests { let x = -42; assert_eq!(felt!(x), Felt::ZERO - Felt::from(42)); - // Expresions + // Expressions let double_closure = |x| x * 2; assert_eq!(felt!(double_closure(5)), Felt::from(10)); assert_eq!(felt!({ 40 + 2 }), Felt::from(42)); diff --git a/crates/starknet-types-core/src/felt/mod.rs b/crates/starknet-types-core/src/felt/mod.rs index 115f3347..37674847 100644 --- a/crates/starknet-types-core/src/felt/mod.rs +++ b/crates/starknet-types-core/src/felt/mod.rs @@ -491,22 +491,22 @@ impl Add<&Felt> for u64 { [0, 0, 0, low] => self.checked_add(low), // Now we need to compare the 3 most significant digits. // There are two relevant cases from now on, either `rhs` behaves like a - // substraction of a `u64` or the result of the sum falls out of range. + // subtraction of a `u64` or the result of the sum falls out of range. // The 3 MSB only match the prime for Felt::max_value(), which is -1 - // in the signed field, so this is equivalent to substracting 1 to `self`. + // in the signed field, so this is equivalent to subtracting 1 to `self`. [hi @ .., _] if hi == PRIME_DIGITS_BE_HI => self.checked_sub(1), // For the remaining values between `[-u64::MAX..0]` (where `{0, -1}` have // already been covered) the MSB matches that of `PRIME - u64::MAX`. // Because we're in the negative number case, we count down. Because `0` // and `-1` correspond to different MSBs, `0` and `1` in the LSB are less - // than `-u64::MAX`, the smallest value we can add to (read, substract its + // than `-u64::MAX`, the smallest value we can add to (read, subtract its // magnitude from) a `u64` number, meaning we exclude them from the valid // case. // For the remaining range, we take the absolute value module-2 while - // correcting by substracting `1` (note we actually substract `2` because - // the absolute value itself requires substracting `1`. + // correcting by subtracting `1` (note we actually subtract `2` because + // the absolute value itself requires subtracting `1`. [hi @ .., low] if hi == PRIME_MINUS_U64_MAX_DIGITS_BE_HI && low >= 2 => { (self).checked_sub(u64::MAX - (low - 2)) } diff --git a/crates/starknet-types-core/src/felt/parity_scale_codec.rs b/crates/starknet-types-core/src/felt/parity_scale_codec.rs index 2cbfba98..55ddfe25 100644 --- a/crates/starknet-types-core/src/felt/parity_scale_codec.rs +++ b/crates/starknet-types-core/src/felt/parity_scale_codec.rs @@ -26,7 +26,7 @@ mod tests { fn parity_scale_codec_serialization() { use parity_scale_codec::{Decode, Encode}; - // use an endianness-asymetric number to test that byte order is correct in serialization + // use an endianness-asymmetric number to test that byte order is correct in serialization let initial_felt = Felt::from_hex("0xabcdef123").unwrap(); // serialize the felt diff --git a/crates/starknet-types-core/src/short_string/mod.rs b/crates/starknet-types-core/src/short_string/mod.rs index 5e2e4470..e899835f 100644 --- a/crates/starknet-types-core/src/short_string/mod.rs +++ b/crates/starknet-types-core/src/short_string/mod.rs @@ -7,7 +7,7 @@ //! A `ShortString` is string that have been checked and is guaranteed to be convertible into a valid `Felt`. //! It checks that the `String` only contains ascii characters and is no longer than 31 characters. //! -//! The convesion to `Felt` is done by using the internal ascii short string as bytes and parse those as a big endian number. +//! The conversion to `Felt` is done by using the internal ascii short string as bytes and parse those as a big endian number. use crate::felt::Felt; use core::str::FromStr; From fa8c332b7bf249ab87eed7ea4cbfc6a3307c9ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Wed, 5 Nov 2025 12:52:41 -0300 Subject: [PATCH 04/10] extern crate alloc import in test --- crates/starknet-types-core/src/felt/macro_impl.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/starknet-types-core/src/felt/macro_impl.rs b/crates/starknet-types-core/src/felt/macro_impl.rs index 245265a0..943e1f75 100644 --- a/crates/starknet-types-core/src/felt/macro_impl.rs +++ b/crates/starknet-types-core/src/felt/macro_impl.rs @@ -21,6 +21,9 @@ macro_rules! felt { #[cfg(test)] mod tests { + #[cfg(feature = "alloc")] + pub extern crate alloc; + use crate::felt::Felt; #[test] @@ -47,7 +50,7 @@ mod tests { assert_eq!(felt!(x), Felt::ONE); let x = "42"; assert_eq!(felt!(x), Felt::from(42)); - let x = String::from("42"); + let x = alloc::string::String::from("42"); assert_eq!(felt!(x), Felt::from(42)); let x = 42u32; assert_eq!(felt!(x), Felt::from(42)); From ef5bf3a08b1b291826cd952f70919be2f10e5518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Thu, 6 Nov 2025 16:48:09 -0300 Subject: [PATCH 05/10] unsigned int const expressions OK --- crates/felt-macro/Cargo.toml | 1 + crates/felt-macro/src/lib.rs | 27 +++++++++++++++++-- .../src/felt/macro_impl.rs | 3 ++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/crates/felt-macro/Cargo.toml b/crates/felt-macro/Cargo.toml index 892e132c..4112c3c0 100644 --- a/crates/felt-macro/Cargo.toml +++ b/crates/felt-macro/Cargo.toml @@ -10,3 +10,4 @@ proc-macro = true syn = { version = "2.0", features = ["full"] } quote = "1.0" proc-macro2 = "1.0" +lambdaworks-math = { version = "0.13.0", default-features = false } diff --git a/crates/felt-macro/src/lib.rs b/crates/felt-macro/src/lib.rs index d406b807..1a5506a4 100644 --- a/crates/felt-macro/src/lib.rs +++ b/crates/felt-macro/src/lib.rs @@ -2,6 +2,15 @@ use proc_macro::TokenStream; use quote::quote; use syn::{Expr, Lit, parse_macro_input}; +use lambdaworks_math::{ + field::{ + element::FieldElement, fields::fft_friendly::stark_252_prime_field::Stark252PrimeField, + }, + unsigned_integer::element::UnsignedInteger, +}; + +type LambdaFieldElement = FieldElement; + #[proc_macro] pub fn felt(input: TokenStream) -> TokenStream { let expr = parse_macro_input!(input as Expr); @@ -57,8 +66,22 @@ pub fn felt(input: TokenStream) -> TokenStream { } .into(), - Lit::Int(lit_int) => quote! { - Felt::from(#lit_int) + Lit::Int(lit_int) => { + let value = (lit_int).base10_parse::().unwrap(); + let fe: LambdaFieldElement = + LambdaFieldElement::from(&UnsignedInteger::from(value)); + let limbs = fe.to_raw().limbs; + let r0 = limbs[0]; + let r1 = limbs[1]; + let r2 = limbs[2]; + let r3 = limbs[3]; + + quote! { + { + const __FELT_VALUE: Felt = Felt::from_raw([#r0, #r1, #r2, #r3]); + __FELT_VALUE + } + } } .into(), diff --git a/crates/starknet-types-core/src/felt/macro_impl.rs b/crates/starknet-types-core/src/felt/macro_impl.rs index 943e1f75..b27821f9 100644 --- a/crates/starknet-types-core/src/felt/macro_impl.rs +++ b/crates/starknet-types-core/src/felt/macro_impl.rs @@ -70,7 +70,8 @@ mod tests { // Use in const expressions const _: Felt = felt!("0x42"); + const _: Felt = felt!(42); + const _: Felt = felt!(42i32); const _: Felt = felt!(true); - const _: Felt = felt!(false); } } From 480552954ec23703d9097efb111622ebc345c538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Thu, 6 Nov 2025 19:37:04 -0300 Subject: [PATCH 06/10] more types handled --- crates/felt-macro/src/lib.rs | 220 +++++++++++------- .../src/felt/macro_impl.rs | 69 +++++- 2 files changed, 196 insertions(+), 93 deletions(-) diff --git a/crates/felt-macro/src/lib.rs b/crates/felt-macro/src/lib.rs index 1a5506a4..30518840 100644 --- a/crates/felt-macro/src/lib.rs +++ b/crates/felt-macro/src/lib.rs @@ -1,124 +1,164 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{Expr, Lit, parse_macro_input}; +use std::ops::Neg; +use syn::{Expr, ExprLit, ExprUnary, Lit, parse_macro_input}; use lambdaworks_math::{ field::{ element::FieldElement, fields::fft_friendly::stark_252_prime_field::Stark252PrimeField, }, + traits::ByteConversion, unsigned_integer::element::UnsignedInteger, }; type LambdaFieldElement = FieldElement; +enum HandleExprOutput { + ComptimeFelt(LambdaFieldElement), + Runtime, +} + #[proc_macro] pub fn felt(input: TokenStream) -> TokenStream { let expr = parse_macro_input!(input as Expr); - match &expr { + match handle_expr(&expr) { + HandleExprOutput::ComptimeFelt(field_element) => { + generate_const_felt_token_stream_from_lambda_field_element(field_element).into() + } + HandleExprOutput::Runtime => quote! { + match Felt::try_from(#expr) { + Ok(f) => f, + Err(_) => panic!("Invalid Felt value"), + } + } + .into(), + } +} + +/// Take the lambda class type for field element, extract its limbs and generate the token stream for a const value of itself +fn generate_const_felt_token_stream_from_lambda_field_element( + value: LambdaFieldElement, +) -> proc_macro2::TokenStream { + let limbs = value.to_raw().limbs; + let r0 = limbs[0]; + let r1 = limbs[1]; + let r2 = limbs[2]; + let r3 = limbs[3]; + + quote! { + { + const __FELT_VALUE: Felt = Felt::from_raw([#r0, #r1, #r2, #r3]); + __FELT_VALUE + } + } +} + +fn handle_expr(expr: &syn::Expr) -> HandleExprOutput { + match expr { Expr::Lit(expr_lit) => match &expr_lit.lit { + Lit::Bool(lit_bool) => HandleExprOutput::ComptimeFelt(match lit_bool.value() { + false => LambdaFieldElement::from_hex_unchecked("0x0"), + true => LambdaFieldElement::from_hex_unchecked("0x1"), + }), + + Lit::Int(lit_int) => { + let value = lit_int.base10_parse::().unwrap(); + + HandleExprOutput::ComptimeFelt(LambdaFieldElement::from(&UnsignedInteger::from( + value, + ))) + } + Lit::Str(lit_str) => { let value = lit_str.value(); - // Check if it's a hex string (starts with 0x or 0X) - if value.starts_with("0x") || value.starts_with("0X") { - // Hex string: use const fn for compile-time validation - quote! { - { - const __FELT_VALUE: Felt = Felt::from_hex_unwrap(#lit_str); - __FELT_VALUE - } - } - .into() + let (is_neg, value) = if let Some(striped) = value.strip_prefix('-') { + (true, striped) + } else { + (false, value.as_str()) + }; + + let lfe = if value.starts_with("0x") || value.starts_with("0X") { + UnsignedInteger::from_hex(value).map(|x| LambdaFieldElement::from(&x)) } else { - // Check for valid decimal format (optional leading minus, then digits) - let is_valid = if let Some(stripped) = value.strip_prefix('-') { - !stripped.is_empty() && stripped.chars().all(|c| c.is_ascii_digit()) - } else { - !value.is_empty() && value.chars().all(|c| c.is_ascii_digit()) - }; - - if !is_valid { - return syn::Error::new_spanned( - lit_str, - format!("Invalid Felt decimal string literal: '{}'. Expected decimal digits (0-9), optionally prefixed with '-'.", value) - ) - .to_compile_error() - .into(); - } - - // Valid format, generate runtime parsing code - quote! { - match ::from_str(#lit_str) { - Ok(f) => f, - Err(_) => panic!(concat!("Invalid Felt decimal string literal: ", #lit_str)), - } - } - .into() + UnsignedInteger::from_dec_str(value).map(|x| LambdaFieldElement::from(&x)) } + .unwrap(); + + HandleExprOutput::ComptimeFelt(if is_neg { lfe.neg() } else { lfe }) } - Lit::Bool(lit_bool) => quote! { - match #lit_bool { - true => Felt::ONE, - false => Felt::ZERO, - } + Lit::ByteStr(lit_byte_str) => { + let bytes = lit_byte_str.value(); + + assert!( + bytes.len() <= 31, + "Short string must be at most 31 characters" + ); + assert!( + bytes.is_ascii(), + "Short string must contain only ASCII characters" + ); + + let mut buffer = [0u8; 32]; + buffer[(32 - bytes.len())..].copy_from_slice(&bytes); + + HandleExprOutput::ComptimeFelt(LambdaFieldElement::from_bytes_be(&buffer).unwrap()) } - .into(), + Lit::Char(lit_char) => { + let char = lit_char.value(); - Lit::Int(lit_int) => { - let value = (lit_int).base10_parse::().unwrap(); - let fe: LambdaFieldElement = - LambdaFieldElement::from(&UnsignedInteger::from(value)); - let limbs = fe.to_raw().limbs; - let r0 = limbs[0]; - let r1 = limbs[1]; - let r2 = limbs[2]; - let r3 = limbs[3]; - - quote! { - { - const __FELT_VALUE: Felt = Felt::from_raw([#r0, #r1, #r2, #r3]); - __FELT_VALUE - } - } + assert!(char.is_ascii(), "Only ASCII characters are handled"); + + let mut buffer = [0u8]; + char.encode_utf8(&mut buffer); + + HandleExprOutput::ComptimeFelt(LambdaFieldElement::from(&UnsignedInteger::from( + u16::from(buffer[0]), + ))) } - .into(), + Lit::Byte(lit_byte) => { + let char = lit_byte.value(); - _ => panic!("Unsupported literal type for felt! macro"), + HandleExprOutput::ComptimeFelt(LambdaFieldElement::from(&UnsignedInteger::from( + u16::from(char), + ))) + } + Lit::CStr(_) | Lit::Float(_) | Lit::Verbatim(_) => panic!("Literal type not handled"), + // `Lit` is a non-exhaustive enum + _ => panic!("Unkown literal type. Not handled"), }, - // Handle negative integer literals: -42, -123, etc. - Expr::Unary(expr_unary) if matches!(expr_unary.op, syn::UnOp::Neg(_)) => { - if let Expr::Lit(syn::ExprLit { - lit: Lit::Int(lit_int), - .. - }) = &*expr_unary.expr - { - // Negative integer literal - quote! { - Felt::from(-#lit_int) - } - .into() - } else { - // Some other unary negation, treat as expression - quote! { - match ::from_str(&#expr) { - Ok(f) => f, - Err(_) => panic!("Invalid Felt value"), - } - } - .into() + // Negative (`-`) prefixed values + Expr::Unary(ExprUnary { + attrs: _attrs, + op: syn::UnOp::Neg(_), + expr, + }) => match handle_expr(expr) { + HandleExprOutput::ComptimeFelt(field_element) => { + HandleExprOutput::ComptimeFelt(field_element.neg()) } - } + HandleExprOutput::Runtime => HandleExprOutput::Runtime, + }, + Expr::Unary(ExprUnary { + attrs: _attrs, + op: syn::UnOp::Not(_), + expr, + }) => match &**expr { + Expr::Lit(ExprLit { + lit: Lit::Bool(lit_bool), + .. + }) => HandleExprOutput::ComptimeFelt(match lit_bool.value() { + false => LambdaFieldElement::from_hex_unchecked("0x1"), + true => LambdaFieldElement::from_hex_unchecked("0x0"), + }), + Expr::Lit(_) => panic!( + "The `!` logical inversion operatior in only allowed before booleans in literal expressions." + ), + _ => HandleExprOutput::Runtime, + }, - // Anything else is handled as a string and will fail if it is not one - _ => quote! { - match Felt::try_from(#expr) { - Ok(f) => f, - Err(_) => panic!("Invalid Felt value"), - } - } - .into(), + _ => HandleExprOutput::Runtime, } } diff --git a/crates/starknet-types-core/src/felt/macro_impl.rs b/crates/starknet-types-core/src/felt/macro_impl.rs index b27821f9..f65aca14 100644 --- a/crates/starknet-types-core/src/felt/macro_impl.rs +++ b/crates/starknet-types-core/src/felt/macro_impl.rs @@ -4,7 +4,8 @@ /// - booleans /// - positive and negative number literals (eg. `5`, `12u8`, `-77`, `-2007i32`) /// - positive and negative decimal string literals (eg. `"5"`, `"-12"`) -/// - positive hexadecimal string literal (eg. `"0x0"`, `"0x42"`) +/// - positive and negative hexadecimal string literal (eg. `"0x0"`, `"0x42"`) +/// - bytes strings, chars, and bytes, all handled as cairo short strings (eg. `'A'`, `b'A'`, `"AAA"`) /// - variables of any type that implements `TryFrom for Felt` (eg. `u32`, `i128`, `&str`, `String`) /// - functions and closure which return type implements `TryFrom for Felt` (eg. `|x| x * 42`, `fn ret42() -> u32 { 42 }` ) /// - code block (eg. `{40 + 2}`) and more generally any expression that returns as types that implements `TryFrom for Felt` @@ -24,13 +25,23 @@ mod tests { #[cfg(feature = "alloc")] pub extern crate alloc; + use core::ops::Neg; + use crate::felt::Felt; #[test] + #[allow(double_negations)] + #[allow(clippy::nonminimal_bool)] fn felt_macro() { // Bools assert_eq!(felt!(false), Felt::ZERO); assert_eq!(felt!(true), Felt::ONE); + assert_eq!(felt!(-true), Felt::from(-1)); + assert_eq!(felt!(!true), Felt::ZERO); + assert_eq!(felt!(!!true), Felt::ONE); + assert_eq!(felt!(!!!true), Felt::ZERO); + assert_eq!(felt!(false | true), Felt::ONE); + assert_eq!(felt!(true & false), Felt::ZERO); // Primitive numbers assert_eq!(felt!(42), Felt::from(42)); @@ -39,15 +50,53 @@ mod tests { assert_eq!(felt!(42u128), Felt::from(42)); assert_eq!(felt!(-42), Felt::ZERO - Felt::from(42)); assert_eq!(felt!(-42i8), Felt::ZERO - Felt::from(42)); + assert_eq!(felt!(-0x42), Felt::ZERO - Felt::from(0x42)); + assert_eq!(felt!(0b1010), Felt::from(0b1010)); + assert_eq!(felt!(0o777), Felt::from(0o777)); // Static &str assert_eq!(felt!("42"), Felt::from(42)); assert_eq!(felt!("-42"), Felt::ZERO - Felt::from(42)); assert_eq!(felt!("0x42"), Felt::from_hex_unwrap("0x42")); + assert_eq!(felt!("-0x42"), Felt::ZERO - Felt::from_hex_unwrap("0x42")); + + // Negated literals + assert_eq!(felt!(-"42"), Felt::ZERO - Felt::from(42)); + assert_eq!(felt!(-"-42"), Felt::from(42)); + assert_eq!(felt!(-true), Felt::from(-1)); + + // Byte string (handles as cairo short strings) + assert_eq!( + felt!(b"SN_MAIN"), + Felt::from_raw([ + 502562008147966918, + 18446744073709551615, + 18446744073709551615, + 17696389056366564951, + ]) + ); + assert_eq!( + felt!(b"SN_SEPOLIA"), + Felt::from_raw([ + 507980251676163170, + 18446744073709551615, + 18446744073708869172, + 1555806712078248243, + ]) + ); + assert_eq!(felt!(b"aa"), Felt::from_hex_unwrap("0x6161")); + assert_eq!(felt!(-b"ab"), Felt::from_hex_unwrap("0x6162").neg()); + + // ASCII chars and bytes, handles as 1 char long cairo short strings + assert_eq!(felt!('0'), Felt::from(b'0')); + assert_eq!(felt!('A'), felt!(b"A")); + assert_eq!(felt!('A'), felt!(b'A')); + assert_eq!(felt!(-'A'), Felt::from(-65)); // Variables let x = true; assert_eq!(felt!(x), Felt::ONE); + assert_eq!(felt!(!x), Felt::ZERO); let x = "42"; assert_eq!(felt!(x), Felt::from(42)); let x = alloc::string::String::from("42"); @@ -55,12 +104,17 @@ mod tests { let x = 42u32; assert_eq!(felt!(x), Felt::from(42)); let x = -42; - assert_eq!(felt!(x), Felt::ZERO - Felt::from(42)); + assert_eq!(felt!(x), Felt::from(-42)); + assert_eq!(felt!(-x), Felt::from(42)); + assert_eq!(felt!(--x), Felt::from(-42)); // Expressions let double_closure = |x| x * 2; assert_eq!(felt!(double_closure(5)), Felt::from(10)); - assert_eq!(felt!({ 40 + 2 }), Felt::from(42)); + assert_eq!(felt!(40 + 2), Felt::from(42)); + assert_eq!(felt!(-{ 40 + 2 }), Felt::from(-42)); + let x = 42; + assert_eq!(felt!({ (40 + 2 == x) | true }), Felt::ONE); // Constants const X: &str = "42"; @@ -70,8 +124,17 @@ mod tests { // Use in const expressions const _: Felt = felt!("0x42"); + const _: Felt = felt!("-0x42"); + const _: Felt = felt!("67"); + const _: Felt = felt!(-"-69"); const _: Felt = felt!(42); + const _: Felt = felt!(-42i16); + const _: Felt = felt!(--0x2i128); + const _: Felt = felt!(-0b101010); + const _: Felt = felt!(42i32); const _: Felt = felt!(42i32); const _: Felt = felt!(true); + const _: Felt = felt!(!false); + const _: Felt = felt!(-true); } } From f71eec8adb66165df2b292f0307339f3b466890e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Thu, 6 Nov 2025 19:46:50 -0300 Subject: [PATCH 07/10] spaned errors --- crates/felt-macro/src/lib.rs | 135 ++++++++++++------ .../src/felt/macro_impl.rs | 4 + 2 files changed, 96 insertions(+), 43 deletions(-) diff --git a/crates/felt-macro/src/lib.rs b/crates/felt-macro/src/lib.rs index 30518840..6b26fd66 100644 --- a/crates/felt-macro/src/lib.rs +++ b/crates/felt-macro/src/lib.rs @@ -1,7 +1,7 @@ use proc_macro::TokenStream; use quote::quote; use std::ops::Neg; -use syn::{Expr, ExprLit, ExprUnary, Lit, parse_macro_input}; +use syn::{Error, Expr, ExprLit, ExprUnary, Lit, Result, parse_macro_input}; use lambdaworks_math::{ field::{ @@ -23,16 +23,17 @@ pub fn felt(input: TokenStream) -> TokenStream { let expr = parse_macro_input!(input as Expr); match handle_expr(&expr) { - HandleExprOutput::ComptimeFelt(field_element) => { + Ok(HandleExprOutput::ComptimeFelt(field_element)) => { generate_const_felt_token_stream_from_lambda_field_element(field_element).into() } - HandleExprOutput::Runtime => quote! { + Ok(HandleExprOutput::Runtime) => quote! { match Felt::try_from(#expr) { Ok(f) => f, - Err(_) => panic!("Invalid Felt value"), + Err(e) => panic!("Invalid Felt value: {}", e), } } .into(), + Err(error) => error.to_compile_error().into(), } } @@ -54,19 +55,27 @@ fn generate_const_felt_token_stream_from_lambda_field_element( } } -fn handle_expr(expr: &syn::Expr) -> HandleExprOutput { +fn handle_expr(expr: &syn::Expr) -> Result { match expr { Expr::Lit(expr_lit) => match &expr_lit.lit { - Lit::Bool(lit_bool) => HandleExprOutput::ComptimeFelt(match lit_bool.value() { - false => LambdaFieldElement::from_hex_unchecked("0x0"), - true => LambdaFieldElement::from_hex_unchecked("0x1"), - }), + Lit::Bool(lit_bool) => Ok(HandleExprOutput::ComptimeFelt(match lit_bool.value() { + false => LambdaFieldElement::from(&UnsignedInteger::from_u64(0)), + true => LambdaFieldElement::from(&UnsignedInteger::from_u64(1)), + })), Lit::Int(lit_int) => { - let value = lit_int.base10_parse::().unwrap(); + let value = match lit_int.base10_parse::() { + Ok(v) => v, + Err(_) => { + return Err(Error::new_spanned( + lit_int, + "Invalid integer literal for Felt conversion", + )); + } + }; - HandleExprOutput::ComptimeFelt(LambdaFieldElement::from(&UnsignedInteger::from( - value, + Ok(HandleExprOutput::ComptimeFelt(LambdaFieldElement::from( + &UnsignedInteger::from(value), ))) } @@ -83,64 +92,103 @@ fn handle_expr(expr: &syn::Expr) -> HandleExprOutput { UnsignedInteger::from_hex(value).map(|x| LambdaFieldElement::from(&x)) } else { UnsignedInteger::from_dec_str(value).map(|x| LambdaFieldElement::from(&x)) - } - .unwrap(); + }; + + let lfe = match lfe { + Ok(v) => v, + Err(_) => { + return Err(Error::new_spanned( + lit_str, + "Invalid string literal for Felt conversion", + )); + } + }; - HandleExprOutput::ComptimeFelt(if is_neg { lfe.neg() } else { lfe }) + Ok(HandleExprOutput::ComptimeFelt(if is_neg { + lfe.neg() + } else { + lfe + })) } Lit::ByteStr(lit_byte_str) => { let bytes = lit_byte_str.value(); - assert!( - bytes.len() <= 31, - "Short string must be at most 31 characters" - ); - assert!( - bytes.is_ascii(), - "Short string must contain only ASCII characters" - ); + if bytes.len() > 31 { + return Err(Error::new_spanned( + lit_byte_str, + "Short string must be at most 31 characters", + )); + } + + if !bytes.is_ascii() { + return Err(Error::new_spanned( + lit_byte_str, + "Short string must contain only ASCII characters", + )); + } let mut buffer = [0u8; 32]; buffer[(32 - bytes.len())..].copy_from_slice(&bytes); - HandleExprOutput::ComptimeFelt(LambdaFieldElement::from_bytes_be(&buffer).unwrap()) + match LambdaFieldElement::from_bytes_be(&buffer) { + Ok(field_element) => Ok(HandleExprOutput::ComptimeFelt(field_element)), + Err(_) => Err(Error::new_spanned( + lit_byte_str, + "Failed to convert byte string to Felt", + )), + } } + Lit::Char(lit_char) => { let char = lit_char.value(); - assert!(char.is_ascii(), "Only ASCII characters are handled"); + if !char.is_ascii() { + return Err(Error::new_spanned( + lit_char, + "Only ASCII characters are supported", + )); + } let mut buffer = [0u8]; char.encode_utf8(&mut buffer); - HandleExprOutput::ComptimeFelt(LambdaFieldElement::from(&UnsignedInteger::from( - u16::from(buffer[0]), + Ok(HandleExprOutput::ComptimeFelt(LambdaFieldElement::from( + &UnsignedInteger::from(u16::from(buffer[0])), ))) } + Lit::Byte(lit_byte) => { let char = lit_byte.value(); - HandleExprOutput::ComptimeFelt(LambdaFieldElement::from(&UnsignedInteger::from( - u16::from(char), + Ok(HandleExprOutput::ComptimeFelt(LambdaFieldElement::from( + &UnsignedInteger::from(u16::from(char)), ))) } - Lit::CStr(_) | Lit::Float(_) | Lit::Verbatim(_) => panic!("Literal type not handled"), + + Lit::CStr(_) | Lit::Float(_) | Lit::Verbatim(_) => { + Err(Error::new_spanned(expr_lit, "Unsupported literal type")) + } + // `Lit` is a non-exhaustive enum - _ => panic!("Unkown literal type. Not handled"), + _ => Err(Error::new_spanned(expr_lit, "Unknown literal type")), }, // Negative (`-`) prefixed values + // Can be used before any other expression Expr::Unary(ExprUnary { attrs: _attrs, op: syn::UnOp::Neg(_), expr, - }) => match handle_expr(expr) { + }) => match handle_expr(expr)? { HandleExprOutput::ComptimeFelt(field_element) => { - HandleExprOutput::ComptimeFelt(field_element.neg()) + Ok(HandleExprOutput::ComptimeFelt(field_element.neg())) } - HandleExprOutput::Runtime => HandleExprOutput::Runtime, + HandleExprOutput::Runtime => Ok(HandleExprOutput::Runtime), }, + + // Opposite (`!`) prefixed values + // Can only be used before literal bool expression and any runtime expression where it is semanticaly valid Expr::Unary(ExprUnary { attrs: _attrs, op: syn::UnOp::Not(_), @@ -149,16 +197,17 @@ fn handle_expr(expr: &syn::Expr) -> HandleExprOutput { Expr::Lit(ExprLit { lit: Lit::Bool(lit_bool), .. - }) => HandleExprOutput::ComptimeFelt(match lit_bool.value() { - false => LambdaFieldElement::from_hex_unchecked("0x1"), - true => LambdaFieldElement::from_hex_unchecked("0x0"), - }), - Expr::Lit(_) => panic!( - "The `!` logical inversion operatior in only allowed before booleans in literal expressions." - ), - _ => HandleExprOutput::Runtime, + }) => Ok(HandleExprOutput::ComptimeFelt(match lit_bool.value() { + false => LambdaFieldElement::from(&UnsignedInteger::from_u64(1)), + true => LambdaFieldElement::from(&UnsignedInteger::from_u64(0)), + })), + Expr::Lit(_) => Err(Error::new_spanned( + expr, + "The `!` logical inversion operator is only allowed before booleans in literal expressions", + )), + _ => Ok(HandleExprOutput::Runtime), }, - _ => HandleExprOutput::Runtime, + _ => Ok(HandleExprOutput::Runtime), } } diff --git a/crates/starknet-types-core/src/felt/macro_impl.rs b/crates/starknet-types-core/src/felt/macro_impl.rs index f65aca14..57eb7826 100644 --- a/crates/starknet-types-core/src/felt/macro_impl.rs +++ b/crates/starknet-types-core/src/felt/macro_impl.rs @@ -115,6 +115,10 @@ mod tests { assert_eq!(felt!(-{ 40 + 2 }), Felt::from(-42)); let x = 42; assert_eq!(felt!({ (40 + 2 == x) | true }), Felt::ONE); + let x = "105".to_string(); + assert_eq!(felt!(x), Felt::from(105)); + let x = true; + assert_eq!(felt!(!x), Felt::from(0)); // Constants const X: &str = "42"; From 75ef55d82f297f9f3e579726b502d2e66fd92681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Thu, 6 Nov 2025 19:52:04 -0300 Subject: [PATCH 08/10] alloc in tests --- crates/starknet-types-core/src/felt/macro_impl.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/starknet-types-core/src/felt/macro_impl.rs b/crates/starknet-types-core/src/felt/macro_impl.rs index 57eb7826..0cf0a9ea 100644 --- a/crates/starknet-types-core/src/felt/macro_impl.rs +++ b/crates/starknet-types-core/src/felt/macro_impl.rs @@ -115,8 +115,14 @@ mod tests { assert_eq!(felt!(-{ 40 + 2 }), Felt::from(-42)); let x = 42; assert_eq!(felt!({ (40 + 2 == x) | true }), Felt::ONE); - let x = "105".to_string(); - assert_eq!(felt!(x), Felt::from(105)); + #[cfg(feature = "alloc")] + { + use alloc::string::String; + let x = String::from("105"); + assert_eq!(felt!(x), Felt::from(105)); + } + let x = "0x313"; + assert_eq!(felt!(x), Felt::from(0x313)); let x = true; assert_eq!(felt!(!x), Felt::from(0)); From 80a40e919f6261da878dafeb92c092dd39c43a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Mon, 10 Nov 2025 18:14:07 -0300 Subject: [PATCH 09/10] review --- crates/felt-macro/src/lib.rs | 93 +++++++++++-------- .../src/felt/macro_impl.rs | 18 ++-- rust-toolchain.toml | 2 +- 3 files changed, 63 insertions(+), 50 deletions(-) diff --git a/crates/felt-macro/src/lib.rs b/crates/felt-macro/src/lib.rs index 6b26fd66..ee83a68f 100644 --- a/crates/felt-macro/src/lib.rs +++ b/crates/felt-macro/src/lib.rs @@ -1,7 +1,7 @@ use proc_macro::TokenStream; use quote::quote; use std::ops::Neg; -use syn::{Error, Expr, ExprLit, ExprUnary, Lit, Result, parse_macro_input}; +use syn::{Error, Expr, ExprLit, ExprUnary, Lit, LitInt, Result, parse_macro_input}; use lambdaworks_math::{ field::{ @@ -63,21 +63,7 @@ fn handle_expr(expr: &syn::Expr) -> Result { true => LambdaFieldElement::from(&UnsignedInteger::from_u64(1)), })), - Lit::Int(lit_int) => { - let value = match lit_int.base10_parse::() { - Ok(v) => v, - Err(_) => { - return Err(Error::new_spanned( - lit_int, - "Invalid integer literal for Felt conversion", - )); - } - }; - - Ok(HandleExprOutput::ComptimeFelt(LambdaFieldElement::from( - &UnsignedInteger::from(value), - ))) - } + Lit::Int(lit_int) => handle_lit_int(lit_int), Lit::Str(lit_str) => { let value = lit_str.value(); @@ -174,26 +160,36 @@ fn handle_expr(expr: &syn::Expr) -> Result { _ => Err(Error::new_spanned(expr_lit, "Unknown literal type")), }, - // Negative (`-`) prefixed values - // Can be used before any other expression - Expr::Unary(ExprUnary { - attrs: _attrs, - op: syn::UnOp::Neg(_), - expr, - }) => match handle_expr(expr)? { - HandleExprOutput::ComptimeFelt(field_element) => { - Ok(HandleExprOutput::ComptimeFelt(field_element.neg())) - } - HandleExprOutput::Runtime => Ok(HandleExprOutput::Runtime), - }, + Expr::Unary(expr_unary) => handle_expr_unary(expr_unary), - // Opposite (`!`) prefixed values - // Can only be used before literal bool expression and any runtime expression where it is semanticaly valid - Expr::Unary(ExprUnary { - attrs: _attrs, - op: syn::UnOp::Not(_), - expr, - }) => match &**expr { + _ => Ok(HandleExprOutput::Runtime), + } +} + +fn handle_lit_int(lit_int: &LitInt) -> Result { + let value = match lit_int.base10_parse::() { + Ok(v) => v, + Err(_) => { + return Err(Error::new_spanned( + lit_int, + "Invalid integer literal for Felt conversion", + )); + } + }; + + Ok(HandleExprOutput::ComptimeFelt(LambdaFieldElement::from( + &UnsignedInteger::from(value), + ))) +} + +fn handle_expr_unary(expr_unary: &ExprUnary) -> Result { + let ExprUnary { + attrs: _attrs, + op, + expr, + } = expr_unary; + match op { + syn::UnOp::Not(_) => match &**expr { Expr::Lit(ExprLit { lit: Lit::Bool(lit_bool), .. @@ -201,13 +197,36 @@ fn handle_expr(expr: &syn::Expr) -> Result { false => LambdaFieldElement::from(&UnsignedInteger::from_u64(1)), true => LambdaFieldElement::from(&UnsignedInteger::from_u64(0)), })), + Expr::Lit(ExprLit { + lit: Lit::Int(_lit_int), + .. + }) => Err(Error::new_spanned( + expr, + "The `!` logical inversion operator is applicable to the `Felt` type", + )), Expr::Lit(_) => Err(Error::new_spanned( expr, "The `!` logical inversion operator is only allowed before booleans in literal expressions", )), _ => Ok(HandleExprOutput::Runtime), }, - - _ => Ok(HandleExprOutput::Runtime), + syn::UnOp::Neg(_) => match &**expr { + Expr::Lit(ExprLit { + attrs: _attrs, + lit: Lit::Int(lit_int), + }) => match handle_lit_int(lit_int)? { + HandleExprOutput::ComptimeFelt(field_element) => { + Ok(HandleExprOutput::ComptimeFelt(field_element.neg())) + } + HandleExprOutput::Runtime => Ok(HandleExprOutput::Runtime), + }, + Expr::Unary(expr_unary) => handle_expr_unary(expr_unary), + _ => Ok(HandleExprOutput::Runtime), + }, + syn::UnOp::Deref(_star) => Err(Error::new_spanned( + expr, + "Deref unary type `*` not supported", + )), + _ => Err(Error::new_spanned(expr, "Unknown unary type")), } } diff --git a/crates/starknet-types-core/src/felt/macro_impl.rs b/crates/starknet-types-core/src/felt/macro_impl.rs index 0cf0a9ea..66d6ae6a 100644 --- a/crates/starknet-types-core/src/felt/macro_impl.rs +++ b/crates/starknet-types-core/src/felt/macro_impl.rs @@ -25,8 +25,6 @@ mod tests { #[cfg(feature = "alloc")] pub extern crate alloc; - use core::ops::Neg; - use crate::felt::Felt; #[test] @@ -36,7 +34,6 @@ mod tests { // Bools assert_eq!(felt!(false), Felt::ZERO); assert_eq!(felt!(true), Felt::ONE); - assert_eq!(felt!(-true), Felt::from(-1)); assert_eq!(felt!(!true), Felt::ZERO); assert_eq!(felt!(!!true), Felt::ONE); assert_eq!(felt!(!!!true), Felt::ZERO); @@ -60,11 +57,6 @@ mod tests { assert_eq!(felt!("0x42"), Felt::from_hex_unwrap("0x42")); assert_eq!(felt!("-0x42"), Felt::ZERO - Felt::from_hex_unwrap("0x42")); - // Negated literals - assert_eq!(felt!(-"42"), Felt::ZERO - Felt::from(42)); - assert_eq!(felt!(-"-42"), Felt::from(42)); - assert_eq!(felt!(-true), Felt::from(-1)); - // Byte string (handles as cairo short strings) assert_eq!( felt!(b"SN_MAIN"), @@ -85,13 +77,11 @@ mod tests { ]) ); assert_eq!(felt!(b"aa"), Felt::from_hex_unwrap("0x6161")); - assert_eq!(felt!(-b"ab"), Felt::from_hex_unwrap("0x6162").neg()); // ASCII chars and bytes, handles as 1 char long cairo short strings assert_eq!(felt!('0'), Felt::from(b'0')); assert_eq!(felt!('A'), felt!(b"A")); assert_eq!(felt!('A'), felt!(b'A')); - assert_eq!(felt!(-'A'), Felt::from(-65)); // Variables let x = true; @@ -107,6 +97,12 @@ mod tests { assert_eq!(felt!(x), Felt::from(-42)); assert_eq!(felt!(-x), Felt::from(42)); assert_eq!(felt!(--x), Felt::from(-42)); + // Be careful there, the type of the variable changes the behaviour of the `!` operator, + // resulting in really different values + let x: i32 = 10; + assert_eq!(felt!(!x), Felt::MAX - Felt::from(10)); + let x: u32 = 10; + assert_eq!(felt!(!x), Felt::from(!10u32)); // Expressions let double_closure = |x| x * 2; @@ -136,7 +132,6 @@ mod tests { const _: Felt = felt!("0x42"); const _: Felt = felt!("-0x42"); const _: Felt = felt!("67"); - const _: Felt = felt!(-"-69"); const _: Felt = felt!(42); const _: Felt = felt!(-42i16); const _: Felt = felt!(--0x2i128); @@ -145,6 +140,5 @@ mod tests { const _: Felt = felt!(42i32); const _: Felt = felt!(true); const _: Felt = felt!(!false); - const _: Felt = felt!(-true); } } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 62bcfba9..c2cd5f89 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.89.0" +channel = "1.87.0" components = ["rustfmt", "clippy", "rust-analyzer"] profile = "minimal" From cc61bf39e5c2e375a80fca84e9d8e1aad467dd40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Mon, 10 Nov 2025 19:40:19 -0300 Subject: [PATCH 10/10] improve error message --- crates/felt-macro/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/felt-macro/src/lib.rs b/crates/felt-macro/src/lib.rs index ee83a68f..88f72932 100644 --- a/crates/felt-macro/src/lib.rs +++ b/crates/felt-macro/src/lib.rs @@ -198,11 +198,11 @@ fn handle_expr_unary(expr_unary: &ExprUnary) -> Result { true => LambdaFieldElement::from(&UnsignedInteger::from_u64(0)), })), Expr::Lit(ExprLit { - lit: Lit::Int(_lit_int), + lit: Lit::Int(_) | Lit::Byte(_), .. }) => Err(Error::new_spanned( expr, - "The `!` logical inversion operator is applicable to the `Felt` type", + "The `!` logical inversion operator is not applicable to the `Felt` type", )), Expr::Lit(_) => Err(Error::new_spanned( expr,