Skip to content

fix(codegen): gate String-method dispatch on string receiver — user method like internals.trim(v,s) (#5271)#5272

Merged
proggeramlug merged 2 commits into
mainfrom
fix/string-method-nonstring-receiver
Jun 17, 2026
Merged

fix(codegen): gate String-method dispatch on string receiver — user method like internals.trim(v,s) (#5271)#5272
proggeramlug merged 2 commits into
mainfrom
fix/string-method-nonstring-receiver

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Fixes #5271. internals.trim(value, schema) (a user method on a non-string receiver) was mis-lowered to String.prototype.trim and failed the compile on arity (String.trim takes no args, got 2), blocking joi.

Fix: added an arity-aware gate (string_only_method_arity_ok) in lower_call/property_get.rs — a builtin-named method on a receiver that is NOT provably a string, whose arg count can't match the String builtin's signature, falls through to normal dynamic method dispatch instead of forcing String.prototype.<m>. Genuine string receivers still fast-path.

Verified: internals.trim("a","b")a:b; string regressions (" hi ".trim(), split, toLowerCase, repeat, padStart) match Node; new test issue_5271_string_method_nonstring_receiver. joi's String.trim error is gone (it now advances to a separate regex-incompatibility wall — Rust regex crate lacks lookahead).

No version/CHANGELOG/Cargo.lock edits (for maintainer to fold in at merge).

Summary by CodeRabbit

  • Bug Fixes
    • Fixed method dispatch when user-defined methods share names with String.prototype builtins (e.g., trim, split, slice) so non-string receivers no longer incorrectly take the String fast path, avoiding arity errors and incorrect results.
  • Tests
    • Added regression tests for the non-string receiver String.prototype name-collision scenario, including matching-builtin-arity cases and coverage for true string receivers and any values that are actually strings.

@coderabbitai

coderabbitai Bot commented Jun 16, 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: af135e0f-315c-4d23-836b-cf441c8f9c64

📥 Commits

Reviewing files that changed from the base of the PR and between 131ca4a and 66ca391.

📒 Files selected for processing (8)
  • 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/lower_call/property_get.rs
  • crates/perry-codegen/src/stmt/let_stmt.rs
  • crates/perry/tests/issue_5271_string_method_nonstring_receiver.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/perry-codegen/src/codegen/closure.rs
🚧 Files skipped from review as they are similar to previous changes (6)
  • 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/stmt/let_stmt.rs
  • crates/perry/tests/issue_5271_string_method_nonstring_receiver.rs

📝 Walkthrough

Walkthrough

Fixes issue #5271 by adding an object_literal_locals field to FnCtx that tracks locals initialized from object literals. let_stmt detects and records such locals; property_get gates the String.prototype fast-path on both arity plausibility and receiver shape, falling through to runtime dispatch for mismatches. Regression tests cover all affected scenarios.

Changes

String.prototype fast-path fix for non-string receivers

Layer / File(s) Summary
FnCtx field definition and initialization
crates/perry-codegen/src/expr/mod.rs, 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
FnCtx gains object_literal_locals: HashSet<u32> with documentation explaining its purpose; all five FnCtx construction sites initialize the field to HashSet::new().
Object literal detection in let_stmt
crates/perry-codegen/src/stmt/let_stmt.rs
Adds is_object_literal_init to recognise both plain Expr::Object and IIFE-lowered object literals; lower_let records matching local IDs into ctx.object_literal_locals.
Arity helper and String fast-path gating
crates/perry-codegen/src/lower_call/property_get.rs
string_only_method_arity_ok encodes valid arity ranges per String.prototype builtin; the is_string_only_method fallback now checks arity plausibility and object-literal receiver shape before taking the fast path, otherwise emitting a js_native_call_method runtime dispatch.
Regression tests
crates/perry/tests/issue_5271_string_method_nonstring_receiver.rs
Test harness and three end-to-end tests compile and run generated TypeScript: arity-mismatch user trim calls user method, same-arity user methods resolve correctly, and genuine string receivers continue using String.prototype semantics.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐇 A rabbit once found a String path astray,
Calling trim on an object — what a dismay!
With arity checks and a literal-locals set,
The fast path now asks: "Is this string? Are you set?"
Wrong dispatches vanished, the tests all turned green —
The cleanest of hops that this warren has seen! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding an arity gate to prevent non-string receivers from incorrectly dispatching to String.prototype methods.
Description check ✅ Passed The description is comprehensive and covers the issue, fix, verification, and testing. However, no explicit test plan section is filled in the template checklist format.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/string-method-nonstring-receiver

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

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/perry-codegen/src/stmt/let_stmt.rs (1)

66-79: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Object-literal receiver tracking is currently too narrow.

ctx.object_literal_locals is only updated inside the !mutable branch and only for direct object-literal-shaped initializers. That misses common shapes like let o = { ... } and aliasing (const b = a), so a later b.trim()/o.split(...) can still be claimed by the String fast-path when arity matches.

Suggested patch shape
-    if !mutable {
-        if let Some(init_expr) = init {
-            ...
-            if is_object_literal_init(init_expr) {
-                ctx.object_literal_locals.insert(id);
-            }
-        }
-    }
+    if let Some(init_expr) = init {
+        if !mutable {
+            if let Some(props) = crate::lower_call::extract_options_fields(ctx, init_expr) {
+                ctx.option_object_locals.insert(id, props);
+            }
+        }
+        if is_object_literal_init(init_expr) {
+            ctx.object_literal_locals.insert(id);
+        } else if let perry_hir::Expr::LocalGet(src_id) = init_expr {
+            if ctx.object_literal_locals.contains(src_id) {
+                ctx.object_literal_locals.insert(id);
+            }
+        }
+    }
🤖 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 66 - 79, The
object-literal receiver tracking in the let_stmt.rs file is too restrictive
because it only updates ctx.object_literal_locals inside the !mutable branch.
This misses cases where mutable bindings (like let o = { ... }) should also
track object-literal locals, causing later method calls on those variables to be
incorrectly claimed by the String fast-path. Move the object-literal tracking
logic (the is_object_literal_init check and ctx.object_literal_locals.insert
call) outside of the !mutable condition so that both mutable and immutable
variable declarations with object-literal initializers are properly tracked,
ensuring that method calls like o.trim() or b.split() resolve to the object's
own method rather than the static String-method fast path.
🧹 Nitpick comments (1)
crates/perry/tests/issue_5271_string_method_nonstring_receiver.rs (1)

78-133: ⚡ Quick win

Add regressions for mutable and inline object-literal receivers.

Current tests are strong, but they don’t pin let o = { ... }; o.trim() or inline method-bearing object literals (IIFE-lowered shape), which are the remaining collision-prone receiver forms.

🤖 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_5271_string_method_nonstring_receiver.rs` around
lines 78 - 133, Add two new regression test functions following the same pattern
as the existing tests in this file. First, add a test for mutable object-literal
receivers (using let o = { trim() { ... } }; o.trim() style) to verify that
user-defined methods on mutable object variables resolve correctly and don't
incorrectly dispatch to String builtins. Second, add a test for inline
object-literal receivers passed directly or in IIFE-lowered forms (like ({
trim() { ... } }).trim()) to verify the same collision-avoidance behavior. Both
tests should use compile_and_run and assert that user-defined methods are
called, not the String builtins, to ensure these edge-case receiver forms don't
regress.
🤖 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_call/property_get.rs`:
- Around line 459-466: The receiver_is_object_literal check in the condition
block is incomplete because it only detects LocalGet references to object
literal locals and direct Object expressions, but misses IIFE-lowered object
literals. Extend the receiver_is_object_literal guard to also detect the
object-building IIFE pattern (a Call expression with a Function body that
returns an object). This will prevent incorrectly routing such patterns to the
String fast-path when they should not be treated as simple object literals.

---

Outside diff comments:
In `@crates/perry-codegen/src/stmt/let_stmt.rs`:
- Around line 66-79: The object-literal receiver tracking in the let_stmt.rs
file is too restrictive because it only updates ctx.object_literal_locals inside
the !mutable branch. This misses cases where mutable bindings (like let o = {
... }) should also track object-literal locals, causing later method calls on
those variables to be incorrectly claimed by the String fast-path. Move the
object-literal tracking logic (the is_object_literal_init check and
ctx.object_literal_locals.insert call) outside of the !mutable condition so that
both mutable and immutable variable declarations with object-literal
initializers are properly tracked, ensuring that method calls like o.trim() or
b.split() resolve to the object's own method rather than the static
String-method fast path.

---

Nitpick comments:
In `@crates/perry/tests/issue_5271_string_method_nonstring_receiver.rs`:
- Around line 78-133: Add two new regression test functions following the same
pattern as the existing tests in this file. First, add a test for mutable
object-literal receivers (using let o = { trim() { ... } }; o.trim() style) to
verify that user-defined methods on mutable object variables resolve correctly
and don't incorrectly dispatch to String builtins. Second, add a test for inline
object-literal receivers passed directly or in IIFE-lowered forms (like ({
trim() { ... } }).trim()) to verify the same collision-avoidance behavior. Both
tests should use compile_and_run and assert that user-defined methods are
called, not the String builtins, to ensure these edge-case receiver forms don't
regress.
🪄 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: c10c3d9a-e786-4745-a3fc-47f287b8c1db

📥 Commits

Reviewing files that changed from the base of the PR and between d46feff and 16ef2d0.

📒 Files selected for processing (8)
  • 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/lower_call/property_get.rs
  • crates/perry-codegen/src/stmt/let_stmt.rs
  • crates/perry/tests/issue_5271_string_method_nonstring_receiver.rs

Comment on lines +459 to 466
let receiver_is_object_literal = matches!(
&**object,
Expr::LocalGet(id) if ctx.object_literal_locals.contains(id)
) || matches!(&**object, Expr::Object(_));
if is_string_only_method
&& string_only_method_arity_ok(property, args.len())
&& !receiver_is_object_literal
&& !is_array_expr(ctx, object)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Receiver-shape guard misses IIFE-lowered object literals.

receiver_is_object_literal does not include the object-building IIFE shape, so inline method-bearing literals can still hit the String fast-path for matching-arity builtin names.

Suggested guard extension
 let receiver_is_object_literal = matches!(
     &**object,
     Expr::LocalGet(id) if ctx.object_literal_locals.contains(id)
-) || matches!(&**object, Expr::Object(_));
+) || matches!(&**object, Expr::Object(_))
+  || matches!(
+      &**object,
+      Expr::Call { callee, args, .. }
+          if matches!(args.first(), Some(Expr::Object(_)))
+              && matches!(
+                  callee.as_ref(),
+                  Expr::Closure { params, .. }
+                      if params.first().map_or(false, |p| p.name == "__perry_obj_iife")
+              )
+  );
📝 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.

Suggested change
let receiver_is_object_literal = matches!(
&**object,
Expr::LocalGet(id) if ctx.object_literal_locals.contains(id)
) || matches!(&**object, Expr::Object(_));
if is_string_only_method
&& string_only_method_arity_ok(property, args.len())
&& !receiver_is_object_literal
&& !is_array_expr(ctx, object)
let receiver_is_object_literal = matches!(
&**object,
Expr::LocalGet(id) if ctx.object_literal_locals.contains(id)
) || matches!(&**object, Expr::Object(_))
|| matches!(
&**object,
Expr::Call { callee, args, .. }
if matches!(args.first(), Some(Expr::Object(_)))
&& matches!(
callee.as_ref(),
Expr::Closure { params, .. }
if params.first().map_or(false, |p| p.name == "__perry_obj_iife")
)
);
if is_string_only_method
&& string_only_method_arity_ok(property, args.len())
&& !receiver_is_object_literal
&& !is_array_expr(ctx, object)
🤖 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_call/property_get.rs` around lines 459 - 466,
The receiver_is_object_literal check in the condition block is incomplete
because it only detects LocalGet references to object literal locals and direct
Object expressions, but misses IIFE-lowered object literals. Extend the
receiver_is_object_literal guard to also detect the object-building IIFE pattern
(a Call expression with a Function body that returns an object). This will
prevent incorrectly routing such patterns to the String fast-path when they
should not be treated as simple object literals.

Ralph Küpper added 2 commits June 17, 2026 05:09
internals.trim(value, schema) on a non-string receiver was mis-lowered to
String.prototype.trim and failed compile on arity. Thread receiver-type info so
String-method fast-path only fires for string receivers; object/any route to
dynamic method dispatch.
@proggeramlug proggeramlug force-pushed the fix/string-method-nonstring-receiver branch from 131ca4a to 66ca391 Compare June 17, 2026 03:10
@proggeramlug proggeramlug merged commit 3730833 into main Jun 17, 2026
14 of 15 checks passed
@proggeramlug proggeramlug deleted the fix/string-method-nonstring-receiver branch June 17, 2026 03:52
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.

codegen: user method named like a String builtin (internals.trim(value, schema)) mis-lowered to String.prototype.<m> on non-string receivers — joi

1 participant