Implements wave-2 decisions 3a = A and 5 = A from the pi decision package.
3a — class extends a Node global native code lacks
class CodexSocket extends WebSocket { ... } now compiles as an ordinary class value. The heritage read answers the constructor value host-global-values.ts already provides, and the subclass adopts it as its callableBase, so super(url) lowers to a call of that value and throws its SC2020 host-global fence ('WebSocket' is a Node global that native code does not provide). No instance can therefore exist:
- a statically visible
new Sock(...)(including anewof any further subclass, which inherits the fence through the base chain) reports the sameSC2020at the construction site, with thehost-globalNode island hint; - a construction through an opaque class value (a parameter, a class expression binding, an untyped slot) builds and throws the fence at run time.
The class value itself is observable exactly as Node's: typeof, name, length, identity through an alias, the prototype.constructor pair, statics, and subclassing. That is what lets a branch only Bun takes (process.versions.bun is absent natively) compile.
A static the class inherits from the global (Sock.OPEN, Sock.probe(…)) is the global's own surface, which native code does not provide. Reading or calling it through the subclass, a further subclass or an alias reports the same SC2020 the read off the global reports. A static the program declares in a form with no lowering there (an accessor, a field without an initializer) names the member. Neither answers undefined.
Target: pi openai-codex-responses.ts:1017 clears.
5 — library functions as values, derived from their direct calls
derivedBuiltinValueFn gives a table row a value shape from the row's own direct-call ABI instead of a per-member contract: the value is a closure whose body is exactly the libCall a direct call with arguments of those types compiles to, interned once per member (%builtin.value.<fn>:<typeKey>:<adapterShape>), so stat === fsp.stat holds across import spellings and Node's name is kept. Derivation is passive — direct-call lowering is untouched, so typed code is byte-identical (zero-cost gate below).
The derived closure takes the row's parameters and nothing else, while Node's function may read arguments past them (mkdtemp's options, realpath's encoding), and a slot converts a function with fewer parameters by dropping the extra arguments. So the value exists only where the read's slot bounds what a call passes (derivedValueSiteUse):
- a typed slot whose every call signature passes at most the row's parameters, or a read that never calls the value (
===,typeof,.name), takes the value; - a typed slot that passes more is the direct call with those arguments, which the row does not lower: it refuses by name with both counts;
- an
any/unknownor inferred slot, a JavaScript source, and the dynamic module object keep the refusal they have on main.
Rows without a value keep the by-name refusal where a source-level function type cannot spell the shape or the value would bypass call-site validation:
CALL_SHAPE_MEMBERS— members whose call lowering reads the call node (thereadFileSyncutf8 fence,statSync's Dirent option,setPriority's argument completion, …);- rest packs (
path.join), omitted-argument defaults, checked-dynamic forms,fscallback dispatchers (fs.read), and any row whose parameter or result is dynamic; - modules whose call lowering lives in their own dispatcher (crypto, zlib, http, stream, …);
- rows with an explicit
valueParamscontract are left exactly as they were — one representation decides each adapter.
Target: pi ls.ts:45 (stat: fsStat in the tool's record literal) now type-checks and takes its value from the row; the record it is written into still needs the class-to-interface view tracked as decision 2/3b (see below).
Evidence
tests/corpus/host-global-extends-class.ts— differential: class value shape, identity, statics, subclass; the unconstructible constructors stay in a branch native code never takes.tests/corpus/builtin-derived-function-values.ts— differential: identity (stat === fsp.stat,ops.stat === stat), calls through the value (sync and awaited async), passing the value as a callback argument, Node'sname.tests/diagnostics/host-global-extends-construction.ts+ snapshot — bothnewsites reportSC2020.tests/diagnostics/host-global-extends-inherited-static.ts+ snapshot — inherited statics through the subclass, a further subclass, an alias and a call reportSC2020; an accessor static names the member; the program's own statics compile.tests/diagnostics/builtin-derived-value-slot-arity.ts+ snapshot — a derived value refuses in a parameter slot and a field slot that pass a second argument, and in anunknownslot.tests/harness/host-global-dynamic.test.ts— the runtime fence for a dynamic construction (and a subclass of the subclass), plus theinstanceof Functionanswer below.packages/compiler/src/frontend/lowering/builtins/module-call-values.test.ts— the derivation's decision table: every shape-validated module derives, every exclusion names a real member, representative rows derive their ABI and refuse the shapes a source-level type cannot spell.
Gates
All on head 0dc6891a9 unless a line says otherwise.
pnpm lint,pnpm format:check: clean.pnpm test:ts7 --baselines-only: "Every TypeScript preflight/order fixture has a recorded baseline." The five new fixtures' baselines are in this branch (one module in order, no preflight diagnostics each), and the order-parity batches overtests/diagnostics/pass (15/15).- focused vitest: unit test 8/8, host-global harness 12/12 (plain and
SCRIPTC_SAN=1), the diagnostics corpus 299/299, the two corpus programs plus the neighbouring function-value and class-extends programs (34 plain), both corpus programs sanitized. - zero-cost gate (
bin/validate --skip=lanes,bench,t262,san-bootstrap --gate-corpus=all, based44d6b7a3): PASS. 5175 identical, 95 both-fail, no differing program and no runtime-only row. Bench geomean +0.08% [95% CI -0.53%, +1.37%], fixtures -0.76% [-1.73%, +0.20%], both within tolerance; self +0.45% [-0.93%, +2.31%], which the tool reports as inconclusive at this run count. The candidate compiler source builds with the candidate (689.6 s). - native bootstraps (
tests/harness/self-hosting-native-driver.test.ts, 32 vCPU / 64 GB Sandbox): both pass on0dc6891a9. Plain, all contracts: 2,312 s (seed 890 s, command probes 368 s, self-rebuild 700 s, Node/native IR and LLVM comparison 62 s, rebuilt compiler emits itself 718 s).SCRIPTC_SAN=1, all contracts: 5,331 s (seed 1,362 s, command probes 860 s, sanitized frontend lowers the compiler 3,711 s, sanitized emitter rebuilds it 3,962 s, IR and LLVM comparisons 77 s and 7 s). - pi (
scriptc coveragethroughcov.sh, 32 vCPU, main at the merge base against this branch): below.
pi delta
scriptc coverage over pi's native entry, rpc entry, and codemode, base (main) vs this branch:
| report | statements analyzed | compiled statically | diagnostics |
|---|---|---|---|
| entry, before | 55288 | 55117 (99%) | SC1090 67, SC2020 29, others unchanged |
| entry, after | 55296 | 55127 (99%) | SC1090 65, SC2020 30, others unchanged |
| rpc, before | 55271 | 55100 (99%) | same as entry |
| rpc, after | 55279 | 55110 (99%) | same as entry |
| codemode | 1751 | 1747 (99%) | unchanged |
Both target sites clear:
packages/ai/src/api/openai-codex-responses.ts:1017:44—SC1090 extending classes not declared in the program ('WebSocket')is gone; theclass extends WebSocketvalue now compiles.packages/coding-agent/src/core/tools/ls.ts:45:8—SC1090 library functions as values (call 'fsStat' directly)is gone;stat: fsStatin the record literal now takes the derived value.
The numbers are the same on 0ded4f73b and on 0dc6891a9; the review fixes change no pi site.
Each of those statements then reports its next fence, and the stacked PR clears both:
ls.ts:46:11:library functions as values (call 'fsReaddir' directly).fs/promises.readdiris listed as a call-shape row here.openai-codex-responses.ts:1027:6:SC2020 'string | URL.toString' … has no scriptc lowering yet, inside the now-compiled class's constructor.
ls.ts:45 compiles but is not usable yet: the slot's return type is the structural { isDirectory: () => boolean }, Stats has no view as that record, and the call through the slot throws a TypeError that names both types. That needs the class-to-structural view (decision 2/3b family), not this PR.
Net: two fewer uncompiled statements in the entry report (171 to 169), ten more compiled statically, and the only site-level change in either direction is those three lines.
Known limitations (deliberate, not regressions)
-
A derived value stored in a slot with the row's parameters and then assigned from that slot to a wider one loses the extra argument:
const ops: { make: (prefix: string) => Promise<string> } = { make: mkdtemp }followed byconst wide: (prefix: string, options: { encoding: "hex" }) => Promise<string> = ops.make. The second conversion wraps an opaque function, and main drops the argument the same way for a program function with an optional parameter. It needs the general fix and is recorded as a follow-up; the one-step forms refuse (above). -
Sock instanceof Functionanswersfalsenatively where Node (whose class chain reachesFunction.prototype) answerstrue. The stacked PR folds the answer for these classes.instanceofuses the.prototypewalk, and a class-built value does not carry that property towardFunction.prototype. Fixing it needs either a static fold (which would break byte-identical IR for typed programs) or a runtime[[Prototype]]change; the harness test pins today's answer so the divergence stays visible.instanceof WebSocketanswersfalse, as it does for every value native code produces. -
A derived value's
lengthis its required-parameter count, so a row that omits Node's trailing options (fs/promises.realpath,fs/promises.mkdtemp) reports a smallerlengththan Node. Typed code cannot read.lengthof a function value today (SC2020), so this is unobservable in type-checked source. -
docs/content/limitations/builtin-function-values.mdxlists a hand-picked subset of filesystem function values; it is intentionally left narrower than what the derivation now accepts. Refreshing it belongs with the compatibility-ledger update that names the new evidence.
🤖 Generated with Claude Code
The latest updates on your projects. Learn more about Vercel for GitHub.