Commit Graph
6750 Commits
Author SHA1 Message Date
Malte UblandClaude Opus 5.5 daf4ff6a2b Decode and validate the WebAssembly modules a program can load, and report them in coverage
`scriptc coverage` now lists every WebAssembly module whose bytes the
build can see, with its size, whether it validates, its function, import
and export counts, and the WebAssembly proposals it uses. Nothing is
translated yet; builds are unchanged.

- packages/compiler/src/wasm/binary.ts decodes and validates a module in
  plain TypeScript (the native compiler compiles it too) at the feature
  level scriptc means to translate: the MVP plus sign-extension,
  non-trapping float-to-int, bulk memory, multi-value, mutable globals,
  reference types and extended constant expressions. Where WebAssembly
  3.0 and V8 relaxed a 2.0 rule (global initializers reading defined
  globals, memory indices), it follows V8, the behavior oracle. Modules
  that use SIMD, threads, exception handling, tail calls, function
  references or GC, memory64 or multiple memories still decode, and are
  reported as needing that proposal instead of as invalid.
- packages/compiler/src/wasm/discover.ts finds modules from four sources:
  paths the compiler resolves from the source text (require.resolve,
  createRequire(...).resolve, import.meta.resolve,
  new URL(..., import.meta.url), __dirname joins), inline base64 and byte-array
  literals, the .wasm files of reached packages whose code mentions
  WebAssembly, and a `scriptc.wasm` list in package.json or the new
  coverage-only `--wasm <file>` flag. Modules are reported once, by
  SHA-256, with every source that found them; module files are input
  probes of the coverage verdict cache. The JSON envelope
  (--print=diagnostics) carries the same records under `wasmModules`.

Evidence: packages/compiler/test/wasm-spec.test.ts runs the decoder over
the 3,017 distinct binary modules of the WebAssembly 2.0 core test suite
(without SIMD) and the extended-const proposal's tests, generated by
gen-wasm-spec-cases.mjs with wabt's wast2json, and requires V8's verdict
for each; 8,000 seeded mutations of those modules must also agree with
V8, as must undici's inline llhttp modules (scalar valid, SIMD reported)
and es-module-lexer's. src/wasm/binary.test.ts checks each feature and
each error class against V8. On the survey's real modules the decoder
agrees with V8 for quickjs-wasi (637 KB, 36 ms) and photon (1.9 MB,
121 ms). tests/coverage-fixtures/wasm pins the four discovery sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:19:39 -07:00
Malte UblandClaude Opus 5.5 c7f3fa6564 Type-check programs that mention WebAssembly and refuse its API precisely
Packages whose declarations name WebAssembly types (quickjs-wasi,
photon-node, and therefore pi) failed scriptc's type check with SC0001
"Cannot find namespace 'WebAssembly'", because scriptc's ES-only type
world never loads lib.dom, so `scriptc coverage` could not analyze them.

