Skip to content

fix(runtime): implement Function.prototype[Symbol.hasInstance] (#3662) - #4098

Merged
proggeramlug merged 1 commit into
mainfrom
fix-3662-function-proto
Jun 2, 2026
Merged

proggeramlug merged 1 commit into
mainfrom
fix-3662-function-proto

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Function.prototype[Symbol.hasInstance] was undefined in Perry — typeof Function.prototype[Symbol.hasInstance] reported "undefined", any reflective use threw, and a class with a custom static [Symbol.hasInstance] was the only way to reach the protocol. This is the missing-method slice of the #3662 "builtins must throw / behave per spec" cluster (the Function.prototype.{apply,call,bind} not-callable checks landed in #4073).

Before

typeof Function.prototype[Symbol.hasInstance]              // "undefined"
Function.prototype[Symbol.hasInstance].call(undefined, {}) // threw TypeError

After (byte-identical to node --experimental-strip-types)

typeof Function.prototype[Symbol.hasInstance]              // "function"
Function.prototype[Symbol.hasInstance].call(undefined, {}) // false
Function.prototype[Symbol.hasInstance].call(Dog, new Dog()) // true

What changed

  • global_this.rs — install_function_has_instance_symbol installs the method on Function.prototype, keyed by the real well-known Symbol.hasInstance (via js_object_set_symbol_property, not an @@-string own property that would leak into getOwnPropertyNames).
  • instanceof.rs — function_prototype_has_instance_thunk + ordinary_has_instance. OrdinaryHasInstance differs from the instanceof operator: a non-callable this yields false, not a TypeError. It reuses the operator's full constructor-resolution path (js_instanceof_dynamic) with a thread-local flag (SUPPRESS_INSTANCEOF_RHS_THROW) that makes the unresolved-RHS tail return false instead of throwing — for this path only.
  • js_instanceof_dynamic now also resolves the Map/Set/RegExp/ArrayBuffer global constructor values (mirroring the synthetic class ids the compile-time operator emits), so reflective hasInstance and a dynamic x instanceof ctorVar work for them.

Testing

New gap test test-files/test_gap_3662_function_hasinstance.ts is byte-identical to Node in both the default auto-optimize and PERRY_NO_AUTO_OPTIMIZE=1 builds. Existing instanceof gap tests still pass; the instanceof operator is unchanged.

Known gap (follow-up)

Array/Object/Date constructor values are not yet identified on the dynamic path (they aren't noop-thunk closures, so identify_global_builtin_constructor returns None), so hi.call(Array, []) and x instanceof ArrayVar stay false. The literal-RHS instanceof operator handles them at compile time; this pre-existing dynamic-path gap is left for a separate change.

`Function.prototype[Symbol.hasInstance]` was undefined: `typeof` reported
"undefined", reflective use (`Function.prototype[Symbol.hasInstance].call(C, v)`
or `C[Symbol.hasInstance](v)`) threw, and a class `static [Symbol.hasInstance]`
was the only way to reach the protocol. This is the missing-method slice of the
#3662 builtins cluster (Function.proto apply/call/bind landed in #4073).

Install the method on Function.prototype keyed by the real well-known
`Symbol.hasInstance` (not an @@-string own property, which would leak into
getOwnPropertyNames). It performs OrdinaryHasInstance via the operator's full
constructor-resolution path (js_instanceof_dynamic), which — crucially — differs
from the `instanceof` operator: a non-callable `this` yields `false`, not a
TypeError. A thread-local suppress flag makes js_instanceof_dynamic return
`false` instead of throwing at its unresolved-RHS tail for this path only.

Also teaches js_instanceof_dynamic to resolve the Map/Set/RegExp/ArrayBuffer
global constructor *values* (mirroring the synthetic class ids the compile-time
operator emits), so both reflective hasInstance and a dynamic `x instanceof
ctorVar` work for them. Array/Object/Date constructor values are not yet
identified on the dynamic path (they aren't noop-thunk closures) — a known gap
for the dynamic form; the literal-RHS operator handles them at compile time.

New gap test test_gap_3662_function_hasinstance.ts is byte-identical to
node --experimental-strip-types in both build paths.
@proggeramlug
proggeramlug merged commit a5d4077 into main Jun 2, 2026
11 checks passed
@proggeramlug
proggeramlug deleted the fix-3662-function-proto branch June 2, 2026 10:58
proggeramlug added a commit that referenced this pull request Jun 2, 2026
… + brand check (#4108)

* fix(runtime): #4101 Function.prototype.toString reconstructs source + brand check

`fn.toString()` returned the generic `"[object Object]"` because Perry never
retained function source and the value-to-string path had no closure case.
`Function.prototype.toString.call(nonFunction)` also silently returned a string
instead of throwing (the brand check deferred from #4098).

Source retention:
- HIR now captures each function's original source by slicing the installed
  module source against the AST span at lowering time (function declarations,
  function expressions, arrows), stored in `Module.closure_source_text` keyed by
  FuncId. `async` is re-prepended when SWC's span excludes the modifier.
- Codegen emits `js_register_function_source(func_ptr, src, len)` alongside the
  existing name registration, keyed by the same wrapper/closure address the
  ClosureHeader carries. A runtime sidecar registry mirrors the name registry.
- `js_jsvalue_to_string` (and the `fn.toString()` codegen fast-path through
  `js_jsvalue_to_string_method`) return the retained source for closures, or a
  synthesized `function <name>() { [native code] }` when no source was retained.
  This also fixes `String(fn)` and `` `${fn}` ``.

Brand check:
- `Function.prototype.toString` is now a dedicated runtime thunk (not the shared
  no-op): it throws a `TypeError` on a non-callable `this` and otherwise returns
  the source. `Object.prototype.toString` keeps its own lenient thunk.
- HIR no longer folds `Function.prototype.toString.call(x)` into `x.toString()`
  (which erased the Function brand); it stays reflective so the thunk runs.
  `Object.prototype.toString.call(x)` still folds (ramda relies on it).

Verified byte-identical to `node --experimental-strip-types` in both the
default auto-optimize and no-optimize paths. Adds node-suite fixture
globals/function-tostring.ts.

* test: add closure_source_text to Module test literals (#4101)

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
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