mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Name the deepest failing path when a checked cast fails inside an optional record (#1548)
This commit is contained in:
@@ -39,6 +39,7 @@ import {
|
||||
isDirentShape,
|
||||
isDynTypedRefType,
|
||||
isRefCounted,
|
||||
isUnitType,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
import { dynDesc, streamTypedRefEligible } from "../../ir/analysis.js";
|
||||
@@ -697,6 +698,46 @@ export class LlDyn {
|
||||
|
||||
/** Emit the standard in-helper pending check: on a pending exception
|
||||
* run `cleanup` and return `dummy`. */
|
||||
/** The failure block of a union dynCheck whose value matched no arm. A
|
||||
* value of the one container kind exactly one arm can take (an object
|
||||
* for the union's only record arm, an array for its only array arm,
|
||||
* beside unit and scalar arms) runs that arm's builder, which throws the
|
||||
* deepest failing path and the reason there (`expected string at
|
||||
* $.order.owner.id, got undefined`) instead of the union's own outer
|
||||
* path. Other values, and an arm builder that unexpectedly succeeds,
|
||||
* fall through to the union's message. Only the failure path changes. */
|
||||
private unionArmFailure(B: BlockBuilder, def: IrUnionDef, kind: string): void {
|
||||
const scalar = (arm: IrType): boolean =>
|
||||
isUnitType(arm) ||
|
||||
arm.kind === "f64" ||
|
||||
arm.kind === "bool" ||
|
||||
arm.kind === "string" ||
|
||||
arm.kind === "bigint";
|
||||
const containers = def.arms.filter((arm) => !scalar(arm));
|
||||
const arm = containers.length === 1 ? containers[0]! : null;
|
||||
const container =
|
||||
arm?.kind === "array"
|
||||
? DYN_KIND.ARR
|
||||
: arm?.kind === "record" && !this.host.recordsById.get(arm.shapeId)?.tuple
|
||||
? DYN_KIND.OBJ
|
||||
: null;
|
||||
if (!arm || container === null) return;
|
||||
const host = this.host;
|
||||
const deep = B.newLabel("dcu.deep");
|
||||
const outer = B.newLabel("dcu.outer");
|
||||
const same = B.tmp();
|
||||
B.line(`${same} = icmp eq i32 ${kind}, ${container}`);
|
||||
B.condBr(same, deep, outer);
|
||||
B.startBlock(deep);
|
||||
const built = B.tmp();
|
||||
B.line(`${built} = call ptr @${this.dynCheckHelper(arm)}(ptr %d, ptr %path)`);
|
||||
this.pendingBail(B, "dcu.deep", () => {}, "ptr null");
|
||||
const rc = vAdapters(host, arm);
|
||||
B.line(`call void ${rc.release}(ptr ${built})`);
|
||||
B.br(outer);
|
||||
B.startBlock(outer);
|
||||
}
|
||||
|
||||
private pendingBail(B: BlockBuilder, hint: string, cleanup: () => void, dummy: string): void {
|
||||
const p = B.tmp();
|
||||
for (const line of this.host.pendingTestLines(p)) B.line(line.trimStart());
|
||||
@@ -2291,6 +2332,7 @@ export class LlDyn {
|
||||
B.line(`call void @scr_dyn_release_v(ptr ${materialized})`);
|
||||
B.terminate(`ret ptr ${checked}`);
|
||||
B.startBlock(lFail);
|
||||
this.unionArmFailure(B, def, kind);
|
||||
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
|
||||
B.terminate(`ret ptr null`);
|
||||
break;
|
||||
@@ -2359,6 +2401,7 @@ export class LlDyn {
|
||||
);
|
||||
B.terminate(`ret ptr ${checked}`);
|
||||
B.startBlock(lFail);
|
||||
this.unionArmFailure(B, def, kind);
|
||||
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
|
||||
B.terminate(`ret ptr null`);
|
||||
break;
|
||||
|
||||
@@ -305,6 +305,46 @@ console.log("unreachable", c.host);
|
||||
);
|
||||
});
|
||||
|
||||
test("optional record member: the failure names the deepest path inside it", async () => {
|
||||
// `Partial<T>` makes every member an `X | undefined` union. A present
|
||||
// object that fails X used to report only the union's own path
|
||||
// ("expected object | undefined at $.order, got object"); the record
|
||||
// arm's builder now reports where inside it the value fails.
|
||||
const r = await compileAndRun(
|
||||
"optional-record-deep",
|
||||
`interface Order {
|
||||
owner: { id: string; plan: { tier: string } };
|
||||
total: number;
|
||||
}
|
||||
interface Envelope { order: Order; tags?: string[] }
|
||||
const texts = [
|
||||
'{"order":{"owner":{"id":"u1"},"total":3}}',
|
||||
'{"order":{"owner":{"id":7,"plan":{"tier":"pro"}},"total":3}}',
|
||||
'{"order":{"owner":{"id":"u1","plan":{"tier":"pro"}},"total":3},"tags":["a",2]}',
|
||||
'{"order":5}',
|
||||
];
|
||||
for (const text of texts) {
|
||||
try {
|
||||
const e = JSON.parse(text) as Partial<Envelope>;
|
||||
console.log("ok", e.order?.owner.id);
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError) console.log(err.message);
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
expect(r.exitCode).toBe(0);
|
||||
expect(r.stdout).toBe(
|
||||
[
|
||||
"expected object at $.order.owner.plan, got undefined",
|
||||
"expected string at $.order.owner.id, got number",
|
||||
"expected string at $.tags[1], got number",
|
||||
"expected object | undefined at $.order, got number",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("JSON null matches nothing", async () => {
|
||||
const r = await compileAndRun(
|
||||
"null-value",
|
||||
|
||||
Reference in New Issue
Block a user