Commit Graph
3704 Commits
Author SHA1 Message Date
Chris Tate 3cc72ac96c Prepare v0.4.1 release 2026-10-21 09:51:40 -05:00
Chris Tate 77aab9c0fd Address review comments on prototype toString conversions 2026-10-21 09:41:55 -05:00
Chris Tate 5a56cc2564 Answer catch-binding instanceof for values thrown as unknown
A union, unknown or any value is converted to a dynamic value before it
is thrown, and promise rejections and other runtime throws carry dynamic
values too. A catch binding then holds a dynamic payload, which the
native class test does not recognize: `e instanceof C` was false for
every class of a hierarchy, and a read narrowed by it trapped. A test
through a parameter of type unknown was already correct.

When a class or one of its subclasses crosses into unknown, the class
dynamic-dispatch pass now routes the catch-binding tests of that class
(`e instanceof C`, the reads it narrows and `e as C`) through helpers
that keep the native test and answer a dynamic payload with the dynamic
class test, as standalone classes already do. The new "dynamic" caught
test selects that payload, so native payloads are not converted.
Programs whose classes never cross into unknown compile as before.
2026-10-21 09:40:20 -05:00
Chris Tate 9d898d0aff Name the stored header size for compact instances 2026-10-21 09:25:11 -05:00
Chris Tate 8fc36e61f2 Merge main 2026-10-21 09:17:28 -05:00
Chris Tate d78bd871f6 Clear recycled blocks for compact instances and keep TypeScript 7 parity 2026-10-21 09:16:06 -05:00
Chris Tateandjlucaso1 973884eabe Support fs.promises.rm options
fs.promises.rm(path, options) accepts the { recursive, force, maxRetries,
retryDelay } literal options that rmSync accepts. It runs the same removal
and rejects with its errors.

Removing a directory without recursive now throws Node's ERR_FS_EISDIR
SystemError from rmSync and fs.promises.rm. A retryDelay outside int32 >= 0
or a maxRetries outside uint32 throws ERR_OUT_OF_RANGE. rmSync checks these
options before the path, and fs.promises.rm checks the path first, as Node
does.

Fixes #1615

Co-authored-by: jlucaso1 <55464917+jlucaso1@users.noreply.github.com>
2026-10-21 09:13:12 -05:00
Chris Tate fdc6c4ec5d Convert string index arguments without libm calls
The runtime's ToIntegerOrInfinity for string index arguments (slice,
substring, charAt, indexOf, startsWith, padStart, repeat and the others
in scr_string.c) converts through int64 instead of calling trunc, which
is a libm call on baseline x86-64. Each slice made two such calls.
2026-10-21 09:11:25 -05:00
Chris Tate 050bbd852c Collect the frontend's cycles when the native compiler disposes it
The frontend's syntax trees and program state refer to each other, so
reference counting frees almost none of them when a build disposes the
frontend. Whether a cycle pass reclaimed them before the build ended
depended on where the collector's schedule happened to be. Passes during
the frontend found its data live and widened the growth fractions, so the
next pass waited until the heap had grown well past its size at disposal,
and the dead frontend and the emitter's garbage stayed until the build
ended.

The native compiler now runs scr_collect_cycles_phase right after it
disposes the frontend, through a host function. The runtime entry point
sweeps, then restarts the growth fractions and the death rule's backoff
from the heap the sweep leaves, as after a productive pass. Under Node
the hook is unset.
2026-10-21 09:09:19 -05:00
Chris Tate 482c2c0178 Return the pages faulted in past a parked arena's frontiers
When a script thread parks its allocator arena, each class slice still
holds the pages past its bump pointer that were faulted in but never
carved: the rest of the huge page the bump pointer is in, and the pages
the pre-fault helper faulted in ahead of it. The same applies to region
chunks when the thread exits. Until another thread claims the arena,
nothing carves there, so a program that continues on threads that never
claim it (parse threads ahead of a check on the main thread) kept them.

- scr_sa_park_arena returns those pages with MADV_DONTNEED. The next
  thread to claim the arena faults them in again as it carves, and they
  read as zero, as fresh pages do.
- scr_rgn_thread_exit returns each chunk's pages past its bump on worker
  threads; nothing carves in an exited thread's regions.
- scr_mem_release_ahead drops pre-fault requests that overlap the range
  and waits for one in progress before returning it, so the helper does
  not fault the pages in again.
