Commit Graph
4404 Commits
Author SHA1 Message Date
Chris Tate 2473207af2 Name the emitter corpus programs instead of numbering them
Rename 8640-emitter-arguments-listeners.cjs and
8641-emitter-leak-warning-exit.cjs to emitter-arguments-listeners.cjs and
emitter-leak-warning-exit.cjs, with their cost files and ts7 baselines, as
AGENTS.md asks for new corpus programs.
2026-10-29 07:18:44 -05:00
Chris Tate b421cf6a38 Pass every argument to emitter listeners that read arguments, and deliver the leak warning at exit 2026-10-29 07:12:59 -05:00
Chris Tate b260a13dda Make fewer syscalls per fs thread-pool completion and pool-thread write (#2519) 2026-10-29 00:20:58 -05:00
Chris Tate 50aa42883f Cache the pool-thread signal check per OS thread
scr_fs_write_once cached whether the calling thread blocks SIGPIPE and
SIGXFSZ in an SCR_TL variable. SCR_TL is empty in builds without thread
instances, so a pool thread's answer applied to the main thread too, and
a later writeSync to a closed pipe ended the process with SIGPIPE
instead of throwing EPIPE. The cache is _Thread_local now. The
fs-write-sync fixture covers a write stream followed by writeSync on a
closed stdout.
2026-10-29 00:18:11 -05:00
Chris Tate 4cfd4f3320 Records: nested-arm filter narrowing, async object-method returns, untyped Map layouts (#2509) 2026-10-28 23:56:59 -05:00
Chris Tate c0ef82865d Run short constant-count loops together when their inner exits saturate (#2493) 2026-10-28 23:49:18 -05:00
Chris Tate 5f27c4d233 Exit cleanly with Node loaded and keep nested entries on Node's stack (NI-7c, NI-3b) (#1905) 2026-10-28 23:43:21 -05:00
Chris Tate f909bdf6c8 Make fewer syscalls per fs thread-pool completion and pool-thread write
- A worker writes the completion wake byte only when the pipe has none.
  The loop reads it, with the queue lock held, when it takes the last
  waiting completion. The pipe holds a byte exactly while completions
  wait, so a burst of completions costs one write and one read instead of
  one write per completion and a read loop per dispatch.
- The fs pool's threads keep SIGPIPE and SIGXFSZ blocked for their
  lifetime. scr_fs_write_once checks once per thread whether both are
  blocked; on such a thread a write makes no signal-mask calls, and a
  write that fails with EPIPE or EFBIG consumes the signal it raised.
  Other threads keep the per-write guard.

fs-write-sync.test.ts covers EPIPE and EFBIG from write streams, whose
writes run on the pool's threads, and matches Node's errors.
2026-10-28 23:34:34 -05:00
Chris Tate 0faade48ae Test that receivers are evaluated once and before effectful keys (#2514) 2026-10-28 23:25:09 -05:00
Chris Tate 5f7a5dc883 Keep an async method's promise in non-promise slots; convert filtered arms beside an exact one
- An async object-literal method whose contextual slot returns a non-promise
  (`() => void`) keeps its own promise return, and one whose slot returns a
  union takes only the union's promise arm (`Promise<T> | void`): adopting
  the whole return type gave the method a non-promise type, refused with
  SC2001.
- A type predicate whose record target is itself an arm of the element
  union keeps that arm's elements as they are and converts the other arms
  with its fields, instead of refusing the filter with SC1090.
2026-10-28 23:10:05 -05:00
Chris Tate a64b5b0820 Keep saturating loops whose copies cannot share the inner loop
The jam of a short constant-count loop now leaves the loop as it is when:

- the inner loop's body assigns the accumulator other than by
  `acc = acc | C`. An iteration that runs past its exit could clear bits
  after the accumulator was full.
- the inner loop's body assigns the inner counter. The copies share the
  counter, so one copy's write changed the iterations of the others.
- the inner loop's start or bound reads the outer counter or a local of the
  outer iteration. The copies share the inner loop's start and bound, which
  read neither the counter's value nor the copies' renamed locals.

The unit test rejects each shape, and the corpus program adds a function
for each that matches Node's output.
2026-10-28 23:01:16 -05:00
Chris Tate 686c34bd25 Store tty.isatty and process.cwd as function values (#2494) 2026-10-28 22:46:19 -05:00
Chris Tate 6f2a18d16c Test262 group B: generator method ABI, empty-pattern throws (#2505) 2026-10-28 22:41:17 -05:00
Chris Tate 3e7528ca86 pi wave 4: path values, union delete, NonNullable unions, BigInt stats, optional-param function values, live accessor literals, clearer stdlib-alias errors (#2497) 2026-10-28 22:33:16 -05:00
Chris Tate c3474cfa7a Test that receivers are evaluated once and before effectful keys
Adds the corpus program receiver-before-effectful-key.ts, with its corpus
cost and order baseline:
- a tuple read with a runtime index that rebinds the tuple or replaces the
  field holding it;
- a keyed read through a finite union of field names whose key rebinds the
  record or replaces the field holding it;
- an inherited-key `in` test on a computed null-prototype receiver, which
  runs the receiver once.

Each case reads the receiver as evaluated before the key, as in Node.
2026-10-28 22:30:01 -05:00
Chris Tate 91b5d267b9 Keep frozen checked records frozen, and give sloppy-mode this the global object (#2453) 2026-10-28 22:02:18 -05:00
Malte UblandClaude Opus 5.5 8e74d0518d Give an untyped empty Map the layout of the value it joins
`x ?? { errors: new Map() }` left the empty map typed Map<any, any>,
which spread through the checker into `new Map(result.errors)` and the
binding holding it, and every use failed with SC2020/SC1090. The
fallback literal now builds as x's record, `new Map(source)` over such
a join copies into the source's layout, an unannotated binding whose
checker type differs only by that residue takes its initializer's
layout, and Map methods on it convert into the receiver's layout. No
map is converted between layouts: each value keeps the one layout it
has.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 19:55:51 -07:00
Malte UblandClaude Opus 5.5 2acd199fbe Give an async object-literal method its contextual return type
An async method in an object literal with no return annotation and a
contextual signature inferred its own (narrower) literal type, while its
`return` literal was built at the contextually typed record: the return
statement then had to convert the wider record into the narrower literal,
which fails with SC2002. The method now takes its return type from the
contextual property's call signature, as the arrow form does. Sync
methods are unchanged; they already adopt their slot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 19:55:50 -07:00
Malte UblandClaude Opus 5.5 9eaa210e1f Narrow filtered union elements into a record with an arm's fields
A written type predicate whose target is no arm of the element union
but the same fields as one, with a nested union field narrowed
(Model<"a"> from the Model<Api> arm), was refused. The kept elements of
that arm now convert field by field, the nested union by its tag; any
other kept element still throws the predicate TypeError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 19:55:50 -07:00
Chris Tate 2ae08e4002 Route more packages and values through Node islands (NI-4c–4g) (#1904) 2026-10-28 21:53:40 -05:00
Malte UblandClaude 5.5 34c39e5443 Throw on empty patterns over checked-dynamic sources
An empty binding pattern still performs its one observable step:
RequireObjectCoercible for `{}` and GetIterator for `[]`. Over a statically
non-nullish source that step is invisible, which is why the empty-pattern
lowering emitted nothing; a checked-dynamic source can be null or undefined
at runtime, where V8 throws.

Emit the runtime null/undefined test for dyn sources in the empty-pattern
branch, and let a JS-file empty object pattern take the source-spelling
message dynDestrCheck already produces for non-empty ones.

Test262 (static-adapted-v6, --workers 8), affected chapters before -> after:
language/statements/class/dstr pass 1518 -> 1526, fail 424 -> 416;
language/expressions/class/dstr pass 1518 -> 1526, fail 424 -> 416. No
pass -> fail.

Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
2026-10-28 19:51:10 -07:00
Malte UblandClaude 5.5 460bd6fb38 Keep a generator method's generator ABI through the prototype-access widening
A class whose methods are reachable through `X.prototype` widens its method
signatures to checked values so a JS caller can hand anything across. The
widening replaced every non-void return with DYN, which erased a GENERATOR
method's generator type; the generator branch below then had no generator
channels to build its metadata from and refused the whole class (SC2001 in
`%init`, re-emitted as a runtime fence in a JS file). Every
`assert.sameValue(c.m, C.prototype.m)` variant of the campaign therefore
refused at runtime, 759 of them in the generator-method chapters alone.

A generator's return kind is already a checked-crossable ABI: dynFrom boxes a
generator by reference and dyn calls resume it through the dyn generator ops,
so only the arguments need the widening. Guard the return widening to
non-generator methods.

Test262, 1,110 affected variants, before/after on the candidate
(class/gen-method 52->76, class/async-gen-method 76->128,
class/scope-gen-meth 0->8); no pass->fail and no typed-path delta.

Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
2026-10-28 19:51:09 -07:00
Chris Tate e506d19cd6 Name destructured builtins, parenthesized functions and util.debuglog as Node does
Reading the name of a function stored in a const followed const aliases to
the definition, but stopped at destructuring: `const { cwd } = process`
gave the binding's name instead of Node's "wrappedCwd". A member
destructured from the process global or a builtin module's namespace now
gives the module table's name for it.

An anonymous function wrapped in parentheses (`const f = (function () {})`)
answered an empty name. As in Node, it takes the name of the binding or
property it initializes through the parentheses.

A builtin read as a property (`util.debuglog.name`) answered the name its
shared adapter was first created under, so after `util.debug` it answered
"debug". The name now comes from the property and the module table, and
util.debug's value is named "debuglog", as in Node.
2026-10-28 21:50:00 -05:00
Chris Tate 52e3dadc20 Match Node for BigIntStats dates, bigint option effects and path namespace spreads
- A BigIntStats Date member is built from its truncated millisecond member,
  as Node's getters do, where the Stats getters round the fractional one: a
  timestamp at least 0.5 ms past a millisecond no longer reads one
  millisecond later than Node.
- statSync, lstatSync and fstatSync evaluate a bigint option whose value can
  have an effect (`bigint: (count++, true)`), in member order after the path
  and before the stat; an exception it throws is the call's.
- An object spread of a node:path namespace in JavaScript copies the module's
  members, read through the module value so the copy holds the same
  functions: an ES module namespace with its keys sorted and `default`, and
  posix, win32 and the default import in Node's member order.
- scr_lib.h declares the BigIntStats readers, so every runtime name the
  backend can emit has a prototype in scr_runtime.h.
2026-10-28 21:16:57 -05:00
Chris Tate 3b520e4fb2 Merge main 2026-10-28 20:50:23 -05:00
Malte Ubl db617bd594 Merge remote-tracking branch 'origin/main' into fix/pi-conversions-layouts 2026-10-28 18:30:14 -07:00
Chris Tate e0c692593c Evaluate a for..of source beside never[] once, keep collection views through a second view, and combine typed promises in untyped JavaScript
- lowerForOfEmptyArrayArm binds the `Set | Map | string | never[]` source
  to a hidden local before the tag test, so a call or getter in the loop
  head runs once and the loop walks the value the test saw.
- markTarget marks a structural view as a collection view when its source
  is already one, and reruns the fixpoint when a view marked earlier
  becomes a collection view, so a Map or Set handed through two views keeps
  answering through the collection.
- Promise.all in a JavaScript source whose call types as Promise<any[]>
  (an unannotated parameter) runs the engine's Promise.all over the entries
  as checked-dynamic values when they are typed promises. A merged
  `Promise<number | string>[]` argument reaches that call since union
  promise arms merge.

Corpus 8419 and 6956 and the js-own-tostring npm fixture cover each case.
2026-10-28 20:22:14 -05:00
Chris Tate d1abf69149 Refresh the runtime profile and convert ASCII case eight bytes at a time (#2492) 2026-10-28 20:15:03 -05:00
Malte UblandClaude Opus 5.5 caf098eff9 Refuse a stdlib-global snapshot read as a value
`const proc = globalThis.process` lowers to alias plumbing: the declaration
emits no storage and no code, because member and call reads resolve through
the global it names. A read that needs the VALUE therefore failed at the use
with the generic "binding form with no lowering", which names neither the
declaration nor the cause.

Register every stdlib-global snapshot declaration (TypeScript sources only,
matching the elision) and report the cause at the declaration (SC2020),
naming the global and, when it has a short spelling, the type the program
asked the compiler to hold. The use site keeps the inherited-blocker cascade
(SC2004) instead of the generic text.

An unread snapshot stays elided: reading a global has no side effects, so
dropping an unread `const p = process` is observationally identical to Node,
and refusing it would break programs that compile harmlessly today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:58:04 -07:00
Malte UblandClaude Opus 5.5 83e26da25e Keep a literal's accessors live through a declared data member
An object literal whose get/set accessor satisfies a member the target type
declares as DATA was refused with SC1090: a record member is one storage
slot, so the accessor could only be snapshotted at construction. Node keeps
the accessor live, and the pi sites that hit this (agent.ts:90,
runner.ts:889, codemode/execution.ts:562) are all live getters — one throws
until the object observes later state — so a snapshot is disqualifying.

One static record layout cannot hold both member kinds at one name, so the
analysis marks the TARGET type instead of inventing a literal-only variant:
when a literal's member kind disagrees with the member the contextual type
declares at the same name, that type keeps checked identity, exactly as a
structural type that carries a class instance's identity does. The literal
then lowers through the checked-dynamic literal path, which defines accessor
members as live get/set properties and data members as plain properties, and
reads, writes, Object.keys, `in` and JSON.stringify on the declared type
dispatch dynamically — Node's own model.

The mirrored direction is closed too: a data property satisfying a declared
accessor member (`interface Live { get size(): number }`) used to be an
SC2002 missing-`%get:`-slot refusal; it now keeps the declared type's
identity as well.

Cost, the accepted trade: every value of a MARKED type becomes a checked
dynamic view — a property table and closure dispatch instead of a static
struct slot, so reads and writes on those types are slower (never wrong).
The cost is scoped to the marked type: a program with no such literal marks
nothing, and the frontend analysis itself adds only one array push per object
literal and one Set lookup per data member, with no checker query unless a
member name is an accessor somewhere in the program. IR is byte-identical
for 64 existing corpus programs compiled before and after. The whole-type
accessor-shape form (static reads, closure slots) remains available as the
faster representation if profiles ever show this matters; it needs the
accessor enumeration surfaces (keys, JSON, inspect, for-in, spread, rest)
that this route gets for free.

Only a single object type can be the literal's target: a multi-arm union
picks its arm from the value itself and an intersection merges members, so
neither is marked and both keep today's behavior. A value that reaches the
slot through a variable rather than a literal keeps today's refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:58:04 -07:00
Malte UblandClaude Opus 5.5 6e396ad721 Read a shared member off a receiver the checker ruled unreachable
An exhaustive switch's `default:` branch declares `const unreachable: never =
event`, and the warning reporter reads a member off that value through an
erasure (`(event as any).type`, `(event as unknown as { type: string }).type`) —
the shape pi's proxy warning takes. The union shared-field read refused it
because the checker types the receiver `never` (or the erasure), not the
union the value still holds.

`lowerUnionProperty` now proceeds when the receiver expression is a `never`
value, directly or through the parenthesized/parenthesized-assertion chain
that escapes it, and reads the field off what the lowered value actually is:
the shared-field read for the union it holds, the record read for a record,
the keyed read for checked-dynamic. `never` admits no reachable call site, so
no reachable program changes shape.

Corpus: tests/corpus/erased-never-member-read.ts (`as any`, `as unknown as`,
and a helper function taking the same shape). Node-identical stdout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:58:03 -07:00
Malte UblandClaude Opus 5.5 7245ec9a04 Admit function values whose completed parameters the slot coercion adapts
A method declaring `ctx?: Ctx` completes to `(id, Ctx | undefined) => string`;
a slot that requires the narrower `(id, ToolCtx) => string` was refused with
SC1090 at the value's birth, because the gate only compared the two
signatures for equality. The slot coercion (funcCoerceAdapter) already
converts every parameter through coercibleValue, the record width conversion
(which also selects a width-liftable arm of a union destination — the
`ToolCtx` into `Ctx | undefined` shape), the partial checked-dynamic
extraction, and the union arm preservation. `funcSlotAdaptable` now asks the
same question through `cleanParamPair`, so the gate admits exactly the pairs
the adapter converts without a trap-only path, and both call sites (the
object-literal method slot and the mapped value target) use it.

The gate also learns the two slot shapes it could not name: an object
literal contextualized by an INDEX SIGNATURE (`Record<string, Handler>`)
takes its member slot from the index signature's value type, and a literal
with no contextual type takes the member's own completed type — the type
every later read of the member spells.

Nested function DECLARATIONS are no longer judged at birth: lowering a
declaration builds the body, not a value, and each value use re-checks the
completed signature against that use's own target. An overloaded nested
declaration with an optional parameter, returned as a value (`return write`
into a one-parameter `Writer`), is now accepted instead of refused.

Corpus: tests/corpus/optional-param-function-values.ts (a required slot over
an optional parameter, an index-signature member with an extra trailing
optional parameter, an annotated value with a defaulted parameter, the nested
overloaded declaration returned as a value, direct-call controls, and a
union-typed slot). Node-identical stdout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:58:02 -07:00
Malte UblandClaude Opus 5.5 cf7814d19c Lower fs statSync/lstatSync/fstatSync with { bigint: true }
Node answers a BigIntStats when the stat call is passed `bigint: true`: the
same snapshot, read through a bigint member table. Every number member of
Stats reads as a bigint — the integers, the `*Ms` millisecond members (the
truncated nanosecond quotient, as the binding computes them) and the
BigIntStats-only `*Ns` nanosecond members (sec * 1e9 + nsec, exactly) —
while only the four time members stay Dates. scriptc refused the option
outright.

The representation is static, chosen by the option literal: the lowerer
validates `bigint` against the call's result type (so
`{ bigint: true, throwIfNoEntry: false }` still narrows
`BigIntStats | undefined`), refuses a runtime-valued option and an option
that disagrees with the destination, and keeps every other option key
fenced. fstatSync joins statSync/lstatSync on the same path, with the
descriptor evaluated before the options.

The IR gains a `bigintStats` handle kind beside `stats`: the same RC
family, the same non-traced leaf and non-dynamic handle treatment, so no
shape, tracing or dyn-check behavior moves for any program that never asks
for one. Its member table is a `bigstats.*` lib-call family whose Date
members reuse the existing stat reads.

The runtime support is its own unit, scr_stats_bigint.c: the snapshot
layout moved into scr_stats_private.h so the bigint getters can read the
same fields without scr_lib.c carrying their code. The unit is gated on the
existing `bigint` feature in both the runtime-pack matrix and the
compiler's from-source unit table (the sanitized lane links from that one),
and `-ffunction-sections` keeps a bigint program that never reads a member
from linking any of the getters. No typed program's IR or LLVM changes.

tests/corpus/7018-bigint-stats.ts pins the member table, the Date/bigint
split, ms/ns agreement, arithmetic on a member, lstat over a symlink,
fstatSync over a descriptor, the throwIfNoEntry union, and the plain Stats
answers of the un-optioned and `bigint: false` forms against Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:58:01 -07:00
Malte UblandClaude Opus 5.5 4c4a53d538 Name the arms a union-typed spread merge cannot pick between
A spread merge like `{ ...base, ...override }` whose sources are union
types has one field set per combination of source arms, so the merged value
can carry fields from several arms at once. No single arm of the
destination union can hold it — naming one would drop the other arms'
fields, which Node keeps — and no runtime tag would be honest. The merge
was refused with the generic "cannot be compiled yet" (SC2001), which says
nothing about the shape the program is building.

The new SC2003 names the destination union and lists the candidate arms,
and its hint gives the two repairs: merge ONE arm per path (narrow every
spread source to the same arm first), or declare one open record type whose
fields are all optional instead of a union of all-optional arms. The
diagnostic fires only for a union-typed destination; other non-record
destinations keep badType.

`NonNullable<T>` over a union already maps to the union value itself, so
tests/corpus/7017-nonnullable-union.ts pins the mapped-type path
(assertions, generic instantiations, narrowing, serialization) against
Node. tests/diagnostics/union-spread-merge.ts re-pins the refusal, and the
union-clone-override-width snapshot moves from SC2001's generic text to
the arm-naming SC2003.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:58:00 -07:00
Malte UblandClaude Opus 5.5 f7a22cb0c7 Lower delete u.k when every union arm declares k optional
A union of records whose every arm declares the same field optional has a
definite answer for `delete u.k`: the field becomes absent, exactly as the
monomorphic optional-field delete writes it. The delete statement used to
fall through to the receiver-agnostic path and refuse the union.

lowerUnionFieldDelete dispatches over the arms (one tag test per arm, the
last arm as the fall-through else) and writes each slot's absent value
through a recordSet on the narrowed arm; the union value is retained once,
so every arm deletes the object the program passed rather than a
projection. Sloppy-mode writes keep their frozen-record semantics.

Everything else keeps its fence: unit arms, class arms, required fields
(a slot with no absence to represent), accessors, index signatures,
optional chains and tuples all return null and reach the existing
diagnostics.

tests/corpus/7016-union-field-delete.ts pins the delete, the absence
afterwards (`in`, optional reads, JSON.stringify), the retained identity
the caller's alias sees, and deleting an already-absent field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:58:00 -07:00
Malte UblandClaude Opus 5.5 81cacab03d Lower node:path's platform namespaces as path module values
path.posix and path.win32 are module objects, not declarations: they are
the same handle `process.getBuiltinModule("node:path")` answers, so
`posix === path.posix` holds and a mutation through one spelling shows
through the other. Reading either namespace as a value mapped to a module
namespace type with no value lowering, and `import { posix } from
"node:path"` was refused outright.

The platform namespaces now lower to the checked-dynamic module value, as
a `path.posix` property read, as a named import, and as a receiver
(`win32.join(...)`); pathModuleValue answers the module handle instead of
building a fresh object literal, which is what makes the identity and the
cached members hold. Type mapping treats a path platform namespace and
@types/node's PlatformPath interface as the dynamic type they are, so a
union with a plain record arm collapses onto it instead of refusing.

tests/corpus/7015-path-platform-namespace-values.ts pins the identity,
cached members, joins and the import forms against Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:57:59 -07:00
Chris Tate 84644dfe9d Send the writeHead head with the response's first write (#2488) 2026-10-28 19:18:26 -05:00
Chris Tate db30118a0e Store tty.isatty and process.cwd as function values
Fixes #2408
Co-authored-by: MathurAditya724 <57684218+MathurAditya724@users.noreply.github.com>
Fixes #2402
2026-10-28 19:18:01 -05:00
Chris Tate 7f0c987140 Key Sets and Maps of identity-arm unions by their pointer representation (#2487) 2026-10-28 19:08:20 -05:00
Chris Tate 500dd3e9b4 Run short constant-count loops together when their inner exits saturate
A loop of two to eight iterations with a constant count, whose body is an
inner loop that exits once an accumulator reaches the value its ORs
saturate at (mandelbrot's pixel pairs: `b |= 2`, `b |= 1`,
`if (b === 3) break`), now runs its iterations in one inner loop: every
iteration's declarations, then one loop running each iteration's body in
turn that exits once every accumulator is full, then every iteration's
trailing assignments in order.

Once an accumulator is full its ORs leave it full, so an iteration that
runs past its own exit leaves its accumulator as its own loop did, and
every other value it computes is dead. The bodies only compute numbers,
read typed-array elements that answer NaN out of range and write their
own locals; the declarations and the inner loop read nothing the trailing
assignments write. The independent iterations then overlap their
dependence chains.
2026-10-28 18:52:05 -05:00
Chris Tate 61c24c08e9 Refresh the runtime profile and convert ASCII case eight bytes at a time
scr_str_case_conv scans ASCII text for the first letter to convert eight
bytes at a time, then converts the rest eight bytes at a time; the tail
and big-endian targets take the byte loop. With a refreshed profile, the
byte-at-a-time scan compiled worse for short strings with nothing to
convert, which made validate-errors slower.

The runtime profile is collected again on this change with
scripts/refresh-runtime-profile.mjs (326 raw profiles). Changed
functions hold 0.0% of its counts, against 3.7% for the previous one.

tests/corpus/string-case-ascii-words.ts covers ASCII strings of every
length from 0 to 40, with letters at block boundaries and next to the
bytes just outside A-Z and a-z.
2026-10-28 18:43:37 -05:00
Chris Tate 18ab17848e Keep the held head across a cork() and uncork() with no write
An uncork() flushed a head writeHead stored even when nothing was
written while corked. Node keeps the stored head until write(), end() or
flushHeaders(), so the flush now sends it only when a write() or
flushHeaders() ran while corked. The http-held-head fixture covers the
three cases.
2026-10-28 18:23:49 -05:00
Chris Tate 94e921728c Island handle operations, native subclasses of island classes, arrays and collections by reference (NI-5b/5c/5d) (#1903) 2026-10-28 18:09:56 -05:00
Chris Tate 7409c85216 Compile .length of computed tuple expressions (#2482) 2026-10-28 17:51:45 -05:00
Chris Tate 3a1ba89194 Throw for .length of a missing tuple element
An unchecked element read of a tuple array (`rows[i].length` with
`rows: [number, string][]`) answers the element's optional value, and the
computed `.length` path discarded it, so a read past the end printed the
tuple's arity. It now holds the receiver and throws Node's TypeError,
"Cannot read properties of undefined (reading 'length')", when the element
is missing, before answering the arity.

The corpus program tuple-length-computed.ts covers a missing element read
directly and through a field holding the array.
2026-10-28 17:04:51 -05:00
Chris Tate 94215d6f0e Keep glibc's memset, memcpy, memcmp and bcmp in zig-linked executables
The BigInt reciprocal divided a two-limb value by one limb, which compiles
to a __udivti3 call. zig provides that builtin from its compiler-rt, a
single object that also defines memset, memcpy, memmove, memcmp, bcmp,
strlen and the _chk variants. Linking it for __udivti3 made those
definitions take precedence over libc's in every glibc executable built
with SCRIPTC_CC=zigcc, and zig's bcmp and memcmp compare one byte per
iteration and its memset stores one byte at a time.

The reciprocal now divides in 32-bit halves with one-limb operations, so
the runtime references no compiler-rt builtin and glibc executables import
the four functions from libc.
2026-10-28 17:01:19 -05:00
Chris Tate 2814b57e20 Send the head writeHead stores with the response's first write or end()
res.writeHead() builds the head and keeps it until the first write(),
end() or flushHeaders(), which sends it with that call's data in one
socket write, as Node's _storeHeader does. A response with an Expect
header sends the head at once, as in Node. The http-held-head server
fixture compares the wire timing with Node's.
2026-10-28 16:35:39 -05:00
Chris Tate 6cc5345a41 Key Sets and Maps of identity-arm unions by their pointer representation
A union that is a Set element or a Map key was a tagged box in the whole
program, because the C map hashes and compares union keys by tag and
payload. One `Set<Node | undefined>` made every `Node | undefined` value
of the program a box, at every read.

A union whose arms all compare by identity (records, class instances,
arrays, functions), beside at most one undefined and one null arm, now
keeps its pointer representation in collections too. The map keys such a
union by reference: the pointer carries the identity, and NULL and the
null sentinel are two more distinct keys. Unions with a string or another
value arm keep their boxes, and library builds keep the earlier rule.

The tests of the box paths keep their unions boxed through a new
`boxedUnions` emission option instead of a Set of each union.
2026-10-28 16:22:38 -05:00
Chris Tate 3a15562085 Follow a redirect whose chunked body has not ended (#2484) 2026-10-28 16:06:56 -05:00
Chris Tateandlinguofeng 46058c67a2 Report an aborted client response's error to its 'error' listeners only
A client response cut off mid-body emitted 'error' ("aborted") whether or
not it had an 'error' listener, so a response without one ended the
process with "Unhandled 'error' event". Node's IncomingMessage reports its
destroy error only to 'error' listeners; 'aborted' and 'close' still fire.

fetch leaves a redirect response by destroying its request, so following
a redirect whose chunked body had not ended crashed the process before
fetch settled. It now follows the redirect.

Fixes #2395

Co-authored-by: linguofeng <460328+linguofeng@users.noreply.github.com>
2026-10-28 15:19:07 -05:00