Commit Graph
1261 Commits
Author SHA1 Message Date
Chris Tate 0b0b7a8319 Merge pull request #835 from vercel-labs/fix/number-undefined-unions
Keep missing reads undefined through number arguments, narrowed slots and lambda results
2026-10-10 21:25:54 -05:00
Chris Tate 3c2eeac1d7 Merge pull request #830 from vercel-labs/perf/alloc-hugepages
Move busy small-object slices to transparent huge pages
2026-10-10 21:24:01 -05:00
Malte Ubl cc7c6cb730 Decide inline truncation for the host when no triple is given
The Node-hosted CLI emits with an empty target triple (the host), while
the native CLI passes its toolchain's triple. Inline truncation keyed on
the triple text alone, so on x86-64 Linux the Node seed emitted
llvm.trunc and the native compiler emitted the inline sequence, and the
native bootstrap's LLVM comparison with the Node seed failed
(self-hosting-native-driver, rebuild-emit). An empty triple now means
the host architecture, as it already does for executable TLS; WASI keeps
the intrinsic. The DataView emission test accepts either truncation
form, since it runs on hosts of both kinds.
2026-10-10 15:11:19 -07:00
Malte Ubl bf17c23af9 Return defaulted and missing reads from TypeScript lambdas
TypeScript types `read ?? "text"` as number when the read is typed number,
and `() => xs[i]` as returning number. Declared functions already widened
their returns for both (the primitive result union and the optional
read), but arrow functions and function expressions kept the checker's
return, so the closure narrowed its result and threw "a 'string' value is
not representable" or "undefined is not representable" (the scanbox
`[string, () => unknown][]` probe table).

Unannotated TypeScript lambdas now get the same widened return as
declared functions. A const or global binding of such a lambda keeps the
checker's parameters and takes the widened result, and calls through the
binding count as optional or primitive-union results for the enclosing
function's return.
2026-10-10 14:47:17 -07:00
Malte Ubl 0027fa3226 Keep undefined in narrowed optional fields and locals
After `obj.field = bytes[i]` on a `number | undefined` field, TypeScript
narrows later reads to number, so the read extracted the number arm
unchecked and an out-of-range read printed 0 instead of undefined. Locals
declared `T | undefined` had the same problem inside functions
(`let x: number | undefined; x = xs[i]; x + 1` gave 1, not NaN).

The optional-read analysis now records instance fields declared exactly
`T | undefined` that receive a possibly missing value; their narrowed
reads keep the stored union, as runtime-optional record fields already
do, and the value propagates into parameters and returns like other
optional reads. Locals declared that way join the runtime-optional
storage tracking, so bare reads keep the union until a guard proves
presence. A member read through such a field throws Node's "Cannot read
properties of undefined" TypeError.
2026-10-10 14:47:12 -07:00
Malte Ubl e52688095d Convert missing number arguments with ToNumber
String.fromCharCode, String.fromCodePoint, the global isNaN and isFinite,
DataView offsets and values, and typed-array slice/subarray bounds took a
`number | undefined` value (an out-of-range read the checker types as
number) through a checked narrow, which threw "undefined is not
representable" or refused to compile. Node converts undefined with
ToNumber: code unit 0, a RangeError for fromCodePoint, isNaN true, offset
0; an undefined slice end is the length.

The Math argument conversion becomes a shared helper that also handles
field reads and any other value lowered as a number-or-undefined union.
2026-10-10 14:46:53 -07:00
Malte Ubl 2d90508bc7 Move busy small-object slices to transparent huge pages
Every class slice used to opt out of transparent huge pages, so each
4 KiB the bump pointers crossed cost a page fault. Those faults (fault
entry, memcg charge, LRU insertion and the page clear) were 6-14% of
tsc-ts samples in the kernel. Backing the whole reservation with huge
pages removes most of them, but makes the 2 MiB page around every bump
pointer resident at once. A worker executable has up to 32 class slices
per script thread, so tRPC at 8 checkers grew from 558 to 955 MB.

