Files
scriptc/packages/compiler/src/backend/llvm/emit-module.ts
T

2862 lines
127 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* LlEmitter's module-level output: module parts, helper definitions, library declarations and FFI callback definitions. */
import { loopUnrollMetadata } from "./periodic-guards.js";
import { relieveHeapPressure } from "../../heap-pressure.js";
import { emitWasmModules } from "./wasm-modules.js";
import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js";
import { type ChunkResult, EmitJournal } from "./emit-journal.js";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { JS_IC_WORDS } from "./js-ic.js";
import {
mangleAsyncSpawn,
mangleClassObj,
mangleFnClosure,
mangleFunction,
mangleGlobal,
mangleVtInstance,
} from "../mangle.js";
import {
type IrGlobal,
type IrNodeIslandModules,
type IrType,
RUNTIME_ERROR_CLASSES,
RUNTIME_EVENT_CLASS,
RUNTIME_EVENT_TARGET_CLASS,
ffiCallbackType,
isFfiContextParam,
isRefCounted,
typeKey,
} from "../../ir/ir.js";
import {
FN_ATTRS,
limitInlineReleases,
releaseSym,
typedRefConstructor,
vAdapters,
} from "./shapes.js";
import { emitLlvmLayouts } from "./layouts.js";
import { emitArraySearchHelpers } from "./array-search.js";
import { emitClassMembershipHelper } from "./classes.js";
import { type RegexMatcherCode, compileRegexMatcher } from "./regex-matchers.js";
import {
MAP_ITER_DECL,
MAP_SIZE_DECL,
MAP_ITER_NUM_DECL,
SCR_MAP_NUM_DEAD,
NUMBER_MAP_ENTRY_DECL,
STRING_MAP_ENTRY_C_DECL,
STRING_MAP_ENTRY_DECL,
} from "./map-number-lookup.js";
import { RUNTIME_ABI_MARKER } from "../runtime-abi.js";
import { threadSlotsBlockDefinitions } from "./thread-slots.js";
import {
ABSENT_FIELD_PAYLOAD,
exactIndexLines,
f64Lit,
ffiNativeParamLl,
ffiNativeReturnLl,
ffiNativeTypeLl,
} from "./common.js";
import { literalCapWord, literalCapWord32 } from "./string-key-hash.js";
import { hasLoneSurrogate, llvmBytes, wtf8Bytes } from "../literals.js";
import { nodeIslandManifest } from "../../node-islands/modules.js";
import { undefinedArmTag } from "../../ir/analysis.js";
import { NULLABLE_ABSENT, NULLABLE_NULL } from "./nullable-unions.js";
import { literalEqualityDefinition } from "./string-literal-equality.js";
import { STRING_READ_HELPERS, stringScanDecl, stringScanInline } from "./string-reads.js";
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
import { InternalCompilerError } from "../../errors.js";
import { targetCpuAttribute } from "../target-cpu.js";
import { CPU_LEVEL_INIT_CALL, cpuDispatchAttributes, cpuLevelDefs } from "./cpu-dispatch.js";
import type { IrFunction } from "../../ir/ir.js";
import { type FunctionLayout, layoutTierOf } from "./function-layout.js";
/** The function bodies emitted in `chunks` contiguous ranges, each by its
* own emitter (LlEmitter.chunkEmitter), then merged in function order: the
* text one emitter gives. */
function emitFunctionsInChunks(emitter: LlEmitter, chunks: number): string[] {
const functions = emitter.mod.functions;
const size = Math.ceil(functions.length / chunks);
const ranges: { chunk: LlEmitter; texts: string[] }[] = [];
for (let start = 0; start < functions.length; start += size) {
const chunk = emitter.chunkEmitter();
const texts = functions.slice(start, start + size).map((fn) => {
const text = chunk.emitFunction(fn);
if (chunk.journal!.overclosed)
throw new Error(`llvm emitter bug: journal definition closed twice in ${fn.name}`);
if (chunk.journal!.unclosed.length > 0)
throw new Error(
`llvm emitter bug: unclosed journal definitions after ${fn.name}: ${chunk.journal!.unclosed.join(", ")}`,
);
return text;
});
ranges.push({ chunk, texts });
}
const fnDefs: string[] = [];
for (const { chunk, texts } of ranges) {
// The plain-data result a worker process would write, round-tripped.
const result = JSON.parse(JSON.stringify(chunk.chunkResult())) as ChunkResult;
const rename = emitter.mergeChunk(result);
for (const text of texts) {
const def = rename(text);
emitter.fnDefText.set(functions[fnDefs.length]!.name, def);
fnDefs.push(def);
}
}
return fnDefs;
}
/** Modules with fewer functions emit in one process, and compute their
* analyses in it: forking and merging would cost more than the workers save
* (SCRIPTC_EMIT_WORKER_FUNCTIONS lowers it, for tests). */
export function workerMinFunctions(): number {
const value = Number(process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] ?? "2000");
return Number.isFinite(value) && value > 0 ? value : 2000;
}
/** A function's estimated emission cost beyond its IR nodes, in nodes:
* fitted to the worker emission times of a large program's ranges. */
const FUNCTION_EMIT_COST = 22;
/** Where each of about `count` contiguous ranges of `functions` starts,
* followed by the end: ranges of about equal estimated emission cost, a
* function's node count plus FUNCTION_EMIT_COST. No range is empty. */
export function emitRangeStarts(
functions: readonly IrFunction[],
count: number,
nodeCount: (fn: IrFunction) => number | undefined,
): number[] {
const costs = functions.map((fn) => (nodeCount(fn) ?? 0) + FUNCTION_EMIT_COST);
const total = costs.reduce((sum, cost) => sum + cost, 0);
const starts = [0];
let sum = 0;
for (let i = 0; i < functions.length - 1 && starts.length < count; i++) {
sum += costs[i]!;
if (sum >= (total * starts.length) / count) starts.push(i + 1);
}
starts.push(functions.length);
return starts;
}
/** The function bodies emitted on forked worker processes, `workers` at a
* time: the function list splits into contiguous ranges of about equal
* estimated cost (emitRangeStarts), each emitted in its own process, which
* inherits this emitter as it is at the fork and records a journal
* (emit-journal.ts). A new range starts whenever a worker ends, and ranges
* merge in function order, so the text is the serial text. A range whose
* worker fails is emitted here when its turn comes. Null without fork
* primitives (Node, Windows, inside a worker) or when the module is small. */
function emitFunctionsOnWorkers(emitter: LlEmitter, workers: number): string[] | null {
const fork = emitter.targetOptions.forkPrimitives;
const functions = emitter.mod.functions;
if (fork === undefined || workers < 2) return null;
if (functions.length < workerMinFunctions()) return null;
// More ranges than workers: a range's merge overlaps the later ranges'
// emission, and a slow range holds back less. Ranges of equal cost finish
// at about the same time.
const starts = emitRangeStarts(functions, Math.min(functions.length, workers * 3), (fn) =>
emitter.nodeIndex.nodeCount(fn),
);
const count = starts.length - 1;
const rangeFunctions = (range: number): readonly IrFunction[] =>
functions.slice(starts[range], starts[range + 1]);
const dir = mkdtempSync(join(tmpdir(), "scriptc-emit-"));
const pids: number[] = [];
// The workers not yet reaped, by pid, with their ranges.
const running = new Map<number, number>();
const start = (range: number): void => {
const pid = fork.fork();
if (pid === 0) {
let code = 1;
try {
emitter.journal = new EmitJournal();
emitter.debug?.record();
const texts: string[] = [];
for (const fn of rangeFunctions(range)) {
texts.push(emitter.emitFunction(fn));
relieveHeapPressure();
}
writeFileSync(join(dir, `${range}.json`), JSON.stringify(emitter.chunkResult()));
// Function text never holds NUL, so it separates the functions.
writeFileSync(join(dir, `${range}.ll`), texts.join("\0"));
code = 0;
} catch {
// The parent emits the range itself.
}
fork.exit(code);
}
pids.push(pid);
if (pid > 0) running.set(pid, range);
};
const fnDefs: string[] = [];
// Ended ranges by index, with what their workers wrote (null when a worker
// failed), waiting to merge in function order.
const ended = new Map<number, { result: ChunkResult; texts: string[] } | null>();
// Reaps a worker and starts the next range in its place. A worker's journal
// records requests, not table contents, so its range's text doesn't depend
// on what merged before it forked.
const reap = (pid: number, range: number): void => {
const status = pid > 0 ? fork.wait(pid) : -1;
running.delete(pid);
if (pids.length < count) start(pids.length);
ended.set(range, status === 0 ? readRange(dir, range) : null);
};
try {
for (let range = 0; range < Math.min(workers, count); range++) start(range);
for (let range = 0; range < count; range++) {
// Until this range's worker ends, reap whichever worker ends first, so
// that `workers` keep running while ranges merge in order. A child
// that isn't a worker (or no `exited`) waits for this range's worker.
while (!ended.has(range)) {
const pid = pids[range]!;
const first = pid > 0 && fork.exited !== undefined ? fork.exited() : pid;
const other = running.get(first);
if (first !== pid && other !== undefined) reap(first, other);
else reap(pid, range);
}
const merged = ended.get(range)!;
ended.delete(range);
const slice = rangeFunctions(range);
if (merged !== null && merged.texts.length === slice.length) {
const rename = emitter.mergeChunk(merged.result);
slice.forEach((fn, index) => {
const def = rename(merged.texts[index]!);
emitter.fnDefText.set(fn.name, def);
fnDefs.push(def);
});
} else
for (const fn of slice) {
const def = emitter.emitFunction(fn);
emitter.fnDefText.set(fn.name, def);
fnDefs.push(def);
relieveHeapPressure();
}
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
return fnDefs;
}
/** A worker's range result, or null when its files are missing. */
function readRange(dir: string, range: number): { result: ChunkResult; texts: string[] } | null {
try {
const result = JSON.parse(readFileSync(join(dir, `${range}.json`), "utf8")) as ChunkResult;
const texts = readFileSync(join(dir, `${range}.ll`), "utf8").split("\0");
return { result, texts };
} catch {
return null;
}
}
/** The attribute groups of the emitted functions: #0 for every function,
* #1 for coroutine bodies and #2 for out-of-line record clones. Each carries
* the build's `--target-cpu` level, `sanitize_address` in sanitized builds,
* and `uwtable` on native targets, so every function has an unwind table
* entry, including those LLVM infers `nounwind` (profilers and debuggers
* unwind through them). A group needs one attribute: without any, #0 holds a
* string attribute that LLVM ignores. Dispatching builds add the x86-64-v3
* copies' #3 and #4 (cpu-dispatch.ts). */
function functionAttributes(emitter: LlEmitter, hasNoInlineRecordClone: boolean): string[] {
const cpu = targetCpuAttribute(emitter.targetCpu);
const asan = emitter.sanitizeAttributes ? " sanitize_address" : "";
const common = `${asan}${emitter.wasi ? "" : " uwtable"}`;
const base = `${common}${cpu}` || ` "scriptc-function"`;
return [
`attributes #0 = {${base} }`,
...(emitter.usesCoroutines ? [`attributes #1 = {${common} presplitcoroutine${cpu} }`] : []),
...(hasNoInlineRecordClone ? [`attributes #2 = { noinline${common}${cpu} }`] : []),
...(emitter.cpuDispatch ? cpuDispatchAttributes(common) : []),
];
}
/** The group line written before each function's text. Functions from one
* source file share a group, which the LLVM helper keeps in the same
* partitions so that their calls to each other stay inside a partition
* (native/llvm-codegen/src/partition.h). A group is numbered by the first
* function of its file in the module's function order, which does not depend
* on the host. Functions without a source file get no group. */
function partitionGroupLines(functions: readonly IrFunction[]): string[] {
const groups = new Map<string, number>();
return functions.map((fn) => {
const file = fn.loc.file;
if (file === "") return PARTITION_GROUP_END;
let group = groups.get(file);
if (group === undefined) groups.set(file, (group = groups.size));
return `${PARTITION_GROUP_END} ${group}`;
});
}
/** Ends the group of the definitions that follow. */
const PARTITION_GROUP_END = "; scriptc-group";
/** Written before the entry module's top-level code (its `%init.<n>`). The
* LLVM helper gives that function and the chain that follows it a partition
* of their own, so an edit of the program's top level recompiles only that
* code (native/llvm-codegen/src/partition.cpp). */
const PARTITION_ENTRY_LINE = "; scriptc-entry";
/** Whether each function is the entry module's top-level code. */
function partitionEntries(functions: readonly IrFunction[], sourceFile: string): boolean[] {
return functions.map((fn) => fn.name.startsWith("%init.") && fn.loc.file === sourceFile);
}
/** Writes the generated helpers, then the function definitions with their
* group lines, and the entry line before the entry module's top-level code.
* With a layout, the definitions it lists, helpers included, move after the
* unlisted helpers and before the other functions, by tier, each in its
* tier's group (`layout-<tier>`). The helper compiles each
* partition's definitions in text order, so listed ones come first in their
* partitions, and it places a group's definitions in the same partitions.
* Everything else keeps its order and its source-file group. */
function writeDefinitions(
out: string[],
layout: FunctionLayout | undefined,
helperTexts: readonly (readonly string[])[],
fnDefs: readonly string[],
groups: readonly string[],
entries: readonly boolean[],
): void {
const tierLine = (tier: number) => `${PARTITION_GROUP_END} layout-${tier}`;
const listed = new Map<number, string[]>();
const listIn = (tier: number, lines: readonly string[]) => {
const list = listed.get(tier) ?? [];
if (!listed.has(tier)) listed.set(tier, list);
for (const line of lines) list.push(line);
};
if (layout === undefined || layout.size === 0) {
for (const texts of helperTexts) for (const text of texts) out.push(text);
} else {
// A helper's text can span several entries (one per line), so the
// definitions are found in the joined text. A listed definition moves
// through its closing brace; what follows it stays in place.
const joined: string[] = [];
for (const texts of helperTexts) for (const text of texts) joined.push(text);
for (const part of joined.join("\n").split(/\n(?=define )/)) {
const tier = part.startsWith("define ") ? layoutTierOf(layout, part) : undefined;
const close = tier === undefined ? -1 : part.search(/\n}(\n|$)/);
if (tier === undefined || close < 0) {
out.push(part);
continue;
}
listIn(tier, [tierLine(tier), part.slice(0, close + 2)]);
const rest = part.slice(close + 3);
if (rest !== "") out.push(rest);
}
}
const unlisted: number[] = [];
fnDefs.forEach((text, i) => {
const tier = layout === undefined || layout.size === 0 ? undefined : layoutTierOf(layout, text);
if (tier === undefined) unlisted.push(i);
else listIn(tier, ["", ...(entries[i] ? [PARTITION_ENTRY_LINE] : []), tierLine(tier), text]);
});
for (const tier of [...listed.keys()].sort((a, b) => a - b))
for (const line of listed.get(tier)!) out.push(line);
for (const [k, i] of unlisted.entries()) {
if (k > 0 || listed.size > 0) out.push("");
if (entries[i]) out.push(PARTITION_ENTRY_LINE);
out.push(groups[i] ?? PARTITION_GROUP_END, fnDefs[i]!);
}
}
export function emitModuleParts(emitter: LlEmitter): string[] {
// Function bodies first (the literal/unit/fn-value tables fill as they
// emit), then the file assembles around them — the runtime ABI’s order.
const chunks = emitter.targetOptions.functionChunks ?? 1;
const fnDefs =
(chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ??
emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) ??
[];
if (fnDefs.length === 0)
for (const fn of emitter.mod.functions) {
const def = emitter.emitFunction(fn);
fnDefs.push(def);
emitter.fnDefText.set(fn.name, def);
relieveHeapPressure();
}
emitter.selectStacklessAsync(fnDefs);
const errorViews: string[] = [];
for (const property of ["name", "message"])
if (emitter.mod.functions.some((fn) => fn.name === `%error.${property}.read`)) {
emitter.declare(`declare void @scr_error_install_${property}_reader(ptr)`);
errorViews.push(
` call void @scr_error_install_${property}_reader(ptr @${mangleFunction(`%error.${property}.read`)})`,
);
}
for (const meta of emitter.classMeta.values()) {
if (meta.def.runtime || meta.root.def.name !== "%Error") continue;
const type: IrType = { kind: "object", className: meta.def.name };
const adapter = emitter.liveDynRefAdapter(type);
const rc = vAdapters(emitter.shapeHost, type);
const key = typeKey(type);
const name = `sc_error_view_${errorViews.length}`;
emitter.resolveThunkDefs.push(
`define internal ptr @${name}(ptr %v) ${FN_ATTRS} {`,
`entry:`,
` %d = call ptr ${typedRefConstructor(emitter.shapeHost, type)}(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ptr ${emitter.cstr(key)}, ${emitter.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
` ret ptr %d`,
`}`,
``,
);
emitter.declare(`declare void @scr_error_register_dyn(ptr, ptr)`);
errorViews.push(
` call void @scr_error_register_dyn(ptr @${mangleVtInstance(meta.def.name)}, ptr @${name})`,
);
}
const layouts = emitLlvmLayouts(
emitter.shapeHost,
emitter.mod,
emitter.classMeta,
emitter.classObjs,
emitter.fnByName,
(t) => emitter.llType(t),
emitter.nullableFields,
emitter.int32Slots,
);
const shapes = layouts.records;
const classShapes = layouts.classes;
const classObjDefs = layouts.classObjects;
const wrappers = emitter.emitFnValueDefs();
const asyncDefs = emitter.emitAsyncScaffolding();
const ffiCallbacks = emitter.emitFfiCallbackDefs();
const hasNoInlineRecordClone = [...emitter.recordCloneShapes].some(
(shapeId) => (emitter.recordsById.get(shapeId)?.fields.length ?? 0) >= 16,
);
// Module globals, FIRST OCCURRENCE per id: a class-expression static
// instantiated through several mixin applications registers one global
// id several times (2041-mixin-values) — C's tentative definitions
// accept the duplicates without an error, LLVM rejects redefinition, so the
// storage AND the release emit once per id here.
const seenGlobalIds = new Set<string>();
const globals = (emitter.mod.globals ?? []).filter((g) => {
if (seenGlobalIds.has(g.id)) return false;
seenGlobalIds.add(g.id);
return true;
});
// Refcounted globals release before main returns — the runtime ABI’s
// sc_release_globals, keeping the RC audit's live count exact. Built
// before the declaration table flushes (it adds the release symbols).
// Two spellings with distinct temp names: the normal exit and the
// uncaught-exception exit are separate blocks of the same function.
// Interned function-value closures are IMMORTAL (rc == SIZE_MAX), so
// an own-property table Object.defineProperties hung on one would
// outlive the RC audit — release it with the globals (the runtime ABI’s
// sc_release_globals tail). Only when the dispatch unit is even
// linked (defineProps is the only writer).
const runtimeFeatures = emitter.runtimeFeatures;
const fnValueProps = [...emitter.fnValues];
if (fnValueProps.length > 0) emitter.declare(`declare void @scr_box_release(ptr)`);
if (emitter.classObjs.size > 0)
emitter.declare(`declare void @scr_classobj_clear_properties(ptr)`);
const globalReleaseLines = (prefix: string): string[] => {
const lines: string[] = [];
globals.forEach((g, i) => {
if (!isRefCounted(g.type)) return;
lines.push(
` %${prefix}${i} = load ptr, ptr @${mangleGlobal(g.id)}`,
` call void ${releaseSym(emitter.shapeHost, g.type)}(ptr %${prefix}${i}) ; ${g.name}`,
);
});
fnValueProps.forEach((name, i) => {
// props sits at %ScrClosure field 3; release is NULL-tolerant —
// cleared so a second release path stays idempotent.
lines.push(
` %${prefix}fp${i} = load ptr, ptr getelementptr inbounds (%ScrClosure, ptr @${mangleFnClosure(name)}, i64 0, i32 3)`,
` call void @scr_box_release(ptr %${prefix}fp${i}) ; ${name}.props`,
` store ptr null, ptr getelementptr inbounds (%ScrClosure, ptr @${mangleFnClosure(name)}, i64 0, i32 3)`,
);
});
for (const name of emitter.classObjs.keys())
lines.push(` call void @scr_classobj_clear_properties(ptr @${mangleClassObj(name)})`);
return lines;
};
// Exit listeners can read MODULE GLOBALS directly, so they must run
// BEFORE the global releases (the runtime ABI’s runExitListeners
// ordering — the atexit half becomes an idempotent no-op).
const usesEvents = runtimeFeatures.processEvents;
const usesChildProcess = runtimeFeatures.childProcess;
const usesFsWatch = runtimeFeatures.fsWatch;
// fs.watchFile programs register the poller unit's exit cleanup — the
// same one-line gating (native/executable.ts links scr_watch_file.c on
// the same predicate).
const usesFsWatchFile = runtimeFeatures.fsWatchFile;
// Programs that call into Node record the libnode pack and its Node
// version before %main (scr_node_island.c).
const usesNodeIsland = runtimeFeatures.nodeIsland;
// Stream-surface programs fill the loop's stream hook (the deferred
// next-tick emissions) and the emitter's post-registration flow kick
// before %main — scr_stream.c links only when the line is emitted
// (native/executable.ts gates on the same predicate).
const usesStream = runtimeFeatures.stream;
// Net-surface programs fill the loop's net hooks (and the netSocket
// handle-dispatch ops for the checked-dynamic boundary); http-surface
// programs additionally stamp the httpReq/httpRes ops — the C main's
// install lines, gated on the same predicates native/executable.ts links by.
const usesNet = runtimeFeatures.net;
const usesNetLookup = runtimeFeatures.netLookup;
const usesDgram = runtimeFeatures.dgram;
const usesHttp2 = runtimeFeatures.http2;
const usesHttp = runtimeFeatures.http;
// Fetch-referencing programs register native fetch before main.
// Runtime selection compiles scr_fetch.c on the same predicate.
const usesFetch = runtimeFeatures.fetch;
// URL programs give the host parser the IDNA unit's ToASCII.
const usesUrlIdna = runtimeFeatures.urlIdna;
const snapshotsTlsCa = runtimeFeatures.tls || runtimeFeatures.tlsCa;
// The process verdict has the same precedence as the C reference
// emitter: node:test owns the final status when present; otherwise an
// embedded process.exitCode owns it; ordinary programs return zero.
const usesNodeTest = runtimeFeatures.nodeTest;
// Declared NOW — the extern block flushes before main assembles.
if (usesEvents) emitter.declare(`declare void @scr_events_install()`);
if (usesChildProcess) emitter.declare(`declare void @scr_child_dyn_install()`);
const childStreamBoxes =
usesChildProcess &&
usesStream &&
emitter.classMeta.has("%Readable") &&
emitter.classMeta.has("%Writable")
? [
emitter.dyn.toDynHelper({ kind: "object", className: "%Readable" }),
emitter.dyn.toDynHelper({ kind: "object", className: "%Writable" }),
]
: null;
// worker.stdout and worker.stderr box the stream unit's Readable the
// same way, and worker.stdin its Writable when the program has one
// (scr_worker_stdio_install, scr_worker_events.c).
const workerStdioBox =
emitter.mod.workers === true && usesStream && emitter.classMeta.has("%Readable")
? (childStreamBoxes?.[0] ??
emitter.dyn.toDynHelper({ kind: "object", className: "%Readable" }))
: null;
const workerStdinBox =
emitter.mod.workers === true && usesStream && emitter.classMeta.has("%Writable")
? (childStreamBoxes?.[1] ??
emitter.dyn.toDynHelper({ kind: "object", className: "%Writable" }))
: null;
if (childStreamBoxes || workerStdioBox || workerStdinBox) {
emitter.declare(
`@scr_stream_child_ops = external constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr }`,
);
}
if (childStreamBoxes)
emitter.declare(`declare void @scr_child_dyn_streams_install(ptr, ptr, ptr)`);
if (workerStdioBox || workerStdinBox)
emitter.declare(`declare void @scr_worker_stdio_install(ptr, ptr, ptr)`);
if (usesFsWatch) emitter.declare(`declare void @scr_watch_install()`);
if (usesFsWatchFile) emitter.declare(`declare void @scr_watch_file_install()`);
if (usesNodeIsland) emitter.declare(`declare void @scr_node_island_install(ptr, ptr)`);
// Interned now: the C-string constants flush before main assembles.
const nodeIslandConfig = usesNodeIsland
? [
emitter.cstr(emitter.mod.nodeIsland?.libnode ?? ""),
emitter.cstr(emitter.mod.nodeIsland?.nodeVersion ?? ""),
]
: null;
// The island module tree: its manifest, and its files unless they ship
// beside the executable (node-islands/modules.ts).
const nodeIslandModules = usesNodeIsland ? emitter.mod.nodeIsland?.modules : undefined;
const nodeIslandModulesCall =
nodeIslandModules === undefined ? null : nodeIslandModulesInstall(emitter, nodeIslandModules);
if (emitter.ffiHasForeignCallback) emitter.declare(`declare void @scr_ffi_install()`);
if (usesStream) emitter.declare(`declare void @scr_stream_install()`);
if (usesNetLookup) emitter.declare(`declare void @scr_net_lookup_install()`);
if (usesNet) {
emitter.declare(`declare void @scr_net_install()`);
emitter.declare(`declare void @scr_net_dyn_install()`);
}
if (usesDgram) emitter.declare(`declare void @scr_dgram_install()`);
if (usesHttp2) emitter.declare(`declare void @scr_http2_dyn_install()`);
if (usesHttp) emitter.declare(`declare void @scr_http_dyn_install()`);
if (usesFetch) emitter.declare(`declare void @scr_fetch_install()`);
if (usesUrlIdna) emitter.declare(`declare void @scr_url_idna_install()`);
if (usesNodeTest) {
emitter.declare(`declare i32 @scr_test_exit_code()`);
emitter.declare(`declare void @scr_test_init()`);
}
if (snapshotsTlsCa) {
emitter.declare(`declare void @scr_tls_ca_install()`);
}
// Inline exit listeners run when something they must beat exists:
// the refcounted-global releases, or the retained-FFI atexit ledger
// sweep (a listener may legitimately release or pump a registration,
// and only the inline call orders ahead of every atexit handler —
// the runtime ABI’s runExitListeners stance). Plain event programs
// with neither keep the atexit path, so their listener timing is
// unchanged.
const hasRefGlobals = globals.some((g) => isRefCounted(g.type)) || fnValueProps.length > 0;
const inlineExitListeners =
usesEvents && (emitter.mod.workers || hasRefGlobals || emitter.ffiHasRetainedCallback);
emitter.declare(`declare i32 @scr_exit_code_hint_get()`);
if (inlineExitListeners) {
emitter.declare(`declare void @scr_run_exit_listeners(double)`);
emitter.declare(`declare i32 @scr_exit_code_hint_get()`);
}
const exitListenerLines = (prefix: string): string[] => {
if (!inlineExitListeners) return [];
return [
` %${prefix}h = call i32 @scr_exit_code_hint_get()`,
` %${prefix}hd = sitofp i32 %${prefix}h to double`,
` call void @scr_run_exit_listeners(double %${prefix}hd)`,
];
};
// The entry's exits release the module globals only where the thread
// frees its heap when its script ends (scr_exit_frees: workers, and
// builds that check for leaks); the main thread of other builds leaves
// it to the operating system.
const exitReleaseLines = (prefix: string): string[] => {
const releases = globalReleaseLines(prefix);
if (releases.length === 0) return releases;
emitter.declare(`declare zeroext i1 @scr_exit_frees()`);
return [
` %${prefix}.frees = call zeroext i1 @scr_exit_frees()`,
` br i1 %${prefix}.frees, label %${prefix}.release, label %${prefix}.kept`,
`${prefix}.release:`,
...releases,
` br label %${prefix}.kept`,
`${prefix}.kept:`,
];
};
const globalReleases = exitReleaseLines("g");
const asyncEntry = emitter.fnByName.get(emitter.mod.entry)?.async === true;
const entryMayThrow = emitter.mayThrow.has(emitter.mod.entry);
// The event loop runs when timers appeared, a promise is made (the loop's
// end reports an unhandled rejection, Node's exit 1), OR any
// async/generator function exists (the C main's hasAsync ||
// hasGenerators || usesTimers gate). Generator programs run
// the loop too: its exit accounting notes still-suspended generator
// fibers as abandoned, so the RC audit downgrades exactly like the
// async loop-exhaustion story.
const runsLoop =
emitter.usesTimers ||
emitter.usesPromises ||
emitter.ffiHasForeignCallback ||
emitter.mod.functions.some((f) => f.async === true || f.generator !== undefined);
const uncaughtReleases = entryMayThrow && !asyncEntry ? exitReleaseLines("gu") : [];
const loopReleasesU = runsLoop ? exitReleaseLines("gl") : [];
const loopReleasesR = runsLoop ? exitReleaseLines("gr") : [];
const topRejectReleases = asyncEntry ? exitReleaseLines("gt") : [];
const topPendingReleases = asyncEntry ? exitReleaseLines("gp") : [];
const loopReportedReleases = runsLoop ? exitReleaseLines("gq") : [];
// main's epilogues read the flag / the loop entry points — declared
// HERE, before the extern block flushes. They run once, so they keep
// the out-of-line scr_exc_pending call; per-call checks inline the
// test (pendingTestLines).
if (entryMayThrow || runsLoop) emitter.declare(`declare zeroext i1 @scr_exc_pending()`);
if (runsLoop) {
emitter.declare(`declare zeroext i1 @scr_loop_run(ptr)`);
if (emitter.mod.commonJsEntry === true)
emitter.declare(`declare void @scr_loop_commonjs_entry()`);
if (emitter.readsLoopMetrics()) emitter.declare(`declare void @scr_perf_loop_mark()`);
emitter.declare(`declare zeroext i1 @scr_report_unhandled_rejections()`);
emitter.declare(`declare void @scr_discard_unhandled_rejections()`);
}
if (asyncEntry) {
emitter.declare(`declare i32 @scr_promise_finish_top_level(ptr)`);
emitter.declare(`declare void @scr_promise_rethrow_top_level(ptr)`);
emitter.declare(`declare void @scr_promise_release(ptr)`);
emitter.declare(`declare void @scr_exit_code_note(i32)`);
}
const topPendingExitLines = (): string[] => {
if (!asyncEntry) return [];
const lines: string[] = [];
// finish_top_level noted 13 unless the program set process.exitCode.
// A failed node:test run outranks either before exit listeners run.
if (usesNodeTest) {
lines.push(
` %tla_program_exit = call i32 @scr_test_exit_code()`,
` %tla_program_exit_zero = icmp eq i32 %tla_program_exit, 0`,
` %tla_hint = call i32 @scr_exit_code_hint_get()`,
` %tla_exit_status = select i1 %tla_program_exit_zero, i32 %tla_hint, i32 %tla_program_exit`,
` call void @scr_exit_code_note(i32 %tla_exit_status)`,
);
}
for (const line of exitListenerLines("xp")) lines.push(line);
for (const line of topPendingReleases) lines.push(line);
// An exit listener may set process.exitCode again, as in Node.
lines.push(` %tla_exit_code = call i32 @scr_exit_code_hint_get()`, ` ret i32 %tla_exit_code`);
return lines;
};
// LIBRARY mode: the runtime entry points the generated library
// symbols delegate to — declared before the extern block flushes.
if (emitter.mod.lib !== undefined) {
if (emitter.wasi) {
emitter.declare(`declare ptr @malloc(${emitter.sizeType})`);
emitter.declare(`declare void @free(ptr)`);
}
emitter.declare(`declare void @scr_library_entry(i1 zeroext, ptr)`);
emitter.declare(`declare void @scr_library_reset()`);
emitter.declare(`declare void @scr_library_check_exc()`);
emitter.declare(`declare void @scr_library_set_sink(ptr, ptr)`);
emitter.declare(`declare void @scr_library_callback_entry_guard(ptr)`);
emitter.declare(`declare void @scr_library_arena_reset()`);
emitter.declare(`declare void @scr_library_collect()`);
if ((emitter.mod.lib.callbacks?.length ?? 0) > 0) {
// Host-callback channels: the registration define's dispatch
// (strcmp over the declared names + the runtime slot store).
// The call-site fetch pair (scr_library_cb_require/_ctx) is
// declared at ffiCall emission like every body-driven runtime
// symbol.
emitter.declare(`declare void @scr_library_cb_set(${emitter.sizeType}, ptr, ptr)`);
emitter.declare(`declare i32 @strcmp(ptr, ptr)`);
}
if (emitter.mod.lib.exports.some((e) => e.params.includes("string"))) {
emitter.declare(`declare ptr @scr_library_str_in(ptr, ${emitter.sizeType})`);
}
if (emitter.mod.lib.exports.some((e) => e.params.includes("bytes"))) {
emitter.declare(`declare ptr @scr_library_bytes_in(ptr, ${emitter.sizeType}, ptr)`);
}
if (emitter.mod.lib.exports.some((e) => e.params.includes("i64"))) {
emitter.declare(`declare double @scr_library_i64_in(i64, ptr)`);
}
if (emitter.mod.lib.exports.some((e) => e.params.includes("u64"))) {
emitter.declare(`declare double @scr_library_u64_in(i64, ptr)`);
}
if (emitter.mod.lib.exports.some((e) => e.returns === "string")) {
emitter.declare(`declare void @scr_library_str_out(ptr, ptr, ptr)`);
}
if (emitter.mod.lib.exports.some((e) => e.returns === "bytes")) {
emitter.declare(`declare void @scr_library_bytes_out(ptr, ptr, ptr)`);
}
if (emitter.mod.lib.exports.some((e) => e.params.includes("cstring"))) {
emitter.declare(`declare ptr @scr_library_cstr_in(ptr, ptr)`);
}
if (emitter.mod.lib.exports.some((e) => e.returns === "cstring")) {
emitter.declare(`declare ptr @scr_library_cstr_out(ptr)`);
}
}
// Helpers assemble BEFORE the declaration table flushes (they add
// write/abort declarations).
const helpers = [
...emitter.helperDefs(),
...emitArraySearchHelpers(emitter.arraySearchKinds, emitter.sizeType, (line) =>
emitter.declare(line),
),
...emitClassMembershipHelper(emitter.classMeta, emitter.sizeType),
...(emitter.publishWalkersCache?.defs() ?? []),
...emitter.publishedClassNamesDefs(),
...(emitter.regions?.moduleDefs() ?? []),
...(emitter.cpuDispatch ? cpuLevelDefs((line) => emitter.declare(line)) : []),
];
if (emitter.constantNumericTables.size > 0)
emitter.declare(`declare double @scr_arr_get_number(ptr, double)`);
// Native matchers of the regex literals inside the compiled subset.
const regexMatchers = new Map<string, RegexMatcherCode>();
for (const [key, re] of emitter.regexInstances) {
const code = compileRegexMatcher(
`${re.sym}_match`,
re.pattern,
re.flags,
emitter.sizeType,
!emitter.wasi,
);
if (code === null) continue;
regexMatchers.set(key, code);
for (const decl of code.decls) emitter.declare(decl);
}
// Replacers of global substitutions with a literal replacement; the call
// sites register only patterns inside the compiled subset.
const regexReplacers: RegexMatcherCode[] = [];
for (const rr of emitter.regexReplacers.values()) {
const code = compileRegexMatcher(
rr.sym,
rr.pattern,
rr.flags,
emitter.sizeType,
!emitter.wasi,
rr.replacement,
);
if (code === null)
throw new InternalCompilerError(`llvm emitter bug: replacer ${rr.sym} did not compile`);
regexReplacers.push(code);
for (const decl of code.decls) emitter.declare(decl);
}
const out: string[] = [
`; Generated by scriptc (LLVM backend) from ${emitter.mod.sourceFile}. Do not edit.`,
``,
// Type shapes shared with the runtime's C ABI. ScrStr is the header
// prefix only (the flexible-array tail is concrete per literal);
// ScrLogArg is { i32 tag; 8-byte union } — i64 at offset 8 matches
// the C layout (4 bytes padding after the tag). ScrUnion/ScrClosure
// mirror scr_runtime.h field-for-field (tag reads, slot peeks, the
// fn pointer, and the caps[] tail all address through them).
`%ScrStr = type { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType} }`,
`%ScrLogArg = type { i32, i64 }`,
`%ScrVt = type { ${emitter.sizeType}, ${emitter.sizeType}, ptr }`,
`%ScrUnion = type { ${emitter.sizeType}, i32, ptr, ptr, ptr, i64 }`,
`%ScrClosure = type { ${emitter.sizeType}, ptr, ${emitter.sizeType}, ptr, i32, i32, ptr, ptr }`,
`%ScrFfiTable = type { ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, i8, ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr }`,
`%ScrRegex = type { ${emitter.sizeType}, ptr, ptr, ptr, double, ptr, ptr, ptr }`,
// ScrArr mirrors scr_runtime.h field-for-field: { rc, len (i32), cap
// (i32), elem, cyc, spare, filled, data } (field numbers in
// SCR_ARR_FIELD), where the dense states follow the cap value slots at
// data, filled bounds the hole-free prefix, and spare holds the storage
// flags and the registry id of the element type's RC descriptor
// (%ScrArrOps, SCR_ELEM_REF only; the runtime assigns ids). An array
// with out-of-line sparse, property or metadata storage keeps a pointer
// to it in the word before its dense allocation. Live dynamic stream
// commits swap its dense, presence and out-of-line storage while
// preserving the target object's identity.
`%ScrArr = type { ${emitter.sizeType}, i32, i32, i8, i8, i16, i32, ptr }`,
`%ScrArrOps = type { ptr, ptr, ptr, i32 }`,
// The ScrMap prefix { rc, key_kind, val_kind, traced } — the inline
// RC fast paths test the traced byte (shapes.ts).
...(emitter.rcHelpers === null ? [] : [`%ScrMapRc = type { ${emitter.sizeType}, i8, i8, i8 }`]),
// The complete ScrMap and its entries, read by the inline number-key
// lookup (sc_map_entry_f64), the inline iteration steps
// (sc_map_iter_entry, sc_map_iter_entry_num) and Map.size
// (sc_map_size): field 8 is the i32 entry count, field 9 the i32 live
// count, field 10 the i64 absent-key filter, entries is field 11,
// nbuckets and buckets fields 13 and 14, the direct index and its i32
// length fields 15 and 16.
...(emitter.decls.has(NUMBER_MAP_ENTRY_DECL) ||
emitter.decls.has(STRING_MAP_ENTRY_DECL) ||
emitter.decls.has(STRING_MAP_ENTRY_C_DECL) ||
emitter.decls.has(MAP_ITER_DECL) ||
emitter.decls.has(MAP_ITER_NUM_DECL) ||
emitter.decls.has(MAP_SIZE_DECL)
? [
`%ScrMapIx = type { ${emitter.sizeType}, i8, i8, i8, i8, i32, ptr, ptr, i32, i32, i64, ptr, i32, i32, ptr, ptr, i32, i32 }`,
]
: []),
// The { key, val, hash } entry of every map but a number-key one, whose
// entry is { key, val } (scr_map.h ScrMapNumEntry). String-keyed
// reference reads load the value slot of the entry scr_map_entry_str
// returns (map-number-lookup.ts).
...(emitter.decls.has(NUMBER_MAP_ENTRY_DECL) ||
emitter.decls.has(STRING_MAP_ENTRY_DECL) ||
emitter.decls.has(MAP_ITER_DECL) ||
emitter.decls.has(MAP_ITER_NUM_DECL)
? [`%ScrMapEntry = type { i64, i64, i64 }`, `%ScrMapNumEntry = type { i64, i64 }`]
: []),
// A compact-value map's { key, hash, val } entry (scr_map.h ScrMapCEntry):
// a 32-bit hash and the value as an address over 8.
...(emitter.decls.has(STRING_MAP_ENTRY_C_DECL)
? [`%ScrMapCEntry = type { i64, i32, i32 }`]
: []),
// 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.
`%ScrError = type { ${emitter.sizeType}, ptr, ptr, ptr, ptr, ptr, i8, i8, i8, i8, i8, ptr, ptr }`,
`%ScrClassObj = type { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr, ptr, ${emitter.sizeType} }`,
// The runtime emitter prefix { rc, vt, reg, cls } — user subclasses
// embed it (classes.ts), and bare-emitter GEPs address through it.
`%ScrEmitter = type { ${emitter.sizeType}, ptr, ptr, ptr }`,
// The runtime stream layout { rc, vt, reg, cls, st } — one struct
// for all five stream classes; stream subclasses embed it.
`%ScrStream = type { ${emitter.sizeType}, ptr, ptr, ptr, ptr }`,
// The EventTarget and Event subclass prefixes { rc, vt, state, cls }.
...(emitter.classMeta.has(RUNTIME_EVENT_TARGET_CLASS)
? [`%ScrEventTargetObj = type { ${emitter.sizeType}, ptr, ptr, ptr }`]
: []),
...(emitter.classMeta.has(RUNTIME_EVENT_CLASS)
? [`%ScrEventObj = type { ${emitter.sizeType}, ptr, ptr, ptr }`]
: []),
// The catch-binding snapshot box { rc, kind, f64, b, payload,
// retain_fn, release_fn, trace_fn } — caughtTest kind reads and
// caughtNarrow payload extraction GEP through it (offsets match
// scr_runtime.h's natural alignment: kind at 8, f64 at 16, b at 24,
// payload at 32).
`%ScrCaught = type { ${emitter.sizeType}, i32, double, i8, ptr, ptr, ptr, ptr }`,
// ScrBytes { rc, len, elem(i32+pad), data, backing, brands, shared }. Indexed
// typed-array access GEPs through this directly: the IR type already
// fixes elem, so the hot path needs neither a runtime kind load nor
// the generic scr_bytes_get/set call.
`%ScrBytes = type { ${emitter.sizeType}, ${emitter.sizeType}, i32, ptr, ptr, i8, i8, i8, i8, ptr }`,
// The capture box { rc, kind, obj_retain, obj_release, obj_trace,
// slot } — TDZ reads peek the payload slot (offset 40) directly.
`%ScrBox = type { ${emitter.sizeType}, i32, ptr, ptr, ptr, i64 }`,
// The stack buffer of the emitted JSON serializers { data, len, cap }.
`%ScrJsonBuf = type { ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ${emitter.sizeType}, ${emitter.sizeType} }`,
// The dynCheck error-path spine { parent, key, index } — the emitted
// builders stack-allocate one per recursion level (dyn.ts).
`%ScrDynPath = type { ptr, ptr, ${emitter.sizeType} }`,
`%ScrIslandModule = type { ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, i32, ptr, ${emitter.sizeType}, ${emitter.sizeType} }`,
`%ScrIslandEdge = type { ptr, ptr, ptr, i32 }`,
// A generator's prefix { rc, receiver, fiber, state, out } and its
// value slot { kind, f64, b, payload, release_fn } (scr_async.c pins
// both): number and boolean yields and drives address OUT directly.
...(emitter.usesGenSlots
? [
`%ScrGenSlot = type { i32, double, i8, ptr, ptr }`,
`%ScrGenHead = type { ${emitter.sizeType}, ptr, ptr, i32, %ScrGenSlot }`,
]
: []),
];
for (const line of shapes.typeDefs) out.push(line);
for (const line of classShapes.typeDefs) out.push(line);
// Thread-instanced library state (abi.instance_per_thread): the
// program TU's mutable globals — module globals, run-once guards, the
// lazily-compiled regex literal caches — and the runtime globals its
// init stamps live in thread-local storage, matching the runtime
// objects compiled with -DSCR_THREAD_INSTANCES. Immutable interned
// data (string literals, unit arms, template arrays, vtables) stays
// shared.
const tl =
emitter.mod.workers === true || emitter.mod.lib?.threadInstances === true
? "thread_local "
: "";
out.push(
``,
`@scr_error_vts = external ${tl}global [${RUNTIME_ERROR_CLASSES.size} x %ScrVt]`,
`declare void @scr_init()`,
...(emitter.compactRegion ? [`declare void @scr_plr_init()`] : []),
`declare void @scr_lib_init(i32, ptr)`,
...(emitter.mod.workers ? [`declare void @scr_runtime_workers_v8()`] : []),
...(emitter.stackChecks?.size ? [`declare void @scr_stack_guard_init()`] : []),
...(emitter.runtimeAbiMarker && emitter.mod.lib === undefined
? [`declare void @${RUNTIME_ABI_MARKER}()`]
: []),
// Overrides the runtime's weak default (scr_lib.c).
...(emitter.detachedExit && emitter.mod.lib === undefined
? [`@scr_detached_exit = constant i8 1`]
: []),
);
for (const d of emitter.decls) out.push(d);
out.push(``);
const flushedDecls = new Set(emitter.decls);
{
const tl =
emitter.mod.workers === true || emitter.mod.lib?.threadInstances === true
? "thread_local "
: "";
out.push(...threadSlotsBlockDefinitions(emitter.threadSlotsBlocks, tl !== ""));
for (let site = 0; site < emitter.dynReadIcSites; site++)
out.push(
`@sc_dyn_read_ic_${site} = internal ${tl}global { ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr } zeroinitializer`,
);
for (let site = 0; site < emitter.jsIcSiteCount; site++)
out.push(`@sc_js_ic_${site} = internal ${tl}global [${JS_IC_WORDS} x i64] zeroinitializer`);
}
if (nodeIslandModulesCall !== null) out.push(...nodeIslandModulesCall.globals);
for (const table of emitter.constantNumericTables.values()) {
const n = table.values.length;
let tableTmp = 0;
const tableIndex = exactIndexLines("%i", String(n), "i64", () => `%ix${tableTmp++}`);
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`,
...tableIndex.lines.map((line) => ` ${line}`),
` %safe = and i1 %initialized, ${tableIndex.ok}`,
` br i1 %safe, label %read, label %fallback`,
`read:`,
` %slot = getelementptr inbounds [${n} x double], ptr @${table.symbol}, i64 0, i64 ${tableIndex.wide}`,
` %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 emitter.literals) {
// Immortal interned ScrStr: { rc = SIZE_MAX, len, cap = len, bytes\0 } —
// the runtime ABI’s static table, retain/release skip rc == SIZE_MAX.
// 64-bit (little-endian) targets split the capacity word: the high
// half carries the precomputed Map key hash (SCR_STR_HASH_CACHE).
// Bit 31 marks all-ASCII literals (SCR_STR_ASCII_BIT) on every target.
const capWord =
emitter.sizeType === "i64"
? literalCapWord(wtf8Bytes(text))
: literalCapWord32(wtf8Bytes(text));
out.push(
`@${lit.sym} = internal global { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, [${lit.len + 1} x i8] } ` +
`{ ${emitter.sizeType} -1, ${emitter.sizeType} ${lit.len}, ${emitter.sizeType} ${capWord}, [${lit.len + 1} x i8] c"${llStrBytes(text)}" }`,
);
}
if (emitter.literals.size > 0) out.push(``);
for (const lit of emitter.bigintLiterals.values()) {
// Immortal ScrBigInt: { rc = SIZE_MAX, len, sign, cap, limbs } with
// pointer-sized little-endian limbs (scr_bigint.c); zero keeps one
// zero limb so limb 0 is always readable.
const size = emitter.sizeType;
const limbBits = size === "i64" ? 64n : 32n;
const mask = (1n << limbBits) - 1n;
const limbs: string[] = [];
for (let m = lit.value < 0n ? -lit.value : lit.value; m > 0n; m = m >> limbBits) {
limbs.push(`${size} ${m & mask}`);
}
const len = limbs.length;
if (len === 0) limbs.push(`${size} 0`);
const sign = lit.value < 0n ? -1 : lit.value > 0n ? 1 : 0;
out.push(
`@${lit.sym} = internal global { ${size}, ${size}, i32, i32, [${limbs.length} x ${size}] } ` +
`{ ${size} -1, ${size} ${len}, i32 ${sign}, i32 ${limbs.length}, [${limbs.length} x ${size}] [${limbs.join(", ")}] }`,
);
}
if (emitter.bigintLiterals.size > 0) out.push(``);
for (const [key, sym] of emitter.unitInstances) {
// One immortal instance per unit-armed (union, tag): tag set, payload
// slot and RC entry points zero — retain/release/collector all skip
// rc == SIZE_MAX, so these never join the RC audit or a trace walk.
const [unionId, tag] = key.split(":");
out.push(
`@${sym} = internal global %ScrUnion { ${emitter.sizeType} -1, i32 ${tag}, ptr null, ptr null, ptr null, i64 0 } ; ${unionId} unit arm`,
);
}
if (emitter.unitInstances.size > 0) out.push(``);
for (const [unionId, sym] of emitter.retypeGuards) {
// Thread-local like module globals: a thread's cast caches hold only
// its own boxes. Never written, the flag folds to false.
out.push(`@${sym} = internal ${tl}global i8 0 ; ${unionId} boxes in a cast cache`);
}
if (emitter.retypeGuards.size > 0) out.push(``);
for (const [unionId, sym] of emitter.absentInstances) {
// One immortal ABSENT field-slot instance per undefined-armed union:
// the undefined arm's tag with payload 1 (ordinary unit instances
// carry 0). Every tag reader sees undefined; recordHas and the
// field reads compare tag and payload, never the address, so the
// per-object copies of separately compiled units agree.
const tag = undefinedArmTag({ kind: "union", unionId }, emitter.unionsById);
out.push(
`@${sym} = internal global %ScrUnion { ${emitter.sizeType} -1, i32 ${tag}, ptr null, ptr null, ptr null, i64 ${ABSENT_FIELD_PAYLOAD} } ; ${unionId} absent field`,
);
}
if (emitter.absentInstances.size > 0) out.push(``);
if (emitter.needsUnionNone)
out.push(
`@sc_union_none = internal constant %ScrUnion zeroinitializer ; NULL union dummy read by inline stores`,
``,
);
if (emitter.nullableUnions.usesNullSentinel) {
// `null` in every `C | null | undefined` union (NULL is undefined):
// one module-scope immortal object like the ABSENT sentinel.
const S = emitter.sizeType;
out.push(
`${NULLABLE_NULL} = internal global { ${S}, ${S}, ${S}, ${S} } { ${S} -1, ${S} 0, ${S} 0, ${S} 0 } ; nullable null arm`,
``,
);
}
if (emitter.needsNullableAbsent) {
// The ABSENT field-slot state of every nullable-pointer union: one
// module-scope immortal object (rc == SIZE_MAX), distinct from NULL
// (undefined) and from every instance. RC entry points and the
// collector skip it like any immortal; field reads compare its address.
const S = emitter.sizeType;
out.push(
`${NULLABLE_ABSENT} = internal global { ${S}, ${S}, ${S}, ${S} } { ${S} -1, ${S} 0, ${S} 0, ${S} 0 } ; nullable absent field`,
``,
);
}
for (const [key, re] of emitter.regexInstances) {
// One immortal ScrRegex per (pattern, flags) literal, pointing at
// the interned source/flags strings. The bc and translated-program
// slots start null (lazy compile, cached by the runtime) — a mutable
// global, not constant. The last slot is the build-time matcher.
const matcher = regexMatchers.has(key) ? `@${re.sym}_match` : "null";
out.push(
`@${re.sym} = internal ${tl}global %ScrRegex { ${emitter.sizeType} -1, ptr ${re.src}, ptr ${re.fl}, ptr null, double 0.0, ptr null, ptr null, ptr ${matcher} } ; ${key.replace(/\n/g, "\\n")}`,
);
}
if (emitter.regexInstances.size > 0) out.push(``);
for (const code of regexMatchers.values()) out.push(...code.lines, ``);
for (const code of regexReplacers) out.push(...code.lines, ``);
for (const [, inst] of emitter.templateStringsInstances) {
// One immortal ScrArr per tagged-template site: a data global of
// interned cooked-string literals, and the ScrArr header over it
// (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, no REF entry
// points, no state bytes: every dense slot is present). Reads retain
// immortal strings — a no-op. Slots are 8 bytes on every target: on
// 32-bit targets each holds the pointer in its low half and zero in its
// high half.
const n = inst.slots.length;
const wide = emitter.sizeType === "i64";
const slot = wide ? "ptr" : "<{ ptr, i32 }>";
const slotValue = (s: string): string =>
wide ? `ptr ${s}` : `<{ ptr, i32 }> <{ ptr ${s}, i32 0 }>`;
out.push(
`@${inst.sym}_data = internal constant [${n} x ${slot}] [ ${inst.slots.map(slotValue).join(", ")} ], align 8`,
`@${inst.sym} = internal global %ScrArr { ${emitter.sizeType} -1, i32 ${n}, i32 ${n}, i8 2, i8 0, i16 0, i32 ${n}, ptr @${inst.sym}_data }`,
);
}
if (emitter.templateStringsInstances.size > 0) out.push(``);
for (const [text, c] of emitter.cstrs) {
// NUL-terminated byte-array constants for scr_jb_puts and indentation.
out.push(`@${c.sym} = internal constant [${c.len + 1} x i8] c"${llStrBytes(text)}"`);
}
if (emitter.cstrs.size > 0) out.push(``);
for (const [body, sym] of emitter.mapOpsConsts) {
out.push(`@${sym} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr } { ${body} }`);
}
if (emitter.mapOpsConsts.size > 0) out.push(``);
for (const sym of emitter.methodIdSlots.values()) out.push(`@${sym} = internal global i32 -1`);
if (emitter.methodIdSlots.size > 0) out.push(``);
for (const line of ffiCallbacks.globals) out.push(line);
if (ffiCallbacks.globals.length > 0) out.push(``);
for (const g of globals) {
const ty = emitter.llType(g.type);
const zero = ty === "double" ? f64Lit(0) : ty === "ptr" ? "null" : "false";
const debug = emitter.debug?.global(g);
out.push(
`@${mangleGlobal(g.id)} = internal ${tl}global ${ty} ${zero}${debug ? `, !dbg ${debug}` : ""} ; ${g.name}`,
);
}
if (globals.length > 0) out.push(``);
writeDefinitions(
out,
emitter.targetOptions.functionLayout,
[
helpers,
ffiCallbacks.defs,
shapes.defs,
classShapes.defs,
classObjDefs,
emitter.walkers.defs,
emitter.dyn.defs,
wrappers,
asyncDefs,
emitter.resolveThunkDefs,
],
fnDefs,
partitionGroupLines(emitter.mod.functions),
partitionEntries(emitter.mod.functions, emitter.mod.sourceFile),
);
if (fnDefs.length === 0) out.push("");
out.push(PARTITION_GROUP_END, "");
// WebAssembly modules translated into this program, and the registry.
for (const line of emitWasmModules(emitter.mod, flushedDecls, emitter.wasmBoundsChecks))
out.push(line);
// main(): scr_init, the program-dependent error-vt interval stamps,
// scr_lib_init(argc, argv), then the entry function. An uncaught
// exception escaping top-level code prints and exits 1 (Node).
// The exception epilogue is emitted when the entry may throw.
const stamps: string[] = [...errorViews];
for (const iv of emitter.errorIntervals) {
const fields: [number, number][] = [
[0, iv.pre],
[1, iv.post],
];
for (const [field, value] of fields) {
stamps.push(
` store ${emitter.sizeType} ${value}, ptr getelementptr inbounds ([${RUNTIME_ERROR_CLASSES.size} x %ScrVt], ptr @scr_error_vts, i64 0, i64 ${iv.kind}, i32 ${field})${field === 1 ? ` ; ${iv.lib}` : ""}`,
);
}
}
if (emitter.emitterInterval !== null) {
// The runtime emitter vtable's interval (emitterVtStampLines): bare
// EventEmitter instances answer instanceof and dispatch dynamic
// teardown under THIS module's preorder numbering.
out.push(`@scr_emitter_vt = external ${tl}global %ScrVt`, ``);
stamps.push(
` store ${emitter.sizeType} ${emitter.emitterInterval.pre}, ptr getelementptr inbounds (%ScrVt, ptr @scr_emitter_vt, i64 0, i32 0)`,
` store ${emitter.sizeType} ${emitter.emitterInterval.post}, ptr getelementptr inbounds (%ScrVt, ptr @scr_emitter_vt, i64 0, i32 1) ; EventEmitter`,
);
}
for (const iv of emitter.streamIntervals) {
// The runtime stream vtables' intervals (streamVtStampLines) — the
// emitter story: instanceof and dynamic teardown dispatch through
// them.
out.push(`@${iv.vt} = external ${tl}global %ScrVt`);
stamps.push(
` store ${emitter.sizeType} ${iv.pre}, ptr getelementptr inbounds (%ScrVt, ptr @${iv.vt}, i64 0, i32 0)`,
` store ${emitter.sizeType} ${iv.post}, ptr getelementptr inbounds (%ScrVt, ptr @${iv.vt}, i64 0, i32 1) ; ${iv.lib}`,
);
}
if (emitter.streamIntervals.length > 0) out.push(``);
if ([...emitter.literals.keys(), ...emitter.cstrs.keys()].some(hasLoneSurrogate)) {
// The runtime skips surrogate work until a lone surrogate may exist
// (scr_wtf8_any); its own producers note one, and literals do here.
out.push(`declare void @scr_wtf8_note()`, ``);
stamps.push(` call void @scr_wtf8_note()`);
}
if (emitter.errorIntervals.length > 0 && emitter.tracedShapes.has("object:%Error")) {
// The cycle fixpoint marked the Error hierarchy (a user subclass
// holds cycle-capable fields — capability is hierarchy-uniform), so
// the runtime's own error allocations need collector headers too.
// Declared inline: the extern block already flushed (LLVM is
// order-free, the helper-defs precedent).
out.push(`declare void @scr_error_set_traced()`, ``);
stamps.push(` call void @scr_error_set_traced()`);
}
if ((entryMayThrow || runsLoop) && emitter.mod.lib === undefined) {
// Declared inline: the extern block already flushed (LLVM is
// order-free — the scr_error_set_traced precedent). Only the
// printer emits here (nothing else declares it); scr_exc_pending
// and the loop entry points rode the Set before the flush.
out.push(
`declare void @scr_exc_print_uncaught()`,
`declare zeroext i1 @scr_exc_handle_uncaught(i1 zeroext)`,
``,
);
}
if (emitter.mod.lib !== undefined) {
// LIBRARY mode: no @main — the profile-declared external
// symbols specified by the library IR instead.
for (const line of emitter.emitLibDefs(globals, globalReleaseLines, stamps)) out.push(line);
for (const line of emitter.inlineRcTail(flushedDecls)) out.push(line);
out.push(...functionAttributes(emitter, hasNoInlineRecordClone));
out.push(``);
out.push(...emitter.fieldAliasMetadata());
out.push(...loopUnrollMetadata(out));
if (emitter.debug !== null) out.push(emitter.debug.render());
return out;
}
out.push(
`define i32 @${emitter.mod.workers ? "sc_context_entry" : emitter.wasi ? "__main_argc_argv" : "main"}(i32 %argc, ptr %argv) ${FN_ATTRS} {`,
`entry:`,
...(emitter.runtimeAbiMarker ? [` call void @${RUNTIME_ABI_MARKER}()`] : []),
...(emitter.cpuDispatch && !emitter.mod.workers ? [CPU_LEVEL_INIT_CALL] : []),
...(emitter.mod.workers ? [` call void @scr_runtime_workers_v8()`] : []),
` call void @scr_init()`,
// Compressed fields hold addresses in the program-lifetime region, which
// is reserved below 32 GiB before compiled code runs.
...(emitter.compactRegion ? [` call void @scr_plr_init()`] : []),
// Each context (the main thread, every worker) installs its stack
// guard before running compiled code that checks it.
...(emitter.stackChecks?.size ? [` call void @scr_stack_guard_init()`] : []),
...stamps,
// Event-surface programs (signal/exit listeners) fill the loop's
// nullable event hooks before %main — scr_events.c links only when
// this line is emitted (native/executable.ts gates on the same predicate).
...(usesEvents ? [` call void @scr_events_install()`] : []),
...(usesChildProcess ? [` call void @scr_child_dyn_install()`] : []),
...(childStreamBoxes
? [
` call void @scr_child_dyn_streams_install(ptr @scr_stream_child_ops, ptr @${childStreamBoxes[0]}, ptr @${childStreamBoxes[1]})`,
]
: []),
...(workerStdioBox || workerStdinBox
? [
` call void @scr_worker_stdio_install(ptr @scr_stream_child_ops, ptr ${workerStdioBox ? `@${workerStdioBox}` : "null"}, ptr ${workerStdinBox ? `@${workerStdinBox}` : "null"})`,
]
: []),
// fs.watch programs fill the loop's watch hooks the same way —
// scr_watch.c links only when this line is emitted.
...(usesFsWatch ? [` call void @scr_watch_install()`] : []),
// fs.watchFile programs register the poller unit's atexit cleanup the
// same way — scr_watch_file.c links only when this line is emitted.
...(usesFsWatchFile ? [` call void @scr_watch_file_install()`] : []),
...(nodeIslandConfig
? [
` call void @scr_node_island_install(ptr ${nodeIslandConfig[0]}, ptr ${nodeIslandConfig[1]})`,
]
: []),
...(nodeIslandModulesCall === null ? [] : [nodeIslandModulesCall.call]),
...(emitter.ffiHasForeignCallback ? [` call void @scr_ffi_install()`] : []),
...(snapshotsTlsCa ? [` call void @scr_tls_ca_install()`] : []),
...(usesFetch ? [` call void @scr_fetch_install()`] : []),
...(usesUrlIdna ? [` call void @scr_url_idna_install()`] : []),
...(usesNet ? [` call void @scr_net_install()`, ` call void @scr_net_dyn_install()`] : []),
...(usesNetLookup ? [` call void @scr_net_lookup_install()`] : []),
...(usesHttp ? [` call void @scr_http_dyn_install()`] : []),
...(usesDgram ? [` call void @scr_dgram_install()`] : []),
...(usesHttp2 ? [` call void @scr_http2_dyn_install()`] : []),
...(usesStream ? [` call void @scr_stream_install()`] : []),
` call void @scr_lib_init(i32 %argc, ptr %argv)`,
// node:test takes Node's --test-update-snapshots out of argv first.
...(usesNodeTest ? [` call void @scr_test_init()`] : []),
...(asyncEntry
? [` %top = call ptr @${mangleAsyncSpawn(emitter.mod.entry)}()`]
: [` call void @${mangleFunction(emitter.mod.entry)}()`]),
// Uncaught exception from top-level code: Node exits 1.
...(entryMayThrow && !asyncEntry
? [
` %exc = call zeroext i1 @scr_exc_pending()`,
` br i1 %exc, label %uncaught, label %ok`,
`uncaught:`,
` %handled = call zeroext i1 @scr_exc_handle_uncaught(i1 false)`,
` br i1 %handled, label %ok, label %fatal`,
`fatal:`,
` call void @scr_exc_print_uncaught()`,
...exitListenerLines("xu"),
...uncaughtReleases,
` %failure_code_0 = call i32 @scr_exit_code_hint_get()`,
` ret i32 %failure_code_0`,
`ok:`,
]
: []),
// The event loop runs to exhaustion (microtasks before timers). A
// throw escaping a timer callback and unhandled promise rejections
// both exit 1, like Node — the C main's loop block exactly.
...(runsLoop
? [
...(emitter.mod.commonJsEntry === true ? [` call void @scr_loop_commonjs_entry()`] : []),
// performance.eventLoopUtilization measures from here on.
...(emitter.readsLoopMetrics() ? [` call void @scr_perf_loop_mark()`] : []),
` %loop_rejection = call zeroext i1 @scr_loop_run(ptr ${asyncEntry ? "%top" : "null"})`,
` %lexc = call zeroext i1 @scr_exc_pending()`,
` br i1 %lexc, label %luncaught, label %lok`,
`luncaught:`,
` call void @scr_exc_print_uncaught()`,
...(asyncEntry ? [` call void @scr_promise_release(ptr %top)`] : []),
...exitListenerLines("xl"),
...loopReleasesU,
` %failure_code_1 = call i32 @scr_exit_code_hint_get()`,
` ret i32 %failure_code_1`,
`lok:`,
` br i1 %loop_rejection, label %lreported, label %lclean`,
`lreported:`,
` call void @scr_discard_unhandled_rejections()`,
...(asyncEntry ? [` call void @scr_promise_release(ptr %top)`] : []),
...exitListenerLines("xq"),
...loopReportedReleases,
` %failure_code_2 = call i32 @scr_exit_code_hint_get()`,
` ret i32 %failure_code_2`,
`lclean:`,
...(asyncEntry
? [
` %tla_status = call i32 @scr_promise_finish_top_level(ptr %top)`,
` %tla_rejected = icmp eq i32 %tla_status, 1`,
` br i1 %tla_rejected, label %tla_fail, label %tla_not_rejected`,
`tla_fail:`,
// The loop already delivered every earlier-checkpoint
// rejection. Drop same-checkpoint competitors before
// surfacing the fatal module verdict.
` call void @scr_discard_unhandled_rejections()`,
` call void @scr_promise_rethrow_top_level(ptr %top)`,
` call void @scr_promise_release(ptr %top)`,
` call void @scr_exc_print_uncaught()`,
...exitListenerLines("xt"),
...topRejectReleases,
` %failure_code_3 = call i32 @scr_exit_code_hint_get()`,
` ret i32 %failure_code_3`,
`tla_not_rejected:`,
` call void @scr_promise_release(ptr %top)`,
]
: []),
` %rej = call zeroext i1 @scr_report_unhandled_rejections()`,
` br i1 %rej, label %lrej, label %lrok`,
`lrej:`,
...exitListenerLines("xr"),
...loopReleasesR,
` %failure_code_4 = call i32 @scr_exit_code_hint_get()`,
` ret i32 %failure_code_4`,
`lrok:`,
...(asyncEntry
? [
` %tla_pending = icmp eq i32 %tla_status, 13`,
` br i1 %tla_pending, label %tla_stuck, label %tla_ok`,
`tla_stuck:`,
...topPendingExitLines(),
`tla_ok:`,
]
: []),
]
: []),
...exitListenerLines("xn"),
...globalReleases,
...(usesNodeTest
? [` %test_exit = call i32 @scr_test_exit_code()`, ` ret i32 %test_exit`]
: [` %native_exit = call i32 @scr_exit_code_hint_get()`, ` ret i32 %native_exit`]),
`}`,
``,
);
for (const line of emitter.inlineRcTail(flushedDecls)) out.push(line);
out.push(
// The sanitized lane's -fsanitize=address link instruments the
// functions marked sanitize_address (the runtime TUs get theirs from
// clang).
...functionAttributes(emitter, hasNoInlineRecordClone),
``,
);
if (emitter.mod.workers) {
out.push(
`declare void @scr_context_cleanup()`,
`declare i32 @scr_worker_argc()`,
`declare ptr @scr_worker_argv()`,
`define i32 @sc_worker_entry(i32 %root) ${FN_ATTRS} {`,
`entry:`,
` %argc = call i32 @scr_worker_argc()`,
` %argv = call ptr @scr_worker_argv()`,
` %result = call i32 @sc_context_entry(i32 %argc, ptr %argv)`,
` ret i32 %result`,
`}`,
`define i32 @main(i32 %argc, ptr %argv) ${FN_ATTRS} {`,
`entry:`,
// Worker threads enter at sc_worker_entry and read the level main set.
...(emitter.cpuDispatch ? [CPU_LEVEL_INIT_CALL] : []),
` %result = call i32 @sc_context_entry(i32 %argc, ptr %argv)`,
` call void @scr_context_cleanup()`,
` ret i32 %result`,
`}`,
);
}
out.push(...emitter.fieldAliasMetadata());
out.push(...loopUnrollMetadata(out));
if (emitter.debug !== null) out.push(emitter.debug.render());
return out;
}
/** The `.loop` lookup helpers only read memory and always return, as
* declared: LLVM then merges two lookups of one key (a has() followed by
* get()) before it inlines them. */
const INLINED_LOOKUP_ATTRS = "alwaysinline memory(read) nounwind willreturn";
/** The number-key entry lookups (sc_map_entry_f64 and sc_map_entry_f64_int)
* and their hashed probes. With `loop` they are the `.loop` flavor, which
* lookups inside a loop call (emitNumberMapEntry): each of its functions is
* inlined, so a lookup that finds its key in a bucket table makes no call,
* and the loop keeps no argument moves or spills around one. */
function numberMapEntryHelpers(sz: string, loop: boolean): string[] {
const flavor = loop ? ".loop" : "";
const attrs = loop ? `${INLINED_LOOKUP_ATTRS} ${FN_ATTRS}` : FN_ATTRS;
let mapTmp = 0;
const mapIndex = exactIndexLines("%k", "%nd", sz, () => `%ix${mapTmp++}`);
return [
`define internal ptr @sc_map_entry_f64${flavor}(ptr %m, double %k) ${attrs} {`,
`entry:`,
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
...(sz === "i64"
? [` %nd32 = load i32, ptr %ndp`, ` %nd = zext i32 %nd32 to i64`]
: [` %nd = load i32, ptr %ndp`]),
// A non-integral key inside the table is as absent as one outside it.
...mapIndex.lines.map((line) => ` ${line}`),
` br i1 ${mapIndex.ok}, label %slot, label %slow`,
`slot:`,
sz === "i64"
? ` %i = add i64 ${mapIndex.wide}, 0`
: ` %i = trunc i64 ${mapIndex.wide} to ${sz}`,
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`,
` %dense = load ptr, ptr %dp`,
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
` %s = load i32, ptr %sp`,
` %empty = icmp eq i32 %s, 0`,
` br i1 %empty, label %miss, label %hit`,
`hit:`,
` %e32 = sub i32 %s, 1`,
sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`,
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
` %entries = load ptr, ptr %ep`,
` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`,
` ret ptr %found`,
`miss:`,
` ret ptr null`,
`slow:`,
` %hashed = call ptr @sc_map_entry_f64_hashed${flavor}(ptr %m, double %k)`,
` ret ptr %hashed`,
`}`,
``,
// The same lookup for a key the emitter knows is an integer (`%ki`,
// the double `%k`'s exact value): the direct index tests the integer
// itself, so the double is converted only by the hashed probe. A
// negative key is not below the index's size as an unsigned value.
`define internal ptr @sc_map_entry_f64_int${flavor}(ptr %m, i64 %ki, double %k) ${attrs} {`,
`entry:`,
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
` %nd32 = load i32, ptr %ndp`,
` %nd = zext i32 %nd32 to i64`,
` %inside = icmp ult i64 %ki, %nd`,
` br i1 %inside, label %slot, label %slow`,
`slot:`,
sz === "i64" ? ` %i = add i64 %ki, 0` : ` %i = trunc i64 %ki to ${sz}`,
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`,
` %dense = load ptr, ptr %dp`,
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
` %s = load i32, ptr %sp`,
` %empty = icmp eq i32 %s, 0`,
` br i1 %empty, label %miss, label %hit`,
`hit:`,
` %e32 = sub i32 %s, 1`,
sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`,
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
` %entries = load ptr, ptr %ep`,
` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`,
` ret ptr %found`,
`miss:`,
` ret ptr null`,
`slow:`,
` %hashed = call ptr @sc_map_entry_f64_hashed_int${flavor}(ptr %m, i64 %ki, double %k)`,
` ret ptr %hashed`,
`}`,
``,
// Hashing (scr_map_find_f64): the SameValueZero key word and its
// scr_key_mix hash. sc_map_entry_f64_int's probe takes the key's double
// with its integer view: its bits are the key word, never NaN, and +0
// for the integer 0. The caller converts the integer: cvtsi2sd writes
// only the low half of its register, so in a function entry, where
// LLVM cannot clear the register first, it waits for the register's
// last value, in a loop of lookups often a value the previous
// iteration loaded.
...hashedNumberProbe(
`sc_map_entry_f64_hashed${flavor}`,
loop,
"double %k",
sz,
[
` %nan = fcmp uno double %k, %k`,
` %zero = fcmp oeq double %k, 0.0`,
` %kz = select i1 %zero, double 0.0, double %k`,
` %kzb = bitcast double %kz to i64`,
` %kb = select i1 %nan, i64 9221120237041090560, i64 %kzb`,
],
"%k",
),
...hashedNumberProbe(
`sc_map_entry_f64_hashed_int${flavor}`,
loop,
"i64 %ki, double %k",
sz,
[
` %kzero = icmp eq i64 %ki, 0`,
` %kbits = bitcast double %k to i64`,
` %kb = select i1 %kzero, i64 0, i64 %kbits`,
],
"%k",
),
];
}
/** The hashed probe of a number-key map (scr_map_find_f64), out of line,
* or inlined with `loop`: `keyLines` define the normalized key word %kb
* from the parameter, and `runtimeKey` is the double passed to the runtime
* lookup. A map with at most SCR_MAP_LINEAR_LIMIT entries keeps no bucket
* table, and neither does a map with the direct index, whose lookups reach
* the probe only for a key outside the index, which is absent. In a small
* map the absent-key filter is tested before its entries are scanned.
* A bucket table is probed without the filter: the filter keeps two of its
* 64 bits per key, so past a few dozen keys it rejects few absent keys, and
* the probe's first bucket read answers those. Compact tables tag each
* 24-bit entry index (+1) with the hash's top byte; wider tables take the
* runtime lookup. */
function hashedNumberProbe(
name: string,
loop: boolean,
param: string,
sz: string,
keyLines: string[],
runtimeKey: string,
): string[] {
return [
`define internal ptr @${name}(ptr %m, ${param}) ${loop ? INLINED_LOOKUP_ATTRS : "noinline"} ${FN_ATTRS} {`,
`entry:`,
...keyLines,
` %x1s = lshr i64 %kb, 30`,
` %x1 = xor i64 %kb, %x1s`,
` %x2 = mul i64 %x1, -4658895280553007687`,
` %x3s = lshr i64 %x2, 27`,
` %x3 = xor i64 %x2, %x3s`,
` %x4 = mul i64 %x3, -7723592293110705685`,
` %x5s = lshr i64 %x4, 31`,
` %x5 = xor i64 %x4, %x5s`,
` %hz = icmp eq i64 %x5, 0`,
` %h = select i1 %hz, i64 1, i64 %x5`,
` %nbp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 13`,
...(sz === "i64"
? [` %nb32 = load i32, ptr %nbp`, ` %nb = zext i32 %nb32 to i64`]
: [` %nb = load i32, ptr %nbp`]),
` %eps = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
` %ents = load ptr, ptr %eps`,
` %linear = icmp eq ${sz} %nb, 0`,
` br i1 %linear, label %unbucketed, label %buckets`,
// A map without a bucket table either indexes its keys directly, and
// then holds none outside the index, or scans its entries.
`unbucketed:`,
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
` %nd = load i32, ptr %ndp`,
` %indexed = icmp ne i32 %nd, 0`,
` br i1 %indexed, label %miss, label %filter`,
`filter:`,
` %f1s = lshr i64 %h, 44`,
` %f1a = and i64 %f1s, 63`,
` %f1 = shl i64 1, %f1a`,
` %f2s = lshr i64 %h, 50`,
` %f2a = and i64 %f2s, 63`,
` %f2 = shl i64 1, %f2a`,
` %fb = or i64 %f1, %f2`,
` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`,
` %filter.bits = load i64, ptr %fp`,
` %fm = and i64 %filter.bits, %fb`,
` %fin = icmp eq i64 %fm, %fb`,
` br i1 %fin, label %scan, label %miss`,
// A number-key entry has no cached hash; a dead entry's key matches no key.
`scan:`,
` %nep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`,
...(sz === "i64"
? [` %ne32 = load i32, ptr %nep`, ` %ne = zext i32 %ne32 to i64`]
: [` %ne = load i32, ptr %nep`]),
` br label %scan.loop`,
`scan.loop:`,
` %si = phi ${sz} [ 0, %scan ], [ %si.next, %scan.next ]`,
` %sdone = icmp eq ${sz} %si, %ne`,
` br i1 %sdone, label %miss, label %scan.entry`,
`scan.entry:`,
` %se = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %si`,
` %sk = load i64, ptr %se`,
` %skq = icmp eq i64 %sk, %kb`,
` br i1 %skq, label %scan.found, label %scan.next`,
`scan.found:`,
` ret ptr %se`,
`scan.next:`,
` %si.next = add ${sz} %si, 1`,
` br label %scan.loop`,
`buckets:`,
` %wide = icmp ugt ${sz} %nb, 16777216`,
` br i1 %wide, label %runtime, label %probe`,
`probe:`,
` %bp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 14`,
` %bk = load ptr, ptr %bp`,
` %mask = sub ${sz} %nb, 1`,
` %ht = lshr i64 %h, 32`,
` %ht32 = trunc i64 %ht to i32`,
` %tag = and i32 %ht32, -16777216`,
sz === "i64" ? ` %hi = add i64 %h, 0` : ` %hi = trunc i64 %h to ${sz}`,
` %i0 = and ${sz} %hi, %mask`,
` br label %probe.loop`,
`probe.loop:`,
` %pi = phi ${sz} [ %i0, %probe ], [ %pi.next, %probe.next ]`,
` %bep = getelementptr inbounds i32, ptr %bk, ${sz} %pi`,
` %b = load i32, ptr %bep`,
` %bempty = icmp eq i32 %b, 0`,
` br i1 %bempty, label %miss, label %probe.tag`,
`probe.tag:`,
` %bt = and i32 %b, -16777216`,
` %btq = icmp eq i32 %bt, %tag`,
` br i1 %btq, label %probe.entry, label %probe.next`,
`probe.entry:`,
` %bix = and i32 %b, 16777215`,
` %bix1 = sub i32 %bix, 1`,
sz === "i32" ? ` %bi = add i32 %bix1, 0` : ` %bi = zext i32 %bix1 to ${sz}`,
` %pe = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %bi`,
` %pk = load i64, ptr %pe`,
` %pkq = icmp eq i64 %pk, %kb`,
` br i1 %pkq, label %probe.found, label %probe.next`,
`probe.found:`,
` ret ptr %pe`,
`probe.next:`,
` %pi.inc = add ${sz} %pi, 1`,
` %pi.next = and ${sz} %pi.inc, %mask`,
` br label %probe.loop`,
`miss:`,
` ret ptr null`,
`runtime:`,
` %r = call ptr @scr_map_entry_f64(ptr %m, double ${runtimeKey}) memory(read) nounwind willreturn`,
` ret ptr %r`,
`}`,
``,
];
}
/** The shared abort helpers (emitted only when referenced): the OOM
* abort of untraced shape allocation and the invalid-union-tag abort —
* both print the runtime ABI’s exact message on fd 2 and abort. */
/** The island module tree's install call before %main, and the constant
* that carries its files when the executable ships them itself
* (scr_node_island.c: scr_node_island_install_modules). */
function nodeIslandModulesInstall(
emitter: LlEmitter,
modules: IrNodeIslandModules,
): { call: string; globals: string[] } {
emitter.declare(
`declare void @scr_node_island_install_modules(ptr, ${emitter.sizeType}, ptr, ${emitter.sizeType}, ptr)`,
);
const manifest = nodeIslandManifest(modules);
const manifestPtr = emitter.cstr(manifest);
const location = emitter.cstr(modules.location ?? "");
const data = modules.location === undefined ? emitter.nodeIslandModuleData : null;
const globals =
data === null || data.length === 0
? []
: [
`@sc_node_island_modules = private unnamed_addr constant [${data.length} x i8] c"${llvmBytes(data, false)}", align 16`,
];
const st = emitter.sizeType;
return {
call: ` call void @scr_node_island_install_modules(ptr ${manifestPtr}, ${st} ${Buffer.byteLength(manifest, "utf8")}, ptr ${globals.length > 0 ? "@sc_node_island_modules" : "null"}, ${st} ${globals.length > 0 ? data!.length : 0}, ptr ${location})`,
globals,
};
}
export function helperDefs(emitter: LlEmitter): string[] {
const defs: string[] = [];
const msgHelper = (fnName: string, msgSym: string, msg: string): void => {
// The message routes through the runtime's trap funnel: executable
// builds expand to the historical bytes-on-stderr + abort; library
// builds route to the registered panic sink (scr_runtime.h).
const bytes = Buffer.byteLength(msg, "utf8");
emitter.declare(`declare void @scr_trap(ptr)`);
defs.push(
`@${msgSym} = internal constant [${bytes + 1} x i8] c"${llStrBytes(msg)}"`,
`define internal void @${fnName}() ${FN_ATTRS} {`,
`entry:`,
` call void @scr_trap(ptr @${msgSym})`,
` unreachable`,
`}`,
``,
);
};
if (emitter.needsOom) msgHelper("sc_oom", "sc_oom_msg", "scriptc: out of memory\n");
if (emitter.needsBadTag) {
msgHelper("sc_bad_tag", "sc_bad_tag_msg", "scriptc: internal error: invalid union tag\n");
}
if (emitter.needsBadKey) {
// The keyed-read miss on a result type that cannot say `undefined`:
// trap like an array OOB read instead of corrupting a typed slot
// (SEMANTICS.md; the C helper's message additionally interpolates
// the runtime key — a trap-path debugging nicety, never reachable
// by a program whose behavior matches Node).
msgHelper(
"sc_bad_key",
"sc_bad_key_msg",
"scriptc: TypeError: record has no key (typed slot — no undefined is representable)\n",
);
}
if (emitter.needsRetainBox) {
// scr_box_retain is a static inline (increment-unless-immortal);
// emitted once with internal linkage, like the record retains.
defs.push(
`define internal ptr @sc_retain_box(ptr %b) ${FN_ATTRS} {`,
`entry:`,
` %rc = load ${emitter.sizeType}, ptr %b`,
` %imm = icmp eq ${emitter.sizeType} %rc, -1`,
` br i1 %imm, label %done, label %inc`,
`inc:`,
` %n = add ${emitter.sizeType} %rc, 1`,
` store ${emitter.sizeType} %n, ptr %b`,
` br label %done`,
`done:`,
` ret ptr %b`,
`}`,
``,
);
}
// Number-key Map/Set lookups probe the runtime's direct index inline:
// while ndense is nonzero every live key is an integer below it, so an
// in-range integral key reads its slot (entry index + 1, 0 = absent).
// Any other key goes to sc_map_entry_f64_hashed, which probes a compact
// bucket table as scr_map_find_f64 does, calls the runtime lookup for a
// wide table, and in a map without a bucket table answers absent when the
// map has the index (a map with buckets keeps no index, scr_map.h) and
// scans the entries otherwise. A lookup in a hashed map so tests the
// index once. The probe stays out of line so that the direct-index path
// stays small at every call site, except in the `.loop` flavor
// (numberMapEntryHelpers). Both only read memory, so LLVM merges lookups
// of one key when nothing is stored in between (a has() followed by
// get(), for example).
if (emitter.decls.has(NUMBER_MAP_ENTRY_DECL))
defs.push(
...numberMapEntryHelpers(emitter.sizeType, false),
...numberMapEntryHelpers(emitter.sizeType, true),
);
// Map/Set iteration steps read the entries array inline: an index is
// live when it is below the entry count and its entry's cached hash is
// nonzero (a tombstone's is zero), as scr_map_iter_live decides, or for
// a number-key map when its key is not SCR_MAP_NUM_DEAD. The entry count
// is read again at every step, so entries appended during the iteration
// are visited.
if (emitter.decls.has(MAP_ITER_DECL) || emitter.decls.has(MAP_ITER_NUM_DECL)) {
const sz = emitter.sizeType;
if (emitter.decls.has(MAP_ITER_DECL))
defs.push(
`define internal ptr @sc_map_iter_entry(ptr %m, double %i) ${FN_ATTRS} {`,
`entry:`,
` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`,
` %n = load i32, ptr %np`,
` %nf = uitofp i32 %n to double`,
` %lo = fcmp oge double %i, 0.0`,
` %hi = fcmp olt double %i, %nf`,
` %in = and i1 %lo, %hi`,
` br i1 %in, label %load, label %none`,
`load:`,
` %ix = fptoui double %i to ${sz}`,
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
` %entries = load ptr, ptr %ep`,
` %e = getelementptr inbounds %ScrMapEntry, ptr %entries, ${sz} %ix`,
` %hp = getelementptr inbounds %ScrMapEntry, ptr %e, i32 0, i32 2`,
` %h = load i64, ptr %hp`,
` %live = icmp ne i64 %h, 0`,
` br i1 %live, label %yes, label %none`,
`yes:`,
` ret ptr %e`,
`none:`,
` ret ptr null`,
`}`,
``,
);
if (emitter.decls.has(MAP_ITER_NUM_DECL))
defs.push(
`define internal ptr @sc_map_iter_entry_num(ptr %m, double %i) ${FN_ATTRS} {`,
`entry:`,
` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`,
` %n = load i32, ptr %np`,
` %nf = uitofp i32 %n to double`,
` %lo = fcmp oge double %i, 0.0`,
` %hi = fcmp olt double %i, %nf`,
` %in = and i1 %lo, %hi`,
` br i1 %in, label %load, label %none`,
`load:`,
` %ix = fptoui double %i to ${sz}`,
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
` %entries = load ptr, ptr %ep`,
` %e = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %ix`,
` %key = load i64, ptr %e`,
` %live = icmp ne i64 %key, ${SCR_MAP_NUM_DEAD}`,
` br i1 %live, label %yes, label %none`,
`yes:`,
` ret ptr %e`,
`none:`,
` ret ptr null`,
`}`,
``,
);
defs.push(
`define internal double @sc_map_iter_count(ptr %m) ${FN_ATTRS} {`,
`entry:`,
` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`,
` %n = load i32, ptr %np`,
` %nf = uitofp i32 %n to double`,
` ret double %nf`,
`}`,
``,
);
}
// Map.size and Set.size read the live entry count (scr_map_size).
if (emitter.decls.has(MAP_SIZE_DECL))
defs.push(
`define internal double @sc_map_size(ptr %m) ${FN_ATTRS} {`,
`entry:`,
` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 9`,
` %n = load i32, ptr %np`,
` %nf = uitofp i32 %n to double`,
` ret double %nf`,
`}`,
``,
);
// String-key Map/Set lookups test the map's absent-key filter inline
// (scr_map.c scr_map_filter_bits): a key whose two filter bits are not
// both set is absent, so most misses on small and medium maps return
// without a call. On 64-bit targets the key's cached hash h is the high
// half of its capacity word, the runtime's widened hash is (h << 32) | h,
// and the filter reads its bits 44..55, which are the capacity word's
// own. A key without a cached hash, and every possible hit, take the
// runtime lookup.
// A compact-value map's lookup (sc_map_entry_str_c) tests the same filter
// and calls scr_map_entry_str_c; its iteration step (sc_map_iter_entry_c)
// reads 16-byte entries and their 32-bit hash.
const stringMapEntry = (helper: string, runtime: string): string[] => [
`define internal ptr @${helper}(ptr %m, ptr %k) ${FN_ATTRS} {`,
`entry:`,
...(emitter.sizeType === "i64"
? [
` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`,
` %filter = load i64, ptr %fp`,
` %capp = getelementptr inbounds %ScrStr, ptr %k, i32 0, i32 2`,
` %cap = load i64, ptr %capp`,
` %cached = icmp uge i64 %cap, 4294967296`,
` br i1 %cached, label %test, label %slow`,
`test:`,
` %s1 = lshr i64 %cap, 44`,
` %i1 = and i64 %s1, 63`,
` %b1 = shl i64 1, %i1`,
` %s2 = lshr i64 %cap, 50`,
` %i2 = and i64 %s2, 63`,
` %b2 = shl i64 1, %i2`,
` %bits = or i64 %b1, %b2`,
` %have = and i64 %filter, %bits`,
` %maybe = icmp eq i64 %have, %bits`,
` br i1 %maybe, label %slow, label %absent`,
`absent:`,
` ret ptr null`,
]
: [` br label %slow`]),
`slow:`,
` %r = call ptr @${runtime}(ptr %m, ptr %k)`,
` ret ptr %r`,
`}`,
``,
];
if (emitter.decls.has(STRING_MAP_ENTRY_DECL))
defs.push(...stringMapEntry("sc_map_entry_str", "scr_map_entry_str"));
if (emitter.decls.has(STRING_MAP_ENTRY_C_DECL)) {
const sz = emitter.sizeType;
defs.push(
...stringMapEntry("sc_map_entry_str_c", "scr_map_entry_str_c"),
`define internal ptr @sc_map_iter_entry_c(ptr %m, double %i) ${FN_ATTRS} {`,
`entry:`,
` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`,
` %n = load i32, ptr %np`,
` %nf = uitofp i32 %n to double`,
` %lo = fcmp oge double %i, 0.0`,
` %hi = fcmp olt double %i, %nf`,
` %in = and i1 %lo, %hi`,
` br i1 %in, label %load, label %none`,
`load:`,
` %ix = fptoui double %i to ${sz}`,
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
` %entries = load ptr, ptr %ep`,
` %e = getelementptr inbounds %ScrMapCEntry, ptr %entries, ${sz} %ix`,
` %hp = getelementptr inbounds %ScrMapCEntry, ptr %e, i32 0, i32 1`,
` %h = load i32, ptr %hp`,
` %live = icmp ne i32 %h, 0`,
` br i1 %live, label %yes, label %none`,
`yes:`,
` ret ptr %e`,
`none:`,
` ret ptr null`,
`}`,
``,
);
}
// String comparisons enter through small internal wrappers that LLVM
// inlines at each site: interned literals and shared values compare by
// identity, and equality rejects different byte lengths, before any
// runtime call (scr_str_eq/scr_str_cmp keep the complete semantics).
if (emitter.decls.has(`declare zeroext i1 @scr_str_eq(ptr, ptr)`)) {
defs.push(
`define internal zeroext i1 @sc_str_eq(ptr %a, ptr %b) ${FN_ATTRS} {`,
`entry:`,
` %same = icmp eq ptr %a, %b`,
` br i1 %same, label %yes, label %lengths`,
`lengths:`,
` %alen.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 1`,
` %blen.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 1`,
` %alen = load ${emitter.sizeType}, ptr %alen.p`,
` %blen = load ${emitter.sizeType}, ptr %blen.p`,
` %samelen = icmp eq ${emitter.sizeType} %alen, %blen`,
` br i1 %samelen, label %bytes, label %no`,
`bytes:`,
` %eq = call zeroext i1 @scr_str_eq(ptr %a, ptr %b)`,
` ret i1 %eq`,
`yes:`,
` ret i1 true`,
`no:`,
` ret i1 false`,
`}`,
``,
);
}
for (const [text, name] of emitter.literalEqualities) {
defs.push(
...literalEqualityDefinition(
name,
wtf8Bytes(text),
emitter.internLiteral(text),
emitter.sizeType,
FN_ATTRS,
),
``,
);
}
// String length and charCodeAt read proven-ASCII strings inline: bit 31
// of the capacity word (SCR_STR_ASCII_BIT, the low half's sign bit on
// little-endian targets) makes UTF-16 units bytes. Unproven strings and
// anything but an in-range index take the runtime's UTF-16 mapping.
// A scan's receiver reads inline too (ScrStrScan): from ordinary global
// storage, or, in worker executables, from the thread's static TLS block,
// one thread-pointer-relative load (see executableTls).
const scanInline = stringScanInline(emitter);
let scanDeclared = false;
for (const helper of STRING_READ_HELPERS) {
if (!emitter.decls.has(helper.decl)) continue;
if (scanInline && !scanDeclared) {
defs.push(stringScanDecl(emitter.sizeType, emitter.executableTls), ``);
scanDeclared = true;
}
defs.push(...helper.define(emitter.sizeType, FN_ATTRS, scanInline), ``);
}
// Ordering decides inline from the first differing bytes. The bytes
// before them are equal, so both strings are at the same position of a
// character: either both bytes continue characters with the same lead
// byte, or both start a character. Byte order is the UTF-16 order for
// two continuation bytes, and for two lead bytes when one starts a
// character below U+D000, whose UTF-16 unit is below every unit of a
// character with a lead byte of at least 0xED. Between lead bytes of at
// least 0xED, a supplementary character (0xF0..0xF4, a high surrogate
// unit) orders below U+E000..U+FFFF (0xEE, 0xEF), and a lead byte of
// 0xED (U+D000..U+D7FF or a lone surrogate) takes the runtime.
// Differing words without a byte of at least 0xED decide in byte
// order; other differing words decide by their first differing byte,
// and strings shorter than a word by their first byte. Otherwise a
// proven-ASCII string on either side orders by bytes (scr_str_cmp), and
// other pairs call scr_str_cmp_u16_bytes. Both only read the two strings,
// so LLVM can merge repeated comparisons of one pair. Callers
// only test the sign of the result.
if (emitter.decls.has(`declare i32 @scr_str_cmp_u16(ptr, ptr)`)) {
const fn = "scr_str_cmp_u16";
emitter.declare("declare i64 @llvm.bswap.i64(i64)");
emitter.declare("declare i64 @llvm.cttz.i64(i64, i1 immarg)");
emitter.declare("declare i8 @llvm.umin.i8(i8, i8)");
emitter.declare("declare i32 @scr_str_cmp(ptr, ptr)");
emitter.declare("declare i32 @scr_str_cmp_u16_bytes(ptr, ptr)");
defs.push(
`define internal i32 @${fn.replace(/^scr_/, "sc_")}(ptr %a, ptr %b) ${FN_ATTRS} {`,
`entry:`,
` %same = icmp eq ptr %a, %b`,
` br i1 %same, label %equal, label %lengths`,
`lengths:`,
` %alen.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 1`,
` %blen.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 1`,
` %alen = load ${emitter.sizeType}, ptr %alen.p`,
` %blen = load ${emitter.sizeType}, ptr %blen.p`,
` %aempty = icmp eq ${emitter.sizeType} %alen, 0`,
` %bempty = icmp eq ${emitter.sizeType} %blen, 0`,
` %empty = or i1 %aempty, %bempty`,
` br i1 %empty, label %order, label %words`,
// Both at least 8 bytes: the first word of each.
`words:`,
` %along = icmp uge ${emitter.sizeType} %alen, 8`,
` %blong = icmp uge ${emitter.sizeType} %blen, 8`,
` %long = and i1 %along, %blong`,
` br i1 %long, label %word, label %first`,
`word:`,
` %awp = getelementptr inbounds %ScrStr, ptr %a, i32 1`,
` %bwp = getelementptr inbounds %ScrStr, ptr %b, i32 1`,
` %aw = load i64, ptr %awp, align 1`,
` %bw = load i64, ptr %bwp, align 1`,
` %wdiffer = icmp ne i64 %aw, %bw`,
` br i1 %wdiffer, label %differ, label %wsame`,
// Equal first words and one length of at most 16 bytes: the word
// ending at that length covers the rest, and any difference lies
// past the equal first word.
`wsame:`,
` %samelen = icmp eq ${emitter.sizeType} %alen, %blen`,
` %short = icmp ule ${emitter.sizeType} %alen, 16`,
` %tailable = and i1 %samelen, %short`,
` br i1 %tailable, label %tail, label %order`,
`tail:`,
` %toff = sub ${emitter.sizeType} %alen, 8`,
` %atp = getelementptr inbounds i8, ptr %awp, ${emitter.sizeType} %toff`,
` %btp = getelementptr inbounds i8, ptr %bwp, ${emitter.sizeType} %toff`,
` %at = load i64, ptr %atp, align 1`,
` %bt = load i64, ptr %btp, align 1`,
` %tdiffer = icmp ne i64 %at, %bt`,
` br i1 %tdiffer, label %differ, label %equal`,
`first:`,
` %adata = getelementptr inbounds %ScrStr, ptr %a, i32 1`,
` %bdata = getelementptr inbounds %ScrStr, ptr %b, i32 1`,
` %x = load i8, ptr %adata`,
` %y = load i8, ptr %bdata`,
` %fdiffer = icmp ne i8 %x, %y`,
` br i1 %fdiffer, label %bytes, label %order`,
// Two differing words. When neither has a byte of at least 0xED
// (a byte b is when its high bit and bit 7 of (b & 0x7f) + 0x13 are
// set), byte-swapped little-endian words compare in byte order.
// Otherwise the lowest set byte of their xor is the first differing
// byte.
`differ:`,
` %dx = phi i64 [ %aw, %word ], [ %at, %tail ]`,
` %dy = phi i64 [ %bw, %word ], [ %bt, %tail ]`,
` %xl = and i64 %dx, 9187201950435737471`,
` %yl = and i64 %dy, 9187201950435737471`,
` %xa = add i64 %xl, 1374463283923456787`,
` %ya = add i64 %yl, 1374463283923456787`,
` %xh = and i64 %dx, %xa`,
` %yh = and i64 %dy, %ya`,
` %hh = or i64 %xh, %yh`,
` %whigh = and i64 %hh, -9187201950435737472`,
` %dlow = icmp eq i64 %whigh, 0`,
` br i1 %dlow, label %wordorder, label %locate`,
`wordorder:`,
` %xsw = call i64 @llvm.bswap.i64(i64 %dx)`,
` %ysw = call i64 @llvm.bswap.i64(i64 %dy)`,
` %wbelow = icmp ult i64 %xsw, %ysw`,
` %wsign = select i1 %wbelow, i32 -1, i32 1`,
` ret i32 %wsign`,
`locate:`,
` %dxor = xor i64 %dx, %dy`,
` %dtz = call i64 @llvm.cttz.i64(i64 %dxor, i1 true)`,
` %dshift = and i64 %dtz, 56`,
` %dxs = lshr i64 %dx, %dshift`,
` %dys = lshr i64 %dy, %dshift`,
` %dxb = trunc i64 %dxs to i8`,
` %dyb = trunc i64 %dys to i8`,
` br label %bytes`,
// The first differing bytes.
`bytes:`,
` %bx = phi i8 [ %x, %first ], [ %dxb, %locate ]`,
` %by = phi i8 [ %y, %first ], [ %dyb, %locate ]`,
` %bmin = call i8 @llvm.umin.i8(i8 %bx, i8 %by)`,
` %blow = icmp ult i8 %bmin, -19`,
` br i1 %blow, label %byte, label %high`,
// Both at least 0xED. A lone surrogate (0xED) takes the runtime;
// supplementary lead bytes (0xF0..0xF4) order below 0xEE and 0xEF
// once 0x10 is subtracted from them.
`high:`,
` %xed = icmp eq i8 %bx, -19`,
` %yed = icmp eq i8 %by, -19`,
` %ed = or i1 %xed, %yed`,
` br i1 %ed, label %order, label %planes`,
`planes:`,
` %xsupp = icmp uge i8 %bx, -16`,
` %ysupp = icmp uge i8 %by, -16`,
` %xlow = add i8 %bx, -16`,
` %ylow = add i8 %by, -16`,
` %xk = select i1 %xsupp, i8 %xlow, i8 %bx`,
` %yk = select i1 %ysupp, i8 %ylow, i8 %by`,
` %kbelow = icmp ult i8 %xk, %yk`,
` %ksign = select i1 %kbelow, i32 -1, i32 1`,
` ret i32 %ksign`,
`byte:`,
` %below = icmp ult i8 %bx, %by`,
` %sign = select i1 %below, i32 -1, i32 1`,
` ret i32 %sign`,
`order:`,
` %acw.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 2`,
` %bcw.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 2`,
` %acw = load i32, ptr %acw.p`,
` %bcw = load i32, ptr %bcw.p`,
` %cws = or i32 %acw, %bcw`,
` %anyascii = icmp slt i32 %cws, 0`,
` br i1 %anyascii, label %memorder, label %units`,
`memorder:`,
` %m = call i32 @scr_str_cmp(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`,
` ret i32 %m`,
`units:`,
` %c = call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`,
` ret i32 %c`,
`equal:`,
` ret i32 0`,
`}`,
``,
);
}
// The declarations these helpers added must land in the extern block,
// which already flushed — append here instead (LLVM is order-free).
return defs.length > 0 ? [...defs] : defs;
}
/** LIBRARY mode: the profile-declared external definitions — the
* export-map wrappers plus init / sink-registration / reset / collect.
* Plain `define` (not `define internal`) — the exact linkage distinction
* that separates the executable lane's @main from everything else. The
* bodies delegate every runtime half to scr_library.c, mirroring the C
* emission line for line, so the two lanes are identical by
* construction. */
export function emitLibDefs(
emitter: LlEmitter,
globals: IrGlobal[],
globalReleaseLines: (prefix: string) => string[],
stamps: string[],
): string[] {
const lib = emitter.mod.lib!;
const autoReset = lib.resultResetSymbol === null;
const out: string[] = [``, `; ── library-mode entries (profile: ${lib.profileName}) ──`, ``];
// Every entry's prologue records its external symbol in the funnel's
// current-entry slot (structured trap-teaching field 2); the symbols
// live as internal constants, one per entry.
const symConst = (sym: string): string => `@sc_lib_sym_${sym}`;
const emitSymConst = (sym: string): void => {
out.push(
`${symConst(sym)} = internal constant [${Buffer.byteLength(sym, "utf8") + 1} x i8] c"${llStrBytes(sym)}"`,
);
};
emitSymConst(lib.initSymbol);
emitSymConst(lib.sinkRegisterSymbol);
if (lib.callbackRegisterSymbol !== null && lib.callbackRegisterSymbol !== undefined)
emitSymConst(lib.callbackRegisterSymbol);
if (lib.resultResetSymbol !== null) emitSymConst(lib.resultResetSymbol);
if (lib.collectSymbol !== null) emitSymConst(lib.collectSymbol);
for (const e of lib.exports) emitSymConst(e.symbol);
out.push(``);
if (emitter.wasi) {
for (const symbol of ["scriptc_alloc", "scriptc_free"]) emitSymConst(symbol);
out.push(
`define ptr @scriptc_alloc(i32 %size) ${FN_ATTRS} {`,
`entry:`,
` call void @scr_library_entry(i1 zeroext false, ptr ${symConst("scriptc_alloc")})`,
` %p = call ptr @malloc(i32 %size)`,
` ret ptr %p`,
`}`,
``,
`define void @scriptc_free(ptr %p) ${FN_ATTRS} {`,
`entry:`,
` call void @scr_library_entry(i1 zeroext false, ptr ${symConst("scriptc_free")})`,
` call void @free(ptr %p)`,
` ret void`,
`}`,
``,
);
}
// The runtime detected-trap overlay table (scr_runtime.h declares it,
// the library trap funnel consults it): flat code/teaching/remediation
// triples, one per runtime trap code (SC4013–SC4019) the profile
// declares text for. The funnel assembles the structured sink message.
// The empty table still defines the symbols the funnel links against.
const ovlCells: string[] = [];
lib.trapOverlays.forEach((o, i) => {
const cell = (name: string, text: string | undefined): void => {
if (text === undefined) {
ovlCells.push("ptr null");
return;
}
const sym = `@sc_lib_ovl_${i}_${name}`;
out.push(
`${sym} = internal constant [${Buffer.byteLength(text, "utf8") + 1} x i8] c"${llStrBytes(text)}"`,
);
ovlCells.push(`ptr ${sym}`);
};
cell("code", o.code);
cell("teach", o.teaching);
cell("rem", o.remediation);
});
out.push(
ovlCells.length === 0
? `@scr_library_trap_overlays = constant [1 x ptr] zeroinitializer`
: `@scr_library_trap_overlays = constant [${ovlCells.length} x ptr] [${ovlCells.join(", ")}]`,
`@scr_library_trap_overlays_len = constant ${emitter.sizeType} ${lib.trapOverlays.length}`,
``,
);
// The init entry: full deterministic reset-and-reevaluate. Program
// globals release and zero first (run-once guards included), then the
// runtime session reset, the error-vt interval stamps verbatim from
// the executable main, %main itself, and the escaped-exception check.
const zeroStores = globals.map((g) => {
const ty = emitter.llType(g.type);
const zero = ty === "double" ? f64Lit(0) : ty === "ptr" ? "null" : "false";
return ` store ${ty} ${zero}, ptr @${mangleGlobal(g.id)} ; ${g.name}`;
});
out.push(
`define void @${lib.initSymbol}() ${FN_ATTRS} {`,
`entry:`,
` call void @scr_library_entry(i1 zeroext true, ptr ${symConst(lib.initSymbol)}) ; init always resets the result arena`,
...globalReleaseLines("ci"),
...zeroStores,
` call void @scr_library_reset()`,
...stamps,
` call void @${mangleFunction(emitter.mod.entry)}()`,
` call void @scr_library_check_exc()`,
` ret void`,
`}`,
``,
`define void @${lib.sinkRegisterSymbol}(ptr %fn, ptr %ctx) ${FN_ATTRS} {`,
`entry:`,
` call void @scr_library_callback_entry_guard(ptr ${symConst(lib.sinkRegisterSymbol)})`,
` call void @scr_library_set_sink(ptr %fn, ptr %ctx)`,
` ret void`,
`}`,
``,
);
if (lib.callbacks !== undefined && lib.callbacks.length > 0) {
// Host-callback channels: the per-channel name constants (the
// registration dispatch's strcmp operands), the per-channel
// unregistered-call trap constants (the ffiCall sites'
// scr_library_cb_require operands), and the registration function:
// a pure store dispatch
// (the sink registration's rule — no entry prologue, no poison
// guard) whose first operation rejects callback-time re-entry
// (SC4026). An unknown or NULL name is a defined -1, never a store.
for (const cb of lib.callbacks) {
out.push(
`@sc_lib_cb_name_${cb.slot} = internal constant [${Buffer.byteLength(cb.name, "utf8") + 1} x i8] c"${llStrBytes(cb.name)}"`,
`@sc_lib_cb_trap_${cb.slot} = internal constant [${Buffer.byteLength(cb.unregisteredTrap, "utf8") + 1} x i8] c"${llStrBytes(cb.unregisteredTrap)}"`,
);
}
out.push(
``,
`define i32 @${lib.callbackRegisterSymbol}(ptr %name, ptr %fn, ptr %ctx) ${FN_ATTRS} {`,
`entry:`,
` call void @scr_library_callback_entry_guard(ptr ${symConst(lib.callbackRegisterSymbol!)})`,
` %isnull = icmp eq ptr %name, null`,
` br i1 %isnull, label %miss, label %try0`,
);
lib.callbacks.forEach((cb, i) => {
const next = i + 1 < lib.callbacks!.length ? `try${i + 1}` : "miss";
out.push(
`try${i}: ; channel '${cb.name}'`,
` %cmp${i} = call i32 @strcmp(ptr %name, ptr @sc_lib_cb_name_${cb.slot})`,
` %eq${i} = icmp eq i32 %cmp${i}, 0`,
` br i1 %eq${i}, label %set${i}, label %${next}`,
`set${i}:`,
` call void @scr_library_cb_set(${emitter.sizeType} ${cb.slot}, ptr %fn, ptr %ctx)`,
` ret i32 0`,
);
});
out.push(`miss:`, ` ret i32 -1`, `}`, ``);
}
if (lib.identity !== undefined && emitter.emitLibraryIdentity) {
// Profile-declared identity getters (the ask-2 sidecar's boot-time
// pairing fence): pure data returns with NO entry prologue — exempt
// from the poisoned guard and every runtime touch (ratified), so a
// host can read them before init and after a trap. The u64 rides
// i64 two's-complement (LLVM integer constants are signed).
for (const line of emitLibraryIdentityLines(lib.identity, FN_ATTRS)) out.push(line);
}
if (lib.resultResetSymbol !== null) {
out.push(
`define void @${lib.resultResetSymbol}() ${FN_ATTRS} {`,
`entry:`,
` call void @scr_library_entry(i1 zeroext false, ptr ${symConst(lib.resultResetSymbol)})`,
` call void @scr_library_arena_reset()`,
` ret void`,
`}`,
``,
);
}
if (lib.collectSymbol !== null) {
out.push(
`define void @${lib.collectSymbol}() ${FN_ATTRS} {`,
`entry:`,
` call void @scr_library_entry(i1 zeroext false, ptr ${symConst(lib.collectSymbol)})`,
` call void @scr_library_collect() ; arena reset + a full cycle collection`,
` ret void`,
`}`,
``,
);
}
for (const e of lib.exports) {
const params: string[] = [];
const body: string[] = [
` call void @scr_library_entry(i1 zeroext ${autoReset ? "true" : "false"}, ptr ${symConst(e.symbol)})`,
];
const args: string[] = [];
if (e.inboundBytesTrap !== undefined) {
// The bytes-in helper's trap message: the compiler-assembled
// structured trap-teaching form (0x01 text 0x1F SC4012 0x1F symbol
// [0x1F remediation]) consumed by the runtime sink.
const trapBytes = Buffer.byteLength(e.inboundBytesTrap, "utf8");
out.push(
`@sc_lib_bytes_trap_${e.symbol} = internal constant [${trapBytes + 1} x i8] c"${llStrBytes(e.inboundBytesTrap)}"`,
``,
);
}
if (e.inboundIntTrap !== undefined) {
// The i64/u64-in helpers' host-contract trap message (ask 4): an
// inbound integer past ±(2^53−1) cannot ride f64 exactly. Same
// assembled structured form, same SC4012 code, same
// emission-invariance argument as the bytes trap.
const trapBytes = Buffer.byteLength(e.inboundIntTrap, "utf8");
out.push(
`@sc_lib_int_trap_${e.symbol} = internal constant [${trapBytes + 1} x i8] c"${llStrBytes(e.inboundIntTrap)}"`,
``,
);
}
if (e.inboundCstringTrap !== undefined) {
// The cstring-in helper's host-contract trap message (a NULL
// `const char *`): same assembled structured form and SC4012 code.
const trapBytes = Buffer.byteLength(e.inboundCstringTrap, "utf8");
out.push(
`@sc_lib_cstr_trap_${e.symbol} = internal constant [${trapBytes + 1} x i8] c"${llStrBytes(e.inboundCstringTrap)}"`,
``,
);
}
e.params.forEach((cls, i) => {
switch (cls) {
case "f64":
params.push(`double %a${i}`);
args.push(`double %a${i}`);
break;
case "bool":
params.push(`i8 %a${i}`);
body.push(` %c${i} = icmp ne i8 %a${i}, 0`);
args.push(`i1 %c${i}`);
break;
case "u8":
params.push(`i8 %a${i}`);
body.push(` %c${i} = uitofp i8 %a${i} to double`);
args.push(`double %c${i}`);
break;
case "u32":
params.push(`i32 %a${i}`);
body.push(` %c${i} = uitofp i32 %a${i} to double`);
args.push(`double %c${i}`);
break;
case "i32":
params.push(`i32 %a${i}`);
body.push(` %c${i} = sitofp i32 %a${i} to double`);
args.push(`double %c${i}`);
break;
case "i64":
// Inbound declared-integer edge (ask 4): the helper converts
// exactly or delivers the host-contract trap (past ±(2^53−1)
// the value cannot ride f64 without rounding).
params.push(`i64 %a${i}`);
body.push(
` %c${i} = call double @scr_library_i64_in(i64 %a${i}, ptr @sc_lib_int_trap_${e.symbol})`,
);
args.push(`double %c${i}`);
break;
case "u64":
params.push(`i64 %a${i}`);
body.push(
` %c${i} = call double @scr_library_u64_in(i64 %a${i}, ptr @sc_lib_int_trap_${e.symbol})`,
);
args.push(`double %c${i}`);
break;
case "string":
params.push(`ptr %a${i}_ptr`, `${emitter.sizeType} %a${i}_len`);
body.push(
` %c${i} = call ptr @scr_library_str_in(ptr %a${i}_ptr, ${emitter.sizeType} %a${i}_len)`,
);
args.push(`ptr %c${i}`);
break;
case "bytes":
params.push(`ptr %a${i}_ptr`, `${emitter.sizeType} %a${i}_len`);
body.push(
` %c${i} = call ptr @scr_library_bytes_in(ptr %a${i}_ptr, ${emitter.sizeType} %a${i}_len, ptr @sc_lib_bytes_trap_${e.symbol})`,
);
args.push(`ptr %c${i}`);
break;
case "cstring":
params.push(`ptr %a${i}`);
body.push(
` %c${i} = call ptr @scr_library_cstr_in(ptr %a${i}, ptr @sc_lib_cstr_trap_${e.symbol})`,
);
args.push(`ptr %c${i}`);
break;
}
});
if (e.returns === "string" || e.returns === "bytes") {
params.push(`ptr %out`, `ptr %out_len`);
}
const target = `@${mangleFunction(e.fnName)}`;
const callArgs = args.join(", ");
let retType = "void";
switch (e.returns) {
case "void":
body.push(
` call void ${target}(${callArgs})`,
` call void @scr_library_check_exc()`,
` ret void`,
);
break;
case "f64":
retType = "double";
body.push(
` %r = call double ${target}(${callArgs})`,
` call void @scr_library_check_exc()`,
` ret double %r`,
);
break;
case "i64":
case "u64":
// The outbound declared-integer edge (ask 4): every value
// reaching this return was PROVEN whole and inside the class's
// range at compile time, so the fp-to-int conversion is exact
// by construction — the crossing carries the mathematically
// exact integer the f64 held.
retType = "i64";
body.push(
` %r = call double ${target}(${callArgs})`,
` call void @scr_library_check_exc()`,
` %z = ${e.returns === "i64" ? "fptosi" : "fptoui"} double %r to i64`,
` ret i64 %z`,
);
break;
case "bool":
retType = "i8";
body.push(
` %r = call i1 ${target}(${callArgs})`,
` call void @scr_library_check_exc()`,
` %z = zext i1 %r to i8`,
` ret i8 %z`,
);
break;
case "string":
body.push(
` %r = call ptr ${target}(${callArgs})`,
` call void @scr_library_check_exc()`,
` call void @scr_library_str_out(ptr %r, ptr %out, ptr %out_len)`,
` ret void`,
);
break;
case "bytes":
body.push(
` %r = call ptr ${target}(${callArgs})`,
` call void @scr_library_check_exc()`,
` call void @scr_library_bytes_out(ptr %r, ptr %out, ptr %out_len)`,
` ret void`,
);
break;
case "cstring":
// The result's arena copy, returned as `const char *`.
retType = "ptr";
body.push(
` %r = call ptr ${target}(${callArgs})`,
` call void @scr_library_check_exc()`,
` %z = call ptr @scr_library_cstr_out(ptr %r)`,
` ret ptr %z`,
);
break;
}
out.push(
`define ${retType} @${e.symbol}(${params.join(", ")}) ${FN_ATTRS} { ; library export ${e.fnName}`,
`entry:`,
...body,
`}`,
``,
);
}
return out;
}
/** C-callable scalar callback trampolines. A callback with an explicit
* context entry receives the closure at that exact ABI position. A raw
* callback loads it from a call-scoped TLS slot installed around the
* outer native call. */
export function emitFfiCallbackDefs(emitter: LlEmitter): { globals: string[]; defs: string[] } {
const globals: string[] = [];
const defs: string[] = [];
if (emitter.ffiCallbackAdapters.size === 0) return { globals, defs };
emitter.declare(`declare void @scr_trap(ptr)`);
const expired = emitter.cstr(
"scriptc: native callback invoked outside its call-scoped lifetime\n",
);
const released = emitter.cstr("scriptc: native callback invoked outside its retained lifetime\n");
for (const adapter of emitter.ffiCallbackAdapters.values()) {
const cb = adapter.callback;
if (adapter.tls !== null)
globals.push(`@${adapter.tls} = internal thread_local global ptr null`);
if (adapter.global !== null)
globals.push(`@${adapter.global} = internal thread_local global ptr null`);
if (adapter.table !== null) {
globals.push(
`@${adapter.table} = internal ${emitter.mod.workers ? "thread_local " : ""}global %ScrFfiTable zeroinitializer`,
);
}
const params = cb.params.flatMap((param, i): string[] => {
if (isFfiContextParam(param)) return [`ptr %ctx`];
if (param === "string" || param === "bytes") {
return [`ptr %a${i}`, `${emitter.sizeType} %a${i}_len`];
}
return [`${ffiNativeParamLl(param, emitter.ffiExtendNarrowIntegers)} %a${i}`];
});
if (cb.invoke === "foreign") {
if (
adapter.table === null ||
!cb.params.some(isFfiContextParam) ||
cb.returns !== "void" ||
cb.params.some((param) => param === "i64" || param === "u64" || param === "pointer")
) {
throw new InternalCompilerError(
"llvm emitter bug: invalid foreign FFI callback descriptor",
);
}
const dispatch = `${adapter.symbol}_dispatch`;
const scriptArgs: string[] = [];
const dispatchBody: string[] = [
`define internal void @${dispatch}(ptr %cb, ptr %call) ${FN_ATTRS} {`,
`entry:`,
` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`,
` %fn = load ptr, ptr %fnp`,
];
for (let i = 0; i < cb.params.length; i++) {
const param = cb.params[i]!;
if (isFfiContextParam(param)) continue;
switch (param) {
case "f64":
case "f32":
case "i8":
case "u16":
case "i16":
emitter.declare(`declare double @scr_ffi_call_get_f64(ptr, ${emitter.sizeType})`);
dispatchBody.push(
` %s${i} = call double @scr_ffi_call_get_f64(ptr %call, ${emitter.sizeType} ${i})`,
);
scriptArgs.push(`double %s${i}`);
break;
case "bool":
emitter.declare(`declare zeroext i1 @scr_ffi_call_get_bool(ptr, ${emitter.sizeType})`);
dispatchBody.push(
` %s${i} = call zeroext i1 @scr_ffi_call_get_bool(ptr %call, ${emitter.sizeType} ${i})`,
);
scriptArgs.push(`i1 %s${i}`);
break;
case "u8":
case "u32":
case "i32":
emitter.declare(`declare double @scr_ffi_call_get_${param}(ptr, ${emitter.sizeType})`);
dispatchBody.push(
` %s${i} = call double @scr_ffi_call_get_${param}(ptr %call, ${emitter.sizeType} ${i})`,
);
scriptArgs.push(`double %s${i}`);
break;
case "cstring":
case "string":
case "bytes": {
emitter.declare(`declare ptr @scr_ffi_call_get_data(ptr, ${emitter.sizeType})`);
emitter.declare(
`declare ${emitter.sizeType} @scr_ffi_call_get_len(ptr, ${emitter.sizeType})`,
);
dispatchBody.push(
` %data${i} = call ptr @scr_ffi_call_get_data(ptr %call, ${emitter.sizeType} ${i})`,
` %len${i} = call ${emitter.sizeType} @scr_ffi_call_get_len(ptr %call, ${emitter.sizeType} ${i})`,
);
if (param === "bytes") {
emitter.declare(`declare ptr @scr_bytes_from_data(ptr, ${emitter.sizeType})`);
dispatchBody.push(
` %s${i} = call ptr @scr_bytes_from_data(ptr %data${i}, ${emitter.sizeType} %len${i})`,
);
} else {
emitter.declare(`declare ptr @scr_str_from_utf8_lossy(ptr, ${emitter.sizeType})`);
dispatchBody.push(
` %s${i} = call ptr @scr_str_from_utf8_lossy(ptr %data${i}, ${emitter.sizeType} %len${i})`,
);
}
scriptArgs.push(`ptr %s${i}`);
break;
}
}
}
dispatchBody.push(
` call void %fn(${[`ptr %cb`, ...scriptArgs].join(", ")})`,
` ret void`,
`}`,
``,
);
for (const line of dispatchBody) defs.push(line);
emitter.declare(`declare ptr @scr_ffi_call_new(ptr, ptr, ptr, ${emitter.sizeType})`);
emitter.declare(`declare void @scr_ffi_post(ptr)`);
defs.push(
`define internal void @${adapter.symbol}(${params.join(", ")}) ${FN_ATTRS} {`,
`entry:`,
` %cb = getelementptr inbounds i8, ptr %ctx, i64 0`,
` %call = call ptr @scr_ffi_call_new(ptr @${adapter.table}, ptr %cb, ptr @${dispatch}, ${emitter.sizeType} ${cb.params.length})`,
);
for (let i = 0; i < cb.params.length; i++) {
const param = cb.params[i]!;
if (isFfiContextParam(param)) continue;
if (param === "cstring") {
emitter.declare(`declare void @scr_ffi_call_copy_cstring(ptr, ${emitter.sizeType}, ptr)`);
defs.push(
` call void @scr_ffi_call_copy_cstring(ptr %call, ${emitter.sizeType} ${i}, ptr %a${i})`,
);
} else if (param === "string" || param === "bytes") {
emitter.declare(
`declare void @scr_ffi_call_copy_${param}(ptr, ${emitter.sizeType}, ptr, ${emitter.sizeType})`,
);
defs.push(
` call void @scr_ffi_call_copy_${param}(ptr %call, ${emitter.sizeType} ${i}, ptr %a${i}, ${emitter.sizeType} %a${i}_len)`,
);
} else if (param === "f32" || param === "i8" || param === "u16" || param === "i16") {
const nativeTy = ffiNativeTypeLl(param);
const op = param === "f32" ? "fpext" : param === "u16" ? "uitofp" : "sitofp";
emitter.declare(`declare void @scr_ffi_call_set_f64(ptr, ${emitter.sizeType}, double)`);
defs.push(
` %wide${i} = ${op} ${nativeTy} %a${i} to double`,
` call void @scr_ffi_call_set_f64(ptr %call, ${emitter.sizeType} ${i}, double %wide${i})`,
);
} else {
const nativeTy = ffiNativeTypeLl(param);
emitter.declare(
`declare void @scr_ffi_call_set_${param}(ptr, ${emitter.sizeType}, ${nativeTy})`,
);
defs.push(
` call void @scr_ffi_call_set_${param}(ptr %call, ${emitter.sizeType} ${i}, ${nativeTy} %a${i})`,
);
}
}
defs.push(` call void @scr_ffi_post(ptr %call)`, ` ret void`, `}`, ``);
continue;
}
defs.push(
`define internal ${ffiNativeReturnLl(cb.returns, emitter.ffiExtendNarrowIntegers)} @${adapter.symbol}(${params.join(", ")}) ${FN_ATTRS} {`,
`entry:`,
adapter.tls !== null
? ` %cb = load ptr, ptr @${adapter.tls}`
: adapter.global !== null
? ` %cb = load ptr, ptr @${adapter.global}`
: ` %cb = getelementptr inbounds i8, ptr %ctx, i64 0`,
` %missing = icmp eq ptr %cb, null`,
` br i1 %missing, label %expired, label %ready`,
`expired:`,
` call void @scr_trap(ptr ${adapter.callback.lifetime === "call" ? expired : released})`,
` unreachable`,
`ready:`,
...emitter.pendingTestLines("%pending"),
` br i1 %pending, label %skip, label %invoke`,
`skip:`,
` ret ${ffiCallbackDummyLl(cb)}`,
`invoke:`,
);
// Validate every native pointer before allocating any copy-in value.
// A later bad slot therefore cannot leak an earlier materialization.
for (let i = 0; i < cb.params.length; i++) {
const param = cb.params[i]!;
if (param !== "cstring" && param !== "string" && param !== "bytes") continue;
const invalid = `%invalid${i}`;
defs.push(` %null${i} = icmp eq ptr %a${i}, null`);
if (param === "cstring") {
defs.push(` ${invalid} = or i1 %null${i}, false`);
} else {
defs.push(
` %nonempty${i} = icmp ne ${emitter.sizeType} %a${i}_len, 0`,
` ${invalid} = and i1 %null${i}, %nonempty${i}`,
);
}
const message =
param === "cstring"
? "scriptc: native callback passed a NULL cstring\n"
: `scriptc: native callback passed a NULL ${param} span with nonzero length\n`;
defs.push(
` br i1 ${invalid}, label %invalid_param${i}, label %param_ok${i}`,
`invalid_param${i}:`,
` call void @scr_trap(ptr ${emitter.cstr(message)})`,
` unreachable`,
`param_ok${i}:`,
);
}
defs.push(
` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`,
` %fn = load ptr, ptr %fnp`,
);
const scriptArgs: string[] = [];
for (let i = 0; i < cb.params.length; i++) {
const param = cb.params[i]!;
if (isFfiContextParam(param)) continue;
switch (param) {
case "i64":
case "u64":
case "pointer": {
const ty = ffiNativeTypeLl(param);
emitter.declare(`declare ptr @scr_bigint_from_${param}(${ty})`);
defs.push(` %s${i} = call ptr @scr_bigint_from_${param}(${ty} %a${i})`);
scriptArgs.push(`ptr %s${i}`);
break;
}
case "f64":
scriptArgs.push(`double %a${i}`);
break;
case "f32":
case "i8":
case "u16":
case "i16": {
const nativeTy = ffiNativeTypeLl(param);
const op = param === "f32" ? "fpext" : param === "u16" ? "uitofp" : "sitofp";
defs.push(` %s${i} = ${op} ${nativeTy} %a${i} to double`);
scriptArgs.push(`double %s${i}`);
break;
}
case "bool":
defs.push(` %s${i} = icmp ne i8 %a${i}, 0`);
scriptArgs.push(`i1 %s${i}`);
break;
case "u8":
defs.push(` %s${i} = uitofp i8 %a${i} to double`);
scriptArgs.push(`double %s${i}`);
break;
case "u32":
defs.push(` %s${i} = uitofp i32 %a${i} to double`);
scriptArgs.push(`double %s${i}`);
break;
case "i32":
defs.push(` %s${i} = sitofp i32 %a${i} to double`);
scriptArgs.push(`double %s${i}`);
break;
case "cstring":
emitter.declare(`declare ${emitter.sizeType} @strlen(ptr)`);
emitter.declare(`declare ptr @scr_str_from_utf8_lossy(ptr, ${emitter.sizeType})`);
defs.push(
` %len${i} = call ${emitter.sizeType} @strlen(ptr %a${i})`,
` %s${i} = call ptr @scr_str_from_utf8_lossy(ptr %a${i}, ${emitter.sizeType} %len${i})`,
);
scriptArgs.push(`ptr %s${i}`);
break;
case "string":
emitter.declare(`declare ptr @scr_str_from_utf8_lossy(ptr, ${emitter.sizeType})`);
defs.push(
` %s${i} = call ptr @scr_str_from_utf8_lossy(ptr %a${i}, ${emitter.sizeType} %a${i}_len)`,
);
scriptArgs.push(`ptr %s${i}`);
break;
case "bytes":
emitter.declare(`declare ptr @scr_bytes_from_data(ptr, ${emitter.sizeType})`);
defs.push(
` %s${i} = call ptr @scr_bytes_from_data(ptr %a${i}, ${emitter.sizeType} %a${i}_len)`,
);
scriptArgs.push(`ptr %s${i}`);
break;
}
}
const ft = ffiCallbackType(cb);
const internalRet = emitter.llType(ft.ret);
const callArgs = [`ptr %cb`, ...scriptArgs].join(", ");
if (cb.lifetime === "retained") {
emitter.declare(`declare ptr @scr_closure_retain_v(ptr)`);
emitter.declare(`declare void @scr_closure_release_v(ptr)`);
defs.push(` %invoke_pin = call ptr @scr_closure_retain_v(ptr %cb)`);
}
if (cb.returns === "void") {
defs.push(
` call void %fn(${callArgs})`,
...(cb.lifetime === "retained"
? [` call void @scr_closure_release_v(ptr %invoke_pin)`]
: []),
` ret void`,
`}`,
``,
);
continue;
}
defs.push(` %result = call ${internalRet} %fn(${callArgs})`);
if (cb.lifetime === "retained") {
defs.push(` call void @scr_closure_release_v(ptr %invoke_pin)`);
}
switch (cb.returns) {
case "i64":
case "u64":
case "pointer": {
const ty = ffiNativeTypeLl(cb.returns);
emitter.declare(`declare ${ty} @scr_bigint_to_${cb.returns}(ptr)`);
emitter.declare(`declare void @scr_bigint_release(ptr)`);
defs.push(
` %out = call ${ty} @scr_bigint_to_${cb.returns}(ptr %result)`,
` call void @scr_bigint_release(ptr %result)`,
` ret ${ty} %out`,
);
break;
}
case "f64":
defs.push(` ret double %result`);
break;
case "f32":
defs.push(` %out = fptrunc double %result to float`, ` ret float %out`);
break;
case "bool":
defs.push(` %out = zext i1 %result to i8`, ` ret i8 %out`);
break;
case "u8":
case "i8":
case "u16":
case "i16":
case "u32":
emitter.declare(`declare double @scr_bit_ushr(double, double)`);
defs.push(
` %coerced = call double @scr_bit_ushr(double %result, double ${f64Lit(0)})`,
` %wide = fptoui double %coerced to i32`,
);
if (cb.returns !== "u32") {
const nativeTy = ffiNativeTypeLl(cb.returns);
defs.push(` %out = trunc i32 %wide to ${nativeTy}`, ` ret ${nativeTy} %out`);
} else {
defs.push(` ret i32 %wide`);
}
break;
case "i32":
emitter.declare(`declare double @scr_bit_or(double, double)`);
defs.push(
` %coerced = call double @scr_bit_or(double %result, double ${f64Lit(0)})`,
` %out = fptosi double %coerced to i32`,
` ret i32 %out`,
);
break;
}
defs.push(`}`, ``);
}
return { globals, defs };
}
export function emitParts(emitter: LlEmitter): string[] {
const parts = [
...limitInlineReleases(emitter.emitModuleParts(), emitter.shapeHost.rcMode),
...emitter.disposeObserverGates(),
];
if (!emitter.executableTls) return parts;
// A worker executable's thread-locals all live in the executable's own
// TLS block: the program's internal ones, and the runtime's external
// ones, because the runtime is always linked statically into the
// executable. So on ELF they can all use the local-exec model: LLVM's
// default general-dynamic sequence (relaxed by the linker to `mov %fs:0`
// plus an add), and even initial-exec (relaxed to `mov $offset` plus an
// indexed access), become one %fs-relative operand LLVM can fold into
// loads and compares. Darwin's thread-local variables have one model.
return parts.map((part) =>
part
.replace(
/^(@[-$._A-Za-z0-9]+ = internal )thread_local global /gm,
"$1thread_local(localexec) global ",
)
.replace(
/^(@[-$._A-Za-z0-9]+ = external )thread_local global /gm,
"$1thread_local(localexec) global ",
),
);
}