mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Adapt method overrides across ABI differences, write Error codes, and close wrapper-reachable gaps (#1497)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
) {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6400-override-any-base.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6401-override-param-variance.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6402-override-js-base/pool.js",
|
||||
"<repo>/tests/corpus/6402-override-js-base/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6403-error-code-write.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6404-error-in-and-assign.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6405-wrapped-default-parameters.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6406-npm-instance-receiver/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6409-catch-binding-shapes.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6410-wrapper-untyped-results.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6411-prototype-reset-constructors/issues.js",
|
||||
"<repo>/tests/corpus/6411-prototype-reset-constructors/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -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": []
|
||||
}
|
||||
+7
@@ -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": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6414-catch-values-roundtrip.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6415-error-code-dynamic-writes.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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})`;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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());
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export interface Writable {
|
||||
uncork(): string;
|
||||
}
|
||||
export function create(options?: { highWaterMark?: number }): Writable;
|
||||
+45
@@ -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;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "corked-writer", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
@@ -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();
|
||||
@@ -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}
|
||||
Reference in New Issue
Block a user