Repository navigation
fix(runtime): implement Function.prototype[Symbol.hasInstance] (#3662) - #4098
Merged
Merged
Conversation
`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.
This was referenced Jun 2, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 customstatic [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 (theFunction.prototype.{apply,call,bind}not-callable checks landed in #4073).Before
After (byte-identical to
node --experimental-strip-types)What changed
global_this.rs—install_function_has_instance_symbolinstalls the method onFunction.prototype, keyed by the real well-knownSymbol.hasInstance(viajs_object_set_symbol_property, not an@@-string own property that would leak intogetOwnPropertyNames).instanceof.rs—function_prototype_has_instance_thunk+ordinary_has_instance.OrdinaryHasInstancediffers from theinstanceofoperator: a non-callablethisyieldsfalse, not aTypeError. 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 returnfalseinstead of throwing — for this path only.js_instanceof_dynamicnow also resolves theMap/Set/RegExp/ArrayBufferglobal constructor values (mirroring the synthetic class ids the compile-time operator emits), so reflectivehasInstanceand a dynamicx instanceof ctorVarwork for them.Testing
New gap test
test-files/test_gap_3662_function_hasinstance.tsis byte-identical to Node in both the default auto-optimize andPERRY_NO_AUTO_OPTIMIZE=1builds. Existinginstanceofgap tests still pass; theinstanceofoperator is unchanged.Known gap (follow-up)
Array/Object/Dateconstructor values are not yet identified on the dynamic path (they aren't noop-thunk closures, soidentify_global_builtin_constructorreturnsNone), sohi.call(Array, [])andx instanceof ArrayVarstayfalse. The literal-RHSinstanceofoperator handles them at compile time; this pre-existing dynamic-path gap is left for a separate change.