mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
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:
co-authored by
Claude Opus 5.5
parent
45f4f0d47e
commit
84fcdb452b
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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">>,
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user