Commit Graph
4679 Commits
Author SHA1 Message Date
Malte UblandClaude Opus 5.5 41364d9c08 Spread wider-than-runtime iterables in array literals and push/unshift
`[...maybe]` over a source the checker typed wider than the iterable it
holds at run time (`T | undefined` from an indexed read under
indexed-access strictness, an optional local) and `push(...value)` over
an untyped or widened source were SC1090 refusals. They now drain through
the checked iterator walk, validating each element into the target slot,
and throw Node's not-iterable TypeError for null/undefined.

`widenedIterableType` recognises a union of exactly one iterable arm plus
unit arms; `checkedIteratorPack` is exported so the array-method spread
path reuses the same walk.

Corpus 8504 covers `[...digit]` from a `string | undefined`, a `[...any]`
array literal, a `push(...any)`, and
`push(...(provider.x ?? []))`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:08 -07:00
Malte UblandClaude Opus 5.5 d9e8655eeb Lower conditional spread shapes beyond one empty arm
A conditional spread whose arms are both populated, nested, or collide
with an earlier contributor (or a required field, or an index-signature
slot) was an SC1090 refusal. Those shapes now retry the whole literal as
a checked-dynamic own-key copy validated into the target record: the
one-entry-per-name desugar cannot express which arm runs at runtime.

The syntax fence defers so the checked-dynamic build gets the first
chance, and fires only where that declines. Inside a checked-dynamic
literal a conditional source lowers arm by arm as the dyn value whose
keys the copy reads, so a fresh arm literal is not reshaped against the
enclosing record's contextual type.

Corpus 8503 covers both arms populated with different shapes, a nested
ternary, a conditional over an earlier contributor, an index-signature
base with a conditional, and a `Headers | undefined` spread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:07 -07:00
Malte UblandClaude Opus 5.5 f9f6d4d23a Copy untyped and index-signature spread sources through a checked dynamic object
`{ ...item }` over `unknown`, `any`, or a `Record<string, unknown>` source
was an SC1090 refusal: the record desugar cannot enumerate keys that are
runtime state. These sources now build a checked-dynamic object and
validate it into the target record, copying own enumerable keys in Node's
insertion order. Keys the target does not declare drop, matching the
existing spread stance.

Corpus 8502 covers a `Record<string, unknown>` into a typed record, an
`unknown` clone, and a merge with an explicit later field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:06 -07:00
Malte UblandClaude Opus 5.5 61ab4fbc25 Lower an object spread of a maybe-absent union source
`{ ...maybeRecord }` where the source is `T | undefined` was an SC1090
refusal when the target field had no earlier contributor. Node treats an
absent source as a no-op, so a present source now copies field by field
through the checked extraction (`x!`): the record arm copies the value,
and the absent unit arm throws the catchable TypeError instead of leaving
a required slot unset. An optional target field keeps its no-copy arm.

Corpus 8501 pins the present-source copy and the optional destination; a
new errors-harness case covers both arms of the required-field path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:06 -07:00
Malte UblandClaude Opus 5.5 d9e61976b0 Read and write union fields whose arms store different types
A union with one arm declaring `details: T | undefined` and another
declaring `details: unknown` (pi's ToolResultEvent, the runner.ts:1208
site) has no single static type every arm converts into, so reads and
writes of that field were refused with "assignment to non-variables" /
"reading 'details' on a union-typed value". TypeScript accepts the write
because the writable type of a property on a union is the UNION of the
arms' property types, so the value is legal there.

Reads now project every arm through the checked-dynamic representation:
boxing a typed value into it cannot fail, so isWidening accepts dynFrom
and the dynamic type becomes the shared read type when an arm declares
it. Writes lower the value once at its own type and let each arm convert
it — a free widening where the arm's slot holds it exactly, a checked
conversion where the slot is narrower, so a value that does not fit the
selected arm throws the catchable TypeError of the dyn->static edge where
JS would store it (the divergence a dyn value written into a typed slot
already documents).

Both paths are reached only where lowering refused before, so programs
that compile today keep their IR unchanged.

