-
Notifications
You must be signed in to change notification settings - Fork 62
feat: add macro #179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: add macro #179
Conversation
add typo check to CI
| use syn::{Expr, Lit, parse_macro_input}; | ||
|
|
||
| #[proc_macro] | ||
| pub fn felt(input: TokenStream) -> TokenStream { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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
| assert_eq!(felt!(-"42"), Felt::ZERO - Felt::from(42)); | ||
| assert_eq!(felt!(-"-42"), Felt::from(42)); |
There was a problem hiding this comment.
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" | |||
There was a problem hiding this comment.
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?
Adding a
felt!macro that allow for easy initialization ofFeltfrom different types