mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Keep the elements of InlineArray fields in their instances (#2380)
This commit is contained in:
@@ -366,6 +366,10 @@ Static code has no `WeakRef`, a `FinalizationRegistry` never runs its cleanup ca
|
||||
|
||||
A long-running program, such as a compiler in watch mode, can free program-lifetime instances one generation at a time with `LifetimeRegion` from [`@scriptc/experimental`](/docs/experimental). Instances created while `region.run(fn)` runs belong to the region. After `region.release()`, scriptc frees them together once nothing outside the region refers to any of them, and removes the `WeakMap` and `WeakSet` entries they are keys of. Instances created outside every region are never freed, as described above.
|
||||
|
||||
### Inline array fields
|
||||
|
||||
An array field of a program-lifetime class whose elements are program-lifetime instances can keep its elements in the instance itself, after the other fields, with `InlineArray` from [`@scriptc/experimental`](/docs/experimental). The constructor writes the field once from one of its parameters; reads that index the field, read its `length` or iterate it load the elements from the instance, and other reads get an array of them. The array passed to the constructor is one that nothing else keeps, and the arrays fields hold are never written. scriptc reports a field that does not keep these rules (SC7009).
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Each failed contract names the declaration and the reason:
|
||||
@@ -468,5 +472,9 @@ $ scriptc build reject.ts
|
||||
<td>SC7008</td>
|
||||
<td>A <code>ProgramLifetime</code> class is not the root of its hierarchy, or is part of a library build.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SC7009</td>
|
||||
<td>An <code>InlineArray</code> field is not on a <code>ProgramLifetime</code> class, is written other than once from a constructor parameter, or is read in a way that could tell its reads apart.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -334,3 +334,73 @@ scriptc requires:
|
||||
Each instance of a key class, and of every class in its hierarchy, carries a 4-byte slot index, 0 until a value is set for it. The first `set` for a key assigns the index; indices are never reused. `get` and `has` read the index, a page of the thread's family and the entry, with no lock, hash or atomic update. A family stores values in pages of 1024 entries, allocated as the thread sets values in them.
|
||||
|
||||
A family holds a reference to each of the thread's values. A value is released when it is replaced or deleted, when its key is freed, and when the thread exits. Keys that are freed with a [`LifetimeRegion`](#lifetimeregion) keep their values until the thread exits, a value that belongs to a region keeps the region allocated while the family holds it, and a value that refers to its own key keeps the key alive. Programs that never construct a `ThreadSlots` do not run any of its code, and their classes have no slot index.
|
||||
|
||||
## InlineArray
|
||||
|
||||
`InlineArray<T>` marks an array field of a [`ProgramLifetime`](/docs/contracts#programlifetime) class whose elements scriptc keeps in the instance itself, after its other fields, instead of in an array object of their own. It suits a program with millions of short arrays that never change once built, such as the children of each node of a syntax tree: an ordinary array costs a separate allocation with a header, and a reference to it in the instance.
|
||||
|
||||
```ts:lists.ts
|
||||
import type { ProgramLifetime } from "@scriptc/types";
|
||||
import type { InlineArray } from "@scriptc/experimental";
|
||||
|
||||
class Token implements ProgramLifetime {
|
||||
readonly text: string;
|
||||
constructor(text: string) {
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
class TokenList implements ProgramLifetime {
|
||||
readonly pos: number;
|
||||
readonly tokens: InlineArray<Token>;
|
||||
constructor(pos: number, tokens: readonly Token[]) {
|
||||
this.pos = pos;
|
||||
this.tokens = tokens;
|
||||
}
|
||||
}
|
||||
|
||||
function width(list: TokenList): number {
|
||||
let total = 0;
|
||||
for (const token of list.tokens) total += token.text.length;
|
||||
return total;
|
||||
}
|
||||
|
||||
const list = new TokenList(0, ["let", "x", "=", "1"].map((text) => new Token(text)));
|
||||
console.log(list.tokens.length, width(list), list.tokens[0]!.text);
|
||||
console.log(list.tokens.map((token) => token.text).join(" "));
|
||||
```
|
||||
|
||||
Both runtimes print the same:
|
||||
|
||||
```console
|
||||
$ node lists.ts
|
||||
4 6 let
|
||||
let x = 1
|
||||
$ scriptc build lists.ts -o lists >/dev/null
|
||||
$ ./lists
|
||||
4 6 let
|
||||
let x = 1
|
||||
```
|
||||
|
||||
### Field rules
|
||||
|
||||
scriptc requires:
|
||||
|
||||
- `T` to be a `ProgramLifetime` class, and the field to be an instance field of a `ProgramLifetime` class;
|
||||
- the constructor of the class that declares the field to write it once, from one of its parameters. A subclass constructor passes one of its own parameters there through `super(...)`. Nothing else writes the field;
|
||||
- the array passed to the constructor to be one that nothing else keeps: a new array (an array literal, a copy such as `slice()`, `[...items]` or `Array.from(items)`, or the result of a function that returns only such arrays or its own argument), a local array at its last use (calls that only read it or add to it may come first, and a loop may pass it and then assign a new array to the local), a parameter whose callers each pass such an array, an array that an InlineArray field already holds, or a `const` array that is never written, such as a shared empty array;
|
||||
- the arrays fields hold never to be written, through a read of the field (which the `readonly` type allows only through a cast) or through any other reference. scriptc follows these arrays through locals, parameters, return values, fields, the elements of arrays, Maps and Sets, and closures, and reports one that reaches a write, a dynamic value (also inside the fields of an object that becomes one), a call through a function value (also inside an array, Map or Set passed there), or a library call other than printing and `JSON.stringify` without a replacer. The report names the field read the array may come from;
|
||||
- the arrays fields hold, and their reads, not to be compared by identity: with `===` or `!==`, as an `indexOf` or `includes` argument, or as a Map key or Set element. Two reads of the field need not give the same array. Compare the elements instead, or keep a copy made with `slice()` where identity matters;
|
||||
- instances of the class not to be constructed through a class value, and not to become dynamic values (for example through a conversion to `unknown`), directly or inside another value that can hold one: an instance of a class with such a field, a record, array, Map or Set, or a value typed as a base class. A dynamic view reads the field as a new array each time, so a class whose instances can become dynamic values cannot hold an InlineArray field. Printing, `JSON.stringify` without a replacer, `publish` and a worker's `workerData` only read or copy the value and are allowed. The report names the conversion.
|
||||
|
||||
scriptc reports a field that does not keep these rules (SC7009).
|
||||
|
||||
### Reads
|
||||
|
||||
Indexing the field, reading its `length` and iterating it with `for...of` read the elements from the instance, directly or through a `const` local used only in those ways. Any other use, such as passing the field to a function or calling one of its methods, gets an array of the elements, which is frozen in scriptc builds. Under Node.js the field holds the array the constructor stored. The two agree because the field rules keep that array from being written.
|
||||
|
||||
### Inline array cost
|
||||
|
||||
Each element takes 4 bytes in the instance, and the field's own slot holds the count. The constructor copies the elements from the array it is passed, so that array can be released. The elements start at the same offset in every class of the hierarchy, after the largest set of fixed fields. A read that gets an array allocates a small header that refers to the instance's elements, and frees it when the read's value is released.
|
||||
|
||||
The elements stay in an ordinary array object, and the field behaves like any other array field, in builds with sanitizers, in programs that use [`LifetimeRegion`](#lifetimeregion), and on platforms other than 64-bit Linux.
|
||||
|
||||
+16
@@ -316,6 +316,22 @@
|
||||
{
|
||||
"text": "Slot cost",
|
||||
"id": "slot-cost"
|
||||
},
|
||||
{
|
||||
"text": "InlineArray",
|
||||
"id": "inlinearray"
|
||||
},
|
||||
{
|
||||
"text": "Field rules",
|
||||
"id": "field-rules"
|
||||
},
|
||||
{
|
||||
"text": "Reads",
|
||||
"id": "reads"
|
||||
},
|
||||
{
|
||||
"text": "Inline array cost",
|
||||
"id": "inline-array-cost"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -43,6 +43,7 @@ import { llvmCommentText } from "./common.js";
|
||||
import { INLINE_UNION_FIELD } from "./inline-unions.js";
|
||||
import { NullableRefFields } from "./nullable-fields.js";
|
||||
import type { Int32Slots } from "../../ir/int32-slots.js";
|
||||
import type { InlineArrayClass } from "../../ir/inline-arrays.js";
|
||||
import {
|
||||
cycleRootLines,
|
||||
traceFieldVisitLines,
|
||||
@@ -117,12 +118,30 @@ export class LlClassMeta {
|
||||
* a compact class instance as a 32-bit index (COMPACT_REF) rather than a
|
||||
* pointer: compactRefLoadLines decodes it, compactRefStoreLines encodes it. */
|
||||
compressed: boolean[] = [];
|
||||
/** Compact classes with an inline array field (ir/inline-arrays.ts): the
|
||||
* field's slot holds the element count as an i32, and the elements follow
|
||||
* the fixed fields as 32-bit references from `tailOffset`, the same offset
|
||||
* in every class of the declaring class's subtree. Absent otherwise. */
|
||||
inlineTail?: LlInlineTail;
|
||||
|
||||
constructor(readonly def: IrClassDef) {
|
||||
this.root = this;
|
||||
}
|
||||
}
|
||||
|
||||
/** A class's inline array field (LlClassMeta.inlineTail). */
|
||||
export interface LlInlineTail {
|
||||
/** The field's index in def.fields. */
|
||||
readonly field: number;
|
||||
/** Byte offset of the first element. */
|
||||
readonly tailOffset: number;
|
||||
/** The elements' class. */
|
||||
readonly elemClass: string;
|
||||
/** The index in a `new` expression's args of the array the field is
|
||||
* initialized from, whose length the allocation reads. */
|
||||
readonly lengthArg: number;
|
||||
}
|
||||
|
||||
/** The class graph: link base/children, number the forest in preorder
|
||||
* (roots and children in module class order, matching the
|
||||
* runtime's stamped error vtables), and compute each hierarchy's virtual
|
||||
@@ -451,6 +470,12 @@ export function classFieldIndex(
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: unknown field ${field} on class ${meta.def.name}`,
|
||||
);
|
||||
// An inline array field's slot holds its element count: its reads and
|
||||
// writes go through inline-arrays.ts.
|
||||
if (meta.inlineTail?.field === idx && meta.compact)
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: field slot of the inline array field ${meta.def.name}.${field}`,
|
||||
);
|
||||
const compressed = meta.compressed[idx] === true;
|
||||
// Callers that read or write a field's storage as a pointer must not see
|
||||
// a compressed one; callers that decode and encode pass "compressed".
|
||||
@@ -783,6 +808,7 @@ export function assignCompactLayouts(
|
||||
field: number,
|
||||
isCompact: (className: string) => boolean,
|
||||
) => boolean,
|
||||
inlineArrays?: ReadonlyMap<string, InlineArrayClass>,
|
||||
): void {
|
||||
const isCompact = (className: string): boolean => {
|
||||
const target = metaMap.get(className);
|
||||
@@ -814,7 +840,18 @@ export function assignCompactLayouts(
|
||||
? base!.compressed[i]!
|
||||
: compressedRef !== undefined && compressedRef(meta.def, i, isCompact),
|
||||
);
|
||||
// An inline array field (ir/inline-arrays.ts) whose elements are compact
|
||||
// keeps its element count in its slot.
|
||||
const inline = inlineArrays?.get(meta.def.name);
|
||||
const inlineField =
|
||||
inline !== undefined && isCompact(inline.elemClass) && isCompact(inline.declaringClass)
|
||||
? inline.fieldIndex
|
||||
: -1;
|
||||
const types = fieldTypes(meta.def).map((t, i) => {
|
||||
if (i === inlineField) {
|
||||
compressed[i] = false;
|
||||
return "i32";
|
||||
}
|
||||
if (!compressed[i]) return t;
|
||||
if (t !== "ptr")
|
||||
throw new InternalCompilerError(
|
||||
@@ -862,6 +899,44 @@ export function assignCompactLayouts(
|
||||
meta.compactSlots = slots;
|
||||
meta.compressed = compressed;
|
||||
}
|
||||
if (inlineArrays !== undefined) assignInlineTails(metaMap, inlineArrays);
|
||||
}
|
||||
|
||||
/** Gives each compact class of an inline array field's subtree its tail
|
||||
* (LlClassMeta.inlineTail): the elements start after the largest fixed part
|
||||
* of any class in the subtree, so that a read through any static type of
|
||||
* the subtree finds them at the same offset. */
|
||||
function assignInlineTails(
|
||||
metaMap: ReadonlyMap<string, LlClassMeta>,
|
||||
inlineArrays: ReadonlyMap<string, InlineArrayClass>,
|
||||
): void {
|
||||
const subtrees = new Map<string, LlClassMeta[]>();
|
||||
for (const [name, plan] of inlineArrays) {
|
||||
const meta = metaMap.get(name);
|
||||
if (meta === undefined || !meta.compact) continue;
|
||||
// The layout gave the field its i32 count only when the elements are
|
||||
// compact (assignCompactLayouts).
|
||||
const slot = meta.compactSlots[plan.fieldIndex];
|
||||
if (slot === undefined || meta.compactElems[slot]?.type !== "i32") continue;
|
||||
const members = subtrees.get(plan.declaringClass);
|
||||
if (members === undefined) subtrees.set(plan.declaringClass, [meta]);
|
||||
else members.push(meta);
|
||||
}
|
||||
for (const members of subtrees.values()) {
|
||||
let end = 0;
|
||||
for (const meta of members)
|
||||
for (const e of meta.compactElems) end = Math.max(end, e.offset + e.size);
|
||||
const tailOffset = alignTo(end, 4);
|
||||
for (const meta of members) {
|
||||
const plan = inlineArrays.get(meta.def.name)!;
|
||||
meta.inlineTail = {
|
||||
field: plan.fieldIndex,
|
||||
tailOffset,
|
||||
elemClass: plan.elemClass,
|
||||
lengthArg: plan.lengthArg,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function alignTo(n: number, align: number): number {
|
||||
@@ -901,6 +976,8 @@ export function classUndefinedFieldInits(
|
||||
if (f.name === "%cause") return;
|
||||
// A nullable-pointer field's zeroed slot already is its unit arm.
|
||||
if (nullable?.get(meta.def.name, f.name)) return;
|
||||
// An inline array field's slot holds its count, which `new` stores.
|
||||
if (meta.inlineTail?.field === i && meta.compact) return;
|
||||
// Only union and dyn fields get a store here; a compressed field is a
|
||||
// class reference, which starts NULL.
|
||||
const { index, compressed } = classFieldIndex(meta, f.name, "compressed");
|
||||
@@ -1421,8 +1498,12 @@ export function emitClassShapes(
|
||||
// members, undefined-admitting union fields at the interned unit
|
||||
// instance. Traced shapes allocate with the collector header; alloc.ts
|
||||
// inlines the allocator fast paths and the alloc note.
|
||||
// An inline array field's instance (LlClassMeta.inlineTail) takes the
|
||||
// field's element count and has room for the elements after its fixed
|
||||
// fields.
|
||||
const tail = meta.compact && regionRefs ? (meta.inlineTail ?? null) : null;
|
||||
const nw: string[] = [
|
||||
`define internal ptr @${mangleClassNew(cls.name)}() ${FN_ATTRS} { ; new ${cls.name}`,
|
||||
`define internal ptr @${mangleClassNew(cls.name)}(${tail !== null ? "i32 %inl.len" : ""}) ${FN_ATTRS} { ; new ${cls.name}`,
|
||||
`entry:`,
|
||||
];
|
||||
const cycType = `@${mangleClassTrace(cls.name)}.cyctype`;
|
||||
@@ -1450,7 +1531,14 @@ export function emitClassShapes(
|
||||
// Compressed fields refer to compact instances by their index in the
|
||||
// program-lifetime region, so every compact instance lives there:
|
||||
// zeroed, 8-byte aligned and never freed.
|
||||
nw.push(...plrAllocLines(host, sizeOf));
|
||||
if (tail !== null) {
|
||||
nw.push(
|
||||
` %inl.n = zext i32 %inl.len to ${host.sizeType}`,
|
||||
` %inl.bytes = shl nuw nsw ${host.sizeType} %inl.n, 2`,
|
||||
` %inl.size = add nuw nsw ${host.sizeType} %inl.bytes, ${tail.tailOffset}`,
|
||||
);
|
||||
nw.push(...plrAllocLines(host, "%inl.size"));
|
||||
} else nw.push(...plrAllocLines(host, sizeOf));
|
||||
} else
|
||||
nw.push(
|
||||
...emitObjectAlloc(
|
||||
@@ -1527,6 +1615,11 @@ export function emitClassShapes(
|
||||
);
|
||||
}
|
||||
nw.push(...undefFieldInits(host, meta, nullable));
|
||||
if (tail !== null)
|
||||
nw.push(
|
||||
` %inl.countp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${meta.compactSlots[tail.field]!}`,
|
||||
` store i32 %inl.len, ptr %inl.countp ; ${llvmCommentText(cls.name)} inline array count`,
|
||||
);
|
||||
// Young instances: program-owned traced classes. Classes rooted at a
|
||||
// runtime base (errors, emitters, events, streams) carry runtime state
|
||||
// and stay with trial deletion, like every runtime family.
|
||||
@@ -1678,6 +1771,10 @@ export function emitClassObjDefs(
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: class object for ${className} without a constructor`,
|
||||
);
|
||||
if (meta.inlineTail !== undefined)
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: class object for ${className}, which has an inline array field`,
|
||||
);
|
||||
const params = ctor.params.slice(valueCtor.firstArg);
|
||||
const paramDecls = params.map((p, i) => `${llType(p.type)} %a${i}`).join(", ");
|
||||
const ctorArgs = params.map((p, i) => `${llType(p.type)} %a${i}`);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* LlEmitter's statement emission: the statement switch, try/catch, switch, loop copies, speculative writes and string self-concatenation. */
|
||||
import { emitInlineArrayInit, inlineFieldTail } from "./inline-arrays.js";
|
||||
import { emitLoopBytesHeaders } from "./bytes-headers.js";
|
||||
import { noteBytesReceiver } from "./expr-bytes.js";
|
||||
import {
|
||||
@@ -620,6 +621,25 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
// release — a release can trigger a cycle collection, which must
|
||||
// never see a heap edge whose count was already given up).
|
||||
// Classes and records share the struct layout, so one emission.
|
||||
// The constructor's write of an inline array field copies the array's
|
||||
// elements into the instance (backend/llvm/inline-arrays.ts).
|
||||
const tail =
|
||||
s.kind === "fieldSet" ? inlineFieldTail(emitter, s.className, s.field) : undefined;
|
||||
if (tail !== undefined && s.kind === "fieldSet") {
|
||||
const obj = emitter.emitStableReceiver(s.obj, [s.value]);
|
||||
const v = emitter.emitExpr(s.value);
|
||||
emitter.emitStoreGuard(obj.name, s);
|
||||
emitter.heapCensus?.countStore(
|
||||
(line) => B.line(line),
|
||||
() => B.tmp(),
|
||||
s.className,
|
||||
s.field,
|
||||
s.value,
|
||||
v.name,
|
||||
);
|
||||
emitInlineArrayInit(emitter, obj.name, s.className, tail, v.name);
|
||||
break;
|
||||
}
|
||||
const nullable =
|
||||
s.kind === "fieldSet" ? emitter.nullableFields.get(s.className, s.field) : null;
|
||||
if (nullable && s.kind === "fieldSet") {
|
||||
|
||||
@@ -88,6 +88,13 @@ import {
|
||||
import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
|
||||
import { versionLoopBoundParameters } from "../../ir/parameter-versions.js";
|
||||
import { inlineExpressionFunctions } from "../../ir/expression-inlining.js";
|
||||
import {
|
||||
inlineArrayReads,
|
||||
NO_INLINE_ARRAYS,
|
||||
planInlineArrays,
|
||||
type InlineArrayPlan,
|
||||
} from "../../ir/inline-arrays.js";
|
||||
import { inlineArraysApply, programLifetimeClassOf } from "./inline-arrays.js";
|
||||
import { pairRowSums } from "../../ir/row-pairs.js";
|
||||
import {
|
||||
allocatedLiteral,
|
||||
@@ -353,6 +360,7 @@ import {
|
||||
classStructSym,
|
||||
programLifetimeClass,
|
||||
type LlClassMeta,
|
||||
type LlInlineTail,
|
||||
} from "./classes.js";
|
||||
import { LifetimeRegions, REGION_TYPES } from "./regions.js";
|
||||
import {
|
||||
@@ -695,6 +703,13 @@ function resultEntryTargets(mod: IrModule): ReadonlySet<string> {
|
||||
return targets;
|
||||
}
|
||||
|
||||
/** The reads and constructions of inline array fields prepared for their
|
||||
* layout (ir/inline-arrays.ts), where the target keeps them inline. */
|
||||
function inlineArraysPass(options: LlvmTargetOptions, mod: IrModule): IrModule {
|
||||
if (!inlineArraysApply(mod, options)) return mod;
|
||||
return inlineArrayReads(mod, planInlineArrays(mod, programLifetimeClassOf(mod)));
|
||||
}
|
||||
|
||||
/** The emitter for `input` after the module passes every emission runs. */
|
||||
function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
// Facts are keyed by node identity: emit the same module whether it was
|
||||
@@ -740,14 +755,17 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
pairRowSums(
|
||||
inlineExpressionFunctions(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
inlineArraysPass(
|
||||
options,
|
||||
inlineExpressionFunctions(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1782,6 +1800,9 @@ export class LlEmitter {
|
||||
/** ProgramLifetime hierarchy roots (contract types): their instances are
|
||||
* immortal from construction (classes.ts programLifetimeClass). */
|
||||
readonly programLifetime: ReadonlySet<string>;
|
||||
/** Inline array fields this emission may keep inline (ir/inline-arrays.ts);
|
||||
* the classes that do have LlClassMeta.inlineTail. */
|
||||
readonly inlineArrays: InlineArrayPlan;
|
||||
numericLocals = new Map<string, IrLocal>();
|
||||
captureIds = new Set<string>();
|
||||
/** Active induction bindings retain their width independently of size_t. */
|
||||
@@ -2228,6 +2249,12 @@ export class LlEmitter {
|
||||
this.immutableFields,
|
||||
this.nodeIndex,
|
||||
);
|
||||
// Inline array fields (ir/inline-arrays.ts), where the module passes
|
||||
// prepared their reads and constructions.
|
||||
this.inlineArrays =
|
||||
options.debugSources === undefined && inlineArraysApply(mod, options)
|
||||
? planInlineArrays(mod, programLifetimeClassOf(mod))
|
||||
: NO_INLINE_ARRAYS;
|
||||
const walkParameters = new Map<string, Set<number>>();
|
||||
if (this.debug === null) {
|
||||
const host = {
|
||||
@@ -2569,6 +2596,7 @@ export class LlEmitter {
|
||||
return nullable !== null && isCompact(nullable.arm.className);
|
||||
}
|
||||
: undefined,
|
||||
this.inlineArrays.classes,
|
||||
);
|
||||
this.compactRegion =
|
||||
mod.lib === undefined &&
|
||||
@@ -4793,6 +4821,13 @@ export class LlEmitter {
|
||||
refs: "pointer" | "compressed" = "pointer",
|
||||
): { ptr: string; type: IrType; inline: boolean; compressed: boolean } {
|
||||
const meta = this.classMetaOf(className);
|
||||
// An inline array field's slot holds its element count: its reads and
|
||||
// writes go through backend/llvm/inline-arrays.ts.
|
||||
const tail = this.inlineTailOf(className);
|
||||
if (tail !== undefined && meta.def.fields[tail.field]!.name === field)
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: field pointer to the inline array field ${className}.${field}`,
|
||||
);
|
||||
const { index, type, compressed } = classFieldIndex(meta, field, refs);
|
||||
const p = this.B.tmp();
|
||||
this.B.line(
|
||||
@@ -7232,7 +7267,19 @@ export class LlEmitter {
|
||||
fieldBase: (shapeId) => this.nullableUnions.fieldBase(shapeId),
|
||||
classInfo: (className) => {
|
||||
const meta = this.classMetaOf(className);
|
||||
const tail = this.inlineTailOf(className);
|
||||
const tailField = tail === undefined ? undefined : meta.def.fields[tail.field]!;
|
||||
return {
|
||||
...(tail !== undefined && tailField !== undefined
|
||||
? {
|
||||
tail: {
|
||||
name: tailField.name,
|
||||
countIndex: meta.compactSlots[tail.field]!,
|
||||
offset: tail.tailOffset,
|
||||
elem: { kind: "object" as const, className: tail.elemClass },
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
def: meta.def,
|
||||
hierarchy: meta.hierarchy,
|
||||
pre: meta.pre,
|
||||
@@ -7240,9 +7287,10 @@ export class LlEmitter {
|
||||
runtimeRooted: meta.root.def.runtime === true,
|
||||
idInRc: meta.idInRc,
|
||||
compact: meta.compact,
|
||||
// Inline scalar-union fields are plain bits: nothing to push.
|
||||
fields: meta.def.fields.flatMap((f) =>
|
||||
this.inlineUnions.classField(className, f.name)
|
||||
// Inline scalar-union fields are plain bits: nothing to push. An
|
||||
// inline array field's elements follow the fixed fields (`tail`).
|
||||
fields: meta.def.fields.flatMap((f, i) =>
|
||||
this.inlineUnions.classField(className, f.name) || tail?.field === i
|
||||
? []
|
||||
: [
|
||||
{
|
||||
@@ -7465,6 +7513,13 @@ export class LlEmitter {
|
||||
return emitReadReceiver(this, e);
|
||||
}
|
||||
|
||||
/** The inline array field of `className`'s instances, where they keep it
|
||||
* inline (LlClassMeta.inlineTail; classes.ts gives their `new` the count). */
|
||||
inlineTailOf(className: string): LlInlineTail | undefined {
|
||||
const meta = this.classMeta.get(className);
|
||||
return meta !== undefined && meta.compact && this.compactRegion ? meta.inlineTail : undefined;
|
||||
}
|
||||
|
||||
/** The frame slot a reassigned stack-object local keeps for its value. */
|
||||
stackObjectSlot(localId: string, type: IrType): string {
|
||||
let slot = this.stackObjectSlots.get(localId);
|
||||
@@ -7654,6 +7709,15 @@ export class LlEmitter {
|
||||
return canBorrowReceiver(this, e);
|
||||
}
|
||||
|
||||
/** A read of an inline array field (ir/inline-arrays.ts). It makes a view
|
||||
* of the elements that the read owns: no field holds an array that could
|
||||
* keep a borrowed copy alive. Known before the class layouts are, from
|
||||
* the module's plan; layouts that keep the field an ordinary array only
|
||||
* lose the borrow. */
|
||||
inlineArrayRead(e: IrExpr): boolean {
|
||||
return e.kind === "fieldGet" && this.inlineArrays.classes.get(e.className)?.field === e.field;
|
||||
}
|
||||
|
||||
/** A reference held as one plain pointer that emitReadReceiver can read
|
||||
* without owning: class instances, records, arrays, strings, typed arrays
|
||||
* and nullable-pointer unions. Tagged union boxes, closures and dynamic
|
||||
@@ -7687,7 +7751,7 @@ export class LlEmitter {
|
||||
if (matchMapRead(e, this.boxedUnionsById)) return false;
|
||||
switch (e.kind) {
|
||||
case "fieldGet":
|
||||
return this.nullableFieldGet(e) === null;
|
||||
return this.nullableFieldGet(e) === null && !this.inlineArrayRead(e);
|
||||
case "recordGet":
|
||||
return true;
|
||||
// A keyed read whose hits are all the result's own pointer.
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
ffiNativeTypeLl,
|
||||
ffiNativeParamLl,
|
||||
ffiNativeReturnLl,
|
||||
SCR_ARR_FIELD,
|
||||
} from "./common.js";
|
||||
import { canStackUnion, emitStackUnion } from "./stack-unions.js";
|
||||
import { emitStackRecordKeyGet, scalarRecordKeyGet } from "./expr-records.js";
|
||||
@@ -1121,8 +1122,28 @@ export function emitCallExpr(
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: new ${e.className} without a constructor`,
|
||||
);
|
||||
// An instance with an inline array field is allocated with room for
|
||||
// the elements of the array its constructor writes the field from
|
||||
// (ir/inline-arrays.ts made that argument a local).
|
||||
const tail = host.inlineTailOf(e.className);
|
||||
let count = "";
|
||||
if (tail !== undefined) {
|
||||
const source = e.args[tail.lengthArg];
|
||||
if (source?.kind !== "varRef")
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: new ${e.className} reads its inline array from an expression`,
|
||||
);
|
||||
const arr = host.emitReadReceiver(source);
|
||||
const lenp = B.tmp();
|
||||
const len = B.tmp();
|
||||
B.line(
|
||||
`${lenp} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 ${SCR_ARR_FIELD.len}`,
|
||||
);
|
||||
B.line(`${len} = load i32, ptr ${lenp}`);
|
||||
count = `i32 ${len}`;
|
||||
}
|
||||
const o = B.tmp();
|
||||
B.line(`${o} = call ptr @${host.allocSym(mangleClassNew(e.className), e)}()`);
|
||||
B.line(`${o} = call ptr @${host.allocSym(mangleClassNew(e.className), e)}(${count})`);
|
||||
host.heapCensus?.countNew((line) => B.line(line), e.className);
|
||||
const out = host.own({ name: o, type: e.type });
|
||||
emitConstruction(host, e, o, ctor);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { dynDesc } from "../../ir/analysis.js";
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { emitInlineArrayLength, inlineFieldTail } from "./inline-arrays.js";
|
||||
import { arraySearchHelperName, arraySearchKind } from "./array-search.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { isStableReceiverOperand, undefinedArmTag } from "../../ir/analysis.js";
|
||||
@@ -650,6 +651,14 @@ export function emitArrIntrinsic(
|
||||
into?: { box: string; tag: string; payload: string },
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
// The length of an inline array field, read from the instance.
|
||||
if (e.method === "length" && e.receiver.kind === "fieldGet") {
|
||||
const tail = inlineFieldTail(host, e.receiver.className, e.receiver.field);
|
||||
if (tail !== undefined) {
|
||||
const obj = host.emitStableReceiver(e.receiver.obj, e.args);
|
||||
return emitInlineArrayLength(host, obj.name, e.receiver.className, tail, e.type);
|
||||
}
|
||||
}
|
||||
// Every array intrinsic borrows its receiver, including mutation and
|
||||
// retained-identity results. Scalar reads also admit the narrower proof
|
||||
// that a mutable binding survives the remaining operand evaluations.
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import {
|
||||
emitInlineArrayGet,
|
||||
emitInlineArrayState,
|
||||
emitInlineArrayView,
|
||||
inlineFieldTail,
|
||||
} from "./inline-arrays.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { isRefCounted, type IrExpr, type IrStrCmpOp, type IrType } from "../../ir/ir.js";
|
||||
@@ -1181,6 +1187,26 @@ export function emitContainerExpr(
|
||||
return out;
|
||||
}
|
||||
case "arrayGet": {
|
||||
// An element of an inline array field, read from the instance.
|
||||
if (e.arr.kind === "fieldGet" && e.arr.type.kind === "array") {
|
||||
const tail = inlineFieldTail(host, e.arr.className, e.arr.field);
|
||||
if (tail !== undefined) {
|
||||
const obj = host.emitStableReceiver(e.arr.obj, [e.index]);
|
||||
const idx = host.emitExpr(e.index);
|
||||
const value = emitInlineArrayGet(
|
||||
host,
|
||||
obj.name,
|
||||
e.arr.className,
|
||||
tail,
|
||||
idx,
|
||||
e.arr.type.elem,
|
||||
true,
|
||||
);
|
||||
return isRefCounted(e.arr.type.elem)
|
||||
? host.own({ name: value, type: e.type })
|
||||
: { name: value, type: e.type };
|
||||
}
|
||||
}
|
||||
// Element reads run no user code: a projection receiver (an array in
|
||||
// a parameter's field) stays rooted while the index preserves edges.
|
||||
const arr = host.emitStableReceiver(e.arr, [e.index]);
|
||||
@@ -1233,6 +1259,16 @@ export function emitContainerExpr(
|
||||
return host.own({ name: read.value, type: e.type });
|
||||
}
|
||||
case "arrayHas": {
|
||||
// An inline array field has no holes: presence is the bounds check.
|
||||
if (e.arr.kind === "fieldGet") {
|
||||
const tail = inlineFieldTail(host, e.arr.className, e.arr.field);
|
||||
if (tail !== undefined) {
|
||||
const obj = host.emitStableReceiver(e.arr.obj, [e.index]);
|
||||
const idx = host.emitExpr(e.index);
|
||||
const name = emitInlineArrayState(host, obj.name, e.arr.className, tail, idx, "has");
|
||||
return { name, type: e.type };
|
||||
}
|
||||
}
|
||||
const arr = host.emitStableReceiver(e.arr, [e.index]);
|
||||
const idx = host.emitExpr(e.index);
|
||||
if (e.arr.type.kind !== "array")
|
||||
@@ -1252,6 +1288,16 @@ export function emitContainerExpr(
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
case "arrayState": {
|
||||
// An inline array field has no holes: presence is the bounds check.
|
||||
if (e.arr.kind === "fieldGet") {
|
||||
const tail = inlineFieldTail(host, e.arr.className, e.arr.field);
|
||||
if (tail !== undefined) {
|
||||
const obj = host.emitStableReceiver(e.arr.obj, [e.index]);
|
||||
const idx = host.emitExpr(e.index);
|
||||
const name = emitInlineArrayState(host, obj.name, e.arr.className, tail, idx, "state");
|
||||
return { name, type: e.type };
|
||||
}
|
||||
}
|
||||
const arr = host.emitStableReceiver(e.arr, [e.index]);
|
||||
const idx = host.emitExpr(e.index);
|
||||
if (e.arr.type.kind !== "array")
|
||||
@@ -1356,6 +1402,14 @@ export function emitRecordExpr(
|
||||
switch (e.kind) {
|
||||
case "fieldGet": {
|
||||
const obj = host.emitReadReceiver(e.obj);
|
||||
// An inline array field read as a value: a frozen array over the
|
||||
// elements (backend/llvm/inline-arrays.ts).
|
||||
const tail = inlineFieldTail(host, e.className, e.field);
|
||||
if (tail !== undefined)
|
||||
return host.own({
|
||||
name: emitInlineArrayView(host, obj.name, e.className, tail),
|
||||
type: e.type,
|
||||
});
|
||||
const { ptr, type, inline } = host.classFieldPtr(
|
||||
obj.name,
|
||||
e.className,
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
import { SCR_ARR_FIELD, llvmCommentText } from "./common.js";
|
||||
import {
|
||||
FN_ATTRS,
|
||||
arrOps,
|
||||
elemAccess,
|
||||
llFieldType,
|
||||
loadStructField,
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
type ShapeHost,
|
||||
} from "./shapes.js";
|
||||
import type { LlvmEmitterContext, LlStreamTypedRefAdapter } from "./expr-context.js";
|
||||
import { inlineFieldTail } from "./inline-arrays.js";
|
||||
import type { NullableRefField } from "./nullable-fields.js";
|
||||
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
||||
import { inlineUnionCopyLines } from "./inline-unions.js";
|
||||
@@ -1703,6 +1705,27 @@ function classFieldDynValue(
|
||||
t: Extract<IrType, { kind: "object" }>,
|
||||
field: LlClassMeta["def"]["fields"][number],
|
||||
): { boxed: string; nullable: NullableRefField | null; fieldValue: string } {
|
||||
// An inline array field reads as a frozen array over the instance's
|
||||
// elements (inline-arrays.ts), which the box keeps.
|
||||
const tail = inlineFieldTail(host, t.className, field.name);
|
||||
if (tail !== undefined) {
|
||||
const countp = B.tmp(),
|
||||
count = B.tmp(),
|
||||
at = B.tmp(),
|
||||
view = B.tmp();
|
||||
B.line(
|
||||
`${countp} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${meta.compactSlots[tail.field]!}`,
|
||||
);
|
||||
B.line(`${count} = load i32, ptr ${countp}`);
|
||||
B.line(`${at} = getelementptr inbounds i8, ptr %p, i64 ${tail.tailOffset}`);
|
||||
const ops = arrOps(host.shapeHost, { kind: "object", className: tail.elemClass });
|
||||
host.declare(`declare ptr @scr_arr_view_cref(ptr, ptr, i32)`);
|
||||
host.declare(`declare void @scr_arr_release(ptr)`);
|
||||
B.line(`${view} = call ptr @scr_arr_view_cref(ptr ${ops}, ptr ${at}, i32 ${count})`);
|
||||
const boxed = host.streamTypedRefBoxValue(B, field.type, view);
|
||||
B.line(`call void @scr_arr_release(ptr ${view})`);
|
||||
return { boxed, nullable: null, fieldValue: view };
|
||||
}
|
||||
const { index, compressed } = classFieldIndex(meta, field.name, "compressed");
|
||||
const fieldPtr = B.tmp();
|
||||
let fieldValue = B.tmp();
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
/* Inline array fields in the LLVM backend (ir/inline-arrays.ts plans them).
|
||||
*
|
||||
* A compact class with such a field (LlClassMeta.inlineTail) keeps the
|
||||
* field's element count as an i32 in the field's slot and the elements as
|
||||
* 32-bit region references after its fixed fields, at the subtree's tail
|
||||
* offset. `new` reads the count from the constructor argument the field is
|
||||
* written from and allocates the instance with room for them; the
|
||||
* constructor's write copies the argument's elements there
|
||||
* (scr_arr_copy_cref_tail). Reads that index the field, read its length or
|
||||
* iterate it load the elements directly; any other read gives a frozen array
|
||||
* over them (scr_arr_view_cref). */
|
||||
|
||||
import { isRefCounted, type IrModule, type IrType } from "../../ir/ir.js";
|
||||
import { mangleClassStruct } from "../mangle.js";
|
||||
import { compactRefLoadLines, type LlInlineTail } from "./classes.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import type { LlvmTargetOptions } from "./emitter.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { arrOps } from "./shapes.js";
|
||||
|
||||
/** Whether `mod` emitted with `options` can keep inline array fields inline:
|
||||
* an executable for a 64-bit target whose compact instances live in the low
|
||||
* region (32-bit references), without lifetime regions (which keep their
|
||||
* own class layouts), and with fields to keep. */
|
||||
export function inlineArraysApply(mod: IrModule, options: LlvmTargetOptions): boolean {
|
||||
return (
|
||||
(mod.contracts?.inlineArrays?.length ?? 0) > 0 &&
|
||||
options.pointerBits !== 32 &&
|
||||
options.lowRegion === true &&
|
||||
mod.lib === undefined &&
|
||||
mod.lifetimeRegions !== true
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether instances of `className` are ProgramLifetime instances: the
|
||||
* root of its hierarchy is declared `implements ProgramLifetime`. */
|
||||
export function programLifetimeClassOf(mod: IrModule): (className: string) => boolean {
|
||||
const roots = new Set(mod.contracts?.programLifetime ?? []);
|
||||
const bases = new Map<string, string | undefined>();
|
||||
for (const cls of mod.classes ?? []) bases.set(cls.name, cls.base);
|
||||
return (className) => {
|
||||
let name: string | undefined = className;
|
||||
let root = className;
|
||||
while (name !== undefined && bases.has(name)) {
|
||||
root = name;
|
||||
name = bases.get(name);
|
||||
}
|
||||
return bases.has(className) && roots.has(root);
|
||||
};
|
||||
}
|
||||
|
||||
/** The field of `className` whose elements its instances keep inline, when
|
||||
* `field` is it. */
|
||||
export function inlineFieldTail(
|
||||
host: LlvmEmitterContext,
|
||||
className: string,
|
||||
field: string,
|
||||
): LlInlineTail | undefined {
|
||||
const tail = host.inlineTailOf(className);
|
||||
if (tail === undefined) return undefined;
|
||||
return host.classMeta.get(className)!.def.fields[tail.field]!.name === field ? tail : undefined;
|
||||
}
|
||||
|
||||
/** Loads the element count of the inline array field of `obj` (an instance
|
||||
* of `className`) as an i32; returns the value's name. */
|
||||
export function emitInlineCount(
|
||||
host: LlvmEmitterContext,
|
||||
obj: string,
|
||||
className: string,
|
||||
tail: LlInlineTail,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const meta = host.classMeta.get(className)!;
|
||||
const countp = B.tmp();
|
||||
const count = B.tmp();
|
||||
B.line(
|
||||
`${countp} = getelementptr inbounds %${mangleClassStruct(className)}, ptr ${obj}, i64 0, i32 ${meta.compactSlots[tail.field]!}`,
|
||||
);
|
||||
B.line(`${count} = load i32, ptr ${countp}`);
|
||||
return count;
|
||||
}
|
||||
|
||||
/** The address of the first element of the inline array field of `obj`. */
|
||||
export function emitInlineElements(
|
||||
host: LlvmEmitterContext,
|
||||
obj: string,
|
||||
tail: LlInlineTail,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const at = B.tmp();
|
||||
B.line(`${at} = getelementptr inbounds i8, ptr ${obj}, i64 ${tail.tailOffset}`);
|
||||
return at;
|
||||
}
|
||||
|
||||
/** The constructor's write of the inline array field of `obj`: copies the
|
||||
* elements of `value` (an array) into the instance, which `new` allocated
|
||||
* for that array's length. */
|
||||
export function emitInlineArrayInit(
|
||||
host: LlvmEmitterContext,
|
||||
obj: string,
|
||||
className: string,
|
||||
tail: LlInlineTail,
|
||||
value: string,
|
||||
): void {
|
||||
const count = emitInlineCount(host, obj, className, tail);
|
||||
const at = emitInlineElements(host, obj, tail);
|
||||
host.declare(`declare void @scr_arr_copy_cref_tail(ptr, ptr, i32)`);
|
||||
host.B.line(`call void @scr_arr_copy_cref_tail(ptr ${value}, ptr ${at}, i32 ${count})`);
|
||||
}
|
||||
|
||||
/** The inline array field of `obj` as an array value: a frozen array over
|
||||
* the elements (scr_arr_view_cref). Returns the owned (+1) array's name. */
|
||||
export function emitInlineArrayView(
|
||||
host: LlvmEmitterContext,
|
||||
obj: string,
|
||||
className: string,
|
||||
tail: LlInlineTail,
|
||||
): string {
|
||||
const count = emitInlineCount(host, obj, className, tail);
|
||||
const at = emitInlineElements(host, obj, tail);
|
||||
const ops = arrOps(host.shapeHost, { kind: "object", className: tail.elemClass });
|
||||
host.declare(`declare ptr @scr_arr_view_cref(ptr, ptr, i32)`);
|
||||
const view = host.B.tmp();
|
||||
host.B.line(`${view} = call ptr @scr_arr_view_cref(ptr ${ops}, ptr ${at}, i32 ${count})`);
|
||||
return view;
|
||||
}
|
||||
|
||||
/** The index as an i64 slot, and whether the number is an integer below
|
||||
* `count` (an i32). */
|
||||
function emitInlineIndex(
|
||||
host: LlvmEmitterContext,
|
||||
count: string,
|
||||
index: LlValue,
|
||||
): { slot: string; within: string } {
|
||||
const B = host.B;
|
||||
const bound = B.tmp();
|
||||
B.line(`${bound} = zext i32 ${count} to i64`);
|
||||
const integer = index.integer;
|
||||
if (integer !== undefined) {
|
||||
let slot = integer.name;
|
||||
if (integer.type === "i32") {
|
||||
slot = B.tmp();
|
||||
B.line(`${slot} = ${integer.signed ? "sext" : "zext"} i32 ${integer.name} to i64`);
|
||||
}
|
||||
const within = B.tmp();
|
||||
B.line(`${within} = icmp ult i64 ${slot}, ${bound}`);
|
||||
return { slot, within };
|
||||
}
|
||||
host.declare(`declare i64 @llvm.fptosi.sat.i64.f64(double)`);
|
||||
const truncated = B.tmp();
|
||||
const back = B.tmp();
|
||||
const exact = B.tmp();
|
||||
const below = B.tmp();
|
||||
const within = B.tmp();
|
||||
B.line(`${truncated} = call i64 @llvm.fptosi.sat.i64.f64(double ${index.name})`);
|
||||
B.line(`${back} = sitofp i64 ${truncated} to double`);
|
||||
B.line(`${exact} = fcmp oeq double ${back}, ${index.name}`);
|
||||
B.line(`${below} = icmp ult i64 ${truncated}, ${bound}`);
|
||||
B.line(`${within} = and i1 ${exact}, ${below}`);
|
||||
return { slot: truncated, within };
|
||||
}
|
||||
|
||||
/** `index in obj.field` (`has`, an i1) or the slot state (`state`, a double:
|
||||
* HOLE=0, VALUE=1) for an inline array field. Its elements are instances,
|
||||
* never holes or undefined, so an index is present exactly when it is an
|
||||
* integer within the count. */
|
||||
export function emitInlineArrayState(
|
||||
host: LlvmEmitterContext,
|
||||
obj: string,
|
||||
className: string,
|
||||
tail: LlInlineTail,
|
||||
index: LlValue,
|
||||
kind: "has" | "state",
|
||||
): string {
|
||||
const count = emitInlineCount(host, obj, className, tail);
|
||||
const { within } = emitInlineIndex(host, count, index);
|
||||
if (kind === "has") return within;
|
||||
const state = host.B.tmp();
|
||||
host.B.line(`${state} = select i1 ${within}, double ${f64Lit(1)}, double ${f64Lit(0)}`);
|
||||
return state;
|
||||
}
|
||||
|
||||
/** `obj.field[index]` for an inline array field: the element at an index
|
||||
* within the count, loaded from the instance. Any other index takes the
|
||||
* runtime getter over a view, which reports it like a read of an ordinary
|
||||
* array. With `retain`, the result is an owned reference when the element
|
||||
* type is reference counted (like emitDenseArrayRead); without, borrowed
|
||||
* from the instance. */
|
||||
export function emitInlineArrayGet(
|
||||
host: LlvmEmitterContext,
|
||||
obj: string,
|
||||
className: string,
|
||||
tail: LlInlineTail,
|
||||
index: LlValue,
|
||||
element: IrType,
|
||||
retain: boolean,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const count = emitInlineCount(host, obj, className, tail);
|
||||
const at = emitInlineElements(host, obj, tail);
|
||||
const { slot, within } = emitInlineIndex(host, count, index);
|
||||
const hit = B.newLabel("inl.get.hit");
|
||||
const miss = B.newLabel("inl.get.miss");
|
||||
const join = B.newLabel("inl.get.join");
|
||||
B.condBr(within, hit, miss);
|
||||
B.startBlock(hit);
|
||||
const p = B.tmp();
|
||||
const ref = B.tmp();
|
||||
const raw = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds i32, ptr ${at}, i64 ${slot}`);
|
||||
B.line(`${ref} = load i32, ptr ${p}`);
|
||||
for (const line of compactRefLoadLines(ref, raw, `${raw}.r`)) B.line(line.trimStart());
|
||||
const value = retain && isRefCounted(element) ? host.retainFrame(raw, element) : raw;
|
||||
const hitEnd = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(miss);
|
||||
// Outside the count: the getter of an ordinary array over the elements
|
||||
// decides, as for any array (an out-of-bounds read traps).
|
||||
const ops = arrOps(host.shapeHost, { kind: "object", className: tail.elemClass });
|
||||
const getter = retain ? "scr_arr_get_ref" : "scr_arr_borrow_ref";
|
||||
host.declare(`declare ptr @scr_arr_view_cref(ptr, ptr, i32)`);
|
||||
host.declare(`declare ptr @${getter}(ptr, double)`);
|
||||
host.declare(`declare void @scr_arr_release(ptr)`);
|
||||
const view = B.tmp();
|
||||
const slow = B.tmp();
|
||||
B.line(`${view} = call ptr @scr_arr_view_cref(ptr ${ops}, ptr ${at}, i32 ${count})`);
|
||||
B.line(`${slow} = call ptr @${getter}(ptr ${view}, double ${indexDouble(host, index)}) cold`);
|
||||
B.line(`call void @scr_arr_release(ptr ${view})`);
|
||||
const missEnd = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = phi ptr [ ${value}, %${hitEnd} ], [ ${slow}, %${missEnd} ]`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The index as a double (an integer snapshot's own double is its name). */
|
||||
function indexDouble(host: LlvmEmitterContext, index: LlValue): string {
|
||||
if (index.type.kind === "f64") return index.name;
|
||||
const d = host.B.tmp();
|
||||
host.B.line(`${d} = sitofp i32 ${index.name} to double`);
|
||||
return d;
|
||||
}
|
||||
|
||||
/** `obj.field.length` for an inline array field. */
|
||||
export function emitInlineArrayLength(
|
||||
host: LlvmEmitterContext,
|
||||
obj: string,
|
||||
className: string,
|
||||
tail: LlInlineTail,
|
||||
type: IrType,
|
||||
): LlValue {
|
||||
const count = emitInlineCount(host, obj, className, tail);
|
||||
const length = host.B.tmp();
|
||||
host.B.line(`${length} = uitofp i32 ${count} to double`);
|
||||
return { name: length, type, uint32: count };
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type IrType,
|
||||
type IrUnionDef,
|
||||
} from "../../ir/ir.js";
|
||||
import { emitInlineArrayGet, inlineFieldTail } from "./inline-arrays.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
@@ -607,6 +608,25 @@ export function emitBorrowedArrayRead(
|
||||
slot: string,
|
||||
): void {
|
||||
const B = host.B;
|
||||
// An element of an inline array field, borrowed from the instance.
|
||||
if (read.arr.kind === "fieldGet" && read.arr.type.kind === "array") {
|
||||
const tail = inlineFieldTail(host, read.arr.className, read.arr.field);
|
||||
if (tail !== undefined) {
|
||||
const obj = host.emitStableReceiver(read.arr.obj, [read.index]);
|
||||
const index = host.emitExpr(read.index);
|
||||
const value = emitInlineArrayGet(
|
||||
host,
|
||||
obj.name,
|
||||
read.arr.className,
|
||||
tail,
|
||||
index,
|
||||
read.arr.type.elem,
|
||||
false,
|
||||
);
|
||||
B.line(`store ptr ${value}, ptr ${slot}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const array = host.emitStableReceiver(read.arr, [read.index]);
|
||||
const loopIndex = host.emitIntegerLoopIndex(read.index);
|
||||
if (loopIndex !== null && host.provenDenseIndex(read.arr, read.index)) {
|
||||
|
||||
@@ -80,6 +80,10 @@ export interface PublishClassInfo {
|
||||
* `compressed` when the field holds a 32-bit reference (classes.ts
|
||||
* LlClassMeta.compressed), which the walker decodes. */
|
||||
fields: { name: string; index: number; type: IrType; compressed?: boolean }[];
|
||||
/** An inline array field (classes.ts LlClassMeta.inlineTail), which
|
||||
* `fields` leaves out: the GEP index of its i32 element count, the byte
|
||||
* offset of its 32-bit references and their type. */
|
||||
tail?: { name: string; countIndex: number; offset: number; elem: IrType };
|
||||
}
|
||||
|
||||
/** scr_pub_mark's object kinds (SCR_PUB_KIND_* in scr_runtime.h), for the RC audit. */
|
||||
@@ -406,6 +410,7 @@ export class PublishWalkers {
|
||||
loc,
|
||||
info.idInRc,
|
||||
info.compact,
|
||||
info.tail,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -564,6 +569,7 @@ export class PublishWalkers {
|
||||
loc: SrcLoc,
|
||||
classId = false,
|
||||
compact = false,
|
||||
tail?: PublishClassInfo["tail"],
|
||||
): string[] {
|
||||
const lines = [
|
||||
head,
|
||||
@@ -592,7 +598,33 @@ export class PublishWalkers {
|
||||
);
|
||||
n++;
|
||||
}
|
||||
lines.push(` br label %done`, `done:`, ` ret void`, `}`);
|
||||
const tailFn =
|
||||
tail === undefined ? null : this.walker(tail.elem, `${path}.${tail.name}[]`, loc);
|
||||
if (tail !== undefined && tailFn !== null) {
|
||||
// Each element of an inline array field, as a 32-bit reference.
|
||||
lines.push(
|
||||
` br label %tail.pre`,
|
||||
`tail.pre:`,
|
||||
` %tail.cp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${tail.countIndex} ; ${llvmComment(tail.name)}`,
|
||||
` %tail.n = load i32, ptr %tail.cp`,
|
||||
` %tail.at = getelementptr inbounds i8, ptr %o, i64 ${tail.offset}`,
|
||||
` br label %tail.head`,
|
||||
`tail.head:`,
|
||||
` %tail.i = phi i32 [ 0, %tail.pre ], [ %tail.next, %tail.latch ]`,
|
||||
` %tail.end = icmp uge i32 %tail.i, %tail.n`,
|
||||
` br i1 %tail.end, label %done, label %tail.body`,
|
||||
`tail.body:`,
|
||||
` %tail.p = getelementptr inbounds i32, ptr %tail.at, i32 %tail.i`,
|
||||
` %tail.r = load i32, ptr %tail.p`,
|
||||
...compactRefLoadLines(`%tail.r`, `%tail.v`, `%tail.d`),
|
||||
...this.push(`%tail.v`, tailFn, `tail`, this.compactRef(tail.elem)),
|
||||
` br label %tail.latch`,
|
||||
`tail.latch:`,
|
||||
` %tail.next = add nuw i32 %tail.i, 1`,
|
||||
` br label %tail.head`,
|
||||
);
|
||||
} else lines.push(` br label %done`);
|
||||
lines.push(`done:`, ` ret void`, `}`);
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* LlEmitter's receivers: receiver reads, and the analyses that decide when calls, receivers and returns borrow. */
|
||||
import { inlineFieldTail } from "./inline-arrays.js";
|
||||
import type { LlEmitter } from "./emitter.js";
|
||||
import {
|
||||
type IrExpr,
|
||||
@@ -133,6 +134,9 @@ export function emitReadReceiver(emitter: LlEmitter, e: IrExpr): LlValue {
|
||||
// A nullable-pointer field has no box to borrow. Projections use
|
||||
// emitUnionProjection; other borrowers get an owned heap box.
|
||||
if (emitter.nullableFieldGet(e)) return emitter.emitExpr(e);
|
||||
// An inline array field has no array to borrow: its read makes a view.
|
||||
if (e.kind === "fieldGet" && inlineFieldTail(emitter, e.className, e.field) !== undefined)
|
||||
return emitter.emitExpr(e);
|
||||
if (e.kind === "recordGet" || e.kind === "fieldGet") {
|
||||
const receiver = emitter.emitReadReceiver(e.obj);
|
||||
if (e.kind === "recordGet") {
|
||||
@@ -224,8 +228,9 @@ export function canBorrowCallArgument(emitter: LlEmitter, value: IrExpr): boolea
|
||||
// An exact capsule owns its instance while the capsule's binding holds it.
|
||||
if (value.kind === "dynCheck" && value.exact)
|
||||
return value.value.kind === "varRef" && emitter.canBorrowCallArgument(value.value);
|
||||
// An inline union field has no box to lend.
|
||||
if (emitter.inlineFieldGet(value)) return false;
|
||||
// An inline union field has no box to lend, an inline array field no
|
||||
// array.
|
||||
if (emitter.inlineFieldGet(value) || emitter.inlineArrayRead(value)) return false;
|
||||
// Class casts reinterpret the same pointer (prefix layouts).
|
||||
if ((value.kind === "upcast" || value.kind === "downcast") && value.value.type.kind === "object")
|
||||
return emitter.canBorrowCallArgument(value.value);
|
||||
@@ -308,10 +313,15 @@ export function canBorrowReceiver(emitter: LlEmitter, e: IrExpr): boolean {
|
||||
case "upcast":
|
||||
return emitter.canBorrowReceiver(e.value);
|
||||
// An inline union field has no box to lend: projections read it
|
||||
// through emitReadReceiver's private stack box.
|
||||
// through emitReadReceiver's private stack box. An inline array field
|
||||
// has no array to lend.
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
return !emitter.inlineFieldGet(e) && emitter.canBorrowReceiver(e.obj);
|
||||
return (
|
||||
!emitter.inlineFieldGet(e) &&
|
||||
!emitter.inlineArrayRead(e) &&
|
||||
emitter.canBorrowReceiver(e.obj)
|
||||
);
|
||||
case "ternary":
|
||||
return emitter.canBorrowReceiver(e.then) && emitter.canBorrowReceiver(e.else_);
|
||||
case "seqExpr": {
|
||||
|
||||
@@ -67,6 +67,8 @@ import {
|
||||
type ContractAnnotations,
|
||||
type ContractNote,
|
||||
} from "../frontend/contract-types.js";
|
||||
import { planInlineArrays } from "../ir/inline-arrays.js";
|
||||
import { checkInlineArraySources } from "../ir/inline-array-sources.js";
|
||||
|
||||
/** The largest array index: 2^32 - 2 (ECMA-262 array index). */
|
||||
const MAX_ARRAY_INDEX = 2 ** 32 - 2;
|
||||
@@ -255,6 +257,7 @@ class ContractChecker {
|
||||
|
||||
run(): ContractCheckResult {
|
||||
this.checkProgramLifetime();
|
||||
this.checkInlineArrays();
|
||||
this.checkNoThrow();
|
||||
this.checkAcyclic();
|
||||
const elements = new Int32ElementContracts(this);
|
||||
@@ -323,6 +326,77 @@ class ContractChecker {
|
||||
this.record("programLifetime", roots);
|
||||
}
|
||||
|
||||
/* ── InlineArray ── */
|
||||
|
||||
/** `readonly f: InlineArray<C>` (experimental, ir/inline-arrays.ts): the
|
||||
* field's elements may live in the instance itself, which only a field
|
||||
* written once from a constructor argument and never again can have.
|
||||
* Checked: the class and C are ProgramLifetime classes, the constructor of
|
||||
* the declaring class writes the field once from one of its parameters,
|
||||
* every subclass constructor passes it one of its own through super(...),
|
||||
* nothing else writes the field, no read of it is compared by identity
|
||||
* (two reads need not give the same array), and no instance of the
|
||||
* hierarchy is constructed through a class value or becomes a dynamic
|
||||
* value. */
|
||||
private checkInlineArrays(): void {
|
||||
if (this.notes.inlineArrays.size === 0) return;
|
||||
const roots = new Set(this.contracts.programLifetime ?? []);
|
||||
const lifetime = (className: string): boolean => {
|
||||
let cls = this.classes.get(className);
|
||||
while (cls?.base !== undefined) cls = this.classes.get(cls.base);
|
||||
return cls !== undefined && roots.has(cls.name);
|
||||
};
|
||||
const entries: string[] = [];
|
||||
const notes = new Map<string, ContractNote>();
|
||||
for (const [key, note] of this.notes.inlineArrays) {
|
||||
const at = key.lastIndexOf("\0");
|
||||
const field = key.slice(at + 1);
|
||||
for (const cls of this.classesAt(key.slice(0, at))) {
|
||||
if (!lifetime(cls.name)) {
|
||||
this.report(
|
||||
"SC7009",
|
||||
note.loc,
|
||||
`'${note.name}' is an InlineArray field of a class that is not ProgramLifetime`,
|
||||
"declare the root class of the hierarchy `implements ProgramLifetime`: inline elements live in the instance, which is never freed",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const entry = `${cls.name}\0${field}`;
|
||||
entries.push(entry);
|
||||
notes.set(entry, note);
|
||||
}
|
||||
}
|
||||
if (entries.length === 0) return;
|
||||
const plan = planInlineArrays(
|
||||
{ ...this.mod, contracts: { ...this.mod.contracts, inlineArrays: entries } },
|
||||
lifetime,
|
||||
);
|
||||
for (const r of plan.rejected) {
|
||||
const note = notes.get(`${r.declaringClass}\0${r.field}`)!;
|
||||
const at = r.loc === undefined ? "" : ` (at ${this.where(r.loc)})`;
|
||||
this.report(
|
||||
"SC7009",
|
||||
note.loc,
|
||||
`'${note.name}' cannot keep its InlineArray contract: ${r.reason}${at}`,
|
||||
"write the field once in the constructor from one of its parameters (subclasses pass theirs through super(...)), never again, and keep the class's instances out of class values and dynamic values",
|
||||
);
|
||||
}
|
||||
// Where the arrays come from and who writes them (ir/inline-array-sources.ts).
|
||||
for (const issue of checkInlineArraySources(this.mod, plan)) {
|
||||
const note = notes.get(`${issue.declaringClass}\0${issue.field}`)!;
|
||||
const from = issue.origin === undefined ? "" : ` (from ${this.where(issue.origin)})`;
|
||||
this.report(
|
||||
"SC7009",
|
||||
issue.loc,
|
||||
`'${note.name}' (InlineArray): ${issue.reason}${from}`,
|
||||
"an InlineArray field holds its array for good: build a new array for each constructor call, and never write to an array a field holds",
|
||||
);
|
||||
}
|
||||
const kept = new Set<string>();
|
||||
for (const cls of plan.classes.values()) kept.add(`${cls.declaringClass}\0${cls.field}`);
|
||||
this.record("inlineArrays", [...kept]);
|
||||
}
|
||||
|
||||
/* ── Acyclic ── */
|
||||
|
||||
private checkAcyclic(): void {
|
||||
|
||||
@@ -107,6 +107,7 @@ export const DIAGNOSTIC_CODES = {
|
||||
SC7006: "Acyclic contract in an unchecked position",
|
||||
SC7007: "Acyclic type can lie on a reference cycle",
|
||||
SC7008: "ProgramLifetime class extends another class",
|
||||
SC7009: "InlineArray field contract not kept",
|
||||
SC8001: "package or module runs in a Node island (note)",
|
||||
SC8002: "Node islands off for a package or module outside the native subset",
|
||||
SC8003: "package marked native is outside the native subset",
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
* build checks (SC6103, frontend/shape-inference)
|
||||
* SC7xxx contract types (@scriptc/types): a NoThrow, Int32, Immutable,
|
||||
* Acyclic or ProgramLifetime contract that does not hold or
|
||||
* cannot be checked (SC7001–SC7008, contracts/check.ts)
|
||||
* cannot be checked (SC7001–SC7009, contracts/check.ts)
|
||||
* SC8xxx Node islands: packages and modules outside the native subset
|
||||
* that run (or, in report-only mode, would run) in a Node
|
||||
* island — unknown project configuration (SC8004), a package
|
||||
|
||||
@@ -16,15 +16,33 @@ import { type HeritageElement, isHeritageElement } from "./heritage-elements.js"
|
||||
|
||||
export const CONTRACT_TYPES_MODULE = "@scriptc/types";
|
||||
|
||||
export type ContractMarker = "Int32" | "NoThrow" | "Immutable" | "Acyclic" | "ProgramLifetime";
|
||||
/** The module and name of the inline array field marker, which is
|
||||
* experimental: `readonly f: InlineArray<C>` on an instance field of a
|
||||
* ProgramLifetime class (ir/inline-arrays.ts). */
|
||||
export const INLINE_ARRAY_MODULE = "@scriptc/experimental";
|
||||
export const INLINE_ARRAY_MARKER = "InlineArray";
|
||||
|
||||
export type ContractMarker =
|
||||
| "Int32"
|
||||
| "NoThrow"
|
||||
| "Immutable"
|
||||
| "Acyclic"
|
||||
| "ProgramLifetime"
|
||||
| typeof INLINE_ARRAY_MARKER;
|
||||
const MARKERS: ReadonlySet<string> = new Set([
|
||||
"Int32",
|
||||
"NoThrow",
|
||||
"Immutable",
|
||||
"Acyclic",
|
||||
"ProgramLifetime",
|
||||
INLINE_ARRAY_MARKER,
|
||||
]);
|
||||
|
||||
/** The module a marker is imported from. */
|
||||
function markerModule(marker: string): string {
|
||||
return marker === INLINE_ARRAY_MARKER ? INLINE_ARRAY_MODULE : CONTRACT_TYPES_MODULE;
|
||||
}
|
||||
|
||||
/** One written contract: what it promises and where the annotation is. */
|
||||
export interface ContractNote {
|
||||
marker: ContractMarker;
|
||||
@@ -67,6 +85,9 @@ export interface ContractAnnotations {
|
||||
acyclic: AcyclicNote[];
|
||||
/** ProgramLifetime classes (`implements ProgramLifetime`) by class key. */
|
||||
classes: Map<string, ContractNote>;
|
||||
/** Inline array fields (`readonly f: InlineArray<C>`) by
|
||||
* `${class key}\0${field name}`. */
|
||||
inlineArrays: Map<string, ContractNote>;
|
||||
diagnostics: ScrDiagnostic[];
|
||||
/** Number of annotations found (for timing and tests). */
|
||||
count: number;
|
||||
@@ -123,7 +144,7 @@ function markerNamed(
|
||||
importDecl === undefined ||
|
||||
!ts.isImportDeclaration(importDecl) ||
|
||||
!ts.isStringLiteral(importDecl.moduleSpecifier) ||
|
||||
importDecl.moduleSpecifier.text !== CONTRACT_TYPES_MODULE
|
||||
importDecl.moduleSpecifier.text !== markerModule(member)
|
||||
)
|
||||
return null;
|
||||
return MARKERS.has(member) ? (member as ContractMarker) : null;
|
||||
@@ -158,6 +179,7 @@ export function collectContractAnnotations(
|
||||
fields: new Map(),
|
||||
acyclic: [],
|
||||
classes: new Map(),
|
||||
inlineArrays: new Map(),
|
||||
diagnostics: [],
|
||||
count: 0,
|
||||
};
|
||||
@@ -166,7 +188,11 @@ export function collectContractAnnotations(
|
||||
ts.SyntaxKind.ExpressionWithTypeArguments,
|
||||
]);
|
||||
for (const sf of files) {
|
||||
if (sf.isDeclarationFile || !sf.text.includes(CONTRACT_TYPES_MODULE)) continue;
|
||||
if (
|
||||
sf.isDeclarationFile ||
|
||||
(!sf.text.includes(CONTRACT_TYPES_MODULE) && !sf.text.includes(INLINE_ARRAY_MODULE))
|
||||
)
|
||||
continue;
|
||||
ts.walkPreorder(
|
||||
sf,
|
||||
(node) => {
|
||||
@@ -183,6 +209,7 @@ export function collectContractAnnotations(
|
||||
out.count++;
|
||||
if (marker === "Acyclic") placeAcyclicAlias(out, checker, node, resolveUnit);
|
||||
else if (marker === "ProgramLifetime") unsupported(out, node, marker);
|
||||
else if (marker === INLINE_ARRAY_MARKER) placeInlineArray(out, node);
|
||||
else place(out, checker, node, marker);
|
||||
},
|
||||
kinds,
|
||||
@@ -203,6 +230,17 @@ function unsupported(out: ContractAnnotations, node: ts.Node, marker: ContractMa
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (marker === INLINE_ARRAY_MARKER) {
|
||||
out.diagnostics.push(
|
||||
contractDiag(
|
||||
"SC7006",
|
||||
locOf(node),
|
||||
"the InlineArray contract is not checked in this position (InlineArray is checked on instance fields of ProgramLifetime classes)",
|
||||
"write it directly as the type of an instance field: readonly f: InlineArray<C>",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const where =
|
||||
marker === "NoThrow"
|
||||
? "NoThrow wraps a function's written return type"
|
||||
@@ -282,6 +320,28 @@ function placeHeritage(
|
||||
unsupported(out, node, marker);
|
||||
}
|
||||
|
||||
/** `readonly f: InlineArray<C>`: an instance field of a class. */
|
||||
function placeInlineArray(out: ContractAnnotations, ref: ts.TypeReferenceNode): void {
|
||||
const parent = ref.parent;
|
||||
if (
|
||||
parent === undefined ||
|
||||
!ts.isPropertyDeclaration(parent) ||
|
||||
parent.type !== ref ||
|
||||
ref.typeArguments?.nodes.length !== 1 ||
|
||||
ts.getModifiers(parent)?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) === true ||
|
||||
!(ts.isIdentifier(parent.name) || ts.isPrivateIdentifier(parent.name))
|
||||
)
|
||||
return unsupported(out, ref, INLINE_ARRAY_MARKER);
|
||||
const cls = parent.parent;
|
||||
if (cls === undefined || !(ts.isClassDeclaration(cls) || ts.isClassExpression(cls)))
|
||||
return unsupported(out, ref, INLINE_ARRAY_MARKER);
|
||||
out.inlineArrays.set(`${contractLocKey(locOf(cls))}\0${parent.name.text}`, {
|
||||
marker: INLINE_ARRAY_MARKER,
|
||||
loc: locOf(ref),
|
||||
name: `${nameText(cls.name)}.${parent.name.text}`,
|
||||
});
|
||||
}
|
||||
|
||||
/** `type T = Acyclic<{ ... }>`: the aliased object type. */
|
||||
function placeAcyclicAlias(
|
||||
out: ContractAnnotations,
|
||||
|
||||
@@ -5012,6 +5012,7 @@ export class Lowerer {
|
||||
classImplementedProtocol: (type) => this.classImplementedProtocol(type),
|
||||
isStdlibFile: (sf) => this.isStdlibFile(sf),
|
||||
isNpmFile: (sf) => this.isNpmFile(sf),
|
||||
isExperimentalFile: (sf) => isExperimentalDeclarationFile(sf, this.experimentalFiles),
|
||||
// The declaration-typed boundary's generated types (decl-projection.ts)
|
||||
// describe data, like an explicitly mapped external type surface.
|
||||
isExternalTypeFile: (sf) =>
|
||||
|
||||
@@ -1470,6 +1470,9 @@ export interface TypeMapperCtx {
|
||||
isStdlibFile: (sf: ts.SourceFile) => boolean;
|
||||
|
||||
isNpmFile: (sf: ts.SourceFile) => boolean;
|
||||
/** The declaration file of @scriptc/experimental, wherever it is installed
|
||||
* (a workspace links it outside node_modules). */
|
||||
isExperimentalFile?: (sf: ts.SourceFile) => boolean;
|
||||
/** True for a declaration file explicitly mapped by coverage's
|
||||
* --external-types option. These declarations are trusted as structural
|
||||
* type descriptions for project-owned values, but their imported runtime
|
||||
@@ -7621,9 +7624,14 @@ function isTypeOnlyNamespace(d: ts.Node): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function packageTypeFile(sf: ts.SourceFile, ctx: Pick<TypeMapperCtx, "isStdlibFile">): boolean {
|
||||
function packageTypeFile(
|
||||
sf: ts.SourceFile,
|
||||
ctx: Pick<TypeMapperCtx, "isStdlibFile" | "isExperimentalFile">,
|
||||
): boolean {
|
||||
return (
|
||||
sf.isDeclarationFile && /[\\/]node_modules[\\/]/.test(sf.fileName) && !ctx.isStdlibFile(sf)
|
||||
sf.isDeclarationFile &&
|
||||
(/[\\/]node_modules[\\/]/.test(sf.fileName) || ctx.isExperimentalFile?.(sf) === true) &&
|
||||
!ctx.isStdlibFile(sf)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -219,6 +219,7 @@ const MODULE_FIELDS: Record<StringFields<Omit<IrModule, "functions">>, Role> = {
|
||||
"contracts.acyclic[]": "other",
|
||||
"contracts.immortalGlobals[]": "other",
|
||||
"contracts.programLifetime[]": "other",
|
||||
"contracts.inlineArrays[]": "other",
|
||||
"wasmModules[].sha256": "other",
|
||||
"wasmModules[].bytes": "other",
|
||||
"wasmUntranslated[].sha256": "other",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,290 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { inlineArrayReads, planInlineArrays } from "./inline-arrays.js";
|
||||
import {
|
||||
F64,
|
||||
VOID,
|
||||
type IrClassDef,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "./ir.js";
|
||||
|
||||
const loc = { file: "inline-arrays.ts", start: 0, end: 0 };
|
||||
const obj = (className: string): IrType => ({ kind: "object", className });
|
||||
const ITEMS: IrType = { kind: "array", elem: obj("Item") };
|
||||
const ref = (localId: string, type: IrType): IrExpr => ({ loc, kind: "varRef", localId, type });
|
||||
const set = (target: IrExpr, className: string, field: string, value: IrExpr): IrStmt => ({
|
||||
loc,
|
||||
kind: "fieldSet",
|
||||
obj: target,
|
||||
className,
|
||||
field,
|
||||
value,
|
||||
});
|
||||
const get = (target: IrExpr, className: string, field: string, type: IrType): IrExpr => ({
|
||||
loc,
|
||||
kind: "fieldGet",
|
||||
obj: target,
|
||||
className,
|
||||
field,
|
||||
type,
|
||||
});
|
||||
const fn = (
|
||||
name: string,
|
||||
params: { id: string; type: IrType }[],
|
||||
body: IrStmt[],
|
||||
locals: { id: string; type: IrType; mutable?: boolean }[] = [],
|
||||
): IrFunction => ({
|
||||
loc,
|
||||
name,
|
||||
params: params.map((p) => ({ localId: p.id, name: p.id, type: p.type })),
|
||||
locals: [...params, ...locals].map((l) => ({
|
||||
id: l.id,
|
||||
name: l.id,
|
||||
type: l.type,
|
||||
mutable: "mutable" in l ? l.mutable === true : false,
|
||||
})),
|
||||
returnType: VOID,
|
||||
body,
|
||||
});
|
||||
const cls = (name: string, fields: [string, IrType][], base?: string): IrClassDef => ({
|
||||
loc,
|
||||
name,
|
||||
...(base ? { base } : {}),
|
||||
fields: fields.map(([field, type]) => ({ name: field, type })),
|
||||
});
|
||||
const listFields: [string, IrType][] = [
|
||||
["pos", F64],
|
||||
["items", ITEMS],
|
||||
];
|
||||
const listCtor = fn(
|
||||
"%List.constructor",
|
||||
[
|
||||
{ id: "this", type: obj("List") },
|
||||
{ id: "pos", type: F64 },
|
||||
{ id: "items", type: ITEMS },
|
||||
],
|
||||
[
|
||||
set(ref("this", obj("List")), "List", "pos", ref("pos", F64)),
|
||||
set(ref("this", obj("List")), "List", "items", ref("items", ITEMS)),
|
||||
],
|
||||
);
|
||||
// `class Flagged extends List { constructor(items, flags, pos) { super(pos, items) } }`.
|
||||
const flaggedCtor = fn(
|
||||
"%Flagged.constructor",
|
||||
[
|
||||
{ id: "this", type: obj("Flagged") },
|
||||
{ id: "nodes", type: ITEMS },
|
||||
{ id: "flags", type: F64 },
|
||||
{ id: "pos", type: F64 },
|
||||
],
|
||||
[
|
||||
{
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
loc,
|
||||
kind: "call",
|
||||
callee: "%List.constructor",
|
||||
args: [
|
||||
{ loc, kind: "upcast", value: ref("this", obj("Flagged")), type: obj("List") },
|
||||
ref("pos", F64),
|
||||
ref("nodes", ITEMS),
|
||||
],
|
||||
type: VOID,
|
||||
},
|
||||
},
|
||||
set(ref("this", obj("Flagged")), "Flagged", "flags", ref("flags", F64)),
|
||||
],
|
||||
);
|
||||
const itemCtor = fn("%Item.constructor", [{ id: "this", type: obj("Item") }], []);
|
||||
const mod = (functions: IrFunction[], extra: IrClassDef[] = []): IrModule => ({
|
||||
irVersion: 17,
|
||||
sourceFile: "inline-arrays.ts",
|
||||
entry: "%main",
|
||||
classes: [
|
||||
cls("Item", []),
|
||||
cls("List", listFields),
|
||||
cls("Flagged", [...listFields, ["flags", F64]], "List"),
|
||||
...extra,
|
||||
],
|
||||
functions: [fn("%main", [], []), itemCtor, listCtor, flaggedCtor, ...functions],
|
||||
records: [],
|
||||
unions: [],
|
||||
globals: [],
|
||||
contracts: { programLifetime: ["Item", "List"], inlineArrays: ["List\0items"] },
|
||||
});
|
||||
const lifetime = (name: string): boolean =>
|
||||
name === "Item" || name === "List" || name === "Flagged";
|
||||
|
||||
test("a subtree is planned with the argument each class takes its length from", () => {
|
||||
const plan = planInlineArrays(mod([]), lifetime);
|
||||
expect(plan.rejected).toEqual([]);
|
||||
expect(plan.classes.get("List")).toMatchObject({ field: "items", fieldIndex: 1, lengthArg: 1 });
|
||||
// Flagged passes its first parameter where List takes the array.
|
||||
expect(plan.classes.get("Flagged")).toMatchObject({ fieldIndex: 1, lengthArg: 0 });
|
||||
});
|
||||
|
||||
test("other writes, class values and dynamic values keep an ordinary field", () => {
|
||||
const conditional = mod([]);
|
||||
conditional.functions = conditional.functions.map((f) =>
|
||||
f.name !== "%List.constructor"
|
||||
? f
|
||||
: {
|
||||
...f,
|
||||
body: [
|
||||
{
|
||||
loc,
|
||||
kind: "if",
|
||||
cond: { loc, kind: "boolLit", value: true, type: { kind: "bool" } },
|
||||
then: [set(ref("this", obj("List")), "List", "items", ref("items", ITEMS))],
|
||||
else_: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
expect(planInlineArrays(conditional, lifetime).rejected[0]?.reason).toContain("conditionally");
|
||||
|
||||
const rewritten = mod([
|
||||
fn(
|
||||
"%List.reset",
|
||||
[
|
||||
{ id: "this", type: obj("List") },
|
||||
{ id: "other", type: ITEMS },
|
||||
],
|
||||
[set(ref("this", obj("List")), "List", "items", ref("other", ITEMS))],
|
||||
),
|
||||
]);
|
||||
expect(planInlineArrays(rewritten, lifetime).rejected[0]?.reason).toContain(
|
||||
"outside the constructor",
|
||||
);
|
||||
|
||||
const value = mod([
|
||||
fn(
|
||||
"%use",
|
||||
[],
|
||||
[
|
||||
{
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "classRef", className: "Flagged", type: { kind: "dyn" } },
|
||||
},
|
||||
],
|
||||
),
|
||||
]);
|
||||
expect(planInlineArrays(value, lifetime).rejected[0]?.reason).toContain("used as a value");
|
||||
|
||||
const dynamic = mod([
|
||||
fn(
|
||||
"%show",
|
||||
[{ id: "list", type: obj("List") }],
|
||||
[
|
||||
{
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "dynFrom", value: ref("list", obj("List")), type: { kind: "dyn" } },
|
||||
},
|
||||
],
|
||||
),
|
||||
]);
|
||||
expect(planInlineArrays(dynamic, lifetime).rejected[0]?.reason).toContain("dynamic value");
|
||||
});
|
||||
|
||||
test("an alias that only indexes the field reads the field", () => {
|
||||
const read: IrExpr = {
|
||||
loc,
|
||||
kind: "arrayGet",
|
||||
arr: ref("nodes", ITEMS),
|
||||
index: { loc, kind: "numLit", value: 0, type: F64 },
|
||||
type: obj("Item"),
|
||||
};
|
||||
const sum = fn(
|
||||
"%sum",
|
||||
[{ id: "list", type: obj("List") }],
|
||||
[
|
||||
{
|
||||
loc,
|
||||
kind: "varDecl",
|
||||
localId: "nodes",
|
||||
init: get(ref("list", obj("List")), "List", "items", ITEMS),
|
||||
},
|
||||
{ loc, kind: "exprStmt", expr: read },
|
||||
{
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
loc,
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: ref("nodes", ITEMS),
|
||||
args: [],
|
||||
type: F64,
|
||||
},
|
||||
},
|
||||
],
|
||||
[{ id: "nodes", type: ITEMS }],
|
||||
);
|
||||
const m = mod([sum]);
|
||||
const out = inlineArrayReads(m, planInlineArrays(m, lifetime)).functions.find(
|
||||
(f) => f.name === "%sum",
|
||||
)!;
|
||||
const text = JSON.stringify(out.body);
|
||||
// The alias is gone: its reads read the field through the parameter.
|
||||
expect(text).not.toContain('"localId":"nodes"');
|
||||
expect(text).toContain('"kind":"fieldGet"');
|
||||
expect(out.body[0]).toMatchObject({ kind: "block", body: [] });
|
||||
});
|
||||
|
||||
test("an alias that escapes stays an array local", () => {
|
||||
const sum = fn(
|
||||
"%keep",
|
||||
[{ id: "list", type: obj("List") }],
|
||||
[
|
||||
{
|
||||
loc,
|
||||
kind: "varDecl",
|
||||
localId: "nodes",
|
||||
init: get(ref("list", obj("List")), "List", "items", ITEMS),
|
||||
},
|
||||
{ loc, kind: "return", value: ref("nodes", ITEMS) },
|
||||
],
|
||||
[{ id: "nodes", type: ITEMS }],
|
||||
);
|
||||
const m = mod([sum]);
|
||||
const out = inlineArrayReads(m, planInlineArrays(m, lifetime));
|
||||
expect(out.functions.find((f) => f.name === "%keep")).toBe(sum);
|
||||
});
|
||||
|
||||
test("new evaluates its arguments into locals before allocating", () => {
|
||||
const literal: IrExpr = { loc, kind: "arrayLit", elems: [], type: ITEMS };
|
||||
const build = fn(
|
||||
"%build",
|
||||
[],
|
||||
[
|
||||
{
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
loc,
|
||||
kind: "new",
|
||||
className: "List",
|
||||
args: [{ loc, kind: "numLit", value: 1, type: F64 }, literal],
|
||||
type: obj("List"),
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
const m = mod([build]);
|
||||
const out = inlineArrayReads(m, planInlineArrays(m, lifetime)).functions.find(
|
||||
(f) => f.name === "%build",
|
||||
)!;
|
||||
const stmt = out.body[0] as IrStmt & { kind: "exprStmt" };
|
||||
expect(stmt.expr.kind).toBe("seqExpr");
|
||||
const seq = stmt.expr as IrExpr & { kind: "seqExpr" };
|
||||
expect(seq.stmts.map((s) => s.kind)).toEqual(["varDecl", "varDecl"]);
|
||||
const made = seq.result as IrExpr & { kind: "new" };
|
||||
expect(made.args.every((a) => a.kind === "varRef")).toBe(true);
|
||||
expect(out.locals.filter((l) => l.id.startsWith("%inl.arg"))).toHaveLength(2);
|
||||
});
|
||||
@@ -0,0 +1,602 @@
|
||||
/* Inline array fields (IrContracts.inlineArrays): an array field of a
|
||||
* ProgramLifetime class whose elements the backend may keep in the
|
||||
* instance itself, after its fixed fields, instead of in an array object.
|
||||
*
|
||||
* The contract makes that possible: the declaring class's constructor writes
|
||||
* the field once, from one of its parameters, and nothing else writes it, so
|
||||
* the field's length is the length of a constructor argument and the
|
||||
* elements never change. `planInlineArrays` finds the hierarchies where the
|
||||
* IR shows exactly that, and the `new` argument each class's instance takes
|
||||
* its length from: the declaring class's parameter, or the parameter a
|
||||
* subclass constructor passes there through its `super(...)` call. A
|
||||
* hierarchy whose IR shows anything else keeps an ordinary array field
|
||||
* (the layout is the backend's choice; the field's value is the same).
|
||||
*
|
||||
* `inlineArrayReads` then prepares the IR for the layout:
|
||||
* - a `new` of such a class evaluates its arguments into locals first, in
|
||||
* order, unless every argument is a local or a constant, so that the
|
||||
* backend reads the length after every argument has run and before it
|
||||
* allocates;
|
||||
* - an immutable local initialized from the field (`const nodes =
|
||||
* list.nodes`) whose every use indexes it, reads its length or iterates
|
||||
* it with `for...of` is replaced by reads of the field, through a local
|
||||
* holding the instance, so that those reads go straight to the elements. */
|
||||
|
||||
import type {
|
||||
IrClassDef,
|
||||
IrExpr,
|
||||
IrFunction,
|
||||
IrLocal,
|
||||
IrModule,
|
||||
IrStmt,
|
||||
IrType,
|
||||
SrcLoc,
|
||||
} from "./ir.js";
|
||||
import {
|
||||
everyExprChild,
|
||||
everyStmtChild,
|
||||
everyStmtList,
|
||||
everyTypeChild,
|
||||
transformStmtList,
|
||||
} from "./traverse.js";
|
||||
|
||||
/** A class whose instances may hold an inline array field. */
|
||||
export interface InlineArrayClass {
|
||||
/** The class that declares the field (the hierarchy's subtree root). */
|
||||
readonly declaringClass: string;
|
||||
readonly field: string;
|
||||
/** The field's index in the class's fields: the same in every class of the
|
||||
* subtree, whose field lists extend their bases'. */
|
||||
readonly fieldIndex: number;
|
||||
/** The class of the elements. */
|
||||
readonly elemClass: string;
|
||||
/** The index in a `new` expression's `args` of the argument the field is
|
||||
* initialized from. */
|
||||
readonly lengthArg: number;
|
||||
}
|
||||
|
||||
/** Why a contract entry keeps an ordinary array field. */
|
||||
export interface InlineArrayRejection {
|
||||
readonly declaringClass: string;
|
||||
readonly field: string;
|
||||
readonly reason: string;
|
||||
/** The use that rules the layout out, when it is one site. */
|
||||
readonly loc?: SrcLoc;
|
||||
}
|
||||
|
||||
export interface InlineArrayPlan {
|
||||
/** By class name, for the declaring classes and their subclasses. */
|
||||
readonly classes: ReadonlyMap<string, InlineArrayClass>;
|
||||
readonly rejected: readonly InlineArrayRejection[];
|
||||
}
|
||||
|
||||
export const NO_INLINE_ARRAYS: InlineArrayPlan = { classes: new Map(), rejected: [] };
|
||||
|
||||
/** The entries of `IrContracts.inlineArrays` (`${class}\0${field}`) whose
|
||||
* hierarchies the IR admits, given which element classes the backend can
|
||||
* store as 32-bit region references (`compactElem`). */
|
||||
export function planInlineArrays(
|
||||
mod: IrModule,
|
||||
compactElem: (className: string) => boolean,
|
||||
): InlineArrayPlan {
|
||||
const entries = mod.contracts?.inlineArrays ?? [];
|
||||
if (entries.length === 0) return NO_INLINE_ARRAYS;
|
||||
const classes = new Map<string, IrClassDef>();
|
||||
const children = new Map<string, string[]>();
|
||||
for (const cls of mod.classes ?? []) {
|
||||
classes.set(cls.name, cls);
|
||||
if (cls.base !== undefined) {
|
||||
const list = children.get(cls.base);
|
||||
if (list === undefined) children.set(cls.base, [cls.name]);
|
||||
else list.push(cls.name);
|
||||
}
|
||||
}
|
||||
const functions = new Map<string, IrFunction>();
|
||||
for (const fn of mod.functions) functions.set(fn.name, fn);
|
||||
const planned = new Map<string, InlineArrayClass>();
|
||||
const rejected: InlineArrayRejection[] = [];
|
||||
for (const entry of entries) {
|
||||
const at = entry.indexOf("\0");
|
||||
const declaringClass = entry.slice(0, at);
|
||||
const field = entry.slice(at + 1);
|
||||
const reject = (reason: string, loc?: SrcLoc): void => {
|
||||
rejected.push({ declaringClass, field, reason, ...(loc !== undefined ? { loc } : {}) });
|
||||
};
|
||||
const decl = classes.get(declaringClass);
|
||||
if (decl === undefined) {
|
||||
reject("the class is not in the program");
|
||||
continue;
|
||||
}
|
||||
const fieldIndex = decl.fields.findIndex((f) => f.name === field);
|
||||
if (fieldIndex < 0) {
|
||||
reject("the class has no such field");
|
||||
continue;
|
||||
}
|
||||
const base = decl.base === undefined ? undefined : classes.get(decl.base);
|
||||
if (base !== undefined && base.fields.some((f) => f.name === field)) {
|
||||
reject("the field is declared by a base class");
|
||||
continue;
|
||||
}
|
||||
const type = decl.fields[fieldIndex]!.type;
|
||||
if (type.kind !== "array" || type.elem.kind !== "object" || !compactElem(type.elem.className)) {
|
||||
reject("the field's elements are not ProgramLifetime instances");
|
||||
continue;
|
||||
}
|
||||
const elemClass = type.elem.className;
|
||||
// The subtree, preorder.
|
||||
const subtree: string[] = [];
|
||||
const collect = (name: string): void => {
|
||||
subtree.push(name);
|
||||
for (const child of children.get(name) ?? []) collect(child);
|
||||
};
|
||||
collect(declaringClass);
|
||||
const members = new Set(subtree);
|
||||
const lengthParams = new Map<string, number>();
|
||||
let reason: string | null = null;
|
||||
for (const name of subtree) {
|
||||
const ctor = functions.get(`%${name}.constructor`);
|
||||
if (ctor === undefined) {
|
||||
reason = `${name} has no constructor`;
|
||||
break;
|
||||
}
|
||||
const param =
|
||||
name === declaringClass
|
||||
? writtenParam(ctor, members, field)
|
||||
: forwardedParam(ctor, classes.get(name)!.base!, lengthParams);
|
||||
if (typeof param === "string") {
|
||||
reason = `${name}: ${param}`;
|
||||
break;
|
||||
}
|
||||
lengthParams.set(name, param);
|
||||
}
|
||||
const other = reason === null ? otherUses(mod, members, field, declaringClass) : null;
|
||||
if (reason !== null || other !== null) {
|
||||
reject(reason ?? other!.reason, other?.loc);
|
||||
continue;
|
||||
}
|
||||
for (const name of subtree)
|
||||
planned.set(name, {
|
||||
declaringClass,
|
||||
field,
|
||||
fieldIndex,
|
||||
elemClass,
|
||||
lengthArg: lengthParams.get(name)! - 1,
|
||||
});
|
||||
}
|
||||
return { classes: planned, rejected };
|
||||
}
|
||||
|
||||
/** The index in `ctor.params` (0 is `this`) of the parameter the declaring
|
||||
* constructor writes the field from, or why there is none. */
|
||||
function writtenParam(
|
||||
ctor: IrFunction,
|
||||
members: ReadonlySet<string>,
|
||||
field: string,
|
||||
): number | string {
|
||||
const self = ctor.params[0]?.localId;
|
||||
const writes: (IrStmt & { kind: "fieldSet" })[] = [];
|
||||
everyStmtList(ctor.body, {
|
||||
expr: () => true,
|
||||
stmt: (s) => {
|
||||
if (s.kind === "fieldSet" && s.field === field && members.has(s.className)) writes.push(s);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (writes.length !== 1) return `the constructor writes the field ${writes.length} times`;
|
||||
const write = writes[0]!;
|
||||
// The write is one of the body's own statements: it runs exactly once.
|
||||
if (!ctor.body.includes(write)) return "the constructor writes the field conditionally";
|
||||
if (write.obj.kind !== "varRef" || write.obj.localId !== self)
|
||||
return "the constructor writes the field of another object";
|
||||
return parameterOf(ctor, write.value);
|
||||
}
|
||||
|
||||
/** The index in `ctor.params` of the parameter a subclass constructor
|
||||
* passes to its base's at the base's length parameter, or why there is none. */
|
||||
function forwardedParam(
|
||||
ctor: IrFunction,
|
||||
base: string,
|
||||
lengthParams: ReadonlyMap<string, number>,
|
||||
): number | string {
|
||||
const baseParam = lengthParams.get(base);
|
||||
if (baseParam === undefined) return "its base class is not planned";
|
||||
const calls: (IrExpr & { kind: "call" })[] = [];
|
||||
everyStmtList(ctor.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "call" && e.callee === `%${base}.constructor`) calls.push(e);
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
if (calls.length !== 1) return `the constructor calls its base's ${calls.length} times`;
|
||||
const arg = calls[0]!.args[baseParam];
|
||||
if (arg === undefined) return "the base constructor call has no argument for the field";
|
||||
return parameterOf(ctor, arg);
|
||||
}
|
||||
|
||||
/** The index in `fn.params` of the parameter `value` reads, when the
|
||||
* parameter is never written; otherwise why it is not one. */
|
||||
function parameterOf(fn: IrFunction, value: IrExpr): number | string {
|
||||
if (value.kind !== "varRef") return "the field's value is not a constructor parameter";
|
||||
const index = fn.params.findIndex((p, i) => i > 0 && p.localId === value.localId);
|
||||
if (index < 0) return "the field's value is not a constructor parameter";
|
||||
const local = fn.locals.find((l) => l.id === value.localId);
|
||||
if (local === undefined || local.boxed === true || assigns(fn, value.localId))
|
||||
return "the constructor parameter is written";
|
||||
// The allocation took its length from the argument, so nothing in the
|
||||
// constructor may change the array first: the field's value is the
|
||||
// parameter's only use.
|
||||
if (reads(fn, value.localId) !== 1)
|
||||
return "the constructor uses the array parameter besides storing it in the field";
|
||||
return index;
|
||||
}
|
||||
|
||||
function reads(fn: IrFunction, localId: string): number {
|
||||
let n = 0;
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "varRef" && e.localId === localId) n++;
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
return n;
|
||||
}
|
||||
|
||||
function assigns(fn: IrFunction, localId: string): boolean {
|
||||
return !everyStmtList(fn.body, {
|
||||
expr: () => true,
|
||||
stmt: (s) => !(s.kind === "assign" && s.localId === localId),
|
||||
});
|
||||
}
|
||||
|
||||
/** Why the rest of the program rules out the layout: another write of the
|
||||
* field, construction through a class value, an instance of the subtree
|
||||
* crossing into dynamic values (whose views of an instance read its fields
|
||||
* by name), or a read of the field compared by identity. Null when nothing
|
||||
* does. */
|
||||
function otherUses(
|
||||
mod: IrModule,
|
||||
members: ReadonlySet<string>,
|
||||
field: string,
|
||||
declaringClass: string,
|
||||
): { reason: string; loc?: SrcLoc } | null {
|
||||
const reaches = holdersOf(mod, members, declaringClass);
|
||||
let reason: string | null = null;
|
||||
let loc: SrcLoc | undefined;
|
||||
// Dynamic values that printing, JSON.stringify without a replacer and
|
||||
// publishing only read, and the options a worker copies its data from.
|
||||
const readOnly = new Set<IrExpr>();
|
||||
for (const fn of mod.functions) {
|
||||
const isDeclaringCtor = fn.name === `%${declaringClass}.constructor`;
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (reason !== null) return false;
|
||||
if (e.kind === "classRef" && members.has(e.className))
|
||||
reason = `${e.className} is used as a value`;
|
||||
else if (
|
||||
(e.kind === "libCall" && (readsArguments(e) || e.fn === "worker.new")) ||
|
||||
(e.kind === "intrinsic" &&
|
||||
(e.name.startsWith("console.") || e.name === "threads.publish"))
|
||||
)
|
||||
for (const a of e.args) readOnly.add(a);
|
||||
else if (
|
||||
(e.kind === "dynFrom" || e.kind === "caughtToDyn") &&
|
||||
!readOnly.has(e) &&
|
||||
reaches(e.value.type)
|
||||
) {
|
||||
reason =
|
||||
"an instance, or a value that can hold one, becomes a dynamic value (a dynamic view reads the field as a new array each time)";
|
||||
loc = e.loc;
|
||||
} else if (e.kind === "fieldIncDec" && e.field === field && members.has(e.className))
|
||||
reason = "the field is written outside the constructor";
|
||||
else if (
|
||||
e.kind === "bin" &&
|
||||
(e.op === "===" || e.op === "!==") &&
|
||||
(readsField(e.left, members, field) || readsField(e.right, members, field))
|
||||
)
|
||||
reason =
|
||||
"the field is compared by identity, and each read of the field may give another array (compare a copy made with slice(), or compare the elements)";
|
||||
return reason === null;
|
||||
},
|
||||
stmt: (s) => {
|
||||
if (reason !== null) return false;
|
||||
if (
|
||||
s.kind === "fieldSet" &&
|
||||
s.field === field &&
|
||||
members.has(s.className) &&
|
||||
!isDeclaringCtor
|
||||
)
|
||||
reason = "the field is written outside the constructor";
|
||||
return reason === null;
|
||||
},
|
||||
});
|
||||
if (reason !== null) return loc === undefined ? { reason } : { reason, loc };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether values of a type may be, or hold, an instance of `members`
|
||||
* (the subtree of `declaringClass`): a value typed as a member or as a base
|
||||
* of the declaring class, or a class instance, record, union, array, Map or
|
||||
* Set whose fields or elements may hold one. A dynamic view of any of them
|
||||
* reads the instance's fields by name. */
|
||||
function holdersOf(
|
||||
mod: IrModule,
|
||||
members: ReadonlySet<string>,
|
||||
declaringClass: string,
|
||||
): (type: IrType) => boolean {
|
||||
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c]));
|
||||
const arms = new Map((mod.unions ?? []).map((u) => [u.id, u.arms]));
|
||||
const records = new Map((mod.records ?? []).map((r) => [r.id, r.fields]));
|
||||
const holderClasses = new Set(members);
|
||||
const holderShapes = new Set<string>();
|
||||
const addWithBases = (name: string): boolean => {
|
||||
let added = false;
|
||||
for (let at: string | undefined = name; at !== undefined; at = classes.get(at)?.base)
|
||||
if (!holderClasses.has(at)) {
|
||||
holderClasses.add(at);
|
||||
added = true;
|
||||
}
|
||||
return added;
|
||||
};
|
||||
addWithBases(declaringClass);
|
||||
const holds = (type: IrType): boolean => {
|
||||
const seen = new Set<string>();
|
||||
let found = false;
|
||||
const visit = (t: IrType): boolean => {
|
||||
if (t.kind === "object") found = holderClasses.has(t.className);
|
||||
else if (t.kind === "record") found = holderShapes.has(t.shapeId);
|
||||
else if (t.kind === "union" && !seen.has(t.unionId)) {
|
||||
seen.add(t.unionId);
|
||||
for (const arm of arms.get(t.unionId) ?? []) if (!visit(arm)) break;
|
||||
}
|
||||
return !found && everyTypeChild(t, visit);
|
||||
};
|
||||
visit(type);
|
||||
return found;
|
||||
};
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const cls of mod.classes ?? [])
|
||||
if (!holderClasses.has(cls.name) && cls.fields.some((f) => holds(f.type)))
|
||||
changed = addWithBases(cls.name) || changed;
|
||||
for (const [shapeId, fields] of records)
|
||||
if (!holderShapes.has(shapeId) && fields.some((f) => holds(f.type))) {
|
||||
holderShapes.add(shapeId);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return holds;
|
||||
}
|
||||
|
||||
/** Library calls that only read their arguments: printing, inspection,
|
||||
* and JSON.stringify without a replacer function. */
|
||||
export function readsArguments(e: IrExpr & { kind: "libCall" }): boolean {
|
||||
if (/^(console|insp|inspect)\./.test(e.fn)) return true;
|
||||
switch (e.fn) {
|
||||
case "array.hasProperties":
|
||||
case "array.hasNamedKey":
|
||||
case "array.namedKeys":
|
||||
case "json.stringifyValue":
|
||||
return true;
|
||||
case "json.stringifyReplacer": {
|
||||
const replacer = e.args[1];
|
||||
return replacer?.kind === "dynFrom" && replacer.value.kind === "unitLit";
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function readsField(e: IrExpr, members: ReadonlySet<string>, field: string): boolean {
|
||||
return e.kind === "fieldGet" && e.field === field && members.has(e.className);
|
||||
}
|
||||
|
||||
/** Prepares the reads and constructions of planned classes (see the module
|
||||
* comment). Returns the module itself when nothing changes. */
|
||||
export function inlineArrayReads(mod: IrModule, plan: InlineArrayPlan): IrModule {
|
||||
if (plan.classes.size === 0) return mod;
|
||||
let changed = false;
|
||||
const functions = mod.functions.map((fn) => {
|
||||
const next = rewriteFunction(fn, plan);
|
||||
if (next !== fn) changed = true;
|
||||
return next;
|
||||
});
|
||||
return changed ? { ...mod, functions } : mod;
|
||||
}
|
||||
|
||||
function isInlineFieldRead(e: IrExpr, plan: InlineArrayPlan): e is IrExpr & { kind: "fieldGet" } {
|
||||
if (e.kind !== "fieldGet") return false;
|
||||
const cls = plan.classes.get(e.className);
|
||||
return cls !== undefined && cls.field === e.field;
|
||||
}
|
||||
|
||||
/** Whether `parent` uses its child `child` (an array value) only by indexing
|
||||
* it, reading its length or iterating it. */
|
||||
function isDirectUse(parent: IrExpr | IrStmt, child: IrExpr): boolean {
|
||||
// Element reads and presence tests (a for...of over an array that may
|
||||
// have holes tests each index first).
|
||||
if (parent.kind === "arrayGet" || parent.kind === "arrayHas" || parent.kind === "arrayState")
|
||||
return parent.arr === child;
|
||||
if (parent.kind === "arrIntrinsic")
|
||||
return parent.method === "length" && parent.receiver === child;
|
||||
if (parent.kind === "forOf") return parent.iterable === child;
|
||||
return false;
|
||||
}
|
||||
|
||||
function rewriteFunction(fn: IrFunction, plan: InlineArrayPlan): IrFunction {
|
||||
const locals = new Map<string, IrLocal>();
|
||||
for (const l of fn.locals) locals.set(l.id, l);
|
||||
// Aliases: immutable locals declared once from an inline field read.
|
||||
const declared = new Map<string, number>();
|
||||
const aliasDecl = new Map<string, IrStmt & { kind: "varDecl" }>();
|
||||
everyStmtList(fn.body, {
|
||||
expr: () => true,
|
||||
stmt: (s) => {
|
||||
if (s.kind === "varDecl") {
|
||||
declared.set(s.localId, (declared.get(s.localId) ?? 0) + 1);
|
||||
const local = locals.get(s.localId);
|
||||
if (
|
||||
s.init !== null &&
|
||||
isInlineFieldRead(s.init, plan) &&
|
||||
local !== undefined &&
|
||||
!local.mutable &&
|
||||
local.boxed !== true
|
||||
)
|
||||
aliasDecl.set(s.localId, s);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
for (const id of [...aliasDecl.keys()]) if (declared.get(id) !== 1) aliasDecl.delete(id);
|
||||
// Every read of an alias must be a direct use.
|
||||
if (aliasDecl.size > 0) {
|
||||
const direct = new Map<string, boolean>();
|
||||
const visitChildren = (parent: IrExpr | IrStmt, children: IrExpr[]): void => {
|
||||
for (const c of children)
|
||||
if (c.kind === "varRef" && aliasDecl.has(c.localId) && !isDirectUse(parent, c))
|
||||
direct.set(c.localId, false);
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "varRef") {
|
||||
if (aliasDecl.has(e.localId) && !direct.has(e.localId)) direct.set(e.localId, true);
|
||||
return true;
|
||||
}
|
||||
visitChildren(e, directChildren(e));
|
||||
return true;
|
||||
},
|
||||
stmt: (s) => {
|
||||
visitChildren(s, directStmtChildren(s));
|
||||
return true;
|
||||
},
|
||||
});
|
||||
for (const [id, ok] of direct) if (!ok) aliasDecl.delete(id);
|
||||
for (const id of [...aliasDecl.keys()]) if (!direct.has(id)) aliasDecl.delete(id);
|
||||
}
|
||||
const needsNew = !everyStmtList(fn.body, {
|
||||
expr: (e) => !(e.kind === "new" && newNeedsLocals(e, plan)),
|
||||
stmt: () => true,
|
||||
});
|
||||
if (aliasDecl.size === 0 && !needsNew) return fn;
|
||||
const newLocals: IrLocal[] = [];
|
||||
let counter = 0;
|
||||
const fresh = (name: string, type: IrType): string => {
|
||||
let id: string;
|
||||
do id = `%inl.${name}.${counter++}`;
|
||||
while (locals.has(id));
|
||||
const local: IrLocal = { id, name: `%inl.${name}`, type, mutable: false };
|
||||
locals.set(id, local);
|
||||
newLocals.push(local);
|
||||
return id;
|
||||
};
|
||||
// Each alias reads the field through a local holding the instance: the
|
||||
// instance expression's own local when it is an immutable one, else a new
|
||||
// one the declaration initializes.
|
||||
const owners = new Map<string, { localId: string; read: IrExpr & { kind: "fieldGet" } }>();
|
||||
for (const [id, decl] of aliasDecl) {
|
||||
const read = decl.init as IrExpr & { kind: "fieldGet" };
|
||||
const obj = read.obj;
|
||||
const own = obj.kind === "varRef" ? locals.get(obj.localId) : undefined;
|
||||
const ownerId =
|
||||
own !== undefined && !own.mutable && own.boxed !== true && !assigns(fn, own.id)
|
||||
? own.id
|
||||
: fresh("owner", obj.type);
|
||||
owners.set(id, { localId: ownerId, read });
|
||||
}
|
||||
const fieldRead = (aliasId: string, loc: SrcLoc): IrExpr => {
|
||||
const owner = owners.get(aliasId)!;
|
||||
return {
|
||||
...owner.read,
|
||||
obj: { kind: "varRef", localId: owner.localId, type: owner.read.obj.type, loc },
|
||||
loc,
|
||||
};
|
||||
};
|
||||
const body = transformStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "varRef" && owners.has(e.localId)) return fieldRead(e.localId, e.loc);
|
||||
if (e.kind === "new" && newNeedsLocals(e, plan)) return newThroughLocals(e, fresh);
|
||||
return e;
|
||||
},
|
||||
stmt: (s) => {
|
||||
if (s.kind === "varDecl" && owners.has(s.localId)) {
|
||||
const owner = owners.get(s.localId)!;
|
||||
const obj = owner.read.obj;
|
||||
// The instance's own local needs no declaration; otherwise the
|
||||
// alias's declaration becomes the owner's.
|
||||
if (obj.kind === "varRef" && obj.localId === owner.localId)
|
||||
return { kind: "block", body: [], loc: s.loc };
|
||||
return { kind: "varDecl", localId: owner.localId, init: obj, loc: s.loc };
|
||||
}
|
||||
return s;
|
||||
},
|
||||
});
|
||||
return { ...fn, body, locals: [...fn.locals, ...newLocals] };
|
||||
}
|
||||
|
||||
/** A `new` of a planned class whose length argument is not a local read. */
|
||||
function newNeedsLocals(e: IrExpr & { kind: "new" }, plan: InlineArrayPlan): boolean {
|
||||
if (!plan.classes.has(e.className)) return false;
|
||||
// The allocation reads the length once every argument has run: unless
|
||||
// they are all locals and constants, which run no code, they are
|
||||
// evaluated into locals first.
|
||||
return !e.args.every((a) => a.kind === "varRef" || isConstant(a));
|
||||
}
|
||||
|
||||
function isConstant(e: IrExpr): boolean {
|
||||
return e.kind === "numLit" || e.kind === "strLit" || e.kind === "boolLit" || e.kind === "unitLit";
|
||||
}
|
||||
|
||||
/** `new C(a, b, ...)` as `(t0 = a, t1 = b, ..., new C(t0, t1, ...))`: the
|
||||
* arguments evaluate in order before the allocation. */
|
||||
function newThroughLocals(
|
||||
e: IrExpr & { kind: "new" },
|
||||
fresh: (name: string, type: IrType) => string,
|
||||
): IrExpr {
|
||||
const stmts: IrStmt[] = [];
|
||||
const args = e.args.map((a) => {
|
||||
const id = fresh("arg", a.type);
|
||||
stmts.push({ kind: "varDecl", localId: id, init: a, loc: a.loc });
|
||||
return { kind: "varRef", localId: id, type: a.type, loc: a.loc } as IrExpr;
|
||||
});
|
||||
return { kind: "seqExpr", stmts, result: { ...e, args }, type: e.type, loc: e.loc } as IrExpr;
|
||||
}
|
||||
|
||||
/** The expression children of `e` that are array operands the alias test
|
||||
* looks at (everything else that reads an alias makes it escape). */
|
||||
function directChildren(e: IrExpr): IrExpr[] {
|
||||
const out: IrExpr[] = [];
|
||||
everyExprChildList(e, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
function directStmtChildren(s: IrStmt): IrExpr[] {
|
||||
const out: IrExpr[] = [];
|
||||
everyStmtChildList(s, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Immediate expression children, through the generic traversal: a child
|
||||
// visitor that collects instead of recursing.
|
||||
function everyExprChildList(e: IrExpr, out: IrExpr[]): void {
|
||||
everyExprChild(
|
||||
e,
|
||||
(c) => {
|
||||
out.push(c);
|
||||
return true;
|
||||
},
|
||||
() => true,
|
||||
);
|
||||
}
|
||||
|
||||
function everyStmtChildList(s: IrStmt, out: IrExpr[]): void {
|
||||
everyStmtChild(
|
||||
s,
|
||||
(c) => {
|
||||
out.push(c);
|
||||
return true;
|
||||
},
|
||||
() => true,
|
||||
);
|
||||
}
|
||||
@@ -1227,6 +1227,12 @@ export interface IrContracts {
|
||||
* static reference counting is elided, and the cycle analysis treats them
|
||||
* as leaves. */
|
||||
programLifetime?: string[];
|
||||
/** Inline array fields, `${declaring class}\0${field}`: an array field of
|
||||
* a ProgramLifetime class that its constructor writes once, from one of
|
||||
* its parameters, and nothing else writes. Its elements are fixed for the
|
||||
* instance's lifetime, so the backend may keep them in the instance itself
|
||||
* (ir/inline-arrays.ts). */
|
||||
inlineArrays?: string[];
|
||||
}
|
||||
|
||||
export type IrFfiValueParamClass =
|
||||
|
||||
@@ -0,0 +1,594 @@
|
||||
import { mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile } from "../src/index.js";
|
||||
|
||||
// InlineArray fields (@scriptc/experimental, ir/inline-arrays.ts): the
|
||||
// elements of a ProgramLifetime class's array field written once from a
|
||||
// constructor parameter live in the instance, after its fixed fields.
|
||||
|
||||
const repoRoot = join(import.meta.dirname, "../../..");
|
||||
|
||||
const header = `
|
||||
import type { ProgramLifetime } from "@scriptc/types";
|
||||
import type { InlineArray } from "@scriptc/experimental";
|
||||
`;
|
||||
|
||||
async function build(
|
||||
source: string,
|
||||
): Promise<{ ok: boolean; codes: string[]; llvm: string; messages: string[] }> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-inline-array-"));
|
||||
try {
|
||||
// The workspace's packages, so that the program resolves both imports.
|
||||
await symlink(join(repoRoot, "node_modules"), join(dir, "node_modules"));
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ll");
|
||||
await writeFile(entry, header + source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" });
|
||||
const llvm = result.ok ? await readFile(outPath, "utf8") : "";
|
||||
return {
|
||||
ok: result.ok,
|
||||
codes: (result.diagnostics ?? []).map((d) => d.code),
|
||||
messages: (result.diagnostics ?? []).map((d) => d.message),
|
||||
llvm,
|
||||
};
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const classes = `
|
||||
class Item implements ProgramLifetime {
|
||||
readonly kind: number;
|
||||
constructor(kind: number) {
|
||||
this.kind = kind;
|
||||
}
|
||||
}
|
||||
class List implements ProgramLifetime {
|
||||
readonly pos: number;
|
||||
readonly items: InlineArray<Item>;
|
||||
constructor(pos: number, items: readonly Item[]) {
|
||||
this.pos = pos;
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
class Flagged extends List {
|
||||
readonly flags: number;
|
||||
constructor(pos: number, items: readonly Item[], flags: number) {
|
||||
super(pos, items);
|
||||
this.flags = flags;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const body = (fn: string, llvm: string): string =>
|
||||
new RegExp(`^define internal [^@]+@sc_bf_${fn}\\([^]*?^}`, "m").exec(llvm)?.[0] ?? "";
|
||||
|
||||
// The inline layout applies to executables whose compact instances live in
|
||||
// the low region: Linux, 64-bit.
|
||||
const lowRegion =
|
||||
process.platform === "linux" && (process.arch === "x64" || process.arch === "arm64");
|
||||
|
||||
test.runIf(lowRegion)("the elements follow the fixed fields, which keep their count", async () => {
|
||||
const { ok, llvm } = await build(
|
||||
classes +
|
||||
`
|
||||
const a = new List(1, [new Item(1), new Item(2)]);
|
||||
const b = new Flagged(2, [new Item(3)], 7);
|
||||
console.log(a.items.length, b.items.length, b.flags);
|
||||
`,
|
||||
);
|
||||
expect(ok).toBe(true);
|
||||
// The field's slot is an i32: the header, pos and the count.
|
||||
expect(llvm).toMatch(/^%sc_o_List = type \{ i32, i32, i32 \}/m);
|
||||
expect(llvm).toMatch(/^%sc_o_Flagged = type \{ i32, i32, i32, i32 \}/m);
|
||||
// `new` takes the count and allocates room for the 32-bit references
|
||||
// after the largest fixed part in the subtree (Flagged's 16 bytes).
|
||||
expect(llvm).toContain("define internal ptr @sc_new_List(i32 %inl.len)");
|
||||
expect(llvm).toContain("%inl.size = add nuw nsw i64 %inl.bytes, 16");
|
||||
// The constructor copies the argument's elements into the instance.
|
||||
expect(llvm).toContain("call void @scr_arr_copy_cref_tail(");
|
||||
});
|
||||
|
||||
test.runIf(lowRegion)("indexing, length and local aliases read the instance", async () => {
|
||||
const { ok, llvm } = await build(
|
||||
classes +
|
||||
`
|
||||
function sum(list: List): number {
|
||||
let s = 0;
|
||||
const items = list.items;
|
||||
for (let i = 0; i < items.length; i++) s += items[i]!.kind;
|
||||
for (const item of list.items) s += item.kind;
|
||||
return s + list.items[0]!.kind;
|
||||
}
|
||||
const list = new List(0, [new Item(1), new Item(2)]);
|
||||
console.log(sum(list));
|
||||
`,
|
||||
);
|
||||
expect(ok).toBe(true);
|
||||
const sum = body("sum", llvm);
|
||||
expect(sum).not.toBe("");
|
||||
// Elements are 32-bit references loaded from the instance; only the
|
||||
// out-of-bounds path makes an array of them, for the runtime getter.
|
||||
expect(sum).toMatch(/getelementptr inbounds i32, ptr %\S+, i64 %\S+\n\s*%\S+ = load i32/);
|
||||
const hits = sum.split(/\n(?=\S)/).filter((block) => block.startsWith("inl.get.hit"));
|
||||
expect(hits.length).toBeGreaterThan(0);
|
||||
for (const block of hits) expect(block).not.toContain("scr_arr_view_cref");
|
||||
for (const line of sum.split("\n").filter((l) => l.includes("scr_arr_view_cref(")))
|
||||
expect(line).not.toContain("scr_arr_get_ref");
|
||||
});
|
||||
|
||||
test.runIf(lowRegion)("for...of tests presence on the instance", async () => {
|
||||
const { ok, llvm } = await build(
|
||||
classes +
|
||||
`
|
||||
// An array that may have holes: each loop over a list tests each index.
|
||||
function holed(items: readonly Item[]): Item[] {
|
||||
const out: Item[] = new Array<Item>(items.length);
|
||||
for (let i = 0; i < items.length; i++) out[i] = items[i]!;
|
||||
return out;
|
||||
}
|
||||
function kinds(list: List): number {
|
||||
let s = 0;
|
||||
for (const item of list.items) s += item.kind;
|
||||
return s;
|
||||
}
|
||||
console.log(kinds(new List(0, holed([new Item(1), new Item(2)]))));
|
||||
`,
|
||||
);
|
||||
expect(ok).toBe(true);
|
||||
const kinds = body("kinds", llvm);
|
||||
expect(kinds).not.toBe("");
|
||||
expect(kinds).not.toContain("scr_arr_state");
|
||||
// Only an index outside the count makes an array, for the runtime getter.
|
||||
for (const block of kinds.split(/\n(?=\S)/).filter((b) => b.includes("scr_arr_view_cref(")))
|
||||
expect(block.startsWith("inl.get.miss")).toBe(true);
|
||||
});
|
||||
|
||||
test.runIf(lowRegion)("a read that escapes gives an array of the elements", async () => {
|
||||
const { ok, llvm } = await build(
|
||||
classes +
|
||||
`
|
||||
function count(items: readonly Item[]): number {
|
||||
return items.length;
|
||||
}
|
||||
function use(list: List): number {
|
||||
return count(list.items) + list.items.indexOf(list.items[0]!);
|
||||
}
|
||||
console.log(use(new List(0, [new Item(1)])));
|
||||
`,
|
||||
);
|
||||
expect(ok).toBe(true);
|
||||
expect(body("use", llvm)).toContain("call ptr @scr_arr_view_cref(");
|
||||
});
|
||||
|
||||
test("writes to the arrays fields hold are refused", async () => {
|
||||
for (const write of [
|
||||
// Through a read of the field.
|
||||
`function grow(list: List): void {
|
||||
(list.items as Item[]).push(new Item(5));
|
||||
}
|
||||
grow(new List(0, [new Item(1)]));`,
|
||||
// Through a parameter and an array element.
|
||||
`function clear(items: readonly Item[]): void {
|
||||
(items as Item[]).length = 0;
|
||||
}
|
||||
const all = [new List(0, [new Item(1)]).items];
|
||||
clear(all[0]!);`,
|
||||
// Through a Map value.
|
||||
`const byPos = new Map<number, readonly Item[]>();
|
||||
const list = new List(0, [new Item(1)]);
|
||||
byPos.set(list.pos, list.items);
|
||||
(byPos.get(0)! as Item[])[0] = new Item(2);`,
|
||||
]) {
|
||||
const result = await build(classes + write);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.codes).toContain("SC7009");
|
||||
expect(result.messages.join("\n")).toContain(
|
||||
"writes to an array that may be an InlineArray field's",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("arrays fields hold do not reach unchecked code inside other values", async () => {
|
||||
for (const [use, where] of [
|
||||
// Inside an array passed through a function value.
|
||||
[
|
||||
`const cb = (xs: Item[][]): void => {
|
||||
xs[0]!.push(new Item(9));
|
||||
};
|
||||
const f: (xs: Item[][]) => void = Math.random() < 2 ? cb : () => {};
|
||||
f([new List(0, [new Item(1)]).items as Item[]]);`,
|
||||
"a call through a function value",
|
||||
],
|
||||
// In a field of a class instance that becomes a dynamic value.
|
||||
[
|
||||
`class Box {
|
||||
readonly arr: readonly Item[];
|
||||
constructor(arr: readonly Item[]) {
|
||||
this.arr = arr;
|
||||
}
|
||||
}
|
||||
const u: unknown = new Box(new List(0, [new Item(1)]).items);
|
||||
(u as { arr: Item[] }).arr.push(new Item(9));`,
|
||||
"a dynamic value",
|
||||
],
|
||||
]) {
|
||||
const result = await build(classes + use);
|
||||
expect(result.ok, use).toBe(false);
|
||||
expect(result.messages.join("\n"), use).toContain(`reaches ${where}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("a write names the field read whose array it may reach", async () => {
|
||||
const result = await build(
|
||||
classes +
|
||||
`
|
||||
function same(items: readonly Item[]): readonly Item[] {
|
||||
return items;
|
||||
}
|
||||
const list = new List(0, [new Item(1)]);
|
||||
const all: (readonly Item[])[] = [same(list.items)];
|
||||
(all[0]! as Item[]).pop();
|
||||
`,
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.messages.join("\n")).toMatch(
|
||||
/writes to an array that may be an InlineArray field's \(from main\.ts:\d+:\d+\)/,
|
||||
);
|
||||
});
|
||||
|
||||
test("other arrays of the element type stay writable", async () => {
|
||||
// Arrays are followed by where they are held, not by type: a helper that
|
||||
// returns its argument gives each caller its own array back.
|
||||
const result = await build(
|
||||
classes +
|
||||
`
|
||||
function same(items: readonly Item[]): readonly Item[] {
|
||||
return items;
|
||||
}
|
||||
const list = new List(0, [new Item(1)]);
|
||||
const views: (readonly Item[])[] = [same(list.items)];
|
||||
const scratch: Item[][] = [[]];
|
||||
const own = same([new Item(2)]) as Item[];
|
||||
scratch[0]!.push(new Item(3));
|
||||
own.push(new Item(4));
|
||||
console.log(views[0]!.length, scratch[0]!.length, own.length);
|
||||
`,
|
||||
);
|
||||
expect(result.messages).toEqual([]);
|
||||
});
|
||||
|
||||
test("a read of the field may reach printing and copies, but no other library call", async () => {
|
||||
const reads = await build(
|
||||
classes +
|
||||
`
|
||||
const list = new List(0, [new Item(1)]);
|
||||
console.log(list.items, JSON.stringify(list.items), [...list.items].length);
|
||||
console.log(new List(1, list.items.slice()).items.length);
|
||||
`,
|
||||
);
|
||||
expect(reads.ok).toBe(true);
|
||||
const frozen = await build(
|
||||
classes +
|
||||
`
|
||||
console.log(Object.isFrozen(new List(0, [new Item(1)]).items));
|
||||
`,
|
||||
);
|
||||
expect(frozen.ok).toBe(false);
|
||||
expect(frozen.messages.join("\n")).toContain("reaches the library call");
|
||||
});
|
||||
|
||||
test("the constructor is passed an array nothing else keeps", async () => {
|
||||
const accepted = [
|
||||
// A literal, a copy, and a factory's result.
|
||||
`function fresh(n: number): Item[] {
|
||||
const out: Item[] = [];
|
||||
for (let i = 0; i < n; i++) out.push(new Item(i));
|
||||
return out;
|
||||
}
|
||||
const a = new List(0, [new Item(1)]);
|
||||
const b = new List(1, a.items.concat([new Item(2)]));
|
||||
console.log(a.items.length, b.items.length, new List(2, fresh(3)).items.length);`,
|
||||
// A local's last use, after writes in place, in either arm of a
|
||||
// conditional, and after an early return.
|
||||
`function make(c: boolean): List {
|
||||
const items = [new Item(1)];
|
||||
items.push(new Item(2));
|
||||
if (items.length > 5) return new List(-1, items);
|
||||
return c ? new List(0, items) : new List(1, items);
|
||||
}
|
||||
console.log(make(true).items.length, make(false).pos);`,
|
||||
// A parameter its callers move.
|
||||
`function wrap(pos: number, items: Item[]): List {
|
||||
items.reverse();
|
||||
return new List(pos, items);
|
||||
}
|
||||
function build(n: number): List {
|
||||
return wrap(n, Array.from({ length: n }, (_, i) => new Item(i)));
|
||||
}
|
||||
console.log(build(2).items[0]!.kind);`,
|
||||
// An array a field already holds.
|
||||
`const a = new List(0, [new Item(1)]);
|
||||
const shared = a.items;
|
||||
console.log(new List(1, shared).items.length, new List(2, shared).pos);`,
|
||||
// An array that is never written, shared by several constructors.
|
||||
`const none: Item[] = [];
|
||||
function make(items: Item[]): List {
|
||||
return new List(0, items.length === 0 ? none : items);
|
||||
}
|
||||
const lists: List[] = [];
|
||||
for (let i = 0; i < 2; i++) lists.push(new List(i, none));
|
||||
console.log(lists.length, make([]).items.length, make([new Item(1)]).items.length);`,
|
||||
// A local that each pass of a loop moves and then replaces.
|
||||
`function pairs(n: number): List[] {
|
||||
const lists: List[] = [];
|
||||
let items: Item[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
items.push(new Item(i));
|
||||
if (i % 2 === 1) {
|
||||
lists.push(new List(i, items));
|
||||
items = [];
|
||||
}
|
||||
}
|
||||
return lists;
|
||||
}
|
||||
console.log(pairs(4).length);`,
|
||||
// A top-level array, read elsewhere.
|
||||
`const items = [new Item(1), new Item(2)];
|
||||
function count(): number {
|
||||
return items.length;
|
||||
}
|
||||
console.log(count());
|
||||
const list = new List(0, items);
|
||||
console.log(list.items.length);`,
|
||||
];
|
||||
for (const source of accepted) {
|
||||
const result = await build(classes + source);
|
||||
expect(result.messages).toEqual([]);
|
||||
}
|
||||
const refused = [
|
||||
// Written after the move.
|
||||
`const items = [new Item(1)];
|
||||
const list = new List(0, items);
|
||||
items.push(new Item(2));
|
||||
console.log(list.items.length);`,
|
||||
// Kept in another variable.
|
||||
`const items = [new Item(1)];
|
||||
const kept = items;
|
||||
const list = new List(0, items);
|
||||
kept.push(new Item(2));
|
||||
console.log(list.items.length);`,
|
||||
// A parameter whose caller keeps the array.
|
||||
`function wrap(items: Item[]): List {
|
||||
return new List(0, items);
|
||||
}
|
||||
const items = [new Item(1)];
|
||||
const list = wrap(items);
|
||||
items.push(new Item(2));
|
||||
console.log(list.items.length);`,
|
||||
// A parameter of another class's constructor, and of a method.
|
||||
`class Section {
|
||||
readonly list: List;
|
||||
constructor(items: Item[]) {
|
||||
this.list = new List(0, items);
|
||||
}
|
||||
}
|
||||
const items = [new Item(1)];
|
||||
const section = new Section(items);
|
||||
items.push(new Item(2));
|
||||
console.log(section.list.items.length);`,
|
||||
`class Maker {
|
||||
make(items: Item[]): List {
|
||||
return new List(0, items);
|
||||
}
|
||||
}
|
||||
class FlagMaker extends Maker {
|
||||
override make(items: Item[]): List {
|
||||
return new Flagged(0, items, 1);
|
||||
}
|
||||
}
|
||||
function run(maker: Maker): number {
|
||||
const items = [new Item(1)];
|
||||
const list = maker.make(items);
|
||||
items.push(new Item(2));
|
||||
return list.items.length;
|
||||
}
|
||||
console.log(run(new Maker()), run(new FlagMaker()));`,
|
||||
// A shared array written elsewhere.
|
||||
`const none: Item[] = [];
|
||||
const a = new List(0, none);
|
||||
const b = new List(1, none);
|
||||
function add(items: Item[]): void {
|
||||
items.push(new Item(2));
|
||||
}
|
||||
add(none);
|
||||
console.log(a.items.length, b.items.length);`,
|
||||
// A function returning a shared array.
|
||||
`const all: Item[] = [new Item(1)];
|
||||
function current(): Item[] {
|
||||
return all;
|
||||
}
|
||||
const list = new List(0, current());
|
||||
all.push(new Item(2));
|
||||
console.log(list.items.length);`,
|
||||
];
|
||||
for (const source of refused) {
|
||||
const result = await build(classes + source);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.codes).toContain("SC7009");
|
||||
expect(result.messages.join("\n")).toContain("may be kept elsewhere");
|
||||
}
|
||||
});
|
||||
|
||||
test("an InlineArray field must keep its contract", async () => {
|
||||
const twice = await build(`
|
||||
class Item implements ProgramLifetime {}
|
||||
class List implements ProgramLifetime {
|
||||
readonly items: InlineArray<Item>;
|
||||
constructor(items: readonly Item[], more: readonly Item[]) {
|
||||
this.items = items;
|
||||
if (more.length > 0) (this as { items: readonly Item[] }).items = more;
|
||||
}
|
||||
}
|
||||
console.log(new List([new Item()], []).items.length);
|
||||
`);
|
||||
expect(twice.ok).toBe(false);
|
||||
expect(twice.codes).toContain("SC7009");
|
||||
|
||||
const mortal = await build(`
|
||||
class Item implements ProgramLifetime {}
|
||||
class List {
|
||||
readonly items: InlineArray<Item>;
|
||||
constructor(items: readonly Item[]) {
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
console.log(new List([new Item()]).items.length);
|
||||
`);
|
||||
expect(mortal.ok).toBe(false);
|
||||
expect(mortal.codes).toContain("SC7009");
|
||||
expect(mortal.messages.join("\n")).toContain("not ProgramLifetime");
|
||||
|
||||
const computed = await build(`
|
||||
class Item implements ProgramLifetime {}
|
||||
class List implements ProgramLifetime {
|
||||
readonly items: InlineArray<Item>;
|
||||
constructor(n: number) {
|
||||
const items: Item[] = [];
|
||||
for (let i = 0; i < n; i++) items.push(new Item());
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
console.log(new List(2).items.length);
|
||||
`);
|
||||
expect(computed.ok).toBe(false);
|
||||
expect(computed.codes).toContain("SC7009");
|
||||
expect(computed.messages.join("\n")).toContain("constructor parameter");
|
||||
});
|
||||
|
||||
const holder = `
|
||||
class Holder {
|
||||
readonly list: List;
|
||||
constructor(list: List) {
|
||||
this.list = list;
|
||||
}
|
||||
}
|
||||
const list = new List(3, [new Item(1), new Item(2)]);
|
||||
`;
|
||||
|
||||
test("an instance inside a dynamic value is refused", async () => {
|
||||
for (const use of [
|
||||
"const u: unknown = new Holder(list);\n(u as { list: { items: Item[] } }).list.items.push(new Item(3));",
|
||||
"const u: unknown = [{ list }];\nconsole.log(u);",
|
||||
"const u: unknown = new Map([[1, new Holder(list)]]);\nconsole.log(u);",
|
||||
]) {
|
||||
const result = await build(classes + holder + use);
|
||||
expect(result.ok, use).toBe(false);
|
||||
expect(result.messages.join("\n"), use).toContain(
|
||||
"an instance, or a value that can hold one, becomes a dynamic value",
|
||||
);
|
||||
expect(result.messages.join("\n"), use).toContain("(at ");
|
||||
}
|
||||
});
|
||||
|
||||
test.runIf(lowRegion)("printing and JSON.stringify show the field as an array", async () => {
|
||||
const { ok, llvm } = await build(
|
||||
classes +
|
||||
holder +
|
||||
"console.log(JSON.stringify({ list }), JSON.stringify(new Holder(list)));\nconsole.log(new Holder(list));",
|
||||
);
|
||||
expect(ok).toBe(true);
|
||||
// The instance's dynamic view reads the field as an array of the elements.
|
||||
const views = llvm
|
||||
.split(/\n(?=define )/)
|
||||
.filter((f) => f.startsWith("define") && f.includes("_materialize("));
|
||||
expect(views.some((f) => f.includes("call ptr @scr_arr_view_cref("))).toBe(true);
|
||||
});
|
||||
|
||||
test("the constructor stores the array parameter and does nothing else with it", async () => {
|
||||
const result = await build(`
|
||||
class Item implements ProgramLifetime {}
|
||||
class List implements ProgramLifetime {
|
||||
readonly items: InlineArray<Item>;
|
||||
readonly count: number;
|
||||
constructor(items: Item[]) {
|
||||
items.push(new Item());
|
||||
this.items = items;
|
||||
this.count = items.length;
|
||||
}
|
||||
}
|
||||
console.log(new List([new Item()]).count);
|
||||
`);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.codes).toContain("SC7009");
|
||||
expect(result.messages.join("\n")).toContain("besides storing it in the field");
|
||||
});
|
||||
|
||||
test("a read of an InlineArray field is not compared by identity", async () => {
|
||||
const result = await build(`
|
||||
class Item implements ProgramLifetime {}
|
||||
class List implements ProgramLifetime {
|
||||
readonly items: InlineArray<Item>;
|
||||
constructor(items: readonly Item[]) {
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
const items = [new Item()];
|
||||
const list = new List(items);
|
||||
console.log(list.items === items);
|
||||
`);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.codes).toContain("SC7009");
|
||||
expect(result.messages.join("\n")).toContain("compared by identity");
|
||||
});
|
||||
|
||||
test("arrays a field's reads may be are not compared by identity", async () => {
|
||||
const program = (use: string) => `
|
||||
class Item implements ProgramLifetime {}
|
||||
class List implements ProgramLifetime {
|
||||
readonly items: InlineArray<Item>;
|
||||
constructor(items: readonly Item[]) {
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
const list = new List([new Item()]);
|
||||
const a = list.items;
|
||||
const b = list.items;
|
||||
${use}
|
||||
`;
|
||||
for (const use of [
|
||||
"console.log(a === b);",
|
||||
"console.log([a].indexOf(b));",
|
||||
"console.log(new Set([a]).has(b));",
|
||||
"const seen = new Map<readonly Item[], number>();\nseen.set(a, 1);\nconsole.log(seen.get(b));",
|
||||
// Set elements from a seed.
|
||||
"console.log(new Set([a, b]).size);",
|
||||
"const all: (readonly Item[])[] = [];\nall.push(a);\nall.push(b);\nconsole.log(new Set(all).size);",
|
||||
"const entries: [readonly Item[], number][] = [[a, 1], [b, 2]];\nconsole.log(new Map(entries).size);",
|
||||
"const byId = new Map<number, readonly Item[]>([[1, a], [2, b]]);\nconsole.log(new Set(byId.values()).size);",
|
||||
]) {
|
||||
const result = await build(program(use));
|
||||
expect(result.ok, use).toBe(false);
|
||||
expect(result.messages.join("\n"), use).toContain("is compared by identity");
|
||||
}
|
||||
// Reading the elements and the length, or a Set of the elements, compares
|
||||
// no field arrays.
|
||||
const reads = await build(
|
||||
program("console.log(a.length === b.length, a[0] === b[0], new Set(a).size);"),
|
||||
);
|
||||
expect(reads.ok).toBe(true);
|
||||
});
|
||||
|
||||
test("InlineArray is checked on instance fields only", async () => {
|
||||
const result = await build(`
|
||||
class Item implements ProgramLifetime {}
|
||||
function count(items: InlineArray<Item>): number {
|
||||
return items.length;
|
||||
}
|
||||
console.log(count([new Item()]));
|
||||
`);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.codes).toContain("SC7006");
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/contract-inline-array-fields.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/contract-inline-array-publish.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -116,3 +116,36 @@ scriptc requires the name to be a string literal, every construction of one name
|
||||
In scriptc builds every instance of a key class's hierarchy carries a 4-byte slot index, assigned by the first `set` for it and never reused. `get` reads the index, a page of the thread's family and the entry, with no lock, hash or atomic update. A family holds a reference to each of the thread's values and releases it when it is replaced or deleted, when its key is freed, and when the thread exits. Keys freed with a `LifetimeRegion` keep their values until the thread exits, a value that belongs to a region keeps the region allocated while the family holds it, and a value that refers to its own key keeps the key alive.
|
||||
|
||||
Under Node.js each family is a `WeakMap` per thread.
|
||||
|
||||
## InlineArray
|
||||
|
||||
`InlineArray<T>` marks an array field of a `ProgramLifetime` class (`@scriptc/types`) whose elements scriptc keeps in the instance itself, after its other fields, instead of in an array object of their own. It suits a program with millions of short arrays that never change once built, such as the children of each node of a syntax tree.
|
||||
|
||||
```ts
|
||||
import type { ProgramLifetime } from "@scriptc/types";
|
||||
import type { InlineArray } from "@scriptc/experimental";
|
||||
|
||||
class NodeList implements ProgramLifetime {
|
||||
readonly pos: number;
|
||||
readonly nodes: InlineArray<Node>;
|
||||
constructor(pos: number, nodes: readonly Node[]) {
|
||||
this.pos = pos;
|
||||
this.nodes = nodes;
|
||||
}
|
||||
}
|
||||
|
||||
function visit(list: NodeList): void {
|
||||
const nodes = list.nodes;
|
||||
for (let i = 0; i < nodes.length; i++) visitNode(nodes[i]!);
|
||||
}
|
||||
```
|
||||
|
||||
- `T` is a `ProgramLifetime` class, and the field is an instance field of a `ProgramLifetime` class.
|
||||
- The constructor of the class that declares the field writes it once, from one of its parameters. A subclass constructor passes one of its own parameters there through `super(...)`. Nothing else writes the field.
|
||||
- Indexing the field, reading its `length` and iterating it with `for...of` read the elements from the instance, directly or through a `const` local used only in those ways. Any other use, such as passing the field to a function or calling one of its methods, gets an array of the elements, which is frozen in scriptc builds.
|
||||
- The array passed to the constructor is one that nothing else keeps: a new array (a literal, a copy, or the result of a function that returns only such arrays or its own argument), a local array at its last use, a parameter whose callers each pass such an array, an array that an InlineArray field already holds, or a `const` array that is never written.
|
||||
- The arrays fields hold are never written, through a read of the field (a cast) or through any other reference. scriptc follows them through locals, parameters, return values, fields, the elements of arrays, Maps and Sets, and closures, and reports one that reaches a write, a dynamic value (also inside the fields of an object that becomes one), a call through a function value (also inside an array, Map or Set passed there), or a library call other than printing and `JSON.stringify` without a replacer.
|
||||
- Two reads of the field need not give the same array, so the arrays fields hold are never compared by identity: with `===` or `!==`, as an `indexOf` or `includes` argument, or as a Map key or Set element. Compare the elements instead, or keep a copy made with `slice()` where identity matters.
|
||||
- Instances of the class are not constructed through a class value, and do not become dynamic values (for example through a conversion to `unknown`), directly or inside another value that can hold one: an instance of a class with such a field, a record, array, Map or Set, or a value typed as a base class. A dynamic view reads the field as a new array each time, so a class whose instances can become dynamic values cannot hold an InlineArray field. Printing, `JSON.stringify` without a replacer, `publish` and a worker's `workerData` only read or copy the value and are allowed.
|
||||
|
||||
scriptc reports a field that does not keep these rules (SC7009). Under Node.js the field holds the array the constructor stored. In builds with sanitizers, in programs that use `LifetimeRegion`, and on platforms other than 64-bit Linux, the elements stay in an ordinary array object.
|
||||
|
||||
Vendored
+10
@@ -74,3 +74,13 @@ export declare class ThreadSlots<K extends object, V extends object> {
|
||||
/** Removes this thread's value for `key`. Returns whether there was one. */
|
||||
delete(key: K): boolean;
|
||||
}
|
||||
|
||||
/** An array field whose elements scriptc keeps in the instance itself
|
||||
* (@scriptc/types ProgramLifetime classes): `readonly f: InlineArray<C>`,
|
||||
* where C is a ProgramLifetime class. The class's constructor writes the
|
||||
* field once, from one of its parameters, and nothing else writes it.
|
||||
* The constructor is passed an array nothing else keeps, and the arrays
|
||||
* fields hold are never written. Reading the field gives an array of its
|
||||
* elements, frozen in scriptc builds; two reads need not give the same
|
||||
* array. Under Node.js it is the array the constructor stored. */
|
||||
export type InlineArray<T> = readonly T[];
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
// thread: Node.js threads share no objects, so every thread has its own
|
||||
// tables, and stored values are published (deep-frozen) as in scriptc.
|
||||
// ThreadSlots is a WeakMap per name and thread: module state is per thread
|
||||
// under Node.js, so each thread sees only the values it set.
|
||||
// under Node.js, so each thread sees only the values it set. InlineArray is
|
||||
// a type (`import type`), with no value here: under Node.js the field holds
|
||||
// the array its constructor stored.
|
||||
|
||||
import { publish } from "@scriptc/threads";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@scriptc/experimental",
|
||||
"version": "0.4.5",
|
||||
"description": "Experimental scriptc APIs, with Node.js implementations: lifetime regions for ProgramLifetime objects, intern tables shared between threads, and per-thread slots on shared objects",
|
||||
"description": "Experimental scriptc APIs, with Node.js implementations: lifetime regions for ProgramLifetime objects, intern tables shared between threads, per-thread slots on shared objects, and array fields kept in their instances",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
|
||||
@@ -1694,6 +1694,56 @@ ScrArr *scr_arr_new_ops_r(const ScrArrOps *ops, size_t initial_cap) {
|
||||
|
||||
ScrArr *scr_arr_new_ops_small_r(const ScrArrOps *ops) { return scr_arr_make_ops(ops, 0, true, true); }
|
||||
|
||||
/* An inline array field's elements as an array value (llvm/inline-arrays.ts).
|
||||
* The field keeps `len` 32-bit references to compact instances at `slots`, in
|
||||
* a program-lifetime instance, and never changes them. The header borrows
|
||||
* them: SCR_ARR_INLINE_SLOTS keeps scr_arr_free_parts from freeing them and
|
||||
* SCR_ARR_FROZEN keeps every write, and so every move, away from them.
|
||||
* Element types whose ops lie past the registry keep their ops pointer in
|
||||
* the word before the slots (scr_arr_ext), which belongs to the instance:
|
||||
* those get a frozen copy instead. */
|
||||
ScrArr *scr_arr_view_cref(const ScrArrOps *ops, uint32_t *slots, uint32_t len) {
|
||||
ScrArr *a = scr_arr_make_ops_kind(ops, 0, false, SCR_ELEM_CREF, false);
|
||||
if (scr_arr_ops_id(ops) == SCR_ARR_OPS_OVERFLOW) {
|
||||
if (len > 0) {
|
||||
scr_arr_grow_dense_to(a, len, SCR_ARR_GROW_EXACT);
|
||||
memcpy(a->data, slots, (size_t)len * sizeof(uint32_t));
|
||||
}
|
||||
} else {
|
||||
a->data = (uint64_t *)slots;
|
||||
a->cap = len;
|
||||
a->spare |= SCR_ARR_INLINE_SLOTS;
|
||||
}
|
||||
a->len = len;
|
||||
a->filled = len;
|
||||
a->spare |= SCR_ARR_FROZEN;
|
||||
return a;
|
||||
}
|
||||
|
||||
/* The constructor's write of an inline array field: the `len` elements of
|
||||
* `src`, an array of the field's element class, as 32-bit references at
|
||||
* `slots`. `len` is the length the instance was allocated for, read from the
|
||||
* same constructor argument, so another length, a hole or an element that
|
||||
* is not a compact instance means the program broke the field's contract. */
|
||||
void scr_arr_copy_cref_tail(const ScrArr *src, uint32_t *slots, uint32_t len) {
|
||||
if (src->len != len)
|
||||
scr_trap("scriptc: an inline array field's array changed length during construction\n");
|
||||
if (len == 0) return;
|
||||
if (src->filled < len) scr_trap("scriptc: an inline array field's array has holes\n");
|
||||
if (src->elem == SCR_ELEM_CREF) {
|
||||
memcpy(slots, src->data, (size_t)len * sizeof(uint32_t));
|
||||
return;
|
||||
}
|
||||
if (src->elem != SCR_ELEM_REF)
|
||||
scr_trap("scriptc: an inline array field's array holds other values\n");
|
||||
for (uint32_t i = 0; i < len; i++) {
|
||||
uint64_t slot = src->data[i];
|
||||
if (slot == 0)
|
||||
scr_trap("scriptc: an inline array field's array holds a missing element\n");
|
||||
slots[i] = scr_cref_encode(slot);
|
||||
}
|
||||
}
|
||||
|
||||
/* Frees an acyclic array whose elements are already released. */
|
||||
static inline __attribute__((always_inline)) void scr_arr_free(ScrArr *a) {
|
||||
scr_arr_free_parts(a);
|
||||
|
||||
@@ -481,6 +481,10 @@ ScrArr *scr_arr_new(ScrElemKind elem, size_t initial_cap); /* returns +1 */
|
||||
* header (the array then allocates with the collector header — see the
|
||||
* struct comment). Returns +1. */
|
||||
ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap);
|
||||
/* Inline array fields (scr_array.c): a frozen array over a field's 32-bit
|
||||
* references, and the constructor's copy of an array into them. */
|
||||
ScrArr *scr_arr_view_cref(const ScrArrOps *ops, uint32_t *slots, uint32_t len);
|
||||
void scr_arr_copy_cref_tail(const ScrArr *src, uint32_t *slots, uint32_t len);
|
||||
/* An empty array expected to receive a few elements: its first slots live
|
||||
* in the array's own block (one allocation; the block of a plain empty
|
||||
* array plus five 8-byte slots). Otherwise exactly scr_arr_new(elem, 0) and
|
||||
|
||||
@@ -25,4 +25,4 @@ function isLetter(ch: number): NoThrow<boolean> {
|
||||
- `Acyclic` on a class (`class C implements Acyclic`), interface (`interface I extends Acyclic`) or object type alias (`type T = Acyclic<{ ... }>`): no chain of references can lead from such an object back to itself. Its objects carry no cycle-collector header.
|
||||
- `ProgramLifetime` in a class's `implements` clause: instances of the class and its subclasses are never freed, so scriptc skips reference counting and cycle collection for them. Memory only: discarded instances are not reclaimed.
|
||||
|
||||
scriptc proves each contract or rejects the program with a diagnostic (SC7001-SC7008) that explains why it does not hold. A program that compiles behaves exactly as under Node.js. See https://scriptc.dev/docs/contracts.
|
||||
scriptc proves each contract or rejects the program with a diagnostic (SC7001-SC7009) that explains why it does not hold. A program that compiles behaves exactly as under Node.js. See https://scriptc.dev/docs/contracts.
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import type { ProgramLifetime } from "@scriptc/types";
|
||||
import type { InlineArray } from "@scriptc/experimental";
|
||||
|
||||
// InlineArray fields (@scriptc/experimental): a ProgramLifetime class whose
|
||||
// constructor writes an array field once from one of its parameters may keep
|
||||
// the elements in the instance itself. Reads that index the field, read its
|
||||
// length or iterate it load the elements from the instance; other reads see
|
||||
// an array of them. Both runtimes print the same.
|
||||
|
||||
abstract class Node implements ProgramLifetime {
|
||||
readonly kind: string;
|
||||
readonly pos: number;
|
||||
constructor(kind: string, pos: number) {
|
||||
this.kind = kind;
|
||||
this.pos = pos;
|
||||
}
|
||||
abstract text(): string;
|
||||
}
|
||||
|
||||
class Leaf extends Node {
|
||||
readonly value: number;
|
||||
constructor(pos: number, value: number) {
|
||||
super("leaf", pos);
|
||||
this.value = value;
|
||||
}
|
||||
text(): string {
|
||||
return String(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
class List implements ProgramLifetime {
|
||||
readonly pos: number;
|
||||
readonly end: number;
|
||||
readonly nodes: InlineArray<Node>;
|
||||
constructor(pos: number, end: number, nodes: readonly Node[]) {
|
||||
this.pos = pos | 0;
|
||||
this.end = end | 0;
|
||||
this.nodes = nodes;
|
||||
}
|
||||
hasTrailingComma(): boolean {
|
||||
const nodes = this.nodes;
|
||||
return nodes.length > 0 && nodes[nodes.length - 1]!.pos < this.end;
|
||||
}
|
||||
}
|
||||
|
||||
// A subclass with the inherited constructor.
|
||||
class MissingList extends List {}
|
||||
|
||||
// A subclass with fields of its own, which passes its parameter through.
|
||||
class ModifierList extends List {
|
||||
readonly flags: number;
|
||||
readonly label: string;
|
||||
constructor(pos: number, end: number, nodes: readonly Node[], flags: number, label: string) {
|
||||
super(pos, end, nodes);
|
||||
this.flags = flags;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
|
||||
class Group extends Node {
|
||||
readonly children: List;
|
||||
constructor(pos: number, children: List) {
|
||||
super("group", pos);
|
||||
this.children = children;
|
||||
}
|
||||
text(): string {
|
||||
const parts: string[] = [];
|
||||
for (const child of this.children.nodes) parts.push(child.text());
|
||||
return `(${parts.join(" ")})`;
|
||||
}
|
||||
}
|
||||
|
||||
function leaves(from: number, count: number): Leaf[] {
|
||||
const out: Leaf[] = [];
|
||||
for (let i = 0; i < count; i++) out.push(new Leaf(from + i, (from + i) * 3));
|
||||
return out;
|
||||
}
|
||||
|
||||
// Direct reads: index, length, a local alias, for...of.
|
||||
function sum(list: List): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < list.nodes.length; i++) s += (list.nodes[i] as Leaf).value;
|
||||
const nodes = list.nodes;
|
||||
for (let i = 0; i < nodes.length; i++) s += nodes[i]!.pos;
|
||||
for (const node of list.nodes) s += node.pos * 2;
|
||||
return s;
|
||||
}
|
||||
|
||||
// The field read as an array value.
|
||||
function describe(nodes: readonly Node[]): string {
|
||||
return nodes.map((n) => n.text()).join(",");
|
||||
}
|
||||
|
||||
function depth(node: Node): number {
|
||||
if (!(node instanceof Group)) return 1;
|
||||
let max = 0;
|
||||
for (const child of node.children.nodes) max = Math.max(max, depth(child));
|
||||
return max + 1;
|
||||
}
|
||||
|
||||
const lengths = [0, 1, 2, 3, 5, 16, 17, 40];
|
||||
const lists: List[] = [];
|
||||
for (const n of lengths) lists.push(new List(n, n * 10, leaves(n, n)));
|
||||
for (const list of lists)
|
||||
console.log(
|
||||
list.nodes.length,
|
||||
sum(list),
|
||||
list.hasTrailingComma(),
|
||||
describe(list.nodes).slice(0, 40),
|
||||
);
|
||||
|
||||
// Subclasses share the elements' layout.
|
||||
const missing = new MissingList(5, 5, []);
|
||||
const modifiers = new ModifierList(1, 9, leaves(1, 4), 7, "mods");
|
||||
const lists2: List[] = [missing, modifiers];
|
||||
for (const list of lists2)
|
||||
console.log(
|
||||
list instanceof ModifierList ? "ModifierList" : list instanceof MissingList ? "MissingList" : "List",
|
||||
list.nodes.length,
|
||||
sum(list),
|
||||
list.pos,
|
||||
list.end,
|
||||
);
|
||||
console.log(modifiers.flags, modifiers.label, modifiers.nodes[3]!.text());
|
||||
|
||||
// Array methods over the field's elements, and element identity.
|
||||
const big = lists[lists.length - 1]!;
|
||||
const third = big.nodes[2]!;
|
||||
console.log(big.nodes.indexOf(third), big.nodes.includes(third), big.nodes[2] === third);
|
||||
console.log(big.nodes.slice(3, 6).map((n) => n.pos).join("|"));
|
||||
console.log(big.nodes.filter((n) => n.pos % 7 === 0).length);
|
||||
console.log([...big.nodes].reverse()[0]!.pos, Array.from(big.nodes).length);
|
||||
console.log(big.nodes.some((n) => n.pos === 77), big.nodes.find((n) => n.pos > 70)?.pos);
|
||||
|
||||
// A list built from another list's elements, and from a slice of them.
|
||||
const copy = new List(0, 0, big.nodes);
|
||||
const part = new ModifierList(0, 0, big.nodes.slice(10, 13), 1, "part");
|
||||
console.log(copy.nodes.length, copy.nodes[39] === big.nodes[39], sum(part), part.nodes[0]!.pos);
|
||||
|
||||
// Inside a value that becomes dynamic, the field shows as an array.
|
||||
class Holder {
|
||||
readonly list: List;
|
||||
constructor(list: List) {
|
||||
this.list = list;
|
||||
}
|
||||
}
|
||||
const shown = new ModifierList(1, 2, leaves(7, 2), 3, "shown");
|
||||
console.log(JSON.stringify({ shown }));
|
||||
console.log(JSON.stringify(new Holder(shown)));
|
||||
|
||||
// The arguments of `new` run before the instance is made, so an argument
|
||||
// that adds to the array the list is built from adds to the list.
|
||||
const growing = leaves(50, 2);
|
||||
const grown = new List(growing.push(new Leaf(52, 1)), 0, growing);
|
||||
console.log(grown.pos, grown.nodes.length, grown.nodes[2]!.pos);
|
||||
|
||||
// Trees whose groups hold lists.
|
||||
function tree(level: number, pos: number): Node {
|
||||
if (level === 0) return new Leaf(pos, pos);
|
||||
const kids: Node[] = [];
|
||||
for (let i = 0; i < level + 1; i++) kids.push(tree(level - 1, pos * 4 + i));
|
||||
return new Group(pos, new List(pos, pos + kids.length, kids));
|
||||
}
|
||||
const root = tree(4, 1);
|
||||
console.log(depth(root), root.text().length, root.text().slice(0, 60));
|
||||
|
||||
// Loops over lists built from arrays that may have holes, in a function that
|
||||
// only reads: each loop keeps its own array of the elements for the whole
|
||||
// loop, also while an inner loop makes another.
|
||||
function holed(nodes: readonly Node[]): Node[] {
|
||||
const out: Node[] = new Array<Node>(nodes.length);
|
||||
for (let i = 0; i < nodes.length; i++) out[i] = nodes[i]!;
|
||||
return out;
|
||||
}
|
||||
function leafSum(group: Group): number {
|
||||
let n = 0;
|
||||
for (const child of group.children.nodes)
|
||||
if (child instanceof Group) for (const leaf of child.children.nodes) n += leaf.pos;
|
||||
return n;
|
||||
}
|
||||
const nested = new Group(
|
||||
0,
|
||||
new List(0, 2, holed([new Group(1, new List(1, 1, holed(leaves(10, 3)))), new Group(2, new List(2, 2, holed(leaves(20, 3))))])),
|
||||
);
|
||||
console.log(leafSum(nested), leafSum(nested));
|
||||
|
||||
// Many lists: every read stays on the instance.
|
||||
let total = 0;
|
||||
for (let round = 0; round < 2000; round++) {
|
||||
const list = new List(round, round + 3, leaves(round, round % 9));
|
||||
total += sum(list) + list.nodes.length;
|
||||
if (list.nodes.length > 0) total += list.nodes.indexOf(list.nodes[list.nodes.length - 1]!);
|
||||
}
|
||||
console.log(total);
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
||||
import { publish } from "@scriptc/threads";
|
||||
import type { ProgramLifetime } from "@scriptc/types";
|
||||
import type { InlineArray } from "@scriptc/experimental";
|
||||
|
||||
// InlineArray fields of ProgramLifetime instances published with
|
||||
// @scriptc/threads. scriptc shares the published graph by reference, with
|
||||
// the elements in the instances; Node.js clones it for the worker. The
|
||||
// worker reads fields only (a Node.js clone has no prototype), so both
|
||||
// runtimes print the same.
|
||||
|
||||
class Node implements ProgramLifetime {
|
||||
readonly kind: string;
|
||||
readonly pos: number;
|
||||
readonly children: NodeList;
|
||||
constructor(kind: string, pos: number, children: NodeList) {
|
||||
this.kind = kind;
|
||||
this.pos = pos;
|
||||
this.children = children;
|
||||
}
|
||||
}
|
||||
|
||||
class NodeList implements ProgramLifetime {
|
||||
readonly pos: number;
|
||||
readonly nodes: InlineArray<Node>;
|
||||
constructor(pos: number, nodes: readonly Node[]) {
|
||||
this.pos = pos;
|
||||
this.nodes = nodes;
|
||||
}
|
||||
}
|
||||
|
||||
class Modifiers extends NodeList {
|
||||
readonly flags: number;
|
||||
constructor(pos: number, nodes: readonly Node[], flags: number) {
|
||||
super(pos, nodes);
|
||||
this.flags = flags;
|
||||
}
|
||||
}
|
||||
|
||||
const none = new NodeList(-1, []);
|
||||
|
||||
function tree(level: number, pos: number): Node {
|
||||
const kids: Node[] = [];
|
||||
if (level > 0) for (let i = 0; i < level + 1; i++) kids.push(tree(level - 1, pos * 4 + i));
|
||||
const list = level % 2 === 0 ? new Modifiers(pos, kids, level) : new NodeList(pos, kids);
|
||||
return new Node(level === 0 ? "leaf" : "group", pos, kids.length === 0 ? none : list);
|
||||
}
|
||||
|
||||
function summary(node: Node): string {
|
||||
const nodes = node.children.nodes;
|
||||
if (nodes.length === 0) return `${node.kind}@${node.pos}`;
|
||||
const parts: string[] = [];
|
||||
for (let i = 0; i < nodes.length; i++) parts.push(summary(nodes[i]!));
|
||||
return `${node.kind}@${node.pos}[${parts.join(" ")}]`;
|
||||
}
|
||||
|
||||
function count(node: Node): number {
|
||||
let n = 1;
|
||||
for (const child of node.children.nodes) n += count(child);
|
||||
return n;
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
const root = publish(tree(4, 1));
|
||||
console.log("main", count(root), summary(root).length, summary(root).slice(0, 80));
|
||||
const worker = new Worker(new URL(import.meta.url), { workerData: root });
|
||||
worker.on("message", (line: string) => console.log(line));
|
||||
worker.on("exit", (code) => console.log("worker exit", code));
|
||||
} else {
|
||||
const root = workerData as Node;
|
||||
const first = root.children.nodes[0]!;
|
||||
parentPort!.postMessage(
|
||||
`worker ${count(root)} ${summary(root).length} ${first.children.nodes.length} ${summary(first).slice(0, 60)}`,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 7100, "san": 5100 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4500, "san": 3900 }
|
||||
@@ -313,6 +313,8 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => {
|
||||
// ThreadSlots blocks and pages hold 32-bit pointers.
|
||||
"5727-thread-slots-basics.ts",
|
||||
"5729-thread-slots-values.ts",
|
||||
// InlineArray fields stay ordinary arrays with 32-bit pointers.
|
||||
"contract-inline-array-fields.ts",
|
||||
// Weak-collection hashes mix 32-bit pointers in 64 bits.
|
||||
"4591-weak-keys-union-records.ts",
|
||||
"7742-weak-collections-many-keys.ts",
|
||||
|
||||
Reference in New Issue
Block a user