diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 83cbe6d54..5b5ec33bc 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -1038,6 +1038,29 @@ export function emitDynamicExpr( B.line(`${n} = xor i1 ${t}, true`); return { name: n, type: e.type }; } + if (e.test === "dynamic") { + // A REF payload whose retain adapter is the dyn adapter (sc_cd's + // by-reference arm). + host.declare(`declare ptr @scr_dyn_retain_v(ptr)`); + const kp = B.tmp(), + k = B.tmp(), + isRef = B.tmp(), + rp = B.tmp(), + r = B.tmp(), + isDyn = B.tmp(), + t = B.tmp(); + B.line(`${kp} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 1`); + B.line(`${k} = load i32, ptr ${kp}`); + B.line(`${isRef} = icmp eq i32 ${k}, 4`); + B.line(`${rp} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 5`); + B.line(`${r} = load ptr, ptr ${rp}`); + B.line(`${isDyn} = icmp eq ptr ${r}, @scr_dyn_retain_v`); + B.line(`${t} = and i1 ${isRef}, ${isDyn}`); + if (e.negated !== true) return { name: t, type: e.type }; + const n = B.tmp(); + B.line(`${n} = xor i1 ${t}, true`); + return { name: n, type: e.type }; + } if (e.test === "object") { host.declare(`declare zeroext i1 @scr_caught_is_object(ptr)`); const t = B.tmp(); diff --git a/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts b/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts index a07741461..645ff8d2c 100644 --- a/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts @@ -1,6 +1,16 @@ import { varRef } from "../../ir/build.js"; -import { BOOL, CAUGHT, DYN, type IrFunction, type IrType } from "../../ir/ir.js"; +import { + BOOL, + CAUGHT, + DYN, + type IrExpr, + type IrFunction, + type IrType, + type SrcLoc, +} from "../../ir/ir.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; +import { classInstanceOf } from "./class-dynamic-dispatch.js"; +import type { ClassInfo } from "./lower-classes.js"; import type { Lowerer } from "./lowerer.js"; /** Exception snapshots retain native objects. Recover their known layouts @@ -10,6 +20,10 @@ export class CaughtValueDispatch { private readonly thrownClasses = new Set(); private readonly convertedClasses = new Set(); private helper: IrFunction | null = null; + /** The dynamic-payload helpers of class tests and extractions, by kind + * and class (dynamicPayloads). */ + private readonly payloadHelpers = new Map(); + private readonly payloadHelperFns = new Set(); process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean { const consumers: IrFunction[] = []; @@ -81,4 +95,164 @@ export class CaughtValueDispatch { } return changed; } + + /** A thrown unknown or union value, and the runtime's own dynamic throws, + * reach a catch binding as a dynamic payload. When an instance of class C + * can be such a payload (C or a subclass of C crosses into unknown, the + * `boxed` classes), `e instanceof C`, the reads it narrows and `e as C` in + * `sites` also answer through the dynamic class test, as standalone classes + * already do. Programs that never box C keep the native payload test. */ + dynamicPayloads( + lowerer: Lowerer, + sites: ReadonlySet, + boxed: ReadonlySet, + ): boolean { + if (boxed.size === 0 || sites.size === 0) return false; + const reached = new Map(); + const reachable = (className: string | undefined): ClassInfo | null => { + if (className === undefined) return null; + let info = reached.get(className); + if (info === undefined) { + const found = lowerer.classes.get(className); + info = + found && + lowerer.inHierarchy(found) && + [...boxed].some((name) => name === className || lowerer.isSubclassOf(name, className)) + ? found + : null; + reached.set(className, info); + } + return info; + }; + let changed = false; + const qualifies = (expr: IrExpr): boolean => + expr.kind === "caughtTest" + ? expr.test === "instanceof" && reachable(expr.className) !== null + : (expr.kind === "caughtNarrow" || expr.kind === "caughtCheck") && + expr.type.kind === "object" && + reachable(expr.type.className) !== null; + for (const fn of sites) { + // The conversions themselves test the native payload. + if (fn === this.helper || this.payloadHelperFns.has(fn)) continue; + const found = !everyStmtList(fn.body, { stmt: () => true, expr: (expr) => !qualifies(expr) }); + if (!found) continue; + fn.body = transformStmtList(fn.body, { + stmt: (stmt) => stmt, + expr: (expr) => { + if (expr.kind === "caughtTest" && expr.test === "instanceof") { + const info = reachable(expr.className); + if (!info) return expr; + changed = true; + const call: IrExpr = { + kind: "call", + callee: this.payloadHelper(lowerer, "instanceof", info, expr.loc).name, + args: [expr.value], + type: BOOL, + loc: expr.loc, + }; + return expr.negated + ? { kind: "unary", op: "!", operand: call, type: BOOL, loc: expr.loc } + : call; + } + if ( + (expr.kind === "caughtNarrow" || expr.kind === "caughtCheck") && + expr.type.kind === "object" + ) { + const info = reachable(expr.type.className); + if (!info) return expr; + changed = true; + const kind = expr.kind === "caughtNarrow" ? "narrow" : "check"; + return { + kind: "call", + callee: this.payloadHelper(lowerer, kind, info, expr.loc).name, + args: [expr.value], + type: expr.type, + loc: expr.loc, + }; + } + return expr; + }, + }); + } + return changed; + } + + /** `%caught.instanceof:C`, `%caught.narrow:C` and `%caught.check:C`: the + * native payload test or extraction, and otherwise a value thrown as + * unknown tested against C and checked into C. A check of a payload that + * is not a C keeps caughtCheck's TypeError. */ + private payloadHelper( + lowerer: Lowerer, + kind: "instanceof" | "narrow" | "check", + info: ClassInfo, + loc: SrcLoc, + ): IrFunction { + const className = info.def.name; + const name = `%caught.${kind}:${className}`; + const existing = this.payloadHelpers.get(name); + if (existing) return existing; + const caught = (): IrExpr => varRef("caught", CAUGHT, loc); + const dynamic = (): IrExpr => ({ kind: "caughtToDyn", value: caught(), type: DYN, loc }); + const native: IrExpr = { + kind: "caughtTest", + value: caught(), + test: "instanceof", + className, + type: BOOL, + loc, + }; + // Only a value thrown as unknown can hold an instance besides a native + // payload, which the native test answers exactly. + const dynamicInstance = (): IrExpr => ({ + kind: "logical", + op: "&&", + left: { kind: "caughtTest", value: caught(), test: "dynamic", type: BOOL, loc }, + right: classInstanceOf(lowerer, dynamic(), info, loc), + type: BOOL, + loc, + }); + const type: IrType = { kind: "object", className }; + const returnType = kind === "instanceof" ? BOOL : type; + let value: IrExpr; + if (kind === "instanceof") { + value = { + kind: "ternary", + cond: native, + then: { kind: "boolLit", value: true, type: BOOL, loc }, + else_: dynamicInstance(), + type: BOOL, + loc, + }; + } else if (kind === "narrow") { + value = { + kind: "ternary", + cond: native, + then: { kind: "caughtNarrow", value: caught(), type, loc }, + else_: lowerer.coerceToExpected(dynamic(), type), + type, + loc, + }; + } else { + value = { + kind: "ternary", + cond: dynamicInstance(), + then: lowerer.coerceToExpected(dynamic(), type), + else_: { kind: "caughtCheck", value: caught(), className, type, loc }, + type, + loc, + }; + } + const helper: IrFunction = { + name, + params: [{ localId: "caught", name: "caught", type: CAUGHT }], + locals: [{ id: "caught", name: "caught", type: CAUGHT, mutable: false }], + returnType, + body: [{ kind: "return", value, loc }], + loc, + }; + this.payloadHelpers.set(name, helper); + this.payloadHelperFns.add(helper); + lowerer.liftedFns.push(helper); + return helper; + } } diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index cb5c28444..252e3fa4b 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -762,6 +762,7 @@ export class ClassDynamicDispatch { const rewrite = new Set(); const toPrimitiveSites = new Set(); const hasKeySites = new Set(); + const caughtSites = new Set(); const discover = (type: IrType): void => { const key = typeKey(type); if (seenTypes.has(key)) return; @@ -827,6 +828,13 @@ export class ClassDynamicDispatch { case "dynHasKey": hasKeySites.add(fn); break; + case "caughtTest": + if (expr.test === "instanceof") caughtSites.add(fn); + break; + case "caughtNarrow": + case "caughtCheck": + if (expr.type.kind === "object") caughtSites.add(fn); + break; case "dynKeyGet": case "dynInvoke": rewrite.add(fn); @@ -865,6 +873,7 @@ export class ClassDynamicDispatch { return true; }, }); + changed = this.caughtDispatch.dynamicPayloads(lowerer, caughtSites, this.boxed) || changed; if (this.boxed.size === 0 && rewrite.size === 0) return changed; const converting = this.convertingHints(lowerer); const calling = this.calledConversions(lowerer); diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 359d45ee7..38ebe4561 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -3544,11 +3544,13 @@ export type IrExpr = * excluding callable and primitive references; * "instanceof" requires `className`: hierarchy payloads use the vtable * interval, while standalone classes use their exact retain adapter. - * Other payloads answer false. `negated` flips the result (the `!==` spelling). */ + * Other payloads answer false. "dynamic" is a value thrown as unknown + * (the payload caughtToDyn returns by reference). `negated` flips the + * result (the `!==` spelling). */ | { kind: "caughtTest"; value: IrExpr; - test: "string" | "number" | "boolean" | "object" | "instanceof"; + test: "string" | "number" | "boolean" | "object" | "instanceof" | "dynamic"; className?: string; negated?: boolean; type: IrType; diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 9635ceb91..74f526e06 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -1117,7 +1117,7 @@ function validateFunction( // helpers. They are not source-level types and never return as values. if (fn.returnType.kind === "caught") err("return type is caught", fn.loc); for (const p of fn.params) { - if (p.type.kind === "caught" && fn.name !== "%caught.dynamicValue") + if (p.type.kind === "caught" && !fn.name.startsWith("%caught.")) err(`param "${p.name}" is caught-typed`, fn.loc); if (!locals.has(p.localId)) { err(`param "${p.name}" has no local entry "${p.localId}"`, fn.loc); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6819-caught-instanceof-dynamic-payloads.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6819-caught-instanceof-dynamic-payloads.ts.json new file mode 100644 index 000000000..6e7228d5a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6819-caught-instanceof-dynamic-payloads.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6819-caught-instanceof-dynamic-payloads.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6819-caught-instanceof-dynamic-payloads.ts b/tests/corpus/6819-caught-instanceof-dynamic-payloads.ts new file mode 100644 index 000000000..c42a803f5 --- /dev/null +++ b/tests/corpus/6819-caught-instanceof-dynamic-payloads.ts @@ -0,0 +1,80 @@ +// `e instanceof C` on a catch binding when the thrown value crossed into +// unknown first: a union, an unknown or any value, a rethrown binding and an +// awaited rejection. The narrowed reads and `e as C` read the same instance. +abstract class Shape { + sides = 0; +} +class Polygon extends Shape { + name = "polygon"; +} +class Square extends Polygon { + size = 3; +} +class Plain { + w = 4; +} +class CodedError extends Error { + code = 7; +} + +function report(label: string, e: unknown): void { + console.log(label, "via unknown", e instanceof Shape, e instanceof Polygon, e instanceof Square, e instanceof Plain); +} + +function attempt(label: string, body: () => void): void { + try { + body(); + } catch (e) { + report(label, e); + console.log(label, "binding", e instanceof Shape, e instanceof Polygon, e instanceof Square, e instanceof Plain, e instanceof Error, e instanceof CodedError); + console.log(label, "negated", !(e instanceof Shape), !(e instanceof Error)); + if (e instanceof Polygon) console.log(label, "narrowed", e.name, e.sides); + if (e instanceof Square) console.log(label, "square", e.size); + if (e instanceof CodedError) console.log(label, "coded", e.code, e.message); + if (e instanceof Shape && !(e instanceof Square)) console.log(label, "shape only", (e as Polygon).name); + } +} + +function throwUnknown(value: unknown): never { + throw value; +} +const choose = (n: number): Polygon | Plain | string => (n === 0 ? new Square() : n === 1 ? new Plain() : n === 2 ? new Polygon() : "text"); +const maybe = (n: number): Square | undefined => (n === 0 ? new Square() : undefined); +const failure = (n: number): CodedError | string => (n === 0 ? new CodedError("failed") : "failed"); + +for (let n = 0; n < 4; n++) attempt(`union ${n}`, () => { throw choose(n); }); +attempt("optional", () => { throw maybe(0); }); +attempt("optional undefined", () => { throw maybe(1); }); +attempt("error union", () => { throw failure(0); }); +attempt("error union string", () => { throw failure(1); }); +attempt("unknown", () => throwUnknown(new Square())); +attempt("unknown plain", () => throwUnknown(new Plain())); +attempt("unknown error", () => throwUnknown(new CodedError("unknown"))); +const anyValue: any = new Polygon(); +attempt("any", () => { throw anyValue; }); +attempt("typed", () => { const shape: Shape = new Square(); throw shape; }); +attempt("rethrow", () => { + try { + throw choose(0); + } catch (inner) { + throw inner; + } +}); + +async function main(): Promise { + try { + await Promise.reject(choose(0)); + } catch (e) { + console.log("rejected", e instanceof Shape, e instanceof Polygon, e instanceof Square, e instanceof Plain); + if (e instanceof Square) console.log("rejected square", e.size, e.name); + } + try { + await (async () => { + throw failure(0); + })(); + } catch (e) { + console.log("async error", e instanceof Error, e instanceof CodedError); + if (e instanceof CodedError) console.log("async coded", e.code); + } +} +void main(); diff --git a/tests/harness/corpus-costs/6819-caught-instanceof-dynamic-payloads.ts.json b/tests/harness/corpus-costs/6819-caught-instanceof-dynamic-payloads.ts.json new file mode 100644 index 000000000..2809fc670 --- /dev/null +++ b/tests/harness/corpus-costs/6819-caught-instanceof-dynamic-payloads.ts.json @@ -0,0 +1 @@ +{ "plain": 3200, "san": 30100 }