Repository navigation
Adapt method overrides across ABI differences, write Error codes, and close wrapper-reachable gaps - #1497
Merged
Conversation
… close wrapper-reachable gaps
Overrides whose parameters widen and whose results convert into the
inherited ones keep the inherited slot, which converts around the
override's own body; Error values take code writes, `in` tests and
Object.assign; and a few gaps behind generic wrappers close:
- Method overrides: each inherited argument converts into the
override's parameter (equal, wider, or a checked extraction from an
`any` slot) and the override's result converts into the inherited one
(narrower, a convertible union, a widening Promise payload, anything
convertible over an `any` base). The slot %C.m keeps the inherited
ABI and forwards to %C.m$own; TypeScript callers typed with the
subclass call $own directly when nothing overrides below. Async
overrides of sync and async bases, and JavaScript overrides of
builtin or TypeScript bases (zod's ZodError.toString) are included;
a toString slot converts its result with ToString.
- `err.code = value` on Error values in TypeScript: strings set the
runtime code slot (and the error's checked-dynamic node, if any); other
values clear it and keep the dynamic own property.
- `'key' in err` with any receiver: declared members answer true;
otherwise a class test over the declaring subclasses, a runtime lookup
of the error's own properties, or an Object.prototype member.
- `Object.assign(err, ...sources)` onto an Error target copies onto the
error, re-reads name, message, stack, cause and code into the typed
slots, and returns the error.
- `Error & { ... }` intersections whose extra members have checkable
types are Error values; those members read and write the error's own
properties, each read checked at its declared type.
- Catch bindings typed `any` read as checked-dynamic values (member
reads and writes, assertions); a multi-statement type guard with an
`unknown` parameter takes the caught value as an ordinary argument;
Error members of a binding narrowed to an Error-extending interface
read through the same value.
- Lambda parameters typed `any` by their default are checked-dynamic
slots; forwarded-only rest tuples with optional positions forward
their slots; `value as never` from an untyped value into a typed
parameter takes the checked extraction to the parameter's type.
- Lexical `this` stops at generic instance frames, so an instance no
longer sees the receiver of the caller that triggered it. The fence
for `this` captured through a plain function now points at the
`this` keyword.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…piled prototype resets
- A function adapter whose source returns `any`/`unknown` (a generic
wrapper's result handed back `as F`) converts the result through the
checked-dynamic bridge when the slot's result is checkable: a record
checks its shape, and a Promise keeps its identity and checks its
payload when it settles. Before, these adapters threw "the call has
no exact lowering" when called.
- In a class constructor, `Object.setPrototypeOf(this, new.target.prototype)`
(directly, through a const, or the `if (Object.setPrototypeOf) ... else
this.__proto__ = ...` form), and `Object.setPrototypeOf(this,
C.prototype)` in C's own constructor when no class extends C, are
no-ops: a compiled instance already has that prototype.
- Object.assign between records copies a source field into a wider
target field (`{ bail: true }` into `bail?: boolean`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from
opt/trace-reachable-blockers
to
opt/variadic-generic-wrappers
October 20, 2026 07:16
…ance-error-expando Brings main into the stack (thrown-value fixes, the lower-exprs split, the checker helper removal). This branch's lower-exprs.ts hunks move to expressions/relational.ts (`in` on errors), expressions/property-access.ts (stdlib members of a structurally narrowed catch binding) and expressions/narrowing.ts (assertions on an `any` catch binding); the `this` blame, the `any` catch-binding read and the setPrototypeOf feature test stay in lower-exprs.ts. The Error member read keeps both this branch's intersection members and #1433's subclass-owned `code`. Corpus 6407 and 6408 renumber to 6412 and 6413 (main has its own 6407 and 6408), and 6414 adds the thrown-value round trip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Writes and deletes of `code` through an error's checked-dynamic node (a `catch (e: any)` binding, a reflective write) changed only the node, so typed `err.code` reads kept the old value. The slot sync that already covered name, message, stack and cause now covers code: a string takes the slot, any other value or a delete clears it. A write, delete or Object.assign through an Error subclass's typed view now syncs the slots the subclass inherits from Error after the class commit, which writes only declared fields. `'code' in err` read the code slot, so an own `code` set to undefined answered false. It now asks the runtime: the slot holds a string, or the error has an own `code` property on its node.
…expando Brings in #1433's review follow-up (now merged into #1413). In `key in err`, own `stack` and Object.prototype keys answer true, keys built-in errors keep outside their class fields stay fenced, and every other key keeps this branch's answer: a declaring subclass or a runtime-set own property. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`throw Object.assign(failure, { bail: true })`, where the record's type
lacks the source's member, used to fall to the Object.assign fence: a
fixed record cannot grow a field. As the operand of a throw, the call now
copies the target into a checked-dynamic object and adds the source's
members. The copy is unobservable, because control leaves with the throw
and nothing on that path reads the target again. Paths that already
lowered run first, so no compiled program changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the review merge and "Keep an Error's code slot in step with dynamic writes and own properties". With main's split, `'code' in err` (now the runtime's error.hasCode) lands in expressions/relational.ts. Corpus 6417-error-code-dynamic-writes renumbers to 6415 (main has its own 6417), and the corpus-cost entries of 6407/6408 follow their renumbering to 6412/6413. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
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.
Stacked on #1433. This PR lets method overrides differ from the method they override in parameter and result types, as long as each value converts. It also handles writes and presence tests of properties on
Errorvalues, and fixes several smaller gaps found in code behind generic wrappers.Summary
anyslot. The override's result must convert into the inherited result: narrower, a union that converts, aPromisewhose payload widens, or any convertible value over ananybase. Examples: anasync acquire(): Promise<void>over a base that returnsany, over an untyped async JavaScript method, or over an async base. AtoString(): string | undefinedoverError'sstring.%C.m, so virtual dispatch and base-typed callers are unchanged. The slot converts the arguments in and the result out around the override's own body,%C.m$own. When nothing overrides below, TypeScript callers typed with the subclass call$owndirectly and see the declared result.ZodError.toString() { return this.message }overError.prototype.toString. Their callers keep the slot, because a JavaScript result type is only inferred.toStringslots hand on what a ToString consumer would make of the result: atoStringthat returnsundefinedreads as"undefined"inString(err)and in templates, as in Node.err.code = valueonErrorvalues typed with an intersection (Error & { code?: string }).'code' in errand inspection use. If the error is already reflected as a checked-dynamic object, that object is updated too.err.code = "E1"landed on a dynamic property whileerr.coderead the empty slot, so the read wasundefined.'key' in erronErrorvalues, with any receiver: a value narrowed fromunknownbyinstanceof Error, a call result, a caught value.true.Object.assign, by throw sites, or through dynamic writes.stackstays fenced: Node has one, a compiled error has none.Object.assign(err, ...sources)onto anErrortarget, theObject.assign(new Error(message), details)idiom. The sources' own properties land on the error,message,nameandcodefeed back into the typed slots, and the call returns the error itself.Error & { status: number; ... }intersections, the type of that idiom's result, areErrorvalues. Members beyondError's own and the system-error metadata read and write the error's own properties, and each read is checked at its declared type. The intersection is admitted only when every extra member has a type a checked read can produce.catch (e: any)(and catch clauses compiled withuseUnknownInCatchVariablesoff) reads the binding as its checked-dynamic value, likeunknown. This covers member reads and writes (e.message = ...; throw e) and assertions (e as Error & { code?: string }).isFetchError(err), with anunknownparameter) is called with the caught value, instead of being refused.Errormembers such asmessageon a binding narrowed to an interface that extendsErrorread through the same value.anyonly by their default ((n = lookup() ?? 5) => ...) are checked-dynamic slots, likeanyparameters of function declarations since Compile event-loop metrics, predicate-narrowed catch bindings and other wrapper-reachable gaps #1433.(...args: Parameters<F>) => fn(...args)for such anF) forward their slots; an omitted argument arrives asundefined, which a default treats as omitted. If the wrapper readsargs.length, the existing refusal stays.value as neverflowing into a typed parameter, from ananyorunknownvalue, takes the checked extraction to the parameter's type, asvalue as Twould. Before, the value was checked as a number.thisin a generic function instance no longer resolves to the receiver of whichever caller triggered the instantiation. An instance lowers as if alone. Before, a JavaScript constructor function compiled per call site (readable-stream'sCorkedRequest, iconv-lite's stream constructors) hit the fence "the binding 'this' captured through a plain nested function" when first reached from code with its own receiver. That fence now points at thethiskeyword instead of the entry file's first line.Forms not covered keep their refusals, or keep running as before:
'code' in errafter an expliciterr.code = undefinedanswersfalse; Node answerstrue. Changing thecodepresence test would change existing programs.err.code = valuekeeps the existing dynamic write. Routing it to the code slot changed how the runtime'sReadable.fromdestroy path combines errors (5614-readable-from-iterable-errors), so that file kind keeps its current behavior.ZodErrorconstructor now compiles but still contains two runtime fences, previously hidden behind the class-level refusal:new.target.prototypeandObject.setPrototypeOf. Constructing aZodErrorthrows the fence.Evidence
anybasein,Object.assignandError & {...}intersectionsanydefaults, optional forwarded tuples,as neverargumentstoStringoverrides ofErrorany, multi-statement guards, writes into caught errorsscripts/zero-cost-gate.mjs, bench, the self-hosted compiler and all corpus entries, sharded over three sandboxes):5807-worker-error-valuesand6390-catch-binding-predicatewriteerr.code = "..."onError & { code?: string }. The write now lands in the code slot (−8 B).6394-in-key-error-subclassesadds the runtime own-property lookup to'key' in err(+0.21%)."code"literal in the always-linkedscr_error.cshifted.rodatain every executable.scr_json.cchanged its emission order, which movedscr_dyn_assign_all. The new C functions now sit at the end ofscr_json.c, which already has the literal.err.codechanged5614-readable-from-iterable-errors.pnpm manifestandpnpm node-compat(no change),pnpm node-compat:check, lint, format,pnpm test:ts7 --baselines-only(baselines added for 6400–6409), diagnostics, frontend and IR unit tests, and the self-hosted IR build all pass.🤖 Generated with Claude Code