Commit Graph
5042 Commits
Author SHA1 Message Date
Chris Tate bcbae8a497 Address review feedback 2026-10-25 16:36:06 -05:00
Chris Tate 6f1efbf42b Remove the committed node_modules symlink 2026-10-25 15:56:53 -05:00
Malte Ubl a5512e5939 Merge remote-tracking branch 'origin/perf/shape-overlay-soundness' into HEAD
# Conflicts:
#	packages/compiler/src/frontend/shape-inference/static-objects.ts
2026-10-25 13:47:20 -07:00
Malte Ubl b04fea812d Merge remote-tracking branch 'origin/fix/js-dynamic-lowering' into HEAD
# Conflicts:
#	packages/compiler/src/frontend/pipeline.ts
#	packages/compiler/src/frontend/shape-inference/overlay.ts
#	packages/compiler/src/frontend/shape-inference/static-objects.ts
#	packages/compiler/test/shape-overlay-render.test.ts
2026-10-25 13:46:26 -07:00
Malte UblandClaude Opus 5.5 8b38aa5d8f Merge perf/shape-overlay-cost; keep the compiler self-hosting; record the new npm cases' order baselines
- array-concat.ts: a boolean predicate (the self-hosted compiler refused the
  truthiness test on an IrType | null | "dyn" value, SC2003).
- TypeScript 7 order baselines for concat-dynamic-args and
  evolving-let-index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:44:32 -07:00
Malte Ubl d4e83259bf Merge remote-tracking branch 'origin/perf/shape-overlay-cost' into HEAD 2026-10-25 13:44:15 -07:00
Malte Ubl 6c1275bb34 Merge remote-tracking branch 'origin/perf/shape-site-clones' into HEAD
# Conflicts:
#	packages/compiler/src/frontend/shape-inference/overlay.ts
2026-10-25 13:44:08 -07:00
Malte UblandClaude Opus 5.5 411852656c Merge perf/shape-analysis-budget; keep argsFrom in a Map the analyzer owns
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>
2026-10-25 13:42:14 -07:00
Malte Ubl 477751ce38 Merge remote-tracking branch 'origin/perf/shape-table-dispatch' into HEAD 2026-10-25 13:40:54 -07:00
Malte Ubl 922a53fa0c Merge remote-tracking branch 'origin/perf/shape-overlay-fallback' into HEAD
# Conflicts:
#	packages/compiler/src/frontend/shape-inference/static-objects.ts
2026-10-25 13:40:45 -07:00
Malte Ubl cbede2e5fd Merge remote-tracking branch 'origin/perf/static-objects' into HEAD
# Conflicts:
#	packages/compiler/src/frontend/pipeline.ts
#	packages/compiler/src/frontend/shape-inference/overlay.ts
#	tests/harness/npm.test.ts
2026-10-25 13:38:56 -07:00
Chris Tate f5c316fbc6 Cut the shape overlay's compile cost (#2249) 2026-10-25 15:12:53 -05:00
Malte UblandClaude Opus 5.5 e31e2ebe75 Dispatch a constant function table without a fallback when every caller passes one of its keys
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>
2026-10-25 13:10:25 -07:00
Malte UblandClaude Opus 5.5 484fc2cb13 Fold members stored after their namespace was created; skip the receiver read in JavaScript functions that never use this
- 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>
2026-10-25 13:10:12 -07:00
Malte UblandClaude Opus 5.5 439f973dd9 Format analyze.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:10:08 -07:00
Malte UblandClaude Opus 5.5 b3d4a3f341 Fix the stack's failing unit and snapshot tests
- analyze.ts keeps argsFrom in a Map the analyzer owns (compiler sources keep
  caches on their owners, not in weak collections).
- constant-graphs.test.ts: lib.math is stored after lib was created, so the
  report shows it as undefined|{...} with a graph of its own.
- maybe-promise-fences.ts: `record instanceof Promise` lowers to false since
  the base's neverPromise lowering; the snapshot no longer lists the refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:01:49 -07:00
