diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 4f152e4bb..e177e36c0 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -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` 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 | diff --git a/benchmarks/runtime/workloads/dynamic-methods.json b/benchmarks/runtime/workloads/dynamic-methods.json deleted file mode 100644 index fa27f4ea8..000000000 --- a/benchmarks/runtime/workloads/dynamic-methods.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "entry": "workloads/dynamic-methods.mjs", - "args": [ - "300000" - ] -} diff --git a/benchmarks/runtime/workloads/dynamic-methods.mjs b/benchmarks/runtime/workloads/dynamic-methods.mjs deleted file mode 100644 index bbf217701..000000000 --- a/benchmarks/runtime/workloads/dynamic-methods.mjs +++ /dev/null @@ -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); diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index 954b040db..db1193b98 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -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. diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index e132f27da..a86ed217e 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -24,6 +24,8 @@ title: "Limitations" ../limitations/language-features/async-iteration.mdx +../limitations/language-features/iterable-values.mdx + ../limitations/language-features/generics.mdx ../limitations/language-features/function-values.mdx diff --git a/docs/content/limitations/builtin-apis/prototypes.mdx b/docs/content/limitations/builtin-apis/prototypes.mdx index 3e0b5789d..170b11793 100644 --- a/docs/content/limitations/builtin-apis/prototypes.mdx +++ b/docs/content/limitations/builtin-apis/prototypes.mdx @@ -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. diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index 8461cc71c..fe6bb2dfb 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -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. diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index c1bab6680..0fa62e558 100644 --- a/docs/content/limitations/builtin-function-values.mdx +++ b/docs/content/limitations/builtin-function-values.mdx @@ -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. diff --git a/docs/content/limitations/language-features/iterable-values.mdx b/docs/content/limitations/language-features/iterable-values.mdx new file mode 100644 index 000000000..2feedc02f --- /dev/null +++ b/docs/content/limitations/language-features/iterable-values.mdx @@ -0,0 +1,2 @@ +- **Iterable values:** A value typed `Iterable` can hold an array, a Set, a Map (for `Iterable<[K, V]>`), a string (for `Iterable`), 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` and are reported at the conversion. diff --git a/docs/content/limitations/record-identity-and-property-order.mdx b/docs/content/limitations/record-identity-and-property-order.mdx index 0381803f6..cc16f74d5 100644 --- a/docs/content/limitations/record-identity-and-property-order.mdx +++ b/docs/content/limitations/record-identity-and-property-order.mdx @@ -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. diff --git a/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx b/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx index 173bdc0c6..de24038cc 100644 --- a/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx +++ b/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx @@ -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. diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index a1b673630..bf5b3be14 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -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"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 5f3805813..b195c3fbb 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -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 { + decodeStrings: false; + write?( + this: Writable, + chunk: string | Buffer, + encoding: string, + callback: (error?: Error | null) => void, + ): void; + } + type StringDuplexOptions = Omit & { + decodeStrings: false; + write?( + this: Duplex, + chunk: string | Buffer, + encoding: string, + callback: (error?: Error | null) => void, + ): void; + }; + type StringTransformOptions = Omit & { + 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; iterator(options?: { destroyOnReturn?: boolean }): AsyncIterableIterator; + /* 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(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): Duplex; + /* pipeline(...stages, callback): the last stream, or the object-mode + * PassThrough that a function destination feeds. */ + type PipelineResult = T extends [ + ...unknown[], + infer D, + (...args: any[]) => unknown, + ] + ? D extends (...args: any[]) => unknown + ? PassThrough + : D + : unknown; + function pipeline(...streams: T): PipelineResult; + /* 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 + | AsyncIterable + | 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[], (...args: any[]) => Promise, ] ? Promise

- : Promise; + : T extends [ + ...unknown[], + (...args: any[]) => Promise, + { signal?: AbortSignal; end?: boolean }, + ] + ? Promise

+ : Promise; namespace promises { function pipeline(...streams: T): PipelinePromise; 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( - ...streams: T - ): T extends [...unknown[], (...args: any[]) => Promise] ? Promise

: Promise; + function pipeline(...streams: T): import("stream").PipelinePromise; function finished( stream: unknown, options?: { diff --git a/packages/compiler/src/backend/llvm/common.ts b/packages/compiler/src/backend/llvm/common.ts index 822ee29e2..ac4e28708 100644 --- a/packages/compiler/src/backend/llvm/common.ts +++ b/packages/compiler/src/backend/llvm/common.ts @@ -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( diff --git a/packages/compiler/src/backend/llvm/dense-array-access.test.ts b/packages/compiler/src/backend/llvm/dense-array-access.test.ts index 1b84cf0a0..6a0123559 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.test.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.test.ts @@ -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 }; diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index 5d7994d42..9158d12cd 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -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; } diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 594634893..04e3b9817 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -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)) { diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index fa5df1f38..156ad8b8d 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -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(``); diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 74736cd1a..913c6e2c9 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -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 diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index f34345ed0..ef512dddd 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -832,10 +832,6 @@ export class LlEmitter { readonly cstrs = new Map(); /** Interned ScrMapOps descriptors (scr_map.h): initializer → symbol. */ readonly mapOpsConsts = new Map(); - /** 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(); /** 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(); readonly dynPromiseAdapters = new Map(); @@ -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); } diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 434435eae..9af677586 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -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(); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.test.ts b/packages/compiler/src/backend/llvm/expr-primitives.test.ts index 6eb9be8f7..f343c956e 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.test.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.test.ts @@ -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, + ); }, ); diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts index 5d8f5f26f..244b3afea 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts @@ -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 diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 7cb0fa772..ceaca3778 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -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 }`, + ``, + ); +} + +/** `@_rops` for a shape with orderVariants: a record-ops table + * whose `select` hook answers the `_o_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 }`, ``, ); } diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 42c803697..14a2e3432 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -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); diff --git a/packages/compiler/src/backend/llvm/js-ic.ts b/packages/compiler/src/backend/llvm/js-ic.ts index 7516673cd..45c18b803 100644 --- a/packages/compiler/src/backend/llvm/js-ic.ts +++ b/packages/compiler/src/backend/llvm/js-ic.ts @@ -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; diff --git a/packages/compiler/src/backend/llvm/key-orders.ts b/packages/compiler/src/backend/llvm/key-orders.ts new file mode 100644 index 000000000..9dbda1414 --- /dev/null +++ b/packages/compiler/src/backend/llvm/key-orders.ts @@ -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); +} diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 24f2bf019..10d83057f 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -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") ) diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index d0888ccbe..0b6041034 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1126,11 +1126,24 @@ export const LIB_FN_SYMS: Record = { // 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 = { "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 = { "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([ "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([ "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([ "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([ "sp.finished", "sp.pipeline", "sp.pipelineStages", + "sp.pipelineStagesOpts", "stream.pipelineStages", "stream.pipelineStagesDyn", + "stream.composeStages", "sp.pipelineOpts", "sp.pipelineOptsSignal", "sp.finishedOpts", diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index 6d6c6f99c..5fd4af1a9 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -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 = { + const kindOf: Record = { "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 = { "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( diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts index 044cb79ae..331ea2199 100644 --- a/packages/compiler/src/backend/llvm/walkers.ts +++ b/packages/compiler/src/backend/llvm/walkers.ts @@ -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); } diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 714bc09a5..1b90bb1f1 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -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. */ diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index a616dbec2..7d3dcbc02 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -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; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index c2536ede8..532051b88 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -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: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 33b1ca38e..18b04b9a6 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -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", diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts index af7270ea9..2e0f305a0 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 75e527aa5..129ac73a3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -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 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, + }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts index 2a77fa85c..8619b4997 100644 --- a/packages/compiler/src/frontend/lowering/builtins/url.ts +++ b/packages/compiler/src/frontend/lowering/builtins/url.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index af3212ef0..8a4a1e417 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -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")) diff --git a/packages/compiler/src/frontend/lowering/class-iterables.ts b/packages/compiler/src/frontend/lowering/class-iterables.ts index 8c58fc0bd..0f4d6c254 100644 --- a/packages/compiler/src/frontend/lowering/class-iterables.ts +++ b/packages/compiler/src/frontend/lowering/class-iterables.ts @@ -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 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 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; diff --git a/packages/compiler/src/frontend/lowering/class-runtime-statics.ts b/packages/compiler/src/frontend/lowering/class-runtime-statics.ts index 310c8f389..707c916ec 100644 --- a/packages/compiler/src/frontend/lowering/class-runtime-statics.ts +++ b/packages/compiler/src/frontend/lowering/class-runtime-statics.ts @@ -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 }, diff --git a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts index 53b0748b4..f42663a13 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts @@ -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 { diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 5b5e283b3..c3b4ad906 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -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"); diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index dcadf3bf3..86f214bc1 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/containers/array-concat.ts b/packages/compiler/src/frontend/lowering/containers/array-concat.ts index b4e285ee9..a7ce0a106 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-concat.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-concat.ts @@ -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[] = [ diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index 180f40435..2f839a20b 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -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, }); } diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index fd4420b69..995cb177a 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1414,7 +1414,8 @@ const BUF_ENCODINGS: Record = { * 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 diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index d3169ec5c..e043daf68 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -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 }; diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index c971c672d..87169539f 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -727,6 +727,11 @@ function keyOrderFinding( written: readonly string[], assignedNames: ReadonlySet, ): { 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); diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index cb2d86804..6c7ce814e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -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`; `{ 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 diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 3ca1471ae..045bebdf4 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -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' diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 1f74cea42..eca3c33f5 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -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)) { diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index 786f3a337..acac99610 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -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`: 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( diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 678b375fe..67130b0a6 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index 9500d6d2d..5ede3efe3 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -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 returns a generator over T. + if (mappedReturn && decl.asteriskToken) + mappedReturn = iterableGeneratorArm(lowerer, mappedReturn); if ( mappedReturn === null && decl.asteriskToken && diff --git a/packages/compiler/src/frontend/lowering/iterable-values.ts b/packages/compiler/src/frontend/lowering/iterable-values.ts new file mode 100644 index 000000000..78de9720c --- /dev/null +++ b/packages/compiler/src/frontend/lowering/iterable-values.ts @@ -0,0 +1,342 @@ +/* `Iterable` 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 | 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 | 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 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 = ; + * if (%src is the array arm) { + * const %arr = ; + * for (let %cur = 0; %cur < %arr.length; %cur++) { const x = %arr[%cur]; } + * } else { + * for (const x of GetIterator(%src)) // 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 | Generator`) 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(); + } +} diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts index 8632bad47..41398a1de 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -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" }, + }); }); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index 4adfb87ab..9cbbd34a2 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -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(); + 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>(); + /** 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(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(); + 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(); + 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[] => { + 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(); + /** The statements a spread literal's sequence evaluates first: the + * spread value's binding, then its fields. */ + const prologues = new Map(); + 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(); + const dropped = new Set(); + 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>(); + 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; + } } diff --git a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts index 5b427a750..95916ee9a 100644 --- a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts +++ b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts @@ -359,13 +359,15 @@ function boolRef(localId: string, loc: ReturnType): 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, ): 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 }; } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 2666e106d..02fd008a8 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -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); diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d8ccc15b7..b23697cab 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -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, +): 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 = { diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index b5acaf9e6..ac8b02b69 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -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 | undefined` values, calls // of `(...) => Promise | 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": diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index b4e3ac102..80463fa6c 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -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, }, ], diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 47f490c7b..57f16bcc7 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -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))), ); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 02e585aaf..ea5ef72ac 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -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 = { + 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 }[]; diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 6540b3a99..8309d69a9 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -56,6 +56,7 @@ import { bindingNeverReassigned } from "./binding-analysis.js"; import { extractIteratorValue, genStep, genValue, stepDrivable } from "./lower-generators.js"; import { LiftedFrame, driveGenerator } from "./containers/iterator-helpers.js"; import { lowerArrayIteratorValue, lowerStringIteratorValue } from "./iterator-adapters.js"; +import { staticReadableStreamElementType } from "./lower-stdlib.js"; import { BOOL, DYN, @@ -176,10 +177,25 @@ export const UNDERSCORE_METHODS: ReadonlyMap = new Map([ ["flush", "_flush"], ]); +/** The option callbacks applied after construction rather than through + * the constructor's callback slots (option name → method name): construct + * on every stream class, and writev on the writable classes whose + * machinery hands it batches (Writable and Duplex). */ +export const STREAM_EXTRA_CALLBACKS: ReadonlyMap = new Map([ + ["construct", "_construct"], + ["writev", "_writev"], +]); + +export function streamExtraCallbacks(cls: string): readonly string[] { + return cls === "%Writable" || cls === "%Duplex" ? ["construct", "writev"] : ["construct"]; +} + /** The completion callback's parameter position in each underscore * method that takes one (cbTuples' "fn" positions). */ export const STREAM_COMPLETION_CALLBACK_INDEX: ReadonlyMap = new Map([ ["_write", 2], + ["_writev", 1], + ["_construct", 0], ["_final", 0], ["_destroy", 1], ["_transform", 2], @@ -317,9 +333,11 @@ function lowerStreamCallback( // (`string | null | undefined`) converts from the encoding argument's // value. if ( - ((i === 0 && (chunkValues || widensInto(lowerer, fullTuple[0]!, funcType.params[0]!, loc))) || + ((((i === 0 && + (chunkValues || widensInto(lowerer, fullTuple[0]!, funcType.params[0]!, loc))) || i === 1) && - (which === "write" || which === "transform") && + (which === "write" || which === "transform")) || + (i === 0 && which === "writev")) && funcType.params[i]!.kind !== "func" && canConvertToDyn( funcType.params[i]!, @@ -429,6 +447,16 @@ interface StreamOptions { * right after construction. */ readableObject: boolean; writableObject: boolean; + /** decodeStrings: false (string writes stay strings), applied with + * stream.decodeStrings right after construction. */ + decodeStringsOff: boolean; + /** A writable side's non-utf8 `defaultEncoding` as written, applied + * with writable.writeEncoding. */ + writeEncoding: string | null; + /** The construct and writev callbacks (options or the class's + * _construct/_writev), applied with stream.setWritev and + * stream.setConstruct right after construction. */ + extras: { name: string; cb: IrExpr }[]; /** Present callbacks in canonical order with their flag bits. */ flags: number; cbs: IrExpr[]; @@ -470,14 +498,19 @@ function streamObjectModes( return out; } -/** Whether an options literal sets the writable side's object mode with a - * `true` literal (objectMode, or writableObjectMode on a duplex stream). - * Reports nothing: streamObjectModes fences the other value forms. */ +/** Whether an options literal makes the writable side hold values: its + * object mode set with a `true` literal (objectMode, or writableObjectMode + * on a duplex stream), or decodeStrings: false (strings stay strings). + * Reports nothing: the options parse fences the other value forms. */ function literalWritesValues(opts: ts.ObjectLiteralExpression, duplexish: boolean): boolean { for (const prop of opts.properties?.nodes ?? []) { if (!ts.isPropertyAssignment(prop)) continue; const name = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : null; + if (name === "decodeStrings") { + if (prop.initializer.kind !== ts.SyntaxKind.TrueKeyword) return true; + continue; + } if (name !== "objectMode" && !(duplexish && name === "writableObjectMode")) continue; if (prop.initializer.kind !== ts.SyntaxKind.FalseKeyword) return true; } @@ -494,6 +527,8 @@ function cbTuples(lowerer: Lowerer): Record { read: [F64], write: [BYTES, STRING, "fn"], final: ["fn"], + construct: ["fn"], + writev: [DYN, "fn"], destroy: [errorOrNull(lowerer), "fn"], transform: [BYTES, STRING, "fn"], flush: ["fn"], @@ -734,9 +769,14 @@ function parseStreamOptions( signal: null, readableObject: false, writableObject: false, + decodeStringsOff: false, + writeEncoding: null, + extras: [], flags: 0, cbs: [], }; + const extraNames = streamExtraCallbacks(`%${ctorName}`); + const writableClass = ctorName !== "Readable"; const followed = followOptionsArg(lowerer, arg); const optsLit = followed === null ? undefined : followed; if (optsLit !== undefined && !ts.isObjectLiteralExpression(optsLit)) { @@ -756,6 +796,12 @@ function parseStreamOptions( ); out.readableObject = objectModes.readable; out.writableObject = objectModes.writable; + // the chunk an option callback receives can be another value than a + // Buffer (literalWritesValues), read before any callback lowers + const writesValues = + optsLit !== undefined && + ts.isObjectLiteralExpression(optsLit) && + literalWritesValues(optsLit, ctorName !== "Readable" && ctorName !== "Writable"); if (out.readableObject) out.hwmR = numLit(16, loc); if (out.writableObject) out.hwmW = numLit(16, loc); const tuples = cbTuples(lowerer); @@ -772,7 +818,7 @@ function parseStreamOptions( lowerer.unsupported("SC1090", prop, `computed or spread members in ${ctorName} options`); } // Callback members: shorthand methods and function-valued properties. - if (accepted.includes(name)) { + if (accepted.includes(name) || extraNames.includes(name)) { let fnNode: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction | null = null; if (ts.isMethodDeclaration(prop)) fnNode = prop; else if ( @@ -823,7 +869,7 @@ function parseStreamOptions( }); present.set( name, - lowerStreamCallback(lowerer, name, fnNode, thisType, tuple, out.writableObject), + lowerStreamCallback(lowerer, name, fnNode, thisType, tuple, writesValues), ); } finally { if (pinned) lowerer.stageSourceTypes.delete(pinned); @@ -872,11 +918,17 @@ function parseStreamOptions( // read up front (streamObjectModes) break; case "decodeStrings": { + // WritableState's `options.decodeStrings === false`; a Readable + // ignores the option if (value.kind === ts.SyntaxKind.TrueKeyword) break; // Node's default + if (value.kind === ts.SyntaxKind.FalseKeyword) { + if (writableClass) out.decodeStringsOff = true; + break; + } lowerer.noLowering( - `${ctorName} with decodeStrings: false`, + `${ctorName} with a runtime 'decodeStrings' value`, prop, - "chunks always decode to Buffers here (Node's default)", + "write 'decodeStrings: true' or 'decodeStrings: false'", ); break; } @@ -911,15 +963,20 @@ function parseStreamOptions( out.badEncoding = t.value; break; } - if (t.isStringLiteralType() && ctorName === "Readable") { - out.pushEncoding = knownBufEncoding(t.value)!; + if (t.isStringLiteralType()) { + // the readable side decodes push(string) with it, and the + // writable side encodes write(string) with it (the name as + // written reaches _write when decodeStrings is false) + if (ctorName !== "Writable" && !out.readableObject) + out.pushEncoding = knownBufEncoding(t.value)!; + if (writableClass) out.writeEncoding = t.value; break; } } lowerer.noLowering( - `the ${ctorName} option 'defaultEncoding'`, + `the ${ctorName} option 'defaultEncoding' with a runtime value`, prop, - "utf8 (the default) is the supported write-side string encoding", + "write the encoding as a string literal", ); break; case "signal": { @@ -980,18 +1037,13 @@ function parseStreamOptions( ); break; } - case "construct": - lowerer.noLowering( - `the ${ctorName} option 'construct'`, - prop, - "deferred construction (_construct) is not lowered yet", - ); - break; case "writev": lowerer.noLowering( `the ${ctorName} option 'writev'`, prop, - "batched writes (_writev) are not lowered yet — writes deliver one chunk at a time", + ctorName === "Readable" + ? "a Readable has no writable side" + : "batched writes on a Transform would bypass its transform — writev is supported on Writable and Duplex", ); break; default: @@ -1008,6 +1060,10 @@ function parseStreamOptions( out.cbs.push(cb); } } + for (const name of extraNames) { + const cb = present.get(name) ?? fallbackCb?.(name) ?? null; + if (cb) out.extras.push({ name, cb }); + } return out; } @@ -1029,6 +1085,9 @@ function streamCtorArgs( signal: IrExpr | null; readableObject: boolean; writableObject: boolean; + decodeStringsOff: boolean; + writeEncoding: string | null; + extras: { name: string; cb: IrExpr }[]; } { const shape = streamCtorShape(cls); const o = parseStreamOptions( @@ -1060,9 +1119,72 @@ function streamCtorArgs( signal: o.signal, readableObject: o.readableObject, writableObject: o.writableObject, + decodeStringsOff: o.decodeStringsOff, + writeEncoding: o.writeEncoding, + extras: o.extras, }; } +/** The follow-ups a stream's options apply over the fresh stream `recv` + * after the chained ones: decodeStrings, the writable defaultEncoding, + * then writev and construct (Node's constructors schedule construct + * last). */ +function streamOptionFollowUps( + parsed: { + decodeStringsOff: boolean; + writeEncoding: string | null; + extras: { name: string; cb: IrExpr }[]; + }, + recv: () => IrExpr, + thisType: IrType, + loc: SrcLoc, + writevFn: IrLibFn = "stream.setWritev", +): IrStmt[] { + const out: IrStmt[] = []; + if (parsed.decodeStringsOff) { + out.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "stream.decodeStrings", + args: [recv(), boolLit(false, loc)], + type: thisType, + loc, + }, + loc, + }); + } + if (parsed.writeEncoding !== null) { + out.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "writable.writeEncoding", + args: [recv(), strLit(parsed.writeEncoding, loc)], + type: thisType, + loc, + }, + loc, + }); + } + for (const name of ["writev", "construct"]) { + const extra = parsed.extras.find((x) => x.name === name); + if (!extra) continue; + out.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: name === "writev" ? writevFn : "stream.setConstruct", + args: [recv(), extra.cb], + type: VOID, + loc, + }, + loc, + }); + } + return out; +} + /** `new Readable(opts?)` and friends — called from lowerNew once the * callee resolved to a builtin stream class. */ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: ClassInfo): IrExpr { @@ -1137,12 +1259,29 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C loc, }; } - if (parsed.signal === null) return built; - // { signal }: addAbortSignal over the fresh stream (answers it +1). + if (parsed.signal !== null) { + // { signal }: addAbortSignal over the fresh stream (answers it +1). + built = { + kind: "libCall", + fn: "stream.addAbortSignal", + args: [built, parsed.signal], + type: thisType, + loc, + }; + } + if (!parsed.decodeStringsOff && parsed.writeEncoding === null && parsed.extras.length === 0) + return built; + const fresh = lowerer.declareHiddenLocal("%stream", thisType); + const followUps = streamOptionFollowUps( + parsed, + () => varRef(fresh.id, thisType, loc), + thisType, + loc, + ); return { - kind: "libCall", - fn: "stream.addAbortSignal", - args: [built, parsed.signal], + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: fresh.id, init: built, loc }, ...followUps], + result: varRef(fresh.id, thisType, loc), type: thisType, loc, }; @@ -1236,10 +1375,12 @@ function streamMethodWrapper( } else if (!typeEquals(got.type, want)) { // A write or transform encoding of another type (`string | null | // undefined`) converts from the encoding argument's value: an - // object-mode write passes undefined, null or the name. + // object-mode write passes undefined, null or the name. A chunk of + // another type (`string | Buffer` under decodeStrings: false) + // converts from the chunk's value. if ( - i === 1 && - (which === "write" || which === "transform") && + (i === 1 || (i === 0 && (chunkValues || which === "writev"))) && + (which === "write" || which === "transform" || which === "writev") && got.type.kind !== "func" && canConvertToDyn( got.type, @@ -1262,8 +1403,7 @@ function streamMethodWrapper( // or throws a TypeError there; a byte-mode side's chunk is a Buffer. if ( i === 0 && - chunkValues && - (which === "write" || which === "transform") && + (which === "writev" || (chunkValues && (which === "write" || which === "transform"))) && got.type.kind !== "func" && canConvertToDyn( got.type, @@ -1380,7 +1520,7 @@ export function lowerStreamSuperCall( (!ts.isObjectLiteralExpression(optsNode) || literalWritesValues(optsNode, cls !== "%Readable" && cls !== "%Writable")); const fallback = (name: string): IrExpr | null => { - const methodName = UNDERSCORE_METHODS.get(name); + const methodName = UNDERSCORE_METHODS.get(name) ?? STREAM_EXTRA_CALLBACKS.get(name); return methodName ? streamMethodWrapper(lowerer, info, name, methodName, thisType, loc, blame, chunkValues) : null; @@ -1401,6 +1541,13 @@ export function lowerStreamSuperCall( cbs.push(cb); } } + // The class's _writev and _construct ride the same way: an options + // writev or construct (applied by initDyn) wins. + const extras: { name: string; cb: IrExpr }[] = []; + for (const name of streamExtraCallbacks(cls)) { + const cb = fallback(name); + if (cb) extras.push({ name, cb }); + } return [ { kind: "exprStmt", @@ -1413,6 +1560,13 @@ export function lowerStreamSuperCall( }, loc, }, + ...streamOptionFollowUps( + { decodeStringsOff: false, writeEncoding: null, extras }, + thisRef, + thisType, + loc, + "stream.setWritevDefault", + ), ]; } } @@ -1499,6 +1653,7 @@ export function lowerStreamSuperCall( loc, }); } + out.push(...streamOptionFollowUps(parsed, thisRef, thisType, loc)); return out; } @@ -1542,18 +1697,18 @@ export function lowerStreamUnderscoreAssign( const loc = locOf(expr); const cls = base.def.name; const clsName = cls.slice(1); - if (methodName === "_writev" || methodName === "_construct") { + if (methodName === "_construct") { + // Node's constructor reads _construct once; a later assignment has no + // effect there lowerer.noLowering( `assigning '${methodName}' on a ${clsName}`, expr.left, - methodName === "_writev" - ? "batched writes (_writev) are not lowered yet — writes deliver one chunk at a time" - : "deferred construction (_construct) is not lowered yet", + "declare _construct on the class or pass the construct option", ); } - const which = optionByMethod.get(methodName)!; + const which = methodName === "_writev" ? "writev" : optionByMethod.get(methodName)!; const { accepted } = streamCtorShape(cls); - if (!accepted.includes(which)) { + if (!accepted.includes(which) && !streamExtraCallbacks(cls).includes(which)) { lowerer.noLowering( `assigning '${methodName}' on a ${clsName}`, expr.left, @@ -1589,6 +1744,7 @@ export function lowerStreamUnderscoreAssign( destroy: "stream.setDestroy", transform: "stream.setTransform", flush: "stream.setFlush", + writev: "stream.setWritev", }; return { kind: "exprStmt", @@ -1658,11 +1814,27 @@ export function lowerStreamStaticCall( } } } + if (member === "fromWeb" && cls === "%Readable") return lowerReadableFromWeb(lowerer, call, loc); if (member !== "from") { lowerer.noLowering(`${cls.slice(1)}.${member}`, call); } + if (cls === "%Duplex") { + const fromArgs = call.arguments.nodes; + if (fromArgs.length !== 1) { + lowerer.noLowering( + `Duplex.from with ${fromArgs.length} arguments`, + call, + "Duplex.from takes one body", + ); + } + return duplexFromBody(lowerer, fromArgs[0]!, locOf(call)); + } if (cls !== "%Readable") { - lowerer.noLowering(`${cls.slice(1)}.from`, call, "Readable.from is the lowered form"); + lowerer.noLowering( + `${cls.slice(1)}.from`, + call, + "Readable.from and Duplex.from are the lowered forms", + ); } const args = call.arguments.nodes; if (args.length > 2) { @@ -1672,16 +1844,184 @@ export function lowerStreamStaticCall( "Readable.from(iterable, options) is the supported form", ); } - const type: IrType = { kind: "object", className: "%Readable" }; + return lowerFromSource(lowerer, call, args[0], args[1], false); +} + +/** Readable.fromWeb(readableStream, options): Node's adapter over the web + * stream's reader (scr_stream_from_web.c). The options are an object + * literal: objectMode a boolean literal, highWaterMark a number, encoding + * a literal name, and signal a variable or property read, applied with + * addAbortSignal as the constructors do. */ +function lowerReadableFromWeb(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr { + const args = call.arguments.nodes; + const readable: IrType = { kind: "object", className: "%Readable" }; + if (args.length === 0 || args.length > 2 || args.some((a) => ts.isSpreadElement(a))) { + lowerer.noLowering( + `Readable.fromWeb with ${args.length} arguments`, + call, + "the supported form is Readable.fromWeb(readableStream, options)", + ); + } + const web = lowerer.lowerExprExpecting(args[0]!, DYN); + let objectMode = false; + let hwm: IrExpr = numLit(-1, loc); + let encoding = ""; + let signal: IrExpr | null = null; + const options = args[1]; + if (options !== undefined) { + if (!ts.isObjectLiteralExpression(options)) { + lowerer.noLowering( + "Readable.fromWeb options that are not an object literal", + options, + "pass { objectMode, highWaterMark, encoding, signal } as an object literal", + ); + } + for (const p of options.properties.nodes) { + const key = + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + ts.isIdentifier(p.name) + ? p.name.text + : null; + const value = + key === null + ? null + : ts.isShorthandPropertyAssignment(p) + ? (p.name as ts.Identifier) + : (p as ts.PropertyAssignment).initializer; + if (key === "objectMode" && value) { + if (value.kind === ts.SyntaxKind.TrueKeyword) objectMode = true; + else if (value.kind !== ts.SyntaxKind.FalseKeyword) + lowerer.noLowering( + "the Readable.fromWeb option 'objectMode' with this value", + p, + "write true or false", + ); + continue; + } + if (key === "highWaterMark" && value) { + hwm = lowerer.lowerExprExpecting(value, F64); + continue; + } + if (key === "encoding" && value) { + const t = lowerer.typeOf(value); + if (t.isStringLiteralType() && knownBufEncoding(t.value) === undefined) { + return nodeThrowExpr( + 1, + "ERR_INVALID_ARG_VALUE", + `The property 'options.encoding' is invalid. Received '${t.value}'`, + readable, + loc, + ); + } + encoding = bufEncoding(lowerer, "the Readable.fromWeb option 'encoding'", value); + continue; + } + if (key === "signal" && value) { + if (!sideEffectFreeOptionValue(value) && !pureRead(value)) { + lowerer.noLowering( + "the Readable.fromWeb option 'signal' with this value", + p, + "pass a variable or property read (const signal = controller.signal)", + ); + } + signal = lowerer.lowerExprExpecting(value, DYN); + continue; + } + lowerer.noLowering( + key === null + ? "Readable.fromWeb options with this member" + : `the Readable.fromWeb option '${key}'`, + p, + "objectMode, highWaterMark, encoding and signal are the Readable.fromWeb options", + ); + } + } + const built: IrExpr = { + kind: "libCall", + fn: "fetch.readableFromWeb", + args: [web, boolLit(objectMode, loc), hwm, strLit(encoding, loc)], + type: readable, + loc, + }; + if (signal === null) return built; + return { + kind: "libCall", + fn: "stream.addAbortSignal", + args: [built, signal], + type: readable, + loc, + }; +} + +/** Readable.from(source, options), and for Duplex.from over an iterable + * (duplex) duplexify.js's from(Duplexify, body, { objectMode: true, + * writable: false }): the same builders with SCR_FROM_DUPLEX, answering a + * Duplexify without a writable side. */ +function lowerFromSource( + lowerer: Lowerer, + at: ts.Node, + srcNode: ts.Expression | undefined, + optsNode: ts.Expression | undefined, + duplex: boolean, +): IrExpr { + const loc = locOf(at); + const call = at; + const type: IrType = { kind: "object", className: duplex ? "%Duplex" : "%Readable" }; + const what = duplex ? "Duplex.from" : "Readable.from"; + // SCR_FROM_DUPLEX + const duplexFlag = duplex ? 32 : 0; const js = isJsSourceFile(ts.getSourceFileOfNode(call)); - const srcNode = args[0]; - const optsNode = args[1]; + // A web ReadableStream is async iterable: from.js reads its iterator. + // Duplex.from takes _duplexify({ readable: Readable.fromWeb(body) }). + const webElem = srcNode ? staticReadableStreamElementType(lowerer, srcNode) : null; + if (srcNode && webElem !== null && duplex) { + const fromWeb: IrExpr = { + kind: "libCall", + fn: "fetch.readableFromWeb", + args: [ + lowerer.lowerExprExpecting(srcNode, DYN), + boolLit(false, loc), + numLit(-1, loc), + strLit("", loc), + ], + type: { kind: "object", className: "%Readable" }, + loc, + }; + return { kind: "libCall", fn: "stream.duplexifyReadable", args: [fromWeb], type, loc }; + } + if (srcNode && webElem !== null) { + const source = lowerWebStreamSource(lowerer, srcNode, webElem, loc); + const helpers = source ? fromGenHelpers(lowerer, source.out, loc) : null; + const opts = optsNode ? fromOptions(lowerer, optsNode) : { flags: 0, hwm: null }; + if (!source || !helpers || !opts || !fromGenChunk(lowerer, webElem)) { + lowerer.noLowering( + `Readable.from over a web ReadableStream of '${lowerer.fmt(webElem)}'`, + srcNode, + ); + } + const hwm = opts!.hwm ? lowerer.lowerExprExpecting(opts!.hwm, F64) : numLit(0, loc); + const gen: IrExpr = { + kind: "callValue", + callee: source!.closure, + args: [], + type: source!.out, + loc, + }; + return { + kind: "libCall", + fn: "readable.fromAsyncGen", + args: [gen, ...helpers!, numLit(opts!.flags, loc), hwm], + type, + loc, + }; + } const srcT = srcNode ? lowerer.mapTypeOf(lowerer.typeOf(srcNode)) : null; const typedElem = (t: IrType | null | undefined): "string" | "bytes" | null => t?.kind === "string" ? "string" : t?.kind === "bytes" && t.elem === "u8" ? "bytes" : null; const single = typedElem(srcT); const elem = srcT?.kind === "array" ? typedElem(srcT.elem) : null; - const opts = optsNode ? fromOptions(lowerer, optsNode) : { flags: 0, hwm: null }; + const opts0 = optsNode ? fromOptions(lowerer, optsNode) : { flags: 0, hwm: null }; + const opts = opts0 ? { ...opts0, flags: opts0.flags | duplexFlag } : opts0; // A class whose [Symbol.iterator]() or [Symbol.asyncIterator]() is a // generator method: from.js calls the method when the stream is built, // then reads the generator like any other. @@ -1779,6 +2119,7 @@ export function lowerStreamStaticCall( // The checked-dynamic form: the runtime takes from.js's ladder // (ERR_INVALID_ARG_TYPE for a non-iterable, the options, null values). const src = srcNode ? lowerer.lowerExprExpecting(srcNode, DYN) : dynUndefinedExpr(loc); + if (duplex) return { kind: "libCall", fn: "stream.duplexFromDyn", args: [src], type, loc }; const o = lowerOperatorValue(lowerer, optsNode, "Readable.from options", loc, true); return { kind: "libCall", fn: "readable.fromDyn", args: [src, o], type, loc }; } @@ -1790,7 +2131,7 @@ export function lowerStreamStaticCall( ); } lowerer.noLowering( - `Readable.from over a '${srcT ? lowerer.fmt(srcT) : "?"}' source`, + `${what} over a '${srcT ? lowerer.fmt(srcT) : "?"}' source`, srcNode ?? call, "the supported sources are a string or Buffer, and arrays, Sets, Maps, generators and class iterables of values that convert to checked-dynamic values", ); @@ -2607,22 +2948,28 @@ export function lowerStreamModuleCall( : null; const stageArgs = opts ? args.slice(0, -1) : args; if (hasPipelineFunctionStage(lowerer, stageArgs)) { - if (opts) { - lowerer.noLowering( - "pipeline with function stages or iterable sources and options", - lastArg, - "pipeline(source, async function* (source) { ... }, destination) without options is supported", - ); - } const staged = lowerPipelineStages(lowerer, stageArgs, loc); + const type = staged.destPromise ?? { kind: "promise", inner: VOID }; // A promise destination's value settles the result (same type). - return staged.wrap({ - kind: "libCall", - fn: "sp.pipelineStages", - args: staged.args, - type: staged.destPromise ?? { kind: "promise", inner: VOID }, - loc, - }); + if (!opts) { + return staged.wrap({ + kind: "libCall", + fn: "sp.pipelineStages", + args: staged.args, + type, + loc, + }); + } + // the options: end, and the signal (flags bit 6) + const flagsArg = staged.args[3]!; + if (flagsArg.kind !== "numLit") + throw new InternalCompilerError("pipeline flags not a literal"); + const args = [...staged.args, boolLit(opts.end, loc)]; + if (opts.signal) { + args[3] = numLit(flagsArg.value | 64, loc); + args.push(opts.signal); + } + return staged.wrap({ kind: "libCall", fn: "sp.pipelineStagesOpts", args, type, loc }); } let streams: IrExpr[]; try { @@ -2803,22 +3150,27 @@ export function lowerStreamModuleCall( } if (hasPipelineFunctionStage(lowerer, args.slice(0, -1))) { const staged = lowerPipelineStages(lowerer, args.slice(0, -1), loc); - if (staged.dest !== 0) { - // callback(err, value): the destination's value reaches the - // callback through the checked-dynamic path. Node answers an - // object-mode PassThrough here; reading it has no lowering. - if (!ts.isExpressionStatement(call.parent)) { - lowerer.noLowering( - "using the PassThrough that pipeline() answers for a function destination", - call, - "read the destination's value in the callback", - ); - } + const cbNode = args[args.length - 1]!; + const cbParams = + ts.isArrowFunction(cbNode) || ts.isFunctionExpression(cbNode) + ? cbNode.parameters.nodes.length + : 0; + if (staged.dest === 2 || (staged.dest === 1 && cbParams > 1)) { + // callback(err, value): the destination's value (undefined for + // an async generator) reaches the callback through the + // checked-dynamic path. const cb = lowerer.lowerExprExpecting(args[args.length - 1]!, DYN); + // SCR_PIPE_TYPED_CB: typed code's callback takes null for no error + const stagedArgs = [...staged.args]; + const flagsArg = stagedArgs[3]!; + if (flagsArg.kind !== "numLit") + throw new InternalCompilerError("pipeline flags not a literal"); + if (!isJsSourceFile(ts.getSourceFileOfNode(call))) + stagedArgs[3] = numLit(flagsArg.value | 128, loc); return staged.wrap({ kind: "libCall", fn: "stream.pipelineStagesDyn", - args: [...staged.args, cb], + args: [...stagedArgs, cb], type: staged.last, loc, }); @@ -2855,7 +3207,12 @@ export function lowerStreamModuleCall( }; } - if (bi.member === "compose") return lowerStreamCompose(lowerer, call, loc); + if (bi.member === "compose") { + if (call.arguments.nodes.some((a) => ts.isSpreadElement(a))) { + lowerer.noLowering("compose with spread arguments", call, "pass the streams directly"); + } + return lowerStreamCompose(lowerer, call.arguments.nodes, loc); + } return null; } @@ -2914,6 +3271,8 @@ type SyncGenType = IrType & { kind: "generator" }; * array, a string or a synchronous generator object), or null. */ function iterableSourceType(lowerer: Lowerer, node: ts.Expression): IrType | null { if (pipelineStageFunction(lowerer, node) !== null) return null; + const webElem = staticReadableStreamElementType(lowerer, node); + if (webElem !== null) return webElem; let t: IrType | null; try { t = lowerer.mapTypeOf(lowerer.typeOf(node)); @@ -2933,12 +3292,245 @@ function iterableSourceType(lowerer: Lowerer, node: ts.Expression): IrType | nul * through an async-from-sync iterator, and an async generator that * yields the same elements resumes its consumer on the same ticks. Null * when the source has no such lowering. */ +/** A web ReadableStream source, as an async generator closure over its + * reader: each pass awaits reader.read() and yields the chunk, as the + * stream's async iterator does; leaving early cancels the stream (its + * iterator's return()), and the reader's lock is released either way. */ +function lowerWebStreamSource( + lowerer: Lowerer, + node: ts.Expression, + elemT: IrType, + loc: SrcLoc, +): { closure: IrExpr; out: SyncGenType } | null { + const resultT = genResultRecord(elemT, VOID, lowerer.shapes, lowerer.unions); + if (!resultT) return null; + const valueField = lowerer.shapes.get(resultT.shapeId)?.fields.find((f) => f.name === "value"); + if (!valueField) return null; + const out: SyncGenType = { + kind: "generator", + async: true, + yieldT: elemT, + retT: VOID, + nextT: UNDEFINED_T, + }; + const genResultT = genResultRecord(elemT, VOID, lowerer.shapes, lowerer.unions); + if (!genResultT) return null; + const value = lowerer.lowerExprExpecting(node, DYN); + const key = `webStreamSource:${typeKey(elemT)}`; + let maker = lowerer.valueHelpers.get(key); + if (!maker) { + maker = `%stream.webStreamSource.${lowerer.valueHelpers.size}`; + const genName = `${maker}.gen`; + const frame = new LiftedFrame(loc); + const srcId = frame.local("src", DYN, false); + const readerId = frame.local("reader", DYN, false); + const activeId = frame.local("active", BOOL, true); + const promiseT: IrType = { kind: "promise", inner: resultT }; + const pId = frame.local("p", promiseT, false); + const rId = frame.local("r", resultT, false); + const reader = (): IrExpr => varRef(readerId, DYN, loc); + const r = (): IrExpr => varRef(rId, resultT, loc); + const setActive = (on: boolean): IrStmt => ({ + kind: "assign", + localId: activeId, + value: { kind: "boolLit", value: on, type: BOOL, loc }, + loc, + }); + const extracted = extractIteratorValue( + lowerer, + elemT, + valueField.type, + { + kind: "recordGet", + obj: r(), + shapeId: resultT.shapeId, + field: "value", + type: valueField.type, + loc, + }, + loc, + ); + if (!extracted) return null; + const invoke = (method: string, args: IrExpr[], calleeName: string): IrExpr => ({ + kind: "dynInvoke", + recv: reader(), + method, + calleeName, + args, + type: DYN, + loc, + }); + const body: IrStmt[] = [ + { + kind: "varDecl", + localId: readerId, + init: { + kind: "dynInvoke", + recv: varRef(srcId, DYN, loc), + method: "getReader", + calleeName: "ReadableStream async iterator", + args: [], + type: DYN, + loc, + }, + loc, + }, + { + kind: "varDecl", + localId: activeId, + init: { kind: "boolLit", value: false, type: BOOL, loc }, + loc, + }, + { + kind: "tryCatch", + tryBody: [ + { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [ + setActive(false), + { + kind: "varDecl", + localId: pId, + init: { + kind: "libCall", + fn: "fetch.readerRead", + args: [reader()], + type: promiseT, + loc, + }, + loc, + }, + { + kind: "varDecl", + localId: rId, + init: { kind: "awaitExpr", value: varRef(pId, promiseT, loc), type: resultT, loc }, + loc, + }, + { + kind: "if", + cond: { + kind: "recordGet", + obj: r(), + shapeId: resultT.shapeId, + field: "done", + type: BOOL, + loc, + }, + then: [{ kind: "break", loc }], + else_: null, + loc, + }, + setActive(true), + { + kind: "exprStmt", + expr: { kind: "yieldExpr", value: extracted, type: VOID, loc }, + loc, + }, + ], + loc, + }, + ], + catchBody: null, + catchLocalId: null, + finallyBody: [ + { + kind: "tryCatch", + tryBody: [ + { + kind: "if", + cond: varRef(activeId, BOOL, loc), + then: [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "async.awaitDyn", + args: [ + invoke( + "cancel", + [dynUndefinedExpr(loc)], + "ReadableStream async iterator return", + ), + ], + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + ], + catchBody: null, + catchLocalId: null, + finallyBody: [ + { + kind: "exprStmt", + expr: invoke("releaseLock", [], "ReadableStream async iterator releaseLock"), + loc, + }, + ], + loc, + }, + ], + loc, + }, + { kind: "return", value: null, loc }, + ]; + const srcLocal = frame.locals.find((l) => l.id === srcId)!; + srcLocal.boxed = true; + lowerer.valueHelpers.set(key, maker); + lowerer.liftedFns.push({ + name: genName, + params: [], + returnType: VOID, + async: true, + captures: [{ localId: srcId, name: "src", type: DYN }], + locals: frame.locals, + body, + generator: { yieldT: elemT, nextT: UNDEFINED_T, resultType: genResultT }, + loc, + }); + const makerSrc: IrLocal = { id: "src.0", name: "src", type: DYN, mutable: false, boxed: true }; + lowerer.liftedFns.push({ + name: maker, + params: [{ localId: makerSrc.id, name: makerSrc.name, type: DYN }], + returnType: funcOf([], out), + locals: [makerSrc], + body: [ + { + kind: "return", + value: { + kind: "closure", + fnName: genName, + captures: [makerSrc.id], + type: funcOf([], out), + loc, + }, + loc, + }, + ], + loc, + }); + } + return { + closure: { kind: "call", callee: maker, args: [value], type: funcOf([], out), loc }, + out, + }; +} + function lowerIterableSource( lowerer: Lowerer, node: ts.Expression, fn: StageFunction | null, loc: SrcLoc, ): { closure: IrExpr; out: SyncGenType } | null { + if (fn === null) { + const webElem = staticReadableStreamElementType(lowerer, node); + if (webElem !== null) return lowerWebStreamSource(lowerer, node, webElem, loc); + } let value: IrExpr; if (fn !== null) { if (fn.parameters.nodes.length > 0) { @@ -3072,7 +3664,7 @@ function lowerIterableSource( interface PipelineStagesLowering { args: IrExpr[]; last: IrType; - dest: 0 | 2; + dest: 0 | 1 | 2; destPromise: (IrType & { kind: "promise" }) | null; wrap: (call: IrExpr) => IrExpr; } @@ -3080,9 +3672,10 @@ interface PipelineStagesLowering { /** pipeline(...) with function stages (pipeline.js). A function may * start the pipeline (a source function, called with `{ signal }`), may * end it (an async function whose promise's value reaches the callback or - * the promise), and runs of async generator functions may sit between - * streams; the last generator of a run that a stream follows is pumped - * into that stream. Each stage's `source` parameter is typed as what the + * the promise, or an async generator function), and runs of async + * generator functions may sit between streams; the last generator of a + * run that a stream follows is pumped into that stream. A function + * destination feeds the object-mode PassThrough that pipeline() answers. Each stage's `source` parameter is typed as what the * pipeline passes it: makeAsyncIterable answers the previous stream * itself (a Readable is async iterable) or the previous stage's * generator. */ @@ -3090,6 +3683,7 @@ function lowerPipelineStages( lowerer: Lowerer, stageArgs: readonly ts.Expression[], loc: SrcLoc, + preLowered?: ReadonlyMap, ): PipelineStagesLowering { if (stageArgs.length > 52) { lowerer.noLowering( @@ -3103,7 +3697,7 @@ function lowerPipelineStages( const helperArgs: IrExpr[] = []; let positions = 0; let mask = 0; - let dest: 0 | 2 = 0; + let dest: 0 | 1 | 2 = 0; let destPromise: (IrType & { kind: "promise" }) | null = null; // What the next function stage receives: null before the first stage, // else the previous stream's type or the previous stage's generator. @@ -3114,14 +3708,19 @@ function lowerPipelineStages( const argOrder: { list: IrExpr[]; index: number }[] = []; for (let j = 0; j < stageArgs.length; j++) { const arg = stageArgs[j]!; - const fn = pipelineStageFunction(lowerer, arg); + // compose's head and tail functions arrive as Duplex.from streams + const fn = preLowered?.has(j) ? null : pipelineStageFunction(lowerer, arg); const syncSourceFn = j === 0 && fn !== null && !ts.isArrowFunction(fn) && fn.asteriskToken !== undefined && !(fn.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); - if (j === 0 && (syncSourceFn || (fn === null && iterableSourceType(lowerer, arg) !== null))) { + if ( + j === 0 && + !preLowered?.has(j) && + (syncSourceFn || (fn === null && iterableSourceType(lowerer, arg) !== null)) + ) { const source = lowerIterableSource(lowerer, arg, syncSourceFn ? fn : null, loc); if (source === null) lowerer.noLowering("pipeline over this source", arg); positions += 1; @@ -3147,7 +3746,7 @@ function lowerPipelineStages( } let stream: IrExpr; try { - stream = lowerStreamArg(lowerer, arg, "pipeline"); + stream = preLowered?.get(j) ?? lowerStreamArg(lowerer, arg, "pipeline"); } catch (e) { if (e instanceof StreamArgTypeThrow) lowerer.noLowering("pipeline over this value", arg); throw e; @@ -3160,15 +3759,6 @@ function lowerPipelineStages( const isLast = j === stageArgs.length - 1; const isAsync = (fn.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); const isGen = !ts.isArrowFunction(fn) && fn.asteriskToken !== undefined; - if (isLast && isGen) { - // pipeline() pumps it into an object-mode PassThrough, whose - // buffering decides when the pipeline finishes - lowerer.noLowering( - "a pipeline destination that is an async generator function", - arg, - "end the pipeline with a stream or an async function (source) { for await (const chunk of source) ... }", - ); - } if (!isAsync || (!isGen && !isLast)) { lowerer.noLowering( isLast @@ -3207,6 +3797,14 @@ function lowerPipelineStages( dest = 2; destPromise = lowered.out; srcT = null; + } else if (isLast) { + // an async generator destination: pumped into the object-mode + // PassThrough that pipeline() answers + const helpers = fromGenHelpers(lowerer, lowered.out, loc); + if (!helpers) lowerer.noLowering("a pipeline destination with this generator type", arg); + helperArgs.push(helpers![0]!, helpers![1]!, helpers![2]!); + dest = 1; + srcT = null; } else { srcT = lowered.out; } @@ -3242,7 +3840,8 @@ function lowerPipelineStages( argDecls.length === 0 ? call : { kind: "seqExpr", stmts: argDecls, result: call, type: call.type, loc }; - // SCR_PIPE_* flags: bits 1-2 the destination, bit 5 the signal and abort. + // SCR_PIPE_* flags: bits 1-2 the destination, bit 5 the signal and + // abort (bit 6, the promise form's options signal, is set by its caller). const flags = (dest << 1) | (mask !== 0 ? 32 : 0); const head: IrExpr[] = [ numLit(streams.length, loc), @@ -3255,7 +3854,7 @@ function lowerPipelineStages( ...helperArgs, ]; const last: IrType = - dest === 2 ? { kind: "object", className: "%PassThrough" } : streams[streams.length - 1]!.type; + dest !== 0 ? { kind: "object", className: "%PassThrough" } : streams[streams.length - 1]!.type; if (mask === 0) return { args: head, last, dest, destPromise, wrap: bindArgs }; // const ac = new AbortController(): its signal goes to the stages, and // finishImpl's ac.abort() runs through the abort closure. @@ -3324,21 +3923,27 @@ function pipelineBoxHelper( /** `() => ac.abort()` over the pipeline's AbortController: an interned * lifted function takes the controller and answers the closure. */ -function pipelineAbortHelper(lowerer: Lowerer, ac: IrExpr, loc: SrcLoc): IrExpr { - const key = "pipelineAbort"; +function pipelineAbortHelper( + lowerer: Lowerer, + ac: IrExpr, + loc: SrcLoc, + withReason = false, +): IrExpr { + const key = withReason ? "pipelineAbortReason" : "pipelineAbort"; let name = lowerer.valueHelpers.get(key); - const abortT = funcOf([], VOID); + const abortT = funcOf(withReason ? [DYN] : [], VOID); if (!name) { name = keyedHelperName(lowerer, "%stream.pipelineAbort", key); lowerer.valueHelpers.set(key, name); const acLocal: IrLocal = { id: "ac.0", name: "ac", type: DYN, mutable: false, boxed: true }; const acRef = varRef(acLocal.id, DYN, loc); + const reasonLocal: IrLocal = { id: "reason.0", name: "reason", type: DYN, mutable: false }; lowerer.liftedFns.push({ name: `${name}.abort`, - params: [], + params: withReason ? [{ localId: reasonLocal.id, name: reasonLocal.name, type: DYN }] : [], returnType: VOID, captures: [{ localId: acLocal.id, name: acLocal.name, type: DYN }], - locals: [acLocal], + locals: withReason ? [acLocal, reasonLocal] : [acLocal], body: [ { kind: "exprStmt", @@ -3347,7 +3952,7 @@ function pipelineAbortHelper(lowerer: Lowerer, ac: IrExpr, loc: SrcLoc): IrExpr recv: acRef, method: "abort", calleeName: "ac.abort", - args: [], + args: withReason ? [varRef(reasonLocal.id, DYN, loc)] : [], type: DYN, loc, }, @@ -3467,6 +4072,373 @@ function lowerStageFunction( } } +/** The async generator a Duplex.from body iterates (duplexify.js's + * fromAsyncGen source), as a closure the runtime calls with next() and + * ack(): each pass awaits next() (the deferred a write or the final + * settles), calls ack() (process.nextTick(cb) and a fresh deferred), + * returns at the done marker, and yields the chunk. */ +function duplexSourceHelper(lowerer: Lowerer, genT: SyncGenType, loc: SrcLoc): IrExpr | null { + const nextT = funcOf([], { kind: "promise", inner: DYN }); + const ackT = funcOf([], VOID); + const type = funcOf([nextT, ackT], genT); + const key = "duplexSource"; + let name = lowerer.valueHelpers.get(key); + if (!name) { + const resultType = genResultRecord(DYN, VOID, lowerer.shapes, lowerer.unions); + if (!resultType) return null; + name = `%stream.duplexSource.${lowerer.valueHelpers.size}`; + const frame = new LiftedFrame(loc); + const nextId = frame.param("next", nextT); + const ackId = frame.param("ack", ackT); + const valueId = frame.local("value", DYN, false); + const value = varRef(valueId, DYN, loc); + const body: IrStmt[] = [ + { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [ + { + kind: "varDecl", + localId: valueId, + init: { + kind: "awaitExpr", + value: { + kind: "callValue", + callee: varRef(nextId, nextT, loc), + args: [], + type: { kind: "promise", inner: DYN }, + loc, + }, + type: DYN, + loc, + }, + loc, + }, + { + kind: "exprStmt", + expr: { + kind: "callValue", + callee: varRef(ackId, ackT, loc), + args: [], + type: VOID, + loc, + }, + loc, + }, + { + kind: "if", + cond: { kind: "libCall", fn: "stream.chunkDone", args: [value], type: BOOL, loc }, + then: [{ kind: "return", value: null, loc }], + else_: null, + loc, + }, + { kind: "exprStmt", expr: { kind: "yieldExpr", value, type: VOID, loc }, loc }, + ], + loc, + }, + { kind: "return", value: null, loc }, + ]; + lowerer.valueHelpers.set(key, name); + lowerer.liftedFns.push({ + name, + params: frame.params, + returnType: VOID, + async: true, + locals: frame.locals, + body, + generator: { yieldT: DYN, nextT: UNDEFINED_T, resultType }, + loc, + }); + } + return { kind: "closure", fnName: name, captures: [], type, loc }; +} + +/** Duplex.from(body) (duplexify.js) for the bodies with a lowering: an + * async generator function, whose source generator the Duplex's writes + * feed (stream.duplexFromGen); a duplex stream, which answers itself; and + * the bodies Readable.from reads (iterables, strings, generators), as a + * Duplexify that is not writable. */ +function duplexFromBody(lowerer: Lowerer, node: ts.Expression, loc: SrcLoc): IrExpr { + const duplex: IrType = { kind: "object", className: "%Duplex" }; + const fn = pipelineStageFunction(lowerer, node); + if (fn !== null) { + const isAsync = (fn.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); + const isGen = !ts.isArrowFunction(fn) && fn.asteriskToken !== undefined; + if (!isAsync) { + lowerer.noLowering( + "Duplex.from over a function that is not async", + node, + "write the body as async function* (source) { ... } or async function (source) { ... }", + ); + } + if (fn.parameters.nodes.length > 2) { + lowerer.noLowering( + "a Duplex.from body with parameters after the options argument", + fn.parameters.nodes[2]!, + "the body takes the source and { signal }", + ); + } + const sourceT: SyncGenType = { + kind: "generator", + async: true, + yieldT: DYN, + retT: VOID, + nextT: UNDEFINED_T, + }; + const lowered = lowerStageFunction(lowerer, fn, sourceT); + const params = lowered.closure.type.kind === "func" ? lowered.closure.type.params : []; + // fromAsyncGen calls the body with { signal } of an AbortController + // that destroy aborts with its error. + let signalArgs: IrExpr[] = []; + let signalDecls: IrStmt[] = []; + if (params.length > 1) { + if (params[1]!.kind !== "dyn") { + lowerer.noLowering( + "a Duplex.from body's options argument with this type", + fn.parameters.nodes[1]!, + "type it as { signal: AbortSignal }", + ); + } + const acLocal = lowerer.declareHiddenLocal("%duplexAbort", DYN); + const ac = varRef(acLocal.id, DYN, loc); + signalDecls = [ + { + kind: "varDecl", + localId: acLocal.id, + init: { kind: "libCall", fn: "fetch.abortControllerNew", args: [], type: DYN, loc }, + loc, + }, + ]; + signalArgs = [ + pipelineAbortHelper(lowerer, ac, loc, true), + { kind: "dynKeyGet", value: ac, key: strLit("signal", loc), type: DYN, loc }, + ]; + } + const withSignal = (fnName: IrLibFn, args: IrExpr[]): IrExpr => { + const call: IrExpr = { + kind: "libCall", + fn: signalArgs.length > 0 ? (`${fnName}Signal` as IrLibFn) : fnName, + args: [...args, ...signalArgs], + type: duplex, + loc, + }; + return signalDecls.length > 0 + ? { kind: "seqExpr", stmts: signalDecls, result: call, type: duplex, loc } + : call; + }; + if (!isGen) { + // an async function: a writable Duplexify whose final waits for + // the function's promise + const asyncSource = duplexSourceHelper(lowerer, sourceT, loc); + const out = lowered.out; + if (out.kind !== "promise" || !asyncSource) + return lowerer.noLowering("Duplex.from over a function with this return type", node); + if ( + out.inner.kind !== "void" && + !canConvertToDyn( + out.inner, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + lowerer.noLowering( + `Duplex.from over a function resolving to '${lowerer.fmt(out.inner)}'`, + node, + ); + } + return withSignal("stream.duplexFromAsyncFn", [ + lowered.closure, + asyncSource, + pipelineBoxHelper(lowerer, out, loc), + ]); + } + const helpers = + lowered.out.kind === "generator" ? fromGenHelpers(lowerer, lowered.out, loc) : null; + const source = duplexSourceHelper(lowerer, sourceT, loc); + if (!helpers || !source) + lowerer.noLowering("Duplex.from over a function with this generator type", node); + return withSignal("stream.duplexFromGen", [lowered.closure, source!, ...helpers!]); + } + // duplexify.js: a stream answers itself (a Duplex) or is wrapped; any + // other body reads as from(Duplexify, body, { objectMode: true, + // writable: false }) + let bodyT: IrType | null; + try { + bodyT = lowerer.mapTypeOf(lowerer.typeOf(node)); + } catch { + bodyT = null; + } + const bodySides = + bodyT?.kind === "object" ? streamSidesOf(lowerer, lowerer.classes.get(bodyT.className)) : null; + let literal: ts.Expression = node; + while (ts.isParenthesizedExpression(literal)) literal = literal.expression; + if (bodySides === null && ts.isObjectLiteralExpression(literal)) + return duplexFromPair(lowerer, literal, loc); + if (bodyT?.kind === "promise") { + // a Duplexify that reads the promise's value + const valueT = bodyT.inner; + if ( + valueT.kind !== "void" && + !canConvertToDyn( + valueT, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + lowerer.noLowering(`Duplex.from over a promise of '${lowerer.fmt(valueT)}'`, node); + } + const promise = lowerer.lowerExpr(node); + if (promise.type.kind !== "promise") { + // a checked-dynamic promise (an untyped function's result) + return { + kind: "libCall", + fn: "stream.duplexFromDyn", + args: [lowerer.coerceToExpected(promise, DYN)], + type: { kind: "object", className: "%Duplex" }, + loc, + }; + } + return { + kind: "libCall", + fn: "stream.duplexFromPromise", + args: [ + promise, + pipelineBoxHelper(lowerer, promise.type as IrType & { kind: "promise" }, loc), + ], + type: { kind: "object", className: "%Duplex" }, + loc, + }; + } + if (bodySides === null) return lowerFromSource(lowerer, node, node, undefined, true); + let stream: IrExpr; + try { + stream = lowerStreamArg(lowerer, node, "compose"); + } catch (e) { + if (e instanceof StreamArgTypeThrow) return e.expr; + throw e; + } + const sides = + stream.type.kind === "object" + ? streamSidesOf(lowerer, lowerer.classes.get(stream.type.className)) + : null; + // _duplexify({ readable }) / _duplexify({ writable }) + if (sides === "r" || sides === "w") { + return { + kind: "libCall", + fn: sides === "r" ? "stream.duplexifyReadable" : "stream.duplexifyWritable", + args: [stream], + type: duplex, + loc, + }; + } + if (sides !== "rw") { + lowerer.noLowering( + "Duplex.from over a value of this type", + node, + "an async generator function, an iterable, a stream, or a { readable, writable } pair is the lowered body", + ); + } + return stream; +} + +/** Duplex.from({ readable, writable }): each member that is a stream with + * that side is used as it is, any other member is Duplex.from of it, and + * the pair reads and writes through _duplexify (duplexify.js). The + * members evaluate in source order. */ +function duplexFromPair( + lowerer: Lowerer, + literal: ts.ObjectLiteralExpression, + loc: SrcLoc, +): IrExpr { + const duplex: IrType = { kind: "object", className: "%Duplex" }; + const sides: { readable?: IrExpr; writable?: IrExpr } = {}; + // The members in source order; when that is not (readable, writable), + // each value is bound to a local first, keeping the literal's + // evaluation order. + const members: { key: "readable" | "writable"; value: IrExpr }[] = []; + for (const p of literal.properties.nodes) { + const key = + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && ts.isIdentifier(p.name) + ? p.name.text + : null; + if (key !== "readable" && key !== "writable") { + lowerer.noLowering( + "Duplex.from over an object with members other than readable and writable", + p, + "pass { readable, writable } with streams or Duplex.from bodies", + ); + } + const value = ts.isShorthandPropertyAssignment(p) + ? (p.name as ts.Identifier) + : (p as ts.PropertyAssignment).initializer; + let valueT: IrType | null; + try { + valueT = lowerer.mapTypeOf(lowerer.typeOf(value)); + } catch { + valueT = null; + } + const valueSides = + valueT?.kind === "object" + ? streamSidesOf(lowerer, lowerer.classes.get(valueT.className)) + : null; + const wanted = key === "readable" ? "r" : "w"; + const direct = valueSides === "rw" || valueSides === wanted; + members.push({ + key: key!, + value: direct + ? lowerStreamArg(lowerer, value, "compose") + : duplexFromBody(lowerer, value, loc), + }); + } + const decls: IrStmt[] = []; + const inOrder = + members.length <= 1 || + (members.length === 2 && members[0]!.key === "readable" && members[1]!.key === "writable"); + for (const m of members) { + if (inOrder) { + sides[m.key] = m.value; + continue; + } + const local = lowerer.declareHiddenLocal(`%duplex${m.key}`, m.value.type); + decls.push({ kind: "varDecl", localId: local.id, init: m.value, loc }); + sides[m.key] = varRef(local.id, m.value.type, loc); + } + const { readable, writable } = sides; + if (readable && writable) { + const call: IrExpr = { + kind: "libCall", + fn: "stream.duplexifyPair", + args: [readable, writable], + type: duplex, + loc, + }; + return decls.length > 0 + ? { kind: "seqExpr", stmts: decls, result: call, type: duplex, loc } + : call; + } + if (decls.length > 0) { + // a repeated member: every value is evaluated, the last one is used + const one = readable ?? writable!; + const call: IrExpr = { + kind: "libCall", + fn: readable ? "stream.duplexifyReadable" : "stream.duplexifyWritable", + args: [one], + type: duplex, + loc, + }; + return { kind: "seqExpr", stmts: decls, result: call, type: duplex, loc }; + } + if (readable) + return { kind: "libCall", fn: "stream.duplexifyReadable", args: [readable], type: duplex, loc }; + if (writable) + return { kind: "libCall", fn: "stream.duplexifyWritable", args: [writable], type: duplex, loc }; + return lowerer.noLowering( + "Duplex.from over an object with neither readable nor writable", + literal, + "pass { readable, writable } with streams or Duplex.from bodies", + ); +} + /** The checker type of a stage's string or Buffer chunks, or null for * other chunk types. */ function stageChunkCheckerType(lowerer: Lowerer, chunk: IrType, at: ts.Node): ts.Type | null { @@ -3481,12 +4453,8 @@ function stageChunkCheckerType(lowerer: Lowerer, chunk: IrType, at: ts.Node): ts * classes fix which positions can be readable and writable; a stream in * the wrong position, a single Readable or Writable (Duplex.from wraps * those), and functions, iterables or web streams have no lowering. */ -function lowerStreamCompose(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr { - const args = call.arguments.nodes; +function lowerStreamCompose(lowerer: Lowerer, args: readonly ts.Expression[], loc: SrcLoc): IrExpr { const duplex: IrType = { kind: "object", className: "%Duplex" }; - if (args.some((a) => ts.isSpreadElement(a))) { - lowerer.noLowering("compose with spread arguments", call, "pass the streams directly"); - } if (args.length === 0) { return nodeThrowExpr( 1, @@ -3496,9 +4464,47 @@ function lowerStreamCompose(lowerer: Lowerer, call: ts.CallExpression, loc: SrcL loc, ); } + // A web ReadableStream first stage reads as an async iterable (the + // pipeline's source), and alone it is Duplex.from of it; a later stage + // would be Duplex.fromWeb of it. + for (let i = 1; i < args.length; i++) { + const a = args[i]!; + // (typeOf on a stage function's name would report what collecting its + // declared signature deferred) + if (pipelineStageFunction(lowerer, a) !== null) continue; + if (staticReadableStreamElementType(lowerer, a) !== null) { + lowerer.noLowering( + "compose with a web ReadableStream after the first stage", + a, + "a web ReadableStream is supported as the first stage of compose or pipeline, and in Readable.from", + ); + } + } + // Duplex.from(stage) for one stage (duplexify.js), and for a function at + // either end; functions between, an iterable first stage or a source + // function run as the pipeline's function stages (compose.js) + if (args.length === 1) return duplexFromBody(lowerer, args[0]!, loc); + const last = args.length - 1; + const isFn = (i: number): boolean => pipelineStageFunction(lowerer, args[i]!) !== null; + const pre = new Map(); + if (isFn(0)) pre.set(0, duplexFromBody(lowerer, args[0]!, loc)); + if (isFn(last)) pre.set(last, duplexFromBody(lowerer, args[last]!, loc)); + const staged = + args.some((_, i) => !pre.has(i) && isFn(i)) || + (!pre.has(0) && iterableSourceType(lowerer, args[0]!) !== null); + if (staged) { + const lowered = lowerPipelineStages(lowerer, args, loc, pre); + return lowered.wrap({ + kind: "libCall", + fn: "stream.composeStages", + args: lowered.args, + type: duplex, + loc, + }); + } let streams: IrExpr[]; try { - streams = args.map((a) => lowerStreamArg(lowerer, a, "compose")); + streams = args.map((a, i) => pre.get(i) ?? lowerStreamArg(lowerer, a, "compose")); } catch (e) { if (e instanceof StreamArgTypeThrow) return e.expr; throw e; @@ -3541,6 +4547,29 @@ function lowerStreamCompose(lowerer: Lowerer, call: ts.CallExpression, loc: SrcL /* ── the Readable operators ──────────────────────────────────────────── */ /** The Readable operators (scr_stream_ops.c — Node's operators.js). */ +/** A stream member's receiver, lowered: a value the checker types as a + * stream class whose lowering is checked-dynamic (a JavaScript binding + * that holds an untyped call's result) or a union (an element read) + * converts to that class, checked where the member runs. */ +function streamReceiverExpr(lowerer: Lowerer, node: ts.Expression): IrExpr { + const value = lowerer.lowerExpr(node); + if (value.type.kind !== "dyn" && value.type.kind !== "union") return value; + let classT: IrType | null; + try { + classT = lowerer.mapTypeOf(lowerer.typeOf(node)); + } catch { + return value; + } + if ( + classT?.kind !== "object" || + streamSidesOf(lowerer, lowerer.classes.get(classT.className)) === null + ) + return value; + return value.type.kind === "dyn" + ? { kind: "dynCheck", value, type: classT, loc: value.loc } + : lowerer.coerceToExpected(value, classT); +} + export const STREAM_OPERATORS: ReadonlySet = new Set([ "map", "filter", @@ -3555,39 +4584,40 @@ export const STREAM_OPERATORS: ReadonlySet = new Set([ "reduce", ]); -/** An operator callback as a checked-dynamic function value. Callbacks - * that declare the options argument ({ signal }) are refused: the - * AbortSignal Node passes there has no representation. map and flatMap - * callbacks must produce strings or Buffers (flatMap: arrays of them, or - * strings) — or checked-dynamic values, which the runtime checks. */ +/** An operator callback as a checked-dynamic function value, and whether + * it declares the options argument ({ signal }), which the runtime then + * passes. Its results convert to checked-dynamic values (flatMap's are + * iterated with yield*). */ function lowerOperatorCallback( lowerer: Lowerer, node: ts.Expression, member: string, maxParams: number, -): IrExpr { +): { fn: IrExpr; takesOptions: boolean; gen?: IrType & { kind: "generator" } } { const loc = locOf(node); const optionsParam = (at: ts.Node): never => lowerer.noLowering( - `${member} callbacks that declare the options argument`, + `${member} callbacks with parameters after the options argument`, at, - "the { signal } argument has no lowering — take only the chunk", + "the callback takes the chunk and { signal }", ); - if ( - (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && - node.parameters.nodes.length > maxParams - ) { - optionsParam(node.parameters.nodes[maxParams]!); + let takesOptions = false; + if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) { + if (node.parameters.nodes.length > maxParams + 1) + optionsParam(node.parameters.nodes[maxParams + 1]!); + takesOptions = node.parameters.nodes.length > maxParams; } const v = lowerer.lowerExpr(node); - if (v.type.kind === "dyn") return v; + // A checked-dynamic function's parameters are not known here: it gets + // the { signal } argument, as Node passes it to every callback. + if (v.type.kind === "dyn") return { fn: v, takesOptions: true }; if ( v.type.kind !== "func" && isJsSourceFile(ts.getSourceFileOfNode(node)) && lowerer.dynConvertible(v.type) ) { // JavaScript passes any value; Node's validateFunction throws. - return { kind: "dynFrom", value: v, type: DYN, loc }; + return { fn: { kind: "dynFrom", value: v, type: DYN, loc }, takesOptions: false }; } if (v.type.kind !== "func") { lowerer.noLowering( @@ -3596,25 +4626,8 @@ function lowerOperatorCallback( "pass a function", ); } - if (v.type.params.length > maxParams) optionsParam(node); - if (member === "map" || member === "flatMap") { - const ret = v.type.ret.kind === "promise" ? v.type.ret.inner : v.type.ret; - const chunk = (t: IrType): boolean => - t.kind === "string" || t.kind === "dyn" || (t.kind === "bytes" && t.elem === "u8"); - const ok = - member === "map" - ? chunk(ret) - : ret.kind === "dyn" || ret.kind === "string" || (ret.kind === "array" && chunk(ret.elem)); - if (!ok) { - lowerer.noLowering( - `${member} callbacks that return '${lowerer.fmt(ret)}'`, - node, - member === "map" - ? "map results are stream chunks: strings or Buffers (object-mode streams of other values are not lowered)" - : "flatMap results are arrays of strings or Buffers, or strings", - ); - } - } + if (v.type.params.length > maxParams + 1) optionsParam(node); + if (v.type.params.length > maxParams) takesOptions = true; const getRecord = (id: string) => lowerer.shapes.get(id); const getUnion = (id: string) => lowerer.unions.get(id); if (!canBoxFuncIntoDyn(v.type, getRecord, getUnion)) { @@ -3624,7 +4637,95 @@ function lowerOperatorCallback( "the callback's parameter and result types must cross the checked-dynamic boundary", ); } - return { kind: "dynFrom", value: v, type: DYN, loc }; + // flatMap over a compiled generator: the runtime steps it through the + // helpers over its type + const gen = member === "flatMap" && v.type.ret.kind === "generator" ? v.type.ret : undefined; + return { fn: { kind: "dynFrom", value: v, type: DYN, loc }, takesOptions, ...(gen && { gen }) }; +} + +/** The operator callbacks' `{ signal }` (operators.js): map and the + * operators over it pass `{ signal: AbortSignal.any([options?.signal] + * .filter(Boolean)) }`. options is the operator's options value. */ +export function operatorSignalOptions(lowerer: Lowerer, options: IrExpr, loc: SrcLoc): IrExpr { + void lowerer; + const list: IrExpr = { + kind: "libCall", + fn: "readable.opSignalList", + args: [options], + type: DYN, + loc, + }; + const signal: IrExpr = { kind: "libCall", fn: "fetch.abortAny", args: [list], type: DYN, loc }; + return { kind: "libCall", fn: "readable.opSignalOpts", args: [signal], type: DYN, loc }; +} + +/** reduce's signal for a reducer that reads it (operators.js): `const ac + * = new AbortController()`, `options.signal.addEventListener('abort', () + * => ac.abort(), { once: true })`, then reduceSignal's ac.abort() closure + * and `{ signal: ac.signal }`. options is the bound options value. */ +export function reduceSignalSetup( + lowerer: Lowerer, + options: IrExpr, + loc: SrcLoc, + declare: (name: string, type: IrType) => IrLocal = (name, type) => + lowerer.declareHiddenLocal(name, type), +): { stmts: IrStmt[]; abort: IrExpr; cbOpts: IrExpr } { + const acLocal = declare("%opAbort", DYN); + const ac = varRef(acLocal.id, DYN, loc); + const sigLocal = declare("%opUserSignal", DYN); + const sig = varRef(sigLocal.id, DYN, loc); + const abort = pipelineAbortHelper(lowerer, ac, loc); + const stmts: IrStmt[] = [ + { + kind: "varDecl", + localId: acLocal.id, + init: { kind: "libCall", fn: "fetch.abortControllerNew", args: [], type: DYN, loc }, + loc, + }, + { + kind: "varDecl", + localId: sigLocal.id, + init: { kind: "libCall", fn: "readable.opSignalOf", args: [options], type: DYN, loc }, + loc, + }, + { + kind: "if", + cond: { kind: "dynTest", test: "truthy", value: sig, type: BOOL, loc }, + then: [ + { + kind: "exprStmt", + expr: { + kind: "dynInvoke", + recv: sig, + method: "addEventListener", + calleeName: "options.signal.addEventListener", + args: [ + { kind: "dynFrom", value: strLit("abort", loc), type: DYN, loc }, + { kind: "dynFrom", value: pipelineAbortHelper(lowerer, ac, loc), type: DYN, loc }, + { kind: "libCall", fn: "readable.opOnceOptions", args: [], type: DYN, loc }, + ], + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + ]; + const signal: IrExpr = { + kind: "dynKeyGet", + value: ac, + key: strLit("signal", loc), + type: DYN, + loc, + }; + return { + stmts, + abort, + cbOpts: { kind: "libCall", fn: "readable.opSignalOpts", args: [signal], type: DYN, loc }, + }; } /** An operator's options (or reduce's initial value): an object literal @@ -3663,21 +4764,37 @@ function lowerStreamOperator( ): IrExpr { const loc = locOf(call); const args = call.arguments.nodes; - const recv = lowerer.lowerExpr(access.expression); + // JS order: the receiver, the callback, then the options; the + // callback's { signal } is built over the options value afterwards. + const decls: IrStmt[] = []; + const bind = (value: IrExpr, name: string): IrExpr => { + const local = lowerer.declareHiddenLocal(name, value.type); + decls.push({ kind: "varDecl", localId: local.id, init: value, loc }); + return varRef(local.id, value.type, loc); + }; + const recv = bind(streamReceiverExpr(lowerer, access.expression), "%opRecv"); const readable: IrType = { kind: "object", className: "%Readable" }; const opts = (i: number): IrExpr => lowerOperatorValue(lowerer, args[i], `${member} options`, loc, true); + let takesOptions = false; + let callbackGen: (IrType & { kind: "generator" }) | undefined; const fnArg = (maxParams: number): IrExpr => { if (args[0] === undefined) return dynUndefinedExpr(loc); - return lowerOperatorCallback(lowerer, args[0], member, maxParams); + const lowered = lowerOperatorCallback(lowerer, args[0], member, maxParams); + takesOptions = lowered.takesOptions; + callbackGen = lowered.gen; + return bind(lowered.fn, "%opFn"); + }; + const lib = (fn: string, a: IrExpr[], type: IrType): IrExpr => { + const call: IrExpr = { kind: "libCall", fn: fn as IrLibFn, args: a, type, loc }; + return { kind: "seqExpr", stmts: decls, result: call, type, loc }; + }; + // the options argument (bound), and the callback's { signal } when it + // declares one + const withSignal = (optsValue: IrExpr): IrExpr[] => { + const o = bind(optsValue, "%opOptions"); + return [o, takesOptions ? operatorSignalOptions(lowerer, o, loc) : dynUndefinedExpr(loc)]; }; - const lib = (fn: string, a: IrExpr[], type: IrType): IrExpr => ({ - kind: "libCall", - fn: fn as IrLibFn, - args: a, - type, - loc, - }); switch (member) { case "map": case "filter": @@ -3689,7 +4806,16 @@ function lowerStreamOperator( : member === "filter" ? "readable.opFilter" : "readable.opFlatMap"; - return lib(fnName, [recv, fn, opts(1)], readable); + const rest = withSignal(opts(1)); + const genHelpers = callbackGen ? fromGenHelpers(lowerer, callbackGen, loc) : null; + if (callbackGen && genHelpers) { + return lib( + callbackGen.async ? "readable.opFlatMapAsyncGen" : "readable.opFlatMapSyncGen", + [recv, fn, ...rest, ...genHelpers], + readable, + ); + } + return lib(fnName, [recv, fn, ...rest], readable); } case "drop": case "take": { @@ -3702,15 +4828,24 @@ function lowerStreamOperator( } case "toArray": return lib("readable.opToArray", [recv, opts(0)], { kind: "promise", inner: DYN }); - case "forEach": - return lib("readable.opForEach", [recv, fnArg(1), opts(1)], { kind: "promise", inner: VOID }); + case "forEach": { + const fn = fnArg(1); + return lib("readable.opForEach", [recv, fn, ...withSignal(opts(1))], { + kind: "promise", + inner: VOID, + }); + } case "some": case "every": { const fn = fnArg(1); - return lib(member === "some" ? "readable.opSome" : "readable.opEvery", [recv, fn, opts(1)], { - kind: "promise", - inner: BOOL, - }); + return lib( + member === "some" ? "readable.opSome" : "readable.opEvery", + [recv, fn, ...withSignal(opts(1))], + { + kind: "promise", + inner: BOOL, + }, + ); } case "find": { const t = lowerer.typeOf(call); @@ -3722,7 +4857,11 @@ function lowerStreamOperator( "the result is a checked-dynamic value — use a boolean predicate and narrow the result", ); } - return lib("readable.opFind", [recv, fnArg(1), opts(1)], { kind: "promise", inner: DYN }); + const fn = fnArg(1); + return lib("readable.opFind", [recv, fn, ...withSignal(opts(1))], { + kind: "promise", + inner: DYN, + }); } case "reduce": { const t = lowerer.typeOf(call); @@ -3749,12 +4888,19 @@ function lowerStreamOperator( } } const fn = fnArg(2); - const init = lowerOperatorValue(lowerer, args[1], "reduce's initial value", loc, false); - return lib( - "readable.opReduce", - [recv, fn, init, boolLit(args.length >= 2, loc), opts(2), numLit(kind, loc)], - { kind: "promise", inner }, + const init = bind( + lowerOperatorValue(lowerer, args[1], "reduce's initial value", loc, false), + "%opInit", ); + const o = bind(opts(2), "%opOptions"); + const head = [recv, fn, init, boolLit(args.length >= 2, loc), o, numLit(kind, loc)]; + if (!takesOptions) return lib("readable.opReduce", head, { kind: "promise", inner }); + const setup = reduceSignalSetup(lowerer, o, loc); + decls.push(...setup.stmts); + return lib("readable.opReduceSignal", [...head, setup.abort, setup.cbOpts], { + kind: "promise", + inner, + }); } } throw new InternalCompilerError(`unknown stream operator ${member}`); @@ -3807,7 +4953,7 @@ function pipeIntoStdio( ): IrExpr { const loc = locOf(call); // JS order: the receiver, then the destination, then the options. - const receiverValue = lowerer.lowerExpr(access.expression); + const receiverValue = streamReceiverExpr(lowerer, access.expression); const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type); const stdio = lowerer.declareHiddenLocal("%pipeStdio", target.type); const options = call.arguments.nodes[1]; @@ -3929,7 +5075,7 @@ function pipeIntoDyn( ): IrExpr { const loc = locOf(call); noteUntypedStreamFlow(lowerer, null); - const receiverValue = lowerer.lowerExpr(access.expression); + const receiverValue = streamReceiverExpr(lowerer, access.expression); const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type); const destination = lowerer.declareHiddenLocal("%pipeDestination", target.type); const stmts: IrStmt[] = [ @@ -4024,6 +5170,80 @@ function pipeIntoDyn( }; } +/** readable.compose(stage, options): compose(this, stage), with + * addAbortSignal(options.signal, composed) when the signal is set + * (operators.js). */ +function lowerReadableCompose( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr { + const loc = locOf(call); + const args = call.arguments.nodes; + if (args.length === 0 || args.length > 2 || args.some((a) => ts.isSpreadElement(a))) { + lowerer.noLowering( + `readable.compose with ${args.length === 0 ? "no stage" : "these arguments"}`, + call, + "the supported form is readable.compose(stage) or readable.compose(stage, { signal })", + ); + } + let signalNode: ts.Expression | null = null; + const options = args[1]; + if ( + options !== undefined && + options.kind !== ts.SyntaxKind.NullKeyword && + !(ts.isIdentifier(options) && options.text === "undefined") + ) { + if (!ts.isObjectLiteralExpression(options)) { + lowerer.noLowering( + "readable.compose options that are not an object literal", + options, + "pass { signal } as an object literal", + ); + } + for (const p of options.properties.nodes) { + const key = + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + ts.isIdentifier(p.name) + ? p.name.text + : null; + if (key !== "signal") { + lowerer.noLowering( + key === null + ? "readable.compose options with this member" + : `the readable.compose option '${key}'`, + p, + "signal is the readable.compose option", + ); + } + const value = ts.isShorthandPropertyAssignment(p) + ? (p.name as ts.Identifier) + : (p as ts.PropertyAssignment).initializer; + if (value.kind === ts.SyntaxKind.NullKeyword) continue; + if (ts.isIdentifier(value) && value.text === "undefined") continue; + // The signal is read after compose runs, so only values whose + // evaluation has no effects. + if (!sideEffectFreeOptionValue(value) && !pureRead(value)) { + lowerer.noLowering( + "the readable.compose option 'signal' with this value", + p, + "pass a variable or property read (const signal = controller.signal)", + ); + } + signalNode = value; + } + } + const composed = lowerStreamCompose(lowerer, [access.expression, args[0]!], loc); + if (signalNode === null) return composed; + return { + kind: "libCall", + fn: "stream.addAbortSignal", + args: [composed, lowerer.lowerExprExpecting(signalNode, DYN)], + type: composed.type, + loc, + }; +} + export function lowerStreamMethodCall( lowerer: Lowerer, call: ts.CallExpression, @@ -4036,6 +5256,12 @@ export function lowerStreamMethodCall( if (opSides === "r" || opSides === "rw") return lowerStreamOperator(lowerer, call, access, member); } + if (member === "compose") { + const composeSides = streamSidesOf(lowerer, info); + return composeSides === "r" || composeSides === "rw" + ? lowerReadableCompose(lowerer, call, access) + : null; + } if (!STREAM_API_MEMBERS.has(member)) return null; const sides = streamSidesOf(lowerer, info); if (!sides) return null; @@ -4138,7 +5364,7 @@ export function lowerStreamMethodCall( loc, ); } - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, STRING); return { kind: "libCall", @@ -4154,7 +5380,7 @@ export function lowerStreamMethodCall( if ((kind === "null" || kind === "union" || kind === "dyn") && member === "unshift") { // unshift(value): an object-mode stream's value at the front, null // the end of the stream, as Node's readableAddChunk takes them - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); return { kind: "libCall", @@ -4164,7 +5390,7 @@ export function lowerStreamMethodCall( loc, }; } - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); if (kind === "null") { return { kind: "libCall", fn: "readable.pushNull", args: [receiver], type: BOOL, loc }; } @@ -4217,7 +5443,23 @@ export function lowerStreamMethodCall( if (member === "read") { requireSide(readableSide, "read"); if (args.length > 1) lowerer.noLowering(`read with ${args.length} arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); + // JavaScript (and a result the declarations type as any) reads any + // chunk: a Buffer, a string once encoded, or an object-mode value. + const resultTs = lowerer.typeOf(call); + if ( + isJsSourceFile(ts.getSourceFileOfNode(call)) || + (resultTs.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 + ) { + const sizeDyn = args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc); + return { + kind: "libCall", + fn: "readable.readDyn", + args: [receiver, sizeDyn], + type: DYN, + loc, + }; + } const size = args[0] ? lowerer.lowerExprExpecting(args[0], F64) : numLit(-1, loc); const type: IrType = unionOf(lowerer, [BYTES, { kind: "nullT" }]); const read: IrExpr = { @@ -4233,7 +5475,7 @@ export function lowerStreamMethodCall( if (member === "pause" || member === "resume") { requireSide(readableSide, member); if (args.length !== 0) lowerer.noLowering(`${member} with arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); return { kind: "libCall", fn: member === "pause" ? "readable.pause" : "readable.resume", @@ -4246,7 +5488,7 @@ export function lowerStreamMethodCall( if (member === "isPaused") { requireSide(readableSide, "isPaused"); if (args.length !== 0) lowerer.noLowering(`isPaused with arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); return { kind: "libCall", fn: "readable.isPaused", args: [receiver], type: BOOL, loc }; } @@ -4263,7 +5505,7 @@ export function lowerStreamMethodCall( // Transform, and pipe() answers the value itself (the receiver // evaluates first, as in JS). if (isValueStream(lowerer, args[0]!)) { - const receiverValue = lowerer.lowerExpr(access.expression); + const receiverValue = streamReceiverExpr(lowerer, access.expression); const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type); const hashValue = lowerer.lowerExpr(args[0]!); const hash = lowerer.declareHiddenLocal("%pipeHash", hashValue.type); @@ -4313,7 +5555,7 @@ export function lowerStreamMethodCall( ); } const end = pipeEndOption(lowerer, args[1], loc); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); return { kind: "libCall", fn: generalPipe(lowerer, dst.type), @@ -4326,7 +5568,7 @@ export function lowerStreamMethodCall( if (member === "unpipe") { requireSide(readableSide, "unpipe"); if (args.length > 1) lowerer.noLowering(`unpipe with ${args.length} arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); if (args[0] !== undefined) { const dst = lowerer.lowerExpr(args[0]); if (dst.type.kind === "dyn") return unpipeDyn(lowerer, receiver, dst, loc); @@ -4387,7 +5629,7 @@ export function lowerStreamMethodCall( if (member === "destroy") { if (args.length > 1) lowerer.noLowering(`destroy with ${args.length} arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); if (args[0] !== undefined) { const err = lowerer.lowerExpr(args[0]); const rootsAtError = (t: IrType): boolean => { @@ -4440,7 +5682,7 @@ export function lowerStreamMethodCall( // the callback took its place (a byte-mode side encodes a string // chunk with it). const writeEnc = (encArg: ts.Expression | undefined, cbArg: ts.Expression | undefined) => { - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); const tail: IrExpr[] = []; if (encArg !== undefined) tail.push(lowerer.lowerExprExpecting(encArg, DYN)); @@ -4470,7 +5712,7 @@ export function lowerStreamMethodCall( } return writeEnc(args[1], undefined); } - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); return { kind: "libCall", fn: "writable.writeDyn", args: [receiver, chunk], type: BOOL, loc }; } @@ -4480,7 +5722,7 @@ export function lowerStreamMethodCall( // lowered value may have collapsed to one arm (checker narrowing) — // re-dispatch on its IR type. if (args.length > 1) lowerer.noLowering("write with a union chunk and more arguments", call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExpr(args[0]!); const fn = chunk.type.kind === "union" @@ -4503,7 +5745,7 @@ export function lowerStreamMethodCall( return writeEnc(args[1], undefined); cbArg = args[1]; } - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = kind === "bytes" ? lowerer.lowerExprExpecting(args[0]!, BYTES) @@ -4538,7 +5780,7 @@ export function lowerStreamMethodCall( encNode = args[1]; cbArg = args[2]; } - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); let flags = 0; const tail: IrExpr[] = []; if (chunkNode !== undefined) { @@ -4585,7 +5827,7 @@ export function lowerStreamMethodCall( if (member === "cork" || member === "uncork") { requireSide(writableSide, member); if (args.length !== 0) lowerer.noLowering(`${member} with arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); return { kind: "libCall", fn: member === "cork" ? "writable.cork" : "writable.uncork", @@ -4605,7 +5847,7 @@ export function lowerStreamMethodCall( ); } const enc = bufEncoding(lowerer, "setEncoding", args[0]!); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); return { kind: "libCall", fn: "readable.setEncoding", @@ -4616,18 +5858,20 @@ export function lowerStreamMethodCall( } if (member === "setDefaultEncoding") { - // The write-side default: utf8 IS the default — admit it; other - // encodings would change what write(string) means. + // The write-side default: lowercased and validated (ERR_UNKNOWN_ENCODING, + // thrown) at run time; answers the stream. requireSide(writableSide, "setDefaultEncoding"); - const t = args[0] ? lowerer.typeOf(args[0]) : null; - if (t?.isStringLiteralType() && (t.value === "utf8" || t.value === "utf-8")) { - return lowerer.lowerExpr(access.expression); - } - lowerer.noLowering( - "setDefaultEncoding", - call, - "utf8 (the default, as a compile-time literal) is the supported write-side string encoding", - ); + if (args.length > 1) + lowerer.noLowering(`setDefaultEncoding with ${args.length} arguments`, call); + const receiver = streamReceiverExpr(lowerer, access.expression); + const enc = args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc); + return { + kind: "libCall", + fn: "writable.setDefaultEncoding", + args: [receiver, enc], + type: receiver.type, + loc, + }; } return null; @@ -4772,7 +6016,7 @@ export function lowerStreamProperty( const propCall = (type: IrType): IrExpr => ({ kind: "libCall", fn: "stream.prop", - args: [lowerer.lowerExpr(expr.expression), strLit(name, loc)], + args: [streamReceiverExpr(lowerer, expr.expression), strLit(name, loc)], type, loc, }); @@ -4795,7 +6039,7 @@ export function lowerStreamProperty( const read: IrExpr = { kind: "libCall", fn: "readable.flowing", - args: [lowerer.lowerExpr(expr.expression)], + args: [streamReceiverExpr(lowerer, expr.expression)], type, loc, }; @@ -4809,7 +6053,7 @@ export function lowerStreamProperty( const read: IrExpr = { kind: "libCall", fn: "stream.errored", - args: [lowerer.lowerExpr(expr.expression)], + args: [streamReceiverExpr(lowerer, expr.expression)], type, loc, }; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 34b345c94..93c6246d9 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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 | 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`). */ + 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) ─────────────────────────── */ diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index cb16fd257..8b826304b 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -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. diff --git a/packages/compiler/src/frontend/lowering/width-lift.ts b/packages/compiler/src/frontend/lowering/width-lift.ts index e3722bc32..126f80f93 100644 --- a/packages/compiler/src/frontend/lowering/width-lift.ts +++ b/packages/compiler/src/frontend/lowering/width-lift.ts @@ -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" }; diff --git a/packages/compiler/src/frontend/ts7/ast-wire.test.ts b/packages/compiler/src/frontend/ts7/ast-wire.test.ts index 03a98a2dc..fd3e611e2 100644 --- a/packages/compiler/src/frontend/ts7/ast-wire.test.ts +++ b/packages/compiler/src/frontend/ts7/ast-wire.test.ts @@ -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); diff --git a/packages/compiler/src/frontend/ts7/ast-wire.ts b/packages/compiler/src/frontend/ts7/ast-wire.ts index 9dd8c9f5c..6b2222eae 100644 --- a/packages/compiler/src/frontend/ts7/ast-wire.ts +++ b/packages/compiler/src/frontend/ts7/ast-wire.ts @@ -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(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; diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 14379f30f..78efe3d17 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -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: 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 — the checker's type of `g.next()` (an // alias for IteratorYieldResult | IteratorReturnResult): // 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 (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`: a union whose arms are the sources that keep their own + * representation — `T[]`, a `Set`, a `Map` (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 diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 7d01bb83d..eb3f8c6ab 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -346,7 +346,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = 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 = 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 = new Set([ "readable.opMap", "readable.opFilter", "readable.opFlatMap", + "readable.opFlatMapAsyncGen", + "readable.opFlatMapSyncGen", "readable.opDrop", "readable.opTake", "rl.createSignal", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 6bf864da6..8edb223b3 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -170,6 +170,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "fetch.streamFrom": { argTypes: [null], result: DYN }, // The chunk/result record depends on ReadableStream; 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++; diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index 7bef40aa6..0a802c03d 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -172,6 +172,7 @@ const MODULE_FIELDS: Record>, 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", diff --git a/packages/compiler/src/ir/ir.test.ts b/packages/compiler/src/ir/ir.test.ts index 52ef9b9c1..d62c9c83c 100644 --- a/packages/compiler/src/ir/ir.test.ts +++ b/packages/compiler/src/ir/ir.test.ts @@ -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 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); diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 4a395feb2..cb0c7e496 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -730,6 +730,32 @@ function isCollectionValueKey(t: IrType): boolean { ); } +/** The element type of an ITERABLE union (`Iterable`, + * iterableUnionType): its one generator arm — a synchronous generator over + * T that returns nothing and takes no `.next()` values — beside source arms + * (T[], Set, 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. */ diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 893c2ad07..85910369a 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -218,6 +218,17 @@ const STREAM_LIB_FNS: ReadonlySet = 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 = 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 = 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. */ diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 163c2a41c..63aa8ff5c 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -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" diff --git a/packages/compiler/src/native/cache.test.ts b/packages/compiler/src/native/cache.test.ts index 3ffdf2335..f4910ff23 100644 --- a/packages/compiler/src/native/cache.test.ts +++ b/packages/compiler/src/native/cache.test.ts @@ -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 { + 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( diff --git a/packages/compiler/src/native/cache.ts b/packages/compiler/src/native/cache.ts index f81311f52..566df308a 100644 --- a/packages/compiler/src/native/cache.ts +++ b/packages/compiler/src/native/cache.ts @@ -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): 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); diff --git a/packages/compiler/src/native/compiler.ts b/packages/compiler/src/native/compiler.ts index d4487aba4..2c850cb14 100644 --- a/packages/compiler/src/native/compiler.ts +++ b/packages/compiler/src/native/compiler.ts @@ -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`); diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index 208274f35..7311b7517 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -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 { 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, limit: number): Generator { + 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 diff --git a/packages/compiler/src/native/text-file.test.ts b/packages/compiler/src/native/text-file.test.ts new file mode 100644 index 000000000..b497bc4a2 --- /dev/null +++ b/packages/compiler/src/native/text-file.test.ts @@ -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); +}); diff --git a/packages/compiler/src/native/text-file.ts b/packages/compiler/src/native/text-file.ts index 6f8990339..d377c5273 100644 --- a/packages/compiler/src/native/text-file.ts +++ b/packages/compiler/src/native/text-file.ts @@ -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 { + 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, + written?: (slice: string) => void, +): void { + const fd = openSync(path, "w"); + try { + for (const part of parts) writeTextPart(fd, part, written); } finally { closeSync(fd); } diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 71b9570b4..0fd5249d3 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -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 () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6913-builtin-constructor-shorthand.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6913-builtin-constructor-shorthand.ts.json new file mode 100644 index 000000000..bd7194022 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6913-builtin-constructor-shorthand.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6913-builtin-constructor-shorthand.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6914-dynamic-builtin-construct.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6914-dynamic-builtin-construct.ts.json new file mode 100644 index 000000000..9bc7d8342 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6914-dynamic-builtin-construct.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6914-dynamic-builtin-construct.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-dynamic-builtin-construct.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-dynamic-builtin-construct.mjs.json new file mode 100644 index 000000000..a817ad3d2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-dynamic-builtin-construct.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6915-dynamic-builtin-construct.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-local-class-static-self-reference.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-local-class-static-self-reference.mjs.json new file mode 100644 index 000000000..aaefe846a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-local-class-static-self-reference.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6916-local-class-static-self-reference.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6917-local-class-expression-self-reference.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6917-local-class-expression-self-reference.ts.json new file mode 100644 index 000000000..41d005bd0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6917-local-class-expression-self-reference.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6917-local-class-expression-self-reference.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6918-local-class-expression-self-reference.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6918-local-class-expression-self-reference.mjs.json new file mode 100644 index 000000000..afed7a70d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6918-local-class-expression-self-reference.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6918-local-class-expression-self-reference.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6919-local-class-inspect-name.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6919-local-class-inspect-name.mjs.json new file mode 100644 index 000000000..229c01bcd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6919-local-class-inspect-name.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6919-local-class-inspect-name.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6921-local-subclass-override-dispatch.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6921-local-subclass-override-dispatch.ts.json new file mode 100644 index 000000000..1878554c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6921-local-subclass-override-dispatch.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6921-local-subclass-override-dispatch.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6922-local-subclass-override-dispatch.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6922-local-subclass-override-dispatch.mjs.json new file mode 100644 index 000000000..4da840cbc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6922-local-subclass-override-dispatch.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6922-local-subclass-override-dispatch.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7014-js-never-array-literal-fields/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7014-js-never-array-literal-fields/main.ts.json new file mode 100644 index 000000000..a67c77476 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7014-js-never-array-literal-fields/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7014-js-never-array-literal-fields/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7020-conditional-spread-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7020-conditional-spread-fields.ts.json new file mode 100644 index 000000000..f6c5594a1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7020-conditional-spread-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7020-conditional-spread-fields.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7021-promise-all-uniform-tuple.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7021-promise-all-uniform-tuple.ts.json new file mode 100644 index 000000000..c1e5f1ae4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7021-promise-all-uniform-tuple.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7021-promise-all-uniform-tuple.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7022-promise-all-dynamic-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7022-promise-all-dynamic-arrays.ts.json new file mode 100644 index 000000000..c1856ac5a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7022-promise-all-dynamic-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7022-promise-all-dynamic-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7023-union-object-patterns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7023-union-object-patterns.ts.json new file mode 100644 index 000000000..bae130837 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7023-union-object-patterns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7023-union-object-patterns.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7024-async-return-fresh-collections.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7024-async-return-fresh-collections.ts.json new file mode 100644 index 000000000..c4b9855ef --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7024-async-return-fresh-collections.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7024-async-return-fresh-collections.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7025-child-listener-chaining.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7025-child-listener-chaining.ts.json new file mode 100644 index 000000000..ed0e7b650 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7025-child-listener-chaining.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7025-child-listener-chaining.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7026-url-object-patterns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7026-url-object-patterns.ts.json new file mode 100644 index 000000000..f050c4d1a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7026-url-object-patterns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7026-url-object-patterns.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7027-search-params-conditional-init.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7027-search-params-conditional-init.ts.json new file mode 100644 index 000000000..9b69861b1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7027-search-params-conditional-init.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7027-search-params-conditional-init.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7028-non-empty-rest-tuples.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7028-non-empty-rest-tuples.ts.json new file mode 100644 index 000000000..a40ef1180 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7028-non-empty-rest-tuples.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7028-non-empty-rest-tuples.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7029-flatmap-union-results.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7029-flatmap-union-results.ts.json new file mode 100644 index 000000000..72d564d3b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7029-flatmap-union-results.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7029-flatmap-union-results.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7033-any-fields-into-declared-records.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7033-any-fields-into-declared-records.ts.json new file mode 100644 index 000000000..57e76b621 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7033-any-fields-into-declared-records.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7033-any-fields-into-declared-records.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7034-optional-field-union-routes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7034-optional-field-union-routes.ts.json new file mode 100644 index 000000000..a78b23cb1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7034-optional-field-union-routes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7034-optional-field-union-routes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-key-order-json.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-key-order-json.ts.json new file mode 100644 index 000000000..2609ff929 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-key-order-json.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7132-key-order-json.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7133-key-order-keys-values-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7133-key-order-keys-values-entries.ts.json new file mode 100644 index 000000000..40729240a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7133-key-order-keys-values-entries.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7133-key-order-keys-values-entries.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7134-key-order-for-in.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7134-key-order-for-in.ts.json new file mode 100644 index 000000000..e44503626 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7134-key-order-for-in.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7134-key-order-for-in.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7135-key-order-inspect.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7135-key-order-inspect.ts.json new file mode 100644 index 000000000..431bf5c9c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7135-key-order-inspect.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7135-key-order-inspect.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7136-key-order-copies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7136-key-order-copies.ts.json new file mode 100644 index 000000000..fea64a145 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7136-key-order-copies.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7136-key-order-copies.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7137-key-order-dynamic.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7137-key-order-dynamic.ts.json new file mode 100644 index 000000000..3b7cab36f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7137-key-order-dynamic.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7137-key-order-dynamic.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7138-key-order-late-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7138-key-order-late-writes.ts.json new file mode 100644 index 000000000..7f5302cd5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7138-key-order-late-writes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7138-key-order-late-writes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7246-stream-construct.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7246-stream-construct.ts.json new file mode 100644 index 000000000..f1dcb625f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7246-stream-construct.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7246-stream-construct.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7247-stream-construct.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7247-stream-construct.mjs.json new file mode 100644 index 000000000..00c6c7f0c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7247-stream-construct.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7247-stream-construct.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7248-stream-writev.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7248-stream-writev.ts.json new file mode 100644 index 000000000..0faf76b0f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7248-stream-writev.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7248-stream-writev.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-buffer-index-of-candidates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-buffer-index-of-candidates.ts.json new file mode 100644 index 000000000..f5299cb9a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-buffer-index-of-candidates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7249-buffer-index-of-candidates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-stream-writev.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-stream-writev.cjs.json new file mode 100644 index 000000000..5374e215e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-stream-writev.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7249-stream-writev.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7250-stream-decode-strings.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7250-stream-decode-strings.ts.json new file mode 100644 index 000000000..67d2b759b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7250-stream-decode-strings.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7250-stream-decode-strings.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7251-stream-write-encodings.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7251-stream-write-encodings.mjs.json new file mode 100644 index 000000000..d4c01fab9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7251-stream-write-encodings.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7251-stream-write-encodings.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7252-duplex-from-generator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7252-duplex-from-generator.ts.json new file mode 100644 index 000000000..d29eed123 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7252-duplex-from-generator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7252-duplex-from-generator.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7253-duplex-from.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7253-duplex-from.mjs.json new file mode 100644 index 000000000..e12ce5a52 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7253-duplex-from.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7253-duplex-from.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7254-compose-functions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7254-compose-functions.ts.json new file mode 100644 index 000000000..07e468aa2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7254-compose-functions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7254-compose-functions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7255-compose-stages.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7255-compose-stages.mjs.json new file mode 100644 index 000000000..036d9836e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7255-compose-stages.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7255-compose-stages.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7256-readable-compose.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7256-readable-compose.mjs.json new file mode 100644 index 000000000..0b5993d32 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7256-readable-compose.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7256-readable-compose.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7257-pipeline-generator-destination.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7257-pipeline-generator-destination.mjs.json new file mode 100644 index 000000000..79354d9ce --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7257-pipeline-generator-destination.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7257-pipeline-generator-destination.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7258-pipeline-generator-destination.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7258-pipeline-generator-destination.ts.json new file mode 100644 index 000000000..313c9f19e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7258-pipeline-generator-destination.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7258-pipeline-generator-destination.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7259-promises-pipeline-stage-options.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7259-promises-pipeline-stage-options.cjs.json new file mode 100644 index 000000000..6fc082ea1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7259-promises-pipeline-stage-options.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7259-promises-pipeline-stage-options.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-iterable-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-iterable-values.ts.json new file mode 100644 index 000000000..cf42c1888 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-iterable-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7380-iterable-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-iterable-forof-semantics.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-iterable-forof-semantics.ts.json new file mode 100644 index 000000000..824dfe11c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-iterable-forof-semantics.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7381-iterable-forof-semantics.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-iterable-generic-repository.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-iterable-generic-repository.ts.json new file mode 100644 index 000000000..7ac2b807f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-iterable-generic-repository.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7382-iterable-generic-repository.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-iterable-union-sources.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-iterable-union-sources.ts.json new file mode 100644 index 000000000..ea8476397 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-iterable-union-sources.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7383-iterable-union-sources.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7400-record-keyof-union-read.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7400-record-keyof-union-read.ts.json new file mode 100644 index 000000000..4f2a4c712 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7400-record-keyof-union-read.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7400-record-keyof-union-read.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7420-for-await-close.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7420-for-await-close.ts.json new file mode 100644 index 000000000..6e48bbc1c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7420-for-await-close.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7420-for-await-close.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7421-for-await-close.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7421-for-await-close.mjs.json new file mode 100644 index 000000000..dea911d1b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7421-for-await-close.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7421-for-await-close.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7422-for-await-untyped-streams.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7422-for-await-untyped-streams.mjs.json new file mode 100644 index 000000000..c2677d0b9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7422-for-await-untyped-streams.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7422-for-await-untyped-streams.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7423-stream-untyped-bindings.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7423-stream-untyped-bindings.mjs.json new file mode 100644 index 000000000..180721bd6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7423-stream-untyped-bindings.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7423-stream-untyped-bindings.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7424-duplex-from-streams.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7424-duplex-from-streams.mjs.json new file mode 100644 index 000000000..a3801c775 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7424-duplex-from-streams.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7424-duplex-from-streams.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7425-duplex-from-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7425-duplex-from-streams.ts.json new file mode 100644 index 000000000..069ae4ef4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7425-duplex-from-streams.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7425-duplex-from-streams.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7426-duplex-from-promise.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7426-duplex-from-promise.mjs.json new file mode 100644 index 000000000..e44b3f001 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7426-duplex-from-promise.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7426-duplex-from-promise.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7427-duplex-from-async-function.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7427-duplex-from-async-function.mjs.json new file mode 100644 index 000000000..22d092c60 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7427-duplex-from-async-function.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7427-duplex-from-async-function.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7428-web-stream-sources.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7428-web-stream-sources.mjs.json new file mode 100644 index 000000000..e17735658 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7428-web-stream-sources.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7428-web-stream-sources.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7429-web-stream-sources.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7429-web-stream-sources.ts.json new file mode 100644 index 000000000..8707c2f2b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7429-web-stream-sources.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7429-web-stream-sources.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7430-readable-from-web.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7430-readable-from-web.mjs.json new file mode 100644 index 000000000..3e26c3ba0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7430-readable-from-web.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7430-readable-from-web.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7431-readable-from-web.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7431-readable-from-web.ts.json new file mode 100644 index 000000000..6461c53af --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7431-readable-from-web.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7431-readable-from-web.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7432-readable-push-views.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7432-readable-push-views.mjs.json new file mode 100644 index 000000000..ffde7918d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7432-readable-push-views.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7432-readable-push-views.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7433-readable-operator-signal.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7433-readable-operator-signal.mjs.json new file mode 100644 index 000000000..5de5d9839 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7433-readable-operator-signal.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7433-readable-operator-signal.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7434-readable-operator-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7434-readable-operator-values.ts.json new file mode 100644 index 000000000..c374d9f03 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7434-readable-operator-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7434-readable-operator-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7435-readable-flatmap-iterables.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7435-readable-flatmap-iterables.mjs.json new file mode 100644 index 000000000..0c3697807 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7435-readable-flatmap-iterables.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7435-readable-flatmap-iterables.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7436-readable-read-any-value.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7436-readable-read-any-value.mjs.json new file mode 100644 index 000000000..6c2f63e17 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7436-readable-read-any-value.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7436-readable-read-any-value.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7437-duplex-from-signal.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7437-duplex-from-signal.mjs.json new file mode 100644 index 000000000..3b0699407 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7437-duplex-from-signal.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7437-duplex-from-signal.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7438-duplex-from-signal.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7438-duplex-from-signal.ts.json new file mode 100644 index 000000000..0702ed0da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7438-duplex-from-signal.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7438-duplex-from-signal.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7439-readable-read-string-chunks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7439-readable-read-string-chunks.ts.json new file mode 100644 index 000000000..e3dab7078 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7439-readable-read-string-chunks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7439-readable-read-string-chunks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/iterable-sources.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/iterable-sources.ts.json new file mode 100644 index 000000000..1b47e8dd2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/iterable-sources.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/iterable-sources.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts.json deleted file mode 100644 index 057d24d55..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts" - ], - "diags": [] -} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 3d22b2360..e17312713 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, + runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 322832f9f..d0194be05 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -129,6 +129,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_dyn_handle_props.c", "handleProps"], // for await over checked-dynamic values. ["scr_dyn_for_await.c", "dynForAwait"], + ["scr_dyn_for_await_stream.c", all("dynForAwait", "stream", "dynAsync")], // fs.ReadStream / fs.WriteStream reference only the stream unit and base units. ["scr_fs_stream.c", "stream"], // FileHandle streams reference both the stream unit and FileHandle. @@ -160,6 +161,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [ // WebCrypto's AES ciphers run over the cipher feature's mbedTLS archive. ["scr_webcrypto_aes.c", all("cipher", "webcrypto")], ["scr_fetch.c", "nativeFetch"], + ["scr_stream_from_web.c", all("stream", "nativeFetch")], ]; export function variantsFor(source) { diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 909412bc7..60ef9dc49 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, + runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index d4cdf0810..3d6b918e9 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -48,7 +48,7 @@ static inline const ScrArrExt *scr_arr_ax(const ScrArr *a) { } #define SCR_AX(a) scr_arr_ax(a) -static void scr_arr_move_inline(ScrArr *a, size_t cap, size_t unit); +static void scr_arr_move_inline(ScrArr *a, size_t cap); /* Bytes before the dense slots' allocation start: the ext pointer's word. */ static size_t scr_arr_prefix(const ScrArr *a) { @@ -60,10 +60,10 @@ static void *scr_arr_alloc_start(const ScrArr *a) { return (char *)scr_arr_block(a) - scr_arr_prefix(a); } -/* Bytes per dense slot with its state: an 8-byte value, a boolean byte or - * a 4-byte compact reference. */ +/* Bytes per dense slot: an 8-byte value or a boolean byte, and its state + * byte when the array has states. */ static size_t scr_arr_slot_unit(const ScrArr *a) { - return scr_arr_slot_width(a) + sizeof(uint8_t); + return scr_arr_slot_width(a) + (scr_arr_has_states(a) ? 1 : 0); } /* Bytes of dense storage (head room included) after the prefix. */ @@ -79,7 +79,7 @@ ScrArrExt *scr_arr_ext(ScrArr *a) { /* The pointer goes in the word before the dense allocation, which an * array with ext always has: inline slots move out, and an array without * storage gets an allocation of the word alone. */ - if (a->spare & SCR_ARR_INLINE_SLOTS) scr_arr_move_inline(a, a->cap, scr_arr_slot_unit(a)); + if (a->spare & SCR_ARR_INLINE_SLOTS) scr_arr_move_inline(a, a->cap); char *start; if (!a->data) { start = scr_mem_alloc(SCR_ARR_EXT_PREFIX); @@ -394,26 +394,14 @@ static void scr_arr_sparse_drop_prefix(ScrArr *a, size_t count) { x->sparse_len -= count; } -/* Bytes per dense slot: the value and its state byte. Boolean arrays - * store a one-byte value, compact reference arrays a 4-byte one. */ -#define SCR_ARR_SLOT_BYTES (sizeof(uint64_t) + sizeof(uint8_t)) -#define SCR_ARR_BOOL_SLOT_BYTES (2 * sizeof(uint8_t)) -#define SCR_ARR_CREF_SLOT_BYTES (sizeof(uint32_t) + sizeof(uint8_t)) - -/* SCR_ARR_*_SLOT_BYTES for an element kind. */ -static size_t scr_elem_slot_unit(uint8_t elem) { - return elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES - : elem == SCR_ELEM_CREF ? SCR_ARR_CREF_SLOT_BYTES - : SCR_ARR_SLOT_BYTES; -} /* Appends to an array smaller than this grow straight to it. */ #define SCR_ARR_MIN_GROWTH ((size_t)4) /* Inline slots: an array built by scr_arr_new_small / scr_arr_new_ops_small * (empty literals the compiler sees appended to), or constructed with a * known element count of up to SCR_ARR_KNOWN_INLINE_MAX, keeps its first - * dense slots and states in its own block, right after the ScrArr, so it - * costs one allocation and its first elements share the header's cache lines. + * dense slots in its own block, right after the ScrArr, so it costs one + * allocation and its first elements share the header's cache lines. * `spare` records it. Growth past the inline capacity moves the slots to an * ordinary allocation; the inline room then stays unused until the array is * freed. */ @@ -464,8 +452,14 @@ static void scr_arr_drop_head(ScrArr *a) { a->spare &= (uint16_t)~SCR_ARR_HEAD_ROOM; if (head == 0) return; size_t cap = a->cap, total = cap + head, live = a->len < cap ? a->len : cap; - uint8_t *end = scr_arr_states_end(a); uint64_t *data = a->data - head; + if (!scr_arr_has_states(a)) { + if (live) memmove(data, a->data, live * scr_arr_slot_width(a)); + a->data = data; + a->cap = total; + return; + } + uint8_t *end = scr_arr_states_end(a); /* The states end moves to the end of the allocation, past the `head` * vacated states: the live states move up with it, and every other state * becomes a hole. */ @@ -487,7 +481,7 @@ static bool scr_arr_drop_front(ScrArr *a, size_t count) { if (scr_arr_slot_width(a) != sizeof(uint64_t) || scr_arr_inline(a)) return false; if (count == 0) return true; size_t head = scr_arr_head(a); - memset(scr_arr_state_run(a, 0, count), SCR_ARR_HOLE, count); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, 0, count), SCR_ARR_HOLE, count); memset(a->data, 0, count * sizeof(uint64_t)); if (head) a->data[-1] = 0; a->data += count; @@ -507,9 +501,9 @@ static bool scr_arr_drop_front(ScrArr *a, size_t count) { } /* Slots requested by the small constructions. The small allocator rounds - * them up to fill its block: 3 eight-byte slots after a plain header in a - * 64-byte block (72 after a collector header), or 4 booleans in a 40-byte - * block. Most small arrays hold up to three elements; two slots make + * them up to fill its block: 4 eight-byte slots after a plain header in a + * 64-byte block (3 in 72 bytes after a collector header), or 8 booleans in + * a 40-byte block. Most small arrays hold up to three elements; two slots make * arrays that grow past them move their storage often enough to slow * programs that build many short lists. */ #if SCR_SMALL_ALLOC @@ -519,18 +513,11 @@ static bool scr_arr_drop_front(ScrArr *a, size_t count) { #endif /* Inline capacity of a small construction with `prefix` bytes of block - * before the slots of `unit` bytes (SCR_ARR_*_SLOT_BYTES). */ -static size_t scr_arr_small_cap(size_t prefix, size_t unit) { + * before slots of `unit` bytes. Inlined, so it folds to a constant for a + * constant unit. */ +static inline size_t scr_arr_small_cap(size_t prefix, size_t unit) { #if SCR_SMALL_ALLOC - /* One return per slot unit, so each folds to a constant at its callers. */ - if (unit == SCR_ARR_BOOL_SLOT_BYTES) - return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_BOOL_SLOT_BYTES)) - prefix) / - SCR_ARR_BOOL_SLOT_BYTES; - if (unit == SCR_ARR_CREF_SLOT_BYTES) - return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_CREF_SLOT_BYTES)) - prefix) / - SCR_ARR_CREF_SLOT_BYTES; - return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_SLOT_BYTES)) - prefix) / - SCR_ARR_SLOT_BYTES; + return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * unit)) - prefix) / unit; #else (void)prefix; (void)unit; @@ -546,8 +533,8 @@ static size_t scr_arr_small_cap(size_t prefix, size_t unit) { #define SCR_ARR_KNOWN_INLINE_MAX ((size_t)16) /* Inline capacity of a construction of `need` elements with `prefix` bytes - * of block before the slots of `unit` bytes (SCR_ARR_*_SLOT_BYTES), rounded - * up to fill the block; 0 when its slots stay separate. */ + * of block before slots of `unit` bytes, rounded up to fill the block; 0 + * when its slots stay separate. */ static size_t scr_arr_known_cap(size_t prefix, size_t need, size_t unit) { if (need == 0 || need > SCR_ARR_KNOWN_INLINE_MAX) return 0; #if SCR_SMALL_ALLOC @@ -561,25 +548,27 @@ static size_t scr_arr_known_cap(size_t prefix, size_t need, size_t unit) { #endif } -/* Points `a` at `cap` inline slots of `unit` bytes (SCR_ARR_*_SLOT_BYTES), - * all holes. */ +/* Points `a` at `cap` inline slots of `unit` bytes: the slot width, plus + * the state byte when the array has states (then all holes). */ static void scr_arr_use_inline(ScrArr *a, size_t cap, size_t unit) { a->data = (uint64_t *)((char *)a + SCR_ARR_INLINE_OFFSET); a->cap = (uint32_t)cap; - memset((char *)a->data + cap * (unit - 1), SCR_ARR_HOLE, cap); + if (scr_arr_has_states(a)) memset((char *)a->data + cap * (unit - 1), SCR_ARR_HOLE, cap); a->spare |= SCR_ARR_INLINE_SLOTS; } /* Move inline slots to their own allocation of `cap` (>= a->cap) slots; the * array's block keeps the unused room until it is freed. */ -static void scr_arr_move_inline(ScrArr *a, size_t cap, size_t unit) { +static void scr_arr_move_inline(ScrArr *a, size_t cap) { size_t width = scr_arr_slot_width(a); - uint64_t *data = scr_mem_alloc(cap * unit); + uint64_t *data = scr_mem_alloc(cap * scr_arr_slot_unit(a)); if (!data) scr_arr_oom(); - uint8_t *end = (uint8_t *)data + cap * (width + 1); memcpy(data, a->data, a->cap * width); - memcpy(end - a->cap, scr_arr_state_run(a, 0, a->cap), a->cap); - memset(end - cap, SCR_ARR_HOLE, cap - a->cap); + if (scr_arr_has_states(a)) { + uint8_t *end = (uint8_t *)data + cap * (width + 1); + memcpy(end - a->cap, scr_arr_state_run(a, 0, a->cap), a->cap); + memset(end - cap, SCR_ARR_HOLE, cap - a->cap); + } a->data = data; a->cap = cap; a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS; @@ -633,22 +622,25 @@ static inline void scr_arr_grow_dense_to(ScrArr *a, size_t need, ScrArrGrow mode scr_arr_grow_dense_slow(a, need, mode); return; } + bool states = scr_arr_has_states(a); + size_t unit = sizeof(uint64_t) + (states ? 1 : 0); size_t cap; if (mode == SCR_ARR_GROW_EXACT) { - cap = scr_arr_block_cap(need, SCR_ARR_SLOT_BYTES); + cap = scr_arr_block_cap(need, unit); } else { cap = a->cap < SCR_ARR_MIN_GROWTH ? SCR_ARR_MIN_GROWTH : a->cap; while (cap < need) cap += cap; if (cap > SCR_ARR_DENSE_LIMIT) cap = SCR_ARR_DENSE_LIMIT; } size_t old = a->cap; - uint64_t *data = a->data ? scr_mem_realloc(a->data, cap * SCR_ARR_SLOT_BYTES) - : scr_mem_alloc(cap * SCR_ARR_SLOT_BYTES); + uint64_t *data = a->data ? scr_mem_realloc(a->data, cap * unit) : scr_mem_alloc(cap * unit); if (!data) scr_arr_oom(); - /* The states end the allocation: move them to its new end. */ - uint8_t *end = (uint8_t *)(data + cap) + cap; - if (old) memmove(end - old, (uint8_t *)(data + old), old); - memset(end - cap, SCR_ARR_HOLE, cap - old); + if (states) { + /* The states end the allocation: move them to its new end. */ + uint8_t *end = (uint8_t *)(data + cap) + cap; + if (old) memmove(end - old, (uint8_t *)(data + old), old); + memset(end - cap, SCR_ARR_HOLE, cap - old); + } a->data = data; a->cap = cap; } @@ -679,6 +671,7 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode) { mode = SCR_ARR_GROW_APPEND; } } + bool states = scr_arr_has_states(a); size_t width = scr_arr_slot_width(a); size_t unit = scr_arr_slot_unit(a); size_t cap; @@ -698,9 +691,9 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode) { * appends benefit from geometric headroom. */ if (mode == SCR_ARR_GROW_SPAN && need > SCR_ARR_DENSE_LIMIT) cap = need; } - if (cap > SIZE_MAX / SCR_ARR_SLOT_BYTES) scr_arr_oom(); /* bounds both slot sizes */ + if (cap > SIZE_MAX / (sizeof(uint64_t) + 1)) scr_arr_oom(); /* bounds every slot unit */ if (scr_arr_inline(a)) { - scr_arr_move_inline(a, cap, unit); + scr_arr_move_inline(a, cap); scr_arr_absorb_sparse(a); return; } @@ -710,7 +703,7 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode) { if (!data) scr_arr_oom(); a->data = data; a->cap = (uint32_t)cap; - memset(scr_arr_state_run(a, 0, cap), SCR_ARR_HOLE, cap); + if (states) memset(scr_arr_state_run(a, 0, cap), SCR_ARR_HOLE, cap); return; } /* Values and states share one allocation. Move the old states to its new @@ -720,9 +713,11 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode) { char *start = scr_mem_realloc((char *)a->data - prefix, prefix + cap * unit); if (!start) scr_arr_oom(); uint64_t *data = (uint64_t *)(start + prefix); - uint8_t *end = (uint8_t *)data + cap * (width + 1); - if (a->cap) memmove(end - a->cap, (uint8_t *)data + a->cap * width, a->cap); - memset(end - cap, SCR_ARR_HOLE, cap - a->cap); + if (states) { + uint8_t *end = (uint8_t *)data + cap * (width + 1); + if (a->cap) memmove(end - a->cap, (uint8_t *)data + a->cap * width, a->cap); + memset(end - cap, SCR_ARR_HOLE, cap - a->cap); + } a->data = data; a->cap = cap; scr_arr_absorb_sparse(a); @@ -732,6 +727,44 @@ static void scr_arr_grow_dense(ScrArr *a, size_t need) { scr_arr_grow_dense_to(a, need, SCR_ARR_GROW_APPEND); } +/* States follow the value slots, and the capacity stays: inline slots + * move to an allocation of their own, and other storage grows by the state + * bytes (immortal storage, which may be static, is copied instead). */ +void scr_arr_add_states(ScrArr *a) { + if (scr_arr_has_states(a)) return; + size_t width = scr_arr_slot_width(a), len = a->len; + if (a->cap == 0) { + a->spare |= SCR_ARR_STATES; + return; + } + if (scr_arr_inline(a)) { + uint64_t *data = scr_mem_alloc(a->cap * (width + 1)); + if (!data) scr_arr_oom(); + memcpy(data, a->data, len * width); + a->data = data; + a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS; + } else if (a->rc == SIZE_MAX) { + size_t prefix = scr_arr_prefix(a); + char *start = scr_mem_alloc(prefix + a->cap * (width + 1)); + if (!start) scr_arr_oom(); + if (prefix) memcpy(start, scr_arr_alloc_start(a), prefix); + memcpy(start + prefix, a->data, len * width); + a->data = (uint64_t *)(start + prefix); + a->spare &= (uint16_t)~SCR_ARR_HEAD_ROOM; + } else { + /* Head room goes back to the capacity first. */ + scr_arr_drop_head(a); + size_t prefix = scr_arr_prefix(a); + char *start = scr_mem_realloc(scr_arr_alloc_start(a), prefix + a->cap * (width + 1)); + if (!start) scr_arr_oom(); + a->data = (uint64_t *)(start + prefix); + } + a->spare |= SCR_ARR_STATES; + uint8_t *end = scr_arr_states_end(a); + memset(end - len, SCR_ARR_VALUE, len); + memset(end - a->cap, SCR_ARR_HOLE, a->cap - len); +} + /* Room for `count` values in front of the first element of a dense array: * moves `data` back into head room, raising `cap`; the caller * stores the new first `count` slots and states. A shifted array reuses the @@ -752,21 +785,24 @@ static bool scr_arr_open_head(ScrArr *a, size_t count) { /* The new allocation: head room for the new values plus half the new * length, then the current dense window (its tail capacity included). */ size_t room = count + (len + count) / 2; - if (room > SCR_ARR_MAX_LENGTH || a->cap > SIZE_MAX / SCR_ARR_SLOT_BYTES - room) return false; + size_t unit = scr_arr_slot_unit(a); + if (room > SCR_ARR_MAX_LENGTH || a->cap > SIZE_MAX / unit - room) return false; size_t total = room + a->cap; size_t prefix = scr_arr_prefix(a); - char *start = scr_mem_alloc(prefix + total * SCR_ARR_SLOT_BYTES); + char *start = scr_mem_alloc(prefix + total * unit); if (!start) scr_arr_oom(); /* The ext pointer's word moves with the allocation. */ if (prefix) memcpy(start, scr_arr_alloc_start(a), prefix); uint64_t *block = (uint64_t *)(start + prefix); - /* The window's states end `room` bytes before the allocation does; the - * room's states past it are holes. */ - uint8_t *end = (uint8_t *)(block + total) + a->cap; memcpy(block + room, a->data, len * sizeof(uint64_t)); - memset(end, SCR_ARR_HOLE, room); - memcpy(end - len, scr_arr_state_run(a, 0, len), len); - memset(end - a->cap, SCR_ARR_HOLE, a->cap - len); + if (scr_arr_has_states(a)) { + /* The window's states end `room` bytes before the allocation does; + * the room's states past it are holes. */ + uint8_t *end = (uint8_t *)(block + total) + a->cap; + memset(end, SCR_ARR_HOLE, room); + memcpy(end - len, scr_arr_state_run(a, 0, len), len); + memset(end - a->cap, SCR_ARR_HOLE, a->cap - len); + } scr_mem_free(scr_arr_alloc_start(a)); a->data = block + room; head = room; @@ -782,10 +818,9 @@ static bool scr_arr_open_head(ScrArr *a, size_t count) { * within that gap past capacity. */ static bool scr_arr_dense_tail_populated(const ScrArr *a) { size_t window = a->cap < SCR_ARR_DENSE_GAP ? a->cap : SCR_ARR_DENSE_GAP; - const uint8_t *states = scr_arr_states_end(a); - if (window == 0 || *scr_arr_state_in(states, a->cap - 1) == SCR_ARR_HOLE) return false; + if (window == 0 || scr_arr_dense_state(a, a->cap - 1) == SCR_ARR_HOLE) return false; size_t populated = 0; - for (size_t i = a->cap - window; i < a->cap; i++) populated += *scr_arr_state_in(states, i) != SCR_ARR_HOLE; + for (size_t i = a->cap - window; i < a->cap; i++) populated += scr_arr_dense_state(a, i) != SCR_ARR_HOLE; return populated * 16 >= window * 3; } @@ -859,7 +894,7 @@ static __attribute__((noinline, pure)) ScrArrStateSlot scr_arr_sparse_state_at(c * sparse entries above it. */ static inline uint8_t scr_arr_state_at(const ScrArr *a, size_t index, uint64_t *out) { if (index < a->cap) { - uint8_t state = *scr_arr_state_in(scr_arr_states_end(a), index); + uint8_t state = scr_arr_dense_state(a, index); if (state == SCR_ARR_HOLE) return SCR_ARR_HOLE; if (out) *out = scr_arr_dense_get(a, index); return state; @@ -873,7 +908,8 @@ static inline uint8_t scr_arr_state_at(const ScrArr *a, size_t index, uint64_t * * element-kind test. */ static inline uint8_t scr_arr_f64_state_at(const ScrArr *a, size_t index, uint64_t *out) { if (index < a->cap) { - uint8_t state = *scr_arr_state_in(scr_arr_states_end_w(a, 8), index); + uint8_t state = !scr_arr_has_states(a) ? (index < a->len ? SCR_ARR_VALUE : SCR_ARR_HOLE) + : *scr_arr_state_in(scr_arr_states_end_w(a, 8), index); if (state == SCR_ARR_HOLE) return SCR_ARR_HOLE; *out = a->data[index]; return state; @@ -887,15 +923,19 @@ static inline bool scr_arr_slot_at(const ScrArr *a, size_t index, uint64_t *out) return scr_arr_state_at(a, index, out) != SCR_ARR_HOLE; } -/* Store an owned slot into an empty index. The caller has already established - * the destination length and does not need a retain. */ +/* Store an owned slot into an empty index. The caller does not need a + * retain. An array with states has its destination length established + * already; an array without states takes only a value at its length, + * whose caller then raises the length, and gets states for any other + * store. */ static void scr_arr_store_state_owned(ScrArr *a, size_t index, uint64_t slot, uint8_t state) { if (state != SCR_ARR_VALUE) scr_arr_cut(a, index); + if (!scr_arr_has_states(a) && (state != SCR_ARR_VALUE || index != a->len)) scr_arr_add_states(a); scr_arr_grow_for_index(a, index); if (index < a->cap) { scr_arr_dense_put(a, index, slot); - *scr_arr_state_ptr(a, index) = state; + if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, index) = state; return; } size_t pos = scr_arr_sparse_lower_bound(SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, index); @@ -911,12 +951,15 @@ static void scr_arr_store_owned(ScrArr *a, size_t index, uint64_t slot) { scr_arr_store_state_owned(a, index, slot, SCR_ARR_VALUE); } +/* Take the slot at `index`, leaving a hole: an array without states gets + * them. */ static bool scr_arr_take_state(ScrArr *a, size_t index, uint64_t *out, uint8_t *state_out) { if (index < a->cap) { - uint8_t state = *scr_arr_state_ptr(a, index); + uint8_t state = scr_arr_dense_state(a, index); if (state == SCR_ARR_HOLE) return false; scr_arr_cut(a, index); + scr_arr_need_states(a); *out = scr_arr_dense_get(a, index); *scr_arr_state_ptr(a, index) = SCR_ARR_HOLE; scr_arr_dense_put(a, index, 0); @@ -941,11 +984,19 @@ static void scr_arr_replace_state_owned(ScrArr *a, size_t index, uint64_t slot, uint8_t old_state; uint64_t old = 0; if (state != SCR_ARR_VALUE) scr_arr_cut(a, index); + /* Without states, a value below the length replaces a value. */ + if (!scr_arr_has_states(a) && (state != SCR_ARR_VALUE || index >= a->len)) { + if (state == SCR_ARR_VALUE && index == a->len) { + scr_arr_store_state_owned(a, index, slot, state); + return; + } + scr_arr_add_states(a); + } if (index < a->cap) { - old_state = *scr_arr_state_ptr(a, index); + old_state = scr_arr_dense_state(a, index); if (old_state == SCR_ARR_VALUE) old = scr_arr_dense_get(a, index); scr_arr_dense_put(a, index, slot); - *scr_arr_state_ptr(a, index) = state; + if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, index) = state; } else { size_t pos = scr_arr_sparse_lower_bound(SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, index); if (pos == SCR_AX(a)->sparse_len || SCR_AX(a)->sparse[pos].index != index) { @@ -973,6 +1024,10 @@ static void scr_arr_note_value(ScrArr *a, size_t index) { /* scr_arr_refill bounded to [0, limit): for range operations whose cost is * proportional to the range, not to the whole array. */ static void scr_arr_refill_to(ScrArr *a, size_t limit) { + if (!scr_arr_has_states(a)) { + scr_arr_refill(a); + return; + } size_t end = a->len < a->cap ? a->len : a->cap; if (limit < end) end = limit; size_t f = a->filled; @@ -1054,7 +1109,11 @@ static bool scr_arr_prop_delete(ScrArr *a, double key) { return true; } +/* Detach the storage of `a` for a rebuild through the element stores. Both + * have states: the detached storage for its readers, and the array for + * stores at any index. */ static ScrArrStorage scr_arr_take_storage(ScrArr *a) { + scr_arr_need_states(a); ScrArrExt *x = scr_arr_ext_of(a); ScrArrStorage old = {a->data, scr_arr_states_end(a), a->cap, SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, SCR_AX(a)->sparse_cap, a->elem, @@ -1091,17 +1150,20 @@ static bool scr_arr_is_dense(const ScrArr *a) { return a->len <= a->cap && (!(a->spare & SCR_ARR_HAS_EXT) || scr_arr_ext_of(a)->sparse_len == 0); } +/* Without states, both move the values only: the caller keeps the + * elements contiguous from 0 and sets the length to their count. */ static void scr_arr_move_dense(ScrArr *a, size_t to, size_t from, size_t count) { if (count == 0) return; scr_arr_cut(a, to); memmove(scr_arr_dense_at(a, to), scr_arr_dense_at(a, from), count * scr_arr_slot_width(a)); - memmove(scr_arr_state_run(a, to, count), scr_arr_state_run(a, from, count), count); + if (scr_arr_has_states(a)) + memmove(scr_arr_state_run(a, to, count), scr_arr_state_run(a, from, count), count); } static void scr_arr_clear_dense(ScrArr *a, size_t from, size_t count) { if (count == 0) return; scr_arr_cut(a, from); - memset(scr_arr_state_run(a, from, count), SCR_ARR_HOLE, count); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, from, count), SCR_ARR_HOLE, count); memset(scr_arr_dense_at(a, from), 0, count * scr_arr_slot_width(a)); } @@ -1170,10 +1232,20 @@ static inline void scr_arr_copy_slots(uint64_t *dst, const uint64_t *src, size_t } } +/* Whether dense slots [from, from + count) of `a` all hold values. */ +static bool scr_arr_dense_run_values(const ScrArr *a, size_t from, size_t count) { + if (!scr_arr_has_states(a)) return from + count <= a->len; + if (from + count <= a->filled) return true; + return scr_arr_value_run(scr_arr_states_end(a), from, from + count) == from + count; +} + /* The destination owns no values in this range. Only retain adapters run * while copying references: no release can collect an intermediate copy. * Self-appends use disjoint ranges after reserving capacity. Both arrays - * hold the same element kind, so their slots have the same width. */ + * hold the same element kind, so their slots have the same width. A + * destination without states keeps none for a range of values: its caller + * makes the range contiguous with its elements and sets the length; any + * other range gives it states. */ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, size_t from, size_t count, bool reverse, bool materialize_holes) { @@ -1182,28 +1254,53 @@ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, /* 8-byte slots: a constant slot size, and constant-width state ends. */ size_t width = scr_arr_slot_width(dst); bool wide = width == sizeof(uint64_t); + /* Callers copy from an array without states only from its elements + * (which a self-copy reads after moving them, past the length). */ + bool values = !scr_arr_has_states(src) || scr_arr_dense_run_values(src, from, count); + if (!values) scr_arr_need_states(dst); + bool refs = scr_elem_is_ref(src->elem); + if (!scr_arr_has_states(dst)) { + if (!reverse && wide) { + scr_arr_copy_slots(dst->data + to, src->data + from, count); + } else if (!reverse) { + memcpy(scr_arr_dense_at(dst, to), scr_arr_dense_at(src, from), count * width); + } else { + for (size_t i = 0; i < count; i++) + scr_arr_dense_put(dst, to + i, scr_arr_dense_get(src, from + count - i - 1)); + } + if (!refs) return; + uint8_t elem = src->elem; + const ScrArrOps *ops = scr_arr_ops(src); + if (wide) + for (size_t i = to; i < to + count; i++) dst->data[i] = scr_elem_retain_with(elem, ops, dst->data[i]); + else + for (size_t i = to; i < to + count; i++) + scr_arr_dense_put(dst, i, scr_elem_retain_with(elem, ops, scr_arr_dense_get(dst, i))); + return; + } uint8_t *dst_states = wide ? scr_arr_states_end_w(dst, 8) : scr_arr_states_end_w(dst, width); - const uint8_t *src_states = wide ? scr_arr_states_end_w(src, 8) : scr_arr_states_end_w(src, width); + const uint8_t *src_states = values ? NULL : wide ? scr_arr_states_end_w(src, 8) : scr_arr_states_end_w(src, width); if (!reverse && wide) { scr_arr_copy_slots(dst->data + to, src->data + from, count); - /* A range below the source's hole-free prefix holds only values. */ - if (from + count <= src->filled) + if (values) scr_arr_set_states(dst_states - to - count, SCR_ARR_VALUE, count); else scr_arr_copy_states(dst_states - to - count, src_states - from - count, count); } else if (!reverse) { memcpy(scr_arr_dense_at(dst, to), scr_arr_dense_at(src, from), count * scr_arr_slot_width(dst)); - scr_arr_copy_states(dst_states - to - count, src_states - from - count, count); + if (values) + scr_arr_set_states(dst_states - to - count, SCR_ARR_VALUE, count); + else + scr_arr_copy_states(dst_states - to - count, src_states - from - count, count); } else { for (size_t i = 0; i < count; i++) { size_t source = from + count - i - 1; scr_arr_dense_put(dst, to + i, scr_arr_dense_get(src, source)); - *scr_arr_state_in(dst_states, to + i) = *scr_arr_state_in(src_states, source); + *scr_arr_state_in(dst_states, to + i) = values ? SCR_ARR_VALUE : *scr_arr_state_in(src_states, source); } } - bool refs = scr_elem_is_ref(src->elem); - if (!refs && !materialize_holes) return; + if (!refs && (!materialize_holes || values)) return; uint8_t elem = src->elem; const ScrArrOps *ops = scr_arr_ops(src); for (size_t i = to; i < to + count; i++) { @@ -1225,19 +1322,12 @@ static void scr_arr_put_slots(ScrArr *a, size_t to, size_t count, const uint64_t for (size_t i = 0; i < count; i++) scr_arr_dense_put(a, to + i, slots[i]); } -/* The slot unit of an array that scr_arr_new, scr_arr_new_small or - * scr_arr_new_inline makes. Their kinds have no element ops, so they are - * never SCR_ELEM_CREF (scr_arr_new_ops makes those). */ -static size_t scr_arr_plain_slot_unit(ScrElemKind elem) { - return elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; -} - /* Inlined into each constructor, so `small` and the slot unit fold to * constants there. */ static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind elem, size_t initial_cap, bool small) { - size_t unit = scr_arr_plain_slot_unit(elem); + size_t unit = scr_elem_slot_width(elem); size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, unit) : scr_arr_known_cap(SCR_ARR_INLINE_OFFSET, initial_cap, unit); ScrArr *a = scr_mem_alloc(inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr)); @@ -1265,13 +1355,18 @@ ScrArr *scr_arr_new(ScrElemKind elem, size_t initial_cap) { ScrArr *scr_arr_new_small(ScrElemKind elem) { return scr_arr_make(elem, 0, true); } /* A non-cycle array whose final length the runtime knows when it builds - * it (regex match results): its `cap` slots and states share the header's - * block, so construction and teardown cost one allocation. Growth past + * it (regex match results): its `cap` slots share the header's block, so + * construction and teardown cost one allocation. With `states`, the array + * starts with states, whose bytes share the block as well. Growth past * `cap` moves the slots out like any inline array. */ -ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { - if (cap == 0) return scr_arr_make(elem, 0, false); +ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap, bool states) { + if (cap == 0) { + ScrArr *a = scr_arr_make(elem, 0, false); + if (states) a->spare |= SCR_ARR_STATES; + return a; + } if (cap > SCR_ARR_MAX_LENGTH) scr_arr_oom(); - size_t unit = scr_arr_plain_slot_unit(elem); + size_t unit = scr_elem_slot_width(elem) + (states ? 1 : 0); ScrArr *a = scr_mem_alloc(SCR_ARR_INLINE_OFFSET + cap * unit); if (!a) scr_arr_oom(); a->rc = 1; @@ -1279,7 +1374,7 @@ ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { a->cap = 0; a->elem = (uint8_t)elem; a->cyc = 0; - a->spare = 0; + a->spare = states ? SCR_ARR_STATES : 0; a->filled = 0; a->data = NULL; scr_arr_use_inline(a, cap, unit); @@ -1290,8 +1385,9 @@ ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { } void scr_arr_outline(ScrArr *a) { - if (scr_arr_inline(a)) scr_arr_move_inline(a, a->cap, scr_arr_slot_unit(a)); - /* The swapped fields do not include `spare`: no head room either. */ + if (scr_arr_inline(a)) scr_arr_move_inline(a, a->cap); + /* Of `spare`, a swap of the storage fields may move only the states + * flag: no head room either. */ if (a->spare & SCR_ARR_HEAD_ROOM) { scr_arr_drop_head(a); scr_arr_absorb_sparse(a); @@ -1325,9 +1421,14 @@ void scr_arr_trace_v(void *a0, ScrTraceVisit visit, void *ctx) { if (SCR_AX(a)->metadata) visit(SCR_AX(a)->metadata, ctx); /* Growth and truncation leave spare capacity empty. Sparse slots and * named properties are separate edges, independent of dense length. */ - const uint8_t *states = scr_arr_states_end(a); - for (size_t i = 0, end = a->len < a->cap ? a->len : a->cap; i < end; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) visit(scr_slot_to_ptr(a->data[i]), ctx); + size_t end = a->len < a->cap ? a->len : a->cap; + if (!scr_arr_has_states(a)) { + for (size_t i = 0; i < end; i++) visit(scr_slot_to_ptr(a->data[i]), ctx); + } else { + const uint8_t *states = scr_arr_states_end(a); + for (size_t i = 0; i < end; i++) { + if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) visit(scr_slot_to_ptr(a->data[i]), ctx); + } } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE) { @@ -1376,7 +1477,8 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops_kind(const uint8_t elem) { ScrArr *a; bool cyc = ops->trace != NULL; - size_t unit = scr_elem_slot_unit(elem); + /* The array starts without states: slots of the value width alone. */ + size_t unit = scr_elem_slot_width(elem); size_t prefix = SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0); size_t inline_cap = small ? scr_arr_small_cap(prefix, unit) : scr_arr_known_cap(prefix, initial_cap, unit); @@ -1441,18 +1543,28 @@ static void scr_arr_destroy(void *object) { * first (and the storage cannot move under the loop); immortal strings * (interned characters, literals) need no call. */ const uint64_t *data = a->data; - const uint8_t *states = scr_arr_states_end_w(a, 8); - /* Slots below the hole-free prefix bound hold values. */ - size_t filled = a->filled < end ? a->filled : end; + /* Slots below the hole-free prefix bound hold values, as do all + * slots below the length of an array without states. */ + size_t filled = !scr_arr_has_states(a) ? end : a->filled < end ? a->filled : end; for (size_t i = 0; i < filled; i++) { ScrStr *s = (ScrStr *)scr_slot_to_ptr(data[i]); if (s && s->rc != SIZE_MAX) scr_str_release(s); } + const uint8_t *states = scr_arr_states_end_w(a, 8); for (size_t i = filled; i < end; i++) { if (*scr_arr_state_in(states, i) != SCR_ARR_VALUE) continue; ScrStr *s = (ScrStr *)scr_slot_to_ptr(data[i]); if (s && s->rc != SIZE_MAX) scr_str_release(s); } + } else if (!scr_arr_has_states(a)) { + /* Each slot is cleared before its release, as with states. */ + uint8_t elem = a->elem; + const ScrArrOps *ops = scr_arr_ops(a); + for (size_t i = 0; i < end; i++) { + uint64_t old = a->data[i]; + a->data[i] = 0; + scr_elem_release_with(elem, ops, old); + } } else { uint8_t *states = scr_arr_states_end(a); uint8_t elem = a->elem; @@ -1535,6 +1647,7 @@ double scr_arr_next_present(const ScrArr *a, double start) { if (!(start < SCR_ARR_LEN_F64(a->len))) return SCR_ARR_LEN_F64(a->len); size_t i = start > 0 ? (size_t)ceil(start) : 0; size_t end = a->cap < a->len ? a->cap : a->len; + if (!scr_arr_has_states(a)) return (double)i; const uint8_t *states = scr_arr_states_end(a); for (; i < end; i++) { if (*scr_arr_state_in(states, i) != SCR_ARR_HOLE) return (double)i; @@ -1573,6 +1686,22 @@ void scr_arr_set_len(ScrArr *a, double length) { * array must cost only the elements removed, not its historical peak. */ size_t dense_end = a->len < a->cap ? a->len : a->cap; size_t dense_stop = next < dense_end ? next : dense_end; + if (!scr_arr_has_states(a)) { + /* Values only: each slot is cleared before its release. The + * length drops first, so a release that reads the array sees the + * values that remain. */ + a->len = (uint32_t)next; + if (scr_elem_is_ref(a->elem)) { + for (size_t i = dense_stop; i < dense_end; i++) { + uint64_t old = a->data[i]; + a->data[i] = 0; + scr_elem_release(a, old); + } + } else if (dense_stop < dense_end) { + memset(scr_arr_dense_at(a, dense_stop), 0, (dense_end - dense_stop) * scr_arr_slot_width(a)); + } + return; + } if (scr_elem_is_ref(a->elem)) { for (size_t i = dense_stop; i < dense_end; i++) { if (*scr_arr_state_ptr(a, i) != SCR_ARR_HOLE) { @@ -1601,8 +1730,20 @@ void scr_arr_set_len(ScrArr *a, double length) { } if (scr_arr_ext_of(a)) scr_arr_ext_of(a)->sparse_len = scr_arr_sparse_lower_bound(scr_arr_ext_of(a)->sparse, scr_arr_ext_of(a)->sparse_len, next); } - } else if (a->len == 0 && next <= SCR_ARR_PRESIZE_LIMIT && (a->cap == 0 || scr_arr_inline(a))) { - scr_arr_reserve_exact(a, next); + } else if (next > a->len) { + /* The indices past the elements are holes. An empty array gives up + * its inline slots first, so its slots and states take one allocation. */ + if (a->len == 0 && next <= SCR_ARR_PRESIZE_LIMIT && (a->cap == 0 || scr_arr_inline(a))) { + if (scr_arr_inline(a)) { + a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS; + a->cap = 0; + a->data = NULL; + } + scr_arr_need_states(a); + scr_arr_reserve_exact(a, next); + } else { + scr_arr_need_states(a); + } } a->len = next; } @@ -1633,11 +1774,24 @@ void scr_arr_copy_range_ex(ScrArr *dst, size_t dst_start, const ScrArr *src, scr_arr_refill_to(dst, dst_start + count); } +/* A destination without states takes a dense copy at its length, whose + * caller then raises the length over the copy; any other copy gives it + * states first. */ static void scr_arr_copy_range_states(ScrArr *dst, size_t dst_start, const ScrArr *src, size_t src_start, size_t count, bool reverse, bool materialize_holes) { - if (src != dst && src_start <= src->cap && count <= src->cap - src_start && - dst_start <= dst->cap && count <= dst->cap - dst_start) { + size_t src_end = scr_arr_has_states(src) ? src->cap : src->len; + bool fits = src != dst && src_start <= src_end && count <= src_end - src_start && + dst_start <= dst->cap && count <= dst->cap - dst_start; + if (!scr_arr_has_states(dst)) { + if (fits && dst_start == dst->len) { + scr_arr_copy_dense(dst, dst_start, src, src_start, count, reverse, materialize_holes); + return; + } + scr_arr_add_states(dst); + fits = fits && dst_start <= dst->cap && count <= dst->cap - dst_start; + } + if (fits) { const uint8_t *dst_states = scr_arr_states_end(dst); size_t empty = 0; while (empty < count && *scr_arr_state_in(dst_states, dst_start + empty) == SCR_ARR_HOLE) empty++; @@ -1723,12 +1877,13 @@ static ScrArr *scr_arr_fill_slot(ScrArr *a, uint64_t slot, uint8_t state, if (from >= until) return scr_arr_retain(a); bool refs = scr_elem_is_ref(a->elem); if (scr_arr_dense_ok(a, until, until - from)) scr_arr_grow_dense(a, until); + if (state != SCR_ARR_VALUE) scr_arr_need_states(a); if (!refs && until <= a->cap) { if (state != SCR_ARR_VALUE) scr_arr_cut(a, from); if (a->elem == SCR_ELEM_BOOL) memset(scr_arr_dense_at(a, from), (int)slot, until - from); else if (a->elem == SCR_ELEM_CREF) for (size_t i = from; i < until; i++) scr_arr_dense_put(a, i, slot); else for (size_t i = from; i < until; i++) a->data[i] = slot; - memset(scr_arr_state_run(a, from, until - from), state, until - from); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, from, until - from), state, until - from); } else { for (size_t i = from; i < until; i++) { uint64_t value = refs && state == SCR_ARR_VALUE ? scr_elem_retain_slot(a, slot) : slot; @@ -1882,7 +2037,7 @@ void *scr_arr_peek_ref(const ScrArr *a, double i) { size_t idx = (size_t)i; if ((double)idx == i) { if (idx < a->filled) return scr_slot_to_ptr(scr_arr_dense_get(a, idx)); - if (idx >= a->len || *scr_arr_state_ptr(a, idx) != SCR_ARR_VALUE) return NULL; + if (idx >= a->len || scr_arr_dense_state(a, idx) != SCR_ARR_VALUE) return NULL; return scr_slot_to_ptr(scr_arr_dense_get(a, idx)); } } @@ -1912,8 +2067,8 @@ bool scr_arr_index_eq(const ScrArr *a, double i, const ScrArr *b, double j) { if (i >= 0 && i < (double)a->cap && i == trunc(i) && j >= 0 && j < (double)b->cap && j == trunc(j)) { size_t ai = (size_t)i, bi = (size_t)j; - bool ap = ai < a->len && *scr_arr_state_ptr(a, ai) == SCR_ARR_VALUE; - bool bp = bi < b->len && *scr_arr_state_ptr(b, bi) == SCR_ARR_VALUE; + bool ap = ai < a->len && scr_arr_dense_state(a, ai) == SCR_ARR_VALUE; + bool bp = bi < b->len && scr_arr_dense_state(b, bi) == SCR_ARR_VALUE; if (!ap || !bp) return ap == bp; av = scr_arr_dense_get(a, ai); bv = scr_arr_dense_get(b, bi); @@ -1953,6 +2108,17 @@ static void scr_arr_set_slot(ScrArr *a, double i, uint64_t slot) { scr_arr_prop_set(a, i, slot); return; } + if (!scr_arr_has_states(a) && idx >= a->len) { + /* An append keeps the array without states; a write past the length + * leaves holes. */ + if (idx == a->len) { + scr_arr_store_owned(a, idx, slot); + a->len = (uint32_t)(idx + 1); + scr_arr_note_value(a, idx); + return; + } + scr_arr_add_states(a); + } if (idx >= a->len) a->len = idx + 1; /* Unlink-then-release: a release can trigger a cycle collection, which * must never see a heap edge whose count was already given up. */ @@ -1978,6 +2144,7 @@ void scr_arr_set_undefined(ScrArr *a, double i) { scr_arr_prop_set_undefined(a, i); return; } + scr_arr_need_states(a); if (idx >= a->len) a->len = idx + 1; scr_arr_replace_state_owned(a, idx, 0, SCR_ARR_UNDEFINED); } @@ -2027,7 +2194,7 @@ double scr_arr_push_many(ScrArr *a, size_t count, const uint64_t *slots) { scr_arr_grow_dense(a, next); if (count) { scr_arr_put_slots(a, from, count, slots); - scr_arr_set_states(scr_arr_state_run(a, from, count), SCR_ARR_VALUE, count); + if (scr_arr_has_states(a)) scr_arr_set_states(scr_arr_state_run(a, from, count), SCR_ARR_VALUE, count); } a->len = (uint32_t)next; /* The appended states are values: a bound at the old length moves to @@ -2036,6 +2203,7 @@ double scr_arr_push_many(ScrArr *a, size_t count, const uint64_t *slots) { else scr_arr_refill(a); return (double)next; } + scr_arr_need_states(a); for (size_t i = 0; i < count; i++) scr_arr_store_owned(a, from + i, slots[i]); a->len = (uint32_t)next; scr_arr_refill(a); @@ -2072,6 +2240,7 @@ double scr_arr_push_spread(ScrArr *a, const ScrArr *src) { scr_arr_refill(a); return SCR_ARR_LEN_F64(a->len); } + scr_arr_need_states(a); a->len = old_len + add; for (size_t i = 0; i < add; i++) { uint64_t slot; @@ -2116,9 +2285,10 @@ double scr_arr_concat_copy(ScrArr *a, const ScrArr *src) { scr_arr_refill(a); return SCR_ARR_LEN_F64(a->len); } + scr_arr_need_states(a); a->len = old_len + add; for (size_t i = 0; i < src->cap; i++) { - if (*scr_arr_state_ptr(src, i) != SCR_ARR_HOLE) { + if (scr_arr_dense_state(src, i) != SCR_ARR_HOLE) { scr_arr_copy_index(a, old_len + i, src, i); } } @@ -2172,7 +2342,7 @@ static double scr_arr_unshift_slot(ScrArr *a, uint64_t slot) { /* The prefix stays put in front of the new first value. */ size_t f = a->filled; a->data[0] = slot; - *scr_arr_state_ptr(a, 0) = SCR_ARR_VALUE; + if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, 0) = SCR_ARR_VALUE; a->len++; a->filled = (uint32_t)(f + 1); return SCR_ARR_LEN_F64(a->len); @@ -2183,7 +2353,7 @@ static double scr_arr_unshift_slot(ScrArr *a, uint64_t slot) { size_t f = a->filled; scr_arr_move_dense(a, 1, 0, old_len); scr_arr_dense_put(a, 0, slot); - *scr_arr_state_ptr(a, 0) = SCR_ARR_VALUE; + if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, 0) = SCR_ARR_VALUE; a->len++; a->filled = (uint32_t)(f + 1); return SCR_ARR_LEN_F64(a->len); @@ -2224,7 +2394,7 @@ double scr_arr_unshift_many(ScrArr *a, size_t count, const uint64_t *slots) { if (scr_arr_is_dense(a) && scr_arr_open_head(a, count)) { size_t f = a->filled; scr_arr_put_slots(a, 0, count, slots); - memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); a->len = old_len + count; a->filled = (uint32_t)(f + count); return SCR_ARR_LEN_F64(a->len); @@ -2234,7 +2404,7 @@ double scr_arr_unshift_many(ScrArr *a, size_t count, const uint64_t *slots) { size_t f = a->filled; scr_arr_move_dense(a, count, 0, old_len); scr_arr_put_slots(a, 0, count, slots); - memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); a->len = old_len + count; a->filled = (uint32_t)(f + count); return SCR_ARR_LEN_F64(a->len); @@ -2261,6 +2431,10 @@ double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src) { size_t add = src->len; if (add == 0) return (double)old_len; if (add > SCR_ARR_MAX_LENGTH - old_len) scr_arr_oom(); + /* Holes and present undefined in the source become present undefined: + * the receiver needs states, before its elements move. */ + if (!scr_arr_has_states(a) && (!scr_arr_is_dense(src) || !scr_arr_dense_run_values(src, 0, add))) + scr_arr_add_states(a); if (scr_arr_is_dense(a) && scr_arr_is_dense(src) && scr_arr_open_head(a, add)) { /* A self-spread reads the original values behind the opened room. */ scr_arr_copy_dense(a, 0, src, src == a ? add : 0, add, false, true); @@ -2308,6 +2482,15 @@ double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src) { ScrArr *scr_arr_reverse(ScrArr *a) { size_t len = a->len; + if (!scr_arr_has_states(a)) { + for (size_t i = 0; i < len / 2; i++) { + size_t j = len - i - 1; + uint64_t slot = scr_arr_dense_get(a, i); + scr_arr_dense_put(a, i, scr_arr_dense_get(a, j)); + scr_arr_dense_put(a, j, slot); + } + return scr_arr_retain(a); + } if (scr_arr_is_dense(a)) { /* A hole-free array stays hole-free; any other prefix is recomputed. */ if (a->filled != len) scr_arr_cut(a, 0); @@ -2343,6 +2526,13 @@ uint8_t scr_arr_pop_state(ScrArr *a, uint64_t *slot_out) { if (slot_out) *slot_out = 0; if (a->len == 0) return SCR_ARR_HOLE; size_t idx = a->len - 1; + if (!scr_arr_has_states(a)) { + if (slot_out) *slot_out = scr_arr_dense_get(a, idx); + scr_arr_dense_put(a, idx, 0); + scr_arr_cut(a, idx); + a->len = (uint32_t)idx; + return SCR_ARR_VALUE; + } uint64_t slot = 0; uint8_t state = SCR_ARR_HOLE; if (!scr_arr_take_state(a, idx, &slot, &state)) state = SCR_ARR_HOLE; @@ -2378,7 +2568,7 @@ uint8_t scr_arr_shift_state(ScrArr *a, uint64_t *slot_out) { if (a->len == 0) return SCR_ARR_HOLE; size_t old_len = a->len; if (scr_arr_is_dense(a)) { - uint8_t state = *scr_arr_state_ptr(a, 0); + uint8_t state = scr_arr_dense_state(a, 0); if (slot_out && state == SCR_ARR_VALUE) *slot_out = scr_arr_dense_get(a, 0); /* The first slot becomes head room; the tail stays where it is. */ if (scr_arr_drop_front(a, 1)) return state; @@ -2449,13 +2639,15 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) { scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), hint ? hint : 1) : scr_arr_new(a->elem, hint ? hint : 1); - out->len = n; size_t old_len = a->len; if (scr_arr_is_dense(a)) { if (n) { + /* The removed elements keep their states unless all are values. */ + if (!scr_arr_dense_run_values(a, from, n)) scr_arr_add_states(out); memcpy(out->data, scr_arr_dense_at(a, from), n * scr_arr_slot_width(a)); - memcpy(scr_arr_state_run(out, 0, n), scr_arr_state_run(a, from, n), n); + if (scr_arr_has_states(out)) memcpy(scr_arr_state_run(out, 0, n), scr_arr_state_run(a, from, n), n); } + out->len = n; scr_arr_refill(out); /* A removed prefix becomes head room. */ if (from == 0 && scr_arr_drop_front(a, n)) return out; @@ -2469,6 +2661,8 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) { else scr_arr_refill(a); return out; } + scr_arr_add_states(out); + out->len = n; ScrArrStorage old = scr_arr_take_storage(a); a->len = 0; for (size_t i = 0; i < old.cap; i++) { @@ -2526,12 +2720,17 @@ ScrArr *scr_arr_splice_insert(ScrArr *a, double start, double deleteCount, * is needed only by direct self-spread runtime callers. */ ScrArr *snapshot = items == a ? scr_arr_slice(a, 0, INFINITY) : NULL; if (snapshot) items = snapshot; + /* Holes and present undefined among the items become present + * undefined: the receiver needs states, before its elements move. */ + if (!scr_arr_dense_run_values(items, 0, add)) scr_arr_need_states(a); ScrArr *removed = scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), n) : scr_arr_new(a->elem, n); if (n) { + if (!scr_arr_dense_run_values(a, from, n)) scr_arr_add_states(removed); memcpy(removed->data, scr_arr_dense_at(a, from), n * scr_arr_slot_width(a)); - memcpy(scr_arr_state_run(removed, 0, n), scr_arr_state_run(a, from, n), n); + if (scr_arr_has_states(removed)) + memcpy(scr_arr_state_run(removed, 0, n), scr_arr_state_run(a, from, n), n); } removed->len = n; scr_arr_refill(removed); @@ -2589,9 +2788,8 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou removed = scr_mem_alloc(n * sizeof(uint64_t)); if (!removed) scr_arr_oom(); } - const uint8_t *states = scr_arr_states_end(a); for (size_t i = from; i < from + n; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) removed[kept++] = a->data[i]; + if (scr_arr_dense_state(a, i) == SCR_ARR_VALUE) removed[kept++] = a->data[i]; } } size_t old_len = a->len; @@ -2606,7 +2804,7 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou scr_arr_move_dense(a, from + count, from + n, old_len - from - n); if (count) { scr_arr_put_slots(a, from, count, slots); - memset(scr_arr_state_run(a, from, count), SCR_ARR_VALUE, count); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, from, count), SCR_ARR_VALUE, count); } if (next_len < old_len) scr_arr_clear_dense(a, next_len, old_len - next_len); a->len = next_len; @@ -2631,13 +2829,15 @@ ScrArr *scr_arr_flat_copy(const ScrArr *a, ScrArr *out, bool flatten) { for (size_t j = 0; j < inner->len; j++) { if (scr_arr_state_at(inner, j, NULL) == SCR_ARR_HOLE) continue; if (out->len == SCR_ARR_MAX_LENGTH) scr_arr_oom(); - size_t at = out->len++; + size_t at = out->len; scr_arr_copy_index(out, at, inner, j); + out->len = (uint32_t)(at + 1); } } else { if (out->len == SCR_ARR_MAX_LENGTH) scr_arr_oom(); - size_t at = out->len++; + size_t at = out->len; scr_arr_copy_index(out, at, a, i); + out->len = (uint32_t)(at + 1); } } scr_arr_refill(out); @@ -2662,10 +2862,12 @@ static bool scr_arr_ref_eq(const ScrArr *a, uint64_t slot, void *v) { return p == v; } +#define SCR_ARR_SCAN_VALUE(states, i) (!(states) || *scr_arr_state_in((states), (i)) == SCR_ARR_VALUE) + double scr_arr_index_of_f64(ScrArr *a, double v) { - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && scr_slot_to_f64(a->data[i]) == v) return (double)i; + if (SCR_ARR_SCAN_VALUE(states, i) && scr_slot_to_f64(a->data[i]) == v) return (double)i; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE && scr_slot_to_f64(SCR_AX(a)->sparse[i].slot) == v) return (double)SCR_AX(a)->sparse[i].index; @@ -2674,9 +2876,9 @@ double scr_arr_index_of_f64(ScrArr *a, double v) { } double scr_arr_index_of_bool(ScrArr *a, bool v) { - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && ((scr_arr_dense_get(a, i) != 0) == v)) return (double)i; + if (SCR_ARR_SCAN_VALUE(states, i) && ((scr_arr_dense_get(a, i) != 0) == v)) return (double)i; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE && (SCR_AX(a)->sparse[i].slot != 0) == v) return (double)SCR_AX(a)->sparse[i].index; @@ -2685,10 +2887,9 @@ double scr_arr_index_of_bool(ScrArr *a, bool v) { } double scr_arr_index_of_ref(ScrArr *a, void *v) { - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && scr_arr_ref_eq(a, scr_arr_dense_get(a, i), v)) - return (double)i; + if (SCR_ARR_SCAN_VALUE(states, i) && scr_arr_ref_eq(a, scr_arr_dense_get(a, i), v)) return (double)i; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE && scr_arr_ref_eq(a, SCR_AX(a)->sparse[i].slot, v)) return (double)SCR_AX(a)->sparse[i].index; @@ -2697,9 +2898,9 @@ double scr_arr_index_of_ref(ScrArr *a, void *v) { } bool scr_arr_includes_f64(ScrArr *a, double v) { - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) != SCR_ARR_VALUE) continue; + if (!SCR_ARR_SCAN_VALUE(states, i)) continue; double x = scr_slot_to_f64(a->data[i]); if (x == v || (x != x && v != v)) return true; /* SameValueZero: NaN hits */ } @@ -2719,9 +2920,9 @@ bool scr_arr_includes_ref(ScrArr *a, void *v) { if (a->elem == SCR_ELEM_REF && scr_arr_ops(a)->retain == scr_dyn_retain_v) { const ScrDyn *needle = (const ScrDyn *)v; if (needle->kind == SCR_DYN_NUM && needle->v.num != needle->v.num) { - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) != SCR_ARR_VALUE) continue; + if (!SCR_ARR_SCAN_VALUE(states, i)) continue; const ScrDyn *d = (const ScrDyn *)scr_slot_to_ptr(a->data[i]); if (d->kind == SCR_DYN_NUM && d->v.num != d->v.num) return true; } @@ -2782,23 +2983,27 @@ ScrArr *scr_arr_slice(ScrArr *a, double start, double end) { scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), hint ? hint : 1) : scr_arr_new(a->elem, hint ? hint : 1); - out->len = n; if (to <= a->cap && to <= a->filled && scr_arr_slot_width(a) == sizeof(uint64_t) && !scr_elem_is_ref(a->elem)) { /* Numbers below the source's hole-free prefix: values only. */ scr_arr_copy_slots(out->data, a->data + from, n); - scr_arr_set_states(scr_arr_states_end_w(out, 8) - n, SCR_ARR_VALUE, n); + out->len = (uint32_t)n; out->filled = (uint32_t)n; return out; } if (to <= a->cap) { scr_arr_copy_dense(out, 0, a, from, n, false, false); + out->len = (uint32_t)n; /* States copied from below the source's bound are present values. */ if (n > 0 && from < a->filled) out->filled = (uint32_t)((to < a->filled ? to : a->filled) - from); scr_arr_refill(out); return out; } + /* A range reaching the sparse entries: the source has states, and so + * does the copy. */ + scr_arr_add_states(out); + out->len = (uint32_t)n; const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < a->cap && i < to; i++) { if (i < from || *scr_arr_state_in(states, i) == SCR_ARR_HOLE) continue; @@ -2928,8 +3133,9 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { if (a->len == 0) return copy ? scr_arr_new(a->elem, 0) : scr_arr_retain(a); size_t dense = a->len < a->cap ? a->len : a->cap; size_t count = 0, undefined_count = 0; - const uint8_t *states = scr_arr_states_end(a); - for (size_t i = 0; i < dense; i++) { + const uint8_t *states = scr_arr_scan_states(a); + if (!states) count = dense; + for (size_t i = 0; states && i < dense; i++) { count += *scr_arr_state_in(states, i) == SCR_ARR_VALUE; undefined_count += *scr_arr_state_in(states, i) == SCR_ARR_UNDEFINED; } @@ -2942,7 +3148,7 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { if (count && !values) scr_arr_oom(); size_t used = 0; for (size_t i = 0; i < dense; i++) - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) values[used++] = scr_arr_dense_get(a, i); + if (SCR_ARR_SCAN_VALUE(states, i)) values[used++] = scr_arr_dense_get(a, i); for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE) values[used++] = SCR_AX(a)->sparse[i].slot; ScrNumberSortKey *numbers = NULL; @@ -2964,8 +3170,10 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { } size_t present = copy ? a->len : count + undefined_count; ScrArr *out = copy ? scr_arr_new(a->elem, present) : a; - if (copy) out->len = a->len; - if (!copy && scr_arr_is_dense(a)) { + if (!copy && !states) { + /* Values only, in place. */ + if (count) scr_arr_put_slots(a, 0, count, values); + } else if (!copy && scr_arr_is_dense(a)) { if (count) scr_arr_put_slots(a, 0, count, values); scr_arr_cut(a, count); memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); @@ -2973,14 +3181,20 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { scr_arr_clear_dense(a, count + undefined_count, a->len - count - undefined_count); if (undefined_count) memset(scr_arr_dense_at(a, count), 0, undefined_count * scr_arr_slot_width(a)); + } else if (copy) { + /* The values, then present undefined for every other index. */ + for (size_t i = 0; i < count; i++) values[i] = scr_elem_retain_slot(a, values[i]); + scr_arr_push_many(out, count, values); + for (size_t i = count; i < present; i++) { + scr_arr_store_state_owned(out, i, 0, SCR_ARR_UNDEFINED); + out->len = (uint32_t)(i + 1); + } } else { - ScrArrStorage old = {0}; - if (!copy) old = scr_arr_take_storage(a); - for (size_t i = 0; i < count; i++) - scr_arr_store_owned(out, i, copy ? scr_elem_retain_slot(a, values[i]) : values[i]); + ScrArrStorage old = scr_arr_take_storage(a); + for (size_t i = 0; i < count; i++) scr_arr_store_owned(out, i, values[i]); for (size_t i = count; i < present; i++) scr_arr_store_state_owned(out, i, 0, SCR_ARR_UNDEFINED); - if (!copy) scr_arr_free_storage(&old); + scr_arr_free_storage(&old); } free(values); scr_arr_refill(out); @@ -4659,7 +4873,7 @@ void scr_arr_sort_values(ScrArr *a, double count, ScrClosure *f, uint32_t arity, * holes, no storage-level undefined and no sparse tail. */ static bool scr_arr_dense_values(const ScrArr *a) { if (!scr_arr_is_dense(a)) return false; - if (a->filled == a->len) return true; + if (a->filled == a->len || !scr_arr_has_states(a)) return true; const uint8_t *states = scr_arr_states_end(a); bool all = true; for (size_t i = 0; i < a->len; i++) all &= *scr_arr_state_in(states, i) == SCR_ARR_VALUE; @@ -4738,18 +4952,16 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, in } else if (a->len >= n && a->cap >= n) { if (!retained) { memcpy(a->data, src, n * sizeof(*src)); - memset(scr_arr_state_run(a, 0, n), SCR_ARR_VALUE, n); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, 0, n), SCR_ARR_VALUE, n); } else { /* Publish each new edge before releasing the value it replaces: a * release can collect cycles. The buffer still holds the slots not - * yet written. The kind, and so the slot width, never changes. */ - size_t width = scr_arr_slot_width(a); + * yet written. */ for (size_t i = 0; i < n; i++) { - uint8_t *state = scr_arr_state_in(scr_arr_states_end_w(a, width), i); - bool held = *state == SCR_ARR_VALUE; + bool held = scr_arr_dense_state(a, i) == SCR_ARR_VALUE; uint64_t old = held ? a->data[i] : 0; a->data[i] = src[i]; - *state = SCR_ARR_VALUE; + if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, i) = SCR_ARR_VALUE; if (held) scr_elem_release(a, old); } } diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 67ff7d528..689eb63ee 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -106,7 +106,7 @@ typedef struct ScrArr { * header. Kept beside `elem` so the RC fast paths test it with one load. */ uint8_t cyc; /* The low SCR_ARR_OPS_SHIFT bits: storage flags (SCR_ARR_INLINE_SLOTS, - * SCR_ARR_HEAD_ROOM, SCR_ARR_UNSHIFTED, SCR_ARR_HAS_EXT). The rest: the + * SCR_ARR_HEAD_ROOM, SCR_ARR_UNSHIFTED, SCR_ARR_HAS_EXT, SCR_ARR_STATES). The rest: the * registry id of the element type's ops (scr_arr_ops), 0 for every kind * but SCR_ELEM_REF and SCR_ELEM_CREF. */ uint16_t spare; @@ -119,10 +119,11 @@ typedef struct ScrArr { uint32_t filled; /* Dense values: `cap` 8-byte slots, `cap` bytes for SCR_ELEM_BOOL or * `cap` 4-byte slots for SCR_ELEM_CREF (scr_arr_slot_width), followed in - * the same allocation by one state byte per slot in reverse slot order - * (scr_arr_states_end). Sparse entries and properties keep 8-byte slots - * for every kind; a bool slot is 0 or 1 wherever it lives, and a CREF - * slot's 8-byte form is the pointer (scr_arr_dense_get). Rarely + * the same allocation, when the array has states (SCR_ARR_STATES), by one + * state byte per slot in reverse slot order (scr_arr_states_end). Sparse + * entries and properties keep 8-byte slots for every kind; a bool slot is + * 0 or 1 wherever it lives, and a CREF slot's 8-byte form is the pointer + * (scr_arr_dense_get). Rarely * used storage (sparse indices, noncanonical numeric properties, * match-result metadata) lives out of line, and an array that has it * (SCR_ARR_HAS_EXT) keeps a pointer to it in the word before the dense @@ -150,14 +151,17 @@ typedef struct ScrArrExt { /* The LLVM backend mirrors ScrArr as %ScrArr: its inline retain/release read * `cyc` (field 4, i8) to decide mark-live / candidate buffering exactly like * scr_arr_retain and scr_arr_release; its inline element access uses len and - * cap (fields 1 and 2, i32), filled (field 6, i32) and data (field 7), and - * finds the state of slot i at data + cap * (slot width + 1) - 1 - i. */ + * cap (fields 1 and 2, i32), spare (field 5, i16, for SCR_ARR_STATES), + * filled (field 6, i32) and data (field 7), and finds the state of slot i + * of an array with states at data + cap * (slot width + 1) - 1 - i. */ _Static_assert(offsetof(ScrArr, len) == sizeof(size_t) && sizeof(((ScrArr *)0)->len) == 4, "emitted length reads load ScrArr.len as %ScrArr field 1, an i32"); _Static_assert(offsetof(ScrArr, cap) == sizeof(size_t) + 4 && sizeof(((ScrArr *)0)->cap) == 4, "emitted capacity reads load ScrArr.cap as %ScrArr field 2, an i32"); _Static_assert(offsetof(ScrArr, cyc) == sizeof(size_t) + 9, "inline RC fast paths read ScrArr.cyc as %ScrArr field 4"); +_Static_assert(offsetof(ScrArr, spare) == sizeof(size_t) + 10, + "inline element access reads ScrArr.spare as %ScrArr field 5"); _Static_assert(offsetof(ScrArr, filled) == sizeof(size_t) + 12, "inline element access reads ScrArr.filled as %ScrArr field 6"); _Static_assert(offsetof(ScrArr, data) == sizeof(size_t) + 16, @@ -171,11 +175,21 @@ _Static_assert(sizeof(ScrArr) == sizeof(size_t) + 16 + sizeof(void *), * count. Unshifted: an unshift had to move every element. Has ext: the word * before the dense allocation (head room included) points at the array's * ScrArrExt; such an array always has an allocation of its own, without - * inline slots. */ + * inline slots. States: the dense slots carry state bytes (see below). */ #define SCR_ARR_INLINE_SLOTS 1u #define SCR_ARR_HEAD_ROOM 2u #define SCR_ARR_UNSHIFTED 4u #define SCR_ARR_HAS_EXT 8u +#define SCR_ARR_STATES 16u + +/* An array starts without state bytes. Without them, every index below + * `len` is a present value in dense storage: len <= cap, there are no + * sparse entries, and `filled` equals `len` whenever emitted code can read + * the array (its inline reads load a state only at or past `filled`). The + * first hole or present `undefined` below the length, + * a length set past the elements, or a sparse entry gives the array its + * states (scr_arr_add_states), which it keeps. */ +static inline bool scr_arr_has_states(const ScrArr *a) { return (a->spare & SCR_ARR_STATES) != 0; } /* The start of the dense slots' allocation: `data` less any head room. */ static inline uint64_t *scr_arr_block(const ScrArr *a) { @@ -199,7 +213,7 @@ static inline struct ScrArrExt *scr_arr_ext_of(const ScrArr *a) { * Id 0 is no ops; ids are assigned on each descriptor's first array, in * any thread. A program with more element types than the field holds keeps * the ops of the rest in the array's ext (SCR_ARR_OPS_OVERFLOW). */ -#define SCR_ARR_OPS_SHIFT 4 +#define SCR_ARR_OPS_SHIFT 5 #define SCR_ARR_OPS_OVERFLOW (UINT16_MAX >> SCR_ARR_OPS_SHIFT) extern const ScrArrOps *scr_arr_ops_table[SCR_ARR_OPS_OVERFLOW]; /* Assigns the id on a descriptor's first array; out of line, so the id @@ -236,12 +250,13 @@ static inline size_t scr_arr_slot_width(const ScrArr *a) { return scr_elem_slot_width(a->elem); } -/* The dense states, one byte per slot, share the values' allocation (inline - * slots included). They follow the `cap` value slots in reverse slot order: - * the state of slot i is the byte i + 1 before scr_arr_states_end(a). Removing - * slots from the front (head room: `data` advances, `cap` drops) leaves every - * remaining state at its address. The address is computed on integers, so a - * NULL `data` stays NULL. */ +/* The dense states of an array with states, one byte per slot, share the + * values' allocation (inline slots included). They follow the `cap` value + * slots in reverse slot order: the state of slot i is the byte i + 1 before + * scr_arr_states_end(a). Removing slots from the front (head room: `data` + * advances, `cap` drops) leaves every remaining state at its address. The + * address is computed on integers, so a NULL `data` stays NULL. Arrays + * without states have no bytes there. */ static inline uint8_t *scr_arr_states_end(const ScrArr *a) { return (uint8_t *)((uintptr_t)a->data + (uintptr_t)a->cap * (scr_arr_slot_width(a) + 1)); } @@ -267,6 +282,25 @@ static inline uint8_t *scr_arr_state_run(const ScrArr *a, size_t from, size_t co return scr_arr_states_end(a) - from - count; } +/* The state of dense slot `index` (< cap), with or without state bytes. */ +static inline uint8_t scr_arr_dense_state(const ScrArr *a, size_t index) { + if (!scr_arr_has_states(a)) return index < a->len ? SCR_ARR_VALUE : SCR_ARR_HOLE; + return *scr_arr_state_ptr(a, index); +} + +/* The dense states of `a` for a scan (scr_arr_state_in), or NULL when it + * has none: every index below its length is then a value. */ +static inline const uint8_t *scr_arr_scan_states(const ScrArr *a) { + return scr_arr_has_states(a) ? scr_arr_states_end(a) : NULL; +} + +/* Give `a` state bytes: present values below the length, holes above it. + * May move the dense slots. */ +void scr_arr_add_states(ScrArr *a); + +static inline void scr_arr_need_states(ScrArr *a) { + if (!scr_arr_has_states(a)) scr_arr_add_states(a); +} /* A CREF slot's pointer, and the slot of a pointer (NULL or a compact * instance below 32 GiB). */ static inline uint64_t scr_cref_decode(uint32_t ref) { return (uint64_t)ref << 3; } @@ -313,7 +347,8 @@ static inline size_t scr_arr_value_run(const uint8_t *states_end, size_t f, size static inline void scr_arr_refill(ScrArr *a) { size_t end = a->len < a->cap ? a->len : a->cap; size_t f = a->filled; - if (f < end) f = scr_arr_value_run(scr_arr_states_end(a), f, end); + if (!scr_arr_has_states(a)) f = end; + else if (f < end) f = scr_arr_value_run(scr_arr_states_end(a), f, end); a->filled = (uint32_t)f; } @@ -345,8 +380,9 @@ ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap); ScrArr *scr_arr_new_small(ScrElemKind elem); ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops); /* scr_arr_new(elem, cap) for a known final length: the `cap` slots share - * the array's own block (one allocation). Returns +1. */ -ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap); + * the array's own block (one allocation). With `states`, the array starts + * with states (for holes or present undefined). Returns +1. */ +ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap, bool states); static inline ScrArr *scr_arr_retain(ScrArr *a) { if (a->rc != SIZE_MAX) { diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 7a6a76cf9..7bcfd0cfb 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -3122,7 +3122,7 @@ static void scr_bytes_fill_from_arr(uint8_t *out, ScrBytesElem elem, const ScrAr /* Sparse tails and explicit undefined retain ordinary iteration * semantics. Numeric array slots own no conversion callbacks. */ if (index < arr->cap) { - if (*scr_arr_state_ptr(arr, index) == SCR_ARR_VALUE) + if (scr_arr_dense_state(arr, index) == SCR_ARR_VALUE) memcpy(&values[i], &arr->data[index], sizeof(double)); else values[i] = NAN; } else values[i] = scr_arr_get_number(arr, (double)index); @@ -3271,16 +3271,24 @@ double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off, } if (nlen == 0) return o > (double)len ? (double)len : o; /* '' matches at the clamp */ if (nlen > len) return -1; + /* Candidates are the positions holding the needle's first byte; only + * those compare the rest. */ + const uint8_t first = needle->data[0]; if (fwd) { size_t start = o > (double)len ? len : (size_t)o; - for (size_t i = start; i + nlen <= len; i++) { - if (memcmp(b->data + i, needle->data, nlen) == 0) return (double)i; + const uint8_t *at = b->data + start, *end = b->data + (len - nlen) + 1; + while (at < end) { + at = memchr(at, first, (size_t)(end - at)); + if (!at) return -1; + if (nlen == 1 || memcmp(at + 1, needle->data + 1, nlen - 1) == 0) return (double)(at - b->data); + at++; } return -1; } size_t start = o > (double)(len - nlen) ? len - nlen : (size_t)o; for (size_t i = start;; i--) { - if (memcmp(b->data + i, needle->data, nlen) == 0) return (double)i; + if (b->data[i] == first && (nlen == 1 || memcmp(b->data + i + 1, needle->data + 1, nlen - 1) == 0)) + return (double)i; if (i == 0) break; } return -1; diff --git a/packages/runtime/src/scr_bytes_dyn.c b/packages/runtime/src/scr_bytes_dyn.c index c492d43b1..280aa423c 100644 --- a/packages/runtime/src/scr_bytes_dyn.c +++ b/packages/runtime/src/scr_bytes_dyn.c @@ -509,9 +509,8 @@ static bool scr_bytes_dyn_buffer(const ScrDyn *recv) { /* One entry per method family, so a program links only the families it * names: each runs its methods on a Buffer receiver and gives any other * receiver the ordinary dispatch. */ -ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, - int32_t *mid_slot, ScrDyn *const *args, size_t argc, - const char *what) { +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { if (scr_bytes_dyn_buffer(recv)) { bool read = !strncmp(method, "read", 4); const char *rest = method + (read ? 4 : 5); @@ -519,45 +518,40 @@ ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *meth if (!strcmp(rest, scr_bytes_dyn_names[at].name)) return scr_bytes_dyn_num_method(recv->v.bytes, read, at, args, argc); } } - return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); + return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, - int32_t *mid_slot, ScrDyn *const *args, size_t argc, - const char *what) { +ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_write(recv->v.bytes, args, argc); - return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); + return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, - int32_t *mid_slot, ScrDyn *const *args, size_t argc, - const char *what) { +ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_fill(recv, args, argc); - return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); + return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, - int32_t *mid_slot, ScrDyn *const *args, size_t argc, - const char *what) { +ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_to_string(recv->v.bytes, args, argc); - return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); + return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); } /* indexOf, lastIndexOf and includes. */ -ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, - int32_t *mid_slot, ScrDyn *const *args, size_t argc, - const char *what) { +ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { if (scr_bytes_dyn_buffer(recv)) { bool includes = !strcmp(method, "includes"); return scr_bytes_dyn_index_of(recv->v.bytes, args, argc, strcmp(method, "lastIndexOf") != 0, includes); } - return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); + return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); } /* toJSON, equals, compare, copy, swap16/32/64, and the refused members. */ -ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, - int32_t *mid_slot, ScrDyn *const *args, size_t argc, - const char *what) { +ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { if (scr_bytes_dyn_buffer(recv)) { ScrBytes *b = recv->v.bytes; if (!strcmp(method, "toJSON")) return scr_bytes_dyn_to_json(b); @@ -590,5 +584,5 @@ ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, return NULL; } } - return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); + return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); } diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 9bd5abf8c..f8dd9a0c0 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v28(void) {} +void scr_runtime_abi_v33(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_copying.c b/packages/runtime/src/scr_copying.c index ee0bb055a..6cdc0683c 100644 --- a/packages/runtime/src/scr_copying.c +++ b/packages/runtime/src/scr_copying.c @@ -25,10 +25,19 @@ static ScrArr *copying_arr_new_like(const ScrArr *a, size_t cap) { ScrArr *out = a->elem == SCR_ELEM_REF || a->elem == SCR_ELEM_CREF ? scr_arr_new_ops(scr_arr_ops(a), cap ? cap : 1) : scr_arr_new(a->elem, cap ? cap : 1); - out->len = cap; return out; } +/* Copy [from, from + count) of `src` to the end of `out` and raise its + * length over the copy. */ +static void copying_arr_append_range(ScrArr *out, const ScrArr *src, size_t from, size_t count, + bool reverse) { + size_t at = out->len; + scr_arr_copy_range_ex(out, at, src, from, count, reverse, true); + out->len = (uint32_t)(at + count); + scr_arr_refill(out); +} + static uint64_t copying_arr_retain_slot(const ScrArr *a, uint64_t slot) { if (!copying_elem_is_ref(a->elem)) return slot; void *p = copying_slot_to_ptr(slot); @@ -41,7 +50,7 @@ static uint64_t copying_arr_retain_slot(const ScrArr *a, uint64_t slot) { ScrArr *scr_arr_to_reversed(const ScrArr *a) { ScrArr *out = copying_arr_new_like(a, a->len); - scr_arr_copy_range_ex(out, 0, a, 0, a->len, true, true); + copying_arr_append_range(out, a, 0, a->len, true); return out; } @@ -60,10 +69,9 @@ ScrArr *scr_arr_to_spliced(const ScrArr *a, double start, } size_t out_len = a->len - ndelete + items->len; ScrArr *out = copying_arr_new_like(a, out_len); - scr_arr_copy_range_ex(out, 0, a, 0, from, false, true); - scr_arr_copy_range_ex(out, from, items, 0, items->len, false, true); - scr_arr_copy_range_ex(out, from + items->len, a, from + ndelete, - a->len - from - ndelete, false, true); + copying_arr_append_range(out, a, 0, from, false); + copying_arr_append_range(out, items, 0, items->len, false); + copying_arr_append_range(out, a, from + ndelete, a->len - from - ndelete, false); return out; } @@ -88,7 +96,7 @@ ScrArr *scr_arr_with_f64(ScrArr *a, double index, double value) { size_t i; if (!copying_arr_with_index(a, index, &i)) return NULL; ScrArr *out = copying_arr_new_like(a, a->len); - scr_arr_copy_range_ex(out, 0, a, 0, a->len, false, true); + copying_arr_append_range(out, a, 0, a->len, false); scr_arr_set_f64(out, (double)i, value); return out; } @@ -97,7 +105,7 @@ ScrArr *scr_arr_with_bool(ScrArr *a, double index, bool value) { size_t i; if (!copying_arr_with_index(a, index, &i)) return NULL; ScrArr *out = copying_arr_new_like(a, a->len); - scr_arr_copy_range_ex(out, 0, a, 0, a->len, false, true); + copying_arr_append_range(out, a, 0, a->len, false); scr_arr_set_bool(out, (double)i, value); return out; } @@ -106,7 +114,7 @@ ScrArr *scr_arr_with_ref(ScrArr *a, double index, void *value) { size_t i; if (!copying_arr_with_index(a, index, &i)) return NULL; ScrArr *out = copying_arr_new_like(a, a->len); - scr_arr_copy_range_ex(out, 0, a, 0, a->len, false, true); + copying_arr_append_range(out, a, 0, a->len, false); uint64_t retained = copying_arr_retain_slot(a, copying_slot_from_ptr(value)); scr_arr_set_ref(out, (double)i, copying_slot_to_ptr(retained)); @@ -117,7 +125,7 @@ ScrArr *scr_arr_with_undefined(ScrArr *a, double index) { size_t i; if (!copying_arr_with_index(a, index, &i)) return NULL; ScrArr *out = copying_arr_new_like(a, a->len); - scr_arr_copy_range_ex(out, 0, a, 0, a->len, false, true); + copying_arr_append_range(out, a, 0, a->len, false); scr_arr_set_undefined(out, (double)i); return out; } diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index 35332d0ca..d716f0de3 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v28(void); +void scr_runtime_abi_v33(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index 844c47001..6dcd546b0 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -1259,9 +1259,9 @@ void scr_cyc_on_release(void *obj) { #define SCR_CYC_ARRAY_WALK(name, visit) \ static __attribute__((noinline)) void name(const ScrArr *arr, void *ctx) { \ size_t end = arr->len < arr->cap ? arr->len : arr->cap; \ - size_t filled = arr->filled < end ? arr->filled : end; \ + const uint8_t *states = scr_arr_scan_states(arr); \ + size_t filled = !states || arr->filled > end ? end : arr->filled; \ for (size_t i = 0; i < filled; i++) visit((void *)(uintptr_t)arr->data[i], ctx); \ - const uint8_t *states = scr_arr_states_end(arr); \ for (size_t i = filled; i < end; i++) \ if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) \ visit((void *)(uintptr_t)arr->data[i], ctx); \ diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 00884fc81..b2e4a5de7 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -363,6 +363,12 @@ typedef struct ScrDynTypedRecordOps { /* The field names as an immortal string[], built on first use by * for-in over a record with every field present (atomic slot). */ ScrArr *keys; + /* A shape whose values enumerate their keys in more than one order + * (IrRecordShape.orderVariants) binds a table with only this hook: it + * answers the table of the record's current order, which a write of an + * optional key can change after the record is boxed. NULL in every other + * table. */ + const struct ScrDynTypedRecordOps *(*select)(void *rec); } ScrDynTypedRecordOps; /* A record capsule's ops while it answers from the native record (no view * created yet), or NULL. */ @@ -425,11 +431,13 @@ struct ScrDyn { void *source_identity; ScrDyn *(*source_access)(void *, bool materialize); struct ScrDynObjectIndex *index; /* optional wide-object lookup; owns no values */ - /* On a runtime-built class prototype (e.g. url.Url.prototype): the - * constructor name (static text) util.inspect prints for objects - * inheriting from it (scr_dyn_class_name). NULL otherwise. Only - * prototypes that live until their thread exits set it, and they - * clear it before release, so freelist reuse never sees it. */ + /* On a runtime-built class prototype (e.g. url.Url.prototype) and + * on a compiled class's prototype object: the constructor name + * (static or immortal text) util.inspect prints for objects + * inheriting from it (scr_dyn_class_name). NULL otherwise. Names + * that are copies live on prototypes that live until their thread + * exits, and those clear it before release; reuse of a freed object + * resets it (scr_dyn_alloc). */ const char *class_name; struct ScrDynRegion *region; /* see ScrDynRegion */ /* The dynamic tier's hidden class (scr_js_object.c): the transition @@ -714,6 +722,9 @@ ScrDyn *scr_dyn_native_set_new(const ScrDyn *seed); ScrDyn *scr_dyn_native_set(ScrMap *value); /* borrowed backing, +1 box */ bool scr_dyn_native_set_is(const ScrDyn *value); ScrDyn *scr_dyn_native_map(ScrMap *value); /* borrowed backing, +1 box */ +/* `new Map(entries)` over a checked-dynamic seed: +1, or NULL with the + * exception pending. */ +ScrDyn *scr_dyn_native_map_new(const ScrDyn *seed); bool scr_dyn_native_map_is(const ScrDyn *value); void scr_regex_reset_last_index(ScrRegex *re, double index); ScrRegex *scr_regex_new_checked(const ScrDyn *pattern, const ScrDyn *flags); @@ -839,6 +850,7 @@ ScrDyn *scr_dyn_iterator(const ScrDyn *value, const ScrStr *spell); /* for await over a checked-dynamic value (scr_dyn_for_await.c). */ ScrDyn *scr_dyn_async_iterator_open(ScrDyn *value, ScrStr *spell); bool scr_dyn_async_iterator_was_sync(void); +void scr_dyn_async_iterator_set_sync(bool sync); ScrDyn *scr_dyn_iterator_optional(const ScrDyn *value); /* from.js's probe: 2 when value[Symbol.asyncIterator] is set, 1 when * value[Symbol.iterator] is, 0 for neither, -1 with an error pending. */ @@ -935,6 +947,12 @@ bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const Sc bool scr_dyn_data_view_is(const ScrDyn *value); ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); +/* Installed by scr_dyn_invoke.c when it interns a builtin constructor value + * (scr_dyn_global_constructor): false when `callee` is not one of them; + * otherwise true with `new callee(...args)` in *result (+1, or NULL with + * the exception pending). */ +extern SCR_TL bool (*scr_dyn_global_construct_fn)(const ScrDyn *callee, const ScrDyn *args, + ScrDyn **result); ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key); /* The Buffer-flavored twin (stream chunks): string coercion/toString * decode utf8 instead of joining elements. */ @@ -1273,19 +1291,15 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b); * result owned (+1). MAY THROW (NULL with the exception pending). */ ScrDyn *scr_dyn_prepare_method(ScrDyn *recv, const char *method); ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -/* scr_dyn_invoke_prepared for a call site whose method name is a constant: - * *mid_slot, the site's -1-initialized slot, caches the name's method id - * after the first call, so later calls skip the name lookup. */ -ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); -/* scr_dyn_invoke_prepared_id for Buffer.prototype method names - * (scr_bytes_dyn.c), one entry per method family: a Buffer receiver runs - * the method, any other takes the ordinary dispatch. */ -ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared for Buffer.prototype method names (scr_bytes_dyn.c), + * one entry per method family: a Buffer receiver runs the method, any other + * takes the ordinary dispatch. */ +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what); /* Keyed read on a FUNC node: the own-property table first (defineProperties * writes land there), then "name" (the box's best-effort static name; "" diff --git a/packages/runtime/src/scr_dyn_builtin_stream.c b/packages/runtime/src/scr_dyn_builtin_stream.c index cdd815642..926f85eea 100644 --- a/packages/runtime/src/scr_dyn_builtin_stream.c +++ b/packages/runtime/src/scr_dyn_builtin_stream.c @@ -108,10 +108,6 @@ static ScrDyn *bcs_undefined(void) { return scr_dyn_retain(scr_dyn_undefined()); } -static ScrDyn *bcs_str(const char *text) { - return bcs_key(text); -} - static void bcs_unsupported(const char *message) { scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020"); } @@ -231,7 +227,7 @@ static ScrStream *bcs_need(ScrDyn *self, const char *reading) { /* ── completion callbacks ───────────────────────────────────────────── */ -enum { BCS_DONE_WRITE, BCS_DONE_FINAL, BCS_DONE_DESTROY, BCS_DONE_TRANSFORM, BCS_DONE_FLUSH }; +enum { BCS_DONE_WRITE, BCS_DONE_FINAL, BCS_DONE_DESTROY, BCS_DONE_TRANSFORM, BCS_DONE_FLUSH, BCS_DONE_CONSTRUCT }; static void bcs_run_destroy_cbs(ScrStream *s, ScrError *err) { BcsRec *rec = bcs_rec(s); @@ -266,6 +262,9 @@ static ScrDyn *bcs_done_call(ScrClosure *closure, ScrDyn *const *args, size_t ar case BCS_DONE_FINAL: scr_stream_final_done(s, err); break; + case BCS_DONE_CONSTRUCT: + scr_stream_construct_done(s, err); + break; case BCS_DONE_DESTROY: { ScrError *kept = err ? scr_error_retain(err) : NULL; scr_stream_destroy_done(s, err); @@ -401,6 +400,30 @@ static void bcs_plain_call(ScrClosure *cb, ScrStream *s, const char *method, int static bool bcs_is_function(ScrDyn *object, const char *name, bool *failed); +/* constructNT: `try { stream._construct(cb) } catch (err) { onConstruct(err) }`. */ +static void bcs_construct_inv(ScrClosure *cb, ScrStream *s) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + ScrDyn *callback = bcs_done(s, BCS_DONE_CONSTRUCT); + ScrDyn *result = bcs_invoke(object, "_construct", &callback, 1); + scr_dyn_release(callback); + if (result) scr_dyn_release(result); + else if (scr_exc_pending()) scr_stream_construct_done(s, bcs_take_error()); + scr_dyn_release(object); +} + +/* clearBuffer's batch: this._writev(chunks, cb); a throw reaches the + * write() caller, as in Node. */ +static void bcs_writev_inv(ScrClosure *cb, ScrStream *s, ScrDyn *chunks) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + ScrDyn *args[2]; + args[0] = scr_dyn_retain(chunks); + args[1] = bcs_done(s, BCS_DONE_WRITE); + ScrDyn *result = bcs_invoke(object, "_writev", args, 2); + for (int i = 0; i < 2; i++) scr_dyn_release(args[i]); + if (result) scr_dyn_release(result); + scr_dyn_release(object); +} + /* prefinish reads `stream._final` when the writable side finishes, so a * method assigned after the constructor runs; without one the finish goes * on as Node's does when `typeof stream._final !== 'function'`. */ @@ -875,16 +898,22 @@ static ScrDyn *bcs_construct(int kind, ScrDyn *self, ScrDyn *const *args, size_t } bool failed = false; bool writes = kind != BCS_READABLE; - if (writes && bcs_is_function(self, "_writev", &failed)) { - bcs_unsupported("_writev on a constructor-function stream is not supported yet"); - return NULL; - } - if (!failed && bcs_is_function(self, "_construct", &failed)) { - bcs_unsupported("_construct on a constructor-function stream is not supported yet"); - return NULL; - } - if (failed) return NULL; + bool transformish = kind == BCS_TRANSFORM || kind == BCS_PASSTHROUGH; ScrDyn *opts = bcs_arg(args, argc, 0); + /* Node's constructors assign options.writev and options.construct to + * the instance (this._writev = options.writev), then read _writev and + * _construct, so an option shadows the prototype's method. */ + if (opts->kind == SCR_DYN_OBJ) { + if (writes && !transformish && !bcs_copy_option(self, opts, "writev", "_writev")) return NULL; + if (!bcs_copy_option(self, opts, "construct", "_construct")) return NULL; + } + bool has_writev = writes && bcs_is_function(self, "_writev", &failed); + if (has_writev && transformish) { + bcs_unsupported("_writev on a constructor-function Transform is not supported yet"); + return NULL; + } + bool has_construct = !failed && bcs_is_function(self, "_construct", &failed); + if (failed) return NULL; ScrStream *s = kind == BCS_READABLE ? scr_stream_new_readable_dyn(opts) : kind == BCS_WRITABLE ? scr_stream_new_writable_dyn(opts) : kind == BCS_DUPLEX ? scr_stream_new_duplex_dyn(opts) @@ -936,6 +965,9 @@ static ScrDyn *bcs_construct(int kind, ScrDyn *self, ScrDyn *const *args, size_t } } scr_stream_set_destroy(s, bcs_hook(self, (void *)bcs_destroy_inv), bcs_destroy_inv); + if (has_writev) scr_stream_set_writev(s, bcs_hook(self, (void *)bcs_writev_inv), bcs_writev_inv); + if (has_construct) + scr_stream_set_construct(s, bcs_hook(self, (void *)bcs_construct_inv), bcs_construct_inv); bcs_hide(self, s); if (!failed) failed = !bcs_adopt_listeners(s, self); if (!failed && kind != BCS_WRITABLE) failed = !bcs_state_view(self, s, true); @@ -1020,17 +1052,6 @@ static ScrClosure *bcs_callback(ScrDyn *fn, ScrDyn *self, ScrStream *s) { return closure; } -/* Strings in another encoding than utf8 have no write path here. */ -static bool bcs_utf8_only(const ScrDyn *encoding) { - if (encoding->kind != SCR_DYN_STR) return true; - ScrStr *canonical = bcs_encoding(encoding); - if (!canonical) return false; - bool utf8 = bcs_is(canonical, "utf8"); - scr_str_release(canonical); - if (!utf8) bcs_unsupported("writing strings in an encoding other than utf8 to a constructor-function stream is not supported yet"); - return utf8; -} - static ScrDyn *bcs_write(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t argc) { ScrDyn *chunk = bcs_arg(args, argc, 0); ScrDyn *encoding = bcs_arg(args, argc, 1); @@ -1039,14 +1060,9 @@ static ScrDyn *bcs_write(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t cb = encoding; encoding = scr_stream_writes_values(s) ? scr_dyn_new_null() : scr_dyn_undefined(); } - bool below; - if (scr_stream_writes_values(s)) { - /* an object-mode side hands the encoding argument to _write */ - below = scr_stream_write_enc(s, chunk, encoding, bcs_callback(cb, self, s)); - } else { - if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) return NULL; - below = scr_stream_write_dyn(s, chunk, bcs_callback(cb, self, s)); - } + /* an object-mode side hands the encoding argument to _write; a byte-mode + * side encodes a string with it (or the default encoding) */ + bool below = scr_stream_write_enc(s, chunk, encoding, bcs_callback(cb, self, s)); if (scr_exc_pending()) return NULL; return scr_dyn_new_bool(below); } @@ -1064,24 +1080,17 @@ static ScrDyn *bcs_end(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t a encoding = scr_dyn_new_null(); } ScrStream *chained = NULL; - if (!bcs_nullish(chunk) && scr_stream_writes_values(s)) { - /* end(chunk) writes the value with the encoding argument */ + if (!bcs_nullish(chunk)) { + /* end(chunk) writes the chunk with the encoding argument first */ + if (scr_stream_prop(s, "writableObjectMode") == 0 && chunk->kind != SCR_DYN_STR && + chunk->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", chunk); + return NULL; + } scr_stream_write_enc(s, chunk, encoding, NULL); if (scr_exc_pending()) return NULL; - chained = scr_stream_end(s, NULL, NULL, bcs_callback(cb, self, s)); - } else if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) { - return NULL; - } else if (bcs_nullish(chunk)) { - chained = scr_stream_end(s, NULL, NULL, bcs_callback(cb, self, s)); - } else if (chunk->kind == SCR_DYN_STR) { - chained = scr_stream_end(s, NULL, chunk->v.str, bcs_callback(cb, self, s)); - } else if (chunk->kind == SCR_DYN_BYTES) { - ScrBytes *bytes = scr_bytes_raw_view(chunk->v.bytes); - chained = scr_stream_end(s, bytes, NULL, bcs_callback(cb, self, s)); - scr_bytes_release(bytes); - } else { - scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", chunk); } + chained = scr_stream_end(s, NULL, NULL, bcs_callback(cb, self, s)); if (chained) scr_stream_release(chained); if (scr_exc_pending()) return NULL; return scr_dyn_retain(self); @@ -1164,8 +1173,26 @@ static ScrDyn *bcs_method_call(ScrClosure *closure, ScrDyn *const *args, size_t switch (method) { case BCS_M_READ_DEFAULT: return bcs_not_implemented("_read()"); - case BCS_M_WRITE_DEFAULT: + case BCS_M_WRITE_DEFAULT: { + /* Writable.prototype._write: this._writev([{ chunk, encoding }], cb) */ + ScrDyn *self = scr_dyn_this_get(); + bool failed = false; + if (bcs_is_function(self, "_writev", &failed)) { + ScrDyn *chunks = scr_dyn_new_arr(); + ScrDyn *entry = scr_dyn_new_obj(); + scr_dyn_obj_set(entry, "chunk", 5, scr_dyn_retain(bcs_arg(args, argc, 0))); + scr_dyn_obj_set(entry, "encoding", 8, scr_dyn_retain(bcs_arg(args, argc, 1))); + scr_dyn_arr_push(chunks, entry); + ScrDyn *pass[2] = {chunks, bcs_arg(args, argc, 2)}; + ScrDyn *result = bcs_invoke(self, "_writev", pass, 2); + scr_dyn_release(chunks); + scr_dyn_release(self); + return result; + } + scr_dyn_release(self); + if (failed) return NULL; return bcs_not_implemented("_write()"); + } case BCS_M_TRANSFORM_DEFAULT: return bcs_not_implemented("_transform()"); case BCS_M_DESTROY_DEFAULT: @@ -1317,9 +1344,12 @@ static ScrDyn *bcs_method_call(ScrClosure *closure, ScrDyn *const *args, size_t scr_stream_uncork(s); result = bcs_undefined(); break; - case BCS_M_SET_DEFAULT_ENCODING: - if (bcs_utf8_only(first)) result = scr_dyn_retain(self); + case BCS_M_SET_DEFAULT_ENCODING: { + ScrStream *chained = scr_stream_set_default_encoding(s, first); + if (chained) scr_stream_release(chained); + if (!scr_exc_pending()) result = scr_dyn_retain(self); break; + } default: bcs_unsupported("Readable.prototype.wrap on a constructor-function stream is not supported yet"); break; diff --git a/packages/runtime/src/scr_dyn_for_await.c b/packages/runtime/src/scr_dyn_for_await.c index 25183c4ee..3a1a6458d 100644 --- a/packages/runtime/src/scr_dyn_for_await.c +++ b/packages/runtime/src/scr_dyn_for_await.c @@ -38,6 +38,11 @@ ScrDyn *scr_dyn_async_iterator_open(ScrDyn *value, ScrStr *spell) { return iterator; } +/* For an iterator another open path made (scr_dyn_for_await_stream.c). */ +void scr_dyn_async_iterator_set_sync(bool sync) { + scr_for_await_sync = sync; +} + /* Whether the iterator the last scr_dyn_async_iterator_open returned is a * synchronous one. Read immediately after the open. */ bool scr_dyn_async_iterator_was_sync(void) { diff --git a/packages/runtime/src/scr_dyn_for_await_stream.c b/packages/runtime/src/scr_dyn_for_await_stream.c new file mode 100644 index 000000000..6facf916c --- /dev/null +++ b/packages/runtime/src/scr_dyn_for_await_stream.c @@ -0,0 +1,61 @@ +/* `for await` over a checked-dynamic value in a program that links the + * stream unit: a native readable stream behind the value (a boxed stream, + * or a Cipheriv/Decipheriv handle's Transform) iterates through an + * iterator object whose next() answers the readable iterator's step as + * { value, done } and whose return() closes it as a typed loop does + * (destroy with an AbortError). Any other value takes + * scr_dyn_async_iterator_open. Linked only by programs with such a loop + * and streams. */ +#include "scr_runtime.h" +#include "scr_stream.h" + +/* caps[0]: the stream */ +static ScrStream *iter_stream(ScrClosure *clo) { + return scr_box_get_ref(clo->caps[0]); /* +1 */ +} + +static ScrDyn *iter_next_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + ScrStream *s = iter_stream(clo); + ScrPromise *p = scr_stream_next_result_dyn(s); + scr_stream_release(s); + ScrDyn *d = scr_dyn_new_promise(p); + scr_promise_release(p); + return d; +} + +static ScrDyn *iter_return_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + ScrStream *s = iter_stream(clo); + scr_stream_release(scr_stream_iterator_close(s)); + scr_stream_release(s); + ScrDyn *result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "value", 5, scr_dyn_retain(scr_dyn_undefined())); + scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(true)); + ScrPromise *p = scr_promise_new(); + scr_promise_fulfill_ref(p, result, scr_dyn_retain_v, scr_dyn_release_v, NULL); + ScrDyn *d = scr_dyn_new_promise(p); + scr_promise_release(p); + return d; +} + +static ScrDyn *iter_method(ScrStream *s, ScrDynThunk thunk, const char *name) { + ScrClosure *clo = scr_closure_new((void *)thunk, 1); + clo->caps[0] = scr_box_new_obj(&scr_stream_retain_v, &scr_stream_release_v, &scr_stream_trace); + scr_box_set_ref(clo->caps[0], scr_stream_retain(s)); + return scr_dyn_new_func(clo, thunk, 0, "()", name); +} + +ScrDyn *scr_stream_dyn_async_iterator_open(ScrDyn *value, ScrStr *spell) { + ScrStream *s = scr_stream_native_readable_of_dyn(value); + if (!s) return scr_dyn_async_iterator_open(value, spell); + /* the loop awaits its results with the async protocol */ + scr_dyn_async_iterator_set_sync(false); + ScrDyn *it = scr_dyn_new_obj(); + scr_dyn_obj_set(it, "next", 4, iter_method(s, &iter_next_thunk, "next")); + scr_dyn_obj_set(it, "return", 6, iter_method(s, &iter_return_thunk, "return")); + scr_stream_release(s); + return it; +} diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index d4189f070..e978ac7aa 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -8,7 +8,6 @@ * scr_json.c; this unit uses only its public surface plus ScrJsonBuf. */ #include "scr_runtime.h" -#include "scr_dyn_methods.h" #include #include @@ -550,11 +549,7 @@ ScrDyn *scr_dyn_array_proto_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) { return scr_dyn_retain(scr_dyn_undefined()); } -/* The dispatch below maps a call's method name to an id once - * (dyn_method_of, scr_dyn_methods.h) and its branches compare ids. Names - * compare by content, and any name the branches do not test is DYN_M_NONE. - * scripts/gen-dyn-methods.mjs writes the header from the DYN_M_ ids used - * here. */ +static bool dyn_name_is(const char *m, const char *n) { return strcmp(m, n) == 0; } /* Handle receivers: hand the whole call to the tag's ops (installed by * the owning unit at main() — a missing install is an internal error the @@ -697,34 +692,29 @@ static bool dyn_arr_reversed_copy(ScrDyn *out, const ScrDyn *source) { /* Names each prototype declares BEYOND what's implemented here — these * throw the "not supported yet" Error instead of mis-answering "is not a * function". */ -static bool dyn_arr_proto_unimpl(int m) { - return m == DYN_M_toString || m == DYN_M_toLocaleString; +static bool dyn_arr_proto_unimpl(const char *m) { + static const char *names[] = { "toString", "toLocaleString", NULL }; + for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; + return false; } -static bool dyn_arr_proto_mutates(int m) { - switch (m) { - case DYN_M_push: case DYN_M_pop: case DYN_M_shift: case DYN_M_unshift: case DYN_M_reverse: - case DYN_M_sort: case DYN_M_fill: case DYN_M_copyWithin: case DYN_M_splice: - return true; - default: - return false; - } +static bool dyn_arr_proto_mutates(const char *m) { + static const char *names[] = { + "push", "pop", "shift", "unshift", "reverse", "sort", "fill", "copyWithin", "splice", NULL + }; + for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; + return false; } -static bool dyn_bytes_proto_real(int m) { - switch (m) { - case DYN_M_slice: case DYN_M_at: case DYN_M_indexOf: case DYN_M_lastIndexOf: case DYN_M_includes: - case DYN_M_join: case DYN_M_forEach: case DYN_M_map: case DYN_M_filter: case DYN_M_some: - case DYN_M_every: case DYN_M_find: case DYN_M_findIndex: case DYN_M_reverse: case DYN_M_fill: - case DYN_M_set: case DYN_M_subarray: case DYN_M_sort: case DYN_M_keys: case DYN_M_values: - case DYN_M_entries: case DYN_M_reduce: case DYN_M_reduceRight: case DYN_M_copyWithin: - case DYN_M_toString: case DYN_M_toLocaleString: - return true; - default: - return false; - } +static bool dyn_bytes_proto_real(const char *m) { + static const char *names[] = { "slice", "at", "indexOf", "lastIndexOf", "includes", "join", + "forEach", "map", "filter", "some", "every", "find", "findIndex", "reverse", "fill", "set", + "subarray", "sort", "keys", "values", "entries", "reduce", "reduceRight", "copyWithin", + "toString", "toLocaleString", NULL }; + for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; + return false; } static ScrDyn *scr_dyn_invoke_impl( - ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc, + ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv); /* Live native arrays: the constant-time methods that loops call once per @@ -734,18 +724,18 @@ static ScrDyn *scr_dyn_invoke_impl( * array. Anything that could observe more — a hole, argument coercion, a * multi-value push, a value the element type rejects — sets *handled false * and takes the snapshot path. */ -static ScrDyn *dyn_live_array_method(ScrDyn *recv, int mid, ScrDyn *const *args, +static ScrDyn *dyn_live_array_method(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what, bool *handled) { const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(recv); if (!ops) return NULL; ScrArr *arr = recv->v.typed_ref.ptr; size_t len = arr->len; - if ((mid == DYN_M_push) && argc == 1 && ops->set) { + if (dyn_name_is(method, "push") && argc == 1 && ops->set) { if (!ops->set(arr, (double)len, args[0])) return NULL; *handled = true; return scr_exc_pending() ? NULL : scr_dyn_new_num((double)arr->len); } - if ((mid == DYN_M_pop)) { + if (dyn_name_is(method, "pop")) { if (len == 0) { *handled = true; return scr_dyn_retain(scr_dyn_undefined()); @@ -756,7 +746,7 @@ static ScrDyn *dyn_live_array_method(ScrDyn *recv, int mid, ScrDyn *const *args, *handled = true; return last; } - if ((mid == DYN_M_at) && (argc == 0 || args[0]->kind == SCR_DYN_NUM || args[0]->kind == SCR_DYN_UNDEF)) { + if (dyn_name_is(method, "at") && (argc == 0 || args[0]->kind == SCR_DYN_NUM || args[0]->kind == SCR_DYN_UNDEF)) { double index = dyn_index_arg(args, argc, 0, 0, what); if (index < 0) index += (double)len; if (index < 0 || index >= (double)len) { @@ -796,25 +786,11 @@ ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what) { - return scr_dyn_invoke_impl(recv, method, dyn_method_of(method), args, argc, what, NULL); -} - -ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, - int32_t *mid_slot, ScrDyn *const *args, size_t argc, - const char *what) { - if (callee) return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); - /* The slot is the call site's, shared by every thread: each writes the - * same id. */ - int32_t mid = __atomic_load_n(mid_slot, __ATOMIC_RELAXED); - if (mid < 0) { - mid = dyn_method_of(method); - __atomic_store_n(mid_slot, mid, __ATOMIC_RELAXED); - } - return scr_dyn_invoke_impl(recv, method, mid, args, argc, what, NULL); + return scr_dyn_invoke_impl(recv, method, args, argc, what, NULL); } static ScrDyn *scr_dyn_invoke_impl( - ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc, + ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv) { if (recv->kind == SCR_DYN_UNDEF || recv->kind == SCR_DYN_NULL) { ScrJsonBuf b; @@ -835,7 +811,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (recv->kind == SCR_DYN_HANDLE) { return scr_dynh_dispatch(recv, method, args, argc, what); } - if (scr_dyn_is_array_prototype(recv) && dyn_arr_proto_mutates(mid)) { + if (scr_dyn_is_array_prototype(recv) && dyn_arr_proto_mutates(method)) { scr_dyn_array_prototype_element_fence(recv); return NULL; } @@ -860,7 +836,7 @@ static ScrDyn *scr_dyn_invoke_impl( * snapshot identity back to the externally visible capsule. */ if (recv->kind == SCR_DYN_TYPED_REF) { bool handled = false; - ScrDyn *fast = dyn_live_array_method(recv, mid, args, argc, what, &handled); + ScrDyn *fast = dyn_live_array_method(recv, method, args, argc, what, &handled); if (handled) return fast; /* Iterator helpers on a compiled generator. */ if (scr_dyn_generator_helper_hook && @@ -885,9 +861,9 @@ static ScrDyn *scr_dyn_invoke_impl( return NULL; } bool mutates = materialized->kind == SCR_DYN_ARR && - dyn_arr_proto_mutates(mid); + dyn_arr_proto_mutates(method); ScrDyn *result = scr_dyn_invoke_impl( - materialized, method, mid, args, argc, what, recv); + materialized, method, args, argc, what, recv); if (mutates && !scr_exc_pending()) scr_dyn_typed_ref_commit(recv); if (result == materialized) { scr_dyn_release(result); @@ -916,18 +892,18 @@ static ScrDyn *scr_dyn_invoke_impl( } if (recv->kind == SCR_DYN_FUNC) { - if ((mid == DYN_M_apply)) { + if (dyn_name_is(method, "apply")) { return scr_dyn_apply_array_like(recv, argc ? args[0] : scr_dyn_undefined(), argc > 1 ? args[1] : scr_dyn_undefined(), what); } - if ((mid == DYN_M_call)) { + if (dyn_name_is(method, "call")) { scr_dyn_this_push_dyn(argc >= 1 ? args[0] : scr_dyn_undefined()); ScrDyn *r = scr_dyn_call(recv, argc > 1 ? args + 1 : NULL, argc > 1 ? argc - 1 : 0, what); scr_dyn_this_pop(); return r; } - if ((mid == DYN_M_bind)) return scr_dyn_bind(recv, args, argc); - if ((mid == DYN_M_toString)) { + if (dyn_name_is(method, "bind")) return scr_dyn_bind(recv, args, argc); + if (dyn_name_is(method, "toString")) { ScrDyn *member = scr_dyn_fn_get(recv, method, strlen(method)); if (member && member->kind != SCR_DYN_UNDEF) { scr_dyn_this_push_dyn(recv); @@ -962,9 +938,9 @@ static ScrDyn *scr_dyn_invoke_impl( } if ((recv->kind == SCR_DYN_SYMBOL || recv->kind == SCR_DYN_BIGINT || recv->kind == SCR_DYN_NUM || recv->kind == SCR_DYN_BOOL || recv->kind == SCR_DYN_STR) && - (mid == DYN_M_valueOf)) return scr_dyn_retain(recv); + dyn_name_is(method, "valueOf")) return scr_dyn_retain(recv); - if (recv->kind == SCR_DYN_NUM && (mid == DYN_M_toFixed)) { + if (recv->kind == SCR_DYN_NUM && dyn_name_is(method, "toFixed")) { double digits = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; ScrStr *text = scr_num_to_fixed(recv->v.num, digits); @@ -973,7 +949,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(text); return result; } - if (recv->kind == SCR_DYN_NUM && (mid == DYN_M_toExponential)) { + if (recv->kind == SCR_DYN_NUM && dyn_name_is(method, "toExponential")) { if (argc && args[0]->kind != SCR_DYN_UNDEF) { dyn_throw_unsupported("Number", method); return NULL; @@ -986,7 +962,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (recv->kind == SCR_DYN_STR) { ScrStr *s = recv->v.str; - if ((mid == DYN_M_slice)) { + if (dyn_name_is(method, "slice")) { double start = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double end = dyn_index_arg(args, argc, 1, scr_str_utf16_len(s), what); @@ -996,16 +972,16 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(piece); return r; } - if ((mid == DYN_M_at) || (mid == DYN_M_concat) || - (mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || - (mid == DYN_M_includes)) { - if (((mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || - (mid == DYN_M_includes)) && + if (dyn_name_is(method, "at") || dyn_name_is(method, "concat") || + dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || + dyn_name_is(method, "includes")) { + if ((dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || + dyn_name_is(method, "includes")) && argc >= 1 && args[0]->kind == SCR_DYN_STR) { - double position = dyn_index_arg(args, argc, 1, (mid == DYN_M_lastIndexOf) ? INFINITY : 0, what); + double position = dyn_index_arg(args, argc, 1, dyn_name_is(method, "lastIndexOf") ? INFINITY : 0, what); if (scr_exc_pending()) return NULL; - if ((mid == DYN_M_includes)) return scr_dyn_new_bool(scr_str_index_of(s, args[0]->v.str, position) >= 0); - if ((mid == DYN_M_indexOf)) return scr_dyn_new_num(scr_str_index_of(s, args[0]->v.str, position)); + if (dyn_name_is(method, "includes")) return scr_dyn_new_bool(scr_str_index_of(s, args[0]->v.str, position) >= 0); + if (dyn_name_is(method, "indexOf")) return scr_dyn_new_num(scr_str_index_of(s, args[0]->v.str, position)); return scr_dyn_new_num(scr_str_last_index_of_from(s, args[0]->v.str, position)); } dyn_throw_unsupported("String", method); @@ -1016,19 +992,19 @@ static ScrDyn *scr_dyn_invoke_impl( } if (recv->kind == SCR_DYN_ARR) { - if ((mid == DYN_M_keys) || (mid == DYN_M_values) || (mid == DYN_M_entries)) + if (dyn_name_is(method, "keys") || dyn_name_is(method, "values") || dyn_name_is(method, "entries")) return scr_dyn_native_handle_iterator(callback_recv ? callback_recv : recv, - (mid == DYN_M_keys) ? 1 : (mid == DYN_M_entries) ? 2 : 0); + dyn_name_is(method, "keys") ? 1 : dyn_name_is(method, "entries") ? 2 : 0); size_t len = recv->v.arr.len; if (recv->v.arr.presence) { bool sparse = false; for (size_t i = 0; i < len; i++) if (!recv->v.arr.presence[i]) { sparse = true; break; } if (!sparse) { free(recv->v.arr.presence); recv->v.arr.presence = NULL; } - else if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || (mid == DYN_M_filter) || - (mid == DYN_M_some) || (mid == DYN_M_every) || (mid == DYN_M_find) || - (mid == DYN_M_findIndex) || (mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex) || - (mid == DYN_M_reduce) || (mid == DYN_M_reduceRight) || (mid == DYN_M_slice) || - (mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || (mid == DYN_M_includes)) { + else if (dyn_name_is(method, "forEach") || dyn_name_is(method, "map") || dyn_name_is(method, "filter") || + dyn_name_is(method, "some") || dyn_name_is(method, "every") || dyn_name_is(method, "find") || + dyn_name_is(method, "findIndex") || dyn_name_is(method, "findLast") || dyn_name_is(method, "findLastIndex") || + dyn_name_is(method, "reduce") || dyn_name_is(method, "reduceRight") || dyn_name_is(method, "slice") || + dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || dyn_name_is(method, "includes")) { ScrDyn *pack = scr_dyn_new_arr(); for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(pack, scr_dyn_retain(args[i])); ScrStr *name = scr_str_new(method, strlen(method)); @@ -1036,7 +1012,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(name); scr_dyn_release(pack); return result; - } else if (dyn_arr_proto_mutates(mid) && !(mid == DYN_M_fill) && !(mid == DYN_M_push) && !(mid == DYN_M_pop) && !(mid == DYN_M_shift)) { + } else if (dyn_arr_proto_mutates(method) && !dyn_name_is(method, "fill") && !dyn_name_is(method, "push") && !dyn_name_is(method, "pop") && !dyn_name_is(method, "shift")) { static const char message[] = "This checked array mutation over holes is not supported yet"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return NULL; @@ -1044,11 +1020,11 @@ static ScrDyn *scr_dyn_invoke_impl( } size_t splice_start = 0; size_t splice_removed = 0; - if (dyn_arr_proto_mutates(mid)) { - bool grows = (mid == DYN_M_push) && argc > 0; - grows = grows || ((mid == DYN_M_unshift) && argc > 0); - bool shrinks = ((mid == DYN_M_pop) || (mid == DYN_M_shift)) && len > 0; - if ((mid == DYN_M_splice)) { + if (dyn_arr_proto_mutates(method)) { + bool grows = dyn_name_is(method, "push") && argc > 0; + grows = grows || (dyn_name_is(method, "unshift") && argc > 0); + bool shrinks = (dyn_name_is(method, "pop") || dyn_name_is(method, "shift")) && len > 0; + if (dyn_name_is(method, "splice")) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; splice_start = dyn_rel_index(startD, len); @@ -1085,33 +1061,33 @@ static ScrDyn *scr_dyn_invoke_impl( return NULL; } } - if (recv->v.arr.frozen && ((mid == DYN_M_push) || - (mid == DYN_M_pop) || (mid == DYN_M_shift) || - (mid == DYN_M_unshift) || (mid == DYN_M_splice))) { + if (recv->v.arr.frozen && (dyn_name_is(method, "push") || + dyn_name_is(method, "pop") || dyn_name_is(method, "shift") || + dyn_name_is(method, "unshift") || dyn_name_is(method, "splice"))) { static const char message[] = "Cannot assign to read only property 'length'"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !(mid == DYN_M_shift)) || - (recv->v.arr.frozen && (((mid == DYN_M_sort) && len > 1) || - ((mid == DYN_M_reverse) && len > 1)))) { + if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !dyn_name_is(method, "shift")) || + (recv->v.arr.frozen && ((dyn_name_is(method, "sort") && len > 1) || + (dyn_name_is(method, "reverse") && len > 1)))) { static const char message[] = "Cannot modify a sealed or frozen array"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } } - if ((mid == DYN_M_push)) { + if (dyn_name_is(method, "push")) { for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(recv, scr_dyn_retain(args[i])); return scr_dyn_new_num((double)recv->v.arr.len); } - if ((mid == DYN_M_pop)) { + if (dyn_name_is(method, "pop")) { if (len == 0) return scr_dyn_retain(scr_dyn_undefined()); ScrDyn *result = scr_dyn_arr_at(recv, (double)(len - 1)); if (!result) return NULL; scr_dyn_release(recv->v.arr.items[--recv->v.arr.len]); return result; } - if ((mid == DYN_M_shift)) { + if (dyn_name_is(method, "shift")) { if (len == 0) return scr_dyn_retain(scr_dyn_undefined()); if (recv->v.arr.sealed) { if (recv->v.arr.frozen) { @@ -1133,7 +1109,7 @@ static ScrDyn *scr_dyn_invoke_impl( recv->v.arr.len = len - 1; return first; /* ownership moves out */ } - if ((mid == DYN_M_unshift)) { + if (dyn_name_is(method, "unshift")) { /* Grow the backing store before shifting the old elements. The * appended placeholders are immortal, so overwriting them does not * abandon retained argument references. */ @@ -1145,7 +1121,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_new_num((double)recv->v.arr.len); } - if ((mid == DYN_M_splice)) { + if (dyn_name_is(method, "splice")) { size_t start = splice_start; size_t removed = splice_removed; size_t inserted = argc > 2 ? argc - 2 : 0; @@ -1175,7 +1151,7 @@ static ScrDyn *scr_dyn_invoke_impl( recv->v.arr.len = next_len; return out; } - if ((mid == DYN_M_slice)) { + if (dyn_name_is(method, "slice")) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double endD = dyn_index_arg(args, argc, 1, (double)len, what); @@ -1187,7 +1163,7 @@ static ScrDyn *scr_dyn_invoke_impl( for (size_t i = start; i < end; i++) scr_dyn_arr_push(out, scr_dyn_retain(recv->v.arr.items[i])); return out; } - if ((mid == DYN_M_at)) { + if (dyn_name_is(method, "at")) { double iD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1195,17 +1171,17 @@ static ScrDyn *scr_dyn_invoke_impl( if (idx < 0 || idx >= (double)len) return scr_dyn_retain(scr_dyn_undefined()); return scr_dyn_arr_at(recv, idx); } - if ((mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || - (mid == DYN_M_includes)) { - if (len == 0) return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + if (dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || + dyn_name_is(method, "includes")) { + if (len == 0) return dyn_name_is(method, "includes") ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); ScrDyn *needle = argc > 0 ? args[0] : scr_dyn_undefined(); - double from = (mid == DYN_M_lastIndexOf) ? (double)len - 1 : 0; + double from = dyn_name_is(method, "lastIndexOf") ? (double)len - 1 : 0; if (argc > 1) { from = dyn_index_arg(args, argc, 1, 0, what); if (scr_exc_pending()) return NULL; } if (!dyn_arr_length_unchanged(recv, len)) return NULL; - if ((mid == DYN_M_lastIndexOf)) { + if (dyn_name_is(method, "lastIndexOf")) { double first = from < 0 ? (double)len + from : from; if (first >= (double)len) first = (double)len - 1; for (size_t i = first < 0 ? 0 : (size_t)first + 1; i > 0; i--) { @@ -1217,16 +1193,16 @@ static ScrDyn *scr_dyn_invoke_impl( if (first < 0) first = 0; for (size_t i = first >= (double)len ? len : (size_t)first; i < len; i++) { const ScrDyn *item = recv->v.arr.items[i]; - bool nan_match = (mid == DYN_M_includes) && + bool nan_match = dyn_name_is(method, "includes") && item->kind == SCR_DYN_NUM && needle->kind == SCR_DYN_NUM && item->v.num != item->v.num && needle->v.num != needle->v.num; if (nan_match || scr_dyn_strict_eq(item, needle)) { - return (mid == DYN_M_includes) ? scr_dyn_new_bool(true) : scr_dyn_new_num((double)i); + return dyn_name_is(method, "includes") ? scr_dyn_new_bool(true) : scr_dyn_new_num((double)i); } } - return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + return dyn_name_is(method, "includes") ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); } - if ((mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex)) { + if (dyn_name_is(method, "findLast") || dyn_name_is(method, "findLastIndex")) { if (!dyn_cb_check(args, argc)) return NULL; ScrDyn *visible_recv = callback_recv ? callback_recv : recv; for (size_t i = len; i > 0; i--) { @@ -1239,16 +1215,16 @@ static ScrDyn *scr_dyn_invoke_impl( bool matched = scr_dyn_truthy(result); scr_dyn_release(result); if (matched) { - if ((mid == DYN_M_findLast)) return item; + if (dyn_name_is(method, "findLast")) return item; scr_dyn_release(item); return scr_dyn_new_num((double)index); } scr_dyn_release(item); } - return (mid == DYN_M_findLast) + return dyn_name_is(method, "findLast") ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(-1); } - if ((mid == DYN_M_join)) { + if (dyn_name_is(method, "join")) { ScrJsonBuf b; scr_jb_init(&b); ScrStr *separator = argc > 0 && args[0]->kind != SCR_DYN_UNDEF @@ -1274,7 +1250,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(joined); return r; } - if ((mid == DYN_M_concat)) { + if (dyn_name_is(method, "concat")) { ScrDyn *out = scr_dyn_new_arr(); for (size_t i = 0; i < len; i++) { if (!dyn_arr_copy_item(out, recv, i, true)) { scr_dyn_release(out); return NULL; } @@ -1295,7 +1271,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if ((mid == DYN_M_flat)) { + if (dyn_name_is(method, "flat")) { double depth = dyn_index_arg(args, argc, 0, 1, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1307,7 +1283,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if ((mid == DYN_M_reverse)) { + if (dyn_name_is(method, "reverse")) { for (size_t i = 0; i < len / 2; i++) { ScrDyn *tmp = recv->v.arr.items[i]; recv->v.arr.items[i] = recv->v.arr.items[len - 1 - i]; @@ -1315,12 +1291,12 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if ((mid == DYN_M_toReversed)) { + if (dyn_name_is(method, "toReversed")) { ScrDyn *out = scr_dyn_new_arr(); if (!dyn_arr_reversed_copy(out, recv)) { scr_dyn_release(out); return NULL; } return out; } - if ((mid == DYN_M_with)) { + if (dyn_name_is(method, "with")) { double indexD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1338,7 +1314,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if ((mid == DYN_M_toSpliced)) { + if (dyn_name_is(method, "toSpliced")) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; size_t start = dyn_rel_index(startD, len); @@ -1355,7 +1331,7 @@ static ScrDyn *scr_dyn_invoke_impl( for (size_t i = start + delete_count; i < len; i++) if (!dyn_arr_copy_item(out, recv, i, false)) { scr_dyn_release(out); return NULL; } return out; } - if ((mid == DYN_M_fill)) { + if (dyn_name_is(method, "fill")) { double startD = dyn_index_arg(args, argc, 1, 0, what); if (scr_exc_pending()) return NULL; double endD = dyn_index_arg(args, argc, 2, (double)len, what); @@ -1383,7 +1359,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if ((mid == DYN_M_copyWithin)) { + if (dyn_name_is(method, "copyWithin")) { double targetD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double startD = dyn_index_arg(args, argc, 1, 0, what); @@ -1416,13 +1392,13 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || - (mid == DYN_M_filter) || (mid == DYN_M_some) || - (mid == DYN_M_every) || (mid == DYN_M_find) || - (mid == DYN_M_findIndex)) { + if (dyn_name_is(method, "forEach") || dyn_name_is(method, "map") || + dyn_name_is(method, "filter") || dyn_name_is(method, "some") || + dyn_name_is(method, "every") || dyn_name_is(method, "find") || + dyn_name_is(method, "findIndex")) { if (!dyn_cb_check(args, argc)) return NULL; ScrDyn *cb = args[0]; - ScrDyn *out = ((mid == DYN_M_map) || (mid == DYN_M_filter)) ? scr_dyn_new_arr() : NULL; + ScrDyn *out = (dyn_name_is(method, "map") || dyn_name_is(method, "filter")) ? scr_dyn_new_arr() : NULL; /* Array iteration methods snapshot length before the first callback. * A shrinking callback can remove a later dense entry; an expanding * callback must not make the new tail observable to this pass. Live @@ -1437,30 +1413,30 @@ static ScrDyn *scr_dyn_invoke_impl( ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]); ScrDyn *r = dyn_call_cb(cb, item, i, visible_recv); if (!r) { scr_dyn_release(item); scr_dyn_release(out); return NULL; } - if ((mid == DYN_M_map)) { + if (dyn_name_is(method, "map")) { scr_dyn_arr_push(out, r); /* ownership moves in */ r = NULL; } else { bool truthy = scr_dyn_truthy(r); scr_dyn_release(r); - if ((mid == DYN_M_filter) && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); - if ((mid == DYN_M_some) && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } - if ((mid == DYN_M_every) && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } - if ((mid == DYN_M_find) && truthy) return item; /* +1 moves out */ - if ((mid == DYN_M_findIndex) && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } + if (dyn_name_is(method, "filter") && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); + if (dyn_name_is(method, "some") && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } + if (dyn_name_is(method, "every") && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } + if (dyn_name_is(method, "find") && truthy) return item; /* +1 moves out */ + if (dyn_name_is(method, "findIndex") && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } } scr_dyn_release(item); } if (out) return out; - if ((mid == DYN_M_some)) return scr_dyn_new_bool(false); - if ((mid == DYN_M_every)) return scr_dyn_new_bool(true); - if ((mid == DYN_M_find)) return scr_dyn_retain(scr_dyn_undefined()); - if ((mid == DYN_M_findIndex)) return scr_dyn_new_num(-1); + if (dyn_name_is(method, "some")) return scr_dyn_new_bool(false); + if (dyn_name_is(method, "every")) return scr_dyn_new_bool(true); + if (dyn_name_is(method, "find")) return scr_dyn_retain(scr_dyn_undefined()); + if (dyn_name_is(method, "findIndex")) return scr_dyn_new_num(-1); return scr_dyn_retain(scr_dyn_undefined()); /* forEach */ } - if ((mid == DYN_M_reduce) || (mid == DYN_M_reduceRight)) { + if (dyn_name_is(method, "reduce") || dyn_name_is(method, "reduceRight")) { if (!dyn_cb_check(args, argc)) return NULL; - bool backward = (mid == DYN_M_reduceRight); + bool backward = dyn_name_is(method, "reduceRight"); ScrDyn *acc = argc > 1 ? scr_dyn_retain(args[1]) : NULL; ScrDyn *visible_recv = callback_recv ? callback_recv : recv; for (size_t step = 0; step < len; step++) { @@ -1482,7 +1458,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if ((mid == DYN_M_flatMap)) { + if (dyn_name_is(method, "flatMap")) { /* JS Array.prototype.flatMap over a dyn array: map + a depth-1 * flatten. Native dyn-array results flatten element-by-element; a * WRAPPED engine array flattens through the routed keyed reads @@ -1520,7 +1496,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if ((mid == DYN_M_sort)) { + if (dyn_name_is(method, "sort")) { ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined(); if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) { /* V8 appends the received value's string image. */ @@ -1534,7 +1510,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (len > 1 && !dyn_arr_sort(recv, cmp->kind == SCR_DYN_FUNC ? cmp : NULL)) return NULL; return scr_dyn_retain(recv); } - if ((mid == DYN_M_toSorted)) { + if (dyn_name_is(method, "toSorted")) { ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined(); if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) { static const char message[] = "The comparison function must be either a function or undefined"; @@ -1549,7 +1525,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_arr_proto_unimpl(mid)) { + if (dyn_arr_proto_unimpl(method)) { dyn_throw_unsupported("Array", method); return NULL; } @@ -1565,14 +1541,14 @@ static ScrDyn *scr_dyn_invoke_impl( #ifdef SCR_LIB scr_trap("scriptc: promise dispatch is unavailable in library mode\n"); #else - if ((mid == DYN_M_then)) { + if (dyn_name_is(method, "then")) { return scr_dyn_promise_then(recv->v.promise, argc >= 1 ? args[0] : NULL, argc >= 2 ? args[1] : NULL, NULL); } - if ((mid == DYN_M_catch)) { + if (dyn_name_is(method, "catch")) { return scr_dyn_promise_then(recv->v.promise, NULL, argc >= 1 ? args[0] : NULL, NULL); } - if ((mid == DYN_M_finally)) { + if (dyn_name_is(method, "finally")) { return scr_dyn_promise_then(recv->v.promise, NULL, NULL, argc >= 1 ? args[0] : NULL); } dyn_throw_not_fn(what); @@ -1624,44 +1600,44 @@ static ScrDyn *scr_dyn_invoke_impl( } /* The typed array methods below are not DataView's; it inherits * Object.prototype's. */ - if ((mid == DYN_M_toString) || (mid == DYN_M_toLocaleString)) { + if (dyn_name_is(method, "toString") || dyn_name_is(method, "toLocaleString")) { ScrStr *tag = scr_str_new("[object DataView]", 17); ScrDyn *out = scr_dyn_new_str(tag); scr_str_release(tag); return out; } - if ((mid == DYN_M_valueOf)) return scr_dyn_retain(recv); + if (dyn_name_is(method, "valueOf")) return scr_dyn_retain(recv); dyn_throw_not_fn(what); return NULL; } size_t blen = bytes->len; - if ((mid == DYN_M_at)) { + if (dyn_name_is(method, "at")) { double iD = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double idx = iD < 0 ? (double)blen + iD : iD; if (idx < 0 || idx >= (double)blen) return scr_dyn_retain(scr_dyn_undefined()); return scr_dyn_new_num(scr_bytes_get(bytes, idx)); } - if ((mid == DYN_M_slice) || (mid == DYN_M_subarray)) { + if (dyn_name_is(method, "slice") || dyn_name_is(method, "subarray")) { /* subarray and Buffer.slice alias their source. TypedArray.slice * owns an independent copy; all keep the receiver's Buffer flavor. */ double startD = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double endD = dyn_bytes_index_arg(args, argc, 1, (double)blen); if (scr_exc_pending()) return NULL; - ScrBytes *out = recv->buffer || (mid == DYN_M_subarray) + ScrBytes *out = recv->buffer || dyn_name_is(method, "subarray") ? scr_bytes_subarray(bytes, startD, endD) : scr_bytes_slice(bytes, startD, endD); ScrDyn *d = recv->buffer ? scr_dyn_new_buffer(out) : scr_dyn_new_bytes(out); scr_bytes_release(out); return d; } - if ((mid == DYN_M_set)) { + if (dyn_name_is(method, "set")) { double offset = dyn_bytes_index_arg(args, argc, 1, 0); if (scr_exc_pending()) return NULL; scr_bytes_set_from_dyn(bytes, argc ? args[0] : scr_dyn_undefined(), offset); return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); } - if ((mid == DYN_M_copyWithin)) { + if (dyn_name_is(method, "copyWithin")) { double target = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double start = dyn_bytes_index_arg(args, argc, 1, 0); @@ -1671,7 +1647,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_bytes_release(scr_bytes_copy_within(bytes, target, start, end)); return scr_dyn_retain(recv); } - if ((mid == DYN_M_fill) && !recv->buffer) { + if (dyn_name_is(method, "fill") && !recv->buffer) { double value; if (!scr_dyn_number_coerce_js(argc ? args[0] : scr_dyn_undefined(), &value)) return NULL; double start = dyn_bytes_index_arg(args, argc, 1, 0); @@ -1681,8 +1657,8 @@ static ScrDyn *scr_dyn_invoke_impl( scr_bytes_release(scr_bytes_fill_elem(bytes, value, start, end)); return scr_dyn_retain(recv); } - if ((mid == DYN_M_join) || ((mid == DYN_M_toString) && !recv->buffer)) { - ScrStr *separator = (mid == DYN_M_join) && argc && args[0]->kind != SCR_DYN_UNDEF + if (dyn_name_is(method, "join") || (dyn_name_is(method, "toString") && !recv->buffer)) { + ScrStr *separator = dyn_name_is(method, "join") && argc && args[0]->kind != SCR_DYN_UNDEF ? scr_dyn_string_coerce_js(args[0]) : scr_str_new(",", 1); if (scr_exc_pending()) { scr_str_release(separator); return NULL; } ScrStr *joined = scr_bytes_join(bytes, separator); @@ -1691,7 +1667,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(joined); return out; } - if (dyn_bytes_proto_real(mid)) { + if (dyn_bytes_proto_real(method)) { dyn_throw_unsupported(scr_bytes_elem_name(bytes->elem), method); return NULL; } @@ -1878,6 +1854,8 @@ static const char dyn_s29[] SCR_DYN_COMPAT_STR = "Date"; static const char dyn_s30[] SCR_DYN_COMPAT_STR = "Map"; static const char dyn_s31[] SCR_DYN_COMPAT_STR = "Set"; static const char dyn_s32[] SCR_DYN_COMPAT_STR = ""; +static const char dyn_s33[] SCR_DYN_COMPAT_STR = "constructing %s%s through a stored value is not supported yet"; +static const char dyn_s34[] SCR_DYN_COMPAT_STR = " from a primitive"; static const char *const dyn_global_ctor_names[] = { dyn_s26, dyn_s29, dyn_s25, dyn_s30, dyn_s31, dyn_s21, dyn_s22, dyn_s24, @@ -1911,12 +1889,42 @@ static ScrDyn *dyn_global_ctor_call(ScrClosure *closure, ScrDyn *const *args, si return NULL; } +/* `new C(...args)` through a stored Object, Date, Map, Set, WeakMap or + * WeakSet builds what the `new` expression builds. RegExp, Promise, and + * Object over a primitive (a wrapper object) are fenced. */ +static bool dyn_global_construct(const ScrDyn *callee, const ScrDyn *args, ScrDyn **result) { + if (callee->kind != SCR_DYN_FUNC || callee->v.fn.thunk != dyn_global_ctor_call) return false; + const char *name = callee->v.fn.clo->fn; + ScrDyn *const *items = args->v.arr.items; + size_t argc = args->v.arr.len; + const ScrDyn *first = argc ? items[0] : scr_dyn_undefined(); + bool primitive = first->kind == SCR_DYN_NUM || first->kind == SCR_DYN_STR || first->kind == SCR_DYN_BOOL || + first->kind == SCR_DYN_BIGINT || first->kind == SCR_DYN_SYMBOL; + if (strcmp(name, dyn_s26) == 0 && !primitive) *result = dyn_global_ctor_call(callee->v.fn.clo, items, argc); + else if (strcmp(name, dyn_s29) == 0) + *result = argc == 0 ? scr_dyn_native_date_new(args) + : argc == 1 ? scr_dyn_native_date_new_value(args) + : scr_dyn_native_date_new_fields(args); + else if (strcmp(name, dyn_s30) == 0) *result = scr_dyn_native_map_new(first); + else if (strcmp(name, dyn_s31) == 0) *result = scr_dyn_native_set_new(first); + else if (strcmp(name, dyn_s22) == 0) *result = scr_weak_map_new((ScrDyn *)first); + else if (strcmp(name, dyn_s24) == 0) *result = scr_weak_set_new((ScrDyn *)first); + else { + char message[128]; + int length = snprintf(message, sizeof message, dyn_s33, name, strcmp(name, dyn_s26) == 0 ? dyn_s34 : dyn_s32); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, dyn_s27); + *result = NULL; + } + return true; +} + static ScrDyn *dyn_global_ctor(const char *name) { for (size_t i = 0; i < DYN_GLOBAL_CTORS; i++) { if (strcmp(dyn_global_ctor_names[i], name) != 0) continue; if (!dyn_global_ctors_registered) { dyn_global_ctors_registered = true; scr_atexit(dyn_global_ctors_cleanup); + scr_dyn_global_construct_fn = dyn_global_construct; } if (!dyn_global_ctors[i]) { /* Each constructor's own length: Object 1, Date 7, RegExp 2, Promise 1, diff --git a/packages/runtime/src/scr_dyn_methods.h b/packages/runtime/src/scr_dyn_methods.h deleted file mode 100644 index 49e5c7254..000000000 --- a/packages/runtime/src/scr_dyn_methods.h +++ /dev/null @@ -1,160 +0,0 @@ -/* Generated by scripts/gen-dyn-methods.mjs from the DYN_M_ ids scr_dyn_invoke.c - * uses. Do not edit by hand. */ -#ifndef SCR_DYN_METHODS_H -#define SCR_DYN_METHODS_H - -#include -#include - -enum { - DYN_M_NONE, - DYN_M_apply, - DYN_M_at, - DYN_M_bind, - DYN_M_call, - DYN_M_catch, - DYN_M_concat, - DYN_M_copyWithin, - DYN_M_entries, - DYN_M_every, - DYN_M_fill, - DYN_M_filter, - DYN_M_finally, - DYN_M_find, - DYN_M_findIndex, - DYN_M_findLast, - DYN_M_findLastIndex, - DYN_M_flat, - DYN_M_flatMap, - DYN_M_forEach, - DYN_M_includes, - DYN_M_indexOf, - DYN_M_join, - DYN_M_keys, - DYN_M_lastIndexOf, - DYN_M_map, - DYN_M_pop, - DYN_M_push, - DYN_M_reduce, - DYN_M_reduceRight, - DYN_M_reverse, - DYN_M_set, - DYN_M_shift, - DYN_M_slice, - DYN_M_some, - DYN_M_sort, - DYN_M_splice, - DYN_M_subarray, - DYN_M_then, - DYN_M_toExponential, - DYN_M_toFixed, - DYN_M_toLocaleString, - DYN_M_toReversed, - DYN_M_toSorted, - DYN_M_toSpliced, - DYN_M_toString, - DYN_M_unshift, - DYN_M_valueOf, - DYN_M_values, - DYN_M_with, - DYN_M_COUNT -}; - -static const char *const dyn_method_names[DYN_M_COUNT] = { - NULL, - "apply", - "at", - "bind", - "call", - "catch", - "concat", - "copyWithin", - "entries", - "every", - "fill", - "filter", - "finally", - "find", - "findIndex", - "findLast", - "findLastIndex", - "flat", - "flatMap", - "forEach", - "includes", - "indexOf", - "join", - "keys", - "lastIndexOf", - "map", - "pop", - "push", - "reduce", - "reduceRight", - "reverse", - "set", - "shift", - "slice", - "some", - "sort", - "splice", - "subarray", - "then", - "toExponential", - "toFixed", - "toLocaleString", - "toReversed", - "toSorted", - "toSpliced", - "toString", - "unshift", - "valueOf", - "values", - "with", -}; - -static const unsigned char dyn_method_lengths[DYN_M_COUNT] = { - 0, 5, 2, 4, 4, 5, 6, 10, 7, 5, 4, 6, 7, 4, 9, 8, - 13, 4, 7, 7, 8, 7, 4, 4, 11, 3, 3, 4, 6, 11, 7, 3, - 5, 5, 4, 4, 6, 8, 4, 13, 7, 14, 10, 8, 9, 8, 7, 7, - 6, 4, -}; - -/* Each name's id at its slot (or the next free one); 0 ends a probe. */ -static const unsigned char dyn_method_slots[256] = { - 0, 0, 0, 0, 12, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 14, 0, 0, 27, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 28, 3, 0, - 22, 0, 20, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, - 0, 0, 0, 0, 0, 33, 31, 0, 0, 0, 0, 0, 36, 35, 0, 0, - 0, 0, 0, 0, 23, 4, 32, 0, 5, 0, 0, 0, 0, 0, 0, 29, - 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 6, 0, 0, 37, 0, - 0, 40, 0, 0, 0, 0, 0, 0, 43, 0, 0, 45, 0, 0, 0, 44, - 0, 7, 0, 0, 0, 0, 42, 0, 25, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, - 46, 0, 0, 39, 0, 41, 0, 9, 0, 0, 13, 0, 0, 0, 0, 8, - 0, 47, 10, 0, 0, 0, 0, 48, 0, 0, 17, 0, 0, 49, 0, 0, - 0, 0, 0, 19, 0, 0, 11, 0, 0, 0, 0, 18, 0, 0, 0, 0, -}; - -#define DYN_METHOD_MAX_LENGTH 14 - -/* The id of a method name, compared by content; DYN_M_NONE for any other - * name. */ -static inline int dyn_method_of(const char *m) { - size_t length = strlen(m); - if (length == 0 || length > DYN_METHOD_MAX_LENGTH) return DYN_M_NONE; - unsigned slot = (unsigned)(length * 7 + (unsigned char)m[0] * 31 + - (unsigned char)m[length - 1]) & 255; - for (;;) { - unsigned id = dyn_method_slots[slot]; - if (id == 0) return DYN_M_NONE; - if (dyn_method_lengths[id] == length && memcmp(m, dyn_method_names[id], length) == 0) return (int)id; - slot = (slot + 1) & 255; - } -} - -#endif diff --git a/packages/runtime/src/scr_dyn_methods.test.ts b/packages/runtime/src/scr_dyn_methods.test.ts deleted file mode 100644 index 77911aa2d..000000000 --- a/packages/runtime/src/scr_dyn_methods.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { join } from "node:path"; -import { expect, test } from "vitest"; - -test("scr_dyn_methods.h lists every method id the checked-dynamic dispatch compares", () => { - const script = join(import.meta.dirname, "../../../scripts/gen-dyn-methods.mjs"); - expect(() => - execFileSync(process.execPath, [script, "--check"], { stdio: "pipe" }), - ).not.toThrow(); -}); diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 735183b89..96e90e47c 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -44,7 +44,9 @@ typedef struct SfChunk { } SfChunk; struct SfReadRequest { - ScrPromise *promise; + ScrPromise *promise; /* or NULL for a sink request */ + const ScrFetchReadSink *sink; /* readableStreamDefaultReaderRead's read request */ + void *sink_ctx; SfReadRequest *next; }; @@ -776,6 +778,11 @@ static SfReadRequest *sf_reader_take_request(SfReader *r) { static void sf_reader_reject_message_all(SfReader *r, const char *message) { SfReadRequest *request; while ((request = sf_reader_take_request(r)) != NULL) { + if (request->sink) { + request->sink->error(request->sink_ctx, NULL); + free(request); + continue; + } sf_type_error(message); scr_promise_reject_pending(request->promise); scr_promise_release(request->promise); @@ -958,6 +965,12 @@ static void sf_reader_fulfill_one(SfReader *r, bool done, ScrDyn *value) { if (!r) return; SfReadRequest *request = sf_reader_take_request(r); if (!request) return; + if (request->sink) { + if (done) request->sink->close(request->sink_ctx); + else request->sink->chunk(request->sink_ctx, value); + free(request); + return; + } scr_promise_fulfill_ref(request->promise, sf_read_result(done, value), &scr_dyn_retain_v, &scr_dyn_release_v, NULL); scr_promise_release(request->promise); @@ -968,6 +981,11 @@ static void sf_reader_reject_all(SfReader *r, ScrDyn *reason) { if (!r) return; SfReadRequest *request; while ((request = sf_reader_take_request(r)) != NULL) { + if (request->sink) { + request->sink->error(request->sink_ctx, reason); + free(request); + continue; + } sf_reject_promise_reason(request->promise, reason); scr_promise_release(request->promise); free(request); @@ -1654,18 +1672,21 @@ static SfReader *sf_stream_get_reader(SfStream *s) { return r; } -static ScrPromise *sf_reader_read(SfReader *r) { - ScrPromise *p = scr_promise_new(); +/* readableStreamDefaultReaderRead: queue the request, then serve it from + * the queue or a pull. false (TypeError pending) for a released reader. */ +static bool sf_reader_read_request(SfReader *r, ScrPromise *p, const ScrFetchReadSink *sink, + void *sink_ctx) { SfStream *s = r->stream; if (!s || s->reader != r) { sf_type_error("This reader has been released"); - scr_promise_reject_pending(p); - return p; + return false; } s->disturbed = true; SfReadRequest *request = calloc(1, sizeof *request); if (!request) sf_oom(); - request->promise = scr_promise_retain(p); + request->promise = p ? scr_promise_retain(p) : NULL; + request->sink = sink; + request->sink_ctx = sink_ctx; if (r->pending_tail) r->pending_tail->next = request; else r->pending_head = request; r->pending_tail = request; @@ -1674,6 +1695,12 @@ static ScrPromise *sf_reader_read(SfReader *r) { sf_stream_pull(s); sf_stream_drain(s); } + return true; +} + +static ScrPromise *sf_reader_read(SfReader *r) { + ScrPromise *p = scr_promise_new(); + if (!sf_reader_read_request(r, p, NULL, NULL)) scr_promise_reject_pending(p); return p; } @@ -1960,6 +1987,15 @@ ScrDyn *scr_fetch_stream_from_string(ScrStr *iterable) { return out; } +bool scr_fetch_reader_read_into(ScrDyn *reader, const ScrFetchReadSink *sink, void *ctx) { + if (!reader || reader->kind != SCR_DYN_HANDLE || + reader->v.handle.tag != SCR_DYNH_WEB_READER) { + sf_type_error("Expected a ReadableStreamDefaultReader"); + return false; + } + return sf_reader_read_request((SfReader *)reader->v.handle.ptr, NULL, sink, ctx); +} + ScrPromise *scr_fetch_reader_read(ScrDyn *reader) { if (!reader || reader->kind != SCR_DYN_HANDLE || reader->v.handle.tag != SCR_DYNH_WEB_READER) { diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index c4397e021..c3dac861d 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -200,5 +200,17 @@ ScrDyn *scr_fetch_stream_from_bytes( ScrDyn *scr_fetch_stream_from_string( ScrStr *iterable); /* borrowed string; +1 handle */ ScrPromise *scr_fetch_reader_read(ScrDyn *reader); /* borrowed handle; +1 */ +/* A read request served by callbacks instead of a promise (the streams + * spec's readableStreamDefaultReaderRead with a read request; Node's + * Readable.fromWeb adapter uses one). chunk and error borrow their value; + * error's reason is NULL when the reader was released. */ +typedef struct ScrFetchReadSink { + void (*chunk)(void *ctx, ScrDyn *value); + void (*close)(void *ctx); + void (*error)(void *ctx, ScrDyn *reason); +} ScrFetchReadSink; +/* Queue a sink read request on a reader handle (borrowed). false with a + * TypeError pending for a value that is not a reader or a released one. */ +bool scr_fetch_reader_read_into(ScrDyn *reader, const ScrFetchReadSink *sink, void *ctx); #endif /* SCR_GENERATOR_H */ diff --git a/packages/runtime/src/scr_itable.c b/packages/runtime/src/scr_itable.c index a333aab95..25a074f49 100644 --- a/packages/runtime/src/scr_itable.c +++ b/packages/runtime/src/scr_itable.c @@ -472,7 +472,7 @@ static ScrArrState scr_itable_element(ScrITableElements *w, size_t i, uint64_t * } if (i < a->cap) { *slot = a->data[i]; - return (ScrArrState)*scr_arr_state_ptr(a, i); + return (ScrArrState)scr_arr_dense_state(a, i); } if (scr_arr_ext_of(a)) { while (w->sparse < scr_arr_ext_of(a)->sparse_len && scr_arr_ext_of(a)->sparse[w->sparse].index < i) w->sparse++; diff --git a/packages/runtime/src/scr_itable.test.c b/packages/runtime/src/scr_itable.test.c index f6d98654a..c85370add 100644 --- a/packages/runtime/src/scr_itable.test.c +++ b/packages/runtime/src/scr_itable.test.c @@ -55,6 +55,7 @@ static ScrArr *make_f64s(const double *values, size_t n) { a->filled = (uint32_t)n; a->elem = SCR_ELEM_F64; /* Values, then one state byte per slot (scr_arr_states_end). */ + a->spare = SCR_ARR_STATES; a->data = calloc(n ? n : 1, sizeof(uint64_t) + 1); for (size_t i = 0; i < n; i++) memcpy(&a->data[i], &values[i], sizeof(double)); memset(scr_arr_state_run(a, 0, n), SCR_ARR_VALUE, n); @@ -170,6 +171,7 @@ int main(void) { assert(scr_itable_size(lists) == 4); ScrArr *big = make_f64s(NULL, 0); big->len = big->cap = big->filled = 100; + big->spare = 0; /* values without states */ free(big->data); big->data = calloc(100, sizeof(uint64_t)); for (unsigned i = 0; i < 100; i++) memcpy(&big->data[i], &(double){i}, sizeof(double)); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index ee45f6d93..bba30a5db 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1021,6 +1021,7 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) { } else if (kind == SCR_DYN_OBJ) { d->v.obj.len = 0; /* cap/entries preserved */ d->v.obj.region = NULL; + d->v.obj.class_name = NULL; /* a released class prototype's name */ SCR_JS_SHAPE_RESET(d); if (d->v.obj.cap <= SCR_DYN_KEY_CACHE_SLOTS) { for (size_t i = 0; i < d->v.obj.cap; i++) { @@ -3594,7 +3595,8 @@ bool scr_dyn_typed_ref_class_has(const ScrDyn *d, const ScrStr *key) { static const ScrDynTypedRecordOps *scr_typed_ref_record_ops(const ScrDyn *d) { if (!d->v.typed_ref.record || d->v.typed_ref.materialized) return NULL; - return (const ScrDynTypedRecordOps *)(const void *)d->v.typed_ref.array; + const ScrDynTypedRecordOps *ops = (const ScrDynTypedRecordOps *)(const void *)d->v.typed_ref.array; + return ops && ops->select ? ops->select(d->v.typed_ref.ptr) : ops; } const ScrDynTypedRecordOps *scr_dyn_typed_ref_live_record(const ScrDyn *d) { @@ -4051,6 +4053,8 @@ bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const Sc bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { return scr_bytes_instanceof_at(value, callee, NULL); } +SCR_TL bool (*scr_dyn_global_construct_fn)(const ScrDyn *callee, const ScrDyn *args, ScrDyn **result) = NULL; + ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) { if (callee->kind == SCR_DYN_FUNC && scr_array_ctor && callee->v.fn.clo == scr_array_ctor->v.fn.clo) return scr_array_ctor_call(callee->v.fn.clo, args->v.arr.items, args->v.arr.len); @@ -4067,6 +4071,9 @@ ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrS scr_bytes_release(bytes); return result; } + ScrDyn *constructed = NULL; + if (scr_dyn_global_construct_fn && scr_dyn_global_construct_fn(callee, args, &constructed)) + return constructed; ScrJsonBuf message; scr_jb_init(&message); scr_jb_write(&message, what->data, what->len); @@ -4221,6 +4228,11 @@ ScrDyn *scr_dyn_class_prototype(ScrDyn *value, ScrDyn *base) { if (!cls->prototype_data) { ScrDyn *data = base->kind == SCR_DYN_UNDEF ? scr_dyn_new_obj() : scr_dyn_obj_create(base); if (!data) return NULL; + /* util.inspect prints an instance with its class's name: the class + * object's interned name, which is immortal. The prototype is owned by + * the class object and no registry keeps it alive, so evaluating a + * function-local class again releases the previous one's prototype. */ + if (cls->name && cls->name->len) data->v.obj.class_name = cls->name->data; cls->prototype_data = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); scr_box_set_ref(cls->prototype_data, data); } @@ -14980,6 +14992,27 @@ ScrDyn *scr_dyn_native_map(ScrMap *value) { return scr_dyn_new_handle(value, SCR_DYNH_MAP); } +ScrDyn *scr_dyn_native_map_new(const ScrDyn *seed) { + ScrDyn *entries = scr_dyn_map_seed_entries(seed); + if (!entries) return NULL; + ScrMap *map = scr_map_new(SCR_MAP_KEY_DYN, SCR_MAP_VAL_REF, &scr_map_dyn_map_ops); + for (size_t i = 0; i < entries->v.arr.len; i++) { + ScrDyn *entry = scr_dyn_map_seed_entry(entries->v.arr.items[i]); + if (!entry) break; + ScrDyn *key = scr_dyn_entry_read(entry, 0); + ScrDyn *value = scr_exc_pending() ? NULL : scr_dyn_entry_read(entry, 1); + scr_dyn_release(entry); + if (value) scr_map_dyn_set(map, key, value, false); + scr_dyn_release(key); + scr_dyn_release(value); + if (scr_exc_pending()) break; + } + scr_dyn_release(entries); + ScrDyn *result = scr_exc_pending() ? NULL : scr_dyn_native_map(map); + scr_map_release(map); + return result; +} + bool scr_dyn_native_map_is(const ScrDyn *value) { return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_MAP; } diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 37ca28f16..c17f44f9d 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -12961,50 +12961,51 @@ static bool scr_set_to_arr_sparse(const ScrMap *s, ScrArr *out) { ScrArr *scr_set_to_arr_f64(const ScrMap *s) { ScrArr *out = scr_arr_new(SCR_ELEM_F64, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; - uint8_t *states = scr_arr_states_end(out); + /* A fresh array without states: the values in order. */ for (size_t i = 0; i < s->nentries; i++) { if (!scr_map_entry_live(s, i)) continue; - out->data[out->len] = scr_map_entry_at(s, i)->key; - *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; + out->data[out->len++] = scr_map_entry_at(s, i)->key; } + scr_arr_refill(out); return out; } ScrArr *scr_set_to_arr_bool(const ScrMap *s) { ScrArr *out = scr_arr_new(SCR_ELEM_BOOL, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; - uint8_t *states = scr_arr_states_end(out); + /* A fresh array without states: the values in order. */ for (size_t i = 0; i < s->nentries; i++) { if (!scr_map_entry_live(s, i)) continue; - scr_arr_dense_put(out, out->len, scr_map_entry_at(s, i)->key != 0); - *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; + scr_arr_dense_put(out, out->len++, scr_map_entry_at(s, i)->key != 0); } + scr_arr_refill(out); return out; } ScrArr *scr_set_to_arr_str(const ScrMap *s) { ScrArr *out = scr_arr_new(SCR_ELEM_STR, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; - uint8_t *states = scr_arr_states_end(out); + /* A fresh array without states: the values in order. */ for (size_t i = 0; i < s->nentries; i++) { if (!scr_map_entry_live(s, i)) continue; scr_str_retain((ScrStr *)(uintptr_t)scr_map_entry_at(s, i)->key); - out->data[out->len] = scr_map_entry_at(s, i)->key; - *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; + out->data[out->len++] = scr_map_entry_at(s, i)->key; } + scr_arr_refill(out); return out; } ScrArr *scr_set_to_arr_ref(const ScrMap *s, const ScrArrOps *ops) { ScrArr *out = scr_arr_new_ops(ops, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; - uint8_t *states = scr_arr_states_end(out); + /* A fresh array without states: the values in order. */ for (size_t i = 0; i < s->nentries; i++) { if (!scr_map_entry_live(s, i)) continue; ops->retain((void *)(uintptr_t)scr_map_entry_at(s, i)->key); scr_arr_dense_put(out, out->len, scr_map_entry_at(s, i)->key); - *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; + out->len++; } + scr_arr_refill(out); return out; } diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 648d1bfc8..1a6a3fc00 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -1706,7 +1706,7 @@ ScrMap *scr_map_clone(const ScrMap *source, bool keys_only, const ScrMapOps *ops const ScrMapOps scr_map_dyn_set_ops = { scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, NULL, NULL, NULL, NULL }; -static const ScrMapOps scr_map_dyn_map_ops = { +const ScrMapOps scr_map_dyn_map_ops = { scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, NULL }; @@ -1779,7 +1779,7 @@ void scr_set_add_all(ScrMap *set, ScrArr *values) { /* Packed values stay owned by the seed array throughout construction. * Avoid taking a temporary owner only to release it after insertion. * Sparse/missing positions retain the existing accessor semantics. */ - if (i < values->cap && *scr_arr_state_ptr(values, i) == SCR_ARR_VALUE) { + if (i < values->cap && scr_arr_dense_state(values, i) == SCR_ARR_VALUE) { uint64_t key = scr_arr_dense_get(values, i); if (set->key_kind == SCR_MAP_KEY_F64) key = scr_map_f64_bits(scr_map_slot_to_f64(key)); scr_map_set(set, scr_map_hash_key(set, key), key, 0); diff --git a/packages/runtime/src/scr_map.h b/packages/runtime/src/scr_map.h index 1f6e3ee12..d1ca387bf 100644 --- a/packages/runtime/src/scr_map.h +++ b/packages/runtime/src/scr_map.h @@ -243,8 +243,10 @@ void *scr_map_get_span_ref(const ScrMap *m, const char *bytes, size_t length); * adapters); NULL keeps the source's descriptor. */ ScrMap *scr_map_clone(const ScrMap *source, bool keys_only, const ScrMapOps *ops); ScrMap *scr_map_clone_dyn(const ScrMap *source, bool keys_only); -/* The descriptor of a Set of checked values (SCR_MAP_KEY_DYN keys). */ +/* The descriptors of a Set and a Map of checked values (SCR_MAP_KEY_DYN + * keys; the Map's values are checked values too). */ extern const ScrMapOps scr_map_dyn_set_ops; +extern const ScrMapOps scr_map_dyn_map_ops; void scr_map_values_into(ScrMap *set, const ScrMap *source, bool checked); void scr_map_dyn_attach(ScrMap *map, const ScrMapDynOps *ops); diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 44f248b5b..4767122dd 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -204,7 +204,7 @@ static void scr_pub_slot(ScrPub *ctx, uint64_t slot, ScrPubFn fn) { static __attribute__((noinline)) void scr_pub_cref_elems(ScrArr *a, ScrPub *ctx, ScrPubFn fn) { size_t dense = a->len < a->cap ? a->len : a->cap; const uint32_t *refs = (const uint32_t *)a->data; - const uint8_t *states = scr_arr_states_end_w(a, sizeof(uint32_t)); + const uint8_t *states = scr_arr_has_states(a) ? scr_arr_states_end_w(a, sizeof(uint32_t)) : NULL; for (size_t i = 0; i < dense; i++) if (!states || *scr_arr_state_in(states, i)) scr_pub_slot(ctx, scr_cref_decode(refs[i]), fn); if (scr_arr_ext_of(a)) @@ -227,7 +227,7 @@ void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { if (!fn) return; /* String and reference elements have 8-byte slots. */ size_t dense = a->len < a->cap ? a->len : a->cap; - const uint8_t *states = scr_arr_states_end_w(a, sizeof(uint64_t)); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < dense; i++) if (!states || *scr_arr_state_in(states, i)) scr_pub_slot(ctx, a->data[i], fn); if (scr_arr_ext_of(a)) diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index b62cba896..158697a14 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -692,7 +692,7 @@ static ScrDyn *scr_regex_groups_of(const ScrArr *a, uint8_t *bc) { for (int k = 1; k < capture_count; k++) { size_t length = strlen(name); bool matched = (size_t)k < a->len && (size_t)k < a->cap && - *scr_arr_state_ptr(a, (size_t)k) == SCR_ARR_VALUE; + scr_arr_dense_state(a, (size_t)k) == SCR_ARR_VALUE; /* ES2025 duplicates sit in distinct alternatives: at most one * participates, and it wins over an earlier undefined. */ if (length && (matched || !scr_dyn_obj_get(groups, name, length))) @@ -845,7 +845,7 @@ static ScrArr *scr_regex_exec_core(ScrStr *s, ScrRegex *re, uint64_t mask, bool if (rc == 1) { int count = lre_get_capture_count(bc); bool spans = lazy != 0 && !input.wide && (count >= 64 || ((mask | lazy) >> count) == 0); - out = scr_arr_new_inline(SCR_ELEM_STR, (size_t)count + spans); + out = scr_arr_new_inline(SCR_ELEM_STR, (size_t)count + spans, true); /* A fresh dense array: every slot is written directly, then the * hole-free prefix is the run of materialized captures from 0. Its * storage pointers are fixed (the inline block never moves here), so @@ -905,14 +905,15 @@ static bool scr_regex_group_bytes(const ScrArr *m, double k, const char **p, siz /* Array lengths stay below 2^53, so signed conversions are exact. */ if (!(k >= 0 && k < (double)(int64_t)m->len)) return false; size_t i = (size_t)(int64_t)k; - if (*scr_arr_state_ptr(m, i) == SCR_ARR_VALUE) { + uint8_t state = scr_arr_dense_state(m, i); + if (state == SCR_ARR_VALUE) { const ScrStr *str = (const ScrStr *)(uintptr_t)m->data[i]; *p = str->data; *n = str->len; return true; } uint64_t span = m->data[i]; - if (*scr_arr_state_ptr(m, i) != SCR_ARR_UNDEFINED || span == SCR_RE_NO_SPAN) return false; + if (state != SCR_ARR_UNDEFINED || span == SCR_RE_NO_SPAN) return false; const ScrStr *subject = (const ScrStr *)(uintptr_t)m->data[m->len - 1]; *p = subject->data + (span >> 32); *n = (size_t)((span & UINT32_MAX) - (span >> 32)); @@ -1043,10 +1044,13 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices int start = scr_capture_index(&input, capture[0]); int end = scr_capture_index(&input, capture[1]); /* One allocation per row, like an exec result: every slot is written - * directly, then the hole-free prefix is the run of captures from 0. */ - ScrArr *row = scr_arr_new_inline(SCR_ELEM_STR, (size_t)capture_count); + * directly, then the hole-free prefix is the run of captures from 0. + * A row needs states only for a capture that did not participate. */ + bool all = true; + for (int k = 0; k < capture_count; k++) all &= capture[2 * k] != NULL && capture[2 * k + 1] != NULL; + ScrArr *row = scr_arr_new_inline(SCR_ELEM_STR, (size_t)capture_count, !all); uint64_t *data = row->data; - uint8_t *states = scr_arr_states_end_w(row, 8); + uint8_t *states = all ? NULL : scr_arr_states_end_w(row, 8); for (int k = 0; k < capture_count; k++) { const uint8_t *cs = capture[2 * k], *ce = capture[2 * k + 1]; if (cs == NULL || ce == NULL) { @@ -1056,11 +1060,11 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices ScrStr *text = scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce)); data[k] = (uint64_t)(uintptr_t)text; - *scr_arr_state_in(states, (size_t)k) = SCR_ARR_VALUE; + if (states) *scr_arr_state_in(states, (size_t)k) = SCR_ARR_VALUE; } } size_t filled = 0; - while (filled < (size_t)capture_count && *scr_arr_state_in(states, filled) == SCR_ARR_VALUE) + while (filled < (size_t)capture_count && (!states || *scr_arr_state_in(states, filled) == SCR_ARR_VALUE)) filled++; row->filled = (uint32_t)filled; row->len = (size_t)capture_count; @@ -2795,7 +2799,7 @@ static ScrDyn *scr_native_regex_invoke(void *ptr, ScrDyn *self, const char *meth if (!row) return scr_exc_pending() ? NULL : scr_dyn_new_null(); ScrDyn *match = scr_dyn_new_arr(); for (size_t k = 0; k < row->len; k++) - scr_dyn_arr_push(match, *scr_arr_state_ptr(row, k) == SCR_ARR_VALUE + scr_dyn_arr_push(match, scr_arr_dense_state(row, k) == SCR_ARR_VALUE ? scr_dyn_new_str((ScrStr *)(uintptr_t)row->data[k]) : scr_dyn_retain(scr_dyn_undefined())); scr_arr_copy_metadata(row, match); diff --git a/packages/runtime/src/scr_region.c b/packages/runtime/src/scr_region.c index dbd74f3ac..25bbcf7ae 100644 --- a/packages/runtime/src/scr_region.c +++ b/packages/runtime/src/scr_region.c @@ -805,7 +805,7 @@ static void scr_rgn_walk_arr(ScrRgnWalk *w, ScrArr *a) { ScrRgnSlotKind k = a->elem == SCR_ELEM_ARR ? (ScrRgnSlotKind){SCR_RGN_W_ARR, NULL} : scr_rgn_slot_kind(scr_arr_ops(a) ? scr_arr_ops(a)->release : NULL); size_t dense = a->len < a->cap ? a->len : a->cap; - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < dense; i++) if (!states || *scr_arr_state_in(states, i) == SCR_ARR_VALUE) scr_rgn_slot_as(w, scr_rgn_slot_ptr(a->data[i]), k); if (!scr_arr_ext_of(a)) return; diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index be5ccb910..275c03039 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -145,9 +145,12 @@ struct ScrStreamState { bool object_mode_opt; /* The parked for-await next() promise (at most one — the loop awaits * each chunk before asking again). next_dyn picks the resolution - * shape: a dyn boxed by tag (the JS lane), or bytes. */ + * shape: a dyn boxed by tag (the JS lane), or bytes; next_result + * wraps the dyn in an iterator result ({ value, done }) for the + * checked-dynamic for-await protocol. */ ScrPromise *next_waiter; bool next_dyn; + bool next_result; /* The iterator's own read() is calling _read: synchronous pushes * buffer, and the waiter takes the whole buffer afterwards (Node's * read() returns everything _read pushed). */ @@ -186,6 +189,19 @@ struct ScrStreamState { * and transform steps receive, are ScrDyn* values (in the ScrBytes* * slots), each counting 1 toward length. */ bool object_mode; + /* The queue holds ScrDyn* values: on an object-mode side, and with + * decodeStrings: false, where strings stay strings (their UTF-16 + * length counts) and Buffers are buffer values. Each entry's encoding + * argument rides in ScrWEntry.enc. */ + bool values, decode_strings_off; + /* The defaultEncoding option or setDefaultEncoding(), as given; NULL + * is utf8. What write(string) encodes with, and the encoding _write + * receives for a string when decodeStrings is false. */ + ScrStr *default_enc; + /* _writev: more than one buffered write reaches it as one batch, and a + * stream without _write sends each write through it. */ + ScrClosure *writev_cb; + ScrStreamWritevInv writev_inv; bool ending, finished, prefinished, final_called, finish_scheduled; bool final_pending; /* _final called, its callback not yet run */ ScrClosure *inflight_cb; /* owned or NULL */ @@ -252,6 +268,7 @@ struct ScrStreamState { * path and _destroy wait. A destroy() in that window marks the stream * destroyed and runs its _destroy once construction completes. */ bool constructing, destroy_deferred; + bool construct_finished; /* onConstruct ran: a second callback is ERR_MULTIPLE_CALLBACK */ ScrError *destroy_err; /* owned: the deferred destroy's error */ ScrClosure *construct_cb; ScrStreamPlainInv construct_inv; @@ -340,18 +357,25 @@ static ScrDyn *scr_stream_entry_box(void *entry, ScrEntryKind kind) { return d; } -/* Writable-queue chunks: Buffers, or dyn values on an object-mode side. */ +/* Writable-queue chunks: Buffers, or dyn values (w.values). A value + * counts 1 on an object-mode side, else its length (a string's UTF-16 + * length, as Node's chunk.length). */ static size_t scr_stream_wchunk_len(const ScrStreamState *st, ScrBytes *chunk) { - return st->w.object_mode ? 1 : chunk->len; + if (st->w.object_mode) return 1; + if (!st->w.values) return chunk->len; + ScrDyn *v = (ScrDyn *)chunk; + if (v->kind == SCR_DYN_STR) return (size_t)scr_str_utf16_len(v->v.str); + if (v->kind == SCR_DYN_BYTES) return v->v.bytes->len; + return 1; } static ScrBytes *scr_stream_wchunk_retain(const ScrStreamState *st, ScrBytes *chunk) { - if (st->w.object_mode) return (ScrBytes *)scr_dyn_retain((ScrDyn *)chunk); + if (st->w.values) return (ScrBytes *)scr_dyn_retain((ScrDyn *)chunk); return scr_bytes_retain(chunk); } static void scr_stream_wchunk_release(const ScrStreamState *st, ScrBytes *chunk) { - if (st->w.object_mode) scr_dyn_release((ScrDyn *)chunk); + if (st->w.values) scr_dyn_release((ScrDyn *)chunk); else scr_bytes_release(chunk); } @@ -377,6 +401,7 @@ static void scr_stream_state_drop(ScrStreamState *st, bool gc) { if (st->destroy_err) scr_error_release(st->destroy_err); if (!gc) { if (st->construct_cb) scr_closure_release(st->construct_cb); + if (st->w.writev_cb) scr_closure_release(st->w.writev_cb); if (st->r.read_cb) scr_closure_release(st->r.read_cb); if (st->w.write_cb) scr_closure_release(st->w.write_cb); if (st->w.final_cb) scr_closure_release(st->w.final_cb); @@ -390,13 +415,14 @@ static void scr_stream_state_drop(ScrStreamState *st, bool gc) { for (size_t i = 0; i < st->drain_srcs.n; i++) scr_stream_release(st->drain_srcs.src[i]); } for (size_t i = st->w.head; i < st->w.n; i++) { - bool traced = st->w.object_mode && scr_stream_dyn_entry_traced((ScrDyn *)st->w.q[i].chunk); + bool traced = st->w.values && scr_stream_dyn_entry_traced((ScrDyn *)st->w.q[i].chunk); if (!gc || !traced) scr_stream_wchunk_release(st, st->w.q[i].chunk); if (!gc && st->w.q[i].cb) scr_closure_release(st->w.q[i].cb); ScrDyn *enc = st->w.q[i].enc; if (enc && (!gc || !scr_stream_dyn_entry_traced(enc))) scr_dyn_release(enc); } free(st->w.q); + if (st->w.default_enc) scr_str_release(st->w.default_enc); free(st->w.end_cbs); free(st->fin.cb); free(st->fin.inv); @@ -426,6 +452,7 @@ void scr_stream_st_trace(ScrStreamState *st, ScrTraceVisit visit, void *ctx) { if (st->construct_cb) visit(st->construct_cb, ctx); if (st->w.write_cb) visit(st->w.write_cb, ctx); if (st->w.final_cb) visit(st->w.final_cb, ctx); + if (st->w.writev_cb) visit(st->w.writev_cb, ctx); if (st->w.inflight_cb) visit(st->w.inflight_cb, ctx); if (st->transform_cb) visit(st->transform_cb, ctx); if (st->flush_cb) visit(st->flush_cb, ctx); @@ -434,7 +461,7 @@ void scr_stream_st_trace(ScrStreamState *st, ScrTraceVisit visit, void *ctx) { for (size_t i = 0; i < st->fin.n; i++) visit(st->fin.cb[i], ctx); for (size_t i = st->w.head; i < st->w.n; i++) { if (st->w.q[i].cb) visit(st->w.q[i].cb, ctx); - if (st->w.object_mode && scr_stream_dyn_entry_traced((ScrDyn *)st->w.q[i].chunk)) + if (st->w.values && scr_stream_dyn_entry_traced((ScrDyn *)st->w.q[i].chunk)) visit(st->w.q[i].chunk, ctx); if (st->w.q[i].enc && scr_stream_dyn_entry_traced(st->w.q[i].enc)) visit(st->w.q[i].enc, ctx); } @@ -1032,23 +1059,10 @@ static void scr_stream_resume_kick(ScrStream *s) { static ScrError *scr_stream_dyn_err(ScrDyn *const *args, size_t argc) { if (argc < 1) return NULL; const ScrDyn *a = args[0]; - if (a->kind == SCR_DYN_UNDEF || a->kind == SCR_DYN_NULL) return NULL; - if (a->kind == SCR_DYN_STR) { - ScrError *e = scr_error_new(SCR_ERR_ERROR, a->v.str); - return e; - } - if (a->kind == SCR_DYN_OBJ) { - ScrDyn *m = scr_dyn_obj_get((ScrDyn *)a, "message", 7); - ScrStr *msg = m && m->kind == SCR_DYN_STR ? scr_str_retain(m->v.str) : scr_str_new("", 0); - ScrError *e = scr_error_new(SCR_ERR_ERROR, msg); - scr_str_release(msg); - return e; - } - /* any other truthy value: Node would wrap it — approximate with "" */ - ScrStr *empty = scr_str_new("", 0); - ScrError *e = scr_error_new(SCR_ERR_ERROR, empty); - scr_str_release(empty); - return e; + if (!scr_dyn_truthy(a)) return NULL; + if (a->kind == SCR_DYN_STR) return scr_error_new(SCR_ERR_ERROR, a->v.str); + /* the error value itself (the same object reaches 'error') */ + return scr_error_from_dyn(a); } static ScrStream *scr_stream_dyn_recv(ScrClosure *clo) { @@ -1071,6 +1085,13 @@ ScrDyn *scr_stream_done_dyn_f(ScrClosure *clo, ScrDyn *const *args, size_t argc) return scr_stream_done_dyn_ret(); } +ScrDyn *scr_stream_done_dyn_c(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + ScrStream *s = scr_stream_dyn_recv(clo); + scr_stream_construct_done(s, scr_stream_dyn_err(args, argc)); + scr_stream_release(s); + return scr_stream_done_dyn_ret(); +} + ScrDyn *scr_stream_done_dyn_d(ScrClosure *clo, ScrDyn *const *args, size_t argc) { ScrStream *s = scr_stream_dyn_recv(clo); scr_stream_destroy_done(s, scr_stream_dyn_err(args, argc)); @@ -1108,7 +1129,7 @@ bool scr_stream_push_dyn(ScrStream *s, const ScrDyn *d) { switch (d->kind) { case SCR_DYN_BYTES: { ScrBytes *b = scr_bytes_raw_view(d->v.bytes); - bool r = scr_stream_push(s, b); + bool r = scr_stream_push_view(s, b); scr_bytes_release(b); return r; } @@ -1208,7 +1229,15 @@ static void scr_stream_next_fulfill(ScrStreamState *st, ScrPromise *w /*moves*/, scr_promise_release(w); return; } - if (dyn) { + if (dyn && st->r.next_result) { + /* { value, done }: the value, or undefined at the end */ + ScrDyn *result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "value", 5, + chunk == NULL ? scr_dyn_retain(scr_dyn_undefined()) + : scr_stream_entry_box(chunk, kind)); + scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(chunk == NULL)); + payload = result; + } else if (dyn) { payload = chunk == NULL ? scr_stream_from_done() : scr_stream_entry_box(chunk, kind); } else { payload = chunk ? (ScrBytes *)chunk : scr_bytes_new(SCR_BYTES_U8, 0); @@ -1334,11 +1363,12 @@ static void scr_stream_settle_next(ScrStream *s) { } } -static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn) { +static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn, bool result) { ScrStreamState *st = s->st; ScrPromise *p = scr_promise_new(); if (!st->has_r) { st->r.next_dyn = dyn; + st->r.next_result = result; scr_stream_next_fulfill(st, scr_promise_retain(p), NULL, SCR_ENTRY_BYTES); return p; } @@ -1350,6 +1380,7 @@ static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn) { return p; } st->r.next_dyn = dyn; + st->r.next_result = result; st->r.next_waiter = scr_promise_retain(p); st->r.next_parked = false; if (!st->r.next_started) { @@ -1373,11 +1404,15 @@ ScrPromise *scr_stream_next_chunk(ScrStream *s) { scr_promise_reject_pending(p); return p; } - return scr_stream_next_chunk_impl(s, false); + return scr_stream_next_chunk_impl(s, false, false); } ScrPromise *scr_stream_next_chunk_dyn(ScrStream *s) { - return scr_stream_next_chunk_impl(s, true); + return scr_stream_next_chunk_impl(s, true, false); +} + +ScrPromise *scr_stream_next_result_dyn(ScrStream *s) { + return scr_stream_next_chunk_impl(s, true, true); } /* ── Readable.from (array, string and Buffer sources) ─────────────────── */ @@ -1516,8 +1551,21 @@ static bool scr_from_check_hwm(double hwm) { * highWaterMark 1 for iterables, the mode's default for the string / * Buffer case or a nullish option, or the option's value. +1, or NULL with the error * pending. Moves f. */ +/* from.js's `new Readable(...)`: a Readable, or for Duplex.from (duplexify.js's + * from(Duplexify, body, { objectMode: true, writable: false })) a Duplexify + * whose writable side is off. */ +static ScrStream *scr_from_new(bool duplex, double hwm, ScrClosure *read, ScrStreamReadInv read_inv, + ScrClosure *destroy, ScrStreamErrInv destroy_inv) { + if (!duplex) return scr_stream_new_readable(hwm, true, true, read, read_inv, destroy, destroy_inv); + ScrStream *d = scr_stream_new_duplex(hwm, hwm, true, true, true, true, false, read, read_inv, NULL, + NULL, NULL, NULL, destroy, destroy_inv); + scr_stream_set_object_mode(d, false, true); + d->cls = "Duplexify"; + return d; +} + static ScrStream *scr_from_stream(ScrFromSrc *f, ScrStreamReadInv read_inv, bool object_mode, - int hwm_kind, double hwm, bool strings) { + int hwm_kind, double hwm, bool strings, bool duplex) { if (hwm_kind == SCR_FROM_HWM_SET && !scr_from_check_hwm(hwm)) { scr_from_src_release(f); return NULL; @@ -1529,8 +1577,8 @@ static ScrStream *scr_from_stream(ScrFromSrc *f, ScrStreamReadInv read_inv, bool scr_box_set_ref(read->caps[0], f); /* moves */ ScrClosure *destroy = NULL; if (!f->single) destroy = scr_closure_new((void *)&scr_from_destroy_inv, 0); - ScrStream *s = scr_stream_new_readable(hwm, true, true, read, read_inv, destroy, - destroy ? &scr_from_destroy_inv : NULL); + ScrStream *s = scr_from_new(duplex, hwm, read, read_inv, destroy, + destroy ? &scr_from_destroy_inv : NULL); ScrStreamState *st = s->st; if (object_mode) { st->r.object_entries = true; @@ -1557,7 +1605,7 @@ ScrStream *scr_stream_new_from_readable(ScrClosure *read, ScrStreamReadInv read_ bool object_mode = (bits & SCR_FROM_BYTE_MODE) == 0; if (bits & SCR_FROM_DEFAULT_HWM) hwm = object_mode ? 16 : -1; else if (!(bits & SCR_FROM_HAS_HWM)) hwm = 1; - ScrStream *s = scr_stream_new_readable(hwm, true, true, read, read_inv, destroy, destroy_inv); + ScrStream *s = scr_from_new((bits & SCR_FROM_DUPLEX) != 0, hwm, read, read_inv, destroy, destroy_inv); if (object_mode) s->st->r.object_entries = true; return s; } @@ -1572,7 +1620,7 @@ ScrStream *scr_stream_from_arr(ScrArr *arr, double flags, double hwm) { f->single = (bits & SCR_FROM_SINGLE) != 0; return scr_from_stream(f, &scr_from_read_inv, (bits & SCR_FROM_BYTE_MODE) == 0, (bits & SCR_FROM_HAS_HWM) ? SCR_FROM_HWM_SET : SCR_FROM_HWM_FROM, hwm, - f->strings); + f->strings, (bits & SCR_FROM_DUPLEX) != 0); } /* The JS lane: one checked-dynamic value into the stream. null is @@ -1636,7 +1684,7 @@ static void scr_from_read_dyn_inv(ScrClosure *cb, ScrStream *s, double size) { scr_from_src_release(f); } -ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { +static ScrStream *scr_from_dyn_ex(ScrDyn *src, ScrDyn *opts, bool duplex) { bool single = src->kind == SCR_DYN_STR || src->kind == SCR_DYN_BYTES; ScrDyn *iter = NULL; /* from.js's iterator, for sources other than arrays */ int kind = 0; @@ -1648,7 +1696,14 @@ ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { if (kind < 0) return NULL; } if (!single && kind == 0) { - scr_dyn_arg_type_fail("iterable", "an instance of Iterable", src); + if (duplex) { + scr_dyn_arg_type_fail("body", + "of type function or an instance of Blob, ReadableStream, WritableStream, " + "Stream, Iterable, AsyncIterable, or Promise or { readable, writable } pair", + src); + } else { + scr_dyn_arg_type_fail("iterable", "an instance of Iterable", src); + } return NULL; } bool object_mode = true; @@ -1695,7 +1750,7 @@ ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { } } if (iter) { - double flags = (object_mode ? 0 : SCR_FROM_BYTE_MODE) | + double flags = (object_mode ? 0 : SCR_FROM_BYTE_MODE) | (duplex ? SCR_FROM_DUPLEX : 0) | (hwm_kind == SCR_FROM_HWM_SET ? SCR_FROM_HAS_HWM : hwm_kind == SCR_FROM_HWM_DEFAULT ? SCR_FROM_DEFAULT_HWM : 0); @@ -1706,7 +1761,47 @@ ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { f->rc = 1; f->dyn = scr_dyn_retain(src); f->single = single; - return scr_from_stream(f, &scr_from_read_dyn_inv, object_mode, hwm_kind, hwm, false); + return scr_from_stream(f, &scr_from_read_dyn_inv, object_mode, hwm_kind, hwm, false, duplex); +} + +ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { + return scr_from_dyn_ex(src, opts, false); +} + +/* Duplex.from(body) over a checked-dynamic body (duplexify.js): a native + * Duplex answers itself, a lone native Readable or Writable is wrapped, a + * promise reads as its value, an iterable reads as Duplex.from does, and + * the other bodies Node accepts (a function, a thenable, a { readable, + * writable } pair) are not supported yet. */ +static ScrStream *scr_stream_boxed(const ScrDyn *value); +static ScrStream *scr_dupx_from_boxed(ScrDyn *boxed); + +ScrStream *scr_stream_duplex_from_dyn(ScrDyn *body) { + ScrStream *native = scr_stream_of_handle(body); /* +1 */ + if (!native) native = scr_stream_boxed(body); /* +1 */ + if (native) { + if (native->st->has_r && native->st->has_w) return native; + /* _duplexify({ readable }) / _duplexify({ writable }) */ + ScrStream *d = native->st->has_r ? scr_stream_duplexify(native, NULL) + : scr_stream_duplexify(NULL, native); + scr_stream_release(native); + return d; + } + if (body->kind == SCR_DYN_PROMISE) return scr_dupx_from_boxed(scr_dyn_retain(body)); + bool unsupported = body->kind == SCR_DYN_FUNC; + if (!unsupported && body->kind == SCR_DYN_OBJ) { + static const char *const keys[] = {"then", "readable", "writable"}; + for (size_t i = 0; i < 3 && !unsupported; i++) { + ScrDyn *v = scr_dyn_obj_get(body, keys[i], strlen(keys[i])); + unsupported = v != NULL && v->kind != SCR_DYN_UNDEF; + } + } + if (unsupported) { + static const char msg[] = "Duplex.from over this body is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "SC2020"); + return NULL; + } + return scr_from_dyn_ex(body, NULL, true); } /* ── object-mode streams for the stream operators (scr_stream_ops.c) ──── */ @@ -1865,6 +1960,8 @@ static void scr_stream_wq_push(ScrStreamState *st, ScrBytes *chunk /*moves*/, Sc static void scr_stream_transform_callback(ScrStream *s); static void scr_stream_do_write_inner(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/); +static ScrDyn *scr_stream_wentry_value(const ScrStreamState *st, ScrBytes *chunk); +static ScrDyn *scr_stream_wentry_encoding(const ScrStreamState *st, ScrDyn *enc); /* One write reaching _write/_transform; the callback reads the write's * encoding argument through w.cur_enc while it runs. */ @@ -1891,7 +1988,7 @@ static void scr_stream_do_write_inner(ScrStream *s, ScrBytes *chunk /*moves*/, S if (st->is_transform || st->passthrough) st->xf_length = st->r.length; if (st->passthrough && st->transform_cb == NULL) { /* identity: push through, complete synchronously */ - if (st->w.object_mode) scr_stream_push_dyn(s, (ScrDyn *)chunk); + if (st->w.values) scr_stream_push_dyn(s, (ScrDyn *)chunk); else scr_stream_add_chunk(s, chunk, false); scr_stream_wchunk_release(st, chunk); if (!scr_exc_pending()) scr_stream_transform_callback(s); @@ -1912,6 +2009,19 @@ static void scr_stream_do_write_inner(ScrStream *s, ScrBytes *chunk /*moves*/, S st->w.wsync = false; return; } + if (st->w.write_cb == NULL && st->w.writev_cb) { + /* Writable.prototype._write: this._writev([{ chunk, encoding }], cb) */ + ScrDyn *chunks = scr_dyn_new_arr(); + ScrDyn *entry = scr_dyn_new_obj(); + scr_dyn_obj_set(entry, "chunk", 5, scr_stream_wentry_value(st, chunk)); + scr_dyn_obj_set(entry, "encoding", 8, scr_stream_wentry_encoding(st, st->w.cur_enc)); + scr_dyn_arr_push(chunks, entry); + scr_stream_wchunk_release(st, chunk); + st->w.writev_inv(st->w.writev_cb, s, chunks); + scr_dyn_release(chunks); + st->w.wsync = false; + return; + } if (st->w.write_cb == NULL) { scr_stream_wchunk_release(st, chunk); ScrError *e = scr_stream_mkerr("ERR_METHOD_NOT_IMPLEMENTED", "The _write() method is not implemented"); @@ -2031,11 +2141,107 @@ static SCR_COLD void scr_stream_writev_buffer(ScrStream *s) { scr_stream_do_write(s, all, cbs, NULL); } +/* A write's chunk and encoding as _writev receives them (+1 each). */ +static ScrDyn *scr_stream_wentry_value(const ScrStreamState *st, ScrBytes *chunk) { + if (st->w.values) return scr_dyn_retain((ScrDyn *)chunk); + return chunk->is_buffer ? scr_dyn_new_buffer(chunk) : scr_dyn_new_bytes(chunk); +} + +static ScrDyn *scr_stream_wentry_encoding(const ScrStreamState *st, ScrDyn *enc) { + if (!st->w.values) { + ScrStr *buffer = scr_str_new("buffer", 6); + ScrDyn *d = scr_dyn_new_str(buffer); + scr_str_release(buffer); + return d; + } + return scr_dyn_retain(enc ? enc : scr_dyn_undefined()); +} + +/* A batch entry's callback: the write's callback, or Node's nop. */ +static ScrDyn *scr_stream_writev_entry_cb_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + if (clo->ncaps > 0) { + ScrClosure *cb = scr_box_get_ref(clo->caps[0]); + ((void (*)(ScrClosure *))cb->fn)(cb); + scr_closure_release(cb); + } + return scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrDyn *scr_stream_writev_entry_cb(ScrClosure *cb /*borrowed or NULL*/) { + ScrClosure *c = scr_closure_new((void *)&scr_stream_writev_entry_cb_call, cb ? 1 : 0); + if (cb) { + c->caps[0] = scr_box_new_obj(&scr_closure_retain_v, &scr_closure_release_v, &scr_closure_trace_v); + scr_box_set_ref(c->caps[0], scr_closure_retain(cb)); + } + return scr_dyn_new_func(c, &scr_stream_writev_entry_cb_call, 1, "(error)", cb ? "" : "nop"); +} + +/* Calls _writev with chunks as the in-flight write of len, whose + * completion runs cb (moves; NULL when every write had none). */ +static void scr_stream_writev_call(ScrStream *s, ScrDyn *chunks, size_t len, ScrClosure *cb) { + ScrStreamState *st = s->st; + st->w.writing = true; + st->w.wsync = true; + st->w.inflight_len = len; + st->w.inflight_cb = cb; + st->w.writev_inv(st->w.writev_cb, s, chunks); + st->w.wsync = false; +} + +/* Node's clearBuffer with _writev and more than one buffered write: the + * writes go out as one [{ chunk, encoding, callback }] batch (its + * allBuffers says whether every encoding is 'buffer'), and the batch's + * completion runs the writes' callbacks in order. */ +static void scr_stream_writev_batch(ScrStream *s) { + ScrStreamState *st = s->st; + size_t total = 0, ncbs = 0; + for (size_t i = st->w.head; i < st->w.n; i++) + if (st->w.q[i].cb) ncbs++; + ScrClosure *cbs = ncbs > 0 ? scr_closure_new((void *)&scr_stream_writev_cbs, ncbs) : NULL; + ScrDyn *chunks = scr_dyn_new_arr(); + bool all_buffers = true; + size_t k = 0; + for (size_t i = st->w.head; i < st->w.n; i++) { + ScrWEntry e = st->w.q[i]; + total += scr_stream_wchunk_len(st, e.chunk); + ScrDyn *entry = scr_dyn_new_obj(); + ScrDyn *encoding = scr_stream_wentry_encoding(st, e.enc); + if (!(encoding->kind == SCR_DYN_STR && encoding->v.str->len == 6 && + memcmp(encoding->v.str->data, "buffer", 6) == 0)) + all_buffers = false; + scr_dyn_obj_set(entry, "chunk", 5, scr_stream_wentry_value(st, e.chunk)); + scr_dyn_obj_set(entry, "encoding", 8, encoding); + scr_dyn_obj_set(entry, "callback", 8, scr_stream_writev_entry_cb(e.cb)); + scr_dyn_arr_push(chunks, entry); + scr_stream_wchunk_release(st, e.chunk); + if (e.enc) scr_dyn_release(e.enc); + if (e.cb) { + cbs->caps[k] = scr_box_new_obj(&scr_closure_retain_v, &scr_closure_release_v, + &scr_closure_trace_v); + scr_box_set_ref(cbs->caps[k], e.cb); /* moves */ + k++; + } + } + scr_dyn_arr_named_put(chunks, "allBuffers", 10, scr_dyn_new_bool(all_buffers)); + st->w.head = 0; + st->w.n = 0; + scr_stream_writev_call(s, chunks, total, cbs); + scr_dyn_release(chunks); +} + /* The buffered writes, one at a time (Node's clearBuffer without _writev). */ static void scr_stream_clear_buffer_each(ScrStream *s) { ScrStreamState *st = s->st; if (st->w.buffer_processing) return; st->w.buffer_processing = true; + if (st->w.writev_cb && !st->w.writing && st->w.n - st->w.head > 1 && st->w.corked == 0 && + !st->destroyed && !st->constructing && !scr_exc_pending()) { + scr_stream_writev_batch(s); + st->w.buffer_processing = false; + return; + } while (!st->w.writing && st->w.head < st->w.n && st->w.corked == 0 && !st->destroyed && !scr_exc_pending()) { ScrWEntry e = st->w.q[st->w.head++]; @@ -2528,6 +2734,27 @@ static void scr_stream_dynopt_transform_inv(ScrClosure *cb, ScrStream *s, ScrByt if (r != NULL) scr_dyn_release(r); } +static void scr_stream_dynopt_construct_inv(ScrClosure *cb, ScrStream *s) { + ScrDyn *fn = scr_stream_dynopt_target(cb); + ScrDyn *a = scr_stream_dynopt_done(s, &scr_stream_done_dyn_c, 1, "(error)"); + ScrDyn *r = scr_dyn_call(fn, &a, 1, "construct"); + scr_dyn_release(a); + scr_dyn_release(fn); + if (r != NULL) scr_dyn_release(r); +} + +static void scr_stream_dynopt_writev_inv(ScrClosure *cb, ScrStream *s, ScrDyn *chunks) { + ScrDyn *fn = scr_stream_dynopt_target(cb); + ScrDyn *args[2]; + args[0] = scr_dyn_retain(chunks); + args[1] = scr_stream_dynopt_done(s, &scr_stream_done_dyn_w, 1, "(error)"); + ScrDyn *r = scr_dyn_call(fn, args, 2, "writev"); + scr_dyn_release(args[0]); + scr_dyn_release(args[1]); + scr_dyn_release(fn); + if (r != NULL) scr_dyn_release(r); +} + static void scr_stream_dynopt_final_inv(ScrClosure *cb, ScrStream *s) { ScrDyn *fn = scr_stream_dynopt_target(cb); ScrDyn *a = scr_stream_dynopt_done(s, &scr_stream_done_dyn_f, 1, "(error)"); @@ -2570,9 +2797,13 @@ typedef struct { bool auto_destroy, emit_close, allow_half_open; bool readable_side, writable_side; bool readable_object, writable_object; /* objectMode and the sided keys */ + bool decode_strings_off; /* decodeStrings: false */ ScrStr *enc; /* owned canonical name, or NULL */ + /* defaultEncoding other than utf8: as given (borrowed) and canonical + * (owned), or NULL */ + ScrStr *default_enc, *default_canonical; /* BORROWED callable fields (owned by the options record). */ - ScrDyn *read, *write, *final_fn, *destroy, *transform, *flush; + ScrDyn *read, *write, *final_fn, *destroy, *transform, *flush, *construct, *writev; } ScrStreamDynOpts; /* Node's `=== false` comparisons (autoDestroy/emitClose/allowHalfOpen and @@ -2647,7 +2878,10 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy out->writable_side = true; out->enc = NULL; out->readable_object = out->writable_object = false; + out->decode_strings_off = false; + out->default_enc = out->default_canonical = NULL; out->read = out->write = out->final_fn = out->destroy = out->transform = out->flush = NULL; + out->construct = out->writev = NULL; if (opts == NULL || opts->kind != SCR_DYN_OBJ) return true; /* Node's `if (options)` */ const bool duplexish = base == 'd' || base == 't' || base == 'p'; const bool transformish = base == 't' || base == 'p'; @@ -2658,10 +2892,9 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy out->readable_object = object || (duplexish && scr_stream_dynopt_truthy(opts, "readableObjectMode", 18)); out->writable_object = object || (duplexish && scr_stream_dynopt_truthy(opts, "writableObjectMode", 18)); } - if (scr_stream_dynopt_false(scr_dyn_obj_get((ScrDyn *)opts, "decodeStrings", 13))) { - static const char m[] = "decodeStrings: false is not supported yet (chunks always decode to Buffers)"; - return scr_stream_dynopt_unsupported(m, sizeof m - 1); - } + /* WritableState: decodeStrings === false keeps string writes */ + out->decode_strings_off = + base != 'r' && scr_stream_dynopt_false(scr_dyn_obj_get((ScrDyn *)opts, "decodeStrings", 13)); { ScrDyn *v = scr_dyn_obj_get((ScrDyn *)opts, "signal", 6); if (v != NULL && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_UNDEF) { @@ -2669,21 +2902,22 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy return scr_stream_dynopt_unsupported(m, sizeof m - 1); } } - if (scr_stream_dynopt_fn(opts, "construct", 9) != NULL) { - static const char m[] = "the stream option 'construct' is not supported yet (deferred construction has no lowering)"; - return scr_stream_dynopt_unsupported(m, sizeof m - 1); - } - if (scr_stream_dynopt_fn(opts, "writev", 6) != NULL) { - static const char m[] = "the stream option 'writev' is not supported yet (writes deliver one chunk at a time)"; + out->construct = scr_stream_dynopt_fn(opts, "construct", 9); + if (base != 'r') out->writev = scr_stream_dynopt_fn(opts, "writev", 6); + if (transformish && out->writev != NULL) { + static const char m[] = "the stream option 'writev' on a Transform is not supported yet (batches would bypass the transform)"; return scr_stream_dynopt_unsupported(m, sizeof m - 1); } { + /* a known encoding or ERR_UNKNOWN_ENCODING (the states' constructors) */ ScrDyn *v = scr_dyn_obj_get((ScrDyn *)opts, "defaultEncoding", 15); if (v != NULL && v->kind == SCR_DYN_STR && !(v->v.str->len == 4 && memcmp(v->v.str->data, "utf8", 4) == 0) && !(v->v.str->len == 5 && memcmp(v->v.str->data, "utf-8", 5) == 0)) { - static const char m[] = "the stream option 'defaultEncoding' is not supported yet (utf8, the default, is the write-side string encoding)"; - return scr_stream_dynopt_unsupported(m, sizeof m - 1); + ScrStr *canonical = scr_bytes_normalize_encoding(v->v.str); + if (canonical == NULL) return false; + out->default_enc = v->v.str; + out->default_canonical = canonical; } } if (transformish) { @@ -2752,8 +2986,33 @@ static ScrClosure *scr_stream_dynopt_pick(ScrDyn *opt, void *dyn_inv, ScrClosure return fb; } -static void scr_stream_dynopt_apply_object(ScrStream *s, const ScrStreamDynOpts *o) { - if (s != NULL && s->st != NULL) scr_stream_set_object_mode(s, o->readable_object, o->writable_object); +static void scr_stream_dynopt_construct_inv(ScrClosure *cb, ScrStream *s); +static void scr_stream_dynopt_writev_inv(ScrClosure *cb, ScrStream *s, ScrDyn *chunks); + +/* The options a dyn constructor applies over the fresh stream: the object + * modes, decodeStrings, defaultEncoding (the push decoding of a readable + * side and the writes of a writable side), writev, and construct, which + * Node's constructor schedules last. Releases the owned canonical name. */ +static void scr_stream_dynopt_apply_object(ScrStream *s, ScrStreamDynOpts *o) { + if (s != NULL && s->st != NULL) { + scr_stream_set_object_mode(s, o->readable_object, o->writable_object); + if (o->decode_strings_off) scr_stream_set_decode_strings(s, false); + if (o->default_enc) { + scr_stream_set_write_encoding(s, o->default_enc); + if (s->st->has_r && !s->st->r.object_entries) + scr_stream_release(scr_stream_set_push_encoding(s, o->default_canonical)); + } + if (o->writev && s->st->has_w) + scr_stream_set_writev(s, scr_stream_dynopt_clo(o->writev, (void *)&scr_stream_dynopt_writev_inv), + &scr_stream_dynopt_writev_inv); + if (o->construct) + scr_stream_set_construct(s, scr_stream_dynopt_clo(o->construct, (void *)&scr_stream_dynopt_construct_inv), + &scr_stream_dynopt_construct_inv); + } + if (o->default_canonical) { + scr_str_release(o->default_canonical); + o->default_canonical = NULL; + } } static void scr_stream_dynopt_apply_enc(ScrStream *s, ScrStr *enc) { @@ -3513,7 +3772,9 @@ typedef ScrPromise *(*ScrPipeNextFn)(ScrClosure *, ScrGen *); typedef ScrDyn *(*ScrPipeReadFn)(ScrClosure *, void *); typedef ScrDyn *(*ScrPipeBoxFn)(ScrClosure *, ScrPromise *); -enum { SCR_PIPE_DEST_STREAM, SCR_PIPE_DEST_PROMISE = 2 }; +enum { SCR_PIPE_DEST_STREAM, SCR_PIPE_DEST_GEN, SCR_PIPE_DEST_PROMISE }; +/* flags bit 7: the callback is typed code's (stream.pipelineStagesDyn) */ +enum { SCR_PIPE_TYPED_CB = 128 }; typedef struct { size_t count; /* the function stages */ @@ -3525,8 +3786,9 @@ typedef struct { int dest; /* the last stage: SCR_PIPE_DEST_* */ unsigned opts_mask; /* bit i: stages[i] takes the { signal } argument */ ScrDyn *signal; /* the pipeline's AbortSignal when opts_mask != 0 */ - /* (next, read, return) for each generator pumped into a stream, in - * pipeline order, or NULL */ + /* (next, read, return) for each generator pumped into a stream (an + * async generator destination's PassThrough included), in pipeline + * order, or NULL */ ScrClosure **helpers; ScrClosure *box; /* SCR_PIPE_DEST_PROMISE: boxes its promise as a dyn promise */ } ScrPipeStages; @@ -3861,7 +4123,7 @@ static void scr_pipe_dest_entry(ScrFiber *fiber, void *arg) { ctx->value = v; if (ctx->value_p) scr_promise_release(ctx->value_p); ctx->value_p = scr_promise_retain(d.typed); - if (pt && v && (v->kind == SCR_DYN_STR || v->kind == SCR_DYN_BYTES)) { + if (pt && v && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_UNDEF) { scr_stream_write_dyn(pt, v, NULL); if (scr_exc_pending()) scr_exc_clear(); } @@ -3965,17 +4227,22 @@ static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosu ret_gen = out; continue; } - /* The destination function: pipeline() answers a PassThrough proxy - * that the promise's value feeds. Node's is in object mode; the - * frontend refuses programs that read it, so a byte-mode - * PassThrough plays its part in the pipeline's bookkeeping. */ + /* The destination function: pipeline() answers an object-mode + * PassThrough proxy that the promise's value or the generator's + * values feed. */ ScrDyn *pt_opts = scr_dyn_new_obj(); + scr_dyn_obj_set(pt_opts, "objectMode", 10, scr_dyn_new_bool(true)); ScrStream *pt = scr_stream_new_passthrough_dyn(pt_opts); scr_dyn_release(pt_opts); size_t pi = ctx->n++; ctx->streams[pi] = pt; ctx->finish_count++; - scr_pipe_dest_promise(ctx, out, fs->box, pi, end); + if (fs->dest == SCR_PIPE_DEST_GEN) { + /* pumpToNode(ret, pt, finish, { end }) */ + scr_pump_start(ctx, out, fs->helpers + 3 * h++, pt, end); + } else { + scr_pipe_dest_promise(ctx, out, fs->box, pi, end); + } /* destroyer(pt, false, true), with its cleanup as the last stream's */ ctx->destroyer[pi] = true; scr_pipe_on(pt, "close", scr_pipe_closure((void *)&scr_pipe_destroyer_close_l, ctx, pi), @@ -4063,12 +4330,14 @@ static ScrPipeStages scr_pipe_stages_of(double count, ScrClosure **stages, doubl } /* callback(error, value) through a checked-dynamic callback: Node passes - * both arguments. */ -static void scr_pipe_dyn_value_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, ScrPromise *value_p) { - (void)value_p; + * both arguments. A typed callback (SCR_PIPE_TYPED_CB) takes null for no + * error, as the typed callback forms do. */ +static void scr_pipe_dyn_value_call(ScrClosure *cb, ScrError *err, ScrDyn *value, bool typed) { ScrDyn *fn = scr_stream_dynopt_target(cb); - ScrDyn *args[2] = {err ? scr_dyn_from_error(err) : scr_dyn_retain(scr_dyn_undefined()), + ScrDyn *none = typed ? scr_dyn_new_null() : scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *args[2] = {err ? scr_dyn_from_error(err) : none, value && !err ? scr_dyn_retain(value) : scr_dyn_retain(scr_dyn_undefined())}; + if (err) scr_dyn_release(none); ScrDyn *r = scr_dyn_call(fn, args, 2, "callback"); scr_dyn_release(args[0]); scr_dyn_release(args[1]); @@ -4076,6 +4345,17 @@ static void scr_pipe_dyn_value_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, if (r != NULL) scr_dyn_release(r); } +static void scr_pipe_dyn_value_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, ScrPromise *value_p) { + (void)value_p; + scr_pipe_dyn_value_call(cb, err, value, false); +} + +static void scr_pipe_dyn_value_typed_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, + ScrPromise *value_p) { + (void)value_p; + scr_pipe_dyn_value_call(cb, err, value, true); +} + /* pipeline(...) with function stages (see ScrPipeStages): positions * marks the function stages among the n + count stages, flags bits 1-2 * give the destination kind, and mask names the stages taking @@ -4097,7 +4377,9 @@ ScrStream *scr_stream_pipeline_stages_dyn(double n, ScrStream **streams, double ScrPipeStages fs = scr_pipe_stages_of(count, stages, positions, flags, mask, helpers, box, signal); ScrClosure *c = scr_stream_dynopt_clo(cb, (void *)&scr_stream_finished_dyn_inv); /* a function destination's value reaches the callback */ - ScrPipeValInv vi = fs.dest != SCR_PIPE_DEST_STREAM ? &scr_pipe_dyn_value_inv : NULL; + ScrPipeValInv vi = fs.dest == SCR_PIPE_DEST_STREAM ? NULL + : ((int)flags & SCR_PIPE_TYPED_CB) ? &scr_pipe_dyn_value_typed_inv + : &scr_pipe_dyn_value_inv; return scr_stream_pipeline_ex((size_t)n, streams, c, &scr_stream_finished_dyn_inv, true, NULL, NULL, &fs, abort, vi); } @@ -4151,7 +4433,7 @@ static void *scr_compose_retain_v(void *p) { static void scr_compose_release_v(void *p) { ScrCompose *c = p; if (--c->rc > 0) return; - scr_stream_release(c->head); + if (c->head) scr_stream_release(c->head); scr_stream_release(c->tail); free(c); } @@ -4211,7 +4493,9 @@ static void scr_compose_destroyed(ScrCompose *c, ScrError *err) { /* d._write: head.write(chunk) ? callback() : (ondrain = callback) */ static void scr_compose_write_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { ScrCompose *c = scr_box_get_ref(cb->caps[0]); - if (scr_stream_write(c->head, chunk, NULL)) { + bool ok = scr_stream_writes_values(s) ? scr_stream_write_dyn(c->head, (ScrDyn *)chunk, NULL) + : scr_stream_write(c->head, chunk, NULL); + if (ok) { if (!scr_exc_pending()) scr_stream_write_done(s, NULL); } else { c->ondrain = true; @@ -4342,43 +4626,54 @@ static void scr_compose_arg_value(size_t i, const char *what, ScrStream *s) { scr_throw_error_msg_code(SCR_ERR_TYPE, m, (size_t)n, "ERR_INVALID_ARG_VALUE"); } -ScrStream *scr_stream_compose(double n_d, ScrStream **streams) { - size_t n = (size_t)n_d; - if (n == 0) { +/* compose(...streams) over n streams and fs's function stages: the stream + * at pipeline position j must be readable unless it is last and writable + * unless it is first. A first stage that is not a stream (an iterable or + * source function) leaves the Duplex without a writable side. */ +static ScrStream *scr_stream_compose_impl(size_t n, ScrStream **streams, const ScrPipeStages *fs, + ScrClosure *abort) { + size_t total = n + (fs ? fs->count : 0); + if (total == 0) { static const char msg[] = "The \"streams\" argument must be specified"; scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_MISSING_ARGS"); return NULL; } - if (n == 1) return scr_stream_retain(streams[0]); /* Duplex.from(duplex) */ - for (size_t i = 0; i < n; i++) { - if (i + 1 < n && !scr_stream_prop(streams[i], "readable")) { - scr_compose_arg_value(i, "readable", streams[i]); + if (total == 1 && n == 1) return scr_stream_retain(streams[0]); /* Duplex.from(duplex) */ + for (size_t j = 0, i = 0; j < total; j++) { + if (fs && j < 64 && ((fs->positions >> j) & 1u)) continue; + if (j + 1 < total && !scr_stream_prop(streams[i], "readable")) { + scr_compose_arg_value(j, "readable", streams[i]); return NULL; } - if (i > 0 && !scr_stream_prop(streams[i], "writable")) { - scr_compose_arg_value(i, "writable", streams[i]); + if (j > 0 && !scr_stream_prop(streams[i], "writable")) { + scr_compose_arg_value(j, "writable", streams[i]); return NULL; } + i++; } + bool stage_head = fs && (fs->positions & 1u); ScrCompose *c = calloc(1, sizeof *c); if (!c) scr_stream_oom(); c->rc = 1; - c->head = scr_stream_retain(streams[0]); + c->head = stage_head ? NULL : scr_stream_retain(streams[0]); c->tail = scr_stream_retain(streams[n - 1]); c->onclose = SCR_COMPOSE_RUNNING; ScrStream *tail = scr_stream_pipeline_ex(n, streams, scr_compose_closure((void *)&scr_compose_finished, c), - &scr_compose_finished, true, NULL, NULL, NULL, NULL, + &scr_compose_finished, true, NULL, NULL, fs, abort, NULL); if (!tail) { scr_compose_release_v(c); return NULL; } scr_stream_release(tail); - c->writable = scr_stream_prop(c->head, "writable") != 0; + c->writable = c->head && scr_stream_prop(c->head, "writable") != 0; c->readable = scr_stream_prop(c->tail, "readable") != 0; + /* readableObjectMode: !!tail?.readableObjectMode, writableObjectMode: + * !!head?.writableObjectMode */ bool object_mode = c->tail->st->has_r && c->tail->st->r.object_entries; + bool w_object = c->head && c->head->st->has_w && c->head->st->w.object_mode; ScrStream *d = scr_stream_new_duplex( - object_mode ? 16 : -1, -1, true, true, true, c->readable, c->writable, + object_mode ? 16 : -1, w_object ? 16 : -1, true, true, true, c->readable, c->writable, c->readable ? scr_compose_closure((void *)&scr_compose_read_inv, c) : NULL, c->readable ? &scr_compose_read_inv : NULL, c->writable ? scr_compose_closure((void *)&scr_compose_write_inv, c) : NULL, @@ -4387,6 +4682,7 @@ ScrStream *scr_stream_compose(double n_d, ScrStream **streams) { c->writable ? &scr_compose_final_inv : NULL, scr_compose_closure((void *)&scr_compose_destroy_inv, c), &scr_compose_destroy_inv); if (object_mode) d->st->r.object_entries = true; + if (w_object) scr_stream_set_object_mode(d, false, true); c->d = scr_stream_retain(d); if (c->writable) { scr_compose_on(c->head, "drain", &c->drain_l, (void *)&scr_compose_drain_l, c, scr_ee_inv_fixed0); @@ -4408,6 +4704,374 @@ ScrStream *scr_stream_compose(double n_d, ScrStream **streams) { return d; } +ScrStream *scr_stream_compose(double n_d, ScrStream **streams) { + return scr_stream_compose_impl((size_t)n_d, streams, NULL, NULL); +} + +/* compose(...stages) with function stages among the streams (the + * pipeline stage layout: scr_stream_pipeline_stages). */ +ScrStream *scr_stream_compose_stages(double n, ScrStream **streams, double count, ScrClosure **stages, + double positions, double flags, double mask, ScrClosure **helpers, + ScrClosure *box, ScrDyn *signal, ScrClosure *abort) { + ScrPipeStages fs = scr_pipe_stages_of(count, stages, positions, flags, mask, helpers, box, signal); + return scr_stream_compose_impl((size_t)n, streams, &fs, abort); +} + +/* ── Duplex.from over a stream or a { readable, writable } pair ───────── + * duplexify.js's _duplexify({ readable, writable }): a Duplexify that + * writes to w (write() answering false waits for 'drain'; final ends w and + * waits for its 'finish') and reads r through 'readable' and read(), with + * each side's end of stream (eos) finishing the Duplex. The readable and + * writable object modes follow r's and w's. */ + +enum { SCR_DUPX_UNSET, SCR_DUPX_NULL, SCR_DUPX_WAIT }; + +typedef struct ScrDuplexify { + size_t rc; + ScrStream *r, *w; /* +1 or NULL */ + ScrStream *d; /* +1 until teardown */ + ScrClosure *drain_l, *finish_l, *readable_l, *end_l; /* +1, the listeners, or NULL */ + ScrClosure *r_eos, *w_eos; /* +1: the eos listeners' cleanups, or NULL */ + bool readable, writable; + bool ondrain, onfinish, onreadable; + int onclose; /* _duplexify's onclose: undefined, null, or d's destroy callback */ + struct ScrDuplexify *live_prev, *live_next; +} ScrDuplexify; + +static SCR_TL ScrDuplexify *scr_dupx_live = NULL; + +static void *scr_dupx_retain_v(void *p) { + ((ScrDuplexify *)p)->rc++; + return p; +} + +static void scr_dupx_release_v(void *p) { + ScrDuplexify *x = p; + if (--x->rc > 0) return; + if (x->r_eos) scr_closure_release(x->r_eos); + if (x->w_eos) scr_closure_release(x->w_eos); + if (x->r) scr_stream_release(x->r); + if (x->w) scr_stream_release(x->w); + free(x); +} + +static ScrClosure *scr_dupx_closure(void *fn, ScrDuplexify *x) { + ScrClosure *cl = scr_closure_new(fn, 1); + cl->caps[0] = scr_box_new_obj(&scr_dupx_retain_v, &scr_dupx_release_v, NULL); + scr_box_set_ref(cl->caps[0], scr_dupx_retain_v(x)); + return cl; +} + +static void scr_dupx_on(ScrStream *s, const char *name, ScrClosure **slot, void *fn, ScrDuplexify *x) { + ScrStr *n = scr_str_new(name, strlen(name)); + *slot = scr_dupx_closure(fn, x); + scr_emitter_release( + scr_emitter_on((ScrEmitter *)s, n, scr_closure_retain(*slot), scr_ee_inv_fixed0, false, false)); + scr_str_release(n); +} + +/* Drop the Duplex and the listeners on r and w (the eos listeners stay + * registered; their cleanups, which hold the context, are dropped). */ +static void scr_dupx_teardown(ScrDuplexify *x) { + if (!x->d) return; + if (x->r_eos) scr_closure_release(x->r_eos); + if (x->w_eos) scr_closure_release(x->w_eos); + x->r_eos = x->w_eos = NULL; + if (x->w) { + scr_compose_off(x->w, "drain", &x->drain_l); + scr_compose_off(x->w, "finish", &x->finish_l); + } + if (x->r) { + scr_compose_off(x->r, "readable", &x->readable_l); + scr_compose_off(x->r, "end", &x->end_l); + } + if (x->live_prev) x->live_prev->live_next = x->live_next; + else scr_dupx_live = x->live_next; + if (x->live_next) x->live_next->live_prev = x->live_prev; + x->live_prev = x->live_next = NULL; + ScrStream *d = x->d; + x->d = NULL; + scr_stream_release(d); + scr_dupx_release_v(x); /* the live list's reference */ +} + +/* At exit, a Duplexify still open also drops its eos listeners on r and + * w: they hold the context, which holds r and w. */ +static void scr_dupx_atexit(void) { + while (scr_dupx_live) { + ScrDuplexify *x = scr_dupx_live; + ScrClosure *eos[2] = {x->r_eos, x->w_eos}; + x->r_eos = x->w_eos = NULL; + for (int i = 0; i < 2; i++) { + if (!eos[i]) continue; + ((void (*)(ScrClosure *))eos[i]->fn)(eos[i]); + scr_closure_release(eos[i]); + } + scr_dupx_teardown(x); + } +} + +/* d's destroy callback: callback(err), then the context lets go. */ +static void scr_dupx_destroyed(ScrDuplexify *x, ScrError *err) { + ScrStream *d = scr_stream_retain(x->d); + scr_dupx_teardown(x); + scr_stream_destroy_done(d, err ? scr_error_retain(err) : NULL); + scr_stream_release(d); +} + +/* onfinished(err): the pending destroy callback, else d.destroy(err) */ +static void scr_dupx_onfinished(ScrDuplexify *x, ScrError *err) { + int onclose = x->onclose; + x->onclose = SCR_DUPX_NULL; + if (!x->d) return; + if (onclose == SCR_DUPX_WAIT) { + scr_dupx_destroyed(x, err); + } else if (err) { + ScrStream *d = scr_stream_retain(x->d); + scr_stream_release(scr_stream_destroy(d, err)); + scr_stream_release(d); + } +} + +/* eos(w): writable = false; an error destroys r */ +static void scr_dupx_w_eos(ScrClosure *cb, ScrStream *s, ScrError *err) { + (void)s; + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + x->writable = false; + if (err && x->r) scr_stream_release(scr_stream_destroy(x->r, err)); + scr_dupx_onfinished(x, err); + scr_dupx_release_v(x); +} + +/* eos(r): readable = false; an error destroys w */ +static void scr_dupx_r_eos(ScrClosure *cb, ScrStream *s, ScrError *err) { + (void)s; + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + x->readable = false; + if (err && x->w) scr_stream_release(scr_stream_destroy(x->w, err)); + scr_dupx_onfinished(x, err); + scr_dupx_release_v(x); +} + +/* d._write: w.write(chunk) ? callback() : (ondrain = callback) */ +static void scr_dupx_write_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + bool ok = scr_stream_writes_values(s) ? scr_stream_write_dyn(x->w, (ScrDyn *)chunk, NULL) + : scr_stream_write(x->w, chunk, NULL); + if (ok) { + if (!scr_exc_pending()) scr_stream_write_done(s, NULL); + } else { + x->ondrain = true; + } + scr_dupx_release_v(x); +} + +/* d._final: w.end(); onfinish = callback */ +static void scr_dupx_final_inv(ScrClosure *cb, ScrStream *s) { + (void)s; + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + ScrStream *r = scr_stream_end(x->w, NULL, NULL, NULL); + if (r) scr_stream_release(r); + x->onfinish = true; + scr_dupx_release_v(x); +} + +static void scr_dupx_drain_l(ScrClosure *cb) { + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + if (x->ondrain && x->d) { + x->ondrain = false; + scr_stream_write_done(x->d, NULL); + } + scr_dupx_release_v(x); +} + +static void scr_dupx_finish_l(ScrClosure *cb) { + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + if (x->onfinish && x->d) { + x->onfinish = false; + scr_stream_final_done(x->d, NULL); + } + scr_dupx_release_v(x); +} + +/* d._read: push r.read() until it answers null (then wait for 'readable') + * or d is full */ +static void scr_dupx_pull(ScrDuplexify *x) { + ScrStream *d = x->d; + while (d && x->r) { + ScrEntryKind kind = SCR_ENTRY_BYTES; + void *chunk = scr_stream_read_n_kind(x->r, -1, &kind); + if (scr_exc_pending()) return; + if (!chunk) { + x->onreadable = true; + return; + } + bool more; + if (d->st->r.object_entries) { + more = scr_stream_add_entry(d, chunk, kind) > 0; + } else if (kind == SCR_ENTRY_STR) { + more = scr_stream_push_str(d, chunk); + scr_str_release(chunk); + } else if (kind == SCR_ENTRY_DYN) { + more = scr_stream_push_dyn(d, chunk); + scr_dyn_release(chunk); + } else { + more = scr_stream_push(d, chunk); + scr_bytes_release(chunk); + } + if (!more || scr_exc_pending()) return; + } +} + +static void scr_dupx_read_inv(ScrClosure *cb, ScrStream *s, double size) { + (void)s; + (void)size; + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + x->onreadable = false; + scr_dupx_pull(x); + scr_dupx_release_v(x); +} + +static void scr_dupx_readable_l(ScrClosure *cb) { + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + if (x->onreadable && x->d) { + x->onreadable = false; + scr_dupx_pull(x); + } + scr_dupx_release_v(x); +} + +static void scr_dupx_end_l(ScrClosure *cb) { + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + if (x->d) scr_stream_push_null(x->d); + scr_dupx_release_v(x); +} + +/* d._destroy */ +static void scr_dupx_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + (void)s; + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + ScrError *e = err ? scr_error_retain(err) : NULL; + if (!e && x->onclose != SCR_DUPX_NULL) e = scr_compose_abort_error(); + x->onreadable = x->ondrain = x->onfinish = false; + if (x->onclose == SCR_DUPX_NULL) { + if (x->d) scr_dupx_destroyed(x, e); + } else { + x->onclose = SCR_DUPX_WAIT; + if (x->w && !x->w->st->destroyed) scr_stream_release(scr_stream_destroy(x->w, e)); + if (x->r && !x->r->st->destroyed) scr_stream_release(scr_stream_destroy(x->r, e)); + } + if (e) scr_error_release(e); + scr_dupx_release_v(x); +} + +/* _duplexify({ readable: r, writable: w }): r and w borrowed, either NULL. + * +1. */ +ScrStream *scr_stream_duplexify(ScrStream *r, ScrStream *w) { + ScrDuplexify *x = calloc(1, sizeof *x); + if (!x) scr_stream_oom(); + x->rc = 1; + x->r = r ? scr_stream_retain(r) : NULL; + x->w = w ? scr_stream_retain(w) : NULL; + x->onclose = SCR_DUPX_UNSET; + x->readable = r && scr_stream_prop(r, "readable") != 0; + x->writable = w && scr_stream_prop(w, "writable") != 0; + /* new Duplexify({ readableObjectMode, writableObjectMode, readable, writable }) */ + bool r_object = r && r->st->has_r && r->st->r.object_entries; + bool w_object = w && w->st->has_w && w->st->w.object_mode; + ScrStream *d = scr_stream_new_duplex( + r_object ? 16 : -1, w_object ? 16 : -1, true, true, true, x->readable, x->writable, + x->readable ? scr_dupx_closure((void *)&scr_dupx_read_inv, x) : NULL, + x->readable ? &scr_dupx_read_inv : NULL, + x->writable ? scr_dupx_closure((void *)&scr_dupx_write_inv, x) : NULL, + x->writable ? &scr_dupx_write_inv : NULL, + x->writable ? scr_dupx_closure((void *)&scr_dupx_final_inv, x) : NULL, + x->writable ? &scr_dupx_final_inv : NULL, + scr_dupx_closure((void *)&scr_dupx_destroy_inv, x), &scr_dupx_destroy_inv); + if (r_object) d->st->r.object_entries = true; + if (w_object) scr_stream_set_object_mode(d, false, true); + d->cls = "Duplexify"; + x->d = scr_stream_retain(d); + if (x->writable) { + x->w_eos = scr_stream_finished(w, scr_dupx_closure((void *)&scr_dupx_w_eos, x), &scr_dupx_w_eos); + scr_dupx_on(w, "drain", &x->drain_l, (void *)&scr_dupx_drain_l, x); + scr_dupx_on(w, "finish", &x->finish_l, (void *)&scr_dupx_finish_l, x); + } + if (x->readable) { + x->r_eos = scr_stream_finished(r, scr_dupx_closure((void *)&scr_dupx_r_eos, x), &scr_dupx_r_eos); + scr_dupx_on(r, "readable", &x->readable_l, (void *)&scr_dupx_readable_l, x); + scr_dupx_on(r, "end", &x->end_l, (void *)&scr_dupx_end_l, x); + } + static SCR_TL bool registered = false; + if (!registered) { + registered = true; + scr_atexit(scr_dupx_atexit); + } + x->live_next = scr_dupx_live; + if (scr_dupx_live) scr_dupx_live->live_prev = x; + scr_dupx_live = x; /* the reference x was created with */ + return d; +} + +/* Duplex.from(promise) (duplexify.js): a Duplexify({ objectMode: true, + * writable: false, read() {} }) that pushes the promise's value unless it + * is nullish, then ends; a rejection destroys it with the reason. */ +typedef struct { + ScrStream *d; /* +1 */ + ScrDyn *boxed; /* +1, the promise boxed */ +} ScrDupxPromise; + +static void scr_dupx_noop_read(ScrClosure *cb, ScrStream *s, double size) { + (void)cb; + (void)s; + (void)size; +} + +static void scr_dupx_promise_entry(ScrFiber *fiber, void *arg) { + (void)fiber; + ScrDupxPromise p = *(ScrDupxPromise *)arg; + free(arg); + /* the boxed promise's payload is a dyn value (NULL: undefined) */ + ScrDyn *v = scr_await_ref(p.boxed->v.promise); + if (!scr_exc_pending()) { + if (v && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_UNDEF) scr_stream_push_object(p.d, v); + if (!scr_exc_pending()) scr_stream_push_null(p.d); + if (scr_exc_pending()) scr_exc_clear(); + if (v) scr_dyn_release(v); + } else { + ScrError *err = scr_pump_caught(); + scr_stream_release(scr_stream_destroy(p.d, err)); + if (scr_exc_pending()) scr_exc_clear(); + scr_error_release(err); + } + scr_stream_release(p.d); + scr_dyn_release(p.boxed); +} + +/* The readable Duplexify over a boxed promise (moves boxed). +1. */ +static ScrStream *scr_dupx_from_boxed(ScrDyn *boxed) { + ScrClosure *read = scr_closure_new((void *)&scr_dupx_noop_read, 0); + ScrStream *d = scr_stream_new_duplex(16, 16, true, true, true, true, false, read, + &scr_dupx_noop_read, NULL, NULL, NULL, NULL, NULL, NULL); + scr_stream_set_object_mode(d, true, true); + d->cls = "Duplexify"; + ScrDupxPromise *ctx = malloc(sizeof *ctx); + if (!ctx) scr_stream_oom(); + ctx->d = scr_stream_retain(d); + ctx->boxed = boxed; + scr_promise_release(scr_async_spawn_after(ctx->boxed->v.promise, &scr_dupx_promise_entry, ctx)); + return d; +} + +/* p and box borrowed (box: the compiler's promise → dyn promise helper). +1. */ +ScrStream *scr_stream_duplex_from_promise(ScrPromise *p, ScrClosure *box) { + return scr_dupx_from_boxed(((ScrPipeBoxFn)box->fn)(box, scr_promise_retain(p))); +} + +ScrStream *scr_stream_duplexify_readable(ScrStream *r) { return scr_stream_duplexify(r, NULL); } +ScrStream *scr_stream_duplexify_writable(ScrStream *w) { return scr_stream_duplexify(NULL, w); } +ScrStream *scr_stream_duplexify_pair(ScrStream *r, ScrStream *w) { return scr_stream_duplexify(r, w); } + /* ── node:stream/promises ───────────────────────────────────────────── * The promise forms ride the callback machinery above with a settling * watcher: caps[0] boxes the pending promise; the terminal status @@ -4484,6 +5148,24 @@ ScrPromise *scr_sp_pipeline_stages(double n, ScrStream **streams, double count, return p; } +/* The options' signal: NULL for a nullish one; false (p rejected with + * ERR_INVALID_ARG_TYPE) for a value that is not an AbortSignal. */ +static bool scr_sp_opts_signal(ScrPromise *p, ScrDyn **signal) { + if (*signal && scr_dyn_is_nullish(*signal)) *signal = NULL; + if (!*signal) return true; + ScrDyn *aborted = NULL; + if ((*signal)->kind == SCR_DYN_OBJ || (*signal)->kind == SCR_DYN_HANDLE) { + aborted = scr_stream_signal_prop(*signal, "aborted"); + } + if (!aborted) { + scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", *signal); + scr_promise_reject_pending(p); + return false; + } + scr_dyn_release(aborted); + return true; +} + /* pipeline(...streams, { signal, end }): end false leaves the destination * open (the promise settles once the stages before it finish); signal is * NULL or borrowed — a value that is not an AbortSignal rejects with @@ -4491,19 +5173,7 @@ ScrPromise *scr_sp_pipeline_stages(double n, ScrStream **streams, double count, ScrPromise *scr_sp_pipeline_opts(double n, ScrStream **streams, bool end, ScrDyn *signal, ScrAbortAddFn add) { ScrPromise *p = scr_promise_new(); - if (signal && scr_dyn_is_nullish(signal)) signal = NULL; - if (signal) { - ScrDyn *aborted = NULL; - if (signal->kind == SCR_DYN_OBJ || signal->kind == SCR_DYN_HANDLE) { - aborted = scr_stream_signal_prop(signal, "aborted"); - } - if (!aborted) { - scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", signal); - scr_promise_reject_pending(p); - return p; - } - scr_dyn_release(aborted); - } + if (!scr_sp_opts_signal(p, &signal)) return p; ScrStream *dst = scr_stream_pipeline_ex((size_t)n, streams, scr_sp_watcher(p), &scr_sp_settle_inv, end, signal, add, NULL, NULL, NULL); if (dst) scr_stream_release(dst); @@ -4511,6 +5181,25 @@ ScrPromise *scr_sp_pipeline_opts(double n, ScrStream **streams, bool end, ScrDyn return p; } +/* scr_sp_pipeline_stages with the options argument: end and the + * options' signal (outer, NULL or borrowed) as scr_sp_pipeline_opts + * takes them. The stages' signal is the pipeline's own. */ +ScrPromise *scr_sp_pipeline_stages_opts(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + ScrDyn *signal, ScrClosure *abort, bool end, + ScrDyn *outer, ScrAbortAddFn add) { + ScrPromise *p = scr_promise_new(); + if (!scr_sp_opts_signal(p, &outer)) return p; + ScrPipeStages fs = scr_pipe_stages_of(count, stages, positions, flags, mask, helpers, box, signal); + ScrPipeValInv vi = fs.dest != SCR_PIPE_DEST_STREAM ? &scr_sp_value_inv : NULL; + ScrStream *dst = scr_stream_pipeline_ex((size_t)n, streams, scr_sp_watcher(p), &scr_sp_settle_inv, + end, outer, add, &fs, abort, vi); + if (dst) scr_stream_release(dst); + else scr_promise_reject_pending(p); + return p; +} + /* ── node:stream/consumers ──────────────────────────────────────────── * text/json/buffer/arrayBuffer over the readable machinery: a native 'data' listener * accumulates every chunk (Buffer chunks as-is; string chunks — an @@ -5542,7 +6231,69 @@ void scr_stream_set_object_mode(ScrStream *s, bool readable, bool writable) { st->r.object_entries = true; st->r.object_mode_opt = true; } - if (writable && st->has_w) st->w.object_mode = true; + if (writable && st->has_w) st->w.object_mode = st->w.values = true; +} + +/* decodeStrings: false — string writes stay strings (scr_stream_write_string). */ +void scr_stream_set_decode_strings(ScrStream *s, bool decode) { + ScrStreamState *st = s->st; + if (decode || !st->has_w) return; + st->w.decode_strings_off = true; + st->w.values = true; +} + +ScrStream *scr_stream_decode_strings(ScrStream *s, bool decode) { + scr_stream_set_decode_strings(s, decode); + return scr_stream_retain(s); +} + +static void scr_stream_store_default_enc(ScrStreamState *st, ScrStr *enc /*moves*/) { + if (st->w.default_enc) scr_str_release(st->w.default_enc); + st->w.default_enc = NULL; + if ((enc->len == 4 && memcmp(enc->data, "utf8", 4) == 0) || + (enc->len == 5 && memcmp(enc->data, "utf-8", 5) == 0)) { + scr_str_release(enc); /* Node's kDefaultUTF8Encoding */ + return; + } + st->w.default_enc = enc; +} + +/* The defaultEncoding option on the writable side, a known encoding as + * given (borrowed; validated by the caller). */ +void scr_stream_set_write_encoding(ScrStream *s, ScrStr *enc) { + if (s->st->has_w) scr_stream_store_default_enc(s->st, scr_str_retain(enc)); +} + +ScrStream *scr_stream_write_encoding(ScrStream *s, ScrStr *enc) { + scr_stream_set_write_encoding(s, enc); + return scr_stream_retain(s); +} + +/* setDefaultEncoding(encoding): the name lowercased, a known encoding or + * ERR_UNKNOWN_ENCODING (thrown); answers the stream (+1). */ +ScrStream *scr_stream_set_default_encoding(ScrStream *s, ScrDyn *enc) { + ScrStr *raw = enc->kind == SCR_DYN_STR ? scr_str_retain(enc->v.str) : scr_dyn_string_coerce_js(enc); + ScrStr *lower = raw; + if (enc->kind == SCR_DYN_STR) { + char *buf = malloc(raw->len ? raw->len : 1); + if (!buf) scr_stream_oom(); + for (size_t i = 0; i < raw->len; i++) { + char c = raw->data[i]; + buf[i] = c >= 'A' && c <= 'Z' ? (char)(c - 'A' + 'a') : c; + } + lower = scr_str_new(buf, raw->len); + free(buf); + scr_str_release(raw); + } + ScrStr *canonical = scr_bytes_normalize_encoding(lower); + if (canonical == NULL) { + scr_str_release(lower); + return scr_stream_retain(s); + } + scr_str_release(canonical); + if (s->st->has_w) scr_stream_store_default_enc(s->st, lower); + else scr_str_release(lower); + return scr_stream_retain(s); } /* stream.objectMode: scr_stream_set_object_mode, answering the receiver @@ -5556,7 +6307,7 @@ ScrStream *scr_stream_object_mode(ScrStream *s, bool readable, bool writable) { * object-mode writable side): the compiler's chunk thunks read the chunk * by this. */ bool scr_stream_writes_values(ScrStream *s) { - return s->st->has_w && s->st->w.object_mode; + return s->st->has_w && s->st->w.values; } /* emit('data', value) with a checked-dynamic chunk: the 'data' slots by @@ -5614,6 +6365,23 @@ bool scr_stream_push(ScrStream *s, ScrBytes *chunk) { return scr_stream_add_chunk(s, chunk, false); } +/* readable.push(chunk) from a program: in byte mode another + * ArrayBufferView becomes a Buffer over the same bytes + * (Stream._uint8ArrayToBuffer); the runtime's own producers push Buffers + * through scr_stream_push. */ +bool scr_stream_push_view(ScrStream *s, ScrBytes *chunk) { + if (chunk && !chunk->is_buffer && !(s->st->has_r && s->st->r.object_entries)) { + ScrBytes *b = scr_bytes_buffer_view(chunk, SCR_BYTES_U8, scr_bytes_byte_offset(chunk), true, + scr_bytes_byte_len(chunk)); + if (!b) return false; + b->is_buffer = true; + bool r = scr_stream_add_chunk(s, b, false); + scr_bytes_release(b); + return r; + } + return scr_stream_add_chunk(s, chunk, false); +} + bool scr_stream_push_str(ScrStream *s, ScrStr *str) { /* object mode: the string is one whole entry, not converted */ if (s->st->has_r && s->st->r.object_entries) @@ -6050,6 +6818,60 @@ static bool scr_stream_write_value(ScrStream *s, ScrDyn *v, ScrClosure *cb) { return scr_stream_write_chunk_enc(s, (ScrBytes *)v, cb, scr_stream_cb_enc(cb)); } +/* A Buffer write on a byte-mode side: with decodeStrings: false the + * queue holds values, so the Buffer is boxed with its 'buffer' encoding. + * Borrows chunk. */ +static bool scr_stream_write_buffer(ScrStream *s, ScrBytes *chunk, ScrClosure *cb) { + ScrStreamState *st = s->st; + if (!st->has_w || !st->w.values) return scr_stream_write_chunk(s, chunk, cb); + ScrDyn *v = chunk->is_buffer ? scr_dyn_new_buffer(chunk) : scr_dyn_new_bytes(chunk); + ScrStr *buffer = scr_str_new("buffer", 6); + ScrDyn *enc = scr_dyn_new_str(buffer); + scr_str_release(buffer); + bool ret = scr_stream_write_chunk_enc(s, (ScrBytes *)v, cb, enc); + scr_dyn_release(enc); + scr_dyn_release(v); + return ret; +} + +/* A string write on a byte-mode side, with the encoding argument as + * given (borrowed; NULL takes the default encoding): decodeStrings + * encodes it (Buffer.from(chunk, encoding)), and with decodeStrings: + * false it stays a string that _write receives with that encoding. */ +static bool scr_stream_write_string(ScrStream *s, ScrStr *str, ScrStr *name, ScrClosure *cb) { + ScrStreamState *st = s->st; + ScrStr *given = name ? scr_str_retain(name) : st->has_w && st->w.default_enc ? scr_str_retain(st->w.default_enc) : NULL; + if (st->has_w && st->w.decode_strings_off) { + ScrStr *e = given ? given : scr_str_new("utf8", 4); + ScrDyn *v = scr_dyn_new_str(str); + ScrDyn *enc = scr_dyn_new_str(e); + scr_str_release(e); + bool ret = scr_stream_write_chunk_enc(s, (ScrBytes *)v, cb, enc); + scr_dyn_release(enc); + scr_dyn_release(v); + return ret; + } + ScrBytes *b; + if (given) { + ScrStr *canonical = scr_bytes_normalize_encoding(given); + scr_str_release(given); + if (canonical == NULL) { + if (cb) scr_closure_release(cb); + return false; + } + b = scr_bytes_from_str(str, canonical); + scr_str_release(canonical); + } else { + b = scr_bytes_new(SCR_BYTES_U8, (double)str->len); + memcpy(b->data, str->data, str->len); + if (!str->ascii) scr_wtf8_to_utf8((char *)b->data, str->len); + } + scr_stream_buffer(b); + bool ret = scr_stream_write_buffer(s, b, cb); + scr_bytes_release(b); + return ret; +} + bool scr_stream_write(ScrStream *s, ScrBytes *chunk, ScrClosure *cb) { if (s->st->has_w && s->st->w.object_mode) { ScrDyn *v = chunk->is_buffer ? scr_dyn_new_buffer(chunk) : scr_dyn_new_bytes(chunk); @@ -6057,7 +6879,22 @@ bool scr_stream_write(ScrStream *s, ScrBytes *chunk, ScrClosure *cb) { scr_dyn_release(v); return ret; } - return scr_stream_write_chunk(s, chunk, cb); + if (!chunk->is_buffer) { + /* Writable's _write: a typed array or DataView chunk becomes a Buffer + * over the same bytes (Stream._uint8ArrayToBuffer), which _write, + * _writev and a decodeStrings: false queue receive. */ + ScrBytes *b = scr_bytes_buffer_view(chunk, SCR_BYTES_U8, scr_bytes_byte_offset(chunk), true, + scr_bytes_byte_len(chunk)); + if (!b) { + if (cb) scr_closure_release(cb); + return false; + } + b->is_buffer = true; + bool ret = scr_stream_write_buffer(s, b, cb); + scr_bytes_release(b); + return ret; + } + return scr_stream_write_buffer(s, chunk, cb); } /* write(null) reached at runtime (a nullable-union chunk's null arm): @@ -6077,13 +6914,7 @@ bool scr_stream_write_str(ScrStream *s, ScrStr *str, ScrClosure *cb) { scr_dyn_release(v); return ret; } - ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)str->len); - memcpy(b->data, str->data, str->len); - if (!str->ascii) scr_wtf8_to_utf8((char *)b->data, str->len); - scr_stream_buffer(b); - bool ret = scr_stream_write_chunk(s, b, cb); - scr_bytes_release(b); - return ret; + return scr_stream_write_string(s, str, NULL, cb); } /* write(chunk, encoding, cb) with the encoding argument as passed (NULL @@ -6103,20 +6934,15 @@ bool scr_stream_write_enc(ScrStream *s, const ScrDyn *chunk, ScrDyn *enc, ScrClo name = scr_dyn_string_coerce_js(enc); if (name && !(name->len == 6 && memcmp(name->data, "buffer", 6) == 0)) { ScrStr *canonical = scr_bytes_normalize_encoding(name); - scr_str_release(name); if (canonical == NULL) { + scr_str_release(name); if (cb) scr_closure_release(cb); return false; } - if (chunk->kind != SCR_DYN_STR) { - scr_str_release(canonical); - return scr_stream_write_dyn(s, chunk, cb); - } - ScrBytes *b = scr_bytes_from_str(chunk->v.str, canonical); scr_str_release(canonical); - scr_stream_buffer(b); - bool ret = scr_stream_write_chunk(s, b, cb); - scr_bytes_release(b); + bool ret = chunk->kind == SCR_DYN_STR ? scr_stream_write_string(s, chunk->v.str, name, cb) + : scr_stream_write_dyn(s, chunk, cb); + scr_str_release(name); return ret; } if (name && chunk->kind == SCR_DYN_STR) { @@ -6535,8 +7361,41 @@ static void scr_stream_emit_readable_now(ScrStream *s) { /* ── tick dispatch ────────────────────────────────────────────────────── */ /* onConstruct + the readable/writable onConstructed hooks. err moves. */ +/* aggregateTwoErrors(inner, outer) for streams without native ops: both + * present and distinct make an AggregateError([outer, inner]) with + * outer's message and code. Borrows both; +1 or NULL. */ +static ScrError *scr_stream_aggregate_errors(ScrError *inner, ScrError *outer) { + if (inner && outer && inner != outer) { + ScrDyn *message = scr_dyn_new_str(outer->message); + ScrError *agg = scr_aggregate_new(message, scr_dyn_undefined()); + scr_dyn_release(message); + if (agg == NULL) return scr_error_retain(outer); + ScrDyn *outer_value = scr_dyn_from_error(outer); + ScrDyn *list = scr_dyn_new_arr(); + scr_dyn_arr_push(list, scr_dyn_retain(outer_value)); + scr_dyn_arr_push(list, scr_dyn_from_error(inner)); + scr_aggregate_set_errors(agg, list); + scr_dyn_release(list); + ScrDyn *code = scr_dyn_obj_read(outer_value, "code", 4); + if (code && code->kind == SCR_DYN_STR) scr_error_set_code(agg, code->v.str->data); + scr_dyn_release(code); + scr_dyn_release(outer_value); + return agg; + } + ScrError *one = outer ? outer : inner; + return one ? scr_error_retain(one) : NULL; +} + static void scr_stream_constructed(ScrStream *s, ScrError *err) { ScrStreamState *st = s->st; + if (st->construct_finished) { + /* onConstruct called again: errorOrDestroy(err ?? ERR_MULTIPLE_CALLBACK) */ + if (!err) err = scr_stream_mkerr("ERR_MULTIPLE_CALLBACK", "Callback called multiple times"); + scr_stream_error_or_destroy(s, err); + scr_error_release(err); + return; + } + st->construct_finished = true; st->constructing = false; if (st->construct_cb) { scr_closure_release(st->construct_cb); @@ -6549,8 +7408,9 @@ static void scr_stream_constructed(ScrStream *s, ScrError *err) { /* aggregateTwoErrors(constructError, destroyError): the owner builds * the AggregateError when both are present and distinct */ ScrError *use; - if (err && derr && err != derr && st->native_ops && st->native_ops->aggregate) { - use = st->native_ops->aggregate(err, derr); + if (err && derr && err != derr) { + use = st->native_ops && st->native_ops->aggregate ? st->native_ops->aggregate(err, derr) + : scr_stream_aggregate_errors(err, derr); } else { use = derr ? derr : err; if (use) scr_error_retain(use); @@ -6589,6 +7449,39 @@ void scr_stream_construct_done(ScrStream *s, ScrError *err /*moves*/) { void scr_stream_set_writev_concat(ScrStream *s) { s->st->writev_concat = true; } +/* A user construct option or _construct method: Node schedules it from the + * constructor (construct(stream, cb)), and the hook's callback reports + * through scr_stream_construct_done. */ +void scr_stream_set_construct(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamPlainInv inv) { + if (s->st->construct_cb || s->st->construct_finished) { + scr_closure_release(cb); + return; + } + scr_stream_construct(s, cb, inv); +} + +/* A class's _writev under options that may carry their own writev (the + * option, an instance property in Node, wins). */ +void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamWritevInv inv) { + if (s->st->has_w && s->st->w.writev_cb == NULL) { + scr_stream_set_writev(s, cb, inv); + return; + } + scr_closure_release(cb); +} + +/* A user writev option or _writev method. */ +void scr_stream_set_writev(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamWritevInv inv) { + ScrStreamState *st = s->st; + if (!st->has_w) { + scr_closure_release(cb); + return; + } + if (st->w.writev_cb) scr_closure_release(st->w.writev_cb); + st->w.writev_cb = cb; + st->w.writev_inv = inv; +} + void scr_stream_fail(ScrStream *s, ScrError *err) { scr_stream_error_or_destroy(s, err); } static void scr_stream_plain_inv(ScrClosure *cb, va_list ap) { @@ -6780,6 +7673,7 @@ static void scr_stream_run_tick(ScrStreamTick *t) { if (st->r.read_cb) { scr_closure_release(st->r.read_cb); st->r.read_cb = NULL; } if (st->w.write_cb) { scr_closure_release(st->w.write_cb); st->w.write_cb = NULL; } if (st->w.final_cb) { scr_closure_release(st->w.final_cb); st->w.final_cb = NULL; } + if (st->w.writev_cb) { scr_closure_release(st->w.writev_cb); st->w.writev_cb = NULL; } if (st->transform_cb) { scr_closure_release(st->transform_cb); st->transform_cb = NULL; } if (st->flush_cb) { scr_closure_release(st->flush_cb); st->flush_cb = NULL; } if (st->destroy_cb) { scr_closure_release(st->destroy_cb); st->destroy_cb = NULL; } @@ -6834,7 +7728,11 @@ static void scr_stream_run_tick(ScrStreamTick *t) { scr_stream_error_buffer(s); break; case SCR_ST_CONSTRUCT: { - if (st->construct_cb) st->construct_inv(st->construct_cb, s); + if (st->construct_cb) { + st->construct_inv(st->construct_cb, s); + /* a throw inside _construct reaches onConstruct on a tick */ + if (scr_exc_pending()) scr_stream_construct_done(s, scr_stream_take_error()); + } break; } case SCR_ST_CONSTRUCTED: { @@ -7038,6 +7936,18 @@ static ScrStream *scr_stream_boxed(const ScrDyn *value) { return NULL; } +/* The native readable stream behind a checked-dynamic value (+1): a + * boxed stream or a Cipheriv/Decipheriv handle's Transform, or NULL. */ +ScrStream *scr_stream_native_readable_of_dyn(const ScrDyn *value) { + ScrStream *s = scr_stream_of_handle(value); + if (!s) s = scr_stream_boxed(value); + if (s && !s->st->has_r) { + scr_stream_release(s); + return NULL; + } + return s; +} + /* stream.handleStream: the stream of a value typed as a stream handle * class (a Cipheriv or Decipheriv), +1. A value of that type can also be * the stream itself, boxed (the stream addAbortSignal answers). */ diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 8b8a72406..d26077b5c 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -65,6 +65,8 @@ typedef void (*ScrStreamReadInv)(ScrClosure *cb, ScrStream *s, double size); typedef void (*ScrStreamChunkInv)(ScrClosure *cb, ScrStream *s, ScrBytes *chunk); typedef void (*ScrStreamPlainInv)(ScrClosure *cb, ScrStream *s); typedef void (*ScrStreamErrInv)(ScrClosure *cb, ScrStream *s, ScrError *err); +/* writev(chunks, callback): chunks is the batch's dyn array (borrowed). */ +typedef void (*ScrStreamWritevInv)(ScrClosure *cb, ScrStream *s, struct ScrDyn *chunks); /* Constructors (+1 result; option callbacks MOVE; NULL = absent). hwm < 0 * means the byte default (65536 — Node 24's getDefaultHighWaterMark). */ @@ -223,6 +225,9 @@ ScrStream *scr_stream_pipeline_stages(double n, ScrStream **streams, double coun double mask, ScrClosure **helpers, ScrClosure *box, struct ScrDyn *signal, ScrClosure *abort, ScrClosure *cb, ScrStreamErrInv inv); +ScrStream *scr_stream_compose_stages(double n, ScrStream **streams, double count, ScrClosure **stages, + double positions, double flags, double mask, ScrClosure **helpers, + ScrClosure *box, ScrDyn *signal, ScrClosure *abort); ScrStream *scr_stream_pipeline_stages_dyn(double n, ScrStream **streams, double count, ScrClosure **stages, double positions, double flags, double mask, ScrClosure **helpers, ScrClosure *box, @@ -261,6 +266,9 @@ ScrDyn *scr_stream_done_dyn_l(ScrClosure *clo, ScrDyn *const *args, size_t argc) /* The readable surface. push/unshift borrow their chunk (strings convert * utf8, Node's decodeStrings default); push answers the below-hwm bool. */ bool scr_stream_push(ScrStream *s, ScrBytes *chunk); +/* readable.push(chunk) from a program: a non-Buffer view becomes a Buffer + * over the same bytes in byte mode. */ +bool scr_stream_push_view(ScrStream *s, ScrBytes *chunk); bool scr_stream_push_str(ScrStream *s, ScrStr *str); /* push(chunk, enc): the per-call literal encoding (canonical); overrides * the stream's defaultEncoding. Borrows both. */ @@ -296,6 +304,14 @@ bool scr_stream_write_dyn(ScrStream *s, const ScrDyn *d, ScrClosure *cb); /* cb bool scr_stream_write_enc(ScrStream *s, const ScrDyn *chunk, ScrDyn *enc, ScrClosure *cb); ScrPromise *scr_stream_next_chunk(ScrStream *s); ScrPromise *scr_stream_next_chunk_dyn(ScrStream *s); +/* The same step resolving with an iterator result ({ value, done }). */ +ScrPromise *scr_stream_next_result_dyn(ScrStream *s); +/* The native readable stream behind a checked-dynamic value (+1), or NULL. */ +ScrStream *scr_stream_native_readable_of_dyn(const struct ScrDyn *value); +/* for await over a checked-dynamic value in a program with streams + * (scr_stream_ops.c): a native readable stream iterates as a typed loop + * does; any other value takes scr_dyn_async_iterator_open. */ +struct ScrDyn *scr_stream_dyn_async_iterator_open(struct ScrDyn *value, ScrStr *spell); /* node:stream/promises — the promise forms over the finished/pipeline * machinery above: a pending void promise the terminal watcher settles * (fulfilled on a clean finish, rejected with the finish status @@ -329,6 +345,7 @@ enum { SCR_FROM_BYTE_MODE = 4, SCR_FROM_HAS_HWM = 8, SCR_FROM_DEFAULT_HWM = 16, /* a nullish highWaterMark: the mode's default */ + SCR_FROM_DUPLEX = 32, /* Duplex.from: a Duplexify with its writable side off */ }; ScrStream *scr_stream_from_arr(ScrArr *arr, double flags, double hwm); /* Readable.from(iterable, options) over a checked-dynamic source: a @@ -414,18 +431,41 @@ int scr_stream_push_object(ScrStream *s, const ScrDyn *v); * dyn array, find and reduce a dyn value — reduce's `kind` picks the * payload instead: 0 dyn, 1 number, 2 boolean, 3 string — some and every * a boolean, forEach nothing. */ -ScrStream *scr_stream_op_map(ScrStream *s, ScrDyn *fn, ScrDyn *options); -ScrStream *scr_stream_op_filter(ScrStream *s, ScrDyn *fn, ScrDyn *options); -ScrStream *scr_stream_op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options); +ScrStream *scr_stream_op_map(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +ScrStream *scr_stream_op_filter(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +ScrStream *scr_stream_op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +/* flatMap whose callback returns a compiled generator: the helpers over its + * type (async: next, read, return, throw; sync: next, return, throw; + * borrowed). */ +ScrStream *scr_stream_op_flat_map_async_gen(ScrStream *s, ScrDyn *fn, ScrDyn *options, + ScrDyn *cb_opts, ScrClosure *next, ScrClosure *read, + ScrClosure *ret, ScrClosure *thr); +ScrStream *scr_stream_op_flat_map_sync_gen(ScrStream *s, ScrDyn *fn, ScrDyn *options, + ScrDyn *cb_opts, ScrClosure *next, ScrClosure *ret, + ScrClosure *thr); ScrStream *scr_stream_op_drop(ScrStream *s, ScrDyn *number, ScrDyn *options); ScrStream *scr_stream_op_take(ScrStream *s, ScrDyn *number, ScrDyn *options); ScrPromise *scr_stream_op_to_array(ScrStream *s, ScrDyn *options); -ScrPromise *scr_stream_op_for_each(ScrStream *s, ScrDyn *fn, ScrDyn *options); -ScrPromise *scr_stream_op_some(ScrStream *s, ScrDyn *fn, ScrDyn *options); -ScrPromise *scr_stream_op_every(ScrStream *s, ScrDyn *fn, ScrDyn *options); -ScrPromise *scr_stream_op_find(ScrStream *s, ScrDyn *fn, ScrDyn *options); +ScrPromise *scr_stream_op_for_each(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +ScrPromise *scr_stream_op_some(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +ScrPromise *scr_stream_op_every(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +ScrPromise *scr_stream_op_find(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); ScrPromise *scr_stream_op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, ScrDyn *options, double kind); +/* reduce whose reducer reads its { signal } (cb_opts): abort is ac.abort() + * (borrowed), run when the reduction ends. */ +ScrPromise *scr_stream_op_reduce_signal(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, + ScrDyn *options, double kind, ScrClosure *abort, + ScrDyn *cb_opts); +/* `options?.signal` (+1, undefined when absent) and `{ once: true }` (+1), + * for the listener that makes options.signal abort reduce's controller. */ +ScrDyn *scr_stream_op_signal_of(ScrDyn *options); +ScrDyn *scr_stream_op_once_options(void); +/* The operator callbacks' { signal } (the options argument): the signals + * AbortSignal.any combines (`[options?.signal].filter(Boolean)`) and the + * object around the combined signal. +1 each. */ +ScrDyn *scr_stream_op_signal_list(ScrDyn *options); +ScrDyn *scr_stream_op_signal_opts(ScrDyn *signal); /* finished(s, options[, cb]) — Node's eos with its listeners and state * predicates: readable/writable are -1 (the default), 0 or 1; on_error is * options.error !== false; signal is NULL or borrowed (ERR_INVALID_ARG_TYPE @@ -445,6 +485,12 @@ ScrPromise *scr_sp_finished_opts(ScrStream *s, double readable, double writable, * ERR_INVALID_ARG_TYPE. Streams borrowed. */ ScrPromise *scr_sp_pipeline_opts(double n, ScrStream **streams, bool end, ScrDyn *signal, ScrAbortAddFn add); +/* scr_sp_pipeline_stages with the options' end and signal (outer). */ +ScrPromise *scr_sp_pipeline_stages_opts(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + ScrDyn *signal, ScrClosure *abort, bool end, + ScrDyn *outer, ScrAbortAddFn add); double scr_stream_prop(ScrStream *s, const char *name); ScrError *scr_stream_errored(ScrStream *s); /* +1 or NULL */ @@ -512,6 +558,57 @@ void scr_stream_flush_done_value(ScrStream *s, ScrError *err, struct ScrDyn *v); void scr_stream_construct(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); void scr_stream_construct_done(ScrStream *s, ScrError *err); void scr_stream_set_writev_concat(ScrStream *s); +/* The user construct option / _construct method (scheduled now; cb + * moves), and the writev option / _writev method (cb moves). */ +void scr_stream_set_construct(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); +void scr_stream_set_writev(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); +void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); +/* Duplex.from (duplexify.js): over an async generator function (the + * compiled stage takes the source generator that the compiled source + * makes from next() and ack(); all borrowed, helpers too), and over a + * checked-dynamic body (borrowed). +1 each. The typed iterable bodies take + * the Readable.from builders with SCR_FROM_DUPLEX. */ +ScrStream *scr_stream_duplex_from_gen(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr); +ScrStream *scr_stream_duplex_from_dyn(struct ScrDyn *body); +/* Readable.fromWeb(readableStream, { objectMode, highWaterMark, encoding }) + * (scr_stream_from_web.c): hwm < 0 is the mode's default, encoding NULL or + * a known name; web and encoding borrowed. +1, or NULL with the error. */ +ScrStream *scr_stream_readable_from_web(struct ScrDyn *web, bool object_mode, double hwm, ScrStr *encoding); +/* Duplex.from over an async function (the compiled stage takes the source + * generator and answers its promise; source and the promise's box helper + * borrowed): a writable Duplexify whose final waits for the promise. +1. */ +ScrStream *scr_stream_duplex_from_async_fn(ScrClosure *stage, ScrClosure *source, ScrClosure *box); +/* The same for a body that declares the options argument: it takes + * { signal } (signal borrowed), and destroy calls abort(err) (borrowed), + * the AbortController's abort, before it settles (fromAsyncGen's + * ac.abort(err)). */ +ScrStream *scr_stream_duplex_from_gen_signal(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr, + ScrClosure *abort, struct ScrDyn *signal); +ScrStream *scr_stream_duplex_from_async_fn_signal(ScrClosure *stage, ScrClosure *source, + ScrClosure *box, ScrClosure *abort, + struct ScrDyn *signal); +/* Duplex.from over a Readable, a Writable, or a { readable, writable } + * pair (duplexify.js's _duplexify): a Duplexify that reads r and writes w + * (either NULL in scr_stream_duplexify; streams borrowed). +1. */ +ScrStream *scr_stream_duplexify(ScrStream *r, ScrStream *w); +ScrStream *scr_stream_duplexify_readable(ScrStream *r); +ScrStream *scr_stream_duplexify_writable(ScrStream *w); +ScrStream *scr_stream_duplexify_pair(ScrStream *r, ScrStream *w); +/* Duplex.from(promise): a readable Duplexify of the promise's value. p and + * box (the compiler's promise → dyn promise helper) borrowed. +1. */ +ScrStream *scr_stream_duplex_from_promise(struct ScrPromise *p, ScrClosure *box); +/* decodeStrings (false keeps string writes as strings), the writable + * defaultEncoding option (a known encoding, borrowed), and + * setDefaultEncoding (validated; answers the stream +1). The chaining + * forms answer the stream (+1). */ +void scr_stream_set_decode_strings(ScrStream *s, bool decode); +ScrStream *scr_stream_decode_strings(ScrStream *s, bool decode); +void scr_stream_set_write_encoding(ScrStream *s, ScrStr *enc); +ScrStream *scr_stream_write_encoding(ScrStream *s, ScrStr *enc); +ScrStream *scr_stream_set_default_encoding(ScrStream *s, struct ScrDyn *enc); +ScrDyn *scr_stream_done_dyn_c(ScrClosure *clo, ScrDyn *const *args, size_t argc); /* The native state's extra operations: view adds its members to the * stream's untyped view and close is the view's close() (both NULL when * absent); aggregate builds aggregateTwoErrors' +1 AggregateError from the diff --git a/packages/runtime/src/scr_stream_from_web.c b/packages/runtime/src/scr_stream_from_web.c new file mode 100644 index 000000000..f88826335 --- /dev/null +++ b/packages/runtime/src/scr_stream_from_web.c @@ -0,0 +1,247 @@ +/* Readable.fromWeb (Node's newStreamReadableFromReadableStream in + * internal/webstreams/adapters.js): a Readable over a web ReadableStream's + * reader. read() queues one read request (scr_fetch_reader_read_into); a + * chunk is pushed in a microtask, the end pushes null and an error + * destroys the Readable. destroy() cancels the reader unless it has + * closed, then completes. reader.closed's settlement marks it closed (a + * rejection also destroys the Readable). Linked only by programs that + * use both streams and the native web streams. */ +#include "scr_runtime.h" +#include "scr_stream.h" + +#include +#include + +typedef struct SfwCtx SfwCtx; +struct SfwCtx { + size_t rc; + ScrStream *readable; /* +1 until teardown */ + ScrDyn *reader; /* +1 */ + ScrDyn *pending; /* +1: pendingChunk, or NULL */ + bool closed; + SfwCtx *live_prev, *live_next; +}; + +static SCR_TL SfwCtx *sfw_live = NULL; + +static SfwCtx *sfw_ref(SfwCtx *c) { + c->rc++; + return c; +} + +static void sfw_unref(SfwCtx *c) { + if (--c->rc > 0) return; + if (c->readable) scr_stream_release(c->readable); + if (c->reader) scr_dyn_release(c->reader); + if (c->pending) scr_dyn_release(c->pending); + free(c); +} + +static void *sfw_retain_v(void *p) { return sfw_ref(p); } +static void sfw_release_v(void *p) { sfw_unref(p); } + +/* Drop the Readable (the cycle through its callbacks). */ +static void sfw_teardown(SfwCtx *c) { + if (c->live_prev) c->live_prev->live_next = c->live_next; + else if (sfw_live == c) sfw_live = c->live_next; + else return; /* already torn down */ + if (c->live_next) c->live_next->live_prev = c->live_prev; + c->live_prev = c->live_next = NULL; + ScrStream *r = c->readable; + c->readable = NULL; + if (r) scr_stream_release(r); + sfw_unref(c); /* the live list's reference */ +} + +static void sfw_atexit(void) { + while (sfw_live) sfw_teardown(sfw_live); +} + +static ScrClosure *sfw_closure(void *fn, SfwCtx *c) { + ScrClosure *cl = scr_closure_new(fn, 1); + cl->caps[0] = scr_box_new_obj(&sfw_retain_v, &sfw_release_v, NULL); + scr_box_set_ref(cl->caps[0], sfw_ref(c)); + return cl; +} + +static SfwCtx *sfw_of(ScrClosure *cl) { return scr_box_get_ref(cl->caps[0]); /* +1 */ } + +/* destroyer(readable, error) */ +static void sfw_destroyer(SfwCtx *c, ScrDyn *reason) { + ScrStream *r = c->readable; + if (!r || scr_stream_prop(r, "destroyed")) return; + ScrError *err = reason ? scr_error_from_dyn(reason) : NULL; + if (scr_exc_pending()) scr_exc_clear(); + scr_stream_release(scr_stream_destroy(r, err)); + if (err) scr_error_release(err); +} + +/* forwardChunk: readable.push(pendingChunk) */ +static void sfw_forward(ScrClosure *job) { + SfwCtx *c = sfw_of(job); + ScrDyn *chunk = c->pending; + c->pending = NULL; + if (chunk && c->readable) { + scr_stream_push_dyn(c->readable, chunk); + if (scr_exc_pending()) scr_exc_clear(); + } + if (chunk) scr_dyn_release(chunk); + sfw_unref(c); +} + +static void sfw_sink_chunk(void *ctx, ScrDyn *value) { + SfwCtx *c = ctx; + if (c->pending) scr_dyn_release(c->pending); + c->pending = scr_dyn_retain(value); + scr_queue_microtask(sfw_closure((void *)&sfw_forward, c)); + sfw_unref(c); /* the request's reference */ +} + +static void sfw_sink_close(void *ctx) { + SfwCtx *c = ctx; + if (c->readable) scr_stream_push_null(c->readable); + if (scr_exc_pending()) scr_exc_clear(); + sfw_unref(c); +} + +static void sfw_sink_error(void *ctx, ScrDyn *reason) { + SfwCtx *c = ctx; + sfw_destroyer(c, reason); + sfw_unref(c); +} + +static const ScrFetchReadSink sfw_sink = {&sfw_sink_chunk, &sfw_sink_close, &sfw_sink_error}; + +/* read(): readableStreamDefaultReaderRead(reader, readRequest) */ +static void sfw_read_inv(ScrClosure *cb, ScrStream *s, double size) { + (void)s; + (void)size; + SfwCtx *c = sfw_of(cb); + if (c->reader) { + if (!scr_fetch_reader_read_into(c->reader, &sfw_sink, sfw_ref(c))) { + sfw_unref(c); + if (scr_exc_pending()) scr_exc_clear(); + } + } + sfw_unref(c); +} + +/* destroy's done(): callback(error) */ +typedef struct { + SfwCtx *c; /* +1 */ + ScrStream *s; /* +1 */ + ScrError *err; /* +1 or NULL */ + ScrDyn *cancel; /* +1: reader.cancel(error)'s promise */ +} SfwDestroy; + +static void sfw_destroy_done(SfwCtx *c, ScrStream *s, ScrError *err) { + scr_stream_destroy_done(s, err ? scr_error_retain(err) : NULL); + sfw_teardown(c); +} + +static void sfw_cancel_entry(ScrFiber *fiber, void *arg) { + (void)fiber; + SfwDestroy d = *(SfwDestroy *)arg; + free(arg); + ScrDyn *v = scr_await_ref(d.cancel->v.promise); /* then(done, done) */ + if (scr_exc_pending()) scr_exc_clear(); + if (v) scr_dyn_release(v); + sfw_destroy_done(d.c, d.s, d.err); + if (d.err) scr_error_release(d.err); + scr_stream_release(d.s); + scr_dyn_release(d.cancel); + sfw_unref(d.c); +} + +static void sfw_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + SfwCtx *c = sfw_of(cb); + if (!c->closed && c->reader) { + /* _destroy(err, cb) gets null for a destroy without an error */ + ScrDyn *reason = err ? scr_dyn_from_error(err) : scr_dyn_new_null(); + ScrDyn *cancel = scr_dyn_invoke(c->reader, "cancel", &reason, 1, "reader.cancel"); + scr_dyn_release(reason); + if (cancel && cancel->kind == SCR_DYN_PROMISE) { + SfwDestroy *d = malloc(sizeof *d); + if (!d) abort(); + d->c = sfw_ref(c); + d->s = scr_stream_retain(s); + d->err = err ? scr_error_retain(err) : NULL; + d->cancel = cancel; + scr_promise_release(scr_async_spawn_after(cancel->v.promise, &sfw_cancel_entry, d)); + sfw_unref(c); + return; + } + if (cancel) scr_dyn_release(cancel); + if (scr_exc_pending()) scr_exc_clear(); + } + sfw_destroy_done(c, s, err); + sfw_unref(c); +} + +/* reader.closed.then(() => closed = true, (e) => { closed = true; destroy(readable, e) }) */ +typedef struct { + SfwCtx *c; /* +1 */ + ScrDyn *closed; /* +1, the promise */ +} SfwClosed; + +static void sfw_closed_entry(ScrFiber *fiber, void *arg) { + (void)fiber; + SfwClosed w = *(SfwClosed *)arg; + free(arg); + ScrDyn *v = scr_await_ref(w.closed->v.promise); + w.c->closed = true; + if (scr_exc_pending()) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *reason = scr_caught_to_dyn(caught); + scr_caught_release(caught); + sfw_destroyer(w.c, reason); + if (reason) scr_dyn_release(reason); + } + if (v) scr_dyn_release(v); + scr_dyn_release(w.closed); + sfw_unref(w.c); +} + +/* web: the ReadableStream (borrowed); hwm < 0 = the mode's default; + * encoding NULL or a known name (borrowed). +1, or NULL with the error + * pending. */ +ScrStream *scr_stream_readable_from_web(ScrDyn *web, bool object_mode, double hwm, ScrStr *encoding) { + if (encoding && encoding->len == 0) encoding = NULL; + if (!web || web->kind != SCR_DYN_HANDLE || web->v.handle.tag != SCR_DYNH_WEB_STREAM) { + scr_dyn_arg_type_fail("readableStream", "an instance of ReadableStream", web); + return NULL; + } + ScrDyn *reader = scr_dyn_invoke(web, "getReader", NULL, 0, "readableStream.getReader"); + if (!reader) return NULL; + SfwCtx *c = calloc(1, sizeof *c); + if (!c) abort(); + c->rc = 1; + c->reader = reader; + ScrStream *r = scr_stream_new_readable(hwm < 0 ? (object_mode ? 16 : -1) : hwm, true, true, + sfw_closure((void *)&sfw_read_inv, c), &sfw_read_inv, + sfw_closure((void *)&sfw_destroy_inv, c), &sfw_destroy_inv); + if (object_mode) scr_stream_set_object_mode(r, true, false); + if (encoding) scr_stream_release(scr_stream_set_encoding(r, encoding)); + c->readable = scr_stream_retain(r); + static SCR_TL bool registered = false; + if (!registered) { + registered = true; + scr_atexit(sfw_atexit); + } + c->live_next = sfw_live; + if (sfw_live) sfw_live->live_prev = c; + sfw_live = c; /* the reference c was created with */ + ScrStr *key = scr_str_new("closed", 6); + ScrDyn *closed = scr_dyn_handle_key_get(reader, key); + scr_str_release(key); + if (closed && closed->kind == SCR_DYN_PROMISE) { + SfwClosed *w = malloc(sizeof *w); + if (!w) abort(); + w->c = sfw_ref(c); + w->closed = closed; + scr_promise_release(scr_async_spawn_after(closed->v.promise, &sfw_closed_entry, w)); + } else if (closed) { + scr_dyn_release(closed); + } + return r; +} diff --git a/packages/runtime/src/scr_stream_ops.c b/packages/runtime/src/scr_stream_ops.c index 8fd220168..a92b4a0e5 100644 --- a/packages/runtime/src/scr_stream_ops.c +++ b/packages/runtime/src/scr_stream_ops.c @@ -267,20 +267,15 @@ static bool op_aborted(const ScrDyn *signal) { return r; } -/* The callback argument: a function that does not read the options - * argument. `max` is how many parameters Node's call passes before the - * options. False with the error pending. */ +/* The callback argument: validateFunction. False with the error pending. + * (The options argument the callback may read is the operator's + * cb_opts.) */ static bool op_check_fn(const ScrDyn *fn, const char *name, uint32_t max) { + (void)max; if (!fn || !scr_dyn_is_callable(fn)) { scr_dyn_arg_type_fail(name, "of type function", fn ? fn : scr_dyn_undefined()); return false; } - if (fn->kind == SCR_DYN_FUNC && fn->v.fn.arity > max) { - static const char m[] = - "stream operator callbacks that read the options argument ({ signal }) are not supported yet"; - scr_throw_error_msg(SCR_ERR_ERROR, m, sizeof m - 1); - return false; - } return true; } @@ -610,8 +605,9 @@ enum { MODE_MAP, MODE_FILTER, MODE_FOREACH, MODE_EVERY }; typedef struct { OpGen g; OpSrc src; - ScrDyn *fn; /* +1 */ - ScrDyn *signal; /* +1 or NULL */ + ScrDyn *fn; /* +1 */ + ScrDyn *signal; /* +1 or NULL */ + ScrDyn *cb_opts; /* +1: the { signal } fn takes as its second argument, or NULL */ int mode; double concurrency, hwm; ScrDyn **queue; /* +1 entries: dyn promises or kEof */ @@ -628,6 +624,7 @@ static void op_map_body_drop(OpGen *g) { src_release(&m->src); scr_dyn_release(m->fn); if (m->signal) scr_dyn_release(m->signal); + if (m->cb_opts) scr_dyn_release(m->cb_opts); for (size_t i = m->qh; i < m->qn; i++) scr_dyn_release(m->queue[i]); free(m->queue); if (m->next_p) scr_promise_release(m->next_p); @@ -734,9 +731,11 @@ static void op_wrap_k(void *self, ScrDyn *v) { } /* Calls fn(chunk): +1 result, or NULL with the exception pending. */ -static ScrDyn *op_call(ScrDyn *fn, ScrDyn *chunk) { - ScrDyn *args[1] = {chunk}; - return scr_dyn_call(fn, args, 1, "fn"); +/* Calls fn(chunk, signalOpt) — the second argument when the callback + * declares it. +1 result, or NULL with the exception pending. */ +static ScrDyn *op_call(OpMap *m, ScrDyn *chunk) { + ScrDyn *args[2] = {chunk, m->cb_opts}; + return scr_dyn_call(m->fn, args, m->cb_opts ? 2 : 1, "fn"); } /* An async wrapper body: `await fn(chunk)` then its transform. The @@ -744,7 +743,7 @@ static ScrDyn *op_call(ScrDyn *fn, ScrDyn *chunk) { static ScrPromise *op_wrap_call(OpMap *m, ScrDyn *chunk, int mode, int stage) { OpWrap *w = op_wrap_new(mode, chunk, stage); ScrPromise *result = scr_promise_retain(w->p); - ScrDyn *r = op_call(m->fn, chunk); + ScrDyn *r = op_call(m, chunk); if (!r) { scr_promise_reject_pending(w->p); } else { @@ -772,7 +771,7 @@ static ScrPromise *op_every_call(OpMap *m, ScrDyn *chunk) { static ScrDyn *map_item(OpMap *m, ScrDyn *chunk) { ScrPromise *p; if (m->mode == MODE_MAP) { - ScrDyn *r = op_call(m->fn, chunk); + ScrDyn *r = op_call(m, chunk); if (!r) { ScrCaught *c = scr_exc_take(); ScrDyn *d = op_rejected_dyn(c); @@ -981,7 +980,7 @@ static void map_thr(OpGen *g, ScrCaught *c) { } static OpMap *map_new(ScrStream *s, ScrDyn *fn, int mode, double concurrency, double hwm, - ScrDyn *signal) { + ScrDyn *signal, ScrDyn *cb_opts) { OpMap *m = op_alloc(sizeof *m); gen_init(&m->g, &op_map_body_drop); m->g.resume = &map_resume; @@ -990,6 +989,7 @@ static OpMap *map_new(ScrStream *s, ScrDyn *fn, int mode, double concurrency, do m->src.s = scr_stream_retain(s); m->fn = scr_dyn_retain(fn); m->signal = signal ? scr_dyn_retain(signal) : NULL; + m->cb_opts = cb_opts && cb_opts->kind != SCR_DYN_UNDEF ? scr_dyn_retain(cb_opts) : NULL; m->mode = mode; m->concurrency = concurrency; m->hwm = hwm; @@ -1137,27 +1137,137 @@ static void slice_thr(OpGen *g, ScrCaught *c) { * goes through the async-from-sync iterator: each step resolves after the * element's PromiseResolve job, and yield* awaits that result before * yielding without a further await. */ +/* yield* over the delegate: an array or string (its elements, through + * the async-from-sync iterator), another iterable's iterator (FLAT_SYNC + * through the async-from-sync iterator, FLAT_ASYNC awaiting next()), or a + * native Readable (FLAT_STREAM, its async iterator). */ +enum { FLAT_NONE, FLAT_SYNC, FLAT_ASYNC, FLAT_STREAM, FLAT_GEN_SYNC, FLAT_GEN_ASYNC }; + +typedef ScrDyn *(*FromSyncFn)(ScrClosure *, ScrGen *); +typedef ScrDyn *(*FromSyncThrowFn)(ScrClosure *, ScrGen *, ScrDyn *); +typedef ScrPromise *(*FromAsyncFn)(ScrClosure *, ScrGen *); +typedef ScrPromise *(*FromAsyncThrowFn)(ScrClosure *, ScrGen *, ScrDyn *); +typedef ScrDyn *(*FromReadFn)(ScrClosure *, void *); +static void op_await_gen(ScrPromise *p, ScrClosure *read, OpCont k, void *self); + typedef struct { OpGen g; OpSrc values; /* the map generator */ ScrDyn *inner; /* +1: the array or string being delegated to, or NULL */ size_t idx; /* element index, or the string's byte offset */ ScrPromise *step; /* the async-from-sync iterator result being awaited */ + ScrDyn *iter; /* +1: the delegate's iterator (FLAT_SYNC / FLAT_ASYNC), or NULL */ + ScrStream *stream; /* +1: the delegate stream (FLAT_STREAM), or NULL */ + ScrGen *gen; /* +1: a compiled generator delegate (FLAT_GEN_*), or NULL */ + /* The compiler's helpers over the callback's generator type (+1 each, + * or NULL): next/return/throw, and read for an async generator. */ + ScrClosure *gnext, *gread, *gret, *gthr; + bool gen_async; + int iter_kind; bool started; } OpFlat; +/* A delegate's member (+1), read as the IR's keyed read does: a compiled + * value (a generator, its result record) through its typed ref, anything + * else through the iterator-result read. NULL with the error pending. */ +static ScrDyn *flat_get(const ScrDyn *v, const char *key) { + if (v->kind == SCR_DYN_TYPED_REF) { + ScrStr *k = scr_str_new(key, strlen(key)); + ScrDyn *r = scr_dyn_typed_ref_key_get(v, k); + scr_str_release(k); + return r; + } + return scr_dyn_iterator_result_get(v, key, strlen(key)); +} + +/* iterator[name](...args) with the iterator as this: +1, or NULL with the + * error pending. */ +static ScrDyn *flat_call(const ScrDyn *iter, const char *name, ScrDyn *const *args, size_t argc) { + ScrDyn *m = flat_get(iter, name); + if (!m) return NULL; + scr_dyn_this_push_dyn(iter); + ScrDyn *r = scr_dyn_call(m, args, argc, name); + scr_dyn_this_pop(); + scr_dyn_release(m); + return r; +} + +/* `const { value, done } = result`: +1 value with *done set, or NULL with + * the error pending. */ +static ScrDyn *flat_result(const ScrDyn *r, bool *done) { + if (r->kind == SCR_DYN_UNDEF || r->kind == SCR_DYN_NULL || r->kind == SCR_DYN_NUM || + r->kind == SCR_DYN_STR || r->kind == SCR_DYN_BOOL) { + ScrDyn *checked = scr_dyn_iterator_result((ScrDyn *)r); /* throws V8's TypeError */ + if (checked) scr_dyn_release(checked); + return NULL; + } + ScrDyn *d = flat_get(r, "done"); + if (!d) return NULL; + *done = scr_dyn_truthy(d); + scr_dyn_release(d); + return flat_get(r, "value"); +} + +static void flat_drop_delegate(OpFlat *f) { + if (f->inner) scr_dyn_release(f->inner); + if (f->iter) scr_dyn_release(f->iter); + if (f->stream) scr_stream_release(f->stream); + if (f->gen) scr_gen_release(f->gen); + f->inner = f->iter = NULL; + f->stream = NULL; + f->gen = NULL; + f->iter_kind = FLAT_NONE; +} + static void op_flat_body_drop(OpGen *g) { OpFlat *f = (OpFlat *)g; src_release(&f->values); - if (f->inner) scr_dyn_release(f->inner); + flat_drop_delegate(f); if (f->step) scr_promise_release(f->step); + if (f->gnext) scr_closure_release(f->gnext); + if (f->gread) scr_closure_release(f->gread); + if (f->gret) scr_closure_release(f->gret); + if (f->gthr) scr_closure_release(f->gthr); free(f); } static void flat_next_value(OpFlat *f); -/* The next element (+1), or NULL when the delegate is exhausted. */ -static ScrDyn *flat_take_element(OpFlat *f) { +/* The next element (+1), or NULL when the delegate is exhausted; for a + * sync iterator NULL with *failed set when next() threw (pending). */ +static ScrDyn *flat_take_element(OpFlat *f, bool *failed) { + *failed = false; + if (f->iter_kind == FLAT_GEN_SYNC) { + ScrDyn *v = ((FromSyncFn)f->gnext->fn)(f->gnext, scr_gen_retain(f->gen)); + if (!v) { + *failed = true; + return NULL; + } + if (v == op_k_done) { + scr_dyn_release(v); + return NULL; + } + return v; + } + if (f->iter_kind == FLAT_SYNC) { + ScrDyn *r = flat_call(f->iter, "next", NULL, 0); + if (!r) { + *failed = true; + return NULL; + } + bool done = false; + ScrDyn *v = flat_result(r, &done); + scr_dyn_release(r); + if (!v) { + *failed = true; + return NULL; + } + if (done) { + scr_dyn_release(v); + return NULL; + } + return v; + } ScrDyn *in = f->inner; if (in->kind == SCR_DYN_ARR) { if (f->idx >= in->v.arr.len) return NULL; @@ -1185,10 +1295,15 @@ static void flat_step_k(void *self, ScrDyn *v) { scr_promise_release(f->step); f->step = NULL; } - if (v == op_k_done || (v && v == op_k_eof)) { + if (!v) { + /* the delegate's next() threw: it leaves the for await */ + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + if (v == op_k_done || v == op_k_eof) { scr_dyn_release(v); - scr_dyn_release(f->inner); - f->inner = NULL; + flat_drop_delegate(f); flat_next_value(f); return; } @@ -1196,6 +1311,33 @@ static void flat_step_k(void *self, ScrDyn *v) { scr_dyn_release(v); } +/* Await(innerResult) for an async delegate: done ends it, else its value + * is yielded as it is. */ +static void flat_iter_k(void *self, ScrDyn *v) { + OpFlat *f = self; + if (!v) { + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + bool done = false; + ScrDyn *value = flat_result(v, &done); + scr_dyn_release(v); + if (!value) { + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + if (done) { + scr_dyn_release(value); + flat_drop_delegate(f); + flat_next_value(f); + return; + } + gen_yield_now(&f->g, value); + scr_dyn_release(value); +} + /* The async-from-sync continuation: PromiseResolve(value) then the result * promise settles; yield* awaits that promise. */ typedef struct { @@ -1220,8 +1362,56 @@ static void op_flat_wrap_k(void *self, ScrDyn *v) { scr_dyn_release(v); } +/* The awaited resume of an async compiled generator delegate, read to its + * value or the done marker: done ends it, else the value is yielded. */ +static void flat_gen_k(void *self, ScrDyn *v) { + OpFlat *f = self; + if (!v) { + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + if (v == op_k_done) { + scr_dyn_release(v); + flat_drop_delegate(f); + flat_next_value(f); + return; + } + gen_yield_now(&f->g, v); + scr_dyn_release(v); +} + static void flat_delegate_step(OpFlat *f) { - ScrDyn *e = flat_take_element(f); + if (f->iter_kind == FLAT_GEN_ASYNC) { + ScrPromise *p = ((FromAsyncFn)f->gnext->fn)(f->gnext, scr_gen_retain(f->gen)); + op_await_gen(p, f->gread, &flat_gen_k, f); + scr_promise_release(p); + return; + } + if (f->iter_kind == FLAT_ASYNC) { + ScrDyn *r = flat_call(f->iter, "next", NULL, 0); + if (!r) { + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + op_await_value(r, &flat_iter_k, f); + scr_dyn_release(r); + return; + } + if (f->iter_kind == FLAT_STREAM) { + ScrPromise *p = scr_stream_next_result_dyn(f->stream); + op_await(p, &flat_iter_k, f); + scr_promise_release(p); + return; + } + bool failed = false; + ScrDyn *e = flat_take_element(f, &failed); + if (failed) { + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } OpFlatWrap *w = op_alloc(sizeof *w); w->base.rc = 1; w->base.drop = &op_flat_wrap_drop; @@ -1247,15 +1437,63 @@ static void flat_val_k(void *self, ScrDyn *v) { gen_complete(&f->g, NULL); return; } - if (v->kind != SCR_DYN_ARR && v->kind != SCR_DYN_STR) { - scr_dyn_release(v); - ScrCaught *c = op_take_error( - op_unsupported("flatMap callback results other than arrays or strings")); - gen_close_source(&f->g, &f->values, c); + if (v->kind == SCR_DYN_ARR || v->kind == SCR_DYN_STR) { + f->inner = v; + f->idx = 0; + flat_delegate_step(f); return; } - f->inner = v; - f->idx = 0; + ScrStream *stream = scr_stream_native_readable_of_dyn(v); + if (stream) { + scr_dyn_release(v); + f->stream = stream; + f->iter_kind = FLAT_STREAM; + flat_delegate_step(f); + return; + } + /* a compiled generator of the callback's declared type: stepped through + * the compiler's helpers */ + if (f->gnext && v->kind == SCR_DYN_TYPED_REF && v->v.typed_ref.type_key && + !strncmp(v->v.typed_ref.type_key, f->gen_async ? "async-generator<" : "generator<", + f->gen_async ? 16 : 10)) { + f->gen = scr_dyn_typed_ref_unbox(v); + scr_dyn_release(v); + f->iter_kind = f->gen_async ? FLAT_GEN_ASYNC : FLAT_GEN_SYNC; + flat_delegate_step(f); + return; + } + /* yield* val: GetIterator(val, async); a compiled generator is its own + * iterator */ + int kind = 0; + ScrDyn *iter = NULL; + if (v->kind == SCR_DYN_TYPED_REF && v->v.typed_ref.type_key && + (!strncmp(v->v.typed_ref.type_key, "async-generator<", 16) || + !strncmp(v->v.typed_ref.type_key, "generator<", 10))) { + kind = v->v.typed_ref.type_key[0] == 'a' ? 2 : 1; + iter = scr_dyn_retain(v); + } else { + iter = scr_dyn_open_iterator(v, &kind, true); + } + if (!iter && kind == 0 && !scr_exc_pending()) { + /* V8's GetIterator over a value without either method */ + if (v->kind == SCR_DYN_NULL || v->kind == SCR_DYN_UNDEF) { + char m[96]; + int n = snprintf(m, sizeof m, + "Cannot read properties of %s (reading 'Symbol(Symbol.asyncIterator)')", + v->kind == SCR_DYN_NULL ? "null" : "undefined"); + scr_throw_error_msg(SCR_ERR_TYPE, m, (size_t)n); + } else { + static const char m[] = "undefined is not a function"; + scr_throw_error_msg(SCR_ERR_TYPE, m, sizeof m - 1); + } + } + scr_dyn_release(v); + if (!iter) { + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + f->iter = iter; + f->iter_kind = kind == 2 ? FLAT_ASYNC : FLAT_SYNC; flat_delegate_step(f); } @@ -1300,19 +1538,85 @@ static void op_flat_ret_k(void *self, ScrDyn *v) { op_await_value(scr_dyn_undefined(), &op_flat_ret_k, r); return; } - if (r->f->inner) { - scr_dyn_release(r->f->inner); - r->f->inner = NULL; - } + flat_drop_delegate(r->f); gen_close_source(&r->f->g, &r->f->values, NULL); } +/* Whether the delegate's iterator has a callable `name` method. */ +static bool flat_iter_has(OpFlat *f, const char *name) { + if (!f->iter) return false; + ScrDyn *m = flat_get(f->iter, name); + if (!m) { + scr_exc_clear(); + return false; + } + bool callable = scr_dyn_is_callable(m); + scr_dyn_release(m); + return callable; +} + static void flat_ret(OpGen *g, ScrDyn *v) { (void)v; + OpFlat *f = (OpFlat *)g; OpFlatRet *r = op_alloc(sizeof *r); r->base.rc = 1; r->base.drop = &op_flat_ret_drop; - r->f = op_ref((OpFlat *)g); + r->f = op_ref(f); + if (f->iter_kind == FLAT_GEN_ASYNC) { + /* the generator's return(): awaited, then the value */ + ScrPromise *p = ((FromAsyncFn)f->gret->fn)(f->gret, scr_gen_retain(f->gen)); + op_await_gen(p, f->gread, &op_flat_ret_k, r); + scr_promise_release(p); + op_unref(r); + return; + } + if (f->iter_kind == FLAT_GEN_SYNC) { + ScrDyn *res = ((FromSyncFn)f->gret->fn)(f->gret, scr_gen_retain(f->gen)); + if (!res) { + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_exc_take()); + op_unref(r); + return; + } + scr_dyn_release(res); + op_await_value(scr_dyn_undefined(), &op_flat_ret_k, r); + op_unref(r); + return; + } + if (f->iter_kind == FLAT_ASYNC) { + /* yield*'s return: Await(iterator.return(value)), then the value */ + if (flat_iter_has(f, "return")) { + ScrDyn *arg = scr_dyn_undefined(); + ScrDyn *res = flat_call(f->iter, "return", &arg, 1); + if (!res) { + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_exc_take()); + op_unref(r); + return; + } + op_await_value(res, &op_flat_ret_k, r); + scr_dyn_release(res); + } else { + r->stage = 1; + op_await_value(scr_dyn_undefined(), &op_flat_ret_k, r); + } + op_unref(r); + return; + } + if (f->iter_kind == FLAT_SYNC && flat_iter_has(f, "return")) { + /* the async-from-sync iterator's return() calls the sync one */ + ScrDyn *arg = scr_dyn_undefined(); + ScrDyn *res = flat_call(f->iter, "return", &arg, 1); + if (!res) { + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_exc_take()); + op_unref(r); + return; + } + scr_dyn_release(res); + } else if (f->iter_kind == FLAT_STREAM) { + scr_stream_release(scr_stream_iterator_close(f->stream)); + } op_await_value(scr_dyn_undefined(), &op_flat_ret_k, r); op_unref(r); } @@ -1321,12 +1625,72 @@ static void flat_ret(OpGen *g, ScrDyn *v) { * yield* rejects with Node's TypeError, which leaves the for await and * closes map's generator. */ static void flat_thr(OpGen *g, ScrCaught *c) { - (void)c; OpFlat *f = (OpFlat *)g; - if (f->inner) { - scr_dyn_release(f->inner); - f->inner = NULL; + if (f->iter_kind == FLAT_GEN_ASYNC || f->iter_kind == FLAT_GEN_SYNC) { + /* the generator's throw(error) answers the next step */ + ScrDyn *err = scr_caught_to_dyn(c); + if (f->iter_kind == FLAT_GEN_ASYNC) { + ScrPromise *p = ((FromAsyncThrowFn)f->gthr->fn)(f->gthr, scr_gen_retain(f->gen), err); + op_await_gen(p, f->gread, &flat_gen_k, f); + scr_promise_release(p); + return; + } + ScrDyn *v = ((FromSyncThrowFn)f->gthr->fn)(f->gthr, scr_gen_retain(f->gen), err); + if (!v) { + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_exc_take()); + return; + } + if (v == op_k_done) { + scr_dyn_release(v); + flat_drop_delegate(f); + flat_next_value(f); + return; + } + op_await_value(v, &flat_step_k, f); + scr_dyn_release(v); + return; } + if ((f->iter_kind == FLAT_ASYNC || f->iter_kind == FLAT_SYNC) && flat_iter_has(f, "throw")) { + /* yield*'s throw: the delegate's throw(error) answers the next step */ + ScrDyn *err = scr_caught_to_dyn(c); + ScrDyn *res = flat_call(f->iter, "throw", &err, 1); + scr_dyn_release(err); + if (!res) { + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_exc_take()); + return; + } + if (f->iter_kind == FLAT_ASYNC) { + op_await_value(res, &flat_iter_k, f); + scr_dyn_release(res); + return; + } + ScrPromise *p = op_resolved(res); + scr_dyn_release(res); + op_await(p, &flat_iter_k, f); + scr_promise_release(p); + return; + } + if (f->iter_kind == FLAT_STREAM) { + /* the stream's iterator rethrows after destroying the stream */ + ScrDyn *err = scr_caught_to_dyn(c); + ScrError *e = err ? scr_error_from_dyn(err) : NULL; + if (scr_exc_pending()) scr_exc_clear(); + if (err) scr_dyn_release(err); + scr_stream_release(scr_stream_destroy(f->stream, e)); + if (e) scr_error_release(e); + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_caught_retain(c)); + return; + } + if (f->iter_kind != FLAT_NONE && flat_iter_has(f, "return")) { + /* no throw method: the iterator closes first */ + ScrDyn *res = flat_call(f->iter, "return", NULL, 0); + if (res) scr_dyn_release(res); + else scr_exc_clear(); + } + flat_drop_delegate(f); ScrError *e = op_coded_error(SCR_ERR_TYPE, NULL, "The iterator does not provide a 'throw' method."); gen_close_source(g, &f->values, op_take_error(e)); } @@ -1370,12 +1734,6 @@ static void op_from_drop(OpObj *o) { /* The helper ABIs: the closure first, then the declared parameters, which * the callee owns. */ -typedef ScrDyn *(*FromSyncFn)(ScrClosure *, ScrGen *); -typedef ScrDyn *(*FromSyncThrowFn)(ScrClosure *, ScrGen *, ScrDyn *); -typedef ScrPromise *(*FromAsyncFn)(ScrClosure *, ScrGen *); -typedef ScrPromise *(*FromAsyncThrowFn)(ScrClosure *, ScrGen *, ScrDyn *); -typedef ScrDyn *(*FromReadFn)(ScrClosure *, void *); - /* `await p` over an async generator's resume promise: the IteratorResult * record goes through `read` (the value, or the done marker) before k. * Awaiting the typed promise itself keeps from.js's single await. */ @@ -2029,6 +2387,466 @@ ScrStream *scr_stream_from_async_iter(ScrDyn *iter, double flags, double hwm) { return scr_stream_from_dyn_iter(scr_dyn_retain(iter), true, flags, hwm); } +/* ── Duplex.from (duplexify.js) ───────────────────────────────────── + * Duplex.from(asyncGeneratorFunction): fromAsyncGen hands the function a + * source generator that the writes and the final feed through a deferred + * promise, and from(Duplexify, value, { objectMode: true, write, final, + * destroy }) reads what the function yields. The source generator is + * compiled (the frontend's Duplex.from source helper): + * + * while (true) { const v = await next(); ack(); if (v is done) return; yield v; } + * + * next() answers the current deferred, a write or the final settles it + * (the chunk, or the done marker), and ack() is fromAsyncGen's + * `process.nextTick(cb)` followed by a fresh deferred. */ + +enum { CHAN_NONE, CHAN_WRITE, CHAN_FINAL, CHAN_ABORT, CHAN_FINAL_ASYNC }; + +typedef struct OpChan OpChan; +struct OpChan { + OpObj base; + ScrStream *d; /* +1, dropped at destroy (or exit) */ + ScrPromise *promise; /* +1: the deferred next() answers, NULL once taken */ + ScrPromise *resolve; /* +1: the deferred a write or the final settles, NULL once settled */ + int settled; /* CHAN_*: what settled the deferred, for ack() */ + bool aborted; /* destroy ran (fromAsyncGen's ac.abort()) */ + ScrClosure *abort; /* +1: the body's AbortController's abort(), or NULL */ + /* An async function body: its promise's then() (duplexify.js) settles + * `promise` there; the final waits for it. */ + bool finalized; /* final() has started */ + bool body_settled; /* the then() callbacks ran */ + bool final_waiting; /* the final's callback awaits the settlement */ + ScrError *body_err; /* +1: what awaiting the then() result throws, or NULL */ + OpChan *live_prev, *live_next; +}; + +static SCR_TL OpChan *op_chan_live = NULL; + +static void op_chan_drop(OpObj *o) { + OpChan *c = (OpChan *)o; + if (c->body_err) scr_error_release(c->body_err); + if (c->abort) scr_closure_release(c->abort); + if (c->d) scr_stream_release(c->d); + if (c->promise) scr_promise_release(c->promise); + if (c->resolve) scr_promise_release(c->resolve); + free(c); +} + +/* createDeferredPromise() */ +static void op_chan_defer(OpChan *c) { + ScrPromise *p = scr_promise_new(); + if (c->promise) scr_promise_release(c->promise); + if (c->resolve) scr_promise_release(c->resolve); + c->promise = p; + c->resolve = scr_promise_retain(p); +} + +/* Unlinks the channel from the live list and drops its stream and its + * deferred (a source still awaiting it stays pending, as in Node). */ +static void op_chan_retire(OpChan *c) { + if (c->live_prev) c->live_prev->live_next = c->live_next; + else if (op_chan_live == c) op_chan_live = c->live_next; + else return; /* already retired */ + if (c->live_next) c->live_next->live_prev = c->live_prev; + c->live_prev = c->live_next = NULL; + ScrStream *d = c->d; + c->d = NULL; + if (c->abort) scr_closure_release(c->abort); + c->abort = NULL; + if (c->promise) scr_promise_release(c->promise); + if (c->resolve) scr_promise_release(c->resolve); + c->promise = c->resolve = NULL; + if (d) scr_stream_release(d); + op_unref(c); /* the live list's reference */ +} + +static void op_chan_atexit(void) { + while (op_chan_live) op_chan_retire(op_chan_live); +} + +static ScrClosure *op_chan_closure(void *fn, OpChan *c) { + ScrClosure *cl = scr_closure_new(fn, 1); + cl->caps[0] = scr_box_new_obj(&op_ref, &op_unref, NULL); + scr_box_set_ref(cl->caps[0], op_ref(c)); + return cl; +} + +static OpChan *op_chan_of(ScrClosure *cb) { return scr_box_get_ref(cb->caps[0]); /* +1 */ } + +/* process.nextTick(cb): the write's or the final's completion. */ +static void op_chan_done_tick(ScrClosure *job) { + ScrStream *d = scr_box_get_ref(job->caps[0]); + bool final = scr_box_get_bool(job->caps[1]); + if (final) scr_stream_final_done(d, NULL); + else scr_stream_write_done(d, NULL); + scr_stream_release(d); +} + +/* The source's `await _promise`: the current deferred (+1). */ +static ScrPromise *op_chan_next_inv(ScrClosure *cb) { + OpChan *c = op_chan_of(cb); + ScrPromise *p = c->promise; + c->promise = NULL; + op_unref(c); + return p ? p : scr_promise_new(); +} + +static ScrError *op_chan_abort_error(void) { + ScrStr *message = scr_str_new("The operation was aborted", 25); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_str_release(e->name); + e->name = scr_str_new("AbortError", 10); + scr_error_set_code(e, "ABORT_ERR"); + e->name_present = true; + e->name_enumerable = true; + return e; +} + +/* After the source's await: process.nextTick(cb), then (unless done) the + * aborted check and a fresh deferred. */ +static void op_chan_final_async_wait(OpChan *c); + +/* The final's completion after `await promise`: process.nextTick(cb, err) */ +static void op_chan_final_async_done(ScrClosure *job) { + OpChan *c = op_chan_of(job); + if (c->d) { + ScrError *err = c->body_err ? scr_error_retain(c->body_err) : NULL; + ScrStream *d = scr_stream_retain(c->d); + scr_stream_final_done(d, err); + scr_stream_release(d); + } + op_unref(c); +} + +/* `await promise` resumed: the completion follows on the next tick */ +static void op_chan_final_async_resume(ScrClosure *job) { + OpChan *c = op_chan_of(job); + scr_next_tick(op_chan_closure((void *)&op_chan_final_async_done, c)); + op_unref(c); +} + +/* The final's async callback (run by the source's nextTick): it awaits + * the then() result, which may still be pending. */ +static void op_chan_final_async_tick(ScrClosure *job) { + OpChan *c = op_chan_of(job); + op_chan_final_async_wait(c); + op_unref(c); +} + +static void op_chan_final_async_wait(OpChan *c) { + if (c->body_settled) scr_queue_microtask(op_chan_closure((void *)&op_chan_final_async_resume, c)); + else c->final_waiting = true; +} + +static void op_chan_ack_inv(ScrClosure *cb) { + OpChan *c = op_chan_of(cb); + int settled = c->settled; + c->settled = CHAN_NONE; + if (settled == CHAN_FINAL_ASYNC) { + scr_next_tick(op_chan_closure((void *)&op_chan_final_async_tick, c)); + op_unref(c); + return; + } + if (c->d && (settled == CHAN_WRITE || settled == CHAN_FINAL)) { + ScrClosure *tick = scr_closure_new((void *)&op_chan_done_tick, 2); + tick->caps[0] = scr_box_new_obj(&scr_stream_retain_v, &scr_stream_release_v, &scr_stream_trace); + scr_box_set_ref(tick->caps[0], scr_stream_retain(c->d)); + tick->caps[1] = scr_box_new(SCR_BOX_BOOL); + scr_box_set_bool(tick->caps[1], settled == CHAN_FINAL); + scr_next_tick(tick); + } + if (settled != CHAN_FINAL && settled != CHAN_ABORT) { + if (c->aborted) { + ScrError *e = op_chan_abort_error(); + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); + } else if (c->d) { + op_chan_defer(c); + } + } + op_unref(c); +} + +/* `_resolve(value)`: the write's chunk or the final's done marker. */ +static void op_chan_settle(OpChan *c, ScrDyn *v /*moves*/, int kind) { + ScrPromise *p = c->resolve; + c->resolve = NULL; + if (!p) { + scr_dyn_release(v); + return; + } + c->settled = kind; + scr_promise_fulfill_ref(p, v, scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_promise_release(p); +} + +/* Duplexify's write(chunk, encoding, cb) */ +static void op_chan_write_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { + OpChan *c = op_chan_of(cb); + op_chan_settle(c, scr_stream_chunk_value(s, chunk), CHAN_WRITE); + op_unref(c); +} + +/* Duplexify's final(cb) */ +static void op_chan_final_inv(ScrClosure *cb, ScrStream *s) { + (void)s; + OpChan *c = op_chan_of(cb); + op_chan_settle(c, scr_stream_from_done(), CHAN_FINAL); + op_unref(c); +} + +/* fromAsyncGen's ac.abort(err): the body's signal fires its abort + * listeners once (a body that declares the options argument); without an + * error the reason is null (destroy passes `err || null`). */ +static void op_chan_abort(OpChan *c, ScrError *err) { + c->aborted = true; + ScrClosure *abort = c->abort; + if (!abort) return; + c->abort = NULL; + ScrDyn *reason = err ? scr_dyn_from_error(err) : NULL; + if (!reason) reason = scr_dyn_new_null(); + ((void (*)(ScrClosure *, ScrDyn *))abort->fn)(abort, reason); /* reason moves in */ + if (scr_exc_pending()) scr_exc_clear(); + scr_closure_release(abort); +} + +/* destroy: fromAsyncGen's (ac.abort(); a source waiting for a write or + * the final is unblocked with the done marker and a no-op callback; + * cb(err)), then from()'s close of the function's iterator. caps: the + * channel and the from() destroy closure. */ +static void op_dup_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + OpChan *c = scr_box_get_ref(cb->caps[0]); + ScrClosure *from = scr_box_get_ref(cb->caps[1]); + op_chan_abort(c, err); + if (c->resolve) op_chan_settle(c, scr_stream_from_done(), CHAN_ABORT); + op_chan_retire(c); + op_unref(c); + op_from_destroy_inv(from, s, err); + scr_closure_release(from); +} + +typedef ScrGen *(*OpSourceFn)(ScrClosure *, ScrClosure *, ScrClosure *); +typedef ScrGen *(*OpStageFn)(ScrClosure *, ScrGen *); +typedef ScrGen *(*OpStageOptsFn)(ScrClosure *, ScrGen *, ScrDyn *); + +/* A new channel; with a signal (borrowed), the body's AbortController's + * abort() (borrowed) is kept for destroy and *opts is { signal } (+1). */ +static OpChan *op_chan_new(ScrClosure *abort, ScrDyn *signal, ScrDyn **opts) { + op_sentinels_init(); + static SCR_TL bool registered = false; + if (!registered) { + registered = true; + scr_atexit(op_chan_atexit); + } + OpChan *c = op_alloc(sizeof *c); + c->base.rc = 1; + c->base.drop = &op_chan_drop; + *opts = NULL; + if (signal) { + c->abort = scr_closure_retain(abort); + *opts = scr_dyn_new_obj(); + scr_dyn_obj_set(*opts, "signal", 6, scr_dyn_retain(signal)); + } + op_chan_defer(c); + return c; +} + +/* Duplex.from(fn) for an async generator function: the compiled stage + * (borrowed) takes the source generator and answers its own; source + * (borrowed) makes the source generator from next() and ack(); the + * compiler's helpers over the stage's generator are borrowed. With a + * signal, the stage also takes { signal }. +1, or NULL with the + * function's throw pending. */ +static ScrStream *op_duplex_from_gen(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr, + ScrClosure *abort, ScrDyn *signal) { + ScrDyn *opts; + OpChan *c = op_chan_new(abort, signal, &opts); + ScrGen *src = ((OpSourceFn)source->fn)(source, op_chan_closure((void *)&op_chan_next_inv, c), + op_chan_closure((void *)&op_chan_ack_inv, c)); + ScrGen *g = NULL; /* src and opts move in */ + if (scr_exc_pending()) { + if (opts) scr_dyn_release(opts); + } else { + g = opts ? ((OpStageOptsFn)stage->fn)(stage, src, opts) : ((OpStageFn)stage->fn)(stage, src); + } + if (scr_exc_pending()) { + if (g) scr_gen_release(g); + op_unref(c); + return NULL; + } + OpFrom *f = op_alloc(sizeof *f); + f->base.rc = 1; + f->base.drop = &op_from_drop; + f->kind = FROM_ASYNC; + f->gen = g; + f->next = scr_closure_retain(next); + f->read = scr_closure_retain(read); + f->ret = scr_closure_retain(ret); + f->thr = scr_closure_retain(thr); + ScrClosure *destroy = scr_closure_new((void *)&op_dup_destroy_inv, 2); + destroy->caps[0] = scr_box_new_obj(&op_ref, &op_unref, NULL); + scr_box_set_ref(destroy->caps[0], op_ref(c)); + destroy->caps[1] = scr_box_new_obj(&scr_closure_retain_v, &scr_closure_release_v, &scr_closure_trace_v); + scr_box_set_ref(destroy->caps[1], op_from_closure((void *)&op_from_destroy_inv, f)); + /* Duplexify({ objectMode: true, highWaterMark: 1, write, final, destroy }) */ + ScrStream *d = scr_stream_new_duplex(1, 1, true, true, true, true, true, + op_from_closure((void *)&op_from_read_inv, f), &op_from_read_inv, + op_chan_closure((void *)&op_chan_write_inv, c), &op_chan_write_inv, + op_chan_closure((void *)&op_chan_final_inv, c), &op_chan_final_inv, + destroy, &op_dup_destroy_inv); + scr_stream_set_object_mode(d, true, true); + d->cls = "Duplexify"; + f->out = d; + op_unref(f); + c->d = scr_stream_retain(d); + c->live_next = op_chan_live; + if (op_chan_live) op_chan_live->live_prev = c; + op_chan_live = c; /* the reference c was created with */ + return d; +} + +ScrStream *scr_stream_duplex_from_gen(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr) { + return op_duplex_from_gen(stage, source, next, read, ret, thr, NULL, NULL); +} + +ScrStream *scr_stream_duplex_from_gen_signal(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr, + ScrClosure *abort, ScrDyn *signal) { + return op_duplex_from_gen(stage, source, next, read, ret, thr, abort, signal); +} + +/* ── Duplex.from(asyncFunction) ── duplexify.js: the function's promise + * must settle to a nullish value (else ERR_INVALID_RETURN_VALUE when the + * final awaits it), a return before the input was consumed destroys the + * Duplex, a rejection destroys it with the reason, and the Duplexify is + * writable only, its final waiting for the promise. */ + +typedef ScrPromise *(*OpAsyncStageFn)(ScrClosure *, ScrGen *); +typedef ScrPromise *(*OpAsyncStageOptsFn)(ScrClosure *, ScrGen *, ScrDyn *); +typedef ScrDyn *(*OpBoxFn)(ScrClosure *, ScrPromise *); + +typedef struct { + OpChan *c; /* +1 */ + ScrDyn *boxed; /* +1, the function's promise boxed */ +} OpAsyncBody; + +static void op_async_body_entry(ScrFiber *fiber, void *arg) { + (void)fiber; + OpAsyncBody b = *(OpAsyncBody *)arg; + free(arg); + OpChan *c = b.c; + ScrDyn *v = scr_await_ref(b.boxed->v.promise); /* NULL: undefined */ + ScrStream *d = c->d ? scr_stream_retain(c->d) : NULL; + if (!scr_exc_pending()) { + if (v && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_UNDEF) { + /* the then() callback throws: awaiting its result rejects */ + char detail[160]; + const char *got = scr_dyn_specific_type(v, detail, sizeof detail); + char msg[320]; + int n = snprintf(msg, sizeof msg, + "Expected nully to be returned from the \"body\" function but got %s.", got); + c->body_err = op_coded_error(SCR_ERR_TYPE, "ERR_INVALID_RETURN_VALUE", msg); + (void)n; + } else if (!c->finalized && d) { + /* returned without consuming the input: destroyer(d) */ + ScrError *e = op_chan_abort_error(); + scr_stream_release(scr_stream_destroy(d, e)); + scr_error_release(e); + } + if (v) scr_dyn_release(v); + } else { + ScrCaught *caught = scr_exc_take(); + ScrError *err = op_caught_error(caught); + scr_caught_release(caught); + if (d) scr_stream_release(scr_stream_destroy(d, err)); + scr_error_release(err); + } + if (scr_exc_pending()) scr_exc_clear(); + if (d) scr_stream_release(d); + c->body_settled = true; + if (c->final_waiting) { + c->final_waiting = false; + op_chan_final_async_wait(c); + } + op_unref(c); + scr_dyn_release(b.boxed); +} + +/* Duplexify's final(cb) for an async function body: finalized, then + * fromAsyncGen's final with the awaiting callback */ +static void op_chan_final_async_inv(ScrClosure *cb, ScrStream *s) { + (void)s; + OpChan *c = op_chan_of(cb); + c->finalized = true; + op_chan_settle(c, scr_stream_from_done(), CHAN_FINAL_ASYNC); + op_unref(c); +} + +/* fromAsyncGen's destroy alone (no from() iterator to close) */ +static void op_chan_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + OpChan *c = op_chan_of(cb); + op_chan_abort(c, err); + if (c->resolve) op_chan_settle(c, scr_stream_from_done(), CHAN_ABORT); + op_chan_retire(c); + op_unref(c); + scr_stream_destroy_done(s, err ? scr_error_retain(err) : NULL); +} + +/* Duplex.from(fn) for an async function: the compiled stage (borrowed) + * takes the source generator (and, with a signal, { signal }) and answers + * its promise; source and box (promise → dyn promise) borrowed. +1, or + * NULL with the function's throw pending. */ +static ScrStream *op_duplex_from_async_fn(ScrClosure *stage, ScrClosure *source, ScrClosure *box, + ScrClosure *abort, ScrDyn *signal) { + ScrDyn *opts; + OpChan *c = op_chan_new(abort, signal, &opts); + ScrGen *src = ((OpSourceFn)source->fn)(source, op_chan_closure((void *)&op_chan_next_inv, c), + op_chan_closure((void *)&op_chan_ack_inv, c)); + ScrPromise *p = NULL; /* src and opts move in */ + if (scr_exc_pending()) { + if (opts) scr_dyn_release(opts); + } else { + p = opts ? ((OpAsyncStageOptsFn)stage->fn)(stage, src, opts) + : ((OpAsyncStageFn)stage->fn)(stage, src); + } + if (scr_exc_pending()) { + if (p) scr_promise_release(p); + op_unref(c); + return NULL; + } + /* Duplexify({ objectMode: true, readable: false, write, final, destroy }) */ + ScrStream *d = scr_stream_new_duplex(16, 16, true, true, true, false, true, NULL, NULL, + op_chan_closure((void *)&op_chan_write_inv, c), &op_chan_write_inv, + op_chan_closure((void *)&op_chan_final_async_inv, c), + &op_chan_final_async_inv, + op_chan_closure((void *)&op_chan_destroy_inv, c), &op_chan_destroy_inv); + scr_stream_set_object_mode(d, true, true); + d->cls = "Duplexify"; + c->d = scr_stream_retain(d); + c->live_next = op_chan_live; + if (op_chan_live) op_chan_live->live_prev = c; + op_chan_live = c; /* the reference c was created with */ + /* value.then(onFulfilled, onRejected) */ + OpAsyncBody *b = malloc(sizeof *b); + if (!b) op_oom(); + b->c = op_ref(c); + b->boxed = ((OpBoxFn)box->fn)(box, p); /* p moves */ + scr_promise_release(scr_async_spawn_after(b->boxed->v.promise, &op_async_body_entry, b)); + return d; +} + +ScrStream *scr_stream_duplex_from_async_fn(ScrClosure *stage, ScrClosure *source, ScrClosure *box) { + return op_duplex_from_async_fn(stage, source, box, NULL, NULL); +} + +ScrStream *scr_stream_duplex_from_async_fn_signal(ScrClosure *stage, ScrClosure *source, + ScrClosure *box, ScrClosure *abort, + ScrDyn *signal) { + return op_duplex_from_async_fn(stage, source, box, abort, signal); +} + /* ── the promise operators ─────────────────────────────────────────── */ enum { PK_DYN, PK_F64, PK_BOOL, PK_STR, PK_VOID }; @@ -2041,6 +2859,8 @@ typedef struct { ScrDyn *fn; /* +1 or NULL */ ScrDyn *signal; /* +1 or NULL */ ScrDyn *acc; /* toArray's array / reduce's accumulator (+1) */ + ScrClosure *ctl; /* +1: reduce's ac.abort(), run when it ends, or NULL */ + ScrDyn *cb_opts; /* +1: the { signal } the reducer takes as its third argument, or NULL */ int op; int kind; /* the promise payload (PK_*) */ bool has_init, got_any, negate; @@ -2055,9 +2875,21 @@ static void op_fn_drop(OpObj *o) { if (a->fn) scr_dyn_release(a->fn); if (a->signal) scr_dyn_release(a->signal); if (a->acc) scr_dyn_release(a->acc); + if (a->ctl) scr_closure_release(a->ctl); + if (a->cb_opts) scr_dyn_release(a->cb_opts); free(a); } +/* reduce's `finally { ac.abort(); }`, before its promise settles. */ +static void op_fn_abort_ctl(OpFn *a) { + if (!a->ctl) return; + ScrClosure *ctl = a->ctl; + a->ctl = NULL; + ((void (*)(ScrClosure *))ctl->fn)(ctl); + if (scr_exc_pending()) scr_exc_clear(); + scr_closure_release(ctl); +} + static OpFn *op_fn_new(int op, int kind) { OpFn *a = op_alloc(sizeof *a); a->base.rc = 1; @@ -2071,6 +2903,7 @@ static OpFn *op_fn_new(int op, int kind) { /* Settles the operator's promise with v converted to its payload kind (a * value of the wrong kind rejects with the checked conversion's error). */ static void op_fn_resolve(OpFn *a, ScrDyn *v /*borrowed*/) { + op_fn_abort_ctl(a); switch (a->kind) { case PK_VOID: scr_promise_fulfill_void(a->p); @@ -2095,7 +2928,10 @@ static void op_fn_resolve(OpFn *a, ScrDyn *v /*borrowed*/) { scr_promise_reject_pending(a->p); } -static void op_fn_reject(OpFn *a, const ScrCaught *c) { op_reject(a->p, c); } +static void op_fn_reject(OpFn *a, const ScrCaught *c) { + op_fn_abort_ctl(a); + op_reject(a->p, c); +} static void op_fn_next(OpFn *a); @@ -2236,8 +3072,8 @@ static void op_fn_val_k(void *self, ScrDyn *v) { op_fn_next(a); return; } - ScrDyn *args[2] = {a->acc, v}; - ScrDyn *r = scr_dyn_call(a->fn, args, 2, "reducer"); + ScrDyn *args[3] = {a->acc, v, a->cb_opts}; + ScrDyn *r = scr_dyn_call(a->fn, args, a->cb_opts ? 3 : 2, "reducer"); scr_dyn_release(v); if (!r) { op_fn_close(a, scr_exc_take(), NULL); @@ -2319,7 +3155,7 @@ static bool op_map_options(const ScrDyn *options, double *concurrency, double *h /* map(fn, options) with fn already checked: +1 generator, or NULL with * the error pending. */ -static OpMap *op_map_make(ScrStream *s, ScrDyn *fn, ScrDyn *options, int mode) { +static OpMap *op_map_make(ScrStream *s, ScrDyn *fn, ScrDyn *options, int mode, ScrDyn *cb_opts) { op_sentinels_init(); if (!op_check_options(options)) return NULL; bool ok; @@ -2330,26 +3166,28 @@ static OpMap *op_map_make(ScrStream *s, ScrDyn *fn, ScrDyn *options, int mode) { if (signal) scr_dyn_release(signal); return NULL; } - OpMap *m = map_new(s, fn, mode, concurrency, hwm, signal); + OpMap *m = map_new(s, fn, mode, concurrency, hwm, signal, cb_opts); if (signal) scr_dyn_release(signal); return m; } -ScrStream *scr_stream_op_map(ScrStream *s, ScrDyn *fn, ScrDyn *options) { +ScrStream *scr_stream_op_map(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { if (!op_check_fn(fn, "fn", 1)) return NULL; - OpMap *m = op_map_make(s, fn, options, MODE_MAP); + OpMap *m = op_map_make(s, fn, options, MODE_MAP, cb_opts); return m ? op_from_gen(&m->g) : NULL; } -ScrStream *scr_stream_op_filter(ScrStream *s, ScrDyn *fn, ScrDyn *options) { +ScrStream *scr_stream_op_filter(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { if (!op_check_fn(fn, "fn", 1)) return NULL; - OpMap *m = op_map_make(s, fn, options, MODE_FILTER); + OpMap *m = op_map_make(s, fn, options, MODE_FILTER, cb_opts); return m ? op_from_gen(&m->g) : NULL; } -ScrStream *scr_stream_op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options) { +static ScrStream *op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts, + ScrClosure *next, ScrClosure *read, ScrClosure *ret, + ScrClosure *thr, bool async_gen) { if (!op_check_fn(fn, "fn", 1)) return NULL; - OpMap *m = op_map_make(s, fn, options, MODE_MAP); + OpMap *m = op_map_make(s, fn, options, MODE_MAP, cb_opts); if (!m) return NULL; OpFlat *f = op_alloc(sizeof *f); gen_init(&f->g, &op_flat_body_drop); @@ -2357,9 +3195,34 @@ ScrStream *scr_stream_op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options) { f->g.ret = &flat_ret; f->g.thr = &flat_thr; f->values.g = &m->g; /* moves */ + if (next) { + f->gnext = scr_closure_retain(next); + f->gread = read ? scr_closure_retain(read) : NULL; + f->gret = scr_closure_retain(ret); + f->gthr = scr_closure_retain(thr); + f->gen_async = async_gen; + } return op_from_gen(&f->g); } +ScrStream *scr_stream_op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { + return op_flat_map(s, fn, options, cb_opts, NULL, NULL, NULL, NULL, false); +} + +/* flatMap whose callback returns a compiled generator: the helpers over + * its type (borrowed) step the generators it yields from. */ +ScrStream *scr_stream_op_flat_map_async_gen(ScrStream *s, ScrDyn *fn, ScrDyn *options, + ScrDyn *cb_opts, ScrClosure *next, ScrClosure *read, + ScrClosure *ret, ScrClosure *thr) { + return op_flat_map(s, fn, options, cb_opts, next, read, ret, thr, true); +} + +ScrStream *scr_stream_op_flat_map_sync_gen(ScrStream *s, ScrDyn *fn, ScrDyn *options, + ScrDyn *cb_opts, ScrClosure *next, ScrClosure *ret, + ScrClosure *thr) { + return op_flat_map(s, fn, options, cb_opts, next, NULL, ret, thr, false); +} + /* drop(number, options) / take(number, options) */ static ScrStream *op_slice(ScrStream *s, ScrDyn *number, ScrDyn *options, bool take) { op_sentinels_init(); @@ -2444,8 +3307,8 @@ static void op_noop_listener(ScrClosure *cb, void *err) { (void)err; } -ScrPromise *scr_stream_op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, - ScrDyn *options, double kind) { +static ScrPromise *op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, + ScrDyn *options, double kind, ScrClosure *ctl, ScrDyn *cb_opts) { op_sentinels_init(); OpFn *a = op_fn_new(FN_REDUCE, (int)kind); if (!op_check_fn(fn, "reducer", 2)) return op_fn_reject_now(a); @@ -2461,6 +3324,7 @@ ScrPromise *scr_stream_op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool ha ScrDyn *reason = op_get(a->signal, "reason"); ScrError *e = op_abort_error(reason ? reason : scr_dyn_undefined()); if (reason) scr_dyn_release(reason); + /* (thrown before the controller exists) */ /* this.once('error', () => {}) */ ScrStr *name = scr_str_new("error", 5); scr_emitter_release(scr_emitter_on((ScrEmitter *)s, name, @@ -2476,40 +3340,97 @@ ScrPromise *scr_stream_op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool ha op_unref(a); return p; } + /* const ac = new AbortController() (options.signal's listener that + * aborts it too is registered by the caller) */ + if (ctl) { + a->ctl = scr_closure_retain(ctl); + a->cb_opts = cb_opts && cb_opts->kind != SCR_DYN_UNDEF ? scr_dyn_retain(cb_opts) : NULL; + } return op_fn_start(a); } +ScrPromise *scr_stream_op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, + ScrDyn *options, double kind) { + return op_reduce(s, fn, init, has_init, options, kind, NULL, NULL); +} + +/* reduce with a reducer that reads its { signal }: abort is ac.abort() + * (borrowed), cb_opts `{ signal: ac.signal }`. */ +ScrPromise *scr_stream_op_reduce_signal(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, + ScrDyn *options, double kind, ScrClosure *abort, + ScrDyn *cb_opts) { + return op_reduce(s, fn, init, has_init, options, kind, abort, cb_opts); +} + +/* `options?.signal`: +1, undefined when absent. */ +ScrDyn *scr_stream_op_signal_of(ScrDyn *options) { + if (!options || options->kind == SCR_DYN_UNDEF || options->kind == SCR_DYN_NULL) + return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *sig = op_get(options, "signal"); + if (scr_exc_pending()) scr_exc_clear(); + return sig ? sig : scr_dyn_retain(scr_dyn_undefined()); +} + +/* `{ once: true }`: +1. */ +ScrDyn *scr_stream_op_once_options(void) { + ScrDyn *o = scr_dyn_new_obj(); + scr_dyn_obj_set(o, "once", 4, scr_dyn_new_bool(true)); + return o; +} + +/* `[options?.signal].filter(Boolean)` for AbortSignal.any: +1 array. */ +ScrDyn *scr_stream_op_signal_list(ScrDyn *options) { + ScrDyn *list = scr_dyn_new_arr(); + if (options && options->kind != SCR_DYN_UNDEF && options->kind != SCR_DYN_NULL) { + /* A signal that fails validateAbortSignal is left out: the operator + * itself throws Node's ERR_INVALID_ARG_TYPE for it. */ + bool ok; + ScrDyn *sig = op_signal(options, &ok); + if (sig) scr_dyn_arr_push(list, sig); /* moves */ + if (scr_exc_pending()) scr_exc_clear(); + } + return list; +} + +/* `{ signal }`, the callbacks' second argument: +1 object (borrows signal). */ +ScrDyn *scr_stream_op_signal_opts(ScrDyn *signal) { + ScrDyn *o = scr_dyn_new_obj(); + scr_dyn_obj_set(o, "signal", 6, scr_dyn_retain(signal)); + return o; +} + /* some/find/every/forEach: the loop over map's generator (filter's for * some, find and every). */ -static ScrPromise *op_gen_fn(ScrStream *s, ScrDyn *fn, ScrDyn *options, int op, int mode, int kind) { +static ScrPromise *op_gen_fn(ScrStream *s, ScrDyn *fn, ScrDyn *options, int op, int mode, int kind, + ScrDyn *cb_opts) { op_sentinels_init(); OpFn *a = op_fn_new(op, kind); if (!op_check_fn(fn, "fn", 1)) return op_fn_reject_now(a); - OpMap *m = op_map_make(s, fn, options, mode); + OpMap *m = op_map_make(s, fn, options, mode, cb_opts); if (!m) return op_fn_reject_now(a); a->src.g = &m->g; /* moves */ return op_fn_start(a); } -ScrPromise *scr_stream_op_some(ScrStream *s, ScrDyn *fn, ScrDyn *options) { - return op_gen_fn(s, fn, options, FN_SOME, MODE_FILTER, PK_BOOL); +ScrPromise *scr_stream_op_some(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { + return op_gen_fn(s, fn, options, FN_SOME, MODE_FILTER, PK_BOOL, cb_opts); } -ScrPromise *scr_stream_op_find(ScrStream *s, ScrDyn *fn, ScrDyn *options) { - return op_gen_fn(s, fn, options, FN_FIND, MODE_FILTER, PK_DYN); +ScrPromise *scr_stream_op_find(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { + return op_gen_fn(s, fn, options, FN_FIND, MODE_FILTER, PK_DYN, cb_opts); } -ScrPromise *scr_stream_op_for_each(ScrStream *s, ScrDyn *fn, ScrDyn *options) { - return op_gen_fn(s, fn, options, FN_FOR_EACH, MODE_FOREACH, PK_VOID); +ScrPromise *scr_stream_op_for_each(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { + return op_gen_fn(s, fn, options, FN_FOR_EACH, MODE_FOREACH, PK_VOID, cb_opts); } /* every(fn, options) = !(await some.call(this, async (...args) => * !(await fn(...args)), options)) */ -ScrPromise *scr_stream_op_every(ScrStream *s, ScrDyn *fn, ScrDyn *options) { +ScrPromise *scr_stream_op_every(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { op_sentinels_init(); OpFn *outer = op_fn_new(FN_EVERY, PK_BOOL); if (!op_check_fn(fn, "fn", 1)) return op_fn_reject_now(outer); - ScrPromise *some = op_gen_fn(s, fn, options, FN_EVERY, MODE_EVERY, PK_DYN); + ScrPromise *some = op_gen_fn(s, fn, options, FN_EVERY, MODE_EVERY, PK_DYN, cb_opts); ScrPromise *p = scr_promise_retain(outer->p); op_await(some, &op_every_k, outer); scr_promise_release(some); diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 58272cf67..f94554642 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -2571,15 +2571,11 @@ ScrArr *scr_str_split_limit(ScrStr *s, ScrStr *sep, double limit_num) { while (count < 64 && (bytes = scr_str_split_span(s, sep, &cursor, false, &len))) slots[count++] = (uint64_t)(uintptr_t)(s->ascii ? scr_str_from_span_ascii(bytes, len) : scr_str_from_span(bytes, len)); - ScrArr *out = scr_arr_new_inline(SCR_ELEM_STR, count); + ScrArr *out = scr_arr_new_inline(SCR_ELEM_STR, count, false); if (count) { - /* A fresh inline array of exactly `count` slots: store the pieces and - * their states directly. */ - uint8_t *states = scr_arr_states_end(out); - for (size_t k = 0; k < count; k++) { - out->data[k] = slots[k]; - *scr_arr_state_in(states, k) = SCR_ARR_VALUE; - } + /* A fresh inline array of exactly `count` slots: store the pieces + * directly. */ + for (size_t k = 0; k < count; k++) out->data[k] = slots[k]; out->len = (uint32_t)count; out->filled = (uint32_t)count; } diff --git a/packages/runtime/src/scr_worker_events.c b/packages/runtime/src/scr_worker_events.c index a23395c91..3d10183f4 100644 --- a/packages/runtime/src/scr_worker_events.c +++ b/packages/runtime/src/scr_worker_events.c @@ -354,23 +354,17 @@ static bool scr_worker_transfer_has(const ScrDyn *transfer, const ScrWorkerView return false; } -/* Whether a method name is `name`: first characters compare inline, so - * the dispatch ladders below call strcmp only for names that can match. */ -static inline bool method_is(const char *method, const char *name) { - return method[0] == name[0] && strcmp(method, name) == 0; -} - static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)what; ScrWorkerView *view = ptr; - if (method_is(method, "ref") || method_is(method, "unref")) { - view->referenced = method_is(method, "ref"); + if (!strcmp(method, "ref") || !strcmp(method, "unref")) { + view->referenced = !strcmp(method, "ref"); return scr_dyn_retain(scr_dyn_undefined()); } - if (view->port && method_is(method, "hasRef")) + if (view->port && !strcmp(method, "hasRef")) return scr_dyn_new_bool(view->referenced && (!view->closed || (view->channel && view->closing))); - if (!view->port && method_is(method, "terminate")) { + if (!view->port && !strcmp(method, "terminate")) { if (!view->termination) { view->termination = scr_promise_new(); if (view->closed) scr_promise_fulfill_ref(view->termination, @@ -379,7 +373,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho } return scr_dyn_new_promise(view->termination); } - if (method_is(method, "postMessage")) { + if (!strcmp(method, "postMessage")) { /* Worker drops messages after exit before validating or cloning. A * closed MessagePort still validates its payload, as Node does. */ if (!view->port && view->closed) return scr_dyn_retain(scr_dyn_undefined()); @@ -411,9 +405,9 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho else scr_worker_thread_post(view->thread, message); return scr_dyn_retain(scr_dyn_undefined()); } - bool once = method_is(method, "once") || method_is(method, "prependOnceListener"); - bool prepend = method_is(method, "prependListener") || method_is(method, "prependOnceListener"); - if (once || prepend || method_is(method, "on") || method_is(method, "addListener")) { + bool once = !strcmp(method, "once") || !strcmp(method, "prependOnceListener"); + bool prepend = !strcmp(method, "prependListener") || !strcmp(method, "prependOnceListener"); + if (once || prepend || !strcmp(method, "on") || !strcmp(method, "addListener")) { ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined(); scr_dyn_check_listener(listener, "listener"); if (scr_exc_pending()) return NULL; @@ -422,7 +416,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho scr_emitter_release(scr_emitter_on_flex(view->events, args[0]->v.str, listener, once, prepend)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (method_is(method, "off") || method_is(method, "removeListener")) { + if (!strcmp(method, "off") || !strcmp(method, "removeListener")) { ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined(); scr_dyn_check_listener(listener, "listener"); if (scr_exc_pending()) return NULL; @@ -431,21 +425,21 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho scr_emitter_release(scr_emitter_off_dyn(view->events, args[0]->v.str, listener)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (method_is(method, "removeAllListeners")) { + if (!strcmp(method, "removeAllListeners")) { bool all = !argc || args[0]->kind == SCR_DYN_UNDEF; if (!all && args[0]->kind != SCR_DYN_STR) return scr_worker_refusal("Worker event names must be strings"); scr_emitter_release(scr_emitter_remove_all(view->events, all ? NULL : args[0]->v.str, all)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (method_is(method, "listenerCount")) { + if (!strcmp(method, "listenerCount")) { if (!argc || args[0]->kind != SCR_DYN_STR) return scr_worker_refusal("Worker event names must be strings"); return scr_dyn_new_num(argc > 1 && args[1]->kind != SCR_DYN_UNDEF ? scr_emitter_listener_count_dyn(view->events, args[0]->v.str, args[1]) : scr_emitter_listener_count(view->events, args[0]->v.str)); } - if (view->port && method_is(method, "close")) { + if (view->port && !strcmp(method, "close")) { if (argc && args[0]->kind != SCR_DYN_UNDEF) { scr_dyn_check_listener(args[0], "callback"); if (scr_exc_pending()) return NULL; @@ -466,7 +460,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho } return scr_dyn_retain(scr_dyn_undefined()); } - if (view->port && method_is(method, "start")) { + if (view->port && !strcmp(method, "start")) { view->started = true; return scr_dyn_retain(scr_dyn_undefined()); } @@ -1027,7 +1021,7 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)what; ScrWorkerView *view = ptr; - if (method_is(method, "postMessage")) { + if (!strcmp(method, "postMessage")) { if (view->closed) { scr_throw_domex("InvalidStateError", "BroadcastChannel is closed."); return NULL; @@ -1051,7 +1045,7 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, scr_message_free(first); return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); } - if (method_is(method, "close")) { + if (!strcmp(method, "close")) { if (!view->closed) { view->closed = true; scr_broadcast_leave(view->channel); @@ -1059,8 +1053,8 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, } return scr_dyn_retain(scr_dyn_undefined()); } - if (method_is(method, "ref") || method_is(method, "unref")) { - view->referenced = method_is(method, "ref"); + if (!strcmp(method, "ref") || !strcmp(method, "unref")) { + view->referenced = !strcmp(method, "ref"); return scr_dyn_retain(self); } ScrDyn *out = NULL; diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index d943acfb2..6e4ead89a 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -1365,11 +1365,13 @@ static void test_exact_construction(void) { } /* The hole-free prefix bound never covers a non-value state, and the - * operations that keep an array hole-free keep the bound at its length. */ + * operations that keep an array hole-free keep the bound at its length. An + * array without states keeps its elements in dense storage. */ static bool filled_ok(const ScrArr *a) { if (a->filled > a->len || a->filled > a->cap) return false; + if (!scr_arr_has_states(a) && (a->len > a->cap || a->filled != a->len)) return false; for (size_t i = 0; i < a->filled; i++) - if (*scr_arr_state_ptr(a, i) != SCR_ARR_VALUE) return false; + if (scr_arr_dense_state(a, i) != SCR_ARR_VALUE) return false; return true; } @@ -1444,7 +1446,7 @@ static void test_filled_prefix(void) { scr_arr_release(scr_arr_sort_primitive(b, false)); check(filled_ok(b), "sorting moves holes past the bound"); for (size_t i = 0; i < b->len; i++) { - if (i < b->cap && *scr_arr_state_ptr(b, i) != SCR_ARR_VALUE) break; + if (i < b->cap && scr_arr_dense_state(b, i) != SCR_ARR_VALUE) break; check(i < b->filled, "sorted values form the bound"); } while (scr_arr_len(b) > 0) { @@ -1464,11 +1466,13 @@ static void test_filled_prefix(void) { } /* Boolean arrays store one byte per dense element, followed by the states in - * the same allocation. Growth moves the states to the end of the larger area, - * densifying absorbs sparse entries into bytes, and every moving and copying - * operation keeps values, holes and present undefined in place. */ + * the same allocation once they have states. Growth moves the states to the + * end of the larger area, densifying absorbs sparse entries into bytes, and + * every moving and copying operation keeps values, holes and present + * undefined in place. */ static bool bool_layout_ok(const ScrArr *a) { - return a->cap == 0 || scr_arr_states_end(a) == (const uint8_t *)a->data + 2 * (size_t)a->cap; + return a->cap == 0 || !scr_arr_has_states(a) || + scr_arr_states_end(a) == (const uint8_t *)a->data + 2 * (size_t)a->cap; } static bool bool_states(const ScrArr *a, const char *want) { @@ -1584,7 +1588,7 @@ static void test_inline_slots(void) { * block; growth past them moves them out like any inline array. */ ScrArr *known = scr_arr_new(SCR_ELEM_F64, 4); check(inline_slots(known) && known->cap >= 4 && known->len == 0 && !scr_arr_has(known, 0) && - scr_arr_states_end(known) == (uint8_t *)(known->data + known->cap) + known->cap, + !scr_arr_has_states(known), "a known-count construction places its slots in the array block"); size_t known_cap = known->cap; for (size_t i = 0; i <= known_cap; i++) scr_arr_push_f64(known, (double)i); @@ -1605,26 +1609,25 @@ static void test_inline_slots(void) { "known-count boolean constructions are inline too"); scr_arr_release(known_flags); ScrArr *a = scr_arr_new_small(SCR_ELEM_F64); - check(inline_slots(a) && a->cap >= 4 && a->len == 0 && - scr_arr_states_end(a) == (uint8_t *)(a->data + a->cap) + a->cap, - "small construction places slots and states in the array block"); + check(inline_slots(a) && a->cap >= 4 && a->len == 0 && !scr_arr_has_states(a), + "small construction places its slots in the array block, without states"); size_t inline_cap = a->cap; for (size_t i = 0; i < inline_cap; i++) scr_arr_push_f64(a, (double)i); check(inline_slots(a) && a->filled == inline_cap, "appends within the inline capacity stay inline"); scr_arr_push_f64(a, 100); check(!inline_slots(a) && a->cap > inline_cap && scr_arr_get_f64(a, 0) == 0 && scr_arr_get_f64(a, (double)inline_cap - 1) == (double)inline_cap - 1 && - scr_arr_get_f64(a, (double)inline_cap) == 100 && - scr_arr_states_end(a) == (uint8_t *)(a->data + a->cap) + a->cap, + scr_arr_get_f64(a, (double)inline_cap) == 100 && !scr_arr_has_states(a), "growth moves inline slots to their own allocation"); scr_arr_release(a); ScrArr *flags = scr_arr_new_small(SCR_ELEM_BOOL); - check(inline_slots(flags) && scr_arr_states_end(flags) == (uint8_t *)flags->data + 2 * (size_t)flags->cap, + check(inline_slots(flags) && flags->cap >= 4 && !scr_arr_has_states(flags), "boolean inline slots are one byte each"); scr_arr_push_bool(flags, true); scr_arr_set_bool(flags, 3, true); - check(scr_arr_get_bool(flags, 0) && !scr_arr_has(flags, 1) && scr_arr_get_bool(flags, 3), + check(scr_arr_get_bool(flags, 0) && !scr_arr_has(flags, 1) && scr_arr_get_bool(flags, 3) && + scr_arr_has_states(flags) && bool_layout_ok(flags), "boolean inline slots keep holes"); scr_arr_release(flags); @@ -2107,8 +2110,8 @@ static void test_head_room(void) { check_f64(scr_arr_shift_f64(q), 0, "shift answers the first element"); check(q->data == block + 1 && q->cap == cap - 1 && q->len == 63, "shift advances the storage instead of moving the tail"); - check(scr_arr_states_end(q) == (uint8_t *)(block + cap) + cap - 1 && head_room(q) && q->data[-1] == 1, - "the remaining states stay at the end of the allocation"); + check(!scr_arr_has_states(q) && head_room(q) && q->data[-1] == 1, + "shift records the vacated slot in front of the data"); check(q->filled == 63, "shift keeps the hole-free prefix"); for (int i = 1; i < 40; i++) scr_arr_shift_f64(q); check(q->data == block + 40 && q->len == 24 && scr_arr_get_f64(q, 0) == 40 && @@ -2223,6 +2226,159 @@ static void test_head_room(void) { #endif } +/* SCR_ARR_STATES (scr_array.h): an array starts without state bytes and + * keeps none while every index below its length is a present value. A + * hole, a present undefined or a length past the elements gives it states, + * with the elements in place. */ +static ScrStr *str_of(const char *text) { return scr_str_new(text, strlen(text)); } + +static bool strs_are(ScrArr *a, const char *want) { + /* want: one character per element, '_' a hole, 'u' undefined. */ + if (scr_arr_len(a) != (double)strlen(want)) return false; + for (size_t i = 0; want[i]; i++) { + double state = scr_arr_state(a, (double)i); + if (want[i] == '_') { + if (state != SCR_ARR_HOLE) return false; + } else if (want[i] == 'u') { + if (state != SCR_ARR_UNDEFINED) return false; + } else { + if (state != SCR_ARR_VALUE) return false; + ScrStr *got = scr_arr_get_ref(a, (double)i); + bool same = got->len == 1 && got->data[0] == want[i]; + scr_str_release(got); + if (!same) return false; + } + } + return filled_ok(a); +} + +static ScrArr *strs_of(const char *text, bool small) { + ScrArr *a = small ? scr_arr_new_small(SCR_ELEM_STR) : scr_arr_new(SCR_ELEM_STR, 0); + for (size_t i = 0; text[i]; i++) scr_arr_push_ref(a, scr_str_new(text + i, 1)); + return a; +} + +static void test_states_on_demand(void) { +#ifdef SCR_RC_AUDIT + long strings0 = scr_str_live_count(); + long arrays0 = scr_arr_live_count(); +#endif + ScrArr *a = strs_of("abcdef", false); + scr_arr_set_ref(a, 1, str_of("B")); + scr_arr_set_ref(a, 6, str_of("g")); + scr_str_release(scr_arr_pop_ref(a)); + scr_arr_set_len(a, 5); + check(!scr_arr_has_states(a) && a->filled == 5 && strs_are(a, "aBcde"), + "pushes, writes below the length, appends, pops and truncation keep no states"); + scr_arr_set_undefined(a, 2); + check(scr_arr_has_states(a) && strs_are(a, "aBude"), "a present undefined gives the array states"); + scr_arr_release(a); + + ScrArr *gap = strs_of("ab", true); + scr_arr_set_ref(gap, 3, str_of("d")); + check(scr_arr_has_states(gap) && strs_are(gap, "ab_d"), "a write past the length leaves holes"); + scr_arr_release(gap); + + ScrArr *longer = strs_of("abc", true); + scr_arr_set_len(longer, 5); + check(scr_arr_has_states(longer) && strs_are(longer, "abc__"), "a longer length adds holes"); + scr_arr_release(longer); + + /* `new Array(n)` and a length set on an empty small array: the inline + * slots give way to one allocation holding the slots and their states. */ + ScrArr *sized = scr_arr_new_small(SCR_ELEM_STR); + scr_arr_set_len(sized, 6); + scr_arr_set_ref(sized, 4, str_of("e")); + check(scr_arr_has_states(sized) && !(sized->spare & SCR_ARR_INLINE_SLOTS) && sized->cap == 6 && + strs_are(sized, "____e_") && filled_ok(sized), + "a length set on an empty inline array reserves that many slots with states"); + scr_arr_release(sized); + + ScrArr *deleted = strs_of("abcd", false); + scr_arr_delete(deleted, 1); + check(scr_arr_has_states(deleted) && strs_are(deleted, "a_cd"), "delete leaves a hole"); + scr_arr_release(deleted); + + /* Conversion keeps out-of-line storage and head room's elements. */ + ScrArr *props = strs_of("abc", true); + scr_arr_set_ref(props, -1, str_of("p")); + scr_arr_set_undefined(props, 3); + ScrStr *prop = scr_arr_get_ref(props, -1); + check(scr_arr_has_states(props) && strs_are(props, "abcu") && prop->data[0] == 'p', + "states keep a noncanonical property"); + scr_str_release(prop); + scr_arr_release(props); + ScrArr *q = strs_of("abcdefghijklmnopqrst", false); + for (int i = 0; i < 3; i++) scr_str_release(scr_arr_shift_ref(q)); + check(!scr_arr_has_states(q) && head_room(q), "shift keeps no states"); + scr_arr_set_undefined(q, 1); + check(scr_arr_has_states(q) && !head_room(q) && strs_are(q, "dufghijklmnopqrst"), + "states move the elements of head room to the front"); + scr_arr_release(q); + + /* Copies keep no states for values, and states for holes. */ + ScrArr *src = strs_of("abcde", false); + ScrArr *part = scr_arr_slice(src, 1, 4); + ScrArr *joined = scr_arr_slice(src, 0, INFINITY); + scr_arr_concat_copy(joined, src); + ScrArr *spliced = scr_arr_splice(joined, 2, 3); + ScrArr *reversed = scr_arr_to_reversed(src); + scr_arr_release(scr_arr_reverse(src)); + scr_arr_release(scr_arr_sort_primitive(src, false)); + check(!scr_arr_has_states(part) && strs_are(part, "bcd"), "a slice of values keeps no states"); + check(!scr_arr_has_states(joined) && strs_are(joined, "ababcde"), + "concat and splice of values keep no states"); + check(!scr_arr_has_states(spliced) && strs_are(spliced, "cde"), "removed values keep no states"); + check(!scr_arr_has_states(reversed) && strs_are(reversed, "edcba"), "toReversed of values keeps no states"); + check(!scr_arr_has_states(src) && strs_are(src, "abcde"), "reverse and sort of values keep no states"); + scr_arr_release(src); + scr_arr_release(part); + scr_arr_release(joined); + scr_arr_release(spliced); + scr_arr_release(reversed); + ScrArr *holey = strs_of("ab", false); + scr_arr_set_ref(holey, 3, str_of("d")); + ScrArr *spread = strs_of("x", true); + scr_arr_push_spread(spread, holey); + ScrArr *concat = strs_of("x", true); + scr_arr_concat_copy(concat, holey); + ScrArr *values = scr_arr_slice(holey, 0, 2); + ScrArr *front = strs_of("yz", false); + scr_arr_unshift_spread(front, holey); + check(scr_arr_has_states(spread) && strs_are(spread, "xabud") && scr_arr_has_states(concat) && + strs_are(concat, "xab_d") && !scr_arr_has_states(values) && strs_are(values, "ab") && + scr_arr_has_states(front) && strs_are(front, "abudyz"), + "copies of holes keep holes or present undefined"); + scr_arr_release(holey); + scr_arr_release(spread); + scr_arr_release(concat); + scr_arr_release(values); + scr_arr_release(front); + + /* Numbers and booleans take the same paths without references. */ + ScrArr *nums = scr_arr_new(SCR_ELEM_F64, 3); + for (int i = 0; i < 40; i++) scr_arr_push_f64(nums, i); + scr_arr_release(scr_arr_fill_f64(nums, 7, 30, INFINITY)); + check(!scr_arr_has_states(nums) && scr_arr_get_number(nums, 39) == 7 && scr_arr_get_number(nums, 40) != 0, + "number fills keep no states"); + scr_arr_release(scr_arr_fill_undefined(nums, 0, 1)); + check(scr_arr_has_states(nums) && scr_arr_state(nums, 0) == SCR_ARR_UNDEFINED && + scr_arr_get_number(nums, 1) == 1 && scr_arr_get_number(nums, 39) == 7 && filled_ok(nums), + "an undefined fill gives a number array states"); + scr_arr_release(nums); + ScrArr *bits = scr_arr_new_small(SCR_ELEM_BOOL); + for (int i = 0; i < 6; i++) scr_arr_push_bool(bits, i % 2 == 0); + scr_arr_set_undefined(bits, 6); + check(scr_arr_has_states(bits) && bool_layout_ok(bits) && scr_arr_get_bool(bits, 4) && + !scr_arr_get_bool(bits, 5) && scr_arr_state(bits, 6) == SCR_ARR_UNDEFINED, + "boolean arrays get one state byte per element"); + scr_arr_release(bits); +#ifdef SCR_RC_AUDIT + check(scr_str_live_count() == strings0, "states on demand: no strings leaked"); + check(scr_arr_live_count() == arrays0, "states on demand: no arrays leaked"); +#endif +} + int main(int argc, char **argv) { if (argc > 1) { ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0); @@ -2279,6 +2435,7 @@ int main(int argc, char **argv) { test_bool_bytes(); test_inline_slots(); test_head_room(); + test_states_on_demand(); fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total); return failed == 0 ? 0 : 1; diff --git a/scripts/gen-dyn-methods.mjs b/scripts/gen-dyn-methods.mjs deleted file mode 100644 index fe4f81a90..000000000 --- a/scripts/gen-dyn-methods.mjs +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env node -/* Writes packages/runtime/src/scr_dyn_methods.h: the method-name ids the - * checked-dynamic dispatch in scr_dyn_invoke.c compares (DYN_M_), the - * names, and an open-addressed hash table mapping a name to its id. The ids - * are the DYN_M_ names scr_dyn_invoke.c uses, so the header follows the - * dispatch code. - * - * node scripts/gen-dyn-methods.mjs write the header - * node scripts/gen-dyn-methods.mjs --check fail when it is out of date */ -import { readFileSync, writeFileSync } from "node:fs"; - -const source = new URL("../packages/runtime/src/scr_dyn_invoke.c", import.meta.url); -const target = new URL("../packages/runtime/src/scr_dyn_methods.h", import.meta.url); -const SLOTS = 256; - -const used = new Set(); -for (const match of readFileSync(source, "utf8").matchAll(/\bDYN_M_([A-Za-z][A-Za-z0-9]*)\b/g)) - if (match[1] !== "NONE" && match[1] !== "COUNT") used.add(match[1]); -const names = [...used].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); -if (names.length >= SLOTS / 2) throw new Error("too many method names for the hash table"); - -// The slot dyn_method_of in the header computes. -const slotOf = (name) => - (name.length * 7 + name.charCodeAt(0) * 31 + name.charCodeAt(name.length - 1)) & (SLOTS - 1); -const slots = new Array(SLOTS).fill(0); -names.forEach((name, i) => { - let slot = slotOf(name); - while (slots[slot] !== 0) slot = (slot + 1) & (SLOTS - 1); - slots[slot] = i + 1; -}); - -const rows = (values, width) => { - const out = []; - for (let i = 0; i < values.length; i += width) - out.push(` ${values.slice(i, i + width).join(", ")},`); - return out; -}; -const text = [ - "/* Generated by scripts/gen-dyn-methods.mjs from the DYN_M_ ids scr_dyn_invoke.c", - " * uses. Do not edit by hand. */", - "#ifndef SCR_DYN_METHODS_H", - "#define SCR_DYN_METHODS_H", - "", - "#include ", - "#include ", - "", - "enum {", - " DYN_M_NONE,", - ...names.map((name) => ` DYN_M_${name},`), - " DYN_M_COUNT", - "};", - "", - "static const char *const dyn_method_names[DYN_M_COUNT] = {", - " NULL,", - ...names.map((name) => ` ${JSON.stringify(name)},`), - "};", - "", - "static const unsigned char dyn_method_lengths[DYN_M_COUNT] = {", - ...rows([0, ...names.map((name) => name.length)], 16), - "};", - "", - `/* Each name's id at its slot (or the next free one); 0 ends a probe. */`, - `static const unsigned char dyn_method_slots[${SLOTS}] = {`, - ...rows(slots, 16), - "};", - "", - `#define DYN_METHOD_MAX_LENGTH ${Math.max(...names.map((name) => name.length))}`, - "", - "/* The id of a method name, compared by content; DYN_M_NONE for any other", - " * name. */", - "static inline int dyn_method_of(const char *m) {", - " size_t length = strlen(m);", - " if (length == 0 || length > DYN_METHOD_MAX_LENGTH) return DYN_M_NONE;", - " unsigned slot = (unsigned)(length * 7 + (unsigned char)m[0] * 31 +", - ` (unsigned char)m[length - 1]) & ${SLOTS - 1};`, - " for (;;) {", - " unsigned id = dyn_method_slots[slot];", - " if (id == 0) return DYN_M_NONE;", - " if (dyn_method_lengths[id] == length && memcmp(m, dyn_method_names[id], length) == 0) return (int)id;", - ` slot = (slot + 1) & ${SLOTS - 1};`, - " }", - "}", - "", - "#endif", - "", -].join("\n"); - -if (process.argv.includes("--check")) { - let current = ""; - try { - current = readFileSync(target, "utf8"); - } catch {} - if (current !== text) { - console.error( - "packages/runtime/src/scr_dyn_methods.h is out of date: run node scripts/gen-dyn-methods.mjs", - ); - process.exit(1); - } -} else { - writeFileSync(target, text); -} diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index 160e33438..3158de997 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -394,8 +394,7 @@ export function dynamicTierSymbols(names) { * not yet traced to its typed root. bench/thread-slots, * bench/channel-messages and bench/worker-messages, added later, link them * through worker messages: - * node:worker_threads values are dyn objects. bench/dynamic-methods is - * untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and + * node:worker_threads values are dyn objects. Every other bench workload is fully typed and must link none. The gate and * tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also * fails when a listed workload stops linking them, so remove a workload here * when a leak fix makes its binary clean. */ @@ -409,7 +408,6 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/intern-table", "bench/thread-slots", "bench/channel-messages", - "bench/dynamic-methods", "bench/worker-messages", "bench/typed-array-scan-workers", "bench/log-summary", diff --git a/tests/corpus/6913-builtin-constructor-shorthand.ts b/tests/corpus/6913-builtin-constructor-shorthand.ts new file mode 100644 index 000000000..7fb5066ed --- /dev/null +++ b/tests/corpus/6913-builtin-constructor-shorthand.ts @@ -0,0 +1,10 @@ +// Builtin constructors in shorthand properties are the constructors themselves. +const direct: Record = { Date: Date, Map: Map, Set: Set }; +const shorthand: Record = { Date, Map, Set, Object, Promise, WeakMap }; +console.log(shorthand.Date === Date, shorthand.Map === direct.Map, shorthand.Set === direct.Set); +console.log(shorthand.Object === Object, shorthand.Promise === Promise, shorthand.WeakMap === WeakMap); +console.log(typeof shorthand.Date, typeof shorthand.Map); +function pick(table: Record, name: string): unknown { + return table[name]; +} +console.log(pick({ Map, Set }, "Set") === Set, pick({ Date }, "Date") === pick(direct, "Date")); diff --git a/tests/corpus/6914-dynamic-builtin-construct.ts b/tests/corpus/6914-dynamic-builtin-construct.ts new file mode 100644 index 000000000..c026a6691 --- /dev/null +++ b/tests/corpus/6914-dynamic-builtin-construct.ts @@ -0,0 +1,54 @@ +// `new` through values that hold builtin constructors. +const ctors: Record = { Date, Map, Set, Object, WeakMap, WeakSet }; +function construct(name: string, ...args: unknown[]): unknown { + const C = ctors[name] as any; + return new C(...args); +} +function make(C: any, ...args: unknown[]): unknown { + return new C(...args); +} +const tag = (value: unknown) => Object.prototype.toString.call(value); + +const d = construct("Date", 0) as Date; +console.log(tag(d), d.getTime(), d.toISOString()); +const d2 = make(Date, 2020, 1, 3, 4, 5, 6, 7) as Date; +console.log(d2.getFullYear(), d2.getMonth(), d2.getDate(), d2.getHours(), d2.getMilliseconds()); +const d3 = make(Date, "2001-02-03T04:05:06Z") as Date; +const d4 = make(Date, d3) as Date; +console.log(d3.getTime(), d4.getTime()); +console.log(typeof make(Date), Number.isNaN((make(Date, "nope") as Date).getTime())); + +const m = construct("Map", [["a", 1], ["b", 2]]) as Map; +console.log(tag(m), m.size, m.get("b"), [...m.keys()].join()); +const m2 = make(Map, m) as Map; +m2.set("c", 3); +console.log(m.size, m2.size, (make(Map) as Map).size, (make(Map, null) as Map).size); + +const s = construct("Set", [1, 2, 2, 3]) as Set; +console.log(tag(s), s.size, s.has(3), (make(Set, "abca") as Set).size); + +const o = construct("Object") as Record; +o.x = 1; +const same = { y: 2 }; +console.log(tag(o), JSON.stringify(o), make(Object, same) === same, tag(make(Object, null))); + +const key = {}; +const w = construct("WeakMap", [[key, "v"]]) as WeakMap; +console.log(tag(w), w.get(key), w.has({})); +const ws = make(WeakSet, [key]) as WeakSet; +console.log(tag(ws), ws.has(key)); + +try { + make(Map, [1]); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} +try { + make(WeakMap, [[1, 2]]); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} + +const DateSlot: new () => unknown = Date; +const MapSlot: new (entries: [string, number][]) => unknown = Map; +console.log(tag(new DateSlot()), (new MapSlot([["k", 1]]) as Map).get("k")); diff --git a/tests/corpus/6915-dynamic-builtin-construct.mjs b/tests/corpus/6915-dynamic-builtin-construct.mjs new file mode 100644 index 000000000..dd13aa325 --- /dev/null +++ b/tests/corpus/6915-dynamic-builtin-construct.mjs @@ -0,0 +1,16 @@ +// `new` through stored builtin constructors in JavaScript. +const table = [Date, Map, Set, Object, WeakMap, WeakSet]; +for (const C of table) { + const value = new C(); + console.log(C.name, typeof value, Object.prototype.toString.call(value), value.constructor === C); +} +const Ctor = table[0]; +console.log(new Ctor(86400000).toISOString(), new Ctor(2024, 0, 31).getDate()); +const MapCtor = table[1]; +const map = new MapCtor(Object.entries({ a: 1, b: 2 })); +map.set("c", 3); +console.log(map.size, map.get("a"), [...map.entries()].length); +const SetCtor = table[2]; +console.log(new SetCtor([...map.keys()]).size, new SetCtor([NaN, NaN, 0, -0]).size); +const named = { Date, Map, Set }; +console.log(named.Date === Date, named.Map === table[1], named.Set === table[2]); diff --git a/tests/corpus/6916-local-class-static-self-reference.mjs b/tests/corpus/6916-local-class-static-self-reference.mjs new file mode 100644 index 000000000..bc19cd0e9 --- /dev/null +++ b/tests/corpus/6916-local-class-static-self-reference.mjs @@ -0,0 +1,52 @@ +// Static fields, static blocks and static methods of function-local classes +// that read the class by its name while the class is being defined. +function registry(prefix) { + class Registry { + static label = Registry.name + ":" + prefix; + static instances = []; + static { + Registry.ready = Registry.instances.length === 0; + } + constructor(id) { + this.id = id; + Registry.instances.push(this); + } + static create(id) { + return new Registry(id); + } + } + return Registry; +} +const R = registry("main"); +R.create(1); +R.create(2); +console.log(R.label, R.ready, R.instances.map((r) => r.id)); +const R2 = registry("other"); +console.log(R2.label, R2.instances.length, R2 === R, R.instances.length); + +function counter(step) { + class Counter { + static zero = new Counter(0); + static one = Counter.zero.next(); + constructor(value) { + this.value = value; + } + next() { + return new Counter(this.value + step); + } + } + return Counter; +} +const C = counter(5); +console.log(C.zero.value, C.one.value, C.one.next().value, C.one instanceof C); + +function described() { + class Shape { + static kinds = ["circle", "square"]; + static describe() { + return Shape.kinds.map((kind) => kind + "@" + Shape.name).join(","); + } + } + return Shape.describe(); +} +console.log(described()); diff --git a/tests/corpus/6917-local-class-expression-self-reference.ts b/tests/corpus/6917-local-class-expression-self-reference.ts new file mode 100644 index 000000000..cff612d29 --- /dev/null +++ b/tests/corpus/6917-local-class-expression-self-reference.ts @@ -0,0 +1,53 @@ +// Function-local class expressions that refer to themselves by their own name. +function counter(start: number) { + const C = class Counter { + value: number; + constructor(value: number) { this.value = value; } + next(): Counter { return new Counter(this.value + 1); } + static first(): Counter { return new Counter(start); } + static isSelf(value: unknown): boolean { return value === Counter; } + }; + return C; +} +const Counter1 = counter(10); +console.log(Counter1.first().next().next().value); +console.log(Counter1.isSelf(Counter1), Counter1.isSelf(counter(1)), Counter1.name); + +function box(seed: T) { + return class Box { + value: T; + constructor(value: T) { this.value = value; } + again(): Box { return new Box(this.value); } + static of(): Box { return new Box(seed); } + }; +} +const StringBox = box("seed"); +console.log(new StringBox("x").again().value, StringBox.of().value); +const NumberBox = box(7); +console.log(NumberBox.of().again().value + 1); + +function nested(n: number) { + const Outer = class Tree { + children: Tree[] = []; + static build(depth: number): Tree { + const tree = new Tree(); + if (depth > 0) tree.children.push(Tree.build(depth - 1), Tree.build(depth - 1)); + return tree; + } + size(): number { return 1 + this.children.reduce((total, child) => total + child.size(), 0); } + }; + return Outer.build(n).size(); +} +console.log(nested(3)); + +// The only reference to the name is a shorthand property. +function registry(tag: string) { + return class Entry { + readonly tag = tag; + static table(): Record { + return { Entry }; + } + }; +} +const Entry1 = registry("e"); +console.log(Entry1.table().Entry === Entry1, new Entry1().tag); diff --git a/tests/corpus/6918-local-class-expression-self-reference.mjs b/tests/corpus/6918-local-class-expression-self-reference.mjs new file mode 100644 index 000000000..4b2440674 --- /dev/null +++ b/tests/corpus/6918-local-class-expression-self-reference.mjs @@ -0,0 +1,61 @@ +// Function-local class expressions in JavaScript whose static members and +// methods read the class by its own name. +function point(scale) { + const P = class Point { + static origin = new Point(0, 0); + static unit = Point.scaled(1); + constructor(x, y) { + this.x = x; + this.y = y; + } + static scaled(n) { + return new Point(n * scale, n * scale); + } + plus(other) { + return new Point(this.x + other.x, this.y + other.y); + } + }; + return P; +} +const P = point(3); +const sum = P.origin.plus(P.unit).plus(P.unit); +console.log(sum.x, sum.y, P.unit instanceof P, P.name); + +function selfName() { + const K = class Named { + static who() { + return Named === this; + } + }; + return [K.who(), K.name, typeof K]; +} +console.log(selfName()); + +function factory(tag) { + return class Tagged { + static count = 0; + constructor() { + Tagged.count++; + } + static make() { + return new Tagged(); + } + get tag() { + return tag + Tagged.count; + } + }; +} +const T = factory("t"); +T.make(); +console.log(T.make().tag, T.count, factory("u").count); + +// The only reference to the name is a shorthand property. +function packed() { + const K = class Packed { + static wrap() { + return { Packed }; + } + }; + return [K.wrap().Packed === K, K.wrap().Packed.name]; +} +console.log(packed()); diff --git a/tests/corpus/6919-local-class-inspect-name.mjs b/tests/corpus/6919-local-class-inspect-name.mjs new file mode 100644 index 000000000..022895591 --- /dev/null +++ b/tests/corpus/6919-local-class-inspect-name.mjs @@ -0,0 +1,51 @@ +// console.log prints instances of function-local classes with prototype +// methods under their class names. +function point() { + class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } + plus(other) { + return new Point(this.x + other.x, this.y + other.y); + } + } + return Point; +} +const Point = point(); +const a = new Point(1, 2); +console.log(a, a.plus(a)); +console.log([new Point(0, 0)], { p: new Point(5, 6) }); + +function animals() { + class Animal { + constructor(name) { + this.name = name; + } + speak() { + return this.name + " makes a sound"; + } + } + class Dog extends Animal { + speak() { + return this.name + " barks"; + } + } + return [new Animal("cat"), new Dog("rex")]; +} +const [cat, dog] = animals(); +console.log(cat, dog); + +function anonymous() { + const Named = class { + value() { + return 1; + } + }; + return [new Named(), new (class { + value() { + return 2; + } + })()]; +} +console.log(anonymous()); diff --git a/tests/corpus/6921-local-subclass-override-dispatch.ts b/tests/corpus/6921-local-subclass-override-dispatch.ts new file mode 100644 index 000000000..0476e56c7 --- /dev/null +++ b/tests/corpus/6921-local-subclass-override-dispatch.ts @@ -0,0 +1,51 @@ +// Overrides declared by function-local subclasses dispatch on the receiver +// when the call site lowers before the function that declares them. +class Shape { + area(): number { return 0; } + get label(): string { return "shape"; } + describe(): string { return this.label + ":" + this.area(); } +} + +function square(side: number): Shape { + class Square extends Shape { + area(): number { return side * side; } + get label(): string { return "square"; } + } + return new Square(); +} + +function scaled(factor: number): Shape { + class Scaled extends Shape { + area(): number { return factor; } + } + class Doubled extends Scaled { + area(): number { return super.area() * 2; } + } + return new Doubled(); +} + +const shapes: Shape[] = [new Shape(), square(3), scaled(5)]; +for (const shape of shapes) console.log(shape.area(), shape.label, shape.describe()); + +function pair() { + class Base { + name(): string { return "base"; } + } + class Derived extends Base { + name(): string { return "derived"; } + } + return [new Base(), new Derived()]; +} +const [first, second] = pair(); +console.log(first.name(), second.name()); + +function counter() { + const Tally = class { + count(): number { return 1; } + }; + const Double = class extends Tally { + count(): number { return 2; } + }; + return new Double(); +} +console.log(counter().count()); diff --git a/tests/corpus/6922-local-subclass-override-dispatch.mjs b/tests/corpus/6922-local-subclass-override-dispatch.mjs new file mode 100644 index 000000000..8439c563d --- /dev/null +++ b/tests/corpus/6922-local-subclass-override-dispatch.mjs @@ -0,0 +1,51 @@ +// Overrides declared by function-local subclasses in JavaScript dispatch on +// the receiver when the call site lowers before the declaring function. +class Animal { + constructor(name) { + this.name = name; + } + speak() { + return this.name + " makes a sound"; + } +} + +function dog(name) { + class Dog extends Animal { + speak() { + return this.name + " barks"; + } + } + return new Dog(name); +} + +function animals() { + class Bird extends Animal { + speak() { + return this.name + " sings"; + } + } + return [new Animal("cat"), new Bird("robin"), dog("rex")]; +} + +for (const animal of animals()) console.log(animal.speak()); +const speak = (animal) => animal.speak(); +console.log(speak(dog("fido"))); + +function pets() { + class Pet { + constructor(name) { + this.name = name; + } + greet() { + return this.name + " waves"; + } + } + class Cat extends Pet { + greet() { + return this.name + " purrs"; + } + } + return [new Pet("ann"), new Cat("tom")]; +} +const [pet, cat] = pets(); +console.log(pet.greet(), cat.greet()); diff --git a/tests/corpus/7014-js-never-array-literal-fields/main.ts b/tests/corpus/7014-js-never-array-literal-fields/main.ts new file mode 100644 index 000000000..cf6d235c0 --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/main.ts @@ -0,0 +1,10 @@ +// A JavaScript package whose object literals start an array property as +// `[]` inside a literal that maps to a record, as zod 3's parse context does +// (`{ common: { issues: [] }, path: params?.path || [] }`), and later push +// records onto it. Node runs the pushes; each array holds them. +import { Schema, rows } from "mini"; + +const s = new Schema(); +console.log(JSON.stringify(s.safeParse(1)), JSON.stringify(s.safeParse("a"))); +console.log(JSON.stringify(s.safeParse(2, { path: ["root"] }))); +console.log(rows()); diff --git a/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.d.ts b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.d.ts new file mode 100644 index 000000000..5925684c0 --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.d.ts @@ -0,0 +1,5 @@ +export interface Issue { code: string; path: (string | number)[]; message: string } +export declare class Schema { + safeParse(data: unknown, params?: { async?: boolean; path?: (string | number)[] }): { success: true; data: string } | { success: false; issues: Issue[] }; +} +export declare function rows(): string; diff --git a/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.js b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.js new file mode 100644 index 000000000..47d24c8ad --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.js @@ -0,0 +1,31 @@ +// A parse context as zod 3 keeps it: the issue list starts as `[]` in a +// nested literal, and the path is the caller's array or `[]`. +export function makeIssue(params) { + const { path, issueData } = params; + const fullPath = [...path, ...(issueData.path || [])]; + return { ...issueData, path: fullPath, message: "m" }; +} +export function addIssue(ctx, issueData) { + const issue = makeIssue({ issueData, path: ctx.path }); + ctx.common.issues.push(issue); +} +export class Schema { + safeParse(data, params) { + const ctx = { + common: { issues: [], async: params?.async ?? false }, + path: params?.path || [], + data, + }; + if (typeof data !== "string") addIssue(ctx, { code: "invalid_type" }); + return ctx.common.issues.length === 0 ? { success: true, data } : { success: false, issues: ctx.common.issues }; + } +} +// Array properties of records in an array literal and in a deeper literal, +// filled after construction. +export function rows() { + const table = { rows: [{ cells: [] }, { cells: [] }], meta: { tags: { list: [] } }, size: 2 }; + table.rows[0].cells.push({ v: 1 }); + table.rows[1].cells.push("text", 3); + table.meta.tags.list.push({ name: "t" }, null); + return JSON.stringify(table); +} diff --git a/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/package.json b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/package.json new file mode 100644 index 000000000..be8bc49a2 --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/package.json @@ -0,0 +1 @@ +{ "name": "mini", "version": "1.0.0", "type": "module", "main": "./index.js", "types": "./index.d.ts" } diff --git a/tests/corpus/7020-conditional-spread-fields.ts b/tests/corpus/7020-conditional-spread-fields.ts new file mode 100644 index 000000000..61c250174 --- /dev/null +++ b/tests/corpus/7020-conditional-spread-fields.ts @@ -0,0 +1,137 @@ +// Conditional spreads that carry several properties, and conditional +// spreads of a record value against an empty arm. The condition runs +// once, the carried values run only when their arm is taken and in +// property order, and the keys appear at the spread's position. +const log: string[] = []; +function note(label: string, value: T): T { + log.push(label); + return value; +} + +interface Extra { + region: string; + zone: number; +} + +function build(flag: boolean, model: string | undefined, extra: Extra | undefined) { + return { + name: note("name", "tool"), + ...(note("cond", flag) ? { verbose: note("verbose", true), depth: note("depth", 2) } : {}), + ...(model ? { model } : {}), + ...(extra ? extra : {}), + last: note("last", 0), + }; +} + +for (const [flag, model, extra] of [ + [true, "m1", { region: "eu", zone: 3 }], + [false, undefined, undefined], + [true, undefined, { region: "us", zone: 1 }], +] as const) { + log.length = 0; + const out = build(flag, model, extra); + console.log(JSON.stringify(out), Object.keys(out).join(","), log.join(" ")); + console.log("verbose" in out, out.depth ?? "no depth", out.region ?? "no region"); +} + +// The carrying arm can be the false arm, and `c && { ... }` carries +// several properties too. +function flags(quiet: boolean, color: boolean) { + return { + ...(quiet ? {} : { level: "info", prefix: "[app]" }), + ...(color && { fg: "green", bg: "black", bold: true }), + }; +} +console.log(JSON.stringify(flags(false, true)), JSON.stringify(flags(true, false))); + +// A record arm in either position, against `{}`, `undefined` or `null`. +interface Auth { + user: string; + token?: string; +} +const auth: Auth = { user: "u", token: "t" }; +const bare: Auth = { user: "v" }; +function request(useAuth: boolean, source: Auth) { + return { + url: "/x", + ...(useAuth ? source : {}), + ...(useAuth ? undefined : { anonymous: true }), + }; +} +console.log(JSON.stringify(request(true, auth)), JSON.stringify(request(false, auth))); +console.log(JSON.stringify(request(true, bare)), Object.keys(request(true, bare)).join(",")); +function fallback(primary: Auth | null, secondary: Auth) { + return { ...(primary === null ? secondary : null), id: 1 }; +} +console.log(JSON.stringify(fallback(null, auth)), JSON.stringify(fallback(bare, auth))); + +// A record arm read through a call runs only when its arm is taken. +let reads = 0; +function current(): Extra { + reads++; + return { region: "ap", zone: reads }; +} +const a = { ...(reads > 0 ? current() : {}), tag: "a" }; +const b = { ...(reads === 0 ? current() : {}), tag: "b" }; +console.log(JSON.stringify(a), JSON.stringify(b), reads); + +// Into a declared shape with optional fields. +interface Options { + cwd: string; + shell?: boolean; + timeout?: number; + env?: string; +} +function options(detached: boolean, env: string | undefined): Options { + return { cwd: "/", ...(detached ? { shell: false, timeout: 5 } : {}), ...(env ? { env } : {}) }; +} +console.log(JSON.stringify(options(true, "prod")), JSON.stringify(options(false, undefined))); + +// Inside 'any'-typed object literals, where the key is absent when the +// empty arm is taken. +function anyLiterals(): void { + function opts(aspect: string | undefined): any { + const o: any = { + mode: "M", + ...(aspect ? { cfg: { aspect } } : {}), + }; + return o; + } + const a = opts("1:1"); + console.log(`${a.mode} ${a.cfg.aspect} ${typeof a.cfg}`); + const b = opts(undefined); + console.log(`${b.mode} ${typeof b.cfg}`); + + // Several carried properties. + function multi(on: boolean): any { + return { base: 1, ...(on ? { x: "X", y: "Y" } : {}) }; + } + const m1 = multi(true); + const m2 = multi(false); + console.log(`${m1.base} ${m1.x} ${m1.y}`); + console.log(`${m2.base} ${typeof m2.x} ${typeof m2.y}`); + + // Reversed orientation and string-literal keys. + function rev(quiet: boolean): any { + return { kind: "r", ...(quiet ? {} : { "log-level": "loud" }) }; + } + console.log(`${rev(false)["log-level"]}`); + console.log(`${typeof rev(true)["log-level"]}`); + + // The spread mid-literal: properties around it evaluate exactly once and + // land in engine key order. + let sides = 0; + function side(v: number): number { + sides++; + return v; + } + function ordered(flag: boolean): any { + const o: any = { first: side(1), ...(flag ? { mid: 2 } : {}), last: side(3) }; + return o; + } + const o1 = ordered(true); + const o2 = ordered(false); + console.log(`${o1.first} ${o1.mid} ${o1.last}`); + console.log(`${o2.first} ${typeof o2.mid} ${o2.last} ${sides}`); +} +anyLiterals(); diff --git a/tests/corpus/7021-promise-all-uniform-tuple.ts b/tests/corpus/7021-promise-all-uniform-tuple.ts new file mode 100644 index 000000000..461351992 --- /dev/null +++ b/tests/corpus/7021-promise-all-uniform-tuple.ts @@ -0,0 +1,46 @@ +// `const pair = await Promise.all([p, q])` over entries of one promise +// type: the checker types the binding as a tuple, and the values arrive +// on the same microtask turn as Node's, in entry order whatever the +// settlement order. +const order: string[] = []; +function later(label: string, turns: number): Promise { + return new Promise((resolve) => { + let left = turns; + const step = (): void => { + if (left-- === 0) { + order.push(`settle ${label}`); + resolve(`${label}!`); + } else queueMicrotask(step); + }; + step(); + }); +} +async function count(n: number): Promise { + return n * 10; +} + +async function main(): Promise { + let ticking = true; + void (async () => { + for (let i = 0; i < 20 && ticking; i++) { + order.push(`t${i}`); + await null; + } + })(); + const bodies = await Promise.all([later("a", 3), later("b", 1)]); + order.push(`bodies ${bodies[0]} ${bodies[1]} ${bodies.length}`); + const numbers = await Promise.all([count(1), count(2), count(3)]); + const [x, y, z] = numbers; + order.push(`numbers ${x + y + z} ${numbers.length}`); + const annotated: [string, string] = await Promise.all([later("c", 0), later("d", 0)]); + order.push(`annotated ${annotated.join("+")}`); + ticking = false; + console.log(order.join("\n")); + try { + const failed = await Promise.all([later("e", 2), Promise.reject(new Error("no"))]); + console.log("unreachable", failed[0]); + } catch (e) { + console.log("caught", (e as Error).message); + } +} +void main(); diff --git a/tests/corpus/7022-promise-all-dynamic-arrays.ts b/tests/corpus/7022-promise-all-dynamic-arrays.ts new file mode 100644 index 000000000..dca5cdcde --- /dev/null +++ b/tests/corpus/7022-promise-all-dynamic-arrays.ts @@ -0,0 +1,41 @@ +// Promise.all over arrays built from 'any' values: the argument is a +// runtime value, so the combinator reads it with Node's iteration and +// resolves each entry (promises, thenables and plain values). +interface Check { + name: string; + ok: boolean; +} +async function check(name: string): Promise { + await null; + return { name, ok: name.length > 2 }; +} + +async function main(): Promise { + const raw: any = JSON.parse('{"items":[{"name":"abc"},{"name":"x"}],"ids":[1,2,3]}'); + const results = await Promise.all(raw.items.map((item: any) => check(item.name))); + for (const r of results) console.log(r.name, r.ok); + const doubled = await Promise.all(raw.ids.map(async (id: number) => id * 2)); + console.log(doubled.join(","), doubled.length); + const mixed = await Promise.all(raw.ids.map((id: number) => (id % 2 ? id : Promise.resolve(-id)))); + console.log(JSON.stringify(mixed)); + const parsed: unknown = JSON.parse('["a","bb","ccc"]'); + const list = Array.isArray(parsed) ? parsed : []; + const lengths = await Promise.all(list.map(async (s) => String(s).length)); + console.log(lengths, lengths.reduce((a, b) => a + b, 0)); + const empty: any = []; + console.log(JSON.stringify(await Promise.all(empty))); + try { + await Promise.all(raw.items.map(async (item: any) => { + if (item.name === "x") throw new Error(`bad ${item.name}`); + return item.name; + })); + } catch (e) { + console.log("rejected", (e as Error).message); + } + try { + await Promise.all(raw.missing); + } catch (e) { + console.log("not iterable", e instanceof TypeError); + } +} +void main(); diff --git a/tests/corpus/7023-union-object-patterns.ts b/tests/corpus/7023-union-object-patterns.ts new file mode 100644 index 000000000..8717e892a --- /dev/null +++ b/tests/corpus/7023-union-object-patterns.ts @@ -0,0 +1,63 @@ +// Object patterns over unions of records and classes read the fields +// every arm declares, in parameters, declarations and loops. +interface Add { + kind: "add"; + path: string; + content: string; + mode?: number; +} +interface Remove { + kind: "remove"; + path: string; + mode?: number; +} +type Change = Add | Remove; + +function describe({ kind, path: file, mode = 0o644 }: Change): string { + return `${kind}:${file}:${mode.toString(8)}`; +} +const changes: Change[] = [ + { kind: "add", path: "a.txt", content: "x" }, + { kind: "remove", path: "b.txt", mode: 0o600 }, +]; +for (const change of changes) console.log(describe(change)); +for (const { kind, path } of changes) console.log(kind, path.length); +function applyChange(change: Change): number { + const { path } = change; + let { kind } = change; + if (kind === "add") kind = "remove"; + return path.length + kind.length; +} +console.log(applyChange(changes[0]!), applyChange(changes[1]!)); + +// The source runs once, and the fields read in pattern order. +let reads = 0; +function next(): Change { + reads++; + return reads % 2 ? changes[0]! : changes[1]!; +} +const { path: p1, kind: k1 } = next(); +const { kind: k2 } = next(); +console.log(p1, k1, k2, reads); + +class Circle { + name: string; + size: number; + constructor(name: string, size: number) { + this.name = name; + this.size = size; + } +} +class Square { + name: string; + size: number; + corners = 4; + constructor(name: string, size: number) { + this.name = name; + this.size = size; + } +} +function label({ name, size }: Circle | Square): string { + return `${name}/${size}`; +} +console.log(label(new Circle("c", 2)), label(new Square("s", 3))); diff --git a/tests/corpus/7024-async-return-fresh-collections.ts b/tests/corpus/7024-async-return-fresh-collections.ts new file mode 100644 index 000000000..8343e028b --- /dev/null +++ b/tests/corpus/7024-async-return-fresh-collections.ts @@ -0,0 +1,48 @@ +// Async functions can return an empty array literal (where the result is +// inferred) or a new Map or Set: the value takes the result's element +// types, as a synchronous return does. +import { existsSync, readdirSync } from "node:fs"; + +async function listJson(dir: string) { + if (!existsSync(dir)) return []; + return readdirSync(dir).filter((name) => name.endsWith(".json")); +} + +async function childPaths(root: string) { + if (root === "") { + return []; + } + const found = [root, `${root}/b`]; + return found; +} + +async function index(names: string[]): Promise> { + if (names.length === 0) return new Map(); + const byName = new Map(); + for (const name of names) byName.set(name, name.length); + return byName; +} + +async function unique(words: string[]): Promise> { + if (words.length === 0) return new Set(); + return new Set(words); +} + +const pick = async (flag: boolean) => (flag ? [] : ["a", "b"]); + +async function main(): Promise { + const missing = await listJson("/nonexistent-dir-for-test"); + missing.push("pushed"); + console.log(missing, missing.length); + console.log(await childPaths(""), await childPaths("/root")); + const empty = await index([]); + empty.set("late", 4); + console.log(empty.size, empty.get("late"), (await index(["ab", "c"])).get("ab")); + const none = await unique([]); + none.add("z"); + console.log([...none], [...(await unique(["x", "x", "y"]))]); + const first = await pick(true); + first.push("c"); + console.log(first, await pick(false)); +} +void main(); diff --git a/tests/corpus/7025-child-listener-chaining.ts b/tests/corpus/7025-child-listener-chaining.ts new file mode 100644 index 000000000..317c87db1 --- /dev/null +++ b/tests/corpus/7025-child-listener-chaining.ts @@ -0,0 +1,42 @@ +// child.on() and child.once() return the child, so registrations chain +// and the result can be stored or passed on. The receiver evaluates once, +// before the listener. Each child starts after the previous one closed. +import { spawn } from "node:child_process"; + +const events: string[] = []; +let lookups = 0; +const children = [spawn("sh", ["-c", "exit 3"], { stdio: "ignore" })]; +function first() { + lookups++; + return children[0]!; +} + +const returned = first() + .on("exit", (code) => events.push(`exit ${code}`)) + .once("close", (code, signal) => { + events.push(`close ${code} ${signal}`); + console.log(events.join(", "), lookups); + second(); + }) + .on("error", (err) => events.push(`error ${err.message}`)); +console.log(returned === children[0], lookups); + +function second(): void { + spawn("sh", ["-c", "exit 0"], { stdio: "ignore" }) + .on("spawn", () => console.log("spawned")) + .on("exit", (code) => console.log("second exit", code)) + .on("close", () => third()); +} + +function third(): void { + const missing = spawn("/nonexistent/command-for-test", [], { stdio: "ignore" }).on( + "error", + (e) => console.log("missing", (e as NodeJS.ErrnoException).code), + ); + console.log(typeof missing.pid); + const detached = spawn("sh", ["-c", "exit 0"], { detached: true, stdio: "ignore" }).once( + "error", + (e) => console.log("spawn failed", e.message), + ); + detached.unref(); +} diff --git a/tests/corpus/7026-url-object-patterns.ts b/tests/corpus/7026-url-object-patterns.ts new file mode 100644 index 000000000..c2bc59958 --- /dev/null +++ b/tests/corpus/7026-url-object-patterns.ts @@ -0,0 +1,27 @@ +// Object patterns over URLs read the URL's getters in pattern order, +// with renames and defaults, in declarations and parameters. +let built = 0; +function parse(text: string): URL { + built++; + return new URL(text); +} +const { protocol, hostname, port } = parse("http://localhost:8080/path?q=1#top"); +console.log(protocol, hostname, port, built); +const { host: where, pathname: path = "/", search, hash, href } = parse("https://example.com/a/b?x=1&y=2#h"); +console.log(where, path, search, hash, href, built); + +function origin({ protocol: scheme, host, username, password }: URL): string { + return `${scheme}//${username}:${password}@${host}`; +} +console.log(origin(new URL("ftp://user:secret@files.example.org:2121/dir"))); + +const { searchParams } = new URL("http://h/?a=1&b=2"); +searchParams.append("c", "3"); +console.log(searchParams.toString(), [...searchParams.keys()].join(",")); + +const { origin: o, port: p } = new URL("https://[::1]:8443/"); +console.log(o, p); +for (const text of ["http://a.test:1", "http://b.test"]) { + const { hostname: name, port: listen } = new URL(text); + console.log(name, listen === "" ? "(default)" : listen); +} diff --git a/tests/corpus/7027-search-params-conditional-init.ts b/tests/corpus/7027-search-params-conditional-init.ts new file mode 100644 index 000000000..580d6c039 --- /dev/null +++ b/tests/corpus/7027-search-params-conditional-init.ts @@ -0,0 +1,21 @@ +// new URLSearchParams(c ? a : b) builds from the arm the condition +// selects; each arm may be a record literal, a string, pairs or another +// URLSearchParams. +let checks = 0; +function flag(value: boolean): boolean { + checks++; + return value; +} +for (const on of [true, false]) { + const params = new URLSearchParams(flag(on) ? { code: "x y", state: "s&t" } : {}); + console.log(params.toString(), params.size); + const query = new URLSearchParams(on ? "a=1&b=2" : [["c", "3"]]); + console.log(query.toString()); + const base = new URLSearchParams("keep=1"); + const copy = new URLSearchParams(on ? base : undefined); + copy.append("more", "2"); + console.log(copy.toString(), base.toString()); + const nested = new URLSearchParams(on ? (checks > 1 ? { n: "2" } : { n: "1" }) : "?empty="); + console.log(nested.toString()); +} +console.log(checks); diff --git a/tests/corpus/7028-non-empty-rest-tuples.ts b/tests/corpus/7028-non-empty-rest-tuples.ts new file mode 100644 index 000000000..5d00952ab --- /dev/null +++ b/tests/corpus/7028-non-empty-rest-tuples.ts @@ -0,0 +1,38 @@ +// Tuples of required positions followed by a rest element of the same +// type (`[string, ...string[]]`, the non-empty list) behave as arrays of +// that type. +type Hosts = [string, ...string[]]; +const LOCAL_HOSTS: Hosts = ["127.0.0.1", "::1", "localhost"]; +const single: [string, ...string[]] = ["only"]; +const pair: readonly [number, number, ...number[]] = [1, 2]; + +function first(hosts: Hosts): string { + return hosts[0]; +} +function isLocalHost(host: string): boolean { + return LOCAL_HOSTS.includes(host); +} +console.log(first(LOCAL_HOSTS), first(single), LOCAL_HOSTS.length, single.length); +console.log(isLocalHost("::1"), isLocalHost("10.0.0.1"), LOCAL_HOSTS.indexOf("localhost")); +console.log(LOCAL_HOSTS.slice(1).join(","), LOCAL_HOSTS.map((h) => h.length)); +for (const host of LOCAL_HOSTS) console.log(host); +const [head, ...rest] = LOCAL_HOSTS; +console.log(head, rest, JSON.stringify(LOCAL_HOSTS)); +single.push("more"); +console.log(single, single[1], pair[0] + pair[1], pair.length); + +function hostsFrom(text: string): Hosts { + const parts = text.split(","); + const out: Hosts = [parts[0]!]; + for (const part of parts.slice(1)) out.push(part); + return out; +} +const parsed = hostsFrom("a,b,c"); +console.log(parsed, parsed.length, `${parsed}`); + +function join(...names: [string, ...string[]]): string { + return names.join("+"); +} +console.log(join("x"), join("x", "y", "z"), join(...LOCAL_HOSTS)); +const lists: Hosts[] = [LOCAL_HOSTS, single]; +console.log(lists.map((l) => l[0]).join(" ")); diff --git a/tests/corpus/7029-flatmap-union-results.ts b/tests/corpus/7029-flatmap-union-results.ts new file mode 100644 index 000000000..efeae4d8f --- /dev/null +++ b/tests/corpus/7029-flatmap-union-results.ts @@ -0,0 +1,44 @@ +// flatMap callbacks that return a value or an array: an array result +// appends its elements (holes skipped), any other result appends itself, +// and an empty `[]` arm appends nothing. +const words = ["a", "", "c", "", "e"]; +console.log(words.flatMap((s) => (s ? s : []))); +console.log(words.flatMap((s, i) => (s ? [s, String(i)] : []))); + +function expand(line: string): string | string[] { + return line.includes(",") ? line.split(",") : line; +} +console.log(["x", "y,z", "w"].flatMap(expand), ["x", "y,z"].flatMap((l) => expand(l)).length); + +const numbers = [1, 2, 3, 4]; +console.log(numbers.flatMap((n) => (n % 2 ? n : [n, n * 10]))); +const sparse: number[] = []; +sparse[0] = 1; +sparse[2] = 3; +console.log(numbers.flatMap((n) => (n === 2 ? sparse : n))); + +interface Entry { + name: string; + tags?: string[]; +} +const entries: Entry[] = [{ name: "a", tags: ["t1", "t2"] }, { name: "b" }]; +console.log(entries.flatMap((e) => e.tags ?? []), entries.flatMap((e) => e.tags ?? e.name)); +let calls = 0; +const out = words.flatMap((s) => { + calls++; + return s.length > 0 ? s.toUpperCase() : []; +}); +console.log(out, calls); + +// One union result type with an empty arm in one callback and a populated +// array arm in another. +const counts = [1, 2, 3]; +console.log(counts.flatMap((n) => (n % 2 ? n : [])), counts.flatMap((n) => (n % 2 ? n : [n, n]))); + +// Array results keep present undefined elements, and an undefined result +// appends itself. +const withGaps: (number | undefined)[] = [4, undefined, 6]; +const maybe: (number | undefined)[] = [1, 2, undefined]; +console.log(maybe.flatMap((n) => (n === 2 ? withGaps : n))); +const gaps: (string | undefined)[] = ["x", undefined]; +console.log(["a", "b"].flatMap((s) => (s === "b" ? gaps : s))); diff --git a/tests/corpus/7033-any-fields-into-declared-records.ts b/tests/corpus/7033-any-fields-into-declared-records.ts new file mode 100644 index 000000000..89c0f69e8 --- /dev/null +++ b/tests/corpus/7033-any-fields-into-declared-records.ts @@ -0,0 +1,28 @@ +// Records whose fields TypeScript types `any` convert into declared +// record types: each such field is checked against the declared type, +// as a typed binding initialized from the value is. +interface Transfer { + source: string; + targetId: string; + bytes: number; + tags: string[]; +} +const meta: any = JSON.parse('{"id":"x-1","size":4,"tags":["a","b"]}'); + +const mapped: Transfer[] = ["a", "b"].map((s, i) => ({ + source: s, + targetId: meta.id, + bytes: meta.size + i, + tags: meta.tags, +})); +console.log(JSON.stringify(mapped)); + +function build(): Transfer { + const record = { source: "c", targetId: meta.id, bytes: meta.size, tags: [] as string[] }; + return record; +} +console.log(JSON.stringify(build())); + +const list: Transfer[] = []; +for (const entry of [{ source: "d", targetId: meta.id, bytes: 1, tags: meta.tags }]) list.push(entry); +console.log(list.length, list[0]!.targetId.toUpperCase(), list[0]!.tags.length); diff --git a/tests/corpus/7034-optional-field-union-routes.ts b/tests/corpus/7034-optional-field-union-routes.ts new file mode 100644 index 000000000..0fb93ff8d --- /dev/null +++ b/tests/corpus/7034-optional-field-union-routes.ts @@ -0,0 +1,26 @@ +// A record whose `T | undefined` field is the one field that tells two +// arms of a union apart converts into that union: a value picks the arm +// that declares the field, and undefined picks the arm without it. +type Summary = + | { written: number; errors: string[]; ignored: number } + | { written: number; ignored: number }; + +function summarize(written: number, ignored: number, errors: string[]): Summary { + return { written, errors: errors.length > 0 ? errors : undefined, ignored }; +} +function errorCount(summary: Summary): number { + return "errors" in summary && summary.errors !== undefined ? summary.errors.length : 0; +} +for (const errors of [[], ["e1", "e2"]]) { + const summary = summarize(3, 1, errors); + console.log(JSON.stringify(summary), errorCount(summary), summary.written + summary.ignored); +} + +type Lookup = { name: string; value: number; source: string } | { name: string; source: string }; +const values = new Map([["a", 1]]); +const lookups: Lookup[] = ["a", "b"].map((name) => ({ + name, + value: values.get(name), + source: "map", +})); +console.log(JSON.stringify(lookups)); diff --git a/tests/corpus/7132-key-order-json.ts b/tests/corpus/7132-key-order-json.ts new file mode 100644 index 000000000..0cd3dff61 --- /dev/null +++ b/tests/corpus/7132-key-order-json.ts @@ -0,0 +1,31 @@ +// JSON.stringify of object literals that write the same keys in different +// orders: each value serializes in the order its literal wrote, as in Node. +type Point = { x: number; y: number; label?: string }; + +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +for (const value of [a, b, c]) console.log(JSON.stringify(value)); +console.log(JSON.stringify([a, b, c])); +console.log(JSON.stringify({ first: b, rest: [c, a] }, null, 2)); + +// Optional keys the literal leaves out, and writes to keys it has. +const p: Point = { y: 2, x: 1 }; +const q: Point = { label: "q", x: 3, y: 4 }; +const r: Point = { x: 5, y: 6, label: "r" }; +q.x = 30; +r.label = "r2"; +for (const point of [p, q, r]) console.log(JSON.stringify(point)); + +// A function typed by one literal's shape receiving the others. +function describe(value: { id: number; name: string; ok: boolean }): string { + return JSON.stringify(value); +} +console.log(describe(c), describe(a)); + +// JSON.parse into the same record type keeps each text's own key order. +type Entry = { id: number; name: string; ok: boolean }; +for (const text of ['{"ok":true,"name":"p","id":7}', '{"id":8,"name":"q","ok":false}']) { + const parsed = JSON.parse(text) as Entry; + console.log(JSON.stringify(parsed), Object.keys(parsed).join(","), describe(parsed)); +} diff --git a/tests/corpus/7133-key-order-keys-values-entries.ts b/tests/corpus/7133-key-order-keys-values-entries.ts new file mode 100644 index 000000000..29f725c24 --- /dev/null +++ b/tests/corpus/7133-key-order-keys-values-entries.ts @@ -0,0 +1,19 @@ +// Object.keys, Object.values, Object.entries and Object.fromEntries over +// object literals that write the same keys in different orders. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +for (const value of [a, b, c]) { + console.log(Object.keys(value).join(",")); + console.log(Object.values(value).join(",")); + console.log(Object.entries(value).map(([key, item]) => `${key}=${String(item)}`).join(" ")); + console.log(JSON.stringify(Object.fromEntries(Object.entries(value)))); +} + +type Row = { year: number; title?: string; score: number }; +const rows: Row[] = [ + { score: 1, year: 2001 }, + { title: "t", year: 2002, score: 2 }, + { year: 2003, score: 3, title: "u" }, +]; +for (const row of rows) console.log(Object.keys(row).join(","), Object.values(row).length); diff --git a/tests/corpus/7134-key-order-for-in.ts b/tests/corpus/7134-key-order-for-in.ts new file mode 100644 index 000000000..89ed16edb --- /dev/null +++ b/tests/corpus/7134-key-order-for-in.ts @@ -0,0 +1,15 @@ +// for-in over object literals that write the same keys in different +// orders, and over values a function received. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +function keysOf(value: { id: number; name: string; ok: boolean }): string { + const keys: string[] = []; + for (const key in value) keys.push(key); + return keys.join(","); +} +for (const value of [a, b, c]) { + const seen: string[] = []; + for (const key in value) seen.push(key.toUpperCase()); + console.log(seen.join(" "), keysOf(value)); +} diff --git a/tests/corpus/7135-key-order-inspect.ts b/tests/corpus/7135-key-order-inspect.ts new file mode 100644 index 000000000..6a11eaf2c --- /dev/null +++ b/tests/corpus/7135-key-order-inspect.ts @@ -0,0 +1,12 @@ +import { inspect } from "node:util"; + +// console.log and util.inspect of object literals that write the same keys +// in different orders, alone and nested. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +console.log(a, b, c); +console.log([a, b, c]); +console.log({ outer: { inner: b }, list: [c] }); +console.log(inspect(b, { depth: 0 }), inspect({ deep: { deeper: c } }, { depth: 0 })); +console.log(`${inspect(c)} / ${inspect(a)}`); diff --git a/tests/corpus/7136-key-order-copies.ts b/tests/corpus/7136-key-order-copies.ts new file mode 100644 index 000000000..5d754bced --- /dev/null +++ b/tests/corpus/7136-key-order-copies.ts @@ -0,0 +1,32 @@ +// Copies of object literals that write the same keys in different orders: +// spreads, spreads with other keys before and after, rest bindings, +// Object.assign and structuredClone keep each value's own order. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +for (const value of [a, b, c]) { + const copy = { ...value }; + const changed = { ...value, id: 10 }; + const after = { ...value, extra: "e" }; + const before = { first: 0, ...value }; + const { name, ...rest } = value; + const assigned = Object.assign({}, value); + const cloned = structuredClone(value); + console.log(JSON.stringify(copy), JSON.stringify(changed)); + console.log(JSON.stringify(after), Object.keys(before).join(",")); + console.log(name, JSON.stringify(rest), Object.keys(rest).join(",")); + console.log(JSON.stringify(assigned), JSON.stringify(cloned)); +} +// Keys after a spread that the spread value may lack: a key the value has +// keeps its place, and a key it lacks goes after the value's keys. +type Options = { id: number; label?: string; size?: number; tag?: string }; +const options: Options[] = [ + { id: 1, size: 2 }, + { size: 3, id: 2 }, + { label: "c", id: 3, tag: "t" }, + { tag: "u", size: 4, id: 4 }, +]; +for (const value of options) { + const merged = { ...value, tag: "x", label: "y", done: true }; + console.log(Object.keys(merged).join(","), JSON.stringify(merged)); +} diff --git a/tests/corpus/7137-key-order-dynamic.ts b/tests/corpus/7137-key-order-dynamic.ts new file mode 100644 index 000000000..55970cb40 --- /dev/null +++ b/tests/corpus/7137-key-order-dynamic.ts @@ -0,0 +1,30 @@ +import { MessageChannel } from "node:worker_threads"; + +// Object literals that write the same keys in different orders, seen +// through untyped values: their keys, JSON text, own property names, and a +// MessageChannel message. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +const values: unknown[] = [a, b, c]; +for (const value of values) { + const object = value as Record; + console.log(Object.keys(object).join(","), JSON.stringify(object)); + console.log(Object.getOwnPropertyNames(object).join(",")); +} +const { port1, port2 } = new MessageChannel(); +let received = 0; +port1.on("message", (message: unknown) => { + console.log("message", JSON.stringify(message), Object.keys(message as object).join(",")); + if (++received === 3) port1.close(); +}); +for (const value of [a, b, c]) port2.postMessage(value); + +// A nested value seen through an untyped view keeps its identity and its +// own key order, and writes through the view reach it. +const holder = { inner: b, tag: "h" }; +const view = holder as unknown as Record; +const inner = view.inner as Record; +console.log(view.inner === view.inner, Object.keys(inner).join(",")); +inner.id = 20; +console.log(b.id, JSON.stringify(b), JSON.stringify(view)); diff --git a/tests/corpus/7138-key-order-late-writes.ts b/tests/corpus/7138-key-order-late-writes.ts new file mode 100644 index 000000000..4166999d7 --- /dev/null +++ b/tests/corpus/7138-key-order-late-writes.ts @@ -0,0 +1,44 @@ +// Optional keys added after construction go last, as in Node, also for +// shapes whose literals write their keys in different orders: a write of a +// missing key, a delete and a later write, and writes in a loop. +type Item = { id: number; name?: string; note?: string; count: number }; + +const items: Item[] = [ + { id: 1, count: 0 }, + { count: 1, name: "b", id: 2 }, + { note: "c", id: 3, count: 2 }, +]; +items[0]!.note = "added"; +items[0]!.name = "later"; +items[1]!.name = "kept"; +delete items[2]!.note; +items[2]!.note = "again"; +for (const item of items) { + if (item.count > 0) item.name ??= "filled"; + console.log(JSON.stringify(item), Object.keys(item).join(",")); +} +const built: Item = { count: 9, id: 9 }; +for (let i = 0; i < 2; i++) { + if (i === 1) built.note = `n${i}`; + console.log(Object.keys(built).join(",")); +} +console.log(built); +// A write's value runs first, so a key it adds comes before the written +// key. +const nested: Item = { count: 3, id: 4 }; +nested.name = ((nested.note = "first"), "second"); +console.log(Object.keys(nested).join(","), JSON.stringify(nested)); +// A value seen as a JavaScript value enumerates later writes in order. +const seen: Item = { id: 5, count: 4 }; +const view: unknown = seen; +console.log(Object.keys(view as object).join(",")); +seen.note = "n"; +seen.name = "m"; +console.log(Object.keys(view as object).join(","), JSON.stringify(view)); +// The write goes to the object the receiver held before the value ran, +// even when the value assigns the receiver's binding. +let target: Item = { id: 6, count: 5 }; +const before = target; +const after: Item = { count: 6, id: 7 }; +target.name = ((target = after), "moved"); +console.log(Object.keys(before).join(","), Object.keys(after).join(","), JSON.stringify(target)); diff --git a/tests/corpus/7246-stream-construct.ts b/tests/corpus/7246-stream-construct.ts new file mode 100644 index 000000000..7cfb82e49 --- /dev/null +++ b/tests/corpus/7246-stream-construct.ts @@ -0,0 +1,156 @@ +// Deferred construction in typed code: the construct option and the +// _construct method on Readable, Writable, Duplex and Transform. Writes +// and reads wait for the callback, an error from it destroys the stream, +// a destroy while constructing reports both errors as an AggregateError, +// a throw inside construct is its error, and a second callback is +// ERR_MULTIPLE_CALLBACK. +import { EventEmitter } from "node:events"; +import { Duplex, Readable, Transform, Writable } from "node:stream"; + +const log: string[] = []; + +function ticks(count: number): void { + let i = 0; + const step = (): void => { + log.push("t" + i); + if (++i < count) process.nextTick(step); + }; + process.nextTick(step); +} + +class Store extends Writable { + private ready = false; + _construct(callback: (error?: Error | null) => void): void { + log.push("store construct"); + setTimeout(() => { + this.ready = true; + log.push("store ready"); + callback(); + }, 2); + } + _write(chunk: Buffer, _enc: string, cb: (error?: Error | null) => void): void { + log.push(`store write ${chunk.toString()} ready=${this.ready}`); + cb(); + } + _final(cb: (error?: Error | null) => void): void { + log.push("store final"); + cb(); + } +} + +class Pair extends Duplex { + constructor() { + super({ readableObjectMode: true }); + } + _construct(callback: (error?: Error | null) => void): void { + log.push("pair construct"); + process.nextTick(callback); + } + _read(): void {} + _write(chunk: Buffer, _enc: string, cb: (error?: Error | null) => void): void { + this.push({ got: chunk.toString() }); + cb(); + } + _final(cb: (error?: Error | null) => void): void { + this.push(null); + cb(); + } +} + +function closed(stream: EventEmitter): Promise { + return new Promise((resolve) => stream.on("close", () => resolve())); +} + +async function main(): Promise { + ticks(4); + const store = new Store(); + log.push("write " + store.write("a")); + store.end("b"); + store.on("finish", () => log.push("store finish")); + await closed(store); + + const source = new Readable({ + construct(cb: (error?: Error | null) => void) { + log.push("source construct"); + process.nextTick(cb); + }, + read() { + log.push("source read"); + this.push("x"); + this.push(null); + }, + }); + source.on("data", (c: Buffer) => log.push("source data " + c.toString())); + await closed(source); + + const pair = new Pair(); + pair.on("data", (v: { got: string }) => log.push("pair data " + v.got)); + pair.write("p"); + pair.end(); + await closed(pair); + + const upper = new Transform({ + construct(cb: (error?: Error | null) => void) { + log.push("upper construct"); + cb(); + }, + transform(c: Buffer, _e: string, cb: (error: Error | null, data?: Buffer) => void) { + cb(null, Buffer.from(c.toString().toUpperCase())); + }, + }); + upper.on("data", (c: Buffer) => log.push("upper " + c.toString())); + upper.end("shout"); + await closed(upper); + + const failing = new Writable({ + construct(cb: (error?: Error | null) => void) { + cb(new Error("no target")); + }, + write(_c: Buffer, _e: string, cb: (error?: Error | null) => void) { + cb(); + }, + }); + failing.on("error", (e: Error) => log.push("failing " + e.message)); + await closed(failing); + + const raced = new Writable({ + construct(cb: (error?: Error | null) => void) { + setTimeout(() => cb(new Error("late")), 2); + }, + write(_c: Buffer, _e: string, cb: (error?: Error | null) => void) { + cb(); + }, + destroy(err: Error | null, cb: (error?: Error | null) => void) { + log.push("raced destroy " + (err ? err.message : "none")); + cb(err); + }, + }); + raced.on("error", (e: Error) => log.push(`raced ${e.name} ${e.message}`)); + raced.destroy(new Error("early")); + await closed(raced); + + const thrown = new Writable({ + construct() { + throw new Error("thrown in construct"); + }, + write(_c: Buffer, _e: string, cb: (error?: Error | null) => void) { + cb(); + }, + }); + thrown.on("error", (e: Error) => log.push("thrown " + e.message)); + await closed(thrown); + + const twice = new Writable({ + construct(cb: (error?: Error | null) => void) { + cb(); + cb(); + }, + write(_c: Buffer, _e: string, cb: (error?: Error | null) => void) { + cb(); + }, + }); + twice.on("error", (e: Error) => log.push("twice " + (e as NodeJS.ErrnoException).code)); + await closed(twice); + console.log(log.join("\n")); +} +main(); diff --git a/tests/corpus/7247-stream-construct.mjs b/tests/corpus/7247-stream-construct.mjs new file mode 100644 index 000000000..355116fa7 --- /dev/null +++ b/tests/corpus/7247-stream-construct.mjs @@ -0,0 +1,101 @@ +// Deferred construction in JavaScript: construct in an options literal, +// in an options value read at run time, and _construct on a constructor +// function's prototype, with the write, read, error, destroy-while- +// constructing, throwing and double-callback paths. +import * as stream from "node:stream"; +import { Writable, Readable } from "node:stream"; +import * as util from "node:util"; +const log = []; +const tick = (n) => { let i = 0; const f = () => { log.push("t" + i); if (++i < n) process.nextTick(f); }; process.nextTick(f); }; +tick(4); +const wopts = { + construct(cb) { log.push("construct"); setTimeout(() => { log.push("constructed"); cb(); }, 2); }, + write(c, e, cb) { log.push("write " + c + "/" + e); cb(); }, + final(cb) { log.push("final"); cb(); }, +}; +const w = new Writable(dyn(wopts)); +log.push("write ret " + w.write("a")); +w.end("b"); +w.on("finish", () => log.push("finish")); +w.on("close", () => { log.push("close"); next(); }); +function next() { + const ropts = { construct(cb) { log.push("r construct"); process.nextTick(cb); }, read() { log.push("read"); r.push("x"); r.push(null); } }; + const r = new Readable(dyn(ropts)); + r.on("data", (c) => log.push("data " + c)); + r.on("end", () => { log.push("r end"); errs(); }); +} +function errs() { + const eo = { construct(cb) { cb(new Error("boom")); }, write(c, e2, cb) { cb(); } }; + const e = new Writable(dyn(eo)); + e.on("error", (err) => log.push("error " + err.message)); + e.on("close", () => { log.push("e close"); destroyDuring(); }); +} +function destroyDuring() { + const dopts = { construct(cb) { setTimeout(() => cb(new Error("late")), 2); }, write(c, e, cb) { cb(); }, destroy(err, cb) { log.push("destroy " + (err && err.message)); cb(err); } }; + const d = new Writable(dyn(dopts)); + d.on("error", (err) => log.push("d error " + err.name + " " + err.message + " " + (err.errors ? err.errors.map((x) => x.message).join("+") : ""))); + d.on("close", () => { log.push("d close"); throwing(); }); + d.destroy(new Error("early")); +} +function throwing() { + const to = { construct(cb) { throw new Error("thrown"); }, write(c, e, cb) { cb(); } }; + const t = new Writable(dyn(to)); + t.on("error", (err) => log.push("t error " + err.message)); + t.on("close", () => { log.push("t close"); twice(); }); +} +function twice() { + const o = { construct(cb) { cb(); cb(); }, write(c, e, cb) { cb(); } }; + const t = new Writable(dyn(o)); + t.on("error", (err) => log.push("twice error " + err.code)); + t.on("close", () => { log.push("twice close"); viaLiteral(); }); + setTimeout(() => t.destroy(), 5); +} +function dyn(o) { + return o; +} +function viaLiteral() { + const lit = new Writable({ + construct(cb) { + log.push("literal construct " + (this === lit)); + setImmediate(cb); + }, + write(chunk, enc, cb) { + log.push(`literal write ${chunk}/${enc}`); + cb(); + }, + }); + lit.end("z", () => { + log.push("literal end"); + viaConstructor(); + }); +} +function viaConstructor() { + function Bad() { + stream.Readable.call(this); + } + util.inherits(Bad, stream.Readable); + Bad.prototype._construct = function () { + throw new Error("ctor threw"); + }; + Bad.prototype._read = function () {}; + function Late(opts) { + stream.Writable.call(this, opts); + } + util.inherits(Late, stream.Writable); + Late.prototype._construct = function (cb) { + log.push("late construct " + (this instanceof Late)); + setTimeout(cb, 1); + }; + Late.prototype._write = function (chunk, enc, cb) { + log.push("late write " + chunk); + cb(); + }; + const late = new Late(); + late.write("q"); + late.end(() => { + log.push("late end"); + const bad = new Bad(); + bad.on("error", (e) => log.push("bad " + e.message)); + bad.on("close", () => console.log(log.join("\n"))); + }); +} diff --git a/tests/corpus/7248-stream-writev.ts b/tests/corpus/7248-stream-writev.ts new file mode 100644 index 000000000..2df298d6d --- /dev/null +++ b/tests/corpus/7248-stream-writev.ts @@ -0,0 +1,124 @@ +// Batched writes in typed code: the writev option and the _writev method +// on Writable and Duplex. Writes buffered behind a write in flight or a +// cork reach writev as one [{ chunk, encoding }] batch whose callback +// runs the writes' callbacks in order; a stream with only writev sends +// each lone write as a batch of one; a later `stream._writev = fn` +// takes over; an object-mode side batches its values. +import { EventEmitter } from "node:events"; +import { Duplex, Writable } from "node:stream"; + +interface Entry { + chunk: Buffer; + encoding: string; +} + +interface ValueEntry { + chunk: unknown; + encoding: string | undefined; +} + +const log: string[] = []; + +function finished(stream: EventEmitter): Promise { + return new Promise((resolve) => stream.on("finish", () => resolve())); +} + +class Batcher extends Writable { + _write(chunk: Buffer, _enc: string, cb: (error?: Error | null) => void): void { + log.push("batcher write " + chunk.toString()); + setTimeout(cb, 1); + } + _writev(chunks: Entry[], cb: (error?: Error | null) => void): void { + log.push(`batcher writev ${chunks.map((c) => c.chunk.toString() + "/" + c.encoding).join(",")}`); + setTimeout(cb, 1); + } +} + +class Echo extends Duplex { + _read(): void {} + _write(chunk: Buffer, _enc: string, cb: (error?: Error | null) => void): void { + this.push(chunk); + cb(); + } + _writev(chunks: Entry[], cb: (error?: Error | null) => void): void { + this.push(Buffer.concat(chunks.map((c) => c.chunk))); + cb(); + } + _final(cb: (error?: Error | null) => void): void { + this.push(null); + cb(); + } +} + +async function main(): Promise { + const batcher = new Batcher(); + batcher.write("a", () => log.push("a done")); + batcher.write("b", () => log.push("b done")); + batcher.write("c"); + batcher.write(Buffer.from("d"), () => log.push("d done")); + batcher.end(() => log.push("batcher end")); + await finished(batcher); + + const corked = new Writable({ + writev(chunks: Entry[], cb: (error?: Error | null) => void) { + log.push(`corked ${chunks.length}: ${chunks.map((c) => c.chunk.toString("hex")).join(" ")}`); + cb(); + }, + }); + corked.cork(); + corked.write("x"); + corked.write("ff", "hex"); + corked.write("y"); + corked.uncork(); + corked.write("z", () => log.push("z done")); + corked.end(); + await finished(corked); + + const late = new Writable({ + write(c: Buffer, _e: string, cb: () => void) { + log.push("late write " + c.toString()); + cb(); + }, + }); + late._writev = (chunks: Entry[], cb: (error?: Error | null) => void) => { + log.push("late writev " + chunks.length); + cb(); + }; + late.cork(); + late.write("1"); + late.write("2"); + late.uncork(); + late.end(); + await finished(late); + + const echo = new Echo(); + const echoed: string[] = []; + echo.on("data", (c: Buffer) => echoed.push(c.toString())); + echo.cork(); + echo.write("e1"); + echo.write("e2"); + process.nextTick(() => { + echo.uncork(); + echo.end("e3"); + }); + await new Promise((resolve) => echo.on("end", resolve)); + log.push("echoed " + echoed.join(",")); + + const values = new Writable({ + objectMode: true, + writev(chunks: ValueEntry[], cb: (error?: Error | null) => void) { + log.push( + "values " + chunks.map((c) => `${JSON.stringify(c.chunk)}/${String(c.encoding)}`).join(","), + ); + cb(); + }, + }); + values.cork(); + values.write({ n: 1 }); + values.write([2], "utf8"); + values.uncork(); + values.end(); + await finished(values); + console.log(log.join("\n")); +} +main(); diff --git a/tests/corpus/7249-buffer-index-of-candidates.ts b/tests/corpus/7249-buffer-index-of-candidates.ts new file mode 100644 index 000000000..ae6c2ae23 --- /dev/null +++ b/tests/corpus/7249-buffer-index-of-candidates.ts @@ -0,0 +1,27 @@ +// Buffer indexOf, lastIndexOf and includes with needles whose first byte +// recurs: a candidate whose later bytes differ is skipped, matches at the +// first and last positions are found, byteOffset clamps and counts from the +// end, and the search reads only the buffer's own window of its memory. +const text = Buffer.from("aab aaab aaaab ab\nab"); +console.log(text.indexOf("ab"), text.indexOf("aab"), text.indexOf("aaab"), text.indexOf("aaaab"), text.indexOf("aaaaab")); +console.log(text.lastIndexOf("ab"), text.lastIndexOf("aab"), text.lastIndexOf("aaab"), text.lastIndexOf("b\na")); +console.log(text.indexOf("ab", 2), text.indexOf("ab", -3), text.indexOf("ab", 100), text.indexOf("ab", -100)); +console.log(text.lastIndexOf("ab", 5), text.lastIndexOf("ab", -4), text.lastIndexOf("ab", 0), text.lastIndexOf("aa", 1)); +console.log(text.indexOf(10), text.indexOf(97, 3), text.lastIndexOf(98), text.lastIndexOf(97, -2), text.includes("b a")); +console.log(text.indexOf(""), text.indexOf("", 4), text.indexOf("", 100), text.lastIndexOf(""), text.indexOf("x")); +console.log(text.indexOf(Buffer.from("a\nab")), text.indexOf("ab\nab"), text.indexOf("ab\nabc"), text.lastIndexOf("aab", 4)); + +// A window into a larger buffer never sees the bytes outside it. +const whole = Buffer.from("xxneedlexx needle needlexx"); +const window = whole.subarray(3, 22); +console.log(window.toString(), window.indexOf("needle"), window.lastIndexOf("needle"), window.indexOf("xx"), window.includes("xne")); +console.log(window.indexOf("eedle", 1), window.lastIndexOf("eed"), window.indexOf("needlex"), window.lastIndexOf("n", 0)); + +// A long search with many first-byte candidates. +const long = Buffer.alloc(20000, "ab"); +long.write("abc", 19990); +console.log(long.indexOf("abc"), long.lastIndexOf("aba"), long.indexOf("abc", 19991), long.lastIndexOf("abc"), long.indexOf("bb")); +const lines: number[] = []; +const fasta = Buffer.from(">ONE\nACGT\nTTGA\n>TWO\nGGCC\n"); +for (let at = fasta.indexOf(10); at !== -1; at = fasta.indexOf(10, at + 1)) lines.push(at); +console.log(lines.join(","), fasta.indexOf(62, 1), fasta.lastIndexOf(62)); diff --git a/tests/corpus/7249-stream-writev.cjs b/tests/corpus/7249-stream-writev.cjs new file mode 100644 index 000000000..fb6d01960 --- /dev/null +++ b/tests/corpus/7249-stream-writev.cjs @@ -0,0 +1,126 @@ +// Batched writes in JavaScript: the writev option in a literal and in an +// options value, and _writev on a constructor function's prototype. The +// batch entries carry chunk, encoding and callback (Node's nop when the +// write passed none), allBuffers says whether every chunk is a Buffer, +// and a stream with only _writev sends a lone write as { chunk, encoding }. +const stream = require("node:stream"); +const util = require("node:util"); + +const log = []; +const describe = (chunks) => + chunks + .map((c) => { + const chunk = Buffer.isBuffer(c.chunk) ? "B:" + c.chunk.toString() : `${typeof c.chunk}:${c.chunk}`; + const callback = typeof c.callback === "function" ? c.callback.name || "fn" : String(c.callback); + return `${chunk}/${c.encoding}/${callback}`; + }) + .join(",") + + ` all=${chunks.allBuffers} keys=${Object.keys(chunks[0]).join(".")}`; + +function viaLiteral(next) { + const w = new stream.Writable({ + write(chunk, enc, cb) { + log.push("write " + chunk); + setTimeout(cb, 1); + }, + writev(chunks, cb) { + log.push("writev " + describe(chunks)); + setTimeout(cb, 1); + }, + }); + w.write("a", () => log.push("cb a")); + w.write("b", () => log.push("cb b")); + w.write("c"); + w.write(Buffer.from("d"), () => log.push("cb d")); + w.end(() => log.push("end")); + w.on("finish", () => { + log.push("finish"); + next(); + }); +} + +function dyn(value) { + return value; +} + +function viaValue(next) { + const options = { + writev(chunks, cb) { + log.push("value " + describe(chunks)); + cb(); + }, + }; + const v = new stream.Writable(dyn(options)); + v.cork(); + v.write("x"); + v.write("6869", "hex"); + v.uncork(); + v.write("z", () => log.push("z cb")); + v.end(); + v.on("finish", () => { + log.push("value finish"); + next(); + }); +} + +function Sink(opts) { + stream.Writable.call(this, opts); +} +util.inherits(Sink, stream.Writable); +Sink.prototype._writev = function (chunks, cb) { + log.push("sink " + (this instanceof Sink) + " " + describe(chunks)); + cb(); +}; + +function viaConstructor(next) { + const s = new Sink({ decodeStrings: false }); + s.cork(); + s.write("a"); + s.write("6869", "hex"); + s.write(Buffer.from("b")); + s.uncork(); + s.write("lone"); + s.end(() => { + log.push("sink end"); + next(); + }); +} + +// The writev option shadows the prototype's _writev: Node's constructor +// assigns it to the instance (this._writev = options.writev). +function viaOptionOverProto(next) { + function W(opts) { + stream.Writable.call(this, opts); + } + Object.setPrototypeOf(W.prototype, stream.Writable.prototype); + Object.setPrototypeOf(W, stream.Writable); + W.prototype._write = function (chunk, enc, cb) { + log.push("proto _write " + String(chunk)); + cb(); + }; + W.prototype._writev = function (chunks, cb) { + log.push("proto _writev " + chunks.length); + cb(); + }; + // (built without a second `new` of a constructor function) + const w = Object.create(W.prototype); + W.call(w, { + writev(chunks, cb) { + log.push("option writev " + chunks.map((c) => String(c.chunk)).join(",") + " " + (this === w)); + cb(); + }, + }); + log.push("own _writev " + Object.prototype.hasOwnProperty.call(w, "_writev")); + w.cork(); + w.write("a"); + w.write("b"); + w.uncork(); + w.end(() => { + log.push("option end"); + next(); + }); +} + +viaLiteral(() => + viaValue(() => viaConstructor(() => viaOptionOverProto(() => console.log(log.join("\n"))))), +); diff --git a/tests/corpus/7250-stream-decode-strings.ts b/tests/corpus/7250-stream-decode-strings.ts new file mode 100644 index 000000000..57e84aff2 --- /dev/null +++ b/tests/corpus/7250-stream-decode-strings.ts @@ -0,0 +1,89 @@ +// decodeStrings: false and the write-side default encoding in typed code: +// a string write reaches _write and _transform as the string with its +// encoding (its UTF-16 length counts toward writableLength), Buffers keep +// 'buffer', defaultEncoding decides how write(string) encodes (and the +// name _write sees when strings stay strings), and setDefaultEncoding() +// lowercases, validates and chains. +import { PassThrough, Transform, Writable } from "node:stream"; + +const log: string[] = []; + +function describe(chunk: string | Buffer, enc: string): string { + return typeof chunk === "string" ? `string:${chunk}/${enc}` : `B:${chunk.toString("hex")}/${enc}`; +} + +class Keeper extends Writable { + seen: string[] = []; + constructor() { + super({ decodeStrings: false, highWaterMark: 4 }); + } + _write(chunk: string | Buffer, enc: string, cb: (error?: Error | null) => void): void { + this.seen.push(describe(chunk, enc)); + setTimeout(cb, 1); + } +} + +async function main(): Promise { + const keeper = new Keeper(); + log.push("ret " + keeper.write("héllo")); + log.push("length " + keeper.writableLength); + keeper.write("6869", "hex"); + keeper.write(Buffer.from("zz")); + keeper.end("end", "latin1"); + await new Promise((resolve) => keeper.on("finish", resolve)); + log.push("keeper " + keeper.seen.join(" ")); + + const hex = new Writable({ + defaultEncoding: "hex", + write(chunk: Buffer, enc: string, cb: (error?: Error | null) => void) { + log.push(`hex ${chunk.toString()} ${enc}`); + cb(); + }, + }); + hex.write("6869"); + hex.write("41", "utf8"); + hex.setDefaultEncoding("BASE64").write("aGk="); + try { + hex.setDefaultEncoding("nope"); + } catch (e) { + log.push(`setDefaultEncoding ${(e as NodeJS.ErrnoException).code} ${(e as Error).message}`); + } + log.push("chains " + (hex.setDefaultEncoding("utf8") === hex)); + hex.end("ok"); + + const both = new Writable({ + decodeStrings: false, + defaultEncoding: "LATIN1", + write(chunk: string | Buffer, enc: string, cb: (error?: Error | null) => void) { + log.push("both " + describe(chunk, enc)); + cb(); + }, + }); + both.write("x"); + both.setDefaultEncoding("UTF-8"); + both.write("y"); + both.end(); + + const t = new Transform({ + decodeStrings: false, + transform(chunk: string | Buffer, enc: string, cb: (error: Error | null, data?: string | Buffer) => void) { + log.push("transform " + describe(chunk, enc)); + cb(null, chunk); + }, + }); + t.on("data", (c: Buffer) => log.push("transform out " + c.toString("hex"))); + t.write("abc"); + t.end("ff", "hex"); + + const p = new PassThrough({ decodeStrings: false }); + p.on("data", (c: Buffer) => log.push("pass " + c.toString())); + p.end("q"); + + try { + new Writable({ defaultEncoding: "bogus" }); + } catch (e) { + log.push("constructor " + (e as NodeJS.ErrnoException).code); + } + setTimeout(() => console.log(log.join("\n")), 10); +} +main(); diff --git a/tests/corpus/7251-stream-write-encodings.mjs b/tests/corpus/7251-stream-write-encodings.mjs new file mode 100644 index 000000000..5c4242ca8 --- /dev/null +++ b/tests/corpus/7251-stream-write-encodings.mjs @@ -0,0 +1,104 @@ +// Write encodings in JavaScript: decodeStrings: false and defaultEncoding +// in options literals and options values, setDefaultEncoding(), and +// strings written in other encodings to constructor-function streams. +import * as stream from "node:stream"; +import { PassThrough, Transform, Writable } from "node:stream"; +import * as util from "node:util"; + +const log = []; +const describe = (c, e) => `${Buffer.isBuffer(c) ? "B:" + c.toString("hex") : typeof c + ":" + c}/${e}`; +const dyn = (value) => value; + +const keep = new Writable({ decodeStrings: false, highWaterMark: 4, write(c, e, cb) { log.push("keep " + describe(c, e)); cb(); } }); +log.push("ret " + keep.write("héllo") + " length " + keep.writableLength); +keep.write("6869", "hex"); +keep.write(Buffer.from("zz")); +keep.end("end", "latin1"); + +const valueOpts = { decodeStrings: false, defaultEncoding: "HEX", write(c, e, cb) { log.push("value " + describe(c, e)); cb(); } }; +const fromValue = new Writable(dyn(valueOpts)); +fromValue.write("ab"); +fromValue.write(Buffer.from("c")); + +const hex = new Writable({ defaultEncoding: "hex", write(c, e, cb) { log.push("hex " + describe(c, e)); cb(); } }); +hex.write("6869"); +hex.write("41", "utf8"); +hex.setDefaultEncoding("BASE64"); +hex.write("aGk="); +try { + hex.setDefaultEncoding("nope"); +} catch (e) { + log.push("setDefaultEncoding " + e.code + " " + e.message); +} +log.push("chains " + (hex.setDefaultEncoding("utf8") === hex)); + +const t = new Transform({ decodeStrings: false, transform(c, e, cb) { log.push("t " + describe(c, e)); cb(null, c); } }); +t.on("data", (c) => log.push("t data " + describe(c, "-"))); +t.write("abc"); +t.write("ff", "hex"); +const p = new PassThrough({ decodeStrings: false }); +p.on("data", (c) => log.push("p data " + describe(c, "-"))); +p.write("q"); + +const om = new Writable({ objectMode: true, defaultEncoding: "hex", write(c, e, cb) { log.push("om " + describe(c, e)); cb(); } }); +om.write("s"); +try { + new Writable({ defaultEncoding: "bogus" }); +} catch (e) { + log.push("constructor " + e.code); +} + +function Sink() { + stream.Writable.call(this); +} +util.inherits(Sink, stream.Writable); +Sink.prototype._write = function (c, e, cb) { + log.push("sink " + describe(c, e)); + cb(); +}; +const sink = new Sink(); +sink.write("6869", "hex"); +sink.setDefaultEncoding("latin1"); +sink.write("é"); +sink.end("YQ==", "base64"); + +// A typed array or DataView chunk reaches _write and _writev as a Buffer +// over the same bytes (Stream._uint8ArrayToBuffer), also with +// decodeStrings: false; in object mode it stays as it is. +const views = new stream.Writable({ + decodeStrings: false, + write(c, e, cb) { + log.push(`view ${Buffer.isBuffer(c)} ${e} ${c.toString()}`); + cb(); + }, +}); +views.write(new Uint8Array([104, 105])); +views.write(new Uint16Array([0x6a6b])); +views.write(new DataView(new Uint8Array([108]).buffer)); +const batch = new stream.Writable({ + writev(chunks, cb) { + log.push(`view batch ${chunks.map((c) => `${Buffer.isBuffer(c.chunk)}:${c.encoding}:${c.chunk}`).join(" ")}`); + cb(); + }, +}); +batch.cork(); +batch.write(new Uint8Array([97])); +batch.write(new Uint8Array([98])); +batch.uncork(); +const source = new Uint8Array([1, 2]); +new stream.Writable({ + write(c, e, cb) { + log.push(`view plain ${Buffer.isBuffer(c)} ${e} ${c.buffer === source.buffer}`); + cb(); + }, +}).write(source); +new stream.Writable({ + objectMode: true, + write(c, e, cb) { + log.push(`view object ${Buffer.isBuffer(c)} ${c instanceof Uint8Array}`); + cb(); + }, +}).write(new Uint8Array([3])); +log.push(`view source ${Buffer.isBuffer(source)}`); + +setTimeout(() => console.log(log.join("\n")), 5); diff --git a/tests/corpus/7252-duplex-from-generator.ts b/tests/corpus/7252-duplex-from-generator.ts new file mode 100644 index 000000000..488748021 --- /dev/null +++ b/tests/corpus/7252-duplex-from-generator.ts @@ -0,0 +1,69 @@ +// Duplex.from over an async generator function in typed code: the +// generator reads what is written to the Duplex and its values are the +// readable side. Both sides are in object mode with a highWaterMark of +// 1; each write's callback runs once the generator asks for the next +// value, and end() reaches the generator as the end of its source. +import { Duplex } from "node:stream"; + +const log: string[] = []; + +async function* numbered(source: AsyncIterable): AsyncGenerator { + let n = 0; + for await (const chunk of source) { + n++; + log.push(`gen got ${String(chunk)}`); + yield `${n}:${String(chunk)}`; + } + log.push("gen done"); + yield `total ${n}`; +} + +async function collect(d: Duplex): Promise { + const out: string[] = []; + for await (const chunk of d) out.push(String(chunk)); + return out; +} + +async function basic(): Promise { + const d = Duplex.from(numbered); + log.push(`sides ${d.readable} ${d.writable}`); + log.push(`object mode ${d.readableObjectMode} ${d.writableObjectMode}`); + log.push(`hwm ${d.readableHighWaterMark} ${d.writableHighWaterMark}`); + d.write("a", () => log.push("write a done")); + d.write("b", () => log.push("write b done")); + d.end("c", () => log.push("end done")); + const out = await collect(d); + log.push(`read ${out.join(",")}`); + log.push(`after ${d.readableEnded} ${d.writableFinished} ${d.destroyed}`); +} + +async function values(): Promise { + // object-mode writes keep their values + const d = Duplex.from(async function* (source: AsyncIterable): AsyncGenerator { + for await (const v of source) yield typeof v + " " + JSON.stringify(v); + }); + d.write({ k: 1 }); + d.write([2, 3]); + d.write(4); + d.end(); + log.push(`values ${(await collect(d)).join(" | ")}`); +} + +async function earlyDestroy(): Promise { + const d = Duplex.from(numbered); + d.on("error", (e: Error) => log.push(`error ${e.message}`)); + d.on("close", () => log.push(`close ${d.destroyed}`)); + d.write("x"); + await new Promise((resolve) => setTimeout(resolve, 5)); + d.destroy(new Error("stop")); + await new Promise((resolve) => setTimeout(resolve, 5)); +} + +async function main(): Promise { + await basic(); + await values(); + await earlyDestroy(); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7253-duplex-from.mjs b/tests/corpus/7253-duplex-from.mjs new file mode 100644 index 000000000..14556348a --- /dev/null +++ b/tests/corpus/7253-duplex-from.mjs @@ -0,0 +1,48 @@ +// Duplex.from in JavaScript: an async generator function, an array, a +// generator object and an async generator object. Over an iterable the +// result is read-only (writable false); over a function it reads what is +// written. The log records the order of the write callbacks, the reads +// and the stream events against each other. +import { Duplex } from "node:stream"; + +const log = []; + +async function* echo(source) { + for await (const chunk of source) { + log.push(`echo ${chunk}`); + yield chunk.toUpperCase(); + } +} + +const fromFn = Duplex.from(echo); +fromFn.on("finish", () => log.push("fn finish")); +fromFn.on("end", () => log.push("fn end")); +fromFn.on("close", () => log.push("fn close")); +fromFn.write("one", () => log.push("fn write one cb")); +fromFn.write("two", () => log.push("fn write two cb")); +fromFn.end(() => log.push("fn end cb")); +for await (const chunk of fromFn) log.push(`fn read ${chunk}`); + +const fromArray = Duplex.from(["p", "q"]); +log.push(`array ${fromArray.readable} ${fromArray.writable} ${fromArray.readableObjectMode}`); +for await (const chunk of fromArray) log.push(`array ${chunk}`); + +function* gen() { + yield "g1"; + yield "g2"; +} +const fromGen = Duplex.from(gen()); +log.push(`gen ${fromGen.writable}`); +for await (const chunk of fromGen) log.push(`gen ${chunk}`); + +async function* agen() { + yield 1; + await null; + yield 2; +} +const fromAgen = Duplex.from(agen()); +fromAgen.on("data", (v) => log.push(`agen data ${v}`)); +await new Promise((resolve) => fromAgen.on("end", resolve)); +log.push(`agen ended ${fromAgen.readableEnded}`); + +console.log(log.join("\n")); diff --git a/tests/corpus/7254-compose-functions.ts b/tests/corpus/7254-compose-functions.ts new file mode 100644 index 000000000..4a2ba7189 --- /dev/null +++ b/tests/corpus/7254-compose-functions.ts @@ -0,0 +1,56 @@ +// stream.compose with async generator function stages in typed code: a +// function at either end becomes Duplex.from of it, functions between +// streams run as pipeline stages, and one stage is Duplex.from of it. +// The composed Duplex writes to the first stage and reads from the last; +// its object modes follow the first stage's writable side and the last +// stage's readable side. +import { Duplex, PassThrough, Transform, compose } from "node:stream"; + +const log: string[] = []; + +async function* upper(source: AsyncIterable): AsyncGenerator { + for await (const chunk of source) yield String(chunk).toUpperCase(); +} + +async function* numbered(source: AsyncIterable): AsyncGenerator { + let n = 0; + for await (const chunk of source) yield `${++n}.${String(chunk)}`; +} + +async function drain(name: string, d: Duplex, chunks: string[]): Promise { + log.push(`${name} r=${d.readable} w=${d.writable} ro=${d.readableObjectMode} wo=${d.writableObjectMode}`); + const out: string[] = []; + d.on("data", (c: unknown) => out.push(String(c))); + const ended = new Promise((resolve) => d.on("end", () => resolve())); + for (const c of chunks) d.write(c); + d.end(); + await ended; + log.push(`${name} ${out.join(" ")}`); +} + +async function main(): Promise { + // functions only: Duplex.from at both ends + await drain("fns", compose(upper, numbered), ["a", "b"]); + // a function between two object-mode streams + await drain( + "middle", + compose(new PassThrough({ objectMode: true }), upper, new PassThrough({ objectMode: true })), + ["c", "d"], + ); + // a byte stream before a function: the Duplex writes bytes + const bytes = compose(new PassThrough(), numbered); + await drain("bytes", bytes, ["e"]); + // a function before a Transform + const tail = new Transform({ + objectMode: true, + transform(chunk: unknown, _enc: string, cb: (err?: Error | null, data?: unknown) => void): void { + cb(null, `<${String(chunk)}>`); + }, + }); + await drain("transform", compose(upper, tail), ["f", "g"]); + // one stage + await drain("single", compose(numbered), ["h", "i", "j"]); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7255-compose-stages.mjs b/tests/corpus/7255-compose-stages.mjs new file mode 100644 index 000000000..ef6542ece --- /dev/null +++ b/tests/corpus/7255-compose-stages.mjs @@ -0,0 +1,55 @@ +// stream.compose in JavaScript with an iterable or generator first stage +// (the Duplex is not writable), a stage that throws (the Duplex errors), +// a destroyed composition (the stages see an AbortError), and compose() +// with no stages (ERR_MISSING_ARGS). +import { PassThrough, compose } from "node:stream"; + +const log = []; + +const double = async function* (source) { + for await (const n of source) yield n * 2; +}; + +const fromArray = compose([1, 2, 3], double); +log.push(`array r=${fromArray.readable} w=${fromArray.writable}`); +for await (const n of fromArray) log.push(`array ${n}`); + +function* counter() { + yield 10; + yield 20; +} +const fromGen = compose(counter(), double, async function* (source) { + for await (const n of source) yield `n=${n}`; +}); +for await (const s of fromGen) log.push(`gen ${s}`); + +const failing = compose(new PassThrough({ objectMode: true }), async function* (source) { + for await (const chunk of source) { + if (chunk === "stop") throw new Error("stage failed on " + chunk); + yield chunk; + } +}, new PassThrough({ objectMode: true })); +failing.on("data", (c) => log.push(`failing data ${c}`)); +failing.on("error", (e) => log.push(`failing error ${e.message}`)); +failing.on("close", () => log.push(`failing close ${failing.destroyed}`)); +failing.write("go"); +failing.write("stop"); +await new Promise((resolve) => failing.on("close", resolve)); + +const stopped = compose(new PassThrough({ objectMode: true }), double); +stopped.on("error", (e) => log.push(`stopped error ${e.name} ${e.code}`)); +stopped.on("close", () => log.push("stopped close")); +stopped.write(4); +stopped.once("data", (n) => { + log.push(`stopped data ${n}`); + stopped.destroy(); +}); +await new Promise((resolve) => stopped.on("close", resolve)); + +try { + compose(); +} catch (e) { + log.push(`empty ${e.code} ${e.message}`); +} + +console.log(log.join("\n")); diff --git a/tests/corpus/7256-readable-compose.mjs b/tests/corpus/7256-readable-compose.mjs new file mode 100644 index 000000000..a9622173e --- /dev/null +++ b/tests/corpus/7256-readable-compose.mjs @@ -0,0 +1,53 @@ +// readable.compose(stage, { signal }): compose(this, stage), with the +// signal aborting the composed Duplex. The stage may be an async +// generator function or a stream; the result reads like the stage's +// output and is not writable when the receiver heads it. +import { PassThrough, Readable, Transform } from "node:stream"; + +const log = []; + +const WORDS = ["alpha", "beta", "gamma"]; +const lengths = async function* (source) { + for await (const w of source) yield `${w}:${w.length}`; +}; + +const source = Readable.from(WORDS); +const viaFn = source.compose(lengths); +log.push(`fn r=${viaFn.readable} w=${viaFn.writable} ro=${viaFn.readableObjectMode}`); +for await (const s of viaFn) log.push(`fn ${s}`); + +const shout = new Transform({ + objectMode: true, + transform(chunk, _enc, cb) { + cb(null, chunk.toUpperCase()); + }, +}); +const source2 = Readable.from(WORDS); +const viaStream = source2.compose(shout); +log.push(`stream ${(await viaStream.toArray()).join(",")}`); + +const pass = new PassThrough({ objectMode: true }); +const chained = pass.compose(lengths).compose(async function* (source) { + for await (const s of source) yield `[${s}]`; +}); +chained.on("data", (s) => log.push(`chained ${s}`)); +pass.write("x"); +pass.end("yz"); +await new Promise((resolve) => chained.on("end", resolve)); + +const ac = new AbortController(); +const signal = ac.signal; +const slow = new Readable({ objectMode: true, read() {} }); +const aborted = slow.compose(lengths, { signal }); +aborted.on("data", (s) => log.push(`aborted data ${s}`)); +aborted.on("error", (e) => log.push(`aborted error ${e.name} ${e.code}`)); +aborted.on("close", () => log.push(`aborted close ${aborted.destroyed}`)); +slow.push("one"); +setTimeout(() => ac.abort(), 10); +await new Promise((resolve) => aborted.on("close", resolve)); + +const source3 = Readable.from(WORDS); +const unsignaled = source3.compose(lengths, undefined); +log.push(`undefined options ${(await unsignaled.toArray()).length}`); + +console.log(log.join("\n")); diff --git a/tests/corpus/7257-pipeline-generator-destination.mjs b/tests/corpus/7257-pipeline-generator-destination.mjs new file mode 100644 index 000000000..0996e06eb --- /dev/null +++ b/tests/corpus/7257-pipeline-generator-destination.mjs @@ -0,0 +1,61 @@ +// pipeline() whose destination is a function: an async generator +// destination is pumped into the object-mode PassThrough pipeline() +// returns, and an async function destination's resolved value is written +// to it (null and undefined are not). The callback runs with the error +// and the destination's value; a generator yielding null fails the +// pipeline with ERR_STREAM_NULL_VALUES. +import { Readable, pipeline } from "node:stream"; + +const log = []; + +const out = pipeline( + Readable.from(["a", "b", "c"]), + async function* (source) { + for await (const chunk of source) yield { chunk, at: log.length }; + }, + (err, value) => log.push(`gen cb ${err} ${value}`), +); +log.push(`gen out r=${out.readable} w=${out.writable} ro=${out.readableObjectMode} wo=${out.writableObjectMode}`); +for await (const item of out) log.push(`gen read ${JSON.stringify(item)}`); + +const summary = pipeline( + Readable.from([3, 4]), + async function* (source) { + for await (const n of source) yield n * n; + }, + async function (source) { + let total = 0; + for await (const n of source) total += n; + return { total }; + }, + (err, value) => log.push(`fn cb ${err} ${JSON.stringify(value)}`), +); +summary.on("data", (v) => log.push(`fn data ${JSON.stringify(v)}`)); +summary.on("end", () => log.push("fn end")); +await new Promise((resolve) => summary.on("close", resolve)); + +const nothing = pipeline( + Readable.from(["x"]), + async function (source) { + for await (const _ of source); + return undefined; + }, + (err) => log.push(`undefined cb ${err}`), +); +nothing.on("data", () => log.push("undefined data")); +nothing.on("end", () => log.push("undefined end")); +await new Promise((resolve) => nothing.on("close", resolve)); + +const bad = pipeline( + Readable.from(["g"]), + async function* (source) { + for await (const chunk of source) yield chunk; + yield null; + }, + (err) => log.push(`null cb ${err && err.code}`), +); +bad.on("data", (c) => log.push(`null data ${c}`)); +bad.on("error", (e) => log.push(`null error ${e.code}`)); +await new Promise((resolve) => bad.on("close", resolve)); + +console.log(log.join("\n")); diff --git a/tests/corpus/7258-pipeline-generator-destination.ts b/tests/corpus/7258-pipeline-generator-destination.ts new file mode 100644 index 000000000..b22b68dce --- /dev/null +++ b/tests/corpus/7258-pipeline-generator-destination.ts @@ -0,0 +1,47 @@ +// pipeline() with a function destination in typed code. @types/node and +// the fallback declarations type the result as the destination stream; +// for a function destination it is the object-mode PassThrough that +// pipeline() returns, read here as a PassThrough. +import { PassThrough, Readable, pipeline } from "node:stream"; + +const log: string[] = []; + +async function* tokens(source: AsyncIterable): AsyncGenerator { + for await (const line of source) { + for (const word of String(line).split(" ")) yield word; + } +} + +async function* tagged(source: AsyncIterable): AsyncGenerator<{ word: string; n: number }> { + let n = 0; + for await (const word of source) yield { word, n: ++n }; +} + +async function main(): Promise { + const out = pipeline(Readable.from(["to be", "or not"]), tokens, tagged, (err: Error | null) => { + log.push(`callback ${err === null || err === undefined ? "ok" : err.message}`); + }) as PassThrough; + log.push(`object mode ${out.readableObjectMode} ${out.writableObjectMode}`); + for await (const item of out) { + const entry = item as { word: string; n: number }; + log.push(`${entry.n} ${entry.word}`); + } + log.push(`ended ${out.readableEnded}`); + + const counted = pipeline( + Readable.from(["x", "y", "z"]), + async function (source: AsyncIterable): Promise { + let n = 0; + for await (const _ of source) n++; + return n; + }, + (err: Error | null) => log.push(`count callback ${err === null || err === undefined}`), + ) as PassThrough; + const values: unknown[] = []; + counted.on("data", (v: unknown) => values.push(v)); + await new Promise((resolve) => counted.on("end", () => resolve())); + log.push(`count values ${JSON.stringify(values)}`); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7259-promises-pipeline-stage-options.cjs b/tests/corpus/7259-promises-pipeline-stage-options.cjs new file mode 100644 index 000000000..a4a3ce396 --- /dev/null +++ b/tests/corpus/7259-promises-pipeline-stage-options.cjs @@ -0,0 +1,65 @@ +// stream/promises pipeline() with function stages or an iterable source +// and the options argument: end: false leaves the destination open, a +// signal aborts the pipeline (the stages see their own signal abort), an +// async function destination's value resolves the promise, and a value +// that is not an AbortSignal rejects with ERR_INVALID_ARG_TYPE. +const { PassThrough, Writable } = require("node:stream"); +const { pipeline } = require("node:stream/promises"); + +const log = []; + +const upper = async function* (source) { + for await (const chunk of source) yield String(chunk).toUpperCase(); +}; + +function sink(name) { + return new Writable({ + objectMode: true, + write(chunk, _enc, cb) { + log.push(`${name} got ${chunk}`); + cb(); + }, + }); +} + +async function main() { + const joined = await pipeline(["a", "b"], upper, async function (source) { + let s = ""; + for await (const c of source) s += c; + return s; + }, { end: true }); + log.push(`joined ${joined}`); + + const open = new PassThrough({ objectMode: true }); + await pipeline(["k"], upper, open, { end: false }); + log.push(`end false: ended ${open.writableEnded}`); + open.end("last"); + const rest = []; + for await (const c of open) rest.push(c); + log.push(`end false: read ${rest.join(",")}`); + + const ac = new AbortController(); + const slow = async function* (source, { signal }) { + signal.addEventListener("abort", () => log.push(`stage signal aborted ${signal.aborted}`)); + for await (const chunk of source) { + yield chunk; + await new Promise((resolve) => setTimeout(resolve, 30)); + } + }; + const aborting = pipeline(["s1", "s2", "s3"], slow, sink("abort"), { signal: ac.signal }); + setTimeout(() => ac.abort(), 10); + try { + await aborting; + } catch (e) { + log.push(`aborted ${e.name} ${e.code}`); + } + + try { + await pipeline(["v"], upper, sink("bad"), { signal: "nope" }); + } catch (e) { + log.push(`bad signal ${e.code}`); + } + console.log(log.join("\n")); +} + +main(); diff --git a/tests/corpus/7380-iterable-values.ts b/tests/corpus/7380-iterable-values.ts new file mode 100644 index 000000000..0c4284285 --- /dev/null +++ b/tests/corpus/7380-iterable-values.ts @@ -0,0 +1,122 @@ +// Iterable values: an array, Set, Map or string stays the same object +// (multi-pass, same identity, same inspection), and a generator or a +// Map/Set/array iterator stays single-pass. for-of, spread, Array.from and +// the Set/Map constructors consume every source. + +function sum(xs: Iterable): number { + let s = 0; + for (const x of xs) s += x; + return s; +} + +function* nums(): Generator { + yield 10; + yield 20; +} + +const arr = [1, 2, 3]; +const scores = new Map([ + ["a", 4], + ["b", 5], +]); +console.log(sum(arr), sum(new Set([1, 1, 2])), sum(scores.values()), sum(nums()), sum(arr.values())); + +// An array stays the same multi-pass object. +const it: Iterable = arr; +console.log(sum(it), sum(it), it === arr, typeof it, it); + +// A generator stays single-pass. +const once: Iterable = nums(); +console.log(sum(once), sum(once), typeof once); + +console.log([...it], Array.from(nums() as Iterable), new Set(it).size); + +// A function returning either a collection iterator or a fresh array. +function pick(rows: Map, ids: string[] | null): Iterable { + if (ids === null) return rows.values(); + const out: number[] = []; + for (const id of ids) out.push(rows.get(id) ?? 0); + return out; +} +let total = 0; +for (const v of pick(scores, null)) total += v; +for (const v of pick(scores, ["b", "b", "z"])) total += v; +console.log(total); + +// Strings iterate by code point; Maps iterate [key, value] entries. +function upper(s: Iterable): string[] { + const out: string[] = []; + for (const w of s) out.push(w.toUpperCase()); + return out; +} +console.log(upper("héllo😀"), upper(["a", "b"]), upper(new Set(["x"]))); +function pairs(p: Iterable<[string, number]>): string { + let out = ""; + for (const [k, v] of p) out += `${k}=${v};`; + return out; +} +console.log(pairs(scores), pairs([["x", 1]]), pairs(scores.entries()), pairs(new Set<[string, number]>([["s", 9]]))); + +// Constructors and Object.fromEntries take any Iterable. +function toMap(p: Iterable<[string, number]>): Map { + return new Map(p); +} +function toSet(xs: Iterable): Set { + return new Set(xs); +} +console.log(toMap([["a", 1], ["b", 2]]), toMap(scores), Object.fromEntries(toMap(new Set<[string, number]>([["c", 3]])))); +console.log(toSet([3, 3, 4]), toSet(new Set([9])), toSet(nums())); + +// Fields, generator functions declared to return Iterable, nested Iterables. +class Holder { + source: Iterable; + constructor(source: Iterable) { + this.source = source; + } + joined(): string { + let out = ""; + for (const s of this.source) out += s; + return out; + } +} +const holder = new Holder(["a", "b"]); +console.log(holder.joined(), holder.joined()); +holder.source = new Set(["x", "y"]); +console.log(holder.joined(), holder); + +function* range(n: number): Iterable { + for (let i = 0; i < n; i++) yield i; +} +console.log([...range(4)], sum(range(5))); + +const grid: Iterable[] = [[1, 2], new Set([3]), range(3)]; +let cells = 0; +for (const row of grid) for (const c of row) cells += c; +console.log(cells, grid.length); + +// A checked cast of an iterator value. +const entries = new Map([["k", 1]]).entries() as Iterable<[string, number]>; +console.log(toMap(entries)); + +// Optional Iterables narrow to the Iterable once proven present. +function totalOf(xs?: Iterable): number { + if (xs === undefined) return -1; + let s = 0; + for (const x of xs) s += x; + return s; +} +function orEmpty(xs: Iterable | null): number[] { + return xs === null ? [] : [...xs]; +} +console.log(totalOf(), totalOf([1, 2]), totalOf(new Set([5])), orEmpty(null), orEmpty(range(2))); +const byName = new Map>([ + ["a", [1, 2]], + ["b", new Set([3])], +]); +for (const k of ["a", "b", "c"]) { + const got = byName.get(k); + if (got !== undefined) console.log(k, [...got]); + else console.log(k, "none"); +} +const top = byName.get("a"); +console.log(top !== undefined ? [...top] : "none", byName); diff --git a/tests/corpus/7381-iterable-forof-semantics.ts b/tests/corpus/7381-iterable-forof-semantics.ts new file mode 100644 index 000000000..10f1d1d2f --- /dev/null +++ b/tests/corpus/7381-iterable-forof-semantics.ts @@ -0,0 +1,122 @@ +// for-of over Iterable keeps the semantics of the held source: an array +// is walked live, a generator is closed on break, return and throw (its +// finally blocks run) but not when it is exhausted or throws itself, and +// labels, closures, await and yield work in the loop body. + +function* noisy(tag: string): Generator { + try { + yield 1; + yield 2; + yield 3; + } finally { + console.log("closed", tag); + } +} + +const early: Iterable = noisy("break"); +for (const v of early) { + if (v === 2) break; + console.log("v", v); +} + +function firstOver(src: Iterable, n: number): number | undefined { + for (const x of src) if (x > n) return x; + return undefined; +} +console.log(firstOver([1, 5, 9], 4), firstOver(noisy("return"), 1), firstOver(noisy("exhausted"), 9)); + +try { + for (const v of noisy("throw") as Iterable) { + if (v === 1) throw new Error("boom"); + } +} catch (e) { + console.log("caught", (e as Error).message); +} + +function* failing(): Generator { + try { + yield 1; + throw new RangeError("generator failed"); + } finally { + console.log("failing finally"); + } +} +try { + for (const v of failing() as Iterable) console.log("got", v); +} catch (e) { + console.log(String(e)); +} + +// The array is read live: elements pushed during the walk are visited. +const grow: number[] = [1, 2]; +let visits = 0; +for (const v of grow as Iterable) { + if (grow.length < 5) grow.push(v * 10); + visits++; +} +console.log(visits, grow); + +// Holes read undefined when T holds it. +function show(xs: Iterable): string { + const out: string[] = []; + for (const x of xs) out.push(String(x)); + return out.join(","); +} +console.log(show([1, , 3]), show([undefined, 2])); + +// Labels, closures over the per-iteration binding, try/finally, a class +// declared in the body, and an existing binding as the loop target. +function run(src: Iterable, tag: string): void { + const fns: (() => number)[] = []; + let acc = 0; + outer: for (const x of src) { + fns.push(() => x * 10); + try { + if (x === 2) continue outer; + class Local { + v = x; + twice(): number { + return this.v * 2; + } + } + acc += new Local().twice(); + const inner = (y: number): number => y + x; + acc += inner(1); + } finally { + acc += 100; + } + } + console.log(tag, acc, fns.map((f) => f()).join(",")); +} +function* three(): Generator { + yield 1; + yield 2; + yield 3; +} +run([1, 2, 3], "array"); +run(three(), "generator"); +run(new Set([1, 2, 3]), "set"); + +let product = 0; +outer: for (const a of [1, 2, 3] as Iterable) { + for (const b of new Set([10, 20]) as Iterable) { + if (b === 20) continue outer; + if (a === 3) break outer; + product += a * b; + } +} +let target = 0; +for (target of [7, 8] as Iterable) { +} +console.log(product, target); + +// await and yield inside the loop body. +async function asyncSum(src: Iterable): Promise { + let s = 0; + for (const x of src) s += await Promise.resolve(x); + return s; +} +function* doubled(src: Iterable): Generator { + for (const x of src) yield x * 2; +} +asyncSum([4, 5]).then((v) => console.log("async", v, [...doubled([1, 2])], [...doubled(new Set([7]))])); diff --git a/tests/corpus/7382-iterable-generic-repository.ts b/tests/corpus/7382-iterable-generic-repository.ts new file mode 100644 index 000000000..3e9e62742 --- /dev/null +++ b/tests/corpus/7382-iterable-generic-repository.ts @@ -0,0 +1,57 @@ +// A generic repository whose query candidates are an Iterable: the +// live row map's values() iterator when nothing is indexed, or an array of +// index hits. Methods returning Iterable keep the class compilable. + +interface Row { + readonly id: string; + readonly tag: string; + readonly n: number; +} + +class Repository { + private readonly rows = new Map(); + private readonly index = new Map(); + + private readonly key: (row: T) => string; + + constructor(key: (row: T) => string) { + this.key = key; + } + + insert(row: T): void { + this.rows.set(row.id, row); + const k = this.key(row); + const ids = this.index.get(k); + if (ids === undefined) this.index.set(k, [row.id]); + else ids.push(row.id); + } + + all(): Iterable { + return this.rows.values(); + } + + candidates(key: string | null): Iterable { + if (key === null) return this.rows.values(); + const out: T[] = []; + for (const id of this.index.get(key) ?? []) { + const row = this.rows.get(id); + if (row !== undefined) out.push(row); + } + return out; + } + + query(key: string | null, where: (row: T) => boolean): T[] { + const matches: T[] = []; + for (const row of this.candidates(key)) if (where(row)) matches.push(row); + return matches; + } +} + +const repo = new Repository((row) => row.tag); +for (let i = 0; i < 12; i++) repo.insert({ id: `r${i}`, tag: i % 3 === 0 ? "red" : "blue", n: i * i }); +console.log(repo.query(null, (r) => r.n > 50).map((r) => r.id).join(",")); +console.log(repo.query("red", (r) => r.n % 2 === 0).map((r) => r.id).join(",")); +console.log(repo.query("green", () => true).length, [...repo.all()].length); +let sum = 0; +for (const row of repo.candidates("blue")) sum += row.n; +console.log(sum, Array.from(repo.candidates(null)).slice(0, 2)); diff --git a/tests/corpus/7383-iterable-union-sources.ts b/tests/corpus/7383-iterable-union-sources.ts new file mode 100644 index 000000000..3c393b256 --- /dev/null +++ b/tests/corpus/7383-iterable-union-sources.ts @@ -0,0 +1,51 @@ +// Iterable unions without an array arm, and optional or nullable Iterable +// slots that take a generator whose return channel is not void. + +function* evens(limit: number): Generator { + for (let i = 0; i <= limit; i += 2) yield i; +} + +// A union of a Set and a generator is an iterable union with no array arm: +// for-of walks every arm through the generator loop. +function total(src: Set | Generator): number { + let sum = 0; + for (const v of src) sum += v; + return sum; +} +console.log(total(new Set([1, 2, 3])), total(evens(6))); + +const labelled = (src: Set | Generator): string[] => { + const out: string[] = []; + outer: for (const v of src) { + if (v > 4) break outer; + out.push(`#${v}`); + } + return out; +}; +console.log(labelled(new Set([1, 3, 5])).join(" "), labelled(evens(10)).join(" ")); + +// A generator that returns a value: its return channel is dropped when it +// flows into an Iterable, required, optional or nullable alike. +function* countdown(from: number): Generator { + for (let i = from; i > 0; i--) yield i; + return "liftoff"; +} + +function joined(xs: Iterable): string { + return [...xs].join(","); +} + +function maybeJoined(xs?: Iterable): string { + return xs === undefined ? "none" : [...xs].join(","); +} + +function nullableJoined(xs: Iterable | null): string { + if (xs === null) return "null"; + let out = ""; + for (const x of xs) out += `${x};`; + return out; +} + +console.log(joined(countdown(3))); +console.log(maybeJoined(countdown(2)), maybeJoined(), maybeJoined(undefined), maybeJoined([7, 8])); +console.log(nullableJoined(countdown(4)), nullableJoined(null), nullableJoined(new Set([9]))); diff --git a/tests/corpus/7400-record-keyof-union-read.ts b/tests/corpus/7400-record-keyof-union-read.ts new file mode 100644 index 000000000..4a62a5067 --- /dev/null +++ b/tests/corpus/7400-record-keyof-union-read.ts @@ -0,0 +1,41 @@ +// Keyed reads of records whose fields have different types, with a key +// typed as `keyof` the record: the read is the union of the field types. +interface Book { + readonly id: string; + readonly title: string; + readonly year: number; + readonly tags: readonly string[]; + readonly available: boolean; +} +type BookPatch = Partial>; + +function changedKeys(current: Book, patch: BookPatch): string[] { + const changed: string[] = []; + for (const key of Object.keys(patch) as (keyof BookPatch)[]) { + const before = current[key]; + const after = patch[key]; + if (after === undefined) continue; + if (Array.isArray(before) && Array.isArray(after)) { + if (before.join("|") !== after.join("|")) changed.push(key); + } else if (before !== after) changed.push(key); + } + return changed; +} + +function describe(book: Book, key: keyof Book): string { + const value = book[key]; + if (typeof value === "string") return `${key}: string ${value}`; + if (typeof value === "number") return `${key}: number ${value.toFixed(1)}`; + if (typeof value === "boolean") return `${key}: boolean ${value ? "yes" : "no"}`; + return `${key}: list of ${value.length} [${value.join(", ")}]`; +} + +const book: Book = { id: "b1", title: "Atlas", year: 1999, tags: ["maps", "travel"], available: true }; +console.log(changedKeys(book, { title: "Atlas", year: 2001 })); +console.log(changedKeys(book, { tags: ["maps", "travel"], available: false })); +console.log(changedKeys(book, { tags: ["maps"], title: "Atlas II", year: undefined })); +for (const key of ["id", "title", "year", "tags", "available"] as const) console.log(describe(book, key)); +const keys: (keyof Book)[] = ["year", "tags"]; +const values = keys.map((k) => book[k]); +console.log(values, values.length); +console.log(JSON.stringify(Object.fromEntries(keys.map((k) => [k, book[k]])))); diff --git a/tests/corpus/7420-for-await-close.ts b/tests/corpus/7420-for-await-close.ts new file mode 100644 index 000000000..52930a02c --- /dev/null +++ b/tests/corpus/7420-for-await-close.ts @@ -0,0 +1,100 @@ +// Leaving a for await loop over an async generator early closes it, as +// AsyncIteratorClose does: return, throw and break inside the body, a +// labeled continue to an outer loop, and a return() at a yield in the +// body all await the generator's return(), so its finally runs before the +// loop's caller continues. An exhausted generator and a rejected next() +// are not closed. After a throw, an error from closing is dropped; after +// a break or return it propagates. +const log: string[] = []; + +async function* numbers(tag: string, failClose = false): AsyncGenerator { + try { + yield 1; + yield 2; + yield 3; + } finally { + log.push(`${tag} finally`); + if (failClose) throw new Error(`${tag} close failed`); + } +} + +async function* slowClose(tag: string): AsyncGenerator { + try { + yield 1; + } finally { + await null; + log.push(`${tag} finally after await`); + } +} + +async function firstOf(tag: string): Promise { + for await (const n of numbers(tag)) return n * 10; + return -1; +} + +async function throwsInside(tag: string): Promise { + for await (const n of numbers(tag)) throw new Error(`${tag} body ${n}`); +} + +async function* takeOne(tag: string): AsyncGenerator { + for await (const n of numbers(tag)) { + yield n; + return; + } +} + +async function* rethrowing(tag: string): AsyncGenerator { + for await (const n of numbers(tag)) { + if (n === 2) throw new Error(`${tag} at ${n}`); + yield `${tag} ${n}`; + } +} + +async function main(): Promise { + log.push(`first ${await firstOf("return")}`); + try { + await throwsInside("throw"); + } catch (e) { + log.push(`caught ${(e as Error).message}`); + } + for await (const n of takeOne("generator-return")) log.push(`took ${n}`); + try { + for await (const s of rethrowing("generator-throw")) log.push(s); + } catch (e) { + log.push(`caught ${(e as Error).message}`); + } + const early = takeOne("consumer-return"); + for await (const n of early) { + log.push(`consumer got ${n}`); + break; + } + try { + for await (const n of numbers("break-close-fails", true)) { + log.push(`break at ${n}`); + break; + } + } catch (e) { + log.push(`caught ${(e as Error).message}`); + } + try { + for await (const n of numbers("throw-close-fails", true)) throw new Error(`body ${n}`); + } catch (e) { + log.push(`caught ${(e as Error).message}`); + } + void Promise.resolve().then(() => log.push("microtask")); + for await (const n of slowClose("slow")) { + log.push(`slow ${n}`); + break; + } + log.push("after slow"); + outer: for (const k of [1, 2]) { + for await (const n of numbers(`labeled ${k}`)) { + log.push(`labeled ${k} got ${n}`); + continue outer; + } + } + for await (const n of numbers("exhausted")) log.push(`exhausted ${n}`); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7421-for-await-close.mjs b/tests/corpus/7421-for-await-close.mjs new file mode 100644 index 000000000..c891a2084 --- /dev/null +++ b/tests/corpus/7421-for-await-close.mjs @@ -0,0 +1,53 @@ +// The same early exits in JavaScript: a for await loop that returns, +// throws or breaks closes its async generator, including one that reaches +// the loop as an untyped parameter, and a source whose return() is still +// running finishes before the code after the loop. +const log = []; + +async function* letters(tag) { + try { + yield "a"; + yield "b"; + } finally { + log.push(`${tag} finally`); + } +} + +async function firstLetter(source) { + for await (const c of source) return c; +} + +async function failOn(source, target) { + for await (const c of source) { + if (c === target) throw new Error(`failed on ${c}`); + log.push(`kept ${c}`); + } +} + +async function* upperUntil(source, stop) { + for await (const c of source) { + if (c === stop) return; + yield c.toUpperCase(); + } +} + +log.push(`first ${await firstLetter(letters("param-return"))}`); +try { + await failOn(letters("param-throw"), "b"); +} catch (e) { + log.push(`caught ${e.message}`); +} +for await (const c of upperUntil(letters("stage-return"), "b")) log.push(`upper ${c}`); +let ticks = 0; +const tick = () => { + log.push(`tick ${ticks}`); + if (++ticks < 4) process.nextTick(tick); +}; +process.nextTick(tick); +for await (const c of letters("break")) { + log.push(`break ${c}`); + break; +} +log.push("after break"); +await new Promise((resolve) => setTimeout(resolve, 1)); +console.log(log.join("\n")); diff --git a/tests/corpus/7422-for-await-untyped-streams.mjs b/tests/corpus/7422-for-await-untyped-streams.mjs new file mode 100644 index 000000000..b7dd21072 --- /dev/null +++ b/tests/corpus/7422-for-await-untyped-streams.mjs @@ -0,0 +1,38 @@ +// for await over a native stream that reaches the loop as an untyped +// value iterates it as a typed loop does, in the same tick order as Node: +// whole reads, an early return (the stream is destroyed), a break, an +// object-mode Transform and a stream error. +import { Readable, PassThrough, Transform } from "node:stream"; +const log = []; +async function readAll(s, tag) { + for await (const c of s) log.push(`${tag} ${typeof c} ${String(c)}`); + log.push(`${tag} done`); +} +async function first(s, tag) { + for await (const c of s) return `${tag} first ${String(c)}`; +} +async function breaks(s, tag) { + for await (const c of s) { log.push(`${tag} got ${String(c)}`); break; } +} +async function failing(s, tag) { + try { for await (const c of s) log.push(`${tag} ${c}`); } catch (e) { log.push(`${tag} caught ${e.message}`); } +} +let n = 0; +const ticker = () => { log.push(`tick ${n}`); if (++n < 6) process.nextTick(ticker); }; +process.nextTick(ticker); +await readAll(Readable.from(["a", "b"]), "arr"); +const r = Readable.from(["x", "y", "z"]); +r.on("close", () => log.push(`r close ${r.destroyed}`)); +log.push(await first(r, "r")); +const pt = new PassThrough(); +pt.on("close", () => log.push("pt close")); +pt.end("bytes"); +await breaks(pt, "pt"); +const t = new Transform({ objectMode: true, transform(c, e, cb) { cb(null, { v: c }); } }); +t.end(1); +await (async (s) => { for await (const c of s) log.push(`t ${JSON.stringify(c)}`); })(t); +const bad = new Readable({ read() { this.destroy(new Error("boom")); } }); +await failing(bad, "bad"); +await readAll(["plain", "array"], "dyn"); +await new Promise((res) => setTimeout(res, 5)); +console.log(log.join("\n")); diff --git a/tests/corpus/7423-stream-untyped-bindings.mjs b/tests/corpus/7423-stream-untyped-bindings.mjs new file mode 100644 index 000000000..23b9e1411 --- /dev/null +++ b/tests/corpus/7423-stream-untyped-bindings.mjs @@ -0,0 +1,33 @@ +// Streams held where the lowering keeps a checked-dynamic value: a +// module-level binding of a stream method's result on a value that a +// JavaScript function returns, an element read of an array of streams, +// and readable.compose on a stream whose function is typed with JSDoc. +import { PassThrough, Readable, Transform } from "node:stream"; + +const words = () => Readable.from(["alpha", "beta"]); +const shouted = words().map((w) => w.toUpperCase()); +console.log("shouted", shouted.readable, shouted.readableObjectMode); +for await (const w of shouted) console.log("shouted", w); + +const counted = words().filter((w) => w.length > 4); +counted.on("data", (w) => console.log("counted", w)); +await new Promise((resolve) => counted.on("end", resolve)); +console.log("counted ended", counted.readableEnded); + +const streams = [new PassThrough({ objectMode: true }), new Transform({ + objectMode: true, + transform(chunk, _enc, cb) { + cb(null, { wrapped: chunk }); + }, +})]; +streams[1].end(7); +for await (const v of streams[1]) console.log("element", JSON.stringify(v)); +streams[0].end("done"); +for await (const v of streams[0]) console.log("element", v); + +/** @returns {Readable} */ +const typedWords = () => Readable.from(["x", "yy"]); +const lengths = typedWords().compose(async function* (source) { + for await (const w of source) yield `${w}:${w.length}`; +}); +console.log("composed", (await lengths.toArray()).join(",")); diff --git a/tests/corpus/7424-duplex-from-streams.mjs b/tests/corpus/7424-duplex-from-streams.mjs new file mode 100644 index 000000000..6f5d43460 --- /dev/null +++ b/tests/corpus/7424-duplex-from-streams.mjs @@ -0,0 +1,47 @@ +// Duplex.from over a lone Readable, a lone Writable and a { readable, +// writable } pair, as duplexify.js's _duplexify wraps them: the Duplex +// reads through the Readable and writes through the Writable, takes each +// side's object mode from the stream behind it, and closes once both ends +// finish; destroying it without an error destroys the streams behind it. +import { Duplex, PassThrough, Readable, Writable } from "node:stream"; +const log = []; +const r = Readable.from(["a", "b"]); +const dr = Duplex.from(r); +log.push(`dr ${dr.readable} ${dr.writable} ${dr.readableObjectMode} ${dr.writableObjectMode}`); +dr.on("end", () => log.push("dr end")); +dr.on("close", () => log.push("dr close")); +for await (const c of dr) log.push(`dr ${c}`); +const got = []; +const w = new Writable({ write(c, e, cb) { got.push(String(c)); log.push(`w write ${c}`); cb(); } }); +const dw = Duplex.from(w); +log.push(`dw ${dw.readable} ${dw.writable} ${dw.writableObjectMode}`); +dw.on("finish", () => log.push("dw finish")); +dw.on("close", () => log.push("dw close")); +dw.write("x", () => log.push("dw cb x")); +dw.end("y", () => log.push("dw end cb")); +await new Promise((res) => dw.on("close", res)); +const inner = new PassThrough({ objectMode: true }); +const out = new PassThrough({ objectMode: true }); +const pair = Duplex.from({ readable: out, writable: inner }); +log.push(`pair ${pair.readable} ${pair.writable} ${pair.readableObjectMode} ${pair.writableObjectMode}`); +inner.on("data", (v) => { log.push(`inner ${JSON.stringify(v)}`); out.write({ echo: v }); }); +inner.on("finish", () => { log.push("inner finish"); out.end(); }); +pair.on("data", (v) => log.push(`pair data ${JSON.stringify(v)}`)); +pair.write({ n: 1 }); +pair.end({ n: 2 }); +await new Promise((res) => pair.on("close", res)); +log.push("pair closed"); +const killed = Duplex.from(new PassThrough()); +killed.on("error", (e) => log.push(`killed error ${e.name}`)); +killed.on("close", () => log.push(`killed close ${killed.destroyed}`)); +killed.destroy(); +await new Promise((res) => setTimeout(res, 5)); +// The pair's members are evaluated in source order; a pair still open at +// exit is left as it is. +const order = []; +const open = Duplex.from({ + writable: (order.push("writable"), new Writable({ write(c, e, cb) { cb(); } })), + readable: (order.push("readable"), Readable.from(["x"])), +}); +log.push(`pair order ${order.join(",")} ${open.readable} ${open.writable}`); +console.log(log.join("\n")); diff --git a/tests/corpus/7425-duplex-from-streams.ts b/tests/corpus/7425-duplex-from-streams.ts new file mode 100644 index 000000000..a9d85b797 --- /dev/null +++ b/tests/corpus/7425-duplex-from-streams.ts @@ -0,0 +1,72 @@ +// Duplex.from over streams in typed code: a Readable that fails destroys +// the Duplex with its error, a slow Writable's callbacks and the end +// callback follow its pace, a pair whose writable member is an async +// generator function's Duplex, compose() with an async function as its +// last stage, and a Duplex over a promise. +import { Duplex, PassThrough, Readable, Writable, compose } from "node:stream"; +const log: string[] = []; +async function main(): Promise { + const failing = new Readable({ read(): void { this.destroy(new Error("source failed")); } }); + const df = Duplex.from(failing); + df.on("error", (e: Error) => log.push(`df error ${e.message}`)); + df.on("close", () => log.push(`df close ${df.destroyed}`)); + df.resume(); + await new Promise((res) => df.on("close", res)); + const slow = new Writable({ + highWaterMark: 2, + write(c: Buffer, _e: string, cb: (err?: Error | null) => void): void { + log.push(`slow write ${c.toString()}`); + setTimeout(cb, 2); + }, + }); + const ds = Duplex.from(slow); + log.push(`write1 ${ds.write("aa", () => log.push("cb aa"))}`); + log.push(`write2 ${ds.write("bb", () => log.push("cb bb"))}`); + ds.on("drain", () => log.push("ds drain")); + ds.end(() => log.push("ds end cb")); + await new Promise((res) => ds.on("finish", res)); + log.push("ds finished"); + const upper = new PassThrough(); + const dp = Duplex.from({ + readable: Readable.from(["r1", "r2"]), + writable: async function* (source: AsyncIterable): AsyncGenerator { + for await (const c of source) log.push(`gen got ${String(c)}`); + yield "unused"; + }, + }); + dp.on("data", (c: unknown) => log.push(`dp data ${String(c)}`)); + dp.write("w1"); + dp.end(); + await new Promise((res) => dp.on("end", res)); + upper.end(); + await more(); + for (const line of log) console.log(line); +} +async function more(): Promise { + const sink = compose(new PassThrough({ objectMode: true }), async function (source: AsyncIterable): Promise { + for await (const c of source) log.push(`sink ${String(c)}`); + }); + log.push(`sink r=${sink.readable} w=${sink.writable}`); + sink.write("p"); + sink.end("q"); + await new Promise((res) => sink.on("finish", res)); + log.push("sink finished"); + const fromReadable: Duplex = Duplex.from(Readable.from([1, 2])); + const values: unknown[] = []; + for await (const v of fromReadable) values.push(v); + log.push(`readable ${JSON.stringify(values)}`); + const written: string[] = []; + const target = new Writable({ + write(c: Buffer, _e: string, cb: (err?: Error | null) => void): void { + written.push(c.toString()); + cb(); + }, + }); + const fromWritable: Duplex = Duplex.from(target); + fromWritable.end("only"); + await new Promise((res) => fromWritable.on("finish", res)); + log.push(`writable ${written.join(",")}`); + const value: Duplex = Duplex.from(Promise.resolve("resolved")); + for await (const v of value) log.push(`promise ${String(v)}`); +} +void main(); diff --git a/tests/corpus/7426-duplex-from-promise.mjs b/tests/corpus/7426-duplex-from-promise.mjs new file mode 100644 index 000000000..46d19586b --- /dev/null +++ b/tests/corpus/7426-duplex-from-promise.mjs @@ -0,0 +1,27 @@ +// Duplex.from over a promise: a Duplex that is not writable and reads the +// promise's value once it settles (nothing for null), then ends; a +// rejection destroys it with the reason. The events interleave with +// microtasks and nextTick callbacks as in Node. +import { Duplex } from "node:stream"; +const log = []; +const tick = (n) => { if (n < 5) { log.push(`tick ${n}`); process.nextTick(() => tick(n + 1)); } }; +const later = (v, ms) => new Promise((res) => setTimeout(() => res(v), ms)); +const a = Duplex.from(Promise.resolve({ answer: 42 })); +log.push(`a ${a.readable} ${a.writable} ${a.readableObjectMode}`); +a.on("data", (v) => log.push(`a data ${JSON.stringify(v)}`)); +a.on("end", () => log.push("a end")); +a.on("close", () => log.push("a close")); +Promise.resolve().then(() => log.push("microtask 1")).then(() => log.push("microtask 2")); +tick(0); +await new Promise((res) => a.on("close", res)); +const b = Duplex.from(later(null, 1)); +b.on("data", () => log.push("b data")); +b.on("end", () => log.push("b end")); +await new Promise((res) => b.on("close", res)); +const c = Duplex.from(Promise.reject(new Error("nope"))); +c.on("error", (e) => log.push(`c error ${e.message}`)); +c.on("close", () => log.push(`c close ${c.destroyed}`)); +await new Promise((res) => c.on("close", res)); +const d = Duplex.from(later("text", 1)); +for await (const v of d) log.push(`d ${v}`); +console.log(log.join("\n")); diff --git a/tests/corpus/7427-duplex-from-async-function.mjs b/tests/corpus/7427-duplex-from-async-function.mjs new file mode 100644 index 000000000..66ce35f9a --- /dev/null +++ b/tests/corpus/7427-duplex-from-async-function.mjs @@ -0,0 +1,40 @@ +// Duplex.from over an async function: a Duplex that is only writable, whose +// writes reach the function's source and whose end waits for the +// function's promise. A value other than null or undefined is +// ERR_INVALID_RETURN_VALUE, a return before the input is consumed aborts +// the Duplex, and a throw destroys it. +import { Duplex } from "node:stream"; +const log = []; +const sink = Duplex.from(async function (source) { + for await (const c of source) log.push(`sink got ${c}`); + log.push("sink done"); +}); +log.push(`sink ${sink.readable} ${sink.writable} ${sink.writableObjectMode}`); +sink.on("finish", () => log.push("sink finish")); +sink.on("close", () => log.push("sink close")); +sink.write("a", () => log.push("cb a")); +sink.end("b", () => log.push("end cb")); +await new Promise((res) => sink.on("close", res)); +const bad = Duplex.from(async function (source) { + for await (const c of source) log.push(`bad got ${c}`); + return 5; +}); +bad.on("error", (e) => log.push(`bad error ${e.code} ${e.message}`)); +bad.on("close", () => log.push("bad close")); +bad.end("x"); +await new Promise((res) => bad.on("close", res)); +const early = Duplex.from(async function (source) { + for await (const c of source) { log.push(`early got ${c}`); break; } +}); +early.on("error", (e) => log.push(`early error ${e.name}`)); +early.on("close", () => log.push(`early close ${early.destroyed}`)); +early.write("1"); +early.write("2"); +await new Promise((res) => early.on("close", res)); +const failing = Duplex.from(async function (source) { + for await (const c of source) throw new Error("fail " + c); +}); +failing.on("error", (e) => log.push(`failing error ${e.message}`)); +failing.write("z"); +await new Promise((res) => failing.on("close", res)); +console.log(log.join("\n")); diff --git a/tests/corpus/7428-web-stream-sources.mjs b/tests/corpus/7428-web-stream-sources.mjs new file mode 100644 index 000000000..72d98e5f8 --- /dev/null +++ b/tests/corpus/7428-web-stream-sources.mjs @@ -0,0 +1,41 @@ +// A web ReadableStream as an async iterable source: Readable.from reads +// its chunks through the stream's iterator (pull-based and start-based +// sources, Uint8Array chunks in byte mode, an error from the controller, +// and an early destroy that cancels the stream), and it heads compose() +// and pipeline() like any async iterable. +import { Readable, Writable, compose, pipeline } from "node:stream"; +const log = []; +let n = 0; +const pulled = new ReadableStream({ + pull(c) { n++; log.push(`pull ${n}`); if (n <= 3) c.enqueue(`p${n}`); else c.close(); }, + cancel(reason) { log.push(`cancel ${reason}`); }, +}); +for await (const c of Readable.from(pulled)) log.push(`got ${c}`); +const bytes = new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("hi")); c.enqueue(new TextEncoder().encode("!")); c.close(); } }); +const br = Readable.from(bytes, { objectMode: false }); +br.on("data", (b) => log.push(`bytes ${Buffer.isBuffer(b)} ${b.toString()}`)); +await new Promise((res) => br.on("end", res)); +const failing = new ReadableStream({ start(c) { c.enqueue("ok"); c.error(new Error("web failed")); } }); +const fr = Readable.from(failing); +fr.on("data", (c) => log.push(`failing data ${c}`)); +fr.on("error", (e) => log.push(`failing error ${e.message}`)); +await new Promise((res) => fr.on("close", res)); +let m = 0; +const endless = new ReadableStream({ + pull(c) { c.enqueue(`e${++m}`); }, + cancel(reason) { log.push(`endless cancel ${reason === undefined ? "undefined" : reason}`); }, +}); +const er = Readable.from(endless); +er.once("data", (c) => { log.push(`endless ${c}`); er.destroy(); }); +await new Promise((res) => er.on("close", res)); +await new Promise((res) => setTimeout(res, 5)); +const head = compose(new ReadableStream({ start(c) { c.enqueue("c1"); c.enqueue("c2"); c.close(); } }), async function* (s) { for await (const c of s) yield `${c}!`; }); +log.push(`compose ${(await head.toArray()).join(",")}`); +await new Promise((resolve) => + pipeline( + new ReadableStream({ start(c) { c.enqueue("q1"); c.enqueue("q2"); c.close(); } }), + new Writable({ objectMode: true, write(c, e, cb) { log.push(`pipeline ${c}`); cb(); } }), + (err) => { log.push(`pipeline callback ${err}`); resolve(undefined); }, + ), +); +console.log(log.join("\n")); diff --git a/tests/corpus/7429-web-stream-sources.ts b/tests/corpus/7429-web-stream-sources.ts new file mode 100644 index 000000000..8243b6873 --- /dev/null +++ b/tests/corpus/7429-web-stream-sources.ts @@ -0,0 +1,42 @@ +// Web ReadableStreams as sources in typed code: Readable.from over a +// ReadableStream, interleaved with nextTick callbacks, and a +// ReadableStream heading compose() into an async generator. +import { Readable, compose } from "node:stream"; + +const log: string[] = []; + +function words(): ReadableStream { + return new ReadableStream({ + start(controller): void { + controller.enqueue("typed"); + controller.enqueue("web"); + controller.close(); + }, + }); +} + +async function main(): Promise { + let n = 0; + const tick = (): void => { + log.push(`tick ${n}`); + if (++n < 4) process.nextTick(tick); + }; + process.nextTick(tick); + const r = Readable.from(words()); + log.push(`object mode ${r.readableObjectMode}`); + for await (const c of r) log.push(`word ${String(c)}`); + const bytes = new ReadableStream({ + start(controller): void { + controller.enqueue(new TextEncoder().encode("ab")); + controller.enqueue(new TextEncoder().encode("cd")); + controller.close(); + }, + }); + const lengths = compose(bytes, async function* (source: AsyncIterable): AsyncGenerator { + for await (const chunk of source) yield `${chunk.length}`; + }); + log.push(`lengths ${(await lengths.toArray()).join(",")}`); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7430-readable-from-web.mjs b/tests/corpus/7430-readable-from-web.mjs new file mode 100644 index 000000000..017556f9d --- /dev/null +++ b/tests/corpus/7430-readable-from-web.mjs @@ -0,0 +1,42 @@ +// Readable.fromWeb: a Readable over a web ReadableStream's reader, in +// byte mode by default (Uint8Array chunks arrive as Buffers), in object +// mode, and with an encoding; an early destroy cancels the stream with +// null, and an error from the stream destroys the Readable. Duplex.from +// and compose() over a lone web ReadableStream read it the same way. +import { Duplex, Readable, compose } from "node:stream"; +const log = []; +const tick = (n) => { if (n < 6) { log.push(`tick ${n}`); process.nextTick(() => tick(n + 1)); } }; +const web = new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("ab")); c.enqueue(new TextEncoder().encode("cd")); c.close(); } }); +const r = Readable.fromWeb(web); +log.push(`r ${r.readableObjectMode} ${r.readableHighWaterMark}`); +r.on("data", (b) => log.push(`data ${Buffer.isBuffer(b)} ${b.toString()}`)); +r.on("end", () => log.push("end")); +r.on("close", () => log.push("close")); +tick(0); +await new Promise((res) => r.on("close", res)); +const words = new ReadableStream({ start(c) { c.enqueue("x"); c.enqueue("y"); c.close(); } }); +const o = Readable.fromWeb(words, { objectMode: true, highWaterMark: 1 }); +for await (const v of o) log.push(`obj ${v}`); +const text = new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("héllo")); c.close(); } }); +const t = Readable.fromWeb(text, { encoding: "utf8" }); +for await (const v of t) log.push(`text ${typeof v} ${v}`); +let m = 0; +const endless = new ReadableStream({ pull(c) { c.enqueue(new Uint8Array([++m])); }, cancel(reason) { log.push(`cancel ${reason && reason.name}`); } }); +const e = Readable.fromWeb(endless); +e.once("data", (b) => { log.push(`endless ${b[0]}`); e.destroy(); }); +e.on("close", () => log.push("endless close")); +await new Promise((res) => e.on("close", res)); +await new Promise((res) => setTimeout(res, 5)); +const failing = new ReadableStream({ start(c) { c.error(new Error("web broke")); } }); +const f = Readable.fromWeb(failing); +f.on("error", (err) => log.push(`failing error ${err.message}`)); +f.on("close", () => log.push("failing close")); +f.resume(); +await new Promise((res) => f.on("close", res)); +const d = Duplex.from(new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("dx")); c.close(); } })); +log.push(`d ${d.readable} ${d.writable} ${d.readableObjectMode}`); +d.on("data", (b) => log.push(`d data ${b.toString()}`)); +await new Promise((res) => d.on("close", res)); +const one = compose(new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("one")); c.close(); } })); +for await (const b of one) log.push(`one ${b.toString()}`); +console.log(log.join("\n")); diff --git a/tests/corpus/7431-readable-from-web.ts b/tests/corpus/7431-readable-from-web.ts new file mode 100644 index 000000000..23e7d2871 --- /dev/null +++ b/tests/corpus/7431-readable-from-web.ts @@ -0,0 +1,47 @@ +// Readable.fromWeb in typed code: a ReadableStream read as +// Buffers, a ReadableStream in object mode with a highWaterMark of +// 1, and an AbortSignal that destroys the Readable and cancels the stream. +import { Readable } from "node:stream"; + +const log: string[] = []; + +async function main(): Promise { + const bytes = new ReadableStream({ + start(controller): void { + controller.enqueue(new TextEncoder().encode("web")); + controller.enqueue(new TextEncoder().encode("bytes")); + controller.close(); + }, + }); + const r = Readable.fromWeb(bytes); + for await (const chunk of r) log.push(`bytes ${Buffer.isBuffer(chunk)} ${String(chunk)}`); + const words = new ReadableStream({ + start(controller): void { + controller.enqueue("alpha"); + controller.enqueue("beta"); + controller.close(); + }, + }); + const o = Readable.fromWeb(words, { objectMode: true, highWaterMark: 1 }); + log.push(`object mode ${o.readableObjectMode} ${o.readableHighWaterMark}`); + for await (const word of o) log.push(`word ${String(word)}`); + const ac = new AbortController(); + const signal = ac.signal; + let n = 0; + const endless = new ReadableStream({ + pull(controller): void { + controller.enqueue(new Uint8Array([++n])); + }, + cancel(reason: unknown): void { + log.push(`cancelled ${(reason as Error).name}`); + }, + }); + const a = Readable.fromWeb(endless, { signal }); + a.on("error", (e: Error) => log.push(`aborted ${e.name}`)); + a.once("data", () => ac.abort()); + await new Promise((resolve) => a.on("close", () => resolve())); + await new Promise((resolve) => setTimeout(resolve, 5)); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7432-readable-push-views.mjs b/tests/corpus/7432-readable-push-views.mjs new file mode 100644 index 000000000..9aca435b6 --- /dev/null +++ b/tests/corpus/7432-readable-push-views.mjs @@ -0,0 +1,16 @@ +// readable.push() in byte mode turns a Uint8Array or another typed array +// into a Buffer over the same bytes (Stream._uint8ArrayToBuffer); the +// pushed array itself is left as it was. +import { Readable } from "node:stream"; +const u = new Uint8Array([100, 101]); +const r = new Readable({ read() {} }); +r.push(u); r.push(new Uint16Array([0x6766])); r.push(null); +for await (const b of r) console.log(Buffer.isBuffer(b), b.toString(), b.length); +console.log("source still", Buffer.isBuffer(u)); +const r1 = new Readable({ read() {} }); +r1.push(new Uint8Array([97, 98])); +r1.push(null); +for await (const b of r1) console.log(Buffer.isBuffer(b), b.toString()); +const r2 = new Readable({ read() {} }); +r2.on("data", (b) => console.log("data", Buffer.isBuffer(b), b.toString())); +r2.push(new Uint8Array([99])); r2.push(null); diff --git a/tests/corpus/7433-readable-operator-signal.mjs b/tests/corpus/7433-readable-operator-signal.mjs new file mode 100644 index 000000000..0fc23c2d0 --- /dev/null +++ b/tests/corpus/7433-readable-operator-signal.mjs @@ -0,0 +1,41 @@ +// The Readable operators pass their callbacks { signal } as the second +// argument; map and filter's signal aborts when the options' signal +// does, reduce's signal is aborted once reduce settles, and map may +// return any value. +import { Readable } from "node:stream"; +const log = []; +log.push(JSON.stringify(await Readable.from([1, 2, 3]).map((n) => ({ n, sq: n * n })).toArray())); +log.push(JSON.stringify(await Readable.from(["a", "bb"]).map((s) => s.length).toArray())); +log.push(JSON.stringify(await Readable.from([1, 2]).map((n, { signal }) => `${n}:${signal.aborted}`).toArray())); +log.push(JSON.stringify(await Readable.from([1, 2, 3, 4]).filter(async (n, opts) => n % 2 === 0 && !opts.signal.aborted).toArray())); +log.push(String(await Readable.from([1, 2, 3]).reduce((acc, n, { signal }) => acc + n + (signal.aborted ? 100 : 0), 0))); +let kept; +await Readable.from([1]).reduce((acc, n, { signal }) => { kept = signal; return acc + n; }, 0); +log.push(`reduce signal after ${kept.aborted}`); +let mapKept; +await Readable.from([1]).map((n, { signal }) => { mapKept = signal; return n; }).toArray(); +log.push(`map signal after ${mapKept.aborted}`); +const ac = new AbortController(); +const events = []; +const p = Readable.from([1, 2, 3]).map(async (n, { signal }) => { + signal.addEventListener("abort", () => events.push(`abort ${n}`)); + await new Promise((r) => setTimeout(r, 10)); + return n; +}, { signal: ac.signal }).toArray(); +setTimeout(() => ac.abort(), 5); +try { await p; } catch (e) { log.push(`map aborted ${e.name}`); } +log.push(events.join(",")); +const dyn = [Readable.from([4, 5])][0]; +log.push(JSON.stringify(await dyn.map((n, { signal }) => n + Number(signal.aborted)).toArray())); +let seen = 0; +await Readable.from([5, 6]).forEach((n, { signal }) => { if (signal) seen++; }); +log.push(`forEach ${seen}`); +log.push(String(await Readable.from([1, 2, 3]).some((n, { signal }) => n > 2 && !signal.aborted))); +log.push(String(await Readable.from([1, 2, 3]).find((n, { signal }) => n > 1 && !signal.aborted))); +log.push(String(await Readable.from([1, 2, 3]).every((n, { signal }) => n > 0 && !signal.aborted))); +// A callback forwarded through an untyped parameter gets { signal } too. +const mapWith = (fn) => Readable.from([1, 2]).map(fn).toArray(); +log.push(JSON.stringify(await mapWith((n, { signal }) => `${n}:${signal.aborted}`))); +const reduceWith = (fn) => Readable.from([1, 2]).reduce(fn, 0); +log.push(String(await reduceWith((acc, n, { signal }) => acc + n + (signal.aborted ? 10 : 0)))); +console.log(log.join("\n")); diff --git a/tests/corpus/7434-readable-operator-values.ts b/tests/corpus/7434-readable-operator-values.ts new file mode 100644 index 000000000..49f7a0fa9 --- /dev/null +++ b/tests/corpus/7434-readable-operator-values.ts @@ -0,0 +1,32 @@ +// Typed Readable operators: map returns numbers and records, flatMap +// takes generator functions and sets, the callbacks may declare the +// { signal } argument, and read() on an object-mode stream answers the +// values pushed. +import { Readable } from "node:stream"; +async function* pair(n: number): AsyncGenerator { + yield n; + yield n * 100; +} +function* letters(s: string): Generator { + for (const c of s) yield c; +} +async function main(): Promise { + const squares = await Readable.from([1, 2, 3]).map((n: number) => n * n).toArray(); + const records = await Readable.from(["a", "bb"]).map((s: string) => ({ s, len: s.length })).toArray(); + const pairs = await Readable.from([1, 2]).flatMap(pair).toArray(); + const chars = await Readable.from(["hi"]).flatMap(letters).toArray(); + const sets = await Readable.from([7]).flatMap((n: number) => new Set([n, n + 1])).toArray(); + const signals = await Readable.from([1]).map((n: number, opts?: { signal?: AbortSignal }) => `${n}:${opts?.signal?.aborted}`).toArray(); + const total = await Readable.from([1, 2]).reduce((acc: number, n: number, opts?: { signal?: AbortSignal }) => acc + n + (opts?.signal?.aborted ? 1 : 0), 0); + console.log(JSON.stringify({ squares, records, pairs, chars, sets, signals, total })); + const r = new Readable({ objectMode: true, read(): void {} }); + r.push({ a: 1 }); + r.push(5); + r.push(null); + r.once("readable", () => { + const first: unknown = r.read(); + const second: unknown = r.read(); + console.log(JSON.stringify(first), second, r.read()); + }); +} +void main(); diff --git a/tests/corpus/7435-readable-flatmap-iterables.mjs b/tests/corpus/7435-readable-flatmap-iterables.mjs new file mode 100644 index 000000000..f0ee79b5a --- /dev/null +++ b/tests/corpus/7435-readable-flatmap-iterables.mjs @@ -0,0 +1,29 @@ +// flatMap over what the callback returns: async and sync generators, a +// Set, a Map, a Readable, and a value that is not iterable (V8's +// TypeError); the generator's steps interleave with process.nextTick as +// in Node, breaking out of the loop runs the inner generator's finally, +// and an inner throw rejects toArray(). +import { Readable } from "node:stream"; +const log = []; +log.push(JSON.stringify(await Readable.from(["x"]).flatMap(async function* (s) { yield s; yield s + s; }).toArray())); +log.push(JSON.stringify(await Readable.from([2]).flatMap(function* (n) { yield n; yield -n; }).toArray())); +log.push(JSON.stringify(await Readable.from([1, 2]).flatMap((n) => new Set([n, n])).toArray())); +log.push(JSON.stringify(await Readable.from(["p"]).flatMap((s) => Readable.from([s, s.toUpperCase()])).toArray())); +log.push(JSON.stringify(await Readable.from([3]).flatMap((n) => new Map([[n, "v"]])).toArray())); +log.push(JSON.stringify(await Readable.from([{ k: 1 }]).flatMap((o) => [o, { k: o.k + 1 }]).toArray())); +try { await Readable.from([1]).flatMap(() => 5).toArray(); } catch (e) { log.push(`${e.name} ${e.message}`); } +try { await Readable.from([1]).flatMap(() => null).toArray(); } catch (e) { log.push(`${e.name} ${e.message}`); } +const order = []; +let n = 0; +const tick = () => { order.push(`t${n}`); if (++n < 5) process.nextTick(tick); }; +process.nextTick(tick); +for await (const v of Readable.from([1]).flatMap(async function* (k) { order.push("gen start"); yield k; order.push("gen mid"); yield k + 1; })) order.push(`v${v}`); +log.push(order.join(" ")); +const closed = []; +const early = Readable.from([1, 2]).flatMap(async function* (k) { try { yield k; yield k * 10; } finally { closed.push(`finally ${k}`); } }); +for await (const v of early) { closed.push(`got ${v}`); break; } +log.push(closed.join(" ")); +const thrown = []; +try { await Readable.from([1]).flatMap(async function* () { yield 1; throw new Error("inner boom"); }).toArray(); } catch (e) { thrown.push(e.message); } +log.push(thrown.join(" ")); +console.log(log.join("\n")); diff --git a/tests/corpus/7436-readable-read-any-value.mjs b/tests/corpus/7436-readable-read-any-value.mjs new file mode 100644 index 000000000..5e19b16c8 --- /dev/null +++ b/tests/corpus/7436-readable-read-any-value.mjs @@ -0,0 +1,17 @@ +// readable.read() answers any value on an object-mode stream, and a +// Buffer or a string (with an encoding) in byte mode. +import { Readable, PassThrough } from "node:stream"; +const r = new Readable({ objectMode: true, read() {} }); +r.push({ a: 1 }); r.push(2); r.push("three"); r.push([4]); r.push(null); +r.once("readable", () => { let v; while ((v = r.read()) !== null) console.log("read", JSON.stringify(v)); }); +const p = new PassThrough({ objectMode: true }); +p.write([1, 2]); +p.end(); +p.once("readable", () => console.log("pt", JSON.stringify(p.read()), p.read())); +const b = new PassThrough(); +b.end("bytes!"); +b.once("readable", () => { const c = b.read(3); console.log("bytes", Buffer.isBuffer(c), String(c), String(b.read())); }); +const e = new PassThrough(); +e.setEncoding("utf8"); +e.end("text"); +e.once("readable", () => { const t = e.read(); console.log("encoded", typeof t, t); }); diff --git a/tests/corpus/7437-duplex-from-signal.mjs b/tests/corpus/7437-duplex-from-signal.mjs new file mode 100644 index 000000000..263225bbb --- /dev/null +++ b/tests/corpus/7437-duplex-from-signal.mjs @@ -0,0 +1,54 @@ +// A Duplex.from body may take { signal } (duplexify.js's fromAsyncGen): +// destroy aborts it with the destroy error, or with an AbortError when +// there is none, and compose passes it to its function stages. +import { Duplex, Readable, compose } from "node:stream"; +import { once } from "node:events"; + +const d = Duplex.from(async function* (source, { signal }) { + console.log("body", typeof signal, signal.aborted); + signal.addEventListener("abort", () => console.log("abort event", signal.aborted, signal.reason)); + for await (const chunk of source) yield String(chunk).toUpperCase(); + console.log("source ended"); +}); +d.on("data", (c) => console.log("data", c)); +d.write("a"); +d.write("b"); +await new Promise((r) => setTimeout(r, 10)); +d.destroy(); +await once(d, "close"); +console.log("closed d"); + +const seen = []; +const fin = Duplex.from(async function (source, { signal }) { + signal.addEventListener("abort", () => console.log("abort", seen.join(","))); + for await (const chunk of source) seen.push(String(chunk)); + console.log("done", signal.aborted); +}); +fin.on("finish", () => console.log("finish")); +fin.write("1"); +fin.write("2"); +fin.end(); +await once(fin, "close"); +console.log("closed fin"); + +const boom = new Error("boom"); +const e = Duplex.from(async function (source, { signal }) { + signal.addEventListener("abort", () => console.log("e abort", String(signal.reason), signal.reason === boom)); + await new Promise((r) => setTimeout(r, 50)); +}); +e.on("error", (err) => console.log("e error", err === boom)); +setTimeout(() => e.destroy(boom), 5); +await new Promise((r) => e.on("close", r)); +console.log("closed e"); + +const c = compose( + async function* (source, { signal }) { + for await (const chunk of source) yield `${chunk}!${signal.aborted}`; + }, + async function* (source, { signal }) { + for await (const chunk of source) yield chunk.toUpperCase(); + }, +); +Readable.from(["a", "b"]).pipe(c); +c.on("data", (x) => console.log("compose", String(x))); +c.on("end", () => console.log("compose end")); diff --git a/tests/corpus/7438-duplex-from-signal.ts b/tests/corpus/7438-duplex-from-signal.ts new file mode 100644 index 000000000..358bd3e68 --- /dev/null +++ b/tests/corpus/7438-duplex-from-signal.ts @@ -0,0 +1,26 @@ +// Typed Duplex.from bodies that declare the options argument: an async +// generator function and an async function. +import { Duplex } from "node:stream"; + +const d = Duplex.from(async function* (source: AsyncIterable, opts: { signal: AbortSignal }) { + opts.signal.addEventListener("abort", () => console.log("aborted", opts.signal.aborted)); + for await (const chunk of source) { + yield `<${chunk}>`; + } +}); +d.on("data", (c: string) => console.log("data", c)); +d.on("end", () => console.log("end")); +d.on("close", () => { + console.log("close"); + const w = Duplex.from(async function (source: AsyncIterable, { signal }: { signal: AbortSignal }): Promise { + let n = 0; + for await (const chunk of source) n += chunk.length; + console.log("total", n, signal.aborted); + }); + w.on("finish", () => console.log("finish")); + w.on("close", () => console.log("w close")); + w.write("abc"); + w.end("de"); +}); +d.write("x"); +d.end("y"); diff --git a/tests/corpus/7439-readable-read-string-chunks.ts b/tests/corpus/7439-readable-read-string-chunks.ts new file mode 100644 index 000000000..2fc5dbed6 --- /dev/null +++ b/tests/corpus/7439-readable-read-string-chunks.ts @@ -0,0 +1,17 @@ +// A typed read() answers the strings a stream holds: the first value a +// lazy Readable.from pull over a parsed array produces, and the decoded +// text once _read calls setEncoding(). +import { Readable } from "node:stream"; +const r = Readable.from(JSON.parse('["j1", "j2"]')); +const chunk = r.read(); +console.log("first", typeof chunk, chunk === null ? "null" : chunk.toString("hex")); +r.once("readable", () => console.log("later", JSON.stringify(r.read()))); +const e = new Readable({ + read() { + this.setEncoding("utf8"); + this.push(Buffer.from("ab")); + this.push(null); + }, +}); +const text = e.read(); +console.log("encoded", typeof text, text === null ? "null" : text.toString("hex")); diff --git a/tests/coverage-fixtures/divergences.ts b/tests/coverage-fixtures/divergences.ts index 83c3ede49..0cf8bcae5 100644 --- a/tests/coverage-fixtures/divergences.ts +++ b/tests/coverage-fixtures/divergences.ts @@ -20,14 +20,18 @@ console.log(tagged.label); const view: Labeled = tagged; console.log(view === tagged); -// Key order: values of one type are built in two orders and the program -// observes them, so the type keeps its declaration order. +// Key order: values of one type are built in two orders, and a spread sets +// an optional key its source lacks, so the values cannot record their own +// orders and the type keeps its declaration order. interface Point { x: number; y: number; + label?: string; } const point: Point = { y: 1, x: 2 }; -console.log(JSON.stringify(point), JSON.stringify({ x: 3, y: 4 })); +const other: Point = { x: 3, y: 4 }; +const labeled: Point = { ...point, label: "p" }; +console.log(JSON.stringify(point), JSON.stringify(other), JSON.stringify(labeled)); // Text outside the built-in collation compares by code point natively. console.log(["б", "а"].sort((p, q) => p.localeCompare(q)).join(","), "яблоко".localeCompare("груша")); diff --git a/tests/diagnostics/destructuring-fences.ts b/tests/diagnostics/destructuring-fences.ts index e42f8a07a..0ee60579d 100644 --- a/tests/diagnostics/destructuring-fences.ts +++ b/tests/diagnostics/destructuring-fences.ts @@ -38,12 +38,12 @@ interface E1 { } interface E2 { kind: "two"; - b: string; + b: number; label: string; } function fromUnion(u: E1 | E2): string { - const { b } = u; // union source: narrow first - return b; + const { b } = u; // union source whose arms type the field differently: narrow first + return String(b); } console.log(fromUnion({ kind: "one", b: "s", count: 1 })); diff --git a/tests/diagnostics/iterable-sources.ts b/tests/diagnostics/iterable-sources.ts new file mode 100644 index 000000000..c93029cce --- /dev/null +++ b/tests/diagnostics/iterable-sources.ts @@ -0,0 +1,17 @@ +// An Iterable value holds an array, Set, Map, string or iterator. A +// class instance with its own [Symbol.iterator] and a typed array have no +// representation there, so the conversion site reports them by name. +class Countdown implements Iterable { + *[Symbol.iterator](): Iterator { + yield 2; + yield 1; + } +} +function sum(xs: Iterable): number { + let s = 0; + for (const x of xs) s += x; + return s; +} +console.log(sum(new Countdown())); +console.log(sum(new Uint8Array([1, 2]))); +export const marker: number = 1; diff --git a/tests/diagnostics/local-class-boundaries.ts b/tests/diagnostics/local-class-boundaries.ts index 2c43e9900..ac1afb2c2 100644 --- a/tests/diagnostics/local-class-boundaries.ts +++ b/tests/diagnostics/local-class-boundaries.ts @@ -1,10 +1,8 @@ function privateBrand() { return class { #value = 1; has(other: object): boolean { return #value in other; } }; } -function selfName() { return class Local { copy() { return new Local(); } }; } class Base {} function derived() { return class extends Base {}; } function generic() { return class { read(value: T): T { return value; } }; } privateBrand(); -selfName(); derived(); generic(); // End of unsupported local-class forms. diff --git a/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts b/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts deleted file mode 100644 index 03043a32f..000000000 --- a/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts +++ /dev/null @@ -1,49 +0,0 @@ -// The conditional-spread idiom inside `any`-typed object literals: -// `{ a, ...(c ? { k: v } : {}) }` builds one of two engine literals behind -// the test, so the key is truly ABSENT when the empty arm is taken — -// engine-exact (`typeof o.k` answers "undefined", JSON drops it), with the -// shared properties evaluating exactly once. - -function opts(aspect: string | undefined): any { - const o: any = { - mode: "M", - ...(aspect ? { cfg: { aspect } } : {}), - }; - return o; -} -const a = opts("1:1"); -console.log(`${a.mode} ${a.cfg.aspect} ${typeof a.cfg}`); -const b = opts(undefined); -console.log(`${b.mode} ${typeof b.cfg}`); - -// Multi-property carrier arms are fine in the island form. -function multi(on: boolean): any { - return { base: 1, ...(on ? { x: "X", y: "Y" } : {}) }; -} -const m1 = multi(true); -const m2 = multi(false); -console.log(`${m1.base} ${m1.x} ${m1.y}`); -console.log(`${m2.base} ${typeof m2.x} ${typeof m2.y}`); - -// Reversed orientation and string-literal keys. -function rev(quiet: boolean): any { - return { kind: "r", ...(quiet ? {} : { "log-level": "loud" }) }; -} -console.log(`${rev(false)["log-level"]}`); -console.log(`${typeof rev(true)["log-level"]}`); - -// The spread mid-literal: properties around it evaluate exactly once and -// land in engine key order. -let sides = 0; -function side(v: number): number { - sides++; - return v; -} -function ordered(flag: boolean): any { - const o: any = { first: side(1), ...(flag ? { mid: 2 } : {}), last: side(3) }; - return o; -} -const o1 = ordered(true); -const o2 = ordered(false); -console.log(`${o1.first} ${o1.mid} ${o1.last}`); -console.log(`${o2.first} ${typeof o2.mid} ${o2.last} ${sides}`); diff --git a/tests/harness/__snapshots__/child-process.ts.txt b/tests/harness/__snapshots__/child-process.ts.txt index f5f9f8f86..a1f2999b7 100644 --- a/tests/harness/__snapshots__/child-process.ts.txt +++ b/tests/harness/__snapshots__/child-process.ts.txt @@ -14,7 +14,7 @@ child-process.ts:27:14 - error SC1090: listeners returning a value (make the cal | ^~~~~~~~~~~~~~~ 28 | // Methods have no bound-value form — call on directly. -child-process.ts:29:7 - error SC2007: values of type '{ (event: "spawn", listener: () => void): void; (event: "exit", listener: (code: number | null, signal: string | null) => void): void; (event: "close", listener: (code: number | null, signal: string | null) => void): void; (event: "error", listener: (err: Error) => void): void; (event: "message", listener: (message:...' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature +child-process.ts:29:7 - error SC2007: values of type '{ (event: "spawn", listener: () => void): ChildProcess; (event: "exit", listener: (code: number | null, signal: string | null) => void): ChildProcess; (event: "close", listener: (code: number | null, signal: string | null) => void): ChildProcess; (event: "error", listener: (err: Error) => void): ChildProcess; (event...' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature 28 | // Methods have no bound-value form — call on directly. 29 | const f = c.on; diff --git a/tests/harness/__snapshots__/coverage-divergences.txt b/tests/harness/__snapshots__/coverage-divergences.txt index 66373655f..8ab286873 100644 --- a/tests/harness/__snapshots__/coverage-divergences.txt +++ b/tests/harness/__snapshots__/coverage-divergences.txt @@ -1,7 +1,7 @@ scriptc coverage tests/coverage-fixtures/divergences.ts - statements analyzed 18 - compile statically 18 (100%) + statements analyzed 20 + compile statically 20 (100%) in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them) fully static — all analyzed statements compile to native code. @@ -14,11 +14,11 @@ scriptc coverage tests/coverage-fixtures/divergences.ts at tests/coverage-fixtures/divergences.ts:20:23 hint: declare the parameter or binding with the value's full type ('Tagged'), or make the shape a class — class instances keep their identity when passed as a base class ×1 SC6002 this object's keys enumerate as 'x, y' natively (the declaration order of 'Point') but as 'y, x' under Node (insertion order), and the program observes the order (JSON.stringify, Object.keys, for-in … - at tests/coverage-fixtures/divergences.ts:29:22 + at tests/coverage-fixtures/divergences.ts:31:22 hint: write the properties in the declaration order of 'Point' (or reorder the declaration to match how objects are built) ×1 SC6003 localeCompare over 'яблоко' compares characters outside the built-in collation (Latin letters, digits, punctuation and symbols) by code point natively, while Node applies ICU's full collation - at tests/coverage-fixtures/divergences.ts:33:70 + at tests/coverage-fixtures/divergences.ts:37:70 hint: results agree with Node for Latin-script text; for other scripts compare explicitly (for example by code point with < and >) so both runtimes agree ×1 SC6004 this 'Circle' reaches a cast to the unrelated class 'Square' in the callee: natively the checked cast throws a TypeError when it executes, while Node reads the members structurally - at tests/coverage-fixtures/divergences.ts:48:20 + at tests/coverage-fixtures/divergences.ts:52:20 hint: narrow with 'instanceof Square' before casting, or cast to a base class or interface both classes share \ No newline at end of file diff --git a/tests/harness/__snapshots__/destructuring-fences.ts.txt b/tests/harness/__snapshots__/destructuring-fences.ts.txt index 743bb5657..41026a7c8 100644 --- a/tests/harness/__snapshots__/destructuring-fences.ts.txt +++ b/tests/harness/__snapshots__/destructuring-fences.ts.txt @@ -19,18 +19,18 @@ destructuring-fences.ts:31:9 - error SC1090: reading a property that has only a | ^~~~~~ 32 | console.log(onlyIn); -destructuring-fences.ts:45:9 - error SC1031: object destructuring of non-record values (the source is union-typed) are not supported yet +destructuring-fences.ts:45:11 - error SC1031: destructuring 'b' from a union whose arms type it differently (every arm must be a record or class with a same-typed field 'b' — narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) is not supported yet 44 | function fromUnion(u: E1 | E2): string { - 45 | const { b } = u; // union source: narrow first - | ^~~~~ - 46 | return b; + 45 | const { b } = u; // union source whose arms type the field differently: narrow first + | ^ + 46 | return String(b); -destructuring-fences.ts:46:10 - error SC2004: uses of 'b' inherit the blocker on its declaration +destructuring-fences.ts:46:17 - error SC2004: uses of 'b' inherit the blocker on its declaration - 45 | const { b } = u; // union source: narrow first - 46 | return b; - | ^ + 45 | const { b } = u; // union source whose arms type the field differently: narrow first + 46 | return String(b); + | ^ 47 | } hint: the declaration of 'b' did not compile — fix the diagnostic reported there and these sites clear with it diff --git a/tests/harness/__snapshots__/iterable-sources.ts.txt b/tests/harness/__snapshots__/iterable-sources.ts.txt new file mode 100644 index 000000000..cac17a49e --- /dev/null +++ b/tests/harness/__snapshots__/iterable-sources.ts.txt @@ -0,0 +1,13 @@ +iterable-sources.ts:15:17 - error SC1090: an Iterable holding a 'Countdown' (an Iterable value holds an array, Set, Map, string, or iterator) is not supported yet + + 14 | } + 15 | console.log(sum(new Countdown())); + | ^~~~~~~~~~~~~~~ + 16 | console.log(sum(new Uint8Array([1, 2]))); + +iterable-sources.ts:16:17 - error SC1090: an Iterable holding a 'Uint8Array' (an Iterable value holds an array, Set, Map, string, or iterator) is not supported yet + + 15 | console.log(sum(new Countdown())); + 16 | console.log(sum(new Uint8Array([1, 2]))); + | ^~~~~~~~~~~~~~~~~~~~~~ + 17 | export const marker: number = 1; \ No newline at end of file diff --git a/tests/harness/__snapshots__/local-class-boundaries.ts.txt b/tests/harness/__snapshots__/local-class-boundaries.ts.txt index 6706a1278..697b1b8ed 100644 --- a/tests/harness/__snapshots__/local-class-boundaries.ts.txt +++ b/tests/harness/__snapshots__/local-class-boundaries.ts.txt @@ -2,18 +2,11 @@ local-class-boundaries.ts:1:34 - error SC1090: function-local classes with priva 1 | function privateBrand() { return class { #value = 1; has(other: object): boolean { return #value in other; } }; } | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 2 | function selfName() { return class Local { copy() { return new Local(); } }; } + 2 | class Base {} -local-class-boundaries.ts:2:64 - error SC1090: named self references inside function-local classes are not supported yet +local-class-boundaries.ts:4:29 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet - 1 | function privateBrand() { return class { #value = 1; has(other: object): boolean { return #value in other; } }; } - 2 | function selfName() { return class Local { copy() { return new Local(); } }; } - | ^~~~~ - 3 | class Base {} - -local-class-boundaries.ts:5:29 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet - - 4 | function derived() { return class extends Base {}; } - 5 | function generic() { return class { read(value: T): T { return value; } }; } + 3 | function derived() { return class extends Base {}; } + 4 | function generic() { return class { read(value: T): T { return value; } }; } | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 6 | privateBrand(); \ No newline at end of file + 5 | privateBrand(); \ No newline at end of file diff --git a/tests/harness/__snapshots__/retired-engine-1123-any-conditional-spread.ts.txt b/tests/harness/__snapshots__/retired-engine-1123-any-conditional-spread.ts.txt deleted file mode 100644 index 707d1dfd9..000000000 --- a/tests/harness/__snapshots__/retired-engine-1123-any-conditional-spread.ts.txt +++ /dev/null @@ -1,6 +0,0 @@ -retired-engine-1123-any-conditional-spread.ts:21:21 - error SC1090: conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields) is not supported yet - - 20 | function multi(on: boolean): any { - 21 | return { base: 1, ...(on ? { x: "X", y: "Y" } : {}) }; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 22 | } \ No newline at end of file diff --git a/tests/harness/__snapshots__/spread-fences.ts.txt b/tests/harness/__snapshots__/spread-fences.ts.txt index 56f8b81ee..eaced4979 100644 --- a/tests/harness/__snapshots__/spread-fences.ts.txt +++ b/tests/harness/__snapshots__/spread-fences.ts.txt @@ -1,4 +1,4 @@ -spread-fences.ts:14:21 - error SC1090: conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields) is not supported yet +spread-fences.ts:14:21 - error SC1090: conditional spreads beyond `...(c ? { field: v } : {})` and `...(c ? record : {})` (one arm must be empty — spell other shapes as optional fields) is not supported yet 13 | const cond = true; 14 | const s1: Shape = { ...(cond ? { x: 1, y: 2 } : { x: 3, y: 4 }) }; diff --git a/tests/harness/corpus-costs/6913-builtin-constructor-shorthand.ts.json b/tests/harness/corpus-costs/6913-builtin-constructor-shorthand.ts.json new file mode 100644 index 000000000..3c9170116 --- /dev/null +++ b/tests/harness/corpus-costs/6913-builtin-constructor-shorthand.ts.json @@ -0,0 +1 @@ +{ "plain": 3202, "san": 4630 } diff --git a/tests/harness/corpus-costs/6914-dynamic-builtin-construct.ts.json b/tests/harness/corpus-costs/6914-dynamic-builtin-construct.ts.json new file mode 100644 index 000000000..a3f53b09c --- /dev/null +++ b/tests/harness/corpus-costs/6914-dynamic-builtin-construct.ts.json @@ -0,0 +1 @@ +{ "plain": 3064, "san": 5586 } diff --git a/tests/harness/corpus-costs/6915-dynamic-builtin-construct.mjs.json b/tests/harness/corpus-costs/6915-dynamic-builtin-construct.mjs.json new file mode 100644 index 000000000..0c65d046c --- /dev/null +++ b/tests/harness/corpus-costs/6915-dynamic-builtin-construct.mjs.json @@ -0,0 +1 @@ +{ "plain": 2821, "san": 4440 } diff --git a/tests/harness/corpus-costs/6916-local-class-static-self-reference.mjs.json b/tests/harness/corpus-costs/6916-local-class-static-self-reference.mjs.json new file mode 100644 index 000000000..0d9aa6b13 --- /dev/null +++ b/tests/harness/corpus-costs/6916-local-class-static-self-reference.mjs.json @@ -0,0 +1 @@ +{ "plain": 3135, "san": 4365 } diff --git a/tests/harness/corpus-costs/6917-local-class-expression-self-reference.ts.json b/tests/harness/corpus-costs/6917-local-class-expression-self-reference.ts.json new file mode 100644 index 000000000..2769f5b72 --- /dev/null +++ b/tests/harness/corpus-costs/6917-local-class-expression-self-reference.ts.json @@ -0,0 +1 @@ +{ "plain": 3074, "san": 4624 } diff --git a/tests/harness/corpus-costs/6918-local-class-expression-self-reference.mjs.json b/tests/harness/corpus-costs/6918-local-class-expression-self-reference.mjs.json new file mode 100644 index 000000000..04f903d8a --- /dev/null +++ b/tests/harness/corpus-costs/6918-local-class-expression-self-reference.mjs.json @@ -0,0 +1 @@ +{ "plain": 3050, "san": 4393 } diff --git a/tests/harness/corpus-costs/6919-local-class-inspect-name.mjs.json b/tests/harness/corpus-costs/6919-local-class-inspect-name.mjs.json new file mode 100644 index 000000000..78aea9163 --- /dev/null +++ b/tests/harness/corpus-costs/6919-local-class-inspect-name.mjs.json @@ -0,0 +1 @@ +{ "plain": 3058, "san": 4846 } diff --git a/tests/harness/corpus-costs/6921-local-subclass-override-dispatch.ts.json b/tests/harness/corpus-costs/6921-local-subclass-override-dispatch.ts.json new file mode 100644 index 000000000..0dd7e966b --- /dev/null +++ b/tests/harness/corpus-costs/6921-local-subclass-override-dispatch.ts.json @@ -0,0 +1 @@ +{ "plain": 2459, "san": 4630 } diff --git a/tests/harness/corpus-costs/6922-local-subclass-override-dispatch.mjs.json b/tests/harness/corpus-costs/6922-local-subclass-override-dispatch.mjs.json new file mode 100644 index 000000000..f2ba6c139 --- /dev/null +++ b/tests/harness/corpus-costs/6922-local-subclass-override-dispatch.mjs.json @@ -0,0 +1 @@ +{ "plain": 3216, "san": 5042 } diff --git a/tests/harness/corpus-costs/7014-js-never-array-literal-fields/main.ts.json b/tests/harness/corpus-costs/7014-js-never-array-literal-fields/main.ts.json new file mode 100644 index 000000000..c5bf12c38 --- /dev/null +++ b/tests/harness/corpus-costs/7014-js-never-array-literal-fields/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3004, "san": 3359 } diff --git a/tests/harness/corpus-costs/7020-conditional-spread-fields.ts.json b/tests/harness/corpus-costs/7020-conditional-spread-fields.ts.json new file mode 100644 index 000000000..ee564e79f --- /dev/null +++ b/tests/harness/corpus-costs/7020-conditional-spread-fields.ts.json @@ -0,0 +1 @@ +{ "plain": 3640, "san": 5580 } diff --git a/tests/harness/corpus-costs/7021-promise-all-uniform-tuple.ts.json b/tests/harness/corpus-costs/7021-promise-all-uniform-tuple.ts.json new file mode 100644 index 000000000..fba6fd530 --- /dev/null +++ b/tests/harness/corpus-costs/7021-promise-all-uniform-tuple.ts.json @@ -0,0 +1 @@ +{ "plain": 2510, "san": 3630 } diff --git a/tests/harness/corpus-costs/7022-promise-all-dynamic-arrays.ts.json b/tests/harness/corpus-costs/7022-promise-all-dynamic-arrays.ts.json new file mode 100644 index 000000000..c59bb483b --- /dev/null +++ b/tests/harness/corpus-costs/7022-promise-all-dynamic-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 3330, "san": 4690 } diff --git a/tests/harness/corpus-costs/7023-union-object-patterns.ts.json b/tests/harness/corpus-costs/7023-union-object-patterns.ts.json new file mode 100644 index 000000000..3f0a047e7 --- /dev/null +++ b/tests/harness/corpus-costs/7023-union-object-patterns.ts.json @@ -0,0 +1 @@ +{ "plain": 2440, "san": 3670 } diff --git a/tests/harness/corpus-costs/7024-async-return-fresh-collections.ts.json b/tests/harness/corpus-costs/7024-async-return-fresh-collections.ts.json new file mode 100644 index 000000000..6285a25c5 --- /dev/null +++ b/tests/harness/corpus-costs/7024-async-return-fresh-collections.ts.json @@ -0,0 +1 @@ +{ "plain": 2570, "san": 4360 } diff --git a/tests/harness/corpus-costs/7025-child-listener-chaining.ts.json b/tests/harness/corpus-costs/7025-child-listener-chaining.ts.json new file mode 100644 index 000000000..44843dc06 --- /dev/null +++ b/tests/harness/corpus-costs/7025-child-listener-chaining.ts.json @@ -0,0 +1 @@ +{ "plain": 2510, "san": 3950 } diff --git a/tests/harness/corpus-costs/7026-url-object-patterns.ts.json b/tests/harness/corpus-costs/7026-url-object-patterns.ts.json new file mode 100644 index 000000000..9d3f28ab1 --- /dev/null +++ b/tests/harness/corpus-costs/7026-url-object-patterns.ts.json @@ -0,0 +1 @@ +{ "plain": 2520, "san": 4000 } diff --git a/tests/harness/corpus-costs/7027-search-params-conditional-init.ts.json b/tests/harness/corpus-costs/7027-search-params-conditional-init.ts.json new file mode 100644 index 000000000..56c46a74b --- /dev/null +++ b/tests/harness/corpus-costs/7027-search-params-conditional-init.ts.json @@ -0,0 +1 @@ +{ "plain": 2240, "san": 3440 } diff --git a/tests/harness/corpus-costs/7028-non-empty-rest-tuples.ts.json b/tests/harness/corpus-costs/7028-non-empty-rest-tuples.ts.json new file mode 100644 index 000000000..147f70b9a --- /dev/null +++ b/tests/harness/corpus-costs/7028-non-empty-rest-tuples.ts.json @@ -0,0 +1 @@ +{ "plain": 2520, "san": 4030 } diff --git a/tests/harness/corpus-costs/7029-flatmap-union-results.ts.json b/tests/harness/corpus-costs/7029-flatmap-union-results.ts.json new file mode 100644 index 000000000..7efdd91bf --- /dev/null +++ b/tests/harness/corpus-costs/7029-flatmap-union-results.ts.json @@ -0,0 +1 @@ +{ "plain": 2640, "san": 4710 } diff --git a/tests/harness/corpus-costs/7033-any-fields-into-declared-records.ts.json b/tests/harness/corpus-costs/7033-any-fields-into-declared-records.ts.json new file mode 100644 index 000000000..1fd3ee053 --- /dev/null +++ b/tests/harness/corpus-costs/7033-any-fields-into-declared-records.ts.json @@ -0,0 +1 @@ +{ "plain": 2640, "san": 3950 } diff --git a/tests/harness/corpus-costs/7034-optional-field-union-routes.ts.json b/tests/harness/corpus-costs/7034-optional-field-union-routes.ts.json new file mode 100644 index 000000000..7e342cfd0 --- /dev/null +++ b/tests/harness/corpus-costs/7034-optional-field-union-routes.ts.json @@ -0,0 +1 @@ +{ "plain": 2420, "san": 3640 } diff --git a/tests/harness/corpus-costs/7132-key-order-json.ts.json b/tests/harness/corpus-costs/7132-key-order-json.ts.json new file mode 100644 index 000000000..aa8658007 --- /dev/null +++ b/tests/harness/corpus-costs/7132-key-order-json.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 3600 } diff --git a/tests/harness/corpus-costs/7133-key-order-keys-values-entries.ts.json b/tests/harness/corpus-costs/7133-key-order-keys-values-entries.ts.json new file mode 100644 index 000000000..58d7b29f8 --- /dev/null +++ b/tests/harness/corpus-costs/7133-key-order-keys-values-entries.ts.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 2800 } diff --git a/tests/harness/corpus-costs/7134-key-order-for-in.ts.json b/tests/harness/corpus-costs/7134-key-order-for-in.ts.json new file mode 100644 index 000000000..e0b1e4dd1 --- /dev/null +++ b/tests/harness/corpus-costs/7134-key-order-for-in.ts.json @@ -0,0 +1 @@ +{ "plain": 1600, "san": 2100 } diff --git a/tests/harness/corpus-costs/7135-key-order-inspect.ts.json b/tests/harness/corpus-costs/7135-key-order-inspect.ts.json new file mode 100644 index 000000000..932ea4809 --- /dev/null +++ b/tests/harness/corpus-costs/7135-key-order-inspect.ts.json @@ -0,0 +1 @@ +{ "plain": 1600, "san": 2500 } diff --git a/tests/harness/corpus-costs/7136-key-order-copies.ts.json b/tests/harness/corpus-costs/7136-key-order-copies.ts.json new file mode 100644 index 000000000..9d76a548c --- /dev/null +++ b/tests/harness/corpus-costs/7136-key-order-copies.ts.json @@ -0,0 +1 @@ +{ "plain": 1900, "san": 3500 } diff --git a/tests/harness/corpus-costs/7137-key-order-dynamic.ts.json b/tests/harness/corpus-costs/7137-key-order-dynamic.ts.json new file mode 100644 index 000000000..12680c6af --- /dev/null +++ b/tests/harness/corpus-costs/7137-key-order-dynamic.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 2700 } diff --git a/tests/harness/corpus-costs/7138-key-order-late-writes.ts.json b/tests/harness/corpus-costs/7138-key-order-late-writes.ts.json new file mode 100644 index 000000000..2b1aad28d --- /dev/null +++ b/tests/harness/corpus-costs/7138-key-order-late-writes.ts.json @@ -0,0 +1 @@ +{ "plain": 1700, "san": 2800 } diff --git a/tests/harness/corpus-costs/7246-stream-construct.ts.json b/tests/harness/corpus-costs/7246-stream-construct.ts.json new file mode 100644 index 000000000..d94320c7e --- /dev/null +++ b/tests/harness/corpus-costs/7246-stream-construct.ts.json @@ -0,0 +1 @@ +{ "plain": 2327, "san": 3064 } diff --git a/tests/harness/corpus-costs/7247-stream-construct.mjs.json b/tests/harness/corpus-costs/7247-stream-construct.mjs.json new file mode 100644 index 000000000..a74da86c8 --- /dev/null +++ b/tests/harness/corpus-costs/7247-stream-construct.mjs.json @@ -0,0 +1 @@ +{ "plain": 2834, "san": 3538 } diff --git a/tests/harness/corpus-costs/7248-stream-writev.ts.json b/tests/harness/corpus-costs/7248-stream-writev.ts.json new file mode 100644 index 000000000..83ccdb03d --- /dev/null +++ b/tests/harness/corpus-costs/7248-stream-writev.ts.json @@ -0,0 +1 @@ +{ "plain": 2294, "san": 3026 } diff --git a/tests/harness/corpus-costs/7249-buffer-index-of-candidates.ts.json b/tests/harness/corpus-costs/7249-buffer-index-of-candidates.ts.json new file mode 100644 index 000000000..5c265ea01 --- /dev/null +++ b/tests/harness/corpus-costs/7249-buffer-index-of-candidates.ts.json @@ -0,0 +1 @@ +{ "plain": 2870, "san": 3656 } diff --git a/tests/harness/corpus-costs/7249-stream-writev.cjs.json b/tests/harness/corpus-costs/7249-stream-writev.cjs.json new file mode 100644 index 000000000..2fc75720e --- /dev/null +++ b/tests/harness/corpus-costs/7249-stream-writev.cjs.json @@ -0,0 +1 @@ +{ "plain": 2640, "san": 2765 } diff --git a/tests/harness/corpus-costs/7250-stream-decode-strings.ts.json b/tests/harness/corpus-costs/7250-stream-decode-strings.ts.json new file mode 100644 index 000000000..b6c0a39a0 --- /dev/null +++ b/tests/harness/corpus-costs/7250-stream-decode-strings.ts.json @@ -0,0 +1 @@ +{ "plain": 2016, "san": 2519 } diff --git a/tests/harness/corpus-costs/7251-stream-write-encodings.mjs.json b/tests/harness/corpus-costs/7251-stream-write-encodings.mjs.json new file mode 100644 index 000000000..7e459d978 --- /dev/null +++ b/tests/harness/corpus-costs/7251-stream-write-encodings.mjs.json @@ -0,0 +1 @@ +{ "plain": 2161, "san": 2661 } diff --git a/tests/harness/corpus-costs/7252-duplex-from-generator.ts.json b/tests/harness/corpus-costs/7252-duplex-from-generator.ts.json new file mode 100644 index 000000000..47f7b9ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7252-duplex-from-generator.ts.json @@ -0,0 +1 @@ +{ "plain": 2522, "san": 3330 } diff --git a/tests/harness/corpus-costs/7253-duplex-from.mjs.json b/tests/harness/corpus-costs/7253-duplex-from.mjs.json new file mode 100644 index 000000000..7743ca7c8 --- /dev/null +++ b/tests/harness/corpus-costs/7253-duplex-from.mjs.json @@ -0,0 +1 @@ +{ "plain": 1849, "san": 2440 } diff --git a/tests/harness/corpus-costs/7254-compose-functions.ts.json b/tests/harness/corpus-costs/7254-compose-functions.ts.json new file mode 100644 index 000000000..19d896024 --- /dev/null +++ b/tests/harness/corpus-costs/7254-compose-functions.ts.json @@ -0,0 +1 @@ +{ "plain": 1782, "san": 2350 } diff --git a/tests/harness/corpus-costs/7255-compose-stages.mjs.json b/tests/harness/corpus-costs/7255-compose-stages.mjs.json new file mode 100644 index 000000000..e8e0ab3d1 --- /dev/null +++ b/tests/harness/corpus-costs/7255-compose-stages.mjs.json @@ -0,0 +1 @@ +{ "plain": 2033, "san": 2680 } diff --git a/tests/harness/corpus-costs/7256-readable-compose.mjs.json b/tests/harness/corpus-costs/7256-readable-compose.mjs.json new file mode 100644 index 000000000..fe1307a1f --- /dev/null +++ b/tests/harness/corpus-costs/7256-readable-compose.mjs.json @@ -0,0 +1 @@ +{ "plain": 2146, "san": 2830 } diff --git a/tests/harness/corpus-costs/7257-pipeline-generator-destination.mjs.json b/tests/harness/corpus-costs/7257-pipeline-generator-destination.mjs.json new file mode 100644 index 000000000..ebaa9214c --- /dev/null +++ b/tests/harness/corpus-costs/7257-pipeline-generator-destination.mjs.json @@ -0,0 +1 @@ +{ "plain": 2008, "san": 2731 } diff --git a/tests/harness/corpus-costs/7258-pipeline-generator-destination.ts.json b/tests/harness/corpus-costs/7258-pipeline-generator-destination.ts.json new file mode 100644 index 000000000..15ff27f21 --- /dev/null +++ b/tests/harness/corpus-costs/7258-pipeline-generator-destination.ts.json @@ -0,0 +1 @@ +{ "plain": 1751, "san": 2442 } diff --git a/tests/harness/corpus-costs/7259-promises-pipeline-stage-options.cjs.json b/tests/harness/corpus-costs/7259-promises-pipeline-stage-options.cjs.json new file mode 100644 index 000000000..6bda6d818 --- /dev/null +++ b/tests/harness/corpus-costs/7259-promises-pipeline-stage-options.cjs.json @@ -0,0 +1 @@ +{ "plain": 2504, "san": 3707 } diff --git a/tests/harness/corpus-costs/7380-iterable-values.ts.json b/tests/harness/corpus-costs/7380-iterable-values.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7380-iterable-values.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } diff --git a/tests/harness/corpus-costs/7381-iterable-forof-semantics.ts.json b/tests/harness/corpus-costs/7381-iterable-forof-semantics.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7381-iterable-forof-semantics.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } diff --git a/tests/harness/corpus-costs/7382-iterable-generic-repository.ts.json b/tests/harness/corpus-costs/7382-iterable-generic-repository.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7382-iterable-generic-repository.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } diff --git a/tests/harness/corpus-costs/7383-iterable-union-sources.ts.json b/tests/harness/corpus-costs/7383-iterable-union-sources.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7383-iterable-union-sources.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } diff --git a/tests/harness/corpus-costs/7400-record-keyof-union-read.ts.json b/tests/harness/corpus-costs/7400-record-keyof-union-read.ts.json new file mode 100644 index 000000000..9b72eaf8d --- /dev/null +++ b/tests/harness/corpus-costs/7400-record-keyof-union-read.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 20500 } diff --git a/tests/harness/corpus-costs/7420-for-await-close.ts.json b/tests/harness/corpus-costs/7420-for-await-close.ts.json new file mode 100644 index 000000000..2df20a0ed --- /dev/null +++ b/tests/harness/corpus-costs/7420-for-await-close.ts.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 2800 } diff --git a/tests/harness/corpus-costs/7421-for-await-close.mjs.json b/tests/harness/corpus-costs/7421-for-await-close.mjs.json new file mode 100644 index 000000000..cb701dc93 --- /dev/null +++ b/tests/harness/corpus-costs/7421-for-await-close.mjs.json @@ -0,0 +1 @@ +{ "plain": 1900, "san": 2600 } diff --git a/tests/harness/corpus-costs/7422-for-await-untyped-streams.mjs.json b/tests/harness/corpus-costs/7422-for-await-untyped-streams.mjs.json new file mode 100644 index 000000000..cb701dc93 --- /dev/null +++ b/tests/harness/corpus-costs/7422-for-await-untyped-streams.mjs.json @@ -0,0 +1 @@ +{ "plain": 1900, "san": 2600 } diff --git a/tests/harness/corpus-costs/7423-stream-untyped-bindings.mjs.json b/tests/harness/corpus-costs/7423-stream-untyped-bindings.mjs.json new file mode 100644 index 000000000..d2ac5db99 --- /dev/null +++ b/tests/harness/corpus-costs/7423-stream-untyped-bindings.mjs.json @@ -0,0 +1 @@ +{ "plain": 4900, "san": 6454 } diff --git a/tests/harness/corpus-costs/7424-duplex-from-streams.mjs.json b/tests/harness/corpus-costs/7424-duplex-from-streams.mjs.json new file mode 100644 index 000000000..14a2052b9 --- /dev/null +++ b/tests/harness/corpus-costs/7424-duplex-from-streams.mjs.json @@ -0,0 +1 @@ +{ "plain": 2607, "san": 3440 } diff --git a/tests/harness/corpus-costs/7425-duplex-from-streams.ts.json b/tests/harness/corpus-costs/7425-duplex-from-streams.ts.json new file mode 100644 index 000000000..9dde8d70e --- /dev/null +++ b/tests/harness/corpus-costs/7425-duplex-from-streams.ts.json @@ -0,0 +1 @@ +{ "plain": 2009, "san": 2743 } diff --git a/tests/harness/corpus-costs/7426-duplex-from-promise.mjs.json b/tests/harness/corpus-costs/7426-duplex-from-promise.mjs.json new file mode 100644 index 000000000..9195268ab --- /dev/null +++ b/tests/harness/corpus-costs/7426-duplex-from-promise.mjs.json @@ -0,0 +1 @@ +{ "plain": 1877, "san": 2357 } diff --git a/tests/harness/corpus-costs/7427-duplex-from-async-function.mjs.json b/tests/harness/corpus-costs/7427-duplex-from-async-function.mjs.json new file mode 100644 index 000000000..c572d9930 --- /dev/null +++ b/tests/harness/corpus-costs/7427-duplex-from-async-function.mjs.json @@ -0,0 +1 @@ +{ "plain": 1811, "san": 2417 } diff --git a/tests/harness/corpus-costs/7428-web-stream-sources.mjs.json b/tests/harness/corpus-costs/7428-web-stream-sources.mjs.json new file mode 100644 index 000000000..e95faaa64 --- /dev/null +++ b/tests/harness/corpus-costs/7428-web-stream-sources.mjs.json @@ -0,0 +1 @@ +{ "plain": 1829, "san": 2414 } diff --git a/tests/harness/corpus-costs/7429-web-stream-sources.ts.json b/tests/harness/corpus-costs/7429-web-stream-sources.ts.json new file mode 100644 index 000000000..6b14e36cc --- /dev/null +++ b/tests/harness/corpus-costs/7429-web-stream-sources.ts.json @@ -0,0 +1 @@ +{ "plain": 1250, "san": 3013 } diff --git a/tests/harness/corpus-costs/7430-readable-from-web.mjs.json b/tests/harness/corpus-costs/7430-readable-from-web.mjs.json new file mode 100644 index 000000000..2266382b4 --- /dev/null +++ b/tests/harness/corpus-costs/7430-readable-from-web.mjs.json @@ -0,0 +1 @@ +{ "plain": 3342, "san": 4411 } diff --git a/tests/harness/corpus-costs/7431-readable-from-web.ts.json b/tests/harness/corpus-costs/7431-readable-from-web.ts.json new file mode 100644 index 000000000..05fab8e59 --- /dev/null +++ b/tests/harness/corpus-costs/7431-readable-from-web.ts.json @@ -0,0 +1 @@ +{ "plain": 1956, "san": 2929 } diff --git a/tests/harness/corpus-costs/7432-readable-push-views.mjs.json b/tests/harness/corpus-costs/7432-readable-push-views.mjs.json new file mode 100644 index 000000000..8a2f5280e --- /dev/null +++ b/tests/harness/corpus-costs/7432-readable-push-views.mjs.json @@ -0,0 +1 @@ +{ "plain": 1582, "san": 2088 } diff --git a/tests/harness/corpus-costs/7433-readable-operator-signal.mjs.json b/tests/harness/corpus-costs/7433-readable-operator-signal.mjs.json new file mode 100644 index 000000000..3ef40f751 --- /dev/null +++ b/tests/harness/corpus-costs/7433-readable-operator-signal.mjs.json @@ -0,0 +1 @@ +{ "plain": 3885, "san": 5128 } diff --git a/tests/harness/corpus-costs/7434-readable-operator-values.ts.json b/tests/harness/corpus-costs/7434-readable-operator-values.ts.json new file mode 100644 index 000000000..fdbfd8cde --- /dev/null +++ b/tests/harness/corpus-costs/7434-readable-operator-values.ts.json @@ -0,0 +1 @@ +{ "plain": 2465, "san": 2933 } diff --git a/tests/harness/corpus-costs/7435-readable-flatmap-iterables.mjs.json b/tests/harness/corpus-costs/7435-readable-flatmap-iterables.mjs.json new file mode 100644 index 000000000..8c31cc905 --- /dev/null +++ b/tests/harness/corpus-costs/7435-readable-flatmap-iterables.mjs.json @@ -0,0 +1 @@ +{ "plain": 2265, "san": 2990 } diff --git a/tests/harness/corpus-costs/7436-readable-read-any-value.mjs.json b/tests/harness/corpus-costs/7436-readable-read-any-value.mjs.json new file mode 100644 index 000000000..c4a7cbc84 --- /dev/null +++ b/tests/harness/corpus-costs/7436-readable-read-any-value.mjs.json @@ -0,0 +1 @@ +{ "plain": 1675, "san": 2211 } diff --git a/tests/harness/corpus-costs/7437-duplex-from-signal.mjs.json b/tests/harness/corpus-costs/7437-duplex-from-signal.mjs.json new file mode 100644 index 000000000..dfbf933f4 --- /dev/null +++ b/tests/harness/corpus-costs/7437-duplex-from-signal.mjs.json @@ -0,0 +1 @@ +{ "plain": 2383, "san": 3146 } diff --git a/tests/harness/corpus-costs/7438-duplex-from-signal.ts.json b/tests/harness/corpus-costs/7438-duplex-from-signal.ts.json new file mode 100644 index 000000000..13b6f4240 --- /dev/null +++ b/tests/harness/corpus-costs/7438-duplex-from-signal.ts.json @@ -0,0 +1 @@ +{ "plain": 1777, "san": 2722 } diff --git a/tests/harness/corpus-costs/7439-readable-read-string-chunks.ts.json b/tests/harness/corpus-costs/7439-readable-read-string-chunks.ts.json new file mode 100644 index 000000000..a93aa2e95 --- /dev/null +++ b/tests/harness/corpus-costs/7439-readable-read-string-chunks.ts.json @@ -0,0 +1 @@ +{ "plain": 2578, "san": 3403 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c4013d288..4d5546ab6 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { 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: [], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/tests/harness/stream-entry-kinds.test.ts b/tests/harness/stream-entry-kinds.test.ts index ec89c5914..b0aa52294 100644 --- a/tests/harness/stream-entry-kinds.test.ts +++ b/tests/harness/stream-entry-kinds.test.ts @@ -1,14 +1,9 @@ /* Stream chunk kinds Node cannot oracle — scriptc-only. * - * A typed read() answers `Buffer | null`. When the read itself makes the - * stream hold strings (the first string a lazy Readable.from pull over a - * dynamic array produces, or setEncoding() inside _read), Node returns - * the string, which the typed result cannot hold, so the read throws the - * encoding fence instead (caught here, so the program exits normally). - * These pins keep that refusal exact and, under - * SCRIPTC_SAN=1 (ASan and the RC audit, like every harness lane), prove - * no string reaches Buffer code. The Node-observable cases are corpus - * programs (stream-dynamic-entry-kinds.*). + * Under SCRIPTC_SAN=1 (ASan and the RC audit, like every harness lane) + * these pins prove no string reaches Buffer code. The Node-observable + * cases are corpus programs (stream-dynamic-entry-kinds.*, and + * 7439-readable-read-string-chunks for read() of strings). */ import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; @@ -73,71 +68,7 @@ function stripAsanFiberWarning(s: string): string { ); } -const READ_FENCE_MESSAGE = - "read() on a stream with an encoding set is not supported yet (consume 'data' events, which deliver strings)"; - describe(`typed stream reads of string chunks (scriptc-only${sanitize ? ", sanitized" : ""})`, () => { - test("a typed read() whose lazy pull yields the first string throws the encoding fence", async () => { - const r = await compileAndRun( - "typed-read-json-string", - `import { Readable } from "node:stream"; -const r = Readable.from(JSON.parse('["j1", "j2"]')); -try { - const chunk = r.read(); - console.log("hex", chunk === null ? "null" : chunk.toString("hex")); -} catch (err) { - console.log("caught", (err as Error).message); -} -`, - ); - expect(r.exitCode).toBe(0); - expect(r.stdout).toBe(`caught ${READ_FENCE_MESSAGE}\n`); - }); - - test("a typed read() whose _read calls setEncoding() throws the encoding fence", async () => { - const r = await compileAndRun( - "typed-read-encoding-in-read", - `import { Readable } from "node:stream"; -const r = new Readable({ - read() { - this.setEncoding("utf8"); - this.push(Buffer.from("ab")); - this.push(null); - }, -}); -try { - const chunk = r.read(); - console.log("hex", chunk === null ? "null" : chunk.toString("hex")); -} catch (err) { - console.log("caught", (err as Error).message); -} -`, - ); - expect(r.exitCode).toBe(0); - expect(r.stdout).toBe(`caught ${READ_FENCE_MESSAGE}\n`); - }); - - test("the JavaScript lane's read() on a JSON source throws the same fence", async () => { - const r = await compileAndRun( - "js-read-json-string", - `'use strict'; -const { Readable } = require('node:stream'); -const r = Readable.from(JSON.parse('["j1"]')); -r.once('readable', () => { - try { - const chunk = r.read(); - console.log('got', chunk === null ? 'null' : chunk.length); - } catch (err) { - console.log('caught', err.message); - } -}); -`, - "cjs", - ); - expect(r.exitCode).toBe(0); - expect(r.stdout).toBe(`caught ${READ_FENCE_MESSAGE}\n`); - }); - test("a dynamic source that mixes Buffers and strings yields each as it is, as Node does", async () => { const r = await compileAndRun( "mixed-dynamic-source",