From 12ff3a3307b77ba59fb2d1ffec3224f5d1afa528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 12 Nov 2026 06:33:53 +0100 Subject: [PATCH 1/6] compile_env: sort COMPILE_ENV_VARS (documented sorted and unique) --- .../perry/src/commands/compile/compile_env.rs | 94 +++++++++---------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/crates/perry/src/commands/compile/compile_env.rs b/crates/perry/src/commands/compile/compile_env.rs index 1d1a59be77..b8a6232bd0 100644 --- a/crates/perry/src/commands/compile/compile_env.rs +++ b/crates/perry/src/commands/compile/compile_env.rs @@ -25,8 +25,10 @@ pub(super) const COMPILE_ENV_VARS: &[&str] = &[ "PERRY_ALLOW_PARTIAL_CODEGEN", "PERRY_ALLOW_PERRY_FEATURES", "PERRY_ALLOW_UNIMPLEMENTED", + "PERRY_ARRAY_FILL_BIRTH", "PERRY_ASSERT_CAPTURE_CELLS", "PERRY_B4_SABOTAGE", + "PERRY_BIRTH_IMAGE", "PERRY_BOX_CAPTURE_ENTRY_CELLS", "PERRY_BYTE_VIEW_LOOP", "PERRY_CALLEE_BINDING_RESOLUTION", @@ -56,74 +58,60 @@ pub(super) const COMPILE_ENV_VARS: &[&str] = &[ "PERRY_EVAL_CSP", "PERRY_EXPORT_DEMAND", "PERRY_EXTRA_LINK_ARGS", - "PERRY_FACTS_BIRTH_CENSUS", - "PERRY_FACTS_BOUNDED_LOOPS", - "PERRY_FACTS_BUDGET_SECS", - "PERRY_FACTS_CALL_KINDS", - "PERRY_FACTS_ELIDE", - "PERRY_FACTS_ELIDE_TA", - "PERRY_FACTS_MAX_PTS", - "PERRY_FACTS_MAX_WORK", - "PERRY_FACTS_NUMBER_LOCALS", - "PERRY_FACTS_INT32_LOCALS", - "PERRY_FACTS_INT_RANGES", - "PERRY_FACTS_INT_DEMAND", - "PERRY_FACTS_HELD_LENGTHS", - "PERRY_FACTS_NAMED_LENGTHS", - "PERRY_FACTS_KEY_SIGN", - "PERRY_FACTS_LITERAL_STRINGS", - "PERRY_FACTS_SLICE_BIRTHS", - "PERRY_FACTS_INBOUNDS_STORE", - "PERRY_FACTS_WIDE_KEYS", - "PERRY_FACTS_MAP_VALUES", - "PERRY_NUMBER_STORE_ARM", - "PERRY_STORE_ARM_DIAG", - "PERRY_FACTS_COMPARE_DEMAND", - "PERRY_FACTS_INT32_READS", - "PERRY_FACTS_LOCAL_RANGES", - "PERRY_FACTS_DIRECT_NUMBER_CALLS", - "PERRY_FACTS_AT_REST", - "PERRY_REP_DEFAULT", - "PERRY_REST_DEFAULT", - "PERRY_REST_ELEMS", - "PERRY_REST_FIELDS", - "PERRY_REST_TIERS", - "PERRY_REST_CAPACITY", - "PERRY_REST_PUSH_FILL", - "PERRY_PUSH_METHOD_TIERS", - "PERRY_ARRAY_FILL_BIRTH", - "PERRY_BIRTH_IMAGE", - "PERRY_SIG_SIGNED_INDEX", - "PERRY_FACTS_ZERO_SIGN", - "PERRY_FACTS_FILTER_DEMAND", - "PERRY_WP_REP_TEMPS", "PERRY_FACTS_ARRAY_BASE", - "PERRY_FACTS_FN_BASES", - "PERRY_FACTS_WALK_INDEXED", "PERRY_FACTS_ARRAY_FIELDS", "PERRY_FACTS_ARRAY_FLOOR", + "PERRY_FACTS_ARRAY_LENGTHS", "PERRY_FACTS_ARRAY_REPS", "PERRY_FACTS_ARRAY_STORE", "PERRY_FACTS_ARRAY_TIER", - "PERRY_FACTS_ARRAY_LENGTHS", + "PERRY_FACTS_AT_REST", + "PERRY_FACTS_BIRTH_CENSUS", + "PERRY_FACTS_BOUNDED_LOOPS", + "PERRY_FACTS_BUDGET_SECS", + "PERRY_FACTS_CALL_KINDS", + "PERRY_FACTS_CANONICAL_NUMBERS", + "PERRY_FACTS_COMPARE_DEMAND", "PERRY_FACTS_CONST_ARRAYS", "PERRY_FACTS_CONST_LENGTH", "PERRY_FACTS_CONST_NUMBERS", + "PERRY_FACTS_DIRECT_NUMBER_CALLS", "PERRY_FACTS_ELEM_UNDEF", + "PERRY_FACTS_ELIDE", + "PERRY_FACTS_ELIDE_TA", "PERRY_FACTS_EXACT_RULES", "PERRY_FACTS_FIELD_READ_CALLS", - "PERRY_FACTS_CANONICAL_NUMBERS", + "PERRY_FACTS_FILTER_DEMAND", + "PERRY_FACTS_FN_BASES", "PERRY_FACTS_GUARD_PRESENCE", "PERRY_FACTS_GUARD_RANGES", + "PERRY_FACTS_HELD_LENGTHS", + "PERRY_FACTS_INBOUNDS_STORE", + "PERRY_FACTS_INT32_LOCALS", + "PERRY_FACTS_INT32_READS", + "PERRY_FACTS_INT_DEMAND", + "PERRY_FACTS_INT_RANGES", + "PERRY_FACTS_KEY_SIGN", "PERRY_FACTS_LEAVING_WRITES", + "PERRY_FACTS_LITERAL_STRINGS", + "PERRY_FACTS_LOCAL_RANGES", + "PERRY_FACTS_MAP_VALUES", + "PERRY_FACTS_MAX_PTS", + "PERRY_FACTS_MAX_WORK", + "PERRY_FACTS_NAMED_LENGTHS", + "PERRY_FACTS_NUMBER_LOCALS", "PERRY_FACTS_RECORD_SIGNATURES", - "PERRY_FACTS_SITE_ARGS", "PERRY_FACTS_SABOTAGE", + "PERRY_FACTS_SITE_ARGS", + "PERRY_FACTS_SLICE_BIRTHS", "PERRY_FACTS_SUPPRESS_BIRTH", "PERRY_FACTS_TYPED_SIGNATURES", "PERRY_FACTS_UNITS", "PERRY_FACTS_VALUE_RECORDS", + "PERRY_FACTS_WALK_INDEXED", "PERRY_FACTS_WIDEN", + "PERRY_FACTS_WIDE_KEYS", + "PERRY_FACTS_ZERO_SIGN", "PERRY_FAMILY_SLOT_SABOTAGE", "PERRY_FAST_MATH", "PERRY_FIELD_REPR_VERIFY", @@ -213,13 +201,14 @@ pub(super) const COMPILE_ENV_VARS: &[&str] = &[ "PERRY_NO_CLASS_ENV", "PERRY_NO_REEXPORT_PRUNE", "PERRY_NUMBER_BY_CONSTRUCTION", + "PERRY_NUMBER_INDEX_SPLIT", "PERRY_NUMBER_LOCAL_LOOP", + "PERRY_NUMBER_STORE_ARM", "PERRY_NUMERIC_ADD_BITS", "PERRY_OBJECT_LITERAL_SHAPE_METHODS", "PERRY_OUTLINE_ENTRY", "PERRY_OUTLINE_ENTRY_CHUNK_STMTS", "PERRY_OUTLINE_METHOD_DISPATCH", - "PERRY_NUMBER_INDEX_SPLIT", "PERRY_OWNED_PUSH_ARRAYS", "PERRY_PACKED_LOOP_ABRUPT", "PERRY_PACKED_LOOP_NUMERIC_ACCUMULATOR", @@ -234,6 +223,7 @@ pub(super) const COMPILE_ENV_VARS: &[&str] = &[ "PERRY_PTR_SHAPE_THIS", "PERRY_PUSHED_ARRAY_RESERVE", "PERRY_PUSH_GROW_INLINE", + "PERRY_PUSH_METHOD_TIERS", "PERRY_REALM_REACH_NAMES", "PERRY_REALM_TEST_FIXTURE", "PERRY_RECEIVER_COLD_ARM_REFRESH", @@ -246,9 +236,17 @@ pub(super) const COMPILE_ENV_VARS: &[&str] = &[ "PERRY_REGION_NODES_PER_BARE", "PERRY_REGION_READS", "PERRY_REGION_SPILL", + "PERRY_REP_DEFAULT", "PERRY_REQUIRE_FAITHFUL_BINDINGS", + "PERRY_REST_CAPACITY", + "PERRY_REST_DEFAULT", + "PERRY_REST_ELEMS", + "PERRY_REST_FIELDS", + "PERRY_REST_PUSH_FILL", + "PERRY_REST_TIERS", "PERRY_RUNTIME_DIR", "PERRY_SEGVIEW", + "PERRY_SIG_SIGNED_INDEX", "PERRY_SIZE_LTO", "PERRY_SIZE_OPT", "PERRY_SIZE_PANIC", @@ -271,6 +269,7 @@ pub(super) const COMPILE_ENV_VARS: &[&str] = &[ "PERRY_STACK_SYMBOLS", "PERRY_STATIC_STORE_BARRIERS", "PERRY_STATIC_STRING_LOWERING", + "PERRY_STORE_ARM_DIAG", "PERRY_STORE_CENSUS", "PERRY_STRING_INIT_CHUNK_SIZE", "PERRY_TARGET_CPU", @@ -297,6 +296,7 @@ pub(super) const COMPILE_ENV_VARS: &[&str] = &[ "PERRY_WINDOWS_SYSROOT", "PERRY_WORKSPACE_ROOT", "PERRY_WP_REP_P1", + "PERRY_WP_REP_TEMPS", "PERRY_WRITE_BARRIERS", "PERRY_XCRUN", "PKG_CONFIG_LIBDIR", From 2d0e5e24e4682e59e50baf1a65e1e31444dd6851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 12 Nov 2026 06:38:17 +0100 Subject: [PATCH 2/6] check_gc_header_constants: register the method and field guard masks as 39359ae502 split them --- scripts/check_gc_header_constants.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/scripts/check_gc_header_constants.py b/scripts/check_gc_header_constants.py index c265bc2aea..db444d587a 100755 --- a/scripts/check_gc_header_constants.py +++ b/scripts/check_gc_header_constants.py @@ -157,12 +157,23 @@ class into `@perry_class_header_image_*`; and ("crates/perry-codegen/src/expr/array_literal.rs", "GC_LAYOUT_POINTER_FREE", "GC_LAYOUT_POINTER_FREE", "array literal: _reserved half of the baked header word"), - # --- the typed-f64 receiver method probe's fused mask ------------------- + # --- the direct-method and field-layout guards' fused masks ------------- + # 39359ae502 (2026-11-04) split the one mask in two on purpose. A + # METHOD-identity guard reads no attribute bit: an own key named like the + # method is in the key list, so the class/ShapeId word (or the family + # word) already misses. A FIELD-layout guard (raw slot loads and stores) + # keeps `OBJ_FLAG_HAS_DESCRIPTORS`. The codegen unit test + # `gc_header_mask_preserves_the_direct_method_contract` pins the same two + # values against each other; this anchors them to the runtime's bits. ("crates/perry-codegen/src/lower_call/method_override.rs", "GC_OBJECT_METHOD_GUARD_MASK_I32", + "0xFF | (GC_FLAG_FORWARDED << 8)", + "method-identity guard: obj_type byte and not-forwarded, no attribute bit"), + ("crates/perry-codegen/src/lower_call/method_override.rs", + "GC_OBJECT_FIELD_GUARD_MASK_I32", "0xFF | (GC_FLAG_FORWARDED << 8)" " | ((OBJ_FLAG_HAS_DESCRIPTORS) << 16)", - "method probe: one fused 32-bit mask over all three header bytes"), + "field-layout guard: one fused 32-bit mask over all three header bytes"), # --- other single-bit restatements -------------------------------------- ("crates/perry-codegen/src/expr/arrays_finds.rs", "GC_TYPE_MAP", From 6457efa71c4b901eff5ec2da01214e418bbc2ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 12 Nov 2026 06:53:47 +0100 Subject: [PATCH 3/6] compile_env: register the five compile-affecting reads the audit reports, exclude the six diagnostic ones with reasons --- .../perry/src/commands/compile/compile_env.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/crates/perry/src/commands/compile/compile_env.rs b/crates/perry/src/commands/compile/compile_env.rs index f557403f66..f7ed28f26a 100644 --- a/crates/perry/src/commands/compile/compile_env.rs +++ b/crates/perry/src/commands/compile/compile_env.rs @@ -75,6 +75,7 @@ pub(super) const COMPILE_ENV_VARS: &[&str] = &[ "PERRY_FACTS_CONST_ARRAYS", "PERRY_FACTS_CONST_LENGTH", "PERRY_FACTS_CONST_NUMBERS", + "PERRY_FACTS_DCE", "PERRY_FACTS_DIRECT_NUMBER_CALLS", "PERRY_FACTS_ELEM_UNDEF", "PERRY_FACTS_ELIDE", @@ -140,6 +141,7 @@ pub(super) const COMPILE_ENV_VARS: &[&str] = &[ "PERRY_GLOBAL_SCRIPT_THIS", "PERRY_GUARDED_ARITH", "PERRY_GUARDED_PREINLINE_MAX_IR_BYTES", + "PERRY_INDEX_RAW_I32", "PERRY_INERT_MATH", "PERRY_INLINE_CTOR", "PERRY_INLINE_HOT_SMALL", @@ -252,6 +254,7 @@ pub(super) const COMPILE_ENV_VARS: &[&str] = &[ "PERRY_SIZE_OPT", "PERRY_SIZE_PANIC", "PERRY_SMALL_STRINGS", + "PERRY_SNAP_CENSUS", "PERRY_SPECIALIZED_ABI", "PERRY_SPECIALIZED_ABI_MAX", "PERRY_SPEC_INLINE", @@ -296,7 +299,9 @@ pub(super) const COMPILE_ENV_VARS: &[&str] = &[ "PERRY_WATCHOS_MIN", "PERRY_WINDOWS_SYSROOT", "PERRY_WORKSPACE_ROOT", + "PERRY_WP_ELEMENT_READS", "PERRY_WP_REP_P1", + "PERRY_WP_REP_P2", "PERRY_WP_REP_TEMPS", "PERRY_WRITE_BARRIERS", "PERRY_XCRUN", @@ -839,6 +844,30 @@ const COMPILE_ENV_EXCLUSIONS: &[(&str, &str)] = &[ "PERRY_WASM_DEBUG_IMPORTS", "Writes IR/report files or diagnostic output without changing emitted objects.", ), + ( + "PERRY_ARRAY_BASE_DIAG", + "Prints the array-base, held-length and named-length verdicts to stderr; no emitted instructions depend on the report.", + ), + ( + "PERRY_FACTS_ZERO_SIGN_WHY", + "Prints why one node's zero sign is unknown; the trace reads the solved facts and changes none.", + ), + ( + "PERRY_REST_DIAG", + "Prints the at-rest element lowering form per site to stderr; no emitted instructions depend on the report.", + ), + ( + "PERRY_SNAP_FACTS", + "Writes the snap census facts dump file only; no fact or emitted code depends on it.", + ), + ( + "PERRY_TEST_IR_OUT", + "Read only by perry-codegen unit tests to keep the IR they compile; the compiler never reads it.", + ), + ( + "PERRY_VALUE_RECORDS_TRACE", + "Prints the value-record ledger and layouts to stderr; no emitted instructions depend on the report.", + ), ]; #[cfg(test)] From 444cb17e6a86c1deb587b6139a8b13733e9d9ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 12 Nov 2026 06:53:47 +0100 Subject: [PATCH 4/6] tests: held and named lengths assert the Arr precondition and the refusal reason, with positive twins --- .../perry-wpfacts/src/tests/held_lengths.rs | 233 +++++++++++++----- .../perry-wpfacts/src/tests/named_lengths.rs | 137 ++++++++-- 2 files changed, 291 insertions(+), 79 deletions(-) diff --git a/crates/perry-wpfacts/src/tests/held_lengths.rs b/crates/perry-wpfacts/src/tests/held_lengths.rs index 2d8eea0372..4db4361587 100644 --- a/crates/perry-wpfacts/src/tests/held_lengths.rs +++ b/crates/perry-wpfacts/src/tests/held_lengths.rs @@ -1,12 +1,31 @@ //! Held lengths (`held_lengths`): a local whose Arrays keep their length //! while its function runs. +//! +//! The fact is asked only of `Rep::Arr` locals, so a local that is no `Arr` +//! in this solve has no stable length whatever the rules say. Every case +//! therefore asserts the representation first (`Case::arr`), and a negative +//! case asserts the rule that refused it (the `# held_len … refused=` line +//! of the dump) beside a positive twin that differs only in the refused +//! construct. use super::*; +use crate::bindings::Rep; use crate::FactsConfig; -/// `(stable_length, stable_front)` of the parameter or top-level `let` -/// named `local` of function or method `func`. -fn fact(src: &str, func: &str, local: &str, cfg: FactsConfig) -> (bool, bool) { +/// What the solve says of one local. +struct Case { + /// The local is a plain program Array (`Rep::Arr`): the fact's + /// precondition. + arr: bool, + stable_length: bool, + stable_front: bool, + /// The local's `# held_len` dump line (none: the rule never judged it). + line: Option, +} + +/// The parameter or top-level `let` named `local` of function or method +/// `func`. +fn case_with(src: &str, func: &str, local: &str, cfg: FactsConfig) -> Case { let module = perry_parser::parse_typescript(src, "t.ts").expect("parses"); let hir = perry_hir::lower_module(&module, "t", "t.ts").expect("lowers"); let f = hir @@ -31,7 +50,13 @@ fn fact(src: &str, func: &str, local: &str, cfg: FactsConfig) -> (bool, bool) { }) }) .unwrap_or_else(|| panic!("local {local} of {func}")); - let pf = facts_with(src, cfg); + let pf = facts_with( + src, + FactsConfig { + diagnostics: true, + ..cfg + }, + ); let mb = pf.bindings_for("t.ts").expect("module bindings"); let b = &mb .locals @@ -39,38 +64,123 @@ fn fact(src: &str, func: &str, local: &str, cfg: FactsConfig) -> (bool, bool) { .find(|(l, _)| *l == id) .unwrap_or_else(|| panic!("local {id} of {func}")) .1; - (b.stable_length, b.stable_front) + let head = format!("# held_len module=0 local={id} "); + Case { + arr: matches!(b.rep, Rep::Arr(_)), + stable_length: b.stable_length, + stable_front: b.stable_front, + line: pf + .dump_text() + .lines() + .find(|l| l.starts_with(&head)) + .map(str::to_string), + } +} + +fn case(func: &str, local: &str) -> Case { + case_with(SRC, func, local, FactsConfig::default()) +} + +/// `func`'s `local` is an `Arr` whose length is held. +#[track_caller] +fn assert_held(func: &str, local: &str, front: bool) { + let c = case(func, local); + assert!(c.arr, "{func}.{local} must be an Arr in this solve"); + assert!( + c.stable_length, + "{func}.{local} has no stable length: {:?}", + c.line + ); + assert_eq!(c.stable_front, front, "{func}.{local} front"); } -fn held(src: &str, func: &str, local: &str) -> bool { - fact(src, func, local, FactsConfig::default()).0 +/// `func`'s `local` is an `Arr`, and rule `rule` (the head of one of the +/// dump line's reasons) refuses its stable length. +#[track_caller] +fn assert_refused(func: &str, local: &str, rule: &str) { + let c = case(func, local); + assert!( + c.arr, + "{func}.{local} must be an Arr in this solve, or the case asserts nothing about the rule" + ); + assert!(!c.stable_length, "{func}.{local} has a stable length"); + assert!(!c.stable_front, "{func}.{local} has a stable front"); + let line = c + .line + .unwrap_or_else(|| panic!("no held_len line for {func}.{local}")); + let why = line + .split_once(" refused=") + .unwrap_or_else(|| panic!("{func}.{local} is not refused: {line}")) + .1; + assert!( + why.split('+').any(|r| r.starts_with(rule)), + "{func}.{local} is refused by another rule than {rule}: {why}" + ); } +// Each case bears its Arrays at its own site (`genW`, `genH`, …): a token is +// its birth site, so a field or a closure that held another case's Array +// would end the fact for that case too. A negative case and its positive +// twin (`…Twin`) differ only in the refused construct. const SRC: &str = r#" -function gen(n: number): number[] { +let k = 0; +function genW(n: number): number[] { + const out: number[] = []; + for (let i = 0; i < n; i++) out.push(i * 2); + return out; +} +function genR(n: number): number[] { + const out: number[] = []; + for (let i = 0; i < n; i++) out.push(i * 2); + return out; +} +function genC(n: number): number[] { + const out: number[] = []; + for (let i = 0; i < n; i++) out.push(i * 2); + return out; +} +function genH(n: number): number[] { + const out: number[] = []; + for (let i = 0; i < n; i++) out.push(i * 2); + return out; +} +function genHT(n: number): number[] { + const out: number[] = []; + for (let i = 0; i < n; i++) out.push(i * 2); + return out; +} +function genL(n: number): number[] { const out: number[] = []; for (let i = 0; i < n; i++) out.push(i * 2); return out; } -// The negative cases bear their Arrays here: a token is its birth site, so -// a field or a closure that held `gen`'s Array would end the fact for the -// positive cases too. -function mk(n: number): number[] { +function genLT(n: number): number[] { + const out: number[] = []; + for (let i = 0; i < n; i++) out.push(i * 2); + return out; +} +function genF(n: number): number[] { + const out: number[] = []; + for (let i = 0; i < n; i++) out.push(i * 2); + return out; +} +function genFT(n: number): number[] { const out: number[] = []; for (let i = 0; i < n; i++) out.push(i * 2); return out; } function touch(v: number): number { return v + 1; } function grow(a: number[]): void { a.push(1); } +function size(a: number[]): number { return a.length; } function walk(n: number): number { - const xs = gen(n); + const xs = genW(n); let s = 0; for (let i = 0; i < xs.length; i++) s += touch(xs[i] ?? 0); return s; } function rec(d: number): number { - const xs = gen(3); + const xs = genR(3); let s = 0; for (let i = 0; i < xs.length; i++) { s += xs[i] ?? 0; @@ -79,14 +189,14 @@ function rec(d: number): number { return s; } function copied(n: number): number { - const xs = gen(n); + const xs = genC(n); const ys = xs.slice(); let s = 0; for (let i = 0; i < ys.length; i++) s += touch(ys[i] ?? 0); return s; } function handed(): number { - const xs = mk(3); + const xs = genH(3); let s = 0; for (let i = 0; i < xs.length; i++) { s += xs[i] ?? 0; @@ -94,90 +204,101 @@ function handed(): number { } return s; } +function handedTwin(): number { + const xs = genHT(3); + let s = 0; + for (let i = 0; i < xs.length; i++) { + s += xs[i] ?? 0; + if (xs.length < 6) touch(xs.length); + } + return s; +} function closed(): number { - const xs = mk(3); + const xs = genL(3); const more = () => { if (xs.length < 6) xs.push(7); }; let s = 0; for (let i = 0; i < xs.length; i++) { s += xs[i] ?? 0; more(); } return s; } +function closedTwin(): number { + const xs = genLT(3); + const more = () => { if (k < 6) k++; }; + let s = 0; + for (let i = 0; i < xs.length; i++) { s += xs[i] ?? 0; more(); } + return s; +} class Holder { - items: number[] = mk(3); + items: number[] = genF(3); more(): void { if (this.items.length < 6) this.items.push(9); } - walk(): number { + walkField(): number { const xs = this.items; let s = 0; for (let i = 0; i < xs.length; i++) { s += xs[i] ?? 0; this.more(); } return s; } } -console.log(walk(4), rec(2), copied(3), handed(), closed(), new Holder().walk()); +class HolderTwin { + count: number = 0; + more(): void { if (this.count < 6) this.count++; } + walkFrame(): number { + const xs = genFT(3); + let s = 0; + for (let i = 0; i < xs.length; i++) { s += xs[i] ?? 0; this.more(); } + return s; + } +} +console.log(walk(4), rec(2), copied(3), handed(), handedTwin(), closed(), closedTwin(), + new Holder().walkField(), new HolderTwin().walkFrame(), size(genW(1))); "#; #[test] fn a_fresh_result_walked_across_calls_has_a_stable_length() { - // `xs` holds what `gen` returned: no field holds it, `walk` hands it to + // `xs` holds what `genW` returned: no field holds it, `walk` hands it to // no call and has no length event on it. Its birth is a literal, so // its length is defined. - assert_eq!( - fact(SRC, "walk", "xs", FactsConfig::default()), - (true, true) - ); + assert_held("walk", "xs", true); } #[test] fn the_birth_function_running_again_below_the_loop_is_no_event() { - // `rec` calls itself, and `gen` pushes onto another Array of the same + // `rec` calls itself, and `genR` pushes onto another Array of the same // birth site: tokens are per site, the fact is per activation. - assert!(held(SRC, "rec", "xs")); + assert_held("rec", "xs", true); } #[test] fn a_slice_has_a_stable_length_and_no_defined_one() { // A `slice` birth records no length (`solve::arr_len`). - assert_eq!( - fact(SRC, "copied", "ys", FactsConfig::default()), - (true, false) - ); + assert_held("copied", "ys", false); } #[test] fn the_function_that_pushes_has_no_stable_length() { - // `gen` pushes onto `out`; `grow` pushes onto its parameter. - assert!(!held(SRC, "gen", "out")); - assert!(!held(SRC, "grow", "a")); + // Rule 3: `genW` pushes onto `out`, `grow` onto its parameter; `size` + // only reads its parameter's length. + assert_refused("genW", "out", "event:own:"); + assert_refused("grow", "a", "event:own:"); + assert_held("size", "a", true); } #[test] fn an_array_handed_to_a_call_has_no_stable_length() { - assert!(!held(SRC, "handed", "xs")); + // Rule 2: `handed` passes `xs` to `grow`; the twin passes `xs.length` + // to `touch`. + assert_refused("handed", "xs", "passed:own:"); + assert_held("handedTwin", "xs", true); } #[test] fn an_array_a_closure_captures_has_no_stable_length() { - assert!(!held(SRC, "closed", "xs")); + // Rule 1: the closure references `xs`; the twin's references `k`. + assert_refused("closed", "xs", "captured:"); + assert_held("closedTwin", "xs", true); } #[test] fn an_array_a_field_holds_has_no_stable_length() { - let module = perry_parser::parse_typescript(SRC, "t.ts").expect("parses"); - let hir = perry_hir::lower_module(&module, "t", "t.ts").expect("lowers"); - let walk = hir - .classes - .iter() - .flat_map(|c| c.methods.iter()) - .find(|f| f.name == "walk") - .expect("Holder.walk"); - let id = walk - .body - .iter() - .find_map(|s| match s { - perry_hir::Stmt::Let { id, name, .. } if name == "xs" => Some(*id), - _ => None, - }) - .expect("xs"); - let pf = facts(SRC); - let mb = pf.bindings_for("t.ts").expect("module bindings"); - let b = &mb.locals.iter().find(|(l, _)| *l == id).expect("local").1; - assert!(!b.stable_length); + // Rule 1: a field is a heap holder; the twin's Array is in its frame. + assert_refused("walkField", "xs", "heap:"); + assert_held("walkFrame", "xs", true); } diff --git a/crates/perry-wpfacts/src/tests/named_lengths.rs b/crates/perry-wpfacts/src/tests/named_lengths.rs index 4e5c109226..31939745d9 100644 --- a/crates/perry-wpfacts/src/tests/named_lengths.rs +++ b/crates/perry-wpfacts/src/tests/named_lengths.rs @@ -1,12 +1,30 @@ //! Named lengths (`named_lengths`): a local through whose name alone its //! functions change the length of its Arrays. +//! +//! The fact is asked only of `Rep::Arr` locals, so every case asserts the +//! representation first (`Case::arr`): a local that is no `Arr` in this +//! solve would have no named length whatever the rule says. A negative +//! case asserts the reason of its refusal (the `# named_len … refused=` +//! line of the dump) beside a positive twin that differs only in the +//! refused construct. use super::*; +use crate::bindings::Rep; use crate::FactsConfig; -/// `named_length` of the parameter or top-level `let` named `local` of -/// function or method `func`. -fn fact(src: &str, func: &str, local: &str, cfg: FactsConfig) -> bool { +/// What the solve says of one local. +struct Case { + /// The local is a plain program Array (`Rep::Arr`): the fact's + /// precondition. + arr: bool, + named_length: bool, + /// The local's `# named_len` dump line (none: the rule never judged it). + line: Option, +} + +/// The parameter or top-level `let` named `local` of function or method +/// `func`. +fn case_with(src: &str, func: &str, local: &str, cfg: FactsConfig) -> Case { let module = perry_parser::parse_typescript(src, "t.ts").expect("parses"); let hir = perry_hir::lower_module(&module, "t", "t.ts").expect("lowers"); let f = hir @@ -31,18 +49,65 @@ fn fact(src: &str, func: &str, local: &str, cfg: FactsConfig) -> bool { }) }) .unwrap_or_else(|| panic!("local {local} of {func}")); - let pf = facts_with(src, cfg); + let pf = facts_with( + src, + FactsConfig { + diagnostics: true, + ..cfg + }, + ); let mb = pf.bindings_for("t.ts").expect("module bindings"); - mb.locals + let b = &mb + .locals .iter() .find(|(l, _)| *l == id) .unwrap_or_else(|| panic!("local {id} of {func}")) - .1 - .named_length + .1; + let head = format!("# named_len module=0 local={id} "); + Case { + arr: matches!(b.rep, Rep::Arr(_)), + named_length: b.named_length, + line: pf + .dump_text() + .lines() + .find(|l| l.starts_with(&head)) + .map(str::to_string), + } +} + +/// `func`'s `local` is an `Arr` with a named length. +#[track_caller] +fn assert_named(func: &str, local: &str) { + let c = case_with(SRC, func, local, FactsConfig::default()); + assert!(c.arr, "{func}.{local} must be an Arr in this solve"); + assert!( + c.named_length, + "{func}.{local} has no named length: {:?}", + c.line + ); } -fn named(func: &str, local: &str) -> bool { - fact(SRC, func, local, FactsConfig::default()) +/// `func`'s `local` is an `Arr`, and the rule refuses its named length for +/// `reason` (the head of the dump line's reason). +#[track_caller] +fn assert_refused(func: &str, local: &str, reason: &str) { + let c = case_with(SRC, func, local, FactsConfig::default()); + assert!( + c.arr, + "{func}.{local} must be an Arr in this solve, or the case asserts nothing about the rule" + ); + assert!(!c.named_length, "{func}.{local} has a named length"); + let line = c + .line + .unwrap_or_else(|| panic!("no named_len line for {func}.{local}")); + let why = line + .split_once(" refused=") + .unwrap_or_else(|| panic!("{func}.{local} is not refused: {line}")) + .1; + assert!( + why.starts_with(reason), + "{func}.{local} is refused for another reason than {reason}: {why}" + ); } // Each case bears its Arrays at its own site: a token is its birth site. @@ -68,6 +133,14 @@ function picked(): number { for (let i = 0; i < xs.length; i++) { s += xs[i] ?? 0; POPS.pop(); } return s; } +const POPS2: number[] = [1, 2, 3, 4, 5, 6]; +const KEPT2: number[] = [7, 8, 9]; +function pickedTwin(): number { + const xs = k ? POPS2 : KEPT2; + let s = 0; + for (let i = 0; i < xs.length; i++) { s += xs[i] ?? 0; xs.pop(); } + return s; +} function pair(a: number[], b: number[]): number { let s = 0; for (let i = 0; i < a.length; i++) { s += a[i] ?? 0; b.pop(); } @@ -90,6 +163,18 @@ class Bag { return s; } } +class BagTwin { + items: number[] = [1, 2, 3]; + walkOwn(): number { + const xs = this.items; + let s = 0; + for (let i = 0; i < xs.length; i++) { + s += xs[i] ?? 0; + if (xs.length < 6) xs[xs.length] = 0; + } + return s; + } +} function elsewhere(a: number[]): void { a.push(1); } function reader(): number { const xs = [1, 2, 3]; @@ -104,58 +189,63 @@ function popper(a: number[], out: number[]): number { return out.length; } const same = [1, 2, 3, 4]; -console.log(own(3), guarded(1), picked(), pair(same, same), apart([1, 2], [3, 4]), - new Bag().walk(), reader(), popper([1, 2, 3], [9]), popper([4], [5, 6])); +console.log(own(3), guarded(1), picked(), pickedTwin(), pair(same, same), apart([1, 2], [3, 4]), + new Bag().walk(), new BagTwin().walkOwn(), reader(), popper([1, 2, 3], [9]), popper([4], [5, 6])); "#; #[test] fn a_local_stored_through_its_own_name_has_a_named_length() { // `xs.push` and `xs[i] = …` both have `xs` as their receiver. - assert!(named("own", "xs")); + assert_named("own", "xs"); } #[test] fn a_narrowed_read_is_a_read_of_the_name() { // Inside `if (xs)` a read of `xs` sees the guard's node. - assert!(named("guarded", "xs")); + assert_named("guarded", "xs"); } #[test] fn an_array_popped_through_another_name_has_no_named_length() { - // `xs` may hold the Array of `POPS`, which `picked` pops by that name. - assert!(!named("picked", "xs")); + // `xs` may hold the Array of `POPS`, which `picked` pops by that name; + // the twin pops `xs`. + assert_refused("picked", "xs", "event:other-name("); + assert_named("pickedTwin", "xs"); } #[test] fn two_parameters_that_may_be_one_array_have_no_named_length() { - assert!(!named("pair", "a")); + assert_refused("pair", "a", "event:other-name("); // `b`'s own events all go through `b`. - assert!(named("pair", "b")); + assert_named("pair", "b"); } #[test] fn two_parameters_that_are_never_one_array_keep_it() { - assert!(named("apart", "a")); + // The twin of `pair`: the same body, called with two Arrays. + assert_named("apart", "a"); } #[test] fn a_store_through_the_field_that_holds_the_array_ends_it() { - assert!(!named("walk", "xs")); + // The twin stores through `xs`. + assert_refused("walk", "xs", "event:other-name("); + assert_named("walkOwn", "xs"); } #[test] fn an_event_in_a_function_that_cannot_name_the_local_is_none() { // `elsewhere` pushes onto the Array, but while a loop of `reader` // without calls runs, `elsewhere` does not; `out` is another Array. - assert!(named("reader", "xs")); + assert_named("reader", "xs"); } #[test] fn a_pop_through_a_parameter_is_placed_in_its_function() { // `out.pop()` has no store: its rule is placed by its receiver's site, // in a summary instance too. - assert!(named("popper", "out")); - assert!(named("popper", "a")); + assert_named("popper", "out"); + assert_named("popper", "a"); } #[test] @@ -164,5 +254,6 @@ fn the_negative_control_claims_it_for_every_array_local() { sabotage: Some("namedlen".to_string()), ..FactsConfig::default() }; - assert!(fact(SRC, "picked", "xs", cfg)); + let c = case_with(SRC, "picked", "xs", cfg); + assert!(c.arr && c.named_length, "{:?}", c.line); } From 42da766ee816b9a70c87b89ac359bc7e2d5f5d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 12 Nov 2026 06:54:10 +0100 Subject: [PATCH 5/6] check_file_size: re-record six stale allowlist counts --- scripts/check_file_size.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/check_file_size.sh b/scripts/check_file_size.sh index 465671e7e8..25e54ce150 100755 --- a/scripts/check_file_size.sh +++ b/scripts/check_file_size.sh @@ -24,11 +24,11 @@ crates/perry/src/commands/compile/run_pipeline.rs 6248 # Generator for-of destructuring remains in the statement-lowering core; split the function group in a follow-up. crates/perry-hir/src/lower_decl/body_stmt.rs 2063 # Representation-aware codegen core; further decomposition is tracked in #1435. -crates/perry-codegen/src/codegen/mod.rs 3673 +crates/perry-codegen/src/codegen/mod.rs 3677 # Representation-aware HIR fact collection; further decomposition is tracked in #1435. crates/perry-codegen/src/collectors/hir_facts.rs 2770 # Representation-aware expression dispatch; further decomposition is tracked in #1435. -crates/perry-codegen/src/expr/mod.rs 2876 +crates/perry-codegen/src/expr/mod.rs 2875 # Loop versioning and representation evidence share lowering state; decomposition is tracked in #1435. crates/perry-codegen/src/stmt/loops.rs 8428 # Representation-aware map core; further decomposition is tracked in #1435. @@ -38,9 +38,9 @@ crates/perry-runtime/src/set.rs 2347 # Representation feedback core; further decomposition is tracked in #1435. crates/perry-runtime/src/typed_feedback.rs 2437 # Snapshot census runner (instr-only, #13935); split after #14259 lands (snap census owner). -crates/perry-runtime/src/snap_census/runner.rs 3576 +crates/perry-runtime/src/snap_census/runner.rs 4679 # Native ABI proof cases; test organization is a separate follow-up. -crates/perry-codegen/tests/native_proof_regressions.rs 15687 +crates/perry-codegen/tests/native_proof_regressions.rs 15688 # Representation feedback regression cases; test organization is a separate follow-up. crates/perry-runtime/src/typed_feedback/tests.rs 2304 # Lowering report schema and its emitters; split emitters in a follow-up. @@ -50,9 +50,9 @@ crates/perry-transform/src/inline/call_inliner.rs 2106 # Web Streams controllers share state; extract reader, tee and BYOB groups in a follow-up. crates/perry-stdlib/src/streams.rs 2448 # Collection policy and debt accounting share phase state; decomposition is tracked in #1435. -crates/perry-runtime/src/gc/policy.rs 2860 +crates/perry-runtime/src/gc/policy.rs 2877 # Shape registry table and publication invariants require a coordinated split. -crates/perry-runtime/src/object/shapes.rs 4928 +crates/perry-runtime/src/object/shapes.rs 4937 # Generated upstream WinRT collections bindings; preserve the vendor snapshot. third_party/windows-winui/windows-collections/src/bindings.rs 3094 # Generated upstream WinRT async bindings; preserve the vendor snapshot. From 3c83e9fa840a78cff86bf29adc8bcbfd5a8bfe8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 12 Nov 2026 07:02:06 +0100 Subject: [PATCH 6/6] regenerate the wasm32 runtime ABI table and ratchet the unrooted-local baseline down (both gates were red on main) --- crates/perry-codegen/src/wasm32/runtime_abi.tsv | 17 ++++++++++++++++- scripts/unrooted_local_shape_baseline.json | 11 +++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/crates/perry-codegen/src/wasm32/runtime_abi.tsv b/crates/perry-codegen/src/wasm32/runtime_abi.tsv index a04255221e..22be4ab038 100644 --- a/crates/perry-codegen/src/wasm32/runtime_abi.tsv +++ b/crates/perry-codegen/src/wasm32/runtime_abi.tsv @@ -13,6 +13,7 @@ clearInterval void i64 clearTimeout void i64 gc_check_trigger_export void getenv ptr ptr +gettimeofday i32s ptr,ptr global_this_builtin_noop_thunk f64 ptr,i64,f64 js_abort_controller_abort void ptr js_abort_controller_abort_reason void ptr,f64 @@ -1046,6 +1047,7 @@ js_container_stop ptr ptr,i32s js_count_queuing_strategy_new f64 f64 js_create_callback f64 i64,i64,i64 js_create_cjs_namespace f64 i32s,ptr,ptr,ptr,ptr +js_create_deferred_namespace f64 i64 js_create_family_contract_check void ptr,f64 js_create_namespace f64 i32s,ptr,ptr,ptr,ptr js_create_namespace_layout f64 ptr,ptr @@ -1190,6 +1192,8 @@ js_declared_map_get f64 f64,f64 js_decode_uri i64 f64 js_decode_uri_component i64 f64 js_decompression_stream_new f64 +js_deferred_module_ready i32s i64,i64,i32s +js_deferred_namespace_not_ready f64 js_delete_result f64 i32s,i32s js_derived_super_bind_current f64 f64,i32u js_derived_super_scope_pop void @@ -3432,6 +3436,12 @@ js_readline_write f64 i64,f64 js_readonly_set_has f64 f64,f64 js_realm_test_fixture_publish void f64 js_receiver_may_own_named_method i32s f64,f64 +js_record_array_clear void f64 +js_record_array_get f64 f64,f64,ptr +js_record_array_length f64 f64 +js_record_array_new f64 i32u,i32u +js_record_array_pop f64 f64,ptr +js_record_array_push f64 f64,ptr js_recv_route_note void i32u js_referenceerror_new ptr ptr js_reflect_apply f64 f64,f64,f64 @@ -3819,8 +3829,10 @@ js_sharp_webp i64 i64,f64 js_sharp_width f64 i64 js_sleep_ms void f64 js_slot_store_obligations void i64,i64,i64 +js_snap_argv_site void i32s,i64 +js_snap_compare_literal void i32s,f64,f64,i32s js_snap_entry_body void -js_snap_entry_module void +js_snap_entry_module void i32s js_snap_env_site void i32s js_snap_require_edge void i32s js_snap_tls_program void ptr,ptr,ptr,ptr @@ -5301,6 +5313,7 @@ perry_verify_facts_entry void i64,i64 perry_verify_facts_field i32s f64,f64,i64,i64,i64,i64,i32s,i32s,i32s,i32s,i64,i64 perry_verify_facts_init void perry_verify_facts_miss i32s f64,i64,i64 +perry_verify_facts_record_array i32s f64,i64,i64 perry_verify_facts_record_claim void f64 perry_verify_facts_record_identity i32s f64,f64,i64,i64 perry_verify_facts_tick void @@ -5311,5 +5324,7 @@ perry_wpfacts_check_call_miss i32s ptr,i64 perry_wpfacts_check_field i32s f64,f64,i64,i64,i64,i32s,i32s,i32s,i32s,i64 perry_wpfacts_check_miss i32s f64,i64 setInterval i64 i64,f64 +setenv i32s ptr,ptr,i32s +unsetenv i32s ptr zzz_perry_verify_facts_call ptr f64,i64,i64,i64 zzz_perry_verify_facts_field i32s f64,f64,i64,i64,i64,i64,i32s,i32s,i32s,i32s,i64,i64 diff --git a/scripts/unrooted_local_shape_baseline.json b/scripts/unrooted_local_shape_baseline.json index 896d616cb3..886af152a2 100644 --- a/scripts/unrooted_local_shape_baseline.json +++ b/scripts/unrooted_local_shape_baseline.json @@ -5,7 +5,7 @@ "crates/perry-ext-http/src/server/handle_dispatch.rs": 2, "crates/perry-ext-http/src/server/request.rs": 7, "crates/perry-ext-http/src/server/types.rs": 1, - "crates/perry-ext-net/src/classes.rs": 2, + "crates/perry-ext-net/src/classes.rs": 1, "crates/perry-stdlib/src/crypto/kdf.rs": 9, "crates/perry-stdlib/src/crypto/keys.rs": 1, "crates/perry-stdlib/src/crypto/sign.rs": 22, @@ -14,10 +14,9 @@ "crates/perry-stdlib/src/events/module_helpers.rs": 1, "crates/perry-stdlib/src/events/once_helpers.rs": 1, "crates/perry-stdlib/src/querystring.rs": 2, - "crates/perry-stdlib/src/sqlite/better.rs": 10, + "crates/perry-stdlib/src/sqlite/better.rs": 2, "crates/perry-stdlib/src/sqlite/bind.rs": 4, "crates/perry-stdlib/src/streams.rs": 14, - "crates/perry-stdlib/src/streams/byob.rs": 12, "crates/perry-stdlib/src/streams/pipe.rs": 4, "crates/perry-stdlib/src/streams/strategy.rs": 9, "crates/perry-stdlib/src/streams/tee.rs": 2, @@ -31,10 +30,10 @@ "crates/perry-stdlib/src/webcrypto/key_object.rs": 1, "crates/perry-stdlib/src/webcrypto/keys.rs": 40, "crates/perry-stdlib/src/webcrypto/util.rs": 1, - "crates/perry-stdlib/src/worker_threads.rs": 12, + "crates/perry-stdlib/src/worker_threads.rs": 8, "crates/perry-stdlib/src/worker_threads/direct_message.rs": 2, - "crates/perry-stdlib/src/worker_threads/worker_surface.rs": 6 + "crates/perry-stdlib/src/worker_threads/worker_surface.rs": 1 }, "schema_version": 3, - "total": 208 + "total": 178 }