mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Merge main
This commit is contained in:
@@ -12,7 +12,6 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` |
|
||||
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
|
||||
| `dictionary-walks` | cpu | `Record<string, number>` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted |
|
||||
| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods |
|
||||
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
|
||||
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
|
||||
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"entry": "workloads/dynamic-methods.mjs",
|
||||
"args": [
|
||||
"300000"
|
||||
]
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// Untyped JavaScript calling builtin methods on values it never declares
|
||||
// types for: string slice/indexOf/toUpperCase/startsWith/padStart, array
|
||||
// push/pop/at/join/includes/indexOf, Map and Set get/set/has/add, number
|
||||
// toFixed, and calls of an object's own methods. Argument: the number of
|
||||
// iterations.
|
||||
const rounds = Number(process.argv[2] ?? "300000");
|
||||
const words = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta"];
|
||||
const counts = new Map();
|
||||
const seen = new Set();
|
||||
const stack = [];
|
||||
const recent = [];
|
||||
const tally = {
|
||||
total: 0,
|
||||
add(value) {
|
||||
this.total += value;
|
||||
return this.total;
|
||||
},
|
||||
};
|
||||
let checksum = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
const word = words[i % words.length];
|
||||
const head = word.slice(0, 3).toUpperCase();
|
||||
const key = head.padStart(4, "-");
|
||||
if (word.indexOf("e") >= 0 && !word.startsWith("z")) checksum += 1;
|
||||
counts.set(key, (counts.get(key) ?? 0) + 1);
|
||||
seen.add(i % 97);
|
||||
if (seen.has(i % 89)) checksum += 2;
|
||||
stack.push(i);
|
||||
if (stack.length > 32) checksum += stack.pop() % 3;
|
||||
recent.push(word);
|
||||
if (recent.length > 4) recent.shift();
|
||||
if (recent.includes("gamma")) checksum += recent.indexOf("gamma");
|
||||
checksum += tally.add(i % 5) % 11;
|
||||
if (i % 1000 === 0) checksum += Number((i / 7).toFixed(2)) | 0;
|
||||
}
|
||||
console.log(checksum, counts.size, seen.size, stack.at(-1), recent.join(","), tally.total);
|
||||
@@ -97,7 +97,7 @@ Coverage and `scriptc build` report the same diagnostic codes. Common ones inclu
|
||||
Some code compiles but can behave differently than under Node.js. Coverage reports the cases it can detect as `SC6xxx` warnings, and `scriptc build` prints the same warnings after a successful build. They never fail a build, but `--fail-on=divergences` can fail a CI job on them.
|
||||
|
||||
- `SC6001`: a record passed to, stored as, asserted as, or stored as an array element of a record type with different properties is copied, and the program then writes through the copy or compares it by identity.
|
||||
- `SC6002`: values of one record type are built in different key orders, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output.
|
||||
- `SC6002`: values of one record type are built in key orders scriptc cannot track per value, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output.
|
||||
- `SC6003`: `localeCompare` compares a string literal outside the built-in Latin-script collation.
|
||||
- `SC6004`: a call passes a class instance to a function that casts it to an unrelated class.
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/language-features/async-iteration.mdx</include>
|
||||
|
||||
<include>../limitations/language-features/iterable-values.mdx</include>
|
||||
|
||||
<include>../limitations/language-features/generics.mdx</include>
|
||||
|
||||
<include>../limitations/language-features/function-values.mdx</include>
|
||||
|
||||
@@ -3,4 +3,6 @@
|
||||
Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly.
|
||||
Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not.
|
||||
`Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported.
|
||||
The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called, and `instanceof` and `new` against them remain unsupported.
|
||||
The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called.
|
||||
`new` through a stored `Object`, `Date`, `Map`, `Set`, `WeakMap`, or `WeakSet` builds the same value as the `new` expression.
|
||||
`new` through a stored `RegExp` or `Promise`, `new` of a stored `Object` with a primitive argument, and `instanceof` against these constructors are unsupported.
|
||||
|
||||
@@ -10,19 +10,30 @@
|
||||
The `objectMode` option, and the `readableObjectMode` and `writableObjectMode` options of a `Duplex`, `Transform`, or `PassThrough`, take `true` or `false` literals in an options object literal; options passed as a value are read when the stream is built.
|
||||
In typed code, a `write` or `transform` callback, or a `_write` or `_transform` method, may declare its chunk with any type that converts to a checked-dynamic value: the chunk converts when the callback runs, and a value of another type throws a TypeError there.
|
||||
An object-mode write passes its encoding argument to `write` and `transform` as Node does (`undefined` when absent, `null` when a callback took its place); a parameter declared `string` receives `""` for `undefined` and `null`, and one declared `string | null | undefined` receives Node's value.
|
||||
`readable.read()` on an object-mode stream answers its Buffers; another value throws a not-supported error (read such values with `'data'` listeners, `for await`, or `pipe`).
|
||||
`readable.read()` on an object-mode stream answers any value it holds.
|
||||
`node:stream/consumers` and readline interfaces read object-mode streams of strings and Buffers; other values throw a not-supported error.
|
||||
`pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), and an async function destination whose value resolves the promise form and reaches the callback's second argument.
|
||||
`pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), an async generator function as the destination, and an async function destination whose value resolves the promise form and reaches the callback's second argument.
|
||||
A stage is written inline, or named by a function declaration or a `const` bound to a function expression that nothing reassigns.
|
||||
The source may also be an array, a string, a synchronous generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node.
|
||||
Stages may take the `{ signal }` argument.
|
||||
An async generator function as the destination, using the PassThrough that `pipeline()` returns for a function destination, and function stages or iterable sources together with pipeline options are unsupported.
|
||||
With a function destination, `pipeline()` returns an object-mode PassThrough that receives an async generator destination's values, or an async function destination's value unless it is `null` or `undefined`.
|
||||
`@types/node` types that result as `NodeJS.WritableStream`, so `for await` over it needs the `PassThrough` type (`pipeline(...) as PassThrough`, or a JSDoc `@type`).
|
||||
In typed code, the callback of a pipeline with an async function destination receives `null` instead of `undefined` when there is no error.
|
||||
`Readable.from` accepts `objectMode` and `highWaterMark` options.
|
||||
The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options; their callbacks take only the chunk, `map` returns strings or Buffers, and `flatMap` returns strings or arrays of them.
|
||||
`readable.compose` is unsupported.
|
||||
`stream.compose` accepts two or more streams, or one `Duplex`; functions, iterables, and web streams as stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream.
|
||||
`pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags.
|
||||
The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options.
|
||||
Their callbacks may take the `{ signal }` argument, `map` may return any value, and `flatMap` may return any iterable, async iterable, or stream.
|
||||
`Duplex.from` accepts an async generator function, which reads what is written to the Duplex, a `Duplex`, `Transform` or `PassThrough`, which it returns, and the values `Readable.from` accepts, which give a Duplex that is not writable.
|
||||
It also accepts a lone `Readable` or `Writable` and a `{ readable, writable }` pair, which the Duplex reads and writes through, a promise, whose value it reads, and an async function, which reads what is written and must resolve to `null` or `undefined`.
|
||||
A `Duplex.from` body function may take the `{ signal }` argument, which is aborted with the error the Duplex is destroyed with.
|
||||
`stream.compose` accepts streams and async generator functions in any position, an async function as the last stage, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it.
|
||||
A web `ReadableStream` is read as an async iterable by `Readable.from` and as the first stage of `compose` or `pipeline`, and through its reader by `Readable.fromWeb` (with `objectMode`, `highWaterMark`, `encoding`, and `signal` in an options object literal) and `Duplex.from`.
|
||||
A web stream in a later compose stage, `Readable.toWeb`, and web `WritableStream` and `TransformStream` interop are unsupported.
|
||||
The `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream.
|
||||
`readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read.
|
||||
`pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags.
|
||||
Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream.
|
||||
The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on Writable and Duplex; `writev` on a Transform or PassThrough, assigning `_construct` after construction, and a non-literal `decodeStrings` or `defaultEncoding` in an options object literal are unsupported.
|
||||
`setDefaultEncoding()` takes any encoding Node knows.
|
||||
A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error.
|
||||
`node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile.
|
||||
`on`, `once`, and `addListener` return the interface, so registrations chain.
|
||||
|
||||
@@ -28,7 +28,8 @@ The `captureRejections` option and `events.errorMonitor` listeners are not suppo
|
||||
The stream calls the object's `_read`, `_write`, `_final`, `_transform`, `_flush`, and `_destroy` with the object as `this`, and the stream prototypes' methods and accessors work on it.
|
||||
Its listeners live in the native stream rather than in `_events`, and `_readableState` and `_writableState` are read-only views that answer the commonly read fields.
|
||||
Typed code that receives such an object as a stream class uses its native stream, and piping a native stream into it works.
|
||||
The options the native streams refuse (`decodeStrings: false`, `signal`, and others) are refused here too, as are `_writev`, `_construct`, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, strings written to a byte-mode stream in encodings other than UTF-8, and pipes into a destination that overrides `write()` or `end()`.
|
||||
The options the native streams refuse (`signal` and others) are refused here too, as are `_writev` on a Transform, `_final` on a Transform, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, and pipes into a destination that overrides `write()` or `end()`.
|
||||
`_construct` and `_writev` are read when `Readable.call(this, options)` or `Writable.call(this, options)` runs, as Node's constructors read them.
|
||||
Other class constructors passed to `util.inherits` or `util.deprecate` are refused.
|
||||
Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported.
|
||||
A stored `util.deprecate` reports a `length` of 4, where Node reports 3.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
- **Iterable values:** A value typed `Iterable<T>` can hold an array, a Set, a Map (for `Iterable<[K, V]>`), a string (for `Iterable<string>`), a generator, or a Map, Set or array iterator.
|
||||
Class instances with their own `[Symbol.iterator]` method and typed arrays can't be stored in an `Iterable<T>` and are reported at the conversion.
|
||||
@@ -10,9 +10,10 @@ A type whose values are only read field by field, from fields the class does not
|
||||
Object spread of a class instance is refused.
|
||||
Dense array literals created directly in checked storage preserve identity, and supported native handles pass by reference.
|
||||
|
||||
`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in one order per record type rather than per object. scriptc chooses that order from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with.
|
||||
The order matches Node.js when every value of the type is created in the same order.
|
||||
When values of one record type are created in different orders, including values of two types with the same properties and property types, or when a key is deleted and assigned again, the type keeps the declaration order of its first type instead, and `SC6002` reports object literals whose order differs where the program observes it.
|
||||
`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in the order the object created them, as in Node.js.
|
||||
scriptc chooses one order per record type from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with.
|
||||
When the object literals of one record type create its keys in different orders, including literals of two types with the same properties and property types, each value records the order its literal used: copies made by object spread, rest destructuring, and record conversions keep their source's order, and assigning an optional key the object lacks, including after `delete`, moves the key last.
|
||||
A type keeps the declaration order of its first type, and `SC6002` reports object literals whose order differs where the program observes it, when one of its values gets its keys from a spread whose source order is not known, from a spread that also sets an optional key the source lacks, or from a copy of a value scriptc cannot trace, or when the type's values can take more than 16 orders.
|
||||
|
||||
A record converted from a checked value, such as `const rows: { score: number }[] = JSON.parse(text)`, keeps the properties its type does not declare.
|
||||
`JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `util.inspect`, and untyped views of the record see them, together with the declared fields, in the source object's key order.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
- **Local class expressions:** Static fields and static blocks in TypeScript files, decorators, private brands, generic members, and references through the class's internal name are unsupported.
|
||||
- **Local class expressions:** Static fields and static blocks in TypeScript files, decorators, private brands, and generic members are unsupported.
|
||||
Static methods read the enclosing function's bindings and are inherited by subclasses.
|
||||
A named class expression's methods and static members can refer to the class by its name.
|
||||
Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields.
|
||||
Local inheritance requires a compiled base class.
|
||||
Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction.
|
||||
|
||||
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
|
||||
runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" },
|
||||
runtime_pack: {
|
||||
kind: "precompiled",
|
||||
package: "@scriptc/runtime-darwin-arm64",
|
||||
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
() => null,
|
||||
(failure: { stderr?: string }) => failure,
|
||||
);
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v28");
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v33");
|
||||
});
|
||||
});
|
||||
|
||||
+117
-28
@@ -3612,20 +3612,20 @@ declare module "child_process" {
|
||||
|
||||
export interface ChildProcess extends Disposable {
|
||||
ref(): void;
|
||||
on(event: "spawn", listener: () => void): void;
|
||||
once(event: "spawn", listener: () => void): void;
|
||||
on(event: "spawn", listener: () => void): this;
|
||||
once(event: "spawn", listener: () => void): this;
|
||||
/* The exit listener may also take Node's second parameter — the
|
||||
* terminating signal's name, null for a normal exit. */
|
||||
on(event: "exit", listener: (code: number | null, signal: string | null) => void): void;
|
||||
on(event: "close", listener: (code: number | null, signal: string | null) => void): void;
|
||||
on(event: "error", listener: (err: Error) => void): void;
|
||||
once(event: "exit", listener: (code: number | null, signal: string | null) => void): void;
|
||||
once(event: "close", listener: (code: number | null, signal: string | null) => void): void;
|
||||
once(event: "error", listener: (err: Error) => void): void;
|
||||
on(event: "message", listener: (message: any) => void): void;
|
||||
on(event: "disconnect", listener: () => void): void;
|
||||
once(event: "message", listener: (message: any) => void): void;
|
||||
once(event: "disconnect", listener: () => void): void;
|
||||
on(event: "exit", listener: (code: number | null, signal: string | null) => void): this;
|
||||
on(event: "close", listener: (code: number | null, signal: string | null) => void): this;
|
||||
on(event: "error", listener: (err: Error) => void): this;
|
||||
once(event: "exit", listener: (code: number | null, signal: string | null) => void): this;
|
||||
once(event: "close", listener: (code: number | null, signal: string | null) => void): this;
|
||||
once(event: "error", listener: (err: Error) => void): this;
|
||||
on(event: "message", listener: (message: any) => void): this;
|
||||
on(event: "disconnect", listener: () => void): this;
|
||||
once(event: "message", listener: (message: any) => void): this;
|
||||
once(event: "disconnect", listener: () => void): this;
|
||||
/* The lifecycle members, Node's exact shapes: pid is undefined exactly
|
||||
* when the spawn failed; exitCode is null while running, the code
|
||||
* after a normal exit, null for a signal death, and -errno once a
|
||||
@@ -3838,9 +3838,8 @@ declare module "util" {
|
||||
export type TextDecoder = globalThis.TextDecoder;
|
||||
export var TextEncoder: typeof globalThis.TextEncoder;
|
||||
export var TextDecoder: typeof globalThis.TextDecoder;
|
||||
/* stream.pipeline's (...streams: unknown[]) => unknown is assignable to
|
||||
* every other target's parameter type, so its overload comes first; no
|
||||
* other target is assignable to it. */
|
||||
/* stream.pipeline's overload comes first: its __promisify__ member keeps
|
||||
* every other target out of it. */
|
||||
export function promisify(
|
||||
fn: typeof import("node:stream").pipeline,
|
||||
): typeof import("node:stream/promises").pipeline;
|
||||
@@ -7148,7 +7147,11 @@ declare module "stream" {
|
||||
encoding: string,
|
||||
callback: (error?: Error | null) => void,
|
||||
): void;
|
||||
writev?(this: Writable, chunks: unknown[], callback: (error?: Error | null) => void): void;
|
||||
writev?(
|
||||
this: Writable,
|
||||
chunks: Array<{ chunk: any; encoding: string }>,
|
||||
callback: (error?: Error | null) => void,
|
||||
): void;
|
||||
final?(this: Writable, callback: (error?: Error | null) => void): void;
|
||||
destroy?(this: Writable, error: Error | null, callback: (error?: Error | null) => void): void;
|
||||
construct?(this: Writable, callback: (error?: Error | null) => void): void;
|
||||
@@ -7183,7 +7186,11 @@ declare module "stream" {
|
||||
readable?: boolean;
|
||||
writable?: boolean;
|
||||
defaultEncoding?: string;
|
||||
writev?(this: Duplex, chunks: unknown[], callback: (error?: Error | null) => void): void;
|
||||
writev?(
|
||||
this: Duplex,
|
||||
chunks: Array<{ chunk: any; encoding: string }>,
|
||||
callback: (error?: Error | null) => void,
|
||||
): void;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
interface TransformOptions {
|
||||
@@ -7268,6 +7275,36 @@ declare module "stream" {
|
||||
): void;
|
||||
} & ObjectModeFlag;
|
||||
|
||||
/* decodeStrings: false — a string write reaches write/transform as the
|
||||
* string with its encoding (Buffers keep 'buffer'). */
|
||||
interface StringWritableOptions extends Omit<WritableOptions, "decodeStrings" | "write"> {
|
||||
decodeStrings: false;
|
||||
write?(
|
||||
this: Writable,
|
||||
chunk: string | Buffer,
|
||||
encoding: string,
|
||||
callback: (error?: Error | null) => void,
|
||||
): void;
|
||||
}
|
||||
type StringDuplexOptions = Omit<DuplexOptions, "decodeStrings" | "write"> & {
|
||||
decodeStrings: false;
|
||||
write?(
|
||||
this: Duplex,
|
||||
chunk: string | Buffer,
|
||||
encoding: string,
|
||||
callback: (error?: Error | null) => void,
|
||||
): void;
|
||||
};
|
||||
type StringTransformOptions = Omit<TransformOptions, "decodeStrings" | "transform"> & {
|
||||
decodeStrings: false;
|
||||
transform?(
|
||||
this: Transform,
|
||||
chunk: string | Buffer,
|
||||
encoding: string,
|
||||
callback: (error?: Error | null, data?: Buffer | string) => void,
|
||||
): void;
|
||||
};
|
||||
|
||||
/* The operators' options: concurrency and highWaterMark bound map's
|
||||
* in-flight callbacks and queued results. */
|
||||
interface ArrayOptions {
|
||||
@@ -7286,12 +7323,16 @@ declare module "stream" {
|
||||
_destroy(error: Error | null, callback: (error?: Error | null) => void): void;
|
||||
[Symbol.asyncIterator](): AsyncIterableIterator<any>;
|
||||
iterator(options?: { destroyOnReturn?: boolean }): AsyncIterableIterator<any>;
|
||||
/* compose(this, stage), destroyed when options.signal aborts. */
|
||||
compose(stage: ComposeStage, options?: { signal: AbortSignal }): Duplex;
|
||||
push(chunk: Buffer | string | null, encoding?: string): boolean;
|
||||
/* An object-mode stream's values. */
|
||||
push(chunk: any, encoding?: string): boolean;
|
||||
unshift(chunk: Buffer | string, encoding?: string): void;
|
||||
unshift(chunk: any, encoding?: string): void;
|
||||
read(size?: number): Buffer | null;
|
||||
/* A Buffer, a string once encoded, an object-mode value, or null (as
|
||||
* @types/node types it). */
|
||||
read(size?: number): any;
|
||||
pause(): this;
|
||||
resume(): this;
|
||||
isPaused(): boolean;
|
||||
@@ -7309,6 +7350,16 @@ declare module "stream" {
|
||||
readonly destroyed: boolean;
|
||||
readonly errored: Error | null;
|
||||
static from(iterable: unknown, options?: ReadableOptions): Readable;
|
||||
/* A Readable over a web ReadableStream's reader. */
|
||||
static fromWeb(
|
||||
readableStream: globalThis.ReadableStream,
|
||||
options?: {
|
||||
encoding?: string;
|
||||
highWaterMark?: number;
|
||||
objectMode?: boolean;
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
): Readable;
|
||||
/* The Readable operators: chunks are strings or Buffers, map and
|
||||
* flatMap callbacks return strings or Buffers (flatMap: arrays of
|
||||
* them, or strings), and callbacks that declare the options argument
|
||||
@@ -7398,6 +7449,7 @@ declare module "stream" {
|
||||
|
||||
class Writable extends EventEmitter {
|
||||
constructor(opts: ObjectWritableOptions);
|
||||
constructor(opts: StringWritableOptions);
|
||||
constructor(opts?: WritableOptions);
|
||||
/* The underscore-method surface (see Readable). */
|
||||
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void;
|
||||
@@ -7475,7 +7527,11 @@ declare module "stream" {
|
||||
* mirrors @types/node: extends Readable, the writable half declared
|
||||
* again. The lowering treats both halves as first-class. */
|
||||
class Duplex extends Readable {
|
||||
/* Duplex.from: an async generator function, an iterable, a string or
|
||||
* a stream (duplexify.js). */
|
||||
static from(body: unknown): Duplex;
|
||||
constructor(opts: ObjectDuplexOptions);
|
||||
constructor(opts: StringDuplexOptions);
|
||||
constructor(opts?: DuplexOptions);
|
||||
/* The underscore-method surface (see Readable). */
|
||||
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void;
|
||||
@@ -7575,6 +7631,7 @@ declare module "stream" {
|
||||
|
||||
class Transform extends Duplex {
|
||||
constructor(opts: ObjectTransformOptions);
|
||||
constructor(opts: StringTransformOptions);
|
||||
constructor(opts?: TransformOptions);
|
||||
/* The underscore-method surface (see Readable). */
|
||||
_transform(
|
||||
@@ -7586,17 +7643,43 @@ declare module "stream" {
|
||||
}
|
||||
class PassThrough extends Transform {
|
||||
constructor(opts: ObjectTransformOptions);
|
||||
constructor(opts: StringTransformOptions);
|
||||
constructor(opts?: TransformOptions);
|
||||
}
|
||||
|
||||
/* addAbortSignal(signal, stream): the stream, destroyed with an
|
||||
* AbortError (cause: signal.reason) when the signal aborts. */
|
||||
function addAbortSignal<T extends Readable | Writable>(signal: AbortSignal, stream: T): T;
|
||||
/* Declared so requires typecheck; each fences at its use site. */
|
||||
function pipeline(...streams: unknown[]): unknown;
|
||||
/* compose(...streams): a Duplex that writes to the first stream and
|
||||
* reads from the last, with the streams running as a pipeline. */
|
||||
function compose(...streams: Array<Readable | Writable>): Duplex;
|
||||
/* pipeline(...stages, callback): the last stream, or the object-mode
|
||||
* PassThrough that a function destination feeds. */
|
||||
type PipelineResult<T extends unknown[]> = T extends [
|
||||
...unknown[],
|
||||
infer D,
|
||||
(...args: any[]) => unknown,
|
||||
]
|
||||
? D extends (...args: any[]) => unknown
|
||||
? PassThrough
|
||||
: D
|
||||
: unknown;
|
||||
function pipeline<T extends unknown[]>(...streams: T): PipelineResult<T>;
|
||||
/* util.promisify's pipeline overload matches only pipeline itself: a
|
||||
* generic signature alone admits every function. */
|
||||
namespace pipeline {
|
||||
const __promisify__: typeof promises.pipeline;
|
||||
}
|
||||
/* A compose stage: a stream, an async generator function over the
|
||||
* previous stage, or (first) an iterable, async iterable or web
|
||||
* ReadableStream. */
|
||||
type ComposeStage =
|
||||
| Readable
|
||||
| Writable
|
||||
| Iterable<unknown>
|
||||
| AsyncIterable<unknown>
|
||||
| globalThis.ReadableStream
|
||||
| ((source: any, options?: { signal: AbortSignal }) => unknown);
|
||||
/* compose(...streams): a Duplex that writes to the first stage and
|
||||
* reads from the last, with the stages running as a pipeline. */
|
||||
function compose(...streams: ComposeStage[]): Duplex;
|
||||
/* finished's options: readable/writable choose which ends count,
|
||||
* error: false ignores 'error' events, signal aborts the wait. */
|
||||
interface FinishedOptions {
|
||||
@@ -7615,13 +7698,20 @@ declare module "stream" {
|
||||
* else 65536 — Node's own state.js split); literal argument only. */
|
||||
function getDefaultHighWaterMark(objectMode: boolean): number;
|
||||
/* The promise pipeline's result: a last stage that is an async
|
||||
* function resolves the promise with its value. */
|
||||
* function resolves the promise with its value (before the options
|
||||
* argument, when one follows). */
|
||||
type PipelinePromise<T extends unknown[]> = T extends [
|
||||
...unknown[],
|
||||
(...args: any[]) => Promise<infer P>,
|
||||
]
|
||||
? Promise<P>
|
||||
: Promise<void>;
|
||||
: T extends [
|
||||
...unknown[],
|
||||
(...args: any[]) => Promise<infer P>,
|
||||
{ signal?: AbortSignal; end?: boolean },
|
||||
]
|
||||
? Promise<P>
|
||||
: Promise<void>;
|
||||
namespace promises {
|
||||
function pipeline<T extends unknown[]>(...streams: T): PipelinePromise<T>;
|
||||
function finished(
|
||||
@@ -7651,6 +7741,7 @@ declare module "stream" {
|
||||
DuplexOptions,
|
||||
TransformOptions,
|
||||
FinishedOptions,
|
||||
PipelinePromise,
|
||||
};
|
||||
}
|
||||
declare module "node:stream" {
|
||||
@@ -7665,9 +7756,7 @@ declare module "stream/promises" {
|
||||
* pipeline's trailing { signal, end } object and finished's options
|
||||
* (readable, writable, error, cleanup, signal) are supported. A last
|
||||
* stage that is an async function resolves the promise with its value. */
|
||||
function pipeline<T extends unknown[]>(
|
||||
...streams: T
|
||||
): T extends [...unknown[], (...args: any[]) => Promise<infer P>] ? Promise<P> : Promise<void>;
|
||||
function pipeline<T extends unknown[]>(...streams: T): import("stream").PipelinePromise<T>;
|
||||
function finished(
|
||||
stream: unknown,
|
||||
options?: {
|
||||
|
||||
@@ -10,9 +10,10 @@ import type {
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
|
||||
/** Field numbers of %ScrArr, which mirrors scr_runtime.h's ScrArr field for
|
||||
* field (its static assertions pin `len`, `cap`, `cyc`, `filled` and
|
||||
* `data`). `len` and `cap` are i32 on every target; the dense states end
|
||||
* the allocation after the `cap` value slots at `data` (scrArrStates). */
|
||||
* field (its static assertions pin `len`, `cap`, `cyc`, `spare`, `filled`
|
||||
* and `data`). `len` and `cap` are i32 on every target; the dense states of
|
||||
* an array with states (SCR_ARR_STATES in `spare`) end the allocation after
|
||||
* the `cap` value slots at `data` (scrArrStates). */
|
||||
export const SCR_ARR_FIELD = {
|
||||
rc: 0,
|
||||
len: 1,
|
||||
@@ -24,6 +25,10 @@ export const SCR_ARR_FIELD = {
|
||||
data: 7,
|
||||
} as const;
|
||||
|
||||
/** The `spare` flag of a %ScrArr whose dense slots carry state bytes
|
||||
* (SCR_ARR_STATES in scr_array.h). */
|
||||
export const SCR_ARR_STATES = 16;
|
||||
|
||||
/** Loads the i32 `len` or `cap` of a %ScrArr at `ptr` into `dest` as a
|
||||
* size-type count. */
|
||||
export function loadArrCount(
|
||||
|
||||
@@ -316,6 +316,55 @@ test.each([32, 64] as const)(
|
||||
},
|
||||
);
|
||||
|
||||
test("arrays without state bytes skip the state stores", () => {
|
||||
const read: IrExpr = { kind: "arrayGet", arr: ref("s", words), index, type: STRING, loc };
|
||||
const write: IrStmt = {
|
||||
kind: "arraySet",
|
||||
arr: ref("s", words),
|
||||
index,
|
||||
value: { kind: "strLit", value: "x", type: STRING, loc },
|
||||
loc,
|
||||
};
|
||||
const push: IrStmt = {
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: ref("a", nums),
|
||||
args: [{ kind: "numLit", value: 2.5, type: F64, loc }],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
};
|
||||
const llvm = emitLlvmModule(
|
||||
module("plain", STRING, [push, write, { kind: "return", value: read, loc }]),
|
||||
);
|
||||
const fn = body(llvm, "plain");
|
||||
// SCR_ARR_STATES (16) in `spare`, field 5.
|
||||
const flag = String.raw`getelementptr inbounds %ScrArr, ptr %t\d+, i32 0, i32 5\n %t\d+ = load i16, ptr %t\d+, !tbaa !\d+\n %t\d+ = and i16 %t\d+, 16\n %t\d+ = icmp ne i16 %t\d+, 0\n`;
|
||||
// A push marks the state only when the array has states.
|
||||
expect(fn).toMatch(
|
||||
new RegExp(
|
||||
`${flag} br i1 %t\\d+, label %arr\\.push\\.state\\d+, label %arr\\.push\\.marked\\d+\\narr\\.push\\.state\\d+:\\n store i8 1, ptr `,
|
||||
),
|
||||
);
|
||||
// Above the prefix, an array without states has no elements (its prefix
|
||||
// is its length): a store there appends at the length and replaces no
|
||||
// value.
|
||||
expect(fn).toMatch(
|
||||
new RegExp(
|
||||
`${flag} br i1 %t\\d+, label %arr\\.set\\.states\\d+, label %arr\\.set\\.plain\\d+`,
|
||||
),
|
||||
);
|
||||
expect(fn).toMatch(
|
||||
/^arr\.set\.plain\d+:\n %t\d+ = icmp eq i64 .*\n br i1 %t\d+, label %arr\.set\.values\d+, label %arr\.set\.slow\d+$/m,
|
||||
);
|
||||
expect(fn).toMatch(/phi i1 \[ %t\d+, %arr\.set\.states\d+ \], \[ false, %arr\.set\.values\d+ \]/);
|
||||
// Reads test no flag: they reach the state check only above the prefix.
|
||||
expect(fn).not.toMatch(/label %arr\.get\.state\d+/);
|
||||
});
|
||||
|
||||
test("state and presence queries answer dense slots inline", () => {
|
||||
const state: IrExpr = { kind: "arrayState", arr: ref("b", flags), index, type: F64, loc };
|
||||
const has: IrExpr = { kind: "arrayHas", arr: ref("s", words), index, type: BOOL, loc };
|
||||
|
||||
@@ -14,13 +14,17 @@
|
||||
* state bytes altogether: the runtime keeps every state below it a present
|
||||
* value, so reads load the slot directly and writes replace it without
|
||||
* touching its state. Other dense indices take the state-checked path,
|
||||
* whose writes extend the bound when they continue it. */
|
||||
* whose writes extend the bound when they continue it. An array without
|
||||
* state bytes (SCR_ARR_STATES clear in `spare`) keeps its bound at its
|
||||
* length, so reads never reach the state check for it; a write past the
|
||||
* bound tests the flag and, without states, appends at the length. */
|
||||
|
||||
import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import {
|
||||
f64Lit,
|
||||
loadArrCount,
|
||||
SCR_ARR_FIELD,
|
||||
SCR_ARR_STATES,
|
||||
scrArrStatePtr,
|
||||
scrArrStates,
|
||||
sizeToDouble,
|
||||
@@ -34,6 +38,24 @@ import { integerOrNaNIndex } from "./wrapping-sums.js";
|
||||
|
||||
const SCR_ARR_VALUE = 1;
|
||||
|
||||
/** Whether the array at `array` has state bytes, as an i1. `spare` changes
|
||||
* only in runtime calls, so its loads take their own alias tag. */
|
||||
export function emitArrayHasStates(host: LlvmEmitterContext, array: string): string {
|
||||
const B = host.B;
|
||||
const sparePtr = B.tmp(),
|
||||
spare = B.tmp(),
|
||||
bit = B.tmp(),
|
||||
has = B.tmp();
|
||||
B.line(
|
||||
`${sparePtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.spare}`,
|
||||
);
|
||||
host.markMemoryPointer(sparePtr, "array:spare");
|
||||
B.line(`${spare} = load i16, ptr ${sparePtr}${host.fieldAliasAttachment(sparePtr)}`);
|
||||
B.line(`${bit} = and i16 ${spare}, ${SCR_ARR_STATES}`);
|
||||
B.line(`${has} = icmp ne i16 ${bit}, 0`);
|
||||
return has;
|
||||
}
|
||||
|
||||
/** Call-site effects of the non-retaining read fallbacks: they only read
|
||||
* module-visible memory, or trap without returning to the caller (the trap
|
||||
* writes runtime-private stderr/TLS state). Declaring this lets LLVM keep
|
||||
@@ -516,19 +538,49 @@ export function emitDenseArraySet(
|
||||
B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
}
|
||||
};
|
||||
const stated = B.newLabel("arr.set.states"),
|
||||
plain = B.newLabel("arr.set.plain"),
|
||||
plainStore = B.newLabel("arr.set.values"),
|
||||
stored = B.newLabel("arr.set.stored.slot");
|
||||
B.condBr(emitArrayHasStates(host, array), stated, plain);
|
||||
B.startBlock(stated);
|
||||
const statePtr = statePointer(host, guard);
|
||||
const valuePtr = valuePointer(host, guard, kind);
|
||||
let oldState: string | null = null;
|
||||
let old: string | null = null;
|
||||
let statedOld: string | null = null;
|
||||
let statedHad: string | null = null;
|
||||
// Compact instances are never released: a replaced one needs nothing.
|
||||
if (kind === "ref") {
|
||||
oldState = B.tmp();
|
||||
old = B.tmp();
|
||||
const oldState = B.tmp();
|
||||
statedOld = B.tmp();
|
||||
statedHad = B.tmp();
|
||||
B.line(`${oldState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
|
||||
B.line(`${old} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
B.line(`${statedOld} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
B.line(`${statedHad} = icmp eq i8 ${oldState}, ${SCR_ARR_VALUE}`);
|
||||
}
|
||||
storeValue(valuePtr);
|
||||
B.line(`store i8 ${SCR_ARR_VALUE}, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
|
||||
const statedEnd = B.currentLabel();
|
||||
B.br(stored);
|
||||
// Without states the bound is the length, so a store here appends at
|
||||
// the length (it replaces no value) or leaves holes, which the runtime
|
||||
// gives states.
|
||||
B.startBlock(plain);
|
||||
const appends = B.tmp();
|
||||
B.line(`${appends} = icmp eq ${host.sizeType} ${offset}, ${len}`);
|
||||
B.condBr(appends, plainStore, slow);
|
||||
B.startBlock(plainStore);
|
||||
storeValue(valuePointer(host, guard, kind));
|
||||
const plainEnd = B.currentLabel();
|
||||
B.br(stored);
|
||||
B.startBlock(stored);
|
||||
let old: string | null = null;
|
||||
let hadValue: string | null = null;
|
||||
if (kind === "ref") {
|
||||
old = B.tmp();
|
||||
hadValue = B.tmp();
|
||||
B.line(`${old} = phi ptr [ ${statedOld}, %${statedEnd} ], [ null, %${plainEnd} ]`);
|
||||
B.line(`${hadValue} = phi i1 [ ${statedHad}, %${statedEnd} ], [ false, %${plainEnd} ]`);
|
||||
}
|
||||
// A store at the bound continues the hole-free prefix.
|
||||
const filledWide = sizedFilled(host, guard.filled),
|
||||
continues = B.tmp();
|
||||
@@ -572,9 +624,7 @@ export function emitDenseArraySet(
|
||||
storeArrCount(host, nextLen, lenPtr, host.fieldAliasAttachment(lenPtr));
|
||||
B.br(storedLabel);
|
||||
B.startBlock(storedLabel);
|
||||
if (kind === "ref" && oldState && old) {
|
||||
const hadValue = B.tmp();
|
||||
B.line(`${hadValue} = icmp eq i8 ${oldState}, ${SCR_ARR_VALUE}`);
|
||||
if (kind === "ref" && hadValue && old) {
|
||||
const releaseLabel = B.newLabel("arr.set.release");
|
||||
B.condBr(hadValue, releaseLabel, join);
|
||||
B.startBlock(releaseLabel);
|
||||
@@ -671,7 +721,14 @@ export function emitDenseArrayPop(
|
||||
raw = address;
|
||||
}
|
||||
B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
// Only an array with states has a state byte to clear.
|
||||
const clear = B.newLabel("arr.pop.state"),
|
||||
cleared = B.newLabel("arr.pop.cleared");
|
||||
B.condBr(emitArrayHasStates(host, array), clear, cleared);
|
||||
B.startBlock(clear);
|
||||
B.line(`store i8 0, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
|
||||
B.br(cleared);
|
||||
B.startBlock(cleared);
|
||||
let last32 = last;
|
||||
if (size === "i64") {
|
||||
last32 = B.tmp();
|
||||
@@ -748,7 +805,14 @@ export function emitDenseArrayPush(
|
||||
} else {
|
||||
B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
}
|
||||
// Only an array with states has a state byte to mark.
|
||||
const mark = B.newLabel("arr.push.state"),
|
||||
marked = B.newLabel("arr.push.marked");
|
||||
B.condBr(emitArrayHasStates(host, array), mark, marked);
|
||||
B.startBlock(mark);
|
||||
B.line(`store i8 ${SCR_ARR_VALUE}, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
|
||||
B.br(marked);
|
||||
B.startBlock(marked);
|
||||
const next = B.tmp(),
|
||||
fastValue = B.tmp();
|
||||
B.line(`${next} = add nuw ${size} ${len}, 1`);
|
||||
@@ -766,6 +830,7 @@ export function emitDenseArrayPush(
|
||||
B.line(`${bumped} = select i1 ${packed}, i32 ${nextFilled}, i32 ${filled}`);
|
||||
B.line(`store i32 ${bumped}, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
|
||||
B.line(`${fastValue} = ${sizeToDouble(size)} ${size} ${next} to double`);
|
||||
const fastEnd = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(slow);
|
||||
const slowValue = B.tmp();
|
||||
@@ -773,6 +838,6 @@ export function emitDenseArrayPush(
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = phi double [ ${fastValue}, %${fast} ], [ ${slowValue}, %${slow} ]`);
|
||||
B.line(`${result} = phi double [ ${fastValue}, %${fastEnd} ], [ ${slowValue}, %${slow} ]`);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { emitPerKeyOrder } from "./key-orders.js";
|
||||
import { typedRefConstructor } from "./shapes.js";
|
||||
import { bindLiveArrayOps } from "./expr-stream-bridges.js";
|
||||
import { BYTES_ELEM_NUM, SCR_ARR_FIELD, f64Lit, sizeToDouble } from "./common.js";
|
||||
import { BYTES_ELEM_NUM, SCR_ARR_FIELD, SCR_ARR_STATES, f64Lit, sizeToDouble } from "./common.js";
|
||||
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
||||
import {
|
||||
INLINE_UNION_FIELD,
|
||||
@@ -37,6 +38,8 @@ import {
|
||||
accessorSlotProp,
|
||||
DYN_HANDLE_KINDS,
|
||||
isDirentShape,
|
||||
KEY_ORDER_FIELD,
|
||||
shapeKeyOrders,
|
||||
isDynTypedRefType,
|
||||
isRefCounted,
|
||||
isUnitType,
|
||||
@@ -1107,9 +1110,10 @@ export class LlDyn {
|
||||
}
|
||||
B.condBr(readable, l0, fail);
|
||||
B.startBlock(l0);
|
||||
// dyn ('unknown') fields match ANY value, present or missing.
|
||||
// dyn ('unknown') fields match ANY value, present or missing. The
|
||||
// order index is no key.
|
||||
for (const f of shape.fields) {
|
||||
if (f.type.kind === "dyn") continue;
|
||||
if (f.type.kind === "dyn" || f.name === KEY_ORDER_FIELD) continue;
|
||||
const { value: m, release } = this.memberRead(B, f.name, f.type);
|
||||
const lNext = B.newLabel("dm.n");
|
||||
this.pendingBail(
|
||||
@@ -1292,9 +1296,10 @@ export class LlDyn {
|
||||
return (
|
||||
shape.fields.every(
|
||||
(f) =>
|
||||
!f.name.startsWith("%") &&
|
||||
f.name !== "__proto__" &&
|
||||
this.jsonSchemaSupported(f.type, visiting, true),
|
||||
f.name === KEY_ORDER_FIELD ||
|
||||
(!f.name.startsWith("%") &&
|
||||
f.name !== "__proto__" &&
|
||||
this.jsonSchemaSupported(f.type, visiting, true)),
|
||||
) &&
|
||||
(!index || this.jsonSchemaSupported(index, visiting))
|
||||
);
|
||||
@@ -1380,7 +1385,9 @@ export class LlDyn {
|
||||
const base = host.nullableUnions.fieldBase(t.shapeId);
|
||||
const index = new Map(shape.fields.map((f, i) => [f.name, i + base]));
|
||||
// The runtime names kept-source keys by this index (record-sources.ts).
|
||||
const order = recordSourceFieldOrder(shape);
|
||||
// The order index is no key: parsed values keep the table's order
|
||||
// and their source's own order.
|
||||
const order = recordSourceFieldOrder(shape).filter((name) => name !== KEY_ORDER_FIELD);
|
||||
if (recordMayKeepSource(shape)) this.keepsRecordSources = true;
|
||||
const fieldTy = `{ ptr, ${S}, ${S}, ptr }`;
|
||||
const entries = order.map((fieldName) => {
|
||||
@@ -1780,6 +1787,32 @@ export class LlDyn {
|
||||
B.line(`store ${member.type} ${oldValue}, ptr ${checkedPtr}`);
|
||||
});
|
||||
if (t.kind !== "record") {
|
||||
// Whether the dense slots carry states (SCR_ARR_STATES in
|
||||
// `spare`) moves with them; the rest of `spare` stays.
|
||||
const cachedSpare = B.tmp(),
|
||||
checkedSpare = B.tmp();
|
||||
const sparePtr = (array: string): string => {
|
||||
const ptr = B.tmp();
|
||||
B.line(
|
||||
`${ptr} = getelementptr inbounds %ScrArr, ptr ${array}, i64 0, i32 ${SCR_ARR_FIELD.spare}`,
|
||||
);
|
||||
return ptr;
|
||||
};
|
||||
const cachedSparePtr = sparePtr(cached),
|
||||
checkedSparePtr = sparePtr(checked);
|
||||
B.line(`${cachedSpare} = load i16, ptr ${cachedSparePtr}`);
|
||||
B.line(`${checkedSpare} = load i16, ptr ${checkedSparePtr}`);
|
||||
const exchange = (own: string, other: string, ptr: string): void => {
|
||||
const kept = B.tmp(),
|
||||
moved = B.tmp(),
|
||||
next = B.tmp();
|
||||
B.line(`${kept} = and i16 ${own}, ${~SCR_ARR_STATES}`);
|
||||
B.line(`${moved} = and i16 ${other}, ${SCR_ARR_STATES}`);
|
||||
B.line(`${next} = or i16 ${kept}, ${moved}`);
|
||||
B.line(`store i16 ${next}, ptr ${ptr}`);
|
||||
};
|
||||
exchange(cachedSpare, checkedSpare, cachedSparePtr);
|
||||
exchange(checkedSpare, cachedSpare, checkedSparePtr);
|
||||
// Sparse slots and numeric properties live out of line; each
|
||||
// array keeps its own match metadata.
|
||||
host.declare("declare void @scr_arr_swap_indexed_ext(ptr, ptr)");
|
||||
@@ -2130,7 +2163,10 @@ export class LlDyn {
|
||||
}
|
||||
B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`);
|
||||
if (recursive) this.recordBuilt(B, name, "%r0");
|
||||
// The order index stays 0: the source's own order is kept below
|
||||
// when it differs from the table.
|
||||
for (const f of shape.fields) {
|
||||
if (f.name === KEY_ORDER_FIELD) continue;
|
||||
const { value: m, release } = this.memberRead(B, f.name, f.type);
|
||||
this.pendingBail(
|
||||
B,
|
||||
@@ -2863,70 +2899,79 @@ export class LlDyn {
|
||||
host.declare(`declare ptr @scr_dyn_new_obj()`);
|
||||
B.line(`${d} = call ptr @scr_dyn_new_obj()`);
|
||||
}
|
||||
// Keys insert in DECLARED order (JS insertion order); internal
|
||||
// '%'-fields follow so a record→dyn→record round trip keeps them.
|
||||
// Keys insert in DECLARED order (JS insertion order), or in the
|
||||
// value's own order for a shape with orderVariants; internal
|
||||
// '%'-fields follow so a record→dyn→record round trip keeps them
|
||||
// (all but the order index, which is no key).
|
||||
const byName = new Map(shape.fields.map((f) => [f.name, f]));
|
||||
// An accessor property is one key whose getter and setter are the
|
||||
// slot closures (see accessorSlotProp), in accessorOrder's place.
|
||||
const order = shape.accessorOrder ?? shape.declaredOrder ?? shape.fields.map((f) => f.name);
|
||||
const inOrder = new Set(order);
|
||||
const accessors = new Set(
|
||||
shape.accessorOrder === undefined
|
||||
? []
|
||||
: shape.fields.flatMap((f) => {
|
||||
const slot = accessorSlotProp(f.name);
|
||||
return slot === null ? [] : [slot.prop];
|
||||
}),
|
||||
);
|
||||
type DynField = { name: string; type: IrType } | { accessor: string };
|
||||
const dynFields: DynField[] = [
|
||||
...order.flatMap((n) => {
|
||||
if (accessors.has(n)) return [{ accessor: n } as DynField];
|
||||
const f = byName.get(n);
|
||||
return f === undefined ? [] : [f as DynField];
|
||||
}),
|
||||
...shape.fields.filter(
|
||||
(f) =>
|
||||
!inOrder.has(f.name) && !(accessors.size > 0 && accessorSlotProp(f.name) !== null),
|
||||
),
|
||||
];
|
||||
for (const entry of dynFields) {
|
||||
if ("accessor" in entry) {
|
||||
host.declare(
|
||||
`declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`,
|
||||
);
|
||||
const boxed = (kind: "get" | "set"): string => {
|
||||
const slot = byName.get(`%${kind}:${entry.accessor}`);
|
||||
if (slot === undefined) return "null";
|
||||
const fv = loadFieldOf(slot.name, slot.type);
|
||||
const conv = B.tmp();
|
||||
B.line(
|
||||
`${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`,
|
||||
);
|
||||
return conv;
|
||||
};
|
||||
const getter = boxed("get");
|
||||
const setter = boxed("set");
|
||||
B.line(
|
||||
`call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const f = entry;
|
||||
const klen = Buffer.byteLength(f.name, "utf8");
|
||||
const fv = loadFieldOf(f.name, f.type);
|
||||
// An ABSENT optional field contributes no key.
|
||||
const lSkip = f.type.kind === "union" ? this.skipIfAbsent(B, fv, f.type) : null;
|
||||
const conv = B.tmp();
|
||||
B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`);
|
||||
B.line(
|
||||
`call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`,
|
||||
const orders =
|
||||
shape.accessorOrder !== undefined ? [shape.accessorOrder] : shapeKeyOrders(shape);
|
||||
const perOrder = (order: readonly string[]): void => {
|
||||
const inOrder = new Set(order);
|
||||
const accessors = new Set(
|
||||
shape.accessorOrder === undefined
|
||||
? []
|
||||
: shape.fields.flatMap((f) => {
|
||||
const slot = accessorSlotProp(f.name);
|
||||
return slot === null ? [] : [slot.prop];
|
||||
}),
|
||||
);
|
||||
if (lSkip !== null) {
|
||||
B.br(lSkip);
|
||||
B.startBlock(lSkip);
|
||||
type DynField = { name: string; type: IrType } | { accessor: string };
|
||||
const dynFields: DynField[] = [
|
||||
...order.flatMap((n) => {
|
||||
if (accessors.has(n)) return [{ accessor: n } as DynField];
|
||||
const f = byName.get(n);
|
||||
return f === undefined ? [] : [f as DynField];
|
||||
}),
|
||||
...shape.fields.filter(
|
||||
(f) =>
|
||||
!inOrder.has(f.name) &&
|
||||
f.name !== KEY_ORDER_FIELD &&
|
||||
!(accessors.size > 0 && accessorSlotProp(f.name) !== null),
|
||||
),
|
||||
];
|
||||
for (const entry of dynFields) {
|
||||
if ("accessor" in entry) {
|
||||
host.declare(
|
||||
`declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`,
|
||||
);
|
||||
const boxed = (kind: "get" | "set"): string => {
|
||||
const slot = byName.get(`%${kind}:${entry.accessor}`);
|
||||
if (slot === undefined) return "null";
|
||||
const fv = loadFieldOf(slot.name, slot.type);
|
||||
const conv = B.tmp();
|
||||
B.line(
|
||||
`${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`,
|
||||
);
|
||||
return conv;
|
||||
};
|
||||
const getter = boxed("get");
|
||||
const setter = boxed("set");
|
||||
B.line(
|
||||
`call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const f = entry;
|
||||
const klen = Buffer.byteLength(f.name, "utf8");
|
||||
const fv = loadFieldOf(f.name, f.type);
|
||||
// An ABSENT optional field contributes no key.
|
||||
const lSkip = f.type.kind === "union" ? this.skipIfAbsent(B, fv, f.type) : null;
|
||||
const conv = B.tmp();
|
||||
B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`);
|
||||
B.line(
|
||||
`call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`,
|
||||
);
|
||||
if (lSkip !== null) {
|
||||
B.br(lSkip);
|
||||
B.startBlock(lSkip);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (orders.length === 1) perOrder(orders[0]!);
|
||||
else emitPerKeyOrder(B, shape, base, "%v", perOrder);
|
||||
// A Dirent row takes Node's Dirent prototype (the type probes) and
|
||||
// its Symbol(type) key; %dtype stays readable but non-enumerable.
|
||||
if (isDirentShape(shape)) {
|
||||
|
||||
@@ -847,21 +847,19 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
for (const code of regexMatchers.values()) out.push(...code.lines, ``);
|
||||
for (const [, inst] of emitter.templateStringsInstances) {
|
||||
// One immortal ScrArr per tagged-template site: a data global of
|
||||
// interned cooked-string literals followed by their states (the
|
||||
// runtime finds them before data + cap * 9), and the ScrArr header over
|
||||
// it (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, no REF
|
||||
// entry points). Every dense slot is present; reads retain immortal
|
||||
// strings — a no-op. Slots are 8 bytes on every target: on 32-bit
|
||||
// targets each holds the pointer in its low half and zero in its high
|
||||
// half, so the states start where the runtime looks for them.
|
||||
// interned cooked-string literals, and the ScrArr header over it
|
||||
// (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, no REF entry
|
||||
// points, no state bytes: every dense slot is present). Reads retain
|
||||
// immortal strings — a no-op. Slots are 8 bytes on every target: on
|
||||
// 32-bit targets each holds the pointer in its low half and zero in its
|
||||
// high half.
|
||||
const n = inst.slots.length;
|
||||
const present = n === 0 ? "zeroinitializer" : `[ ${inst.slots.map(() => "i8 1").join(", ")} ]`;
|
||||
const wide = emitter.sizeType === "i64";
|
||||
const slot = wide ? "ptr" : "<{ ptr, i32 }>";
|
||||
const slotValue = (s: string): string =>
|
||||
wide ? `ptr ${s}` : `<{ ptr, i32 }> <{ ptr ${s}, i32 0 }>`;
|
||||
out.push(
|
||||
`@${inst.sym}_data = internal constant <{ [${n} x ${slot}], [${n} x i8] }> <{ [${n} x ${slot}] [ ${inst.slots.map(slotValue).join(", ")} ], [${n} x i8] ${present} }>, align 8`,
|
||||
`@${inst.sym}_data = internal constant [${n} x ${slot}] [ ${inst.slots.map(slotValue).join(", ")} ], align 8`,
|
||||
`@${inst.sym} = internal global %ScrArr { ${emitter.sizeType} -1, i32 ${n}, i32 ${n}, i8 2, i8 0, i16 0, i32 ${n}, ptr @${inst.sym}_data }`,
|
||||
);
|
||||
}
|
||||
@@ -875,8 +873,6 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
out.push(`@${sym} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr } { ${body} }`);
|
||||
}
|
||||
if (emitter.mapOpsConsts.size > 0) out.push(``);
|
||||
for (const sym of emitter.methodIdSlots.values()) out.push(`@${sym} = internal global i32 -1`);
|
||||
if (emitter.methodIdSlots.size > 0) out.push(``);
|
||||
|
||||
for (const line of ffiCallbacks.globals) out.push(line);
|
||||
if (ffiCallbacks.globals.length > 0) out.push(``);
|
||||
|
||||
@@ -440,6 +440,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
const length = emitter.emitExpr(s.length);
|
||||
if (s.arr.type.kind !== "array")
|
||||
throw new InternalCompilerError("llvm emitter bug: arraySetLength on non-array");
|
||||
if (s.reserve === true) {
|
||||
emitter.declare(`declare void @scr_arr_reserve_len(ptr, double)`);
|
||||
B.line(`call void @scr_arr_reserve_len(ptr ${arr.name}, double ${length.name})`);
|
||||
break;
|
||||
}
|
||||
emitter.emitPublishedArrayGuard(arr.name, s.arr.type, s.pop === true ? 2 : 3);
|
||||
emitter.declare(`declare void @scr_arr_set_len(ptr, double)`);
|
||||
// Assigning the current length (a reused work array reset to where
|
||||
|
||||
@@ -832,10 +832,6 @@ export class LlEmitter {
|
||||
readonly cstrs = new Map<string, { sym: string; len: number }>();
|
||||
/** Interned ScrMapOps descriptors (scr_map.h): initializer → symbol. */
|
||||
readonly mapOpsConsts = new Map<string, string>();
|
||||
/** Method-id slots of checked-dynamic method calls
|
||||
* (scr_dyn_invoke_prepared_id): method name → symbol of an i32 the
|
||||
* runtime fills with the name's id on the first call. */
|
||||
readonly methodIdSlots = new Map<string, string>();
|
||||
/** Type-directed walker functions (JSON serializers, the indent
|
||||
* rewriter, union ToString/join) — interned per typeKey/unionId, defs
|
||||
* flushed with the shape helpers. */
|
||||
@@ -1029,6 +1025,11 @@ export class LlEmitter {
|
||||
get usesDynBuiltinStream(): boolean {
|
||||
return this.runtimeFeatures.dynBuiltinStream;
|
||||
}
|
||||
/** A checked-dynamic for-await may hold a native stream: the program
|
||||
* links the stream unit (and the dyn-async one the loop needs). */
|
||||
get dynForAwaitStreams(): boolean {
|
||||
return this.runtimeFeatures.stream && this.runtimeFeatures.dynAsync;
|
||||
}
|
||||
/** Runtime-arm dispatchers for live union values. */
|
||||
readonly liveDynUnionRefAdapters = new Map<string, string>();
|
||||
readonly dynPromiseAdapters = new Map<string, string>();
|
||||
@@ -2474,16 +2475,6 @@ export class LlEmitter {
|
||||
return `@${c.sym}`;
|
||||
}
|
||||
|
||||
/** The method-id slot for a constant method name (methodIdSlots). */
|
||||
methodIdSlot(name: string): string {
|
||||
let sym = this.methodIdSlots.get(name);
|
||||
if (!sym) {
|
||||
sym = this.contentSymbol("sc_mid_", name);
|
||||
this.methodIdSlots.set(name, sym);
|
||||
}
|
||||
return `@${sym}`;
|
||||
}
|
||||
|
||||
needBadTag(): void {
|
||||
this.needsBadTag = true;
|
||||
}
|
||||
@@ -6474,7 +6465,10 @@ export class LlEmitter {
|
||||
return streamDataAdapter(this, cbT);
|
||||
}
|
||||
|
||||
streamDoneFnFor(kind: "w" | "f" | "d" | "t" | "l", doneT: IrType & { kind: "func" }): string {
|
||||
streamDoneFnFor(
|
||||
kind: "w" | "f" | "d" | "t" | "l" | "c",
|
||||
doneT: IrType & { kind: "func" },
|
||||
): string {
|
||||
return streamDoneFnFor(this, kind, doneT);
|
||||
}
|
||||
|
||||
@@ -6498,7 +6492,7 @@ export class LlEmitter {
|
||||
return zlibBytesThunkFor(this, cbT);
|
||||
}
|
||||
|
||||
streamCbThunkFor(kind: "r" | "w" | "f" | "d" | "t" | "l" | "e", cbT: IrType): string {
|
||||
streamCbThunkFor(kind: "r" | "w" | "f" | "d" | "t" | "l" | "e" | "c" | "v", cbT: IrType): string {
|
||||
return streamCbThunkFor(this, kind, cbT);
|
||||
}
|
||||
|
||||
|
||||
@@ -250,10 +250,8 @@ export function emitDynamicExpr(
|
||||
}
|
||||
// Buffer.prototype method names take their scr_bytes_dyn.c entry, which
|
||||
// runs them on Buffer receivers and dispatches any other receiver.
|
||||
// Each call passes its method name's id slot, so only the first call
|
||||
// looks the name up.
|
||||
const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id";
|
||||
host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared";
|
||||
host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
let name = host.cstr(e.calleeName);
|
||||
if (e.calleeNameValue !== undefined) {
|
||||
const value = emitBorrowedInput(host, e.calleeNameValue);
|
||||
@@ -262,7 +260,7 @@ export function emitDynamicExpr(
|
||||
}
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`,
|
||||
`${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`,
|
||||
);
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
|
||||
@@ -312,7 +312,7 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits"
|
||||
});
|
||||
|
||||
test.each([32, 64] as const)(
|
||||
"tagged-template string arrays keep 8-byte slots before their states on %i-bit targets",
|
||||
"tagged-template string arrays keep 8-byte slots without states on %i-bit targets",
|
||||
(bits) => {
|
||||
const strings: IrExpr = {
|
||||
kind: "templateStrings",
|
||||
@@ -325,14 +325,17 @@ test.each([32, 64] as const)(
|
||||
mod.functions[0]!.body = [{ kind: "exprStmt", expr: strings, loc }];
|
||||
const llvm = emitLlvmModule(mod, { pointerBits: bits });
|
||||
const data = /^@sc_tsa_0_data = internal constant .*$/m.exec(llvm)![0];
|
||||
// The runtime reads each slot as 8 bytes and finds the states after
|
||||
// cap * 8 bytes of slots (scr_arr_states_end).
|
||||
if (bits === 64) expect(data).toContain("<{ [3 x ptr], [3 x i8] }>");
|
||||
// The runtime reads each slot as 8 bytes; an array without states
|
||||
// (`spare` 0 in its header) has no state bytes after them.
|
||||
if (bits === 64) expect(data).toMatch(/^\S+ = internal constant \[3 x ptr\] /);
|
||||
else {
|
||||
expect(data).toContain("<{ [3 x <{ ptr, i32 }>], [3 x i8] }>");
|
||||
expect(data).toMatch(/^\S+ = internal constant \[3 x <\{ ptr, i32 \}>\] /);
|
||||
expect(data.match(/<\{ ptr @\S+, i32 0 \}>/g)).toHaveLength(3);
|
||||
}
|
||||
expect(data).toContain("[3 x i8] [ i8 1, i8 1, i8 1 ]");
|
||||
expect(data).not.toContain("x i8]");
|
||||
expect(llvm).toMatch(
|
||||
/^@sc_tsa_0 = internal global %ScrArr \{ i\d+ -1, i32 3, i32 3, i8 2, i8 0, i16 0, i32 3, /m,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ test("live record capsules bind field hooks in snapshot order", () => {
|
||||
const llvm = emitLlvmModule(module, { pointerBits: 64 });
|
||||
const tables = [
|
||||
...llvm.matchAll(
|
||||
/^@(\S+)_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr \} \{ i64 (\d+), /gm,
|
||||
/^@(\S+)_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr, ptr \} \{ i64 (\d+), /gm,
|
||||
),
|
||||
];
|
||||
// root and leaf get field hooks; the record with an index-like field name
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
classFieldAttributesName,
|
||||
isClassOwnEnumerableFieldName,
|
||||
isDirentShape,
|
||||
KEY_ORDER_FIELD,
|
||||
shapeKeyOrders,
|
||||
isDynTypedRefType,
|
||||
isRefCounted,
|
||||
shapeHasAccessorSlots,
|
||||
@@ -44,6 +46,7 @@ import type { NullableRefField } from "./nullable-fields.js";
|
||||
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
||||
import { inlineUnionCopyLines } from "./inline-unions.js";
|
||||
import { recordMayKeepSource } from "./record-sources.js";
|
||||
import { emitPerKeyOrder } from "./key-orders.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
/** An owned (+1) union value for a nullable-pointer class field slot `p`
|
||||
@@ -1065,52 +1068,68 @@ export function streamTypedRefMaterializeAdapter(
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = call ptr @scr_dyn_new_obj()`);
|
||||
const byName = new Map(shape.fields.map((field) => [field.name, field]));
|
||||
const order = shape.declaredOrder ?? shape.fields.map((field) => field.name);
|
||||
const inOrder = new Set(order);
|
||||
const fields = [
|
||||
...order.map((name) => byName.get(name)).filter((field) => field !== undefined),
|
||||
...shape.fields.filter((field) => !inOrder.has(field.name)),
|
||||
];
|
||||
// A shape with orderVariants has one field list per key order; its
|
||||
// order index is no key.
|
||||
const orders = shapeKeyOrders(shape);
|
||||
const fieldsOf = (order: readonly string[]): IrRecordShape["fields"][number][] => {
|
||||
const inOrder = new Set(order);
|
||||
return [
|
||||
...order.map((name) => byName.get(name)).filter((field) => field !== undefined),
|
||||
...shape.fields.filter(
|
||||
(field) => !inOrder.has(field.name) && field.name !== KEY_ORDER_FIELD,
|
||||
),
|
||||
];
|
||||
};
|
||||
// Published before the field converters, like the array hooks below.
|
||||
// A Dirent row reads through its finished view (the prototype and
|
||||
// the non-enumerable %dtype).
|
||||
const dirent = isDirentShape(shape);
|
||||
if (!dirent && liveRecordOpsEligible(fields)) {
|
||||
adapter.recordOps = `@${snapshot}_rops`;
|
||||
liveRecordOps(host, t, snapshot, fields);
|
||||
}
|
||||
for (const field of fields) {
|
||||
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
|
||||
const fieldPtr = B.tmp();
|
||||
B.line(
|
||||
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
|
||||
);
|
||||
const fieldValue = loadStructField(
|
||||
B,
|
||||
host.sizeType,
|
||||
fieldPtr,
|
||||
field.type,
|
||||
host.inlineUnions.record(field.type),
|
||||
);
|
||||
// An ABSENT optional field contributes no key.
|
||||
const undefinedTag = undefinedArmTag(field.type, host.unionsById);
|
||||
let skip: string | null = null;
|
||||
if (undefinedTag >= 0 && field.type.kind === "union") {
|
||||
const absent = host.fieldAbsentTestIn(B, fieldValue, field.type.unionId);
|
||||
skip = B.newLabel("live.record.absent");
|
||||
const set = B.newLabel("live.record.set");
|
||||
B.condBr(absent, skip, set);
|
||||
B.startBlock(set);
|
||||
}
|
||||
const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue);
|
||||
B.line(
|
||||
`call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`,
|
||||
);
|
||||
if (skip !== null) {
|
||||
B.br(skip);
|
||||
B.startBlock(skip);
|
||||
if (!dirent && orders.every((order) => liveRecordOpsEligible(fieldsOf(order)))) {
|
||||
if (orders.length === 1) {
|
||||
adapter.recordOps = `@${snapshot}_rops`;
|
||||
liveRecordOps(host, t, snapshot, fieldsOf(orders[0]!));
|
||||
} else {
|
||||
orders.forEach((order, i) =>
|
||||
liveRecordOps(host, t, `${snapshot}_o${i}`, fieldsOf(order)),
|
||||
);
|
||||
adapter.recordOps = `@${snapshot}_rops`;
|
||||
liveRecordOrderSelect(host, shape, snapshot);
|
||||
}
|
||||
}
|
||||
emitPerKeyOrder(B, shape, host.nullableUnions.fieldBase(t.shapeId), "%p", (order) => {
|
||||
for (const field of fieldsOf(order)) {
|
||||
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
|
||||
const fieldPtr = B.tmp();
|
||||
B.line(
|
||||
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
|
||||
);
|
||||
const fieldValue = loadStructField(
|
||||
B,
|
||||
host.sizeType,
|
||||
fieldPtr,
|
||||
field.type,
|
||||
host.inlineUnions.record(field.type),
|
||||
);
|
||||
// An ABSENT optional field contributes no key.
|
||||
const undefinedTag = undefinedArmTag(field.type, host.unionsById);
|
||||
let skip: string | null = null;
|
||||
if (undefinedTag >= 0 && field.type.kind === "union") {
|
||||
const absent = host.fieldAbsentTestIn(B, fieldValue, field.type.unionId);
|
||||
skip = B.newLabel("live.record.absent");
|
||||
const set = B.newLabel("live.record.set");
|
||||
B.condBr(absent, skip, set);
|
||||
B.startBlock(set);
|
||||
}
|
||||
const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue);
|
||||
B.line(
|
||||
`call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`,
|
||||
);
|
||||
if (skip !== null) {
|
||||
B.br(skip);
|
||||
B.startBlock(skip);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (dirent) {
|
||||
host.declare(`declare void @scr_dyn_dirent_finish(ptr)`);
|
||||
B.line(`call void @scr_dyn_dirent_finish(ptr ${out})`);
|
||||
@@ -1373,7 +1392,35 @@ function liveRecordOps(
|
||||
``,
|
||||
`@${names} = internal constant [${fields.length} x ptr] [${literals.join(", ")}]`,
|
||||
// Mutable: the runtime fills the trailing key-array slot on first use.
|
||||
`@${ops} = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get}, i8 ${optional.length === 0 ? 1 : 0}, ptr null }`,
|
||||
`@${ops} = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get}, i8 ${optional.length === 0 ? 1 : 0}, ptr null, ptr null }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
/** `@<snapshot>_rops` for a shape with orderVariants: a record-ops table
|
||||
* whose `select` hook answers the `_o<i>_rops` table (liveRecordOps) of the
|
||||
* record's current key order, which a write of an optional key can change
|
||||
* after the record is boxed. */
|
||||
function liveRecordOrderSelect(
|
||||
host: LlvmEmitterContext,
|
||||
shape: IrRecordShape,
|
||||
snapshot: string,
|
||||
): void {
|
||||
const B = new BlockBuilder();
|
||||
const out = B.slot();
|
||||
B.entryAllocas.push(`${out} = alloca ptr`);
|
||||
emitPerKeyOrder(B, shape, host.nullableUnions.fieldBase(shape.id), "%p", (_, i) => {
|
||||
B.line(`store ptr @${snapshot}_o${i}_rops, ptr ${out}`);
|
||||
});
|
||||
const ops = B.tmp();
|
||||
B.line(`${ops} = load ptr, ptr ${out}`);
|
||||
B.terminate(`ret ptr ${ops}`);
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal ptr @${snapshot}_select(ptr %p) ${FN_ATTRS} { ; live record ops by key order`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
`@${snapshot}_rops = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr } { ${host.sizeType} 0, ptr null, ptr null, ptr null, i8 0, ptr null, ptr @${snapshot}_select }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ export function streamDataAdapter(
|
||||
|
||||
export function streamDoneFnFor(
|
||||
host: LlvmEmitterContext,
|
||||
kind: "w" | "f" | "d" | "t" | "l",
|
||||
kind: "w" | "f" | "d" | "t" | "l" | "c",
|
||||
doneT: IrType & { kind: "func" },
|
||||
): string {
|
||||
const key = `sd:${kind}:${typeKey(doneT)}`;
|
||||
@@ -260,19 +260,21 @@ export function streamDoneFnFor(
|
||||
}
|
||||
}
|
||||
const entry =
|
||||
kind === "w"
|
||||
? "scr_stream_write_done"
|
||||
: kind === "f"
|
||||
? "scr_stream_final_done"
|
||||
: kind === "d"
|
||||
? "scr_stream_destroy_done"
|
||||
: kind === "t"
|
||||
? valueData
|
||||
? "scr_stream_transform_done_value"
|
||||
: "scr_stream_transform_done"
|
||||
: valueData
|
||||
? "scr_stream_flush_done_value"
|
||||
: "scr_stream_flush_done";
|
||||
kind === "c"
|
||||
? "scr_stream_construct_done"
|
||||
: kind === "w"
|
||||
? "scr_stream_write_done"
|
||||
: kind === "f"
|
||||
? "scr_stream_final_done"
|
||||
: kind === "d"
|
||||
? "scr_stream_destroy_done"
|
||||
: kind === "t"
|
||||
? valueData
|
||||
? "scr_stream_transform_done_value"
|
||||
: "scr_stream_transform_done"
|
||||
: valueData
|
||||
? "scr_stream_flush_done_value"
|
||||
: "scr_stream_flush_done";
|
||||
const twoSlot = (kind === "t" || kind === "l") && !valueData;
|
||||
host.declare(
|
||||
`declare void @${entry}(ptr, ptr${twoSlot ? ", ptr, ptr" : valueData ? ", ptr" : ""})`,
|
||||
@@ -634,7 +636,7 @@ export function zlibBytesThunkFor(
|
||||
|
||||
export function streamCbThunkFor(
|
||||
host: LlvmEmitterContext,
|
||||
kind: "r" | "w" | "f" | "d" | "t" | "l" | "e",
|
||||
kind: "r" | "w" | "f" | "d" | "t" | "l" | "e" | "c" | "v",
|
||||
cbT: IrType,
|
||||
): string {
|
||||
if (cbT.kind !== "func")
|
||||
@@ -650,9 +652,11 @@ export function streamCbThunkFor(
|
||||
? ["ptr %s", "double %size"]
|
||||
: kind === "w" || kind === "t"
|
||||
? ["ptr %s", "ptr %chunk"]
|
||||
: kind === "d" || kind === "e"
|
||||
? ["ptr %s", "ptr %err"]
|
||||
: ["ptr %s"];
|
||||
: kind === "v"
|
||||
? ["ptr %s", "ptr %chunks"]
|
||||
: kind === "d" || kind === "e"
|
||||
? ["ptr %s", "ptr %err"]
|
||||
: ["ptr %s"];
|
||||
const declared = cbT.params;
|
||||
const hasThis = declared[0] !== undefined && declared[0].kind === "object";
|
||||
if (declared.length === 0) {
|
||||
@@ -661,7 +665,9 @@ export function streamCbThunkFor(
|
||||
);
|
||||
}
|
||||
const off = hasThis ? 1 : 0;
|
||||
const full = (kind === "r" ? 1 : kind === "w" || kind === "t" ? 3 : kind === "d" ? 2 : 1) + off;
|
||||
const full =
|
||||
(kind === "r" ? 1 : kind === "w" || kind === "t" ? 3 : kind === "d" || kind === "v" ? 2 : 1) +
|
||||
off;
|
||||
if (declared.length > full) {
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: stream '${kind}' callback declares ${declared.length} params (frontend must fence)`,
|
||||
@@ -709,6 +715,31 @@ export function streamCbThunkFor(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (kind === "v" && pos === 0) {
|
||||
// writev's chunks: the batch array (borrowed from the runtime), as
|
||||
// a dyn value or converted to the declared type
|
||||
if (p.kind === "dyn") {
|
||||
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
|
||||
d.push(` %vc${i} = call ptr @scr_dyn_retain_v(ptr %chunks)`);
|
||||
passed.push(`ptr %vc${i}`);
|
||||
} else {
|
||||
const helper = host.dyn.dynCheckHelper(p, false);
|
||||
const ty = host.llType(p);
|
||||
host.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`);
|
||||
d.push(
|
||||
` %vt${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %chunks, ptr null)`,
|
||||
` %vtbad${i} = call zeroext i1 @scr_exc_pending()`,
|
||||
` br i1 %vtbad${i}, label %vtfail${i}, label %vtok${i}`,
|
||||
`vtfail${i}:`,
|
||||
...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []),
|
||||
` ret void`,
|
||||
`vtok${i}:`,
|
||||
);
|
||||
passed.push(`${ty} %vt${i}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const isChunkPos = (kind === "w" || kind === "t") && pos === 0;
|
||||
const isEncPos = (kind === "w" || kind === "t") && pos === 1;
|
||||
const isErrPos = (kind === "d" || kind === "e") && pos === 0;
|
||||
@@ -717,7 +748,7 @@ export function streamCbThunkFor(
|
||||
? false
|
||||
: kind === "w" || kind === "t"
|
||||
? pos === 2
|
||||
: kind === "f" || kind === "l"
|
||||
: kind === "f" || kind === "l" || kind === "c"
|
||||
? pos === 0
|
||||
: pos === 1;
|
||||
if (p.kind === "dyn") {
|
||||
@@ -763,7 +794,7 @@ export function streamCbThunkFor(
|
||||
d.push(` br label %ego${i}`, `ego${i}:`, ` %edv${i} = load ptr, ptr %edslot${i}`);
|
||||
passed.push(`ptr %edv${i}`);
|
||||
} else if (isDonePos) {
|
||||
const glue = `scr_stream_done_dyn_${kind}`;
|
||||
const glue = `scr_stream_done_dyn_${kind === "v" ? "w" : kind}`;
|
||||
host.declare(`declare ptr @${glue}(ptr, ptr, ${host.sizeType})`);
|
||||
host.declare(`declare ptr @scr_dyn_new_func(ptr, ptr, i32, ptr, ptr)`);
|
||||
const clo = doneClosure(`@${glue}`, `dd${i}`);
|
||||
@@ -884,7 +915,8 @@ export function streamCbThunkFor(
|
||||
);
|
||||
passed.push(`ptr %euv${i}`);
|
||||
} else if (isDonePos) {
|
||||
const doneKind = kind as "w" | "f" | "d" | "t" | "l"; // "e" has no done position
|
||||
// "e" has no done position; writev completes as a write
|
||||
const doneKind = (kind === "v" ? "w" : kind) as "w" | "f" | "d" | "t" | "l" | "c";
|
||||
if (p.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: stream done callback not a func");
|
||||
const doneFn = host.streamDoneFnFor(doneKind, p);
|
||||
|
||||
@@ -732,7 +732,7 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue {
|
||||
|
||||
/** dynInvoke's method read on an ordinary object through the site's
|
||||
* cache, else scr_dyn_prepare_method. Returns the owned method (or NULL,
|
||||
* which scr_dyn_invoke_prepared_id answers with the full dispatch). */
|
||||
* which scr_dyn_invoke_prepared answers with the full dispatch). */
|
||||
export function emitJsIcMethod(host: Host, e: ExprOf<"dynInvoke">, receiver: string): string {
|
||||
const B = host.B;
|
||||
const method = e.method;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/* Key-order dispatch for shapes whose values record the order their keys
|
||||
* were written in (IrRecordShape.orderVariants): an emitter that walks a
|
||||
* record's keys emits one walk per order and branches on the value's
|
||||
* KEY_ORDER_FIELD. */
|
||||
import type { IrRecordShape } from "../../ir/ir.js";
|
||||
import { KEY_ORDER_FIELD, shapeKeyOrders } from "../../ir/ir.js";
|
||||
import { mangleRecordStruct } from "../mangle.js";
|
||||
import type { BlockBuilder } from "./blocks.js";
|
||||
|
||||
/** Emits `walk(order, index)` for record `rec` of `shape`, once per order behind a
|
||||
* switch on the value's order index when the shape has several. Each walk
|
||||
* starts in its own block, and the code after this call continues in the
|
||||
* block they all branch to. */
|
||||
export function emitPerKeyOrder(
|
||||
B: BlockBuilder,
|
||||
shape: IrRecordShape,
|
||||
fieldBase: number,
|
||||
rec: string,
|
||||
walk: (order: readonly string[], index: number) => void,
|
||||
): void {
|
||||
const orders = shapeKeyOrders(shape);
|
||||
if (orders.length === 1) {
|
||||
walk(orders[0]!, 0);
|
||||
return;
|
||||
}
|
||||
const index = shape.fields.findIndex((f) => f.name === KEY_ORDER_FIELD) + fieldBase;
|
||||
const p = B.tmp();
|
||||
const n = B.tmp();
|
||||
const i = B.tmp();
|
||||
B.line(
|
||||
`${p} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr ${rec}, i64 0, i32 ${index} ; .${KEY_ORDER_FIELD}`,
|
||||
);
|
||||
B.line(`${n} = load double, ptr ${p}`);
|
||||
B.line(`${i} = fptosi double ${n} to i32`);
|
||||
const labels = orders.map(() => B.newLabel("ko"));
|
||||
const join = B.newLabel("ko.join");
|
||||
B.terminate(
|
||||
`switch i32 ${i}, label %${labels[0]} [ ${labels
|
||||
.slice(1)
|
||||
.map((label, k) => `i32 ${k + 1}, label %${label}`)
|
||||
.join(" ")} ]`,
|
||||
);
|
||||
orders.forEach((order, k) => {
|
||||
B.startBlock(labels[k]!);
|
||||
walk(order, k);
|
||||
B.br(join);
|
||||
});
|
||||
B.startBlock(join);
|
||||
}
|
||||
@@ -553,7 +553,11 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
// which also takes those streams.
|
||||
e.fn === "stream.unboxDyn" && host.usesDynBuiltinStream
|
||||
? "scr_dyn_builtin_stream_unbox"
|
||||
: LIB_FN_SYMS[e.fn]);
|
||||
: // for await over a checked-dynamic value iterates a native
|
||||
// stream behind it (scr_dyn_for_await_stream.c)
|
||||
e.fn === "dyn.asyncIteratorOpen" && host.dynForAwaitStreams
|
||||
? "scr_stream_dyn_async_iterator_open"
|
||||
: LIB_FN_SYMS[e.fn]);
|
||||
if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
|
||||
const args = READ_ONLY_STRING_LIB_FNS.has(e.fn)
|
||||
? emitBorrowedInputs(host, e.args)
|
||||
@@ -734,7 +738,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
if (e.fn === "process.nextTick") return host.emitAsyncContextLibCall(e);
|
||||
if (
|
||||
e.fn === "sp.pipeline" ||
|
||||
e.fn === "sp.pipelineStages" ||
|
||||
e.fn.startsWith("sp.pipelineStages") ||
|
||||
e.fn.startsWith("sp.pipelineOpts") ||
|
||||
e.fn.startsWith("sp.finishedOpts")
|
||||
)
|
||||
|
||||
@@ -1126,11 +1126,24 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
// chaining forms answer the receiver +1. The throwing members
|
||||
// (listeners and option callbacks run synchronously inside) ride the
|
||||
// generic pending check; loop liveness rides USES_TIMERS_LIB_FNS.
|
||||
"readable.push": "scr_stream_push",
|
||||
"readable.push": "scr_stream_push_view",
|
||||
"readable.pushStr": "scr_stream_push_str",
|
||||
"readable.pushStrEnc": "scr_stream_push_str_enc",
|
||||
"readable.pushEncoding": "scr_stream_set_push_encoding",
|
||||
"stream.objectMode": "scr_stream_object_mode",
|
||||
"stream.decodeStrings": "scr_stream_decode_strings",
|
||||
"stream.duplexFromGen": "scr_stream_duplex_from_gen",
|
||||
"stream.duplexFromDyn": "scr_stream_duplex_from_dyn",
|
||||
"stream.duplexifyReadable": "scr_stream_duplexify_readable",
|
||||
"stream.duplexifyWritable": "scr_stream_duplexify_writable",
|
||||
"stream.duplexifyPair": "scr_stream_duplexify_pair",
|
||||
"stream.duplexFromPromise": "scr_stream_duplex_from_promise",
|
||||
"stream.duplexFromAsyncFn": "scr_stream_duplex_from_async_fn",
|
||||
"stream.duplexFromGenSignal": "scr_stream_duplex_from_gen_signal",
|
||||
"stream.duplexFromAsyncFnSignal": "scr_stream_duplex_from_async_fn_signal",
|
||||
"fetch.readableFromWeb": "scr_stream_readable_from_web",
|
||||
"writable.writeEncoding": "scr_stream_write_encoding",
|
||||
"writable.setDefaultEncoding": "scr_stream_set_default_encoding",
|
||||
"stream.chunkDone": "scr_stream_chunk_done",
|
||||
"readable.pushNull": "scr_stream_push_null",
|
||||
"readable.pushDyn": "scr_stream_push_dyn",
|
||||
@@ -1194,6 +1207,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"readable.opMap": "scr_stream_op_map",
|
||||
"readable.opFilter": "scr_stream_op_filter",
|
||||
"readable.opFlatMap": "scr_stream_op_flat_map",
|
||||
"readable.opFlatMapAsyncGen": "scr_stream_op_flat_map_async_gen",
|
||||
"readable.opFlatMapSyncGen": "scr_stream_op_flat_map_sync_gen",
|
||||
"readable.opDrop": "scr_stream_op_drop",
|
||||
"readable.opTake": "scr_stream_op_take",
|
||||
"readable.opToArray": "scr_stream_op_to_array",
|
||||
@@ -1202,6 +1217,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"readable.opEvery": "scr_stream_op_every",
|
||||
"readable.opFind": "scr_stream_op_find",
|
||||
"readable.opReduce": "scr_stream_op_reduce",
|
||||
"readable.opSignalList": "scr_stream_op_signal_list",
|
||||
"readable.opReduceSignal": "scr_stream_op_reduce_signal",
|
||||
"readable.opSignalOf": "scr_stream_op_signal_of",
|
||||
"readable.opOnceOptions": "scr_stream_op_once_options",
|
||||
"readable.opSignalOpts": "scr_stream_op_signal_opts",
|
||||
"readable.pipe": "scr_stream_pipe",
|
||||
"readable.pipeGeneral": "scr_stream_pipe_general",
|
||||
"writable.cork": "scr_stream_cork",
|
||||
@@ -1707,6 +1727,8 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"readable.opMap",
|
||||
"readable.opFilter",
|
||||
"readable.opFlatMap",
|
||||
"readable.opFlatMapAsyncGen",
|
||||
"readable.opFlatMapSyncGen",
|
||||
"readable.opDrop",
|
||||
"readable.opTake",
|
||||
"readable.opToArray",
|
||||
@@ -1715,6 +1737,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"readable.opEvery",
|
||||
"readable.opFind",
|
||||
"readable.opReduce",
|
||||
"readable.opReduceSignal",
|
||||
"readable.resume",
|
||||
"readable.pipe",
|
||||
"readable.pipeGeneral",
|
||||
@@ -1736,6 +1759,19 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"stream.setDestroy",
|
||||
"stream.setTransform",
|
||||
"stream.setFlush",
|
||||
"stream.setConstruct",
|
||||
"stream.duplexFromGen",
|
||||
"stream.duplexFromDyn",
|
||||
"stream.duplexifyReadable",
|
||||
"stream.duplexifyWritable",
|
||||
"stream.duplexifyPair",
|
||||
"stream.duplexFromPromise",
|
||||
"stream.duplexFromAsyncFn",
|
||||
"stream.duplexFromGenSignal",
|
||||
"stream.duplexFromAsyncFnSignal",
|
||||
"fetch.readableFromWeb",
|
||||
"stream.setWritev",
|
||||
"stream.setWritevDefault",
|
||||
"process.activeResources",
|
||||
"stream.finished",
|
||||
"stream.finishedDyn",
|
||||
@@ -1745,8 +1781,10 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"sp.finished",
|
||||
"sp.pipeline",
|
||||
"sp.pipelineStages",
|
||||
"sp.pipelineStagesOpts",
|
||||
"stream.pipelineStages",
|
||||
"stream.pipelineStagesDyn",
|
||||
"stream.composeStages",
|
||||
"sp.pipelineOpts",
|
||||
"sp.pipelineOptsSignal",
|
||||
"sp.finishedOpts",
|
||||
|
||||
@@ -187,18 +187,24 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
e.fn === "stream.setFinal" ||
|
||||
e.fn === "stream.setDestroy" ||
|
||||
e.fn === "stream.setTransform" ||
|
||||
e.fn === "stream.setFlush"
|
||||
e.fn === "stream.setFlush" ||
|
||||
e.fn === "stream.setConstruct" ||
|
||||
e.fn === "stream.setWritev" ||
|
||||
e.fn === "stream.setWritevDefault"
|
||||
) {
|
||||
// The underscore-method assignment surface: the runtime slot swaps
|
||||
// its closure (+1 moves in) and invoke thunk — the next
|
||||
// _read/_write/... dispatch uses it, Node's timing.
|
||||
const kindOf: Record<string, "r" | "w" | "f" | "d" | "t" | "l"> = {
|
||||
const kindOf: Record<string, "r" | "w" | "f" | "d" | "t" | "l" | "c" | "v"> = {
|
||||
"stream.setRead": "r",
|
||||
"stream.setWrite": "w",
|
||||
"stream.setFinal": "f",
|
||||
"stream.setDestroy": "d",
|
||||
"stream.setTransform": "t",
|
||||
"stream.setFlush": "l",
|
||||
"stream.setConstruct": "c",
|
||||
"stream.setWritev": "v",
|
||||
"stream.setWritevDefault": "v",
|
||||
};
|
||||
const symOf: Record<string, string> = {
|
||||
"stream.setRead": "scr_stream_set_read",
|
||||
@@ -207,6 +213,9 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
"stream.setDestroy": "scr_stream_set_destroy",
|
||||
"stream.setTransform": "scr_stream_set_transform",
|
||||
"stream.setFlush": "scr_stream_set_flush",
|
||||
"stream.setConstruct": "scr_stream_set_construct",
|
||||
"stream.setWritev": "scr_stream_set_writev",
|
||||
"stream.setWritevDefault": "scr_stream_set_writev_default",
|
||||
};
|
||||
const recv = host.emitExpr(e.args[0]!);
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
@@ -692,12 +701,15 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
}
|
||||
if (
|
||||
e.fn === "sp.pipelineStages" ||
|
||||
e.fn === "sp.pipelineStagesOpts" ||
|
||||
e.fn === "stream.pipelineStages" ||
|
||||
e.fn === "stream.pipelineStagesDyn"
|
||||
e.fn === "stream.pipelineStagesDyn" ||
|
||||
e.fn === "stream.composeStages"
|
||||
) {
|
||||
// (n, k, positions, flags, mask, s1..sn, stage1..stagek, helpers...,
|
||||
// box?, (signal, abort)?[, cb]): the streams, the stage closures and
|
||||
// the helpers ride stack arrays; absent groups pass null.
|
||||
// box?, (signal, abort)?[, cb | end, signal?]): the streams, the
|
||||
// stage closures and the helpers ride stack arrays; absent groups
|
||||
// pass null.
|
||||
const [nArg, kArg, positionsArg, flagsArg, maskArg] = e.args;
|
||||
if (
|
||||
nArg?.kind !== "numLit" ||
|
||||
@@ -710,7 +722,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
const n = nArg.value;
|
||||
const k = kArg.value;
|
||||
const flags = flagsArg.value;
|
||||
const helperCount = 3 * pipelinePumpedRuns(n + k, positionsArg.value);
|
||||
const helperCount = 3 * pipelinePumpedRuns(n + k, positionsArg.value, flags);
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const packed = (from: number, count: number): string => {
|
||||
if (count === 0) return "null";
|
||||
@@ -740,6 +752,21 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
if (e.fn === "sp.pipelineStages") {
|
||||
host.declare(`declare ptr @scr_sp_pipeline_stages(${params})`);
|
||||
B.line(`${t} = call ptr @scr_sp_pipeline_stages(${head})`);
|
||||
} else if (e.fn === "sp.pipelineStagesOpts") {
|
||||
const end = args[at++]!.name;
|
||||
const outer = take((flags & 64) !== 0);
|
||||
let add = "null";
|
||||
if (outer !== "null") {
|
||||
host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`);
|
||||
add = "@scr_events_add_abort_listener";
|
||||
}
|
||||
host.declare(`declare ptr @scr_sp_pipeline_stages_opts(${params}, i1, ptr, ptr)`);
|
||||
B.line(
|
||||
`${t} = call ptr @scr_sp_pipeline_stages_opts(${head}, i1 ${end}, ptr ${outer}, ptr ${add})`,
|
||||
);
|
||||
} else if (e.fn === "stream.composeStages") {
|
||||
host.declare(`declare ptr @scr_stream_compose_stages(${params})`);
|
||||
B.line(`${t} = call ptr @scr_stream_compose_stages(${head})`);
|
||||
} else if (e.fn === "stream.pipelineStagesDyn") {
|
||||
host.declare(`declare ptr @scr_stream_pipeline_stages_dyn(${params}, ptr)`);
|
||||
B.line(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { emitPerKeyOrder } from "./key-orders.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* Structure-walking helper EMITTERS for the LLVM backend — the .ll mirror
|
||||
@@ -328,78 +329,80 @@ export class LlWalkers {
|
||||
if (cyclic) this.jbLeave(B);
|
||||
return;
|
||||
}
|
||||
// Fields serialize in DECLARED order (JS insertion order); internal
|
||||
// Fields serialize in DECLARED order (JS insertion order), or in the
|
||||
// value's own order for a shape with orderVariants; internal
|
||||
// '%'-fields stay hidden — jsonWriteHelper's contract, ported comment
|
||||
// and all.
|
||||
const order = shape.declaredOrder ?? shape.fields.map((f) => f.name);
|
||||
const inOrder = new Set(order);
|
||||
if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) {
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: declaredOrder of shape ${shapeId} omits a non-internal field`,
|
||||
);
|
||||
}
|
||||
const byName = new Map(shape.fields.map((f) => [f.name, f]));
|
||||
// Function-valued properties are omitted, as JSON.stringify omits them.
|
||||
const emitFields = order
|
||||
.map((n) => byName.get(n))
|
||||
.filter((f) => f !== undefined)
|
||||
.filter((f) => f.type.kind !== "func");
|
||||
const droppable =
|
||||
emitFields.some((f) => undefinedArmTag(f.type, this.host.unionsById) >= 0) ||
|
||||
!!shape.indexValue;
|
||||
this.putc(B, "%b", "123"); // '{'
|
||||
if (!droppable) {
|
||||
emitFields.forEach((f, i) => {
|
||||
this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`);
|
||||
if (edgeable(f.type)) this.jbEdgeProp(B, f.name);
|
||||
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
|
||||
B.line(
|
||||
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`,
|
||||
emitPerKeyOrder(B, shape, fieldBase, "%v", (order) => {
|
||||
const inOrder = new Set(order);
|
||||
if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) {
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: declaredOrder of shape ${shapeId} omits a non-internal field`,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const first = B.slot();
|
||||
B.entryAllocas.push(`${first} = alloca i1`);
|
||||
B.line(`store i1 true, ptr ${first}`);
|
||||
const comma = (): void => {
|
||||
const isf = B.tmp();
|
||||
const lc = B.newLabel("jwc.c");
|
||||
const lj = B.newLabel("jwc.j");
|
||||
B.line(`${isf} = load i1, ptr ${first}`);
|
||||
B.condBr(isf, lj, lc);
|
||||
B.startBlock(lc);
|
||||
this.putc(B, "%b", "44"); // ','
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
B.line(`store i1 false, ptr ${first}`);
|
||||
};
|
||||
for (const f of emitFields) {
|
||||
const utag = undefinedArmTag(f.type, this.host.unionsById);
|
||||
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
|
||||
let skip: string | null = null;
|
||||
if (utag >= 0 && f.type.kind === "union") {
|
||||
// Undefined-valued field: dropped, like Node.
|
||||
const tag = this.unionTag(B, v, f.type.unionId);
|
||||
const isu = B.tmp();
|
||||
B.line(`${isu} = icmp eq i32 ${tag}, ${utag}`);
|
||||
const lw = B.newLabel("jwf.w");
|
||||
skip = B.newLabel("jwf.s");
|
||||
B.condBr(isu, skip, lw);
|
||||
B.startBlock(lw);
|
||||
}
|
||||
comma();
|
||||
this.puts(B, "%b", jsonObjectKeyLabel(f.name));
|
||||
if (edgeable(f.type)) this.jbEdgeProp(B, f.name);
|
||||
B.line(
|
||||
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`,
|
||||
);
|
||||
if (skip !== null) {
|
||||
B.br(skip);
|
||||
B.startBlock(skip);
|
||||
}
|
||||
}
|
||||
if (shape.indexValue) this.emitOverflowEntries(B, shape, first, edgeable(shape.indexValue));
|
||||
}
|
||||
const byName = new Map(shape.fields.map((f) => [f.name, f]));
|
||||
// Function-valued properties are omitted, as JSON.stringify omits them.
|
||||
const emitFields = order
|
||||
.map((n) => byName.get(n))
|
||||
.filter((f) => f !== undefined)
|
||||
.filter((f) => f.type.kind !== "func");
|
||||
const droppable =
|
||||
emitFields.some((f) => undefinedArmTag(f.type, this.host.unionsById) >= 0) ||
|
||||
!!shape.indexValue;
|
||||
this.putc(B, "%b", "123"); // '{'
|
||||
if (!droppable) {
|
||||
emitFields.forEach((f, i) => {
|
||||
this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`);
|
||||
if (edgeable(f.type)) this.jbEdgeProp(B, f.name);
|
||||
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
|
||||
B.line(
|
||||
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const first = B.slot();
|
||||
B.entryAllocas.push(`${first} = alloca i1`);
|
||||
B.line(`store i1 true, ptr ${first}`);
|
||||
const comma = (): void => {
|
||||
const isf = B.tmp();
|
||||
const lc = B.newLabel("jwc.c");
|
||||
const lj = B.newLabel("jwc.j");
|
||||
B.line(`${isf} = load i1, ptr ${first}`);
|
||||
B.condBr(isf, lj, lc);
|
||||
B.startBlock(lc);
|
||||
this.putc(B, "%b", "44"); // ','
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
B.line(`store i1 false, ptr ${first}`);
|
||||
};
|
||||
for (const f of emitFields) {
|
||||
const utag = undefinedArmTag(f.type, this.host.unionsById);
|
||||
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
|
||||
let skip: string | null = null;
|
||||
if (utag >= 0 && f.type.kind === "union") {
|
||||
// Undefined-valued field: dropped, like Node.
|
||||
const tag = this.unionTag(B, v, f.type.unionId);
|
||||
const isu = B.tmp();
|
||||
B.line(`${isu} = icmp eq i32 ${tag}, ${utag}`);
|
||||
const lw = B.newLabel("jwf.w");
|
||||
skip = B.newLabel("jwf.s");
|
||||
B.condBr(isu, skip, lw);
|
||||
B.startBlock(lw);
|
||||
}
|
||||
comma();
|
||||
this.puts(B, "%b", jsonObjectKeyLabel(f.name));
|
||||
if (edgeable(f.type)) this.jbEdgeProp(B, f.name);
|
||||
B.line(
|
||||
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`,
|
||||
);
|
||||
if (skip !== null) {
|
||||
B.br(skip);
|
||||
B.startBlock(skip);
|
||||
}
|
||||
}
|
||||
if (shape.indexValue) this.emitOverflowEntries(B, shape, first, edgeable(shape.indexValue));
|
||||
}
|
||||
});
|
||||
this.putc(B, "%b", "125"); // '}'
|
||||
if (cyclic) this.jbLeave(B);
|
||||
}
|
||||
|
||||
@@ -281,6 +281,11 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit((u) => u.opts.handleProps === true, "scr_dyn_handle_props.c"),
|
||||
// for await over checked-dynamic values.
|
||||
unit((u) => u.opts.dynForAwait === true, "scr_dyn_for_await.c"),
|
||||
// for await over checked-dynamic values that may hold native streams.
|
||||
unit(
|
||||
(u) => u.opts.dynForAwait === true && u.opts.stream && u.opts.dynAsync,
|
||||
"scr_dyn_for_await_stream.c",
|
||||
),
|
||||
// FileHandle streams reference both the stream unit and FileHandle.
|
||||
unit((u) => u.opts.stream && u.opts.fileHandle, "scr_fs_stream_fh.c"),
|
||||
// The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD,
|
||||
@@ -332,6 +337,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unitArgs((u) => (u.tlsCa && targetPlatform(u.driver) === "win32" ? ["-lcrypt32"] : [])),
|
||||
// Fetch uses the native network, TLS, and HTTP runtime.
|
||||
unit((u) => u.nativeFetch, "scr_fetch.c"),
|
||||
// Readable.fromWeb reads a web ReadableStream through its reader.
|
||||
unit((u) => u.nativeFetch && u.opts.stream, "scr_stream_from_web.c"),
|
||||
];
|
||||
|
||||
/** The arguments of the optional runtime inputs, in link order. */
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* of the link contract: a program object keeps this symbol undefined and the
|
||||
* matching runtime defines it, so incompatible manual links fail before the
|
||||
* program can start. */
|
||||
export const RUNTIME_ABI_VERSION = 28 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v28" as const;
|
||||
export const RUNTIME_ABI_VERSION = 33 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v33" as const;
|
||||
|
||||
/** Object consumption is useful today, but the complete scr_* surface may
|
||||
* still change before 1.0. The versioned marker prevents accidental mixing;
|
||||
|
||||
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
|
||||
runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
|
||||
@@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
|
||||
runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" },
|
||||
compiler: {
|
||||
command: "clang",
|
||||
identity: "fixture clang",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { boolLit, strLit } from "../../../ir/build.js";
|
||||
import { boolLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { requestGeneralStreamSurface } from "../general-streams.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
@@ -93,32 +93,47 @@ export function lowerChildMethodCall(
|
||||
'"exit", "close", "error", "message", and "disconnect" are the supported child events (as literals)',
|
||||
);
|
||||
}
|
||||
if (!ts.isExpressionStatement(call.parent)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`chaining child.${name}(...) (the result is void here — register each listener as its own statement)`,
|
||||
);
|
||||
}
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
// `child.on(...)` returns the child: a chained call or a stored result
|
||||
// reads the receiver, which evaluates once, before the listener.
|
||||
let receiver = lowerer.lowerExpr(access.expression);
|
||||
const chained = ts.isExpressionStatement(call.parent)
|
||||
? null
|
||||
: {
|
||||
local: lowerer.declareHiddenLocal("%childListenerTarget", receiver.type),
|
||||
init: receiver,
|
||||
};
|
||||
if (chained) receiver = varRef(chained.local.id, chained.local.type, loc);
|
||||
const registered = (registration: IrExpr): IrExpr =>
|
||||
chained === null
|
||||
? registration
|
||||
: {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: chained.local.id, init: chained.init, loc },
|
||||
{ kind: "exprStmt", expr: registration, loc },
|
||||
],
|
||||
result: varRef(chained.local.id, chained.local.type, loc),
|
||||
type: chained.local.type,
|
||||
loc,
|
||||
};
|
||||
const once: IrExpr = { kind: "boolLit", value: name === "once", type: BOOL, loc };
|
||||
if (event === "message") {
|
||||
return {
|
||||
return registered({
|
||||
kind: "libCall",
|
||||
fn: "child.onMessage",
|
||||
args: [receiver, ipcMessageListener(lowerer, call.arguments.nodes[1]!), once],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
});
|
||||
}
|
||||
if (event === "disconnect") {
|
||||
return {
|
||||
return registered({
|
||||
kind: "libCall",
|
||||
fn: "child.onDisconnect",
|
||||
args: [receiver, ipcDisconnectListener(lowerer, call.arguments.nodes[1]!), once],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
});
|
||||
}
|
||||
const listenerNode = stripTypeCasts(call.arguments.nodes[1]!);
|
||||
if (
|
||||
@@ -163,7 +178,7 @@ export function lowerChildMethodCall(
|
||||
call.arguments.nodes[1]!,
|
||||
);
|
||||
}
|
||||
return {
|
||||
return registered({
|
||||
kind: "libCall",
|
||||
fn: "child.onDyn",
|
||||
args: [
|
||||
@@ -173,7 +188,7 @@ export function lowerChildMethodCall(
|
||||
],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
});
|
||||
}
|
||||
if (event === "spawn") {
|
||||
if (cb.type.kind !== "func" || cb.type.params.length !== 0 || cb.type.ret.kind !== "void") {
|
||||
@@ -182,7 +197,13 @@ export function lowerChildMethodCall(
|
||||
call.arguments.nodes[1]!,
|
||||
);
|
||||
}
|
||||
return { kind: "libCall", fn: "child.onSpawn", args: [receiver, cb], type: VOID, loc };
|
||||
return registered({
|
||||
kind: "libCall",
|
||||
fn: "child.onSpawn",
|
||||
args: [receiver, cb],
|
||||
type: VOID,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
if (cb.type.kind !== "func" || cb.type.params.length > (event === "error" ? 1 : 2)) {
|
||||
lowerer.unsupported(
|
||||
@@ -243,13 +264,13 @@ export function lowerChildMethodCall(
|
||||
`${event} listeners whose signal parameter is not 'Signals | null' (got '${lowerer.fmt(sigParam!)}')`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
return registered({
|
||||
kind: "libCall",
|
||||
fn: event === "close" ? "child.onClose" : "child.onExit",
|
||||
args: [receiver, cb],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
});
|
||||
}
|
||||
if (param !== undefined && !(param.kind === "object" && param.className === "%Error")) {
|
||||
lowerer.unsupported(
|
||||
@@ -258,7 +279,13 @@ export function lowerChildMethodCall(
|
||||
`error listeners whose parameter is not 'Error' (got '${lowerer.fmt(param)}')`,
|
||||
);
|
||||
}
|
||||
return { kind: "libCall", fn: "child.onError", args: [receiver, cb], type: VOID, loc };
|
||||
return registered({
|
||||
kind: "libCall",
|
||||
fn: "child.onError",
|
||||
args: [receiver, cb],
|
||||
type: VOID,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
// child.kill(signal?) — Node's semantics exactly: the name resolves
|
||||
// through Node's signal table (unknown names throw the ERR_UNKNOWN_SIGNAL
|
||||
|
||||
@@ -9,6 +9,7 @@ import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
@@ -376,8 +377,14 @@ export function lowerPromiseStaticCall(
|
||||
);
|
||||
}
|
||||
const entries = lowerer.lowerExpr(argNode);
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(call)) && entries.type.kind === "dyn")
|
||||
return checkedPromiseAll(lowerer, entries, loc);
|
||||
if (entries.type.kind === "dyn") return checkedPromiseAll(lowerer, entries, loc);
|
||||
// An 'any'-typed argument that holds a typed value (`const xs: any =
|
||||
// []`): the combinator reads it as the runtime value it is.
|
||||
if (
|
||||
(lowerer.typeOf(argNode).flags & ts.TypeFlags.Any) !== 0 &&
|
||||
lowerer.dynConvertible(entries.type)
|
||||
)
|
||||
return checkedPromiseAll(lowerer, lowerer.coerceToExpected(entries, DYN), loc);
|
||||
|
||||
// loadPlugin(p)))` — the loadPlugins shape, where the checker
|
||||
// spells `any[]`): the ENGINE's own Promise.all runs over the
|
||||
@@ -845,3 +852,89 @@ function lowerTypedSettledCombinator(
|
||||
}
|
||||
return { kind: "intrinsic", name: "promise.allSettled", args: [entries], type: resultT, loc };
|
||||
}
|
||||
|
||||
/** `const pair = await Promise.all([p, q])` where every entry is the same
|
||||
* Promise<T> and the binding's type is the checker's tuple `[T, T]`: the
|
||||
* uniform entries settle through one runtime combinator into a T[] (the
|
||||
* same combinator and turn as the array form), and the binding receives
|
||||
* the tuple of its elements. Null for every other await. */
|
||||
export function lowerAwaitedUniformPromiseAll(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.AwaitExpression,
|
||||
): IrExpr | null {
|
||||
let operand: ts.Expression = expr.expression;
|
||||
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
|
||||
if (
|
||||
!ts.isCallExpression(operand) ||
|
||||
operand.questionDotToken ||
|
||||
!ts.isPropertyAccessExpression(operand.expression) ||
|
||||
operand.arguments.nodes.length !== 1 ||
|
||||
isJsSourceFile(ts.getSourceFileOfNode(expr))
|
||||
)
|
||||
return null;
|
||||
const argNode = operand.arguments.nodes[0]!;
|
||||
if (
|
||||
!ts.isArrayLiteralExpression(argNode) ||
|
||||
argNode.elements.nodes.length === 0 ||
|
||||
argNode.elements.nodes.some(ts.isSpreadElement)
|
||||
)
|
||||
return null;
|
||||
let user: ts.Node = expr;
|
||||
while (ts.isParenthesizedExpression(user.parent)) user = user.parent;
|
||||
const decl = user.parent;
|
||||
if (!ts.isVariableDeclaration(decl) || decl.initializer !== user || !ts.isIdentifier(decl.name))
|
||||
return null;
|
||||
if (lowerer.stdlibGlobalMember(operand.expression, "Promise") !== "all") return null;
|
||||
const tuple = lowerer.mapTypeOf(lowerer.typeOf(decl.name));
|
||||
if (tuple?.kind !== "record") return null;
|
||||
const shape = lowerer.shapes.get(tuple.shapeId);
|
||||
if (!shape?.tuple || shape.fields.length !== argNode.elements.nodes.length) return null;
|
||||
const elems = argNode.elements.nodes.map((el) => lowerer.lowerExpr(el));
|
||||
const first = elems[0]!.type;
|
||||
if (
|
||||
first.kind !== "promise" ||
|
||||
first.inner.kind === "void" ||
|
||||
!elems.every((e) => typeEquals(e.type, first)) ||
|
||||
!shape.fields.every((f) => typeEquals(f.type, first.inner))
|
||||
)
|
||||
return null;
|
||||
const loc = locOf(expr);
|
||||
const values = arrayOf(first.inner);
|
||||
const local = lowerer.declareHiddenLocal("%allValues", values);
|
||||
const all: IrExpr = {
|
||||
kind: "intrinsic",
|
||||
name: "promise.all",
|
||||
args: [{ kind: "arrayLit", elems, type: arrayOf(first), loc }],
|
||||
type: { kind: "promise", inner: values },
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: local.id,
|
||||
init: { kind: "awaitExpr", value: all, type: values, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
// Shape fields are sorted by name; a tuple field's name is its position.
|
||||
result: {
|
||||
kind: "recordLit",
|
||||
fields: shape.fields.map((f) => ({
|
||||
name: f.name,
|
||||
value: {
|
||||
kind: "arrayGet",
|
||||
arr: varRef(local.id, values, loc),
|
||||
index: { kind: "numLit", value: Number(f.name), type: F64, loc },
|
||||
type: f.type,
|
||||
loc,
|
||||
},
|
||||
})),
|
||||
type: tuple,
|
||||
loc,
|
||||
},
|
||||
type: tuple,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -195,7 +195,30 @@ export function lowerSearchParamsNew(
|
||||
);
|
||||
}
|
||||
const arg = args[0];
|
||||
if (arg === undefined || (ts.isIdentifier(arg) && arg.text === "undefined")) {
|
||||
if (arg === undefined) {
|
||||
return { kind: "libCall", fn: "sp.new", args: [], type: SEARCH_PARAMS_T, loc };
|
||||
}
|
||||
return lowerSearchParamsInit(lowerer, arg, loc);
|
||||
}
|
||||
|
||||
/** The URLSearchParams built from one init expression. A conditional
|
||||
* init (`new URLSearchParams(c ? { a: "1" } : {})`) evaluates its
|
||||
* condition first and builds from the arm it selects, so each arm may be
|
||||
* any supported init shape. */
|
||||
function lowerSearchParamsInit(lowerer: Lowerer, arg: ts.Expression, loc: SrcLoc): IrExpr {
|
||||
let inner = arg;
|
||||
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
|
||||
if (ts.isConditionalExpression(inner)) {
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: lowerer.lowerCondition(inner.condition),
|
||||
then: lowerSearchParamsInit(lowerer, inner.whenTrue, loc),
|
||||
else_: lowerSearchParamsInit(lowerer, inner.whenFalse, loc),
|
||||
type: SEARCH_PARAMS_T,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (ts.isIdentifier(arg) && arg.text === "undefined") {
|
||||
return { kind: "libCall", fn: "sp.new", args: [], type: SEARCH_PARAMS_T, loc };
|
||||
}
|
||||
// The object-literal init: `{ a: "1", b: "2" }` appends pairs in
|
||||
|
||||
@@ -85,7 +85,7 @@ import {
|
||||
lowerDynamicEmitterOn,
|
||||
} from "./lower-event-emitter.js";
|
||||
import { protocolArm, protocolFieldArm } from "./class-protocol-dispatch.js";
|
||||
import { STREAM_OPERATORS } from "./lower-stream.js";
|
||||
import { STREAM_OPERATORS, operatorSignalOptions, reduceSignalSetup } from "./lower-stream.js";
|
||||
import { deletedPropertyNames, fixedFieldFamily } from "./js-class-fields.js";
|
||||
|
||||
const STREAM_METHODS = new Set([
|
||||
@@ -5640,13 +5640,16 @@ export class ClassDynamicDispatch {
|
||||
const readableT: IrType = { kind: "object", className: "%Readable" };
|
||||
const op = (fn: string, args: IrExpr[], type: IrType): IrStmt[] =>
|
||||
ret({ kind: "libCall", fn: fn as IrLibFn, args: [receiver, ...args], type, loc });
|
||||
// Node passes every callback its { signal }; the callback's
|
||||
// declared parameters are not known here.
|
||||
const sig = (): IrExpr => operatorSignalOptions(lowerer, arg(1), loc);
|
||||
switch (method) {
|
||||
case "map":
|
||||
return op("readable.opMap", [arg(0), arg(1)], readableT);
|
||||
return op("readable.opMap", [arg(0), arg(1), sig()], readableT);
|
||||
case "filter":
|
||||
return op("readable.opFilter", [arg(0), arg(1)], readableT);
|
||||
return op("readable.opFilter", [arg(0), arg(1), sig()], readableT);
|
||||
case "flatMap":
|
||||
return op("readable.opFlatMap", [arg(0), arg(1)], readableT);
|
||||
return op("readable.opFlatMap", [arg(0), arg(1), sig()], readableT);
|
||||
case "drop":
|
||||
return op("readable.opDrop", [arg(0), arg(1)], readableT);
|
||||
case "take":
|
||||
@@ -5654,25 +5657,49 @@ export class ClassDynamicDispatch {
|
||||
case "toArray":
|
||||
return op("readable.opToArray", [arg(0)], { kind: "promise", inner: DYN });
|
||||
case "forEach":
|
||||
return op("readable.opForEach", [arg(0), arg(1)], { kind: "promise", inner: VOID });
|
||||
return op("readable.opForEach", [arg(0), arg(1), sig()], {
|
||||
kind: "promise",
|
||||
inner: VOID,
|
||||
});
|
||||
case "some":
|
||||
return op("readable.opSome", [arg(0), arg(1)], { kind: "promise", inner: BOOL });
|
||||
return op("readable.opSome", [arg(0), arg(1), sig()], { kind: "promise", inner: BOOL });
|
||||
case "every":
|
||||
return op("readable.opEvery", [arg(0), arg(1)], { kind: "promise", inner: BOOL });
|
||||
return op("readable.opEvery", [arg(0), arg(1), sig()], {
|
||||
kind: "promise",
|
||||
inner: BOOL,
|
||||
});
|
||||
case "find":
|
||||
return op("readable.opFind", [arg(0), arg(1)], { kind: "promise", inner: DYN });
|
||||
case "reduce":
|
||||
return op(
|
||||
"readable.opReduce",
|
||||
[
|
||||
arg(0),
|
||||
arg(1),
|
||||
{ kind: "boolLit", value: dispatch.source.args.length >= 2, type: BOOL, loc },
|
||||
arg(2),
|
||||
{ kind: "numLit", value: 0, type: F64, loc },
|
||||
],
|
||||
{ kind: "promise", inner: DYN },
|
||||
);
|
||||
return op("readable.opFind", [arg(0), arg(1), sig()], { kind: "promise", inner: DYN });
|
||||
case "reduce": {
|
||||
// the dispatch function's locals: there is no function
|
||||
// context while dispatch bodies are built
|
||||
const setup = reduceSignalSetup(lowerer, arg(2), loc, (name, type) => {
|
||||
const local = {
|
||||
id: `${name}.${dispatch.fn.locals.length}`,
|
||||
name,
|
||||
type,
|
||||
mutable: false,
|
||||
};
|
||||
dispatch.fn.locals.push(local);
|
||||
return local;
|
||||
});
|
||||
return [
|
||||
...setup.stmts,
|
||||
...op(
|
||||
"readable.opReduceSignal",
|
||||
[
|
||||
arg(0),
|
||||
arg(1),
|
||||
{ kind: "boolLit", value: dispatch.source.args.length >= 2, type: BOOL, loc },
|
||||
arg(2),
|
||||
{ kind: "numLit", value: 0, type: F64, loc },
|
||||
setup.abort,
|
||||
setup.cbOpts,
|
||||
],
|
||||
{ kind: "promise", inner: DYN },
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (method === "destroy" && (!incoming.length || incoming[0]?.kind === "unitLit"))
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import type { IrExpr, IrType } from "../../ir/ir.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { accessorCall } from "./lower-classes.js";
|
||||
import { iterableGenerator, iterableUnionOf } from "./iterable-values.js";
|
||||
|
||||
/** The generator a class instance's `[Symbol.iterator]` returns, when that
|
||||
* method is a generator method taking no arguments: `obj[Symbol.iterator]()`,
|
||||
* method is a generator method taking no arguments (or the iterator an
|
||||
* Iterable<T> value opens over its current arm): `obj[Symbol.iterator]()`,
|
||||
* called once, as for-of, spread and Array.from call it. Null for other
|
||||
* values, and for classes whose iterator the program can replace at run
|
||||
* time (an instance property, a prototype write). */
|
||||
export function classGeneratorIterable(lowerer: Lowerer, value: IrExpr): IrExpr | null {
|
||||
// An Iterable<T> value opens an iterator over its current arm.
|
||||
const iterable = iterableUnionOf(lowerer, value.type);
|
||||
if (iterable) return iterableGenerator(lowerer, value, iterable);
|
||||
if (value.type.kind !== "object" || !iteratesThroughGenerator(lowerer, value.type)) return null;
|
||||
const className = value.type.className;
|
||||
const ret = lowerer.findMethodOn(lowerer.classes.get(className)!, "sym:iterator")!.sig.ret;
|
||||
@@ -17,6 +22,7 @@ export function classGeneratorIterable(lowerer: Lowerer, value: IrExpr): IrExpr
|
||||
/** Whether values of `type` iterate through a generator method (the check
|
||||
* classGeneratorIterable makes, from the type alone). */
|
||||
export function iteratesThroughGenerator(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (iterableUnionOf(lowerer, type)) return true;
|
||||
if (type.kind !== "object") return false;
|
||||
const info = lowerer.classes.get(type.className);
|
||||
if (!info || info.def.runtime || info.fields.has("sym:iterator")) return false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { BOOL, DYN, STRING, type IrExpr, type IrStmt } from "../../ir/ir.js";
|
||||
import { BOOL, DYN, STRING, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js";
|
||||
import { locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { classValueRef, type ClassInfo } from "./lower-classes.js";
|
||||
@@ -15,9 +15,29 @@ export function hasRuntimeStatics(info: ClassInfo): boolean {
|
||||
|
||||
/** Constructor descriptors preserve inherited getter receivers and static
|
||||
* initialization order. Factory evaluations additionally own independent
|
||||
* fields and closures when the same factory is called repeatedly. */
|
||||
export function initializeRuntimeStatics(lowerer: Lowerer, info: ClassInfo, value: IrExpr): IrExpr {
|
||||
if (!info.runtimeStatics && !info.runtimePrototypeMembers) return value;
|
||||
* fields and closures when the same factory is called repeatedly.
|
||||
*
|
||||
* `binding` is the local that holds the class under its own name (a
|
||||
* function-local class declaration's binding, or the inner name of a
|
||||
* function-local class expression whose body names itself), declared
|
||||
* without a value. It is assigned before any static member is defined, so
|
||||
* static initializers and methods read the class through it. */
|
||||
export function initializeRuntimeStatics(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
value: IrExpr,
|
||||
binding?: IrLocal,
|
||||
): IrExpr {
|
||||
if (!info.runtimeStatics && !info.runtimePrototypeMembers) {
|
||||
if (!binding) return value;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "assign", localId: binding.id, value, loc: value.loc }],
|
||||
result: varRef(binding.id, binding.type, value.loc),
|
||||
type: binding.type,
|
||||
loc: value.loc,
|
||||
};
|
||||
}
|
||||
const loc = value.loc;
|
||||
const local = lowerer.declareHiddenLocal("%staticClass", value.type);
|
||||
const receiver = varRef(local.id, value.type, loc);
|
||||
@@ -28,6 +48,16 @@ export function initializeRuntimeStatics(lowerer: Lowerer, info: ClassInfo, valu
|
||||
: dynUndefinedExpr(loc);
|
||||
const statements: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: local.id, init: value, loc },
|
||||
...(binding
|
||||
? [
|
||||
{
|
||||
kind: "assign" as const,
|
||||
localId: binding.id,
|
||||
value: lowerer.coerceToExpected(receiver, binding.type),
|
||||
loc,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "dyn.classInherit", args: [boxed, base], type: DYN, loc },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { planRecordUnionLift, planRecordUnionWrap } from "../union-retag.js";
|
||||
import type { WidthLift } from "../width-lift.js";
|
||||
import type { IrType } from "../../../ir/ir.js";
|
||||
import { isUnitType, typeEquals, UNDEFINED_T } from "../../../ir/ir.js";
|
||||
import { canDynCheckTo, isUnitType, typeEquals, UNDEFINED_T } from "../../../ir/ir.js";
|
||||
import { type ClassInfo, findMethodOn, findGenericMethodOn } from "../lower-classes.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
|
||||
@@ -21,6 +21,22 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
|
||||
if (dst.kind === "dyn" && src.kind !== "dyn" && lowerer.dynConvertible(src)) {
|
||||
return { how: "dynIn" };
|
||||
}
|
||||
// An 'unknown' (dyn) SOURCE field into a typed slot — a field TypeScript
|
||||
// types `any` (a JSON.parse read), which the checker lets into any
|
||||
// declared field type: the checked extraction, the same dynCheck a
|
||||
// typed binding initialized from that value applies.
|
||||
if (
|
||||
src.kind === "dyn" &&
|
||||
dst.kind !== "dyn" &&
|
||||
dst.kind !== "union" &&
|
||||
canDynCheckTo(
|
||||
dst,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(id) => lowerer.unions.get(id),
|
||||
)
|
||||
) {
|
||||
return { how: "dynOut" };
|
||||
}
|
||||
if (dst.kind === "union") {
|
||||
if (src.kind === "union") {
|
||||
if (lowerer.unionRetagMappable(src.unionId, dst.unionId)) return { how: "retag" };
|
||||
@@ -108,6 +124,10 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
|
||||
return { how: "discriminantRoute" };
|
||||
}
|
||||
}
|
||||
if (selected.length === 2 && src.kind === "record") {
|
||||
const route = presenceRoute(lowerer, src.shapeId, selected[0]!, selected[1]!);
|
||||
if (route) return route;
|
||||
}
|
||||
if (selected.length !== 1) return null;
|
||||
return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm };
|
||||
}
|
||||
@@ -202,6 +222,42 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Two record arms a source record fits, where one arm declares a field
|
||||
* the other lacks and the source types that field `T | undefined`
|
||||
* (`{ count, errors: errs.length ? errs : undefined }` into `{ count;
|
||||
* errors: string[] } | { count }`): the field's value picks the arm at
|
||||
* run time. Null for any other pair. */
|
||||
function presenceRoute(
|
||||
lowerer: Lowerer,
|
||||
sourceId: string,
|
||||
a: { tag: number; arm: IrType },
|
||||
b: { tag: number; arm: IrType },
|
||||
): WidthLift | null {
|
||||
const source = lowerer.shapes.get(sourceId);
|
||||
if (!source || source.tuple || source.indexValue || a.arm.kind !== "record") return null;
|
||||
if (b.arm.kind !== "record") return null;
|
||||
const shapeA = lowerer.shapes.get(a.arm.shapeId);
|
||||
const shapeB = lowerer.shapes.get(b.arm.shapeId);
|
||||
if (!shapeA || !shapeB) return null;
|
||||
const optional = (t: IrType): boolean =>
|
||||
t.kind === "union" && lowerer.armTag(t.unionId, UNDEFINED_T) >= 0;
|
||||
for (const [present, absent, presentShape, absentShape] of [
|
||||
[a, b, shapeA, shapeB],
|
||||
[b, a, shapeB, shapeA],
|
||||
] as const) {
|
||||
const extra = presentShape.fields.filter(
|
||||
(f) => !absentShape.fields.some((g) => g.name === f.name),
|
||||
);
|
||||
if (extra.length !== 1 || optional(extra[0]!.type)) continue;
|
||||
if (absentShape.fields.some((g) => !presentShape.fields.some((f) => f.name === g.name)))
|
||||
continue;
|
||||
const field = source.fields.find((f) => f.name === extra[0]!.name);
|
||||
if (!field || !optional(field.type)) continue;
|
||||
return { how: "presenceRoute", field: field.name, present, absent };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** True for the unit-only element types (`(null | undefined)[]`, the
|
||||
* `readonly []` mapping): a union whose every arm is a unit. */
|
||||
export function unitOnlyElem(lowerer: Lowerer, t: IrType): boolean {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { buildArrayConversion } from "./builders.js";
|
||||
import type { WidthLift } from "../width-lift.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType, SrcLoc } from "../../../ir/ir.js";
|
||||
import { BOOL, DYN, F64, STRING } from "../../../ir/ir.js";
|
||||
import { BOOL, DYN, F64, STRING, UNDEFINED_T } from "../../../ir/ir.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import { findStaticOn, findGenericStaticOn } from "../lower-classes.js";
|
||||
import { lowerRecordOvfCaptureHelper } from "../containers/indexed-objects.js";
|
||||
@@ -155,6 +155,76 @@ export function applyWidthLift(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
case "presenceRoute": {
|
||||
if (dst.kind !== "union" || value.type.kind !== "record")
|
||||
throw new InternalCompilerError("lowerer bug: presenceRoute lift shape");
|
||||
const sourceType = value.type;
|
||||
const fieldType = lowerer.shapes
|
||||
.get(sourceType.shapeId)
|
||||
?.fields.find((f) => f.name === lift.field)?.type;
|
||||
if (fieldType?.kind !== "union")
|
||||
throw new InternalCompilerError("lowerer bug: presenceRoute field shape");
|
||||
// One helper per pair: the conversion reads the source twice.
|
||||
const key = `presence:${typeKey(sourceType)}:${dst.unionId}:${lift.field}`;
|
||||
let helper = lowerer.valueHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = `%union.presence.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, helper);
|
||||
const source = (): IrExpr => varRef("v.0", sourceType, loc);
|
||||
const arm = (route: { tag: number; arm: IrType }): IrExpr => {
|
||||
const inner = lowerer.widthLiftPlan(sourceType, route.arm);
|
||||
if (!inner)
|
||||
throw new InternalCompilerError(
|
||||
"lowerer bug: planned presenceRoute arm stopped lifting",
|
||||
);
|
||||
return {
|
||||
kind: "unionWrap",
|
||||
unionId: dst.unionId,
|
||||
tag: route.tag,
|
||||
value: lowerer.applyWidthLift(inner, source(), route.arm, loc),
|
||||
type: dst,
|
||||
loc,
|
||||
};
|
||||
};
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [{ localId: "v.0", name: "v", type: sourceType }],
|
||||
returnType: dst,
|
||||
locals: [{ id: "v.0", name: "v", type: sourceType, mutable: false }],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId: fieldType.unionId,
|
||||
tag: lowerer.armTag(fieldType.unionId, UNDEFINED_T),
|
||||
value: {
|
||||
kind: "recordGet",
|
||||
obj: source(),
|
||||
shapeId: sourceType.shapeId,
|
||||
field: lift.field,
|
||||
type: fieldType,
|
||||
loc,
|
||||
},
|
||||
negated: false,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: arm(lift.absent),
|
||||
else_: arm(lift.present),
|
||||
type: dst,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [value], type: dst, loc };
|
||||
}
|
||||
case "width": {
|
||||
if (dst.kind !== "record" || value.type.kind !== "record")
|
||||
throw new InternalCompilerError("lowerer bug: width lift shape");
|
||||
@@ -220,6 +290,8 @@ export function applyWidthLift(
|
||||
throw new InternalCompilerError("lowerer bug: dynIn lift against a non-dyn slot");
|
||||
return { kind: "dynFrom", value, type: DYN, loc };
|
||||
}
|
||||
case "dynOut":
|
||||
return { kind: "dynCheck", value, type: dst, loc };
|
||||
case "upcast": {
|
||||
if (dst.kind !== "object" || value.type.kind !== "object")
|
||||
throw new InternalCompilerError("lowerer bug: upcast lift shape");
|
||||
|
||||
@@ -834,6 +834,10 @@ function buildArrayHofFn(
|
||||
loc,
|
||||
},
|
||||
readLen,
|
||||
// map's result has the receiver's length, holes where the receiver
|
||||
// has them. Storage for it is reserved first and the length is set
|
||||
// after the loop, so a result without holes is filled by appends and
|
||||
// gets no state bytes.
|
||||
...(method === "map"
|
||||
? [
|
||||
{
|
||||
@@ -841,6 +845,7 @@ function buildArrayHofFn(
|
||||
arr: varRef("out.0", outT, loc),
|
||||
length: varRef("n.0", F64, loc),
|
||||
loc,
|
||||
reserve: true,
|
||||
} satisfies IrStmt,
|
||||
]
|
||||
: []),
|
||||
@@ -872,6 +877,16 @@ function buildArrayHofFn(
|
||||
loc,
|
||||
},
|
||||
]),
|
||||
...(method === "map"
|
||||
? [
|
||||
{
|
||||
kind: "arraySetLength",
|
||||
arr: varRef("out.0", outT, loc),
|
||||
length: varRef("n.0", F64, loc),
|
||||
loc,
|
||||
} satisfies IrStmt,
|
||||
]
|
||||
: []),
|
||||
{ kind: "return", value: varRef("out.0", outT, loc), loc },
|
||||
];
|
||||
} else if (method === "filter" && fastT) {
|
||||
@@ -1557,6 +1572,19 @@ export function lowerArrayFlatMapCall(
|
||||
}
|
||||
if (fnRet.kind === "union") {
|
||||
const def = lowerer.unions.get(fnRet.unionId);
|
||||
const plan = def?.arms.some((a) => a.kind === "array")
|
||||
? flatMapUnionPlan(lowerer, call, argNode, fnRet)
|
||||
: null;
|
||||
if (plan) {
|
||||
const helper = flatMapUnionHelper(lowerer, elem, fnRet, arity, plan, loc, present);
|
||||
return {
|
||||
kind: "call",
|
||||
callee: helper,
|
||||
args: [receiver, fnArg],
|
||||
type: arrayOf(plan.outElem),
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (def?.arms.some((a) => a.kind === "array")) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1622,6 +1650,278 @@ export function lowerArrayFlatMapCall(
|
||||
};
|
||||
}
|
||||
|
||||
/** How each arm of a flatMap callback's union result contributes to the
|
||||
* output: an array arm appends its elements, an array arm the checker
|
||||
* types `never[]` (an empty literal such as `s ? s : []`) appends nothing,
|
||||
* and any other arm appends itself. */
|
||||
interface FlatMapUnionPlan {
|
||||
outElem: IrType;
|
||||
arms: ("spread" | "empty" | "value")[];
|
||||
}
|
||||
|
||||
/** The plan for a callback returning `T | U[]`: every non-array arm must
|
||||
* convert into the result's element type without a check, and every
|
||||
* array arm's element type must be the result's. Null when an arm does
|
||||
* not. */
|
||||
function flatMapUnionPlan(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
argNode: ts.Expression,
|
||||
fnRet: IrType & { kind: "union" },
|
||||
): FlatMapUnionPlan | null {
|
||||
const out = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (out?.kind !== "array") return null;
|
||||
const outElem = out.elem;
|
||||
const arms = lowerer.unions.get(fnRet.unionId)?.arms;
|
||||
if (!arms) return null;
|
||||
// The `never[]` constituents of the callback's checked result.
|
||||
const emptyArms: IrType[] = [];
|
||||
const signature = lowerer.checker.getCallSignatures(lowerer.typeOf(argNode))[0];
|
||||
if (signature) {
|
||||
const ret = lowerer.checker.getReturnTypeOfSignature(signature);
|
||||
for (const part of ret.isUnionType() ? ts.constituentTypes(ret) : [ret]) {
|
||||
if (!lowerer.checker.isArrayType(part)) continue;
|
||||
const [element] = lowerer.checker.getTypeArguments(part as ts.TypeReference);
|
||||
if (element === undefined || (element.flags & ts.TypeFlags.Never) === 0) continue;
|
||||
const mapped = lowerer.mapTypeOf(part);
|
||||
if (mapped) emptyArms.push(mapped);
|
||||
}
|
||||
}
|
||||
const converts = (t: IrType): boolean => {
|
||||
if (typeEquals(t, outElem)) return true;
|
||||
if (outElem.kind !== "union") return false;
|
||||
return lowerer.armTag(outElem.unionId, t) >= 0;
|
||||
};
|
||||
const plan: FlatMapUnionPlan["arms"] = [];
|
||||
for (const arm of arms) {
|
||||
if (arm.kind === "array" && emptyArms.some((e) => typeEquals(e, arm))) plan.push("empty");
|
||||
// An array arm copies its elements as they are stored, a present
|
||||
// undefined included, so its element type is the result's.
|
||||
else if (arm.kind === "array" && typeEquals(arm.elem, outElem)) plan.push("spread");
|
||||
else if (arm.kind !== "array" && converts(arm)) plan.push("value");
|
||||
else return null;
|
||||
}
|
||||
return { outElem, arms: plan };
|
||||
}
|
||||
|
||||
/** The flatMap loop over a union-returning callback: each result tests
|
||||
* its arm and appends its elements, nothing, or itself (Node's
|
||||
* IsArray decision per value). */
|
||||
function flatMapUnionHelper(
|
||||
lowerer: Lowerer,
|
||||
elem: IrType,
|
||||
fnRet: IrType & { kind: "union" },
|
||||
arity: number,
|
||||
plan: FlatMapUnionPlan,
|
||||
loc: SrcLoc,
|
||||
present: boolean,
|
||||
): string {
|
||||
// The plan is part of the key: one union result type can carry an empty
|
||||
// `never[]` arm in one callback and a populated array arm in another.
|
||||
const key = `flatMapUnion:${typeKey(elem)}:${typeKey(fnRet)}:${typeKey(plan.outElem)}:${plan.arms.join(",")}:${arity}${present ? ":present" : ""}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = `%arr.flatMap.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(key, name);
|
||||
const arrT = arrayOf(elem);
|
||||
const outT = arrayOf(plan.outElem);
|
||||
const fnT = funcOf([callbackValueType(lowerer, elem, present), F64, arrT].slice(0, arity), fnRet);
|
||||
const arms = lowerer.unions.get(fnRet.unionId)!.arms;
|
||||
const outRef = (): IrExpr => varRef("out.0", outT, loc);
|
||||
const resultRef = (): IrExpr => varRef("r.0", fnRet, loc);
|
||||
const outLength = (): IrExpr => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: outRef(),
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const locals: IrLocal[] = [
|
||||
...arrayCallbackLocals(arrT, fnT),
|
||||
{ id: "out.0", name: "out", type: outT, mutable: false },
|
||||
{ id: "r.0", name: "r", type: fnRet, mutable: false },
|
||||
];
|
||||
const armBody = (tag: number): IrStmt[] => {
|
||||
const arm = arms[tag]!;
|
||||
const narrowed: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
unionId: fnRet.unionId,
|
||||
tag,
|
||||
value: resultRef(),
|
||||
type: arm,
|
||||
loc,
|
||||
};
|
||||
if (plan.arms[tag] === "empty") return [];
|
||||
if (plan.arms[tag] === "value") {
|
||||
return [
|
||||
arrayValueStore(
|
||||
lowerer,
|
||||
outRef(),
|
||||
outLength(),
|
||||
lowerer.coerceToExpected(
|
||||
// A unit arm's value is the unit itself, which has no
|
||||
// narrowed representation.
|
||||
isUnitType(arm)
|
||||
? {
|
||||
kind: "unitLit",
|
||||
unit: arm.kind === "nullT" ? "null" : "undefined",
|
||||
type: arm,
|
||||
loc,
|
||||
}
|
||||
: narrowed,
|
||||
plan.outElem,
|
||||
),
|
||||
plan.outElem,
|
||||
loc,
|
||||
),
|
||||
];
|
||||
}
|
||||
const innerT = arm as IrType & { kind: "array" };
|
||||
const listId = `list.${tag}`;
|
||||
const lengthId = `m.${tag}`;
|
||||
const indexId = `j.${tag}`;
|
||||
const itemId = `inner.${tag}`;
|
||||
locals.push(
|
||||
{ id: listId, name: "list", type: innerT, mutable: false },
|
||||
{ id: lengthId, name: "m", type: F64, mutable: false },
|
||||
{ id: indexId, name: "j", type: F64, mutable: true },
|
||||
{ id: itemId, name: "inner", type: arrayValueType(lowerer, innerT.elem), mutable: false },
|
||||
);
|
||||
const list = (): IrExpr => varRef(listId, innerT, loc);
|
||||
const index = (): IrExpr => varRef(indexId, F64, loc);
|
||||
return [
|
||||
{ kind: "varDecl", localId: listId, init: narrowed, loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: lengthId,
|
||||
init: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: list(),
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "for",
|
||||
init: { kind: "varDecl", localId: indexId, init: numLit(0, loc), loc },
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "<",
|
||||
left: index(),
|
||||
right: varRef(lengthId, F64, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
update: {
|
||||
kind: "assign",
|
||||
localId: indexId,
|
||||
value: { kind: "bin", op: "+", left: index(), right: numLit(1, loc), type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
body: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: arrayIndexPresent(list(), index(), loc),
|
||||
then: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: itemId,
|
||||
init: arrayValueRead(lowerer, list(), index(), innerT.elem, loc),
|
||||
loc,
|
||||
},
|
||||
arrayValueStore(
|
||||
lowerer,
|
||||
outRef(),
|
||||
outLength(),
|
||||
varRef(itemId, arrayValueType(lowerer, innerT.elem), loc),
|
||||
plan.outElem,
|
||||
loc,
|
||||
),
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
];
|
||||
};
|
||||
let dispatch: IrStmt[] = armBody(arms.length - 1);
|
||||
for (let tag = arms.length - 2; tag >= 0; tag--) {
|
||||
dispatch = [
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId: fnRet.unionId,
|
||||
tag,
|
||||
value: resultRef(),
|
||||
negated: false,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: armBody(tag),
|
||||
else_: dispatch,
|
||||
loc,
|
||||
},
|
||||
];
|
||||
}
|
||||
const body: IrStmt[] = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "out.0",
|
||||
init: { kind: "arrayLit", elems: [], type: outT, loc },
|
||||
loc,
|
||||
},
|
||||
arrayLengthDeclaration(arrT, loc),
|
||||
countedFor(loc, varRef("n.0", F64, loc), () => [
|
||||
{
|
||||
kind: "if",
|
||||
cond: currentArrayIndexPresent(arrT, loc),
|
||||
then: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "r.0",
|
||||
init: callArrayCallback(
|
||||
arrT,
|
||||
fnT,
|
||||
fnRet,
|
||||
arity,
|
||||
callbackValueRead(
|
||||
lowerer,
|
||||
varRef("a.0", arrT, loc),
|
||||
varRef("i.0", F64, loc),
|
||||
elem,
|
||||
present,
|
||||
loc,
|
||||
),
|
||||
loc,
|
||||
),
|
||||
loc,
|
||||
},
|
||||
...dispatch,
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
]),
|
||||
{ kind: "return", value: outRef(), loc },
|
||||
];
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: arrayCallbackParams(arrT, fnT),
|
||||
returnType: outT,
|
||||
locals,
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
/** The flatMap loop (array-returning callback), from existing IR nodes:
|
||||
*
|
||||
* out = []; n = a.length;
|
||||
|
||||
@@ -97,11 +97,15 @@ export function arrayConcatHelper(
|
||||
return [
|
||||
{ kind: "varDecl", localId: "offset.0", init: length(out), loc },
|
||||
{ kind: "varDecl", localId: "count.0", init: length(source), loc },
|
||||
// Storage is reserved first and the length is set after the copy, so
|
||||
// a source without holes is copied by appends and the result gets no
|
||||
// state bytes.
|
||||
{
|
||||
kind: "arraySetLength",
|
||||
arr: out,
|
||||
length: { kind: "bin", op: "+", left: offset, right: count, type: F64, loc },
|
||||
loc,
|
||||
reserve: true,
|
||||
},
|
||||
countedFor(loc, count, () => [
|
||||
{
|
||||
@@ -134,6 +138,12 @@ export function arrayConcatHelper(
|
||||
loc,
|
||||
},
|
||||
]),
|
||||
{
|
||||
kind: "arraySetLength",
|
||||
arr: out,
|
||||
length: { kind: "bin", op: "+", left: offset, right: count, type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
];
|
||||
};
|
||||
const body: IrStmt[] = [
|
||||
|
||||
@@ -1263,17 +1263,28 @@ class FusedLoop {
|
||||
loc,
|
||||
});
|
||||
if (holes) {
|
||||
// The result keeps the source's holes: storage for its length is
|
||||
// reserved first and the length is set after the loop, so a result
|
||||
// without holes is filled by appends and gets no state bytes.
|
||||
const length = (): IrExpr => ({
|
||||
kind: "ternary",
|
||||
cond: { kind: "bin", op: "<", left: lo, right: hi, type: BOOL, loc },
|
||||
then: { kind: "bin", op: "-", left: hi, right: lo, type: F64, loc },
|
||||
else_: numLit(0, loc),
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
stateDecls.push({
|
||||
kind: "arraySetLength",
|
||||
arr: varRef(out, resultT, loc),
|
||||
length: {
|
||||
kind: "ternary",
|
||||
cond: { kind: "bin", op: "<", left: lo, right: hi, type: BOOL, loc },
|
||||
then: { kind: "bin", op: "-", left: hi, right: lo, type: F64, loc },
|
||||
else_: numLit(0, loc),
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
length: length(),
|
||||
loc,
|
||||
reserve: true,
|
||||
});
|
||||
epilogue.push({
|
||||
kind: "arraySetLength",
|
||||
arr: varRef(out, resultT, loc),
|
||||
length: length(),
|
||||
loc,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1414,7 +1414,8 @@ const BUF_ENCODINGS: Record<string, string | undefined> = {
|
||||
* does not know — the ladder callers (stream options) turn unknown
|
||||
* literals into Node's runtime ERR_UNKNOWN_ENCODING throw. */
|
||||
export function knownBufEncoding(name: string): string | undefined {
|
||||
return own(BUF_ENCODINGS, name);
|
||||
// Buffer.isEncoding: any letter case
|
||||
return own(BUF_ENCODINGS, name) ?? own(BUF_ENCODINGS, name.toLowerCase());
|
||||
}
|
||||
|
||||
/** The literal encoding argument of a Buffer surface, normalized — or a
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
isPrimitiveCollectionKey,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
UNDEFINED_T,
|
||||
} from "../../../ir/ir.js";
|
||||
import {
|
||||
COLLECTION_ITERATOR_METHODS,
|
||||
@@ -44,7 +45,7 @@ import { strCharsCall } from "./array-construction.js";
|
||||
import { arrayElementRead } from "./array-iteration.js";
|
||||
import { isSearchParamsIterable } from "../builtins/url.js";
|
||||
import { classGeneratorIterable } from "../class-iterables.js";
|
||||
import { generatorDrain } from "../iterator-adapters.js";
|
||||
import { generatorDrain, lowerCollectionIteratorValue } from "../iterator-adapters.js";
|
||||
|
||||
/** Ambient Map method calls. `get`/`set`/`has`/`delete`/`clear` lower to
|
||||
* mapIntrinsic; `forEach` desugars to a direct call of a synthetic loop
|
||||
@@ -1345,6 +1346,17 @@ export function mapFromSeedValue(
|
||||
const drained = generatorDrain(lowerer, generator, entry, loc);
|
||||
return drained ? mapFromSeedValue(lowerer, drained, mapT) : null;
|
||||
}
|
||||
// A Set of [key, value] pairs walks its values like a generator of them.
|
||||
if (seed.type.kind === "set" && seed.type.elem.kind === "record") {
|
||||
const walk = lowerCollectionIteratorValue(
|
||||
lowerer,
|
||||
seed,
|
||||
"values",
|
||||
{ kind: "generator", yieldT: seed.type.elem, retT: VOID, nextT: UNDEFINED_T },
|
||||
loc,
|
||||
);
|
||||
return walk ? mapFromSeedValue(lowerer, walk, mapT) : null;
|
||||
}
|
||||
if (seed.type.kind === "map") {
|
||||
if (!typeEquals(seed.type, mapT)) return null;
|
||||
return { kind: "mapIntrinsic", method: "clone", receiver: seed, args: [], type: mapT, loc };
|
||||
|
||||
@@ -727,6 +727,11 @@ function keyOrderFinding(
|
||||
written: readonly string[],
|
||||
assignedNames: ReadonlySet<string>,
|
||||
): { native: string; node: string; detail: "reordered" | "added-later" } | null {
|
||||
// Values of a shape with orderVariants enumerate in the order their
|
||||
// literal wrote (key-order-witness.ts).
|
||||
const mapped = safely(() => lowerer.mapTypeOf(contextual));
|
||||
if (mapped?.kind === "record" && lowerer.shapes.get(mapped.shapeId)?.orderVariants !== undefined)
|
||||
return null;
|
||||
// A shape whose table the witness pass re-chose (key-order-witness.ts)
|
||||
// enumerates that table, not the checker's declaration order.
|
||||
const table = witnessTable(lowerer, contextual);
|
||||
|
||||
@@ -58,6 +58,25 @@ export function jsHoldsDynObject(lowerer: Lowerer, expr: ts.Expression): boolean
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The literal is the source of an array destructuring: an assignment's
|
||||
* right side or a declaration's initializer under an array pattern. */
|
||||
function isPatternSource(expr: ts.ArrayLiteralExpression): boolean {
|
||||
let node: ts.Node = expr;
|
||||
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const parent = node.parent;
|
||||
if (ts.isBinaryExpression(parent))
|
||||
return (
|
||||
parent.right === node &&
|
||||
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isArrayLiteralExpression(parent.left)
|
||||
);
|
||||
return (
|
||||
ts.isVariableDeclaration(parent) &&
|
||||
parent.initializer === node &&
|
||||
ts.isArrayBindingPattern(parent.name)
|
||||
);
|
||||
}
|
||||
|
||||
/** An empty TypeScript literal whose own type is `never[]`: tsc's type for
|
||||
* `[]` with no contextual array type (`return []` in an async function,
|
||||
* whose contextual type is `string[] | PromiseLike<string[]>`; `{ files: [] }`).
|
||||
@@ -125,6 +144,22 @@ export function lowerArrayLiteral(
|
||||
)
|
||||
mapped = inferred;
|
||||
}
|
||||
// A destructuring pattern's implied type ends in a rest element
|
||||
// (`[h, ...t] = [1, 2, 3]` supplies `[number, ...number[]]`), which maps
|
||||
// to an array. The literal keeps its own fixed tuple layout, read by
|
||||
// position like any tuple source.
|
||||
if (
|
||||
expected === undefined &&
|
||||
ctxType &&
|
||||
isPatternSource(expr) &&
|
||||
lowerer.checker.isTupleType(ctxType) &&
|
||||
(
|
||||
(ctxType as ts.TupleTypeReference).getTarget() as ts.TupleType | undefined
|
||||
)?.elementFlags?.some((flag) => (flag & ts.ElementFlags.Rest) !== 0)
|
||||
) {
|
||||
const own = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (own?.kind === "record" && lowerer.shapes.get(own.shapeId)?.tuple) mapped = own;
|
||||
}
|
||||
// A JS literal whose OWN inferred type is never-tainted
|
||||
// (neverTaintedJsType — the evolving `const gb = []`, the mixed
|
||||
// command tuple `['pwd', []]`) carries no element information: route
|
||||
|
||||
@@ -1321,6 +1321,15 @@ export function lowerRecordKeyRead(
|
||||
shape.fields.every((f) => typeEquals(f.type, shape.fields[0]!.type))
|
||||
) {
|
||||
declared = shape.fields[0]!.type;
|
||||
} else if (shape.fields.length > 0) {
|
||||
// Fields of different types (`row[key]` with `key: keyof Row`): the
|
||||
// checker types the read as the union of the field types the key can
|
||||
// name. When every declared field surfaces in that union (an arm, or a
|
||||
// union that widens into it), the per-field dispatch wraps each hit
|
||||
// into its arm.
|
||||
const read = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (read !== null && read.kind === "union" && recordKeyResultOk(lowerer, shape, read))
|
||||
declared = read;
|
||||
}
|
||||
// Fields of different types answer a checked-dynamic value where nothing
|
||||
// typed the read: in JavaScript, and where the checker types it 'any'
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
runtimeOptionalUseOf,
|
||||
} from "../lower-exprs.js";
|
||||
import { runtimeOptionalReceiverRead } from "./narrowing.js";
|
||||
import { iterableUnionOf } from "../iterable-values.js";
|
||||
import { builtinGlobalValueAt } from "./property-access.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import {
|
||||
@@ -69,6 +70,7 @@ import {
|
||||
builtinModuleConstOf,
|
||||
builtinModuleFnOf,
|
||||
builtinModulesArrayLit,
|
||||
identifierReadSymbol,
|
||||
stdlibGlobalNameOf,
|
||||
} from "../surfaces.js";
|
||||
import { builtinConstantsObject } from "../lower-builtin-values.js";
|
||||
@@ -321,6 +323,16 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
// value arm before routing the member/element operation so dynamic
|
||||
// keys and builtin record walkers do not see a union receiver.
|
||||
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
// An optional Iterable proven present re-tags into the Iterable.
|
||||
if (
|
||||
g.type.kind === "union" &&
|
||||
narrowed?.kind === "union" &&
|
||||
narrowed.unionId !== g.type.unionId &&
|
||||
iterableUnionOf(lowerer, narrowed)
|
||||
) {
|
||||
const helper = lowerer.narrowedRetagHelper(expr, g.type.unionId, narrowed.unionId, loc);
|
||||
if (helper) return { kind: "call", callee: helper, args: [ref], type: narrowed, loc };
|
||||
}
|
||||
const arithmeticUnion =
|
||||
g.type.kind === "union" &&
|
||||
lowerer.unions.get(g.type.unionId)?.arms.length === 2 &&
|
||||
@@ -753,7 +765,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
return lowerGlobalValue(lowerer, expr);
|
||||
}
|
||||
{
|
||||
const sym = lowerer.checker.getSymbolAtLocation(expr);
|
||||
const sym = identifierReadSymbol(lowerer, expr);
|
||||
// The temporal global is typed (TS 7's lib slice) but Node 24 does
|
||||
// not define it: the read throws Node's ReferenceError.
|
||||
if (isTemporalGlobalRoot(lowerer, expr, sym)) {
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
} from "../lower-exprs.js";
|
||||
import { isConstAssertionTypeNode, unitOnlyUnion } from "../../type-mapper.js";
|
||||
import { assertsGlobalObject, extractionCopiesReference } from "../assertion-identity.js";
|
||||
import { iterableSlotValue, iterableUnionOf } from "../iterable-values.js";
|
||||
|
||||
/** `c ? a : b`. An explicit destination constructs each branch at that
|
||||
* layout before joining it. Without one, retain the expression's inferred
|
||||
@@ -692,6 +693,18 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
const hosted = hostedStorageView(lowerer, expr, narrowed, expr.loc);
|
||||
if (hosted) return hosted;
|
||||
}
|
||||
// An optional Iterable narrowed to the iterable union (`xs !== undefined`):
|
||||
// re-tag into it, so for-of, spread and the other consumers see the
|
||||
// Iterable itself.
|
||||
if (
|
||||
narrowed?.kind === "union" &&
|
||||
narrowed.unionId !== expr.type.unionId &&
|
||||
iterableUnionOf(lowerer, narrowed)
|
||||
) {
|
||||
const helper = lowerer.narrowedRetagHelper(node, expr.type.unionId, narrowed.unionId, expr.loc);
|
||||
if (helper)
|
||||
return { kind: "call", callee: helper, args: [expr], type: narrowed, loc: expr.loc };
|
||||
}
|
||||
// A checker type narrowed to a UNIT arm (the `=== undefined` branch)
|
||||
// also stays union-typed: a unit arm has no payload to extract, and
|
||||
// nothing useful reads such a value anyway. (Standalone undefined maps
|
||||
@@ -1524,6 +1537,10 @@ export function lowerAsExpression(
|
||||
// reach it (assertion-identity.ts).
|
||||
if (extractionCopiesReference(lowerer, target) && assertsGlobalObject(lowerer, expr))
|
||||
return inner;
|
||||
// `value as Iterable<T>`: the value's own iterator, stepped lazily with
|
||||
// each element checked against T (the generator arm's adapter).
|
||||
const iterable = iterableSlotValue(lowerer, inner, target);
|
||||
if (iterable) return iterable;
|
||||
if (
|
||||
target.kind === "void" ||
|
||||
!canDynCheckTo(
|
||||
|
||||
@@ -933,11 +933,11 @@ export function lowerObjectLiteral(
|
||||
// spread's own position (any position: they introduce one fresh
|
||||
// name, collision-fenced below). Everything else stays fenced.
|
||||
const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression);
|
||||
if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
|
||||
if (cs === "unsupported" && conditionalRecordSpreadOf(lowerer, prop.expression) === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
prop,
|
||||
"conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields)",
|
||||
"conditional spreads beyond `...(c ? { field: v } : {})` and `...(c ? record : {})` (one arm must be empty — spell other shapes as optional fields)",
|
||||
);
|
||||
}
|
||||
// Spread ORDER is fenced in the field-by-field desugar below, not
|
||||
@@ -1721,64 +1721,71 @@ export function lowerObjectLiteral(
|
||||
if (ts.isSpreadAssignment(prop)) {
|
||||
const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression);
|
||||
if (cs && cs !== "unsupported") {
|
||||
// `...(c ? { k: v } : {})` — ONE conditional field at the
|
||||
// spread's own position: cond evaluates exactly once, `v` only
|
||||
// when the non-empty arm is taken (ternary arms are lazy), and
|
||||
// the empty arm holds the interned undefined arm — exactly the
|
||||
// omitted-optional-field representation, which is also why the
|
||||
// target field must be optional (tsc types the idiom that way).
|
||||
const csProp = cs.props[0]!; // single-prop-checked in the fence pass
|
||||
const name = csProp.name.text;
|
||||
const fieldType = fieldTypes.get(name);
|
||||
if (!fieldType) {
|
||||
if (shape.indexValue) {
|
||||
// `...(c ? { k: v } : {})` — one conditional field per carried
|
||||
// property at the spread's own position: cond evaluates exactly
|
||||
// once (into a hidden local when several fields test it), each
|
||||
// `v` only when the non-empty arm is taken (ternary arms are
|
||||
// lazy), in property order, and the empty arm holds the interned
|
||||
// undefined arm — exactly the omitted-optional-field
|
||||
// representation, which is also why the target fields must be
|
||||
// optional (tsc types the idiom that way).
|
||||
let cond: IrExpr | null = null;
|
||||
for (const csProp of cs.props) {
|
||||
const name = csProp.name.text;
|
||||
const fieldType = fieldTypes.get(name);
|
||||
if (!fieldType) {
|
||||
if (shape.indexValue) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
prop,
|
||||
"conditional spreads into index-signature keys (overflow entries model presence — write the key in an if statement instead)",
|
||||
);
|
||||
}
|
||||
shapeMismatch(prop);
|
||||
}
|
||||
// An 'unknown' slot holds the checked-dynamic undefined for the
|
||||
// empty arm, the absent-property read the width copy uses too.
|
||||
const absent =
|
||||
lowerer.absentFieldValue(fieldType, locOf(prop)) ??
|
||||
(fieldType.kind === "dyn" ? dynUndefinedExpr(locOf(prop)) : null);
|
||||
if (!absent) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
prop,
|
||||
"conditional spreads into index-signature keys (overflow entries model presence — write the key in an if statement instead)",
|
||||
`conditional spreads onto the required field '${name}' (the empty arm leaves it undefined — declare the field optional)`,
|
||||
);
|
||||
}
|
||||
shapeMismatch(prop);
|
||||
if (fields.some((f) => f.name === name)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
prop,
|
||||
`conditional spread of '${name}' over an earlier '${name}' (the desugar keeps one entry per name — restructure so each name has one contributor)`,
|
||||
);
|
||||
}
|
||||
if (cond === null) {
|
||||
cond = lowerer.lowerCondition(cs.cond);
|
||||
if (cs.props.length > 1) cond = capture(cond);
|
||||
}
|
||||
const valueNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp;
|
||||
let v = ts.isPropertyAssignment(csProp)
|
||||
? lowerer.lowerExpr(csProp.initializer)
|
||||
: lowerer.lowerShorthandValue(csProp);
|
||||
v = lowerer.coerceInto(valueNode, v, fieldType);
|
||||
if (!typeEquals(v.type, fieldType)) lowerer.badType(valueNode, lowerer.typeOf(valueNode));
|
||||
conditionalNames.add(name);
|
||||
orderSegments.push({ kind: "key", name, definite: false });
|
||||
fields.push({
|
||||
name,
|
||||
value: {
|
||||
kind: "ternary",
|
||||
cond,
|
||||
then: cs.whenTrue ? v : absent,
|
||||
else_: cs.whenTrue ? absent : v,
|
||||
type: fieldType,
|
||||
loc: locOf(prop),
|
||||
},
|
||||
});
|
||||
}
|
||||
// An 'unknown' slot holds the checked-dynamic undefined for the
|
||||
// empty arm, the absent-property read the width copy uses too.
|
||||
const absent =
|
||||
lowerer.absentFieldValue(fieldType, locOf(prop)) ??
|
||||
(fieldType.kind === "dyn" ? dynUndefinedExpr(locOf(prop)) : null);
|
||||
if (!absent) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
prop,
|
||||
`conditional spreads onto the required field '${name}' (the empty arm leaves it undefined — declare the field optional)`,
|
||||
);
|
||||
}
|
||||
if (fields.some((f) => f.name === name)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
prop,
|
||||
`conditional spread of '${name}' over an earlier '${name}' (the desugar keeps one entry per name — restructure so each name has one contributor)`,
|
||||
);
|
||||
}
|
||||
const cond = lowerer.lowerCondition(cs.cond);
|
||||
const valueNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp;
|
||||
let v = ts.isPropertyAssignment(csProp)
|
||||
? lowerer.lowerExpr(csProp.initializer)
|
||||
: lowerer.lowerShorthandValue(csProp);
|
||||
v = lowerer.coerceInto(valueNode, v, fieldType);
|
||||
if (!typeEquals(v.type, fieldType)) lowerer.badType(valueNode, lowerer.typeOf(valueNode));
|
||||
conditionalNames.add(name);
|
||||
orderSegments.push({ kind: "key", name, definite: false });
|
||||
fields.push({
|
||||
name,
|
||||
value: {
|
||||
kind: "ternary",
|
||||
cond,
|
||||
then: cs.whenTrue ? v : absent,
|
||||
else_: cs.whenTrue ? absent : v,
|
||||
type: fieldType,
|
||||
loc: locOf(prop),
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// `{ ...base, ... }` — field-by-field copy of a known record
|
||||
@@ -1790,10 +1797,13 @@ export function lowerObjectLiteral(
|
||||
// overridden. Field snapshots above preserve the copy order.
|
||||
let srcNode: ts.Expression = prop.expression;
|
||||
while (ts.isParenthesizedExpression(srcNode)) srcNode = srcNode.expression;
|
||||
const condRecord = conditionalRecordSpreadOf(lowerer, prop.expression);
|
||||
const srcLowered = capture(
|
||||
prop === expr.properties.nodes[0] && leadingSpreadLowered !== null
|
||||
? leadingSpreadLowered
|
||||
: lowerer.lowerExpr(srcNode),
|
||||
condRecord !== null
|
||||
? lowerConditionalRecordSpread(lowerer, condRecord, locOf(prop))
|
||||
: prop === expr.properties.nodes[0] && leadingSpreadLowered !== null
|
||||
? leadingSpreadLowered
|
||||
: lowerer.lowerExpr(srcNode),
|
||||
);
|
||||
// Array callbacks can receive a record-or-undefined ABI even when
|
||||
// TypeScript describes a required record. Select the copy strategy
|
||||
@@ -2731,6 +2741,62 @@ function syntacticAndSpread(
|
||||
return { cond, props, whenTrue: true };
|
||||
}
|
||||
|
||||
/** `...(c ? source : {})` (either arm order; the empty arm may also be
|
||||
* `undefined` or `null`) where `source` maps to a record and is not the
|
||||
* literal the field desugar handles (conditionalSpreadOf). The spread
|
||||
* copies the source's fields when the source arm is taken and nothing
|
||||
* otherwise, which is the copy of a `Record | undefined` spread source. */
|
||||
export function conditionalRecordSpreadOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): { cond: ts.Expression; source: ts.Expression; whenTrue: boolean } | null {
|
||||
let e: ts.Expression = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isConditionalExpression(e)) return null;
|
||||
const unwrap = (a: ts.Expression): ts.Expression => {
|
||||
let x = a;
|
||||
while (ts.isParenthesizedExpression(x)) x = x.expression;
|
||||
return x;
|
||||
};
|
||||
const isEmpty = (x: ts.Expression): boolean =>
|
||||
(ts.isObjectLiteralExpression(x) && x.properties.nodes.length === 0) ||
|
||||
x.kind === ts.SyntaxKind.NullKeyword ||
|
||||
(ts.isIdentifier(x) &&
|
||||
x.text === "undefined" &&
|
||||
(lowerer.typeOf(x).flags & ts.TypeFlags.Undefined) !== 0);
|
||||
const whenTrue = unwrap(e.whenTrue);
|
||||
const whenFalse = unwrap(e.whenFalse);
|
||||
const sourceIsTrue = isEmpty(whenFalse) && !isEmpty(whenTrue);
|
||||
if (!sourceIsTrue && !(isEmpty(whenTrue) && !isEmpty(whenFalse))) return null;
|
||||
const source = sourceIsTrue ? whenTrue : whenFalse;
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(source))?.kind !== "record") return null;
|
||||
return { cond: e.condition, source, whenTrue: sourceIsTrue };
|
||||
}
|
||||
|
||||
/** The spread source of a conditionalRecordSpreadOf match: `c ? source :
|
||||
* undefined` as a `Record | undefined` value (cond first, the source only
|
||||
* when its arm is taken). */
|
||||
function lowerConditionalRecordSpread(
|
||||
lowerer: Lowerer,
|
||||
spread: { cond: ts.Expression; source: ts.Expression; whenTrue: boolean },
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const cond = lowerer.lowerCondition(spread.cond);
|
||||
const source = lowerer.lowerExpr(spread.source);
|
||||
if (source.type.kind !== "record") lowerer.badType(spread.source, lowerer.typeOf(spread.source));
|
||||
const type = lowerer.withUndefinedArm(source.type);
|
||||
const present = lowerer.coerceToExpected(source, type);
|
||||
const absent = lowerer.wrappedUndefined(type, loc)!;
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond,
|
||||
then: spread.whenTrue ? present : absent,
|
||||
else_: spread.whenTrue ? absent : present,
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A conditional spread's carrier property: `name: value` or shorthand,
|
||||
* with an identifier/string-literal name. */
|
||||
export type CondSpreadProp = (ts.PropertyAssignment | ts.ShorthandPropertyAssignment) & {
|
||||
@@ -3262,6 +3328,9 @@ export function lowerShorthandValue(
|
||||
}
|
||||
return lowerer.classValueRef(classInfo, propName);
|
||||
}
|
||||
// A standard-library global (`{ Date }`): the value the long form
|
||||
// `{ Date: Date }` reads.
|
||||
if (lowerer.isStdlibSymbol(resolved)) return lowerer.lowerExpr(propName);
|
||||
}
|
||||
lowerer.rejectUnresolvedSymbol(
|
||||
symbol
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { iterableGeneratorArm } from "./iterable-values.js";
|
||||
import { mapAsyncGeneratorAnnotation } from "../type-mapper.js";
|
||||
import { dynUndefinedExpr } from "../../ir/build.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
@@ -147,6 +148,9 @@ export function declaredReturnType(
|
||||
// (`.action(() => { throw ... })`). `never` VALUES stay unmapped.
|
||||
if (retTsType.flags & ts.TypeFlags.Never) return VOID;
|
||||
let mappedReturn = lowerer.mapTypeOf(retTsType);
|
||||
// A generator declared to return Iterable<T> returns a generator over T.
|
||||
if (mappedReturn && decl.asteriskToken)
|
||||
mappedReturn = iterableGeneratorArm(lowerer, mappedReturn);
|
||||
if (
|
||||
mappedReturn === null &&
|
||||
decl.asteriskToken &&
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
/* `Iterable<T>` values. The type maps to the iterable union
|
||||
* (type-mapper.ts iterableUnionType): an array, Set, Map or string keeps
|
||||
* its own arm, so the value stays the same multi-pass object (identity,
|
||||
* inspection and typeof are the source's own), and every iterator — a
|
||||
* generator, an array, Map or Set iterator, a checked-dynamic iterator —
|
||||
* enters the generator arm, single-pass as in JavaScript.
|
||||
*
|
||||
* Consumers open an iterator over the current arm the way for-of's
|
||||
* GetIterator does: an array, Set, Map or string arm starts a fresh live
|
||||
* walk (the sources' own iterator helpers), the generator arm is itself.
|
||||
* for-of gives the array arm its own array loop, so iterating an Iterable
|
||||
* that holds an array runs at array-loop speed. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { varRef, numLit } from "../../ir/build.js";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
iterableArmsElem,
|
||||
typeEquals,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import { typeKey } from "../type-mapper.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { PoisonError, type Lowerer } from "./lowerer.js";
|
||||
import { noLoweringDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
|
||||
import { generatorLoopBinding, lowerForOfGenerator, type GenType } from "./lower-generators.js";
|
||||
import {
|
||||
iteratorSlotAdapter,
|
||||
lowerArrayIteratorValue,
|
||||
lowerCollectionIteratorValue,
|
||||
lowerStringIteratorValue,
|
||||
} from "./iterator-adapters.js";
|
||||
import { arrayValueRead, arrayValueType } from "./array-values.js";
|
||||
|
||||
/** An iterable union and the tags of its arms. */
|
||||
export interface IterableUnion {
|
||||
unionId: string;
|
||||
type: IrType & { kind: "union" };
|
||||
elem: IrType;
|
||||
arms: readonly IrType[];
|
||||
/** The generator arm: a sync generator over `elem`. */
|
||||
gen: GenType;
|
||||
}
|
||||
|
||||
/** The iterable union `t` names, or null. */
|
||||
export function iterableUnionOf(lowerer: Lowerer, t: IrType): IterableUnion | null {
|
||||
const info = iterableSlotUnionOf(lowerer, t);
|
||||
// An optional Iterable (`Iterable<T> | undefined`) is consumed only once
|
||||
// narrowed to the iterable union itself.
|
||||
if (info === null || info.arms.some((arm) => arm.kind === "undefinedT" || arm.kind === "nullT"))
|
||||
return null;
|
||||
return info;
|
||||
}
|
||||
|
||||
/** The iterable union `t` names, also an optional or nullable one
|
||||
* (`Iterable<T> | undefined`, whose unit arms sit beside the iterable
|
||||
* arms), or null. */
|
||||
function iterableSlotUnionOf(lowerer: Lowerer, t: IrType): IterableUnion | null {
|
||||
if (t.kind !== "union") return null;
|
||||
const arms = lowerer.unions.get(t.unionId)?.arms;
|
||||
if (!arms) return null;
|
||||
const elem = iterableArmsElem(arms);
|
||||
if (elem === null) return null;
|
||||
const gen = arms.find((arm) => arm.kind === "generator") as GenType;
|
||||
return { unionId: t.unionId, type: t, elem, arms, gen };
|
||||
}
|
||||
|
||||
/** The generator arm of an iterable union (what a generator function
|
||||
* declared to return Iterable<T> returns), or `t` itself. */
|
||||
export function iterableGeneratorArm(lowerer: Lowerer, t: IrType): IrType {
|
||||
return iterableUnionOf(lowerer, t)?.gen ?? t;
|
||||
}
|
||||
|
||||
/** A value flowing into an iterable-typed slot, optional or nullable
|
||||
* ones included: a source arm wraps as itself; a generator over other
|
||||
* channel types or a checked-dynamic iterator (a Map or Set iterator)
|
||||
* adapts into the generator arm. Null when neither applies, leaving the
|
||||
* ordinary conversion fences to refuse other sources (class iterables,
|
||||
* typed arrays, other element types). */
|
||||
export function iterableSlotValue(lowerer: Lowerer, expr: IrExpr, expected: IrType): IrExpr | null {
|
||||
const info = iterableSlotUnionOf(lowerer, expected);
|
||||
if (info === null || expr.type.kind === "union") return null;
|
||||
const wrap = (value: IrExpr): IrExpr => ({
|
||||
kind: "unionWrap",
|
||||
unionId: info.unionId,
|
||||
tag: lowerer.armTag(info.unionId, value.type),
|
||||
value,
|
||||
type: info.type,
|
||||
loc: expr.loc,
|
||||
});
|
||||
if (lowerer.armTag(info.unionId, expr.type) >= 0) return wrap(expr);
|
||||
if (expr.type.kind === "generator" || expr.type.kind === "dyn") {
|
||||
const adapted = iteratorSlotAdapter(lowerer, expr, info.gen);
|
||||
if (adapted && typeEquals(adapted.type, info.gen)) return wrap(adapted);
|
||||
}
|
||||
// Class instances (their own [Symbol.iterator]) and typed arrays have no
|
||||
// arm: refuse them here, by name.
|
||||
if (expr.type.kind === "object" || expr.type.kind === "bytes") {
|
||||
lowerer.pushDiag(
|
||||
unsupportedDiag(
|
||||
"SC1090",
|
||||
expr.loc,
|
||||
`an Iterable<${lowerer.fmt(info.elem)}> holding a '${lowerer.fmt(expr.type)}' (an Iterable value holds an array, Set, Map, string, or iterator)`,
|
||||
),
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** GetIterator on an iterable union value: an interned helper answering a
|
||||
* generator over the current arm — a fresh live walk of an array, Set, Map
|
||||
* or string, or the generator arm itself. */
|
||||
export function iterableGenerator(lowerer: Lowerer, value: IrExpr, info: IterableUnion): IrExpr {
|
||||
const loc = value.loc;
|
||||
const key = `iterableOf:${typeKey(info.type)}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (!name) {
|
||||
const u = varRef("u.0", info.type, loc);
|
||||
const body: IrStmt[] = [];
|
||||
let genTag = -1;
|
||||
info.arms.forEach((arm, tag) => {
|
||||
if (arm.kind === "generator") {
|
||||
genTag = tag;
|
||||
return;
|
||||
}
|
||||
const narrowed: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
unionId: info.unionId,
|
||||
tag,
|
||||
value: u,
|
||||
type: arm,
|
||||
loc,
|
||||
};
|
||||
const walk =
|
||||
arm.kind === "array"
|
||||
? lowerArrayIteratorValue(lowerer, narrowed, "values", info.gen, loc)
|
||||
: arm.kind === "set"
|
||||
? lowerCollectionIteratorValue(lowerer, narrowed, "values", info.gen, loc)
|
||||
: arm.kind === "map"
|
||||
? lowerCollectionIteratorValue(lowerer, narrowed, "entries", info.gen, loc)
|
||||
: arm.kind === "string"
|
||||
? lowerStringIteratorValue(lowerer, narrowed, info.gen, loc)
|
||||
: null;
|
||||
if (walk === null || !typeEquals(walk.type, info.gen)) {
|
||||
lowerer.pushDiag(
|
||||
noLoweringDiag(
|
||||
`iterating an Iterable<${lowerer.fmt(info.elem)}> that holds a ${arm.kind}`,
|
||||
loc,
|
||||
),
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId: info.unionId,
|
||||
tag,
|
||||
negated: false,
|
||||
value: u,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [{ kind: "return", value: walk, loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
});
|
||||
body.push({
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "unionNarrow",
|
||||
unionId: info.unionId,
|
||||
tag: genTag,
|
||||
value: u,
|
||||
type: info.gen,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
name = keyedHelperName(lowerer, "%iterable.of", key);
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "u.0", name: "u", type: info.type }],
|
||||
returnType: info.gen,
|
||||
locals: [{ id: "u.0", name: "u", type: info.type, mutable: false }],
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [value], type: info.gen, loc };
|
||||
}
|
||||
|
||||
/** Whether a loop body declares a class (a class has one lowering per
|
||||
* declaration, so its body cannot lower twice). */
|
||||
function declaresClass(body: ts.Statement): boolean {
|
||||
let found = false;
|
||||
ts.walkPreorder(body, (node) => {
|
||||
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
||||
found = true;
|
||||
return "stop";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/** `for (x of iterable)` over an iterable union:
|
||||
*
|
||||
* { const %src = <iterable>;
|
||||
* if (%src is the array arm) {
|
||||
* const %arr = <array arm>;
|
||||
* for (let %cur = 0; %cur < %arr.length; %cur++) { const x = %arr[%cur]; <body> }
|
||||
* } else {
|
||||
* for (const x of GetIterator(%src)) <body> // the generator for-of
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* The array arm walks exactly like an array for-of (live length, the
|
||||
* backend's dense-array loop forms), so an Iterable holding an array
|
||||
* iterates at array speed; every other arm drives its iterator with the
|
||||
* generator for-of's IteratorClose rules. The body lowers once for each
|
||||
* loop; a body that declares a class lowers only once, in the generator
|
||||
* for-of over every arm. */
|
||||
export function lowerForOfIterable(
|
||||
lowerer: Lowerer,
|
||||
stmt: ts.ForOfStatement,
|
||||
iterable: IrExpr,
|
||||
info: IterableUnion,
|
||||
labels?: string[],
|
||||
): IrStmt {
|
||||
const loc = locOf(stmt);
|
||||
const generatorLoop = (source: IrExpr): IrStmt =>
|
||||
lowerForOfGenerator(
|
||||
lowerer,
|
||||
stmt,
|
||||
iterableGenerator(lowerer, source, info) as IrExpr & { type: GenType },
|
||||
labels,
|
||||
);
|
||||
const arrayTag = info.arms.findIndex((arm) => arm.kind === "array");
|
||||
// A union without an array arm (`Set<T> | Generator<T>`) has only the
|
||||
// generator loop.
|
||||
if (stmt.awaitModifier || declaresClass(stmt.statement) || arrayTag < 0)
|
||||
return generatorLoop(iterable);
|
||||
const arrT = info.arms[arrayTag]!;
|
||||
lowerer.scopes.push(new Map());
|
||||
try {
|
||||
const src = lowerer.declareHiddenLocal("%iterableSource", info.type);
|
||||
const srcRef = (): IrExpr => varRef(src.id, info.type, loc);
|
||||
const arr = lowerer.declareHiddenLocal("%arrayIterator", arrT);
|
||||
const cursor = lowerer.declareHiddenLocal("%arrayCursor", F64);
|
||||
cursor.mutable = true;
|
||||
const arrRef = varRef(arr.id, arrT, loc);
|
||||
const curRef = varRef(cursor.id, F64, loc);
|
||||
// The element: a hole reads undefined, which an undefined-armed T
|
||||
// holds; for other element types the read traps, as typed reads do.
|
||||
const element: IrExpr = typeEquals(arrayValueType(lowerer, info.elem), info.elem)
|
||||
? arrayValueRead(lowerer, arrRef, curRef, info.elem, loc)
|
||||
: { kind: "arrayGet", arr: arrRef, index: curRef, type: info.elem, loc };
|
||||
const arrayLoop = ((): IrStmt => {
|
||||
lowerer.scopes.push(new Map());
|
||||
try {
|
||||
const binding = generatorLoopBinding(lowerer, stmt, element, info.elem);
|
||||
const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement), labels);
|
||||
return {
|
||||
kind: "for",
|
||||
init: { kind: "varDecl", localId: cursor.id, init: numLit(0, loc), loc },
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "<",
|
||||
left: curRef,
|
||||
right: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: arrRef,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
update: {
|
||||
kind: "assign",
|
||||
localId: cursor.id,
|
||||
value: { kind: "bin", op: "+", left: curRef, right: numLit(1, loc), type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
body: [...binding, ...body],
|
||||
...(labels && { labels }),
|
||||
loc,
|
||||
};
|
||||
} finally {
|
||||
lowerer.scopes.pop();
|
||||
}
|
||||
})();
|
||||
return {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "varDecl", localId: src.id, init: iterable, loc },
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId: info.unionId,
|
||||
tag: arrayTag,
|
||||
negated: false,
|
||||
value: srcRef(),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: arr.id,
|
||||
init: {
|
||||
kind: "unionNarrow",
|
||||
unionId: info.unionId,
|
||||
tag: arrayTag,
|
||||
value: srcRef(),
|
||||
type: arrT,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
arrayLoop,
|
||||
],
|
||||
else_: [generatorLoop(srcRef())],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
} finally {
|
||||
lowerer.scopes.pop();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { F64, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js";
|
||||
import {
|
||||
F64,
|
||||
KEY_ORDER_FIELD,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import { observeRecordLiteralRebuilds, transformExpr } from "../../ir/traverse.js";
|
||||
import { ShapeRegistry } from "../type-mapper.js";
|
||||
import {
|
||||
@@ -21,7 +28,7 @@ interface Fixture {
|
||||
notes: KeyOrderNotes;
|
||||
record(fields: [string, IrType][], order: string[]): IrType & { kind: "record" };
|
||||
literal(type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr;
|
||||
run(body: IrStmt[]): void;
|
||||
run(body: IrStmt[]): IrFunction;
|
||||
order(type: IrType & { kind: "record" }): string[] | undefined;
|
||||
}
|
||||
|
||||
@@ -35,6 +42,14 @@ function fixture(): Fixture {
|
||||
classes: new Map(),
|
||||
armTag: (unionId: string, arm: IrType): number =>
|
||||
unionId === "u0" && arm.kind === "undefinedT" ? 1 : -1,
|
||||
recordFieldPresent: (obj: IrExpr, shapeId: string, field: string): IrExpr => ({
|
||||
kind: "recordHas",
|
||||
obj,
|
||||
shapeId,
|
||||
field,
|
||||
type: { kind: "bool" },
|
||||
loc,
|
||||
}),
|
||||
} as unknown as Lowerer;
|
||||
const record = (fields: [string, IrType][], order: string[]): IrType & { kind: "record" } => ({
|
||||
kind: "record",
|
||||
@@ -55,15 +70,20 @@ function fixture(): Fixture {
|
||||
segments ?? keys.map((name) => ({ kind: "key", name, definite: true })),
|
||||
);
|
||||
};
|
||||
const run = (body: IrStmt[]): void => {
|
||||
const run = (body: IrStmt[]): IrFunction => {
|
||||
const fn: IrFunction = { name: "main", params: [], returnType: F64, locals: [], body, loc };
|
||||
applyKeyOrderWitnesses(lowerer, [fn]);
|
||||
return fn;
|
||||
};
|
||||
const order = (type: IrType & { kind: "record" }): string[] | undefined =>
|
||||
shapes.get(type.shapeId)?.declaredOrder;
|
||||
return { lowerer, shapes, notes, record, literal, run, order };
|
||||
}
|
||||
|
||||
/** The order index a literal stores (KEY_ORDER_FIELD), when it stores one. */
|
||||
const orderIndexOf = (lit: IrExpr): IrExpr | undefined =>
|
||||
lit.kind === "recordLit" ? lit.fields.find((x) => x.name === KEY_ORDER_FIELD)?.value : undefined;
|
||||
|
||||
const observe = (value: IrExpr): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "jsonStringify", value, type: { kind: "string" }, loc },
|
||||
@@ -109,7 +129,7 @@ test("literals that agree with the table leave the same array in place", () => {
|
||||
expect(f.notes.changed.size).toBe(0);
|
||||
});
|
||||
|
||||
test("values built in two orders keep today's table", () => {
|
||||
test("values built in two orders each record their literal's order", () => {
|
||||
const f = fixture();
|
||||
const ab = f.record(
|
||||
[
|
||||
@@ -118,8 +138,71 @@ test("values built in two orders keep today's table", () => {
|
||||
],
|
||||
["a", "b"],
|
||||
);
|
||||
f.run([observe(f.literal(ab, ["b", "a"])), observe(f.literal(ab, ["a", "b"]))]);
|
||||
const ba = f.literal(ab, ["b", "a"]);
|
||||
const aOrder = f.literal(ab, ["a", "b"]);
|
||||
f.run([observe(ba), observe(aOrder)]);
|
||||
expect(f.order(ab)).toEqual(["a", "b"]);
|
||||
const shape = f.shapes.get(ab.shapeId)!;
|
||||
expect(shape.orderVariants).toEqual([
|
||||
["a", "b"],
|
||||
["b", "a"],
|
||||
]);
|
||||
expect(shape.fields.map((x) => x.name)).toEqual([KEY_ORDER_FIELD, "a", "b"]);
|
||||
expect(orderIndexOf(ba)).toMatchObject({ kind: "numLit", value: 1 });
|
||||
expect(orderIndexOf(aOrder)).toMatchObject({ kind: "numLit", value: 0 });
|
||||
});
|
||||
|
||||
test("a copy of a value with orders stores its source's index", () => {
|
||||
const f = fixture();
|
||||
const abc = f.record(
|
||||
[
|
||||
["a", F64],
|
||||
["b", F64],
|
||||
["c", F64],
|
||||
],
|
||||
["a", "b", "c"],
|
||||
);
|
||||
const ab = f.record(
|
||||
[
|
||||
["a", F64],
|
||||
["b", F64],
|
||||
],
|
||||
["a", "b"],
|
||||
);
|
||||
const read = (field: string): IrExpr => ({
|
||||
kind: "recordGet",
|
||||
obj: ref("x", abc),
|
||||
shapeId: abc.shapeId,
|
||||
field,
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const rest = f.notes.copy(
|
||||
{
|
||||
kind: "recordLit",
|
||||
fields: [
|
||||
{ name: "a", value: read("a") },
|
||||
{ name: "b", value: read("b") },
|
||||
],
|
||||
type: ab,
|
||||
loc,
|
||||
},
|
||||
abc.shapeId,
|
||||
);
|
||||
f.run([
|
||||
decl("x", f.literal(abc, ["c", "b", "a"])),
|
||||
observe(f.literal(abc, ["a", "b", "c"])),
|
||||
observe(rest),
|
||||
]);
|
||||
expect(f.shapes.get(ab.shapeId)!.orderVariants).toEqual([
|
||||
["a", "b"],
|
||||
["b", "a"],
|
||||
]);
|
||||
expect(orderIndexOf(rest)).toMatchObject({
|
||||
kind: "recordGet",
|
||||
obj: { kind: "varRef", localId: "x" },
|
||||
field: KEY_ORDER_FIELD,
|
||||
});
|
||||
});
|
||||
|
||||
test("an unobserved shape keeps today's table", () => {
|
||||
@@ -337,7 +420,7 @@ test("straight-line writes of late-only keys keep program order", () => {
|
||||
expect(f.order(o)).toEqual(["id", "y", "x"]);
|
||||
});
|
||||
|
||||
test("late-only keys written through different bindings keep today's table", () => {
|
||||
test("late-only keys written through different bindings move to the end per value", () => {
|
||||
const f = fixture();
|
||||
const o = f.record(
|
||||
[
|
||||
@@ -354,9 +437,13 @@ test("late-only keys written through different bindings keep today's table", ()
|
||||
observe(ref("o", o)),
|
||||
]);
|
||||
expect(f.order(o)).toEqual(["id", "x", "y"]);
|
||||
expect(f.shapes.get(o.shapeId)!.orderVariants).toEqual([
|
||||
["id", "x", "y"],
|
||||
["id", "y", "x"],
|
||||
]);
|
||||
});
|
||||
|
||||
test("a late-only key written in a loop over an outer binding keeps today's table", () => {
|
||||
test("a late-only key written in a loop guards the write with its move", () => {
|
||||
const f = fixture();
|
||||
const o = f.record(
|
||||
[
|
||||
@@ -366,7 +453,7 @@ test("a late-only key written in a loop over an outer binding keeps today's tabl
|
||||
],
|
||||
["id", "x", "y"],
|
||||
);
|
||||
f.run([
|
||||
const body: IrStmt[] = [
|
||||
decl("o", f.literal(o, ["id"])),
|
||||
set(ref("o", o), "y"),
|
||||
{
|
||||
@@ -376,11 +463,18 @@ test("a late-only key written in a loop over an outer binding keeps today's tabl
|
||||
loc,
|
||||
},
|
||||
observe(ref("o", o)),
|
||||
]);
|
||||
];
|
||||
const fn = f.run(body);
|
||||
expect(f.order(o)).toEqual(["id", "x", "y"]);
|
||||
const loop = fn.body.find((st) => st.kind === "while") as IrStmt & { kind: "while" };
|
||||
const guarded = loop.body[0]!;
|
||||
expect(guarded.kind).toBe("block");
|
||||
const [move, write] = (guarded as IrStmt & { kind: "block" }).body;
|
||||
expect(move).toMatchObject({ kind: "if", cond: { kind: "unary", op: "!" } });
|
||||
expect(write).toMatchObject({ kind: "recordSet", field: "x" });
|
||||
});
|
||||
|
||||
test("re-adding a key a literal creates earlier keeps today's table", () => {
|
||||
test("re-adding a key a literal creates earlier moves it to the end of that value", () => {
|
||||
const f = fixture();
|
||||
const o = f.record(
|
||||
[
|
||||
@@ -399,4 +493,119 @@ test("re-adding a key a literal creates earlier keeps today's table", () => {
|
||||
};
|
||||
f.run([decl("o", f.literal(o, ["a", "b"])), del, set(ref("o", o), "a"), observe(ref("o", o))]);
|
||||
expect(f.order(o)).toEqual(["a", "b"]);
|
||||
expect(f.shapes.get(o.shapeId)!.orderVariants).toEqual([
|
||||
["a", "b"],
|
||||
["b", "a"],
|
||||
]);
|
||||
});
|
||||
|
||||
test("a moving write evaluates its value before it tests and moves the key", () => {
|
||||
const f = fixture();
|
||||
const o = f.record(
|
||||
[
|
||||
["id", F64],
|
||||
["x", OPT],
|
||||
["y", OPT],
|
||||
],
|
||||
["id", "x", "y"],
|
||||
);
|
||||
// `o.x = ((o.y = 2), 3)`: y is created first.
|
||||
const value: IrExpr = {
|
||||
kind: "seqExpr",
|
||||
stmts: [set(ref("o", o), "y")],
|
||||
result: num(3),
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const write: IrStmt = {
|
||||
kind: "recordSet",
|
||||
obj: ref("o", o),
|
||||
shapeId: o.shapeId,
|
||||
field: "x",
|
||||
value,
|
||||
loc,
|
||||
};
|
||||
const fn = f.run([
|
||||
decl("o", f.literal(o, ["id"])),
|
||||
observe(f.literal(o, ["x", "id"])),
|
||||
write,
|
||||
observe(ref("o", o)),
|
||||
]);
|
||||
const shape = f.shapes.get(o.shapeId)!;
|
||||
expect(shape.orderVariants).toBeDefined();
|
||||
const block = fn.body[2] as IrStmt & { kind: "block" };
|
||||
expect(block.kind).toBe("block");
|
||||
// The receiver evaluates first, since the value may assign its binding.
|
||||
const [receiver, first, guard, written] = block.body;
|
||||
expect(receiver).toMatchObject({ kind: "varDecl", init: { kind: "varRef", localId: "o" } });
|
||||
const target = (receiver as IrStmt & { kind: "varDecl" }).localId;
|
||||
expect(first).toMatchObject({ kind: "varDecl", init: { kind: "seqExpr" } });
|
||||
const local = (first as IrStmt & { kind: "varDecl" }).localId;
|
||||
expect(guard).toMatchObject({ kind: "if", cond: { kind: "unary", op: "!" } });
|
||||
expect(written).toMatchObject({
|
||||
kind: "recordSet",
|
||||
obj: { kind: "varRef", localId: target },
|
||||
field: "x",
|
||||
value: { kind: "varRef", localId: local },
|
||||
});
|
||||
// The write inside the value moves y in its own block.
|
||||
const init = (first as IrStmt & { kind: "varDecl" }).init as IrExpr & { kind: "seqExpr" };
|
||||
expect(init.stmts[0]).toMatchObject({ kind: "block" });
|
||||
});
|
||||
|
||||
test("a key after a spread that the source may lack picks its order from the source's keys", () => {
|
||||
const f = fixture();
|
||||
const s = f.record(
|
||||
[
|
||||
["a", F64],
|
||||
["b", OPT],
|
||||
["c", OPT],
|
||||
],
|
||||
["a", "b", "c"],
|
||||
);
|
||||
const t = f.record(
|
||||
[
|
||||
["a", F64],
|
||||
["b", F64],
|
||||
["c", OPT],
|
||||
],
|
||||
["a", "b", "c"],
|
||||
);
|
||||
const read = (field: string): IrExpr => ({
|
||||
kind: "recordGet",
|
||||
obj: ref("x", s),
|
||||
shapeId: s.shapeId,
|
||||
field,
|
||||
type: OPT,
|
||||
loc,
|
||||
});
|
||||
// `{ ...x, b: 1 }`
|
||||
const merged = f.notes.literal(
|
||||
{
|
||||
kind: "recordLit",
|
||||
fields: [
|
||||
{ name: "a", value: read("a") },
|
||||
{ name: "c", value: read("c") },
|
||||
{ name: "b", value: num(1) },
|
||||
],
|
||||
type: t,
|
||||
loc,
|
||||
},
|
||||
[
|
||||
{ kind: "spread", shapeId: s.shapeId, maybeAbsent: false },
|
||||
{ kind: "key", name: "b", definite: true },
|
||||
],
|
||||
);
|
||||
f.run([decl("x", f.literal(s, ["c", "a"])), observe(f.literal(s, ["a", "c"])), observe(merged)]);
|
||||
// b keeps its place in the source's order when the source has it, and
|
||||
// follows the source's keys when it does not.
|
||||
expect(f.shapes.get(t.shapeId)!.orderVariants).toEqual([
|
||||
["a", "c", "b"],
|
||||
["c", "a", "b"],
|
||||
["a", "b", "c"],
|
||||
]);
|
||||
expect(orderIndexOf(merged)).toMatchObject({
|
||||
kind: "ternary",
|
||||
cond: { kind: "recordHas", obj: { kind: "varRef", localId: "x" }, field: "b" },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,12 +41,57 @@
|
||||
* value: that shape, and every shape copied from it, keeps today's table
|
||||
* (and today's SC6002 warnings). Shapes that no order surface observes
|
||||
* keep today's table too. */
|
||||
import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType } from "../../ir/ir.js";
|
||||
import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild, everyTypeChild } from "../../ir/traverse.js";
|
||||
import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType, SrcLoc } from "../../ir/ir.js";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
KEY_ORDER_FIELD,
|
||||
shapeHasAccessorSlots,
|
||||
typeEquals,
|
||||
UNDEFINED_T,
|
||||
} from "../../ir/ir.js";
|
||||
import {
|
||||
everyExprChild,
|
||||
everyStmtChild,
|
||||
everyTypeChild,
|
||||
transformStmtList,
|
||||
} from "../../ir/traverse.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { ALWAYS_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
|
||||
|
||||
/** The statements a key-order helper runs for record `ref`: `walks[0]`,
|
||||
* its walk over the shape's table, or, for a shape with orderVariants, a
|
||||
* branch per order (walks[i] for order i) on the value's KEY_ORDER_FIELD.
|
||||
* `walks` follows shapeKeyOrders. */
|
||||
export function perKeyOrder(
|
||||
shape: IrRecordShape,
|
||||
ref: IrExpr,
|
||||
walks: readonly IrStmt[][],
|
||||
loc: SrcLoc,
|
||||
): IrStmt[] {
|
||||
let tail = walks[0]!;
|
||||
for (let i = walks.length - 1; i >= 1; i--) {
|
||||
const index: IrExpr = {
|
||||
kind: "recordGet",
|
||||
obj: ref,
|
||||
shapeId: shape.id,
|
||||
field: KEY_ORDER_FIELD,
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const cond: IrExpr = {
|
||||
kind: "bin",
|
||||
op: "===",
|
||||
left: index,
|
||||
right: { kind: "numLit", value: i, type: F64, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
tail = [{ kind: "if", cond, then: walks[i]!, else_: tail, loc }];
|
||||
}
|
||||
return tail;
|
||||
}
|
||||
|
||||
/** One property of an object literal, in source order. */
|
||||
export type KeyOrderSegment =
|
||||
/** An explicit key (`definite`), or a conditional spread's key, which
|
||||
@@ -713,4 +758,496 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...resolved.order];
|
||||
notes.changed.add(id);
|
||||
}
|
||||
|
||||
// ── per-instance orders ─────────────────────────────────────────────
|
||||
// A shape whose literals write its keys in more than one order has no
|
||||
// one table. Each value then records which of the orders it was written
|
||||
// in (KEY_ORDER_FIELD), when every construction's order is known
|
||||
// statically or copied from a value that records its own:
|
||||
// - a literal of explicit keys writes its order;
|
||||
// - a copy from a shape with one resolved order writes that order;
|
||||
// - a copy from a value of a shape with orders writes the source's
|
||||
// order, filtered to the copy's keys (rest bindings, width copies);
|
||||
// - clones copy the field, and every other construction (synthesized
|
||||
// literals, checked conversions, runtime producers) leaves 0, today's
|
||||
// table, as before.
|
||||
// A write of an optional key a value lacks moves the key to the end of
|
||||
// the value's order, as in Node: the orders include every order such
|
||||
// writes reach, and each write site updates the index after it evaluates
|
||||
// the value and before it writes the key.
|
||||
const MAX_ORDERS = 16;
|
||||
const unresolved = (id: string): boolean => result.get(id) === "unknown";
|
||||
/** `order` over every key of `table`: keys the order lacks follow it. */
|
||||
const complete = (order: readonly string[], table: readonly string[]): string[] => {
|
||||
const keys = new Set(table);
|
||||
const out = order.filter((k) => keys.has(k));
|
||||
const seen = new Set(out);
|
||||
return [...out, ...table.filter((k) => !seen.has(k))];
|
||||
};
|
||||
const literalOrder = (segments: readonly KeyOrderSegment[]): string[] | null => {
|
||||
const out: string[] = [];
|
||||
for (const segment of segments) {
|
||||
if (segment.kind !== "key" || !segment.definite) return null;
|
||||
if (!isArrayIndex(segment.name)) out.push(segment.name);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
/** `{ ...value }` of a value of the literal's own shape: a copy of the
|
||||
* value's order. */
|
||||
const selfSpread = (segments: readonly KeyOrderSegment[], id: string): boolean =>
|
||||
segments.length === 1 &&
|
||||
segments[0]!.kind === "spread" &&
|
||||
segments[0]!.shapeId === id &&
|
||||
!segments[0]!.maybeAbsent;
|
||||
const sameOrder = (a: readonly string[], b: readonly string[]): boolean =>
|
||||
a.join("\0") === b.join("\0");
|
||||
const indexOfOrder = (orders: readonly string[][], order: readonly string[]): number =>
|
||||
orders.findIndex((o) => sameOrder(o, order));
|
||||
/** The keys writes can add to a value of the shape, or null when a
|
||||
* clone override can (the clone's order is its source's). */
|
||||
const movingKeys = (id: string, f: ShapeFacts, table: readonly string[]): string[] | null => {
|
||||
const keys = new Set(table);
|
||||
const moving = new Set<string>();
|
||||
for (const w of f.writes) {
|
||||
if (w.kind === "delete" || !keys.has(w.field) || !maybeAbsent(id, w.field)) continue;
|
||||
if (w.kind === "clone") return null;
|
||||
moving.add(w.field);
|
||||
}
|
||||
return [...moving];
|
||||
};
|
||||
/** `values[i]` for the order index `current` holds, as a chain of
|
||||
* tests. */
|
||||
const indexChain = (current: IrExpr, values: readonly number[], loc: SrcLoc): IrExpr => {
|
||||
let out: IrExpr = { kind: "numLit", value: values[values.length - 1]!, type: F64, loc };
|
||||
for (let i = values.length - 2; i >= 0; i--) {
|
||||
if (out.kind === "numLit" && out.value === values[i]) continue;
|
||||
out = {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "===",
|
||||
left: current,
|
||||
right: { kind: "numLit", value: i, type: F64, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: { kind: "numLit", value: values[i]!, type: F64, loc },
|
||||
else_: out,
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const moves = new Map<string, Map<string, number[]>>();
|
||||
/** Adds to `orders` every order writes of `moving` keys reach from them
|
||||
* and records each key's transitions; false past MAX_ORDERS. */
|
||||
const closeUnderMoves = (id: string, orders: string[][], moving: readonly string[]): boolean => {
|
||||
const transitions = new Map<string, number[]>(moving.map((k) => [k, []]));
|
||||
for (let i = 0; i < orders.length; i++) {
|
||||
for (const key of moving) {
|
||||
const moved = [...orders[i]!.filter((k) => k !== key), key];
|
||||
let index = indexOfOrder(orders, moved);
|
||||
if (index < 0) {
|
||||
if (orders.length === MAX_ORDERS) return false;
|
||||
index = orders.push(moved) - 1;
|
||||
}
|
||||
transitions.get(key)![i] = index;
|
||||
}
|
||||
}
|
||||
if (moving.length > 0) moves.set(id, transitions);
|
||||
return true;
|
||||
};
|
||||
// Observed shapes, and the shapes their values copy their orders from.
|
||||
const wanted = new Set(observed);
|
||||
const pending = [...observed];
|
||||
while (pending.length > 0) {
|
||||
const f = facts.get(pending.pop()!);
|
||||
if (f === undefined) continue;
|
||||
const sources = [
|
||||
...f.copies.map((c) => c.source),
|
||||
...f.literals.flatMap((segments) =>
|
||||
segments.flatMap((s) => (s.kind === "spread" ? [s.shapeId] : [])),
|
||||
),
|
||||
];
|
||||
for (const source of sources) {
|
||||
if (wanted.has(source)) continue;
|
||||
wanted.add(source);
|
||||
pending.push(source);
|
||||
}
|
||||
}
|
||||
const orderable = (
|
||||
id: string,
|
||||
f: ShapeFacts,
|
||||
): { shape: IrRecordShape; moving: string[] } | null => {
|
||||
const shape = shapes.get(id);
|
||||
if (!wanted.has(id) || !unresolved(id) || !inScope(shape)) return null;
|
||||
const moving = movingKeys(id, f, tableOf(shape));
|
||||
return moving === null ? null : { shape, moving };
|
||||
};
|
||||
const variants = new Map<string, string[][]>();
|
||||
for (const [id, f] of facts) {
|
||||
const candidate = orderable(id, f);
|
||||
if (candidate === null || f.literals.length === 0) continue;
|
||||
const table = tableOf(candidate.shape);
|
||||
const orders: string[][] = [table];
|
||||
let ok = true;
|
||||
const add = (order: readonly string[]): void => {
|
||||
const full = complete(order, table);
|
||||
if (indexOfOrder(orders, full) < 0) orders.push(full);
|
||||
};
|
||||
for (const segments of f.literals) {
|
||||
if (selfSpread(segments, id)) continue;
|
||||
const order = literalOrder(segments);
|
||||
if (order === null) ok = false;
|
||||
else add(order);
|
||||
}
|
||||
for (const c of f.copies) {
|
||||
if (c.source === id) continue;
|
||||
const source = result.get(c.source);
|
||||
if (source === undefined || source === "unknown") ok = false;
|
||||
else add(source.order.filter((k) => c.keys.has(k)));
|
||||
}
|
||||
if (!ok || orders.length > MAX_ORDERS || !closeUnderMoves(id, orders, candidate.moving))
|
||||
continue;
|
||||
if (orders.length > 1) variants.set(id, orders);
|
||||
else moves.delete(id);
|
||||
}
|
||||
// Shapes built only from values of one shape with orders, all in one
|
||||
// way: copies (rest bindings, width copies), or literals spreading the
|
||||
// value between the same explicit keys. Order i is the source's order i
|
||||
// over the shape's keys, after the keys before the spread and before
|
||||
// the keys after it. A key after the spread that the source value may
|
||||
// lack goes where the source has it, or after the source's keys when it
|
||||
// has not: such a literal's orders are one per source order and set of
|
||||
// those keys the source has, and `select.index[present][i]` is the order
|
||||
// for source order i with the keys of bit set `present`.
|
||||
type Template = {
|
||||
source: string;
|
||||
prefix: string[];
|
||||
suffix: string[];
|
||||
select?: { keys: string[]; index: number[][] };
|
||||
};
|
||||
const templateOf = (segments: readonly KeyOrderSegment[], id: string): Template | null => {
|
||||
const prefix: string[] = [];
|
||||
const suffix: string[] = [];
|
||||
let source: string | null = null;
|
||||
for (const segment of segments) {
|
||||
if (segment.kind === "opaque") return null;
|
||||
if (segment.kind === "spread") {
|
||||
if (source !== null || segment.maybeAbsent || segment.shapeId === id) return null;
|
||||
source = segment.shapeId;
|
||||
continue;
|
||||
}
|
||||
if (!segment.definite) return null;
|
||||
(source === null ? prefix : suffix).push(segment.name);
|
||||
}
|
||||
return source === null ? null : { source, prefix, suffix };
|
||||
};
|
||||
const templateKey = (t: Template): string => JSON.stringify([t.source, t.prefix, t.suffix]);
|
||||
const copyTemplate = (source: string): Template => ({ source, prefix: [], suffix: [] });
|
||||
const mirrors = new Map<string, Template>();
|
||||
for (const [id, f] of facts) {
|
||||
if (variants.has(id) || f.pinned || f.runtime || f.opaque) continue;
|
||||
const candidate = orderable(id, f);
|
||||
if (candidate === null || f.literals.length + f.copies.length === 0) continue;
|
||||
const templates: (Template | null)[] = f.literals.map((segments) => templateOf(segments, id));
|
||||
for (const c of f.copies) templates.push(copyTemplate(c.source));
|
||||
const first = templates[0];
|
||||
if (
|
||||
first === null ||
|
||||
first === undefined ||
|
||||
templates.some((t) => t === null || templateKey(t) !== templateKey(first))
|
||||
)
|
||||
continue;
|
||||
const orders = variants.get(first.source);
|
||||
if (orders === undefined || mirrors.has(first.source)) continue;
|
||||
const table = tableOf(candidate.shape);
|
||||
const tested = first.suffix.filter((k) => maybeAbsent(first.source, k));
|
||||
if (tested.length > 4) continue;
|
||||
const mirrorOf = (o: readonly string[], lacks: ReadonlySet<string>): string[] => {
|
||||
const order = [...first.prefix];
|
||||
for (const k of o) if (!lacks.has(k) && !order.includes(k)) order.push(k);
|
||||
for (const k of first.suffix) if (!order.includes(k)) order.push(k);
|
||||
return complete(order, table);
|
||||
};
|
||||
let mirrored: string[][];
|
||||
let template: Template = first;
|
||||
if (tested.length === 0) mirrored = orders.map((o) => mirrorOf(o, new Set()));
|
||||
else {
|
||||
mirrored = [];
|
||||
const index: number[][] = [];
|
||||
for (let present = 0; present < 1 << tested.length; present++) {
|
||||
const lacks = new Set(tested.filter((_, j) => (present & (1 << j)) === 0));
|
||||
const row: number[] = [];
|
||||
for (const o of orders) {
|
||||
const order = mirrorOf(o, lacks);
|
||||
let i = indexOfOrder(mirrored, order);
|
||||
if (i < 0) i = mirrored.push(order) - 1;
|
||||
row.push(i);
|
||||
}
|
||||
index.push(row);
|
||||
}
|
||||
if (mirrored.length > MAX_ORDERS) continue;
|
||||
template = {
|
||||
source: first.source,
|
||||
prefix: first.prefix,
|
||||
suffix: first.suffix,
|
||||
select: { keys: tested, index },
|
||||
};
|
||||
}
|
||||
if (!closeUnderMoves(id, mirrored, candidate.moving)) continue;
|
||||
if (mirrored.every((o) => sameOrder(o, mirrored[0]!))) {
|
||||
// Every order of the source agrees over these keys: one table.
|
||||
moves.delete(id);
|
||||
const shape = candidate.shape;
|
||||
const only: string[] = mirrored[0] ?? table;
|
||||
if (!sameOrder(only, table))
|
||||
shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...only];
|
||||
continue;
|
||||
}
|
||||
variants.set(id, mirrored);
|
||||
mirrors.set(id, template);
|
||||
}
|
||||
if (variants.size === 0) return;
|
||||
|
||||
// Every construction of those shapes, to give each its order index. A
|
||||
// copy reads its source's index, so its source must be a plain binding.
|
||||
const sites = new Map<string, (IrExpr & { kind: "recordLit" })[]>();
|
||||
/** The statements a spread literal's sequence evaluates first: the
|
||||
* spread value's binding, then its fields. */
|
||||
const prologues = new Map<IrExpr, readonly IrStmt[]>();
|
||||
const siteOf = (e: IrExpr): boolean => {
|
||||
if (e.kind === "seqExpr" && e.result.kind === "recordLit") prologues.set(e.result, e.stmts);
|
||||
if (e.kind === "recordLit" && e.type.kind === "record" && variants.has(e.type.shapeId)) {
|
||||
const list = sites.get(e.type.shapeId) ?? [];
|
||||
list.push(e);
|
||||
sites.set(e.type.shapeId, list);
|
||||
}
|
||||
return everyExprChild(e, siteOf, siteStmt);
|
||||
};
|
||||
const siteStmt = (st: IrStmt): boolean => everyStmtChild(st, siteOf, siteStmt);
|
||||
for (const fn of fns) for (const st of fn.body) siteStmt(st);
|
||||
/** The binding a copy or spread reads its fields from. */
|
||||
const copySource = (lit: IrExpr & { kind: "recordLit" }, source: string): IrExpr | null => {
|
||||
for (const field of lit.fields) {
|
||||
const value = field.value;
|
||||
if (value.kind === "recordGet" && value.shapeId === source && value.obj.kind === "varRef")
|
||||
return value.obj;
|
||||
}
|
||||
for (const st of prologues.get(lit) ?? []) {
|
||||
if (st.kind !== "varDecl" || st.init === null) continue;
|
||||
const t = st.init.type;
|
||||
if (t.kind === "record" && t.shapeId === source)
|
||||
return { kind: "varRef", localId: st.localId, type: t, loc: lit.loc };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const orderIndex = (id: string, lit: IrExpr & { kind: "recordLit" }): IrExpr | number | null => {
|
||||
const orders = variants.get(id)!;
|
||||
const segments = notes.literalOf(lit);
|
||||
if (segments !== undefined && selfSpread(segments, id)) {
|
||||
const obj = copySource(lit, id);
|
||||
if (obj === null) return null;
|
||||
return {
|
||||
kind: "recordGet",
|
||||
obj,
|
||||
shapeId: id,
|
||||
field: KEY_ORDER_FIELD,
|
||||
type: F64,
|
||||
loc: lit.loc,
|
||||
};
|
||||
}
|
||||
const mirror = mirrors.get(id);
|
||||
if (segments !== undefined && mirror !== undefined) {
|
||||
const obj = copySource(lit, mirror.source);
|
||||
if (obj === null) return null;
|
||||
const sourceIndex: IrExpr = {
|
||||
kind: "recordGet",
|
||||
obj,
|
||||
shapeId: mirror.source,
|
||||
field: KEY_ORDER_FIELD,
|
||||
type: F64,
|
||||
loc: lit.loc,
|
||||
};
|
||||
const select = mirror.select;
|
||||
if (select === undefined) return sourceIndex;
|
||||
// Tests the source for each key in turn, then picks by its index.
|
||||
const pick = (j: number, present: number): IrExpr => {
|
||||
if (j === select.keys.length)
|
||||
return indexChain(sourceIndex, select.index[present]!, lit.loc);
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: lowerer.recordFieldPresent(obj, mirror.source, select.keys[j]!, lit.loc),
|
||||
then: pick(j + 1, present | (1 << j)),
|
||||
else_: pick(j + 1, present),
|
||||
type: F64,
|
||||
loc: lit.loc,
|
||||
};
|
||||
};
|
||||
return pick(0, 0);
|
||||
}
|
||||
if (segments !== undefined) {
|
||||
const order = literalOrder(segments);
|
||||
return order === null ? null : indexOfOrder(orders, complete(order, orders[0]!));
|
||||
}
|
||||
const source = notes.copyOf(lit);
|
||||
if (source === undefined) return 0;
|
||||
if (variants.has(source) && (source === id || mirrors.get(id)?.source === source)) {
|
||||
const obj = copySource(lit, source);
|
||||
if (obj === null) return null;
|
||||
return {
|
||||
kind: "recordGet",
|
||||
obj,
|
||||
shapeId: source,
|
||||
field: KEY_ORDER_FIELD,
|
||||
type: F64,
|
||||
loc: lit.loc,
|
||||
};
|
||||
}
|
||||
const resolved = result.get(source);
|
||||
if (resolved === undefined || resolved === "unknown") return null;
|
||||
const keys = new Set(
|
||||
lit.fields.filter((x) => x.value.kind !== "fieldAbsent").map((x) => x.name),
|
||||
);
|
||||
return indexOfOrder(
|
||||
orders,
|
||||
complete(
|
||||
resolved.order.filter((k) => keys.has(k)),
|
||||
orders[0]!,
|
||||
),
|
||||
);
|
||||
};
|
||||
const plan = new Map<IrExpr & { kind: "recordLit" }, IrExpr | number>();
|
||||
const dropped = new Set<string>();
|
||||
for (const [id, list] of sites) {
|
||||
for (const lit of list) {
|
||||
const index = orderIndex(id, lit);
|
||||
if (index === null || (typeof index === "number" && index < 0)) dropped.add(id);
|
||||
else plan.set(lit, index);
|
||||
}
|
||||
}
|
||||
// Write sites of keys that move: each reads and updates the value's
|
||||
// index before the write.
|
||||
const writeSites: { st: IrStmt & { kind: "recordSet" }; fn: IrFunction }[] = [];
|
||||
let writeFn: IrFunction | null = null;
|
||||
const writeExpr = (e: IrExpr): boolean => everyExprChild(e, writeExpr, writeOf);
|
||||
const writeOf = (st: IrStmt): boolean => {
|
||||
if (
|
||||
st.kind === "recordSet" &&
|
||||
st.value.kind !== "fieldAbsent" &&
|
||||
moves.get(st.shapeId)?.has(st.field)
|
||||
)
|
||||
writeSites.push({ st, fn: writeFn! });
|
||||
return everyStmtChild(st, writeExpr, writeOf);
|
||||
};
|
||||
for (const fn of fns) {
|
||||
writeFn = fn;
|
||||
for (const st of fn.body) writeOf(st);
|
||||
}
|
||||
// A shape whose copies cannot read their source keeps today's table, and
|
||||
// so do the shapes that mirror it.
|
||||
for (const [id, mirror] of mirrors) if (dropped.has(mirror.source)) dropped.add(id);
|
||||
for (const [lit, index] of plan) {
|
||||
const id = (lit.type as IrType & { kind: "record" }).shapeId;
|
||||
if (dropped.has(id)) continue;
|
||||
const value: IrExpr =
|
||||
typeof index === "number" ? { kind: "numLit", value: index, type: F64, loc: lit.loc } : index;
|
||||
lit.fields.push({ name: KEY_ORDER_FIELD, value });
|
||||
}
|
||||
let temporaries = 0;
|
||||
const replacements = new Map<IrFunction, Map<IrStmt, IrStmt>>();
|
||||
for (const { st, fn } of writeSites) {
|
||||
if (dropped.has(st.shapeId)) continue;
|
||||
const transitions = moves.get(st.shapeId)!.get(st.field)!;
|
||||
const loc = st.loc;
|
||||
// A receiver other than a binding evaluates once, into a local. So does
|
||||
// a binding when the value evaluates first (below): the value may
|
||||
// assign the binding, and the write still goes to the object the
|
||||
// binding held before.
|
||||
const pure =
|
||||
st.value.kind === "numLit" ||
|
||||
st.value.kind === "strLit" ||
|
||||
st.value.kind === "boolLit" ||
|
||||
st.value.kind === "varRef";
|
||||
let plain = st.obj;
|
||||
while (plain.kind === "unionNarrow" || plain.kind === "downcast") plain = plain.value;
|
||||
const prologue: IrStmt[] = [];
|
||||
let obj = st.obj;
|
||||
if (plain.kind !== "varRef" || !pure) {
|
||||
const localId = `%keyOrderReceiver.${temporaries++}`;
|
||||
fn.locals.push({ id: localId, name: "%keyOrderReceiver", type: st.obj.type, mutable: false });
|
||||
prologue.push({ kind: "varDecl", localId, init: st.obj, loc });
|
||||
obj = { kind: "varRef", localId, type: st.obj.type, loc };
|
||||
}
|
||||
const current: IrExpr = {
|
||||
kind: "recordGet",
|
||||
obj,
|
||||
shapeId: st.shapeId,
|
||||
field: KEY_ORDER_FIELD,
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const next = indexChain(current, transitions, loc);
|
||||
// The value evaluates before the key is tested and moved: it may add
|
||||
// other keys to the same value, which then come first.
|
||||
let value = st.value;
|
||||
if (!pure) {
|
||||
const localId = `%keyOrderValue.${temporaries++}`;
|
||||
fn.locals.push({ id: localId, name: "%keyOrderValue", type: value.type, mutable: false });
|
||||
prologue.push({ kind: "varDecl", localId, init: value, loc });
|
||||
value = { kind: "varRef", localId, type: value.type, loc };
|
||||
}
|
||||
const absent: IrExpr = {
|
||||
kind: "unary",
|
||||
op: "!",
|
||||
operand: lowerer.recordFieldPresent(obj, st.shapeId, st.field, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
const update: IrStmt = {
|
||||
kind: "recordSet",
|
||||
obj,
|
||||
shapeId: st.shapeId,
|
||||
field: KEY_ORDER_FIELD,
|
||||
value: next,
|
||||
loc,
|
||||
};
|
||||
// The write becomes a block that moves the key first.
|
||||
const write: IrStmt = {
|
||||
kind: "recordSet",
|
||||
obj,
|
||||
shapeId: st.shapeId,
|
||||
field: st.field,
|
||||
value,
|
||||
loc,
|
||||
};
|
||||
const guard: IrStmt = { kind: "if", cond: absent, then: [update], else_: null, loc };
|
||||
const block: IrStmt = { kind: "block", body: [...prologue, guard, write], loc };
|
||||
let replaced = replacements.get(fn);
|
||||
if (replaced === undefined) {
|
||||
replaced = new Map();
|
||||
replacements.set(fn, replaced);
|
||||
}
|
||||
replaced.set(st, block);
|
||||
}
|
||||
for (const [fn, replaced] of replacements) {
|
||||
fn.body = transformStmtList(fn.body, {
|
||||
expr: (e) => e,
|
||||
stmt: (st) => replaced.get(st) ?? st,
|
||||
});
|
||||
}
|
||||
for (const [id, orders] of variants) {
|
||||
if (dropped.has(id)) continue;
|
||||
const shape = shapes.get(id)!;
|
||||
const indexKeys: string[] = (shape.declaredOrder ?? []).filter(isArrayIndex);
|
||||
shapes.addKeyOrderField(id);
|
||||
const tables: string[][] = [];
|
||||
for (const order of orders) {
|
||||
const table: string[] = [...indexKeys];
|
||||
for (const key of order) table.push(key);
|
||||
tables.push(table);
|
||||
}
|
||||
shape.orderVariants = tables;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,13 +359,15 @@ function boolRef(localId: string, loc: ReturnType<typeof locOf>): IrExpr {
|
||||
|
||||
/** The loop's stream: a JavaScript value typed as a stream can arrive as a
|
||||
* checked-dynamic value (returned through an untyped function), which
|
||||
* checks back into the stream class. */
|
||||
* checks back into the stream class, or as a union with it (an element
|
||||
* read), which narrows to it. */
|
||||
function streamSource(
|
||||
lowerer: Lowerer,
|
||||
plan: NodeReadablePlan,
|
||||
loc: ReturnType<typeof locOf>,
|
||||
): IrExpr {
|
||||
const v = lowerer.lowerExpr(plan.source);
|
||||
if (v.type.kind === "union") return lowerer.coerceToExpected(v, plan.type);
|
||||
if (v.type.kind !== "dyn") return v;
|
||||
return { kind: "dynCheck", value: v, type: plan.type, loc };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { perKeyOrder } from "./key-order-witness.js";
|
||||
import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js";
|
||||
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js";
|
||||
import { lowerDynStreamMemberCall } from "./dyn-streams.js";
|
||||
@@ -73,6 +74,7 @@ import {
|
||||
isDynTypedRefType,
|
||||
isUnitType,
|
||||
shapeHasAccessorSlots,
|
||||
shapeKeyOrders,
|
||||
typeEquals,
|
||||
RUNTIME_EMITTER_CLASS,
|
||||
} from "../../ir/ir.js";
|
||||
@@ -4404,6 +4406,21 @@ export function lowerDynDispatchMethodCall(
|
||||
lowerer.chainBlocked(call, access)
|
||||
)
|
||||
return null;
|
||||
// readable.compose lowers its stages from their syntax, so it needs the
|
||||
// stream's class: a stream a JavaScript function returns untyped has none.
|
||||
if (method === "compose") {
|
||||
const recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
if (
|
||||
recvT?.kind === "object" &&
|
||||
streamSidesOf(lowerer, lowerer.classes.get(recvT.className)) !== null
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
"readable.compose on a stream held as a checked-dynamic value",
|
||||
call,
|
||||
"type the stream where it comes from, such as a JSDoc @returns {Readable} on the function that returns it",
|
||||
);
|
||||
}
|
||||
}
|
||||
if (call.arguments.nodes.some((arg) => ts.isSpreadElement(arg))) {
|
||||
const loc = locOf(call);
|
||||
const local = lowerer.declareHiddenLocal("%spreadReceiver", DYN);
|
||||
@@ -7090,8 +7107,12 @@ export function recordKeysArrayCall(
|
||||
loc,
|
||||
},
|
||||
];
|
||||
const order = current.declaredOrder ?? current.fields.map((f) => f.name);
|
||||
for (const name of order) {
|
||||
// One walk per key order (see perKeyOrder).
|
||||
const orders = shapeKeyOrders(current);
|
||||
const walks = orders.map((): IrStmt[] => []);
|
||||
for (const [k, name] of orders.flatMap((order, k) =>
|
||||
order.map((name) => [k, name] as const),
|
||||
)) {
|
||||
const f = current.fields.find((x) => x.name === name)!;
|
||||
const pushStmt: IrStmt = {
|
||||
kind: "exprStmt",
|
||||
@@ -7108,12 +7129,13 @@ export function recordKeysArrayCall(
|
||||
// Fields that can be absent push behind their presence test (the
|
||||
// key exists exactly when Object.keys would list it).
|
||||
const present = lowerer.recordFieldPresent(ref, argIr.shapeId, f.name, loc);
|
||||
body.push(
|
||||
walks[k]!.push(
|
||||
present.kind === "recordHas"
|
||||
? { kind: "if", cond: present, then: [pushStmt], else_: null, loc }
|
||||
: pushStmt,
|
||||
);
|
||||
}
|
||||
body.push(...perKeyOrder(current, ref, walks, loc));
|
||||
body.push({ kind: "return", value: outRef, loc });
|
||||
fn.body = body;
|
||||
};
|
||||
@@ -8799,8 +8821,12 @@ function lowerObjectStaticCall(
|
||||
loc,
|
||||
},
|
||||
];
|
||||
const order = current.declaredOrder ?? current.fields.map((f) => f.name);
|
||||
for (const name of order) {
|
||||
// One walk per key order (see perKeyOrder).
|
||||
const orders = shapeKeyOrders(current);
|
||||
const walks = orders.map((): IrStmt[] => []);
|
||||
for (const [k, name] of orders.flatMap((order, k) =>
|
||||
order.map((name) => [k, name] as const),
|
||||
)) {
|
||||
const f = current.fields.find((x) => x.name === name)!;
|
||||
const raw: IrExpr = {
|
||||
kind: "recordGet",
|
||||
@@ -8962,8 +8988,9 @@ function lowerObjectStaticCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
body.push(stmt);
|
||||
walks[k]!.push(stmt);
|
||||
}
|
||||
body.push(...perKeyOrder(current, ref, walks, loc));
|
||||
body.push({ kind: "return", value: outRef, loc });
|
||||
fn.body = body;
|
||||
};
|
||||
@@ -9913,7 +9940,9 @@ export function lowerObjectMethodCall(
|
||||
if (
|
||||
info &&
|
||||
!found &&
|
||||
(STREAM_API_MEMBERS.has(access.name.text) || STREAM_OPERATORS.has(access.name.text)) &&
|
||||
(STREAM_API_MEMBERS.has(access.name.text) ||
|
||||
STREAM_OPERATORS.has(access.name.text) ||
|
||||
access.name.text === "compose") &&
|
||||
streamSidesOf(lowerer, info) !== null
|
||||
) {
|
||||
const stream = lowerStreamMethodCall(lowerer, call, access, info);
|
||||
|
||||
@@ -133,6 +133,7 @@ import {
|
||||
STREAM_PROP_MEMBERS,
|
||||
STREAM_COMPLETION_CALLBACK_INDEX,
|
||||
UNDERSCORE_METHODS,
|
||||
streamExtraCallbacks,
|
||||
lowerStreamNew,
|
||||
lowerStreamSuperCall,
|
||||
streamCtorShape,
|
||||
@@ -184,6 +185,7 @@ import { constructionEscape } from "./construction-escapes.js";
|
||||
import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js";
|
||||
import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js";
|
||||
import { reflectedClassStaticMethodValue } from "./class-method-values.js";
|
||||
import { identifierReadSymbol } from "./surfaces.js";
|
||||
|
||||
export function returnsOnlyThis(member: ts.MethodDeclaration): boolean {
|
||||
const body = member.body;
|
||||
@@ -2746,11 +2748,11 @@ export function collectClassShapeInner(
|
||||
`overriding the stream member '${name}' (the runtime owns the stream surface)`,
|
||||
);
|
||||
}
|
||||
if (name === "_writev" || name === "_construct") {
|
||||
if (name === "_writev" && !streamExtraCallbacks(streamBase.def.name).includes("writev")) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
memberName,
|
||||
`declaring '${name}' on a stream subclass (${name === "_writev" ? "batched writes are" : "deferred construction is"} not lowered — writes deliver one chunk at a time)`,
|
||||
`declaring '_writev' on a ${streamBase.def.name.slice(1)} subclass (batched writes are supported on Writable and Duplex subclasses)`,
|
||||
);
|
||||
}
|
||||
const accepted = streamCtorShape(streamBase.def.name).accepted;
|
||||
@@ -6034,6 +6036,28 @@ export function lowerClassExpressionInfo(
|
||||
return info;
|
||||
}
|
||||
|
||||
/** True when a named class expression's body refers to the class value by
|
||||
* its own name (`class Box { static of() { return new Box(); } }`, a
|
||||
* shorthand property `{ Box }` included); type annotations naming it
|
||||
* (`again(): Box`) do not count. */
|
||||
function classExpressionNamesItself(lowerer: Lowerer, expr: ts.ClassExpression): boolean {
|
||||
const name = expr.name;
|
||||
if (!name) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(name);
|
||||
if (!symbol) return false;
|
||||
let found = false;
|
||||
for (const member of expr.members.nodes) {
|
||||
ts.walkPreorder(member, (n) => {
|
||||
if (found || ts.isTypeNode(n)) return "skip";
|
||||
if (ts.isIdentifier(n) && n.text === name.text && identifierReadSymbol(lowerer, n) === symbol)
|
||||
found = true;
|
||||
return undefined;
|
||||
});
|
||||
if (found) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** `class {…}` in expression position: a class definition bound to no
|
||||
* statement — once the static side is a value, the expression IS the
|
||||
* definition plus a classRef over it. A DECORATED class expression
|
||||
@@ -6041,8 +6065,16 @@ export function lowerClassExpressionInfo(
|
||||
* mints a class at all: evaluating the expression IS the
|
||||
* ReferenceError, so it lowers to exactly that read — every evaluation
|
||||
* throws identically, which is why the once-evaluated restriction and
|
||||
* the member fences don't apply. */
|
||||
export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclaration): IrExpr {
|
||||
* the member fences don't apply.
|
||||
*
|
||||
* `binding` is the local of a function-local class declaration's name,
|
||||
* declared without a value; the class value assigns it before the class's
|
||||
* static members are defined (see initializeRuntimeStatics). */
|
||||
export function lowerClassExpression(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.ClassLikeDeclaration,
|
||||
binding?: IrLocal,
|
||||
): IrExpr {
|
||||
if (
|
||||
decoratorNodesOf(expr).length > 0 ||
|
||||
expr.members.nodes.some((m) => decoratorNodesOf(m).length > 0)
|
||||
@@ -6058,6 +6090,41 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara
|
||||
}
|
||||
}
|
||||
const info = lowerClassExpressionInfo(lowerer, expr);
|
||||
// A function-local class expression's own name is a binding of its body:
|
||||
// declared before the members lower, so their references to the name
|
||||
// resolve to it. The class value captures it before it is assigned, as
|
||||
// it captures a class declaration's binding.
|
||||
if (
|
||||
!binding &&
|
||||
info.localClass &&
|
||||
ts.isClassExpression(expr) &&
|
||||
expr.name &&
|
||||
classExpressionNamesItself(lowerer, expr)
|
||||
) {
|
||||
const own = lowerer.declareLocal(
|
||||
expr.name,
|
||||
expr.name.text,
|
||||
{ kind: "classval", className: info.def.name },
|
||||
true,
|
||||
);
|
||||
const value = lowerClassValue(lowerer, expr, info, own);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: own.id, init: null, loc: value.loc }],
|
||||
result: value,
|
||||
type: value.type,
|
||||
loc: value.loc,
|
||||
};
|
||||
}
|
||||
return lowerClassValue(lowerer, expr, info, binding);
|
||||
}
|
||||
|
||||
function lowerClassValue(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.ClassLikeDeclaration,
|
||||
info: ClassInfo,
|
||||
binding: IrLocal | undefined,
|
||||
): IrExpr {
|
||||
if (
|
||||
!info.localClass &&
|
||||
!info.expressionInitializationEmitted &&
|
||||
@@ -6104,7 +6171,7 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara
|
||||
const clsRef: IrExpr = { kind: "varRef", localId: clsLocal.id, type: value.type, loc };
|
||||
const boxed: IrExpr = { kind: "dynFrom", value: clsRef, type: DYN, loc };
|
||||
info.localPrototypeData = true;
|
||||
return initializeRuntimeStatics(lowerer, info, {
|
||||
const built: IrExpr = {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: baseLocal.id, init: base, loc },
|
||||
@@ -6137,7 +6204,8 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara
|
||||
result: clsRef,
|
||||
type: value.type,
|
||||
loc,
|
||||
});
|
||||
};
|
||||
return initializeRuntimeStatics(lowerer, info, built, binding);
|
||||
}
|
||||
if (info.localClass?.baseExpression && info.base && value.kind === "classRef") {
|
||||
const loc = locOf(expr);
|
||||
@@ -6152,7 +6220,7 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara
|
||||
const clsRef: IrExpr = { kind: "varRef", localId: clsLocal.id, type: value.type, loc };
|
||||
const basePrototype = classPrototypeData(lowerer, info.base, loc, baseRef)!;
|
||||
info.localPrototypeData = true;
|
||||
return initializeRuntimeStatics(lowerer, info, {
|
||||
const built: IrExpr = {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: baseLocal.id, init: base, loc },
|
||||
@@ -6191,13 +6259,15 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara
|
||||
result: clsRef,
|
||||
type: value.type,
|
||||
loc,
|
||||
});
|
||||
};
|
||||
return initializeRuntimeStatics(lowerer, info, built, binding);
|
||||
}
|
||||
if (!info.localClass) return value;
|
||||
const classValue = initializeRuntimeStatics(
|
||||
lowerer,
|
||||
info,
|
||||
value.kind === "classRef" ? { ...value, captures: info.localClass.captureSources } : value,
|
||||
binding,
|
||||
);
|
||||
// The base resolves statically here, but Node still evaluates the
|
||||
// heritage expression each time the class definition runs, first.
|
||||
@@ -7124,12 +7194,102 @@ export function inHierarchy(lowerer: Lowerer, info: ClassInfo): boolean {
|
||||
* dynamically. Abstract re-declarations don't count (they carry no
|
||||
* implementation; the concrete ones below them do, via the recursion). */
|
||||
export function overrideBelow(lowerer: Lowerer, info: ClassInfo, name: string): boolean {
|
||||
if (info.decl !== null && localOverrideBelow(lowerer, info.decl, name)) return true;
|
||||
return info.subclasses.some((s) => {
|
||||
const m = s.methods.get(name);
|
||||
return (m !== undefined && m.abstract !== true) || lowerer.overrideBelow(s, name);
|
||||
});
|
||||
}
|
||||
|
||||
/** The class node an `extends` clause names: a class declaration, or the
|
||||
* class expression a variable is initialized with. */
|
||||
function extendedClassNode(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): ts.Node | null {
|
||||
const heritage = decl.heritageClauses?.nodes.find(
|
||||
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
|
||||
)?.types?.nodes[0]?.expression;
|
||||
if (!heritage || !ts.isIdentifier(heritage)) return null;
|
||||
let symbol = lowerer.checker.getSymbolAtLocation(heritage);
|
||||
if (symbol && symbol.flags & ts.SymbolFlags.Alias)
|
||||
symbol = lowerer.checker.getAliasedSymbol(symbol);
|
||||
const target = symbol?.valueDeclaration;
|
||||
if (!target) return null;
|
||||
if (ts.isClassDeclaration(target) || ts.isClassExpression(target)) return target;
|
||||
if (ts.isVariableDeclaration(target) && target.initializer) {
|
||||
let init: ts.Expression = target.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (ts.isClassExpression(init)) return init;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The function-local classes that extend the class node `base`. Function
|
||||
* bodies lower on demand, after code that may already call methods of a
|
||||
* class their local classes extend, so such a subclass is not yet in its
|
||||
* base's `subclasses` when those calls lower. */
|
||||
function localSubclassesOf(lowerer: Lowerer, base: ts.Node): readonly ts.ClassLikeDeclaration[] {
|
||||
let index = lowerer.localSubclassIndex;
|
||||
if (index === null) {
|
||||
index = new Map();
|
||||
const files = lowerer.moduleOrder.length > 0 ? lowerer.moduleOrder : [lowerer.entry];
|
||||
for (const sf of files) {
|
||||
if (!sf.text.includes("extends")) continue;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
(ts.isClassDeclaration(node) || ts.isClassExpression(node)) &&
|
||||
node.heritageClauses !== undefined &&
|
||||
functionLocalClass(node)
|
||||
) {
|
||||
const extended = extendedClassNode(lowerer, node);
|
||||
if (extended) {
|
||||
const list = index!.get(extended);
|
||||
if (list) list.push(node);
|
||||
else index!.set(extended, [node]);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
lowerer.localSubclassIndex = index;
|
||||
}
|
||||
return index.get(base) ?? [];
|
||||
}
|
||||
|
||||
/** True when a function-local class below the class node `base` declares
|
||||
* a concrete instance method or accessor `name` (accessors are keyed
|
||||
* `get:x` and `set:x`). A call on `base`'s class then dispatches on the
|
||||
* receiver; devirtualization turns it back into a direct call when no such
|
||||
* override is reachable. */
|
||||
function localOverrideBelow(
|
||||
lowerer: Lowerer,
|
||||
base: ts.Node,
|
||||
name: string,
|
||||
visited?: Set<ts.Node>,
|
||||
): boolean {
|
||||
const subclasses = localSubclassesOf(lowerer, base);
|
||||
if (subclasses.length === 0) return false;
|
||||
visited ??= new Set();
|
||||
if (visited.has(base)) return false;
|
||||
visited.add(base);
|
||||
const accessor = name.startsWith("get:") ? "get" : name.startsWith("set:") ? "set" : null;
|
||||
const member = accessor ? name.slice(4) : name;
|
||||
return subclasses.some(
|
||||
(sub) =>
|
||||
sub.members.nodes.some(
|
||||
(m) =>
|
||||
(accessor === null
|
||||
? ts.isMethodDeclaration(m)
|
||||
: accessor === "get"
|
||||
? ts.isGetAccessorDeclaration(m)
|
||||
: ts.isSetAccessorDeclaration(m)) &&
|
||||
m.body !== undefined &&
|
||||
m.name !== undefined &&
|
||||
(ts.isIdentifier(m.name) || ts.isStringLiteral(m.name)) &&
|
||||
m.name.text === member &&
|
||||
!ts.getModifiers(m)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword),
|
||||
) || localOverrideBelow(lowerer, sub, name, visited),
|
||||
);
|
||||
}
|
||||
|
||||
/** The nearest GENERIC-method declaration of `name` at/above `info` —
|
||||
* findMethodOn's twin over the genericMethods tables. */
|
||||
export function findGenericMethodOn(
|
||||
@@ -8987,8 +9147,9 @@ export function superCallStmt(
|
||||
// The SYNTHESIZED constructor of a ctor-less stream subclass:
|
||||
// super() with default options (underscore methods still bind; a
|
||||
// construction passing options requires a declared constructor —
|
||||
// lowerNew fences that). Zero options ⇒ exactly one init stmt.
|
||||
return lowerStreamSuperCall(
|
||||
// lowerNew fences that). Zero options ⇒ the init stmt, then the
|
||||
// class's _writev/_construct follow-ups.
|
||||
const stmts = lowerStreamSuperCall(
|
||||
lowerer,
|
||||
info,
|
||||
base,
|
||||
@@ -8996,7 +9157,8 @@ export function superCallStmt(
|
||||
thisLocal,
|
||||
loc,
|
||||
info.decl ?? info.ctor!,
|
||||
)[0]!;
|
||||
);
|
||||
return stmts.length === 1 ? stmts[0]! : { kind: "block", body: stmts, loc };
|
||||
}
|
||||
lowerer.noteSuperConstructorEdge(`%${base.def.name}.constructor`);
|
||||
const call: IrStmt = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js";
|
||||
import { lowerAwaitedUniformPromiseAll } from "./builtins/promises.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* Expression lowering: the expression dispatch (lowerExpr), literals
|
||||
@@ -575,6 +576,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
// boundary for malformed/upstream ASTs.
|
||||
lowerer.unsupported("SC1090", expr, "top-level await (await outside async functions)");
|
||||
}
|
||||
const uniformAll = lowerAwaitedUniformPromiseAll(lowerer, expr);
|
||||
if (uniformAll) return uniformAll;
|
||||
const value = lowerer.lowerExpr(expr.expression);
|
||||
// A promise-or-absent union (`Promise<T> | undefined` values, calls
|
||||
// of `(...) => Promise<void> | void` callbacks): the await handles
|
||||
@@ -2092,6 +2095,7 @@ export function typeofAnswer(arm: IrType): string | null {
|
||||
case "array":
|
||||
case "map":
|
||||
case "set":
|
||||
case "generator":
|
||||
case "regex":
|
||||
case "bytes":
|
||||
case "url":
|
||||
|
||||
@@ -687,8 +687,12 @@ export function lowerForAwaitGenerator(
|
||||
lowerer.scopes.push(new Map());
|
||||
try {
|
||||
const g = lowerer.declareHiddenLocal("%fag", genT);
|
||||
const done = lowerer.declareHiddenLocal("%fagdone", BOOL);
|
||||
done.mutable = true;
|
||||
// Whether a value has arrived and the body holds the generator open:
|
||||
// leaving the loop then (break, return, a throw, or a return() at a
|
||||
// yield in the body) closes it, as AsyncIteratorClose does; an
|
||||
// exhausted generator and a rejected next() are not closed.
|
||||
const active = lowerer.declareHiddenLocal("%fagactive", BOOL);
|
||||
active.mutable = true;
|
||||
const p = lowerer.declareHiddenLocal("%fagpromise", promiseT);
|
||||
const r = lowerer.declareHiddenLocal("%fagresult", recT);
|
||||
const gRef = (): IrExpr => ({ kind: "varRef", localId: g.id, type: genT, loc });
|
||||
@@ -709,7 +713,14 @@ export function lowerForAwaitGenerator(
|
||||
`for-await over an async generator yielding '${lowerer.fmt(genT.yieldT)}' (no per-element extraction exists)`,
|
||||
);
|
||||
}
|
||||
const setActive = (value: boolean): IrStmt => ({
|
||||
kind: "assign",
|
||||
localId: active.id,
|
||||
value: { kind: "boolLit", value, type: BOOL, loc },
|
||||
loc,
|
||||
});
|
||||
const head: IrStmt[] = [
|
||||
setActive(false),
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: p.id,
|
||||
@@ -737,19 +748,12 @@ export function lowerForAwaitGenerator(
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "assign",
|
||||
localId: done.id,
|
||||
value: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "break", loc },
|
||||
],
|
||||
then: [{ kind: "break", loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
...generatorLoopBinding(lowerer, stmt, extracted, genT.yieldT),
|
||||
setActive(true),
|
||||
];
|
||||
const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement), labels);
|
||||
const closePromise: IrExpr = {
|
||||
@@ -766,34 +770,40 @@ export function lowerForAwaitGenerator(
|
||||
{ kind: "varDecl", localId: g.id, init: iterable, loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: done.id,
|
||||
localId: active.id,
|
||||
init: { kind: "boolLit", value: false, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
body: [...head, ...body],
|
||||
...(labels && { labels }),
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "unary",
|
||||
op: "!",
|
||||
operand: { kind: "varRef", localId: done.id, type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
kind: "tryCatch",
|
||||
tryBody: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "awaitExpr", value: closePromise, type: recT, loc },
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
body: [...head, ...body],
|
||||
...(labels && { labels }),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
catchBody: null,
|
||||
catchLocalId: null,
|
||||
// After a throw, an error from closing the generator is dropped.
|
||||
dropFinallyErrors: true,
|
||||
finallyBody: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "varRef", localId: active.id, type: BOOL, loc },
|
||||
then: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "awaitExpr", value: closePromise, type: recT, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { perKeyOrder } from "./key-order-witness.js";
|
||||
import {
|
||||
dynUndefinedExpr,
|
||||
boolLit,
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
isDynTypedRefType,
|
||||
recordTextCodecClass,
|
||||
shapeHasAccessorSlots,
|
||||
shapeKeyOrders,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
@@ -1647,7 +1649,12 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
|
||||
});
|
||||
}
|
||||
body.push(depthGate("[Object]"), ...begin());
|
||||
for (const fname of order) {
|
||||
// One walk per key order (see perKeyOrder).
|
||||
const orders = shapeKeyOrders(current);
|
||||
const walks = orders.map((): IrStmt[] => []);
|
||||
for (const [k, fname] of orders.flatMap((order, k) =>
|
||||
order.map((name) => [k, name] as const),
|
||||
)) {
|
||||
const ft = byName.get(fname);
|
||||
if (!ft) continue;
|
||||
const show = entry(
|
||||
@@ -1655,12 +1662,13 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
|
||||
boolLit(false, loc),
|
||||
);
|
||||
const present = lowerer.recordFieldPresent(v(), t.shapeId, fname, loc);
|
||||
body.push(
|
||||
walks[k]!.push(
|
||||
present.kind === "recordHas"
|
||||
? { kind: "if", cond: present, then: [show], else_: null, loc }
|
||||
: show,
|
||||
);
|
||||
}
|
||||
body.push(...perKeyOrder(current, v(), walks, loc));
|
||||
body.push(
|
||||
ret(end(strLit("", loc), strLit("{", loc), strLit("}", loc), false, boolLit(false, loc))),
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js";
|
||||
import { declaredMemberOf, isPromisePrototypeRead } from "./expressions/property-access.js";
|
||||
import { hostsAll } from "./expressions/hosted-narrowing.js";
|
||||
import { requestGeneralStreamSurface } from "./general-streams.js";
|
||||
import { lowerForAwaitDynStream } from "./dyn-streams.js";
|
||||
import { lowerForAwaitDynStream, nodeStreamInterfaceSide } from "./dyn-streams.js";
|
||||
import { lowerForAwaitValueStream } from "./builtins/hash-stream.js";
|
||||
import { lowerChildPropertyWrite } from "./handle-properties.js";
|
||||
import {
|
||||
@@ -67,9 +67,11 @@ import {
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrGlobal,
|
||||
type IrLibFn,
|
||||
type IrLocal,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
SEARCH_PARAMS_T,
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
@@ -106,6 +108,7 @@ import {
|
||||
COMPOUND_ASSIGN_OPS,
|
||||
type CompoundOp,
|
||||
STR_METHODS,
|
||||
NARROW_FIRST,
|
||||
UNSUPPORTED_STMT,
|
||||
isStdlibMember,
|
||||
sideEffectFreeOptionValue,
|
||||
@@ -163,7 +166,11 @@ import { objectFactorySignature } from "./object-factory-new.js";
|
||||
import { isClassCallback, lowerClassCallbackAssign } from "./class-callbacks.js";
|
||||
import { classForInHasKey, classPropertiesHelper } from "./class-dynamic-dispatch.js";
|
||||
import { genericIfaceBindingKeepsClass, superConditionLead } from "./lower-classes.js";
|
||||
import { lowerStreamUnderscoreAssign, streamClassAliasDecl } from "./lower-stream.js";
|
||||
import {
|
||||
lowerStreamUnderscoreAssign,
|
||||
streamClassAliasDecl,
|
||||
streamSidesOf,
|
||||
} from "./lower-stream.js";
|
||||
import { staticAssertInstanceDecl } from "./assert-instances.js";
|
||||
import {
|
||||
lowerHttpResPropertyAssignment,
|
||||
@@ -239,6 +246,7 @@ import { npmStaticPackageOfPath } from "../npm-static.js";
|
||||
import { fenceNodeModuleMutation } from "./lower-node-module.js";
|
||||
import { fencePerfEntryKeyWalk } from "./builtins/performance.js";
|
||||
import { classGeneratorIterable } from "./class-iterables.js";
|
||||
import { iterableUnionOf, lowerForOfIterable } from "./iterable-values.js";
|
||||
|
||||
interface UsingRegistration {
|
||||
acquire: IrStmt[];
|
||||
@@ -1523,6 +1531,14 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
|
||||
const info = lowerer.lowerClassExpressionInfo(stmt);
|
||||
const type: IrType = { kind: "classval", className: info.def.name };
|
||||
const local = lowerer.declareLocal(stmt.name, stmt.name.text, type, true);
|
||||
// Static members of a function-local class run while the class value is
|
||||
// built and may read the class by name: the class value assigns the
|
||||
// binding before them.
|
||||
if (info.localClass && (info.runtimeStatics || info.runtimePrototypeMembers))
|
||||
return [
|
||||
{ kind: "varDecl", localId: local.id, init: null, loc: locOf(stmt) },
|
||||
{ kind: "exprStmt", expr: lowerer.lowerClassExpression(stmt, local), loc: locOf(stmt) },
|
||||
];
|
||||
const init = lowerer.lowerClassExpression(stmt);
|
||||
return { kind: "varDecl", localId: local.id, init, loc: locOf(stmt) };
|
||||
}
|
||||
@@ -3210,6 +3226,20 @@ export function lowerBindingPattern(
|
||||
lowerModuleNamespacePattern(lowerer, pattern, nsSource, isLet, out);
|
||||
return;
|
||||
}
|
||||
if (srcType.kind === "url") {
|
||||
lowerUrlObjectPattern(lowerer, pattern, srcRef, isLet, out);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
srcType.kind === "union" &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(pattern)) &&
|
||||
lowerer.unions
|
||||
.get(srcType.unionId)
|
||||
?.arms.every((arm) => arm.kind === "record" || arm.kind === "object")
|
||||
) {
|
||||
lowerUnionObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out);
|
||||
return;
|
||||
}
|
||||
if (srcType.kind !== "record") {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
@@ -3424,7 +3454,10 @@ function patternSourceValue(
|
||||
!arms ||
|
||||
!present?.length ||
|
||||
present.length === arms.length ||
|
||||
(present.length > 1 && !(arrayPattern && present.every((arm) => arm.kind === "array")))
|
||||
(present.length > 1 &&
|
||||
!(arrayPattern
|
||||
? present.every((arm) => arm.kind === "array")
|
||||
: present.every((arm) => arm.kind === "record" || arm.kind === "object")))
|
||||
)
|
||||
return value;
|
||||
for (const [tag, arm] of arms.entries()) {
|
||||
@@ -3541,6 +3574,149 @@ function lowerModuleNamespacePattern(
|
||||
}
|
||||
}
|
||||
|
||||
/** The URL getters an object pattern reads (`const { protocol, hostname }
|
||||
* = new URL(text)`), each a read of the URL's current serialization. */
|
||||
const URL_PATTERN_GETTERS: Record<string, IrLibFn> = {
|
||||
href: "url.href",
|
||||
origin: "url.origin",
|
||||
protocol: "url.protocol",
|
||||
username: "url.username",
|
||||
password: "url.password",
|
||||
host: "url.host",
|
||||
hostname: "url.hostname",
|
||||
port: "url.port",
|
||||
pathname: "url.pathname",
|
||||
search: "url.search",
|
||||
hash: "url.hash",
|
||||
};
|
||||
|
||||
/** An object pattern over a URL: each element reads its getter in pattern
|
||||
* order, as `url.name` does; `searchParams` binds the URL's live view.
|
||||
* Methods and rest elements stay fenced. */
|
||||
function lowerUrlObjectPattern(
|
||||
lowerer: Lowerer,
|
||||
pattern: ts.ObjectBindingPattern,
|
||||
srcRef: () => IrExpr,
|
||||
isLet: boolean,
|
||||
out: IrStmt[],
|
||||
): void {
|
||||
for (const el of pattern.elements.nodes) {
|
||||
if (el.name === undefined) continue;
|
||||
if (el.dotDotDotToken) {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
el,
|
||||
"rest bindings over URL sources (a URL's properties are prototype getters, which rest elements do not copy)",
|
||||
);
|
||||
}
|
||||
const prop = el.propertyName ?? el.name;
|
||||
const propName =
|
||||
ts.isIdentifier(prop) ||
|
||||
ts.isComputedPropertyName(prop) ||
|
||||
ts.isStringLiteralLike(prop) ||
|
||||
ts.isNumericLiteral(prop)
|
||||
? patternKeyNameOf(lowerer, prop as ts.PropertyName)
|
||||
: null;
|
||||
const loc = locOf(el);
|
||||
let value: IrExpr;
|
||||
if (propName !== null && Object.hasOwn(URL_PATTERN_GETTERS, propName)) {
|
||||
value = {
|
||||
kind: "libCall",
|
||||
fn: URL_PATTERN_GETTERS[propName]!,
|
||||
args: [srcRef()],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
} else if (propName === "searchParams") {
|
||||
value = {
|
||||
kind: "libCall",
|
||||
fn: "url.searchParams",
|
||||
args: [srcRef()],
|
||||
type: SEARCH_PARAMS_T,
|
||||
loc,
|
||||
};
|
||||
} else {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
el,
|
||||
propName === null
|
||||
? "destructuring with computed keys that do not fold to one property name"
|
||||
: `destructuring '${propName}' from a URL (${Object.keys(URL_PATTERN_GETTERS).join(", ")} and searchParams are the supported URL properties)`,
|
||||
);
|
||||
}
|
||||
if (el.initializer) value = applyBindingDefault(lowerer, el, value);
|
||||
lowerer.bindPatternTarget(el.name, value, isLet, out);
|
||||
}
|
||||
}
|
||||
|
||||
/** An object pattern over a union of records and class instances
|
||||
* (`function f({ kind, path }: Add | Remove)`): each element reads a data
|
||||
* field every arm declares with one type, through the runtime tag, as
|
||||
* `value.field` does. Rest elements and fields the arms type differently
|
||||
* stay fenced. */
|
||||
function lowerUnionObjectPattern(
|
||||
lowerer: Lowerer,
|
||||
pattern: ts.ObjectBindingPattern,
|
||||
srcRef: () => IrExpr,
|
||||
srcType: IrType & { kind: "union" },
|
||||
isLet: boolean,
|
||||
out: IrStmt[],
|
||||
): void {
|
||||
const arms = lowerer.unions.get(srcType.unionId)?.arms ?? [];
|
||||
for (const el of pattern.elements.nodes) {
|
||||
if (el.name === undefined) continue;
|
||||
if (el.dotDotDotToken) {
|
||||
lowerer.unsupported("SC1031", el, `rest bindings over union sources (${NARROW_FIRST})`);
|
||||
}
|
||||
const prop = el.propertyName ?? el.name;
|
||||
const propName =
|
||||
ts.isIdentifier(prop) ||
|
||||
ts.isComputedPropertyName(prop) ||
|
||||
ts.isStringLiteralLike(prop) ||
|
||||
ts.isNumericLiteral(prop)
|
||||
? patternKeyNameOf(lowerer, prop as ts.PropertyName)
|
||||
: null;
|
||||
if (propName === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
el,
|
||||
"destructuring with computed keys that do not fold to one property name",
|
||||
);
|
||||
}
|
||||
let common: IrType | null = null;
|
||||
for (const arm of arms) {
|
||||
let ft: IrType | undefined;
|
||||
if (arm.kind === "record") {
|
||||
ft = lowerer.shapes.get(arm.shapeId)?.fields.find((f) => f.name === propName)?.type;
|
||||
} else if (arm.kind === "object") {
|
||||
ft = lowerer.classes.get(arm.className)?.fields.get(propName);
|
||||
}
|
||||
if (!ft || ft.kind === "void" || isUnitType(ft) || (common && !typeEquals(common, ft))) {
|
||||
common = null;
|
||||
break;
|
||||
}
|
||||
common = ft;
|
||||
}
|
||||
if (common === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
el,
|
||||
`destructuring '${propName}' from a union whose arms type it differently (every arm must be a record or class with a same-typed field '${propName}' — ${NARROW_FIRST})`,
|
||||
);
|
||||
}
|
||||
let value: IrExpr = {
|
||||
kind: "unionDisc",
|
||||
unionId: srcType.unionId,
|
||||
field: propName,
|
||||
value: srcRef(),
|
||||
type: common,
|
||||
loc: locOf(el),
|
||||
};
|
||||
if (el.initializer) value = applyBindingDefault(lowerer, el, value);
|
||||
lowerer.bindPatternTarget(el.name, value, isLet, out);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a pattern's first element has a computed key that does not
|
||||
* fold to one property name, so the key evaluates at run time. */
|
||||
function startsWithRuntimeKey(lowerer: Lowerer, pattern: ts.ObjectBindingPattern): boolean {
|
||||
@@ -9513,6 +9689,21 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
|
||||
) {
|
||||
return lowerForAwaitStdin(lowerer, stmt);
|
||||
}
|
||||
// NodeJS.WritableStream is what @types/node answers for pipeline()
|
||||
// with a function destination, whose value is a PassThrough.
|
||||
if (nodeStreamInterfaceSide(lowerer, lowerer.typeOf(stmt.expression)) === "w") {
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(stmt.expression));
|
||||
const sides =
|
||||
mapped?.kind === "object"
|
||||
? streamSidesOf(lowerer, lowerer.classes.get(mapped.className))
|
||||
: null;
|
||||
if (sides !== "r" && sides !== "rw")
|
||||
lowerer.noLowering(
|
||||
"for await over a NodeJS.WritableStream",
|
||||
stmt.expression,
|
||||
"type the stream as the readable class it is: pipeline() answers a PassThrough for a function destination (pipeline(...) as PassThrough, or a JSDoc @type)",
|
||||
);
|
||||
}
|
||||
// A checked-dynamic NodeJS.ReadableStream iterates its native stream,
|
||||
// and a Hash, Hmac, Cipheriv or Decipheriv iterates its Transform.
|
||||
const dynStream =
|
||||
@@ -9699,6 +9890,10 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
|
||||
}
|
||||
iterSrc ??= stmt.expression;
|
||||
let iterable = lowerer.lowerExpr(iterSrc);
|
||||
{
|
||||
const iterableUnion = iterableUnionOf(lowerer, iterable.type);
|
||||
if (iterableUnion) return lowerForOfIterable(lowerer, stmt, iterable, iterableUnion, labels);
|
||||
}
|
||||
let tupleSource: {
|
||||
shapeId: string;
|
||||
fields: { name: string; type: IrType }[];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,7 @@ import { valueStreamOf } from "./builtins/hash-stream.js";
|
||||
import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js";
|
||||
import { iteratorSlotAdapter } from "./iterator-adapters.js";
|
||||
import { iterableSlotValue } from "./iterable-values.js";
|
||||
import type { FieldLift } from "./coercions/structural-plans.js";
|
||||
import { lowerBuiltinCall } from "./builtin-calls.js";
|
||||
import { CoercionState } from "./coercions/state.js";
|
||||
@@ -3309,6 +3310,9 @@ export class Lowerer {
|
||||
* the class and constructed-class counts, it tells caches over the class
|
||||
* graph when the graph changed. */
|
||||
subclassLinks = 0;
|
||||
/** Function-local classes by the class node they extend, built on first
|
||||
* use (localSubclassesOf in lower-classes.ts). */
|
||||
localSubclassIndex: Map<ts.Node, ts.ClassLikeDeclaration[]> | null = null;
|
||||
/** Set while a subclass constructor's super() call notes its base
|
||||
* constructor edge: that edge does not instantiate the base exactly. */
|
||||
private superConstructorEdge = false;
|
||||
@@ -9450,6 +9454,11 @@ export class Lowerer {
|
||||
const adapted = iteratorSlotAdapter(this, expr, expected);
|
||||
if (adapted) return adapted;
|
||||
}
|
||||
// Iterable-typed slots: other iterators enter the generator arm.
|
||||
if (expected.kind === "union" && !typeEquals(expr.type, expected)) {
|
||||
const iterable = iterableSlotValue(this, expr, expected);
|
||||
if (iterable) return iterable;
|
||||
}
|
||||
|
||||
// an any-typed slot marshals implicitly (tsc allows the assignment;
|
||||
// the marshal is where its semantics live). OUT: an 'any' value
|
||||
@@ -10858,9 +10867,29 @@ export class Lowerer {
|
||||
// Contextual types (such as Proxy descriptors' symbol-valued fields)
|
||||
// must not force an intermediate record with incompatible field slots.
|
||||
if (this.returnsLiteralIntoDyn(node, expected)) return this.lowerExprExpecting(node, expected);
|
||||
if (this.returnsFreshCollection(node) && expected.kind !== "void")
|
||||
return this.lowerExprExpecting(node, expected);
|
||||
return this.asyncReturnValueOf(node, this.lowerExpr(node), expected);
|
||||
}
|
||||
|
||||
/** An array literal or a `new Map(...)`, `new Set(...)` or `new
|
||||
* Array(...)` of the builtins: a fresh collection, never a thenable, so
|
||||
* an async return takes the declared result type as its construction
|
||||
* layout the way a synchronous return does (`return []` in a function
|
||||
* whose inferred result is `Promise<string[]>`). */
|
||||
private returnsFreshCollection(node: ts.Expression): boolean {
|
||||
let e = node;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (ts.isArrayLiteralExpression(e)) return true;
|
||||
return (
|
||||
ts.isNewExpression(e) &&
|
||||
ts.isIdentifier(e.expression) &&
|
||||
(this.isStdlibGlobal(e.expression, "Map") ||
|
||||
this.isStdlibGlobal(e.expression, "Set") ||
|
||||
this.isStdlibGlobal(e.expression, "Array"))
|
||||
);
|
||||
}
|
||||
|
||||
/** A fresh JS object literal returned into a checked-native (dyn) result,
|
||||
* which lowers straight into that storage. */
|
||||
private returnsLiteralIntoDyn(node: ts.Expression, expected: IrType): boolean {
|
||||
@@ -10992,7 +11021,11 @@ export class Lowerer {
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (this.adoptsReturnedPromise() && !this.returnsLiteralIntoDyn(node, expected)) {
|
||||
if (
|
||||
this.adoptsReturnedPromise() &&
|
||||
!this.returnsLiteralIntoDyn(node, expected) &&
|
||||
!this.returnsFreshCollection(node)
|
||||
) {
|
||||
// The async return lowers its operand untyped first (a promise
|
||||
// adopts before its payload converts); a maybe-promise union needs
|
||||
// the statement form, which branches between adoption and fulfillment.
|
||||
@@ -11455,8 +11488,8 @@ export class Lowerer {
|
||||
}
|
||||
}
|
||||
|
||||
lowerClassExpression(expr: ts.ClassLikeDeclaration): IrExpr {
|
||||
return lowerClassExpression(this, expr);
|
||||
lowerClassExpression(expr: ts.ClassLikeDeclaration, binding?: IrLocal): IrExpr {
|
||||
return lowerClassExpression(this, expr, binding);
|
||||
}
|
||||
|
||||
/* ── the class graph (single inheritance) ─────────────────────────── */
|
||||
|
||||
@@ -3375,6 +3375,17 @@ export function isStdlibSymbol(lowerer: Lowerer, symbol: ts.Symbol | undefined):
|
||||
* tamper-guard prologue) registered in stdlibGlobalAliases;
|
||||
* - a const binding of the Map or Set constructor (`const M = Map`,
|
||||
* builtinClassAliasOf). */
|
||||
/** The symbol an identifier reads. A shorthand property's name (`{ Date }`)
|
||||
* reads the binding of the same name, not the property it declares. */
|
||||
export function identifierReadSymbol(lowerer: Lowerer, expr: ts.Identifier): ts.Symbol | undefined {
|
||||
if (expr.parent && ts.isShorthandPropertyAssignment(expr.parent) && expr.parent.name === expr)
|
||||
return (
|
||||
lowerer.checker.getShorthandAssignmentValueSymbol(expr.parent) ??
|
||||
lowerer.checker.getSymbolAtLocation(expr)
|
||||
);
|
||||
return lowerer.checker.getSymbolAtLocation(expr);
|
||||
}
|
||||
|
||||
export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): string | null {
|
||||
if (ts.isParenthesizedExpression(expr)) return stdlibGlobalNameOf(lowerer, expr.expression);
|
||||
// The global-object detection chain (`typeof global < "u" ? global :
|
||||
@@ -3386,7 +3397,7 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin
|
||||
return stdlibGlobalNameOf(lowerer, taken ? expr.whenTrue : expr.whenFalse);
|
||||
}
|
||||
if (ts.isIdentifier(expr)) {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(expr);
|
||||
const symbol = identifierReadSymbol(lowerer, expr);
|
||||
// `globalThis` itself: a reserved intrinsic — tsc rejects user
|
||||
// global bindings, but local parameters can shadow it. Its special
|
||||
// symbol has no ordinary declarations for the provenance check.
|
||||
|
||||
@@ -11,6 +11,16 @@ export type WidthLift =
|
||||
| { how: "retag" }
|
||||
| { how: "unitRetag" }
|
||||
| { how: "liftWrap"; tag: number; arm: IrType }
|
||||
/** A record into a union of two record arms that differ by one field:
|
||||
* the source field `field` (`T | undefined`) selects `present` when it
|
||||
* holds a value and `absent`, which lacks the field, when it is
|
||||
* undefined. */
|
||||
| {
|
||||
how: "presenceRoute";
|
||||
field: string;
|
||||
present: { tag: number; arm: IrType };
|
||||
absent: { tag: number; arm: IrType };
|
||||
}
|
||||
| { how: "width" }
|
||||
| { how: "unionWidth" }
|
||||
| { how: "arr" }
|
||||
@@ -20,5 +30,6 @@ export type WidthLift =
|
||||
| { how: "clsWidth" }
|
||||
| { how: "narrow" }
|
||||
| { how: "dynIn" }
|
||||
| { how: "dynOut" }
|
||||
| { how: "upcast" }
|
||||
| { how: "funcAdapt" };
|
||||
|
||||
@@ -101,6 +101,30 @@ test("node words respect a byte view's offset, signed spans and unsigned flags",
|
||||
expect(() => file.kind(5)).toThrow(AstDecodeError);
|
||||
});
|
||||
|
||||
test("strings, string offsets and node words read alike at every byte alignment", () => {
|
||||
const original = response();
|
||||
for (let shift = 0; shift < 4; shift++) {
|
||||
const padded = new Uint8Array(original.length + 8).fill(0xff);
|
||||
padded.set(original, shift);
|
||||
const bytes = padded.subarray(shift, shift + original.length);
|
||||
const file = new AstWireFile(bytes);
|
||||
expect([file.string(0), file.string(2), file.text(1), file.text(3)]).toEqual([
|
||||
"abcfile!",
|
||||
"file",
|
||||
"abcfile!",
|
||||
"file",
|
||||
]);
|
||||
expect([file.kind(3), file.next(3), file.parent(4), file.data(4)]).toEqual([
|
||||
AstKind.Identifier,
|
||||
4,
|
||||
2,
|
||||
0x40000002,
|
||||
]);
|
||||
word(bytes, 56, 9);
|
||||
expect(() => new AstWireFile(bytes)).toThrow("invalid string table offset");
|
||||
}
|
||||
});
|
||||
|
||||
test("truncated headers, sections and node tables are rejected", () => {
|
||||
for (let size = 0; size < HEADER_SIZE; size++)
|
||||
expect(() => new AstWireFile(new Uint8Array(size))).toThrow(AstDecodeError);
|
||||
|
||||
@@ -109,6 +109,20 @@ function invalidNodeIndex(index: number): never {
|
||||
* byte order. */
|
||||
const LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
|
||||
|
||||
/** `count` little-endian words of `bytes` from `offset`, which the caller has
|
||||
* bounds-checked: a view of the response when the words are aligned on a
|
||||
* little-endian host, a copy of their bytes when they are not, and a decoded
|
||||
* copy on a big-endian host. Where a section starts depends on the lengths of
|
||||
* the strings before it (file paths among them), so most files are unaligned. */
|
||||
function astWords(bytes: Uint8Array, offset: number, count: number): Uint32Array {
|
||||
const start = bytes.byteOffset + offset;
|
||||
if (LITTLE_ENDIAN && start % 4 === 0) return new Uint32Array(bytes.buffer, start, count);
|
||||
const words = new Uint32Array(count);
|
||||
if (LITTLE_ENDIAN) new Uint8Array(words.buffer).set(bytes.subarray(offset, offset + count * 4));
|
||||
else for (let i = 0; i < count; i++) words[i] = astU32(bytes, offset + i * 4);
|
||||
return words;
|
||||
}
|
||||
|
||||
function hexWord(value: number): string {
|
||||
const digits = "0123456789abcdef";
|
||||
let result = "";
|
||||
@@ -125,14 +139,15 @@ export class AstWireFile {
|
||||
readonly stringCount: number;
|
||||
readonly contentHash: string;
|
||||
readonly parseOptionsKey: string;
|
||||
private readonly stringOffsets: number;
|
||||
private readonly strings: number;
|
||||
private readonly extended: number;
|
||||
private readonly structured: number;
|
||||
private readonly nodes: number;
|
||||
/** The node table as 32-bit words: a view of the response when it is
|
||||
* word-aligned on a little-endian host, otherwise a decoded copy. */
|
||||
/** The node table as 32-bit words (astWords). */
|
||||
private readonly words: Uint32Array;
|
||||
/** The string table's start and end offsets, two words per string
|
||||
* (astWords). */
|
||||
private readonly stringWords: Uint32Array;
|
||||
/** Decoded strings by string word index / 2. */
|
||||
private readonly stringCache: (string | undefined)[];
|
||||
private preorderTree: AstPreorder | null | undefined;
|
||||
@@ -142,34 +157,28 @@ export class AstWireFile {
|
||||
const metadata = astU32(bytes, HEADER_OFFSET_METADATA);
|
||||
if (metadata >>> 24 !== PROTOCOL_VERSION)
|
||||
throw new AstDecodeError(`unsupported protocol version ${metadata >>> 24}`);
|
||||
this.stringOffsets = astU32(bytes, HEADER_OFFSET_STRING_TABLE_OFFSETS);
|
||||
const stringOffsets = astU32(bytes, HEADER_OFFSET_STRING_TABLE_OFFSETS);
|
||||
this.strings = astU32(bytes, HEADER_OFFSET_STRING_TABLE);
|
||||
this.extended = astU32(bytes, HEADER_OFFSET_EXTENDED_DATA);
|
||||
this.structured = astU32(bytes, HEADER_OFFSET_STRUCTURED_DATA);
|
||||
this.nodes = astU32(bytes, HEADER_OFFSET_NODES);
|
||||
if (
|
||||
this.stringOffsets < HEADER_SIZE ||
|
||||
this.strings < this.stringOffsets ||
|
||||
stringOffsets < HEADER_SIZE ||
|
||||
this.strings < stringOffsets ||
|
||||
this.extended < this.strings ||
|
||||
this.structured < this.extended ||
|
||||
this.nodes < this.structured ||
|
||||
this.nodes > bytes.length ||
|
||||
(this.strings - this.stringOffsets) % 8 !== 0 ||
|
||||
(this.strings - stringOffsets) % 8 !== 0 ||
|
||||
(this.structured - this.extended) % 4 !== 0 ||
|
||||
(bytes.length - this.nodes) % NODE_LEN !== 0
|
||||
) {
|
||||
throw new AstDecodeError("invalid section layout");
|
||||
}
|
||||
this.nodeCount = (bytes.length - this.nodes) / NODE_LEN;
|
||||
this.stringCount = (this.strings - this.stringOffsets) / 8;
|
||||
const wordCount = this.nodeCount * NODE_WORDS;
|
||||
const start = bytes.byteOffset + this.nodes;
|
||||
if (LITTLE_ENDIAN && start % 4 === 0) {
|
||||
this.words = new Uint32Array(bytes.buffer, start, wordCount);
|
||||
} else {
|
||||
this.words = new Uint32Array(wordCount);
|
||||
for (let i = 0; i < wordCount; i++) this.words[i] = astU32(bytes, this.nodes + i * 4);
|
||||
}
|
||||
this.stringCount = (this.strings - stringOffsets) / 8;
|
||||
this.words = astWords(bytes, this.nodes, this.nodeCount * NODE_WORDS);
|
||||
this.stringWords = astWords(bytes, stringOffsets, this.stringCount * 2);
|
||||
this.stringCache = new Array<string | undefined>(this.stringCount).fill(undefined);
|
||||
if (this.nodeCount < 2) throw new AstDecodeError("missing root");
|
||||
if (this.kind(1) === KIND_NODE_LIST || this.parent(1) > 1)
|
||||
@@ -184,8 +193,8 @@ export class AstWireFile {
|
||||
// source text, so pairs overlap and are not globally monotone. Handles
|
||||
// address words (0, 2, 4...), not pair ordinals.
|
||||
for (let i = 0; i < this.stringCount; i++) {
|
||||
const start = astU32(bytes, this.stringOffsets + i * 8);
|
||||
const end = astU32(bytes, this.stringOffsets + i * 8 + 4);
|
||||
const start = this.stringWords[i * 2]!;
|
||||
const end = this.stringWords[i * 2 + 1]!;
|
||||
if (end < start || end > this.extended - this.strings)
|
||||
throw new AstDecodeError("invalid string table offset");
|
||||
}
|
||||
@@ -242,8 +251,8 @@ export class AstWireFile {
|
||||
throw new AstDecodeError(`invalid string index ${index}`);
|
||||
const cached = this.stringCache[index >>> 1];
|
||||
if (cached !== undefined) return cached;
|
||||
const start = astU32(this.bytes, this.stringOffsets + index * 4);
|
||||
const end = astU32(this.bytes, this.stringOffsets + (index + 1) * 4);
|
||||
const start = this.stringWords[index]!;
|
||||
const end = this.stringWords[index + 1]!;
|
||||
const text = decodeAstString(this.bytes, this.strings + start, end - start);
|
||||
this.stringCache[index >>> 1] = text;
|
||||
return text;
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
isSupportedMapKey,
|
||||
isSupportedMapValue,
|
||||
isSupportedSetElem,
|
||||
iterableArmsElem,
|
||||
isUnitType,
|
||||
mapOf,
|
||||
NULL_T,
|
||||
@@ -42,7 +43,7 @@ import {
|
||||
UNDEFINED_T,
|
||||
VOID,
|
||||
} from "../ir/ir.js";
|
||||
import { BIGINT_T } from "../ir/ir.js";
|
||||
import { BIGINT_T, KEY_ORDER_FIELD } from "../ir/ir.js";
|
||||
import { literalValues } from "./literal-values.js";
|
||||
import { remapUnionDiscriminant } from "./union-discriminants.js";
|
||||
|
||||
@@ -554,6 +555,21 @@ export class ShapeRegistry {
|
||||
return missing.length === 0 ? order : [...order, ...missing];
|
||||
}
|
||||
|
||||
/** Gives a shape with orderVariants its KEY_ORDER_FIELD, in place,
|
||||
* after the bodies lowered (key-order-witness.ts adds the field to each
|
||||
* of the shape's literals). Allocation zeroes it, which every other
|
||||
* construction keeps. */
|
||||
addKeyOrderField(shapeId: string): void {
|
||||
const shape = this.byId.get(shapeId);
|
||||
if (!shape || shape.fields.some((f) => f.name === KEY_ORDER_FIELD)) return;
|
||||
shape.fields = [...shape.fields, { name: KEY_ORDER_FIELD, type: F64 }].sort((a, b) =>
|
||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
||||
);
|
||||
this.revision++;
|
||||
const key = this.keyOf(shape.fields, false, shape.indexValue);
|
||||
if (!this.byKey.has(key)) this.byKey.set(key, shapeId);
|
||||
}
|
||||
|
||||
/** Adds optional fields to an interned shape in place: members a type
|
||||
* assertion attaches to the shape's values (`r as R & { tag: string }`
|
||||
* followed by `view.tag = ...`; see assertion-members.ts). Every value
|
||||
@@ -2361,6 +2377,27 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
const args = checker.getTypeArguments(ref);
|
||||
if (args.length === 0) return arrayOf(unitOnlyUnion(unions));
|
||||
if (tupleShape?.elementFlags === undefined) return null;
|
||||
// A leading-required tuple ending in a rest element whose positions
|
||||
// all share one type (`[string, ...string[]]`, the non-empty list)
|
||||
// is an array of that type: every read, write and method behaves as
|
||||
// the array's, and tsc already proves the required positions exist.
|
||||
const flags = tupleShape.elementFlags;
|
||||
if (
|
||||
flags.length >= 2 &&
|
||||
(flags[flags.length - 1]! & ts.ElementFlags.Rest) !== 0 &&
|
||||
flags.slice(0, -1).every((f) => (f & ts.ElementFlags.Required) !== 0)
|
||||
) {
|
||||
const elems = args.map((a) => mapType(a, ctx));
|
||||
const first = elems[0];
|
||||
if (
|
||||
first &&
|
||||
first.kind !== "void" &&
|
||||
isSupportedArrayElem(first) &&
|
||||
elems.every((e) => e !== null && typeEquals(e, first))
|
||||
)
|
||||
return arrayOf(first);
|
||||
return null;
|
||||
}
|
||||
// Optional/rest elements have no fixed native tuple layout.
|
||||
if (tupleShape.elementFlags.some((f) => !(f & ts.ElementFlags.Required))) {
|
||||
return null;
|
||||
@@ -3935,6 +3972,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
...channels,
|
||||
};
|
||||
}
|
||||
// Iterable<T>: the iterable union (iterableUnionType) — the source value
|
||||
// itself in its own arm, so an array stays the same multi-pass object and
|
||||
// a generator stays single-pass.
|
||||
if (isStdlibInterface("Iterable")) {
|
||||
const arg = checker.getTypeArguments(widened as ts.TypeReference)[0];
|
||||
const elem = arg ? mapType(arg, ctx) : null;
|
||||
return elem ? iterableUnionType(elem, ctx.shapes, unions) : null;
|
||||
}
|
||||
// IteratorResult<T, TReturn> — the checker's type of `g.next()` (an
|
||||
// alias for IteratorYieldResult<T> | IteratorReturnResult<TReturn>):
|
||||
// maps to the SAME interned record genResume answers, so `const r =
|
||||
@@ -4989,7 +5034,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
a.kind === "union" ||
|
||||
a.kind === "date" ||
|
||||
a.kind === "dyn" ||
|
||||
a.kind === "generator",
|
||||
// A generator arm only comes from an Iterable<T> (an optional
|
||||
// one keeps its unit arms beside the iterable union's).
|
||||
(a.kind === "generator" && iterableArmsElem(arms) === null),
|
||||
) ||
|
||||
!maybePromiseUnionOk(parts, arms, ctx)
|
||||
) {
|
||||
@@ -5857,6 +5904,18 @@ function emptyNeverArrayProp(p: ts.Symbol, fieldTs: ts.Type, checker: ts.TypeChe
|
||||
);
|
||||
}
|
||||
|
||||
/** A JavaScript object literal's property typed `never[]` (`{ issues: [] }`).
|
||||
* Nothing checks a JavaScript file's later writes, which can push any value
|
||||
* onto it (`ctx.common.issues.push(issue)`), so it holds the checked-dynamic
|
||||
* array rather than never's f64 element array. */
|
||||
function jsNeverArrayProp(p: ts.Symbol, fieldTs: ts.Type, checker: ts.TypeChecker): boolean {
|
||||
return (
|
||||
jsObjectLiteralProperty(p) &&
|
||||
checker.isArrayType(fieldTs) &&
|
||||
(checker.getTypeArguments(fieldTs as ts.TypeReference)[0]!.flags & ts.TypeFlags.Never) !== 0
|
||||
);
|
||||
}
|
||||
|
||||
/** A JavaScript object literal's property (`{ lastComponent: undefined }`,
|
||||
* `{ user: null }`). Nothing checks a JavaScript file's later writes, which
|
||||
* can store any value in it (`path.lastComponent = { ... }`), so a
|
||||
@@ -5868,6 +5927,12 @@ function isUnitOnlyIr(t: IrType, ctx: TypeMapperCtx): boolean {
|
||||
return def !== undefined && def.arms.every((arm) => isUnitType(arm));
|
||||
}
|
||||
|
||||
/** A property a JavaScript object literal declares. The checker also adds
|
||||
* each missing property, optional and `undefined`, to every literal of a
|
||||
* union of literals (`{ _tag, value } | { _tag, error }` gains
|
||||
* `error?: undefined` and `value?: undefined`); those keep their unit-only
|
||||
* type, so a literal still converts to the one arm whose properties it
|
||||
* has. */
|
||||
function jsObjectLiteralProperty(p: ts.Symbol): boolean {
|
||||
const d = p.valueDeclaration;
|
||||
// `__proto__: null` sets the literal's prototype; it declares no property.
|
||||
@@ -5875,6 +5940,7 @@ function jsObjectLiteralProperty(p: ts.Symbol): boolean {
|
||||
d !== undefined &&
|
||||
(ts.isPropertyAssignment(d) || ts.isShorthandPropertyAssignment(d)) &&
|
||||
ts.symbolName(p) !== "__proto__" &&
|
||||
(p.flags & ts.SymbolFlags.Optional) === 0 &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(d))
|
||||
);
|
||||
}
|
||||
@@ -6432,6 +6498,46 @@ export function unitOnlyUnion(unions: UnionRegistry): IrType {
|
||||
return { kind: "union", unionId: unions.intern(arms) };
|
||||
}
|
||||
|
||||
/** `Iterable<T>`: a union whose arms are the sources that keep their own
|
||||
* representation — `T[]`, a `Set<T>`, a `Map<K, V>` (T the `[K, V]`
|
||||
* tuple), a string (T string) — and a generator over T, which also holds
|
||||
* every other iterator (Map/Set and array iterators adapt into one). The
|
||||
* generator arm marks the union as iterable (iterableArmsElem): no other
|
||||
* union has one. Null when T has no array or generator form. */
|
||||
export function iterableUnionType(
|
||||
elem: IrType,
|
||||
shapes: ShapeRegistry,
|
||||
unions: UnionRegistry,
|
||||
): IrType | null {
|
||||
if (elem.kind === "void" || elem.kind === "generator" || !isSupportedArrayElem(elem)) return null;
|
||||
if (!genResultRecord(elem, VOID, shapes, unions)) return null;
|
||||
const arms: IrType[] = [
|
||||
{ kind: "array", elem },
|
||||
{ kind: "generator", yieldT: elem, retT: VOID, nextT: UNDEFINED_T },
|
||||
];
|
||||
const elemArms = elem.kind === "union" ? unions.get(elem.unionId)?.arms : undefined;
|
||||
if (isSupportedSetElem(elem, elemArms)) arms.push({ kind: "set", elem });
|
||||
if (elem.kind === "string") arms.push(STRING);
|
||||
if (elem.kind === "record") {
|
||||
const shape = shapes.get(elem.shapeId);
|
||||
const [k, v] = shape?.fields ?? [];
|
||||
if (
|
||||
shape?.tuple &&
|
||||
shape.fields.length === 2 &&
|
||||
k?.name === "0" &&
|
||||
v?.name === "1" &&
|
||||
isSupportedMapKey(
|
||||
k.type,
|
||||
k.type.kind === "union" ? unions.get(k.type.unionId)?.arms : undefined,
|
||||
) &&
|
||||
isSupportedMapValue(v.type)
|
||||
)
|
||||
arms.push({ kind: "map", key: k.type, value: v.type });
|
||||
}
|
||||
arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
|
||||
return { kind: "union", unionId: unions.intern(arms) };
|
||||
}
|
||||
|
||||
/** The IteratorResult record of a generator's value channels: `{ done:
|
||||
* boolean, value: V }` where V is dyn (an any/unknown channel) or the
|
||||
* canonical union of the yield arms, the return arms (when the return
|
||||
@@ -7947,6 +8053,7 @@ function mapRecordTypeInner(
|
||||
if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam))
|
||||
pt = unitOnlyUnion(ctx.unions);
|
||||
if (pt !== null && isUnitOnlyIr(pt, ctx) && jsObjectLiteralProperty(p)) pt = DYN;
|
||||
if (pt?.kind === "array" && jsNeverArrayProp(p, fieldTs, checker)) pt = DYN;
|
||||
// dyn FIELDS map now (`{ v: unknown }`, `[string, unknown]` tuples):
|
||||
// the slot carries a dyn value exactly like an `unknown`-valued
|
||||
// overflow entry — same RC adapters, same dynFrom conversion on the
|
||||
|
||||
@@ -346,7 +346,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"sp.finished",
|
||||
"sp.pipeline",
|
||||
"sp.pipelineStages",
|
||||
"sp.pipelineStagesOpts",
|
||||
"stream.pipelineStages",
|
||||
"stream.composeStages",
|
||||
"stream.pipelineStagesDyn",
|
||||
"sc.text",
|
||||
"sc.json",
|
||||
@@ -385,6 +387,14 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"writable.writeEnc",
|
||||
"writable.end",
|
||||
"writable.uncork",
|
||||
"writable.setDefaultEncoding",
|
||||
"stream.duplexFromGen",
|
||||
"stream.duplexFromDyn",
|
||||
"stream.duplexFromPromise",
|
||||
"stream.duplexFromAsyncFn",
|
||||
"stream.duplexFromGenSignal",
|
||||
"stream.duplexFromAsyncFnSignal",
|
||||
"fetch.readableFromWeb",
|
||||
"stream.destroy",
|
||||
"stream.destroyErr",
|
||||
"stream.iteratorClose",
|
||||
@@ -519,6 +529,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"readable.opMap",
|
||||
"readable.opFilter",
|
||||
"readable.opFlatMap",
|
||||
"readable.opFlatMapAsyncGen",
|
||||
"readable.opFlatMapSyncGen",
|
||||
"readable.opDrop",
|
||||
"readable.opTake",
|
||||
"rl.createSignal",
|
||||
|
||||
@@ -170,6 +170,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"fetch.streamFrom": { argTypes: [null], result: DYN },
|
||||
// The chunk/result record depends on ReadableStream<T>; validated by validate.ts.
|
||||
"fetch.readerRead": { argTypes: [DYN], result: VOID },
|
||||
/** Readable.fromWeb (scr_stream_from_web.c): the stream, objectMode,
|
||||
* highWaterMark (< 0: the mode's default) and encoding ("": none). */
|
||||
"fetch.readableFromWeb": { argTypes: [DYN, BOOL, F64, STRING], result: VOID },
|
||||
"json.parse": { argTypes: [STRING], result: DYN },
|
||||
"json.parseReviver": { argTypes: [STRING, DYN], result: DYN },
|
||||
"json.stringifyReplacer": { argTypes: [DYN, DYN, STRING], result: DYN },
|
||||
@@ -3953,6 +3956,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"stream.setDestroy": { argTypes: [null, null], result: VOID },
|
||||
"stream.setTransform": { argTypes: [null, null], result: VOID },
|
||||
"stream.setFlush": { argTypes: [null, null], result: VOID },
|
||||
"stream.setConstruct": { argTypes: [null, null], result: VOID },
|
||||
"stream.setWritev": { argTypes: [null, null], result: VOID },
|
||||
"stream.setWritevDefault": { argTypes: [null, null], result: VOID },
|
||||
/** The dyn-options twins (a checked-dynamic options record — the JS
|
||||
* lane's `super(options)` forwarding and `new Readable(dynVar)`): the
|
||||
* option walk runs at RUNTIME with Node's reading rules. newDyn:
|
||||
@@ -3979,6 +3985,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"stream.pipeline": { argTypes: [F64], result: VOID },
|
||||
"stream.pipelineDyn": { argTypes: [F64], result: VOID },
|
||||
"stream.pipelineStages": { argTypes: [F64, F64, F64, F64, F64], result: VOID },
|
||||
"stream.composeStages": { argTypes: [F64, F64, F64, F64, F64], result: VOID },
|
||||
"stream.pipelineStagesDyn": { argTypes: [F64, F64, F64, F64, F64], result: VOID },
|
||||
/** stream.compose(count, s1..sn): the Duplex over a pipeline of the
|
||||
* streams (+1). MAY THROW (ERR_INVALID_ARG_VALUE for a stream that is
|
||||
@@ -4023,6 +4030,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
argTypes: [F64, F64, F64, F64, F64],
|
||||
result: { kind: "promise", inner: VOID },
|
||||
},
|
||||
/** sp.pipelineStages with the options argument: the same head, then
|
||||
* end (a bool) and, with flags bit 6, the options' signal (dyn). */
|
||||
"sp.pipelineStagesOpts": {
|
||||
argTypes: [F64, F64, F64, F64, F64],
|
||||
result: { kind: "promise", inner: VOID },
|
||||
},
|
||||
/** node:stream/consumers — the promise consumers over the readable
|
||||
* machinery: (s) → a pending promise settled at the terminal point
|
||||
* with the accumulated result (sc.text: the utf8 decode, sc.json: the
|
||||
@@ -4079,6 +4092,28 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
/** The objectMode option's sides (readable, writable), applied right
|
||||
* after construction; answers the receiver (+1, the chaining shape). */
|
||||
"stream.objectMode": { argTypes: [null, BOOL, BOOL], result: VOID },
|
||||
"stream.decodeStrings": { argTypes: [null, BOOL], result: VOID },
|
||||
"stream.duplexFromGen": { argTypes: [null, null, null, null, null, null], result: VOID },
|
||||
"stream.duplexFromDyn": { argTypes: [DYN], result: VOID },
|
||||
/** Duplex.from over a lone Readable or Writable, or a { readable,
|
||||
* writable } pair of streams (duplexify.js's _duplexify). */
|
||||
"stream.duplexifyReadable": { argTypes: [null], result: VOID },
|
||||
"stream.duplexifyWritable": { argTypes: [null], result: VOID },
|
||||
"stream.duplexifyPair": { argTypes: [null, null], result: VOID },
|
||||
/** Duplex.from(promise): the promise and its box helper. */
|
||||
"stream.duplexFromPromise": { argTypes: [null, null], result: VOID },
|
||||
/** Duplex.from(asyncFunction): the stage, its source helper and the
|
||||
* promise's box helper. */
|
||||
"stream.duplexFromAsyncFn": { argTypes: [null, null, null], result: VOID },
|
||||
/** The same for a body that declares the options argument: then the
|
||||
* AbortController's abort() and its signal. */
|
||||
"stream.duplexFromGenSignal": {
|
||||
argTypes: [null, null, null, null, null, null, null, DYN],
|
||||
result: VOID,
|
||||
},
|
||||
"stream.duplexFromAsyncFnSignal": { argTypes: [null, null, null, null, DYN], result: VOID },
|
||||
"writable.writeEncoding": { argTypes: [null, STRING], result: VOID },
|
||||
"writable.setDefaultEncoding": { argTypes: [null, DYN], result: VOID },
|
||||
/** Whether a readable.nextChunkDyn step answered the end (the done
|
||||
* marker; an object-mode stream can yield undefined). */
|
||||
"stream.chunkDone": { argTypes: [DYN], result: BOOL },
|
||||
@@ -4114,17 +4149,39 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* forEach nothing; reduce: [stream, fn, init, hasInit, options, kind]
|
||||
* with the payload kind (0 dyn, 1 number, 2 boolean, 3 string). The
|
||||
* promise-returning ones reject instead of throwing. */
|
||||
"readable.opMap": { argTypes: [null, DYN, DYN], result: VOID },
|
||||
"readable.opFilter": { argTypes: [null, DYN, DYN], result: VOID },
|
||||
"readable.opFlatMap": { argTypes: [null, DYN, DYN], result: VOID },
|
||||
"readable.opMap": { argTypes: [null, DYN, DYN, DYN], result: VOID },
|
||||
"readable.opFilter": { argTypes: [null, DYN, DYN, DYN], result: VOID },
|
||||
"readable.opFlatMap": { argTypes: [null, DYN, DYN, DYN], result: VOID },
|
||||
/** flatMap whose callback returns a compiled generator: the helpers
|
||||
* over its type follow (async: next, read, return, throw; sync: next,
|
||||
* return, throw). */
|
||||
"readable.opFlatMapAsyncGen": {
|
||||
argTypes: [null, DYN, DYN, DYN, null, null, null, null],
|
||||
result: VOID,
|
||||
},
|
||||
"readable.opFlatMapSyncGen": { argTypes: [null, DYN, DYN, DYN, null, null, null], result: VOID },
|
||||
"readable.opDrop": { argTypes: [null, DYN, DYN], result: VOID },
|
||||
"readable.opTake": { argTypes: [null, DYN, DYN], result: VOID },
|
||||
"readable.opToArray": { argTypes: [null, DYN], result: { kind: "promise", inner: DYN } },
|
||||
"readable.opForEach": { argTypes: [null, DYN, DYN], result: { kind: "promise", inner: VOID } },
|
||||
"readable.opSome": { argTypes: [null, DYN, DYN], result: { kind: "promise", inner: BOOL } },
|
||||
"readable.opEvery": { argTypes: [null, DYN, DYN], result: { kind: "promise", inner: BOOL } },
|
||||
"readable.opFind": { argTypes: [null, DYN, DYN], result: { kind: "promise", inner: DYN } },
|
||||
"readable.opForEach": {
|
||||
argTypes: [null, DYN, DYN, DYN],
|
||||
result: { kind: "promise", inner: VOID },
|
||||
},
|
||||
"readable.opSome": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: BOOL } },
|
||||
"readable.opEvery": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: BOOL } },
|
||||
"readable.opFind": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: DYN } },
|
||||
/** The map family's last argument is the callback's { signal } (or
|
||||
* undefined). reduceSignal adds ac.abort() (a closure) and
|
||||
* { signal: ac.signal } for a reducer that reads them. */
|
||||
"readable.opReduce": { argTypes: [null, DYN, DYN, BOOL, DYN, F64], result: VOID },
|
||||
"readable.opReduceSignal": {
|
||||
argTypes: [null, DYN, DYN, BOOL, DYN, F64, null, DYN],
|
||||
result: VOID,
|
||||
},
|
||||
"readable.opSignalList": { argTypes: [DYN], result: DYN },
|
||||
"readable.opSignalOpts": { argTypes: [DYN], result: DYN },
|
||||
"readable.opSignalOf": { argTypes: [DYN], result: DYN },
|
||||
"readable.opOnceOptions": { argTypes: [], result: DYN },
|
||||
"readable.resume": { argTypes: [null], result: VOID },
|
||||
"readable.isPaused": { argTypes: [null], result: BOOL },
|
||||
"readable.pipe": { argTypes: [null, null, BOOL], result: VOID },
|
||||
@@ -4832,10 +4889,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
export type IrLibFn = keyof typeof LIB_FN_SIGS;
|
||||
|
||||
/** The function-stage runs of a pipeline of `total` stages that a stream
|
||||
* follows (positions bit j: stage j is a function): each pumps its last
|
||||
* generator into that stream through one (next, read, return) triple. */
|
||||
export function pipelinePumpedRuns(total: number, positions: number): number {
|
||||
let runs = 0;
|
||||
* follows (positions bit j: stage j is a function), and an async
|
||||
* generator destination's run (flags bits 1-2 = 1, pumped into the
|
||||
* PassThrough pipeline() answers): each pumps its last generator through
|
||||
* one (next, read, return) triple. */
|
||||
export function pipelinePumpedRuns(total: number, positions: number, flags: number): number {
|
||||
let runs = ((flags >> 1) & 3) === 1 ? 1 : 0;
|
||||
for (let j = 0; j + 1 < total; j++) {
|
||||
if (Math.floor(positions / 2 ** j) % 2 === 1 && Math.floor(positions / 2 ** (j + 1)) % 2 === 0)
|
||||
runs++;
|
||||
|
||||
@@ -172,6 +172,7 @@ const MODULE_FIELDS: Record<StringFields<Omit<IrModule, "functions">>, Role> = {
|
||||
"records[].fields[].name": "other",
|
||||
"records[].declaredOrder[]": "other",
|
||||
"records[].accessorOrder[]": "other",
|
||||
"records[].orderVariants[][]": "other",
|
||||
"unions[].id": "other",
|
||||
"unions[].discriminant.field": "other",
|
||||
"unions[].discriminant.cases[].values[]": "other",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
canDynCheckTo,
|
||||
type IrBytesElem,
|
||||
isJsonSafeType,
|
||||
iterableArmsElem,
|
||||
isJsonStringifySafeType,
|
||||
isJsonStringifyDynamicType,
|
||||
type IrRecordShape,
|
||||
@@ -22,6 +23,29 @@ import {
|
||||
typeKey,
|
||||
} from "./ir.js";
|
||||
|
||||
describe("iterable unions", () => {
|
||||
const gen = (yieldT: IrType): IrType => ({
|
||||
kind: "generator",
|
||||
yieldT,
|
||||
retT: { kind: "void" },
|
||||
nextT: { kind: "undefinedT" },
|
||||
});
|
||||
|
||||
test("names the element type of an Iterable<T> union", () => {
|
||||
expect(iterableArmsElem([arrayOf(F64), gen(F64), { kind: "set", elem: F64 }])).toEqual(F64);
|
||||
expect(iterableArmsElem([arrayOf(STRING), gen(STRING), STRING])).toEqual(STRING);
|
||||
expect(iterableArmsElem([arrayOf(F64), gen(F64), { kind: "undefinedT" }])).toEqual(F64);
|
||||
});
|
||||
|
||||
test("rejects unions without exactly one plain generator arm over the same element", () => {
|
||||
expect(iterableArmsElem([arrayOf(F64), STRING])).toBeNull();
|
||||
expect(iterableArmsElem([arrayOf(STRING), gen(F64)])).toBeNull();
|
||||
expect(iterableArmsElem([arrayOf(F64), gen(F64), gen(STRING)])).toBeNull();
|
||||
expect(iterableArmsElem([gen(F64), STRING])).toBeNull();
|
||||
expect(iterableArmsElem([arrayOf(F64), { ...gen(F64), nextT: F64 } as IrType])).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("IR kind sets", () => {
|
||||
test("keeps procStream as the scalar handle exception", () => {
|
||||
expect(HANDLE_KINDS.has("procStream")).toBe(true);
|
||||
|
||||
@@ -730,6 +730,32 @@ function isCollectionValueKey(t: IrType): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** The element type of an ITERABLE union (`Iterable<T>`,
|
||||
* iterableUnionType): its one generator arm — a synchronous generator over
|
||||
* T that returns nothing and takes no `.next()` values — beside source arms
|
||||
* (T[], Set<T>, a Map of [K, V] entries, a string), and the undefined and
|
||||
* null arms of an optional Iterable. A generator arm is legal only in such
|
||||
* a union. Null for every other union. */
|
||||
export function iterableArmsElem(arms: readonly IrType[]): IrType | null {
|
||||
let elem: IrType | null = null;
|
||||
for (const arm of arms) {
|
||||
if (arm.kind !== "generator") continue;
|
||||
if (elem !== null || arm.async || arm.retT.kind !== "void" || arm.nextT.kind !== "undefinedT")
|
||||
return null;
|
||||
elem = arm.yieldT;
|
||||
}
|
||||
if (elem === null) return null;
|
||||
for (const arm of arms) {
|
||||
if (arm.kind === "generator" || arm.kind === "undefinedT" || arm.kind === "nullT") continue;
|
||||
if (arm.kind === "array" || arm.kind === "set") {
|
||||
if (!typeEquals(arm.elem, elem)) return null;
|
||||
} else if (arm.kind === "string") {
|
||||
if (elem.kind !== "string") return null;
|
||||
} else if (arm.kind !== "map") return null;
|
||||
}
|
||||
return elem;
|
||||
}
|
||||
|
||||
/** Set elements and Map keys share storage, equality and ownership rules. */
|
||||
export function isSupportedSetElem(t: IrType, unionArms?: IrType[]): boolean {
|
||||
return isSupportedMapKey(t, unionArms);
|
||||
@@ -1665,6 +1691,24 @@ export interface IrRecordShape {
|
||||
* an accessor property named once (see accessorSlotProp). The
|
||||
* checked-dynamic copy lists its keys in this order. */
|
||||
accessorOrder?: string[];
|
||||
/** Every order the shape's values enumerate their keys in, when its
|
||||
* object literals write the keys in more than one order (see
|
||||
* key-order-witness.ts); declaredOrder is the first. Each value holds the
|
||||
* index of its order in the hidden number field KEY_ORDER_FIELD: the
|
||||
* literal that wrote it stores its order's index, a copy its source's,
|
||||
* and every other construction leaves 0. Each key-order surface
|
||||
* dispatches on that field. */
|
||||
orderVariants?: string[][];
|
||||
}
|
||||
|
||||
/** The hidden field of a shape with orderVariants: the index of the value's
|
||||
* key order. */
|
||||
export const KEY_ORDER_FIELD = "%order";
|
||||
|
||||
/** The orders a record shape's values enumerate their keys in: each order
|
||||
* of a shape with orderVariants, or its one table. */
|
||||
export function shapeKeyOrders(shape: IrRecordShape): readonly (readonly string[])[] {
|
||||
return shape.orderVariants ?? [shape.declaredOrder ?? shape.fields.map((f) => f.name)];
|
||||
}
|
||||
|
||||
/** fs.Dirent rows: the hidden %dtype entry kind (libuv's UV_DIRENT
|
||||
@@ -1943,8 +1987,18 @@ export type IrStmt =
|
||||
* RangeError from the runtime. `pop` marks the stores that implement
|
||||
* `pop()`: a frozen (published) array fails with pop's TypeError rather
|
||||
* than the length assignment's, and pop's `length = 0` store on an empty
|
||||
* array is dropped unless the array may be frozen. */
|
||||
| { kind: "arraySetLength"; arr: IrExpr; length: IrExpr; loc: SrcLoc; pop?: boolean }
|
||||
* array is dropped unless the array may be frozen. `reserve` marks a
|
||||
* capacity hint instead: dense storage for `length` elements of a fresh
|
||||
* array that statements fill next, with no change to its length and no
|
||||
* observable effect. */
|
||||
| {
|
||||
kind: "arraySetLength";
|
||||
arr: IrExpr;
|
||||
length: IrExpr;
|
||||
loc: SrcLoc;
|
||||
pop?: boolean;
|
||||
reserve?: boolean;
|
||||
}
|
||||
/** Write an explicit present `undefined` state without shrinking length. */
|
||||
| { kind: "arraySetUndefined"; arr: IrExpr; index: IrExpr; loc: SrcLoc }
|
||||
/** Delete an indexed/property slot without changing Array.length. */
|
||||
|
||||
@@ -218,6 +218,17 @@ const STREAM_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"child.stdioStream",
|
||||
"stream.chunkDone",
|
||||
"stream.objectMode",
|
||||
"stream.decodeStrings",
|
||||
"stream.duplexFromGen",
|
||||
"stream.duplexFromDyn",
|
||||
"stream.duplexFromPromise",
|
||||
"stream.duplexFromAsyncFn",
|
||||
"stream.duplexFromGenSignal",
|
||||
"stream.duplexFromAsyncFnSignal",
|
||||
"fetch.readableFromWeb",
|
||||
"stream.duplexifyReadable",
|
||||
"stream.duplexifyWritable",
|
||||
"stream.duplexifyPair",
|
||||
"console.bind",
|
||||
"crypto.hashStream",
|
||||
"crypto.hashStreamPiped",
|
||||
@@ -230,6 +241,7 @@ const STREAM_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"sp.finished",
|
||||
"sp.pipeline",
|
||||
"sp.pipelineStages",
|
||||
"sp.pipelineStagesOpts",
|
||||
"stream.addAbortSignal",
|
||||
"stream.addAbortSignalFn",
|
||||
"stream.destroy",
|
||||
@@ -257,6 +269,7 @@ const STREAM_LIB_PREFIXES: readonly string[] = [
|
||||
"sp.pipelineOpts",
|
||||
"stream.finishedOpts",
|
||||
"stream.pipelineStages",
|
||||
"stream.composeStages",
|
||||
"stream.set",
|
||||
"transform.",
|
||||
"writable.",
|
||||
@@ -270,6 +283,12 @@ const READABLE_DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"readable.fromDone",
|
||||
"readable.fromDyn",
|
||||
"readable.fromIter",
|
||||
"stream.duplexFromGen",
|
||||
"stream.duplexFromDyn",
|
||||
"stream.duplexFromPromise",
|
||||
"stream.duplexFromAsyncFn",
|
||||
"stream.duplexFromGenSignal",
|
||||
"stream.duplexFromAsyncFnSignal",
|
||||
]);
|
||||
|
||||
/** libCall name prefixes of the Readable entry points that await through the dyn-async unit. */
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
} from "./ir.js";
|
||||
import {
|
||||
arrayOf,
|
||||
iterableArmsElem,
|
||||
BYTES_ELEMENT_NAME,
|
||||
BOOL,
|
||||
BYTES_U8,
|
||||
@@ -801,7 +802,7 @@ export function validateModule(mod: IrModule): IrValidationError[] {
|
||||
arm.kind === "union" ||
|
||||
arm.kind === "dyn" ||
|
||||
arm.kind === "date" ||
|
||||
arm.kind === "generator"
|
||||
(arm.kind === "generator" && iterableArmsElem(u.arms) === null)
|
||||
) {
|
||||
errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc });
|
||||
}
|
||||
@@ -4102,8 +4103,10 @@ function validateFunction(
|
||||
e.fn === "stream.pipelineDyn" ||
|
||||
e.fn === "stream.compose" ||
|
||||
e.fn === "stream.pipelineStages" ||
|
||||
e.fn === "stream.composeStages" ||
|
||||
e.fn === "stream.pipelineStagesDyn" ||
|
||||
e.fn === "sp.pipelineStages" ||
|
||||
e.fn === "sp.pipelineStagesOpts" ||
|
||||
e.fn === "sp.pipeline" ||
|
||||
e.fn === "sp.pipelineOpts" ||
|
||||
e.fn === "sp.pipelineOptsSignal" ||
|
||||
@@ -4287,6 +4290,22 @@ function validateFunction(
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (e.fn === "fetch.readableFromWeb") {
|
||||
// Readable.fromWeb: (stream, objectMode, highWaterMark, encoding) → a Readable.
|
||||
if (
|
||||
e.args.length !== 4 ||
|
||||
e.args[0]?.type.kind !== "dyn" ||
|
||||
e.args[1]?.type.kind !== "bool" ||
|
||||
e.args[2]?.type.kind !== "f64" ||
|
||||
e.args[3]?.type.kind !== "string" ||
|
||||
e.type.kind !== "object"
|
||||
)
|
||||
err(
|
||||
`libCall ${e.fn} must take (stream, objectMode, highWaterMark, encoding) and return a Readable`,
|
||||
e.loc,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (e.fn === "fetch.readerRead") {
|
||||
if (e.type.kind !== "promise" || e.type.inner.kind !== "record") {
|
||||
err(`libCall fetch.readerRead must return a promise of a read-result record`, e.loc);
|
||||
@@ -5471,6 +5490,19 @@ function validateFunction(
|
||||
e.fn === "stream.destroy" ||
|
||||
e.fn === "stream.destroyErr" ||
|
||||
e.fn === "stream.objectMode" ||
|
||||
e.fn === "stream.decodeStrings" ||
|
||||
e.fn === "stream.duplexFromGen" ||
|
||||
e.fn === "stream.duplexFromDyn" ||
|
||||
e.fn === "stream.duplexifyReadable" ||
|
||||
e.fn === "stream.duplexifyWritable" ||
|
||||
e.fn === "stream.duplexifyPair" ||
|
||||
e.fn === "stream.duplexFromPromise" ||
|
||||
e.fn === "stream.duplexFromAsyncFn" ||
|
||||
e.fn === "stream.duplexFromGenSignal" ||
|
||||
e.fn === "stream.duplexFromAsyncFnSignal" ||
|
||||
e.fn === "stream.setConstruct" ||
|
||||
e.fn === "stream.setWritev" ||
|
||||
e.fn === "stream.setWritevDefault" ||
|
||||
e.fn === "stream.iteratorClose" ||
|
||||
e.fn === "stream.prop" ||
|
||||
e.fn === "stream.errored" ||
|
||||
@@ -5480,8 +5512,10 @@ function validateFunction(
|
||||
e.fn === "stream.pipelineDyn" ||
|
||||
e.fn === "stream.compose" ||
|
||||
e.fn === "stream.pipelineStages" ||
|
||||
e.fn === "stream.composeStages" ||
|
||||
e.fn === "stream.pipelineStagesDyn" ||
|
||||
e.fn === "sp.pipelineStages" ||
|
||||
e.fn === "sp.pipelineStagesOpts" ||
|
||||
e.fn === "stream.addAbortSignal" ||
|
||||
e.fn === "stream.addAbortSignalFn" ||
|
||||
e.fn === "stream.finishedOpts" ||
|
||||
@@ -5521,6 +5555,38 @@ function validateFunction(
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (e.fn === "stream.duplexFromPromise") {
|
||||
// Duplex.from(promise): (promise, box) → a Duplex.
|
||||
if (e.args[0]?.type.kind !== "promise" || e.args[1]?.type.kind !== "func")
|
||||
err(`libCall ${e.fn} takes a promise and its box helper`, e.loc);
|
||||
if (!isStreamObject(e.type)) err(`libCall ${e.fn} must return a stream class`, e.loc);
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
e.fn === "stream.duplexifyReadable" ||
|
||||
e.fn === "stream.duplexifyWritable" ||
|
||||
e.fn === "stream.duplexifyPair"
|
||||
) {
|
||||
// Duplex.from over streams: (r), (w) or (r, w) → a Duplex.
|
||||
const want = e.fn === "stream.duplexifyPair" ? 2 : 1;
|
||||
if (e.args.length !== want || !e.args.every((a) => isStreamObject(a.type)))
|
||||
err(`libCall ${e.fn} takes ${want} stream argument(s)`, e.loc);
|
||||
if (!isStreamObject(e.type)) err(`libCall ${e.fn} must return a stream class`, e.loc);
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
e.fn === "stream.duplexFromGen" ||
|
||||
e.fn === "stream.duplexFromDyn" ||
|
||||
e.fn === "stream.duplexFromAsyncFn" ||
|
||||
e.fn === "stream.duplexFromGenSignal" ||
|
||||
e.fn === "stream.duplexFromAsyncFnSignal"
|
||||
) {
|
||||
// Duplex.from: (stage, source, helpers...) or (body) → a Duplex.
|
||||
if (!isStreamObject(e.type)) {
|
||||
err(`libCall ${e.fn} must return a stream class, got ${e.type.kind}`, e.loc);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (e.fn.endsWith(".newDyn")) {
|
||||
// The dyn-options constructor: (optsDyn) → the stream class.
|
||||
if (!isStreamObject(e.type)) {
|
||||
@@ -5591,13 +5657,15 @@ function validateFunction(
|
||||
if (
|
||||
e.fn === "stream.pipelineStages" ||
|
||||
e.fn === "stream.pipelineStagesDyn" ||
|
||||
e.fn === "sp.pipelineStages"
|
||||
e.fn === "sp.pipelineStages" ||
|
||||
e.fn === "sp.pipelineStagesOpts" ||
|
||||
e.fn === "stream.composeStages"
|
||||
) {
|
||||
// (n, k, positions, flags, mask, s1..sn, stage1..stagek,
|
||||
// helpers..., box?, (signal, abort)?[, cb]): positions bit j marks
|
||||
// stage j a function, flags bits 1-2 the destination, bit 5 the
|
||||
// signal group; helpers are three per function run a stream
|
||||
// follows.
|
||||
// helpers..., box?, (signal, abort)?[, cb | end, signal?]):
|
||||
// positions bit j marks stage j a function, flags bits 1-2 the
|
||||
// destination, bit 5 the signal group, bit 6 the options' signal;
|
||||
// helpers are three per pumped function run.
|
||||
const [n, k, positions, flags] = e.args
|
||||
.slice(0, 4)
|
||||
.map((a) => (a?.kind === "numLit" ? a.value : -1));
|
||||
@@ -5608,9 +5676,14 @@ function validateFunction(
|
||||
if (Math.floor(positions! / 2 ** j) % 2 === 1) functions++;
|
||||
}
|
||||
const lastIsFunction = Math.floor(positions! / 2 ** (total - 1)) % 2 === 1;
|
||||
const helperCount = 3 * pipelinePumpedRuns(total, positions!);
|
||||
const helperCount = 3 * pipelinePumpedRuns(total, positions!, flags!);
|
||||
const groups = helperCount + (destKind === 2 ? 1 : 0) + (flags! & 32 ? 2 : 0);
|
||||
const cbArgs = e.fn === "sp.pipelineStages" ? 0 : 1;
|
||||
const cbArgs =
|
||||
e.fn === "sp.pipelineStagesOpts"
|
||||
? 1 + (flags! & 64 ? 1 : 0)
|
||||
: e.fn === "sp.pipelineStages" || e.fn === "stream.composeStages"
|
||||
? 0
|
||||
: 1;
|
||||
if (
|
||||
flags! < 0 ||
|
||||
n! < 0 ||
|
||||
@@ -5619,7 +5692,8 @@ function validateFunction(
|
||||
total > 52 ||
|
||||
positions! >= 2 ** total ||
|
||||
functions !== k ||
|
||||
(destKind === 2) !== lastIsFunction ||
|
||||
destKind === 3 ||
|
||||
(destKind !== 0) !== lastIsFunction ||
|
||||
total < 2 ||
|
||||
e.args.length !== 5 + n! + k! + groups + cbArgs
|
||||
) {
|
||||
@@ -5647,12 +5721,19 @@ function validateFunction(
|
||||
if (e.args[at2]?.type.kind !== "dyn" || e.args[at2 + 1]?.type.kind !== "func")
|
||||
err(`libCall ${e.fn} signal and abort must be a dyn and a func`, e.loc);
|
||||
}
|
||||
if (cbArgs) {
|
||||
if (e.fn === "sp.pipelineStagesOpts") {
|
||||
const tail = e.args.slice(e.args.length - cbArgs);
|
||||
if (tail[0]?.type.kind !== "bool" || (cbArgs > 1 && tail[1]?.type.kind !== "dyn"))
|
||||
err(`libCall ${e.fn} end and signal must be a bool and a dyn`, e.loc);
|
||||
if (e.type.kind !== "promise") err(`libCall ${e.fn} must return a promise`, e.loc);
|
||||
} else if (cbArgs) {
|
||||
const cbK = e.args[e.args.length - 1]?.type.kind;
|
||||
if (e.fn === "stream.pipelineStages" ? cbK !== "func" : cbK !== "dyn") {
|
||||
err(`libCall ${e.fn} callback has the wrong kind`, e.loc);
|
||||
}
|
||||
if (!isStreamObject(e.type)) err(`libCall ${e.fn} must return a stream`, e.loc);
|
||||
} else if (e.fn === "stream.composeStages") {
|
||||
if (!isStreamObject(e.type)) err(`libCall ${e.fn} must return a stream`, e.loc);
|
||||
} else if (e.type.kind !== "promise") {
|
||||
err(`libCall ${e.fn} must return a promise`, e.loc);
|
||||
}
|
||||
@@ -5687,6 +5768,8 @@ function validateFunction(
|
||||
e.fn === "readable.opMap" ||
|
||||
e.fn === "readable.opFilter" ||
|
||||
e.fn === "readable.opFlatMap" ||
|
||||
e.fn === "readable.opFlatMapAsyncGen" ||
|
||||
e.fn === "readable.opFlatMapSyncGen" ||
|
||||
e.fn === "readable.opDrop" ||
|
||||
e.fn === "readable.opTake"
|
||||
) {
|
||||
@@ -5695,13 +5778,30 @@ function validateFunction(
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (e.fn === "readable.opReduce") {
|
||||
if (e.fn === "readable.opReduce" || e.fn === "readable.opReduceSignal") {
|
||||
const inner = e.type.kind === "promise" ? e.type.inner : null;
|
||||
const kind = e.args[5]?.kind === "numLit" ? e.args[5].value : -1;
|
||||
const want = [DYN, F64, BOOL, STRING][kind];
|
||||
if (!isStreamObject(e.args[0]?.type) || !inner || !want || !typeEquals(inner, want)) {
|
||||
err(`libCall readable.opReduce must return the promise its kind names`, e.loc);
|
||||
}
|
||||
if (e.fn === "readable.opReduceSignal" && e.args[6]?.type.kind !== "func")
|
||||
err(`libCall ${e.fn} takes ac.abort() as a closure`, e.loc);
|
||||
return true;
|
||||
}
|
||||
if (e.fn === "readable.opOnceOptions") {
|
||||
if (e.args.length !== 0 || e.type.kind !== "dyn")
|
||||
err(`libCall ${e.fn} takes no arguments`, e.loc);
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
e.fn === "readable.opSignalList" ||
|
||||
e.fn === "readable.opSignalOpts" ||
|
||||
e.fn === "readable.opSignalOf"
|
||||
) {
|
||||
// The operator callbacks' { signal }: (dyn) → dyn.
|
||||
if (e.args.length !== 1 || e.args[0]?.type.kind !== "dyn" || e.type.kind !== "dyn")
|
||||
err(`libCall ${e.fn} takes and returns a checked-dynamic value`, e.loc);
|
||||
return true;
|
||||
}
|
||||
if (e.fn === "readable.fromDone") {
|
||||
@@ -5762,6 +5862,9 @@ function validateFunction(
|
||||
e.fn === "readable.setEncoding" ||
|
||||
e.fn === "readable.pushEncoding" ||
|
||||
e.fn === "stream.objectMode" ||
|
||||
e.fn === "stream.decodeStrings" ||
|
||||
e.fn === "writable.writeEncoding" ||
|
||||
e.fn === "writable.setDefaultEncoding" ||
|
||||
e.fn === "stream.destroy" ||
|
||||
e.fn === "stream.destroyErr" ||
|
||||
e.fn === "stream.iteratorClose"
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { contentDigest, NativeCache, openNativeCache } from "./cache.js";
|
||||
import { contentDigest, fileDigest, NativeCache, openNativeCache } from "./cache.js";
|
||||
|
||||
const directories: string[] = [];
|
||||
function directory(): string {
|
||||
@@ -19,11 +19,24 @@ function directory(): string {
|
||||
directories.push(path);
|
||||
return path;
|
||||
}
|
||||
function* generate(parts: readonly string[]): Generator<string> {
|
||||
yield* parts;
|
||||
}
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("a file's digest read in blocks is the digest of its bytes", () => {
|
||||
const path = join(directory(), "module.ll");
|
||||
const bytes = Buffer.alloc(3 * 1024 * 1024 + 17);
|
||||
for (let index = 0; index < bytes.length; index++) bytes[index] = (index * 31) & 0xff;
|
||||
writeFileSync(path, bytes);
|
||||
expect(fileDigest(path)).toBe(contentDigest(bytes));
|
||||
writeFileSync(path, "");
|
||||
expect(fileDigest(path)).toBe(contentDigest(""));
|
||||
});
|
||||
|
||||
test("cache hits require an intact payload and digest", () => {
|
||||
const cache = new NativeCache(directory());
|
||||
const key = contentDigest("program identity");
|
||||
@@ -42,7 +55,8 @@ test("text parts publish one digest-checked entry and oversized reads are misses
|
||||
const cache = new NativeCache(directory());
|
||||
const key = contentDigest("parts identity");
|
||||
const parts = ['{"a":', '"\u00e9é😀"', ",", "", '"b":1}'];
|
||||
cache.writeParts("frontend", key, parts);
|
||||
// Parts are generated while they are written, and hashed in the same pass.
|
||||
cache.writeParts("frontend", key, generate(parts));
|
||||
const bytes = cache.read("frontend", key);
|
||||
expect(bytes?.toString("utf8")).toBe(parts.join(""));
|
||||
expect(readFileSync(join(cache.root, "frontend", key + ".sha256"), "utf8").trim()).toBe(
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
closeSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
openSync,
|
||||
readFileSync,
|
||||
readSync,
|
||||
readdirSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
@@ -11,7 +14,7 @@ import {
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { resolveBuildCacheRoot } from "../backend/cache-root.js";
|
||||
import { writeTextPartsSync } from "./text-file.js";
|
||||
import { writeTextGeneratedSync } from "./text-file.js";
|
||||
|
||||
/** UTF-8 bytes one native compiler string can hold (the runtime's
|
||||
* SCR_STR_MAX_CAP). Exceeding it traps rather than throwing, so payloads
|
||||
@@ -25,6 +28,23 @@ export function contentDigest(value: string | Uint8Array): string {
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
/** contentDigest of a file's bytes, read in 1 MiB blocks so a large file
|
||||
* (a module's LLVM text) is never held whole. */
|
||||
export function fileDigest(path: string): string {
|
||||
const hash = createHash("sha256");
|
||||
const block = Buffer.alloc(1024 * 1024);
|
||||
const fd = openSync(path, "r");
|
||||
try {
|
||||
for (let read = readSync(fd, block, 0, block.length, null); read > 0;) {
|
||||
hash.update(block.subarray(0, read));
|
||||
read = readSync(fd, block, 0, block.length, null);
|
||||
}
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
/** A compiler cache contains private source and executable data. The host
|
||||
* checks ownership and permissions before this class accepts a root. */
|
||||
export class NativeCache {
|
||||
@@ -86,15 +106,15 @@ export class NativeCache {
|
||||
}
|
||||
|
||||
/** Publish text parts without joining them into one string. */
|
||||
writeParts(family: string, key: string, parts: readonly string[]): void {
|
||||
writeParts(family: string, key: string, parts: Generator<string>): void {
|
||||
let stage: string | null = null;
|
||||
try {
|
||||
const directory = join(this.root, family);
|
||||
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
||||
stage = mkdtempSync(join(directory, ".write-"));
|
||||
writeTextPartsSync(join(stage, "payload"), parts);
|
||||
// One pass over the parts: they may be generated while they are written.
|
||||
const hash = createHash("sha256");
|
||||
for (const part of parts) hash.update(part);
|
||||
writeTextGeneratedSync(join(stage, "payload"), parts, (slice) => hash.update(slice));
|
||||
writeFileSync(join(stage, "digest"), hash.digest("hex") + "\n");
|
||||
const path = join(directory, key);
|
||||
renameSync(join(stage, "payload"), path);
|
||||
|
||||
@@ -56,7 +56,7 @@ import { clearFenceEvalCaches } from "../library/fence-eval.js";
|
||||
import { archiveNativeLibrary, localizeNativeLibrary, linkNativeWasmLibrary } from "./library.js";
|
||||
import type { NativeToolchain } from "./toolchain.js";
|
||||
import { evaluateNativeComptime } from "./comptime.js";
|
||||
import { contentDigest, type NativeCache } from "./cache.js";
|
||||
import { contentDigest, fileDigest, type NativeCache } from "./cache.js";
|
||||
import {
|
||||
nativeProfileKey,
|
||||
nativeProfileRefusal,
|
||||
@@ -186,7 +186,7 @@ export class NativeCompiler {
|
||||
key = contentDigest(
|
||||
JSON.stringify({
|
||||
schema: 1,
|
||||
llvm: contentDigest(readFileSync(input)),
|
||||
llvm: fileDigest(input),
|
||||
outputKind,
|
||||
optimization,
|
||||
source,
|
||||
@@ -361,7 +361,8 @@ export class NativeCompiler {
|
||||
const inputDirectory = join(stage, "input");
|
||||
mkdirSync(inputDirectory);
|
||||
const llvmInput = outputKind === "llvm" ? stagedOutput : join(inputDirectory, "program.ll");
|
||||
writeFileSync(llvmInput, llvm);
|
||||
// In slices: the module text is never encoded whole.
|
||||
writeTextPartsSync(llvmInput, [llvm]);
|
||||
timing("llvm-write");
|
||||
if (outputKind === "llvm") {
|
||||
renameSync(stagedOutput, output);
|
||||
@@ -495,7 +496,7 @@ export class NativeCompiler {
|
||||
renameSync(stagedOutput + ".dSYM", output + ".dSYM");
|
||||
}
|
||||
mkdirSync(options.outDir, { recursive: true });
|
||||
writeFileSync(llvmPath, llvm);
|
||||
writeTextPartsSync(llvmPath, [llvm]);
|
||||
let irPath: string | undefined;
|
||||
if (options.emitIr) {
|
||||
irPath = join(options.outDir, `${stem}.ir.json`);
|
||||
@@ -583,7 +584,8 @@ export class NativeCompiler {
|
||||
stage = mkdtempSync(join(dirname(archivePath), ".scriptc-library-"));
|
||||
const llvm = prepared.llvm;
|
||||
const llvmInput = join(stage, "program.ll");
|
||||
writeFileSync(llvmInput, llvm);
|
||||
// In slices: the module text is never encoded whole.
|
||||
writeTextPartsSync(llvmInput, [llvm]);
|
||||
timing("llvm-write");
|
||||
const features = prepared.features;
|
||||
const pack = selectNativeRuntimePack(
|
||||
@@ -646,7 +648,7 @@ export class NativeCompiler {
|
||||
});
|
||||
}
|
||||
timing("native-archive");
|
||||
writeFileSync(llvmPath, llvm);
|
||||
writeTextPartsSync(llvmPath, [llvm]);
|
||||
let irPath: string | undefined;
|
||||
if (options.emitIr) {
|
||||
irPath = join(options.outDir, `${stem}.lib.ir.json`);
|
||||
|
||||
@@ -32,7 +32,7 @@ import type { LibraryProfile } from "../library/library-profile.js";
|
||||
import type { IrModule } from "../ir/ir.js";
|
||||
import { deserializeModuleBytes, serializeModuleParts } from "../ir/serialize.js";
|
||||
import { contentDigest, NATIVE_STRING_BYTES, type NativeCache } from "./cache.js";
|
||||
import { batchTextParts } from "./text-file.js";
|
||||
import { batchTextParts, textSlices } from "./text-file.js";
|
||||
import type { NativeToolchain } from "./toolchain.js";
|
||||
import { nativeFileIdentity } from "./file-identity.js";
|
||||
|
||||
@@ -61,37 +61,57 @@ interface FrontendCacheEntry {
|
||||
ir: string[] | null;
|
||||
}
|
||||
|
||||
function frontendCacheParts(probes: FrontendInputSnapshot, input: NativeExecutableInput): string[] {
|
||||
/** The entry's JSON text, generated as it is written. The LLVM text is
|
||||
* escaped in slices (`textSlices`), the same JSON string as escaping it
|
||||
* whole, so a large module is never copied whole. */
|
||||
function* frontendCacheParts(
|
||||
probes: FrontendInputSnapshot,
|
||||
input: NativeExecutableInput,
|
||||
): Generator<string> {
|
||||
const metadata = JSON.stringify({
|
||||
schema: "scriptc.native-frontend.v3",
|
||||
probes,
|
||||
input: { ...input, llvm: "", ir: null },
|
||||
});
|
||||
const parts = [metadata.slice(0, metadata.length - 1) + ',"llvm":', JSON.stringify(input.llvm)];
|
||||
if (input.ir === null) parts.push(',"ir":null}');
|
||||
yield metadata.slice(0, metadata.length - 1) + ',"llvm":"';
|
||||
for (const slice of textSlices(input.llvm)) yield JSON.stringify(slice).slice(1, -1);
|
||||
yield '"';
|
||||
if (input.ir === null) yield ',"ir":null}';
|
||||
else {
|
||||
parts.push(',"ir":[');
|
||||
yield ',"ir":[';
|
||||
for (let index = 0; index < input.ir.length; index++) {
|
||||
if (index > 0) parts.push(",");
|
||||
parts.push(JSON.stringify(input.ir[index]!));
|
||||
if (index > 0) yield ",";
|
||||
yield JSON.stringify(input.ir[index]!);
|
||||
}
|
||||
parts.push("]}");
|
||||
yield "]}";
|
||||
}
|
||||
}
|
||||
|
||||
/** The parts while their total length stays within `limit`; past it, a
|
||||
* throw abandons the write. */
|
||||
function* boundedParts(parts: Generator<string>, limit: number): Generator<string> {
|
||||
let length = 0;
|
||||
for (const part of parts) {
|
||||
length += part.length;
|
||||
if (length > limit) throw new Error("frontend cache entry exceeds one native string");
|
||||
yield part;
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
/** Publish an entry only when a native reader can decode it as one string;
|
||||
* UTF-16 length never exceeds the UTF-8 byte length. */
|
||||
* UTF-16 length never exceeds the UTF-8 byte length. A longer entry
|
||||
* abandons its write, which writeParts discards. */
|
||||
function publishFrontendCache(
|
||||
cache: NativeCache,
|
||||
key: string,
|
||||
probes: FrontendInputSnapshot,
|
||||
input: NativeExecutableInput,
|
||||
): void {
|
||||
const parts = frontendCacheParts(probes, input);
|
||||
let length = 0;
|
||||
for (const part of parts) length += part.length;
|
||||
if (length <= NATIVE_STRING_BYTES) cache.writeParts("frontend", key, parts);
|
||||
cache.writeParts(
|
||||
"frontend",
|
||||
key,
|
||||
boundedParts(frontendCacheParts(probes, input), NATIVE_STRING_BYTES),
|
||||
);
|
||||
}
|
||||
|
||||
/** The cached input when every frontend input still matches, else the
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { textSlices, writeTextPartsSync } from "./text-file.js";
|
||||
|
||||
const directories: string[] = [];
|
||||
afterEach(() => {
|
||||
for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("text slices keep surrogate pairs whole and escape as the whole text does", () => {
|
||||
const text = 'ab😀cé😀😀\ud800x"\\\n' + "z".repeat(5);
|
||||
for (let length = 2; length <= 8; length++) {
|
||||
const slices = [...textSlices(text, length)];
|
||||
expect(slices.join("")).toBe(text);
|
||||
for (const slice of slices) {
|
||||
expect(slice.length).toBeLessThanOrEqual(length);
|
||||
const last = slice.charCodeAt(slice.length - 1);
|
||||
// A lone high surrogate of the text may end a slice; a pair's never does.
|
||||
if (last >= 0xd800 && last <= 0xdbff) expect(slice.endsWith("\ud800")).toBe(true);
|
||||
}
|
||||
expect(slices.map((slice) => JSON.stringify(slice).slice(1, -1)).join("")).toBe(
|
||||
JSON.stringify(text).slice(1, -1),
|
||||
);
|
||||
}
|
||||
expect([...textSlices("")]).toEqual([]);
|
||||
});
|
||||
|
||||
test("a part larger than one slice is written as its UTF-8 bytes", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-text-file-"));
|
||||
directories.push(dir);
|
||||
// A surrogate pair across the first slice boundary (1 MiB units).
|
||||
const large = "a".repeat(1024 * 1024 - 1) + "😀" + "b".repeat(1024 * 1024 + 3);
|
||||
const path = join(dir, "out.txt");
|
||||
const seen: string[] = [];
|
||||
writeTextPartsSync(path, ["head,", large, "", "tail"], (slice) => seen.push(slice));
|
||||
const expected = Buffer.from("head," + large + "tail", "utf8");
|
||||
expect(readFileSync(path).equals(expected)).toBe(true);
|
||||
expect(seen.join("")).toBe("head," + large + "tail");
|
||||
expect(Math.max(...seen.map((slice) => slice.length))).toBeLessThanOrEqual(1024 * 1024);
|
||||
});
|
||||
@@ -24,17 +24,61 @@ export function batchTextParts(parts: readonly string[]): string[] {
|
||||
return batches;
|
||||
}
|
||||
|
||||
/** Write text parts to `path` in order without joining them first. Each part
|
||||
* is one write; pass batched parts (`batchTextParts`) for many small ones. */
|
||||
export function writeTextPartsSync(path: string, parts: readonly string[]): void {
|
||||
/** The most UTF-16 units of a part that one write encodes. */
|
||||
const TEXT_WRITE_LENGTH = 1024 * 1024;
|
||||
|
||||
/** `text` in slices of at most `length` (2 or more) UTF-16 units. A slice
|
||||
* never ends between the two halves of a surrogate pair, so the slices
|
||||
* encode, as UTF-8 or as JSON string contents, to the encoding of `text`. */
|
||||
export function* textSlices(text: string, length = TEXT_WRITE_LENGTH): Generator<string> {
|
||||
for (let start = 0; start < text.length;) {
|
||||
let end = Math.min(text.length, start + length);
|
||||
if (end < text.length) {
|
||||
const unit = text.charCodeAt(end - 1);
|
||||
if (unit >= 0xd800 && unit <= 0xdbff) end--;
|
||||
}
|
||||
yield text.slice(start, end);
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
|
||||
/** Encodes and writes `part` to `fd` in slices (`textSlices`), so a large
|
||||
* part is never encoded whole. `written` sees each slice after it is
|
||||
* written. */
|
||||
function writeTextPart(fd: number, part: string, written?: (slice: string) => void): void {
|
||||
for (const slice of textSlices(part)) {
|
||||
const bytes = Buffer.from(slice, "utf8");
|
||||
for (let offset = 0; offset < bytes.length;) {
|
||||
offset += writeSync(fd, bytes, offset, bytes.length - offset);
|
||||
}
|
||||
written?.(slice);
|
||||
}
|
||||
}
|
||||
|
||||
/** Write text parts to `path` in order without joining them first; pass
|
||||
* batched parts (`batchTextParts`) for many small ones. */
|
||||
export function writeTextPartsSync(
|
||||
path: string,
|
||||
parts: readonly string[],
|
||||
written?: (slice: string) => void,
|
||||
): void {
|
||||
const fd = openSync(path, "w");
|
||||
try {
|
||||
for (const part of parts) {
|
||||
const bytes = Buffer.from(part, "utf8");
|
||||
for (let offset = 0; offset < bytes.length;) {
|
||||
offset += writeSync(fd, bytes, offset, bytes.length - offset);
|
||||
}
|
||||
}
|
||||
for (const part of parts) writeTextPart(fd, part, written);
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
}
|
||||
|
||||
/** writeTextPartsSync for parts generated while they are written. */
|
||||
export function writeTextGeneratedSync(
|
||||
path: string,
|
||||
parts: Generator<string>,
|
||||
written?: (slice: string) => void,
|
||||
): void {
|
||||
const fd = openSync(path, "w");
|
||||
try {
|
||||
for (const part of parts) writeTextPart(fd, part, written);
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
|
||||
@@ -378,7 +378,7 @@ done:
|
||||
expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]);
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v28");
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v33");
|
||||
|
||||
const clangExe = join(dir, "clang-program");
|
||||
const linkDriver = join(dir, "clang-driver.c");
|
||||
@@ -712,7 +712,7 @@ done:
|
||||
.split("\n");
|
||||
expect(undefinedSymbols).toContain("_sf_scale");
|
||||
expect(undefinedSymbols).toContain("_sf_callback_mix");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v28");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v33");
|
||||
});
|
||||
|
||||
test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6913-builtin-constructor-shorthand.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6914-dynamic-builtin-construct.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6915-dynamic-builtin-construct.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6916-local-class-static-self-reference.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6917-local-class-expression-self-reference.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6918-local-class-expression-self-reference.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6919-local-class-inspect-name.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6921-local-subclass-override-dispatch.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6922-local-subclass-override-dispatch.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7014-js-never-array-literal-fields/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7020-conditional-spread-fields.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7021-promise-all-uniform-tuple.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7022-promise-all-dynamic-arrays.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7023-union-object-patterns.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user