Relational string operators (`<`, `<=`, `>`, `>=`) compared the UTF-8
bytes, which is code-point order. Node compares UTF-16 code units, so the
results differed for characters above U+FFFF against U+E000..U+FFFF and
for lone surrogates against surrogate pairs. The default `sort()`
comparator, `util.inspect` with `sorted: true` and dynamic comparisons
already used UTF-16 order; the default comparator of a dynamic array's
`sort()` did not.
Typed comparisons and the dynamic array sort now use `scr_str_cmp_u16`,
and the IR's `strCmp` node no longer has a `utf16` flag. The two orders
agree unless both of the first differing bytes are at least 0xED, so:
- the inline wrapper decides differing first words, or last words of
equal lengths up to 16 bytes, in byte order when neither word has a
byte of at least 0xED (a SWAR test). Other differing words, and the
first bytes of shorter strings, decide by their first differing byte:
in byte order when one of the two is below 0xED, and by plane when both
are lead bytes from 0xEE to 0xF4 (a supplementary character orders
below U+E000..U+FFFF);
- otherwise a proven-ASCII string on either side compares with `memcmp`
(`scr_str_cmp`), and other pairs call `scr_str_cmp_u16_bytes`. It finds
the first differing byte with words up to byte 32, then with 32-byte
SSE2 or NEON blocks where the target has them. The call is marked as
only reading its arguments, so repeated comparisons of one pair merge.
- `scr_str_cmp_u16` itself takes the same `memcmp` path for proven-ASCII
strings.
The native compiler sorts record fields with `<` and checks the order
with `sort()`, so it reported SC9001 for 5888-inspect-sorted.ts, whose
object has the keys "\u{1F600}" and "\uffff". 5888 runs in the
native-frontend self-host check. Adds corpus 6023.
A loader `L = () => M ??= NS` over a lazily initialized module's
namespace (optionally through __toESM) is respelled at every `L().name`
as the module's initializer call followed by the export's live read;
loaders whose result escapes as a value stay refused. The canonical
export table keeps a getter it cannot chase as a live accessor, so a
lazy re-export still initializes its module at the first read. An
ES-module importer copies those accessors once when it links the
CommonJS module, in the lexer's order, exactly like Node's loader;
require() keeps reading them live.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`globalThis as { [KEY]?: State }` is the usual way for several modules
to share process-wide state. globalThis lowers to a checked-dynamic
value, and asserting a checked-dynamic value to a record or array type
took the checked conversion, which builds a new native value. A write
through the asserted value went to that copy, so other modules never
saw it; string-keyed and index-signature views threw "expected object".
- An assertion of the global object (globalThis, global, also through
`as unknown as`) keeps the checked-dynamic value when the conversion
would copy, so member reads and writes reach the global object.
- File-scope and local bindings of such an assertion hold the global
object (collectGlobals, lowerVarDecl), so an exported
`const state = globalThis as ...` works across modules.
- The divergence scan no longer reports these assertions as SC6001
width copies.
- Runtime: one global-object box per thread (scr_global_object), which
globalThis, global and globalThis.globalThis all answer, and direct
`globalThis[symbol]` accesses use that box's symbol-keyed properties,
so every spelling of the global object sees one property.
Other checked-dynamic values keep the checked conversion: a native
value held as unknown unwraps by identity there, and a checked-dynamic
tree is copied at the documented boundary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a generic function, `node instanceof Identifier || node instanceof
PrivateIdentifier` narrows `node` to `T & PrivateIdentifier` when
Identifier has every member PrivateIdentifier has: TypeScript reduces the
union of the two intersections by structural subtyping. The read was
lowered as a downcast to PrivateIdentifier, and the narrowing guard
aborted when it held an Identifier. tsc-ts's NodeFactory.cloneNode has
this pattern, so declaration emit aborted.
When the checker types a read of a type-parameter intersection as a
subclass, and another class under the value's class is assignable to that
subclass without extending it, the read is now a class union over those
classes, and member reads and method calls on it dispatch over the union.
When this instantiation's class is unrelated to the narrowed class but
assignable to it (`T = Identifier`), member calls use the instantiation's
class. A union method helper whose body lowering is refused is no longer
left registered without a body.
Recognizing the empty-tuple representation interned the unit-only union,
and a rest parameter's IR type was mapped before checking that it is a
tuple at all. Both shifted union ids in programs with ordinary array rest
parameters that read `.length`. Probe through the checker and registry
lookups only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
util.inspect prints a handle whose Symbol.toStringTag ends with "." and its class name in place of the name, as Node does: WebAssembly.Module {} rather than Module [WebAssembly.Module] {}. The same holds for Instance, Memory, Table and Global.
The WebAssembly API entry points move to scr_wasm.h, which scr_runtime.h includes with scr_wasm_abi.h, so every scr_* symbol the backend can emit has a header declaration. The emitted registry array is renamed sc_wasm_registry_modules, following the compiler-emitted symbol prefix.
The native compiler failed on corpus programs that the Node-hosted
compiler compiles. Four compiler causes:
- A record that width-lifts into several members of a discriminated
union had no conversion plan. As a field of a larger record, that
removed the outer member it belonged to, and the outer record could
then lift into the only member left: a `recordSet` statement built with
a nested `recordGet` literal became a `break` statement, and a switch
on `kind` read it with the `recordSet` layout. The native compiler
stopped at the narrowing guard on 5585-record-results-inlined.ts and
similar programs. Such a record now converts through the discriminant:
each member it width-lifts into is a route, and the value's
discriminant selects the member at run time, as same-layout members
already do.
- Three compiler paths asserted a value that can be null, and the native
compiler threw "null is not representable in the target union":
stableCreateRequireBindingReason7 (`cache.get(decl)!` from a cache that
stores null for stable createRequire bindings; 2631-create-require,
2932-create-require-program), the Int32 contract check (`catchLocalId!`
for a bindingless `catch {}`; 4970-int32-call-abi) and
StringIndexBounds.binding (a memo that stores null). The caches are
read without assertions and the catch test checks for null.
- scriptc adopts alwaysStrict, esModuleInterop and
allowSyntheticDefaultImports from a project's tsconfig, but the TS 7
SDK schema no longer declares them. A native compiler keeps only
declared fields when it builds or parses a typed record, so these
options were dropped, and the default-import check refused
`import fs from "node:fs"` (SC1012, 2391-default-builtin-imports and the
3055 to 3057 module-setting variants). The options a project reports
are parsed with these fields declared, and Ts7CompilerOptions uses the
same type.
- An object-literal method kept its inferred return type where an arrow
function or function expression takes the return type of its
contextual signature. For a method that returns an array of statement
literals, a `varDecl` literal with `init: null` was converted to the
member whose `init` holds only expressions, and the conversion threw
(Array.fromAsync's array loop: 5854-array-from-async,
5855-array-from-async-js). A synchronous object-literal method now
takes its slot from the property of the same name on the literal's
contextual type and adopts its return the same way.
Adds corpus 6022 and 6024. 2391, 2631, 3055, 4970, 5585 and 5854 run in
the native-frontend self-host check, which now also links the dyn-async
unit when the program uses it, as the production build does.
node:test changes, matching Node 24.22:
- mock.fn() and t.mock.fn() without an original return undefined and
record the arguments of each call, whatever the callers pass. The
original and the initial implementation are a function that returns
undefined, so restore() brings that function back.
- mock.fn<F>() with an explicit function type records F's parameters
when F's return type is void or includes undefined. A return type
without undefined, such as mock.fn<() => number>(), throws SC2020.
mock.fn()'s default type, (...args: any[]) => undefined, maps to the
checked-dynamic rest ABI used for program-declared (...args: unknown[])
functions: the calls log records the argument array, and the mock's
wrapper spreads it into the implementation.
A function whose fixed parameters are all unknown or any, or that has
none, now converts to a checked-dynamic rest signature without fixed
parameters, such as (...args: unknown[]) => void: each call passes the
arguments in order, undefined for the ones the caller left out. This is
how () => {} reaches mockImplementation and mockImplementationOnce, and
it applies to any such slot in TypeScript programs, which threw SC1090
before.
The mock-fn-default fixture and corpus program 5816 compare with Node,
plain and with SCRIPTC_SAN=1.
paramShapes, which builds the parameters of an instantiated signature
(generic call instances and the erased instance that a method value or
a reflected prototype slot uses), refused an optional parameter unless
its type had an undefined union arm. An interface view implemented by a
class (`handle?: Handle | null`) and an erased type parameter map to the
checked-dynamic type, which holds undefined directly; paramShape, the
declared-signature path, already accepts it.
Direct calls of such a method failed with "optional parameters of type
'unknown'", and reflecting its class prototype from the dynamic-dispatch
fixpoint raised the same refusal outside any statement and crashed the
compiler with an uncaught PoisonError. paramShapes now treats the
parameter as omittable, so the instance lowers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The checked-receiver brand check was meant for JavaScript methods, whose
`this` is the actual call-site value. It also accepted TypeScript
`unknown` receivers, which the private-members diagnostics fixture pins as
refused (narrow first). It now applies to JavaScript sources only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The native CLI built from main aborted on the record-boundaries and
value-objects benchmarks with "a narrowed value does not have the type its
narrowing proved". record-results.ts builds recordSet statements in a map
callback whose value field is a recordGet literal. Converting that array
into IrStmt[] chose the break/continue arm: the literal also fits the
shorter varRef-like expression arms, so its conversion into IrExpr was
ambiguous, and an ambiguous field conversion made widthLiftPlan drop every
arm that needed it. The one arm left was break/continue, reached by
dropping obj, shapeId, field and value. The record kept kind "recordSet"
under the break/continue tag, so mapStmtChildren's narrowing guard aborted.
IR record shapes keep the discriminant as a plain string, so a record's
layout alone cannot pick its arm. When a record that carries a
discriminated union's discriminant field fits several arms, widthLiftPlan
now plans a route through every arm it converts into, selected at run time
by the discriminant's value, as JavaScript does (a value no arm names
throws the catchable TypeError). Conversions that resolved before are
unchanged; the zero-cost gate shows identical IR, LLVM and executables for
the benchmarks and the corpus, and the native CLI now compiles every
benchmark workload.
`#target in this` inside a JavaScript method reads the actual call-site
receiver, a checked value, so the brand check fenced at run time
(undici's FastIterableIterator, and the corpus program that pins it,
which coverage counted as one deferred site). The brand is present
exactly when the value is a class instance at or below the declarer:
a typed reference boxed under the declarer or a descendant answers true,
and one boxed under an ancestor answers with an instanceof test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cjs-lexer-invisible and hoisted-workspace compiled only once their
untyped dependencies were selected; both now match Node in both lanes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An untyped package another package's JavaScript imports is selected
whether resolution answers no declarations or its JavaScript. A
DefinitelyTyped twin qualifies by identity (@types/<mangled name>):
the declarations only check the program, and the JavaScript compiles
with its inferred surface, so a twin that disagrees with the shipped
JavaScript fails the build at the use rather than compiling against it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Function-local classes whose private names are used only on `this` now
compile, so the two diagnostics fixtures test a brand check on another
object instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reflecting a class prototype (the checked view's prototype helper) lowers
each member's value and key from emitReachable. A member whose generic
method instance or computed key had no lowering threw a PoisonError out of
ClassDynamicDispatch and crashed the compiler (seen once undici's classes
compile). The walk now contains those errors per member, drops the
member's diagnostic, and refuses the reflection at run time like its other
unsupported cases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`new Agent({ keepAliveTimeout: 10 })` against undici's
`constructor({ factory = defaultFactory, maxOrigins = Infinity, ...options }
= {})` failed with SC2002: the literal was laid out against the record the
checker infers for the pattern, which lacks the properties the rest
element collects.
When a TypeScript object literal flows into a checked-value slot whose
contextual type is a JavaScript destructuring pattern with a rest element
(or that pattern's `= {}` default), and the literal names a property the
type does not declare, the literal is built as the plain object the
JavaScript side receives. Every other literal keeps its previous lowering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Higher-order wrappers over `(...args: any) => any` that forward `arguments`
with `fn.apply(this, arguments)` and type their options with
`Parameters<F>` / `ReturnType<F>`, and their rest-tuple rewrite, did not
compile. Generic calls already monomorphize per call site; these pieces now
resolve to that site's concrete signature:
- Rest parameters typed as fixed tuples (`...args: [A, B]`, `...args: A`
bound to a tuple, `...args: Parameters<F>`) flatten to positional
parameters, so `(...args: [A, B]) => R` and `(a: A, b: B) => R` share one
function type. Trailing optional positions become `T | undefined` slots.
- `Parameters<F>`, `ReturnType<F>`, `Awaited<ReturnType<F>>` and the
`ReturnType<F> extends PromiseLike<infer U> ? U : …` spelling resolve
through F's binding.
- A parameterless function expression reading `arguments` lowers against
the fixed signature its value is asserted to (or contextually typed as);
`arguments` is that signature's tuple.
- `f.apply(thisArg, args)` and `f(...args)` over a forwarded tuple call
directly, passing `thisArg` as the receiver. A tuple that is only
forwarded is never allocated.
- `fn.name` on a function parameter folds to the call site's static name;
the instantiation key carries it.
Also: optional callback fields narrowed at the call (`o.cb ? o.cb(x) : …`)
compile, effect-free surplus arguments to fixed-arity function values are
dropped, and `arguments.length` no longer counts a `this` parameter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An __esm initializer becomes a run-once closure over its body in the
bundle's own scope. Interop bindings it assigns once from require()
become that require at the assignment, with their member reads respelled
as reads of the required module; a namespace read only through members
becomes an object literal of getters over the module's live bindings.
Asynchronous initializers, initializer results, and namespaces kept as
values (a memoized lazy loader) stay refused with a message naming them.
The default selection now also compiles a package typed by a
DefinitelyTyped twin that describes its installed release (same major,
or major.minor below 1.0), and untyped dependencies of compiled
packages. A twin for another release is refused with both versions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each evaluation of a class declared inside a function creates fresh
private names, while the compiled class shares one layout across
evaluations, so any private member refused the class. undici's webidl
createIterator declares such a class (FastIterableIterator) at module load.
The two agree when every private name is used only on `this` inside the
class's own instance members (`this.#x`, `#x in this`), never inside a
nested class or non-arrow function, and the class has no static members:
each instance then meets only the methods of its own evaluation. Those
classes now compile; everything else keeps the refusal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
undici keeps two key tables that both hold a `Symbol('close')`, and
MockClient inherits a slot under one while declaring a method under the
other. Symbol slots were named by their printable spelling, so the two
collided ("distinct symbol keys sharing the printable name").
A key-table key whose description an earlier symbol identity already holds
now takes an ordinal in its internal slot name; inspect drops the ordinal
and prints Node's spelling for both. Const keys keep their plain names, so
programs that compiled before are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Interop the rewrite cannot finish now refuses its package only when the
compiled program can load that file: the module order closed over every
literal require() and import() and, in package JavaScript, every import
and export declaration (a computed specifier keeps every file loadable).
A package reached through a leaf subpath and types from its main entry
stays static. Auto mode also no longer refuses a whitespace/syntax
minified bundle whose interop helpers keep their names: the recognizers
judge it instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A CommonJS function with parameters that reads `arguments` was refused
because sloppy-mode `arguments` aliases the parameters. Strict code (a
'use strict' prologue on the file or an enclosing function, or a class
body) and functions with non-simple parameter lists (defaults or
destructuring) get Node's unmapped object, whose elements never alias the
parameters, so they now take the hidden arguments array the way ES module
functions and class methods already do. undici's
`webidl.argumentLengthCheck(arguments, 1, 'getCookies')` compiles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>