Commit Graph
6750 Commits
Author SHA1 Message Date
Malte Ubl 0ffc2af2a1 Merge branch 'ni/i4-deferred' into ni/i5-shared-modules 2026-10-27 19:16:13 -07:00
Malte Ubl 81d25a0fc5 Merge branch 'ni/i3-exports' into ni/i4-deferred 2026-10-27 19:16:12 -07:00
Malte Ubl 1428e41841 Merge branch 'ni/i2-handles' into ni/i3-exports 2026-10-27 19:16:06 -07:00
Malte UblandClaude 5.5 7d1a7f3882 Merge origin/ni/i2-handles into the local branch
Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
2026-10-27 19:15:54 -07:00
Chris Tate d97e816848 Keep integer clones off records that may be views
A checked conversion can answer a record view: an object of the target
record type whose fields live in the record it views, at that record's
offsets (record-views.ts). An integer clone read the fields of a borrowed
record at its own shape's offsets, so for a view it read the view's unused
slots and returned wrong results.

Record shapes that may be views are no longer references a clone reads, so
functions that read them keep the ordinary body, which reads a view's fields
through the original record. The emitter now plans integer clones after
record views.

integer-recursion-records.ts gains a recursive sum over a view.
2026-10-27 21:15:37 -05:00
Malte Ubl ad45f05ca8 Merge branch 'ni/i4-deferred' into ni/i5-shared-modules 2026-10-27 19:14:39 -07:00
Malte Ubl 8c4eb85122 Merge branch 'ni/i3-exports' into ni/i4-deferred 2026-10-27 19:14:34 -07:00
Malte Ubl a1742e6377 Merge branch 'ni/i2-handles' into ni/i3-exports 2026-10-27 19:14:25 -07:00
Malte UblandClaude 5.5 13c18782a6 Bind an exported function declaration into island shared modules
An exported function declaration is a mutable binding, so its module can
replace it; the hooks treated only `let` and `var` exports as live and
snapshotted the function, leaving island imports on the first function.
Bind function exports like `var`, and cover a reassigned function export
in the shared-singleton corpus program.

Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
2026-10-27 19:14:14 -07:00
Malte UblandClaude 5.5 1d2ab2e874 Treat aliases of an accessor class as unreadable in the effect gate
Reading a class with a static accessor is refused because the accessor
can run import-time effects, but `const B = A` gave the alias the kind
"value", so a later read through `B` was admitted as pure and the
accessor's effects could move with the deferred module. Propagate the
"accessors" kind through alias chains to a fixed point and cover the
alias in the refusal test.

Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
2026-10-27 19:14:09 -07:00
Malte UblandClaude 5.5 4535c4f90e Merge the hosted-loop base into the handles branch
Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
2026-10-27 19:14:04 -07:00
Malte UblandClaude 5.5 1ab14172c6 Keep a unary island-call argument effectful and link islands beside streams
`+x`, `-x` and `~x` convert their operand with ToNumber, which calls a
user-defined `valueOf`; treating such an argument as inert let the
lowering read the island method after the conversion ran, so the call
reached a method the conversion had replaced. Only a numeric literal
operand is inert now, and the corpus program's `valueOf` replaces the
method it is called through.

A program that uses web streams and Node islands selected one
`scr_async.c` variant by condition count, so the tie between the
one-condition variants left either SCR_NATIVE_REACTIONS or
SCR_NODE_ISLANDS undefined and the hosted-loop symbols unresolved. Add
the combined variant, with a matrix test that pins the selection for
both features together and for each alone.

Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
2026-10-27 19:12:36 -07:00
Malte UblandClaude Opus 5.5 da240b72e0 Merge ni/3-hosted-loop into ni/i2-handles
Brings the island signal listener bookkeeping and migrated timer refresh
fixes into the stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:08:55 -07:00
Malte Ubl f43332a0b6 Merge remote-tracking branch 'origin/ni/i10-js-island-requires' into ni/i11-cross-heap-gc 2026-10-27 19:07:22 -07:00
Malte Ubl f4702d6901 Merge remote-tracking branch 'origin/ni/i9-recorded-deferral' into ni/i10-js-island-requires 2026-10-27 19:07:12 -07:00
Malte UblandClaude Opus 5.5 11918b4d93 Treat the worker-startup ordering program as a race
g06 races a Worker's startup against a 300 ms busy timer. One macOS x64
run had Node's worker start after the busy loop (h-sync first) while the
native runs had it before (i-worker first), the same timing dependence
that 1d94942bf marked for g01, p10 and s06. The program now has to
produce the lines of one of Node's outputs, in any order, like the other
race programs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:06:49 -07:00
Malte UblandClaude Opus 5.5 d06155048e Only bare binding getters are pure; compare getter exports for mutation
A member read in an export getter (a.b or a[b]) can invoke another
accessor that runs code on every read, so the recorder now accepts only a
bare identifier as a plain binding getter. Everything else keeps its
runtime read and is not snapshotted.

