Skip to content

fix(codegen): any-typed startsWith/endsWith/lastIndexOf must dynamic-dispatch, not the static String builtin#5666

Merged
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:fix/codegen-anytyped-string-method-dispatch
Jun 25, 2026
Merged

fix(codegen): any-typed startsWith/endsWith/lastIndexOf must dynamic-dispatch, not the static String builtin#5666
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:fix/codegen-anytyped-string-method-dispatch

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Problem

When the receiver of .startsWith() / .endsWith() / .lastIndexOf() is any-typed (its TS type was erased at bundle time), codegen routed the call to the static String.prototype builtin, which returns a boolean/number. But an any-typed receiver may be an object with its own same-named method that returns something else.

Minimal repro (zod):

const s = z.string().startsWith("./");   // perry: boolean (wrong)   node: ZodString
s.describe("x");                          // → "(boolean).describe is not a function"

ZodString.startsWith() returns a refined schema, not a boolean, so a chained .describe() / .optional() then threw on a primitive. This blocks compiled programs that build zod schemas with these string refinements.

Fix

Drop startsWith / endsWith / lastIndexOf from the any-typed string-method fallback in lower_call/property_get.rs, so they dynamic-dispatch: an object's own method wins, while a genuine any-typed string receiver is still serviced by the runtime's jsval.is_string() arm of js_native_call_method.

