Commit Graph
3505 Commits
Author SHA1 Message Date
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
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 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 9cb9750f92 Set a trusted build aside while a getter runs outside a visit session
A getter or proxy trap that runs during a trusted build outside any
visit session now runs with the build's state set aside, as inside a
session. A conversion the getter starts, such as a recursive union
that opens and ends its own session, no longer clears the mark that
keeps the rest of the outer build from trusting its earlier match.
2026-10-25 03:34:33 -05:00
Chris Tate 342b770cf2 Keep a Map.get result owned when its key can replace the map's holder
The receiver of a borrowed Map.get is read before its key. When the key's
evaluation replaces the field or local that holds the map
(`this.m.get(this.key())` where key() assigns this.m), the read runs on
the old map, which only the statement still holds, and the borrowed
value outlives it. A borrow now also requires the read itself, receiver
and key included, to preserve reference edges; otherwise the read keeps
its own reference.

Corpus 8181 and the emission test cover a key method that replaces the
field.
2026-10-25 03:25:37 -05:00
Chris Tate 975270cef5 Decode base64 eight characters at a time
Buffer.from(text, "base64" | "base64url") decoded each four-character
group with four lookups and a sign test, then stored its three bytes one
at a time. The decoder now first takes complete groups two at a time:
each character's value comes from a table that holds it already shifted
into place within its group's 24 bits, a single bit test covers all
eight characters, and each group's three bytes go out in one four-byte
store whose last byte the next store overwrites. A group with any other
character (padding, whitespace, an invalid character) takes the
previous path, and the two-group step resumes after it, so line-wrapped
text keeps most of the gain. The destination already holds two spare
bytes, which cover the extra store byte.
2026-10-25 03:02:53 -05:00
Chris Tate 7e20a75c8f Split readline input in one scan and copy ASCII strings into buffers whole (#2192) 2026-10-25 02:59:39 -05:00
Chris Tate 669000de97 Compile large sanitized dev programs in shards (#2194) 2026-10-25 02:42:37 -05:00
Chris Tate 39b461f2fd Compile namespace-style npm packages: guarded declarations and var exports (#2172) 2026-10-25 02:37:06 -05:00
Chris Tate 6bd675420b Merge main 2026-10-25 02:06:50 -05:00
Chris Tate 8b0921243f Compile large sanitized dev programs in shards 2026-10-25 01:59:32 -05:00
Chris Tate 3ef34770bd Read optional elements of bindings that hold undefined as undefined
`a?.[i]` on a binding whose type admits undefined, such as a
`string[] | undefined` parameter, threw "undefined is not representable
in the target union" when the binding held undefined. Element access
replaced an identifier receiver whose storage has an undefined arm with
a checked narrow to its present arm before the optional chain ran, so
the chain saw a receiver that is never nullish and lowered `?.[` as a
plain element read.

Element access now leaves the receiver to the optional chain when the
access carries `?.`; the chain tests the undefined arm and lowers the
access again inside its guard. This applies to arrays, tuples and
record keys. Property access (`a?.length`, `a?.at(0)`) already lowered
through the chain.

Corpus 8330 matches Node's output.
2026-10-25 01:41:31 -05:00
Chris Tate db2cb5c99e Split readline input in one scan and copy ASCII strings into buffers whole
readline split redirected input byte by byte, twice per chunk (once to
find whether the chunk ends a line, once to split it). Before splitting
it copied each chunk out of its Buffer and into a scratch buffer, decoded
it into a new string, and copied the unfinished line and the whole chunk
into a joined buffer. Now:

- one scan per chunk: memchr for a CR once, then LF to LF with memchr
  when the chunk has none, and the LF, CR and CRLF split otherwise;
- the unfinished line joins only the first line of the chunk;
- an ASCII chunk with no partial UTF-8 sequence held from the previous
  one is split where it is, and a chunk that is not shared memory is
  read from its own storage. Lines are still copied into their strings
  before the first is delivered.

Buffer.from(string, 'latin1' | 'ascii') encoded character by character;
an ASCII prefix, all of an ASCII string, is now copied with memcpy.

Line events, their order and the CRLF delay are unchanged. Corpus 8344
covers lines joined across chunks, a line longer than a chunk, empty
lines, mixed line endings and UTF-8 split across chunks. The new
line-reader workload reads a 67 MB log through readline over a file
stream.
2026-10-25 01:35:28 -05:00
Chris Tate 2e052854c5 Merge main 2026-10-25 01:21:07 -05:00
Chris Tate 25d0ad9193 Parse JSON into discriminated unions in one pass (#2189) 2026-10-25 01:16:43 -05:00
Chris Tate 9d7f0182e5 Keep CommonJS wrapper names and redeclared exports out of namespace rewrites
- 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.
2026-10-25 01:04:55 -05:00
Chris Tate 5d3e0c7a8b Support X509Certificate fields and methods, TLS certificates and tls.Server contexts (#2052) 2026-10-25 01:00:24 -05:00
Chris Tate 3243e843b3 Call the declared base method for super calls in JavaScript method values (#2188) 2026-10-25 00:57:31 -05:00
Chris Tate 4963dcf8ec Cache absent properties in the dynamic tier's property caches (#2186) 2026-10-25 00:46:10 -05:00
Chris Tate e5ba073468 Parse JSON into discriminated unions in one pass
`JSON.parse(text) as T` for a discriminated union of records took the
checked-dynamic route: it parsed the text into a dynamic tree, matched
and copied that tree into records, then freed it. The one-pass schema
parser now builds these unions directly, as record members, array
elements and whole documents. For an object, a shallow scan of its
members finds the discriminant's value, which selects the one record arm
that holds that literal, and the object is parsed with that arm's
schema. An object with a missing, non-string or unknown discriminant, or
any member the arm rejects, declines, and the checked route then gives
the result or the error as before.

The compiler admits unions whose record arms each name their string
literals, every literal naming one arm, plus null and undefined arms.
2026-10-25 00:44:25 -05:00
Chris Tate 478feb15cd Call the declared base method for super calls in JavaScript method values
Since 76735a8d9 (#1975), a `super.m()` call in a JavaScript class method
whose `this` is the call-site receiver reflected the base class's whole
prototype and read `m` from it. An instance's member lookup first tests
its property bag, which inherits from its class's prototype data, so once
the base's prototype was reflected, `child[k]` for a method the subclass
overrides found the base's method there: `value[name](arg)` on a subclass
instance called the base method.

While no code reads a prototype method by name, `super.m()` now calls the
declaring class's receiver-independent method value with the receiver, as
the method's [[HomeObject]] lookup answers, without reflecting the base
prototype. Programs that read prototype methods by name keep the
reflected lookup.
2026-10-25 00:26:55 -05:00
Chris Tate 3e54e0edb3 Read fs.promises as a module and check fs read and write arguments as Node does (#2098) 2026-10-25 00:23:19 -05:00
Chris Tate 2269fd9e1f Merge main 2026-10-24 23:48:53 -05:00
Chris Tate 3e115ce08c Cache absent properties in the dynamic tier's property caches
A read site's cache only remembered properties it found on the receiver
or its first prototype. Reads of properties an object lacks, which
validation libraries make constantly (zod reads ctx.skipChecks,
payload.memo, x.aborted and _zod.optin on every check), walked the whole
shape chain and then repeated the lookup on the generic path every time:
7.9 million such reads in one zod benchmark run.

- Read sites call a new probe, scr_js_get_probe, that answers undefined
  for a key the receiver and its chain of plain shaped objects (at most
  one prototype, ending in the default prototype) lack, and caches that
  answer. The entry holds while the receiver's shape, its prototype link,
  and that prototype's shape are unchanged. Its tag and placeholder
  holder make the inline entry-0 test in compiled code miss, so the
  answer always comes from the probe. Method reads keep
  scr_js_load_probe, so a missing method still reaches the slow path and
  its "is not a function" error. Names of Object.prototype members are
  never cached as absent.
- Receivers of one shape with different prototypes (zod's internals
  objects) each keep an entry instead of replacing one another.
- A site whose four-way record is full consults a per-thread table keyed
  by shape and key before walking shapes, and fills it.
2026-10-24 23:47:41 -05:00
Chris Tate 468ef7692b Drop the refused never-promise instanceof case from the maybe-promise fences
instanceof Promise on values that are never promises now compiles to
false after the operand is evaluated, so the diagnostics fixture no longer
lists it as refused. 8326-never-promise-instanceof covers records, arrays,
dates, collections, regular expressions, byte arrays and unions with null,
and a call operand that still runs.
2026-10-24 23:35:56 -05:00
Chris Tate 37031fa7c2 Convert a loop's string receiver to UTF-16 at once, and publish it only when the scan holds another string (#2180) 2026-10-24 23:34:50 -05:00
Chris Tate f06f47aca1 Resolve promises with an argument in the rejectUnauthorized corpus program
8179-tls-reject-unauthorized-values.mjs called the resolve function of an
untyped new Promise() with no argument. tsgo types that function as taking
one argument and reported SC0001 for each call, while tsc-ts accepted them,
so the checker-identity comparison failed. The calls now pass undefined.
2026-10-24 23:27:46 -05:00
Chris Tate d2326c4c89 Look up a number Map key on its integer view (#2170) 2026-10-24 23:27:03 -05:00
Chris Tate 863a71e37c Number hoisted speculative leaves per function (#2179) 2026-10-24 23:19:34 -05:00
Chris Tate faa54f6443 Cache only number-key integer views that need no conversion
A Map/Set lookup's integer key view was cached for the function by the key
operand's name. When the view was a conversion emitted at a lookup in a
branch, a later lookup of the same operand (a wide integer constant) reused
it from a block the conversion does not dominate, and LLVM rejected the
module. Only the value's own integer or uint32 view and integral literals
are cached now.
2026-10-24 22:58:41 -05:00
Chris Tate add5f4869b Convert a loop's string receiver to UTF-16 at once, and publish it only when the scan holds another string 2026-10-24 22:53:15 -05:00
Chris Tate 688c1e03a1 Keep undefined results, literal text and hoisted vars in the shape overlay
- 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.
2026-10-24 22:52:52 -05:00
Chris Tate c21c26495d Start integer loops from the integers their starts carry (#2174) 2026-10-24 22:46:20 -05:00
Chris Tate f4eda0fb6c Number hoisted speculative leaves per function 2026-10-24 22:43:27 -05:00
Chris Tate be878b4eb9 Freeze class instances held in untyped values (#2061) 2026-10-24 22:43:12 -05:00
Chris Tate 0fbb47382d Read elements of native arrays passed to untyped code by index (#2171) 2026-10-24 22:32:09 -05:00
Chris Tate 65ef34218b Prove a typed-array loop's views disjoint and give each its own alias scope (#2167) 2026-10-24 22:26:22 -05:00
Malte UblandClaude Opus 5.5 22b686c7da Compile pre-module constructor functions as classes and namespace functions as direct functions
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>
2026-10-24 20:25:14 -07:00
Malte UblandClaude Opus 5.5 9d3365c99b Compile namespace-style npm packages: guarded declarations and var exports
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>
2026-10-24 20:24:57 -07:00
Malte UblandClaude Opus 5.5 4a8a6567ca Add the shape overlay: typed JavaScript from analysis-proven facts (opt-in)
The shape overlay (DESIGN.md, approved route) rewrites the JavaScript of
opted-in npm packages with JSDoc types the shape analysis proves: record
typedefs for non-escaping literal sites (optional fields for spread
sources), class field, parameter, return and local types, receiver casts,
and branches the analysis proved unreachable replaced by checks. The
lowering trusts only annotations carrying the overlay's marker; package
JSDoc stays a hint (its type tags are neutralized in overlaid files).

- Inferred-fact checks: every checked conversion lowered in an overlaid
  file records its site, and a failure throws a distinct internal error
  ("scriptc internal error: an inferred type was contradicted at <file:line>
  … This is a bug in scriptc's shape analysis, not in the program's data")
  naming the opt-outs; SCRIPTC_SHAPE_CHECK_LOG makes checks log instead
  (validation mode).
- Opt-outs: `--no-shape-overlay`, and `"scriptc": {"packages": {"<name>":
  {"shapes": false}}}` per package.
- Build report: SC6103 counts the declarations typed, the checks inserted
  and the branches replaced.
- `x instanceof Promise` on plain data folds to false.

The overlay runs only with SCRIPTC_SHAPE_OVERLAY=1 while it is new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:24:51 -07:00