The mutation check that makes an earlier island module eager copied only
data properties of a module value, so an object exported through a
binding getter could stay stale when a later load changed it. The shape
now reads binding getters the same way the snapshot does, and compares
them by reading again, so those modules no longer defer.

Corpus fixtures pin both: a getter that reads a member counts every read,
and a later load that mutates a getter-exported object keeps both modules
eager.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:06:41 -07:00
Malte UblandClaude Opus 5.5 9ad04922b2 Reject a literal import() of a module inside a rejected import graph
A module reached by an already-rejected literal import() is `covered`, but a
direct import() of that module still needs its own rejection entry: its graph
rejects when that import runs, so it must not join the compiled graph merely
because another rejected import also reaches it. Without the entry the module
is compiled and lowering fails on the unloaded binding instead of the
import() rejecting at run time as Node does.

Walk each direct root independently and extend the unloaded-dynamic-import
fixture with a direct import() of the rejected graph's dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:05:26 -07:00
Malte UblandClaude Opus 5.5 3e643fbb7c Merge origin/opt/npm-unloaded-dynamic
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:05:04 -07:00
Malte UblandClaude Opus 5.5 9931a36bba Absorb a top-type result in a distributed conditional member
The checker's union of `{}` (or `object`, `Object`) with a concrete result
is the top type itself, but mapDistributedConditional mapped the parts
separately and interned a union with a `dyn` arm. Emitting the member then
failed validation with SC9001 ("union ...: arm 0 is dyn"). Return the
checked-dynamic type as soon as a part maps to it, like the existing
`unknown`/`any` absorption.

Cover it in the units corpus: an `Empty<K>` member holds its mapping in a
local so the emitted union is actually exercised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:04:58 -07:00
Malte UblandClaude Opus 5.5 8887f35f74 Fix island signal listener bookkeeping and migrated timer refresh
An island signal registration is now tagged with the raw listener it
installed, so a once registration and an on registration of one function
dispatch through their own listeners instead of both resolving to the plain
function, and the once registration no longer fires indefinitely. The
removal handler forgets the entry whose raw listener is no longer on
process, so a once wrapper that removes itself leaves its runtime
registration behind no longer; duplicate registrations of one function
share a raw listener, and the most recent of those is removed.

refresh() of a timer armed before Node loads re-arms at now + the delay the
timer was created with. Its Node handle was created with the remaining delay
at handoff and a due timer had no handle at all, so the handle is replaced
rather than refreshed.