A slice now starts on small pages. Once it has carved 8 MiB, the rest
of its range switches to huge pages (MADV_HUGEPAGE). Only slices that
already hold 8 MiB pay for a partial last huge page. The reservation is
huge-page aligned, so the switch point and every slice start on a 2 MiB
boundary. The slow path raises a slice's bump limit in two steps (to the
switch point, then to the slice end), so the inline fast paths are
unchanged. The policy is Linux-only; other targets keep one limit per
slice, as before. The allocator checks now build with _GNU_SOURCE on
Linux like the runtime packs, so the madvise advice is compiled in and
checked (alignment, contiguous carving across the switch, VmFlags nh/hg).

Measured with tsc-ts best-v1 (parallel-check + 6247795 e24057a 64d9af6
1c42c55), built with this runtime vs origin/main, on two Linux Vercel
Sandboxes (8 vCPU; 5-7 interleaved runs, medians). Columns: wall, max
RSS, minor faults; "all" is MADV_HUGEPAGE on the whole reservation:

  single-threaded   wall    RSS     faults         all: wall / RSS
  Excalidraw        -6.1%   +0.8%   331k -> 112k   -10.0% / +3.5%
  Playwright        -1.8%   +1.0%   371k -> 111k    -8.4% / +3.2%
  TypeORM           -7.2%   +0.9%   259k ->  75k   -11.8% / +4.1%
  tRPC              -5.0%   +2.0%   101k ->  47k    -9.0% / +11.3%
  tsc-ts self-check -2.9%   +2.6%   158k ->  64k    -3.1% / +8.4%

  --checkers 8
  Excalidraw        -2.9%   +2.6%  1075k -> 508k    -6.3% / +10.6%
  Playwright       -10.0%   +2.6%   739k -> 381k   -10.3% / +12.2%
  TypeORM           -5.9%   +2.2%   372k -> 232k   -10.1% / +27.7%
  tRPC              -2.5%   +2.5%   159k -> 125k    -4.3% / +71.2%
  tsc-ts self-check -4.0%   +3.9%   213k -> 172k   -12.1% / +46.3%

Switching at 2 or 4 MiB was up to about 1.5 points faster single-threaded
but cost up to +11% RSS at 8 checkers. 16 MiB gave back most of the gain.
Prefaulting small pages with MADV_POPULATE_WRITE did not reduce sys time.
Collapsing the small-page prefix with MADV_COLLAPSE at the switch did not
help either. The runtime benchmark suite (--layouts=4 --runs=16) shows a
geomean change of -1.2%: inventory-report -9.9%, validate-errors -5.3%,
log-summary -2.9%, and regex-logs +2.1%. regex-logs peaks at 6.8 MB and
never reaches the switch point; with one binary the policy measures -0.9%
there, so its delta comes from code layout.
2026-10-10 14:03:22 -07:00
Malte Ubl 028622fc75 Read cached null answers without a non-null assertion
The NaN-coded read analysis memoized lookups whose answer can be null
(symbols, eligible functions, call sites, plain-number returns) and read
them back with `map.get(key)!`. Under Node that returns the cached null,
but in the natively compiled compiler `!` is a checked extraction, which
threw "null is not representable" while lowering programs
(self-hosting-native-frontend, the native CLI bootstrap contracts). Read
the cache through an undefined check instead.
2026-10-10 14:03:21 -07:00
Malte Ubl d1791b9165 Inline two-argument Math.min and Math.max
Every two-argument Math.min/Math.max was an out-of-line call to
scr_math_min/scr_math_max (94 sites in tsc-ts, including
compareCodePoints and IdentifierTable.intern). LLVM's minimum/maximum
intrinsics have exactly their semantics: NaN propagates and -0 orders
below +0. On x86-64 they lower to minsd/maxsd with inline NaN and
signed-zero handling, on arm64 to fmin/fmax.
2026-10-10 14:03:21 -07:00
Malte Ubl d4aeeea0f5 Carry typed-array reads as plain numbers where undefined acts like NaN
A typed-array element read answers undefined for an index outside
[0, length), so the default lowering returns a heap-boxed
`number | undefined` and widens every binding, parameter and return the
read reaches. In the tsc-ts scanner that boxed every character read and
turned every character predicate (isLineBreak, isIdentifierPart, ...)
into a function taking a boxed union.

