Commit Graph
177 Commits
Author SHA1 Message Date
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 8b4f83ac02 Collect the native compiler profile on the release commit (#2138) 2026-10-24 18:25:03 -05:00
Chris Tate a5c7a4e983 Report how well the native compiler's profile matches its functions (#2117) 2026-10-24 17:39:30 -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 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 427e383b5e Cache untyped property access in programs that declare classes (reland)
Reapplies #2017, which #2038 reverted after effect's observability-otlp
fixture crashed in plain builds. The crash was the candidate-buffer
corruption that the previous commit fixes: the cached property sites
change when the collector runs, and with enough environment variables a
pass buffered a string box that was later pooled while still buffered.
2026-10-24 08:22:30 -05:00
Chris Tate 7278c3c467 Read a union's discriminant once in checked casts (#2073) 2026-10-24 04:51:36 -05:00
Chris Tate e3fb35cc68 Cap the Linux test step's memory and move memory-heavy side files apart (#2030) 2026-10-23 23:48:45 -05:00
Chris Tate 36daee3f61 Fix the CI failure 2026-10-23 23:16:58 -05:00
Chris Tate 7cdfe16c9b Cap the Linux test step's memory and spread memory-heavy side files
The Linux test shards run their suites in a transient systemd scope with
MemoryMax=13G and no swap, below the runner's 16 GB. A process tree that
grows past the cap loses its largest process to the kernel, and the job
log names the failing test, where the runner itself ran out of memory
and left no log. Hosts without systemd run the suites directly.

The side-queue balancer also takes a table of memory-heavy files, with
their measured peak. They run whole in the cost-balanced task, and each
goes to the shard with the least memory-heavy work so far, after the
time-balanced files. decl-boundary.test.ts, which peaks at about 1.8 GB
alone and had no time measurement, so vitest's file sharding put it in
shard 11, is the first entry. It now runs in shard 1 (plain) and 6 (san).
2026-10-23 14:24:38 -05:00
Chris Tate e3b42a311d Add --target-cpu to compile x86-64 programs for a microarchitecture level
Release builds compile for the x86-64 baseline, so LLVM vectorizes with
SSE2 only. `--target-cpu=<baseline|x86-64-v2|x86-64-v3|x86-64-v4|native>`
compiles the program's code for a higher level: every emitted function
carries the level as its `target-cpu`, so LLVM can use AVX2, BMI2, FMA or
AVX-512. The executable then runs only on CPUs of that level.

- `native` resolves to the highest level the build machine supports, read
  from /proc/cpuinfo on Linux and `sysctl machdep.cpu` on macOS.
- The level is part of the executable cache key; `baseline` and builds
  without the option produce the same output and keys as before.
- The runtime pack keeps its baseline code.
- Targets other than x86-64 refuse a level (SC3002). The CLI's cached
  bootstrap route leaves builds with the option to the full compiler.
- `bench:runtime` takes `--baseline-target-cpu` and `--candidate-target-cpu`.
2026-10-23 13:10:16 -05:00
Chris Tate 4dcb83c418 Free a typed teardown's blocks in the order it met them (#1849) 2026-10-23 12:09:37 -05:00
Chris Tate a10c183b13 Revert "Cache untyped property access in programs that declare classes (#1995)" 2026-10-23 09:05:55 -05:00
Chris Tate 8eedab9fde Cache untyped property access in programs that declare classes
A program that declares a class reads and writes untyped properties
through helpers that first select the receiver's property bag. The bag
helpers return jsvals, so the helpers read the bag through js.toDyn;
keyed helpers test each class's key table between the key's coercion and
the read; and a program with subclasses normalizes the receiver
(value = %dyn.class.normalize(value)) at the start of each helper. The
inline-cache recognizer accepted none of these forms, so every property
site in such a program called its helper. It now looks through dyn/jsval
conversions of the receiver, accepts capsule branches before a keyed
helper's final read, and accepts a rebinding of the receiver to a bag of
itself. Plain objects and arrays then read and write through the site's
cache, as they do in programs without classes.

bench/dynamic-properties (new) exercises these sites.
2026-10-23 08:41:40 -05:00
Chris Tate 19e0fe8bcd Merge main and read cipher options through getters and prototypes 2026-10-23 05:33:14 -05:00
Chris Tate 0314d9db3e Dispatch checked-dynamic method calls on method ids 2026-10-22 23:52:25 -05:00
Chris Tate f030e5ad44 Check with tsc-ts 0.0.8 on every platform
tsc-ts 0.0.8's API server serves on the named pipe the Node host
connects over on Windows (tsc-ts --api --pipe), and the native compiler
already starts it over its standard streams there, so tsc-ts is the
default checker on every platform. SCRIPTC_CHECKER=tsgo still selects
tsgo.

The compiler depends on tsc-ts 0.0.8, which the release-age exclusions
admit with its platform packages and @scriptc/threads 0.4.2.

The Windows CLI smoke test runs with both checkers, and
verify-native-cli.mjs --run checks that a distribution's tsc-ts runs on
its platform (tsc-ts.exe on Windows).
2026-10-22 22:36:11 -05:00
Chris Tate dc2b238df7 Revert "Dispatch checked-dynamic method calls on method ids (#1851)" 2026-10-22 18:02:51 -05:00
Chris Tate 289a6f619d Merge main 2026-10-22 17:39:40 -05:00
Chris Tate 5195e2b63a Merge main 2026-10-22 17:19:37 -05:00
Chris Tate 846552b0e2 Support crypto.getCipherInfo, AES-CCM and the AES key wrap ciphers
getCipherInfo(nameOrNid[, options]) runs in a new runtime unit,
scr_cipher_info.c, over a table that scripts/gen-cipher-info.mjs generates
from Node 24.22's getCipherInfo. The table covers every cipher OpenSSL
names, including the ones without a native implementation. Lookups match
names case-insensitively and accept OpenSSL's nids. The unit applies
cipher.js's argument checks and GetCipherInfo's keyLength and ivLength
rules, and returns the info object in Node's key order. CipherInfo maps
to a checked-dynamic value, so TypeScript and JavaScript code receive
Node's object. Reading the nid Node leaves out for the SIV and SM4 GCM,
CCM and XTS ciphers, where the declared type gives number, throws a
TypeError.

createCipheriv and createDecipheriv support aes-128/192/256-ccm (and the
id-aesN-ccm names) over OpenSSL's ccm128 steps. They follow Node's tag and
IV lengths, the plaintextLength requirement, the message-length caps and
the one-update rule, and report a failed authentication from final().

They also support the RFC 3394 and RFC 5649 key wrap ciphers (aes-N-wrap,
aesN-wrap, id-aesN-wrap, the -pad forms and the -inv ciphers) over
wrap128.c's routines, with Node's lengths, integrity failures and
one-update rule.

The new names follow the existing ones in the cipher table, so the lookup
cost of the existing ciphers is unchanged.
2026-10-22 17:18:54 -05:00
Chris Tate 8bc4586b9a Select AST children by wire slot in the generated getters 2026-10-22 16:56:13 -05:00
Chris Tate 7cce36354f Dispatch checked-dynamic method calls on method ids
A method call on a value of unknown type (untyped JavaScript, `any`)
picked its builtin by comparing the method name with strcmp against each
candidate, in long ladders per receiver kind: a string method could test
dozens of names before it matched.

- The name is now looked up once per call in a generated hash table
  (scr_dyn_methods.h, from scripts/gen-dyn-methods.mjs), and each ladder
  compares the resulting id. The table verifies the name's bytes, so a
  name is matched by its content, as before.
- The worker event methods compare the first character before strcmp.
- bench/dynamic-methods calls string, array, Map, Set, number and own
  methods from untyped JavaScript.
2026-10-22 11:21:10 -05:00
Chris Tate 65a68470cf Free a typed teardown's blocks in the order it met them
A typed teardown freed each block onto its free list as it went, and the
free lists, which are LIFO, handed a torn-down structure's blocks to the
next allocations in the reverse order. A structure that a loop rebuilds
the same way each time was therefore laid out mirrored every other
generation, and every walk over it, its teardown included, ran through
memory backwards, which the hardware prefetchers handle three to four times
worse. A program that rebuilds a tree of 2^21 - 1 object literals 12
times, summing each generation before dropping it, took 26 to 28 ns per
node to build, 24 to sum and 16 to drop in those generations, against
6.8, 6.3 and 5.3 in the others. With this change, every generation runs
at the second set of times, and the program takes 0.50 s instead of
1.18 s (Node: 0.91 s).

The teardown of a class or record type with children of its own type now
keeps the blocks it frees in a segment, linked through their first words
and built in order through a tail pointer. A node releases its own block
first, places it ahead of its children's when its address is the lower
(object literals allocate before their fields) and behind them otherwise
(constructor arguments build the children first), and the chain's first
teardown frees the whole segment onto the free list in one step. A
teardown past DIRECT_DESTROY_CHAIN appends through scr_rc_destroy_seg,
which keeps scr_rc_destroy's depth budget and queue. Blocks outside the
calling thread's arena are freed as before, and targets without the
inline allocation paths keep the old teardown.
2026-10-22 11:12:23 -05:00
Chris Tate 2b657e6e3c Format the split tools' output with the tools checkout's oxfmt configuration
The split tools ran oxfmt in the checkout they rewrite. oxfmt looks up
its configuration from that directory and from the formatted files'
directories, and runs an oxfmt.config.ts it finds there, so a branch that
rehome-split rewrites could run code with the maintainer's permissions.
The tools now format through format.mjs, which passes the tools
checkout's .oxfmtrc.jsonc with --config and disables the nested lookup,
so no configuration file of the rewritten checkout is read or run. With
no files to format it does nothing, where oxfmt would format the whole
directory.

The split-tools tests cover split-methods and move-state on a checkout
whose root and src directory commit an oxfmt.config.ts that records it
ran.
2026-10-22 11:12:02 -05:00
Chris Tate 76d7d29b13 Merge main 2026-10-22 10:34:23 -05:00
Chris Tate 9b317ba97d Allow the JSX lowering tests' TypeScript 5 import
jsx-source.test.ts compares the JSX rewrite with TypeScript 5's
transpileModule output, so it imports typescript5 as the other test
oracles on the TypeScript 5 island allowlist do. scripts/test-ts7.mjs
did not list it, and the TypeScript 7 parity job failed since #1786.
2026-10-22 06:48:13 -05:00
Chris Tate c47a0e90ae Compile JSX with the classic runtime
Fixes #889
Co-authored-by: papack <33105506+papack@users.noreply.github.com>
2026-10-22 04:27:35 -05:00
Chris Tate 48176a471b Merge main 2026-10-22 02:23:51 -05:00
Chris Tate ecee5fdaaf Store references to ProgramLifetime instances in 4 bytes on Linux 2026-10-21 23:25:33 -05:00
Chris Tate 388523cee4 Address review feedback on the lowering-order check 2026-10-21 21:09:39 -05:00
Chris Tate 3b5e233618 Lower to the same IR whatever order reachable bodies lower in 2026-10-21 20:28:16 -05:00
Chris Tate 2b48451cf4 Build the native compiler with profile-guided optimization 2026-10-21 15:33:37 -05:00
Chris Tate cf1e79b5f9 Keep the write-target AST getters out of the generated region 2026-10-21 11:49:05 -05:00
Chris Tate d244b84931 Keep the split scripts' reads and writes inside the worktree 2026-10-21 03:47:21 -05:00
Chris Tate cb75c51eda Fail the zero-cost gate when it compares nothing
A benchmark workload or the self-hosted compiler that fails to build
with both checkouts now fails the gate, and so does a run in which no
program built with both. Before this, a run whose builds all failed the
same way, such as with a helper of another version, reported PASS. A
build error now prints in full under its program; its first line is
often a warning.
2026-10-21 03:45:36 -05:00
Chris Tate 71e3dc5b80 Keep shorthand-property imports in split methods and treat extends as a value use in split modules 2026-10-21 03:21:11 -05:00
Chris Tate 79f98f4684 Regenerate the compatibility files offline, and after merges (#1609) 2026-10-20 21:25:47 -05:00
Chris Tate 100e7738a0 Keep generated-file conflicts until regeneration succeeds, and take the side for the merged pin
regen-generated.mjs resolved each conflicted generated file with
`git checkout <ref> --`, which also staged it, so a failing generator left
the conflict resolved with the branch's stale file. It now writes the
chosen side's file to the working tree only and stages it after both
generators succeed.

It also always took MERGE_HEAD's ledger, so merging a branch with an older
Node pin into one with a newer pin regenerated from a ledger for the older
pin and failed. It now takes the side (index stage 2 or 3) whose ledger is
for the merged pin in node-v24.json, and when neither is, stops and says
to run pnpm node-compat, which fetches the pinned docs.
2026-10-20 21:21:14 -05:00
Chris Tate bde7afa980 Compile undici from a built-in implementation (#1425) 2026-10-20 21:14:37 -05:00
Chris Tate 9c24e64ee2 Regenerate the compatibility files offline, and after merges
pnpm node-compat fetched Node's docs on every run, even though only the
classification changes between pins. pnpm node-compat --local now
reclassifies the committed ledger rows from the current sources and
rewrites the ledger and the public artifact, the same files a fetching
run writes while the pin is unchanged. scripts/regen-generated.mjs uses
it after a merge: for each generated file still in conflict it takes the
merged-in branch's version, regenerates the surface manifest and the
compatibility files from the merged sources, and stages them, so these
files never need a hand merge.
2026-10-20 20:50:04 -05:00
Chris Tate 9eae4af46c Drop the undici shim type test from the TypeScript 5 import allowlist
The test checks the shims with the TypeScript 7 checker and no longer
imports typescript5, so test-ts7 reported its allowlist entry as stale.
2026-10-20 20:08:11 -05:00
Chris Tate 50f93d0be0 Merge main 2026-10-20 19:05:22 -05:00
Chris Tate f1d694b47b Move the emitter's loop optimization state into loop-state.ts
LlEmitter's field list and constructor are where most concurrent edits to
emitter.ts collide. A feature now defines its state in its own file with
defineModuleState (emitter-state.ts) and reads it through the returned key,
so it no longer edits the class. The loop optimizations' twelve fields and
their per-function setup move to loop-state.ts; the emitted IR is unchanged.

scripts/split holds the transforms used for the emitter and lowering
splits, and rehome-split.mjs, which carries an open branch across a split.
2026-10-20 18:58:42 -05:00
Chris Tate f3da708774 Compile polyfill packages that are Node builtins as the builtins (#1458) 2026-10-20 18:28:42 -05:00
Chris Tate 74633db92f Merge main 2026-10-20 18:21:35 -05:00
Chris Tate fd34d72e00 Keep each runtime benchmark workload in its own file
workloads.json listed every workload in one array, and its README table
listed them in one hand order, so changes that each added a workload
appended at the same place and conflicted. Each workload is now a JSON
file in benchmarks/runtime/workloads/ named after it, read through
benchmarks/runtime/workloads.mjs by the bench runner, the zero-cost
gate, the strace script and the dynamic-tier test, ordered by category
and then name. The loader still reads workloads.json in a checkout from
before this change, so the gate can compare against one. The README
table follows the same order.
2026-10-20 18:08:10 -05:00
Chris Tate 3306c78914 Check the frontend's types against tsc-ts/api (#1538) 2026-10-20 13:49:45 -05:00
Chris Tate f3c4ba0c20 Run macOS CI jobs nightly and for pull requests that change darwin code
Pushes to main now run every lane except macos, so a merged commit no
longer queues the macOS native build, the darwin test slices and the
macOS LLVM artifact jobs. The nightly sweep and manual runs still run
every lane. Pull requests keep running the macOS lane when
scripts/ci-lanes.mjs selects it.

- The iOS runtime pack builds move from mobile_runtime_packs (linux lane)
  to mobile_runtime_packs_ios (macos lane), since they run on macOS
  runners. The Android pack stays in the linux lane.
- The macos_full dispatch input is replaced by macos_suite (full or
  darwin, default full). Manual runs run the complete macOS suite unless
  macos_suite is darwin.
- ci-lanes.mjs also selects the macos lane for paths naming kqueue, the
  runtime's platform header, and every file of the darwin suite in
  scripts/ci-test-plan.mjs (adding tests/harness/differential.test.ts
  and the darwin runtime pack's archive test).
- The required test check counts a skipped macOS job as passing when the
  macos lane is off, as it already did for pull requests.
2026-10-20 13:43:22 -05:00