mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-10 01:56:15 +08:00
Widen record parameters and returns by flow class (#998)
This commit is contained in:
@@ -16,6 +16,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `numeric-kernels` | cpu | sieve, hashing, `number[][]` matmul, `Float64Array` |
|
||||
| `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates |
|
||||
| `optional-fields` | cpu | records and class instances with `number \| undefined` and other scalar-union fields |
|
||||
| `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions |
|
||||
| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains |
|
||||
| `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` |
|
||||
| `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe |
|
||||
|
||||
@@ -84,6 +84,13 @@
|
||||
"10"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "record-boundaries",
|
||||
"entry": "workloads/record-boundaries.ts",
|
||||
"args": [
|
||||
"8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "validate-errors",
|
||||
"entry": "workloads/validate-errors.ts",
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// Records built from unchecked array reads that cross annotated function
|
||||
// boundaries: points returned from a lookup and passed to a metric,
|
||||
// samples defaulted through `??` inside the callee, and a span grown by
|
||||
// a recursive helper. Every read here finds its element.
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
function pointAt(xs: number[], ys: number[], i: number): Point {
|
||||
return { x: xs[i], y: ys[i] };
|
||||
}
|
||||
function distance(p: Point, q: Point): number {
|
||||
const dx = p.x - q.x;
|
||||
const dy = p.y - q.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
function pathLength(xs: number[], ys: number[], rounds: number): number {
|
||||
let total = 0;
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
for (let i = 0; i + 1 < xs.length; i++) total += distance(pointAt(xs, ys, i), pointAt(xs, ys, i + 1));
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
interface Sample {
|
||||
value: number;
|
||||
weight: number;
|
||||
}
|
||||
function weighted(s: Sample): number {
|
||||
return (s.value ?? 0) * (s.weight ?? 1);
|
||||
}
|
||||
function blend(a: Sample, b: Sample): Sample {
|
||||
return { value: (a.value + b.value) / 2, weight: a.weight + b.weight };
|
||||
}
|
||||
function mix(values: number[], weights: number[], rounds: number): number {
|
||||
let total = 0;
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
for (let i = 0; i + 1 < values.length; i++) {
|
||||
const s = blend({ value: values[i], weight: weights[i] }, { value: values[i + 1], weight: weights[i + 1] });
|
||||
total += weighted(s);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
interface Span {
|
||||
lo: number;
|
||||
hi: number;
|
||||
}
|
||||
function widen(span: Span, values: number[], from: number, to: number): Span {
|
||||
if (from >= to) return span;
|
||||
const v = values[from];
|
||||
return widen({ lo: Math.min(span.lo, v), hi: Math.max(span.hi, v) }, values, from + 1, to);
|
||||
}
|
||||
function spans(values: number[], rounds: number): number {
|
||||
let total = 0;
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
for (let i = 0; i + 16 <= values.length; i += 16) {
|
||||
const s = widen({ lo: values[i], hi: values[i] }, values, i + 1, i + 16);
|
||||
total += s.hi - s.lo;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
const xs: number[] = [];
|
||||
const ys: number[] = [];
|
||||
const weights: number[] = [];
|
||||
for (let i = 0; i < 4096; i++) {
|
||||
xs.push((i * 37) % 101);
|
||||
ys.push((i * 53) % 97);
|
||||
weights.push(((i * 7) % 11) / 4);
|
||||
}
|
||||
const path = pathLength(xs, ys, Math.floor(300 * scale));
|
||||
const mixed = mix(xs, weights, Math.floor(300 * scale));
|
||||
const spanned = spans(ys, Math.floor(300 * scale));
|
||||
console.log("path", path.toFixed(3), "mix", mixed.toFixed(3), "spans", spanned.toFixed(3));
|
||||
@@ -4828,6 +4828,8 @@ function inferredOptionalRecordType(
|
||||
actual: IrType,
|
||||
): IrType {
|
||||
if (decl.type || hasJsTypeAnnotation(decl)) return declared;
|
||||
const symbol = ts.isIdentifier(decl.name) ? lowerer.resolveValueSymbol(decl.name) : undefined;
|
||||
if (symbol && lowerer.runtimeOptionalRecordsKeptDeclared.has(symbol)) return declared;
|
||||
if (lowerer.isRuntimeOptionalRecordVariant(actual, declared)) return actual;
|
||||
if (declared.kind !== "record" || actual.kind !== "record") return declared;
|
||||
const target = lowerer.shapes.get(declared.shapeId);
|
||||
|
||||
@@ -76,6 +76,7 @@ import { NarrowingStability } from "./narrowing-stability.js";
|
||||
import { ArrayElementStates } from "./runtime-optional-elements.js";
|
||||
import { indexReadInBounds } from "./runtime-optional-bounds.js";
|
||||
import { ArrayOwnership } from "./runtime-optional-ownership.js";
|
||||
import { RecordFlowClasses } from "./runtime-optional-records.js";
|
||||
import { StringIndexBounds } from "./string-index-bounds.js";
|
||||
import { NanCodedReads } from "./nan-coded-reads.js";
|
||||
import { RuntimeOptionalLocals } from "./runtime-optional-locals.js";
|
||||
@@ -1798,6 +1799,10 @@ export class Lowerer {
|
||||
/** Record fields promoted to an undefined-armed union by an indexed-read
|
||||
* value. The key is the concrete emitted shape and field name. */
|
||||
readonly runtimeOptionalFields = new Set<string>();
|
||||
/** Record bindings whose flow class reaches storage outside the class:
|
||||
* they keep the declared layout, so an initializer from unchecked reads
|
||||
* converts once instead of the binding adopting the widened variant. */
|
||||
readonly runtimeOptionalRecordsKeptDeclared = new Set<ts.Symbol>();
|
||||
/** Callback pattern parameters carry an optional source without making
|
||||
* every name destructured from a present source optional. */
|
||||
readonly runtimeOptionalPatternTypes = new Map<ts.Node, IrType>();
|
||||
@@ -3804,6 +3809,61 @@ export class Lowerer {
|
||||
}
|
||||
const mappedOrNull = (node: ts.Node): IrType | null =>
|
||||
panicSafe(() => this.mapTypeOf(this.typeOf(node)), null);
|
||||
const admitsUndefined = (type: IrType): boolean => typeEquals(addUndefined(type), type);
|
||||
const plainRecordShape = (type: IrType | null | undefined): string | null => {
|
||||
if (type?.kind !== "record") return null;
|
||||
const shape = this.shapes.get(type.shapeId);
|
||||
return shape && !shape.tuple && !shape.indexValue && shape.fields.length > 0
|
||||
? type.shapeId
|
||||
: null;
|
||||
};
|
||||
// Parameters and results read the signatures collected above; bindings
|
||||
// ask the checker, so the scan builds no layout of its own.
|
||||
const parameterType = (node: ts.Node): IrType | null => {
|
||||
const parameter = node.parent;
|
||||
if (!parameter || !ts.isParameter(parameter)) return null;
|
||||
const fn = parameter.parent;
|
||||
const symbol =
|
||||
(ts.isFunctionDeclaration(fn) || ts.isMethodDeclaration(fn)) && fn.name
|
||||
? symbolOf(fn.name)
|
||||
: null;
|
||||
const index = (fn as ts.FunctionLikeDeclaration).parameters.indexOf(parameter);
|
||||
return (symbol && signatureBySymbol.get(symbol)?.params[index]?.type) ?? null;
|
||||
};
|
||||
const returnTypeOf = (fn: ts.FunctionDeclaration | ts.MethodDeclaration): IrType | null => {
|
||||
const symbol = fn.name ? symbolOf(fn.name) : null;
|
||||
return (symbol && signatureBySymbol.get(symbol)?.returnType) ?? null;
|
||||
};
|
||||
const recordFlows = new RecordFlowClasses(sourceFiles, {
|
||||
symbolOf,
|
||||
declarationOf: (symbol) => this.checker.valueDeclarationOf(symbol),
|
||||
mayBeRecord: (node) => {
|
||||
if (node.parent && ts.isParameter(node.parent))
|
||||
return plainRecordShape(parameterType(node)) !== null;
|
||||
const t = panicSafe(() => this.typeOf(node), null);
|
||||
return (
|
||||
t !== null &&
|
||||
(t.flags & ts.TypeFlags.Object) !== 0 &&
|
||||
!this.checker.isArrayType(t) &&
|
||||
this.checker.getCallSignatures(t).length === 0 &&
|
||||
this.checker.getConstructSignatures(t).length === 0
|
||||
);
|
||||
},
|
||||
prefetchTypes: (nodes) => this.checker.prefetchCollectionTypes(nodes),
|
||||
mayReturnRecord: (fn) => plainRecordShape(returnTypeOf(fn)) !== null,
|
||||
familyOf: (method) => familyBySymbol.get(method),
|
||||
recordShape: (node) =>
|
||||
plainRecordShape(
|
||||
node.parent && ts.isParameter(node.parent) ? parameterType(node) : mappedOrNull(node),
|
||||
),
|
||||
returnShape: (fn) => plainRecordShape(returnTypeOf(fn)),
|
||||
isGlobal: (node, name) => this.isStdlibGlobal(node, name),
|
||||
});
|
||||
/** Optional fields of the records a visible function returns. */
|
||||
const returnFields = new Map<ts.Symbol, Set<string>>();
|
||||
/** Record parameters that receive a value with optional fields, by the
|
||||
* parameter's own symbol. */
|
||||
const recordParameterSlots = new Map<ts.Symbol, { target: ts.Symbol; index: number }>();
|
||||
const nanCoded = new NanCodedReads(
|
||||
{
|
||||
symbolOf,
|
||||
@@ -4081,10 +4141,8 @@ export class Lowerer {
|
||||
if (op === ts.SyntaxKind.AmpersandAmpersandToken)
|
||||
return mayBeOptional(e.left) || mayBeOptional(e.right);
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression)) {
|
||||
const symbol = symbolOf(e.expression);
|
||||
const fields = symbol === null ? undefined : optionalFields.get(symbol);
|
||||
return fields?.has(fieldName(e)) ?? false;
|
||||
if (ts.isPropertyAccessExpression(e)) {
|
||||
return fieldsOfValue(e.expression)?.has(fieldName(e)) ?? false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -4179,9 +4237,40 @@ export class Lowerer {
|
||||
// a spread or a parameter receives the source's optional fields.
|
||||
const fieldsOfValue = (node: ts.Expression): ReadonlySet<string> | undefined => {
|
||||
const e = peel(node);
|
||||
if (!ts.isIdentifier(e)) return undefined;
|
||||
const symbol = symbolOf(e);
|
||||
return symbol === null ? undefined : optionalFields.get(symbol);
|
||||
if (ts.isIdentifier(e)) {
|
||||
const symbol = symbolOf(e);
|
||||
// A binding whose class reaches other storage keeps the declared
|
||||
// layout, so its fields read as present.
|
||||
return symbol === null || recordFlows.opened(symbol)
|
||||
? undefined
|
||||
: optionalFields.get(symbol);
|
||||
}
|
||||
// A fresh literal carries the fields its values can leave undefined.
|
||||
if (ts.isObjectLiteralExpression(e)) {
|
||||
const out = spreadFieldsOf(e);
|
||||
for (const prop of e.properties) {
|
||||
if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) continue;
|
||||
const name = prop.name;
|
||||
if (!(ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)))
|
||||
continue;
|
||||
const value = ts.isPropertyAssignment(prop) ? prop.initializer : (name as ts.Expression);
|
||||
if (mayBeOptional(value)) out.add(name.text);
|
||||
}
|
||||
return out.size > 0 ? out : undefined;
|
||||
}
|
||||
if (
|
||||
ts.isCallExpression(e) &&
|
||||
(ts.isIdentifier(e.expression) || ts.isPropertyAccessExpression(e.expression))
|
||||
) {
|
||||
const symbol = symbolOf(ts.isIdentifier(e.expression) ? e.expression : e.expression.name);
|
||||
return symbol === null ? undefined : returnFields.get(symbol);
|
||||
}
|
||||
if (ts.isConditionalExpression(e)) {
|
||||
const a = fieldsOfValue(e.whenTrue);
|
||||
const b = fieldsOfValue(e.whenFalse);
|
||||
return a && b ? new Set([...a, ...b]) : (a ?? b);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const noteFieldsFrom = (symbol: ts.Symbol, source: ts.Expression): boolean => {
|
||||
const fields = fieldsOfValue(source);
|
||||
@@ -5222,27 +5311,20 @@ export class Lowerer {
|
||||
if (!parameter || !ts.isIdentifier(parameter.name)) continue;
|
||||
const bound = symbolOf(parameter.name);
|
||||
if (!bound) continue;
|
||||
let promoted: IrType = parameterShape.type;
|
||||
const declared = this.shapes.get(parameterShape.type.shapeId);
|
||||
// The parameter's layout settles with its flow class once
|
||||
// the fixed point ends. A literal or call argument only adds
|
||||
// fields the declaration calls present.
|
||||
const fresh = !ts.isIdentifier(peel(arg));
|
||||
for (const field of fields) {
|
||||
const source =
|
||||
promoted.kind === "record"
|
||||
? this.shapes.get(promoted.shapeId)?.fields.find((f) => f.name === field)
|
||||
: undefined;
|
||||
if (source) {
|
||||
promoted = this.runtimeOptionalRecordField(
|
||||
promoted,
|
||||
field,
|
||||
addUndefined(source.type),
|
||||
);
|
||||
const slot = declared?.fields.find((f) => f.name === field);
|
||||
if (!slot || (fresh && admitsUndefined(slot.type))) continue;
|
||||
if (noteFieldSymbol(bound, field)) {
|
||||
provenance?.note("record-parameter", parameter, arg);
|
||||
changed = true;
|
||||
}
|
||||
if (noteFieldSymbol(bound, field)) changed = true;
|
||||
}
|
||||
if (!typeEquals(promoted, parameterShape.type)) {
|
||||
parameterShape.type = promoted;
|
||||
this.runtimeOptionalBindingTypes.set(bound, promoted);
|
||||
provenance?.note("record-parameter", parameter, arg);
|
||||
changed = true;
|
||||
}
|
||||
recordParameterSlots.set(bound, { target, index: i });
|
||||
}
|
||||
}
|
||||
if (ts.isSpreadElement(arg) || !shape) return;
|
||||
@@ -5324,7 +5406,25 @@ export class Lowerer {
|
||||
};
|
||||
const scanReturns = (symbol: ts.Symbol, decl: ts.FunctionLikeDeclaration): boolean => {
|
||||
let changed = false;
|
||||
const recordReturn = recordFlows.returnClass(symbol) !== null;
|
||||
const returned = signatureBySymbol.get(symbol)?.returnType;
|
||||
const returnedShape = returned?.kind === "record" ? this.shapes.get(returned.shapeId) : null;
|
||||
for (const expression of returnsOf(decl)) {
|
||||
const fields = recordReturn ? fieldsOfValue(expression) : undefined;
|
||||
if (fields) {
|
||||
for (const field of fields) {
|
||||
// A result field that already admits undefined keeps the
|
||||
// checker's narrowing.
|
||||
const slot = returnedShape?.fields.find((f) => f.name === field);
|
||||
if (slot && admitsUndefined(slot.type)) continue;
|
||||
const set = returnFields.get(symbol) ?? new Set<string>();
|
||||
if (set.has(field)) continue;
|
||||
set.add(field);
|
||||
returnFields.set(symbol, set);
|
||||
provenance?.note("record-return", decl.name ?? decl, expression);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (mayBeOptional(expression) && !optionalReturns.has(symbol)) {
|
||||
optionalReturns.add(symbol);
|
||||
provenance?.note("return", decl.name ?? decl, expression);
|
||||
@@ -5497,6 +5597,70 @@ export class Lowerer {
|
||||
this.runtimeOptionalGlobals.add(global);
|
||||
}
|
||||
}
|
||||
// Record layouts settle per flow class: a closed class widens every
|
||||
// member to the union of its members' optional fields, and a class that
|
||||
// reaches other storage keeps the declared layout for every member.
|
||||
const classFields = new Map<number, Set<string>>();
|
||||
const addClassFields = (id: number | null, fields: ReadonlySet<string>): void => {
|
||||
if (id === null) return;
|
||||
const set = classFields.get(id) ?? new Set<string>();
|
||||
for (const field of fields) set.add(field);
|
||||
classFields.set(id, set);
|
||||
};
|
||||
for (const [symbol, fields] of optionalFields)
|
||||
addClassFields(recordFlows.bindingClass(symbol), fields);
|
||||
for (const [symbol, fields] of returnFields)
|
||||
addClassFields(recordFlows.returnClass(symbol), fields);
|
||||
const settledFields = (
|
||||
id: number | null,
|
||||
own: ReadonlySet<string> | undefined,
|
||||
): ReadonlySet<string> | undefined =>
|
||||
id === null
|
||||
? own
|
||||
: classFields.has(id) && recordFlows.closed(id)
|
||||
? classFields.get(id)
|
||||
: undefined;
|
||||
const widenRecordFields = (type: IrType, fields: ReadonlySet<string>): IrType => {
|
||||
let promoted = type;
|
||||
for (const field of fields) {
|
||||
const source =
|
||||
promoted.kind === "record"
|
||||
? this.shapes.get(promoted.shapeId)?.fields.find((f) => f.name === field)
|
||||
: undefined;
|
||||
if (source)
|
||||
promoted = this.runtimeOptionalRecordField(promoted, field, addUndefined(source.type));
|
||||
}
|
||||
return promoted;
|
||||
};
|
||||
for (const [symbol, id] of recordFlows.bindings())
|
||||
if (classFields.has(id) && !recordFlows.closed(id))
|
||||
this.runtimeOptionalRecordsKeptDeclared.add(symbol);
|
||||
// Every tracked parameter settles with its class, including one that
|
||||
// only receives plain records. An untracked one (a function used as a
|
||||
// value, a method another member name or view can reach) widens to the
|
||||
// fields its own arguments carry.
|
||||
for (const { symbol, fn, index } of recordFlows.parameters())
|
||||
recordParameterSlots.set(symbol, { target: fn, index });
|
||||
for (const [bound, { target, index }] of recordParameterSlots) {
|
||||
const shape = signatureBySymbol.get(target)?.params[index];
|
||||
if (!shape || shape.type.kind !== "record") continue;
|
||||
const fields = settledFields(recordFlows.bindingClass(bound), optionalFields.get(bound));
|
||||
if (!fields) continue;
|
||||
const promoted = widenRecordFields(shape.type, fields);
|
||||
if (typeEquals(promoted, shape.type)) continue;
|
||||
shape.type = promoted;
|
||||
this.runtimeOptionalBindingTypes.set(bound, promoted);
|
||||
}
|
||||
for (const [symbol, sig] of signatureBySymbol) {
|
||||
const id = recordFlows.returnClass(symbol);
|
||||
const fields = id === null ? undefined : settledFields(id, undefined);
|
||||
if (fields && sig.returnType.kind === "record") {
|
||||
const next = widenRecordFields(sig.returnType, fields);
|
||||
sig.returnType = next;
|
||||
if (sig.top) sig.top.returnType = next;
|
||||
if (sig.method) sig.method.ret = next;
|
||||
}
|
||||
}
|
||||
for (const [symbol, sig] of signatureBySymbol) {
|
||||
const params = optionalParams.get(symbol);
|
||||
if (params) {
|
||||
@@ -5577,7 +5741,7 @@ export class Lowerer {
|
||||
if (global) global.type = promoted;
|
||||
}
|
||||
}
|
||||
const fields = optionalFields.get(symbol);
|
||||
const fields = settledFields(recordFlows.bindingClass(symbol), optionalFields.get(symbol));
|
||||
if (!dynamicObjectEntryRows.has(symbol) && !optionalSymbols.has(symbol) && !fields) return;
|
||||
const current = this.mapTypeOf(this.typeOf(node.name));
|
||||
if (!current) return;
|
||||
@@ -5594,16 +5758,7 @@ export class Lowerer {
|
||||
};
|
||||
}
|
||||
}
|
||||
if (fields && promoted.kind === "record") {
|
||||
for (const field of fields) {
|
||||
const source =
|
||||
promoted.kind === "record"
|
||||
? this.shapes.get(promoted.shapeId)?.fields.find((f) => f.name === field)
|
||||
: undefined;
|
||||
if (!source) continue;
|
||||
promoted = this.runtimeOptionalRecordField(promoted, field, addUndefined(source.type));
|
||||
}
|
||||
}
|
||||
if (fields && promoted.kind === "record") promoted = widenRecordFields(promoted, fields);
|
||||
if (!typeEquals(promoted, current)) {
|
||||
this.runtimeOptionalBindingTypes.set(symbol, promoted);
|
||||
const global = ts.isIdentifier(node.name)
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { closeSourceParser, parseSourceFile } from "../ts7/source-parser-node.js";
|
||||
import { RecordFlowClasses, type RecordFlowHost } from "./runtime-optional-records.js";
|
||||
|
||||
afterAll(closeSourceParser);
|
||||
|
||||
/** Flow classes over one parsed file. Names are unique per test, so a
|
||||
* binding's symbol is its name; a slot holds a record when its annotation
|
||||
* names a type starting with `R` (the shape) or, unannotated, when it is
|
||||
* initialized from an object literal or a call. */
|
||||
function classes(source: string): {
|
||||
classOf(name: string): number | null;
|
||||
closed(name: string): boolean;
|
||||
opened(name: string): boolean;
|
||||
returns(name: string): number | null;
|
||||
} {
|
||||
const file = parseSourceFile("records.ts", source, "ts");
|
||||
const symbols = new Map<string, ts.Symbol>();
|
||||
const declarations = new Map<ts.Symbol, ts.Node>();
|
||||
const symbolFor = (name: string): ts.Symbol => {
|
||||
let symbol = symbols.get(name);
|
||||
if (!symbol) {
|
||||
symbol = { name, declarations: [] } as unknown as ts.Symbol;
|
||||
symbols.set(name, symbol);
|
||||
}
|
||||
return symbol;
|
||||
};
|
||||
ts.walkPreorder(file, (node) => {
|
||||
if (
|
||||
(ts.isVariableDeclaration(node) ||
|
||||
ts.isParameter(node) ||
|
||||
ts.isFunctionDeclaration(node) ||
|
||||
ts.isClassDeclaration(node) ||
|
||||
ts.isMethodDeclaration(node)) &&
|
||||
node.name &&
|
||||
ts.isIdentifier(node.name)
|
||||
) {
|
||||
const symbol = symbolFor(node.name.text);
|
||||
declarations.set(symbol, node);
|
||||
(symbol.declarations as unknown as ts.Node[]).push(node);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
const annotated = (type: ts.TypeNode | undefined): string | null => {
|
||||
if (!type || !ts.isTypeReferenceNode(type) || !ts.isIdentifier(type.typeName)) return null;
|
||||
return type.typeName.text.startsWith("R") ? type.typeName.text : null;
|
||||
};
|
||||
const host: RecordFlowHost = {
|
||||
symbolOf: (node) =>
|
||||
ts.isIdentifier(node) && node.text !== "console" && node.text !== "undefined"
|
||||
? symbolFor(node.text)
|
||||
: null,
|
||||
declarationOf: (symbol) => declarations.get(symbol),
|
||||
recordShape: (node) => {
|
||||
const declaration = node.parent;
|
||||
if (!declaration) return null;
|
||||
if (ts.isParameter(declaration)) return annotated(declaration.type);
|
||||
if (!ts.isVariableDeclaration(declaration)) return null;
|
||||
if (declaration.type) return annotated(declaration.type);
|
||||
const init = declaration.initializer;
|
||||
return init && (ts.isObjectLiteralExpression(init) || ts.isCallExpression(init)) ? "R" : null;
|
||||
},
|
||||
returnShape: (fn) => annotated(fn.type),
|
||||
mayBeRecord: (node) => host.recordShape(node) !== null,
|
||||
prefetchTypes: () => {},
|
||||
mayReturnRecord: (fn) => annotated(fn.type) !== null,
|
||||
familyOf: () => undefined,
|
||||
isGlobal: (node, name) => ts.isIdentifier(node) && node.text === name,
|
||||
};
|
||||
const flows = new RecordFlowClasses([file], host);
|
||||
const classOf = (name: string): number | null => flows.bindingClass(symbolFor(name));
|
||||
return {
|
||||
classOf,
|
||||
closed: (name) => {
|
||||
const id = classOf(name);
|
||||
return id !== null && flows.closed(id);
|
||||
},
|
||||
opened: (name) => flows.opened(symbolFor(name)),
|
||||
returns: (name) => flows.returnClass(symbolFor(name)),
|
||||
};
|
||||
}
|
||||
|
||||
test("literals, parameters, returns and bindings form one closed class", () => {
|
||||
const flows = classes(`
|
||||
function make(a: number[], i: number): R { return { v: a[i] }; }
|
||||
function show(p: R): string { return String(p.v); }
|
||||
function pass(q: R): R { return q; }
|
||||
const made = make([], 0);
|
||||
const passed = pass(made);
|
||||
console.log(show(passed), show({ v: 1 }), passed === made, made.v);
|
||||
`);
|
||||
expect(flows.closed("made")).toBe(true);
|
||||
for (const name of ["passed", "p", "q"]) expect(flows.classOf(name)).toBe(flows.classOf("made"));
|
||||
expect(flows.returns("make")).toBe(flows.classOf("made"));
|
||||
expect(flows.returns("pass")).toBe(flows.classOf("made"));
|
||||
});
|
||||
|
||||
test("a member stored elsewhere opens its whole class", () => {
|
||||
const flows = classes(`
|
||||
function keep(p: R, out: unknown[]): void { out.push(p); }
|
||||
const o = { v: 1 };
|
||||
keep(o, []);
|
||||
const other = { v: 2 };
|
||||
console.log(other.v);
|
||||
`);
|
||||
expect(flows.classOf("p")).toBe(flows.classOf("o"));
|
||||
expect(flows.closed("o")).toBe(false);
|
||||
expect(flows.opened("p")).toBe(true);
|
||||
expect(flows.closed("other")).toBe(true);
|
||||
expect(flows.opened("other")).toBe(false);
|
||||
});
|
||||
|
||||
test("methods the runtime can call without a call site are not tracked", () => {
|
||||
const flows = classes(`
|
||||
class Plain { use(p: R): number { return p.v; } }
|
||||
class Listener { handleEvent(e: R): void { console.log(e.v); } }
|
||||
class Mine extends EventTarget { mine(q: R): number { return q.v; } }
|
||||
class Base { base(): number { return 0; } }
|
||||
class Derived extends Base { derived(s: R): number { return s.v; } }
|
||||
new Plain().use({ v: 1 });
|
||||
new Listener().handleEvent({ v: 2 });
|
||||
new Mine().mine({ v: 3 });
|
||||
new Derived().derived({ v: 4 });
|
||||
`);
|
||||
expect(flows.classOf("p")).not.toBeNull();
|
||||
expect(flows.classOf("s")).not.toBeNull();
|
||||
expect(flows.classOf("e")).toBeNull();
|
||||
expect(flows.classOf("q")).toBeNull();
|
||||
});
|
||||
|
||||
test("a function used as a value has no tracked parameters", () => {
|
||||
const flows = classes(`
|
||||
function show(p: R): string { return String(p.v); }
|
||||
const shown = [].map(show);
|
||||
const r = { v: 1 };
|
||||
show(r);
|
||||
`);
|
||||
expect(flows.classOf("p")).toBeNull();
|
||||
expect(flows.closed("r")).toBe(false);
|
||||
});
|
||||
|
||||
test("shorthand properties, element writes and foreign sources open a class", () => {
|
||||
const flows = classes(`
|
||||
const nested = { v: 1 };
|
||||
const holder = { nested };
|
||||
const fromField = holder.inner;
|
||||
const copied = { v: 2 };
|
||||
const box: unknown[] = [copied];
|
||||
const read = { v: 3 };
|
||||
console.log(read.v, { ...read }, read === undefined, \`\${read}\`);
|
||||
`);
|
||||
expect(flows.closed("nested")).toBe(false);
|
||||
expect(flows.closed("fromField")).toBe(false);
|
||||
expect(flows.closed("copied")).toBe(false);
|
||||
expect(flows.closed("read")).toBe(true);
|
||||
});
|
||||
|
||||
test("members of different declared shapes do not share a widened layout", () => {
|
||||
const flows = classes(`
|
||||
function wide(p: RWide): RWide { return p; }
|
||||
const narrow: RNarrow = { v: 1 };
|
||||
wide(narrow);
|
||||
`);
|
||||
expect(flows.classOf("narrow")).toBe(flows.classOf("p"));
|
||||
expect(flows.closed("narrow")).toBe(false);
|
||||
// Not opened: the class reaches no other storage.
|
||||
expect(flows.opened("narrow")).toBe(false);
|
||||
});
|
||||
|
||||
test("assignments join their target and a foreign value opens it", () => {
|
||||
const flows = classes(`
|
||||
let target = { v: 1 };
|
||||
const joined = { v: 2 };
|
||||
target = joined;
|
||||
let other = { v: 3 };
|
||||
other ??= target;
|
||||
let foreign = { v: 4 };
|
||||
foreign = holder.inner;
|
||||
`);
|
||||
expect(flows.classOf("target")).toBe(flows.classOf("joined"));
|
||||
expect(flows.classOf("other")).toBe(flows.classOf("target"));
|
||||
expect(flows.closed("target")).toBe(true);
|
||||
expect(flows.closed("foreign")).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,727 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
|
||||
/** Program queries for the record-flow proof. */
|
||||
export interface RecordFlowHost {
|
||||
symbolOf(node: ts.Node): ts.Symbol | null;
|
||||
declarationOf(symbol: ts.Symbol): ts.Node | undefined;
|
||||
/** The node's checker type can be a record: an object type that is not
|
||||
* an array, a function or a class instance. Answers without building a
|
||||
* layout, so the scan leaves shape interning order alone. */
|
||||
mayBeRecord(node: ts.Node): boolean;
|
||||
/** Batches the checker type queries `mayBeRecord` makes for `nodes`. */
|
||||
prefetchTypes(nodes: readonly ts.Node[]): void;
|
||||
/** The same question for a function's result. */
|
||||
mayReturnRecord(fn: Callable): boolean;
|
||||
/** The methods sharing a dispatch slot with `method` (itself included),
|
||||
* or undefined when it shares none. */
|
||||
familyOf(method: ts.Symbol): readonly ts.Symbol[] | undefined;
|
||||
/** The shape id of the node's plain record layout (named fields, no
|
||||
* tuple and no index signature), or null for any other value. */
|
||||
recordShape(node: ts.Node): string | null;
|
||||
/** The shape id of a function's plain record result, or null. */
|
||||
returnShape(fn: Callable): string | null;
|
||||
isGlobal(node: ts.Expression, name: string): boolean;
|
||||
}
|
||||
|
||||
/** A function declaration or a class method. */
|
||||
export type Callable = ts.FunctionDeclaration | ts.MethodDeclaration;
|
||||
|
||||
/** Calls that read a record without keeping or converting it. */
|
||||
const READING_CALLS = new Map([
|
||||
["console", new Set(["log", "info", "warn", "error", "debug", "dir"])],
|
||||
["JSON", new Set(["stringify"])],
|
||||
["Object", new Set(["keys", "values", "entries", "hasOwn", "getOwnPropertyNames"])],
|
||||
["Reflect", new Set(["has", "ownKeys"])],
|
||||
["Array", new Set(["isArray"])],
|
||||
]);
|
||||
|
||||
/** Method names the runtime calls without a visible call site. */
|
||||
const PROTOCOL_METHODS = new Set([
|
||||
"handleEvent",
|
||||
"toJSON",
|
||||
"toString",
|
||||
"toLocaleString",
|
||||
"valueOf",
|
||||
"then",
|
||||
"next",
|
||||
"return",
|
||||
"throw",
|
||||
]);
|
||||
|
||||
/** Global functions that read a record without keeping it. */
|
||||
const READING_FUNCTIONS = new Set(["String", "structuredClone"]);
|
||||
|
||||
/** Record values that flow between local bindings, the parameters and the
|
||||
* returns of functions whose every call site is visible, grouped into
|
||||
* classes that must share one layout. Widening a record field to hold
|
||||
* undefined changes the layout, so a class can take the widened layout only
|
||||
* when it is closed: every value entering it is a fresh object literal or
|
||||
* comes from another member, every use of a member reads or writes its
|
||||
* fields, passes it to another member, or only inspects it, and every
|
||||
* member declares the same shape. A record that reaches any other storage
|
||||
* keeps the declared layout, since converting it there would copy the
|
||||
* object. */
|
||||
export class RecordFlowClasses {
|
||||
private readonly parent: number[] = [];
|
||||
private readonly open: boolean[] = [];
|
||||
/** The declaration each slot reads its shape from: a binding or
|
||||
* parameter name, or a function for its result. */
|
||||
private readonly slotNodes: ts.Node[] = [];
|
||||
private readonly slotReturns: boolean[] = [];
|
||||
private readonly shapeChecked = new Map<number, boolean>();
|
||||
private readonly bindingSlots = new Map<ts.Symbol, number>();
|
||||
private readonly returnSlots = new Map<ts.Symbol, number>();
|
||||
/** Visible functions by symbol: plain declarations called only by name,
|
||||
* and methods whose dispatch slot only direct calls reach. */
|
||||
private readonly functions = new Map<ts.Symbol, Callable>();
|
||||
/** Members sharing each visible method's dispatch slot. */
|
||||
private readonly families = new Map<ts.Symbol, readonly ts.Symbol[]>();
|
||||
/** String keys of `x["name"]` accesses. */
|
||||
private readonly literalKeys = new Set<string>();
|
||||
/** Identifiers by name; symbols resolve only for the names of slots and
|
||||
* functions, so the scan asks the checker about few nodes. */
|
||||
private readonly names = new Map<string, ts.Identifier[]>();
|
||||
private readonly references = new Map<ts.Symbol, ts.Identifier[]>();
|
||||
/** Names written as shorthand properties (`{ p }`): the checker resolves
|
||||
* those to the property, so a binding of that name stays open. */
|
||||
private readonly shorthandNames = new Set<string>();
|
||||
|
||||
constructor(
|
||||
private readonly files: readonly ts.SourceFile[],
|
||||
private readonly host: RecordFlowHost,
|
||||
) {
|
||||
this.index();
|
||||
this.collectFunctions();
|
||||
this.collectSlots();
|
||||
this.connect();
|
||||
}
|
||||
|
||||
/** The class id of a binding or parameter slot, or null when the symbol
|
||||
* is not a tracked record slot. */
|
||||
bindingClass(symbol: ts.Symbol): number | null {
|
||||
const slot = this.bindingSlots.get(symbol);
|
||||
return slot === undefined ? null : this.find(slot);
|
||||
}
|
||||
|
||||
/** The binding or parameter is in a class that reaches other storage. */
|
||||
opened(symbol: ts.Symbol): boolean {
|
||||
const slot = this.bindingSlots.get(symbol);
|
||||
return slot !== undefined && this.open[this.find(slot)]!;
|
||||
}
|
||||
|
||||
/** Every tracked binding and parameter with its class id. */
|
||||
bindings(): [ts.Symbol, number][] {
|
||||
const out: [ts.Symbol, number][] = [];
|
||||
for (const [symbol, slot] of this.bindingSlots) out.push([symbol, this.find(slot)]);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Every tracked parameter: its symbol, function and position. */
|
||||
parameters(): { symbol: ts.Symbol; fn: ts.Symbol; index: number }[] {
|
||||
const out: { symbol: ts.Symbol; fn: ts.Symbol; index: number }[] = [];
|
||||
for (const [fn, decl] of this.functions) {
|
||||
for (let index = 0; index < decl.parameters.length; index++) {
|
||||
const symbol = this.host.symbolOf(decl.parameters[index]!.name);
|
||||
if (symbol && this.bindingSlots.has(symbol)) out.push({ symbol, fn, index });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The class id of a visible function's record return, or null. */
|
||||
returnClass(symbol: ts.Symbol): number | null {
|
||||
const slot = this.returnSlots.get(symbol);
|
||||
return slot === undefined ? null : this.find(slot);
|
||||
}
|
||||
|
||||
/** The declaration of a visible function or method, whose parameters
|
||||
* and return are tracked slots. */
|
||||
visibleFunction(symbol: ts.Symbol): Callable | undefined {
|
||||
return this.functions.get(symbol);
|
||||
}
|
||||
|
||||
/** A class can take a widened layout: nothing outside it reaches its
|
||||
* members and they all declare one record shape. */
|
||||
closed(classId: number): boolean {
|
||||
const root = this.find(classId);
|
||||
if (this.open[root]) return false;
|
||||
const cached = this.shapeChecked.get(root);
|
||||
if (cached !== undefined) return cached;
|
||||
let shape: string | null | undefined;
|
||||
let uniform = true;
|
||||
for (let slot = 0; slot < this.parent.length && uniform; slot++) {
|
||||
if (this.find(slot) !== root) continue;
|
||||
const node = this.slotNodes[slot]!;
|
||||
const own = this.slotReturns[slot]
|
||||
? this.host.returnShape(node as ts.FunctionDeclaration)
|
||||
: this.host.recordShape(node);
|
||||
// Members of different shapes convert between each other.
|
||||
if (own === null || (shape !== undefined && own !== shape)) uniform = false;
|
||||
shape = own;
|
||||
}
|
||||
this.shapeChecked.set(root, uniform);
|
||||
return uniform;
|
||||
}
|
||||
|
||||
private newSlot(node: ts.Node, isReturn = false): number {
|
||||
const id = this.parent.length;
|
||||
this.parent.push(id);
|
||||
this.open.push(false);
|
||||
this.slotNodes.push(node);
|
||||
this.slotReturns.push(isReturn);
|
||||
return id;
|
||||
}
|
||||
|
||||
private find(slot: number): number {
|
||||
let root = slot;
|
||||
while (this.parent[root] !== root) root = this.parent[root]!;
|
||||
while (this.parent[slot] !== root) {
|
||||
const next = this.parent[slot]!;
|
||||
this.parent[slot] = root;
|
||||
slot = next;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
private union(a: number, b: number): void {
|
||||
const ra = this.find(a);
|
||||
const rb = this.find(b);
|
||||
if (ra === rb) return;
|
||||
this.parent[rb] = ra;
|
||||
this.open[ra] = this.open[ra]! || this.open[rb]!;
|
||||
}
|
||||
|
||||
private markOpen(slot: number): void {
|
||||
this.open[this.find(slot)] = true;
|
||||
}
|
||||
|
||||
private index(): void {
|
||||
for (const file of this.files) {
|
||||
ts.walkPreorder(file, (node) => {
|
||||
if (ts.isElementAccessExpression(node)) {
|
||||
const key = peel(node.argumentExpression);
|
||||
if (ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key))
|
||||
this.literalKeys.add(key.text);
|
||||
return undefined;
|
||||
}
|
||||
if (!ts.isIdentifier(node)) return undefined;
|
||||
if (node.parent && ts.isShorthandPropertyAssignment(node.parent))
|
||||
this.shorthandNames.add(node.text);
|
||||
const list = this.names.get(node.text);
|
||||
if (list) list.push(node);
|
||||
else this.names.set(node.text, [node]);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** The identifiers that name `symbol`, its declaration included. */
|
||||
private referencesOf(symbol: ts.Symbol): readonly ts.Identifier[] {
|
||||
let list = this.references.get(symbol);
|
||||
if (!list) {
|
||||
list = (this.names.get(symbol.name) ?? []).filter(
|
||||
(node) => this.host.symbolOf(node) === symbol,
|
||||
);
|
||||
this.references.set(symbol, list);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private collectFunctions(): void {
|
||||
for (const file of this.files) {
|
||||
if (file.isDeclarationFile || isJsFile(file)) continue;
|
||||
ts.walkPreorder(file, (node) => {
|
||||
if (!ts.isFunctionDeclaration(node) || !node.name || !node.body) return undefined;
|
||||
if (node.asteriskToken || node.typeParameters?.length) return undefined;
|
||||
const flags = ts.getCombinedModifierFlags(node);
|
||||
if (flags & (ts.ModifierFlags.Export | ts.ModifierFlags.Async | ts.ModifierFlags.Ambient))
|
||||
return undefined;
|
||||
if (node.parameters.some((p) => p.dotDotDotToken || !ts.isIdentifier(p.name)))
|
||||
return undefined;
|
||||
if (usesArguments(node.body)) return undefined;
|
||||
const symbol = this.host.symbolOf(node.name);
|
||||
if (!symbol || (symbol.declarations?.length ?? 0) !== 1) return undefined;
|
||||
const calledByName = this.referencesOf(symbol).every((reference) => {
|
||||
if (reference === node.name) return true;
|
||||
const call = reference.parent;
|
||||
return (
|
||||
!!call &&
|
||||
ts.isCallExpression(call) &&
|
||||
call.expression === reference &&
|
||||
!call.arguments.some(ts.isSpreadElement)
|
||||
);
|
||||
});
|
||||
if (calledByName) {
|
||||
this.functions.set(symbol, node);
|
||||
this.families.set(symbol, [symbol]);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
this.collectMethods();
|
||||
}
|
||||
|
||||
/** Methods whose every member is a plain body and whose name no other
|
||||
* declaration, dynamic access or literal key shares: then every call that
|
||||
* can dispatch to the slot names a member directly. */
|
||||
/** The class extends, directly or through its bases, a class not
|
||||
* declared in the program's own sources. */
|
||||
private extendsForeignClass(decl: ts.ClassDeclaration): boolean {
|
||||
const seen = new Set<ts.Node>();
|
||||
let current: ts.ClassDeclaration = decl;
|
||||
for (;;) {
|
||||
if (seen.has(current)) return true;
|
||||
seen.add(current);
|
||||
const heritage = current.heritageClauses?.find(
|
||||
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
|
||||
);
|
||||
const base = heritage?.types[0]?.expression;
|
||||
if (!base) return false;
|
||||
const symbol = ts.isIdentifier(base) ? this.host.symbolOf(base) : null;
|
||||
const target = symbol ? this.host.declarationOf(symbol) : undefined;
|
||||
if (!target || !ts.isClassDeclaration(target)) return true;
|
||||
const file = target.getSourceFile();
|
||||
if (file.isDeclarationFile || isJsFile(file)) return true;
|
||||
current = target;
|
||||
}
|
||||
}
|
||||
|
||||
private collectMethods(): void {
|
||||
const candidates = new Map<ts.Symbol, ts.MethodDeclaration>();
|
||||
for (const file of this.files) {
|
||||
if (file.isDeclarationFile || isJsFile(file)) continue;
|
||||
ts.walkPreorder(file, (node) => {
|
||||
if (!ts.isMethodDeclaration(node) || !ts.isIdentifier(node.name) || !node.body)
|
||||
return undefined;
|
||||
const owner = node.parent;
|
||||
if (!owner || !ts.isClassDeclaration(owner)) return undefined;
|
||||
if (
|
||||
ts.getCombinedModifierFlags(owner) &
|
||||
(ts.ModifierFlags.Export | ts.ModifierFlags.Ambient)
|
||||
)
|
||||
return undefined;
|
||||
// The runtime calls protocol methods and the overrides of its own
|
||||
// classes (`_read`, `handleEvent`) with no call site to see.
|
||||
if (PROTOCOL_METHODS.has(node.name.text) || this.extendsForeignClass(owner))
|
||||
return undefined;
|
||||
if (node.asteriskToken || node.typeParameters?.length) return undefined;
|
||||
if (
|
||||
ts.getCombinedModifierFlags(node) &
|
||||
(ts.ModifierFlags.Async | ts.ModifierFlags.Abstract)
|
||||
)
|
||||
return undefined;
|
||||
if (node.parameters.some((p) => p.dotDotDotToken || !ts.isIdentifier(p.name)))
|
||||
return undefined;
|
||||
if (usesArguments(node.body)) return undefined;
|
||||
const symbol = this.host.symbolOf(node.name);
|
||||
if (symbol && (symbol.declarations?.length ?? 0) === 1) candidates.set(symbol, node);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
const decided = new Set<ts.Symbol>();
|
||||
for (const [symbol, decl] of candidates) {
|
||||
if (decided.has(symbol)) continue;
|
||||
const family = this.host.familyOf(symbol) ?? [symbol];
|
||||
for (const member of family) decided.add(member);
|
||||
if (!family.every((member) => candidates.has(member))) continue;
|
||||
const name = (decl.name as ts.Identifier).text;
|
||||
if (this.literalKeys.has(name)) continue;
|
||||
const members = new Set(family);
|
||||
const direct = (this.names.get(name) ?? []).every((id) => {
|
||||
const parent = id.parent;
|
||||
const owner = this.host.symbolOf(id);
|
||||
if (owner && members.has(owner)) {
|
||||
if (parent && ts.isMethodDeclaration(parent) && parent.name === id) return true;
|
||||
const call = parent?.parent;
|
||||
return (
|
||||
!!parent &&
|
||||
ts.isPropertyAccessExpression(parent) &&
|
||||
parent.name === id &&
|
||||
!!call &&
|
||||
ts.isCallExpression(call) &&
|
||||
call.expression === parent &&
|
||||
!call.arguments.some(ts.isSpreadElement)
|
||||
);
|
||||
}
|
||||
// Any other member, property or key of this name could reach the
|
||||
// slot through a structural or dynamic view of an instance.
|
||||
return !parent || !isMemberName(id, parent);
|
||||
});
|
||||
if (!direct) continue;
|
||||
for (const member of family) {
|
||||
this.functions.set(member, candidates.get(member)!);
|
||||
this.families.set(member, family);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private collectSlots(): void {
|
||||
const names: ts.Identifier[] = [];
|
||||
for (const file of this.files) {
|
||||
if (file.isDeclarationFile || isJsFile(file)) continue;
|
||||
ts.walkPreorder(file, (node) => {
|
||||
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) names.push(node.name);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
// The scan runs before reachability is known, so it asks about bodies
|
||||
// lowering may skip; one batch answers them.
|
||||
this.host.prefetchTypes(names);
|
||||
for (const name of names) {
|
||||
if (!this.host.mayBeRecord(name)) continue;
|
||||
const symbol = this.host.symbolOf(name);
|
||||
if (symbol) this.bindingSlots.set(symbol, this.newSlot(name));
|
||||
}
|
||||
for (const [symbol, decl] of this.functions) {
|
||||
for (const parameter of decl.parameters) {
|
||||
if (!this.host.mayBeRecord(parameter.name)) continue;
|
||||
const bound = this.host.symbolOf(parameter.name);
|
||||
if (bound) this.bindingSlots.set(bound, this.newSlot(parameter.name));
|
||||
}
|
||||
if (this.host.mayReturnRecord(decl)) this.returnSlots.set(symbol, this.newSlot(decl, true));
|
||||
}
|
||||
// A dispatch slot has one ABI: members share each parameter's and the
|
||||
// result's layout.
|
||||
for (const [symbol, family] of this.families) {
|
||||
if (family[0] !== symbol || family.length < 2) continue;
|
||||
const decls = family.map((member) => this.functions.get(member)!);
|
||||
const first = decls[0]!;
|
||||
for (let i = 0; i < first.parameters.length; i++) {
|
||||
const head = this.slotOf(first.parameters[i]!.name);
|
||||
for (const decl of decls.slice(1)) {
|
||||
const other = decl.parameters[i] ? this.slotOf(decl.parameters[i]!.name) : undefined;
|
||||
if (head !== undefined && other !== undefined) this.union(head, other);
|
||||
else if (head !== undefined) this.markOpen(head);
|
||||
else if (other !== undefined) this.markOpen(other);
|
||||
}
|
||||
}
|
||||
const results = family.map((member) => this.returnSlots.get(member));
|
||||
for (const result of results.slice(1)) {
|
||||
if (results[0] !== undefined && result !== undefined) this.union(results[0], result);
|
||||
else if (results[0] !== undefined) this.markOpen(results[0]);
|
||||
else if (result !== undefined) this.markOpen(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private slotOf(name: ts.Node): number | undefined {
|
||||
const symbol = this.host.symbolOf(name);
|
||||
return symbol ? this.bindingSlots.get(symbol) : undefined;
|
||||
}
|
||||
|
||||
/** The calls that can reach a visible function or method, with the
|
||||
* callee expression each one names. */
|
||||
private callsOf(fn: ts.Symbol): ts.CallExpression[] {
|
||||
const out: ts.CallExpression[] = [];
|
||||
for (const member of this.families.get(fn) ?? [fn]) {
|
||||
for (const reference of this.referencesOf(member)) {
|
||||
const parent = reference.parent;
|
||||
if (!parent) continue;
|
||||
const callee =
|
||||
ts.isPropertyAccessExpression(parent) && parent.name === reference ? parent : reference;
|
||||
const call = callee.parent;
|
||||
if (call && ts.isCallExpression(call) && call.expression === callee) out.push(call);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private connect(): void {
|
||||
for (const [symbol, slot] of this.bindingSlots) {
|
||||
const declaration = this.host.declarationOf(symbol);
|
||||
if (declaration && ts.isVariableDeclaration(declaration)) {
|
||||
const list = declaration.parent;
|
||||
const statement = list?.parent;
|
||||
if (
|
||||
!list ||
|
||||
!ts.isVariableDeclarationList(list) ||
|
||||
!statement ||
|
||||
!ts.isVariableStatement(statement) ||
|
||||
ts.getCombinedModifierFlags(declaration) & ts.ModifierFlags.Export
|
||||
) {
|
||||
this.markOpen(slot);
|
||||
} else if (declaration.initializer) {
|
||||
this.source(declaration.initializer, slot);
|
||||
}
|
||||
} else if (declaration && ts.isParameter(declaration)) {
|
||||
if (declaration.questionToken) this.markOpen(slot);
|
||||
if (declaration.initializer) this.source(declaration.initializer, slot);
|
||||
const fn = declaration.parent as Callable;
|
||||
const index = fn.parameters.indexOf(declaration);
|
||||
const fnSymbol = fn.name ? this.host.symbolOf(fn.name) : null;
|
||||
for (const call of fnSymbol ? this.callsOf(fnSymbol) : []) {
|
||||
const arg = call.arguments[index];
|
||||
if (arg) this.source(arg, slot);
|
||||
else if (!declaration.initializer) this.markOpen(slot);
|
||||
}
|
||||
} else {
|
||||
this.markOpen(slot);
|
||||
}
|
||||
if (this.shorthandNames.has(symbol.name)) this.markOpen(slot);
|
||||
for (const reference of this.referencesOf(symbol)) {
|
||||
if (declaration && (declaration as { name?: ts.Node }).name === reference) continue;
|
||||
if (!this.use(reference, slot)) this.markOpen(slot);
|
||||
}
|
||||
}
|
||||
for (const [symbol, slot] of this.returnSlots) {
|
||||
const decl = this.functions.get(symbol)!;
|
||||
for (const expression of returnExpressions(decl)) this.source(expression, slot);
|
||||
}
|
||||
}
|
||||
|
||||
/** Joins the slot with the class a value comes from, or opens the slot
|
||||
* when the value can be an object some other storage already holds. */
|
||||
private source(node: ts.Expression, slot: number): void {
|
||||
const e = peel(node);
|
||||
if (ts.isObjectLiteralExpression(e)) return;
|
||||
if (ts.isIdentifier(e)) {
|
||||
const symbol = this.host.symbolOf(e);
|
||||
const other = symbol ? this.bindingSlots.get(symbol) : undefined;
|
||||
if (other !== undefined) this.union(slot, other);
|
||||
else this.markOpen(slot);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
ts.isCallExpression(e) &&
|
||||
(ts.isIdentifier(e.expression) || ts.isPropertyAccessExpression(e.expression))
|
||||
) {
|
||||
const callee = ts.isIdentifier(e.expression) ? e.expression : e.expression.name;
|
||||
const symbol = this.host.symbolOf(callee);
|
||||
const other = symbol ? this.returnSlots.get(symbol) : undefined;
|
||||
if (other !== undefined) this.union(slot, other);
|
||||
else this.markOpen(slot);
|
||||
return;
|
||||
}
|
||||
if (ts.isConditionalExpression(e)) {
|
||||
this.source(e.whenTrue, slot);
|
||||
this.source(e.whenFalse, slot);
|
||||
return;
|
||||
}
|
||||
if (ts.isBinaryExpression(e)) {
|
||||
const op = e.operatorToken.kind;
|
||||
if (
|
||||
op === ts.SyntaxKind.QuestionQuestionToken ||
|
||||
op === ts.SyntaxKind.BarBarToken ||
|
||||
op === ts.SyntaxKind.AmpersandAmpersandToken
|
||||
) {
|
||||
this.source(e.left, slot);
|
||||
this.source(e.right, slot);
|
||||
return;
|
||||
}
|
||||
if (op === ts.SyntaxKind.CommaToken) {
|
||||
this.source(e.right, slot);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.markOpen(slot);
|
||||
}
|
||||
|
||||
/** True when this reference to a member only reads or writes its fields,
|
||||
* passes the record to another member (a flow `source` joins), or
|
||||
* inspects it without keeping or converting it. */
|
||||
private use(reference: ts.Identifier, slot: number): boolean {
|
||||
let value: ts.Node = reference;
|
||||
for (;;) {
|
||||
const user = value.parent;
|
||||
if (!user) return false;
|
||||
if (
|
||||
ts.isParenthesizedExpression(user) ||
|
||||
ts.isNonNullExpression(user) ||
|
||||
ts.isAsExpression(user) ||
|
||||
ts.isSatisfiesExpression(user) ||
|
||||
ts.isTypeAssertion(user)
|
||||
) {
|
||||
value = user;
|
||||
continue;
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(user) && user.expression === value) return true;
|
||||
if (ts.isElementAccessExpression(user) && user.expression === value) return true;
|
||||
if (ts.isSpreadAssignment(user)) return true;
|
||||
if (ts.isTemplateSpan(user) || ts.isTypeOfExpression(user)) return true;
|
||||
if (ts.isExpressionStatement(user)) return true;
|
||||
if (ts.isPrefixUnaryExpression(user) && user.operator === ts.SyntaxKind.ExclamationToken)
|
||||
return true;
|
||||
if (
|
||||
(ts.isIfStatement(user) || ts.isWhileStatement(user) || ts.isDoStatement(user)) &&
|
||||
user.expression === value
|
||||
)
|
||||
return true;
|
||||
if (ts.isForInStatement(user) && user.expression === value) return true;
|
||||
if (ts.isConditionalExpression(user)) {
|
||||
if (user.condition === value) return true;
|
||||
value = user;
|
||||
continue;
|
||||
}
|
||||
if (ts.isBinaryExpression(user)) {
|
||||
const op = user.operatorToken.kind;
|
||||
if (
|
||||
op === ts.SyntaxKind.QuestionQuestionToken ||
|
||||
op === ts.SyntaxKind.BarBarToken ||
|
||||
op === ts.SyntaxKind.AmpersandAmpersandToken
|
||||
) {
|
||||
// The record can become the result: follow the result's use.
|
||||
value = user;
|
||||
continue;
|
||||
}
|
||||
if (op === ts.SyntaxKind.CommaToken) {
|
||||
if (user.left === value) return true;
|
||||
value = user;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.EqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsToken
|
||||
) {
|
||||
const other = user.left === value ? user.right : user.left;
|
||||
const e = peel(other);
|
||||
if (e.kind === ts.SyntaxKind.NullKeyword || isUndefined(e)) return true;
|
||||
// Comparing identity needs both records in one layout.
|
||||
this.source(other, slot);
|
||||
return true;
|
||||
}
|
||||
const assigns =
|
||||
op === ts.SyntaxKind.EqualsToken ||
|
||||
op === ts.SyntaxKind.QuestionQuestionEqualsToken ||
|
||||
op === ts.SyntaxKind.BarBarEqualsToken ||
|
||||
op === ts.SyntaxKind.AmpersandAmpersandEqualsToken;
|
||||
// A record assigned to a member joins it when the member's own
|
||||
// reference is visited, below.
|
||||
if (assigns && user.right === value) return this.isMember(user.left);
|
||||
if (assigns && user.left === value && value === reference) {
|
||||
this.source(user.right, slot);
|
||||
return true;
|
||||
}
|
||||
if (op === ts.SyntaxKind.InKeyword || op === ts.SyntaxKind.InstanceOfKeyword) return true;
|
||||
return false;
|
||||
}
|
||||
if (ts.isVariableDeclaration(user) && user.initializer === value) {
|
||||
if (ts.isObjectBindingPattern(user.name)) return true;
|
||||
return this.isMember(user.name);
|
||||
}
|
||||
if (ts.isReturnStatement(user)) {
|
||||
const fn = enclosingFunction(user);
|
||||
const symbol =
|
||||
fn && (ts.isFunctionDeclaration(fn) || ts.isMethodDeclaration(fn)) && fn.name
|
||||
? this.host.symbolOf(fn.name)
|
||||
: null;
|
||||
return !!symbol && this.returnSlots.has(symbol);
|
||||
}
|
||||
if (ts.isCallExpression(user) && user.expression !== value) {
|
||||
const callee = peel(user.expression);
|
||||
if (ts.isIdentifier(callee)) {
|
||||
if (READING_FUNCTIONS.has(callee.text) && this.host.isGlobal(callee, callee.text))
|
||||
return true;
|
||||
const symbol = this.host.symbolOf(callee);
|
||||
const fn = symbol ? this.functions.get(symbol) : undefined;
|
||||
if (!fn) return false;
|
||||
const parameter = fn.parameters[user.arguments.indexOf(value as ts.Expression)];
|
||||
return !!parameter && this.isMember(parameter.name);
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(callee)) {
|
||||
const symbol = this.host.symbolOf(callee.name);
|
||||
const method = symbol ? this.functions.get(symbol) : undefined;
|
||||
if (method) {
|
||||
const parameter = method.parameters[user.arguments.indexOf(value as ts.Expression)];
|
||||
return !!parameter && this.isMember(parameter.name);
|
||||
}
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression)) {
|
||||
const methods = READING_CALLS.get(callee.expression.text);
|
||||
return (
|
||||
!!methods &&
|
||||
methods.has(callee.name.text) &&
|
||||
this.host.isGlobal(callee.expression, callee.expression.text)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private isMember(node: ts.Node): boolean {
|
||||
if (!ts.isIdentifier(node)) return false;
|
||||
const symbol = this.host.symbolOf(node);
|
||||
return !!symbol && this.bindingSlots.has(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
/** The return expressions of a function body, excluding nested functions. */
|
||||
export function returnExpressions(fn: ts.FunctionLikeDeclaration): ts.Expression[] {
|
||||
const out: ts.Expression[] = [];
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isReturnStatement(node)) {
|
||||
if (node.expression) out.push(node.expression);
|
||||
return;
|
||||
}
|
||||
if (ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node))
|
||||
return;
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
fn.body?.forEachChild(visit);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The identifier names a member: a property, method or accessor in a
|
||||
* class, interface, type or object literal, a property read, or an object
|
||||
* pattern key. */
|
||||
function isMemberName(id: ts.Identifier, parent: ts.Node): boolean {
|
||||
if (ts.isPropertyAccessExpression(parent)) return parent.name === id;
|
||||
if (ts.isBindingElement(parent))
|
||||
return (
|
||||
parent.propertyName === id ||
|
||||
(!parent.propertyName && parent.name === id && ts.isObjectBindingPattern(parent.parent))
|
||||
);
|
||||
return (
|
||||
(ts.isMethodDeclaration(parent) ||
|
||||
ts.isPropertyDeclaration(parent) ||
|
||||
ts.isMethodSignatureDeclaration(parent) ||
|
||||
ts.isPropertySignatureDeclaration(parent) ||
|
||||
ts.isGetAccessorDeclaration(parent) ||
|
||||
ts.isSetAccessorDeclaration(parent) ||
|
||||
ts.isPropertyAssignment(parent) ||
|
||||
ts.isShorthandPropertyAssignment(parent)) &&
|
||||
(parent as { name?: ts.Node }).name === id
|
||||
);
|
||||
}
|
||||
|
||||
function enclosingFunction(node: ts.Node): ts.FunctionLikeDeclaration | null {
|
||||
for (let n = node.parent; n; n = n.parent)
|
||||
if (ts.isFunctionLike(n)) return n as ts.FunctionLikeDeclaration;
|
||||
return null;
|
||||
}
|
||||
|
||||
function usesArguments(body: ts.Node): boolean {
|
||||
let found = false;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (found) return;
|
||||
if (ts.isIdentifier(node) && node.text === "arguments") found = true;
|
||||
else if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node))
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
body.forEachChild(visit);
|
||||
return found;
|
||||
}
|
||||
|
||||
function isUndefined(node: ts.Expression): boolean {
|
||||
return ts.isIdentifier(node) && node.text === "undefined";
|
||||
}
|
||||
|
||||
function isJsFile(file: ts.SourceFile): boolean {
|
||||
return /\.(c|m)?jsx?$/.test(file.fileName);
|
||||
}
|
||||
|
||||
function peel(node: ts.Expression): ts.Expression {
|
||||
let e = node;
|
||||
while (
|
||||
ts.isParenthesizedExpression(e) ||
|
||||
ts.isAsExpression(e) ||
|
||||
ts.isTypeAssertion(e) ||
|
||||
ts.isSatisfiesExpression(e) ||
|
||||
ts.isNonNullExpression(e)
|
||||
)
|
||||
e = e.expression;
|
||||
return e;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5325-record-boundary-returns.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5326-record-boundary-params.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5327-record-boundary-identity.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5328-record-boundary-open.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5329-record-boundary-recursion.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5330-record-boundary-fields.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5331-record-boundary-methods.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Functions whose annotated or inferred return is a record built from
|
||||
// unchecked element reads return `undefined` in each field a missing
|
||||
// element leaves empty, as Node does.
|
||||
interface Pair {
|
||||
left: number;
|
||||
right: number;
|
||||
}
|
||||
function pairAt(values: number[], i: number): Pair {
|
||||
return { left: values[i], right: values[i + 1] };
|
||||
}
|
||||
function choose(values: number[], i: number, flip: boolean): Pair {
|
||||
return flip ? { left: values[i + 1], right: values[i] } : pairAt(values, i);
|
||||
}
|
||||
function inferred(values: number[], i: number) {
|
||||
return { left: values[i - 1], right: values[i] };
|
||||
}
|
||||
function forward(values: number[], i: number): Pair {
|
||||
const pair = pairAt(values, i);
|
||||
return pair;
|
||||
}
|
||||
|
||||
const values = [3, 1, 4];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const p = pairAt(values, i);
|
||||
const c = choose(values, i, i % 2 === 1);
|
||||
const q = inferred(values, i);
|
||||
const f = forward(values, i);
|
||||
console.log(p.left, p.right, c.left ?? "none", c.right ?? "none", q.left, f.right);
|
||||
console.log(JSON.stringify(p), JSON.stringify(q), String(p.right + 1), p.right === undefined);
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const p = pairAt(values, i);
|
||||
total += (p.left ?? 0) * 10 + (p.right ?? 0);
|
||||
}
|
||||
console.log(total);
|
||||
@@ -0,0 +1,32 @@
|
||||
// Record parameters receive literals built from unchecked reads, call
|
||||
// results and bindings; each missing element reads as undefined inside
|
||||
// the callee.
|
||||
interface Sample {
|
||||
value: number;
|
||||
weight: number;
|
||||
label: string;
|
||||
}
|
||||
function describe(s: Sample): string {
|
||||
const scaled = s.value * (s.weight ?? 1);
|
||||
return `${s.label}:${s.value ?? "?"}:${s.weight}:${Number.isNaN(scaled) ? "nan" : scaled}`;
|
||||
}
|
||||
function heavier(a: Sample, b: Sample): Sample {
|
||||
return (a.weight ?? 0) >= (b.weight ?? 0) ? a : b;
|
||||
}
|
||||
function sampleAt(values: number[], weights: number[], i: number): Sample {
|
||||
return { value: values[i], weight: weights[i], label: `s${i}` };
|
||||
}
|
||||
|
||||
const values = [5, 6, 7];
|
||||
const weights = [0.5, 2];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
console.log(describe({ value: values[i], weight: weights[i], label: `lit${i}` }));
|
||||
const made = sampleAt(values, weights, i);
|
||||
console.log(describe(made), describe(heavier(made, { value: 1, weight: 1, label: "one" })));
|
||||
}
|
||||
const fixed: Sample = { value: 9, weight: 3, label: "fixed" };
|
||||
console.log(describe(fixed), describe(heavier(fixed, sampleAt(values, weights, 3))).length);
|
||||
console.log(
|
||||
[0, 1, 2, 3].map((i) => describe(sampleAt(values, weights, i))).join(" | "),
|
||||
JSON.stringify(sampleAt(values, weights, 2)),
|
||||
);
|
||||
@@ -0,0 +1,35 @@
|
||||
// A record keeps its identity across parameters and returns that some
|
||||
// caller fills from unchecked reads: writes through a parameter reach the
|
||||
// caller's object, and a returned argument is the same object.
|
||||
interface Counter {
|
||||
count: number;
|
||||
name: string;
|
||||
}
|
||||
function bump(c: Counter): number {
|
||||
c.count = (c.count ?? 0) + 1;
|
||||
return c.count;
|
||||
}
|
||||
function same(c: Counter): Counter {
|
||||
return c;
|
||||
}
|
||||
function fresh(counts: number[], i: number): Counter {
|
||||
return { count: counts[i], name: `c${i}` };
|
||||
}
|
||||
|
||||
const counts = [10];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const c = fresh(counts, i);
|
||||
console.log(bump(c), c.count, same(c) === c);
|
||||
}
|
||||
const plain: Counter = { count: 5, name: "plain" };
|
||||
console.log(bump(plain), plain.count);
|
||||
const alias = same(plain);
|
||||
alias.count = 40;
|
||||
console.log(plain.count, alias === plain, bump(alias), plain.count);
|
||||
let pick = fresh(counts, 0);
|
||||
if (pick.count === 10) pick = plain;
|
||||
console.log(bump(pick), plain.count, pick === plain);
|
||||
const literal = { count: counts[3], name: "lit" };
|
||||
bump(literal);
|
||||
bump(literal);
|
||||
console.log(literal.count, literal.name);
|
||||
@@ -0,0 +1,28 @@
|
||||
// Records that reach storage outside their parameters and bindings (an
|
||||
// array, an object field, a callback) keep their declared layout and their
|
||||
// identity. These inputs hold every element they read.
|
||||
interface Cell {
|
||||
row: number;
|
||||
col: number;
|
||||
}
|
||||
function store(cell: Cell, out: Cell[]): Cell {
|
||||
out.push(cell);
|
||||
return cell;
|
||||
}
|
||||
function key(cell: Cell): string {
|
||||
return `${cell.row},${cell.col}`;
|
||||
}
|
||||
const rows = [1, 2, 3];
|
||||
const cols = [4, 5, 6];
|
||||
const out: Cell[] = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const cell = { row: rows[i], col: cols[i] };
|
||||
const kept = store(cell, out);
|
||||
cell.col += 10;
|
||||
console.log(kept === cell, out[i] === cell, key(out[i]!));
|
||||
}
|
||||
const holder: { cell: Cell } = { cell: { row: rows[0], col: cols[0] } };
|
||||
const view = holder.cell;
|
||||
view.row = 99;
|
||||
console.log(holder.cell.row, view === holder.cell);
|
||||
console.log(out.map(key).join(";"), key({ row: rows[1], col: cols[2] }));
|
||||
@@ -0,0 +1,25 @@
|
||||
// Widened records through recursive and nested calls: each level builds
|
||||
// its argument from unchecked reads that run past the end of the input.
|
||||
interface Span {
|
||||
lo: number;
|
||||
hi: number;
|
||||
}
|
||||
function extend(span: Span, values: number[], i: number): Span {
|
||||
if (i > values.length) return span;
|
||||
const next: Span = { lo: Math.min(span.lo ?? Infinity, values[i] ?? Infinity), hi: values[i + 1] };
|
||||
return extend(next, values, i + 1);
|
||||
}
|
||||
function size(span: Span): number {
|
||||
return (span.hi ?? span.lo ?? 0) - (span.lo ?? 0);
|
||||
}
|
||||
function even(span: Span, n: number): boolean {
|
||||
return n === 0 ? span.hi === undefined : odd({ lo: span.lo, hi: span.hi }, n - 1);
|
||||
}
|
||||
function odd(span: Span, n: number): boolean {
|
||||
return n === 0 ? span.hi !== undefined : even(span, n - 1);
|
||||
}
|
||||
const values = [7, 3, 9, 1];
|
||||
const result = extend({ lo: values[0], hi: values[1] }, values, 0);
|
||||
console.log(result.lo, result.hi, size(result), JSON.stringify(result));
|
||||
console.log(size(extend({ lo: 2, hi: 5 }, values, 2)), even(result, 3), odd(result, 2));
|
||||
for (let i = 0; i < 6; i++) console.log(size({ lo: values[i], hi: values[i + 2] }));
|
||||
@@ -0,0 +1,24 @@
|
||||
// Boolean and string fields from unchecked reads cross the same
|
||||
// boundaries; spreads, destructuring and keyed reads of the widened
|
||||
// records see undefined where an element is missing.
|
||||
interface Entry {
|
||||
name: string;
|
||||
active: boolean;
|
||||
rank: number;
|
||||
}
|
||||
function entryAt(names: string[], flags: boolean[], ranks: number[], i: number): Entry {
|
||||
return { name: names[i], active: flags[i], rank: ranks[i] };
|
||||
}
|
||||
function render(e: Entry): string {
|
||||
const { name, active } = e;
|
||||
const copy = { ...e, seen: true };
|
||||
const keys = Object.keys(copy).join("/");
|
||||
return `${name ?? "anon"}|${active === undefined ? "?" : active}|${e["rank"]}|${keys}|${JSON.stringify(copy)}`;
|
||||
}
|
||||
const names = ["ada", "bo"];
|
||||
const flags = [true];
|
||||
const ranks = [1, 2, 3];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
console.log(render(entryAt(names, flags, ranks, i)));
|
||||
console.log(render({ name: names[i], active: flags[i] ?? false, rank: ranks[i] }));
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Method parameters and results share one layout across their dispatch
|
||||
// slot. A record built from unchecked reads reaches a method with its
|
||||
// missing fields undefined, and writes through the parameter reach the
|
||||
// caller's object.
|
||||
interface Reading {
|
||||
value: number;
|
||||
scale: number;
|
||||
}
|
||||
class Meter {
|
||||
total = 0;
|
||||
record(r: Reading): number {
|
||||
this.total += (r.value ?? 0) * (r.scale ?? 1);
|
||||
r.value = (r.value ?? 0) + 1;
|
||||
return r.value;
|
||||
}
|
||||
readingAt(values: number[], scales: number[], i: number): Reading {
|
||||
return { value: values[i], scale: scales[i] };
|
||||
}
|
||||
}
|
||||
class DoubleMeter extends Meter {
|
||||
record(r: Reading): number {
|
||||
return super.record(r) * 2;
|
||||
}
|
||||
}
|
||||
const values = [3, 4];
|
||||
const scales = [10];
|
||||
const meters: Meter[] = [new Meter(), new DoubleMeter()];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
for (const meter of meters) {
|
||||
const literal = { value: values[i], scale: scales[i] };
|
||||
const made = meter.readingAt(values, scales, i);
|
||||
console.log(meter.record(literal), literal.value, meter.record(made), made.value, made.scale);
|
||||
}
|
||||
}
|
||||
const plain: Reading = { value: 1, scale: 2 };
|
||||
console.log(meters[1]!.record(plain), plain.value, meters.map((m) => m.total).join(","));
|
||||
@@ -185,6 +185,8 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => {
|
||||
"5232-inline-union-fields-dynamic.ts",
|
||||
"5234-inline-union-class-fields.ts",
|
||||
"5237-inline-union-frame-objects.ts",
|
||||
"5327-record-boundary-identity.ts",
|
||||
"5331-record-boundary-methods.ts",
|
||||
"1020-async-basics.ts",
|
||||
"1021-async-ordering.ts",
|
||||
"1022-async-exceptions.ts",
|
||||
|
||||
Reference in New Issue
Block a user