Repository navigation
Train lang 3: a function is one value with its own properties, kept keys through record assertions, untyped calls through typed slots, generator arrays refused beside an island - #3405
Conversation
An `as unknown as T` between two record types lowers to a width copy that reads only the declared fields of the source type. Node erases the assertion, so a target field the source type does not declare still answers the key the object carries; the copy answered undefined instead (SPEC.md finding K1). A record a checked conversion built keeps its undeclared keys beside it. recordWidthHelper now registers each target field the plan leaves absent, and finalizeRecordLinks rewrites that field's value in the projection to read the name off the source record's live checked view when the record has one, and to keep the absent value otherwise. The rewrite is gated on the finalize scan's sourced shapes, so a program that never converts a kept source into the source type keeps its projection and IR byte for byte. Fields outside the dynCheck domain keep their absent value rather than emit an invalid check; a kept value of the wrong type throws the catchable TypeError (types don't lie). Corpus program 8978 covers the repro, a function parameter and return, a nested field, a union-typed variable and a null-armed target, a union field, an untyped field, an absent key, a two-step copy, a plain record, and arrays, all differential against Node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The width copy that carries undeclared keys has to answer an undeclared key from the record's kept source. The first form built the record's capsule and read it with the dynamic key cache; both reach the dynamic tier, so a program that only projected a record through `as unknown as T` linked the tier's key-get, construct and proxy-get units and its executable grew by a third. Answer the key from the kept source's own view instead: the new runtime entry scr_record_source_get reads the key as the view's own property (scr_dyn_obj_read), so a data property answers its value and an accessor runs with the view as its receiver, as it does for every other observer of the kept source. The view holds the source object's own properties and links no prototype, so a key the source lacks answers the undefined singleton, which the field's own absence then decides. The read never builds the record's capsule, and the lowering calls it through record.sourceGet. The entry point is a unit of its own (scr_record_source_get.c) rather than an addition to scr_json.c, and its header is not listed in scr_runtime.h: the compiler names the symbol directly, so no other unit's preprocessed text changes, every default object in the runtime pack stays byte-identical, and a program that never projects a record links nothing new. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A function may carry an own data property of any name, the inherited Function.prototype and Object.prototype members included: `fn.call = f` shadows the builtin exactly as JS says, and Node reads the own property at a call site. The typed lowering now stores those names in the closure's own-property table, and the runtime reads the table first — in a build of scr_json.c and scr_dyn_invoke.c selected only for the programs that write, define or delete one of those names. Every other program links the unchanged objects, byte for byte.
- runtime-features.ts: a `fnMemberProps` runtime feature answers true when a function spells one of the nine inherited member names as the key of a `dyn.keySet`/`keySetComputed`/`keyDelete`/`keyDeleteComputed`, or as the key of a `dyn.defineProperty`/`definePropertyShapes` on a boxed callable. The key is followed through the hidden local a keyed write evaluates it into (lower-exprs.ts) and the box a string key takes, in one forward pass with an early exit, and per function because a hidden key local is function-scope: the same `%setKey.0` names a different value in every function. A definition counts only on a boxed callable, since class and object lowering define `constructor` and `toString` on ordinary objects that this path never sees. `Object.assign(fn, {…})` lowers to `dyn.assignAll` and is not examined, so such a program keeps the default refusal main gives it.
- runtime-pack-matrix.mjs: scr_json.c gains a `fn-member-props` variant and a `node-island-fn-member-props` build that pairs it with the node-island build, and scr_dyn_invoke.c gains `fn-member-props`; the JS-object-model and worker twins follow from the existing clones. backend/native/executable.ts emits `SCR_FN_MEMBER_PROPS` only for a program the rule marks, so the source build names the same objects the matrix selects.
- scr_json.c: +51 lines, nothing removed. Both behaviour changes sit behind `#ifdef SCR_FN_MEMBER_PROPS`, so the default build's preprocessed source is token-identical to main and a program that does not use the feature links the unchanged object. In the feature build `scr_dyn_fn_property_fence` refuses only `caller`, `arguments` and `__proto__` — Node answers those through the builtin accessor (a TypeError) and the prototype setter, and neither is lowered on a function yet, so refusing them beats inventing an own data property Node would not create — while `defineProperty`, `keySet` and `keyDelete` store, overwrite and delete the table entry like any other key, and `scr_dyn_value_of` calls an own `valueOf` before answering the receiver.
- scr_dyn_invoke.c: +29 lines, nothing removed, also gated. The checked-dynamic call path reads an own call/apply/bind when the closure carries a table, before the native call/apply/bind lowering; a function that never received a property pays one pointer test.
- callable-properties.ts: the write target admits an inherited name without a user declaration (the write IS the own property JS creates), while a read still asks for one, so a plain `fn.call` keeps the protocol lowering it always had. `FUNCTION_INHERITED_MEMBERS` gains `toString`.
- lower-expando.ts: the SC1090 refusal for those nine names and its member set are deleted; the read-only length/name/prototype/caller/arguments fence stays.
Zero cost. The gate against 915aaa1 compares 5,216 corpus programs: 5,105 byte-identical, 95 that fail to compile the same way on both sides, and 16 differences — each one a program that writes the feature's names, with byte-identical IR and LLVM and only the linked runtime objects changed:
corpus/7037-js-var-open-writes/main.ts, 7040-es5-error-constructor.mjs, 7045-instanceof-constructor-values.mjs, 7753-typed-array-named-properties.js, 8110-builtin-prototype-own-members.js, 8260-class-prototype-views.mjs, 8321-typed-array-constructor-kind.js, 8702-js-primitive-object-coercion.js, 8963-js-boolean-method-receiver.js, 8973-js-boolean-prototype-method-values.js, bound-constructor-inspect-names.cjs, checked-addition.js, es5-constructor-inspect-names.cjs, js-callable-proxy.js, js-class-member-dispatch.js, js-primitive-wrappers.js
Fifteen match the change's signature in `--allow`; checked-addition.js deletes `object.valueOf`, which the signature's `delete` alternative covers. The two self-hosting fixtures and the self program are identical. The dynamic-tier symbol check passes with `scripts/zero-cost-gate.mjs` untouched: the script is byte-identical to main. Timings: bench geomean +0.26% [-0.58%, +1.03%] and fixtures +0.52% [-0.28%, +1.31%], both within tolerance; self -1.07% [-1.69%, +2.27%], inconclusive, which the gate reports as a spread rather than a regression.
Origin/main also changed lower-stmts.ts in this round; the branch rebases onto it cleanly, and no other file the branch touches moved on main.
Tests: corpus 9224-callable-protocol-members (typed shadowing, `Object.assign`, `delete`, keys, typeof) and 9225-dynamic-fn-protocol-shadow (checked-dynamic apply/call/bind dispatch, `defineProperty`, `delete`) run under Node and as compiled binaries and compare byte for byte. runtime-features.test.ts pins the rule, including the per-function scoping case and the keys it must not match; runtime-pack-matrix.test.ts pins the variant selection for both units.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`f.apply(thisArg, tuple)` on a callable whose own type declares `apply` resolves statically to a direct call of the function body: the wrapper that the protocol fast path folds away. For a value with an own `apply` table entry that is the wrong callee — Node reads the table and calls that entry — and the general member-call path, which does read the table, cannot lower the argument list here: a forwarded tuple has no value form (its positions are the caller's native slots). The combination now refuses by name with SC1090 instead of running the wrong callee. - callable-properties.ts: `callableDeclaresMember` answers whether the value's own type declares `name` as a user member, so a statically resolved protocol form can ask before it takes its shortcut. - typed-arguments.ts: `lowerForwardingApply` asks it for `apply` after the forwarded-tuple guard. An array argument is a value and keeps working through the general member-call path. - tests/corpus/9226-callable-own-apply-dispatch.ts: the argument forms that do have a value (array literal, tuple, plain array) dispatch to the own entry, a replaced entry is followed, and a plain call never reads the table. Byte-identical to Node. - tests/diagnostics/callable-own-apply-forwarded-tuple.ts: the refusal, with its snapshot and order-parity baselines. No program that compiled on main stops compiling: main refused the write that creates the own `apply` in the first place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `function fmt() {}` merged with `namespace fmt { export const digits = 2 }`
is ONE value in Node: the transform assigns each exported member onto the
existing function object, so `Object.keys(fmt)` lists them, `"digits" in fmt`
answers true, inspect prints them, and a read through any alias sees a later
write. scriptc kept each member in a namespace-mangled module global, so
`Object.keys(fmt)` answered `[]` and the properties were invisible.
Every exported member of a merged block is now an own-property table entry of
the function object (callable-properties.ts, the same table expando writes and
`Object.assign(fn, ...)` use), written at the member's source position so the
own-key order follows the block, and read through the same keyed operations
the `any` view of the function emits. Bare reads inside the block read the
table; bare writes have no storage to name and are refused with the qualified
spelling.
Namespaces that do not merge with a function keep their flattened module
globals byte for byte. Members the table cannot represent (a class, an enum, a
nested namespace, an import= alias, a destructuring pattern, a binding without
an initializer, all of which Node's transform either skips or stores
differently) are refused by name at collection.
`in` on a typed callable receiver asks the table (`dynHasKey`), matching the
`any` view, so `"digits" in fmt` answers as Node does.
Corpus 9227 pins the merge both ways; two diagnostics fixtures pin the bare
write and the unsupported member forms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
node:test's `Mock<F>` is `F & { mock: MockFunctionContext<F> }`: the value IS
the function. scriptc mapped the intersection to a separate hybrid record
`{ %call: F, mock }`, so `typeof mockFn` answered "object", the value was not
the function, and every consumer needed a `%call` unwrap.
`Mock<F>` now maps through the ordinary callable-intersection path to F's func
type, and the mock lowering returns the wrapper closure itself, defining the
context as a non-enumerable own property of its table (`dyn.defineProperty`,
so `Object.keys(mockFn)` stays empty and `mockFn.mock` reads the live log).
The mock-specific type-mapper machinery (`mapHybridCallableRecord`,
`isMockHybridIntersection`) and `mock-shapes.ts`'s `mockType` are deleted.
`hybridCallUnwrap` and the `%call` arms of `coerceToExpected` now handle only
callable CLASS instances (the chalk-shim builder), the one remaining `%call`
producer.
`mock.method` keeps two wrappers: the object's field records the object as
`this`, while the returned mock records `this` as undefined, as Node's bare
call of the returned function does (corpus 5810).
The proxy-vs-own-property reflection difference is documented on the
limitations page; corpus 9228 pins `typeof`, identity, aliasing and the call
log both ways.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two refusal fixtures record their rendered diagnostics, and the four new fixtures record their TypeScript preflight/order baselines. Every existing baseline is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node serves MockFunctionContext through a proxy: reads of `mockFn.mock` answer, but the proxy has no own property, so `"mock" in mockFn` is false and `Object.getOwnPropertyNames` omits it. scriptc stores a non-enumerable own property instead, which is what makes `Object.keys` empty like Node; the comment claimed the proxy made `in` true, which the differential program had already shown is not the case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A unit's record cannot carry the merged-namespace member registry or the per-file function-member store list: both are filled by splitFiles before any unit lowers, and a unit that grew either would no longer replay. Register them with checkedTable beside nsBlocks, the same collection shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The function member column listed its scr_json.c builds by hand: one alone and one beside the Node island build. #3040 adds a column to the same unit the same way, for WeakRef, and neither list names the other's feature. With both merged, a program that writes a function member and constructs a WeakRef had no build carrying both defines and linked one without SCR_FN_MEMBER_PROPS or without SCR_WEAK_REF. withBuildColumn adds a column as a twin of every build the unit has so far, so the unit has a build for every combination whichever order the columns land in, and whether or not another change adds or removes one. The builds are the same as before; the one beside the island build is now named fn-member-props-node-island. A test checks every unit: each combination of the features its builds name has exactly one build, carrying each feature's defines. Co-Authored-By: Claude Code <noreply@anthropic.com>
A function inherits `call`, `apply`, `bind` and `toString` from Function.prototype, so reading one as a value answers a function: with no own property of that name, and again after an own property that shadowed it is deleted. The checked-dynamic read (scr_dyn_fn_get) answered own properties, `name`, `length`, `constructor`, `apply` and builtin statics, and nothing else, so `typeof fn.call` was "undefined" where Node answers "function", and `delete fn.call` left nothing behind to read. Member calls were never affected: they dispatch through the native call/bind lowering. The SCR_FN_MEMBER_PROPS build of scr_json.c now answers the inherited member when the function has no own property of that name: one builtin value per name, shared by every function, with Node's name and length, that runs the builtin over its `this` (`fn.call.call(fn, thisArg, ...)`, `fn.bind.call(fn, thisArg)`). Function.prototype.toString has no native source text, so its value keeps the refusal the member call `fn.toString()` gives, and a string conversion does not mistake the inherited builtin for a formatter the program installed. Every added line is inside the #ifdef, so the default object is unchanged. The fnMemberProps rule selected that build for a program that writes, defines or deletes one of the protocol names. It now also selects it for a program that spells `call` or `bind` as a dynamic read key, so a plain `typeof fn.call` is right without any write, and for one that deletes a `toString`, which reveals the inherited member. A spelled read of `toString` cannot select the build: `String(value)` on a dynamic value lowers to that very read in programs that never name the member. A read through a key computed at run time proves nothing either; both keep the build the rest of the program selects. Corpus 8887 pins the inherited values (typeof, hasOwnProperty, `in`, identity across functions, name and length), held values called on a function and on a non-callable, shadow then delete, deletes of members that were never own, re-adding after a delete, an own toString, and the same reads through computed keys; 8888 is its JavaScript twin. runtime-features.test.ts pins the rule. Co-Authored-By: Claude Code <noreply@anthropic.com>
Two findings from the review of #3157. A literal bracket read (`fn["call"]`) evaluates its key into the dynamic tier, so the read carries a boxed string literal, which the rule did not count as a spelled key: in a program with no other trigger the read still linked the default build and answered undefined. The rule now unwraps the box. The inherited `call` value refused every receiver that is not a function value, so `fn.call.call(proxy, thisArg, ...)` threw a TypeError for a callable Proxy, which Node invokes. It now runs any callable. `bind` and `toString` build on the function's own closure and text; on a callable that is not a function value they refuse by name (SC2020) instead of claiming the receiver is not callable. 8888 pins the Proxy receiver and the bracket reads; runtime-features.test.ts pins the boxed key. Co-Authored-By: Claude Code <noreply@anthropic.com>
Four cases the review bot found in the function+namespace merge:
- `namespace f.inner { … }` merged with `function f` stored the inner block's exports on `f` itself, so `Object.keys(f)` answered the inner names where Node answers `inner`. The dotted declaration is refused by name, like the `export namespace inner` spelling.
- A bare reference to an exported function read the property the block had not stored yet and failed its checked conversion. Node's transform keeps the declaration as the block's own hoisted binding and does not qualify the reference, so the bare name is the declared function, before the store and after the property is replaced.
- A qualified call named the declaration, so after `Object.assign(fmt, { helper })` it still ran the old function. The call now reads the property, as the qualified read already did.
- An exported interface, type alias or type-only namespace beside a runtime member was refused with SC1090. They are no properties of the function object and are skipped, as in a namespace that merges with no function.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The base branch rewrote the own-member scan around ownMemberOp (a keyed library call or a call into the lifted define helper) and mayHoldCallable; this branch added the delete of a `toString` to the same statements. The scan keeps the base's structure and tests the revealed-by-delete names on the operation it found. Co-Authored-By: Claude Code <noreply@anthropic.com>
main merged #3097 (delete an undeclared record key from the record's kept source), which adds the library call record.sourceDelete beside the two this branch adds, record.sourceGet and record.sourceHasKey. The four conflicts are those entries meeting at the same place, in the symbol table, the signatures, the may-throw list and the LLVM dispatch; each keeps both sides' entries. Co-Authored-By: Claude Code <noreply@anthropic.com>
scr_record_source_get.c was listed as a base runtime source, so every executable linked its object. Nothing of it is reachable in a program that never projects a record, but the object still leaves its FILE symbol in the symbol table: a fully typed benchmark built from this branch carries `scr_record_source_get.c` there, which no executable built from main has, so every program's executable differed from main's. The unit is now optional, selected by its two library calls (record.sourceGet, record.sourceHasKey) through the runtime feature recordSourceGet, the way main gates the kept-source delete (recordSourceDelete): the pack matrix lists it under OPTIONAL_RUNTIME_SOURCES, the from-source lane's RUNTIME_UNITS, the library archive and the library flavors gate on the same fact, and the executable and library cache identities carry it. This is the change the branch's author made locally on 2026-11-05 and never pushed (the commit was unsigned); it is applied here on top of the review fixes and main, with the presence test added to the rule. Co-Authored-By: Claude Code <noreply@anthropic.com>
No conflicts. main's #3245 lists the required link feature keys once; recordSourceGet is an optional key (absent when off), which that list does not name. Co-Authored-By: Claude Code <noreply@anthropic.com>
The pack test that asks every unit for one build per combination of its features enumerated all of them. A change that declares an implication between two features (RUNTIME_FEATURE_IMPLIES, as the FormData work does: `formData` implies `webBodies`) adds no build for the combination no program can have, so whichever of the two changes merged second would fail this test on `scr_fetch.c with formData`. The test now reads the table when the matrix exports one: a combination that names a feature without what it implies must have no build, and a feature's own defines are those of the smallest build a program with it can link. Without the table every combination is possible, as before. Co-Authored-By: Claude Code <noreply@anthropic.com>
Store function+namespace merges and mock functions in the callable table
`import x = fmt.member` on a function+namespace merge failed to compile
when the member was a `let` or a `var` (review): the alias statement
looked for the member's module global, and a merge member has none since
its storage became the function object's own property. The alias of a
`const` or a function member compiled and read the member live, or the
declaration, where Node's `const x = fmt.member` keeps the property it
copied: after `Object.assign(fmt, { digits: 4 })` the alias answered 4
where Node answers the old value, and an alias of a function whose
property had been replaced before the alias ran the declaration.
The alias of every merge member now owns storage, and the alias
statement fills it from the function object's table. Aliases of members
of a namespace merged with a class or an enum were right and are pinned
too.
Two neighbours of the same kind:
- A generic or an overloaded function exported from a merged namespace
crashed the compiler (an uncaught PoisonError where its property is
stored). It has no single value to store, so it is refused by name.
- A reference to an alias from a function written above the alias
statement read the alias's storage: with storage for every member's
alias that is a call through an unset function, and for a string
alias it was a segmentation fault on main. Node's transform leaves
such a reference unbound and throws ReferenceError whenever the
function runs, so the reference is refused by name, as the same
reference at module level already was.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…ntity without one A slot declared as an array of generators became a checked-dynamic value (the canonical mapping now applies the array storage contract). Two things followed that this settles once every body is lowered (generator-arrays.ts). In a build that runs a package or a module in a Node island, a generator that crosses the boundary is not the same value on the other side yet (the island value matrix pins that for parameters and fields). The array form added silent cases of it: eleven observations in four matrix cells (generator.p, generator-lazy.p, generator-infinite.p, iter-array.p) went from a build-time refusal to a wrong answer. Such a build now keeps the array type these slots had before, which has no storage, so each use that was refused by name is refused by the same rule (SC1101 for the conversion to unknown[], SC8010 for the element handed to the island), and a program that reaches the end of lowering with such a slot, where the backend stopped with an internal error, is refused by name (SC8010). The same refusal covers a program that loads a module into an island itself (requireModule). The three matrix entries pinned as wrong are refusals again: the table is the one on main for these cells. Without an island the array compiles as before, and a generator read back out of it is the generator that went in. The conversion of a checked-dynamic value into a generator-typed slot is the iterator adapter, a new generator that steps its source: `const list = [g]; const h = list[0]!; h === g` was false. In a program that has such an array every adapter call first asks whether the value is the native generator of exactly the slot's type and then takes it through the checked cast. Programs without such an array keep their adapter calls, so no program that compiled before changes. Co-Authored-By: Claude Code <noreply@anthropic.com>
The second lang train lands first; this branch builds on it. Co-Authored-By: Claude Code <noreply@anthropic.com>
No conflict. Against main the branch is its eleven files; the island value matrix table is main's with the three generator-lazy.p entries back to refusals. Co-Authored-By: Claude Code <noreply@anthropic.com>
No conflict. Main now holds the island value matrix re-pin (#3367), which pinned the generator cells as main produced them. The table is regenerated on this merge in the next commit. Co-Authored-By: Claude Code <noreply@anthropic.com>
…assed through
A function declared with an `any` parameter, stored in a slot whose
function type names a record with required members and called back
without that type, threw before it ran: the caller's argument was
converted into the slot's parameter type first, a checked conversion from
an untyped value ("expected string at $.diff, got undefined"). Node runs
the function. Neither end of that call names the slot's type.
function show(payload: any) { … }
const def: { show: (payload: { diff: string }) => string } = { show };
const loose: (payload: any) => string = def.show;
loose({}); // Node: show runs. Before: TypeError
def.show({} as any); // the program's claim: still throws
The rule sits beside the one for argument counts (function-views.ts): a
consumer that hands a view more than its type admits starts from the
function the view forwards to.
- A conversion of the slot's function type to one whose parameter is
wider (untyped where the slot names a type, a union with more arms, a
record that requires less, a base class) tests for the views that
convert that parameter and returns the function itself, or the function
converted to the wide type when its own type is another.
- The same adapter asks for the checked-dynamic boundary's view too (a
callable that crossed `any` before it was stored in the slot) and checks
the callable itself into the wide type.
- A box sees through a view in the backend's box builder for the view's
signature, the one place every box of that signature is made: in a
program that boxes a function of that type, the view's body carries a
mark (IrFunction.boxesSource). This replaces the
`%fn.box` helper, which only saw unnamed `dynFrom` expressions, so a
function boxed by name or inside a record, array, Map, instance or call
result is reached as well, for the argument-count views too.
- A call the compiler typed by itself (an untyped package function
specialized for the callback it is handed, a local declared `any` that
holds a function) asks whether the callee is a view and then checks the
untyped argument against the function's own parameter
(callValue.untyped; JavaScript source is known by its file).
- A function literal whose parameter annotation spells `any` keeps its own
signature in a function-typed destination instead of adopting the
slot's, so it is a function behind a view like any other.
What still checks: a call through the slot's own type, the parameter type
the function itself declares, and a function whose parameter type is the
slot's (contextually typed, or a generic function instantiated there).
No run-time representation is added: a view already holds the function in
its first capture. A typed call through the slot is unchanged.
Co-Authored-By: Claude Code <noreply@anthropic.com>
No conflict. Main now holds train islands-1 (#3377), which regenerated the island value matrix; the table is regenerated on this merge in the next commit. Co-Authored-By: Claude Code <noreply@anthropic.com>
- record.sourceHasKey (#2999) says whether it can throw: it cannot. It asks the record's kept view for an own key; no accessor runs (builtin-attributes.test.ts, main's #3227). - The run-time refusal of Function.prototype.toString on a dynamic value (#3053's build of scr_json.c) names its diagnostic code, SC2020, as its neighbour does (scripts/runtime-refusals.mjs --strict, main's #3137). The refusal baseline is regenerated in the next commit. Co-Authored-By: Claude Code <noreply@anthropic.com>
No conflict. Co-Authored-By: Claude Code <noreply@anthropic.com>
…nvoke.c; regenerate the refusal baseline Main's islands-1 gives scr_dyn_invoke.c a Node island build. #3053's fn-member-props column was added in front of it, so a program with both features had no build that carries both defines (the combination test of runtime-pack-matrix.test.ts names it). The column now follows the island build and doubles it. packages/runtime/refusal-sites.json is regenerated with scripts/runtime-refusals.mjs --write: #3053's three refusal sites in scr_json.c join the list (393 sites, each with a code). Co-Authored-By: Claude Code <noreply@anthropic.com>
…/lang-3 No conflict. Co-Authored-By: Claude Code <noreply@anthropic.com>
Main now holds trains cli-5, startup-1 and cli-4. Conflict, frontend/canon-map.ts, the array case: main added the rule that a type that is itself an array of checked-dynamic elements is the checked-dynamic value, at the place where this branch notes an array of generators. Both rules stay, each for its element kind. The island value matrix and conformance list are regenerated on this merge in the next commit. Co-Authored-By: Claude Code <noreply@anthropic.com>
Conflicts: - frontend/canon-map.ts, the array case: #3366's note for an array of generators and main's rule for an array of checked-dynamic elements; both stay (the resolution #3366's own branch has). - frontend/lowering/lower-modules.ts: #3053's store of a hoisted function member and main's load of a closed island's generated module were added in front of the same statement; both stay. - ir/builtin-never-throw.ts: main's dyn.classValueHash and #2999's record.sourceHasKey at the end of the list; main's first. - ir/codec.generated.ts: regenerated on the merged sources (pnpm generate:ir-codec). Co-Authored-By: Claude Code <noreply@anthropic.com>
Main now holds the landing rollup (#3395: train lang-2, startup-2, islands-3), the load-cost train and perf-4. Conflicts: - executable-features.ts, native-link-info-core.ts, native/contracts.ts, executable-cache.ts, index.ts: main's moduleCompileCache and #3053's fnMemberProps were added at the same places; both rows stay, main's first. - ir/builtin-never-throw.ts: main's two module.compileCache rows, then #2999's record.sourceHasKey. - frontend/lowering/lower-modules.ts: #3053's store of a hoisted function member, then the rollup's closed-island block with its continue; both stay. - ir/codec.generated.ts: regenerated on the merged sources (pnpm generate:ir-codec, layout 772ae1c67381f949). Co-Authored-By: Claude Code <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Zero-cost gate on the pushed head |
|
HOLD: do not merge yet. The review bot left two findings on this train that are not reproduced or refuted yet: 🤖 Generated with Claude Code |
A call through an own call, apply or bind property of an untyped function read the property inside the dispatch, after the arguments had run. An argument that replaced or deleted the property changed the function the call ran, and an argument that stored one where there was none was called in place of the builtin. JavaScript reads the callee first. The prepare step that reads the callee of every other receiver before the arguments now reads the own property of a function too, and a prepared call that found none runs the builtin. A caller without a prepare step reads the property when it calls, as before. Only the builds of scr_dyn_invoke.c with function member properties change. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Hold lifted: both review-bot threads are answered and resolved.
The lanes and bootstraps on 42882dc are CI's run on this push; the full sandbox round (both lanes, both native bootstraps) ran on the tree before the merge of main and the fix, as in the description. |
…lang-5 is cut on top of lang-3 Co-Authored-By: Claude Code <noreply@anthropic.com>
Main gained train node-1 (vercel-labs#3353) and train cli-18. No conflict with lang-3 (vercel-labs#3405) or lang-4 (vercel-labs#3410), which this branch is cut on. Co-Authored-By: Claude Code <noreply@anthropic.com>
Four language fixes validated together in one round and landed as one branch: each was proven by its owner on its own head; this is the four merged on main, with their conflicts resolved once. The round ran on top of train lang-2 before it landed (with the landing rollup, #3395); the checks on the tree merged with today's main are at the end.
What lands
fn.call = f,Object.defineProperty(fn, "bind", …)anddelete fn.applycreate, define and delete an own property as in Node (main refused the write, SC1090); a function merged with a namespace (function fmt() {}andnamespace fmt { export const digits = 2 }) and amock.fn()keep their members on the function itself, soObject.keys(fmt),"digits" in fmtand a member replaced withObject.assignanswer as Node does;fn.call,fn.bindandfn.toStringread as values; animport x = ns.memberalias of a merge member takes its snapshot. A program that never writes one of a function's inherited member names links the unchanged runtime objects: the paths live in a per-program build (SCR_FN_MEMBER_PROPS) ofscr_json.candscr_dyn_invoke.c.const narrow = JSON.parse(text) as Base; const wide = narrow as unknown as Wide; wide.extra: Node reads the value, main readundefined. The width copy reads a field the source type does not declare from the kept source of a checked value, with its own check; a program that never converts a checked value into the source type keeps its copy byte for byte.fix/untyped-call-through-slot(no PR; it changes Give a function every argument of the final call, whatever slots it passed through #3263's function views, which train lang-2 landed): an untyped call reaches the function behind a typed slot. A function declared with ananyparameter, stored in a slot whose function type names a record with required members and called back without that type, threwTypeError: expected string at $.diff, got undefinedbefore it ran, because the argument was converted into the slot's parameter type first. Node runs the function. The call is now checked against the function that runs; a call through the slot's own type is checked as before.Not in this train: the compiler fixes found on the Vercel CLI (they land through train cli-1).
Conflicts and fix-ups on the train
record-sources.ts: main rewrotefunctionKeepsRecordSourcesand added the source-test fold at the place where Carry kept-source keys through record width copies #2999 adds the kept-key read: main's function stays, Carry kept-source keys through record width copies #2999's functions and imports are added.scr_json.test.c: both new tests stay.fsExistsValueand Admit an own property for every function member, in a runtime build #3053'sfnMemberPropswere added at the same six places (both rows stay);lower-calls.ts: main moved the call through a callee value intolowerCalleeValueCall, where Admit an own property for every function member, in a runtime build #3053's change is applied;runtime-pack-matrix.mjs: main removed the Node island builds ofscr_json.cand three more units (they call through the installed table) and gavescr_dyn_invoke.cone, so Admit an own property for every function member, in a runtime build #3053'sfn-member-propscolumn is a twin of the builds that exist, forscr_json.c(default and object model) and forscr_dyn_invoke.c(default, island and object model). The combination test ofruntime-pack-matrix.test.tsholds that.record.sourceHasKey(Carry kept-source keys through record width copies #2999) says that it cannot throw; Admit an own property for every function member, in a runtime build #3053's run-time refusal ofFunction.prototype.toStringon a dynamic value names its code (SC2020), andpackages/runtime/refusal-sites.jsonis regenerated (393 sites).frontend/canon-map.ts(the resolution Keep arrays of generators refused beside a Node island, and their identity without one #3366's own branch has),lower-modules.ts(Admit an own property for every function member, in a runtime build #3053's store of a hoisted function member and main's load of a closed island's generated module in front of one statement; both stay),ir/builtin-never-throw.ts(main's row, then Carry kept-source keys through record width copies #2999's).ir/codec.generated.tsis regenerated on the merged sources;ir/fields.generated.tsregenerates to itself.Validation
All in Vercel Sandboxes. One full round on the train (
c5f1073e18: train lang-2's head5b32646f38, which contains main851b59b90f, plus the four members), compared with lang-2's head, so every row below is this train's.pnpm -r buildpasses.packages/cli/test/mcp.test.ts, "SIGTERM stops running programs before the server exits" ("condition not met in time"), a timing test of the CLI's MCP server on a loaded lane box: no member changes a file underpackages/cli, and the file alone, sanitized, passes 8 of 8 on the same tree.--gate-corpus=all: PASS. 5,880 programs: 5,509 identical, 288 fail to build on both sides (the island programs, built without a libnode pack on both sides), 81 allowed and every one a member's row, 2 of the train's new programs that the base cannot build (Admit an own property for every function member, in a runtime build #3053's), no runtime-only row.union-node-sets-9.0%), 2 reported slower (regex-logs+15.7% [+6.7%, +44.2%],atomics-contended+5.3% [+0.0%, +7.8%]). Both of those executables are byte-identical to the base's in the gate, so the two readings are the machine, not the train.ir/codec.generated.tsregenerated and committed, no other generated file changes (generate:ir-fields,manifest,node-compat --local,gen-builtin-object-members --check,runtime-refusals --write);pnpm lint(393 runtime refusal sites, none missing),format:check,node-compat:check, the ts7 baselines; 27 guard test files, 260 tests; the members' corpus programs with their neighbours (namespaces, mocks, callables, function views, main's variadic programs),differential.test.tsandllvm-differential.test.ts244 of 244 plain,differential.test.ts122 of 122 sanitized; 55 unit and harness test files with the diagnostics snapshots,dyncheck,npm-staticand the island harness, 1,111 tests; the diagnostics snapshots sanitized, 370 of 370;scripts/checker-identity.mjsfor the untyped-call branch's four programs; the island value matrix against the committed table, 239 of 239.arguments, called through an untyped value or throughapply).The 81 rows, by member
SCR_FN_MEMBER_PROPSbuild ofscr_json.cwith identical IR and LLVM (exe: resized scr_dyn_fn_get, +0.06% to +0.83%): programs that write a member through a computed key on an untyped receiver, the class its owner's round had 40 of; one of them,8739-replaced-method-worker-threads.ts, is on main since 2026-11-07. Three have changed code, as in its own round:1963-namespace-aliases.ts(+221.8%: its function+namespace merge now lives on the function),5810-mock-method-records.ts(+26.1%) and5811-mock-tracker-restore.ts(+24.8%). Nine are programs it adds (+0.27% to +27.1% against what the base makes of them).fix/untyped-call-through-slot, 18 rows: the 17 of its own list (its four programs, Give a function every argument of the final call, whatever slots it passed through #3263's three, six programs that box a function type they hold a view of,self/cli,fixture/llvm-emitter,fixture/llvm-layouts; -0.13% to +1.14%) and8839-value-list-callback-shapes.ts, on main since 2026-11-07, of the same class (one box builder gains the view test).checked-inherited-fields.ts(+15.98%; +15.45% in its own round: the kept key is read through a check that can throw, which brings the unwinder into that unit) andkey-order-spread-copies.ts(+0.17%), both on its owner's list;8857-union-arm-keyed-table.ts(+0.62%), on main since 2026-11-06, the same width helper.--gate-max-size-growth=250is for1963-namespace-aliases.ts. No fully typed program is among the rows: each uses the feature its member is about, or is the compiler.The pushed head (
aa4b28b88c= the train merged with maincfe2e1b6ba, which has the landing rollup)moduleCompileCachebeside Admit an own property for every function member, in a runtime build #3053'sfnMemberProps);ir/builtin-never-throw.ts(main's two rows, then Carry kept-source keys through record width copies #2999's);lower-modules.ts(Admit an own property for every function member, in a runtime build #3053's store of a hoisted function member, then main's closed-island block; both stay);ir/codec.generated.ts, regenerated on the merged sources.pnpm lint(393 refusal sites, none missing),format:check,node-compat:check, the ts7 baselines; 27 guard test files, 260 tests; the members' corpus programs, 244 of 244 plain and 122 of 122 sanitized; 55 unit and harness test files, 1,119 tests; the diagnostics snapshots sanitized, 375 of 375; checker identity for the untyped-call programs; the island value matrix against main's table, 239 of 239; the variadic matrix as above (21 same, 64 refused, 3 loud, 2 silent).cfe2e1b6ba,--gate-corpus=all(gate-only round, identity verdict; its self-compile timing follows as a comment): 5,528 identical, 315 fail to build on both sides, nothing that differs outside the allow list.Not run
The macOS lanes; the Node island conformance lane (no member touches island code beyond #3366's refusals; the island value matrix ran, above); a pi or Vercel CLI build (the untyped-call branch was measured against pi's repro by its owner).
🤖 Generated with Claude Code