Corpus: union-mixed-field-write.ts covers the pi shape (an `unknown` arm
beside specific arms), a payload from each arm's own view, assignment in
expression position, an `undefined` payload, and a receiver that is
already the union.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:05 -07:00
Malte UblandClaude Opus 5.5 5bcabf9dd5 Lower a finite-literal keyed read over a subset of the declared fields
`filter[resourceType]` where the receiver is a record with several
optional members and the key type is a finite union of literal keys
(`"extensions" | "skills" | ...`) refused with SC1090 whenever the
checker's answer type did not cover every declared field. That is the
common settings-record shape: the key domain names four of the members
and the rest have unrelated types.

The key type is a finite domain, so the read now dispatches over exactly
the fields it can name, comparing the key against each canonical
property spelling. The answer is the join of the reachable fields' types
— precisely the checker's answer type — and a key outside the domain
reads undefined, as a missing property does. Nothing is emitted for a
field the key type cannot name, so no widening to the full record leaks
into downstream lowering.

The dispatch is gated on the shape having no index signature, on the
access not being a call (`o[k]()` keeps its fence for inherited
methods), on the reachable fields being a proper subset, and on the join
holding an undefined arm so a smuggled key has an answer. A key domain
that names every field keeps the single keyed-read helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:03 -07:00
Malte UblandClaude Opus 5.5 91deb8b5a0 Lower a tuple read with a runtime index as a positional dispatch
`t[i]` on a fixed-shape tuple where `i` is not a literal refused with
SC1090. A tuple is a positional record, so the read now dispatches over
its arity: the element at an integer index in [0, arity), and undefined
for anything else — a fraction, a negative, NaN, Infinity, or one past
the end, exactly Node's array-like read.

The answer type is the join of the element types plus undefined, the
same policy the array index read uses, so a consumer observes a miss
like any other optional element read: `t[i]!` narrows through the
existing checked-narrow path and arithmetic on a miss is NaN. The
receiver and the index are hoisted into hidden locals when they cannot
be repeated (receiver first, since JavaScript evaluates it before the
key). The chain stays inline because the arity is a compile-time
constant, unlike the lifted array helper whose length is not.

Writes keep their fence: a stored position must be a literal index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:03 -07:00
Malte UblandClaude Opus 5.5 5d8d6b35a1 Address the child-lifecycle, stream-alias and fs write-options reviews
- child_process: the exit/close/error dispatch loops read the live
  listener registry in their loop condition, so a listener that called
  off/removeListener on a sibling cut the in-flight emit short. Node
  copies the listener array before it emits; detach the registry
  (scr_child_detach_exit/_close/_err), deliver that snapshot, release
  each closure, and free the array in both platform arms.
- process: a first-class alias of stdout/stderr (`const out =
  process.stdout`) keeps the stream's type but read `writableLength`
  through the keyed dynamic get, which answered undefined. Fold that
  read to the same synchronous-write 0 the direct lowering answers,
  leaving the runtime untouched.
- fs: a `const` write-options record was inlined at the write site even
  when it was mutable, so `OPTS.mode = 0o644` before the call diverged
  from Node, which reads the record when the call runs. Accept only a
  record whose properties are all readonly (`as const`, `Readonly<>`);
  a mutable one keeps the SC2020 fence.
- Refresh the node-types-fenced snapshot: `path.win32` sub-namespaces
  lower now, so the PlatformPath.sep fence is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:15:37 -07:00
Malte UblandClaude Opus 5.5 a0849484ab Read an expression default through a module namespace getter
`export default <expr>` and the anonymous default function/class have no
named binding for the dynamic namespace getter to read, so a namespace
used as a dynamic value fenced its `default` property with SC1090. They
do have storage: the module registers a `default` global keyed by its
default export symbol, which is what a default import reads. The getter
keeps that symbol per export and answers the snapshot coerced to `dyn`,
so the boxed namespace's `default` matches Node; a missing registration
still fences SC1090 and a value with no checked-dynamic form still
fences SC1101.