Most consumers cannot tell undefined from NaN: relational, arithmetic
and bitwise operators, truthiness, Math functions, String.fromCharCode,
typed-array indexing, switch over number cases, and equality with a
number that is never undefined. The new analysis (nan-coded-reads.ts)
follows reads through local bindings, parameters of directly called
functions and methods (no overrides, no value uses), and their returns.
A slot is NaN-coded when every value it receives is a NaN-coded read or
slot, or any other number (converted with ToNumber on entry), and either:

- it never holds a genuine NaN (integer elements, integer constants,
  guarded integer arithmetic), so NaN stands for undefined exactly; any
  consumer that takes the value whole (an argument, a stored or returned
  value, a literal element, concatenation) gets the exact
  `number | undefined` rebuilt, and the optional-read analysis treats
  that occurrence as optional, so it lowers as before; or
- every consumer is NaN-insensitive.

Equality of two NaN-coded values that never hold a genuine NaN also
answers true for two NaNs, as undefined === undefined does. Any other
slot keeps the established union lowering; asserted reads (`t[i]!`) are
unchanged.

tsc-ts best-v1 (scriptc main + this commit vs main, Linux sandbox,
Xeon 2.5 GHz, interleaved, medians of 5, diagnostics identical):

| app | parse | total (single-threaded) | wall --checkers 8 |
| --- | --- | --- | --- |
| tRPC | 1.263 -> 0.866 s (-31.4%) | 3.035 -> 2.527 s (-16.7%) | -13.0% |
| Excalidraw | 2.637 -> 1.865 s (-29.3%) | 14.99 -> 14.04 s (-6.3%) | -16.2% |
| Playwright | 3.150 -> 2.202 s (-30.1%) | 12.93 -> 12.14 s (-6.1%) | -12.4% |
| TypeORM | 2.562 -> 2.036 s (-20.5%) | 10.93 -> 10.46 s (-4.3%) | -8.6% |
| self-check | 2.080 -> 1.529 s (-26.5%) | 7.06 -> 6.68 s (-5.4%) | -15.1% |

The tsc-ts IR has 646 instead of 697 scr_union_new_f64 sites (the rest
are cold), 22 instead of 168 typed-read helper calls, and 217 instead of
572 scr_union_get_f64 calls; the scanner's hot functions allocate no
union boxes. Binary size -0.2%.
2026-10-10 14:03:21 -07:00
Malte Ubl 7d08a7441c Truncate inline on x86-64 and test typed-read presence without trunc
Baseline x86-64 has no rounding instruction, so every llvm.trunc became
a call to libm's trunc. The typed-array read helper paid that call on
every read: it decided presence with `i >= 0 && i < length &&
Math.trunc(i) === i` before the inline element load.

- Math.trunc (and the DataView offset truncation) now take an integer
  round trip on x86-64 targets: values of magnitude below 2^52 go through
  fptosi/sitofp with the sign restored by copysign (so -0.5 and -0 give
  -0); larger values, NaN and the infinities are already integral and
  keep their value. Other targets keep llvm.trunc, which is one
  instruction there.
- The ToUint32 slow path is only reached for magnitudes above 2^53 (and
  non-finite values), which are integers, so its truncation was a no-op
  and is gone.
- For integer element kinds, the read helper performs the element read
  that yields NaN for an invalid index (one inline index check) and
  answers undefined exactly when that read is NaN: an integer element is
  never NaN. Float element kinds keep the range and integrality test.
2026-10-10 14:03:20 -07:00
Malte Ubl 5d0b38ef87 Keep null distinct from the TDZ in forward-captured nullable bindings
A closure created above the `let`/`const` it captures holds the binding
in a TDZ box whose NULL payload slot marks "not yet initialized". A
union of one reference arm with `null` and/or `undefined` (`Set<T> |
null`, `string | undefined`, `C | null`) is represented as the bare arm
pointer with NULL standing for the unit arm, so storing that unit value
made the box indistinguishable from the temporal dead zone: every later
read or write threw "Cannot access 'x' before initialization" although
the declaration had run.

The self-hosted compiler trips on exactly this shape in
lowerer.ts (`let dynamicViews: Set<string> | null = null`, read by a
callback created earlier), which made scriptc 0.2.8 fail to build
tsc-ts's relater with SC3004. Node evaluates the source correctly; the
native code was wrong.

