mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
merge: integrate compiler performance improvements
- Combine the latest frontend and LLVM converter improvements with static buffer and prototype support.
This commit is contained in:
@@ -89,7 +89,7 @@ import {
|
||||
traceArg,
|
||||
vAdapters,
|
||||
} from "./shapes.js";
|
||||
import type { ExprOf, LibCallExpr, LlStreamTypedRefAdapter, LlStreamTypedRefContext, LlValue } from "./expr-context.js";
|
||||
import type { ExprOf, LibCallExpr, LlStreamTypedRefAdapter, LlValue } from "./expr-context.js";
|
||||
|
||||
export { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
@@ -4698,19 +4698,10 @@ export class LlEmitter {
|
||||
}
|
||||
|
||||
liveDynRefAdapter(t: IrType): LlStreamTypedRefAdapter {
|
||||
const key = typeKey(t);
|
||||
const existing = this.liveDynRefAdapters.get(key);
|
||||
if (existing) return existing;
|
||||
if (!streamTypedRefEligible(t) && !isDynTypedRefType(t)) {
|
||||
throw new InternalCompilerError(`llvm emitter bug: live dyn ref of ${key}`);
|
||||
throw new InternalCompilerError(`llvm emitter bug: live dyn ref of ${typeKey(t)}`);
|
||||
}
|
||||
const prefix = `sc_ldr_${this.liveDynRefAdapters.size}`;
|
||||
return streamTypedRefMaterializeAdapter(
|
||||
this,
|
||||
t,
|
||||
{ prefix, adapters: this.liveDynRefAdapters },
|
||||
`${prefix}_materialize`,
|
||||
);
|
||||
return streamTypedRefMaterializeAdapter(this, t);
|
||||
}
|
||||
|
||||
liveDynUnionRefAdapter(
|
||||
@@ -4723,17 +4714,8 @@ export class LlEmitter {
|
||||
B: BlockBuilder,
|
||||
t: IrType,
|
||||
value: string,
|
||||
ctx: LlStreamTypedRefContext,
|
||||
): string {
|
||||
return streamTypedRefBoxValue(this, B, t, value, ctx);
|
||||
}
|
||||
|
||||
streamTypedRefMaterializeAdapter(
|
||||
t: IrType,
|
||||
ctx: LlStreamTypedRefContext,
|
||||
preferredSnapshot?: string,
|
||||
): LlStreamTypedRefAdapter {
|
||||
return streamTypedRefMaterializeAdapter(this, t, ctx, preferredSnapshot);
|
||||
return streamTypedRefBoxValue(this, B, t, value);
|
||||
}
|
||||
|
||||
streamFromArrayAdapter(
|
||||
|
||||
@@ -23,9 +23,3 @@ export interface LlStreamTypedRefAdapter {
|
||||
snapshot: string;
|
||||
commit: string;
|
||||
}
|
||||
|
||||
export interface LlStreamTypedRefContext {
|
||||
prefix: string;
|
||||
adapters: Map<string, LlStreamTypedRefAdapter>;
|
||||
unions?: Map<string, string>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { DYN, STRING, UNDEFINED_T, VOID, type IrFunction, type IrModule, type IrType } from "../../ir/ir.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
|
||||
function sharedGraph(roots: number): IrModule {
|
||||
const loc = { file: "live-references.ts", start: 0, end: 0 };
|
||||
const leaf: IrType = { kind: "record", shapeId: "leaf" };
|
||||
const root: IrType = { kind: "record", shapeId: "root" };
|
||||
const next: IrType = { kind: "union", unionId: "next" };
|
||||
const inputs: (IrType & { kind: "union" })[] = Array.from({ length: roots }, (_, index) => ({ kind: "union", unionId: `input${index}` }));
|
||||
return {
|
||||
irVersion: 13, sourceFile: loc.file, entry: "main",
|
||||
records: [
|
||||
{ id: "leaf", fields: [{ name: "name", type: STRING }, { name: "next", type: next }, { name: "children", type: { kind: "array", elem: leaf } }] },
|
||||
{ id: "root", fields: [{ name: "first", type: leaf }, { name: "second", type: leaf }] },
|
||||
],
|
||||
unions: [{ id: "next", arms: [leaf, UNDEFINED_T] }, ...inputs.map((type) => ({
|
||||
id: type.unionId, arms: [root, leaf, STRING],
|
||||
}))],
|
||||
functions: [
|
||||
{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc },
|
||||
...inputs.map((type, index): IrFunction => ({
|
||||
name: `box${index}`, params: [{ name: "value", localId: "value", type }], returnType: DYN,
|
||||
locals: [{ id: "value", name: "value", type, mutable: false }], loc,
|
||||
body: [{ kind: "return" as const, value: {
|
||||
kind: "dynFrom" as const, value: { kind: "varRef" as const, localId: "value", type, loc }, liveRef: true, type: DYN, loc,
|
||||
}, loc }],
|
||||
})),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
test("overlapping union roots share the recursive materialization graph", () => {
|
||||
for (const pointerBits of [32, 64] as const) {
|
||||
for (const roots of [2, 32]) {
|
||||
const llvm = emitLlvmModule(sharedGraph(roots), { pointerBits });
|
||||
const materializers = [...llvm.matchAll(/^define internal ptr @\S+\(ptr %p\).*; materialize live stream value (.+)$/gm)];
|
||||
// One record root, one recursive leaf, and its array, independent of
|
||||
// how many enclosing union arms reach the shared graph.
|
||||
expect(materializers).toHaveLength(3);
|
||||
expect(new Set(materializers.map((match) => match[1])).size).toBe(3);
|
||||
expect(llvm.match(/; materialize live union value /g)).toHaveLength(roots + 1);
|
||||
const symbols = [...llvm.matchAll(/^define .*? @(\S+)\(/gm)].map((match) => match[1]);
|
||||
expect(new Set(symbols).size).toBe(symbols.length);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("converter registries belong to one emitted module", () => {
|
||||
const original = sharedGraph(2);
|
||||
const first = emitLlvmModule(original);
|
||||
const changed = sharedGraph(2);
|
||||
changed.records![0]!.fields.push({ name: "extra", type: STRING });
|
||||
const second = emitLlvmModule(changed);
|
||||
expect(second).not.toBe(first);
|
||||
expect(emitLlvmModule(original)).toBe(first);
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import { BlockBuilder } from "./blocks.js";
|
||||
import { classFieldIndex, classStructSym } from "./classes.js";
|
||||
import { llvmCommentText } from "./common.js";
|
||||
import { FN_ATTRS, llFieldType, releaseSym, traceArg, vAdapters } from "./shapes.js";
|
||||
import type { LlvmEmitterContext, LlStreamTypedRefAdapter, LlStreamTypedRefContext } from "./expr-context.js";
|
||||
import type { LlvmEmitterContext, LlStreamTypedRefAdapter } from "./expr-context.js";
|
||||
|
||||
export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): string {
|
||||
if (!isRefCounted(inner) || inner.kind === "dyn") {
|
||||
@@ -304,15 +304,7 @@ export function liveDynUnionRefAdapter(host: LlvmEmitterContext,
|
||||
|
||||
const adapters = new Map<number, LlStreamTypedRefAdapter>();
|
||||
for (const { arm, tag } of mutableArms) {
|
||||
const prefix = `${sym}_${tag}`;
|
||||
adapters.set(
|
||||
tag,
|
||||
host.streamTypedRefMaterializeAdapter(
|
||||
arm,
|
||||
{ prefix, adapters: new Map() },
|
||||
`${prefix}_materialize`,
|
||||
),
|
||||
);
|
||||
adapters.set(tag, host.liveDynRefAdapter(arm));
|
||||
}
|
||||
|
||||
const B = new BlockBuilder();
|
||||
@@ -406,107 +398,10 @@ export function liveDynUnionRefAdapter(host: LlvmEmitterContext,
|
||||
return sym;
|
||||
}
|
||||
|
||||
function nestedTypedRefUnionAdapter(
|
||||
host: LlvmEmitterContext,
|
||||
t: IrType & { kind: "union" },
|
||||
ctx: LlStreamTypedRefContext,
|
||||
): string {
|
||||
const key = typeKey(t);
|
||||
if (ctx.unions === undefined) ctx.unions = new Map<string, string>();
|
||||
const unions = ctx.unions;
|
||||
const existing = unions.get(key);
|
||||
if (existing) return existing;
|
||||
const def = host.unionsById.get(t.unionId);
|
||||
if (!def) {
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: typed-ref union ${t.unionId} is undeclared`,
|
||||
);
|
||||
}
|
||||
const sym = `${ctx.prefix}_union_${unions.size}`;
|
||||
unions.set(key, sym);
|
||||
|
||||
const B = new BlockBuilder();
|
||||
const tagPtr = B.tmp();
|
||||
const tagValue = B.tmp();
|
||||
B.line(`${tagPtr} = getelementptr inbounds %ScrUnion, ptr %u, i64 0, i32 1`);
|
||||
B.line(`${tagValue} = load i32, ptr ${tagPtr}`);
|
||||
const bad = B.newLabel("tr.union.bad");
|
||||
const labels = def.arms.map(() => B.newLabel("tr.union.arm"));
|
||||
B.terminate(
|
||||
`switch i32 ${tagValue}, label %${bad} [ ${def.arms.map((_, index) => `i32 ${index}, label %${labels[index]}`).join(" ")} ]`,
|
||||
);
|
||||
def.arms.forEach((arm, index) => {
|
||||
B.startBlock(labels[index]!);
|
||||
if (streamTypedRefEligible(arm) || isDynTypedRefType(arm)) {
|
||||
const adapter = host.streamTypedRefMaterializeAdapter(arm, ctx);
|
||||
const rc = vAdapters(host.shapeHost, arm);
|
||||
const armKey = typeKey(arm);
|
||||
const payloadPtr = B.tmp();
|
||||
const payload = B.tmp();
|
||||
const boxed = B.tmp();
|
||||
B.line(`${payloadPtr} = getelementptr inbounds %ScrUnion, ptr %u, i64 0, i32 5`);
|
||||
B.line(`${payload} = load ptr, ptr ${payloadPtr}`);
|
||||
B.line(
|
||||
`${boxed} = call ptr ${typedRefConstructor(host.shapeHost, arm)}(ptr ${payload}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(armKey)}, ${host.sizeType} ${Buffer.byteLength(armKey, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
|
||||
);
|
||||
B.terminate(`ret ptr ${boxed}`);
|
||||
} else if (arm.kind === "undefinedT") {
|
||||
host.declare(`declare ptr @scr_dyn_undefined()`);
|
||||
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
|
||||
const undef = B.tmp();
|
||||
const boxed = B.tmp();
|
||||
B.line(`${undef} = call ptr @scr_dyn_undefined()`);
|
||||
B.line(`${boxed} = call ptr @scr_dyn_retain_v(ptr ${undef})`);
|
||||
B.terminate(`ret ptr ${boxed}`);
|
||||
} else if (arm.kind === "nullT") {
|
||||
host.declare(`declare ptr @scr_dyn_new_null()`);
|
||||
const boxed = B.tmp();
|
||||
B.line(`${boxed} = call ptr @scr_dyn_new_null()`);
|
||||
B.terminate(`ret ptr ${boxed}`);
|
||||
} else if (arm.kind === "f64") {
|
||||
host.declare(`declare double @scr_union_get_f64(ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_new_num(double)`);
|
||||
const value = B.tmp();
|
||||
const boxed = B.tmp();
|
||||
B.line(`${value} = call double @scr_union_get_f64(ptr %u)`);
|
||||
B.line(`${boxed} = call ptr @scr_dyn_new_num(double ${value})`);
|
||||
B.terminate(`ret ptr ${boxed}`);
|
||||
} else if (arm.kind === "bool") {
|
||||
host.declare(`declare zeroext i1 @scr_union_get_bool(ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_new_bool(i1 zeroext)`);
|
||||
const value = B.tmp();
|
||||
const boxed = B.tmp();
|
||||
B.line(`${value} = call zeroext i1 @scr_union_get_bool(ptr %u)`);
|
||||
B.line(`${boxed} = call ptr @scr_dyn_new_bool(i1 ${value})`);
|
||||
B.terminate(`ret ptr ${boxed}`);
|
||||
} else {
|
||||
const payloadPtr = B.tmp();
|
||||
const payload = B.tmp();
|
||||
const boxed = B.tmp();
|
||||
B.line(`${payloadPtr} = getelementptr inbounds %ScrUnion, ptr %u, i64 0, i32 5`);
|
||||
B.line(`${payload} = load ptr, ptr ${payloadPtr}`);
|
||||
B.line(`${boxed} = call ptr @${host.dyn.toDynHelper(arm)}(ptr ${payload})`);
|
||||
B.terminate(`ret ptr ${boxed}`);
|
||||
}
|
||||
});
|
||||
B.startBlock(bad);
|
||||
host.declare(`declare void @scr_trap(ptr)`);
|
||||
B.line(`call void @scr_trap(ptr ${host.cstr("scriptc: internal error: invalid union tag\n")})`);
|
||||
B.terminate(`unreachable`);
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal ptr @${sym}(ptr %u) ${FN_ATTRS} { ; typed-ref union ${key}`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return sym;
|
||||
}
|
||||
|
||||
export function streamTypedRefBoxValue(host: LlvmEmitterContext,
|
||||
B: BlockBuilder,
|
||||
t: IrType,
|
||||
value: string,
|
||||
ctx: LlStreamTypedRefContext,
|
||||
): string {
|
||||
const boxed = B.tmp();
|
||||
if (
|
||||
@@ -515,7 +410,7 @@ export function streamTypedRefBoxValue(host: LlvmEmitterContext,
|
||||
(arm) => streamTypedRefEligible(arm) || isDynTypedRefType(arm),
|
||||
) ?? false)
|
||||
) {
|
||||
B.line(`${boxed} = call ptr @${nestedTypedRefUnionAdapter(host, t, ctx)}(ptr ${value})`);
|
||||
B.line(`${boxed} = call ptr @${host.liveDynUnionRefAdapter(t)}(ptr ${value})`);
|
||||
return boxed;
|
||||
}
|
||||
if (!streamTypedRefEligible(t) && !isDynTypedRefType(t)) {
|
||||
@@ -529,7 +424,7 @@ export function streamTypedRefBoxValue(host: LlvmEmitterContext,
|
||||
);
|
||||
return boxed;
|
||||
}
|
||||
const nested = host.streamTypedRefMaterializeAdapter(t, ctx);
|
||||
const nested = host.liveDynRefAdapter(t);
|
||||
const rc = vAdapters(host.shapeHost, t);
|
||||
const key = typeKey(t);
|
||||
|
||||
@@ -541,16 +436,15 @@ export function streamTypedRefBoxValue(host: LlvmEmitterContext,
|
||||
|
||||
export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
|
||||
t: IrType,
|
||||
ctx: LlStreamTypedRefContext,
|
||||
preferredSnapshot?: string,
|
||||
): LlStreamTypedRefAdapter {
|
||||
const key = typeKey(t);
|
||||
const existing = ctx.adapters.get(key);
|
||||
const existing = host.liveDynRefAdapters.get(key);
|
||||
if (existing) return existing;
|
||||
const snapshot = preferredSnapshot ??
|
||||
`${ctx.prefix}_nested_${ctx.adapters.size}`;
|
||||
// The converter depends only on the static type. Intern the complete
|
||||
// graph across roots and union arms so recursive types emit once.
|
||||
const snapshot = `sc_ldr_${host.liveDynRefAdapters.size}_materialize`;
|
||||
const adapter: LlStreamTypedRefAdapter = { snapshot, commit: "null" };
|
||||
ctx.adapters.set(key, adapter);
|
||||
host.liveDynRefAdapters.set(key, adapter);
|
||||
if (isDynTypedRefType(t)) {
|
||||
const fields = host.classMeta.get(t.className)?.def.fields;
|
||||
if (fields && !classDynViewSupported(fields, (id) => host.recordsById.get(id), (id) => host.unionsById.get(id))) {
|
||||
@@ -596,7 +490,7 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
|
||||
B.line(`${boolValue} = trunc i8 ${fieldValue} to i1`);
|
||||
fieldValue = boolValue;
|
||||
}
|
||||
const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue, ctx);
|
||||
const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue);
|
||||
B.line(`call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`);
|
||||
}
|
||||
if (meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)) {
|
||||
@@ -650,7 +544,6 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
|
||||
B,
|
||||
field.type,
|
||||
fieldValue,
|
||||
ctx,
|
||||
);
|
||||
B.line(`call void @scr_dyn_arr_push(ptr ${out}, ptr ${boxed})`);
|
||||
}
|
||||
@@ -682,7 +575,6 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
|
||||
B,
|
||||
field.type,
|
||||
fieldValue,
|
||||
ctx,
|
||||
);
|
||||
B.line(`call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`);
|
||||
}
|
||||
@@ -730,7 +622,7 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
|
||||
value = B.tmp();
|
||||
B.line(`${value} = call ptr @scr_arr_get_ref(ptr %p, double ${index}) ; +1`);
|
||||
}
|
||||
const boxed = host.streamTypedRefBoxValue(B, elem, value, ctx);
|
||||
const boxed = host.streamTypedRefBoxValue(B, elem, value);
|
||||
B.line(`call void @scr_dyn_arr_push(ptr ${out}, ptr ${boxed})`);
|
||||
if (isRefCounted(elem)) {
|
||||
B.line(`call void ${releaseSym(host.shapeHost, elem)}(ptr ${value})`);
|
||||
@@ -779,7 +671,7 @@ export function streamFromArrayAdapter(host: LlvmEmitterContext,
|
||||
elem.kind !== "dyn" &&
|
||||
elem.kind !== "string" &&
|
||||
elem.kind !== "union";
|
||||
const snapshot = `${sym}_materialize`;
|
||||
let snapshot = `${sym}_materialize`;
|
||||
let value: string;
|
||||
if (elem.kind === "f64") {
|
||||
host.declare(`declare double @scr_arr_get_f64(ptr, double)`);
|
||||
@@ -872,11 +764,9 @@ export function streamFromArrayAdapter(host: LlvmEmitterContext,
|
||||
const keyPtr = host.cstr(key);
|
||||
let commit: string;
|
||||
if (streamTypedRefEligible(elem)) {
|
||||
commit = host.streamTypedRefMaterializeAdapter(
|
||||
elem,
|
||||
{ prefix: snapshot, adapters: new Map() },
|
||||
snapshot,
|
||||
).commit;
|
||||
const adapter = host.liveDynRefAdapter(elem);
|
||||
snapshot = adapter.snapshot;
|
||||
commit = adapter.commit;
|
||||
} else {
|
||||
commit = host.streamTypedRefCommitAdapter(elem, snapshot);
|
||||
host.resolveThunkDefs.push(
|
||||
|
||||
@@ -868,14 +868,14 @@ export function provenanceElidedConstDecl(lowerer: Lowerer, decl: ts.VariableDec
|
||||
}
|
||||
|
||||
function withRuntimeOptionalScope<T>(lowerer: Lowerer, lower: () => T): T {
|
||||
const optionalOnEntry = [...lowerer.runtimeOptionalLocals];
|
||||
lowerer.runtimeOptionalLocals.beginScope();
|
||||
try {
|
||||
return lower();
|
||||
} finally {
|
||||
// A guard inside one branch or loop body proves presence only for
|
||||
// its remaining statements. Keep assignments that reactivated a
|
||||
// slot, and restore every absence possibility from the outer scope.
|
||||
for (const local of optionalOnEntry) lowerer.runtimeOptionalLocals.add(local);
|
||||
lowerer.runtimeOptionalLocals.endScope();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { RuntimeOptionalLocals } from "./runtime-optional-locals.js";
|
||||
import { sanitizeUnregisteredClassTypes } from "./sanitize-class-types.js";
|
||||
import { buildUnionNarrow } from "./union-narrow.js";
|
||||
import { planUnionRetag, buildUnionRetag, planRecordUnionWrap, buildRecordUnionWrap } from "./union-retag.js";
|
||||
@@ -1242,7 +1243,7 @@ export class Lowerer {
|
||||
/** Locals widened beyond the checker's type because an inferred indexed
|
||||
* read can be absent at runtime. Bare reads preserve that union until a
|
||||
* surrounding JavaScript guard/default consumes it. */
|
||||
readonly runtimeOptionalLocals = new Set<IrLocal>();
|
||||
readonly runtimeOptionalLocals = new RuntimeOptionalLocals();
|
||||
/** An effectful switch test can change the original binding after the
|
||||
* discriminant was captured. A case match narrows the captured value,
|
||||
* not that mutable binding; preserve its tagged representation on reads. */
|
||||
@@ -2922,8 +2923,15 @@ export class Lowerer {
|
||||
}
|
||||
return e;
|
||||
};
|
||||
const callableSymbols = new Map<ts.Expression, ts.Symbol | null>();
|
||||
const callableSymbolOf = (node: ts.Expression): ts.Symbol | null => {
|
||||
const cached = callableSymbols.get(node);
|
||||
if (cached !== undefined) return cached;
|
||||
let symbol = symbolOf(ts.isPropertyAccessExpression(node) ? node.name : node);
|
||||
if (symbol === null || signatureBySymbol.has(symbol)) {
|
||||
callableSymbols.set(node, symbol);
|
||||
return symbol;
|
||||
}
|
||||
const seen = new Set<ts.Symbol>();
|
||||
while (symbol && !signatureBySymbol.has(symbol) && !seen.has(symbol)) {
|
||||
seen.add(symbol);
|
||||
@@ -2934,6 +2942,7 @@ export class Lowerer {
|
||||
symbol = symbolOf(peel(declaration.initializer));
|
||||
} else break;
|
||||
}
|
||||
callableSymbols.set(node, symbol);
|
||||
return symbol;
|
||||
};
|
||||
const explicitlyNonNull = (node: ts.Expression): boolean => {
|
||||
@@ -3022,9 +3031,10 @@ export class Lowerer {
|
||||
const e = peel(node);
|
||||
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.PlusToken) return null;
|
||||
const stringArrayRead = (part: ts.Expression): boolean => {
|
||||
const t = this.mapTypeOf(this.typeOf(part));
|
||||
const p = peel(part);
|
||||
if (t?.kind !== "string" || !ts.isElementAccessExpression(p)) return false;
|
||||
if (!ts.isElementAccessExpression(p)) return false;
|
||||
const t = this.mapTypeOf(this.typeOf(part));
|
||||
if (t?.kind !== "string") return false;
|
||||
const recv = this.mapTypeOf(this.typeOf(p.expression));
|
||||
return recv?.kind === "array" && recv.elem.kind === "string";
|
||||
};
|
||||
@@ -3343,50 +3353,52 @@ export class Lowerer {
|
||||
}
|
||||
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
||||
const callbackIndices = hofCallbackIndices(node.expression.name.text, node.arguments.length >= 2);
|
||||
const receiverNode = node.expression.expression;
|
||||
const receiverTs = this.typeOf(receiverNode);
|
||||
let receiver = this.mapTypeOf(receiverTs);
|
||||
// An array method after an optional-chain guard only runs on the
|
||||
// present receiver. The checker includes the chain's short-circuit
|
||||
// undefined in intermediate call types, so use the same narrowed
|
||||
// receiver the method lowerer sees when promoting HOF callbacks.
|
||||
if (
|
||||
callbackIndices !== null && receiver?.kind === "union" &&
|
||||
(node.expression.questionDotToken !== undefined || isOptionalChainTail(this, node))
|
||||
) {
|
||||
receiver = this.mapTypeOf(this.checker.getNonNullableType(receiverTs));
|
||||
}
|
||||
const callback = callbackIndices === null ? undefined : node.arguments[0];
|
||||
const tuple = receiver?.kind === "record" && this.shapes.get(receiver.shapeId)?.tuple === true;
|
||||
// A JS/evolving-any array may have acquired a precise FLOW type
|
||||
// at this call while its actual binding remains a checked-dynamic
|
||||
// array. Its callback consumes DYN values through runtime method
|
||||
// dispatch, so widening that callback to the native array value
|
||||
// ABI makes it impossible to box back into DYN. Judge a simple
|
||||
// binding by its declaration type, the same storage fact local
|
||||
// declaration lowering uses.
|
||||
let nativeArrayReceiver = receiver?.kind === "array" || tuple;
|
||||
if (nativeArrayReceiver && ts.isIdentifier(receiverNode)) {
|
||||
const receiverSymbol = symbolOf(receiverNode);
|
||||
const declaration = receiverSymbol ? this.checker.valueDeclarationOf(receiverSymbol) : undefined;
|
||||
if (declaration && ts.isVariableDeclaration(declaration)) {
|
||||
const declared = this.mapTypeOf(this.typeOf(declaration.name));
|
||||
if (declared?.kind !== "array" && !(declared?.kind === "record" && this.shapes.get(declared.shapeId)?.tuple === true)) {
|
||||
nativeArrayReceiver = false;
|
||||
const callback = node.arguments[0];
|
||||
if (callbackIndices !== null && callback !== undefined && !ts.isSpreadElement(callback)) {
|
||||
const receiverNode = node.expression.expression;
|
||||
const receiverTs = this.typeOf(receiverNode);
|
||||
let receiver = this.mapTypeOf(receiverTs);
|
||||
// An array method after an optional-chain guard only runs on the
|
||||
// present receiver. The checker includes the chain's short-circuit
|
||||
// undefined in intermediate call types, so use the same narrowed
|
||||
// receiver the method lowerer sees when promoting HOF callbacks.
|
||||
if (
|
||||
receiver?.kind === "union" &&
|
||||
(node.expression.questionDotToken !== undefined || isOptionalChainTail(this, node))
|
||||
) {
|
||||
receiver = this.mapTypeOf(this.checker.getNonNullableType(receiverTs));
|
||||
}
|
||||
const tuple = receiver?.kind === "record" && this.shapes.get(receiver.shapeId)?.tuple === true;
|
||||
// A JS/evolving-any array may have acquired a precise FLOW type
|
||||
// at this call while its actual binding remains a checked-dynamic
|
||||
// array. Its callback consumes DYN values through runtime method
|
||||
// dispatch, so widening that callback to the native array value
|
||||
// ABI makes it impossible to box back into DYN. Judge a simple
|
||||
// binding by its declaration type, the same storage fact local
|
||||
// declaration lowering uses.
|
||||
let nativeArrayReceiver = receiver?.kind === "array" || tuple;
|
||||
if (nativeArrayReceiver && ts.isIdentifier(receiverNode)) {
|
||||
const receiverSymbol = symbolOf(receiverNode);
|
||||
const declaration = receiverSymbol ? this.checker.valueDeclarationOf(receiverSymbol) : undefined;
|
||||
if (declaration && ts.isVariableDeclaration(declaration)) {
|
||||
const declared = this.mapTypeOf(this.typeOf(declaration.name));
|
||||
if (declared?.kind !== "array" && !(declared?.kind === "record" && this.shapes.get(declared.shapeId)?.tuple === true)) {
|
||||
nativeArrayReceiver = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nativeArrayReceiver && callback && !ts.isSpreadElement(callback)) {
|
||||
if (promoteHofCallback(callback, callbackIndices!)) changed = true;
|
||||
const method = node.expression.name.text;
|
||||
if ((method === "reduce" || method === "reduceRight") &&
|
||||
(node.arguments.length < 2 || callbackReturnsOptional(callback))) {
|
||||
if (promoteHofCallback(callback, [0, 1])) changed = true;
|
||||
const result = this.runtimeOptionalType(this.irTypeOf(node));
|
||||
const previous = this.runtimeOptionalReduceTypes.get(node);
|
||||
if (!previous || !typeEquals(previous, result)) {
|
||||
this.runtimeOptionalReduceTypes.set(node, result);
|
||||
changed = true;
|
||||
if (nativeArrayReceiver) {
|
||||
if (promoteHofCallback(callback, callbackIndices)) changed = true;
|
||||
const method = node.expression.name.text;
|
||||
if ((method === "reduce" || method === "reduceRight") &&
|
||||
(node.arguments.length < 2 || callbackReturnsOptional(callback))) {
|
||||
if (promoteHofCallback(callback, [0, 1])) changed = true;
|
||||
const result = this.runtimeOptionalType(this.irTypeOf(node));
|
||||
const previous = this.runtimeOptionalReduceTypes.get(node);
|
||||
if (!previous || !typeEquals(previous, result)) {
|
||||
this.runtimeOptionalReduceTypes.set(node, result);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { F64, type IrLocal } from "../../ir/ir.js";
|
||||
import { RuntimeOptionalLocals } from "./runtime-optional-locals.js";
|
||||
|
||||
const local = (id: string): IrLocal => ({ id, name: id, type: F64, mutable: true });
|
||||
|
||||
test("scope exit restores entry facts and keeps surviving assignments", () => {
|
||||
const facts = new RuntimeOptionalLocals();
|
||||
const outer = local("outer"), added = local("added"), temporary = local("temporary");
|
||||
facts.add(outer);
|
||||
facts.beginScope();
|
||||
expect(facts.delete(outer)).toBe(true);
|
||||
expect(facts.delete(outer)).toBe(false);
|
||||
facts.add(added);
|
||||
facts.add(temporary);
|
||||
facts.delete(temporary);
|
||||
facts.endScope();
|
||||
expect(facts.has(outer)).toBe(true);
|
||||
expect(facts.has(added)).toBe(true);
|
||||
expect(facts.has(temporary)).toBe(false);
|
||||
});
|
||||
|
||||
test("nested restores preserve the original entry generation", () => {
|
||||
const facts = new RuntimeOptionalLocals();
|
||||
const value = local("value");
|
||||
facts.add(value);
|
||||
facts.beginScope();
|
||||
facts.beginScope();
|
||||
facts.delete(value);
|
||||
facts.add(value);
|
||||
facts.endScope();
|
||||
facts.delete(value);
|
||||
facts.endScope();
|
||||
expect(facts.has(value)).toBe(true);
|
||||
});
|
||||
|
||||
test("an absent outer member added and removed inside a child stays absent until the outer exit", () => {
|
||||
const facts = new RuntimeOptionalLocals();
|
||||
const value = local("value");
|
||||
facts.add(value);
|
||||
facts.beginScope();
|
||||
facts.delete(value);
|
||||
facts.beginScope();
|
||||
facts.add(value);
|
||||
facts.delete(value);
|
||||
facts.endScope();
|
||||
expect(facts.has(value)).toBe(false);
|
||||
facts.endScope();
|
||||
expect(facts.has(value)).toBe(true);
|
||||
expect(() => facts.endScope()).toThrow("not open");
|
||||
});
|
||||
|
||||
test("nested operations agree with snapshot-and-union restoration", () => {
|
||||
const values = Array.from({ length: 12 }, (_, index) => local(String(index)));
|
||||
const facts = new RuntimeOptionalLocals();
|
||||
const reference = new Set<IrLocal>();
|
||||
const snapshots: IrLocal[][] = [];
|
||||
let random = 0xabc123;
|
||||
for (let step = 0; step < 12000; step++) {
|
||||
random = (Math.imul(random, 1664525) + 1013904223) >>> 0;
|
||||
const value = values[(random >>> 8) % values.length]!;
|
||||
const action = random % 6;
|
||||
if (action === 0 && snapshots.length < 12) {
|
||||
snapshots.push([...reference]);
|
||||
facts.beginScope();
|
||||
} else if (action === 1 && snapshots.length > 0) {
|
||||
for (const entry of snapshots.pop()!) reference.add(entry);
|
||||
facts.endScope();
|
||||
} else if (action < 4) {
|
||||
reference.add(value);
|
||||
facts.add(value);
|
||||
} else {
|
||||
expect(facts.delete(value)).toBe(reference.delete(value));
|
||||
}
|
||||
for (const entry of values) expect(facts.has(entry), `step ${step}, local ${entry.id}`).toBe(reference.has(entry));
|
||||
}
|
||||
while (snapshots.length > 0) {
|
||||
for (const entry of snapshots.pop()!) reference.add(entry);
|
||||
facts.endScope();
|
||||
}
|
||||
for (const entry of values) expect(facts.has(entry)).toBe(reference.has(entry));
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { IrLocal } from "../../ir/ir.js";
|
||||
|
||||
/** Absence facts survive assignments, but a branch-local guard must not
|
||||
* narrow an outer binding after that branch ends. Scope exit restores the
|
||||
* members present on entry while retaining new facts learned inside it. */
|
||||
export class RuntimeOptionalLocals {
|
||||
private generation = 0;
|
||||
private readonly active = new Map<IrLocal, number>();
|
||||
private readonly scopes: { generation: number; removed: Map<IrLocal, number> }[] = [];
|
||||
|
||||
has(local: IrLocal): boolean { return this.active.has(local); }
|
||||
|
||||
add(local: IrLocal): void {
|
||||
if (!this.active.has(local)) this.active.set(local, ++this.generation);
|
||||
}
|
||||
|
||||
delete(local: IrLocal): boolean {
|
||||
const generation = this.active.get(local);
|
||||
if (generation === undefined) return false;
|
||||
this.active.delete(local);
|
||||
const scope = this.scopes[this.scopes.length - 1];
|
||||
if (scope !== undefined && generation <= scope.generation) scope.removed.set(local, generation);
|
||||
return true;
|
||||
}
|
||||
|
||||
beginScope(): void {
|
||||
// Only removals of entry members need undoing. Recording their original
|
||||
// generation avoids copying all locals seen by the lowering pass.
|
||||
this.scopes.push({ generation: this.generation, removed: new Map() });
|
||||
}
|
||||
|
||||
endScope(): void {
|
||||
const scope = this.scopes.pop();
|
||||
if (scope === undefined) throw new Error("runtime optional scope is not open");
|
||||
for (const [local, generation] of scope.removed) {
|
||||
// A removed member may have been added again in a nested scope. Keep
|
||||
// its original age so an enclosing scope still recognizes it.
|
||||
const current = this.active.get(local);
|
||||
if (current === undefined || current > generation) this.active.set(local, generation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,13 +103,23 @@ export class AstFile {
|
||||
* properties are views over the wire; they never copy identity-bearing
|
||||
* nodes into structural records. */
|
||||
export class AstNode {
|
||||
constructor(readonly file: AstFile, readonly index: number) {}
|
||||
readonly kind: SyntaxKind;
|
||||
readonly pos: number;
|
||||
readonly end: number;
|
||||
readonly flags: NodeFlags;
|
||||
readonly data: number;
|
||||
|
||||
constructor(readonly file: AstFile, readonly index: number) {
|
||||
// Materialize immutable scalar metadata once. Lowering repeatedly reads
|
||||
// it while refining the same node, without needing another wire decode.
|
||||
const wire = file.wire;
|
||||
this.kind = wire.kind(index);
|
||||
this.pos = wire.pos(index);
|
||||
this.end = wire.end(index);
|
||||
this.flags = wire.flags(index);
|
||||
this.data = wire.data(index);
|
||||
}
|
||||
|
||||
get kind(): SyntaxKind { return this.file.wire.kind(this.index); }
|
||||
get pos(): number { return this.file.wire.pos(this.index); }
|
||||
get end(): number { return this.file.wire.end(this.index); }
|
||||
get flags(): NodeFlags { return this.file.wire.flags(this.index); }
|
||||
get data(): number { return this.file.wire.data(this.index); }
|
||||
get id(): string { return `${this.index}.${this.kind}.${this.file.root.path}`; }
|
||||
get parent(): AstNode | undefined {
|
||||
const index = this.file.wire.semanticParent(this.index);
|
||||
|
||||
@@ -59,6 +59,27 @@ test("hash words remain unsigned and ordered", () => {
|
||||
expect(file.parseOptionsKey).toBe("4294967295");
|
||||
});
|
||||
|
||||
test("node words respect a byte view's offset, signed spans and unsigned flags", () => {
|
||||
const original = response();
|
||||
const padded = new Uint8Array(original.length + 11).fill(0xff);
|
||||
padded.set(original, 3);
|
||||
const bytes = padded.subarray(3, 3 + original.length);
|
||||
const at = 120 + 3 * NODE_LEN;
|
||||
word(bytes, at + 4, 0xffffffff);
|
||||
word(bytes, at + 8, 0x80000000);
|
||||
word(bytes, at + 24, 0xfedcba98);
|
||||
const file = new AstWireFile(bytes);
|
||||
expect(file.kind(3)).toBe(AstKind.Identifier);
|
||||
expect(file.pos(3)).toBe(-1);
|
||||
expect(file.end(3)).toBe(-2147483648);
|
||||
expect(file.flags(3)).toBe(0xfedcba98);
|
||||
expect(file.data(3)).toBe(0x40000002);
|
||||
expect(file.next(3)).toBe(4);
|
||||
expect(file.parent(3)).toBe(2);
|
||||
expect(file.kind(4)).toBe(AstKind.Identifier);
|
||||
expect(() => file.kind(5)).toThrow(AstDecodeError);
|
||||
});
|
||||
|
||||
test("truncated headers, sections and node tables are rejected", () => {
|
||||
for (let size = 0; size < HEADER_SIZE; size++) expect(() => new AstWireFile(new Uint8Array(size))).toThrow(AstDecodeError);
|
||||
for (const [offset, value] of [[0, 0], [24, 40], [28, 59], [28, 67], [32, 59], [36, 69], [40, 119], [40, 0xffffffff], [48, 9], [52, 8], [56, 2]]) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AstDecodeError, AstMsgpackReader, astBounds, astI32, astU32, decodeAstString } from "./ast-bytes.js";
|
||||
import { AstDecodeError, AstMsgpackReader, astBounds, astU32, decodeAstString } from "./ast-bytes.js";
|
||||
import {
|
||||
AstKind, HEADER_OFFSET_EXTENDED_DATA, HEADER_OFFSET_HASH_HI0, HEADER_OFFSET_HASH_HI1,
|
||||
HEADER_OFFSET_HASH_LO0, HEADER_OFFSET_HASH_LO1, HEADER_OFFSET_METADATA, HEADER_OFFSET_NODES,
|
||||
@@ -64,10 +64,12 @@ export class AstWireFile {
|
||||
private readonly extended: number;
|
||||
private readonly structured: number;
|
||||
private readonly nodes: number;
|
||||
private readonly view: DataView;
|
||||
private readonly stringCache = new Map<number, string>();
|
||||
|
||||
constructor(private readonly bytes: Uint8Array) {
|
||||
astBounds(bytes, 0, HEADER_SIZE);
|
||||
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const metadata = astU32(bytes, HEADER_OFFSET_METADATA);
|
||||
if ((metadata >>> 24) !== PROTOCOL_VERSION) throw new AstDecodeError(`unsupported protocol version ${metadata >>> 24}`);
|
||||
this.stringOffsets = astU32(bytes, HEADER_OFFSET_STRING_TABLE_OFFSETS);
|
||||
@@ -104,20 +106,23 @@ export class AstWireFile {
|
||||
return this.nodes + index * NODE_LEN;
|
||||
}
|
||||
|
||||
kind(index: number): number { return astU32(this.bytes, this.nodeOffset(index) + NODE_OFFSET_KIND); }
|
||||
pos(index: number): number { return astI32(this.bytes, this.nodeOffset(index) + NODE_OFFSET_POS); }
|
||||
end(index: number): number { return astI32(this.bytes, this.nodeOffset(index) + NODE_OFFSET_END); }
|
||||
flags(index: number): number { return astU32(this.bytes, this.nodeOffset(index) + NODE_OFFSET_FLAGS); }
|
||||
data(index: number): number { return astU32(this.bytes, this.nodeOffset(index) + NODE_OFFSET_DATA); }
|
||||
// The constructor validates the complete fixed-width node table, and
|
||||
// nodeOffset validates its index. Read each word in one operation while
|
||||
// preserving little-endian decoding even in an unaligned byte view.
|
||||
kind(index: number): number { return this.view.getUint32(this.nodeOffset(index) + NODE_OFFSET_KIND, true); }
|
||||
pos(index: number): number { return this.view.getInt32(this.nodeOffset(index) + NODE_OFFSET_POS, true); }
|
||||
end(index: number): number { return this.view.getInt32(this.nodeOffset(index) + NODE_OFFSET_END, true); }
|
||||
flags(index: number): number { return this.view.getUint32(this.nodeOffset(index) + NODE_OFFSET_FLAGS, true); }
|
||||
data(index: number): number { return this.view.getUint32(this.nodeOffset(index) + NODE_OFFSET_DATA, true); }
|
||||
|
||||
next(index: number): number {
|
||||
const next = astU32(this.bytes, this.nodeOffset(index) + NODE_OFFSET_NEXT);
|
||||
const next = this.view.getUint32(this.nodeOffset(index) + NODE_OFFSET_NEXT, true);
|
||||
if (next !== 0 && (next <= index || next >= this.nodeCount)) throw new AstDecodeError("invalid sibling link");
|
||||
return next;
|
||||
}
|
||||
|
||||
parent(index: number): number {
|
||||
const parent = astU32(this.bytes, this.nodeOffset(index) + NODE_OFFSET_PARENT);
|
||||
const parent = this.view.getUint32(this.nodeOffset(index) + NODE_OFFSET_PARENT, true);
|
||||
if (parent >= this.nodeCount || (index <= 1 ? parent > index : parent >= index)) throw new AstDecodeError("invalid parent link");
|
||||
return parent;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export class CheckerCache {
|
||||
readonly assignableTypes = new Map<Type, Map<Type, boolean>>();
|
||||
readonly nonNullableType = new Map<Type, Type | undefined>();
|
||||
readonly propertiesOfType = new Map<Type, readonly Ts7Symbol[]>();
|
||||
readonly propertyOfType = new Map<Type, Map<string, Ts7Symbol | undefined>>();
|
||||
readonly indexInfosOfType = new Map<Type, readonly IndexInfo[]>();
|
||||
readonly typeArgumentsOf = new Map<Type, readonly Type[]>();
|
||||
readonly arrayTypeAnswer = new Map<Type, boolean>();
|
||||
@@ -67,6 +68,7 @@ export class CheckerCache {
|
||||
this.assignableTypes.clear();
|
||||
this.nonNullableType.clear();
|
||||
this.propertiesOfType.clear();
|
||||
this.propertyOfType.clear();
|
||||
this.indexInfosOfType.clear();
|
||||
this.typeArgumentsOf.clear();
|
||||
this.arrayTypeAnswer.clear();
|
||||
|
||||
@@ -253,3 +253,70 @@ test("negative assignability answers remain independent in both directions", ()
|
||||
expect(() => facade.isTypeAssignableTo(b, a)).toThrow("disposed");
|
||||
expect(requests).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("property lookup caches hits and misses by type and name within each project", () => {
|
||||
const requests: string[] = [];
|
||||
let fail = true;
|
||||
const snapshot = new SemanticSnapshot(1, {
|
||||
text: (method, payload) => {
|
||||
expect(method).toBe("getPropertyOfType");
|
||||
const query = JSON.parse(payload) as { project: string; type: number; name: string };
|
||||
requests.push(`${query.project}:${query.type}:${query.name}`);
|
||||
if (query.name === "retry" && fail) { fail = false; throw new Error("transient failure"); }
|
||||
if (query.name === "missing") return "null";
|
||||
return JSON.stringify({ id: query.type * 10 + (query.project === "first" ? 1 : 2), project: query.project,
|
||||
name: query.name, flags: 4, checkFlags: 0 });
|
||||
},
|
||||
binary: () => { throw new Error("unexpected binary request"); },
|
||||
});
|
||||
const first = snapshot.addProject("first", () => undefined);
|
||||
const second = snapshot.addProject("second", () => undefined);
|
||||
const a = new CheckerFacade(new SemanticChecker(first));
|
||||
const b = new CheckerFacade(new SemanticChecker(second));
|
||||
const firstType = first.type({ id: 1, flags: TypeFlags.Object });
|
||||
const otherType = first.type({ id: 2, flags: TypeFlags.Object });
|
||||
const secondType = second.type({ id: 1, flags: TypeFlags.Object });
|
||||
const symbol = a.getPropertyOfType(firstType, "value");
|
||||
for (let repeat = 0; repeat < 3; repeat++) {
|
||||
expect(a.getPropertyOfType(firstType, "value")).toBe(symbol);
|
||||
expect(a.getPropertyOfType(firstType, "missing")).toBeUndefined();
|
||||
expect(a.getPropertyOfType(otherType, "value")).not.toBe(symbol);
|
||||
expect(b.getPropertyOfType(secondType, "value")).not.toBe(symbol);
|
||||
}
|
||||
expect(requests).toEqual(["first:1:value", "first:1:missing", "first:2:value", "second:1:value"]);
|
||||
expect(() => a.getPropertyOfType(firstType, "retry")).toThrow("transient failure");
|
||||
expect(a.getPropertyOfType(firstType, "retry")).toBeDefined();
|
||||
a.getPropertyOfType(firstType, "retry");
|
||||
expect(requests.slice(4)).toEqual(["first:1:retry", "first:1:retry"]);
|
||||
first.dispose();
|
||||
expect(() => a.getPropertyOfType(firstType, "value")).toThrow("disposed");
|
||||
expect(() => a.getPropertyOfType(firstType, "missing")).toThrow("disposed");
|
||||
expect(b.getPropertyOfType(secondType, "value")).toBeDefined();
|
||||
snapshot.dispose();
|
||||
});
|
||||
|
||||
test("dependency edits invalidate positive and negative property answers", () => {
|
||||
const h = connect({ "tsconfig.json": config,
|
||||
"main.ts": 'import { value } from "./dependency.js"; export const result = value;',
|
||||
"dependency.ts": "export const value = { before: 1 };" });
|
||||
try {
|
||||
const first = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] });
|
||||
const project = first.getProjects()[0]!;
|
||||
const root = project.program.getSourceFile(h.path("main.ts"))!;
|
||||
const old = new CheckerFacade(project.checker);
|
||||
const type = old.getTypeAtLocation(declaration(root, "result").name!);
|
||||
expect(old.getPropertyOfType(type, "before")).toBeDefined();
|
||||
expect(old.getPropertyOfType(type, "after")).toBeUndefined();
|
||||
h.paths.set(h.path("dependency.ts"), "export const value = { after: 2 };");
|
||||
const second = h.session.updateSnapshot({ fileChanges: { changed: [h.path("dependency.ts")] } });
|
||||
const nextProject = second.getProjects()[0]!;
|
||||
const nextRoot = nextProject.program.getSourceFile(h.path("main.ts"))!;
|
||||
expect(nextRoot).toBe(root);
|
||||
const next = new CheckerFacade(nextProject.checker);
|
||||
const nextType = next.getTypeAtLocation(declaration(nextRoot, "result").name!);
|
||||
expect(next.getPropertyOfType(nextType, "before")).toBeUndefined();
|
||||
expect(next.getPropertyOfType(nextType, "after")).toBeDefined();
|
||||
expect(old.getPropertyOfType(type, "before")).toBeDefined();
|
||||
expect(old.getPropertyOfType(type, "after")).toBeUndefined();
|
||||
} finally { h.close(); }
|
||||
});
|
||||
|
||||
@@ -288,7 +288,17 @@ export class CheckerFacade {
|
||||
/** 5.9.3's type.getProperty(name). */
|
||||
getPropertyOfType(type: Type, name: string): Ts7Symbol | undefined {
|
||||
this.ensureActive();
|
||||
return this.raw.getPropertyOfType(type, name);
|
||||
let properties = this.cache.propertyOfType.get(type);
|
||||
if (properties === undefined) {
|
||||
properties = new Map<string, Ts7Symbol | undefined>();
|
||||
this.cache.propertyOfType.set(type, properties);
|
||||
}
|
||||
// Absence is an answer too. Both keys belong to this immutable project;
|
||||
// another snapshot or project must ask its own checker.
|
||||
if (properties.has(name)) return properties.get(name);
|
||||
const symbol = this.raw.getPropertyOfType(type, name);
|
||||
properties.set(name, symbol);
|
||||
return symbol;
|
||||
}
|
||||
|
||||
/** The 5.9.3 checker never answered undefined from getTypeAtLocation-
|
||||
|
||||
@@ -9286,6 +9286,12 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/live-reference-shared-converters.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/live-reference-shared-converters.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/llvm-read-receiver-lifetime.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/llvm-read-receiver-lifetime.ts"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
class Cell {
|
||||
name: string;
|
||||
value: number;
|
||||
next: Cell | undefined = undefined;
|
||||
children: Cell[] = [];
|
||||
constructor(name: string, value: number) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
class Envelope {
|
||||
label: string;
|
||||
child: Cell;
|
||||
constructor(label: string, child: Cell) {
|
||||
this.label = label;
|
||||
this.child = child;
|
||||
}
|
||||
}
|
||||
|
||||
function first(value: Cell | Envelope | undefined): unknown { return value; }
|
||||
function second(value: Cell | string | null): unknown { return value; }
|
||||
function third(value: Envelope | number | boolean): unknown { return value; }
|
||||
function render(value: unknown): string { return JSON.stringify(value); }
|
||||
|
||||
const leaf = new Cell("leaf", 1);
|
||||
const parent = new Cell("parent", 2);
|
||||
parent.next = leaf;
|
||||
parent.children.push(leaf);
|
||||
const envelope = new Envelope("shared", parent);
|
||||
const one = first(parent);
|
||||
const two = second(parent);
|
||||
const three = third(envelope);
|
||||
console.log(one === two, (one as Cell) === parent, (three as Envelope).child === parent);
|
||||
console.log(JSON.stringify(one), JSON.stringify(two), JSON.stringify(three));
|
||||
|
||||
leaf.value = 9;
|
||||
console.log(JSON.stringify(one), JSON.stringify(three));
|
||||
const view = one as { value: unknown; next: unknown; children: unknown };
|
||||
view.value = 7;
|
||||
console.log(parent.value, (two as Cell).value);
|
||||
const childView = view.next as { value: unknown };
|
||||
childView.value = 11;
|
||||
console.log(leaf.value, parent.children[0] === leaf);
|
||||
|
||||
// The same type occurs as a union arm, a nested field, and a stream element.
|
||||
async function streams(): Promise<void> {
|
||||
const records: { name: string; cell: Cell }[] = [
|
||||
{ name: "first", cell: parent }, { name: "second", cell: leaf },
|
||||
];
|
||||
const direct: unknown = records[0]!;
|
||||
console.log(render(direct));
|
||||
for await (const item of ReadableStream.from(records)) {
|
||||
console.log(item.name, item.cell === parent || item.cell === leaf, render(item));
|
||||
item.cell.value++;
|
||||
}
|
||||
console.log(parent.value, leaf.value, render(direct));
|
||||
console.log(first(undefined) === undefined, second(null) === null, second("text"), third(4), third(false));
|
||||
|
||||
parent.next = parent;
|
||||
try { JSON.stringify(one); }
|
||||
catch (error) { console.log("cycle", error instanceof TypeError); }
|
||||
parent.next = leaf;
|
||||
console.log(JSON.stringify(two));
|
||||
}
|
||||
|
||||
void streams();
|
||||
Reference in New Issue
Block a user