Skip to content

perf: generalize object-write loops and stable dynamic keys#6821

Closed
proggeramlug wants to merge 3 commits into
PerryTS:mainfrom
proggeramlug:perf/6812-stable-dynamic-key-pic
Closed

perf: generalize object-write loops and stable dynamic keys#6821
proggeramlug wants to merge 3 commits into
PerryTS:mainfrom
proggeramlug:perf/6812-stable-dynamic-key-pic

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Generalize the guarded object-write loop fast path from exactly two fields to bounded 1–4 field writes.
  • Add conservative runtime preflight checks for receiver shape, storage bounds, and value/layout safety.
  • Recognize immutable locals initialized from string literals (for example, const key = "x") as static keys, so stable dynamic-key writes reuse the guarded PIC and loop fast path.
  • Keep mutable, computed, and alternating keys on the complete generic property-write path.

Validation

  • cargo fmt --all -- --check
  • cargo test -p perry-codegen --test native_proof_regressions — 250 passed
  • Node/Perry matrix parity verified for all measured cells.
  • key_stable_dynamic: Node 138 ms, Perry 124 ms in the follow-up measurement.
  • key_alternating_dynamic remains the conservative fallback control case.

Closes #6812

Summary by CodeRabbit

  • New Features
    • Improved object property-write loop specialization for faster execution across one to four fields.
    • Added optimized handling when property keys come from immutable local string literals.
  • Bug Fixes
    • Expanded correctness checks for typed numeric slots and invalid/sparse receivers, with proper fallback for dynamic keys and non-finite values.
  • Tests
    • Added new gap and regression tests covering optimized fast paths, fallback/throws, accessors, proxies, and dynamic assignments.
  • Documentation
    • Added detailed benchmark docs, matrices, and collected results for the object-write generalization.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 243e61cd-c5de-44db-9094-1ac085edd92d

📥 Commits

Reviewing files that changed from the base of the PR and between 871679c and 1677a10.

📒 Files selected for processing (1)
  • scripts/check_file_size.sh

📝 Walkthrough

Walkthrough

The PR generalizes guarded numeric object-write loops from two fields to one through four fields, adds immutable string-local static-key support, strengthens runtime preflight validation, and adds regression tests plus benchmark tooling and measurements.

Changes

Object-write optimization

