Skip to content

Commit e9f2176

Browse files
fix(values): encode BoundedInt in its compact representation; deref boxed returns
Two remaining marshaling asymmetries: - `Value::to_ptr` wrote a BoundedInt as the un-biased value in a 32-byte felt slot, while the native representation (and `from_ptr`) is the compact one: `value - lower` stored in `repr_bit_width()` bits. A BoundedInt nested in an aggregate (top-level arguments are still gated by the arch.rs panic, #1217) was both silently mis-decoded and overran its slot, corrupting neighboring elements — `Array<BoundedInt<3, 10>>` returned garbage. - `parse_result` passed a returned `Box<T>`'s return-pointer slot straight to `from_ptr` without dereferencing it. Any function that uses a non-ZST builtin returns through a return pointer, so e.g. `fn(...) -> Box<felt252>` using pedersen decoded two raw heap addresses as the payload. Mirror the Nullable arm's deref. Add `RangeExt::repr_encode`/`repr_decode` as the single implementation of the compact encoding (felt-wrapping subtraction so negative lower bounds round-trip) and use the decode side from both existing duplicates (`Value::from_ptr` and the register path in `parse_result`). The `to_ptr` arm now also validates against the type's range rather than only the value's embedded one. Fixes `test_to_ptr_bounded_int_valid`, which asserted the buggy 32-byte encoding ([16, ...] instead of `value - lower` = [6, 0] in the 2-byte 9-bit representation), and its embedded range, which didn't match the type's (`BoundedInt<10, 510>` parses to the range `[10, 511)`). Adds a VM-vs-native test for a boxed return forced through the return pointer, and round-trip unit tests for `Array<BoundedInt<3, 10>>` (asserting the 1-byte biased element buffer) and a negative-lower range. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 164a316 commit e9f2176

6 files changed

Lines changed: 201 additions & 52 deletions

File tree

src/arch.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ impl AbiArgument for ValueWithInfoWrapper<'_> {
7979
abi.capacity.to_bytes(buffer)?;
8080
}
8181
(Value::BoundedInt { .. }, CoreTypeConcrete::BoundedInt(_)) => {
82+
// TODO: implement top-level BoundedInt arguments on top of
83+
// `RangeExt::repr_encode` (dispatch on `repr_bit_width()`: <=64 via the
84+
// `u64` impl, <=128 via `u128`, wider by memory like `Felt`).
8285
// See: https://github.com/starkware-libs/cairo_native/issues/1217
8386
native_panic!("todo: implement AbiArgument for Value::BoundedInt case")
8487
}

src/executor.rs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ use cairo_lang_sierra::{
4242
program_registry::ProgramRegistry,
4343
};
4444
use libc::c_void;
45-
use num_bigint::BigInt;
46-
use num_traits::One;
45+
use num_bigint::BigUint;
4746
use std::{alloc::Layout, arch::global_asm, ptr::NonNull};
4847

