Set a trusted build aside while a getter runs outside a visit session

A getter or proxy trap that runs during a trusted build outside any
visit session now runs with the build's state set aside, as inside a
session. A conversion the getter starts, such as a recursive union
that opens and ends its own session, no longer clears the mark that
keeps the rest of the outer build from trusting its earlier match.
This commit is contained in:
Chris Tate
2026-10-25 03:34:33 -05:00
parent 0c6ed44227
commit 9cb9750f92
2 changed files with 49 additions and 13 deletions
+9 -13
View File
@@ -10932,9 +10932,9 @@ static void scr_dyn_visit_exit(void) {
}
/* Getters and proxy traps run user code, which may start conversions of
* its own and catch their failures. It runs with the session set aside,
* so its conversions use their own tables and leave the session's
* entries alone. */
* its own and catch their failures. It runs with the session and any
* trusted build set aside, so its conversions use their own tables and
* leave the session's entries and the trusted build's state alone. */
typedef struct {
size_t depth;
ScrDynVisitSet matching, built, viewed;
@@ -10977,12 +10977,10 @@ static void scr_dyn_visit_resume(const ScrDynVisitState *saved) {
scr_dyn_session_ran_code = true;
}
/* Calls a getter for a member read, outside any visit session. */
/* Calls a getter for a member read, outside any visit session or trusted
* build. */
static ScrDyn *scr_dyn_visit_call_getter(ScrDyn *getter) {
if (scr_dyn_visit_depth == 0) {
if (scr_dyn_trusted) scr_dyn_session_ran_code = true;
return scr_dyn_call(getter, NULL, 0, "getter");
}
if (scr_dyn_visit_depth == 0 && !scr_dyn_trusted) return scr_dyn_call(getter, NULL, 0, "getter");
ScrDynVisitState saved;
scr_dyn_visit_suspend(&saved);
ScrDyn *result = scr_dyn_call(getter, NULL, 0, "getter");
@@ -10990,12 +10988,10 @@ static ScrDyn *scr_dyn_visit_call_getter(ScrDyn *getter) {
return result;
}
/* A proxy's get trap for a member read, outside any visit session. */
/* A proxy's get trap for a member read, outside any visit session or
* trusted build. */
static ScrDyn *scr_dyn_visit_proxy_get(const ScrDyn *d, ScrStr *name) {
if (scr_dyn_visit_depth == 0) {
if (scr_dyn_trusted) scr_dyn_session_ran_code = true;
return scr_dyn_proxy_get(d, name);
}
if (scr_dyn_visit_depth == 0 && !scr_dyn_trusted) return scr_dyn_proxy_get(d, name);
ScrDynVisitState saved;
scr_dyn_visit_suspend(&saved);
ScrDyn *result = scr_dyn_proxy_get(d, name);
+40
View File
@@ -686,6 +686,46 @@ console.log(JSON.stringify(plain));
]);
});
test("nested unions: a getter that converts a recursive union keeps a non-recursive build untrusted", async () => {
// The getter converts a recursive union, which opens and ends a visit
// session of its own, before it answers. The outer build still matches
// the getter's second answer in full, and the failure names that union.
const r = await compileAndRun(
"flat-discriminated-getter-session",
`type Tree = { kind: "leaf"; n: number } | { kind: "node"; left: Tree; right: Tree };
type Inner = { tag: "x"; n: number } | { tag: "y"; s: string };
type Outer = { kind: "a"; inner: Inner } | { kind: "b"; other: number };
const v = JSON.parse('{"kind":"a"}') as unknown;
let calls = 0;
let leaves = 0;
Object.defineProperty(v, "inner", {
enumerable: true,
get() {
calls++;
const t = JSON.parse('{"kind":"node","left":{"kind":"leaf","n":1},"right":{"kind":"leaf","n":2}}') as Tree;
if (t.kind === "node") leaves += 2;
return calls === 1 ? JSON.parse('{"tag":"x","n":1}') : JSON.parse('{"tag":"x","n":"bad"}');
},
});
try {
const o = v as Outer;
console.log("ok", JSON.stringify(o));
} catch (err) {
console.log((err as Error).message);
}
console.log("getter calls", calls, "leaves", leaves);
const plain = JSON.parse('{"kind":"a","inner":{"tag":"y","s":"z"}}') as Outer;
console.log(JSON.stringify(plain));
`,
);
expect(r.exitCode).toBe(0);
expect(r.stdout.trimEnd().split("\n")).toEqual([
"expected object | object at $.inner, got object",
"getter calls 2 leaves 4",
'{"kind":"a","inner":{"tag":"y","s":"z"}}',
]);
});
test("array where a record is expected names the mismatch at the root", async () => {
const r = await compileAndRun(
"root-kind",