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>
`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>
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>
After a fetch body's handle gained its web view, a read, cancel or pipe
started on the view left Response.bodyUsed false until the view's pull
reached the body, where Node's bodyUsed reads the stream's disturbed state
at once. bodyUsed now also answers the view's disturbed state, which the
web streams' bridge reports (ScrWebStreamBridge.disturbed).
A program that names none of the web stream classes linked them only for
member accesses the checker resolves to ReadableStream, so body.tee() or
for await over a body inside a function whose parameters have no type
threw a TypeError. The same member names on an untyped receiver, and
for await over an untyped value, now link the classes in a program that
links the fetch unit.
Adds corpus program fetch-body-web-stream-helpers.mjs, with its corpus
cost and preflight/order baseline, and bodyUsed cases to
fetch-body-web-stream-linked.mjs.
In JavaScript, a Response or Request body answered getReader, cancel and
locked, and every other ReadableStream member was undefined: tee,
pipeTo, pipeThrough, values and for await threw TypeErrors where Node's
body is a ReadableStream.
In programs that link the web stream classes, the body's handle now
stands for a ReadableStream once any other member is used
(sf_stream_web_view): a stream with a zero high-water mark whose native
source reads the body (scr_webstream_readable_over). From then on every
member goes to it, the class's method values and getters run on it
through the alias's view (scr_dyn_handle_class_receiver), and text(),
clone(), new Response(body) and fetch uploads read the body through it,
so its lock and bodyUsed follow it as in Node. A body locked when the
view is made keeps the view locked until the lock is released. Once
only the view's source holds the body, the body lets the view go.
The compiler links the classes into a program whose JavaScript reads a
ReadableStream member that the fetch profile lists as unsupported by the
native stream, or iterates a ReadableStream with for await, and that
names none of the classes: %main calls webstream.install first.
Programs without web streams build to the same binaries as before.
Adds corpus programs fetch-body-web-stream-linked.mjs,
fetch-body-web-stream-unlinked.mjs and fetch-body-web-stream-network.mjs,
with their corpus costs and preflight/order baselines.
A JavaScript function that never refers to `this` no longer reads the
call receiver at entry. The coverage report counted that read as the
statement's dynamic operation, so these fixtures now report fewer
statements in the dynamic tier: in js-gap/gap.js, the JSDoc-typed
`typed` and `untyped` were both dynamic only through the read, and
`untyped` is now dynamic through its arithmetic alone.
A `for (k in o)` or `for (k of a)` whose head is an expression stores into
it on every iteration. The strict-mode check of constructor and method
bodies now treats such a head like the target of `=`: a property store
not made through `this`, a store into a read-only global, and a store into
an undeclared name keep the constructor function, since sloppy mode drops
or completes those stores where a class body throws.
The static-objects-order fixture gains loops.js, whose methods store from
a for-in head into an undeclared name and from a for-of head into a
property of a string.
An element write of a `T | undefined` binding holds a computed index in a
temporary ahead of its two stores, which then read the receiver after the
index. When the index can rebind the receiver (`a[(a = b, 0)] = u`), the
write now keeps reading the receiver first, so it stores into the array
the receiver held before the index ran.
The key conversion of a union index (a sequence that declares a temporary
and may read the constant "undefined" key) no longer counts as running
other code, so such an index is still held.
A union local that is copied to another local (`const t = pp[p0]; p0 = t`)
was a whole-value use, so both locals held heap boxes: every number[]
read held in such a local allocated a `%ScrUnion`, and every copy
retained and released it.
Call lifetimes now record statement copies between union locals
(copiedLocals) instead of rejecting the source. Local union storage
takes a copied local when every local it is copied to also has private
storage, so the copy snapshots the tag and bits and the source's box
stays private. Copies in expression position and copies of parameters
keep the heap path.
An element assignment of a `T | undefined` binding with a computed index
holds the index in one temporary ahead of its two stores, so the key
conversion of a union index declares its temporary once.
g05-zlib starts a zlib.gzip from a 0 ms island timer and arms a 2 ms
timer on the native side. The 0 ms timer's callback runs before the 2 ms
timer is due, so the gzip job races that timer: if the job completes on
libuv's thread pool before the timer's turn, its callback runs first.
Node orders the two the other way only because its first gzip usually
takes longer than the wait. With zlib already loaded and the thread pool
started, Node prints i-zlib first in about a quarter of runs. On fast
macOS hosts the native runs took the other order.
Mark it `// @order: race`, as g01 (fs), g03 (child process) and g04
(dns) already are, so its native runs must produce the lines of one of
Node's outputs in any order, and name thread-pool work among the races
the harness describes.
text(), json(), bytes() and the other body readers of a Response or
Request whose body is a ReadableStream asked the stream's source for more
chunks after draining its queue only when the stream had a zero
high-water mark. When the queue had filled before the read began (a timer
ran after the Response was made), the reader took the queued chunk and
then waited for a close that no pull would bring, and the read never
settled. The reader now pulls whenever it empties the queue, so it reads
to the end, as Node does. This applies to streams from ReadableStream.from,
from an underlying source, and to JavaScript ReadableStreams.
Adds corpus programs fetch-body-queued-before-read.ts and
fetch-body-queued-before-read-untyped.mjs, with their corpus costs and
preflight/order baselines.
scr_unwind_raise walks the stack with plans cached per return address.
The first raise through a return address finds the function's FDE
through the .eh_frame_hdr of the object that contains it, runs the CFI
up to the call, and finds the call site's landing pad in the LSDA; the
plan records the frame's CFA rule, where its callee-saved registers are
saved, and the landing pad. Later raises look the plans up, restore the
registers, and jump to the first landing pad. Plans are inserted under
a lock and read without one.
Landing pads continue the unwind with scr_unwind_resume instead of
`resume`, so the walk goes on from their frame. A frame whose CFI,
pointer encodings or personality the plans do not express, or a return
address without an FDE, leaves the unwind to libgcc's forced unwind
from the frame the walk started at, as before. Other Linux targets keep
libgcc's unwinder; their scr_unwind_resume calls _Unwind_Resume.
bench:runtime gains layered-errors, a workload whose errors are thrown
below small functions that hold references and caught per entry in a
loop, and the corpus gains unwind-landing-pad-chains, which pins landing
pads that continue the unwind, including in frames LLVM inlines into a
caller whose handler takes the exception.
The island host replaced process.on, process.addListener and
process.prependListener with separate wrappers, and process.emit with its
own, as own enumerable properties of process. Island code could see the
difference: Object.keys(process) listed the four, process.on was no
longer process.addListener, prependListener was named addListener, and
each was an own property where Node inherits it.
One wrapper now serves on and addListener, and prependListener and emit
keep their names. They are defined on process's own prototype, which
Node gives process below EventEmitter.prototype, with
EventEmitter.prototype's attributes. process has no own property for
them, so Object.keys, Object.hasOwn and for...in see what they see in
Node. for...in lists a prototype's keys before its parent's, so the
keys EventEmitter.prototype lists before the wrapped ones (_events,
_eventsCount, _maxListeners, setMaxListeners, getMaxListeners) are
defined there too, with the same values, in their order.
6891 compares the identities, names, lengths, own-ness, Object.keys and
for...in over process against Node.
The corpus case passed `new Swapper(tricky)` straight to `+`, which the
argument check already treated as effectful, so it never distinguished
the two lowerings. Bind the swapper to a local: an identifier is what
the old check called inert, and the program now prints "replaced
replaced" without the fix and matches Node with it. The class also used
a parameter property, which Node's strip-only mode rejects.
Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
A checked conversion can answer a record view: an object of the target
record type whose fields live in the record it views, at that record's
offsets (record-views.ts). An integer clone read the fields of a borrowed
record at its own shape's offsets, so for a view it read the view's unused
slots and returned wrong results.
Record shapes that may be views are no longer references a clone reads, so
functions that read them keep the ordinary body, which reads a view's fields
through the original record. The emitter now plans integer clones after
record views.
integer-recursion-records.ts gains a recursive sum over a view.
`+x`, `-x` and `~x` convert their operand with ToNumber, which calls a
user-defined `valueOf`; treating such an argument as inert let the
lowering read the island method after the conversion ran, so the call
reached a method the conversion had replaced. Only a numeric literal
operand is inert now, and the corpus program's `valueOf` replaces the
method it is called through.
A program that uses web streams and Node islands selected one
`scr_async.c` variant by condition count, so the tie between the
one-condition variants left either SCR_NATIVE_REACTIONS or
SCR_NODE_ISLANDS undefined and the hosted-loop symbols unresolved. Add
the combined variant, with a matrix test that pins the selection for
both features together and for each alone.
Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
A module reached by an already-rejected literal import() is `covered`, but a
direct import() of that module still needs its own rejection entry: its graph
rejects when that import runs, so it must not join the compiled graph merely
because another rejected import also reaches it. Without the entry the module
is compiled and lowering fails on the unloaded binding instead of the
import() rejecting at run time as Node does.
Walk each direct root independently and extend the unloaded-dynamic-import
fixture with a direct import() of the rejected graph's dependency.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The checker's union of `{}` (or `object`, `Object`) with a concrete result
is the top type itself, but mapDistributedConditional mapped the parts
separately and interned a union with a `dyn` arm. Emitting the member then
failed validation with SC9001 ("union ...: arm 0 is dyn"). Return the
checked-dynamic type as soon as a part maps to it, like the existing
`unknown`/`any` absorption.
Cover it in the units corpus: an `Empty<K>` member holds its mapping in a
local so the emitted union is actually exercised.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An island signal registration is now tagged with the raw listener it
installed, so a once registration and an on registration of one function
dispatch through their own listeners instead of both resolving to the plain
function, and the once registration no longer fires indefinitely. The
removal handler forgets the entry whose raw listener is no longer on
process, so a once wrapper that removes itself leaves its runtime
registration behind no longer; duplicate registrations of one function
share a raw listener, and the most recent of those is removed.
refresh() of a timer armed before Node loads re-arms at now + the delay the
timer was created with. Its Node handle was created with the remaining delay
at handoff and a due timer had no handle at all, so the handle is replaced
rather than refreshed.
The signal program registers one function once and on; a new timer program
refreshes a timer most of whose delay passed before Node loaded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>