The signal program registers one function once and on; a new timer program
refreshes a timer most of whose delay passed before Node loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:04:26 -07:00
Chris Tate 439c485506 Represent unions of classes as the instance pointer itself (#2448) 2026-10-27 21:04:17 -05:00
Chris Tate e585024a97 Optimize the speed runtime with a committed runtime profile (#2446) 2026-10-27 21:03:29 -05:00
Chris Tate 677c74c78a Give integer clones borrowed records and worker programs
Integer clones run a pure number function on int32 values and fall back to
the ordinary body when a value leaves the int32 range. Until now a clone
could only take numbers, booleans and borrowed number arrays, and worker
programs had no clones. Recursive functions that walk a record graph, such
as a tree count, always ran on doubles.

A clone now also takes borrowed record parameters:

- A parameter qualifies when its type is a record or `R | null` for a
  record `R`, and its function borrows it.
- The clone reads number fields (exactly int32, otherwise it falls back),
  boolean fields and reference fields of those records in place, tests
  references against `null`, narrows them to the record arm (`null` falls
  back, so the ordinary body reports it), and passes them to other clones.
- A clone has no effect, so the records it reaches stay alive and unchanged
  while it runs: it reads them without retaining them.
- Fields that a clone cannot read in place (inline unions, optional
  properties, other kinds) keep the function on the ordinary path, as do
  class instances.

ELF worker executables now have clones too:

- The stack guard is the thread's own.
- A clone polls the thread's pending word, which a stop request also sets,
  when it is called and every 64 iterations of a loop over plain tail
  calls. On a set word it falls back, and its dispatcher forwards the
  pending exception or stop. Accumulating loops need no poll: their virtual
  stack bounds them.
- Worker programs on other targets keep no clones.

New corpus programs: integer-recursion-records.ts and
integer-recursion-records-workers.ts.
2026-10-27 21:02:26 -05:00
Chris Tate b1e3c46976 Convert unions with typed array, date and regexp arms to strings
String(), template literals and `+` with a string refused a union with an
object arm other than a program class or URL (SC1090). classUnionToString
now also takes typed array, Date and RegExp arms and converts each arm as
ensureString converts it alone: a typed array through its join (a Buffer
through its own toString), a date through toString, a regular expression
through its source and flags.

The npm any-chaining fixture compiles with this PR's index-signature
record conversions and then reached String() of a `URL | Uint8Array`
option in its package. It now matches Node, and its KNOWN entry is
removed.

Adds corpus program union-object-arms-to-string.ts, its corpus cost and
its preflight/order baseline.
2026-10-27 20:49:51 -05:00
Chris Tate 05810305cf Compile small release and speed programs in partitions 2026-10-27 20:49:11 -05:00
Chris Tate b2acdc90a5 Check the lowerer's dynamic import rejections like its other load inputs
The lowerer keeps LoadResult.dynamicImportRejections, the import() roots
that never load. Unit records now check its size, as they check
inlineRequireCycleCalls, so the unit journal covers it.
2026-10-27 20:48:22 -05:00
Chris Tate 6758fabe80 Evaluate computed in receivers and read getter arms in asserted probes
A statically decided `in` over a union of fixed record shapes treated
every field read as a pure receiver, so `"k" in (make().stored as R)`
folded to a constant without calling make(), and the several-arm answer
re-read the receiver once per tag test. A field read is now pure only
over a pure object (isSafeToRepeat). Any other receiver rides one tag test
when the answer is constant, and the several-arm answer evaluates it once
into a hidden local instead of refusing.

A presence probe through an assertion (`typeof (u as R).member`) answered
an arm with a getter of that name as if it lacked the member: undefined,
without running the getter. The per-arm read now calls the getter on that
arm, as Node's read does, and an arm with only a setter keeps the
assertion's checked extraction.

asserted-arm-probe.ts covers both: a receiver whose call is counted, and a
getter arm beside a data arm.
2026-10-27 20:44:33 -05:00
Chris Tate ca4a162e35 Merge main 2026-10-27 20:40:14 -05:00
Chris Tate a0494cdfa3 Keep optional-chain calls out of presence exits
A call whose checker type is never exits the statement only when the call
is made. In an optional chain (`maybe?.fail()`, `this.helper?.fail()`,
`fail?.()`), a missing receiver or callee ends the chain without making
the call, and the checker still types the call never when the chain's
base has a non-nullable declared type, as an array element read does.
Such a statement completes, so a read after it is not proven present.

exitsAbruptly now skips a call whose callee chain uses `?.`.
2026-10-27 20:38:14 -05:00
Chris Tate 1856713700 Represent unions of classes as the instance pointer itself
A union of two or more class arms and at most two unit arms (`A | B`,
`A | B | undefined`, sum types over classes) now holds the instance
pointer instead of a reference-counted %ScrUnion box, as unions of
records already do. NULL and the null sentinel encode the unit arms.

The arm of a value is the arm whose preorder interval holds the
instance's class id. On 64-bit targets, hierarchy members already keep
their class id in the high half of the rc word, or in the 32-bit header
of ProgramLifetime classes. A standalone class that is an arm of such a
union now keeps its id the same way and counts in the low half; the
count reaches the immortal all-ones value instead of wrapping, as for
hierarchy members.

Each union has a tag helper that narrowing checks inline. Unions over
the same classes share one retain, release and trace: retain and
release use the shared 32-bit count, and only a release of the last
reference dispatches on the arm to that class's release. Unions over
ProgramLifetime classes retain and release nothing. When one arm
carries a collector header, every class of every arm's hierarchy does.

Arms may not extend one another, and must all be compact or all keep
the rc word, and all be ProgramLifetime classes or none. 32-bit
targets, library builds, lifetime-region programs, debug builds, and
unions used as Map keys or Set elements keep boxes.
2026-10-27 20:30:01 -05:00
Chris Tate f662831bda Merge main 2026-10-27 20:23:37 -05:00
Chris Tate 21fa3a8ec7 Merge main 2026-10-27 20:22:19 -05:00
Chris Tate 59a08406ac Merge main 2026-10-27 20:19:47 -05:00
Malte UblandClaude 5.5 cc3ed3abf4 Merge origin/main into fix/distributed-conditional-members
Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
2026-10-27 18:16:20 -07:00
Malte UblandClaude 5.5 447493b0a9 Merge origin/main into opt/npm-unloaded-dynamic
Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
2026-10-27 18:16:04 -07:00
Chris Tate 95e715febd Optimize the speed runtime units with a committed runtime profile
The LLVM helper's runtime-unit now optimizes a unit itself (--opt-level),
with IR instrumentation (--profile-generate) or a profile (--profile-use),
through the same pipeline as program code. The Linux x64 pack's speed
units are compiled to unoptimized bitcode and optimized there with
packages/runtime-pack-common/profiles/linux-x64-gnu.profdata.gz, so one
LLVM writes and reads every profile. Release and library flavors are
unchanged. The pack manifest records how the profile matched the units.

Code the runtime profile finds cold is optimized for speed, not size: a
runtime function the training programs rarely call can be hot in another
program. scr_message.c is optimized without the profile.

scripts/refresh-runtime-profile.mjs trains the profile on the runtime
benchmark workloads and, optionally, a tsc-ts checkout, and keeps only the
records of functions the instrumented runtime units define
(merge-profiles --keep-functions). Tests check that the profile still
matches the runtime and names only runtime functions.
2026-10-27 20:11:44 -05:00
Chris Tate a4a844f446 Revert "Keep frozen checked records frozen, and give sloppy-mode this the global object" (#2444) 2026-10-27 20:08:07 -05:00
Chris Tate e00c08d4af Treat a call that never returns as an exit in presence proofs
presentAtUse proves that a binding read cannot be undefined when an
earlier `if` leaves only for an absent value, as in
`if (x === undefined) return;` or `throw`. A branch that calls a function
declared to return `never`, such as `if (x === undefined) fail();` or
`this.fail()`, also never completes. It did not count, so the read stayed
possibly undefined.

A function returning such a read as `string` was then taken to return
undefined too. A string[] it filled counted as holding undefined
elements, which applies to every string[] in the program, so an element
of another string[] passed to process.stdout.write failed to compile
(SC2020, "process.stdout.write of non-string data").

exitsAbruptly now counts an expression statement whose call has type
`never`. The lowerer answers that from the checker
(PresenceBindings.neverReturns).
2026-10-27 20:06:19 -05:00
Chris Tate 6d94457760 Relower units that add Node island module loads and name the island set a setting 2026-10-27 19:55:16 -05:00
Chris Tate 248030cdd8 Merge main 2026-10-27 19:47:24 -05:00
Chris Tate b16c739d34 Classify zero-cost gate differences instead of failing every runtime change (#2271) 2026-10-27 19:27:25 -05:00
Chris Tate 919b79b62a Revert "Keep frozen checked records frozen, and give sloppy-mode this the global object" 2026-10-27 19:20:37 -05:00
Chris Tate 7d670f0e2a Merge main 2026-10-27 18:53:59 -05:00
Chris Tate d2595a4a70 Merge main 2026-10-27 18:49:24 -05:00
Chris Tate dfb7afcdd7 Load Node.js in-process on first use: libnode bridge and runtime loader (#1685) 2026-10-27 18:48:02 -05:00
Malte UblandClaude Opus 5.5 4c2adcf123 Convert a union of tuples into an array slot
A value typed as a union of tuples or arrays, such as the `as const`
key-table read `SEQUENCES[functionKey]` in pi, was refused with SC2003
when it flowed into a `string[]` or `readonly string[]` slot. A single
tuple already converted through the width family's tuple-to-array
rebuild; a union of tuples had no plan at all.

widthLiftPlan gains the unionArr step for an array destination: every
non-unit arm must lift on its own tag (a tuple rebuilds through
tupleArr, an array copies or lifts through arr), and a unit arm takes
the stranded TypeError, since the slot has no representation for it.
%union.arr dispatches on the runtime tag and applies each arm's lift;
applyWidthLift and one coerceToExpected hook wire it into the top-level
edges that the nested slots already reach through the plan.

The step is planned only after the union-into-one-of-its-arms rule and
only when no arm of the source is already the destination, so every
union that converted before - `T[] | undefined` into `T[]`, optional
array fields, `??=` - keeps byte-identical IR; only sources with no
conversion at all gain one.

Adds the differential corpus program, its cost file, an IR unit test,
and the order-parity baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:44:46 -07:00
Chris Tate e941d5d659 Run JavaScript WritableStream, TransformStream and pipes natively (#2437) 2026-10-27 18:43:35 -05:00
Chris Tate ccd87f44fc Keep the scr_map and three-library tests inside their timeouts (#2441) 2026-10-27 18:35:52 -05:00
Chris Tate ea8c1e6a34 Keep frozen checked records frozen, and give sloppy-mode this the global object (#2436) 2026-10-27 18:33:05 -05:00