- scr_alloc.test.c checks that a worker that shares and carves past the
  huge-page switch leaves no resident pages past its frontier, keeps its
  blocks, and that the next thread carves on from the frontier, zeroed.
2026-10-21 09:04:23 -05:00
Chris Tateandblurname a7faf91515 Accept defaulted callbacks in literals under an erased generic member
A generic arrow or method held by an object literal is also lowered with
its type parameters erased, for the literal's own value. In that lowering
the object it returns builds as a checked-dynamic value, so a callback
member with an optional or defaulted parameter, such as
`decode(value, at = [])`, met a slot type that maps to a dynamic value.
The completed-signature check refused it with SC1090, although the same
code in a standalone generic function compiled.

A slot whose type maps to a dynamic value now takes the function the same
way an 'any' slot does: the value boxes into a checked-dynamic function,
which completes omitted arguments with undefined.

Fixes #1373

Co-authored-by: blurname <50226652+blurname@users.noreply.github.com>
2026-10-21 08:59:12 -05:00
Chris Tateandejc3 e41858c7df Compile assignment and update targets wrapped in parentheses or non-null assertions
TypeScript accepts parentheses and non-null assertions around the
reference an assignment, a `++`/`--` update or a for-in/of head writes
(`(x) = 1`, `h.sums[k]! += 10`), and both are erased at run time. The
compiler now reads its source files with the AstFile writeTargets option:
the writer's `left`, `operand` or `initializer` is the reference itself
and the reference's `parent` is the writer, so lowering and every analysis
of a binding's or member's writes see one shape. The reference keeps its
own source span. Files read without the option, such as type nodes from
the checker and the AST conformance tests, keep TypeScript's exact tree.

Fixes #1625

Co-authored-by: ejc3 <1694850+ejc3@users.noreply.github.com>
2026-10-21 08:58:51 -05:00
Chris Tate 9c1cabb08c Remove repeated lookups from the class dynamic-dispatch passes
ClassDynamicDispatch.process runs once per reachability wave and builds
dispatch arms for every boxed class. Several lookups inside it grew with
the program and repeated for every dispatch or member:

- propertyReceivers was recomputed at every call site, including once
  per property dispatch. It now returns its last plans for a mode while
  the boxed classes, the registered classes, the subclass links
  (Lowerer.subclassLinks) and the constructed classes
  (Lowerer.constructedClassCount) are unchanged, and each boxed class
  still maps to the same ClassInfo. In canonical mode it also skips the
  subtree of a class it has already seen, which adds no plans.
- The property dispatch loop tests the answered-class set before the
  member lookups, and the method and protocol loops test candidates in
  a Set instead of with Array.includes. A dynamic method call scans the
  boxed constructors for a static method only when no instance candidate
  answers the method, the only case that reads the result.
- Lowerer.liftedFnNamed indexes liftedFns by name and replaces the
  linear liftedFns.find scans in the dispatch, prototype, descriptor and
  method-value helpers.

The emitted IR is unchanged.
2026-10-21 08:57:32 -05:00
Chris Tate 927504af87 Read cached LLVM in parts on warm builds
A warm build that finds the program's LLVM in the cache read the file into
one string before emitting the program object. A program whose LLVM is
past the engine's string limit (three.js nodes, 816 MB) failed there with
`RangeError: Invalid string length`, though its cold build, which keeps
the LLVM in parts, succeeds. The cache hit now reads the file with
readTextParts: parts of at most 64 Mi UTF-16 units, decoded across reads,
none ending inside a surrogate pair, which the emitter takes as a cold
build hands them over.
2026-10-21 08:52:43 -05:00
Chris Tateandjlucaso1 f168402e1f Name the reason in the CompileError for untranslated WebAssembly modules
A WebAssembly module the build finds but does not translate (one that uses
a proposal outside the translated feature set, an invalid one, or one the
translator rejects) now rejects with a CompileError that says the module
was found and names the reason, for example "it uses simd, outside the
translated WebAssembly feature set". Bytes the build did not see keep the
existing message.

The build emits the hash, length and reason of each such module in a
second table, scr_wasm_untranslated, next to scr_wasm_registry; the runtime
reads it when a lookup misses. WebAssembly.validate still answers false for
these modules, and the error stays a catchable CompileError.

Fixes #1617