Corpus: 8412-module-namespace-untyped gains an `export default 42`
module and reads its boxed and typed `default`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:09:45 -07:00
Malte Ubl 5c79a40570 Merge branch 'ni/i10-js-island-requires' into ni/i11-cross-heap-gc 2026-10-28 09:06:47 -07:00
Malte Ubl 930e447a5b Merge branch 'ni/i9-recorded-deferral' into ni/i10-js-island-requires 2026-10-28 09:06:43 -07:00
Malte Ubl 88678fa9ac Merge branch 'ni/i8-exit' into ni/i9-recorded-deferral 2026-10-28 09:06:01 -07:00
Malte Ubl a7d8e3ed98 Merge branch 'ni/i7-routing' into ni/i8-exit
# Conflicts:
#	tests/harness/npm-suite.ts
2026-10-28 09:05:56 -07:00
Malte UblandClaude Opus 5.5 10f974aab9 Merge ni/i6-subclasses into ni/i7-routing
Resolve tests/harness/npm.test.ts by keeping main's npm-suite split and
porting this branch's island KNOWN entry and stderr frame normalisation
into tests/harness/npm-suite.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:59:57 -07:00
Malte UblandClaude Opus 5.5 3f91c43a44 Merge origin/main into fix/pi-wp6b-stdlib-tail
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:51:03 -07:00
Malte UblandClaude Opus 5.5 fc407d0751 Lower Array.prototype.join over a dynamic separator
`.join(sep)` over a checked-dynamic separator fenced with SC2020. JS
substitutes the default comma only for the undefined VALUE; every other
value goes through ToString, which prints "null" for null, the digits for
a number or boolean, and throws the catchable TypeError for a symbol.
Evaluate the separator once into a hidden local, test it against the
undefined dyn kind, and answer either the comma literal or
dyn.toStringCoerce of the value.

Unlike the statically typed arms (string, and a union with an undefined
arm), a dynamic separator cannot be resolved statically, so this is the
runtime test the dynamic tier exists for.

Fixes the 2 pi sites (@anthropic-ai/sdk's node.mjs, pi's
interactive-mode.ts).

Evidence: tests/corpus/array-join-dynamic-separator.ts, run under Node and
natively in the plain and sanitized differential lanes, plus its
order-parity baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:50:54 -07:00
Malte Ubl d1566d2746 Merge branch 'ni/i10-js-island-requires' into ni/i11-cross-heap-gc 2026-10-28 08:48:18 -07:00
Malte Ubl a476e02b3a Merge branch 'ni/i9-recorded-deferral' into ni/i10-js-island-requires 2026-10-28 08:48:10 -07:00
Malte Ubl bda1292e5c Merge branch 'ni/i8-exit' into ni/i9-recorded-deferral 2026-10-28 08:47:58 -07:00
Malte Ubl 8a918e7aff Merge branch 'ni/i7-routing' into ni/i8-exit 2026-10-28 08:47:47 -07:00
Malte UblandClaude Opus 5.5 0fb9e25464 Merge ni/i6-subclasses into ni/i7-routing
Resolve tests/harness/npm.test.ts by keeping main's npm-suite split and
porting this branch's island KNOWN entry and stderr frame normalisation
into tests/harness/npm-suite.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:47:41 -07:00
Malte UblandClaude Opus 5.5 3b86b83c45 Merge origin/main into ni/i6-subclasses
Resolve tests/harness/npm.test.ts by taking main's npm-suite split and
porting the island path normalisation and the two island KNOWN entries
into tests/harness/npm-suite.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:46:53 -07:00
Malte Ubl e5f57e2093 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-28 08:43:13 -07:00
Malte Ubl 29a31446d3 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-28 08:43:06 -07:00
Malte UblandClaude Opus 5.5 1d91abadfa Lower Object.getOwnPropertyNames over fixed records
Object.getOwnPropertyNames over a statically typed record fenced with
SC2020 even though Object.keys over the same record lowers. Every own
property a scriptc record carries is an enumerable declared field (or an
index signature's overflow), so both surfaces answer the same key walk:
the interned %obj.keys helper for closed shapes, the declared-then-
overflow walk for index signatures, and the live checked view for a
record that kept undeclared properties of its checked source.

Fixes ~26 pi sites in typebox's guard walk (and the AST validators built
on it) plus gaxios/js-yaml reflection.

Keep the fences where the walk is not the answer: tuples and arrays (their
own names include "length" and the indices), accessor-carrying shapes
(the static walk cannot list accessor names), performance entries, X509,
Maps and class instances.

