Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/typo.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"crates/felt-macro",
"crates/starknet-types-core",
]

Expand Down
13 changes: 13 additions & 0 deletions crates/felt-macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[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"
lambdaworks-math = { version = "0.13.0", default-features = false }
213 changes: 213 additions & 0 deletions crates/felt-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
use proc_macro::TokenStream;
use quote::quote;
use std::ops::Neg;
use syn::{Error, Expr, ExprLit, ExprUnary, Lit, Result, 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<Stark252PrimeField>;

enum HandleExprOutput {
ComptimeFelt(LambdaFieldElement),
Runtime,
}

#[proc_macro]
pub fn felt(input: TokenStream) -> TokenStream {
let expr = parse_macro_input!(input as Expr);

match handle_expr(&expr) {
Ok(HandleExprOutput::ComptimeFelt(field_element)) => {
generate_const_felt_token_stream_from_lambda_field_element(field_element).into()
}
Ok(HandleExprOutput::Runtime) => quote! {
match Felt::try_from(#expr) {
Ok(f) => f,
Err(e) => panic!("Invalid Felt value: {}", e),
}
}
.into(),
Err(error) => error.to_compile_error().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) -> Result<HandleExprOutput> {
match expr {
Expr::Lit(expr_lit) => match &expr_lit.lit {
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 = match lit_int.base10_parse::<u128>() {
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::Str(lit_str) => {
let value = lit_str.value();

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 {
UnsignedInteger::from_dec_str(value).map(|x| LambdaFieldElement::from(&x))
};

let lfe = match lfe {
Ok(v) => v,
Err(_) => {
return Err(Error::new_spanned(
lit_str,
"Invalid string literal for Felt conversion",
));
}
};

Ok(HandleExprOutput::ComptimeFelt(if is_neg {
lfe.neg()
} else {
lfe
}))
}

Lit::ByteStr(lit_byte_str) => {
let bytes = lit_byte_str.value();

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);

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();

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);

Ok(HandleExprOutput::ComptimeFelt(LambdaFieldElement::from(
&UnsignedInteger::from(u16::from(buffer[0])),
)))
}

Lit::Byte(lit_byte) => {
let char = lit_byte.value();

Ok(HandleExprOutput::ComptimeFelt(LambdaFieldElement::from(
&UnsignedInteger::from(u16::from(char)),
)))
}

Lit::CStr(_) | Lit::Float(_) | Lit::Verbatim(_) => {
Err(Error::new_spanned(expr_lit, "Unsupported literal type"))
}

// `Lit` is a non-exhaustive enum
_ => 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),
},

// 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 {
Expr::Lit(ExprLit {
lit: Lit::Bool(lit_bool),
..
}) => 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),
},

_ => Ok(HandleExprOutput::Runtime),
}
}
3 changes: 3 additions & 0 deletions crates/starknet-types-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
2 changes: 1 addition & 1 deletion crates/starknet-types-core/src/felt/alloc_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
Loading