This is exactly the fix already applied to indexOf / includes (#1341) for the same reason (an any-typed receiver that is actually an array).

Testing

Regression-tested against Node: typed and any-typed startsWith/endsWith/lastIndexOf, plus array includes/indexOf, all still match. z.string().startsWith("./").describe("x") now returns a ZodString as in Node.

Summary by CodeRabbit

  • Bug Fixes
    • Improved instanceof behavior to honor Symbol.hasInstance with correct precedence, own-property lookup, and proper handling of non-callable cases.
    • Fixed defineProperty for Symbol keys on classes and functions so descriptors are applied correctly for value, get, and set.
    • Corrected method dispatch for startsWith, endsWith, and lastIndexOf on Any-typed string receivers to prevent incorrect string-only routing.
  • Build
    • Resolved an optimized library build path that could omit routed well-known libraries, avoiding missing-symbol link failures.

…dispatch

An any-typed receiver may be an object with its own same-named method (e.g.
zod's z.string().startsWith() returns a refined schema, not a boolean). Forcing
the static String builtin returned a primitive, so a chained .describe()/
.optional() threw. Drop these three from the any-typed string-method fallback so
they dynamic-dispatch; a genuine string receiver is still served by the runtime
is_string() arm. Same shape as the indexOf/includes fix (PerryTS#1341).
@coderabbitai

coderabbitai Bot commented Jun 25, 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: 4856dcae-93c1-4b3a-8895-04d700ba649c

📥 Commits

Reviewing files that changed from the base of the PR and between 45abacf and b796c30.

📒 Files selected for processing (2)
  • crates/perry-runtime/src/object/instanceof.rs
  • crates/perry-runtime/src/object/object_ops/define_property.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/perry-runtime/src/object/object_ops/define_property.rs
  • crates/perry-runtime/src/object/instanceof.rs

📝 Walkthrough

Walkthrough

The PR updates property-call lowering, adds symbol-keyed descriptor storage and @@hasInstance handling, and preserves well-known libraries in the optimized-libs fresh path.

Changes

Lowered call dispatch

Layer / File(s) Summary
Stop string-forcing selected methods
crates/perry-codegen/src/lower_call/property_get.rs
startsWith, endsWith, and lastIndexOf no longer take the string-only fast path and now fall through to runtime dispatch.

Symbol-keyed property hooks

Layer / File(s) Summary
Store symbol-keyed descriptors
crates/perry-runtime/src/object/object_ops/define_property.rs
Symbol keys on class refs and closure receivers now write into class static-symbol tables or closure symbol side tables, with accessor rebinding for get/set descriptors.
Resolve @@hasInstance
crates/perry-runtime/src/object/instanceof.rs
js_instanceof_dynamic checks own @@hasInstance, and js_instanceof looks up class static-symbol hooks before the existing instance-matching path.

Optimized libs fresh-path merge

Layer / File(s) Summary
Merge well-known libraries
crates/perry/src/commands/compile/optimized_libs/driver.rs
The fresh-archives branch keeps the earlier well-known set, extends it with auto-resolved tokio-using libraries, and derives prefer_well_known_before_stdlib from the merged result.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PerryTS/perry#5272: Touches the same property_get lowering path and method-dispatch behavior for Any receivers.

Poem

🐇 I hopped through symbols, bright and sly,
@@hasInstance winked as I ran by.
Fresh libs stayed bundled, neat and warm,
while stringy paths leapt free from form.
Thump-thump — the code now knows the storm.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main fix to any-typed string-method dispatch.
Description check ✅ Passed The description covers the problem, fix, and testing, though it uses custom headings instead of the template.
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 unit tests (beta)
  • Create PR with unit tests

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: 4

🧹 Nitpick comments (1)
crates/perry-codegen/src/lower_call/property_get.rs (1)

135-147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update the earlier method-list comment to avoid contradicting this fallback.

Lines 95-96 still say startsWith/endsWith are unambiguous string-only methods, but this new block says they must not be string-forced. Please remove or rewrite that earlier mention so the dispatch policy is consistent.

🤖 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 135 - 147,
Update the earlier method-list comment in property_get::lower_call to match the
new dispatch behavior for startsWith and endsWith. The current note in the
string-only branch contradicts the later fallback logic in js_native_call_method
handling, so remove or rewrite that mention to say these methods should not be
forced through the static string path and must be dispatched at runtime like
indexOf/includes when the receiver is Any-typed.
🤖 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-runtime/src/object/instanceof.rs`:
- Around line 130-140: Update the `instanceof` handling in
`object/instanceof.rs` so the `Symbol.hasInstance` lookup in the object path
does not silently fall through when an own property exists but is not callable.
In the `type_ref` branch that uses `js_object_has_own_symbol`, make the
`js_object_get_symbol_property` result either invoke `js_native_call_value` when
`value_is_callable(cb)` is true, or raise a `TypeError` for any other
non-null/non-undefined own value instead of continuing. Apply the same rule in
the class-static-symbol path so both code paths enforce callable own
`@@hasInstance` consistently.
- Around line 765-789: The `instanceof` handling in `object/instanceof.rs`
should prioritize the registered `@@hasInstance` hook before the ordinary
class-chain fast path, since `new C() instanceof C` can currently short-circuit
too early. Update the logic around the `class_static_symbol_lookup` /
`js_native_call_value` hook check so it runs before any class-chain return in
the `instanceof` evaluation flow, while preserving the existing OWN-only lookup
and truthy/falsy return behavior.

In `@crates/perry-runtime/src/object/object_ops/define_property.rs`:
- Around line 365-390: The Symbol define-property fallback in define_property.rs
is treating generic descriptors as data descriptors and overwriting existing
closure values with undefined. Update the non-accessor branch in the object
define path to mirror the ordinary Symbol handling: only call
js_object_set_symbol_property when the descriptor actually supplies a value,
preserve the existing fn[sym] for generic redefines like { enumerable: true },
and make sure writable/enumerable/configurable attributes are recorded before
returning, consistent with the other Symbol property paths.
- Around line 297-305: The `define_property` handling in
`desc_read_field`/`js_class_register_static_symbol` is skipping explicit
`undefined` values by checking `is_undefined()`, so `Object.defineProperty(C,
sym, { value: undefined })` never creates the own symbol entry. Update the
`descriptor_value` handling to gate on whether the `value` field is present in
the descriptor, not whether it is undefined, and pass the field bits through to
`js_class_register_static_symbol` even when the stored value is `undefined`.

---

Nitpick comments:
In `@crates/perry-codegen/src/lower_call/property_get.rs`:
- Around line 135-147: Update the earlier method-list comment in
property_get::lower_call to match the new dispatch behavior for startsWith and
endsWith. The current note in the string-only branch contradicts the later
fallback logic in js_native_call_method handling, so remove or rewrite that
mention to say these methods should not be forced through the static string path
and must be dispatched at runtime like indexOf/includes when the receiver is
Any-typed.
🪄 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: 8ae08397-bba5-4134-a391-3393a024a34f

📥 Commits

Reviewing files that changed from the base of the PR and between e68b493 and 30df30e.

📒 Files selected for processing (4)
  • crates/perry-codegen/src/lower_call/property_get.rs
  • crates/perry-runtime/src/object/instanceof.rs
  • crates/perry-runtime/src/object/object_ops/define_property.rs
  • crates/perry/src/commands/compile/optimized_libs/driver.rs

Comment thread crates/perry-runtime/src/object/instanceof.rs
Comment thread crates/perry-runtime/src/object/instanceof.rs Outdated
Comment thread crates/perry-runtime/src/object/object_ops/define_property.rs Outdated
Comment thread crates/perry-runtime/src/object/object_ops/define_property.rs
… defineProperty

This PR's commit bundles the same instanceof.rs / define_property.rs changes as
PerryTS#5667; apply the identical review fixes here:

- instanceof.rs: consult an own `@@hasInstance` (registered hook + defineProperty
  form) BEFORE the class-chain fast path; a present-but-non-callable own value
  throws `TypeError`, only `undefined`/`null` falls through (shared
  `dispatch_own_has_instance` helper, both dynamic and class-static paths).
- define_property.rs: gate symbol value-writes on descriptor-field presence so
  `{ value: undefined }` is preserved and a generic redefine like
  `{ enumerable: true }` does not clobber an existing entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@proggeramlug proggeramlug force-pushed the fix/codegen-anytyped-string-method-dispatch branch from 45abacf to b796c30 Compare June 25, 2026 07:16
@proggeramlug proggeramlug merged commit 2225a73 into PerryTS:main Jun 25, 2026
15 checks passed
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.

1 participant