Adapt method overrides across ABI differences, write Error codes, and close wrapper-reachable gaps (#1497)

This commit is contained in:
Chris Tate
2026-10-20 12:10:19 -05:00
committed by GitHub
69 changed files with 1941 additions and 99 deletions
@@ -127,6 +127,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
"str.encodeUriComponent": "scr_str_encode_uri_component",
"error.cause": "scr_error_cause",
"error.setCause": "scr_error_set_cause",
"error.setCode": "scr_error_set_code_value",
"error.setCodeDyn": "scr_error_set_code_dyn",
"error.assign": "scr_error_assign_dyn",
"error.hasKey": "scr_error_has_key",
"error.hasCode": "scr_error_has_code",
"error.defineCause": "scr_error_define_cause",
"error.deleteCause": "scr_error_delete_cause",
"error.hasCause": "scr_error_has_cause",
@@ -52,6 +52,20 @@ export function refuseExecExceptionCode(lowerer: Lowerer, node: ts.Node): never
* declaration is not stdlib). Reads only: writes keep their fence (no
* compiled program constructs an errno error). Null for non-error
* receivers and non-stdlib members, so the chain keeps trying. */
/** Error members with slots of their own: never read through the dynamic
* node, even when an intersection restates them. */
const ERROR_SLOT_MEMBERS = new Set([
"code",
"cause",
"stack",
"message",
"name",
"errno",
"syscall",
"path",
"dest",
]);
export function lowerErrorCodeProperty(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
@@ -135,6 +149,33 @@ export function lowerErrorCodeProperty(
declared,
);
}
// A member an `Error & { ... }` intersection adds beyond Error's own and
// the system-error metadata (`Object.assign(new Error(m), { status })`):
// an own property of the error's checked-dynamic node, read at the
// declared type (the intersection mapping admits only checkable types).
if (
recvT.className === "%Error" &&
!ERROR_SLOT_MEMBERS.has(expr.name.text) &&
!expr.questionDotToken &&
!lowerer.isStdlibMember(expr) &&
lowerer.typeOf(expr.expression).isIntersectionType()
) {
const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (declared && declared.kind !== "void") {
const receiver = lowerer.lowerExpr(expr.expression);
const loc = locOf(expr);
return lowerer.coerceToExpected(
{
kind: "dynKeyGet",
value: lowerer.coerceToExpected(receiver, DYN),
key: { kind: "strLit", value: expr.name.text, type: STRING, loc },
type: DYN,
loc,
},
declared,
);
}
}
// A program class under the receiver's class can carry its own `code`
// (a declared field or a property added through an untyped reference),
// which Node reads instead of the errno code: read through the error's
@@ -4,6 +4,7 @@ import { InternalCompilerError } from "../../../errors.js";
import type { IrExpr, IrParam, IrStmt, IrType, SrcLoc } from "../../../ir/ir.js";
import {
canAdaptDynFuncTo,
canDynCheckTo,
DYN,
F64,
isUnitType,
@@ -341,6 +342,19 @@ export function funcCoerceAdapter(
voidRet = toT.ret.kind === "dyn" ? "dyn" : "strand";
}
}
// An untyped result (a generic wrapper's `any` handed back `as F`) takes
// the checked-dynamic extraction to the slot's result: a record checks
// its shape, a Promise keeps its identity and checks its payload when it
// settles.
const checkedRet =
strandRet &&
fromT.ret.kind === "dyn" &&
canDynCheckTo(
toT.ret,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
);
if (checkedRet) strandRet = false;
const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`;
const existing = lowerer.coercions.retags.get(key);
if (existing) return existing;
@@ -440,6 +454,10 @@ export function funcCoerceAdapter(
`a '${lowerer.fmt(fromT)}' function invoked through a '${lowerer.fmt(toT)}' slot (the result cannot convert to '${lowerer.fmt(toT.ret)}' — the checker's loose function compatibility admitted the assignment, but the call has no exact lowering)`,
),
];
} else if (checkedRet) {
body = [
{ kind: "return", value: { kind: "dynCheck", value: call, type: toT.ret, loc }, loc },
];
} else {
const result = lowerer.coerceToExpected(call, toT.ret);
if (!typeEquals(result.type, toT.ret))
@@ -1288,12 +1288,14 @@ export function lowerAsExpression(
// the requested shape; an assertion does not bypass runtime checks.
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
const bridged =
narrowed &&
(narrowed.kind === "f64" ||
narrowed.kind === "bool" ||
narrowed.kind === "string" ||
narrowed.kind === "object" ||
narrowed.kind === "dyn");
(narrowed &&
(narrowed.kind === "f64" ||
narrowed.kind === "bool" ||
narrowed.kind === "string" ||
narrowed.kind === "object" ||
narrowed.kind === "dyn")) ||
// `catch (e: any)`: the binding reads as its checked-dynamic value.
(narrowed === null && (lowerer.typeOf(expr.expression).flags & ts.TypeFlags.Any) !== 0);
if (!bridged) lowerer.unsupported("SC1063", expr);
}
const inner = lowerer.lowerExpr(expr.expression);
@@ -1142,6 +1142,23 @@ export function lowerPropertyAccessExpr(
);
}
}
// A stdlib-declared member (`message` of an interface extending Error)
// on a catch binding narrowed to a structural type: the binding reads
// as its checked-dynamic value, so the member does too.
if (!expr.questionDotToken && lowerer.caughtLocalOf(expr.expression)) {
const recv = lowerer.lowerExpr(expr.expression);
if (recv.kind === "caughtToDyn") {
const read: IrExpr = {
kind: "dynKeyGet",
key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) },
value: recv,
type: DYN,
loc,
};
const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
return declared && declared.kind !== "void" ? lowerer.coerceToExpected(read, declared) : read;
}
}
// The lib fence's PROPERTY chokepoint: a stdlib-declared member that
// no lowering above claimed ([1,2].entries, Math.SQRT2, Promise.all,
// re.exec as a value, ...) reports SC2020 here.
@@ -1405,9 +1405,10 @@ export function lowerInExpression(
// predicate's `err instanceof Error && "code" in err` shape): `code`
// answers from the runtime error's code slot (stamped by fs/system/
// event/spawn errors, absent on plain constructions — exactly Node's
// own-property answer), and the always-declared members fold true.
// Everything else fences: the runtime object carries no other dynamic
// properties to ask (`stack` would be a lie — Node has one, we don't).
// own-property answer), and the always-present members (message, name,
// stack, Object.prototype's keys, members the static class declares)
// fold true. Other keys test the declaring subclasses and the error's
// own runtime properties; keys of built-in errors' extra slots fence.
if (recv.type.kind === "object") {
// %DOMException first: `'cause' in e` answers the options form's
// own-property record (the runtime slot), and code/name/message are
@@ -1429,77 +1430,76 @@ export function lowerInExpression(
if (key === "cause") {
return { kind: "libCall", fn: "error.hasCause", args: [recv], type: BOOL, loc };
}
// The code slot, or an own `code` with another value
// (`err.code = undefined` is present).
if (key === "code") {
const codeType = lowerer.envValueType();
if (codeType.kind !== "union")
throw new InternalCompilerError("lowerer bug: error code type is not a union");
const undefTag = lowerer.armTag(codeType.unionId, UNDEFINED_T);
const read: IrExpr = {
kind: "libCall",
fn: "error.code",
args: [recv],
type: codeType,
loc,
};
return {
kind: "unionIsTag",
unionId: codeType.unionId,
tag: undefTag,
negated: true,
value: read,
type: BOOL,
loc,
};
}
if (
(key === "message" || key === "name") &&
(recv.kind === "varRef" || recv.kind === "caughtNarrow")
) {
return { kind: "boolLit", value: true, type: BOOL, loc };
return { kind: "libCall", fn: "error.hasCode", args: [recv], type: BOOL, loc };
}
// The receiver evaluates for its effects when the answer is static.
const always = (): IrExpr =>
recv.kind === "varRef" || recv.kind === "caughtNarrow"
? { kind: "boolLit", value: true, type: BOOL, loc }
: {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: recv, loc }],
result: { kind: "boolLit", value: true, type: BOOL, loc },
type: BOOL,
loc,
};
if (key === "message" || key === "name") return always();
// Every Error has its own `stack`, and Error.prototype's `toString`
// and `constructor` are among Object.prototype's keys.
if (
(key === "stack" || OBJECT_PROTOTYPE_KEYS.has(key)) &&
(recv.kind === "varRef" || recv.kind === "caughtNarrow")
) {
return { kind: "boolLit", value: true, type: BOOL, loc };
}
// Any other key: a member a program class declares. The receiver's
// runtime class is its static class or a descendant; the key is
// present exactly when that class (or an ancestor) declares it. Keys
// built-in errors keep outside their class fields depend on the
// runtime error and stay fenced, as do keys no program class declares.
if (key === "stack" || OBJECT_PROTOTYPE_KEYS.has(key)) return always();
// A member the static class declares is always present.
const declares = (c: ClassInfo): boolean =>
c.fields.has(key) ||
lowerer.findMethodOn(c, key) !== null ||
lowerer.findMethodOn(c, `get:${key}`) !== null ||
lowerer.findMethodOn(c, `set:${key}`) !== null;
if (
(recv.kind === "varRef" || recv.kind === "caughtNarrow") &&
!BUILTIN_ERROR_SLOT_KEYS.has(key) &&
[...lowerer.classes.values()].some((c) => c.builtinError !== true && declares(c))
) {
const staticName = recv.type.className;
const staticInfo = lowerer.classes.get(staticName);
if (staticInfo && declares(staticInfo))
return { kind: "boolLit", value: true, type: BOOL, loc };
const staticInfo = lowerer.classes.get(recv.type.className);
if (staticInfo && declares(staticInfo)) return always();
// Any other key: a descendant class that declares it (instanceof), or
// an own property set at runtime (a throw site's extra fields,
// Object.assign, writes through a dynamic view). Keys built-in errors
// keep outside their class fields depend on the runtime error and
// stay fenced.
if (!BUILTIN_ERROR_SLOT_KEYS.has(key)) {
const stable = recv.kind === "varRef" || recv.kind === "caughtNarrow";
const slot = stable ? null : lowerer.declareHiddenLocal("%inRecv", recv.type);
const ref = (): IrExpr => (slot ? varRef(slot.id, recv.type, loc) : recv);
const tests: IrExpr[] = [];
const staticName = recv.type.className;
for (const c of lowerer.classes.values()) {
const name = c.def.name;
if (name === staticName || !lowerer.isSubclassOf(name, staticName) || !declares(c))
continue;
// A declaring ancestor below the static class already covers it.
if (c.base && c.base.def.name !== staticName && declares(c.base)) continue;
tests.push({ kind: "instanceOf", value: recv, className: name, type: BOOL, loc });
tests.push({ kind: "instanceOf", value: ref(), className: name, type: BOOL, loc });
}
return tests.reduce<IrExpr>(
(acc, test) =>
acc.kind === "boolLit"
? test
: { kind: "logical", op: "||", left: acc, right: test, type: BOOL, loc },
{ kind: "boolLit", value: false, type: BOOL, loc },
);
tests.push({
kind: "libCall",
fn: "error.hasKey",
args: [ref(), { kind: "strLit", value: key, type: STRING, loc }],
type: BOOL,
loc,
});
const answer = tests.reduce((acc, test) => ({
kind: "logical",
op: "||",
left: acc,
right: test,
type: BOOL,
loc,
}));
if (!slot) return answer;
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: slot.id, init: recv, loc }],
result: answer,
type: BOOL,
loc,
};
}
lowerer.unsupported(
"SC1090",
@@ -48,6 +48,10 @@ export interface FnCtx {
argumentsLocal?: IrLocal | null;
/** Arrows inherit the nearest ordinary function's arguments binding. */
inheritsArguments?: boolean;
/** A function lowered on demand on top of an unrelated caller's frames
* (a generic instance): lexical `this` resolution stops here, so the
* body sees what it sees when lowered alone. */
thisBoundary?: true;
/** Tuple bindings of this function that are only forwarded
* (typed-arguments.ts onlyForwarded): binding local id → the native
* slots forwarding reads. Such a binding is never materialized. Local ids
@@ -161,7 +165,16 @@ export function captureContextBinding(
throw new InternalCompilerError("lowerer bug: no active function context");
const direct = bindingInContext(current, symbol);
if (direct !== null) return { local: direct, origin: direct, error: null };
for (let depth = stack.length - 2; depth >= 0; depth--) {
let floor = 0;
if (symbol === undefined) {
for (let depth = stack.length - 1; depth >= 0; depth--) {
if (stack[depth]!.thisBoundary) {
floor = depth;
break;
}
}
}
for (let depth = stack.length - 2; depth >= floor; depth--) {
const origin = bindingInContext(stack[depth]!, symbol);
if (origin === null) continue;
@@ -905,6 +905,8 @@ export function lowerGenericInstance(
: lowerer.bodyReturnType(isAsync, inst.returnType);
const fnCtx = newFnCtx(false, null, null, bodyReturn);
fnCtx.isAsync = isAsync;
// Instances lower on demand, on top of whichever caller reached them.
fnCtx.thisBoundary = true;
// Implicit-any instances whose declared return did not map lower in
// return-INFERENCE mode: `return` statements record here bare, and the
// post-pass (resolveInferredReturn) settles the type and wraps them.
@@ -4381,6 +4381,14 @@ function lowerCaughtPredicateCall(
const paramSymbol = lowerer.checker.getSymbolAtLocation(param.name);
if (!paramSymbol) return null;
const ret = decl.body.statements.nodes.length === 1 ? decl.body.statements.nodes[0] : undefined;
// A longer guard whose parameter is `unknown` (or `any`) takes the caught
// value as an ordinary checked-dynamic argument: the call path converts
// the binding, and the narrowed binding reads through the same value.
if (
(!ret || !ts.isReturnStatement(ret) || !ret.expression) &&
lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(paramSymbol))?.kind === "dyn"
)
return null;
if (!ret || !ts.isReturnStatement(ret) || !ret.expression) {
lowerer.unsupported(
"SC1090",
@@ -6922,6 +6930,7 @@ function recordAssignHelper(
const helper = `%obj.assign.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, helper);
const sShape = lowerer.shapes.get(srcShapeId)!;
const tShape = lowerer.shapes.get(targetShapeId)!;
const tT: IrType = { kind: "record", shapeId: targetShapeId };
const sT: IrType = { kind: "record", shapeId: srcShapeId };
const tRef: IrExpr = { kind: "varRef", localId: "t.0", type: tT, loc };
@@ -6936,12 +6945,13 @@ function recordAssignHelper(
type: f.type,
loc,
};
const targetType = tShape.fields.find((x) => x.name === f.name)!.type;
const set: IrStmt = {
kind: "recordSet",
obj: tRef,
shapeId: targetShapeId,
field: f.name,
value: get,
value: typeEquals(targetType, f.type) ? get : lowerer.coerceToExpected(get, targetType),
loc,
};
const present = lowerer.recordFieldPresent(sRef, srcShapeId, f.name, loc);
@@ -7489,6 +7499,8 @@ function lowerObjectStaticCall(
};
}
}
const onError = lowerObjectAssignOntoError(lowerer, call);
if (onError) return onError;
const hybrid = lowerObjectAssignHybrid(lowerer, call);
if (hybrid) return hybrid;
// `Object.assign({}, lit)` — an EMPTY fresh-literal target and one
@@ -7581,7 +7593,12 @@ function lowerObjectStaticCall(
!shapeHasAccessorSlots(sShape) &&
sShape.fields.every((sf) => {
const tf = tShape.fields.find((x) => x.name === sf.name);
return tf !== undefined && typeEquals(tf.type, sf.type);
// A source value converts into a wider target field (`{ bail:
// true }` into `bail?: boolean`).
return (
tf !== undefined &&
(typeEquals(tf.type, sf.type) || lowerer.coercibleValue(sf.type, tf.type))
);
});
if (ok) {
const loc = locOf(call);
@@ -7599,6 +7616,37 @@ function lowerObjectStaticCall(
}
}
}
// `throw Object.assign(record, { extra })`: an error-shaped record given
// a member its type lacks, thrown at once. A fixed record cannot grow
// the member, so the throw carries a checked-dynamic copy of the target
// with the source's members added. The copy is unobservable: control
// leaves with the throw, so nothing on that path reads the target again.
{
let parent: ts.Node | undefined = call.parent;
while (parent && ts.isParenthesizedExpression(parent)) parent = parent.parent;
const argNodes = call.arguments.nodes;
if (
parent &&
ts.isThrowStatement(parent) &&
argNodes.length === 2 &&
!argNodes.some((a) => ts.isSpreadElement(a))
) {
const tProbe = tryLowerExpression(lowerer, argNodes[0]!);
const sProbe = tryLowerExpression(lowerer, argNodes[1]!);
if (
tProbe?.type.kind === "record" &&
lowerer.dynConvertible(tProbe.type) &&
sProbe &&
(sProbe.type.kind === "dyn" || lowerer.dynConvertible(sProbe.type))
) {
const loc = locOf(call);
const target = lowerer.coerceToExpected(lowerer.lowerExpr(argNodes[0]!), DYN);
const source = lowerer.coerceToExpected(lowerer.lowerExpr(argNodes[1]!), DYN);
if (target.type.kind === "dyn" && source.type.kind === "dyn")
return { kind: "libCall", fn: "dyn.assign", args: [target, source], type: DYN, loc };
}
}
}
// `Object.assign(target, ...sources)` over a CHECKED-DYNAMIC target
// — the n-ary/spread form (`Object.assign({}, ...plugins.map(p =>
// p.options), coreOptions)`, support.js's option-table merge). The
@@ -9678,6 +9726,30 @@ export function lowerObjectMethodCall(
`calls of the abstract method '${method}' with no concrete implementation below the receiver's static class`,
);
}
// The static receiver's nearest declaration is an override adapted to
// the inherited ABI, and nothing below overrides it: call its body
// directly under its declared signature (exact parameters and result).
// The slot function stays reachable; it lifts the body.
const adapted = found.declarer.returnAdapters?.get(method);
// A TypeScript override's own entry answers its declared result. A
// JavaScript body's result is inferred, so its callers keep the slot.
const declarerDecl = found.declarer.decl;
if (
adapted &&
!(declarerDecl && isJsSourceFile(declarerDecl.getSourceFile())) &&
!lowerer.overrideBelow(info, method)
) {
lowerer.noteEdge(`%${found.declarer.def.name}.${method}`);
const receiver = lowerReceiver();
const args = lowerer.completeArgs(call.arguments.nodes, adapted.params, locOf(call), call);
return {
kind: "call",
callee: `%${found.declarer.def.name}.${method}$own`,
args: [lowerer.upcastTo(receiver, found.declarer.def.name), ...args],
type: adapted.ret,
loc: locOf(call),
};
}
if (lowerer.overrideBelow(info, method)) lowerer.noteVirtualEdge(info, method);
else lowerer.noteEdge(`%${found.declarer.def.name}.${method}`);
const receiver = lowerReceiver();
@@ -10288,3 +10360,60 @@ function maybePromiseHandlerResult(
}
return stmt;
}
/** `Object.assign(err, ...sources)` in TypeScript onto an error-hierarchy
* target (`Object.assign(new Error(message), details)`): the sources'
* own members land on the error — the runtime copy over its
* checked-dynamic node, whose name, message, stack, cause and code feed
* back into the typed slots — and the call answers the error itself.
* Null for every other target. */
function lowerObjectAssignOntoError(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
const argNodes = call.arguments.nodes;
if (
isJsSourceFile(call.getSourceFile()) ||
argNodes.length < 2 ||
argNodes.some((a) => ts.isSpreadElement(a))
)
return null;
const probe = tryLowerExpression(lowerer, argNodes[0]!);
if (probe?.type.kind !== "object" || !lowerer.isErrorRootedClass(probe.type.className))
return null;
const loc = locOf(call);
const target = lowerer.lowerExpr(argNodes[0]!);
const slot = lowerer.declareHiddenLocal("%assignErr", target.type);
const sources: IrExpr[] = [];
for (const node of argNodes.slice(1)) {
const raw = lowerer.lowerExpr(node);
fenceSymbolFieldCopy(lowerer, node, raw.type);
const source = lowerer.coerceToExpected(raw, DYN);
if (source.type.kind !== "dyn") {
lowerer.unsupported(
"SC1090",
node,
`Object.assign onto an Error from a '${lowerer.fmt(raw.type)}' source (sources enter the error's dynamic properties)`,
);
}
sources.push(source);
}
const ref = (): IrExpr => ({ kind: "varRef", localId: slot.id, type: target.type, loc });
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: slot.id, init: target, loc },
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "error.assign",
args: [ref(), { kind: "dynArrLit", elems: sources, type: DYN, loc }],
type: VOID,
loc,
},
loc,
},
],
result: ref(),
type: target.type,
loc,
};
}
@@ -30,6 +30,7 @@ import type { FnCtx, Lowerer } from "./lowerer.js";
import { inlineNamespaceRequireOf, lowerWithInlineRequireInit } from "./cjs-inline-require.js";
import {
BOOL,
canDynCheckTo,
DATE_T,
DYN,
F64,
@@ -499,6 +500,12 @@ export interface ClassInfo {
/** Own JS overrides whose signature cannot use the inherited ABI. The slot
* retains that ABI, but its implementation throws before executing. */
methodEntryFences?: Map<string, ScrDiagnostic>;
/** Own TypeScript overrides whose declared result differs from the
* inherited ABI's but converts into it (`Promise<void>` over `any`, a
* union narrowed to its inherited arm): the slot function `%C.m` keeps the
* inherited ABI and converts the result of the body, which lowers as
* `%C.m$own` with the declared result (async when declared so). */
returnAdapters?: Map<string, { params: ParamShape[]; ret: IrType; async: boolean }>;
/** OWN GENERIC instance methods (own type parameters — `m<T>(x: T)`),
* monomorphized per call site like top-level generic functions: instance
* `n` is the module function `%C.m%n` taking `this` as param 0. They
@@ -2249,6 +2256,7 @@ export function collectClassShapeInner(
}
>();
const methodEntryFences = new Map<string, ScrDiagnostic>();
const returnAdapters = new Map<string, { params: ParamShape[]; ret: IrType; async: boolean }>();
// Own accessor declarations ("get:x"/"set:x" → node), for the
// partial-override analysis below (diagnostics need the node).
const accessorNodes = new Map<string, ts.AccessorDeclaration>();
@@ -3434,7 +3442,67 @@ export function collectClassShapeInner(
overridden.declarer.decl !== null &&
isJsSourceFile(overridden.declarer.decl.getSourceFile()) &&
overridden.sig.gen === undefined;
if (fencedJsOverride) {
// A TypeScript override that only changes the result, into a
// type that converts into the inherited one (`Promise<void>` over
// an `any` base, `string | undefined` over `string`): the slot
// keeps the inherited ABI and converts the body's result at the
// boundary (lowerClassMethodMemberInner).
const ownAsync =
member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true;
if (
!fencedJsOverride &&
!functionLocalClass(decl) &&
overridden.sig.gen === undefined &&
// An async base's slot answers its promise: a Promise type, or
// the checked-dynamic value of an untyped async method.
(overridden.sig.async !== true ||
overridden.sig.ret.kind === "promise" ||
overridden.sig.ret.kind === "dyn") &&
!overridden.sig.abstract &&
overridden.sig.params.length === shapes.length &&
overridden.sig.params.every(
(p, i) =>
p.mode === shapes[i]!.mode &&
(p.mode === "required" || p.mode === "omittable") &&
// Each inherited argument converts into the override's
// parameter: equal, a checked extraction from an `any`
// slot, or a widening (a contravariant parameter).
(typeEquals(p.type, shapes[i]!.type) ||
(p.type.kind === "dyn" &&
canDynCheckTo(
shapes[i]!.type,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
)) ||
lowerer.coercibleValue(p.type, shapes[i]!.type)),
) &&
(ownAsync ? ft.ret.kind === "promise" : true) &&
(typeEquals(ft.ret, overridden.sig.ret) ||
(overridden.sig.ret.kind === "dyn"
? ft.ret.kind !== "void" && lowerer.dynConvertible(ft.ret)
: lowerer.coercibleValue(ft.ret, overridden.sig.ret) ||
// A promise whose payload widens into the inherited one
// (`Promise<void>` over an untyped async `Promise<any>`).
(ft.ret.kind === "promise" &&
overridden.sig.ret.kind === "promise" &&
lowerer.promisePayloadWidens(ft.ret, overridden.sig.ret)) ||
// A JavaScript override's inferred `any` result checks
// into the inherited type (`toString() { return
// this.message }` over Error's).
(ft.ret.kind === "dyn" &&
isJsSourceFile(member.getSourceFile()) &&
overridden.sig.ret.kind !== "void" &&
canDynCheckTo(
overridden.sig.ret,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
))))
) {
returnAdapters.set(mName, { params: [...shapes], ret: ft.ret, async: ownAsync });
shapes.splice(0, shapes.length, ...overridden.sig.params);
ft.ret = overridden.sig.ret;
if (overridden.sig.async === true) thunkAsyncSlot(overridden, mName);
} else if (fencedJsOverride) {
// A dormant JS method must not poison the whole class. Keep
// the inherited call/dispatch ABI and refuse at method entry;
// lowering this body with the base signature would change JS.
@@ -3489,7 +3557,34 @@ export function collectClassShapeInner(
// fiber spawn wrapper; vtable slots hold raw implementations) —
// an override chain touching an async method on either end would
// put a spawn wrapper behind a virtual slot, so it fences.
if (overridden && (asyncMember || overridden.sig.async === true)) {
// An override chain touching async methods dispatches through
// plain slot functions that return the body's promise: the async
// bodies lower as `%C.m$own` behind them (thunkAsyncSlot).
if (
overridden &&
!returnAdapters.has(mName) &&
(asyncMember || overridden.sig.async === true) &&
!isJsSourceFile(member.getSourceFile()) &&
!functionLocalClass(decl) &&
member.asteriskToken === undefined &&
overridden.sig.gen === undefined &&
!overridden.sig.abstract &&
ft.ret.kind === "promise" &&
typeEquals(ft.ret, overridden.sig.ret) &&
overridden.sig.params.length === shapes.length &&
overridden.sig.params.every(
(p, i) => typeEquals(p.type, shapes[i]!.type) && p.mode === shapes[i]!.mode,
)
) {
if (overridden.sig.async === true) thunkAsyncSlot(overridden, mName);
if (asyncMember)
returnAdapters.set(mName, { params: [...shapes], ret: ft.ret, async: true });
}
if (
overridden &&
!returnAdapters.has(mName) &&
(asyncMember || overridden.sig.async === true)
) {
const message = `overriding ${overridden.sig.async === true ? "the async method" : "a method with an async method"} '${mName}' (async methods dispatch statically — the vtable slot machinery has no fiber-spawn story)`;
if (isJsSourceFile(member.getSourceFile()))
methodEntryFences.set(mName, unsupportedDiag("SC1090", locOf(member.name), message));
@@ -3520,7 +3615,7 @@ export function collectClassShapeInner(
} else {
methods.set(
mName,
asyncMember
asyncMember && !returnAdapters.has(mName)
? { params: shapes, ret: ft.ret, async: true as const }
: { params: shapes, ret: ft.ret },
);
@@ -4238,6 +4333,7 @@ export function collectClassShapeInner(
fieldOrder,
methods,
...(methodEntryFences.size > 0 ? { methodEntryFences } : {}),
...(returnAdapters.size > 0 ? { returnAdapters } : {}),
decl,
...(emitOverride !== undefined ? { emitOverride } : {}),
ctor,
@@ -6616,6 +6712,28 @@ function abstractMemberSignature(
/** A nominal return subtype uses the same owned pointer as the inherited
* return slot. The current class is still being collected, so consult its
* already collected base chain before the complete class graph is available. */
/** An inherited ASYNC method that an override now dispatches against: its
* slot becomes a plain function returning the body's promise, and the
* async body lowers behind it as `%C.m$own` (the adapter path with an
* identity conversion), so a virtual call never enters an async body
* without its spawn wrapper. */
function thunkAsyncSlot(
overridden: { declarer: ClassInfo; sig: ClassMethodSignature },
mName: string,
): void {
if (overridden.sig.async !== true) return;
const declarer = overridden.declarer;
declarer.returnAdapters ??= new Map();
if (!declarer.returnAdapters.has(mName)) {
declarer.returnAdapters.set(mName, {
params: [...overridden.sig.params],
ret: overridden.sig.ret,
async: true,
});
}
delete overridden.sig.async;
}
function overrideReturnAbi(
lowerer: Lowerer,
declared: IrType,
@@ -7838,6 +7956,82 @@ function lowerClassMethodMemberInner(
allowDiagSink: true,
});
}
// An override converting its declared result into the inherited ABI:
// the body lowers as `%C.m$own` under its declared result, and the slot
// function forwards to it and converts what it returns.
const adapter = info.returnAdapters?.get(mName);
if (adapter) {
const ownSig: ClassMethodSignature = { ...sig, params: adapter.params, ret: adapter.ret };
if (adapter.async) ownSig.async = true;
const own = lowerClassMethodBody(lowerer, info, fnLike, `%${className}.${mName}$own`, ownSig);
if (!own) return null;
lowerer.liftedFns.push(own);
const loc = locOf(fnLike);
const params: IrParam[] = [
{ localId: "this.0", name: "this", type: thisType },
...sig.params.map((p, i) => ({ localId: `p.${i}`, name: `p${i}`, type: p.type })),
];
const fnCtx = newFnCtx(false, null, null, sig.ret);
lowerer.fnStack.push(fnCtx);
try {
const call: IrExpr = {
kind: "call",
callee: own.name,
args: params.map((p, i): IrExpr => {
const ref: IrExpr = { kind: "varRef", localId: p.localId, type: p.type, loc };
// `this` passes through; each inherited argument converts into
// the override's declared parameter (checked where it narrows).
return i === 0 ? ref : lowerer.coerceInto(fnLike.name, ref, adapter.params[i - 1]!.type);
}),
type: adapter.ret,
loc,
};
// A void slot discards the override's result after running it. A
// toString slot hands on what every ToString consumer of the result
// makes of it (`String(err)`, templates): undefined reads
// "undefined", as in Node. Other slots check the result into the
// inherited type.
const result = (): IrExpr =>
mName === "toString" && sig.ret.kind === "string"
? call.type.kind === "dyn"
? { kind: "libCall", fn: "dyn.stringConstructor", args: [call], type: STRING, loc }
: lowerer.ensureString(call, fnLike.name)
: lowerer.coerceInto(fnLike.name, call, sig.ret);
const body: IrStmt[] =
sig.ret.kind === "void"
? [
{ kind: "exprStmt", expr: call, loc },
{ kind: "return", value: null, loc },
]
: [{ kind: "return", value: result(), loc }];
return {
name: `%${className}.${mName}`,
params,
returnType: sig.ret,
locals: [
...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
...lowerer.ctx.locals,
],
body,
loc,
};
} finally {
lowerer.fnStack.pop();
}
}
return lowerClassMethodBody(lowerer, info, fnLike, `%${className}.${mName}`, sig);
}
function lowerClassMethodBody(
lowerer: Lowerer,
info: ClassInfo,
fnLike: ts.MethodDeclaration | ts.AccessorDeclaration,
fnName: string,
sig: ClassMethodSignature,
): IrFunction | null {
const className = info.def.name;
const thisType: IrType = { kind: "object", className };
if (!fnLike.body) return null;
const prevClass = lowerer.currentClass;
lowerer.currentClass = info;
// ASYNC methods: the module function is an async IrFunction — its
@@ -7879,7 +8073,7 @@ function lowerClassMethodMemberInner(
const body = [...declared.prologue, ...lowerer.lowerStmts(fnLike.body.statements.nodes)];
appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(fnLike));
const fn: IrFunction = {
name: `%${className}.${mName}`,
name: fnName,
params,
returnType: bodyReturn,
locals: lowerer.ctx.locals,
@@ -484,7 +484,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// Only arrows can see an enclosing method's `this` (JS lexical-this);
// `this` in plain nested functions is already a tsc error under
// noImplicitThis, so the generic scope walk here is preflight-safe.
const local = lowerer.resolveThis();
const local = lowerer.resolveThis(expr);
if (local) {
const value: IrExpr = { kind: "varRef", localId: local.id, type: local.type, loc };
// `this.#x` in a function-local class's prototype method, whose `this`
@@ -1057,6 +1057,15 @@ export function nullishBranches(
export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr {
let e: ts.Expression = expr;
while (ts.isParenthesizedExpression(e)) e = e.expression;
// `if (Object.setPrototypeOf)`: the feature test of transpiled Error
// subclasses' prototype reset. Node always has it.
if (
ts.isPropertyAccessExpression(e) &&
!e.questionDotToken &&
e.name.text === "setPrototypeOf" &&
lowerer.isStdlibGlobal(e.expression, "Object")
)
return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) };
if (ts.isPropertyAccessExpression(e)) {
const presence = lowerPromiseThenPresence(lowerer, e, {
kind: "boolLit",
@@ -2257,9 +2266,14 @@ 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") {
// `catch (e: any)` (or useUnknownInCatchVariables off) reads the same way.
if (
narrowed?.kind === "dyn" ||
(narrowed === null && (lowerer.typeOf(node).flags & ts.TypeFlags.Any) !== 0)
) {
return { kind: "caughtToDyn", value: ref, type: DYN, loc };
}
// A binding a user type predicate (or an assertion) narrowed to a
// structural type (`isErrorLike(e)` → `{ message: string; … }`, an
// intersection like `TypeError & { code: unknown }`): the thrown value is
@@ -1496,6 +1496,8 @@ const THROWN_AS_DYN: ReadonlySet<IrType["kind"]> = new Set([
]);
export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null {
if (ownPrototypeResetStatement(lowerer, stmt))
return { kind: "block", body: [], loc: locOf(stmt) };
if (ts.isClassDeclaration(stmt)) {
if (!stmt.name) lowerer.unsupported("SC1090", stmt, "anonymous local class declarations");
const info = lowerer.lowerClassExpressionInfo(stmt);
@@ -7478,6 +7480,34 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
loc,
};
}
// `err.code = v` on an Error value (`err as Error & { code?:
// string }`): typed reads come from the runtime error's code
// slot, so the write lands there. A value that is not statically
// a string checks at runtime: strings take the slot, anything
// else clears it and stays a dynamic own property. The
// system-error members (errno, syscall, path, dest) read
// through the checked-dynamic node and keep the dynamic write, as
// do JavaScript files, whose reads use that node too.
if (
!isJsSourceFile(expr.getSourceFile()) &&
recv?.type.kind === "object" &&
lowerer.isErrorRootedClass(recv.type.className) &&
expr.left.name.text === "code"
) {
const loc = locOf(expr);
const stringValue = isStringTyped(lowerer.typeOf(expr.right));
return {
kind: "exprStmt",
expr: {
kind: "libCall",
fn: stringValue ? "error.setCode" : "error.setCodeDyn",
args: [recv, lowerer.lowerExprExpecting(expr.right, stringValue ? STRING : DYN)],
type: VOID,
loc,
},
loc,
};
}
if (recv?.type.kind === "object" && recv.type.className === "%Error") {
const loc = locOf(expr);
return {
@@ -11298,6 +11328,141 @@ export function stdlibBufferVarAlias(lowerer: Lowerer, decl: ts.VariableDeclarat
);
}
/** A checker type that is a string or string literal (or a union of them). */
function isStringTyped(t: ts.Type): boolean {
if (t.isUnionType()) return ts.constituentTypes(t).every(isStringTyped);
return (t.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral)) !== 0;
}
/** The Error-subclass prototype reset of transpiled code, inside a class
* constructor:
*
* const proto = new.target.prototype;
* if (Object.setPrototypeOf) Object.setPrototypeOf(this, proto);
* else this.__proto__ = proto;
*
* (also `Object.setPrototypeOf(this, new.target.prototype)` and
* `Object.setPrototypeOf(this, C.prototype)` in C's own constructor when no
* class extends C). A compiled instance already has exactly that prototype,
* so each statement is a no-op. True for those statements. */
function ownPrototypeResetStatement(lowerer: Lowerer, stmt: ts.Statement): boolean {
if (ts.isVariableStatement(stmt)) {
const decls = stmt.declarationList.declarations.nodes;
if (decls.length !== 1 || (stmt.declarationList.flags & ts.NodeFlags.Const) === 0) return false;
const decl = decls[0]!;
if (!ts.isIdentifier(decl.name) || !decl.initializer) return false;
const ctor = enclosingConstructor(decl);
if (!ctor || !ownPrototypeRef(lowerer, decl.initializer, ctor)) return false;
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
if (!symbol) return false;
// Every other use of the binding must be one of the reset statements.
let onlyResets = true;
ts.walkPreorder(ctor, (n) => {
if (!onlyResets) return "stop";
if (!ts.isIdentifier(n) || n === decl.name) return undefined;
if (lowerer.checker.getSymbolAtLocation(n) !== symbol) return undefined;
const statement = n.parent?.parent;
if (
!statement ||
!ts.isExpressionStatement(statement) ||
resetTarget(lowerer, statement.expression) !== n
)
onlyResets = false;
return undefined;
});
return onlyResets;
}
if (!ts.isExpressionStatement(stmt)) return false;
const ctor = enclosingConstructor(stmt);
if (!ctor) return false;
const target = resetTarget(lowerer, stmt.expression);
return target !== null && ownPrototypeRef(lowerer, target, ctor);
}
/** The prototype operand of `Object.setPrototypeOf(this, P)` or
* `this.__proto__ = P`, or null. */
function resetTarget(lowerer: Lowerer, expr: ts.Expression): ts.Expression | null {
if (
ts.isCallExpression(expr) &&
expr.arguments.nodes.length === 2 &&
expr.arguments.nodes[0]!.kind === ts.SyntaxKind.ThisKeyword &&
ts.isPropertyAccessExpression(expr.expression) &&
expr.expression.name.text === "setPrototypeOf" &&
lowerer.isStdlibGlobal(expr.expression.expression, "Object")
)
return expr.arguments.nodes[1]!;
if (
ts.isBinaryExpression(expr) &&
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
ts.isPropertyAccessExpression(expr.left) &&
expr.left.expression.kind === ts.SyntaxKind.ThisKeyword &&
expr.left.name.text === "__proto__"
)
return expr.right;
return null;
}
/** The class constructor whose body holds `node`, crossing only statements
* and blocks (any nested function rebinds `this`). */
function enclosingConstructor(node: ts.Node): ts.ConstructorDeclaration | null {
for (let cur = node.parent; cur !== undefined; cur = cur.parent) {
if (ts.isConstructorDeclaration(cur)) return cur;
if (
ts.isFunctionLike(cur) ||
ts.isClassDeclaration(cur) ||
ts.isClassExpression(cur) ||
ts.isSourceFile(cur)
)
return null;
}
return null;
}
/** Whether `node` denotes the prototype of the object `ctor` constructs:
* `new.target.prototype`, a const holding it, or the class's own
* `C.prototype` when no class extends C. */
function ownPrototypeRef(
lowerer: Lowerer,
node: ts.Expression,
ctor: ts.ConstructorDeclaration,
): boolean {
while (ts.isParenthesizedExpression(node)) node = node.expression;
if (ts.isPropertyAccessExpression(node) && node.name.text === "prototype") {
const base = node.expression;
if (
ts.isMetaProperty(base) &&
base.keywordToken === ts.SyntaxKind.NewKeyword &&
base.name.text === "target"
)
return true;
const cls = ctor.parent;
if (!ts.isIdentifier(base) || !(ts.isClassDeclaration(cls) || ts.isClassExpression(cls)))
return false;
const symbol = lowerer.resolveValueSymbol(base);
const info = symbol ? lowerer.classBySymbol.get(symbol) : undefined;
if (!info || symbol!.valueDeclaration !== cls) return false;
for (const other of lowerer.classes.values()) {
if (other !== info && lowerer.isSubclassOf(other.def.name, info.def.name)) return false;
}
return true;
}
if (ts.isIdentifier(node)) {
const symbol = lowerer.resolveValueSymbol(node);
const decl = symbol?.valueDeclaration;
return (
decl !== undefined &&
ts.isVariableDeclaration(decl) &&
decl.initializer !== undefined &&
ts.isVariableDeclarationList(decl.parent) &&
(decl.parent.flags & ts.NodeFlags.Const) !== 0 &&
enclosingConstructor(decl) === ctor &&
!ts.isIdentifier(decl.initializer) &&
ownPrototypeRef(lowerer, decl.initializer, ctor)
);
}
return false;
}
function isAssertionInitializer(node: ts.Expression): boolean {
let e = node;
while (ts.isParenthesizedExpression(e)) e = e.expression;
@@ -9820,6 +9820,8 @@ export class Lowerer {
node = node.expression;
if (expected) {
if (ts.isConditionalExpression(node)) return lowerTernary(this, node, expected);
const escaped = this.neverAssertionInto(node, expected);
if (escaped) return escaped;
const fresh = this.emptyCollectionFor(node, expected);
if (fresh) return this.coerceInto(node, fresh, expected);
}
@@ -10733,6 +10735,14 @@ export class Lowerer {
return findMethodOn(this, info, name);
}
/** A class whose root is the runtime's %Error (builtin errors and their
* program subclasses). */
isErrorRootedClass(className: string): boolean {
let info = this.classes.get(className) ?? null;
while (info && info.base) info = info.base;
return info?.def.name === "%Error";
}
isSubclassOf(sub: string, sup: string): boolean {
return isSubclassOf(this, sub, sup);
}
@@ -11031,24 +11041,30 @@ export class Lowerer {
): void {
const loc = locOf(decl);
const name = nameNode.text;
const tupleType = this.irTypeOf(nameNode);
const fn = decl.parent;
const symbol = this.checker.getSymbolAtLocation(nameNode);
const isRead = (id: ts.Identifier): boolean =>
id !== nameNode && this.checker.getSymbolAtLocation(id) === symbol;
const pure = fn !== undefined && symbol !== undefined && onlyForwarded(this, fn, isRead);
// A forwarded-only tuple is never built: a tuple with optional
// positions (`Parameters<(n?: T) => R>`, whose length varies per call)
// forwards its slots, an omitted position as undefined — which a
// callee's default treats exactly as an omitted argument. Its length
// is the one read that would observe the difference.
const tupleType =
pure &&
elemShapes.length > 0 &&
this.mapTypeOf(this.typeOf(nameNode)) === null &&
!readsLength(fn!, isRead)
? this.forwardedSlotsTuple(elemShapes)
: this.irTypeOf(nameNode);
const slots = elemShapes.map((shape, i) => {
const slot = this.declareHiddenLocal(`%${name}.${i}`, shape.type);
params.push({ localId: slot.id, name: `%${name}.${i}`, type: shape.type });
return slot;
});
const body = this.declareLocal(nameNode, name, tupleType, true);
const fn = decl.parent;
const symbol = this.checker.getSymbolAtLocation(nameNode);
if (
fn !== undefined &&
symbol !== undefined &&
onlyForwarded(
this,
fn,
(id) => id !== nameNode && this.checker.getSymbolAtLocation(id) === symbol,
)
) {
if (pure) {
this.ctx.pureTuples.set(body.id, slots);
return;
}
@@ -11056,6 +11072,38 @@ export class Lowerer {
prologue.push({ kind: "varDecl", localId: body.id, init, loc });
}
/** `value as never` flowing into a typed slot: `never` is assignable to
* every type, so the assertion promises the slot's type. A dynamic value
* takes the checked extraction to it, as `value as T` would. Null for
* every other form, and for numeric, dynamic and union slots, which
* keep the existing lowering. */
private neverAssertionInto(node: ts.Expression, expected: IrType): IrExpr | null {
if (!(ts.isAsExpression(node) || ts.isTypeAssertion(node))) return null;
if (expected.kind === "f64" || expected.kind === "dyn" || expected.kind === "union")
return null;
if (!(this.checker.getTypeFromTypeNode(node.type).flags & ts.TypeFlags.Never)) return null;
const innerTs = this.typeOf(node.expression);
if (!(innerTs.flags & ts.TypeFlags.Any) && this.mapTypeOf(innerTs)?.kind !== "dyn") return null;
if (
!canDynCheckTo(
expected,
(id) => this.shapes.get(id),
(id) => this.unions.get(id),
)
)
return null;
const inner = this.lowerExpr(node.expression);
if (inner.type.kind !== "dyn") return null;
return { kind: "dynCheck", value: inner, type: expected, loc: locOf(node) };
}
/** The tuple record of a forwarded-only rest tuple's native slots. */
private forwardedSlotsTuple(elemShapes: readonly ParamShape[]): IrType {
const fields = elemShapes.map((shape, i) => ({ name: String(i), type: shape.type }));
fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
return { kind: "record", shapeId: this.shapes.intern(fields, true) };
}
/** A fresh tuple holding the given positional slots, typed `tupleType`
* (a tuple record, or the empty-array form of `[]`). */
tupleOfSlots(slots: readonly IrLocal[], tupleType: IrType, blame: ts.Node, loc: SrcLoc): IrExpr {
@@ -11497,8 +11545,8 @@ export class Lowerer {
/** Lexical `this` — the enclosing method's this-param, possibly captured
* through arrows (function expressions/declarations reset `this` in JS;
* their bodies never see an enclosing method's binding). */
resolveThis(): IrLocal | null {
return this.resolveKey(undefined);
resolveThis(blame?: ts.Node): IrLocal | null {
return this.resolveKey(undefined, blame);
}
peekLocal(ident: ts.Identifier): IrLocal | null {
@@ -13337,6 +13385,31 @@ export class Lowerer {
}
}
/** Whether some read of a binding (identified by `isRead`) in `fn` takes
* its `.length`. */
function readsLength(fn: ts.Node, isRead: (id: ts.Identifier) => boolean): boolean {
let found = false;
ts.walkPreorder(fn, (n) => {
if (found) return "stop";
if (!ts.isIdentifier(n) || !isRead(n)) return undefined;
let top: ts.Node = n;
while (
top.parent &&
(ts.isParenthesizedExpression(top.parent) ||
ts.isAsExpression(top.parent) ||
ts.isTypeAssertion(top.parent) ||
ts.isSatisfiesExpression(top.parent) ||
ts.isNonNullExpression(top.parent))
)
top = top.parent;
const p = top.parent;
if (p && ts.isPropertyAccessExpression(p) && p.expression === top && p.name.text === "length")
found = true;
return undefined;
});
return found;
}
/** A union of record layouts flowing into one record slot: a function
* returning differently-shaped object literals (`{ ok: false }` and
* `{ ok: true, size }`, whose union the checker normalizes to arms with
@@ -321,7 +321,9 @@ export function forwardedTupleType(lowerer: Lowerer, node: ts.Expression): IrTyp
// Checker-only probe first: an ordinary array rest never maps here.
restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx) !== null
) {
const t = lowerer.mapTypeOf(lowerer.typeOf(operand));
// A tuple with optional positions has no value type of its own; its
// forwarded-only binding carries the slots' tuple (declareRestTupleParam).
const t = lowerer.mapTypeOf(lowerer.typeOf(operand)) ?? lowerer.peekLocal(operand)?.type;
if (t && isTupleValueType(lowerer, t)) return t;
}
return null;
+42 -3
View File
@@ -4670,7 +4670,9 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
// A parameter a TypeScript program types `any` takes whatever the
// caller passes: the checked-dynamic slot, as an `any`-typed binding
// already is (dynFallbackType). Declaration files keep their paths.
// Only function declarations and spelled function types: lambdas and
// Only function declarations, spelled function types and lambda
// parameters whose default is the only source of their type (`(n =
// lookup() ?? 5) => ...` with an `any` lookup): other lambdas and
// methods in object literals keep their existing checked-dynamic story.
if (
!pt &&
@@ -4678,7 +4680,12 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
decl !== undefined &&
ts.isParameter(decl) &&
decl.parent !== undefined &&
(ts.isFunctionDeclaration(decl.parent) || ts.isFunctionTypeNode(decl.parent)) &&
(ts.isFunctionDeclaration(decl.parent) ||
ts.isFunctionTypeNode(decl.parent) ||
((ts.isArrowFunction(decl.parent) || ts.isFunctionExpression(decl.parent)) &&
decl.initializer !== undefined &&
decl.type === undefined &&
!isJsSourceFile(decl.getSourceFile()))) &&
!decl.getSourceFile().isDeclarationFile &&
(checker.getTypeOfSymbol(p).flags & ts.TypeFlags.Any) !== 0
)
@@ -6147,6 +6154,7 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType
});
if (!hasError) return null;
let error: IrType | null = null;
const extra: ts.Symbol[] = [];
for (const part of parts) {
const mapped = mapType(part, ctx);
if (mapped?.kind === "object" && RUNTIME_ERROR_CLASSES.has(mapped.className)) {
@@ -6167,14 +6175,45 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType
: ["code", "syscall", "path", "dest"].includes(property.name)
? "string"
: null;
if (!expected || !(property.flags & ts.SymbolFlags.Optional)) return null;
if (!expected) {
extra.push(property);
continue;
}
const field = mapType(ctx.checker.getTypeOfSymbol(property), ctx);
const arms =
field?.kind === "union" ? ctx.unions.get(field.unionId)?.arms : field ? [field] : [];
if (!arms?.length || !arms.every((arm) => arm.kind === expected || arm.kind === "undefinedT"))
return null;
// A required member must still admit the absent value the slot reads.
if (
!(property.flags & ts.SymbolFlags.Optional) &&
!arms.some((arm) => arm.kind === "undefinedT")
)
return null;
}
}
if (!error) return null;
// Members beyond Error's own and the system-error metadata (`Error & {
// status: number }`, Object.assign's result) are own properties of the
// error's checked-dynamic node: each must map to a type a checked read
// can produce. Error's own string members keep their slots.
for (const property of extra) {
const field = mapType(ctx.checker.getTypeOfSymbol(property), ctx);
if (!field || field.kind === "void") return null;
if (property.name === "message" || property.name === "name") {
if (field.kind !== "string") return null;
continue;
}
if (property.name === "stack" || property.name === "cause") return null;
if (
!canDynCheckTo(
field,
(id) => ctx.shapes.get(id),
(id) => ctx.unions.get(id),
)
)
return null;
}
return error;
}
@@ -422,6 +422,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"error.stack",
"error.cause",
"error.setCause",
"error.assign",
"error.deleteCause",
"dyn.objectTag",
// Numeric coercion runs user valueOf/toString — throws propagate.
@@ -3258,6 +3258,23 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"error.hasCause": { argTypes: [null], result: BOOL },
/** Assignment borrows both operands and retains the new cause. */
"error.setCause": { argTypes: [null, DYN], result: VOID },
/** `err.code = s` on an error-hierarchy receiver: the code slot that
* error.code reads and coded throws stamp. Borrows both; never throws. */
"error.setCode": { argTypes: [null, STRING], result: VOID },
/** `err.code = v` with a value that is not statically a string: strings
* take the code slot, anything else clears it and sets the dynamic own
* property. Borrows both; never throws. */
"error.setCodeDyn": { argTypes: [null, DYN], result: VOID },
/** `Object.assign(err, ...sources)` onto an error-hierarchy receiver
* (args: the error, a dyn array of the sources): the own-member copy
* onto its checked-dynamic node, then the typed slots (name, message,
* stack, cause, code) re-read from it. Throws what the copy throws. */
"error.assign": { argTypes: [null, DYN], result: VOID },
/** `key in err` for a key no compiled class of the receiver declares
* (args: the error, the key): a runtime-set own property (Object.assign,
* throw-site fields, dynamic writes) or an Object.prototype member. */
"error.hasKey": { argTypes: [null, STRING], result: BOOL },
"error.hasCode": { argTypes: [null], result: BOOL },
"error.defineCause": { argTypes: [null, DYN], result: VOID },
"error.deleteCause": { argTypes: [null], result: VOID },
/** The compiler-resolved Node-parity throw for always-throwing lowered
+5
View File
@@ -4113,6 +4113,11 @@ function validateFunction(
e.fn === "error.cause" ||
e.fn === "error.hasCause" ||
e.fn === "error.setCause" ||
e.fn === "error.setCode" ||
e.fn === "error.setCodeDyn" ||
e.fn === "error.assign" ||
e.fn === "error.hasKey" ||
e.fn === "error.hasCode" ||
e.fn === "error.defineCause" ||
e.fn === "error.deleteCause"
) {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6400-override-any-base.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6401-override-param-variance.ts"
],
"diags": []
}
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/6402-override-js-base/pool.js",
"<repo>/tests/corpus/6402-override-js-base/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6403-error-code-write.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6404-error-in-and-assign.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6405-wrapped-default-parameters.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6406-npm-instance-receiver/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6409-catch-binding-shapes.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6410-wrapper-untyped-results.ts"
],
"diags": []
}
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/6411-prototype-reset-constructors/issues.js",
"<repo>/tests/corpus/6411-prototype-reset-constructors/main.ts"
],
"diags": []
}
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/6412-js-error-tostring-override/errors.js",
"<repo>/tests/corpus/6412-js-error-tostring-override/main.ts"
],
"diags": []
}
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/6413-override-js-async-base/lock.js",
"<repo>/tests/corpus/6413-override-js-async-base/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6414-catch-values-roundtrip.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6415-error-code-dynamic-writes.ts"
],
"diags": []
}
+15
View File
@@ -282,6 +282,21 @@ SCR_COLD void scr_undef_global_read(ScrStr *name);
* +1 or NULL when absent (the compiler's undefined arm). The _msg_code
* thrower is scr_throw_error_msg with the code stamped on the payload. */
void scr_error_set_code(ScrError *e, const char *code);
/* The program's `err.code = s` write into the same slot (and the error's
* checked-dynamic node, when it has one). Borrows both (scr_json.c). */
void scr_error_set_code_value(ScrError *e, ScrStr *code);
/* `err.code = v` with a value that is not statically a string: a string
* takes the slot as above; anything else clears the slot and lands on the
* error's checked-dynamic node, as a dynamic property write would.
* Borrows both (scr_json.c). */
void scr_error_set_code_dyn(ScrError *e, struct ScrDyn *value);
/* Object.assign(err, ...sources): the copy onto the error's checked-dynamic
* node, then the typed slots re-read from it. Borrows both (scr_json.c). */
void scr_error_assign_dyn(ScrError *e, const struct ScrDyn *sources);
/* `key in err` for a key no compiled class declares: a runtime-set own
* property or an Object.prototype member. Borrows both (scr_json.c). */
bool scr_error_has_key(ScrError *e, const ScrStr *key);
bool scr_error_has_code(ScrError *e);
const char *scr_error_message_code(const ScrStr *msg);
ScrStr *scr_error_code(ScrError *e);
SCR_COLD void scr_throw_error_msg_code(int kind, const char *message, size_t len, const char *code);
+139 -9
View File
@@ -4428,6 +4428,7 @@ static void scr_dyn_descriptor_fields_drop(ScrDyn **fields) {
}
static bool scr_dyn_key_is_index(const char *key, size_t len, double *out);
static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key);
static void scr_error_sync_slots(const ScrDyn *view, ScrDyn *obj, const ScrStr *key);
/* Property tables belong to closures, not temporary checked wrappers. Seed
* the configurable, non-writable builtin members once so redefinitions and
@@ -6719,6 +6720,10 @@ ScrStr *scr_dyn_format_j(const ScrDyn *d) {
typedef struct {
ScrError *err; /* retained (pins the address — no pointer reuse) */
ScrDyn *dyn; /* retained */
/* An own `code` that is not a string (`err.code = undefined`), retained,
* or NULL. The code slot holds only strings, and a subclass's typed view
* rebuilds its properties from the slots on every read. */
ScrDyn *code;
} ScrErrDynEnt;
static SCR_TL ScrErrDynEnt *scr_errdyn_cache = NULL;
@@ -6822,6 +6827,7 @@ static void scr_errdyn_sweep(void) {
for (size_t i = 0; i < n_dead; i++) {
scr_error_release(dead[i].err);
scr_errdyn_release_node(dead[i].dyn);
scr_dyn_release(dead[i].code);
}
free(dead);
}
@@ -6842,6 +6848,7 @@ static void scr_errdyn_append(ScrError *e, ScrDyn *d) {
}
scr_errdyn_cache[scr_errdyn_n].err = scr_error_retain(e);
scr_errdyn_cache[scr_errdyn_n].dyn = scr_dyn_retain(d);
scr_errdyn_cache[scr_errdyn_n].code = NULL;
scr_errdyn_n++;
if (scr_errdyn_n * 2 > scr_errdyn_slots) {
free(scr_errdyn_by_err);
@@ -6897,6 +6904,7 @@ static void scr_errdyn_teardown(void) {
for (size_t i = 0; i < n; i++) {
scr_error_release(cache[i].err);
scr_errdyn_release_node(cache[i].dyn);
scr_dyn_release(cache[i].code);
}
free(cache);
}
@@ -7048,7 +7056,13 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) {
/* errno rides fs errors (with syscall) and zlib errors (without). */
if (e->system_call || e->system_errno != 0)
scr_dyn_obj_set(d, "errno", 5, scr_dyn_new_num(e->system_errno));
if (e->code) scr_dyn_obj_set(d, "code", 4, scr_dyn_new_str(e->code));
if (e->code) {
scr_dyn_obj_set(d, "code", 4, scr_dyn_new_str(e->code));
} else {
size_t hit = scr_errdyn_find(e, true);
if (hit != SIZE_MAX && scr_errdyn_cache[hit].code != NULL)
scr_dyn_obj_set(d, "code", 4, scr_dyn_retain(scr_errdyn_cache[hit].code));
}
if (e->system_call) scr_dyn_obj_set(d, "syscall", 7, scr_dyn_new_str(e->system_call));
}
if (!e->errno_order && e->system_path) scr_dyn_obj_set(d, "path", 4, scr_dyn_new_str(e->system_path));
@@ -7250,18 +7264,35 @@ ScrError *scr_errdyn_err_of(const ScrDyn *d) {
void scr_errdyn_put(ScrError *e, ScrDyn *d) { scr_errdyn_append(e, d); }
/* Error.cause has one live value even when the Error already crossed a
* checked-value boundary. Reflective data-property edits update its slot. */
static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key) {
/* An Error's slots (name, message, stack, cause, code) have one live value
* even when the Error already crossed a checked-value boundary: writes and
* deletes of those own properties through its checked-dynamic node update
* the slots typed reads use. `view` is the error's node; `obj` holds the
* properties (the node itself, or a typed view's materialized object). */
static void scr_error_sync_slots(const ScrDyn *view, ScrDyn *obj, const ScrStr *key) {
bool cause = key->len == 5 && memcmp(key->data, "cause", 5) == 0;
bool stack = key->len == 5 && memcmp(key->data, "stack", 5) == 0;
bool name = key->len == 4 && memcmp(key->data, "name", 4) == 0;
bool message = key->len == 7 && memcmp(key->data, "message", 7) == 0;
if (!cause && !stack && !name && !message) return;
bool code = key->len == 4 && memcmp(key->data, "code", 4) == 0;
if (!cause && !stack && !name && !message && !code) return;
if (obj == NULL || obj->kind != SCR_DYN_OBJ) return;
size_t hit = scr_errdyn_find(view, false);
if (hit == SIZE_MAX) return;
ScrDynEntry *entry = scr_dyn_entry(view, key);
ScrDynEntry *entry = scr_dyn_entry(obj, key);
ScrError *e = scr_errdyn_cache[hit].err;
if (code) {
/* A string takes the slot; any other value clears it and stays an own
* property, which the entry keeps for views rebuilt from the slots. */
bool string = entry && !entry->accessor && entry->value->kind == SCR_DYN_STR;
ScrStr *next = string ? scr_str_retain(entry->value->v.str) : NULL;
scr_str_release(e->code);
e->code = next;
ScrDyn *other = entry && !string && !entry->accessor ? scr_dyn_retain(entry->value) : NULL;
scr_dyn_release(scr_errdyn_cache[hit].code);
scr_errdyn_cache[hit].code = other;
return;
}
if (name || message) {
/* The native error's toString and inspect read these fields. */
if (!entry || entry->accessor || entry->value->kind != SCR_DYN_STR) return;
@@ -7292,6 +7323,10 @@ static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key) {
e->cause_enumerable = entry && entry->enumerable;
}
static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key) {
scr_error_sync_slots(view, view, key);
}
/* Error.prototype.toString over the checked-dynamic error encoding (an
* object carrying the reserved "%error" marker): `name` defaults to "Error"
* and `message` to "", both read through the prototype; either empty leaves
@@ -8323,7 +8358,12 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
if (scr_event_ref_hooks && scr_event_ref_hooks->set(recv, key, value)) return;
ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv);
scr_dyn_key_set(materialized, key, value);
if (!scr_exc_pending()) scr_dyn_typed_ref_commit(recv);
if (!scr_exc_pending()) {
scr_dyn_typed_ref_commit(recv);
/* An Error subclass's class commit writes its declared fields; the
* slots it inherits from Error take the write here. */
scr_error_sync_slots(recv, materialized, key);
}
scr_dyn_release(materialized);
return;
}
@@ -10097,7 +10137,10 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) {
scr_dyn_entry(materialized, key) != NULL;
if (!scr_exc_pending()) scr_dyn_key_delete(materialized, key, strict);
bool deleted = own && !scr_exc_pending();
if (!scr_exc_pending()) scr_dyn_typed_ref_commit(recv);
if (!scr_exc_pending()) {
scr_dyn_typed_ref_commit(recv);
scr_error_sync_slots(recv, materialized, key);
}
scr_dyn_release(materialized);
if (deleted && scr_exc_pending()) {
/* The native layout requires this field, so the commit rejected the
@@ -12478,7 +12521,17 @@ ScrDyn *scr_dyn_assign(ScrDyn *target, const ScrDyn *src) {
if (target->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(target);
ScrDyn *assigned = scr_dyn_assign(materialized, src);
if (!scr_exc_pending()) scr_dyn_typed_ref_commit(target);
if (!scr_exc_pending()) {
scr_dyn_typed_ref_commit(target);
/* An Error subclass's inherited slots take the copied values. */
static const char *const slots[] = { "name", "message", "stack", "cause", "code" };
for (size_t i = 0; i < sizeof slots / sizeof slots[0]; i++) {
ScrStr *key = scr_str_new(slots[i], strlen(slots[i]));
if (materialized->kind == SCR_DYN_OBJ && scr_dyn_entry(materialized, key) != NULL)
scr_error_sync_slots(target, materialized, key);
scr_str_release(key);
}
}
scr_dyn_release(assigned);
scr_dyn_release(materialized);
return scr_exc_pending() ? NULL : scr_dyn_retain(target);
@@ -14603,3 +14656,80 @@ ScrDyn *scr_dyn_iter_take(const ScrDyn *src, size_t n, const char *spell) {
bool scr_dyn_iterates_by_protocol(const ScrDyn *value) {
return scr_dyn_sync_generator(value);
}
/* The program's writes onto compiled errors (scr_error.h). Kept at the end
* of the file, after every function they call, so the rest of the unit
* emits in its existing order. */
/* `err.code = s` from the program (an Error asserted to carry `code`):
* the same slot every coded throw stamps, so reads and `'code' in err`
* observe the write. Borrows both. */
void scr_error_set_code_value(ScrError *e, ScrStr *code) {
scr_str_release(e->code);
e->code = scr_str_retain(code);
/* An error that already crossed into the checked-dynamic tree renders
* and reads through that node, so the write lands there too. */
ScrDyn *view = scr_errdyn_view_of(e);
if (view != NULL && view->kind == SCR_DYN_OBJ) scr_dyn_obj_set(view, "code", 4, scr_dyn_new_str(code));
}
void scr_error_set_code_dyn(ScrError *e, ScrDyn *value) {
if (value != NULL && value->kind == SCR_DYN_STR) {
scr_error_set_code_value(e, value->v.str);
return;
}
scr_str_release(e->code);
e->code = NULL;
ScrDyn *view = scr_dyn_from_error(e);
ScrStr *key = scr_str_new("code", 4);
scr_dyn_key_set(view, key, value);
scr_str_release(key);
scr_dyn_release(view);
}
/* Object.assign(err, ...sources) onto a compiled error: each source's own
* members land on the error's checked-dynamic node, then the slots that
* typed reads use (name, message, stack, cause, code) take the result.
* Borrows both; throws what the copy throws (a nullish source is skipped,
* as in JS). */
void scr_error_assign_dyn(ScrError *e, const ScrDyn *sources) {
ScrDyn *view = scr_dyn_from_error(e);
ScrDyn *r = scr_dyn_assign_all(view, sources);
if (r != NULL) {
scr_dyn_release(r);
if (view->kind == SCR_DYN_OBJ) {
scr_error_commit_dyn(e, view);
if (!scr_exc_pending()) {
ScrDyn *code = scr_dyn_obj_get(view, "code", 4);
ScrStr *next = code != NULL && code->kind == SCR_DYN_STR ? scr_str_retain(code->v.str) : NULL;
scr_str_release(e->code);
e->code = next;
}
}
}
scr_dyn_release(view);
}
/* `'code' in err`: the code slot holds a string, or the error has an own
* `code` property with another value (`err.code = undefined`), which lives
* on its checked-dynamic node. Borrows; never throws. */
bool scr_error_has_code(ScrError *e) {
if (e->code != NULL) return true;
const ScrDyn *view = scr_errdyn_view_of(e);
if (view == NULL) return false;
ScrStr *key = scr_str_new("code", 4);
bool own = scr_dyn_has_own(view, key);
scr_str_release(key);
return own;
}
/* `key in err` for a key no compiled class of the receiver declares: an own
* property set at runtime, which lives on the error's checked-dynamic node
* (that node exists once anything reflected or wrote through it), or a
* member of Object.prototype. Borrows both; never throws. */
bool scr_error_has_key(ScrError *e, const ScrStr *key) {
const ScrDyn *view = scr_errdyn_view_of(e);
if (view != NULL && view->kind == SCR_DYN_OBJ && scr_dyn_find_entry(view, key->data, key->len) != NULL)
return true;
return scr_dyn_object_proto_name(key);
}
+35
View File
@@ -0,0 +1,35 @@
// Overrides that narrow an `any`-typed base result: a typed async override
// over an untyped base method, and a sync override returning a union.
class Pool {
name = "pool";
acquire(): any {
return Promise.resolve("base");
}
describe(): any {
return this.name;
}
}
class MeteredPool extends Pool {
waits = 0;
override async acquire(): Promise<void> {
this.waits++;
await super.acquire();
}
override describe(): string | undefined {
return this.waits > 1 ? `metered ${this.waits}` : undefined;
}
}
async function use(pool: Pool, label: string): Promise<void> {
const got = await pool.acquire();
console.log(label, got === undefined ? "undefined" : got, String(pool.describe()));
}
async function main(): Promise<void> {
const metered = new MeteredPool();
await use(new Pool(), "plain");
await use(metered, "metered");
await metered.acquire();
console.log(metered.waits, metered.describe());
const pools: Pool[] = [new Pool(), metered];
for (const p of pools) console.log(typeof p.describe());
}
main();
@@ -0,0 +1,33 @@
// Overrides whose parameters differ from the base: a typed parameter over
// an `any` slot, a wider (contravariant) parameter, and a narrowed result.
class Handler {
handle(input: any): any {
return `base:${input}`;
}
score(value: string): number | undefined {
return value.length;
}
}
class TypedHandler extends Handler {
override handle(input: string): string {
return `typed:${input.toUpperCase()}`;
}
override score(value: string | number): number {
return typeof value === "string" ? value.length * 10 : value;
}
}
function run(h: Handler): void {
console.log(h.handle("abc"), h.score("xy"));
}
run(new Handler());
run(new TypedHandler());
const typed = new TypedHandler();
console.log(typed.handle("q"), typed.score(7));
try {
run2(new TypedHandler());
} catch (e) {
console.log("caught", (e as Error).name);
}
function run2(h: Handler): void {
console.log(h.handle(42));
}
@@ -0,0 +1,26 @@
// TypeScript overrides of an untyped JavaScript base: a typed async
// override over an untyped method, and a narrowed toString.
import { Pool } from "./pool.js";
class MeteredPool extends Pool {
waits = 0;
override async acquire(): Promise<number> {
this.waits++;
const n: number = await super.acquire();
return n * 10;
}
override toString(): string {
return `Metered(${this.waits})`;
}
}
async function main(): Promise<void> {
const pools: Pool[] = [new Pool(2), new MeteredPool(3)];
for (const pool of pools) {
const got = await pool.acquire();
console.log(String(got), pool.toString(), `${pool}`);
pool.release();
}
const metered = new MeteredPool(1);
console.log(await metered.acquire(), metered.waits);
}
main();
@@ -0,0 +1,17 @@
// An untyped JavaScript base class, the shape of a small npm semaphore.
export class Pool {
constructor(size) {
this.size = size;
this.taken = 0;
}
async acquire() {
this.taken++;
return this.taken;
}
release() {
this.taken--;
}
toString() {
return `Pool(${this.taken}/${this.size})`;
}
}
+39
View File
@@ -0,0 +1,39 @@
// Own properties written on Error values typed with an intersection.
const coded = new Error("c") as Error & { code?: string };
coded.code = "E1";
console.log(coded.code, "code" in coded);
function has(err: Error): boolean {
return "code" in err;
}
console.log(has(coded), has(new Error("plain")));
try {
throw coded;
} catch (e) {
console.log((e as Error & { code?: string }).code);
}
// System-error members first, then the code: reads stay consistent.
type SysError = Error & { code?: string; errno?: number; syscall?: string; path?: string };
function sysError(message: string, code: string | undefined): SysError {
const err = new Error(message) as SysError;
err.errno = -2;
err.syscall = "open";
err.path = "/missing";
err.code = code;
return err;
}
const enoent = sysError("no such file", "ENOENT");
console.log(enoent.code, enoent.errno, enoent.syscall, enoent.path, "code" in enoent);
console.log(enoent.message, enoent instanceof Error);
const uncoded = sysError("no code", undefined);
console.log(uncoded.code, uncoded.syscall);
// A code that is only known at runtime.
function withCode(err: Error & { code?: string }, code: unknown): void {
err.code = code as string;
}
const late = new Error("late") as Error & { code?: string };
withCode(late, "ELATE");
console.log(late.code);
late.code = "EAGAIN";
console.log(late.code, late.message);
+144
View File
@@ -0,0 +1,144 @@
// `'key' in err` on Error values of every receiver form, and
// Object.assign onto a fresh Error.
class HttpError extends Error {
status = 404;
get retryable(): boolean {
return this.status >= 500;
}
describe(): string {
return `${this.status}`;
}
}
// A receiver narrowed from unknown by instanceof.
function statusOf(value: unknown): unknown {
if (value instanceof Error) {
return "status" in value ? value.status : "none";
}
return "not an error";
}
console.log(statusOf(new HttpError("not found")), statusOf(new Error("plain")), statusOf("text"));
// Parameters, call results and caught values; members a subclass declares
// as fields, accessors and methods; Error.prototype's own members.
function keys(err: Error): string {
return [
"status" in err,
"retryable" in err,
"describe" in err,
"toString" in err,
"missing" in err,
"message" in err,
].join(" ");
}
console.log(keys(new HttpError("h")));
console.log(keys(new Error("e")));
console.log(keys(new TypeError("t")));
function latest(): Error {
return new HttpError("latest");
}
console.log("status" in latest(), "message" in latest());
function caught(fn: () => void): string {
try {
fn();
return "ok";
} catch (e) {
if (e instanceof Error && "status" in e) return `status ${String(e.status)}`;
return "plain";
}
}
console.log(
caught(() => {
throw new HttpError("t");
}),
caught(() => {
throw new Error("u");
}),
);
// Properties set at runtime are own properties too.
const expando = new Error("x");
(expando as unknown as { status: number }).status = 1;
console.log(keys(expando));
// Object.assign copies the source's own properties onto the error and
// answers the error itself.
interface ErrorLike {
message: string;
status?: number;
code?: string;
}
function toError(error: ErrorLike | string): Error {
return typeof error === "string"
? new Error(error)
: Object.assign(new Error(error.message), error);
}
const assigned = toError({ message: "boom", status: 500, code: "EBOOM" });
console.log(assigned.message, (assigned as Error & { code?: string }).code, "status" in assigned);
console.log(statusOf(assigned), assigned instanceof Error, keys(assigned));
const renamed = toError({ message: "renamed" });
console.log(renamed.message, "status" in renamed, toError("text").message);
const fresh = new Error("before");
console.log(Object.assign(fresh, { message: "after" }) === fresh, fresh.message);
// An Error intersection carrying extra fields: Object.assign's result,
// read and written through its declared members.
interface ApiErrorFields {
status: number;
action: string | undefined;
code: string | undefined;
link: string | undefined;
}
function apiError(message: string, fields: ApiErrorFields) {
const error = Object.assign(new Error(message), fields);
return error;
}
const gateway = apiError("bad gateway", { status: 502, action: undefined, code: "E502", link: "https://example.com" });
console.log(gateway.message, gateway.status, gateway.code, gateway.link, "action" in gateway, gateway.action);
gateway.status = 503;
console.log(gateway.status, gateway instanceof Error);
Object.assign(gateway, { status: 504, code: "E504" });
console.log(gateway.status, gateway.code);
function describeApiError(error: Error & { status?: number }): string {
return `${error.message}:${error.status ?? "none"}`;
}
console.log(describeApiError(gateway), describeApiError(new Error("plain")));
// Object.assign into a record whose fields are wider than the source's.
interface BuildFailure {
message: string;
bail?: boolean;
code?: string;
}
function failure(message: string, bail: boolean): BuildFailure {
const err: BuildFailure = { message };
if (bail) Object.assign(err, { bail: true });
return Object.assign(err, { code: "E_BUILD" });
}
for (const f of [failure("stop", true), failure("go", false)])
console.log(f.message, f.bail, f.code);
// A record given a member its type lacks and thrown at once.
interface FailureRecord {
from: string;
isGraceful: boolean;
errorValue: unknown;
}
function classifyFailure(err: unknown, from: string): FailureRecord {
return { from, isGraceful: from === "user", errorValue: err };
}
function handleFailure(err: unknown, from: string): string {
const failure = classifyFailure(err, from);
if (failure.from === "build") {
throw Object.assign(failure, { bail: true });
}
return `${failure.from} ${failure.isGraceful}`;
}
for (const from of ["build", "user"]) {
try {
console.log("handled", handleFailure(new Error("x"), from));
} catch (e) {
const thrownRecord = e as { from: string; bail?: boolean; isGraceful: boolean };
console.log("thrown", thrownRecord.from, thrownRecord.bail, thrownRecord.isGraceful);
}
}
@@ -0,0 +1,67 @@
// Lambdas whose parameters take their type from an `any` default, passed
// through generic wrappers that forward their arguments.
function trace<F extends (...args: any[]) => any>(name: string, fn: F): F {
return ((...args: Parameters<F>) => {
console.log("span", name);
return fn(...args);
}) as F;
}
function traceArgs<A extends unknown[], R>(name: string, fn: (...args: A) => R): (...args: A) => R {
return (...args: A) => {
console.log("span", name);
return fn(...args);
};
}
function lookup(key: string): any {
return key === "set" ? 7 : undefined;
}
const doubled = trace("doubled", (n = lookup("unset") ?? 5) => n * 2);
console.log(doubled(), doubled(4));
const sum = trace("sum", (n = lookup("set") ?? 5, m = 1) => n * 4 + m);
console.log(sum(), sum(2), sum(2, 3));
const label = trace("label", (prefix = lookup("unset") ?? "item", count: number = 0) => `${prefix}#${count}`);
console.log(label(), label("row"), label(undefined, 3));
const tripled = traceArgs("tripled", (n: number = lookup("unset") ?? 5) => n * 3);
console.log(tripled(), tripled(2));
// A wrapper that also reports tags computed from the wrapped function's
// result, handing its untyped result on through `as never`.
function traceTagged<F extends (...args: any[]) => any>(
name: string,
fn: F,
resultTags?: (
result: ReturnType<F> extends PromiseLike<infer U> ? U : ReturnType<F>,
) => Record<string, unknown>,
): F {
return ((...args: Parameters<F>) => {
const result: unknown = fn(...args);
if (resultTags) console.log(name, JSON.stringify(resultTags(result as never)));
return result;
}) as F;
}
const summarize = traceTagged(
"summarize",
(limit = lookup("unset") ?? 2) => ({ kept: Number(limit), dropped: 1 }),
(r) => ({ total: r.kept + r.dropped }),
);
console.log(summarize().kept, summarize(5).kept);
// The same through a function expression whose result is untyped.
function traceTaggedAny<F extends (...args: any[]) => any>(
name: string,
fn: F,
resultTags: (result: ReturnType<F>) => Record<string, unknown>,
): F {
return function (this: unknown, ...args: Parameters<F>) {
const result = fn.apply(this, args);
console.log(name, JSON.stringify(resultTags(result as never)));
return result;
} as F;
}
const measure = traceTaggedAny(
"measure",
(width = lookup("set") ?? 1, height = 2) => ({ area: Number(width) * height }),
(r) => ({ area: r.area }),
);
console.log(measure().area, measure(3, 4).area);
@@ -0,0 +1,7 @@
// A package's constructor function, compiled per call site, reads its own
// receiver while the constructor that reached it has a receiver too.
import { create } from "corked-writer";
const w = create({ highWaterMark: 4 });
console.log(w.uncork());
console.log(w.uncork());
console.log(create().uncork());
@@ -0,0 +1,4 @@
export interface Writable {
uncork(): string;
}
export function create(options?: { highWaterMark?: number }): Writable;
@@ -0,0 +1,45 @@
'use strict';
// Constructor functions in the shape of a stream implementation: a request
// object built inside another constructor keeps its receiver in a var for
// a callback, and the writer is also exported as a value.
function CorkedRequest(state) {
var _this = this;
this.next = null;
this.entry = null;
this.finish = function () {
onCorkedFinish(_this, state);
};
}
module.exports = {
create: function (options) {
return new Writable(options);
},
Writable: Writable,
};
Writable.WritableState = WritableState;
function WritableState(options, stream) {
this.highWaterMark = options.highWaterMark || 16;
this.pending = 2;
this.stream = stream;
this.corkedRequestsFree = new CorkedRequest(this);
}
WritableState.prototype.getBuffer = function getBuffer() {
return this.pending;
};
function Writable(options) {
this._writableState = new WritableState(options || {}, this);
this.label = "w";
}
Writable.prototype.uncork = function () {
var state = this._writableState;
var req = state.corkedRequestsFree;
req.finish();
state.corkedRequestsFree = new CorkedRequest(state);
return this.label + " " + req.next + " " + state.highWaterMark;
};
function onCorkedFinish(corkReq, state) {
state.pending--;
corkReq.next = "finished:" + state.pending;
}
@@ -0,0 +1 @@
{ "name": "corked-writer", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
+69
View File
@@ -0,0 +1,69 @@
// Catch bindings typed any, multi-statement guards over catch bindings,
// and writes into a caught error.
interface SafeFetchError extends Error {
kind: "safe-fetch";
host: string;
}
function isSafeFetchError(e: unknown): e is SafeFetchError {
if (!(e instanceof Error)) return false;
return "kind" in e && e.kind === "safe-fetch";
}
function blocked(host: string): Error {
return Object.assign(new Error(`blocked ${host}`), { kind: "safe-fetch", host });
}
function run(fn: () => void): string {
try {
fn();
return "ok";
} catch (err) {
if (isSafeFetchError(err)) return `guarded ${err.host} ${err.message}`;
return `other ${err instanceof Error ? err.message : String(err)}`;
}
}
console.log(run(() => {}));
console.log(run(() => {
throw blocked("example.com");
}));
console.log(run(() => {
throw new Error("plain");
}));
console.log(run(() => {
throw "text";
}));
function annotate(fn: () => void): void {
try {
fn();
} catch (e: any) {
e.message = `config: ${e.message}`;
if (e.code === "ENOPE") e.hint = "check the file";
throw e;
}
}
for (const code of ["ENOPE", "EOTHER"]) {
try {
annotate(() => {
throw Object.assign(new Error("missing"), { code });
});
} catch (e) {
const err = e as Error & { code?: string; hint?: string };
console.log(err.message, err.code, err.hint, err instanceof Error);
}
}
// An assertion on a catch binding typed any.
function codeOf(fn: () => void): string {
try {
fn();
return "none";
} catch (e: any) {
const err = e as Error & { code?: string };
return `${err.message}:${err.code ?? "-"}`;
}
}
console.log(
codeOf(() => {
throw Object.assign(new Error("coded"), { code: "E1" });
}),
codeOf(() => {}),
);
@@ -0,0 +1,50 @@
// Generic wrappers returning their untyped result `as F`: async functions,
// records with unknown fields, and rejections passing through.
function trace<F extends (...args: any[]) => any>(name: string, fn: F): F {
return ((...args: Parameters<F>) => {
console.log("span", name);
return fn(...args);
}) as F;
}
function traceThis<F extends (...args: any[]) => any>(name: string, fn: F): F {
return function (this: unknown, ...args: Parameters<F>) {
const result: unknown = fn.apply(this, args);
console.log("span", name, typeof result);
return result;
} as F;
}
function lookup(key: string): any {
return key === "set" ? 7 : undefined;
}
const fetchCount = trace("fetchCount", async (base: number): Promise<number> => {
await null;
return base + 1;
});
const fetchUser = traceThis("fetchUser", async (id: string): Promise<{ id: string; admin: boolean }> => ({
id,
admin: id === "root",
}));
const fails = trace("fails", async (why: string): Promise<number> => {
throw new Error(why);
});
const summarize = trace("summarize", (limit = lookup("unset") ?? 2) => ({ kept: limit, dropped: 1 }));
const measure = traceThis("measure", (n: number) => ({ size: n, label: lookup("set") }));
async function main(): Promise<void> {
console.log(await fetchCount(1));
const user = await fetchUser("root");
console.log(user.id, user.admin);
try {
await fails("nope");
} catch (e) {
console.log("caught", e instanceof Error ? e.message : String(e));
}
const s = summarize();
console.log(s.kept, s.dropped, summarize(5).kept);
const m = measure(3);
console.log(m.size, m.label);
const pending = fetchCount(10);
console.log("before");
console.log(await pending);
}
main();
export {};
@@ -0,0 +1,27 @@
// An Error subclass in the shape of a validation library's: the constructor
// resets the prototype to new.target's, as transpiled code does.
export class IssueError extends Error {
constructor(issues) {
super();
this.issues = [];
const actualProto = new.target.prototype;
if (Object.setPrototypeOf) {
Object.setPrototypeOf(this, actualProto);
} else {
this.__proto__ = actualProto;
}
this.name = "IssueError";
this.issues = issues;
}
get message() {
return JSON.stringify(this.issues);
}
get isEmpty() {
return this.issues.length === 0;
}
}
export class StrictIssueError extends IssueError {
get strict() {
return true;
}
}
@@ -0,0 +1,33 @@
// Constructors that reset the instance prototype, the transpiled idiom for
// Error subclasses.
import { IssueError, StrictIssueError } from "./issues.js";
class AppError extends Error {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
this.name = "AppError";
}
describe(): string {
return `${this.name}: ${this.message}`;
}
}
class NotFound extends AppError {
constructor(what: string) {
super(`${what} not found`);
Object.setPrototypeOf(this, NotFound.prototype);
this.name = "NotFound";
}
}
const app = new AppError("boom");
const missing = new NotFound("user");
console.log(app.describe(), app instanceof AppError, app instanceof Error);
console.log(missing.describe(), missing instanceof NotFound, missing instanceof AppError);
const issues = new IssueError([{ path: "a" }]);
console.log(issues.message, issues.isEmpty, issues instanceof IssueError, issues.name);
const strict = new StrictIssueError([]);
console.log(strict.isEmpty, strict.strict, strict instanceof IssueError, String(strict instanceof StrictIssueError));
try {
throw new NotFound("page");
} catch (e) {
console.log(e instanceof NotFound ? e.describe() : "other");
}
@@ -0,0 +1,22 @@
// JavaScript Error subclasses whose toString overrides infer results that
// differ from Error.prototype.toString's string.
export class IssueError extends Error {
constructor(issues) {
super("invalid");
this.issues = issues;
this.name = "IssueError";
}
/** @returns {string | undefined} */
toString() {
return this.issues.length > 0 ? this.issues.join("; ") : undefined;
}
}
export class LabelError extends Error {
constructor(label, detail) {
super(detail);
this.label = label;
}
toString() {
return this.label;
}
}
@@ -0,0 +1,9 @@
// TypeScript callers of JavaScript toString overrides whose inferred
// results differ from the inherited string.
import { IssueError, LabelError } from "./errors.js";
const issues = new IssueError(["a is required", "b must be a number"]);
console.log(issues.toString());
console.log(String(issues), `${new IssueError([])}`);
const errors: Error[] = [issues, new LabelError("db", "down"), new Error("plain")];
for (const e of errors) console.log(e.toString(), e instanceof IssueError);
console.log(errors.map((e) => String(e)).join(" | "));
@@ -0,0 +1,22 @@
// An untyped JavaScript lock whose async acquire resolves to an untyped
// ticket.
export class Lock {
constructor() {
this.held = false;
this.log = [];
this.queue = makeQueue(this.log);
}
async acquire() {
const ticket = await this.queue.wait();
this.held = true;
this.log.push("base acquire");
return ticket;
}
release() {
this.held = false;
}
}
function makeQueue(log) {
return { wait: () => new Promise((resolve) => setTimeout(() => resolve(log.length), 1)) };
}
@@ -0,0 +1,30 @@
// TypeScript overrides returning Promise<void> over an untyped JavaScript
// async method: an async override and a promise-returning one.
import { Lock } from "./lock.js";
class TimedLock extends Lock {
waits = 0;
async acquire(): Promise<void> {
this.waits++;
await super.acquire();
this.log.push("timed acquire");
}
}
class EagerLock extends Lock {
acquire(): Promise<void> {
this.log.push("eager");
return Promise.resolve();
}
}
async function main(): Promise<void> {
const locks: Lock[] = [new TimedLock(), new EagerLock(), new Lock()];
for (const lock of locks) {
await lock.acquire();
console.log(lock.held, lock.log.join(","));
}
const timed = new TimedLock();
await timed.acquire();
console.log(timed.waits, timed.held);
const viaBase: Lock = new TimedLock();
console.log(await new Lock().acquire(), await viaBase.acquire());
}
main();
+114
View File
@@ -0,0 +1,114 @@
// Thrown values of every kind read back through catch bindings: primitives,
// arrays, plain objects, Errors and subclasses, undefined and null, through
// un-narrowed reads and through typeof, instanceof, Array.isArray and
// predicate narrowing.
class AppError extends Error {
code: string;
constructor(message: string, code: string) {
super(message);
this.name = "AppError";
this.code = code;
}
}
interface Shaped {
kind: string;
size: number;
}
function isShaped(value: unknown): value is Shaped {
return typeof value === "object" && value !== null && "kind" in value && "size" in value;
}
// Each value is thrown at its own static type (values thrown from `unknown`
// storage are covered separately).
const letters = ["a", "b", "c"];
const none: string[] = [];
const shapedValue: Shaped = { kind: "box", size: 3 };
const emptyObject = {};
const plainError = new Error("plain");
const typeError = new TypeError("typed");
const appError = new AppError("app", "E_APP");
const thrown: unknown[] = [
5,
0,
-1.5,
"text",
"",
true,
false,
letters,
none,
shapedValue,
emptyObject,
plainError,
typeError,
appError,
undefined,
null,
];
function throwAt(index: number): never {
switch (index) {
case 0:
throw 5;
case 1:
throw 0;
case 2:
throw -1.5;
case 3:
throw "text";
case 4:
throw "";
case 5:
throw true;
case 6:
throw false;
case 7:
throw letters;
case 8:
throw none;
case 9:
throw shapedValue;
case 10:
throw emptyObject;
case 11:
throw plainError;
case 12:
throw typeError;
case 13:
throw appError;
case 14:
throw undefined;
default:
throw null;
}
}
function describe(index: number): string {
try {
throwAt(index);
} catch (e) {
const parts: string[] = [String(e === thrown[index])];
if (typeof e === "number") parts.push(`number ${e + 1}`);
else if (typeof e === "string") parts.push(`string ${e.length}`);
else if (typeof e === "boolean") parts.push(`boolean ${!e}`);
else if (Array.isArray(e)) parts.push(`array ${e.length} ${e.join("+")}`);
else if (e instanceof AppError) parts.push(`app ${e.code} ${e.message}`);
else if (e instanceof Error) parts.push(`error ${e.name} ${e.message}`);
else if (isShaped(e)) parts.push(`shaped ${e.kind} ${e.size}`);
else if (e === undefined) parts.push("undefined");
else if (e === null) parts.push("null");
else parts.push(`other ${typeof e}`);
return parts.join(" ");
}
}
for (let i = 0; i < thrown.length; i++) console.log(i, describe(i));
for (let i = 0; i < 10; i++) {
try {
if (i === 5) throw i;
} catch (e) {
console.log("loop", e === 5, e);
}
}
try {
throw ["x", "y"];
} catch (e) {
const list = e as string[];
console.log(list[0], list[1], list.length);
}
@@ -0,0 +1,64 @@
// An Error's code written through a dynamic view (a `catch (e: any)`
// binding), copied onto an Error subclass with Object.assign, set to
// undefined, or deleted is what typed reads and `'code' in err` see, on
// Errors and on Error subclasses.
class CodedError extends Error {}
const original = new Error("a") as NodeJS.ErrnoException;
try {
throw original;
} catch (e: any) {
e.code = "E_ANY";
}
console.log("any write", original.code, "code" in original);
try {
throw original;
} catch (e: any) {
delete e.code;
}
console.log("any delete", original.code, "code" in original);
function codeOf(err: NodeJS.ErrnoException): string | undefined {
return err.code;
}
const sub = new CodedError("b");
Object.assign(sub, { code: "E_ASSIGN" });
console.log("subclass assign", codeOf(sub), "code" in sub);
const plain = new Error("c") as NodeJS.ErrnoException;
Object.assign(plain, { code: "E_PLAIN" });
console.log("assign", plain.code, "code" in plain);
const cleared = new Error("d") as NodeJS.ErrnoException;
cleared.code = undefined;
console.log("undefined", cleared.code, "code" in cleared);
const fresh = new Error("e") as NodeJS.ErrnoException;
console.log("fresh", fresh.code, "code" in fresh);
const subCleared = new CodedError("f") as NodeJS.ErrnoException;
subCleared.code = undefined;
console.log("subclass undefined", subCleared.code, "code" in subCleared);
try {
throw subCleared;
} catch (e: any) {
console.log("subclass undefined through any", e.code, "code" in e);
}
subCleared.code = "E_SUB";
console.log("subclass string", subCleared.code, "code" in subCleared);
try {
throw subCleared;
} catch (e: any) {
e.code = undefined;
}
console.log("subclass any undefined", subCleared.code, "code" in subCleared);
try {
throw subCleared;
} catch (e: any) {
delete e.code;
}
console.log("subclass delete", subCleared.code, "code" in subCleared);
const subAssigned = new CodedError("g");
Object.assign(subAssigned, { code: undefined });
console.log("subclass assign undefined", codeOf(subAssigned), "code" in subAssigned);
@@ -0,0 +1 @@
{"plain":4713}
@@ -0,0 +1 @@
{"plain":3445}
@@ -0,0 +1 @@
{"plain":5309}
@@ -0,0 +1 @@
{"plain":3632}
@@ -0,0 +1 @@
{"plain":4787}
@@ -0,0 +1 @@
{"plain":4127}
@@ -0,0 +1 @@
{"plain":4693}
@@ -0,0 +1 @@
{"plain":3726}
@@ -0,0 +1 @@
{"plain":3837}
@@ -0,0 +1 @@
{"plain":4711}
@@ -0,0 +1 @@
{"plain":3803}
@@ -0,0 +1 @@
{"plain":3820}
@@ -0,0 +1 @@
{"plain":4942}
@@ -0,0 +1 @@
{"plain":3425}