- scriptc.d.ts declares lib.dom's WebAssembly namespace and BufferSource
  (admitting every ArrayBufferView, so Node's Buffer types fit).
- The type mapper gives every type declared in the namespace, and unions
  over one, the checked-dynamic representation, so declarations that only
  mention them compile.
- Every reached use of the API value refuses at its site with a named
  SC2020 diagnostic: `WebAssembly.<member>` reads and calls,
  `new WebAssembly.<Class>`, `instanceof WebAssembly.<Class>`, the bare
  namespace, `globalThis.WebAssembly`, and the global object viewed
  through a cast to a type with a WebAssembly member (pi codemode's
  spelling). TypeScript sources fail the build; JavaScript sources get a
  located runtime fence.
- `typeof WebAssembly` and `typeof WebAssembly.<member>` need no value and
  fold to Node's answers ("object", "function"), including inside
  `typeof x === "..."` tests, so feature probes in JavaScript glue behave
  as in Node. Before, a JavaScript probe answered "string" (the identity
  token) and silently took the non-wasm branch.
- The surface manifest projects the refusal (stdlib.webassembly), and the
  compatibility ledger's `WebAssembly` global row moves from
  not-implemented to refused; no other row changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:19:38 -07:00
Malte UblandClaude Opus 5.5 4b78531ab4 Size handle tables by a fixed capacity and let handles cross threads
Appending a ScrDynHandleTag used to grow the always-linked scr_dynh_ops
table, which shifted .bss and code addresses in every program, including
programs that never box the new handle (Blob and File paid this when
they were added). The table is now sized by SCR_DYNH_CAPACITY (128; 52
tags are in use), so a link-gated unit can add a tag without changing
any other program's executable. This change is the one layout shift that
makes later tags free.

Structured clone between threads (postMessage, workerData) rejected
every handle other than MessagePorts and buffers. A handle's ops can now
carry a ScrDynHandleTransfer (share on the sender, adopt on the
receiver, drop with the message), which scr_message.c uses for any tag,
so a gated unit such as a future WebAssembly.Module needs no reference
from the message encoder and no per-tag registry. No handle declares the
hooks yet; scr_message.test.c exercises them across threads through a
test handle in the last capacity slot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:19:38 -07:00
Malte UblandClaude Opus 5.5 d3bfa69323 Coerce the value passed to a JavaScript setter that overrides Error.name or message
A JavaScript class that extends Error and declares `set name(v)` or
`set message(v)` (quickjs-wasi's JSException) made the Error property
write dispatcher, which exists once any program writes `.name` or
`.message` on an Error, pass a string where the untyped setter takes a
dynamic value. IR validation failed with SC9001 "expected dyn, got
string". The dispatcher now coerces the value to the setter's parameter
type, which leaves TypeScript setters (string parameters) unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:19:37 -07: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 8e457b2b05 Record ts7 preflight/order baselines for the new corpus programs
pnpm test:ts7 --baselines-only reported the fifteen new corpus programs as
missing. Recorded with SCRIPTC_UPDATE_BASELINES=1 from the native frontend;
every entry has no preflight diagnostics and a single-module order, and no
existing baseline changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:20 -07:00
Malte UblandClaude Opus 5.5 d84e410981 Upcast devirtualized calls whose override returns a subclass
A base method calling `this.set()` where only a subclass is constructed
devirtualizes to the subclass override. With a polymorphic `this` return
type, the override's lowered return type is the subclass object while the
site keeps the base method's type, and the validator reported an internal
compiler error (SC9001: "call %Sub.set type object != return object"). The
Next.js extract hit this once its placeholder ResponseCookies instance was
gone: MiddlewareResponseCookies became the only constructed class.

The direct call now carries the callee's call-site return type and upcasts
to the site's class; any other mismatch keeps the virtual call. Calls whose
types already matched are emitted exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:20 -07:00
Malte UblandClaude Opus 5.5 3149e7be91 Regenerate the surface manifest
Date.prototype.toUTCString and the static Response.json operation are
static surfaces now (pnpm manifest). pnpm node-compat leaves the Node
ledger and the public artifact unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:19 -07:00
Malte UblandClaude Opus 5.5 b831d840d7 Accept getters that never return a value
A getter that only throws (`get page(): never`, or Next.js's unannotated
removed-API getters) or has no return statement declared a VOID property
type, so the class was refused (SC2001). No read of such a getter produces
a value other than undefined; the property now takes the checked-dynamic
slot, and each read throws the getter's error or answers undefined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:19 -07:00
Malte UblandClaude Opus 5.5 cd63b935e0 Instantiate generics with a literal undefined argument
`id(undefined)` binds T = undefined, which the type mapper answers as VOID
(the return convention), so the instance's parameter was refused (SC2001)
although a declared `v: undefined` parameter compiles. Instantiated
parameters are value positions and now take the unit-only slot, as
declared ones do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:18 -07:00
Malte UblandClaude Opus 5.5 dd0c255fa7 Stringify class instances that declare toJSON
`JSON.stringify(new Url(...))` was refused (SC1090) for every program
class, although JSON.stringify of a class with a toJSON hook serializes
whatever toJSON(key) returns. A class whose chain declares toJSON now
crosses into the runtime JSON walker through its checked-dynamic class
view, which calls the hook with the root key "" and handles nested hooks;
an undefined hook result keeps the dyn-root undefined rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:18 -07:00
Malte UblandClaude Opus 5.5 a8b7bafefb Lower ...(cond && { k: v }) spreads as conditional fields
Spreading a falsy primitive copies nothing, so `...(cond && { k: v })` is
the optional-field idiom `...(cond ? { k: v } : {})`. The `&&` spelling
(the @edge-runtime/cookies parseSetCookie shape) was refused with a record
shape mismatch (SC2002).

When the spread's type mixes the record with primitive arms, the literal
lowering now uses the existing conditional-spread desugar: cond evaluates
once, v only when taken, and the absent arm leaves the optional field
undefined. Record and record-or-unit spreads keep the source-copy path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:18 -07:00
Malte UblandClaude Opus 5.5 e6dbad9567 Destructure Maps and Sets with array patterns
`const [[k, v], ...rest] = map` was refused (SC1031). The source now
snapshots once into its iteration order (Map entries as pairs, Set
values), the spread's own machinery, and the pattern lowers as an array
pattern over that snapshot, as string sources already do. Element defaults
keep a fence because they run between iterator steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:17 -07:00
Malte UblandClaude Opus 5.5 30e23af63f Store fetch Headers, Request, and Response handles in class fields
A class field typed `Headers`, `Request`, or `Response` (or an array or
nullable form of them) maps to checked-dynamic storage, and the class-field
rule refused it (SC1090) although `unknown`, Map values, and closures
already store the same native handles.

Such fields now take the checked-dynamic slot, like the worker handles;
reads keep the handle's identity and the static fetch member lowering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:17 -07:00
Malte UblandClaude Opus 5.5 6c1af69d03 Add static Response.json
`Response.json(data, init)` was refused (SC2020). It now builds a native
Response the way undici does: ResponseInit converts, the data serializes
through the checked-dynamic JSON serializer (toJSON hooks included;
TypeError "Value is not JSON serializable" when JSON.stringify answers
undefined), then the response initializes like the constructor with
"application/json" as the default content type, so status, statusText,
header, and null-body-status errors keep their order.

The fetch compatibility profile moves Response.json from unsupported to a
static operation with the static-response-json differential fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:17 -07:00
Malte UblandClaude Opus 5.5 1fd405c033 Add Date.prototype.toUTCString
`new Date(ms).toUTCString()` was refused (SC2020). It now lowers to a
runtime formatter over the stored time value with V8's output: "Www, DD
Mmm YYYY HH:mm:ss GMT" in UTC, a leading "-" and at least four digits for
negative years, and "Invalid Date" for an invalid time value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:16 -07:00
Malte UblandClaude Opus 5.5 58d3981476 Accept initializer-less symbol-keyed fields assigned in the constructor
A computed TypeScript field without an initializer (`[INTERNALS]: T`)
was refused even with a stable Symbol()/Symbol.for() or literal key, which
contradicts the documented rule that TypeScript fields need an initializer
or an unconditional constructor assignment.

Such a field now takes its declared type and the identifier fields'
definite-assignment rule: an unconditional top-level `this[key] = ...` in
the constructor, otherwise the checked deferred-initialization slot. Types
that admit undefined (optional fields) start undefined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:16 -07:00
Malte UblandClaude Opus 5.5 4d3dde55a7 Return undefined from JSON.stringify of unknown roots where it is observed
JSON.stringify answers undefined for an undefined, function, or symbol
root. Through `unknown` (or a generic instantiated with it) the dyn-root
lowering converted the runtime result to a string, so the result was the
text "undefined": `text === undefined` was false and `typeof text` was
"string".

Where the result reaches a use that distinguishes undefined from that text
(undefined/null comparisons, typeof, ??, ?., truth tests, a return from a
function declaring undefined, or an unannotated local binding used that way), the
call now keeps the checked `string | undefined` result the replacer path
already produces; later string uses of such a binding narrow by the
checker's flow type. Other uses keep the existing string ABI, so programs
that cannot observe the difference compile unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:15 -07:00
Malte UblandClaude Opus 5.5 def4626396 Lower optional calls of always-present library methods as plain calls
`h.getSetCookie?.()` on a native Headers handle failed with SC2020: the
optional-chain lowering first reads the callee as a value, and native
handle methods have no first-class value. A required standard-library
method on a never-nullish receiver always exists, so `?.()` is the plain
call.

When that method read has no lowering, the optional chain now re-dispatches
the plain call. Reads that lower keep the existing path unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:15 -07:00
Malte UblandClaude Opus 5.5 43cefeb56b Retag flow-narrowed unions before primitive toString()
`headers.host.toString()` after `!Array.isArray(headers.host)` narrows a
`string | number | string[]` slot to `string | number`, and the union
toString lowering accepted the narrowed checker type but emitted the toString
node over the wider storage union. The validator rejected the non-stringable
arm as an internal compiler error (SC9001).

The lowering now retags the read to the proven union (the same checked
coercion other narrowed reads use) before the per-union ToString helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:15 -07:00
Malte UblandClaude Opus 5.5 2957a9c042 Report unparsable fetch and Request URLs as "Failed to parse URL from"
undici's Request constructor wraps the URL parser's failure: TypeError
"Failed to parse URL from <input>" with the parser's TypeError as its
non-enumerable cause. fetch() rejected, and new Request() threw, with the
bare "Invalid URL" error instead.

Both paths now replace the pending parser error with the wrapping
TypeError (sf_throw_url_parse_failure).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:14 -07:00
Malte UblandClaude Opus 5.5 46d238fc0a Send repeated Set-Cookie request headers as one combined line
Headers stores each Set-Cookie value separately, but undici dispatches its
header list's combined value: one line with the values joined by ", " at
the position of the first one. fetch() wrote one line per value.

The request serializer now joins later Set-Cookie values into the first
entry and skips the rest, for every HeadersInit form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:14 -07:00
Malte UblandClaude Opus 5.5 77b137268d Visit each Set-Cookie value separately in Headers#forEach
Fetch's "sort and combine" never combines Set-Cookie: Node's Headers
iterator yields one entry per Set-Cookie value, in insertion order, while
get() still joins them with ", ". The native forEach walked the sorted
unique names and read the combined value, so repeated cookies reached the
callback as one joined string.

forEach now indexes the expanded list per step (sf_headers_sorted_entry),
keeping the live re-sort so callback mutations shift later entries as in
Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:16:14 -07:00
Malte Ubl d68db45c93 Merge origin/main into opt/pi-small-fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	packages/runtime/src/scr_async.h
2026-10-18 15:15:21 -07:00
Malte UblandClaude Opus 5.5 c600e8daa5 Delete V8's own stack on Error subclasses with a stack accessor
quickjs-wasi's JSException constructor runs `delete this.stack` so V8's
own stack property stops shadowing the class's stack accessor; scriptc
fenced the delete, so every guest exception reaching the host threw the
fence error. Compiled errors keep the trace internally and class dispatch
already reads the accessor, so the delete now evaluates its receiver and
nothing else, for Error subclasses whose chain declares a stack getter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:15:05 -07:00
Malte UblandClaude Opus 5.5 50235c42c5 Support pi-tui's terminal setup on process streams
Three gaps on pi-tui's terminal start path:
- `if (process.stdin.setRawMode)` read the method as a value (SC2020).
  The read now answers the stream object's member: the method on a TTY,
  undefined otherwise, as on Node.
- process.stdin.setEncoding had no runtime method, and a string 'data'
  listener was fenced. stdin now decodes with the shared StringDecoder
  once per read (split multibyte sequences stay whole, end() flushes the
  tail before 'end'), string listeners receive the text, and dynamic
  listeners receive what Node delivers. A string listener with no
  encoding set throws the setEncoding-state TypeError, as child streams
  do.
- process.stdout.on("resize", cb) (once/off/removeListener, stderr too)
  registers on the stream object, which listens for SIGWINCH and emits
  when a TTY's size changed; the listener never keeps the loop alive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:15:05 -07:00
Malte UblandClaude Opus 5.5 98b3ced80c Resolve bare core-module names to the builtin over npm packages
Node checks its builtins before node_modules for a bare core-module name,
so an installed npm package named string_decoder (a transitive dependency
of pi) never answers `import ... from "string_decoder"`. scriptc
attributed the import to the package and refused it. The npm resolution
now answers null for bare builtin names (prefix-only builtins such as
node:test keep their bare names for packages), for ES imports and CommonJS
requires alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:15:05 -07:00
Malte UblandClaude Opus 5.5 bcc35ddf1a Call chalk.bold from TypeScript
#1309 let a checker-untyped receiver call a member named like a
String.prototype-only method (`bold`, `italics`, ...), refusing only a
string receiver at run time. TypeScript importing chalk's JavaScript
source types the default export as a plain function, so `chalk.bold` is
an `any` member on a typed receiver and still fenced; pi calls it in
nine places. A checker-untyped member now takes the same path. The chalk 5
fixture pins root, aliased, and Chalk-instance `bold` calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:15:04 -07:00
Malte UblandClaude Opus 5.5 71b99913a2 Allow WeakMap- and WeakSet-typed class fields
Weak collections already lower to checked-dynamic storage as locals, but a
class field of those types was fenced as a dynamic-typed field (pi's
AgentSession keys entry ids by message in a WeakMap field). Declared,
optional, and constructor-assigned fields of those types now store the
native collection like the locals do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:15:04 -07:00
Malte UblandClaude Opus 5.5 0bb9b750fe Iterate class instances whose async iterator is an async generator
pi-ai consumes every provider stream with `for await` over an EventStream
whose `async *[Symbol.asyncIterator](): AsyncIterator<T>` is an async
generator method; that was SC1070. The loop now opens the iterator with
the method call and drives the returned async generator like any other
(break and throw close it). An `async *` declaration annotated with the
protocol interfaces (AsyncIterator, AsyncIterableIterator, AsyncIterable)
maps like AsyncGenerator. Boxing a class instance whose fields hold a
promise now links the dynamic async unit (the materializer boxes the
promise), which EventStream's own `this.isComplete(event)` calls need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:15:04 -07:00
Malte UblandClaude Opus 5.5 8a02efaa91 Type process.getBuiltinModule results as module namespaces
Under @types/node, process.getBuiltinModule("node:os") types as the
module namespace, which lowered as an unchecked dynamic value and failed.
A string-literal id naming a supported builtin now lowers to the namespace
value an `import * as` binding has, including pi's cast-and-probe spelling
`(process as P).getBuiltinModule?.("node:os") ?? null`; a nullable
namespace binding narrowed present keys the same member tables, and a
namespace is truthy. process.features read as a whole value (`"sea" in
process.features`) is an object holding none of the flags, matching the
existing member reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:15:04 -07:00
Malte UblandClaude Opus 5.5 5e964965f6 Assign process.emitWarning
pi's CLI setup replaces process.emitWarning to silence warnings. The
assignment now stores the replacement: later process.emitWarning(...)
calls apply it to their arguments, and runtime-built warnings (Node's own
modules call process.emitWarning) reach it as (message, type[, code])
instead of the report and the 'warning' listeners. process.title writes
already lowered; the corpus program pins both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:15:03 -07: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
Malte UblandClaude Opus 5.5 89bca68cbe Compute with NaN-boxed jsval values in the dynamic tier
Stage S3 of the two-tier design: dynamic-tier functions compute with
jsval, a new IR type holding a 64-bit NaN-boxed word whose numbers,
booleans, undefined and null are immediates, so a number between two
dynamic operators is no longer a heap node.

- IR: the jsval type and js.* builtins (operators and the boundary
  conversions fromDyn/toDyn/fromF64/fromBool/fromStr/fromBigInt/toF64).
  The jsval pass rewrites dynamic-tier functions after tier assignment:
  operator chains, comparisons, number checks and truthiness of operator
  results run on jsvals; dyn locals become jsval locals when that saves
  boxing; dynamic functions called only from dynamic functions return a
  jsval. IR version 17 serializes each function's tier.
- Runtime: scr_js.h and scr_js_value.c behind the dynTier feature
  (invariant I3); static runtime units never include scr_js.h.
- Backend: number cases inline (immediate doubles and NUM cells), always
  inlined jsval retain/release; 32-bit targets keep boxed cells.
- Interprocedural analyses see jsval conversions as the dyn forms they
  replace, so no fact about a static function changes.

Static functions are untouched: typed programs and the self-hosted
compiler's static functions compile to byte-identical LLVM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:11:32 -07:00
Chris Tate 356d968528 Merge main 2026-10-18 17:07:58 -05: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
Malte UblandClaude Opus 5.5 29909ed1aa Give tsc's void-init export preamble no storage for class exports
tsc opens every CommonJS file with `exports.A = exports.B = void 0;` and
later aliases `exports.A = A;`. Collection registered an export global
for the chain's first name at the preamble, while the preamble lowered
to nothing and the class alias registered and stored nothing. Importers
then read `lib.A` from a global that was never assigned, and `new
lib.A()` crashed with a segfault (chardet's encoding modules).

The preamble detection moves to program.ts so collection and lowering
share it. A dropped preamble whose real assignment is a class alias now
registers no storage, so importers resolve to the class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:04:56 -07:00
Malte UblandClaude Opus 5.5 8a204b8e0f Dispatch methods on class instances passed to untyped JS callbacks
`list.map((rec) => rec.match(x))` over an array of class instances in a
JavaScript file lowers the callback with a checked-dynamic parameter and
checks the boxed closure into the native callback type. The instances
cross into the dynamic tier only when the callback is called, inside
that function boundary, so the native-capsule dispatch never learned
their classes and `rec.match` threw "rec.match is not a function". One
class or several, inherited methods or own, all failed (chardet's
recogniser list, any `.map`/`.filter`/`.forEach` with an untyped arrow).

A dynamic value checked into a function type now registers the classes
its parameters carry, as a dynFrom of those values already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:04:56 -07: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
Malte UblandClaude Opus 5.5 4bfced6008 Answer wrapper constructors for a primitive's .constructor
With object identity exact, dequal(1, 2) went from a fenced error to a
silent `true`: `(1).constructor` read undefined natively, so
`(ctor = a.constructor) === b.constructor` held for any two primitives.
Constant-key `.constructor` reads on dynamic values now answer the same
interned closure the Number, String, Boolean and BigInt identifiers read
for primitives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:49:14 -07:00
Malte UblandClaude Opus 5.5 ae531a06ce Keep the compatibility paths out of the always-linked runtime units
The zero-cost gate showed every benchmark executable changing: the new
string literals and floating-point constants grew the merged constant
pools of scr_json.o and scr_lib.o, which every program linking those units
carries, and edits to scr_dyn_fn_get and scr_dyn_has_own changed code the
typed benchmarks reach.

- The builtin constructor values, the inherited Object.prototype members
  and the stored timer functions move to scr_dyn_invoke.c, which links only
  into programs that use them: the frontend now selects that unit for
  dyn.globalConstructor, dyn.builtinFunction, dyn.objectTag and
  constant-key reads of constructor/valueOf/toString/hasOwnProperty/
  propertyIsEnumerable. Those reads call scr_dyn_inherited_read after the
  ordinary read (and scr_dyn_object_prototype_member before it, since the
  ordinary read fences Object.prototype itself); computed-key reads keep
  their old answer. The unit's new strings live in their own section so
  programs linking it for other reasons keep their bytes.
- scr_date_parse_get_time keeps its exact body; Date.parse and
  new Date(string) call the new scr_date_parse (bounded grammars, then
  V8's parser), and JavaScript `new Date(value)` calls
  scr_dyn_native_date_new_value, so fs.Stats dates are untouched. The
  parser and UTC(t) avoid new pooled constants.
- Object.prototype.toString's "[object Error]" moves to
  scr_dyn_object_tag_of; scr_json.c is unchanged.
- Calling or constructing through stored constructor values and
  own-property checks on Object.prototype itself are not supported in
  this change (they would touch shared code).

Also pins the IMF-fixdate (cookie Expires) parse and Date#toUTCString
round-trip in the corpus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:49:14 -07:00
Chris Tate 385465ff24 Remove dynamic-path leaks from typed code (two-tier stage S1) (#1326) 2026-10-18 16:47:13 -05:00
Chris Tate 5a574e86ce Raise the static hello size ceiling to 96 KiB on Linux
The tree-shaking test failed on Linux x64 after main was merged into
this branch: "expected 84496 to be less than 81920". Measured with the
release runtime pack (hello = console.log("hello", "world")):

  tree                 file   R+E segment
  main b866ae323       80288  0x5fe9
  989d0d6bc            79928  0x5fe9
  merge 474012f04      84488  0x6579

No runtime family comes back: hello's absence checks still pass. The
code that grows is the cycle collector and the release paths that feed
it, which hello's exit reaches (scr_lib_init -> scr_lib_cleanup ->
scr_arr_release -> scr_cyc_on_release -> scr_cyc_collect_due -> the
pass). Main grew it by 1,447 bytes (the death rule and
scr_mem_order_for_free), this branch by 1,386 (the inline nursery
appends in the runtime's release functions, scr_cyc_route_bypassed and
the window). Main left .text 23 bytes under the executable segment's
24 KiB page, so together they cross it, and every later segment and the
file move by 4 KiB. 96 KiB, the Mach-O ceiling, restores several pages
of slack.
2026-10-18 16:46:10 -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 af0c7f6a8c Answer the right operand of && over an always-truthy left
`list && list[0]` with an array, object or function on the left and a
union result (the element read may be undefined) coerced the left
operand into that union, which traps at run time ("a 'number[]' value
is not representable in the target union") where Node answers the
element. Every object is truthy: evaluate the left for its effects and
answer the right.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:42:19 -07:00
Malte UblandClaude Opus 5.5 11c97074e2 Answer the object a constructor returns from new
A class constructor's `return expr` was evaluated and dropped, so
`new C()` answered the constructed instance even where Node answers the
returned object (`if (C.instance) return C.instance;` built a second
instance).

A return that yields an instance of the class itself stores it in a
per-class result slot, read and cleared by one helper at every
construction site (direct new, class values, the checked dynamic
construction dispatchers, record-to-class coercion). A return of any
other object refuses with SC1090 (compile time in TypeScript, a
deferred fence in JavaScript); a JavaScript value typed only at run time
is tested when it returns. super() into a class that can return an
existing instance refuses, since Node would rebind the subclass's this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:42:18 -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