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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/compiler/src/backend/llvm/lib-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
41 changes: 41 additions & 0 deletions packages/compiler/src/frontend/lowering/builtins/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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))
Expand Down
14 changes: 8 additions & 6 deletions packages/compiler/src/frontend/lowering/expressions/narrowing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
114 changes: 57 additions & 57 deletions packages/compiler/src/frontend/lowering/expressions/relational.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand Down
15 changes: 14 additions & 1 deletion packages/compiler/src/frontend/lowering/function-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/frontend/lowering/generic-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading