mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Throw a record extended by Object.assign as a checked-dynamic copy
`throw Object.assign(failure, { bail: true })`, where the record's type
lacks the source's member, used to fall to the Object.assign fence: a
fixed record cannot grow a field. As the operand of a throw, the call now
copies the target into a checked-dynamic object and adds the source's
members. The copy is unobservable, because control leaves with the throw
and nothing on that path reads the target again. Paths that already
lowered run first, so no compiled program changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fa2dad9051
commit
ac6b5ebe97
@@ -7601,6 +7601,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
|
||||
|
||||
@@ -117,3 +117,28 @@ function failure(message: string, bail: boolean): BuildFailure {
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user