mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-30 07:14:39 +08:00
Merge main into perf/int32-or-zero-peephole
This commit is contained in:
@@ -54,7 +54,7 @@ fib.s
|
||||
|
||||
Different output kinds accumulate in `.scriptc/`; rebuilding a kind updates its file.
|
||||
|
||||
The IR and LLVM source outputs require only Node. On supported macOS, Linux, and Windows hosts, assembly and object outputs use the matching native helper installed with scriptc and do not invoke an external compiler, archiver, linker, or SDK. The object is a relocatable program object with undefined <code>scr_*</code> runtime symbols and a required <code>scr_runtime_abi_v12</code> marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by <code>--print=native-link-info</code>. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, <code>main</code> entry, installed precompiled runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See <a href="/native-objects">Native Program Objects</a> for complete C-driver and direct-linker examples. <code>--emit=exe</code> is the default. LLVM builds emit the program object through the helper and link precompiled runtime objects; runtime development with AddressSanitizer compiles an instrumented C runtime.
|
||||
The IR and LLVM source outputs require only Node. On supported macOS, Linux, and Windows hosts, assembly and object outputs use the matching native helper installed with scriptc and do not invoke an external compiler, archiver, linker, or SDK. The object is a relocatable program object with undefined <code>scr_*</code> runtime symbols and a required <code>scr_runtime_abi_v13</code> marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by <code>--print=native-link-info</code>. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, <code>main</code> entry, installed precompiled runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See <a href="/native-objects">Native Program Objects</a> for complete C-driver and direct-linker examples. <code>--emit=exe</code> is the default. LLVM builds emit the program object through the helper and link precompiled runtime objects; runtime development with AddressSanitizer compiles an instrumented C runtime.
|
||||
|
||||
## scriptc run
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ Without `-o`, artifacts are written to `.scriptc/` next to the input. Different
|
||||
|
||||
### External linking
|
||||
|
||||
Program objects define `main` and leave selected `scr_*` runtime functions undefined. A strong reference to `scr_runtime_abi_v12` checks runtime compatibility at link time.
|
||||
Program objects define `main` and leave selected `scr_*` runtime functions undefined. A strong reference to `scr_runtime_abi_v13` checks runtime compatibility at link time.
|
||||
|
||||
`--print=native-link-info` emits the program object and a JSON recipe containing the runtime pack and link order for external builds. This object ABI is experimental and requires an exact runtime version. See [Native Program Objects](/docs/native-objects) for examples.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ title: "Native Program Objects"
|
||||
|
||||
The external object ABI is **experimental**. Its `scr_*` function and data surface may change before 1.0, so consumers must use the exact runtime-pack version reported by the same compiler installation. This is stricter than semver compatibility.
|
||||
|
||||
The object intentionally leaves its selected runtime symbols undefined. It also holds a strong reference to `scr_runtime_abi_v12`, which the matching runtime defines. Linking an object against a runtime with another ABI marker fails at link time with the missing versioned symbol; it cannot become a latent runtime incompatibility.
|
||||
The object intentionally leaves its selected runtime symbols undefined. It also holds a strong reference to `scr_runtime_abi_v13`, which the matching runtime defines. Linking an object against a runtime with another ABI marker fails at link time with the missing versioned symbol; it cannot become a latent runtime incompatibility.
|
||||
|
||||
## Machine-readable link information
|
||||
|
||||
|
||||
@@ -15,4 +15,4 @@ $ ./app-ld
|
||||
|
||||
`cc` uses the C compiler as a linker driver. `ld` links the same precompiled runtime objects directly with the selected Apple SDK. The object defines `main`; it is a complete program object, not a library to load into another process. Use `scriptc build --lib --profile ...` for a host-callable static library.
|
||||
|
||||
The external object ABI is experimental. Always consume the runtime pack at the exact `runtime_pack.version` reported by the same scriptc installation. The object requires `scr_runtime_abi_v12`, so a mismatched runtime fails during the link instead of starting with an incompatible ABI.
|
||||
The external object ABI is experimental. Always consume the runtime pack at the exact `runtime_pack.version` reported by the same scriptc installation. The object requires `scr_runtime_abi_v13`, so a mismatched runtime fails during the link instead of starting with an incompatible ABI.
|
||||
|
||||
@@ -34,7 +34,7 @@ Builds use a bounded persistent cache. Unchanged source can reuse the validated
|
||||
- `scriptc run <file.ts>` — compile and run
|
||||
- `scriptc coverage <file.ts>` — what compiles statically, and why the rest doesn't
|
||||
|
||||
`scriptc build app.ts --emit=ir|llvm|asm|obj` selects serialized typed IR, textual LLVM IR, target assembly, or a relocatable program object as the one primary artifact. `--emit=exe` is the default. Assembly/object emission uses the matching helper on supported macOS, Linux, and Windows hosts (and produces WASI artifacts when that target is selected). Objects retain undefined `scr_*` runtime references plus the `scr_runtime_abi_v12` compatibility marker; they are not library archives. External consumption is experimental and requires the exact matching runtime. `scriptc build app.ts --print=native-link-info -o app.o` prints the versioned JSON target/runtime/link recipe without performing a link. `--emit=asm|obj --sanitize` is rejected until ASan pipeline parity is available.
|
||||
`scriptc build app.ts --emit=ir|llvm|asm|obj` selects serialized typed IR, textual LLVM IR, target assembly, or a relocatable program object as the one primary artifact. `--emit=exe` is the default. Assembly/object emission uses the matching helper on supported macOS, Linux, and Windows hosts (and produces WASI artifacts when that target is selected). Objects retain undefined `scr_*` runtime references plus the `scr_runtime_abi_v13` compatibility marker; they are not library archives. External consumption is experimental and requires the exact matching runtime. `scriptc build app.ts --print=native-link-info -o app.o` prints the versioned JSON target/runtime/link recipe without performing a link. `--emit=asm|obj --sanitize` is rejected until ASan pipeline parity is available.
|
||||
|
||||
For embedder-hosted modules that are not installed npm packages, coverage can map an exact bare specifier to a local declaration with repeatable `--external-types <specifier=file.d.ts>` options. This is analysis-only: the types unblock application measurement, while runtime module uses remain reported as blockers.
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 12, marker: "scr_runtime_abi_v12" },
|
||||
runtime_abi: { version: 13, marker: "scr_runtime_abi_v13" },
|
||||
runtime_pack: {
|
||||
kind: "precompiled",
|
||||
package: "@scriptc/runtime-darwin-arm64",
|
||||
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
() => null,
|
||||
(failure: { stderr?: string }) => failure,
|
||||
);
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v12");
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v13");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -292,14 +292,11 @@ export function constructorThisAliases(fn: IrFunction, thisId: string): Set<stri
|
||||
* field stores and reads (the body length when it never is). Statements run
|
||||
* in order, so every store in an earlier statement completes while the
|
||||
* object is still private. Within and after the escaping statement nothing
|
||||
* is private any more. With `selfStores`, storing `this` into one of its
|
||||
* own fields is not an escape (it exposes nothing to other code, but it
|
||||
* does create a cycle). */
|
||||
* is private any more. */
|
||||
export function constructorEscapePoint(
|
||||
fn: IrFunction,
|
||||
thisId: string,
|
||||
baseCtor: string | null,
|
||||
options: { selfStores?: boolean } = {},
|
||||
): number {
|
||||
let escaped = false;
|
||||
const aliases = constructorThisAliases(fn, thisId);
|
||||
@@ -338,14 +335,7 @@ export function constructorEscapePoint(
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "fieldSet" && isThisRef(s.obj)) {
|
||||
if (options.selfStores === true) {
|
||||
// `this` itself, possibly wrapped into the field's union type.
|
||||
const self = s.value.kind === "unionWrap" ? s.value.value : s.value;
|
||||
if (isThisRef(self)) return wrapper(s.obj) && wrapper(self);
|
||||
}
|
||||
return wrapper(s.obj) && expr(s.value);
|
||||
}
|
||||
if (s.kind === "fieldSet" && isThisRef(s.obj)) return wrapper(s.obj) && expr(s.value);
|
||||
if (s.kind === "exprStmt" && isThisRef(s.expr)) return wrapper(s.expr);
|
||||
// Declaring an alias is not an escape; its uses are checked like `this`.
|
||||
if (s.kind === "varDecl" && aliases.has(s.localId) && s.init !== null) return wrapper(s.init);
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
DYN,
|
||||
F64,
|
||||
STRING,
|
||||
VOID,
|
||||
type IrExpr,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "../ir/ir.js";
|
||||
import { computeConstructionFacts, computeImmutableFields, stableGlobals } from "./immutability.js";
|
||||
|
||||
const loc = { file: "immutability.ts", start: 0, end: 1 };
|
||||
const obj = (className: string): IrType => ({ kind: "object", className });
|
||||
const value = (type: IrType): IrExpr => ({ kind: "varRef", localId: "v", type, loc });
|
||||
const thisRef = (className: string): IrExpr => ({
|
||||
kind: "varRef",
|
||||
localId: "this.0",
|
||||
type: obj(className),
|
||||
loc,
|
||||
});
|
||||
const setThis = (className: string, field: string): IrStmt => ({
|
||||
kind: "fieldSet",
|
||||
obj: thisRef(className),
|
||||
className,
|
||||
field,
|
||||
value: value(F64),
|
||||
loc,
|
||||
});
|
||||
const call = (callee: string, args: IrExpr[]): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "call", callee, args, type: VOID, loc },
|
||||
loc,
|
||||
});
|
||||
function ctor(className: string, body: IrStmt[]): IrModule["functions"][number] {
|
||||
return {
|
||||
name: `%${className}.constructor`,
|
||||
params: [{ localId: "this.0", name: "this", type: obj(className) }],
|
||||
locals: [],
|
||||
returnType: VOID,
|
||||
body,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
function module(
|
||||
classes: NonNullable<IrModule["classes"]>,
|
||||
functions: IrModule["functions"],
|
||||
): IrModule {
|
||||
return {
|
||||
irVersion: 15,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
classes,
|
||||
functions: [
|
||||
{ name: "main", locals: [], params: [], returnType: VOID, body: [], loc },
|
||||
...functions,
|
||||
],
|
||||
};
|
||||
}
|
||||
const fields = (...names: string[]) => names.map((name) => ({ name, type: F64 }));
|
||||
|
||||
/** The fields of `className` whose constructor stores are private. */
|
||||
function privateFields(mod: IrModule, className: string): string[] {
|
||||
const facts = computeConstructionFacts(mod);
|
||||
const fn = mod.functions.find((f) => f.name === `%${className}.constructor`)!;
|
||||
return fn.body
|
||||
.filter((s): s is Extract<IrStmt, { kind: "fieldSet" }> => s.kind === "fieldSet")
|
||||
.filter((s) => facts.privateStores.has(s))
|
||||
.map((s) => s.field);
|
||||
}
|
||||
|
||||
test("constructor stores before this escapes are private", () => {
|
||||
const mod = module(
|
||||
[{ name: "P", fields: fields("a", "b"), loc }],
|
||||
[ctor("P", [setThis("P", "a"), call("register", [thisRef("P")]), setThis("P", "b")])],
|
||||
);
|
||||
expect(privateFields(mod, "P")).toEqual(["a"]);
|
||||
});
|
||||
|
||||
test("storing this into its own field ends private construction", () => {
|
||||
// A later read of the field yields `this` again, so any use of it
|
||||
// (publishing it, handing it to a worker) would expose the object.
|
||||
const self: IrStmt = { ...setThis("P", "self"), value: thisRef("P") } as IrStmt;
|
||||
const mod = module(
|
||||
[{ name: "P", fields: [...fields("a", "b"), { name: "self", type: obj("P") }], loc }],
|
||||
[ctor("P", [setThis("P", "a"), self, setThis("P", "b")])],
|
||||
);
|
||||
expect(privateFields(mod, "P")).toEqual(["a"]);
|
||||
});
|
||||
|
||||
test("a base constructor's escape makes every subclass store public", () => {
|
||||
const superCall = call("%Base.constructor", [
|
||||
{ kind: "upcast", value: thisRef("Sub"), type: obj("Base"), loc },
|
||||
]);
|
||||
const mod = module(
|
||||
[
|
||||
{ name: "Base", fields: fields("a"), loc },
|
||||
{ name: "Sub", base: "Base", fields: fields("a", "b"), loc },
|
||||
],
|
||||
[
|
||||
ctor("Base", [setThis("Base", "a"), call("register", [thisRef("Base")])]),
|
||||
ctor("Sub", [superCall, setThis("Sub", "b")]),
|
||||
],
|
||||
);
|
||||
expect(privateFields(mod, "Base")).toEqual(["a"]);
|
||||
expect(privateFields(mod, "Sub")).toEqual([]);
|
||||
});
|
||||
|
||||
test("a constructor called on an existing object keeps every guard", () => {
|
||||
const mod = module([{ name: "P", fields: fields("a"), loc }], [ctor("P", [setThis("P", "a")])]);
|
||||
expect(privateFields(mod, "P")).toEqual(["a"]);
|
||||
mod.functions[0]!.body.push(call("%P.constructor", [value(obj("P"))]));
|
||||
expect(privateFields(mod, "P")).toEqual([]);
|
||||
});
|
||||
|
||||
test("library builds keep every guard", () => {
|
||||
const mod = module([{ name: "P", fields: fields("a"), loc }], [ctor("P", [setThis("P", "a")])]);
|
||||
mod.lib = {} as NonNullable<IrModule["lib"]>;
|
||||
expect(privateFields(mod, "P")).toEqual([]);
|
||||
});
|
||||
|
||||
test("construction-only fields exclude later writes and dynamic stores", () => {
|
||||
const mod = module(
|
||||
[{ name: "P", fields: fields("a", "b"), loc }],
|
||||
[ctor("P", [setThis("P", "a"), setThis("P", "b")])],
|
||||
);
|
||||
mod.functions[0]!.body.push(
|
||||
{ kind: "fieldSet", obj: value(obj("P")), className: "P", field: "b", value: value(F64), loc },
|
||||
{ kind: "exprStmt", expr: { kind: "dynFrom", value: value(obj("P")), type: DYN, loc }, loc },
|
||||
);
|
||||
const facts = computeImmutableFields(mod)!;
|
||||
expect(facts.classField("P", "a")).toBe(true);
|
||||
expect(facts.classField("P", "b")).toBe(false);
|
||||
// A runtime store into a dynamic value can reach P through its capsule.
|
||||
mod.functions[0]!.body.push({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.keySet",
|
||||
args: [value(DYN), value(STRING), value(DYN)],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
expect(computeImmutableFields(mod)!.classField("P", "a")).toBe(false);
|
||||
});
|
||||
|
||||
test("module constants are stable once assigned exactly once", () => {
|
||||
const mod = module([], []);
|
||||
mod.globals = [
|
||||
{ id: "%g.k", name: "k", type: STRING, mutable: false },
|
||||
{ id: "%g.m", name: "m", type: STRING, mutable: true },
|
||||
{ id: "%g.t", name: "t", type: STRING, mutable: false, tdz: true },
|
||||
{ id: "%g.twice", name: "twice", type: STRING, mutable: false },
|
||||
];
|
||||
const assign = (localId: string): IrStmt => ({
|
||||
kind: "assign",
|
||||
localId,
|
||||
value: value(STRING),
|
||||
loc,
|
||||
});
|
||||
mod.functions[0]!.body.push(
|
||||
assign("%g.k"),
|
||||
assign("%g.m"),
|
||||
assign("%g.t"),
|
||||
assign("%g.twice"),
|
||||
assign("%g.twice"),
|
||||
);
|
||||
expect([...stableGlobals(mod)]).toEqual(["%g.k"]);
|
||||
});
|
||||
@@ -0,0 +1,214 @@
|
||||
/* Static immutability facts the backend uses to drop run-time immutability
|
||||
* checks (see computeConstructionFacts). Pure functions of the IR module. */
|
||||
import type { IrExpr, IrModule, IrStmt } from "../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../ir/traverse.js";
|
||||
import {
|
||||
computeCycleMutability,
|
||||
constructorEscapePoint,
|
||||
constructorThisAliases,
|
||||
fieldKey,
|
||||
type CycleMutability,
|
||||
} from "./cycle-mutability.js";
|
||||
|
||||
/** Fields that are never written once their object can be observed outside
|
||||
* its own construction (cycle-mutability.ts derives the facts from every
|
||||
* store in the IR, including typed-ref commits from checked-dynamic code,
|
||||
* and disables them for library builds and dynamic islands).
|
||||
*
|
||||
* Outside constructors, a reference read from such a field stays valid for
|
||||
* as long as its owner object is alive: nothing can replace it and release
|
||||
* the old value. Constructors themselves may still write their own
|
||||
* `this`'s fields, so the facts do not apply while one runs. */
|
||||
export class ImmutableFields {
|
||||
constructor(
|
||||
private readonly mutability: CycleMutability,
|
||||
private readonly unitOfClass: (className: string) => string,
|
||||
) {}
|
||||
|
||||
/** `obj.field` on a class instance is construction-only. */
|
||||
classField(className: string, field: string): boolean {
|
||||
return this.unit(this.unitOfClass(className), field);
|
||||
}
|
||||
|
||||
/** `obj.field` on a record is never written after the literal. */
|
||||
recordField(shapeId: string, field: string): boolean {
|
||||
return this.unit(`record:${shapeId}`, field);
|
||||
}
|
||||
|
||||
private unit(unit: string, field: string): boolean {
|
||||
return (
|
||||
!this.mutability.all &&
|
||||
!this.mutability.units.has(unit) &&
|
||||
!this.mutability.fields.has(fieldKey(unit, field))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function computeImmutableFields(mod: IrModule): ImmutableFields | null {
|
||||
if (mod.lib !== undefined) return null;
|
||||
const baseOf = new Map((mod.classes ?? []).map((c) => [c.name, c.base ?? null] as const));
|
||||
const roots = new Map<string, string>();
|
||||
const rootOf = (name: string): string => {
|
||||
const known = roots.get(name);
|
||||
if (known !== undefined) return known;
|
||||
let cur = name;
|
||||
for (let base = baseOf.get(cur); base !== null && base !== undefined; base = baseOf.get(cur))
|
||||
cur = base;
|
||||
roots.set(name, cur);
|
||||
return cur;
|
||||
};
|
||||
const unitOfClass = (name: string): string => `object:${rootOf(name)}`;
|
||||
const mutability = computeCycleMutability(mod, unitOfClass);
|
||||
return mutability.all ? null : new ImmutableFields(mutability, unitOfClass);
|
||||
}
|
||||
|
||||
/** Stores that provably target an object no other code can reach yet.
|
||||
*
|
||||
* A published object (@scriptc/threads `publish`) rejects every write, so
|
||||
* the emitter guards each store into a type the program may publish. A
|
||||
* store into the constructor's own `this` cannot target a published object
|
||||
* when nothing outside the construction can hold `this` at that point:
|
||||
*
|
||||
* - the constructor runs on a fresh object: it is only reached through
|
||||
* `new` (or a construct thunk) for its class or a subclass, never by a
|
||||
* direct call on an existing object (`%C.constructor` called anywhere but
|
||||
* as the direct super call of a subclass constructor disqualifies C and
|
||||
* every ancestor, which that call reaches with the same receiver);
|
||||
* - no ancestor constructor, which runs before this body continues, lets
|
||||
* `this` escape, and the store lies in a top-level statement before the
|
||||
* first one in which this constructor lets it escape
|
||||
* (cycle-mutability's constructorEscapePoint: `this`, or a constant alias
|
||||
* of it, may only be a field-store target, a field-read receiver or the
|
||||
* direct base constructor's receiver; storing it anywhere, including
|
||||
* into one of its own fields whose later reads would hand it out again,
|
||||
* and closures and class references capturing it count as escapes).
|
||||
* A subclass constructor's code only runs after this body returns.
|
||||
*
|
||||
* Library builds keep every guard: their constructors can be invoked by an
|
||||
* embedder in ways the module does not show. */
|
||||
export interface ConstructionFacts {
|
||||
/** `fieldSet` statements whose target is a still-private `this`. */
|
||||
privateStores: Set<IrStmt>;
|
||||
}
|
||||
|
||||
export function computeConstructionFacts(mod: IrModule): ConstructionFacts {
|
||||
const facts: ConstructionFacts = { privateStores: new Set() };
|
||||
if (mod.lib !== undefined) return facts;
|
||||
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c] as const));
|
||||
const ctorClass = new Map([...classes.keys()].map((name) => [`%${name}.constructor`, name]));
|
||||
const fnByName = new Map(mod.functions.map((fn) => [fn.name, fn] as const));
|
||||
const thisOf = (name: string): string | null =>
|
||||
fnByName.get(`%${name}.constructor`)?.params[0]?.localId ?? null;
|
||||
const isThisRef = (e: IrExpr, thisId: string | null): boolean => {
|
||||
if (thisId === null) return false;
|
||||
if (e.kind === "varRef") return e.localId === thisId;
|
||||
if (e.kind === "upcast") return isThisRef(e.value, thisId);
|
||||
if (e.kind === "seqExpr") return isThisRef(e.result, thisId);
|
||||
return false;
|
||||
};
|
||||
|
||||
// Constructors reached with a receiver that may not be fresh.
|
||||
const reused = new Set<string>();
|
||||
const markReused = (name: string): void => {
|
||||
for (let c: string | undefined = name; c !== undefined && classes.has(c);) {
|
||||
if (reused.has(c)) break;
|
||||
reused.add(c);
|
||||
c = classes.get(c)!.base;
|
||||
}
|
||||
};
|
||||
for (const fn of mod.functions) {
|
||||
const own = ctorClass.get(fn.name);
|
||||
const thisId = own !== undefined ? thisOf(own) : null;
|
||||
const baseCtor =
|
||||
own !== undefined && classes.get(own)!.base !== undefined
|
||||
? `%${classes.get(own)!.base}.constructor`
|
||||
: null;
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "call") {
|
||||
const target = ctorClass.get(e.callee);
|
||||
if (
|
||||
target !== undefined &&
|
||||
!(e.callee === baseCtor && e.args.length > 0 && isThisRef(e.args[0]!, thisId))
|
||||
)
|
||||
markReused(target);
|
||||
} else if (e.kind === "closure") {
|
||||
const target = ctorClass.get(e.fnName);
|
||||
if (target !== undefined) markReused(target);
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => everyStmtChild(s, expr, stmt);
|
||||
fn.body.every(stmt);
|
||||
}
|
||||
|
||||
// The first escaping statement of each constructor (body length: none).
|
||||
// A class without a constructor function of its own (runtime roots) is
|
||||
// treated as escaping at once.
|
||||
const points = new Map<string, number>();
|
||||
const escapePoint = (name: string): number => {
|
||||
const known = points.get(name);
|
||||
if (known !== undefined) return known;
|
||||
const fn = fnByName.get(`%${name}.constructor`);
|
||||
const base = classes.get(name)?.base;
|
||||
const thisId = thisOf(name);
|
||||
const point =
|
||||
fn === undefined || thisId === null || classes.get(name)?.runtime === true
|
||||
? 0
|
||||
: constructorEscapePoint(fn, thisId, base !== undefined ? `%${base}.constructor` : null);
|
||||
points.set(name, point);
|
||||
return point;
|
||||
};
|
||||
const ancestorEscapes = (name: string): boolean => {
|
||||
for (let c = classes.get(name)?.base; c !== undefined; c = classes.get(c)?.base) {
|
||||
const fn = fnByName.get(`%${c}.constructor`);
|
||||
if (!classes.has(c) || fn === undefined || escapePoint(c) < fn.body.length) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
for (const [name] of classes) {
|
||||
const fn = fnByName.get(`%${name}.constructor`);
|
||||
if (fn === undefined || reused.has(name) || ancestorEscapes(name)) continue;
|
||||
const point = escapePoint(name);
|
||||
if (point === 0) continue;
|
||||
const aliases = constructorThisAliases(fn, thisOf(name)!);
|
||||
const isOwn = (e: IrExpr): boolean =>
|
||||
(e.kind === "varRef" && aliases.has(e.localId)) ||
|
||||
(e.kind === "upcast" && isOwn(e.value)) ||
|
||||
(e.kind === "seqExpr" && isOwn(e.result));
|
||||
const expr = (e: IrExpr): boolean => everyExprChild(e, expr, stmt);
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "fieldSet" && isOwn(s.obj)) facts.privateStores.add(s);
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
for (let i = 0; i < point; i++) stmt(fn.body[i]!);
|
||||
}
|
||||
return facts;
|
||||
}
|
||||
|
||||
/** Module constants whose value never changes once initialized: immutable,
|
||||
* not TDZ-checked (every read follows the initialization), no import-cycle
|
||||
* init flag, and at most one assignment in the whole module. Such a global
|
||||
* owns its value for the rest of the program, so a read of it can be
|
||||
* borrowed across any later operand or call. */
|
||||
export function stableGlobals(mod: IrModule): Set<string> {
|
||||
const candidates = new Set(
|
||||
(mod.globals ?? [])
|
||||
.filter((g) => !g.mutable && g.tdz !== true && g.initFlag === undefined)
|
||||
.map((g) => g.id),
|
||||
);
|
||||
const assigned = new Set<string>();
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "assignExpr" && candidates.has(e.localId)) candidates.delete(e.localId);
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "assign" && candidates.has(s.localId)) {
|
||||
if (assigned.has(s.localId)) candidates.delete(s.localId);
|
||||
assigned.add(s.localId);
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
for (const fn of mod.functions) fn.body.every(stmt);
|
||||
return candidates;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { IrExpr } from "../../ir/ir.js";
|
||||
import type { Writes } from "./reference-writes.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
|
||||
/** For a runtime input whose ABI borrows rather than consumes ownership.
|
||||
@@ -14,18 +15,33 @@ export function emitBorrowedInput(host: LlvmEmitterContext, value: IrExpr): LlVa
|
||||
* while every operation preserves existing owners. Compute suffix facts
|
||||
* once, keeping left-to-right evaluation and the owning fallback for each
|
||||
* argument independently. The consumer must borrow all inputs and preserve
|
||||
* reference edges itself; mutating runtime operations use emitBorrowedInput. */
|
||||
* reference edges itself; mutating runtime operations use emitBorrowedInput.
|
||||
* With `consumerWrites` (the locations the consumer may overwrite), a
|
||||
* projection also borrows when neither the consumer nor any later operand
|
||||
* can write a location its read depends on (ReferenceWrites). */
|
||||
export function borrowableInputs(
|
||||
host: LlvmEmitterContext,
|
||||
inputs: readonly IrExpr[],
|
||||
consumerPreserves = true,
|
||||
consumerWrites?: Writes,
|
||||
): boolean[] {
|
||||
const result: boolean[] = new Array(inputs.length);
|
||||
let preserves = consumerPreserves;
|
||||
const writes = host.referenceWrites;
|
||||
let suffix: Writes =
|
||||
consumerWrites !== undefined
|
||||
? consumerWrites
|
||||
: consumerPreserves
|
||||
? null
|
||||
: (writes?.heapAll ?? null);
|
||||
for (let i = inputs.length - 1; i >= 0; i--) {
|
||||
const value = inputs[i]!;
|
||||
result[i] = host.canBorrowCallArgument(value) || (preserves && host.canBorrowReceiver(value));
|
||||
result[i] =
|
||||
host.canBorrowCallArgument(value) ||
|
||||
(preserves && host.canBorrowReceiver(value)) ||
|
||||
(writes !== null && host.canBorrowReceiver(value) && host.unwrittenRead(value, suffix));
|
||||
preserves = preserves && host.referenceEffects.preserves(value);
|
||||
if (writes !== null) suffix = writes.union([suffix, writes.expr(value)]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -36,3 +52,21 @@ export function emitBorrowedInputs(host: LlvmEmitterContext, inputs: readonly Ir
|
||||
borrowed[index] ? host.emitReadReceiver(value) : host.emitExpr(value),
|
||||
);
|
||||
}
|
||||
|
||||
/** The writes after each input: the consumer's plus every later input's
|
||||
* (null entries: nothing; an empty array without ReferenceWrites). */
|
||||
export function inputSuffixWrites(
|
||||
host: LlvmEmitterContext,
|
||||
inputs: readonly IrExpr[],
|
||||
consumerWrites: Writes,
|
||||
): Writes[] {
|
||||
const writes = host.referenceWrites;
|
||||
if (writes === null) return [];
|
||||
const out: Writes[] = new Array(inputs.length);
|
||||
let suffix = consumerWrites;
|
||||
for (let i = inputs.length - 1; i >= 0; i--) {
|
||||
out[i] = suffix;
|
||||
suffix = writes.union([suffix, writes.expr(inputs[i]!)]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,20 @@ import type {
|
||||
} from "../../ir/ir.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
|
||||
/** Field numbers of %ScrArr, which mirrors scr_runtime.h's ScrArr field for
|
||||
* field (its static assertions pin `cyc` and `data`). */
|
||||
export const SCR_ARR_FIELD = {
|
||||
rc: 0,
|
||||
len: 1,
|
||||
cap: 2,
|
||||
elem: 3,
|
||||
cyc: 4,
|
||||
ops: 5,
|
||||
data: 6,
|
||||
present: 7,
|
||||
ext: 8,
|
||||
} as const;
|
||||
|
||||
/** Lines that decide whether the double `index` is an integer in
|
||||
* `[0, length)`, where `length` is an unsigned `sizeType` count, and give
|
||||
* that integer as an i64. `%{ok}` is the answer; `%{wide}` is meaningful
|
||||
|
||||
@@ -77,7 +77,17 @@ test.each([32, 64] as const)("strict scalar reads check dense storage inline (%i
|
||||
|
||||
test("reference reads retain inline and keep a cold +1 runtime fallback", () => {
|
||||
const read: IrExpr = { kind: "arrayGet", arr: ref("s", words), index, type: STRING, loc };
|
||||
const llvm = emitLlvmModule(module("word", STRING, [{ kind: "return", value: read, loc }]));
|
||||
// An element removal in the body keeps the read from being lent as a
|
||||
// borrowed return: the result must own its reference.
|
||||
const truncate: IrStmt = {
|
||||
kind: "arraySetLength",
|
||||
arr: ref("s", words),
|
||||
length: { kind: "numLit", value: 1, type: F64, loc },
|
||||
loc,
|
||||
};
|
||||
const llvm = emitLlvmModule(
|
||||
module("word", STRING, [truncate, { kind: "return", value: read, loc }]),
|
||||
);
|
||||
const fn = body(llvm, "word");
|
||||
expect(fn).toMatch(/icmp ne ptr .*, null/);
|
||||
expect(fn).toContain("@scr_str_retain_v(");
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
* `undefined`, sparse indices, noncanonical numeric properties (negative,
|
||||
* fractional, NaN, infinite, 2^32-1), and out-of-range reads branch to the
|
||||
* unchanged runtime call, which is marked cold. The ScrArr field indices
|
||||
* match the C layout in scr_runtime.h: len 1, cap 2, data 7, present 8. */
|
||||
* (SCR_ARR_FIELD) match the C layout in scr_runtime.h. */
|
||||
|
||||
import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import { SCR_ARR_FIELD } from "./common.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
|
||||
@@ -85,10 +86,14 @@ function emitDenseGuard(
|
||||
data = B.tmp(),
|
||||
statesPtr = B.tmp(),
|
||||
states = B.tmp();
|
||||
B.line(`${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 7`);
|
||||
B.line(
|
||||
`${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.data}`,
|
||||
);
|
||||
host.markMemoryPointer(dataPtr, "array:header");
|
||||
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
|
||||
B.line(`${statesPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 8`);
|
||||
B.line(
|
||||
`${statesPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.present}`,
|
||||
);
|
||||
host.markMemoryPointer(statesPtr, "array:header");
|
||||
B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`);
|
||||
const widen = (value: string): string => {
|
||||
@@ -389,9 +394,9 @@ export function emitDenseArrayPush(
|
||||
join = B.newLabel("arr.push.join");
|
||||
B.condBr(room, fast, slow);
|
||||
B.startBlock(fast);
|
||||
const dataPtr = header(7),
|
||||
const dataPtr = header(SCR_ARR_FIELD.data),
|
||||
data = B.tmp(),
|
||||
statesPtr = header(8),
|
||||
statesPtr = header(SCR_ARR_FIELD.present),
|
||||
states = B.tmp();
|
||||
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
|
||||
B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { typedRefConstructor } from "./shapes.js";
|
||||
import { bindLiveArrayOps } from "./expr-stream-bridges.js";
|
||||
import { BYTES_ELEM_NUM, f64Lit } from "./common.js";
|
||||
import { BYTES_ELEM_NUM, SCR_ARR_FIELD, f64Lit } from "./common.js";
|
||||
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* ScrDyn helpers for the LLVM backend: per-type match predicates
|
||||
@@ -1199,8 +1199,8 @@ export class LlDyn {
|
||||
: [
|
||||
{ index: 1, type: host.sizeType, name: "length" },
|
||||
{ index: 2, type: host.sizeType, name: "capacity" },
|
||||
{ index: 7, type: "ptr" as const, name: "data" },
|
||||
{ index: 8, type: "ptr" as const, name: "presence" },
|
||||
{ index: SCR_ARR_FIELD.data, type: "ptr" as const, name: "data" },
|
||||
{ index: SCR_ARR_FIELD.present, type: "ptr" as const, name: "presence" },
|
||||
];
|
||||
members.forEach((member) => {
|
||||
const cachedPtr = B.tmp();
|
||||
|
||||
@@ -99,6 +99,26 @@ test("separate LLVM parts preserve complete module bytes in native, WASI, and de
|
||||
}
|
||||
});
|
||||
|
||||
test("the shared indexed-read helpers carry an inline hint", () => {
|
||||
const helper = (name: string): IrFunction => ({
|
||||
name,
|
||||
params: [],
|
||||
returnType: F64,
|
||||
locals: [],
|
||||
body: [{ kind: "return", value: { kind: "numLit", value: 1, type: F64, loc }, loc }],
|
||||
loc,
|
||||
});
|
||||
const module = moduleFor([]);
|
||||
const llvm = emitLlvmModule({
|
||||
...module,
|
||||
functions: [...module.functions, helper("%arr.idxOr.0"), helper("plain")],
|
||||
});
|
||||
const define = (name: string): string =>
|
||||
llvm.split("\n").find((line) => line.startsWith("define") && line.endsWith(`{ ; ${name}`))!;
|
||||
expect(define("%arr.idxOr.0")).toMatch(/\) inlinehint #0 \{/);
|
||||
expect(define("plain")).not.toContain("inlinehint");
|
||||
});
|
||||
|
||||
test("throwing-call count does not multiply identical scope cleanup", () => {
|
||||
for (const boxed of [false, true]) {
|
||||
const small = emitLlvmModule(moduleFor(Array.from({ length: 4 }, call), { boxed }));
|
||||
|
||||
@@ -99,6 +99,7 @@ import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
|
||||
import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
|
||||
import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
|
||||
import { analyzeIntegerRanges, INT32_RANGE, type IntegerRanges } from "../../ir/integer-ranges.js";
|
||||
import { unshareNodes } from "../../ir/unshare.js";
|
||||
import { analyzeInt32Slots, type Int32Slots } from "../../ir/int32-slots.js";
|
||||
import { analyzeFinalFields, constructorClass, type FinalFields } from "../../ir/final-fields.js";
|
||||
import { foldConstantGlobals } from "../../ir/constant-globals.js";
|
||||
@@ -159,10 +160,11 @@ import {
|
||||
type MapReadLifetimes,
|
||||
} from "./map-read-lifetimes.js";
|
||||
import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { analyzeWalks, findWalkBorrows } from "./walk-borrows.js";
|
||||
import { borrowableInputs, emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { analyzeWalks, findWalkBorrows, type WalkBorrowHost } from "./walk-borrows.js";
|
||||
import { findReboundParameters } from "./rebound-parameters.js";
|
||||
import { ReferenceEffects } from "./reference-effects.js";
|
||||
import { ReferenceWrites, type Writes } from "./reference-writes.js";
|
||||
import { LlvmDebugInfo } from "./debug-info.js";
|
||||
import { StackCallbacks } from "./stack-callbacks.js";
|
||||
import { findLoopArrayBorrows } from "./loop-array-borrows.js";
|
||||
@@ -197,6 +199,7 @@ import { emitBorrowedResultCall, emitCallExpr } from "./expr-calls.js";
|
||||
import { emitDynamicExpr } from "./expr-dynamic.js";
|
||||
import { emitIntrinsicExpr, emitSerializationExpr, emitAsyncExpr } from "./expr-async.js";
|
||||
import { PublishWalkers } from "./publish.js";
|
||||
import { computeImmutableFields, stableGlobals, type ImmutableFields } from "../immutability.js";
|
||||
import { computePublishedTypes, type PublishedTypes } from "../publication.js";
|
||||
import { emitJsInteropExpr, emitExpr } from "./expr-dispatch.js";
|
||||
import {
|
||||
@@ -301,7 +304,7 @@ import { LlWalkers } from "./walkers.js";
|
||||
import { isObjectArm, NULLABLE_ABSENT, NULLABLE_NULL, NullableUnions } from "./nullable-unions.js";
|
||||
import { emitNullablePresent, emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
||||
import { literalCapWord, literalCapWord32 } from "./string-key-hash.js";
|
||||
import { NUMBER_MAP_ENTRY_DECL } from "./map-number-lookup.js";
|
||||
import { NUMBER_MAP_ENTRY_DECL, STRING_MAP_ENTRY_DECL } from "./map-number-lookup.js";
|
||||
import { STRING_READ_HELPERS } from "./string-reads.js";
|
||||
import {
|
||||
emitConcatInputs,
|
||||
@@ -407,7 +410,10 @@ export interface LlvmTargetOptions {
|
||||
detachedExit?: boolean;
|
||||
}
|
||||
|
||||
export function emitLlvmModule(mod: IrModule, options: LlvmTargetOptions = {}): string {
|
||||
export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
|
||||
// Facts are keyed by node identity: emit the same module whether it was
|
||||
// lowered in memory or read back from its serialized form.
|
||||
const mod = unshareNodes(input);
|
||||
// Keep source storage intact for debugger inspection in dev builds.
|
||||
return new LlEmitter(
|
||||
options.debugSources === undefined
|
||||
@@ -419,9 +425,10 @@ export function emitLlvmModule(mod: IrModule, options: LlvmTargetOptions = {}):
|
||||
|
||||
/** Keep large translation units below the host's single-string limit. */
|
||||
export function emitLlvmModuleSource(
|
||||
mod: IrModule,
|
||||
input: IrModule,
|
||||
options: LlvmTargetOptions = {},
|
||||
): string | readonly string[] {
|
||||
const mod = unshareNodes(input);
|
||||
const parts = new LlEmitter(
|
||||
options.debugSources === undefined
|
||||
? scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction))
|
||||
@@ -598,6 +605,12 @@ export class LlEmitter {
|
||||
* fiberless-async analysis scans it for suspension entry points. */
|
||||
private readonly fnDefText = new Map<string, string>();
|
||||
readonly referenceEffects: ReferenceEffects;
|
||||
/** Field-sensitive reference writes (null in debug builds). */
|
||||
referenceWrites: ReferenceWrites | null = null;
|
||||
private readonly borrowedReturnKeyCache = new Map<string, string[]>();
|
||||
/** The statement list being emitted and the current index, for blocks
|
||||
* whose bindings end with the list (emitBlock and function bodies). */
|
||||
private stmtCursor: { stmts: IrStmt[]; index: number } | null = null;
|
||||
readonly optionalArrayReads: OptionalArrayReads;
|
||||
readonly checkedNarrows: CheckedNarrows;
|
||||
callArrayReads = new Map<IrExpr, LocalArrayRead>();
|
||||
@@ -641,6 +654,9 @@ export class LlEmitter {
|
||||
readonly ffiHasRetainedCallback: boolean;
|
||||
private readonly ffiHasForeignCallback: boolean;
|
||||
private readonly globalTypes = new Map<string, IrType>();
|
||||
/** Module constants that keep one value once initialized
|
||||
* (immutability.ts stableGlobals). */
|
||||
private stableGlobals: ReadonlySet<string> = new Set();
|
||||
private readonly tdzGlobals = new Map<string, string>();
|
||||
readonly constantNumericTables: ReadonlyMap<string, ConstantNumericTable>;
|
||||
/** May-throw analysis (computeMayThrow): pending
|
||||
@@ -656,6 +672,13 @@ export class LlEmitter {
|
||||
/** Static types whose values can be published (@scriptc/threads): their
|
||||
* stores carry the frozen-object guard. Null without publish calls. */
|
||||
readonly publishedTypes: PublishedTypes | null;
|
||||
/** Construction-only class and record fields (immutability.ts); null
|
||||
* where the facts are disabled. */
|
||||
private readonly immutableFields: ImmutableFields | null;
|
||||
/** Constructor function names: their bodies may still write their own
|
||||
* `this`'s construction-only fields. */
|
||||
private readonly constructorFunctions: ReadonlySet<string>;
|
||||
private inConstructor = false;
|
||||
/** Method names with at least one may-throw implementation — the
|
||||
* virtualCall pending check's key. */
|
||||
readonly mayThrowMethods = new Set<string>();
|
||||
@@ -888,6 +911,8 @@ export class LlEmitter {
|
||||
this.workerEntryPolls = mt.workerEntryPolls ?? null;
|
||||
this.stackChecks = mt.stackChecks ?? null;
|
||||
this.publishedTypes = computePublishedTypes(mod);
|
||||
this.immutableFields = computeImmutableFields(mod);
|
||||
this.constructorFunctions = new Set((mod.classes ?? []).map((c) => `%${c.name}.constructor`));
|
||||
this.indirectMayThrow = mt.indirect || mod.workers === true;
|
||||
for (const cls of mod.classes ?? []) {
|
||||
for (const m of cls.methods ?? []) {
|
||||
@@ -911,6 +936,15 @@ export class LlEmitter {
|
||||
(call) => this.optionalArrayReads.get(call) !== null,
|
||||
mod.classes ?? [],
|
||||
);
|
||||
if (this.debug === null)
|
||||
this.referenceWrites = new ReferenceWrites(
|
||||
this.fnByName,
|
||||
mod.classes ?? [],
|
||||
(call) => this.optionalArrayReads.get(call) !== null,
|
||||
new Set((mod.globals ?? []).map((g) => g.id)),
|
||||
{ unions: mod.unions, records: mod.records },
|
||||
this.immutableFields,
|
||||
);
|
||||
const walkParameters = new Map<string, Set<number>>();
|
||||
if (this.debug === null) {
|
||||
const host = {
|
||||
@@ -918,8 +952,9 @@ export class LlEmitter {
|
||||
borrowsWithoutOwning: (e: IrExpr) => this.borrowsWithoutOwning(e),
|
||||
};
|
||||
for (const fn of this.fnByName.values()) {
|
||||
if (!this.referenceEffects.functions.has(fn.name)) continue;
|
||||
const walks = findWalkBorrows(fn, host);
|
||||
const preserving = this.referenceEffects.functions.has(fn.name);
|
||||
if (!preserving && this.referenceWrites === null) continue;
|
||||
const walks = findWalkBorrows(fn, preserving ? host : this.unwrittenWalkHost(fn, host));
|
||||
if (walks.size === 0) continue;
|
||||
this.walkBorrowsByFunction.set(fn.name, walks);
|
||||
const indexes = new Set<number>();
|
||||
@@ -1027,6 +1062,7 @@ export class LlEmitter {
|
||||
this.globalTypes.set(g.id, g.type);
|
||||
if (g.tdz) this.tdzGlobals.set(g.id, g.name);
|
||||
}
|
||||
this.stableGlobals = stableGlobals(mod);
|
||||
// Runtime error, EventEmitter, and stream classes have known layouts.
|
||||
// Subclasses embed the ScrEmitter/ScrStream prefixes from classes.ts.
|
||||
const classes = mod.classes ?? [];
|
||||
@@ -1885,12 +1921,14 @@ export class LlEmitter {
|
||||
`%ScrFfiTable = type { ptr, ${this.sizeType}, ${this.sizeType}, ptr, i8, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ptr }`,
|
||||
`%ScrRegex = type { ${this.sizeType}, ptr, ptr, ptr, double, ptr, ptr }`,
|
||||
// ScrArr mirrors scr_runtime.h field-for-field: { rc, len, cap, elem,
|
||||
// elem_retain, elem_release, elem_trace, data, present, ext }, where
|
||||
// ext points at the out-of-line sparse, property and metadata storage
|
||||
// (NULL until first used). Live dynamic stream commits swap its dense,
|
||||
// presence and out-of-line storage while preserving the target
|
||||
// object's identity.
|
||||
`%ScrArr = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, i32, ptr, ptr, ptr, ptr, ptr, ptr }`,
|
||||
// cyc, ops, data, present, ext } (field numbers in SCR_ARR_FIELD),
|
||||
// where ops points at the element type's constant RC descriptor
|
||||
// (%ScrArrOps, SCR_ELEM_REF only) and ext at the out-of-line sparse,
|
||||
// property and metadata storage (NULL until first used). Live dynamic
|
||||
// stream commits swap its dense, presence and out-of-line storage while
|
||||
// preserving the target object's identity.
|
||||
`%ScrArr = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, i32, i32, ptr, ptr, ptr, ptr }`,
|
||||
`%ScrArrOps = type { ptr, ptr, ptr }`,
|
||||
// The ScrMap prefix { rc, key_kind, val_kind, val_retain, val_release,
|
||||
// val_trace, key_retain, key_release, key_trace } — the inline RC
|
||||
// fast paths test the two trace slots (shapes.ts).
|
||||
@@ -1903,9 +1941,13 @@ export class LlEmitter {
|
||||
...(this.decls.has(NUMBER_MAP_ENTRY_DECL)
|
||||
? [
|
||||
`%ScrMapIx = type { ${this.sizeType}, i32, i32, ptr, ptr, ptr, ptr, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ${this.sizeType}, ptr, ${this.sizeType}, ptr, ptr, ptr, ${this.sizeType} }`,
|
||||
`%ScrMapEntry = type { i64, i64, i64 }`,
|
||||
]
|
||||
: []),
|
||||
// String-keyed reference reads load the value slot of the entry
|
||||
// scr_map_entry_str returns (map-number-lookup.ts).
|
||||
...(this.decls.has(NUMBER_MAP_ENTRY_DECL) || this.decls.has(STRING_MAP_ENTRY_DECL)
|
||||
? [`%ScrMapEntry = type { i64, i64, i64 }`]
|
||||
: []),
|
||||
// The runtime error prefix { rc, vt, name, message, code, cause } and the
|
||||
// class-object shape { rc, pre, post, ctor, name } — field reads on
|
||||
// builtin errors and classval loads GEP through these.
|
||||
@@ -2071,7 +2113,7 @@ export class LlEmitter {
|
||||
out.push(
|
||||
`@${inst.sym}_data = internal constant [${n} x ptr] [ ${inst.slots.map((s) => `ptr ${s}`).join(", ")} ]`,
|
||||
`@${inst.sym}_present = internal constant [${n} x i8] ${present}`,
|
||||
`@${inst.sym} = internal global %ScrArr { ${this.sizeType} -1, ${this.sizeType} ${n}, ${this.sizeType} ${n}, i32 2, ptr null, ptr null, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null }`,
|
||||
`@${inst.sym} = internal global %ScrArr { ${this.sizeType} -1, ${this.sizeType} ${n}, ${this.sizeType} ${n}, i32 2, i32 0, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null }`,
|
||||
);
|
||||
}
|
||||
if (this.templateStringsInstances.size > 0) out.push(``);
|
||||
@@ -5093,6 +5135,7 @@ export class LlEmitter {
|
||||
const numericFn = withInitializerBindings(fn, initializerBindings);
|
||||
this.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l]));
|
||||
this.borrowedParameters.clear();
|
||||
this.referenceWrites?.enter(fn);
|
||||
this.reboundOwnerSlots.clear();
|
||||
this.projectedParameters.clear();
|
||||
const projectedParameterIndexes = this.callLifetimes.parameters.get(fn.name);
|
||||
@@ -5133,6 +5176,7 @@ export class LlEmitter {
|
||||
this.callLifetimes,
|
||||
this.referenceEffects,
|
||||
this.optionalArrayReads,
|
||||
this.referenceWrites,
|
||||
)
|
||||
: new Set();
|
||||
this.localArrayReads = findLocalArrayReads(
|
||||
@@ -5173,6 +5217,7 @@ export class LlEmitter {
|
||||
this.currentReturnType = fn.returnType;
|
||||
this.currentGenerator = fn.generator ?? null;
|
||||
this.currentBorrowedReturn = this.borrowedReturns.has(fn.name);
|
||||
this.inConstructor = this.constructorFunctions.has(fn.name);
|
||||
this.currentWasiCoro = null;
|
||||
this.logArgSlots = 0;
|
||||
|
||||
@@ -5339,7 +5384,7 @@ export class LlEmitter {
|
||||
}
|
||||
if (this.stackChecks?.has(fn.name)) this.emitStackCheck();
|
||||
if (this.workerEntryPolls?.has(fn.name)) this.emitPendingCheck(undefined, true);
|
||||
this.emitStmts(fn.body);
|
||||
this.emitStmts(fn.body, true);
|
||||
// Implicit exit of a void function: release the function scope unless
|
||||
// the body already terminated its final block (return, or a throw
|
||||
// whose unwind released everything down to depth 0).
|
||||
@@ -5403,7 +5448,13 @@ export class LlEmitter {
|
||||
// Lifted functions receive their closure first (the callValue ABI).
|
||||
if (fn.captures !== undefined) params.unshift("ptr %sc_env");
|
||||
const ret = this.llType(fn.returnType);
|
||||
const attrs = coro !== null ? "#1" : FN_ATTRS;
|
||||
let attrs = coro !== null ? "#1" : FN_ATTRS;
|
||||
// The shared indexed-read helper (lowerSafeIndexRead) is a dense bounds
|
||||
// test, a state test and a load behind cold runtime fallbacks, called
|
||||
// from every `a[i]` of one element type. Without the hint LLVM keeps the
|
||||
// hot instances out of line once they have many callers: every read pays
|
||||
// a call, and an integer index converts to a double and back across it.
|
||||
if (fn.name.startsWith("%arr.idxOr.")) attrs = `inlinehint ${attrs}`;
|
||||
const debug = this.debugScope === null ? "" : ` !dbg ${this.debugScope}`;
|
||||
const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name);
|
||||
const body = `define internal ${ret} @${symbol}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`;
|
||||
@@ -5508,11 +5559,17 @@ export class LlEmitter {
|
||||
this.canBorrowCallArgument(value.then) &&
|
||||
this.canBorrowCallArgument(value.else_)
|
||||
);
|
||||
// A construction-only field of a stable owner keeps its value until the
|
||||
// owner dies: no later operand or callee can replace and release it.
|
||||
if ((value.kind === "fieldGet" || value.kind === "recordGet") && this.immutableRead(value))
|
||||
return this.canBorrowCallArgument(value.obj);
|
||||
// An optional chain's bound receiver stays unchanged until its body
|
||||
// finishes (see emitReadReceiver).
|
||||
if (value.kind === "chainRecv") return this.chainSlots.has(value.id);
|
||||
if (value.kind !== "varRef") return false;
|
||||
const binding = this.binding(value.localId);
|
||||
// A module constant owns its value for the rest of the program.
|
||||
if (binding.kind === "global") return this.stableGlobals.has(value.localId);
|
||||
return (
|
||||
binding.kind === "local" &&
|
||||
binding.local !== undefined &&
|
||||
@@ -5525,15 +5582,213 @@ export class LlEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
/** A plain-pointer read of a construction-only field outside any
|
||||
* constructor (ImmutableFields). */
|
||||
private immutableRead(e: ExprOf<"fieldGet" | "recordGet">): boolean {
|
||||
if (this.inConstructor || this.immutableFields === null) return false;
|
||||
const t = e.type.kind;
|
||||
if (t !== "object" && t !== "record" && t !== "array" && t !== "string") return false;
|
||||
return e.kind === "fieldGet"
|
||||
? this.immutableFields.classField(e.className, e.field)
|
||||
: this.immutableFields.recordField(e.shapeId, e.field);
|
||||
}
|
||||
|
||||
/** A projection read whose locations nothing writes while a borrowed
|
||||
* read of it is in use (ReferenceWrites), given the writes of that
|
||||
* extent. The root binding itself must stay alive across it too, which
|
||||
* its own location (`l:`, `g:`, `bx`) covers. */
|
||||
unwrittenRead(e: IrExpr, writes: Writes): boolean {
|
||||
const rw = this.referenceWrites;
|
||||
if (rw === null) return false;
|
||||
const keys = rw.readKeys(e, (call) => this.borrowedReturnKeys(call.callee));
|
||||
return keys !== null && !rw.conflicts(keys, writes);
|
||||
}
|
||||
|
||||
/** A stable binding initialized by a projection can borrow it when the
|
||||
* rest of its block (its whole lifetime) writes none of the projection's
|
||||
* locations: the source edge then keeps the value alive, and uses that
|
||||
* escape still retain their own copies. */
|
||||
private unwrittenAlias(s: IrStmt & { kind: "varDecl" }): boolean {
|
||||
const cursor = this.stmtCursor;
|
||||
const rw = this.referenceWrites;
|
||||
if (rw === null || s.init === null || cursor === null || cursor.stmts[cursor.index] !== s)
|
||||
return false;
|
||||
if (
|
||||
!this.plainReference(s.init.type) ||
|
||||
!this.canBorrowReceiver(s.init) ||
|
||||
!this.unownedWalk(s.init)
|
||||
)
|
||||
return false;
|
||||
const writes = rw.union([rw.expr(s.init), rw.stmts(cursor.stmts.slice(cursor.index + 1))]);
|
||||
return this.unwrittenRead(s.init, writes);
|
||||
}
|
||||
|
||||
/** An optional element read into a projection-only local can borrow the
|
||||
* element when the rest of the block writes no array element and none of
|
||||
* the locations keeping the array alive (which must itself be read
|
||||
* without a frame owner). */
|
||||
private unwrittenLocalArrayRead(s: IrStmt & { kind: "varDecl" }, read: LocalArrayRead): boolean {
|
||||
const cursor = this.stmtCursor;
|
||||
const rw = this.referenceWrites;
|
||||
if (rw === null || s.init === null || cursor === null || cursor.stmts[cursor.index] !== s)
|
||||
return false;
|
||||
if (!this.unownedWalk(read.array)) return false;
|
||||
const keys = rw.readKeys(read.array);
|
||||
if (keys === null || rw.expr(read.index) !== null) return false;
|
||||
const writes = rw.union([rw.expr(s.init), rw.stmts(cursor.stmts.slice(cursor.index + 1))]);
|
||||
return !rw.conflicts(["a", ...keys], writes);
|
||||
}
|
||||
|
||||
/** An optional element read passed to a projection-only parameter can
|
||||
* borrow the element when the callee and later arguments (`writes`)
|
||||
* write no array element and nothing keeping the array alive. */
|
||||
unwrittenCallArrayRead(read: LocalArrayRead, writes: Writes): boolean {
|
||||
const rw = this.referenceWrites;
|
||||
if (rw === null) return false;
|
||||
const keys = rw.readKeys(read.array);
|
||||
if (keys === null || rw.expr(read.index) !== null) return false;
|
||||
return !rw.conflicts(["a", ...keys], writes);
|
||||
}
|
||||
|
||||
/** `return x` of a local whose slot is an owned scope entry of the same
|
||||
* type: the return can take over the local's reference. */
|
||||
private movableReturn(
|
||||
value: IrExpr | null,
|
||||
): { scope: LlScopeEntry[]; entry: LlScopeEntry; index: number } | null {
|
||||
if (value === null || value.kind !== "varRef" || !isRefCounted(value.type)) return null;
|
||||
if (this.currentBorrowedReturn || this.currentWasiCoro !== null) return null;
|
||||
const binding = this.binding(value.localId);
|
||||
if (binding.kind !== "local" || binding.local === undefined || binding.local.boxed) return null;
|
||||
if (!typeEquals(binding.type, value.type) || !typeEquals(this.currentReturnType, value.type))
|
||||
return null;
|
||||
for (const scope of this.scopes) {
|
||||
const index = scope.findIndex((entry) => entry.slot === binding.slot);
|
||||
if (index < 0) continue;
|
||||
const entry = scope[index]!;
|
||||
if (entry.boxed || entry.lazy || !typeEquals(entry.type, value.type)) return null;
|
||||
return { scope, entry, index };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A walk host for a body that may write the heap: each walk must read
|
||||
* only locations the whole body (with its callees) never writes;
|
||||
* rebinding the walk locals themselves releases nothing. */
|
||||
private unwrittenWalkHost(fn: IrFunction, host: WalkBorrowHost): WalkBorrowHost {
|
||||
return {
|
||||
...host,
|
||||
unwritten: (value, candidates) =>
|
||||
this.referenceWrites?.unwrittenWalk(fn, value, candidates, (call) =>
|
||||
this.borrowedReturnKeys(call.callee),
|
||||
) ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
/** emitReadReceiver yields a plain pointer for `e` that no statement
|
||||
* frame owns, so it can outlive the statement (walk-borrows' walks, from
|
||||
* any unboxed plain-pointer binding, plus element reads). */
|
||||
private unownedWalk(e: IrExpr): boolean {
|
||||
switch (e.kind) {
|
||||
case "varRef": {
|
||||
const binding = this.binding(e.localId);
|
||||
if (binding.kind === "global") return this.plainReference(binding.type);
|
||||
return (
|
||||
binding.kind === "local" &&
|
||||
binding.local !== undefined &&
|
||||
!binding.local.boxed &&
|
||||
!binding.local.tdz &&
|
||||
this.plainReference(binding.local.type) &&
|
||||
!this.stringSlices.has(e.localId) &&
|
||||
!this.storedSplits.has(e.localId) &&
|
||||
!this.localUnionStorage.has(e.localId)
|
||||
);
|
||||
}
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
return this.borrowsWithoutOwning(e) && this.unownedWalk(e.obj);
|
||||
case "unionNarrow":
|
||||
case "downcast":
|
||||
case "upcast":
|
||||
return this.borrowsWithoutOwning(e) && this.unownedWalk(e.value);
|
||||
case "unionWrap":
|
||||
if (!this.borrowsWithoutOwning(e)) return false;
|
||||
return e.value.kind === "unitLit" || this.unownedWalk(e.value);
|
||||
case "ternary": {
|
||||
const arm = (v: IrExpr): boolean =>
|
||||
(v.kind === "libCall" && v.fn === "error.nodeThrow") || this.unownedWalk(v);
|
||||
return this.borrowsWithoutOwning(e) && arm(e.then) && arm(e.else_);
|
||||
}
|
||||
case "call":
|
||||
return (
|
||||
this.borrowsWithoutOwning(e) &&
|
||||
e.args.every((arg) => !isRefCounted(arg.type) || this.unownedWalk(arg))
|
||||
);
|
||||
case "arrayGet":
|
||||
return (
|
||||
!this.isStackUnionSource(e) &&
|
||||
this.optionalArrayReads.get(e) === null &&
|
||||
this.unownedWalk(e.arr)
|
||||
);
|
||||
case "seqExpr": {
|
||||
const guarded = guardedValue(e);
|
||||
return guarded !== null && this.unownedWalk(guarded);
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Every location a borrowed-return callee's walks can read (its body's
|
||||
* field, record and element reads, through nested borrowed returns). */
|
||||
private borrowedReturnKeys(callee: string): string[] | null {
|
||||
if (!this.borrowedReturns.has(callee)) return null;
|
||||
const known = this.borrowedReturnKeyCache.get(callee);
|
||||
if (known) return known;
|
||||
const rw = this.referenceWrites!;
|
||||
const keys = new Set<string>();
|
||||
const seen = new Set([callee]);
|
||||
const pending = [callee];
|
||||
while (pending.length > 0) {
|
||||
const fn = this.fnByName.get(pending.pop()!);
|
||||
if (!fn) continue;
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (e) => {
|
||||
if (e.kind === "fieldGet") keys.add(rw.fieldKey(e.className, e.field));
|
||||
else if (e.kind === "recordGet") {
|
||||
keys.add(`r:${e.field}`);
|
||||
keys.add("r:*");
|
||||
} else if (e.kind === "arrayGet") keys.add("a");
|
||||
else if (e.kind === "call" && this.borrowedReturns.has(e.callee) && !seen.has(e.callee)) {
|
||||
seen.add(e.callee);
|
||||
pending.push(e.callee);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
const result = [...keys];
|
||||
this.borrowedReturnKeyCache.set(callee, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── statements ──────────────────────────────────────────────────────────
|
||||
|
||||
private emitStmts(stmts: IrStmt[]): void {
|
||||
for (const s of stmts) {
|
||||
// Statements after a terminator are unreachable (dead code after
|
||||
// return/break/continue). Skip them so no dropped SSA definition
|
||||
// can leak forward.
|
||||
if (this.B.isTerminated()) return;
|
||||
this.emitStmt(s);
|
||||
private emitStmts(stmts: IrStmt[], scoped = false): void {
|
||||
const saved = this.stmtCursor;
|
||||
const cursor = scoped ? { stmts, index: 0 } : null;
|
||||
try {
|
||||
for (let i = 0; i < stmts.length; i++) {
|
||||
// Statements after a terminator are unreachable (dead code after
|
||||
// return/break/continue). Skip them so no dropped SSA definition
|
||||
// can leak forward.
|
||||
if (this.B.isTerminated()) return;
|
||||
if (cursor) cursor.index = i;
|
||||
this.stmtCursor = cursor;
|
||||
this.emitStmt(stmts[i]!);
|
||||
}
|
||||
} finally {
|
||||
this.stmtCursor = saved;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5546,7 +5801,7 @@ export class LlEmitter {
|
||||
const scope: LlScopeEntry[] = [];
|
||||
this.scopes.push(scope);
|
||||
setup?.(scope);
|
||||
this.emitStmts(stmts);
|
||||
this.emitStmts(stmts, true);
|
||||
const ended = endsWithJump(stmts);
|
||||
this.scopes.pop();
|
||||
if (!ended) this.releaseScope(scope);
|
||||
@@ -5610,7 +5865,13 @@ export class LlEmitter {
|
||||
}
|
||||
const localRead = this.localArrayReads.get(s.localId);
|
||||
if (localRead) {
|
||||
const owner = emitLocalArrayRead(this, localRead, b.slot);
|
||||
const owner = emitLocalArrayRead(
|
||||
this,
|
||||
!localRead.borrow && this.unwrittenLocalArrayRead(s, localRead)
|
||||
? { ...localRead, borrow: true }
|
||||
: localRead,
|
||||
b.slot,
|
||||
);
|
||||
if (owner) this.scopes[this.scopes.length - 1]!.push(owner);
|
||||
break;
|
||||
}
|
||||
@@ -5699,7 +5960,7 @@ export class LlEmitter {
|
||||
b.local &&
|
||||
isRefCounted(b.type) &&
|
||||
this.stableCallBindings.has(s.localId) &&
|
||||
this.canBorrowCallArgument(s.init)
|
||||
(this.canBorrowCallArgument(s.init) || this.unwrittenAlias(s))
|
||||
) {
|
||||
const v = this.emitReadReceiver(s.init);
|
||||
B.line(`store ptr ${v.name}, ptr ${b.slot}`);
|
||||
@@ -5770,8 +6031,27 @@ export class LlEmitter {
|
||||
// fresh, publishable and never written): its graph becomes
|
||||
// immortal, so reads skip reference counting and the collector
|
||||
// never visits it.
|
||||
for (const line of this.publishWalkers.emitPublish(v.name, v.type, s.loc))
|
||||
B.line(line.trimStart());
|
||||
const lines = this.publishWalkers.emitPublish(v.name, v.type, s.loc);
|
||||
for (const line of lines) B.line(line.trimStart());
|
||||
// Reads of this global skip retains from here on, which is only
|
||||
// sound once the graph really is immortal. Node.js never fails
|
||||
// here, so a refusal means the contract proof was wrong: stop
|
||||
// rather than keep running with a mortal graph read as immortal.
|
||||
if (lines.length > 0) {
|
||||
this.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
this.declare(`declare void @scr_trap(ptr)`);
|
||||
const refused = B.tmp();
|
||||
B.line(`${refused} = call zeroext i1 @scr_exc_pending()`);
|
||||
const stop = B.newLabel("immutable.refused");
|
||||
const done = B.newLabel("immutable.published");
|
||||
B.condBr(refused, stop, done);
|
||||
B.startBlock(stop);
|
||||
B.line(
|
||||
`call void @scr_trap(ptr ${this.cstr("scriptc: internal error: an Immutable value could not be published\n")})`,
|
||||
);
|
||||
B.terminate("unreachable");
|
||||
B.startBlock(done);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -5889,21 +6169,21 @@ export class LlEmitter {
|
||||
const obj = this.emitStableReceiver(s.obj, [s.value]);
|
||||
// (The guard precedes the value here: the nullable store
|
||||
// evaluates it itself.)
|
||||
this.emitPublishedFieldGuard(obj.name, s.className, s.field);
|
||||
this.emitStoreGuard(obj.name, s);
|
||||
this.emitNullableFieldStore(obj.name, s.className, s.field, nullable, s.value);
|
||||
break;
|
||||
}
|
||||
if (s.kind === "fieldSet" && this.int32Slots.isField(s.className, s.field)) {
|
||||
const obj = this.emitStableReceiver(s.obj, [s.value]);
|
||||
const v = this.emitExpr(s.value);
|
||||
this.emitPublishedFieldGuard(obj.name, s.className, s.field);
|
||||
this.emitStoreGuard(obj.name, s);
|
||||
const { ptr } = this.classFieldPtr(obj.name, s.className, s.field);
|
||||
this.storeInt32Field(ptr, v, s.value);
|
||||
break;
|
||||
}
|
||||
const obj = this.emitStableReceiver(s.obj, [s.value]);
|
||||
const v = this.emitExpr(s.value);
|
||||
if (s.kind === "fieldSet") this.emitPublishedFieldGuard(obj.name, s.className, s.field);
|
||||
if (s.kind === "fieldSet") this.emitStoreGuard(obj.name, s);
|
||||
else if (this.publishedTypes?.recordGuarded(s.shapeId) === true)
|
||||
this.emitPublishedFieldGuard(obj.name, null, s.field);
|
||||
const { ptr, type } =
|
||||
@@ -6608,10 +6888,29 @@ export class LlEmitter {
|
||||
// the actual ret. The parked value owns a synthetic slot-backed
|
||||
// scope entry during each copy so a throwing finally releases it.
|
||||
let v: LlValue | null = null;
|
||||
if (s.value !== null && this.currentBorrowedReturn) {
|
||||
const moved = this.finallyStack.length === 0 ? this.movableReturn(s.value) : null;
|
||||
if (moved !== null) {
|
||||
// Returning an owned local hands its reference to the caller: no
|
||||
// retain now and no release of the local below.
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ptr, ptr ${moved.entry.slot}`);
|
||||
v = { name: t, type: s.value!.type };
|
||||
moved.scope.splice(moved.scope.indexOf(moved.entry), 1);
|
||||
this.releaseForJump(0, 0);
|
||||
moved.scope.splice(moved.index, 0, moved.entry);
|
||||
} else if (s.value !== null && this.currentBorrowedReturn) {
|
||||
// A borrowed-return body hands back its walk without a reference
|
||||
// (it has no try statements, so no finally can intervene).
|
||||
v = this.emitReadReceiver(s.value);
|
||||
// (it has no try statements, so no finally can intervene). An
|
||||
// optional element read lends the element its array keeps alive.
|
||||
const read = this.optionalArrayReads.get(s.value);
|
||||
if (read) {
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
emitLocalArrayRead(this, { ...read, borrow: true }, slot);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ptr, ptr ${slot}`);
|
||||
v = { name: t, type: s.value.type };
|
||||
} else v = this.emitReadReceiver(s.value);
|
||||
} else if (s.value !== null) {
|
||||
v = this.emitExpr(s.value);
|
||||
this.moveTemp(v);
|
||||
@@ -6643,7 +6942,7 @@ export class LlEmitter {
|
||||
this.scopes = savedScopes;
|
||||
this.finallyStack = savedFinally;
|
||||
this.tryStack = savedTry;
|
||||
} else {
|
||||
} else if (moved === null) {
|
||||
this.releaseForJump(0, 0);
|
||||
}
|
||||
if (!B.isTerminated()) {
|
||||
@@ -6898,7 +7197,7 @@ export class LlEmitter {
|
||||
const caseLabels = s.cases.map(() => B.newLabel("sw.c"));
|
||||
const defaultIdx = s.cases.findIndex((c) => c.test === null);
|
||||
const fallback = defaultIdx >= 0 ? caseLabels[defaultIdx]! : end;
|
||||
if (!emitLiteralSwitch(this, disc, s.cases, caseLabels, fallback)) {
|
||||
if (!emitLiteralSwitch(this, disc, s.disc, s.cases, caseLabels, fallback)) {
|
||||
s.cases.forEach((c, i) => {
|
||||
if (c.test === null) return;
|
||||
// Lazy source-order test evaluation (a test after the match never
|
||||
@@ -7354,6 +7653,13 @@ export class LlEmitter {
|
||||
];
|
||||
}
|
||||
|
||||
/** The frozen-object guard of a class field store, unless the store
|
||||
* provably targets a still-private `this` (PublishedTypes.privateStores). */
|
||||
private emitStoreGuard(obj: string, s: Extract<IrStmt, { kind: "fieldSet" }>): void {
|
||||
if (this.publishedTypes?.privateStores.has(s) === true) return;
|
||||
this.emitPublishedFieldGuard(obj, s.className, s.field);
|
||||
}
|
||||
|
||||
/** The frozen-object guard of a class field or record field store:
|
||||
* Node's "Cannot assign to read only property" TypeError when the target
|
||||
* is published. `className` null means a record (`#<Object>`). */
|
||||
@@ -7510,6 +7816,10 @@ export class LlEmitter {
|
||||
if (result.owner) this.ownSlot(result.owner.slot, result.owner.type);
|
||||
return result.value;
|
||||
}
|
||||
// A borrowed-return helper (such as an optional element read) lends its
|
||||
// result like any borrowed-return call.
|
||||
if (e.kind === "call" && this.borrowedReturns.has(e.callee) && this.canBorrowReceiver(e))
|
||||
return emitBorrowedResultCall(this, e);
|
||||
const arrayRead = this.optionalArrayReads.get(e);
|
||||
if (arrayRead) return emitProjectedArrayRead(this, arrayRead);
|
||||
const narrow = this.checkedNarrows.get(e);
|
||||
@@ -7775,17 +8085,21 @@ export class LlEmitter {
|
||||
const wrap = this.borrowableNullableWrap(e);
|
||||
return wrap === "unit" || (wrap === "ref" && this.canBorrowReceiver(e.value));
|
||||
}
|
||||
// A borrowed-return callee preserves edges, so its result stays
|
||||
// reachable from borrowable arguments until the consumer runs.
|
||||
case "call":
|
||||
return (
|
||||
this.borrowedReturns.has(e.callee) &&
|
||||
e.args.every(
|
||||
(arg) =>
|
||||
(!isRefCounted(arg.type) || this.canBorrowReceiver(arg)) &&
|
||||
this.referenceEffects.preserves(arg),
|
||||
)
|
||||
);
|
||||
// A borrowed-return result stays reachable from its borrowed
|
||||
// arguments until the consumer runs. Each argument is lent without a
|
||||
// snapshot, so neither the callee nor a later argument may replace
|
||||
// what it was read from: a callee that preserves every edge cannot,
|
||||
// and any other one must leave the argument's locations unwritten.
|
||||
case "call": {
|
||||
if (!this.borrowedReturns.has(e.callee)) return false;
|
||||
if (!e.args.every((arg) => this.referenceEffects.preserves(arg))) return false;
|
||||
const preserving = this.referenceEffects.functions.has(e.callee);
|
||||
if (preserving)
|
||||
return e.args.every((arg) => !isRefCounted(arg.type) || this.canBorrowReceiver(arg));
|
||||
if (this.referenceWrites === null) return false;
|
||||
const lent = borrowableInputs(this, e.args, false, this.referenceWrites.summary(e.callee));
|
||||
return e.args.every((arg, i) => !isRefCounted(arg.type) || lent[i] === true);
|
||||
}
|
||||
// A required element read: the (borrowed) array owns the element
|
||||
// while an edge-preserving index computes.
|
||||
case "arrayGet":
|
||||
@@ -7833,17 +8147,20 @@ export class LlEmitter {
|
||||
const indexes = this.callLifetimes.borrowed.get(fn.name);
|
||||
return new Set(fn.params.filter((_, i) => indexes?.has(i)).map((p) => p.localId));
|
||||
};
|
||||
const hostFor = (fn: IrFunction) => {
|
||||
const hostFor = (fn: IrFunction): WalkBorrowHost => {
|
||||
const allowed = borrowedIds(fn);
|
||||
return {
|
||||
const host = {
|
||||
pointerLocal: (local: IrLocal) => pointer(local.type),
|
||||
borrowsWithoutOwning: (e: IrExpr) => this.borrowsWithoutOwning(e),
|
||||
borrowedReturn: (callee: string) => this.borrowedReturns.has(callee),
|
||||
parameterAllowed: (id: string) => allowed.has(id),
|
||||
};
|
||||
return this.referenceEffects.functions.has(fn.name) ? host : this.unwrittenWalkHost(fn, host);
|
||||
};
|
||||
const preserving = [...this.fnByName.values()].filter((fn) =>
|
||||
this.referenceEffects.functions.has(fn.name),
|
||||
// With ReferenceWrites, a body that writes none of its walks' locations
|
||||
// qualifies like one that preserves every edge.
|
||||
const preserving = [...this.fnByName.values()].filter(
|
||||
(fn) => this.referenceEffects.functions.has(fn.name) || this.referenceWrites !== null,
|
||||
);
|
||||
const candidates = preserving.filter((fn) => {
|
||||
if (!pointer(fn.returnType) || fn.async || fn.generator || fn.captures) return false;
|
||||
@@ -7863,6 +8180,8 @@ export class LlEmitter {
|
||||
if (this.borrowedReturns.has(fn.name)) continue;
|
||||
if (!analyzeWalks(fn, hostFor(fn)).returnsWalk) continue;
|
||||
this.borrowedReturns.add(fn.name);
|
||||
// Nested borrowed returns extend the locations a caller depends on.
|
||||
this.borrowedReturnKeyCache.clear();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
@@ -7887,6 +8206,14 @@ export class LlEmitter {
|
||||
return this.nullableFieldGet(e) === null;
|
||||
case "recordGet":
|
||||
return true;
|
||||
// emitReadReceiver lends a required element of a borrowed array when
|
||||
// computing the index removes no edge.
|
||||
case "arrayGet":
|
||||
return (
|
||||
e.arr.type.kind === "array" &&
|
||||
this.optionalArrayReads.get(e) === null &&
|
||||
this.referenceEffects.preserves(e.index)
|
||||
);
|
||||
case "unionNarrow":
|
||||
return this.nullableUnions.has(e.unionId);
|
||||
case "downcast":
|
||||
|
||||
@@ -25,7 +25,13 @@ import { f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl } from "./
|
||||
import { canStackUnion, emitStackUnion } from "./stack-unions.js";
|
||||
import { emitCallArrayRead } from "./local-array-reads.js";
|
||||
import { emitStackMapRead } from "./map-read-lifetimes.js";
|
||||
import { borrowableInputs, emitBorrowedInput, emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
import {
|
||||
borrowableInputs,
|
||||
emitBorrowedInput,
|
||||
emitBorrowedInputs,
|
||||
inputSuffixWrites,
|
||||
} from "./borrowed-inputs.js";
|
||||
import type { Writes } from "./reference-writes.js";
|
||||
|
||||
export function emitCallExpr(
|
||||
host: LlvmEmitterContext,
|
||||
@@ -58,12 +64,18 @@ export function emitCallExpr(
|
||||
const borrowed = host.callLifetimes.borrowed.get(e.callee);
|
||||
const projected = host.callLifetimes.parameters.get(e.callee);
|
||||
const inputs = borrowed
|
||||
? borrowableInputs(host, e.args, host.referenceEffects.functions.has(e.callee))
|
||||
? borrowableInputs(
|
||||
host,
|
||||
e.args,
|
||||
host.referenceEffects.functions.has(e.callee),
|
||||
host.referenceWrites?.summary(e.callee),
|
||||
)
|
||||
: [];
|
||||
// Borrowed snapshots live through this call, not through the whole
|
||||
// enclosing expression. Otherwise a large initializer accumulates
|
||||
// owners and repeats their cleanup at every later throwing call.
|
||||
if (borrowed) host.frames.push([]);
|
||||
let suffixes: Writes[] | undefined;
|
||||
const args = e.args.map((a, index) => {
|
||||
if (borrowed?.has(index)) {
|
||||
// A heap-borrowing helper may retain or return its argument.
|
||||
@@ -73,7 +85,18 @@ export function emitCallExpr(
|
||||
const callback = host.stackCallbacks.emit(host, a);
|
||||
if (callback) return callback;
|
||||
const read = host.callArrayReads.get(a);
|
||||
if (read) return emitCallArrayRead(host, read);
|
||||
if (read) {
|
||||
suffixes ??= inputSuffixWrites(
|
||||
host,
|
||||
e.args,
|
||||
host.referenceWrites?.summary(e.callee) ?? null,
|
||||
);
|
||||
const lend =
|
||||
!read.borrow &&
|
||||
index < suffixes.length &&
|
||||
host.unwrittenCallArrayRead(read, suffixes[index]!);
|
||||
return emitCallArrayRead(host, lend ? { ...read, borrow: true } : read);
|
||||
}
|
||||
const mapRead = host.mapReadLifetimes.arguments.get(a);
|
||||
if (mapRead) {
|
||||
const result = emitStackMapRead(host, mapRead);
|
||||
@@ -739,6 +762,7 @@ export function emitCallExpr(
|
||||
host,
|
||||
e.args,
|
||||
host.referenceEffects.functions.has(ctorName),
|
||||
host.referenceWrites?.constructWrites(e.className),
|
||||
);
|
||||
host.frames.push([]);
|
||||
const args = e.args.map((a, i) => {
|
||||
@@ -951,7 +975,15 @@ export function emitCallExpr(
|
||||
// (LlEmitter.computeVirtualBorrowing): those are passed like borrowed
|
||||
// arguments of a direct call, owned snapshots living in this frame.
|
||||
const borrowed = host.virtualSlotBorrowed(meta.root.def.name, slotIdx);
|
||||
const inputs = borrowed.size > 0 ? borrowableInputs(host, e.args, false) : [];
|
||||
const inputs =
|
||||
borrowed.size > 0
|
||||
? borrowableInputs(
|
||||
host,
|
||||
e.args,
|
||||
false,
|
||||
host.referenceWrites?.virtualWrites(e.className, e.method),
|
||||
)
|
||||
: [];
|
||||
if (borrowed.size > 0) host.frames.push([]);
|
||||
const args = e.args.map((a, index) =>
|
||||
borrowed.has(index) && inputs[index] ? host.emitReadReceiver(a) : host.emitExpr(a),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { isStableReceiverOperand, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { type IrExpr, type IrType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js";
|
||||
import { mangleResolveThunk } from "../mangle.js";
|
||||
import {
|
||||
arrOps,
|
||||
elemAccess,
|
||||
FN_ATTRS,
|
||||
mapKeyAccess,
|
||||
@@ -23,11 +24,13 @@ import {
|
||||
borrowsMapReadInputs,
|
||||
borrowsMapMutationReceiver,
|
||||
inlineNumberKey,
|
||||
entryStringKey,
|
||||
} from "./map-read-lifetimes.js";
|
||||
import {
|
||||
emitNumberMapGetRef,
|
||||
emitNumberMapGetScalar,
|
||||
emitNumberMapHas,
|
||||
emitStringMapGetRef,
|
||||
} from "./map-number-lookup.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { emitStringSliceRead } from "./string-slices.js";
|
||||
@@ -1119,12 +1122,15 @@ export function emitMapLikeIntrinsic(
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
const numberKey = inlineNumberKey(k);
|
||||
const stringKey = entryStringKey(k);
|
||||
if (value.kind === "union") {
|
||||
const isnull = B.tmp();
|
||||
const t = B.tmp();
|
||||
let raw: string;
|
||||
if (numberKey !== null) {
|
||||
raw = emitNumberMapGetRef(host, r.name, numberKey, value);
|
||||
} else if (stringKey !== null) {
|
||||
raw = emitStringMapGetRef(host, r.name, stringKey, value);
|
||||
} else {
|
||||
host.declare(`declare ptr @scr_map_get_${kAcc}_ref(ptr, ${kTy})`);
|
||||
raw = B.tmp();
|
||||
@@ -1197,6 +1203,10 @@ export function emitMapLikeIntrinsic(
|
||||
const raw = emitNumberMapGetRef(host, r.name, numberKey, value);
|
||||
return host.wrapNullable(raw, raw, value, valueTag, e.type, undefTag);
|
||||
}
|
||||
if (stringKey !== null && isRefCounted(value)) {
|
||||
const raw = emitStringMapGetRef(host, r.name, stringKey, value);
|
||||
return host.wrapNullable(raw, raw, value, valueTag, e.type, undefTag);
|
||||
}
|
||||
host.declare(`declare ptr @scr_map_get_${kAcc}_ref(ptr, ${kTy})`);
|
||||
const raw = B.tmp();
|
||||
B.line(`${raw} = call ptr @scr_map_get_${kAcc}_ref(ptr ${r.name}, ${k.args})`);
|
||||
@@ -1290,10 +1300,18 @@ export function emitMapLikeIntrinsic(
|
||||
return { name: "", type: e.type };
|
||||
case "toArray": {
|
||||
if (receiverType.kind !== "set") throw new InternalCompilerError("unreachable");
|
||||
// Fresh +1 elem[] of the live entries in insertion order.
|
||||
host.declare(`declare ptr @scr_set_to_arr_${kAcc}(ptr)`);
|
||||
// Fresh +1 elem[] of the live entries in insertion order; a REF result
|
||||
// takes its element type's array descriptor.
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ptr @scr_set_to_arr_${kAcc}(ptr ${r.name})`);
|
||||
if (kAcc === "ref") {
|
||||
host.declare(`declare ptr @scr_set_to_arr_ref(ptr, ptr)`);
|
||||
B.line(
|
||||
`${t} = call ptr @scr_set_to_arr_ref(ptr ${r.name}, ptr ${arrOps(host.shapeHost, receiverType.elem)})`,
|
||||
);
|
||||
} else {
|
||||
host.declare(`declare ptr @scr_set_to_arr_${kAcc}(ptr)`);
|
||||
B.line(`${t} = call ptr @scr_set_to_arr_${kAcc}(ptr ${r.name})`);
|
||||
}
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js";
|
||||
import { BlockBuilder } from "./blocks.js";
|
||||
import { classFieldIndex, classStructSym } from "./classes.js";
|
||||
import { llvmCommentText } from "./common.js";
|
||||
import { SCR_ARR_FIELD, llvmCommentText } from "./common.js";
|
||||
import {
|
||||
FN_ATTRS,
|
||||
elemAccess,
|
||||
@@ -176,9 +176,9 @@ export function streamTypedRefCommitAdapter(
|
||||
const storageMembers = [
|
||||
{ name: "len", index: 1, type: host.sizeType },
|
||||
{ name: "cap", index: 2, type: host.sizeType },
|
||||
{ name: "data", index: 7, type: "ptr" },
|
||||
{ name: "present", index: 8, type: "ptr" },
|
||||
{ name: "ext", index: 9, type: "ptr" },
|
||||
{ name: "data", index: SCR_ARR_FIELD.data, type: "ptr" },
|
||||
{ name: "present", index: SCR_ARR_FIELD.present, type: "ptr" },
|
||||
{ name: "ext", index: SCR_ARR_FIELD.ext, type: "ptr" },
|
||||
];
|
||||
for (const member of storageMembers) {
|
||||
lines.push(
|
||||
|
||||
@@ -12,7 +12,7 @@ import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { ReferenceEffects } from "./reference-effects.js";
|
||||
import { exactIndexLines } from "./common.js";
|
||||
import { SCR_ARR_FIELD, exactIndexLines } from "./common.js";
|
||||
|
||||
export interface LocalArrayRead {
|
||||
type: IrType;
|
||||
@@ -513,7 +513,9 @@ function emitDenseReferenceArrayRead(
|
||||
statePtr = B.tmp(),
|
||||
denseState = B.tmp(),
|
||||
densePresent = B.tmp();
|
||||
B.line(`${statesPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 8`);
|
||||
B.line(
|
||||
`${statesPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.present}`,
|
||||
);
|
||||
host.markMemoryPointer(statesPtr, "array:header");
|
||||
B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`);
|
||||
B.line(`${statePtr} = getelementptr inbounds i8, ptr ${states}, ${host.sizeType} ${offset}`);
|
||||
@@ -526,7 +528,9 @@ function emitDenseReferenceArrayRead(
|
||||
data = B.tmp(),
|
||||
valuePtr = B.tmp(),
|
||||
raw = B.tmp();
|
||||
B.line(`${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 7`);
|
||||
B.line(
|
||||
`${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.data}`,
|
||||
);
|
||||
host.markMemoryPointer(dataPtr, "array:header");
|
||||
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
|
||||
B.line(`${valuePtr} = getelementptr inbounds i64, ptr ${data}, ${host.sizeType} ${offset}`);
|
||||
|
||||
@@ -3,18 +3,22 @@ import { everyStmtList } from "../../ir/traverse.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { ReferenceEffects } from "./reference-effects.js";
|
||||
import type { OptionalArrayReads } from "./local-array-reads.js";
|
||||
import type { ReferenceWrites } from "./reference-writes.js";
|
||||
|
||||
/** Limit a borrow to a loop whose complete iteration preserves heap edges.
|
||||
* Initialization and unrelated work elsewhere in the function may mutate.
|
||||
* The array remains owned by its unchanged local or global binding; the
|
||||
* iteration binding can only project it or forward it to proven consumers.
|
||||
* Callbacks, suspension, capture, reference writes, and escaping uses keep
|
||||
* the ordinary owned read. Missing elements retain the runtime's checks. */
|
||||
* the ordinary owned read. Missing elements retain the runtime's checks.
|
||||
* With ReferenceWrites, an iteration that writes no array element (nor the
|
||||
* binding holding the array) suffices, whatever else it writes. */
|
||||
export function findLoopArrayBorrows(
|
||||
fn: IrFunction,
|
||||
lifetimes: CallLifetimes,
|
||||
effects: ReferenceEffects,
|
||||
optionalReads?: OptionalArrayReads,
|
||||
writes?: ReferenceWrites | null,
|
||||
): ReadonlySet<IrStmt> {
|
||||
const result = new Set<IrStmt>();
|
||||
if (fn.async || fn.generator) return result;
|
||||
@@ -45,19 +49,28 @@ export function findLoopArrayBorrows(
|
||||
!local.tdz &&
|
||||
projected?.has(loop.localId) &&
|
||||
bindings?.has(loop.localId) &&
|
||||
effects.preservesScope(loop.body) &&
|
||||
effects.preserves(loop.iterable)
|
||||
((effects.preservesScope(loop.body) && effects.preserves(loop.iterable)) ||
|
||||
// The loop owns the evaluated array; only element removals can
|
||||
// release an element it lends.
|
||||
(writes && !writes.conflicts(["a"], writes.stmts(loop.body))))
|
||||
)
|
||||
result.add(loop);
|
||||
return true;
|
||||
}
|
||||
if (loop.kind !== "for" && loop.kind !== "while" && loop.kind !== "doWhile") return true;
|
||||
if (
|
||||
!effects.preservesScope(loop.body) ||
|
||||
(loop.cond && !effects.preserves(loop.cond)) ||
|
||||
(loop.kind === "for" && loop.update && !effects.preservesScope([loop.update]))
|
||||
)
|
||||
return true;
|
||||
const preserving =
|
||||
effects.preservesScope(loop.body) &&
|
||||
(!loop.cond || effects.preserves(loop.cond)) &&
|
||||
(loop.kind !== "for" || !loop.update || effects.preservesScope([loop.update]));
|
||||
const loopWrites =
|
||||
preserving || !writes
|
||||
? null
|
||||
: writes.union([
|
||||
writes.stmts(loop.body),
|
||||
loop.cond ? writes.expr(loop.cond) : null,
|
||||
loop.kind === "for" && loop.update ? writes.stmt(loop.update) : null,
|
||||
]);
|
||||
if (!preserving && !writes) return true;
|
||||
everyStmtList(loop.body, {
|
||||
expr: () => true,
|
||||
stmt: (stmt) => {
|
||||
@@ -71,7 +84,12 @@ export function findLoopArrayBorrows(
|
||||
return true;
|
||||
const source =
|
||||
stmt.init.kind === "arrayGet" ? stmt.init.arr : optionalReads?.get(stmt.init)?.array;
|
||||
if (source && stableSource(source)) result.add(stmt);
|
||||
if (!source || !stableSource(source)) return true;
|
||||
if (preserving) result.add(stmt);
|
||||
else {
|
||||
const keys = writes!.readKeys(source);
|
||||
if (keys && !writes!.conflicts(["a", ...keys], loopWrites)) result.add(stmt);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
* scr_map_entry_f64 otherwise. The generated code then reads the entry's
|
||||
* value slot itself and retains a reference value with the value type's own
|
||||
* retain, which is the adapter the collection was constructed with.
|
||||
* String-keyed reference reads use the same entry contract through
|
||||
* scr_map_entry_str, so their retain is inline rather than the runtime's
|
||||
* indirect adapter call.
|
||||
*/
|
||||
import type { IrType } from "../../ir/ir.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
@@ -31,6 +34,16 @@ export function emitNumberMapSlot(host: LlvmEmitterContext, entry: string, type:
|
||||
return value;
|
||||
}
|
||||
|
||||
export const STRING_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_str(ptr, ptr)";
|
||||
|
||||
/** The entry for a string `key`, or null when absent. Valid until the next mutation. */
|
||||
export function emitStringMapEntry(host: LlvmEmitterContext, map: string, key: string): string {
|
||||
host.declare(STRING_MAP_ENTRY_DECL);
|
||||
const entry = host.B.tmp();
|
||||
host.B.line(`${entry} = call ptr @scr_map_entry_str(ptr ${map}, ptr ${key})`);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/** Map.get of a reference value: a +1 value, or null when absent — the
|
||||
* contract of scr_map_get_f64_ref. */
|
||||
export function emitNumberMapGetRef(
|
||||
@@ -39,8 +52,24 @@ export function emitNumberMapGetRef(
|
||||
key: string,
|
||||
value: IrType,
|
||||
): string {
|
||||
return emitMapEntryGetRef(host, emitNumberMapEntry(host, map, key), value);
|
||||
}
|
||||
|
||||
/** Map.get of a reference value under a string key, with scr_map_get_str_ref's
|
||||
* contract. The value type's own retain replaces the collection's adapter, so
|
||||
* the retain is inline instead of an indirect call in the runtime. */
|
||||
export function emitStringMapGetRef(
|
||||
host: LlvmEmitterContext,
|
||||
map: string,
|
||||
key: string,
|
||||
value: IrType,
|
||||
): string {
|
||||
return emitMapEntryGetRef(host, emitStringMapEntry(host, map, key), value);
|
||||
}
|
||||
|
||||
/** The +1 value of a found entry, or null for a missing (null) entry. */
|
||||
function emitMapEntryGetRef(host: LlvmEmitterContext, entry: string, value: IrType): string {
|
||||
const B = host.B;
|
||||
const entry = emitNumberMapEntry(host, map, key);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
const hit = B.newLabel("mapn.hit"),
|
||||
|
||||
@@ -111,6 +111,11 @@ test("local map results keep scalar and owned reference payloads without heap wr
|
||||
expect(ir).toContain("call ptr @sc_map_entry_f64(");
|
||||
expect(ir).not.toContain("@scr_map_get_");
|
||||
} else if (value === BOOL) expect(ir).toMatch(/zext i8 %\w+ to i64/);
|
||||
else if (value === STRING) {
|
||||
// String keys read reference values through the entry and retain inline.
|
||||
expect(ir).toContain("call ptr @scr_map_entry_str(");
|
||||
expect(ir).not.toContain("@scr_map_get_str_ref");
|
||||
}
|
||||
if (value === STRING) expect(ir).toContain("@scr_str_release");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { emitNumberMapGetRef, emitNumberMapGetScalar } from "./map-number-lookup.js";
|
||||
import {
|
||||
emitNumberMapGetRef,
|
||||
emitNumberMapGetScalar,
|
||||
emitStringMapGetRef,
|
||||
} from "./map-number-lookup.js";
|
||||
import { mapKeyAccess, mapKeyParamType } from "./shapes.js";
|
||||
|
||||
function hasTypedCollectionReceiver(
|
||||
@@ -167,6 +171,11 @@ export function inlineNumberKey(key: { access: string; value?: string }): string
|
||||
return key.access === "f64" && key.value !== undefined ? key.value : null;
|
||||
}
|
||||
|
||||
/** The key's string operand when a reference read can take the entry path. */
|
||||
export function entryStringKey(key: { access: string; value?: string }): string | null {
|
||||
return key.access === "str" && key.value !== undefined ? key.value : null;
|
||||
}
|
||||
|
||||
export interface StackMapRead {
|
||||
value: LlValue;
|
||||
owner: { slot: string; type: IrType } | null;
|
||||
@@ -205,9 +214,14 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
|
||||
B.line(`store i64 ${scalar.bits}, ptr ${payload}`);
|
||||
}
|
||||
} else if (isRefCounted(read.value)) {
|
||||
const raw = B.tmp();
|
||||
host.declare(`declare ptr @scr_map_get_${access}_ref(ptr, ${keyType})`);
|
||||
B.line(`${raw} = call ptr @scr_map_get_${access}_ref(ptr ${receiver.name}, ${key.args})`);
|
||||
const stringKey = entryStringKey(key);
|
||||
let raw: string;
|
||||
if (stringKey !== null) raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value);
|
||||
else {
|
||||
raw = B.tmp();
|
||||
host.declare(`declare ptr @scr_map_get_${access}_ref(ptr, ${keyType})`);
|
||||
B.line(`${raw} = call ptr @scr_map_get_${access}_ref(ptr ${receiver.name}, ${key.args})`);
|
||||
}
|
||||
B.line(`${found} = icmp ne ptr ${raw}, null`);
|
||||
B.line(`store i64 0, ptr ${payload}`);
|
||||
B.line(`store ptr ${raw}, ptr ${payload}`);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@ import {
|
||||
mangleRecordTrace,
|
||||
} from "../mangle.js";
|
||||
import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js";
|
||||
import { llvmCommentText } from "./common.js";
|
||||
import { SCR_ARR_FIELD, llvmCommentText } from "./common.js";
|
||||
import type { NullableUnions } from "./nullable-unions.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
@@ -197,7 +197,7 @@ export function releaseSym(host: ShapeHost, t: IrType): string {
|
||||
|
||||
/** How a runtime family decides whether an object carries a ScrCycHdr:
|
||||
* never, always, or per object through the trace slots its C release
|
||||
* tests (`a->elem_trace`, `m->key_trace || m->val_trace`). */
|
||||
* tests (`a->cyc`, `m->key_trace || m->val_trace`). */
|
||||
type RcCycle = "none" | "always" | "arr" | "map";
|
||||
|
||||
interface InlineRcFamily {
|
||||
@@ -318,9 +318,9 @@ export function inlineRcDecls(host: ShapeHost): string[] {
|
||||
function rcHeaderTest(cycle: "arr" | "map", yes: string, no: string): string[] {
|
||||
if (cycle === "arr") {
|
||||
return [
|
||||
` %tracep = getelementptr inbounds %ScrArr, ptr %o, i32 0, i32 6 ; elem_trace`,
|
||||
` %trace = load ptr, ptr %tracep`,
|
||||
` %headered = icmp ne ptr %trace, null`,
|
||||
` %cycp = getelementptr inbounds %ScrArr, ptr %o, i32 0, i32 ${SCR_ARR_FIELD.cyc} ; cyc`,
|
||||
` %cyc = load i32, ptr %cycp`,
|
||||
` %headered = icmp ne i32 %cyc, 0`,
|
||||
` br i1 %headered, label %${yes}, label %${no}`,
|
||||
];
|
||||
}
|
||||
@@ -566,7 +566,7 @@ function elemKindNum(elem: IrType): number {
|
||||
/** Array construction call text: ref elements
|
||||
* (records, unions, closures — and cycle-capable inner arrays, whose
|
||||
* SCR_ELEM_ARR spelling would hide them from the outer array's trace)
|
||||
* construct through scr_arr_new_ref with the element type's `_v` RC entry
|
||||
* construct through scr_arr_new_ops with the element type's `_v` RC entry
|
||||
* points; every other element kind keeps the plain scr_arr_new call. */
|
||||
export function arrNewCall(host: ShapeHost, elem: IrType, capText: string): string {
|
||||
const useRef =
|
||||
@@ -590,9 +590,21 @@ export function arrNewCall(host: ShapeHost, elem: IrType, capText: string): stri
|
||||
host.declare(`declare ptr @scr_arr_new(i32, ${host.sizeType})`);
|
||||
return `call ptr @scr_arr_new(i32 ${elemKindNum(elem)}, ${host.sizeType} ${capText})`;
|
||||
}
|
||||
host.declare(`declare ptr @scr_arr_new_ops(ptr, ${host.sizeType})`);
|
||||
return `call ptr @scr_arr_new_ops(ptr ${arrOps(host, elem)}, ${host.sizeType} ${capText})`;
|
||||
}
|
||||
|
||||
/** The constant %ScrArrOps descriptor (retain, release, trace) of a REF
|
||||
* element type, defined once per module and shared by all its arrays. */
|
||||
export function arrOps(host: ShapeHost, elem: IrType): string {
|
||||
const v = vAdapters(host, elem);
|
||||
host.declare(`declare ptr @scr_arr_new_ref(ptr, ptr, ptr, ${host.sizeType})`);
|
||||
return `call ptr @scr_arr_new_ref(ptr ${v.retain}, ptr ${v.release}, ptr ${traceArg(host, elem)}, ${host.sizeType} ${capText})`;
|
||||
const trace = traceArg(host, elem);
|
||||
const part = (sym: string): string => sym.replace(/^@/, "").replace(/[^A-Za-z0-9_.]/g, "_");
|
||||
const name = `@sc_arrops.${part(v.retain)}.${part(v.release)}.${part(trace)}`;
|
||||
host.declare(
|
||||
`${name} = internal constant %ScrArrOps { ptr ${v.retain}, ptr ${v.release}, ptr ${trace} }`,
|
||||
);
|
||||
return name;
|
||||
}
|
||||
|
||||
/* ── capture boxes ────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -167,18 +167,23 @@ test("string operands preserve their left-to-right snapshot when a later assignm
|
||||
expect(facts(f).parameters.has("replace")).toBe(false);
|
||||
});
|
||||
|
||||
test("a later global write keeps mutable string arguments owned", () => {
|
||||
test("a callee's global write cannot replace the caller's mutable string locals", () => {
|
||||
const source = fn("source", [], str("next"));
|
||||
source.body.unshift({ kind: "assign", localId: "%g.changed", value: str("effect"), loc });
|
||||
const compare = fn("compare", ["text"], equal(ref("text"), call("source", [], STRING)));
|
||||
// A later write anywhere in this function rules out parameter borrowing.
|
||||
// A write anywhere in this function rules out parameter borrowing, but
|
||||
// the rebound local itself owns its value across the later operand.
|
||||
compare.body.unshift({ kind: "assign", localId: "text", value: str("first"), loc });
|
||||
const module = mod(source, compare);
|
||||
module.globals = [{ id: "%g.changed", name: "changed", type: STRING, mutable: true }];
|
||||
const ir = body(module, "sc_f_compare");
|
||||
const sourceCall = ir.indexOf("@sc_f_source");
|
||||
expect(sourceCall).toBeGreaterThan(0);
|
||||
expect(ir.slice(0, sourceCall)).toContain("@scr_str_retain_v");
|
||||
expect(ir.slice(0, sourceCall)).not.toContain("@scr_str_retain_v");
|
||||
// Reading the global the later operand replaces still takes a snapshot.
|
||||
compare.body = [ret(equal(ref("%g.changed"), call("source", [], STRING)))];
|
||||
const global = body(module, "sc_bf_compare");
|
||||
expect(global.slice(0, global.indexOf("@sc_f_source"))).toContain("@scr_str_retain_v");
|
||||
});
|
||||
|
||||
test("immutable lexical owners survive arbitrary later operands", () => {
|
||||
@@ -214,8 +219,22 @@ test("reference fields borrow across preserving calls but snapshot across writes
|
||||
source.locals.push({ id: "changed", name: "changed", type: STRING, mutable: true });
|
||||
source.body.unshift({ kind: "assign", localId: "changed", value: str("effect"), loc });
|
||||
expect(body(module, "sc_bf_compare")).not.toContain("@scr_str_retain_v");
|
||||
// A global write replaces no record field: the projection still borrows.
|
||||
source.body.unshift({ kind: "assign", localId: "%g.changed", value: str("effect"), loc });
|
||||
module.globals = [{ id: "%g.changed", name: "changed", type: STRING, mutable: true }];
|
||||
module.globals = [
|
||||
{ id: "%g.changed", name: "changed", type: STRING, mutable: true },
|
||||
{ id: "%g.record", name: "record", type: record, mutable: true },
|
||||
];
|
||||
expect(body(module, "sc_bf_compare")).not.toContain("@scr_str_retain_v");
|
||||
// Writing the projected field of any record does replace it.
|
||||
source.body.unshift({
|
||||
kind: "recordSet",
|
||||
obj: ref("%g.record", record),
|
||||
shapeId: "text",
|
||||
field: "value",
|
||||
value: str("effect"),
|
||||
loc,
|
||||
});
|
||||
expect(body(module, "sc_bf_compare")).toContain("@scr_str_retain_v");
|
||||
f.body = [ret(equal(str("constant"), projection))];
|
||||
expect(body(module, "sc_bf_compare")).not.toContain("@scr_str_retain_v");
|
||||
@@ -342,7 +361,7 @@ test.each(methodCases)(
|
||||
},
|
||||
);
|
||||
|
||||
test("string search snapshots both receiver and needle before a numeric argument writes globally", () => {
|
||||
test("string search borrows rebound locals but snapshots a global a later argument writes", () => {
|
||||
const position = fn("position", [], num(0));
|
||||
position.body.unshift({ kind: "assign", localId: "%g.changed", value: str("effect"), loc });
|
||||
const search = fn(
|
||||
@@ -361,9 +380,16 @@ test("string search snapshots both receiver and needle before a numeric argument
|
||||
const searchCall = ir.indexOf("@scr_str_index_of");
|
||||
expect(start).toBeGreaterThan(0);
|
||||
expect(searchCall).toBeGreaterThan(start);
|
||||
// Literals need no owner, but both reads take independent snapshots.
|
||||
expect(ir.slice(0, start).match(/@scr_str_retain_v/g)).toHaveLength(2);
|
||||
expect(ir.slice(searchCall)).toContain("@scr_str_release");
|
||||
// The rebound locals own their values across the call: no snapshots.
|
||||
expect(ir.slice(0, start)).not.toContain("@scr_str_retain_v");
|
||||
// A receiver read from the global the numeric argument replaces does.
|
||||
search.body = [
|
||||
ret(intrinsic(ref("%g.changed"), "indexOf", [ref("needle"), call("position", [], F64)], F64)),
|
||||
];
|
||||
const global = body(module, "sc_bf_search");
|
||||
const call2 = global.indexOf("@sc_f_position");
|
||||
expect(global.slice(0, call2).match(/@scr_str_retain_v/g)).toHaveLength(1);
|
||||
expect(global.slice(global.indexOf("@scr_str_index_of"))).toContain("@scr_str_release");
|
||||
});
|
||||
|
||||
test("global string ownership cannot be inferred from an immutable-looking reference", () => {
|
||||
|
||||
@@ -7,6 +7,11 @@ const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, l
|
||||
const string = (value: string): IrExpr => ({ kind: "strLit", value, type: STRING, loc });
|
||||
|
||||
function emit(type: IrType, tests: (IrExpr | null)[], pointerBits: 32 | 64 = 64): string {
|
||||
return emitWith({ kind: "varRef", localId: "value", type, loc }, tests, pointerBits);
|
||||
}
|
||||
|
||||
function emitWith(disc: IrExpr, tests: (IrExpr | null)[], pointerBits: 32 | 64 = 64): string {
|
||||
const type = disc.kind === "varRef" ? disc.type : F64;
|
||||
const mod: IrModule = {
|
||||
irVersion: 15,
|
||||
sourceFile: loc.file,
|
||||
@@ -22,7 +27,7 @@ function emit(type: IrType, tests: (IrExpr | null)[], pointerBits: 32 | 64 = 64)
|
||||
body: [
|
||||
{
|
||||
kind: "switch",
|
||||
disc: { kind: "varRef", localId: "value", type, loc },
|
||||
disc,
|
||||
cases: tests.map((test, index) => ({
|
||||
test,
|
||||
body: [{ kind: "return", value: number(index), loc }],
|
||||
@@ -39,22 +44,37 @@ function emit(type: IrType, tests: (IrExpr | null)[], pointerBits: 32 | 64 = 64)
|
||||
)![0];
|
||||
}
|
||||
|
||||
test("integer dispatch guards range and fractions before selecting a native case", () => {
|
||||
for (const [values, conversion, bits] of [
|
||||
[[-2147483648, -1, 0, 2147483647], "fptosi", "i32"],
|
||||
[[0, 1, 2147483648, 4294967295], "fptoui", "i32"],
|
||||
[[-9007199254740991, -1, 0, 9007199254740991], "fptosi", "i64"],
|
||||
test("integer dispatch round-trips a frozen conversion before selecting a native case", () => {
|
||||
for (const [values, bits] of [
|
||||
[[-2147483648, -1, 0, 2147483647], "i32"],
|
||||
[[0, 1, 2147483648, 4294967295], "i64"],
|
||||
[[-9007199254740991, -1, 0, 9007199254740991], "i64"],
|
||||
] as const) {
|
||||
const body = emit(F64, values.map(number));
|
||||
expect(body).toContain(`switch ${bits}`);
|
||||
expect(body).toContain("fcmp oge double");
|
||||
expect(body).toContain("fcmp ole double");
|
||||
expect(body).toContain(`${conversion} double`);
|
||||
expect(body).toContain(`fptosi double`);
|
||||
expect(body).toContain(`freeze ${bits}`);
|
||||
expect(body).toContain(`sitofp ${bits}`);
|
||||
expect(body).toContain("fcmp oeq double");
|
||||
expect(body.indexOf("br i1")).toBeLessThan(body.indexOf(`${conversion} double`));
|
||||
// NaN, infinities and out-of-range values fail the exact round trip;
|
||||
// no separate range tests are needed.
|
||||
expect(body).not.toContain("fcmp oge double");
|
||||
expect(body).not.toContain("fcmp ole double");
|
||||
expect(body.indexOf("freeze")).toBeLessThan(body.indexOf("br i1"));
|
||||
}
|
||||
});
|
||||
|
||||
test("a proven integer discriminant selects without converting", () => {
|
||||
const value: IrExpr = { kind: "varRef", localId: "value", type: F64, loc };
|
||||
const disc: IrExpr = { kind: "bin", op: "|", left: value, right: number(0), type: F64, loc };
|
||||
const body = emitWith(disc, [1, 2, 3, 4].map(number));
|
||||
expect(body).toContain("switch i32");
|
||||
// The ToInt32 coercion of `value | 0` converts once; dispatch adds no
|
||||
// round trip of its own.
|
||||
expect(body).not.toContain("freeze");
|
||||
expect(body).not.toContain("fcmp oeq double");
|
||||
});
|
||||
|
||||
test("duplicate numeric labels keep the first destination and share zero", () => {
|
||||
const body = emit(F64, [-0, 0, 1, 2, 3, 1].map(number));
|
||||
const dispatch = body.split("\n").find((line) => line.includes("switch i32"))!;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { IrExpr, IrStmt } from "../../ir/ir.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { withinInt32 } from "../../ir/integer-ranges.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
|
||||
type SwitchCase = Extract<IrStmt, { kind: "switch" }>["cases"][number];
|
||||
@@ -16,6 +17,7 @@ function integerLiteral(expr: IrExpr): number | null {
|
||||
function emitIntegerSwitch(
|
||||
host: LlvmEmitterContext,
|
||||
disc: LlValue,
|
||||
discExpr: IrExpr | undefined,
|
||||
cases: readonly SwitchCase[],
|
||||
labels: readonly string[],
|
||||
fallback: string,
|
||||
@@ -36,30 +38,49 @@ function emitIntegerSwitch(
|
||||
if (targets.size < 4) return false;
|
||||
|
||||
const B = host.B;
|
||||
const unsigned = min >= 0 && max > 2147483647 && max <= 4294967295;
|
||||
const bits = unsigned || (min >= -2147483648 && max <= 2147483647) ? "i32" : "i64";
|
||||
const low = B.tmp();
|
||||
const high = B.tmp();
|
||||
const inside = B.tmp();
|
||||
const convert = B.newLabel("sw.convert");
|
||||
const dispatch = B.newLabel("sw.integer");
|
||||
B.line(`${low} = fcmp oge double ${disc.name}, ${f64Lit(min)}`);
|
||||
B.line(`${high} = fcmp ole double ${disc.name}, ${f64Lit(max)}`);
|
||||
B.line(`${inside} = and i1 ${low}, ${high}`);
|
||||
B.condBr(inside, convert, fallback);
|
||||
B.startBlock(convert);
|
||||
const integer = B.tmp();
|
||||
const bits = min >= -2147483648 && max <= 2147483647 ? "i32" : "i64";
|
||||
let selector: string;
|
||||
const integer = exactInteger(host, disc, discExpr);
|
||||
if (integer) {
|
||||
// A proven exact integer selects directly. Narrowing is exact when its
|
||||
// range fits the case width; otherwise dispatch on the full value.
|
||||
const wide = widenInteger(host, integer);
|
||||
if (bits === "i64" || !withinInt32(integer.range)) {
|
||||
selector = wide;
|
||||
return terminateSwitch(host, "i64", selector, targets, fallback);
|
||||
}
|
||||
selector = B.tmp();
|
||||
B.line(`${selector} = trunc i64 ${wide} to i32`);
|
||||
return terminateSwitch(host, bits, selector, targets, fallback);
|
||||
}
|
||||
// freeze keeps NaN and out-of-range conversions from producing poison.
|
||||
// The exact round trip then rejects every value that is not an integer
|
||||
// of the case width: NaN and infinities compare unordered, a frozen
|
||||
// out-of-range conversion converts back to a different value, and -0
|
||||
// selects case 0 exactly like strict equality.
|
||||
const raw = B.tmp();
|
||||
const restored = B.tmp();
|
||||
const exact = B.tmp();
|
||||
// Ordered range checks dominate conversion: NaN and out-of-range values
|
||||
// must not produce poison. The round trip rejects fractional inputs.
|
||||
B.line(`${integer} = ${unsigned ? "fptoui" : "fptosi"} double ${disc.name} to ${bits}`);
|
||||
B.line(`${restored} = ${unsigned ? "uitofp" : "sitofp"} ${bits} ${integer} to double`);
|
||||
const dispatch = B.newLabel("sw.integer");
|
||||
selector = B.tmp();
|
||||
B.line(`${raw} = fptosi double ${disc.name} to ${bits}`);
|
||||
B.line(`${selector} = freeze ${bits} ${raw}`);
|
||||
B.line(`${restored} = sitofp ${bits} ${selector} to double`);
|
||||
B.line(`${exact} = fcmp oeq double ${disc.name}, ${restored}`);
|
||||
B.condBr(exact, dispatch, fallback);
|
||||
B.startBlock(dispatch);
|
||||
B.terminate(
|
||||
`switch ${bits} ${integer}, label %${fallback} [ ${[...targets]
|
||||
return terminateSwitch(host, bits, selector, targets, fallback);
|
||||
}
|
||||
|
||||
function terminateSwitch(
|
||||
host: LlvmEmitterContext,
|
||||
bits: "i32" | "i64",
|
||||
selector: string,
|
||||
targets: ReadonlyMap<number, string>,
|
||||
fallback: string,
|
||||
): true {
|
||||
host.B.terminate(
|
||||
`switch ${bits} ${selector}, label %${fallback} [ ${[...targets]
|
||||
.map(([value, label]) => `${bits} ${value}, label %${label}`)
|
||||
.join(" ")} ]`,
|
||||
);
|
||||
@@ -121,11 +142,13 @@ function emitStringSwitch(
|
||||
export function emitLiteralSwitch(
|
||||
host: LlvmEmitterContext,
|
||||
disc: LlValue,
|
||||
discExpr: IrExpr | undefined,
|
||||
cases: readonly SwitchCase[],
|
||||
labels: readonly string[],
|
||||
fallback: string,
|
||||
): boolean {
|
||||
if (disc.type.kind === "f64") return emitIntegerSwitch(host, disc, cases, labels, fallback);
|
||||
if (disc.type.kind === "f64")
|
||||
return emitIntegerSwitch(host, disc, discExpr, cases, labels, fallback);
|
||||
if (disc.type.kind === "string") return emitStringSwitch(host, disc, cases, labels, fallback);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ export interface WalkBorrowHost {
|
||||
/** Parameters that may become candidates; once calling conventions are
|
||||
* fixed, only borrowed parameters qualify. Defaults to every parameter. */
|
||||
parameterAllowed?(localId: string): boolean;
|
||||
/** For bodies that may write the heap: whether a walk only reads
|
||||
* locations the whole body never writes, given the current candidates
|
||||
* (whose own rebinding releases nothing). Absent: the body preserves
|
||||
* every edge. */
|
||||
unwritten?(value: IrExpr, candidates: ReadonlySet<string>): boolean;
|
||||
}
|
||||
|
||||
export interface WalkFacts {
|
||||
@@ -34,7 +39,9 @@ export interface WalkFacts {
|
||||
* calling convention.
|
||||
*
|
||||
* Soundness rests on the whole body preserving heap edges (the caller
|
||||
* passes only functions in ReferenceEffects.functions): neither the body
|
||||
* passes only functions in ReferenceEffects.functions, or supplies
|
||||
* `unwritten` to admit only walks over locations the body never writes;
|
||||
* the argument below then applies to those locations): neither the body
|
||||
* nor anything it calls removes a field, element or global reference, so
|
||||
* every object reachable from a parameter at entry stays reachable (and
|
||||
* alive) until the function returns. Such a local therefore needs no
|
||||
@@ -133,6 +140,10 @@ export function analyzeWalks(fn: IrFunction, host: WalkBorrowHost): WalkFacts {
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
return host.borrowsWithoutOwning(e) && walk(e.obj);
|
||||
// A required element of a walked array (the read still traps when the
|
||||
// element is missing).
|
||||
case "arrayGet":
|
||||
return host.borrowsWithoutOwning(e) && walk(e.arr);
|
||||
case "unionNarrow":
|
||||
case "downcast":
|
||||
case "upcast":
|
||||
@@ -155,12 +166,20 @@ export function analyzeWalks(fn: IrFunction, host: WalkBorrowHost): WalkFacts {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const walkOrThrow = (e: IrExpr): boolean =>
|
||||
(e.kind === "libCall" && e.fn === "error.nodeThrow") || walk(e);
|
||||
const isThrow = (e: IrExpr): boolean => e.kind === "libCall" && e.fn === "error.nodeThrow";
|
||||
const walkOrThrow = (e: IrExpr): boolean => isThrow(e) || walk(e);
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const id of candidates) {
|
||||
if (definitions.get(id)!.every((value) => value === null || walk(value))) continue;
|
||||
if (
|
||||
definitions
|
||||
.get(id)!
|
||||
.every(
|
||||
(value) =>
|
||||
value === null || (walk(value) && host.unwritten?.(value, candidates) !== false),
|
||||
)
|
||||
)
|
||||
continue;
|
||||
candidates.delete(id);
|
||||
changed = true;
|
||||
}
|
||||
@@ -169,6 +188,11 @@ export function analyzeWalks(fn: IrFunction, host: WalkBorrowHost): WalkFacts {
|
||||
const returnsWalk =
|
||||
!guarded &&
|
||||
returns.length > 0 &&
|
||||
returns.every((value) => value !== null && walkOrThrow(value));
|
||||
returns.every(
|
||||
(value) =>
|
||||
value !== null &&
|
||||
walkOrThrow(value) &&
|
||||
(isThrow(value) || host.unwritten?.(value, candidates) !== false),
|
||||
);
|
||||
return { locals: result, returnsWalk };
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import type { IrExpr, IrModule, IrStmt, IrType } from "../ir/ir.js";
|
||||
import { typeKey } from "../ir/ir.js";
|
||||
import { everyStmtList } from "../ir/traverse.js";
|
||||
import { computeConstructionFacts } from "./immutability.js";
|
||||
|
||||
/** Array methods that write their receiver (the guarded ones). */
|
||||
export const MUTATING_ARRAY_METHODS: ReadonlySet<string> = new Set([
|
||||
@@ -45,11 +46,19 @@ export class PublishedTypes {
|
||||
readonly records = new Set<string>();
|
||||
/** typeKeys of array, map and set types. */
|
||||
readonly containers = new Set<string>();
|
||||
/** Constructor stores into a `this` nothing else can reach yet
|
||||
* (immutability.ts): never a published target, so never guarded. */
|
||||
privateStores: ReadonlySet<IrStmt> = new Set();
|
||||
|
||||
classGuarded(className: string): boolean {
|
||||
return this.classes.has(className);
|
||||
}
|
||||
|
||||
/** Whether a class field store needs the guard. */
|
||||
fieldStoreGuarded(s: IrStmt): boolean {
|
||||
return s.kind === "fieldSet" && this.classes.has(s.className) && !this.privateStores.has(s);
|
||||
}
|
||||
|
||||
recordGuarded(shapeId: string): boolean {
|
||||
return this.records.has(shapeId);
|
||||
}
|
||||
@@ -83,6 +92,7 @@ export function computePublishedTypes(mod: IrModule): PublishedTypes | null {
|
||||
const records = new Map((mod.records ?? []).map((r) => [r.id, r]));
|
||||
const unions = new Map((mod.unions ?? []).map((u) => [u.id, u]));
|
||||
const out = new PublishedTypes();
|
||||
out.privateStores = computeConstructionFacts(mod).privateStores;
|
||||
const seen = new Set<string>();
|
||||
const stack = [...roots];
|
||||
const addHierarchy = (className: string): void => {
|
||||
@@ -142,6 +152,7 @@ export function computePublishedTypes(mod: IrModule): PublishedTypes | null {
|
||||
export function writesPublished(published: PublishedTypes, node: IrExpr | IrStmt): boolean {
|
||||
switch (node.kind) {
|
||||
case "fieldSet":
|
||||
return published.fieldStoreGuarded(node);
|
||||
case "fieldIncDec":
|
||||
return published.classGuarded(node.className);
|
||||
case "recordSet":
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* of the link contract: a program object keeps this symbol undefined and the
|
||||
* matching runtime defines it, so incompatible manual links fail before the
|
||||
* program can start. */
|
||||
export const RUNTIME_ABI_VERSION = 12 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v12" as const;
|
||||
export const RUNTIME_ABI_VERSION = 13 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v13" as const;
|
||||
|
||||
/** Object consumption is useful today, but the complete scr_* surface may
|
||||
* still change before 1.0. The versioned marker prevents accidental mixing;
|
||||
|
||||
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 12, marker: "scr_runtime_abi_v12" },
|
||||
runtime_abi: { version: 13, marker: "scr_runtime_abi_v13" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
|
||||
@@ -113,7 +113,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 12, marker: "scr_runtime_abi_v12" },
|
||||
runtime_abi: { version: 13, marker: "scr_runtime_abi_v13" },
|
||||
compiler: {
|
||||
command: "clang",
|
||||
identity: "fixture clang",
|
||||
|
||||
@@ -35,9 +35,11 @@ import {
|
||||
integerArithmeticRange,
|
||||
withinInt32,
|
||||
type IntegerRange,
|
||||
type IntegerRanges,
|
||||
type IntegerSlotFacts,
|
||||
} from "../ir/integer-ranges.js";
|
||||
import { findInitializerBindings, withInitializerBindings } from "../ir/initializer-bindings.js";
|
||||
import { unshareNodes } from "../ir/unshare.js";
|
||||
import { dynamicallyStoredFamilies, functionCallFacts, Int32Slots } from "../ir/int32-slots.js";
|
||||
import { computeMayThrow, type MayThrowReason } from "../backend/may-throw.js";
|
||||
import { SourceLocations } from "../backend/source-locations.js";
|
||||
@@ -47,6 +49,9 @@ import {
|
||||
type ContractNote,
|
||||
} from "../frontend/contract-types.js";
|
||||
|
||||
/** The largest array index: 2^32 - 2 (ECMA-262 array index). */
|
||||
const MAX_ARRAY_INDEX = 2 ** 32 - 2;
|
||||
|
||||
export interface ContractCheckResult {
|
||||
diagnostics: ScrDiagnostic[];
|
||||
contracts: IrContracts | null;
|
||||
@@ -78,7 +83,8 @@ export function checkContracts(
|
||||
annotations: ContractAnnotations,
|
||||
sourceTexts: ReadonlyMap<string, string>,
|
||||
): ContractCheckResult {
|
||||
const checker = new ContractChecker(mod, annotations, sourceTexts);
|
||||
// Range proofs are keyed by node identity: check the module emission sees.
|
||||
const checker = new ContractChecker(unshareNodes(mod), annotations, sourceTexts);
|
||||
return checker.run();
|
||||
}
|
||||
|
||||
@@ -899,12 +905,25 @@ class ImmutableContracts {
|
||||
);
|
||||
for (const p of fn.params) candidates.delete(p.localId);
|
||||
const allowed = new Set<IrExpr>();
|
||||
let ranges: IntegerRanges | undefined;
|
||||
// An element write at a negative, fractional or too-large index adds an
|
||||
// ordinary property instead, and publication refuses arrays that carry
|
||||
// properties: only writes at provable array indexes keep a value fresh.
|
||||
// Typed-array writes never add properties.
|
||||
const arrayIndex = (index: IrExpr): boolean => {
|
||||
ranges ??= analyzeIntegerRanges(fn);
|
||||
const range = ranges.get(index);
|
||||
return (
|
||||
range !== null && range !== undefined && range.min >= 0 && range.max <= MAX_ARRAY_INDEX
|
||||
);
|
||||
};
|
||||
walk(fn.body, {
|
||||
stmt: (s) => {
|
||||
if (s.kind === "return" && s.value?.kind === "varRef") allowed.add(s.value);
|
||||
if ((s.kind === "fieldSet" || s.kind === "recordSet") && s.obj.kind === "varRef")
|
||||
allowed.add(s.obj);
|
||||
if ((s.kind === "arraySet" || s.kind === "bytesSet") && s.arr.kind === "varRef")
|
||||
if (s.kind === "bytesSet" && s.arr.kind === "varRef") allowed.add(s.arr);
|
||||
if (s.kind === "arraySet" && s.arr.kind === "varRef" && arrayIndex(s.index))
|
||||
allowed.add(s.arr);
|
||||
},
|
||||
expr: (e) => {
|
||||
|
||||
@@ -400,6 +400,74 @@ export function checkedNarrowCall(
|
||||
};
|
||||
}
|
||||
|
||||
/** A checked extraction found a value arm other than the one the checker
|
||||
* promised (a narrowing the program invalidated before the read). */
|
||||
export const OTHER_ARM_FAILURE_MESSAGE =
|
||||
"a value of another type is not representable in the target union (a value narrowed or asserted past it still held it)";
|
||||
|
||||
/** checkedNarrowCall's in-place form with the TypeError of the consuming
|
||||
* operation: `messageFor` names Node's error for finding another arm
|
||||
* (`o.f is not a function`), or null for the stranded-value message. Null
|
||||
* when the operand cannot be evaluated more than once.
|
||||
*
|
||||
* The failure path stays small for wide unions: arms are grouped by their
|
||||
* message (every non-nullish arm without its own shares one), so it tests
|
||||
* at most the null and undefined arms before a final throw. */
|
||||
export function checkedArmExtraction(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
tag: number,
|
||||
arm: IrType,
|
||||
messageFor: (found: IrType) => string | null,
|
||||
): IrExpr | null {
|
||||
if (value.type.kind !== "union" || !isSafeToRepeat(value)) return null;
|
||||
const unionId = value.type.unionId;
|
||||
const from = lowerer.unions.get(unionId);
|
||||
const stored = from?.arms[tag];
|
||||
if (!from || from.arms.length < 2 || !stored || !typeEquals(stored, arm)) return null;
|
||||
const loc = value.loc;
|
||||
const isTag = (t: number): IrExpr => ({
|
||||
kind: "unionIsTag",
|
||||
unionId,
|
||||
tag: t,
|
||||
negated: false,
|
||||
value,
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const groups = new Map<string, number[]>();
|
||||
from.arms.forEach((found, t) => {
|
||||
if (t === tag) return;
|
||||
const message =
|
||||
messageFor(found) ??
|
||||
(isUnitType(found)
|
||||
? unionNarrowFailureMessage(found, (type) => lowerer.fmt(type))
|
||||
: OTHER_ARM_FAILURE_MESSAGE);
|
||||
const tags = groups.get(message);
|
||||
if (tags) tags.push(t);
|
||||
else groups.set(message, [t]);
|
||||
});
|
||||
// The largest group needs no test of its own: it is whatever remains.
|
||||
const ordered = [...groups].sort((a, b) => a[1].length - b[1].length);
|
||||
const [lastMessage] = ordered.pop()!;
|
||||
let failure: IrExpr = nodeThrowExpr(1, "", lastMessage, arm, loc);
|
||||
for (const [message, tags] of ordered.reverse()) {
|
||||
let cond = isTag(tags[0]!);
|
||||
for (const t of tags.slice(1))
|
||||
cond = { kind: "logical", op: "||", left: cond, right: isTag(t), type: BOOL, loc };
|
||||
const thrown = nodeThrowExpr(1, "", message, arm, loc);
|
||||
failure = { kind: "ternary", cond, then: thrown, else_: failure, type: arm, loc };
|
||||
}
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: isTag(tag),
|
||||
then: { kind: "unionNarrow", unionId, tag, value, type: arm, loc },
|
||||
else_: failure,
|
||||
type: arm,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The operand of a checked extraction in either form checkedNarrowCall
|
||||
* produces, or null for every other expression. */
|
||||
export function checkedNarrowOperand(lowerer: Lowerer, value: IrExpr): IrExpr | null {
|
||||
|
||||
@@ -509,10 +509,13 @@ export function finishOptionalChain(
|
||||
return { kind: "optChain", id, receiver, body, type: DYN, loc };
|
||||
}
|
||||
let type = lowerer.irTypeOf(expr);
|
||||
// The checker can call a receiver present that holds a nullish value at
|
||||
// runtime (runtime-optional reads; a stale narrowing restored to its
|
||||
// declared type). Either nullish arm short-circuits to undefined.
|
||||
const hiddenOptionalResult =
|
||||
(type.kind !== "union" || lowerer.armTag(type.unionId, UNDEFINED_T) < 0) &&
|
||||
receiver.type.kind === "union" &&
|
||||
lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0 &&
|
||||
(lowerer.unions.get(receiver.type.unionId)?.arms.some(isUnitType) ?? false) &&
|
||||
body.type.kind !== "generator" &&
|
||||
body.type.kind !== "jsval"
|
||||
? lowerer.withUndefinedArmOf(body.type)
|
||||
|
||||
@@ -176,6 +176,7 @@ import {
|
||||
completeArgs,
|
||||
} from "./call-arguments.js";
|
||||
import { isThisParameter, type ParamShape } from "./call-signatures.js";
|
||||
import { checkedArmExtraction } from "./coercions/unions.js";
|
||||
import {
|
||||
requireObjLitGenericReceiver,
|
||||
objLitGenericFnNodeOf,
|
||||
@@ -8996,8 +8997,19 @@ export function lowerObjectMethodCall(
|
||||
});
|
||||
let callee = target ? lowerer.fieldGetExpr(target, locOf(access), access) : null;
|
||||
if (callee?.type.kind === "union" && callableType !== null) {
|
||||
const helper = lowerer.narrowedArmHelper(callee.type.unionId, callableType, locOf(access));
|
||||
if (helper !== null) {
|
||||
// Calling a cleared callback field throws Node's call TypeError.
|
||||
const inPlace = checkedArmExtraction(
|
||||
lowerer,
|
||||
callee,
|
||||
lowerer.armTag(callee.type.unionId, callableType),
|
||||
callableType,
|
||||
() => `${access.getText()} is not a function`,
|
||||
);
|
||||
const helper = inPlace
|
||||
? null
|
||||
: lowerer.narrowedArmHelper(callee.type.unionId, callableType, locOf(access));
|
||||
if (inPlace) callee = inPlace;
|
||||
else if (helper !== null) {
|
||||
callee = lowerer.checkedNarrowCall(helper, callee, callableType, locOf(access));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { dirname } from "node:path";
|
||||
import * as posix from "node:path/posix";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { checkedClassAssertion, checkedClassUnionAssertion } from "./class-assertions.js";
|
||||
import { checkedArmExtraction } from "./coercions/unions.js";
|
||||
import { narrowClassUnion, narrowStoredClassValue } from "./class-unions.js";
|
||||
import { lowerUnionFieldRead, lowerUnionFieldWrite } from "./expressions/union-fields.js";
|
||||
import { captureContextArguments } from "./function-context.js";
|
||||
@@ -1073,6 +1074,13 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
// tests, so only side-effect-free reads compose — and when every arm
|
||||
// agrees the whole expression folds to that one string (dropping only
|
||||
// the pure read, the trust-the-checker bet lowerUnitComparison makes).
|
||||
// An effectful producer (a getter) evaluates once into a hidden local.
|
||||
const prelude: IrStmt[] = [];
|
||||
if (operand.type.kind === "union" && !isSafeToRepeat(operand)) {
|
||||
const local = lowerer.declareHiddenLocal("%typeofOperand", operand.type);
|
||||
prelude.push({ kind: "varDecl", localId: local.id, init: operand, loc });
|
||||
operand = varRef(local.id, operand.type, loc);
|
||||
}
|
||||
if (operand.type.kind === "union" && isSafeToRepeat(operand)) {
|
||||
const def = lowerer.unions.get(operand.type.unionId);
|
||||
const answers = def?.arms.map(typeofAnswer);
|
||||
@@ -1123,7 +1131,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
return prelude.length > 0
|
||||
? { kind: "seqExpr", stmts: prelude, result, type: STRING, loc }
|
||||
: result;
|
||||
}
|
||||
}
|
||||
if (operand.type.kind === "dyn") {
|
||||
@@ -3662,6 +3672,10 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
narrowed.className !== expr.type.className &&
|
||||
lowerer.isSubclassOf(narrowed.className, expr.type.className)
|
||||
) {
|
||||
if (lowerer.narrowing.untrusted(node, narrowed)) {
|
||||
const checked = checkedClassAssertion(lowerer, expr, narrowed, expr.loc);
|
||||
if (checked) return checked;
|
||||
}
|
||||
return { kind: "downcast", value: expr, type: narrowed, loc: expr.loc };
|
||||
}
|
||||
return expr;
|
||||
@@ -3708,6 +3722,8 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
const arrayTags = lowerer.arrayValueTags(expr.type.unionId);
|
||||
if (arrayTags.length === 1) {
|
||||
const arm = def!.arms[arrayTags[0]!]!;
|
||||
const checked = checkedStaleArm(lowerer, node, expr, arrayTags[0]!, arm);
|
||||
if (checked) return checked;
|
||||
return {
|
||||
kind: "unionNarrow",
|
||||
unionId: expr.type.unionId,
|
||||
@@ -3745,6 +3761,14 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
const values = literalValues(lowerer.checker.getTypeOfSymbol(property));
|
||||
const arm = values && literalUnionArm(def, values, (id) => lowerer.shapes.get(id));
|
||||
if (arm) {
|
||||
const checked = checkedStaleArm(
|
||||
lowerer,
|
||||
node,
|
||||
expr,
|
||||
lowerer.armTag(expr.type.unionId, arm),
|
||||
arm,
|
||||
);
|
||||
if (checked) return checked;
|
||||
return {
|
||||
kind: "unionNarrow",
|
||||
unionId: expr.type.unionId,
|
||||
@@ -3779,6 +3803,10 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
) &&
|
||||
lowerer.isSubclassOf(narrowed.className, valueType.className)
|
||||
) {
|
||||
const checked = checkedStaleArm(lowerer, node, expr, valueTag, valueType, narrowed);
|
||||
if (checked) {
|
||||
return checkedClassAssertion(lowerer, checked, narrowed, expr.loc) ?? checked;
|
||||
}
|
||||
return {
|
||||
kind: "downcast",
|
||||
value: {
|
||||
@@ -3795,6 +3823,8 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
const checked = checkedStaleArm(lowerer, node, expr, tag, narrowed);
|
||||
if (checked) return checked;
|
||||
return {
|
||||
kind: "unionNarrow",
|
||||
unionId: expr.type.unionId,
|
||||
@@ -3805,6 +3835,66 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
};
|
||||
}
|
||||
|
||||
/** The checked form of maybeNarrow's arm extraction for a narrowing the
|
||||
* checker may hold past code that replaced the value (a call clearing the
|
||||
* property; see NarrowingStability). Every other arm throws the TypeError
|
||||
* Node raises for the operation consuming the read (`Cannot read
|
||||
* properties of undefined (reading 'x')`, `o.f is not a function`), or the
|
||||
* stranded-value TypeError where Node would carry the value on. Null when
|
||||
* the narrowing is provably current. `narrowed` is the checker's type when
|
||||
* it refines the arm (a subclass view). */
|
||||
function checkedStaleArm(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Node,
|
||||
value: IrExpr,
|
||||
tag: number,
|
||||
arm: IrType,
|
||||
narrowed: IrType = arm,
|
||||
): IrExpr | null {
|
||||
if (value.type.kind !== "union" || tag < 0) return null;
|
||||
if (!lowerer.narrowing.untrusted(node, narrowed)) return null;
|
||||
const inPlace = checkedArmExtraction(lowerer, value, tag, arm, (found) =>
|
||||
staleReadMessage(node, found),
|
||||
);
|
||||
if (inPlace) return inPlace;
|
||||
const helper = lowerer.narrowedArmHelper(value.type.unionId, arm, value.loc);
|
||||
return helper ? lowerer.checkedNarrowCall(helper, value, arm, value.loc) : null;
|
||||
}
|
||||
|
||||
/** Node's TypeError for the operation consuming `node` when it holds a
|
||||
* value of type `found`, or null where Node would not throw. */
|
||||
function staleReadMessage(node: ts.Node, found: IrType): string | null {
|
||||
let child: ts.Node = node;
|
||||
let parent = node.parent;
|
||||
while (ts.isParenthesizedExpression(parent)) {
|
||||
child = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (ts.isCallExpression(parent) && parent.expression === child && !parent.questionDotToken) {
|
||||
return found.kind === "func" ? null : `${node.getText()} is not a function`;
|
||||
}
|
||||
const unit = found.kind === "undefinedT" ? "undefined" : found.kind === "nullT" ? "null" : null;
|
||||
if (unit === null) return null;
|
||||
if (
|
||||
!(ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) ||
|
||||
parent.expression !== child ||
|
||||
parent.questionDotToken
|
||||
)
|
||||
return null;
|
||||
let key: string | null = null;
|
||||
if (ts.isPropertyAccessExpression(parent)) key = parent.name.text;
|
||||
else if (ts.isStringLiteral(parent.argumentExpression)) key = parent.argumentExpression.text;
|
||||
else if (ts.isNumericLiteral(parent.argumentExpression))
|
||||
key = String(Number(parent.argumentExpression.text));
|
||||
if (key === null) return null;
|
||||
const store = parent.parent;
|
||||
return ts.isBinaryExpression(store) &&
|
||||
store.left === parent &&
|
||||
store.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
||||
? `Cannot set properties of ${unit} (setting '${key}')`
|
||||
: `Cannot read properties of ${unit} (reading '${key}')`;
|
||||
}
|
||||
|
||||
/** A runtime-optional slot can be assigned while a truthy guard is in
|
||||
* force. TypeScript then types a direct member receiver as the plain arm,
|
||||
* even though an OOB-safe assignment may have restored undefined. Check
|
||||
|
||||
@@ -71,6 +71,7 @@ import {
|
||||
import { everyExprChild, everyStmtChild, transformStmtList } from "../../ir/traverse.js";
|
||||
import { RuntimeOptionalProvenance } from "./runtime-optional-provenance.js";
|
||||
import { presentAtUse, reassignedBindings } from "./runtime-optional-guards.js";
|
||||
import { NarrowingStability } from "./narrowing-stability.js";
|
||||
import { ArrayElementStates } from "./runtime-optional-elements.js";
|
||||
import { indexReadInBounds } from "./runtime-optional-bounds.js";
|
||||
import { ArrayOwnership } from "./runtime-optional-ownership.js";
|
||||
@@ -1898,6 +1899,10 @@ export class Lowerer {
|
||||
* discriminant was captured. A case match narrows the captured value,
|
||||
* not that mutable binding; preserve its tagged representation on reads. */
|
||||
readonly unstableSwitchBindings = new Set<ts.Symbol>();
|
||||
/** Whether a checker narrowing of a mutable member or closure-assigned
|
||||
* binding still holds at its use (calls do not reset TypeScript's
|
||||
* narrowing). Stale ones read the declared storage or check it. */
|
||||
readonly narrowing = new NarrowingStability(this);
|
||||
/** All storage slots widened for runtime absence, including slots whose
|
||||
* current control-flow branch has temporarily narrowed the value. */
|
||||
readonly runtimeOptionalStorageLocals = new Set<IrLocal>();
|
||||
@@ -5705,6 +5710,7 @@ export class Lowerer {
|
||||
|
||||
run(): LowerResult {
|
||||
const parts = this.splitFiles();
|
||||
this.narrowing.setFiles(parts.map((fp) => fp.sf));
|
||||
// The coverage remainder deliberately visits every body that reachable
|
||||
// emit skipped. Those files are already phase-managed, so an ordinary
|
||||
// checker miss would stay a one-node IPC query instead of falling back
|
||||
@@ -6074,6 +6080,7 @@ export class Lowerer {
|
||||
emitReachable(extraRoots?: readonly string[]): { reachable: Set<string>; result: LowerResult } {
|
||||
this.deferredModuleInitializers = new DeferredModuleInitializers();
|
||||
const parts = this.splitFiles();
|
||||
this.narrowing.setFiles(parts.map((fp) => fp.sf));
|
||||
// Direct lowering callers do not necessarily run program preflight.
|
||||
// Establish the same managed header/top-level batch here before
|
||||
// collection, while the production path simply finds warm memos.
|
||||
@@ -7058,6 +7065,11 @@ export class Lowerer {
|
||||
// receiver-kind check downstream sees the narrowed arm.
|
||||
const narrowed = this.chainNarrowedType.get(node);
|
||||
if (narrowed) return narrowed;
|
||||
// A stale narrowing of a mutable member or closure-assigned binding in
|
||||
// an absence/type test (or a slot accepting the declared type) reads
|
||||
// as declared, so the test inspects the stored value.
|
||||
const widened = this.narrowing.widenedType(node);
|
||||
if (widened) return widened;
|
||||
const t = this.checker.getTypeAtLocation(node);
|
||||
// ALIASED-TYPEOF narrowing: inside a branch a `type === 'string'`
|
||||
// test proves (type = typeof val, both never reassigned), references
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule, validateModule } from "../../index.js";
|
||||
import type { IrModule } from "../../ir/ir.js";
|
||||
|
||||
async function lower(source: string): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-narrowing-stability-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
output = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
||||
const module = deserializeModule(await readFile(output, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
return module;
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** The TypeError messages a function's checked reads can throw. */
|
||||
function thrownMessages(module: IrModule, name: string): string[] {
|
||||
const fn = module.functions.find((f) => f.name === name || f.name.endsWith(`.${name}`));
|
||||
if (!fn) throw new Error(`no function ${name}`);
|
||||
const messages: string[] = [];
|
||||
JSON.stringify(fn.body, (_key, value: unknown) => {
|
||||
const node = value as { kind?: string; fn?: string; args?: { value?: unknown }[] };
|
||||
if (node?.kind === "libCall" && node.fn === "error.nodeThrow")
|
||||
messages.push(String(node.args?.[2]?.value));
|
||||
return value;
|
||||
});
|
||||
return messages;
|
||||
}
|
||||
|
||||
const SOURCE = `
|
||||
class Tree {
|
||||
parent: Tree | undefined;
|
||||
depth: number;
|
||||
constructor(parent: Tree | undefined) {
|
||||
this.parent = parent;
|
||||
this.depth = parent ? parent.depth + 1 : 0;
|
||||
}
|
||||
}
|
||||
class Box {
|
||||
label: string | undefined = undefined;
|
||||
clear(): void { this.label = undefined; }
|
||||
}
|
||||
function visit(t: Tree): number { return t.depth; }
|
||||
function constructionOnly(t: Tree): number {
|
||||
if (t.parent) {
|
||||
visit(t);
|
||||
return t.parent.depth;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
function noCall(b: Box): number {
|
||||
if (b.label !== undefined) return b.label.length + b.label.length;
|
||||
return 0;
|
||||
}
|
||||
function libraryCalls(b: Box, xs: number[]): number {
|
||||
if (b.label !== undefined) {
|
||||
xs.push(Math.max(1, 2));
|
||||
console.log("x");
|
||||
return b.label.length;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function userCall(b: Box): number {
|
||||
if (b.label !== undefined) {
|
||||
visit(new Tree(undefined));
|
||||
return b.label.length;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function methodCall(b: Box): number {
|
||||
if (b.label !== undefined) {
|
||||
b.clear();
|
||||
return b.label.length;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function loopBackEdge(b: Box): number {
|
||||
let n = 0;
|
||||
if (b.label !== undefined) {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
n += b.label.length;
|
||||
b.clear();
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
const b = new Box();
|
||||
b.label = "abc";
|
||||
console.log(constructionOnly(new Tree(new Tree(undefined))), noCall(b), libraryCalls(b, []),
|
||||
userCall(b), methodCall(b), loopBackEdge(b));
|
||||
`;
|
||||
|
||||
test("narrowed reads keep the unchecked extraction where nothing can change the field", async () => {
|
||||
const module = await lower(SOURCE);
|
||||
expect(thrownMessages(module, "constructionOnly")).toEqual([]);
|
||||
expect(thrownMessages(module, "noCall")).toEqual([]);
|
||||
expect(thrownMessages(module, "libraryCalls")).toEqual([]);
|
||||
});
|
||||
|
||||
test("narrowed reads after code that can clear the field check it with Node's TypeError", async () => {
|
||||
const module = await lower(SOURCE);
|
||||
const read = "Cannot read properties of undefined (reading 'length')";
|
||||
expect(thrownMessages(module, "userCall")).toContain(read);
|
||||
expect(thrownMessages(module, "methodCall")).toContain(read);
|
||||
expect(thrownMessages(module, "loopBackEdge")).toContain(read);
|
||||
});
|
||||
|
||||
test("standard-library arguments read as declared only where the lowering is generic", async () => {
|
||||
// JSON.stringify and clearInterval lower only the narrowed argument types;
|
||||
// console methods and type-parameter slots take the declared value.
|
||||
const module = await lower(`
|
||||
interface Opts {
|
||||
p?: { [k: string]: number };
|
||||
}
|
||||
function touch(): number {
|
||||
return 1;
|
||||
}
|
||||
let handle: ReturnType<typeof setInterval> | null = null;
|
||||
const stop = (): void => {
|
||||
handle = null;
|
||||
};
|
||||
function specialized(o: Opts): string {
|
||||
o.p = { a: 1 };
|
||||
touch();
|
||||
return JSON.stringify(o.p);
|
||||
}
|
||||
function clearAfterCall(): void {
|
||||
if (handle !== null) {
|
||||
touch();
|
||||
clearInterval(handle);
|
||||
}
|
||||
}
|
||||
function formatted(o: Opts, out: (Opts["p"])[]): void {
|
||||
o.p = { a: 2 };
|
||||
touch();
|
||||
console.log(o.p);
|
||||
out.push(o.p);
|
||||
}
|
||||
handle = setInterval(() => {}, 1000);
|
||||
console.log(specialized({}));
|
||||
clearAfterCall();
|
||||
stop();
|
||||
formatted({}, []);
|
||||
`);
|
||||
expect(thrownMessages(module, "formatted")).toEqual([]);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@
|
||||
* the order-parity suite holding both lanes to identical diagnostics and
|
||||
* module order over the whole corpus. The phase-4 flip deleted the 5.9.3
|
||||
* pipeline; its recorded verdicts live on as the canary baselines
|
||||
* (test/ts7/baselines/order-parity.json), and the rationale prose the old
|
||||
* (test/ts7/baselines/order-parity/), and the rationale prose the old
|
||||
* program.ts carried was folded in here where behavior demands it.
|
||||
*
|
||||
* Differences from the retired 5.9.3 lane, each deliberate and each
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* already close to it). The compact encoding is therefore produced as ordered
|
||||
* text parts and decoded from bytes one function at a time; neither direction
|
||||
* materializes the complete document as a single string. */
|
||||
import type { IrFunction, IrModule } from "./ir.js";
|
||||
import type { IrExpr, IrFunction, IrModule, IrStmt } from "./ir.js";
|
||||
|
||||
// Version 15 expands the native Error prefix with filesystem metadata.
|
||||
// Older modules embed incompatible subclass offsets and must be rejected.
|
||||
@@ -99,6 +99,16 @@ export function deserializeModule(json: string): IrModule {
|
||||
return checkedVersion(JSON.parse(json, decodeNumber) as IrModule);
|
||||
}
|
||||
|
||||
/** A node as it reads back from the compact encoding: a fresh object, as
|
||||
* is every node inside it. */
|
||||
export function reparseExpr(node: IrExpr): IrExpr {
|
||||
return JSON.parse(JSON.stringify(node, encodeNumber), decodeNumber) as IrExpr;
|
||||
}
|
||||
|
||||
export function reparseStmt(node: IrStmt): IrStmt {
|
||||
return JSON.parse(JSON.stringify(node, encodeNumber), decodeNumber) as IrStmt;
|
||||
}
|
||||
|
||||
const QUOTE = 0x22;
|
||||
const BACKSLASH = 0x5c;
|
||||
const COMMA = 0x2c;
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { emitLlvmModule } from "../backend/llvm/emitter.js";
|
||||
import { analyzeIntegerRanges } from "./integer-ranges.js";
|
||||
import { F64, VOID, type IrExpr, type IrModule, type IrStmt } from "./ir.js";
|
||||
import { deserializeModule, serializeModule } from "./serialize.js";
|
||||
import { everyStmtList } from "./traverse.js";
|
||||
import { unshareNodes } from "./unshare.js";
|
||||
import { validateModule } from "./validate.js";
|
||||
|
||||
const loc = { file: "unshare.ts", start: 0, end: 1 };
|
||||
const num = (value: number): IrExpr => ({
|
||||
kind: "numLit",
|
||||
value,
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const add = (left: IrExpr, right: IrExpr): IrExpr => ({
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left,
|
||||
right,
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
|
||||
/** One varRef object read at two sites: `x` is unknown at the first and
|
||||
* exactly 5 at the second, as lowering helpers that reuse a read produce;
|
||||
* and one `p + 2` expression stored at two sites. */
|
||||
function sharedRead(): { mod: IrModule; read: IrExpr } {
|
||||
const read: IrExpr = { kind: "varRef", localId: "x", type: F64, loc };
|
||||
const twice = add({ kind: "varRef", localId: "p", type: F64, loc }, num(2));
|
||||
const body: IrStmt[] = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "x",
|
||||
init: { kind: "varRef", localId: "p", type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "varDecl", localId: "a", init: add(read, num(1)), loc },
|
||||
{ kind: "assign", localId: "x", value: num(5), loc },
|
||||
{ kind: "varDecl", localId: "b", init: add(read, num(1)), loc },
|
||||
// A repeated node with children: its leaves repeat with it.
|
||||
{ kind: "varDecl", localId: "c", init: twice, loc },
|
||||
{ kind: "assign", localId: "c", value: twice, loc },
|
||||
{
|
||||
kind: "return",
|
||||
value: add(
|
||||
{ kind: "varRef", localId: "a", type: F64, loc },
|
||||
{
|
||||
kind: "varRef",
|
||||
localId: "b",
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
),
|
||||
loc,
|
||||
},
|
||||
];
|
||||
const mod: IrModule = {
|
||||
irVersion: 15,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
functions: [
|
||||
{ name: "main", loc, params: [], locals: [], returnType: VOID, body: [] },
|
||||
{
|
||||
name: "f",
|
||||
loc,
|
||||
params: [{ name: "p", localId: "p", type: F64 }],
|
||||
locals: ["p", "x", "a", "b", "c"].map((id) => ({
|
||||
id,
|
||||
name: id,
|
||||
type: F64,
|
||||
mutable: true,
|
||||
})),
|
||||
returnType: F64,
|
||||
body,
|
||||
},
|
||||
],
|
||||
};
|
||||
return { mod, read };
|
||||
}
|
||||
|
||||
function nodes(mod: IrModule): (IrExpr | IrStmt)[] {
|
||||
const out: (IrExpr | IrStmt)[] = [];
|
||||
const visit = (node: IrExpr | IrStmt): boolean => (out.push(node), true);
|
||||
for (const fn of mod.functions) everyStmtList(fn.body, { expr: visit, stmt: visit });
|
||||
return out;
|
||||
}
|
||||
|
||||
test("every occurrence gets its own node without mutating the input", () => {
|
||||
const { mod, read } = sharedRead();
|
||||
const before = serializeModule(mod);
|
||||
const tree = unshareNodes(mod);
|
||||
expect(serializeModule(mod)).toBe(before);
|
||||
expect(serializeModule(tree)).toBe(before);
|
||||
const all = nodes(tree);
|
||||
expect(new Set(all).size).toBe(all.length);
|
||||
expect(all).not.toContain(read);
|
||||
// The entry function had no shared node and is kept as it is.
|
||||
expect(tree.functions[0]).toBe(mod.functions[0]);
|
||||
expect(unshareNodes(tree)).toBe(tree);
|
||||
});
|
||||
|
||||
test("ranges are per occurrence once nodes are unshared", () => {
|
||||
const { mod, read } = sharedRead();
|
||||
// Keyed by identity, the shared read holds the join of both sites.
|
||||
expect(analyzeIntegerRanges(mod.functions[1]!).get(read)).toBeNull();
|
||||
const fn = unshareNodes(mod).functions[1]!;
|
||||
const reads = nodes({ ...mod, functions: [fn] }).filter(
|
||||
(node) => node.kind === "varRef" && node.localId === "x",
|
||||
);
|
||||
const ranges = analyzeIntegerRanges(fn);
|
||||
expect(reads.map((node) => ranges.get(node as IrExpr) ?? null)).toEqual([
|
||||
null,
|
||||
{ min: 5, max: 5 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("emission matches the module read back from its serialized form", () => {
|
||||
const { mod } = sharedRead();
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
const restored = deserializeModule(serializeModule(mod, true));
|
||||
expect(emitLlvmModule(mod)).toBe(emitLlvmModule(restored));
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { IrExpr, IrFunction, IrModule, IrStmt } from "./ir.js";
|
||||
import { reparseExpr, reparseStmt } from "./serialize.js";
|
||||
import { everyExprChild, everyStmtChild, mapExprChildren, mapStmtChildren } from "./traverse.js";
|
||||
|
||||
/** Give every occurrence of an executable node its own object.
|
||||
*
|
||||
* Lowering helpers may place one node at several sites (a binding's varRef
|
||||
* read in a loop condition and in its body, a shared `break`). Analyses key
|
||||
* their facts by node identity (integer ranges, int32 write proofs, borrow
|
||||
* and lifetime facts), so a shared node carries one fact for all of its
|
||||
* sites: integer ranges keep the join, which loses what one site proves. A
|
||||
* serialized module gives every occurrence its own node; this makes the
|
||||
* in-memory module equivalent, so analyses and emission behave the same on
|
||||
* a module lowered in memory and on one read back from its artifact.
|
||||
* Functions without a repeated node are kept as they are; the input is
|
||||
* never mutated. */
|
||||
export function unshareNodes(mod: IrModule): IrModule {
|
||||
const seen = new Set<IrExpr | IrStmt>();
|
||||
const repeated = new Set<IrExpr | IrStmt>();
|
||||
const affected = new Set<IrFunction>();
|
||||
let current: IrFunction | null = null;
|
||||
// A repeated node marks the function holding the later occurrence. Its
|
||||
// subtree is walked again, so every node under it is repeated as well.
|
||||
const visit = (node: IrExpr | IrStmt): void => {
|
||||
if (!seen.has(node)) seen.add(node);
|
||||
else {
|
||||
repeated.add(node);
|
||||
affected.add(current!);
|
||||
}
|
||||
};
|
||||
const findExpr = (node: IrExpr): boolean => {
|
||||
visit(node);
|
||||
return everyExprChild(node, findExpr, findStmt);
|
||||
};
|
||||
const findStmt = (node: IrStmt): boolean => {
|
||||
visit(node);
|
||||
return everyStmtChild(node, findExpr, findStmt);
|
||||
};
|
||||
for (const fn of mod.functions) {
|
||||
current = fn;
|
||||
for (const node of fn.body) findStmt(node);
|
||||
}
|
||||
if (affected.size === 0) return mod;
|
||||
// Rebuilding a function copies every node that has children; a repeated
|
||||
// childless node reads back as a fresh object at each of its sites there.
|
||||
const expr = (node: IrExpr): IrExpr => {
|
||||
const copy = mapExprChildren(node, expr, stmt);
|
||||
return copy !== node || !repeated.has(node) ? copy : reparseExpr(node);
|
||||
};
|
||||
const stmt = (node: IrStmt): IrStmt => {
|
||||
const copy = mapStmtChildren(node, expr, stmt);
|
||||
return copy !== node || !repeated.has(node) ? copy : reparseStmt(node);
|
||||
};
|
||||
return {
|
||||
...mod,
|
||||
functions: mod.functions.map((fn) =>
|
||||
affected.has(fn) ? { ...fn, body: fn.body.map(stmt) } : fn,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -314,7 +314,10 @@ test("canonical byte loops keep their induction variable and indices integral",
|
||||
|
||||
expect(ll).toContain("alloca i64 ; integer induction index");
|
||||
expect(ll).toContain("icmp ult i64");
|
||||
expect(ll).not.toContain("bytes.index.valid");
|
||||
// The proven in-bounds index needs no element check at all. Do not assert
|
||||
// on `fptosi double` here: the fixture's inline ToUint32 coercion of the
|
||||
// running sum legitimately uses it.
|
||||
expect(ll).not.toContain("bytes.index");
|
||||
});
|
||||
|
||||
test("a body mutation keeps the byte loop on the general f64 path", () => {
|
||||
|
||||
@@ -329,11 +329,15 @@ console.log(alias(new Item()).value, snapshot(new Item()).value);
|
||||
// The rebound parameter keeps borrowing (its new value lives in an owner
|
||||
// slot), but the alias saved before the rebinding must own its value.
|
||||
// `new Item()` calls the borrowed constructor body, so no retain transfers
|
||||
// the fresh instance into it: the saved alias and the returned value.
|
||||
// the fresh instance into it. The saved alias retains once, and `return
|
||||
// saved` moves that owned scope entry into the result instead of retaining
|
||||
// it again and releasing the local at scope exit: the only releases are the
|
||||
// rebound parameter's owner slot (before the store and at exit).
|
||||
const snapshot = body(llvm, "sc_bf_snapshot");
|
||||
expect(snapshot.match(/@sc_retain_Item/g)).toHaveLength(2);
|
||||
expect(snapshot.match(/@sc_retain_Item/g)).toHaveLength(1);
|
||||
expect(snapshot).toContain("@sc_bf__x25_Item_constructor");
|
||||
expect(snapshot).toContain("@sc_release_Item");
|
||||
expect(snapshot.match(/@sc_release_Item/g)).toHaveLength(2);
|
||||
expect(snapshot).toMatch(/(%t\d+) = load ptr, ptr %sc_l_saved_0\n[^]*ret ptr \1\n/);
|
||||
});
|
||||
|
||||
test("unchanged captured parameters are boxed when a closure first needs them", async () => {
|
||||
|
||||
@@ -312,7 +312,7 @@ ${[0, 1, 2, 3, 4, 5].map((index) => ` call void @add${index}(i64 ${index + 2})`
|
||||
expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]);
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v12");
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v13");
|
||||
|
||||
const clangExe = join(dir, "clang-program");
|
||||
const linkDriver = join(dir, "clang-driver.c");
|
||||
@@ -646,7 +646,7 @@ ${[0, 1, 2, 3, 4, 5].map((index) => ` call void @add${index}(i64 ${index + 2})`
|
||||
.split("\n");
|
||||
expect(undefinedSymbols).toContain("_sf_scale");
|
||||
expect(undefinedSymbols).toContain("_sf_callback_mix");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v12");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v13");
|
||||
});
|
||||
|
||||
test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
|
||||
|
||||
@@ -217,15 +217,39 @@ test("a throwing later source argument releases the retained snapshot on its exc
|
||||
const module = await lower(`
|
||||
class Item { value = 7; }
|
||||
function read(item: Item, ignored: number): number { return item.value + ignored; }
|
||||
function fail(items: Item[]): number { items.pop(); throw new Error("failed"); }
|
||||
function work(items: Item[]): number { return read(items[0], fail(items)); }
|
||||
try { console.log(work([new Item()])); } catch {}
|
||||
`);
|
||||
// The later argument removes the element, so the snapshot must own it.
|
||||
const work = body(emitLlvmModule(module), "sc_bf_work");
|
||||
const failure = work.indexOf("@sc_bf_fail");
|
||||
expect(failure).toBeGreaterThan(0);
|
||||
expect(work.slice(0, failure)).toContain("@sc_retain_Item");
|
||||
expect(work.slice(failure)).toMatch(PENDING_CHECK);
|
||||
const exceptionEdge = work.slice(failure, work.indexOf("@sc_bf_read"));
|
||||
expect(exceptionEdge).toContain("@sc_release_Item");
|
||||
expect(work).not.toContain("@scr_union_release");
|
||||
});
|
||||
|
||||
test("a throwing later source argument that writes no element leaves the snapshot borrowed", async () => {
|
||||
const module = await lower(`
|
||||
class Item { value = 7; }
|
||||
function read(item: Item, ignored: number): number { return item.value + ignored; }
|
||||
function fail(): number { throw new Error("failed"); }
|
||||
function work(items: Item[]): number { return read(items[0], fail()); }
|
||||
try { console.log(work([new Item()])); } catch {}
|
||||
`);
|
||||
// Neither the callee nor the later argument writes an array element (the
|
||||
// Error constructor's stores into its own fresh object do not count), so
|
||||
// the borrowed parameter's array keeps the element alive and no edge,
|
||||
// normal or exceptional, owes a release.
|
||||
const work = body(emitLlvmModule(module), "sc_bf_work");
|
||||
const failure = work.indexOf("@sc_f_fail");
|
||||
expect(failure).toBeGreaterThan(0);
|
||||
expect(work.slice(failure)).toMatch(PENDING_CHECK);
|
||||
expect(work.slice(failure)).toContain("@sc_release_Item");
|
||||
expect(work).not.toContain("@sc_retain_Item");
|
||||
expect(work).not.toContain("@sc_release_Item");
|
||||
expect(work).not.toContain("@scr_union_release");
|
||||
});
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/001-hello.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/002-log-args.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/100-number-format.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1000-json-stringify-basics.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1001-json-escapes-unicode.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1002-json-parse-cast.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1003-json-parse-unions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1004-json-parse-errors.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1005-json-nested.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1006-json-fs-config.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1007-json-rc-stress.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1008-json-null-arms.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1009-json-optional-fields.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/101-arithmetic.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1010-json-stringify-space.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1011-json-unknown-typeof.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/102-comparisons.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1020-async-basics.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1021-async-ordering.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1022-async-exceptions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1023-async-rc-stress.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1024-async-pending-exit.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1025-async-promise-capture.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1026-throw-promise.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1027-async-return-promise.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1028-async-return-record-literals.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1029-async-eager-chains.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/103-ternary.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/104-ternary-empty-arrays.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1050-comptime-tables.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1051-comptime-strings.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1052-comptime-records.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1053-comptime-json.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1054-comptime-modules/tables.ts",
|
||||
"<repo>/tests/corpus/1054-comptime-modules/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/110-number-radix.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1100-island-eval-basics.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1101-island-marshal-unicode.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1102-island-rc-stress.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/111-record-entries.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1110-math-members.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1111-math-random.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1112-number-methods.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1113-string-methods.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1114-string-unicode-island.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1115-parse-globals.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1116-island-static-mix.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1117-typeof-static-union.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1118-object-spread-conditional.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1119-switch-union.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user