Skip to content

Commit a1bfb99

Browse files
fix(values): make Value::to_ptr always return the inline representation (#1655)
For memory-allocated types (>=2-variant enums and aggregates containing one), `Value::to_ptr` returned a *wrapper* pointer (a slot holding the data pointer) while `Value::from_ptr` always reads the inline representation. Every recursive consumer had to un-box the wrapper, and four of six forgot: - the Array arm copied `elem_layout.size()` bytes out of the 8-byte wrapper, so even `Array<bool>` was corrupted (the 16-byte-aligned enum body keeps the tag bit clear, decoding every element as variant 0); - the Felt252Dict arm had the same omission (`Felt252Dict<bool>`); - the `AbiArgument` Box and Nullable arms memcpy'd the wrapper bytes into the heap block (`Box<SomeEnum>`). Enumerating all callers shows nothing consumes the wrapper — every call site either stripped it immediately or was one of these bugs — so invert the contract instead of patching each site: `to_ptr` now always returns a pointer to the inline representation per `TypeBuilder::layout()`, exactly what `from_ptr` reads, and the by-pointer ABI decision lives only in `crate::arch`'s `AbiArgument` impl. Also make the Felt252Dict arm follow the same convention: it returned the `FeltDict*` itself instead of a slot holding it, so a dict nested in an aggregate copied 8 bytes of HashMap internals instead of the dict pointer. The arch.rs dict arm now dereferences the slot once. The Nullable arm also now passes the payload type id like the Box arm does. Only reachable via the `invoke_dynamic(&[Value])` API; the Starknet contract path marshals felts directly and is unaffected. Adds a VM-vs-native regression test for `Array<bool>` and `to_ptr`->`from_ptr` round-trip unit tests (bool array, struct with bool, nested enum, bool dict) that the new symmetric contract enables. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 69f61e9 commit a1bfb99

4 files changed

Lines changed: 185 additions & 47 deletions

File tree

src/arch.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use cairo_lang_sierra::{
1515
ids::ConcreteTypeId,
1616
program_registry::ProgramRegistry,
1717
};
18-
use std::ptr::{null, NonNull};
18+
use std::ptr::null;
1919

2020
mod aarch64;
2121
mod x86_64;
@@ -76,7 +76,7 @@ impl AbiArgument for ValueWithInfoWrapper<'_> {
7676
if matches!(value, Value::Null) {
7777
null::<()>().to_bytes(buffer)?;
7878
} else {
79-
let ptr = value.to_ptr(self.arena, self.registry, self.type_id)?;
79+
let ptr = value.to_ptr(self.arena, self.registry, &info.ty)?;
8080

8181
let layout = self.registry.get_type(&info.ty)?.layout(self.registry)?;
8282
let heap_ptr = unsafe {
@@ -126,9 +126,9 @@ impl AbiArgument for ValueWithInfoWrapper<'_> {
126126
}
127127
(Value::Enum { tag, value, .. }, CoreTypeConcrete::Enum(info)) => {
128128
if self.info.is_memory_allocated(self.registry)? {
129+
// Memory-allocated types are passed by pointer to their inline
130+
// representation.
129131
let abi_ptr = self.value.to_ptr(self.arena, self.registry, self.type_id)?;
130-
131-
let abi_ptr = unsafe { *abi_ptr.cast::<NonNull<()>>().as_ref() };
132132
abi_ptr.as_ptr().to_bytes(buffer)?;
133133
} else {
134134
match info
@@ -158,10 +158,11 @@ impl AbiArgument for ValueWithInfoWrapper<'_> {
158158
(Value::Felt252Dict { .. }, CoreTypeConcrete::Felt252Dict(_)) => {
159159
// TODO: Assert that `info.ty` matches all the values' types.
160160

161-
self.value
162-
.to_ptr(self.arena, self.registry, self.type_id)?
163-
.as_ptr()
164-
.to_bytes(buffer)?
161+
let ptr = self.value.to_ptr(self.arena, self.registry, self.type_id)?;
162+
163+
// The dict's inline representation is a slot holding the `FeltDict`
164+
// pointer; the ABI passes that pointer by value.
165+
unsafe { *ptr.cast::<*mut ()>().as_ref() }.to_bytes(buffer)?
165166
}
166167
(
167168
Value::Secp256K1Point(Secp256k1Point { x, y, is_infinity }),
@@ -186,9 +187,9 @@ impl AbiArgument for ValueWithInfoWrapper<'_> {
186187
(Value::Sint8(value), CoreTypeConcrete::Sint8(_)) => value.to_bytes(buffer)?,
187188
(Value::Struct { fields, .. }, CoreTypeConcrete::Struct(info)) => {
188189
if self.info.is_memory_allocated(self.registry)? {
190+
// Memory-allocated types are passed by pointer to their inline
191+
// representation.
189192
let abi_ptr = self.value.to_ptr(self.arena, self.registry, self.type_id)?;
190-
191-
let abi_ptr = unsafe { *abi_ptr.cast::<NonNull<()>>().as_ref() };
192193
abi_ptr.as_ptr().to_bytes(buffer)?;
193194
} else {
194195
fields

src/values.rs

Lines changed: 113 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ impl Value {
157157
}
158158

159159
/// Allocates the value in the given arena so it can be passed to the JIT engine or a compiled program.
160+
///
161+
/// Invariant: the returned pointer always points to the value's inline representation as
162+
/// given by [`TypeBuilder::layout`], which is exactly what [`Value::from_ptr`] reads. The
163+
/// ABI decision of passing memory-allocated types by pointer is made by the `AbiArgument`
164+
/// impl in `crate::arch`, not here.
160165
pub(crate) fn to_ptr(
161166
&self,
162167
arena: &Bump,
@@ -269,7 +274,6 @@ impl Value {
269274
let mut layout: Option<Layout> = None;
270275
let mut data = Vec::with_capacity(info.members.len());
271276

272-
let mut is_memory_allocated = false;
273277
for (member_type_id, member) in info.members.iter().zip(members) {
274278
let member_ty = registry.get_type(member_type_id)?;
275279
let member_layout = member_ty.layout(registry)?;
@@ -281,19 +285,7 @@ impl Value {
281285
layout = Some(new_layout);
282286

283287
let member_ptr = member.to_ptr(arena, registry, member_type_id)?;
284-
data.push((
285-
member_layout,
286-
offset,
287-
if member_ty.is_memory_allocated(registry)? {
288-
is_memory_allocated = true;
289-
290-
// Undo the wrapper pointer added because the member's memory
291-
// allocated flag.
292-
*member_ptr.cast::<NonNull<()>>().as_ref()
293-
} else {
294-
member_ptr
295-
},
296-
));
288+
data.push((member_layout, offset, member_ptr));
297289
}
298290

299291
let ptr = arena
@@ -308,12 +300,7 @@ impl Value {
308300
);
309301
}
310302

311-
if is_memory_allocated {
312-
// alloc returns a ref, so its never null
313-
NonNull::new_unchecked(arena.alloc(ptr) as *mut _).cast()
314-
} else {
315-
NonNull::new_unchecked(ptr).cast()
316-
}
303+
NonNull::new_unchecked(ptr).cast()
317304
} else {
318305
Err(Error::UnexpectedValue(format!(
319306
"expected value of type {:?} but got a struct",
@@ -327,18 +314,8 @@ impl Value {
327314
native_assert!(*tag < info.variants.len(), "Variant index out of range.");
328315

329316
let payload_type_id = &info.variants[*tag];
330-
let payload_ty = registry.get_type(payload_type_id)?;
331317
let payload = value.to_ptr(arena, registry, payload_type_id)?;
332318

333-
// Undo the wrapper pointer added when the payload is memory
334-
// allocated (e.g. a nested >=2-variant enum), so that the copy
335-
// below reads the payload data rather than the wrapper pointer.
336-
let payload = if payload_ty.is_memory_allocated(registry)? {
337-
*payload.cast::<NonNull<()>>().as_ref()
338-
} else {
339-
payload
340-
};
341-
342319
let (layout, tag_layout, variant_layouts) =
343320
crate::types::r#enum::get_layout_for_variants(
344321
registry,
@@ -362,12 +339,7 @@ impl Value {
362339
variant_layouts[*tag].size(),
363340
);
364341

365-
if resolved_ty.is_memory_allocated(registry)? {
366-
// alloc returns a reference so its never null
367-
NonNull::new_unchecked(arena.alloc(ptr) as *mut _).cast()
368-
} else {
369-
NonNull::new_unchecked(ptr).cast()
370-
}
342+
NonNull::new_unchecked(ptr).cast()
371343
} else {
372344
Err(Error::UnexpectedValue(format!(
373345
"expected value of type {:?} but got an enum value",
@@ -417,7 +389,10 @@ impl Value {
417389
);
418390
}
419391

420-
NonNull::new_unchecked(dict_ptr as *mut ()).cast()
392+
// The dict's inline representation is a single pointer to the
393+
// `FeltDict`, so return a slot holding that pointer.
394+
// alloc returns a reference so its never null
395+
NonNull::new_unchecked(arena.alloc(dict_ptr) as *mut _).cast()
421396
} else {
422397
Err(Error::UnexpectedValue(format!(
423398
"expected value of type {:?} but got a felt dict",
@@ -1766,6 +1741,107 @@ mod test {
17661741
_ => panic!("Unexpected error type: {:?}", result),
17671742
}
17681743
}
1744+
1745+
/// Helper for the round-trip tests below: a `bool` value (a 2-variant enum
1746+
/// of unit structs, hence memory-allocated).
1747+
fn bool_value(value: bool) -> Value {
1748+
Value::Enum {
1749+
tag: value as usize,
1750+
value: Box::new(Value::Struct {
1751+
fields: Vec::new(),
1752+
debug_name: None,
1753+
}),
1754+
debug_name: None,
1755+
}
1756+
}
1757+
1758+
/// `to_ptr` returns a pointer to the inline representation, which is
1759+
/// exactly what `from_ptr` reads — so any value must round-trip.
1760+
fn assert_roundtrip(program_src: &str, type_idx: usize, value: Value) {
1761+
let program = ProgramParser::new().parse(program_src).unwrap();
1762+
let registry = ProgramRegistry::<CoreType, CoreLibfunc>::new(&program).unwrap();
1763+
let type_id = &program.type_declarations[type_idx].id;
1764+
1765+
let arena = Bump::new();
1766+
let ptr = value.to_ptr(&arena, &registry, type_id).unwrap();
1767+
let result = Value::from_ptr(ptr, type_id, &registry).unwrap();
1768+
1769+
assert_eq!(result, value);
1770+
}
1771+
1772+
#[test]
1773+
fn test_roundtrip_bool_array() {
1774+
assert_roundtrip(
1775+
"type Unit = Struct<ut@Tuple>;
1776+
type bool = Enum<ut@core::bool, Unit, Unit>;
1777+
type BoolArray = Array<bool>;",
1778+
2,
1779+
Value::Array(vec![
1780+
bool_value(true),
1781+
bool_value(false),
1782+
bool_value(true),
1783+
bool_value(true),
1784+
]),
1785+
);
1786+
}
1787+
1788+
#[test]
1789+
fn test_roundtrip_struct_with_bool() {
1790+
assert_roundtrip(
1791+
"type u8 = u8;
1792+
type Unit = Struct<ut@Tuple>;
1793+
type bool = Enum<ut@core::bool, Unit, Unit>;
1794+
type MyStruct = Struct<ut@MyStruct, u8, bool>;",
1795+
3,
1796+
Value::Struct {
1797+
fields: vec![Value::Uint8(123), bool_value(true)],
1798+
debug_name: None,
1799+
},
1800+
);
1801+
}
1802+
1803+
#[test]
1804+
fn test_roundtrip_nested_enum() {
1805+
let program_src = "type felt252 = felt252;
1806+
type Inner = Enum<ut@Inner, felt252, felt252>;
1807+
type Outer = Enum<ut@Outer, Inner, Inner>;";
1808+
1809+
for (outer_tag, inner_tag) in [(0, 0), (0, 1), (1, 0), (1, 1)] {
1810+
assert_roundtrip(
1811+
program_src,
1812+
2,
1813+
Value::Enum {
1814+
tag: outer_tag,
1815+
value: Box::new(Value::Enum {
1816+
tag: inner_tag,
1817+
value: Box::new(Value::Felt252(Felt::from(0x1234))),
1818+
debug_name: None,
1819+
}),
1820+
debug_name: None,
1821+
},
1822+
);
1823+
}
1824+
}
1825+
1826+
#[test]
1827+
fn test_roundtrip_bool_dict() {
1828+
assert_roundtrip(
1829+
"type Unit = Struct<ut@Tuple>;
1830+
type bool = Enum<ut@core::bool, Unit, Unit>;
1831+
type BoolDict = Felt252Dict<bool>;",
1832+
2,
1833+
Value::Felt252Dict {
1834+
value: [
1835+
(Felt::from(0), bool_value(true)),
1836+
(Felt::from(1), bool_value(false)),
1837+
(Felt::from(2), bool_value(true)),
1838+
]
1839+
.into_iter()
1840+
.collect(),
1841+
debug_name: None,
1842+
},
1843+
);
1844+
}
17691845
}
17701846

17711847
mod range_serde {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
fn run_test(data: Array<bool>) -> u32 {
2+
let mut count: u32 = 0;
3+
for value in data {
4+
if value {
5+
count += 1;
6+
}
7+
}
8+
count
9+
}

tests/tests/enums.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,55 @@ fn nested_enum_argument_matches_vm() {
9999
});
100100
}
101101
}
102+
103+
#[test]
104+
fn bool_array_argument_matches_vm() {
105+
// `bool` is a 2-variant enum, so it is memory-allocated and `to_ptr` must
106+
// un-wrap each element before copying it into the array's data buffer. The
107+
// program counts the `true` elements, so any corruption changes the count.
108+
let program = &load_program_and_runner("programs/array_bool_arg");
109+
110+
// The VM tag of a 2-variant enum equals the variant index.
111+
let values = [true, false, true, true, false];
112+
113+
let result_vm = run_vm_program(
114+
program,
115+
"run_test",
116+
vec![Arg::Array(
117+
values
118+
.iter()
119+
.map(|&v| Arg::Value(Felt::from(v as u64)))
120+
.collect(),
121+
)],
122+
Some(DEFAULT_GAS as usize),
123+
)
124+
.unwrap();
125+
126+
let result_native = run_native_program(
127+
program,
128+
"run_test",
129+
&[Value::Array(
130+
values
131+
.iter()
132+
.map(|&v| Value::Enum {
133+
tag: v as usize,
134+
value: Box::new(Value::Struct {
135+
fields: Vec::new(),
136+
debug_name: None,
137+
}),
138+
debug_name: None,
139+
})
140+
.collect(),
141+
)],
142+
Some(DEFAULT_GAS),
143+
Option::<DummySyscallHandler>::None,
144+
);
145+
146+
compare_outputs(
147+
&program.1,
148+
&program.2.find_function("run_test").unwrap().id,
149+
&result_vm,
150+
&result_native,
151+
)
152+
.expect("bool array argument must agree between VM and native");
153+
}

0 commit comments

Comments
 (0)