TDZ boxes of nullable-union bindings now start at the module's immortal
NULLABLE_ABSENT sentinel and the TDZ test compares against it. No value
of the binding can hold that sentinel, and every RC entry point and the
cycle collector already skip it, so box release, trace and the
initializing store need no other change.

Corpus: 4596-tdz-nullable-forward-capture.
2026-10-10 14:03:19 -07:00
Malte Ubl 810a0b1d56 Merge pull request #826 from vercel-labs/fix/tscts-gaps/04-callback-fields
tsc-ts -7%: keep native method calls when only an unrelated callback field shares the name
2026-10-10 14:03:48 -05:00
Malte Ubl b979bf5434 Merge pull request #825 from vercel-labs/fix/tscts-gaps/03-error-int32
tsc-ts -7%: keep int32 fields when an Error subclass forwards its constructor arguments
2026-10-10 14:03:32 -05:00
Malte Ubl 64206c72e7 Merge pull request #824 from vercel-labs/fix/tscts-gaps/02-union-fields
Read and write union class fields declared with different types
2026-10-10 14:03:07 -05:00
Malte Ubl 443b3dc666 Merge pull request #823 from vercel-labs/fix/tscts-gaps/01-optional-receivers
Narrow optional base-class receivers before shared union field access
2026-10-10 13:39:28 -05:00
Malte Ubl 3eeea34ce5 Keep native method calls when only an unrelated callback field shares the name
The class method census marks a method name as replaceable when a
function-typed property write uses that name, so a scanner storing its
error callback in `this.onError` made every call of any class's
`onError` method go through the checked property bag. That bag is a
dynamic store naming any property, so the int32 slot analysis also gave up
int32 storage for every class field exposed to dynamic code.

A function-typed write now marks the name only when an instance of a class
that has the method can inhabit the receiver's type: an assignability check
against the instance types of the method's owners, with `this` in an
instance member read as its class. Generic or anonymous owners, `this`
outside instance members, type parameters, `any` and `unknown` receivers
keep the conservative answer. Writes through interfaces or subclasses that
can reach a method still replace it.

Test: class-method-mutations.test.ts. Corpus:
4597-callback-field-method-name.
2026-10-10 09:01:04 -07:00
Malte Ubl 8d8d478f80 Keep int32 fields when an Error subclass forwards its constructor arguments
A class extending Error without its own constructor (or one passing
options to super) initializes the builtin prefix through the
error.ctorOptions helper, whose message and options arguments are dynamic.
The int32 slot analysis did not list that helper among the dynamic helpers
that never store into a live class view, so a single such class anywhere in
a program counted as a dynamic store naming every property: every f64
field of every class exposed to dynamic code lost its int32 storage, and
the integer facts of the functions reading those fields went with it.

error.ctorOptions runs scr_error_init_options, the same initializer
error.newOptions (already listed) runs: it stamps the receiver's
runtime-owned error prefix and only reads `options.cause`. In the native
tsc-ts self-check, two such classes cost 7% total time and 10% check time.

Unit test: int32-slots.test.ts. Corpus: 4596-error-subclass-int-fields
pins Error subclasses forwarding message and options beside integer
fields.
2026-10-10 09:01:03 -07:00
Malte Ubl 0d679c1b98 Read and write union class fields declared with different types
A field shared by every class arm of a union could only be read or
written when all arms declared it with one IR type. Syntax-tree code
commonly narrows a node to several classes with `instanceof` where one
class declares the field optional and another required (`initializer:
Expression | undefined` beside `initializer: Expression`), or as a
subclass beside its base, and both forms were refused.

Reads now dispatch on the union tag, read each arm's own slot, and widen
it to the arm field type every other one converts into without a check
(a union wrap or class upcast); the caller narrows the result to the
checker's flow type as before. Writes evaluate the value once as the arm
field type that every other arm's storage accepts and widen it per arm.
Arms whose field types are unrelated keep the existing refusal.

