diff --git a/changelog.d/step5-p7-region-f64.md b/changelog.d/step5-p7-region-f64.md new file mode 100644 index 0000000000..380c7f3d79 --- /dev/null +++ b/changelog.d/step5-p7-region-f64.md @@ -0,0 +1,3 @@ +### Performance + +- Loop regions can use shape-proven F64 field reads in Number arithmetic, including values carried through local variables. Learned regions refuse receivers whose requested field is not an identity F64 lane and use the generic path for them. diff --git a/crates/perry-codegen/src/codegen/static_shape_ids.rs b/crates/perry-codegen/src/codegen/static_shape_ids.rs index 32229c9608..ade9a3db9e 100644 --- a/crates/perry-codegen/src/codegen/static_shape_ids.rs +++ b/crates/perry-codegen/src/codegen/static_shape_ids.rs @@ -504,13 +504,14 @@ pub(crate) fn static_shape_id_for_keys_global(keys_global: &str) -> Option /// inline key of the birth shape, or when a key in `boxed_mask` (a bare /// store of a value not proven a canonical double) sits on a non-`Any` lane /// of the birth rep: the runtime's pack refuses that word too (charter step -/// 5). A returned id is a guard immediate: it joins the module's seed set -/// like any other. +/// 5). The third result is R in region-key order: only identity F64 lanes +/// of this exact birth ShapeId set a bit. A returned id is a guard immediate: +/// it joins the module's seed set like any other. pub(crate) fn static_region_slots( keys_global: &str, keys: &[String], boxed_mask: u32, -) -> Option<(u32, Vec)> { +) -> Option<(u32, Vec, u32)> { MODULE_STATIC_IDS.with(|m| { let m = m.borrow(); let (id, shape) = m.get(keys_global)?; @@ -538,8 +539,15 @@ pub(crate) fn static_region_slots( { return None; } + let r_mask = slots.iter().enumerate().fold(0u32, |mask, (i, &slot)| { + if (shape.rep >> (2 * slot)) & 0b11 == 0b01 { + mask | (1 << i) + } else { + mask + } + }); note_guard_id(*id, Some(shape)); - Some((*id, slots)) + Some((*id, slots, r_mask)) }) } diff --git a/crates/perry-codegen/src/codegen/static_shape_ids_tests.rs b/crates/perry-codegen/src/codegen/static_shape_ids_tests.rs index ca396503f7..9a7a8d3011 100644 --- a/crates/perry-codegen/src/codegen/static_shape_ids_tests.rs +++ b/crates/perry-codegen/src/codegen/static_shape_ids_tests.rs @@ -368,3 +368,28 @@ fn class_birth_names_anon_shapes_as_literals_and_skips_class_zero() { assert_eq!(o.class_id, 0); assert!(o.shape.is_none()); } + +#[test] +fn region_static_r_is_the_exact_birth_shapes_f64_key_mask() { + let shape = BirthShape { + rep: 0b01 | (0b01 << 4), + ..class("ra\0rb\0rc\0", 3, 0x517) + }; + let global = "p7_region_keys".to_string(); + MODULE_STATIC_IDS.with(|m| { + m.borrow_mut() + .insert(global.clone(), (SHAPE_ID_BASE + 917, shape)); + }); + let keys = vec!["rc".to_string(), "rb".to_string(), "ra".to_string()]; + let (_, slots, r) = static_region_slots(&global, &keys, 0).expect("birth keys are inline"); + assert_eq!(slots, vec![2, 1, 0]); + assert_eq!(r, 0b101, "R follows key order, not birth slot order"); + assert!( + static_region_slots(&global, &keys, 0b001).is_none(), + "a boxed store to an F64 birth lane is refused" + ); + MODULE_STATIC_IDS.with(|m| { + m.borrow_mut().remove(&global); + }); + take_module_static_seeds(); +} diff --git a/crates/perry-codegen/src/collectors/mod.rs b/crates/perry-codegen/src/collectors/mod.rs index 1a3ea444fa..ac2a98cc14 100644 --- a/crates/perry-codegen/src/collectors/mod.rs +++ b/crates/perry-codegen/src/collectors/mod.rs @@ -117,6 +117,10 @@ pub(crate) use proven_this::{ tower_route_profitable as pshape_tower_route_profitable, }; pub(crate) use ptr_numarray::{NumArrayDensity, NumArrayLocal}; +pub(crate) use ptr_shape::{ + collect_numeric_by_construction_locals_in_region, region_number_flow_reads, + RegionNumberAssumptions, +}; pub(crate) use ptr_shape::{ptr_shape_locals_enabled, PtrShapeLocal}; pub(crate) use ptr_shape_callbacks::collect_array_callback_shapes; pub(crate) use ptr_shape_returns::collect_exported_return_shapes; diff --git a/crates/perry-codegen/src/collectors/ptr_shape.rs b/crates/perry-codegen/src/collectors/ptr_shape.rs index dda88b6e52..bed993494d 100644 --- a/crates/perry-codegen/src/collectors/ptr_shape.rs +++ b/crates/perry-codegen/src/collectors/ptr_shape.rs @@ -1968,6 +1968,10 @@ mod numeric; use numeric::{ collect_numeric_by_construction_locals, prove_group_numeric_fields, prove_numeric_fields, }; +pub(crate) use numeric::{ + collect_numeric_by_construction_locals_in_region, region_number_flow_reads, + RegionNumberAssumptions, +}; // #8105: the same locals fixpoint, consumed outside the `Ptr` pass by // `collectors/number_by_construction.rs`. pub(in crate::collectors) use numeric::collect_numeric_by_construction_locals as collect_numeric_by_construction_locals_for_type_analysis; diff --git a/crates/perry-codegen/src/collectors/ptr_shape_numeric.rs b/crates/perry-codegen/src/collectors/ptr_shape_numeric.rs index 1e3ef0550f..b3970abb85 100644 --- a/crates/perry-codegen/src/collectors/ptr_shape_numeric.rs +++ b/crates/perry-codegen/src/collectors/ptr_shape_numeric.rs @@ -413,6 +413,13 @@ pub(super) fn prove_group_numeric_fields<'a>( // ── #7770: numeric-by-construction locals ────────────────────────────────── +/// Extra leaves and entry candidates for one guarded region's F clone. +pub(crate) struct RegionNumberAssumptions<'a> { + pub(crate) entry_candidates: &'a HashSet, + pub(crate) static_numbers: &'a HashSet, + pub(crate) f64_reads: &'a HashSet, +} + /// Locals whose every write is number-producing by construction — above all /// the loop counter (`let i = 0` + `i++`) that feeds a provenance /// `new C(i, i + 1)`. @@ -448,6 +455,38 @@ pub(in crate::collectors) fn collect_numeric_by_construction_locals<'a>( // completely `o`'s shape was proven. Empty for every pre-existing caller. shape_members: &HashSet, shape_numeric_fields: &HashSet, +) -> HashSet { + collect_numeric_by_construction_locals_in_region( + stmts, + boxed_vars, + module_globals, + not_bigint_locals, + const_local_inits, + numeric_ta_views, + shape_members, + shape_numeric_fields, + None, + ) +} + +pub(crate) fn collect_numeric_by_construction_locals_in_region<'a>( + stmts: &'a [Stmt], + boxed_vars: &HashSet, + module_globals: &HashMap, + not_bigint_locals: &HashSet, + const_local_inits: &HashMap>, + // #8619: view bindings proven to hold a numeric-kind typed array (spec-ABI + // `TaPtr` params). Empty for the `Ptr` type-analysis caller. + numeric_ta_views: &HashSet, + // #10777: shape-proven receivers visible to THIS walk, and the property + // names numeric on all of them. Both were hardcoded empty here, so + // `expr_numeric_by_construction`'s `PropertyGet` arm — gated on + // `members.contains(id)` — could never fire for a function-scope walk. An + // accumulator written `h = h + o.a` was therefore never admitted, however + // completely `o`'s shape was proven. Empty for every pre-existing caller. + shape_members: &HashSet, + shape_numeric_fields: &HashSet, + region: Option<&RegionNumberAssumptions<'_>>, ) -> HashSet { // ONE write walker for both fixpoints (`collect_not_bigint_locals` and // this one) — see its doc for why sharing is load-bearing. `None` = a @@ -464,6 +503,12 @@ pub(in crate::collectors) fn collect_numeric_by_construction_locals<'a>( // constructors without trusting their erased annotation. let mut stable_local_inits = const_local_inits.clone(); for (&id, local_writes) in &writes { + // An entry-tested loop-carried local can have just one F-body write. + // That write is not a stable initializer and must be judged through + // the running fixed-point assumption (h = h + x). + if region.is_some_and(|r| r.entry_candidates.contains(&id)) { + continue; + } if let [Some(init)] = local_writes.as_slice() { stable_local_inits.entry(id).or_insert(Some(*init)); } @@ -472,17 +517,28 @@ pub(in crate::collectors) fn collect_numeric_by_construction_locals<'a>( let empty_fields: HashSet = shape_numeric_fields.clone(); let mut numeric: HashSet = let_bound .into_iter() + .chain( + region + .into_iter() + .flat_map(|r| r.entry_candidates.iter().copied()), + ) .filter(|id| !boxed_vars.contains(id) && !module_globals.contains_key(id)) .collect(); + if let Some(r) = region { + numeric.extend(r.static_numbers.iter().copied()); + } loop { let mut drop: Vec = Vec::new(); for &id in &numeric { + if region.is_some_and(|r| r.static_numbers.contains(&id)) { + continue; + } let ok = writes .get(&id) .map(|ws| { ws.iter().all(|w| match w { None => false, - Some(e) => expr_numeric_by_construction( + Some(e) => expr_numeric_by_construction_with_region( e, &ParamEnv::None, &empty_members, @@ -492,12 +548,13 @@ pub(in crate::collectors) fn collect_numeric_by_construction_locals<'a>( &numeric, numeric_ta_views, 0, + region.map(|r| r.f64_reads), ), }) }) - // A `let_bound` id always has its `Let` recorded; treat a - // missing entry as unproven rather than as vacuously true. - .unwrap_or(false); + // Only a strictly tested entry candidate may have no local + // write inside F; its incoming value is the guarded leaf. + .unwrap_or_else(|| region.is_some_and(|r| r.entry_candidates.contains(&id))); if !ok { drop.push(id); } @@ -512,6 +569,48 @@ pub(in crate::collectors) fn collect_numeric_by_construction_locals<'a>( numeric } +/// Trace Number-consuming local uses back through the shared exhaustive +/// write inventory to the property reads feeding them. The caller intersects +/// these expression identities with the planner's fresh bare reads, so a +/// read after an E2 call never becomes an R leaf. +pub(crate) fn region_number_flow_reads( + stmts: &[Stmt], + roots: &HashSet, +) -> (HashSet, HashSet, HashSet) { + fn deps(e: &Expr, reads: &mut HashSet, locals: &mut Vec) { + match e { + Expr::PropertyGet { .. } => { + reads.insert(e as *const Expr as usize); + return; + } + Expr::LocalGet(id) => { + locals.push(*id); + return; + } + _ => {} + } + perry_hir::walker::walk_expr_children(e, &mut |child| deps(child, reads, locals)); + } + + let mut writes = HashMap::new(); + let mut bound = HashSet::new(); + super::super::not_bigint_locals::collect_writes(stmts, &mut writes, &mut bound); + let mut reads = HashSet::new(); + let mut seen = HashSet::new(); + let mut pending: Vec = roots.iter().copied().collect(); + while let Some(id) = pending.pop() { + if !seen.insert(id) { + continue; + } + if let Some(ws) = writes.get(&id) { + for value in ws.iter().flatten() { + deps(value, &mut reads, &mut pending); + } + } + } + (reads, seen, bound) +} + // ── The expression-level proof ───────────────────────────────────────────── /// Number-by-construction: the expression's runtime value is a JS Number for @@ -535,13 +634,48 @@ pub(super) fn expr_numeric_by_construction( // pass). numeric_ta_views: &HashSet, depth: usize, +) -> bool { + expr_numeric_by_construction_with_region( + e, + param_env, + members, + numeric_fields, + not_bigint_locals, + const_local_inits, + numeric_locals, + numeric_ta_views, + depth, + None, + ) +} + +#[allow(clippy::too_many_arguments)] +fn expr_numeric_by_construction_with_region( + e: &Expr, + param_env: &ParamEnv<'_>, + members: &HashSet, + numeric_fields: &HashSet, + not_bigint_locals: &HashSet, + const_local_inits: &HashMap>, + numeric_locals: &HashSet, + // #8619: view bindings PROVEN to permanently hold a numeric-kind typed + // array — a spec-ABI `TaPtr` parameter (the entry contract binds the raw + // header of a proven numeric non-view typed array). A read + // `view_id[numeric_index]` is then a Number (in-bounds) or `undefined` + // (OOB) by construction, never a pointer/string, which the Add rule below + // launders into a genuine Number. Empty on every path that is not a + // specialized-entry local proof (the class-field provers, the `Ptr` + // pass). + numeric_ta_views: &HashSet, + depth: usize, + region_f64_reads: Option<&HashSet>, ) -> bool { if depth > 16 { return false; } use perry_hir::BinaryOp; let rec = |x: &Expr| { - expr_numeric_by_construction( + expr_numeric_by_construction_with_region( x, param_env, members, @@ -551,6 +685,7 @@ pub(super) fn expr_numeric_by_construction( numeric_locals, numeric_ta_views, depth + 1, + region_f64_reads, ) }; // A numeric index into one of these compiler-owned constructors can only @@ -628,6 +763,12 @@ pub(super) fn expr_numeric_by_construction( numeric_storage && rec(index) }; match e { + Expr::PropertyGet { .. } + if region_f64_reads + .is_some_and(|reads| reads.contains(&(e as *const Expr as usize))) => + { + true + } Expr::Number(_) | Expr::Integer(_) | Expr::PodLayoutSizeOf { .. } @@ -738,7 +879,7 @@ pub(super) fn expr_numeric_by_construction( return !sites.is_empty() && sites.iter().all(|args| { args.get(pos).map(|a| { - expr_numeric_by_construction( + expr_numeric_by_construction_with_region( a, &ParamEnv::None, members, @@ -748,6 +889,7 @@ pub(super) fn expr_numeric_by_construction( numeric_locals, numeric_ta_views, depth + 1, + region_f64_reads, ) }) == Some(true) }); @@ -762,7 +904,7 @@ pub(super) fn expr_numeric_by_construction( // A single-Let const temp: chase its init (function // scope, so no parameter mapping applies to it). if let Some(Some(init)) = const_local_inits.get(id) { - return expr_numeric_by_construction( + return expr_numeric_by_construction_with_region( init, &ParamEnv::None, members, @@ -772,6 +914,7 @@ pub(super) fn expr_numeric_by_construction( numeric_locals, numeric_ta_views, depth + 1, + region_f64_reads, ); } // #7770: a local every one of whose writes is @@ -819,3 +962,108 @@ pub(super) fn expr_provably_not_bigint(e: &Expr, not_bigint_locals: &HashSet Expr { + Expr::PropertyGet { + object: Box::new(Expr::LocalGet(OBJECT)), + property: "x".to_string(), + byte_offset: 0, + } + } + + fn let_read(id: u32) -> Stmt { + Stmt::Let { + id, + name: format!("n{id}"), + ty: perry_hir::types::Type::Any, + mutable: false, + init: Some(read()), + } + } + + fn add_to_acc(id: u32) -> Stmt { + Stmt::Expr(Expr::LocalSet( + ACC, + Box::new(Expr::Binary { + op: perry_hir::BinaryOp::Add, + left: Box::new(Expr::LocalGet(ACC)), + right: Box::new(Expr::LocalGet(id)), + }), + )) + } + + fn number_set(stmts: &[Stmt], fresh_read: usize) -> HashSet { + let boxed = HashSet::new(); + let globals = HashMap::new(); + let empty_ids = HashSet::new(); + let empty_fields = HashSet::new(); + let inits = HashMap::new(); + let entry = HashSet::from([ACC]); + let reads = HashSet::from([fresh_read]); + let region = RegionNumberAssumptions { + entry_candidates: &entry, + static_numbers: &empty_ids, + f64_reads: &reads, + }; + collect_numeric_by_construction_locals_in_region( + stmts, + &boxed, + &globals, + &empty_ids, + &inits, + &empty_ids, + &empty_ids, + &empty_fields, + Some(®ion), + ) + } + + #[test] + fn an_e2_stale_second_read_drops_the_loop_carried_number_fact() { + let mut stmts = vec![let_read(FRESH), add_to_acc(FRESH)]; + let fresh_ptr = match &stmts[0] { + Stmt::Let { init: Some(e), .. } => e as *const Expr as usize, + _ => unreachable!(), + }; + let first = number_set(&stmts, fresh_ptr); + assert!(first.contains(&FRESH) && first.contains(&ACC)); + + // The intervening call makes the second slot read stale in the + // region planner. Only the first read's exact Expr identity is an + // F64 leaf; the second write must withdraw ACC from N_F. + stmts.push(Stmt::Expr(Expr::Call { + callee: Box::new(Expr::LocalGet(99)), + args: Vec::new(), + type_args: Vec::new(), + byte_offset: 0, + })); + stmts.push(let_read(STALE)); + stmts.push(add_to_acc(STALE)); + let fresh_ptr = match &stmts[0] { + Stmt::Let { init: Some(e), .. } => e as *const Expr as usize, + _ => unreachable!(), + }; + let stale_ptr = match &stmts[3] { + Stmt::Let { init: Some(e), .. } => e as *const Expr as usize, + _ => unreachable!(), + }; + let roots = HashSet::from([ACC]); + let (flows, locals, bound) = region_number_flow_reads(&stmts, &roots); + assert!(flows.contains(&fresh_ptr) && flows.contains(&stale_ptr)); + assert!(locals.contains(&ACC) && locals.contains(&FRESH) && locals.contains(&STALE)); + assert!(bound.contains(&FRESH) && bound.contains(&STALE) && !bound.contains(&ACC)); + let after = number_set(&stmts, fresh_ptr); + assert!(after.contains(&FRESH)); + assert!(!after.contains(&STALE)); + assert!(!after.contains(&ACC), "the stale write must drop ACC"); + } +} diff --git a/crates/perry-codegen/src/expr/receiver_range.rs b/crates/perry-codegen/src/expr/receiver_range.rs index 9e58ab1ae7..36db635a79 100644 --- a/crates/perry-codegen/src/expr/receiver_range.rs +++ b/crates/perry-codegen/src/expr/receiver_range.rs @@ -130,6 +130,9 @@ pub(crate) enum Route { /// A region guard's STATIC supplier matched (DESIGN §4.1): the receiver /// carries the driver's static id, so the region ran with no word. RloopStatic = 28, + /// One F-body iteration whose exact chosen supplier guarantees at least + /// one F64 region key (P7 acceptance census). + RloopFRep = 34, } /// `PERRY_RECV_ROUTE_COUNT=1` at COMPILE time: emit one diff --git a/crates/perry-codegen/src/expr/region_loop_tests.rs b/crates/perry-codegen/src/expr/region_loop_tests.rs index 8793dba439..c62d9b6201 100644 --- a/crates/perry-codegen/src/expr/region_loop_tests.rs +++ b/crates/perry-codegen/src/expr/region_loop_tests.rs @@ -64,7 +64,7 @@ fn put(key: &str, value: Expr) -> Stmt { } /// `function probe(o, v, n) { let h = 0; for (let i = 0; i < n; i++) { body } return h; }` -fn loop_ir(name: &str, body: Vec) -> String { +fn loop_ir_with_return(name: &str, body: Vec, result: Expr) -> String { let mut m = Module::new(name); m.functions = vec![Function { id: 1, @@ -100,7 +100,7 @@ fn loop_ir(name: &str, body: Vec) -> String { }), body, }, - Stmt::Return(Some(Expr::LocalGet(H))), + Stmt::Return(Some(result)), ], is_async: false, is_generator: false, @@ -115,6 +115,10 @@ fn loop_ir(name: &str, body: Vec) -> String { String::from_utf8(compile_module(&m, opts()).expect("module compiles")).expect("UTF-8 IR") } +fn loop_ir(name: &str, body: Vec) -> String { + loop_ir_with_return(name, body, Expr::LocalGet(H)) +} + /// The blocks of the probe function, label -> (instructions, successors). fn blocks(ir: &str) -> HashMap, Vec)> { let mut out = HashMap::new(); @@ -342,15 +346,15 @@ fn a_region_that_stores_every_key_it_names_has_no_spill_copy() { ); } -/// The prime call's last argument: the boxed-store mask (charter step 5). +/// The prime call's penultimate argument: the boxed-store mask (charter step 5). fn prime_boxed_masks(ir: &str) -> Vec { ir.lines() .filter(|l| l.contains("@js_region_loop_prime(")) .filter_map(|l| { - // The call can carry trailing attributes after its closing parenthesis. - let call = l.split_once(')')?.0; - let last_arg = call.rsplit_once("i32 ")?.1; - last_arg.trim().parse().ok() + // The R mask follows the boxed-store mask; the call may carry + // trailing LLVM attributes after its closing parenthesis. + let (before_r, _) = l.rsplit_once(", i32 ")?; + before_r.rsplit_once("i32 ")?.1.trim().parse().ok() }) .collect() } @@ -389,3 +393,103 @@ fn a_bare_store_of_a_value_not_proven_a_double_names_its_key_to_the_prime() { "a literal double is a valid value of every lane: {masks:?}" ); } + +/// Last prime argument, before LLVM's call attributes (e.g. the +/// `"gc-leaf-function"` suffix), is the requested region R mask. +fn prime_rep_masks(ir: &str) -> Vec { + ir.lines() + .filter(|line| line.contains("@js_region_loop_prime(")) + .filter_map(|line| { + line.rsplit_once("i32 ")? + .1 + .split_once(')')? + .0 + .trim() + .parse() + .ok() + }) + .collect() +} + +/// A fresh bare read used by a Number-consuming add requests an F64 lane. +/// The prime must refuse an Any receiver, so this is an actual R-bearing +/// region rather than a vacuous mask argument. +#[test] +fn a_number_consuming_bare_read_sets_the_prime_rep_mask() { + let ir = loop_ir( + "region_loop_rep", + vec![Stmt::Expr(Expr::LocalSet( + H, + Box::new(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::LocalGet(H)), + right: Box::new(get("x")), + }), + ))], + ); + assert!(ir.contains("rloop.fast"), "region did not form:\n{ir}"); + let masks = prime_rep_masks(&ir); + assert!(!masks.is_empty(), "no learned prime in\n{ir}"); + assert!( + masks.iter().all(|&m| m == 1), + "fresh x read must request key 0: {masks:?}" + ); +} + +/// The F-local fixed point follows the fresh F64 read through a temporary and +/// a loop-carried accumulator. Entry is strict; G and post-loop code retain +/// the ordinary dynamic add. Removing the scoped materialization or the +/// entry check makes this test fail. +#[test] +fn a_region_number_local_is_admitted_only_in_f() { + const TEMP: u32 = 6; + let body = vec![ + Stmt::Let { + id: TEMP, + name: "temp".to_string(), + ty: Type::Any, + mutable: false, + init: Some(get("x")), + }, + Stmt::Expr(Expr::LocalSet( + H, + Box::new(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::LocalGet(H)), + right: Box::new(Expr::LocalGet(TEMP)), + }), + )), + ]; + let ir = loop_ir_with_return( + "region_number_scope", + body, + Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::LocalGet(H)), + right: Box::new(Expr::Integer(1)), + }, + ); + assert!(ir.contains("rloop.fast"), "region did not form:\n{ir}"); + let masks = prime_rep_masks(&ir); + assert!( + !masks.is_empty() && masks.iter().all(|&m| m == 1), + "the temp's source must request R=1: {masks:?}\n{ir}" + ); + assert!( + ir.contains("rloop.fast") && ir.contains("fadd double"), + "F must use numeric add:\n{ir}" + ); + assert!( + ir.contains("rloop.guard") && ir.contains("icmp ult i64"), + "A_F must strictly test the loop-carried accumulator:\n{ir}" + ); + assert!( + ir.lines() + .filter(|line| { + line.contains("call ") && line.contains("@js_dynamic_string_or_number_add(") + }) + .count() + >= 2, + "G and post-loop adds must remain dynamic (no scope leak):\n{ir}" + ); +} diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index c368d29d9e..dfaecc1d3e 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -1136,7 +1136,7 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { module.declare_function( "js_region_loop_prime", I64, - &[PTR, I32, I32, I64, I64, I64, I64, I64, I32, I32, I32], + &[PTR, I32, I32, I64, I64, I64, I64, I64, I32, I32, I32, I32], ); // Design step 4: the per-class mint with the driver's static id, and the // literal-shape seed. diff --git a/crates/perry-codegen/src/stmt/region_loop/guard.rs b/crates/perry-codegen/src/stmt/region_loop/guard.rs index a90702caa4..c3e109c7ea 100644 --- a/crates/perry-codegen/src/stmt/region_loop/guard.rs +++ b/crates/perry-codegen/src/stmt/region_loop/guard.rs @@ -92,6 +92,7 @@ pub(super) fn emit_prime_call( (I32, &last), (I32, &rv.stored_mask.to_string()), (I32, &rv.boxed_mask.to_string()), + (I32, &rv.r_mask.to_string()), ], ) } @@ -117,6 +118,7 @@ pub(super) fn emit_body_guard_direct( rv: &Receiver, sites: &Sites, word: &str, + entry_tests: &[u32], inline_l: &str, spill_l: &str, fail_l: &str, @@ -145,7 +147,11 @@ pub(super) fn emit_body_guard_direct( let handle = crate::expr::receiver_range::emit_handle(ctx.block(), &test.biased); let expected = ctx.block().trunc(I64, word, I32); let sid = field_i32(ctx, &handle, 4); - let eq = ctx.block().icmp_eq(I32, &sid, &expected); + let mut eq = ctx.block().icmp_eq(I32, &sid, &expected); + if !entry_tests.is_empty() { + let number_ok = emit_number_entry_tests(ctx, entry_tests)?; + eq = ctx.block().and(I1, &eq, &number_ok); + } let admit = if rv.has_store { Some(store_admission(ctx, &handle, true)) } else { @@ -193,7 +199,7 @@ pub(super) fn emit_body_guard_direct( /// ShapeId against the id, so a declared type (a parameter `p: C`, a /// reassigned binding) serves as well as a proven one — a receiver of any /// other shape misses into the learned supplier. -fn static_region_word(ctx: &FnCtx<'_>, rv: &Receiver) -> Option { +fn static_region_word(ctx: &FnCtx<'_>, rv: &Receiver) -> Option<(u64, u32)> { let class_name = crate::type_analysis::receiver_class_name(ctx, &rv.recv.expr()).or_else(|| { match rv.recv { @@ -210,12 +216,13 @@ fn static_region_word(ctx: &FnCtx<'_>, rv: &Receiver) -> Option { } })?; let keys_global = ctx.class_keys_globals.get(&class_name)?; - let (id, slots) = crate::codegen::static_region_slots(keys_global, &rv.keys, rv.boxed_mask)?; + let (id, slots, r_mask) = + crate::codegen::static_region_slots(keys_global, &rv.keys, rv.boxed_mask)?; let mut word = u64::from(id); for (i, slot) in slots.iter().enumerate() { word |= u64::from(*slot) << (32 + SLOT_BITS * i as u32); } - Some(word) + Some((word, r_mask)) } /// A guard whose receiver the compiler names (DESIGN §4.1, static-exclusive): @@ -279,7 +286,8 @@ pub(super) fn emit_guard( ) -> Result<(String, String, String)> { // A receiver whose class the compiler names takes its guard's ShapeId // from the driver's static id (DESIGN §4.1): no loaded supplier. - if let Some(w) = static_region_word(ctx, rv) { + if let Some((w, r_mask)) = static_region_word(ctx, rv) { + rv.r_mask = r_mask; return emit_static_guard(ctx, rv, w); } // A retired region (every bounded prime refused) is decided by the word diff --git a/crates/perry-codegen/src/stmt/region_loop/mod.rs b/crates/perry-codegen/src/stmt/region_loop/mod.rs index b601823ef3..d77a61cee6 100644 --- a/crates/perry-codegen/src/stmt/region_loop/mod.rs +++ b/crates/perry-codegen/src/stmt/region_loop/mod.rs @@ -184,6 +184,8 @@ pub(crate) struct Receiver { /// Bit `i`: a bare store may write `keys[i]` a value not proven a /// canonical double (the word must then give it an `Any` lane). boxed_mask: u32, + /// Bit `i`: a read assumes the key has an identity F64 lane. + r_mask: u32, /// `i1`: the guard matched this receiver's SPILL word (flipped id). spill: String, sites: Option<(String, String)>, @@ -194,6 +196,37 @@ pub(crate) struct Receiver { slots: Vec, } +/// Whether this exact fresh bare read is protected by an R bit in an +/// active F clone. The published word (or exact static birth id) guarantees +/// the slot holds a canonical raw JS Number; G and post-loop have no Active +/// fact and therefore never answer true. +pub(crate) fn is_f64_read(ctx: &FnCtx<'_>, e: &Expr) -> bool { + let Expr::PropertyGet { + object, property, .. + } = e + else { + return false; + }; + let Some(recv) = Recv::of(object) else { + return false; + }; + let ptr = e as *const Expr as usize; + // bare::try_lower_bare_get consults only the innermost Active fact. + // Looking through the stack would claim Number for a read the inner + // clone actually lowers through the generic path. + ctx.region_loop_facts.last().is_some_and(|facts| { + facts.bare.contains(&ptr) + && facts.receivers.iter().any(|rv| { + rv.recv == recv + && rv + .keys + .iter() + .position(|key| key == property) + .is_some_and(|i| rv.r_mask & (1 << i) != 0) + }) + }) +} + /// A region whose body has not been lowered yet: `lower_stmts` recognises the /// body slice by address and splits it. #[derive(Clone)] @@ -209,6 +242,11 @@ pub(crate) struct Pending { recheck: Recheck, receivers: Vec, bare: HashSet, + bare_reads: Vec<(usize, Recv, String)>, + number_local_uses: HashSet, + declared_locals: HashSet, + number_locals: Vec, + entry_tests: Vec, trees: HashSet, token: u64, /// Loop regions: which split copy [`lower_loop`] is lowering — the one @@ -419,12 +457,13 @@ fn begin_with( let mut receivers: Vec = p .receivers .iter() - .map(|(r, k, st, sm, bm)| Receiver { + .map(|(r, k, st, sm, bm, rm)| Receiver { recv: *r, keys: k.clone(), has_store: *st, stored_mask: effective_stored_mask(*sm, k.len()), boxed_mask: *bm, + r_mask: *rm, spill: "false".to_string(), sites: None, word: String::new(), @@ -449,6 +488,18 @@ fn begin_with( all = ctx.block().and(I1, &all, &pass); arrs.push(a); } + let (number_locals, entry_tests) = number_facts( + ctx, + body, + &receivers, + &p.bare_reads, + &p.number_local_uses, + &p.declared_locals, + ); + if !entry_tests.is_empty() { + let number_ok = emit_number_entry_tests(ctx, &entry_tests)?; + all = ctx.block().and(I1, &all, &number_ok); + } ctx.block().store(I1, &all, &valid_slot); let dirty_slot = ctx.func.alloca_entry(I1); ctx.block().store(I1, "false", &dirty_slot); @@ -468,6 +519,11 @@ fn begin_with( recheck: p.recheck, receivers, bare: p.bare, + bare_reads: p.bare_reads, + number_local_uses: p.number_local_uses, + declared_locals: p.declared_locals, + number_locals, + entry_tests, trees: p.trees, token, spill_mode: false, @@ -530,6 +586,89 @@ fn body_region_plan(ctx: &FnCtx<'_>, body: &[Stmt]) -> Option<(usize, Plan)> { None } +/// Resolve the planner's exact fresh reads against the R guaranteed by +/// each receiver's chosen supplier, then run the existing Number-local +/// greatest fixed point with those reads as additional leaves. +fn number_facts( + ctx: &FnCtx<'_>, + tail: &[Stmt], + receivers: &[Receiver], + bare_reads: &[(usize, Recv, String)], + number_local_uses: &HashSet, + declared_locals: &HashSet, +) -> (Vec, Vec) { + let f64_reads: HashSet = bare_reads + .iter() + .filter_map(|(ptr, recv, key)| { + receivers.iter().find(|rv| rv.recv == *recv).and_then(|rv| { + rv.keys + .iter() + .position(|k| k == key) + .filter(|i| rv.r_mask & (1 << i) != 0) + .map(|_| *ptr) + }) + }) + .collect(); + if f64_reads.is_empty() { + return (Vec::new(), Vec::new()); + } + let entry_candidates: HashSet = number_local_uses + .iter() + .copied() + .filter(|id| { + ctx.locals.contains_key(id) + && !declared_locals.contains(id) + && !ctx.boxed_vars.contains(id) + && !ctx.module_globals.contains_key(id) + && !ctx.number_by_construction_locals.contains(id) + }) + .collect(); + let empty_inits = HashMap::new(); + let empty_ids = HashSet::new(); + let empty_fields = HashSet::new(); + let assumptions = crate::collectors::RegionNumberAssumptions { + entry_candidates: &entry_candidates, + static_numbers: ctx.number_by_construction_locals, + f64_reads: &f64_reads, + }; + let numeric = crate::collectors::collect_numeric_by_construction_locals_in_region( + tail, + &ctx.boxed_vars, + ctx.module_globals, + ctx.not_bigint_locals, + &empty_inits, + &empty_ids, + &empty_ids, + &empty_fields, + Some(&assumptions), + ); + let mut locals: Vec = numeric + .iter() + .copied() + .filter(|id| !ctx.number_by_construction_locals.contains(id)) + .collect(); + locals.sort_unstable(); + let mut tests: Vec = locals + .iter() + .copied() + .filter(|id| entry_candidates.contains(id)) + .collect(); + tests.sort_unstable(); + (locals, tests) +} + +/// Strict Number entry condition for each loop-carried local that the F +/// clone assumes. The same check is repeated when G can re-enter F. +fn emit_number_entry_tests(ctx: &mut FnCtx<'_>, ids: &[u32]) -> Result { + let mut ok = "true".to_string(); + for &id in ids { + let value = lower_expr(ctx, &Expr::LocalGet(id))?; + let number = crate::stmt::loops::emit_js_value_is_number(ctx, &value); + ok = ctx.block().and(I1, &ok, &number); + } + Ok(ok) +} + /// A body region's pending split at `split_at`. fn body_pending(p: Plan, body: &[Stmt], split_at: usize) -> Pending { let token = NEXT_TOKEN.with(|t| { @@ -540,12 +679,13 @@ fn body_pending(p: Plan, body: &[Stmt], split_at: usize) -> Pending { let receivers: Vec = p .receivers .iter() - .map(|(r, k, st, sm, bm)| Receiver { + .map(|(r, k, st, sm, bm, rm)| Receiver { recv: *r, keys: k.clone(), has_store: *st, stored_mask: effective_stored_mask(*sm, k.len()), boxed_mask: *bm, + r_mask: *rm, spill: "false".to_string(), sites: None, word: String::new(), @@ -561,6 +701,11 @@ fn body_pending(p: Plan, body: &[Stmt], split_at: usize) -> Pending { recheck: Recheck::None, receivers, bare: p.bare, + bare_reads: p.bare_reads, + number_local_uses: p.number_local_uses, + declared_locals: p.declared_locals, + number_locals: Vec::new(), + entry_tests: Vec::new(), trees: p.trees, token, spill_mode: false, @@ -745,6 +890,11 @@ pub(crate) fn lower_split( }; let retry = ctx.region_loops[idx].retry.clone(); let bare = ctx.region_loops[idx].bare.clone(); + let bare_reads = ctx.region_loops[idx].bare_reads.clone(); + let number_local_uses = ctx.region_loops[idx].number_local_uses.clone(); + let declared_locals = ctx.region_loops[idx].declared_locals.clone(); + let planned_number_locals = ctx.region_loops[idx].number_locals.clone(); + let planned_entry_tests = ctx.region_loops[idx].entry_tests.clone(); let trees = ctx.region_loops[idx].trees.clone(); let dirty_slot = ctx.region_loops[idx].dirty_slot.clone(); let arrs = ctx.region_loops[idx].arrays.clone(); @@ -772,7 +922,7 @@ pub(crate) fn lower_split( // Where the entry decision is emitted; its terminator is written LAST, // once `verify` has judged F-body. - let decide; + let mut decide; let mut decide_top: Option<(usize, String, String)> = None; // With a nested body region the top tests the dirty flag first: G-body // sets it to come back (`Pending::retry`); `decide_top` then carries @@ -834,6 +984,10 @@ pub(crate) fn lower_split( let pass = arrays::emit_guard(ctx, a)?; ok = ctx.block().and(I1, &ok, &pass); } + if !planned_entry_tests.is_empty() { + let number_ok = emit_number_entry_tests(ctx, &planned_entry_tests)?; + ok = ctx.block().and(I1, &ok, &number_ok); + } ctx.block().store(I1, &ok, slot); ctx.block().store(I1, "false", &d_slot); let rc_idx = ctx.current_block; @@ -865,6 +1019,23 @@ pub(crate) fn lower_split( } } + let (number_locals, entry_tests) = if valid_slot.is_some() { + (planned_number_locals, planned_entry_tests) + } else { + number_facts( + ctx, + tail, + &receivers, + &bare_reads, + &number_local_uses, + &declared_locals, + ) + }; + if valid_slot.is_none() && direct.is_none() && !entry_tests.is_empty() { + let number_ok = emit_number_entry_tests(ctx, &entry_tests)?; + decide.1 = ctx.block().and(I1, &decide.1, &number_ok); + } + // F-body, once per layout; each copy is verified on its own IR. let mut copies: Vec<(String, bool)> = Vec::with_capacity(modes.len()); for (ci, &mode) in modes.iter().enumerate() { @@ -876,10 +1047,16 @@ pub(crate) fn lower_split( let fl = ctx.block_label(fb); ctx.current_block = fb; note(ctx, Route::RloopF); + if receivers.iter().any(|rv| rv.r_mask != 0) { + note(ctx, Route::RloopFRep); + } if let Some(d) = &retry { ctx.block().store(I1, "true", d); } let scan_start = ctx.func.num_blocks(); + let number_scope = ctx.next_loop_proof_scope_id(); + ctx.receiver_descriptors + .materialize_number_locals(number_scope, &number_locals); ctx.region_loop_facts.push(Active { receivers: receivers.clone(), bare: bare.clone(), @@ -902,6 +1079,7 @@ pub(crate) fn lower_split( None => lower_list(ctx, tail), }; let active = ctx.region_loop_facts.pop().expect("pushed above"); + ctx.receiver_descriptors.dematerialize_scope(number_scope); r?; if !ctx.block().is_terminated() { ctx.block().br(&join_l); @@ -998,7 +1176,16 @@ pub(crate) fn lower_split( slow_l.clone() }; let rv = receivers[0].clone(); - emit_body_guard_direct(ctx, &rv, sites, word, &inline_t, &spill_t, &slow_l)?; + emit_body_guard_direct( + ctx, + &rv, + sites, + word, + &entry_tests, + &inline_t, + &spill_t, + &slow_l, + )?; } else if copies.len() == 2 { // Body region: the guard passed -> pick the copy for the word's layout. let target = |c: &(String, bool)| if c.1 { c.0.clone() } else { slow_l.clone() }; diff --git a/crates/perry-codegen/src/stmt/region_loop/plan.rs b/crates/perry-codegen/src/stmt/region_loop/plan.rs index 1990c10876..5ff13698df 100644 --- a/crates/perry-codegen/src/stmt/region_loop/plan.rs +++ b/crates/perry-codegen/src/stmt/region_loop/plan.rs @@ -88,6 +88,35 @@ pub(super) fn fact_tree_leaves<'e>( Some((recv?, reads)) } +/// Candidate reads inside a Number-consuming expression. The planner's +/// exact bare-access set filters these by freshness after the walk. +fn number_operand_reads(e: &Expr, out: &mut Vec<(usize, Recv, String)>) { + if let Expr::PropertyGet { + object, property, .. + } = e + { + if let Some(r) = Recv::of(object) { + out.push((e as *const Expr as usize, r, property.clone())); + } + } + perry_hir::walker::walk_expr_children(e, &mut |child| number_operand_reads(child, out)); +} + +/// Local value operands whose incoming Number-ness can be discharged by +/// the region's strict entry tests. A receiver beneath PropertyGet is an +/// object, not a candidate Number value. +fn number_operand_locals(e: &Expr, out: &mut HashSet) { + match e { + Expr::LocalGet(id) => { + out.insert(*id); + return; + } + Expr::PropertyGet { .. } => return, + _ => {} + } + perry_hir::walker::walk_expr_children(e, &mut |child| number_operand_locals(child, out)); +} + pub(super) fn kill(st: &mut St) { if let Some(m) = st { m.clear(); @@ -121,6 +150,10 @@ pub(super) struct Planner<'p, 'a> { /// Array receivers (S3) and their static index bound. arrays: &'p HashMap, bare: HashSet, + /// Potential R keys, filtered against exact fresh bare reads at finish. + number_reads: Vec<(usize, Recv, String)>, + number_local_uses: HashSet, + bare_reads: Vec<(usize, Recv, String)>, bare_arrays: HashSet, /// A body region nested in this loop region (array regions): while its /// tail is walked, its bare accesses run no JS and its fact trees only @@ -156,7 +189,11 @@ impl Planner<'_, '_> { let fresh = st.as_ref().is_some_and(|m| m.get(&r) == Some(&FRESH)); if fresh && self.cands.contains(&r) && self.covered(r, key) { if self.record { - self.bare.insert(e as *const Expr as usize); + let ptr = e as *const Expr as usize; + self.bare.insert(ptr); + if !store { + self.bare_reads.push((ptr, r, key.to_string())); + } if store { self.bare_stores.insert(r); if boxed { @@ -265,6 +302,12 @@ impl Planner<'_, '_> { st } Expr::Binary { left, right, .. } => { + if self.record { + number_operand_reads(left, &mut self.number_reads); + number_operand_reads(right, &mut self.number_reads); + number_operand_locals(left, &mut self.number_local_uses); + number_operand_locals(right, &mut self.number_local_uses); + } let keys = self.keys; let cands = self.cands; let covered = |r: Recv, k: &str| { @@ -275,7 +318,16 @@ impl Planner<'_, '_> { if self.record { self.trees.insert(e as *const Expr as usize); for l in reads { - self.bare.insert(l as *const Expr as usize); + let ptr = l as *const Expr as usize; + self.bare.insert(ptr); + if let Expr::PropertyGet { + object, property, .. + } = l + { + if let Some(r) = Recv::of(object) { + self.bare_reads.push((ptr, r, property.clone())); + } + } } } dirty(&mut st); @@ -290,6 +342,10 @@ impl Planner<'_, '_> { st } Expr::Unary { op, operand } => { + if self.record && !matches!(op, UnaryOp::Not) { + number_operand_reads(operand, &mut self.number_reads); + number_operand_locals(operand, &mut self.number_local_uses); + } st = self.expr(operand, st); if !matches!(op, UnaryOp::Not) && !self.prim(operand) { kill(&mut st); @@ -297,6 +353,17 @@ impl Planner<'_, '_> { st } Expr::Compare { op, left, right } => { + if self.record + && matches!( + op, + CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge + ) + { + number_operand_reads(left, &mut self.number_reads); + number_operand_reads(right, &mut self.number_reads); + number_operand_locals(left, &mut self.number_local_uses); + number_operand_locals(right, &mut self.number_local_uses); + } st = self.expr(left, st); st = self.expr(right, st); if !matches!(op, CompareOp::Eq | CompareOp::Ne) @@ -797,9 +864,12 @@ pub(super) fn receiver_eligible(ctx: &FnCtx<'_>, r: Recv) -> bool { } pub(super) struct Plan { - /// `(receiver, keys, has a bare store, stored mask, boxed-store mask)`. - pub(super) receivers: Vec<(Recv, Vec, bool, u32, u32)>, + /// `(receiver, keys, has a bare store, stored mask, boxed-store mask, R mask)`. + pub(super) receivers: Vec<(Recv, Vec, bool, u32, u32, u32)>, pub(super) bare: HashSet, + pub(super) bare_reads: Vec<(usize, Recv, String)>, + pub(super) number_local_uses: HashSet, + pub(super) declared_locals: HashSet, pub(super) trees: HashSet, pub(super) recheck: Recheck, /// Array receivers with a bare read, and their static index bound. @@ -865,6 +935,9 @@ pub(super) fn plan( keys: &keys, arrays: &arrays, bare: HashSet::new(), + number_reads: Vec::new(), + number_local_uses: HashSet::new(), + bare_reads: Vec::new(), bare_arrays: HashSet::new(), inner: inner.map(|(_, b, t)| (b, t)), in_inner: false, @@ -927,6 +1000,12 @@ pub(super) fn plan( }; } let bare = std::mem::take(&mut p.bare); + let number_reads = std::mem::take(&mut p.number_reads); + let bare_reads = std::mem::take(&mut p.bare_reads); + let mut number_local_uses = std::mem::take(&mut p.number_local_uses); + let (flow_reads, flow_locals, declared_locals) = + crate::collectors::region_number_flow_reads(tail, &number_local_uses); + number_local_uses.extend(flow_locals); let trees = std::mem::take(&mut p.trees); let bare_stores = std::mem::take(&mut p.bare_stores); let boxed_stores = std::mem::take(&mut p.boxed_stores); @@ -959,7 +1038,7 @@ pub(super) fn plan( } } } - let mut receivers: Vec<(Recv, Vec, bool, u32, u32)> = used + let mut receivers: Vec<(Recv, Vec, bool, u32, u32, u32)> = used .into_iter() .map(|r| { let boxed = boxed_stores.get(&r).map_or(0, |ks| { @@ -969,19 +1048,33 @@ pub(super) fn plan( .filter(|(_, k)| ks.contains(*k)) .fold(0u32, |m, (i, _)| m | 1 << i) }); + let r_mask = bare_reads.iter().fold(0u32, |mask, (ptr, nr, key)| { + if *nr == r + && (flow_reads.contains(ptr) || number_reads.iter().any(|(p, _, _)| p == ptr)) + { + if let Some(i) = keys[&r].iter().position(|k| k == key) { + return mask | (1 << i); + } + } + mask + }); ( r, keys[&r].clone(), bare_stores.contains(&r), stored.get(&r).copied().unwrap_or(0), boxed, + r_mask, ) }) .collect(); - receivers.sort_by_key(|(r, _, _, _, _)| *r); + receivers.sort_by_key(|(r, _, _, _, _, _)| *r); Some(Plan { receivers, bare, + bare_reads, + number_local_uses, + declared_locals, trees, recheck, arrays: plan_arrays, diff --git a/crates/perry-codegen/src/type_analysis/numeric.rs b/crates/perry-codegen/src/type_analysis/numeric.rs index 190ec6c40e..8098d35368 100644 --- a/crates/perry-codegen/src/type_analysis/numeric.rs +++ b/crates/perry-codegen/src/type_analysis/numeric.rs @@ -351,6 +351,9 @@ pub(crate) fn is_numeric_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { Expr::PropertyGet { object, property, .. } => { + if crate::stmt::region_loop::is_f64_read(ctx, e) { + return true; + } if matches!( crate::lower_call::guarded_path_type(ctx, e), Some(HirType::Number | HirType::Int32) @@ -699,6 +702,9 @@ pub(crate) fn expr_produces_canonical_raw_f64(ctx: &FnCtx<'_>, e: &Expr) -> bool Expr::PropertyGet { object, property, .. } => { + if crate::stmt::region_loop::is_f64_read(ctx, e) { + return true; + } let Some(fact) = ctx.ptr_shape_receiver_fact(object.as_ref()) else { return false; }; diff --git a/crates/perry-codegen/src/wasm32/runtime_abi.tsv b/crates/perry-codegen/src/wasm32/runtime_abi.tsv index 6894712f35..1c093a6730 100644 --- a/crates/perry-codegen/src/wasm32/runtime_abi.tsv +++ b/crates/perry-codegen/src/wasm32/runtime_abi.tsv @@ -3194,7 +3194,7 @@ js_regexp_to_string ptr ptr js_region_guard_pack i64 i32u,i32u,i64,i64,i64,i64,i64 js_region_guard_prime i64 ptr,i32u,i32u,i64,i64,i64,i64,i64 js_region_loop_pack i64 i32u,i32u,i64,i64,i64,i64,i64,i32u,i32u -js_region_loop_prime i64 ptr,i32u,i32u,i64,i64,i64,i64,i64,i32u,i32u,i32u +js_region_loop_prime i64 ptr,i32u,i32u,i64,i64,i64,i64,i64,i32u,i32u,i32u,i32u js_register_anon_shape_class_id void i32u js_register_aux_has_active void ptr js_register_aux_pump void ptr diff --git a/crates/perry-runtime/src/hot_diag.rs b/crates/perry-runtime/src/hot_diag.rs index ef490bbbad..6aee6f97ed 100644 --- a/crates/perry-runtime/src/hot_diag.rs +++ b/crates/perry-runtime/src/hot_diag.rs @@ -1540,7 +1540,7 @@ fn buffer_dump() { /// Receiver-route admission census names, indexed by the route number the /// emitted call passes. **Must match `receiver_range::Route` in perry-codegen.** -const RECV_ROUTE_NAMES: [&str; 33] = [ +const RECV_ROUTE_NAMES: [&str; 35] = [ "generic", "generic_mru_hit", "generic_way_hit", @@ -1593,6 +1593,10 @@ const RECV_ROUTE_NAMES: [&str; 33] = [ // Runtime-counted by `js_region_loop_prime`: refused because a key a bare // store may write a non-double into is not an `Any` lane (charter step 5). "rt_rloop_refuse_f64_stored", + "rt_rloop_refuse_rep", + // Emitted only in a route-census build: F ran with at least one R bit + // backed by its chosen static or learned supplier. + "rloop_f_rep", ]; /// The runtime-counted routes: see [`RECV_ROUTE_NAMES`]. @@ -1610,9 +1614,10 @@ pub(crate) const RT_ROUTE_RLOOP_REFUSE_SPILL_UNSERVABLE: u32 = 25; pub(crate) const RT_ROUTE_RLOOP_REFUSE_RANGE: u32 = 26; pub(crate) const RT_ROUTE_RLOOP_RETIRE: u32 = 27; pub(crate) const RT_ROUTE_RLOOP_REFUSE_F64_STORED: u32 = 32; +pub(crate) const RT_ROUTE_RLOOP_REFUSE_REP: u32 = 33; -static RECV_ROUTES: [std::sync::atomic::AtomicU64; 33] = - [const { std::sync::atomic::AtomicU64::new(0) }; 33]; +static RECV_ROUTES: [std::sync::atomic::AtomicU64; 35] = + [const { std::sync::atomic::AtomicU64::new(0) }; 35]; static RECV_ROUTES_REPORT: std::sync::Once = std::sync::Once::new(); /// Set by the first emitted `js_recv_route_note`, i.e. only in a binary /// compiled with `PERRY_RECV_ROUTE_COUNT=1`; the runtime-counted routes are a diff --git a/crates/perry-runtime/src/object/class_birth_rep_tests.rs b/crates/perry-runtime/src/object/class_birth_rep_tests.rs index 09780b1921..ca12d9ed78 100644 --- a/crates/perry-runtime/src/object/class_birth_rep_tests.rs +++ b/crates/perry-runtime/src/object/class_birth_rep_tests.rs @@ -157,6 +157,45 @@ fn a_region_word_refuses_a_boxed_store_into_an_f64_lane() { ); } +/// P7: a learned region may publish a Number-read word only for an exact +/// F64 identity lane. A wrong-rep receiver keeps the site empty for G. +#[test] +fn a_region_prime_refuses_a_requested_number_read_on_an_any_lane() { + use super::shapes::{js_region_loop_prime, REGION_GUARD_WORD_EMPTY}; + use core::sync::atomic::{AtomicU64, Ordering}; + + let k = keys(b"p7a\0p7b\0", 2); + let typed = js_object_shape_id_for_class_keys(k, 2, CID, REP_F64); + let untyped = js_object_shape_id_for_class_keys(k, 2, CID, REP_ANY); + let (a, b) = unsafe { + let (slots, len) = crate::object::keys_array_dense_slots_resolved( + k as usize as *const crate::array::ArrayHeader, + ); + assert!(len >= 2); + ((*slots).to_bits(), (*slots.add(1)).to_bits()) + }; + let site = AtomicU64::new(REGION_GUARD_WORD_EMPTY); + let prime = |id, key, r_mask| unsafe { + js_region_loop_prime(&site, id, 1, key, 0, 0, 0, 0, 0, 0, 0, r_mask) + }; + assert_eq!(prime(untyped, a, 1), REGION_GUARD_WORD_EMPTY); + assert_eq!(site.load(Ordering::Relaxed), REGION_GUARD_WORD_EMPTY); + assert_eq!(prime(typed, b, 1), REGION_GUARD_WORD_EMPTY); + assert_eq!(site.load(Ordering::Relaxed), REGION_GUARD_WORD_EMPTY); + let word = prime(typed, a, 1); + assert_ne!(word, REGION_GUARD_WORD_EMPTY); + assert_eq!(site.load(Ordering::Relaxed), word); + + // A deprecated lane is still safe for existing objects but is no + // longer a publishable identity fact for a new learned region. + assert!(super::shapes::shape_record_by_id(typed) + .expect("typed shape record") + .deprecate_rep_slot(0)); + site.store(REGION_GUARD_WORD_EMPTY, Ordering::Relaxed); + assert_eq!(prime(typed, a, 1), REGION_GUARD_WORD_EMPTY); + assert_eq!(site.load(Ordering::Relaxed), REGION_GUARD_WORD_EMPTY); +} + /// Design step 4 x T1: the rep is part of a static id's content, so a class /// birth with an `F64` lane adopts its static id like an all-`Any` one, and /// the same keys with the other rep are another content under another id. diff --git a/crates/perry-runtime/src/object/shapes.rs b/crates/perry-runtime/src/object/shapes.rs index ec2ae47852..835e8f36c5 100644 --- a/crates/perry-runtime/src/object/shapes.rs +++ b/crates/perry-runtime/src/object/shapes.rs @@ -2285,8 +2285,15 @@ pub extern "C" fn js_region_loop_pack( stored_mask: u32, boxed_mask: u32, ) -> u64 { - region_loop_pack(shape_id, n, [k0, k1, k2, k3, k4], stored_mask, boxed_mask) - .unwrap_or(REGION_GUARD_WORD_EMPTY) + region_loop_pack( + shape_id, + n, + [k0, k1, k2, k3, k4], + stored_mask, + boxed_mask, + 0, + ) + .unwrap_or(REGION_GUARD_WORD_EMPTY) } /// Why [`js_region_loop_pack`] refused a shape — the route census's refusal @@ -2310,6 +2317,8 @@ enum RegionRefusal { Range, /// A key a bare store may write a non-double into is not an `Any` lane. F64Stored, + /// A requested Number read is not on an identity F64 lane. + Rep, } fn region_loop_pack( @@ -2318,6 +2327,7 @@ fn region_loop_pack( keys: [u64; 5], stored_mask: u32, boxed_mask: u32, + r_mask: u32, ) -> Result { use RegionRefusal::*; if !is_site_matchable_shape_id(shape_id) || n == 0 || n > REGION_GUARD_MAX_KEYS { @@ -2364,6 +2374,14 @@ fn region_loop_pack( }; let mut word = u64::from(id); for (i, &(spilled, n_at)) in at.iter().enumerate().take(n as usize) { + if r_mask & (1 << i) != 0 + && (spilled + || n_at >= super::field_rep::REP_SLOTS as usize + || super::field_rep::slot_rep(descriptor.rep, n_at as u32) + != super::field_rep::REP_F64) + { + return Err(Rep); + } if !spilled && boxed_mask & (1 << i) != 0 && (n_at as u32) < super::field_rep::REP_SLOTS @@ -2409,8 +2427,16 @@ pub unsafe extern "C" fn js_region_loop_prime( last: u32, stored_mask: u32, boxed_mask: u32, + r_mask: u32, ) -> u64 { - let verdict = region_loop_pack(shape_id, n, [k0, k1, k2, k3, k4], stored_mask, boxed_mask); + let verdict = region_loop_pack( + shape_id, + n, + [k0, k1, k2, k3, k4], + stored_mask, + boxed_mask, + r_mask, + ); region_loop_prime_census(verdict); let packed = verdict.unwrap_or(REGION_GUARD_WORD_EMPTY); if word.is_null() { @@ -2437,7 +2463,7 @@ fn region_loop_prime_census(verdict: Result) { use crate::hot_diag::{ recv_route_note_runtime, RT_ROUTE_RLOOP_PRIME_OK, RT_ROUTE_RLOOP_REFUSE_ABSENT, RT_ROUTE_RLOOP_REFUSE_BAND, RT_ROUTE_RLOOP_REFUSE_F64_STORED, RT_ROUTE_RLOOP_REFUSE_KIND, - RT_ROUTE_RLOOP_REFUSE_RANGE, RT_ROUTE_RLOOP_REFUSE_SPILL_STORED, + RT_ROUTE_RLOOP_REFUSE_RANGE, RT_ROUTE_RLOOP_REFUSE_REP, RT_ROUTE_RLOOP_REFUSE_SPILL_STORED, RT_ROUTE_RLOOP_REFUSE_SPILL_UNSERVABLE, RT_ROUTE_RLOOP_REFUSE_SUMMARY, }; let route = match verdict { @@ -2450,6 +2476,7 @@ fn region_loop_prime_census(verdict: Result) { Err(RegionRefusal::SpillUnservable) => RT_ROUTE_RLOOP_REFUSE_SPILL_UNSERVABLE, Err(RegionRefusal::Range) => RT_ROUTE_RLOOP_REFUSE_RANGE, Err(RegionRefusal::F64Stored) => RT_ROUTE_RLOOP_REFUSE_F64_STORED, + Err(RegionRefusal::Rep) => RT_ROUTE_RLOOP_REFUSE_REP, }; recv_route_note_runtime(route); } @@ -2470,6 +2497,7 @@ static KEEP_JS_REGION_LOOP_PRIME: unsafe extern "C" fn( u32, u32, u32, + u32, ) -> u64 = js_region_loop_prime; // --------------------------------------------------------------------------- diff --git a/scripts/ci_e2e_scope.py b/scripts/ci_e2e_scope.py index 95671838cd..325ca5fda4 100755 --- a/scripts/ci_e2e_scope.py +++ b/scripts/ci_e2e_scope.py @@ -156,7 +156,6 @@ "temp_root_operand_temporaries", "typed_array_rmw_8692", "typed_array_update_lowering", - "typed_shape_declared_at_allocation", "typed_shape_descriptor", "typed_shape_descriptors", "system_boolean_result", diff --git a/scripts/thread_local_cold_allowlist.json b/scripts/thread_local_cold_allowlist.json index 51951cb404..61ad52aeeb 100644 --- a/scripts/thread_local_cold_allowlist.json +++ b/scripts/thread_local_cold_allowlist.json @@ -1,12 +1,12 @@ { "_comment": "Files still declaring raw `thread_local!`. The count is the number of DECLARATIONS that survive into a shipping build \u2014 each one pays `_tlv_get_addr` per read on Darwin \u2014 and it is a ratchet, so adding a `static` to an already-listed file fails whether or not it opens a new block. New code should use `crate::perry_thread_local!` \u2014 see crates/perry-runtime/src/tls_hot.rs. Regenerate with scripts/check_thread_locals.py --update.", - "_hot_declarations": 528, + "_hot_declarations": 522, "files": { "crates/perry-runtime/src/agent.rs": 1, "crates/perry-runtime/src/arena/block.rs": 5, "crates/perry-runtime/src/arena/page_meta/mod.rs": 2, "crates/perry-runtime/src/async_context.rs": 3, - "crates/perry-runtime/src/async_hooks.rs": 7, + "crates/perry-runtime/src/async_hooks.rs": 6, "crates/perry-runtime/src/builtins/console.rs": 4, "crates/perry-runtime/src/builtins/formatting.rs": 9, "crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs": 1, @@ -31,8 +31,7 @@ "crates/perry-runtime/src/gc/barrier_arming.rs": 3, "crates/perry-runtime/src/gc/cycle_malloc_trim.rs": 1, "crates/perry-runtime/src/gc/fromspace_scan.rs": 1, - "crates/perry-runtime/src/gc/layout.rs": 1, - "crates/perry-runtime/src/gc/layout_tables.rs": 3, + "crates/perry-runtime/src/gc/layout_tables.rs": 2, "crates/perry-runtime/src/gc/malloc.rs": 2, "crates/perry-runtime/src/gc/mod.rs": 3, "crates/perry-runtime/src/gc/promote_in_place.rs": 11, @@ -53,7 +52,7 @@ "crates/perry-runtime/src/node_http2_constants.rs": 1, "crates/perry-runtime/src/node_inspector.rs": 2, "crates/perry-runtime/src/node_repl.rs": 1, - "crates/perry-runtime/src/node_stream_constructors.rs": 3, + "crates/perry-runtime/src/node_stream_constructors.rs": 2, "crates/perry-runtime/src/node_stream_tests.rs": 24, "crates/perry-runtime/src/node_submodules/blob.rs": 4, "crates/perry-runtime/src/node_submodules/diagnostics.rs": 10,