diff --git a/changelog.d/11795-matrix-any-str-regressions.md b/changelog.d/11795-matrix-any-str-regressions.md new file mode 100644 index 0000000000..a12451201b --- /dev/null +++ b/changelog.d/11795-matrix-any-str-regressions.md @@ -0,0 +1,7 @@ +Fixed the loop-region slowdowns #11680 introduced for fields that are not +Number fields. A Number read of an `any` field (or of a field born a string) +no longer refuses the whole region: the guard tests the slot's value on the +object instead (`h += o.a` on an `any` class field: 71 -> 11 instructions per +iteration). A string read through `.length` no longer asks for a Number lane +(92 -> 64), and a method call on a receiver whose class is proven keeps its +guard-free direct call inside a region (90 -> 61). diff --git a/crates/perry-codegen/src/expr/member_update.rs b/crates/perry-codegen/src/expr/member_update.rs index 99bd62138d..2d829c2ed3 100644 --- a/crates/perry-codegen/src/expr/member_update.rs +++ b/crates/perry-codegen/src/expr/member_update.rs @@ -207,7 +207,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // (Phase 5a's proven `this` never claims numeric fields, so this // site remains Phase-3b-local-only in practice.) { - let fact = ctx.ptr_shape_receiver_fact(object.as_ref()).cloned(); + let fact = ctx.ptr_shape_store_fact(object.as_ref()).cloned(); { if let Some(fact) = fact { if fact.numeric_fields.contains(property.as_str()) { diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 98e087d53b..17af279d8e 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -2763,7 +2763,14 @@ impl<'a> FnCtx<'a> { &self, e: &perry_hir::Expr, ) -> Option<&crate::collectors::PtrShapeLocal> { - if !self.repsel_context_allows_ptr_shape { + // An admitted region's authority covers its receivers' STORES only + // (`ptr_shape_store_fact`). A read or a call's dispatch keeps the + // route it has outside the region: it writes nothing the region's + // facts rest on, and the region's own bare stores keep every lane's + // representation, so the exact-class proof stays true inside it. + if !self.repsel_context_allows_ptr_shape + && self.repsel_ptr_shape_context_denial != Some(PTR_SHAPE_REGION_AUTHORITY) + { // #7106 follow-up: this early return is the whole of mechanism 2. // The fact EXISTS — `collect_shape_proven_ptr_locals` already ran // and already recorded a `select()` for it — and every access site @@ -2782,6 +2789,26 @@ impl<'a> FnCtx<'a> { } } + /// The `Ptr` proof a STORE to `e` may act on. + /// + /// An admitted loop/body region owns its receivers' stores + /// ([`PTR_SHAPE_REGION_AUTHORITY`]): its live ShapeId guard and store + /// admission replace the unguarded store route, so inside the region a + /// store never takes it. Reads and a call's dispatch are not under that + /// authority ([`FnCtx::ptr_shape_receiver_fact`]). + pub(crate) fn ptr_shape_store_fact( + &self, + e: &perry_hir::Expr, + ) -> Option<&crate::collectors::PtrShapeLocal> { + if !self.repsel_context_allows_ptr_shape { + if crate::opt_report::enabled() { + self.report_ptr_shape_context_drop(e); + } + return None; + } + self.ptr_shape_receiver_fact(e) + } + /// Shared exact-shape lookup for a local, with clone-parameter overlays /// taking precedence over ordinary native facts. fn ptr_shape_local_fact(&self, id: u32) -> Option<&crate::collectors::PtrShapeLocal> { diff --git a/crates/perry-codegen/src/expr/property_set.rs b/crates/perry-codegen/src/expr/property_set.rs index aa8f7a455f..c3c0d66113 100644 --- a/crates/perry-codegen/src/expr/property_set.rs +++ b/crates/perry-codegen/src/expr/property_set.rs @@ -921,7 +921,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, assignment_strict: bool) - field_index, ); let route_proven = ctx - .ptr_shape_receiver_fact(object.as_ref()) + .ptr_shape_store_fact(object.as_ref()) .is_some_and(|fact| fact.class_name == class_name); if !route_proven && crate::expr::class_field_inline_guard::class_instances_grow_past_layout( @@ -1004,7 +1004,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, assignment_strict: bool) - // sealed target would otherwise silently accept a raw // store where the spec requires a strict TypeError. let ptr_shape_proven = ctx - .ptr_shape_receiver_fact(object.as_ref()) + .ptr_shape_store_fact(object.as_ref()) .map(|fact| fact.class_name == class_name) .unwrap_or(false); // A contained offset proof may carry completed diff --git a/crates/perry-codegen/src/expr/region_loop_tests.rs b/crates/perry-codegen/src/expr/region_loop_tests.rs index 8048ecb59e..05c2e9b22f 100644 --- a/crates/perry-codegen/src/expr/region_loop_tests.rs +++ b/crates/perry-codegen/src/expr/region_loop_tests.rs @@ -462,9 +462,10 @@ fn a_region_r_proven_increment_store_clears_only_its_number_boxed_bit() { ); } -/// 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. +/// A fresh bare read used by a Number-consuming add requests R. The prime +/// serves an Any lane only with `REGION_LOOP_WORD_VALUE_TEST`, which the +/// guard honours with a value test, 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( @@ -487,6 +488,80 @@ fn a_number_consuming_bare_read_sets_the_prime_rep_mask() { ); } +/// The R-bearing guard tests the R slot's value whenever the learned word +/// carries the value-test bit (bit 63: a signed compare against 0), and the +/// test is the strict Number test below the tag band. +#[test] +fn a_number_read_guard_value_tests_a_word_that_asks() { + let ir = loop_ir( + "region_loop_value_test", + vec![Stmt::Expr(Expr::LocalSet( + H, + Box::new(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::LocalGet(H)), + right: Box::new(get("x")), + }), + ))], + ); + let bl = blocks(&ir); + let value: Vec<&Vec> = bl + .iter() + .filter(|(l, _)| { + l.strip_prefix("rloop.guard.value.") + .is_some_and(|t| t.chars().all(|c| c.is_ascii_digit())) + }) + .map(|(_, (insts, _))| insts) + .collect(); + assert!(!value.is_empty(), "no value-test block in\n{ir}"); + assert!( + value.iter().all(|insts| { + insts.iter().any(|i| i.contains("load double")) + && insts + .iter() + .any(|i| i.contains("and i64") && i.contains("9223372036854775807")) + && insts + .iter() + .any(|i| i.contains("icmp ult i64") && i.contains("9221401712017801216")) + }), + "every value test must load the slot and test it below the tag band:\n{value:#?}" + ); + assert!( + bl.iter() + .filter(|(l, _)| l.starts_with("rloop.guard.value.need")) + .all(|(_, (insts, _))| insts.iter().any(|i| i.contains("icmp slt i64"))), + "the value test must be selected by the word's sign bit" + ); +} + +/// A read beneath another property access is that access's receiver +/// (`o.x.length`), never a Number operand: the region asks for no R. +#[test] +fn a_receiver_read_beneath_a_property_access_requests_no_number_lane() { + let ir = loop_ir( + "region_loop_receiver_not_operand", + vec![Stmt::Expr(Expr::LocalSet( + H, + Box::new(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::LocalGet(H)), + right: Box::new(Expr::PropertyGet { + object: Box::new(get("x")), + property: "length".to_string(), + byte_offset: 0, + }), + }), + ))], + ); + let masks = prime_rep_masks(&ir); + assert!(!masks.is_empty(), "the region must form:\n{ir}"); + assert!( + masks.iter().all(|&m| m == 0), + "`o.x` is `.length`'s receiver, not a Number operand: {masks:?}" + ); + assert!(!ir.contains("rloop.guard.value"), "no R, so no value test"); +} + /// 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 diff --git a/crates/perry-codegen/src/stmt/ptr_shape_region_report_tests.rs b/crates/perry-codegen/src/stmt/ptr_shape_region_report_tests.rs index 72764c92ee..3960cbf6f8 100644 --- a/crates/perry-codegen/src/stmt/ptr_shape_region_report_tests.rs +++ b/crates/perry-codegen/src/stmt/ptr_shape_region_report_tests.rs @@ -20,6 +20,16 @@ fn fixture() -> Module { panic!("fixture loop") }; body.insert(0, receiver); + // A compound update alongside the put-value store: the admitted region + // owns its receivers' STORES, so the generic copy's update of this + // receiver is where the region authority refuses the unguarded route. + body.push(Stmt::Expr(Expr::PropertyUpdate { + object: Box::new(Expr::LocalGet(1)), + property: "value".into(), + op: BinaryOp::Add, + prefix: false, + strict: false, + })); m } diff --git a/crates/perry-codegen/src/stmt/region_loop/guard.rs b/crates/perry-codegen/src/stmt/region_loop/guard.rs index 7121af92c5..555aae6bf4 100644 --- a/crates/perry-codegen/src/stmt/region_loop/guard.rs +++ b/crates/perry-codegen/src/stmt/region_loop/guard.rs @@ -170,12 +170,36 @@ pub(super) fn emit_body_guard_direct( None => ctx.block().cond_br(cond, target, other), } }; - to(ctx, &eq, inline_l, &flip_l, &admit); + // A word carrying VALUE_TEST_BIT enters F only once the object's R + // slots hold Numbers; the matched ShapeId makes the word's slots its own. + let (inline_to, spill_to) = if may_value_test(rv) { + let mut targets = Vec::with_capacity(2); + for target in [inline_l, spill_l] { + let vt = ctx.new_block("rloop.guard.value"); + let test = ctx.new_block("rloop.guard.value.test"); + let vt_l = ctx.block_label(vt); + let test_l = ctx.block_label(test); + let saved = ctx.current_block; + ctx.current_block = vt; + let need = ctx.block().icmp_slt(I64, word, "0"); + ctx.block().cond_br(&need, &test_l, target); + ctx.current_block = test; + let ok = value_tests_on(ctx, rv, word, &handle); + ctx.block().cond_br(&ok, target, fail_l); + ctx.current_block = saved; + targets.push(vt_l); + } + let spill_to = targets.pop().expect("two targets"); + (targets.pop().expect("two targets"), spill_to) + } else { + (inline_l.to_string(), spill_l.to_string()) + }; + to(ctx, &eq, &inline_to, &flip_l, &admit); ctx.current_block = flip; let exp_f = ctx.block().xor(I32, &expected, FLIP_I32); let eq_f = ctx.block().icmp_eq(I32, &sid, &exp_f); - to(ctx, &eq_f, spill_l, &miss_l, &admit); + to(ctx, &eq_f, &spill_to, &miss_l, &admit); ctx.current_block = miss; let tries = ctx.block().load(I32, &sites.tries_g); @@ -198,7 +222,7 @@ pub(super) fn emit_body_guard_direct( /// Class provenance chooses the supplier when available; otherwise a class /// hint suffices. Neither licenses a slot access: the guard compares the live /// ShapeId against the supplier's id, and a different shape selects G. -fn static_region_word(ctx: &FnCtx<'_>, rv: &Receiver) -> Option<(u64, u32, bool)> { +fn static_region_word(ctx: &FnCtx<'_>, rv: &Receiver) -> Option<(u64, u32, u32, bool)> { // Use containment's exact class to select the supplier when available. // This consumes only class provenance: the compared ShapeId below remains // the sole authority for slot locations and Number representation. @@ -220,13 +244,127 @@ fn static_region_word(ctx: &FnCtx<'_>, rv: &Receiver) -> Option<(u64, u32, bool) }) })?; let keys_global = ctx.class_keys_globals.get(&class_name)?; - let (id, slots, r_mask) = + let (id, slots, f64_lanes) = crate::codegen::static_region_slots(keys_global, &rv.keys, rv.boxed_mask)?; + // The runtime's rule (`region_loop_pack`), decided here for the static + // birth shape: a requested Number read on an identity F64 lane is free, + // and one on an `Any` lane is served by a value test on the object + // unless a bare store may write that key a non-Number, or the plan does + // not allow value tests (`vt_mask`: a guard re-run every iteration). + let tested = rv.r_mask & rv.vt_mask & !f64_lanes & !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, r_mask, proven_class.is_some())) + if tested != 0 { + word |= VALUE_TEST_BIT; + } + Some((word, f64_lanes | tested, tested, proven_class.is_some())) +} + +/// Bit 63 of a region word, `REGION_LOOP_WORD_VALUE_TEST`: a Number read +/// (R) of this word sits on a lane that does not guarantee a Number for every +/// carrier, so the guard tests the R slots' values on the object. +pub(super) const VALUE_TEST_BIT: u64 = 1 << 63; + +/// The R slots' value test: each slot `rv.vt_mask` names (decoded from +/// `word`) of the object `handle` holds a raw canonical Number now — the +/// strict test the number entry tests use (no tag, no INT32 box, no mirror +/// of the tag band). Only valid where `word` matched the object's ShapeId. +fn value_tests_on(ctx: &mut FnCtx<'_>, rv: &Receiver, word: &str, handle: &str) -> String { + let mut ok = "true".to_string(); + for i in 0..rv.keys.len() { + if (rv.r_mask & rv.vt_mask) & (1 << i) == 0 { + continue; + } + let shift = (32 + SLOT_BITS * i as u32).to_string(); + let s = ctx.block().lshr(I64, word, &shift); + let slot = ctx.block().and(I64, &s, "63"); + let p = super::bare::slot_ptr(ctx, handle, &slot); + let v = ctx.block().load(DOUBLE, &p); + let number = crate::stmt::loops::emit_js_value_is_number(ctx, &v); + ok = ctx.block().and(I1, &ok, &number); + } + ok +} + +/// Does `rv` have an R slot its word may ask the guard to value-test? +pub(super) fn may_value_test(rv: &Receiver) -> bool { + rv.r_mask & rv.vt_mask != 0 +} + +/// A loop region's re-check compare for one receiver: the object's ShapeId +/// `sid` against the expected id `exp`, plus the R slots' value test when the +/// word asks for one (JS may have written those slots since the guard). +/// +/// The re-check sits on the hot path of every iteration that may run JS, so +/// a LEARNED word without [`VALUE_TEST_BIT`] must cost exactly the plain +/// compare. Its bit is known only at run time, so the compare is against a +/// loop-invariant id that a word WITH the bit replaces by `EMPTY` (which no +/// object carries): such a word fails the re-check and the loop continues in +/// G (today's code), never in an F whose R facts were not re-proven. A static +/// word knows its bit when compiled: one with the bit re-tests the values, +/// one without pays nothing. +pub(super) fn emit_recheck_eq( + ctx: &mut FnCtx<'_>, + rv: &Receiver, + sid: &str, + exp: &str, +) -> Result { + if !may_value_test(rv) { + return Ok(ctx.block().icmp_eq(I32, sid, exp)); + } + if rv.expected_shape.is_some() { + let eq = ctx.block().icmp_eq(I32, sid, exp); + let word = rv.word.clone(); + return emit_value_tests(ctx, rv, &word, &eq); + } + let need = ctx.block().icmp_slt(I64, &rv.word, "0"); + let exp_fast = ctx.block().select(I1, &need, I32, "-1", exp); + Ok(ctx.block().icmp_eq(I32, sid, &exp_fast)) +} + +/// `pass` and, when it holds and `word` carries [`VALUE_TEST_BIT`], the R +/// slots' value test on the receiver (re-derived from its binding: `pass` +/// proves it is an object whose ShapeId `word` names). A word without the bit +/// costs one sign test; a receiver with no testable R costs nothing. +pub(super) fn emit_value_tests( + ctx: &mut FnCtx<'_>, + rv: &Receiver, + word: &str, + pass: &str, +) -> Result { + if !may_value_test(rv) { + return Ok(pass.to_string()); + } + // Branch on `pass` first, then on the word's sign bit: a failing compare + // keeps its one branch, and a word without the bit pays one sign test. + let chk = ctx.new_block("rloop.guard.value.need"); + let vt = ctx.new_block("rloop.guard.value"); + let join = ctx.new_block("rloop.guard.value.join"); + let chk_l = ctx.block_label(chk); + let vt_l = ctx.block_label(vt); + let join_l = ctx.block_label(join); + let from = ctx.block().label.clone(); + ctx.block().cond_br(pass, &chk_l, &join_l); + ctx.current_block = chk; + let need = ctx.block().icmp_slt(I64, word, "0"); + ctx.block().cond_br(&need, &vt_l, &join_l); + ctx.current_block = vt; + let recv_box = lower_recv(ctx, rv.recv)?; + let handle = handle_of(ctx, &recv_box); + let ok = value_tests_on(ctx, rv, word, &handle); + let vt_end = ctx.block().label.clone(); + ctx.block().br(&join_l); + ctx.current_block = join; + Ok(ctx.block().phi( + I1, + &[ + ("false", from.as_str()), + ("true", chk_l.as_str()), + (&ok, vt_end.as_str()), + ], + )) } /// A guard whose receiver the compiler names (DESIGN §4.1, static-exclusive): @@ -297,7 +435,7 @@ fn emit_static_guard( let mut shape_edges: Vec<(&str, &str)> = miss_edges.iter().map(|l| ("0", l.as_str())).collect(); shape_edges.push((sid.as_str(), hit_end.as_str())); rv.expected_shape = Some(ctx.block().phi(I32, &shape_edges)); - Ok((word.to_string(), pass, "false".to_string())) + Ok(((word as i64).to_string(), pass, "false".to_string())) } /// Whether `rv`'s guard takes the static supplier (DESIGN §4.1). Every guard @@ -313,11 +451,15 @@ 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, r_mask, uses_ptr_shape_class)) = static_region_word(ctx, rv) { + if let Some((w, r_mask, tested, uses_ptr_shape_class)) = static_region_word(ctx, rv) { rv.r_mask = r_mask; + rv.vt_mask = tested; rv.uses_ptr_shape_class = uses_ptr_shape_class; return emit_static_guard(ctx, rv, w); } + // A learned word may carry VALUE_TEST_BIT for any R key: its guard must + // honour the bit whatever the plan allowed a static word. + rv.vt_mask = rv.r_mask; // A retired region (every bounded prime refused) is decided by the word // alone: one load and one compare, before the receiver is even tested. let (sites, word) = emit_guard_word(ctx, rv); diff --git a/crates/perry-codegen/src/stmt/region_loop/mod.rs b/crates/perry-codegen/src/stmt/region_loop/mod.rs index 9e2ec9700a..90bbb2dc42 100644 --- a/crates/perry-codegen/src/stmt/region_loop/mod.rs +++ b/crates/perry-codegen/src/stmt/region_loop/mod.rs @@ -75,8 +75,8 @@ pub(crate) use self::arrays::{ use self::bare::note; pub(crate) use self::bare::{try_lower_bare_get, try_lower_bare_put, try_lower_fact_add_tree}; use self::guard::{ - decode_slots, emit_body_guard_direct, emit_guard, emit_guard_word, field_i32, handle_of, - has_static_supplier, lower_recv, store_admission, + decode_slots, emit_body_guard_direct, emit_guard, emit_guard_word, emit_recheck_eq, + emit_value_tests, field_i32, handle_of, has_static_supplier, lower_recv, store_admission, }; pub(crate) use self::numeric_expression::try_lower_numeric_compare; use self::plan::{ @@ -189,8 +189,14 @@ 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. + /// Bit `i`: a read assumes the key holds a raw canonical Number (R). r_mask: u32, + /// Bit `i`: an R key the guard value-tests on the object when the word + /// carries `VALUE_TEST_BIT` (a lane that is not an identity F64 lane). + /// Before the guard: the R keys the plan lets a STATIC word value-test. + /// After it: a static word's exact `Any`-lane R keys, or every R key of a + /// learned word (which may ask for any of them). + vt_mask: u32, /// `i1`: the guard matched this receiver's SPILL word (flipped id). spill: String, sites: Option<(String, String)>, @@ -205,8 +211,9 @@ pub(crate) struct Receiver { /// 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. +/// the slot holds a canonical raw JS Number — by its identity F64 lane, or by +/// the guard's value test when the word carries `VALUE_TEST_BIT`; 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, .. @@ -472,6 +479,11 @@ fn begin_with( stored_mask: effective_stored_mask(*sm, k.len()), boxed_mask: *bm, r_mask: *rm, + // A static word's value-tested R is re-tested at every + // re-check; a loop that re-checks every iteration would pay + // that test every iteration for what R saves, so it takes no + // value-tested R (a learned word is decided by its bit). + vt_mask: if p.recheck == Recheck::Always { 0 } else { *rm }, spill: "false".to_string(), sites: None, word: String::new(), @@ -483,6 +495,7 @@ fn begin_with( for rv in receivers.iter_mut() { let (word, pass, spill) = emit_guard(ctx, rv)?; rv.spill = spill; + let pass = emit_value_tests(ctx, rv, &word, &pass)?; all = ctx.block().and(I1, &all, &pass); decode_slots(ctx, rv, &word); } @@ -708,6 +721,7 @@ fn body_pending(p: Plan, body: &[Stmt], split_at: usize) -> Pending { stored_mask: effective_stored_mask(*sm, k.len()), boxed_mask: *bm, r_mask: *rm, + vt_mask: *rm, spill: "false".to_string(), sites: None, word: String::new(), @@ -996,7 +1010,9 @@ pub(crate) fn lower_split( } else { exp }; - let eq = ctx.block().icmp_eq(I32, &sid, &exp); + // A re-check follows JS that may have written an R slot + // of a value-tested lane: the values are tested again. + let eq = emit_recheck_eq(ctx, rv, &sid, &exp)?; let eq = if rv.has_store { let adm = store_admission(ctx, &h, false); ctx.block().and(I1, &eq, &adm) @@ -1039,6 +1055,7 @@ pub(crate) fn lower_split( for rv in receivers.iter_mut() { let (word, pass, spill) = emit_guard(ctx, rv)?; rv.spill = spill; + let pass = emit_value_tests(ctx, rv, &word, &pass)?; all = ctx.block().and(I1, &all, &pass); decode_slots(ctx, rv, &word); } diff --git a/crates/perry-codegen/src/stmt/region_loop/numeric_expression.rs b/crates/perry-codegen/src/stmt/region_loop/numeric_expression.rs index e719624e43..dafa643a7b 100644 --- a/crates/perry-codegen/src/stmt/region_loop/numeric_expression.rs +++ b/crates/perry-codegen/src/stmt/region_loop/numeric_expression.rs @@ -134,11 +134,13 @@ fn emit( uses_ptr_shape_class: false, keys: vec![key.to_string()], has_store: false, - // This expression never stores. R already requires an inline F64 - // identity lane, so it also refuses spill words without a store bit. + // This expression never stores. R requires an inline lane (an + // identity F64 one, or a value-tested one), so it also refuses spill + // words without a store bit. stored_mask: 0, boxed_mask: 0, r_mask: 1, + vt_mask: 1, spill: "false".to_string(), sites: None, word: String::new(), diff --git a/crates/perry-codegen/src/stmt/region_loop/plan.rs b/crates/perry-codegen/src/stmt/region_loop/plan.rs index 17bb2850a1..72950d4a7d 100644 --- a/crates/perry-codegen/src/stmt/region_loop/plan.rs +++ b/crates/perry-codegen/src/stmt/region_loop/plan.rs @@ -90,14 +90,26 @@ pub(super) fn fact_tree_leaves<'e>( /// Candidate reads inside a Number-consuming expression. The planner's /// exact bare-access set filters these by freshness after the walk. +/// +/// Only a value the operator itself consumes is a candidate. A read beneath +/// another property access is that access's RECEIVER (`o.a.length` reads +/// `o.a` as an object), and a read beneath an element access or a call is +/// that operation's key, receiver or argument: none of them is a Number +/// operand, so none may ask the region for a Number lane (R). 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())); + match e { + Expr::PropertyGet { + object, property, .. + } => { + if let Some(r) = Recv::of(object) { + out.push((e as *const Expr as usize, r, property.clone())); + } + return; } + Expr::IndexGet { .. } | Expr::Call { .. } | Expr::CallSpread { .. } | Expr::New { .. } => { + return + } + _ => {} } perry_hir::walker::walk_expr_children(e, &mut |child| number_operand_reads(child, out)); } @@ -857,13 +869,15 @@ pub(super) fn receiver_eligible(ctx: &FnCtx<'_>, r: Recv) -> bool { // A derived constructor's `this` may still be in its TDZ (before // `super()`), and reading it throws: the preheader must not hoist // that read, so such a body's `this` is not a region receiver. - // A `this` whose shape is already PROVEN (a proven-shape method clone) - // reads its slots with no guard at all; a region would only add one. + // + // A receiver whose shape is already PROVEN (Ptr, or a + // proven-shape method clone's `this`) stays eligible: the region adds + // one guard but brings the R facts and bare stores a proof alone does + // not (`h += o.a` on such a local: 24 -> 11 instructions), and its + // reads and calls keep their proven routes inside the region + // (`FnCtx::ptr_shape_receiver_fact`). Recv::This => { - !ctx.this_stack.is_empty() - && !ctx.in_static_member - && ctx.super_called_stack.is_empty() - && ctx.ptr_shape_receiver_fact(&Expr::This).is_none() + !ctx.this_stack.is_empty() && !ctx.in_static_member && ctx.super_called_stack.is_empty() } Recv::Local(id) => { !ctx.boxed_vars.contains(&id) @@ -878,7 +892,6 @@ pub(super) fn receiver_eligible(ctx: &FnCtx<'_>, r: Recv) -> bool { && !ctx.local_slot_reps.contains_key(&id) && !ctx.integer_locals.contains(&id) && !ctx.receiver_descriptors.contains_buffer_view(id) - && ctx.ptr_shape_receiver_fact(&Expr::LocalGet(id)).is_none() && (ctx.locals.contains_key(&id) || ctx.closure_captures.contains_key(&id) || ctx.module_globals.contains_key(&id)) 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 4d24999eb8..76d80d3b24 100644 --- a/crates/perry-runtime/src/object/class_birth_rep_tests.rs +++ b/crates/perry-runtime/src/object/class_birth_rep_tests.rs @@ -157,11 +157,17 @@ 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. +/// P7: a learned region publishes a Number-read (R) word for an exact F64 +/// identity lane as it is. On any other non-SPECIAL inline lane (an `Any` +/// lane, or a deprecated F64 one) it publishes the word with +/// `REGION_LOOP_WORD_VALUE_TEST`, so the emitted guard tests the slot's value +/// on the object before F runs. An R key a bare store may write a non-Number +/// into is refused: no guard-time test could cover that store. #[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}; +fn a_region_prime_value_tests_a_requested_number_read_on_a_non_identity_lane() { + use super::shapes::{ + js_region_loop_prime, REGION_GUARD_WORD_EMPTY, REGION_LOOP_WORD_VALUE_TEST, + }; use core::sync::atomic::{AtomicU64, Ordering}; let k = keys(b"p7a\0p7b\0", 2); @@ -175,25 +181,40 @@ fn a_region_prime_refuses_a_requested_number_read_on_an_any_lane() { ((*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) + let prime = |id, key, stored, boxed, r_mask| unsafe { + js_region_loop_prime(&site, id, 1, key, 0, 0, 0, 0, 0, stored, boxed, 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); + let tested = |word: u64| word & REGION_LOOP_WORD_VALUE_TEST != 0; + + // An Any lane: published, and the guard must test the value. + let word = prime(untyped, a, 0, 0, 1); assert_ne!(word, REGION_GUARD_WORD_EMPTY); assert_eq!(site.load(Ordering::Relaxed), word); + assert_eq!(word as u32, untyped); + assert!(tested(word), "R on an Any lane must ask for a value test"); + // The same key read without R asks for nothing. + assert!(!tested(prime(untyped, a, 0, 0, 0))); + // The typed shape's second key is an Any lane; its first is identity F64. + assert!(tested(prime(typed, b, 0, 0, 1))); + let word = prime(typed, a, 0, 0, 1); + assert_ne!(word, REGION_GUARD_WORD_EMPTY); + assert!(!tested(word), "an identity F64 lane needs no value test"); - // A deprecated lane is still safe for existing objects but is no - // longer a publishable identity fact for a new learned region. + // A bare store that may write a non-Number into an R key: refused. The + // same store without R is admitted, so the refusal is the R + boxed pair. + assert_ne!(prime(untyped, a, 1, 1, 0), REGION_GUARD_WORD_EMPTY); + site.store(REGION_GUARD_WORD_EMPTY, Ordering::Relaxed); + assert_eq!(prime(untyped, a, 1, 1, 1), REGION_GUARD_WORD_EMPTY); + assert_eq!(site.load(Ordering::Relaxed), REGION_GUARD_WORD_EMPTY); + + // A deprecated lane is no longer an identity fact for a new learned + // region: its R is served by the value test. 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); + let word = prime(typed, a, 0, 0, 1); + assert_ne!(word, REGION_GUARD_WORD_EMPTY); + assert!(tested(word), "a deprecated lane must ask for a value test"); } /// Design step 4 x T1: the rep is part of a static id's content, so a class diff --git a/crates/perry-runtime/src/object/region_numeric_read_tests.rs b/crates/perry-runtime/src/object/region_numeric_read_tests.rs index 02a23c1ced..391a83be0b 100644 --- a/crates/perry-runtime/src/object/region_numeric_read_tests.rs +++ b/crates/perry-runtime/src/object/region_numeric_read_tests.rs @@ -127,13 +127,16 @@ fn unmarked_numeric_read_retains_generation_holes_rep_and_absence_checks() { for (id, expected) in [ (mint(1, 0, d.rep), RegionRefusal::Kind), (mint(0, 1, d.rep), RegionRefusal::Kind), - (mint(0, 0, REP_ANY), RegionRefusal::Rep), ] { assert_eq!( region_loop_pack(id, 1, [bits(key), 0, 0, 0, 0], 0, 0, 1), Err(expected) ); } + // An Any lane serves the read only through the guard's value test. + let word = region_loop_pack(mint(0, 0, REP_ANY), 1, [bits(key), 0, 0, 0, 0], 0, 0, 1) + .expect("an Any lane is served with a value test"); + assert_ne!(word & REGION_LOOP_WORD_VALUE_TEST, 0); assert_eq!( region_loop_pack( object_shape_stamp(obj), @@ -149,7 +152,7 @@ fn unmarked_numeric_read_retains_generation_holes_rep_and_absence_checks() { } #[test] -fn unmarked_numeric_read_refuses_accessors_and_deprecated_lanes() { +fn unmarked_numeric_read_refuses_accessors_and_value_tests_deprecated_lanes() { let _lock = crate::gc::global_side_table_test_lock(); let _gc = crate::gc::GcSuppressScope::new(); unsafe { @@ -172,9 +175,12 @@ fn unmarked_numeric_read_refuses_accessors_and_deprecated_lanes() { shape_record_by_id(object_shape_stamp(other)) .unwrap() .deprecate_rep_slot(1); - assert_eq!( - prime(&site, other, bits(other_key)), - REGION_GUARD_WORD_EMPTY + let word = prime(&site, other, bits(other_key)); + assert_ne!(word, REGION_GUARD_WORD_EMPTY); + assert_ne!( + word & REGION_LOOP_WORD_VALUE_TEST, + 0, + "a deprecated lane is served only through the guard's value test" ); } } @@ -196,7 +202,11 @@ fn unmarked_numeric_read_word_misses_after_mutation_or_exotic_reclassification() 0, ); assert_ne!(object_shape_stamp(obj), before as u32); - assert_eq!(prime(&site, obj, bits(key)), REGION_GUARD_WORD_EMPTY); + // The generalized lane is served only through the guard's value test, + // which `true` fails. + let after = prime(&site, obj, bits(key)); + assert_ne!(after as u32, before as u32); + assert_ne!(after & REGION_LOOP_WORD_VALUE_TEST, 0); let (proto, proto_key) = record("rnr_proto"); let before = prime(&site, proto, bits(proto_key)); diff --git a/crates/perry-runtime/src/object/shapes.rs b/crates/perry-runtime/src/object/shapes.rs index fcc198d834..ddc31fe238 100644 --- a/crates/perry-runtime/src/object/shapes.rs +++ b/crates/perry-runtime/src/object/shapes.rs @@ -2492,7 +2492,9 @@ static KEEP_JS_REGION_GUARD_PRIME: unsafe extern "C" fn( /// A read-only numeric region may also use `OrdinaryUnmarked`: the missing /// birth mark withdraws store permission, not the own-data slot layout. /// Receivers with virtual read semantics remain refused by their prototype -/// classification, and every covered key must be requested as inline F64. +/// classification, and every covered key must be requested as an inline +/// Number read. A Number read (R) on a lane that is not an identity F64 lane +/// sets [`REGION_LOOP_WORD_VALUE_TEST`]: the guard then tests the value. #[no_mangle] #[allow(clippy::too_many_arguments)] pub extern "C" fn js_region_loop_pack( @@ -2538,7 +2540,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, or a bare + /// A requested Number read is spill-located, on a SPECIAL lane, or on a + /// non-identity lane a bare store may write a non-Number into; or a bare /// store targets a SPECIAL lane that requires the checked slot funnel. Rep, } @@ -2600,6 +2603,7 @@ fn region_loop_pack( shape_id }; let mut word = u64::from(id); + let mut value_test = false; for (i, &(spilled, n_at)) in at.iter().enumerate().take(n as usize) { // A canonical Number cannot preserve a ConstFn body identity. Every // SPECIAL write must use the checked slot funnel before storing; @@ -2612,13 +2616,25 @@ fn region_loop_pack( { return Err(Rep); } - 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 r_mask & (1 << i) != 0 { + // R wants the slot's raw bits to be a canonical Number. An inline + // identity F64 lane guarantees it for every carrier. Any other + // inline lane that is not SPECIAL (Any, or a deprecated F64) can + // hold it per OBJECT: the word then carries + // `REGION_LOOP_WORD_VALUE_TEST` and the emitted guard tests each + // R slot's value on the object before F runs (and again on every + // re-check). Inside F nothing writes such a slot except a bare + // store, so a key a bare store may write a non-Number into + // (`boxed_mask`) cannot be R. + if spilled || n_at >= super::field_rep::REP_SLOTS as usize { + return Err(Rep); + } + match super::field_rep::slot_rep(descriptor.rep, n_at as u32) { + super::field_rep::REP_F64 => {} + super::field_rep::REP_SPECIAL => return Err(Rep), + _ if boxed_mask & (1 << i) != 0 => return Err(Rep), + _ => value_test = true, + } } if !spilled && boxed_mask & (1 << i) != 0 @@ -2635,6 +2651,9 @@ fn region_loop_pack( }; word |= (field as u64) << (32 + REGION_GUARD_SLOT_BITS * i as u32); } + if value_test { + word |= REGION_LOOP_WORD_VALUE_TEST; + } Ok(word) } @@ -2644,6 +2663,15 @@ fn region_loop_pack( /// FIRST and skips the receiver test (DESIGN §4.3). pub const REGION_LOOP_WORD_RETIRED: u64 = u64::MAX; +/// Bit 63 of a published loop-region word: some key the region reads as a +/// Number (R) sits on a lane that does not guarantee one for every carrier +/// (an `Any` or deprecated lane), so the emitted guard must test each R +/// slot's value on the object itself before F runs. Field indices occupy +/// bits 32..62 at most (five 6-bit fields), so the bit is free; the word's +/// id half is a ShapeId, so a word carrying it is never +/// [`REGION_LOOP_WORD_RETIRED`]. +pub const REGION_LOOP_WORD_VALUE_TEST: u64 = 1 << 63; + /// Compute a loop region's word ([`js_region_loop_pack`]) and publish it; the /// store-side twin of [`js_region_guard_prime`], with its memory ordering. /// `last` is non-zero on the site's final bounded attempt: a refusal then diff --git a/crates/perry/tests/region_any_str_reads.rs b/crates/perry/tests/region_any_str_reads.rs new file mode 100644 index 0000000000..9d9409e809 --- /dev/null +++ b/crates/perry/tests/region_any_str_reads.rs @@ -0,0 +1,251 @@ +//! Loop regions over fields that are not Number fields (#11680's matrix +//! regressions): an `any` field read as a Number, a string field read through +//! `.length`, and a method call on a receiver whose exact class is proven. +//! +//! Each program is compiled with `--trace llvm`; the test checks the emitted +//! IR of `run` and the program's output (the expected strings are node's). +use std::path::PathBuf; +use std::process::Command; +use std::sync::atomic::{AtomicUsize, Ordering}; + +struct TestDir(PathBuf); +impl Drop for TestDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +/// `(IR of every `run` function, stdout)`. +fn compile(source: &str) -> (String, String) { + static NEXT: AtomicUsize = AtomicUsize::new(0); + let dir = TestDir(std::env::temp_dir().join(format!( + "perry-region-any-str-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + ))); + std::fs::create_dir_all(&dir.0).unwrap(); + let entry = dir.0.join("main.ts"); + let binary = dir.0.join("main_bin"); + std::fs::write(&entry, source).unwrap(); + let build = Command::new(env!("CARGO_BIN_EXE_perry")) + .current_dir(&dir.0) + .args([ + "compile", + "--no-auto-optimize", + "--no-cache", + "--trace", + "llvm", + ]) + .arg(&entry) + .arg("-o") + .arg(&binary) + .output() + .unwrap(); + assert!( + build.status.success(), + "compile failed: {}", + String::from_utf8_lossy(&build.stderr) + ); + let ir = std::fs::read_to_string(dir.0.join(".perry-trace/llvm/main_ts.ll")).unwrap(); + let run = Command::new(&binary).output().unwrap(); + assert!( + run.status.success(), + "execution failed: {}", + String::from_utf8_lossy(&run.stderr) + ); + let mut bodies = String::new(); + let mut inside = false; + for line in ir.lines() { + if line.starts_with("define ") { + inside = line.contains("_ts__run"); + } + if inside { + bodies.push_str(line); + bodies.push('\n'); + if line == "}" { + inside = false; + } + } + } + assert!(!bodies.is_empty(), "no `run` function in the IR"); + (bodies, String::from_utf8(run.stdout).unwrap()) +} + +/// The region R mask each learned prime call requests (its last argument). +fn prime_r_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() +} + +/// The bodies of the guard's value-test blocks (`rloop.guard.value.N:`). +fn value_test_blocks(ir: &str) -> Vec { + let mut out = Vec::new(); + let mut cur: Option = None; + for line in ir.lines() { + if !line.starts_with(' ') && line.ends_with(':') { + if let Some(block) = cur.take() { + out.push(block); + } + let label = line.trim_end_matches(':'); + let tail = label.strip_prefix("rloop.guard.value."); + if tail.is_some_and(|t| t.chars().all(|c| c.is_ascii_digit())) { + cur = Some(String::new()); + } + continue; + } + if let Some(block) = cur.as_mut() { + block.push_str(line); + block.push('\n'); + } + } + out.extend(cur); + out +} + +const ANY_CLASS: &str = r#" +class C { + a: any; + d: any; + constructor(a: any, d: any) { this.a = a; this.d = d; } +} +function run(n: number, o: any): any { + let h = 0.0; + for (let k = 0; k < n; k++) { + o.d = k; + h += o.a; + } + return h; +} +"#; + +/// An `any` field read as a Number keeps its region: the learned word asks +/// for R, and the guard serves it with a value test of the slot, so a Number +/// runs F and a string runs G, both with node's answer. +#[test] +fn an_any_field_number_read_is_served_by_a_guard_value_test() { + let (ir, stdout) = compile(&format!( + "{ANY_CLASS} +const o = new C(2, 0); +console.log(run(10, o)); +o.a = \"s\"; +console.log(run(3, o)); +o.a = 1.5; +console.log(run(4, o), o.d); +" + )); + assert_eq!(stdout, "20\n0sss\n6 3\n"); + let masks = prime_r_masks(&ir); + assert!( + !masks.is_empty() && masks.iter().all(|&m| m != 0), + "the region must request R for `o.a`: {masks:?}" + ); + let tests: Vec = value_test_blocks(&ir); + assert!( + tests + .iter() + .any(|block| block.contains("load double") && block.contains("9221401712017801216")), + "the guard must load each R slot and compare it below the tag band:\n{tests:#?}\n{ir}" + ); +} + +/// A string field read through `.length` is a receiver, never a Number +/// operand: the region asks for no Number lane and emits no value test. +#[test] +fn a_string_field_length_read_requests_no_number_lane() { + let (ir, stdout) = compile( + r#" +function run(n: number, o: any): number { + let h = 0; + for (let k = 0; k < n; k++) { + o.d = k; + h += o.a.length; + } + return h; +} +console.log(run(5, { a: "abc", d: "dd" })); +"#, + ); + assert_eq!(stdout, "15\n"); + let masks = prime_r_masks(&ir); + assert!( + !masks.is_empty(), + "the region must form (it serves `o.d` and `o.a`):\n{ir}" + ); + assert!( + masks.iter().all(|&m| m == 0), + "`o.a` is `.length`'s receiver, not a Number operand: {masks:?}" + ); + assert!(!ir.contains("rloop.guard.value"), "no R, so no value test"); +} + +/// A Number field on an identity F64 lane pays no value test: the static +/// word of a literal names its lanes, and the guard emits none. +#[test] +fn a_number_field_read_pays_no_value_test() { + let (ir, stdout) = compile( + r#" +const SINK: any[] = []; +function run(n: number): number { + const o: any = { a: 1, d: 16 }; + let h = 0.0; + for (let k = 0; k < n; k++) { + o.d = k; + h += o.a; + } + SINK.push(o); + return h + o.d; +} +console.log(run(10)); +"#, + ); + assert_eq!(stdout, "19\n"); + assert!(ir.contains("rloop.fast"), "the region must form:\n{ir}"); + assert!( + !ir.contains("rloop.guard.value"), + "an F64 lane needs no value test:\n{ir}" + ); +} + +/// A method call on a receiver whose exact class is proven (Ptr) +/// stays the guard-free direct call inside an admitted region: the region +/// owns its receivers' slot accesses, not a call's dispatch. +#[test] +fn a_proven_receiver_method_call_stays_guard_free_inside_a_region() { + let (ir, stdout) = compile( + r#" +class C { + a: number; + d: number; + constructor(a: number, d: number) { this.a = a; this.d = d; } + m() { return this.a; } +} +function run(n: number): any { + const o: any = new C(3, 0); + let h = 0.0; + for (let k = 0; k < n; k++) { + o.d = k; + h += o.m(); + } + if (h < 0) return o; + return h + o.d; +} +console.log(run(10)); +"#, + ); + assert_eq!(stdout, "39\n"); + assert!(ir.contains("rloop.fast"), "the region must form:\n{ir}"); + assert!( + !ir.contains("@js_typed_feedback_method_direct_call_guard("), + "the proven receiver's call must not take the guarded dispatch:\n{ir}" + ); +} diff --git a/test-files/test_gap_region_any_lane_number_read.ts b/test-files/test_gap_region_any_lane_number_read.ts new file mode 100644 index 0000000000..dde467d509 --- /dev/null +++ b/test-files/test_gap_region_any_lane_number_read.ts @@ -0,0 +1,127 @@ +// A loop region that reads a field as a Number (`h += o.a`) on a lane that +// does not guarantee a Number for every object of its shape (an `any` class +// field, a literal field born a string): the region must still run, and every +// value that is NOT a Number must take the generic path, whether it is there +// at loop entry or written by JS the loop calls. +class C { + a: any; + b: any; + c: any; + e: any; + d: any; + constructor(a: any, b: any, c: any, e: any, d: any) { + this.a = a; + this.b = b; + this.c = c; + this.e = e; + this.d = d; + } +} + +function sum1(n: number, o: any): any { + let h = 0.0; + for (let k = 0; k < n; k++) { + o.d = k; + h += o.a; + } + return h; +} + +function sum4(n: number, o: any): any { + let h = 0.0; + for (let k = 0; k < n; k++) { + o.d = k; + const r0 = o.a; + const r1 = o.b; + const r2 = o.c; + const r3 = o.e; + h += r0 + r1 + r2 + r3; + } + return h; +} + +const o = new C(1, 2, 4, 8, 16); +console.log("number", sum1(10, o), sum4(10, o), o.d); +for (const v of ["s", 2147483647, 2147483648, -0, NaN, 1.5, true, null, undefined, "7"]) { + o.a = v; + console.log("entry", String(v), sum1(5, o), sum4(3, o)); +} +o.a = 3; +o.b = { valueOf() { return 100; } }; +console.log("valueOf", sum4(4, o)); +o.b = 2; + +// JS the loop calls writes a non-Number into a field the region reads. +let pokes = 0; +function poke(x: any, k: number): void { + pokes++; + if (k === 3) x.a = "x"; + if (k === 6) x.a = 5; +} +function sumCall(n: number, x: any): any { + let h = 0.0; + for (let k = 0; k < n; k++) { + h += x.a; + poke(x, k); + } + return h; +} +const p = new C(1, 2, 4, 8, 16); +console.log("call", sumCall(10, p), pokes, p.a); + +// A literal whose field is born a string, later holding a Number. +const lit: any = { a: "a", b: "bb", c: "cccc", e: "eeee", d: "dddd" }; +console.log("lit-string", sum1(4, lit), lit.a.length); +lit.a = 6; +console.log("lit-number", sum1(4, lit)); +lit.a = 6 | 0; +console.log("lit-int", sum1(4, lit)); + +// A string field read through `.length` is a receiver, not a Number operand. +function lens(n: number, x: any): number { + let h = 0; + for (let k = 0; k < n; k++) { + x.d = k; + h += x.a.length; + } + return h; +} +console.log("length", lens(5, { a: "abc", b: "bb", c: "c", e: "e", d: "d" })); +console.log("length-class", lens(5, new C("abcd", 1, 2, 3, 4))); + +// The receiver is young and the loop allocates, so a collection can move it +// between the guard and the reads. +function churn(n: number): any { + const q = new C(1, 2, 4, 8, 16); + const junk: any[] = []; + let h = 0.0; + for (let k = 0; k < n; k++) { + q.d = k; + h += q.a + q.b; + junk.push({ k, s: "v" + k }); + if (junk.length > 4096) junk.length = 0; + } + q.a = "end"; + return h + q.a + junk.length; +} +console.log("churn", churn(200000)); + +// A module-level receiver whose class names its birth shape statically. +const m: any = new C(1, 2, 4, 8, 16); +function sumStatic(n: number): any { + let h = 0.0; + for (let k = 0; k < n; k++) { + m.d = k; + const r0 = m.a; + const r1 = m.b; + const r2 = m.c; + const r3 = m.e; + h += r0 + r1 + r2 + r3; + } + return h; +} +console.log("static", sumStatic(10)); +m.c = "c"; +console.log("static-string", sumStatic(3)); +m.c = 4; +console.log("static-back", sumStatic(10)); diff --git a/test-files/test_gap_region_method_call_receiver.ts b/test-files/test_gap_region_method_call_receiver.ts new file mode 100644 index 0000000000..8ffbf72afb --- /dev/null +++ b/test-files/test_gap_region_method_call_receiver.ts @@ -0,0 +1,68 @@ +// A loop region over a receiver whose exact class is proven, with a method +// call on that receiver in the body. The call's dispatch keeps the proven +// direct route, and whatever the method writes into the fields the region +// reads (a Number, a string, an object with valueOf) is seen by the next +// iteration exactly as node sees it. +const SINK: any[] = []; + +class C { + a: number; + d: number; + constructor(a: number, d: number) { + this.a = a; + this.d = d; + } + m(): number { + return this.a; + } + poke(k: number): void { + if (k === 4) (this as any).a = "s"; + if (k === 7) (this as any).a = { valueOf() { return 100; } }; + if (k === 9) this.a = 2; + } +} + +function viaMethod(n: number): any { + const o: any = new C(3, 0); + let h = 0.0; + for (let k = 0; k < n; k++) { + o.d = k; + h += o.m(); + } + if (h < 0) return o; + return h + o.d; +} + +function pokeThenRead(n: number): any { + const o: any = new C(1, 0); + let h: any = 0.0; + for (let k = 0; k < n; k++) { + o.d = k; + h += o.a; + o.poke(k); + } + SINK.push(o); + return h; +} + +class D extends C { + m(): number { + return this.a * 10; + } +} + +function subclass(n: number): any { + const o: any = new D(2, 0); + let h = 0.0; + for (let k = 0; k < n; k++) { + o.d = k; + h += o.m(); + } + SINK.push(o); + return h; +} + +console.log("method", viaMethod(10), viaMethod(1000)); +console.log("poke", String(pokeThenRead(12))); +console.log("subclass", subclass(5)); +console.log("sink", SINK.length);