Commit Graph
2648 Commits
Author SHA1 Message Date
Chris Tate dd2444fbb6 Keep React Flight's development-only stack parsing out of production builds, and make stack mode cheaper (#1330) 2026-10-18 18:01:06 -05:00
Chris Tate 6dff370714 Compile inline require(), computed and chained CommonJS exports, and a module's own exports object (#1329) 2026-10-18 17:45:15 -05:00
Chris Tate 5b0c0f66b9 Compile classes that extend a generic class factory's result (#1335) 2026-10-18 17:38:32 -05:00
Chris Tate 33bed3118c Short-circuit optional chain tails over unchecked reads (#1334) 2026-10-18 17:37:15 -05:00
Chris Tate 4c27ab8bd7 Compile classes that extend a generic class factory's result
`class Counter extends base(42) {}` with `function base<T>(initial: T) {
return class { value: T = initial; }; }` failed with SC1090 "extending
computed expressions": the call's result type is the factory's local class
instantiated with T = number, which only maps inside the factory's own
generic instance.

A call of a generic function whose result is one of its local classes (or
an instance of one) now keys the instance on its type bindings and maps the
result inside that instance, where the local class takes the instance's
name and bindings. The class value then works as a base (`extends
base(42)`), as a `const` binding, through `new`, and for method calls and
field reads on its instances. Each instantiation has its own class, so
`base(1) === base(1)` is false and `instanceof` tells them apart, as in
Node. Calls inside another generic instance bind through the declaration;
calls elsewhere also record the instantiated checker type, so later uses of
it name the same class.

Fixes #1241
Co-authored-by: BaoLei <50226652+blurname@users.noreply.github.com>
2026-10-18 17:17:22 -05:00
Chris Tate 238d782fdc Keep the CommonJS caches on their owners and recount 6110's fences
The exports-assignment cache moves from a module-level WeakMap onto the
Lowerer (cjsExportsAssignments), and the inline require() calls that close
a require cycle move from a module-level WeakSet into preflight's result:
checkPreflight stores them on the LoadResult, and lowering receives them
with the other preflight facts (LowerOptions.inlineRequireCycleCalls).

6110's call and apply wrappers now compile `root === module.exports` and
`typeof module.exports`, which print what Node prints when the wrappers'
untaken branches run. The class base's `console.log(module.exports)` is the
one statement left as a runtime fence, and its body is lowered once, so
the program's deferred-fence count is 1.
2026-10-18 17:17:00 -05:00
Malte UblandClaude Opus 5.5 0e9e908942 Short-circuit optional chain tails over unchecked reads
`a[0]?.b.c` read `.c` from undefined when the element was missing: the
tail only joined the guard when the checker typed the receiver nullable,
and an array or index-signature read is typed present while it lowers to
an undefined-armed value. The tail check now also accepts receivers that
can lower to a hidden undefined; present receivers re-dispatch the plain
lowering. A non-null assertion stays inside the chain (`a?.b!.c`), and
parentheses end it for checked-dynamic reads (`(a?.b).c` throws).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:13:17 -07:00
Chris Tate 64e3ecdc99 Pin frame records that are arms of record unions in the corpus (#1332) 2026-10-18 17:05:23 -05:00
Chris Tate 3b0ffb24e4 Compile overloaded methods on interfaces and object types (#1333) 2026-10-18 17:01:44 -05:00
Chris Tate 961d27802f Make selected npm packages join the type-checked program (#1331) 2026-10-18 16:57:35 -05:00
Chris Tate 385465ff24 Remove dynamic-path leaks from typed code (two-tier stage S1) (#1326) 2026-10-18 16:47:13 -05:00
Malte Ubl 59edd160aa Merge remote-tracking branch 'origin/main' into opt/flight-stacks
# Conflicts:
#	packages/compiler/src/backend/llvm/emitter.ts
2026-10-18 14:46:05 -07:00
Malte UblandClaude Opus 5.5 f4daefa6be Compile overloaded methods on interfaces and object types
An interface or type-literal member with several call signatures (the `on("event", handler)` registry shape, arity overloads, type overloads sharing one implementation) failed its whole record shape: the type mapper maps only single-signature function types, so the member mapped to nothing and every type mentioning the interface failed with it.

JavaScript stores one function under such a member; overloads only constrain call sites. The member now keeps a record slot holding that function. Its type joins the overloads per parameter position and result: equal types stay, scalar mixes form their union, and other mixes travel as checked `unknown` values when every joined type crosses that boundary both ways (differing callbacks become checked function values that any implementation's callback type adapts from). Positions some overloads omit are optional, and an overload's own type parameters erase to `unknown`. Rest parameters and a void result beside a value result keep the member fenced, now with a diagnostic naming the overload and parameter that block it.

Calls convert their arguments into the slot and the result back to the resolved overload's type, on record and union receivers, through optional chains, and on call results. Object-literal methods find their member slot through the literal's contextual type, so an implementation with optional parameters fills an arity overload set (and a single-signature member's slot). Value reads of the member keep SC2007. Library, declaration-file and class overload declarations are unchanged.

The zero-cost gate passes against the parent commit with all 2379 programs identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:40:40 -07:00
Malte UblandClaude Opus 5.5 6eb11bf7f4 Pin frame records that are arms of record unions in the corpus
A record shape that is an arm of a union of records carries a hidden
shape word at struct index 1, so its fields start at index 2. The new
program keeps such records in frame storage while they own string and
array fields, reads and writes those fields, passes the shapes through
the union, compares identities and serializes them. With the frame
storage's reference-field slots addressed at index + 1 instead of the
shape's field base, the compiled program segfaults in the plain lane
and ASan reports a heap-buffer-overflow in the sanitized one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:40:37 -07:00
Chris Tate d0182e59c8 Merge main 2026-10-18 16:28:52 -05:00
Chris Tate f5eb459888 Unblock common npm packages: type-only imports, @types packages, punycode and constants (#1285) 2026-10-18 16:25:22 -05:00
Chris Tate db0066c542 Record corpus costs for 6110 and explain its deferred-fence count (#1328) 2026-10-18 16:13:03 -05:00
Chris Tate 66027d850b Explain 6110's deferred-fence count and record its corpus costs
Corpus 6110 has three statements with no static lowering, and its
@deferred-fences directive is 9 because coverage counts each lowering of a
wrapper body: the .call and .apply wrapper bodies are each lowered four
times. The program's header now says so. The commit also adds the
program's corpus-costs entries, which shard balancing reads.
2026-10-18 15:53:37 -05:00
Chris Tate ea7bcd9846 Resolve builtin classes through const aliases (#1327) 2026-10-18 15:42:15 -05:00
Malte UblandClaude Opus 5.5 e90c77d36a Make selected npm packages join the type-checked program
scriptc's resolver and tsgo must answer one runtime file for every
opted-in npm edge; four disagreements left the file outside tsgo's
program (SC2013 "did not join the type-checked program") for express,
axios, body-parser, execa, fast-glob, globby, knex, uuid and tldts:

- uuid: the node-condition hoist re-added the inner "types" key after
  the claims were deleted; tsgo then took the browser build. Delete the
  claims after the hoist.
- tldts: tsgo's TypeScript-first lookup took the shipped root index.ts
  over "main". The fs shadow hides installed TypeScript sources of
  opted-in packages that have no same-stem runtime JavaScript.
- express: tsgo redirected same-version duplicate installs to one copy,
  either dropping a copy or merging two Node module instances into one.
  The shadow serves opted-in package.json files without "version".
- deep chains (get-intrinsic, dunder-proto, run-parallel, reusify,
  supports-color): maxNodeModuleJsDepth charges every node_modules hop.
  The program-root fixpoint also roots opted-in bare edges' runtime
  files that the program is missing.

All changes are gated on --npm-static being active.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 13:40:18 -07:00
Malte UblandClaude Opus 5.5 87b85d5b28 Name the weak collection constructors whole in the dispose gate
The runtime ABI test reads every @scr_* spelling in the backend sources
as a symbol the backend can emit; the gate's regex spelled a truncated
@scr_weak_ prefix. Test the two declarations by their full names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 13:34:43 -07:00
Malte Ubl 0620e30501 Merge origin/main into opt/tier-s1-leaks 2026-10-18 13:31:56 -07:00
Malte UblandClaude Opus 5.5 78100a4124 Record ts7 order baselines for the new CommonJS corpus programs
A full SCRIPTC_UPDATE_BASELINES=1 re-record of order-parity.test.ts changed no existing baseline; these are the seven new fixtures (6150-6156). 6150's order places late.js after main.js, the lazy inline-require edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 13:29:28 -07:00
Malte UblandClaude Opus 5.5 fbcdb1d641 Compile inline require(), computed and chained CommonJS exports, and a module's own exports object
The npm compatibility probes ranked these CommonJS forms among the top blockers. They now compile with Node's semantics inside the static CommonJS model, where exports stay module globals or alias plumbing to declarations. Every remaining form keeps a refusal that names the construct.

- Inline require() of program modules (cjs-inline-require.ts). `require("./x").f`, `.f(...)` and `new (require("./x").C)()` over a table module run the module's run-once %init at the call's position, then resolve the member like a binding does. `require("./x")` as a value over a single-value module is that value (`require("debug")("ns")`, table entries, construction). Inline requires are preflight module edges. An edge in a function body is lazy: it orders the dependency after the requiring module and does not close initialization cycles. Other require() calls name their construct instead of reporting an unlowerable 'Require' value. A package that nothing installed resolves throws MODULE_NOT_FOUND.
- module.exports = require("./y") or a require binding (re-export), = TypeError (a builtin global), and = any computed value (`a || b`, ternary, `new X()`). Chained `exports.a = exports.b = v` evaluates v once. Export members assigned in value position (`const src = exports.src = []`) or inside functions write the export global. A fresh `exports = module.exports = {}` reset at the top of a module is a no-op. Any other rebinding of `exports` is refused by name.
- A module's own exports object (cjs-exports-object.ts). Identity comparisons, typeof and truthiness of `exports` / `module.exports` use the object that top-level `this` already represents, while the module keeps its original exports. This covers the `root === module.exports` statement that used to vanish from `.call(this)` wrappers. `exports.f(x)` calls the member's export global when no assigned value can read `this`. Other value uses are refused by name, including in CommonJS files where the message used to say "ES module".
- tsc interop (cjs-tsc-interop.ts). `const x_1 = __importDefault(require(...))` and `__importStar(...)` are require bindings, stamped and unstamped. The helper declarations lower to nothing once every call is such a binding.
- Export tables: method entries compile, and `...require("./x")` spread entries now evaluate x at their place. Before, the module was never initialized.
- TypeScript reads of members a JavaScript module attached to an exported function go through the function's own property table.
- Nested require destructuring of program modules, `const { codes: { E } } = require("./errors")`, destructures the named export at the require (SC1012). `const { A } = ns` over a module binding snapshots the export.

The capture-buffer fence tests merged from opt/cjs-exports-value used `root === module.exports`, which now compiles. They use eval() to keep testing the fence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 13:29:28 -07:00
Chris Tate 3be66127c2 Fence refused statements inside capture buffers instead of dropping them (#1322) 2026-10-18 15:24:10 -05:00
Chris Tate 547e59529f Resolve builtin classes through const aliases
`const E = EventEmitter`, `const M = Map`, `const T = TypeError` and
`const R = Readable` stopped at SC2020, and so did `typeof EventEmitter`,
`Readable.name`, `Map.name`, `Error.name` and `===` between builtin
classes. The zlib class alias resolver now covers the error classes,
EventEmitter, the node:stream classes and Map/Set: a const binding of one
(an alias of an alias, or an export used through a named or namespace
import) constructs, extends and tests instanceof like the direct name,
and typeof, .name and === fold for aliases and direct names.

In TypeScript files the alias owns no runtime slot and other value uses
report SC1090 at the use. In JavaScript files a copy of a name or of a
globalThis property keeps its slot, which holds the same value as the
name it copies; builtin module members (`zlib.Gzip`) own none, as before.
2026-10-18 15:23:20 -05:00
Malte UblandClaude Opus 5.5 a2707ac8d6 Name the inner location in package-anchored npm-static refusals
A package-anchored preflight diagnostic that makes the build drop an npm package was reported only at the consumer's import, as `SC2013 cannot compile npm package 'ws' statically: SC1012: …`, with the package file and line lost. The refusal reason now carries them (`SC1012 at ws/lib/websocket.js:30: …`), in the SC2013 diagnostic and in the coverage report's package status. This is how the SC1012 construct behind ws, readable-stream, through2, socket.io and fastify was identified (nested require patterns, and a rest element in fastify), and how openai's SC1013 is located (`openai/internal/auth/x509-transport-state.mjs:2`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 13:07:01 -07:00
Malte UblandClaude Opus 5.5 c20c21b22c Declare the deferred fences of 6110 for the coverage contract
The corpus program deliberately carries refused statements on untaken paths; the `// @deferred-fences: 9` header is the coverage sweep's exemption for such programs (9 failed statements across the speculative and final lowerings), so it still runs against Node in the differential oracle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 13:05:47 -07:00
Chris Tate b866ae3236 Report refused local classes from speculative type queries again (#1321) 2026-10-18 15:04:40 -05:00
Malte UblandClaude Opus 5.5 147013d7b9 Merge origin/main into opt/npm-small-fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:58:58 -07:00
Malte UblandClaude Opus 5.5 40c56b96ce Merge origin/main into opt/npm-small-fixes
Keep both the entry module's DEP0040 link warning and #1303's CommonJS
top-level this object in the module init header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:58:53 -07:00
Malte UblandClaude Opus 5.5 26b703a821 Keep React Flight's development-only stack parsing out of production builds
React Flight's production server never runs its V8 structured-stack parser
(ReactFlightStackConfigV8): every use is behind __DEV__. Builds compiled from
its sources keep that code behind `throw new Error('... should never be
called in production mode')` stubs, so scriptc lowered it, failed on
Error.prepareStackTrace, and a dead `.stack` read turned on stack-trace mode
(a frame push in every function) for the whole program.

- Statements after return/throw/break/continue in a function body are not
  lowered. Declarations keep their hoisting through the existing forward
  machinery (later function declarations, `var` reads undefined, `let`/
  `const` TDZ); class, enum, module, import/export and generic function
  declarations still lower. Functions referenced only from such code are
  unreached and dead-stripped.
- Stack-trace mode requires a stack read reachable through live code
  (observesStacks).
- Synchronous functions in executables push and pop their source frame
  inline instead of calling scr_stack_enter/leave.
- Functions that only perform operations which never create or throw an
  Error, and only call such functions, keep no frame (captureFreeFunctions):
  no capture can include them.

Error.prepareStackTrace and CallSite stay refused: only development Flight
runs them. Good code is byte-identical (zero-cost gate: bench and
self-hosted compiler IR, LLVM and executables).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:44:31 -07:00
Chris Tate 1c1400272f Keep the compiler's kind and code methods direct (#1317) 2026-10-18 14:43:02 -05:00
Chris Tate 42591b1eed Read zlib options held in typed objects (#1316) 2026-10-18 14:24:54 -05:00
Chris Tate 470d317703 Keep class methods named kind and code direct in the compiler
resolveInferredReturn turns a return statement into a runtime fence
through a Record<string, unknown> view with element writes
(`mutable["kind"] = ...`). The class method census treats a literal
element write as a possible write to every method of that name, so in the
native compiler every method named `kind` or `code` became a dynamic
callback: AstWireFile.kind, called for each AST node access, went through
a property bag and a method value.

Property writes go through the receiver check instead, and no class
instance is assignable to Record<string, unknown>, so those methods are
called directly again. Output of the compiler is unchanged.
2026-10-18 14:21:32 -05:00
Chris Tate 240f576805 Add execution tiers to the IR and a report-only tier analysis (#1315) 2026-10-18 14:08:12 -05:00
Malte UblandClaude Opus 5.5 5746dc30c5 Expect the compile to refuse stream-shims and node24-file-url-conversion again
With the local-class refusal reported again, both npm fixtures fail to compile with SC1090 as they did before #1303; restore their build expectations. The deleted corpus program 6102 also leaves the measured cost table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:05:40 -07:00
Malte UblandClaude Opus 5.5 f649959609 Report refused local classes from speculative type queries again
The previous change dropped the diagnostics of a function-local class refused during a speculative whole-program type query, expecting the class's own declaration to report them. In JavaScript that declaration only defers them to a runtime fence, so two npm programs that the compile refused (stream-shims and node24-file-url-conversion, both SC1090) began to compile and diverged from Node at run time. The speculative query now leaves the refusal reported, as it was wherever such an analysis already caught the poison, and only stops the poison from escaping the compiler.

The JavaScript crash repro becomes a diagnostics fixture (local-class-type-query-js.js); the corpus program 6102 never crashed the base compiler and is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:05:25 -07:00
Malte Ubl a273999cb0 Merge branch 'opt/cjs-exports-value' into opt/cjs-exports-model 2026-10-18 12:05:24 -07:00
Malte UblandClaude Opus 5.5 8531a45509 Record baselines for the capture-buffer fence fixtures
The diagnostics snapshot for speculative-function-refusal.ts and the ts7 order-parity baselines for it and 6110-cjs-speculative-wrapper-fences.cjs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:04:28 -07:00
Chris Tate d9f0005ecb Represent unions of records as the record pointer itself (#1225) 2026-10-18 14:04:16 -05:00
Chris Tate a5548597e5 Read zlib options held in typed objects
A zlib options object that reaches the runtime as a typed reference (a
cast object literal such as `{ level: 0 } as zlib.ZlibOptions`, which the
checked-dynamic options slot holds by reference) was not a plain
checked-dynamic object, so the options reader found no keys: every option
was ignored and out-of-range values were not reported. The sync, callback
and promisified one-shots and the stream constructors compressed with the
defaults instead.

scr_zlib_options_parse and scr_zlib_stream_new now read a typed reference
through its materialized view, as the blob and fetch option readers do, so
the options apply and invalid ones throw Node's ERR_OUT_OF_RANGE.
2026-10-18 14:02:03 -05:00
Chris Tate 329ea4387e Convert a Date to local time once for its local getters and follow TZ changes (#1313) 2026-10-18 13:57:42 -05:00
Malte UblandClaude Opus 5.5 b0ce48f168 Fence refused statements inside capture buffers instead of dropping them
A refused statement in a JavaScript function body was dropped without a diagnostic or runtime fence when the body lowered inside a capture buffer whose owner kept the result. tryLowerExpression lowers the receiver of `(function () { ... }).call(this)` or `.apply(...)` and a computed class base that way: the refused statement (for example `console.log(root === module.exports)`) was missing from the kept function, its diagnostic was handed back to the enclosing statement, and that statement, having lowered cleanly, filed it as an informational runtime-fence entry. The program printed nothing for that statement.

lowerStmts now emits the statement's runtime fence inside a buffer as well (Lowerer.capturedRuntimeFence, the same message deferToRuntimeFence would give), leaving the diagnostics with the buffer's owner. The three JavaScript class initializer sites that skipped their fence under a buffer (static field, instance field, static initializer block) do the same. An owner that discards the candidate discards the fence with it, so every program that compiled before keeps byte-identical IR unless it contained such a dropped statement: all 3006 corpus programs produce the same IR as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 11:47:04 -07:00
Malte UblandClaude Opus 5.5 fa64c19fdc Build the coverage result without a conditional spread over its tiers
The self-hosted compiler refuses a conditional spread of `tiers` over a
LowerResult that already declares the field, so assign it instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 11:45:52 -07:00
Malte UblandClaude Opus 5.5 8597a364f6 Merge origin/main into opt/npm-small-fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 11:45:47 -07:00
Chris Tate 2d9604eff5 Fix get-or-create records, local-class type queries, builtin typeof and CommonJS top-level this (#1303) 2026-10-18 13:44:35 -05:00
Chris Tate e5c45f8253 Merge main 2026-10-18 13:41:18 -05:00
Chris Tate f80eb5fc39 Give every arm of record unions that share arms a collector header
A shape union's arms all carry a collector header when one of them does.
The emitter applied that rule in one pass over the unions in module order,
so a header that a later union gave a shared arm did not reach the other
arms of an earlier union. With First = A | B and Second = B | C, where only
C is cyclic, B got a header through Second, First's trace helper then
called A's trace function, and A had none: the module failed LLVM
verification with "use of undefined value '@sc_rtrace_r0'".

The rule now repeats until no union adds a header. Corpus
5919-record-unions-shared-arms chains Third = D | A, First = A | B and
Second = B | C, so the requirement passes from Second through First to
Third.
2026-10-18 13:39:12 -05:00
Chris Tate fe7b1cd199 Convert a Date to local time once for its local getters and follow TZ changes
Each local-time getter (getFullYear, getMonth, getDate, getHours, ...,
getTimezoneOffset) called localtime_r and walked the calendar for its
own field. The thread now keeps its last local conversion, keyed by the
instant and a timezone generation, so reading several local fields of
one Date converts it once. The UTC walk and the second computation use
integer division instead of floor.

Writing or deleting process.env.TZ calls tzset and starts a new
generation, so later local-time reads use the new zone, as in Node.
Before, the zone the process started with stayed in effect.
2026-10-18 13:33:41 -05:00