- A function body or class static block treats its top-level function
declarations as var-scoped: duplicates are legal in strict code, and a
let/const/class of the same name is SC0006.
- Strictness is the code around a node: a function declaration's own
"use strict" no longer makes its declaring context strict.
- Every part of a class is strict; object literal methods and accessors
are strict only by their own prologue or their surroundings, and an
arrow function's block body can carry "use strict".
- The bare compareArray reads b.length, then a.length, and a.length again
before every element pair, converting it as the upstream `i < a.length`
does, so a string length compares its elements.
- Tests that relied on the old handling of undeclared names now use
constructs that are still refused or still compile: the installed-entry
diagnostics fixture reports a syntax error (a read of an undeclared name
is now Node's run-time ReferenceError), and static-objects-order's
loops.js stores from its for-in head into `undefined` instead of an
undeclared name (a store into an undeclared name refuses its package with
SC1091).
- lowerForOfEmptyArrayArm binds the `Set | Map | string | never[]` source
to a hidden local before the tag test, so a call or getter in the loop
head runs once and the loop walks the value the test saw.
- markTarget marks a structural view as a collection view when its source
is already one, and reruns the fixpoint when a view marked earlier
becomes a collection view, so a Map or Set handed through two views keeps
answering through the collection.
- Promise.all in a JavaScript source whose call types as Promise<any[]>
(an unannotated parameter) runs the engine's Promise.all over the entries
as checked-dynamic values when they are typed promises. A merged
`Promise<number | string>[]` argument reaches that call since union
promise arms merge.
Corpus 8419 and 6956 and the js-own-tostring npm fixture cover each case.
A `for (k in o)` or `for (k of a)` whose head is an expression stores into
it on every iteration. The strict-mode check of constructor and method
bodies now treats such a head like the target of `=`: a property store
not made through `this`, a store into a read-only global, and a store into
an undeclared name keep the constructor function, since sloppy mode drops
or completes those stores where a class body throws.
The static-objects-order fixture gains loops.js, whose methods store from
a for-in head into an undeclared name and from a for-of head into a
property of a string.
A module reached by an already-rejected literal import() is `covered`, but a
direct import() of that module still needs its own rejection entry: its graph
rejects when that import runs, so it must not join the compiled graph merely
because another rejected import also reaches it. Without the entry the module
is compiled and lowering fails on the unloaded binding instead of the
import() rejecting at run time as Node does.
Walk each direct root independently and extend the unloaded-dynamic-import
fixture with a direct import() of the rejected graph's dependency.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compiler sources keep caches on their owners, not in weak collections
(compiler-weak-collections.test.ts).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the key is a parameter of a function that never escapes, every call
passes a string literal key of the table, and the body neither assigns the
parameter nor uses arguments, the dispatch needs no arm for other keys. In
nsbench the fallback boxed the Vec probe into a dynamic call, so every Vec
carried the property bag and lived on the heap; without it the workload
inlines into one loop. SCRIPTC_SHAPE_EXHAUSTIVE_TABLES=0 opts out.
nsbench (n=20000, overlay on): 36 -> 8.2 ms (Node about 21 ms).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- static-objects: a primitive member stored once by an unconditional
top-level statement of the object's own file (app.math.EPSILON = 1e-9)
reads as its literal where the read provably runs after the store: inside
functions when the statements before the store run no code, and in later
top-level statements of that file. SCRIPTC_SHAPE_FOLD_STORES=0 opts out.
- JavaScript functions read the ambient receiver at entry only when this
occurs in their parameters or body outside nested non-arrow functions.
- npm case static-stores pins both, with the overlay on and off.
nsbench (n=20000, overlay on): 64.4 -> 36 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scriptc.typePatches entry can now replace a declaration file of the
package (or of its @types twin, written @types/<name>/<path>) with a
local .d.ts ("files"), or give the package a declaration entry ("types").
A supplied entry answers the package's types file and the declarations
TypeScript probes beside its JavaScript entries, and exists for the
program host, the resolver and npm-static's untyped-package test, so a
package without declarations becomes a typed package. Declaration patches
still apply on top. A files target the installed copy does not ship is
SC8031.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rewrites decide on a read's non-nullish receivers, since the analysis
also gives an absent key's read undefined (ns.util before ns.util = {...}
runs). A rewrite then removed the property access, so a receiver that was
in fact null or undefined no longer threw.
- A read whose receiver may also be nullish is rewritten to the read as
written followed by the replacement in a comma expression: a direct
reference becomes (recv.k, name) and a folded member (recv.k, literal),
so a nullish receiver throws Node's TypeError and any other receiver
yields the same value.
- A function table call through such a table reads table[key] first,
before its arguments, where the call reads it.
- The static-tables case reads, calls and dispatches through receivers
that are the constant object or undefined, and records the ts7
baselines of the npm cases added in this change.
A project can now patch a package's declarations in package.json:
"scriptc": { "typePatches": { "<package>@<range>": { "declarations":
{ "<selector>": "<type>" } } } }. A leading "|" widens the declared type,
any other type replaces it. Selectors name declarations by path
(Type.member, fn(), fn(param), list()[], Env[string], export=(), default,
file.js:local()) and apply to the package's .d.ts, its JSDoc and its
@types twin alike.
Patches edit file text where every consumer reads it: the program host's
source reads (both checker worlds) and the npm-static passes that read
package files themselves. Only type nodes and JSDoc type tags change, and
no edit adds or removes a line. Installed copies are selected by version;
an @types twin by the runtime package's installed version. Patches that do
not apply as written warn (SC8030-SC8033) and the warnings repeat on
executable cache hits. Without the setting nothing changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A class's constructor and methods are analyzed in receiver contexts
rooted at each member. Inside a per-site clone of the function that
declares the class, they resolved that function's bindings in the
default context: their writes through captured variables were lost,
and reads of the written objects folded to stale literals. A function
with a class declaration or expression anywhere inside is no longer
cloned per site.
The static-tables case adds a factory whose class method writes the
factory's object at initialization. It prints Node's `1,1,1` (it
printed `1,1,0`), and the count of constant reads stays at 8. zod3's
report is unchanged: 180 constant objects and 360 foldable reads.
The overlay validation suites (npm, npm-static, three, Effect 3 and 4) now
pass with the overlay on. Each cause had an npm repro or suite case that
failed with the overlay and passed without it:
Analysis facts:
- Symbol members: Symbol.for/keyFor are functions, well-known symbols are
symbols, other members unknown.
- Reads that may answer undefined: index reads past the end (unless the
enclosing for loop proves them in bounds of an array that never shrinks or
has holes), computed reads of objects, keys an object literal lacks at
creation, optional-chain short circuits, `let x;` before assignment.
- Object.create(proto) inherits proto members; arguments past the declared
parameters are unknown; `new` of a primitive answers nothing.
- Arithmetic on bigints answers bigints (number|bigint is never printed).
Overlay:
- Package JSDoc is a declaration: it stays as written and the overlay adds
only types the JSDoc does not declare.
- Objects held by module-level bindings (and what they hold) are never typed
as records: a typed return would copy them.
- Only checks of overlay facts carry the inferred-fact message.
- static-objects ignores nullish receivers outside optional chains; folded
literal keys are unparenthesized.
Fallback (the overlay never adds a failure the default build lacks):
- the overlay state is kept per load (a later load reset it, so the
fallback never ran);
- deferred refusals in unreached code count;
- a preflight, deferred refusals or IR the program without the overlay does
not have make the build compile without the overlay.
CI: the shape_overlay job builds its own native packages; a manual run with
scope shape_overlay runs it alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three JavaScript lowering bugs threw a checked conversion where Node answers:
- concat with a checked-dynamic argument lowered it as an element, so an
array argument threw. The argument now spreads when it is an array at run
time and is appended otherwise.
- `let c; while ((c = str[i++]) !== "]" && c)`: the checker types c from the
index read, without the undefined a read past the end answers. Evolving-let
inference ignores index-read writes, and reads of a JS `let x;` statement
binding keep the actual value.
- Strict equality on a flow-narrowed dynamic JS name compares unchecked.
npm cases concat-dynamic-args and evolving-let-index match Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two cases where a per-site clone lost writes, so later reads of the
written objects folded to their initial literals:
- A function walked in an enclosing clone's or receiver's context is
no longer cloned per site. The clone was its own context root, so the
variables it captures resolved to the default context's bindings, and
its writes through them reached no object.
- A clone's receiver now receives what the function's own receiver
does, such as the object `bind` supplies, and an arrow's clone keeps
the arrow's lexical `this`. Before, a bound function called at module
initialization wrote through a fresh, empty receiver.
The static-tables case writes at initialization through a helper's
captured binding and through a bound function. It prints Node's `1,1`
(it printed `0,0`), and the count of constant reads stays at 8. zod3's
report keeps its 180 constant objects and 360 foldable reads.
Static-object specialization for namespace-style JavaScript, with the
shape overlay on (shape-inference/static-objects.ts):
- Functions stored in object literals that a top-level statement creates
(`ns.registry = { run: function () {} }`) are hoisted to module
functions, like those stored by assignment.
- `table[key](...)` on exactly one constant object whose members are all
hoisted functions, with a string key, compares the key against each
member and calls its function directly; any other key reads the table
as written. The members do not use `this`, the table has no accessors,
and comparing a string runs no code.
- A read of a primitive member written in the object's own literal
(`Kind = { CIRCLE: 1 }`) is replaced by the literal. Members stored
after the object was created stay reads (they could be read first).
- A read in another JavaScript file of a hoisted namespace member refers
to it through the defining module's exports: the member joins the
module's `module.exports = { ... }` literal, and the reading file binds
the module right after the top-level statement that requires it. This
applies when the exports object is constant and nothing enumerates,
reflects on, or lets it escape, and the module does not require the
reading file.
The overlay's cross-file class types and member function types are now
on by default (SCRIPTC_SHAPE_CROSS_FILE=0 and
SCRIPTC_SHAPE_MEMBER_FUNCTIONS=0 turn them off).
The analysis records computed callee reads as computed reads.
New npm case static-tables (with a JavaScript consumer) and
tests/harness/static-tables.test.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A direct call drops its receiver's evaluation, so a name receiver must
hold its value whenever the read runs: a parameter, a function
declaration, or a variable declared with an initializer by an earlier
top-level statement and never assigned again. A variable read before it
is assigned (undefined, a TypeError) or in its temporal dead zone (a
ReferenceError) keeps the member read.
- A store between a constructor and its methods counts as a definition
only when its receiver is a constant object, which has no setters,
reached without running code, and its key is not `__proto__`. A store
that may run a setter keeps the constructor function, since the setter
could see the prototype without its methods.
- `propertyIsEnumerable` asked of the prototype or the constructor, or
reached other than as a direct method call, keeps the constructor
function: assigned methods are enumerable and class methods are not.
The static-ns fixture covers each case and prints what Node prints.
- A guarded declaration of a name the CommonJS wrapper defines (`var
exports = exports || {}`, and `module`, `require`, `__filename`,
`__dirname`, `arguments`) reads the wrapper's value, not undefined, so
it keeps its text instead of becoming a fresh object.
- The write scan counts destructuring assignments, and destructuring
for-in/for-of targets, as writes, so a hoisted function that sets the
namespace before its guarded declaration keeps the declaration as it
is.
- An exported `var` that a later declaration of the same name sets
again is not settled at the export: importers would read the
redeclared value, where Node copies the first one into
module.exports. It stays refused (SC1090), as before. The npm case
var-export-redeclared pins it.
- A function's results include undefined when a call can run off the end
of its body or reach a bare return, so the overlay's return type keeps
undefined and a caller's branch on it stays reachable. `new` of an ES5
constructor ignores primitive results, and functions called with `new`
get no @returns tag.
- Package JSDoc tags are neutralized only in comments: string, template
and regular expression literals and JSX text keep their text.
- A branch replaced as unreachable keeps the var names it declares that
its scope declares nowhere else, so references outside it read
undefined. A branch that declares a function in a block is kept.
- The shape-overlay-facts npm case covers all three; four files of the
overlay change are formatted.
With the shape overlay on, a first round of structural rewrites runs before
the typed overlay (shape-inference/static-objects.ts, stage 2b of
DESIGN-constant-objects.md), proven by the constant object graphs:
- An ES5 constructor stored by a top-level statement whose prototype is a
constant object of plain methods becomes a class declaration with those
methods, when the program cannot tell: the function is only ever
constructed and never escapes, its prototype is never replaced, nothing
enumerates instances with for-in or reflects on the prototype or the
function, nothing reads their names or sources, the bodies are
strict-mode safe, and only definitions run between the constructor and
its last method.
- A constant namespace member holding a `this`-free function expression
becomes a function declaration, and reads of the member in the same file
whose receiver is exactly that constant object refer to it directly.
The rounds rerun the analysis on the rebuilt program, the overlay now also
covers the program's own JavaScript files, and the build report counts the
rewrites. A JavaScript switch whose case tests lower to another type than
the discriminant compares with === instead of being refused.
Namespace benchmark (n=20000): 1.55 s -> 0.42-0.47 s, output identical to
Node (typed TypeScript twin 85 ms, Node 21 ms).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two refusals kept pre-module JavaScript packages from compiling:
- `var ns = ns || {}` (and `?? {}`): the read in the variable's own
initializer made the checker type the namespace `any`, and requirers
then failed to reach the binding (SC1030). In a module, the read sees
the hoisted `undefined` whenever nothing else writes the variable, so a
new package-source normalization respells the declaration as its
default (`var ns = ({})`) when no other declaration, assignment, update,
function, class, parameter or local of that name exists in the file and
no earlier top-level statement mentions it.
- `module.exports = { ns }` with a `var` binding was refused as exporting
a mutable binding by reference. A `var` (or `let`) whose initialized
declaration precedes the export statement at top level and that nothing
reassigns exports exactly the value Node copies, so it now aliases like
a const.
The namespace benchmark (results/design-shape-inference/nsbench) now
compiles and matches Node; the new npm case legacy-namespaces pins a
namespace library with guarded declarations, constructor functions with
prototype methods, enums, a revealing module and a handler registry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two refusals kept pre-module JavaScript packages from compiling:
- `var ns = ns || {}` (and `?? {}`): the read in the variable's own
initializer made the checker type the namespace `any`, and requirers
then failed to reach the binding (SC1030). In a module, the read sees
the hoisted `undefined` whenever nothing else writes the variable, so a
new package-source normalization respells the declaration as its
default (`var ns = ({})`) when no other declaration, assignment, update,
function, class, parameter or local of that name exists in the file and
no earlier top-level statement mentions it.
- `module.exports = { ns }` with a `var` binding was refused as exporting
a mutable binding by reference. A `var` (or `let`) whose initialized
declaration precedes the export statement at top level and that nothing
reassigns exports exactly the value Node copies, so it now aliases like
a const.
The namespace benchmark (results/design-shape-inference/nsbench) now
compiles and matches Node; the new npm case legacy-namespaces pins a
namespace library with guarded declarations, constructor functions with
prototype methods, enums, a revealing module and a handler registry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zValidator's default failure hook serializes zod 4's ZodError with
`name` after `message` natively, and with the first `name` zod's factory
assigns ("$ZodError") when the second assignment took the typed slot.
ZodError instances are views of a JavaScript class extending Error: each
view rebuilds `message` and `name` from the error slots at fixed
positions (after `stack`, and after every other property), and the
property bag that holds the dynamically added properties copied its
committed, possibly stale, values over them.
A JavaScript Error subclass with a property bag now marks its class
(errorBagOrder), and its view calls scr_error_dyn_follow_bag after the
bag is copied: `name` and `message` move to follow the property they
followed when the bag was committed, and data values come from the
slots. TypeScript classes keep their views.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zod 4's z.record rejected every plain object natively ("expected record,
received object"): its isPlainObject reads o.constructor.prototype, which
was undefined, and asks whether that prototype has its own isPrototypeOf,
which Object.prototype answered false.
- The Object, Array and Error constructors held as values have their
builtin prototype as a non-writable, non-enumerable, non-configurable
own `prototype`; reading Function's constructor value's prototype
throws SC2020 instead of answering undefined.
- hasOwnProperty / Object.hasOwn on Object.prototype answer its builtin
members, and on Array.prototype its constructor;
Object.getOwnPropertyNames lists them in Node's order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>