fix(codegen): guard declared string self-append operator (#7841) - #7881
Conversation
📝 WalkthroughWalkthroughDeclared-string ChangesDeclared-string self-append
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant DeclaredStringLocal
participant SelfAppendLowering
participant RuntimeHelpers
DeclaredStringLocal->>SelfAppendLowering: load destination and RHS
SelfAppendLowering->>SelfAppendLowering: inspect runtime NaN-box tags
SelfAppendLowering->>RuntimeHelpers: call string append or dynamic string-or-number addition
RuntimeHelpers-->>DeclaredStringLocal: store self-append result
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
crates/perry-codegen/tests/shadow_slot_hygiene.rs (1)
1226-1233: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBind the assertions to the dispatch, not to call presence.
Both arms are always emitted for a non-string rhs, so both
containschecks pass even if the tag test is wrong or absent. Reuse the existingblock_def_offsethelper to assert thatjs_string_appendappears inside thestrapp.appendblock and thatjs_dynamic_string_or_number_addappears inside thestrapp.dynamicblock. Also assert that the destination tag comparison against the heap-string tag exists.🤖 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/tests/shadow_slot_hygiene.rs` around lines 1226 - 1233, Update the assertions in the shadow-slot hygiene test to validate dispatch placement rather than global call presence. Reuse block_def_offset to assert js_string_append is within strapp.append and js_dynamic_string_or_number_add is within strapp.dynamic, and add an assertion that the destination tag comparison against the heap-string tag is emitted.
🤖 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/lower_string_concat.rs`:
- Around line 195-202: The lhs snapshot is lost when rhs evaluation mutates the
same local without allocation. In
crates/perry-codegen/src/lower_string_concat.rs:195-202, root the destination
unconditionally on the non-string path and use group.reread_emitted(ctx,
lhs_root) instead of reloading slot; apply the same change in the proven-string
SSO arm at crates/perry-codegen/src/lower_string_concat.rs:295-299, and add a
regression covering a heap-string destination with an rhs assignment to that
same local.
In `@crates/perry/tests/issue_7841_declared_string_self_append.rs`:
- Around line 25-35: Update the compiler environment setup in the test’s
command-building loop to set PERRY_CANONICAL_STR_LOCALS explicitly for both
canonical_strings values: enable canonical locals for the canonical arm and
disable them for the non-canonical arm, preventing inherited environment
settings from affecting either compilation.
---
Nitpick comments:
In `@crates/perry-codegen/tests/shadow_slot_hygiene.rs`:
- Around line 1226-1233: Update the assertions in the shadow-slot hygiene test
to validate dispatch placement rather than global call presence. Reuse
block_def_offset to assert js_string_append is within strapp.append and
js_dynamic_string_or_number_add is within strapp.dynamic, and add an assertion
that the destination tag comparison against the heap-string tag is emitted.
🪄 Autofix
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: 24081808-d82e-4288-81be-c96ed1220293
📒 Files selected for processing (4)
changelog.d/7881-declared-string-self-append.mdcrates/perry-codegen/src/lower_string_concat.rscrates/perry-codegen/tests/shadow_slot_hygiene.rscrates/perry/tests/issue_7841_declared_string_self_append.rs
| // The coercion can collect. If rhs evaluation needed an old-value | ||
| // root, re-read it; otherwise rhs was inert and the local slot is | ||
| // still the same value, so reload its GC-updated bits directly. | ||
| let lhs_after_coercion = if protect_lhs { | ||
| group.reread_emitted(ctx, lhs_root) | ||
| } else { | ||
| ctx.block().load(DOUBLE, slot) | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift
protect_lhs gates the wrong property, so both arms can lose the compound-assignment snapshot. operand_may_collect reports GC risk only. An rhs that stores to the same local without allocating leaves protect_lhs false, and both arms then reload the post-assignment value from slot instead of the snapshot taken before rhs evaluation. let s: string = "<long heap string>"; s += (s = "b") reaches the SSO arm and produces "bb". The supplied order += (order = "new") case does not catch this because both operands are SSO and route to dother, which uses the snapshot register.
crates/perry-codegen/src/lower_string_concat.rs#L195-L202: root the destination unconditionally on the non-string path and replace thectx.block().load(DOUBLE, slot)fallback withgroup.reread_emitted(ctx, lhs_root).crates/perry-codegen/src/lower_string_concat.rs#L295-L299: apply the same change on the proven-string SSO arm, and add a regression case with a heap-string destination and an rhs that assigns to the same local.
📍 Affects 1 file
crates/perry-codegen/src/lower_string_concat.rs#L195-L202(this comment)crates/perry-codegen/src/lower_string_concat.rs#L295-L299
🤖 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/lower_string_concat.rs` around lines 195 - 202, The
lhs snapshot is lost when rhs evaluation mutates the same local without
allocation. In crates/perry-codegen/src/lower_string_concat.rs:195-202, root the
destination unconditionally on the non-string path and use
group.reread_emitted(ctx, lhs_root) instead of reloading slot; apply the same
change in the proven-string SSO arm at
crates/perry-codegen/src/lower_string_concat.rs:295-299, and add a regression
covering a heap-string destination with an rhs assignment to that same local.
| let mut compiler = Command::new(perry_bin()); | ||
| compiler | ||
| .current_dir(dir) | ||
| .arg("compile") | ||
| .arg(&entry) | ||
| .arg("-o") | ||
| .arg(&output) | ||
| .arg("--no-cache"); | ||
| if !canonical_strings { | ||
| compiler.env("PERRY_CANONICAL_STR_LOCALS", "0"); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Set PERRY_CANONICAL_STR_LOCALS explicitly for both arms.
The canonical arm inherits the parent environment. If the environment already sets PERRY_CANONICAL_STR_LOCALS=0, both loop iterations compile the same configuration and the non-canonical lowering is the only one under test. The failure is silent.
🛡️ Proposed fix
- if !canonical_strings {
- compiler.env("PERRY_CANONICAL_STR_LOCALS", "0");
- }
+ compiler.env(
+ "PERRY_CANONICAL_STR_LOCALS",
+ if canonical_strings { "1" } else { "0" },
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let mut compiler = Command::new(perry_bin()); | |
| compiler | |
| .current_dir(dir) | |
| .arg("compile") | |
| .arg(&entry) | |
| .arg("-o") | |
| .arg(&output) | |
| .arg("--no-cache"); | |
| if !canonical_strings { | |
| compiler.env("PERRY_CANONICAL_STR_LOCALS", "0"); | |
| } | |
| let mut compiler = Command::new(perry_bin()); | |
| compiler | |
| .current_dir(dir) | |
| .arg("compile") | |
| .arg(&entry) | |
| .arg("-o") | |
| .arg(&output) | |
| .arg("--no-cache"); | |
| compiler.env( | |
| "PERRY_CANONICAL_STR_LOCALS", | |
| if canonical_strings { "1" } else { "0" }, | |
| ); |
🤖 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/tests/issue_7841_declared_string_self_append.rs` around lines 25
- 35, Update the compiler environment setup in the test’s command-building loop
to set PERRY_CANONICAL_STR_LOCALS explicitly for both canonical_strings values:
enable canonical locals for the canonical arm and disable them for the
non-canonical arm, preventing inherited environment settings from affecting
either compilation.
Summary
s += rhsstring-builder lowering choose concatenation versus numeric addition from the destination's runtime tag, not its erased TypeScript annotationvalueOf/toStringhooks run once in spec orderjs_string_appendfor real heap strings, including canonical and boxed string-local modesRegression coverage
The compiled-program test covers:
let c: string = (42 as any); c += 1(43, typenumber)valueOfonce,toStringzero times)PERRY_CANONICAL_STR_LOCALS=0Validation
cargo test -p perry-codegen— all test binaries passedcargo test -p perry --test issue_7841_declared_string_self_append declared_string_self_append_uses_runtime_values_and_preserves_order -- --exact421 string; fix43 numberbash scripts/check_file_size.shQuiet M1 mini, 21 paired alternating samples per arm, two compilers against one runtime:
s += "ab"s += "ab"s += numbers += numberAll benchmark outputs matched in every sample; mini load stayed between 1.51 and 1.73.
Closes #7841
Summary by CodeRabbit
Bug Fixes
+=behavior for string-annotated variables initialized with numeric values.Tests