diff --git a/changelog.d/codegen-p3-b3-delete-int32-consumers.md b/changelog.d/codegen-p3-b3-delete-int32-consumers.md new file mode 100644 index 00000000000..88766a64508 --- /dev/null +++ b/changelog.d/codegen-p3-b3-delete-int32-consumers.md @@ -0,0 +1 @@ +Performance and cleanup: compiled code no longer tests for, or decodes, an INT32-tagged Number. Since P3 B4a nothing can produce one, so a Number has one encoding, the plain double, for every operand, proven or not. Removed from generated code: the INT32 arms of `===`/`!==` and of the relational operators against a Number, the per-local decode of a comparison graph, the boxed arm of the shared array-key normalizer, the INT32 half of the typed-entry and direct-closure-call guards and their convert block, the switch discriminant's canonical view (a numeric `switch` now compares the discriminant directly), the Number-parameter entry conversion, and the Number receiver's INT32 test. `PERRY_WP_REP_P3` is gone: it restored a tag test that no longer exists. The runtime's own INT32 readers are unchanged (P3 B4b removes them). `PERRY_VERIFY_FACTS=1` still checks every trusted Number is a plain double (P3 B3). diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs index c52974c2950..1bec59a0a84 100644 --- a/crates/perry-codegen/src/codegen/closure.rs +++ b/crates/perry-codegen/src/codegen/closure.rs @@ -698,17 +698,6 @@ pub(super) fn compile_closure( private_resume_frame, private_async_resume_frame, ); - // A closure region's Number parameter: an int32 box becomes the - // equal plain double once, here (`split_regions`). - let arg_name = if region_number_params.contains(&p.id) { - let is_int = super::typed_entry::emit_int32_box_test(blk, &arg_name); - let bits = blk.bitcast_double_to_i64(&arg_name); - let low = blk.trunc(I64, &bits, I32); - let as_f64 = blk.sitofp(I32, &low, DOUBLE); - blk.select(I1, &is_int, DOUBLE, &as_f64, &arg_name) - } else { - arg_name - }; let slot = super::arguments::store_param_slot(blk, p, &closure_boxed_vars, &arg_name); if let Some(slot_idx) = shadow_slot_map .get(&p.id) diff --git a/crates/perry-codegen/src/codegen/closure/trampoline.rs b/crates/perry-codegen/src/codegen/closure/trampoline.rs index c9a791e521e..06bff7c7778 100644 --- a/crates/perry-codegen/src/codegen/closure/trampoline.rs +++ b/crates/perry-codegen/src/codegen/closure/trampoline.rs @@ -1,6 +1,5 @@ //! Raw-return branches of typed closure entry trampolines. -use super::super::typed_abi::{TypedFunctionTrampolineKind, TypedParamRep}; -use super::super::typed_entry::emit_typed_arg_to_raw_after_entry_tier; +use super::super::typed_abi::{emit_typed_arg_to_raw, TypedFunctionTrampolineKind, TypedParamRep}; use crate::types::{LlvmType, DOUBLE, I1, I32, I64}; pub(super) fn emit_typed_closure_trampoline_fast_value( @@ -16,7 +15,7 @@ pub(super) fn emit_typed_closure_trampoline_fast_value( let raw_args: Vec = arg_names .iter() .zip(arg_reps.iter()) - .map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg)) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) .collect(); let mut typed_args: Vec<(LlvmType, &str)> = Vec::with_capacity(raw_args.len() + 1); typed_args.push((I64, "%this_closure")); @@ -33,7 +32,7 @@ pub(super) fn emit_typed_closure_trampoline_fast_value( let raw_args: Vec = arg_names .iter() .zip(arg_reps.iter()) - .map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg)) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) .collect(); let mut typed_args: Vec<(LlvmType, &str)> = Vec::with_capacity(raw_args.len() + 1); typed_args.push((I64, "%this_closure")); @@ -50,7 +49,7 @@ pub(super) fn emit_typed_closure_trampoline_fast_value( let raw_args: Vec = arg_names .iter() .zip(arg_reps.iter()) - .map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg)) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) .collect(); let mut typed_args: Vec<(LlvmType, &str)> = Vec::with_capacity(raw_args.len() + 1); typed_args.push((I64, "%this_closure")); @@ -68,7 +67,7 @@ pub(super) fn emit_typed_closure_trampoline_fast_value( let raw_args: Vec = arg_names .iter() .zip(arg_reps.iter()) - .map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg)) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) .collect(); let mut typed_args: Vec<(LlvmType, &str)> = Vec::with_capacity(raw_args.len() + 1); typed_args.push((I64, "%this_closure")); diff --git a/crates/perry-codegen/src/codegen/facts_const_length.rs b/crates/perry-codegen/src/codegen/facts_const_length.rs index 88ad1a83b90..3d9ede12bc5 100644 --- a/crates/perry-codegen/src/codegen/facts_const_length.rs +++ b/crates/perry-codegen/src/codegen/facts_const_length.rs @@ -15,7 +15,7 @@ use std::cell::Cell; use perry_hir::Expr; use crate::expr::FnCtx; -use crate::types::{DOUBLE, I32, I64}; +use crate::types::{DOUBLE, I32}; pub(crate) fn enabled() -> bool { static ON: std::sync::OnceLock = std::sync::OnceLock::new(); @@ -67,14 +67,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> anyhow::Result bool { static ON: std::sync::OnceLock = std::sync::OnceLock::new(); @@ -87,14 +86,9 @@ pub(crate) fn witness_exact(ctx: &mut crate::expr::FnCtx<'_>, value: &str, id: u if !crate::verify_facts::enabled() { return; } - // A proven Number is a canonical double (P3, `facts_rep`): the witness - // admits an exact int32 double only, never an INT32 box. - let exact = if super::facts_rep::canonical_numbers() { - let bits = ctx.block().bitcast_double_to_i64(value); - super::typed_abi::emit_typed_i32_plain_guard_and_raw(ctx.block(), value, &bits).0 - } else { - super::typed_abi::emit_typed_i32_guard_and_raw(ctx.block(), value).0 - }; + // A Number is a plain double: the witness admits an exact int32 double. + let bits = ctx.block().bitcast_double_to_i64(value); + let exact = super::typed_abi::emit_typed_i32_plain_guard_and_raw(ctx.block(), value, &bits).0; crate::verify_facts::tick(ctx); let miss = ctx.new_block("int32.input.verify_miss"); let next = ctx.new_block("int32.input.admitted"); @@ -121,11 +115,9 @@ pub(crate) fn witness_exact(ctx: &mut crate::expr::FnCtx<'_>, value: &str, id: u } /// The i32 of a double stored into proven local `id`, if it is one: an exact -/// int32, so `fptosi`. The value is a proven Number, a canonical double (P3, -/// `facts_rep::canonical_numbers`); with `PERRY_WP_REP_P3=0` an INT32 box -/// converts through its payload. The HIR's release of a temp (`x = -/// undefined` after its last read, which wpfacts skips) keeps the ordinary -/// NaN-safe conversion. +/// int32, so `fptosi`. The value is a proven Number, a plain double. The +/// HIR's release of a temp (`x = undefined` after its last read, which +/// wpfacts skips) keeps the ordinary NaN-safe conversion. pub(crate) fn store_from_double( ctx: &mut crate::expr::FnCtx<'_>, value: &str, @@ -136,16 +128,7 @@ pub(crate) fn store_from_double( return None; } witness(ctx, value, id); - if super::facts_rep::canonical_numbers() { - return Some(ctx.block().fptosi(DOUBLE, value, I32)); - } - let blk = ctx.block(); - let bits = blk.bitcast_double_to_i64(value); - let is_int = super::emit_int32_box_test(blk, value); - let low = blk.trunc(I64, &bits, I32); - let safe = blk.select(crate::types::I1, &is_int, DOUBLE, "0.0", value); - let plain = blk.fptosi(DOUBLE, &safe, I32); - Some(blk.select(crate::types::I1, &is_int, I32, &low, &plain)) + Some(ctx.block().fptosi(DOUBLE, value, I32)) } #[cfg(test)] diff --git a/crates/perry-codegen/src/codegen/facts_number_locals.rs b/crates/perry-codegen/src/codegen/facts_number_locals.rs index 3fce334aee4..4acbdfa6d20 100644 --- a/crates/perry-codegen/src/codegen/facts_number_locals.rs +++ b/crates/perry-codegen/src/codegen/facts_number_locals.rs @@ -14,9 +14,8 @@ //! build checks every consumed claim, at the parameter entry and at each //! store into a proven `let` (`accept_let_value`). //! -//! A parameter the body never rebinds or mutates converts an INT32 box to its -//! plain double at entry, as a split region's Number parameter does, and is -//! then a Number by construction: the entry conversion is its only write. A +//! A parameter the body never rebinds or mutates is a Number by construction: +//! its argument, a plain double, is its only write. A //! `let` only answers `local_is_number` (`FnCtx::facts_number_locals`). It //! does not join `number_by_construction_locals`, whose members also promise //! that every write's operands are Numbers: `total += x.length` keeps @@ -29,7 +28,7 @@ use std::collections::{HashMap, HashSet}; use perry_hir::{Param, Stmt}; -use crate::types::{DOUBLE, I1, I32, I64}; +use crate::types::DOUBLE; pub(crate) fn enabled() -> bool { static ON: std::sync::OnceLock = std::sync::OnceLock::new(); @@ -168,22 +167,3 @@ pub(crate) fn none() -> &'static HashSet { static NONE: std::sync::OnceLock> = std::sync::OnceLock::new(); NONE.get_or_init(HashSet::new) } - -/// The plain double of a Number argument: an INT32 box converts, every other -/// Number is its own bits. -/// -/// P3 B1, C4 (DESIGN-p3 rev 3.1 §2.3): a Number the facts prove is a plain -/// double (every value reaching the parameter is a program Number or an -/// audited builtin NUM, `perry-wpfacts/NUM_SEEDS.md`), so with -/// `PERRY_FACTS_CANONICAL_NUMBERS` on the argument is taken as it is. The -/// entry witness (`emit_param_witnesses`) checks it in check mode. -pub(crate) fn emit_entry_number(blk: &mut crate::block::LlBlock, arg: &str) -> String { - if crate::expr::facts_canonical::enabled() { - return arg.to_string(); - } - let is_int = super::typed_entry::emit_int32_box_test(blk, arg); - let bits = blk.bitcast_double_to_i64(arg); - let low = blk.trunc(I64, &bits, I32); - let as_f64 = blk.sitofp(I32, &low, DOUBLE); - blk.select(I1, &is_int, DOUBLE, &as_f64, arg) -} diff --git a/crates/perry-codegen/src/codegen/facts_number_locals_tests.rs b/crates/perry-codegen/src/codegen/facts_number_locals_tests.rs index 8d70c30c757..3f12f294697 100644 --- a/crates/perry-codegen/src/codegen/facts_number_locals_tests.rs +++ b/crates/perry-codegen/src/codegen/facts_number_locals_tests.rs @@ -118,21 +118,12 @@ fn proven_bindings_take_raw_double_ops_and_own_no_root_slot() { 0, "a proven Number binding never holds a pointer:\n{body}" ); - // P3 B1, C4: a proven Number is a plain double (the NUM fact is audited, - // `perry-wpfacts/NUM_SEEDS.md`), so the parameter takes its argument's bits - // with no INT32 decode at entry; the entry witness checks it in check - // mode. `PERRY_FACTS_CANONICAL_NUMBERS=0` restores the decode. - if crate::expr::facts_canonical::enabled() { - assert!( - !body.contains(crate::nanbox::INT32_TAG_I64), - "a proven parameter takes its argument's bits at entry:\n{body}" - ); - } else { - assert!( - body.contains(crate::nanbox::INT32_TAG_I64), - "a proven parameter converts an INT32 box at entry:\n{body}" - ); - } + // A Number is a plain double, so the parameter takes its argument's bits + // with no decode at entry; the entry witness checks it in check mode. + assert!( + !body.contains(crate::nanbox::INT32_TAG_I64), + "a proven parameter takes its argument's bits at entry:\n{body}" + ); } #[test] diff --git a/crates/perry-codegen/src/codegen/facts_rep.rs b/crates/perry-codegen/src/codegen/facts_rep.rs index 9ae1af1f188..fdb965dfffe 100644 --- a/crates/perry-codegen/src/codegen/facts_rep.rs +++ b/crates/perry-codegen/src/codegen/facts_rep.rs @@ -12,20 +12,16 @@ //! //! `PERRY_WP_REP_P1=0` restores the collectors' decision everywhere. //! -//! Phase P3 (§10.5): a proven Number is a canonical double. Proven code never -//! creates an INT32 box, and an INT32 box from native code reaches only a ⊤ -//! node, never a proven one, so a proven Number's reader does not test the -//! INT32 tag. `PERRY_WP_REP_P3=0` restores the tag test. +//! Phase P3 (§10.5): a Number is a canonical double, its one encoding. No +//! reader tests for another. //! //! Phase P2 (§10.4), first slice: a parameter is a local like any other, so //! one the facts give `I32` takes the canonical-i32 slot at entry -//! (`bind_params`). Its arguments are already a plain double there -//! (`facts_number_locals::emit_entry_number`), and the I32 interval is +//! (`bind_params`). Its arguments are already a plain double there, and the I32 interval is //! exact, so the slot's value is one `fptosi`. The calling convention does //! not change yet. A method call whose every callee returns only Numbers //! (wpfacts `ModuleBindings::number_calls`, the call's return -//! representation) is a Number: a canonical double (P3), so its readers do -//! not test the INT32 tag (`number_call`). `PERRY_WP_REP_P2=0` keeps +//! representation) is a Number (`number_call`). `PERRY_WP_REP_P2=0` keeps //! parameters in their double slots and call results unproven. use std::collections::HashSet; @@ -51,17 +47,6 @@ pub(crate) fn local(ctx: &crate::expr::FnCtx<'_>, id: u32) -> Option { ctx.wpfacts.local_reps.get(&id).copied() } -/// Phase P3: proven Numbers are canonical doubles (module docs). -pub(crate) fn canonical_numbers() -> bool { - static ON: std::sync::OnceLock = std::sync::OnceLock::new(); - *ON.get_or_init(|| { - !matches!( - std::env::var("PERRY_WP_REP_P3").as_deref(), - Ok("0") | Ok("off") | Ok("false") - ) - }) -} - /// Phase P2 (module docs). pub(crate) fn typed_params() -> bool { static ON: std::sync::OnceLock = std::sync::OnceLock::new(); @@ -94,7 +79,6 @@ fn claimed_call(ctx: &crate::expr::FnCtx<'_>, callee: &perry_hir::Expr) -> Optio return None; }; let usable = typed_params() - && canonical_numbers() && !ctx.wpfacts_off && !ctx.wpfacts.interp_guard && *byte_offset != 0 @@ -103,8 +87,8 @@ fn claimed_call(ctx: &crate::expr::FnCtx<'_>, callee: &perry_hir::Expr) -> Optio } /// Check mode: `value`, the result of the call through `callee` that -/// `number_call` proves a Number, is a canonical double, neither a tagged -/// value nor an INT32 box. A miss reports the false proof and exits 86. +/// `number_call` proves a Number, is a plain double, not a tagged value. A +/// miss reports the false proof and exits 86. pub(crate) fn witness_number_call( ctx: &mut crate::expr::FnCtx<'_>, callee: &perry_hir::Expr, diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs index 4ed23f1a067..9472fbc621a 100644 --- a/crates/perry-codegen/src/codegen/function.rs +++ b/crates/perry-codegen/src/codegen/function.rs @@ -29,10 +29,7 @@ use super::typed_abi::{ typed_i1_function_name, typed_i32_function_name, typed_param_reps_for_params, typed_string_function_name, TypedFunctionTrampolineKind, TypedParamRep, }; -use super::typed_entry::{ - emit_tiered_entry_dispatch, emit_typed_arg_to_raw_after_entry_tier, typed_entry_arg_guard, - EntryArgGuard, -}; +use super::typed_entry::{emit_tiered_entry_dispatch, typed_entry_arg_guard, EntryArgGuard}; /// Internal body name for a self-recursive allocator whose arena-state pointer /// is threaded through recursive calls (#8591). @@ -236,7 +233,7 @@ fn emit_typed_public_trampoline_fast_value( let raw_args: Vec = arg_names .iter() .zip(arg_reps.iter()) - .map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg)) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) .collect(); let typed_args: Vec<(LlvmType, &str)> = raw_args .iter() @@ -249,7 +246,7 @@ fn emit_typed_public_trampoline_fast_value( let raw_args: Vec = arg_names .iter() .zip(arg_reps.iter()) - .map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg)) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) .collect(); let typed_args: Vec<(LlvmType, &str)> = raw_args .iter() @@ -263,7 +260,7 @@ fn emit_typed_public_trampoline_fast_value( let raw_args: Vec = arg_names .iter() .zip(arg_reps.iter()) - .map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg)) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) .collect(); let typed_args: Vec<(LlvmType, &str)> = raw_args .iter() @@ -278,7 +275,7 @@ fn emit_typed_public_trampoline_fast_value( let raw_args: Vec = arg_names .iter() .zip(arg_reps.iter()) - .map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg)) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) .collect(); let typed_args: Vec<(LlvmType, &str)> = raw_args .iter() @@ -962,15 +959,6 @@ pub(super) fn compile_function( } _ => {} } - // A split region's Number parameter (its only caller proved a - // Number) and a facts-proven one may arrive as an int32 box; the - // body's Number proof means a plain double, so convert once here. - let arg_name = - if region_number_params.contains(&p.id) || facts_number_params.contains(&p.id) { - super::facts_number_locals::emit_entry_number(blk, &arg_name) - } else { - arg_name - }; let slot = super::arguments::store_param_slot(blk, p, &boxed_vars, &arg_name); param_roots.push((p.id, slot.clone())); map.insert(p.id, slot); diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index b725e8e2ccd..8621d21b8ef 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -492,12 +492,7 @@ pub(super) fn compile_method( format!("%arg{}", p.id) }; let number = facts_number_params.contains(&p.id); - let value = if number { - super::facts_number_locals::emit_entry_number(blk, &arg_name) - } else { - arg_name.clone() - }; - let slot = super::arguments::store_param_slot(blk, p, &method_boxed_vars, &value); + let slot = super::arguments::store_param_slot(blk, p, &method_boxed_vars, &arg_name); if number { shadow_slot_map.remove(&p.id); } else if let Some(slot_idx) = shadow_slot_map.get(&p.id).copied() { @@ -509,11 +504,9 @@ pub(super) fn compile_method( map.insert(p.id, slot); if index_param_ids.contains(&p.id) { // Every route proved the signed-i32 value class before this - // body. A raw parameter is that value; otherwise a statically - // proven route normally passes a plain double and the guarded - // stable public entry may also pass Perry's canonical INT32 - // NaN-box, so use the shared already-guarded conversion rather - // than `fptosi` (which cannot consume a tagged INT32 value). + // body. A raw parameter is that value; otherwise every route + // passes an exact int32 double, which the shared + // already-guarded conversion reads with one `fptosi`. let raw_i32 = if raw_index { super::index_entry::raw_param_name(p.id) } else { diff --git a/crates/perry-codegen/src/codegen/method_trampolines.rs b/crates/perry-codegen/src/codegen/method_trampolines.rs index a8a5aa0efc0..5c1f59e9c33 100644 --- a/crates/perry-codegen/src/codegen/method_trampolines.rs +++ b/crates/perry-codegen/src/codegen/method_trampolines.rs @@ -6,14 +6,11 @@ use crate::module::LlModule; use crate::types::{LlvmType, DOUBLE, I1, I32, I64}; use super::typed_abi::{ - typed_f64_method_name, typed_i1_method_name, typed_i32_method_name, + emit_typed_arg_to_raw, typed_f64_method_name, typed_i1_method_name, typed_i32_method_name, typed_param_reps_for_params, typed_string_method_name, TypedFunctionTrampolineKind, TypedParamRep, }; -use super::typed_entry::{ - emit_tiered_entry_dispatch, emit_typed_arg_to_raw_after_entry_tier, typed_entry_arg_guard, - EntryArgGuard, -}; +use super::typed_entry::{emit_tiered_entry_dispatch, typed_entry_arg_guard, EntryArgGuard}; fn emit_typed_fast_value( blk: &mut crate::block::LlBlock, @@ -27,7 +24,7 @@ fn emit_typed_fast_value( let raw_args: Vec = arg_names .iter() .zip(arg_reps.iter()) - .map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg)) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) .collect(); let typed_args: Vec<(LlvmType, &str)> = raw_args .iter() @@ -40,7 +37,7 @@ fn emit_typed_fast_value( let raw_args: Vec = arg_names .iter() .zip(arg_reps.iter()) - .map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg)) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) .collect(); let typed_args: Vec<(LlvmType, &str)> = raw_args .iter() @@ -54,7 +51,7 @@ fn emit_typed_fast_value( let raw_args: Vec = arg_names .iter() .zip(arg_reps.iter()) - .map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg)) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) .collect(); let typed_args: Vec<(LlvmType, &str)> = raw_args .iter() @@ -69,7 +66,7 @@ fn emit_typed_fast_value( let raw_args: Vec = arg_names .iter() .zip(arg_reps.iter()) - .map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg)) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) .collect(); let typed_args: Vec<(LlvmType, &str)> = raw_args .iter() @@ -243,10 +240,10 @@ pub(super) fn emit_guarded_nonnegative_index( let mut raw_index_values: Vec = Vec::with_capacity(index_param_ids.len()); for (index, param_id) in index_param_ids.iter().enumerate() { let arg = format!("%arg{param_id}"); - let (exact_i32, raw_i32) = super::typed_abi::emit_typed_i32_guard_and_raw( - wf.block_mut(guard_block_idx).unwrap(), - &arg, - ); + let guard_block = wf.block_mut(guard_block_idx).unwrap(); + let bits = guard_block.bitcast_double_to_i64(&arg); + let (exact_i32, raw_i32) = + super::typed_abi::emit_typed_i32_plain_guard_and_raw(guard_block, &arg, &bits); raw_index_values.push(raw_i32.clone()); let admitted_idx = wf.num_blocks(); let admitted_label = wf diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index de15346772e..b590b022e58 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -361,8 +361,8 @@ mod typed_abi; mod typed_abi_opt_report; mod typed_entry; pub(crate) use typed_entry::{ - emit_int32_bits_test, emit_int32_box_test, emit_ordered_number_test, - emit_plain_number_bits_test, emit_plain_number_test, typed_entry_arg_guard, + emit_ordered_number_test, emit_plain_number_bits_test, emit_plain_number_test, + typed_entry_arg_guard, }; #[cfg(test)] mod unknown_func_tests; @@ -386,11 +386,9 @@ pub(crate) use spec_abi::{ spec_abi_enabled, spec_entry_name, spec_function_name, SpecDispatch, SpecFnPlan, }; pub(crate) use typed_abi::{ - emit_typed_arg_guard, emit_typed_arg_to_raw, emit_typed_f64_guard, - emit_typed_f64_to_raw_guarded, emit_typed_i1_guard, emit_typed_i32_guard_and_raw, - emit_typed_i32_plain_guard_and_raw, emit_typed_i32_raw_assuming_guarded, - emit_typed_string_guard, generic_closure_body_name, generic_function_body_name, - generic_method_body_name, nonnegative_index_fast_array_method_name, + emit_typed_arg_guard, emit_typed_arg_to_raw, emit_typed_f64_guard, emit_typed_i1_guard, + emit_typed_i32_plain_guard_and_raw, emit_typed_string_guard, generic_closure_body_name, + generic_function_body_name, generic_method_body_name, nonnegative_index_fast_array_method_name, nonnegative_index_fast_array_params, nonnegative_index_method_name, typed_arg_is_guard_candidate, typed_f64_closure_name, typed_f64_function_name, typed_f64_method_name, typed_f64_receiver_method_info, typed_f64_receiver_method_name, diff --git a/crates/perry-codegen/src/codegen/signature_entry.rs b/crates/perry-codegen/src/codegen/signature_entry.rs index 204807934b7..97a900df5ff 100644 --- a/crates/perry-codegen/src/codegen/signature_entry.rs +++ b/crates/perry-codegen/src/codegen/signature_entry.rs @@ -702,7 +702,7 @@ pub(crate) fn try_lower_typed_method_call( /// The boxed entry: the public `F` of a typed function (§3.3). Each argument /// takes its literal default when it is `undefined` (§4.1), is decoded to -/// its parameter's representation (§4.2: `emit_entry_number`, then one +/// its parameter's representation (§4.2: a Number is its own bits, then one /// `fptosi` for I32), is witnessed in check mode (R2), and the entry /// tail-calls `F$sig`; an I32 result goes back as a canonical double (R4). pub(crate) fn emit_boxed_entry( @@ -869,7 +869,6 @@ fn emit_entry( raw.push((DOUBLE, value)); continue; } - let value = super::facts_number_locals::emit_entry_number(blk, &value); if let Some(site) = &sites[i] { let ok = emit_rep_test(blk, rep, sig.ranges[i], &value).unwrap(); blk.call_void("perry_verify_facts_tick", &[]); diff --git a/crates/perry-codegen/src/codegen/typed_abi.rs b/crates/perry-codegen/src/codegen/typed_abi.rs index e8b9b14665a..4932bd6b3ec 100644 --- a/crates/perry-codegen/src/codegen/typed_abi.rs +++ b/crates/perry-codegen/src/codegen/typed_abi.rs @@ -66,22 +66,18 @@ impl TypedParamRep { } } -/// Inline the exact contract of runtime `js_typed_i32_arg_guard` and return -/// both its predicate and the decoded signed lane. +/// The int32 lane's inline entry predicate, and the decoded signed lane. A +/// Number is a plain double, so the value admits when it round-trips the +/// SATURATING conversion exactly. Every NaN, and so every NaN-boxed tag +/// pattern, converts to 0 and compares unequal, and an out-of-range value +/// saturates and compares unequal, so no separate tag or range test is +/// needed. `-0` round-trips as `+0` and is excluded by its bits: the i32 lane +/// cannot carry it. `bits` is `value`'s word. /// /// Typed/nonnegative method dispatch is itself hot enough that two leaf calls /// (guard, then conversion) dominate small bodies such as ECS `SparseSet.has`. /// Keep the proof in generated IR so LLVM can reuse `raw` for the subsequent -/// nonnegative test and specialized call. The safe-value select is -/// load-bearing: `fptosi` is poison for tagged NaNs, infinities, and -/// out-of-range doubles even when a later select would choose the tagged arm. -/// The plain-double half of [`emit_typed_i32_guard_and_raw`], for a call site -/// that already proves its value a canonical double: the value admits when it -/// round-trips the SATURATING conversion exactly. Every NaN, and so every -/// NaN-boxed tag pattern, converts to 0 and compares unequal, and an -/// out-of-range value saturates and compares unequal, so no separate tag or -/// range test is needed. `-0` round-trips as `+0` and is excluded by its -/// bits: the i32 lane cannot carry it. +/// nonnegative test and specialized call. pub(crate) fn emit_typed_i32_plain_guard_and_raw( blk: &mut crate::block::LlBlock, value: &str, @@ -96,38 +92,14 @@ pub(crate) fn emit_typed_i32_plain_guard_and_raw( (blk.and(I1, &integral, ¬_negative_zero), raw) } -pub(crate) fn emit_typed_i32_guard_and_raw( - blk: &mut crate::block::LlBlock, - value: &str, -) -> (String, String) { - use crate::types::{I1, I32, I64}; - - let bits = blk.bitcast_double_to_i64(value); - let is_tagged = super::emit_int32_bits_test(blk, &bits); - - let (plain_ok, plain_raw) = emit_typed_i32_plain_guard_and_raw(blk, value, &bits); - let admitted = blk.or(I1, &is_tagged, &plain_ok); - - let tagged_raw = blk.trunc(I64, &bits, I32); - let raw = blk.select(I1, &is_tagged, I32, &tagged_raw, &plain_raw); - (admitted, raw) -} - /// Decode a value after an enclosing entry guard has established the contract -/// above. The select again shields `fptosi` from canonical INT32 NaN-boxes; -/// no range/integrality work is repeated in the specialized body. +/// above: an exact int32 double, so one `fptosi`. No range/integrality work is +/// repeated in the specialized body. pub(crate) fn emit_typed_i32_raw_assuming_guarded( blk: &mut crate::block::LlBlock, value: &str, ) -> String { - use crate::types::{DOUBLE, I1, I32, I64}; - - let bits = blk.bitcast_double_to_i64(value); - let is_tagged = super::emit_int32_bits_test(blk, &bits); - let safe_plain = blk.select(I1, &is_tagged, DOUBLE, "0.0", value); - let plain_raw = blk.fptosi(DOUBLE, &safe_plain, I32); - let tagged_raw = blk.trunc(I64, &bits, I32); - blk.select(I1, &is_tagged, I32, &tagged_raw, &plain_raw) + blk.fptosi(crate::types::DOUBLE, value, crate::types::I32) } pub(crate) fn typed_param_rep_for_type(ty: &Type) -> Option { @@ -239,7 +211,10 @@ pub(crate) fn emit_typed_arg_guard( ) -> String { match rep { TypedParamRep::F64 => emit_typed_f64_guard(blk, arg), - TypedParamRep::I32 => emit_typed_i32_guard_and_raw(blk, arg).0, + TypedParamRep::I32 => { + let bits = blk.bitcast_double_to_i64(arg); + emit_typed_i32_plain_guard_and_raw(blk, arg, &bits).0 + } TypedParamRep::I1 => emit_typed_i1_guard(blk, arg), TypedParamRep::StringRef => emit_typed_string_guard(blk, arg), } @@ -266,9 +241,8 @@ pub(crate) fn emit_typed_string_guard(blk: &mut crate::block::LlBlock, arg: &str blk.or(I1, &heap, &short) } -/// Inline the exact contract of runtime `js_typed_f64_arg_guard` -/// (`JSValue::is_number || JSValue::is_int32`), the way -/// [`emit_typed_i32_guard_and_raw`] already does for the i32 lane. +/// The Number lane's inline entry predicate (`JSValue::is_number`), the way +/// [`emit_typed_i32_plain_guard_and_raw`] does for the i32 lane. /// /// The public entry of every function with a boxed-double clone runs this /// guard on each numeric parameter before dispatching; a one-line predicate @@ -276,33 +250,17 @@ pub(crate) fn emit_typed_string_guard(blk: &mut crate::block::LlBlock, arg: &str /// for a compare it could have done in four instructions. Same predicate, /// same routing decision. pub(crate) fn emit_typed_f64_guard(blk: &mut crate::block::LlBlock, arg: &str) -> String { - use crate::types::{I1, I64}; + use crate::types::I64; let bits = blk.bitcast_double_to_i64(arg); // The sign-closed numeric proof excludes both the positive tag band and // its negative mirror (including HANDLE_TAG). Ingress canonicalization // ensures numeric payload NaNs cannot reach this boxed entry. let magnitude = blk.and(I64, &bits, "9223372036854775807"); - let is_plain_number = blk.icmp_ult( + blk.icmp_ult( I64, &magnitude, &crate::nanbox::i64_literal(crate::nanbox::SHORT_STRING_TAG), - ); - // JSValue::is_int32: the INT32 tag with any payload. - let is_int32 = super::emit_int32_bits_test(blk, &bits); - blk.or(I1, &is_plain_number, &is_int32) -} - -/// Inline `js_typed_f64_arg_to_raw` for a value the F64 guard admitted: an -/// INT32-tagged value converts its low lane, anything else is already the -/// double the clone wants. Only valid after [`emit_typed_f64_guard`] passed -/// (a tagged non-number would otherwise reach the clone as its raw bits). -pub(crate) fn emit_typed_f64_to_raw_guarded(blk: &mut crate::block::LlBlock, arg: &str) -> String { - use crate::types::{DOUBLE, I1, I32, I64}; - let bits = blk.bitcast_double_to_i64(arg); - let is_int32 = super::emit_int32_bits_test(blk, &bits); - let low = blk.trunc(I64, &bits, I32); - let converted = blk.sitofp(I32, &low, DOUBLE); - blk.select(I1, &is_int32, DOUBLE, &converted, arg) + ) } pub(crate) fn emit_typed_arg_to_raw( @@ -311,7 +269,8 @@ pub(crate) fn emit_typed_arg_to_raw( arg: &str, ) -> String { match rep { - TypedParamRep::F64 => emit_typed_f64_to_raw_guarded(blk, arg), + // The F64 guard admitted a plain double, the value the clone wants. + TypedParamRep::F64 => arg.to_string(), TypedParamRep::I32 => emit_typed_i32_raw_assuming_guarded(blk, arg), // `js_typed_i1_arg_to_raw`: the guard admitted only the two boolean // tags, so the raw bit is plain tag identity. diff --git a/crates/perry-codegen/src/codegen/typed_abi/tests.rs b/crates/perry-codegen/src/codegen/typed_abi/tests.rs index 02cca2370f6..f9dc3da2635 100644 --- a/crates/perry-codegen/src/codegen/typed_abi/tests.rs +++ b/crates/perry-codegen/src/codegen/typed_abi/tests.rs @@ -337,6 +337,7 @@ fn numeric_captures_share_the_actual_value_ordered_entry_tier() { .expect("public closure entry"); let entry = public.split("typed_closure_public.fast").next().unwrap(); assert_eq!(entry.matches("fcmp ord double").count(), 2, "{public}"); - assert!(public.contains("typed_closure_public.int32"), "{public}"); + assert!(public.contains("typed_closure_public.nan"), "{public}"); + assert!(!public.contains("int32_convert"), "{public}"); assert!(public.contains("$generic"), "{public}"); } diff --git a/crates/perry-codegen/src/codegen/typed_entry.rs b/crates/perry-codegen/src/codegen/typed_entry.rs index 17c0f000120..37f620e5a1f 100644 --- a/crates/perry-codegen/src/codegen/typed_entry.rs +++ b/crates/perry-codegen/src/codegen/typed_entry.rs @@ -1,20 +1,16 @@ //! Two-tier public-entry dispatch for typed and specialized clones. //! //! A public entry decides per call whether its arguments satisfy the clone's -//! representation contract. For a Number lane that contract admits both plain -//! doubles and INT32-tagged boxes, and the clone wants a plain double. Deciding -//! both in one predicate and normalizing unconditionally made every call pay -//! the rare int32 case first: `addNum(a, b)` ran a 13-instruction combined -//! guard plus two `scvtf`/`fcsel` normalizations before its `fadd`. +//! representation contract. For a Number lane that contract admits the plain +//! double, the one encoding a Number has, which is also what the clone wants. //! //! Tier 1 admits ordered doubles only (`fcmp ord`, which x86-64 lowers to //! `ucomisd; jnp`) and passes them through untouched. Every boxed tag is a NaN //! pattern, so an ordered value is always a plain number and no bit test is //! needed on the common path. Only a NaN-patterned argument reaches tier 2, //! which runs the sign-closed plain-number test (canonical and payload NaNs -//! that are not tags) and the int32-box test, converting an int32 box to the -//! equal double. Everything else takes the unchanged generic body. The routing -//! decision per value is identical to the one-tier form. +//! that are not tags). Everything else takes the unchanged generic body. The +//! routing decision per value is identical to the one-tier form. //! //! Tier 1 is a real branch, not a predicate folded into tier 2: LLVM rewrites //! `or(fcmp ord, masked compare)` and the shift/rotate forms of the @@ -23,7 +19,7 @@ use crate::block::LlBlock; use crate::function::LlFunction; -use crate::types::{DOUBLE, I1, I32, I64}; +use crate::types::{DOUBLE, I1, I64}; /// Largest bit pattern (as a signed i64) that is a plain JS Number. Perry's /// own tags occupy the positive-qNaN top words `0x7FF9..=0x7FFF`; every other @@ -33,8 +29,7 @@ pub(crate) const PLAIN_NUMBER_MAX_I64: &str = "9221401712017801215"; // 0x7FF8_F /// Entry contract for one argument. #[derive(Debug, Clone, Default)] pub(crate) struct EntryArgGuard { - /// Number lane: plain doubles pass in tier 1, int32 boxes are converted to - /// the equal double in tier 2. + /// Number lane: ordered doubles pass in tier 1, NaN Numbers in tier 2. pub number: bool, /// Any other exact predicate (typed leaf guard or descriptor validator), /// already emitted in the entry block. @@ -65,21 +60,6 @@ pub(crate) fn emit_ordered_number_test(blk: &mut LlBlock, value: &str) -> String blk.fcmp("ord", value, "0.0") } -/// `JSValue::is_int32` — the INT32 tag with any 32-bit payload. -pub(crate) fn emit_int32_box_test(blk: &mut LlBlock, value: &str) -> String { - let bits = blk.bitcast_double_to_i64(value); - emit_int32_bits_test(blk, &bits) -} - -/// THE emitted int32-Number test on raw bits (`perry_runtime::value:: -/// is_int32_bits`): tag `0x7FFE` with bits 32..47 clear. A top-16-only test -/// reads a `C.prototype` ref or an internal marker as an int32 Number. -pub(crate) fn emit_int32_bits_test(blk: &mut LlBlock, bits: &str) -> String { - let identity_mask = crate::nanbox::i64_literal(!crate::nanbox::INT32_MASK); - let identity = blk.and(I64, bits, &identity_mask); - blk.icmp_eq(I64, &identity, crate::nanbox::INT32_TAG_I64) -} - /// Entry contract for a typed-ABI lane: Number lanes use the two tiers, the /// other representations keep their exact inline predicate. pub(crate) fn typed_entry_arg_guard( @@ -99,19 +79,6 @@ pub(crate) fn typed_entry_arg_guard( } } -/// Raw clone argument for a value that passed [`emit_tiered_entry_dispatch`]: -/// a Number lane already is the plain double the clone wants. -pub(crate) fn emit_typed_arg_to_raw_after_entry_tier( - blk: &mut LlBlock, - rep: super::typed_abi::TypedParamRep, - value: &str, -) -> String { - match rep { - super::typed_abi::TypedParamRep::F64 => value.to_string(), - _ => super::typed_abi::emit_typed_arg_to_raw(blk, rep, value), - } -} - fn and_all(blk: &mut LlBlock, parts: Vec) -> Option { let mut acc: Option = None; for part in parts { @@ -126,10 +93,9 @@ fn and_all(blk: &mut LlBlock, parts: Vec) -> Option { /// Emit the dispatch starting in block 0 of `wf`, which must be unterminated /// and already hold every `exact` guard and `extra_guard`. /// -/// `fast` receives one value per argument: the original argument, or for a -/// Number lane the plain double (tier 1) / converted int32 box (tier 2), merged -/// by a phi so the clone is called from exactly one site and stays inlinable. -/// It returns the value to return. +/// `fast` receives the arguments. Both tiers branch to its one block, so the +/// clone is called from exactly one site and stays inlinable. It returns the +/// value to return. pub(crate) fn emit_tiered_entry_dispatch( wf: &mut LlFunction, label_prefix: &str, @@ -142,7 +108,7 @@ pub(crate) fn emit_tiered_entry_dispatch( debug_assert_eq!(args.len(), guards.len()); let any_number = guards.iter().any(|g| g.number); - let (entry_label, tier1_guard) = { + let tier1_guard = { let blk = wf.block_mut(0).unwrap(); let mut parts = Vec::new(); for (arg, guard) in args.iter().zip(guards) { @@ -156,8 +122,7 @@ pub(crate) fn emit_tiered_entry_dispatch( if let Some(extra) = &extra_guard { parts.push(extra.clone()); } - let tier1 = and_all(blk, parts); - (blk.label.clone(), tier1) + and_all(blk, parts) }; let Some(tier1_guard) = tier1_guard else { @@ -172,21 +137,14 @@ pub(crate) fn emit_tiered_entry_dispatch( .create_block(&format!("{label_prefix}.fast")) .label .clone(); - let tier2 = if any_number { + let tier2 = any_number.then(|| { let idx = wf.num_blocks(); let label = wf - .create_block(&format!("{label_prefix}.int32")) - .label - .clone(); - let norm_idx = wf.num_blocks(); - let norm_label = wf - .create_block(&format!("{label_prefix}.int32_convert")) + .create_block(&format!("{label_prefix}.nan")) .label .clone(); - Some((idx, label, norm_idx, norm_label)) - } else { - None - }; + (idx, label) + }); let fallback_idx = wf.num_blocks(); let fallback_label = wf .create_block(&format!("{label_prefix}.fallback")) @@ -195,75 +153,36 @@ pub(crate) fn emit_tiered_entry_dispatch( let miss_label = tier2 .as_ref() - .map(|(_, label, _, _)| label.clone()) + .map(|(_, label)| label.clone()) .unwrap_or_else(|| fallback_label.clone()); wf.block_mut(0) .unwrap() .cond_br(&tier1_guard, &fast_label, &miss_label); // Tier 2: some Number argument is a NaN pattern. Admit the plain ones - // (canonical or payload NaN outside the tag band, either sign) and int32 - // boxes; convert the boxes and join the fast block. - let converted: Option<(String, Vec)> = tier2.map(|(idx, _, norm_idx, norm_label)| { - let mut int32_tests: Vec> = Vec::with_capacity(args.len()); - { - let blk = wf.block_mut(idx).unwrap(); - let mut parts = Vec::new(); - for (arg, guard) in args.iter().zip(guards) { - if guard.number { - let plain = emit_plain_number_test(blk, arg); - let int32 = emit_int32_box_test(blk, arg); - parts.push(blk.or(I1, &plain, &int32)); - int32_tests.push(Some(int32)); - } else { - int32_tests.push(None); - } - if let Some(exact) = &guard.exact { - parts.push(exact.clone()); - } + // (canonical or payload NaN outside the tag band, either sign); they are + // already the double the clone wants. + if let Some((idx, _)) = tier2 { + let blk = wf.block_mut(idx).unwrap(); + let mut parts = Vec::new(); + for (arg, guard) in args.iter().zip(guards) { + if guard.number { + parts.push(emit_plain_number_test(blk, arg)); } - if let Some(extra) = &extra_guard { - parts.push(extra.clone()); + if let Some(exact) = &guard.exact { + parts.push(exact.clone()); } - let ok = and_all(blk, parts).expect("tier 2 has at least one number lane"); - blk.cond_br(&ok, &norm_label, &fallback_label); } - let blk = wf.block_mut(norm_idx).unwrap(); - let values: Vec = args - .iter() - .zip(&int32_tests) - .map(|(arg, int32)| match int32 { - Some(is_int32) => { - let bits = blk.bitcast_double_to_i64(arg); - let low = blk.trunc(I64, &bits, I32); - let converted = blk.sitofp(I32, &low, DOUBLE); - blk.select(I1, is_int32, DOUBLE, &converted, arg) - } - None => arg.clone(), - }) - .collect(); - blk.br(&fast_label); - (norm_label, values) - }); + if let Some(extra) = &extra_guard { + parts.push(extra.clone()); + } + let ok = and_all(blk, parts).expect("tier 2 has at least one number lane"); + blk.cond_br(&ok, &fast_label, &fallback_label); + } { let blk = wf.block_mut(fast_idx).unwrap(); - let values: Vec = match &converted { - Some((norm_label, converted_values)) => args - .iter() - .zip(guards) - .zip(converted_values) - .map(|((arg, guard), conv)| { - if guard.number { - blk.phi(DOUBLE, &[(arg, &entry_label), (conv, norm_label)]) - } else { - arg.clone() - } - }) - .collect(), - None => args.to_vec(), - }; - let value = fast(blk, &values); + let value = fast(blk, args); blk.ret(DOUBLE, &value); } diff --git a/crates/perry-codegen/src/expr/compare.rs b/crates/perry-codegen/src/expr/compare.rs index e0769082b64..daf41238797 100644 --- a/crates/perry-codegen/src/expr/compare.rs +++ b/crates/perry-codegen/src/expr/compare.rs @@ -507,7 +507,7 @@ pub(crate) fn lower_string_literal_strict_eq_with_others( /// **misses**, so a fast path that settles only the hit is worth nothing — /// each case below settles one direction of the real traffic. /// -/// Three cases leave without a call. Each is an exact restatement of what +/// Two cases leave without a call. Each is an exact restatement of what /// `js_jsvalue_equals` computes for that input, not an approximation: /// /// * **identical bits** ⇒ equal, *unless* the value is a plain (untagged) @@ -520,7 +520,6 @@ pub(crate) fn lower_string_literal_strict_eq_with_others( /// encoding is canonical — same bytes and same length give the same bit /// pattern — which is the argument `lower_string_strict_eq_inline` and the /// runtime's own both-short-string arm already rely on. -/// * **both INT32, different bits** ⇒ different integers, same argument. /// /// Distinct `POINTER_TAG` values always take the runtime call. Not every /// pointer-tag payload is a GC allocation: registered and well-known symbols, @@ -766,11 +765,7 @@ fn lower_strict_eq_inline_any( .block() .icmp_eq(I64, &r_tag, crate::nanbox::SHORT_STRING_TAG_TOP16_I64); let both_sso = ctx.block().and(I1, &l_sso, &r_sso); - let l_i32 = crate::codegen::emit_int32_bits_test(ctx.block(), &l_bits); - let r_i32 = crate::codegen::emit_int32_bits_test(ctx.block(), &r_bits); - let both_i32 = ctx.block().and(I1, &l_i32, &r_i32); - let canonical = ctx.block().or(I1, &both_sso, &both_i32); - ctx.block().cond_br(&canonical, &false_l, &slow_l); + ctx.block().cond_br(&both_sso, &false_l, &slow_l); ctx.current_block = slow_idx; let slow_res = ctx @@ -815,26 +810,14 @@ fn is_plain_number_or_undefined(ctx: &FnCtx<'_>, expr: &Expr) -> bool { } } -/// Normalize Perry's compact INT32 immediate to an ordinary IEEE double. -/// Every other bit pattern is left unchanged. That makes a subsequent `fcmp` -/// exact for an arbitrary JS value against a proven Number: non-number tags -/// remain NaNs (and therefore compare unequal), while both Number encodings -/// participate in the numeric comparison. -fn normalize_int32_immediate(ctx: &mut FnCtx<'_>, value: &str) -> String { - let bits = ctx.block().bitcast_double_to_i64(value); - let is_i32 = crate::codegen::emit_int32_bits_test(ctx.block(), &bits); - let raw = ctx.block().trunc(I64, &bits, I32); - let decoded = ctx.block().sitofp(I32, &raw, DOUBLE); - ctx.block().select(I1, &is_i32, DOUBLE, &decoded, value) -} - /// Strict equality where exactly one operand is a proven Number. /// -/// An i32-proven operand needs one numeric comparison after compact INT32 -/// normalization. General Numbers keep the encoding comparison: encode the -/// proven operand rather than converting the varying operand, preserving -/// fractional byte-dependent recurrences. Saturation and the round trip reject -/// NaN, infinity, out-of-range Numbers and fractions on the compact arm. +/// A Number has one encoding, the plain double, and every other JS value is a +/// NaN bit pattern. So an arbitrary value against a proven Number is one +/// numeric comparison: a non-number compares unequal. An i32-proven operand +/// takes the `fcmp`. General Numbers keep the comparison on the bits, which +/// leaves the varying operand out of the floating-point domain and so +/// preserves fractional byte-dependent recurrences. fn lower_strict_eq_against_number( ctx: &mut FnCtx<'_>, dynamic_expr: &Expr, @@ -844,21 +827,16 @@ fn lower_strict_eq_against_number( dynamic_is_left: bool, integer_operand: bool, ) -> String { - let plain = is_plain_number_or_undefined(ctx, dynamic_expr); - // Raw checked bytes must not take compact-integer normalization. Keep - // this admission compatible with main's integer-operand emitter. - let integer_operand = integer_operand && !plain; - let _ = integer_operand; + // Raw checked bytes keep the bits comparison below. + let integer_operand = integer_operand && !is_plain_number_or_undefined(ctx, dynamic_expr); let (dynamic, number) = if dynamic_is_left { (l, r) } else { (r, l) }; if integer_operand { - let dynamic = normalize_int32_immediate(ctx, dynamic); - let number = normalize_int32_immediate(ctx, number); let predicate = if matches!(op, CompareOp::Ne) { "une" } else { "oeq" }; - let bit = ctx.block().fcmp(predicate, &dynamic, &number); + let bit = ctx.block().fcmp(predicate, dynamic, number); let tagged = ctx.block().select( I1, &bit, @@ -868,24 +846,7 @@ fn lower_strict_eq_against_number( ); return ctx.block().bitcast_i64_to_double(&tagged); } - let number = normalize_int32_immediate(ctx, number); - let number = number.as_str(); let bits = ctx.block().bitcast_double_to_i64(dynamic); - let compact = if plain { - None - } else { - let is_i32 = crate::codegen::emit_int32_bits_test(ctx.block(), &bits); - let integer = ctx - .block() - .call(I32, "llvm.fptosi.sat.i32.f64", &[(DOUBLE, number)]); - let roundtrip = ctx.block().sitofp(I32, &integer, DOUBLE); - let exact = ctx.block().fcmp("oeq", &roundtrip, number); - let payload = ctx.block().trunc(I64, &bits, I32); - let same_integer = ctx.block().icmp_eq(I32, &payload, &integer); - let compact = ctx.block().and(I1, &is_i32, &exact); - let compact = ctx.block().and(I1, &compact, &same_integer); - Some(compact) - }; // Raw doubles compare by bits, except that both signed zeros are equal and // NaN is unequal to itself. Keeping the NaN check on the proven operand // avoids a floating-point round trip through the varying value as well. @@ -898,11 +859,7 @@ fn lower_strict_eq_against_number( let both_zero = ctx.block().and(I1, &dynamic_zero, &number_zero); let raw = ctx.block().or(I1, &same_bits, &both_zero); let ordered = ctx.block().fcmp("oeq", number, number); - let raw = ctx.block().and(I1, &raw, &ordered); - let equal = match compact { - Some(compact) => ctx.block().or(I1, &raw, &compact), - None => raw, - }; + let equal = ctx.block().and(I1, &raw, &ordered); let bit = if matches!(op, CompareOp::Ne) { ctx.block().xor(I1, &equal, "true") } else { @@ -921,7 +878,7 @@ fn lower_strict_eq_against_number( /// Inline the primitive-number arm of Abstract Relational Comparison when one /// operand is already proven numeric. /// -/// Ordinary Numbers, compact INT32 values, `undefined`, `null`, and booleans +/// Ordinary Numbers, `undefined`, `null`, and booleans /// have a side-effect-free `ToPrimitive`/`ToNumber` result. Strings, BigInts, /// Symbols, and objects retain the complete runtime helper because their /// coercion either follows different rules or may execute user code. @@ -931,7 +888,6 @@ fn lower_relational_against_number( l: &str, r: &str, dynamic_is_left: bool, - proven_expr: &Expr, fallback_fn: &str, ) -> String { let dynamic = if dynamic_is_left { l } else { r }; @@ -939,7 +895,6 @@ fn lower_relational_against_number( // Use the same raw-Number admission as the fully dynamic comparison. // Negative IEEE values and canonical NaNs precede the positive tag band. let is_plain_number = emit_is_unboxed_number(ctx, &bits); - let is_i32 = crate::codegen::emit_int32_bits_test(ctx.block(), &bits); // undefined/null/false/true are four consecutive immediate values. // A range decides admission without separately classifying each value. debug_assert_eq!(crate::nanbox::TAG_UNDEFINED + 3, crate::nanbox::TAG_TRUE); @@ -950,8 +905,7 @@ fn lower_relational_against_number( let zero_offset = ctx.block().sub(I64, &bits, crate::nanbox::TAG_NULL_I64); let is_zero = ctx.block().icmp_ult(I64, &zero_offset, "2"); let is_true = ctx.block().icmp_eq(I64, &bits, crate::nanbox::TAG_TRUE_I64); - let primitive = ctx.block().or(I1, &is_plain_number, &is_i32); - let primitive = ctx.block().or(I1, &primitive, &is_singleton); + let primitive = ctx.block().or(I1, &is_plain_number, &is_singleton); let fast_idx = ctx.new_block("relnum.fast"); let slow_idx = ctx.new_block("relnum.slow"); @@ -962,26 +916,17 @@ fn lower_relational_against_number( ctx.block().cond_br(&primitive, &fast_l, &slow_l); ctx.current_block = fast_idx; - let raw = ctx.block().trunc(I64, &bits, I32); - let int_value = ctx.block().sitofp(I32, &raw, DOUBLE); - let dynamic_number = ctx.block().select(I1, &is_i32, DOUBLE, &int_value, dynamic); - let dynamic_number = ctx - .block() - .select(I1, &is_zero, DOUBLE, "0.0", &dynamic_number); + let dynamic_number = ctx.block().select(I1, &is_zero, DOUBLE, "0.0", dynamic); let dynamic_number = ctx .block() .select(I1, &is_true, DOUBLE, "1.0", &dynamic_number); // `undefined` deliberately retains its tagged NaN. Ordered fcmp then // returns false for all four relational operators, exactly as ToNumber. - let proven_number = if crate::type_analysis::expr_produces_canonical_raw_f64(ctx, proven_expr) { - (if dynamic_is_left { r } else { l }).to_string() - } else { - normalize_int32_immediate(ctx, if dynamic_is_left { r } else { l }) - }; + let proven_number = if dynamic_is_left { r } else { l }; let (left_number, right_number) = if dynamic_is_left { - (&dynamic_number, &proven_number) + (dynamic_number.as_str(), proven_number) } else { - (&proven_number, &dynamic_number) + (proven_number, dynamic_number.as_str()) }; let pred = match op { CompareOp::Lt => "olt", @@ -2029,7 +1974,6 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { &l, &r, !left_numeric, - if left_numeric { left } else { right }, fname, )); } diff --git a/crates/perry-codegen/src/expr/compare_chain.rs b/crates/perry-codegen/src/expr/compare_chain.rs index 239d0a47444..ce8708f6015 100644 --- a/crates/perry-codegen/src/expr/compare_chain.rs +++ b/crates/perry-codegen/src/expr/compare_chain.rs @@ -142,7 +142,7 @@ fn switch(ctx: &mut FnCtx<'_>, value: &str, targets: &[String]) { } /// Lower a Boolean decision tree with dynamic stable operands. Classification -/// and compact-int decoding occur once per local. Numeric execution contains +/// occurs once per local. Numeric execution contains /// only ordered/unordered fcmp and short-circuit branches. All non-numeric /// comparisons of the same operator share one call and continuation switch; /// ToPrimitive is still called separately for every reached relational node. @@ -204,8 +204,8 @@ pub(crate) fn try_lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result, expr: &Expr) -> Result) -> String { ir[start..end].to_string() } +/// The comparand of the one emitted plain-number test. +const PLAIN_NUMBER_MAX: &str = "9221401712017801215"; // 0x7FF8_FFFF_FFFF_FFFF + #[test] fn forty_dynamic_comparisons_classify_once_and_share_the_slow_call() { let ir = ir(chain(40), vec![]); assert_eq!(ir.matches("call double @js_rel_ge(").count(), 1, "{ir}"); - assert_eq!(ir.matches("sitofp i32").count(), 1, "{ir}"); - // The one exact int32 test (`emit_int32_bits_test`), shared by the graph. - assert_eq!(ir.matches(crate::nanbox::INT32_TAG_I64).count(), 1, "{ir}"); + // One plain-number classification, shared by the graph, and no decode: a + // Number has one encoding. + assert_eq!(ir.matches(PLAIN_NUMBER_MAX).count(), 1, "{ir}"); + assert!(!ir.contains("sitofp i32"), "{ir}"); + assert!(!ir.contains(crate::nanbox::INT32_TAG_I64), "{ir}"); assert!(!ir.contains("lshr i64"), "{ir}"); assert!(ir.contains("phi i1"), "{ir}"); assert!( @@ -99,7 +104,7 @@ fn reassignment_before_the_graph_still_classifies_once() { ); assert!(ir.contains("cmpgraph."), "{ir}"); assert_eq!(ir.matches("call double @js_rel_ge(").count(), 1, "{ir}"); - assert_eq!(ir.matches("sitofp i32").count(), 1, "{ir}"); + assert_eq!(ir.matches(PLAIN_NUMBER_MAX).count(), 1, "{ir}"); } #[test] @@ -147,13 +152,15 @@ fn strict_number_literal_equalities_need_no_coercion_or_slow_arm() { }, vec![], ); - assert_eq!(ir.matches("sitofp i32").count(), 1, "{ir}"); + // Non-number tags are NaNs, so the graph classifies nothing. + assert!(!ir.contains("sitofp i32"), "{ir}"); + assert!(!ir.contains(PLAIN_NUMBER_MAX), "{ir}"); assert!(!ir.contains("cmpgraph.coerce"), "{ir}"); assert!(!ir.contains("@js_eq("), "{ir}"); } #[test] -fn mixed_graph_does_not_decode_a_constructively_numeric_local() { +fn mixed_graph_does_not_classify_a_constructively_numeric_local() { let expr = Expr::Logical { op: LogicalOp::And, left: Box::new(comparison(CompareOp::Ge, 100.0)), @@ -171,12 +178,8 @@ fn mixed_graph_does_not_decode_a_constructively_numeric_local() { init: Some(Expr::Number(1.25)), }; let good = ir(expr.clone(), vec![local.clone()]); - assert_eq!(good.matches("sitofp i32").count(), 1, "{good}"); - assert_eq!( - good.matches(crate::nanbox::INT32_TAG_I64).count(), - 1, - "{good}" - ); + assert_eq!(good.matches(PLAIN_NUMBER_MAX).count(), 1, "{good}"); + assert!(!good.contains(crate::nanbox::INT32_TAG_I64), "{good}"); let dynamic = ir( expr, vec![ @@ -184,6 +187,6 @@ fn mixed_graph_does_not_decode_a_constructively_numeric_local() { Stmt::Expr(Expr::LocalSet(2, Box::new(Expr::String("1".into())))), ], ); - assert_eq!(dynamic.matches("sitofp i32").count(), 2, "{dynamic}"); + assert_eq!(dynamic.matches(PLAIN_NUMBER_MAX).count(), 2, "{dynamic}"); assert!(dynamic.contains("call double @js_rel_lt("), "{dynamic}"); } diff --git a/crates/perry-codegen/src/expr/compare_tests.rs b/crates/perry-codegen/src/expr/compare_tests.rs index 3bb6e449ef4..8bd203b6519 100644 --- a/crates/perry-codegen/src/expr/compare_tests.rs +++ b/crates/perry-codegen/src/expr/compare_tests.rs @@ -707,19 +707,13 @@ fn dynamic_strict_eq_against_number_compares_both_encodings_without_js_eq() { Expr::LocalGet(X), Expr::Number(7.0), ); - // The exact int32 test (`emit_int32_bits_test`): the identity mask - // (bits 32..63) compared with the whole INT32 tag word. A test of the - // top 16 bits alone (`lshr 48`, `icmp eq 32766`) reads a `C.prototype` - // ref or an internal marker as an int32 Number. - let mask = crate::nanbox::i64_literal(!crate::nanbox::INT32_MASK); + // A Number has one encoding: no test of the 0x7FFE word, whole or top 16 + // bits (`lshr 48`, `icmp eq 32766`), and no int-to-double decode. assert!( - ir.contains(&mask) && ir.contains(crate::nanbox::INT32_TAG_I64), - "the compact-INT32 normalization guard is absent or inexact:\n{ir}" - ); - assert!( - !ir.contains(&format!(", {}", crate::nanbox::INT32_TAG_TOP16_I64)), - "an int32 test compares only the top 16 bits:\n{ir}" + !ir.contains(crate::nanbox::INT32_TAG_I64) && !ir.contains(", 32766"), + "a strict equality against a Number tests the INT32 tag:\n{ir}" ); + assert!(!ir.contains("sitofp i32"), "{ir}"); assert!( ir.contains("fcmp oeq double"), "dynamic-vs-number equality did not become numeric fcmp:\n{ir}" @@ -927,8 +921,8 @@ fn integer_literal_equality_has_one_numeric_compare() { assert_eq!(ir.matches("fcmp ").count(), 1, "one numeric compare: {ir}"); assert!(ir.contains(&format!("fcmp {pred} double")), "{ir}"); assert!( - ir.contains("sitofp i32"), - "compact numbers still normalize: {ir}" + !ir.contains("sitofp i32"), + "a Number has one encoding, nothing to normalize: {ir}" ); assert!( !ir.contains("llvm.fptosi.sat"), diff --git a/crates/perry-codegen/src/expr/hit_path_tests.rs b/crates/perry-codegen/src/expr/hit_path_tests.rs index 7e8a0853fc2..148515cb339 100644 --- a/crates/perry-codegen/src/expr/hit_path_tests.rs +++ b/crates/perry-codegen/src/expr/hit_path_tests.rs @@ -478,7 +478,11 @@ fn typed_public_entries_guard_inline_and_test_plain_doubles_first() { add_entry.contains("9221401712017801215"), "tier 1 must be the plain-double compare:\n{add_entry}" ); - assert!(add_entry.contains("_public.int32"), "{add_entry}"); + assert!(add_entry.contains("_public.nan"), "{add_entry}"); + assert!( + !add_entry.contains(crate::nanbox::INT32_TAG_I64) && !add_entry.contains("phi double"), + "tier 2 admits NaN Numbers and converts nothing:\n{add_entry}" + ); assert_eq!( add_entry.matches("add$spec_").count() + add_entry.matches("add$typed_").count(), 1, diff --git a/crates/perry-codegen/src/expr/index_key.rs b/crates/perry-codegen/src/expr/index_key.rs index 78115fd3b43..4576b050132 100644 --- a/crates/perry-codegen/src/expr/index_key.rs +++ b/crates/perry-codegen/src/expr/index_key.rs @@ -2,7 +2,7 @@ //! numeric normalizer otherwise. Rejected keys keep their original JS value. use super::FnCtx; use crate::function::LlFunction; -use crate::types::{DOUBLE, I1, I32, I64}; +use crate::types::{DOUBLE, I1, I32}; use perry_hir::Expr; const NORMALIZE: &str = "perry_array_index_normalize_i32"; @@ -114,21 +114,10 @@ fn publish(ctx: &mut FnCtx<'_>) { f.linkage = "linkonce_odr".into(); f.no_inline = true; let b = f.create_block("entry"); - let bits = b.bitcast_double_to_i64("%key"); - let boxed = crate::codegen::emit_int32_bits_test(b, &bits); - b.cond_br(&boxed, "boxed.1", "raw.2"); - - let b = f.create_block("boxed"); - let payload = b.trunc(I64, &bits, I32); - let nonnegative = b.icmp_sge(I32, &payload, "0"); - let index = b.select(I1, &nonnegative, I32, &payload, "-1"); - b.ret(I32, &index); - - let b = f.create_block("raw"); let ge = b.fcmp("oge", "%key", "0.0"); let le = b.fcmp("ole", "%key", "2147483647.0"); let range = b.and(I1, &ge, &le); - b.cond_br(&range, "convert.3", "reject.4"); + b.cond_br(&range, "convert.1", "reject.2"); // The ordered range branch dominates conversion. No speculative fptosi // can see NaN, a tag, an infinity, or an out-of-range Number. diff --git a/crates/perry-codegen/src/lower_call/early_branches.rs b/crates/perry-codegen/src/lower_call/early_branches.rs index 919ffb2662f..039f42a2fb5 100644 --- a/crates/perry-codegen/src/lower_call/early_branches.rs +++ b/crates/perry-codegen/src/lower_call/early_branches.rs @@ -852,7 +852,6 @@ pub(crate) fn emit_known_closure_body_call( let typed_label = ctx.block_label(typed_idx); let generic_label = ctx.block_label(generic_idx); let typed_merge_label = ctx.block_label(typed_merge_idx); - let entry_label = ctx.block().label.clone(); let mut tier1 = Vec::new(); for (value, guard) in lowered_args.iter().zip(&guards) { if guard.number { @@ -865,24 +864,16 @@ pub(crate) fn emit_known_closure_body_call( if let Some(c) = &capture_guard { tier1.push(c.clone()); } - let converted: Option<(String, Vec>)> = if any_number { - let int32_idx = ctx.new_block("closure_direct.int32"); - let convert_idx = ctx.new_block("closure_direct.int32_convert"); - let int32_label = ctx.block_label(int32_idx); - let convert_label = ctx.block_label(convert_idx); + if any_number { + let nan_idx = ctx.new_block("closure_direct.nan"); + let nan_label = ctx.block_label(nan_idx); let tier1 = and_all(ctx.block(), tier1).expect("a number lane has a tier-1 test"); - ctx.block().cond_br(&tier1, &typed_label, &int32_label); - ctx.current_block = int32_idx; + ctx.block().cond_br(&tier1, &typed_label, &nan_label); + ctx.current_block = nan_idx; let mut tier2 = Vec::new(); - let mut int32_tests = Vec::with_capacity(guards.len()); for (value, guard) in lowered_args.iter().zip(&guards) { if guard.number { - let plain = crate::codegen::emit_plain_number_test(ctx.block(), value); - let int32 = crate::codegen::emit_int32_box_test(ctx.block(), value); - tier2.push(ctx.block().or(I1, &plain, &int32)); - int32_tests.push(Some(int32)); - } else { - int32_tests.push(None); + tier2.push(crate::codegen::emit_plain_number_test(ctx.block(), value)); } if let Some(exact) = &guard.exact { tier2.push(exact.clone()); @@ -892,48 +883,22 @@ pub(crate) fn emit_known_closure_body_call( tier2.push(c.clone()); } let ok = and_all(ctx.block(), tier2).expect("tier 2 has a number lane"); - ctx.block().cond_br(&ok, &convert_label, &generic_label); - ctx.current_block = convert_idx; - let values = lowered_args - .iter() - .zip(&int32_tests) - .map(|(value, int32)| { - int32.as_ref().map(|is_int32| { - let blk = ctx.block(); - let bits = blk.bitcast_double_to_i64(value); - let low = blk.trunc(I64, &bits, I32); - let conv = blk.sitofp(I32, &low, DOUBLE); - blk.select(I1, is_int32, DOUBLE, &conv, value) - }) - }) - .collect(); - ctx.block().br(&typed_label); - Some((convert_label, values)) + ctx.block().cond_br(&ok, &typed_label, &generic_label); } else { match and_all(ctx.block(), tier1) { Some(g) => ctx.block().cond_br(&g, &typed_label, &generic_label), None => ctx.block().br(&typed_label), } - None - }; + } ctx.current_block = typed_idx; let mut typed_args_storage: Vec = Vec::with_capacity(lowered_args.len()); - for (k, (value, rep)) in lowered_args.iter().zip(typed_param_reps.iter()).enumerate() { - let raw = match &converted { - Some((convert_label, values)) if guards[k].number => { - let conv = values[k].as_ref().expect("a number lane converts"); - ctx.block().phi( - DOUBLE, - &[ - (value.as_str(), entry_label.as_str()), - (conv.as_str(), convert_label.as_str()), - ], - ) - } - _ => crate::codegen::emit_typed_arg_to_raw(ctx.block(), *rep, value), - }; - typed_args_storage.push(raw); + for (value, rep) in lowered_args.iter().zip(typed_param_reps.iter()) { + typed_args_storage.push(crate::codegen::emit_typed_arg_to_raw( + ctx.block(), + *rep, + value, + )); } let mut typed_args: Vec<(crate::types::LlvmType, &str)> = Vec::with_capacity(typed_args_storage.len() + 1); diff --git a/crates/perry-codegen/src/lower_call/index_split.rs b/crates/perry-codegen/src/lower_call/index_split.rs index c8a625beb0a..b20663d4f57 100644 --- a/crates/perry-codegen/src/lower_call/index_split.rs +++ b/crates/perry-codegen/src/lower_call/index_split.rs @@ -2,11 +2,10 @@ //! proven Numbers. //! //! The public entry of such a method guards a boxed argument with the exact -//! i32 test: an INT32 box or a double that converts exactly, then a sign -//! test. A call site that proves the argument a canonical double -//! (`expr_produces_canonical_raw_f64`) applies that predicate's plain-double -//! half (`emit_typed_i32_plain_guard_and_raw`, the very half the public guard -//! uses) inline and selects the clone or the generic body itself: the clone +//! i32 test: a double that converts exactly, then a sign test. A call site +//! that proves the argument a canonical double +//! (`expr_produces_canonical_raw_f64`) applies that predicate +//! (`emit_typed_i32_plain_guard_and_raw`) inline and selects the clone or the generic body itself: the clone //! admits exactly the values the public guard admits (no `-0`, NaN, fraction //! or value outside the i32 range). //! @@ -153,8 +152,8 @@ pub(crate) fn number_index_positions( } /// Check mode (`PERRY_VERIFY_FACTS=1`): the argument the proof covers is an -/// exact int32 (a canonical double, never `-0`; an INT32 box too with -/// `PERRY_WP_REP_P3=0`) and not negative (fact `index argument`). +/// exact int32 (a plain double, never `-0`) and not negative (fact `index +/// argument`). fn witness_nonneg(ctx: &mut FnCtx<'_>, value: &str, proof: Proof) { crate::verify_facts::consumed( crate::verify_facts::Fact::IndexArgument, @@ -165,14 +164,9 @@ fn witness_nonneg(ctx: &mut FnCtx<'_>, value: &str, proof: Proof) { return; } let blk = ctx.block(); - // A proven Number is a canonical double (P3): the witness admits an - // exact int32 double only, never an INT32 box. - let (exact, int) = if crate::codegen::facts_rep::canonical_numbers() { - let bits = blk.bitcast_double_to_i64(value); - crate::codegen::emit_typed_i32_plain_guard_and_raw(blk, value, &bits) - } else { - crate::codegen::emit_typed_i32_guard_and_raw(blk, value) - }; + // A Number is a plain double: the witness admits an exact int32 double. + let bits = blk.bitcast_double_to_i64(value); + let (exact, int) = crate::codegen::emit_typed_i32_plain_guard_and_raw(blk, value, &bits); let nonnegative = blk.icmp_sge(I32, &int, "0"); let ok = blk.and(I1, &exact, &nonnegative); crate::verify_facts::tick(ctx); @@ -212,14 +206,10 @@ fn proven_raw(ctx: &mut FnCtx<'_>, id: u32, value: &str) -> String { proven_number_raw(ctx, value) } -/// The i32 of `value`, a Number the facts prove an int32: a canonical -/// double (P3, `facts_rep::canonical_numbers`), so one `fptosi`. With -/// `PERRY_WP_REP_P3=0` an INT32 box converts through its payload. +/// The i32 of `value`, a Number the facts prove an int32: a plain double, so +/// one `fptosi`. fn proven_number_raw(ctx: &mut FnCtx<'_>, value: &str) -> String { - if crate::codegen::facts_rep::canonical_numbers() { - return ctx.block().fptosi(crate::types::DOUBLE, value, I32); - } - crate::codegen::emit_typed_i32_raw_assuming_guarded(ctx.block(), value) + ctx.block().fptosi(crate::types::DOUBLE, value, I32) } /// Call `target`'s `$idx_u31` clone when every selected argument is a @@ -248,8 +238,7 @@ pub(crate) fn emit_number_index_split( continue; } let blk = ctx.block(); - // The public entry's own predicate, restricted to a canonical double - // (its INT32-box half can never match): both admit the same set, and + // The public entry's own predicate: both admit the same set, and // `-0` is excluded because the clone reads its index back as `+0`. let bits = blk.bitcast_double_to_i64(&value); let (exact, int) = crate::codegen::emit_typed_i32_plain_guard_and_raw(blk, &value, &bits); diff --git a/crates/perry-codegen/src/lower_call/property_get/builtin_kind_guard.rs b/crates/perry-codegen/src/lower_call/property_get/builtin_kind_guard.rs index 31401fbce03..0100f83a4a5 100644 --- a/crates/perry-codegen/src/lower_call/property_get/builtin_kind_guard.rs +++ b/crates/perry-codegen/src/lower_call/property_get/builtin_kind_guard.rs @@ -558,16 +558,14 @@ fn emit_receiver_kind_branch( None } // A number is any IEEE double outside Perry's positive tag band - // 0x7FF9..=0x7FFF (`JSValue::is_number`), or an INT32-tagged value. + // 0x7FF9..=0x7FFF (`JSValue::is_number`). ReceiverKind::Number => { let blk = ctx.block(); let bits = blk.bitcast_double_to_i64(recv); let tag = blk.lshr(I64, &bits, "48"); let band_offset = blk.sub(I64, &tag, crate::nanbox::SHORT_STRING_TAG_TOP16_I64); let in_tag_band = blk.icmp_ult(I64, &band_offset, "7"); - let is_double = blk.xor(I1, &in_tag_band, "true"); - let is_int32 = crate::codegen::emit_int32_bits_test(blk, &bits); - let is_number = blk.or(I1, &is_double, &is_int32); + let is_number = blk.xor(I1, &in_tag_band, "true"); let is_number = blk.and(I1, &is_number, pristine); blk.cond_br(&is_number, builtin_label, generic_label); None diff --git a/crates/perry-codegen/src/lower_call/scalar_method.rs b/crates/perry-codegen/src/lower_call/scalar_method.rs index 4c526491857..ac8fb0b3c82 100644 --- a/crates/perry-codegen/src/lower_call/scalar_method.rs +++ b/crates/perry-codegen/src/lower_call/scalar_method.rs @@ -1163,10 +1163,8 @@ pub(super) fn try_lower_scalar_replaced_method_call( ctx.current_block = fast_idx; let mut raw_locals = HashMap::new(); for (id, value) in &guard_values { - raw_locals.insert( - *id, - crate::codegen::emit_typed_f64_to_raw_guarded(ctx.block(), value.as_str()), - ); + // The guard admitted a plain double, the raw value itself. + raw_locals.insert(*id, value.clone()); } let mut fast_args = Vec::with_capacity(args.len()); for arg in args { diff --git a/crates/perry-codegen/src/nanbox.rs b/crates/perry-codegen/src/nanbox.rs index d7d6f7fe178..5b5de52f91c 100644 --- a/crates/perry-codegen/src/nanbox.rs +++ b/crates/perry-codegen/src/nanbox.rs @@ -28,7 +28,6 @@ pub const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; pub const HANDLE_TAG: u64 = crate::runtime_abi::HANDLE_TAG; pub const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; pub const INT32_TAG: u64 = 0x7FFE_0000_0000_0000; -pub const INT32_MASK: u64 = 0x0000_0000_FFFF_FFFF; pub const STRING_TAG: u64 = 0x7FFF_0000_0000_0000; pub const BIGINT_TAG: u64 = 0x7FFA_0000_0000_0000; /// Low end of the Perry-owned qNaN tag band. A NaN-boxed value is a primitive @@ -68,11 +67,6 @@ pub const SHORT_STRING_TAG_TOP16_I64: &str = "32761"; /// against the u64 tags in `tag_strings_match_u64_values`. pub const POINTER_TAG_TOP16_I64: &str = "32765"; pub const BIGINT_TAG_TOP16_I64: &str = "32762"; -/// `INT32_TAG >> 48`. Used by the inline `===` prefix, which settles two -/// INT32-tagged operands by bit inequality — the encoding -/// `INT32_TAG | (v as u32 as u64)` is canonical, so different bits are -/// different integers. Asserted in `tag_strings_match_u64_values`. -pub const INT32_TAG_TOP16_I64: &str = "32766"; /// Format a `u64` as a signed LLVM i64 literal (LLVM IR integer literals are signed). pub fn i64_literal(v: u64) -> String { @@ -136,7 +130,6 @@ mod tests { ); assert_eq!(i64_literal(POINTER_TAG >> 48), POINTER_TAG_TOP16_I64); assert_eq!(i64_literal(BIGINT_TAG >> 48), BIGINT_TAG_TOP16_I64); - assert_eq!(i64_literal(INT32_TAG >> 48), INT32_TAG_TOP16_I64); } /// #7511 — the inline pointer-bearing test emitted at class-field stores diff --git a/crates/perry-codegen/src/stmt/binding_cell.rs b/crates/perry-codegen/src/stmt/binding_cell.rs index 35ce135b118..52bc8c48125 100644 --- a/crates/perry-codegen/src/stmt/binding_cell.rs +++ b/crates/perry-codegen/src/stmt/binding_cell.rs @@ -89,8 +89,7 @@ pub(crate) fn mint_scope_object(ctx: &mut FnCtx<'_>, members: &[u32], tdz: bool) // high half; their typed loads and stores touch only the low bytes. for &id in members { let (seed, note) = if crate::expr::is_compiler_private_async_i32_control_local(ctx, id) { - // 0x7FFE_0000_0000_0000 (INT32_TAG) - ("9222809086901354496", "primitive_i32_control_cell") + (crate::nanbox::INT32_TAG_I64, "primitive_i32_control_cell") } else if crate::expr::is_compiler_private_async_i1_control_local(ctx, id) { // 0x7FFC_0000_0000_0000 ("9222246136947933184", "primitive_i1_control_cell") diff --git a/crates/perry-codegen/src/stmt/byte_result_tests.rs b/crates/perry-codegen/src/stmt/byte_result_tests.rs index 73210036756..f4cf840d156 100644 --- a/crates/perry-codegen/src/stmt/byte_result_tests.rs +++ b/crates/perry-codegen/src/stmt/byte_result_tests.rs @@ -107,10 +107,9 @@ fn admitted_general_byte_result_does_not_use_a_pointer_slot() { !result.contains("store ptr addrspace(1)"), "Number-or-undefined was put in a GC pointer slot:\n{result}" ); - assert_eq!( - result.matches(crate::nanbox::INT32_TAG_I64).count(), - 1, - "only the constant-side Number normalizer needs a compact tag check:\n{result}" + assert!( + !result.contains(crate::nanbox::INT32_TAG_I64), + "a Number has one encoding, so no side needs a tag check:\n{result}" ); assert!(ir.contains("ta.read.oob")); let slow = ir diff --git a/crates/perry-codegen/src/stmt/loops.rs b/crates/perry-codegen/src/stmt/loops.rs index 0e076b04cc1..d4e50842b70 100644 --- a/crates/perry-codegen/src/stmt/loops.rs +++ b/crates/perry-codegen/src/stmt/loops.rs @@ -6721,8 +6721,7 @@ fn dynamic_bound_private_counter_is_safe( pub(crate) fn emit_js_value_is_number(ctx: &mut FnCtx<'_>, value: &str) -> String { let n_bits = ctx.block().bitcast_double_to_i64(value); // Every boxed tag occupies the positive suffix [0x7FF9_0000_0000_0000, - // 0x7FFF_FFFF_FFFF_FFFF]. INT32 boxes are excluded too: their payload - // still needs unboxing before floating-point arithmetic. + // 0x7FFF_FFFF_FFFF_FFFF]. // // The test ignores the sign bit, so it also rejects the MIRROR of the tag // band (negative NaNs whose magnitude lies in it). A value that passes diff --git a/crates/perry-codegen/src/stmt/switch_stmt.rs b/crates/perry-codegen/src/stmt/switch_stmt.rs index 74316a091a8..f5d22c98eac 100644 --- a/crates/perry-codegen/src/stmt/switch_stmt.rs +++ b/crates/perry-codegen/src/stmt/switch_stmt.rs @@ -1,7 +1,7 @@ //! `Stmt::Switch` lowering. use super::*; -use crate::types::{DOUBLE, I1, I32, I64}; +use crate::types::I64; /// `switch (disc) { case A: ...; break; case B: ...; default: ... }` /// lowering. Each case gets a (test, body) block pair; bodies fall @@ -15,7 +15,7 @@ use crate::types::{DOUBLE, I1, I32, I64}; /// switch (NaN != NaN). /// /// All-numeric switches: when EVERY case test is a compile-time -/// integer-valued numeric literal, the discriminant is canonicalized once and +/// integer-valued numeric literal, the discriminant is /// dispatched through a balanced binary-search tree of `fcmp` branches — /// O(log n) compares instead of O(n) `js_switch_strict_equals` calls. Only the /// DISPATCH changes: bodies keep source order and fall-through, the default @@ -51,16 +51,6 @@ pub(crate) fn lower_switch( }; let tree_plan = numeric_literal_dispatch_plan(cases); - // Canonicalized numeric view of the discriminant, shared by every - // numeric-literal case of a tower lowering. Emitted here, in the - // discriminant's block, which dominates every test block. - let numeric_view = (tree_plan.is_none() - && cases.iter().any(|c| { - c.test - .as_ref() - .is_some_and(|t| integral_numeric_literal(t).is_some()) - })) - .then(|| emit_canonical_numeric_view(ctx, &dv)); // Allocate body blocks (and, for the tower lowering, test blocks) for // every case up front so we can wire up the fall-through edges before @@ -131,8 +121,8 @@ pub(crate) fn lower_switch( } else { match &tree_plan { Some(plan) => { - // Dense numeric switch: canonicalize the discriminant once and - // dispatch through a balanced comparison tree. Case tests are + // Dense numeric switch: dispatch through a balanced + // comparison tree. Case tests are // all literals, so skipping their (side-effect-free) in-order // evaluation is unobservable. emit_numeric_tree_dispatch(ctx, &dv, plan, &body_blocks, &no_match_target_label); @@ -206,14 +196,7 @@ pub(crate) fn lower_switch( }; if let Some(test_expr) = case.test.as_ref() { - if emit_literal_case_test( - ctx, - &dv, - test_expr, - numeric_view.as_ref(), - &body_label, - &next_label, - )? { + if emit_literal_case_test(ctx, &dv, test_expr, &body_label, &next_label)? { continue; } let cv = lower_expr(ctx, test_expr)?; @@ -314,21 +297,15 @@ fn numeric_literal_dispatch_plan(cases: &[perry_hir::SwitchCase]) -> Option, dv: &str, @@ -336,18 +313,9 @@ fn emit_numeric_tree_dispatch( body_blocks: &[usize], no_match_label: &str, ) { - let NumericView { - canonical, - is_numeric, - } = emit_canonical_numeric_view(ctx, dv); - let root_idx = ctx.new_block("switch.bst"); - let root_label = ctx.block_label(root_idx); - ctx.block() - .cond_br(&is_numeric, &root_label, no_match_label); - ctx.current_block = root_idx; emit_numeric_tree_range( ctx, - &canonical, + dv, plan, 0, plan.len() - 1, @@ -438,40 +406,6 @@ fn integral_numeric_literal(test: &perry_hir::Expr) -> Option { (v.is_finite() && v.fract() == 0.0).then_some(v) } -/// The discriminant as `js_switch_strict_equals`' numeric arm sees it. -struct NumericView { - /// int32 boxes sign-extended to a double; every other value unchanged. - canonical: String, - /// The value is a Number (plain double or int32 box). - is_numeric: String, -} - -/// Canonicalization mirrors `js_switch_strict_equals`' numeric arm exactly: -/// -/// - int32-boxed (`(bits & !INT32_MASK) == INT32_TAG`) → sign-extend the low -/// 32 bits and `sitofp` (so int32-boxed 7 matches `case 7:`). -/// - any other value whose top-16 tag lies outside Perry's tag band -/// (`SHORT_STRING_TAG..=STRING_TAG`) is already a raw double — including -/// real NaNs, which then fail every ordered `fcmp` and fall out at the -/// no-match edge, exactly like NaN != NaN in the helper. -/// - strings/bools/undefined/null/pointers (tagged, non-int32) can never -/// strictly equal a number. -fn emit_canonical_numeric_view(ctx: &mut FnCtx<'_>, dv: &str) -> NumericView { - let bits = ctx.block().bitcast_double_to_i64(dv); - let is_int32 = crate::codegen::emit_int32_bits_test(ctx.block(), &bits); - let lo32 = ctx.block().trunc(I64, &bits, I32); - let int32_as_double = ctx.block().sitofp(I32, &lo32, DOUBLE); - let canonical = ctx - .block() - .select(I1, &is_int32, DOUBLE, &int32_as_double, dv); - let is_untagged_number = emit_js_value_is_number(ctx, dv); - let is_numeric = ctx.block().or(I1, &is_untagged_number, &is_int32); - NumericView { - canonical, - is_numeric, - } -} - /// Emit an inline `===` case test for a literal case expression and branch to /// `body_label` on a match, `next_label` otherwise. Returns `false` (emitting /// nothing) for any other case expression, which keeps the runtime helper. @@ -480,8 +414,8 @@ fn emit_canonical_numeric_view(ctx: &mut FnCtx<'_>, dv: &str) -> NumericView { /// - string literal: the same inline decision `x === "lit"` uses /// (`expr::lower_string_literal_strict_eq`: pooled identity, SSO /// immediate, heap length and bytes); -/// - integer-valued numeric literal: `fcmp oeq` on the canonical numeric view -/// (NaN never matches, `-0 === 0`, an int32 box equals its double); +/// - integer-valued numeric literal: `fcmp oeq` on the discriminant's bits +/// (NaN and every non-number, a NaN pattern, never match; `-0 === 0`); /// - `true`/`false`/`null`/`undefined`: singleton bit identity. /// /// Literals have no evaluation side effects, so skipping their lowering is @@ -491,7 +425,6 @@ fn emit_literal_case_test( ctx: &mut FnCtx<'_>, dv: &str, test: &perry_hir::Expr, - numeric_view: Option<&NumericView>, body_label: &str, next_label: &str, ) -> Result { @@ -512,17 +445,11 @@ fn emit_literal_case_test( let bits = blk.bitcast_double_to_i64(dv); blk.icmp_eq(I64, &bits, tag) } - _ => match (integral_numeric_literal(test), numeric_view) { - (Some(value), Some(view)) => { - let blk = ctx.block(); - let eq = blk.fcmp( - "oeq", - &view.canonical, - &crate::nanbox::double_literal(value), - ); - blk.and(I1, &view.is_numeric, &eq) - } - _ => return Ok(false), + _ => match integral_numeric_literal(test) { + Some(value) => ctx + .block() + .fcmp("oeq", dv, &crate::nanbox::double_literal(value)), + None => return Ok(false), }, }; ctx.block().cond_br(&hit, body_label, next_label); diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index d752e9001a8..8fd69a57557 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -1275,10 +1275,9 @@ fn guards_number_arg(ir: impl AsRef) -> bool { && ir_line_with(ir, &["icmp ule i64 %", ", 9221401712017801215"]) } -/// The one-tier Number guard (`emit_typed_f64_guard`): `is_number || is_int32` -/// decided by the sign-closed magnitude compare -/// `(bits & 0x7FFF_FFFF_FFFF_FFFF) ) -> bool { let ir = ir.as_ref(); ir_line_with(ir, &["and i64 %", ", 9223372036854775807"]) && ir_line_with(ir, &["icmp ult i64 %", ", 9221401712017801216"]) - && ir_line_with(ir, &["icmp eq i64 %", ", 9222809086901354496"]) } -/// Number lane, tier 2: the INT32 identity test and the `sitofp` that converts -/// the box to the double the clone wants. Replaces -/// `call double @js_typed_f64_arg_to_raw`. -fn unboxes_number_arg(ir: impl AsRef) -> bool { - let ir = ir.as_ref(); - ir_line_with(ir, &["icmp eq i64 %", ", 9222809086901354496"]) && ir.contains("sitofp i32 %") +/// A test of the 0x7FFE word. A Number has one encoding, the plain double, so +/// a Number lane passes its guarded argument as it is: no INT32 identity test +/// and no decode. Replaces the assertions of +/// `call double @js_typed_f64_arg_to_raw` and of the inline INT32 decode. +fn tests_int32_tag(ir: impl AsRef) -> bool { + ir_line_with(ir.as_ref(), &["icmp eq i64 %", ", 9222809086901354496"]) } /// Boolean lane (`emit_typed_i1_guard`): `bits - TAG_FALSE ) -> bool { ir_line_with(ir, &["icmp eq i64 %", ", 9222246136947933188"]) } -/// Int32 lane (`emit_typed_i32_guard_and_raw`): the INT32 identity test plus +/// Int32 lane (`emit_typed_i32_plain_guard_and_raw`): the INT32 identity test plus /// the plain-double range bounds that admit an integral double. /// Replaces `call i32 @js_typed_i32_arg_guard`. fn guards_i32_arg(ir: impl AsRef) -> bool { @@ -1352,7 +1350,7 @@ fn inline_typed_guard_predicates_reject_unguarded_and_neighbouring_lanes() { for (name, matched) in [ ("number", guards_number_arg(unguarded)), ("number_banded", guards_number_arg_banded(unguarded)), - ("number_unbox", unboxes_number_arg(unguarded)), + ("int32_tag", tests_int32_tag(unguarded)), ("bool", guards_bool_arg(unguarded)), ("bool_unbox", unboxes_bool_arg(unguarded)), ("i32", guards_i32_arg(unguarded)), @@ -11860,7 +11858,7 @@ fn typed_f64_function_clone_emits_internal_clone_and_guarded_call() { // converts an INT32 box. `, 32761` alone no longer says that — the string // and i32 lanes carry the same constant. assert!(guards_number_arg(&ir), "{ir}"); - assert!(unboxes_number_arg(&ir), "{ir}"); + assert!(!tests_int32_tag(&ir), "{ir}"); assert!(ir.contains(&format!("call double @{typed}")), "{ir}"); assert!( ir.contains(&format!("call double @{generic_body}(")), @@ -11884,7 +11882,7 @@ fn typed_f64_public_trampoline_dispatches_before_generic_body() { "typed function should keep a separate generic body:\n{ir}" ); assert!( - guards_number_arg(&wrapper_ir) && unboxes_number_arg(&wrapper_ir), + guards_number_arg(&wrapper_ir) && !tests_int32_tag(&wrapper_ir), "public wrapper should guard and unbox numeric JSValue args:\n{wrapper_ir}" ); let typed_call = wrapper_ir @@ -12104,7 +12102,7 @@ fn typed_f64_function_clone_accepts_mixed_raw_signature_and_direct_call() { assert!( typed_ir.contains("sitofp i32 %arg2 to double") && typed_ir.contains("fadd double") - && !unboxes_number_arg(&typed_ir) + && !tests_int32_tag(&typed_ir) && !typed_ir.contains("js_nanbox"), "typed clone body should avoid JSValue traffic on the hot path:\n{typed_ir}" ); @@ -12601,7 +12599,7 @@ fn typed_i1_numeric_predicate_function_uses_f64_params_and_public_wrapper() { ); assert!( guards_number_arg(&wrapper_ir) - && unboxes_number_arg(&wrapper_ir) + && !tests_int32_tag(&wrapper_ir) && wrapper_ir.contains(&format!("call i1 @{typed}(double ")), "public wrapper should guard/unbox f64 args before the i1 clone:\n{wrapper_ir}" ); @@ -12610,7 +12608,7 @@ fn typed_i1_numeric_predicate_function_uses_f64_params_and_public_wrapper() { "public wrapper should retain a generic JSValue fallback:\n{wrapper_ir}" ); assert!( - guards_number_arg_banded(&caller_ir) && unboxes_number_arg(&caller_ir) + guards_number_arg_banded(&caller_ir) && !tests_int32_tag(&caller_ir) && caller_ir.contains(&format!("call i1 @{typed}(double ")), "direct FuncRef lowering should use the mixed-signature typed-i1 clone after f64 guards:\n{caller_ir}" ); @@ -13374,13 +13372,13 @@ fn typed_i1_numeric_predicate_method_uses_f64_params_and_guarded_direct_call() { ); assert!( guards_number_arg(&wrapper_ir) - && unboxes_number_arg(&wrapper_ir) + && !tests_int32_tag(&wrapper_ir) && wrapper_ir.contains(&format!("call i1 @{typed}(double ")), "public method wrapper should guard/unbox f64 args before the i1 clone:\n{wrapper_ir}" ); assert!( contains_inline_direct_method_shape_guard(caller_ir) - && guards_number_arg_banded(&caller_ir) && unboxes_number_arg(&caller_ir) + && guards_number_arg_banded(&caller_ir) && !tests_int32_tag(&caller_ir) && caller_ir.contains(&format!("call i1 @{typed}(double ")), "exact direct method call should use the mixed-signature typed-i1 clone after f64 guards:\n{caller_ir}" ); @@ -13500,7 +13498,7 @@ fn typed_f64_method_clone_emits_internal_clone_and_guarded_direct_call() { // converts an INT32 box. `, 32761` alone no longer says that — the string // and i32 lanes carry the same constant. assert!(guards_number_arg(&ir), "{ir}"); - assert!(unboxes_number_arg(&ir), "{ir}"); + assert!(!tests_int32_tag(&ir), "{ir}"); assert!( ir.contains(&format!("call double @{typed}(double ")), "typed direct call should target the clone:\n{ir}" @@ -13534,7 +13532,7 @@ fn typed_f64_method_public_trampoline_dispatches_before_generic_body() { let wrapper_ir = function_ir_section(&ir, public); assert!( - guards_number_arg(&wrapper_ir) && unboxes_number_arg(&wrapper_ir), + guards_number_arg(&wrapper_ir) && !tests_int32_tag(&wrapper_ir), "public method wrapper should guard and unbox numeric JSValue args:\n{wrapper_ir}" ); let typed_call = wrapper_ir @@ -13879,7 +13877,7 @@ fn typed_f64_closure_clone_emits_internal_clone_and_guarded_direct_call() { "{ir}" ); assert!( - guards_number_arg(&wrapper_ir) && unboxes_number_arg(&wrapper_ir), + guards_number_arg(&wrapper_ir) && !tests_int32_tag(&wrapper_ir), "public closure wrapper should guard and unbox numeric JSValue args:\n{wrapper_ir}" ); assert!( @@ -13966,7 +13964,7 @@ fn typed_f64_closure_clone_accepts_immutable_numeric_capture() { assert!( loads_entry_capture_word(typed_ir, 0) && typed_ir.contains("bitcast i64") - && unboxes_number_arg(&typed_ir), + && !tests_int32_tag(&typed_ir), "typed-f64 captured closure should load immutable numeric capture as JSValue bits inline from the closure handle:\n{typed_ir}" ); assert!( @@ -14312,7 +14310,7 @@ fn typed_i1_numeric_predicate_closure_uses_f64_params_and_guarded_direct_call() "numeric-predicate typed closure clone should use f64 params and i1 return:\n{ir}" ); assert!( - guards_number_arg(&wrapper_ir) && unboxes_number_arg(&wrapper_ir) + guards_number_arg(&wrapper_ir) && !tests_int32_tag(&wrapper_ir) && wrapper_ir.contains(&format!("call i1 @{typed}(i64 %this_closure")), "public closure wrapper should guard/unbox numeric JSValue args and call the typed clone:\n{wrapper_ir}" ); @@ -14323,7 +14321,7 @@ fn typed_i1_numeric_predicate_closure_uses_f64_params_and_guarded_direct_call() assert!( ir.contains(&format!("call i1 @{typed}(i64 ")) && guards_number_arg_banded(&ir) - && unboxes_number_arg(&ir), + && !tests_int32_tag(&ir), "direct local closure call should guard/unbox numeric args and call the typed clone:\n{ir}" ); assert!( @@ -15078,8 +15076,8 @@ fn scalar_method_boolean_predicate_guards_public_numeric_arguments() { ir.contains("scalar_method_arg_guard.fast") && ir.contains("scalar_method_arg_guard.fallback") && guards_number_arg(&ir) - && ir.contains("sitofp i32 %"), - "{case} public numeric arg should guard/unbox before scalar inline:\n{ir}" + && !tests_int32_tag(&ir), + "{case} public numeric arg should guard before scalar inline:\n{ir}" ); assert!( ir.contains("call double @js_native_call_method_by_id"), @@ -15160,10 +15158,10 @@ fn scalar_method_boolean_predicate_guards_public_numeric_argument_expressions() ir.contains("scalar_method_arg_guard.fast") && ir.contains("scalar_method_arg_guard.fallback") && ir.matches(", 9221401712017801215").count() >= 2 - && ir.matches("sitofp i32 %").count() >= 2 + && !tests_int32_tag(&ir) && ir.contains("fmul double") && ir.contains("fadd double"), - "public numeric arg expression should guard/unbox locals and rebuild arithmetic as raw f64 before scalar inline:\n{ir}" + "public numeric arg expression should guard locals and rebuild arithmetic as raw f64 before scalar inline:\n{ir}" ); assert!( ir.contains("call double @js_native_call_method_by_id"), diff --git a/scripts/check_file_size.sh b/scripts/check_file_size.sh index 15aa3ba0e23..9aea52e1ae7 100755 --- a/scripts/check_file_size.sh +++ b/scripts/check_file_size.sh @@ -24,7 +24,7 @@ crates/perry/src/commands/compile/run_pipeline.rs 6191 # Generator for-of destructuring remains in the statement-lowering core; split the function group in a follow-up. crates/perry-hir/src/lower_decl/body_stmt.rs 2063 # Representation-aware codegen core; further decomposition is tracked in #1435. -crates/perry-codegen/src/codegen/mod.rs 3588 +crates/perry-codegen/src/codegen/mod.rs 3586 # Representation-aware HIR fact collection; further decomposition is tracked in #1435. crates/perry-codegen/src/collectors/hir_facts.rs 2760 # Representation-aware expression dispatch; further decomposition is tracked in #1435. @@ -40,7 +40,7 @@ crates/perry-runtime/src/typed_feedback.rs 2437 # Snapshot census runner (instr-only, #13935); split after #14259 lands (snap census owner). crates/perry-runtime/src/snap_census/runner.rs 3572 # Native ABI proof cases; test organization is a separate follow-up. -crates/perry-codegen/tests/native_proof_regressions.rs 15680 +crates/perry-codegen/tests/native_proof_regressions.rs 15678 # Representation feedback regression cases; test organization is a separate follow-up. crates/perry-runtime/src/typed_feedback/tests.rs 2304 # Lowering report schema and its emitters; split emitters in a follow-up. diff --git a/tests/parity/language/number/test_p3_b3_top_operands.ts b/tests/parity/language/number/test_p3_b3_top_operands.ts new file mode 100644 index 00000000000..3dffb09d399 --- /dev/null +++ b/tests/parity/language/number/test_p3_b3_top_operands.ts @@ -0,0 +1,155 @@ +// P3 B3 controls (DESIGN-p3 rev 3.1 §3 B3): the codegen paths that used to +// test for an INT32-tagged Number, driven with operands the facts do not +// prove (values through `any`, JSON.parse, mixed arrays). A Number has one +// encoding, so each path is one numeric test; every non-Number must still +// take its own arm (the negative controls). Output must match node exactly, +// in normal and in check mode. + +function vals(): any[] { + // Unproven on purpose: parsed at run time, mixed kinds. + const parsed = JSON.parse('[0, 1, 7, -1, 2.5, 2147483647, -2147483648, 4294967296, 1e300, "7", "1", "", null, true, false]'); + return parsed.concat([-0, NaN, Infinity, -Infinity, undefined, 7n, { valueOf() { return 7; } }, [7], "abc", Symbol.iterator]); +} +function show(v: any): string { + if (typeof v === "symbol") return "sym"; + if (typeof v === "function") return "fn"; + if (typeof v === "bigint") return v + "n"; + if (typeof v === "object" && v !== null) return Array.isArray(v) ? "[" + v + "]" : "{obj}"; + return Object.is(v, -0) ? "-0" : String(v); +} + +// 1. `===` / `!==` of an unproven value against a Number: an int literal, a +// fraction, a Number local, both operand orders. +function eqLit(v: any): string { + return [v === 7, 7 === v, v !== 7, v === 0, v === 2.5, 2.5 !== v, v === -1, v === 2147483647, v === 4294967296].map(b => b ? 1 : 0).join(""); +} +function eqNum(v: any, n: number): boolean { return v === n; } +function neNum(v: any, n: number): boolean { return n !== v; } +// 2. Relational against a Number: undefined, null and booleans convert +// inline; strings, BigInts and objects keep the coercing helper; a Symbol throws. +function rel(v: any, n: number): string { + try { + return [v < n, v <= n, v > n, v >= n, n < v, n <= v, n > v, n >= v].map(b => b ? 1 : 0).join(""); + } catch (e) { + return "throws:" + (e as Error).constructor.name; + } +} +// 3. Two unproven operands: the `===` prefix and the dynamic relational. +function eqAny(a: any, b: any): boolean { return a === b; } +function ltAny(a: any, b: any): string { + try { return String(a < b); } catch (e) { return "T"; } +} +// 4. A comparison graph over one unproven local. +function graph(v: any): string { + const b = v === 0 || v === 7 || v === 2.5 || v !== v; + let a = "T", c = "T"; + try { a = v >= 1 && v < 8 || v === -1 ? "1" : "0"; } catch (e) {} + try { c = String(v > 100 || v <= -100 || v >= 2 && v <= 3); } catch (e) {} + return a + (b ? 1 : 0) + c; +} +// 5. switch: a dense numeric tree, and a tower mixing literal kinds. +function dense(v: any): string { + switch (v) { + case 0: return "zero"; + case 1: return "one"; + case 2: return "two"; + case 3: return "three"; + case 7: return "seven"; + case -1: return "minus"; + case 2147483647: return "max"; + case -2147483648: return "min"; + default: return "none"; + } +} +function tower(v: any): string { + switch (v) { + case "7": return "str7"; + case 7: return "num7"; + case true: return "true"; + case null: return "null"; + case undefined: return "undef"; + case 0: return "num0"; + case "": return "empty"; + default: return "none"; + } +} +// 6. An unproven array key: only a non-negative int32 Number indexes. +const table = [10, 11, 12, 13, 14, 15, 16, 17]; +function at(k: any): string { + const t: any = table; + let r: any; + try { r = t[k]; } catch (e) { return "T"; } + return typeof r === "function" ? "fn" : String(r); +} +function put(k: any): string { + const t: any = [0, 0, 0]; + // A huge index would only measure sparse-array growth. + if (typeof k === "number" && k > 1000) return "big"; + try { t[k] = 5; } catch (e) { return "T"; } + return t.length + ":" + t[0] + t[1] + t[2]; +} +// 7. Typed entries: Number and int32 lanes, called with every kind. +function addNum(a: number, b: number): number { return a + b; } +function mixI32(a: number, b: number): number { return (a ^ b) | (a & 255); } +function isPos(a: number): boolean { return a > 0; } +class Bits { + seed: number; + constructor(seed: number) { this.seed = seed; } + mix(a: number, b: number): number { return ((a ^ b) + this.seed) | 0; } + scale(a: number): number { return a * 1.5 + this.seed; } + get(i: number): number { return table[i] ?? -1; } +} +const bits = new Bits(3); +function callEntries(v: any): string { + const out: string[] = []; + const f: any = addNum, g: any = mixI32, h: any = isPos, b: any = bits; + for (const call of [() => f(v, 1), () => f(1, v), () => g(v, 5), () => g(5, v), () => h(v), () => b.mix(v, 9), () => b.scale(v), () => b.get(v)]) { + try { out.push(show(call())); } catch (e) { out.push("T"); } + } + return out.join(" "); +} +// 8. Closures: a direct typed call and a captured Number. +function closures(v: any): string { + const k = 2; + const mul = (x: number): number => x * k; + const add = (x: number, y: number): number => x + y + k; + const m: any = mul, a: any = add; + let r = ""; + try { r += show(m(v)) + " " + show(a(v, 1)) + " " + show(a(1, v)); } catch (e) { r += "T"; } + return r; +} +// 9. A method on an unproven receiver that may be a Number. +function method(v: any): string { + try { return String(v.toString()) + "/" + typeof v.valueOf(); } catch (e) { return "T"; } +} +// 10. Arithmetic over unproven leaves (the guarded diamond's dynamic arm). +function arith(v: any): string { + try { return [show(v + 1), show(v - 1), show(v * 2), show(v | 0), show(v >>> 0), show((v >> 1) & 7)].join(","); } catch (e) { return "T"; } +} + +const all = vals(); +for (let i = 0; i < all.length; i++) { + const v = all[i]; + console.log( + show(v) + " | " + eqLit(v) + " " + (eqNum(v, 7) ? 1 : 0) + (neNum(v, 7) ? 1 : 0) + (eqNum(v, NaN) ? 1 : 0) + (eqNum(v, -0) ? 1 : 0) + + " | " + rel(v, 7) + " " + rel(v, 0) + " " + rel(v, NaN) + + " | " + graph(v) + " | " + dense(v) + " " + tower(v) + + " | " + at(v) + " " + put(v) + " | " + callEntries(v) + " | " + closures(v) + " | " + method(v) + " | " + arith(v), + ); +} +// Pairs of unproven operands, every kind against every kind. +let grid = ""; +for (let i = 0; i < all.length; i++) { + for (let j = 0; j < all.length; j++) grid += (eqAny(all[i], all[j]) ? "=" : ".") + ltAny(all[i], all[j]).charAt(0); + grid += "\n"; +} +console.log(grid); + +// Numbers that only look like the old tagged form: integers computed and +// stored through unproven slots keep comparing as the doubles they are. +const box: any = { n: 5, list: [5, 6, 7] as any[] }; +function reread(o: any): string { + const n = o.n, e = o.list[1]; + return [n === 5, e === 6, n + e, o.list[n - 3], dense(n - 4), dense(e - 6), tower(n + 2), n < e, e <= 6, isPos(n - 5), mixI32(n, e)].join(","); +} +console.log(reread(box), reread(JSON.parse('{"n":5,"list":[5,6,7]}')));