mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
6125 lines
255 KiB
TypeScript
6125 lines
255 KiB
TypeScript
import { emitPerKeyOrder } from "./key-orders.js";
|
|
import { typedRefConstructor } from "./shapes.js";
|
|
import { bindLiveArrayOps } from "./expr-stream-bridges.js";
|
|
import {
|
|
BYTES_ELEM_NUM,
|
|
CLOSURE_KIND,
|
|
SCR_ARR_FIELD,
|
|
SCR_ARR_STATES,
|
|
f64Lit,
|
|
sizeToDouble,
|
|
} from "./common.js";
|
|
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
|
import {
|
|
INLINE_UNION_FIELD,
|
|
loadInlineUnion,
|
|
stackUnionBox,
|
|
storeInlineUnion,
|
|
unionBoxBits,
|
|
} from "./inline-unions.js";
|
|
import { InternalCompilerError } from "../../errors.js";
|
|
/* ScrDyn helpers for the LLVM backend: per-type match predicates
|
|
* (dynMatchHelper), checked builders (dynCheckHelper), static→dyn
|
|
* converters (toDynHelper), the type-independent singletons (String
|
|
* (unknown), caught→dyn, the keyed read, the destructuring
|
|
* RequireObjectCoercible, GetIterator+N), and the checked-dynamic
|
|
* function boundary's thunk/box/adapter triple. Helpers use the runtime ABI,
|
|
* explicit reference ownership, and path-annotated failure messages.
|
|
*
|
|
* dyn layout facts this file compiles against (scr_runtime.h):
|
|
* ScrDyn { size_t rc; ScrDynKind kind; bool buffer; union v; }
|
|
* v is naturally 8-byte aligned; size-bearing offsets below
|
|
* are selected from the target C ABI.
|
|
* v.num double | v.b i8 | v.str/v.bytes ptr at +16;
|
|
* v.arr { len +16, cap +24, items +32 };
|
|
* v.obj { len +16 } (own properties; read them through
|
|
* scr_dyn_member_*, which know both object modes);
|
|
* v.fn { clo +16, thunk +24, sig +32, name +40, arity +48 }.
|
|
* ScrDynKind: NULL=0 BOOL=1 NUM=2 STR=3 ARR=4 OBJ=5 UNDEF=6 BYTES=7
|
|
* FUNC=8 HANDLE=9.
|
|
* ScrBytes { rc +0; len +8; elem +16; data +24 }.
|
|
* ScrDynPath { parent, key, index } — the %ScrDynPath type. */
|
|
import type { IrRecordShape, IrType, IrUnionDef } from "../../ir/ir.js";
|
|
import {
|
|
accessorSlotProp,
|
|
DYN_HANDLE_KINDS,
|
|
isDirentShape,
|
|
KEY_ORDER_FIELD,
|
|
shapeKeyOrders,
|
|
isDynTypedRefType,
|
|
isRefCounted,
|
|
isUnitType,
|
|
recordHandleCodecClass,
|
|
textCodecRecordClass,
|
|
typeKey,
|
|
} from "../../ir/ir.js";
|
|
import { dynDesc, streamTypedRefEligible } from "../../ir/analysis.js";
|
|
import { mangleRecordNew, mangleRecordStruct } from "../mangle.js";
|
|
import { BlockBuilder } from "./blocks.js";
|
|
import { llvmCommentText } from "./common.js";
|
|
import { JS_BITS } from "./jsval-encoding.js";
|
|
import {
|
|
arrNewCall,
|
|
elemAccess,
|
|
llFieldType,
|
|
mapFlagsOffset,
|
|
releaseSym,
|
|
traceAdapter,
|
|
traceArg,
|
|
vAdapters,
|
|
} from "./shapes.js";
|
|
import { LlvmUnsupportedError } from "./unsupported.js";
|
|
import { recordMayKeepSource, recordSourceFieldOrder } from "./record-sources.js";
|
|
import type { WalkerHost } from "./walkers.js";
|
|
import type { LlStreamTypedRefAdapter } from "./expr-context.js";
|
|
|
|
/** typeKey with `?` after every `(T | undefined)[]` (optionalElements),
|
|
* at any depth of arrays, Maps, Sets and promises: checked conversions to
|
|
* `number[][]` and `(number | undefined)[][]` need separate helpers, though
|
|
* the two types share a typeKey. */
|
|
function checkedConversionKey(t: IrType): string {
|
|
switch (t.kind) {
|
|
case "array":
|
|
return `array<${checkedConversionKey(t.elem)}>${t.optionalElements ? "?" : ""}`;
|
|
case "map":
|
|
return `map<${checkedConversionKey(t.key)},${checkedConversionKey(t.value)}>`;
|
|
case "set":
|
|
return `set<${checkedConversionKey(t.elem)}>`;
|
|
case "promise":
|
|
return `promise<${checkedConversionKey(t.inner)}>`;
|
|
default:
|
|
return typeKey(t);
|
|
}
|
|
}
|
|
|
|
/** Whether to-dyn of a record shape keeps the record itself: an empty shape
|
|
* and an event-listener record (a `handleEvent` function field) box their
|
|
* identity. Every other shape, tuples included, copies its fields into a
|
|
* fresh checked-dynamic value. */
|
|
export function recordDynKeepsIdentity(shape: IrRecordShape): boolean {
|
|
if (shape.tuple) return false;
|
|
return (
|
|
(shape.fields.length === 0 && !shape.indexValue) ||
|
|
shape.fields.some((f) => f.name === "handleEvent" && f.type.kind === "func")
|
|
);
|
|
}
|
|
|
|
/** ScrDynKind values (scr_runtime.h). */
|
|
/** ScrDynHandleTag values (scr_dyn.h's explicit values; dyn-handle-tags.test.ts checks them). */
|
|
export const DYN_HANDLE_TAG_NUM: Record<string, number> = {
|
|
httpReq: 0,
|
|
httpRes: 1,
|
|
netSocket: 2,
|
|
netServer: 3,
|
|
http2Session: 4,
|
|
http2Stream: 5,
|
|
httpClientReq: 6,
|
|
child: 16,
|
|
fileHandle: 17,
|
|
dir: 44,
|
|
dgramSocket: 62,
|
|
cryptoHash: 52,
|
|
cryptoHmac: 53,
|
|
};
|
|
|
|
/** The handle tags of TextEncoder and TextDecoder records boxed as dyn
|
|
* (scr_dyn.h; scr_text_codec_dyn.c boxes and unboxes them). */
|
|
export const TEXT_CODEC_TAG_NUM: Record<"TextEncoder" | "TextDecoder" | "StringDecoder", number> = {
|
|
TextDecoder: 76,
|
|
TextEncoder: 77,
|
|
StringDecoder: 78,
|
|
};
|
|
|
|
/** The decoder record fields scr_text_decoder_to_dyn reads, in its
|
|
* parameter order. */
|
|
const TEXT_DECODER_FIELDS = ["%TextDecoder", "%fatal", "%ignoreBOM", "%state"] as const;
|
|
const STRING_DECODER_FIELDS = ["%enc", "%pending"] as const;
|
|
|
|
/** Handle kinds whose runtime unit boxes them through its own function,
|
|
* which installs the tag's ops on first use (scr_crypto.h). Programs that
|
|
* never box one of these values do not link its dynamic members. */
|
|
const DYN_HANDLE_BOX_FN: Record<string, string> = {
|
|
cryptoHash: "scr_crypto_hash_to_dyn",
|
|
cryptoHmac: "scr_crypto_hash_to_dyn",
|
|
dgramSocket: "scr_dgram_to_dyn",
|
|
};
|
|
|
|
/** Handle kinds whose box holds a unit-owned wrapper rather than the
|
|
* handle itself (a dgram socket's box carries its untyped listeners):
|
|
* the unit's own function unwraps it, with the tag check and TypeError
|
|
* of scr_dyn_handle_unbox. */
|
|
const DYN_HANDLE_UNBOX_FN: Record<string, string> = {
|
|
dgramSocket: "scr_dgram_from_dyn",
|
|
};
|
|
|
|
/** ScrMessageKind (scr_dyn.h): the node kinds message readers test. */
|
|
export const MESSAGE_KIND = {
|
|
UNDEFINED: 0,
|
|
NULL: 1,
|
|
BOOL: 2,
|
|
NUMBER: 3,
|
|
STRING: 4,
|
|
} as const;
|
|
|
|
export const DYN_KIND = {
|
|
NULL: 0,
|
|
BOOL: 1,
|
|
NUM: 2,
|
|
STR: 3,
|
|
ARR: 4,
|
|
OBJ: 5,
|
|
UNDEF: 6,
|
|
BYTES: 7,
|
|
FUNC: 8,
|
|
HANDLE: 9,
|
|
PROMISE: 10,
|
|
TYPED_REF: 12 /* SCR_DYN_TYPED_REF — static Web-stream transit capsule */,
|
|
PROXY: 13,
|
|
BIGINT: 14,
|
|
SYMBOL: 15,
|
|
} as const;
|
|
|
|
/** Union arm kinds whose matchers reject every plain dyn object
|
|
* (trustedArmByLiteral). */
|
|
const NON_OBJECT_ARM_KINDS: ReadonlySet<IrType["kind"]> = new Set<IrType["kind"]>([
|
|
"f64",
|
|
"string",
|
|
"bool",
|
|
"nullT",
|
|
"undefinedT",
|
|
"bigint",
|
|
"symbol",
|
|
]);
|
|
|
|
/** What the dyn helpers need beyond the walker host: interned immortal
|
|
* unit instances (undefined-armed dynCheck targets build them). */
|
|
export interface DynHost extends WalkerHost {
|
|
unitInstanceRef(unionId: string, tag: number): string;
|
|
/** See LlEmitter.retypeGuardRef. */
|
|
retypeGuardRef(unionId: string): string;
|
|
/** See ClassHost.virtualEntry. */
|
|
virtualEntry?(implFn: string): string;
|
|
/** See ClassHost.programLifetime. */
|
|
readonly programLifetime?: ReadonlySet<string>;
|
|
/** The immortal ABSENT field-slot state of an undefined-armed union. */
|
|
absentInstanceRef(unionId: string): string;
|
|
/** The shared adapter record: live array capsules read its `arrayOps`, so a
|
|
* narrower return type would hand the native compiler a copy without it. */
|
|
liveDynRefAdapter(t: IrType): LlStreamTypedRefAdapter;
|
|
/** See moduleUsesDynBuiltinStream. */
|
|
usesDynBuiltinStream?(): boolean;
|
|
liveDynUnionRefAdapter(t: IrType & { kind: "union" }): string;
|
|
/** The module has declaration-typed boundary casts (IrModule.declBoundary). */
|
|
declBoundary?(): boolean;
|
|
dynPromiseAdapter(t: IrType): string;
|
|
isErrorClass(className: string): boolean;
|
|
classSubtypes(className: string): readonly string[];
|
|
/** The strict subclasses whose class-value constructor takes the same
|
|
* arguments as `className`'s (classValueAbiEquals): the class values a
|
|
* checked `classval` slot accepts besides its own class. */
|
|
classValueSubtypes(className: string): readonly string[];
|
|
/** emitter.ts's pendingTestLines: the inline pending-exception test. */
|
|
pendingTestLines(dest: string): string[];
|
|
/** The closures with a jsval body (IrFunction.jsEntry) on targets with
|
|
* jsval words, which the box builders give a jsval entry. */
|
|
jsEntryClosures?(): readonly JsEntryClosure[];
|
|
}
|
|
|
|
/** A closure with a jsval body (IrFunction.jsEntry), for the box builders. */
|
|
export interface JsEntryClosure {
|
|
/** The closure's entry symbol (ScrClosure.fn) and signature. */
|
|
closure: string;
|
|
params: readonly IrType[];
|
|
ret: IrType;
|
|
/** The jsval body's entry symbol and signature. */
|
|
body: string;
|
|
bodyParams: readonly IrType[];
|
|
bodyRet: IrType;
|
|
}
|
|
|
|
const FN_ATTRS = "#0";
|
|
|
|
/** scr_dyn_builtin_stream_check's kind for a runtime stream class (the
|
|
* numbering of scr_dyn_builtin_class.c), or null. */
|
|
function builtinStreamKind(className: string): number | null {
|
|
const kinds: Record<string, number> = {
|
|
"%Readable": 2,
|
|
"%Writable": 3,
|
|
"%Duplex": 4,
|
|
"%Transform": 5,
|
|
"%PassThrough": 6,
|
|
};
|
|
return kinds[className] ?? null;
|
|
}
|
|
|
|
/** Unions whose discriminant has fewer string literals keep one literal test
|
|
* per arm (dispatchValues): one runtime lookup costs about what a few
|
|
* literal tests do. */
|
|
const DISCRIMINANT_DISPATCH_VALUES = 4;
|
|
|
|
export class LlDyn {
|
|
private readonly dynMatchers = new Map<string, string>();
|
|
private readonly dynBuilders = new Map<string, string>();
|
|
private readonly toDynFns = new Map<string, string>();
|
|
private readonly dynFuncThunks = new Map<string, string>();
|
|
private readonly dynFuncBoxes = new Map<string, string>();
|
|
private readonly jsEntryThunks = new Map<string, string>();
|
|
private readonly dynFuncAdapters = new Map<string, string>();
|
|
private readonly messageListeners = new Map<string, string | null>();
|
|
private readonly messageReaders = new Map<string, string>();
|
|
private readonly promiseDynAdapters = new Map<string, string>();
|
|
private readonly mapDynOps = new Map<string, string>();
|
|
private dynToStrFn: string | null = null;
|
|
private caughtToDynFn: string | null = null;
|
|
private readonly helperMemo = new Map<string, string>();
|
|
private readonly recursiveShapes = new Map<string, boolean>();
|
|
private readonly holdsRecursiveShapes = new Map<string, boolean>();
|
|
/** Emitted function definitions, in interning order. */
|
|
readonly defs: string[] = [];
|
|
|
|
private readonly recordViewTables = new Map<string, string>();
|
|
|
|
constructor(private readonly host: DynHost) {}
|
|
|
|
/** Journal replay (emit-journal.ts): builds or finds the helper a chunk
|
|
* emitter recorded, and answers its name. */
|
|
replayHelper(op: string, args: readonly unknown[]): string {
|
|
switch (op) {
|
|
case "dyn.collectionDynOps":
|
|
return this.collectionDynOps(args[0] as IrType & { kind: "map" | "set" });
|
|
case "dyn.dynMatchHelper":
|
|
return this.dynMatchHelper(args[0] as IrType, args[1] as boolean);
|
|
case "dyn.jsonSchema":
|
|
return this.jsonSchema(args[0] as IrType);
|
|
case "dyn.recordNameTable": {
|
|
const shapeId = args[0] as string;
|
|
return this.recordNameTable(shapeId, this.host.recordsById.get(shapeId)!).names;
|
|
}
|
|
case "dyn.jsonParseHelper":
|
|
return this.jsonParseHelper(args[0] as IrType)!;
|
|
case "dyn.dynCheckHelper":
|
|
return this.dynCheckHelper(args[0] as IrType, args[1] as boolean);
|
|
case "dyn.toDynHelper":
|
|
return this.toDynHelper(args[0] as IrType);
|
|
case "dyn.promiseDynAdapterHelper":
|
|
return this.promiseDynAdapterHelper(args[0] as IrType);
|
|
case "dyn.dynFuncThunkHelper":
|
|
return this.dynFuncThunkHelper(args[0] as IrType & { kind: "func" });
|
|
case "dyn.dynFuncBoxHelper":
|
|
return this.dynFuncBoxHelper(args[0] as IrType & { kind: "func" });
|
|
case "dyn.jsEntryThunk":
|
|
return this.jsEntryThunk(args[0] as string, args[1] as IrType[], args[2] as IrType);
|
|
case "dyn.dynFuncAdapterHelper":
|
|
return this.dynFuncAdapterHelper(args[0] as IrType & { kind: "func" });
|
|
case "dyn.dynToStrHelper":
|
|
return this.dynToStrHelper();
|
|
case "dyn.dynIterNHelper":
|
|
return this.dynIterNHelper();
|
|
case "dyn.dynDestrCheckHelper":
|
|
return this.dynDestrCheckHelper();
|
|
case "dyn.dynKeyGetHelper":
|
|
return this.dynKeyGetHelper();
|
|
case "dyn.dynComputedKeyGetHelper":
|
|
return this.dynComputedKeyGetHelper();
|
|
case "dyn.caughtToDynHelper":
|
|
return this.caughtToDynHelper();
|
|
case "dyn.messageListenerHelper":
|
|
return this.messageListenerHelper(args[0] as IrType & { kind: "func" }) ?? "";
|
|
case "dyn.messageReaderHelper":
|
|
return this.messageReaderHelper(args[0] as IrType);
|
|
default:
|
|
throw new InternalCompilerError(`llvm emitter bug: no dyn helper replay ${op}`);
|
|
}
|
|
}
|
|
|
|
/** The types a dyn matcher or builder for `t` checks structurally: record
|
|
* fields and index values, array elements and union arms. */
|
|
private structuralChildren(t: IrType): readonly IrType[] {
|
|
switch (t.kind) {
|
|
case "record": {
|
|
const shape = this.host.recordsById.get(t.shapeId);
|
|
if (!shape) return [];
|
|
const fields = shape.fields.map((f) => f.type);
|
|
return shape.indexValue ? [...fields, shape.indexValue] : fields;
|
|
}
|
|
case "array":
|
|
return [t.elem];
|
|
case "union":
|
|
return this.host.unionsById.get(t.unionId)?.arms ?? [];
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/** A record or array type that reaches itself through its fields,
|
|
* elements or union arms. Only these can describe a cyclic dyn value, so
|
|
* only their matchers and builders track visited values. */
|
|
private isRecursiveShape(t: IrType): boolean {
|
|
if (t.kind !== "record" && t.kind !== "array") return false;
|
|
const key = typeKey(t);
|
|
const known = this.recursiveShapes.get(key);
|
|
if (known !== undefined) return known;
|
|
const seen = new Set<string>();
|
|
const pending = [...this.structuralChildren(t)];
|
|
let recursive = false;
|
|
while (pending.length > 0 && !recursive) {
|
|
const next = pending.pop()!;
|
|
const nextKey = typeKey(next);
|
|
if (nextKey === key) recursive = true;
|
|
else if (!seen.has(nextKey)) {
|
|
seen.add(nextKey);
|
|
pending.push(...this.structuralChildren(next));
|
|
}
|
|
}
|
|
this.recursiveShapes.set(key, recursive);
|
|
return recursive;
|
|
}
|
|
|
|
/** A type with a recursive shape among the types it reaches. Its matcher
|
|
* and builder keep one visit session open for all of its members, so a
|
|
* dyn value reached through two of them converts once. */
|
|
private holdsRecursiveShape(t: IrType): boolean {
|
|
const key = typeKey(t);
|
|
const known = this.holdsRecursiveShapes.get(key);
|
|
if (known !== undefined) return known;
|
|
const seen = new Set<string>([key]);
|
|
const pending = [...this.structuralChildren(t)];
|
|
let holds = false;
|
|
while (pending.length > 0 && !holds) {
|
|
const next = pending.pop()!;
|
|
const nextKey = typeKey(next);
|
|
if (seen.has(nextKey)) continue;
|
|
seen.add(nextKey);
|
|
if (this.isRecursiveShape(next)) holds = true;
|
|
else pending.push(...this.structuralChildren(next));
|
|
}
|
|
this.holdsRecursiveShapes.set(key, holds);
|
|
return holds;
|
|
}
|
|
|
|
/** `name` around the structural body `${name}_body`, inside one visit
|
|
* session of the runtime. A recursive shape's matcher ("match") answers
|
|
* true for a (value, matcher) pair it is already checking further up the
|
|
* stack. A recursive shape's builder ("build") answers the value it
|
|
* already built for the same dyn value in the session, so cycles and
|
|
* shared values keep their identity. Matchers ("visit") and builders
|
|
* ("memo") of types holding recursive shapes only keep the session open
|
|
* across their members. */
|
|
private emitVisitWrapper(
|
|
name: string,
|
|
t: IrType,
|
|
mode: "match" | "visit" | "build" | "memo",
|
|
): void {
|
|
const host = this.host;
|
|
const what = mode === "match" || mode === "build" ? "visits tracked" : "one visit session";
|
|
if (mode === "match" || mode === "visit") {
|
|
host.declare(`declare zeroext i1 @scr_dyn_match_visit(ptr, ptr, ptr)`);
|
|
this.defs.push(
|
|
`define internal zeroext i1 @${name}(ptr %d) ${FN_ATTRS} { ; matches ${llvmCommentText(typeKey(t))}, ${what}`,
|
|
`entry:`,
|
|
` %ok = call zeroext i1 @scr_dyn_match_visit(ptr %d, ptr ${mode === "match" ? `@${name}` : "null"}, ptr @${name}_body)`,
|
|
` ret i1 %ok`,
|
|
`}`,
|
|
``,
|
|
);
|
|
return;
|
|
}
|
|
const retain = mode === "build" ? vAdapters(host, t).retain : "null";
|
|
host.declare(`declare ptr @scr_dyn_build_visit(ptr, ptr, ptr, ptr, ptr)`);
|
|
this.defs.push(
|
|
`define internal ptr @${name}(ptr %d, ptr %path) ${FN_ATTRS} { ; check ${llvmCommentText(typeKey(t))}, ${what}`,
|
|
`entry:`,
|
|
` %r = call ptr @scr_dyn_build_visit(ptr %d, ptr %path, ptr ${mode === "build" ? `@${name}` : "null"}, ptr @${name}_body, ptr ${retain})`,
|
|
` ret ptr %r`,
|
|
`}`,
|
|
``,
|
|
);
|
|
}
|
|
|
|
/** The runtime call that gives a matcher or builder body a capsule's
|
|
* dyn view: inside a visit session (`wrapped`), a capsule keeps the view
|
|
* it was first given, so an outer visit can keep reading it. */
|
|
private viewFn(wrapped: boolean): string {
|
|
const fn = wrapped ? "scr_dyn_visit_view" : "scr_dyn_typed_ref_peek";
|
|
this.host.declare(`declare ptr @${fn}(ptr)`);
|
|
return fn;
|
|
}
|
|
|
|
/** In a recursive shape's builder body: record `value`, just allocated
|
|
* for %d, before its members are built. */
|
|
private recordBuilt(B: BlockBuilder, name: string, value: string): void {
|
|
this.host.declare(`declare void @scr_dyn_build_add(ptr, ptr, ptr)`);
|
|
B.line(`call void @scr_dyn_build_add(ptr %d, ptr @${name}, ptr ${value})`);
|
|
}
|
|
|
|
/** Whether a builder of `def` may pick a plain object's arm by its literal
|
|
* discriminator alone inside a trusted build (scr_dyn_trust_enter): the
|
|
* arms' literals are disjoint, and every arm without a literal has a kind
|
|
* no plain object matches. The value then matched exactly one arm with a
|
|
* literal, the arm whose literal it carries. */
|
|
private trustedArmByLiteral(def: IrUnionDef): boolean {
|
|
const discriminant = def.discriminant;
|
|
if (!discriminant) return false;
|
|
const seen = new Set<string>();
|
|
for (const { values } of discriminant.cases) {
|
|
for (const value of values) {
|
|
const key = `${typeof value}:${String(value)}`;
|
|
if (seen.has(key)) return false;
|
|
seen.add(key);
|
|
}
|
|
}
|
|
return def.arms.every(
|
|
(arm, tag) =>
|
|
discriminant.cases.some((candidate) => candidate.tag === tag) ||
|
|
NON_OBJECT_ARM_KINDS.has(arm.kind),
|
|
);
|
|
}
|
|
|
|
/** Per arm of `def`, the dyn kind of the values a builder may convert into
|
|
* that arm without matching its structure inside a trusted build, or null.
|
|
* A value there already matched one of the union's arms, so the arm is
|
|
* known when it is the only arm that takes values of its kind: the arm
|
|
* whose literal a plain object carries (trustedArmByLiteral), or the one
|
|
* arm a plain object or an array can match at all. */
|
|
private trustedArmKinds(def: IrUnionDef): (number | null)[] {
|
|
const byLiteral = this.trustedArmByLiteral(def);
|
|
const objectArms = def.arms.flatMap((arm, tag) =>
|
|
NON_OBJECT_ARM_KINDS.has(arm.kind) ? [] : [tag],
|
|
);
|
|
return def.arms.map((arm, tag) => {
|
|
if (def.discriminant?.cases.some((candidate) => candidate.tag === tag))
|
|
return byLiteral ? DYN_KIND.OBJ : null;
|
|
if (objectArms.length !== 1 || objectArms[0] !== tag) return null;
|
|
if (arm.kind === "array") return DYN_KIND.ARR;
|
|
if (arm.kind === "record" && this.host.recordsById.get(arm.shapeId)?.tuple !== true)
|
|
return DYN_KIND.OBJ;
|
|
return null;
|
|
});
|
|
}
|
|
|
|
/** The order a dyn value tries a union's arms in: the canonical order,
|
|
* except that among the record arms, those with more required fields go
|
|
* first, then those with more fields. Records are width-tolerant, so an
|
|
* arm with fewer fields (`{}`, or one whose fields are all optional,
|
|
* matches every object) would otherwise take an object that a wider arm
|
|
* also matches and drop the fields only the wider arm has. */
|
|
private unionArmOrder(def: IrUnionDef): number[] {
|
|
const width = (i: number): [number, number] | null => {
|
|
const arm = def.arms[i]!;
|
|
if (arm.kind !== "record") return null;
|
|
const shape = this.host.recordsById.get(arm.shapeId);
|
|
if (!shape || shape.tuple) return null;
|
|
const required = shape.fields.filter(
|
|
(field) =>
|
|
field.type.kind !== "union" ||
|
|
!this.host.unionsById.get(field.type.unionId)?.arms.some((x) => x.kind === "undefinedT"),
|
|
).length;
|
|
return [required, shape.fields.length];
|
|
};
|
|
const order = def.arms.map((_, i) => i);
|
|
const records = order.filter((i) => width(i) !== null);
|
|
const widest = [...records].sort((x, y) => {
|
|
const [rx, fx] = width(x)!;
|
|
const [ry, fy] = width(y)!;
|
|
return ry - rx || fy - fx || x - y;
|
|
});
|
|
records.forEach((slot, k) => (order[slot] = widest[k]!));
|
|
return order;
|
|
}
|
|
|
|
/** The string literals of `def`'s discriminant when its arms are chosen
|
|
* by reading the discriminant once (unionArmsByDiscriminant), or null
|
|
* when the union keeps one literal test per arm: some literal is not a
|
|
* string, or there are fewer than DISCRIMINANT_DISPATCH_VALUES. */
|
|
private dispatchValues(def: IrUnionDef): string[] | null {
|
|
const values: string[] = [];
|
|
for (const guard of def.discriminant?.cases ?? []) {
|
|
if (guard.values.length === 0) return null;
|
|
for (const value of guard.values) {
|
|
if (typeof value !== "string") return null;
|
|
if (!values.includes(value)) values.push(value);
|
|
}
|
|
}
|
|
return values.length < DISCRIMINANT_DISPATCH_VALUES ? null : values;
|
|
}
|
|
|
|
/** Branches to `hits[tag]` for the first arm in `order` that `%d` matches
|
|
* (unionArmMatch), or to `none` when no arm does, for
|
|
* a union whose discriminant literals are `values` (dispatchValues). The
|
|
* discriminant is read once (scr_dyn_discriminant_case, against the
|
|
* constant `table`), and only the arms whose literal it holds and the arms
|
|
* without a literal are tested, in `order`: every other arm's literal test
|
|
* fails. An arm's failed check can run a getter that changes the
|
|
* discriminant, so it is read again after each one; when its case
|
|
* changed, the tests continue arm by arm after the failed arm, as they
|
|
* would have run from the first arm. A value the runtime cannot classify
|
|
* that way (a typed capsule, an accessor, an inherited discriminant) takes
|
|
* the tests arm by arm. */
|
|
private unionArmsByDiscriminant(
|
|
B: BlockBuilder,
|
|
def: IrUnionDef,
|
|
values: readonly string[],
|
|
order: readonly number[],
|
|
hits: readonly string[],
|
|
none: string,
|
|
preserveRefs: boolean,
|
|
table: string,
|
|
trustedKinds: readonly (number | null)[] = def.arms.map(() => null),
|
|
): void {
|
|
const discriminant = def.discriminant!;
|
|
const literalsOf = (tag: number): readonly (string | number | boolean)[] | null =>
|
|
discriminant.cases.find((candidate) => candidate.tag === tag)?.values ?? null;
|
|
const host = this.host;
|
|
this.defs.push(
|
|
`${table} = private unnamed_addr constant [${values.length} x ptr] [${values.map((value) => `ptr ${host.internLiteral(value)}`).join(", ")}]`,
|
|
``,
|
|
);
|
|
host.declare(`declare i32 @scr_dyn_discriminant_case(ptr, ptr, ptr, ${host.sizeType})`);
|
|
const classify = (): string => {
|
|
const index = B.tmp();
|
|
B.line(
|
|
`${index} = call i32 @scr_dyn_discriminant_case(ptr %d, ptr ${host.internLiteral(discriminant.field)}, ptr ${table}, ${host.sizeType} ${values.length})`,
|
|
);
|
|
return index;
|
|
};
|
|
// The arm-by-arm tests, entered at any arm (`from[i]` tests order[i]
|
|
// and the arms after it).
|
|
const tests = B.newLabel("du.tests");
|
|
const from = order.map((_, i) => (i === 0 ? tests : B.newLabel("du.t")));
|
|
const after = (position: number): string =>
|
|
position + 1 < order.length ? from[position + 1]! : none;
|
|
// Tests the arms a value in case `k` (-1: no literal) can match.
|
|
const tryCase = (k: number): void => {
|
|
const value = k < 0 ? null : values[k]!;
|
|
const positions = order
|
|
.map((_, position) => position)
|
|
.filter((position) => {
|
|
const literals = literalsOf(order[position]!);
|
|
return literals === null || (value !== null && literals.includes(value));
|
|
});
|
|
if (positions.length === 0) {
|
|
B.br(none);
|
|
return;
|
|
}
|
|
positions.forEach((position, index) => {
|
|
const tag = order[position]!;
|
|
const failed = B.newLabel("du.c.f");
|
|
this.unionArmStructure(B, def, tag, hits[tag]!, failed, preserveRefs, trustedKinds[tag]!);
|
|
B.startBlock(failed);
|
|
const next = index === positions.length - 1 ? none : B.newLabel("du.c.n");
|
|
const same = B.tmp();
|
|
B.line(`${same} = icmp eq i32 ${classify()}, ${k}`);
|
|
B.condBr(same, next, after(position));
|
|
if (next !== none) B.startBlock(next);
|
|
});
|
|
};
|
|
const index = classify();
|
|
const unmatched = B.newLabel("du.none");
|
|
const cases = values.map(() => B.newLabel("du.case"));
|
|
B.terminate(
|
|
`switch i32 ${index}, label %${tests} [ i32 -1, label %${unmatched} ${cases.map((label, k) => `i32 ${k}, label %${label}`).join(" ")} ]`,
|
|
);
|
|
values.forEach((_, k) => {
|
|
B.startBlock(cases[k]!);
|
|
tryCase(k);
|
|
});
|
|
B.startBlock(unmatched);
|
|
tryCase(-1);
|
|
order.forEach((tag, position) => {
|
|
B.startBlock(from[position]!);
|
|
this.unionArmMatch(
|
|
B,
|
|
def,
|
|
tag,
|
|
hits[tag]!,
|
|
after(position),
|
|
preserveRefs,
|
|
trustedKinds[tag]!,
|
|
);
|
|
});
|
|
}
|
|
|
|
/** Branch to a variant only after its literal discriminator and complete
|
|
* structure match. Use the same path for predicates and builders. With a
|
|
* `trustedKind` (builders, trustedArmKinds), a value of that dyn kind
|
|
* inside a trusted build skips the structure match once its literal
|
|
* matched. */
|
|
private unionArmMatch(
|
|
B: BlockBuilder,
|
|
def: IrUnionDef,
|
|
tag: number,
|
|
yes: string,
|
|
no: string,
|
|
preserveRefs = false,
|
|
trustedKind: number | null = null,
|
|
): void {
|
|
const guard = def.discriminant?.cases.find((candidate) => candidate.tag === tag);
|
|
if (guard && def.discriminant) {
|
|
const structure = B.newLabel("du.shape");
|
|
const field = this.host.internLiteral(def.discriminant.field);
|
|
guard.values.forEach((value, index) => {
|
|
const suffix =
|
|
typeof value === "string" ? "str" : typeof value === "boolean" ? "bool" : "num";
|
|
const ty = typeof value === "string" ? "ptr" : typeof value === "boolean" ? "i1" : "double";
|
|
const operand =
|
|
typeof value === "string"
|
|
? this.host.internLiteral(value)
|
|
: typeof value === "boolean"
|
|
? String(value)
|
|
: f64Lit(value);
|
|
this.host.declare(
|
|
`declare zeroext i1 @scr_dyn_field_eq_${suffix}(ptr, ptr, ${ty}${ty === "i1" ? " zeroext" : ""})`,
|
|
);
|
|
const matches = B.tmp();
|
|
B.line(
|
|
`${matches} = call zeroext i1 @scr_dyn_field_eq_${suffix}(ptr %d, ptr ${field}, ${ty} ${operand})`,
|
|
);
|
|
const next = index === guard.values.length - 1 ? no : B.newLabel("du.literal");
|
|
B.condBr(matches, structure, next);
|
|
if (index !== guard.values.length - 1) B.startBlock(next);
|
|
});
|
|
B.startBlock(structure);
|
|
}
|
|
this.unionArmStructure(B, def, tag, yes, no, preserveRefs, trustedKind);
|
|
}
|
|
|
|
/** The part of unionArmMatch past the arm's literal test. With a
|
|
* `trustedKind`, a value of that dyn kind inside a trusted build skips the
|
|
* structure match. */
|
|
private unionArmStructure(
|
|
B: BlockBuilder,
|
|
def: IrUnionDef,
|
|
tag: number,
|
|
yes: string,
|
|
no: string,
|
|
preserveRefs: boolean,
|
|
trustedKind: number | null = null,
|
|
): void {
|
|
if (trustedKind !== null) {
|
|
this.host.declare(`declare zeroext i1 @scr_dyn_trusted_build()`);
|
|
const kind = this.kindOf(B, "%d");
|
|
const sameKind = B.tmp();
|
|
const trusted = B.tmp();
|
|
const skip = B.tmp();
|
|
B.line(`${sameKind} = icmp eq i32 ${kind}, ${trustedKind}`);
|
|
B.line(`${trusted} = call zeroext i1 @scr_dyn_trusted_build()`);
|
|
B.line(`${skip} = and i1 ${sameKind}, ${trusted}`);
|
|
const full = B.newLabel("du.full");
|
|
B.condBr(skip, yes, full);
|
|
B.startBlock(full);
|
|
}
|
|
const matches = B.tmp();
|
|
B.line(
|
|
`${matches} = call zeroext i1 @${this.dynMatchHelper(def.arms[tag]!, preserveRefs)}(ptr %d)`,
|
|
);
|
|
B.condBr(matches, yes, no);
|
|
}
|
|
|
|
private get S(): "i32" | "i64" {
|
|
return this.host.sizeType;
|
|
}
|
|
private abiOffset(native64: number, wasm32: number): number {
|
|
return this.S === "i32" ? wasm32 : native64;
|
|
}
|
|
private get indexMaxDiv10(): string {
|
|
return this.S === "i32" ? "429496728" : "1844674407370955160";
|
|
}
|
|
|
|
/** The value LLVM type of a dynCheck result / toDyn operand for `t`. */
|
|
private valTy(t: IrType): string {
|
|
return t.kind === "f64" || t.kind === "procStream"
|
|
? "double"
|
|
: t.kind === "bool"
|
|
? "i1"
|
|
: "ptr";
|
|
}
|
|
|
|
private collectionDynOps(t: IrType & { kind: "map" | "set" }): string {
|
|
const key = typeKey(t);
|
|
const prior = this.mapDynOps.get(key);
|
|
if (prior) return prior;
|
|
const name = `sc_map_dyn_${this.mapDynOps.size}`;
|
|
this.mapDynOps.set(key, name);
|
|
this.host.journalAllocated?.(name, "dyn.collectionDynOps", [t]);
|
|
const keyType = t.kind === "map" ? t.key : t.elem;
|
|
const valueType: IrType = t.kind === "map" ? t.value : { kind: "f64" };
|
|
const emitSlot = (type: IrType, role: string): void => {
|
|
const ty = this.valTy(type);
|
|
const unpack =
|
|
ty === "double"
|
|
? "bitcast i64 %slot to double"
|
|
: ty === "i1"
|
|
? "trunc i64 %slot to i1"
|
|
: "inttoptr i64 %slot to ptr";
|
|
const pack =
|
|
ty === "double"
|
|
? "bitcast double %value to i64"
|
|
: ty === "i1"
|
|
? "zext i1 %value to i64"
|
|
: "ptrtoint ptr %value to i64";
|
|
// Collection reads expose the original mutable element, including
|
|
// records/arrays inside unions, rather than a detached dyn snapshot.
|
|
const B = new BlockBuilder();
|
|
B.line(`%value = ${unpack}`);
|
|
if (
|
|
type.kind === "union" &&
|
|
this.host.unionsById.get(type.unionId)?.arms.some(streamTypedRefEligible)
|
|
) {
|
|
B.line(`%result = call ptr @${this.host.liveDynUnionRefAdapter(type)}(ptr %value)`);
|
|
} else if (type.kind === "record" || type.kind === "array") {
|
|
const adapter = this.host.liveDynRefAdapter(type);
|
|
const rc = vAdapters(this.host, type);
|
|
const typeId = typeKey(type);
|
|
B.line(
|
|
`%result = call ptr ${typedRefConstructor(this.host, type)}(ptr %value, ptr ${rc.retain}, ptr ${rc.release}, ptr ${this.host.cstr(typeId)}, ${this.S} ${Buffer.byteLength(typeId, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
|
|
);
|
|
for (const line of bindLiveArrayOps(this.host, adapter, "%result")) B.line(line);
|
|
} else {
|
|
B.line(`%result = call ptr @${this.toDynHelper(type)}(${ty} %value)`);
|
|
}
|
|
B.terminate("ret ptr %result");
|
|
const unbox = this.dynCheckHelper(type);
|
|
this.defs.push(
|
|
`define internal ptr @${name}_${role}_box(i64 %slot) ${FN_ATTRS} {`,
|
|
B.render(),
|
|
`}`,
|
|
"",
|
|
`define internal i64 @${name}_${role}_unbox(ptr %d) ${FN_ATTRS} {`,
|
|
` %value = call ${ty === "i1" ? "zeroext i1" : ty} @${unbox}(ptr %d, ptr null)`,
|
|
` %result = ${pack}`,
|
|
` ret i64 %result`,
|
|
`}`,
|
|
"",
|
|
);
|
|
};
|
|
emitSlot(keyType, "key");
|
|
emitSlot(valueType, "value");
|
|
this.defs.push(
|
|
`@${name} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr } { ptr ${this.host.cstr(key)}, ptr @${name}_key_box, ptr @${name}_key_unbox, ptr @${this.dynMatchHelper(keyType)}, ptr @${name}_value_box, ptr @${name}_value_unbox }`,
|
|
"",
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/* ── dyn node plumbing ─────────────────────────────────────────────── */
|
|
|
|
/** Loads a dyn node's kind tag (after the size_t rc word). */
|
|
/** i1: the checked-dynamic value `d` is undefined. */
|
|
isUndefined(B: BlockBuilder, d: string): string {
|
|
const is = B.tmp();
|
|
B.line(`${is} = icmp eq i32 ${this.kindOf(B, d)}, ${DYN_KIND.UNDEF}`);
|
|
return is;
|
|
}
|
|
|
|
private kindOf(B: BlockBuilder, d: string): string {
|
|
const p = B.tmp();
|
|
const k = B.tmp();
|
|
B.line(`${p} = getelementptr inbounds i8, ptr ${d}, i64 ${this.abiOffset(8, 4)} ; ->kind`);
|
|
B.line(`${k} = load i32, ptr ${p}`);
|
|
return k;
|
|
}
|
|
|
|
/** Loads the 8-byte payload slot at +16 as the given LLVM type. */
|
|
private payloadOf(B: BlockBuilder, d: string, ty: "double" | "ptr" | "i64"): string {
|
|
const p = B.tmp();
|
|
const v = B.tmp();
|
|
B.line(`${p} = getelementptr inbounds i8, ptr ${d}, i64 16 ; ->v`);
|
|
B.line(`${v} = load ${ty}, ptr ${p}`);
|
|
return v;
|
|
}
|
|
|
|
/** Loads v.b (i8 at +16) as i1. */
|
|
private boolOf(B: BlockBuilder, d: string): string {
|
|
const p = B.tmp();
|
|
const raw = B.tmp();
|
|
const b = B.tmp();
|
|
B.line(`${p} = getelementptr inbounds i8, ptr ${d}, i64 16 ; ->v.b`);
|
|
B.line(`${raw} = load i8, ptr ${p}`);
|
|
B.line(`${b} = trunc i8 ${raw} to i1`);
|
|
return b;
|
|
}
|
|
|
|
/** Loads v.arr/v.obj length (size_t at +16). */
|
|
private lenOf(B: BlockBuilder, d: string): string {
|
|
const p = B.tmp();
|
|
const n = B.tmp();
|
|
B.line(`${p} = getelementptr inbounds i8, ptr ${d}, i64 16 ; ->v.arr.len`);
|
|
B.line(`${n} = load ${this.S}, ptr ${p}`);
|
|
return n;
|
|
}
|
|
|
|
/** Loads v.arr.items / v.obj.entries after two size_t fields. */
|
|
private itemsOf(B: BlockBuilder, d: string): string {
|
|
const p = B.tmp();
|
|
const it = B.tmp();
|
|
B.line(
|
|
`${p} = getelementptr inbounds i8, ptr ${d}, i64 ${this.abiOffset(32, 24)} ; ->v.arr.items`,
|
|
);
|
|
B.line(`${it} = load ptr, ptr ${p}`);
|
|
return it;
|
|
}
|
|
|
|
/** Loads items[i] (borrowed ScrDyn *). */
|
|
private itemAt(B: BlockBuilder, items: string, i: string): string {
|
|
const p = B.tmp();
|
|
const e = B.tmp();
|
|
B.line(`${p} = getelementptr inbounds ptr, ptr ${items}, ${this.S} ${i}`);
|
|
B.line(`${e} = load ptr, ptr ${p}`);
|
|
return e;
|
|
}
|
|
|
|
/** Own property i of object `d` (borrowed key bytes, key length and
|
|
* value), read through the runtime's accessors: the object may be in
|
|
* layout or dictionary mode (scr_dyn.h). */
|
|
private entryAt(
|
|
B: BlockBuilder,
|
|
d: string,
|
|
i: string,
|
|
): { key: string; keyLen: string; value: string } {
|
|
this.host.declare(`declare ptr @scr_dyn_member_key(ptr, ${this.S})`);
|
|
this.host.declare(`declare ${this.S} @scr_dyn_member_key_len(ptr, ${this.S})`);
|
|
this.host.declare(`declare ptr @scr_dyn_member_value(ptr, ${this.S})`);
|
|
const key = B.tmp();
|
|
const keyLen = B.tmp();
|
|
const value = B.tmp();
|
|
B.line(`${key} = call ptr @scr_dyn_member_key(ptr ${d}, ${this.S} ${i})`);
|
|
B.line(`${keyLen} = call ${this.S} @scr_dyn_member_key_len(ptr ${d}, ${this.S} ${i})`);
|
|
B.line(`${value} = call ptr @scr_dyn_member_value(ptr ${d}, ${this.S} ${i})`);
|
|
return { key, keyLen, value };
|
|
}
|
|
|
|
/** An ScrStr's (len, data) pair — len via the %ScrStr header, data the
|
|
* flexible tail at +24. */
|
|
private strParts(B: BlockBuilder, s: string): { len: string; data: string } {
|
|
const lp = B.tmp();
|
|
const len = B.tmp();
|
|
const data = B.tmp();
|
|
B.line(`${lp} = getelementptr inbounds %ScrStr, ptr ${s}, i64 0, i32 1`);
|
|
B.line(`${len} = load ${this.S}, ptr ${lp}`);
|
|
B.line(`${data} = getelementptr inbounds i8, ptr ${s}, i64 ${this.abiOffset(24, 12)} ; ->data`);
|
|
return { len, data };
|
|
}
|
|
|
|
/** `call ptr @scr_dyn_retain_v(x)` (+1; immortals skip). */
|
|
private retainDyn(B: BlockBuilder, x: string): string {
|
|
this.host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
|
|
const t = B.tmp();
|
|
B.line(`${t} = call ptr @scr_dyn_retain_v(ptr ${x})`);
|
|
return t;
|
|
}
|
|
|
|
/** THE immortal undefined dyn value (borrowed — retain to own). */
|
|
private undef(B: BlockBuilder): string {
|
|
this.host.declare(`declare ptr @scr_dyn_undefined()`);
|
|
const t = B.tmp();
|
|
B.line(`${t} = call ptr @scr_dyn_undefined()`);
|
|
return t;
|
|
}
|
|
|
|
/** scr_dyn_obj_get with a compile-time key: borrowed member or null. */
|
|
private objGetLit(B: BlockBuilder, d: string, key: string): string {
|
|
this.host.declare(`declare ptr @scr_dyn_obj_get(ptr, ptr, ${this.S})`);
|
|
const t = B.tmp();
|
|
const len = Buffer.byteLength(key, "utf8");
|
|
B.line(
|
|
`${t} = call ptr @scr_dyn_obj_get(ptr ${d}, ptr ${this.host.cstr(key)}, ${this.S} ${len}) ; .${llvmCommentText(key)}`,
|
|
);
|
|
return t;
|
|
}
|
|
|
|
/** Branches past the caller's next emission when an undefined-armed
|
|
* field value is the ABSENT state. Returns the label the caller must branch
|
|
* to and start after its emission, or null when the type has no
|
|
* undefined arm. */
|
|
private skipIfAbsent(
|
|
B: BlockBuilder,
|
|
value: string,
|
|
t: IrType & { kind: "union" },
|
|
): string | null {
|
|
const def = this.host.unionsById.get(t.unionId);
|
|
const utag = def ? def.arms.findIndex((arm) => arm.kind === "undefinedT") : -1;
|
|
if (utag < 0) return null;
|
|
const absent = this.host.fieldAbsentTestIn(B, value, t.unionId);
|
|
const lSkip = B.newLabel("tdr.absent");
|
|
const lSet = B.newLabel("tdr.set");
|
|
B.condBr(absent, lSkip, lSet);
|
|
B.startBlock(lSet);
|
|
return lSkip;
|
|
}
|
|
|
|
/** A checked undefined-armed field value, or the field's ABSENT state
|
|
* when the value is undefined because the source object has no own
|
|
* property of that name (a missing JSON key stays missing; an explicit
|
|
* undefined stays present). Proxy sources answer through their reads, so
|
|
* they count as present. The checked value is a +1 union; replacing it
|
|
* with the immortal absent instance releases nothing (unit instances are
|
|
* immortal too). */
|
|
private presenceOf(
|
|
B: BlockBuilder,
|
|
d: string,
|
|
key: string,
|
|
t: IrType & { kind: "union" },
|
|
value: string,
|
|
): string {
|
|
const def = this.host.unionsById.get(t.unionId);
|
|
const utag = def ? def.arms.findIndex((arm) => arm.kind === "undefinedT") : -1;
|
|
if (utag < 0) return value;
|
|
const tag = emitUnionTag(B, this.host.nullableUnions.get(t.unionId), value);
|
|
const isUndef = B.tmp();
|
|
B.line(`${isUndef} = icmp eq i32 ${tag}, ${utag}`);
|
|
const kind = this.kindOf(B, d);
|
|
const isObj = B.tmp();
|
|
B.line(`${isObj} = icmp eq i32 ${kind}, ${DYN_KIND.OBJ}`);
|
|
const probe = B.tmp();
|
|
B.line(`${probe} = and i1 ${isUndef}, ${isObj}`);
|
|
const slot = B.slot();
|
|
B.entryAllocas.push(`${slot} = alloca ptr`);
|
|
B.line(`store ptr ${value}, ptr ${slot}`);
|
|
const lProbe = B.newLabel("dcr.has");
|
|
const lJoin = B.newLabel("dcr.hasj");
|
|
B.condBr(probe, lProbe, lJoin);
|
|
B.startBlock(lProbe);
|
|
const own = this.objGetLit(B, d, key);
|
|
const missing = B.tmp();
|
|
const chosen = B.tmp();
|
|
B.line(`${missing} = icmp eq ptr ${own}, null`);
|
|
B.line(
|
|
`${chosen} = select i1 ${missing}, ptr ${this.host.absentInstanceRef(t.unionId)}, ptr ${value}`,
|
|
);
|
|
B.line(`store ptr ${chosen}, ptr ${slot}`);
|
|
B.br(lJoin);
|
|
B.startBlock(lJoin);
|
|
const out = B.tmp();
|
|
B.line(`${out} = load ptr, ptr ${slot}`);
|
|
return out;
|
|
}
|
|
|
|
/** Ordinary property lookup, including inherited data and accessors (+1). */
|
|
private objReadLit(B: BlockBuilder, d: string, key: string, held = false): string {
|
|
const read = held ? "scr_dyn_obj_read_held" : "scr_dyn_obj_read";
|
|
this.host.declare(`declare ptr @${read}(ptr, ptr, ${this.S})`);
|
|
const value = B.tmp();
|
|
B.line(
|
|
`${value} = call ptr @${read}(ptr ${d}, ptr ${this.host.cstr(key)}, ${this.S} ${Buffer.byteLength(key, "utf8")})`,
|
|
);
|
|
return value;
|
|
}
|
|
|
|
/** A member read for a matcher or builder of `t`: values that can reach a
|
|
* recursive shape go through the runtime's held read, so its visit
|
|
* tracking does not count the read's reference. Returns the value and
|
|
* the matching release. */
|
|
private memberRead(B: BlockBuilder, key: string, t: IrType): { value: string; release: string } {
|
|
// An accessor slot reads the getter or setter of the accessor property
|
|
// the checked-dynamic copy holds for it (see accessorSlotProp).
|
|
const slot = accessorSlotProp(key);
|
|
if (slot !== null) {
|
|
this.host.declare(`declare ptr @scr_dyn_obj_accessor_fn(ptr, ptr, ${this.S}, i1 zeroext)`);
|
|
this.host.declare(`declare void @scr_dyn_release(ptr)`);
|
|
const fn = B.tmp();
|
|
B.line(
|
|
`${fn} = call ptr @scr_dyn_obj_accessor_fn(ptr %d, ptr ${this.host.cstr(slot.prop)}, ${this.S} ${Buffer.byteLength(slot.prop, "utf8")}, i1 ${slot.kind === "set" ? "true" : "false"})`,
|
|
);
|
|
return { value: fn, release: "scr_dyn_release" };
|
|
}
|
|
const held = this.isRecursiveShape(t) || this.holdsRecursiveShape(t);
|
|
const release = held ? "scr_dyn_release_held" : "scr_dyn_release";
|
|
this.host.declare(`declare void @${release}(ptr)`);
|
|
return { value: this.objReadLit(B, "%d", key, held), release };
|
|
}
|
|
|
|
/** Appends a borrowed string, preserving its full byte length. */
|
|
private putScrStr(B: BlockBuilder, buf: string, s: string): void {
|
|
this.host.declare(`declare void @scr_jb_put_str(ptr, ptr)`);
|
|
B.line(`call void @scr_jb_put_str(ptr ${buf}, ptr ${s})`);
|
|
}
|
|
|
|
/** Fixed text and JSON tokens hold no lone surrogate, so they append
|
|
* without the builder's surrogate-pair seam check. */
|
|
private puts(B: BlockBuilder, buf: string, text: string): void {
|
|
this.host.declare(`declare void @scr_jb_write_utf8(ptr, ptr, ${this.S})`);
|
|
B.line(
|
|
`call void @scr_jb_write_utf8(ptr ${buf}, ptr ${this.host.cstr(text)}, ${this.S} ${Buffer.byteLength(text, "utf8")}) ; ${JSON.stringify(text)}`,
|
|
);
|
|
}
|
|
|
|
/** A size_t counting loop: emits header/body, calls `body(i)`, closes.
|
|
* The body must not terminate its final block. */
|
|
private i64Loop(
|
|
B: BlockBuilder,
|
|
hint: string,
|
|
limit: string,
|
|
body: (i: string, brNext: () => void, next: string) => void,
|
|
): void {
|
|
const iSlot = B.slot();
|
|
B.entryAllocas.push(`${iSlot} = alloca ${this.S}`);
|
|
B.line(`store ${this.S} 0, ptr ${iSlot}`);
|
|
const lc = B.newLabel(`${hint}.c`);
|
|
const lb = B.newLabel(`${hint}.b`);
|
|
const ln = B.newLabel(`${hint}.n`);
|
|
const le = B.newLabel(`${hint}.e`);
|
|
B.br(lc);
|
|
B.startBlock(lc);
|
|
const i = B.tmp();
|
|
const cont = B.tmp();
|
|
B.line(`${i} = load ${this.S}, ptr ${iSlot}`);
|
|
B.line(`${cont} = icmp ult ${this.S} ${i}, ${limit}`);
|
|
B.condBr(cont, lb, le);
|
|
B.startBlock(lb);
|
|
body(i, () => B.br(ln), ln);
|
|
B.br(ln);
|
|
B.startBlock(ln);
|
|
const i2 = B.tmp();
|
|
B.line(`${i2} = add ${this.S} ${i}, 1`);
|
|
B.line(`store ${this.S} ${i2}, ptr ${iSlot}`);
|
|
B.br(lc);
|
|
B.startBlock(le);
|
|
}
|
|
|
|
/** Emit the standard in-helper pending check: on a pending exception
|
|
* run `cleanup` and return `dummy`. */
|
|
/** The failure block of a union dynCheck whose value matched no arm. A
|
|
* value of the one container kind exactly one arm can take (an object
|
|
* for the union's only record arm, an array for its only array arm,
|
|
* beside unit and scalar arms) runs that arm's builder, which throws the
|
|
* deepest failing path and the reason there (`expected string at
|
|
* $.order.owner.id, got undefined`) instead of the union's own outer
|
|
* path. Other values, and an arm builder that unexpectedly succeeds,
|
|
* fall through to the union's message. Only the failure path changes. */
|
|
private unionArmFailure(B: BlockBuilder, def: IrUnionDef, kind: string): void {
|
|
const scalar = (arm: IrType): boolean =>
|
|
isUnitType(arm) ||
|
|
arm.kind === "f64" ||
|
|
arm.kind === "bool" ||
|
|
arm.kind === "string" ||
|
|
arm.kind === "bigint";
|
|
const containers = def.arms.filter((arm) => !scalar(arm));
|
|
const arm = containers.length === 1 ? containers[0]! : null;
|
|
const container =
|
|
arm?.kind === "array"
|
|
? DYN_KIND.ARR
|
|
: arm?.kind === "record" && !this.host.recordsById.get(arm.shapeId)?.tuple
|
|
? DYN_KIND.OBJ
|
|
: null;
|
|
if (!arm || container === null) return;
|
|
const host = this.host;
|
|
const deep = B.newLabel("dcu.deep");
|
|
const outer = B.newLabel("dcu.outer");
|
|
const same = B.tmp();
|
|
B.line(`${same} = icmp eq i32 ${kind}, ${container}`);
|
|
B.condBr(same, deep, outer);
|
|
B.startBlock(deep);
|
|
const built = B.tmp();
|
|
B.line(`${built} = call ptr @${this.dynCheckHelper(arm)}(ptr %d, ptr %path)`);
|
|
this.pendingBail(B, "dcu.deep", () => {}, "ptr null");
|
|
const rc = vAdapters(host, arm);
|
|
B.line(`call void ${rc.release}(ptr ${built})`);
|
|
B.br(outer);
|
|
B.startBlock(outer);
|
|
}
|
|
|
|
private pendingBail(B: BlockBuilder, hint: string, cleanup: () => void, dummy: string): void {
|
|
const p = B.tmp();
|
|
for (const line of this.host.pendingTestLines(p)) B.line(line.trimStart());
|
|
const lu = B.newLabel(`${hint}.u`);
|
|
const lk = B.newLabel(`${hint}.k`);
|
|
B.condBr(p, lu, lk);
|
|
B.startBlock(lu);
|
|
cleanup();
|
|
B.terminate(`ret ${dummy}`);
|
|
B.startBlock(lk);
|
|
}
|
|
|
|
/** A capsule boxed from this class by this program carries the class's
|
|
* key constant itself: return its instance (+1) without comparing names
|
|
* or calling the capsule's retain adapter. Other values continue. */
|
|
private exactClassCapsule(B: BlockBuilder, t: IrType & { kind: "object" }): void {
|
|
const key = typeKey(t);
|
|
const present = B.tmp();
|
|
const inspect = B.newLabel("dc.exact.inspect");
|
|
const capsule = B.newLabel("dc.exact.capsule");
|
|
const hit = B.newLabel("dc.exact.hit");
|
|
const miss = B.newLabel("dc.exact.miss");
|
|
B.line(`${present} = icmp ne ptr %d, null`);
|
|
B.condBr(present, inspect, miss);
|
|
B.startBlock(inspect);
|
|
const typed = B.tmp();
|
|
B.line(`${typed} = icmp eq i32 ${this.kindOf(B, "%d")}, ${DYN_KIND.TYPED_REF}`);
|
|
B.condBr(typed, capsule, miss);
|
|
B.startBlock(capsule);
|
|
const keySlot = B.tmp(),
|
|
keyPtr = B.tmp(),
|
|
same = B.tmp();
|
|
B.line(
|
|
`${keySlot} = getelementptr inbounds i8, ptr %d, i64 ${this.abiOffset(40, 28)} ; ->v.typed_ref.type_key`,
|
|
);
|
|
B.line(`${keyPtr} = load ptr, ptr ${keySlot}`);
|
|
B.line(`${same} = icmp eq ptr ${keyPtr}, ${this.host.cstr(key)}`);
|
|
B.condBr(same, hit, miss);
|
|
B.startBlock(hit);
|
|
const object = this.payloadOf(B, "%d", "ptr");
|
|
const retained = B.tmp();
|
|
B.line(`${retained} = call ptr ${vAdapters(this.host, t).retain}(ptr ${object})`);
|
|
B.terminate(`ret ptr ${retained}`);
|
|
B.startBlock(miss);
|
|
}
|
|
|
|
/* ── dynMatchHelper (walkers.ts, ported) ──────────────────────── */
|
|
|
|
/** Native subclass capsules also satisfy a checked base-class slot.
|
|
* Class layouts share their base prefix, so unboxing retains the same
|
|
* pointer and virtual dispatch still uses the object's original vtable. */
|
|
private typedRefMatches(B: BlockBuilder, t: IrType): string {
|
|
return this.typedRefMatchesAny(
|
|
B,
|
|
isDynTypedRefType(t)
|
|
? this.host.classSubtypes(t.className).map((className) => ({ kind: "object", className }))
|
|
: [t],
|
|
);
|
|
}
|
|
|
|
/** `scr_dyn_record_view(d, sources, n, key, key_len, new)` for
|
|
* view target `targetId`: the view (+1) of the record a capsule `%d`
|
|
* holds when its type is one of the target's sources, else NULL. The
|
|
* sources table lists each source's type key and the byte offsets of
|
|
* the target's fields in it. */
|
|
private recordViewCall(B: BlockBuilder, targetId: string): string {
|
|
const host = this.host;
|
|
let table = this.recordViewTables.get(targetId);
|
|
const sources = host.recordViews!.sources(targetId);
|
|
const target = host.recordsById.get(targetId)!;
|
|
if (table === undefined) {
|
|
table = `@sc_rv_${this.recordViewTables.size}`;
|
|
this.recordViewTables.set(targetId, table);
|
|
const S = this.S;
|
|
const entries = sources.map((sourceId, k) => {
|
|
const source = host.recordsById.get(sourceId)!;
|
|
const struct = mangleRecordStruct(sourceId);
|
|
const base = host.nullableUnions.fieldBase(sourceId);
|
|
const offsets = target.fields.map((field) => {
|
|
const index = source.fields.findIndex((f) => f.name === field.name);
|
|
// A partial view's target field the source does not declare has
|
|
// no slot: all ones (SIZE_MAX) marks it absent (record-views.ts).
|
|
if (index < 0) return `${S} -1`;
|
|
return `${S} ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 0, i32 ${index + base}) to ${S})`;
|
|
});
|
|
const offsetTable = `${table}_${k}`;
|
|
this.defs.push(
|
|
`${offsetTable} = internal constant [${offsets.length} x ${S}] [${offsets.join(", ")}]`,
|
|
);
|
|
const key = typeKey({ kind: "record", shapeId: sourceId });
|
|
return `{ ptr, ${S}, ptr } { ptr ${host.cstr(key)}, ${S} ${Buffer.byteLength(key, "utf8")}, ptr ${offsetTable} }`;
|
|
});
|
|
this.defs.push(
|
|
`${table} = internal constant [${entries.length} x { ptr, ${S}, ptr }] [${entries.join(", ")}]`,
|
|
``,
|
|
);
|
|
}
|
|
const key = typeKey({ kind: "record", shapeId: targetId });
|
|
host.declare(`declare ptr @scr_dyn_record_view(ptr, ptr, ${this.S}, ptr, ${this.S}, ptr)`);
|
|
const view = B.tmp();
|
|
B.line(
|
|
`${view} = call ptr @scr_dyn_record_view(ptr %d, ptr ${table}, ${this.S} ${sources.length}, ptr ${host.cstr(key)}, ${this.S} ${Buffer.byteLength(key, "utf8")}, ptr @${mangleRecordNew(targetId)})`,
|
|
);
|
|
return view;
|
|
}
|
|
|
|
private typedRefMatchesAny(B: BlockBuilder, types: readonly IrType[]): string {
|
|
this.host.declare(`declare zeroext i1 @scr_dyn_typed_ref_is(ptr, ptr, ${this.S})`);
|
|
let matched = "false";
|
|
for (const type of types) {
|
|
const key = typeKey(type);
|
|
const next = B.tmp();
|
|
B.line(
|
|
`${next} = call zeroext i1 @scr_dyn_typed_ref_is(ptr %d, ptr ${this.host.cstr(key)}, ${this.S} ${Buffer.byteLength(key, "utf8")})`,
|
|
);
|
|
if (matched === "false") matched = next;
|
|
else {
|
|
const either = B.tmp();
|
|
B.line(`${either} = or i1 ${matched}, ${next}`);
|
|
matched = either;
|
|
}
|
|
}
|
|
return matched;
|
|
}
|
|
|
|
/** `sc_dm_<n>(ptr d) -> i1` — does this dyn fit T? Never throws. */
|
|
dynMatchHelper(t: IrType, preserveRefs = false): string {
|
|
const key = checkedConversionKey(t);
|
|
const matcherKey = preserveRefs ? `identity:${key}` : key;
|
|
const existing = this.dynMatchers.get(matcherKey);
|
|
if (existing) return existing;
|
|
const name = `sc_dm_${this.dynMatchers.size}`;
|
|
this.dynMatchers.set(matcherKey, name);
|
|
this.host.journalAllocated?.(name, "dyn.dynMatchHelper", [t, preserveRefs]);
|
|
const codec = textCodecRecordClass(t, (id) => this.host.recordsById.get(id));
|
|
if (codec !== null) {
|
|
// A codec matches only its own handle box.
|
|
const B = new BlockBuilder();
|
|
const matched = this.handleTagMatches(B, TEXT_CODEC_TAG_NUM[codec]);
|
|
B.terminate(`ret i1 ${matched}`);
|
|
this.defs.push(
|
|
`define internal zeroext i1 @${name}(ptr %d) ${FN_ATTRS} { ; matches ${key}`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
const recursive = this.isRecursiveShape(t);
|
|
const wrapped = recursive || this.holdsRecursiveShape(t);
|
|
const B = new BlockBuilder();
|
|
if (isRefCounted(t) && t.kind !== "dyn") {
|
|
const matched = this.typedRefMatches(B, t);
|
|
const lRef = B.newLabel("dm.tr");
|
|
const lNext = B.newLabel("dm.nt");
|
|
B.condBr(matched, lRef, lNext);
|
|
B.startBlock(lRef);
|
|
B.terminate(`ret i1 true`);
|
|
B.startBlock(lNext);
|
|
}
|
|
// A union matches a capsule of another type through its arms below: a
|
|
// class capsule in a `C | undefined` field matches the `C` arm.
|
|
if (t.kind !== "dyn" && t.kind !== "union") {
|
|
const view = this.viewFn(wrapped);
|
|
this.host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
|
const kind = this.kindOf(B, "%d");
|
|
const capsule = B.tmp();
|
|
B.line(`${capsule} = icmp eq i32 ${kind}, ${DYN_KIND.TYPED_REF}`);
|
|
const lCapsule = B.newLabel("dm.tr.mat");
|
|
const lPlain = B.newLabel("dm.tr.plain");
|
|
B.condBr(capsule, lCapsule, lPlain);
|
|
B.startBlock(lCapsule);
|
|
// A class target matches only capsules of that class (tested above):
|
|
// a materialized view is a plain object, which never carries a class
|
|
// brand, so materializing one cannot change the answer.
|
|
if (preserveRefs || (t.kind === "object" && t.className !== "%Error")) {
|
|
B.terminate(`ret i1 false`);
|
|
} else {
|
|
const materialized = B.tmp();
|
|
const matched = B.tmp();
|
|
B.line(`${materialized} = call ptr @${view}(ptr %d)`);
|
|
B.line(`${matched} = call zeroext i1 @${name}(ptr ${materialized})`);
|
|
B.line(`call void @scr_dyn_release_v(ptr ${materialized})`);
|
|
B.terminate(`ret i1 ${matched}`);
|
|
}
|
|
B.startBlock(lPlain);
|
|
}
|
|
const kindIs = (k: number): void => {
|
|
const kd = this.kindOf(B, "%d");
|
|
const r = B.tmp();
|
|
B.line(`${r} = icmp eq i32 ${kd}, ${k}`);
|
|
B.terminate(`ret i1 ${r}`);
|
|
};
|
|
switch (t.kind) {
|
|
case "date": {
|
|
this.host.declare(`declare zeroext i1 @scr_dyn_native_date_is(ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call zeroext i1 @scr_dyn_native_date_is(ptr %d)`);
|
|
B.terminate(`ret i1 ${r}`);
|
|
break;
|
|
}
|
|
case "url":
|
|
case "searchParams": {
|
|
const native = t.kind === "url" ? "url" : "search_params";
|
|
this.host.declare(`declare zeroext i1 @scr_dyn_native_${native}_is(ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call zeroext i1 @scr_dyn_native_${native}_is(ptr %d)`);
|
|
B.terminate(`ret i1 ${r}`);
|
|
break;
|
|
}
|
|
case "regex": {
|
|
this.host.declare(`declare zeroext i1 @scr_dyn_native_regex_is(ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call zeroext i1 @scr_dyn_native_regex_is(ptr %d)`);
|
|
B.terminate(`ret i1 ${r}`);
|
|
break;
|
|
}
|
|
case "map":
|
|
case "set": {
|
|
this.host.declare(`declare zeroext i1 @scr_dyn_native_collection_is(ptr, i32, ptr)`);
|
|
const r = B.tmp();
|
|
const typed =
|
|
t.kind === "map" ? t.key.kind !== "dyn" || t.value.kind !== "dyn" : t.elem.kind !== "dyn";
|
|
B.line(
|
|
`${r} = call zeroext i1 @scr_dyn_native_collection_is(ptr %d, i32 ${t.kind === "map" ? 1 : 0}, ptr ${typed ? this.host.cstr(typeKey(t)) : "null"})`,
|
|
);
|
|
B.terminate(`ret i1 ${r}`);
|
|
break;
|
|
}
|
|
case "bigint":
|
|
kindIs(DYN_KIND.BIGINT);
|
|
break;
|
|
case "promise":
|
|
kindIs(DYN_KIND.PROMISE);
|
|
break;
|
|
case "procStream": {
|
|
this.host.declare(`declare zeroext i1 @scr_dyn_process_stdio_is(ptr)`);
|
|
const matched = B.tmp();
|
|
B.line(`${matched} = call zeroext i1 @scr_dyn_process_stdio_is(ptr %d)`);
|
|
B.terminate(`ret i1 ${matched}`);
|
|
break;
|
|
}
|
|
case "symbol":
|
|
kindIs(DYN_KIND.SYMBOL);
|
|
break;
|
|
case "f64":
|
|
kindIs(DYN_KIND.NUM);
|
|
break;
|
|
case "string":
|
|
kindIs(DYN_KIND.STR);
|
|
break;
|
|
case "bool":
|
|
kindIs(DYN_KIND.BOOL);
|
|
break;
|
|
case "nullT":
|
|
kindIs(DYN_KIND.NULL);
|
|
break;
|
|
case "undefinedT":
|
|
kindIs(DYN_KIND.UNDEF);
|
|
break;
|
|
case "dyn":
|
|
// An `unknown` target: every dyn value fits, undefined included.
|
|
B.terminate(`ret i1 true`);
|
|
break;
|
|
case "bytes": {
|
|
this.host.declare(`declare zeroext i1 @scr_dyn_bytes_is(ptr, i32)`);
|
|
const matched = B.tmp();
|
|
B.line(
|
|
`${matched} = call zeroext i1 @scr_dyn_bytes_is(ptr %d, i32 ${BYTES_ELEM_NUM[t.elem]})`,
|
|
);
|
|
B.terminate(`ret i1 ${matched}`);
|
|
break;
|
|
}
|
|
case "func":
|
|
this.host.declare(`declare zeroext i1 @scr_dyn_is_callable(ptr)`);
|
|
{
|
|
const matched = B.tmp();
|
|
B.line(`${matched} = call zeroext i1 @scr_dyn_is_callable(ptr %d)`);
|
|
B.terminate(`ret i1 ${matched}`);
|
|
}
|
|
break;
|
|
case "classval": {
|
|
this.host.declare(`declare zeroext i1 @scr_dyn_class_is(ptr, ptr)`);
|
|
const matched = B.tmp();
|
|
B.line(
|
|
`${matched} = call zeroext i1 @scr_dyn_class_is(ptr %d, ptr ${this.host.cstr(key)})`,
|
|
);
|
|
B.terminate(`ret i1 ${matched}`);
|
|
break;
|
|
}
|
|
case "generator":
|
|
// Only an exact native capsule carries these channel types.
|
|
B.terminate(`ret i1 false`);
|
|
break;
|
|
case "object": {
|
|
if (t.className !== "%Error") {
|
|
// Exact class capsules returned true before materialization.
|
|
// Plain dyn objects carry no class brand.
|
|
B.terminate(`ret i1 false`);
|
|
break;
|
|
}
|
|
const kd = this.kindOf(B, "%d");
|
|
const isObj = B.tmp();
|
|
B.line(`${isObj} = icmp eq i32 ${kd}, ${DYN_KIND.OBJ}`);
|
|
const lObj = B.newLabel("dm.err.obj");
|
|
const lFail = B.newLabel("dm.err.fail");
|
|
B.condBr(isObj, lObj, lFail);
|
|
B.startBlock(lObj);
|
|
const marker = this.objGetLit(B, "%d", "%error");
|
|
const present = B.tmp();
|
|
B.line(`${present} = icmp ne ptr ${marker}, null`);
|
|
B.terminate(`ret i1 ${present}`);
|
|
B.startBlock(lFail);
|
|
B.terminate(`ret i1 false`);
|
|
break;
|
|
}
|
|
case "record": {
|
|
const shape = this.host.recordsById.get(t.shapeId);
|
|
if (!shape)
|
|
throw new InternalCompilerError(
|
|
`llvm emitter bug: dynCheck of unknown shape ${t.shapeId}`,
|
|
);
|
|
const fail = B.newLabel("dm.f");
|
|
const kd = this.kindOf(B, "%d");
|
|
// A tuple matches a JSON ARRAY of EXACTLY its arity, positionally.
|
|
if (shape.tuple) {
|
|
const byIndex = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
|
|
const isArr = B.tmp();
|
|
B.line(`${isArr} = icmp eq i32 ${kd}, ${DYN_KIND.ARR}`);
|
|
const l1 = B.newLabel("dm.a");
|
|
B.condBr(isArr, l1, fail);
|
|
B.startBlock(l1);
|
|
const len = this.lenOf(B, "%d");
|
|
const lenOk = B.tmp();
|
|
B.line(`${lenOk} = icmp eq ${this.S} ${len}, ${byIndex.length}`);
|
|
const l2 = B.newLabel("dm.l");
|
|
B.condBr(lenOk, l2, fail);
|
|
B.startBlock(l2);
|
|
const items = this.itemsOf(B, "%d");
|
|
for (const [i, f] of byIndex.entries()) {
|
|
const e = this.itemAt(B, items, `${i}`);
|
|
const m = B.tmp();
|
|
B.line(
|
|
`${m} = call zeroext i1 @${this.dynMatchHelper(f.type, preserveRefs)}(ptr ${e})`,
|
|
);
|
|
const ln = B.newLabel("dm.i");
|
|
B.condBr(m, ln, fail);
|
|
B.startBlock(ln);
|
|
}
|
|
B.terminate(`ret i1 true`);
|
|
B.startBlock(fail);
|
|
B.terminate(`ret i1 false`);
|
|
break;
|
|
}
|
|
const isObj = B.tmp();
|
|
B.line(`${isObj} = icmp eq i32 ${kd}, ${DYN_KIND.OBJ}`);
|
|
const l0 = B.newLabel("dm.o");
|
|
let readable = isObj;
|
|
if (!shape.indexValue) {
|
|
const proxy = B.tmp();
|
|
readable = B.tmp();
|
|
B.line(`${proxy} = icmp eq i32 ${kd}, ${DYN_KIND.PROXY}`);
|
|
B.line(`${readable} = or i1 ${isObj}, ${proxy}`);
|
|
}
|
|
B.condBr(readable, l0, fail);
|
|
B.startBlock(l0);
|
|
// dyn ('unknown') fields match ANY value, present or missing. The
|
|
// order index is no key.
|
|
for (const f of shape.fields) {
|
|
if (f.type.kind === "dyn" || f.name === KEY_ORDER_FIELD) continue;
|
|
const { value: m, release } = this.memberRead(B, f.name, f.type);
|
|
const lNext = B.newLabel("dm.n");
|
|
this.pendingBail(
|
|
B,
|
|
"dm.read",
|
|
() => B.line(`call void @${release}(ptr ${m})`),
|
|
"i1 false",
|
|
);
|
|
const ok = B.tmp();
|
|
B.line(`${ok} = call zeroext i1 @${this.dynMatchHelper(f.type, preserveRefs)}(ptr ${m})`);
|
|
B.line(`call void @${release}(ptr ${m})`);
|
|
B.condBr(ok, lNext, fail);
|
|
B.startBlock(lNext);
|
|
}
|
|
// Index-signature shapes: UNDECLARED keys must fit the overflow
|
|
// value type. A dyn value type accepts anything.
|
|
if (shape.indexValue && shape.indexValue.kind !== "dyn") {
|
|
this.host.declare(`declare i32 @memcmp(ptr, ptr, ${this.S})`);
|
|
const n = this.lenOf(B, "%d");
|
|
this.i64Loop(B, "dm.ov", n, (i, brNext) => {
|
|
const ent = this.entryAt(B, "%d", i);
|
|
// Skip declared keys.
|
|
for (const f of shape.fields) {
|
|
const klen = Buffer.byteLength(f.name, "utf8");
|
|
const lenEq = B.tmp();
|
|
B.line(`${lenEq} = icmp eq ${this.S} ${ent.keyLen}, ${klen}`);
|
|
const lCmp = B.newLabel("dm.kc");
|
|
const lNo = B.newLabel("dm.kn");
|
|
B.condBr(lenEq, lCmp, lNo);
|
|
B.startBlock(lCmp);
|
|
const c = B.tmp();
|
|
const same = B.tmp();
|
|
B.line(
|
|
`${c} = call i32 @memcmp(ptr ${ent.key}, ptr ${this.host.cstr(f.name)}, ${this.S} ${klen}) ; ${llvmCommentText(f.name)}`,
|
|
);
|
|
B.line(`${same} = icmp eq i32 ${c}, 0`);
|
|
const lNo2 = B.newLabel("dm.kn");
|
|
const skip = B.newLabel("dm.ks");
|
|
B.condBr(same, skip, lNo2);
|
|
B.startBlock(skip);
|
|
brNext();
|
|
B.startBlock(lNo2);
|
|
B.br(lNo);
|
|
B.startBlock(lNo);
|
|
}
|
|
const ok = B.tmp();
|
|
B.line(
|
|
`${ok} = call zeroext i1 @${this.dynMatchHelper(shape.indexValue!, preserveRefs)}(ptr ${ent.value})`,
|
|
);
|
|
const lOk = B.newLabel("dm.vo");
|
|
B.condBr(ok, lOk, fail);
|
|
B.startBlock(lOk);
|
|
});
|
|
}
|
|
B.terminate(`ret i1 true`);
|
|
B.startBlock(fail);
|
|
B.terminate(`ret i1 false`);
|
|
break;
|
|
}
|
|
case "array": {
|
|
const m = this.dynMatchHelper(t.elem, preserveRefs);
|
|
const fail = B.newLabel("dm.f");
|
|
const kd = this.kindOf(B, "%d");
|
|
const isArr = B.tmp();
|
|
B.line(`${isArr} = icmp eq i32 ${kd}, ${DYN_KIND.ARR}`);
|
|
const l0 = B.newLabel("dm.a");
|
|
B.condBr(isArr, l0, fail);
|
|
B.startBlock(l0);
|
|
const n = this.lenOf(B, "%d");
|
|
const items = this.itemsOf(B, "%d");
|
|
this.i64Loop(B, "dm.el", n, (i, brNext, next) => {
|
|
this.host.declare(`declare zeroext i1 @scr_dyn_arr_has_index(ptr, ${this.S})`);
|
|
const present = B.tmp();
|
|
B.line(`${present} = call zeroext i1 @scr_dyn_arr_has_index(ptr %d, ${this.S} ${i})`);
|
|
const valueLabel = B.newLabel("dm.present");
|
|
const holeLabel = B.newLabel("dm.hole");
|
|
B.condBr(present, valueLabel, holeLabel);
|
|
B.startBlock(holeLabel);
|
|
brNext();
|
|
B.startBlock(valueLabel);
|
|
const e = this.itemAt(B, items, i);
|
|
if (t.optionalElements) {
|
|
// A `(T | undefined)[]` stored as T elements takes undefined
|
|
// elements as well.
|
|
const undef = B.tmp();
|
|
const checkLabel = B.newLabel("dm.check");
|
|
B.line(`${undef} = icmp eq i32 ${this.kindOf(B, e)}, ${DYN_KIND.UNDEF}`);
|
|
B.condBr(undef, next, checkLabel);
|
|
B.startBlock(checkLabel);
|
|
}
|
|
const ok = B.tmp();
|
|
B.line(`${ok} = call zeroext i1 @${m}(ptr ${e})`);
|
|
const lOk = B.newLabel("dm.eo");
|
|
B.condBr(ok, lOk, fail);
|
|
B.startBlock(lOk);
|
|
});
|
|
B.terminate(`ret i1 true`);
|
|
B.startBlock(fail);
|
|
B.terminate(`ret i1 false`);
|
|
break;
|
|
}
|
|
case "union": {
|
|
const def = this.host.unionsById.get(t.unionId);
|
|
if (!def)
|
|
throw new InternalCompilerError(
|
|
`llvm emitter bug: dynCheck of unknown union ${t.unionId}`,
|
|
);
|
|
// Arms in canonical order; any full match answers true.
|
|
const yes = B.newLabel("dm.y");
|
|
const values = this.dispatchValues(def);
|
|
if (values !== null) {
|
|
const no = B.newLabel("dm.n");
|
|
this.unionArmsByDiscriminant(
|
|
B,
|
|
def,
|
|
values,
|
|
def.arms.map((_, tag) => tag),
|
|
def.arms.map(() => yes),
|
|
no,
|
|
preserveRefs,
|
|
`@${name}.cases`,
|
|
);
|
|
B.startBlock(no);
|
|
} else
|
|
for (let tag = 0; tag < def.arms.length; tag++) {
|
|
const ln = B.newLabel("dm.n");
|
|
this.unionArmMatch(B, def, tag, yes, ln, preserveRefs);
|
|
B.startBlock(ln);
|
|
}
|
|
B.terminate(`ret i1 false`);
|
|
B.startBlock(yes);
|
|
B.terminate(`ret i1 true`);
|
|
break;
|
|
}
|
|
default: {
|
|
const h = DYN_HANDLE_KINDS.get(t.kind);
|
|
if (!h) throw new LlvmUnsupportedError(`dynMatch:${t.kind}`);
|
|
const kd = this.kindOf(B, "%d");
|
|
const isHandle = B.tmp();
|
|
B.line(`${isHandle} = icmp eq i32 ${kd}, ${DYN_KIND.HANDLE}`);
|
|
const lHandle = B.newLabel("dm.handle");
|
|
const lNo = B.newLabel("dm.handle.no");
|
|
B.condBr(isHandle, lHandle, lNo);
|
|
B.startBlock(lHandle);
|
|
const tagPtr = B.tmp();
|
|
const tag = B.tmp();
|
|
B.line(
|
|
`${tagPtr} = getelementptr inbounds i8, ptr %d, i64 ${this.abiOffset(24, 20)} ; ->v.handle.tag`,
|
|
);
|
|
B.line(`${tag} = load i32, ptr ${tagPtr}`);
|
|
const matched = B.tmp();
|
|
B.line(`${matched} = icmp eq i32 ${tag}, ${DYN_HANDLE_TAG_NUM[t.kind]}`);
|
|
B.terminate(`ret i1 ${matched}`);
|
|
B.startBlock(lNo);
|
|
B.terminate(`ret i1 false`);
|
|
break;
|
|
}
|
|
}
|
|
this.defs.push(
|
|
`define internal zeroext i1 @${wrapped ? `${name}_body` : name}(ptr %d) ${FN_ATTRS} { ; matches ${key}`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
if (wrapped) this.emitVisitWrapper(name, t, recursive ? "match" : "visit");
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/* ── schema-directed JSON.parse ───────────────────────────────── */
|
|
|
|
private readonly jsonSchemas = new Map<string, string>();
|
|
private readonly jsonParsers = new Map<string, string>();
|
|
private readonly recordNameTables = new Map<string, { names: string; lens: string }>();
|
|
/** Some emitted helper can keep a record's undeclared source: a checked
|
|
* record builder that notes it, or a JSON schema of a closed record
|
|
* (alloc.ts RECORD_SOURCES_GATE). */
|
|
keepsRecordSources = false;
|
|
|
|
/** Targets the one-pass parser (scr_json_parse_schema) can build
|
|
* directly: number, boolean and string leaves inside arrays and records
|
|
* (no tuple, internal '%' slot or `__proto__` member; at most 64 declared
|
|
* fields, and at least one unless an index signature collects the
|
|
* members), record members typed as unions of those leaves, records and
|
|
* arrays, null and undefined (optional and nullable members; see
|
|
* jsonUnionSupported), discriminated unions of records anywhere (see
|
|
* jsonDiscriminatedSupported), and index signatures whose value type is
|
|
* not a union. Everything else keeps the checked-dynamic route alone. */
|
|
private jsonSchemaSupported(t: IrType, visiting: Set<string>, member = false): boolean {
|
|
switch (t.kind) {
|
|
case "f64":
|
|
case "bool":
|
|
case "string":
|
|
return true;
|
|
case "array":
|
|
return this.jsonSchemaSupported(t.elem, visiting);
|
|
case "record": {
|
|
if (visiting.has(t.shapeId)) return true;
|
|
const shape = this.host.recordsById.get(t.shapeId);
|
|
if (!shape || shape.tuple) return false;
|
|
const index = shape.indexValue;
|
|
if (shape.fields.length > 64 || (shape.fields.length === 0 && !index)) return false;
|
|
visiting.add(t.shapeId);
|
|
return (
|
|
shape.fields.every(
|
|
(f) =>
|
|
f.name === KEY_ORDER_FIELD ||
|
|
(!f.name.startsWith("%") &&
|
|
f.name !== "__proto__" &&
|
|
this.jsonSchemaSupported(f.type, visiting, true)),
|
|
) &&
|
|
(!index || this.jsonSchemaSupported(index, visiting))
|
|
);
|
|
}
|
|
case "union": {
|
|
const def = this.host.unionsById.get(t.unionId);
|
|
if (def?.discriminant) return this.jsonDiscriminatedSupported(def, visiting);
|
|
return member && this.jsonUnionSupported(t, visiting);
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** A discriminated union the one-pass parser writes directly, as a record
|
|
* member, an array element or the whole document: record arms that each
|
|
* name the string literals their discriminant member holds, every
|
|
* literal naming one arm, plus null and undefined arms. An object then
|
|
* selects the one arm whose literal its discriminant member holds, the
|
|
* only arm the checked route's literal tests leave to match
|
|
* (unionArmsByDiscriminant); an object holding no such literal declines. */
|
|
private jsonDiscriminatedSupported(def: IrUnionDef, visiting: Set<string>): boolean {
|
|
const discriminant = def.discriminant!;
|
|
const literals = new Set<string>();
|
|
let records = 0;
|
|
const supported = def.arms.every((arm, tag) => {
|
|
if (arm.kind === "nullT" || arm.kind === "undefinedT") return true;
|
|
if (arm.kind !== "record") return false;
|
|
records++;
|
|
const guard = discriminant.cases.find((candidate) => candidate.tag === tag);
|
|
if (!guard || guard.values.length === 0) return false;
|
|
for (const value of guard.values) {
|
|
if (typeof value !== "string" || literals.has(value)) return false;
|
|
literals.add(value);
|
|
}
|
|
return this.jsonSchemaSupported(arm, visiting);
|
|
});
|
|
return supported && records >= 2;
|
|
}
|
|
|
|
/** A union record member the one-pass parser writes directly: every arm
|
|
* a number, boolean, string, null, undefined, or a supported record or
|
|
* array, with at most one record and one array arm and no discriminant.
|
|
* Each JSON value kind then selects at most one arm, so the checked
|
|
* route's first full match is that arm, and a value that arm rejects
|
|
* fails every arm. */
|
|
private jsonUnionSupported(t: IrType & { kind: "union" }, visiting: Set<string>): boolean {
|
|
const def = this.host.unionsById.get(t.unionId);
|
|
if (!def || def.discriminant) return false;
|
|
let records = 0,
|
|
arrays = 0;
|
|
return def.arms.every((arm) => {
|
|
switch (arm.kind) {
|
|
case "f64":
|
|
case "bool":
|
|
case "string":
|
|
case "nullT":
|
|
case "undefinedT":
|
|
return true;
|
|
case "record":
|
|
return ++records === 1 && this.jsonSchemaSupported(arm, visiting);
|
|
case "array":
|
|
return ++arrays === 1 && this.jsonSchemaSupported(arm, visiting);
|
|
default:
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
/** The ScrJsonSchema constant for t (scr_runtime.h layout). Records list
|
|
* fields in declaration order — the order JSON.stringify writes — so the
|
|
* runtime's next-field guess usually matches without a scan. */
|
|
private jsonSchema(t: IrType): string {
|
|
const key = typeKey(t);
|
|
const existing = this.jsonSchemas.get(key);
|
|
if (existing) return existing;
|
|
const host = this.host;
|
|
const S = host.sizeType;
|
|
const name = `sc_jsch_${this.jsonSchemas.size}`;
|
|
this.jsonSchemas.set(key, name);
|
|
this.host.journalAllocated?.(name, "dyn.jsonSchema", [t]);
|
|
let body: string;
|
|
switch (t.kind) {
|
|
case "f64":
|
|
case "bool":
|
|
case "string": {
|
|
const kind = t.kind === "f64" ? 0 : t.kind === "bool" ? 1 : 2;
|
|
body = `${S} ${kind}, ${S} 0, ptr null, ptr null, ptr null, ptr null, ptr null, ptr null, ${S} 0`;
|
|
break;
|
|
}
|
|
case "array": {
|
|
const elem = this.jsonSchema(t.elem);
|
|
const ctor = `${name}_new`;
|
|
this.defs.push(
|
|
`define internal ptr @${ctor}(${S} %cap) ${FN_ATTRS} {`,
|
|
`entry:`,
|
|
` %a = ${arrNewCall(host, t.elem, "%cap")}`,
|
|
` ret ptr %a`,
|
|
`}`,
|
|
``,
|
|
);
|
|
body = `${S} 4, ${S} 0, ptr null, ptr @${elem}, ptr null, ptr @${ctor}, ptr ${vAdapters(host, t).release}, ptr null, ${S} 0`;
|
|
break;
|
|
}
|
|
case "record": {
|
|
const shape = host.recordsById.get(t.shapeId);
|
|
if (!shape)
|
|
throw new InternalCompilerError(
|
|
`llvm emitter bug: JSON schema of unknown shape ${t.shapeId}`,
|
|
);
|
|
const struct = mangleRecordStruct(t.shapeId);
|
|
const base = host.nullableUnions.fieldBase(t.shapeId);
|
|
const index = new Map(shape.fields.map((f, i) => [f.name, i + base]));
|
|
// The runtime names kept-source keys by this index (record-sources.ts).
|
|
// The order index is no key: parsed values keep the table's order
|
|
// and their source's own order.
|
|
const order = recordSourceFieldOrder(shape).filter((name) => name !== KEY_ORDER_FIELD);
|
|
if (recordMayKeepSource(shape)) this.keepsRecordSources = true;
|
|
const fieldTy = `{ ptr, ${S}, ${S}, ptr }`;
|
|
const entries = order.map((fieldName) => {
|
|
const field = shape.fields[index.get(fieldName)! - base]!;
|
|
const sub = this.jsonSchema(field.type);
|
|
const offset = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${index.get(fieldName)}) to ${S})`;
|
|
return `${fieldTy} { ptr ${host.cstr(fieldName)}, ${S} ${Buffer.byteLength(fieldName, "utf8")}, ${S} ${offset}, ptr @${sub} }`;
|
|
});
|
|
let fields = "null";
|
|
if (entries.length > 0) {
|
|
fields = `@${name}_fields`;
|
|
this.defs.push(
|
|
`${fields} = internal constant [${entries.length} x ${fieldTy}] [${entries.join(", ")}]`,
|
|
``,
|
|
);
|
|
}
|
|
// An index signature's undeclared members go to the overflow map
|
|
// after the declared fields.
|
|
const overflow = shape.indexValue
|
|
? {
|
|
elem: `@${this.jsonSchema(shape.indexValue)}`,
|
|
offset: `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${shape.fields.length + base}) to ${S})`,
|
|
}
|
|
: { elem: "null", offset: "0" };
|
|
body = `${S} 3, ${S} ${entries.length}, ptr ${fields}, ptr ${overflow.elem}, ptr @${mangleRecordNew(t.shapeId)}, ptr null, ptr ${vAdapters(host, t).release}, ptr null, ${S} ${overflow.offset}`;
|
|
break;
|
|
}
|
|
case "union": {
|
|
// A discriminated union can be a whole document, whose value the
|
|
// parser releases when text follows it.
|
|
const release = host.unionsById.get(t.unionId)?.discriminant
|
|
? vAdapters(host, t).release
|
|
: "null";
|
|
body = `${S} 5, ${S} 0, ptr null, ptr null, ptr null, ptr null, ptr ${release}, ptr @${this.jsonUnionSchema(t, name)}, ${S} 0`;
|
|
break;
|
|
}
|
|
default:
|
|
throw new InternalCompilerError(`llvm emitter bug: JSON schema of ${t.kind}`);
|
|
}
|
|
this.defs.push(
|
|
`@${name} = internal constant { ${S}, ${S}, ptr, ptr, ptr, ptr, ptr, ptr, ${S} } { ${body} }`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/** The ScrJsonUnion descriptor of a union member (scr_json.h layout):
|
|
* its slot representation, the arm each JSON value kind selects, and the
|
|
* values the runtime stores for null and for a missing member. Boxed
|
|
* unions build their boxes through `<name>_box`. A discriminated union
|
|
* also lists its object arms by discriminant literal (`<name>_cases`),
|
|
* and, because it can be an array element or a whole document, whose
|
|
* values are boxes unless the union is a pointer union, it keeps its null
|
|
* value and box whatever its field representation. */
|
|
private jsonUnionSchema(t: IrType & { kind: "union" }, name: string): string {
|
|
const host = this.host;
|
|
const S = host.sizeType;
|
|
const def = host.unionsById.get(t.unionId);
|
|
if (!def) throw new InternalCompilerError(`llvm emitter bug: JSON schema of ${t.unionId}`);
|
|
const nullable = host.nullableUnions.has(t.unionId);
|
|
const inline = host.inlineUnions.record(t);
|
|
const repr = nullable ? 0 : inline ? 1 : 2;
|
|
// Indexed by SCR_JARM_*: null, number, boolean, string, object, array, absent.
|
|
const armKinds = ["nullT", "f64", "bool", "string", "record", "array", "undefinedT"];
|
|
const tags = armKinds.map((kind) => def.arms.findIndex((arm) => arm.kind === kind));
|
|
const arms = armKinds.map((_, i) =>
|
|
i >= 1 && i <= 5 && tags[i]! >= 0
|
|
? `ptr @${this.jsonSchema(def.arms[tags[i]!]!)}`
|
|
: "ptr null",
|
|
);
|
|
const nullTag = tags[0]!;
|
|
const absentTag = tags[6]!;
|
|
const values = !inline || def.discriminant !== undefined;
|
|
const nullValue = values && nullTag >= 0 ? host.unitInstanceRef(t.unionId, nullTag) : "null";
|
|
const absentValue = values && absentTag >= 0 ? host.absentInstanceRef(t.unionId) : "null";
|
|
let box = "null";
|
|
if (repr === 2 || (def.discriminant && !nullable)) {
|
|
box = `@${name}_box`;
|
|
const lines = [`define internal ptr ${box}(i32 %tag, i64 %bits) ${FN_ATTRS} {`, `entry:`];
|
|
const cases: string[] = [];
|
|
const blocks: string[] = [];
|
|
def.arms.forEach((arm, tag) => {
|
|
if (arm.kind === "nullT" || arm.kind === "undefinedT") return;
|
|
cases.push(`i32 ${tag}, label %arm${tag}`);
|
|
blocks.push(`arm${tag}:`);
|
|
if (arm.kind === "f64") {
|
|
host.declare(`declare ptr @scr_union_new_f64(i32, double)`);
|
|
blocks.push(
|
|
` %f${tag} = bitcast i64 %bits to double`,
|
|
` %u${tag} = call ptr @scr_union_new_f64(i32 ${tag}, double %f${tag})`,
|
|
);
|
|
} else if (arm.kind === "bool") {
|
|
host.declare(`declare ptr @scr_union_new_bool(i32, i1 zeroext)`);
|
|
blocks.push(
|
|
` %b${tag} = trunc i64 %bits to i1`,
|
|
` %u${tag} = call ptr @scr_union_new_bool(i32 ${tag}, i1 zeroext %b${tag})`,
|
|
);
|
|
} else {
|
|
const rc = vAdapters(host, arm);
|
|
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
|
|
blocks.push(
|
|
` %p${tag} = inttoptr i64 %bits to ptr`,
|
|
` %u${tag} = call ptr @scr_union_new_ref(i32 ${tag}, ptr %p${tag}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, arm)})`,
|
|
);
|
|
}
|
|
blocks.push(` ret ptr %u${tag}`);
|
|
});
|
|
this.defs.push(
|
|
...lines,
|
|
` switch i32 %tag, label %none [ ${cases.join(" ")} ]`,
|
|
`none:`,
|
|
` unreachable`,
|
|
...blocks,
|
|
`}`,
|
|
``,
|
|
);
|
|
}
|
|
let cases = "null";
|
|
if (def.discriminant) {
|
|
const discriminant = def.discriminant;
|
|
const caseTy = `{ ptr, ${S}, ${S}, ptr }`;
|
|
const entries = discriminant.cases.flatMap((guard) =>
|
|
guard.values.map(
|
|
(value) =>
|
|
`${caseTy} { ptr ${host.cstr(String(value))}, ${S} ${Buffer.byteLength(String(value), "utf8")}, ${S} ${guard.tag}, ptr @${this.jsonSchema(def.arms[guard.tag]!)} }`,
|
|
),
|
|
);
|
|
const table = `@${name}_case_table`;
|
|
cases = `@${name}_cases`;
|
|
this.defs.push(
|
|
`${table} = internal constant [${entries.length} x ${caseTy}] [${entries.join(", ")}]`,
|
|
`${cases} = internal constant { ptr, ${S}, ${S}, ptr } { ptr ${host.cstr(discriminant.field)}, ${S} ${Buffer.byteLength(discriminant.field, "utf8")}, ${S} ${entries.length}, ptr ${table} }`,
|
|
``,
|
|
);
|
|
}
|
|
const descriptor = `${name}_union`;
|
|
this.defs.push(
|
|
`@${descriptor} = internal constant { ${S}, [7 x ${S}], [7 x ptr], ptr, ptr, ptr, ptr } { ${S} ${repr}, [7 x ${S}] [${tags.map((tag) => `${S} ${tag}`).join(", ")}], [7 x ptr] [${arms.join(", ")}], ptr ${nullValue}, ptr ${absentValue}, ptr ${box}, ptr ${cases} }`,
|
|
``,
|
|
);
|
|
return descriptor;
|
|
}
|
|
|
|
/** `record.sourceSpread(built, value)`: the built record keeps value's
|
|
* kept-source order over its own declared names. */
|
|
emitRecordSourceSpread(B: BlockBuilder, built: string, value: string, shapeId: string): void {
|
|
const host = this.host;
|
|
const shape = host.recordsById.get(shapeId)!;
|
|
this.keepsRecordSources = true;
|
|
const names = this.recordNameTable(shapeId, shape);
|
|
host.declare(`declare void @scr_record_source_spread(ptr, ptr, ptr, ptr, ${host.sizeType})`);
|
|
B.line(
|
|
`call void @scr_record_source_spread(ptr ${built}, ptr ${value}, ptr ${names.names}, ptr ${names.lens}, ${host.sizeType} ${shape.fields.length})`,
|
|
);
|
|
}
|
|
|
|
/** The declared-name tables scr_record_source_note scans: parallel
|
|
* constant arrays of field-name bytes and byte lengths in declaration
|
|
* order (the order the record serializes), interned per shape. Internal
|
|
* '%'-fields follow, so they never count as undeclared source keys. */
|
|
private recordNameTable(shapeId: string, shape: IrRecordShape): { names: string; lens: string } {
|
|
const existing = this.recordNameTables.get(shapeId);
|
|
if (existing) return existing;
|
|
const host = this.host;
|
|
const S = host.sizeType;
|
|
const base = `sc_rnames_${this.recordNameTables.size}`;
|
|
const table = { names: `@${base}`, lens: `@${base}_lens` };
|
|
this.recordNameTables.set(shapeId, table);
|
|
this.host.journalAllocated?.(base, "dyn.recordNameTable", [shapeId]);
|
|
const n = shape.fields.length;
|
|
const names = recordSourceFieldOrder(shape);
|
|
this.defs.push(
|
|
`@${base} = internal constant [${n} x ptr] [${names.map((name) => `ptr ${host.cstr(name)}`).join(", ")}]`,
|
|
`@${base}_lens = internal constant [${n} x ${S}] [${names.map((name) => `${S} ${Buffer.byteLength(name, "utf8")}`).join(", ")}]`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return table;
|
|
}
|
|
|
|
/** `sc_jp_<n>(ptr text) -> T` for `JSON.parse(text) as T`, or null when T
|
|
* is not a schema target. Tries the one-pass schema parser; when it
|
|
* declines (any input the checked route might reject or read
|
|
* differently), runs the checked-dynamic route — scr_json_parse plus the
|
|
* dynCheck builder — which reports the exact error. Returns +1, or null
|
|
* with the pending flag set. */
|
|
jsonParseHelper(t: IrType): string | null {
|
|
if (
|
|
t.kind !== "record" &&
|
|
t.kind !== "array" &&
|
|
!(t.kind === "union" && this.host.unionsById.get(t.unionId)?.discriminant)
|
|
)
|
|
return null;
|
|
const key = typeKey(t);
|
|
const existing = this.jsonParsers.get(key);
|
|
if (existing) return existing;
|
|
if (!this.jsonSchemaSupported(t, new Set())) return null;
|
|
const host = this.host;
|
|
const name = `sc_jp_${this.jsonParsers.size}`;
|
|
this.jsonParsers.set(key, name);
|
|
this.host.journalAllocated?.(name, "dyn.jsonParseHelper", [t]);
|
|
const schema = this.jsonSchema(t);
|
|
const check = this.dynCheckHelper(t);
|
|
host.declare(`declare ptr @scr_json_parse_schema(ptr, ptr)`);
|
|
host.declare(`declare ptr @scr_json_parse(ptr)`);
|
|
host.declare(`declare void @scr_dyn_release(ptr)`);
|
|
this.defs.push(
|
|
`define internal ptr @${name}(ptr %text) ${FN_ATTRS} { ; JSON.parse as ${llvmCommentText(key)}`,
|
|
`entry:`,
|
|
` %fast = call ptr @scr_json_parse_schema(ptr %text, ptr @${schema})`,
|
|
` %hit = icmp ne ptr %fast, null`,
|
|
` br i1 %hit, label %done, label %slow`,
|
|
`done:`,
|
|
` ret ptr %fast`,
|
|
`slow:`,
|
|
` %d = call ptr @scr_json_parse(ptr %text)`,
|
|
` %bad = icmp eq ptr %d, null`,
|
|
` br i1 %bad, label %fail, label %check`,
|
|
`fail:`,
|
|
` ret ptr null`,
|
|
`check:`,
|
|
` %v = call ptr @${check}(ptr %d, ptr null)`,
|
|
` call void @scr_dyn_release(ptr %d)`,
|
|
` ret ptr %v`,
|
|
`}`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/* ── dynCheckHelper (walkers.ts, ported) ──────────────────────── */
|
|
|
|
/** `sc_dc_<n>(ptr d, ptr path) -> T` — validate the checked-dynamic tree against T and
|
|
* BUILD the typed value (+1), or throw the catchable path-annotated
|
|
* TypeError and return a dummy with the pending flag set. */
|
|
/** The array a live view's snapshot commits back into, for element types
|
|
* whose arrays can hold foreign elements (LlEmitter.mayHoldForeign):
|
|
* like dynCheckHelper's array case on a checked array `%d`, except that
|
|
* an element the element type cannot hold is stored as a foreign element
|
|
* (undefined as a present undefined) instead of failing the conversion.
|
|
* Boolean arrays, whose slots cannot hold one, keep the failing check. */
|
|
dynViewArrayHelper(t: IrType & { kind: "array" }): string {
|
|
const builderKey = `view:${typeKey(t)}`;
|
|
const existing = this.dynBuilders.get(builderKey);
|
|
if (existing) return existing;
|
|
const name = `sc_dva_${this.dynBuilders.size}`;
|
|
this.dynBuilders.set(builderKey, name);
|
|
const host = this.host;
|
|
const elem = t.elem;
|
|
const check = this.dynCheckHelper(elem);
|
|
const match = this.dynMatchHelper(elem);
|
|
const B = new BlockBuilder();
|
|
const n = this.lenOf(B, "%d");
|
|
const a = B.tmp();
|
|
B.line(`${a} = ${arrNewCall(host, elem, n)}`);
|
|
const items = this.itemsOf(B, "%d");
|
|
const acc = elemAccess(elem);
|
|
const accTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
|
host.declare(
|
|
`declare double @scr_arr_push_${acc}(ptr, ${acc === "bool" ? "i1 zeroext" : accTy})`,
|
|
);
|
|
host.declare(`declare zeroext i1 @scr_dyn_arr_has_index(ptr, ${this.S})`);
|
|
host.declare(`declare void @scr_arr_set_len(ptr, double)`);
|
|
host.declare(`declare zeroext i1 @scr_arr_set_foreign(ptr, double, ptr)`);
|
|
this.i64Loop(B, "dva", n, (i) => {
|
|
const index = B.tmp();
|
|
B.line(`${index} = ${sizeToDouble(this.S)} ${this.S} ${i} to double`);
|
|
const present = B.tmp();
|
|
B.line(`${present} = call zeroext i1 @scr_dyn_arr_has_index(ptr %d, ${this.S} ${i})`);
|
|
const valueLabel = B.newLabel("dva.present");
|
|
const holeLabel = B.newLabel("dva.hole");
|
|
const typedLabel = B.newLabel("dva.typed");
|
|
const foreignLabel = B.newLabel("dva.foreign");
|
|
const doneLabel = B.newLabel("dva.done");
|
|
B.condBr(present, valueLabel, holeLabel);
|
|
B.startBlock(holeLabel);
|
|
const length = B.tmp();
|
|
B.line(`${length} = fadd double ${index}, 1.0`);
|
|
B.line(`call void @scr_arr_set_len(ptr ${a}, double ${length})`);
|
|
B.br(doneLabel);
|
|
B.startBlock(valueLabel);
|
|
const e = this.itemAt(B, items, i);
|
|
const fits = B.tmp();
|
|
B.line(`${fits} = call zeroext i1 @${match}(ptr ${e})`);
|
|
B.condBr(fits, typedLabel, foreignLabel);
|
|
B.startBlock(foreignLabel);
|
|
const kept = B.tmp();
|
|
B.line(`${kept} = call zeroext i1 @scr_arr_set_foreign(ptr ${a}, double ${index}, ptr ${e})`);
|
|
B.condBr(kept, doneLabel, typedLabel);
|
|
B.startBlock(typedLabel);
|
|
const v = B.tmp();
|
|
B.line(`${v} = call ${this.valTy(elem)} @${check}(ptr ${e}, ptr null)`);
|
|
this.pendingBail(
|
|
B,
|
|
"dva",
|
|
() => {
|
|
host.declare(`declare void @scr_arr_release(ptr)`);
|
|
B.line(`call void @scr_arr_release(ptr ${a})`);
|
|
},
|
|
"ptr null",
|
|
);
|
|
const pushed = B.tmp();
|
|
B.line(`${pushed} = call double @scr_arr_push_${acc}(ptr ${a}, ${accTy} ${v})`);
|
|
B.br(doneLabel);
|
|
B.startBlock(doneLabel);
|
|
});
|
|
B.terminate(`ret ptr ${a}`);
|
|
this.defs.push(
|
|
`define internal ptr @${name}(ptr %d, ptr %path) ${FN_ATTRS} { ; view commit ${typeKey(t)}`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
return name;
|
|
}
|
|
|
|
dynCheckHelper(t: IrType, preserveRefs = false): string {
|
|
// A `(T | undefined)[]` stored as T elements also takes undefined ones.
|
|
const key = checkedConversionKey(t);
|
|
const builderKey = preserveRefs ? `identity:${key}` : key;
|
|
const existing = this.dynBuilders.get(builderKey);
|
|
if (existing) return existing;
|
|
const name = `sc_dc_${this.dynBuilders.size}`;
|
|
this.dynBuilders.set(builderKey, name);
|
|
this.host.journalAllocated?.(name, "dyn.dynCheckHelper", [t, preserveRefs]);
|
|
const recursive = this.isRecursiveShape(t);
|
|
const memo = !recursive && this.holdsRecursiveShape(t) && this.valTy(t) === "ptr";
|
|
const host = this.host;
|
|
const retTy = this.valTy(t);
|
|
const dummy =
|
|
retTy === "double" ? `double ${f64Lit(0)}` : retTy === "i1" ? "i1 false" : "ptr null";
|
|
host.declare(`declare void @scr_dyn_check_fail(ptr, ptr, ptr)`);
|
|
const want = host.cstr(dynDesc(t, this.host.recordsById, this.host.unionsById));
|
|
const B = new BlockBuilder();
|
|
if (t.kind === "classval") {
|
|
host.declare(`declare ptr @scr_dyn_class_check(ptr, ptr, ptr)`);
|
|
// A subclass's class value checks into the base class's slot when
|
|
// construction through the slot can call its thunk with the base
|
|
// constructor's arguments; any other subclass fails the check.
|
|
const subclasses = host.classValueSubtypes(t.className);
|
|
if (subclasses.length > 0) {
|
|
host.declare(`declare zeroext i1 @scr_dyn_class_is(ptr, ptr)`);
|
|
for (const className of subclasses) {
|
|
const subKey = host.cstr(typeKey({ kind: "classval", className }));
|
|
const matched = B.tmp();
|
|
B.line(`${matched} = call zeroext i1 @scr_dyn_class_is(ptr %d, ptr ${subKey})`);
|
|
const yes = B.newLabel("sub.yes");
|
|
const no = B.newLabel("sub.no");
|
|
B.condBr(matched, yes, no);
|
|
B.startBlock(yes);
|
|
const sub = B.tmp();
|
|
B.line(`${sub} = call ptr @scr_dyn_class_check(ptr %d, ptr ${subKey}, ptr %path)`);
|
|
B.terminate(`ret ptr ${sub}`);
|
|
B.startBlock(no);
|
|
}
|
|
}
|
|
const checked = B.tmp();
|
|
B.line(
|
|
`${checked} = call ptr @scr_dyn_class_check(ptr %d, ptr ${host.cstr(key)}, ptr %path)`,
|
|
);
|
|
B.terminate(`ret ptr ${checked}`);
|
|
this.defs.push(
|
|
`define internal ptr @${name}(ptr %d, ptr %path) ${FN_ATTRS} {`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
const codec = textCodecRecordClass(t, (id) => host.recordsById.get(id));
|
|
if (codec !== null) {
|
|
// The record the codec's handle box holds (+1), or the path TypeError.
|
|
const checked = B.tmp();
|
|
if (codec === "StringDecoder") {
|
|
host.declare(`declare ptr @scr_string_decoder_from_dyn(ptr, ptr, ptr)`);
|
|
B.line(
|
|
`${checked} = call ptr @scr_string_decoder_from_dyn(ptr %d, ptr %path, ptr ${want})`,
|
|
);
|
|
} else {
|
|
host.declare(`declare ptr @scr_text_codec_from_dyn(ptr, i1 zeroext, ptr, ptr)`);
|
|
B.line(
|
|
`${checked} = call ptr @scr_text_codec_from_dyn(ptr %d, i1 ${codec === "TextDecoder"}, ptr %path, ptr ${want})`,
|
|
);
|
|
}
|
|
B.terminate(`ret ptr ${checked}`);
|
|
this.defs.push(
|
|
`define internal ptr @${name}(ptr %d, ptr %path) ${FN_ATTRS} {`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
if (isRefCounted(t) && t.kind !== "dyn") {
|
|
host.declare(`declare ptr @scr_dyn_typed_ref_unbox(ptr)`);
|
|
if (t.kind === "object" && t.className === "%Error") {
|
|
host.declare(`declare ptr @scr_errdyn_err_of(ptr)`);
|
|
const error = B.tmp(),
|
|
found = B.tmp();
|
|
B.line(`${error} = call ptr @scr_errdyn_err_of(ptr %d)`);
|
|
B.line(`${found} = icmp ne ptr ${error}, null`);
|
|
const yes = B.newLabel("dc.error"),
|
|
next = B.newLabel("dc.noterror");
|
|
B.condBr(found, yes, next);
|
|
B.startBlock(yes);
|
|
B.terminate(`ret ptr ${error}`);
|
|
B.startBlock(next);
|
|
}
|
|
if (isDynTypedRefType(t)) this.exactClassCapsule(B, t);
|
|
const matched = this.typedRefMatches(B, t);
|
|
const lRef = B.newLabel("dc.tr");
|
|
const lNext = B.newLabel("dc.nt");
|
|
B.condBr(matched, lRef, lNext);
|
|
B.startBlock(lRef);
|
|
const ref = B.tmp();
|
|
B.line(`${ref} = call ptr @scr_dyn_typed_ref_unbox(ptr %d)`);
|
|
B.terminate(`ret ptr ${ref}`);
|
|
B.startBlock(lNext);
|
|
if (t.kind === "record" && host.recordViews?.targets.has(t.shapeId) === true) {
|
|
// A capsule of a record with all of this type's fields converts
|
|
// to a view of that record: both names keep one object
|
|
// (record-views.ts, decision D3).
|
|
const view = this.recordViewCall(B, t.shapeId);
|
|
const viewed = B.tmp();
|
|
B.line(`${viewed} = icmp ne ptr ${view}, null`);
|
|
const lView = B.newLabel("dc.view");
|
|
const lOther = B.newLabel("dc.noview");
|
|
B.condBr(viewed, lView, lOther);
|
|
B.startBlock(lView);
|
|
B.terminate(`ret ptr ${view}`);
|
|
B.startBlock(lOther);
|
|
}
|
|
// A constructor-function stream (scr_dyn_builtin_stream.c) carries
|
|
// its runtime stream.
|
|
const streamKind = t.kind === "object" ? builtinStreamKind(t.className) : null;
|
|
if (streamKind !== null && this.host.usesDynBuiltinStream?.() === true) {
|
|
host.declare(`declare ptr @scr_dyn_builtin_stream_check(ptr, double)`);
|
|
const own = B.tmp(),
|
|
hit = B.tmp();
|
|
B.line(
|
|
`${own} = call ptr @scr_dyn_builtin_stream_check(ptr %d, double ${f64Lit(streamKind)})`,
|
|
);
|
|
B.line(`${hit} = icmp ne ptr ${own}, null`);
|
|
const lOwn = B.newLabel("dc.ctor"),
|
|
lNotOwn = B.newLabel("dc.notctor");
|
|
B.condBr(hit, lOwn, lNotOwn);
|
|
B.startBlock(lOwn);
|
|
B.terminate(`ret ptr ${own}`);
|
|
B.startBlock(lNotOwn);
|
|
}
|
|
if (t.kind !== "union" && preserveRefs) {
|
|
const kind = this.kindOf(B, "%d");
|
|
const capsule = B.tmp();
|
|
B.line(`${capsule} = icmp eq i32 ${kind}, ${DYN_KIND.TYPED_REF}`);
|
|
const reject = B.newLabel("dc.identity.reject");
|
|
const plain = B.newLabel("dc.identity.plain");
|
|
B.condBr(capsule, reject, plain);
|
|
B.startBlock(reject);
|
|
B.line(
|
|
`call void @scr_dyn_check_fail(ptr %path, ptr ${host.cstr("reference with its original layout")}, ptr %d)`,
|
|
);
|
|
B.terminate(`ret ${dummy}`);
|
|
B.startBlock(plain);
|
|
} else if (t.kind !== "union") {
|
|
const view = this.viewFn(recursive || memo);
|
|
host.declare(`declare ptr @scr_dyn_typed_ref_cached_cast(ptr, ptr, ${host.sizeType})`);
|
|
host.declare(
|
|
`declare void @scr_dyn_typed_ref_cache_cast(ptr, ptr, ${host.sizeType}, ptr, ptr, ptr, i1)`,
|
|
);
|
|
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
|
const rc = vAdapters(host, t);
|
|
const kind = this.kindOf(B, "%d");
|
|
const capsule = B.tmp();
|
|
B.line(`${capsule} = icmp eq i32 ${kind}, ${DYN_KIND.TYPED_REF}`);
|
|
const lCapsule = B.newLabel("dc.tr.cap");
|
|
const lPlain = B.newLabel("dc.tr.plain");
|
|
B.condBr(capsule, lCapsule, lPlain);
|
|
B.startBlock(lCapsule);
|
|
const cached = B.tmp();
|
|
B.line(
|
|
`${cached} = call ptr @scr_dyn_typed_ref_cached_cast(ptr %d, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")})`,
|
|
);
|
|
const hasCached = B.tmp();
|
|
B.line(`${hasCached} = icmp ne ptr ${cached}, null`);
|
|
if (recursive) {
|
|
// A cached record keeps its identity: the refresh below rewrites
|
|
// it in place. Later visits of this capsule in the session answer
|
|
// it, instead of refreshing it while this visit rebuilds it.
|
|
host.declare(`declare void @scr_dyn_build_note(ptr, ptr, ptr)`);
|
|
B.line(`call void @scr_dyn_build_note(ptr %d, ptr @${name}, ptr ${cached})`);
|
|
}
|
|
if (t.kind !== "record" && t.kind !== "array") {
|
|
const lCached = B.newLabel("dc.tr.hit");
|
|
const lMaterialize = B.newLabel("dc.tr.mat");
|
|
B.condBr(hasCached, lCached, lMaterialize);
|
|
B.startBlock(lCached);
|
|
B.terminate(`ret ptr ${cached}`);
|
|
B.startBlock(lMaterialize);
|
|
}
|
|
const materialized = B.tmp();
|
|
const checked = B.tmp();
|
|
B.line(`${materialized} = call ptr @${view}(ptr %d)`);
|
|
B.line(`${checked} = call ${retTy} @${name}(ptr ${materialized}, ptr %path)`);
|
|
B.line(`call void @scr_dyn_release_v(ptr ${materialized})`);
|
|
const ok = B.tmp();
|
|
B.line(`${ok} = icmp ne ptr ${checked}, null`);
|
|
if (t.kind === "record" || t.kind === "array") {
|
|
const shape = t.kind === "record" ? host.recordsById.get(t.shapeId) : undefined;
|
|
if (t.kind === "record" && !shape) {
|
|
throw new InternalCompilerError(
|
|
`llvm emitter bug: cached typed-ref cast of unknown shape ${t.shapeId}`,
|
|
);
|
|
}
|
|
const lOk = B.newLabel("dc.tr.ok");
|
|
const lBad = B.newLabel("dc.tr.bad");
|
|
B.condBr(ok, lOk, lBad);
|
|
B.startBlock(lBad);
|
|
const lBadDrop = B.newLabel("dc.tr.bad.drop");
|
|
const lBadRet = B.newLabel("dc.tr.bad.ret");
|
|
B.condBr(hasCached, lBadDrop, lBadRet);
|
|
B.startBlock(lBadDrop);
|
|
B.line(`call void ${releaseSym(host, t)}(ptr ${cached})`);
|
|
B.br(lBadRet);
|
|
B.startBlock(lBadRet);
|
|
B.terminate(`ret ptr null`);
|
|
B.startBlock(lOk);
|
|
const lRefresh = B.newLabel("dc.tr.refresh");
|
|
const lCache = B.newLabel("dc.tr.put");
|
|
B.condBr(hasCached, lRefresh, lCache);
|
|
B.startBlock(lRefresh);
|
|
if (t.kind !== "record") {
|
|
// Inline slots live in each header's block; move them out
|
|
// before the storage pointers change hands.
|
|
host.declare("declare void @scr_arr_outline(ptr)");
|
|
B.line(`call void @scr_arr_outline(ptr ${cached})`);
|
|
B.line(`call void @scr_arr_outline(ptr ${checked})`);
|
|
}
|
|
const base = t.kind === "record" ? host.nullableUnions.fieldBase(t.shapeId) : 1;
|
|
const members =
|
|
t.kind === "record"
|
|
? [
|
|
...shape!.fields.map((field, index) => ({
|
|
index: index + base,
|
|
type: llFieldType(field.type, host.inlineUnions.record(field.type)),
|
|
name: field.name,
|
|
})),
|
|
...(shape!.indexValue
|
|
? [
|
|
{
|
|
index: shape!.fields.length + base,
|
|
type: "ptr" as const,
|
|
name: "[key: string] overflow",
|
|
},
|
|
]
|
|
: []),
|
|
]
|
|
: [
|
|
// The dense states follow the data slots: they move with data and capacity.
|
|
{ index: SCR_ARR_FIELD.len, type: "i32", name: "length" },
|
|
{ index: SCR_ARR_FIELD.cap, type: "i32", name: "capacity" },
|
|
{ index: SCR_ARR_FIELD.data, type: "ptr" as const, name: "data" },
|
|
{ index: SCR_ARR_FIELD.filled, type: "i32" as const, name: "filled" },
|
|
];
|
|
members.forEach((member) => {
|
|
const cachedPtr = B.tmp();
|
|
const checkedPtr = B.tmp();
|
|
const oldValue = B.tmp();
|
|
const newValue = B.tmp();
|
|
const struct = t.kind === "record" ? `%${mangleRecordStruct(t.shapeId)}` : "%ScrArr";
|
|
B.line(
|
|
`${cachedPtr} = getelementptr inbounds ${struct}, ptr ${cached}, i64 0, i32 ${member.index}`,
|
|
);
|
|
B.line(
|
|
`${checkedPtr} = getelementptr inbounds ${struct}, ptr ${checked}, i64 0, i32 ${member.index}`,
|
|
);
|
|
B.line(
|
|
`${oldValue} = load ${member.type}, ptr ${cachedPtr} ; ${llvmCommentText(member.name)}`,
|
|
);
|
|
B.line(`${newValue} = load ${member.type}, ptr ${checkedPtr}`);
|
|
B.line(`store ${member.type} ${newValue}, ptr ${cachedPtr}`);
|
|
B.line(`store ${member.type} ${oldValue}, ptr ${checkedPtr}`);
|
|
});
|
|
if (t.kind !== "record") {
|
|
// Whether the dense slots carry states (SCR_ARR_STATES in
|
|
// `spare`) moves with them; the rest of `spare` stays.
|
|
const cachedSpare = B.tmp(),
|
|
checkedSpare = B.tmp();
|
|
const sparePtr = (array: string): string => {
|
|
const ptr = B.tmp();
|
|
B.line(
|
|
`${ptr} = getelementptr inbounds %ScrArr, ptr ${array}, i64 0, i32 ${SCR_ARR_FIELD.spare}`,
|
|
);
|
|
return ptr;
|
|
};
|
|
const cachedSparePtr = sparePtr(cached),
|
|
checkedSparePtr = sparePtr(checked);
|
|
B.line(`${cachedSpare} = load i16, ptr ${cachedSparePtr}`);
|
|
B.line(`${checkedSpare} = load i16, ptr ${checkedSparePtr}`);
|
|
const exchange = (own: string, other: string, ptr: string): void => {
|
|
const kept = B.tmp(),
|
|
moved = B.tmp(),
|
|
next = B.tmp();
|
|
B.line(`${kept} = and i16 ${own}, ${~SCR_ARR_STATES}`);
|
|
B.line(`${moved} = and i16 ${other}, ${SCR_ARR_STATES}`);
|
|
B.line(`${next} = or i16 ${kept}, ${moved}`);
|
|
B.line(`store i16 ${next}, ptr ${ptr}`);
|
|
};
|
|
exchange(cachedSpare, checkedSpare, cachedSparePtr);
|
|
exchange(checkedSpare, cachedSpare, checkedSparePtr);
|
|
// Sparse slots and numeric properties live out of line; each
|
|
// array keeps its own match metadata.
|
|
host.declare("declare void @scr_arr_swap_indexed_ext(ptr, ptr)");
|
|
B.line(`call void @scr_arr_swap_indexed_ext(ptr ${cached}, ptr ${checked})`);
|
|
}
|
|
if (recursive) {
|
|
// The memo recorded the record built for the view: later visits
|
|
// in the session answer the cached record that now holds it.
|
|
host.declare(`declare void @scr_dyn_build_rekey(ptr, ptr, ptr)`);
|
|
B.line(
|
|
`call void @scr_dyn_build_rekey(ptr ${materialized}, ptr @${name}, ptr ${cached})`,
|
|
);
|
|
}
|
|
B.line(`call void ${releaseSym(host, t)}(ptr ${checked})`);
|
|
B.terminate(`ret ptr ${cached}`);
|
|
B.startBlock(lCache);
|
|
B.line(
|
|
`call void @scr_dyn_typed_ref_cache_cast(ptr %d, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr ${checked}, ptr ${rc.retain}, ptr ${rc.release}, i1 ${traceAdapter(host, t) !== null ? "true" : "false"})`,
|
|
);
|
|
B.terminate(`ret ptr ${checked}`);
|
|
} else {
|
|
const lCache = B.newLabel("dc.tr.put");
|
|
const lReturn = B.newLabel("dc.tr.ret");
|
|
B.condBr(ok, lCache, lReturn);
|
|
B.startBlock(lCache);
|
|
B.line(
|
|
`call void @scr_dyn_typed_ref_cache_cast(ptr %d, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr ${checked}, ptr ${rc.retain}, ptr ${rc.release}, i1 ${traceAdapter(host, t) !== null ? "true" : "false"})`,
|
|
);
|
|
B.br(lReturn);
|
|
B.startBlock(lReturn);
|
|
B.terminate(`ret ptr ${checked}`);
|
|
}
|
|
B.startBlock(lPlain);
|
|
}
|
|
}
|
|
/** kind test with the standard fail path (got = %d). */
|
|
const requireKind = (k: number, hint: string): void => {
|
|
const kd = this.kindOf(B, "%d");
|
|
const ok = B.tmp();
|
|
B.line(`${ok} = icmp eq i32 ${kd}, ${k}`);
|
|
const lo = B.newLabel(`${hint}.k`);
|
|
const lf = B.newLabel(`${hint}.f`);
|
|
B.condBr(ok, lo, lf);
|
|
B.startBlock(lf);
|
|
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
|
|
B.terminate(`ret ${dummy}`);
|
|
B.startBlock(lo);
|
|
};
|
|
switch (t.kind) {
|
|
case "date": {
|
|
host.declare(`declare ptr @scr_dyn_native_date_check(ptr, ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_native_date_check(ptr %d, ptr %path)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "url":
|
|
case "searchParams": {
|
|
const native = t.kind === "url" ? "url" : "search_params";
|
|
host.declare(`declare ptr @scr_dyn_native_${native}_check(ptr, ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_native_${native}_check(ptr %d, ptr %path)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "regex": {
|
|
host.declare(`declare ptr @scr_dyn_native_regex_check(ptr, ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_native_regex_check(ptr %d, ptr %path)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "map":
|
|
case "set": {
|
|
host.declare(`declare ptr @scr_dyn_native_collection_check(ptr, i32, ptr, ptr)`);
|
|
const r = B.tmp();
|
|
const typed =
|
|
t.kind === "map" ? t.key.kind !== "dyn" || t.value.kind !== "dyn" : t.elem.kind !== "dyn";
|
|
B.line(
|
|
`${r} = call ptr @scr_dyn_native_collection_check(ptr %d, i32 ${t.kind === "map" ? 1 : 0}, ptr ${typed ? host.cstr(typeKey(t)) : "null"}, ptr %path)`,
|
|
);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "bigint": {
|
|
requireKind(DYN_KIND.BIGINT, "dc");
|
|
host.declare(`declare ptr @scr_bigint_retain(ptr)`);
|
|
const v = this.payloadOf(B, "%d", "ptr");
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_bigint_retain(ptr ${v})`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "symbol": {
|
|
requireKind(DYN_KIND.SYMBOL, "dc");
|
|
host.declare(`declare ptr @scr_sym_retain(ptr)`);
|
|
const v = this.payloadOf(B, "%d", "ptr");
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_sym_retain(ptr ${v})`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "f64": {
|
|
requireKind(DYN_KIND.NUM, "dc");
|
|
const v = this.payloadOf(B, "%d", "double");
|
|
B.terminate(`ret double ${v}`);
|
|
break;
|
|
}
|
|
case "bool": {
|
|
requireKind(DYN_KIND.BOOL, "dc");
|
|
const v = this.boolOf(B, "%d");
|
|
B.terminate(`ret i1 ${v}`);
|
|
break;
|
|
}
|
|
case "string": {
|
|
requireKind(DYN_KIND.STR, "dc");
|
|
host.declare(`declare ptr @scr_str_retain_v(ptr)`);
|
|
const s = this.payloadOf(B, "%d", "ptr");
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_str_retain_v(ptr ${s})`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "dyn": {
|
|
// Preserve the capsule at an unknown boundary. Generic dyn
|
|
// operations materialize its one cached detached view, while
|
|
// strict equality keeps repeated stream references identical.
|
|
const retained = this.retainDyn(B, "%d");
|
|
B.terminate(`ret ptr ${retained}`);
|
|
break;
|
|
}
|
|
case "procStream": {
|
|
host.declare(`declare double @scr_dyn_process_stdio_fd(ptr, ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call double @scr_dyn_process_stdio_fd(ptr %d, ptr %path)`);
|
|
B.terminate(`ret double ${r}`);
|
|
break;
|
|
}
|
|
case "bytes": {
|
|
host.declare(`declare zeroext i1 @scr_dyn_bytes_is(ptr, i32)`);
|
|
const matched = B.tmp();
|
|
B.line(
|
|
`${matched} = call zeroext i1 @scr_dyn_bytes_is(ptr %d, i32 ${BYTES_ELEM_NUM[t.elem]})`,
|
|
);
|
|
const yes = B.newLabel("dc.bytes");
|
|
const no = B.newLabel("dc.bytes.fail");
|
|
B.condBr(matched, yes, no);
|
|
B.startBlock(no);
|
|
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
|
|
B.terminate(`ret ptr null`);
|
|
B.startBlock(yes);
|
|
host.declare(`declare ptr @scr_dyn_bytes_unbox(ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_bytes_unbox(ptr %d)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "generator":
|
|
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
|
|
B.terminate(`ret ptr null`);
|
|
break;
|
|
case "promise": {
|
|
requireKind(DYN_KIND.PROMISE, "dc");
|
|
host.declare(`declare ptr @scr_promise_retain(ptr)`);
|
|
const promise = this.payloadOf(B, "%d", "ptr"),
|
|
retained = B.tmp();
|
|
if (t.inner.kind !== "dyn") {
|
|
host.declare(`declare ptr @scr_promise_new()`);
|
|
host.declare(`declare void @scr_promise_race_add(ptr, ptr, ptr)`);
|
|
B.line(`${retained} = call ptr @scr_promise_new()`);
|
|
host.declare(`declare void @scr_promise_share_identity(ptr, ptr)`);
|
|
B.line(`call void @scr_promise_share_identity(ptr ${retained}, ptr ${promise})`);
|
|
B.line(
|
|
`call void @scr_promise_race_add(ptr ${retained}, ptr ${promise}, ptr @${host.dynPromiseAdapter(t.inner)})`,
|
|
);
|
|
B.terminate(`ret ptr ${retained}`);
|
|
break;
|
|
}
|
|
B.line(`${retained} = call ptr @scr_promise_retain(ptr ${promise})`);
|
|
B.terminate(`ret ptr ${retained}`);
|
|
break;
|
|
}
|
|
case "object": {
|
|
// The %Error extraction (an instanceof-Error narrow on unknown):
|
|
// validate the checked-dynamic tree's error encoding — the reserved "%error" marker
|
|
// caughtToDyn builds — and extract through the runtime's IDENTITY
|
|
// CACHE (scr_error_from_dyn): a dyn error that came from a runtime
|
|
// ScrError answers that very instance, so out-and-back crossings
|
|
// compare reference-equal (the tracing suite's shape); alien
|
|
// %error objects rebuild once and cache the pair. The C walker's
|
|
// arm exactly.
|
|
if (t.className !== "%Error") {
|
|
// Exact class capsules returned before this switch. A plain dyn
|
|
// object cannot acquire a class brand structurally.
|
|
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
|
|
B.terminate(`ret ptr null`);
|
|
break;
|
|
}
|
|
const kd = this.kindOf(B, "%d");
|
|
const isObj = B.tmp();
|
|
B.line(`${isObj} = icmp eq i32 ${kd}, ${DYN_KIND.OBJ}`);
|
|
const lObj = B.newLabel("dce.o");
|
|
const lFail = B.newLabel("dce.f");
|
|
B.condBr(isObj, lObj, lFail);
|
|
B.startBlock(lObj);
|
|
const marker = this.objGetLit(B, "%d", "%error");
|
|
const hasMarker = B.tmp();
|
|
B.line(`${hasMarker} = icmp ne ptr ${marker}, null`);
|
|
const lGo = B.newLabel("dce.g");
|
|
B.condBr(hasMarker, lGo, lFail);
|
|
B.startBlock(lFail);
|
|
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
|
|
B.terminate(`ret ptr null`);
|
|
B.startBlock(lGo);
|
|
host.declare(`declare ptr @scr_error_from_dyn(ptr)`);
|
|
const e = B.tmp();
|
|
B.line(`${e} = call ptr @scr_error_from_dyn(ptr %d)`);
|
|
B.terminate(`ret ptr ${e}`);
|
|
break;
|
|
}
|
|
case "record": {
|
|
const shape = host.recordsById.get(t.shapeId);
|
|
if (!shape)
|
|
throw new InternalCompilerError(
|
|
`llvm emitter bug: dynCheck of unknown shape ${t.shapeId}`,
|
|
);
|
|
const struct = mangleRecordStruct(t.shapeId);
|
|
const base = host.nullableUnions.fieldBase(t.shapeId);
|
|
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base]));
|
|
const releaseR = (): void => {
|
|
B.line(`call void ${releaseSym(host, t)}(ptr %r0)`);
|
|
};
|
|
// One shared path node, restored per use (lives only during the
|
|
// nested call).
|
|
const pathSlot = "%dcp";
|
|
B.entryAllocas.push(`${pathSlot} = alloca %ScrDynPath`);
|
|
const setPath = (keyText: string | null, index: string): void => {
|
|
const pp = B.tmp();
|
|
const kp = B.tmp();
|
|
const ip = B.tmp();
|
|
B.line(`${pp} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 0`);
|
|
B.line(`store ptr %path, ptr ${pp}`);
|
|
B.line(`${kp} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 1`);
|
|
B.line(`store ptr ${keyText === null ? "null" : host.cstr(keyText)}, ptr ${kp}`);
|
|
B.line(`${ip} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 2`);
|
|
B.line(`store ${host.sizeType} ${index}, ptr ${ip}`);
|
|
};
|
|
const setPathKeyPtr = (keyPtr: string): void => {
|
|
const pp = B.tmp();
|
|
const kp = B.tmp();
|
|
const ip = B.tmp();
|
|
B.line(`${pp} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 0`);
|
|
B.line(`store ptr %path, ptr ${pp}`);
|
|
B.line(`${kp} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 1`);
|
|
B.line(`store ptr ${keyPtr}, ptr ${kp}`);
|
|
B.line(`${ip} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 2`);
|
|
B.line(`store ${host.sizeType} 0, ptr ${ip}`);
|
|
};
|
|
// An inline union field copies the checked box's bits; the caller
|
|
// releases the box afterwards.
|
|
const storeInto = (fieldName: string, ft: IrType, value: string): void => {
|
|
const idx = fieldIndex.get(fieldName)!;
|
|
const p = B.tmp();
|
|
B.line(
|
|
`${p} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${idx} ; .${llvmCommentText(fieldName)}`,
|
|
);
|
|
const fieldTy = llFieldType(ft, host.inlineUnions.record(ft));
|
|
if (fieldTy === INLINE_UNION_FIELD) {
|
|
const { tag, bits } = unionBoxBits(B, value, host.unionNoneRef());
|
|
storeInlineUnion(B, p, tag, bits);
|
|
} else if (fieldTy === "i8") {
|
|
const z = B.tmp();
|
|
B.line(`${z} = zext i1 ${value} to i8`);
|
|
B.line(`store i8 ${z}, ptr ${p}`);
|
|
} else {
|
|
B.line(`store ${fieldTy} ${value}, ptr ${p}`);
|
|
}
|
|
};
|
|
const releaseInline = (ft: IrType, value: string): void => {
|
|
if (host.inlineUnions.record(ft))
|
|
B.line(`call void ${releaseSym(host, ft)}(ptr ${value})`);
|
|
};
|
|
// Tuple targets: a JSON ARRAY of exactly the arity.
|
|
if (shape.tuple) {
|
|
const byIndex = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
|
|
const arityWant = host.cstr(`array of length ${byIndex.length}`);
|
|
requireKind(DYN_KIND.ARR, "dct");
|
|
const len = this.lenOf(B, "%d");
|
|
const lenOk = B.tmp();
|
|
B.line(`${lenOk} = icmp eq ${host.sizeType} ${len}, ${byIndex.length}`);
|
|
const lGo = B.newLabel("dct.g");
|
|
const lAr = B.newLabel("dct.a");
|
|
B.condBr(lenOk, lGo, lAr);
|
|
B.startBlock(lAr);
|
|
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${arityWant}, ptr %d)`);
|
|
B.terminate(`ret ptr null`);
|
|
B.startBlock(lGo);
|
|
B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`);
|
|
if (recursive) this.recordBuilt(B, name, "%r0");
|
|
const items = this.itemsOf(B, "%d");
|
|
byIndex.forEach((f, i) => {
|
|
setPath(null, `${i}`);
|
|
const e = this.itemAt(B, items, `${i}`);
|
|
const v = B.tmp();
|
|
B.line(
|
|
`${v} = call ${this.valTy(f.type)} @${this.dynCheckHelper(f.type, preserveRefs)}(ptr ${e}, ptr ${pathSlot})`,
|
|
);
|
|
storeInto(f.name, f.type, v);
|
|
releaseInline(f.type, v);
|
|
this.pendingBail(B, "dct", releaseR, "ptr null");
|
|
});
|
|
B.terminate(`ret ptr %r0`);
|
|
break;
|
|
}
|
|
if (shape.indexValue) requireKind(DYN_KIND.OBJ, "dcr");
|
|
else {
|
|
const kind = this.kindOf(B, "%d"),
|
|
object = B.tmp(),
|
|
proxy = B.tmp(),
|
|
readable = B.tmp();
|
|
B.line(`${object} = icmp eq i32 ${kind}, ${DYN_KIND.OBJ}`);
|
|
B.line(`${proxy} = icmp eq i32 ${kind}, ${DYN_KIND.PROXY}`);
|
|
B.line(`${readable} = or i1 ${object}, ${proxy}`);
|
|
const ok = B.newLabel("dcr.object"),
|
|
fail = B.newLabel("dcr.fail");
|
|
B.condBr(readable, ok, fail);
|
|
B.startBlock(fail);
|
|
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
|
|
B.terminate(`ret ptr null`);
|
|
B.startBlock(ok);
|
|
}
|
|
if (shape.fields.length === 0 && !shape.indexValue) {
|
|
const sourceAccessor = `${this.toDynHelper(t)}_source_access`;
|
|
const rc = vAdapters(host, t);
|
|
host.declare(`declare ptr @scr_dyn_obj_source_cast(ptr, ptr, ptr)`);
|
|
const source = B.tmp();
|
|
const found = B.tmp();
|
|
const lSource = B.newLabel("dcr.source");
|
|
const lCopy = B.newLabel("dcr.copy");
|
|
B.line(
|
|
`${source} = call ptr @scr_dyn_obj_source_cast(ptr %d, ptr @${sourceAccessor}, ptr ${rc.retain})`,
|
|
);
|
|
B.line(`${found} = icmp ne ptr ${source}, null`);
|
|
B.condBr(found, lSource, lCopy);
|
|
B.startBlock(lSource);
|
|
B.terminate(`ret ptr ${source}`);
|
|
B.startBlock(lCopy);
|
|
}
|
|
B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`);
|
|
if (recursive) this.recordBuilt(B, name, "%r0");
|
|
// The order index stays 0: the source's own order is kept below
|
|
// when it differs from the table.
|
|
for (const f of shape.fields) {
|
|
if (f.name === KEY_ORDER_FIELD) continue;
|
|
const { value: m, release } = this.memberRead(B, f.name, f.type);
|
|
this.pendingBail(
|
|
B,
|
|
"dcr.read",
|
|
() => {
|
|
B.line(`call void @${release}(ptr ${m})`);
|
|
releaseR();
|
|
},
|
|
"ptr null",
|
|
);
|
|
setPath(f.name, "0");
|
|
const value = B.tmp();
|
|
B.line(
|
|
`${value} = call ${this.valTy(f.type)} @${this.dynCheckHelper(f.type, preserveRefs)}(ptr ${m}, ptr ${pathSlot})`,
|
|
);
|
|
B.line(`call void @${release}(ptr ${m})`);
|
|
storeInto(f.name, f.type, value);
|
|
this.pendingBail(
|
|
B,
|
|
"dcr",
|
|
() => {
|
|
releaseInline(f.type, value);
|
|
releaseR();
|
|
},
|
|
"ptr null",
|
|
);
|
|
if (f.type.kind === "union") {
|
|
const stored = this.presenceOf(B, "%d", f.name, f.type, value);
|
|
if (stored !== value) storeInto(f.name, f.type, stored);
|
|
}
|
|
releaseInline(f.type, value);
|
|
}
|
|
// A closed layout cannot store undeclared keys: a source object
|
|
// that has them stays beside the record so its reflective
|
|
// surfaces still observe them (scr_record_source_note).
|
|
if (recordMayKeepSource(shape)) {
|
|
this.keepsRecordSources = true;
|
|
const names = this.recordNameTable(t.shapeId, shape);
|
|
host.declare(
|
|
`declare void @scr_record_source_note(ptr, ptr, ptr, ptr, ${host.sizeType})`,
|
|
);
|
|
B.line(
|
|
`call void @scr_record_source_note(ptr %r0, ptr %d, ptr ${names.names}, ptr ${names.lens}, ${host.sizeType} ${shape.fields.length})`,
|
|
);
|
|
}
|
|
// Index-signature shapes CAPTURE undeclared keys into the
|
|
// overflow map.
|
|
if (shape.indexValue) {
|
|
const iv = shape.indexValue;
|
|
host.declare(`declare i32 @memcmp(ptr, ptr, ${host.sizeType})`);
|
|
host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`);
|
|
host.declare(`declare void @scr_str_release(ptr)`);
|
|
const ovfp = B.tmp();
|
|
const ovf = B.tmp();
|
|
B.line(
|
|
`${ovfp} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${shape.fields.length + base}`,
|
|
);
|
|
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
|
|
if (shape.fields.length === 0) {
|
|
// Object.create(null)'s dictionary keeps its null prototype:
|
|
// ScrDyn.null_proto becomes SCR_MAP_NULL_PROTO.
|
|
const npp = B.tmp();
|
|
const np = B.tmp();
|
|
const flagsp = B.tmp();
|
|
B.line(
|
|
`${npp} = getelementptr inbounds i8, ptr %d, i64 ${this.abiOffset(13, 9)} ; ->null_proto`,
|
|
);
|
|
B.line(`${np} = load i8, ptr ${npp}`);
|
|
B.line(
|
|
`${flagsp} = getelementptr inbounds i8, ptr ${ovf}, i64 ${mapFlagsOffset(host.sizeType)}`,
|
|
);
|
|
B.line(`store i8 ${np}, ptr ${flagsp}`);
|
|
}
|
|
const n = this.lenOf(B, "%d");
|
|
this.i64Loop(B, "dcv", n, (i, brNext) => {
|
|
const ent = this.entryAt(B, "%d", i);
|
|
for (const f of shape.fields) {
|
|
const klen = Buffer.byteLength(f.name, "utf8");
|
|
const lenEq = B.tmp();
|
|
B.line(`${lenEq} = icmp eq ${host.sizeType} ${ent.keyLen}, ${klen}`);
|
|
const lCmp = B.newLabel("dcv.kc");
|
|
const lNo = B.newLabel("dcv.kn");
|
|
B.condBr(lenEq, lCmp, lNo);
|
|
B.startBlock(lCmp);
|
|
const c = B.tmp();
|
|
const same = B.tmp();
|
|
B.line(
|
|
`${c} = call i32 @memcmp(ptr ${ent.key}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}) ; ${llvmCommentText(f.name)}`,
|
|
);
|
|
B.line(`${same} = icmp eq i32 ${c}, 0`);
|
|
const skip = B.newLabel("dcv.ks");
|
|
const lNo2 = B.newLabel("dcv.kn");
|
|
B.condBr(same, skip, lNo2);
|
|
B.startBlock(skip);
|
|
brNext();
|
|
B.startBlock(lNo2);
|
|
B.br(lNo);
|
|
B.startBlock(lNo);
|
|
}
|
|
let ev: string;
|
|
if (iv.kind === "dyn") {
|
|
ev = this.retainDyn(B, ent.value);
|
|
} else {
|
|
setPathKeyPtr(ent.key);
|
|
ev = B.tmp();
|
|
B.line(
|
|
`${ev} = call ${this.valTy(iv)} @${this.dynCheckHelper(iv, preserveRefs)}(ptr ${ent.value}, ptr ${pathSlot})`,
|
|
);
|
|
this.pendingBail(B, "dcv", releaseR, "ptr null");
|
|
}
|
|
const ek = B.tmp();
|
|
B.line(`${ek} = call ptr @scr_str_new(ptr ${ent.key}, ${host.sizeType} ${ent.keyLen})`);
|
|
if (iv.kind === "f64") {
|
|
host.declare(`declare void @scr_map_set_str_f64(ptr, ptr, double)`);
|
|
B.line(`call void @scr_map_set_str_f64(ptr ${ovf}, ptr ${ek}, double ${ev})`);
|
|
} else if (iv.kind === "bool") {
|
|
host.declare(`declare void @scr_map_set_str_bool(ptr, ptr, i1 zeroext)`);
|
|
B.line(`call void @scr_map_set_str_bool(ptr ${ovf}, ptr ${ek}, i1 ${ev})`);
|
|
} else {
|
|
host.declare(`declare void @scr_map_set_str_ref(ptr, ptr, ptr)`);
|
|
B.line(
|
|
`call void @scr_map_set_str_ref(ptr ${ovf}, ptr ${ek}, ptr ${ev}) ; v moves in`,
|
|
);
|
|
}
|
|
B.line(`call void @scr_str_release(ptr ${ek})`);
|
|
});
|
|
}
|
|
// A record built from a frozen object is frozen too, as the object it
|
|
// copies (frozen-records.ts makes such shapes freezable).
|
|
if (shape.freezable) {
|
|
host.declare(`declare zeroext i1 @scr_dyn_checked_record_frozen(ptr)`);
|
|
host.declare(`declare void @scr_record_freeze(ptr)`);
|
|
const frozen = B.tmp();
|
|
const lFreeze = B.newLabel("dcr.freeze");
|
|
const lDone = B.newLabel("dcr.frozen");
|
|
B.line(`${frozen} = call i1 @scr_dyn_checked_record_frozen(ptr %d)`);
|
|
B.condBr(frozen, lFreeze, lDone);
|
|
B.startBlock(lFreeze);
|
|
B.line(`call void @scr_record_freeze(ptr %r0)`);
|
|
B.br(lDone);
|
|
B.startBlock(lDone);
|
|
}
|
|
B.terminate(`ret ptr %r0`);
|
|
break;
|
|
}
|
|
case "array": {
|
|
const elem = t.elem;
|
|
const c = this.dynCheckHelper(elem, preserveRefs);
|
|
requireKind(DYN_KIND.ARR, "dca");
|
|
const n = this.lenOf(B, "%d");
|
|
const a = B.tmp();
|
|
B.line(`${a} = ${arrNewCall(host, elem, n)}`);
|
|
if (recursive) this.recordBuilt(B, name, a);
|
|
const items = this.itemsOf(B, "%d");
|
|
const pathSlot = "%dcp";
|
|
B.entryAllocas.push(`${pathSlot} = alloca %ScrDynPath`);
|
|
const acc = elemAccess(elem);
|
|
const accTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
|
host.declare(
|
|
`declare double @scr_arr_push_${acc}(ptr, ${acc === "bool" ? "i1 zeroext" : accTy})`,
|
|
);
|
|
this.i64Loop(B, "dca", n, (i) => {
|
|
host.declare(`declare zeroext i1 @scr_dyn_arr_has_index(ptr, ${this.S})`);
|
|
const present = B.tmp();
|
|
B.line(`${present} = call zeroext i1 @scr_dyn_arr_has_index(ptr %d, ${this.S} ${i})`);
|
|
const valueLabel = B.newLabel("dca.present");
|
|
const holeLabel = B.newLabel("dca.hole");
|
|
const doneLabel = B.newLabel("dca.done");
|
|
B.condBr(present, valueLabel, holeLabel);
|
|
B.startBlock(holeLabel);
|
|
host.declare(`declare void @scr_arr_set_len(ptr, double)`);
|
|
const index = B.tmp();
|
|
const length = B.tmp();
|
|
B.line(`${index} = ${sizeToDouble(this.S)} ${this.S} ${i} to double`);
|
|
B.line(`${length} = fadd double ${index}, 1.0`);
|
|
B.line(`call void @scr_arr_set_len(ptr ${a}, double ${length})`);
|
|
B.br(doneLabel);
|
|
B.startBlock(valueLabel);
|
|
const pp = B.tmp();
|
|
const kp = B.tmp();
|
|
const ip = B.tmp();
|
|
B.line(`${pp} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 0`);
|
|
B.line(`store ptr %path, ptr ${pp}`);
|
|
B.line(`${kp} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 1`);
|
|
B.line(`store ptr null, ptr ${kp}`);
|
|
B.line(`${ip} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 2`);
|
|
B.line(`store ${host.sizeType} ${i}, ptr ${ip}`);
|
|
const e = this.itemAt(B, items, i);
|
|
if (t.optionalElements) {
|
|
// An undefined element is a present undefined element.
|
|
const undef = B.tmp();
|
|
const storeUndefined = B.newLabel("dca.undef");
|
|
const checkLabel = B.newLabel("dca.check");
|
|
B.line(`${undef} = icmp eq i32 ${this.kindOf(B, e)}, ${DYN_KIND.UNDEF}`);
|
|
B.condBr(undef, storeUndefined, checkLabel);
|
|
B.startBlock(storeUndefined);
|
|
host.declare(`declare void @scr_arr_set_undefined(ptr, double)`);
|
|
const at = B.tmp();
|
|
B.line(`${at} = ${sizeToDouble(this.S)} ${this.S} ${i} to double`);
|
|
B.line(`call void @scr_arr_set_undefined(ptr ${a}, double ${at})`);
|
|
B.br(doneLabel);
|
|
B.startBlock(checkLabel);
|
|
}
|
|
const v = B.tmp();
|
|
B.line(`${v} = call ${this.valTy(elem)} @${c}(ptr ${e}, ptr ${pathSlot})`);
|
|
this.pendingBail(
|
|
B,
|
|
"dca",
|
|
() => {
|
|
host.declare(`declare void @scr_arr_release(ptr)`);
|
|
B.line(`call void @scr_arr_release(ptr ${a})`);
|
|
},
|
|
"ptr null",
|
|
);
|
|
const pushed = B.tmp();
|
|
B.line(`${pushed} = call double @scr_arr_push_${acc}(ptr ${a}, ${accTy} ${v})`);
|
|
B.br(doneLabel);
|
|
B.startBlock(doneLabel);
|
|
});
|
|
B.terminate(`ret ptr ${a}`);
|
|
break;
|
|
}
|
|
case "union": {
|
|
const def = host.unionsById.get(t.unionId);
|
|
if (!def)
|
|
throw new InternalCompilerError(
|
|
`llvm emitter bug: dynCheck of unknown union ${t.unionId}`,
|
|
);
|
|
// Arms in unionArmOrder, first FULL match wins. The matched arm's
|
|
// builder can no longer fail, and it runs as a trusted build: a
|
|
// nested union inside it picks the arm trustedArmKinds names without
|
|
// matching the structure that this match already checked.
|
|
const trustedKinds = preserveRefs ? def.arms.map(() => null) : this.trustedArmKinds(def);
|
|
const trustedCall = (call: () => void): void => {
|
|
host.declare(`declare zeroext i1 @scr_dyn_trust_enter()`);
|
|
host.declare(`declare void @scr_dyn_trust_exit(i1 zeroext)`);
|
|
const previous = B.tmp();
|
|
B.line(`${previous} = call zeroext i1 @scr_dyn_trust_enter()`);
|
|
call();
|
|
B.line(`call void @scr_dyn_trust_exit(i1 zeroext ${previous})`);
|
|
};
|
|
const build = (i: number): void => {
|
|
const arm = def.arms[i]!;
|
|
if (arm.kind === "undefinedT" || arm.kind === "nullT") {
|
|
// A matched unit arm builds nothing: THE interned immortal
|
|
// instance (rc == SIZE_MAX — no retain owed).
|
|
B.terminate(`ret ptr ${host.unitInstanceRef(t.unionId, i)}`);
|
|
} else if (arm.kind === "f64" || arm.kind === "procStream") {
|
|
host.declare(`declare ptr @scr_union_new_f64(i32, double)`);
|
|
const x = B.tmp();
|
|
const u = B.tmp();
|
|
B.line(
|
|
`${x} = call double @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`,
|
|
);
|
|
B.line(`${u} = call ptr @scr_union_new_f64(i32 ${i}, double ${x})`);
|
|
B.terminate(`ret ptr ${u}`);
|
|
} else if (arm.kind === "bool") {
|
|
host.declare(`declare ptr @scr_union_new_bool(i32, i1 zeroext)`);
|
|
const x = B.tmp();
|
|
const u = B.tmp();
|
|
B.line(
|
|
`${x} = call zeroext i1 @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`,
|
|
);
|
|
B.line(`${u} = call ptr @scr_union_new_bool(i32 ${i}, i1 ${x})`);
|
|
B.terminate(`ret ptr ${u}`);
|
|
} else if (host.nullableUnions.has(t.unionId)) {
|
|
// A nullable union is its (+1) reference payload.
|
|
const x = B.tmp();
|
|
trustedCall(() =>
|
|
B.line(
|
|
`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`,
|
|
),
|
|
);
|
|
B.terminate(`ret ptr ${x}`);
|
|
} else {
|
|
const rc = vAdapters(host, arm);
|
|
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
|
|
const x = B.tmp();
|
|
const u = B.tmp();
|
|
trustedCall(() =>
|
|
B.line(
|
|
`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`,
|
|
),
|
|
);
|
|
B.line(
|
|
`${u} = call ptr @scr_union_new_ref(i32 ${i}, ptr ${x}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, arm)})`,
|
|
);
|
|
B.terminate(`ret ptr ${u}`);
|
|
}
|
|
};
|
|
const order = this.unionArmOrder(def);
|
|
const values = this.dispatchValues(def);
|
|
if (values !== null) {
|
|
const hits = def.arms.map(() => B.newLabel("dcu.h"));
|
|
const unmatched = B.newLabel("dcu.n");
|
|
this.unionArmsByDiscriminant(
|
|
B,
|
|
def,
|
|
values,
|
|
order,
|
|
hits,
|
|
unmatched,
|
|
preserveRefs,
|
|
`@${name}.cases`,
|
|
trustedKinds,
|
|
);
|
|
for (const i of order) {
|
|
B.startBlock(hits[i]!);
|
|
build(i);
|
|
}
|
|
B.startBlock(unmatched);
|
|
} else
|
|
for (const i of order) {
|
|
const lHit = B.newLabel("dcu.h");
|
|
const lNext = B.newLabel("dcu.n");
|
|
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs, trustedKinds[i]!);
|
|
B.startBlock(lHit);
|
|
build(i);
|
|
B.startBlock(lNext);
|
|
}
|
|
if (preserveRefs) {
|
|
B.line(
|
|
`call void @scr_dyn_check_fail(ptr %path, ptr ${host.cstr("reference with its original layout")}, ptr %d)`,
|
|
);
|
|
B.terminate(`ret ptr null`);
|
|
break;
|
|
}
|
|
// Preserve an exact typed-ref arm above. If no producer tag matched,
|
|
// retry the union against the ordinary copy-based structural
|
|
// snapshot (the compiler's width-coercion stance, rather than a
|
|
// live narrowed alias).
|
|
const view = this.viewFn(recursive || memo);
|
|
host.declare(`declare ptr @scr_dyn_typed_ref_cached_cast(ptr, ptr, ${host.sizeType})`);
|
|
host.declare(
|
|
`declare void @scr_dyn_typed_ref_cache_cast(ptr, ptr, ${host.sizeType}, ptr, ptr, ptr, i1)`,
|
|
);
|
|
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
|
host.declare(`declare void @scr_union_release(ptr)`);
|
|
const rc = vAdapters(host, t);
|
|
const kind = this.kindOf(B, "%d");
|
|
const capsule = B.tmp();
|
|
B.line(`${capsule} = icmp eq i32 ${kind}, ${DYN_KIND.TYPED_REF}`);
|
|
const lCapsule = B.newLabel("dcu.cap");
|
|
const lFail = B.newLabel("dcu.fail");
|
|
B.condBr(capsule, lCapsule, lFail);
|
|
B.startBlock(lCapsule);
|
|
if (host.nullableUnions.has(t.unionId)) {
|
|
// No box identity to cache: the structural snapshot's payload is
|
|
// the value, and NULL is the undefined arm, so failure is the
|
|
// pending exception rather than a NULL result.
|
|
const materialized = B.tmp();
|
|
const checked = B.tmp();
|
|
B.line(`${materialized} = call ptr @${view}(ptr %d)`);
|
|
B.line(`${checked} = call ptr @${name}(ptr ${materialized}, ptr %path)`);
|
|
B.line(`call void @scr_dyn_release_v(ptr ${materialized})`);
|
|
B.terminate(`ret ptr ${checked}`);
|
|
B.startBlock(lFail);
|
|
this.unionArmFailure(B, def, kind);
|
|
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
|
|
B.terminate(`ret ptr null`);
|
|
break;
|
|
}
|
|
const cached = B.tmp();
|
|
B.line(
|
|
`${cached} = call ptr @scr_dyn_typed_ref_cached_cast(ptr %d, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")})`,
|
|
);
|
|
const hasCached = B.tmp();
|
|
B.line(`${hasCached} = icmp ne ptr ${cached}, null`);
|
|
const materialized = B.tmp();
|
|
const checked = B.tmp();
|
|
B.line(`${materialized} = call ptr @${view}(ptr %d)`);
|
|
B.line(`${checked} = call ptr @${name}(ptr ${materialized}, ptr %path)`);
|
|
B.line(`call void @scr_dyn_release_v(ptr ${materialized})`);
|
|
const ok = B.tmp();
|
|
B.line(`${ok} = icmp ne ptr ${checked}, null`);
|
|
const lOk = B.newLabel("dcu.ok");
|
|
const lBad = B.newLabel("dcu.bad");
|
|
B.condBr(ok, lOk, lBad);
|
|
B.startBlock(lBad);
|
|
const lBadDrop = B.newLabel("dcu.bad.drop");
|
|
const lBadRet = B.newLabel("dcu.bad.ret");
|
|
B.condBr(hasCached, lBadDrop, lBadRet);
|
|
B.startBlock(lBadDrop);
|
|
B.line(`call void @scr_union_release(ptr ${cached})`);
|
|
B.br(lBadRet);
|
|
B.startBlock(lBadRet);
|
|
B.terminate(`ret ptr null`);
|
|
B.startBlock(lOk);
|
|
const lRefresh = B.newLabel("dcu.refresh");
|
|
const lCache = B.newLabel("dcu.put");
|
|
B.condBr(hasCached, lRefresh, lCache);
|
|
B.startBlock(lRefresh);
|
|
const fields: [number, string, string][] = [
|
|
[1, "i32", "tag"],
|
|
[2, "ptr", "retain"],
|
|
[3, "ptr", "release"],
|
|
[4, "ptr", "trace"],
|
|
[5, "i64", "slot"],
|
|
];
|
|
fields.forEach(([index, fieldType, fieldName]) => {
|
|
const cachedPtr = B.tmp();
|
|
const checkedPtr = B.tmp();
|
|
const oldValue = B.tmp();
|
|
const newValue = B.tmp();
|
|
B.line(
|
|
`${cachedPtr} = getelementptr inbounds %ScrUnion, ptr ${cached}, i64 0, i32 ${index}`,
|
|
);
|
|
B.line(
|
|
`${checkedPtr} = getelementptr inbounds %ScrUnion, ptr ${checked}, i64 0, i32 ${index}`,
|
|
);
|
|
B.line(`${oldValue} = load ${fieldType}, ptr ${cachedPtr} ; ${fieldName}`);
|
|
B.line(`${newValue} = load ${fieldType}, ptr ${checkedPtr}`);
|
|
B.line(`store ${fieldType} ${newValue}, ptr ${cachedPtr}`);
|
|
B.line(`store ${fieldType} ${oldValue}, ptr ${checkedPtr}`);
|
|
});
|
|
B.line(`call void @scr_union_release(ptr ${checked})`);
|
|
B.terminate(`ret ptr ${cached}`);
|
|
B.startBlock(lCache);
|
|
// Later casts refresh this box in place (above), so from now on
|
|
// retypes of this union must not share a box (unionRetype).
|
|
B.line(`store i8 1, ptr ${host.retypeGuardRef(t.unionId)}`);
|
|
B.line(
|
|
`call void @scr_dyn_typed_ref_cache_cast(ptr %d, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr ${checked}, ptr ${rc.retain}, ptr ${rc.release}, i1 ${traceAdapter(host, t) !== null ? "true" : "false"})`,
|
|
);
|
|
B.terminate(`ret ptr ${checked}`);
|
|
B.startBlock(lFail);
|
|
this.unionArmFailure(B, def, kind);
|
|
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
|
|
B.terminate(`ret ptr null`);
|
|
break;
|
|
}
|
|
case "func": {
|
|
// The checked-dynamic function boundary, OUT direction: an
|
|
// IDENTICAL boxed signature unwraps the closure directly;
|
|
// anything else wraps in the per-target adapter closure whose
|
|
// caps[0] obj-box owns the dyn value (untraced).
|
|
const adapter = this.dynFuncAdapterHelper(t);
|
|
const sigLit = host.cstr(key);
|
|
const lWrap = B.newLabel("dcf.w");
|
|
host.declare(`declare zeroext i1 @scr_dyn_is_callable(ptr)`);
|
|
const callable = B.tmp();
|
|
const valid = B.newLabel("dcf.k");
|
|
const invalid = B.newLabel("dcf.f");
|
|
B.line(`${callable} = call zeroext i1 @scr_dyn_is_callable(ptr %d)`);
|
|
B.condBr(callable, valid, invalid);
|
|
B.startBlock(invalid);
|
|
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
|
|
B.terminate(`ret ${dummy}`);
|
|
B.startBlock(valid);
|
|
const direct = B.tmp();
|
|
const lSignature = B.newLabel("dcf.signature");
|
|
B.line(`${direct} = icmp eq i32 ${this.kindOf(B, "%d")}, ${DYN_KIND.FUNC}`);
|
|
B.condBr(direct, lSignature, lWrap);
|
|
B.startBlock(lSignature);
|
|
host.declare(`declare i32 @strcmp(ptr, ptr)`);
|
|
const sigp = B.tmp();
|
|
const sig = B.tmp();
|
|
B.line(
|
|
`${sigp} = getelementptr inbounds i8, ptr %d, i64 ${this.abiOffset(32, 24)} ; ->v.fn.sig`,
|
|
);
|
|
B.line(`${sig} = load ptr, ptr ${sigp}`);
|
|
const cmp = B.tmp();
|
|
const same = B.tmp();
|
|
B.line(`${cmp} = call i32 @strcmp(ptr ${sig}, ptr ${sigLit})`);
|
|
B.line(`${same} = icmp eq i32 ${cmp}, 0`);
|
|
const lSame = B.newLabel("dcf.s");
|
|
B.condBr(same, lSame, lWrap);
|
|
B.startBlock(lSame);
|
|
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
|
|
const clop = B.tmp();
|
|
const clo = B.tmp();
|
|
const r = B.tmp();
|
|
B.line(`${clop} = getelementptr inbounds i8, ptr %d, i64 16 ; ->v.fn.clo`);
|
|
B.line(`${clo} = load ptr, ptr ${clop}`);
|
|
B.line(`${r} = call ptr @scr_closure_retain_v(ptr ${clo})`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
B.startBlock(lWrap);
|
|
host.declare(`declare ptr @scr_closure_new(ptr, ${host.sizeType})`);
|
|
host.declare(`declare ptr @scr_box_new_obj(ptr, ptr, ptr)`);
|
|
host.declare(`declare void @scr_box_set_ref(ptr, ptr)`);
|
|
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
|
|
host.declare(`declare void @scr_dyn_trace_v(ptr, ptr, ptr)`);
|
|
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
|
const a = B.tmp();
|
|
B.line(`${a} = call ptr @scr_closure_new(ptr @${adapter}, ${host.sizeType} 1)`);
|
|
host.declare(`declare i32 @scr_dyn_function_kind(ptr)`);
|
|
const functionKind = B.tmp();
|
|
const targetKind = B.tmp();
|
|
B.line(`${functionKind} = call i32 @scr_dyn_function_kind(ptr %d)`);
|
|
B.line(`${targetKind} = getelementptr inbounds %ScrClosure, ptr ${a}, i64 0, i32 4`);
|
|
const adapterKind = B.tmp();
|
|
B.line(
|
|
`${adapterKind} = or i32 ${functionKind}, ${CLOSURE_KIND.CHECKED_ADAPTER} ; preserves the checked callable in caps[0]`,
|
|
);
|
|
B.line(`store i32 ${adapterKind}, ptr ${targetKind}`);
|
|
const box = B.tmp();
|
|
B.line(
|
|
`${box} = call ptr @scr_box_new_obj(ptr @scr_dyn_retain_v, ptr @scr_dyn_release_v, ptr @scr_dyn_trace_v)`,
|
|
);
|
|
const capp = B.tmp();
|
|
B.line(`${capp} = getelementptr inbounds %ScrClosure, ptr ${a}, i64 1 ; caps[0]`);
|
|
B.line(`store ptr ${box}, ptr ${capp}`);
|
|
const rd = this.retainDyn(B, "%d");
|
|
B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${rd})`);
|
|
// The shim is the same JS function under another signature: it
|
|
// takes the boxed closure's identity (kept alive through caps[0]).
|
|
host.declare(`declare void @scr_dyn_adopt_identity(ptr, ptr)`);
|
|
B.line(`call void @scr_dyn_adopt_identity(ptr ${a}, ptr %d)`);
|
|
B.terminate(`ret ptr ${a}`);
|
|
break;
|
|
}
|
|
default: {
|
|
// Runtime HANDLE targets: a tag-checked reference unwrap (+1 —
|
|
// identity, no copy; the runtime throws the path-annotated
|
|
// TypeError on any other kind or tag).
|
|
const h = DYN_HANDLE_KINDS.get(t.kind);
|
|
if (h) {
|
|
const r = B.tmp();
|
|
const unbox = DYN_HANDLE_UNBOX_FN[t.kind];
|
|
if (unbox) {
|
|
host.declare(`declare ptr @${unbox}(ptr, ptr, ptr)`);
|
|
B.line(`${r} = call ptr @${unbox}(ptr %d, ptr %path, ptr ${want})`);
|
|
} else {
|
|
host.declare(`declare ptr @scr_dyn_handle_unbox(ptr, i32, ptr, ptr)`);
|
|
B.line(
|
|
`${r} = call ptr @scr_dyn_handle_unbox(ptr %d, i32 ${DYN_HANDLE_TAG_NUM[t.kind]}, ptr %path, ptr ${want})`,
|
|
);
|
|
}
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
throw new LlvmUnsupportedError(`type:${t.kind}`);
|
|
}
|
|
}
|
|
this.defs.push(
|
|
`define internal ${retTy === "i1" ? "zeroext i1" : retTy} @${recursive || memo ? `${name}_body` : name}(ptr %d, ptr %path) ${FN_ATTRS} { ; check ${key}`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
if (recursive || memo) this.emitVisitWrapper(name, t, recursive ? "build" : "memo");
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/** `i1`: %d is a handle box with tag `tag`. */
|
|
private handleTagMatches(B: BlockBuilder, tag: number): string {
|
|
const kd = this.kindOf(B, "%d");
|
|
const isHandle = B.tmp();
|
|
B.line(`${isHandle} = icmp eq i32 ${kd}, ${DYN_KIND.HANDLE}`);
|
|
const lHandle = B.newLabel("dm.handle");
|
|
const lDone = B.newLabel("dm.handle.done");
|
|
const entry = B.currentLabel();
|
|
B.condBr(isHandle, lHandle, lDone);
|
|
B.startBlock(lHandle);
|
|
const tagPtr = B.tmp();
|
|
const loaded = B.tmp();
|
|
B.line(
|
|
`${tagPtr} = getelementptr inbounds i8, ptr %d, i64 ${this.abiOffset(24, 20)} ; ->v.handle.tag`,
|
|
);
|
|
B.line(`${loaded} = load i32, ptr ${tagPtr}`);
|
|
const same = B.tmp();
|
|
B.line(`${same} = icmp eq i32 ${loaded}, ${tag}`);
|
|
B.br(lDone);
|
|
B.startBlock(lDone);
|
|
const matched = B.tmp();
|
|
B.line(`${matched} = phi i1 [ false, %${entry} ], [ ${same}, %${lHandle} ]`);
|
|
return matched;
|
|
}
|
|
|
|
/** A TextEncoder, TextDecoder or StringDecoder record boxes as its
|
|
* handle (+1), which holds the record by reference. The decoders pass
|
|
* the byte offsets of the fields their boxed methods read. */
|
|
private textCodecToDyn(
|
|
B: BlockBuilder,
|
|
t: IrType & { kind: "record" },
|
|
shape: IrRecordShape,
|
|
codec: "TextEncoder" | "TextDecoder" | "StringDecoder",
|
|
): void {
|
|
const host = this.host;
|
|
const rc = vAdapters(host, t);
|
|
const r = B.tmp();
|
|
if (codec === "TextEncoder") {
|
|
host.declare(`declare ptr @scr_text_encoder_to_dyn(ptr, ptr, ptr)`);
|
|
B.line(
|
|
`${r} = call ptr @scr_text_encoder_to_dyn(ptr %v, ptr ${rc.retain}, ptr ${rc.release})`,
|
|
);
|
|
} else {
|
|
const struct = mangleRecordStruct(t.shapeId);
|
|
const base = host.nullableUnions.fieldBase(t.shapeId);
|
|
const start = B.tmp();
|
|
B.line(`${start} = ptrtoint ptr %v to ${this.S}`);
|
|
const fields: readonly string[] =
|
|
codec === "StringDecoder" ? STRING_DECODER_FIELDS : TEXT_DECODER_FIELDS;
|
|
const offsets = fields.map((field) => {
|
|
const index = shape.fields.findIndex((f) => f.name === field);
|
|
if (index < 0)
|
|
throw new InternalCompilerError(`llvm emitter bug: ${codec} record lacks ${field}`);
|
|
const p = B.tmp();
|
|
const at = B.tmp();
|
|
const offset = B.tmp();
|
|
B.line(
|
|
`${p} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${index + base} ; .${llvmCommentText(field)}`,
|
|
);
|
|
B.line(`${at} = ptrtoint ptr ${p} to ${this.S}`);
|
|
B.line(`${offset} = sub ${this.S} ${at}, ${start}`);
|
|
return `${this.S} ${offset}`;
|
|
});
|
|
const fn =
|
|
codec === "StringDecoder" ? "scr_string_decoder_to_dyn" : "scr_text_decoder_to_dyn";
|
|
host.declare(`declare ptr @${fn}(ptr, ptr, ptr, ${fields.map(() => this.S).join(", ")})`);
|
|
B.line(
|
|
`${r} = call ptr @${fn}(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ${offsets.join(", ")})`,
|
|
);
|
|
}
|
|
B.terminate(`ret ptr ${r}`);
|
|
}
|
|
|
|
/* ── toDynHelper (walkers.ts, ported) ─────────────────────────── */
|
|
|
|
/** `sc_td_<n>(<T> v) -> ptr` — build a fresh dyn value from a
|
|
* static one (+1), DEEP-COPYING composites. Borrows the operand.
|
|
* Never throws. */
|
|
toDynHelper(t: IrType): string {
|
|
const key = typeKey(t);
|
|
const existing = this.toDynFns.get(key);
|
|
if (existing) return existing;
|
|
const name = `sc_td_${this.toDynFns.size}`;
|
|
this.toDynFns.set(key, name);
|
|
this.host.journalAllocated?.(name, "dyn.toDynHelper", [t]);
|
|
const host = this.host;
|
|
const B = new BlockBuilder();
|
|
let sourceAccessor: { name: string; release: string } | null = null;
|
|
switch (t.kind) {
|
|
case "date": {
|
|
// A Date handle holding the same Date object.
|
|
host.declare(`declare ptr @scr_dyn_native_date(ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_native_date(ptr %v)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "url":
|
|
case "searchParams": {
|
|
const native = t.kind === "url" ? "url" : "search_params";
|
|
host.declare(`declare ptr @scr_dyn_native_${native}(ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_native_${native}(ptr %v)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "regex": {
|
|
host.declare(`declare ptr @scr_dyn_native_regex(ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_native_regex(ptr %v)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "map":
|
|
case "set": {
|
|
host.declare(`declare ptr @scr_dyn_native_${t.kind}(ptr)`);
|
|
if (
|
|
t.kind === "map" ? t.key.kind !== "dyn" || t.value.kind !== "dyn" : t.elem.kind !== "dyn"
|
|
) {
|
|
host.declare(`declare void @scr_map_dyn_attach(ptr, ptr)`);
|
|
B.line(`call void @scr_map_dyn_attach(ptr %v, ptr @${this.collectionDynOps(t)})`);
|
|
}
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_native_${t.kind}(ptr %v)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "bigint": {
|
|
host.declare(`declare ptr @scr_dyn_new_bigint(ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_new_bigint(ptr %v)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "symbol": {
|
|
host.declare(`declare ptr @scr_dyn_new_symbol(ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_new_symbol(ptr %v)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "f64": {
|
|
host.declare(`declare ptr @scr_dyn_new_num(double)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_new_num(double %v)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "bool": {
|
|
host.declare(`declare ptr @scr_dyn_new_bool(i1 zeroext)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_new_bool(i1 %v)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "procStream": {
|
|
host.declare(`declare ptr @scr_process_stdio(double)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_process_stdio(double %v)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "string": {
|
|
host.declare(`declare ptr @scr_dyn_new_str(ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_new_str(ptr %v) ; retains v`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "generator": {
|
|
// Keep the suspended fiber and its channels by identity. Member
|
|
// calls are generated from the exact capsule key by the frontend.
|
|
const rc = vAdapters(host, t);
|
|
const r = B.tmp();
|
|
const snapshot = `${name}_snapshot`;
|
|
const message =
|
|
"property reflection on a native generator through an untyped value is not supported yet";
|
|
host.declare(`declare void @scr_throw_error_msg_code(i32, ptr, ${host.sizeType}, ptr)`);
|
|
this.defs.push(
|
|
`define internal ptr @${snapshot}(ptr %v) ${FN_ATTRS} {`,
|
|
`entry:`,
|
|
` call void @scr_throw_error_msg_code(i32 0, ptr ${host.cstr(message)}, ${host.sizeType} ${Buffer.byteLength(message, "utf8")}, ptr ${host.cstr("SC1071")})`,
|
|
` ret ptr null`,
|
|
`}`,
|
|
``,
|
|
);
|
|
B.line(
|
|
`${r} = call ptr ${typedRefConstructor(host, t)}(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${snapshot}, ptr null)`,
|
|
);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "object": {
|
|
const className = t.className;
|
|
if (host.isErrorClass(className)) {
|
|
host.declare(`declare ptr @scr_dyn_from_error(ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_from_error(ptr %v)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
if (!isDynTypedRefType(t)) {
|
|
throw new InternalCompilerError(`llvm emitter bug: to-dyn of runtime class ${className}`);
|
|
}
|
|
const adapter = host.liveDynRefAdapter(t);
|
|
const rc = vAdapters(host, t);
|
|
const keyLit = host.cstr(typeKey(t));
|
|
|
|
const r = B.tmp();
|
|
B.line(
|
|
`${r} = call ptr ${typedRefConstructor(host, t)}(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ptr ${keyLit}, ${host.sizeType} ${Buffer.byteLength(typeKey(t), "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
|
|
);
|
|
// The same live view as a direct box: class members for `in` and
|
|
// the class name for inspection.
|
|
for (const line of bindLiveArrayOps(host, adapter, r)) B.line(line);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "dyn": {
|
|
const r = this.retainDyn(B, "%v");
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "classval": {
|
|
host.declare(`declare ptr @scr_dyn_new_class(ptr, ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_new_class(ptr %v, ptr ${host.cstr(key)})`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "func": {
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @${this.dynFuncBoxHelper(t)}(ptr %v, ptr null)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "bytes": {
|
|
host.declare(`declare ptr @scr_dyn_new_bytes(ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_new_bytes(ptr %v)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
case "record": {
|
|
const shape = host.recordsById.get(t.shapeId);
|
|
if (!shape)
|
|
throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown shape ${t.shapeId}`);
|
|
const codec = recordHandleCodecClass(shape);
|
|
if (codec !== null) {
|
|
this.textCodecToDyn(B, t, shape, codec);
|
|
break;
|
|
}
|
|
const struct = mangleRecordStruct(t.shapeId);
|
|
const base = host.nullableUnions.fieldBase(t.shapeId);
|
|
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base]));
|
|
const loadFieldOf = (fname: string, ft: IrType): string => {
|
|
const p = B.tmp();
|
|
B.line(
|
|
`${p} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${fieldIndex.get(fname)!} ; .${llvmCommentText(fname)}`,
|
|
);
|
|
// An inline union field lends a private stack box (ABSENT kept).
|
|
if (host.inlineUnions.record(ft)) {
|
|
const { tag, bits } = loadInlineUnion(B, p);
|
|
return stackUnionBox(B, host.sizeType, tag, bits);
|
|
}
|
|
const raw = B.tmp();
|
|
B.line(`${raw} = load ${llFieldType(ft, false)}, ptr ${p}`);
|
|
if (llFieldType(ft, false) !== "i8") return raw;
|
|
const b = B.tmp();
|
|
B.line(`${b} = trunc i8 ${raw} to i1`);
|
|
return b;
|
|
};
|
|
host.declare(`declare void @scr_dyn_obj_set(ptr, ptr, ${host.sizeType}, ptr)`);
|
|
// CYCLE-CAPABLE shapes guard the deep copy: enter TRAPS on a value
|
|
// already being converted (a cyclic value has no finite dyn copy —
|
|
// SEMANTICS.md).
|
|
const cyclicRec = traceAdapter(host, t) !== null;
|
|
if (cyclicRec) {
|
|
host.declare(`declare void @scr_dyn_from_enter(ptr)`);
|
|
host.declare(`declare void @scr_dyn_from_leave()`);
|
|
B.line(`call void @scr_dyn_from_enter(ptr %v)`);
|
|
}
|
|
if (shape.tuple) {
|
|
// A tuple converts as the JSON ARRAY it is everywhere else.
|
|
host.declare(`declare ptr @scr_dyn_new_arr()`);
|
|
host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
|
|
const byIndex = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
|
|
const d = B.tmp();
|
|
B.line(`${d} = call ptr @scr_dyn_new_arr()`);
|
|
for (const f of byIndex) {
|
|
const fv = loadFieldOf(f.name, f.type);
|
|
const conv = B.tmp();
|
|
B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`);
|
|
B.line(`call void @scr_dyn_arr_push(ptr ${d}, ptr ${conv})`);
|
|
}
|
|
if (cyclicRec) B.line(`call void @scr_dyn_from_leave()`);
|
|
host.declare(`declare ptr @scr_dyn_mark_snapshot(ptr)`);
|
|
B.line(`call ptr @scr_dyn_mark_snapshot(ptr ${d})`);
|
|
B.terminate(`ret ptr ${d}`);
|
|
break;
|
|
}
|
|
const d = B.tmp();
|
|
if (recordDynKeepsIdentity(shape)) {
|
|
const rc = vAdapters(host, t);
|
|
sourceAccessor = {
|
|
name: `${name}_source_access`,
|
|
release: rc.release,
|
|
};
|
|
host.declare(`declare ptr @scr_dyn_new_obj_with_identity(ptr, ptr, ptr)`);
|
|
B.line(
|
|
`${d} = call ptr @scr_dyn_new_obj_with_identity(ptr %v, ptr ${rc.retain}, ptr @${sourceAccessor.name})`,
|
|
);
|
|
} else if (shape.indexValue && shape.fields.length === 0) {
|
|
// A pure index-signature record keeps a null prototype
|
|
// (SCR_MAP_NULL_PROTO on its overflow map; Object.groupBy).
|
|
host.declare(`declare ptr @scr_dyn_new_obj()`);
|
|
host.declare(`declare ptr @scr_dyn_new_obj_null_proto()`);
|
|
const ovfp = B.tmp();
|
|
const ovf = B.tmp();
|
|
const flagsp = B.tmp();
|
|
const flags = B.tmp();
|
|
const np = B.tmp();
|
|
const alloc = B.tmp();
|
|
B.line(`${ovfp} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 1`);
|
|
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
|
|
B.line(
|
|
`${flagsp} = getelementptr inbounds i8, ptr ${ovf}, i64 ${mapFlagsOffset(host.sizeType)}`,
|
|
);
|
|
B.line(`${flags} = load i8, ptr ${flagsp}`);
|
|
B.line(`${np} = icmp ne i8 ${flags}, 0`);
|
|
B.line(
|
|
`${alloc} = select i1 ${np}, ptr @scr_dyn_new_obj_null_proto, ptr @scr_dyn_new_obj`,
|
|
);
|
|
B.line(`${d} = call ptr ${alloc}()`);
|
|
} else {
|
|
host.declare(`declare ptr @scr_dyn_new_obj()`);
|
|
B.line(`${d} = call ptr @scr_dyn_new_obj()`);
|
|
}
|
|
// Keys insert in DECLARED order (JS insertion order), or in the
|
|
// value's own order for a shape with orderVariants; internal
|
|
// '%'-fields follow so a record→dyn→record round trip keeps them
|
|
// (all but the order index, which is no key).
|
|
const byName = new Map(shape.fields.map((f) => [f.name, f]));
|
|
// An accessor property is one key whose getter and setter are the
|
|
// slot closures (see accessorSlotProp), in accessorOrder's place.
|
|
const orders =
|
|
shape.accessorOrder !== undefined ? [shape.accessorOrder] : shapeKeyOrders(shape);
|
|
const perOrder = (order: readonly string[]): void => {
|
|
const inOrder = new Set(order);
|
|
const accessors = new Set(
|
|
shape.accessorOrder === undefined
|
|
? []
|
|
: shape.fields.flatMap((f) => {
|
|
const slot = accessorSlotProp(f.name);
|
|
return slot === null ? [] : [slot.prop];
|
|
}),
|
|
);
|
|
type DynField = { name: string; type: IrType } | { accessor: string };
|
|
const dynFields: DynField[] = [
|
|
...order.flatMap((n) => {
|
|
if (accessors.has(n)) return [{ accessor: n } as DynField];
|
|
const f = byName.get(n);
|
|
return f === undefined ? [] : [f as DynField];
|
|
}),
|
|
...shape.fields.filter(
|
|
(f) =>
|
|
!inOrder.has(f.name) &&
|
|
f.name !== KEY_ORDER_FIELD &&
|
|
!(accessors.size > 0 && accessorSlotProp(f.name) !== null),
|
|
),
|
|
];
|
|
for (const entry of dynFields) {
|
|
if ("accessor" in entry) {
|
|
host.declare(
|
|
`declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`,
|
|
);
|
|
const boxed = (kind: "get" | "set"): string => {
|
|
const slot = byName.get(`%${kind}:${entry.accessor}`);
|
|
if (slot === undefined) return "null";
|
|
const fv = loadFieldOf(slot.name, slot.type);
|
|
const conv = B.tmp();
|
|
B.line(
|
|
`${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`,
|
|
);
|
|
return conv;
|
|
};
|
|
const getter = boxed("get");
|
|
const setter = boxed("set");
|
|
B.line(
|
|
`call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`,
|
|
);
|
|
continue;
|
|
}
|
|
const f = entry;
|
|
const klen = Buffer.byteLength(f.name, "utf8");
|
|
const fv = loadFieldOf(f.name, f.type);
|
|
// An ABSENT optional field contributes no key.
|
|
const lSkip = f.type.kind === "union" ? this.skipIfAbsent(B, fv, f.type) : null;
|
|
const conv = B.tmp();
|
|
B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`);
|
|
B.line(
|
|
`call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`,
|
|
);
|
|
if (lSkip !== null) {
|
|
B.br(lSkip);
|
|
B.startBlock(lSkip);
|
|
}
|
|
}
|
|
};
|
|
if (orders.length === 1) perOrder(orders[0]!);
|
|
else emitPerKeyOrder(B, shape, base, "%v", perOrder);
|
|
// A Dirent row takes Node's Dirent prototype (the type probes) and
|
|
// its Symbol(type) key; %dtype stays readable but non-enumerable.
|
|
if (isDirentShape(shape)) {
|
|
host.declare(`declare void @scr_dyn_dirent_finish(ptr)`);
|
|
B.line(`call void @scr_dyn_dirent_finish(ptr ${d})`);
|
|
}
|
|
if (shape.indexValue) {
|
|
const iv = shape.indexValue;
|
|
host.declare(`declare ptr @scr_map_keys_js_order(ptr)`);
|
|
host.declare(`declare double @scr_arr_len(ptr)`);
|
|
host.declare(`declare ptr @scr_arr_get_ref(ptr, double)`);
|
|
host.declare(`declare void @scr_str_release(ptr)`);
|
|
host.declare(`declare void @scr_arr_release(ptr)`);
|
|
const ovfp = B.tmp();
|
|
const ovf = B.tmp();
|
|
B.line(
|
|
`${ovfp} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${shape.fields.length + base}`,
|
|
);
|
|
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
|
|
const ks = B.tmp();
|
|
const len = B.tmp();
|
|
B.line(`${ks} = call ptr @scr_map_keys_js_order(ptr ${ovf})`);
|
|
B.line(`${len} = call double @scr_arr_len(ptr ${ks})`);
|
|
B.countedLoop(len, (i) => {
|
|
const k = B.tmp();
|
|
B.line(`${k} = call ptr @scr_arr_get_ref(ptr ${ks}, double ${i}) ; key (+1)`);
|
|
const { len: klen, data: kdata } = this.strParts(B, k);
|
|
if (iv.kind === "f64" || iv.kind === "bool") {
|
|
const outTy = iv.kind === "f64" ? "double" : "i8";
|
|
const outSlot = B.slot();
|
|
B.entryAllocas.push(`${outSlot} = alloca ${outTy}`);
|
|
B.line(`store ${outTy} ${iv.kind === "f64" ? f64Lit(0) : "0"}, ptr ${outSlot}`);
|
|
host.declare(
|
|
`declare zeroext i1 @scr_map_get_str_${iv.kind === "f64" ? "f64" : "bool"}(ptr, ptr, ptr)`,
|
|
);
|
|
const found = B.tmp();
|
|
B.line(
|
|
`${found} = call zeroext i1 @scr_map_get_str_${iv.kind === "f64" ? "f64" : "bool"}(ptr ${ovf}, ptr ${k}, ptr ${outSlot})`,
|
|
);
|
|
const raw = B.tmp();
|
|
B.line(`${raw} = load ${outTy}, ptr ${outSlot}`);
|
|
let val = raw;
|
|
if (iv.kind === "bool") {
|
|
val = B.tmp();
|
|
B.line(`${val} = trunc i8 ${raw} to i1`);
|
|
}
|
|
const boxed = B.tmp();
|
|
if (iv.kind === "f64") {
|
|
host.declare(`declare ptr @scr_dyn_new_num(double)`);
|
|
B.line(`${boxed} = call ptr @scr_dyn_new_num(double ${val})`);
|
|
} else {
|
|
host.declare(`declare ptr @scr_dyn_new_bool(i1 zeroext)`);
|
|
B.line(`${boxed} = call ptr @scr_dyn_new_bool(i1 ${val})`);
|
|
}
|
|
B.line(
|
|
`call void @scr_dyn_obj_set(ptr ${d}, ptr ${kdata}, ${host.sizeType} ${klen}, ptr ${boxed})`,
|
|
);
|
|
} else if (iv.kind === "dyn") {
|
|
// get_str_ref returns +1 — exactly the ownership obj_set takes.
|
|
host.declare(`declare ptr @scr_map_get_str_ref(ptr, ptr)`);
|
|
const hit = B.tmp();
|
|
B.line(`${hit} = call ptr @scr_map_get_str_ref(ptr ${ovf}, ptr ${k})`);
|
|
B.line(
|
|
`call void @scr_dyn_obj_set(ptr ${d}, ptr ${kdata}, ${host.sizeType} ${klen}, ptr ${hit})`,
|
|
);
|
|
} else {
|
|
host.declare(`declare ptr @scr_map_get_str_ref(ptr, ptr)`);
|
|
const hit = B.tmp();
|
|
B.line(`${hit} = call ptr @scr_map_get_str_ref(ptr ${ovf}, ptr ${k})`);
|
|
const conv = B.tmp();
|
|
B.line(`${conv} = call ptr @${this.toDynHelper(iv)}(ptr ${hit})`);
|
|
B.line(
|
|
`call void @scr_dyn_obj_set(ptr ${d}, ptr ${kdata}, ${host.sizeType} ${klen}, ptr ${conv})`,
|
|
);
|
|
B.line(`call void ${releaseSym(host, iv)}(ptr ${hit})`);
|
|
}
|
|
B.line(`call void @scr_str_release(ptr ${k})`);
|
|
});
|
|
B.line(`call void @scr_arr_release(ptr ${ks})`);
|
|
}
|
|
if (cyclicRec) B.line(`call void @scr_dyn_from_leave()`);
|
|
let snapshot = d;
|
|
if (!shape.indexValue && !recordDynKeepsIdentity(shape)) {
|
|
// Undeclared properties the record kept from its checked source
|
|
// rejoin the copy in their own-key positions.
|
|
host.declare(`declare ptr @scr_record_source_apply(ptr, ptr)`);
|
|
snapshot = B.tmp();
|
|
B.line(`${snapshot} = call ptr @scr_record_source_apply(ptr ${d}, ptr %v)`);
|
|
}
|
|
host.declare(`declare ptr @scr_dyn_mark_snapshot(ptr)`);
|
|
B.line(`call ptr @scr_dyn_mark_snapshot(ptr ${snapshot})`);
|
|
B.terminate(`ret ptr ${snapshot}`);
|
|
break;
|
|
}
|
|
case "array": {
|
|
const elem = t.elem;
|
|
host.declare(`declare ptr @scr_dyn_new_arr()`);
|
|
host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
|
|
host.declare(`declare double @scr_arr_len(ptr)`);
|
|
// Cycle-capable arrays guard the deep copy like records above.
|
|
const cyclicArr = traceAdapter(host, t) !== null;
|
|
if (cyclicArr) {
|
|
host.declare(`declare void @scr_dyn_from_enter(ptr)`);
|
|
host.declare(`declare void @scr_dyn_from_leave()`);
|
|
B.line(`call void @scr_dyn_from_enter(ptr %v)`);
|
|
}
|
|
const d = B.tmp();
|
|
B.line(`${d} = call ptr @scr_dyn_new_arr()`);
|
|
const len = B.tmp();
|
|
B.line(`${len} = call double @scr_arr_len(ptr %v)`);
|
|
B.countedLoop(len, (i) => {
|
|
host.declare(`declare double @scr_arr_state(ptr, double)`);
|
|
const state = B.tmp();
|
|
const undefinedState = B.tmp();
|
|
const undefinedLabel = B.newLabel("tda.undefined");
|
|
const holeLabel = B.newLabel("tda.hole");
|
|
const presentLabel = B.newLabel("tda.present");
|
|
const valueLabel = B.newLabel("tda.value");
|
|
const doneLabel = B.newLabel("tda.done");
|
|
B.line(`${state} = call double @scr_arr_state(ptr %v, double ${i})`);
|
|
const hole = B.tmp();
|
|
B.line(`${hole} = fcmp oeq double ${state}, 0.0`); // SCR_ARR_HOLE
|
|
B.condBr(hole, holeLabel, presentLabel);
|
|
B.startBlock(holeLabel);
|
|
host.declare(`declare void @scr_dyn_arr_push_hole(ptr)`);
|
|
B.line(`call void @scr_dyn_arr_push_hole(ptr ${d})`);
|
|
B.br(doneLabel);
|
|
B.startBlock(presentLabel);
|
|
B.line(`${undefinedState} = fcmp oeq double ${state}, 2.0`); // SCR_ARR_UNDEFINED
|
|
if (host.mayHoldForeign(t.elem)) {
|
|
// A foreign element (SCR_ARR_FOREIGN) holds its own value.
|
|
const typedLabel = B.newLabel("tda.typed");
|
|
const foreignLabel = B.newLabel("tda.foreign");
|
|
B.terminate(`br i1 ${undefinedState}, label %${undefinedLabel}, label %${typedLabel}`);
|
|
B.startBlock(typedLabel);
|
|
const foreign = B.tmp();
|
|
B.line(`${foreign} = fcmp oeq double ${state}, 3.0`);
|
|
B.condBr(foreign, foreignLabel, valueLabel);
|
|
B.startBlock(foreignLabel);
|
|
host.declare(`declare ptr @scr_arr_get_foreign(ptr, double)`);
|
|
host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
|
|
const payload = B.tmp();
|
|
B.line(`${payload} = call ptr @scr_arr_get_foreign(ptr %v, double ${i}) ; +1`);
|
|
B.line(`call void @scr_dyn_arr_push(ptr ${d}, ptr ${payload})`);
|
|
B.br(doneLabel);
|
|
} else {
|
|
B.terminate(`br i1 ${undefinedState}, label %${undefinedLabel}, label %${valueLabel}`);
|
|
}
|
|
B.startBlock(undefinedLabel);
|
|
const retained = this.retainDyn(B, this.undef(B));
|
|
B.line(`call void @scr_dyn_arr_push(ptr ${d}, ptr ${retained})`);
|
|
B.terminate(`br label %${doneLabel}`);
|
|
B.startBlock(valueLabel);
|
|
if (elem.kind === "f64" || elem.kind === "bool") {
|
|
const acc = elem.kind;
|
|
const accTy = elem.kind === "f64" ? "double" : "i1";
|
|
host.declare(
|
|
`declare ${elem.kind === "bool" ? "zeroext i1" : accTy} @scr_arr_get_${acc}(ptr, double)`,
|
|
);
|
|
const e = B.tmp();
|
|
B.line(`${e} = call ${accTy} @scr_arr_get_${acc}(ptr %v, double ${i})`);
|
|
const boxed = B.tmp();
|
|
if (elem.kind === "f64") {
|
|
host.declare(`declare ptr @scr_dyn_new_num(double)`);
|
|
B.line(`${boxed} = call ptr @scr_dyn_new_num(double ${e})`);
|
|
} else {
|
|
host.declare(`declare ptr @scr_dyn_new_bool(i1 zeroext)`);
|
|
B.line(`${boxed} = call ptr @scr_dyn_new_bool(i1 ${e})`);
|
|
}
|
|
B.line(`call void @scr_dyn_arr_push(ptr ${d}, ptr ${boxed})`);
|
|
} else {
|
|
host.declare(`declare ptr @scr_arr_get_ref(ptr, double)`);
|
|
const e = B.tmp();
|
|
B.line(`${e} = call ptr @scr_arr_get_ref(ptr %v, double ${i}) ; +1`);
|
|
const conv = B.tmp();
|
|
B.line(`${conv} = call ptr @${this.toDynHelper(elem)}(ptr ${e})`);
|
|
B.line(`call void @scr_dyn_arr_push(ptr ${d}, ptr ${conv})`);
|
|
B.line(`call void ${releaseSym(host, elem)}(ptr ${e})`);
|
|
}
|
|
B.terminate(`br label %${doneLabel}`);
|
|
B.startBlock(doneLabel);
|
|
});
|
|
host.declare(`declare void @scr_arr_copy_metadata(ptr, ptr)`);
|
|
B.line(`call void @scr_arr_copy_metadata(ptr %v, ptr ${d})`);
|
|
if (cyclicArr) B.line(`call void @scr_dyn_from_leave()`);
|
|
host.declare(`declare ptr @scr_dyn_mark_snapshot(ptr)`);
|
|
B.line(`call ptr @scr_dyn_mark_snapshot(ptr ${d})`);
|
|
B.terminate(`ret ptr ${d}`);
|
|
break;
|
|
}
|
|
case "union": {
|
|
const def = host.unionsById.get(t.unionId);
|
|
if (!def)
|
|
throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown union ${t.unionId}`);
|
|
const nullable = host.nullableUnions.get(t.unionId);
|
|
const tag = emitUnionTag(B, nullable, "%v");
|
|
const bad = B.newLabel("tdu.bad");
|
|
const labels = def.arms.map(() => B.newLabel("tdu.a"));
|
|
B.terminate(
|
|
`switch i32 ${tag}, label %${bad} [ ${def.arms.map((_, i) => `i32 ${i}, label %${labels[i]}`).join(" ")} ]`,
|
|
);
|
|
def.arms.forEach((arm, i) => {
|
|
B.startBlock(labels[i]!);
|
|
if (arm.kind === "undefinedT") {
|
|
const u = this.undef(B);
|
|
const r = this.retainDyn(B, u);
|
|
B.terminate(`ret ptr ${r}`);
|
|
} else if (arm.kind === "nullT") {
|
|
host.declare(`declare ptr @scr_dyn_new_null()`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_new_null()`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
} else if (arm.kind === "f64" || arm.kind === "procStream") {
|
|
host.declare(`declare double @scr_union_get_f64(ptr)`);
|
|
const x = B.tmp();
|
|
const r = B.tmp();
|
|
B.line(`${x} = call double @scr_union_get_f64(ptr %v)`);
|
|
B.line(`${r} = call ptr @${this.toDynHelper(arm)}(double ${x})`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
} else if (arm.kind === "bool") {
|
|
host.declare(`declare zeroext i1 @scr_union_get_bool(ptr)`);
|
|
host.declare(`declare ptr @scr_dyn_new_bool(i1 zeroext)`);
|
|
const x = B.tmp();
|
|
const r = B.tmp();
|
|
B.line(`${x} = call zeroext i1 @scr_union_get_bool(ptr %v)`);
|
|
B.line(`${r} = call ptr @scr_dyn_new_bool(i1 ${x})`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
} else if (arm.kind === "func") {
|
|
// A boxable function arm crosses through the checked-dynamic
|
|
// function boundary (the dynFrom func special case, sans name).
|
|
const p = emitUnionPeek(B, nullable, "%v");
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @${this.dynFuncBoxHelper(arm)}(ptr ${p}, ptr null)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
} else {
|
|
const p = emitUnionPeek(B, nullable, "%v");
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @${this.toDynHelper(arm)}(ptr ${p}) ; ${arm.kind}`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
}
|
|
});
|
|
B.startBlock(bad);
|
|
host.needBadTag();
|
|
B.line(`call void @sc_bad_tag()`);
|
|
B.terminate(`unreachable`);
|
|
break;
|
|
}
|
|
case "promise": {
|
|
// Promises box by REFERENCE (SCR_DYN_PROMISE — identity is the
|
|
// promise): promise<dyn> carries its ScrPromise directly (the
|
|
// payload is already a dyn value); any other inner boxes an
|
|
// ADAPTER promise whose settle callback converts the payload
|
|
// (rejections copy raw inside the runtime's cb-waiter machinery).
|
|
if (t.inner.kind === "dyn") {
|
|
host.declare(`declare ptr @scr_dyn_new_promise(ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_new_promise(ptr %v)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
host.declare(`declare ptr @scr_dyn_new_promise_adapting(ptr, ptr)`);
|
|
const adapter = this.promiseDynAdapterHelper(t.inner);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_new_promise_adapting(ptr %v, ptr @${adapter})`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
default: {
|
|
// Runtime HANDLE kinds box by REFERENCE (identity — no copy):
|
|
// scr_dyn_new_handle retains the borrowed operand through the
|
|
// tag's installed ops.
|
|
const h = DYN_HANDLE_KINDS.get(t.kind);
|
|
if (h) {
|
|
const box = DYN_HANDLE_BOX_FN[t.kind];
|
|
const r = B.tmp();
|
|
if (box) {
|
|
host.declare(`declare ptr @${box}(ptr)`);
|
|
B.line(`${r} = call ptr @${box}(ptr %v)`);
|
|
} else {
|
|
host.declare(`declare ptr @scr_dyn_new_handle(ptr, i32)`);
|
|
B.line(
|
|
`${r} = call ptr @scr_dyn_new_handle(ptr %v, i32 ${DYN_HANDLE_TAG_NUM[t.kind]})`,
|
|
);
|
|
}
|
|
B.terminate(`ret ptr ${r}`);
|
|
break;
|
|
}
|
|
throw new LlvmUnsupportedError(`type:${t.kind}`);
|
|
}
|
|
}
|
|
this.defs.push(
|
|
`define internal ptr @${name}(${this.valTy(t)} %v) ${FN_ATTRS} { ; to-dyn ${key}`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
if (sourceAccessor) {
|
|
this.defs.push(
|
|
`define internal ptr @${sourceAccessor.name}(ptr %v, i1 %materialize) ${FN_ATTRS} { ; record source ${key}`,
|
|
`entry:`,
|
|
` br i1 %materialize, label %snapshot, label %release`,
|
|
`snapshot:`,
|
|
` %d = call ptr @${name}(ptr %v)`,
|
|
` ret ptr %d`,
|
|
`release:`,
|
|
` call void ${sourceAccessor.release}(ptr %v)`,
|
|
` ret ptr null`,
|
|
`}`,
|
|
``,
|
|
);
|
|
}
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/** The checked-dynamic tree-promise settle adapter for one fulfillment payload type —
|
|
* promiseDynAdapterHelper (walkers.ts), ported:
|
|
* `void sc_pda_<n>(ptr %dst, ptr %src)` reads src's fulfilled payload
|
|
* by its compile-time kind, converts it to a dyn value, and fulfills
|
|
* dst with it (scr_dyn_new_promise_adapting's callback; rejections
|
|
* never reach an adapter — the runtime copies them raw). Interned per
|
|
* inner typeKey. */
|
|
private promiseDynAdapterHelper(inner: IrType): string {
|
|
const key = typeKey(inner);
|
|
const existing = this.promiseDynAdapters.get(key);
|
|
if (existing) return existing;
|
|
const name = `sc_pda_${this.promiseDynAdapters.size}`;
|
|
this.promiseDynAdapters.set(key, name);
|
|
this.host.journalAllocated?.(name, "dyn.promiseDynAdapterHelper", [inner]);
|
|
const host = this.host;
|
|
const B = new BlockBuilder();
|
|
host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
|
|
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
|
|
host.declare(`declare void @scr_dyn_trace_v(ptr, ptr, ptr)`);
|
|
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
|
const fulfill = (dv: string): void => {
|
|
B.line(
|
|
`call void @scr_promise_fulfill_ref(ptr %dst, ptr ${dv}, ptr @scr_dyn_retain_v, ptr @scr_dyn_release_v, ptr @scr_dyn_trace_v)`,
|
|
);
|
|
};
|
|
switch (inner.kind) {
|
|
case "void":
|
|
case "undefinedT": {
|
|
const u = this.undef(B);
|
|
fulfill(this.retainDyn(B, u));
|
|
break;
|
|
}
|
|
case "nullT": {
|
|
host.declare(`declare ptr @scr_dyn_new_null()`);
|
|
const dv = B.tmp();
|
|
B.line(`${dv} = call ptr @scr_dyn_new_null()`);
|
|
fulfill(dv);
|
|
break;
|
|
}
|
|
case "procStream":
|
|
case "f64": {
|
|
host.declare(`declare double @scr_promise_payload_f64(ptr)`);
|
|
const x = B.tmp();
|
|
const dv = B.tmp();
|
|
B.line(`${x} = call double @scr_promise_payload_f64(ptr %src)`);
|
|
B.line(`${dv} = call ptr @${this.toDynHelper(inner)}(double ${x})`);
|
|
fulfill(dv);
|
|
break;
|
|
}
|
|
case "bool": {
|
|
host.declare(`declare zeroext i1 @scr_promise_payload_bool(ptr)`);
|
|
host.declare(`declare ptr @scr_dyn_new_bool(i1 zeroext)`);
|
|
const x = B.tmp();
|
|
const dv = B.tmp();
|
|
B.line(`${x} = call zeroext i1 @scr_promise_payload_bool(ptr %src)`);
|
|
B.line(`${dv} = call ptr @scr_dyn_new_bool(i1 ${x})`);
|
|
fulfill(dv);
|
|
break;
|
|
}
|
|
case "string": {
|
|
host.declare(`declare ptr @scr_promise_payload_str(ptr)`);
|
|
host.declare(`declare ptr @scr_dyn_new_str(ptr)`);
|
|
host.declare(`declare void @scr_str_release(ptr)`);
|
|
const s = B.tmp();
|
|
const dv = B.tmp();
|
|
B.line(`${s} = call ptr @scr_promise_payload_str(ptr %src) ; +1`);
|
|
B.line(`${dv} = call ptr @scr_dyn_new_str(ptr ${s}) ; retains s`);
|
|
B.line(`call void @scr_str_release(ptr ${s})`);
|
|
fulfill(dv);
|
|
break;
|
|
}
|
|
default: {
|
|
// Ref-payload inners (records, arrays, bytes, %Error, unions,
|
|
// nested promises, handles): extract (+1 via the stored retain),
|
|
// convert through the shared to-dyn spelling, release the extract.
|
|
host.declare(`declare ptr @scr_promise_payload_ref(ptr)`);
|
|
const pv = B.tmp();
|
|
B.line(`${pv} = call ptr @scr_promise_payload_ref(ptr %src) ; +1`);
|
|
const dv = this.toDynExpr(B, inner, pv);
|
|
if (isRefCounted(inner)) B.line(`call void ${releaseSym(host, inner)}(ptr ${pv})`);
|
|
fulfill(dv);
|
|
break;
|
|
}
|
|
}
|
|
B.terminate(`ret void`);
|
|
this.defs.push(
|
|
`define internal void @${name}(ptr %dst, ptr %src) ${FN_ATTRS} { ; dyn-box settle adapter for promise<${key}>`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/* ── the dyn ToString pair (dynToStrHelper, ported) ────────────────── */
|
|
|
|
/** Node's String() over a dyn value — sc_ds (+1 result) over the
|
|
* recursive sc_ds_buf walker. Borrowed operand; an exception that an
|
|
* object's toString or valueOf throws stays pending. */
|
|
dynToStrHelper(): string {
|
|
if (this.dynToStrFn) return this.dynToStrFn;
|
|
const name = "sc_ds";
|
|
this.dynToStrFn = name;
|
|
this.host.journalAllocated?.(name, "dyn.dynToStrHelper", []);
|
|
const host = this.host;
|
|
host.declare(`declare void @scr_jb_init(ptr)`);
|
|
host.declare(`declare ptr @scr_jb_finish(ptr)`);
|
|
host.declare(`declare void @scr_jb_putc(ptr, i8)`);
|
|
host.declare(`declare void @scr_jb_puts(ptr, ptr)`);
|
|
host.declare(`declare void @scr_str_release(ptr)`);
|
|
|
|
// The recursive buffer walker.
|
|
{
|
|
const B = new BlockBuilder();
|
|
const kd = this.kindOf(B, "%d");
|
|
const done = B.newLabel("ds.d");
|
|
const labels = new Map<number, string>();
|
|
for (const k of [
|
|
DYN_KIND.NULL,
|
|
DYN_KIND.BOOL,
|
|
DYN_KIND.NUM,
|
|
DYN_KIND.STR,
|
|
DYN_KIND.ARR,
|
|
DYN_KIND.OBJ,
|
|
DYN_KIND.UNDEF,
|
|
DYN_KIND.BYTES,
|
|
DYN_KIND.FUNC,
|
|
DYN_KIND.HANDLE,
|
|
DYN_KIND.PROMISE,
|
|
DYN_KIND.TYPED_REF,
|
|
DYN_KIND.PROXY,
|
|
DYN_KIND.BIGINT,
|
|
DYN_KIND.SYMBOL,
|
|
]) {
|
|
labels.set(k, B.newLabel(`ds.k${k}`));
|
|
}
|
|
const branches: string[] = [];
|
|
for (const [kind, label] of labels) branches.push(`i32 ${kind}, label %${label}`);
|
|
B.terminate(`switch i32 ${kd}, label %${done} [ ${branches.join(" ")} ]`);
|
|
B.startBlock(labels.get(DYN_KIND.PROXY)!);
|
|
host.declare(`declare void @scr_dyn_proxy_unsupported(ptr)`);
|
|
B.line(`call void @scr_dyn_proxy_unsupported(ptr ${host.cstr("string conversion")})`);
|
|
B.br(done);
|
|
B.startBlock(labels.get(DYN_KIND.TYPED_REF)!);
|
|
{
|
|
host.declare(`declare ptr @scr_dyn_typed_ref_string_view(ptr)`);
|
|
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
|
const materialized = B.tmp();
|
|
B.line(`${materialized} = call ptr @scr_dyn_typed_ref_string_view(ptr %d)`);
|
|
B.line(`call void @sc_ds_buf(ptr %b, ptr ${materialized})`);
|
|
B.line(`call void @scr_dyn_release_v(ptr ${materialized})`);
|
|
B.br(done);
|
|
}
|
|
B.startBlock(labels.get(DYN_KIND.UNDEF)!);
|
|
this.puts(B, "%b", "undefined");
|
|
B.br(done);
|
|
B.startBlock(labels.get(DYN_KIND.NULL)!);
|
|
this.puts(B, "%b", "null");
|
|
B.br(done);
|
|
B.startBlock(labels.get(DYN_KIND.BOOL)!);
|
|
{
|
|
const bv = this.boolOf(B, "%d");
|
|
const s = B.tmp();
|
|
B.line(`${s} = select i1 ${bv}, ptr ${host.cstr("true")}, ptr ${host.cstr("false")}`);
|
|
B.line(`call void @scr_jb_puts(ptr %b, ptr ${s})`);
|
|
B.br(done);
|
|
}
|
|
B.startBlock(labels.get(DYN_KIND.BIGINT)!);
|
|
{
|
|
host.declare(`declare ptr @scr_bigint_to_string(ptr, double)`);
|
|
const v = this.payloadOf(B, "%d", "ptr");
|
|
const s = B.tmp();
|
|
B.line(`${s} = call ptr @scr_bigint_to_string(ptr ${v}, double ${f64Lit(10)})`);
|
|
this.putScrStr(B, "%b", s);
|
|
B.line(`call void @scr_str_release(ptr ${s})`);
|
|
B.br(done);
|
|
}
|
|
B.startBlock(labels.get(DYN_KIND.SYMBOL)!);
|
|
{
|
|
host.declare(`declare ptr @scr_dyn_string_coerce(ptr)`);
|
|
const s = B.tmp();
|
|
B.line(`${s} = call ptr @scr_dyn_string_coerce(ptr %d)`);
|
|
B.line(`call void @scr_str_release(ptr ${s})`);
|
|
B.br(done);
|
|
}
|
|
B.startBlock(labels.get(DYN_KIND.NUM)!);
|
|
{
|
|
// String(n): NaN/Infinity spelled out, not the JSON null.
|
|
const x = this.payloadOf(B, "%d", "double");
|
|
host.declare(`declare void @scr_jb_put_number(ptr, double)`);
|
|
B.line(`call void @scr_jb_put_number(ptr %b, double ${x})`);
|
|
B.br(done);
|
|
}
|
|
B.startBlock(labels.get(DYN_KIND.STR)!);
|
|
{
|
|
const s = this.payloadOf(B, "%d", "ptr");
|
|
this.putScrStr(B, "%b", s);
|
|
B.br(done);
|
|
}
|
|
B.startBlock(labels.get(DYN_KIND.ARR)!);
|
|
{
|
|
// Array.prototype.toString: join(",") — null/undefined ELEMENTS
|
|
// print empty (unlike top level), nested arrays flatten. The length
|
|
// is read once, as join reads it.
|
|
const n = this.lenOf(B, "%d");
|
|
this.i64Loop(B, "ds.ar", n, (i, brNext) => {
|
|
const nz = B.tmp();
|
|
B.line(`${nz} = icmp ugt ${this.S} ${i}, 0`);
|
|
const lcm = B.newLabel("ds.cm");
|
|
const lel = B.newLabel("ds.el");
|
|
B.condBr(nz, lcm, lel);
|
|
B.startBlock(lcm);
|
|
B.line(`call void @scr_jb_putc(ptr %b, i8 44)`);
|
|
B.br(lel);
|
|
B.startBlock(lel);
|
|
// Each index reads the element as join does (scr_dyn_arr_at, +1):
|
|
// from the current storage, undefined past the current length,
|
|
// and through the prototype for a hole. An element's toString
|
|
// can append to, shrink or reallocate the array; a getter that
|
|
// throws answers NULL with the exception pending.
|
|
host.declare(`declare ptr @scr_dyn_arr_at(ptr, double)`);
|
|
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
|
const index = B.tmp();
|
|
B.line(`${index} = uitofp ${this.S} ${i} to double`);
|
|
const e = B.tmp();
|
|
B.line(`${e} = call ptr @scr_dyn_arr_at(ptr %d, double ${index})`);
|
|
const missing = B.tmp();
|
|
B.line(`${missing} = icmp eq ptr ${e}, null`);
|
|
const lGot = B.newLabel("ds.gt");
|
|
B.condBr(missing, done, lGot);
|
|
B.startBlock(lGot);
|
|
const ek = this.kindOf(B, e);
|
|
const isU = B.tmp();
|
|
const isN = B.tmp();
|
|
const unit = B.tmp();
|
|
B.line(`${isU} = icmp eq i32 ${ek}, ${DYN_KIND.UNDEF}`);
|
|
B.line(`${isN} = icmp eq i32 ${ek}, ${DYN_KIND.NULL}`);
|
|
B.line(`${unit} = or i1 ${isU}, ${isN}`);
|
|
const lRec = B.newLabel("ds.rc");
|
|
const lSkip = B.newLabel("ds.sk");
|
|
B.condBr(unit, lSkip, lRec);
|
|
B.startBlock(lSkip);
|
|
B.line(`call void @scr_dyn_release_v(ptr ${e})`);
|
|
brNext();
|
|
B.startBlock(lRec);
|
|
B.line(`call void @sc_ds_buf(ptr %b, ptr ${e})`);
|
|
B.line(`call void @scr_dyn_release_v(ptr ${e})`);
|
|
// The first exception an element's conversion throws ends the
|
|
// conversion; later elements are not read.
|
|
const thrown = B.tmp();
|
|
for (const line of host.pendingTestLines(thrown)) B.line(line.trimStart());
|
|
const lMore = B.newLabel("ds.mo");
|
|
B.condBr(thrown, done, lMore);
|
|
B.startBlock(lMore);
|
|
});
|
|
B.br(done);
|
|
}
|
|
B.startBlock(labels.get(DYN_KIND.OBJ)!);
|
|
{
|
|
// The checked-dynamic tree's error encoding renders Error.prototype.toString;
|
|
// other objects convert with ToPrimitive's string hint: a toString or
|
|
// valueOf of their own or of their prototypes, else "[object Object]".
|
|
const marker = this.objGetLit(B, "%d", "%error");
|
|
const isErr = B.tmp();
|
|
B.line(`${isErr} = icmp ne ptr ${marker}, null`);
|
|
const lErr = B.newLabel("ds.er");
|
|
const lPlain = B.newLabel("ds.pl");
|
|
B.condBr(isErr, lErr, lPlain);
|
|
B.startBlock(lPlain);
|
|
{
|
|
// Null leaves the exception the conversion threw pending.
|
|
host.declare(`declare ptr @scr_dyn_string_coerce_js(ptr)`);
|
|
host.declare(`declare void @scr_str_release(ptr)`);
|
|
const text = B.tmp();
|
|
B.line(`${text} = call ptr @scr_dyn_string_coerce_js(ptr %d)`);
|
|
const converted = B.tmp();
|
|
B.line(`${converted} = icmp ne ptr ${text}, null`);
|
|
const lPut = B.newLabel("ds.pp");
|
|
B.condBr(converted, lPut, done);
|
|
B.startBlock(lPut);
|
|
this.putScrStr(B, "%b", text);
|
|
B.line(`call void @scr_str_release(ptr ${text})`);
|
|
B.br(done);
|
|
}
|
|
B.startBlock(lErr);
|
|
// Error.prototype.toString, reading name and message through the
|
|
// prototype. Null leaves the exception its conversion threw pending.
|
|
host.declare(`declare ptr @scr_dyn_error_string(ptr)`);
|
|
host.declare(`declare void @scr_str_release(ptr)`);
|
|
const text = B.tmp();
|
|
B.line(`${text} = call ptr @scr_dyn_error_string(ptr %d)`);
|
|
const written = B.tmp();
|
|
B.line(`${written} = icmp ne ptr ${text}, null`);
|
|
const lPut = B.newLabel("ds.ep");
|
|
B.condBr(written, lPut, done);
|
|
B.startBlock(lPut);
|
|
this.putScrStr(B, "%b", text);
|
|
B.line(`call void @scr_str_release(ptr ${text})`);
|
|
B.br(done);
|
|
}
|
|
B.startBlock(labels.get(DYN_KIND.BYTES)!);
|
|
{
|
|
// Buffer-flavored values coerce utf8; typed arrays join their
|
|
// elements and a DataView answers "[object DataView]" (the
|
|
// runtime's conversion).
|
|
const bufp = B.tmp();
|
|
const bufRaw = B.tmp();
|
|
const isBuf = B.tmp();
|
|
B.line(
|
|
`${bufp} = getelementptr inbounds i8, ptr %d, i64 ${this.abiOffset(12, 8)} ; ->buffer`,
|
|
);
|
|
B.line(`${bufRaw} = load i8, ptr ${bufp}`);
|
|
B.line(`${isBuf} = icmp ne i8 ${bufRaw}, 0`);
|
|
const lBuf = B.newLabel("ds.bu");
|
|
const lJoin = B.newLabel("ds.bj");
|
|
B.condBr(isBuf, lBuf, lJoin);
|
|
B.startBlock(lBuf);
|
|
host.declare(`declare ptr @scr_bytes_to_str(ptr, ptr)`);
|
|
const bytes = this.payloadOf(B, "%d", "ptr");
|
|
const enc = host.internLiteral("utf8");
|
|
const txt = B.tmp();
|
|
B.line(`${txt} = call ptr @scr_bytes_to_str(ptr ${bytes}, ptr ${enc})`);
|
|
this.putScrStr(B, "%b", txt);
|
|
B.line(`call void @scr_str_release(ptr ${txt})`);
|
|
B.br(done);
|
|
B.startBlock(lJoin);
|
|
host.declare(`declare ptr @scr_dyn_to_string(ptr, ptr)`);
|
|
const joined = B.tmp();
|
|
B.line(`${joined} = call ptr @scr_dyn_to_string(ptr %d, ptr null)`);
|
|
this.putScrStr(B, "%b", joined);
|
|
B.line(`call void @scr_str_release(ptr ${joined})`);
|
|
B.br(done);
|
|
}
|
|
B.startBlock(labels.get(DYN_KIND.FUNC)!);
|
|
{
|
|
// Function.prototype.toString: the native-code form.
|
|
this.puts(B, "%b", "function ");
|
|
const namep = B.tmp();
|
|
const nm = B.tmp();
|
|
B.line(
|
|
`${namep} = getelementptr inbounds i8, ptr %d, i64 ${this.abiOffset(40, 28)} ; ->v.fn.name`,
|
|
);
|
|
B.line(`${nm} = load ptr, ptr ${namep}`);
|
|
const nn = B.tmp();
|
|
B.line(`${nn} = icmp ne ptr ${nm}, null`);
|
|
const lt = B.newLabel("ds.fn");
|
|
const lj = B.newLabel("ds.fj");
|
|
B.condBr(nn, lt, lj);
|
|
B.startBlock(lt);
|
|
B.line(`call void @scr_jb_puts(ptr %b, ptr ${nm})`);
|
|
B.br(lj);
|
|
B.startBlock(lj);
|
|
this.puts(B, "%b", "() { [native code] }");
|
|
B.br(done);
|
|
}
|
|
B.startBlock(labels.get(DYN_KIND.HANDLE)!);
|
|
{
|
|
host.declare(`declare ptr @scr_dyn_to_string(ptr, ptr)`);
|
|
const s = B.tmp();
|
|
B.line(`${s} = call ptr @scr_dyn_to_string(ptr %d, ptr null)`);
|
|
this.putScrStr(B, "%b", s);
|
|
B.line(`call void @scr_str_release(ptr ${s})`);
|
|
B.br(done);
|
|
}
|
|
B.startBlock(labels.get(DYN_KIND.PROMISE)!);
|
|
// Object.prototype.toString with the Promise @@toStringTag.
|
|
this.puts(B, "%b", "[object Promise]");
|
|
B.br(done);
|
|
B.startBlock(done);
|
|
B.terminate(`ret void`);
|
|
this.defs.push(
|
|
`define internal void @${name}_buf(ptr %b, ptr %d) ${FN_ATTRS} { ; String(unknown), recursive`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
}
|
|
|
|
// The +1 wrapper with the string fast path.
|
|
{
|
|
const B = new BlockBuilder();
|
|
const kd = this.kindOf(B, "%d");
|
|
const isStr = B.tmp();
|
|
B.line(`${isStr} = icmp eq i32 ${kd}, ${DYN_KIND.STR}`);
|
|
const lFast = B.newLabel("ds.f");
|
|
const lSlow = B.newLabel("ds.s");
|
|
B.condBr(isStr, lFast, lSlow);
|
|
B.startBlock(lFast);
|
|
host.declare(`declare ptr @scr_str_retain_v(ptr)`);
|
|
const s = this.payloadOf(B, "%d", "ptr");
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_str_retain_v(ptr ${s})`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
B.startBlock(lSlow);
|
|
const buf = "%dsb";
|
|
B.entryAllocas.push(`${buf} = alloca %ScrJsonBuf`);
|
|
B.line(`call void @scr_jb_init(ptr ${buf})`);
|
|
B.line(`call void @${name}_buf(ptr ${buf}, ptr %d)`);
|
|
const out = B.tmp();
|
|
B.line(`${out} = call ptr @scr_jb_finish(ptr ${buf})`);
|
|
B.terminate(`ret ptr ${out}`);
|
|
this.defs.push(
|
|
`define internal ptr @${name}(ptr %d) ${FN_ATTRS} { ; String(unknown) -> owned (+1)`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
}
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/* ── caughtToDyn (walkers.ts, ported) ─────────────────────────── */
|
|
|
|
/** A catch binding flowing into an `unknown` slot: the typed→unknown
|
|
* deep-copy stance over the exception snapshot's runtime kind. */
|
|
caughtToDynHelper(): string {
|
|
if (this.caughtToDynFn) return this.caughtToDynFn;
|
|
const name = "sc_cd";
|
|
this.caughtToDynFn = name;
|
|
this.host.journalAllocated?.(name, "dyn.caughtToDynHelper", []);
|
|
const host = this.host;
|
|
const B = new BlockBuilder();
|
|
const kp = B.tmp();
|
|
const kd = B.tmp();
|
|
B.line(`${kp} = getelementptr inbounds %ScrCaught, ptr %c, i64 0, i32 1`);
|
|
B.line(`${kd} = load i32, ptr ${kp}`);
|
|
const lF64 = B.newLabel("cd.f");
|
|
const lBool = B.newLabel("cd.b");
|
|
const lStr = B.newLabel("cd.s");
|
|
const lObj = B.newLabel("cd.o");
|
|
const lDef = B.newLabel("cd.d");
|
|
// SCR_EXC_F64=1, BOOL=2, STR=3, REF=4 (default), OBJ=5.
|
|
B.terminate(
|
|
`switch i32 ${kd}, label %${lDef} [ i32 1, label %${lF64} i32 2, label %${lBool} i32 3, label %${lStr} i32 5, label %${lObj} ]`,
|
|
);
|
|
B.startBlock(lF64);
|
|
host.declare(`declare ptr @scr_dyn_new_num(double)`);
|
|
{
|
|
const xp = B.tmp();
|
|
const x = B.tmp();
|
|
const r = B.tmp();
|
|
B.line(`${xp} = getelementptr inbounds %ScrCaught, ptr %c, i64 0, i32 2`);
|
|
B.line(`${x} = load double, ptr ${xp}`);
|
|
B.line(`${r} = call ptr @scr_dyn_new_num(double ${x})`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
}
|
|
B.startBlock(lBool);
|
|
host.declare(`declare ptr @scr_dyn_new_bool(i1 zeroext)`);
|
|
{
|
|
const xp = B.tmp();
|
|
const raw = B.tmp();
|
|
const x = B.tmp();
|
|
const r = B.tmp();
|
|
B.line(`${xp} = getelementptr inbounds %ScrCaught, ptr %c, i64 0, i32 3`);
|
|
B.line(`${raw} = load i8, ptr ${xp}`);
|
|
B.line(`${x} = trunc i8 ${raw} to i1`);
|
|
B.line(`${r} = call ptr @scr_dyn_new_bool(i1 ${x})`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
}
|
|
B.startBlock(lStr);
|
|
host.declare(`declare ptr @scr_dyn_new_str(ptr)`);
|
|
{
|
|
const pp = B.tmp();
|
|
const p = B.tmp();
|
|
const r = B.tmp();
|
|
B.line(`${pp} = getelementptr inbounds %ScrCaught, ptr %c, i64 0, i32 4`);
|
|
B.line(`${p} = load ptr, ptr ${pp}`);
|
|
B.line(`${r} = call ptr @scr_dyn_new_str(ptr ${p}) ; _new_str retains`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
}
|
|
B.startBlock(lObj);
|
|
host.declare(`declare zeroext i1 @scr_error_is(ptr)`);
|
|
{
|
|
const pp = B.tmp();
|
|
const p = B.tmp();
|
|
B.line(`${pp} = getelementptr inbounds %ScrCaught, ptr %c, i64 0, i32 4`);
|
|
B.line(`${p} = load ptr, ptr ${pp}`);
|
|
const isErr = B.tmp();
|
|
B.line(`${isErr} = call zeroext i1 @scr_error_is(ptr ${p})`);
|
|
const lErr = B.newLabel("cd.e");
|
|
B.condBr(isErr, lErr, lDef);
|
|
B.startBlock(lErr);
|
|
// The identity-cached crossing (scr_json.c): one error instance,
|
|
// one dyn node — the C walker's arm exactly.
|
|
host.declare(`declare ptr @scr_dyn_from_error(ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_from_error(ptr ${p})`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
}
|
|
B.startBlock(lDef);
|
|
// SCR_EXC_REF: a thrown dyn value passes back BY REFERENCE (identity
|
|
// with every other holder — the dyn adapters discriminate); non-dyn
|
|
// REF and non-Error objects keep the "[object Object]" approximation
|
|
// — truthy, typeof "object", fields unreadable.
|
|
host.declare(`declare ptr @scr_dyn_new_obj()`);
|
|
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
|
|
{
|
|
const rfp = B.tmp();
|
|
const rf = B.tmp();
|
|
B.line(`${rfp} = getelementptr inbounds %ScrCaught, ptr %c, i64 0, i32 5`);
|
|
B.line(`${rf} = load ptr, ptr ${rfp}`);
|
|
const isDyn = B.tmp();
|
|
B.line(`${isDyn} = icmp eq ptr ${rf}, @scr_dyn_retain_v`);
|
|
const lRef = B.newLabel("cd.r");
|
|
const lPlain = B.newLabel("cd.p");
|
|
B.condBr(isDyn, lRef, lPlain);
|
|
B.startBlock(lRef);
|
|
const pp = B.tmp();
|
|
const p = B.tmp();
|
|
const r = B.tmp();
|
|
B.line(`${pp} = getelementptr inbounds %ScrCaught, ptr %c, i64 0, i32 4`);
|
|
B.line(`${p} = load ptr, ptr ${pp}`);
|
|
B.line(`${r} = call ptr @scr_dyn_retain_v(ptr ${p})`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
B.startBlock(lPlain);
|
|
const e = B.tmp();
|
|
B.line(`${e} = call ptr @scr_dyn_new_obj()`);
|
|
B.terminate(`ret ptr ${e}`);
|
|
}
|
|
this.defs.push(
|
|
`define internal ptr @${name}(ptr %c) ${FN_ATTRS} { ; caught -> unknown (+1, fresh tree)`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/* ── the keyed read (dynKeyGetHelper, ported) ──────────────────────── */
|
|
|
|
/** `sc_dyn_key_get(ptr d, ptr k, i1 opt) -> ptr` — d[k] on a dyn value.
|
|
* Result +1; throws on non-optional nullish receivers. */
|
|
dynComputedKeyGetHelper(): string {
|
|
const name = "sc_dyn_computed_key_get";
|
|
if (this.helperMemo.has(name)) return name;
|
|
this.helperMemo.set(name, name);
|
|
const B = new BlockBuilder();
|
|
const host = this.host;
|
|
this.host.journalAllocated?.(name, "dyn.dynComputedKeyGetHelper", []);
|
|
const stringHelper = this.dynKeyGetHelper();
|
|
host.declare(`declare ptr @scr_dyn_property_key_value(ptr)`);
|
|
host.declare(`declare ptr @scr_dyn_symbol_key_get(ptr, ptr, i1 zeroext)`);
|
|
host.declare(`declare void @scr_dyn_release(ptr)`);
|
|
const key = B.tmp();
|
|
B.line(`${key} = call ptr @scr_dyn_property_key_value(ptr %key)`);
|
|
const missing = B.tmp();
|
|
B.line(`${missing} = icmp eq ptr ${key}, null`);
|
|
const fail = B.newLabel("kg.fail");
|
|
const dispatch = B.newLabel("kg.dispatch");
|
|
B.condBr(missing, fail, dispatch);
|
|
B.startBlock(fail);
|
|
B.terminate("ret ptr null");
|
|
B.startBlock(dispatch);
|
|
const kind = this.kindOf(B, key);
|
|
const symbol = B.tmp();
|
|
B.line(`${symbol} = icmp eq i32 ${kind}, ${DYN_KIND.SYMBOL}`);
|
|
const sym = B.newLabel("kg.symbol");
|
|
const str = B.newLabel("kg.string");
|
|
B.condBr(symbol, sym, str);
|
|
B.startBlock(sym);
|
|
const sv = B.tmp();
|
|
B.line(`${sv} = call ptr @scr_dyn_symbol_key_get(ptr %d, ptr ${key}, i1 %opt)`);
|
|
B.line(`call void @scr_dyn_release(ptr ${key})`);
|
|
B.terminate(`ret ptr ${sv}`);
|
|
B.startBlock(str);
|
|
const text = this.payloadOf(B, key, "ptr");
|
|
const value = B.tmp();
|
|
B.line(`${value} = call ptr @${stringHelper}(ptr %d, ptr ${text}, i1 %opt)`);
|
|
B.line(`call void @scr_dyn_release(ptr ${key})`);
|
|
B.terminate(`ret ptr ${value}`);
|
|
this.defs.push(
|
|
`define internal ptr @${name}(ptr %d, ptr %key, i1 zeroext %opt) ${FN_ATTRS} {`,
|
|
B.render(),
|
|
"}",
|
|
"",
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
dynKeyGetHelper(): string {
|
|
const memoKey = "%dynKeyGet";
|
|
const existing = this.dynBuilders.get(memoKey);
|
|
if (existing) return existing;
|
|
const name = "sc_dyn_key_get";
|
|
this.dynBuilders.set(memoKey, name);
|
|
this.host.journalAllocated?.(name, "dyn.dynKeyGetHelper", []);
|
|
const host = this.host;
|
|
const B = new BlockBuilder();
|
|
const kd = this.kindOf(B, "%d");
|
|
const kParts = this.strParts(B, "%k");
|
|
const retainUndef = (): void => {
|
|
const u = this.undef(B);
|
|
const r = this.retainDyn(B, u);
|
|
B.terminate(`ret ptr ${r}`);
|
|
};
|
|
// undefined/null receivers: opt answers undefined; otherwise throw
|
|
// Node's TypeError with the key spliced in.
|
|
{
|
|
const isU = B.tmp();
|
|
const isN = B.tmp();
|
|
const unit = B.tmp();
|
|
B.line(`${isU} = icmp eq i32 ${kd}, ${DYN_KIND.UNDEF}`);
|
|
B.line(`${isN} = icmp eq i32 ${kd}, ${DYN_KIND.NULL}`);
|
|
B.line(`${unit} = or i1 ${isU}, ${isN}`);
|
|
const lUnit = B.newLabel("kg.u");
|
|
const lNext = B.newLabel("kg.n");
|
|
B.condBr(unit, lUnit, lNext);
|
|
B.startBlock(lUnit);
|
|
const lOpt = B.newLabel("kg.o");
|
|
const lThrow = B.newLabel("kg.t");
|
|
B.condBr("%opt", lOpt, lThrow);
|
|
B.startBlock(lOpt);
|
|
retainUndef();
|
|
B.startBlock(lThrow);
|
|
host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`);
|
|
host.declare(`declare ptr @scr_str_concat(ptr, ptr)`);
|
|
host.declare(`declare void @scr_str_release(ptr)`);
|
|
host.declare(`declare void @scr_throw_error(i32, ptr)`);
|
|
const baseU = "Cannot read properties of undefined (reading '";
|
|
const baseN = "Cannot read properties of null (reading '";
|
|
const base = B.tmp();
|
|
B.line(`${base} = select i1 ${isU}, ptr ${host.cstr(baseU)}, ptr ${host.cstr(baseN)}`);
|
|
const baseLen = B.tmp();
|
|
B.line(
|
|
`${baseLen} = select i1 ${isU}, ${host.sizeType} ${Buffer.byteLength(baseU)}, ${host.sizeType} ${Buffer.byteLength(baseN)}`,
|
|
);
|
|
const head = B.tmp();
|
|
B.line(`${head} = call ptr @scr_str_new(ptr ${base}, ${host.sizeType} ${baseLen})`);
|
|
const withKey = B.tmp();
|
|
B.line(`${withKey} = call ptr @scr_str_concat(ptr ${head}, ptr %k)`);
|
|
B.line(`call void @scr_str_release(ptr ${head})`);
|
|
const tail = B.tmp();
|
|
B.line(`${tail} = call ptr @scr_str_new(ptr ${host.cstr("')")}, ${host.sizeType} 2)`);
|
|
const msg = B.tmp();
|
|
B.line(`${msg} = call ptr @scr_str_concat(ptr ${withKey}, ptr ${tail})`);
|
|
B.line(`call void @scr_str_release(ptr ${withKey})`);
|
|
B.line(`call void @scr_str_release(ptr ${tail})`);
|
|
B.line(`call void @scr_throw_error(i32 1, ptr ${msg}) ; SCR_ERR_TYPE; takes ownership`);
|
|
B.terminate(`ret ptr null`);
|
|
B.startBlock(lNext);
|
|
}
|
|
{
|
|
const isSymbol = B.tmp();
|
|
B.line(`${isSymbol} = icmp eq i32 ${kd}, ${DYN_KIND.SYMBOL}`);
|
|
const lSymbol = B.newLabel("kg.sym");
|
|
const lNext = B.newLabel("kg.n");
|
|
B.condBr(isSymbol, lSymbol, lNext);
|
|
B.startBlock(lSymbol);
|
|
host.declare(`declare ptr @scr_dyn_symbol_get(ptr, ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_symbol_get(ptr %d, ptr %k)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
B.startBlock(lNext);
|
|
}
|
|
// Typed stream capsules expose their one cached, refreshed dyn view to
|
|
// ordinary property reads while the capsule itself keeps identity.
|
|
{
|
|
const isTyped = B.tmp();
|
|
B.line(`${isTyped} = icmp eq i32 ${kd}, ${DYN_KIND.TYPED_REF}`);
|
|
const lTyped = B.newLabel("kg.tr");
|
|
const lNext = B.newLabel("kg.n");
|
|
B.condBr(isTyped, lTyped, lNext);
|
|
B.startBlock(lTyped);
|
|
// Live arrays answer `length` and own elements from the native array;
|
|
// rebuilding the snapshot per read would make index loops quadratic.
|
|
host.declare(`declare ptr @scr_dyn_typed_ref_key_get(ptr, ptr)`);
|
|
const fast = B.tmp();
|
|
const hit = B.tmp();
|
|
B.line(`${fast} = call ptr @scr_dyn_typed_ref_key_get(ptr %d, ptr %k)`);
|
|
B.line(`${hit} = icmp ne ptr ${fast}, null`);
|
|
const lFast = B.newLabel("kg.tr.fast");
|
|
const lView = B.newLabel("kg.tr.view");
|
|
B.condBr(hit, lFast, lView);
|
|
B.startBlock(lFast);
|
|
B.terminate(`ret ptr ${fast}`);
|
|
B.startBlock(lView);
|
|
host.declare(`declare ptr @scr_dyn_typed_ref_materialize(ptr)`);
|
|
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
|
const materialized = B.tmp();
|
|
const r = B.tmp();
|
|
B.line(`${materialized} = call ptr @scr_dyn_typed_ref_materialize(ptr %d)`);
|
|
B.line(`${r} = call ptr @${name}(ptr ${materialized}, ptr %k, i1 %opt)`);
|
|
B.line(`call void @scr_dyn_release_v(ptr ${materialized})`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
B.startBlock(lNext);
|
|
}
|
|
|
|
{
|
|
const isProxy = B.tmp();
|
|
B.line(`${isProxy} = icmp eq i32 ${kd}, ${DYN_KIND.PROXY}`);
|
|
const lProxy = B.newLabel("kg.proxy");
|
|
const lNext = B.newLabel("kg.n");
|
|
B.condBr(isProxy, lProxy, lNext);
|
|
B.startBlock(lProxy);
|
|
host.declare(`declare ptr @scr_dyn_proxy_get(ptr, ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_proxy_get(ptr %d, ptr %k)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
B.startBlock(lNext);
|
|
}
|
|
|
|
// OBJ: the own member or the inherited builtin Error constructor.
|
|
{
|
|
const isObj = B.tmp();
|
|
B.line(`${isObj} = icmp eq i32 ${kd}, ${DYN_KIND.OBJ}`);
|
|
const lObj = B.newLabel("kg.ob");
|
|
const lNext = B.newLabel("kg.n");
|
|
B.condBr(isObj, lObj, lNext);
|
|
B.startBlock(lObj);
|
|
host.declare(`declare ptr @scr_dyn_obj_read(ptr, ptr, ${host.sizeType})`);
|
|
const r = B.tmp();
|
|
B.line(
|
|
`${r} = call ptr @scr_dyn_obj_read(ptr %d, ptr ${kParts.data}, ${host.sizeType} ${kParts.len})`,
|
|
);
|
|
B.terminate(`ret ptr ${r}`);
|
|
B.startBlock(lNext);
|
|
}
|
|
// HANDLE: the tag's modeled properties through the installed ops.
|
|
{
|
|
const isH = B.tmp();
|
|
B.line(`${isH} = icmp eq i32 ${kd}, ${DYN_KIND.HANDLE}`);
|
|
const lH = B.newLabel("kg.h");
|
|
const lNext = B.newLabel("kg.n");
|
|
B.condBr(isH, lH, lNext);
|
|
B.startBlock(lH);
|
|
host.declare(`declare ptr @scr_dyn_handle_key_get(ptr, ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_handle_key_get(ptr %d, ptr %k)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
B.startBlock(lNext);
|
|
}
|
|
// PROMISE: Promise.prototype.then/catch/finally as function values;
|
|
// every other key is undefined (promises have no own properties).
|
|
{
|
|
const isP = B.tmp();
|
|
B.line(`${isP} = icmp eq i32 ${kd}, ${DYN_KIND.PROMISE}`);
|
|
const lP = B.newLabel("kg.p");
|
|
const lNext = B.newLabel("kg.n");
|
|
B.condBr(isP, lP, lNext);
|
|
B.startBlock(lP);
|
|
host.declare(`declare ptr @scr_dyn_promise_key_get(ptr, ptr)`);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @scr_dyn_promise_key_get(ptr %d, ptr %k)`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
B.startBlock(lNext);
|
|
}
|
|
// The canonical-index parse, shared by BYTES/ARR/STR: digits only,
|
|
// no leading zero. Leaves (digits, idx).
|
|
const parseIndex = (): { digits: string; idx: string } => {
|
|
const digitsSlot = B.slot();
|
|
const idxSlot = B.slot();
|
|
B.entryAllocas.push(`${digitsSlot} = alloca i1`, `${idxSlot} = alloca ${host.sizeType}`);
|
|
B.line(`store i1 false, ptr ${digitsSlot}`);
|
|
B.line(`store ${host.sizeType} 0, ptr ${idxSlot}`);
|
|
// k->len > 0 && !(k->len > 1 && k->data[0] == '0')
|
|
const nz = B.tmp();
|
|
B.line(`${nz} = icmp ugt ${host.sizeType} ${kParts.len}, 0`);
|
|
const lGo = B.newLabel("kg.ix");
|
|
const lOut = B.newLabel("kg.io");
|
|
B.condBr(nz, lGo, lOut);
|
|
B.startBlock(lGo);
|
|
const multi = B.tmp();
|
|
B.line(`${multi} = icmp ugt ${host.sizeType} ${kParts.len}, 1`);
|
|
const c0 = B.tmp();
|
|
B.line(`${c0} = load i8, ptr ${kParts.data}`);
|
|
const isZero = B.tmp();
|
|
B.line(`${isZero} = icmp eq i8 ${c0}, 48`);
|
|
const leading = B.tmp();
|
|
B.line(`${leading} = and i1 ${multi}, ${isZero}`);
|
|
const lParse = B.newLabel("kg.ip");
|
|
B.condBr(leading, lOut, lParse);
|
|
B.startBlock(lParse);
|
|
B.line(`store i1 true, ptr ${digitsSlot}`);
|
|
this.i64Loop(B, "kg.id", kParts.len, (i) => {
|
|
const cp = B.tmp();
|
|
const c = B.tmp();
|
|
B.line(`${cp} = getelementptr inbounds i8, ptr ${kParts.data}, ${host.sizeType} ${i}`);
|
|
B.line(`${c} = load i8, ptr ${cp}`);
|
|
const lt0 = B.tmp();
|
|
const gt9 = B.tmp();
|
|
B.line(`${lt0} = icmp ult i8 ${c}, 48`);
|
|
B.line(`${gt9} = icmp ugt i8 ${c}, 57`);
|
|
const cur = B.tmp();
|
|
B.line(`${cur} = load ${host.sizeType}, ptr ${idxSlot}`);
|
|
const over = B.tmp();
|
|
B.line(`${over} = icmp ugt ${host.sizeType} ${cur}, ${this.indexMaxDiv10}`);
|
|
const bad0 = B.tmp();
|
|
const bad = B.tmp();
|
|
B.line(`${bad0} = or i1 ${lt0}, ${gt9}`);
|
|
B.line(`${bad} = or i1 ${bad0}, ${over}`);
|
|
const lBad = B.newLabel("kg.ib");
|
|
const lStep = B.newLabel("kg.is");
|
|
B.condBr(bad, lBad, lStep);
|
|
B.startBlock(lBad);
|
|
B.line(`store i1 false, ptr ${digitsSlot}`);
|
|
B.br(lOut);
|
|
B.startBlock(lStep);
|
|
const ten = B.tmp();
|
|
const digit = B.tmp();
|
|
const digitSize = B.tmp();
|
|
const nx = B.tmp();
|
|
B.line(`${ten} = mul ${host.sizeType} ${cur}, 10`);
|
|
B.line(`${digit} = sub i8 ${c}, 48`);
|
|
B.line(`${digitSize} = zext i8 ${digit} to ${host.sizeType}`);
|
|
B.line(`${nx} = add ${host.sizeType} ${ten}, ${digitSize}`);
|
|
B.line(`store ${host.sizeType} ${nx}, ptr ${idxSlot}`);
|
|
});
|
|
B.br(lOut);
|
|
B.startBlock(lOut);
|
|
const digits = B.tmp();
|
|
const idx = B.tmp();
|
|
B.line(`${digits} = load i1, ptr ${digitsSlot}`);
|
|
B.line(`${idx} = load ${host.sizeType}, ptr ${idxSlot}`);
|
|
return { digits, idx };
|
|
};
|
|
const isLength = (): string => {
|
|
host.declare(`declare i32 @memcmp(ptr, ptr, ${host.sizeType})`);
|
|
const slot = B.slot();
|
|
B.entryAllocas.push(`${slot} = alloca i1`);
|
|
B.line(`store i1 false, ptr ${slot}`);
|
|
const len6 = B.tmp();
|
|
B.line(`${len6} = icmp eq ${host.sizeType} ${kParts.len}, 6`);
|
|
const lCmp = B.newLabel("kg.l6");
|
|
const lj = B.newLabel("kg.lj");
|
|
B.condBr(len6, lCmp, lj);
|
|
B.startBlock(lCmp);
|
|
const c = B.tmp();
|
|
const same = B.tmp();
|
|
B.line(
|
|
`${c} = call i32 @memcmp(ptr ${kParts.data}, ptr ${host.cstr("length")}, ${host.sizeType} 6)`,
|
|
);
|
|
B.line(`${same} = icmp eq i32 ${c}, 0`);
|
|
B.line(`store i1 ${same}, ptr ${slot}`);
|
|
B.br(lj);
|
|
B.startBlock(lj);
|
|
const out = B.tmp();
|
|
B.line(`${out} = load i1, ptr ${slot}`);
|
|
return out;
|
|
};
|
|
// Parse once up front (pure), branch per kind below — C parses per
|
|
// branch, but the computation is effect-free and identical.
|
|
const lenHit = isLength();
|
|
const { digits, idx } = parseIndex();
|
|
// BYTES: .length and canonical-index byte reads answer like Node.
|
|
{
|
|
const isB = B.tmp();
|
|
B.line(`${isB} = icmp eq i32 ${kd}, ${DYN_KIND.BYTES}`);
|
|
const lB = B.newLabel("kg.by");
|
|
const lNext = B.newLabel("kg.n");
|
|
B.condBr(isB, lB, lNext);
|
|
B.startBlock(lB);
|
|
host.declare(`declare ptr @scr_dyn_bytes_key_get(ptr, ptr)`);
|
|
const result = B.tmp();
|
|
B.line(`${result} = call ptr @scr_dyn_bytes_key_get(ptr %d, ptr %k)`);
|
|
B.terminate(`ret ptr ${result}`);
|
|
B.startBlock(lNext);
|
|
}
|
|
// FUNC: own props (defineProperties writes), then name/length.
|
|
{
|
|
const isF = B.tmp();
|
|
B.line(`${isF} = icmp eq i32 ${kd}, ${DYN_KIND.FUNC}`);
|
|
const lF = B.newLabel("kg.fn");
|
|
const lNext = B.newLabel("kg.n");
|
|
B.condBr(isF, lF, lNext);
|
|
B.startBlock(lF);
|
|
host.declare(`declare ptr @scr_dyn_fn_get(ptr, ptr, ${host.sizeType})`);
|
|
const m = B.tmp();
|
|
B.line(
|
|
`${m} = call ptr @scr_dyn_fn_get(ptr %d, ptr ${kParts.data}, ${host.sizeType} ${kParts.len})`,
|
|
);
|
|
const has = B.tmp();
|
|
B.line(`${has} = icmp ne ptr ${m}, null`);
|
|
const lHit = B.newLabel("kg.fh");
|
|
const lMiss = B.newLabel("kg.fm");
|
|
B.condBr(has, lHit, lMiss);
|
|
B.startBlock(lHit);
|
|
B.terminate(`ret ptr ${m}`);
|
|
B.startBlock(lMiss);
|
|
retainUndef();
|
|
B.startBlock(lNext);
|
|
}
|
|
// ARR / STR: length + canonical-index element/char reads.
|
|
{
|
|
const isA = B.tmp();
|
|
const isS = B.tmp();
|
|
const either = B.tmp();
|
|
B.line(`${isA} = icmp eq i32 ${kd}, ${DYN_KIND.ARR}`);
|
|
B.line(`${isS} = icmp eq i32 ${kd}, ${DYN_KIND.STR}`);
|
|
B.line(`${either} = or i1 ${isA}, ${isS}`);
|
|
const lAS = B.newLabel("kg.as");
|
|
const lNext = B.newLabel("kg.n");
|
|
B.condBr(either, lAS, lNext);
|
|
B.startBlock(lAS);
|
|
host.declare(`declare double @scr_str_utf16_len(ptr)`);
|
|
host.declare(`declare ptr @scr_dyn_new_num(double)`);
|
|
const lLen = B.newLabel("kg.al");
|
|
const lIdx = B.newLabel("kg.ai");
|
|
B.condBr(lenHit, lLen, lIdx);
|
|
B.startBlock(lLen);
|
|
{
|
|
const lArr = B.newLabel("kg.aa");
|
|
const lStr = B.newLabel("kg.asr");
|
|
B.condBr(isA, lArr, lStr);
|
|
B.startBlock(lArr);
|
|
const n = this.lenOf(B, "%d");
|
|
const nd = B.tmp();
|
|
const r = B.tmp();
|
|
B.line(`${nd} = ${sizeToDouble(host.sizeType)} ${host.sizeType} ${n} to double`);
|
|
B.line(`${r} = call ptr @scr_dyn_new_num(double ${nd})`);
|
|
B.terminate(`ret ptr ${r}`);
|
|
B.startBlock(lStr);
|
|
const s = this.payloadOf(B, "%d", "ptr");
|
|
const n2 = B.tmp();
|
|
const r2 = B.tmp();
|
|
B.line(`${n2} = call double @scr_str_utf16_len(ptr ${s})`);
|
|
B.line(`${r2} = call ptr @scr_dyn_new_num(double ${n2})`);
|
|
B.terminate(`ret ptr ${r2}`);
|
|
}
|
|
B.startBlock(lIdx);
|
|
{
|
|
const lTry = B.newLabel("kg.at");
|
|
const lMiss = B.newLabel("kg.am");
|
|
B.condBr(digits, lTry, lMiss);
|
|
B.startBlock(lTry);
|
|
const lArr = B.newLabel("kg.ae");
|
|
const lStr = B.newLabel("kg.ac");
|
|
B.condBr(isA, lArr, lStr);
|
|
B.startBlock(lArr);
|
|
const n = this.lenOf(B, "%d");
|
|
const inR = B.tmp();
|
|
B.line(`${inR} = icmp ult ${host.sizeType} ${idx}, ${n}`);
|
|
const lHit = B.newLabel("kg.ah");
|
|
B.condBr(inR, lHit, lMiss);
|
|
B.startBlock(lHit);
|
|
const items = this.itemsOf(B, "%d");
|
|
const e = this.itemAt(B, items, idx);
|
|
const r = this.retainDyn(B, e);
|
|
B.terminate(`ret ptr ${r}`);
|
|
B.startBlock(lStr);
|
|
host.declare(`declare ptr @scr_str_char_at(ptr, double)`);
|
|
host.declare(`declare ptr @scr_dyn_new_str(ptr)`);
|
|
host.declare(`declare void @scr_str_release(ptr)`);
|
|
const s = this.payloadOf(B, "%d", "ptr");
|
|
const n2 = B.tmp();
|
|
B.line(`${n2} = call double @scr_str_utf16_len(ptr ${s})`);
|
|
const idxD = B.tmp();
|
|
B.line(`${idxD} = ${sizeToDouble(host.sizeType)} ${host.sizeType} ${idx} to double`);
|
|
const inR2 = B.tmp();
|
|
B.line(`${inR2} = fcmp olt double ${idxD}, ${n2}`);
|
|
const lHit2 = B.newLabel("kg.ash");
|
|
B.condBr(inR2, lHit2, lMiss);
|
|
B.startBlock(lHit2);
|
|
const ch = B.tmp();
|
|
B.line(`${ch} = call ptr @scr_str_char_at(ptr ${s}, double ${idxD})`);
|
|
const r2 = B.tmp();
|
|
B.line(`${r2} = call ptr @scr_dyn_new_str(ptr ${ch})`);
|
|
B.line(`call void @scr_str_release(ptr ${ch})`);
|
|
B.terminate(`ret ptr ${r2}`);
|
|
B.startBlock(lMiss);
|
|
const lArrNamed = B.newLabel("kg.an");
|
|
const lOther = B.newLabel("kg.ao");
|
|
B.condBr(isA, lArrNamed, lOther);
|
|
B.startBlock(lArrNamed);
|
|
host.declare(`declare ptr @scr_dyn_arr_named_get(ptr, ptr)`);
|
|
const named = B.tmp();
|
|
B.line(`${named} = call ptr @scr_dyn_arr_named_get(ptr %d, ptr %k)`);
|
|
B.terminate(`ret ptr ${named}`);
|
|
B.startBlock(lOther);
|
|
retainUndef();
|
|
}
|
|
B.startBlock(lNext);
|
|
}
|
|
retainUndef();
|
|
this.defs.push(
|
|
`define internal ptr @${name}(ptr %d, ptr %k, i1 zeroext %opt) ${FN_ATTRS} { ; d[k] on dyn`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/* ── destructuring RequireObjectCoercible (ported) ─────────────────── */
|
|
|
|
dynDestrCheckHelper(): string {
|
|
const memoKey = "%dynDestrCheck";
|
|
const existing = this.dynBuilders.get(memoKey);
|
|
if (existing) return existing;
|
|
const name = "sc_dyn_destr_check";
|
|
this.dynBuilders.set(memoKey, name);
|
|
this.host.journalAllocated?.(name, "dyn.dynDestrCheckHelper", []);
|
|
const host = this.host;
|
|
host.declare(`declare void @scr_jb_init(ptr)`);
|
|
host.declare(`declare ptr @scr_jb_finish(ptr)`);
|
|
host.declare(`declare void @scr_jb_puts(ptr, ptr)`);
|
|
host.declare(`declare void @scr_throw_error(i32, ptr)`);
|
|
const B = new BlockBuilder();
|
|
const kd = this.kindOf(B, "%d");
|
|
const isU = B.tmp();
|
|
const isN = B.tmp();
|
|
const unit = B.tmp();
|
|
B.line(`${isU} = icmp eq i32 ${kd}, ${DYN_KIND.UNDEF}`);
|
|
B.line(`${isN} = icmp eq i32 ${kd}, ${DYN_KIND.NULL}`);
|
|
B.line(`${unit} = or i1 ${isU}, ${isN}`);
|
|
const lThrow = B.newLabel("ddc.t");
|
|
const lOk = B.newLabel("ddc.o");
|
|
B.condBr(unit, lThrow, lOk);
|
|
B.startBlock(lOk);
|
|
B.terminate(`ret void`);
|
|
B.startBlock(lThrow);
|
|
const buf = "%ddb";
|
|
B.entryAllocas.push(`${buf} = alloca %ScrJsonBuf`);
|
|
B.line(`call void @scr_jb_init(ptr ${buf})`);
|
|
const hasProp = B.tmp();
|
|
B.line(`${hasProp} = icmp ne ptr %firstProp, null`);
|
|
const lProp = B.newLabel("ddc.p");
|
|
const lBare = B.newLabel("ddc.b");
|
|
const lTail = B.newLabel("ddc.e");
|
|
B.condBr(hasProp, lProp, lBare);
|
|
B.startBlock(lProp);
|
|
this.puts(B, buf, "Cannot destructure property '");
|
|
B.line(`call void @scr_jb_puts(ptr ${buf}, ptr %firstProp)`);
|
|
this.puts(B, buf, "' of '");
|
|
B.line(`call void @scr_jb_puts(ptr ${buf}, ptr %spell)`);
|
|
this.puts(B, buf, "' as it is ");
|
|
B.br(lTail);
|
|
B.startBlock(lBare);
|
|
this.puts(B, buf, "Cannot destructure '");
|
|
B.line(`call void @scr_jb_puts(ptr ${buf}, ptr %spell)`);
|
|
this.puts(B, buf, "' as it is ");
|
|
B.br(lTail);
|
|
B.startBlock(lTail);
|
|
const tail = B.tmp();
|
|
B.line(`${tail} = select i1 ${isU}, ptr ${host.cstr("undefined.")}, ptr ${host.cstr("null.")}`);
|
|
B.line(`call void @scr_jb_puts(ptr ${buf}, ptr ${tail})`);
|
|
const msg = B.tmp();
|
|
B.line(`${msg} = call ptr @scr_jb_finish(ptr ${buf})`);
|
|
B.line(`call void @scr_throw_error(i32 1, ptr ${msg}) ; SCR_ERR_TYPE`);
|
|
B.terminate(`ret void`);
|
|
this.defs.push(
|
|
`define internal void @${name}(ptr %d, ptr %spell, ptr %firstProp) ${FN_ATTRS} { ; destructuring RequireObjectCoercible`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/* ── GetIterator + first-N steps (dynIterNHelper, ported) ──────────── */
|
|
|
|
dynIterNHelper(): string {
|
|
const memoKey = "%dynIterN";
|
|
const existing = this.dynBuilders.get(memoKey);
|
|
if (existing) return existing;
|
|
const name = "sc_dyn_iter_n";
|
|
this.dynBuilders.set(memoKey, name);
|
|
this.host.journalAllocated?.(name, "dyn.dynIterNHelper", []);
|
|
const host = this.host;
|
|
host.declare(`declare ptr @scr_dyn_new_arr()`);
|
|
host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
|
|
host.declare(`declare ptr @scr_dyn_new_num(double)`);
|
|
host.declare(`declare void @scr_str_release(ptr)`);
|
|
const B = new BlockBuilder();
|
|
const kd = this.kindOf(B, "%d");
|
|
// Typed stream capsules iterate their cached, refreshed dyn view.
|
|
{
|
|
const isTyped = B.tmp();
|
|
B.line(`${isTyped} = icmp eq i32 ${kd}, ${DYN_KIND.TYPED_REF}`);
|
|
const lTyped = B.newLabel("din.tr");
|
|
const lNext = B.newLabel("din.nt");
|
|
B.condBr(isTyped, lTyped, lNext);
|
|
B.startBlock(lTyped);
|
|
// Compiled generators step through the iterator protocol.
|
|
host.declare(`declare i1 @scr_dyn_iterates_by_protocol(ptr)`);
|
|
host.declare(`declare ptr @scr_dyn_iter_take(ptr, ${host.sizeType}, ptr)`);
|
|
const protocol = B.tmp();
|
|
const lProtocol = B.newLabel("din.pr");
|
|
const lView = B.newLabel("din.vw");
|
|
B.line(`${protocol} = call i1 @scr_dyn_iterates_by_protocol(ptr %d)`);
|
|
B.condBr(protocol, lProtocol, lView);
|
|
B.startBlock(lProtocol);
|
|
const stepped = B.tmp();
|
|
B.line(
|
|
`${stepped} = call ptr @scr_dyn_iter_take(ptr %d, ${host.sizeType} %n, ptr %spelling)`,
|
|
);
|
|
B.terminate(`ret ptr ${stepped}`);
|
|
B.startBlock(lView);
|
|
host.declare(`declare ptr @scr_dyn_typed_ref_materialize(ptr)`);
|
|
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
|
const materialized = B.tmp();
|
|
const out = B.tmp();
|
|
B.line(`${materialized} = call ptr @scr_dyn_typed_ref_materialize(ptr %d)`);
|
|
B.line(`${out} = call ptr @${name}(ptr ${materialized}, ${host.sizeType} %n, ptr %spelling)`);
|
|
B.line(`call void @scr_dyn_release_v(ptr ${materialized})`);
|
|
B.terminate(`ret ptr ${out}`);
|
|
B.startBlock(lNext);
|
|
}
|
|
|
|
const okA = B.tmp();
|
|
const okS = B.tmp();
|
|
const okB = B.tmp();
|
|
const ok01 = B.tmp();
|
|
const ok = B.tmp();
|
|
B.line(`${okA} = icmp eq i32 ${kd}, ${DYN_KIND.ARR}`);
|
|
B.line(`${okS} = icmp eq i32 ${kd}, ${DYN_KIND.STR}`);
|
|
B.line(`${okB} = icmp eq i32 ${kd}, ${DYN_KIND.BYTES}`);
|
|
B.line(`${ok01} = or i1 ${okA}, ${okS}`);
|
|
B.line(`${ok} = or i1 ${ok01}, ${okB}`);
|
|
const lGo = B.newLabel("din.g");
|
|
const lThrow = B.newLabel("din.t");
|
|
B.condBr(ok, lGo, lThrow);
|
|
// Other iterables step through the iterator protocol; non-iterables
|
|
// throw V8's exact not-iterable TypeError.
|
|
B.startBlock(lThrow);
|
|
host.declare(`declare ptr @scr_dyn_iter_take(ptr, ${host.sizeType}, ptr)`);
|
|
const taken = B.tmp();
|
|
B.line(`${taken} = call ptr @scr_dyn_iter_take(ptr %d, ${host.sizeType} %n, ptr %spelling)`);
|
|
B.terminate(`ret ptr ${taken}`);
|
|
B.startBlock(lGo);
|
|
const out = B.tmp();
|
|
B.line(`${out} = call ptr @scr_dyn_new_arr()`);
|
|
this.i64Loop(B, "din", "%n", (i) => {
|
|
const itemSlot = B.slot();
|
|
B.entryAllocas.push(`${itemSlot} = alloca ptr`);
|
|
const lArr = B.newLabel("din.ia");
|
|
const lBy = B.newLabel("din.ib");
|
|
const lStr = B.newLabel("din.is");
|
|
const lPush = B.newLabel("din.ip");
|
|
const lNotA = B.newLabel("din.na");
|
|
B.condBr(okA, lArr, lNotA);
|
|
B.startBlock(lNotA);
|
|
B.condBr(okB, lBy, lStr);
|
|
// ARR: item = i < len ? retain(items[i]) : retain(undefined)
|
|
B.startBlock(lArr);
|
|
{
|
|
const n = this.lenOf(B, "%d");
|
|
const inR = B.tmp();
|
|
B.line(`${inR} = icmp ult ${host.sizeType} ${i}, ${n}`);
|
|
const lHit = B.newLabel("din.ah");
|
|
const lMiss = B.newLabel("din.am");
|
|
B.condBr(inR, lHit, lMiss);
|
|
B.startBlock(lHit);
|
|
const items = this.itemsOf(B, "%d");
|
|
const e = this.itemAt(B, items, i);
|
|
const r = this.retainDyn(B, e);
|
|
B.line(`store ptr ${r}, ptr ${itemSlot}`);
|
|
B.br(lPush);
|
|
B.startBlock(lMiss);
|
|
const u = this.undef(B);
|
|
const r2 = this.retainDyn(B, u);
|
|
B.line(`store ptr ${r2}, ptr ${itemSlot}`);
|
|
B.br(lPush);
|
|
}
|
|
// BYTES: by numeric element.
|
|
B.startBlock(lBy);
|
|
{
|
|
const bts = this.payloadOf(B, "%d", "ptr");
|
|
const blenp = B.tmp();
|
|
const blen = B.tmp();
|
|
B.line(
|
|
`${blenp} = getelementptr inbounds i8, ptr ${bts}, i64 ${this.abiOffset(8, 4)} ; ->len`,
|
|
);
|
|
B.line(`${blen} = load ${host.sizeType}, ptr ${blenp}`);
|
|
const inR = B.tmp();
|
|
B.line(`${inR} = icmp ult ${host.sizeType} ${i}, ${blen}`);
|
|
const lHit = B.newLabel("din.bh");
|
|
const lMiss = B.newLabel("din.bm");
|
|
B.condBr(inR, lHit, lMiss);
|
|
B.startBlock(lHit);
|
|
host.declare(`declare double @scr_bytes_get(ptr, double)`);
|
|
const index = B.tmp();
|
|
const bd = B.tmp();
|
|
const r = B.tmp();
|
|
B.line(`${index} = ${sizeToDouble(host.sizeType)} ${host.sizeType} ${i} to double`);
|
|
B.line(`${bd} = call double @scr_bytes_get(ptr ${bts}, double ${index})`);
|
|
B.line(`${r} = call ptr @scr_dyn_new_num(double ${bd})`);
|
|
B.line(`store ptr ${r}, ptr ${itemSlot}`);
|
|
B.br(lPush);
|
|
B.startBlock(lMiss);
|
|
const u = this.undef(B);
|
|
const r2 = this.retainDyn(B, u);
|
|
B.line(`store ptr ${r2}, ptr ${itemSlot}`);
|
|
B.br(lPush);
|
|
}
|
|
// STR: whole code POINTS (the string iterator, not charAt).
|
|
B.startBlock(lStr);
|
|
{
|
|
host.declare(`declare double @scr_str_utf16_len(ptr)`);
|
|
host.declare(`declare ptr @scr_str_cp_at(ptr, double)`);
|
|
host.declare(`declare ptr @scr_dyn_new_str(ptr)`);
|
|
const s = this.payloadOf(B, "%d", "ptr");
|
|
const len = B.tmp();
|
|
B.line(`${len} = call double @scr_str_utf16_len(ptr ${s})`);
|
|
const atSlot = B.slot();
|
|
const stepSlot = B.slot();
|
|
B.entryAllocas.push(`${atSlot} = alloca double`, `${stepSlot} = alloca ${host.sizeType}`);
|
|
B.line(`store double ${f64Lit(0)}, ptr ${atSlot}`);
|
|
B.line(`store ${host.sizeType} 0, ptr ${stepSlot}`);
|
|
const lc = B.newLabel("din.sc");
|
|
const lb = B.newLabel("din.sb");
|
|
const le = B.newLabel("din.se");
|
|
B.br(lc);
|
|
B.startBlock(lc);
|
|
const st = B.tmp();
|
|
const at = B.tmp();
|
|
const c1 = B.tmp();
|
|
const c2 = B.tmp();
|
|
const cont = B.tmp();
|
|
B.line(`${st} = load ${host.sizeType}, ptr ${stepSlot}`);
|
|
B.line(`${at} = load double, ptr ${atSlot}`);
|
|
B.line(`${c1} = icmp ult ${host.sizeType} ${st}, ${i}`);
|
|
B.line(`${c2} = fcmp olt double ${at}, ${len}`);
|
|
B.line(`${cont} = and i1 ${c1}, ${c2}`);
|
|
B.condBr(cont, lb, le);
|
|
B.startBlock(lb);
|
|
const cp = B.tmp();
|
|
const cpl = B.tmp();
|
|
const at2 = B.tmp();
|
|
B.line(`${cp} = call ptr @scr_str_cp_at(ptr ${s}, double ${at})`);
|
|
B.line(`${cpl} = call double @scr_str_utf16_len(ptr ${cp})`);
|
|
B.line(`${at2} = fadd double ${at}, ${cpl}`);
|
|
B.line(`store double ${at2}, ptr ${atSlot}`);
|
|
B.line(`call void @scr_str_release(ptr ${cp})`);
|
|
const st2 = B.tmp();
|
|
B.line(`${st2} = add ${host.sizeType} ${st}, 1`);
|
|
B.line(`store ${host.sizeType} ${st2}, ptr ${stepSlot}`);
|
|
B.br(lc);
|
|
B.startBlock(le);
|
|
const atF = B.tmp();
|
|
B.line(`${atF} = load double, ptr ${atSlot}`);
|
|
const inR = B.tmp();
|
|
B.line(`${inR} = fcmp olt double ${atF}, ${len}`);
|
|
const lHit = B.newLabel("din.sh");
|
|
const lMiss = B.newLabel("din.sm");
|
|
B.condBr(inR, lHit, lMiss);
|
|
B.startBlock(lHit);
|
|
const cp2 = B.tmp();
|
|
const r = B.tmp();
|
|
B.line(`${cp2} = call ptr @scr_str_cp_at(ptr ${s}, double ${atF})`);
|
|
B.line(`${r} = call ptr @scr_dyn_new_str(ptr ${cp2})`);
|
|
B.line(`call void @scr_str_release(ptr ${cp2})`);
|
|
B.line(`store ptr ${r}, ptr ${itemSlot}`);
|
|
B.br(lPush);
|
|
B.startBlock(lMiss);
|
|
const u = this.undef(B);
|
|
const r2 = this.retainDyn(B, u);
|
|
B.line(`store ptr ${r2}, ptr ${itemSlot}`);
|
|
B.br(lPush);
|
|
}
|
|
B.startBlock(lPush);
|
|
const item = B.tmp();
|
|
B.line(`${item} = load ptr, ptr ${itemSlot}`);
|
|
B.line(`call void @scr_dyn_arr_push(ptr ${out}, ptr ${item}) ; push takes ownership`);
|
|
});
|
|
B.terminate(`ret ptr ${out}`);
|
|
this.defs.push(
|
|
`define internal ptr @${name}(ptr %d, ${host.sizeType} %n, ptr %spelling) ${FN_ATTRS} { ; destructuring GetIterator + N steps`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/* ── the checked-dynamic function boundary (ported) ────────────────── */
|
|
|
|
/** The dyn argument spelling of one static value: dyn passes through
|
|
* (+1), funcs box anonymously, everything else rides toDyn. `expr` is
|
|
* BORROWED in every arm. Emits into the given builder. */
|
|
/** A live capsule of a record or array value (+1; the value is
|
|
* borrowed), as `dynFrom` with `liveRef` builds it; null for other
|
|
* types. Closures boxed in a declaration-typed boundary program return
|
|
* their records and arrays this way. */
|
|
private liveToDyn(B: BlockBuilder, t: IrType, expr: string): string | null {
|
|
const host = this.host;
|
|
if (t.kind === "union") {
|
|
const arms = host.unionsById.get(t.unionId)?.arms ?? [];
|
|
if (!arms.some((arm) => arm.kind === "record" || arm.kind === "array")) return null;
|
|
const boxed = B.tmp();
|
|
B.line(`${boxed} = call ptr @${host.liveDynUnionRefAdapter(t)}(ptr ${expr})`);
|
|
return boxed;
|
|
}
|
|
if ((t.kind !== "record" && t.kind !== "array") || !streamTypedRefEligible(t)) return null;
|
|
const key = typeKey(t);
|
|
const adapter = host.liveDynRefAdapter(t);
|
|
const rc = vAdapters(host, t);
|
|
const boxed = B.tmp();
|
|
B.line(
|
|
`${boxed} = call ptr ${typedRefConstructor(host, t)}(ptr ${expr}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
|
|
);
|
|
for (const line of bindLiveArrayOps(host, adapter, boxed)) B.line(line);
|
|
return boxed;
|
|
}
|
|
|
|
private toDynExpr(B: BlockBuilder, t: IrType, expr: string): string {
|
|
if (t.kind === "dyn") return this.retainDyn(B, expr);
|
|
if (t.kind === "func") {
|
|
const box = this.dynFuncBoxHelper(t);
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @${box}(ptr ${expr}, ptr null)`);
|
|
return r;
|
|
}
|
|
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr @${this.toDynHelper(t)}(${this.valTy(t)} ${expr})`);
|
|
return r;
|
|
}
|
|
|
|
/** The call thunk for one closure signature: validate each dyn arg
|
|
* into the declared param type, call through the closure, convert the
|
|
* result back to a dyn value (+1). */
|
|
dynFuncThunkHelper(t: IrType & { kind: "func" }): string {
|
|
const key = typeKey(t);
|
|
const existing = this.dynFuncThunks.get(key);
|
|
if (existing) return existing;
|
|
const name = `sc_dfk_${this.dynFuncThunks.size}`;
|
|
this.dynFuncThunks.set(key, name);
|
|
this.host.journalAllocated?.(name, "dyn.dynFuncThunkHelper", [t]);
|
|
const host = this.host;
|
|
const B = new BlockBuilder();
|
|
const argNames: string[] = [];
|
|
const typedRest = t.rest === true && t.restAbi === "typed";
|
|
const packTail = (start: number): string => {
|
|
host.declare(`declare ptr @scr_dyn_new_arr()`);
|
|
host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
|
|
const packed = B.tmp();
|
|
B.line(`${packed} = call ptr @scr_dyn_new_arr()`);
|
|
const riSlot = B.slot();
|
|
B.entryAllocas.push(`${riSlot} = alloca ${host.sizeType}`);
|
|
B.line(`store ${host.sizeType} ${start}, ptr ${riSlot}`);
|
|
const lc = B.newLabel("dfk.rc");
|
|
const lb = B.newLabel("dfk.rb");
|
|
const le = B.newLabel("dfk.re");
|
|
B.br(lc);
|
|
B.startBlock(lc);
|
|
const ri = B.tmp();
|
|
const cont = B.tmp();
|
|
B.line(`${ri} = load ${host.sizeType}, ptr ${riSlot}`);
|
|
B.line(`${cont} = icmp ult ${host.sizeType} ${ri}, %argc`);
|
|
B.condBr(cont, lb, le);
|
|
B.startBlock(lb);
|
|
const ap = B.tmp();
|
|
const av = B.tmp();
|
|
B.line(`${ap} = getelementptr inbounds ptr, ptr %args, ${host.sizeType} ${ri}`);
|
|
B.line(`${av} = load ptr, ptr ${ap}`);
|
|
const rv = this.retainDyn(B, av);
|
|
B.line(`call void @scr_dyn_arr_push(ptr ${packed}, ptr ${rv})`);
|
|
const ri2 = B.tmp();
|
|
B.line(`${ri2} = add ${host.sizeType} ${ri}, 1`);
|
|
B.line(`store ${host.sizeType} ${ri2}, ptr ${riSlot}`);
|
|
B.br(lc);
|
|
B.startBlock(le);
|
|
return packed;
|
|
};
|
|
t.params.forEach((p, i) => {
|
|
if (typedRest && i === t.params.length - 1) {
|
|
const packed = packTail(i);
|
|
const a = B.tmp();
|
|
B.line(`${a} = call ${this.valTy(p)} @${this.dynCheckHelper(p)}(ptr ${packed}, ptr null)`);
|
|
host.declare(`declare void @scr_dyn_release(ptr)`);
|
|
B.line(`call void @scr_dyn_release(ptr ${packed})`);
|
|
this.pendingBail(
|
|
B,
|
|
"dfk.rest",
|
|
() => {
|
|
t.params.slice(0, i).forEach((q, j) => {
|
|
if (isRefCounted(q)) B.line(`call void ${releaseSym(host, q)}(ptr ${argNames[j]})`);
|
|
});
|
|
},
|
|
"ptr null",
|
|
);
|
|
argNames.push(a);
|
|
return;
|
|
}
|
|
// JS arity: a missing argument IS the undefined dyn value.
|
|
const adSlot = B.slot();
|
|
B.entryAllocas.push(`${adSlot} = alloca ptr`);
|
|
const has = B.tmp();
|
|
B.line(`${has} = icmp ult ${host.sizeType} ${i}, %argc`);
|
|
const lHas = B.newLabel("dfk.h");
|
|
const lMiss = B.newLabel("dfk.m");
|
|
const lj = B.newLabel("dfk.j");
|
|
B.condBr(has, lHas, lMiss);
|
|
B.startBlock(lHas);
|
|
const ap = B.tmp();
|
|
const av = B.tmp();
|
|
B.line(`${ap} = getelementptr inbounds ptr, ptr %args, ${host.sizeType} ${i}`);
|
|
B.line(`${av} = load ptr, ptr ${ap}`);
|
|
B.line(`store ptr ${av}, ptr ${adSlot}`);
|
|
B.br(lj);
|
|
B.startBlock(lMiss);
|
|
const u = this.undef(B);
|
|
B.line(`store ptr ${u}, ptr ${adSlot}`);
|
|
B.br(lj);
|
|
B.startBlock(lj);
|
|
const ad = B.tmp();
|
|
B.line(`${ad} = load ptr, ptr ${adSlot}`);
|
|
if (p.kind === "dyn") {
|
|
argNames.push(this.retainDyn(B, ad));
|
|
} else {
|
|
const pathSlot = B.slot();
|
|
B.entryAllocas.push(`${pathSlot} = alloca %ScrDynPath`);
|
|
const pp = B.tmp();
|
|
const kp2 = B.tmp();
|
|
const ip = B.tmp();
|
|
B.line(`${pp} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 0`);
|
|
B.line(`store ptr null, ptr ${pp}`);
|
|
B.line(`${kp2} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 1`);
|
|
B.line(`store ptr null, ptr ${kp2}`);
|
|
B.line(`${ip} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 2`);
|
|
B.line(`store ${host.sizeType} ${i}, ptr ${ip}`);
|
|
const a = B.tmp();
|
|
B.line(
|
|
`${a} = call ${this.valTy(p)} @${this.dynCheckHelper(p)}(ptr ${ad}, ptr ${pathSlot})`,
|
|
);
|
|
this.pendingBail(
|
|
B,
|
|
"dfk",
|
|
() => {
|
|
t.params.slice(0, i).forEach((q, j) => {
|
|
if (isRefCounted(q)) B.line(`call void ${releaseSym(host, q)}(ptr ${argNames[j]})`);
|
|
});
|
|
},
|
|
"ptr null",
|
|
);
|
|
// Declaration-typed boundary programs: an argument package code
|
|
// passes keeps its identity (scr_dyn_decl_adopt).
|
|
const capsule = this.host.declBoundary?.() === true ? this.liveToDyn(B, p, a) : null;
|
|
if (capsule !== null) {
|
|
host.declare(`declare void @scr_dyn_decl_adopt(ptr, ptr)`);
|
|
host.declare(`declare void @scr_dyn_release(ptr)`);
|
|
B.line(`call void @scr_dyn_decl_adopt(ptr ${ad}, ptr ${capsule})`);
|
|
B.line(`call void @scr_dyn_release(ptr ${capsule})`);
|
|
this.pendingBail(
|
|
B,
|
|
"dfk.adopt",
|
|
() => {
|
|
t.params.slice(0, i + 1).forEach((q, j) => {
|
|
const name = j === i ? a : argNames[j];
|
|
if (isRefCounted(q)) B.line(`call void ${releaseSym(host, q)}(ptr ${name})`);
|
|
});
|
|
},
|
|
"ptr null",
|
|
);
|
|
}
|
|
argNames.push(a);
|
|
}
|
|
});
|
|
// VARIADIC (rest-marked) signatures: one extra trailing dyn-array
|
|
// param carries the call's arguments from index params.length on.
|
|
const rest = t.rest && !typedRest ? packTail(t.argumentsAll ? 0 : t.params.length) : null;
|
|
// The closure CONSUMES its params (+1 each moved in).
|
|
const fnp = B.tmp();
|
|
const fn = B.tmp();
|
|
B.line(`${fnp} = getelementptr inbounds %ScrClosure, ptr %c, i64 0, i32 1`);
|
|
B.line(`${fn} = load ptr, ptr ${fnp}`);
|
|
const retTy = t.ret.kind === "void" ? "void" : this.valTy(t.ret);
|
|
const callArgs = [
|
|
`ptr %c`,
|
|
...t.params.map((p, i) => `${this.valTy(p)} ${argNames[i]}`),
|
|
...(rest !== null ? [`ptr ${rest}`] : []),
|
|
].join(", ");
|
|
if (t.ret.kind === "void") {
|
|
B.line(`call void ${fn}(${callArgs})`);
|
|
this.pendingBail(B, "dfkr", () => {}, "ptr null");
|
|
const u = this.undef(B);
|
|
const r = this.retainDyn(B, u);
|
|
B.terminate(`ret ptr ${r}`);
|
|
} else if (t.ret.kind === "dyn") {
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ptr ${fn}(${callArgs})`);
|
|
this.pendingBail(B, "dfkr", () => {}, "ptr null");
|
|
B.terminate(`ret ptr ${r}`);
|
|
} else {
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ${retTy} ${fn}(${callArgs})`);
|
|
this.pendingBail(B, "dfkr", () => {}, "ptr null");
|
|
const out =
|
|
this.host.declBoundary?.() === true
|
|
? (this.liveToDyn(B, t.ret, r) ?? this.toDynExpr(B, t.ret, r))
|
|
: this.toDynExpr(B, t.ret, r);
|
|
if (isRefCounted(t.ret)) B.line(`call void ${releaseSym(host, t.ret)}(ptr ${r})`);
|
|
B.terminate(`ret ptr ${out}`);
|
|
}
|
|
this.defs.push(
|
|
`define internal ptr @${name}(ptr %c, ptr %args, ${host.sizeType} %argc) ${FN_ATTRS} { ; dyn call thunk for ${key}`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/** `sc_jt_<n>(ptr c, ptr args, size argc) -> ptr`, a function value's
|
|
* jsval entry (ScrDynJsEntry) into a closure's jsval body `target`
|
|
* (IrFunction.jsEntry, 64-bit targets): passes each argument word to the
|
|
* body, a missing one as undefined and a dyn parameter's boxed, and
|
|
* returns the body's result as an owned word. Like the closure's own
|
|
* entry, the body consumes its arguments. With an exception pending the
|
|
* result is undefined. */
|
|
jsEntryThunk(target: string, params: readonly IrType[], ret: IrType): string {
|
|
const existing = this.jsEntryThunks.get(target);
|
|
if (existing) return existing;
|
|
const name = `sc_jt_${this.jsEntryThunks.size}`;
|
|
this.jsEntryThunks.set(target, name);
|
|
this.host.journalAllocated?.(name, "dyn.jsEntryThunk", [target, params, ret]);
|
|
const host = this.host;
|
|
const B = new BlockBuilder();
|
|
const undefinedWord = `inttoptr (i64 ${JS_BITS.undefined} to ptr)`;
|
|
const args = params.map((p, i) => {
|
|
const slot = B.slot();
|
|
B.entryAllocas.push(`${slot} = alloca ptr`);
|
|
B.line(`store ptr ${undefinedWord}, ptr ${slot}`);
|
|
const has = B.tmp();
|
|
B.line(`${has} = icmp ult ${host.sizeType} ${i}, %argc`);
|
|
const lHas = B.newLabel("jt.h");
|
|
const lj = B.newLabel("jt.j");
|
|
B.condBr(has, lHas, lj);
|
|
B.startBlock(lHas);
|
|
const ap = B.tmp();
|
|
const av = B.tmp();
|
|
B.line(`${ap} = getelementptr inbounds ptr, ptr %args, ${host.sizeType} ${i}`);
|
|
B.line(`${av} = load ptr, ptr ${ap}`);
|
|
B.line(`store ptr ${av}, ptr ${slot}`);
|
|
B.br(lj);
|
|
B.startBlock(lj);
|
|
const word = B.tmp();
|
|
B.line(`${word} = load ptr, ptr ${slot}`);
|
|
const owned = B.tmp();
|
|
if (p.kind === "jsval") {
|
|
host.declare(`declare ptr @scr_js_retain(ptr)`);
|
|
B.line(`${owned} = call ptr @scr_js_retain(ptr ${word})`);
|
|
} else if (p.kind === "dyn") {
|
|
host.declare(`declare ptr @scr_js_to_dyn_borrow(ptr)`);
|
|
B.line(`${owned} = call ptr @scr_js_to_dyn_borrow(ptr ${word})`);
|
|
} else {
|
|
throw new InternalCompilerError(`llvm emitter bug: jsval entry parameter ${p.kind}`);
|
|
}
|
|
return `ptr ${owned}`;
|
|
});
|
|
const callArgs = [`ptr %c`, ...args].join(", ");
|
|
if (ret.kind === "void") {
|
|
B.line(`call void @${target}(${callArgs})`);
|
|
this.pendingBail(B, "jtr", () => {}, `ptr ${undefinedWord}`);
|
|
B.terminate(`ret ptr ${undefinedWord}`);
|
|
} else {
|
|
const r = B.tmp();
|
|
B.line(`${r} = call ${this.valTy(ret)} @${target}(${callArgs})`);
|
|
this.pendingBail(B, "jtr", () => {}, `ptr ${undefinedWord}`);
|
|
if (ret.kind === "jsval") {
|
|
B.terminate(`ret ptr ${r}`);
|
|
} else if (ret.kind === "f64") {
|
|
const raw = B.tmp();
|
|
const nan = B.tmp();
|
|
const bits = B.tmp();
|
|
const word = B.tmp();
|
|
B.line(`${raw} = bitcast double ${r} to i64`);
|
|
B.line(`${nan} = fcmp uno double ${r}, ${r}`);
|
|
B.line(`${bits} = select i1 ${nan}, i64 ${JS_BITS.canonicalNan}, i64 ${raw}`);
|
|
B.line(`${word} = inttoptr i64 ${bits} to ptr`);
|
|
B.terminate(`ret ptr ${word}`);
|
|
} else if (ret.kind === "bool") {
|
|
const bits = B.tmp();
|
|
const word = B.tmp();
|
|
B.line(`${bits} = select i1 ${r}, i64 ${JS_BITS.true}, i64 ${JS_BITS.false}`);
|
|
B.line(`${word} = inttoptr i64 ${bits} to ptr`);
|
|
B.terminate(`ret ptr ${word}`);
|
|
} else {
|
|
// dyn, or a typed result boxed as the closure's own entry would.
|
|
const d = ret.kind === "dyn" ? r : this.toDynExpr(B, ret, r);
|
|
if (ret.kind !== "dyn" && isRefCounted(ret))
|
|
B.line(`call void ${releaseSym(host, ret)}(ptr ${r})`);
|
|
host.declare(`declare ptr @scr_js_from_dyn(ptr)`);
|
|
const word = B.tmp();
|
|
B.line(`${word} = call ptr @scr_js_from_dyn(ptr ${d})`);
|
|
B.terminate(`ret ptr ${word}`);
|
|
}
|
|
}
|
|
this.defs.push(
|
|
`define internal ptr @${name}(ptr %c, ptr %args, ${host.sizeType} %argc) ${FN_ATTRS} { ; jsval entry into ${llvmCommentText(target)}`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/** The box builder dynFrom emits for one closure signature. */
|
|
dynFuncBoxHelper(t: IrType & { kind: "func" }): string {
|
|
const key = typeKey(t);
|
|
const existing = this.dynFuncBoxes.get(key);
|
|
if (existing) return existing;
|
|
const name = `sc_dfb_${this.dynFuncBoxes.size}`;
|
|
this.dynFuncBoxes.set(key, name);
|
|
this.host.journalAllocated?.(name, "dyn.dynFuncBoxHelper", [t]);
|
|
const host = this.host;
|
|
const thunk = this.dynFuncThunkHelper(t);
|
|
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
|
|
host.declare(`declare ptr @scr_dyn_new_func(ptr, ptr, i32, ptr, ptr)`);
|
|
const sigLit = host.cstr(key);
|
|
// A closure of this signature with a jsval body gets the function
|
|
// value's jsval entry, however the value is boxed: the closure's
|
|
// entry pointer names it.
|
|
const entries =
|
|
t.rest === true
|
|
? []
|
|
: (host.jsEntryClosures?.() ?? []).filter(
|
|
(c) =>
|
|
c.params.length === t.params.length &&
|
|
c.params.every((p, i) => typeKey(p) === typeKey(t.params[i]!)) &&
|
|
typeKey(c.ret) === typeKey(t.ret),
|
|
);
|
|
const register: string[] = [];
|
|
if (entries.length > 0) {
|
|
host.declare(`declare void @scr_dyn_func_set_js(ptr, ptr, ptr)`);
|
|
register.push(
|
|
` %fnp = getelementptr inbounds %ScrClosure, ptr %c, i64 0, i32 1`,
|
|
` %fn = load ptr, ptr %fnp`,
|
|
);
|
|
entries.forEach((c, i) => {
|
|
const jsEntry = this.jsEntryThunk(c.body, c.bodyParams, c.bodyRet);
|
|
register.push(
|
|
` %is${i} = icmp eq ptr %fn, @${c.closure}`,
|
|
` br i1 %is${i}, label %set${i}, label %next${i}`,
|
|
`set${i}:`,
|
|
` call void @scr_dyn_func_set_js(ptr %r, ptr @${thunk}, ptr @${jsEntry})`,
|
|
` ret ptr %r`,
|
|
`next${i}:`,
|
|
);
|
|
});
|
|
}
|
|
this.defs.push(
|
|
`define internal ptr @${name}(ptr %v, ptr %fname) ${FN_ATTRS} { ; box ${key} into dyn`,
|
|
`entry:`,
|
|
` %c = call ptr @scr_closure_retain_v(ptr %v)`,
|
|
` %r = call ptr @scr_dyn_new_func(ptr %c, ptr @${thunk}, i32 ${t.params.length - (t.restAbi === "typed" ? 1 : 0)}, ptr ${sigLit}, ptr %fname)`,
|
|
...register,
|
|
` ret ptr %r`,
|
|
`}`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/** `sc_dml_<n>(ptr c, ptr message) -> i1`, the typed message entry of a
|
|
* closure registered as a 'message' listener (ScrDynMessageListener):
|
|
* builds the closure's parameter from the message's nodes and calls the
|
|
* closure. It answers false, without calling it, when the message does
|
|
* not have the parameter's exact layout, and the listener then runs
|
|
* through its thunk on the decoded message, as before. Null when the
|
|
* signature's parameter has no message reader (messageReadable). */
|
|
messageListenerHelper(t: IrType & { kind: "func" }): string | null {
|
|
const key = typeKey(t);
|
|
const existing = this.messageListeners.get(key);
|
|
if (existing !== undefined) return existing;
|
|
const param = t.params[0];
|
|
const supported =
|
|
t.params.length === 1 &&
|
|
t.rest !== true &&
|
|
param !== undefined &&
|
|
(t.ret.kind === "void" || t.ret.kind === "f64" || t.ret.kind === "bool") &&
|
|
this.messageReadable(param, new Set());
|
|
if (!supported) {
|
|
// A signature without an entry still takes a number.
|
|
this.messageListeners.set(key, null);
|
|
this.host.journalAllocated?.("", "dyn.messageListenerHelper", [t]);
|
|
this.host.journalBuilt?.();
|
|
return null;
|
|
}
|
|
const name = `sc_dml_${this.messageListeners.size}`;
|
|
this.messageListeners.set(key, name);
|
|
this.host.journalAllocated?.(name, "dyn.messageListenerHelper", [t]);
|
|
const B = new BlockBuilder();
|
|
B.entryAllocas.push(`%fail = alloca i1`);
|
|
B.line(`store i1 false, ptr %fail`);
|
|
const value = B.tmp();
|
|
B.line(
|
|
`${value} = call ${this.valTy(param)} @${this.messageReaderHelper(param)}(ptr %m, ${this.host.sizeType} 0, ptr %fail)`,
|
|
);
|
|
const failed = B.tmp();
|
|
B.line(`${failed} = load i1, ptr %fail`);
|
|
const lMiss = B.newLabel("dml.miss");
|
|
const lCall = B.newLabel("dml.call");
|
|
B.condBr(failed, lMiss, lCall);
|
|
B.startBlock(lMiss);
|
|
B.terminate(`ret i1 false`);
|
|
B.startBlock(lCall);
|
|
// The closure consumes its parameter; a pending exception stays for
|
|
// the emitter, as after the thunk.
|
|
const fnp = B.tmp();
|
|
const fn = B.tmp();
|
|
B.line(`${fnp} = getelementptr inbounds %ScrClosure, ptr %c, i64 0, i32 1`);
|
|
B.line(`${fn} = load ptr, ptr ${fnp}`);
|
|
const args = `ptr %c, ${this.valTy(param)} ${value}`;
|
|
if (t.ret.kind === "void") B.line(`call void ${fn}(${args})`);
|
|
else B.line(`${B.tmp()} = call ${this.valTy(t.ret)} ${fn}(${args})`);
|
|
B.terminate(`ret i1 true`);
|
|
this.defs.push(
|
|
`define internal zeroext i1 @${name}(ptr %c, ptr %m) ${FN_ATTRS} { ; message entry for ${llvmCommentText(key)}`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/** Whether a message reader can build T: numbers, booleans and strings;
|
|
* records of them with only declared fields, none of union type; and
|
|
* boxed unions of those arms and null or undefined without a literal
|
|
* discriminator, with at most one number, boolean and string arm. */
|
|
private messageReadable(t: IrType, seen: Set<string>): boolean {
|
|
switch (t.kind) {
|
|
case "f64":
|
|
case "bool":
|
|
case "string":
|
|
return true;
|
|
case "record": {
|
|
const shape = this.host.recordsById.get(t.shapeId);
|
|
if (!shape || shape.tuple || shape.indexValue || shape.fields.length === 0) return false;
|
|
if (seen.has(t.shapeId) || this.isRecursiveShape(t)) return false;
|
|
seen.add(t.shapeId);
|
|
const readable = shape.fields.every(
|
|
(f) =>
|
|
!f.name.startsWith("%") &&
|
|
f.type.kind !== "union" &&
|
|
llFieldType(f.type, this.host.inlineUnions.record(f.type)) !== INLINE_UNION_FIELD &&
|
|
this.messageReadable(f.type, seen),
|
|
);
|
|
seen.delete(t.shapeId);
|
|
return readable;
|
|
}
|
|
case "union": {
|
|
const def = this.host.unionsById.get(t.unionId);
|
|
// A discriminated union's checked conversion also tests the arm's
|
|
// literal discriminator, which the readers do not.
|
|
if (!def || def.discriminant || this.host.nullableUnions.has(t.unionId)) return false;
|
|
const scalars = new Set<string>();
|
|
for (const arm of def.arms) {
|
|
if (arm.kind === "nullT" || arm.kind === "undefinedT") continue;
|
|
if (arm.kind === "f64" || arm.kind === "bool" || arm.kind === "string") {
|
|
if (scalars.has(arm.kind)) return false;
|
|
scalars.add(arm.kind);
|
|
continue;
|
|
}
|
|
if (arm.kind !== "record" || !this.messageReadable(arm, seen)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** `sc_dmr_<n>(ptr message, size node, ptr fail) -> T`: T (+1) built
|
|
* from a message node, for a type messageReadable accepts. Sets *fail
|
|
* and returns a dummy when the node does not have T's layout: the arm
|
|
* the checked conversion would choose, a record's declared keys in
|
|
* declaration order and no others. */
|
|
private messageReaderHelper(t: IrType): string {
|
|
const key = typeKey(t);
|
|
const existing = this.messageReaders.get(key);
|
|
if (existing) return existing;
|
|
const name = `sc_dmr_${this.messageReaders.size}`;
|
|
this.messageReaders.set(key, name);
|
|
this.host.journalAllocated?.(name, "dyn.messageReaderHelper", [t]);
|
|
const host = this.host;
|
|
const S = host.sizeType;
|
|
if (t.kind !== "record") host.declare(`declare i32 @scr_message_kind(ptr, ${S})`);
|
|
const retTy = this.valTy(t);
|
|
const dummy = retTy === "double" ? f64Lit(0) : retTy === "i1" ? "false" : "null";
|
|
const B = new BlockBuilder();
|
|
const lFail = B.newLabel("dmr.fail");
|
|
// scr_message_fields tests a record's kind itself.
|
|
const kind = B.tmp();
|
|
if (t.kind !== "record") B.line(`${kind} = call i32 @scr_message_kind(ptr %m, ${S} %n)`);
|
|
const requireKind = (want: number): void => {
|
|
const ok = B.tmp();
|
|
B.line(`${ok} = icmp eq i32 ${kind}, ${want}`);
|
|
const lOk = B.newLabel("dmr.kind");
|
|
B.condBr(ok, lOk, lFail);
|
|
B.startBlock(lOk);
|
|
};
|
|
switch (t.kind) {
|
|
case "f64":
|
|
case "bool": {
|
|
host.declare(`declare double @scr_message_number(ptr, ${S})`);
|
|
requireKind(t.kind === "f64" ? MESSAGE_KIND.NUMBER : MESSAGE_KIND.BOOL);
|
|
const n = B.tmp();
|
|
B.line(`${n} = call double @scr_message_number(ptr %m, ${S} %n)`);
|
|
if (t.kind === "f64") B.terminate(`ret double ${n}`);
|
|
else {
|
|
const b = B.tmp();
|
|
B.line(`${b} = fcmp une double ${n}, ${f64Lit(0)}`);
|
|
B.terminate(`ret i1 ${b}`);
|
|
}
|
|
break;
|
|
}
|
|
case "string": {
|
|
host.declare(`declare ptr @scr_message_string(ptr, ${S})`);
|
|
requireKind(MESSAGE_KIND.STRING);
|
|
const str = B.tmp();
|
|
B.line(`${str} = call ptr @scr_message_string(ptr %m, ${S} %n)`);
|
|
B.terminate(`ret ptr ${str}`);
|
|
break;
|
|
}
|
|
case "record": {
|
|
const shape = host.recordsById.get(t.shapeId)!;
|
|
const order = recordSourceFieldOrder(shape);
|
|
const fields = new Map(shape.fields.map((f, i) => [f.name, { field: f, index: i }]));
|
|
const base = host.nullableUnions.fieldBase(t.shapeId);
|
|
const struct = mangleRecordStruct(t.shapeId);
|
|
const names = this.recordNameTable(t.shapeId, shape);
|
|
host.declare(`declare zeroext i1 @scr_message_fields(ptr, ${S}, ptr, ptr, ${S}, ptr)`);
|
|
B.entryAllocas.push(`%kids = alloca [${order.length} x ${S}]`);
|
|
const matched = B.tmp();
|
|
B.line(
|
|
`${matched} = call zeroext i1 @scr_message_fields(ptr %m, ${S} %n, ptr ${names.names}, ptr ${names.lens}, ${S} ${order.length}, ptr %kids)`,
|
|
);
|
|
const lBuild = B.newLabel("dmr.build");
|
|
B.condBr(matched, lBuild, lFail);
|
|
B.startBlock(lBuild);
|
|
B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`);
|
|
order.forEach((fieldName, i) => {
|
|
const { field, index } = fields.get(fieldName)!;
|
|
const kidp = B.tmp();
|
|
const kid = B.tmp();
|
|
B.line(
|
|
`${kidp} = getelementptr inbounds [${order.length} x ${S}], ptr %kids, i64 0, i64 ${i}`,
|
|
);
|
|
B.line(`${kid} = load ${S}, ptr ${kidp}`);
|
|
const v = B.tmp();
|
|
B.line(
|
|
`${v} = call ${this.valTy(field.type)} @${this.messageReaderHelper(field.type)}(ptr %m, ${S} ${kid}, ptr %fail)`,
|
|
);
|
|
const failed = B.tmp();
|
|
B.line(`${failed} = load i1, ptr %fail`);
|
|
const lDrop = B.newLabel("dmr.drop");
|
|
const lStore = B.newLabel("dmr.store");
|
|
B.condBr(failed, lDrop, lStore);
|
|
B.startBlock(lDrop);
|
|
B.line(`call void ${releaseSym(host, t)}(ptr %r0)`);
|
|
B.terminate(`ret ptr null`);
|
|
B.startBlock(lStore);
|
|
const p = B.tmp();
|
|
B.line(
|
|
`${p} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${index + base} ; .${llvmCommentText(fieldName)}`,
|
|
);
|
|
const fieldTy = llFieldType(field.type, host.inlineUnions.record(field.type));
|
|
if (fieldTy === "i8") {
|
|
const z = B.tmp();
|
|
B.line(`${z} = zext i1 ${v} to i8`);
|
|
B.line(`store i8 ${z}, ptr ${p}`);
|
|
} else {
|
|
B.line(`store ${fieldTy} ${v}, ptr ${p}`);
|
|
}
|
|
});
|
|
B.terminate(`ret ptr %r0`);
|
|
break;
|
|
}
|
|
case "union": {
|
|
const def = host.unionsById.get(t.unionId)!;
|
|
def.arms.forEach((arm, i) => {
|
|
const lNext = B.newLabel("dmr.next");
|
|
if (arm.kind === "nullT" || arm.kind === "undefinedT") {
|
|
const hit = B.tmp();
|
|
B.line(
|
|
`${hit} = icmp eq i32 ${kind}, ${arm.kind === "nullT" ? MESSAGE_KIND.NULL : MESSAGE_KIND.UNDEFINED}`,
|
|
);
|
|
const lHit = B.newLabel("dmr.unit");
|
|
B.condBr(hit, lHit, lNext);
|
|
B.startBlock(lHit);
|
|
B.terminate(`ret ptr ${host.unitInstanceRef(t.unionId, i)}`);
|
|
B.startBlock(lNext);
|
|
return;
|
|
}
|
|
if (arm.kind !== "record") {
|
|
const want =
|
|
arm.kind === "f64"
|
|
? MESSAGE_KIND.NUMBER
|
|
: arm.kind === "bool"
|
|
? MESSAGE_KIND.BOOL
|
|
: MESSAGE_KIND.STRING;
|
|
const hit = B.tmp();
|
|
B.line(`${hit} = icmp eq i32 ${kind}, ${want}`);
|
|
const lHit = B.newLabel("dmr.arm");
|
|
B.condBr(hit, lHit, lNext);
|
|
B.startBlock(lHit);
|
|
}
|
|
// A record arm that does not match leaves *fail clear for the
|
|
// next arm, like the checked conversion's next-arm match.
|
|
const v = B.tmp();
|
|
B.line(
|
|
`${v} = call ${this.valTy(arm)} @${this.messageReaderHelper(arm)}(ptr %m, ${S} %n, ptr %fail)`,
|
|
);
|
|
if (arm.kind === "record") {
|
|
const failed = B.tmp();
|
|
B.line(`${failed} = load i1, ptr %fail`);
|
|
const lHit = B.newLabel("dmr.arm");
|
|
const lMiss = B.newLabel("dmr.miss");
|
|
B.condBr(failed, lMiss, lHit);
|
|
B.startBlock(lMiss);
|
|
B.line(`store i1 false, ptr %fail`);
|
|
B.br(lNext);
|
|
B.startBlock(lHit);
|
|
}
|
|
const u = B.tmp();
|
|
if (arm.kind === "f64") {
|
|
host.declare(`declare ptr @scr_union_new_f64(i32, double)`);
|
|
B.line(`${u} = call ptr @scr_union_new_f64(i32 ${i}, double ${v})`);
|
|
} else if (arm.kind === "bool") {
|
|
host.declare(`declare ptr @scr_union_new_bool(i32, i1 zeroext)`);
|
|
B.line(`${u} = call ptr @scr_union_new_bool(i32 ${i}, i1 ${v})`);
|
|
} else {
|
|
const rc = vAdapters(host, arm);
|
|
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
|
|
B.line(
|
|
`${u} = call ptr @scr_union_new_ref(i32 ${i}, ptr ${v}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, arm)})`,
|
|
);
|
|
}
|
|
B.terminate(`ret ptr ${u}`);
|
|
B.startBlock(lNext);
|
|
});
|
|
B.br(lFail);
|
|
break;
|
|
}
|
|
default:
|
|
throw new InternalCompilerError(`llvm emitter bug: message reader of ${key}`);
|
|
}
|
|
B.startBlock(lFail);
|
|
B.line(`store i1 true, ptr %fail`);
|
|
B.terminate(`ret ${retTy} ${dummy}`);
|
|
this.defs.push(
|
|
`define internal ${retTy === "i1" ? "zeroext i1" : retTy} @${name}(ptr %m, ${S} %n, ptr %fail) ${FN_ATTRS} { ; message reader for ${llvmCommentText(key)}`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
|
|
/** The adapter closure body for one TARGET signature: caps[0] is an
|
|
* untraced obj-box owning the dyn function value. */
|
|
dynFuncAdapterHelper(t: IrType & { kind: "func" }): string {
|
|
const key = typeKey(t);
|
|
const existing = this.dynFuncAdapters.get(key);
|
|
if (existing) return existing;
|
|
const name = `sc_dfa_${this.dynFuncAdapters.size}`;
|
|
this.dynFuncAdapters.set(key, name);
|
|
this.host.journalAllocated?.(name, "dyn.dynFuncAdapterHelper", [t]);
|
|
const host = this.host;
|
|
const B = new BlockBuilder();
|
|
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
|
host.declare(`declare ptr @scr_dyn_call(ptr, ptr, ${host.sizeType}, ptr)`);
|
|
host.declare(`declare void @scr_dyn_release(ptr)`);
|
|
const retTy = t.ret.kind === "void" ? "void" : this.valTy(t.ret);
|
|
const dummy =
|
|
t.ret.kind === "void"
|
|
? "void"
|
|
: retTy === "double"
|
|
? `double ${f64Lit(0)}`
|
|
: retTy === "i1"
|
|
? "i1 false"
|
|
: "ptr null";
|
|
const capp = B.tmp();
|
|
const box = B.tmp();
|
|
B.line(`${capp} = getelementptr inbounds %ScrClosure, ptr %sc_env, i64 1 ; caps[0]`);
|
|
B.line(`${box} = load ptr, ptr ${capp}`);
|
|
const fnv = B.tmp();
|
|
B.line(`${fnv} = call ptr @scr_box_get_ref(ptr ${box}) ; +1`);
|
|
// The adapter OWNS its params (closure ABI); each converts to a dyn
|
|
// argument (borrowed by the conversion) and releases.
|
|
let argsPtr = "null";
|
|
const argVals: string[] = [];
|
|
const checkedRest = t.rest === true && t.restAbi === undefined;
|
|
let packed: string | null = null;
|
|
if (checkedRest) {
|
|
host.declare(`declare ptr @scr_dyn_apply(ptr, ptr, ptr)`);
|
|
host.declare(`declare ptr @scr_dyn_new_arr()`);
|
|
host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
|
|
host.declare(`declare void @scr_dyn_arr_push_spread(ptr, ptr, ptr)`);
|
|
packed = t.argumentsAll ? "%rest" : B.tmp();
|
|
if (!t.argumentsAll) B.line(`${packed} = call ptr @scr_dyn_new_arr()`);
|
|
t.params.forEach((p, i) => {
|
|
if (!t.argumentsAll) {
|
|
const v = this.toDynExpr(B, p, `%a${i}`);
|
|
B.line(`call void @scr_dyn_arr_push(ptr ${packed}, ptr ${v})`);
|
|
}
|
|
if (isRefCounted(p)) B.line(`call void ${releaseSym(host, p)}(ptr %a${i})`);
|
|
});
|
|
if (!t.argumentsAll) {
|
|
B.line(
|
|
`call void @scr_dyn_arr_push_spread(ptr ${packed}, ptr %rest, ptr ${host.cstr("")})`,
|
|
);
|
|
B.line(`call void @scr_dyn_release(ptr %rest)`);
|
|
}
|
|
} else if (t.params.length > 0) {
|
|
const arr = B.slot();
|
|
B.entryAllocas.push(`${arr} = alloca [${t.params.length} x ptr]`);
|
|
t.params.forEach((p, i) => {
|
|
const v = this.toDynExpr(B, p, `%a${i}`);
|
|
argVals.push(v);
|
|
const slotp = B.tmp();
|
|
B.line(
|
|
`${slotp} = getelementptr inbounds [${t.params.length} x ptr], ptr ${arr}, i64 0, ${host.sizeType} ${i}`,
|
|
);
|
|
B.line(`store ptr ${v}, ptr ${slotp}`);
|
|
if (isRefCounted(p)) B.line(`call void ${releaseSym(host, p)}(ptr %a${i})`);
|
|
});
|
|
argsPtr = arr;
|
|
}
|
|
// The kind is FUNC by construction; `what` is unreachable — spelled
|
|
// anyway (the C's "value").
|
|
const r = B.tmp();
|
|
if (packed) {
|
|
B.line(
|
|
`${r} = call ptr @scr_dyn_apply(ptr ${fnv}, ptr ${packed}, ptr ${host.cstr("value")})`,
|
|
);
|
|
B.line(`call void @scr_dyn_release(ptr ${packed})`);
|
|
} else
|
|
B.line(
|
|
`${r} = call ptr @scr_dyn_call(ptr ${fnv}, ptr ${argsPtr}, ${host.sizeType} ${t.params.length}, ptr ${host.cstr("value")})`,
|
|
);
|
|
B.line(`call void @scr_dyn_release(ptr ${fnv})`);
|
|
for (const v of argVals) B.line(`call void @scr_dyn_release(ptr ${v})`);
|
|
this.pendingBail(B, "dfa", () => {}, dummy === "void" ? "void" : dummy);
|
|
if (t.ret.kind === "void") {
|
|
B.line(`call void @scr_dyn_release(ptr ${r})`);
|
|
B.terminate(`ret void`);
|
|
} else if (t.ret.kind === "dyn") {
|
|
B.terminate(`ret ptr ${r}`);
|
|
} else {
|
|
// Validate the dyn result into the target's return type — a lying
|
|
// wrapper throws the catchable TypeError here (path "$").
|
|
const out = B.tmp();
|
|
B.line(
|
|
`${out} = call ${this.valTy(t.ret)} @${this.dynCheckHelper(t.ret)}(ptr ${r}, ptr null)`,
|
|
);
|
|
B.line(`call void @scr_dyn_release(ptr ${r})`);
|
|
B.terminate(`ret ${this.valTy(t.ret)} ${out}`);
|
|
}
|
|
const params = [
|
|
"ptr %sc_env",
|
|
...t.params.map((p, i) => `${this.valTy(p)} %a${i}`),
|
|
...(checkedRest ? ["ptr %rest"] : []),
|
|
].join(", ");
|
|
this.defs.push(
|
|
`define internal ${retTy === "i1" ? "zeroext i1" : retTy} @${name}(${params}) ${FN_ATTRS} { ; dyn fn adapter to ${key}`,
|
|
B.render(),
|
|
`}`,
|
|
``,
|
|
);
|
|
this.host.journalBuilt?.();
|
|
return name;
|
|
}
|
|
}
|