Co-authored-by: jlucaso1 <55464917+jlucaso1@users.noreply.github.com>
2026-10-21 08:52:18 -05:00
Chris Tate f2fd1c85a2 Run Buffer methods on Buffers held as checked-dynamic values 2026-10-21 08:50:54 -05:00
Chris Tateandsaboursepideh e50454caf3 Support TypedArray.of
TypedArray.of(...items) on the numeric typed-array constructors creates a
new array of its arguments. Each argument converts with ToNumber and then
to the element type, in argument order after all arguments are
evaluated, as in Node. Arguments that are all numbers copy directly;
other values and spreads collect into an array that goes through
TypedArray.from's conversion.

Spreading a dynamic iterable into a call's items (Array.of, push,
TypedArray.of) now drains it through the iterator protocol with the
spread call's TypeErrors, and a typed iterable spread into dynamic items
boxes and drains the same way. Before, both reached a run-time SC2020.

Fixes #1440

Co-authored-by: saboursepideh <25549001+saboursepideh@users.noreply.github.com>
2026-10-21 08:48:03 -05:00
Chris Tateandsaboursepideh 726c2ce3d7 Freeze, seal and query functions through their property tables
A plain function's own properties (name, length, prototype, and the ones a
program assigns, as in `hash.outputLen = 32`) live in its property table.
Object.freeze, Object.seal and Object.preventExtensions of a function now
act on that table and answer the function, and Object.isFrozen,
Object.isSealed and Object.isExtensible answer from it, as in Node:

- Object.freeze of a function was refused as a possibly-aliased value,
  and Object.isFrozen of one threw SC2020;
- Object.isExtensible answered false and Object.isSealed true for every
  function.

Adding a property to a frozen or non-extensible function throws Node's
TypeError; assigning a read-only one throws the runtime's existing
read-only TypeError. Class constructors keep their fences. This lets
@noble/hashes' createHasher return its frozen hasher, so the sha256
program in the issue prints the digest Node prints.

Fixes #1437

