mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Keep integer clones off records that may be views
A checked conversion can answer a record view: an object of the target record type whose fields live in the record it views, at that record's offsets (record-views.ts). An integer clone read the fields of a borrowed record at its own shape's offsets, so for a view it read the view's unused slots and returned wrong results. Record shapes that may be views are no longer references a clone reads, so functions that read them keep the ordinary body, which reads a view's fields through the original record. The emitter now plans integer clones after record views. integer-recursion-records.ts gains a recursive sum over a view.
This commit is contained in:
@@ -2288,30 +2288,6 @@ export class LlEmitter {
|
||||
};
|
||||
}
|
||||
this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes, this.nodeIndex);
|
||||
// Integer clones signal a stack overflow through their result. Worker
|
||||
// programs have them only as ELF executables, where the stack guard
|
||||
// and the pending word clones poll for termination are plain
|
||||
// thread-locals; debug builds and stack traces keep every frame.
|
||||
// Clones read number arrays and record graphs their function borrows
|
||||
// (64-bit layouts).
|
||||
this.cloneReferences = this.sizeType === "i64" ? this.recordCloneReferences() : undefined;
|
||||
this.integerClones =
|
||||
this.debug === null &&
|
||||
this.stackChecks !== null &&
|
||||
(mod.workers !== true || this.executableTls) &&
|
||||
mod.lib === undefined &&
|
||||
!this.stackTraces
|
||||
? findIntegerClones(
|
||||
mod,
|
||||
this.sizeType === "i64"
|
||||
? (fn, index) => this.callLifetimes.borrowed.get(fn)?.has(index) === true
|
||||
: undefined,
|
||||
this.cloneReferences,
|
||||
)
|
||||
: NO_INTEGER_CLONES;
|
||||
// Integer clones and their dispatchers call each other through their
|
||||
// own lowering, which keeps plain calls and flag tests.
|
||||
for (const name of this.integerClones.members) this.unwindAbi.delete(name);
|
||||
this.stackCallbacks = new StackCallbacks(this.fnByName);
|
||||
this.receiverReaders = new AmbientReceiverReaders(this.fnByName, this.nodeIndex);
|
||||
this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks);
|
||||
@@ -2467,6 +2443,30 @@ export class LlEmitter {
|
||||
this.shapeHost.recordViews = views;
|
||||
this.recordViewTargets = views.targets as Set<string>;
|
||||
}
|
||||
// Integer clones signal a stack overflow through their result. Worker
|
||||
// programs have them only as ELF executables, where the stack guard
|
||||
// and the pending word clones poll for termination are plain
|
||||
// thread-locals; debug builds and stack traces keep every frame.
|
||||
// Clones read number arrays and record graphs their function borrows
|
||||
// (64-bit layouts), but no record that may be a view (planned above).
|
||||
this.cloneReferences = this.sizeType === "i64" ? this.recordCloneReferences() : undefined;
|
||||
this.integerClones =
|
||||
this.debug === null &&
|
||||
this.stackChecks !== null &&
|
||||
(mod.workers !== true || this.executableTls) &&
|
||||
mod.lib === undefined &&
|
||||
!this.stackTraces
|
||||
? findIntegerClones(
|
||||
mod,
|
||||
this.sizeType === "i64"
|
||||
? (fn, index) => this.callLifetimes.borrowed.get(fn)?.has(index) === true
|
||||
: undefined,
|
||||
this.cloneReferences,
|
||||
)
|
||||
: NO_INTEGER_CLONES;
|
||||
// Integer clones and their dispatchers call each other through their
|
||||
// own lowering, which keeps plain calls and flag tests.
|
||||
for (const name of this.integerClones.members) this.unwindAbi.delete(name);
|
||||
this.walkers = new LlWalkers(this.shapeHost);
|
||||
this.dyn = new LlDyn(this.shapeHost);
|
||||
for (const g of mod.globals ?? []) {
|
||||
@@ -4925,17 +4925,23 @@ export class LlEmitter {
|
||||
* `inline`: the slot stores a scalar union's tag and bits. */
|
||||
/** The record layouts integer clones read in place: record pointers,
|
||||
* `R | null` unions of a record (NULL is `null`), and fields stored as a
|
||||
* double, an i8 boolean or such a pointer. */
|
||||
* double, an i8 boolean or such a pointer. Records that may be views
|
||||
* (record-views.ts) are not among them. */
|
||||
private recordCloneReferences(): CloneReferences {
|
||||
// A view's fields live in the record it views (recordViewFieldPtr), not
|
||||
// at its own shape's offsets.
|
||||
const plain = (shapeId: string): boolean =>
|
||||
this.recordsById.has(shapeId) && this.shapeHost.recordViews?.targets.has(shapeId) !== true;
|
||||
const nullable = (unionId: string): NullableUnion | null => {
|
||||
const single = this.nullableUnions.single(unionId);
|
||||
if (single === null || single.nullTag !== -1 || single.arm.kind !== "record") return null;
|
||||
if (!plain(single.arm.shapeId)) return null;
|
||||
const unit = this.unionsById.get(unionId)?.arms[single.unitTag];
|
||||
return unit?.kind === "nullT" ? single : null;
|
||||
};
|
||||
const reference = (type: IrType): boolean =>
|
||||
type.kind === "record"
|
||||
? this.recordsById.has(type.shapeId)
|
||||
? plain(type.shapeId)
|
||||
: type.kind === "union" && nullable(type.unionId) !== null;
|
||||
return {
|
||||
reference,
|
||||
@@ -4947,7 +4953,7 @@ export class LlEmitter {
|
||||
field: (shapeId, field) => {
|
||||
const shape = this.recordsById.get(shapeId);
|
||||
const index = shape?.fields.findIndex((f) => f.name === field) ?? -1;
|
||||
if (shape === undefined || index < 0) return null;
|
||||
if (shape === undefined || index < 0 || !plain(shapeId)) return null;
|
||||
const type = shape.fields[index]!.type;
|
||||
if (this.inlineUnions.record(type)) return null;
|
||||
if (type.kind !== "f64" && type.kind !== "bool" && !reference(type)) return null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Pure integer recursion over record graphs: counts and sums read from
|
||||
// fields, sums that leave the int32 range, number fields that are not
|
||||
// int32s, boolean fields, null tests and narrows, and endless recursion.
|
||||
// int32s, boolean fields, null tests and narrows, endless recursion, and
|
||||
// records that view another record.
|
||||
|
||||
interface Tree {
|
||||
left: Tree | null;
|
||||
@@ -106,6 +107,14 @@ function nodeSum(node: Node): number {
|
||||
return node.next === null ? node.value : node.value + nodeSum(node.next);
|
||||
}
|
||||
|
||||
// A record converted from an untyped value into a narrower record type is
|
||||
// a view of the original: its fields are the original's.
|
||||
type Measured = { label: string; hi: number; open: boolean; lo: number };
|
||||
type Span = { lo: number; hi: number };
|
||||
function spanSum(span: Span, n: number): number {
|
||||
return n === 0 ? span.lo : span.hi + spanSum(span, n - 1);
|
||||
}
|
||||
|
||||
function attempt(label: string, run: () => number | boolean): void {
|
||||
try {
|
||||
console.log(label, run());
|
||||
@@ -158,3 +167,8 @@ attempt("ring total", () => total(ring));
|
||||
console.log("pair", sumPair({ a: 2147483647, b: 1 }), sumPair({ a: 3, b: 4 }), sumPair({ a: 0.5, b: 0.25 }));
|
||||
console.log("link", linkSum({ value: 1, next: { value: 2, next: { value: 3 } } }));
|
||||
console.log("node", nodeSum(new Node(1, new Node(2, new Node(2147483647, null)))));
|
||||
const measured: Measured = { label: "m", hi: 2, open: true, lo: 7 };
|
||||
const untyped: unknown = measured;
|
||||
const span = untyped as Span;
|
||||
span.hi = 3;
|
||||
console.log("view", spanSum(span, 4), measured.hi, spanSum({ lo: 1, hi: 1 }, 3));
|
||||
|
||||
Reference in New Issue
Block a user