mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +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.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
- **Symbol fields:** Instance fields require a stable module-level `Symbol()` or `Symbol.for()` binding with a literal description or constant string binding.
|
||||
A `[Symbol.toStringTag]` field or getter with a string value sets the tag that `String()`, `Object.prototype.toString` and `util.inspect` print; fields keyed by other well-known symbols are unsupported.
|
||||
Reassigned keys, runtime-computed descriptions, distinct symbols with the same description in one class, and copying symbol fields with rest bindings or `Object.assign` are unsupported.
|
||||
JavaScript object spreads copy represented own symbol fields.
|
||||
Equal registry keys share a field across modules.
|
||||
|
||||
@@ -760,3 +760,154 @@ test("a parameter stored as its last use takes the owned convention", () => {
|
||||
const passed = ctor([{ kind: "exprStmt", expr: call("use", [ref("item", cell)]), loc }]);
|
||||
expect(analyze(passed).borrowed.get("%Box.constructor")).toEqual(new Set([0, 1]));
|
||||
});
|
||||
|
||||
test("a wrap into a pointer union forwards the wrapped binding to the callee's parameter", () => {
|
||||
const wrap = (id: string): IrExpr => ({
|
||||
kind: "unionWrap",
|
||||
unionId: "optional",
|
||||
tag: 0,
|
||||
value: ref(id, record),
|
||||
type: optional,
|
||||
loc,
|
||||
});
|
||||
const leaf = helper("leaf");
|
||||
const outer = helper("outer");
|
||||
outer.params[0]!.type = outer.locals[0]!.type = record;
|
||||
outer.locals.push(local("cell", record));
|
||||
outer.body = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "cell",
|
||||
init: { kind: "recordLit", fields: [], type: record, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "exprStmt", expr: call("leaf", [wrap("value")]), loc },
|
||||
ret(call("leaf", [wrap("cell")])),
|
||||
];
|
||||
const functions = new Map([leaf, outer].map((fn) => [fn.name, fn]));
|
||||
const analyzeWith = (pointer: boolean) =>
|
||||
analyzeCallLifetimes(
|
||||
functions,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
() => pointer,
|
||||
);
|
||||
// A boxed union builds a new box around the payload: a whole-value use.
|
||||
expect(analyzeWith(false).parameters.get("outer")).toBeUndefined();
|
||||
expect(analyzeWith(false).locals.get("outer")).toEqual(new Set());
|
||||
// A pointer union passes the payload pointer itself.
|
||||
expect(analyzeWith(true).parameters.get("outer")).toEqual(new Set([0]));
|
||||
expect(analyzeWith(true).locals.get("outer")).toEqual(new Set(["cell"]));
|
||||
// A callee that keeps its parameter keeps the wrapped binding too.
|
||||
leaf.body.push(ret(ref("value")));
|
||||
expect(analyzeWith(true).parameters.get("outer")).toBeUndefined();
|
||||
expect(analyzeWith(true).locals.get("outer")).toEqual(new Set());
|
||||
});
|
||||
|
||||
test("a pointer-wrapped binding needs a callee that keeps the narrowed payload in place", () => {
|
||||
const wrapCell: IrExpr = {
|
||||
kind: "unionWrap",
|
||||
unionId: "optional",
|
||||
tag: 0,
|
||||
value: ref("cell", record),
|
||||
type: optional,
|
||||
loc,
|
||||
};
|
||||
const outer = helper("outer", []);
|
||||
outer.locals.push(local("cell", record));
|
||||
outer.body = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "cell",
|
||||
init: { kind: "recordLit", fields: [], type: record, loc },
|
||||
loc,
|
||||
},
|
||||
ret(call("leaf", [wrapCell])),
|
||||
];
|
||||
const read = (value: IrExpr): IrExpr => ({
|
||||
kind: "recordGet",
|
||||
obj: value,
|
||||
shapeId: "cell",
|
||||
field: "x",
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const keeps = (leaf: IrFunction, ...more: IrFunction[]): boolean => {
|
||||
const result = analyzeCallLifetimes(
|
||||
new Map([outer, leaf, ...more].map((fn) => [fn.name, fn])),
|
||||
(className) => className === "Node",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
() => true,
|
||||
);
|
||||
return result.locals.get("outer")?.has("cell") !== true;
|
||||
};
|
||||
const leafWith = (body: IrStmt[]): IrFunction => ({ ...helper("leaf"), body });
|
||||
// Tag tests and field reads of the narrowed payload leave it in place.
|
||||
expect(keeps(leafWith([{ kind: "exprStmt", expr: tag("value"), loc }]))).toBe(false);
|
||||
expect(keeps(leafWith([ret(read(narrow("value")))]))).toBe(false);
|
||||
// So do field reads through a checked narrowing, which throws on the
|
||||
// other arm (`value.x` of a `T | undefined` parameter).
|
||||
const checked: IrExpr = {
|
||||
kind: "ternary",
|
||||
cond: tag("value"),
|
||||
then: { kind: "libCall", fn: "error.nodeThrow", args: [], type: record, loc },
|
||||
else_: narrow("value"),
|
||||
type: record,
|
||||
loc,
|
||||
};
|
||||
expect(keeps(leafWith([ret(read(checked))]))).toBe(false);
|
||||
// Returning it keeps the payload.
|
||||
const returnsChecked = leafWith([ret(checked)]);
|
||||
returnsChecked.returnType = record;
|
||||
expect(keeps(returnsChecked)).toBe(true);
|
||||
// Returning the narrowed payload, or a narrowed nullish read, keeps it,
|
||||
// although the union itself is only projected.
|
||||
const returned = leafWith([ret(narrow("value"))]);
|
||||
returned.returnType = record;
|
||||
expect(analyze(returned).parameters.get("leaf")).toEqual(new Set([0]));
|
||||
expect(keeps(returned)).toBe(true);
|
||||
const defaulted = leafWith([
|
||||
ret({ kind: "nullish", left: ref("value"), right: ref("other", record), type: record, loc }),
|
||||
]);
|
||||
defaulted.locals.push(local("other", record));
|
||||
expect(keeps(defaulted)).toBe(true);
|
||||
// So does a nullable-pointer field store of the union.
|
||||
const self: IrType = { kind: "object", className: "Node" };
|
||||
const stored = leafWith([
|
||||
{
|
||||
kind: "fieldSet",
|
||||
obj: ref("this", self),
|
||||
className: "Node",
|
||||
field: "next",
|
||||
value: ref("value"),
|
||||
loc,
|
||||
},
|
||||
]);
|
||||
stored.params = [{ localId: "value", name: "value", type: optional }];
|
||||
stored.locals.push(local("this", self));
|
||||
expect(keeps(stored)).toBe(true);
|
||||
// A narrowed payload passed on, or the union passed on, is in place when
|
||||
// the next callee keeps it in place.
|
||||
const inner = (body: IrStmt[], type: IrType = record): IrFunction => {
|
||||
const fn = helper("inner");
|
||||
fn.params[0]!.type = fn.locals[0]!.type = type;
|
||||
fn.body = body;
|
||||
return fn;
|
||||
};
|
||||
const passNarrowed = leafWith([ret(call("inner", [narrow("value")]))]);
|
||||
expect(keeps(passNarrowed, inner([ret(read(ref("value", record)))]))).toBe(false);
|
||||
const returnsRecord = inner([ret(ref("value", record))]);
|
||||
returnsRecord.returnType = record;
|
||||
expect(keeps(passNarrowed, returnsRecord)).toBe(true);
|
||||
const passUnion = leafWith([ret(call("inner", [ref("value")]))]);
|
||||
expect(keeps(passUnion, inner([ret(read(narrow("value")))], optional))).toBe(false);
|
||||
const innerReturned = inner([ret(narrow("value"))], optional);
|
||||
innerReturned.returnType = record;
|
||||
expect(keeps(passUnion, innerReturned)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -14,6 +14,11 @@ import { storedLastUseRead } from "./local-extents.js";
|
||||
interface ForwardedUse {
|
||||
callee: string;
|
||||
index: number;
|
||||
/** The binding reached the call inside a nullable-pointer union
|
||||
* (PointerWrapTest): the callee's parameter holds the binding itself as
|
||||
* its payload, so the callee must keep the payload, not only the union,
|
||||
* in place. */
|
||||
wrapped: boolean;
|
||||
}
|
||||
|
||||
interface Uses {
|
||||
@@ -21,6 +26,13 @@ interface Uses {
|
||||
written: Set<string>;
|
||||
declarations: Map<string, number>;
|
||||
forwards: Map<string, ForwardedUse[]>;
|
||||
/** Union bindings whose payload a use may keep: a narrowed read that is
|
||||
* not projected or passed on, a narrowed nullish read, a store into a
|
||||
* nullable-pointer field. The union itself can still be only projected. */
|
||||
payloadKept: Set<string>;
|
||||
/** Narrowed payloads of union bindings passed directly as call arguments:
|
||||
* the payload stays in place if the callee's parameter is proved. */
|
||||
payloadForwards: Map<string, ForwardedUse[]>;
|
||||
}
|
||||
|
||||
interface Parameter {
|
||||
@@ -99,19 +111,55 @@ const NO_INLINE_FIELDS: InlineFieldTest = { record: () => false, classField: ()
|
||||
* reads the record and keeps no reference to it. */
|
||||
export type RecordDynCopyTest = (shapeId: string) => boolean;
|
||||
|
||||
/** Wraps into a nullable-pointer union whose value is the payload pointer
|
||||
* itself (LlEmitter.borrowableNullableWrap): the wrapped binding reaches the
|
||||
* callee's parameter unchanged. */
|
||||
export type PointerWrapTest = (wrap: IrExpr & { kind: "unionWrap" }) => boolean;
|
||||
|
||||
function collectUses(
|
||||
fn: IrFunction,
|
||||
nullableField: NullableFieldTest,
|
||||
registerReturn: boolean,
|
||||
inlineField: InlineFieldTest,
|
||||
recordDynCopy: RecordDynCopyTest,
|
||||
pointerWrap: PointerWrapTest,
|
||||
): Uses {
|
||||
const uses: Uses = {
|
||||
invalid: new Set(),
|
||||
written: new Set(),
|
||||
declarations: new Map(),
|
||||
forwards: new Map(),
|
||||
payloadKept: new Set(),
|
||||
payloadForwards: new Map(),
|
||||
};
|
||||
const forward = (
|
||||
table: Map<string, ForwardedUse[]>,
|
||||
localId: string,
|
||||
use: ForwardedUse,
|
||||
): void => {
|
||||
let forwards = table.get(localId);
|
||||
if (!forwards) table.set(localId, (forwards = []));
|
||||
forwards.push(use);
|
||||
};
|
||||
/** The union binding `x` whose payload `value` is: `unionNarrow(x)`, or a
|
||||
* checked narrowing `unionIsTag(x) ? <libCall> : unionNarrow(x)` (either
|
||||
* order), whose other branch is visited as any use. */
|
||||
function narrowedBinding(value: IrExpr): string | null {
|
||||
if (value.kind === "unionNarrow")
|
||||
return value.value.kind === "varRef" ? value.value.localId : null;
|
||||
if (
|
||||
value.kind !== "ternary" ||
|
||||
value.cond.kind !== "unionIsTag" ||
|
||||
value.cond.value.kind !== "varRef"
|
||||
)
|
||||
return null;
|
||||
const id = value.cond.value.localId;
|
||||
if (value.else_.kind === "libCall" && narrowedBinding(value.then) === id)
|
||||
return expr(value.else_) ? id : null;
|
||||
if (value.then.kind === "libCall" && narrowedBinding(value.else_) === id)
|
||||
return expr(value.then) ? id : null;
|
||||
return null;
|
||||
}
|
||||
const aliases: { alias: string; source: string }[] = [];
|
||||
const stringInput = (value: IrExpr): boolean => {
|
||||
if (value.kind === "varRef" && value.type.kind === "string") return true;
|
||||
@@ -145,6 +193,12 @@ function collectUses(
|
||||
if (borrowsMapReadInputs(node)) return expr(node.receiver) && node.args.every(stringInput);
|
||||
break;
|
||||
case "unionNarrow":
|
||||
// The payload may escape; the union itself does not.
|
||||
if (node.value.kind === "varRef") {
|
||||
uses.payloadKept.add(node.value.localId);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case "unionIsTag":
|
||||
if (node.value.kind === "varRef") return true;
|
||||
break;
|
||||
@@ -161,12 +215,16 @@ function collectUses(
|
||||
node.left.kind === "varRef" &&
|
||||
node.left.type.kind === "union" &&
|
||||
node.type.kind !== "union"
|
||||
)
|
||||
) {
|
||||
uses.payloadKept.add(node.left.localId);
|
||||
return expr(node.right);
|
||||
}
|
||||
break;
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
if (node.obj.kind === "varRef") return true;
|
||||
// A field read of a binding, or of a union binding's narrowed
|
||||
// payload, keeps neither.
|
||||
if (node.obj.kind === "varRef" || narrowedBinding(node.obj) !== null) return true;
|
||||
break;
|
||||
case "dynFrom":
|
||||
// A copied record converts field by field; a live capsule keeps it.
|
||||
@@ -200,13 +258,19 @@ function collectUses(
|
||||
return node.args.every(expr);
|
||||
case "call":
|
||||
node.args.forEach((arg, index) => {
|
||||
if (arg.kind !== "varRef") {
|
||||
const narrowed = narrowedBinding(arg);
|
||||
if (narrowed !== null) {
|
||||
forward(uses.payloadForwards, narrowed, { callee: node.callee, index, wrapped: false });
|
||||
return;
|
||||
}
|
||||
const wrapped =
|
||||
arg.kind === "unionWrap" && arg.value.kind === "varRef" && pointerWrap(arg);
|
||||
const value = wrapped ? arg.value : arg;
|
||||
if (value.kind !== "varRef") {
|
||||
expr(arg);
|
||||
return;
|
||||
}
|
||||
let forwards = uses.forwards.get(arg.localId);
|
||||
if (!forwards) uses.forwards.set(arg.localId, (forwards = []));
|
||||
forwards.push({ callee: node.callee, index });
|
||||
forward(uses.forwards, value.localId, { callee: node.callee, index, wrapped });
|
||||
});
|
||||
return true;
|
||||
case "assignExpr":
|
||||
@@ -250,8 +314,11 @@ function collectUses(
|
||||
node.value.kind === "varRef" &&
|
||||
node.value.type.kind === "union" &&
|
||||
nullableField(node.className, node.field)
|
||||
)
|
||||
) {
|
||||
// The field retains the payload.
|
||||
uses.payloadKept.add(node.value.localId);
|
||||
return expr(node.obj);
|
||||
}
|
||||
const stored = fieldStore(
|
||||
node.obj,
|
||||
node.value,
|
||||
@@ -345,10 +412,14 @@ export function analyzeCallLifetimes(
|
||||
registerReturns: ReadonlySet<string> = new Set(),
|
||||
inlineField: InlineFieldTest = NO_INLINE_FIELDS,
|
||||
recordDynCopy: RecordDynCopyTest = () => false,
|
||||
pointerWrap: PointerWrapTest = () => false,
|
||||
): CallLifetimes {
|
||||
const usesByFunction = new Map<string, Uses>();
|
||||
const lazyCaptures = new LazyCaptures(functions);
|
||||
const nodes = new Map<string, Parameter[]>();
|
||||
/** Union parameters that keep their payload in place too, which a
|
||||
* pointer-wrapped argument needs (ForwardedUse.wrapped). */
|
||||
const payloadNodes = new Map<string, (Parameter | undefined)[]>();
|
||||
const unsafe: Parameter[] = [];
|
||||
const consumed: { fn: string; index: number }[] = [];
|
||||
const result: CallLifetimes = {
|
||||
@@ -361,7 +432,7 @@ export function analyzeCallLifetimes(
|
||||
};
|
||||
for (const fn of functions.values()) {
|
||||
if (fn.generator !== undefined && fn.classCaptures === undefined) {
|
||||
const uses = collectUses(fn, nullableField, false, inlineField, recordDynCopy);
|
||||
const uses = collectUses(fn, nullableField, false, inlineField, recordDynCopy, pointerWrap);
|
||||
const locals = new Map(fn.locals.map((local) => [local.id, local]));
|
||||
const stable = new Set<string>();
|
||||
for (const param of fn.params) {
|
||||
@@ -386,6 +457,7 @@ export function analyzeCallLifetimes(
|
||||
registerReturns.has(fn.name),
|
||||
inlineField,
|
||||
recordDynCopy,
|
||||
pointerWrap,
|
||||
);
|
||||
usesByFunction.set(fn.name, uses);
|
||||
// Class-capturing bodies get only local facts (computed below from
|
||||
@@ -460,23 +532,50 @@ export function analyzeCallLifetimes(
|
||||
return node;
|
||||
}),
|
||||
);
|
||||
payloadNodes.set(
|
||||
fn.name,
|
||||
fn.params.map((param) => {
|
||||
if (param.type.kind !== "union") return undefined;
|
||||
const node: Parameter = { safe: !uses.payloadKept.has(param.localId), callers: new Set() };
|
||||
if (!node.safe) unsafe.push(node);
|
||||
return node;
|
||||
}),
|
||||
);
|
||||
}
|
||||
/** The fact a forwarded use needs from the callee's parameter. */
|
||||
const target = (use: ForwardedUse): Parameter | undefined =>
|
||||
use.wrapped ? payloadNodes.get(use.callee)?.[use.index] : nodes.get(use.callee)?.[use.index];
|
||||
for (const fn of functions.values()) {
|
||||
const params = nodes.get(fn.name);
|
||||
const uses = usesByFunction.get(fn.name);
|
||||
if (!params || !uses) continue;
|
||||
fn.params.forEach((param, index) => {
|
||||
const node = params[index]!;
|
||||
const depend = (node: Parameter, callees: (Parameter | undefined)[]): void => {
|
||||
if (!node.safe) return;
|
||||
for (const use of uses.forwards.get(param.localId) ?? []) {
|
||||
const callee = nodes.get(use.callee)?.[use.index];
|
||||
for (const callee of callees) {
|
||||
if (!callee) {
|
||||
node.safe = false;
|
||||
unsafe.push(node);
|
||||
break;
|
||||
return;
|
||||
}
|
||||
callee.callers.add(node);
|
||||
}
|
||||
};
|
||||
fn.params.forEach((param, index) => {
|
||||
const node = params[index]!;
|
||||
const forwards = uses.forwards.get(param.localId) ?? [];
|
||||
depend(node, forwards.map(target));
|
||||
const payload = payloadNodes.get(fn.name)?.[index];
|
||||
if (!payload) return;
|
||||
// The payload stays in place when the union does, when every callee
|
||||
// the union reaches keeps its payload too, and when every callee its
|
||||
// narrowed payload reaches proves that parameter.
|
||||
depend(payload, [
|
||||
node,
|
||||
...forwards.map((use) => payloadNodes.get(use.callee)?.[use.index]),
|
||||
...(uses.payloadForwards.get(param.localId) ?? []).map(
|
||||
(use) => nodes.get(use.callee)?.[use.index],
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < unsafe.length; i++) {
|
||||
@@ -508,7 +607,7 @@ export function analyzeCallLifetimes(
|
||||
)
|
||||
continue;
|
||||
const forwards = uses.forwards.get(local.id) ?? [];
|
||||
if (forwards.every((use) => result.parameters.get(use.callee)?.has(use.index) === true)) {
|
||||
if (forwards.every((use) => target(use)?.safe === true)) {
|
||||
projected.add(local.id);
|
||||
if (eligible(local) && !uses.written.has(local.id)) safe.add(local.id);
|
||||
}
|
||||
|
||||
@@ -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()`] : []),
|
||||
|
||||
@@ -1809,6 +1809,7 @@ export class LlEmitter {
|
||||
const shape = this.recordsById.get(shapeId);
|
||||
return shape !== undefined && !recordDynKeepsIdentity(shape);
|
||||
},
|
||||
(wrap) => this.borrowableNullableWrap(wrap) === "ref",
|
||||
);
|
||||
this.lentStringParameters =
|
||||
this.debug === null ? findLentStringParameters(this.fnByName, this.callLifetimes) : new Map();
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
isRefCounted,
|
||||
shapeHasAccessorSlots,
|
||||
typeKey,
|
||||
type IrClassDef,
|
||||
} from "../../ir/ir.js";
|
||||
import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js";
|
||||
import { BlockBuilder } from "./blocks.js";
|
||||
@@ -272,9 +273,7 @@ export function streamTypedRefCommitAdapter(
|
||||
);
|
||||
});
|
||||
}
|
||||
const symbols = new Map(
|
||||
(meta.def.symbolFields ?? []).map((symbol) => [symbol.field, symbol.globalId]),
|
||||
);
|
||||
const symbols = new Map((meta.def.symbolFields ?? []).map((symbol) => [symbol.field, symbol]));
|
||||
const fields = meta.def.fields.filter(
|
||||
(field) =>
|
||||
(isClassOwnEnumerableFieldName(field.name) || symbols.has(field.name)) &&
|
||||
@@ -294,13 +293,15 @@ export function streamTypedRefCommitAdapter(
|
||||
const inline = host.inlineUnions.classField(t.className, field.name);
|
||||
const fieldTy = int32 ? "i32" : llFieldType(field.type, inline);
|
||||
const checkTy = host.llType(field.type);
|
||||
const symbolGlobal = symbols.get(field.name);
|
||||
if (symbolGlobal) {
|
||||
const symbolKey = symbols.get(field.name);
|
||||
if (symbolKey) {
|
||||
host.declare(`declare ptr @scr_dyn_symbol_key_get(ptr, ptr, i1 zeroext)`);
|
||||
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
const keyLines = symbolKeyLines(host, symbolKey, `f${index}_keyraw`);
|
||||
lines.push(
|
||||
` %f${index}_keyraw = load ptr, ptr @${mangleGlobal(symbolGlobal)}`,
|
||||
...keyLines.load,
|
||||
` %f${index}_key = call ptr @${host.dyn.toDynHelper(SYMBOL_T)}(ptr %f${index}_keyraw)`,
|
||||
...keyLines.release,
|
||||
);
|
||||
if (field.type.kind === "symbol") {
|
||||
// A base constructor can mutate the instance before this
|
||||
@@ -328,7 +329,7 @@ export function streamTypedRefCommitAdapter(
|
||||
lines.push(
|
||||
` %${raw} = call ptr @scr_dyn_obj_get(ptr %d, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")})`,
|
||||
);
|
||||
if (meta.def.tracksOwnFields && !symbolGlobal)
|
||||
if (meta.def.tracksOwnFields && !symbolKey)
|
||||
lines.push(
|
||||
` %f${index}_present = icmp ne ptr %${raw}, null`,
|
||||
` br i1 %f${index}_present, label %f${index}_read, label %${after}`,
|
||||
@@ -339,7 +340,7 @@ export function streamTypedRefCommitAdapter(
|
||||
` %${undef} = call ptr @scr_dyn_undefined()`,
|
||||
` %${input} = select i1 %${missing}, ptr %${undef}, ptr %${raw}`,
|
||||
` %${next} = call ${field.type.kind === "bool" ? "zeroext " : ""}${checkTy} @${host.dyn.dynCheckHelper(field.type)}(ptr %${input}, ptr null)`,
|
||||
...(symbolGlobal ? [` call void @scr_dyn_release_v(ptr %${raw})`] : []),
|
||||
...(symbolKey ? [` call void @scr_dyn_release_v(ptr %${raw})`] : []),
|
||||
...host.pendingTestLines(`%${pending}`),
|
||||
` br i1 %${pending}, label %done, label %${store}`,
|
||||
`${store}:`,
|
||||
@@ -494,16 +495,18 @@ export function streamTypedRefCommitAdapter(
|
||||
? `declare void @scr_dyn_symbol_key_placeholder(ptr, ptr)`
|
||||
: `declare void @scr_dyn_key_delete_computed(ptr, ptr, i1 zeroext)`,
|
||||
);
|
||||
[...symbols.values()].forEach((globalId, index) =>
|
||||
[...symbols.values()].forEach((symbol, index) => {
|
||||
const keyLines = symbolKeyLines(host, symbol, `bag_symraw${index}`);
|
||||
lines.push(
|
||||
` %bag_symraw${index} = load ptr, ptr @${mangleGlobal(globalId)}`,
|
||||
...keyLines.load,
|
||||
` %bag_sym${index} = call ptr @${host.dyn.toDynHelper(SYMBOL_T)}(ptr %bag_symraw${index})`,
|
||||
...keyLines.release,
|
||||
meta.def.tracksOwnFields
|
||||
? ` call void @scr_dyn_symbol_key_placeholder(ptr %bag, ptr %bag_sym${index})`
|
||||
: ` call void @scr_dyn_key_delete_computed(ptr %bag, ptr %bag_sym${index}, i1 zeroext false)`,
|
||||
` call void @scr_dyn_release_v(ptr %bag_sym${index})`,
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES);
|
||||
lines.push(
|
||||
` %bag_ptr = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 ${index}`,
|
||||
@@ -700,6 +703,27 @@ export function liveDynUnionRefAdapter(
|
||||
return sym;
|
||||
}
|
||||
|
||||
/** The raw ScrSym of a class's symbol field key into `%name`: a module
|
||||
* key is borrowed from its global, a well-known one comes from the
|
||||
* runtime's registry (+1, released by the `release` lines once the key
|
||||
* value holds it). */
|
||||
function symbolKeyLines(
|
||||
host: LlvmEmitterContext,
|
||||
symbol: NonNullable<IrClassDef["symbolFields"]>[number],
|
||||
name: string,
|
||||
): { load: string[]; release: string[] } {
|
||||
if ("globalId" in symbol)
|
||||
return { load: [` %${name} = load ptr, ptr @${mangleGlobal(symbol.globalId)}`], release: [] };
|
||||
host.declare("declare ptr @scr_sym_well_known(ptr)");
|
||||
host.declare("declare void @scr_sym_release(ptr)");
|
||||
return {
|
||||
load: [
|
||||
` %${name} = call ptr @scr_sym_well_known(ptr ${host.internLiteral(symbol.wellKnown)})`,
|
||||
],
|
||||
release: [` call void @scr_sym_release(ptr %${name})`],
|
||||
};
|
||||
}
|
||||
|
||||
export function streamTypedRefBoxValue(
|
||||
host: LlvmEmitterContext,
|
||||
B: BlockBuilder,
|
||||
@@ -924,8 +948,10 @@ export function streamTypedRefMaterializeAdapter(
|
||||
B.line(`${fieldValue} = sitofp i32 ${raw} to double`);
|
||||
} else if (inline) fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true);
|
||||
else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`);
|
||||
B.line(`${rawKey} = load ptr, ptr @${mangleGlobal(symbol.globalId)}`);
|
||||
const keyLines = symbolKeyLines(host, symbol, rawKey.slice(1));
|
||||
for (const line of keyLines.load) B.line(line.trim());
|
||||
const key = host.streamTypedRefBoxValue(B, SYMBOL_T, rawKey);
|
||||
for (const line of keyLines.release) B.line(line.trim());
|
||||
const afterSymbol =
|
||||
meta.def.tracksOwnFields || field.type.kind === "symbol"
|
||||
? B.newLabel("symbol.after")
|
||||
@@ -1490,9 +1516,26 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM
|
||||
// The constructor's name, as util.inspect prints before the fields.
|
||||
const jsName = meta.def.jsName ?? meta.def.name.replace(/^%/, "");
|
||||
const className = jsName === "" ? "null" : host.internLiteral(jsName);
|
||||
// Its Symbol.toStringTag getter's string, for Object.prototype.toString
|
||||
// and util.inspect.
|
||||
let stringTag = "null";
|
||||
if (meta.def.stringTagHelper) {
|
||||
// The runtime lends the instance; the helper takes its own reference.
|
||||
stringTag = `@${snapshot}_ctag`;
|
||||
const retain = vAdapters(host.shapeHost, { kind: "object", className: meta.def.name }).retain;
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal ptr ${stringTag}(ptr %p) ${FN_ATTRS} { ; Symbol.toStringTag of ${meta.def.name}`,
|
||||
`entry:`,
|
||||
` %owned = call ptr ${retain}(ptr %p)`,
|
||||
` %tag = call ptr @${mangleFunction(meta.def.stringTagHelper)}(ptr %owned)`,
|
||||
` ret ptr %tag`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
host.resolveThunkDefs.push(
|
||||
`@${names} = internal constant [${members.length} x ptr] [${literals.join(", ")}]`,
|
||||
`@${ops} = internal constant { ptr, ${host.sizeType}, ptr, ptr } { ptr ${exact}, ${host.sizeType} ${members.length}, ptr @${names}, ptr ${className} }`,
|
||||
`@${ops} = internal constant { ptr, ${host.sizeType}, ptr, ptr, ptr } { ptr ${exact}, ${host.sizeType} ${members.length}, ptr @${names}, ptr ${className}, ptr ${stringTag} }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { primitiveConstructorOr } from "./lower-exprs.js";
|
||||
import { TO_STRING_TAG_FIELD, wellKnownSymbolOfField } from "./symbol-fields.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js";
|
||||
import { constructedValue } from "./constructor-returns.js";
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
type IrClassDef,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrParam,
|
||||
@@ -153,6 +155,41 @@ const STREAM_NUM_PROPERTIES = new Set([
|
||||
const streamProperty = (name: string): boolean =>
|
||||
STREAM_BOOL_PROPERTIES.has(name) || STREAM_NUM_PROPERTIES.has(name);
|
||||
|
||||
/** `(instance) => string`, the string a class's inherited
|
||||
* Symbol.toStringTag getter answers, which the runtime reads for
|
||||
* Object.prototype.toString and util.inspect of an instance seen as
|
||||
* `unknown`. None for a class without a string getter, or whose own tag
|
||||
* field shadows it. */
|
||||
function stringTagHelper(lowerer: Lowerer, info: ClassInfo, loc: SrcLoc): IrFunction | null {
|
||||
if (info.fields.has(TO_STRING_TAG_FIELD)) return null;
|
||||
const getter = findMethodOn(lowerer, info, "get:sym:toStringTag");
|
||||
if (getter?.sig.ret.kind !== "string") return null;
|
||||
const type: IrType = { kind: "object", className: info.def.name };
|
||||
const owner = getter.declarer.def.name;
|
||||
return {
|
||||
name: `%class.stringTag:${info.def.name}`,
|
||||
params: [{ localId: "this", name: "this", type }],
|
||||
locals: [{ id: "this", name: "this", type, mutable: false }],
|
||||
returnType: STRING,
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: accessorCall(
|
||||
lowerer,
|
||||
owner,
|
||||
"get:sym:toStringTag",
|
||||
lowerer.upcastTo(varRef("this", type, loc), owner),
|
||||
[],
|
||||
STRING,
|
||||
loc,
|
||||
),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function symbolMemberKey(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
@@ -1375,10 +1412,27 @@ export class ClassDynamicDispatch {
|
||||
const info = lowerer.classes.get(className);
|
||||
if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) continue;
|
||||
if (info.symbolFields && info.def.symbolFields === undefined) {
|
||||
info.def.symbolFields = [...info.symbolFields].flatMap(([symbol, field]) => {
|
||||
const global = lowerer.globalsBySymbol.get(symbol);
|
||||
return global?.type.kind === "symbol" ? [{ field, globalId: global.id }] : [];
|
||||
});
|
||||
info.def.symbolFields = [...info.symbolFields].flatMap(
|
||||
([symbol, field]): NonNullable<IrClassDef["symbolFields"]> => {
|
||||
const global = lowerer.globalsBySymbol.get(symbol);
|
||||
if (global?.type.kind === "symbol") return [{ field, globalId: global.id }];
|
||||
const wellKnown = wellKnownSymbolOfField(field);
|
||||
return wellKnown === null ? [] : [{ field, wellKnown }];
|
||||
},
|
||||
);
|
||||
}
|
||||
const tagged = (current: ClassInfo): ClassInfo[] => [
|
||||
current,
|
||||
...current.subclasses.flatMap(tagged),
|
||||
];
|
||||
for (const current of tagged(info)) {
|
||||
if (current.def.stringTagHelper !== undefined) continue;
|
||||
const fn = stringTagHelper(lowerer, current, functions[0]!.loc);
|
||||
if (!fn) continue;
|
||||
current.def.stringTagHelper = fn.name;
|
||||
this.generated.add(fn);
|
||||
lowerer.liftedFns.push(fn);
|
||||
changed = true;
|
||||
}
|
||||
if (this.prototypeHelper && !this.prototypeClasses.has(className)) {
|
||||
this.prototypeClasses.add(className);
|
||||
@@ -3662,10 +3716,29 @@ export class ClassDynamicDispatch {
|
||||
name,
|
||||
lowerer.coerceToExpected(varRef(global.id, global.type, loc), DYN),
|
||||
);
|
||||
} else if (wellKnownSymbolOfField(name) !== null) {
|
||||
// A field keyed by a well-known symbol (Symbol.toStringTag).
|
||||
symbols.set(name, {
|
||||
kind: "dynFrom",
|
||||
type: DYN,
|
||||
loc,
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "sym.wellKnown",
|
||||
args: [
|
||||
{ kind: "strLit", value: wellKnownSymbolOfField(name)!, type: STRING, loc },
|
||||
],
|
||||
type: SYMBOL_T,
|
||||
loc,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
for (let owner: ClassInfo | null = info; owner; owner = owner.base) {
|
||||
for (const name of [...owner.methods.keys()])
|
||||
for (const method of [...owner.methods.keys()]) {
|
||||
// Accessors key by the property they define (`get [Symbol.toStringTag]`).
|
||||
const name =
|
||||
method.startsWith("get:") || method.startsWith("set:") ? method.slice(4) : method;
|
||||
if (name.startsWith("sym:")) {
|
||||
symbols.set(name, {
|
||||
kind: "dynFrom",
|
||||
@@ -3680,6 +3753,7 @@ export class ClassDynamicDispatch {
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const branches: IrStmt[] = [];
|
||||
if (!dispatch.write && !dispatch.reflect && !info.def.tracksOwnFields) {
|
||||
|
||||
@@ -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);
|
||||
@@ -426,7 +456,19 @@ export function collectClassMethodMutations(
|
||||
(ts.isElementAccessExpression(base) &&
|
||||
ts.isStringLiteralLike(base.argumentExpression) &&
|
||||
base.argumentExpression.text === "prototype");
|
||||
if (prototype) {
|
||||
// `Object.prototype.toString.call(value)` calls the builtin and
|
||||
// keeps no reference to it, so it replaces nothing.
|
||||
const builtinCall =
|
||||
ts.isPropertyAccessExpression(node) &&
|
||||
ts.isPropertyAccessExpression(base) &&
|
||||
ts.isIdentifier(unwrap(base.expression)) &&
|
||||
lowerer.isStdlibGlobal(unwrap(base.expression) as ts.Identifier, "Object") &&
|
||||
ts.isPropertyAccessExpression(node.parent) &&
|
||||
node.parent.expression === node &&
|
||||
(node.parent.name.text === "call" || node.parent.name.text === "apply") &&
|
||||
ts.isCallExpression(node.parent.parent) &&
|
||||
node.parent.parent.expression === node.parent;
|
||||
if (prototype && !builtinCall) {
|
||||
if (ts.isPropertyAccessExpression(node)) lowerer.noteMethodAccess(node.name.text, node);
|
||||
else if (ts.isStringLiteralLike(node.argumentExpression))
|
||||
lowerer.noteMethodAccess(node.argumentExpression.text, node);
|
||||
@@ -611,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;
|
||||
@@ -628,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;
|
||||
@@ -650,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);
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
import { isClassCallback } from "./class-callbacks.js";
|
||||
import { TO_STRING_TAG_FIELD } from "./symbol-fields.js";
|
||||
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
||||
import { DATE_TIME_FIELD, dateRootOf } from "./builtins/date-classes.js";
|
||||
import { dateValueMethod } from "./builtins/dates.js";
|
||||
@@ -29,7 +30,15 @@ type ConversionMethod = "sym:toPrimitive" | "valueOf" | "toString";
|
||||
* Date`, or through Object.prototype.toString ("[object Object]"). */
|
||||
type Plan =
|
||||
| { method: ConversionMethod; ret: IrType }
|
||||
| { method: null; date?: "toString" | "valueOf" };
|
||||
| { method: null; date?: "toString" | "valueOf"; tag?: ToStringTag };
|
||||
|
||||
/** Where Object.prototype.toString finds a class's Symbol.toStringTag: a
|
||||
* string field (`[Symbol.toStringTag] = "LRUCache"`) or a getter that
|
||||
* returns a string, declared on `owner`. */
|
||||
interface ToStringTag {
|
||||
owner: ClassInfo;
|
||||
getter: boolean;
|
||||
}
|
||||
|
||||
const PRIMITIVE_KINDS = new Set(["f64", "string", "bool", "bigint", "undefinedT", "nullT"]);
|
||||
|
||||
@@ -52,6 +61,17 @@ export function classToStringConversion(
|
||||
);
|
||||
}
|
||||
|
||||
/** Object.prototype.toString of a program class instance: "[object " +
|
||||
* its Symbol.toStringTag + "]" (an own tag field, else an inherited
|
||||
* getter), or "[object Object]". The class's own conversion methods do not
|
||||
* take part. Null as for classToStringConversion, and for Date subclasses. */
|
||||
export function classObjectTag(lowerer: Lowerer, value: IrExpr, node: ts.Node): IrExpr | null {
|
||||
if (value.type.kind !== "object") return null;
|
||||
const info = lowerer.classes.get(value.type.className);
|
||||
if (!info || dateRootOf(info) !== null) return null;
|
||||
return classConversion(lowerer, value, "tag", node, STRING, (tag) => tag);
|
||||
}
|
||||
|
||||
/** ToNumber(ToPrimitive(value, "number")) of a program class instance
|
||||
* (`Number(x)`): `[Symbol.toPrimitive]("number")`, then `valueOf` and then
|
||||
* `toString`, and NaN for "[object Object]". Null as for
|
||||
@@ -122,7 +142,7 @@ function mayBeBigint(lowerer: Lowerer, type: IrType): boolean {
|
||||
function classConversion(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
hint: ConversionHint,
|
||||
hint: ConversionHint | "tag",
|
||||
node: ts.Node,
|
||||
resultType: IrType,
|
||||
finish: (primitive: IrExpr) => IrExpr,
|
||||
@@ -131,7 +151,7 @@ function classConversion(
|
||||
const info = lowerer.classes.get(value.type.className);
|
||||
if (!info || !plainProgramClass(info)) return null;
|
||||
const classes = subtree(lowerer, info);
|
||||
for (const cls of classes) refuseReplacedConversions(lowerer, cls, node);
|
||||
if (hint !== "tag") for (const cls of classes) refuseReplacedConversions(lowerer, cls, node);
|
||||
const plans = new Map(classes.map((cls) => [cls, planOf(lowerer, cls, hint, node)]));
|
||||
// A class whose plan differs from its base's starts a group; the deepest
|
||||
// groups are tested first, so each instance takes its own class's plan.
|
||||
@@ -164,12 +184,52 @@ function classConversion(
|
||||
};
|
||||
return finish(dateValueMethod(plan.date, time, loc)!);
|
||||
}
|
||||
if (plan.method === null && plan.tag) {
|
||||
// Object.prototype.toString: "[object " + the tag + "]".
|
||||
const owner = plan.tag.owner.def.name;
|
||||
const tag: IrExpr = plan.tag.getter
|
||||
? accessorCall(
|
||||
lowerer,
|
||||
owner,
|
||||
"get:sym:toStringTag",
|
||||
lowerer.upcastTo(object, owner),
|
||||
[],
|
||||
STRING,
|
||||
loc,
|
||||
)
|
||||
: {
|
||||
kind: "fieldGet",
|
||||
obj: lowerer.upcastTo(object, owner),
|
||||
className: owner,
|
||||
field: TO_STRING_TAG_FIELD,
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
return finish({
|
||||
kind: "strConcat",
|
||||
left: {
|
||||
kind: "strConcat",
|
||||
left: { kind: "strLit", value: "[object ", type: STRING, loc },
|
||||
right: tag,
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
right: { kind: "strLit", value: "]", type: STRING, loc },
|
||||
type: STRING,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
if (plan.method === null)
|
||||
return finish({ kind: "strLit", value: "[object Object]", type: STRING, loc });
|
||||
const args: IrExpr[] =
|
||||
plan.method === "sym:toPrimitive" ? [{ kind: "strLit", value: hint, type: STRING, loc }] : [];
|
||||
return finish(accessorCall(lowerer, cls.def.name, plan.method, object, args, plan.ret, loc));
|
||||
};
|
||||
// "[object Object]" reads nothing of the instance, but the receiver
|
||||
// expression still runs, once.
|
||||
const own = plans.get(info)!;
|
||||
if (tops.length === 0 && own.method === null && !own.date && !own.tag && !isSafeToRepeat(value))
|
||||
prefix.push({ kind: "exprStmt", expr: value, loc });
|
||||
let result = convert(info, receiver);
|
||||
for (let i = tops.length - 1; i >= 0; i--) {
|
||||
const top = tops[i]!;
|
||||
@@ -228,10 +288,28 @@ function depth(info: ClassInfo): number {
|
||||
}
|
||||
|
||||
function samePlan(a: Plan, b: Plan): boolean {
|
||||
return (
|
||||
a.method === b.method &&
|
||||
(a.method !== null || b.method !== null || a.date === (b as { date?: string }).date)
|
||||
);
|
||||
if (a.method !== null || b.method !== null) return a.method === b.method;
|
||||
return a.date === b.date && a.tag?.owner === b.tag?.owner && a.tag?.getter === b.tag?.getter;
|
||||
}
|
||||
|
||||
/** The class's Symbol.toStringTag, when it has one: the class at the top
|
||||
* of the chain whose layout holds the tag field (an own property, which
|
||||
* shadows any getter), or its getter. Other tag types would make
|
||||
* Object.prototype.toString answer "Object" or the value depending on the
|
||||
* run, and are refused. */
|
||||
function toStringTagOf(lowerer: Lowerer, info: ClassInfo, node: ts.Node): ToStringTag | null {
|
||||
const type = info.fields.get(TO_STRING_TAG_FIELD);
|
||||
if (type !== undefined) {
|
||||
if (type.kind !== "string") refuse(lowerer, node, info, "a non-string Symbol.toStringTag");
|
||||
let owner = info;
|
||||
while (owner.base?.fields.has(TO_STRING_TAG_FIELD)) owner = owner.base;
|
||||
return { owner, getter: false };
|
||||
}
|
||||
const getter = findMethodOn(lowerer, info, "get:sym:toStringTag");
|
||||
if (!getter) return null;
|
||||
if (getter.sig.ret.kind !== "string")
|
||||
refuse(lowerer, node, info, "a non-string Symbol.toStringTag");
|
||||
return { owner: getter.declarer, getter: true };
|
||||
}
|
||||
|
||||
/** A conversion method the class declares or inherits (a field of that
|
||||
@@ -246,7 +324,16 @@ function declared(
|
||||
return found ? { ret: found.sig.ret } : null;
|
||||
}
|
||||
|
||||
function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: ts.Node): Plan {
|
||||
function planOf(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
hint: ConversionHint | "tag",
|
||||
node: ts.Node,
|
||||
): Plan {
|
||||
if (hint === "tag") {
|
||||
const tag = toStringTagOf(lowerer, info, node);
|
||||
return tag ? { method: null, tag } : { method: null };
|
||||
}
|
||||
const exotic = declared(lowerer, info, "sym:toPrimitive");
|
||||
if (exotic) {
|
||||
if (!primitive(lowerer, exotic.ret))
|
||||
@@ -271,7 +358,8 @@ function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: t
|
||||
refuse(lowerer, node, info, `a '${method}' method that can return an object`);
|
||||
return { method, ret: found.ret };
|
||||
}
|
||||
return { method: null };
|
||||
const tag = toStringTagOf(lowerer, info, node);
|
||||
return tag ? { method: null, tag } : { method: null };
|
||||
}
|
||||
|
||||
function primitive(lowerer: Lowerer, type: IrType): boolean {
|
||||
@@ -292,8 +380,6 @@ function refuseReplacedConversions(lowerer: Lowerer, info: ClassInfo, node: ts.N
|
||||
if (lowerer.prototypeMethodAccesses.has(method))
|
||||
refuse(lowerer, node, info, `a '${method}' the program reaches through a prototype`);
|
||||
}
|
||||
if (findMethodOn(lowerer, info, "get:sym:toStringTag") || info.fields.has("sym:toStringTag"))
|
||||
refuse(lowerer, node, info, "a Symbol.toStringTag");
|
||||
}
|
||||
|
||||
function refuse(lowerer: Lowerer, node: ts.Node, info: ClassInfo, what: string): never {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -707,11 +707,17 @@ function rewriteFunction(
|
||||
* unchecked. */
|
||||
export function refuseUncheckedCycleReads(
|
||||
lowerer: Lowerer,
|
||||
classes: readonly { baseValueGlobal?: string; symbolFields?: { globalId: string }[] }[],
|
||||
classes: readonly {
|
||||
baseValueGlobal?: string;
|
||||
symbolFields?: readonly ({ globalId: string } | { wellKnown: string })[];
|
||||
}[],
|
||||
): void {
|
||||
if (lowerer.cycleInitFlags.size === 0) return;
|
||||
for (const cls of classes) {
|
||||
const ids = [cls.baseValueGlobal, ...(cls.symbolFields ?? []).map((f) => f.globalId)];
|
||||
const ids = [
|
||||
cls.baseValueGlobal,
|
||||
...(cls.symbolFields ?? []).map((f) => ("globalId" in f ? f.globalId : undefined)),
|
||||
];
|
||||
for (const id of ids) {
|
||||
const flag = id === undefined ? undefined : lowerer.cycleInitFlags.get(id);
|
||||
if (flag === undefined) continue;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -187,7 +187,7 @@ import {
|
||||
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerChildMethodValue, lowerChildPropertyCall } from "./handle-properties.js";
|
||||
import { classToNumberConversion } from "./class-to-primitive.js";
|
||||
import { classObjectTag, classToNumberConversion } from "./class-to-primitive.js";
|
||||
import { hasRuntimeStatics } from "./class-runtime-statics.js";
|
||||
import { structuralReadType } from "./class-unions.js";
|
||||
import {
|
||||
@@ -3116,6 +3116,10 @@ function lowerObjectPrototypeCall(
|
||||
const immediate = immediateObjectTagReceiver(lowerer, receiverNode);
|
||||
const receiver = immediate?.effect ?? lowerer.lowerExpr(receiverNode);
|
||||
const tag = immediate?.tag ?? objectToStringTag(lowerer, receiver.type);
|
||||
if (tag === null && immediate === null && call.arguments.nodes.length === 1) {
|
||||
const classTag = classObjectTag(lowerer, receiver, receiverNode);
|
||||
if (classTag) return classTag;
|
||||
}
|
||||
// A generator object answers its tag through its checked capsule.
|
||||
if (tag === null && receiver.type.kind !== "dyn" && receiver.type.kind !== "generator") {
|
||||
lowerer.noLowering(
|
||||
|
||||
@@ -8161,7 +8161,10 @@ export function classMemberNameOf(lowerer: Lowerer, name: ts.PropertyName): stri
|
||||
member === "asyncIterator" ||
|
||||
member === "dispose" ||
|
||||
member === "asyncDispose" ||
|
||||
member === "toPrimitive"
|
||||
member === "toPrimitive" ||
|
||||
// A toStringTag field keeps its symbol field name; its accessor and
|
||||
// method forms take the method slot name.
|
||||
(member === "toStringTag" && !ts.isPropertyDeclaration(name.parent))
|
||||
) {
|
||||
return `sym:${member}`;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { jsFuncNameOf, type Lowerer } from "./lowerer.js";
|
||||
import { isJsSourceFile } from "../program.js";
|
||||
import {
|
||||
BOOL,
|
||||
CAUGHT,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
@@ -44,9 +45,9 @@ import {
|
||||
shapeKeyOrders,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
||||
import { symbolFieldDisplayName } from "./symbol-fields.js";
|
||||
import { symbolFieldDisplayName, TO_STRING_TAG_FIELD } from "./symbol-fields.js";
|
||||
import { eventTargetRootOf } from "./lower-event-target.js";
|
||||
import { dateRootOf } from "./builtins/date-classes.js";
|
||||
import { lowerSystemErrorMap } from "./lower-system-errors.js";
|
||||
@@ -2000,8 +2001,86 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
|
||||
// observable way — Node's inspect omits them entirely (verified),
|
||||
// so a class whose only fields are private prints as `C {}`.
|
||||
const visible = info.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name));
|
||||
// A Symbol.toStringTag getter (an own tag field is listed instead)
|
||||
// brackets the tag after the name, `Name [tag] {`, when it is a
|
||||
// nonempty string other than the name. A getter that throws shows
|
||||
// no tag, as in Node.
|
||||
const tagGetter = info.fields.has(TO_STRING_TAG_FIELD)
|
||||
? null
|
||||
: findMethodOn(lowerer, info, "get:sym:toStringTag");
|
||||
let named: (plain: string, make: (head: IrExpr) => IrExpr) => IrExpr = (plain) =>
|
||||
strLit(plain, loc);
|
||||
if (tagGetter?.sig.ret.kind === "string") {
|
||||
const owner = tagGetter.declarer.def.name;
|
||||
locals.push(
|
||||
{ id: "tag.0", name: "tag", type: STRING, mutable: true },
|
||||
{ id: "tagError.0", name: "tagError", type: CAUGHT, mutable: false },
|
||||
);
|
||||
body.push(
|
||||
{ kind: "varDecl", localId: "tag.0", init: strLit("", loc), loc },
|
||||
{
|
||||
kind: "tryCatch",
|
||||
tryBody: [
|
||||
{
|
||||
kind: "assign",
|
||||
localId: "tag.0",
|
||||
value: accessorCall(
|
||||
lowerer,
|
||||
owner,
|
||||
"get:sym:toStringTag",
|
||||
lowerer.upcastTo(v(), owner),
|
||||
[],
|
||||
STRING,
|
||||
loc,
|
||||
),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
catchBody: [],
|
||||
catchLocalId: "tagError.0",
|
||||
finallyBody: null,
|
||||
loc,
|
||||
},
|
||||
);
|
||||
const tag = (): IrExpr => varRef("tag.0", STRING, loc);
|
||||
const shown: IrExpr = {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: {
|
||||
kind: "strEq",
|
||||
negated: true,
|
||||
left: tag(),
|
||||
right: strLit("", loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
right: {
|
||||
kind: "strEq",
|
||||
negated: true,
|
||||
left: tag(),
|
||||
right: strLit(display, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
const head = concatAll(
|
||||
[strLit(`${display === "" ? "Object" : display} [`, loc), tag(), strLit("]", loc)],
|
||||
loc,
|
||||
);
|
||||
named = (plain, make) => ({
|
||||
kind: "ternary",
|
||||
cond: shown,
|
||||
then: make(head),
|
||||
else_: strLit(plain, loc),
|
||||
type: STRING,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const after = (text: string) => (head: IrExpr) => concatAll([head, strLit(text, loc)], loc);
|
||||
if (visible.length === 0) {
|
||||
body = [ret(strLit(display === "" ? "{}" : `${display} {}`, loc))];
|
||||
body.push(ret(named(display === "" ? "{}" : `${display} {}`, after(" {}"))));
|
||||
break;
|
||||
}
|
||||
const get = (field: string, type: IrType): IrExpr => ({
|
||||
@@ -2012,7 +2091,22 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
|
||||
type,
|
||||
loc,
|
||||
});
|
||||
body = [depthGate(display === "" ? "[Object]" : `[${display}]`), ...begin()];
|
||||
body.push(
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "bin", op: ">", left: r(), right: d(), type: BOOL, loc },
|
||||
then: [
|
||||
ret(
|
||||
named(display === "" ? "[Object]" : `[${display}]`, (head) =>
|
||||
concatAll([strLit("[", loc), head, strLit("]", loc)], loc),
|
||||
),
|
||||
),
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
...begin(),
|
||||
);
|
||||
// def.fields carries layout order: the base chain first, then own —
|
||||
// exactly the own-property insertion order of a constructor that
|
||||
// assigns in declaration order (SEMANTICS.md 36's stance). SYMBOL-
|
||||
@@ -2041,7 +2135,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
|
||||
ret(
|
||||
end(
|
||||
strLit("", loc),
|
||||
strLit(display === "" ? "{" : `${display} {`, loc),
|
||||
named(display === "" ? "{" : `${display} {`, after(" {")),
|
||||
strLit("}", loc),
|
||||
false,
|
||||
boolLit(false, loc),
|
||||
|
||||
@@ -1183,19 +1183,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.
|
||||
|
||||
@@ -3756,7 +3756,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,
|
||||
@@ -3837,8 +3837,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) =>
|
||||
@@ -8102,16 +8102,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));
|
||||
@@ -8288,7 +8296,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
|
||||
@@ -8423,7 +8432,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) {
|
||||
@@ -8541,7 +8551,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
|
||||
@@ -11941,12 +11953,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)
|
||||
@@ -11970,8 +11985,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 {
|
||||
|
||||
@@ -77,6 +77,8 @@ export function fenceSymbolFieldCopy(
|
||||
}
|
||||
|
||||
export function classSymbolKeyOf(lowerer: Lowerer, key: ts.Expression): ClassSymbolKey | null {
|
||||
const wellKnown = wellKnownSymbolKeyOf(lowerer, key);
|
||||
if (wellKnown !== undefined) return wellKnown;
|
||||
if (
|
||||
!ts.isIdentifier(key) &&
|
||||
(!ts.isPropertyAccessExpression(key) || key.questionDotToken || !ts.isIdentifier(key.name))
|
||||
@@ -90,6 +92,53 @@ export function classSymbolKeyOf(lowerer: Lowerer, key: ts.Expression): ClassSym
|
||||
return sym ? classSymbolKeyOfSymbol(lowerer, sym) : null;
|
||||
}
|
||||
|
||||
/** Well-known symbols a class field may be keyed by: those no protocol
|
||||
* consults on an instance, or whose protocol the lowering reads from the
|
||||
* field (Symbol.toStringTag, by the class string conversions). A field
|
||||
* keyed by any other well-known symbol (Symbol.iterator, toPrimitive,
|
||||
* hasInstance, ...) would be skipped by its protocol, so it keeps the
|
||||
* computed-key refusal. */
|
||||
const FIELD_WELL_KNOWN_SYMBOLS: ReadonlySet<string> = new Set(["toStringTag"]);
|
||||
|
||||
/** The key of `Symbol.<name>` on the standard global: undefined when the
|
||||
* expression is not one, null when it is a well-known symbol a field may
|
||||
* not be keyed by. The identity is the lib's declaration of the property,
|
||||
* one symbol for every program reference. */
|
||||
function wellKnownSymbolKeyOf(
|
||||
lowerer: Lowerer,
|
||||
key: ts.Expression,
|
||||
): ClassSymbolKey | null | undefined {
|
||||
while (ts.isParenthesizedExpression(key)) key = key.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(key) ||
|
||||
key.questionDotToken ||
|
||||
!ts.isIdentifier(key.expression) ||
|
||||
!ts.isIdentifier(key.name) ||
|
||||
!lowerer.isStdlibGlobal(key.expression, "Symbol")
|
||||
)
|
||||
return undefined;
|
||||
const name = key.name.text;
|
||||
const sym = lowerer.checker.getSymbolAtLocation(key.name);
|
||||
if (!sym || !(lowerer.typeOf(key).flags & ts.TypeFlags.UniqueESSymbol)) return undefined;
|
||||
if (!FIELD_WELL_KNOWN_SYMBOLS.has(name)) return null;
|
||||
return { sym, identity: sym, fieldName: wellKnownSymbolFieldName(name) };
|
||||
}
|
||||
|
||||
/** The layout name of a field keyed by the well-known `Symbol.<name>`: a
|
||||
* reserved symbol-field name, printed as Node spells the key. */
|
||||
export function wellKnownSymbolFieldName(name: string): string {
|
||||
return `${FIELD_PREFIX}Symbol(Symbol.${name})`;
|
||||
}
|
||||
|
||||
/** The well-known symbol a field name is keyed by (`toStringTag`), or null. */
|
||||
export function wellKnownSymbolOfField(field: string): string | null {
|
||||
const match = /^%symbol:Symbol\(Symbol\.(\w+)\)$/.exec(field);
|
||||
return match === null ? null : match[1]!;
|
||||
}
|
||||
|
||||
/** The layout name of a `Symbol.toStringTag` field. */
|
||||
export const TO_STRING_TAG_FIELD = wellKnownSymbolFieldName("toStringTag");
|
||||
|
||||
/** A literal Symbol()/Symbol.for() initialized once at module scope. The
|
||||
* whole-file proof also covers var declarations emitted by bundlers. Keys
|
||||
* cannot be used before initialization: a computed class field evaluates
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3927,10 +3927,14 @@ function exactCycleRefusal7(
|
||||
if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
||||
return `the default export expression at ${lineOf(stmt)} has no initialization state inside the cycle`;
|
||||
}
|
||||
// `export * from` binds each name to the declaring member's own
|
||||
// binding, like a named re-export, so only `export * as ns` (a
|
||||
// namespace object of a member) has state of its own.
|
||||
if (
|
||||
ts.isExportDeclaration(stmt) &&
|
||||
!stmt.isTypeOnly &&
|
||||
(stmt.exportClause === undefined || ts.isNamespaceExport(stmt.exportClause))
|
||||
stmt.exportClause !== undefined &&
|
||||
ts.isNamespaceExport(stmt.exportClause)
|
||||
) {
|
||||
const dep = targetOf(stmt);
|
||||
if (dep !== null && members.has(dep)) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -157,10 +157,12 @@ const MODULE_FIELDS: Record<StringFields<Omit<IrModule, "functions">>, Role> = {
|
||||
"classes[].errorNameAfter": "other",
|
||||
"classes[].prototypeDataHelper": "reference",
|
||||
"classes[].instancePrototypeHelper": "reference",
|
||||
"classes[].stringTagHelper": "reference",
|
||||
"classes[].localCaptures[].name": "other",
|
||||
"classes[].localCaptures[].localId": "other",
|
||||
"classes[].symbolFields[].field": "other",
|
||||
"classes[].symbolFields[].globalId": "other",
|
||||
"classes[].symbolFields[].wellKnown": "other",
|
||||
"classes[].fields[].name": "other",
|
||||
"classes[].methods[]": "method",
|
||||
"classes[].prototypeNames[]": "other",
|
||||
@@ -281,6 +283,10 @@ const REFERENCE_FIXTURES: Record<string, () => IrModule> = {
|
||||
...module(),
|
||||
classes: [{ name: "%m0.Cell", fields: [], instancePrototypeHelper: TARGET, loc }],
|
||||
}),
|
||||
"classes[].stringTagHelper": () => ({
|
||||
...module(),
|
||||
classes: [{ name: "%m0.Cell", fields: [], stringTagHelper: TARGET, loc }],
|
||||
}),
|
||||
"lib.exports[].fnName": () => ({
|
||||
...module(),
|
||||
lib: {
|
||||
|
||||
@@ -24,6 +24,7 @@ export function moduleFunctionReferences(mod: IrModule): string[] {
|
||||
for (const cls of mod.classes ?? []) {
|
||||
if (cls.prototypeDataHelper !== undefined) names.push(cls.prototypeDataHelper);
|
||||
if (cls.instancePrototypeHelper !== undefined) names.push(cls.instancePrototypeHelper);
|
||||
if (cls.stringTagHelper !== undefined) names.push(cls.stringTagHelper);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
@@ -1494,6 +1494,9 @@ export interface IrClassDef {
|
||||
prototypeDataHelper?: string;
|
||||
/** Native instance to its actual prototype, including factory captures and subclasses. */
|
||||
instancePrototypeHelper?: string;
|
||||
/** Native instance to the string its inherited Symbol.toStringTag getter
|
||||
* answers, read by the runtime for an instance seen as `unknown`. */
|
||||
stringTagHelper?: string;
|
||||
/** Public JavaScript fields record their creation in the shared property
|
||||
* bag. Its descriptors preserve presence and insertion order independently
|
||||
* of native layout order; field values remain in their native slots. */
|
||||
@@ -1507,8 +1510,10 @@ export interface IrClassDef {
|
||||
* dynamic write keeps `name` and `message` where the bag recorded them
|
||||
* (scr_error_dyn_follow_bag). */
|
||||
errorBagOrder?: boolean;
|
||||
/** Stable module symbol identities for public instance layout fields. */
|
||||
symbolFields?: { field: string; globalId: string }[];
|
||||
/** Stable symbol identities for public instance layout fields: the
|
||||
* module global holding the key, or the name of the well-known symbol
|
||||
* (`toStringTag` for `Symbol.toStringTag`). */
|
||||
symbolFields?: ({ field: string } & ({ globalId: string } | { wellKnown: string }))[];
|
||||
/** RUNTIME-PROVIDED class (the builtin Error hierarchy): the struct, RC
|
||||
* helpers, and vtable live in the runtime (ScrError / scr_error_*), so
|
||||
* backends emit no definitions for it — only the preorder-interval
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -502,10 +502,29 @@ export function validateModule(mod: IrModule): IrValidationError[] {
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const symbol of cls.symbolFields ?? []) {
|
||||
const global = mod.globals?.find((global) => global.id === symbol.globalId);
|
||||
if (cls.stringTagHelper !== undefined) {
|
||||
const helper = functionsByName.get(cls.stringTagHelper);
|
||||
const receiver = helper?.params[0]?.type;
|
||||
if (
|
||||
global?.type.kind !== "symbol" ||
|
||||
!helper ||
|
||||
helper.params.length !== 1 ||
|
||||
receiver?.kind !== "object" ||
|
||||
receiver.className !== cls.name ||
|
||||
helper.returnType.kind !== "string" ||
|
||||
helper.captures !== undefined ||
|
||||
helper.classCaptures !== undefined
|
||||
) {
|
||||
errors.push({
|
||||
message: `class ${cls.name}: string tag helper must be a noncapturing (instance) => string function`,
|
||||
loc: cls.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const symbol of cls.symbolFields ?? []) {
|
||||
const global =
|
||||
"globalId" in symbol ? mod.globals?.find((global) => global.id === symbol.globalId) : null;
|
||||
if (
|
||||
("globalId" in symbol ? global?.type.kind !== "symbol" : symbol.wellKnown === "") ||
|
||||
!cls.fields.some((field) => field.name === symbol.field)
|
||||
) {
|
||||
errors.push({ message: `class ${cls.name}: invalid symbol field metadata`, loc: cls.loc });
|
||||
|
||||
@@ -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/7660-pointer-union-forwarded-arguments.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7663-pointer-union-stored-payload.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7751-class-well-known-symbol-fields.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8105-url-idna-hosts.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8213-cycle-star-reexport/services.ts",
|
||||
"<repo>/tests/corpus/8213-cycle-star-reexport/runtime.ts",
|
||||
"<repo>/tests/corpus/8213-cycle-star-reexport/sdk.ts",
|
||||
"<repo>/tests/corpus/8213-cycle-star-reexport/main.ts"
|
||||
],
|
||||
"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",
|
||||
|
||||
@@ -398,6 +398,10 @@ typedef struct ScrDynTypedClassOps {
|
||||
size_t count;
|
||||
ScrStr *const *names; /* immortal member-name strings */
|
||||
ScrStr *class_name; /* immortal constructor name; NULL for an anonymous class */
|
||||
/* The string an inherited Symbol.toStringTag getter answers for the
|
||||
* instance (+1), or NULL with a pending exception from the getter;
|
||||
* NULL when the class has no such getter. */
|
||||
ScrStr *(*string_tag)(void *instance);
|
||||
} ScrDynTypedClassOps;
|
||||
|
||||
/* A compiled JavaScript class whose instances record their own properties
|
||||
@@ -677,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
|
||||
@@ -921,6 +933,7 @@ void scr_dyn_install_iterator_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_has_instance_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_async_iterator_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_async_dispose_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_to_string_tag_symbol(ScrDyn *key);
|
||||
/* True when key is Symbol.asyncDispose (registered by the symbol unit when
|
||||
* the program names it); handle symbol_get ops outside that unit use it. */
|
||||
bool scr_dyn_is_async_dispose_symbol(const ScrDyn *key);
|
||||
@@ -1084,6 +1097,9 @@ ScrDyn *scr_record_source_apply(ScrDyn *snapshot, const void *rec);
|
||||
/* Attach the prototype member names of a class instance capsule
|
||||
* (idempotent), so `in` sees its methods and accessors. */
|
||||
void scr_dyn_typed_ref_bind_class(ScrDyn *d, const ScrDynTypedClassOps *ops);
|
||||
/* A class instance capsule's inherited Symbol.toStringTag string from its
|
||||
* class's getter (+1); NULL when it has none or the getter threw. */
|
||||
ScrStr *scr_dyn_typed_ref_class_tag(const ScrDyn *d);
|
||||
/* Attach the bag and prototype hooks of a capsule of a bag-tracked class
|
||||
* instance (idempotent). */
|
||||
void scr_dyn_typed_ref_bind_bag_class(ScrDyn *d, const ScrDynTypedBagClassOps *ops);
|
||||
|
||||
@@ -144,6 +144,10 @@ static void scr_dyn_display_buf(ScrJsonBuf *b, const ScrDyn *d, bool to_string)
|
||||
return;
|
||||
|
||||
case SCR_DYN_TYPED_REF: {
|
||||
/* ToString of a class instance that converts through
|
||||
* Object.prototype.toString shows its Symbol.toStringTag getter's
|
||||
* string. */
|
||||
if (to_string && scr_dyn_typed_ref_put_tag(b, d)) return;
|
||||
ScrDyn *materialized = scr_dyn_typed_ref_materialize(d);
|
||||
scr_dyn_display_buf(b, materialized, to_string);
|
||||
scr_dyn_release(materialized);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -1258,6 +1258,20 @@ static bool insp_tag_names_class(const char *tag, const char *cls) {
|
||||
memcmp(tag + tag_len - cls_len, cls, cls_len) == 0;
|
||||
}
|
||||
|
||||
/* The Symbol.toStringTag a class instance inherits from its class's
|
||||
* getter (+1), which Node brackets after the class name when it is a
|
||||
* nonempty string other than the name. An own tag field is listed with
|
||||
* the fields instead. A getter that throws shows no tag, as in Node. */
|
||||
static ScrStr *insp_inherited_tag(const ScrDyn *instance, const ScrStr *name) {
|
||||
ScrStr *tag = scr_dyn_typed_ref_class_tag(instance);
|
||||
if (!tag && scr_exc_pending()) scr_exc_clear();
|
||||
if (tag && (tag->len == 0 || (tag->len == name->len && memcmp(tag->data, name->data, name->len) == 0))) {
|
||||
scr_str_release(tag);
|
||||
return NULL;
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
/* A native Set or Map held as a checked-dynamic value: `Set(n) { a, b }`,
|
||||
* `Map(n) { k => v }`, at most 100 entries like Node's maxArrayLength. */
|
||||
/* Node's formatWeakCollection: the entries of a WeakMap or WeakSet are not
|
||||
@@ -1979,10 +1993,23 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
|
||||
free(cls);
|
||||
} else if (name && materialized->kind == SCR_DYN_OBJ &&
|
||||
scr_dyn_obj_get(materialized, "%error", 6) == NULL) {
|
||||
char *cls = malloc(name->len + 1);
|
||||
ScrStr *tag = insp_inherited_tag(d, name);
|
||||
if (scr_exc_pending()) {
|
||||
scr_dyn_release(materialized);
|
||||
return scr_str_new("", 0);
|
||||
}
|
||||
size_t tag_len = tag ? tag->len + 3 : 0;
|
||||
char *cls = malloc(name->len + tag_len + 1);
|
||||
if (!cls) scr_trap("scriptc: out of memory\n");
|
||||
memcpy(cls, name->data, name->len);
|
||||
cls[name->len] = '\0';
|
||||
if (tag) {
|
||||
/* Node's `Name [tag]` prefix. */
|
||||
memcpy(cls + name->len, " [", 2);
|
||||
memcpy(cls + name->len + 2, tag->data, tag->len);
|
||||
cls[name->len + tag_len - 1] = ']';
|
||||
scr_str_release(tag);
|
||||
}
|
||||
cls[name->len + tag_len] = '\0';
|
||||
size_t nseen = g_nseen;
|
||||
g_pending_seen = instance;
|
||||
out = scr_insp_obj(materialized, recurse, depth, cls);
|
||||
|
||||
@@ -202,6 +202,19 @@ void scr_dyn_install_has_instance_symbol(ScrDyn *key) {
|
||||
scr_atexit(scr_has_instance_symbol_cleanup);
|
||||
}
|
||||
|
||||
/* Symbol.toStringTag, installed when the program first evaluates it: no
|
||||
* object holds the key before then. */
|
||||
static SCR_TL ScrDyn *scr_to_string_tag_symbol;
|
||||
static void scr_to_string_tag_symbol_cleanup(void) {
|
||||
scr_dyn_release(scr_to_string_tag_symbol);
|
||||
scr_to_string_tag_symbol = NULL;
|
||||
}
|
||||
void scr_dyn_install_to_string_tag_symbol(ScrDyn *key) {
|
||||
if (scr_to_string_tag_symbol) return;
|
||||
scr_to_string_tag_symbol = scr_dyn_retain(key);
|
||||
scr_atexit(scr_to_string_tag_symbol_cleanup);
|
||||
}
|
||||
|
||||
void scr_dyn_install_iterator_symbol(ScrDyn *key) {
|
||||
if (scr_iterator_symbol) return;
|
||||
scr_iterator_symbol = scr_dyn_retain(key);
|
||||
@@ -1273,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;
|
||||
@@ -1422,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;
|
||||
@@ -3932,6 +3967,12 @@ static bool scr_dyn_name_in_strs(const ScrStr *key, ScrStr *const *names, size_t
|
||||
return false;
|
||||
}
|
||||
|
||||
ScrStr *scr_dyn_typed_ref_class_tag(const ScrDyn *d) {
|
||||
if (!d || d->kind != SCR_DYN_TYPED_REF || d->prototype) return NULL;
|
||||
const ScrDynTypedClassOps *ops = scr_typed_ref_class_ops(d);
|
||||
return ops && ops->string_tag ? ops->string_tag(d->v.typed_ref.ptr) : NULL;
|
||||
}
|
||||
|
||||
bool scr_dyn_typed_ref_class_has(const ScrDyn *d, const ScrStr *key) {
|
||||
if (!d || d->kind != SCR_DYN_TYPED_REF || d->prototype) return false;
|
||||
const ScrDynTypedClassOps *ops = scr_typed_ref_class_ops(d);
|
||||
@@ -4020,6 +4061,9 @@ ScrDyn *scr_dyn_index_get(const ScrDyn *d, double index) {
|
||||
return scr_dyn_typed_ref_element(d, i);
|
||||
}
|
||||
|
||||
static ScrStr *scr_dyn_typed_ref_conversion_tag(const ScrDyn *d);
|
||||
static ScrStr *scr_dyn_tag_string(const ScrStr *tag);
|
||||
|
||||
ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) {
|
||||
/* An instance of a class declared `extends Date` converts through its
|
||||
* toString, declared or inherited. With an exception pending the view is
|
||||
@@ -4033,6 +4077,16 @@ ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) {
|
||||
scr_str_release(empty);
|
||||
return out;
|
||||
}
|
||||
/* A class instance converting through Object.prototype.toString shows
|
||||
* its Symbol.toStringTag getter's string. */
|
||||
ScrStr *class_tag = scr_dyn_typed_ref_conversion_tag(d);
|
||||
if (class_tag || scr_exc_pending()) {
|
||||
ScrStr *text = class_tag ? scr_dyn_tag_string(class_tag) : scr_str_new("", 0);
|
||||
scr_str_release(class_tag);
|
||||
ScrDyn *out = scr_dyn_new_str(text);
|
||||
scr_str_release(text);
|
||||
return out;
|
||||
}
|
||||
ScrDyn *view = scr_dyn_typed_ref_materialize(d);
|
||||
const char *tag = scr_event_ref_hooks ? scr_event_ref_hooks->string_tag(d) : NULL;
|
||||
if (!tag || view->kind != SCR_DYN_OBJ || scr_dyn_property_owner(view, "toString", 8)) return view;
|
||||
@@ -8321,11 +8375,66 @@ void scr_dyn_throw(ScrDyn *d) {
|
||||
scr_dyn_trace_v, scr_dyn_is_object(d));
|
||||
}
|
||||
|
||||
static ScrStr *scr_dyn_tag_string(const ScrStr *tag) {
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
scr_jb_puts(&b, "[object ");
|
||||
scr_jb_write(&b, tag->data, tag->len);
|
||||
scr_jb_putc(&b, ']');
|
||||
return scr_jb_finish(&b);
|
||||
}
|
||||
|
||||
/* The Symbol.toStringTag getter's string of a class instance whose
|
||||
* ToString is Object.prototype.toString (its class has no toString; the
|
||||
* string hint never reaches valueOf, since that answers a string): +1, or
|
||||
* NULL otherwise or with the getter's exception pending. */
|
||||
static ScrStr *scr_dyn_typed_ref_conversion_tag(const ScrDyn *d) {
|
||||
ScrStr *to_string_key = scr_str_new("toString", 8);
|
||||
bool own_conversion = scr_dyn_typed_ref_class_has(d, to_string_key);
|
||||
scr_str_release(to_string_key);
|
||||
return own_conversion ? NULL : scr_dyn_typed_ref_class_tag(d);
|
||||
}
|
||||
|
||||
bool scr_dyn_typed_ref_put_tag(ScrJsonBuf *b, const ScrDyn *d) {
|
||||
ScrStr *tag = scr_dyn_typed_ref_conversion_tag(d);
|
||||
if (!tag) return scr_exc_pending();
|
||||
scr_jb_puts(b, "[object ");
|
||||
scr_jb_write(b, tag->data, tag->len);
|
||||
scr_jb_putc(b, ']');
|
||||
scr_str_release(tag);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Object.prototype.toString of an object whose own or inherited
|
||||
* @@toStringTag is a string (+1), NULL otherwise or with a pending
|
||||
* exception from its getter. */
|
||||
static ScrStr *scr_dyn_string_tag(const ScrDyn *d) {
|
||||
ScrStr *getter_tag = scr_dyn_typed_ref_class_tag(d);
|
||||
if (getter_tag) {
|
||||
ScrStr *out = scr_dyn_tag_string(getter_tag);
|
||||
scr_str_release(getter_tag);
|
||||
return out;
|
||||
}
|
||||
if (scr_exc_pending() || !scr_to_string_tag_symbol) return NULL;
|
||||
ScrDyn *tag = scr_dyn_symbol_key_get(d, scr_to_string_tag_symbol, true);
|
||||
if (!tag) return NULL;
|
||||
ScrStr *out = tag->kind == SCR_DYN_STR ? scr_dyn_tag_string(tag->v.str) : NULL;
|
||||
scr_dyn_release(tag);
|
||||
return out;
|
||||
}
|
||||
|
||||
ScrStr *scr_dyn_object_tag(const ScrDyn *d) {
|
||||
if (d->kind == SCR_DYN_PROXY) {
|
||||
scr_dyn_proxy_unsupported("Symbol.toStringTag lookup");
|
||||
return NULL;
|
||||
}
|
||||
/* Object.prototype.toString answers a string @@toStringTag over the
|
||||
* builtin tag of a plain object or a class instance. */
|
||||
if ((d->kind == SCR_DYN_OBJ || (d->kind == SCR_DYN_TYPED_REF && !scr_dyn_ref_is_error(d) &&
|
||||
!scr_dyn_generator(d)))) {
|
||||
ScrStr *tag = scr_dyn_string_tag(d);
|
||||
if (tag || scr_exc_pending()) return tag;
|
||||
}
|
||||
if (d->kind == SCR_DYN_TYPED_REF) {
|
||||
if (scr_dyn_ref_is_error(d)) return scr_str_new("[object Error]", 14);
|
||||
if (scr_dyn_generator(d)) {
|
||||
@@ -9668,6 +9777,13 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) {
|
||||
return NULL;
|
||||
}
|
||||
if (d->kind == SCR_DYN_TYPED_REF) {
|
||||
ScrStr *tag = scr_dyn_typed_ref_conversion_tag(d);
|
||||
if (tag) {
|
||||
ScrStr *out = scr_dyn_tag_string(tag);
|
||||
scr_str_release(tag);
|
||||
return out;
|
||||
}
|
||||
if (scr_exc_pending()) return NULL;
|
||||
ScrDyn *materialized = scr_dyn_typed_ref_string_view(d);
|
||||
ScrStr *out = scr_dyn_string_coerce_js(materialized);
|
||||
scr_dyn_release(materialized);
|
||||
@@ -9688,7 +9804,7 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) {
|
||||
scr_error_release(error);
|
||||
return text;
|
||||
}
|
||||
return scr_str_new("[object Object]", 15);
|
||||
return scr_dyn_object_tag(d);
|
||||
}
|
||||
if (!m || m->kind != SCR_DYN_FUNC) { scr_dyn_release(m); continue; }
|
||||
/* OrdinaryToPrimitive performs a method call, not a bare function
|
||||
|
||||
@@ -119,6 +119,11 @@ _Static_assert(offsetof(ScrJsonBuf, data) == 0 && offsetof(ScrJsonBuf, len) == s
|
||||
"LLVM backend expects ScrJsonBuf to start with { data, len, cap }");
|
||||
|
||||
void scr_jb_init(ScrJsonBuf *b);
|
||||
/* String conversion of a class instance whose ToString is
|
||||
* Object.prototype.toString and whose class has a Symbol.toStringTag
|
||||
* getter: appends "[object Tag]" and answers true, also with the getter's
|
||||
* exception pending; false when the ordinary conversion applies. */
|
||||
bool scr_dyn_typed_ref_put_tag(ScrJsonBuf *b, const ScrDyn *d);
|
||||
/* Release a buffer's text without finishing it (error paths). */
|
||||
void scr_jb_abandon(ScrJsonBuf *b);
|
||||
/* Append a borrowed byte span verbatim, including embedded NUL bytes. */
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -90,6 +90,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) {
|
||||
scr_dyn_install_has_instance_symbol(key);
|
||||
scr_dyn_release(key);
|
||||
}
|
||||
if (name->len == 11 && !memcmp(name->data, "toStringTag", 11)) {
|
||||
ScrDyn *key = scr_dyn_new_symbol(symbol);
|
||||
scr_dyn_install_to_string_tag_symbol(key);
|
||||
scr_dyn_release(key);
|
||||
}
|
||||
if (name->len == 13 && !memcmp(name->data, "asyncIterator", 13)) {
|
||||
ScrDyn *key = scr_dyn_new_symbol(symbol);
|
||||
scr_dyn_install_async_iterator_symbol(key);
|
||||
|
||||
@@ -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,127 @@
|
||||
// Records and class instances passed to `T | undefined` and `T | null`
|
||||
// parameters. The callee receives the object pointer itself, so a binding
|
||||
// that the callee only reads keeps its frame storage; callees that keep,
|
||||
// return or compare the argument still see one shared object.
|
||||
interface Vec {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
interface Tagged {
|
||||
name: string;
|
||||
at: Vec;
|
||||
}
|
||||
function vec(x: number, y: number): Vec {
|
||||
return { x, y };
|
||||
}
|
||||
function len(v: Vec | undefined): number {
|
||||
return v === undefined ? -1 : Math.sqrt(v.x * v.x + v.y * v.y);
|
||||
}
|
||||
function sum(a: Vec | undefined, b: Vec): Vec {
|
||||
return a === undefined ? b : { x: a.x + b.x, y: a.y + b.y };
|
||||
}
|
||||
function label(t: Tagged | undefined): string {
|
||||
return t === undefined ? "none" : t.name + "@" + t.at.x + "," + t.at.y;
|
||||
}
|
||||
function depth(v: Vec | undefined, n: number): number {
|
||||
if (v === undefined || n === 0) return 0;
|
||||
return v.x + depth(v, n - 1);
|
||||
}
|
||||
|
||||
const kept: (Vec | undefined)[] = [];
|
||||
function keep(v: Vec | undefined): number {
|
||||
kept.push(v);
|
||||
return kept.length;
|
||||
}
|
||||
function same(a: Vec | undefined, b: Vec): boolean {
|
||||
return a === b;
|
||||
}
|
||||
function orNull(v: Vec | undefined): Vec | undefined {
|
||||
return v !== undefined && v.x < 0 ? undefined : v;
|
||||
}
|
||||
|
||||
class Cell {
|
||||
value: number;
|
||||
next: Cell | null;
|
||||
constructor(value: number, next: Cell | null) {
|
||||
this.value = value;
|
||||
this.next = next;
|
||||
}
|
||||
}
|
||||
function cellTotal(c: Cell | null): number {
|
||||
let total = 0;
|
||||
for (let at = c; at !== null; at = at.next) total += at.value;
|
||||
return total;
|
||||
}
|
||||
function fail(v: Vec | undefined): number {
|
||||
if (v !== undefined && v.x > 100) throw new Error("too far: " + v.x);
|
||||
return v === undefined ? 0 : v.x;
|
||||
}
|
||||
|
||||
function projections(): void {
|
||||
const a = vec(3, 4);
|
||||
const b = sum(a, vec(1, 1));
|
||||
console.log("len", len(a), len(b), len(undefined));
|
||||
const c = sum(sum(a, b), a);
|
||||
console.log("sum", c.x, c.y, sum(undefined, a).x);
|
||||
console.log("depth", depth(a, 5), depth(undefined, 3));
|
||||
let total = 0;
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const p = vec(i, i + 1);
|
||||
total += len(p) + len(sum(p, a));
|
||||
}
|
||||
console.log("loop", total.toFixed(6));
|
||||
}
|
||||
|
||||
function mutated(): void {
|
||||
const p = vec(1, 2);
|
||||
console.log("before", len(p));
|
||||
p.x = 6;
|
||||
p.y = 8;
|
||||
console.log("after", len(p));
|
||||
}
|
||||
|
||||
function strings(): void {
|
||||
const prefix = ["a", "b", "c"].join("-");
|
||||
const t: Tagged = { name: prefix + "!", at: vec(2, 5) };
|
||||
console.log("label", label(t), label(undefined));
|
||||
const u: Tagged = { name: label(t), at: t.at };
|
||||
console.log("nested", label(u));
|
||||
}
|
||||
|
||||
function escapes(): void {
|
||||
const a = vec(7, 8);
|
||||
const count = keep(a);
|
||||
const b = vec(9, 10);
|
||||
console.log("kept", count, kept[0] === a, kept[0]!.x, keep(b), kept[1]!.y);
|
||||
a.x = 70;
|
||||
console.log("shared", kept[0]!.x);
|
||||
const c = vec(1, 1);
|
||||
console.log("same", same(c, c), same(c, vec(1, 1)), same(undefined, c));
|
||||
const d = vec(-1, 2);
|
||||
const e = vec(4, 5);
|
||||
console.log("returned", orNull(d), orNull(e) === e, orNull(e)!.y);
|
||||
}
|
||||
|
||||
function classes(): void {
|
||||
const tail = new Cell(3, null);
|
||||
const list = new Cell(1, new Cell(2, tail));
|
||||
console.log("cells", cellTotal(list), cellTotal(tail), cellTotal(null));
|
||||
}
|
||||
|
||||
function throwing(): void {
|
||||
const near = vec(5, 0);
|
||||
const far = vec(500, 0);
|
||||
try {
|
||||
console.log("near", fail(near));
|
||||
console.log("far", fail(far));
|
||||
} catch (e) {
|
||||
console.log("caught", (e as Error).message, near.x, far.x);
|
||||
}
|
||||
}
|
||||
|
||||
projections();
|
||||
mutated();
|
||||
strings();
|
||||
escapes();
|
||||
classes();
|
||||
throwing();
|
||||
@@ -0,0 +1,83 @@
|
||||
// Records passed to `T | undefined` parameters whose callee keeps the
|
||||
// narrowed record: pushed into a global array (also through a non-null
|
||||
// assertion), kept through `??`, stored in a nullable class field or a record
|
||||
// field, or passed on to a callee that keeps it. Each record must outlive its
|
||||
// caller's frame. A callee that only reads the narrowed record lets it stay
|
||||
// where it is.
|
||||
interface Tagged {
|
||||
id: number;
|
||||
tags: string[];
|
||||
}
|
||||
interface Slot {
|
||||
held: Tagged | undefined;
|
||||
}
|
||||
const kept: Tagged[] = [];
|
||||
const slot: Slot = { held: undefined };
|
||||
function pushNarrowed(t: Tagged | undefined): void {
|
||||
if (t !== undefined) kept.push(t);
|
||||
}
|
||||
function pushAsserted(t: Tagged | undefined): void {
|
||||
kept.push(t!);
|
||||
}
|
||||
function keepDefaulted(t: Tagged | undefined): void {
|
||||
kept.push(t ?? { id: -1, tags: [] });
|
||||
}
|
||||
function storeInSlot(t: Tagged | undefined): void {
|
||||
if (t !== undefined) slot.held = t;
|
||||
}
|
||||
function keepRecord(t: Tagged): void {
|
||||
kept.push(t);
|
||||
}
|
||||
function passOn(t: Tagged | undefined): void {
|
||||
if (t !== undefined) keepRecord(t);
|
||||
}
|
||||
function readOnly(t: Tagged | undefined): number {
|
||||
return t === undefined ? 0 : t.id + t.tags.length;
|
||||
}
|
||||
class Holder {
|
||||
last: Tagged | undefined = undefined;
|
||||
take(t: Tagged | undefined): void {
|
||||
this.last = t;
|
||||
}
|
||||
}
|
||||
const holder = new Holder();
|
||||
function fill(round: number): number {
|
||||
let read = 0;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const a: Tagged = { id: round * 10 + i, tags: ["a", String(i)] };
|
||||
pushNarrowed(a);
|
||||
const b: Tagged = { id: round * 10 + i + 100, tags: ["b"] };
|
||||
keepDefaulted(b);
|
||||
const c: Tagged = { id: round * 10 + i + 200, tags: ["c"] };
|
||||
storeInSlot(c);
|
||||
const d: Tagged = { id: round * 10 + i + 300, tags: ["d", "d"] };
|
||||
holder.take(d);
|
||||
const e: Tagged = { id: round * 10 + i + 400, tags: [] };
|
||||
passOn(e);
|
||||
const g: Tagged = { id: round * 10 + i + 600, tags: ["g", "g", "g"] };
|
||||
pushAsserted(g);
|
||||
const f: Tagged = { id: round * 10 + i + 500, tags: ["f"] };
|
||||
read += readOnly(f);
|
||||
}
|
||||
return read;
|
||||
}
|
||||
// Reuses the stack the callers above ran on, and allocates collectable
|
||||
// records, before the kept ones are read.
|
||||
function churn(n: number): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const t: Tagged = { id: i, tags: [String(i), String(i + 1)] };
|
||||
total += t.tags.length + readOnly(t);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
let reads = 0;
|
||||
for (let round = 0; round < 4; round++) {
|
||||
reads += fill(round);
|
||||
reads += churn(200);
|
||||
}
|
||||
console.log("reads", reads);
|
||||
console.log("kept", kept.length);
|
||||
for (const t of kept) console.log(t.id, t.tags.join("+"));
|
||||
console.log("slot", slot.held === undefined ? "none" : slot.held.id, slot.held?.tags.join("+"));
|
||||
console.log("holder", holder.last === undefined ? "none" : holder.last.id, holder.last?.tags.join("+"));
|
||||
@@ -0,0 +1,115 @@
|
||||
// Class fields keyed by well-known symbols, Symbol.toStringTag first (the
|
||||
// lru-cache shape), beside toStringTag getters and module symbol keys.
|
||||
// String conversion, Object.prototype.toString and util.inspect read the
|
||||
// tag as Node does, through typed values and through `unknown`.
|
||||
|
||||
const limit = Symbol("limit");
|
||||
|
||||
class Cache {
|
||||
size = 0;
|
||||
[limit] = 10;
|
||||
[Symbol.toStringTag] = "LRUCache";
|
||||
}
|
||||
|
||||
class Sub extends Cache {
|
||||
extra = 1;
|
||||
}
|
||||
|
||||
class Retag extends Cache {
|
||||
constructor() {
|
||||
super();
|
||||
this[Symbol.toStringTag] = "Retagged";
|
||||
}
|
||||
}
|
||||
|
||||
class Tagged {
|
||||
x = 1;
|
||||
get [Symbol.toStringTag](): string {
|
||||
return "Custom";
|
||||
}
|
||||
}
|
||||
|
||||
class TaggedSub extends Tagged {
|
||||
y = 2;
|
||||
}
|
||||
|
||||
class Named {
|
||||
get [Symbol.toStringTag](): string {
|
||||
return "Named";
|
||||
}
|
||||
}
|
||||
|
||||
class Blank {
|
||||
get [Symbol.toStringTag](): string {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
class Plain {
|
||||
n = 3;
|
||||
}
|
||||
|
||||
const c = new Cache();
|
||||
console.log(String(c), `${c}`, Object.prototype.toString.call(c), c[Symbol.toStringTag]);
|
||||
console.log(c);
|
||||
console.log(Object.keys(c), JSON.stringify(c));
|
||||
console.log(new Sub(), String(new Sub()));
|
||||
const r: Cache = new Retag();
|
||||
console.log(String(r), r);
|
||||
c[Symbol.toStringTag] = "Other";
|
||||
console.log(`${c}`, c);
|
||||
|
||||
const t = new Tagged();
|
||||
console.log(String(t), `${t}`, t, [t], { deep: { deeper: { t } } });
|
||||
const ts: Tagged = new TaggedSub();
|
||||
console.log(String(ts), ts);
|
||||
console.log(new Named(), new Blank(), String(new Blank()));
|
||||
console.log(String(new Plain()), new Plain());
|
||||
|
||||
const values: unknown[] = [new Cache(), new Tagged(), new TaggedSub(), new Plain()];
|
||||
for (const v of values) {
|
||||
console.log(Object.prototype.toString.call(v), String(v), (v as { [Symbol.toStringTag]?: unknown })[Symbol.toStringTag]);
|
||||
}
|
||||
console.log(values);
|
||||
|
||||
const literal: unknown = { [Symbol.toStringTag]: "Literal", a: 1 };
|
||||
console.log(Object.prototype.toString.call(literal), String(literal), `${literal}`);
|
||||
|
||||
// util.inspect ignores a Symbol.toStringTag getter that throws, typed or
|
||||
// held as an untyped value.
|
||||
class Throwing {
|
||||
get [Symbol.toStringTag](): string {
|
||||
throw new Error("tag");
|
||||
}
|
||||
}
|
||||
const throwing: unknown = new Throwing();
|
||||
console.log(new Throwing(), [new Throwing()], throwing, [throwing]);
|
||||
|
||||
// A conversion to "[object Object]" still evaluates its receiver.
|
||||
let made = 0;
|
||||
function make(): Plain {
|
||||
made++;
|
||||
return new Plain();
|
||||
}
|
||||
function fail(): Plain {
|
||||
throw new Error("receiver");
|
||||
}
|
||||
console.log(Object.prototype.toString.call(make()), String(make()), made);
|
||||
try {
|
||||
console.log(Object.prototype.toString.call(fail()));
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
}
|
||||
|
||||
// ToString of a class with valueOf and no toString reads its tag; the
|
||||
// default hint calls valueOf.
|
||||
class Valued {
|
||||
valueOf(): number {
|
||||
return 1;
|
||||
}
|
||||
get [Symbol.toStringTag](): string {
|
||||
return "Valued";
|
||||
}
|
||||
}
|
||||
const valued: unknown = new Valued();
|
||||
console.log(String(valued), `${valued}`, [valued].join(), valued + "", Object.prototype.toString.call(valued));
|
||||
@@ -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,11 @@
|
||||
// A module of an import cycle re-exports another member with `export *`
|
||||
// (pi-coding-agent's sdk.js → agent-session-runtime.js →
|
||||
// agent-session-services.js → sdk.js). The re-exported names are live
|
||||
// bindings of the member that declares them, so they read the same values,
|
||||
// and the same temporal dead zone errors, as in Node.
|
||||
import { createSession, describeRuntime, Runtime, SDK_VERSION } from "./sdk.ts";
|
||||
import { servicesReport } from "./services.ts";
|
||||
|
||||
console.log(servicesReport());
|
||||
console.log(createSession("main").describe());
|
||||
console.log(describeRuntime(), new Runtime("direct").name, SDK_VERSION);
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createServices, servicesLabel } from "./services.ts";
|
||||
|
||||
export class Runtime {
|
||||
readonly name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
describe(): string {
|
||||
return `${this.name} runtime with ${servicesLabel}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const runtimeLabel = "runtime";
|
||||
|
||||
export function describeRuntime(): string {
|
||||
return `${runtimeLabel} (${typeof createServices})`;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createServices } from "./services.ts";
|
||||
|
||||
export * from "./runtime.ts";
|
||||
|
||||
export const SDK_VERSION = "1.4.0";
|
||||
|
||||
export function createSession(name: string) {
|
||||
return createServices(name);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Runtime, SDK_VERSION, describeRuntime, runtimeLabel } from "./sdk.ts";
|
||||
|
||||
export const servicesLabel = "services";
|
||||
|
||||
function probe(read: () => unknown): string {
|
||||
try {
|
||||
return String(read());
|
||||
} catch (error) {
|
||||
return `${(error as Error).name}: ${(error as Error).message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// This module runs first: sdk.ts and runtime.ts have not run yet.
|
||||
const early = [
|
||||
probe(() => SDK_VERSION),
|
||||
probe(() => runtimeLabel),
|
||||
probe(() => typeof describeRuntime),
|
||||
probe(() => new Runtime("early").name),
|
||||
];
|
||||
|
||||
export function servicesReport(): string {
|
||||
return `early ${early.join(" | ")}; late ${SDK_VERSION} ${runtimeLabel}`;
|
||||
}
|
||||
|
||||
export function createServices(name: string): Runtime {
|
||||
return new Runtime(name);
|
||||
}
|
||||
@@ -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": 3800, "san": 4300 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3000, "san": 4500 }
|
||||
@@ -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