Commit Graph
29 Commits
Author SHA1 Message Date
Chris Tate 06f7b8631f Merge perf/shape-overlay-cost 2026-10-26 11:53:34 -05:00
Chris Tate e8a64267cb Call toJSON on foreign elements and merge main 2026-10-25 13:13:02 -05:00
Malte UblandClaude Opus 5.5 34eef83ef4 Decide concat spreading of dynamic values at run time; keep JS lets assigned from index reads unchecked
Three JavaScript lowering bugs threw a checked conversion where Node answers:

- concat with a checked-dynamic argument lowered it as an element, so an
  array argument threw. The argument now spreads when it is an array at run
  time and is appended otherwise.
- `let c; while ((c = str[i++]) !== "]" && c)`: the checker types c from the
  index read, without the undefined a read past the end answers. Evolving-let
  inference ignores index-read writes, and reads of a JS `let x;` statement
  binding keep the actual value.
- Strict equality on a flow-narrowed dynamic JS name compares unchecked.

npm cases concat-dynamic-args and evolving-let-index match Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 09:19:53 -07:00
Chris Tate a538ff42f0 Read only value slots in lastIndexOf with an element-typed needle
lastIndexOf with a needle of the element type read every non-hole index
as the element type. An undefined or foreign element written through a
dynamic view (or a hole that toSorted turned into undefined) has no
element-typed payload, so the read aborted with "array index N is
undefined". The search now reads an index only when it holds a value,
and skips undefined and foreign elements, as Node's strict equality
does for a string or number needle.
2026-10-24 16:19:16 -05:00
Chris Tate 970a53c1ba Store (T | undefined)[] elements as T so widened arrays stay one array
A number[], string[] or boolean[] seen as (T | undefined)[] through a
cast, a binding or a parameter is the same array in Node: undefined
written through either name is visible through both. The type mapper
now stores (T | undefined)[] of a number, string or boolean T as T
elements, which hold a present undefined by their slot state, so the
wider name shares the storage instead of converting to a separate
union array.

Writers of these arrays store undefined as a present undefined:
element writes, push, unshift and splice, literals, Array.from (with a
length or a mapper), Array.of, generator drains, flatMap, rest packs,
copies from Map and Set values and Object.values, and checked
conversions from dynamic values. Promise.all over Promise<T |
undefined>[] fills the result inside the combinator's settlement, so it
settles on Node's turn. The element-state analysis counts callbacks and
iterables whose checker type admits undefined.

Readers keep their optional reads: index reads through a union of
arrays, searches with a direct needle, and asserted reads, which throw
a TypeError for undefined. Comparators and callbacks that receive
present elements take the element type, without an adapter.
2026-10-24 00:05:48 -05:00
Chris Tate e7943984a5 Bind both Dates before comparing, and cache only new Date locals as time values 2026-10-23 15:50:09 -05:00
Chris Tate 8d5b3377aa Represent Dates as objects with identity
A Date is a refcounted object holding its time value, shared with the
checked-dynamic Date handle, so it keeps its identity through unknown.
Dates compile in arrays (includes, indexOf, lastIndexOf, join, default
sort by toString() text), in Date | undefined and Date | null slots, as
Map and Set keys and values, in index signatures, and in records, tuples,
class fields, closures, promises and generators. === compares identity,
relational operators compare time values, new Date(date) makes a new
Date, and JSON.stringify writes toJSON().

Closes #225
Co-authored-by: Max B <max@tfq.at>
2026-10-22 20:33:09 -05:00
Malte UblandClaude Opus 5.5 982572c173 Dispatch splice on arrays of unknown elements
Arrays the checker types with unknown elements are stored checked-dynamic,
and their methods dispatch on the runtime array. splice was missing from
that dispatch table, so unknown[] receivers refused it, including generic
bodies instantiated at unknown (pool.splice(i, 1) in a sample<T> helper
gets such an instantiation once a program has a dynamic tier). The
runtime's dynInvoke already implements splice; the table now names it,
and arrays of unknown elements take the dispatch from the array-method
lowering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:10:08 -07:00
Chris Tate 79684d0ef4 Name lowered helpers and closures independently of lowering order
Lowering named helpers by the size of their registry at first use
(%union.retag.N, %arr.map.N, ...), lifted closures and optional-chain
locals by compiler-wide counters (%fnN, chain.N), generic instances by
their instance count, and records and unions by creation order (rN, uN).
Class expressions, mixin instances, function-local generics, non-exported
namespace segments and some module globals carried the byte offset of their
declaration. A name therefore depended on which unit lowered first and on
the positions of unrelated code.

Helpers, instances, records and unions are now named by a hash of the key
they are interned by (keyedHelperName). Closures and optional-chain ids are
named by their lowering unit, their structural position and a count per
key within the unit. Offset-based names use the structural position: the
chain of enclosing named declarations (each with its kind, name and
ordinal) and the node's ordinal among same-kind nodes inside the nearest
one, so an edit outside that declaration leaves the name unchanged.
Interfaces and type aliases anchor the positions inside them, and default
library files are named by their base name, so tsgo and tsc-ts, which
install them in different directories, give the same positions.

