Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/compiler/src/frontend/lowering/lower-classes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js";
import { adaptPromiseExecutor } from "./promise-executor-adapter.js";
import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js";
import {
constructedFromValue,
Expand Down Expand Up @@ -9856,7 +9857,9 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
// catch-side instanceof and the uncaught printer working). First
// settle wins, exactly JS: reject-after-resolve and double-reject
// are no-ops, and an executor throw after any settle is swallowed.
const executor = lowerer.lowerExpr(args[0]!);
let executor = lowerer.lowerExpr(args[0]!);
if (executor.type.kind !== "func") lowerer.badType(args[0]!, lowerer.typeOf(args[0]!));
executor = adaptPromiseExecutor(lowerer, executor, type.inner, loc) ?? executor;
if (executor.type.kind !== "func") lowerer.badType(args[0]!, lowerer.typeOf(args[0]!));
if (executor.type.params.length > 1) {
const rj = executor.type.params[1]!;
Expand Down
9 changes: 7 additions & 2 deletions packages/compiler/src/frontend/lowering/lower-exprs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2132,8 +2132,13 @@ export function caughtRead(
}
// Unnarrowed reads cross into unknown. The caught-value dispatch pass
// retains known native class payloads through live identity capsules;
// other payload kinds use the runtime's ordinary conversion.
if (narrowed?.kind === "dyn") {
// other payload kinds use the runtime's ordinary conversion. A binding
// annotated `any` (`catch (error: any)`) reads the same checked-dynamic
// value: its member reads, writes and deletes go through it.
if (
narrowed?.kind === "dyn" ||
(narrowed === null && (lowerer.typeOf(node).flags & ts.TypeFlags.Any) !== 0)
) {
return { kind: "caughtToDyn", value: ref, type: DYN, loc };
}
lowerer.unsupported("SC1063", node);
Expand Down
102 changes: 102 additions & 0 deletions packages/compiler/src/frontend/lowering/promise-executor-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { varRef } from "../../ir/build.js";
import {
type IrExpr,
type IrLocal,
type IrParam,
type IrType,
type SrcLoc,
VOID,
funcOf,
typeKey,
} from "../../ir/ir.js";
import { newFnCtx, type Lowerer } from "./lowerer.js";

/** An executor VALUE whose parameters are checked-dynamic (a JavaScript
* function declaration: `function run(resolve, reject) {...}; return new
* Promise(run)`): the promise's native executor is an adapter closure that
* receives the native (resolve, reject) pair and calls the original with
* both boxed into the checked-dynamic tree, so the JavaScript body calls
* them like any function value. Null when the executor's shape is not
* adaptable (more than two parameters, a rest parameter, or a parameter
* that neither is dyn nor already the native closure). */
export function adaptPromiseExecutor(
lowerer: Lowerer,
executor: IrExpr,
inner: IrType,
loc: SrcLoc,
): IrExpr | null {
const execT = executor.type;
if (execT.kind !== "func" || execT.rest || execT.params.length > 2) return null;
if (!execT.params.some((p) => p.kind === "dyn")) return null;
const resolveT = funcOf(inner.kind === "void" ? [] : [inner], VOID);
const rejectT = funcOf([{ kind: "object", className: "%Error" }], VOID);
const native = [resolveT, rejectT];
if (
!execT.params.every((p, i) => p.kind === "dyn" || typeKey(p) === typeKey(native[i]!)) ||
!native.slice(0, execT.params.length).every((t) => lowerer.dynConvertible(t))
)
return null;
const adaptT = funcOf(native, VOID);
const key = `promise.executor:${typeKey(execT)}:${typeKey(inner)}`;
let name = lowerer.arrHofHelpers.get(key);
if (name === undefined) {
name = `%promise.executor.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, name);
const impl = `${name}.impl`;
const captured: IrLocal = { id: "f.0", name: "f", type: execT, mutable: false, boxed: true };
const params: IrParam[] = [
{ localId: "resolve.0", name: "resolve", type: resolveT },
{ localId: "reject.0", name: "reject", type: rejectT },
];
const ctx = newFnCtx(false, null, null, VOID);
lowerer.fnStack.push(ctx);
let args: IrExpr[];
try {
args = execT.params.map((p, i) =>
lowerer.coerceToExpected(varRef(params[i]!.localId, native[i]!, loc), p),
);
} finally {
lowerer.fnStack.pop();
}
lowerer.liftedFns.push({
name: impl,
params,
returnType: VOID,
captures: [{ localId: captured.id, name: captured.name, type: execT }],
locals: [
captured,
...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
...ctx.locals,
],
body: [
{
kind: "exprStmt",
expr: {
kind: "callValue",
callee: varRef(captured.id, execT, loc),
args,
type: execT.ret,
loc,
},
loc,
},
],
loc,
});
lowerer.liftedFns.push({
name,
params: [{ localId: captured.id, name: captured.name, type: execT }],
returnType: adaptT,
locals: [captured],
body: [
{
kind: "return",
value: { kind: "closure", fnName: impl, captures: [captured.id], type: adaptT, loc },
loc,
},
],
loc,
});
}
return { kind: "call", callee: name, args: [executor], type: adaptT, loc };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6490-js-named-promise-executor/main.ts"
],
"diags": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6491-catch-any-bindings.ts"
],
"diags": []
}
22 changes: 22 additions & 0 deletions tests/corpus/6490-js-named-promise-executor/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// A JavaScript library that builds its promise from a named executor
// function (`function run(resolve, reject) {...}; return new Promise(run)`,
// the classic retry helper shape): the executor's parameters are untyped,
// so it receives the native resolve and reject as checked-dynamic function
// values and calls them like any JavaScript function.
import { attempt } from "later";
async function main(): Promise<void> {
const v = await attempt(async (_bail: (e: Error) => void, n: number): Promise<string> => {
if (n < 3) throw new Error("try " + n);
return "done after " + n;
}, 5);
console.log(v);
try {
await attempt((bail: (e: Error) => void): string => {
bail(new Error("stop"));
return "ignored";
}, 5);
} catch (e) {
console.log("caught", (e as Error).message);
}
}
main();

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions tests/corpus/6490-js-named-promise-executor/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"compilerOptions": { "strict": true, "types": ["node"] }
}
34 changes: 34 additions & 0 deletions tests/corpus/6491-catch-any-bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// A catch binding annotated `any` reads the caught value through its
// checked-dynamic face: member reads, expando writes, `delete` and `in`
// act on the thrown object itself, and a rethrow keeps its identity.
const thrown: Error[] = [];

function fail(n: number): never {
const e = new Error(`failure ${n}`);
thrown.push(e);
throw e;
}

for (const n of [1, 2]) {
try {
try {
fail(n);
} catch (error: any) {
error.bail = n === 1;
console.log("marked:", error.bail, "bail" in error, error.message);
if (error.bail) {
delete error.bail;
console.log("deleted:", error.bail, "bail" in error);
}
throw error;
}
} catch (outer) {
console.log("outer:", outer === thrown[n - 1], (outer as Error).message, "bail" in (outer as object));
}
}

try {
JSON.parse("{");
} catch (error: any) {
console.log(error.name, typeof error.message, error instanceof SyntaxError);
}
Loading