Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion packages/compiler/src/backend/c/c-emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type {
import { ffiCallbackType, funcOf, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, mapOf, moduleEmbedsCompressedNpm, moduleUsesChildProcess, moduleUsesDgram, moduleUsesDynInvoke, moduleEmbedsBuiltin, moduleUsesFetch, moduleUsesFsWatch, moduleUsesHttp2, moduleUsesHttpServer, moduleUsesNet, moduleUsesNodeTest, moduleUsesProcessEvents, moduleUsesStream, moduleUsesTls, moduleUsesTlsCa, POINTER_KINDS, type PointerKind, RUNTIME_EMITTER_CLASS, STRING, VOID } from "../../ir/ir.js";
import { undefinedArmTag } from "../../ir/analysis.js";
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js";
import { allocateFfiCallbackAdapters, hasForeignFfiCallback, hasRetainedFfiCallback, type FfiCallbackAdapter } from "../ffi-callbacks.js";
import {
mangleAsyncSpawn,
Expand All @@ -57,7 +58,7 @@ import {
mangleVtSlot,
mangleWrapper,
} from "../mangle.js";
import { cCommentText, cFnPtrCast, cType, releaseCallC, cStringLiteral, cDecl } from "./types.js";
import { cCommentText, cFnPtrCast, cType, releaseCallC, cStringLiteral, cDecl, cNumberLiteral } from "./types.js";
import { computeMayThrow } from "./may-throw.js";
import { unionTruthyHelper, unionEqHelper, unionToStrHelper, unionJoinHelper, jsonWriteHelper, jsonIndentHelper, dynMatchHelper, dynCheckHelper, dynFuncBoxHelper, dynToStrHelper, caughtToDynHelper, toDynHelper, recordKeyGetHelper, recordKeySetHelper } from "./walkers.js";
import { VtSlot, ClassMeta, emitStructDefs, vtEntriesFor, vtSlotParams, emitVtableDecls, emitVtableInstances, emitVtAdapterDefs, emitHierarchyClassHelpers, emitClassObjs, emitCtorThunkDefs, errorVtStampLines, emitterVtStampLines, streamVtStampLines, traceAdapterC, traceArgC, boxNewC, arrNewC } from "./shapes.js";
Expand Down Expand Up @@ -267,6 +268,7 @@ export class CEmitter {
readonly ffiHasRetainedCallback: boolean;
readonly ffiHasForeignCallback: boolean;
readonly globalsById = new Map<string, IrGlobal>();
readonly constantNumericTables: ReadonlyMap<string, ConstantNumericTable>;
readonly unionsById = new Map<string, IrUnionDef>();
/** Active optional-chain bind temps, by chain id (chainRecv reads). */
readonly chainTemps = new Map<string, Temp>();
Expand Down Expand Up @@ -436,6 +438,7 @@ export class CEmitter {
sourceText?: string,
private readonly options: CEmitOptions = {},
) {
this.constantNumericTables = findConstantNumericTables(mod);
this.ffiCallbackAdapters = allocateFfiCallbackAdapters(mod.ffiImports ?? []);
this.ffiHasRetainedCallback = hasRetainedFfiCallback(mod.ffiImports ?? []);
this.ffiHasForeignCallback = hasForeignFfiCallback(mod.ffiImports ?? []);
Expand Down Expand Up @@ -684,6 +687,20 @@ export class CEmitter {
``,
];
out.push(...this.emitBytesElementHelpers());
for (const table of this.constantNumericTables.values()) {
out.push(
`static const double ${table.symbol}[] = { ${table.values.map(cNumberLiteral).join(", ")} };`,
`static inline double ${table.symbol}_get(const ScrArr *a, double i) {`,
// Range checks precede the conversion: NaN, infinities and large
// numbers must never reach a C float-to-integer conversion.
` if (a != NULL && i >= 0.0 && i < ${table.values.length}.0) {`,
` size_t index = (size_t)i;`,
` if ((double)index == i) return ${table.symbol}[index];`,
` }`,
` return scr_arr_get_number(a, i);`,
`}`, ``,
);
}
// Struct defs render into their own buffer BEFORE the unit-instance
// table flushes: class newFns point undefined-armed union fields at
// interned unit instances (fields start as JS's undefined, not NULL),
Expand Down
3 changes: 2 additions & 1 deletion packages/compiler/src/backend/c/exprs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1712,7 +1712,8 @@ function emitContainerExpr(
return emitter.newTemp(e.type, `scr_arr_len(${r.name})`);
case "getNumber": {
const index = emitter.emitExpr(e.args[0]!);
return emitter.newTemp(e.type, `scr_arr_get_number(${r.name}, ${index.name})`);
const table = e.receiver.kind === "varRef" ? emitter.constantNumericTables.get(e.receiver.localId) : undefined;
return emitter.newTemp(e.type, `${table ? `${table.symbol}_get` : "scr_arr_get_number"}(${r.name}, ${index.name})`);
}
case "nextPresent": {
const start = emitter.emitExpr(e.args[0]!);
Expand Down
33 changes: 33 additions & 0 deletions packages/compiler/src/backend/llvm/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import type {
import { CAUGHT, ffiCallbackType, isDynTypedRefType, isFfiContextParam, isRefCounted, isUnitType, moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, moduleUsesChildProcess, moduleUsesDynInvoke, moduleUsesFetch, moduleUsesFsWatch, moduleUsesHttpServer, moduleUsesNet, moduleUsesNodeTest, moduleUsesProcessEvents, moduleUsesStream, moduleUsesTls, moduleUsesTlsCa, NPM_COMPRESS_MIN, POINTER_KINDS, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, typeKey, VOID } from "../../ir/ir.js";
import { matchIntegerBytesForLoop } from "../../ir/integer-loops.js";
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js";
import { allocateFfiCallbackAdapters, hasForeignFfiCallback, hasRetainedFfiCallback, type FfiCallbackAdapter } from "../ffi-callbacks.js";
import { RUNTIME_ABI_MARKER } from "../runtime-abi.js";
import { computeMayThrow } from "../c/may-throw.js";
Expand Down Expand Up @@ -258,6 +259,7 @@ class LlEmitter {
private readonly ffiHasRetainedCallback: boolean;
private readonly ffiHasForeignCallback: boolean;
private readonly globalTypes = new Map<string, IrType>();
private readonly constantNumericTables: ReadonlyMap<string, ConstantNumericTable>;
/** May-throw analysis (the C emitter's computeMayThrow, shared): pending
* checks are emitted only after calls that can actually raise. */
private readonly mayThrow: Set<string>;
Expand Down Expand Up @@ -373,6 +375,7 @@ class LlEmitter {
private logArgSlots = 0;

constructor(private readonly mod: IrModule, options: LlvmTargetOptions) {
this.constantNumericTables = findConstantNumericTables(mod);
this.sizeType = options.pointerBits === 32 ? "i32" : "i64";
this.wasi = options.wasi === true;
this.emitLibraryIdentity = options.emitLibraryIdentity !== false;
Expand Down Expand Up @@ -1037,6 +1040,7 @@ class LlEmitter {
// Helpers assemble BEFORE the declaration table flushes (they add
// write/abort declarations).
const helpers = this.helperDefs();
if (this.constantNumericTables.size > 0) this.declare(`declare double @scr_arr_get_number(ptr, double)`);

const out: string[] = [
`; Generated by scriptc (LLVM backend) from ${this.mod.sourceFile}. Do not edit.`,
Expand Down Expand Up @@ -1112,6 +1116,35 @@ class LlEmitter {
);
for (const d of this.decls) out.push(d);
out.push(``);
for (const table of this.constantNumericTables.values()) {
const n = table.values.length;
out.push(
`@${table.symbol} = private constant [${n} x double] [${table.values.map((v) => `double ${f64Lit(v)}`).join(", ")}]`,
`define internal double @${table.symbol}_get(ptr %a, double %i) alwaysinline #0 {`,
`entry:`,
` %initialized = icmp ne ptr %a, null`,
` %nonnegative = fcmp oge double %i, ${f64Lit(0)}`,
` %below = fcmp olt double %i, ${f64Lit(n)}`,
` %range = and i1 %nonnegative, %below`,
` %safe = and i1 %initialized, %range`,
` br i1 %safe, label %convert, label %fallback`,
`convert:`,
// The branch must dominate fptoui: NaN/out-of-range conversion
// would produce poison. Fractional indices also need a fallback.
` %index = fptoui double %i to ${this.sizeType}`,
` %roundtrip = uitofp ${this.sizeType} %index to double`,
` %integer = fcmp oeq double %roundtrip, %i`,
` br i1 %integer, label %read, label %fallback`,
`read:`,
` %slot = getelementptr inbounds [${n} x double], ptr @${table.symbol}, ${this.sizeType} 0, ${this.sizeType} %index`,
` %value = load double, ptr %slot`,
` ret double %value`,
`fallback:`,
` %generic = call double @scr_arr_get_number(ptr %a, double %i)`,
` ret double %generic`,
`}`, ``,
);
}
for (const [text, lit] of this.literals) {
// Immortal interned ScrStr: { rc = SIZE_MAX, len, cap = len, bytes\0 } —
// the C emitter's static table, retain/release skip rc == SIZE_MAX.
Expand Down
3 changes: 2 additions & 1 deletion packages/compiler/src/backend/llvm/expr-containers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,10 @@ export function emitArrIntrinsic(host: LlvmEmitterContext, e: IrExpr & { kind: "
}
case "getNumber": {
const index = host.emitExpr(e.args[0]!);
const table = e.receiver.kind === "varRef" ? host.constantNumericTables.get(e.receiver.localId) : undefined;
host.declare(`declare double @scr_arr_get_number(ptr, double)`);
const t = B.tmp();
B.line(`${t} = call double @scr_arr_get_number(ptr ${r.name}, double ${index.name})`);
B.line(`${t} = call double @${table ? `${table.symbol}_get` : "scr_arr_get_number"}(ptr ${r.name}, double ${index.name})`);
return { name: t, type: e.type };
}
case "nextPresent": {
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/backend/llvm/expr-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* with LlEmitter; helpers receive this structural view at delegation. */
import type { IrBytesElem, IrExpr, IrFfiImport, IrFunction, IrLibFn, IrLocal, IrModule, IrRecordShape, IrStmt, IrType, IrUnionDef, SrcLoc } from "../../ir/ir.js";
import type { FfiCallbackAdapter } from "../ffi-callbacks.js";
import type { ConstantNumericTable } from "../../ir/constant-tables.js";
import type { BlockBuilder } from "./blocks.js";
import type { LlClassMeta } from "./classes.js";
import type { LlDyn } from "./dyn.js";
Expand Down Expand Up @@ -54,6 +55,7 @@ export interface LlvmEmitterContext extends ShapeHost {
closeOverrideWrapFor(cbUnion: IrType, retServer: boolean): string;
cstr(text: string): string;
currentGenerator: { yieldT: IrType; nextT: IrType; } | null;
constantNumericTables: ReadonlyMap<string, ConstantNumericTable>;
currentWasiCoro: { kind: "async" | "generator"; id: string; handle: string; self: string; finalLabel: string; cleanupLabel: string; suspendLabel: string; } | null;
declare(decl: string): void;
dyn: LlDyn;
Expand Down
72 changes: 72 additions & 0 deletions packages/compiler/src/ir/constant-tables.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { expect, test } from "vitest";
import { findConstantNumericTables } from "./constant-tables.js";
import { arrayOf, F64, funcOf, VOID, type IrExpr, type IrModule, type IrStmt } from "./ir.js";
import { validateModule } from "./validate.js";

const loc = { file: "constant-tables.ts", start: 0, end: 0 };
const type = arrayOf(F64);
const id = "%g.table";
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
const ref = (): IrExpr => ({ kind: "varRef", localId: id, type, loc });
const read = (index = num(0)): IrExpr => ({ kind: "arrIntrinsic", method: "getNumber", receiver: ref(), args: [index], type: F64, loc });
const expr = (value: IrExpr): IrStmt => ({ kind: "exprStmt", expr: value, loc });

function fixture(): IrModule {
return {
irVersion: 11, sourceFile: loc.file, entry: "main",
globals: [{ id, name: "table", type, mutable: false }],
functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [
{ kind: "assign", localId: id, value: { kind: "arrayLit", elems: [num(2), num(-0), num(Infinity)], type, loc }, loc },
expr(read()),
], loc }],
};
}

test("finds literal tables without mutating or deleting their original initialization", () => {
const mod = fixture();
expect(validateModule(mod)).toEqual([]);
const before = structuredClone(mod);
expect(findConstantNumericTables(mod).get(id)?.values).toEqual([2, -0, Infinity]);
expect(mod).toEqual(before);
});

test.each(["alias", "element", "length", "argument", "return", "reassign", "index mutation", "capture", "callback"])("refuses %s uses anywhere in the module", (use) => {
const mod = fixture();
const body = mod.functions[0]!.body;
const mutation: IrStmt = { kind: "arraySet", arr: ref(), index: num(0), value: num(9), loc };
if (use === "alias") {
mod.functions[0]!.locals.push({ id: "alias", name: "alias", type, mutable: false });
body.push({ kind: "varDecl", localId: "alias", init: ref(), loc });
} else if (use === "element") body.push(mutation);
else if (use === "length") body.push({ kind: "arraySetLength", arr: ref(), length: num(0), loc });
else if (use === "argument") body.push(expr({ kind: "call", callee: "external", args: [ref()], type: VOID, loc }));
else if (use === "return") body.push({ kind: "return", value: ref(), loc });
else if (use === "reassign") body.push(body[0]!);
else if (use === "index mutation") body.push(expr(read({ kind: "arrIntrinsic", method: "pop", receiver: ref(), args: [], type: F64, loc })));
else if (use === "capture") body.push(expr({ kind: "closure", fnName: "callback", captures: [id], type: funcOf([], VOID), loc }));
else mod.functions.push({ name: "laterCallback", params: [], returnType: VOID, locals: [], body: [mutation], loc });
expect(findConstantNumericTables(mod).size).toBe(0);
});

test("permits primitive reads and length queries, including nested numeric indices", () => {
const mod = fixture();
mod.functions[0]!.body.push(
expr(read(read(num(0)))),
expr({ kind: "arrIntrinsic", method: "length", receiver: ref(), args: [], type: F64, loc }),
expr({ kind: "arrayGet", arr: ref(), index: num(1), type: F64, loc }),
);
expect(validateModule(mod)).toEqual([]);
expect(findConstantNumericTables(mod).size).toBe(1);
});

test.each(["mutable", "spread", "effectful", "empty", "large", "missing initializer"])("leaves %s tables on the generic path", (reason) => {
const mod = fixture();
const init = (mod.functions[0]!.body[0] as IrStmt & { kind: "assign" }).value as IrExpr & { kind: "arrayLit" };
if (reason === "mutable") mod.globals![0]!.mutable = true;
else if (reason === "spread") init.spreads = [0];
else if (reason === "effectful") init.elems[0] = { kind: "call", callee: "effect", args: [], type: F64, loc };
else if (reason === "empty") init.elems = [];
else if (reason === "large") init.elems = Array.from({ length: 257 }, () => num(1));
else mod.functions[0]!.body.shift();
expect(findConstantNumericTables(mod).size).toBe(0);
});
93 changes: 93 additions & 0 deletions packages/compiler/src/ir/constant-tables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { IrExpr, IrModule } from "./ir.js";

export interface ConstantNumericTable {
symbol: string;
values: readonly number[];
}

const MAX_TABLE_ELEMENTS = 256;

function literalNumber(expr: IrExpr): number | null {
if (expr.kind === "numLit") return expr.value;
if (expr.kind === "unary" && expr.op === "-") {
const value = literalNumber(expr.operand);
if (value !== null) return -value;
}
return null;
}

/** Find module globals whose only observable uses are primitive indexed
* reads and length queries. `const` only protects the binding: every other
* use (including aliases, exports to dynamic code, mutation, and passing or
* returning the array) disqualifies the table. All functions are inspected,
* including callbacks, and nested index expressions are checked too.
*
* This is read specialization only. Backends retain the original allocation,
* initialization, receiver evaluation and cleanup. They must guard against
* an uninitialized receiver and preserve the generic accessor for invalid
* indices; the constant data must never make a not-yet-initialized array
* observable early. No IR or runtime representation changes are needed. */
export function findConstantNumericTables(mod: IrModule): ReadonlyMap<string, ConstantNumericTable> {
const candidates = new Map<string, { values: number[] | null; writes: number; reads: number; rejected: boolean }>();
for (const global of mod.globals ?? []) {
if (!global.mutable && global.type.kind === "array" && global.type.elem.kind === "f64") {
candidates.set(global.id, { values: null, writes: 0, reads: 0, rejected: false });
}
}
if (candidates.size === 0) return new Map();

function candidateFor(expr: IrExpr) {
return expr.kind === "varRef" && expr.type.kind === "array" && expr.type.elem.kind === "f64"
? candidates.get(expr.localId) : undefined;
}

function visit(value: unknown): void {
if (Array.isArray(value)) { value.forEach(visit); return; }
if (value === null || typeof value !== "object") return;
const node = value as Record<string, unknown>;
if (node["kind"] === "closure") {
for (const id of (node as IrExpr & { kind: "closure" }).captures) {
const candidate = candidates.get(id);
if (candidate) candidate.rejected = true;
}
}
if (node["kind"] === "arrIntrinsic") {
const read = node as IrExpr & { kind: "arrIntrinsic" };
const candidate = candidateFor(read.receiver);
if (candidate && ((read.method === "getNumber" && read.args.length === 1) ||
(read.method === "length" && read.args.length === 0))) {
if (read.method === "getNumber") candidate.reads++;
visit(read.args);
return;
}
}
if (node["kind"] === "arrayGet" || node["kind"] === "arrayHas" || node["kind"] === "arrayState") {
const read = node as IrExpr & { kind: "arrayGet" | "arrayHas" | "arrayState" };
if (candidateFor(read.arr)) { visit(read.index); return; }
}
const candidate = typeof node["localId"] === "string" ? candidates.get(node["localId"]) : undefined;
if (candidate) {
if (node["kind"] === "assign" && ++candidate.writes === 1) {
const init = node["value"] as IrExpr;
if (init.kind === "arrayLit" && !init.spreads?.length && init.elems.length > 0 && init.elems.length <= MAX_TABLE_ELEMENTS) {
const values = init.elems.map(literalNumber);
if (values.every((n): n is number => n !== null)) candidate.values = values;
}
if (candidate.values === null) candidate.rejected = true;
} else {
candidate.rejected = true;
}
}
for (const [key, child] of Object.entries(node)) {
if (key !== "type" && key !== "loc") visit(child);
}
}
visit(mod.functions);
const tables = new Map<string, ConstantNumericTable>();
for (const [id, candidate] of candidates) {
if (!candidate.rejected && candidate.writes === 1 && candidate.reads > 0 && candidate.values !== null) {
tables.set(id, { symbol: `sc_const_numbers_${tables.size}`, values: candidate.values });
}
}
return tables;
}
Loading
Loading