Corpus: 4594-union-class-field-mixed-types.
2026-10-10 09:01:03 -07:00
Malte Ubl b901ec78eb Narrow optional base-class receivers before shared union field access
A parameter typed as a base class becomes runtime-optional storage
(`Base | undefined`) when a caller can pass an array hole. After
`instanceof` narrowed it to several subclasses and a guard such as
`node.label === undefined` ended its active runtime-optional state, the
next `node.label` lowered the receiver as the storage union, which has no
retag to the checker's class union, and the shared field read or write was
refused. Union property reads and union field writes now convert such a
receiver with the checked extraction and class test that typed slots
already use.

Corpus: 4595-union-class-field-optional-receiver.
2026-10-10 09:01:02 -07:00
Chris Tate c7e82741f9 Merge pull request #822 from vercel-labs/ctate/prepare-v0-2-04de95
Prepare v0.2.8
v0.2.8
2026-10-10 10:45:36 -05:00
Chris Tate b40dbe9bb7 Show 0.2.8 in the coverage diagnostics example 2026-10-10 10:18:09 -05:00
Chris Tate c67e08a9cf Prepare v0.2.8
- Synchronize published packages and the compiler surface manifest at v0.2.8, and bump the runtime ABI marker to v9.
- Add release notes for worker graph sharing, catchable deep recursion, parallel code generation, runtime performance, and compatibility fixes.
2026-10-10 10:08:39 -05:00
Chris Tate 1bf547a122 Report a rejected declaration's own diagnostic (#818) 2026-10-10 10:01:46 -05:00
Chris Tate 9e0c6564d4 Format the LLVM emitter bootstrap test 2026-10-10 08:36:39 -05:00
Chris Tate 8e2197fe10 Merge main into ctate/report-rejected-declaration-a704c5 2026-10-10 08:07:08 -05:00
Chris Tate da2517a976 Keep record layouts stable for shorthand and literal-argument fields (#819) 2026-10-10 07:59:01 -05:00
Chris Tate ec5d419080 Give the sanitized LLVM emitter bootstrap room to emit itself (#820) 2026-10-10 04:00:46 -05:00
Chris Tate e8e644c7e0 Give the sanitized LLVM emitter bootstrap room to emit itself
The natively built emitter records an allocation stack for every object
under ASan, so emitting its own module takes several times longer than
the plain build and now runs close to the 300 s per-stage limit on shared
runners. Sanitized runs get a 900 s per-stage limit and a matching test
budget; plain runs keep the existing limits.
2026-10-10 03:58:05 -05:00
Chris Tate 2ff5711a54 Keep record layouts stable for shorthand and literal-argument fields
A shorthand property and an object literal passed as an argument no
longer widen the declared record layout of the binding or parameter
that receives them. A widened layout made every existing record that
flowed into that binding or parameter convert to a copy, so later
writes through the copy were lost. Those literals keep a missing
element in their own fields and convert, with a check, into a declared
record.

The debug-variables fixture iterates an array whose elements may be
undefined, so its loop binding keeps the union layout the debugger
check inspects.
2026-10-10 03:40:36 -05:00
Chris Tate 9302fc9c93 Merge main into ctate/report-rejected-declaration-a704c5 2026-10-10 01:54:50 -05:00
Chris Tate 6cf3d4744b Read holes in for-of loops and missing-element shorthand properties as undefined (#817) 2026-10-10 01:08:12 -05:00
Malte Ubl 1b6d0723e9 Merge pull request #814 from vercel-labs/perf/workers/03-typed-publication
@scriptc/threads: publish typed object graphs to workers by reference (parallel tsc-ts 4.72 s -> 0.97 s)
2026-10-10 01:06:23 -05:00
Malte Ubl 272aef3247 Release @scriptc/threads with the other packages
sync-versions stamps packages/threads with the CLI version (0.2.7 now),
the release workflow checks that version and publishes the package
before scriptc, and RELEASING.md lists it with the packages that always
release together. npm trusted publishing must be configured for
@scriptc/threads before the next release: a package without it fails
before upload.
2026-10-10 01:00:36 -05:00
Malte Ubl 6835e5a734 Document sharing published graphs between workers
The new docs page (/docs/threads) explains @scriptc/threads: a runnable
example that publishes a million-order ledger and sends it to four
workers, verified under Node.js 24 and as a scriptc executable; what
publish accepts and refuses, and that a refused publish leaves the graph
unchanged; the frozen-object TypeErrors of the write guard; a comparison
with Node's structured clone (constant-time sends, preserved classes and
identity, immortal objects) and the three rules that make a program
behave the same under both; the automatic sharing of deeply frozen
untyped data; and the cost model. The limitations page gains a Published
graphs section (immortality, class and identity differences, write-check
evaluation order, private fields) and links the new page from the
worker-threads section.

This is the first page added since the migration, so the route-test
fixture now accepts pages without a legacy URL, and the agent guide's
checklist for new pages names the fixture.
2026-10-10 01:00:36 -05:00
Malte Ubl 8406d3fcd9 Publish strings as leaves and record their ASCII fact first
A published string can never record facts in its header later, so
publication proves ASCII before marking (every UTF-16 index mapper
answers ASCII strings by identity) and primes the Map key hash only for
short strings: hashing every source text cost more than the rest of the
walk, and the map code already skips caching into immortal strings.
Strings are marked when pushed instead of going through the worklist.
SCRIPTC_PUBLISH_TRACE reports object counts and walk/commit times.
2026-10-10 01:00:36 -05:00
Malte Ubl bf508eb70f Publish typed object graphs to workers by reference (@scriptc/threads)
The new @scriptc/threads package exports publish(value) and
sharesPublishedGraphs. Node.js runs its JavaScript: publish validates
the whole graph (plain objects, arrays, class instances, Maps, Sets and
primitives other than symbols), then deep-freezes it, and published Maps
and Sets reject mutation. scriptc builds lower both exports to
intrinsics and never compile the package body.

In scriptc, publish makes the graph immutable and immortal: a
compiler-emitted walker per static type (llvm/publish.ts) marks every
object rc == SIZE_MAX through a runtime worklist (scr_publish.c), so
retains, releases and the collector skip it and nothing frees it, and
the objects leave the collector's buffers. Hierarchy members dispatch on
the concrete class; arrays, maps, sets, strings and dyn subgraphs have
runtime walkers. Static types over-approximate a graph, so an
unpublishable type gets a refusing walker that fails only when a value
is present, and every mark is logged so a refusal restores the whole
graph, exactly like the Node implementation's validate-first order.

A typed-ref capsule over a published object travels through postMessage
and workerData as a pointer (a fresh capsule in the receiver; vtables,
adapters and type keys are process-wide); immortal dyn graphs inside a
message are shared the same way. Iterating a published map no longer
writes its iteration depth.

Stores into the static types a program publishes (computed by
backend/publication.ts, a class with its whole hierarchy) carry a guard
that throws Node's frozen-object TypeErrors, and may-throw counts them.
The corpus program pins the contract against Node: shared and cyclic
class graphs with arrays, maps, sets and records read identically on
both threads, every write form fails with Node's message, unpublished
instances of the same classes stay writable, a refused publish leaves
its graph unchanged, and a worker-published graph reaches the main
thread. packages/threads/README.md documents the contract and the
differences (prototypes and identity across messages).

Each mark records its object kind, so a successful publish takes the
objects out of the RC audit's live counts (immortal objects are never
freed).

Dates (time values) and process streams (descriptors) are scalars in
scriptc, so their walkers refuse a date field or root directly instead
of pushing its bits as a pointer; the refusal corpus program pins the
statically typed refusals (date, function, RegExp, typed array and Error
values in class and record fields, and a date root) against Node.

Main lowers a discarded or union-typed `xs.pop()` to a length
truncation; those stores now carry a `pop` marker so a published array
fails with pop's own TypeError ("Cannot delete property 'N'", or the
length message for an empty array, whose `length = 0` store the emitter
drops unless the array type can be published).
2026-10-10 01:00:36 -05:00
Malte Ubl 1c5583bba8 Merge pull request #813 from vercel-labs/perf/workers/02-zero-copy-plain-data
Share deeply frozen plain data with workers without copying
2026-10-10 01:00:32 -05:00
Malte Ubl fd5615a274 Return from immortal releases on the inline path
The inline half of a class or record release sent NULL, last and
immortal references to the out-of-line slow release. Static literals
were the only immortal objects then; a published graph is released as
often as ordinary objects (tsc-ts checking a published program spent
more time in the no-op calls of Node releases than in any checker
function). The rarely taken branch now tests for immortal first; the
common decrement path is unchanged.
2026-10-10 00:57:58 -05:00
Malte Ubl 3487d17348 Publish deeply frozen plain-data graphs to workers without copying
A postMessage or workerData graph whose objects and arrays are all
frozen, ordinary and hold only primitives, strings and such containers
is now shared by pointer: every node and string becomes immortal
(rc == SIZE_MAX, so retains, releases and the cycle collector skip it
and nothing frees it), strings cache their key hash first, and the
nodes leave the sender's cycle buffers. Posting an already published
root is O(1). Anything else is cloned as before; SCRIPTC_PUBLISH=0
turns publication off and SCRIPTC_PUBLISH_TRACE=1 reports each choice.