Recursive record and union placeholders are named by the printed type, the
structural position of the declaration and the same for up to three levels
of type arguments; a description that two types still share gets a
numbered suffix for the later one. A generic class instance keyed by a
literal type argument is named by the literal's text or the argument's
description instead of its checker type id. Two different keys with the
same name throw an InternalCompilerError.

The SC6101 performance warning recognizes the new closure names
(fn.<hash>, with _<name> for a named function expression) and lists the
functions that rank alike in source order. The module lists its records
and unions by id. The generated program is unchanged apart from these
names and that order, and test:lowering-order now fails on any name
difference.
2026-10-22 03:44:38 -05:00
Chris Tate 777bfb43f2 Revert "Name lowered helpers and closures independently of lowering order"
This reverts #1750 (05e2f404d), which made main CI fail.
2026-10-22 01:40:17 -05:00
Chris Tate 61f1329b32 Name lowered helpers and closures independently of lowering order
Lowering named helpers by the size of their registry at first use
(%union.retag.N, %arr.map.N, ...), lifted closures and optional-chain
locals by compiler-wide counters (%fnN, chain.N), generic instances by
their instance count, and records and unions by creation order (rN, uN).
Class expressions, mixin instances, function-local generics, non-exported
namespace segments and some module globals carried the byte offset of their
declaration. A name therefore depended on which unit lowered first and on
the positions of unrelated code.

