mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Prove that the arrays InlineArray fields hold are never written
The constructor copies the array it is passed into the instance, and other reads of the field give a new array over the elements, while under Node.js the field holds that array itself. The contract check now proves the two agree, and reports SC7009 where it cannot: - The arrays fields hold are never written. A whole-program graph of the places arrays are held (locals, parameters, results, class and record fields, and the elements of arrays, Maps and Sets by where those are held) follows every array a field read may give, and reports a write to one, or one that reaches a dynamic value, a call through a function value or a library call other than printing and JSON.stringify without a replacer. Functions that return their argument give each caller its own argument back. The report names the field read. - The array passed to the constructor is one nothing else keeps: a new array (a literal, a copy, or the result of a function that returns only such arrays or its argument), a local array at its last use (after calls that only read it or write it in place, or in a loop that then assigns the local a new array), a parameter whose every caller passes such an array, an array a field already holds, or a const array that is never written, which is then followed like a field's. The review fixes from before stay: `new` evaluates its arguments into locals unless they are all locals and constants, so that the allocation reads the length after every argument has run; the constructor uses the array parameter only to store it; and the dynamic view of an instance reads the field as an array of its elements.
This commit is contained in:
@@ -368,7 +368,7 @@ A long-running program, such as a compiler in watch mode, can free program-lifet
|
||||
|
||||
### 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, which is frozen in scriptc builds. scriptc reports a field that does not keep these rules (SC7009).
|
||||
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
|
||||
|
||||
|
||||
@@ -388,14 +388,16 @@ 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, a call through a function value, or a library call other than printing and `JSON.stringify` without a replacer. The report names the field read the array may come from;
|
||||
- reads of the field not to be compared with `===` or `!==`: two reads need not give the same array. Compare a read kept in a local instead;
|
||||
- instances of the class not to be constructed through a class value, and not to become dynamic values (for example through `console.log` or `JSON.stringify`).
|
||||
- instances of the class not to be constructed through a class value, and not to become dynamic values (for example through `JSON.stringify` or a conversion to `unknown`).
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
|
||||
@@ -464,6 +464,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".
|
||||
@@ -940,6 +946,8 @@ function undefFieldInits(
|
||||
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");
|
||||
|
||||
@@ -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";
|
||||
@@ -1622,6 +1624,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();
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
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;
|
||||
@@ -379,6 +380,17 @@ class ContractChecker {
|
||||
"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]);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,8 +14,9 @@
|
||||
*
|
||||
* `inlineArrayReads` then prepares the IR for the layout:
|
||||
* - a `new` of such a class evaluates its arguments into locals first, in
|
||||
* order, when the length argument is not already a local, so that the
|
||||
* backend can read the length before it allocates;
|
||||
* 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
|
||||
@@ -220,9 +221,26 @@ function parameterOf(fn: IrFunction, value: IrExpr): number | string {
|
||||
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,
|
||||
@@ -435,10 +453,15 @@ function rewriteFunction(fn: IrFunction, plan: InlineArrayPlan): IrFunction {
|
||||
|
||||
/** A `new` of a planned class whose length argument is not a local read. */
|
||||
function newNeedsLocals(e: IrExpr & { kind: "new" }, plan: InlineArrayPlan): boolean {
|
||||
const cls = plan.classes.get(e.className);
|
||||
if (cls === undefined) return false;
|
||||
const arg = e.args[cls.lengthArg];
|
||||
return arg !== undefined && arg.kind !== "varRef";
|
||||
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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
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";
|
||||
@@ -7,6 +8,8 @@ import { compile } from "../src/index.js";
|
||||
// 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";
|
||||
@@ -15,9 +18,10 @@ import type { InlineArray } from "@scriptc/experimental";
|
||||
async function build(
|
||||
source: string,
|
||||
): Promise<{ ok: boolean; codes: string[]; llvm: string; messages: string[] }> {
|
||||
// Inside the workspace, so that the program resolves both packages.
|
||||
const dir = await mkdtemp(join(import.meta.dirname, ".inline-array-"));
|
||||
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);
|
||||
@@ -159,6 +163,235 @@ console.log(use(new List(0, [new Item(1)])));
|
||||
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("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 {}
|
||||
@@ -205,6 +438,50 @@ console.log(new List(2).items.length);
|
||||
expect(computed.messages.join("\n")).toContain("constructor parameter");
|
||||
});
|
||||
|
||||
test.runIf(lowRegion)(
|
||||
"an instance inside a dynamic value shows the field as an array",
|
||||
async () => {
|
||||
const { ok, llvm } = await build(
|
||||
classes +
|
||||
`
|
||||
class Holder {
|
||||
readonly list: List;
|
||||
constructor(list: List) {
|
||||
this.list = list;
|
||||
}
|
||||
}
|
||||
const list = new List(3, [new Item(1), new Item(2)]);
|
||||
console.log(JSON.stringify({ list }), JSON.stringify(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 {}
|
||||
|
||||
@@ -143,7 +143,9 @@ function visit(list: NodeList): void {
|
||||
- `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, a call through a function value, 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 a read of the field is never compared with `===` or `!==`. Compare a read kept in a local instead.
|
||||
- Instances of the class are not constructed through a class value, and do not become dynamic values (for example through `console.log` or `JSON.stringify`).
|
||||
- Instances of the class are not constructed through a class value, and do not become dynamic values (for example through `JSON.stringify` or a conversion to `unknown`).
|
||||
|
||||
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
+4
-3
@@ -79,7 +79,8 @@ export declare class ThreadSlots<K extends object, V extends object> {
|
||||
* (@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.
|
||||
* 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. */
|
||||
* 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[];
|
||||
|
||||
@@ -137,6 +137,23 @@ 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);
|
||||
|
||||
Reference in New Issue
Block a user