With the receiver freezing what it receives (Node's clone is not
frozen) and no identity comparison across messages, the behavior
matches Node's structured clone. Published nodes leave the RC audit's live counts, since they are never
freed, and the small-integer dyn cache becomes process-wide (filled
before main): a published graph can outlive the thread that created its
nodes, and the thread-local table died with its thread.

The new corpus program pins that
contract both ways, including a worker-published graph read after the
worker exited and a shallowly frozen graph that must still be cloned.
2026-10-10 00:57:58 -05:00
Malte Ubl 33b040d9d7 Merge pull request #812 from vercel-labs/perf/workers/01-worker-tax
Worker tax 1.47x -> 1.04x (Linux), 1.91x -> 1.07x (macOS): per-thread allocator, TLS blocks, one exception alert
2026-10-10 00:57:54 -05:00
Chris Tate 1767fe7fa0 Merge main into perf/workers/01-worker-tax 2026-10-10 00:55:35 -05:00
Chris Tate cbbdf1560c Merge pull request #806 from vercel-labs/perf/tscts/10-checker-hot-paths
tsc-ts -8.6%: borrow checker hot paths (walks, wraps, casts, borrowed returns)
2026-10-10 00:33:08 -05:00
Chris Tate 69b1523466 Merge main into perf/tscts/10-checker-hot-paths 2026-10-10 00:24:43 -05:00
Chris Tate f992727487 Report a rejected declaration's own diagnostic when a discarded probe flushed it first
Deferred collection diagnostics now stay attached to their declaration until a
copy reaches the final report, so a discarded probe buffer or a successful
nested collection can no longer drop them. Uses of a binding whose type names
a rejected class report that class's diagnostic before the inherited-blocker
cascade.
2026-10-09 23:07:57 -05:00
Chris Tate 2d15471497 Read missing elements as NaN in every static Math argument
Positional arguments of Math.hypot now take the same undefined-to-NaN
conversion as Math.max, Math.min and the fixed-arity statics, and a
direct indexed read that can miss converts the same way as a binding
holding one.
2026-10-09 22:45:10 -05:00
Chris Tate 92642d775e Read holes in for-of loops and missing-element shorthand properties as undefined
A for-of loop over an array whose own facts show a hole or a stored
undefined binds T | undefined, and the typed parameters, locals, record
fields and Math arguments it reaches widen to hold it. Loops over arrays
the element-state analysis proves free of holes read each element
directly, without a per-element state test.

Object literals built from a missing-element read keep the property as
undefined in shorthand and long form, through spreads, nested records,
arrays of records, annotated locals and literal arguments to typed
parameters.
2026-10-09 22:05:36 -05:00
Chris Tate e74dac7d5c Make the worker stack-overflow fixture independent of event order (#816) 2026-10-09 21:21:48 -05:00
Chris Tate f388612dcd Make the worker stack-overflow fixture independent of event order
A worker's messages and its uncaught 'error' event reach the parent on
separate channels in Node, so their relative order is not specified and
can differ under load. The fixture now records both and reports them when
the worker exits, which Node and native executables deliver after both.
2026-10-09 21:19:20 -05:00
Chris Tate 198f27eea9 Keep the native compiler independent of weak keys (#810) 2026-10-09 20:37:13 -05:00