Commit Graph
3695 Commits
Author SHA1 Message Date
Chris Tate 00b58e5c7a Map UTF-16 indexes of long non-ASCII strings in constant time, and quick-check normalize (#2294) 2026-10-25 21:59:27 -05:00
Chris Tate cc2266ab1d Add --cpu-dispatch for x86-64 executables (#2288) 2026-10-25 21:36:00 -05:00
Chris Tate 0abe5063ff Wait for every released package to resolve on npm before the GitHub release (#2281) 2026-10-25 21:28:51 -05:00
Chris Tate 018dbb766c Compute Math functions as V8 does and link libm only when a program needs it (#2287) 2026-10-25 21:26:20 -05:00
Chris Tate 59e78f298a Name the package declaration and its patch when a declared-type check fails (#2268) 2026-10-25 21:20:34 -05:00
Chris Tate 43a8c6d8cc Link the Linux release helpers against LLVM built with ThinLTO and a helper profile (#2282) 2026-10-25 21:09:17 -05:00
Chris Tate 0b483e8650 Map UTF-16 indexes of long non-ASCII strings in constant time and quick-check normalize
- Index walks (slice, substring, substr, startsWith/endsWith with a
  position, lastIndexOf results) on a non-ASCII receiver of 64 KiB or
  more: once they would have paid for one pass over it, the receiver's
  index entry keeps every unit's byte offset (a 32-bit base per 64 units
  and one byte per unit), and indexes map with two loads. Before that, a
  walk within 64 units of the hot cursor starts from it, without the index
  search. The index entry stays 128 bytes on 64-bit targets.
- lastIndexOf on non-ASCII receivers tests the needle's first byte before
  comparing, as the ASCII path does.
- normalize: a quick check (UAX #15) returns the receiver when every code
  point is inert for the form: unchanged when normalized alone, combining
  class 0, and, in the ranges checked, never the second element of a
  composition. Other text normalizes segment by segment, each starting at
  a starter, so runs of inert code points copy as they are and a lone
  precomposed letter's result is kept per form. The per-code-point facts
  come from unicode_normalize, 256 code points at a time.
2026-10-25 21:04:29 -05:00
Chris Tate d21f0d7c8b Pin the identity of native values seen as untyped values (#2290) 2026-10-25 21:04:04 -05:00
Chris Tate 794a8cf8a8 Compute products of typed-array reads in 32 bits under bitwise operators (#2289) 2026-10-25 20:58:49 -05:00
Malte UblandClaude Opus 5.5 1493088594 Pin the identity of native values seen as untyped values
A native record, array or class instance boxed as unknown or any is the same
object: every box of it is one value, untyped writes, optional fields,
deletes and expandos show through typed reads and other boxes, nested
records and arrays are the typed program's own, and the value keys Map, Set
and WeakMap entries as itself. Main already behaves this way since records
and arrays box as live references; this corpus program pins it for the
single object model's later stages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:33:21 -07:00
Chris Tate 8981b5c0a2 Resolve computed createRequire specifiers at run time (#2079) 2026-10-25 20:29:51 -05:00
Chris Tate c820a6c57f Compute products of typed-array reads in 32 bits under bitwise operators
A sum of products of integer typed-array reads that only reaches a
bitwise operator, such as `(p[j] * 77 + p[j + 1] * 150) >> 8`, now adds
in 32-bit integers like a sum of the reads themselves: a product whose
every result is a safe integer, and never -0, is an exact integer or NaN.
A NaN-coded read passes its validity bit up through those sums, so the
integer result selects 0 for a missed read without a floating compare,
and a loop over such sums vectorizes without converting to doubles.
2026-10-25 20:29:18 -05:00
Chris Tate 8e4837f94a Run untyped pipeline function stages and later iterable stages (#2276) 2026-10-25 20:17:26 -05:00
Chris Tate 07c5e79ffd Add --cpu-dispatch: x86-64-v3 copies of the program's code chosen at startup
With --cpu-dispatch, an x86-64 executable carries two copies of each
program function: the baseline code and a copy compiled for x86-64-v3.
main reads CPUID and XGETBV once. Calls between the program's functions
go to the caller's copy. A function that is also named outside the
copies (a function value, a call from main, a runtime callback or a
coroutine body) keeps a dispatcher under its own name, which tests the
level and tail-calls one copy. Functions without such a use get no
dispatcher, so their copies have the callers the original had and LLVM
inlines them the same way.

The executable still runs on any x86-64 CPU. SCRIPTC_CPU_LEVEL caps the
level at run time. The option is off by default, applies to executables
without a --target-cpu level, and is part of the executable cache key.
bench:runtime takes --baseline-cpu-dispatch and --candidate-cpu-dispatch,
and the LLVM differential suite runs each program at levels 0 and 3 with
SCRIPTC_CPU_DISPATCH=1.
2026-10-25 20:00:16 -05:00
Chris Tate dd418b067a Compute Math functions as V8 does and link libm only when a program needs it 2026-10-25 19:48:38 -05:00
Chris Tate 58a64ffb36 Discard the result of void generic methods called through class-backed interfaces (#2275) 2026-10-25 18:56:01 -05:00
Chris Tate 5dfa62b9fc Merge main 2026-10-25 18:54:10 -05:00
Chris Tate 681ac6ee15 Quote declaration-derived arguments in suggested type-patch commands 2026-10-25 18:53:50 -05:00
Chris Tate 4c3a1d9c98 Link the Linux release helpers against LLVM built with ThinLTO and a helper profile
The Linux helpers linked LLVM 22.1.8's release archive. The helper's code
generation is LLVM's code, so building that LLVM from its release sources
with ThinLTO and with a profile of the helper's own work makes code
generation 5-9% faster than the helper as released, with identical objects.

- native/llvm-codegen/llvm.profdata.gz is the profile, collected by
  scripts/refresh-llvm-profile.mjs: it builds LLVM with IR instrumentation
  and an instrumented helper, and runs that helper on tsc-ts, five Effect
  fixtures, the runtime benchmark workloads and every 50th corpus program
  (scripts/llvm-helper-inputs.mjs). It follows LLVM's sources, so it needs
  a refresh when LLVM is bumped.
- scripts/build-llvm-dist.mjs builds LLVM with that profile and ThinLTO,
  and packs it with a checksum. The llvm-dist workflow, run by hand, builds
  it for x64 and arm64, requires a helper linked against it to give the
  same objects as one linked against the release archive
  (scripts/check-llvm-dist.mjs), prints both helpers' times and the
  profile's mismatch count, and attaches the archive to the LLVM_DIST_TAG
  release of this repository.
- The release workflow's Linux jobs, and the wasm32-wasi job, which also
  uploads the x64 GNU helper, download that archive and build the helper
  with SCRIPTC_LLVM_THINLTO=1: clang, lld and ThinLTO, and the profile for
  the helper's own sources. Without the archive, or if that build fails,
  the helper links the release archive as before.
- The four Linux helper packages share their build in
  native/llvm-codegen/linux-helper.mjs. Without SCRIPTC_LLVM_THINLTO it
  builds the same executable as before.
2026-10-25 18:53:25 -05:00
Chris Tate 68e148add9 Wait for every released package to resolve on npm before the GitHub release
The release workflow publishes each package and moves on once npm accepts
the upload. A package whose processing on npm stalls was visible only on
npmjs.com.

A new npm-available job runs after "Publish to npm". It checks each package
of the release with `npm view <name>@<version> version --prefer-online`
every 2 minutes (scripts/wait-npm-available.mjs) until every package
resolves, and reports each package's time from upload to available in the
log and the job summary. When a package still does not resolve after 2
hours, the job fails and names each missing package and version. It only
reads from the registry.

The publish job lists every package of the release and records the time
npm accepted each upload, keeps those times with its upload receipts, and
passes both to the new job. Create GitHub Release now waits for the new
job, so the release is created and marked complete only once every
package resolves; a later release run checks again and skips uploads npm
already accepted.
2026-10-25 18:51:56 -05:00
Chris Tate 5f9fd5d49e Clone union records whose override is an empty array literal (#2278) 2026-10-25 18:49:23 -05:00
Chris Tate a848a7b349 Resolve the pipeline promise to undefined for an untyped iterable destination
When an untyped destination function returned an async iterable, the
promise form settled its result without a payload, and code that read the
awaited value as an untyped value dereferenced NULL. The promise form for
an untyped destination now resolves to undefined when the destination
returned no promise, or its promise carries no value, as Node does.
2026-10-25 18:44:18 -05:00
Chris Tate 14bb8d4156 Convert asserted values that no ordinary conversion reaches (#2260) 2026-10-25 18:27:26 -05:00
Malte Ubl c7ac4d7001 Expect the types patch command in the same-name origin test 2026-10-25 16:25:10 -07:00
Chris Tate 417e3a6d08 Call function tables without boxing or reference counting (#2173) 2026-10-25 18:17:54 -05:00
Chris Tate 693b038b15 Keep the result of void generic methods called through class-backed interfaces
A class may implement a void interface method with a method that returns a
value, and JavaScript passes that value on (`console.log(c.publish(m))`).
The call now keeps the dispatch's untyped result, as a call through a
non-generic void member already does, instead of replacing it with
undefined: printing or interpolating it shows the value, and binding it to
a void-typed name checks it.
2026-10-25 18:17:32 -05:00
Chris Tate 81fc7bfa89 Map any parameters of interface method and call signatures as checked values (#2273) 2026-10-25 18:14:23 -05:00
Chris Tate 30cf205e90 Run untyped pipeline function stages and later iterable stages
A pipeline stage that arrives as an untyped value now follows Node's
stage ladder for more shapes. As the first stage, a function is a source
function: the pipeline calls it with { signal } while it is set up and
pumps its result, which must be iterable (ERR_INVALID_RETURN_VALUE
otherwise), and a { readable, writable } pair pumps its readable side.

After the first stage, the pipeline call is lowered once per combination
of its first three untyped stages being functions or not, and a typeof
test picks the branch at run time. A function stage is called with the
previous stage and { signal }; before the last stage its result must be
async iterable, and as the last stage a promise resolves the pipeline's
value and an async iterable feeds the PassThrough pipeline() returns
(SCR_PIPE_DEST_DYN). Any other value that is not a stream becomes
Duplex.from of it, which the next stage reads from without the previous
stage being piped into it, and Duplex.from's ERR_INVALID_ARG_TYPE for
the values it rejects.

An untyped stage followed by an untyped function stage hands its value to
that stage through a cell created per pipeline call, so the function
receives the previous stage's own iterator, as Node's makeAsyncIterable
answers it, instead of an async generator over it.

Untyped web streams, pairs after the first stage, and function stages
that return a stream throw the not-supported error.

RUNTIME_ABI_VERSION goes from 45 to 46 for the new stream exports.
2026-10-25 18:10:32 -05:00
Chris Tate 56099ae21c Erase lambdas that implement a generic method through their context (#2274) 2026-10-25 18:07:47 -05:00
Malte Ubl c60bfe8b57 Merge branch 'feat/type-patch-origins' into feat/type-patch-command 2026-10-25 16:02:48 -07:00
Malte UblandClaude Opus 5.5 7675b7d653 Add scriptc types patch and scriptc types list
`scriptc types patch <package> <selector> <type> [--range] [--dry-run]`
finds the installed package from the working directory, shows every
declaration the selector matches (declaration files, JSDoc, @types
package) with its type before and after, and adds the patch to the
nearest package.json: into an entry whose range covers the installed
version, or under ^<version>. Nothing is written when the selector
matches nothing. `scriptc types list` shows each patch and what it
matches. Both CLI hosts run the same implementation with their own
parser. The TypeError note for a wrong declaration now names the command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 16:00:29 -07:00
Chris Tate 27c00f6577 util: runtime format strings, inspect.custom and colors, untyped promisify (#1911) 2026-10-25 17:46:31 -05:00
Chris Tate b63dc67f9e Let boxable function arms ride a checked-dynamic union (#2269) 2026-10-25 17:44:23 -05:00
Chris Tate 46785f8eba Address review feedback 2026-10-25 17:35:38 -05:00
Chris Tate fad9cbdf57 Keep U+11A7 after a Hangul LV syllable in NFC and NFKC (#2266) 2026-10-25 17:31:45 -05:00
Chris Tate 8be991aeca Skip worker cancellation polls in short loops inside another loop (#2261) 2026-10-25 17:14:42 -05:00
Malte UblandClaude Opus 5.5 062700bdde Convert asserted values that no ordinary conversion reaches
`x as T` erases (or, record to record, converts), and the value then meets
its slot with x's representation. Where no conversion reaches the slot
the program was refused:

- a union whose arms only partly convert into T (`api as Partial<Record<
  string, S>>` on `S | Partial<Record<K, S>> | undefined`) failed at the
  next typed slot with SC2003;
- a record or array whose member types the assertion narrows (`models as
  Model<"one">[]` on a union-element array, `item as Narrow` with a
  narrower field) failed with SC2002 or SC2003.

The lowerer now remembers the asserted type of an erased value, and a slot
that would refuse it converts it as asserted: the arms T excludes throw
the catchable TypeError of a lying assertion and the others convert, and
records and arrays take the checked deep conversion, which validates the
value member by member and names the deepest failing path, as a cast of an
unknown value does. The conversions run only where the slot would
otherwise refuse the value, so everything that already lowered keeps its
code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:10:31 -07:00
Chris Tate 67c38b5361 Keep the whole union of an optional parameter that reaches a recursive placeholder (#2241) 2026-10-25 17:10:27 -05:00
Malte UblandClaude Opus 5.5 56e8417746 Name the package declaration and its patch when a declared-type check fails
A value that contradicts a type an npm package declares used to throw
"expected number at $, got undefined" and nothing else. Such a check now
names the declaration and the type patch that admits the value:

    profilekit@3.1.0 declares Profile.email as string (index.d.ts:7)
    if that declaration is wrong, patch it in package.json: ...

Origins are found for values a package function returns under a written
return type, for properties of records typed by a package-declared type
(record projections into such a type are keyed by it), and for
declaration-boundary casts, whose origin the projection records from the
declaration-world checker. Generic declarations get none: their types
come from the program's own instantiation.

Checked conversions carry dynCheck.origin, an index into
IrModule.declOrigins; the site's existing cold exception block calls
scr_type_origin_note (new runtime unit behind the typeOrigins feature),
which appends the note to the pending TypeError and derives the selector
suffix and the admitted kind from the failure. Union extractions get the
note per arm at compile time. The hot path is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:06:50 -07:00
Chris Tate f1a69d9bb0 Probe hashed number-key maps without normalizing integer keys (#2264) 2026-10-25 17:03:41 -05:00
Malte UblandClaude Opus 5.5 171214b2c2 Discard the result of void generic methods called through class-backed interfaces
A generic method called through an interface value that classes
implement dispatches by name on the checked-dynamic receiver, and the
result converts to the call's instantiated return type. For a void
method, that conversion (`unknown` to `void`) was refused (SC1100). The
result is now discarded, as JavaScript does.

The dispatch helper selected overriding subclasses' erased instances with
a ternary, which has no void form (an internal compiler error once the
call got that far). A void method now selects them with if statements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:02:26 -07:00
Malte UblandClaude Opus 5.5 3375304945 Erase lambdas that implement a generic method through their context
A lambda with no type parameters of its own, contextually typed by a
generic signature (`send: (m) => …` for an interface's `send<T>(m:
Message<T>): string`), takes that signature's type parameters: its
parameter is `Message<T>`. A generic lambda written with its own type
parameters already lowers once, erased, with them bound to `unknown`, but
this one tried to map `Message<T>` with `T` unbound and was refused
(SC2009).

It now lowers the same way, erased, binding the contextual signature's
type parameters to `unknown`. Parameters bound in the enclosing scope
keep their bindings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:02:24 -07:00
Malte UblandClaude Opus 5.5 be0e825ac1 Map any parameters of interface method and call signatures as checked values
A parameter a TypeScript program types `any` maps to the checked-dynamic
slot for function declarations and spelled function types, but not for
method or call signatures of interfaces and type literals. So
`interface Tool { execute(id: string, params: any): Promise<string> }`
was refused (SC2009), along with every value of the interface, whenever
no class implementing it gave it a class view.

Method and call signatures in program source now take the same rule.

pi's ToolDefinition declares `execute(toolCallId: string, params:
Static<TParams>, …)`. Under --node-islands, typebox's types resolve to
`any`, so the parameter is `any`. The refusal rejected the AgentSession
class through its `_customTools: ToolDefinition[]` field, and
CreateAgentSessionRuntimeResult with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:02:20 -07:00
Malte UblandClaude Opus 5.5 605e312e78 Let boxable function arms ride a checked-dynamic union
A union with a checked-dynamic arm (`unknown`, fetch's AbortSignal)
maps to the checked-dynamic value when every other arm is one the
checked-dynamic tree holds faithfully. Function arms were excluded, so
`Progress | AbortSignal | undefined` was refused (SC2009).

The tree boxes a function whose parameters check in and whose result
converts out by reference (canBoxFuncIntoDyn), and `typeof x ===
"function"` narrows it back out to the typed function. Such arms now
join the collapse. The corpus program checks calls, identity (`===`
against the original), and the other arm's members against Node.

pi's SessionManager.listAll takes `onProgressOrSignal?: SessionListProgress
| AbortSignal`. The refusal rejected the whole SessionManager class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:02:19 -07:00
Malte UblandClaude Opus 5.5 52abff7c14 Clone union records whose override is an empty array literal
Spreading a union of records and replacing a shared field keeps each
value's variant (lowerUnionRecordClone), but only when every override's
type lifts into the field. An empty array literal is typed `never[]`,
which maps to nothing, so `{ ...entry, content: [] }` over a union fell
back to the ordinary literal path. That path cannot build a union of
spread records, and the literal was refused (SC2001).

An empty array literal now builds an empty array of each arm's own
element type. This covers a field that is an array and a field that is a
union with exactly one array arm (`string | Part[]`).

pi's session-manager repairs messages with missing content this way
(`return [{ ...message, content: [] }]`). The refusal rejected the whole
SessionManager class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:02:07 -07:00
Chris Tate 5f76d41a71 Keep U+11A7 after a Hangul LV syllable in NFC and NFKC
libunicode composes an LV syllable with U+11A7 as if it were a trailing
consonant (U+11A8..U+11C2), which drops it: "가ᆧ".normalize()
gave "가" where Node gives "가ᆧ". U+11A7 is a class-0 vowel
that composes with nothing, so for the composing forms the code points
between occurrences of it normalize on their own and it is kept as it
is. The guard sits in front of the vendored library (scr_unicode_normalize),
for String.prototype.normalize and the NFC step of IDNA mapping.

8009-normalize-hangul-jungseong-o-yae prints the code points of all four
forms for U+11A7 after LV and LVT syllables, conjoining jamo, other
starters and combining marks.
2026-10-25 16:57:11 -05:00
Chris Tate 8d485dcf5f Convert promise and function values into a union's arm of their kind (#2257) 2026-10-25 16:56:50 -05:00
Chris Tate 60c89fd456 Keep record values that subtype reduction folds into an inferred return type (#2069) 2026-10-25 16:50:31 -05:00
Malte UblandClaude Opus 5.5 5a3d6d7903 Call function tables without boxing or reference counting
Values typed `F | undefined` for a function type F are now plain closure
pointers (nullable-pointer unions), so a keyed read of a
`Record<string, F>` or a Map.get no longer allocates a union box. Keyed
record reads, string-keyed Map.get and checked extractions lend the stored
closure as a receiver or callee when ReferenceWrites proves the call cannot
write the locations it was read from, and optional-chain receivers lend
under the same proof. Owned record overflow reads use the inline entry
lookup.

Indirect closure calls skip the ambient receiver bracket for closures
whose bodies never observe it (new ScrClosure.function_kind bit
SCR_FN_RECEIVER_FREE; signature adapters that only forward the receiver
copy it from their original), and a callValue that forwards `dyn.this`
needs no bracket at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 14:49:00 -07:00
Chris Tate 3a900ed9b6 Keep the poll in nested loops whose counter can stop changing at 2^53
The range analysis caps a counter that steps by 1 at 2^53, where
`j + 1 === j`, so a loop such as `for (let j = 2 ** 53 - 2;
j < 2 ** 53 + 10; j++)` has a range of three values at its condition and
was taken for a loop of three runs. It never ends, and without its poll
worker.terminate() could not stop it. A loop's runs now count as bounded
only when a step from every value in its counter's range gives a
different value.
2026-10-25 16:38:27 -05:00