Commit Graph
31 Commits
Author SHA1 Message Date
Chris Tate 97d6ebac6a Revert "Check the optional-record path test's cast at the cast (#1592)" 2026-10-20 20:22:18 -05:00
Chris Tate aec7758dfb Revert the checked-cast fallback (#1561); keep the optional-chain fix (#1601) 2026-10-20 19:46:23 -05:00
Chris Tate 2230ffbb27 Check the optional-record path test's cast at the cast
The test from #1548 casts JSON.parse to Partial<Envelope> into a
block-local const, which continues as its parsed value since #1561, as
in Node. The path messages are now pinned with the cast passed to a
typed parameter, which is checked at the cast, and the block-local form
is pinned against Node.
2026-10-20 19:25:18 -05:00
Malte Ubl c76892dc74 Revert "Continue a failed checked cast with its value in a dynamic tail (#1561)"
This reverts commit 6bb2711c4b, reversing
changes made to cd833e68a4.
2026-10-20 16:42:01 -07:00
Chris Tate 6bb2711c4b Continue a failed checked cast with its value in a dynamic tail (#1561) 2026-10-20 18:15:01 -05:00
Malte UblandClaude Opus 5.5 1ef31a1b74 Continue a failed checked cast with its value in a dynamic tail
`JSON.parse(text) as T`, `const x: T = anyValue` and `await res.json()
as T` threw `TypeError: expected X at $.path, got Y` when the value did
not fit `T`. Node never checks an assertion and continues with the value
it has, so the build-container listener rejected an order Node accepts
and a JWT with a string `exp` became a 500.

A failed cast now keeps its value when it initializes a `const`/`let`
binding declared in a function or block and only the code after the cast
reads the binding. That code (the statements after the cast in each
enclosing statement list, up to the declaration) lowers twice: the
static copy, unchanged, runs while the value fits; a dynamic copy, in
which the binding and everything derived from it is typed `any`, runs
after a failure, so missing members read undefined, wrong-typed members
keep their values, extra members stay visible in source order, and
objects keep their identity. The cast sets the flag that selects the
copy: the fused JSON.parse helper's slow path and the plain check call
scr_dyn_check_recover, which takes back the TypeError and parks the
value for checkedCast.fallbackTaken/fallbackTake.

A failed value that flows into a typed slot outside the region is
checked there, as any `any` value is: it continues when it fits and
throws at that use when it does not. A statement of the dynamic copy
with no `any` lowering becomes a runtime fence that throws only when a
failed value reaches it. Module-level bindings, regions with hoisted
declarations or super() calls, union targets and targets holding class instances, and
--tier=static-only (and library mode) keep the throwing cast.

`a?.b.c` on an `any` value now short-circuits only on `a`, so `.c`
throws when `a.b` is undefined, as in Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:25:33 -07:00
Malte UblandClaude Opus 5.5 b5e99ac67b Name the deepest failing path when a checked cast fails inside an optional record
A checked cast such as `JSON.parse(text) as Partial<T>` makes every member
an `X | undefined` union. When a present object failed X, the error named
only the union's own path ("expected object | undefined at $.order, got
object"), not the missing or mistyped member inside it.

When no arm of a union matches and the value is the one container kind
exactly one arm can take (an object for the union's only record arm, an
array for its only array arm, beside unit and scalar arms), the failure
block now runs that arm's builder, which throws at the deepest failing
path with its reason ("expected string at $.order.owner.id, got
number"). Other values keep the union's message.

Only the failure block of union dynCheck builders changes; the matching
path is untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 13:13:35 -07:00
Chris Tate 201857eb6c Read the class base after the callable-base case in super calls
superCallStmt asserted the class base before checking for a callable
base. A class that extends an ordinary function constructor has no base
class info, so the assertion held null. Node ignores the unused value;
the native compiler throws the TypeError of a non-null assertion that
held null, and failed to compile programs with such classes (for example
the Effect stream fixtures). The base is now read after that case
returns.
2026-10-18 07:54:21 -05:00
Malte Ubl a850eca98d Pin kept extras in the width-tolerance dyncheck test
The checked conversion now keeps undeclared keys for serialization, so the scriptc-only test that pinned dropping them compares against Node instead.
2026-10-16 06:31:52 -07:00
Malte Ubl dff726715b Keep undeclared keys of checked JSON records
`const parsed: { score: number }[] = JSON.parse(doc)` printed a different
checksum from Node: the checked conversion (and the schema-directed
JSON.parse fast path) stored only the declared fields of each object in
its native record, so JSON.stringify(parsed) silently dropped `id`,
`name` and `active`, and every other reflective surface agreed.

A closed record converted from an ordinary object now keeps the source's
own properties beside it when the object has undeclared keys or holds the
declared ones out of declaration order. The runtime keeps a per-thread
table from the native record to a view of the source's own keys
(placeholders for declared keys, which the record still owns), released
through the native dispose hook when the record is freed:

- the checked record builder notes its source (scr_record_source_note);
- the schema parser parses undeclared members into the view instead of
  declining, so typed parses keep their one-pass fast path;
- typed JSON.stringify writes such records in source key order, declared
  fields through a generated per-shape field writer and the others through
  the dyn writer;
- record capsules adopt the view and refresh it in place, so untyped reads
  and writes see and persist undeclared properties;
- util.inspect and Object.keys/values/entries answer through the live view
  (values/entries check the extra values into their static result type and
  throw the checked-conversion TypeError when one does not fit).

Copies made by object spread, Object.assign, rest destructuring or
conversion to another record type still hold only the declared fields;
the limitations page documents this.
2026-10-16 06:31:51 -07:00
Malte Ubl 6cd96eec49 Lower compound assignment with untyped operands like the expanded form
`checksum += parsed[i].score` (a number local plus an `any` value) failed
with SC1043 "comparing non-number, non-string values" in TypeScript, while
the expanded `checksum = checksum + parsed[i].score` compiled: the
compound lowering only routed checked-dynamic operands through the
runtime operators in JavaScript files.

Locals, record fields and array elements now lower `op=` with an untyped
right operand (or an untyped local target) exactly like `x = x op rhs`:
dyn.add / dyn.arithmetic / dyn.bitwise, then the checked conversion into
the target's declared type. Numeric operators keep Node's ToNumber
semantics; a `+=` that produces a string for a `number` target throws the
same catchable TypeError as the expanded assignment and leaves the target
unchanged (pinned in tests/harness/dyncheck.test.ts).

Remaining SC1043 refusals for arithmetic, unary, update and compound
operators now name the operator and operand types instead of the
comparison wording.

The retired-engine-763 diagnostics fixture compiles now and matches Node,
so it moves to the corpus as 5343.
2026-10-16 06:31:51 -07: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 dedb568ade Pin wrong-typed input on the schema-directed JSON.parse path
Seventeen Order inputs whose shape disagrees with the target (wrong leaf
kinds at the top level, in nested records, record arrays and string
arrays; null for object and number; missing fields; object vs array at a
field and at the root; duplicate keys; truncated JSON; 1e400 and a lone
surrogate; extra nested members; reordered keys) plus an Item[] root.
The expected lines are the output of the checked-route-only build
(perf/harness); the fast path must decline or agree on every one.
2026-10-08 15:05:00 -05:00
Malte Ubl 0c2232deb0 Parse typed JSON straight into native records
`JSON.parse(text) as T` used to build a checked-dynamic ScrDyn tree, copy
it into the native layout with the generated dynCheck builder, and then
dispose the tree. Together that was about 68% of json-records.

For record/array targets with number, boolean and string leaves, the
backend now emits a ScrJsonSchema (field offsets in declaration order,
constructors, releases) and a wrapper. The wrapper calls the new
scr_json_parse_schema, a one-pass parser that writes directly into native
records and arrays. Whenever it declines (syntax errors anywhere, kind
mismatches, missing or duplicate declared keys, excess depth) it releases
its partial value without throwing, and the wrapper reruns the unchanged
scr_json_parse + dynCheck route. Every error message and position stays
byte-identical. The lexer helpers are shared through a quiet mode and a
factored number scanner, so values decode bit-identically.

Tests: new differential corpus program json-typed-parse-direct.ts; a
dyncheck harness case comparing typed and untyped error messages through
the fused path; white-box C tests for accept/decline and leak-freedom in
scr_json.test.c; an LLVM emission test (64-bit and wasm32).

Linux (sandbox x86_64, runs=15):
| workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node |
| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: |
| json-records | 713.2 | 249.3 | -65.0% | -65.6% .. -64.1% | faster | +2.2% | 439.04 | 0.57x |
| regex-logs | 350.8 | 353.7 | +0.8% | -0.9% .. +2.3% | neutral | +0.0% | 238.42 | 1.48x |
| ast-interp | 379.1 | 376.8 | -0.6% | -1.8% .. +0.4% | neutral | +0.0% | 227.45 | 1.66x |
| records-sort | 1200.5 | 1207.8 | +0.6% | -2.4% .. +5.7% | neutral | +0.0% | 232.17 | 5.20x |
| template-render | 248.3 | 248.1 | -0.1% | -2.3% .. +1.0% | neutral | +0.0% | 202.16 | 1.23x |
| functional-pipeline | 402.0 | 395.3 | -1.6% | -3.8% .. +0.5% | neutral | +0.0% | 254.57 | 1.55x |
| async-pipeline | 202.4 | 201.7 | -0.3% | -1.4% .. +2.0% | neutral | +0.0% | 90.14 | 2.24x |
| word-graph | 366.9 | 348.2 | -5.1% | -12.9% .. +4.3% | neutral | +0.0% | 576.56 | 0.60x |
| alloc-trees | 513.5 | 525.5 | +2.3% | -2.1% .. +4.0% | neutral | +0.0% | 132.99 | 3.95x |
| numeric-kernels | 121.4 | 122.5 | +0.8% | -1.9% .. +3.6% | neutral | +0.0% | 135.91 | 0.90x |
| validate-errors | 206.0 | 209.2 | +1.5% | -3.6% .. +4.1% | neutral | +0.0% | 703.47 | 0.30x |
| csv-numbers | 450.9 | 448.6 | -0.5% | -2.2% .. +2.0% | neutral | +0.0% | 499.25 | 0.90x |
| log-summary | 253.5 | 252.6 | -0.3% | -1.5% .. +2.0% | neutral | +0.0% | 279.67 | 0.90x |
| inventory-report | 433.7 | 433.2 | -0.1% | -6.0% .. +3.6% | neutral | +0.0% | 413.79 | 1.05x |
geomean change -7.4%; geomean candidate/node 1.321x -> 1.223x

macOS (arm64, runs=15):
| workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node |
| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: |
| json-records | 292.3 | 153.0 | -47.7% | -50.1% .. -46.6% | faster | +0.4% | 248.7 | 0.61x |
| (all 13 other workloads neutral; see results/h16-typed-json/local-1.json) |
geomean change -4.8%; geomean candidate/node 1.497x -> 1.425x
2026-10-08 15:05:00 -05:00
Chris Tate c08ab955ae Enforce linting and consistent source formatting
- Enforce oxlint and oxfmt for owned JavaScript and TypeScript with independent CI checks.
- Preserve compiler import boundaries and protected fixtures while cleaning up lint findings.
- Standardize source formatting and validate import, fixture, and embedded-source protections.
2026-10-05 17:51:24 -05:00
Chris Tate e2d58f60d2 Expand static support for common Effect 4 patterns (#600)
* Expand static compilation for common Effect 4 patterns

* Run every Effect 4 fixture slice in CI

* Preserve existing corpus behavior across Effect 4 lowering

* Preserve Effect platform callbacks and native stream state

* Record new corpus baselines and retain child stdin refusals

* Retire the obsolete stateful regex diagnostic

* Preserve promise reference count layout for cycle collection
2026-10-01 17:36:29 -05:00
Chris Tate 2069684b40 Expand static Effect support across common APIs (#598)
* Expand static Effect support across common APIs

* Preserve compiler parity across expanded Effect patterns

* Fix compiler regressions exposed by Effect coverage

* Bound shared type conversion traversal

* Clarify conversion contracts and tidy fixtures

* Run every Effect fixture in CI shards

* Preserve inferred array assignments in concurrent Effect programs

* Record inferred array fixture compiler ordering
2026-10-01 02:49:31 -05:00
Chris Tate 3f66c8e20b Make LLVM the sole production backend
- Remove C emission, debugging options, and automatic backend fallback.
- Complete LLVM lowering and native bootstrap coverage across supported targets.
- Link executables and libraries with precompiled C runtime packs, including iOS and Android.
2026-09-29 20:54:09 -05:00
Chris Tate 280fe1a8da feat: preserve class and child handles through unknown (#331)
* feat: preserve class and child handles through unknown

* fix: hide private fields from unknown views
2026-09-15 19:30:22 -05:00
Chris Tate 2c0f01ad93 HTTP Agent and dynamic-handle compatibility
- Add Agent connection accounting and HTTP/socket compatibility surfaces
- Support dynamic Buffer at and slice access
- Pin behavior with differential corpus coverage
2026-07-24 18:52:19 -05:00
Chris Tate 6b6445ab90 Retire the "DOM" nickname in comments, internal docs, and message prose: the checked-dynamic tree / dyn values
- The ratified rename rider (world-unification §7 lane 5): "DOM" collided with the browser DOM and, post-lane-1, the structure is no longer a document tree. Comments and doc prose now say "the checked-dynamic tree" for the noun and "dyn" adjectivally; runtime symbol names (scr_dyn_*), the error.domClone libCall, and the real web DOMException family are unchanged.
- The few user-facing strings carrying the nickname move with it (the structuredClone cyclic message, the dc subscriber/publish fences, two emitter ICE texts); diagnostics snapshots regenerated (96 pass). One private compiler constant (DOM_PROTO_METHOD_NAMES -> DYN_PROTO_METHOD_NAMES) rides along.
- Mechanical sweep, 142 files, comments/prose only — no behavior change.
2026-07-24 11:14:58 -05:00
Chris Tate b98d726382 for-of, destructuring, and spread over island values held in 'unknown' drain the engine's own iterator; engine arrays exit into 'any[]' slots
- The iteration pack's JSVAL arm is ARMED (scr_dyn_iter_pack, scr_dyn_arr_push_spread, scr_dyn_pack_push_spread_iter route through the new iter_drain ops row): the ENGINE's iterator protocol drains into a fresh DOM array — engine arrays, generators, Maps, Sets, and Symbol.iterator implementations step exactly as Node runs them, elements wrapping back scalar-normalized.
- for-of over checked-dynamic values LOWERS now (both backends): the source packs once through dyn.iterPack and a hidden index loop binds each element as a dyn value (new dyn.arrLen/dyn.arrAt accessors) — an eager snapshot, the matchAll stance. Pattern heads, pre-declared and var heads, and labeled break/continue all ride the desugar. Non-iterables throw V8's for-of CallPrinter spellings, threaded into the engine-side guard.
- The validated exit gains the jsval-element-array arm (scr_jsval_exit_jsval_arr, both backends): an 'any' value exits into 'any[]'- and 'any[] | undefined'-declared slots Array.isArray-gated, elements BY REFERENCE (identity crosses, the spine is a snapshot) — the withPlugins loadPlugins(plugins) boundary, the driver frontier's first named poison.
- Corpus 2604 and npm case jsval-iterate (package-minted Set/Map/generator/Symbol.iterator) are byte-identical to Node under both backends; dyncheck's jsval-iterate pin flips from the retired island fence to the armed answers; SAN clean over the drain/wrap/exit churn.
2026-07-24 11:04:10 -05:00
Chris Tate 8cb44e4345 Record order-parity baselines and the island destructuring fence for the merged train
- Baselines for the renumbered fixtures, purely additive
- iterPack loud-fences engine-held sources instead of claiming not-iterable on values Node iterates
2026-07-24 08:15:35 -05:00
Chris Tate 6a4376c9ee The string-or-object union family collapses into the checked-dynamic world
- Unions carrying an object/unknown-flavored arm beside dyn-subsumable siblings map to dyn wholesale; typeof and isArray narrowing dispatch natively
- Class instances, Maps, Sets, functions, promises, and regexes stay excluded so typed semantics never degrade; the component fence names the blocking sibling
- A dyn element makes the whole array the DOM value; element reads and writes ride the routed machinery

# Conflicts:
#	tests/harness/dyncheck.test.ts
2026-07-24 08:00:33 -05:00
Chris Tate 613d205a25 Routed operations over engine-held dyn values: keyed access, calls, method dispatch, and nullish tests
- Keyed reads and writes, dynCall, and method dispatch on engine-held values route through the engine, which runs its own prototypes JS-exact
- One generic host shim carries boxed dyn functions into engine calls over the uniform thunk signature
- Nullish coalescing and optional chains unfence for every dyn value, engine-held or not
- Mixed island-vs-DOM deepStrictEqual comparisons loud-fence instead of minting a fabricated failure
- The formatter driver's support slice runs past its flatMap wall
2026-07-24 07:59:12 -05:00
Chris Tate 42c2725e5d Collapse string|object-family unions into the checked-dynamic representation, retiring their SC2009 fences
- mapType: a union with an 'object'/'unknown'/'{}'-flavored (dyn) arm beside only dyn-subsumable siblings — scalars, units, dynFrom-convertible records/arrays — maps to DYN wholesale; class/Map/Set/func/promise/regex arms keep their typed homes, and the SC2009 classifier now names the sibling that cannot ride the collapse instead of blaming the dyn arm.
- A dyn ELEMENT makes the whole array the DOM value (unknown[], object[], the plugins-slot (string | object)[]): the DOM array literal builds from TS annotations, .length on collapsed-array receivers is a DOM keyed read, element writes ride dyn.keySet (ARR index set/extend with undefined-hole padding), and for-of over dyn gets a pointed operation fence instead of the stale component story.
- Appendix-A repro 4 stops SC2009-fencing: an island plugins array wraps at the collapsed slot (dynFromJsval) and the element read reaches the loud keyed-read ladder until the routing lane arms it (dyncheck pin jsval-collapsed-union-slot).
- Corpus 2584/2585 differential both backends (guard ladder, option tables, unknown[] first-class; SAN clean); json-dyn snapshot repinned where the unknown[] declaration fence retires; conformance movers verified locally: discriminateObjectTypesOnly, unknownPropertiesAreAssignableToObjectUnion, unknownLikeUnionObjectFlagsNotPropagated.
- SEMANTICS 398 documents the collapse rule, the exact subsumable-arm domain, and the width/identity consequences (deep-copy entry, full-width JSON, per-site fences for non-subsumable values).
2026-07-24 02:54:47 -05:00
Chris Tate cbb567f901 Fixtures for the jsval→DOM crossing: rows 1-3 differential, the fence ladder pinned
- corpus 2578 (TS) and 2579 (JS, the JSDoc @param {object} repro shape) pin typeof/truthiness/String()/===, scalar normalization, representation-free narrowing, and the identity round trip against Node on both backends
- npm case jsval-into-dyn drives the prettier-plugins shape over a real package surface (the new plugstub fixture package: any-declared exports carrying engine functions)
- dyncheck jsval-* tests pin every loud fence (keyed read/write, member calls, 'in', destructuring, JSON.stringify, structuredClone, Object.keys, deepStrictEqual, inspect) plus the RC-clean catch-and-continue unwind under the SAN lane
- the errors.test.ts SC1101 pin flips to the now Node-exact behavior (the retired "one unbridgeable mix": the wrapped argument compiles and the unbound-this read throws Node's own TypeError); ts7 order-parity baselines record the three new fixtures
2026-07-24 01:59:28 -05:00
Chris Tate 584d3b4338 Move the honesty-ladder exemplar to writeContinue — cork graduated to a real dispatch 2026-07-24 00:35:32 -05:00
Chris Tate cb00ff773c Pin recursive-shape checked-cast failure paths in the dyncheck harness
- a lying cast three knots deep renders the full path ($.children[0].children[0].label)
2026-07-23 13:26:12 -05:00
Chris Tate 23d5918a53 init 2026-07-22 18:08:04 -05:00