mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Verify checker-proven narrowings at run time
unionNarrow, downcast and caughtNarrow extracted a payload as the arm, class or caught kind TypeScript's narrowing proved, with no runtime check. A wrong proof read the payload as another type. Each extraction now verifies its proof once: the union tag (for a two-arm union, that the value does not hold the other arm, which LLVM folds under either dominating test), the target's vtable preorder intervals (shared with instanceof), or the caught kind or class (shared with caughtTest). A mismatch calls scr_trap with an internal-error message from a cold block. A correct program never reaches it: narrowings the frontend cannot prove already lower to checked extraction, which throws the catchable TypeError, and analyses treat these expressions as non-throwing, so a failing guard aborts instead of unwinding. The guards found a stale narrowing the stability analysis missed: a binding initialized or assigned from a narrowed read whose narrowing a callback invalidated inherits that narrowing (`const held: Expr = current`). main segfaults on that program where Node throws a TypeError. NarrowingStability now treats such bindings like captured mutable ones, so their narrowed reads use checked extraction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0bdcccef80
commit
e12db2974f
@@ -398,6 +398,7 @@ import {
|
||||
} from "./shapes.js";
|
||||
import type { ExprOf, LibCallExpr, LlStreamTypedRefAdapter, LlValue } from "./expr-context.js";
|
||||
import { INLINE_LITERAL_BYTES, literalEqualityDefinition } from "./string-literal-equality.js";
|
||||
import { emitDowncastGuard, emitUnionNarrowGuard } from "./narrow-guards.js";
|
||||
|
||||
export { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
@@ -10356,11 +10357,16 @@ export class LlEmitter {
|
||||
if (canStackUnion(e, this.boxedUnionsById)) return emitStackUnion(this, e).value;
|
||||
if (e.kind === "unionNarrow") {
|
||||
const union = this.emitUnionProjection(e.value);
|
||||
emitUnionNarrowGuard(this, union.name, e.unionId, e.tag);
|
||||
return { name: this.unionPeek(union.name, e.unionId), type: e.type };
|
||||
}
|
||||
if ((e.kind === "downcast" || e.kind === "upcast") && e.value.type.kind === "object") {
|
||||
// Prefix layouts: a class cast reads the operand's own pointer.
|
||||
return { name: this.emitReadReceiver(e.value).name, type: e.type };
|
||||
// Prefix layouts: a class cast reads the operand's own pointer; a
|
||||
// downcast verifies its target class (narrow-guards.ts).
|
||||
const receiver = this.emitReadReceiver(e.value);
|
||||
if (e.kind === "downcast" && e.type.kind === "object")
|
||||
emitDowncastGuard(this, receiver.name, e.value.type.className, e.type.className);
|
||||
return { name: receiver.name, type: e.type };
|
||||
}
|
||||
// A nullable-pointer field has no box to borrow. Projections use
|
||||
// emitUnionProjection; other borrowers get an owned heap box.
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
mangleVtStruct,
|
||||
} from "../mangle.js";
|
||||
import {
|
||||
classMembershipIntervals,
|
||||
classEnvironmentIndex,
|
||||
classStructSym,
|
||||
vtEntriesFor,
|
||||
@@ -52,6 +51,7 @@ import {
|
||||
inputSuffixWrites,
|
||||
} from "./borrowed-inputs.js";
|
||||
import type { Writes } from "./reference-writes.js";
|
||||
import { emitClassMembership, emitDowncastGuard } from "./narrow-guards.js";
|
||||
|
||||
export function emitCallExpr(
|
||||
host: LlvmEmitterContext,
|
||||
@@ -1129,8 +1129,11 @@ export function emitCallExpr(
|
||||
// Prefix layout: both directions are reinterprets of the SAME
|
||||
// pointer — no RC traffic, ownership transfers from the operand
|
||||
// temp to the result temp (struck so the one +1 releases exactly
|
||||
// once, under the RESULT type's release).
|
||||
// once, under the RESULT type's release). A downcast verifies the
|
||||
// class tsc's narrowing proved (narrow-guards.ts).
|
||||
const v = host.emitExpr(e.value);
|
||||
if (e.kind === "downcast" && e.type.kind === "object" && v.type.kind === "object")
|
||||
emitDowncastGuard(host, v.name, v.type.className, e.type.className);
|
||||
if (isRefCounted(v.type)) host.moveTemp(v);
|
||||
return host.own({ name: v.name, type: e.type });
|
||||
}
|
||||
@@ -1142,24 +1145,14 @@ export function emitCallExpr(
|
||||
// The test only reads the vtable word: a borrowable operand needs no
|
||||
// retain/release pair around it.
|
||||
const v = emitBorrowedInputs(host, [e.value])[0]!;
|
||||
const target = host.classMetaOf(e.className);
|
||||
const pre = host.loadVtPre(v.name, e.value.type.className);
|
||||
let result: string | undefined;
|
||||
for (const interval of classMembershipIntervals(host.classMeta, target.def.name)) {
|
||||
const ge = B.tmp(),
|
||||
le = B.tmp(),
|
||||
test = B.tmp();
|
||||
B.line(`${ge} = icmp uge ${host.sizeType} ${pre}, ${interval.pre}`);
|
||||
B.line(`${le} = icmp ule ${host.sizeType} ${pre}, ${interval.post}`);
|
||||
B.line(`${test} = and i1 ${ge}, ${le} ; instanceof ${e.className}`);
|
||||
if (result === undefined) result = test;
|
||||
else {
|
||||
const combined = B.tmp();
|
||||
B.line(`${combined} = or i1 ${result}, ${test}`);
|
||||
result = combined;
|
||||
}
|
||||
}
|
||||
return { name: result!, type: e.type };
|
||||
const result = emitClassMembership(
|
||||
host,
|
||||
v.name,
|
||||
e.value.type.className,
|
||||
e.className,
|
||||
`instanceof ${e.className}`,
|
||||
);
|
||||
return { name: result, type: e.type };
|
||||
}
|
||||
case "virtualCall": {
|
||||
// Dispatch through the receiver's vtable: the slot lives on the
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { classMembershipIntervals } from "./classes.js";
|
||||
import {
|
||||
emitCaughtInstanceof,
|
||||
emitCaughtNarrowGuard,
|
||||
emitUnionNarrowGuard,
|
||||
} from "./narrow-guards.js";
|
||||
import { borrowableInputs, emitBorrowedInput, emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
import { preservesDynTest } from "./checked-value-lifetimes.js";
|
||||
import { typedRefConstructor } from "./shapes.js";
|
||||
@@ -295,13 +300,14 @@ export function emitDynamicExpr(
|
||||
return host.own({ name: host.unionNewOwned(e.unionId, e.tag, v), type: e.type });
|
||||
}
|
||||
case "unionNarrow": {
|
||||
// Tag-UNCHECKED payload extraction: the frontend emits this only
|
||||
// where tsc's control-flow narrowing proved the tag. Ref payloads
|
||||
// come out +1. The receiver is consumed before any later expression.
|
||||
// Payload extraction where tsc's control-flow narrowing proved the
|
||||
// tag; the guard verifies it (narrow-guards.ts). Ref payloads come
|
||||
// out +1. The receiver is consumed before any later expression.
|
||||
const u = host.emitUnionProjection(e.value);
|
||||
const arm = e.type;
|
||||
if (isUnitType(arm))
|
||||
throw new InternalCompilerError(`llvm emitter bug: unionNarrow to unit arm ${arm.kind}`);
|
||||
emitUnionNarrowGuard(host, u.name, e.unionId, e.tag);
|
||||
const v = host.unionExtract(u.name, e.unionId, arm);
|
||||
return host.own({ name: v, type: arm });
|
||||
}
|
||||
@@ -974,56 +980,7 @@ export function emitDynamicExpr(
|
||||
// borrowed. SCR_EXC_STR = 3, SCR_EXC_F64 = 1, SCR_EXC_BOOL = 2.
|
||||
const c = host.emitReadReceiver(e.value);
|
||||
if (e.test === "instanceof") {
|
||||
const target = host.classMetaOf(e.className!);
|
||||
if (!target.hierarchy) {
|
||||
// Standalone class payloads have no vtable word. Their retain
|
||||
// adapter identifies the exact native layout in the snapshot.
|
||||
const adapterSlot = B.tmp();
|
||||
const adapter = B.tmp();
|
||||
B.line(`${adapterSlot} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 5`);
|
||||
B.line(`${adapter} = load ptr, ptr ${adapterSlot}`);
|
||||
let result: string | undefined;
|
||||
const intervals = classMembershipIntervals(host.classMeta, target.def.name);
|
||||
for (const candidate of host.classMeta.values()) {
|
||||
if (
|
||||
candidate.hierarchy ||
|
||||
!intervals.some((range) => range.pre <= candidate.pre && candidate.pre <= range.post)
|
||||
)
|
||||
continue;
|
||||
const rc = vAdapters(host.shapeHost, { kind: "object", className: candidate.def.name });
|
||||
const matches = B.tmp();
|
||||
B.line(`${matches} = icmp eq ptr ${adapter}, ${rc.retain}`);
|
||||
if (result === undefined) result = matches;
|
||||
else {
|
||||
const joined = B.tmp();
|
||||
B.line(`${joined} = or i1 ${result}, ${matches}`);
|
||||
result = joined;
|
||||
}
|
||||
}
|
||||
if (result === undefined)
|
||||
throw new InternalCompilerError("empty standalone class membership");
|
||||
if (e.negated !== true) return { name: result, type: e.type };
|
||||
const negated = B.tmp();
|
||||
B.line(`${negated} = xor i1 ${result}, true`);
|
||||
return { name: negated, type: e.type };
|
||||
}
|
||||
host.declare(
|
||||
`declare zeroext i1 @scr_caught_instanceof(ptr, ${host.sizeType}, ${host.sizeType})`,
|
||||
);
|
||||
let t: string | undefined;
|
||||
for (const interval of classMembershipIntervals(host.classMeta, target.def.name)) {
|
||||
const test = B.tmp();
|
||||
B.line(
|
||||
`${test} = call zeroext i1 @scr_caught_instanceof(ptr ${c.name}, ${host.sizeType} ${interval.pre}, ${host.sizeType} ${interval.post})`,
|
||||
);
|
||||
if (t === undefined) t = test;
|
||||
else {
|
||||
const combined = B.tmp();
|
||||
B.line(`${combined} = or i1 ${t}, ${test}`);
|
||||
t = combined;
|
||||
}
|
||||
}
|
||||
if (t === undefined) throw new InternalCompilerError("empty class membership");
|
||||
const t = emitCaughtInstanceof(host, c.name, e.className!);
|
||||
if (e.negated !== true) return { name: t, type: e.type };
|
||||
const n = B.tmp();
|
||||
B.line(`${n} = xor i1 ${t}, true`);
|
||||
@@ -1097,10 +1054,18 @@ export function emitDynamicExpr(
|
||||
return out;
|
||||
}
|
||||
case "caughtNarrow": {
|
||||
// Checker-trusted extraction (the matching caughtTest was proven
|
||||
// by tsc's narrowing): scalars read the snapshot's slots,
|
||||
// refcounted payloads come out retained (+1). Box borrowed.
|
||||
// Extraction where tsc's narrowing proved the matching caughtTest;
|
||||
// the guard verifies it (narrow-guards.ts). Scalars read the
|
||||
// snapshot's slots, refcounted payloads come out retained (+1). Box
|
||||
// borrowed.
|
||||
const c = host.emitReadReceiver(e.value);
|
||||
if (
|
||||
e.type.kind === "f64" ||
|
||||
e.type.kind === "bool" ||
|
||||
e.type.kind === "string" ||
|
||||
e.type.kind === "object"
|
||||
)
|
||||
emitCaughtNarrowGuard(host, c.name, e.type);
|
||||
if (e.type.kind === "f64") {
|
||||
const p = B.tmp();
|
||||
const v = B.tmp();
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/* Runtime guards for checker-proven narrowings.
|
||||
*
|
||||
* `unionNarrow`, `downcast` and `caughtNarrow` extract a payload whose arm,
|
||||
* class or kind TypeScript's control-flow narrowing proved. Emitted code
|
||||
* verifies that proof once at the narrowing site: the tag, class interval or
|
||||
* caught kind is compared and a mismatch stops the program through the
|
||||
* runtime trap funnel instead of reading the payload as the wrong type. A
|
||||
* correct program never takes the failing branch: the frontend lowers every
|
||||
* narrowing it cannot prove through checked extraction, which throws the
|
||||
* catchable TypeError. A failing guard is therefore a compiler bug, so it
|
||||
* aborts rather than unwinding through code that assumed the extraction
|
||||
* could not throw. The guards are cheap: one load and compare per site,
|
||||
* which LLVM folds where an identical test already dominates. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { classMembershipIntervals } from "./classes.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { vAdapters } from "./shapes.js";
|
||||
|
||||
export const NARROW_GUARD_MESSAGES = {
|
||||
union: "scriptc: internal error: a narrowed union value holds a different arm\n",
|
||||
downcast: "scriptc: internal error: a downcast object is not an instance of its target class\n",
|
||||
caught: "scriptc: internal error: a narrowed catch binding holds a different value\n",
|
||||
} as const;
|
||||
|
||||
/** Continues in a fresh block where `ok` holds; otherwise traps with the
|
||||
* message for `kind`. */
|
||||
export function emitNarrowGuard(
|
||||
host: LlvmEmitterContext,
|
||||
ok: string,
|
||||
kind: keyof typeof NARROW_GUARD_MESSAGES,
|
||||
): void {
|
||||
const B = host.B;
|
||||
host.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
|
||||
host.declare(`declare void @scr_trap(ptr)`);
|
||||
const expected = B.tmp();
|
||||
B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${ok}, i1 true)`);
|
||||
const pass = B.newLabel("narrow.ok");
|
||||
const fail = B.newLabel("narrow.bad");
|
||||
B.condBr(expected, pass, fail);
|
||||
B.startBlock(fail);
|
||||
B.line(`call void @scr_trap(ptr ${host.cstr(NARROW_GUARD_MESSAGES[kind])})`);
|
||||
B.terminate("unreachable");
|
||||
B.startBlock(pass);
|
||||
}
|
||||
|
||||
/** Verifies that the union value `unionName` holds arm `tag`. A two-arm
|
||||
* union holds it exactly when it does not hold the other arm; testing the
|
||||
* other arm lets LLVM fold the guard under a dominating `!== other` test as
|
||||
* well as under `=== tag` (a tag outside the arms is not a union value). */
|
||||
export function emitUnionNarrowGuard(
|
||||
host: LlvmEmitterContext,
|
||||
unionName: string,
|
||||
unionId: string,
|
||||
tag: number,
|
||||
): void {
|
||||
const ok = host.B.tmp();
|
||||
const actual = host.unionTag(unionName, unionId);
|
||||
if (host.unionsById.get(unionId)?.arms.length === 2)
|
||||
host.B.line(`${ok} = icmp ne i32 ${actual}, ${1 - tag}`);
|
||||
else host.B.line(`${ok} = icmp eq i32 ${actual}, ${tag}`);
|
||||
emitNarrowGuard(host, ok, "union");
|
||||
}
|
||||
|
||||
/** The i1 `instanceof` test of the borrowed object `objName`, statically a
|
||||
* `staticClassName`, against `targetClassName`'s preorder intervals. */
|
||||
export function emitClassMembership(
|
||||
host: LlvmEmitterContext,
|
||||
objName: string,
|
||||
staticClassName: string,
|
||||
targetClassName: string,
|
||||
comment: string,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const target = host.classMetaOf(targetClassName);
|
||||
const pre = host.loadVtPre(objName, staticClassName);
|
||||
let result: string | undefined;
|
||||
for (const interval of classMembershipIntervals(host.classMeta, target.def.name)) {
|
||||
const ge = B.tmp(),
|
||||
le = B.tmp(),
|
||||
test = B.tmp();
|
||||
B.line(`${ge} = icmp uge ${host.sizeType} ${pre}, ${interval.pre}`);
|
||||
B.line(`${le} = icmp ule ${host.sizeType} ${pre}, ${interval.post}`);
|
||||
B.line(`${test} = and i1 ${ge}, ${le} ; ${comment}`);
|
||||
if (result === undefined) result = test;
|
||||
else {
|
||||
const combined = B.tmp();
|
||||
B.line(`${combined} = or i1 ${result}, ${test}`);
|
||||
result = combined;
|
||||
}
|
||||
}
|
||||
return result!;
|
||||
}
|
||||
|
||||
/** Verifies that the borrowed object `objName` is a `targetClassName`. */
|
||||
export function emitDowncastGuard(
|
||||
host: LlvmEmitterContext,
|
||||
objName: string,
|
||||
staticClassName: string,
|
||||
targetClassName: string,
|
||||
): void {
|
||||
const ok = emitClassMembership(
|
||||
host,
|
||||
objName,
|
||||
staticClassName,
|
||||
targetClassName,
|
||||
`downcast ${targetClassName}`,
|
||||
);
|
||||
emitNarrowGuard(host, ok, "downcast");
|
||||
}
|
||||
|
||||
/** The i1 `e instanceof C` test of the borrowed catch snapshot `caught`.
|
||||
* Hierarchy classes compare the OBJ payload's vtable preorder; standalone
|
||||
* class payloads have no vtable word, so their retain adapter identifies
|
||||
* the exact native layout. */
|
||||
export function emitCaughtInstanceof(
|
||||
host: LlvmEmitterContext,
|
||||
caught: string,
|
||||
className: string,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const target = host.classMetaOf(className);
|
||||
const intervals = classMembershipIntervals(host.classMeta, target.def.name);
|
||||
if (!target.hierarchy) {
|
||||
const adapterSlot = B.tmp();
|
||||
const adapter = B.tmp();
|
||||
B.line(`${adapterSlot} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 5`);
|
||||
B.line(`${adapter} = load ptr, ptr ${adapterSlot}`);
|
||||
let result: string | undefined;
|
||||
for (const candidate of host.classMeta.values()) {
|
||||
if (
|
||||
candidate.hierarchy ||
|
||||
!intervals.some((range) => range.pre <= candidate.pre && candidate.pre <= range.post)
|
||||
)
|
||||
continue;
|
||||
const rc = vAdapters(host.shapeHost, { kind: "object", className: candidate.def.name });
|
||||
const matches = B.tmp();
|
||||
B.line(`${matches} = icmp eq ptr ${adapter}, ${rc.retain}`);
|
||||
if (result === undefined) result = matches;
|
||||
else {
|
||||
const joined = B.tmp();
|
||||
B.line(`${joined} = or i1 ${result}, ${matches}`);
|
||||
result = joined;
|
||||
}
|
||||
}
|
||||
if (result === undefined) throw new InternalCompilerError("empty standalone class membership");
|
||||
return result;
|
||||
}
|
||||
host.declare(
|
||||
`declare zeroext i1 @scr_caught_instanceof(ptr, ${host.sizeType}, ${host.sizeType})`,
|
||||
);
|
||||
let result: string | undefined;
|
||||
for (const interval of intervals) {
|
||||
const test = B.tmp();
|
||||
B.line(
|
||||
`${test} = call zeroext i1 @scr_caught_instanceof(ptr ${caught}, ${host.sizeType} ${interval.pre}, ${host.sizeType} ${interval.post})`,
|
||||
);
|
||||
if (result === undefined) result = test;
|
||||
else {
|
||||
const combined = B.tmp();
|
||||
B.line(`${combined} = or i1 ${result}, ${test}`);
|
||||
result = combined;
|
||||
}
|
||||
}
|
||||
if (result === undefined) throw new InternalCompilerError("empty class membership");
|
||||
return result;
|
||||
}
|
||||
|
||||
/** SCR_EXC_F64 = 1, SCR_EXC_BOOL = 2, SCR_EXC_STR = 3 (scr_exception.h). */
|
||||
export const CAUGHT_SCALAR_KINDS = { f64: 1, bool: 2, string: 3 } as const;
|
||||
|
||||
/** Verifies that the borrowed catch snapshot `caught` holds a value of the
|
||||
* narrowed type: a scalar's kind tag, or an object's class. */
|
||||
export function emitCaughtNarrowGuard(
|
||||
host: LlvmEmitterContext,
|
||||
caught: string,
|
||||
type: { kind: "f64" | "bool" | "string" } | { kind: "object"; className: string },
|
||||
): void {
|
||||
const B = host.B;
|
||||
let ok: string;
|
||||
if (type.kind === "object") {
|
||||
ok = emitCaughtInstanceof(host, caught, type.className);
|
||||
} else {
|
||||
const kp = B.tmp();
|
||||
const k = B.tmp();
|
||||
ok = B.tmp();
|
||||
B.line(`${kp} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 1`);
|
||||
B.line(`${k} = load i32, ptr ${kp}`);
|
||||
B.line(`${ok} = icmp eq i32 ${k}, ${CAUGHT_SCALAR_KINDS[type.kind]}`);
|
||||
}
|
||||
emitNarrowGuard(host, ok, "caught");
|
||||
}
|
||||
@@ -6,9 +6,10 @@ import type { IrExpr } from "../../../ir/ir.js";
|
||||
export function isSafeToRepeat(expr: IrExpr): boolean {
|
||||
if (expr.kind === "varRef") return true;
|
||||
if (expr.kind === "recordGet" || expr.kind === "fieldGet") return isSafeToRepeat(expr.obj);
|
||||
// The trust-the-checker narrowing bridges over a pure read: extraction
|
||||
// and downcast are reads too (the +1 on ref payloads is RC bookkeeping,
|
||||
// not an observable effect — each emission owns its own copy).
|
||||
// The narrowing bridges over a pure read: extraction and downcast are
|
||||
// reads too (their guards only compare, and the +1 on ref payloads is RC
|
||||
// bookkeeping, not an observable effect — each emission owns its own
|
||||
// copy).
|
||||
if (expr.kind === "unionNarrow" || expr.kind === "downcast") return isSafeToRepeat(expr.value);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3128,9 +3128,9 @@ export function lowerTernary(
|
||||
* union-typed reference at use sites (`if (r.kind === "ok") { ...r... }`
|
||||
* types `r` as the ok-arm inside the branch); the IR value is still the
|
||||
* tagged union, so the read is bridged with a `unionNarrow` extracting
|
||||
* the arm's payload. The extraction is tag-UNCHECKED at runtime —
|
||||
* soundness rests entirely on tsc having proven the tag (the project's
|
||||
* trust-the-checker thesis; see docs/ir.md). A checker type that maps to
|
||||
* the arm's payload. The extraction relies on tsc having proven the tag;
|
||||
* backends verify it once and trap on a mismatch, so a wrong proof stops
|
||||
* the program instead of reading another arm. A checker type that maps to
|
||||
* the same union (unnarrowed use), to a SUB-union (partial narrowing —
|
||||
* unrepresentable without a re-tag), or to nothing (`never` in an
|
||||
* exhaustive default) leaves the expression union-typed. */
|
||||
@@ -3320,8 +3320,8 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
}
|
||||
// instanceof narrowing for classes: tsc types this USE as a subclass of
|
||||
// the IR value's class (only a dynamic instanceof test narrows a class
|
||||
// type), so the read bridges with an unchecked static downcast — the
|
||||
// same trust-the-checker contract as the union bridge below.
|
||||
// type), so the read bridges with a static downcast that backends verify
|
||||
// at run time, like the union bridge below.
|
||||
if (expr.type.kind === "object") {
|
||||
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
if (narrowed?.kind === "union") {
|
||||
@@ -3972,7 +3972,7 @@ function runtimeOptionalAssertionErases(
|
||||
* (`w = 5; if (w !== null)`), the comparison is statically decided and
|
||||
* folds to a bool literal — that drops only a side-effect-free read
|
||||
* (flow narrowing applies to references, never to calls), and soundness
|
||||
* is the same trust-the-checker bet unionNarrow already makes. Null when
|
||||
* rests on the same narrowing proof unionNarrow relies on. Null when
|
||||
* neither side is a unit literal (not a unit comparison). */
|
||||
export function lowerUnitComparison(
|
||||
lowerer: Lowerer,
|
||||
@@ -11904,8 +11904,8 @@ function lowerCjsExportTableThisMember(
|
||||
/** A read of a catch binding — the caught analog of maybeNarrow. Where
|
||||
* tsc's control-flow narrowing has proven a supported test (`instanceof`
|
||||
* over a hierarchy class, `typeof` over a primitive), the read bridges
|
||||
* with a kind-unchecked `caughtNarrow` extraction (trust-the-checker,
|
||||
* exactly like unionNarrow). Every OTHER read is the narrowness fence:
|
||||
* with a `caughtNarrow` extraction whose kind backends verify at run time,
|
||||
* exactly like unionNarrow. Every OTHER read is the narrowness fence:
|
||||
* the binding's payload is a typed exception snapshot, not a dyn, so
|
||||
* un-narrowed uses have nothing sound to lower to. */
|
||||
export function caughtRead(
|
||||
|
||||
@@ -5405,7 +5405,7 @@ function lowerBranchSwitch(
|
||||
* cell, deliberately narrower than dyn: the supported uses are the
|
||||
* narrowing tests (`e instanceof C` over hierarchy classes, `typeof e
|
||||
* === "string"/"number"/"boolean"`), reads under a proven narrow
|
||||
* (caughtRead bridges them with caughtNarrow, trust-the-checker like
|
||||
* (caughtRead bridges them with caughtNarrow, verified at run time like
|
||||
* unionNarrow), and rethrow (`throw e`). Unsupported raw uses and
|
||||
* assignments are fenced (SC1063 and friends); destructuring patterns
|
||||
* are SC1062. Captures retain the snapshot beyond the catch scope.
|
||||
|
||||
@@ -11,9 +11,10 @@ import type { Lowerer } from "./lowerer.js";
|
||||
* after assignments and tests, and keeps that narrowing across calls: a
|
||||
* callback, method or getter that clears or replaces the property (or a
|
||||
* closure that reassigns the binding) leaves the reference typed by its old
|
||||
* value. Lowering bridges a narrowed union read with an UNCHECKED payload
|
||||
* extraction and folds `?.`, truthiness and `typeof` by the narrowed type,
|
||||
* so such a read would interpret the wrong arm. This analysis decides when
|
||||
* value. Lowering bridges a narrowed union read with a payload extraction
|
||||
* that traps on the wrong arm and folds `?.`, truthiness and `typeof` by the
|
||||
* narrowed type, so such a read would abort or fold to the old value instead
|
||||
* of throwing the catchable TypeError. This analysis decides when
|
||||
* the narrowing is provably current:
|
||||
*
|
||||
* - every property in the reference is construction-only (no store to its
|
||||
@@ -30,6 +31,10 @@ import type { Lowerer } from "./lowerer.js";
|
||||
* A loop around the use that starts inside that span contributes its
|
||||
* whole body, since its back edge reaches the use again.
|
||||
*
|
||||
* A binding initialized or assigned from a read whose narrowing may be
|
||||
* stale inherits that narrowing (`const held: Expr = current` keeps the arm
|
||||
* `current` was typed with), so its narrowed reads are never trusted.
|
||||
*
|
||||
* TypeScript never carries a property narrowing into a nested function,
|
||||
* and every narrowing (an assignment, test, predicate or assertion of the
|
||||
* reference) is an occurrence of the reference, so a narrowed use always
|
||||
@@ -42,6 +47,7 @@ export class NarrowingStability {
|
||||
private readonly verdicts = new Map<ts.Node, boolean>();
|
||||
private readonly effects = new Map<ts.Node, boolean>();
|
||||
private readonly capturedMutable = new Map<ts.Symbol, boolean>();
|
||||
private readonly staleBindings = new Map<ts.Symbol, boolean>();
|
||||
private readonly identifierWrites = new Map<ts.SourceFile, Map<string, ts.Identifier[]>>();
|
||||
private readonly widened = new Map<ts.Node, ts.Type | null>();
|
||||
private readonly plainContainers = new Map<ts.Node, boolean>();
|
||||
@@ -90,7 +96,7 @@ export class NarrowingStability {
|
||||
}
|
||||
if (ts.isIdentifier(node)) {
|
||||
const symbol = this.bindingOf(node);
|
||||
if (!symbol || !this.isCapturedMutable(symbol)) return false;
|
||||
if (!symbol || !this.mayHoldStaleNarrowing(symbol)) return false;
|
||||
return !this.trusted(node);
|
||||
}
|
||||
return false;
|
||||
@@ -113,7 +119,8 @@ export class NarrowingStability {
|
||||
declared = this.declaredPropertyType(node);
|
||||
} else {
|
||||
const symbol = this.bindingOf(node);
|
||||
declared = symbol && this.isCapturedMutable(symbol) ? checker.getTypeOfSymbol(symbol) : null;
|
||||
declared =
|
||||
symbol && this.mayHoldStaleNarrowing(symbol) ? checker.getTypeOfSymbol(symbol) : null;
|
||||
}
|
||||
if (declared === null) return null;
|
||||
const narrowedTs = checker.getTypeAtLocation(node);
|
||||
@@ -245,6 +252,10 @@ export class NarrowingStability {
|
||||
)
|
||||
return false;
|
||||
}
|
||||
if (ts.isIdentifier(chain.root)) {
|
||||
const root = this.bindingOf(chain.root);
|
||||
if (root !== null && this.inheritsStaleNarrowing(root)) return false;
|
||||
}
|
||||
const rootStable =
|
||||
chain.root.kind === ts.SyntaxKind.ThisKeyword ||
|
||||
(ts.isIdentifier(chain.root) && !this.rootCapturedMutable(chain.root));
|
||||
@@ -346,6 +357,63 @@ export class NarrowingStability {
|
||||
return !program.wildcard && !program.mutable.has(name) && !program.accessors.has(name);
|
||||
}
|
||||
|
||||
/** A binding whose narrowed reads may observe a narrowing that no longer
|
||||
* holds: another function assigns it, or it inherits a stale narrowing. */
|
||||
private mayHoldStaleNarrowing(symbol: ts.Symbol): boolean {
|
||||
return this.isCapturedMutable(symbol) || this.inheritsStaleNarrowing(symbol);
|
||||
}
|
||||
|
||||
/** The binding is initialized or assigned from a narrowed read that may be
|
||||
* stale, so TypeScript's narrowing of the binding may be stale too. */
|
||||
private inheritsStaleNarrowing(symbol: ts.Symbol): boolean {
|
||||
const cached = this.staleBindings.get(symbol);
|
||||
if (cached !== undefined) return cached;
|
||||
// A cycle of bindings assigned from each other contributes nothing.
|
||||
this.staleBindings.set(symbol, false);
|
||||
let result = false;
|
||||
try {
|
||||
const decl = this.lowerer.checker.valueDeclarationOf(symbol);
|
||||
if (decl !== undefined && ts.isVariableDeclaration(decl)) {
|
||||
if (decl.initializer !== undefined && this.mayBeStale(decl.initializer)) result = true;
|
||||
else {
|
||||
for (const write of this.writesIn(decl.getSourceFile()).get(symbol.name) ?? []) {
|
||||
const assignment = write.parent;
|
||||
if (
|
||||
ts.isBinaryExpression(assignment) &&
|
||||
assignment.left === write &&
|
||||
assignment.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
this.lowerer.resolveValueSymbol(write) === symbol &&
|
||||
this.mayBeStale(assignment.right)
|
||||
) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.staleBindings.set(symbol, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** A reference TypeScript narrows below its declared type where the
|
||||
* narrowing may no longer hold. */
|
||||
private mayBeStale(expression: ts.Expression): boolean {
|
||||
let e = expression;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isIdentifier(e) && !ts.isPropertyAccessExpression(e)) return false;
|
||||
const checker = this.lowerer.checker;
|
||||
let declared: ts.Type | null;
|
||||
if (ts.isPropertyAccessExpression(e)) declared = this.declaredPropertyType(e);
|
||||
else {
|
||||
const symbol = this.bindingOf(e);
|
||||
declared = symbol === null ? null : checker.getTypeOfSymbol(symbol);
|
||||
}
|
||||
if (declared === null || checker.getTypeAtLocation(e) === declared) return false;
|
||||
return !this.trusted(e);
|
||||
}
|
||||
|
||||
/** A `let`/`var`/parameter binding assigned from a function other than
|
||||
* the one that declares it: any call may change it. */
|
||||
private isCapturedMutable(symbol: ts.Symbol): boolean {
|
||||
|
||||
@@ -344,8 +344,8 @@ export type IrType =
|
||||
* Runtime representation is a refcounted snapshot box (ScrCaught) holding
|
||||
* the taken exception: a kind tag plus the payload. Even NARROWER than
|
||||
* dyn: the only expressions a caught value may appear in are `caughtTest`
|
||||
* (kind/instanceof tests), `caughtNarrow` (checker-trusted extraction
|
||||
* under a proven test), and the `rethrow` statement — the frontend
|
||||
* (kind/instanceof tests), `caughtNarrow` (extraction under a proven
|
||||
* test, verified at run time), and the `rethrow` statement — the frontend
|
||||
* rejects every other use with the narrowing hint. */
|
||||
| { kind: "caught" }
|
||||
| { kind: "promise"; inner: IrType } // heap, refcounted, settled-once
|
||||
@@ -2953,11 +2953,12 @@ export type IrExpr =
|
||||
* fulfillment payload (scr_await_void) and rejections flow untyped.
|
||||
* Ownership of the operand transfers to the result, like upcast. */
|
||||
| { kind: "promiseVoidWiden"; value: IrExpr; type: IrType; loc: SrcLoc }
|
||||
/** Checker-trusted narrowing of a base-class value to a subclass (`type`
|
||||
* is the subclass). The frontend emits this only where tsc's control-flow
|
||||
* narrowing has already proven the dynamic class (an `instanceof` guard)
|
||||
* — the same trust-the-checker contract as unionNarrow: no runtime check,
|
||||
* a pointer reinterpret with ownership transferring like upcast. */
|
||||
/** Narrowing of a base-class value to a subclass (`type` is the
|
||||
* subclass). The frontend emits this only where tsc's control-flow
|
||||
* narrowing has already proven the dynamic class (an `instanceof` guard),
|
||||
* like unionNarrow. Backends verify the class once and trap on a mismatch
|
||||
* a correct program never reaches, then reinterpret the pointer with
|
||||
* ownership transferring like upcast. */
|
||||
| { kind: "downcast"; value: IrExpr; type: IrType; loc: SrcLoc }
|
||||
/** `x instanceof C` where x's static class and C are both in extends-
|
||||
* hierarchies: an O(1) preorder-interval check against the vtable the
|
||||
@@ -3386,12 +3387,13 @@ export type IrExpr =
|
||||
type: IrType;
|
||||
loc: SrcLoc;
|
||||
}
|
||||
/** Checker-trusted extraction of a catch binding's payload as `type` —
|
||||
* the caught analog of unionNarrow: the frontend emits this only where
|
||||
* tsc's control-flow narrowing has already proven the matching test
|
||||
* (`e instanceof C` / `typeof e === "string"`), so the read is
|
||||
* kind-UNCHECKED at runtime. `type` is f64, bool, string, or a
|
||||
* class object; refcounted results come out retained (+1). */
|
||||
/** Extraction of a catch binding's payload as `type` — the caught
|
||||
* analog of unionNarrow: the frontend emits this only where tsc's
|
||||
* control-flow narrowing has already proven the matching test
|
||||
* (`e instanceof C` / `typeof e === "string"`). Backends verify the kind
|
||||
* or class once and trap on a mismatch a correct program never reaches.
|
||||
* `type` is f64, bool, string, or a class object; refcounted results come
|
||||
* out retained (+1). */
|
||||
| { kind: "caughtNarrow"; value: IrExpr; type: IrType; loc: SrcLoc }
|
||||
/** CHECKED extraction of a catch binding's payload as a hierarchy-class
|
||||
* instance — the caught analog of dynCheck, emitted for `e as C` casts
|
||||
@@ -3417,11 +3419,12 @@ export type IrExpr =
|
||||
* varRef (borrowed); `type` is dyn; the result is a fresh tree (+1),
|
||||
* never aliasing the payload. Never throws. */
|
||||
| { kind: "caughtToDyn"; value: IrExpr; type: IrType; loc: SrcLoc }
|
||||
/** Union payload extraction, tag-UNCHECKED: `value` is union-typed,
|
||||
* `type` is arms[tag], and the backend reads the payload assuming the tag
|
||||
* — SOUNDNESS RESTS ON tsc's control-flow narrowing (the frontend emits
|
||||
* this only where the checker has already narrowed the expression to that
|
||||
* arm; see docs/ir.md). Refcounted payloads come out retained (+1). */
|
||||
/** Union payload extraction: `value` is union-typed and `type` is
|
||||
* arms[tag]. The frontend emits this only where tsc's control-flow
|
||||
* narrowing has already narrowed the expression to that arm; backends
|
||||
* verify the tag once and trap on a mismatch a correct program never
|
||||
* reaches, so a wrong proof cannot read the payload as another arm.
|
||||
* Refcounted payloads come out retained (+1). */
|
||||
| { kind: "unionNarrow"; unionId: string; tag: number; value: IrExpr; type: IrType; loc: SrcLoc }
|
||||
/** Discriminant read `r.kind` on a union receiver: every arm is a
|
||||
* record/class possessing field `field` with the SAME primitive IR type
|
||||
|
||||
@@ -2907,7 +2907,7 @@ function validateFunction(
|
||||
case "downcast": {
|
||||
// A pointer reinterpret is sound exactly between hierarchy
|
||||
// relatives: upcast widens a STRICT descendant to an ancestor,
|
||||
// downcast (checker-trusted) narrows an ancestor to a STRICT
|
||||
// downcast (verified at run time) narrows an ancestor to a STRICT
|
||||
// descendant. Upcast additionally widens CLASS VALUES
|
||||
// (classval:D → classval:C): the same pointer, type-only — legal
|
||||
// exactly when D strictly descends from C AND the two completed
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile } from "../src/index.js";
|
||||
|
||||
async function emitLlvm(source: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-narrow-guards-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ll");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" });
|
||||
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
|
||||
return await readFile(outPath, "utf8");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** The trap call guarding narrowings of `kind`, after its message constant. */
|
||||
function guardCount(llvm: string, message: string): number {
|
||||
const constant = [...llvm.matchAll(/^(@[\w.]+) = [^\n]*c"([^"]*)"/gm)].find(([, , text]) =>
|
||||
text!.startsWith(message),
|
||||
);
|
||||
if (constant === undefined) return 0;
|
||||
return llvm.split(`call void @scr_trap(ptr ${constant[1]})`).length - 1;
|
||||
}
|
||||
|
||||
test("typeof narrowing verifies the union tag before reading the payload", async () => {
|
||||
const llvm = await emitLlvm(`
|
||||
function width(v: string | number): number {
|
||||
if (typeof v === "string") return v.length;
|
||||
return v;
|
||||
}
|
||||
console.log(width("abc") + width(4));
|
||||
`);
|
||||
expect(guardCount(llvm, "scriptc: internal error: a narrowed union value")).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
/** The tag compares that feed narrowing guards, as `eq K` / `ne K`. */
|
||||
function guardCompares(llvm: string): string[] {
|
||||
const compares: string[] = [];
|
||||
for (const fn of llvm.split("\ndefine ")) {
|
||||
for (const [, expected, ok] of fn.matchAll(
|
||||
/(%[\w.]+) = call i1 @llvm\.expect\.i1\(i1 (%[\w.]+), i1 true\)/g,
|
||||
)) {
|
||||
if (!fn.includes(`br i1 ${expected}, label %narrow.ok`)) continue;
|
||||
const def = new RegExp(`^\\s*${ok} = icmp (eq|ne) i32 %[\\w.]+, (\\d+)$`, "m").exec(fn);
|
||||
if (def) compares.push(`${def[1]} ${def[2]}`);
|
||||
}
|
||||
}
|
||||
return compares;
|
||||
}
|
||||
|
||||
test("two-arm union guards test the other arm; wider unions test the arm", async () => {
|
||||
const two = await emitLlvm(`
|
||||
function width(v: string | number): number {
|
||||
if (typeof v === "string") return v.length;
|
||||
return v;
|
||||
}
|
||||
console.log(width("abc") + width(4));
|
||||
`);
|
||||
const twoCompares = guardCompares(two);
|
||||
expect(twoCompares.length).toBeGreaterThan(0);
|
||||
expect(twoCompares.every((c) => c.startsWith("ne "))).toBe(true);
|
||||
const three = await emitLlvm(`
|
||||
function width(v: string | number | boolean): number {
|
||||
if (typeof v === "string") return v.length;
|
||||
if (typeof v === "number") return v;
|
||||
return v ? 1 : 0;
|
||||
}
|
||||
console.log(width("abc") + width(4) + width(true));
|
||||
`);
|
||||
const threeCompares = guardCompares(three);
|
||||
expect(threeCompares.length).toBeGreaterThan(0);
|
||||
expect(threeCompares.every((c) => c.startsWith("eq "))).toBe(true);
|
||||
});
|
||||
|
||||
test("instanceof narrowing verifies the downcast class interval", async () => {
|
||||
const llvm = await emitLlvm(`
|
||||
class Base { id = 1; }
|
||||
class Child extends Base { values = [2, 4]; }
|
||||
function count(value: Base): number {
|
||||
if (value instanceof Child) return value.values.length;
|
||||
return value.id;
|
||||
}
|
||||
console.log(count(new Child()), count(new Base()));
|
||||
`);
|
||||
expect(guardCount(llvm, "scriptc: internal error: a downcast object")).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("catch-binding narrowing verifies the caught kind and class", async () => {
|
||||
const llvm = await emitLlvm(`
|
||||
class Failure extends Error { code = 7; }
|
||||
function run(which: number): string {
|
||||
try {
|
||||
if (which === 0) throw "text";
|
||||
throw new Failure("bad");
|
||||
} catch (e) {
|
||||
if (typeof e === "string") return e;
|
||||
if (e instanceof Failure) return String(e.code);
|
||||
return "other";
|
||||
}
|
||||
}
|
||||
console.log(run(0), run(1));
|
||||
`);
|
||||
expect(guardCount(llvm, "scriptc: internal error: a narrowed catch binding")).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
test("programs without narrowings carry no guards", async () => {
|
||||
const llvm = await emitLlvm(`
|
||||
let total = 0;
|
||||
for (let i = 0; i < 10; i++) total += i;
|
||||
console.log(total);
|
||||
`);
|
||||
expect(llvm).not.toContain("scriptc: internal error: a narrowed");
|
||||
expect(llvm).not.toContain("scriptc: internal error: a downcast");
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5770-guarded-union-narrows.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5771-guarded-downcasts.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5772-guarded-catch-narrows.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5773-stale-narrowing-inherited.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Union narrowings that TypeScript proves are verified at run time: every
|
||||
// narrowing below reads its payload through a tag guard. Correct programs
|
||||
// never trip it, so output matches Node across typeof, nullable, tagged,
|
||||
// switch, early-return, loop and closure narrowings, including payloads
|
||||
// borrowed for field reads and owned ones stored into arrays.
|
||||
type Shape =
|
||||
| { kind: "circle"; r: number }
|
||||
| { kind: "rect"; w: number; h: number }
|
||||
| { kind: "label"; text: string };
|
||||
|
||||
class Node2 {
|
||||
value: number;
|
||||
next: Node2 | null;
|
||||
constructor(value: number, next: Node2 | null) {
|
||||
this.value = value;
|
||||
this.next = next;
|
||||
}
|
||||
}
|
||||
|
||||
function describe(v: string | number | boolean): string {
|
||||
if (typeof v === "string") return `string:${v.length}:${v.toUpperCase()}`;
|
||||
if (typeof v === "number") return `number:${(v * 2).toFixed(1)}`;
|
||||
return `boolean:${v ? "yes" : "no"}`;
|
||||
}
|
||||
|
||||
function area(s: Shape): number {
|
||||
switch (s.kind) {
|
||||
case "circle":
|
||||
return Math.round(Math.PI * s.r * s.r);
|
||||
case "rect":
|
||||
return s.w * s.h;
|
||||
case "label":
|
||||
return s.text.length;
|
||||
}
|
||||
}
|
||||
|
||||
function sumList(head: Node2 | null): number {
|
||||
let total = 0;
|
||||
let cur = head;
|
||||
while (cur !== null) {
|
||||
total += cur.value;
|
||||
cur = cur.next;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function firstWord(text: string | undefined): string {
|
||||
if (text === undefined) return "<none>";
|
||||
const words = text.split(" ");
|
||||
return words[0] ?? "";
|
||||
}
|
||||
|
||||
function pickNames(items: (string | number)[]): string[] {
|
||||
const names: string[] = [];
|
||||
for (const item of items) {
|
||||
if (typeof item !== "string") continue;
|
||||
names.push(item);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function makeFormatter(prefix: string | null): (n: number) => string {
|
||||
const p = prefix ?? "#";
|
||||
return (n) => `${p}${n}`;
|
||||
}
|
||||
|
||||
const values: (string | number | boolean)[] = ["abc", 4.25, true, "", 0, false];
|
||||
for (const v of values) console.log(describe(v));
|
||||
|
||||
const shapes: Shape[] = [
|
||||
{ kind: "circle", r: 2 },
|
||||
{ kind: "rect", w: 3, h: 5 },
|
||||
{ kind: "label", text: "hello" },
|
||||
];
|
||||
let totalArea = 0;
|
||||
for (let round = 0; round < 1000; round++) for (const s of shapes) totalArea += area(s);
|
||||
console.log("area", totalArea);
|
||||
|
||||
let list: Node2 | null = null;
|
||||
for (let i = 1; i <= 100; i++) list = new Node2(i, list);
|
||||
console.log("list", sumList(list), sumList(null));
|
||||
|
||||
console.log(firstWord("guarded narrows"), firstWord(undefined), firstWord(""));
|
||||
console.log(pickNames(["a", 1, "b", 2, "c"]).join(","));
|
||||
const fmt = makeFormatter(null);
|
||||
const fmt2 = makeFormatter("n=");
|
||||
console.log(fmt(1), fmt2(2));
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// instanceof narrowing downcasts objects to subclasses; the downcast is
|
||||
// verified against the target's preorder intervals at run time. Covers
|
||||
// multi-level hierarchies, abstract bases, generic classes, and receivers
|
||||
// borrowed for field reads as well as owned values passed on.
|
||||
abstract class Animal {
|
||||
name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
abstract sound(): string;
|
||||
}
|
||||
class Dog extends Animal {
|
||||
tricks = ["sit", "roll"];
|
||||
sound(): string {
|
||||
return "woof";
|
||||
}
|
||||
}
|
||||
class Puppy extends Dog {
|
||||
age = 1;
|
||||
sound(): string {
|
||||
return "yip";
|
||||
}
|
||||
}
|
||||
class Cat extends Animal {
|
||||
lives = 9;
|
||||
sound(): string {
|
||||
return "meow";
|
||||
}
|
||||
}
|
||||
|
||||
class Box<T> {
|
||||
value: T;
|
||||
constructor(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
class LabeledBox<T> extends Box<T> {
|
||||
label: string;
|
||||
constructor(value: T, label: string) {
|
||||
super(value);
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
|
||||
function describe(a: Animal): string {
|
||||
if (a instanceof Puppy) return `${a.name} puppy age ${a.age} ${a.tricks.length} tricks`;
|
||||
if (a instanceof Dog) return `${a.name} dog ${a.tricks.join("+")}`;
|
||||
if (a instanceof Cat) return `${a.name} cat ${a.lives} lives`;
|
||||
return `${a.name} unknown`;
|
||||
}
|
||||
|
||||
function keepDogs(all: Animal[]): Dog[] {
|
||||
const dogs: Dog[] = [];
|
||||
for (const a of all) if (a instanceof Dog) dogs.push(a);
|
||||
return dogs;
|
||||
}
|
||||
|
||||
function labelOf(b: Box<number>): string {
|
||||
return b instanceof LabeledBox ? `${b.label}=${b.value}` : `box=${b.value}`;
|
||||
}
|
||||
|
||||
function labelOfText(b: Box<string>): string {
|
||||
return b instanceof LabeledBox ? `${b.label}:${b.value}` : `text:${b.value}`;
|
||||
}
|
||||
|
||||
const zoo: Animal[] = [new Dog("rex"), new Cat("tom"), new Puppy("bit"), new Cat("kit")];
|
||||
for (const a of zoo) console.log(describe(a), a.sound());
|
||||
console.log(
|
||||
"dogs",
|
||||
keepDogs(zoo)
|
||||
.map((d) => d.name)
|
||||
.join(","),
|
||||
);
|
||||
|
||||
let tricks = 0;
|
||||
for (let round = 0; round < 2000; round++)
|
||||
for (const a of zoo) if (a instanceof Dog) tricks += a.tricks.length;
|
||||
console.log("tricks", tricks);
|
||||
|
||||
console.log(labelOf(new Box(1)), labelOf(new LabeledBox(2, "two")));
|
||||
console.log(labelOfText(new Box("a")), labelOfText(new LabeledBox("b", "bee")));
|
||||
@@ -0,0 +1,63 @@
|
||||
// Catch bindings narrowed by typeof or instanceof extract their payload
|
||||
// through a kind or class guard. Covers string, number and boolean throws,
|
||||
// Error subclass hierarchies, and a thrown standalone class.
|
||||
class AppError extends Error {
|
||||
code: number;
|
||||
constructor(message: string, code: number) {
|
||||
super(message);
|
||||
this.name = "AppError";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
class NotFound extends AppError {
|
||||
path: string;
|
||||
constructor(path: string) {
|
||||
super(`missing ${path}`, 404);
|
||||
this.name = "NotFound";
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
class Signal {
|
||||
reason: string;
|
||||
constructor(reason: string) {
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
function fail(which: number): void {
|
||||
if (which === 0) throw "text";
|
||||
if (which === 1) throw 7.5;
|
||||
if (which === 2) throw false;
|
||||
if (which === 3) throw new NotFound("/etc/x");
|
||||
if (which === 4) throw new AppError("bad", 500);
|
||||
if (which === 5) throw new RangeError("range");
|
||||
if (which === 6) throw new Signal("stop");
|
||||
}
|
||||
|
||||
function classify(which: number): string {
|
||||
try {
|
||||
fail(which);
|
||||
return "ok";
|
||||
} catch (e) {
|
||||
if (typeof e === "string") return `string ${e.length} ${e}`;
|
||||
if (typeof e === "number") return `number ${e * 2}`;
|
||||
if (typeof e === "boolean") return `boolean ${!e}`;
|
||||
if (e instanceof NotFound) return `notfound ${e.path} ${e.code}`;
|
||||
if (e instanceof AppError) return `app ${e.code} ${e.message}`;
|
||||
if (e instanceof Error) return `error ${e.name}: ${e.message}`;
|
||||
if (e instanceof Signal) return `signal ${e.reason}`;
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
for (let which = 0; which <= 7; which++) console.log(which, classify(which));
|
||||
|
||||
let codes = 0;
|
||||
for (let i = 0; i < 500; i++) {
|
||||
try {
|
||||
fail(3 + (i % 2));
|
||||
} catch (e) {
|
||||
if (e instanceof AppError) codes += e.code;
|
||||
}
|
||||
}
|
||||
console.log("codes", codes);
|
||||
@@ -0,0 +1,74 @@
|
||||
// A binding initialized or assigned from a stale narrowed read inherits the
|
||||
// stale narrowing: after reset() stores a Lit, TypeScript still types
|
||||
// `current` as Neg, and `const held: Expr = current` narrows `held` to Neg
|
||||
// too. Reads of such bindings observe the value stored now, as Node does:
|
||||
// members every arm has read the current arm, and an arm-only member of
|
||||
// the wrong arm throws the catchable TypeError instead of reading another
|
||||
// record's slot.
|
||||
type Lit = { kind: "lit"; value: number };
|
||||
type Neg = { kind: "neg"; operand: Expr };
|
||||
type Expr = Lit | Neg;
|
||||
|
||||
function show(e: Expr): string {
|
||||
return e.kind === "lit" ? String(e.value) : `-${show(e.operand)}`;
|
||||
}
|
||||
|
||||
function fromInitializer(replace: boolean): string {
|
||||
let current: Expr | null = null;
|
||||
const reset = (): void => {
|
||||
current = { kind: "lit", value: 7 };
|
||||
};
|
||||
current = { kind: "neg", operand: { kind: "lit", value: 1 } };
|
||||
if (replace) reset();
|
||||
if (current === null) return "null";
|
||||
const held: Expr = current;
|
||||
const kind = held.kind;
|
||||
try {
|
||||
return `${kind} ${show(held.operand)}`;
|
||||
} catch (e) {
|
||||
return `${kind} caught ${(e as Error).name}`;
|
||||
}
|
||||
}
|
||||
|
||||
function fromAssignment(replace: boolean): string {
|
||||
let current: Expr | null = null;
|
||||
const reset = (): void => {
|
||||
current = { kind: "lit", value: 8 };
|
||||
};
|
||||
current = { kind: "neg", operand: { kind: "lit", value: 2 } };
|
||||
if (replace) reset();
|
||||
let held: Expr = { kind: "lit", value: 0 };
|
||||
if (current === null) return show(held);
|
||||
held = current;
|
||||
try {
|
||||
return `${held.kind} ${show(held.operand)}`;
|
||||
} catch (e) {
|
||||
return `${held.kind} caught ${(e as Error).name}`;
|
||||
}
|
||||
}
|
||||
|
||||
class Holder {
|
||||
expr: Expr | null = null;
|
||||
replace(): void {
|
||||
this.expr = { kind: "lit", value: 9 };
|
||||
}
|
||||
}
|
||||
|
||||
function fromProperty(replace: boolean): string {
|
||||
const h = new Holder();
|
||||
h.expr = { kind: "neg", operand: { kind: "lit", value: 3 } };
|
||||
if (replace) h.replace();
|
||||
if (h.expr === null) return "null";
|
||||
const held: Expr = h.expr;
|
||||
try {
|
||||
return `${held.kind} ${show(held.operand)}`;
|
||||
} catch (e) {
|
||||
return `${held.kind} caught ${(e as Error).name}`;
|
||||
}
|
||||
}
|
||||
|
||||
for (const replace of [false, true]) {
|
||||
console.log("initializer:", fromInitializer(replace));
|
||||
console.log("assignment:", fromAssignment(replace));
|
||||
console.log("property:", fromProperty(replace));
|
||||
}
|
||||
Reference in New Issue
Block a user