Co-authored-by: saboursepideh <25549001+saboursepideh@users.noreply.github.com>
2026-10-21 08:47:37 -05:00
Chris Tate 13790b88c7 Call an object's own or inherited toString in template literals and join 2026-10-21 08:44:31 -05:00
Chris Tate 0237d70df0 Add profile-guided optimization (#1635) 2026-10-21 08:43:57 -05:00
Chris Tate 7f9371c4b2 Write merged profiles through a uniquely named temporary file
merge-profiles wrote the merged profile to `<output>.tmp` and renamed it
into place. Opening that fixed path followed an existing link there and
truncated its target. The temporary file is now created exclusively with
createUniqueFile next to the output, as emission does, and is removed when
writing fails or the stream reports an error.
2026-10-21 08:40:46 -05:00
Chris Tate 538bd2b948 Walk IR child lists with loops instead of per-node closures (#1654) 2026-10-21 08:37:47 -05:00
Chris Tate 721f5d46ef Sort by several keys without calling a chained comparator (#1652) 2026-10-21 08:35:40 -05:00
Chris Tate 9cf8662099 Walk IR child lists with loops instead of per-node closures
everyExprChild, everyStmtChild, mapExprChildren and mapStmtChildren passed
an arrow function to every() or map() for each child list. In the native
compiler that arrow is a closure allocated and released at every visited
node, and each element passes through Array.prototype.every's callback
adapter. The child lists are now walked with index loops in the same
order, so every IR pass that traverses the module visits the same nodes
with less work per node.
2026-10-21 08:35:02 -05:00
Chris Tate f58c321c07 Sort by several keys without calling a chained comparator
A comparator that chains key differences with `||`, such as
`(b.score ?? 0) - (a.score ?? 0) || a.id - b.id`, ran through the
comparator sort: TimSort called the closure once per comparison. Each
term orders one number field, ascending or descending, and a term that is
0 or NaN falls through to the next, so the chain orders elements by their
keys with the first deciding. With no key NaN or -0 that order is
consistent, and a stable sort by the keys gives the comparator sort's
permutation.

The recognizer now accepts such chains of up to eight terms over number
fields of records and class instances, and a key may read an optional
number field with a number default (`x.k ?? 0`), also as a single key. The
runtime reads and checks every key of every element before it moves a
slot, then runs stable keyed passes from the last key to the first. A NaN
or -0 key, or a chain with any other term, keeps the comparator sort.
2026-10-21 08:31:35 -05:00
Chris Tate c1bb553964 Give ProgramLifetime class instances a 32-bit header and packed fields 2026-10-21 08:30:09 -05:00
Chris Tate 0cc0674308 Prepare the first build of an executable in a frontend process
The frontend, lowering and LLVM emission of an optimized executable build run in a child
process when no earlier build of the same invocation emitted its native program object.
The parent passes the LLVM text to the native helper as the file the child wrote, so the
helper runs after the pages the frontend used are released. Later builds of the
invocation find most native partitions cached and peak in the frontend, so they, along
with development builds and builds with SCRIPTC_FRONTEND_PROCESS=0, prepare their input
in-process. The child runs with the parent's Node options without --eval, --print and
their code, or inspector, watch and test runner options. With SCRIPTC_TIMING=1, it
reports its phases on the parent's clock.
2026-10-21 08:23:18 -05:00
Chris Tate 1ee5c84cad Read BYTES_PER_ELEMENT on narrowed-away and untyped typed arrays (#1639) 2026-10-21 04:24:36 -05:00
Chris Tate 8b4e74d050 Type empty arrays filled by later writes inside functions by their reads (#1632) 2026-10-21 04:23:15 -05:00
Chris Tate d60d7dbbc7 Skip the allocator's per-slice MADV_DODUMP calls when no core can be written (#1638) 2026-10-21 04:22:43 -05:00
Chris Tate c7569cf502 Limit profile-driven imports between partitions 2026-10-21 04:21:34 -05:00
Chris Tateandsaboursepideh 916303beff Read BYTES_PER_ELEMENT on narrowed-away and untyped typed arrays
`a instanceof Uint8Array || (... a.BYTES_PER_ELEMENT === 1)`, the
@noble/hashes isBytes guard, narrows `a` to never on the right side when
the guard is specialized for a Uint8Array argument, so the read missed
the typed-array member lowering; it now answers the element size from
the receiver's storage. The checked-dynamic read of BYTES_PER_ELEMENT on
a typed array or Buffer (an untyped argument) answered undefined; it now
answers the element size, as each kind's prototype does in Node.

Refs #1437

Co-authored-by: saboursepideh <25549001+saboursepideh@users.noreply.github.com>
2026-10-21 04:21:00 -05:00
Chris Tate 472fe057ec Mark allocator slices dumpable only when the kernel would write a core
Each class slice a thread opens took one madvise(MADV_DODUMP) call,
which takes the mmap lock for writing, and the threads of a worker
executable open hundreds of slices as they start. The slices are now
made dumpable only when the kernel would write a core of the process:
when kernel.core_pattern starts with '|' or '@', or when RLIMIT_CORE is
above 0. The reservation stays MADV_DONTDUMP in every case.
2026-10-21 04:19:17 -05:00
Chris Tate 17db4cd443 Resolve evolving-array reads through the file's identifier index
The evolving-array analysis walked the binding's whole function and
resolved the symbol of every identifier in it, once per empty-array
binding, so a function with many such bindings cost quadratic time. It now
takes the file's identifiers spelled like the binding from the per-file
index (identifiersNamed), keeps those inside the binding's function, and
resolves only them.
2026-10-21 04:19:04 -05:00
Chris Tate e65ebfd2c7 Fail the zero-cost gate when it compares nothing (#1637) 2026-10-21 03:50:00 -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 8065be3ccd Add profile-guided optimization 2026-10-21 03:13:45 -05:00
Chris Tate 046c30bbb9 Convert possibly undefined indexes of element writes with ToNumber (#1634) 2026-10-21 03:08:52 -05:00
Chris Tateandejc3 b237cf1ff0 Convert possibly undefined indexes of element writes with ToNumber
An element write whose index the checker types as a number but whose
storage may hold undefined (a for...of variable over an array that may
have holes) converted the index with ToNumber only on plain-array writes.
Typed-array writes, compound and update writes, element assignments used
as values and element targets in destructuring assignment now convert it
the same way, as the element reads already do, instead of refusing it as
a non-number key.

Fixes #1622

Co-authored-by: ejc3 <1694850+ejc3@users.noreply.github.com>
2026-10-21 03:04:59 -05:00
Chris Tate 1f554fbc75 Compile the typed-array callback methods (#1633) 2026-10-21 03:01:01 -05:00
Chris Tateandejc3 2078392a52 Compile the typed-array callback methods
forEach, map, filter, some, every, find, findIndex, findLast,
findLastIndex, reduce and reduceRight compile on every typed-array kind
through one shared helper per method shape. The receiver, the callback and
reduce's initial value evaluate in order before the helper runs; the
helper reads the length once and each element when it visits it, so the
callback's writes show in later elements. Callbacks receive the element,
its index and the typed array (reduce's receive the accumulator first).
map and filter return a new typed array of the receiver's kind, map
storing each result with that kind's element conversion, and a Buffer's
map and filter return a Buffer. reduce without an initial value throws
Node's TypeError on an empty typed array. A thisArg keeps the fence, as
for arrays.

Fixes #1620

Co-authored-by: ejc3 <1694850+ejc3@users.noreply.github.com>
2026-10-21 02:55:08 -05:00
Chris Tate 095acffb46 Compile generic arrow bindings inside functions that use no enclosing frame (#1627) 2026-10-21 02:54:32 -05:00
Chris Tate 3e08632467 Refuse values of generic bindings declared inside functions
Each call of a function makes a new function object for a generic arrow
or function expression bound inside it, so `make() !== make()` when make
returns the binding, and such values are distinct Map and Set keys. The
compiled instances are shared module functions, which cannot give that
identity, so a value use of such a binding keeps an SC1090 fence that
names the reason. Calls of the binding still compile.
2026-10-21 02:51:36 -05:00
Chris Tateandejc3 009a01d074 Type empty arrays filled by later writes inside functions by their reads
An unannotated `const xs = []` (or `let`) inside a function, which
TypeScript types by the elements written before each read, takes the array
type its settled reads have: records pushed in a loop give a record array,
numbers a number[], strings a string[]. Reads inside closures, which the
checker leaves unsettled, do not count. When settled reads disagree or the
binding is reassigned, the binding is a dynamic array, as the same binding
already is at module scope. Before, such a binding was always number[] and
pushes of other values were refused.

Fixes #1623

Co-authored-by: ejc3 <1694850+ejc3@users.noreply.github.com>
2026-10-21 02:40:20 -05:00
Chris Tate b3f62a7206 Test records returned across an npm package boundary from different shapes (#1631) 2026-10-21 02:38:43 -05:00
Chris Tateanddevin-ai-integration[bot] 3592a9b7c1 Test records returned across an npm package boundary from different shapes
A JavaScript package method that returns one of two literal shapes (a
result reply or a failure reply) through one declared interface with
optional members hands its consumer the members of the shape it built,
including the ones only one shape has. scriptc 0.3.4 dropped them (the
failure reply arrived as { version, id } and read as a result); the
corpus program keeps that case covered: direct and looped calls, an
async method and a sync one, JSON output, Object.keys and `in`.

Fixes #1314

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-21 02:34:46 -05:00
Chris Tate 87cb214a41 Answer typed-array member probes and destructure into typed-array elements (#1630) 2026-10-21 02:33:14 -05:00
Chris Tateandsaboursepideh e3d31c2a65 Answer typed-array member probes and destructure into typed-array elements
A typed array or Buffer in a compiled program is always a built-in kind
(programs cannot subclass them, add own properties or change their
prototypes), so Node 24's prototype chains for them are known exactly.

- A read of a member the type does not declare (in JavaScript, or under
  @ts-ignore) answers undefined when that chain lacks the name, as Node's
  GetV does: the base64 and hex feature probes (`typeof bytes.toHex`) that
  libraries such as @noble/hashes make. Names Node has, including the
  Buffer prototype's members on a Uint8Array that may hold a Buffer, keep
  the fence.
- `"name" in bytes` with a literal member name answers from the same
  chains, and `bytes.BYTES_PER_ELEMENT` is the element size.
- Array destructuring assignment into typed-array elements
  (`[high[i], low[i]] = [h, l]`) evaluates the receiver, the index and the
  value in order and writes the element as `b[i] = v` does.

Refs #1437

Co-authored-by: saboursepideh <25549001+saboursepideh@users.noreply.github.com>
2026-10-21 02:28:56 -05:00
Chris Tate 00ee68f421 Compile Math.max and Math.min with spreads among other arguments (#1629) 2026-10-21 02:21:37 -05:00
Chris Tateandejc3 89beb65cc6 Compile Math.max and Math.min with spreads among other arguments
Math.max and Math.min with a spread and other arguments (`Math.max(...xs,
5)`) pack their arguments, in order and with each spread flattened, into
one number[] that the runtime folds, as Math.hypot already does. The fold
keeps NaN poisoning, -0 below +0 and the empty fold's infinities, and the
arguments evaluate left to right before it. The refusal hint now names
the supported forms.

Fixes #1621

Co-authored-by: ejc3 <1694850+ejc3@users.noreply.github.com>
2026-10-21 02:17:47 -05:00