Evidence: tests/corpus/object-get-own-property-names-records.ts, run under
Node and natively in the plain and sanitized differential lanes, plus its
order-parity baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:38:16 -07:00
Chris Tate dea433dacd Call the receiver copy for direct calls whose result is discarded
A direct call to an untyped method that returns `this` on every path,
used as a statement (`builder.add(part);`), boxed its receiver into a
capsule that nothing read. Such a call now runs the method's copy that
returns nothing, as the class dispatch helpers already do.
2026-10-28 10:21:05 -05:00
Chris Tate a484390e07 Merge main 2026-10-28 09:50:05 -05:00
Chris Tate 3a476d5f32 Fix the X509 recursive-type crash and lower the refused array species forms (#2467) 2026-10-28 09:45:10 -05:00
Chris Tate 1cf59d42e4 Lower Response.clone and Request.clone on typed values
Response.clone() and Request.clone() on a value typed Response or Request
were refused: in TypeScript the fallback declarations had no clone, and
the fetch profile listed both as unsupported, so the call compiled to an
SC2020 refusal, also in JavaScript where the checker types the value. The
checked-dynamic handles already implement them (the body tees, and the
clone gets a copy of the headers with the same guard), so typed calls now
dispatch to those implementations, as text() and json() do.

The declarations gain clone(), and the fetch profile lists both as static
methods with the new static-clone fetch fixture as their differential
evidence. The surface manifest is regenerated, and the surface-manifest
probes that kept clone fenced now fence blob() and formData().

Adds the static-clone fixture and corpus programs fetch-clone.ts and
fetch-clone-untyped.mjs, with their corpus costs and preflight/order
baselines.
2026-10-28 09:33:31 -05:00
Chris Tate 6e17225ffc Keep module-level lets unboxed in the dynamic tier and return receivers from dispatch helpers (#2471) 2026-10-28 09:15:26 -05:00
Chris Tate 25bb2a43ef Lower optional-chain promise methods, promise-or-unit Promise.resolve, and Array.from(typedArray, mapper) (#2465) 2026-10-28 09:05:56 -05:00
Chris Tate ec79f2aca0 Destructure narrowed and joined union sources, await promise-armed unions, support writeFileSync(fd) (#2466) 2026-10-28 08:57:38 -05:00
Chris Tate f041da4b10 Drop the computed Array.isArray refusal from the stdlib fence snapshot
`Array.isArray([1])` folds after evaluating its argument, so the stdlib
fence battery no longer reports SC1090 for it. The snapshot drops that
diagnostic and the statement notes that it lowers.
2026-10-28 08:56:20 -05:00
Chris Tate 2c7b67c5aa Keep module-level lets unboxed in the dynamic tier and return receivers from dispatch helpers
- The jsval pass also promotes dyn module globals to jsval globals when
  only dynamic-tier functions read and write them and the boxing their
  writes save outweighs the boxing their reads add, the measure locals
  use. `checksum += f(x)` in a top-level loop no longer allocates a
  number cell per update.
- A class dispatch helper that calls a method returning `this` on every
  path calls a copy of the method that returns nothing and answers its
  own receiver, the capsule of the same instance at its exact class, so
  the call boxes nothing.
2026-10-28 08:35:25 -05:00
Malte UblandClaude Opus 5.5 08ec7fd291 Merge ni/i10-js-island-requires into ni/i11-cross-heap-gc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:43:05 -07:00
Malte UblandClaude Opus 5.5 0fcc4d15a4 Merge ni/i9-recorded-deferral into ni/i10-js-island-requires
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:43:00 -07:00
Malte UblandClaude Opus 5.5 433dc179ef Merge ni/i8-exit into ni/i9-recorded-deferral
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:42:55 -07:00
Malte UblandClaude Opus 5.5 6ec5b3ff58 Merge ni/i7-routing into ni/i8-exit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:42:43 -07:00
Malte UblandClaude Opus 5.5 9edf1d3aa3 Merge ni/i6-subclasses into ni/i7-routing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:42:18 -07:00
Malte UblandClaude Opus 5.5 86056af745 Merge ni/i5-shared-modules into ni/i6-subclasses
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:41:14 -07:00
Malte UblandClaude Opus 5.5 4d7f64c461 path/stdio: sub-namespace bindings and stdout writableLength
`import { win32 } from "node:path"` binds the win32 sub-namespace itself,
so `win32.normalize(...)` must key the win32 table on every platform —
Node answers each namespace its own platform's semantics anywhere.
`builtinNamespaceModuleOf` now resolves a named `win32`/`posix` binding
of `path`, `path/posix` or `path/win32` to that sub-namespace module
instead of falling through to a dynamic receiver.

`process.stdout.writableLength` / `stderr.writableLength` are now
accepted and answer 0: the compiled binary writes fd 1/2 synchronously,
so nothing is queued when the property is read, and `write()` answers
true for the same reason. Backpressure probes stay correct. The ambient
`tty.WriteStream` declares the property.

Corpus 8414 pins the value and its type before and after writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:38:16 -07:00
Malte UblandClaude Opus 5.5 882c87d425 process/child: void slots, listener removal by identity, signalCode
Three shapes from pi's process/child helpers:

1. A cleanup arrow such as `() => process.off(signal, handler)` sits in
   @types/node's `Process`-returning slot. The lambda's ABI was decided
   from its inferred return type before the contextual void slot was
   consulted, so the arrow promised a `Process` value the lowering cannot
   produce and the registration fenced. `lambdaSignature` now takes the
   contextual void answer first; the adoption block further down already
   computes it, so every program that compiles today keeps its IR.
2. `child.off`/`removeListener` were unimplemented. They join the
   `child.on` family, are limited to the removable literals `spawn`,
   `exit`, `close` and `error`, and require the listener value that was
   registered — removal is by closure identity, scanning the registry
   from the end as Node does. New C runtime entry points
   `scr_child_off_spawn/_exit/_close/_error` release the entry and shift
   the tail.
3. `child.signalCode` reads `string | null` from the recorded exit
   signal, with `scr_child_has_signal_code`/`scr_child_signal_code`
   behind the same typed-union wrap as `spawnRes.signal`; the IR
   validator pins the `string | null` result.

The process listener registration paths (stdin events, IPC message and
disconnect, uncaughtException and uncaughtExceptionMonitor,
unhandledRejection, warning, signals, exit) now accept any discarded
expression, matching `child.on` — `resultIsDiscarded` rather than a bare
expression statement — so `handlers.push(() => process.off(…))` compiles.

Evidence: corpus 8412 (removal through `off` and `removeListener` for
all four events, `signalCode` while running, after a normal exit, after
a fatal signal and after a failed spawn, idempotent double removal) and
the `node-types` fixture plus project-config test that pins pi's exact
`() => process.off(...)` cleanup closure. Ambient declarations gain the
`signalCode` property and the four `off`/`removeListener` overloads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:38:15 -07:00
Malte UblandClaude Opus 5.5 c3232c265d fs: accept a const options binding in three-argument writeFileSync
The three-argument form required its options argument to be an inline
object literal, so a shared `const SCRATCH = { encoding: "utf-8",
mode: 0o600 } as const` binding — the shape pi's temp-file helpers use —
fell through to SC2020 even though every option was a literal. Resolve a
`const` initializer through `constInitializerOf`, strip casts and
parentheses, and admit a literal whose every option member is a
literal-valued property, which is exactly what the inline form accepted.

Corpus 8413 pins the creation mode and that an existing file keeps its
mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:38:14 -07:00
Malte UblandClaude Opus 5.5 86299300f6 Fold Array.isArray over a computed argument after evaluating it
`Array.isArray(v)` is a compile-time constant when `v` has a static type:
an `T[]` or a tuple is an array, every other kind is not. The fold only
accepted a bare variable, a record field or a field read, because only
those could be dropped without losing effects, so `Array.isArray(make())`
and `Array.isArray([1, 2])` reached an SC1090 refusal.

A computed argument now folds the same way after being evaluated for its
effects: the value goes into a sequence whose discarded statement is the
argument. Unions keep answering by their runtime tag, and `unknown`
values keep their real runtime test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:38:08 -07:00
Malte UblandClaude Opus 5.5 c7c214532e Lower species forms through a narrowing wrapper and a union flatMap
The species lowering for a callback method rebuilt its `%arr.<method>`
call by shape — the result had to be a call taking the receiver as its
first argument — so two forms the method lowering does produce fell to
SC1090 whenever the program declared a class that extends Array:

- a filter whose callback narrows: the filtered array comes back through
  the checked-narrowing wrapper, `call(%arr.narrow, [call(%arr.filter,
  …)])`, and the species path saw a call whose first argument is not the
  receiver;
- a flatMap whose callback returns a union with an array arm, which
  lowers to `%arr.flatMap.<n>` — a helper boundedArrayHofHelper rejected
  because arrHofInfo, which holds the pipeline-fusable helpers, has no
  entry for it.

The call is now found by walking the expression for the `%arr.` helper
call over the receiver, whatever wraps it, and boundedArrayHofHelper
accepts any `%arr.` helper (forEach excepted, it makes no array), so the
union flatMap earns a bounded variant. Hidden locals are still declared
in the old order, so a program that compiled before gets byte-identical
IR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:38:07 -07:00
Malte UblandClaude Opus 5.5 f09e02255d Guard the X509 dynamic-value fence against recursive types
refuseX509DynValues walks a converted value's type looking for an
X509Certificate record, and it memoized only records (by shape id) and
unions (by union id). A type that reaches itself through an array, set,
map or promise has no id to memoize by, so `type Rec = string | Rec[]`
in a program that also holds a certificate recursed until the compiler
ran out of stack — a crash, not a diagnostic.

The walk now keeps an identity-based visited set over those kinds: a type
met again while it is still being visited answers false, the least fixed
point of reachability, since nothing outside the cycle makes a
certificate reachable. Unions keep their memo, which now also covers a
union cycle that names one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:38:06 -07:00
Malte UblandClaude Opus 5.5 071be693d9 Support writeFileSync(fd, data)
Node's writeFileSync accepts an open file descriptor where it accepts a
path: it writes the data at the descriptor's current offset and leaves the
descriptor open, since closeSync owns its lifetime. The static fs surface
declared only the path forms, so `fs.writeFileSync(fd, text)` failed type
mapping — the compiler was missing a documented overload, not a
conversion.

Declare the (fd, data) and (fd, data, "utf8" | "utf-8") forms in the
node-fallback typings, route the call by the first argument's static f64
type to a new fs.writeFileSyncFd libfn, and implement it as
scr_fs_write_file_fd: a write loop over the whole string through the
descriptor-validating scr_fs_write_bytes, resuming short writes, with the
descriptor operation's errno shape on failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:38:00 -07:00
Malte UblandClaude Opus 5.5 99ebf7501d Await a union whose promise arm settles a union
`Promise<A | B> | undefined` awaits as Awaited of the operand, which the
maybe-promise lowering already computes: it awaits the narrowed promise
arm and coerces its settled union, and the non-promise arms take JS's one
microtask hop. The await lowering refused any union inner instead, so the
promise-or-unit shape now goes through that same path; a union whose
promise arm has no combined result keeps the SC1090 fence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:37:59 -07:00
Malte UblandClaude Opus 5.5 a0a6910e4e Destructure union sources by narrowing first and joining arm reads
An object pattern over a union whose arms type a field differently, or do
not declare it at all, was refused with SC1031, even though the pattern
element's own type is the checker's answer: the join of the arms' declared
types, with undefined for an arm that omits the key, or the narrowed arm
when a discriminant test or an `in` guard refined the source at the
pattern's site.

Keep the unionDisc fast path wherever every arm declares the field with
one type, so programs that compile today emit identical IR. Otherwise read
through the existing keyed join (unionKeyGet): each arm answers its
declared type and a record arm that does not declare the key answers
undefined, which is exactly the desugar's own property read. Only
destructuring passes the new missingFieldAsUndefined flag, so dotted reads
keep their current refusals.

Narrow first when the un-narrowed pattern does not fit: bind the
checker-narrowed view of the source to a hidden local — a trusting
arm extraction for one arm, a sub-union re-tag whose excluded arms trap,
and the stored layout view for an intersection refinement — and lower the
pattern over that view.

A rest element over a union source packs each arm's unconsumed fields into
a fresh record of the checker's arm-wise rest type, selected by the
runtime tag; CopyDataProperties makes a fresh object too, so nothing
aliases the source. Shapes that do not spell the rest type still fence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:37:58 -07:00
Malte UblandClaude Opus 5.5 b8bb5bfdb0 Record the new corpus programs' CI costs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:37:52 -07:00