mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Lower optional-chain promise methods, promise-or-unit Promise.resolve, and Array.from(typedArray, mapper) (#2465)
This commit is contained in:
@@ -5,7 +5,11 @@ import * as ts from "../../ts7/adapter.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { countedFor, varRef } from "../../../ir/build.js";
|
||||
import { arrayValueStore, optionalElementValue, primitiveElement } from "../array-values.js";
|
||||
import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js";
|
||||
import {
|
||||
maybePromiseResolve,
|
||||
maybePromiseUnionOf,
|
||||
promiseOrUnitUnionOf,
|
||||
} from "../maybe-promise.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -670,11 +674,17 @@ export function lowerPromiseStaticCall(
|
||||
if (v.type.kind !== "promise") lowerer.badType(argNode, argT);
|
||||
return v;
|
||||
}
|
||||
const maybePromise = argIr ? maybePromiseUnionOf(lowerer, argIr) : null;
|
||||
// A promise-or-unit union (`Promise<void> | void`,
|
||||
// `Promise<T> | undefined`) resolves exactly like a maybe-promise
|
||||
// one: Node adopts a promise operand and fulfills every other value
|
||||
// as itself (a nullish arm fulfills undefined).
|
||||
const maybePromise = argIr
|
||||
? (maybePromiseUnionOf(lowerer, argIr) ?? promiseOrUnitUnionOf(lowerer, argIr))
|
||||
: null;
|
||||
if (maybePromise) {
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const v = lowerer.lowerExpr(argNode);
|
||||
const mp = maybePromiseUnionOf(lowerer, v.type);
|
||||
const mp = maybePromiseUnionOf(lowerer, v.type) ?? promiseOrUnitUnionOf(lowerer, v.type);
|
||||
const resolved =
|
||||
resultT?.kind === "promise" && mp
|
||||
? maybePromiseResolve(lowerer, v, mp, resultT, loc)
|
||||
|
||||
@@ -1158,6 +1158,37 @@ export function lowerArrayFromCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// `Array.from(typedArray, mapper)`: the element read is live (a
|
||||
// mapper that writes the source changes later reads, as in Node),
|
||||
// so this is a loop over the typed array, not a toArray snapshot.
|
||||
if (src.type.kind === "bytes" && args.length === 2) {
|
||||
const callback = lowerArrayCallback(lowerer, args[1]!, [F64], arrayOf(F64));
|
||||
if (callback.arity > 2)
|
||||
lowerer.noLowering("Array.from mapper with an array parameter", args[1]!);
|
||||
const fnRet = callback.fnArg.type.ret;
|
||||
if (fnRet.kind === "void" || fnRet.kind === "func")
|
||||
lowerer.badType(call, lowerer.typeOf(call));
|
||||
requireProducedArrayElement(lowerer, call, "'Array.from(typedArray, mapper)'", fnRet);
|
||||
const outElem = callbackArrayElement(lowerer, call, fnRet);
|
||||
// The element kind rides in the key because the lifted helper's
|
||||
// source parameter carries it (the reads themselves are always f64).
|
||||
const key = `fromBytes:${src.type.elem}:${typeKey(outElem)}:${typeKey(fnRet)}:${callback.arity}`;
|
||||
let helper = lowerer.arrHofHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = keyedHelperName(lowerer, "%arr.fromBytes", key);
|
||||
lowerer.arrHofHelpers.set(key, helper);
|
||||
lowerer.liftedFns.push(
|
||||
buildArrayFromBytesFn(lowerer, helper, src.type, outElem, fnRet, callback.arity, loc),
|
||||
);
|
||||
}
|
||||
return {
|
||||
kind: "call",
|
||||
callee: helper,
|
||||
args: [src, callback.fnArg],
|
||||
type: arrayOf(outElem),
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A union of typed arrays and number arrays (indexed-unions.ts).
|
||||
const indexedArms = indexedUnionArms(lowerer, src.type);
|
||||
if (indexedArms && args.length === 1)
|
||||
@@ -1320,6 +1351,96 @@ function buildArrayFromArrayFn(
|
||||
};
|
||||
}
|
||||
|
||||
/** The `Array.from(typedArray, mapper)` generation loop: every index is
|
||||
* read through the typed array itself (live, like Node's iteration), so
|
||||
* a mapper that writes the source is visible to the indexes it has not
|
||||
* visited yet. */
|
||||
function buildArrayFromBytesFn(
|
||||
lowerer: Lowerer,
|
||||
name: string,
|
||||
bytesT: IrType & { kind: "bytes" },
|
||||
outElem: IrType,
|
||||
fnRet: IrType,
|
||||
arity: number,
|
||||
loc: SrcLoc,
|
||||
): IrFunction {
|
||||
const outT = arrayOf(outElem);
|
||||
const fnT = funcOf([F64, F64].slice(0, arity), fnRet);
|
||||
const a = varRef("a.0", bytesT, loc);
|
||||
const i = varRef("i.0", F64, loc);
|
||||
const out = varRef("out.0", outT, loc);
|
||||
// The index parameter is the second one, never read for an index the
|
||||
// element covers, so an element-only mapper drops it.
|
||||
const reads: IrExpr[] = [
|
||||
{ kind: "bytesIntrinsic", method: "get", receiver: a, args: [i], type: F64, loc },
|
||||
i,
|
||||
];
|
||||
const mapped: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee: varRef("f.0", fnT, loc),
|
||||
args: reads.slice(0, arity),
|
||||
type: fnRet,
|
||||
loc,
|
||||
};
|
||||
const held = !typeEquals(fnRet, outElem);
|
||||
const value: IrExpr = held ? varRef("v.0", fnRet, loc) : mapped;
|
||||
return {
|
||||
name,
|
||||
params: [
|
||||
{ localId: "a.0", name: "a", type: bytesT },
|
||||
{ localId: "f.0", name: "f", type: fnT },
|
||||
],
|
||||
returnType: outT,
|
||||
locals: [
|
||||
{ id: "a.0", name: "a", type: bytesT, mutable: true },
|
||||
{ id: "f.0", name: "f", type: fnT, mutable: true },
|
||||
{ id: "out.0", name: "out", type: outT, mutable: false },
|
||||
{ id: "i.0", name: "i", type: F64, mutable: true },
|
||||
...(held ? [{ id: "v.0", name: "v", type: fnRet, mutable: false }] : []),
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "out.0",
|
||||
init: { kind: "arrayLit", elems: [], type: outT, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "varDecl", localId: "i.0", init: numLit(0, loc), loc },
|
||||
{
|
||||
kind: "while",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "<",
|
||||
left: i,
|
||||
right: {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "length",
|
||||
receiver: a,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
body: [
|
||||
...(held ? [{ kind: "varDecl", localId: "v.0", init: mapped, loc } as const] : []),
|
||||
arrayValueStore(lowerer, out, i, value, outElem, loc),
|
||||
{
|
||||
kind: "assign",
|
||||
localId: "i.0",
|
||||
value: { kind: "bin", op: "+", left: i, right: numLit(1, loc), type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: out, loc },
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `Array.from(s)` / `[...s]` on a STRING: the code-point split into a
|
||||
* fresh string[] (scr_str_code_points). */
|
||||
export function strCharsCall(src: IrExpr, loc: SrcLoc): IrExpr {
|
||||
|
||||
@@ -279,7 +279,11 @@ import { isThisParameter, type ParamShape } from "./call-signatures.js";
|
||||
import { dynamicOverloadChoice } from "./overload-choice.js";
|
||||
import { lowerStringIteratorValue } from "./iterator-adapters.js";
|
||||
import { checkedArmExtraction } from "./coercions/unions.js";
|
||||
import { maybePromiseReturnStmt, promiseArmedUnionOf } from "./maybe-promise.js";
|
||||
import {
|
||||
maybePromiseReturnStmt,
|
||||
promiseArmedUnionOf,
|
||||
promiseOrUnitUnionOf,
|
||||
} from "./maybe-promise.js";
|
||||
import {
|
||||
requireObjLitGenericReceiver,
|
||||
objLitGenericFnNodeOf,
|
||||
@@ -5998,9 +6002,19 @@ export function lowerPromiseMethodCall(
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (call.questionDotToken || access.questionDotToken) return null;
|
||||
if (lowerer.chainBlocked(access, call)) return null;
|
||||
const member = access.name.text;
|
||||
if (member !== "then" && member !== "catch" && member !== "finally") return null;
|
||||
// A call that ENDS an optional chain (`refreshing?.catch(h)`,
|
||||
// `client?.close().catch(h)`) checker-types as the chain's
|
||||
// undefined-armed union while the body this lowering builds is the
|
||||
// promise arm — `finishOptionalChain` wraps it back into the union.
|
||||
// Take the arm so the handler typing sees the promise it handles.
|
||||
const callResultType = (): IrType | undefined => {
|
||||
const t = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (!t || t.kind === "promise") return t ?? undefined;
|
||||
return promiseOrUnitUnionOf(lowerer, t)?.promise ?? t;
|
||||
};
|
||||
let recvT =
|
||||
lowerer.mapTypeOf(lowerer.typeOf(access.expression)) ??
|
||||
dynFallbackType(lowerer, access.expression, lowerer.typeOf(access.expression));
|
||||
@@ -6117,7 +6131,7 @@ export function lowerPromiseMethodCall(
|
||||
const rejectedNode = call.arguments.nodes[1]!;
|
||||
const fulfilledAbsent = isAbsentHandler(fulfilledNode);
|
||||
const rejectedAbsent = isAbsentHandler(rejectedNode);
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const resultT = callResultType();
|
||||
// An untyped call in JavaScript whose receiver arrived as a typed
|
||||
// promise (a function instantiated with one): Promise.prototype.then
|
||||
// over the promise as a checked-dynamic value, which shares its
|
||||
@@ -6516,7 +6530,7 @@ export function lowerPromiseMethodCall(
|
||||
`then handlers whose parameter is not the settled value's type (expected '${lowerer.fmt(inner)}', got '${lowerer.fmt(param)}')`,
|
||||
);
|
||||
}
|
||||
let resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
let resultT = callResultType();
|
||||
// JavaScript whose declared result type has no mapping (zod's
|
||||
// `Promise.all(items).then((r) => ParseStatus.mergeArray(status, r))`,
|
||||
// typed by a .d.ts union): the handler's value settles the promise as
|
||||
@@ -6864,7 +6878,7 @@ export function lowerPromiseMethodCall(
|
||||
"value — take `(e)` or `(e: unknown)` and narrow with instanceof)",
|
||||
);
|
||||
}
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const resultT = callResultType();
|
||||
if (resultT?.kind !== "promise") {
|
||||
lowerer.noLowering(
|
||||
"catch with this handler's result type",
|
||||
@@ -6908,7 +6922,10 @@ export function lowerPromiseMethodCall(
|
||||
if (ts.isBlock(hb)) {
|
||||
catchBody = lowerer.lowerStmts(hb.statements.nodes);
|
||||
} else if (R.kind === "void") {
|
||||
catchBody = [{ kind: "exprStmt", expr: lowerer.lowerExpr(hb), loc: locOf(hb) }];
|
||||
// The handler's value is discarded, so the body lowers as a
|
||||
// statement: `() => undefined` is a pure no-op (a bare unit
|
||||
// literal has no IR value), not an expression to evaluate.
|
||||
catchBody = [lowerer.lowerExprStatement(hb)];
|
||||
} else {
|
||||
// Bare-expression handler: `(e) => v` (promise results flatten
|
||||
// through the async-return path, like any `return v`).
|
||||
|
||||
@@ -23,6 +23,18 @@ export function maybePromiseUnionOf(lowerer: Lowerer, t: IrType): MaybePromiseUn
|
||||
return armed;
|
||||
}
|
||||
|
||||
/** The promise arm of a promise-or-unit union (`Promise<void> | void`,
|
||||
* `Promise<T> | undefined`): exactly one promise arm and only nullish
|
||||
* arms beside it, so no other arm carries a value the promise path would
|
||||
* have to reconcile. A guard that proves the value present (`?.`, an
|
||||
* explicit narrow) leaves this arm alone. */
|
||||
export function promiseOrUnitUnionOf(lowerer: Lowerer, t: IrType): MaybePromiseUnion | null {
|
||||
const armed = promiseArmedUnionOf(lowerer, t);
|
||||
if (!armed) return null;
|
||||
if (!armed.def.arms.every((a, i) => i === armed.promiseTag || isUnitType(a))) return null;
|
||||
return armed;
|
||||
}
|
||||
|
||||
/** Any union with exactly one promise arm: a maybe-promise union or a
|
||||
* promise-or-unit union (`Promise<void> | void`). An async `return`
|
||||
* resolves both the same way: adopt the promise arm, fulfill otherwise. */
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/array-from-typed-array-mapper.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/promise-catch-optional-chain.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/promise-resolve-promise-or-unit.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/promise-void-catch-undefined.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// `Array.from(typedArray, mapper)`: the mapper receives each element as a
|
||||
// number and its index, and the element reads are LIVE — iteration reads
|
||||
// index i after visiting i-1, so a mapper that writes the source changes
|
||||
// the values the loop has not visited yet. The result is a plain array of
|
||||
// the mapper's results.
|
||||
|
||||
const bytes = new Uint8Array([104, 105, 33]);
|
||||
console.log(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(""));
|
||||
console.log(Array.from(bytes, (byte, index) => `${index}:${byte}`).join(","));
|
||||
|
||||
const live = new Uint8Array([1, 2, 3]);
|
||||
const doubled = Array.from(live, (value, index) => {
|
||||
if (index === 0) live[2] = 30;
|
||||
return value * 2;
|
||||
});
|
||||
console.log(doubled.join(","));
|
||||
|
||||
// A mapper that ignores its parameters, and an empty source.
|
||||
console.log(Array.from(new Uint8Array(0), () => 7).length);
|
||||
console.log(Array.from(bytes, () => "x").join(""));
|
||||
|
||||
// Element identity survives the map: objects are not copied.
|
||||
const words = new Uint8Array([1, 2]);
|
||||
const boxed = Array.from(words, (word) => ({ word }));
|
||||
console.log(boxed.length, boxed[1]!.word);
|
||||
@@ -0,0 +1,64 @@
|
||||
// `?.` reaches a promise method through a receiver that may be nullish:
|
||||
// `p?.catch(handler)` guards the method read (JS calls nothing when `p` is
|
||||
// nullish), and the chain tail `f()?.then(..).catch(..)` guards the whole
|
||||
// tail. The guarded call itself is an ordinary promise method call, so the
|
||||
// handler typing sees the promise arm, and the chain result is
|
||||
// `Promise<..> | undefined`.
|
||||
|
||||
interface Client {
|
||||
name: string;
|
||||
}
|
||||
|
||||
class Box {
|
||||
opening: Promise<Client> | undefined;
|
||||
pending: Promise<void> | undefined;
|
||||
|
||||
constructor(opening: Promise<Client> | undefined, pending: Promise<void> | undefined) {
|
||||
this.opening = opening;
|
||||
this.pending = pending;
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
this.opening?.catch(() => undefined);
|
||||
console.log("1 opening guarded");
|
||||
await this.pending?.catch(() => undefined);
|
||||
console.log("2 pending guarded");
|
||||
}
|
||||
}
|
||||
|
||||
function open(name: string): Promise<Client> {
|
||||
return Promise.resolve({ name });
|
||||
}
|
||||
|
||||
function pick(flag: boolean): Promise<string> | undefined {
|
||||
return flag ? Promise.resolve("value") : undefined;
|
||||
}
|
||||
|
||||
function tail(flag: boolean): Promise<number | undefined> {
|
||||
return pick(flag)!
|
||||
.then((value) => {
|
||||
console.log("tail then", value);
|
||||
return value.length;
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
function guardedTail(flag: boolean): void {
|
||||
pick(flag)?.then((value) => {
|
||||
console.log("guarded then", value);
|
||||
return value.length;
|
||||
});
|
||||
console.log("3 tail attached", flag);
|
||||
}
|
||||
|
||||
const present = new Box(open("digest"), Promise.resolve());
|
||||
const absent = new Box(undefined, undefined);
|
||||
present
|
||||
.run()
|
||||
.then(() => absent.run())
|
||||
.then(() => tail(true))
|
||||
.then(() => {
|
||||
guardedTail(true);
|
||||
guardedTail(false);
|
||||
})
|
||||
.then(() => console.log("done"));
|
||||
@@ -0,0 +1,34 @@
|
||||
// `Promise.resolve(u)` over a promise-or-unit union (`Promise<void> |
|
||||
// void`): Node adopts a promise operand — the SAME promise comes
|
||||
// back — and fulfills a nullish one as undefined. The settled turn of the
|
||||
// adopted promise is unchanged, so a handler chain observes the same
|
||||
// microtask order either way.
|
||||
|
||||
let runs = 0;
|
||||
|
||||
async function step(label: string): Promise<void> {
|
||||
runs++;
|
||||
await Promise.resolve();
|
||||
console.log("step", label, runs);
|
||||
}
|
||||
|
||||
// A callback whose declared result is `Promise<void> | void`: the
|
||||
// async path answers a promise, the sync one answers undefined.
|
||||
function handler(flag: boolean): Promise<void> | void {
|
||||
if (flag) return step("async");
|
||||
runs++;
|
||||
console.log("sync handler", runs);
|
||||
}
|
||||
|
||||
function call(flag: boolean): Promise<void> {
|
||||
return Promise.resolve(handler(flag)).then(() => console.log("call done", flag));
|
||||
}
|
||||
|
||||
const pending: Promise<void> | void = Promise.resolve();
|
||||
console.log("identity", Promise.resolve(pending) === pending);
|
||||
|
||||
console.log("start");
|
||||
call(true)
|
||||
.then(() => call(false))
|
||||
.then(() => call(true))
|
||||
.then(() => console.log("runs", runs));
|
||||
@@ -0,0 +1,31 @@
|
||||
// `Promise<void>.catch(() => undefined)` — the handler's value is
|
||||
// discarded and the recovered promise settles as void. The handler runs
|
||||
// exactly once, and the chain continues on the next turn whether the
|
||||
// receiver fulfilled or rejected.
|
||||
|
||||
const ok: Promise<void> = Promise.resolve();
|
||||
const bad: Promise<void> = Promise.reject(new Error("boom"));
|
||||
|
||||
ok.catch(() => undefined).then(() => console.log("fulfilled recovered"));
|
||||
|
||||
bad
|
||||
.catch(() => {
|
||||
console.log("rejected recovered");
|
||||
})
|
||||
.then(() => console.log("rejected continued"));
|
||||
|
||||
let runs = 0;
|
||||
|
||||
function recover(): Promise<void> {
|
||||
runs++;
|
||||
return runs % 2 === 1 ? Promise.reject(new Error("odd")) : Promise.resolve();
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
await recover().catch(() => undefined);
|
||||
console.log("awaited recover", runs);
|
||||
await recover().catch(() => undefined);
|
||||
console.log("awaited recover", runs);
|
||||
}
|
||||
|
||||
void run().then(() => console.log("done"));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1400, "san": 1200 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1000, "san": 900 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 900, "san": 900 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1600, "san": 900 }
|
||||
Reference in New Issue
Block a user