Skip to content

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

Open
proggeramlug wants to merge 1 commit into
mainfrom
fix/string-method-nonstring-receiver
Open

fix(codegen): gate String-method dispatch on string receiver — user method like internals.trim(v,s) (#5271)#5272
proggeramlug wants to merge 1 commit 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 on objects with methods sharing names with String.prototype builtins (e.g., trim, split, slice). User-defined methods are now correctly called instead of incorrectly attempting String method dispatch on non-string receivers.

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.
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 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
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 and specifically describes the main fix: gating String-method dispatch on string receiver types, using the concrete user method example (internals.trim) and issue reference (#5271).
Description check ✅ Passed The PR description covers what was fixed (issue #5271), why it mattered (joi compilation failure), the solution (arity-aware gate), and verification (regression testing and new test case).
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.

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