mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Name the deepest failing path when a checked cast fails inside an optional record
A checked cast such as `JSON.parse(text) as Partial<T>` makes every member
an `X | undefined` union. When a present object failed X, the error named
only the union's own path ("expected object | undefined at $.order, got
object"), not the missing or mistyped member inside it.
When no arm of a union matches and the value is 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), the failure
block now runs that arm's builder, which throws at the deepest failing
path with its reason ("expected string at $.order.owner.id, got
number"). Other values keep the union's message.
Only the failure block of union dynCheck builders changes; the matching
path is untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
139542bcf1
commit
b5e99ac67b
@@ -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