mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-24 06:14:01 +08:00
Merge commit '26946d6b2' into opt/webcrypto-hmac
This commit is contained in:
@@ -46,6 +46,8 @@ SCRIPTC_TEST_WORKERS=4 SCRIPTC_SAN=1 pnpm test # sanitized lane
|
||||
|
||||
Corpus programs are differential tests against Node: every program runs under Node and as a compiled native binary, and stdout, stderr, and exit codes must match byte-for-byte. A new feature lands with corpus programs that pin its behavior both ways.
|
||||
|
||||
Record a new or remeasured program's CI cost (used to balance the test shards) in `tests/harness/corpus-costs/<program>.json`, not in `tests/harness/corpus-costs.json`; see the README there.
|
||||
|
||||
Before opening a PR that adds or changes corpus, diagnostic, or frontend fixtures, run `pnpm test:ts7 --baselines-only`. This fast completeness check uses the same fixture discovery as the parity suite; it does not type-check or replace behavioral parity. Generate missing preflight/order baselines, review their diagnostics and module order, and run the affected `order-parity.test.ts` batches before pushing. Keep existing baseline entries unchanged unless the corresponding behavior intentionally changes. The full `pnpm test:ts7` command also checks completeness before starting its sweep.
|
||||
|
||||
A change that must not cost good code anything, such as a compatibility fix that replaces a refusal, proves it with the zero-cost gate. The gate compiles the benchmark workloads, the self-hosted compiler, and the TypeScript corpus with two built checkouts. It requires byte-identical IR, LLVM, and release executables, and compile time within tolerance. `--allow=<regex>` names the change's pattern signature: programs whose sources match may differ, and the PR lists them. Any other difference fails the gate, which names the first differing function. The gate also enforces the two-tier runtime separation: no executable may gain symbols from `DYNAMIC_TIER_PREFIXES` in `scripts/zero-cost-compare.mjs`, and fully typed benchmarks may link none; extend that list as dynamic-tier units land. `--quick` checks only the benchmarks; `--cachegrind` adds per-function instruction counts for executables that differ. Run it in a Sandbox with both checkouts prepared:
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, typed array, string, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `.return()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one. `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError. `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators. In JavaScript, untyped array, string, Map and Set iterators answer the same methods. `console.log`, `util.inspect` and `Object.prototype.toString` print iterator objects as Node does: `Object [Iterator Helper] {}`, `Object [Generator] {}`, `Object [Array Iterator] {}`, and Map and Set iterators with the entries they have left. `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and `String()` and template literals of typed iterator objects are unsupported.
|
||||
- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, typed array, string, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `.return()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one. `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError. `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators. In JavaScript, untyped generators and array, string, Map and Set iterators answer the same methods, and array destructuring of untyped values steps generators, iterators, Maps and Sets the way Node does. Helper methods on untyped async generators are unsupported. `console.log`, `util.inspect` and `Object.prototype.toString` print iterator objects as Node does: `Object [Iterator Helper] {}`, `Object [Generator] {}`, `Object [Array Iterator] {}`, and Map and Set iterators with the entries they have left. `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and `String()` and template literals of typed iterator objects are unsupported.
|
||||
|
||||
@@ -4014,14 +4014,9 @@ export class LlDyn {
|
||||
const name = "sc_dyn_iter_n";
|
||||
this.dynBuilders.set(memoKey, name);
|
||||
const host = this.host;
|
||||
host.declare(`declare void @scr_jb_init(ptr)`);
|
||||
host.declare(`declare ptr @scr_jb_finish(ptr)`);
|
||||
host.declare(`declare void @scr_jb_puts(ptr, ptr)`);
|
||||
host.declare(`declare void @scr_throw_error(i32, ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_new_arr()`);
|
||||
host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_new_num(double)`);
|
||||
host.declare(`declare ptr @scr_f64_to_scrstr(double)`);
|
||||
host.declare(`declare void @scr_str_release(ptr)`);
|
||||
const B = new BlockBuilder();
|
||||
const kd = this.kindOf(B, "%d");
|
||||
@@ -4033,6 +4028,21 @@ export class LlDyn {
|
||||
const lNext = B.newLabel("din.nt");
|
||||
B.condBr(isTyped, lTyped, lNext);
|
||||
B.startBlock(lTyped);
|
||||
// Compiled generators step through the iterator protocol.
|
||||
host.declare(`declare i1 @scr_dyn_iterates_by_protocol(ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_iter_take(ptr, ${host.sizeType}, ptr)`);
|
||||
const protocol = B.tmp();
|
||||
const lProtocol = B.newLabel("din.pr");
|
||||
const lView = B.newLabel("din.vw");
|
||||
B.line(`${protocol} = call i1 @scr_dyn_iterates_by_protocol(ptr %d)`);
|
||||
B.condBr(protocol, lProtocol, lView);
|
||||
B.startBlock(lProtocol);
|
||||
const stepped = B.tmp();
|
||||
B.line(
|
||||
`${stepped} = call ptr @scr_dyn_iter_take(ptr %d, ${host.sizeType} %n, ptr %spelling)`,
|
||||
);
|
||||
B.terminate(`ret ptr ${stepped}`);
|
||||
B.startBlock(lView);
|
||||
host.declare(`declare ptr @scr_dyn_typed_ref_materialize(ptr)`);
|
||||
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
const materialized = B.tmp();
|
||||
@@ -4057,71 +4067,13 @@ export class LlDyn {
|
||||
const lGo = B.newLabel("din.g");
|
||||
const lThrow = B.newLabel("din.t");
|
||||
B.condBr(ok, lGo, lThrow);
|
||||
// V8's exact not-iterable TypeError.
|
||||
// Other iterables step through the iterator protocol; non-iterables
|
||||
// throw V8's exact not-iterable TypeError.
|
||||
B.startBlock(lThrow);
|
||||
const buf = "%dib";
|
||||
B.entryAllocas.push(`${buf} = alloca %ScrJsonBuf`);
|
||||
B.line(`call void @scr_jb_init(ptr ${buf})`);
|
||||
const hasSpelling = B.tmp();
|
||||
B.line(`${hasSpelling} = icmp ne ptr %spelling, null`);
|
||||
const named = B.newLabel("din.named"),
|
||||
unnamed = B.newLabel("din.unnamed");
|
||||
B.condBr(hasSpelling, named, unnamed);
|
||||
B.startBlock(named);
|
||||
B.line(`call void @scr_jb_puts(ptr ${buf}, ptr %spelling)`);
|
||||
const namedMessage = B.tmp();
|
||||
B.line(`${namedMessage} = call ptr @scr_jb_finish(ptr ${buf})`);
|
||||
B.line(`call void @scr_throw_error(i32 1, ptr ${namedMessage})`);
|
||||
B.terminate(`ret ptr null`);
|
||||
B.startBlock(unnamed);
|
||||
const lU = B.newLabel("din.u");
|
||||
const lN = B.newLabel("din.n");
|
||||
const lB2 = B.newLabel("din.b");
|
||||
const lNum = B.newLabel("din.m");
|
||||
const lF = B.newLabel("din.f");
|
||||
const lDef = B.newLabel("din.d");
|
||||
const lTail = B.newLabel("din.e");
|
||||
B.terminate(
|
||||
`switch i32 ${kd}, label %${lDef} [ i32 ${DYN_KIND.UNDEF}, label %${lU} i32 ${DYN_KIND.NULL}, label %${lN} i32 ${DYN_KIND.BOOL}, label %${lB2} i32 ${DYN_KIND.NUM}, label %${lNum} i32 ${DYN_KIND.FUNC}, label %${lF} ]`,
|
||||
);
|
||||
B.startBlock(lU);
|
||||
this.puts(B, buf, "undefined");
|
||||
B.br(lTail);
|
||||
B.startBlock(lN);
|
||||
this.puts(B, buf, "object null");
|
||||
B.br(lTail);
|
||||
B.startBlock(lB2);
|
||||
{
|
||||
const bv = this.boolOf(B, "%d");
|
||||
const s = B.tmp();
|
||||
B.line(
|
||||
`${s} = select i1 ${bv}, ptr ${host.cstr("boolean true")}, ptr ${host.cstr("boolean false")}`,
|
||||
);
|
||||
B.line(`call void @scr_jb_puts(ptr ${buf}, ptr ${s})`);
|
||||
B.br(lTail);
|
||||
}
|
||||
B.startBlock(lNum);
|
||||
{
|
||||
this.puts(B, buf, "number ");
|
||||
const x = this.payloadOf(B, "%d", "double");
|
||||
const s = B.tmp();
|
||||
B.line(`${s} = call ptr @scr_f64_to_scrstr(double ${x})`);
|
||||
this.putScrStr(B, buf, s);
|
||||
B.line(`call void @scr_str_release(ptr ${s})`);
|
||||
B.br(lTail);
|
||||
}
|
||||
B.startBlock(lF);
|
||||
this.puts(B, buf, "function");
|
||||
B.br(lTail);
|
||||
B.startBlock(lDef);
|
||||
this.puts(B, buf, "object");
|
||||
B.br(lTail);
|
||||
B.startBlock(lTail);
|
||||
this.puts(B, buf, " is not iterable (cannot read property Symbol(Symbol.iterator))");
|
||||
const msg = B.tmp();
|
||||
B.line(`${msg} = call ptr @scr_jb_finish(ptr ${buf})`);
|
||||
B.line(`call void @scr_throw_error(i32 1, ptr ${msg}) ; SCR_ERR_TYPE`);
|
||||
B.terminate(`ret ptr null`);
|
||||
host.declare(`declare ptr @scr_dyn_iter_take(ptr, ${host.sizeType}, ptr)`);
|
||||
const taken = B.tmp();
|
||||
B.line(`${taken} = call ptr @scr_dyn_iter_take(ptr %d, ${host.sizeType} %n, ptr %spelling)`);
|
||||
B.terminate(`ret ptr ${taken}`);
|
||||
B.startBlock(lGo);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = call ptr @scr_dyn_new_arr()`);
|
||||
|
||||
@@ -891,12 +891,15 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.typedRefIs": "scr_dyn_typed_ref_is_key",
|
||||
"dyn.classIs": "scr_dyn_class_is_key",
|
||||
"dyn.iterPack": "scr_dyn_iter_pack",
|
||||
"dyn.destructureRest": "scr_dyn_destructure_rest",
|
||||
"dyn.iterator": "scr_dyn_iterator",
|
||||
"dyn.arrayFromIterator": "scr_dyn_array_from_iterator",
|
||||
"dyn.iteratorResult": "scr_dyn_iterator_result",
|
||||
"dyn.iteratorCanStep": "scr_dyn_iterator_can_step",
|
||||
"dyn.iteratorStep": "scr_dyn_iterator_step",
|
||||
"dyn.iteratorStepDone": "scr_dyn_iterator_step_done",
|
||||
"dyn.setGeneratorMembers": "scr_dyn_set_generator_members",
|
||||
"dyn.iteratorHelperMethod": "scr_dyn_iterator_helper_method",
|
||||
"dyn.collectionIteratorIndex": "scr_dyn_collection_iterator_index",
|
||||
"dyn.mapSeedEntries": "scr_dyn_map_seed_entries",
|
||||
"dyn.mapSeedEntry": "scr_dyn_map_seed_entry",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
funcOf,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
VOID,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
@@ -18,6 +19,22 @@ import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
type GeneratorType = Extract<IrType, { kind: "generator" }>;
|
||||
type Invoke = Extract<IrExpr, { kind: "dynInvoke" }>;
|
||||
type KeyGet = Extract<IrExpr, { kind: "dynKeyGet" }>;
|
||||
|
||||
/** The iterator helper methods the runtime answers on checked iterators. */
|
||||
const HELPER_METHODS = new Set([
|
||||
"map",
|
||||
"filter",
|
||||
"take",
|
||||
"drop",
|
||||
"flatMap",
|
||||
"reduce",
|
||||
"toArray",
|
||||
"forEach",
|
||||
"some",
|
||||
"every",
|
||||
"find",
|
||||
]);
|
||||
|
||||
/** An untyped consumer resumes the original native fiber. The capsule's
|
||||
* exact type key selects the channel conversions and result layout. */
|
||||
@@ -29,6 +46,54 @@ export class GeneratorDynamicDispatch {
|
||||
>();
|
||||
private readonly generated = new Set<IrFunction>();
|
||||
private readonly properties = new Map<string, { fn: IrFunction; types: Set<string> }>();
|
||||
/** Whether %main registers the next/return getters with the runtime. */
|
||||
private membersRegistered = false;
|
||||
|
||||
/** The getter for a generator capsule's `method` (next, return, throw or
|
||||
* an iterator helper): a checked function over the capsule's type keys
|
||||
* that answers the method as a function value. `read` is the dyn read
|
||||
* whose fallback it keeps for other receivers. */
|
||||
private ensureProperty(lowerer: Lowerer, method: string, read: KeyGet): IrFunction {
|
||||
const existing = this.properties.get(method);
|
||||
if (existing) return existing.fn;
|
||||
const loc = read.loc;
|
||||
const fn: IrFunction = {
|
||||
name: `%dyn.generator.get.${method}`,
|
||||
params: [{ localId: "value", name: "value", type: DYN }],
|
||||
returnType: DYN,
|
||||
locals: [{ id: "value", name: "value", type: DYN, mutable: false }],
|
||||
body: [{ kind: "return", value: { ...read, value: varRef("value", DYN, loc) }, loc }],
|
||||
loc,
|
||||
};
|
||||
this.properties.set(method, { fn, types: new Set() });
|
||||
this.generated.add(fn);
|
||||
lowerer.liftedFns.push(fn);
|
||||
// The runtime's own helper functions answer the helper methods.
|
||||
if (HELPER_METHODS.has(method)) return fn;
|
||||
lowerer.liftedFns.push({
|
||||
name: `%dyn.generator.method.${method}`,
|
||||
params: [{ localId: "arg", name: "arg", type: DYN }],
|
||||
returnType: DYN,
|
||||
locals: [{ id: "arg", name: "arg", type: DYN, mutable: false }],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "dynInvoke",
|
||||
method,
|
||||
calleeName: `Generator.${method}`,
|
||||
recv: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc },
|
||||
args: [varRef("arg", DYN, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
return fn;
|
||||
}
|
||||
|
||||
process(
|
||||
lowerer: Lowerer,
|
||||
@@ -37,6 +102,7 @@ export class GeneratorDynamicDispatch {
|
||||
): boolean {
|
||||
const seen = new Set<string>();
|
||||
const rewrite = new Set<IrFunction>();
|
||||
let runtimeSteps = false;
|
||||
const discover = (type: IrType): void => {
|
||||
const key = typeKey(type);
|
||||
if (seen.has(key)) return;
|
||||
@@ -56,10 +122,22 @@ export class GeneratorDynamicDispatch {
|
||||
if (expr.kind === "dynFrom") discover(expr.value.type);
|
||||
if (expr.kind === "dynInvoke" && ["next", "return", "throw"].includes(expr.method))
|
||||
rewrite.add(fn);
|
||||
// The runtime steps the values iterator helpers and destructuring
|
||||
// receive.
|
||||
if (
|
||||
(expr.kind === "dynInvoke" && HELPER_METHODS.has(expr.method)) ||
|
||||
(expr.kind === "dynKeyGet" &&
|
||||
expr.key.kind === "strLit" &&
|
||||
HELPER_METHODS.has(expr.key.value)) ||
|
||||
expr.kind === "dynIterN" ||
|
||||
(expr.kind === "libCall" && expr.fn === "dyn.destructureRest")
|
||||
)
|
||||
runtimeSteps = true;
|
||||
if (
|
||||
expr.kind === "dynKeyGet" &&
|
||||
expr.key.kind === "strLit" &&
|
||||
["next", "return", "throw"].includes(expr.key.value)
|
||||
(["next", "return", "throw"].includes(expr.key.value) ||
|
||||
HELPER_METHODS.has(expr.key.value))
|
||||
)
|
||||
rewrite.add(fn);
|
||||
return true;
|
||||
@@ -74,56 +152,20 @@ export class GeneratorDynamicDispatch {
|
||||
fn.body = transformStmtList(fn.body, {
|
||||
stmt: (stmt) => stmt,
|
||||
expr: (expr) => {
|
||||
// A helper method read reaches the runtime's helpers through the
|
||||
// same getters once generators are boxed.
|
||||
if (
|
||||
expr.kind === "dynKeyGet" &&
|
||||
expr.key.kind === "strLit" &&
|
||||
["next", "return", "throw"].includes(expr.key.value)
|
||||
(["next", "return", "throw"].includes(expr.key.value) ||
|
||||
(HELPER_METHODS.has(expr.key.value) && this.generators.size > 0))
|
||||
) {
|
||||
const method = expr.key.value;
|
||||
let property = this.properties.get(method);
|
||||
if (!property) {
|
||||
const loc = expr.loc;
|
||||
const fn: IrFunction = {
|
||||
name: `%dyn.generator.get.${method}`,
|
||||
params: [{ localId: "value", name: "value", type: DYN }],
|
||||
returnType: DYN,
|
||||
locals: [{ id: "value", name: "value", type: DYN, mutable: false }],
|
||||
body: [
|
||||
{ kind: "return", value: { ...expr, value: varRef("value", DYN, loc) }, loc },
|
||||
],
|
||||
loc,
|
||||
};
|
||||
property = { fn, types: new Set() };
|
||||
this.properties.set(method, property);
|
||||
this.generated.add(fn);
|
||||
lowerer.liftedFns.push(fn);
|
||||
lowerer.liftedFns.push({
|
||||
name: `%dyn.generator.method.${method}`,
|
||||
params: [{ localId: "arg", name: "arg", type: DYN }],
|
||||
returnType: DYN,
|
||||
locals: [{ id: "arg", name: "arg", type: DYN, mutable: false }],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "dynInvoke",
|
||||
method,
|
||||
calleeName: `Generator.${method}`,
|
||||
recv: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc },
|
||||
args: [varRef("arg", DYN, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
if (!this.properties.has(method)) changed = true;
|
||||
const getter = this.ensureProperty(lowerer, method, expr);
|
||||
return {
|
||||
kind: "call",
|
||||
callee: property.fn.name,
|
||||
callee: getter.name,
|
||||
args: [expr.value],
|
||||
type: DYN,
|
||||
loc: expr.loc,
|
||||
@@ -179,6 +221,50 @@ export class GeneratorDynamicDispatch {
|
||||
},
|
||||
});
|
||||
}
|
||||
// Iterator helpers called on untyped values may reach a compiled
|
||||
// generator's capsule; the runtime drives it through these getters,
|
||||
// which %main registers first.
|
||||
const firstFn = functions[0];
|
||||
if (
|
||||
runtimeSteps &&
|
||||
!this.membersRegistered &&
|
||||
firstFn !== undefined &&
|
||||
[...this.generators.values()].some((t) => t.async !== true)
|
||||
) {
|
||||
const loc = firstFn.loc;
|
||||
const read = (method: string): KeyGet => ({
|
||||
kind: "dynKeyGet",
|
||||
value: varRef("value", DYN, loc),
|
||||
key: { kind: "strLit", value: method, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
const getterValue = (method: string): IrExpr => ({
|
||||
kind: "dynFrom",
|
||||
value: {
|
||||
kind: "closure",
|
||||
fnName: this.ensureProperty(lowerer, method, read(method)).name,
|
||||
captures: [],
|
||||
type: funcOf([DYN], DYN),
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
lowerer.mainPrelude.push({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.setGeneratorMembers",
|
||||
args: [getterValue("next"), getterValue("return")],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
this.membersRegistered = true;
|
||||
changed = true;
|
||||
}
|
||||
for (const [method, property] of this.properties)
|
||||
for (const [key] of this.generators) {
|
||||
if (property.types.has(key)) continue;
|
||||
@@ -196,18 +282,26 @@ export class GeneratorDynamicDispatch {
|
||||
then: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "dynFrom",
|
||||
value: {
|
||||
kind: "closure",
|
||||
fnName: `%dyn.generator.method.${method}`,
|
||||
captures: [],
|
||||
type: funcOf([DYN], DYN),
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
value: HELPER_METHODS.has(method)
|
||||
? {
|
||||
kind: "libCall",
|
||||
fn: "dyn.iteratorHelperMethod",
|
||||
args: [{ kind: "strLit", value: method, type: STRING, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "dynFrom",
|
||||
value: {
|
||||
kind: "closure",
|
||||
fnName: `%dyn.generator.method.${method}`,
|
||||
captures: [],
|
||||
type: funcOf([DYN], DYN),
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
@@ -228,10 +322,14 @@ export class GeneratorDynamicDispatch {
|
||||
const supplied = dispatch.source.args.length > 0;
|
||||
const incoming = supplied ? varRef("p.1", DYN, loc) : dynUndefinedExpr(loc);
|
||||
const channel = mode === "next" ? type.nextT : type.retT;
|
||||
// return() with no value completes with undefined, as the typed
|
||||
// .return() does, whatever the return channel's type.
|
||||
const arg =
|
||||
mode === "throw"
|
||||
? incoming
|
||||
: channel.kind === "undefinedT" || channel.kind === "void"
|
||||
: channel.kind === "undefinedT" ||
|
||||
channel.kind === "void" ||
|
||||
(mode === "return" && !supplied)
|
||||
? null
|
||||
: lowerer.coerceToExpected(incoming, channel);
|
||||
const convertible = canConvertToDyn(
|
||||
@@ -263,25 +361,36 @@ export class GeneratorDynamicDispatch {
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
const resume = (sent: IrExpr | null): IrStmt => ({
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "dynFrom",
|
||||
value: {
|
||||
kind: "genResume",
|
||||
mode,
|
||||
gen: { kind: "dynCheck", value: receiver, type, loc },
|
||||
arg: sent,
|
||||
type: resultType,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
const valid =
|
||||
convertible && (arg === null || mode === "throw" || typeEquals(arg.type, channel));
|
||||
if (valid && mode === "return" && arg !== null)
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: "undefined", value: incoming, type: BOOL, loc },
|
||||
then: [resume(null)],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
body.push(
|
||||
convertible && (arg === null || mode === "throw" || typeEquals(arg.type, channel))
|
||||
? {
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "dynFrom",
|
||||
value: {
|
||||
kind: "genResume",
|
||||
mode,
|
||||
gen: { kind: "dynCheck", value: receiver, type, loc },
|
||||
arg,
|
||||
type: resultType,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
}
|
||||
valid
|
||||
? resume(arg)
|
||||
: {
|
||||
kind: "runtimeFence",
|
||||
code: "SC1071",
|
||||
|
||||
@@ -3135,6 +3135,7 @@ function rememberEvaluationError(
|
||||
export function buildMain(lowerer: Lowerer): IrFunction {
|
||||
const loc: SrcLoc = { file: lowerer.entry.fileName, start: 0, end: 0 };
|
||||
const body: IrStmt[] = [
|
||||
...lowerer.mainPrelude,
|
||||
...cjsModuleRegistryPrelude(lowerer, loc),
|
||||
...handleEventReaderPrelude(lowerer, loc),
|
||||
];
|
||||
|
||||
@@ -2397,7 +2397,7 @@ export function lowerBindingPattern(
|
||||
)
|
||||
? {
|
||||
kind: "libCall",
|
||||
fn: "dyn.iterPack",
|
||||
fn: "dyn.destructureRest",
|
||||
args: [srcRef(), { kind: "strLit", value: dynSpell ?? "", type: STRING, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
|
||||
@@ -10642,6 +10642,10 @@ export class Lowerer {
|
||||
return lowerDefaultExport(this, stmt);
|
||||
}
|
||||
|
||||
/** Statements %main runs before any module initializer: runtime
|
||||
* registrations the lowering passes add. */
|
||||
readonly mainPrelude: IrStmt[] = [];
|
||||
|
||||
buildMain(): IrFunction {
|
||||
return buildMain(this);
|
||||
}
|
||||
|
||||
@@ -569,6 +569,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"dyn.propertyIsEnumerableComputed",
|
||||
// the destructuring pack throws V8's TypeError on non-iterable dyn kinds
|
||||
"dyn.iterPack",
|
||||
"dyn.destructureRest",
|
||||
"dyn.arrayFromIterator",
|
||||
"dyn.iterator",
|
||||
"dyn.iteratorResult",
|
||||
|
||||
@@ -176,12 +176,15 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* not iterable (cannot read property Symbol(Symbol.iterator))"). In the
|
||||
* may-throw seed set. */
|
||||
"dyn.iterPack": { argTypes: [DYN, STRING], result: DYN },
|
||||
"dyn.destructureRest": { argTypes: [DYN, STRING], result: DYN },
|
||||
"dyn.arrayFromIterator": { argTypes: [DYN], result: DYN },
|
||||
"dyn.iterator": { argTypes: [DYN, STRING], result: DYN },
|
||||
"dyn.iteratorResult": { argTypes: [DYN], result: DYN },
|
||||
"dyn.iteratorCanStep": { argTypes: [DYN, DYN], result: BOOL },
|
||||
"dyn.iteratorStep": { argTypes: [DYN], result: DYN },
|
||||
"dyn.iteratorStepDone": { argTypes: [DYN], result: BOOL },
|
||||
"dyn.setGeneratorMembers": { argTypes: [DYN, DYN], result: VOID },
|
||||
"dyn.iteratorHelperMethod": { argTypes: [STRING], result: DYN },
|
||||
"dyn.collectionIteratorIndex": { argTypes: [DYN], result: F64 },
|
||||
"dyn.mapSeedEntries": { argTypes: [DYN], result: DYN },
|
||||
"dyn.mapSeedEntry": { argTypes: [DYN], result: DYN },
|
||||
|
||||
@@ -127,21 +127,54 @@ export function everyExprChild(
|
||||
expr: (expr: IrExpr) => boolean,
|
||||
stmt: (stmt: IrStmt) => boolean,
|
||||
): boolean {
|
||||
// The most common node kinds come first: the engine tests string cases
|
||||
// one after another, in source order.
|
||||
switch (node.kind) {
|
||||
case "numLit":
|
||||
case "varRef":
|
||||
return true;
|
||||
case "call":
|
||||
return node.args.every((child) => expr(child));
|
||||
case "dynFrom":
|
||||
return expr(node.value);
|
||||
case "strLit":
|
||||
return true;
|
||||
case "moduleNsRef":
|
||||
case "libCall":
|
||||
return node.args.every((child) => expr(child));
|
||||
case "dynCheck":
|
||||
return expr(node.value);
|
||||
case "strEq":
|
||||
return expr(node.left) && expr(node.right);
|
||||
case "fieldGet":
|
||||
return expr(node.obj);
|
||||
case "ternary":
|
||||
return expr(node.cond) && expr(node.then) && expr(node.else_);
|
||||
case "closure":
|
||||
return true;
|
||||
case "boolLit":
|
||||
return true;
|
||||
case "numLit":
|
||||
return true;
|
||||
case "unitLit":
|
||||
return true;
|
||||
case "varRef":
|
||||
return true;
|
||||
case "seqExpr":
|
||||
return node.stmts.every((child) => stmt(child)) && expr(node.result);
|
||||
case "dynCall":
|
||||
return (
|
||||
expr(node.callee) &&
|
||||
(node.receiver === undefined || expr(node.receiver)) &&
|
||||
(node.calleeNameValue === undefined || expr(node.calleeNameValue)) &&
|
||||
node.args.every((child) => expr(child))
|
||||
);
|
||||
case "dynTest":
|
||||
return expr(node.value);
|
||||
case "dynScalarEq":
|
||||
return expr(node.left) && expr(node.right);
|
||||
case "upcast":
|
||||
return expr(node.value);
|
||||
case "bin":
|
||||
return expr(node.left) && expr(node.right);
|
||||
case "moduleNsRef":
|
||||
return true;
|
||||
case "unary":
|
||||
return expr(node.operand);
|
||||
case "incDec":
|
||||
@@ -156,14 +189,10 @@ export function everyExprChild(
|
||||
return expr(node.left) && expr(node.right);
|
||||
case "strConcat":
|
||||
return expr(node.left) && expr(node.right);
|
||||
case "strEq":
|
||||
return expr(node.left) && expr(node.right);
|
||||
case "strCmp":
|
||||
return expr(node.left) && expr(node.right);
|
||||
case "toString":
|
||||
return expr(node.operand);
|
||||
case "ternary":
|
||||
return expr(node.cond) && expr(node.then) && expr(node.else_);
|
||||
case "nullish":
|
||||
return expr(node.left) && expr(node.right);
|
||||
case "orDefault":
|
||||
@@ -206,12 +235,8 @@ export function everyExprChild(
|
||||
return node.seed === undefined || expr(node.seed);
|
||||
case "setIntrinsic":
|
||||
return expr(node.receiver) && node.args.every((child) => expr(child));
|
||||
case "call":
|
||||
return node.args.every((child) => expr(child));
|
||||
case "ffiCall":
|
||||
return node.args.every((child) => expr(child));
|
||||
case "closure":
|
||||
return true;
|
||||
case "callValue":
|
||||
return (
|
||||
expr(node.callee) &&
|
||||
@@ -240,8 +265,6 @@ export function everyExprChild(
|
||||
return expr(node.callee) && node.args.every((child) => expr(child));
|
||||
case "instanceOfValue":
|
||||
return expr(node.value) && expr(node.classValue);
|
||||
case "upcast":
|
||||
return expr(node.value);
|
||||
case "promiseVoidWiden":
|
||||
return expr(node.value);
|
||||
case "downcast":
|
||||
@@ -250,8 +273,6 @@ export function everyExprChild(
|
||||
return expr(node.value);
|
||||
case "virtualCall":
|
||||
return node.args.every((child) => expr(child));
|
||||
case "fieldGet":
|
||||
return expr(node.obj);
|
||||
case "recordLit":
|
||||
return node.fields.every((child) => expr(child.value));
|
||||
case "recordClone":
|
||||
@@ -260,15 +281,6 @@ export function everyExprChild(
|
||||
return expr(node.obj);
|
||||
case "recordKeyGet":
|
||||
return expr(node.obj) && expr(node.key);
|
||||
case "dynFrom":
|
||||
return expr(node.value);
|
||||
case "dynCall":
|
||||
return (
|
||||
expr(node.callee) &&
|
||||
(node.receiver === undefined || expr(node.receiver)) &&
|
||||
(node.calleeNameValue === undefined || expr(node.calleeNameValue)) &&
|
||||
node.args.every((child) => expr(child))
|
||||
);
|
||||
case "dynInvoke":
|
||||
return (
|
||||
expr(node.recv) &&
|
||||
@@ -282,16 +294,10 @@ export function everyExprChild(
|
||||
node.fields === undefined ||
|
||||
node.fields.every((child) => expr(child.key) && expr(child.value))
|
||||
);
|
||||
case "dynTest":
|
||||
return expr(node.value);
|
||||
case "dynKeyGet":
|
||||
return expr(node.key) && expr(node.value);
|
||||
case "dynHasKey":
|
||||
return expr(node.value);
|
||||
case "dynScalarEq":
|
||||
return expr(node.left) && expr(node.right);
|
||||
case "seqExpr":
|
||||
return node.stmts.every((child) => stmt(child)) && expr(node.result);
|
||||
case "dynDestrCheck":
|
||||
return expr(node.value);
|
||||
case "dynIterN":
|
||||
@@ -332,12 +338,8 @@ export function everyExprChild(
|
||||
return expr(node.left) && expr(node.right);
|
||||
case "intrinsic":
|
||||
return node.args.every((child) => expr(child));
|
||||
case "libCall":
|
||||
return node.args.every((child) => expr(child));
|
||||
case "jsonStringify":
|
||||
return expr(node.value);
|
||||
case "dynCheck":
|
||||
return expr(node.value);
|
||||
}
|
||||
node satisfies never;
|
||||
throw new Error("unhandled IR node");
|
||||
@@ -349,19 +351,27 @@ export function everyStmtChild(
|
||||
expr: (expr: IrExpr) => boolean,
|
||||
stmt: (stmt: IrStmt) => boolean,
|
||||
): boolean {
|
||||
// The most common node kinds come first: the engine tests string cases
|
||||
// one after another, in source order.
|
||||
switch (node.kind) {
|
||||
case "varDecl":
|
||||
return node.init === null || expr(node.init);
|
||||
case "assign":
|
||||
return expr(node.value);
|
||||
case "exprStmt":
|
||||
return expr(node.expr);
|
||||
case "return":
|
||||
return node.value === null || expr(node.value);
|
||||
case "if":
|
||||
return (
|
||||
expr(node.cond) &&
|
||||
node.then.every((child) => stmt(child)) &&
|
||||
(node.else_ === null || node.else_.every((child) => stmt(child)))
|
||||
);
|
||||
case "varDecl":
|
||||
return node.init === null || expr(node.init);
|
||||
case "exprStmt":
|
||||
return expr(node.expr);
|
||||
case "assign":
|
||||
return expr(node.value);
|
||||
case "block":
|
||||
return node.body.every((child) => stmt(child));
|
||||
case "fieldSet":
|
||||
return expr(node.obj) && expr(node.value);
|
||||
case "while":
|
||||
return expr(node.cond) && node.body.every((child) => stmt(child));
|
||||
case "doWhile":
|
||||
@@ -393,10 +403,6 @@ export function everyStmtChild(
|
||||
return expr(node.arr) && expr(node.index) && expr(node.value);
|
||||
case "forOf":
|
||||
return expr(node.iterable) && node.body.every((child) => stmt(child));
|
||||
case "return":
|
||||
return node.value === null || expr(node.value);
|
||||
case "fieldSet":
|
||||
return expr(node.obj) && expr(node.value);
|
||||
case "recordSet":
|
||||
return expr(node.obj) && expr(node.value);
|
||||
case "recordKeySet":
|
||||
@@ -407,8 +413,6 @@ export function everyStmtChild(
|
||||
return true;
|
||||
case "continue":
|
||||
return true;
|
||||
case "block":
|
||||
return node.body.every((child) => stmt(child));
|
||||
case "throw":
|
||||
return expr(node.value);
|
||||
case "runtimeFence":
|
||||
@@ -432,21 +436,54 @@ export function mapExprChildren(
|
||||
expr: (expr: IrExpr) => IrExpr,
|
||||
stmt: (stmt: IrStmt) => IrStmt,
|
||||
): IrExpr {
|
||||
// The most common node kinds come first: the engine tests string cases
|
||||
// one after another, in source order.
|
||||
switch (node.kind) {
|
||||
case "numLit":
|
||||
case "varRef":
|
||||
return node;
|
||||
case "call":
|
||||
return { ...node, args: node.args.map((child) => expr(child)) };
|
||||
case "dynFrom":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "strLit":
|
||||
return node;
|
||||
case "moduleNsRef":
|
||||
case "libCall":
|
||||
return { ...node, args: node.args.map((child) => expr(child)) };
|
||||
case "dynCheck":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "strEq":
|
||||
return { ...node, left: expr(node.left), right: expr(node.right) };
|
||||
case "fieldGet":
|
||||
return { ...node, obj: expr(node.obj) };
|
||||
case "ternary":
|
||||
return { ...node, cond: expr(node.cond), then: expr(node.then), else_: expr(node.else_) };
|
||||
case "closure":
|
||||
return node;
|
||||
case "boolLit":
|
||||
return node;
|
||||
case "numLit":
|
||||
return node;
|
||||
case "unitLit":
|
||||
return node;
|
||||
case "varRef":
|
||||
return node;
|
||||
case "seqExpr":
|
||||
return { ...node, stmts: node.stmts.map((child) => stmt(child)), result: expr(node.result) };
|
||||
case "dynCall": {
|
||||
const result: Extract<IrExpr, { kind: "dynCall" }> = { ...node, callee: expr(node.callee) };
|
||||
if (node.receiver !== undefined) result.receiver = expr(node.receiver);
|
||||
if (node.calleeNameValue !== undefined) result.calleeNameValue = expr(node.calleeNameValue);
|
||||
result.args = node.args.map((child) => expr(child));
|
||||
return result;
|
||||
}
|
||||
case "dynTest":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "dynScalarEq":
|
||||
return { ...node, left: expr(node.left), right: expr(node.right) };
|
||||
case "upcast":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "bin":
|
||||
return { ...node, left: expr(node.left), right: expr(node.right) };
|
||||
case "moduleNsRef":
|
||||
return node;
|
||||
case "unary":
|
||||
return { ...node, operand: expr(node.operand) };
|
||||
case "incDec":
|
||||
@@ -461,14 +498,10 @@ export function mapExprChildren(
|
||||
return { ...node, left: expr(node.left), right: expr(node.right) };
|
||||
case "strConcat":
|
||||
return { ...node, left: expr(node.left), right: expr(node.right) };
|
||||
case "strEq":
|
||||
return { ...node, left: expr(node.left), right: expr(node.right) };
|
||||
case "strCmp":
|
||||
return { ...node, left: expr(node.left), right: expr(node.right) };
|
||||
case "toString":
|
||||
return { ...node, operand: expr(node.operand) };
|
||||
case "ternary":
|
||||
return { ...node, cond: expr(node.cond), then: expr(node.then), else_: expr(node.else_) };
|
||||
case "nullish":
|
||||
return { ...node, left: expr(node.left), right: expr(node.right) };
|
||||
case "orDefault":
|
||||
@@ -542,12 +575,8 @@ export function mapExprChildren(
|
||||
receiver: expr(node.receiver),
|
||||
args: node.args.map((child) => expr(child)),
|
||||
};
|
||||
case "call":
|
||||
return { ...node, args: node.args.map((child) => expr(child)) };
|
||||
case "ffiCall":
|
||||
return { ...node, args: node.args.map((child) => expr(child)) };
|
||||
case "closure":
|
||||
return node;
|
||||
case "callValue": {
|
||||
const callee = expr(node.callee);
|
||||
if (node.receiver === undefined)
|
||||
@@ -581,8 +610,6 @@ export function mapExprChildren(
|
||||
return { ...node, callee: expr(node.callee), args: node.args.map((child) => expr(child)) };
|
||||
case "instanceOfValue":
|
||||
return { ...node, value: expr(node.value), classValue: expr(node.classValue) };
|
||||
case "upcast":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "promiseVoidWiden":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "downcast":
|
||||
@@ -591,8 +618,6 @@ export function mapExprChildren(
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "virtualCall":
|
||||
return { ...node, args: node.args.map((child) => expr(child)) };
|
||||
case "fieldGet":
|
||||
return { ...node, obj: expr(node.obj) };
|
||||
case "recordLit":
|
||||
return {
|
||||
...node,
|
||||
@@ -608,15 +633,6 @@ export function mapExprChildren(
|
||||
return { ...node, obj: expr(node.obj) };
|
||||
case "recordKeyGet":
|
||||
return { ...node, obj: expr(node.obj), key: expr(node.key) };
|
||||
case "dynFrom":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "dynCall": {
|
||||
const result: Extract<IrExpr, { kind: "dynCall" }> = { ...node, callee: expr(node.callee) };
|
||||
if (node.receiver !== undefined) result.receiver = expr(node.receiver);
|
||||
if (node.calleeNameValue !== undefined) result.calleeNameValue = expr(node.calleeNameValue);
|
||||
result.args = node.args.map((child) => expr(child));
|
||||
return result;
|
||||
}
|
||||
case "dynInvoke": {
|
||||
const result: Extract<IrExpr, { kind: "dynInvoke" }> = { ...node, recv: expr(node.recv) };
|
||||
if (node.calleeNameValue !== undefined) result.calleeNameValue = expr(node.calleeNameValue);
|
||||
@@ -636,16 +652,10 @@ export function mapExprChildren(
|
||||
value: expr(child.value),
|
||||
})),
|
||||
};
|
||||
case "dynTest":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "dynKeyGet":
|
||||
return { ...node, key: expr(node.key), value: expr(node.value) };
|
||||
case "dynHasKey":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "dynScalarEq":
|
||||
return { ...node, left: expr(node.left), right: expr(node.right) };
|
||||
case "seqExpr":
|
||||
return { ...node, stmts: node.stmts.map((child) => stmt(child)), result: expr(node.result) };
|
||||
case "dynDestrCheck":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "dynIterN":
|
||||
@@ -686,12 +696,8 @@ export function mapExprChildren(
|
||||
return { ...node, left: expr(node.left), right: expr(node.right) };
|
||||
case "intrinsic":
|
||||
return { ...node, args: node.args.map((child) => expr(child)) };
|
||||
case "libCall":
|
||||
return { ...node, args: node.args.map((child) => expr(child)) };
|
||||
case "jsonStringify":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "dynCheck":
|
||||
return { ...node, value: expr(node.value) };
|
||||
}
|
||||
node satisfies never;
|
||||
throw new Error("unhandled IR node");
|
||||
@@ -703,13 +709,11 @@ export function mapStmtChildren(
|
||||
expr: (expr: IrExpr) => IrExpr,
|
||||
stmt: (stmt: IrStmt) => IrStmt,
|
||||
): IrStmt {
|
||||
// The most common node kinds come first: the engine tests string cases
|
||||
// one after another, in source order.
|
||||
switch (node.kind) {
|
||||
case "varDecl":
|
||||
return { ...node, init: node.init === null ? null : expr(node.init) };
|
||||
case "assign":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "exprStmt":
|
||||
return { ...node, expr: expr(node.expr) };
|
||||
case "return":
|
||||
return { ...node, value: node.value === null ? null : expr(node.value) };
|
||||
case "if":
|
||||
return {
|
||||
...node,
|
||||
@@ -717,6 +721,16 @@ export function mapStmtChildren(
|
||||
then: node.then.map((child) => stmt(child)),
|
||||
else_: node.else_ === null ? null : node.else_.map((child) => stmt(child)),
|
||||
};
|
||||
case "varDecl":
|
||||
return { ...node, init: node.init === null ? null : expr(node.init) };
|
||||
case "exprStmt":
|
||||
return { ...node, expr: expr(node.expr) };
|
||||
case "assign":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "block":
|
||||
return { ...node, body: node.body.map((child) => stmt(child)) };
|
||||
case "fieldSet":
|
||||
return { ...node, obj: expr(node.obj), value: expr(node.value) };
|
||||
case "while":
|
||||
return { ...node, cond: expr(node.cond), body: node.body.map((child) => stmt(child)) };
|
||||
case "doWhile":
|
||||
@@ -755,10 +769,6 @@ export function mapStmtChildren(
|
||||
iterable: expr(node.iterable),
|
||||
body: node.body.map((child) => stmt(child)),
|
||||
};
|
||||
case "return":
|
||||
return { ...node, value: node.value === null ? null : expr(node.value) };
|
||||
case "fieldSet":
|
||||
return { ...node, obj: expr(node.obj), value: expr(node.value) };
|
||||
case "recordSet":
|
||||
return { ...node, obj: expr(node.obj), value: expr(node.value) };
|
||||
case "recordKeySet":
|
||||
@@ -769,8 +779,6 @@ export function mapStmtChildren(
|
||||
return node;
|
||||
case "continue":
|
||||
return node;
|
||||
case "block":
|
||||
return { ...node, body: node.body.map((child) => stmt(child)) };
|
||||
case "throw":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "runtimeFence":
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6077-generator-helpers-untyped.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6078-destructure-untyped-iterables.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6079-generator-helpers-any.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -714,6 +714,22 @@ double scr_dyn_collection_iterator_index(const ScrDyn *iterator);
|
||||
* than 2 and 3) and the index its next step reads from. */
|
||||
void scr_dyn_native_iterator_state(const ScrDyn *iterator, unsigned *kind, unsigned *selection,
|
||||
ScrMap **collection, double *index);
|
||||
/* A compiled program that boxes generators and calls iterator helpers on
|
||||
* untyped values registers the checked functions that read a generator
|
||||
* capsule's next and return methods (each takes the capsule). The
|
||||
* iterator helpers below then drive compiled generators. */
|
||||
void scr_dyn_set_generator_members(ScrDyn *next_getter, ScrDyn *return_getter);
|
||||
/* An iterator helper method called on a synchronous generator capsule:
|
||||
* true with *out set (+1, or NULL with the exception pending) when
|
||||
* `method` is one. NULL until the program registers its generator
|
||||
* members. */
|
||||
extern SCR_TL bool (*scr_dyn_generator_helper_hook)(ScrDyn *recv, const char *method,
|
||||
ScrDyn *const *args, size_t argc,
|
||||
ScrDyn **out);
|
||||
/* The Iterator.prototype helper method `name` as a function value (+1),
|
||||
* the value a compiled generator's helper property reads; undefined for
|
||||
* other names. */
|
||||
ScrDyn *scr_dyn_iterator_helper_method(const ScrStr *name);
|
||||
/* The ordinary object snapshot plus a retained typed source. source_access
|
||||
* releases that source when materialize=false and returns a fresh snapshot
|
||||
* when true. */
|
||||
@@ -858,6 +874,19 @@ void scr_dyn_arr_push_spread(ScrDyn *arr, const ScrDyn *src, const char *what);
|
||||
* compile-time source spelling), else the runtime kind wording. Borrows
|
||||
* both; NULL with the exception pending on the throw. */
|
||||
ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg);
|
||||
/* A rest element's pack (`const [a, ...rest] = d`): scr_dyn_iter_pack's
|
||||
* kinds pack as there, and other iterables step through the iterator
|
||||
* protocol to the end. +1, or NULL with the error pending. */
|
||||
ScrDyn *scr_dyn_destructure_rest(const ScrDyn *src, const ScrStr *msg);
|
||||
/* Destructuring's first `n` values (`const [a, b] = d`) of a dyn source
|
||||
* other than an array, string or bytes (the emitted walk packs those),
|
||||
* through the iterator protocol: an iterator with values left closes.
|
||||
* `spell` is the compile-time not-iterable wording, or NULL. +1, or NULL
|
||||
* with the error pending. */
|
||||
ScrDyn *scr_dyn_iter_take(const ScrDyn *src, size_t n, const char *spell);
|
||||
/* True for typed capsules that iterate through the protocol (a compiled
|
||||
* synchronous generator) rather than through their materialized view. */
|
||||
bool scr_dyn_iterates_by_protocol(const ScrDyn *value);
|
||||
/* Map constructor seeds: retain array inputs for live iteration, accept
|
||||
* nullish inputs as empty, and validate each entry before reading 0/1. */
|
||||
ScrDyn *scr_dyn_map_seed_entries(const ScrDyn *src);
|
||||
|
||||
@@ -815,6 +815,10 @@ static ScrDyn *scr_dyn_invoke_impl(
|
||||
bool handled = false;
|
||||
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 &&
|
||||
scr_dyn_generator_helper_hook(recv, method, args, argc, &fast))
|
||||
return fast;
|
||||
if (scr_event_ref_hooks &&
|
||||
scr_event_ref_hooks->invoke(recv, method, args, argc, what, &fast))
|
||||
return fast;
|
||||
|
||||
@@ -4704,6 +4704,19 @@ static int scr_ih_method_index(const char *name, size_t len);
|
||||
static ScrDyn *scr_iterator_helper_method(ScrDyn *self, int index, ScrDyn *const *args, size_t argc);
|
||||
static ScrDyn *scr_ih_method_function(int index);
|
||||
|
||||
static bool scr_dyn_sync_generator(const ScrDyn *value);
|
||||
extern SCR_TL ScrDyn *(*scr_generator_member_hook)(const ScrDyn *, const char *, size_t);
|
||||
|
||||
/* scr_iterator_read of an iterator's own next or return method: a
|
||||
* compiled generator answers through the program's registered getters. */
|
||||
static ScrDyn *scr_iterator_member(const ScrDyn *iterator, const char *key, size_t len) {
|
||||
if (scr_generator_member_hook && scr_dyn_sync_generator(iterator)) {
|
||||
ScrDyn *member = scr_generator_member_hook(iterator, key, len);
|
||||
if (member) return member;
|
||||
}
|
||||
return scr_iterator_read(iterator, key, len);
|
||||
}
|
||||
|
||||
static ScrDyn *scr_native_iterator_get(void *ptr, const char *key, size_t len) {
|
||||
if (len != 4 || memcmp(key, "next", 4)) {
|
||||
/* The Iterator.prototype helper methods. */
|
||||
@@ -4893,7 +4906,7 @@ static bool scr_ih_budget(const ScrDyn *limit, double *out) {
|
||||
* Node's TypeError. Answers false when an exception is pending after. */
|
||||
static bool scr_ih_close(ScrDyn *iterator, bool abrupt) {
|
||||
ScrCaught *first = abrupt ? scr_exc_take() : NULL;
|
||||
ScrDyn *method = scr_iterator_read(iterator, "return", 6);
|
||||
ScrDyn *method = scr_iterator_member(iterator, "return", 6);
|
||||
bool ok = method != NULL;
|
||||
if (ok && method->kind != SCR_DYN_UNDEF && method->kind != SCR_DYN_NULL) {
|
||||
ScrDyn *result = scr_iterator_call(method, iterator, "return");
|
||||
@@ -4960,7 +4973,7 @@ static ScrDyn *scr_ih_direct(const ScrDyn *self, const char *method) {
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
|
||||
return NULL;
|
||||
}
|
||||
return scr_iterator_read(self, "next", 4);
|
||||
return scr_iterator_member(self, "next", 4);
|
||||
}
|
||||
|
||||
static ScrDyn *scr_ih_result(ScrDyn *value, bool done) {
|
||||
@@ -5017,7 +5030,7 @@ static ScrDyn *scr_ih_flattenable(ScrDyn *value, ScrDyn **next_out) {
|
||||
}
|
||||
scr_dyn_release(method);
|
||||
if (!iterator) return NULL;
|
||||
ScrDyn *next = scr_iterator_read(iterator, "next", 4);
|
||||
ScrDyn *next = scr_iterator_member(iterator, "next", 4);
|
||||
if (!next) {
|
||||
scr_dyn_release(iterator);
|
||||
return NULL;
|
||||
@@ -13967,3 +13980,115 @@ ScrDyn *scr_caught_to_dyn(const ScrCaught *c) {
|
||||
return scr_dyn_new_obj();
|
||||
}
|
||||
}
|
||||
|
||||
/* ── compiled generators through untyped values ─────────────────────── */
|
||||
|
||||
static SCR_TL ScrDyn *scr_generator_next_getter;
|
||||
static SCR_TL ScrDyn *scr_generator_return_getter;
|
||||
/* Set with the getters, so only programs that register them link the
|
||||
* iterator helpers through these paths. External, so the compiler keeps
|
||||
* the calls through them indirect. */
|
||||
SCR_TL ScrDyn *(*scr_generator_member_hook)(const ScrDyn *, const char *, size_t) = NULL;
|
||||
SCR_TL bool (*scr_dyn_generator_helper_hook)(ScrDyn *, const char *, ScrDyn *const *, size_t,
|
||||
ScrDyn **) = NULL;
|
||||
static ScrDyn *scr_generator_member(const ScrDyn *capsule, const char *key, size_t len);
|
||||
static bool scr_generator_helper_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args,
|
||||
size_t argc, ScrDyn **out);
|
||||
|
||||
static void scr_generator_members_cleanup(void) {
|
||||
scr_dyn_release(scr_generator_next_getter);
|
||||
scr_dyn_release(scr_generator_return_getter);
|
||||
scr_generator_next_getter = NULL;
|
||||
scr_generator_return_getter = NULL;
|
||||
scr_generator_member_hook = NULL;
|
||||
scr_dyn_generator_helper_hook = NULL;
|
||||
}
|
||||
|
||||
void scr_dyn_set_generator_members(ScrDyn *next_getter, ScrDyn *return_getter) {
|
||||
if (!scr_generator_next_getter) scr_atexit(scr_generator_members_cleanup);
|
||||
scr_dyn_release(scr_generator_next_getter);
|
||||
scr_dyn_release(scr_generator_return_getter);
|
||||
scr_generator_next_getter = scr_dyn_retain(next_getter);
|
||||
scr_generator_return_getter = scr_dyn_retain(return_getter);
|
||||
scr_generator_member_hook = &scr_generator_member;
|
||||
scr_dyn_generator_helper_hook = &scr_generator_helper_invoke;
|
||||
}
|
||||
|
||||
static bool scr_dyn_sync_generator(const ScrDyn *value) {
|
||||
return scr_dyn_generator(value) && value->v.typed_ref.type_key[0] == 'g';
|
||||
}
|
||||
|
||||
/* A synchronous generator capsule's next or return method (+1), read
|
||||
* through the program's registered getters. NULL for other keys. */
|
||||
static ScrDyn *scr_generator_member(const ScrDyn *capsule, const char *key, size_t len) {
|
||||
ScrDyn *getter = len == 4 && !memcmp(key, "next", 4) ? scr_generator_next_getter
|
||||
: len == 6 && !memcmp(key, "return", 6) ? scr_generator_return_getter : NULL;
|
||||
if (!getter) return NULL;
|
||||
ScrDyn *args[1] = { (ScrDyn *)capsule };
|
||||
return scr_dyn_call(getter, args, 1, "next");
|
||||
}
|
||||
|
||||
static bool scr_generator_helper_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args,
|
||||
size_t argc, ScrDyn **out) {
|
||||
if (!scr_dyn_sync_generator(recv)) return false;
|
||||
int index = scr_ih_method_index(method, strlen(method));
|
||||
if (index < 0) return false;
|
||||
*out = scr_iterator_helper_method(recv, index, args, argc);
|
||||
return true;
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_iterator_helper_method(const ScrStr *name) {
|
||||
int index = scr_ih_method_index(name->data, name->len);
|
||||
return index >= 0 ? scr_ih_method_function(index) : scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
|
||||
/* Destructuring over an iterable the pack walk cannot list (a compiled
|
||||
* generator, an iterator or iterator helper, a Map or Set): GetIterator,
|
||||
* then the first `n` values through the iterator protocol, or every value
|
||||
* when `all`, into a fresh array. Positions past the end read undefined,
|
||||
* and an iterator with values left closes. `spell` is the compile-time
|
||||
* not-iterable wording, or NULL. +1, or NULL with the error pending. */
|
||||
static ScrDyn *scr_dyn_iter_steps(const ScrDyn *src, const ScrStr *spell, size_t n, bool all) {
|
||||
ScrDyn *iterator = scr_dyn_iterator(src, spell);
|
||||
if (!iterator) return NULL;
|
||||
ScrDyn *next = scr_iterator_member(iterator, "next", 4);
|
||||
if (!next) {
|
||||
scr_dyn_release(iterator);
|
||||
return NULL;
|
||||
}
|
||||
ScrDyn *out = scr_dyn_new_arr();
|
||||
bool done = false;
|
||||
for (size_t i = 0; all ? !done : i < n; i++) {
|
||||
ScrDyn *value = done ? scr_dyn_retain(scr_dyn_undefined()) : scr_ih_step(iterator, next, &done);
|
||||
if (!value) goto fail;
|
||||
if (all && done) scr_dyn_release(value);
|
||||
else scr_dyn_arr_push(out, value);
|
||||
}
|
||||
if (!done && !scr_ih_close(iterator, false)) goto fail;
|
||||
scr_dyn_release(next);
|
||||
scr_dyn_release(iterator);
|
||||
return out;
|
||||
fail:
|
||||
scr_dyn_release(out);
|
||||
scr_dyn_release(next);
|
||||
scr_dyn_release(iterator);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_destructure_rest(const ScrDyn *src, const ScrStr *msg) {
|
||||
bool packs = src->kind == SCR_DYN_ARR || src->kind == SCR_DYN_STR || src->kind == SCR_DYN_BYTES ||
|
||||
(src->kind == SCR_DYN_HANDLE && scr_dyn_handle_ops_of(src)->iter_pack) ||
|
||||
(src->kind == SCR_DYN_TYPED_REF && !scr_dyn_iterates_by_protocol(src));
|
||||
return packs ? scr_dyn_iter_pack(src, msg) : scr_dyn_iter_steps(src, msg, 0, true);
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_iter_take(const ScrDyn *src, size_t n, const char *spell) {
|
||||
ScrStr *spelling = spell && *spell ? scr_str_new(spell, strlen(spell)) : NULL;
|
||||
ScrDyn *out = scr_dyn_iter_steps(src, spelling, n, false);
|
||||
scr_str_release(spelling);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool scr_dyn_iterates_by_protocol(const ScrDyn *value) {
|
||||
return scr_dyn_sync_generator(value);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
#!/usr/bin/env node
|
||||
// Refreshes tests/harness/corpus-costs.json from the logs of the Linux test shards of green CI runs on main.
|
||||
//
|
||||
// It first folds the per-program cost files (tests/harness/corpus-costs/<program>.json, which changes that add or
|
||||
// remeasure a program write instead of editing the table) into the table and deletes them.
|
||||
//
|
||||
// Usage: node scripts/ci-corpus-costs.mjs <job log> ...
|
||||
//
|
||||
// Each log is a downloaded "test (Linux, plain|san, i/10)" job log. vitest prints every case slower than its
|
||||
// threshold with its duration; for each program this takes the median of its differential and of its
|
||||
// llvm-differential case over the logs of its flavor, and stores their sum. Programs without a measurement keep
|
||||
// their current value.
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { globSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const path = new URL("../tests/harness/corpus-costs.json", import.meta.url);
|
||||
const table = JSON.parse(readFileSync(path, "utf8"));
|
||||
const filesDir = fileURLToPath(new URL("../tests/harness/corpus-costs", import.meta.url));
|
||||
let folded = 0;
|
||||
for (const rel of globSync("**/*.json", { cwd: filesDir })) {
|
||||
const key = rel.replaceAll("\\", "/").slice(0, -".json".length);
|
||||
const entry = JSON.parse(readFileSync(join(filesDir, rel), "utf8"));
|
||||
for (const flavor of ["plain", "san"])
|
||||
if (entry[flavor] !== undefined) table[flavor][key] = entry[flavor];
|
||||
rmSync(join(filesDir, rel));
|
||||
folded++;
|
||||
}
|
||||
const samples = { plain: {}, san: {} };
|
||||
for (const log of process.argv.slice(2)) {
|
||||
// Logs keep the terminal color codes (ESC [ ... m); drop them before matching.
|
||||
@@ -40,4 +55,4 @@ for (const flavor of ["plain", "san"])
|
||||
Object.entries(table[flavor]).sort(([a], [b]) => a.localeCompare(b)),
|
||||
);
|
||||
writeFileSync(path, JSON.stringify(table, null, 2) + "\n");
|
||||
console.log(`updated ${updated} program costs`);
|
||||
console.log(`updated ${updated} program costs, folded ${folded} per-program cost files`);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Iterator helpers on compiled generators that reach untyped parameters:
|
||||
// the runtime's helpers step the generator through its own next() and
|
||||
// close it through return().
|
||||
const log = [];
|
||||
const flush = () => log.splice(0).join(" ");
|
||||
function* count(n) {
|
||||
try {
|
||||
for (let i = 1; i <= n; i++) {
|
||||
log.push("y" + i);
|
||||
yield i;
|
||||
}
|
||||
return "done";
|
||||
} finally {
|
||||
log.push("closed");
|
||||
}
|
||||
}
|
||||
function twice(it) { return it.map((x) => x * 2).toArray(); }
|
||||
console.log(twice(count(3)), flush());
|
||||
function firstOver(it, k) { return it.find((x) => x > k); }
|
||||
console.log(firstOver(count(9), 3), flush());
|
||||
function stepper(it) {
|
||||
const h = it.filter((x) => x % 2 === 1);
|
||||
return [h.next(), h.next(), h.return(), h.next()];
|
||||
}
|
||||
console.log(stepper(count(9)), flush());
|
||||
function sum(it) { return it.reduce((a, b) => a + b, 0); }
|
||||
console.log(sum(count(4)), flush());
|
||||
function product(it) { return it.reduce((a, b) => a * b); }
|
||||
console.log(product(count(4)), flush());
|
||||
function lazy(it) { return it.drop(1).take(2); }
|
||||
console.log([...lazy(count(9))], flush());
|
||||
function each(it) { const out = []; it.forEach((x, i) => out.push(x + ":" + i)); return out; }
|
||||
console.log(each(count(3)), flush());
|
||||
function some(it) { return it.some((x) => x === 2); }
|
||||
console.log(some(count(5)), flush());
|
||||
function every(it) { return it.every((x) => x < 3); }
|
||||
console.log(every(count(5)), flush());
|
||||
function flat(it) { return it.flatMap((x) => [x, -x]).toArray(); }
|
||||
console.log(flat(count(2)), flush());
|
||||
function chain(it) { return it.map((x) => x + 1).filter((x) => x % 2 === 0).map(String).toArray(); }
|
||||
console.log(chain(count(6)), flush());
|
||||
function loop(it) {
|
||||
const out = [];
|
||||
for (const x of it.map((x) => x * 10)) {
|
||||
out.push(x);
|
||||
if (x >= 20) break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
console.log(loop(count(5)), flush());
|
||||
function from(it) { return Array.from(it.take(2)); }
|
||||
console.log(from(count(5)), flush());
|
||||
function zeroTake(it) { return it.take(0).toArray(); }
|
||||
console.log(zeroTake(count(5)), flush());
|
||||
function after(it) { const a = it.take(1).toArray(); return [a, it.next()]; }
|
||||
console.log(after(count(3)), flush());
|
||||
function failing(it) {
|
||||
try {
|
||||
it.map((x) => { if (x === 2) throw new Error("boom " + x); return x; }).toArray();
|
||||
} catch (e) {
|
||||
return e.message;
|
||||
}
|
||||
}
|
||||
console.log(failing(count(5)), flush());
|
||||
function errorOf(run) { try { run(); } catch (e) { return e.name + ": " + e.message; } }
|
||||
function check(it) {
|
||||
return [
|
||||
errorOf(() => it.map(42)),
|
||||
errorOf(() => it.take(-1)),
|
||||
errorOf(() => it.reduce((a, b) => a + b)),
|
||||
];
|
||||
}
|
||||
console.log(check(count(0)), flush());
|
||||
function kinds(it) { return [typeof it.map, typeof it.toArray, typeof it.next]; }
|
||||
console.log(kinds(count(1)));
|
||||
function readNext(it) { const next = it.next; return [next.call(it), next.call(it)]; }
|
||||
console.log(readNext(count(5)), flush());
|
||||
function first(it) { for (const x of it) return x; }
|
||||
console.log(first(count(3)), flush());
|
||||
function closeEarly(it) { it.next(); return it.return(); }
|
||||
console.log(closeEarly(count(3)), flush());
|
||||
@@ -0,0 +1,60 @@
|
||||
// Array destructuring of untyped iterables: Map and Set objects, their
|
||||
// iterators, compiled generators and objects with a Symbol.iterator method
|
||||
// step through the iterator protocol, and an iterator with values left
|
||||
// closes.
|
||||
const m = new Map([["a", 1], ["b", 2], ["c", 3]]);
|
||||
function two(it) { const [x, y] = it; return [x, y]; }
|
||||
console.log(two(m), two(new Set([7, 8, 9])), two(m.keys()), two("hey"), two([1]));
|
||||
function rest(it) { const [x, ...more] = it; return [x, more]; }
|
||||
console.log(rest(m.values()), rest(new Set([1, 2, 3])), rest(m.entries()));
|
||||
function* g() {
|
||||
try {
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
} finally {
|
||||
console.log("finally");
|
||||
}
|
||||
}
|
||||
console.log(two(g()), rest(g()));
|
||||
function holes(it) { const [, b, , d] = it; return [b, d]; }
|
||||
console.log(holes(g()));
|
||||
function none(it) { const [] = it; return "none"; }
|
||||
console.log(none(g()));
|
||||
function* short() { yield "only"; }
|
||||
console.log(two(short()));
|
||||
const custom = {
|
||||
[Symbol.iterator]() {
|
||||
let i = 0;
|
||||
return {
|
||||
next: () => ({ value: i++, done: i > 5 }),
|
||||
return() {
|
||||
console.log("return called");
|
||||
return {};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
console.log(two(custom));
|
||||
const helper = [1, 2, 3, 4].values().map((x) => x * 3);
|
||||
console.log(two(helper), [...helper]);
|
||||
for (const bad of [1, null, undefined, {}, true]) {
|
||||
try {
|
||||
two(bad);
|
||||
} catch (e) {
|
||||
console.log(e.name, e.message);
|
||||
}
|
||||
}
|
||||
const throwing = {
|
||||
[Symbol.iterator]() {
|
||||
return {
|
||||
next() { throw new Error("next failed"); },
|
||||
return() { console.log("not called"); return {}; },
|
||||
};
|
||||
},
|
||||
};
|
||||
try {
|
||||
two(throwing);
|
||||
} catch (e) {
|
||||
console.log(e.message);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Iterator helpers on generators passed through `any` and `unknown`.
|
||||
function* letters(): Generator<string, void, unknown> {
|
||||
yield "a";
|
||||
yield "b";
|
||||
yield "c";
|
||||
}
|
||||
function upper(it: any): string[] {
|
||||
return it.map((s: string) => s.toUpperCase()).toArray();
|
||||
}
|
||||
console.log(upper(letters()));
|
||||
const boxed: unknown = letters();
|
||||
console.log((boxed as any).filter((s: string) => s !== "b").toArray());
|
||||
const held: any[] = [letters(), letters()];
|
||||
console.log(held.map((h) => h.drop(1).toArray()));
|
||||
function* numbered(): Generator<number, string, unknown> {
|
||||
yield 1;
|
||||
yield 2;
|
||||
return "end";
|
||||
}
|
||||
function firstTwo(it: any): number[] {
|
||||
return it.take(2).toArray();
|
||||
}
|
||||
const later: any = numbered();
|
||||
console.log(firstTwo(numbered()), later.some((n: number) => n > 1));
|
||||
@@ -2252,6 +2252,9 @@
|
||||
"6074-stored-collection-helpers-js.js": 4100,
|
||||
"6075-print-iterators.ts": 3800,
|
||||
"6076-print-iterators-js.js": 3500,
|
||||
"6077-generator-helpers-untyped.js": 2550,
|
||||
"6078-destructure-untyped-iterables.js": 2500,
|
||||
"6079-generator-helpers-any.ts": 2550,
|
||||
"6100-record-get-or-create.ts": 3771,
|
||||
"6101-record-get-or-create-unchecked/main.ts": 2107,
|
||||
"6103-typeof-builtin-bindings.cjs": 2202,
|
||||
@@ -5341,6 +5344,9 @@
|
||||
"6074-stored-collection-helpers-js.js": 3600,
|
||||
"6075-print-iterators.ts": 3450,
|
||||
"6076-print-iterators-js.js": 3300,
|
||||
"6077-generator-helpers-untyped.js": 3050,
|
||||
"6078-destructure-untyped-iterables.js": 2550,
|
||||
"6079-generator-helpers-any.ts": 2700,
|
||||
"6100-record-get-or-create.ts": 3173,
|
||||
"6101-record-get-or-create-unchecked/main.ts": 3228,
|
||||
"6103-typeof-builtin-bindings.cjs": 1796,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Per-program corpus costs
|
||||
|
||||
A change that adds a corpus program, or changes how long one takes, records the program's CI cost here instead of editing `../corpus-costs.json`: one file per program, named after it, with the milliseconds of its differential plus llvm-differential case per flavor.
|
||||
|
||||
```json
|
||||
{ "plain": 2700, "san": 3200 }
|
||||
```
|
||||
|
||||
`5637-child-max-buffer-sync.ts` goes in `5637-child-max-buffer-sync.ts.json`, and a directory program such as `2946-child-fork-ipc/main.ts` in `2946-child-fork-ipc/main.ts.json`. Either flavor may be left out. These files override the table for shard balancing (`tests/harness/shard.ts`), and `scripts/ci-corpus-costs.mjs` folds them into the table and deletes them when it refreshes the costs from CI logs. Separate files keep changes that add programs from editing the same lines of the table.
|
||||
@@ -36,7 +36,7 @@ const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests");
|
||||
// SCRIPTC_TEST_SHARD="i/n" (CI's matrix) keeps only this shard's slice of
|
||||
// the corpus — the file is this suite's wall-time monster, so vitest's
|
||||
// file-granular --shard alone cannot split it. Slices are balanced by each
|
||||
// program's measured cost (corpus-costs.json). Unset = everything.
|
||||
// program's measured cost (corpus-costs.json and corpus-costs/). Unset = everything.
|
||||
const ENTRY_EXTS = ["ts", "js", "mjs", "cjs"];
|
||||
const files = corpusSelect(
|
||||
ENTRY_EXTS.flatMap((ext) => [
|
||||
|
||||
@@ -3,19 +3,20 @@
|
||||
* depends only on the item's key (list growth never moves an existing item),
|
||||
* and no spec means everything. differential/llvm-differential/npm/server
|
||||
* lean on these properties for CI sharding — see shard.ts. */
|
||||
import { globSync } from "node:fs";
|
||||
import { globSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { basename, join } from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
||||
import {
|
||||
balancedShardSelect,
|
||||
corpusCostFiles,
|
||||
corpusSelect,
|
||||
loadCorpusCosts,
|
||||
parseShardSpec,
|
||||
shardOf,
|
||||
shardSelect,
|
||||
shardSuffix,
|
||||
skipUnownedTest,
|
||||
} from "./shard.js";
|
||||
import corpusCosts from "./corpus-costs.json";
|
||||
import fixtureCosts from "./effect4-costs.json";
|
||||
import effectCosts from "./effect-costs.json";
|
||||
|
||||
@@ -268,7 +269,7 @@ describe("corpusSelect", () => {
|
||||
);
|
||||
|
||||
test("measured costs cover the corpus and balance the shards", () => {
|
||||
const costs = corpusCosts.plain as Record<string, number>;
|
||||
const costs = loadCorpusCosts().plain;
|
||||
const measured = programs.filter((k) => costs[k] !== undefined).length;
|
||||
expect(measured / programs.length).toBeGreaterThan(0.9);
|
||||
const loads = Array.from({ length: 10 }, (_, i) =>
|
||||
@@ -279,4 +280,30 @@ describe("corpusSelect", () => {
|
||||
);
|
||||
expect(Math.max(...loads) / Math.min(...loads)).toBeLessThan(1.1);
|
||||
});
|
||||
|
||||
test("every per-program cost file names a corpus program", () => {
|
||||
const known = new Set(programs);
|
||||
for (const key of corpusCostFiles().keys())
|
||||
expect(known, `corpus-costs/${key}.json`).toContain(key);
|
||||
});
|
||||
|
||||
test("per-program cost files override the table, and invalid ones throw", () => {
|
||||
const dir = join(
|
||||
import.meta.dirname,
|
||||
"../../node_modules/.cache/scriptc-tests/corpus-costs-files",
|
||||
);
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
mkdirSync(join(dir, "200-dir"), { recursive: true });
|
||||
writeFileSync(join(dir, "100-flat.ts.json"), '{ "plain": 2500, "san": 4000 }\n');
|
||||
writeFileSync(join(dir, "200-dir/main.ts.json"), '{ "san": 900 }\n');
|
||||
expect([...corpusCostFiles(dir)]).toEqual([
|
||||
["100-flat.ts", { plain: 2500, san: 4000 }],
|
||||
["200-dir/main.ts", { san: 900 }],
|
||||
]);
|
||||
for (const bad of ['{ "plian": 1 }', '{ "plain": 0 }', '{ "san": "9" }']) {
|
||||
writeFileSync(join(dir, "100-flat.ts.json"), bad);
|
||||
expect(() => corpusCostFiles(dir)).toThrow(/plain|san/);
|
||||
}
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
+49
-7
@@ -16,7 +16,9 @@
|
||||
* everything: local runs are unchanged.
|
||||
*/
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { globSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { Suite, Test, TestContext } from "vitest";
|
||||
|
||||
export interface ShardSpec {
|
||||
@@ -94,11 +96,53 @@ export function balancedShardSelect<T>(
|
||||
return items.filter((_, index) => owners[index] === spec.index);
|
||||
}
|
||||
|
||||
/** Measured milliseconds per corpus program and flavor (corpus-costs.json): its differential plus
|
||||
* llvm-differential case on a CI runner. */
|
||||
type CorpusCosts = { plain: Record<string, number>; san: Record<string, number> };
|
||||
/** Measured milliseconds per corpus program and flavor: its differential plus llvm-differential case on a CI
|
||||
* runner. corpus-costs.json is the table scripts/ci-corpus-costs.mjs refreshes from CI logs. A change that adds
|
||||
* or remeasures a program records its costs in corpus-costs/<program>.json instead (`{ "plain": ms, "san": ms }`,
|
||||
* a directory program as corpus-costs/<dir>/main.ts.json), which overrides the table until the next refresh
|
||||
* folds it in. One file per program keeps changes that add programs from editing the same lines. */
|
||||
export type CorpusCosts = { plain: Record<string, number>; san: Record<string, number> };
|
||||
let corpusCostTable: CorpusCosts | undefined;
|
||||
|
||||
/** The per-program cost files: program key → its file's costs. Throws on a field other than plain/san or a
|
||||
* cost that is not a positive finite number. */
|
||||
export function corpusCostFiles(
|
||||
dir: string = fileURLToPath(new URL("./corpus-costs", import.meta.url)),
|
||||
): Map<string, Partial<Record<"plain" | "san", number>>> {
|
||||
const files = new Map<string, Partial<Record<"plain" | "san", number>>>();
|
||||
for (const rel of globSync("**/*.json", { cwd: dir })
|
||||
.map((path) => path.replaceAll("\\", "/"))
|
||||
.sort()) {
|
||||
const key = rel.slice(0, -".json".length);
|
||||
const entry = JSON.parse(readFileSync(join(dir, rel), "utf8")) as Record<string, unknown>;
|
||||
for (const [flavor, cost] of Object.entries(entry)) {
|
||||
if (
|
||||
(flavor !== "plain" && flavor !== "san") ||
|
||||
typeof cost !== "number" ||
|
||||
!Number.isFinite(cost) ||
|
||||
cost <= 0
|
||||
)
|
||||
throw new Error(
|
||||
`corpus-costs/${rel}: ${flavor} must be "plain" or "san" with a positive cost`,
|
||||
);
|
||||
}
|
||||
files.set(key, entry as Partial<Record<"plain" | "san", number>>);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
/** corpus-costs.json with the per-program files applied. */
|
||||
export function loadCorpusCosts(): CorpusCosts {
|
||||
const table = JSON.parse(
|
||||
readFileSync(new URL("./corpus-costs.json", import.meta.url), "utf8"),
|
||||
) as CorpusCosts;
|
||||
for (const [key, entry] of corpusCostFiles()) {
|
||||
if (entry.plain !== undefined) table.plain[key] = entry.plain;
|
||||
if (entry.san !== undefined) table.san[key] = entry.san;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
/** The corpus programs this shard owns, balanced by measured cost. differential.test.ts and
|
||||
* llvm-differential.test.ts both select with this, so a shard owns the same programs in both lanes and their
|
||||
* compile cache serves both. A program without a measurement weighs as much as the median measured one, so
|
||||
@@ -109,9 +153,7 @@ export function corpusSelect<T>(
|
||||
sanitize: boolean,
|
||||
spec: ShardSpec | undefined = parseShardSpec(),
|
||||
): T[] {
|
||||
corpusCostTable ??= JSON.parse(
|
||||
readFileSync(new URL("./corpus-costs.json", import.meta.url), "utf8"),
|
||||
) as CorpusCosts;
|
||||
corpusCostTable ??= loadCorpusCosts();
|
||||
const costs = sanitize ? corpusCostTable.san : corpusCostTable.plain;
|
||||
const known = Object.values(costs).sort((a, b) => a - b);
|
||||
const median = known.length === 0 ? 1 : known[Math.floor(known.length / 2)]!;
|
||||
|
||||
Reference in New Issue
Block a user