Skip to content

Commit aa98a79

Browse files
cptarturksew1
andauthored
Support spans in data transformer (#3163)
<!-- Reference any GitHub issues resolved by this PR --> Closes #3136 ## Introduced changes <!-- A brief description of the changes --> - Added support for using Spans in data transformer through `array![...].span()`. ## Checklist <!-- Make sure all of these are complete --> - [x] Linked relevant issue - [x] Updated relevant documentation - [x] Added relevant tests - [x] Performed self-review of the code - [x] Added changes to `CHANGELOG.md` --------- Co-authored-by: ksew1 <[email protected]>
1 parent 577ce97 commit aa98a79

File tree

7 files changed

+307
-3
lines changed

7 files changed

+307
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919

2020
### Cast
2121

22+
#### Added
23+
24+
- Support for `array![].span()` in `--arguments` command
25+
2226
#### Changed
2327

2428
- `verify` command now supports the `--class-hash` for Walnut verification
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use crate::sierra_abi::SupportedCalldataKind;
2+
use crate::sierra_abi::data_representation::AllowedCalldataArgument;
3+
use anyhow::{Result, bail, ensure};
4+
use cairo_lang_parser::utils::SimpleParserDatabase;
5+
use cairo_lang_syntax::node::ast::{
6+
BinaryOperator, Expr, ExprBinary, ExprFunctionCall, PathSegment,
7+
};
8+
use cairo_lang_syntax::node::{Terminal, TypedSyntaxNode};
9+
use starknet::core::types::contract::AbiEntry;
10+
11+
impl SupportedCalldataKind for ExprBinary {
12+
fn transform(
13+
&self,
14+
expected_type: &str,
15+
abi: &[AbiEntry],
16+
db: &SimpleParserDatabase,
17+
) -> Result<AllowedCalldataArgument> {
18+
let op = self.op(db);
19+
let lhs = self.lhs(db);
20+
let rhs = self.rhs(db);
21+
22+
if !matches!(op, BinaryOperator::Dot(_)) {
23+
let op = op.as_syntax_node().get_text_without_trivia(db);
24+
bail!(r#"Invalid operator, expected ".", got "{op}""#)
25+
}
26+
27+
let Expr::InlineMacro(lhs) = lhs else {
28+
let lhs = lhs.as_syntax_node().get_text_without_trivia(db);
29+
bail!(r#"Only "array![]" is supported as left-hand side of "." operator, got "{lhs}""#);
30+
};
31+
32+
let Expr::FunctionCall(rhs) = rhs else {
33+
let rhs = rhs.as_syntax_node().get_text_without_trivia(db);
34+
bail!(r#"Only calling ".span()" on "array![]" is supported, got "{rhs}""#);
35+
};
36+
37+
assert_is_span(&rhs, db)?;
38+
let expected_type = expected_type.replace("Span", "Array");
39+
lhs.transform(&expected_type, abi, db)
40+
}
41+
}
42+
43+
fn assert_is_span(expr: &ExprFunctionCall, db: &SimpleParserDatabase) -> Result<()> {
44+
match expr
45+
.path(db)
46+
.elements(db)
47+
.last()
48+
.expect("Function call must have a name")
49+
{
50+
PathSegment::Simple(simple) => {
51+
let function_name = simple.ident(db).text(db);
52+
ensure!(
53+
function_name == "span",
54+
r#"Invalid function name, expected "span", got "{function_name}""#
55+
);
56+
Ok(())
57+
}
58+
PathSegment::WithGenericArgs(_) => {
59+
bail!("Invalid path specified: generic args in function call not supported")
60+
}
61+
}
62+
}

crates/data-transformer/src/sierra_abi/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use cairo_lang_syntax::node::ast::Expr;
44
use data_representation::AllowedCalldataArgument;
55
use starknet::core::types::contract::AbiEntry;
66

7+
mod binary;
78
mod complex_types;
89
pub(super) mod data_representation;
910
mod literals;
@@ -40,8 +41,8 @@ pub(super) fn build_representation(
4041
Expr::FunctionCall(item) => item.transform(expected_type, abi, db),
4142
Expr::InlineMacro(item) => item.transform(expected_type, abi, db),
4243
Expr::Tuple(item) => item.transform(expected_type, abi, db),
44+
Expr::Binary(item) => item.transform(expected_type, abi, db),
4345
Expr::Parenthesized(_)
44-
| Expr::Binary(_)
4546
| Expr::Block(_)
4647
| Expr::Match(_)
4748
| Expr::If(_)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "data_transformer_contract"
3+
version = "0.1.0"
4+
edition = "2024_07"
5+
6+
# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html
7+
8+
[dependencies]
9+
starknet = "2.10.0"
10+
alexandria_data_structures = "0.4.0"
11+
12+
[dev-dependencies]
13+
snforge_std = "0.39.0"
14+
assert_macros = "2.10.0"
15+
16+
[[target.starknet-contract]]
17+
sierra = true
18+
19+
[scripts]
20+
test = "snforge test"
21+
22+
[tool.scarb]
23+
allow-prebuilt-plugins = ["snforge_std"]
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#[derive(Serde, Drop)]
2+
pub struct SimpleStruct {
3+
a: felt252,
4+
}
5+
6+
#[derive(Serde, Drop)]
7+
pub struct NestedStructWithField {
8+
a: SimpleStruct,
9+
b: felt252,
10+
}
11+
12+
#[derive(Serde, Drop)]
13+
pub enum Enum {
14+
One: (),
15+
Two: u128,
16+
Three: NestedStructWithField,
17+
}
18+
19+
#[derive(Serde, Drop)]
20+
pub struct ComplexStruct {
21+
a: NestedStructWithField,
22+
b: felt252,
23+
c: u8,
24+
d: i32,
25+
e: Enum,
26+
f: ByteArray,
27+
g: Array<felt252>,
28+
h: u256,
29+
i: (i128, u128),
30+
}
31+
32+
#[derive(Serde, Drop)]
33+
pub struct BitArray {
34+
bit: felt252,
35+
}
36+
37+
#[starknet::interface]
38+
pub trait IDataTransformer<TContractState> {
39+
fn simple_fn(ref self: TContractState, a: felt252);
40+
fn u256_fn(ref self: TContractState, a: u256);
41+
fn signed_fn(ref self: TContractState, a: i32);
42+
fn unsigned_fn(ref self: TContractState, a: u32);
43+
fn tuple_fn(ref self: TContractState, a: (felt252, u8, Enum));
44+
fn complex_fn(
45+
ref self: TContractState,
46+
arr: Array<Array<felt252>>,
47+
one: u8,
48+
two: i16,
49+
three: ByteArray,
50+
four: (felt252, u32),
51+
five: bool,
52+
six: u256,
53+
);
54+
fn simple_struct_fn(ref self: TContractState, a: SimpleStruct);
55+
fn nested_struct_fn(ref self: TContractState, a: NestedStructWithField);
56+
fn enum_fn(ref self: TContractState, a: Enum);
57+
fn complex_struct_fn(ref self: TContractState, a: ComplexStruct);
58+
fn external_struct_fn(
59+
ref self: TContractState, a: BitArray, b: alexandria_data_structures::bit_array::BitArray,
60+
);
61+
fn span_fn(ref self: TContractState, a: Span<felt252>);
62+
}
63+
64+
#[starknet::contract]
65+
mod DataTransformer {
66+
use super::*;
67+
use core::starknet::ContractAddress;
68+
69+
#[storage]
70+
struct Storage {
71+
balance: felt252,
72+
}
73+
74+
#[constructor]
75+
fn constructor(ref self: ContractState, init_owner: ContractAddress) {}
76+
77+
#[abi(embed_v0)]
78+
impl DataTransformerImpl of super::IDataTransformer<ContractState> {
79+
fn simple_fn(ref self: ContractState, a: felt252) {}
80+
fn u256_fn(ref self: ContractState, a: u256) {}
81+
fn signed_fn(ref self: ContractState, a: i32) {}
82+
fn unsigned_fn(ref self: ContractState, a: u32) {}
83+
fn tuple_fn(ref self: ContractState, a: (felt252, u8, Enum)) {}
84+
fn complex_fn(
85+
ref self: ContractState,
86+
arr: Array<Array<felt252>>,
87+
one: u8,
88+
two: i16,
89+
three: ByteArray,
90+
four: (felt252, u32),
91+
five: bool,
92+
six: u256,
93+
) {}
94+
fn simple_struct_fn(ref self: ContractState, a: SimpleStruct) {}
95+
fn nested_struct_fn(ref self: ContractState, a: NestedStructWithField) {}
96+
fn enum_fn(ref self: ContractState, a: Enum) {}
97+
fn complex_struct_fn(ref self: ContractState, a: ComplexStruct) {}
98+
fn external_struct_fn(
99+
ref self: ContractState,
100+
a: BitArray,
101+
b: alexandria_data_structures::bit_array::BitArray,
102+
) {}
103+
fn span_fn(ref self: ContractState, a: Span<felt252>) {}
104+
}
105+
}

crates/data-transformer/tests/integration.rs

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ use test_case::test_case;
1313
use tokio::sync::OnceCell;
1414
use url::Url;
1515

16-
// https://sepolia.starkscan.co/class/0x02a9b456118a86070a8c116c41b02e490f3dcc9db3cad945b4e9a7fd7cec9168#code
16+
// Deployment of contract from /tests/data/data_transformer
1717
const TEST_CLASS_HASH: Felt =
18-
Felt::from_hex_unchecked("0x02a9b456118a86070a8c116c41b02e490f3dcc9db3cad945b4e9a7fd7cec9168");
18+
Felt::from_hex_unchecked("0x032e6763d5e778f153e5b6ea44200d94ec89aac7b42a0aef0e4e0caac8dafdab");
1919

2020
static CLASS: OnceCell<ContractClass> = OnceCell::const_new();
2121

@@ -435,6 +435,113 @@ async fn test_happy_case_nested_struct_function_cairo_expression_input() -> anyh
435435
Ok(())
436436
}
437437

438+
#[tokio::test]
439+
async fn test_happy_case_span_function_cairo_expression_input() -> anyhow::Result<()> {
440+
let contract_class = CLASS.get_or_init(init_class).await.to_owned();
441+
442+
let input = String::from("array![1, 2, 3].span()");
443+
444+
let result = Calldata::new(input)
445+
.serialized(contract_class, &get_selector_from_name("span_fn").unwrap())?;
446+
447+
let expected_output = [
448+
Felt::from_hex_unchecked("0x3"),
449+
Felt::from_hex_unchecked("0x1"),
450+
Felt::from_hex_unchecked("0x2"),
451+
Felt::from_hex_unchecked("0x3"),
452+
];
453+
454+
assert_eq!(result, expected_output);
455+
456+
Ok(())
457+
}
458+
459+
#[tokio::test]
460+
async fn test_happy_case_empty_span_function_cairo_expression_input() -> anyhow::Result<()> {
461+
let contract_class = CLASS.get_or_init(init_class).await.to_owned();
462+
463+
let input = String::from("array![].span()");
464+
465+
let result = Calldata::new(input)
466+
.serialized(contract_class, &get_selector_from_name("span_fn").unwrap())?;
467+
468+
let expected_output = [Felt::from_hex_unchecked("0x0")];
469+
470+
assert_eq!(result, expected_output);
471+
472+
Ok(())
473+
}
474+
475+
#[tokio::test]
476+
async fn test_span_function_array_input() {
477+
let contract_class = CLASS.get_or_init(init_class).await.to_owned();
478+
479+
let input = String::from("array![1, 2, 3]");
480+
481+
let result = Calldata::new(input)
482+
.serialized(contract_class, &get_selector_from_name("span_fn").unwrap());
483+
484+
result
485+
.unwrap_err()
486+
.assert_contains(r#"Expected "core::array::Span::<core::felt252>", got array"#);
487+
}
488+
489+
#[tokio::test]
490+
async fn test_span_function_unsupported_method() {
491+
let contract_class = CLASS.get_or_init(init_class).await.to_owned();
492+
493+
let input = String::from("array![1, 2, 3].into()");
494+
495+
let result = Calldata::new(input)
496+
.serialized(contract_class, &get_selector_from_name("span_fn").unwrap());
497+
498+
result
499+
.unwrap_err()
500+
.assert_contains(r#"Invalid function name, expected "span", got "into""#);
501+
}
502+
503+
#[tokio::test]
504+
async fn test_span_function_unsupported_operator() {
505+
let contract_class = CLASS.get_or_init(init_class).await.to_owned();
506+
507+
let input = String::from("array![1, 2, 3]*span()");
508+
509+
let result = Calldata::new(input)
510+
.serialized(contract_class, &get_selector_from_name("span_fn").unwrap());
511+
512+
result
513+
.unwrap_err()
514+
.assert_contains(r#"Invalid operator, expected ".", got "*""#);
515+
}
516+
517+
#[tokio::test]
518+
async fn test_span_function_unsupported_right_hand_side() {
519+
let contract_class = CLASS.get_or_init(init_class).await.to_owned();
520+
521+
let input = String::from("array![1, 2, 3].span");
522+
523+
let result = Calldata::new(input)
524+
.serialized(contract_class, &get_selector_from_name("span_fn").unwrap());
525+
526+
result
527+
.unwrap_err()
528+
.assert_contains(r#"Only calling ".span()" on "array![]" is supported, got "span""#);
529+
}
530+
531+
#[tokio::test]
532+
async fn test_span_function_unsupported_left_hand_side() {
533+
let contract_class = CLASS.get_or_init(init_class).await.to_owned();
534+
535+
let input = String::from("(1, 2, 3).span");
536+
537+
let result = Calldata::new(input)
538+
.serialized(contract_class, &get_selector_from_name("span_fn").unwrap());
539+
540+
result.unwrap_err().assert_contains(
541+
r#"Only "array![]" is supported as left-hand side of "." operator, got "(1, 2, 3)""#,
542+
);
543+
}
544+
438545
#[tokio::test]
439546
async fn test_happy_case_enum_function_empty_variant_cairo_expression_input() -> anyhow::Result<()>
440547
{
@@ -451,6 +558,7 @@ async fn test_happy_case_enum_function_empty_variant_cairo_expression_input() ->
451558

452559
Ok(())
453560
}
561+
454562
#[tokio::test]
455563
async fn test_happy_case_enum_function_one_argument_variant_cairo_expression_input()
456564
-> anyhow::Result<()> {

docs/src/starknet/calldata-transformation.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Cast supports most important Cairo corelib types:
7373
* `EthAddress`
7474
* `bytes31`
7575
* `Array` - using `array![]` macro
76+
* `Span` - using `array![].span()` macro and function call
7677

7778
Numeric types (primitives and `felt252`) can be paseed with type suffix specified for example `--arguments 420_u64`.
7879

0 commit comments

Comments
 (0)