diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 46580bfe5..cd5a8b3b5 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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; } + // 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; diff --git a/tests/corpus/integer-recursion-records.ts b/tests/corpus/integer-recursion-records.ts index 6a3f0c31e..17c3a11f6 100644 --- a/tests/corpus/integer-recursion-records.ts +++ b/tests/corpus/integer-recursion-records.ts @@ -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));