Helpers, instances, records and unions are now named by a hash of the key
they are interned by (keyedHelperName), and recursive records and unions by
their printed type and declaration. Closures and optional-chain ids are
named by their lowering unit, their structural position and a count per
key within the unit. Offset-based names use the structural position: the
chain of enclosing named declarations and the node's ordinal among
same-kind nodes inside the nearest one, so an edit outside that
declaration leaves the name unchanged. Two different keys with the same
name throw an InternalCompilerError. The module lists its records and
unions by id. The generated program is unchanged apart from these names
and that order, and test:lowering-order now fails on any name difference.
2026-10-21 23:26:29 -05:00
Chris Tate e41f642a55 Compile literal-key 'in' over index-signature records, fresh array spread insertions, and any-annotated dictionaries (#1514) 2026-10-20 18:46:29 -05:00
Chris Tate 5765d89227 Check the frontend's types against tsc-ts/api
The frontend calls tokenStartOfNode, getTextOfNode, getSourceFileOfNode and
forEachChild instead of AstNode's position and traversal methods. Node lists
answer hasTrailingComma(), and a declaration's modifiers are a modifier list
with their flags.

Symbol, Type, Signature, IndexInfo, TypePredicate, Program, TypeChecker and
Diagnostic have tsc-ts/api's members and types. Compiler code reads symbol
names with symbolName(symbol).

tsc-ts goes to 0.0.5. api-conformance.ts asserts that AstNode, AstNodeList,
SourceFile and each node interface are assignable to tsc-ts/api's interface of
the same name, and names the members where the wire format differs.
2026-10-20 13:41:39 -05:00
Malte UblandClaude Opus 5.5 7bb72e1e17 Compile literal-key 'in' over index-signature records, fresh array spread insertions, and any-annotated dictionaries
- '"k" in record' with a literal key naming no declared member of an
  index-signature record (a match's named groups) takes the keyed
  presence probe a runtime key takes.
- Spreading a fresh array literal (directly, through a conditional, or
  through a const used only there) into push/unshift or an insertion
  converts its elements into the destination's element layout; nothing
  else can reach them, so the copy is unobservable.
- A const annotated with a type the checker reads as 'any' and
  initialized by an empty object literal holds the checked-dynamic
  object, so keyed writes fill it (the empty struct could hold nothing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 03:09:53 -07:00
Chris Tate 3c749a2895 Read AST lists through their nodes
The frontend's node lists become node lists, { pos, end, nodes }, the
shape of tsc-ts's NodeList, so that the frontend can later read tsc-ts's
own nodes. AstNode's list getters return an AstNodeList, ts.NodeArray<T>
is that class, and the frontend reads a list's elements through nodes.
The lists already carried their positions on the wire.

The class is the type, not a structural interface, because native code
represents a class and a record differently.

A decoded file keeps each list's nodes array, as before, and each read
of a list field returns a new AstNodeList over it, so the file holds no
more objects than before. nodes keeps its identity; a list object is not
the same object on each read.

The change is mechanical: a type-aware rewrite of each list field read,
plus hand edits where lists are read through casts, generic accessors
or test mocks. The generated getters and types follow from
generate-ts7-ast-schema.mjs. AstFile and Ts7Session drop their unused
listMetadata hook.
2026-10-19 06:33:33 -05:00
Chris Tate 0210e54e49 Lower filter, find, findIndex, some and every on tuples
Fixed tuples, readonly or mutable, lower these predicate methods like map
and flatMap: each position is read when it is visited, find, findIndex,
some and every stop at the first deciding position, and predicate results
and narrowing follow the array methods' rules.

Fixes #1171
Co-authored-by: BaoLei <50226652+blurname@users.noreply.github.com>
2026-10-17 15:50:17 -05:00
Chris Tate 022ac6eba9 Store arrays of interface values natively and read interface fields directly
Arrays whose elements are class-backed interface values are native arrays
of the element capsules, so iteration, indexing and most array methods no
longer go through the checked-dynamic array, and method calls receive a
receiver they can dispatch. Methods without a native form for these
elements run on the array's live dynamic view.

A read of an interface property signature has the property's static type
and reads the field of an implementing class directly. Accessors, object
literals and JavaScript classes keep the checked property read.
2026-10-14 17:38:02 -05:00
Chris Tate 3f8e0bcb13 Use plain wording in docs, comments and test names 2026-10-14 11:43:44 -05:00
Chris Tate 7e6294ca0f Make scriptc compilation native-only 2026-10-12 12:25:30 -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 b4134160f4 Splice runtime-optional items without a temporary item array
`a.splice(i, n, v)` where v may hold undefined at run time (a for-of
binding or unchecked read, as in reorderCandidates) always built a
present-undefined item array. It now evaluates receiver, start, count,
and v once, in order, and branches: a present v is spliced as a plain
element literal, a missing one keeps the item array. In statement
position the emitter now also looks through such sequences and
conditionals, so the present arm drops its removed elements with
scr_arr_splice_drop and allocates nothing.
2026-10-09 10:10:23 -07:00
Malte Ubl c75cc167dd Append runtime-optional push values in place
`a.push(v)` where v may hold undefined at run time (an unchecked array
read or a hole-observing for-of binding, as in tsc-ts's filterType loop)
built a temporary one-element array, stored v or present undefined into
it, and spread it onto the receiver: an allocation, a growth, a copy and
two releases per append. Such pushes now evaluate every argument first,
then append each present value through the ordinary push (and its inline
dense fast path) and each missing one as a present-undefined slot at the
current length, which is what Node stores. Pure receivers and arguments
are re-read instead of copied into owned temporaries.

Call arguments read from arrays of nullable references now take the
inline dense lookup like other projections (the value is the element
pointer itself, so the path is a few loads), and scr_arr_peek_ref answers
dense in-range indices before the general lookup. compareSymbols'
`declarations[0]!` arguments no longer leave compiled code.
2026-10-09 10:10:22 -07:00
Chris Tate 477f97a840 Track optional values precisely through callbacks, captures and pops (#784) 2026-10-09 10:33:07 -05:00
Chris Tate f6390d878b Carry optional values, never-returning calls and native iterators through typed returns (#739) 2026-10-08 11:04:59 -05:00
Chris Tate 06b03f06a9 Compose class references across unions and collections (#734)
* Compose class references across unions and collections

- Compare class, function and native references held in differently represented unions with `===`, `!==` and `Object.is` while preserving subclass identity, and refuse comparisons that a structural union arm leaves undecidable
- Keep subclass values intact through base-typed array and Set insertion spreads, `concat`, and inferred class-predicate `filter`/`find`/`findLast`, and allow checked class refinement of captured optional values with several class arms
- Split the sanitized compiler self-rebuild CI job at the IR boundary, after giving sort helpers distinct IR nodes so serialized and in-memory IR emit the same LLVM

* Keep compact comparisons for plain union arms

- Compare a plain operand with a union that already holds its type through the existing inline union comparison, preserving established parameter representations
- Update the coverage expectation for the accepted class refinement
2026-10-08 07:35:21 -05:00
Chris Tate 08772d01c8 Preserve narrowed class values and collection composition
- Preserve narrowed class and generic values, abstract properties, and standalone caught-class identity.
- Support identity-preserving array concatenation and typed-array collection values, with focused differential and sanitizer coverage.
- Share revision-matched bootstrap seeds across CI jobs while retaining command, rebuild, packaging, and sanitizer checks.
2026-10-08 01:21:43 -05:00
Chris Tate c48e5bea09 Improve native callbacks, value narrowing, and runtime metadata
- Keep typed callbacks on native paths while preserving receivers, evaluation order, and method replacement.
- Preserve caught class identity, collection narrowing, and missing indexed values across typed code.
- Add standard terminal access and filesystem error metadata with differential coverage and versioned runtime compatibility.
2026-10-07 21:54:58 -05:00
Chris Tate f22013f177 Improve array storage and bulk transformations
- Share dense array storage and move owned ranges without rebuilding containers.
- Batch array construction and insertion while preserving evaluation order and bounded temporary storage.
- Lower indexed mutations natively and preserve typed-array reversal across numeric element types.
2026-10-06 01:05:40 -05:00
Chris Tate 6d16696b8e Refactor container lowering and callback builders
- Give arrays, collections, iterators, and indexed objects explicit lowering owners.
- Share callback frames and live collection iteration while preserving existing behavior.
- Enforce container ownership boundaries with compiler quality checks.
2026-10-05 20:00:42 -05:00