From 572771051f8ee8ff7275a2cc18f34b8f83782d83 Mon Sep 17 00:00:00 2001 From: TomerStarkware Date: Wed, 5 Aug 2026 16:54:30 +0300 Subject: [PATCH] refactor(cast): unify downcast felt252 canonicalization branches Both arms of the felt252 canonicalization in build_downcast reduce to "subtract PRIME when the felt exceeds a threshold": 0 for a non-positive destination range, HALF_PRIME for one that straddles zero. This also fixes a VM divergence: the non-positive branch subtracted PRIME unconditionally, interpreting felt 0 as -PRIME. For a destination whose lower bound is exactly 1 - PRIME no lower-bound check is emitted, so downcast>(0) returned Some where the VM returns None (and under-charged the range check builtin, 2 vs 3). With the 0 threshold, felt 0 stays 0 and is rejected by the upper-bound check, matching the VM on both the value and the builtin counter. Co-Authored-By: Claude Fable 5 --- src/libfuncs/cast.rs | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/libfuncs/cast.rs b/src/libfuncs/cast.rs index 286160b39..6029ae257 100644 --- a/src/libfuncs/cast.rs +++ b/src/libfuncs/cast.rs @@ -24,7 +24,7 @@ use melior::{ ir::{r#type::IntegerType, Block, Location, Value, ValueLike}, Context, }; -use num_bigint::{BigInt, Sign}; +use num_bigint::{BigInt, BigUint, Sign}; use num_traits::One; /// Select and call the correct libfunc builder function from the selector. @@ -145,26 +145,28 @@ pub fn build_downcast<'ctx, 'this>( // 2. if it is a bounded_int, we need to offset the value to get the // actual value. let src_value = if is_signed && src_ty.is_felt252(registry)? { - if src_range.upper.is_one() { - let adj_offset = - entry.const_int_from_type(context, location, PRIME.clone(), src_value.r#type())?; - entry.append_op_result(arith::subi(src_value, adj_offset, location))? - } else { - let adj_offset = entry.const_int_from_type( - context, - location, - HALF_PRIME.clone(), - src_value.r#type(), - )?; - let is_negative = - entry.cmpi(context, CmpiPredicate::Ugt, src_value, adj_offset, location)?; + // A felt is interpreted as negative (`felt - PRIME`) when it exceeds + // a threshold: HALF_PRIME when the destination range straddles zero, + // and 0 when it is non-positive (every nonzero felt is negative, + // while felt 0 stays 0 and must fail the bounds check). + let adj_offset = entry.const_int_from_type( + context, + location, + if src_range.upper.is_one() { + BigUint::ZERO + } else { + HALF_PRIME.clone() + }, + src_value.r#type(), + )?; + let is_negative = + entry.cmpi(context, CmpiPredicate::Ugt, src_value, adj_offset, location)?; - let k_prime = - entry.const_int_from_type(context, location, PRIME.clone(), src_value.r#type())?; - let adj_value = entry.append_op_result(arith::subi(src_value, k_prime, location))?; + let k_prime = + entry.const_int_from_type(context, location, PRIME.clone(), src_value.r#type())?; + let adj_value = entry.append_op_result(arith::subi(src_value, k_prime, location))?; - entry.append_op_result(arith::select(is_negative, adj_value, src_value, location))? - } + entry.append_op_result(arith::select(is_negative, adj_value, src_value, location))? } else if src_ty.is_bounded_int(registry)? && src_range.lower != BigInt::ZERO { let dst_offset = entry.const_int_from_type( context,