Plan partial record views for a subset source

A record literal asserted to a wider record type (`{ truncated: true }
as TruncationResult`) is a checked cast whose source shape lacks some of
the target's fields. Slice C1 of the presence-narrowing design: teach the
record-view plan, the LLVM backend and the runtime about the PARTIAL view
such a cast answers.

- `dynCheck.partialView` marks the cast; the validator requires a
  `dynFrom{liveRef}` record operand whose every field is a target field
  with an equal type.
- `planRecordViews` records the (target, source) pairs it cannot plan as
  full views and exposes `partial`, `partialTargets` and
  `partialMissing`; the cycle analysis owns these views like the rest.
- `recordViewCall` emits the `SIZE_MAX` sentinel for a target field the
  source does not declare.
- `scr_record_view_slot` answers NULL for a field the viewed record does
  not declare (its offset table holds `SIZE_MAX`), and
  `scr_record_view_field` passes that answer through. The emitted view
  guard, the clone helper and a presence test (`recordHas`) ask for that
  slot and stop with `scr_trap` and the program's own message when it is
  absent, so the runtime gains no string constant and no function: a new
  function in a linked unit shifts the pack for every program. A program
  without a partial assertion compiles to the same bytes.
- `recordHas` asks the view when the field is missing from some partial
  source, and still honors an undefined-armed field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-29 05:39:30 -07:00
