mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Merge main
This commit is contained in:
@@ -217,17 +217,22 @@ jobs:
|
||||
# worker budget covers both processes; file and case shard axes stay
|
||||
# separate so every fixture still runs exactly once in each lane.
|
||||
# The tests run in a transient scope capped below the runner's 16 GB, so
|
||||
# the runner itself keeps memory: past 14 GB of RAM the scope swaps, as
|
||||
# the whole runner did before (a sanitized shard peaks near 16 GB), and
|
||||
# when RAM and swap run out the kernel kills the scope's largest process.
|
||||
# the runner itself keeps memory: past 15 GB of RAM the scope swaps, as
|
||||
# the whole runner did before, and when RAM and swap run out the kernel
|
||||
# kills the scope's largest process. A sanitized shard peaks near 16 GB,
|
||||
# and the sanitized self-hosting native-frontend build alone needs 15 GB
|
||||
# (its clang compile of the frontend takes 10.5 GB).
|
||||
# OOMPolicy=continue leaves the rest of the scope running, so vitest
|
||||
# reports the killed worker and the job log names the test.
|
||||
# reports the killed worker and the job log names the test. Node sizes
|
||||
# its default heap from the memory it may use, which the scope lowers
|
||||
# below 4 GB; the workers keep the 4 GB heap they had on the bare
|
||||
# runner.
|
||||
- name: Tests (corpus and integration suites)
|
||||
run: |
|
||||
ulimit -c 0
|
||||
if [ -d /run/systemd/system ]; then
|
||||
exec sudo --preserve-env systemd-run --scope --quiet -p MemoryMax=14G -p OOMPolicy=continue \
|
||||
--uid="$(id -u)" --gid="$(id -g)" -- env PATH="$PATH" HOME="$HOME" node scripts/ci-test.mjs
|
||||
exec sudo --preserve-env systemd-run --scope --quiet -p MemoryMax=15G -p OOMPolicy=continue \
|
||||
--uid="$(id -u)" --gid="$(id -g)" -- env PATH="$PATH" HOME="$HOME" NODE_OPTIONS="--max-old-space-size=4096" node scripts/ci-test.mjs
|
||||
fi
|
||||
node scripts/ci-test.mjs
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `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 |
|
||||
| `json-unions` | cpu | `JSON.parse` into discriminated unions: an event log with eight kinds and nested expression trees with nine, evaluated after the cast |
|
||||
| `keyed-option-writes` | cpu | class methods whose names are also keys of option records written through element accesses: `enabled[name] = true` over a union of literal names and `totals["count"] = n` |
|
||||
| `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe |
|
||||
| `log-summary`, `inventory-report` | cpu | the multi-module applications from `benchmarks/builds`, on 300k-line inputs |
|
||||
| `number-maps` | cpu | number-keyed Map/Set: a dense-id table, sparse ids in many small maps, get/has/set mixes |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/json-unions.ts",
|
||||
"args": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// JSON text parsed into discriminated unions: an event log with eight kinds
|
||||
// of event and a nested expression tree with nine kinds of node, cast with
|
||||
// `JSON.parse(text) as T`, then aggregated and evaluated. Argument: the
|
||||
// number of rounds.
|
||||
type Event =
|
||||
| { kind: "click"; target: string; x: number; y: number }
|
||||
| { key: string; kind: "key"; repeat: boolean }
|
||||
| { kind: "scroll"; offset: number }
|
||||
| { kind: "resize"; height: number; width: number }
|
||||
| { kind: "focus"; target: string }
|
||||
| { kind: "blur"; target: string }
|
||||
| { kind: "load"; ms: number; url: string }
|
||||
| { code: number; kind: "error"; message: string };
|
||||
|
||||
type Expr =
|
||||
| { kind: "num"; value: number }
|
||||
| { kind: "var"; name: string }
|
||||
| { kind: "neg"; operand: Expr }
|
||||
| { kind: "add"; left: Expr; right: Expr }
|
||||
| { kind: "sub"; left: Expr; right: Expr }
|
||||
| { kind: "mul"; left: Expr; right: Expr }
|
||||
| { kind: "max"; items: Expr[] }
|
||||
| { else: Expr; kind: "if"; test: Expr; then: Expr }
|
||||
| { body: Expr; kind: "let"; name: string; value: Expr };
|
||||
|
||||
let seed = 12345;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
|
||||
const targets = ["menu", "search", "button", "list", "footer"];
|
||||
function eventText(i: number): string {
|
||||
const target = targets[i % targets.length]!;
|
||||
switch (i % 8) {
|
||||
case 0:
|
||||
return `{"kind":"click","target":"${target}","x":${i % 640},"y":${i % 480}}`;
|
||||
case 1:
|
||||
return `{"key":"k${i % 26}","kind":"key","repeat":${i % 3 === 0}}`;
|
||||
case 2:
|
||||
return `{"kind":"scroll","offset":${(i * 7) % 2000}}`;
|
||||
case 3:
|
||||
return `{"height":${400 + (i % 300)},"kind":"resize","width":${600 + (i % 500)}}`;
|
||||
case 4:
|
||||
return `{"kind":"focus","target":"${target}"}`;
|
||||
case 5:
|
||||
return `{"kind":"blur","target":"${target}"}`;
|
||||
case 6:
|
||||
return `{"kind":"load","ms":${i % 900},"url":"/page/${i % 50}"}`;
|
||||
default:
|
||||
return `{"code":${400 + (i % 100)},"kind":"error","message":"failed ${i % 13}"}`;
|
||||
}
|
||||
}
|
||||
|
||||
const names = ["a", "b", "c", "d"];
|
||||
function exprText(depth: number): string {
|
||||
const pick = depth <= 0 ? Math.floor(random() * 2) : Math.floor(random() * 9);
|
||||
switch (pick) {
|
||||
case 0:
|
||||
return `{"kind":"num","value":${Math.floor(random() * 100)}}`;
|
||||
case 1:
|
||||
return `{"kind":"var","name":"${names[Math.floor(random() * names.length)]!}"}`;
|
||||
case 2:
|
||||
return `{"kind":"neg","operand":${exprText(depth - 1)}}`;
|
||||
case 3:
|
||||
case 4:
|
||||
case 5: {
|
||||
const kind = pick === 3 ? "add" : pick === 4 ? "sub" : "mul";
|
||||
return `{"kind":"${kind}","left":${exprText(depth - 1)},"right":${exprText(depth - 1)}}`;
|
||||
}
|
||||
case 6:
|
||||
return `{"items":[${exprText(depth - 1)},${exprText(depth - 2)},${exprText(depth - 2)}],"kind":"max"}`;
|
||||
case 7:
|
||||
return `{"else":${exprText(depth - 2)},"kind":"if","test":${exprText(depth - 1)},"then":${exprText(depth - 2)}}`;
|
||||
default:
|
||||
return `{"body":${exprText(depth - 1)},"kind":"let","name":"${names[Math.floor(random() * names.length)]!}","value":${exprText(depth - 2)}}`;
|
||||
}
|
||||
}
|
||||
|
||||
function evaluate(e: Expr, env: Map<string, number>): number {
|
||||
switch (e.kind) {
|
||||
case "num":
|
||||
return e.value;
|
||||
case "var":
|
||||
return env.get(e.name) ?? 1;
|
||||
case "neg":
|
||||
return -evaluate(e.operand, env);
|
||||
case "add":
|
||||
return (evaluate(e.left, env) + evaluate(e.right, env)) % 1000003;
|
||||
case "sub":
|
||||
return (evaluate(e.left, env) - evaluate(e.right, env)) % 1000003;
|
||||
case "mul":
|
||||
return (evaluate(e.left, env) * evaluate(e.right, env)) % 1000003;
|
||||
case "max": {
|
||||
let best = -Infinity;
|
||||
for (const item of e.items) best = Math.max(best, evaluate(item, env));
|
||||
return best;
|
||||
}
|
||||
case "if":
|
||||
return evaluate(e.test, env) > 0 ? evaluate(e.then, env) : evaluate(e.else, env);
|
||||
case "let": {
|
||||
const inner = new Map(env);
|
||||
inner.set(e.name, evaluate(e.value, env));
|
||||
return evaluate(e.body, inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rounds = Number(process.argv[2] ?? "6");
|
||||
const eventsText = `[${Array.from({ length: 60000 }, (_, i) => eventText(i)).join(",")}]`;
|
||||
const treesText = `[${Array.from({ length: 400 }, () => exprText(9)).join(",")}]`;
|
||||
let clicks = 0;
|
||||
let keys = 0;
|
||||
let scrolled = 0;
|
||||
let area = 0;
|
||||
let loadMs = 0;
|
||||
let errors = 0;
|
||||
let focus = 0;
|
||||
let total = 0;
|
||||
for (let round = 0; round < rounds; round++) {
|
||||
const events = JSON.parse(eventsText) as Event[];
|
||||
for (const event of events) {
|
||||
switch (event.kind) {
|
||||
case "click":
|
||||
clicks += event.x + event.y;
|
||||
break;
|
||||
case "key":
|
||||
if (event.repeat) keys++;
|
||||
break;
|
||||
case "scroll":
|
||||
scrolled += event.offset;
|
||||
break;
|
||||
case "resize":
|
||||
area += (event.width * event.height) % 1000;
|
||||
break;
|
||||
case "focus":
|
||||
case "blur":
|
||||
focus += event.target.length;
|
||||
break;
|
||||
case "load":
|
||||
loadMs += event.ms;
|
||||
break;
|
||||
case "error":
|
||||
errors += event.code % 7;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const trees = JSON.parse(treesText) as Expr[];
|
||||
const env = new Map([
|
||||
["a", 3],
|
||||
["b", 5],
|
||||
]);
|
||||
for (const tree of trees) total = (total + evaluate(tree, env)) % 1000003;
|
||||
}
|
||||
console.log(clicks, keys, scrolled, area, loadMs, errors, focus, total);
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/keyed-option-writes.ts",
|
||||
"args": [
|
||||
"4"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Class methods whose names are also keys of option records written through
|
||||
// element accesses: `enabled[name] = true` over a union of literal names and
|
||||
// `totals["count"] = n`. The writes store booleans and numbers into plain
|
||||
// records, so the methods keep their native calls.
|
||||
type Step = "scale" | "offset" | "clamp";
|
||||
|
||||
const enabled: Record<Step, boolean> = { scale: false, offset: false, clamp: false };
|
||||
function enable(name: Step): void {
|
||||
enabled[name] = true;
|
||||
}
|
||||
|
||||
const totals: Record<string, number> = {};
|
||||
|
||||
class Transform {
|
||||
private readonly factor: number;
|
||||
private readonly shift: number;
|
||||
constructor(factor: number, shift: number) {
|
||||
this.factor = factor;
|
||||
this.shift = shift;
|
||||
}
|
||||
scale(x: number): number {
|
||||
return enabled.scale ? x * this.factor : x;
|
||||
}
|
||||
offset(x: number): number {
|
||||
return enabled.offset ? x + this.shift : x;
|
||||
}
|
||||
clamp(x: number): number {
|
||||
if (!enabled.clamp) return x;
|
||||
return x < 0 ? 0 : x > 1000 ? 1000 : x;
|
||||
}
|
||||
}
|
||||
|
||||
class Counter {
|
||||
private seen = 0;
|
||||
private sum = 0;
|
||||
add(value: number): void {
|
||||
this.seen++;
|
||||
this.sum += value;
|
||||
}
|
||||
count(): number {
|
||||
return this.seen;
|
||||
}
|
||||
mean(): number {
|
||||
return this.seen === 0 ? 0 : this.sum / this.seen;
|
||||
}
|
||||
}
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
const steps: Step[] = ["scale", "offset", "clamp"];
|
||||
for (const step of steps) enable(step);
|
||||
|
||||
const transforms: Transform[] = [];
|
||||
for (let i = 0; i < 16; i++) transforms.push(new Transform(1 + (i % 5) * 0.25, (i % 7) - 3));
|
||||
const counters: Counter[] = [];
|
||||
for (let i = 0; i < 8; i++) counters.push(new Counter());
|
||||
|
||||
let seed = 12345;
|
||||
let checksum = 0;
|
||||
const rounds = Math.floor(2_000_000 * scale);
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
const t = transforms[seed & 15]!;
|
||||
const value = t.clamp(t.offset(t.scale((seed >>> 8) % 1200)));
|
||||
const counter = counters[(seed >>> 4) & 7]!;
|
||||
counter.add(value);
|
||||
checksum += value;
|
||||
if ((i & 65535) === 0) totals["count"] = counter.count();
|
||||
}
|
||||
|
||||
let mean = 0;
|
||||
let seen = 0;
|
||||
for (const counter of counters) {
|
||||
mean += counter.mean();
|
||||
seen += counter.count();
|
||||
}
|
||||
console.log("seen", seen, "count", totals["count"], "mean", (mean / counters.length).toFixed(4), "checksum", checksum);
|
||||
@@ -1,5 +1,4 @@
|
||||
- **URL:** Hosts of special URLs that hold non-ASCII text, or percent-escapes that decode to it, throw `ERR_INVALID_URL`, and `xn--` labels are kept without Punycode validation.
|
||||
Absolute inputs and relative inputs with runtime string or URL bases, including optional bases, compile.
|
||||
- **URL:** Absolute inputs and relative inputs with runtime string or URL bases, including optional bases, compile.
|
||||
`URL.canParse`, `URL.parse`, `toString()`, and `toJSON()` compile; the factories use the same parser as construction, returning `false` or `null` for rejected inputs while preserving conversion errors.
|
||||
`href`, `protocol`, `username`, `password`, `host`, `hostname`, `port`, `pathname`, `search`, and `hash` support assignment in native code.
|
||||
Invalid component writes are ignored except for invalid `href` values, which throw; credentials are percent-encoded and ports accept valid leading digits.
|
||||
|
||||
@@ -193,6 +193,11 @@ function builtinStreamKind(className: string): number | null {
|
||||
return kinds[className] ?? null;
|
||||
}
|
||||
|
||||
/** Unions whose discriminant has fewer string literals keep one literal test
|
||||
* per arm (dispatchValues): one runtime lookup costs about what a few
|
||||
* literal tests do. */
|
||||
const DISCRIMINANT_DISPATCH_VALUES = 4;
|
||||
|
||||
export class LlDyn {
|
||||
private readonly dynMatchers = new Map<string, string>();
|
||||
private readonly dynBuilders = new Map<string, string>();
|
||||
@@ -413,6 +418,109 @@ export class LlDyn {
|
||||
return order;
|
||||
}
|
||||
|
||||
/** The string literals of `def`'s discriminant when its arms are chosen
|
||||
* by reading the discriminant once (unionArmsByDiscriminant), or null
|
||||
* when the union keeps one literal test per arm: some literal is not a
|
||||
* string, or there are fewer than DISCRIMINANT_DISPATCH_VALUES. */
|
||||
private dispatchValues(def: IrUnionDef): string[] | null {
|
||||
const values: string[] = [];
|
||||
for (const guard of def.discriminant?.cases ?? []) {
|
||||
if (guard.values.length === 0) return null;
|
||||
for (const value of guard.values) {
|
||||
if (typeof value !== "string") return null;
|
||||
if (!values.includes(value)) values.push(value);
|
||||
}
|
||||
}
|
||||
return values.length < DISCRIMINANT_DISPATCH_VALUES ? null : values;
|
||||
}
|
||||
|
||||
/** Branches to `hits[tag]` for the first arm in `order` that `%d` matches
|
||||
* (unionArmMatch), or to `none` when no arm does, for
|
||||
* a union whose discriminant literals are `values` (dispatchValues). The
|
||||
* discriminant is read once (scr_dyn_discriminant_case, against the
|
||||
* constant `table`), and only the arms whose literal it holds and the arms
|
||||
* without a literal are tested, in `order`: every other arm's literal test
|
||||
* fails. An arm's failed check can run a getter that changes the
|
||||
* discriminant, so it is read again after each one; when its case
|
||||
* changed, the tests continue arm by arm after the failed arm, as they
|
||||
* would have run from the first arm. A value the runtime cannot classify
|
||||
* that way (a typed capsule, an accessor, an inherited discriminant) takes
|
||||
* the tests arm by arm. */
|
||||
private unionArmsByDiscriminant(
|
||||
B: BlockBuilder,
|
||||
def: IrUnionDef,
|
||||
values: readonly string[],
|
||||
order: readonly number[],
|
||||
hits: readonly string[],
|
||||
none: string,
|
||||
preserveRefs: boolean,
|
||||
table: string,
|
||||
): void {
|
||||
const discriminant = def.discriminant!;
|
||||
const literalsOf = (tag: number): readonly (string | number | boolean)[] | null =>
|
||||
discriminant.cases.find((candidate) => candidate.tag === tag)?.values ?? null;
|
||||
const host = this.host;
|
||||
this.defs.push(
|
||||
`${table} = private unnamed_addr constant [${values.length} x ptr] [${values.map((value) => `ptr ${host.internLiteral(value)}`).join(", ")}]`,
|
||||
``,
|
||||
);
|
||||
host.declare(`declare i32 @scr_dyn_discriminant_case(ptr, ptr, ptr, ${host.sizeType})`);
|
||||
const classify = (): string => {
|
||||
const index = B.tmp();
|
||||
B.line(
|
||||
`${index} = call i32 @scr_dyn_discriminant_case(ptr %d, ptr ${host.internLiteral(discriminant.field)}, ptr ${table}, ${host.sizeType} ${values.length})`,
|
||||
);
|
||||
return index;
|
||||
};
|
||||
// The arm-by-arm tests, entered at any arm (`from[i]` tests order[i]
|
||||
// and the arms after it).
|
||||
const tests = B.newLabel("du.tests");
|
||||
const from = order.map((_, i) => (i === 0 ? tests : B.newLabel("du.t")));
|
||||
const after = (position: number): string =>
|
||||
position + 1 < order.length ? from[position + 1]! : none;
|
||||
// Tests the arms a value in case `k` (-1: no literal) can match.
|
||||
const tryCase = (k: number): void => {
|
||||
const value = k < 0 ? null : values[k]!;
|
||||
const positions = order
|
||||
.map((_, position) => position)
|
||||
.filter((position) => {
|
||||
const literals = literalsOf(order[position]!);
|
||||
return literals === null || (value !== null && literals.includes(value));
|
||||
});
|
||||
if (positions.length === 0) {
|
||||
B.br(none);
|
||||
return;
|
||||
}
|
||||
positions.forEach((position, index) => {
|
||||
const tag = order[position]!;
|
||||
const failed = B.newLabel("du.c.f");
|
||||
this.unionArmStructure(B, def, tag, hits[tag]!, failed, preserveRefs);
|
||||
B.startBlock(failed);
|
||||
const next = index === positions.length - 1 ? none : B.newLabel("du.c.n");
|
||||
const same = B.tmp();
|
||||
B.line(`${same} = icmp eq i32 ${classify()}, ${k}`);
|
||||
B.condBr(same, next, after(position));
|
||||
if (next !== none) B.startBlock(next);
|
||||
});
|
||||
};
|
||||
const index = classify();
|
||||
const unmatched = B.newLabel("du.none");
|
||||
const cases = values.map(() => B.newLabel("du.case"));
|
||||
B.terminate(
|
||||
`switch i32 ${index}, label %${tests} [ i32 -1, label %${unmatched} ${cases.map((label, k) => `i32 ${k}, label %${label}`).join(" ")} ]`,
|
||||
);
|
||||
values.forEach((_, k) => {
|
||||
B.startBlock(cases[k]!);
|
||||
tryCase(k);
|
||||
});
|
||||
B.startBlock(unmatched);
|
||||
tryCase(-1);
|
||||
order.forEach((tag, position) => {
|
||||
B.startBlock(from[position]!);
|
||||
this.unionArmMatch(B, def, tag, hits[tag]!, after(position), preserveRefs);
|
||||
});
|
||||
}
|
||||
|
||||
/** Branch to a variant only after its literal discriminator and complete
|
||||
* structure match. Use the same path for predicates and builders. */
|
||||
private unionArmMatch(
|
||||
@@ -450,6 +558,18 @@ export class LlDyn {
|
||||
});
|
||||
B.startBlock(structure);
|
||||
}
|
||||
this.unionArmStructure(B, def, tag, yes, no, preserveRefs);
|
||||
}
|
||||
|
||||
/** The part of unionArmMatch past the arm's literal test. */
|
||||
private unionArmStructure(
|
||||
B: BlockBuilder,
|
||||
def: IrUnionDef,
|
||||
tag: number,
|
||||
yes: string,
|
||||
no: string,
|
||||
preserveRefs: boolean,
|
||||
): void {
|
||||
const matches = B.tmp();
|
||||
B.line(
|
||||
`${matches} = call zeroext i1 @${this.dynMatchHelper(def.arms[tag]!, preserveRefs)}(ptr %d)`,
|
||||
@@ -1278,11 +1398,26 @@ export class LlDyn {
|
||||
);
|
||||
// Arms in canonical order; any full match answers true.
|
||||
const yes = B.newLabel("dm.y");
|
||||
for (let tag = 0; tag < def.arms.length; tag++) {
|
||||
const ln = B.newLabel("dm.n");
|
||||
this.unionArmMatch(B, def, tag, yes, ln, preserveRefs);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
const values = this.dispatchValues(def);
|
||||
if (values !== null) {
|
||||
const no = B.newLabel("dm.n");
|
||||
this.unionArmsByDiscriminant(
|
||||
B,
|
||||
def,
|
||||
values,
|
||||
def.arms.map((_, tag) => tag),
|
||||
def.arms.map(() => yes),
|
||||
no,
|
||||
preserveRefs,
|
||||
`@${name}.cases`,
|
||||
);
|
||||
B.startBlock(no);
|
||||
} else
|
||||
for (let tag = 0; tag < def.arms.length; tag++) {
|
||||
const ln = B.newLabel("dm.n");
|
||||
this.unionArmMatch(B, def, tag, yes, ln, preserveRefs);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
B.terminate(`ret i1 false`);
|
||||
B.startBlock(yes);
|
||||
B.terminate(`ret i1 true`);
|
||||
@@ -2462,12 +2597,8 @@ export class LlDyn {
|
||||
);
|
||||
// Arms in unionArmOrder, first FULL match wins. The matched arm's
|
||||
// builder can no longer fail.
|
||||
for (const i of this.unionArmOrder(def)) {
|
||||
const build = (i: number): void => {
|
||||
const arm = def.arms[i]!;
|
||||
const lHit = B.newLabel("dcu.h");
|
||||
const lNext = B.newLabel("dcu.n");
|
||||
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs);
|
||||
B.startBlock(lHit);
|
||||
if (arm.kind === "undefinedT" || arm.kind === "nullT") {
|
||||
// A matched unit arm builds nothing: THE interned immortal
|
||||
// instance (rc == SIZE_MAX — no retain owed).
|
||||
@@ -2506,8 +2637,36 @@ export class LlDyn {
|
||||
);
|
||||
B.terminate(`ret ptr ${u}`);
|
||||
}
|
||||
B.startBlock(lNext);
|
||||
}
|
||||
};
|
||||
const order = this.unionArmOrder(def);
|
||||
const values = this.dispatchValues(def);
|
||||
if (values !== null) {
|
||||
const hits = def.arms.map(() => B.newLabel("dcu.h"));
|
||||
const unmatched = B.newLabel("dcu.n");
|
||||
this.unionArmsByDiscriminant(
|
||||
B,
|
||||
def,
|
||||
values,
|
||||
order,
|
||||
hits,
|
||||
unmatched,
|
||||
preserveRefs,
|
||||
`@${name}.cases`,
|
||||
);
|
||||
for (const i of order) {
|
||||
B.startBlock(hits[i]!);
|
||||
build(i);
|
||||
}
|
||||
B.startBlock(unmatched);
|
||||
} else
|
||||
for (const i of order) {
|
||||
const lHit = B.newLabel("dcu.h");
|
||||
const lNext = B.newLabel("dcu.n");
|
||||
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs);
|
||||
B.startBlock(lHit);
|
||||
build(i);
|
||||
B.startBlock(lNext);
|
||||
}
|
||||
if (preserveRefs) {
|
||||
B.line(
|
||||
`call void @scr_dyn_check_fail(ptr %path, ptr ${host.cstr("reference with its original layout")}, ptr %d)`,
|
||||
|
||||
@@ -344,6 +344,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
// Fetch-referencing programs register native fetch before main.
|
||||
// Runtime selection compiles scr_fetch.c on the same predicate.
|
||||
const usesFetch = runtimeFeatures.fetch;
|
||||
// URL programs give the host parser the IDNA unit's ToASCII.
|
||||
const usesUrlIdna = runtimeFeatures.urlIdna;
|
||||
const snapshotsTlsCa = runtimeFeatures.tls || runtimeFeatures.tlsCa;
|
||||
// The process verdict has the same precedence as the C reference
|
||||
// emitter: node:test owns the final status when present; otherwise an
|
||||
@@ -396,6 +398,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
if (usesHttp2) emitter.declare(`declare void @scr_http2_dyn_install()`);
|
||||
if (usesHttp) emitter.declare(`declare void @scr_http_dyn_install()`);
|
||||
if (usesFetch) emitter.declare(`declare void @scr_fetch_install()`);
|
||||
if (usesUrlIdna) emitter.declare(`declare void @scr_url_idna_install()`);
|
||||
|
||||
if (usesNodeTest) {
|
||||
emitter.declare(`declare i32 @scr_test_exit_code()`);
|
||||
@@ -1027,6 +1030,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
...(emitter.ffiHasForeignCallback ? [` call void @scr_ffi_install()`] : []),
|
||||
...(snapshotsTlsCa ? [` call void @scr_tls_ca_install()`] : []),
|
||||
...(usesFetch ? [` call void @scr_fetch_install()`] : []),
|
||||
...(usesUrlIdna ? [` call void @scr_url_idna_install()`] : []),
|
||||
...(usesNet ? [` call void @scr_net_install()`, ` call void @scr_net_dyn_install()`] : []),
|
||||
...(usesNetLookup ? [` call void @scr_net_lookup_install()`] : []),
|
||||
...(usesHttp ? [` call void @scr_http_dyn_install()`] : []),
|
||||
|
||||
@@ -14,6 +14,10 @@ import type { CcDriver } from "./native/driver.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/** The section flags the libregexp/libunicode objects build with (the
|
||||
* runtime pack's executable_section_elimination flags). */
|
||||
const LRE_SECTION_FLAGS = ["-ffunction-sections", "-fdata-sections"];
|
||||
|
||||
import {
|
||||
QJS_COMMIT,
|
||||
MBEDTLS_VERSION,
|
||||
@@ -191,13 +195,13 @@ export function createVendorArchives(context: VendorArchiveContext) {
|
||||
(sanitize ? "asan" : "plain") +
|
||||
(driver.argv.length === 1 && driver.argv[0] === "clang" ? "" : "-zigcc") +
|
||||
`-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
|
||||
const cacheDir = join(cacheRoot, `${QJS_COMMIT.slice(0, 12)}-lre-${flavor}`);
|
||||
const cacheDir = join(cacheRoot, `${QJS_COMMIT.slice(0, 12)}-lre-sections-${flavor}`);
|
||||
return LRE_SOURCES.map((f) => join(cacheDir, f.replace(/\.c$/, ".o")));
|
||||
}
|
||||
|
||||
/** The libregexp objects for one flavor, compiled lazily on the first
|
||||
* regex-using build (~1s) and cached under
|
||||
* <build-cache>/vendor/<commit>-lre-<flavor>-<target>-<toolchain>/*.o — with the same atomic-rename
|
||||
* <build-cache>/vendor/<commit>-lre-sections-<flavor>-<target>-<toolchain>/*.o — with the same atomic-rename
|
||||
* publish (parallel first builds race safely; losers discard their work).
|
||||
* Plain is -Os to keep regex-using binaries small; asan matches the final link so
|
||||
* the sanitized lane instruments the matcher too. Cross targets get their
|
||||
@@ -228,6 +232,10 @@ export function createVendorArchives(context: VendorArchiveContext) {
|
||||
vendor: vendorEngineDir(),
|
||||
sources: LRE_SOURCES,
|
||||
objects: lreObjectPaths(sanitize, driver, buildIdentity, cacheRoot),
|
||||
// Section elimination keeps only the libunicode tables a program
|
||||
// reaches: the URL host parser's NFC links the normalization tables
|
||||
// without the regex property ones.
|
||||
flags: LRE_SECTION_FLAGS,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -296,6 +304,7 @@ export function createVendorArchives(context: VendorArchiveContext) {
|
||||
vendor: string;
|
||||
sources: readonly string[];
|
||||
objects: string[];
|
||||
flags?: readonly string[];
|
||||
}): Promise<string[]> {
|
||||
const { sanitize, driver, cacheRoot, flavor, name, vendor, sources, objects } = options;
|
||||
if ((await Promise.all(objects.map(validVendorArtifact))).every(Boolean)) return objects;
|
||||
@@ -312,6 +321,7 @@ export function createVendorArchives(context: VendorArchiveContext) {
|
||||
"-std=c11",
|
||||
...driver.targetArgs,
|
||||
...(sanitize ? ["-O1", "-fsanitize=address"] : ["-Os"]),
|
||||
...(options.flags ?? []),
|
||||
"-I",
|
||||
vendor,
|
||||
"-c",
|
||||
|
||||
@@ -709,7 +709,8 @@ export class Int32ElementContracts {
|
||||
const findChainsIn = (st: IrStmt): boolean => everyStmtChild(st, findChains, findChainsIn);
|
||||
for (const st of fn.body) findChainsIn(st);
|
||||
// Parameters whose callers are not all visible hold opaque arrays.
|
||||
if (this.valueEscaped.has(fn.name) || this.runtimeVirtual(fn.name))
|
||||
const runtimeVirtual = this.runtimeVirtual;
|
||||
if (this.valueEscaped.has(fn.name) || runtimeVirtual(fn.name))
|
||||
for (const p of fn.params)
|
||||
if (this.holds(p.type)) {
|
||||
const why = this.valueEscaped.has(fn.name)
|
||||
|
||||
@@ -403,7 +403,10 @@ export function lowerWebAssemblyValueOf(
|
||||
recv: IrExpr,
|
||||
loc: IrExpr["loc"],
|
||||
): IrExpr | null {
|
||||
const ctx = { checker: lowerer.checker, isStdlibFile: lowerer.isStdlibFile };
|
||||
const ctx = {
|
||||
checker: lowerer.checker,
|
||||
isStdlibFile: (sf: ts.SourceFile) => lowerer.isStdlibFile(sf),
|
||||
};
|
||||
if (!isWebAssemblyType(lowerer.typeOf(receiver), ctx)) return null;
|
||||
lowerer.usesWebAssembly = true;
|
||||
return { kind: "libCall", fn: "wasm.valueOf", args: [recv], type: DYN, loc };
|
||||
|
||||
@@ -36,9 +36,20 @@ export function collectClassMethodMutations(
|
||||
const computedWrites: {
|
||||
key: ts.Expression | undefined;
|
||||
receiver: ts.Expression | undefined;
|
||||
/** The written element access, when the write has one. */
|
||||
target?: ts.ElementAccessExpression;
|
||||
site: ts.Node;
|
||||
}[] = [];
|
||||
const propertyWrites: { target: ts.PropertyAccessExpression; site: ts.Node }[] = [];
|
||||
/** Writes and deletes of a method name through an element access with a
|
||||
* known key (`o["m"] = f`, `o[k] = f` with `k: "m" | "n"`), judged like
|
||||
* the named writes in propertyWrites. */
|
||||
const keyedWrites: {
|
||||
name: string;
|
||||
receiver: ts.Expression;
|
||||
target: ts.ElementAccessExpression;
|
||||
site: ts.Node;
|
||||
}[] = [];
|
||||
const reflection: ts.PropertyAccessExpression[] = [];
|
||||
const globals: ts.Identifier[] = [];
|
||||
const jsAssignments: (ts.PropertyAccessExpression | ts.ElementAccessExpression)[] = [];
|
||||
@@ -274,13 +285,32 @@ export function collectClassMethodMutations(
|
||||
sameBinding(receiver, keys.arguments.nodes[0]!)
|
||||
);
|
||||
};
|
||||
const markKey = (key: ts.Expression, site: ts.Node, receiver?: ts.Expression): void => {
|
||||
/** A write of the known key `name`: through an element access, a write
|
||||
* to a method name is judged like a named write (keyedWrites). */
|
||||
const markKeyed = (
|
||||
name: string,
|
||||
site: ts.Node,
|
||||
receiver?: ts.Expression,
|
||||
target?: ts.ElementAccessExpression,
|
||||
): void => {
|
||||
if (receiver && target && methods.has(name) && name !== "prototype" && name !== "__proto__") {
|
||||
receiverFields.add(name);
|
||||
keyedWrites.push({ name, receiver, target, site });
|
||||
} else markName(name, site);
|
||||
};
|
||||
const markKey = (
|
||||
key: ts.Expression,
|
||||
site: ts.Node,
|
||||
receiver?: ts.Expression,
|
||||
target?: ts.ElementAccessExpression,
|
||||
): void => {
|
||||
const value = unwrap(key);
|
||||
if (ts.isStringLiteralLike(value)) markName(value.text, site);
|
||||
else if (ts.isNumericLiteral(value)) markName(String(Number(value.text)), site);
|
||||
if (ts.isStringLiteralLike(value)) markKeyed(value.text, site, receiver, target);
|
||||
else if (ts.isNumericLiteral(value))
|
||||
markKeyed(String(Number(value.text)), site, receiver, target);
|
||||
// The key keeps its assertions: `slots[at as number]` and `slots[at!]`
|
||||
// write a number key whatever the type of `at`.
|
||||
else computedWrites.push({ key, receiver, site });
|
||||
else computedWrites.push(target ? { key, receiver, target, site } : { key, receiver, site });
|
||||
};
|
||||
const markFields = (value: ts.Expression, site: ts.Node, receiver?: ts.Expression): void => {
|
||||
value = unwrap(value);
|
||||
@@ -307,7 +337,7 @@ export function collectClassMethodMutations(
|
||||
propertyWrites.push({ target, site });
|
||||
} else markName(name, site, arrow);
|
||||
} else if (ts.isElementAccessExpression(target))
|
||||
markKey(target.argumentExpression, site, target.expression);
|
||||
markKey(target.argumentExpression, site, target.expression, target);
|
||||
else if (ts.isArrayLiteralExpression(target)) {
|
||||
for (const element of target.elements.nodes) {
|
||||
if (ts.isSpreadElement(element)) markTarget(element.expression, site);
|
||||
@@ -623,7 +653,7 @@ export function collectClassMethodMutations(
|
||||
if (lowerer.prototypeMethodAccesses.has(name)) lowerer.noteMethodSite(name, site);
|
||||
else lowerer.noteMethodAccess(name, site);
|
||||
}
|
||||
for (const { key, receiver, site } of computedWrites) {
|
||||
for (const { key, receiver, target, site } of computedWrites) {
|
||||
// A class method declared with a well-known symbol key is not a write;
|
||||
// any other write with one can replace that method.
|
||||
const symbolName = key ? wellKnownMethodName(unwrap(key)) : null;
|
||||
@@ -640,7 +670,7 @@ export function collectClassMethodMutations(
|
||||
const type = key ? lowerer.typeOf(key) : undefined;
|
||||
const names = type && (literalValues(type) ?? (key ? literalLoopKeys(unwrap(key)) : null));
|
||||
if (names) {
|
||||
for (const name of names) markName(String(name), site);
|
||||
for (const name of names) markKeyed(String(name), site, receiver, target);
|
||||
continue;
|
||||
}
|
||||
if (key && receiver && preservesExistingMethod(key, receiver, site)) continue;
|
||||
@@ -662,6 +692,19 @@ export function collectClassMethodMutations(
|
||||
markUnknownKey(site);
|
||||
}
|
||||
}
|
||||
lowerer.checker.prefetchClassCollection(
|
||||
keyedWrites.flatMap((write) => [write.target, write.receiver]),
|
||||
[],
|
||||
);
|
||||
for (const { name, receiver, target, site } of keyedWrites) {
|
||||
if (
|
||||
!dictionaryView(receiver) &&
|
||||
(functionFree(lowerer.typeOf(target)) || !methodOwnerCanInhabit(receiver, name))
|
||||
)
|
||||
continue;
|
||||
if (lowerer.prototypeMethodAccesses.has(name)) lowerer.noteMethodSite(name, site);
|
||||
else lowerer.noteMethodAccess(name, site);
|
||||
}
|
||||
if (numericKey) {
|
||||
for (const name of [...methods, ...arrowFields]) {
|
||||
if (String(Number(name)) === name) markName(name, numericKey);
|
||||
|
||||
@@ -1019,7 +1019,9 @@ class FusedLoop {
|
||||
private callee(arg: IrExpr & { type: FuncType }): Callee {
|
||||
const known = this.callees.find((c) => c.arg === arg);
|
||||
if (known) return known;
|
||||
const target = this.inlineTarget(arg);
|
||||
// Called as a plain function: the loop is not the target's receiver.
|
||||
const inlineTarget = this.inlineTarget;
|
||||
const target = inlineTarget(arg);
|
||||
let callee: Callee;
|
||||
if (target && arg.kind === "closure") {
|
||||
const captures = new Map<string, string>();
|
||||
|
||||
@@ -1182,10 +1182,10 @@ function bindSource(
|
||||
|
||||
/** Where chain code allocates its locals: a lifted helper function's own
|
||||
* frame, or hidden locals of the function being lowered (a fused for-of). */
|
||||
interface ChainFrame {
|
||||
local(base: string, type: IrType, mutable: boolean): string;
|
||||
abstract class ChainFrame {
|
||||
abstract local(base: string, type: IrType, mutable: boolean): string;
|
||||
/** `id = id + 1` over a number local. */
|
||||
bump(id: string): IrStmt;
|
||||
abstract bump(id: string): IrStmt;
|
||||
}
|
||||
|
||||
/** `id = id + 1` over a number local. */
|
||||
@@ -1206,11 +1206,13 @@ function bumpLocal(id: string, loc: SrcLoc): IrStmt {
|
||||
}
|
||||
|
||||
/** Hidden locals of the function being lowered. */
|
||||
class HostFrame implements ChainFrame {
|
||||
class HostFrame extends ChainFrame {
|
||||
constructor(
|
||||
private readonly lowerer: Lowerer,
|
||||
private readonly loc: SrcLoc,
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
local(base: string, type: IrType, mutable: boolean): string {
|
||||
const local = this.lowerer.declareHiddenLocal(`%${base}`, type);
|
||||
local.mutable = mutable;
|
||||
@@ -1223,11 +1225,13 @@ class HostFrame implements ChainFrame {
|
||||
|
||||
/** The locals and parameters of a lifted helper function under
|
||||
* construction. */
|
||||
export class LiftedFrame implements ChainFrame {
|
||||
export class LiftedFrame extends ChainFrame {
|
||||
readonly locals: IrLocal[] = [];
|
||||
readonly params: IrParam[] = [];
|
||||
private n = 0;
|
||||
constructor(private readonly loc: SrcLoc) {}
|
||||
constructor(private readonly loc: SrcLoc) {
|
||||
super();
|
||||
}
|
||||
local(base: string, type: IrType, mutable: boolean): string {
|
||||
const id = `${base}.${this.n++}`;
|
||||
this.locals.push({ id, name: base, type, mutable });
|
||||
|
||||
@@ -645,8 +645,7 @@ function extendsParameter(
|
||||
info: GenericFnInfo,
|
||||
): boolean {
|
||||
const params = new Set<ts.Symbol>();
|
||||
const fn = info.decl as { parameters?: { nodes: readonly ts.ParameterDeclaration[] } };
|
||||
for (const param of fn.parameters?.nodes ?? []) {
|
||||
for (const param of info.decl.parameters.nodes) {
|
||||
const symbol = ts.isIdentifier(param.name)
|
||||
? lowerer.checker.getSymbolAtLocation(param.name)
|
||||
: undefined;
|
||||
@@ -672,8 +671,7 @@ function declaredInside(node: ts.Node, owner: ts.Node): boolean {
|
||||
* return gives anything else (or nothing). Nested functions and classes keep
|
||||
* their own returns. */
|
||||
function returnedLocalClass(lowerer: Lowerer, info: GenericFnInfo): ts.ClassLikeDeclaration | null {
|
||||
const fn = info.decl as ts.Node & { body?: ts.Node };
|
||||
const body = fn.body;
|
||||
const body = info.decl.body;
|
||||
if (!body) return null;
|
||||
const classOf = (node: ts.Expression): ts.ClassLikeDeclaration | null => {
|
||||
let expr = node;
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { expect, test } from "vitest";
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
import { extendInstantiationPath, MAX_INSTANTIATION_RECURSION } from "./instantiation-path.js";
|
||||
|
||||
/** A stand-in declaration: paths compare declarations by identity only. */
|
||||
const node = (): ts.Node => ({}) as ts.Node;
|
||||
|
||||
test("independent declarations and roots do not consume a recursive budget", () => {
|
||||
const declaration = {};
|
||||
const declaration = node();
|
||||
for (let i = 0; i < MAX_INSTANTIATION_RECURSION * 2; i++)
|
||||
expect(extendInstantiationPath(null, declaration)).not.toBeNull();
|
||||
let path = extendInstantiationPath(null, declaration)!;
|
||||
for (let i = 0; i < MAX_INSTANTIATION_RECURSION * 2; i++)
|
||||
path = extendInstantiationPath(path, {})!;
|
||||
path = extendInstantiationPath(path, node())!;
|
||||
expect(extendInstantiationPath(path, declaration)).not.toBeNull();
|
||||
});
|
||||
|
||||
test("bounds fresh recursive demands across intervening declarations", () => {
|
||||
const first = {};
|
||||
const second = {};
|
||||
const first = node();
|
||||
const second = node();
|
||||
let path = extendInstantiationPath(null, first)!;
|
||||
for (let i = 1; i < MAX_INSTANTIATION_RECURSION; i++) {
|
||||
path = extendInstantiationPath(path, second)!;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
|
||||
/** Queued bodies preserve their demand ancestry. Independent concrete uses
|
||||
* do not consume a recursion budget; only fresh instances on one path do. */
|
||||
export interface InstantiationPath {
|
||||
declaration: object;
|
||||
declaration: ts.Node;
|
||||
parent: InstantiationPath | null;
|
||||
}
|
||||
|
||||
@@ -9,7 +11,7 @@ export const MAX_INSTANTIATION_RECURSION = 100;
|
||||
|
||||
export function extendInstantiationPath(
|
||||
parent: InstantiationPath | null,
|
||||
declaration: object,
|
||||
declaration: ts.Node,
|
||||
): InstantiationPath | null {
|
||||
let repetitions = 0;
|
||||
for (let path = parent; path; path = path.parent) {
|
||||
|
||||
@@ -1189,19 +1189,22 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi
|
||||
}
|
||||
if (e.kind === ts.SyntaxKind.NullKeyword) return true;
|
||||
if (ts.isIdentifier(e) && e.text === "undefined") return true;
|
||||
if (ts.isConditionalExpression(e)) {
|
||||
return (
|
||||
jsDynHoldableInitializer(lowerer, e.whenTrue) &&
|
||||
jsDynHoldableInitializer(lowerer, e.whenFalse)
|
||||
);
|
||||
}
|
||||
// The arms of a conditional or logical expression: checked values, or
|
||||
// strings, numbers and booleans, which box into the checked-dynamic
|
||||
// result (`const APP_NAME = config.name || "pi"`).
|
||||
const arm = (x: ts.Expression): boolean => {
|
||||
if (jsDynHoldableInitializer(lowerer, x)) return true;
|
||||
const kind = lowerer.mapTypeOf(lowerer.typeOf(x))?.kind;
|
||||
return kind === "string" || kind === "f64" || kind === "bool";
|
||||
};
|
||||
if (ts.isConditionalExpression(e)) return arm(e.whenTrue) && arm(e.whenFalse);
|
||||
if (
|
||||
ts.isBinaryExpression(e) &&
|
||||
(e.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ||
|
||||
e.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
|
||||
e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
|
||||
) {
|
||||
return jsDynHoldableInitializer(lowerer, e.left) && jsDynHoldableInitializer(lowerer, e.right);
|
||||
return arm(e.left) && arm(e.right);
|
||||
}
|
||||
// A member of a checked module value is itself checked, including a
|
||||
// callable whose inferred overloads cannot describe one native ABI.
|
||||
|
||||
@@ -3763,7 +3763,7 @@ export class Lowerer {
|
||||
positionOf: (node) => this.positions.of(node),
|
||||
shapes: this.shapes,
|
||||
unions: this.unions,
|
||||
classNamer: this.classNamer,
|
||||
classNamer: (decl) => this.classNamer(decl),
|
||||
resolveTypeParam: this.typeParamResolver,
|
||||
resolveTypeParamTs: this.typeParamTsResolver,
|
||||
isErasing: () => this.erasingDepth > 0,
|
||||
@@ -3844,8 +3844,8 @@ export class Lowerer {
|
||||
},
|
||||
mixinIntersectionInstance: (widened) => mixinIntersectionInstanceType(this, widened),
|
||||
classImplementedProtocol: (type) => this.classImplementedProtocol(type),
|
||||
isStdlibFile: this.isStdlibFile,
|
||||
isNpmFile: this.isNpmFile,
|
||||
isStdlibFile: (sf) => this.isStdlibFile(sf),
|
||||
isNpmFile: (sf) => this.isNpmFile(sf),
|
||||
// The declaration-typed boundary's generated types (decl-projection.ts)
|
||||
// describe data, like an explicitly mapped external type surface.
|
||||
isExternalTypeFile: (sf) =>
|
||||
@@ -8109,16 +8109,24 @@ export class Lowerer {
|
||||
lower: () => IrFunction | null;
|
||||
}
|
||||
>();
|
||||
const bodyRoots = (...roots: (ts.Node | undefined | null)[]): ts.Node[] =>
|
||||
roots.filter((root) => root !== undefined && root !== null);
|
||||
const functionRoots = (decl: ts.FunctionLikeDeclaration): ts.Node[] =>
|
||||
bodyRoots(...decl.parameters.nodes.map((param) => param.initializer), decl.body);
|
||||
const classCtorRoots = (info: ClassInfo): ts.Node[] =>
|
||||
bodyRoots(
|
||||
...(info.ctor?.parameters?.nodes ?? []).map((param) => param.initializer),
|
||||
info.ctor?.body,
|
||||
...info.fieldOrder.map((field) => field.initializer),
|
||||
);
|
||||
// Roots are collected with plain pushes: spread arguments into a rest
|
||||
// parameter pass the nodes as dynamic values.
|
||||
const addRoot = (roots: ts.Node[], root: ts.Node | undefined | null): void => {
|
||||
if (root !== undefined && root !== null) roots.push(root);
|
||||
};
|
||||
const functionRoots = (decl: ts.FunctionLikeDeclaration): ts.Node[] => {
|
||||
const roots: ts.Node[] = [];
|
||||
for (const param of decl.parameters.nodes) addRoot(roots, param.initializer);
|
||||
addRoot(roots, decl.body);
|
||||
return roots;
|
||||
};
|
||||
const classCtorRoots = (info: ClassInfo): ts.Node[] => {
|
||||
const roots: ts.Node[] = [];
|
||||
for (const param of info.ctor?.parameters?.nodes ?? []) addRoot(roots, param.initializer);
|
||||
addRoot(roots, info.ctor?.body);
|
||||
for (const field of info.fieldOrder) addRoot(roots, field.initializer);
|
||||
return roots;
|
||||
};
|
||||
const classMemberRoots = (info: ClassInfo): ts.Node[] => {
|
||||
const roots = classCtorRoots(info);
|
||||
for (const { member } of this.classMethodMembers(info)) roots.push(...functionRoots(member));
|
||||
@@ -8313,7 +8321,8 @@ export class Lowerer {
|
||||
register(`%${cName}.set:${prop}`, [], () => this.throwingSetterFn(info, prop));
|
||||
}
|
||||
};
|
||||
for (const name of collectionEdges) this.onEdge(name);
|
||||
const onEdge = this.onEdge;
|
||||
for (const name of collectionEdges) onEdge(name);
|
||||
// Generic instances queued by the bodies above lower here (an instance
|
||||
// body fires edges of its own and can queue further instances of
|
||||
// either kind — function instances and class instantiations drain to
|
||||
@@ -8448,7 +8457,8 @@ export class Lowerer {
|
||||
// profile-mapped exports are called from outside the graph, so they
|
||||
// seed the worklist beside the init bodies. Unknown names are inert
|
||||
// (the export-map resolution reports them as SC4002 later).
|
||||
for (const root of extraRoots ?? []) this.onEdge?.(root);
|
||||
const rootEdge = this.onEdge;
|
||||
for (const root of extraRoots ?? []) rootEdge?.(root);
|
||||
const shuffle = lowerShuffle();
|
||||
const drainUnits = (): void => {
|
||||
while (queue.length > 0) {
|
||||
@@ -8566,7 +8576,9 @@ export class Lowerer {
|
||||
/** Reachability hook (see emitReachable): fires when lowering resolves a
|
||||
* reference to a lowerable body. */
|
||||
noteEdge(name: string): void {
|
||||
if (this.onEdge) this.onEdge(name);
|
||||
// Called as a plain function: the instance is not its receiver.
|
||||
const onEdge = this.onEdge;
|
||||
if (onEdge) onEdge(name);
|
||||
}
|
||||
|
||||
/** A subclass constructor's super() edge: the base constructor runs, but
|
||||
@@ -11978,12 +11990,15 @@ export class Lowerer {
|
||||
|
||||
/** Method-like members with their collected method-map names. Body
|
||||
* lowering omits abstract declarations; ABI analysis must include them
|
||||
* because an abstract-typed call selects the same virtual slot. */
|
||||
*classMethodMembers(
|
||||
* because an abstract-typed call selects the same virtual slot. An array,
|
||||
* not a generator: a generator's yielded values are dynamic values, which
|
||||
* would make every syntax node one. */
|
||||
classMethodMembers(
|
||||
info: ClassInfo,
|
||||
includeAbstract = false,
|
||||
): Generator<{ mName: string; member: ts.MethodDeclaration | ts.AccessorDeclaration }> {
|
||||
if (!info.decl) return; // builtin error classes: runtime-provided bodies
|
||||
): { mName: string; member: ts.MethodDeclaration | ts.AccessorDeclaration }[] {
|
||||
const members: { mName: string; member: ts.MethodDeclaration | ts.AccessorDeclaration }[] = [];
|
||||
if (!info.decl) return members; // builtin error classes: runtime-provided bodies
|
||||
for (const member of info.decl.members.nodes) {
|
||||
const fnLike =
|
||||
ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member)
|
||||
@@ -12007,8 +12022,9 @@ export class Lowerer {
|
||||
: `${ts.isGetAccessor(fnLike) ? "get" : "set"}:${baseName}`;
|
||||
const signature = info.methods.get(mName);
|
||||
if (!signature || (!fnLike.body && !(includeAbstract && signature.abstract))) continue;
|
||||
yield { mName, member: fnLike };
|
||||
members.push({ mName, member: fnLike });
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
lowerClassCtor(info: ClassInfo): IrFunction {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { expect, test } from "vitest";
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
import { SiteDependencies } from "./site-dependencies.js";
|
||||
|
||||
/** A stand-in node fact: facts are compared by identity. */
|
||||
const node = (): ts.Node => ({}) as ts.Node;
|
||||
|
||||
test("a changed fact makes the sites that read it stale", () => {
|
||||
const dependencies = new SiteDependencies(3);
|
||||
const a = {};
|
||||
const a = node();
|
||||
const b = "key";
|
||||
dependencies.begin(0);
|
||||
dependencies.read(a);
|
||||
@@ -20,8 +24,8 @@ test("a changed fact makes the sites that read it stale", () => {
|
||||
|
||||
test("a visit that changes a fact makes every reader of its facts stale", () => {
|
||||
const dependencies = new SiteDependencies(3);
|
||||
const shared = {};
|
||||
const own = {};
|
||||
const shared = node();
|
||||
const own = node();
|
||||
dependencies.begin(0);
|
||||
dependencies.read(shared);
|
||||
dependencies.end(false);
|
||||
@@ -37,7 +41,7 @@ test("a visit that changes a fact makes every reader of its facts stale", () =>
|
||||
|
||||
test("reads outside a visit are not recorded", () => {
|
||||
const dependencies = new SiteDependencies(1);
|
||||
const fact = {};
|
||||
const fact = node();
|
||||
dependencies.read(fact);
|
||||
dependencies.changed(fact);
|
||||
expect(dependencies.isStale(0)).toBe(false);
|
||||
@@ -45,7 +49,7 @@ test("reads outside a visit are not recorded", () => {
|
||||
|
||||
test("a site visited again is listed once per fact", () => {
|
||||
const dependencies = new SiteDependencies(3);
|
||||
const fact = {};
|
||||
const fact = node();
|
||||
for (const site of [0, 2, 1, 2, 0]) {
|
||||
dependencies.begin(site);
|
||||
dependencies.read(fact);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
|
||||
/** The facts each site of a fixed-point scan read on its last visit, so that
|
||||
* a pass visits again only the sites whose facts changed since.
|
||||
*
|
||||
@@ -13,14 +15,19 @@
|
||||
* that with a pass over every site once a pass changes nothing, and resumes
|
||||
* when that pass changes a fact: a fact this tracking misses costs passes,
|
||||
* not a different result. */
|
||||
/** A fact a site reads: a symbol, a syntax node, or the name of a derived
|
||||
* fact. A typed union keeps nodes and symbols native values; as `unknown`
|
||||
* every syntax node would become a dynamic value. */
|
||||
export type SiteFact = ts.Node | ts.Symbol | string;
|
||||
|
||||
export class SiteDependencies {
|
||||
/** Each fact's entry: the visit that last read it, so that a visit lists a
|
||||
* fact once, followed by the sites that read it, ascending. */
|
||||
private readonly readers = new Map<unknown, number[]>();
|
||||
private readonly readers = new Map<SiteFact, number[]>();
|
||||
private readonly stale: Uint8Array;
|
||||
private site = -1;
|
||||
private visit = 0;
|
||||
private facts: unknown[] = [];
|
||||
private facts: SiteFact[] = [];
|
||||
|
||||
constructor(siteCount: number) {
|
||||
this.stale = new Uint8Array(siteCount);
|
||||
@@ -46,7 +53,7 @@ export class SiteDependencies {
|
||||
}
|
||||
|
||||
/** The site being visited read `fact`. */
|
||||
read(fact: unknown): void {
|
||||
read(fact: SiteFact): void {
|
||||
if (this.site < 0) return;
|
||||
const entry = this.readers.get(fact);
|
||||
if (entry === undefined) {
|
||||
@@ -72,7 +79,7 @@ export class SiteDependencies {
|
||||
}
|
||||
|
||||
/** `fact` changed: every site that read it is visited again. */
|
||||
changed(fact: unknown): void {
|
||||
changed(fact: SiteFact): void {
|
||||
const entry = this.readers.get(fact);
|
||||
if (entry !== undefined) for (let i = 1; i < entry.length; i++) this.stale[entry[i]!] = 1;
|
||||
}
|
||||
|
||||
@@ -182,11 +182,28 @@ export class StringIndexBounds {
|
||||
private readonly identifiers = new Map<ts.Node, Map<string, ts.Identifier[]>>();
|
||||
|
||||
constructor(
|
||||
private readonly symbolAt: (node: ts.Node) => ts.Symbol | null,
|
||||
private readonly declarationOf: (symbol: ts.Symbol) => ts.Node | undefined,
|
||||
private readonly isString: (node: ts.Expression) => boolean,
|
||||
private readonly lookupSymbol: (node: ts.Node) => ts.Symbol | null,
|
||||
private readonly lookupDeclaration: (symbol: ts.Symbol) => ts.Node | undefined,
|
||||
private readonly stringTyped: (node: ts.Expression) => boolean,
|
||||
) {}
|
||||
|
||||
// The lookups are called as plain functions: the instance is not their
|
||||
// receiver.
|
||||
private symbolAt(node: ts.Node): ts.Symbol | null {
|
||||
const lookup = this.lookupSymbol;
|
||||
return lookup(node);
|
||||
}
|
||||
|
||||
private declarationOf(symbol: ts.Symbol): ts.Node | undefined {
|
||||
const lookup = this.lookupDeclaration;
|
||||
return lookup(symbol);
|
||||
}
|
||||
|
||||
private isString(node: ts.Expression): boolean {
|
||||
const test = this.stringTyped;
|
||||
return test(node);
|
||||
}
|
||||
|
||||
/** True when `read` (an element access on a string receiver) is proven
|
||||
* to name an existing code unit. */
|
||||
inBounds(read: ts.ElementAccessExpression): boolean {
|
||||
|
||||
@@ -254,13 +254,18 @@ function aliasedPackageName(pkgDir: string): string | null {
|
||||
export function npmStaticTransformPkgJson(pkg: Record<string, unknown>): void {
|
||||
delete pkg["types"];
|
||||
delete pkg["typings"];
|
||||
const transform = (v: unknown): void => {
|
||||
if (v === null || typeof v !== "object") return;
|
||||
if (Array.isArray(v)) {
|
||||
for (const item of v) transform(item);
|
||||
return;
|
||||
}
|
||||
const obj = v as Record<string, unknown>;
|
||||
const transform = (v: unknown): unknown => {
|
||||
if (v === null || typeof v !== "object") return v;
|
||||
if (Array.isArray(v)) return v.map(transform);
|
||||
// The result is a new object, built in key order; the first entry of a
|
||||
// key wins.
|
||||
const entries: [string, unknown][] = [];
|
||||
const seen = new Set<string>();
|
||||
const add = (key: string, value: unknown): void => {
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
entries.push([key, value]);
|
||||
};
|
||||
// 0. The "node" condition HOISTS: Node's runtime resolution matches it
|
||||
// (yaml's `{ types, node: "./dist/index.js", default: "./browser/
|
||||
// index.js" }` runs the node dist; the browser dist is a DIFFERENT
|
||||
@@ -270,24 +275,18 @@ export function npmStaticTransformPkgJson(pkg: Record<string, unknown>): void {
|
||||
// under import+require (either matcher finds it there), an object's
|
||||
// entries splice inline — earlier real keys keep winning, exactly
|
||||
// Node's first-match-in-key-order rule.
|
||||
if (obj["node"] !== undefined) {
|
||||
const entries = Object.entries(obj);
|
||||
for (const k of Object.keys(obj)) delete obj[k];
|
||||
for (const [k, val] of entries) {
|
||||
if (k !== "node") {
|
||||
if (!(k in obj)) obj[k] = val;
|
||||
continue;
|
||||
}
|
||||
const inner: Record<string, unknown> =
|
||||
typeof val === "string"
|
||||
? { import: val, require: val }
|
||||
: val !== null && typeof val === "object" && !Array.isArray(val)
|
||||
? (val as Record<string, unknown>)
|
||||
: {};
|
||||
for (const [ik, iv] of Object.entries(inner)) {
|
||||
if (!(ik in obj)) obj[ik] = iv;
|
||||
}
|
||||
for (const [key, value] of Object.entries(v as Record<string, unknown>)) {
|
||||
if (key !== "node") {
|
||||
add(key, value);
|
||||
continue;
|
||||
}
|
||||
const inner: Record<string, unknown> =
|
||||
typeof value === "string"
|
||||
? { import: value, require: value }
|
||||
: value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
for (const [innerKey, innerValue] of Object.entries(inner)) add(innerKey, innerValue);
|
||||
}
|
||||
// 1. The types claims go AFTER the hoist: a node object's own
|
||||
// "types" key (uuid's `node: { types: "./dist/index.d.ts", default:
|
||||
@@ -295,15 +294,17 @@ export function npmStaticTransformPkgJson(pkg: Record<string, unknown>): void {
|
||||
// in place tsgo follows it, maps the hidden declaration to its
|
||||
// same-stem JS (dist/index.js, the browser build) and disagrees with
|
||||
// this resolver's runtime answer.
|
||||
delete obj["types"];
|
||||
delete obj["typings"];
|
||||
for (const value of Object.values(obj)) transform(value);
|
||||
return Object.fromEntries(
|
||||
entries
|
||||
.filter(([key]) => key !== "types" && key !== "typings")
|
||||
.map(([key, value]): [string, unknown] => [key, transform(value)]),
|
||||
);
|
||||
};
|
||||
if (pkg["exports"] !== undefined) transform(pkg["exports"]);
|
||||
if (pkg["exports"] !== undefined) pkg["exports"] = transform(pkg["exports"]);
|
||||
// The "imports" field maps "#" specifiers with the same condition objects
|
||||
// (chalk 5's `"#supports-color": { node, default }`), so Node's "node"
|
||||
// target hoists there too.
|
||||
if (pkg["imports"] !== undefined) transform(pkg["imports"]);
|
||||
if (pkg["imports"] !== undefined) pkg["imports"] = transform(pkg["imports"]);
|
||||
}
|
||||
|
||||
/** npmStaticTransformPkgJson over a package.json TEXT (the fs shadow's
|
||||
|
||||
@@ -52,8 +52,10 @@ export class FrontendServices {
|
||||
}
|
||||
private sourceParser(): Ts7SourceParser {
|
||||
this.ensureOpen();
|
||||
// Called as a plain function: the instance is not its receiver.
|
||||
const createApi = this.createApi;
|
||||
return (this.parser ??= new Ts7SourceParser(
|
||||
(options) => this.createApi({ ...options, collectTiming: false }),
|
||||
(options) => createApi({ ...options, collectTiming: false }),
|
||||
this.cwd,
|
||||
));
|
||||
}
|
||||
@@ -62,9 +64,10 @@ export class FrontendServices {
|
||||
}
|
||||
evaluateComptime(source: string, timeoutMs: number): unknown {
|
||||
this.ensureOpen();
|
||||
if (this.comptimeEvaluator === undefined)
|
||||
const evaluate = this.comptimeEvaluator;
|
||||
if (evaluate === undefined)
|
||||
throw new Error("this compiler host does not provide compile-time evaluation");
|
||||
return this.comptimeEvaluator(source, timeoutMs);
|
||||
return evaluate(source, timeoutMs);
|
||||
}
|
||||
createProgramHost(options?: Ts7HostOptions): Ts7Host {
|
||||
this.ensureOpen();
|
||||
|
||||
@@ -64,7 +64,9 @@ export interface SemanticQuery {
|
||||
export class SemanticProgram {
|
||||
constructor(private readonly sourceFile: (path: string) => AstNode | undefined) {}
|
||||
getSourceFile(path: string): AstNode | undefined {
|
||||
return this.sourceFile(path);
|
||||
// Called as a plain function: the instance is not its receiver.
|
||||
const sourceFile = this.sourceFile;
|
||||
return sourceFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ export class Ts7SourceParser {
|
||||
}
|
||||
|
||||
private connect(): Ts7Api {
|
||||
return this.createApi({
|
||||
// Called as a plain function: the instance is not its receiver.
|
||||
const createApi = this.createApi;
|
||||
return createApi({
|
||||
cwd: this.cwd,
|
||||
fs: {
|
||||
readFile: (path) => this.files.get(tsgoPath(path)) ?? null,
|
||||
|
||||
@@ -715,6 +715,10 @@ interface RuntimeFeatureRule {
|
||||
/** Gives every rule in the table one type. */
|
||||
const rule = (r: RuntimeFeatureRule): RuntimeFeatureRule => r;
|
||||
|
||||
/* The node:url and WHATWG URL entry points: every URL value comes from
|
||||
* one, and parsing one can reach the UTS #46 host processing. */
|
||||
const URL_PREFIXES: readonly string[] = ["url."];
|
||||
|
||||
/* The HTTP, HTTPS, HTTP/2 and TLS surfaces, which link the net unit. */
|
||||
const NET_PREFIXES: readonly string[] = ["net.", "http.", "tls.", "https.", "http2."];
|
||||
|
||||
@@ -1063,9 +1067,10 @@ const RUNTIME_FEATURE_RULES = {
|
||||
/** scr_url_legacy.c parses and stringifies queries. */
|
||||
qs: rule({ fns: [...QS_LIB_FNS, ...LEGACY_URL_LIB_FNS] }),
|
||||
/** scr_url_legacy.c lowercases and normalizes hostnames with the regex
|
||||
* unit's libunicode, NODE_DEBUG matching in scr_util_callable.c and
|
||||
* readline's terminal unit use the regex unit, and locale case mapping
|
||||
* runs its default conversion. */
|
||||
* unit's libunicode, the WHATWG URL host parser runs UTS #46 through
|
||||
* scr_idna.c (which links with the regex unit), NODE_DEBUG matching in
|
||||
* scr_util_callable.c and readline's terminal unit use the regex unit,
|
||||
* and locale case mapping runs its default conversion. */
|
||||
regex: rule({
|
||||
fns: [
|
||||
...REGEX_LIB_FNS,
|
||||
@@ -1074,13 +1079,17 @@ const RUNTIME_FEATURE_RULES = {
|
||||
"rl.terminal",
|
||||
"str.localeCase",
|
||||
],
|
||||
prefixes: URL_PREFIXES,
|
||||
expr: (node, _tables) =>
|
||||
node.kind === "regexLit" ||
|
||||
node.kind === "regexIntrinsic" ||
|
||||
(node.kind === "strIntrinsic" && CASE_MAPPING_STR_METHODS.has(node.method)),
|
||||
types: ["regex"],
|
||||
types: ["regex", "url"],
|
||||
}),
|
||||
searchParams: rule({ fns: ["url.searchParams"], prefixes: ["sp."], types: ["searchParams"] }),
|
||||
/** URL programs install the IDNA unit's ToASCII for the WHATWG host
|
||||
* parser (scr_url_idna_install). */
|
||||
urlIdna: rule({ prefixes: URL_PREFIXES, types: ["url"] }),
|
||||
/** stdin setEncoding / string data listeners / stdio 'resize' events. A
|
||||
* typed string 'data' listener takes setEncoding-decoded chunks; the
|
||||
* process stream object's setEncoding and 'resize' members are reached
|
||||
@@ -1177,6 +1186,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
qs: false,
|
||||
regex: false,
|
||||
searchParams: false,
|
||||
urlIdna: false,
|
||||
stdioExtended: false,
|
||||
stream: false,
|
||||
streamDyn: false,
|
||||
|
||||
@@ -9,9 +9,10 @@ import { inlineRcOption } from "../src/backend/llvm/shapes.js";
|
||||
|
||||
// Programs whose function bodies request module-level helpers and tables
|
||||
// that the chunk journal (emit-journal.ts) carries: checked-dynamic
|
||||
// conversions, inspection, publication walkers, live record views, stream
|
||||
// adapters, regex instances, colliding literal digests, interned C strings,
|
||||
// speculative loop sites, typed message listeners and readers.
|
||||
// conversions (with their discriminant tables), inspection, publication
|
||||
// walkers, live record views, stream adapters, regex instances, colliding
|
||||
// literal digests, interned C strings, speculative loop sites, typed message
|
||||
// listeners and readers.
|
||||
const PROGRAMS = [
|
||||
"1003-json-parse-unions.ts",
|
||||
"114-live-record-delete.ts",
|
||||
@@ -21,6 +22,7 @@ const PROGRAMS = [
|
||||
"4900-console-instances.ts",
|
||||
"5612-readable-from-iterables.cjs",
|
||||
"5702-publish-candidate-buffers.ts",
|
||||
"6305-json-discriminant-dispatch.ts",
|
||||
"7321-speculative-loop-bail-sites.ts",
|
||||
"7360-worker-typed-records.ts",
|
||||
"node24-text-utilities-coercion.js",
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../src/index.js";
|
||||
import type { IrModule } from "../src/ir/ir.js";
|
||||
import { everyStmtList } from "../src/ir/traverse.js";
|
||||
|
||||
async function lowerSource(source: string): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-keyed-writes-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
await writeFile(entry, source);
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" });
|
||||
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
|
||||
return deserializeModule(await readFile(outPath, "utf8"));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** Calls in `run` that go through a dynamic value. */
|
||||
function dynamicCalls(mod: IrModule): number {
|
||||
const fn = mod.functions.find((f) => f.name === "run");
|
||||
if (!fn) throw new Error("no run function");
|
||||
let n = 0;
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "dynCall" || e.kind === "dynInvoke") n++;
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
return n;
|
||||
}
|
||||
|
||||
const shape = `
|
||||
type Step = "scale" | "clamp";
|
||||
class Transform {
|
||||
scale(x: number): number {
|
||||
return x * 2;
|
||||
}
|
||||
clamp(x: number): number {
|
||||
return x < 0 ? 0 : x;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
test("element writes of data under method names keep the methods native", async () => {
|
||||
const mod = await lowerSource(`${shape}
|
||||
const enabled: Record<Step, boolean> = { scale: false, clamp: false };
|
||||
function enable(name: Step): void {
|
||||
enabled[name] = true;
|
||||
}
|
||||
const totals: Record<string, number> = {};
|
||||
function run(t: Transform): number {
|
||||
enable("scale");
|
||||
totals["clamp"] = 1;
|
||||
return t.clamp(t.scale(3));
|
||||
}
|
||||
console.log(run(new Transform()));
|
||||
`);
|
||||
expect(dynamicCalls(mod)).toBe(0);
|
||||
});
|
||||
|
||||
test("element writes through untyped receivers can still replace a method", async () => {
|
||||
const mod = await lowerSource(`${shape}
|
||||
function replace(target: object, name: Step): void {
|
||||
(target as any)[name] = (x: number) => x + 100;
|
||||
}
|
||||
function run(t: Transform): number {
|
||||
replace(t, "scale");
|
||||
return t.clamp(t.scale(3));
|
||||
}
|
||||
console.log(run(new Transform()));
|
||||
`);
|
||||
expect(dynamicCalls(mod)).toBeGreaterThan(0);
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6305-json-discriminant-dispatch.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6306-discriminant-getter-dispatch.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8105-url-idna-hosts.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8215-js-any-logical-module-bindings/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -386,6 +386,10 @@ async function build() {
|
||||
? [
|
||||
await archive("libregexp", lreSources, quickjs, [
|
||||
...vendorTarget,
|
||||
// Section elimination keeps only the libunicode tables a
|
||||
// program reaches: the URL host parser's NFC links the
|
||||
// normalization tables without the regex property ones.
|
||||
...matrix.executable_section_elimination.compile_flags,
|
||||
"-std=c11",
|
||||
"-Os",
|
||||
"-I",
|
||||
|
||||
@@ -681,6 +681,14 @@ void scr_dyn_copy_entry(ScrDyn *target, const ScrDyn *source, const ScrDynEntry
|
||||
bool scr_dyn_field_eq_str(const ScrDyn *d, const ScrStr *key, const ScrStr *value);
|
||||
bool scr_dyn_field_eq_num(const ScrDyn *d, const ScrStr *key, double value);
|
||||
bool scr_dyn_field_eq_bool(const ScrDyn *d, const ScrStr *key, bool value);
|
||||
/* The case a union's checked match takes from its string discriminant: the
|
||||
* index in `values` of the string `d`'s own data property `key` holds, or
|
||||
* -1 when every literal test above fails (`d` is not an object, or its own
|
||||
* data property `key` holds no string among `values`), or -2 when only the
|
||||
* tests themselves can tell (a typed capsule, an accessor, or a property
|
||||
* `d` does not own). Borrowed args. */
|
||||
int32_t scr_dyn_discriminant_case(const ScrDyn *d, const ScrStr *key, ScrStr *const *values,
|
||||
size_t count);
|
||||
|
||||
/* Object.keys/values/entries over the checked-dynamic tree: JS own-key order (array-index
|
||||
* keys ascending first), dyn-array results (+1); values/entries RETAIN
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* UTS #46 domain processing for node:url — the conversions Node applies to
|
||||
* url.parse() hostnames (ToASCII), url.domainToASCII()/domainToUnicode()
|
||||
* and url.format()'s unicode option: nontransitional mapping, NFC, label
|
||||
* validity (leading combining marks, ContextJ, and the RFC 5893 bidi rule),
|
||||
* and Punycode. It matches Node 24's implementation, including its
|
||||
* WHATWG URL hosts and url.parse() hostnames (ToASCII),
|
||||
* url.domainToASCII()/domainToUnicode() and url.format()'s unicode option:
|
||||
* nontransitional mapping, NFC, label validity (leading combining marks,
|
||||
* ContextJ, and the RFC 5893 bidi rule), and Punycode. It matches Node 24's implementation, including its
|
||||
* validity-data snapshot and the shortcuts its label checks take; the
|
||||
* differential corpus and test/url-legacy-cases.txt (generated from Node)
|
||||
* are the oracle.
|
||||
@@ -511,3 +511,6 @@ ScrStr *scr_idna_to_unicode(const char *p, size_t n) {
|
||||
}
|
||||
return scr_jb_finish(&out);
|
||||
}
|
||||
|
||||
/* The URL host parser's UTS #46 ToASCII (scr_url.c). */
|
||||
void scr_url_idna_install(void) { scr_url_host_to_ascii = &scr_idna_to_ascii; }
|
||||
|
||||
@@ -1286,10 +1286,14 @@ void scr_dyn_release(ScrDyn *d) {
|
||||
return;
|
||||
}
|
||||
#ifndef SCR_RC_AUDIT
|
||||
/* Number and string boxes own at most one string, are never cycle
|
||||
* candidates and cannot be weak keys: return them to the free list. */
|
||||
/* Number and string boxes own at most one string and cannot be weak
|
||||
* keys: return them to the free list. Their releases never buffer them,
|
||||
* but a collector pass can (a box that traces its payload reaches them,
|
||||
* and scr_xg_release buffers what it settles), so a buffered box leaves
|
||||
* its candidate buffer first. */
|
||||
if ((d->kind == SCR_DYN_NUM || d->kind == SCR_DYN_STR) && !d->prototype &&
|
||||
!d->symbol_properties && !d->symbol_keys && scr_dyn_free_count < SCR_DYN_FREE_MAX) {
|
||||
scr_cyc_dead(d);
|
||||
if (d->kind == SCR_DYN_STR) scr_str_release(d->v.str);
|
||||
d->v.str = (ScrStr *)scr_dyn_free_misc;
|
||||
scr_dyn_free_misc = d;
|
||||
@@ -1435,6 +1439,24 @@ bool scr_dyn_field_eq_num(const ScrDyn *d, const ScrStr *key, double value) {
|
||||
return matches;
|
||||
}
|
||||
|
||||
int32_t scr_dyn_discriminant_case(const ScrDyn *d, const ScrStr *key, ScrStr *const *values,
|
||||
size_t count) {
|
||||
if (!d) return -1;
|
||||
if (d->kind == SCR_DYN_TYPED_REF) return -2;
|
||||
if (d->kind != SCR_DYN_OBJ) return -1;
|
||||
for (size_t i = 0; i < d->v.obj.len; i++) {
|
||||
const ScrDynEntry *entry = &d->v.obj.entries[i];
|
||||
if (entry->key_len != key->len || memcmp(entry->key, key->data, key->len) != 0) continue;
|
||||
if (entry->accessor) return -2;
|
||||
const ScrDyn *member = entry->value;
|
||||
if (!member || member->kind != SCR_DYN_STR) return -1;
|
||||
for (size_t k = 0; k < count; k++)
|
||||
if (scr_str_eq(member->v.str, values[k])) return (int32_t)k;
|
||||
return -1;
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
bool scr_dyn_field_eq_bool(const ScrDyn *d, const ScrStr *key, bool value) {
|
||||
ScrDyn *member = scr_dyn_discriminant(d, key);
|
||||
bool matches = member && member->kind == SCR_DYN_BOOL && member->v.b == value;
|
||||
|
||||
@@ -391,6 +391,30 @@ static bool buffered_after_read(ScrDyn *d) {
|
||||
return buffered(d);
|
||||
}
|
||||
|
||||
/* A collector pass can buffer a number or string box that it reached
|
||||
* through an owner that traces it: scr_xg_release, settling an edge a
|
||||
* restricted pass skipped, calls scr_cyc_on_release on the target. The
|
||||
* box's last release must take it out of its candidate buffer before its
|
||||
* block returns to the free list, which hands it to the next box. */
|
||||
static void buffered_leaf_release(void) {
|
||||
for (uint16_t gen = SCR_CYC_NURSERY; gen < SCR_CYC_NGENS; gen++) {
|
||||
ScrStr *text = scr_str_new("buffered", 8);
|
||||
ScrDyn *leaves[2] = {scr_dyn_new_str(text), scr_dyn_new_num(0.5)};
|
||||
scr_str_release(text);
|
||||
for (int i = 0; i < 2; i++) {
|
||||
scr_cyc_set_gen(scr_cyc_hdr(leaves[i]), gen);
|
||||
scr_cyc_on_release(leaves[i]);
|
||||
scr_dyn_release(leaves[i]);
|
||||
}
|
||||
ScrDyn *fresh[2] = {scr_dyn_new_num(1.5), scr_dyn_new_num(2.5)};
|
||||
assert(!buffered(fresh[0]) && !buffered(fresh[1]));
|
||||
scr_collect_cycles();
|
||||
assert(fresh[0]->v.num == 1.5 && fresh[1]->v.num == 2.5);
|
||||
scr_dyn_release(fresh[0]);
|
||||
scr_dyn_release(fresh[1]);
|
||||
}
|
||||
}
|
||||
|
||||
static ScrDyn *parse_text(const char *text) {
|
||||
ScrStr *input = scr_str_new(text, strlen(text));
|
||||
ScrDyn *parsed = scr_json_parse(input);
|
||||
@@ -823,6 +847,7 @@ int main(void) {
|
||||
shared_property_keys();
|
||||
checked_leaf_cycles();
|
||||
checked_number_storage();
|
||||
buffered_leaf_release();
|
||||
json_intact_regions();
|
||||
json_schema_parse();
|
||||
json_schema_special_names();
|
||||
|
||||
@@ -21,10 +21,8 @@
|
||||
* case preserved, C0-encoded), a single "/" roots a normalized path,
|
||||
* anything else is an OPAQUE path with the C0 control set encoded
|
||||
* (data:text/plain,hi there)
|
||||
*
|
||||
* DOCUMENTED DIVERGENCE: a special host that holds non-ASCII text after
|
||||
* percent-decoding is rejected with "Invalid URL" (no UTS #46 processing),
|
||||
* and "xn--" labels are kept without Punycode validation.
|
||||
* - UTS #46 processing of non-ASCII hosts through the IDNA unit
|
||||
* (scr_url_idna_install)
|
||||
*
|
||||
* Failures THROW catchable TypeErrors through the exception cell with
|
||||
* Node's messages; callers are compiler-emitted pending checks. */
|
||||
@@ -430,16 +428,21 @@ static int hex_val(char c) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* UTS #46 ToASCII (scr_idna.c), which programs that use URLs install at
|
||||
* startup (scr_url_idna_install). NULL refuses hosts that hold non-ASCII
|
||||
* text. */
|
||||
ScrStr *(*scr_url_host_to_ascii)(const char *p, size_t n) = NULL;
|
||||
|
||||
/* The URL Standard's host parser for the constructor and the setters,
|
||||
* with canonical IPv6 serialization:
|
||||
* - a non-special (opaque) host refuses forbidden host code points and
|
||||
* percent-encodes the C0 control set;
|
||||
* - a special host is percent-decoded, lowercased, refused when it holds a
|
||||
* forbidden domain code point, and serialized as dotted decimal when it
|
||||
* ends in a number (or refused when that number is invalid).
|
||||
* Divergence: a special host that decodes to non-ASCII needs UTS #46
|
||||
* processing, which this unit does not link, so it is refused; "xn--"
|
||||
* labels are kept without Punycode validation. */
|
||||
* - a special host is percent-decoded and lowercased; when that leaves
|
||||
* non-ASCII text it goes through UTS #46 ToASCII (as Node's host parser
|
||||
* does, an ASCII host, "xn--" labels included, is only lowercased). The
|
||||
* result is refused when it holds a forbidden domain code point, and
|
||||
* serialized as dotted decimal when it ends in a number (or refused when
|
||||
* that number is invalid). */
|
||||
static bool parse_host(const char *hp, size_t host_len, bool special, bool is_file,
|
||||
ScrStr **host) {
|
||||
bool ipv6 = host_len > 0 && hp[0] == '[';
|
||||
@@ -467,6 +470,7 @@ static bool parse_host(const char *hp, size_t host_len, bool special, bool is_fi
|
||||
*host = ub_take(&hb);
|
||||
return true;
|
||||
}
|
||||
bool non_ascii = false;
|
||||
for (size_t i = 0; i < host_len; i++) {
|
||||
unsigned char c = (unsigned char)hp[i];
|
||||
int hi, lo;
|
||||
@@ -475,12 +479,27 @@ static bool parse_host(const char *hp, size_t host_len, bool special, bool is_fi
|
||||
c = (unsigned char)(hi * 16 + lo);
|
||||
i += 2;
|
||||
}
|
||||
if (url_host_byte_class[c] & URL_FORBIDDEN_DOMAIN) {
|
||||
if (c >= 0x80) non_ascii = true;
|
||||
if (c >= 'A' && c <= 'Z') c = (unsigned char)(c - 'A' + 'a');
|
||||
ub_push(&hb, (char)c);
|
||||
}
|
||||
if (non_ascii) {
|
||||
/* UTS #46 ToASCII, or refused without the IDNA unit. */
|
||||
ScrStr *ascii = scr_url_host_to_ascii ? scr_url_host_to_ascii(hb.data, hb.len) : NULL;
|
||||
free(hb.data);
|
||||
if (!ascii || ascii->len == 0) {
|
||||
scr_str_release(ascii);
|
||||
return false;
|
||||
}
|
||||
ub_init(&hb);
|
||||
ub_append(&hb, ascii->data, ascii->len);
|
||||
scr_str_release(ascii);
|
||||
}
|
||||
for (size_t i = 0; i < hb.len; i++) {
|
||||
if (url_host_byte_class[(unsigned char)hb.data[i]] & URL_FORBIDDEN_DOMAIN) {
|
||||
free(hb.data);
|
||||
return false;
|
||||
}
|
||||
if (c >= 'A' && c <= 'Z') c = (unsigned char)(c - 'A' + 'a');
|
||||
ub_push(&hb, (char)c);
|
||||
}
|
||||
if (url_ends_in_number(hb.data, hb.len)) {
|
||||
UrlBuf ip;
|
||||
|
||||
@@ -46,6 +46,10 @@ void scr_url_set_checked(ScrUrl *u, ScrStr *field, const ScrDyn *value); /* borr
|
||||
* null and has no lowering for an object that is not a URL. Borrowed; +1,
|
||||
* or NULL with the error pending. */
|
||||
ScrDyn *scr_url_to_http_options(ScrUrl *u);
|
||||
/* UTS #46 ToASCII for the host parser: NULL until scr_url_idna_install
|
||||
* (scr_idna.c, emitted at startup by programs that use URLs) sets it. */
|
||||
extern ScrStr *(*scr_url_host_to_ascii)(const char *p, size_t n);
|
||||
void scr_url_idna_install(void);
|
||||
ScrDyn *scr_url_to_http_options_dyn(const ScrDyn *value);
|
||||
/* node:url's legacy surface (scr_url_legacy.c — linked with the regex,
|
||||
* querystring and dyn-async units). url.parse() answers a checked-dynamic
|
||||
|
||||
@@ -394,7 +394,8 @@ 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
|
||||
* node:worker_threads values are dyn objects. bench/json-unions, added
|
||||
* later too, decodes JSON.parse trees. bench/dynamic-methods is
|
||||
* untyped JavaScript. 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
|
||||
@@ -414,6 +415,7 @@ export const DYNAMIC_WORKLOADS = new Set([
|
||||
"bench/typed-array-scan-workers",
|
||||
"bench/log-summary",
|
||||
"bench/inventory-report",
|
||||
"bench/json-unions",
|
||||
]);
|
||||
|
||||
/** Names of the functions (text and weak symbols) in `nm` output. Mach-O
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// JSON cast into a union with many string discriminant literals: the cast
|
||||
// reads the discriminant once and tests only the arm whose literal it
|
||||
// holds, plus the arms that are not records. An arm can list several
|
||||
// literals, and values that are not objects reach the other arms in
|
||||
// canonical order. Valid casts only (see 1002-json-parse-cast.ts); JSON keys
|
||||
// and record fields alphabetical for stringify parity.
|
||||
|
||||
type Expr =
|
||||
| { kind: "num"; value: number }
|
||||
| { kind: "str"; text: string }
|
||||
| { kind: "neg"; operand: Expr }
|
||||
| { kind: "add" | "mul"; left: Expr; right: Expr }
|
||||
| { args: Expr[]; kind: "call"; name: string }
|
||||
| { kind: "method"; name: string; target: Expr }
|
||||
| { kind: "list"; items: Expr[] };
|
||||
|
||||
function show(e: Expr): string {
|
||||
switch (e.kind) {
|
||||
case "num":
|
||||
return String(e.value);
|
||||
case "str":
|
||||
return JSON.stringify(e.text);
|
||||
case "neg":
|
||||
return `-(${show(e.operand)})`;
|
||||
case "add":
|
||||
return `(${show(e.left)} + ${show(e.right)})`;
|
||||
case "mul":
|
||||
return `(${show(e.left)} * ${show(e.right)})`;
|
||||
case "call":
|
||||
return `${e.name}(${e.args.map(show).join(", ")})`;
|
||||
case "method":
|
||||
return `${show(e.target)}.${e.name}()`;
|
||||
case "list":
|
||||
return `[${e.items.map(show).join(", ")}]`;
|
||||
}
|
||||
}
|
||||
|
||||
function evaluate(e: Expr): number {
|
||||
switch (e.kind) {
|
||||
case "num":
|
||||
return e.value;
|
||||
case "str":
|
||||
return e.text.length;
|
||||
case "neg":
|
||||
return -evaluate(e.operand);
|
||||
case "add":
|
||||
return evaluate(e.left) + evaluate(e.right);
|
||||
case "mul":
|
||||
return evaluate(e.left) * evaluate(e.right);
|
||||
case "call":
|
||||
return e.args.reduce((sum, arg) => sum + evaluate(arg), 0);
|
||||
case "method":
|
||||
return evaluate(e.target);
|
||||
case "list":
|
||||
return e.items.length;
|
||||
}
|
||||
}
|
||||
|
||||
const source =
|
||||
'{"kind":"add","left":{"kind":"mul","left":{"kind":"num","value":3},' +
|
||||
'"right":{"kind":"neg","operand":{"kind":"num","value":4}}},' +
|
||||
'"right":{"args":[{"kind":"str","text":"abc"},{"kind":"num","value":2}],"kind":"call","name":"sum"}}';
|
||||
const tree = JSON.parse(source) as Expr;
|
||||
console.log(show(tree));
|
||||
console.log(evaluate(tree));
|
||||
console.log(JSON.stringify(tree) === source);
|
||||
|
||||
const method = JSON.parse(
|
||||
'{"kind":"method","name":"size","target":{"items":[{"kind":"num","value":1}],"kind":"list"}}',
|
||||
) as Expr;
|
||||
console.log(show(method), evaluate(method));
|
||||
|
||||
// A deep list of every arm, built from text.
|
||||
const parts: string[] = [];
|
||||
for (let i = 0; i < 200; i++) {
|
||||
parts.push(
|
||||
i % 3 === 0
|
||||
? `{"kind":"num","value":${i}}`
|
||||
: i % 3 === 1
|
||||
? `{"kind":"neg","operand":{"kind":"num","value":${i}}}`
|
||||
: `{"kind":"mul","left":{"kind":"num","value":${i}},"right":{"kind":"str","text":"xy"}}`,
|
||||
);
|
||||
}
|
||||
const list = JSON.parse(`{"items":[${parts.join(",")}],"kind":"list"}`) as Expr;
|
||||
if (list.kind === "list") {
|
||||
let total = 0;
|
||||
for (const item of list.items) total += evaluate(item);
|
||||
console.log(list.items.length, total);
|
||||
}
|
||||
|
||||
// Arms that are not records: a value that is not an object can only be one
|
||||
// of them.
|
||||
type Token =
|
||||
| { kind: "ident"; name: string }
|
||||
| { kind: "number"; value: number }
|
||||
| { kind: "string"; value: string }
|
||||
| { kind: "punct"; symbol: string }
|
||||
| { kind: "comment"; line: number; text: string }
|
||||
| string
|
||||
| number;
|
||||
|
||||
function describe(token: Token): string {
|
||||
if (typeof token === "string") return `raw ${token}`;
|
||||
if (typeof token === "number") return `count ${token}`;
|
||||
switch (token.kind) {
|
||||
case "ident":
|
||||
return `ident ${token.name}`;
|
||||
case "number":
|
||||
return `number ${token.value}`;
|
||||
case "string":
|
||||
return `string ${JSON.stringify(token.value)}`;
|
||||
case "punct":
|
||||
return `punct ${token.symbol}`;
|
||||
case "comment":
|
||||
return `comment ${token.line}: ${token.text}`;
|
||||
}
|
||||
}
|
||||
|
||||
const tokens = JSON.parse(
|
||||
'[{"kind":"ident","name":"x"},{"kind":"punct","symbol":"="},{"kind":"number","value":42},' +
|
||||
'{"kind":"string","value":"hi"},{"kind":"comment","line":3,"text":"note"},"spare",7]',
|
||||
) as Token[];
|
||||
for (const token of tokens) console.log(describe(token));
|
||||
@@ -0,0 +1,55 @@
|
||||
// A checked cast into a union with many string discriminant literals reads
|
||||
// the discriminant once, but a getter that runs while an arm's fields are
|
||||
// checked can change it. The cast then continues with the arms after the
|
||||
// one that failed, testing each arm's literal again, and matches the arm
|
||||
// the new discriminant names. Each getter changes the discriminant on its
|
||||
// first call only; the program reads the getter once itself, so Node (where
|
||||
// the cast runs no code) reaches the same state. Valid casts only (see
|
||||
// 1002-json-parse-cast.ts).
|
||||
|
||||
type Shape =
|
||||
| { kind: "circle"; radius: number }
|
||||
| { kind: "square"; side: number }
|
||||
| { kind: "rect"; height: number; width: number }
|
||||
| { kind: "tri"; a: number; b: number; c: number };
|
||||
|
||||
function area(shape: Shape): number {
|
||||
switch (shape.kind) {
|
||||
case "circle":
|
||||
return Math.round(Math.PI * shape.radius * shape.radius);
|
||||
case "square":
|
||||
return shape.side * shape.side;
|
||||
case "rect":
|
||||
return shape.width * shape.height;
|
||||
case "tri": {
|
||||
const s = (shape.a + shape.b + shape.c) / 2;
|
||||
return Math.sqrt(s * (s - shape.a) * (s - shape.b) * (s - shape.c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The circle arm reads `radius`, whose getter turns the value into a square.
|
||||
let squareReads = 0;
|
||||
const square: any = {
|
||||
kind: "circle",
|
||||
get radius() {
|
||||
squareReads++;
|
||||
if (squareReads === 1) square.kind = "square";
|
||||
return "none";
|
||||
},
|
||||
side: 3,
|
||||
};
|
||||
const asSquare = square as Shape;
|
||||
void square.radius;
|
||||
console.log(square.kind, asSquare.kind, area(asSquare));
|
||||
|
||||
// Values without getters take the arm their discriminant names.
|
||||
const plain: any[] = [
|
||||
{ kind: "rect", width: 2, height: 5 },
|
||||
{ kind: "tri", a: 3, b: 4, c: 5 },
|
||||
{ kind: "circle", radius: 2 },
|
||||
];
|
||||
for (const value of plain) {
|
||||
const shape = value as Shape;
|
||||
console.log(shape.kind, area(shape));
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// WHATWG URL hosts that hold non-ASCII text, directly or after
|
||||
// percent-decoding, go through UTS #46 ToASCII as the host parser in Node
|
||||
// does: mapped and normalized labels become Punycode, and disallowed code
|
||||
// points, invalid joiners and bidi violations make the URL invalid. Cases
|
||||
// from the WPT url test data, plus setters, pathToFileURL UNC hosts and
|
||||
// URL.canParse.
|
||||
import url from "node:url";
|
||||
|
||||
const parses = [
|
||||
["http://é@é", null],
|
||||
["http://GOO\u200B\u2060\uFEFFgoo.com", "http://other.com/"],
|
||||
["http://www.foo。bar.com", "http://other.com/"],
|
||||
["http://Go.com", "http://other.com/"],
|
||||
["http://你好你好", "http://other.com/"],
|
||||
["https://faß.ExAmPlE/", null],
|
||||
["http://0Xc0.0250.01", "http://other.com/"],
|
||||
["ftp://%e2%98%83", null],
|
||||
["https://%e2%98%83", null],
|
||||
["file://a\u00ADb/p", null],
|
||||
["file://a%C2%ADb/p", null],
|
||||
["file://loC𝐀𝐋𝐇𝐨𝐬𝐭/usr/bin", null],
|
||||
["https://a%C2%ADb/", null],
|
||||
["http://münchen.de/straße", null],
|
||||
["https://日本語.jp/", null],
|
||||
["http://例え.テスト/", null],
|
||||
["http://xn--mnchen-3ya.de/", null],
|
||||
["http://a\u200Db.com/", null],
|
||||
["http://אa.com/", null],
|
||||
["http://ab�c/", null],
|
||||
["ws://ÉXAMPLE.org:8080/x", null],
|
||||
];
|
||||
function describe(value) {
|
||||
return [value.href, value.host, value.hostname, value.origin].join(" | ");
|
||||
}
|
||||
for (const [input, base] of parses) {
|
||||
let line;
|
||||
try {
|
||||
line = describe(base === null ? new URL(input) : new URL(input, base));
|
||||
} catch (e) {
|
||||
line = `throws ${e.code}`;
|
||||
}
|
||||
console.log(JSON.stringify(input), "=>", line, URL.canParse(input, base ?? undefined));
|
||||
}
|
||||
const writes = [
|
||||
["host", "https://x/", "ß"],
|
||||
["host", "https://example.com/", "a%C2%ADb"],
|
||||
["hostname", "https://example.com/", "a%C2%ADb"],
|
||||
["hostname", "https://example.com/", "BÜCHER.example"],
|
||||
];
|
||||
for (const [name, href, value] of writes) {
|
||||
const target = new URL(href);
|
||||
if (name === "host") target.host = value;
|
||||
else target.hostname = value;
|
||||
console.log(name, JSON.stringify(value), "=>", describe(target));
|
||||
}
|
||||
for (const path of ["\\\\münchen\\share\\f", "\\\\BÜCHER\\s\\f"]) {
|
||||
console.log(JSON.stringify(path), url.pathToFileURL(path, { windows: true }).href);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Module bindings of a JavaScript package whose initializers combine an
|
||||
// untyped value with a string, number or boolean through `||`, `&&`, `??`
|
||||
// or `?:` (pi-coding-agent's config.js): other modules and functions read
|
||||
// the values as in Node.
|
||||
import { banner, usage } from "app-config";
|
||||
|
||||
console.log(banner);
|
||||
console.log(usage("check"));
|
||||
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
let pkg = {};
|
||||
try {
|
||||
pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf-8"));
|
||||
}
|
||||
catch (e) {
|
||||
if (e.code !== "ENOENT")
|
||||
throw e;
|
||||
}
|
||||
const configName = pkg.appConfig?.name;
|
||||
export const APP_NAME = configName || "app";
|
||||
export const APP_TITLE = configName ? APP_NAME : "App";
|
||||
export const CONFIG_DIR = pkg.appConfig?.configDir || ".config";
|
||||
export const VERSION = pkg.version ?? "0.0.0";
|
||||
export const RETRIES = pkg.appConfig?.retries ?? 3;
|
||||
export const isBundled = typeof APP_BUNDLED !== "undefined" && APP_BUNDLED;
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export declare const banner: string;
|
||||
export declare function usage(kind: "check" | "list"): string;
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export { banner, usage } from "./usage.js";
|
||||
Generated
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
import { APP_NAME, APP_TITLE, CONFIG_DIR, RETRIES, VERSION, isBundled } from "./config.js";
|
||||
|
||||
const USAGE = {
|
||||
check: `${APP_NAME} check --dir ${CONFIG_DIR}`,
|
||||
list: `${APP_NAME} list`,
|
||||
};
|
||||
|
||||
export const banner = `${APP_TITLE} ${VERSION} (bundled: ${isBundled}, retries: ${RETRIES})`;
|
||||
|
||||
export function usage(kind) {
|
||||
return `${USAGE[kind]} [${APP_NAME.toUpperCase()}_HOME=${CONFIG_DIR}] ${typeof isBundled} ${RETRIES + 1}`;
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "app-config", "version": "2.1.0", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "appConfig": { "configDir": ".app" } }
|
||||
@@ -298,8 +298,10 @@ test("pull requests run the full suite on Linux and the darwin suite on macOS",
|
||||
// runner, and directly elsewhere.
|
||||
const linuxRun = linux.steps.at(-1)!.run as string;
|
||||
expect(linuxRun.startsWith("ulimit -c 0\n")).toBe(true);
|
||||
expect(linuxRun).toContain("systemd-run --scope --quiet -p MemoryMax=14G -p OOMPolicy=continue");
|
||||
expect(linuxRun).toMatch(/-- env PATH="\$PATH" HOME="\$HOME" node scripts\/ci-test\.mjs\n/);
|
||||
expect(linuxRun).toContain("systemd-run --scope --quiet -p MemoryMax=15G -p OOMPolicy=continue");
|
||||
expect(linuxRun).toMatch(
|
||||
/-- env PATH="\$PATH" HOME="\$HOME" NODE_OPTIONS="--max-old-space-size=4096" node scripts\/ci-test\.mjs\n/,
|
||||
);
|
||||
expect(linuxRun.endsWith("fi\nnode scripts/ci-test.mjs\n")).toBe(true);
|
||||
expect(macos.steps.at(-1)!.run).toBe("node scripts/ci-test.mjs");
|
||||
for (const job of [linux, macos]) expect(job.strategy.matrix.flavor).toEqual(["plain", "san"]);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 5844, "san": 7609 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3800, "san": 4900 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2500, "san": 3500 }
|
||||
@@ -396,6 +396,95 @@ console.log("unreachable", s.kind);
|
||||
expect(r.stderr).toContain("Uncaught TypeError: expected object | object at $, got object");
|
||||
});
|
||||
|
||||
test("a union read by its discriminant fails with every arm named, as arm-by-arm tests do", async () => {
|
||||
// Five record arms with six literals, plus string and number arms: the
|
||||
// check reads `kind` once and tests only the arm it names and the
|
||||
// arms that are not records.
|
||||
const r = await compileAndRun(
|
||||
"union-discriminant-dispatch",
|
||||
`type Expr =
|
||||
| { kind: "num"; value: number }
|
||||
| { kind: "str"; text: string }
|
||||
| { kind: "neg"; operand: Expr }
|
||||
| { kind: "add" | "mul"; left: Expr; right: Expr }
|
||||
| { items: Expr[]; kind: "list" }
|
||||
| string
|
||||
| number;
|
||||
const inputs = [
|
||||
'{"kind":"num"}',
|
||||
'{"kind":"nope","value":1}',
|
||||
'{"kind":7,"value":1}',
|
||||
'{"value":1}',
|
||||
'{"kind":"neg","operand":{"kind":"add","left":{"kind":"num","value":1},"right":{"kind":"str","text":5}}}',
|
||||
'true',
|
||||
'{"items":["a",3,{"kind":"mul","left":"x","right":2}],"kind":"list"}',
|
||||
];
|
||||
for (const input of inputs) {
|
||||
try {
|
||||
console.log(JSON.stringify(JSON.parse(input) as Expr));
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
const any = "number | object | object | object | object | object | string";
|
||||
expect(r.exitCode).toBe(0);
|
||||
expect(r.stdout).toBe(
|
||||
[
|
||||
`expected ${any} at $, got object`,
|
||||
`expected ${any} at $, got object`,
|
||||
`expected ${any} at $, got object`,
|
||||
`expected ${any} at $, got object`,
|
||||
`expected ${any} at $, got object`,
|
||||
`expected ${any} at $, got boolean`,
|
||||
'{"items":["a",3,{"kind":"mul","left":"x","right":2}],"kind":"list"}',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("a getter that changes the discriminant during an arm's check continues with the later arms", async () => {
|
||||
// Each getter sets `kind` and fails its arm's check: to a later arm's
|
||||
// literal (that arm matches), to an earlier arm's literal (arm-by-arm
|
||||
// tests do not go back), and to a literal no arm holds.
|
||||
const r = await compileAndRun(
|
||||
"union-discriminant-getter",
|
||||
`type Shape =
|
||||
| { kind: "circle"; radius: number }
|
||||
| { kind: "square"; side: number }
|
||||
| { kind: "rect"; height: number; width: number }
|
||||
| { kind: "tri"; a: number; b: number; c: number };
|
||||
const flip = (value: any, to: string): string => {
|
||||
value.kind = to;
|
||||
return "none";
|
||||
};
|
||||
const later: any = { kind: "circle", get radius() { return flip(later, "square"); }, side: 3 };
|
||||
const earlier: any = { kind: "square", radius: 2, get side() { return flip(earlier, "circle"); } };
|
||||
const unknown: any = { kind: "square", get side() { return flip(unknown, "hexagon"); } };
|
||||
const inputs = [later, earlier, unknown];
|
||||
for (const input of inputs) {
|
||||
try {
|
||||
const shape = input as Shape;
|
||||
console.log("matched", shape.kind);
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
const any = "object | object | object | object";
|
||||
expect(r.exitCode).toBe(0);
|
||||
expect(r.stdout).toBe(
|
||||
[
|
||||
"matched square",
|
||||
`expected ${any} at $, got object`,
|
||||
`expected ${any} at $, got object`,
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("nested paths point at the offending element", async () => {
|
||||
const r = await compileAndRun(
|
||||
"nested-path",
|
||||
|
||||
Reference in New Issue
Block a user