Layer / File(s) Summary
Immutable string-key PIC support
crates/perry-codegen/src/expr/..., crates/perry-codegen/src/stmt/let_stmt.rs, crates/perry-codegen/src/codegen/..., crates/perry-codegen/tests/native_proof_regressions.rs
Immutable locals initialized from string literals are tracked and can use the static write PIC; mutable or computed keys retain the generic path.
One-to-four-field loop lowering
crates/perry-codegen/src/stmt/loops.rs, crates/perry-codegen/src/runtime_decls/objects.rs
Object-array write matching, finite-range proofs, guard invocation, packed-slot decoding, and fast stores now support up to four fields.
Runtime preflight and guard ABI
crates/perry-runtime/src/proxy/put_value.rs, crates/perry-runtime/src/gc/layout.rs, crates/perry-runtime/src/proxy.rs
The generalized guard validates keys, shared receiver layouts, slot bounds, typed descriptors, layout identity, and bounded field counts before returning packed slots.
Behavioral validation and benchmark evidence
test-files/test_gap_6812_object_write_loop_generalization.ts, benchmarks/object-write-6812/*, changelog.d/*, scripts/check_file_size.sh
Tests cover accepted and rejected object-write cases, while benchmark scripts and documents record parity, timing, path classification, validation results, and the file-size allowlist update.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Compiler
  participant RuntimeGuard
  participant ObjectArray
  participant FastLoop
  Compiler->>RuntimeGuard: pass up to four property keys and receiver count
  RuntimeGuard->>ObjectArray: validate shared shape, slots, and typed layout
  ObjectArray-->>RuntimeGuard: return packed slot indexes
  RuntimeGuard-->>Compiler: accept or reject fast path
  Compiler->>FastLoop: emit numeric stores for validated fields
Loading

Possibly related PRs

  • PerryTS/perry#6811: Introduced the related two-field object-write guard and versioned loop fast path extended here.
  • PerryTS/perry#6822: Modifies the same immutable-key, generalized guard, codegen, and regression-test pipeline.

Suggested labels: ready

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the summary and validation, but it omits required template sections like Changes and Test plan. Add the missing Changes and Test plan sections, and include a brief related-issue summary plus any screenshots/output if relevant.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: generalized object-write loops and stable dynamic keys.
Linked Issues check ✅ Passed The changes match #6812’s goals by extending the object-write fast path, adding safety checks, tests, benchmarks, and stable dynamic-key support.
Out of Scope Changes check ✅ Passed The added files and edits appear directly related to the benchmark, runtime, codegen, tests, and docs needed for #6812.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/perry-codegen/src/expr/proxy_reflect.rs (1)

442-448: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Static-key resolution logic duplicated three times. The same "resolve Expr::String literal or immutable Expr::LocalGet via ctx.const_string_locals" rule is implemented independently in three places instead of sharing one helper; a future extension to the recognized key shapes could easily miss one or two copies and silently desync the optimization coverage between the PIC write path and the loop fast paths.

  • crates/perry-codegen/src/expr/proxy_reflect.rs#L442-L448: keep static_write_key here (or move to a shared module like type_analysis) and mark it pub(crate) so loops.rs can reuse it directly.
  • crates/perry-codegen/src/stmt/loops.rs#L2094-L2098: replace the inline match in match_store with a call to the shared static_write_key(ctx, key.as_ref()).
  • crates/perry-codegen/src/stmt/loops.rs#L2564-L2568: replace the inline match in match_class_field_versioned_loop with the same shared helper call.
♻️ Sketch: share one helper
-fn static_write_key(ctx: &FnCtx<'_>, key: &Expr) -> Option<String> {
+pub(crate) fn static_write_key(ctx: &FnCtx<'_>, key: &Expr) -> Option<String> {
     match key {
         Expr::String(property) => Some(property.clone()),
         Expr::LocalGet(id) => ctx.const_string_locals.get(id).cloned(),
         _ => None,
     }
 }
-        let property = match key.as_ref() {
-            Expr::String(property) => property.clone(),
-            Expr::LocalGet(id) => ctx.const_string_locals.get(id).cloned()?,
-            _ => return None,
-        };
+        let property = crate::expr::proxy_reflect::static_write_key(ctx, key.as_ref())?;

(same replacement at the match_class_field_versioned_loop site)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/expr/proxy_reflect.rs` around lines 442 - 448,
Centralize static-key resolution in
crates/perry-codegen/src/expr/proxy_reflect.rs:442-448 by making
static_write_key accessible as pub(crate), then reuse it from both inline-match
sites in crates/perry-codegen/src/stmt/loops.rs:2094-2098 (match_store) and
2564-2568 (match_class_field_versioned_loop), passing key.as_ref() and
preserving the existing String/const_string_locals behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-codegen/src/stmt/let_stmt.rs`:
- Around line 1531-1535: Move the const_string_locals insertion logic from its
current position to the start of the let-statement handling in the relevant
function, before module-global and boxed-variable early returns. Apply it for
every non-mutable local whose initializer is a string literal, while preserving
the existing id/value insertion behavior.

---

Nitpick comments:
In `@crates/perry-codegen/src/expr/proxy_reflect.rs`:
- Around line 442-448: Centralize static-key resolution in
crates/perry-codegen/src/expr/proxy_reflect.rs:442-448 by making
static_write_key accessible as pub(crate), then reuse it from both inline-match
sites in crates/perry-codegen/src/stmt/loops.rs:2094-2098 (match_store) and
2564-2568 (match_class_field_versioned_loop), passing key.as_ref() and
preserving the existing String/const_string_locals behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0391fd8f-cd89-42ab-adb1-746d67717cab

📥 Commits

Reviewing files that changed from the base of the PR and between 2a68962 and 871679c.

📒 Files selected for processing (23)
  • benchmarks/object-write-6812/README.md
  • benchmarks/object-write-6812/baseline.md
  • benchmarks/object-write-6812/canonical.ts
  • benchmarks/object-write-6812/followup-stable-key.md
  • benchmarks/object-write-6812/matrix.ts
  • benchmarks/object-write-6812/results.md
  • benchmarks/object-write-6812/run_matrix.py
  • changelog.d/6812-immutable-string-key-loop-pic.md
  • changelog.d/6812-object-write-loop-generalization.md
  • crates/perry-codegen/src/codegen/closure.rs
  • crates/perry-codegen/src/codegen/entry.rs
  • crates/perry-codegen/src/codegen/function.rs
  • crates/perry-codegen/src/codegen/method.rs
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/expr/proxy_reflect.rs
  • crates/perry-codegen/src/runtime_decls/objects.rs
  • crates/perry-codegen/src/stmt/let_stmt.rs
  • crates/perry-codegen/src/stmt/loops.rs
  • crates/perry-codegen/tests/native_proof_regressions.rs
  • crates/perry-runtime/src/gc/layout.rs
  • crates/perry-runtime/src/proxy.rs
  • crates/perry-runtime/src/proxy/put_value.rs
  • test-files/test_gap_6812_object_write_loop_generalization.ts

Comment on lines +1531 to +1535
if !mutable {
if let Some(perry_hir::Expr::String(value)) = init {
ctx.const_string_locals.insert(id, value.clone());
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

const_string_locals never populated for module-level or boxed immutable string locals.

This insertion sits after the module-globals early return (Line 1004) and the boxed-var early returns (Lines 1061, 1108). A top-level const key = "x"; referenced inside a hot loop resolves through the module-globals branch and returns before ever reaching this block, so it's silently excluded from the new static-key fast path (fallback is still correct, just unoptimized). Move the check earlier so it runs unconditionally for every non-mutable string-literal local.

🔧 Proposed fix: hoist the check before any early return
     ctx.local_id_to_name.insert(id, name.to_string());
     if !mutable {
         if let Some(init_expr) = init {
+            if let perry_hir::Expr::String(value) = init_expr {
+                ctx.const_string_locals.insert(id, value.clone());
+            }
             if let Some(props) = crate::lower_call::extract_options_fields(ctx, init_expr) {
                 ctx.option_object_locals.insert(id, props);
             }
@@
     }
-    if !mutable {
-        if let Some(perry_hir::Expr::String(value)) = init {
-            ctx.const_string_locals.insert(id, value.clone());
-        }
-    }
     Ok(())
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/stmt/let_stmt.rs` around lines 1531 - 1535, Move the
const_string_locals insertion logic from its current position to the start of
the let-statement handling in the relevant function, before module-global and
boxed-variable early returns. Apply it for every non-mutable local whose
initializer is a string literal, while preserving the existing id/value
insertion behavior.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Superseded by merged PR #6822, which contains these commits plus the numeric-bound follow-up. Closing this duplicate PR to avoid a redundant/conflicting merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: generalize guarded object-write fast paths beyond the #6811 two-field loop

1 participant