diff --git a/benchmarks/object-write-6812/README.md b/benchmarks/object-write-6812/README.md
new file mode 100644
index 000000000..c39308594
--- /dev/null
+++ b/benchmarks/object-write-6812/README.md
@@ -0,0 +1,58 @@
+# Object-write coverage benchmark (#6812)
+
+This directory preserves the source and measurement corpus for the guarded
+object-write generalization tracked by GitHub issue #6812.
+
+`canonical.ts` is intentionally byte-for-byte equivalent to the historical
+#6759/#6809 acceptance source quoted in the issue. Do not scale or otherwise
+change it; use a separate matrix cell when a longer timing window is needed.
+
+`baseline.md` contains the immutable pre-implementation matrix, classification,
+profiles, and raw samples. `results.md` contains the bounded 1–4-field
+implementation evidence, final samples, IR/code-size comparison, and validation
+results.
+
+Build the compiler and static archives from a clean worktree with an isolated
+target directory:
+
+```bash
+PERRY_OBJECT_WRITE_TARGET=/absolute/path/to/an/isolated/cargo-target
+CARGO_TARGET_DIR="$PERRY_OBJECT_WRITE_TARGET" \
+ cargo build --release \
+ -p perry -p perry-runtime-static -p perry-stdlib-static
+```
+
+Compile with the default auto-optimizing pipeline:
+
+```bash
+PERRY_RUNTIME_DIR="$PERRY_OBJECT_WRITE_TARGET/release" \
+ "$PERRY_OBJECT_WRITE_TARGET/release/perry" \
+ benchmarks/object-write-6812/canonical.ts -o bin_write
+```
+
+Do not set `PERRY_NO_AUTO_OPTIMIZE`, override release codegen units, or reuse
+archives from another worktree. Alternate Node and Perry runs on the same idle
+host and report all raw samples plus the medians.
+
+`matrix.ts` contains the required coverage/rejection cells. Compile it once,
+then use `run_matrix.py` to warm each implementation, verify the checksum, and
+collect the raw in-program `Date.now()` samples in strict Node/Perry order:
+
+```bash
+PERRY_RUNTIME_DIR="$PERRY_OBJECT_WRITE_TARGET/release" \
+ "$PERRY_OBJECT_WRITE_TARGET/release/perry" \
+ benchmarks/object-write-6812/matrix.ts -o bin_matrix
+
+python3 benchmarks/object-write-6812/run_matrix.py \
+ --perry ./bin_matrix --samples 15
+```
+
+The runner emits machine-readable JSON first, followed by a median summary and
+a collapsible table containing every raw sample and checksum. Redirect or
+capture the complete output when collecting evidence; a medians-only excerpt
+is not sufficient to reproduce a result.
+
+For a rejected bounded clone, set
+`PERRY_TRACE_OBJECT_ARRAY_WRITE_GUARD=1` when running the Perry executable to
+print the cold preflight rejection reason. The trace is disabled by default and
+is not reached on successful guards.
diff --git a/benchmarks/object-write-6812/baseline.md b/benchmarks/object-write-6812/baseline.md
new file mode 100644
index 000000000..e9fd78676
--- /dev/null
+++ b/benchmarks/object-write-6812/baseline.md
@@ -0,0 +1,181 @@
+# #6812 pre-implementation coverage baseline
+
+Collected on 2026-07-23 before changing any compiler or runtime source.
+
+## Provenance and protocol
+
+- Source: `origin/main` commit
+ `20f19758f370f7616530824218d87f46a67cd3f0`, which is the required #6811
+ merge.
+- Clean worktree branch: `perf/6812-object-write-generalization`.
+- Isolated Cargo target:
+ `/private/tmp/perry-6812-target.qyHg3D`.
+- Compiler: Perry 0.5.1264, built with the issue's exact release command and
+ the repository release/LTO profile.
+- App compilation used the default auto-optimizing pipeline. Neither
+ `PERRY_NO_AUTO_OPTIMIZE` nor a codegen-unit override was set.
+- Host: macOS 26.5 (25F71), Apple M1 Max, arm64, 10 logical CPUs.
+- Node: v26.3.0.
+- Rust: rustc 1.96.1, cargo 1.96.1.
+- Each matrix cell had one Node/Perry warmup followed by 15 strict
+ Node-then-Perry pairs. The runner compared `(writes, checksum)` after every
+ pair and retained all samples without outlier filtering.
+- No old Perry, Node, Cargo, rustc, or matrix process was active when the
+ definitive capture began. Sporadic OS/UI interruptions remain visible in
+ the raw data instead of being discarded.
+
+The exact historical canonical source produced:
+
+| Implementation | Raw `write_ms` samples | Median | Sink |
+|---|---|---:|---:|
+| Node | `[7, 8, 7, 7, 8, 7, 8, 8, 7, 7, 8, 8, 8, 8, 7]` | 8 ms | 9595200 |
+| Perry | `[5, 6, 6, 5, 6, 6, 6, 5, 6, 6, 6, 6, 5, 6, 6]` | 6 ms | 9595200 |
+
+The one-millisecond shift from the published 7/5 medians is `Date.now()`
+quantization at this duration; Perry still beats Node on the exact unchanged
+source.
+
+## Executable matrix
+
+| Dimension | Cell | Writes | Node median | Perry median |
+|---|---|---:|---:|---:|
+| Key | `o.x` | 120000000 | 131 ms | 689 ms |
+| Key | `o["x"]` | 120000000 | 132 ms | 693 ms |
+| Key | stable `o[k]` | 120000000 | 138 ms | 2425 ms |
+| Key | alternating dynamic keys | 24000000 | 183 ms | 625 ms |
+| RHS | numeric scalar | 120000000 | 133 ms | 698 ms |
+| RHS | pointer-capable value | 96000000 | 120 ms | 1480 ms |
+| RHS | allocating literal | 19200000 | 119 ms | 11126 ms |
+| RHS | function call | 108000000 | 121 ms | 3260 ms |
+| Receiver shapes | monomorphic | 120000000 | 135 ms | 706 ms |
+| Receiver shapes | 2-shape | 96000000 | 131 ms | 5432 ms |
+| Receiver shapes | 4-shape | 60000000 | 108 ms | 3330 ms |
+| Receiver shapes | transition before loop | 120000000 | 131 ms | 681 ms |
+| Fields/iteration | 1 | 120000000 | 131 ms | 692 ms |
+| Fields/iteration | 2 | 240000000 | 148 ms | 138 ms |
+| Fields/iteration | 4 | 384000000 | 137 ms | 1444 ms |
+| Fields/iteration | 8 | 384000000 | 102 ms | 1272 ms |
+| Loop form | single counted loop | 120000000 | 163 ms | 2249 ms |
+| Loop form | current nested loop | 240000000 | 149 ms | 138 ms |
+| Loop form | stable local bounds | 192000000 | 129 ms | 868 ms |
+| Loop form | non-zero inner start | 192000000 | 120 ms | 872 ms |
+| Storage | inline existing slot | 120000000 | 132 ms | 693 ms |
+| Storage | wide/overflow object | 120000000 | 180 ms | 7154 ms |
+| Receiver kind | anonymous object | 120000000 | 109 ms | 692 ms |
+| Receiver kind | class instance | 120000000 | 108 ms | 700 ms |
+| Receiver kind | class-id-zero plain object | 120000000 | 110 ms | 6356 ms |
+
+Every one of the 375 measured Node/Perry pairs had identical write counts and
+checksums.
+
+## Deterministic path classification
+
+The retained 856,485-byte pre-optimization LLVM module was classified by
+function-local calls to `js_put_value_set`, `js_put_value_set_ic_miss`, and
+`js_object_array_numeric_write2_guard`. The timed loop was also inspected
+separately when its function contains setup writes.
+
+| Cell | Timed path | IR calls (`generic`, `PIC`, `clone`) | Reason / rejection |
+|---|---|---:|---|
+| `o.x` | static PIC | 0, 1, 0 | Literal key and call-free numeric RHS. |
+| `o["x"]` | static PIC | 0, 1, 0 | Lowers to the same literal-key form as dot access. |
+| stable `o[k]` | generic | 1, 0, 0 | Key is a local expression, not `Expr::String`. |
+| alternating keys | generic | 1, 0, 0 | Dynamic PropertyKey conversion/identity stays semantic. |
+| numeric RHS | static PIC | 0, 1, 0 | Safepoint-free scalar value. |
+| pointer RHS | static PIC | 0, 1, 0 | Safepoint-free, but retains layout/string-alias/barrier handling. |
+| allocating RHS | generic | 1, 0, 0 | RHS can allocate after the receiver reference is evaluated. |
+| function RHS | generic | 1, 0, 0 | RHS call is a safepoint and may throw/observe user code. |
+| monomorphic shape | static PIC | 0, 1, 0 | Stable discriminated ShapeId token primes and hits. |
+| 2-shape | static PIC, runtime miss | 0, 1, 0 | One-entry cache thrashes on alternating shape tokens. |
+| 4-shape | static PIC, runtime miss | 0, 1, 0 | One-entry cache thrashes on four shape tokens. |
+| transition before loop | static PIC | 0, 2, 0 | Setup transition has its own site; timed site sees one stable post-transition token. |
+| 1 field | static PIC | 0, 1, 0 | Whole-loop matcher requires exactly two stores. |
+| 2 fields | whole-loop clone | 0, 2, 1 | Once-per-nest proof succeeds; both PIC sites remain in the semantic clone. |
+| 4 fields | four static PICs | 0, 4, 0 | Whole-loop matcher requires exactly two stores. |
+| 8 fields | eight static PICs | 0, 8, 0 | Whole-loop matcher requires exactly two stores. |
+| single counted loop | static PIC | 0, 1, 0 | Whole-loop proof requires the dense nested-array form. |
+| current nested loop | whole-loop clone | 0, 2, 1 | Exact #6811 shape. |
+| stable local bounds | static PICs | 0, 2, 0 | Counted-loop matcher requires literal bounds. |
+| nonzero inner start | static PICs | 0, 2, 0 | Dense-prefix proof deliberately starts at zero. |
+| inline storage | static PIC | 0, 1, 0 | Existing slot is within the physical inline allocation. |
+| overflow storage | static PIC, runtime miss | 1 setup, 1, 0 | Dynamic setup is generic; timed slot 4 cannot prime against a four-slot inline allocation. |
+| anonymous object | static PIC | 0, 1, 0 | Compiler-created regular object has a nonzero class/shape identity. |
+| class instance | static PIC | 0, 1, 0 | Eligible regular instance with stable class/shape identity. |
+| class-id-zero object | static PIC, runtime miss | 0, 1, 0 | Parsed plain object fails the runtime `class_id != 0` eligibility guard. |
+
+The key measured discontinuity is the bounded static numeric nest:
+
+- two fields: 240M stores in 138 ms through the transactional clone;
+- one field: 120M stores in 692 ms through a per-store PIC;
+- four fields: 384M stores in 1444 ms through four per-store PICs.
+
+The four-field cell is the highest-volume sound miss in the matrix and is
+10.5× slower than Node, while the established two-field proof is already
+slightly faster than Node.
+
+## Sample evidence
+
+The stripped executable was linked once more with `PERRY_LINK_MAP` (which does
+not alter generated code) and sampled with macOS `sample`.
+
+For stable dynamic-key writes, 1,204 of 1,466 main-thread samples (82.1%) were
+under `js_put_value_set`. Of those, 519 reached
+`try_existing_own_data_overwrite`, and 108 were under
+`runtime_store_jsvalue_slot`, with further frames in layout-note and write
+barrier work. The addresses were resolved against these linker-map ranges:
+
+```text
+0x100489E00 + 0xBBC js_put_value_set
+0x100298750 + 0x2D0 try_existing_own_data_overwrite
+0x10014AA44 + 0x0AC runtime_store_jsvalue_slot
+```
+
+For the four-field static cell, 441 of 448 post-startup runnable samples
+(98.4%) were directly in the inlined `main` body, with no child runtime frame.
+Together with the four PIC call sites retained in IR, this shows the cost is
+the repeated inlined per-store guard sequence, not a generic-runtime call.
+The two-field executable result demonstrates the benefit of paying a bounded
+preflight once and then keeping the complete nest call/GC-free.
+
+## Baseline code size
+
+| Artifact | Size |
+|---|---:|
+| Matrix LLVM IR | 856,485 bytes / 21,966 lines |
+| Matrix app object | 278,968 bytes |
+| Matrix executable | 6,302,720 bytes |
+| Linked `main` text contribution | 47,232 bytes |
+| Canonical executable | 6,170,504 bytes |
+
+
+All raw matrix samples and checksums
+
+| Cell | Checksum | Node ms | Perry ms |
+|---|---:|---|---|
+| `key_dot` | `122876400` | `[132, 131, 130, 132, 132, 131, 130, 131, 133, 132, 131, 131, 130, 132, 131]` | `[690, 688, 690, 691, 688, 689, 688, 711, 701, 688, 690, 688, 689, 689, 695]` |
+| `key_literal` | `122876400` | `[131, 134, 132, 133, 132, 134, 132, 131, 131, 132, 132, 132, 133, 131, 131]` | `[692, 691, 693, 693, 701, 690, 689, 695, 691, 692, 693, 694, 693, 701, 691]` |
+| `key_stable_dynamic` | `122876400` | `[138, 138, 140, 139, 136, 136, 135, 148, 140, 141, 138, 136, 139, 137, 140]` | `[2425, 2448, 2431, 2384, 2395, 2383, 2653, 2549, 2442, 2456, 2423, 2419, 2399, 2408, 2435]` |
+| `key_alternating_dynamic` | `31194000` | `[183, 266, 184, 179, 194, 185, 183, 312, 182, 181, 178, 183, 181, 182, 180]` | `[637, 796, 840, 846, 623, 625, 790, 667, 610, 613, 605, 644, 614, 607, 610]` |
+| `rhs_numeric` | `122876400` | `[133, 139, 132, 134, 136, 133, 132, 132, 133, 133, 132, 132, 133, 134, 133]` | `[699, 711, 697, 702, 698, 697, 700, 696, 706, 697, 684, 683, 695, 701, 712]` |
+| `rhs_pointer` | `40800` | `[121, 120, 119, 120, 120, 120, 119, 120, 119, 121, 121, 122, 122, 119, 120]` | `[1487, 1488, 1480, 1480, 1477, 1481, 1478, 1482, 1480, 1478, 1515, 1482, 1481, 1479, 1479]` |
+| `rhs_allocating` | `22076400` | `[119, 114, 153, 143, 134, 120, 117, 113, 117, 113, 112, 113, 142, 195, 119]` | `[10936, 12509, 16052, 13348, 11307, 11055, 10870, 10729, 10714, 10823, 10733, 11126, 15021, 13009, 11158]` |
+| `rhs_call` | `110876400` | `[123, 191, 124, 119, 126, 119, 118, 121, 129, 120, 129, 119, 145, 120, 121]` | `[3309, 3418, 3311, 3214, 3217, 3260, 3256, 3264, 3300, 3243, 3250, 3589, 3864, 3235, 3218]` |
+| `shape_monomorphic` | `122876400` | `[135, 135, 135, 133, 136, 134, 135, 137, 133, 136, 133, 135, 135, 135, 135]` | `[709, 708, 705, 706, 721, 707, 713, 700, 697, 691, 705, 709, 695, 712, 689]` |
+| `shape_two` | `98876400` | `[130, 131, 150, 131, 130, 134, 137, 131, 130, 132, 134, 132, 130, 132, 130]` | `[5330, 5963, 5401, 5432, 7717, 5527, 5456, 5429, 5440, 5433, 5432, 5368, 5320, 5352, 5326]` |
+| `shape_four` | `62876400` | `[107, 106, 108, 109, 107, 108, 109, 106, 109, 110, 114, 109, 106, 108, 107]` | `[3330, 3332, 3323, 3327, 3395, 3337, 3327, 3341, 3392, 3373, 3387, 3317, 3284, 3296, 3288]` |
+| `shape_transition_before_loop` | `122876400` | `[130, 131, 131, 131, 131, 131, 133, 131, 130, 133, 131, 132, 132, 132, 132]` | `[679, 680, 691, 680, 682, 680, 681, 682, 680, 681, 682, 682, 681, 681, 682]` |
+| `fields_one` | `122876400` | `[131, 132, 130, 131, 132, 131, 131, 131, 131, 132, 132, 131, 130, 131, 131]` | `[691, 695, 693, 692, 690, 689, 692, 689, 689, 693, 713, 691, 692, 693, 690]` |
+| `fields_two` | `239995200` | `[149, 148, 150, 148, 147, 148, 149, 150, 148, 148, 148, 148, 147, 149, 149]` | `[138, 138, 139, 139, 137, 138, 137, 138, 137, 138, 138, 138, 138, 137, 140]` |
+| `fields_four` | `383990400` | `[136, 136, 137, 136, 137, 137, 136, 137, 137, 137, 137, 137, 137, 141, 138]` | `[1432, 1446, 1447, 1444, 1441, 1443, 1446, 1445, 1444, 1446, 1443, 1442, 1446, 1455, 1441]` |
+| `fields_eight` | `383980800` | `[102, 102, 101, 100, 101, 102, 104, 102, 102, 112, 101, 103, 102, 114, 102]` | `[1260, 1262, 1259, 1260, 1273, 1272, 1270, 1661, 1782, 1276, 1591, 1276, 1272, 1266, 1264]` |
+| `loop_single_counted` | `287997118800` | `[164, 163, 164, 163, 163, 163, 163, 164, 164, 164, 164, 162, 163, 162, 163]` | `[2286, 2258, 2247, 2250, 2251, 2254, 2248, 2246, 2248, 2247, 2247, 2249, 2249, 2248, 2253]` |
+| `loop_current_nested` | `239995200` | `[150, 149, 148, 150, 149, 150, 148, 151, 148, 148, 148, 151, 149, 150, 149]` | `[138, 139, 138, 138, 138, 139, 138, 138, 138, 137, 140, 138, 137, 138, 138]` |
+| `loop_stable_local_bounds` | `191995200` | `[130, 129, 129, 129, 129, 128, 129, 130, 130, 130, 132, 130, 130, 129, 128]` | `[882, 865, 868, 873, 869, 916, 864, 868, 867, 868, 869, 882, 887, 866, 864]` |
+| `loop_nonzero_start` | `191995200` | `[119, 122, 119, 120, 119, 121, 120, 120, 119, 120, 121, 120, 120, 121, 119]` | `[872, 876, 872, 874, 887, 871, 872, 871, 871, 871, 872, 887, 873, 872, 886]` |
+| `storage_inline` | `122876400` | `[132, 131, 131, 132, 132, 133, 132, 142, 136, 133, 132, 134, 132, 131, 132]` | `[692, 693, 693, 694, 692, 693, 695, 694, 693, 686, 684, 682, 695, 692, 692]` |
+| `storage_overflow` | `122876400` | `[180, 180, 178, 179, 178, 180, 190, 179, 180, 180, 179, 179, 181, 191, 179]` | `[7169, 7120, 7171, 7149, 7167, 7142, 7154, 7121, 7130, 7139, 7165, 7135, 7174, 7432, 7163]` |
+| `receiver_anonymous` | `122876400` | `[110, 110, 108, 109, 109, 110, 108, 109, 109, 108, 110, 108, 109, 109, 110]` | `[692, 693, 693, 692, 692, 692, 693, 693, 692, 693, 691, 694, 692, 692, 698]` |
+| `receiver_class_instance` | `122876400` | `[105, 108, 109, 107, 107, 108, 109, 144, 106, 114, 110, 108, 107, 113, 108]` | `[697, 695, 707, 705, 695, 687, 700, 950, 897, 896, 700, 700, 956, 691, 706]` |
+| `receiver_class_id_zero` | `122876400` | `[110, 111, 106, 105, 106, 107, 120, 111, 107, 107, 1125, 134, 119, 106, 137]` | `[6381, 6672, 6203, 6112, 6106, 6596, 6356, 6217, 6198, 42809, 20211, 7303, 6197, 6235, 7880]` |
+
+
diff --git a/benchmarks/object-write-6812/canonical.ts b/benchmarks/object-write-6812/canonical.ts
new file mode 100644
index 000000000..24abf9138
--- /dev/null
+++ b/benchmarks/object-write-6812/canonical.ts
@@ -0,0 +1,21 @@
+// #6759/#6809 acceptance: property writes to existing fields.
+const objs: any[] = [];
+for (let i = 0; i < 2400; i++) {
+ objs.push({ a: i, b: i * 2, c: 0, d: 0 });
+}
+
+const t0 = Date.now();
+for (let r = 0; r < 2000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const o = objs[i];
+ o.c = r + i;
+ o.d = r - i;
+ }
+}
+const t1 = Date.now();
+
+let sink = 0;
+for (let i = 0; i < 2400; i++) {
+ sink += objs[i].c + objs[i].d;
+}
+console.log("write_ms", t1 - t0, "sink", sink);
diff --git a/benchmarks/object-write-6812/followup-stable-key.md b/benchmarks/object-write-6812/followup-stable-key.md
new file mode 100644
index 000000000..7a190ff79
--- /dev/null
+++ b/benchmarks/object-write-6812/followup-stable-key.md
@@ -0,0 +1,14 @@
+# Immutable dynamic-key follow-up
+
+This follow-up measures the `const key = "x"; object[key] = ...` slice on top
+of the bounded object-write loop fast path. The compiler was built from this
+branch with isolated release artifacts; outputs were checked against Node.
+
+| Cell | Node | Perry | Writes | Sink |
+| --- | ---: | ---: | ---: | ---: |
+| `key_stable_dynamic` | 138 ms | 124 ms | 120,000,000 | 122,876,400 |
+| `key_alternating_dynamic` | 180 ms | 615 ms | 24,000,000 | 31,194,000 |
+
+The stable-key cell now reaches the same bounded loop fast path as the static
+key cells. Alternating keys remain intentionally generic and are retained as
+the rejection/control case.
diff --git a/benchmarks/object-write-6812/matrix.ts b/benchmarks/object-write-6812/matrix.ts
new file mode 100644
index 000000000..3ca442dbf
--- /dev/null
+++ b/benchmarks/object-write-6812/matrix.ts
@@ -0,0 +1,532 @@
+// Scaled object-write coverage/rejection matrix for #6812.
+//
+// Every timed cell is deliberately at least about 100 ms on the development
+// host. Setup and checksum work stay outside the timed region. Run one cell at
+// a time:
+//
+// node --experimental-strip-types matrix.ts key_dot
+// ./matrix key_dot
+
+type CellResult = {
+ elapsed: number;
+ writes: number;
+ sink: number;
+};
+
+function checksum(objects: any[], keys: string[]): number {
+ let sink = 0;
+ for (let i = 0; i < objects.length; i++) {
+ const object: any = objects[i];
+ for (let k = 0; k < keys.length; k++) {
+ sink += object[keys[k]];
+ }
+ }
+ return sink;
+}
+
+function keyDot(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 50000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = r + i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 120000000, sink: checksum(objects, ["x"]) };
+}
+
+function keyLiteral(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 50000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object["x"] = r + i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 120000000, sink: checksum(objects, ["x"]) };
+}
+
+function keyStableDynamic(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const key = "x";
+ const t0 = Date.now();
+ for (let r = 0; r < 50000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object[key] = r + i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 120000000, sink: checksum(objects, ["x"]) };
+}
+
+function keyAlternatingDynamic(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 10000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ const key = (i & 1) === 0 ? "x" : "y";
+ object[key] = r + i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 24000000, sink: checksum(objects, ["x", "y"]) };
+}
+
+function rhsNumeric(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 50000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = r + i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 120000000, sink: checksum(objects, ["x"]) };
+}
+
+function rhsPointer(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const pointer: any = { value: 17 };
+ const t0 = Date.now();
+ for (let r = 0; r < 40000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = pointer;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ let sink = 0;
+ for (let i = 0; i < objects.length; i++) {
+ sink += objects[i].x.value;
+ }
+ return { elapsed, writes: 96000000, sink };
+}
+
+function rhsAllocating(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 8000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = { value: r + i };
+ }
+ }
+ const elapsed = Date.now() - t0;
+ let sink = 0;
+ for (let i = 0; i < objects.length; i++) {
+ sink += objects[i].x.value;
+ }
+ return { elapsed, writes: 19200000, sink };
+}
+
+function rhsCall(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const candidates: any[] = [(left: number, right: number) => left + right];
+ const produce: any = candidates[0];
+ const t0 = Date.now();
+ for (let r = 0; r < 45000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = produce(r, i);
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 108000000, sink: checksum(objects, ["x"]) };
+}
+
+function shapeMonomorphic(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 50000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = r + i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 120000000, sink: checksum(objects, ["x"]) };
+}
+
+function shapeTwo(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push(
+ (i & 1) === 0 ? { x: i, a: 0 } : { x: i, b: 0 },
+ );
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 40000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = r + i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 96000000, sink: checksum(objects, ["x"]) };
+}
+
+function shapeFour(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ const kind = i & 3;
+ if (kind === 0) objects.push({ x: i, a: 0 });
+ else if (kind === 1) objects.push({ x: i, b: 0 });
+ else if (kind === 2) objects.push({ x: i, c: 0 });
+ else objects.push({ x: i, d: 0 });
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 25000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = r + i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 60000000, sink: checksum(objects, ["x"]) };
+}
+
+function shapeTransitionBeforeLoop(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ for (let i = 0; i < 2400; i++) {
+ objects[i].added = i;
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 50000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = r + i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 120000000, sink: checksum(objects, ["x"]) };
+}
+
+function fieldsOne(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 50000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = r + i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 120000000, sink: checksum(objects, ["x"]) };
+}
+
+function fieldsTwo(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 50000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.z = r + i;
+ object.w = r - i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return {
+ elapsed,
+ writes: 240000000,
+ sink: checksum(objects, ["z", "w"]),
+ };
+}
+
+function fieldsFour(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 40000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = r + i;
+ object.y = r - i;
+ object.z = r + i + 1;
+ object.w = r - i - 1;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return {
+ elapsed,
+ writes: 384000000,
+ sink: checksum(objects, ["x", "y", "z", "w"]),
+ };
+}
+
+function fieldsEight(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ a: 0, b: 0, c: 0, d: 0, e: 0, f: 0, g: 0, h: 0 });
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 20000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.a = r + i;
+ object.b = r - i;
+ object.c = r + i + 1;
+ object.d = r - i - 1;
+ object.e = r + i + 2;
+ object.f = r - i - 2;
+ object.g = r + i + 3;
+ object.h = r - i - 3;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return {
+ elapsed,
+ writes: 384000000,
+ sink: checksum(objects, ["a", "b", "c", "d", "e", "f", "g", "h"]),
+ };
+}
+
+function loopSingleCounted(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const t0 = Date.now();
+ for (let n = 0; n < 120000000; n++) {
+ const object: any = objects[n % 2400];
+ object.x = n;
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 120000000, sink: checksum(objects, ["x"]) };
+}
+
+function loopCurrentNested(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 50000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.z = r + i;
+ object.w = r - i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return {
+ elapsed,
+ writes: 240000000,
+ sink: checksum(objects, ["z", "w"]),
+ };
+}
+
+function loopStableLocalBounds(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const rounds = 40000;
+ const length = 2400;
+ const t0 = Date.now();
+ for (let r = 0; r < rounds; r++) {
+ for (let i = 0; i < length; i++) {
+ const object: any = objects[i];
+ object.z = r + i;
+ object.w = r - i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return {
+ elapsed,
+ writes: 192000000,
+ sink: checksum(objects, ["z", "w"]),
+ };
+}
+
+function loopNonzeroStart(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2405; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 40000; r++) {
+ for (let i = 5; i < 2405; i++) {
+ const object: any = objects[i];
+ object.z = r + i;
+ object.w = r - i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return {
+ elapsed,
+ writes: 192000000,
+ sink: checksum(objects, ["z", "w"]),
+ };
+}
+
+function storageInline(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i, y: i * 2, z: 0, w: 0 });
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 50000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = r + i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 120000000, sink: checksum(objects, ["x"]) };
+}
+
+function storageOverflow(): CellResult {
+ const objects: any[] = [];
+ const overflowKey = "x";
+ for (let i = 0; i < 2400; i++) {
+ const object: any = { a0: 0, a1: 0, a2: 0, a3: 0 };
+ object[overflowKey] = i;
+ objects.push(object);
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 50000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = r + i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 120000000, sink: checksum(objects, ["x"]) };
+}
+
+function receiverAnonymous(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push({ x: i });
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 50000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = r + i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 120000000, sink: checksum(objects, ["x"]) };
+}
+
+function receiverClassInstance(): CellResult {
+ class Cell {
+ x = 0;
+ }
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push(new Cell());
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 50000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = r + i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 120000000, sink: checksum(objects, ["x"]) };
+}
+
+function receiverClassIdZero(): CellResult {
+ const objects: any[] = [];
+ for (let i = 0; i < 2400; i++) {
+ objects.push(JSON.parse('{"x":0}'));
+ }
+ const t0 = Date.now();
+ for (let r = 0; r < 50000; r++) {
+ for (let i = 0; i < 2400; i++) {
+ const object: any = objects[i];
+ object.x = r + i;
+ }
+ }
+ const elapsed = Date.now() - t0;
+ return { elapsed, writes: 120000000, sink: checksum(objects, ["x"]) };
+}
+
+const name = process.argv[2] || "key_dot";
+let result: CellResult;
+if (name === "key_dot") result = keyDot();
+else if (name === "key_literal") result = keyLiteral();
+else if (name === "key_stable_dynamic") result = keyStableDynamic();
+else if (name === "key_alternating_dynamic") result = keyAlternatingDynamic();
+else if (name === "rhs_numeric") result = rhsNumeric();
+else if (name === "rhs_pointer") result = rhsPointer();
+else if (name === "rhs_allocating") result = rhsAllocating();
+else if (name === "rhs_call") result = rhsCall();
+else if (name === "shape_monomorphic") result = shapeMonomorphic();
+else if (name === "shape_two") result = shapeTwo();
+else if (name === "shape_four") result = shapeFour();
+else if (name === "shape_transition_before_loop") {
+ result = shapeTransitionBeforeLoop();
+} else if (name === "fields_one") result = fieldsOne();
+else if (name === "fields_two") result = fieldsTwo();
+else if (name === "fields_four") result = fieldsFour();
+else if (name === "fields_eight") result = fieldsEight();
+else if (name === "loop_single_counted") result = loopSingleCounted();
+else if (name === "loop_current_nested") result = loopCurrentNested();
+else if (name === "loop_stable_local_bounds") result = loopStableLocalBounds();
+else if (name === "loop_nonzero_start") result = loopNonzeroStart();
+else if (name === "storage_inline") result = storageInline();
+else if (name === "storage_overflow") result = storageOverflow();
+else if (name === "receiver_anonymous") result = receiverAnonymous();
+else if (name === "receiver_class_instance") result = receiverClassInstance();
+else if (name === "receiver_class_id_zero") result = receiverClassIdZero();
+else throw new Error(`unknown matrix cell: ${name}`);
+
+console.log(
+ "cell",
+ name,
+ "ms",
+ result.elapsed,
+ "writes",
+ result.writes,
+ "sink",
+ result.sink,
+);
diff --git a/benchmarks/object-write-6812/results.md b/benchmarks/object-write-6812/results.md
new file mode 100644
index 000000000..96de0b57c
--- /dev/null
+++ b/benchmarks/object-write-6812/results.md
@@ -0,0 +1,145 @@
+# #6812 bounded object-write generalization results
+
+Collected on 2026-07-23/24 from the same clean worktree, host, isolated Cargo
+target, release/LTO compiler, and default auto-optimized runtime described in
+[`baseline.md`](baseline.md). All post-change application objects were
+regenerated with `PERRY_NO_CACHE=1`; application execution used the normal
+production configuration.
+
+## Implemented slice
+
+The #6811 whole-nest proof is generalized from exactly two writes to a bounded
+descriptor of one through four static fields:
+
+- the compiler preserves property/source order (including duplicates), proves
+ every arithmetic intermediate finite over the complete loop domain, performs
+ one preflight, and emits a finite call/GC/side-exit-free fast clone;
+- the runtime validates the dense receiver prefix, shared shape, object state,
+ physical slot bounds, and intact typed descriptors before publishing four
+ packed 16-bit slot lanes;
+- finite numeric bits are admitted in verified raw-f64 or ordinary JSValue
+ typed slots;
+- the once-only lookup compares live string-pool keys by content and does not
+ accidentally depend on whether another site interned the same name;
+- the previous two-field exported ABI remains available for cached generated
+ objects;
+- a failed proof enters the original semantic clone before any store. Dynamic
+ keys, safepointing/pointer RHS expressions, mixed shapes, holes, short
+ arrays, descriptors/mutability flags, overflow slots, class-id-zero objects,
+ nonzero/variable bounds, and five or more writes remain bounded fallbacks.
+
+Set `PERRY_TRACE_OBJECT_ARRAY_WRITE_GUARD=1` to print a cold rejection reason.
+The cold logger is reached only after a proof branch has already failed; a
+successful production guard performs no environment lookup or logging.
+
+## Required 15-pair measurements
+
+One warmup was followed by 15 strict Node-then-Perry pairs. Every pair matched
+the write count and checksum.
+
+| Fields/iteration | Baseline Perry | Final Node | Final Perry | Change |
+|---:|---:|---:|---:|---:|
+| 1 | 692 ms | 133 ms | **122 ms** | **5.67x faster** |
+| 2 | 138 ms | 150 ms | **139 ms** | +0.7% (noise) |
+| 4 | 1,444 ms | 142 ms | **127 ms** | **11.37x faster** |
+| 8 (bounded rejection) | 1,272 ms | 102 ms | 1,271 ms | -0.1% (unchanged) |
+
+Raw alternating samples:
+
+| Cell | Checksum | Node ms | Perry ms |
+|---|---:|---|---|
+| `fields_one` | `122876400` | `[130, 133, 133, 130, 131, 131, 136, 137, 135, 135, 133, 138, 135, 133, 138]` | `[121, 121, 120, 121, 124, 124, 123, 121, 122, 121, 123, 126, 123, 122, 119]` |
+| `fields_two` | `239995200` | `[170, 172, 148, 150, 148, 149, 149, 152, 149, 151, 147, 148, 158, 150, 154]` | `[155, 149, 139, 137, 138, 139, 139, 138, 137, 137, 140, 144, 139, 138, 144]` |
+| `fields_four` | `383990400` | `[140, 143, 145, 141, 145, 142, 138, 142, 140, 136, 142, 158, 151, 136, 136]` | `[131, 132, 125, 126, 130, 124, 127, 126, 127, 130, 131, 140, 137, 123, 123]` |
+| `fields_eight` | `383980800` | `[100, 100, 100, 106, 104, 101, 103, 100, 102, 101, 104, 107, 107, 102, 102]` | `[1260, 1265, 1268, 1273, 1271, 1272, 1274, 1270, 1269, 1278, 1267, 1274, 1271, 1273, 1279]` |
+
+The exact unchanged canonical source also used 15 alternating pairs:
+
+| Implementation | Raw `write_ms` samples | Median | Sink |
+|---|---|---:|---:|
+| Node | `[8, 7, 8, 8, 8, 8, 9, 9, 7, 8, 7, 7, 7, 8, 8]` | 8 ms | 9595200 |
+| Perry | `[6, 5, 6, 6, 6, 6, 5, 7, 6, 6, 5, 6, 5, 6, 6]` | **6 ms** | 9595200 |
+
+The established two-field and canonical paths therefore show no material
+regression. The newly supported one- and four-field cells are faster than Node
+on the same protocol.
+
+## Full executable parity sweep
+
+The final 25-cell matrix was also executed once in strict Node/Perry order
+after the definitive release build. All 25 write-count/checksum pairs matched.
+The intended boundaries remained visible: dynamic/safepointing, polymorphic,
+wide-storage, variable-bound, eight-field, and class-id-zero cells stayed on
+their previous paths, while eligible static call-free nested cells reached
+Node parity.
+
+| Cell | Node | Perry |
+|---|---:|---:|
+| `key_dot` | 136 ms | 120 ms |
+| `key_literal` | 133 ms | 123 ms |
+| `key_stable_dynamic` | 139 ms | 2,403 ms |
+| `key_alternating_dynamic` | 181 ms | 612 ms |
+| `rhs_numeric` | 135 ms | 124 ms |
+| `rhs_pointer` | 118 ms | 1,488 ms |
+| `rhs_allocating` | 152 ms | 11,196 ms |
+| `rhs_call` | 117 ms | 3,246 ms |
+| `shape_monomorphic` | 136 ms | 123 ms |
+| `shape_two` | 129 ms | 5,350 ms |
+| `shape_four` | 113 ms | 3,407 ms |
+| `shape_transition_before_loop` | 136 ms | 124 ms |
+| `fields_one` | 137 ms | 126 ms |
+| `fields_two` | 155 ms | 142 ms |
+| `fields_four` | 146 ms | 131 ms |
+| `fields_eight` | 104 ms | 1,306 ms |
+| `loop_single_counted` | 173 ms | 2,334 ms |
+| `loop_current_nested` | 154 ms | 146 ms |
+| `loop_stable_local_bounds` | 135 ms | 910 ms |
+| `loop_nonzero_start` | 126 ms | 942 ms |
+| `storage_inline` | 137 ms | 126 ms |
+| `storage_overflow` | 181 ms | 7,315 ms |
+| `receiver_anonymous` | 113 ms | 122 ms |
+| `receiver_class_instance` | 105 ms | 123 ms |
+| `receiver_class_id_zero` | 112 ms | 6,740 ms |
+
+This one-pair sweep is parity/rejection evidence, not a replacement for the
+15-pair performance measurements above.
+
+## Generated IR and code size
+
+Function-local inspection of the definitive uncached LLVM module found:
+
+- `fieldsOne`: one generalized guard, one fallback PIC;
+- `fieldsTwo`: one generalized guard, two fallback PICs;
+- `fieldsFour`: one generalized guard, four fallback PICs;
+- `fieldsEight`: no generalized guard, eight fallback PICs;
+- 16 generated fast inner blocks across the matrix and zero calls in them;
+- zero calls to the old two-field guard from new code.
+
+| Artifact | Baseline | Final | Growth |
+|---|---:|---:|---:|
+| Matrix LLVM IR | 856,485 B / 21,966 lines | 892,819 B / 22,764 lines | +4.24% / +3.63% |
+| Matrix app object | 278,968 B | 284,800 B | +2.09% |
+| Linked `main` text | 47,232 B | 49,012 B | +3.77% |
+| Matrix executable | 6,302,720 B | 6,302,720 B | 0% |
+| Canonical executable | 6,170,504 B | 6,170,512 B | +8 B |
+
+## Validation
+
+- `cargo fmt --all -- --check`
+- `git diff --check`
+- library-target Clippy completed for both changed crates; a stricter
+ all-target run reaches pre-existing denied test lints in untouched files
+- GC store inventory self-test and full inventory: 1,161 files, 245 audited
+ sites, 65 allowlisted
+- codegen native proof regressions: 248 passed
+- shadow-slot hygiene: 10 passed
+- runtime library tests: 1,460 passed, 3 ignored, 0 failed
+- release adversarial corpus: exact Node parity in default,
+ `PERRY_DISABLE_CLASS_FIELD_INLINE=1`, `PERRY_TYPED_FEEDBACK=1`, and
+ `PERRY_VERIFY_TYPED_INTACT=1` modes
+- full gap driver: 387 parity passes, 0 compile failures, 0 crashes; the seven
+ reported mismatches were all recognized by the driver as already
+ known/triaged, and it finished with `Gap gate OK`
+- the new `test_gap_6812_object_write_loop_generalization` passed in that full
+ release-driver run; its final expanded inherited-setter/Proxy corpus also
+ passed a focused release-driver rerun
diff --git a/benchmarks/object-write-6812/run_matrix.py b/benchmarks/object-write-6812/run_matrix.py
new file mode 100644
index 000000000..24237498f
--- /dev/null
+++ b/benchmarks/object-write-6812/run_matrix.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python3
+"""Run the #6812 object-write matrix with alternating Node/Perry samples."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+import statistics
+import subprocess
+import sys
+from pathlib import Path
+
+
+CASES = [
+ ("Key", "o.x", "key_dot"),
+ ("Key", 'o["x"]', "key_literal"),
+ ("Key", "stable o[k]", "key_stable_dynamic"),
+ ("Key", "alternating dynamic keys", "key_alternating_dynamic"),
+ ("RHS", "numeric scalar", "rhs_numeric"),
+ ("RHS", "pointer-capable value", "rhs_pointer"),
+ ("RHS", "allocating literal", "rhs_allocating"),
+ ("RHS", "function call", "rhs_call"),
+ ("Receiver shapes", "monomorphic", "shape_monomorphic"),
+ ("Receiver shapes", "2-shape", "shape_two"),
+ ("Receiver shapes", "4-shape", "shape_four"),
+ ("Receiver shapes", "transition before loop", "shape_transition_before_loop"),
+ ("Fields/iteration", "1", "fields_one"),
+ ("Fields/iteration", "2", "fields_two"),
+ ("Fields/iteration", "4", "fields_four"),
+ ("Fields/iteration", "8", "fields_eight"),
+ ("Loop form", "single counted loop", "loop_single_counted"),
+ ("Loop form", "current nested loop", "loop_current_nested"),
+ ("Loop form", "stable local bounds", "loop_stable_local_bounds"),
+ ("Loop form", "non-zero inner start", "loop_nonzero_start"),
+ ("Storage", "inline existing slot", "storage_inline"),
+ ("Storage", "wide/overflow object", "storage_overflow"),
+ ("Receiver kind", "anonymous object", "receiver_anonymous"),
+ ("Receiver kind", "class instance", "receiver_class_instance"),
+ ("Receiver kind", "class-id-zero plain object", "receiver_class_id_zero"),
+]
+
+RESULT_RE = re.compile(
+ r"cell (?P\S+) ms (?P\d+) writes (?P\d+) "
+ r"sink (?P-?(?:\d+(?:\.\d+)?|NaN|Infinity))"
+)
+
+
+def run_once(command: list[str]) -> dict[str, int | float | str]:
+ completed = subprocess.run(
+ command,
+ check=True,
+ capture_output=True,
+ text=True,
+ )
+ match = RESULT_RE.search(completed.stdout)
+ if match is None:
+ raise RuntimeError(
+ f"missing matrix result in {command!r}\n"
+ f"stdout:\n{completed.stdout}\nstderr:\n{completed.stderr}"
+ )
+ result: dict[str, int | float | str] = {
+ "case": match.group("case"),
+ "ms": int(match.group("ms")),
+ "writes": int(match.group("writes")),
+ "sink": match.group("sink"),
+ }
+ return result
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--perry", type=Path, required=True)
+ parser.add_argument("--node", default="node")
+ parser.add_argument("--samples", type=int, default=15)
+ parser.add_argument("--warmups", type=int, default=1)
+ parser.add_argument(
+ "--cases",
+ help="comma-separated matrix case ids (default: all)",
+ )
+ args = parser.parse_args()
+
+ if args.samples < 1 or args.warmups < 0:
+ parser.error("--samples must be positive and --warmups non-negative")
+ if not args.perry.is_file():
+ parser.error(f"Perry executable does not exist: {args.perry}")
+
+ source = Path(__file__).with_name("matrix.ts").resolve()
+ selected = None if args.cases is None else set(args.cases.split(","))
+ cases = [case for case in CASES if selected is None or case[2] in selected]
+ unknown = set() if selected is None else selected - {case[2] for case in cases}
+ if unknown:
+ parser.error(f"unknown case ids: {', '.join(sorted(unknown))}")
+
+ report = []
+ for dimension, cell, case_id in cases:
+ node_command = [
+ args.node,
+ "--experimental-strip-types",
+ str(source),
+ case_id,
+ ]
+ perry_command = [str(args.perry.resolve()), case_id]
+
+ for _ in range(args.warmups):
+ node_warm = run_once(node_command)
+ perry_warm = run_once(perry_command)
+ if (node_warm["writes"], node_warm["sink"]) != (
+ perry_warm["writes"],
+ perry_warm["sink"],
+ ):
+ raise RuntimeError(
+ f"warmup output mismatch for {case_id}: "
+ f"Node={node_warm}, Perry={perry_warm}"
+ )
+
+ node_samples: list[int] = []
+ perry_samples: list[int] = []
+ expected_output = None
+ for _ in range(args.samples):
+ node_result = run_once(node_command)
+ perry_result = run_once(perry_command)
+ node_output = (node_result["writes"], node_result["sink"])
+ perry_output = (perry_result["writes"], perry_result["sink"])
+ if node_output != perry_output:
+ raise RuntimeError(
+ f"output mismatch for {case_id}: "
+ f"Node={node_result}, Perry={perry_result}"
+ )
+ if expected_output is not None and expected_output != node_output:
+ raise RuntimeError(
+ f"non-deterministic checksum for {case_id}: "
+ f"expected={expected_output}, observed={node_output}"
+ )
+ expected_output = node_output
+ node_samples.append(int(node_result["ms"]))
+ perry_samples.append(int(perry_result["ms"]))
+
+ report.append(
+ {
+ "dimension": dimension,
+ "cell": cell,
+ "id": case_id,
+ "writes": expected_output[0],
+ "sink": expected_output[1],
+ "node_ms": node_samples,
+ "node_median_ms": statistics.median(node_samples),
+ "perry_ms": perry_samples,
+ "perry_median_ms": statistics.median(perry_samples),
+ }
+ )
+ print(
+ f"[{case_id}] Node {statistics.median(node_samples)} ms, "
+ f"Perry {statistics.median(perry_samples)} ms",
+ file=sys.stderr,
+ flush=True,
+ )
+
+ print(json.dumps(report, indent=2))
+ print()
+ print("| Dimension | Cell | Writes | Node median | Perry median |")
+ print("|---|---|---:|---:|---:|")
+ for row in report:
+ print(
+ f"| {row['dimension']} | {row['cell']} | {row['writes']} | "
+ f"{row['node_median_ms']} ms | {row['perry_median_ms']} ms |"
+ )
+ print()
+ print("")
+ print("Raw alternating samples and checksums
")
+ print()
+ print("| Cell | Checksum | Node ms | Perry ms |")
+ print("|---|---:|---|---|")
+ for row in report:
+ node_samples = ", ".join(str(sample) for sample in row["node_ms"])
+ perry_samples = ", ".join(str(sample) for sample in row["perry_ms"])
+ print(
+ f"| `{row['id']}` | `{row['sink']}` | "
+ f"`[{node_samples}]` | `[{perry_samples}]` |"
+ )
+ print()
+ print(" ")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/changelog.d/6812-immutable-string-key-loop-pic.md b/changelog.d/6812-immutable-string-key-loop-pic.md
new file mode 100644
index 000000000..8a97ab9d8
--- /dev/null
+++ b/changelog.d/6812-immutable-string-key-loop-pic.md
@@ -0,0 +1,7 @@
+## Immutable string-key object-write fast path
+
+Object-write loop specialization now recognizes an immutable local initialized
+from a string literal (for example, `const key = "x"`) as a static property
+key. It reuses the guarded write PIC and bounded loop fast path without
+retaining a movable runtime string pointer. Mutable or computed keys continue
+to use the fully generic property-write path.
diff --git a/changelog.d/6812-object-write-loop-generalization.md b/changelog.d/6812-object-write-loop-generalization.md
new file mode 100644
index 000000000..af0645cd4
--- /dev/null
+++ b/changelog.d/6812-object-write-loop-generalization.md
@@ -0,0 +1 @@
+perf(codegen, runtime): generalize the once-validated, call/GC-free numeric object-array write clone from exactly two fields to a bounded one-to-four static fields. The compiler proves finite numeric intermediates, and the preflight resolves live string-pool keys plus verified raw-f64/ordinary JSValue typed slots into a fixed packed descriptor before the first store. Mixed layouts, descriptors, diagnostic modes, dynamic/safepointing expressions, overflow storage, and larger bodies retain the complete semantic fallback.
diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs
index aad31ed27..8f4b32526 100644
--- a/crates/perry-codegen/src/codegen/closure.rs
+++ b/crates/perry-codegen/src/codegen/closure.rs
@@ -755,6 +755,7 @@ pub(super) fn compile_closure(
native_facts: &native_facts,
locals,
local_types,
+ const_string_locals: std::collections::HashMap::new(),
current_block: 0,
discard_expr_value: false,
func_names,
diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs
index 0d8c725a9..226d19494 100644
--- a/crates/perry-codegen/src/codegen/entry.rs
+++ b/crates/perry-codegen/src/codegen/entry.rs
@@ -675,6 +675,7 @@ pub(super) fn compile_module_entry(
native_facts: &main_native_facts,
locals: HashMap::new(),
local_types: init_local_types,
+ const_string_locals: HashMap::new(),
current_block: 0,
discard_expr_value: false,
func_names,
@@ -1276,6 +1277,7 @@ pub(super) fn compile_module_entry(
native_facts: &init_native_facts,
locals: HashMap::new(),
local_types: HashMap::new(),
+ const_string_locals: HashMap::new(),
current_block: 0,
discard_expr_value: false,
func_names,
diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs
index 0caf21849..0d5b84c3c 100644
--- a/crates/perry-codegen/src/codegen/function.rs
+++ b/crates/perry-codegen/src/codegen/function.rs
@@ -470,6 +470,7 @@ pub(super) fn compile_function(
native_facts: &native_facts,
locals,
local_types,
+ const_string_locals: std::collections::HashMap::new(),
current_block: 0,
discard_expr_value: false,
func_names,
diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs
index 5de56e038..2dc75f72f 100644
--- a/crates/perry-codegen/src/codegen/method.rs
+++ b/crates/perry-codegen/src/codegen/method.rs
@@ -388,6 +388,7 @@ pub(super) fn compile_method(
native_facts: &native_facts,
locals,
local_types,
+ const_string_locals: std::collections::HashMap::new(),
current_block: 0,
discard_expr_value: false,
func_names,
@@ -1383,6 +1384,7 @@ pub(super) fn compile_static_method(
native_facts: &native_facts,
locals,
local_types,
+ const_string_locals: std::collections::HashMap::new(),
current_block: 0,
discard_expr_value: false,
func_names,
diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs
index 3175129c6..e02c9bf66 100644
--- a/crates/perry-codegen/src/expr/mod.rs
+++ b/crates/perry-codegen/src/expr/mod.rs
@@ -176,6 +176,11 @@ pub(crate) struct FnCtx<'a> {
/// tracking" extension). Populated from function params and `Stmt::Let`
/// declarations as they're lowered.
pub local_types: std::collections::HashMap,
+ /// Immutable locals whose initializer is a string literal. These values
+ /// can be resolved to the module's interned string global at a use site;
+ /// unlike a runtime dynamic-key cache, this does not retain a movable
+ /// string pointer in generated cache state.
+ pub const_string_locals: std::collections::HashMap,
/// Index into `func.blocks()` pointing at the block currently receiving
/// instructions. Lowering fns update this when control flow splits.
pub current_block: usize,
diff --git a/crates/perry-codegen/src/expr/proxy_reflect.rs b/crates/perry-codegen/src/expr/proxy_reflect.rs
index b6de695c5..bb9251f5b 100644
--- a/crates/perry-codegen/src/expr/proxy_reflect.rs
+++ b/crates/perry-codegen/src/expr/proxy_reflect.rs
@@ -236,7 +236,7 @@ fn lower_put_value_static_write_ic(
receiver: &Expr,
strict: bool,
) -> Result