4948
mod aot;
@@ -454,9 +453,14 @@ fn parse_result(
454453
registry,
455454
)?),
456455
CoreTypeConcrete::Box(info) => unsafe {
457-
let ptr =
458-
return_ptr.unwrap_or_else(|| NonNull::new_unchecked(ret_registers[0] as *mut ()));
459-
let value = Value::from_ptr(ptr, &info.ty, registry)?;
456+
// With a return pointer the returned value is the box's inline
457+
// representation — a slot holding the payload pointer — so it must be
458+
// dereferenced once. Without one, the register holds the payload
459+
// pointer itself.
460+
let ptr = return_ptr.map_or(ret_registers[0] as *mut (), |x| {
461+
*x.cast::<*mut ()>().as_ref()
462+
});
463+
let value = Value::from_ptr(NonNull::new_unchecked(ptr), &info.ty, registry)?;
460464
Ok(value)
461465
},
462466
CoreTypeConcrete::EcPoint(_) | CoreTypeConcrete::EcState(_) => Ok(Value::from_ptr(
@@ -523,17 +527,14 @@ fn parse_result(
523527
match return_ptr {
524528
Some(return_ptr) => Ok(Value::from_ptr(return_ptr, type_id, registry)?),
525529
None => {
526-
let mut data = if info.range.repr_bit_width() <= 64 {
527-
BigInt::from(ret_registers[0])
530+
let raw = if info.range.repr_bit_width() <= 64 {
531+
BigUint::from(ret_registers[0])
528532
} else {
529-
BigInt::from(((ret_registers[1] as u128) << 64) | ret_registers[0] as u128)
533+
BigUint::from(((ret_registers[1] as u128) << 64) | ret_registers[0] as u128)
530534
};
531535

532-
data &= (BigInt::one() << info.range.repr_bit_width()) - BigInt::one();
533-
data += &info.range.lower;
534-
535536
Ok(Value::BoundedInt {
536-
value: data.into(),
537+
value: info.range.repr_decode(raw).into(),
537538
range: info.range.clone(),
538539
})
539540
}

src/utils/range_ext.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1+
use crate::utils::{get_integer_layout, PRIME};
12
use cairo_lang_sierra::extensions::utils::Range;
23
use num_bigint::{BigInt, BigUint, Sign};
3-
use num_traits::One;
4+
use num_traits::{Euclid, One};
45

56
pub trait RangeExt {
67
/// Width in bits when the offset is zero (aka. the natural representation).
78
fn zero_based_bit_width(&self) -> u32;
89
/// Width in bits when the offset is not necessarily zero (aka. the compact representation).
910
fn repr_bit_width(&self) -> u32;
11+
/// Encode a value into the compact representation: the stored bits are
12+
/// `(value - lower) mod PRIME`, laid out little-endian over the full
13+
/// `get_integer_layout(repr_bit_width())` size. Returns `None` when the
14+
/// value is not within the range.
15+
fn repr_encode(&self, value: &BigInt) -> Option<Vec<u8>>;
16+
/// Decode raw stored bits back into the value they represent: mask to
17+
/// `repr_bit_width()` bits and add `lower`.
18+
fn repr_decode(&self, raw: BigUint) -> BigInt;
1019
}
1120

1221
impl RangeExt for Range {
@@ -44,4 +53,24 @@ impl RangeExt for Range {
4453
// FIXME: Workaround for segfault in canonicalization (including LLVM 19).
4554
((self.size() - BigInt::one()).bits() as u32).max(1)
4655
}
56+
57+
fn repr_encode(&self, value: &BigInt) -> Option<Vec<u8>> {
58+
// The subtraction is felt arithmetic so that ranges with a negative
59+
// lower bound round-trip: values are field elements in `[0, PRIME)`,
60+
// and `repr_decode`'s addition wraps the same way.
61+
let prime = BigInt::from_biguint(Sign::Plus, PRIME.clone());
62+
let stored = (value - &self.lower).rem_euclid(&prime);
63+
if stored >= self.size() {
64+
return None;
65+
}
66+
67+
let mut bytes = stored.magnitude().to_bytes_le();
68+
bytes.resize(get_integer_layout(self.repr_bit_width()).size(), 0);
69+
Some(bytes)
70+
}
71+
72+
fn repr_decode(&self, raw: BigUint) -> BigInt {
73+
let mask = (BigUint::one() << self.repr_bit_width()) - BigUint::one();
74+
BigInt::from_biguint(Sign::Plus, raw & mask) + &self.lower
75+
}
4776
}

src/values.rs

Lines changed: 116 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::{
88
runtime::FeltDict,
99
starknet::{ArrayAbi, Secp256k1Point, Secp256r1Point},
1010
types::{ec_point, ec_state, TypeBuilder},
11-
utils::{felt252_bigint, felt_from_slot, get_integer_layout, layout_repeat, RangeExt, PRIME},
11+
utils::{felt252_bigint, felt_from_slot, get_integer_layout, layout_repeat, RangeExt},
1212
};
1313
use bumpalo::Bump;
1414
use cairo_lang_sierra::{
@@ -23,7 +23,7 @@ use cairo_lang_sierra::{
2323
};
2424
use educe::Educe;
2525
use num_bigint::{BigInt, BigUint, Sign};
26-
use num_traits::{Euclid, One};
26+
use num_traits::One;
2727
use starknet_types_core::{curve::ProjectivePoint, felt::Felt};
2828
use std::{
2929
alloc::Layout,
@@ -229,22 +229,35 @@ impl Value {
229229
.into());
230230
}
231231

232-
let prime = BigInt::from_biguint(Sign::Plus, PRIME.clone());
233-
let lower = lower.rem_euclid(&prime);
234-
let upper = upper.rem_euclid(&prime);
232+
let range = match Self::resolve_type(ty, registry)? {
233+
CoreTypeConcrete::BoundedInt(info)
234+
| CoreTypeConcrete::BoundedIntGuarantee(info) => &info.range,
235+
_ => {
236+
return Err(Error::UnexpectedValue(format!(
237+
"expected value of type {:?} but got a bounded int",
238+
type_id.debug_name
239+
)))
240+
}
241+
};
235242

236-
// Check if value is within the valid range
237-
if !(lower <= value && value < upper) {
238-
return Err(CompilerError::BoundedIntOutOfRange {
243+
// The native representation is the compact one: `value - lower`
244+
// stored in `repr_bit_width()` bits, which is what `from_ptr`
245+
// decodes and what compiled code expects.
246+
let data = range.repr_encode(&value).ok_or_else(|| {
247+
CompilerError::BoundedIntOutOfRange {
239248
value: Box::new(value),
240-
range: Box::new((lower, upper)),
249+
range: Box::new((range.lower.clone(), range.upper.clone())),
241250
}
242-
.into());
243-
}
251+
})?;
244252

245-
let ptr = arena.alloc_layout(get_integer_layout(252)).cast();
246-
let data = felt252_bigint(value).to_bytes_le();
247-
ptr.cast::<[u8; 32]>().as_mut().copy_from_slice(&data);
253+
let ptr: NonNull<()> = arena
254+
.alloc_layout(get_integer_layout(range.repr_bit_width()))
255+
.cast();
256+
std::ptr::copy_nonoverlapping(
257+
data.as_ptr(),
258+
ptr.cast::<u8>().as_ptr(),
259+
data.len(),
260+
);
248261
ptr
249262
}
250263

@@ -853,19 +866,13 @@ impl Value {
853866
CoreTypeConcrete::Const(_) => native_panic!("implement const from_ptr"),
854867
CoreTypeConcrete::BoundedInt(info)
855868
| CoreTypeConcrete::BoundedIntGuarantee(info) => {
856-
let mut data = BigInt::from_biguint(
857-
Sign::Plus,
858-
BigUint::from_bytes_le(slice::from_raw_parts(
859-
ptr.cast::<u8>().as_ptr(),
860-
(info.range.repr_bit_width().next_multiple_of(8) >> 3) as usize,
861-
)),
862-
);
863-
864-
data &= (BigInt::one() << info.range.repr_bit_width()) - BigInt::one();
865-
data += &info.range.lower;
869+
let raw = BigUint::from_bytes_le(slice::from_raw_parts(
870+
ptr.cast::<u8>().as_ptr(),
871+
(info.range.repr_bit_width().next_multiple_of(8) >> 3) as usize,
872+
));
866873

867874
Self::BoundedInt {
868-
value: data.into(),
875+
value: info.range.repr_decode(raw).into(),
869876
range: info.range.clone(),
870877
}
871878
}
@@ -1474,23 +1481,93 @@ mod test {
14741481
// Create the registry for the program
14751482
let registry = ProgramRegistry::<CoreType, CoreLibfunc>::new(&program).unwrap();
14761483

1477-
// Valid case
1484+
// Valid case: `BoundedInt<10, 510>` (a `Range` of `[10, 511)`) has a
1485+
// 9-bit compact representation (2 bytes), storing `value - lower`.
1486+
let value = Value::BoundedInt {
1487+
value: Felt::from(16),
1488+
range: Range {
1489+
lower: BigInt::from(10),
1490+
upper: BigInt::from(511),
1491+
},
1492+
};
1493+
1494+
let arena = Bump::new();
1495+
let ptr = value
1496+
.to_ptr(&arena, &registry, &program.type_declarations[1].id)
1497+
.unwrap();
1498+
1499+
assert_eq!(unsafe { *ptr.cast::<[u8; 2]>().as_ptr() }, [6, 0]);
14781500
assert_eq!(
1479-
unsafe {
1480-
*Value::BoundedInt {
1481-
value: Felt::from(16),
1482-
range: Range {
1483-
lower: BigInt::from(10),
1484-
upper: BigInt::from(510),
1485-
},
1486-
}
1487-
.to_ptr(&Bump::new(), &registry, &program.type_declarations[1].id)
1488-
.unwrap()
1489-
.cast::<[u32; 8]>()
1490-
.as_ptr()
1501+
Value::from_ptr(ptr, &program.type_declarations[1].id, &registry).unwrap(),
1502+
value
1503+
);
1504+
}
1505+
1506+
#[test]
1507+
fn test_roundtrip_bounded_int_array() {
1508+
// `BoundedInt<3, 10>` has a 3-bit compact representation (1-byte layout
1509+
// and stride), storing `value - 3`.
1510+
let program = ProgramParser::new()
1511+
.parse(
1512+
"type B = BoundedInt<3, 10>;
1513+
type A = Array<B>;",
1514+
)
1515+
.unwrap();
1516+
let registry = ProgramRegistry::<CoreType, CoreLibfunc>::new(&program).unwrap();
1517+
1518+
let bounded = |v: u64| Value::BoundedInt {
1519+
value: Felt::from(v),
1520+
range: Range {
1521+
lower: BigInt::from(3),
1522+
upper: BigInt::from(11),
14911523
},
1492-
[16, 0, 0, 0, 0, 0, 0, 0]
1524+
};
1525+
let value = Value::Array(vec![bounded(3), bounded(7), bounded(9)]);
1526+
1527+
let arena = Bump::new();
1528+
let ptr = value
1529+
.to_ptr(&arena, &registry, &program.type_declarations[1].id)
1530+
.unwrap();
1531+
1532+
// Check the raw element buffer: 1-byte stride of biased values.
1533+
let abi = unsafe { ptr.cast::<crate::starknet::ArrayAbi<u8>>().as_ref() };
1534+
assert_eq!(
1535+
unsafe { std::slice::from_raw_parts(abi.ptr, 3) },
1536+
&[0, 4, 6]
14931537
);
1538+
1539+
assert_eq!(
1540+
Value::from_ptr(ptr, &program.type_declarations[1].id, &registry).unwrap(),
1541+
value
1542+
);
1543+
}
1544+
1545+
#[test]
1546+
fn test_roundtrip_bounded_int_negative_lower() {
1547+
let program = ProgramParser::new()
1548+
.parse("type B = BoundedInt<-5, 5>;")
1549+
.unwrap();
1550+
let registry = ProgramRegistry::<CoreType, CoreLibfunc>::new(&program).unwrap();
1551+
1552+
for v in [-5_i64, -1, 0, 4] {
1553+
let value = Value::BoundedInt {
1554+
value: Felt::from(v),
1555+
range: Range {
1556+
lower: BigInt::from(-5),
1557+
upper: BigInt::from(6),
1558+
},
1559+
};
1560+
1561+
let arena = Bump::new();
1562+
let ptr = value
1563+
.to_ptr(&arena, &registry, &program.type_declarations[0].id)
1564+
.unwrap();
1565+
1566+
assert_eq!(
1567+
Value::from_ptr(ptr, &program.type_declarations[0].id, &registry).unwrap(),
1568+
value
1569+
);
1570+
}
14941571
}
14951572

14961573
#[test]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
use core::pedersen::pedersen;
2+
3+
fn run_test(a: felt252, b: felt252) -> Box<felt252> {
4+
BoxTrait::new(pedersen(a, b))
5+
}

tests/tests/programs.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,3 +314,37 @@ fn no_op() {
314314
)
315315
.unwrap();
316316
}
317+
318+
#[test]
319+
fn box_return_forced_through_return_ptr() {
320+
// The pedersen builtin makes the function return more than one value (the
321+
// builtin plus the box), so the box comes back through the return pointer,
322+
// which `parse_result` must dereference once before reading the payload.
323+
let program = &load_program_and_runner("programs/box_return_with_pedersen");
324+
325+
let (a, b) = (Felt::from(1234), Felt::from(5678));
326+
327+
let result_vm = run_vm_program(
328+
program,
329+
"run_test",
330+
vec![Arg::Value(a), Arg::Value(b)],
331+
Some(DEFAULT_GAS as usize),
332+
)
333+
.unwrap();
334+
335+
let result_native = run_native_program(
336+
program,
337+
"run_test",
338+
&[Value::Felt252(a), Value::Felt252(b)],
339+
Some(DEFAULT_GAS),
340+
Option::<DummySyscallHandler>::None,
341+
);
342+
343+
compare_outputs(
344+
&program.1,
345+
&program.2.find_function("run_test").unwrap().id,
346+
&result_vm,
347+
&result_native,
348+
)
349+
.expect("boxed return through return pointer must agree between VM and native");
350+
}

0 commit comments

Comments
 (0)