From 45b611c12b951680f134a8e209e7f1e4cf6c0e2e Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Wed, 1 Jul 2026 18:51:48 +0800 Subject: [PATCH] fix(ssa): preserve observable cast chain boundaries --- .../src/ssa/ir/dfg/simplify/cast.rs | 88 ++++++++++++++++++- .../src/ssa/opt/expand_signed_math.rs | 27 +++--- .../src/ssa/opt/flatten_cfg.rs | 4 +- 3 files changed, 102 insertions(+), 17 deletions(-) diff --git a/compiler/noirc_evaluator/src/ssa/ir/dfg/simplify/cast.rs b/compiler/noirc_evaluator/src/ssa/ir/dfg/simplify/cast.rs index 9ee8f23b842..d76c9492d0c 100644 --- a/compiler/noirc_evaluator/src/ssa/ir/dfg/simplify/cast.rs +++ b/compiler/noirc_evaluator/src/ssa/ir/dfg/simplify/cast.rs @@ -26,9 +26,15 @@ pub(super) fn simplify_cast( } if let Value::Instruction { instruction, .. } = &dfg[value] - && let Instruction::Cast(original_value, _) = &dfg[*instruction] + && let Instruction::Cast(original_value, intermediate_typ) = &dfg[*instruction] { let original_value = *original_value; + let original_typ = dfg.type_of_value(original_value).unwrap_numeric(); + // Constant folding can later observe truncation or extension at the intermediate type. + // Collapse the chain only when folding the two casts is equivalent to folding one. + if !cast_chain_is_equivalent(original_typ, *intermediate_typ, dst_typ) { + return None; + } return match simplify_cast(original_value, dst_typ, dfg) { None => SimplifiedToInstruction(Instruction::Cast(original_value, dst_typ)), simpler => simpler, @@ -119,9 +125,53 @@ pub(super) fn simplify_cast( } } +fn cast_chain_is_equivalent( + original_typ: NumericType, + intermediate_typ: NumericType, + dst_typ: NumericType, +) -> bool { + let integer = |typ| match typ { + NumericType::Signed { bit_size } => (bit_size, true), + NumericType::Unsigned { bit_size } => (bit_size, false), + NumericType::NativeField => (FieldElement::max_num_bits(), false), + }; + let (original_bit_size, original_is_signed) = integer(original_typ); + let (intermediate_bit_size, intermediate_is_signed) = integer(intermediate_typ); + let (dst_bit_size, dst_is_signed) = integer(dst_typ); + + if dst_bit_size <= intermediate_bit_size { + if dst_bit_size <= original_bit_size { + // Both paths keep the same low destination bits. + return true; + } + + // The intermediate and direct casts both widen the original value. Their extension bits + // agree unless a signed original crosses exactly one signed destination boundary. + return !original_is_signed || intermediate_is_signed == dst_is_signed; + } + + if intermediate_bit_size < original_bit_size { + // The intermediate cast discards bits which the wider destination would otherwise retain. + return false; + } + + if intermediate_bit_size == original_bit_size { + // Retyping equal-width bits only matters when the destination sign-extends them. + return !dst_is_signed || intermediate_is_signed == original_is_signed; + } + + // Both casts widen. An unsigned original always zero-extends. For a signed original, the + // intermediate and destination signedness must agree so that extension happens at both + // boundaries or neither one. + !original_is_signed || intermediate_is_signed == dst_is_signed +} + #[cfg(test)] mod tests { - use crate::{assert_ssa_snapshot, ssa::ssa_gen::Ssa}; + use crate::{ + assert_ssa_snapshot, + ssa::{interpreter::value::Value, opt::CONSTANT_FOLDING_MAX_ITER, ssa_gen::Ssa}, + }; #[test] fn unsigned_u8_to_i8_safe() { @@ -298,6 +348,40 @@ mod tests { "); } + #[test] + fn preserves_observable_cast_boundaries() { + let src = " + acir(inline) fn main f0 { + b0(v0: i8, v1: u8, v2: i16): + constrain v0 == i8 -1 + constrain v1 == u8 255 + constrain v2 == i16 256 + v3 = cast v0 as u8 + v4 = cast v3 as i16 + v5 = cast v1 as i8 + v6 = cast v5 as i16 + v7 = truncate v2 to 8 bits, max_bit_size: 16 + v8 = cast v7 as u8 + v9 = cast v8 as u16 + v10 = truncate v2 to 8 bits, max_bit_size: 16 + v11 = cast v10 as i8 + v12 = cast v11 as i16 + v13 = cast v0 as u16 + v14 = cast v13 as i16 + return v4, v6, v9, v12, v14 + } + "; + + let ssa = Ssa::from_str_simplifying(src).unwrap(); + let ssa = ssa.fold_constants_using_constraints(CONSTANT_FOLDING_MAX_ITER); + let result = ssa.interpret(vec![Value::i8(-1), Value::u8(255), Value::i16(256)]).unwrap(); + + assert_eq!( + result, + vec![Value::i16(255), Value::i16(-1), Value::u16(0), Value::i16(0), Value::i16(255),] + ); + } + #[test] fn simplifies_cast_from_field_4_to_i8() { let src = " diff --git a/compiler/noirc_evaluator/src/ssa/opt/expand_signed_math.rs b/compiler/noirc_evaluator/src/ssa/opt/expand_signed_math.rs index a61383e0a2e..35e54480830 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/expand_signed_math.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/expand_signed_math.rs @@ -515,19 +515,20 @@ mod tests { v27 = cast v26 as u8 v28 = mod v20, v27 v29 = cast v10 as u1 - v30 = cast v28 as Field - v31 = sub Field 128, v30 - v32 = cast v10 as Field - v33 = mul v31, v32 - v34 = mul v33, Field 2 - v35 = add v30, v34 - v37 = eq v30, Field 0 - v38 = not v37 - v39 = cast v38 as Field - v40 = mul v35, v39 - v41 = cast v40 as u8 - v42 = cast v40 as i8 - return v42 + v30 = cast v29 as u8 + v31 = cast v28 as Field + v32 = sub Field 128, v31 + v33 = cast v29 as Field + v34 = mul v32, v33 + v35 = mul v34, Field 2 + v36 = add v31, v35 + v38 = eq v31, Field 0 + v39 = not v38 + v40 = cast v39 as Field + v41 = mul v36, v40 + v42 = cast v41 as u8 + v43 = cast v41 as i8 + return v43 } "#); } diff --git a/compiler/noirc_evaluator/src/ssa/opt/flatten_cfg.rs b/compiler/noirc_evaluator/src/ssa/opt/flatten_cfg.rs index b1e01412cda..589de1a2903 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/flatten_cfg.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/flatten_cfg.rs @@ -2327,7 +2327,7 @@ mod tests { v12 = cast v11 as u8 v13 = load v6 -> u8 v14 = not v5 - v15 = cast v4 as u8 + v15 = cast v5 as u8 v16 = cast v14 as u8 v17 = unchecked_mul v15, v12 v18 = unchecked_mul v16, v13 @@ -2336,7 +2336,7 @@ mod tests { enable_side_effects v14 v20 = load v6 -> u8 v21 = cast v14 as u8 - v22 = cast v4 as u8 + v22 = cast v5 as u8 v23 = unchecked_mul v22, v20 store v23 at v6 enable_side_effects u1 1