Commit Graph
6750 Commits
Author SHA1 Message Date
Chris Tate 9cb8ed3ce5 Map resolved empty mapped types as the empty object type (#2127) 2026-10-24 14:25:04 -05:00
Chris Tate d388cdd9cf Merge main 2026-10-24 14:20:14 -05:00
Chris Tate 02c2c89fd6 Merge main 2026-10-24 14:18:51 -05:00
Chris Tate 40faf6a9f1 Collect the native compiler profile on the release commit
The release workflow's new cli-profile job runs
scripts/refresh-cli-profile.mjs on Linux x64 and uploads the profile.
Every platform's native CLI build uses it through
SCRIPTC_NATIVE_PROFILE_USE, and the GitHub release attaches it as
scriptc-<version>-cli.profdata.gz. When the job fails, the CLI builds
use the committed profile.

build-native-cli.mts accepts a gzip-compressed profile in
SCRIPTC_NATIVE_PROFILE_USE when the name ends in .gz.
refresh-cli-profile.mjs creates the directory of --output before it
starts.
2026-10-24 14:16:32 -05:00
Chris Tate 7499f9bd39 Keep versioned loops' slow copies free of speculative integer copies (#2137) 2026-10-24 14:14:40 -05:00
Chris Tate 4efcbbdfe4 Report inferred shapes for untyped npm JavaScript (#2036) 2026-10-24 13:49:30 -05:00
Chris Tate 3f03c90e86 Keep versioned loops' slow copies free of speculative integer copies
The copy of a dense-array, byte-window, counted, callback or typed-array
versioned loop that runs when its guard fails no longer gives its loops
speculative integer or int32 kind copies, which emitted each loop again for
a path that rarely runs. Proven integer loops, which emit one copy, are kept
in it.
2026-10-24 13:47:17 -05:00
Chris Tate bf02b9d7cc Write a number-keyed Map update through the entry its read found (#2130) 2026-10-24 13:38:20 -05:00
Chris Tate 3896957230 Report JSON.parse syntax errors with V8's messages and positions (#2129) 2026-10-24 13:34:12 -05:00
Chris Tate 908d87d3e0 Key widened returns by their type-parameter bindings and skip self-calls
Two review findings on the widened-return cache:

- A nested function lowered under different type-parameter bindings reused
  the record layout of the first instance it was widened in. In
  `function outer<T>(x: T) { const inner = () => ...; return inner(); }`,
  the closures of outer<string> and outer<boolean> returned the records of
  outer<number>, and returning one threw a TypeError. A kept result now
  records the bindings it was mapped under (typeParamBindings, its
  checker-type and implicit-any twins, and whether an erased body is
  lowering) and answers only under the same ones.
- A function that calls itself on one return cut its own widening, so its
  result was never kept and a chain of such functions forwarding to each
  other on two branches was scanned 2^depth times. A call of the function
  itself returns its own type, whose arms add nothing, so the scan now
  skips it.
2026-10-24 13:32:14 -05:00
Chris Tate a8c0fa772b Use fixed timestamps in the fs.cp preserveTimestamps test 2026-10-24 13:28:53 -05:00
Chris Tate c33d693775 Answer instanceof against the primitive wrapper constructors (#2126) 2026-10-24 13:28:00 -05:00
Chris Tate 9dfaddf7bc Check unchanged frontend inputs by file identity before hashing them (#2128) 2026-10-24 13:24:46 -05:00
Chris Tate 587443428c Link Linux executables with lld when the driver finds it
Linux executables linked through the clang driver with its default linker,
GNU ld. lld links the same objects in about half the time.

- Native and Node-hosted Linux exe links pass -fuse-ld=lld when ld.lld is
  beside the driver's executable (where clang looks first) or on PATH, and
  the default driver is in use (no SCRIPTC_LINKER, no sanitizer link).
- A link with lld that fails runs again with the driver's default linker,
  whose error is the one reported when that fails too.
2026-10-24 13:22:26 -05:00
Chris Tate b78520559f Follow values through shadowed members, unknown keys on arrays, JSON callbacks and proxies
- Named loads also read what was written under that name or an unknown key where an own field
  shadows a builtin: an instance's constructor, a function's call, apply, bind, prototype, name
  and length, a class's prototype and name, an array's methods. Named loads of arrays read what
  was written under unknown keys, which computed stores on arrays now record.
- Computed stores into builtins, promises and collections escape, as named stores there do.
- JSON.stringify calls toJSON on arrays, functions and classes, and on values written under
  unknown keys. A JSON.parse reviver's results are part of the result and of later calls' values.
- Reflect.construct(Proxy, ...) and globalThis.Proxy record a proxy operation, as new Proxy does.
2026-10-24 13:20:02 -05:00
Chris Tate c244ddc4c5 Answer 'send', 'disconnect' and 'channel' in a child process from its IPC channel
A child process has these members only when it was created with an IPC
channel (fork, or an "ipc" stdio entry), and keeps them after the channel
closes and after the child exits. The 'in' test on native handles answered
true for every declared member, so a child spawned without IPC reported
them and entered IPC-only branches. The three names now answer from the
runtime: scr_child_ipc_present reports whether the child was created with
a channel.

Corpus 8290 tests a forked and a spawned child before and after
disconnect and exit.
2026-10-24 13:18:19 -05:00
Chris Tate f8f3194e0c Merge main 2026-10-24 13:13:52 -05:00
Chris Tate 29a78af70b Merge main 2026-10-24 13:13:07 -05:00
Chris Tate 7ca5bd465b Keep while-loop counters bounded by a typed array length in integers (#2124) 2026-10-24 13:08:15 -05:00
Malte UblandClaude Opus 5.5 50ccfbcbe2 Report inferred shapes for untyped npm JavaScript (shape inference, stage 1)
SCRIPTC_DEBUG_SHAPES=1 runs a whole-program allocation-site points-to
analysis over opted-in npm packages' JavaScript and the user files that
call into them, and prints which object and array literals a native
record would represent exactly, the inferred class field types, the
census of writes and reflection on untracked receivers, and the hot
functions' parameter and return types. Report only: nothing reaches the
lowering, and the analysis runs only when the variable is set.

Design: results/design-shape-inference/DESIGN.md (scriptc-perf).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 11:07:57 -07:00
Chris Tate 94c650a9c2 Write a number-keyed Map update through the entry its read found
m.set(k, f(m.get(k))) and a get followed by a set of the same key, with a
number or boolean value, now reuse the entry the read resolved, as
string-keyed maps already do: the write overwrites the value in place
when the map is the same and the key has the read's bits, and takes the
ordinary set otherwise. Before, the write probed the table again: the two
lookups were not merged, because each sits behind its own direct-index
test.
2026-10-24 13:07:38 -05:00
Chris Tate 50b7c806ce Check unchanged frontend inputs by file identity before hashing them
A warm native build validates its frontend cache entry by reading and
SHA-256 hashing every source and declaration file the frontend read.

A tracked read now also records the file's identity (device, inode,
size, modification and change time) when the identity is the same before
and after the read and the file had been unchanged for two seconds. A
later check accepts a file whose identity is unchanged without reading
it, and hashes it otherwise. A write always gives a file a newer change
time, which tools cannot set back, so an unchanged identity means the
content hashed with it. The equivalent-edit check uses the same identity
for the unchanged sources.
2026-10-24 12:52:54 -05:00
Chris Tate 0ea0707861 Reuse dev builds' frontend output after edits that keep token positions (#2123) 2026-10-24 12:51:02 -05:00
Malte UblandClaude Opus 5.5 864feec4cf Map resolved empty mapped types as the empty object type
A mapped type with no members was refused (SC2001) to keep `Partial<T>`
inside a generic body, where keyof T is still unknown, from interning a
wrong empty record. That also refused mapped types the checker resolved
from concrete inputs whose key filter keeps no key: `Record<never, T>`,
`Partial<{}>`, and a catalog type such as
`{ [K in KeyOf<Data, "image"> as Id<K>]: Model }` for data without image
models. When the mapped type comes through an alias whose arguments mention
no type parameter (or a module-level alias without arguments), it is the
empty object type and lowers like `{}`.

pi-ai declares its per-provider image and classifier model catalogs this
way; most providers have none, which refused 105 declaration groups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:50:44 -07:00
Malte UblandClaude Opus 5.5 b11cd04ce9 Answer instanceof against the primitive wrapper constructors
`value instanceof Boolean` (or Number, String, BigInt, Symbol) died at run
time in JavaScript with Node's TypeError for a function without a prototype
object, because those constructors are compiled as plain conversion
closures; in TypeScript the right-hand side was refused (SC1090). A
compiled program never creates a boxed primitive (`new Boolean(...)` has
no lowering) and a primitive is never an instance, so the test is false
for every value. It now folds to false after evaluating the operand.

typebox's value guards (IsBoolean, IsNumber, IsString) test this way, so
pi's tool-schema construction and validation died at its first guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:50:43 -07:00
Malte UblandClaude Opus 5.5 684a0abd59 Store AbortController and AbortSignal handles in class fields
A class field typed AbortController or AbortSignal (plain, optional,
nullable, or in an array) maps to checked-dynamic storage, and the
class-field rule refused it (SC1090) although the same handles already
live in locals, Map values, and closures. They now take the checked-dynamic
slot, like the fetch Headers, Request, and Response handles.

controller?.abort() on a nullable controller was refused as a method call
(also for locals): the static abort lowering ran only without an optional
chain. It now runs again once the chain has narrowed the receiver.

Found by pi's TUI Editor (autocompleteAbort: AbortController | undefined).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:50:42 -07:00
Chris Tate 8796e91f5e Keep while-loop counters bounded by a typed array length in integers 2026-10-24 12:37:29 -05:00
Chris Tate 71646287b2 Divide one-limb BigInt quotients in one pass and chain limb carries (#2121) 2026-10-24 12:30:30 -05:00
Chris Tate 31d3ba07ca Reuse dev builds' frontend output after edits that keep token positions
A native build reuses its stale frontend cache entry when every changed
source is token-equivalent to the text the entry was built from, but dev
builds never did: their debug information records source lines and
columns, which a token-equivalent edit can move.

Builds with debug information (dev builds without --strip) now reuse the
entry when every changed source is token-equivalent and every token keeps
its line and column (tokenPositionEquivalentSource): edits to a comment's
text within its line, comments and whitespace after the last token, and
trailing whitespace. Edits that move a token still rebuild.

The token scanner also reads a `/` after a non-null assertion as division
(`data[i]! / 7`). It used to read it as the start of a regular expression
and give up on the file, so comment edits in files with that pattern
always rebuilt. A `!` after a line break, or after an operator or a
keyword such as `return`, still starts a negated regular expression, and
a `!` after `)`, `}`, `++` or `--` gives up.
2026-10-24 12:22:19 -05:00
Chris Tate 32752f768e Merge main 2026-10-24 12:19:43 -05:00
Chris Tate aa4e046046 Emit function bodies on up to eight workers (#2118) 2026-10-24 12:04:33 -05:00
Chris Tate 41664c903b Walk files iteratively in the untyped collection inference
The declaration and reference sweeps recursed through ts.forEachChild, two
JS frames per AST level, so a 7000-level expression overflowed the stack
before the expression lowering's 200-level SC1090 fence. Both sweeps use
ts.walkPreorder now, and primitiveOf answers null past the same 200 levels.
2026-10-24 12:03:38 -05:00
Chris Tate 4f0975cbbe Compile more npm and CommonJS JavaScript dependencies statically 2026-10-24 12:01:26 -05:00
Chris Tate ff70c33e38 Divide one-limb BigInt quotients in one pass and chain limb carries
Dividing BigInts of the same limb count, where the quotient is one limb,
takes Knuth's quotient estimate from the top limbs as they read
normalized and settles it with one pass of a - q * b over the operands,
instead of copying both operands shifted and running algorithm D. The
remainder, when wanted, is that pass's result.

The limb loops for add, subtract, multiply by a limb, and multiply-add
and multiply-subtract by a limb process four limbs a pass, with the
carries in add-with-carry chains (__builtin_addcll and __builtin_subcll
where the compiler has them).
2026-10-24 11:59:33 -05:00
Chris Tate fe394f8c30 Keep an unchanged executable on its first Linux rebuild (#2112) 2026-10-24 11:57:52 -05:00
Malte UblandClaude Opus 5.5 d037c66e8c Report JSON.parse syntax errors with V8's messages and positions
JSON.parse's SyntaxError messages were shaped like V8's but differed from
Node's in several ways:
- Every positional message now ends with V8's "(line L column C)", where
  \n, \r and \r\n end lines.
- Positions are UTF-16 offsets, as in Node, not byte offsets.
- An object or array cut off after '{', a property name, a value or an
  array element reports the token V8 expected ("Expected property name or
  '}'", "Expected ':' after property name", "Expected ',' or '}' after
  property value", "Expected ',' or ']' after array element") instead of
  "Unexpected end of JSON input".
- After a comma in an object the message is "Expected double-quoted
  property name".
- An unterminated string reports the end of the input, and a truncated
  \u escape is a "Bad Unicode escape" there.
- A leading zero followed by a digit is an "Unexpected number".
- A misspelled literal names the first differing character, or the end of
  the input.
- An unexpected character is named with the source as V8 does: whole when
  it is under 21 units, otherwise ten units of context on each side; the
  sources "NaN", "Infinity", "undefined" and "[object Object]" name only
  themselves; a quote or a number there is an "Unexpected string" or
  "Unexpected number".

The new corpus program 7993-json-parse-error-messages pins 76 inputs
against Node, including non-ASCII text before the error, line breaks, and
long sources with split surrogate pairs at the context edges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 09:55:05 -07:00
Chris Tate 40f0b6d9e0 Refresh the native compiler's optimization profile (#2120) 2026-10-24 11:51:12 -05:00
Chris Tate 557daa3e9d Refresh the native compiler's optimization profile
The committed profile was collected before about 200 commits that changed
12,800 lines of the compiler. Functions whose control flow changed since are
optimized as without a profile, so its gain had mostly gone: a cold tsc-ts
speed build was 2.7% faster with it than with no profile, against 5% to 6%
with a fresh one.

Regenerated with scripts/refresh-cli-profile.mjs on this commit.
2026-10-24 11:47:19 -05:00
Chris Tate c09385c7ab Emit function bodies on up to eight workers
The native compiler forked four worker processes to emit the function
bodies of a large module. It now forks one per available core, up to
eight. SCRIPTC_EMIT_WORKERS still sets the count. The output is the same
text at any count.
2026-10-24 11:29:56 -05:00
Chris Tate 396c6944ec Report how well the native compiler's profile matches its functions
The native compiler is built with a committed optimization profile. A
function whose control flow changed since the profile was collected is
optimized as without one, so the profile loses its gain as compiler code
changes, with nothing to show it.

With --profile-use, the helper now counts what the profile annotation
reports while it applies the profile: functions that took the profile's
counts, functions whose control flow no longer matches (hash mismatch),
and functions the profile has no record of. It sums the counts over
partitions, keeps each partition's counts beside its cached summary, and
prints them as one line of JSON after a successful emit. The compiler
reads that line (an older helper prints none) and returns it as the
executable artifact's profileMatch.

build-native-cli prints the share of profiled functions that match, and
warns below 90% that the profile should be refreshed. On main aa1c26845, a
profile refreshed on that commit matches 92.7% of the compiler's profiled
functions, and the one committed about 200 commits earlier 87.2%; that one
kept about half of the fresh profile's gain on a cold tsc-ts build.
2026-10-24 11:20:47 -05:00
Chris Tate 2967a1b8c0 Propagate exceptions by unwinding from directly called functions on Linux
Emitted functions report an exception by returning with the thread's
pending flag set, and every caller tests the flag after each call that
may throw. On Linux executables (glibc and musl), functions reached only
by direct calls now propagate exceptions by unwinding the native stack
instead:

- unwindAbiFunctions selects the may-throw functions that are not async
  or generators, constructors, virtually called or overridden methods,
  closure targets, the entry, library exports, module initializers or
  callees of coroutine bodies.
- A native unwind costs about 2 µs a raise and 0.1 µs a frame, so functions that
  exceptions are likely to cross keep the flag: those with a `throw` of
  their own, those up to three calls above one, and recursive functions,
  counting calls through methods, closures and callbacks.
- Their propagating exits call scr_unwind_raise (scr_unwind.c), a forced
  unwind through _Unwind_ForcedUnwind with the exception still in the
  active cell.
- Direct calls to them are `invoke`s without a pending check. The
  landing pad runs the frame's usual unwind path: it releases the frame's
  values and then enters the enclosing try handler or resumes the unwind.
- Remaining plain calls to them (wrappers, adapters, runtime-facing entry
  points) become invokes whose landing pad continues on the pending flag,
  or resumes when the wrapper is itself invoked. Every other call is
  marked `nounwind`, so inlining a raising function at an invoke keeps its
  calls plain.
- When emitted text breaks these rules (a raising function's address is
  taken, or an invoked function returns with the flag pending), the
  module is emitted again with those functions on the pending flag.

Emitted code does not declare scr_unwind_raise `noreturn`, so sanitized
builds do not run the sanitizer's no-return hook, which needs stack
bounds that fiber stacks do not report. Instead, scr_unwind_raise clears
the shadow of the frames it unwinds.

Executables link a static unwinder: libgcc_eh with the host toolchain and
the glibc runtime packs, and Zig's libunwind with Zig and the musl packs.
macOS, Windows, Android, WASI, libraries and debug builds keep the
pending flag everywhere and emit the same code as before.

The throw-chains runtime workload measures exceptions caught 2, 5 and 20
recursive calls above the throw, through direct calls and a callback,
and a parser that backtracks by throwing.
2026-10-24 11:03:34 -05:00
Chris Tate 56b9158de2 Keep integer index arithmetic in integers across nested counted loops 2026-10-24 10:42:05 -05:00
Chris Tate 022428ead6 Write the first stdin chunk after both readers start in the 'readable' EOF test (#2109) 2026-10-24 10:30:31 -05:00
Chris Tate 205f099957 Keep an unchanged executable on its first Linux rebuild
On Linux the executable cache stored an executable only from a key's second
link on, so the first unchanged rebuild after a build linked again, and only
the build after that restored the executable.

- A key's first link on Linux is traced, in the same verbose link. Instead
  of storing the executable, the cache records the output's file identity
  together with the link's inputs once the output is in place.
- keep() answers an unchanged rebuild whose output still has that identity
  and whose inputs are unchanged. The build then neither links nor copies:
  the output stays as it is.
- A rebuild whose output changed or is missing links again; from that second
  link on, the executable is stored as before and restored where needed.
- The cache records the output after every Linux build that places one, so
  the next unchanged rebuild keeps it.

darwin keeps its existing behavior.
2026-10-24 10:26:38 -05:00
Chris Tate 7406ec63f4 Read the node index in more LLVM emission analyses (#2108) 2026-10-24 10:19:42 -05:00
Chris Tate 53baad4ebe Pump untyped iterable pipeline sources
A pipeline whose first stage is a checked-dynamic value now checks it at
run time, as Node's stage ladder does: a stream is piped as before, and a
string or a value with a Symbol.asyncIterator or Symbol.iterator method is
pumped into the next stage through an async generator over its iterator.
A typed Set, Map, generator or async generator object as the first stage
takes the same path. This covers the callback and stream/promises forms,
with or without function stages and pipeline options.

A write whose callback fails synchronously no longer sets needDrain, so no
'drain' follows the error and a pumped source stops at the failed write,
as in Node.

RUNTIME_ABI_VERSION goes from 37 to 42 for scr_stream_dyn_iterable_source.
2026-10-24 10:12:06 -05:00
Chris Tate 9e4f1253c6 Keep unchanged partitions cached across edits 2026-10-24 10:11:29 -05:00
Chris Tate e35621323d Trust no certificate for an empty ca in TLS clients
tls.connect, https.request and http2.connect verified the server against
the default CAs when ca was an empty Buffer, because the runtime read a
zero-length ca as none. The compiled calls now pass a null ca for none
(an omitted ca, or an empty string, which Node reads as no ca) and a
non-null one for any Buffer, and runtime option records read ca the same
way, so an empty Buffer or array trusts no certificate, as in Node. The
connection pool keys tell an empty ca from none. An empty array, which
threw, is an empty trust store too. A runtime-valued empty string ca in
http2.connect is refused instead of being read as an empty store.
2026-10-24 10:06:35 -05:00
Chris Tate 72eb9318bf Analyze int32 slots in queue order, after fixing an order-dependent narrowing (#2107) 2026-10-24 10:02:34 -05:00
Chris Tate 38730910c8 Write the first stdin chunk after both readers start in the 'readable' EOF case
A reader that starts after the second chunk arrives reads both chunks in
one 'readable' event. On loaded test workers Node started that late while
the compiled program did not, so the two outputs differed. The first chunk
now waits 500 ms.
2026-10-24 09:56:15 -05:00