Malte UblandClaude Opus 5.5 3c95271fc1 Record the TypeScript 7 order baselines of the stack's npm cases
static-objects, static-tables, concat-dynamic-args, evolving-let-index and
the three shape-* cases landed in this stack without preflight/order
baselines, so the TypeScript 7 parity job failed on every PR above them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 12:30:00 -07:00
Malte UblandClaude Opus 5.5 59d3dfdbfc Keep the compiler self-hosting: a boolean predicate in the concat helper
The self-hosted compiler refused the truthiness test on an
IrType | null | "dyn" value (SC2003, no layout for the union result).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 12:13:11 -07:00
Malte UblandClaude Opus 5.5 c3a72b2d3f Cut the shape overlay's compile cost: memo across builds, comparison first, one analysis for fold-only rewrites
- memo.ts carries a build's overlay verdicts (refused annotations and files,
  overlay off, the default build's deferred refusals) to the next build of the
  same JavaScript, in the build cache.
- Builds whose only refusals are deferred lower the program without the
  overlay first, and drop only what the added refusals blame; when the overlay
  goes off, that lowering is the build's.
- Method families join only methods a call reaches together (receiver unions,
  extends chains): ZodString's _addCheck returns ZodString, so zod 3 needs no
  reload.
- Refusals in a class's fields or at new C blame annotations that name C.
- Rewrites that only fold constant reads compose with the typed overlay
  planned on the same analysis (one rebuild and analysis instead of two).
- Shape analysis prefetches symbols only; input tracking notes a file's text
  once per tracker.
- A declaration a project type patch edited gets no overlay annotation
  (setShapeOverlayProtectedSpans, for the type-patches layer).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 12:12:45 -07:00
Chris Tate 2a0489a334 Keep the TypeError of receivers that may be null in static-object rewrites
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.
2026-10-25 13:23:54 -05:00
Chris Tate 88717dd3f5 Address review feedback 2026-10-25 12:16:30 -05:00
Chris Tate 54899bcc10 Leave factories that declare a class out of per-site clones
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.
2026-10-25 11:38:58 -05:00
Malte UblandClaude Opus 5.5 5455a4d817 Make the shape overlay's facts sound on the real-package suites
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>
2026-10-25 09:20:03 -07:00
Malte UblandClaude Opus 5.5 34eef83ef4 Decide concat spreading of dynamic values at run time; keep JS lets assigned from index reads unchecked
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>
2026-10-25 09:19:53 -07:00
Chris Tate e29ea20af1 Address review feedback 2026-10-25 11:02:41 -05:00
Chris Tate 6aa70ff3a6 Merge base branch 2026-10-25 11:00:33 -05:00
Chris Tate 7c0cbce93a Address review feedback 2026-10-25 10:42:26 -05:00
Chris Tate cae940eb0b Read added primitive members and unmatched exec groups as unknown values
- A member that String.prototype, Number.prototype, Boolean.prototype and
  Object.prototype lack reads undefined only while no write may add it.
  Assignments to one of those prototypes, Object.defineProperty,
  Object.assign, setPrototypeOf and delete on one, writes to an unknown
  value, and passing a prototype to unknown code add the name (or every
  name), and the reads already made take an unknown value as well. String
  indices past the end follow the same rule.
- RegExp.prototype.exec results hold undefined as well as strings: a
  capture group that did not take part in the match is undefined, as in
  String.prototype.match results.
