Commit Graph
1744 Commits
Author SHA1 Message Date
Malte UblandClaude Opus 5.5 577b0135a9 Add execution tiers to the IR and a report-only tier analysis
Stage S2 of the two-tier design. IrFunction gains a tier (absent means
static); lowering assigns it from function bodies, placing functions that
use the dyn vocabulary in the dynamic tier, and the validator enforces
invariant I1: a static function performs no dynamic operation, only
boundary operations (dynFrom, dynCheck, kind tests) and moves. Serialized
IR keeps the field out in this stage and loaders derive it again, so every
IR, LLVM and executable artifact is unchanged.

Every committed refusal and runtime fence becomes a recorded demotion
request attributed to the binding, field, function instance, class or
module the dynamic tier would take over, while the compiler still refuses
and fences as before. `scriptc coverage` reports dynamic-tier statements
(statementsDynamic), `--explain-dynamic[=<name>]` prints the demotion
chains, and `--tier=auto|static-only` is wired through build, run and
coverage for stage S6; both modes compile identically today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:12:27 -07:00
Chris Tate 2fa4b19b45 Keep object-literal computed keys out of the method replacement census (#1294) 2026-10-18 09:46:31 -05:00
Chris Tate ff90245b27 Read fields as undefined before their assignment (#1292) 2026-10-18 09:38:26 -05:00
Chris Tate 0bdcccef80 Lower child_process.exec's callback forms (#1290) 2026-10-18 09:26:38 -05:00
Chris Tate 6c4e4c39b4 Keep object-literal computed keys out of the method replacement census
An object literal with a computed key that does not fold to a constant,
such as `{ [key]: 1 }`, entered the method replacement census as a write
with an unknown key and no receiver. That marked every class method name
in the program as replaceable, so method calls went through the checked
property lookup. A computed key in an object literal defines a property of
the new object, which is not a class instance, so the census now skips it.
Literals passed to Object.assign or Object.defineProperties are still
judged with their receiver.

Closes #962
Co-authored-by: Quim T. <26749475+espetro@users.noreply.github.com>
2026-10-18 09:23:31 -05:00
Chris Tate 0d4e6a0cc1 Compile classic CommonJS packages: late requires, JSON requires, nested builtin destructuring, settled require cycles (#1287) 2026-10-18 09:16:05 -05:00
Chris Tate 27f78a981d Read fields as undefined before their assignment
A constructor that let other code read a field before assigning it (a base
constructor calling an override, or the constructor calling its own method)
read an empty slot, which crashed the executable for string fields. Node
reads undefined.

Fields assigned after construction can expose the instance now admit
undefined. Allocation writes undefined into them. String, number, boolean,
bigint and union fields read as runtime-optional values, and class, array,
record and collection fields use the deferred-init reads, whose member
access throws a TypeError. Classes whose construction cannot expose the
instance before their assignments keep their layouts.
2026-10-18 09:14:01 -05:00
Chris Tate 9d0012ddab Lower child_process.exec's callback forms
exec(command, callback) and exec(command, { encoding, maxBuffer },
callback) lower like execFile's callback slice. The command line runs
with /bin/sh -c, as execSync runs it, the call returns the live
ChildProcess, and the error-first callback receives the captured utf8
stdout and stderr. A non-zero exit, including the shell's 127 for a
missing command, reports Node's "Command failed: <command>\n<stderr>":
scr_exec_file takes a shell flag so the message shows the command line.

Under @types/node, child_process.ExecException maps to Error, as
ExecFileException does. Its code member is the child's numeric exit
status, which the runtime error's string code slot does not hold, so
reading it, testing it with `in`, or testing its typeof is a named
refusal.
2026-10-18 09:05:09 -05:00
Chris Tate 8c1d94e72e Compile large closure environments in linear time (#1284) 2026-10-18 08:51:59 -05:00
Chris Tate 4ed75111a7 Carry lifetime regions across await and reuse freed region chunks (#1194) 2026-10-18 08:47:04 -05:00
Malte Ubl 8255315883 Merge origin/main into opt/cjs-interop 2026-10-18 06:46:23 -07:00
Chris Tate f2e03bdb37 Keep records, tuples and spread updates that own references in frame storage (#1205) 2026-10-18 08:44:15 -05:00
Chris Tate b143778641 Read events.defaultMaxListeners from the ES module namespace copy (#1280) 2026-10-18 08:34:13 -05:00
Malte UblandClaude Opus 5.5 ae91df908e Admit require cycles whose modules settle their exports first
A require cycle was always refused (SC1016). Node answers a require of a
module that is still evaluating with its exports as they stand, while the
compiled bindings alias the final declarations; the two agree when every
module of the cycle assigns all of its exports before its first statement
that can run another module's code, every other use of its export object
is a plain member read, and no module's top-level code can reach its
binding of another member. Each module then initializes exactly as it
would outside the cycle. This admits semver's comparator/range cycle
("hoisted class for cyclic dependency"); other cycles keep SC1016 with a
reason naming the offending statement.

The require reachability scan no longer treats a declaration's own name
as a read (esbuild's `var X = class {...}` before its requires), treats a
plain write of an export member as running only a setter published for it
(on-finished's `module.exports.isFinished = isFinished`), and keeps the
conservative scan for writes of __proto__, __esModule and members of a
replaced export object.

Evidence: corpus 6145 (the cycle), 6142 gains the esbuild layout; the
npm-static harness compiles the published fs.realpath@1.0.0,
on-finished@2.4.1 (with ee-first@1.1.1) and statuses@2.0.1 statically;
program.test.ts pins the TDZ analysis through var bindings plus the cycle
admission and refusals. Preflight/order baselines recorded for the new
fixtures; a full re-record changed no other fixture. Dependencies and
limitations docs describe the supported require forms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 06:24:04 -07:00
Malte UblandClaude Opus 5.5 9118cbc258 Record corpus 5973's preflight baseline and skip the scaling test under ASan
The order-parity baseline for the new corpus program records its module
order and empty preflight diagnostics. Uncached sanitized builds rebuild
the runtime for every compile, which takes longer than either measured
size, so the compile-time growth test runs in the plain lane only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 06:23:52 -07:00
Malte UblandClaude Opus 5.5 ed5aa1043f Merge origin/main into opt/02-value-records
Resolve the emitter.ts import conflict by keeping both RESULT_DST_ID
(frame-stored result slots) and specializeCallbackCalls (#1206).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 06:23:47 -07:00
Malte UblandClaude Opus 5.5 10c5e269d7 Merge origin/main into opt/06-regions
Resolve the scr_resume_fiber conflict with stackless async coroutines
(#1204): the region the fiber carries is made current before either
resume path and restored after both. scr_async_spawn_stackless now
captures the spawning region like the fiber spawns, so stackless bodies
(every async body in 5013 and 5023 after the merge) keep allocating in
their region after an await and keep it alive while they exist.

5024-lifetime-region-await-forms pins regions carried by stackless and
fiber (for await) bodies that spawn and await each other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 06:23:44 -07:00
Chris Tate 31b52e2152 Remove the last element of a packed array inline on pop (#1282) 2026-10-18 08:21:05 -05:00
Chris Tate fc68a582d0 Remove the last element of a packed array inline on pop
pop and a discarded pop (the length - 1 store) branch on the array's
length and hole-free prefix bound. When the bound equals the length, the
last element is a present dense value: the emitted code clears its slot
and state, lowers the bound and the length, and moves the slot out (or
releases it for a discarded pop). An empty array answers undefined
without a call. Holes, present undefined and sparse storage keep the
runtime call.
2026-10-18 07:59:03 -05:00
Chris Tate 201857eb6c Read the class base after the callable-base case in super calls
superCallStmt asserted the class base before checking for a callable
base. A class that extends an ordinary function constructor has no base
class info, so the assertion held null. Node ignores the unused value;
the native compiler throws the TypeError of a non-null assertion that
held null, and failed to compile programs with such classes (for example
the Effect stream fixtures). The base is now read after that case
returns.
2026-10-18 07:54:21 -05:00
Chris Tate 1cd06fda4c Read events.defaultMaxListeners from the ES module namespace copy
Reads of defaultMaxListeners through an ES module namespace import or a
named import answer the namespace's copy of the export, which keeps its
value from program start until module.syncBuiltinESMExports() refreshes it,
as in Node. The default import, the EventEmitter class and CommonJS module
bindings keep reading the current default. A destructured CommonJS binding
of defaultMaxListeners, which held a placeholder string in JavaScript
files, is refused.
2026-10-18 07:44:23 -05:00
Chris Tate 572b26ef85 Answer typeof for builtin module functions and classes (#1279) 2026-10-18 07:16:53 -05:00
Chris Tate 4d95b28dde Add t.assert.snapshot and t.assert.fileSnapshot (#1274) 2026-10-18 06:55:39 -05:00
Chris Tate 0ee069f094 Answer typeof for builtin module functions and classes
`typeof zlib.Inflate` and `typeof zlib.createGzip` stopped at SC1090 or
SC2020, because the operand lowered as a library function or class
value. A typeof of a function or class exported by a Node builtin
module, read through a plain binding, now folds to "function" as in
Node: the read has no side effects to preserve.
2026-10-18 06:53:41 -05:00
Chris Tate 14c1cddffe Merge main 2026-10-18 06:17:56 -05:00
Chris Tate 0dac6c5094 Serialize checked-dynamic members of typed records like JSON.stringify (#1275) 2026-10-18 05:59:12 -05:00
Chris Tate 85fe87ffbf Convert Date values into unknown slots as Dates
A TypeScript Date placed in an `unknown` slot (an annotated binding, an
object or array literal, a record or class field) becomes a Date holding
the same time value. Before, a Date member of an `unknown` object literal
became a function, and other conversions were refused. util.inspect prints
these Dates in their ISO form or as Invalid Date, and their methods can be
read as values through untyped references. Comparisons with a Date operand
keep their fence: each conversion creates a new Date.
2026-10-18 05:40:08 -05:00
Chris Tate b779ef71de Return a worker's memory once it and the other workers are done (#1273) 2026-10-18 05:38:03 -05:00
Chris Tate d09b380098 Serialize checked-dynamic members of typed records like JSON.stringify
The undeclared properties a checked record keeps from its source, and the
entries of an `unknown` index signature, now follow SerializeJSONProperty:
toJSON runs with the member's key, getters are read, a Buffer becomes its
{ type, data } form, undefined, function and symbol results are dropped,
and a circular structure throws the circular-structure TypeError instead of
overflowing the stack. Members whose writing cannot run program code are
written directly; the others go through the JSON.stringify protocol walk.
Index-signature entries are written from the overflow map in place,
without building a key array.
2026-10-18 05:35:23 -05:00
Chris Tate 89b5baedd7 Add t.assert.snapshot and t.assert.fileSnapshot
node:test changes, matching Node 24.22:

- t.assert.snapshot(value[, options]) serializes the value with Node's
  default serializer, JSON.stringify(value, null, 2), or with an
  options.serializers array literal applied in order, and compares it
  with assert.strictEqual against the snapshot stored under
  "<test full name> <n>" in "<entry file>.snapshot".
- t.assert.fileSnapshot(value, path[, options]) compares the serialized
  value with the contents of path.
- Snapshot files are read in the form Node writes them, exports[`key`] =
  `value`; statements, with template escapes and CRLF line endings
  cooked as in a template literal.
- Missing snapshots, unreadable snapshot files and throwing serializers
  throw Node's ERR_INVALID_STATE errors, with Node's messages.
- Running the binary with Node's --test-update-snapshots flag as its
  first argument records the values and writes the snapshot files after
  the last test, keys sorted as Node sorts them. The flag is kept out of
  process.argv and appears in process.execArgv, which is otherwise [].
- Snapshot assertions count toward the test's plan.

The snapshot fixture compares the reporter output with Node, plain and
with SCRIPTC_SAN=1, against a snapshot file Node wrote. The update-mode
harness test runs Node and the binary with --test-update-snapshots and
compares the files they write. The node:test normalization also drops
the "... N lines matching cause stack trace ..." line Node prints for an
error cause, with the stack frames it summarizes.
2026-10-18 05:31:40 -05:00
Chris Tate 4fe392cd25 Return a worker's memory once it and the other workers are done
On Linux, a worker thread that exits without having made any object it
allocated reachable from another thread gives back all of its memory: the
pages it carved, including ProgramLifetime instances and other objects it
never freed, and the system blocks it still holds. The memory goes back
once no other worker thread is running, so that returning it does not slow
the workers that are still running.

A worker carves past each slice's frontier while the arena's free lists and
returned regions are set aside, takes from those free lists only once a
slice has no bump room left, and records the system blocks it allocates.
Messages and intern tables that share published graphs by pointer, and the
process-wide key arrays of typed records, mark the thread as sharing; so
does a remote free of one of its blocks. A worker that did not share parks
with its arena claimed and its discard queued: returning pages makes the
kernel interrupt every running thread of the process to flush its TLB, and
glibc's trim locks the arenas other threads allocate from. The last worker
to park starts one helper that returns each queued worker's pages, frees
its system blocks, and restores its arena to its state at the claim, with
the free blocks the worker took free again. A thread that finds every arena
claimed runs the queued discards itself. A worker that shared keeps its
memory as before.

SCR_SA_DISCARD=0 keeps every worker's memory; SCR_SA_DISCARD=poison makes
the discarded pages inaccessible instead, for tests.
2026-10-18 05:15:17 -05:00
Chris Tate ffd760bfa2 Merge main 2026-10-18 04:55:11 -05:00
Chris Tate 5093ab9a7a Add markAsUncloneable and worker[Symbol.asyncDispose] (#1269) 2026-10-18 04:41:04 -05:00
Chris Tate 6d80207e4c Convert errors held as any with Error.prototype.toString
Errors that reach checked-dynamic code are objects carrying the reserved
"%error" marker, with name and message either own or on their prototype.
Several string conversions handled them differently from Node:

- `"" + error`, `error + ""` and `error == "TypeError: ..."` converted
  through the plain-object path and answered "[object Object]".
- Template literals and Array join read name and message as own members
  only, so a builtin error whose name is inherited printed its message
  alone.
- String(error) used the native error, which did not see name and message
  assigned through the dynamic view.

scr_dyn_error_string implements Error.prototype.toString over the
encoding, reading both members through the prototype, and the template
renderer, Array join, ToPrimitive, ToString and the numeric conversion use
it. Assigning a string name or message through the dynamic view now also
updates the native error, as assigning stack and cause already did.
2026-10-18 04:07:51 -05:00
Chris Tate ab1c271dde Support the regex v flag (#1268) 2026-10-18 04:01:38 -05:00
Chris Tate a18335e6c4 Revert "Return a worker's memory when it exits without sharing any of it" (#1267) 2026-10-18 04:00:54 -05:00
Chris Tate a0b1e93dec Merge main 2026-10-18 03:52:20 -05:00
Chris Tate 4f3bf565d6 Key the differential oracle cache on the inherited environment (#1266) 2026-10-18 03:51:05 -05:00
Chris Tate 51777dee63 Add markAsUncloneable and worker[Symbol.asyncDispose]
markAsUncloneable(value) marks a plain object, a record, an error or a
class instance (scr_json.c); structuredClone and postMessage then throw
DataCloneError "Cannot clone object of unsupported type." for it, also
where it is nested. As in Node, the mark has no effect on primitives,
arrays, typed arrays and ArrayBuffers. Marked values are retained until
the thread exits; with nothing marked, the clone paths add one branch.

worker[Symbol.asyncDispose]() terminates the worker and resolves with
undefined after its 'exit' event, so `await using` disposes a Worker at
the end of its block. The Worker handle's symbol_get op answers the
well-known symbol: the symbol unit registers Symbol.asyncDispose with the
dyn core when the program names it, and scr_dyn_is_async_dispose_symbol
tells the worker unit, so worker programs that never name the symbol do
not link the symbol unit.

static-support.json adds rows for markAsUncloneable, isInternalThread
(already lowered as false and declared) and worker[Symbol.asyncDispose]().

Corpus 5990-5991 match Node v24.22.0; corpus-costs.json records their
costs.
2026-10-18 03:43:06 -05:00
Chris Tate d375212a32 Add BroadcastChannel and the remaining Node 24 worker_threads surfaces (#1189) 2026-10-18 03:38:36 -05:00
Chris Tate 84916f25e4 Support the regex v flag
Regex literals and new RegExp calls accept the v (unicode sets) flag, which
the vendored engine implements: class subtraction and intersection, nested
classes, \q{...} string literals and properties of strings. The flag uses
unicode mode for pattern decoding and string iteration, and flags read in
getter order (dgimsuvy). Regex literals now also report their flags in
getter order, as Node does for /x/yg, and the flag getters (global,
unicodeSets and the rest) compile on statically typed regexes.

In v mode the engine rejected `-` and doubled punctuation outside a
character class, and the `\-` escape inside \q{...}. It now applies the
class-only checks inside classes and \q{...} only, and parses \q{...}
contents with the class escapes, matching Node. The d flag is still
refused.
2026-10-18 03:37:40 -05:00
Chris Tate 3fd1d2bde6 Revert "Return a worker's memory when it exits without sharing any of it (#1258)"
This reverts commit 2385fc4fff.
2026-10-18 03:37:07 -05:00
Chris Tate 1899b5f6ae Key the differential oracle cache on the inherited environment
The Node oracle runs with the harness's environment, but its cached
verdict was keyed only on SCRIPTC_TEST_ENV among the variables. A
program whose output reads NODE_ENV or another variable reused a
verdict recorded under a different value. The key now covers every
inherited variable except the test runner's worker numbers, which
differ between workers of one run.

Closes #51
Co-authored-by: Patrick Wozniak <4481570+patlux@users.noreply.github.com>
2026-10-18 03:28:59 -05:00
Chris Tate d7ac6d3165 Merge main 2026-10-18 03:17:25 -05:00
Chris Tate 85ed5fdb5a Lower Date.prototype.toJSON with Node's null for invalid dates (#1255) 2026-10-18 02:30:06 -05:00
Chris Tate 0a5e2442df Read .groups on matchAll rows in JavaScript callbacks (#1262) 2026-10-18 02:26:22 -05:00
Chris Tate 7c0db7d2b3 Merge main 2026-10-18 02:07:18 -05:00
Chris Tate fe47b3861f Add Float16Array (#1249) 2026-10-18 02:06:17 -05:00
Chris Tate 8e677e5053 Merge main 2026-10-18 02:04:31 -05:00
Chris Tate 2385fc4fff Return a worker's memory when it exits without sharing any of it (#1258) 2026-10-18 01:54:28 -05:00