union, intersection, difference, symmetricDifference, isSubsetOf,
isSupersetOf and isDisjointFrom now compile with a Map argument whose key type
is the Set's element type, as well as with a Set. A Map's set record reads its
keys, and its size, has and keys are the builtins, so the helper loops walk
and test the Map's keys in the order the spec gives. Other set-like arguments
are still refused.
deflateSync, inflateSync, gzipSync, gunzipSync, deflateRawSync,
inflateRawSync and unzipSync, and their callback forms, accept Node's
options and run Node's processChunkSync over the zlib stream engine.
- Options (windowBits, level, memLevel, strategy, dictionary, chunkSize,
finishFlush, maxOutputLength, rejectGarbageAfterEnd) are validated at
run time with Node's ladder, shared with the stream classes; info is
refused at run time.
- Errors carry Node's errno and code; maxOutputLength reports
ERR_BUFFER_TOO_LARGE and rejectGarbageAfterEnd
ERR_TRAILING_JUNK_AFTER_STREAM_END. The default-options decoders also
read every gzip member.
- Untyped JavaScript inputs may be strings, TypedArrays, DataViews or
ArrayBuffers; other values throw Node's ERR_INVALID_ARG_TYPE after the
options are checked.
- Results are Buffers, as in Node: they convert to their text.
Corpus programs 5378, 5379 and 5415 cover the sync, callback and untyped
input forms.
`checksum += parsed[i].score` (a number local plus an `any` value) failed
with SC1043 "comparing non-number, non-string values" in TypeScript, while
the expanded `checksum = checksum + parsed[i].score` compiled: the
compound lowering only routed checked-dynamic operands through the
runtime operators in JavaScript files.
Locals, record fields and array elements now lower `op=` with an untyped
right operand (or an untyped local target) exactly like `x = x op rhs`:
dyn.add / dyn.arithmetic / dyn.bitwise, then the checked conversion into
the target's declared type. Numeric operators keep Node's ToNumber
semantics; a `+=` that produces a string for a `number` target throws the
same catchable TypeError as the expanded assignment and leaves the target
unchanged (pinned in tests/harness/dyncheck.test.ts).
Remaining SC1043 refusals for arithmetic, unary, update and compound
operators now name the operator and operand types instead of the
comparison wording.
The retired-engine-763 diagnostics fixture compiles now and matches Node,
so it moves to the corpus as 5343.
node:test's mock.fn(original[, implementation]) and t.mock.fn compile.
A Mock<F> is the function-with-properties record { %call, mock }: the
callable is a wrapper closure that shares the mock context with the mock
field. The context records each call's arguments, result, error and this,
and keeps the current implementation, the original and the one-time
implementations by call index. fn.mock supports, as in Node 24.22:
- calls, read as a copy of the log;
- callCount();
- mockImplementation(fn);
- mockImplementationOnce(fn[, onCall]), for the next call by default, a
later registration for the same index replacing the earlier one;
- resetCalls();
- restore(), which brings back the original.
A throwing implementation records the error and rethrows it. Functions
with fixed parameters or a single rest array parameter are supported; a
union return type needs an undefined arm. mock.fn() without an original,
mock.method, the tracker's reset and restoreAll, and t.mock.method are
refused with SC2020.
The node:test fixture mock-fn compares the behavior with Node, and
tests/diagnostics/mock-fences.ts covers the refusals.
createInterface accepts history, historySize, removeHistoryDuplicates,
completer, tabSize and escapeCodeTimeout, as Node does for a non-terminal
interface: tabSize, escapeCodeTimeout and historySize are checked in the
constructor's order before the interface opens (ERR_OUT_OF_RANGE and
ERR_INVALID_ARG_VALUE with Node's messages), a completer that is not a
function throws ERR_INVALID_ARG_VALUE, and the values are kept on the
interface. Options may be written as shorthand properties.
rl.history answers the history option itself, or the interface's own
empty array, and historySize, historyIndex, crlfDelay, tabSize,
escapeCodeTimeout, line and terminal read as in Node. 'history'
listeners can be registered; without terminal mode Node records no
history, so they never fire.
@scriptc/experimental exports InternTable<K, V>: a named table that every
thread of a scriptc program shares and that each thread has its own copy
of under Node.js. Keys are strings or numbers with Map equality; values are
published (@scriptc/threads) when stored, and the first stored value of a
key is kept. Members are get, has, size, getOrInsert and
getOrInsertComputed.
The runtime table (scr_itable.c) splits each table into 64 segments.
Lookups take no lock and do not write; inserts take a segment's spin lock
for a probe and a few stores, and callbacks run without a lock. Number
tables keep keys and values in the index; string tables keep entries in
segment-owned chunks behind tagged slots. Replaced indexes are kept.
The compiler lowers the class to a synthetic class with one f64 handle
field and five intrinsics, with interned helpers per member and type and
an in-place lowering of getOrInsertComputed that creates a function
expression callback only on a miss. Stored values go through the publish
intrinsic, so their types get the write guard. scriptc refuses names that
are not string literals, one name opened with different type arguments,
and key or value types a table cannot store.
scr_publish walks small graphs in inline buffers and reads
SCRIPTC_PUBLISH_TRACE once, instead of allocating and calling getenv on
every call.
events.once(emitter, name) was refused. The emitter dispatches statically
emitted events to typed listeners through invokers built per argument
tuple, so the lowering takes the event's tuple from the program-wide event
table and builds a capture-free function that boxes that tuple into the
any[] Node fulfills with, and one for the 'error' tuple.
scr_events_once registers both as one-shot typed listeners behind emitted
adapters (emitterOnceAdapter). The first to fire settles the promise
(fulfilled with the arguments, or rejected with the error) and removes
the other, as in Node; awaiting 'error' itself registers only the one
listener. The shared state holds the emitter and the listener closures
weakly, so a never-fired once creates no cycle.
Options, computed or meta event names, EventTarget and stream receivers,
and events emitted with computed names or checked-dynamic listeners stay
refused. The fallback declarations type the result Promise<unknown[]>;
under @types/node (Promise<any[]>) TypeScript code narrows it to
unknown[].
timers/promises setTimeout(delay, value) and setImmediate(value) were
refused: only the void forms lowered. The value now travels as an
already-resolved promise of its own type (the Promise.resolve lowering),
and scr_tp_set_timeout_value / scr_tp_set_immediate_value copy its
settlement into the result when the timer or immediate fires, so the
result keeps the value's type. An options object literal takes `ref`;
`ref: false` unrefs the timer, so it no longer keeps the process alive.
AbortSignal options and other option forms stay refused, with their own
messages.
@types/node types setInterval as NodeJS.AsyncIterator<T>, which maps to
no IR type, so a for await loop over setInterval(delay, value) was refused
in projects with @types/node. The interval's async generator type now
comes from the iterator's type argument, and a for await loop whose head
is that call consumes it directly.
The fallback declarations type the new forms.
- dns.lookup(hostname, callback), lookup(hostname, family, callback)
and lookup(hostname, options, callback) with family (0/4/6, 'IPv4',
'IPv6'), all, order and verbatim, validated with Node's messages.
getaddrinfo answers any family, ordered verbatim or one family first;
literal addresses answer on the next tick, an empty hostname gives
Node's null answer and DEP0118 warning, and failures carry code,
errno, syscall and hostname.
- dns.lookupService(address, port, callback) checks the address and
port as Node does and answers getnameinfo's host and service.
- dns.getServers() lists the nameservers in /etc/resolv.conf, and
getDefaultResultOrder/setDefaultResultOrder keep the default order
('verbatim').
- A typed callback receives "" and NaN (or []) after a failure, where
Node passes undefined; the limitations page says so.
Corpus 5648-5650 cover each case; the options-record diagnostics no longer
list the all and hints options as refused.
Typed and JavaScript classes can extend the global EventTarget and Event.
%EventTarget and %Event are runtime-provided hierarchy roots like the
EventEmitter: a subclass instance embeds a prefix pointing at the engine
state that super() creates (the target's listener core, the event's
state), and the backend's emitter prefix handling becomes a table of
runtime prefixes that covers all three bases.
The instances' checked-dynamic capsules use the runtime RC adapters, so
the engine recognizes them: a target instance is its listeners' `this`
and its events' target, and an Event subclass instance is the event its
listeners receive, with its own fields and methods. The inherited
methods and accessors lower directly on typed receivers (including
super.dispatchEvent) and answer through the class property dispatch and
the runtime's capsule hooks for checked-dynamic values, with Node's
util.inspect output, @@toStringTag and leak-warning class name. Members
read as values are the prototype's functions, also for plain handles.
A target keeps its own capsule in its state, so repeated dispatches reuse
it; the state and an event's target are traced, so the collector
reclaims targets whose listeners capture them. Subclass members that
spell an inherited member are refused, since the engine would never call
or read them.
Listener objects that are class instances now work: when a program can
register EventTarget listeners and declares a class with a handleEvent
method, %main installs a compiled `listener.handleEvent` reader that the
engine uses at every dispatch, so the class's own, inherited or
overridden method runs with the instance as `this`, as in Node. An
unannotated arrow in an object literal whose contextual type maps to a
checked-dynamic slot or to no lowered type (an EventTarget subclass's
listener union) no longer adopts the void callback slot's return type,
so `{ handleEvent: (e) => n++ }` passed as a listener keeps the result
its record field promises.
Program-lifetime instances were created with rc == SIZE_MAX, which every runtime contract reads as "published": the frozen-object guards throw on stores, postMessage and workerData share the object by pointer instead of cloning it, and the publish walker stops there. So programs that publish could not use ProgramLifetime (SC7007 refused it), and an unpublished instance posted to a worker would have been shared instead of cloned.
They are now created with the midpoint count 2^63 (2^31 on 32-bit targets). Only the class's empty retain and release helpers count references on these types, so nothing ever changes or frees that count; the midpoint keeps even an unpaired generic retain or release away from 0 and SIZE_MAX. Until the program publishes an instance it is an ordinary mutable, thread-local object to every runtime contract: stores succeed, postMessage clones it. Publication marks it SIZE_MAX like any object it reaches (the visited mark, logged and restored when a refusal undoes the publish), after which the frozen guards throw Node's TypeErrors and other threads read it by pointer. No header, extra word, runtime change or new instruction: the count itself is the published mark. The cycle analysis keeps the hierarchies untraced, so the collector never visits an instance, as for every untraced class.
SC7007 no longer refuses program-lifetime classes reachable from a publish() call; it still requires a root class in an executable. Without the marker nothing changes: tsc-ts best-v7 built with and without this change is byte-identical.
Tests: corpus 4634-contract-program-lifetime-publish (Node vs native): program-lifetime hierarchies with parent cycles, maps and sets of them, maps keyed by them and mortal scopes around them, published on the main thread and a worker and read on the other; writes after publication throw on both threads; unpublished instances stay writable; a refused publish leaves visited instances writable; a later publish reaches published and fresh instances; an unpublished instance in workerData is cloned. With the old SIZE_MAX count the program dies at its first constructor store. classes.test.ts covers the count on both pointer widths, the walker and the frozen guard. The contract-program-lifetime-publish diagnostics fixture is removed (the program now compiles). Docs: contracts and limitations pages, @scriptc/types.
`implements Acyclic` resolved to Record<never, never>, and the frontend
indexes implemented types as class protocols by symbol, so every Record
value in the program took checked identity (on tsc-ts: Int32 field
contracts failed with SC7003). The default is now AcyclicObject, an empty
interface only @scriptc/types declares, and type arguments in heritage
clauses are reported (they would make a class implement its own type).
Corpus 4642 stores an Acyclic class with an Int32 field in a Record.
explainCycle picks the shortest cycle through the unit and a mutable
edge (BFS distances forward and backward within the component): on
tsc-ts `Node.parent -> Node` instead of a 20-step path through the
checker.
The may-throw analysis now models handlers. A throwing operation or call
inside a `try` with a `catch`, or inside a try/catch whose `finally`
never completes normally (it returns, breaks or continues, discarding the
pending exception), adds no seed and no call edge. The catch and finally
bodies are analyzed like any other code, so rethrows and a finally that
re-raises stay exact. Worker programs (a termination request unwinds
through catch) and generator bodies (a `.return()` does too) keep the
conservative answer.
Unreachable statements no longer seed: the frontend's SC9002 trap after a
body that returns from both its try and its catch (or ends in a switch
with a default, or an infinite loop) is dead code. On tsc-ts this drops
14 functions from the may-throw set and 44 pending-exception checks.
A NoThrow function may now contain handled calls; the SC7001 hint names
the remedy that applies (catch it, remove the recursion, or the worker
exception).
`class C implements Acyclic`, `interface I extends Acyclic` and
`type T = Acyclic<{ ... }>` promise that no chain of references leads
from such an object back to itself. The check is the cycle analysis that
decides tracing, so a proven type carries no cycle-collector header. When
it does not hold, SC7007 shows the cycle: the path of fields, and why it
can close (a field assigned after construction, with the first such
write; an array, Map or Set; a function, dynamic or promise field; a
subclass sharing the layout). cycle-analysis.ts gains explainCycle over
the same graph with edge labels; computeTraced is unchanged.
Acyclic elsewhere is reported with SC7006. Docs, @scriptc/types,
corpus 4642, diagnostics snapshots and TypeScript baselines included.
`class Node implements ProgramLifetime` (@scriptc/types, import type only, erased for Node) promises that instances of the class and of its subclasses are kept until the program exits. scriptc then creates them immortal: `new` stores rc = SIZE_MAX and allocates no collector header, the class's retain and release entry points are empty (so reference counting on every static type of the hierarchy folds away), and the cycle analysis treats the hierarchy as a leaf, which untraces types that only reached it. Immortal objects are already part of every RC, collector, weak-collection and publication contract (static literals, published graphs).
The promise only changes memory use; SC7007 refuses it where something else could observe it: on a class that is not the root of its hierarchy (including built-in bases), in library builds, and on classes whose instances can be published to other threads. SC7006 reports the marker outside a class's implements clause.
tsc-ts with nine roots marked (Node, NodeList, FlowNode, Symbol, Type, Signature, IndexInfo, SymbolLinks, NodeLinks): single-threaded wall -25.8% Excalidraw, -22.2% TypeORM, -25.9% self-check, -28.5% Sentry; peak RSS -6.5% to -16.5%; identical diagnostics.
- NoThrow diagnostics name compiler-resolved Node errors (class and message)
and explain parameters the frontend compiles as T | undefined because a
caller passes a possibly missing value (for example xs[i]!), naming that
call.
- Synthetic dynamic-class helpers (%dyn.*) carry the location of the code
that first needed them; they are no longer bound to annotations there.
- Int32 hints only parenthesize where needed; the NoThrow hint no longer
suggests try/catch, which does not discharge the contract.
- Diagnostics snapshots and TypeScript preflight/order baselines for the new
fixtures.
- Docs: /docs/contracts and a limitations entry.
Deferred collection diagnostics now stay attached to their declaration until a
copy reaches the final report, so a discarded probe buffer or a successful
nested collection can no longer drop them. Uses of a binding whose type names
a rejected class report that class's diagnostic before the inherited-blocker
cascade.
A for-of loop over an array whose own facts show a hole or a stored
undefined binds T | undefined, and the typed parameters, locals, record
fields and Math arguments it reaches widen to hold it. Loops over arrays
the element-state analysis proves free of holes read each element
directly, without a per-element state test.
Object literals built from a missing-element read keep the property as
undefined in shorthand and long form, through spreads, nested records,
arrays of records, annotated locals and literal arguments to typed
parameters.