2026-10-25 10:31:13 -05:00
Chris Tate e87dfe9398 Keep captured bindings and bound receivers in per-site clones
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.
2026-10-25 10:23:48 -05:00
Chris Tate 7721fbd92d Address review feedback 2026-10-25 10:19:26 -05:00
Chris Tate e324c07a66 Neutralize the overlay's doc ranges in one pass over the file
renderShapeOverlay rebuilt the whole source text once per JSDoc range, so
a file with many annotations cost time quadratic in its size. The ranges are
leading trivia of declarations, equal or disjoint, so the text is now built
once from the ranges in order.
2026-10-25 10:02:52 -05:00
Malte UblandClaude Opus 5.5 df02c72407 Analyze init factories once per call site
Static-object specialization's stage 3 prerequisite
(DESIGN-constant-objects.md): a function called from module
initialization (top-level code outside loops, or another such clone) is
analyzed once per call site, up to 64 sites per function and 4 nested
clones deep. The objects and arrays it creates are one allocation per
call site, and each clone runs at most once, so they are singletons: the
schemas zod's and valibot's builders create at init become constant
objects instead of "init-factory" near misses.

Stage 1 report (SCRIPTC_DEBUG_SHAPES=const):

- zod 3.25 (z.object bench): constant objects 7 -> 48 (fields 57 -> 155),
  init-factory near misses 12 -> 1, foldable reads 22 -> 72.
- valibot 1.1: constant objects 2 -> 5, init-factory 6 -> 0.

The overlay's annotations and the zod and namespace benchmarks are
unchanged. The overlay counts union arms rather than atoms, so the
per-site allocations of one record shape stay one typedef.
SCRIPTC_SHAPE_SITE_CLONES sets the per-function limit (0: off).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 07:34:05 -07:00
Malte UblandClaude Opus 5.5 d9c1b89b5d Cut the shape overlay's compile cost; keep package JSDoc it does not replace
Compile cost (Hono fixture: 53 s -> 99 s with the overlay before):

- The overlay is planned once per program text: a reload that only drops
  refused annotations renders the cached plan (overlay.ts plan/render
  split) and reuses the cached structural rewrites, instead of analyzing,
  reporting and planning again.
- The overlay's analyses run without per-call-site clones, which only the
  constant-graph report uses so far (SCRIPTC_SHAPE_SITE_CLONES opts in).
- A failing round also drops what the deferred refusals blame, saving a
  reload; deferred refusals no longer blame every annotation of their
  enclosing function (most of them exist without the overlay).