co-authored by Claude Opus 5.5
parent 45f4f0d47e
commit 84fcdb452b
13 changed files with 380 additions and 26 deletions
+5 -2
View File
@@ -1179,8 +1179,11 @@ export class LlDyn {
const struct = mangleRecordStruct(sourceId);
const base = host.nullableUnions.fieldBase(sourceId);
const offsets = target.fields.map((field) => {
const index = source.fields.findIndex((f) => f.name === field.name) + base;
return `${S} ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 0, i32 ${index}) to ${S})`;
const index = source.fields.findIndex((f) => f.name === field.name);
// A partial view's target field the source does not declare has
// no slot: all ones (SIZE_MAX) marks it absent (record-views.ts).
if (index < 0) return `${S} -1`;
return `${S} ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 0, i32 ${index + base}) to ${S})`;
});
const offsetTable = `${table}_${k}`;
this.defs.push(
@@ -5060,6 +5060,8 @@ export class LlEmitter {
idx,
(decl) => this.declare(decl),
this.rcMode === undefined,
(text) => this.cstr(text),
this.shapeHost.recordViews?.partialTargets.has(shapeId) === true,
);
} else {
p = this.B.tmp();
@@ -10,6 +10,7 @@ import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js";
import { isRefCounted, type IrExpr, type IrStrCmpOp, type IrType } from "../../ir/ir.js";
import { mangleRecordClone, mangleRecordNew } from "../mangle.js";
import { arrNewCall, elemAccess } from "./shapes.js";
import { recordViewTestLines } from "./record-views.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import { offsetIndex } from "./expr-bytes.js";
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
@@ -1473,16 +1474,68 @@ export function emitRecordExpr(
return host.emitRecordKeyGet(e);
case "recordHas": {
// Own-property presence: only an undefined-armed union slot can
// lack its property, by holding the ABSENT state.
// lack its property, by holding the ABSENT state, and a PARTIAL
// record view (record-views.ts) can lack a field its viewed record
// does not declare.
const fieldType = host.recordsById
.get(e.shapeId)
?.fields.find((f) => f.name === e.field)?.type;
const obj = host.emitReadReceiver(e.obj);
if (host.shapeHost.recordViews?.partialMissing(e.shapeId, e.field) !== true) {
const { ptr, type, inline } = host.recordFieldPtr(obj.name, e.shapeId, e.field);
if (type.kind !== "union" || undefinedArmTag(type, host.unionsById) < 0)
return { name: "true", type: e.type };
const v = host.loadField(ptr, type, inline);
const absent = host.fieldAbsentTest(v, type.unionId);
const t = B.tmp();
B.line(`${t} = xor i1 ${absent}, true`);
return { name: t, type: e.type };
}
// A field some partial view of this shape lacks: ask the view first.
// Only a present field's slot may be looked up — a partial view's
// absent field answers no slot at all (scr_record_view_slot).
const position = host.recordsById.get(e.shapeId)!.fields.findIndex((f) => f.name === e.field);
host.declare(`declare ptr @scr_record_view_slot(ptr, ${host.sizeType})`);
const isView = B.tmp();
const rhFrom = B.currentLabel();
const view = B.newLabel("rh.view");
const done = B.newLabel("rh.done");
for (const line of recordViewTestLines(host.sizeType, obj.name, isView))
B.line(line.trimStart());
// Not a view: every field of the target has its slot.
B.condBr(isView, view, done, `, !prof !{!"branch_weights", i32 1, i32 1000}`);
B.startBlock(view);
const slot = B.tmp();
B.line(
`${slot} = call ptr @scr_record_view_slot(ptr ${obj.name}, ${host.sizeType} ${position})`,
);
const hasSlot = B.tmp();
B.line(`${hasSlot} = icmp ne ptr ${slot}, null`);
B.br(done);
B.startBlock(done);
const hasView = B.tmp();
B.line(`${hasView} = phi i1 [ true, %${rhFrom} ], [ ${hasSlot}, %${view} ]`);
if (fieldType === undefined || fieldType.kind !== "union")
return { name: hasView, type: e.type };
const unionId = fieldType.unionId;
if (undefinedArmTag(fieldType, host.unionsById) < 0) return { name: hasView, type: e.type };
// The field is also undefined-armed: a present slot still answers by
// its ABSENT state, a field the view lacks by the view.
const check = B.newLabel("rh.present");
const join = B.newLabel("rh.join");
const from = B.currentLabel();
B.condBr(hasView, check, join);
B.startBlock(check);
const { ptr, type, inline } = host.recordFieldPtr(obj.name, e.shapeId, e.field);
if (type.kind !== "union" || undefinedArmTag(type, host.unionsById) < 0)
return { name: "true", type: e.type };
const v = host.loadField(ptr, type, inline);
const absent = host.fieldAbsentTest(v, type.unionId);
const t = B.tmp();
B.line(`${t} = xor i1 ${absent}, true`);
return { name: t, type: e.type };
const absent = host.fieldAbsentTest(v, unionId);
const present = B.tmp();
B.line(`${present} = xor i1 ${absent}, true`);
B.br(join);
B.startBlock(join);
const answer = B.tmp();
B.line(`${answer} = phi i1 [ false, %${from} ], [ ${present}, %${check} ]`);
return { name: answer, type: e.type };
}
case "fieldAbsent":
return { name: host.absentInstanceRef(e.unionId), type: e.type };
@@ -3,8 +3,11 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile, deserializeModule } from "../../index.js";
import type { IrModule } from "../../ir/ir.js";
import { DYN } from "../../ir/ir.js";
import type { IrExpr, IrModule } from "../../ir/ir.js";
import { computeTraced } from "../cycle-analysis.js";
import { everyStmtList } from "../../ir/traverse.js";
import { emitLlvmModule } from "./emitter.js";
import { planRecordViews } from "./record-views.js";
/** The views planned for `source`'s module, as target field names → source
@@ -128,3 +131,98 @@ ${conversions}
Object.freeze(wide);`);
expect(views).toEqual(new Map());
});
/** `source`'s module shaped the way the frontend lowers a partial
* assertion (C2): the checked cast of the source record literal carries
* `partialView` over a `dynFrom{liveRef}` of that literal. */
async function partialModule(
source: string,
sourceFields: string,
targetFields: string,
): Promise<IrModule> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-record-views-"));
try {
const entry = join(dir, "main.ts"),
outPath = join(dir, "main.ir.json");
await writeFile(entry, source);
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
const mod: IrModule = deserializeModule(await readFile(outPath, "utf8"));
const idOf = (fields: string): string =>
(mod.records ?? []).find((shape) => shape.fields.map((f) => f.name).join(",") === fields)!.id;
const exprs: IrExpr[] = [];
for (const fn of mod.functions)
everyStmtList(fn.body, {
stmt: () => true,
expr: (e) => {
exprs.push(e);
return true;
},
});
const lit = exprs.find(
(e) =>
e.kind === "recordLit" && e.type.kind === "record" && e.type.shapeId === idOf(sourceFields),
);
const check = exprs.find(
(e) =>
e.kind === "dynCheck" && e.type.kind === "record" && e.type.shapeId === idOf(targetFields),
);
if (!lit || !check || check.kind !== "dynCheck") throw new Error("shapes not found");
check.partialView = true;
check.value = { kind: "dynFrom", liveRef: true, value: lit, type: DYN, loc: check.loc };
return mod;
} finally {
await rm(dir, { recursive: true, force: true });
}
}
test("a partial assertion plans a partial view with an absent-field sentinel", async () => {
const mod = await partialModule(
`
type Wide = { a: number; b: string };
type Narrow = { a: number; z: boolean };
const wide: Wide = { a: 1, b: "x" };
const o: unknown = wide;
const v = o as Narrow;
console.log(v.a);
`,
"a,b",
"a,z",
);
const idOf = (fields: string): string =>
(mod.records ?? []).find((shape) => shape.fields.map((f) => f.name).join(",") === fields)!.id;
const target = idOf("a,z");
const source = idOf("a,b");
const views = planRecordViews(mod, { traced: () => false, excluded: () => false });
expect(views.targets.has(target)).toBe(true);
expect(views.sources(target)).toEqual([source]);
expect(views.partial(target, source)).toBe(true);
expect(views.partialTargets.has(target)).toBe(true);
expect(views.partialMissing(target, "z")).toBe(true);
expect(views.partialMissing(target, "a")).toBe(false);
// The offset table marks the target field the source does not declare
// with the all-ones sentinel.
const llvm = emitLlvmModule(mod);
expect(llvm).toMatch(/@sc_rv_\d+_0 = internal constant \[2 x i64\] \[i64 ptrtoint .*, i64 -1\]/);
});
test("a full source keeps an ordinary view, not a partial one", async () => {
const mod = await partialModule(
`
type Wide = { a: number; b: string };
type Narrow = { a: number };
const wide: Wide = { a: 1, b: "x" };
const o: unknown = wide;
const v = o as Narrow;
console.log(v.a);
`,
"a,b",
"a",
);
const idOf = (fields: string): string =>
(mod.records ?? []).find((shape) => shape.fields.map((f) => f.name).join(",") === fields)!.id;
const views = planRecordViews(mod, { traced: () => false, excluded: () => false });
expect(views.sources(idOf("a"))).toEqual([idOf("a,b")]);
expect(views.partial(idOf("a"), idOf("a,b"))).toBe(false);
expect(views.partialTargets.size).toBe(0);
});
@@ -15,7 +15,15 @@
* checked conversion can produce (the closure of dynCheck targets), and
* some other record type whose values reach untyped code as live capsules
* (the closure of live dynFrom types) has all of B's fields. A program
* without checked conversions has none, so its code is unchanged. */
* without checked conversions has none, so its code is unchanged.
*
* A PARTIAL view (`dynCheck.partialView`) pairs a record literal's shape A
* with the wider record B it was asserted to, though A lacks some of B's
* fields. The view is a B value whose record-source entry names A, with
* the sentinel SIZE_MAX for every B field A does not declare: a slot lookup
* answers NULL there (scr_record_view_slot), so a presence test answers false
* and an address lookup stops the program. The frontend's checked reads of
* those fields throw their TypeError first. */
import {
isDirentShape,
KEY_ORDER_FIELD,
@@ -34,9 +42,28 @@ export interface RecordViews {
readonly targets: ReadonlySet<string>;
/** The record shapes whose capsules convert to a view of `target`. */
sources(target: string): readonly string[];
/** Whether `target` may be a PARTIAL view of `source`: some of its
* fields are absent, because `source` does not declare them (a partial
* record literal asserted to a wider type). */
partial(target: string, source: string): boolean;
/** Shape ids a partial record assertion may produce a view of. */
readonly partialTargets: ReadonlySet<string>;
/** Whether some partial source of view target `target` lacks `field`. */
partialMissing(target: string, field: string): boolean;
}
export const NO_RECORD_VIEWS: RecordViews = { targets: new Set(), sources: () => [] };
/** The trap of an internal reader of a field a partial view's viewed record
* lacks (a width copy, say): a source-level read of the field threw first. */
export const PARTIAL_VIEW_ABSENT =
"scriptc: a value asserted as a wider record has no property at this field (copy it explicitly)\n";
export const NO_RECORD_VIEWS: RecordViews = {
targets: new Set(),
sources: () => [],
partial: () => false,
partialTargets: new Set(),
partialMissing: () => false,
};
interface ViewOptions {
readonly traced: (shapeId: string) => boolean;
@@ -155,16 +182,30 @@ export function planRecordViews(mod: IrModule, options: ViewOptions): RecordView
};
const checked: IrType[] = [];
const boxed: IrType[] = [];
// (target B, source A) of a partial record assertion's checked cast.
const partialPairs: { target: string; source: string }[] = [];
for (const fn of mod.functions)
everyStmtList(fn.body, {
stmt: () => true,
expr: (e) => {
if (e.kind === "dynCheck") checked.push(e.type);
else if (e.kind === "dynFrom" && e.liveRef === true) boxed.push(e.value.type);
if (e.kind === "dynCheck") {
checked.push(e.type);
if (
e.partialView === true &&
e.type.kind === "record" &&
e.value.kind === "dynFrom" &&
e.value.value.type.kind === "record"
)
partialPairs.push({
target: e.type.shapeId,
source: e.value.value.type.shapeId,
});
} else if (e.kind === "dynFrom" && e.liveRef === true) boxed.push(e.value.type);
return true;
},
});
if (checked.length === 0 || boxed.length === 0) return NO_RECORD_VIEWS;
if ((checked.length === 0 || boxed.length === 0) && partialPairs.length === 0)
return NO_RECORD_VIEWS;
const reachable = closure(checked);
const live = closure(boxed);
if (reachable.size === 0) return NO_RECORD_VIEWS;
@@ -182,6 +223,24 @@ export function planRecordViews(mod: IrModule, options: ViewOptions): RecordView
}
if (found.length > 0) sources.set(id, found);
}
// Partial pairs join the plan before the cycle analysis, which owns their
// view's capsule reference like any other. A pair whose source has every
// target field is an ordinary full view and is already planned above.
const partials = new Map<string, Set<string>>();
for (const pair of partialPairs) {
const target = shapes.get(pair.target);
const source = shapes.get(pair.source);
if (!target || !source || !plainShape(target) || !plainShape(source)) continue;
if (target.freezable === true || source.freezable === true) continue;
if (options.traced(pair.target) || options.excluded(pair.target)) continue;
if (covers(source, target)) continue;
const list = sources.get(pair.target) ?? [];
if (!list.includes(pair.source)) list.push(pair.source);
sources.set(pair.target, list);
const set = partials.get(pair.target);
if (set) set.add(pair.source);
else partials.set(pair.target, new Set([pair.source]));
}
// A view's record-source entry owns the source's capsule, an edge the
// cycle analysis does not see. A view stays off every cycle only when its
// source cannot hold a value of the target type, such as the view itself,
@@ -199,11 +258,34 @@ export function planRecordViews(mod: IrModule, options: ViewOptions): RecordView
changed = true;
if (kept.length > 0) sources.set(id, kept);
else sources.delete(id);
const partial = partials.get(id);
if (!partial) continue;
for (const source of [...partial]) if (!kept.includes(source)) partial.delete(source);
if (partial.size === 0) partials.delete(id);
}
}
if (sources.size === 0) return NO_RECORD_VIEWS;
const targets = new Set(sources.keys());
return { targets, sources: (target) => sources.get(target) ?? [] };
// The target fields some partial source lacks: their presence test is a
// runtime view probe, and their slot lookup traps.
const missing = new Map<string, Set<string>>();
for (const [id, list] of partials) {
const absent = new Set<string>();
const target = shapes.get(id);
for (const source of list) {
const declared = new Set((shapes.get(source)?.fields ?? []).map((field) => field.name));
for (const field of target?.fields ?? [])
if (!declared.has(field.name)) absent.add(field.name);
}
missing.set(id, absent);
}
return {
targets,
sources: (target) => sources.get(target) ?? [],
partial: (target, source) => partials.get(target)?.has(source) === true,
partialTargets: new Set(partials.keys()),
partialMissing: (target, field) => missing.get(target)?.has(field) === true,
};
}
/** Lines setting `dest` (i1) to whether the record `obj` of a view target
@@ -228,7 +310,12 @@ export function recordViewTestLines(sizeType: "i32" | "i64", obj: string, dest:
* `obj` of view-target shape `struct`: the slot itself, or the viewed
* record's slot when `obj` is a view. Optimized builds test the mark
* inline and look the slot up out of line; unoptimized builds, which give
* every value a frame slot, make the address one call. */
* every value a frame slot, make the address one call.
*
* `partial` names a shape a PARTIAL view may present: its viewed record can
* lack the field, and the slot lookup answers NULL there (scr_record_view_slot),
* so the address stops with `scr_trap` and `cstr(PARTIAL_VIEW_ABSENT)`. The
* message is the program's own constant; the runtime carries none. */
export function recordViewFieldPtr(
B: BlockBuilder,
sizeType: "i32" | "i64",
@@ -238,13 +325,25 @@ export function recordViewFieldPtr(
position: number,
declare: (decl: string) => void,
outOfLine: boolean,
cstr: (text: string) => string,
partial = false,
): string {
if (outOfLine) {
declare(`declare ptr @scr_record_view_field(ptr, ${sizeType}, ${sizeType})`);
const field = "scr_record_view_field";
declare(`declare ptr @${field}(ptr, ${sizeType}, ${sizeType})`);
const ptr = B.tmp();
B.line(
`${ptr} = call ptr @scr_record_view_field(ptr ${obj}, ${sizeType} ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 0, i32 ${slotIndex}) to ${sizeType}), ${sizeType} ${position})`,
`${ptr} = call ptr @${field}(ptr ${obj}, ${sizeType} ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 0, i32 ${slotIndex}) to ${sizeType}), ${sizeType} ${position})`,
);
if (!partial) return ptr;
const absent = B.tmp();
B.line(`${absent} = icmp eq ptr ${ptr}, null`);
const trap = B.newLabel("rv.absent");
const join = B.newLabel("rv.present");
B.condBr(absent, trap, join, `, !prof !{!"branch_weights", i32 1, i32 1000}`);
B.startBlock(trap);
absentTrapLines(B, cstr, declare);
B.startBlock(join);
return ptr;
}
const own = B.tmp();
@@ -260,9 +359,34 @@ export function recordViewFieldPtr(
declare(`declare ptr @scr_record_view_slot(ptr, ${sizeType})`);
const viewed = B.tmp();
B.line(`${viewed} = call ptr @scr_record_view_slot(ptr ${obj}, ${sizeType} ${position})`);
let viewFrom = view;
if (partial) {
const absent = B.tmp();
B.line(`${absent} = icmp eq ptr ${viewed}, null`);
const trap = B.newLabel("rv.absent");
const present = B.newLabel("rv.present");
B.condBr(absent, trap, present);
B.startBlock(trap);
absentTrapLines(B, cstr, declare);
viewFrom = present;
B.startBlock(present);
}
B.br(join);
B.startBlock(join);
const ptr = B.tmp();
B.line(`${ptr} = phi ptr [ ${own}, %${from} ], [ ${viewed}, %${view} ]`);
B.line(`${ptr} = phi ptr [ ${own}, %${from} ], [ ${viewed}, %${viewFrom} ]`);
return ptr;
}
/** The block of an address lookup of a field a partial view lacks: the
* program stops with its own message (PARTIAL_VIEW_ABSENT) and does not
* return. */
export function absentTrapLines(
B: BlockBuilder,
cstr: (text: string) => string,
declare: (decl: string) => void,
): void {
declare(`declare void @scr_trap(ptr)`);
B.line(`call void @scr_trap(ptr ${cstr(PARTIAL_VIEW_ABSENT)})`);
B.terminate("unreachable");
}
@@ -28,6 +28,10 @@ function declarationHost(
declare(decl) {
declarations.push(decl);
},
cstr(text) {
declarations.push(`@cstr ${text}`);
return "@sc_cstr";
},
needOom() {},
mapOps: (operands) => (operands.every((op) => op === "null") ? "null" : "@sc_map_ops"),
sizeType: pointerBits === 32 ? "i32" : "i64",
+20 -3
View File
@@ -73,7 +73,7 @@ import {
disposeFieldLines,
plainRefType,
} from "./arenas.js";
import { recordViewTestLines, type RecordViews } from "./record-views.js";
import { PARTIAL_VIEW_ABSENT, recordViewTestLines, type RecordViews } from "./record-views.js";
import { recordMayKeepSource } from "./record-sources.js";
import { LlvmUnsupportedError } from "./unsupported.js";
@@ -109,6 +109,9 @@ export interface ShapeHost {
/** Byte offset from an object pointer back to ScrCycHdr.buf. */
readonly cycleBufOffset: number;
declare(decl: string): void;
/** The module constant for a string (LlEmitter.cstr), for a trap message
* an emitted accessor passes to the runtime. */
cstr(text: string): string;
/** Inline the small-object allocator's fast paths (llvm/alloc.ts). */
readonly inlineAlloc?: boolean;
/** Record and class fields that store a scalar union's tag and bits
@@ -2450,15 +2453,29 @@ export function emitRecordShapes(
const inline = host.inlineUnions.record(field.type);
const fieldTy = llFieldType(field.type, inline);
if (host.recordViews?.targets.has(shape.id) === true) {
// A view's fields live in the viewed record (record-views.ts).
// A view's fields live in the viewed record (record-views.ts); a
// shape a partial view may present answers NULL for a field the
// viewed record lacks, so the copy stops with the program's own
// message (a source-level read threw first).
const partial = host.recordViews.partialTargets.has(shape.id);
host.declare(`declare ptr @scr_record_view_slot(ptr, ${host.sizeType})`);
const fromView = partial
? [
` %sn${i} = icmp eq ptr %sw${i}, null`,
` br i1 %sn${i}, label %sv${i}.absent, label %sv${i}.join`,
`sv${i}.absent:`,
` call void @scr_trap(ptr ${host.cstr(PARTIAL_VIEW_ABSENT)})`,
` unreachable`,
]
: [` br label %sv${i}.join`];
if (partial) host.declare(`declare void @scr_trap(ptr)`);
clone.push(
` %so${i} = getelementptr inbounds %${struct}, ptr %src, i64 0, i32 ${index}`,
...recordViewTestLines(host.sizeType, "%src", `%sr${i}v`),
` br i1 %sr${i}v, label %sv${i}.view, label %sv${i}.join`,
`sv${i}.view:`,
` %sw${i} = call ptr @scr_record_view_slot(ptr %src, ${host.sizeType} ${i})`,
` br label %sv${i}.join`,
...fromView,
`sv${i}.join:`,
` %sp${i} = phi ptr [ %so${i}, %${i === 0 ? "entry" : `sv${i - 1}.join`} ], [ %sw${i}, %sv${i}.view ]`,
` %sv${i} = load ${fieldTy}, ptr %sp${i}`,
@@ -293,6 +293,8 @@ export class LlWalkers {
index - base,
(decl) => this.host.declare(decl),
this.host.rcMode === undefined,
(text) => this.host.cstr(text),
this.host.recordViews?.partialTargets.has(shapeId) === true,
);
} else {
p = B.tmp();
+2 -1
View File
@@ -2115,10 +2115,11 @@ export type IrFieldsCurrent = [
Unread<FieldsNotRead<Extract<NonNullable<Extract<NonNullable<Extract<IrExpr, { kind: "dynCall" }>["spreads"]>, readonly unknown[]>[number]>, object>, "arg" | "what">>,
Unread<ValuesNotRead<Extract<NonNullable<Extract<NonNullable<Extract<IrExpr, { kind: "dynCall" }>["spreads"]>, readonly unknown[]>[number]>, object>["arg"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynCall" }>, "kind" | "callee" | "receiver" | "calleeName" | "calleeNameValue" | "args" | "spreads" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynCheck" }>, "kind" | "value" | "preserveRefs" | "adopt" | "exact" | "inferredSite" | "origin" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynCheck" }>, "kind" | "value" | "preserveRefs" | "adopt" | "exact" | "partialView" | "inferredSite" | "origin" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynCheck" }>["preserveRefs"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynCheck" }>["adopt"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynCheck" }>["exact"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynCheck" }>["partialView"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynCheck" }>["origin"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynDestrCheck" }>, "kind" | "value" | "spelling" | "firstProp" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynFrom" }>, "kind" | "value" | "fnName" | "fnLength" | "liveRef" | "type" | "loc">>,
+8
View File
@@ -4098,6 +4098,13 @@ export type IrExpr =
/** `exact`: the value is known to be a capsule this program boxed from
* exactly the class `type` names (a `dyn.typedRefKeyIs` test guards the
* expression), so the check is one load of the capsule's instance. */
/** `partialView`: `value` is `dynFrom{liveRef}` of a record whose fields
* are a SUBSET of `type`'s, with equal types — a partial record literal
* asserted to a wider record type (`{ truncated: true } as
* TruncationResult`). The conversion answers a partial record view
* (record-views.ts): a `type` value whose fields the source lacks are
* absent, so presence tests answer false and reads of them throw the
* catchable TypeError. */
/** `adopt`: the declaration-typed boundary's cast (decl-projection.ts).
* The conversion preserves identity: a source object or array that
* something else still references (reference count above one) is
@@ -4116,6 +4123,7 @@ export type IrExpr =
preserveRefs?: true;
adopt?: true;
exact?: true;
partialView?: true;
/** Shape inference's overlay: this conversion checks an
* analysis-proven fact ("<package> <file>:<line>"); a failure is a
* compiler analysis bug and reports itself as one. */
+31
View File
@@ -6295,6 +6295,37 @@ function validateFunction(
) {
err(`dynCheck against non-JSON-representable type ${e.type.kind}`, e.loc);
}
if (e.partialView === true) {
// A partial record assertion (record-views.ts): the operand is a
// live record whose fields are a SUBSET of the target's, with
// equal types, so the view lacks only properties the record
// never had.
if (
e.type.kind !== "record" ||
e.value.kind !== "dynFrom" ||
e.value.liveRef !== true ||
e.value.value.type.kind !== "record"
) {
err(`partialView dynCheck needs a live-record operand and a record target`, e.loc);
break;
}
const target = records.get(e.type.shapeId);
const source = records.get(e.value.value.type.shapeId);
if (!target || !source) {
err(`partialView dynCheck names an unknown record shape`, e.loc);
break;
}
for (const field of source.fields) {
const found = target.fields.find((f) => f.name === field.name);
if (!found || typeKey(found.type) !== typeKey(field.type)) {
err(
`partialView source field "${field.name}" is not a target field with an equal type`,
e.loc,
);
break;
}
}
}
break;
}
default: {
+4
View File
@@ -1342,6 +1342,10 @@ typedef struct {
size_t key_len;
const size_t *offsets;
} ScrRecordViewSource;
/* The slot of field `position` of a view, or NULL for a field the viewed
* record does not declare (a PARTIAL view holds SIZE_MAX; record-views.ts).
* A field address of a shape some partial view may present tests the answer
* and traps with the program's message. */
void *scr_record_view_slot(void *view, size_t position);
void *scr_record_view_field(void *value, size_t offset, size_t position);
void *scr_dyn_record_view(ScrDyn *d, const ScrRecordViewSource *sources, size_t count,
+9 -2
View File
@@ -4404,10 +4404,17 @@ void *scr_dyn_record_view(ScrDyn *d, const ScrRecordViewSource *sources, size_t
return view;
}
/* The slot of field `position` of a view (emitted view guards, cold). */
/* The slot of field `position` of a view (emitted view guards, cold), or
* NULL when the view cannot have it: a PARTIAL view (a record literal
* asserted to a wider record type) holds SIZE_MAX for every field its
* viewed record does not declare. The guard that gets NULL stops with
* scr_trap and the program's own message; a presence test asks for the same
* slot and answers by it. */
void *scr_record_view_slot(void *view, size_t position) {
ScrRecordSource *entry = scr_record_source_slot(view);
return (char *)entry->viewed + entry->offsets[position];
size_t offset = entry->offsets[position];
if (offset == SIZE_MAX) return NULL;
return (char *)entry->viewed + offset;
}
/* The address of field `position` of a value of a view target type, whose