- A parameter whose `typeof` tests the proven values never pass stays
  untyped: the typed parameter would narrow to never in that branch,
  which the lowering refuses (zod 4's Doc.write).
- Deferred refusals in methods blame the arguments of the method's call
  sites (implicit instantiations), like those in functions.

Package JSDoc: the overlay neutralized the package's JSDoc in every
overlaid file, so declarations it does not type lost their documented
types and the lowering instantiated them per call site instead. In
commander, this changed which refusal `--help` reaches: the run passed
the refusal the default build stops at and failed later, in
Command.helpInformation, at a checked conversion of the package's own
documented contract (a help width that is undefined off a TTY). Nothing
the analysis proved was contradicted (SCRIPTC_SHAPE_CHECK_LOG logs no
inferred-fact failure). Only the leading JSDoc of the declarations the
overlay annotates is neutralized now; commander's overlay build fails
exactly where its default build does, and the OVERLAY_KNOWN entries go.

The overlay npm suite (134 cases), the shape tests, lint and format pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 07:34:05 -07:00
Malte UblandClaude Opus 5.5 fc3f0de981 Keep shape inference within its budget on large packages
The analysis exceeded its 20M-step budget on the Hono fixture (32.5 s)
and on moment 2.30 (60 s, two files), so it reported no facts for them.

- Widening: a value node holding more than 32 atoms (merged code that
  every value flows through, such as moment's utilities) becomes ⊤. Its
  values escape, so the facts stay sound, and later atoms escape instead
  of multiplying constraints. SCRIPTC_SHAPE_WIDEN sets the limit.
- A member a string, number or boolean prototype lacks reads undefined
  instead of creating a builtin atom per name (Hono's request and route
  objects meet strings in shared helpers).
- Members of builtin instances (`RegExp.instance`) no longer chain into
  static builtin paths.
- Symbol resolution is memoized per identifier.

Hono: 1.16M steps in 2.4 s with complete facts (55 constant objects).
moment: 115k steps in 0.4 s. zod 3 and the namespace benchmark are
unchanged (same steps and overlay annotations).

SCRIPTC_DEBUG_SHAPES_PROFILE prints where the solver's steps go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 07:34:04 -07:00
Malte UblandClaude Opus 5.5 0c0077da21 Call constant function tables and cross-module namespace members directly
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>
2026-10-25 07:34:04 -07:00
Malte UblandClaude Opus 5.5 0d4954f40c Never let the shape overlay break compilation
With the overlay on, a lowering refusal no longer fails the build. The
annotations the refusal depends on are dropped and the program is loaded
and lowered again, so those declarations compile untyped, as they do
without the overlay:

- Each overlay annotation carries an id (`@scriptcShape #<pos>`).
- shape-inference/fallback.ts blames the annotations of the declarations
  enclosing a refusal and of the declarations its expression names,
  following unannotated functions' returns.
- Rounds that stop converging drop whole files' overlays, then the
  overlay for the build. SC6103 reports what was dropped.

Also:

- The tsc gate's project world is built over the files as written; it
  used to see the overlay's files, which failed the typecheck of three
  npm cases.
- Arrays a builtin creates (match results, `new Array(n)`) are no longer
  typed by the overlay: they may have holes, and the library types them.
- compile() accepts `shapeOverlay: true`.

tests/harness/npm-shape-overlay.test.ts runs the whole npm suite with the
overlay on (npm.test.ts's body moves to npm-suite.ts): every case builds
and runs as in the default build, and a refused build that compiles with
the overlay matches Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 07:34:03 -07:00
Chris Tate 700526ab17 Merge base branch 2026-10-25 08:19:36 -05:00
Chris Tate d378a4dee7 Keep static-object rewrites where receivers, setters or enumerability differ
- 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.
2026-10-25 05:29:55 -05:00
Chris Tate 1b40ac0c64 Merge main 2026-10-25 05:15:58 -05:00
Chris Tate 2abe4a7e65 Undo the main merge on a stacked branch 2026-10-25 05:03:57 -05:00
Chris Tate f07329f569 Fix CI, address review feedback and merge main 2026-10-25 04:57:15 -05:00
Chris Tate 7db7b4dd86 Prove int32 array candidates in one analysis that revises dropped ones (#2196) 2026-10-25 04:39:23 -05:00
Chris Tate fa27df8b06 Speculate floor-mods and products by powers of two as masks and shifts (#2199) 2026-10-25 04:34:53 -05:00
Chris Tate fa02fcd8ae Report which packages and modules would run in a Node island (#1691) 2026-10-25 04:32:22 -05:00
Chris Tate 03b754e891 Convert dynamic values into nested unions in one pass (#1914) 2026-10-25 04:18:21 -05:00
Chris Tate 8f655df65e Hold a Map.get result as the entry's reference until the map can change (#2099) 2026-10-25 03:59:34 -05:00
Chris Tate 4050c0fb5c Merge main and skip package.json files without a scriptc key 2026-10-25 03:58:49 -05:00
Chris Tate e38f7c329d Speculate floor-mods and products by powers of two as masks and shifts
In a speculative integer loop, `v - Math.floor(v / 2^n) * 2^n` computed
the floor division as an arithmetic shift, multiplied it back by 2^n with
overflow and -0 checks, and subtracted with range checks. For a safe
integer v and n in [0, 52] the result is the floor-mod, which is v's low
n bits: the fast copy now computes it as `v & (2^n - 1)`, which lies in
[0, 2^n) and needs no check. A product with a power of two (a literal or
`Math.pow(2, n)`) becomes a shift, with one test that the other factor's
magnitude is at most (2^53 - 1) >> n; its zero result is +0.
2026-10-25 03:56:48 -05:00
Chris Tate c1f61c05c4 Merge main 2026-10-25 03:55:15 -05:00
Chris Tate e99b31dab1 Decode base64 eight characters at a time (#2197) 2026-10-25 03:45:47 -05:00