mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
- Add shared ArrayBuffer storage and native Unicode grapheme segmentation. - Preserve builtin modules, globals, and byte views across JavaScript bindings. - Compile package initialization expressions and optional native callables.
9376 lines
475 KiB
TypeScript
9376 lines
475 KiB
TypeScript
import { InternalCompilerError } from "../../errors.js";
|
|
/* Expression C emission: the whole IrExpr dispatch (emitExpr) — every IR
|
|
* expression lands in a fresh C temp, with RC ownership tracked on the
|
|
* emitter's frames (see the discipline comment in emitter core). */
|
|
import type { CEmitter, Temp } from "./c-emitter.js";
|
|
import { arrayOf, BOOL, BYTES_U8, bytesOf, canMarshalFuncIntoIsland, CHILDSTREAM_T, CHILDWRITER_T, DYN, F64, type IrExpr, type IrLibFn, type IrRecordShape, type IrType, islandPromisePayloadTag, classDynViewSupported, isClassOwnEnumerableFieldName, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, MAY_THROW_LIB_FNS, NETSOCKET_T, RUNTIME_ERROR_CLASSES, STRING, typeEquals, typeKey } from "../../ir/ir.js";
|
|
import { BYTES_NUM_KIND_C, BYTES_NUM_VAR_C, bytesElemKindC, cDecl, cFnPtrCast, cNumberLiteral, cStringLiteral, cType, DV_GET_KIND_C, DV_SET_KIND_C, elemAccess, mapKeyAccess, mapKeyKindC, mapValKindC, releaseCallC, retainCallC, vAdapters } from "./types.js";
|
|
import { mangleClassNew, mangleClassRetain, mangleClassStruct, mangleField, mangleFnClosure, mangleFunction, mangleGlobal, mangleLocal, mangleRecordClone, mangleRecordNew, mangleRecordStruct, mangleVtStruct } from "../mangle.js";
|
|
import { OVERFLOW_MEMBER } from "./shapes.js";
|
|
import { checkGlobalTdz, readBox, writeBox } from "./bindings.js";
|
|
import { dynDestrCheckHelper, dynIterNHelper, dynKeyGetHelper, unionWidenHelper } from "./walkers.js";
|
|
import { collectFfiRetainedOps, parseFfiCallbackKey } from "../ffi-callbacks.js";
|
|
import { genResultThunkFor } from "./async.js";
|
|
import { isStableReceiverOperand, matchStringSelfConcat, newValueMayThrow, streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js";
|
|
|
|
function streamTypedRefCommitAdapter(
|
|
emitter: CEmitter,
|
|
t: IrType,
|
|
snapshot: string,
|
|
defs: string[],
|
|
): string {
|
|
if (t.kind === "bytes") {
|
|
const commit = `${snapshot}_commit`;
|
|
emitter.walkerProtos.push(
|
|
`static void ${commit}(void *sc_p, const ScrDyn *sc_d); /* commit live stream element ${typeKey(t)} */`,
|
|
);
|
|
defs.push(
|
|
`static void ${commit}(void *sc_p, const ScrDyn *sc_d) {`,
|
|
` ScrBytes *sc_target = (ScrBytes *)sc_p;`,
|
|
` ScrBytes *sc_next = ${emitter.dynCheckHelper(t)}(sc_d, NULL);`,
|
|
` if (!sc_next) return;`,
|
|
` scr_bytes_copy_contents(sc_target, sc_next);`,
|
|
` scr_bytes_release(sc_next);`,
|
|
`}`,
|
|
``,
|
|
);
|
|
return `&${commit}`;
|
|
}
|
|
if (t.kind === "array") {
|
|
const commit = `${snapshot}_commit`;
|
|
emitter.walkerProtos.push(
|
|
`static void ${commit}(void *sc_p, const ScrDyn *sc_d); /* commit live stream element ${typeKey(t)} */`,
|
|
);
|
|
defs.push(
|
|
`static void ${commit}(void *sc_p, const ScrDyn *sc_d) {`,
|
|
` ScrArr *sc_target = (ScrArr *)sc_p;`,
|
|
` ScrArr *sc_next = ${emitter.dynCheckHelper(t)}(sc_d, NULL);`,
|
|
` if (!sc_next) return;`,
|
|
` size_t sc_target_len = sc_target->len;`,
|
|
` size_t sc_target_cap = sc_target->cap;`,
|
|
` uint64_t *sc_target_data = sc_target->data;`,
|
|
` uint8_t *sc_target_present = sc_target->present;`,
|
|
` ScrArrSparseSlot *sc_target_sparse = sc_target->sparse;`,
|
|
` size_t sc_target_sparse_len = sc_target->sparse_len;`,
|
|
` size_t sc_target_sparse_cap = sc_target->sparse_cap;`,
|
|
` ScrArrProp *sc_target_props = sc_target->props;`,
|
|
` size_t sc_target_prop_len = sc_target->prop_len;`,
|
|
` size_t sc_target_prop_cap = sc_target->prop_cap;`,
|
|
` sc_target->len = sc_next->len;`,
|
|
` sc_target->cap = sc_next->cap;`,
|
|
` sc_target->data = sc_next->data;`,
|
|
` sc_target->present = sc_next->present;`,
|
|
` sc_target->sparse = sc_next->sparse;`,
|
|
` sc_target->sparse_len = sc_next->sparse_len;`,
|
|
` sc_target->sparse_cap = sc_next->sparse_cap;`,
|
|
` sc_target->props = sc_next->props;`,
|
|
` sc_target->prop_len = sc_next->prop_len;`,
|
|
` sc_target->prop_cap = sc_next->prop_cap;`,
|
|
` sc_next->len = sc_target_len;`,
|
|
` sc_next->cap = sc_target_cap;`,
|
|
` sc_next->data = sc_target_data;`,
|
|
` sc_next->present = sc_target_present;`,
|
|
` sc_next->sparse = sc_target_sparse;`,
|
|
` sc_next->sparse_len = sc_target_sparse_len;`,
|
|
` sc_next->sparse_cap = sc_target_sparse_cap;`,
|
|
` sc_next->props = sc_target_props;`,
|
|
` sc_next->prop_len = sc_target_prop_len;`,
|
|
` sc_next->prop_cap = sc_target_prop_cap;`,
|
|
` scr_arr_release(sc_next);`,
|
|
`}`,
|
|
``,
|
|
);
|
|
return `&${commit}`;
|
|
}
|
|
if (isDynTypedRefType(t)) {
|
|
const meta = emitter.classMeta.get(t.className);
|
|
if (!meta) {
|
|
throw new InternalCompilerError(`emitter bug: typed-ref commit of unknown class ${t.className}`);
|
|
}
|
|
const commit = `${snapshot}_commit`;
|
|
emitter.walkerProtos.push(
|
|
`static void ${commit}(void *sc_p, const ScrDyn *sc_d); /* commit unknown class ${typeKey(t)} */`,
|
|
);
|
|
defs.push(
|
|
`static void ${commit}(void *sc_p, const ScrDyn *sc_d) {`,
|
|
` ${cDecl(t, "sc_target")} = (${cType(t).trim()})sc_p;`,
|
|
);
|
|
for (const field of meta.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name))) {
|
|
const member = mangleField(field.name);
|
|
const keyLit = cStringLiteral(Buffer.from(field.name, "utf8"));
|
|
const keyLen = Buffer.byteLength(field.name, "utf8");
|
|
defs.push(
|
|
` {`,
|
|
` const ScrDyn *sc_member = scr_dyn_obj_get(sc_d, ${keyLit}, ${keyLen});`,
|
|
` ${cDecl(field.type, "sc_next")} = ${emitter.dynCheckHelper(field.type)}(sc_member ? sc_member : scr_dyn_undefined(), NULL);`,
|
|
` if (scr_exc_pending()) return;`,
|
|
` ${cDecl(field.type, "sc_old")} = sc_target->${member};`,
|
|
` sc_target->${member} = sc_next;`,
|
|
...(isRefCounted(field.type) ? [` ${releaseCallC(field.type, "sc_old")};`] : [" (void)sc_old;"]),
|
|
` }`,
|
|
);
|
|
}
|
|
defs.push(`}`, ``);
|
|
return `&${commit}`;
|
|
}
|
|
if (t.kind !== "record") return "NULL";
|
|
const shape = emitter.recordsById.get(t.shapeId);
|
|
if (!shape) {
|
|
throw new InternalCompilerError(`emitter bug: stream typed-ref commit of unknown shape ${t.shapeId}`);
|
|
}
|
|
const commit = `${snapshot}_commit`;
|
|
emitter.walkerProtos.push(
|
|
`static void ${commit}(void *sc_p, const ScrDyn *sc_d); /* commit live stream element ${typeKey(t)} */`,
|
|
);
|
|
defs.push(
|
|
`static void ${commit}(void *sc_p, const ScrDyn *sc_d) {`,
|
|
` ${cDecl(t, "sc_target")} = (${cType(t).trim()})sc_p;`,
|
|
` ${cDecl(t, "sc_next")} = ${emitter.dynCheckHelper(t)}(sc_d, NULL);`,
|
|
` if (!sc_next) return;`,
|
|
);
|
|
for (const field of shape.fields) {
|
|
const member = mangleField(field.name);
|
|
defs.push(
|
|
` {`,
|
|
` ${cDecl(field.type, "sc_old")} = sc_target->${member};`,
|
|
` sc_target->${member} = sc_next->${member};`,
|
|
` sc_next->${member} = sc_old;`,
|
|
` }`,
|
|
);
|
|
}
|
|
if (shape.indexValue) {
|
|
defs.push(
|
|
` {`,
|
|
` ScrMap *sc_old = sc_target->${OVERFLOW_MEMBER};`,
|
|
` sc_target->${OVERFLOW_MEMBER} = sc_next->${OVERFLOW_MEMBER};`,
|
|
` sc_next->${OVERFLOW_MEMBER} = sc_old;`,
|
|
` }`,
|
|
);
|
|
}
|
|
defs.push(
|
|
` ${releaseCallC(t, "sc_next")};`,
|
|
`}`,
|
|
``,
|
|
);
|
|
return `&${commit}`;
|
|
}
|
|
|
|
export interface StreamTypedRefAdapter {
|
|
snapshot: string;
|
|
commit: string;
|
|
}
|
|
|
|
interface StreamTypedRefContext {
|
|
prefix: string;
|
|
defs: string[];
|
|
adapters: Map<string, StreamTypedRefAdapter>;
|
|
unions?: Map<string, string>;
|
|
}
|
|
|
|
/** Boxes a union selected at runtime, preserving mutable/class arm identity
|
|
* while converting scalar and unit arms normally. It shares the enclosing
|
|
* adapter context so recursive class fields reuse the prototype already
|
|
* registered by streamTypedRefAdapter instead of recursing forever. */
|
|
function nestedTypedRefUnionAdapter(
|
|
emitter: CEmitter,
|
|
t: IrType & { kind: "union" },
|
|
ctx: StreamTypedRefContext,
|
|
): string {
|
|
const key = typeKey(t);
|
|
if (ctx.unions === undefined) ctx.unions = new Map<string, string>();
|
|
const unions = ctx.unions;
|
|
const existing = unions.get(key);
|
|
if (existing) return existing;
|
|
const def = emitter.unionsById.get(t.unionId);
|
|
if (!def) throw new InternalCompilerError(`emitter bug: typed-ref union ${t.unionId} is undeclared`);
|
|
const sym = `${ctx.prefix}_union_${unions.size}`;
|
|
unions.set(key, sym);
|
|
emitter.walkerProtos.push(`static ScrDyn *${sym}(ScrUnion *sc_u); /* typed-ref union ${key} */`);
|
|
const lines = [`static ScrDyn *${sym}(ScrUnion *sc_u) {`, ` switch (sc_u->tag) {`];
|
|
def.arms.forEach((arm, tag) => {
|
|
if (streamTypedRefEligible(arm) || isDynTypedRefType(arm)) {
|
|
const adapter = streamTypedRefAdapter(emitter, arm, ctx);
|
|
const rc = vAdapters(arm);
|
|
const armKey = typeKey(arm);
|
|
const keyLit = cStringLiteral(Buffer.from(armKey, "utf8"));
|
|
lines.push(
|
|
` case ${tag}: return scr_dyn_new_typed_ref(scr_union_peek(sc_u), &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(armKey, "utf8")}, &${adapter.snapshot}, ${adapter.commit});`,
|
|
);
|
|
} else if (arm.kind === "undefinedT") {
|
|
lines.push(` case ${tag}: return scr_dyn_retain(scr_dyn_undefined());`);
|
|
} else if (arm.kind === "nullT") {
|
|
lines.push(` case ${tag}: return scr_dyn_new_null();`);
|
|
} else if (arm.kind === "f64") {
|
|
lines.push(` case ${tag}: return scr_dyn_new_num(scr_union_get_f64(sc_u));`);
|
|
} else if (arm.kind === "bool") {
|
|
lines.push(` case ${tag}: return scr_dyn_new_bool(scr_union_get_bool(sc_u));`);
|
|
} else {
|
|
lines.push(` case ${tag}: return ${emitter.toDynHelper(arm)}((${cType(arm).trim()})scr_union_peek(sc_u));`);
|
|
}
|
|
});
|
|
lines.push(
|
|
` default: scr_trap("scriptc: internal error: invalid union tag\\n");`,
|
|
` }`,
|
|
`}`,
|
|
``,
|
|
);
|
|
ctx.defs.push(...lines);
|
|
return sym;
|
|
}
|
|
|
|
/** Build the live dyn view of one typed stream value. Mutable reference
|
|
* children are capsules of their own, so a write through `v.child.x` or
|
|
* `v.items[0]` commits directly to that child's static source instead of
|
|
* disappearing into the parent's detached snapshot. */
|
|
function streamTypedRefAdapter(
|
|
emitter: CEmitter,
|
|
t: IrType,
|
|
ctx: StreamTypedRefContext,
|
|
preferredSnapshot?: string,
|
|
): StreamTypedRefAdapter {
|
|
const key = typeKey(t);
|
|
const existing = ctx.adapters.get(key);
|
|
if (existing) return existing;
|
|
const snapshot = preferredSnapshot ??
|
|
`${ctx.prefix}_nested_${ctx.adapters.size}`;
|
|
const adapter: StreamTypedRefAdapter = { snapshot, commit: "NULL" };
|
|
/* Register before walking children: recursive record/array types refer
|
|
* back to this prototype without recursively generating helpers. */
|
|
ctx.adapters.set(key, adapter);
|
|
if (isDynTypedRefType(t)) {
|
|
const fields = emitter.classMeta.get(t.className)?.def.fields;
|
|
if (fields && !classDynViewSupported(fields, (id) => emitter.recordsById.get(id), (id) => emitter.unionsById.get(id))) {
|
|
adapter.snapshot = "scr_dyn_class_view_unavailable";
|
|
return adapter;
|
|
}
|
|
}
|
|
emitter.walkerProtos.push(
|
|
`static ScrDyn *${snapshot}(void *sc_p); /* materialize live stream value ${key} */`,
|
|
);
|
|
adapter.commit = streamTypedRefCommitAdapter(emitter, t, snapshot, ctx.defs);
|
|
|
|
const box = (child: IrType, expr: string): string => {
|
|
if (
|
|
child.kind === "union" &&
|
|
(emitter.unionsById.get(child.unionId)?.arms.some(
|
|
(arm) => streamTypedRefEligible(arm) || isDynTypedRefType(arm),
|
|
) ?? false)
|
|
) {
|
|
return `${nestedTypedRefUnionAdapter(emitter, child, ctx)}(${expr})`;
|
|
}
|
|
if (!streamTypedRefEligible(child) && !isDynTypedRefType(child)) {
|
|
return `${emitter.toDynHelper(child)}(${expr})`;
|
|
}
|
|
const nested = streamTypedRefAdapter(emitter, child, ctx);
|
|
const rc = vAdapters(child);
|
|
const childKey = typeKey(child);
|
|
const keyLit = cStringLiteral(Buffer.from(childKey, "utf8"));
|
|
return `scr_dyn_new_typed_ref(${expr}, &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(childKey, "utf8")}, &${nested.snapshot}, ${nested.commit})`;
|
|
};
|
|
|
|
const lines = [
|
|
`static ScrDyn *${snapshot}(void *sc_p) { /* materialize live stream value ${key} */`,
|
|
` ${cDecl(t, "v")} = (${cType(t).trim()})sc_p;`,
|
|
];
|
|
if (isDynTypedRefType(t)) {
|
|
const meta = emitter.classMeta.get(t.className);
|
|
if (!meta) {
|
|
throw new InternalCompilerError(`emitter bug: typed-ref materialize of unknown class ${t.className}`);
|
|
}
|
|
lines.push(` ScrDyn *d = scr_dyn_new_obj();`);
|
|
for (const field of meta.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name))) {
|
|
const keyLit = cStringLiteral(Buffer.from(field.name, "utf8"));
|
|
lines.push(
|
|
` scr_dyn_obj_set(d, ${keyLit}, ${Buffer.byteLength(field.name, "utf8")}, ${box(field.type, `v->${mangleField(field.name)}`)});`,
|
|
);
|
|
}
|
|
lines.push(` return d;`);
|
|
} else if (t.kind === "record") {
|
|
const shape = emitter.recordsById.get(t.shapeId);
|
|
if (!shape) {
|
|
throw new InternalCompilerError(
|
|
`emitter bug: stream typed-ref materialize of unknown shape ${t.shapeId}`,
|
|
);
|
|
}
|
|
const ordinary = shape.indexValue || shape.fields.some(
|
|
(field) => field.name === "handleEvent" && field.type.kind === "func",
|
|
);
|
|
if (ordinary) {
|
|
lines.push(` return ${emitter.toDynHelper(t)}(v);`, `}`, ``);
|
|
ctx.defs.push(...lines);
|
|
return adapter;
|
|
}
|
|
if (shape.tuple) {
|
|
lines.push(` ScrDyn *d = scr_dyn_new_arr();`);
|
|
const fields = [...shape.fields].sort(
|
|
(a, b) => Number(a.name) - Number(b.name),
|
|
);
|
|
for (const field of fields) {
|
|
lines.push(
|
|
` scr_dyn_arr_push(d, ${box(field.type, `v->${mangleField(field.name)}`)});`,
|
|
);
|
|
}
|
|
} else {
|
|
lines.push(` ScrDyn *d = scr_dyn_new_obj();`);
|
|
const byName = new Map(shape.fields.map((field) => [field.name, field]));
|
|
const order = shape.declaredOrder ?? shape.fields.map((field) => field.name);
|
|
const inOrder = new Set(order);
|
|
const fields = [
|
|
...order.map((name) => byName.get(name)).filter((field) => field !== undefined),
|
|
...shape.fields.filter((field) => !inOrder.has(field.name)),
|
|
];
|
|
for (const field of fields) {
|
|
const keyLit = cStringLiteral(Buffer.from(field.name, "utf8"));
|
|
lines.push(
|
|
` scr_dyn_obj_set(d, ${keyLit}, ${Buffer.byteLength(field.name, "utf8")}, ${box(field.type, `v->${mangleField(field.name)}`)});`,
|
|
);
|
|
}
|
|
}
|
|
lines.push(` return d;`);
|
|
} else if (t.kind === "array") {
|
|
const elem = t.elem;
|
|
lines.push(
|
|
` ScrDyn *d = scr_dyn_new_arr();`,
|
|
` for (size_t sc_i = 0; sc_i < v->len; sc_i++) {`,
|
|
);
|
|
if (elem.kind === "f64") {
|
|
lines.push(
|
|
` scr_dyn_arr_push(d, ${box(elem, "scr_arr_get_f64(v, (double)sc_i)")});`,
|
|
);
|
|
} else if (elem.kind === "bool") {
|
|
lines.push(
|
|
` scr_dyn_arr_push(d, ${box(elem, "scr_arr_get_bool(v, (double)sc_i)")});`,
|
|
);
|
|
} else {
|
|
lines.push(
|
|
` ${cDecl(elem, "sc_value")} = (${cType(elem).trim()})scr_arr_get_ref(v, (double)sc_i);`,
|
|
` scr_dyn_arr_push(d, ${box(elem, "sc_value")});`,
|
|
` ${releaseCallC(elem, "sc_value")};`,
|
|
);
|
|
}
|
|
lines.push(` }`, ` return d;`);
|
|
} else {
|
|
lines.push(` return ${emitter.toDynHelper(t)}(v);`);
|
|
}
|
|
lines.push(`}`, ``);
|
|
ctx.defs.push(...lines);
|
|
return adapter;
|
|
}
|
|
|
|
/** One per-type capsule adapter for Web API arguments whose JavaScript
|
|
* contract preserves the exact input reference. */
|
|
export function liveDynRefAdapter(
|
|
emitter: CEmitter,
|
|
t: IrType,
|
|
): StreamTypedRefAdapter {
|
|
const key = typeKey(t);
|
|
const existing = emitter.liveDynRefAdapters.get(key);
|
|
if (existing) return existing;
|
|
if (!streamTypedRefEligible(t) && !isDynTypedRefType(t)) {
|
|
throw new InternalCompilerError(`emitter bug: live dyn ref of ${key}`);
|
|
}
|
|
const prefix = `sc_ldr_${emitter.liveDynRefAdapters.size}`;
|
|
const defs: string[] = [];
|
|
const adapter = streamTypedRefAdapter(
|
|
emitter,
|
|
t,
|
|
{ prefix, defs, adapters: new Map() },
|
|
`${prefix}_materialize`,
|
|
);
|
|
emitter.liveDynRefAdapters.set(key, adapter);
|
|
emitter.walkerDefs.push(...defs);
|
|
return adapter;
|
|
}
|
|
|
|
/** A union itself is only the tagged box; the identity Web APIs expose is
|
|
* the selected mutable arm. Dispatch on the tag and point the typed capsule
|
|
* directly at that arm's payload. Non-mutable arms keep ordinary dynFrom
|
|
* semantics. */
|
|
function liveDynUnionRefAdapter(
|
|
emitter: CEmitter,
|
|
t: IrType & { kind: "union" },
|
|
): string {
|
|
const key = typeKey(t);
|
|
const existing = emitter.liveDynUnionRefAdapters.get(key);
|
|
if (existing) return existing;
|
|
const union = emitter.unionsById.get(t.unionId);
|
|
if (!union) {
|
|
throw new InternalCompilerError(`emitter bug: live dyn ref of unknown union ${t.unionId}`);
|
|
}
|
|
const mutableArms = union.arms
|
|
.map((arm, tag) => ({ arm, tag }))
|
|
.filter(({ arm }) => streamTypedRefEligible(arm) || isDynTypedRefType(arm));
|
|
if (mutableArms.length === 0) {
|
|
throw new InternalCompilerError(`emitter bug: live dyn ref of immutable union ${key}`);
|
|
}
|
|
|
|
const sym = `sc_ldu_${emitter.liveDynUnionRefAdapters.size}`;
|
|
emitter.liveDynUnionRefAdapters.set(key, sym);
|
|
const sig = `static ScrDyn *${sym}(ScrUnion *sc_u)`;
|
|
emitter.walkerProtos.push(`${sig}; /* materialize live union value ${key} */`);
|
|
const defs: string[] = [];
|
|
const adapters = new Map<number, StreamTypedRefAdapter>();
|
|
for (const { arm, tag } of mutableArms) {
|
|
const prefix = `${sym}_${tag}`;
|
|
adapters.set(
|
|
tag,
|
|
streamTypedRefAdapter(
|
|
emitter,
|
|
arm,
|
|
{ prefix, defs, adapters: new Map() },
|
|
`${prefix}_materialize`,
|
|
),
|
|
);
|
|
}
|
|
|
|
defs.push(`${sig} {`, ` switch (sc_u->tag) {`);
|
|
for (const { arm, tag } of mutableArms) {
|
|
const adapter = adapters.get(tag)!;
|
|
const rc = vAdapters(arm);
|
|
const armKey = typeKey(arm);
|
|
const keyLit = cStringLiteral(Buffer.from(armKey, "utf8"));
|
|
defs.push(
|
|
` case ${tag}:`,
|
|
` return scr_dyn_new_typed_ref(scr_union_peek(sc_u), &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(armKey, "utf8")}, &${adapter.snapshot}, ${adapter.commit});`,
|
|
);
|
|
}
|
|
const mutableTags = new Set(mutableArms.map(({ tag }) => tag));
|
|
union.arms.forEach((arm, tag) => {
|
|
if (mutableTags.has(tag)) return;
|
|
if (arm.kind === "undefinedT") {
|
|
defs.push(` case ${tag}: return scr_dyn_retain(scr_dyn_undefined());`);
|
|
} else if (arm.kind === "nullT") {
|
|
defs.push(` case ${tag}: return scr_dyn_new_null();`);
|
|
} else if (arm.kind === "f64") {
|
|
defs.push(` case ${tag}: return scr_dyn_new_num(scr_union_get_f64(sc_u));`);
|
|
} else if (arm.kind === "bool") {
|
|
defs.push(` case ${tag}: return scr_dyn_new_bool(scr_union_get_bool(sc_u));`);
|
|
} else {
|
|
defs.push(` case ${tag}: return ${emitter.toDynHelper(arm)}((${cType(arm).trim()})scr_union_peek(sc_u));`);
|
|
}
|
|
});
|
|
defs.push(
|
|
` default: scr_trap("scriptc: internal error: invalid union tag\\n");`,
|
|
` }`,
|
|
`}`,
|
|
``,
|
|
);
|
|
emitter.walkerDefs.push(...defs);
|
|
return sym;
|
|
}
|
|
|
|
function streamFromArrayAdapter(
|
|
emitter: CEmitter,
|
|
t: IrType & { kind: "array" },
|
|
): string {
|
|
const elem = t.elem;
|
|
const key = typeKey(elem);
|
|
const existing = emitter.streamFromArrayAdapters.get(key);
|
|
if (existing) return existing;
|
|
const sym = `sc_sfa_${emitter.streamFromArrayAdapters.size}`;
|
|
emitter.streamFromArrayAdapters.set(key, sym);
|
|
const sig = `static ScrDyn *${sym}(ScrArr *sc_a, double sc_i)`;
|
|
emitter.walkerProtos.push(`${sig}; /* ReadableStream.from array<${key}> */`);
|
|
const unionDef = elem.kind === "union"
|
|
? emitter.unionsById.get(elem.unionId)
|
|
: undefined;
|
|
if (elem.kind === "union" && !unionDef) {
|
|
throw new InternalCompilerError(`emitter bug: streamFrom of unknown union ${elem.unionId}`);
|
|
}
|
|
const unionRefArms = unionDef?.arms
|
|
.map((arm, tag) => ({ arm, tag }))
|
|
.filter(({ arm }) =>
|
|
isRefCounted(arm) && arm.kind !== "dyn" && arm.kind !== "string"
|
|
) ?? [];
|
|
const typedRef = isRefCounted(elem) &&
|
|
elem.kind !== "dyn" &&
|
|
elem.kind !== "string" &&
|
|
elem.kind !== "union";
|
|
const snapshot = `${sym}_materialize`;
|
|
const d: string[] = [];
|
|
let commit = "NULL";
|
|
if (typedRef) {
|
|
if (streamTypedRefEligible(elem)) {
|
|
const adapter = streamTypedRefAdapter(
|
|
emitter,
|
|
elem,
|
|
{ prefix: snapshot, defs: d, adapters: new Map() },
|
|
snapshot,
|
|
);
|
|
commit = adapter.commit;
|
|
} else {
|
|
const snapshotSig = `static ScrDyn *${snapshot}(void *sc_p)`;
|
|
emitter.walkerProtos.push(`${snapshotSig}; /* materialize stream element ${key} */`);
|
|
d.push(
|
|
`${snapshotSig} {`,
|
|
` return ${emitter.toDynHelper(elem)}((${cType(elem).trim()})sc_p);`,
|
|
`}`,
|
|
``,
|
|
);
|
|
commit = streamTypedRefCommitAdapter(emitter, elem, snapshot, d);
|
|
}
|
|
}
|
|
const unionCommits = new Map<number, string>();
|
|
for (const { arm, tag } of unionRefArms) {
|
|
const armKey = typeKey(arm);
|
|
const armSnapshot = `${snapshot}_${tag}`;
|
|
const snapshotSig = `static ScrDyn *${armSnapshot}(void *sc_p)`;
|
|
emitter.walkerProtos.push(`${snapshotSig}; /* materialize stream union arm ${armKey} */`);
|
|
d.push(
|
|
`${snapshotSig} {`,
|
|
` return ${emitter.toDynHelper(arm)}((${cType(arm).trim()})sc_p);`,
|
|
`}`,
|
|
``,
|
|
);
|
|
unionCommits.set(
|
|
tag,
|
|
streamTypedRefCommitAdapter(emitter, arm, armSnapshot, d),
|
|
);
|
|
}
|
|
d.push(`${sig} { /* ReadableStream.from array<${key}> */`);
|
|
if (elem.kind === "f64") {
|
|
d.push(
|
|
` return ${emitter.toDynHelper(elem)}(scr_arr_get_f64(sc_a, sc_i));`,
|
|
);
|
|
} else if (elem.kind === "bool") {
|
|
d.push(
|
|
` return ${emitter.toDynHelper(elem)}(scr_arr_get_bool(sc_a, sc_i));`,
|
|
);
|
|
} else {
|
|
d.push(` ${cDecl(elem, "sc_v")} = (${cType(elem).trim()})scr_arr_get_ref(sc_a, sc_i);`);
|
|
if (elem.kind === "union") {
|
|
d.push(` ScrDyn *sc_d;`, ` switch (sc_v->tag) {`);
|
|
for (const { arm, tag } of unionRefArms) {
|
|
const armKey = typeKey(arm);
|
|
const rc = vAdapters(arm);
|
|
const keyLit = cStringLiteral(Buffer.from(armKey, "utf8"));
|
|
d.push(
|
|
` case ${tag}:`,
|
|
` sc_d = scr_dyn_new_typed_ref(scr_union_peek(sc_v), &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(armKey, "utf8")}, &${snapshot}_${tag}, ${unionCommits.get(tag) ?? "NULL"});`,
|
|
` break;`,
|
|
);
|
|
}
|
|
d.push(
|
|
` default:`,
|
|
` sc_d = ${emitter.toDynHelper(elem)}(sc_v);`,
|
|
` break;`,
|
|
` }`,
|
|
);
|
|
} else if (typedRef) {
|
|
const rc = vAdapters(elem);
|
|
const keyLit = cStringLiteral(Buffer.from(key, "utf8"));
|
|
const keyLen = Buffer.byteLength(key, "utf8");
|
|
d.push(
|
|
` ScrDyn *sc_d = scr_dyn_new_typed_ref(sc_v, &${rc.retain}, &${rc.release}, ${keyLit}, ${keyLen}, &${snapshot}, ${commit});`,
|
|
);
|
|
} else {
|
|
d.push(` ScrDyn *sc_d = ${emitter.toDynHelper(elem)}(sc_v);`);
|
|
}
|
|
d.push(
|
|
` ${releaseCallC(elem, "sc_v")};`,
|
|
` return sc_d;`,
|
|
);
|
|
}
|
|
d.push(`}`, ``);
|
|
emitter.walkerDefs.push(...d);
|
|
return sym;
|
|
}
|
|
|
|
function dynPromiseAdapter(
|
|
emitter: CEmitter,
|
|
inner: IrType,
|
|
): string {
|
|
if (!isRefCounted(inner) || inner.kind === "dyn") {
|
|
throw new InternalCompilerError(
|
|
`dynamic promise adapter requires a concrete reference type, got ${typeKey(inner)}`,
|
|
);
|
|
}
|
|
const key = typeKey(inner);
|
|
const existing = emitter.dynPromiseAdapters.get(key);
|
|
if (existing) return existing;
|
|
const sym = `sc_dpa_${emitter.dynPromiseAdapters.size}`;
|
|
emitter.dynPromiseAdapters.set(key, sym);
|
|
const sig = `static void ${sym}(ScrPromise *sc_dst, ScrPromise *sc_src)`;
|
|
emitter.walkerProtos.push(`${sig}; /* checked-dynamic promise exit ${key} */`);
|
|
let fulfill: string;
|
|
if (inner.kind === "string") {
|
|
fulfill = `scr_promise_fulfill_str(sc_dst, sc_v);`;
|
|
} else {
|
|
const rc = vAdapters(inner);
|
|
fulfill = `scr_promise_fulfill_ref(sc_dst, sc_v, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(inner)});`;
|
|
}
|
|
emitter.walkerDefs.push(
|
|
`${sig} { /* checked-dynamic promise exit ${key} */`,
|
|
` ScrDyn *sc_d = (ScrDyn *)scr_promise_payload_ref(sc_src);`,
|
|
` ${cDecl(inner, "sc_v")} = ${emitter.dynCheckHelper(inner)}(sc_d, NULL);`,
|
|
` scr_dyn_release(sc_d);`,
|
|
` if (scr_exc_pending()) {`,
|
|
` scr_promise_reject_pending(sc_dst);`,
|
|
` return;`,
|
|
` }`,
|
|
` ${fulfill}`,
|
|
`}`,
|
|
``,
|
|
);
|
|
return sym;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Evaluate a receiver as a borrow when it is a direct, unboxed
|
|
* binding and all later operands are stable. The binding's scope/global
|
|
* owner then keeps the value alive, avoiding retain/release traffic around
|
|
* every indexed access. Any uncertain shape falls back to an owned temp. */
|
|
export function emitStableReceiver(emitter: CEmitter, receiver: IrExpr, following: IrExpr[]): Temp {
|
|
if (
|
|
receiver.kind === "varRef" &&
|
|
following.every((operand) => isStableReceiverOperand(operand, receiver.localId))
|
|
) {
|
|
const local = emitter.currentLocals.get(receiver.localId);
|
|
if (local && !local.boxed) {
|
|
return emitter.newBorrowedTemp(receiver.type, mangleLocal(receiver.localId));
|
|
}
|
|
if (!local && emitter.globalsById.has(receiver.localId)) {
|
|
checkGlobalTdz(emitter, emitter.globalsById.get(receiver.localId)!);
|
|
return emitter.newBorrowedTemp(receiver.type, mangleGlobal(receiver.localId));
|
|
}
|
|
}
|
|
return emitter.emitExpr(receiver);
|
|
}
|
|
|
|
function emitMapLikeIntrinsic(
|
|
emitter: CEmitter,
|
|
e: Extract<IrExpr, { kind: "mapIntrinsic" | "setIntrinsic" }>,
|
|
): Temp {
|
|
const r = emitter.emitExpr(e.receiver);
|
|
const receiverType = e.receiver.type;
|
|
if (e.kind === "mapIntrinsic" && receiverType.kind !== "map") {
|
|
throw new InternalCompilerError("emitter bug: mapIntrinsic on non-map");
|
|
}
|
|
if (e.kind === "setIntrinsic" && receiverType.kind !== "set") {
|
|
throw new InternalCompilerError("emitter bug: setIntrinsic on non-set");
|
|
}
|
|
if (receiverType.kind !== "map" && receiverType.kind !== "set") {
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
const key = receiverType.kind === "map" ? receiverType.key : receiverType.elem;
|
|
const kAcc = mapKeyAccess(key);
|
|
const method = e.method;
|
|
switch (method) {
|
|
case "get": {
|
|
if (receiverType.kind !== "map") throw new InternalCompilerError("unreachable");
|
|
const value = receiverType.value;
|
|
const vAcc = elemAccess(value);
|
|
// The union construction is type-directed HERE, like
|
|
// process.envGet — the runtime knows no tags. Ref values come
|
|
// back +1 (ownership MOVES into the fresh union box on a hit);
|
|
// scalars ride an out-param behind a found flag; a miss is the
|
|
// interned immortal undefined-arm instance. When V is itself a
|
|
// union, the stored box IS the result: `undefined` sorts last
|
|
// in canonical arm order, so V's tags coincide with the result
|
|
// union's and no re-tag exists (validated).
|
|
const k = emitter.emitExpr(e.args[0]!);
|
|
if (value.kind === "dyn") {
|
|
const t = emitter.newTemp(e.type, `(ScrDyn *)scr_map_get_${kAcc}_ref(${r.name}, ${k.name})`);
|
|
emitter.line(`if (!${t.name}) ${t.name} = scr_dyn_undefined();`);
|
|
return t;
|
|
}
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: map get result is not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
if (!def || undefTag < 0) throw new InternalCompilerError("emitter bug: map get union lacks its undefined arm");
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, undefTag);
|
|
if (value.kind === "union") {
|
|
const t = emitter.newTemp(
|
|
e.type,
|
|
`(ScrUnion *)scr_map_get_${kAcc}_ref(${r.name}, ${k.name})`,
|
|
);
|
|
emitter.line(`if (!${t.name}) ${t.name} = ${absent};`);
|
|
return t;
|
|
}
|
|
const valueTag = def.arms.findIndex((a) => typeEquals(a, value));
|
|
if (valueTag < 0) throw new InternalCompilerError("emitter bug: map get union lacks its value arm");
|
|
if (value.kind === "f64" || value.kind === "bool") {
|
|
const out = `sc_t${emitter.tempCounter++}`;
|
|
const found = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(value, out)} = 0;`);
|
|
emitter.line(`bool ${found} = scr_map_get_${kAcc}_${vAcc}(${r.name}, ${k.name}, &${out});`);
|
|
const make = `scr_union_new_${value.kind}(${valueTag}, ${out})`;
|
|
return emitter.newTemp(e.type, `${found} ? ${make} : ${absent}`);
|
|
}
|
|
const rc = vAdapters(value);
|
|
const v = emitter.newTemp(value, `(${cType(value).trim()})scr_map_get_${kAcc}_ref(${r.name}, ${k.name})`);
|
|
emitter.moveTemp(v); // moves into the box when present; NULL otherwise
|
|
const present = `scr_union_new_ref(${valueTag}, ${v.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(value)})`;
|
|
return emitter.newTemp(e.type, `${v.name} ? ${present} : ${absent}`);
|
|
}
|
|
case "set": {
|
|
if (receiverType.kind !== "map") throw new InternalCompilerError("unreachable");
|
|
// Key borrowed (the runtime retains stored string keys); the
|
|
// value MOVES in (replacement releases the old value inside).
|
|
const k = emitter.emitExpr(e.args[0]!);
|
|
const v = emitter.emitExpr(e.args[1]!);
|
|
const vAcc = elemAccess(receiverType.value);
|
|
if (vAcc === "ref") emitter.moveTemp(v);
|
|
emitter.line(`scr_map_set_${kAcc}_${vAcc}(${r.name}, ${k.name}, ${v.name});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "add": {
|
|
if (receiverType.kind !== "set") throw new InternalCompilerError("unreachable");
|
|
// Element borrowed (the runtime retains stored strings); the unit
|
|
// value is 0. Re-adding overwrites in place, preserving insertion.
|
|
const k = emitter.emitExpr(e.args[0]!);
|
|
emitter.line(`scr_map_set_${kAcc}_f64(${r.name}, ${k.name}, 0);${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "has": {
|
|
const k = emitter.emitExpr(e.args[0]!);
|
|
return emitter.newTemp(e.type, `scr_map_has_${kAcc}(${r.name}, ${k.name})`);
|
|
}
|
|
case "delete": {
|
|
const k = emitter.emitExpr(e.args[0]!);
|
|
return emitter.newTemp(e.type, `scr_map_delete_${kAcc}(${r.name}, ${k.name})`);
|
|
}
|
|
case "size":
|
|
return emitter.newTemp(e.type, `scr_map_size(${r.name})`);
|
|
case "clear":
|
|
emitter.line(`scr_map_clear(${r.name});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "iterCount":
|
|
return emitter.newTemp(e.type, `scr_map_iter_count(${r.name})`);
|
|
case "iterLive": {
|
|
const i = emitter.emitExpr(e.args[0]!);
|
|
return emitter.newTemp(e.type, `scr_map_iter_live(${r.name}, ${i.name})`);
|
|
}
|
|
case "iterKey": {
|
|
// String/ref keys and elements come back +1.
|
|
const i = emitter.emitExpr(e.args[0]!);
|
|
return emitter.newTemp(e.type, `scr_map_iter_key_${kAcc}(${r.name}, ${i.name})`);
|
|
}
|
|
case "iterValue": {
|
|
if (receiverType.kind !== "map") throw new InternalCompilerError("unreachable");
|
|
const value = receiverType.value;
|
|
const vAcc = elemAccess(value);
|
|
const i = emitter.emitExpr(e.args[0]!);
|
|
const read =
|
|
vAcc === "ref"
|
|
? `(${cType(value).trim()})scr_map_iter_val_ref(${r.name}, ${i.name})`
|
|
: `scr_map_iter_val_${vAcc}(${r.name}, ${i.name})`;
|
|
return emitter.newTemp(e.type, read);
|
|
}
|
|
case "iterEnter":
|
|
emitter.line(`scr_map_iter_enter(${r.name});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "iterExit":
|
|
emitter.line(`scr_map_iter_exit(${r.name});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "toArray": {
|
|
if (receiverType.kind !== "set") throw new InternalCompilerError("unreachable");
|
|
// Fresh +1 elem[] of the live entries in insertion order.
|
|
return emitter.newTemp(e.type, `scr_set_to_arr_${kAcc}(${r.name})`);
|
|
}
|
|
default: {
|
|
const _exhaustive: never = method;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
type ExprOf<K extends IrExpr["kind"]> = Extract<IrExpr, { kind: K }>;
|
|
|
|
function emitLiteralExpr(
|
|
emitter: CEmitter,
|
|
e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">,
|
|
): Temp {
|
|
switch (e.kind) {
|
|
case "numLit":
|
|
return emitter.newTemp(e.type, cNumberLiteral(e.value));
|
|
case "boolLit":
|
|
return emitter.newTemp(e.type, e.value ? "true" : "false");
|
|
case "strLit": {
|
|
const sym = emitter.internLiteral(e.value);
|
|
return emitter.newTemp(e.type, retainCallC(e.type, `(ScrStr *)&${sym}`));
|
|
}
|
|
case "moduleNsRef": {
|
|
const sym = emitter.internLiteral(`module:${e.moduleId}`);
|
|
return emitter.newTemp(e.type, retainCallC(e.type, `(ScrStr *)&${sym}`));
|
|
}
|
|
case "unitLit":
|
|
// unitLits are consumed inline by the unionWrap case (a unit arm is
|
|
// tag-only); one reaching the generic dispatch escaped its wrap.
|
|
throw new InternalCompilerError(`emitter bug: bare unitLit '${e.unit}'`);
|
|
case "varRef": {
|
|
const integerLoopIndex = emitter.integerLoopIndex(e);
|
|
if (integerLoopIndex !== null) return emitter.newTemp(e.type, `(double)${integerLoopIndex}`);
|
|
const local = emitter.currentLocals.get(e.localId);
|
|
if (!local && emitter.globalsById.has(e.localId)) {
|
|
checkGlobalTdz(emitter, emitter.globalsById.get(e.localId)!);
|
|
const gname = mangleGlobal(e.localId);
|
|
return emitter.newTemp(e.type, isRefCounted(e.type) ? retainCallC(e.type, gname) : gname);
|
|
}
|
|
const name = mangleLocal(e.localId);
|
|
if (local?.boxed) {
|
|
return emitter.newTemp(e.type, readBox(emitter, local));
|
|
}
|
|
return emitter.newTemp(e.type, isRefCounted(e.type) ? retainCallC(e.type, name) : name);
|
|
}
|
|
default: {
|
|
const _exhaustive: never = e;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Unsigned operations avoid signed overflow and implementation-defined
|
|
* right shifts/conversions. Reinterpret the final bits numerically. */
|
|
function emitIntegerBits(emitter: CEmitter, e: ExprOf<"bin" | "unary">, left: string, right?: string): Temp {
|
|
const a = `sc_bits_${emitter.tempCounter++}`;
|
|
const out = `sc_bits_${emitter.tempCounter++}`;
|
|
emitter.line(`uint32_t ${a} = (uint32_t)(int64_t)${left};`);
|
|
let operation: string;
|
|
if (e.kind === "unary") operation = `~${a}`;
|
|
else {
|
|
const b = `sc_bits_${emitter.tempCounter++}`;
|
|
const shift = e.op === "<<" || e.op === ">>" || e.op === ">>>";
|
|
emitter.line(`uint32_t ${b} = (uint32_t)(int64_t)${right}${shift ? " & 31u" : ""};`);
|
|
operation = e.op === ">>" ? `(${a} >> ${b}) | ((${a} & 0x80000000u) ? ~(UINT32_MAX >> ${b}) : 0u)`
|
|
: `${a} ${e.op === ">>>" ? ">>" : e.op} ${b}`;
|
|
}
|
|
emitter.line(`uint32_t ${out} = ${operation};`);
|
|
return emitter.newTemp(e.type, e.op === ">>>" ? `(double)${out}` : `(double)${out} - ((${out} & 0x80000000u) ? 4294967296.0 : 0.0)`);
|
|
}
|
|
|
|
function emitOperatorExpr(
|
|
emitter: CEmitter,
|
|
e: ExprOf<"bin" | "unary" | "incDec" | "fieldIncDec" | "assignExpr" | "seqExpr">,
|
|
): Temp {
|
|
switch (e.kind) {
|
|
case "bin": {
|
|
const l = emitter.emitExpr(e.left);
|
|
const r = emitter.emitExpr(e.right);
|
|
if ((e.op === "+" || e.op === "-") && emitter.integerRanges.get(e)) {
|
|
return emitter.newTemp(e.type, `(double)((int64_t)${l.name} ${e.op} (int64_t)${r.name})`);
|
|
}
|
|
if (["&", "|", "^", "<<", ">>", ">>>"].includes(e.op) && emitter.integerRanges.get(e.left) && emitter.integerRanges.get(e.right)) {
|
|
return emitIntegerBits(emitter, e, l.name, r.name);
|
|
}
|
|
switch (e.op) {
|
|
case "%":
|
|
return emitter.newTemp(e.type, `fmod(${l.name}, ${r.name})`);
|
|
case "**":
|
|
return emitter.newTemp(e.type, `pow(${l.name}, ${r.name})`);
|
|
case "===":
|
|
return emitter.newTemp(e.type, `${l.name} == ${r.name}`);
|
|
case "!==":
|
|
return emitter.newTemp(e.type, `${l.name} != ${r.name}`);
|
|
// The bitwise six ride runtime helpers (ToInt32/ToUint32 wrap,
|
|
// 5-bit shift masks, exact C-portable arithmetic shifts).
|
|
case "&":
|
|
return emitter.newTemp(e.type, `scr_bit_and(${l.name}, ${r.name})`);
|
|
case "|":
|
|
return emitter.newTemp(e.type, `scr_bit_or(${l.name}, ${r.name})`);
|
|
case "^":
|
|
return emitter.newTemp(e.type, `scr_bit_xor(${l.name}, ${r.name})`);
|
|
case "<<":
|
|
return emitter.newTemp(e.type, `scr_bit_shl(${l.name}, ${r.name})`);
|
|
case ">>":
|
|
return emitter.newTemp(e.type, `scr_bit_shr(${l.name}, ${r.name})`);
|
|
case ">>>":
|
|
return emitter.newTemp(e.type, `scr_bit_ushr(${l.name}, ${r.name})`);
|
|
default:
|
|
return emitter.newTemp(e.type, `${l.name} ${e.op} ${r.name}`);
|
|
}
|
|
}
|
|
case "unary": {
|
|
const v = emitter.emitExpr(e.operand);
|
|
if (e.op === "~") return emitter.integerRanges.get(e.operand)
|
|
? emitIntegerBits(emitter, e, v.name) : emitter.newTemp(e.type, `scr_bit_not(${v.name})`);
|
|
return emitter.newTemp(e.type, `${e.op}${v.name}`);
|
|
}
|
|
case "incDec": {
|
|
// Expression-position ++/--: read, write ±1, yield old (postfix)
|
|
// or new (prefix) — the yielded value is snapshotted into its own
|
|
// temp, so later writes to the binding can't disturb it. Receivers
|
|
// are always f64 (frontend fence), so no RC bookkeeping.
|
|
const local = emitter.currentLocals.get(e.localId);
|
|
const one = e.op === "+" ? "+ 1" : "- 1";
|
|
if (local?.boxed) {
|
|
const old = emitter.newTemp(e.type, readBox(emitter, local));
|
|
if (e.prefix) {
|
|
const t = emitter.newTemp(e.type, `${old.name} ${one}`);
|
|
writeBox(emitter, local, t.name);
|
|
return t;
|
|
}
|
|
writeBox(emitter, local, `${old.name} ${one}`);
|
|
return old;
|
|
}
|
|
if (!local && !emitter.globalsById.has(e.localId)) {
|
|
throw new InternalCompilerError(`emitter bug: incDec of unknown binding ${e.localId}`);
|
|
}
|
|
const target = local ? mangleLocal(e.localId) : mangleGlobal(e.localId);
|
|
const t = emitter.newTemp(e.type, e.prefix ? `${target} ${one}` : target);
|
|
emitter.line(`${target} = ${e.prefix ? t.name : `${t.name} ${one}`};`);
|
|
return t;
|
|
}
|
|
case "fieldIncDec": {
|
|
// ++/-- over a class FIELD in expression position: one receiver
|
|
// evaluation, read-modify-write, old/new snapshotted into temps
|
|
// like the local form. f64 fields compute in place; CHECKED-
|
|
// DYNAMIC fields validate the number OUT (dynCheck — the catchable
|
|
// TypeError on non-numbers), compute, and box the result back into
|
|
// the slot. Unlink-then-release like fieldSet: the old box leaves
|
|
// the heap edge before its count is given up (scr_cycle.c).
|
|
const obj = emitter.emitExpr(e.obj);
|
|
const field = `${obj.name}->${mangleField(e.field)}`;
|
|
const one = e.op === "+" ? "+ 1" : "- 1";
|
|
if (!e.fieldDyn) {
|
|
const old = emitter.newTemp(e.type, field);
|
|
if (e.prefix) {
|
|
const t = emitter.newTemp(e.type, `${old.name} ${one}`);
|
|
emitter.line(`${field} = ${t.name};${emitter.srcComment(e.loc)}`);
|
|
return t;
|
|
}
|
|
emitter.line(`${field} = ${old.name} ${one};${emitter.srcComment(e.loc)}`);
|
|
return old;
|
|
}
|
|
const helper = emitter.dynCheckHelper(e.type);
|
|
const old = emitter.fallibleTemp(e.type, `${helper}(${field}, NULL)`);
|
|
const next = emitter.newTemp(e.type, `${old.name} ${one}`);
|
|
const oldBox = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`ScrDyn *${oldBox} = ${field};`);
|
|
emitter.line(`${field} = scr_dyn_new_num(${next.name});${emitter.srcComment(e.loc)}`);
|
|
emitter.releaseValue(oldBox, DYN);
|
|
return e.prefix ? next : old;
|
|
}
|
|
case "assignExpr": {
|
|
// `x = e` in expression position: e evaluates once, the binding takes
|
|
// its OWN reference (retain for ref kinds — the frame keeps the temp's
|
|
// reference for the yielded value, released at statement end as usual),
|
|
// and the temp is the expression's value. Mirrors the `assign`
|
|
// statement's old-value release / boxed-set behavior exactly.
|
|
const local = emitter.currentLocals.get(e.localId);
|
|
const concat = e.value;
|
|
const suffix = matchStringSelfConcat(e.localId, concat);
|
|
if (suffix && concat.kind === "strConcat") {
|
|
// Keep the old left value alive across suffix evaluation, then
|
|
// detach whichever value the binding holds at that point. This is
|
|
// the expression-position twin of stmt assign's ownership handoff.
|
|
const snapshot = emitter.emitExpr(concat.left);
|
|
const right = emitter.emitExpr(suffix);
|
|
if (!local && !emitter.globalsById.has(e.localId)) {
|
|
throw new InternalCompilerError(`emitter bug: assignExpr to unknown binding ${e.localId}`);
|
|
}
|
|
const target = local ? mangleLocal(e.localId) : mangleGlobal(e.localId);
|
|
if (local?.boxed) {
|
|
emitter.line(`scr_box_set_ref(${target}, NULL);`);
|
|
} else {
|
|
const old = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`ScrStr *${old} = ${target};`);
|
|
emitter.line(`${target} = NULL;`);
|
|
emitter.releaseValue(old, snapshot.type);
|
|
}
|
|
const result = emitter.newTemp(e.type, `scr_str_concat(${snapshot.name}, ${right.name})`);
|
|
// An assignment expression yields its own +1, so give the binding
|
|
// a retained sibling reference rather than moving result out.
|
|
const stored = retainCallC(result.type, result.name);
|
|
if (local?.boxed) {
|
|
emitter.line(`scr_box_set_ref(${target}, ${stored});`);
|
|
} else {
|
|
emitter.line(`${target} = ${stored};`);
|
|
}
|
|
return result;
|
|
}
|
|
const v = emitter.emitExpr(e.value);
|
|
if (local?.boxed) {
|
|
// box_set takes ownership of the passed reference, so hand it a
|
|
// retained copy and keep the temp's own reference for the yield.
|
|
const stored = isRefCounted(v.type) ? retainCallC(v.type, v.name) : v.name;
|
|
writeBox(emitter, local, stored);
|
|
return v;
|
|
}
|
|
if (!local && !emitter.globalsById.has(e.localId)) {
|
|
throw new InternalCompilerError(`emitter bug: assignExpr to unknown binding ${e.localId}`);
|
|
}
|
|
if (!local) checkGlobalTdz(emitter, emitter.globalsById.get(e.localId)!);
|
|
const target = local ? mangleLocal(e.localId) : mangleGlobal(e.localId);
|
|
if (isRefCounted(v.type)) {
|
|
// Old-value release is NULL-tolerant for globals (statics start
|
|
// NULL); locals always hold a live value by definite assignment.
|
|
emitter.releaseValue(target, v.type);
|
|
emitter.line(`${target} = ${retainCallC(v.type, v.name)};`);
|
|
} else {
|
|
emitter.line(`${target} = ${v.name};`);
|
|
}
|
|
return v;
|
|
}
|
|
case "seqExpr": {
|
|
// Keep saved operands alive through the enclosing expression:
|
|
// tuple spreads and JS arguments reuse them in later call slots.
|
|
// The current frame still cleans up on the path that created them,
|
|
// including a lazy branch that is skipped on later loop iterations.
|
|
emitter.scopes.push([]);
|
|
for (const s of e.stmts) emitter.emitStmt(s);
|
|
const result = emitter.emitExpr(e.result);
|
|
emitter.currentFrame().push(...emitter.scopes.pop()!);
|
|
return result;
|
|
}
|
|
default: {
|
|
const _exhaustive: never = e;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
function emitControlExpr(
|
|
emitter: CEmitter,
|
|
e: ExprOf<"dynDestrCheck" | "dynIterN" | "toBool" | "logical" | "ternary" | "optChain" | "chainRecv" | "orDefault" | "nullish">,
|
|
): Temp {
|
|
switch (e.kind) {
|
|
case "dynDestrCheck": {
|
|
// RequireObjectCoercible with V8's destructuring TypeError. dyn
|
|
// values check in the runtime helper and pass through unchanged
|
|
// (same temp, same ownership); island values check in the engine
|
|
// (a fresh +1 cell for the same value comes back).
|
|
const v = emitter.emitExpr(e.value);
|
|
const first = e.firstProp !== undefined ? cStringLiteral(Buffer.from(e.firstProp, "utf8")) : "NULL";
|
|
const spell = cStringLiteral(Buffer.from(e.spelling, "utf8"));
|
|
if (e.value.type.kind === "jsval") {
|
|
return emitter.fallibleTemp(e.type, `scr_jsval_destr_check(${v.name}, ${spell}, ${first})`);
|
|
}
|
|
const helper = dynDestrCheckHelper(emitter);
|
|
emitter.line(`${helper}(${v.name}, ${spell}, ${first});${emitter.srcComment(e.loc)}`);
|
|
emitter.emitPendingCheck();
|
|
return v;
|
|
}
|
|
case "dynIterN": {
|
|
// GetIterator + first-N steps as a fresh array (V8's exact
|
|
// not-iterable TypeError on non-iterables): the dyn helper for
|
|
// dyn operands, the engine's real iterator protocol for island
|
|
// ones.
|
|
const v = emitter.emitExpr(e.value);
|
|
if (e.value.type.kind === "jsval") {
|
|
return emitter.fallibleTemp(e.type, `scr_jsval_iter_n(${v.name}, ${e.count})`);
|
|
}
|
|
const helper = dynIterNHelper(emitter);
|
|
return emitter.fallibleTemp(e.type, `${helper}(${v.name}, ${e.count})`);
|
|
}
|
|
case "toBool": {
|
|
// JS ToBoolean. The operand temp (for strings: owned by the current
|
|
// frame) is only read here and released at statement end as usual.
|
|
const v = emitter.emitExpr(e.operand);
|
|
return emitter.newTemp(e.type, emitter.truthyC(v));
|
|
}
|
|
case "logical": {
|
|
// JS value semantics: `a && b` ≡ `toBool(a) ? b : a`, `a || b` ≡
|
|
// `toBool(a) ? a : b` — the result is an operand value, not a bool.
|
|
// The left operand evaluates exactly once and its ownership moves
|
|
// into the result temp; when the branch takes the right operand
|
|
// instead, the stale left value is released first and the right
|
|
// operand runs in its own frame (its leftover temps release inside
|
|
// the branch). Same move/release dance as `ternary`.
|
|
const l = emitter.emitExpr(e.left);
|
|
emitter.moveTemp(l);
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.type, name)} = ${l.name};`);
|
|
const truthy = emitter.truthyC({ name, type: e.type });
|
|
emitter.line(`if (${e.op === "&&" ? truthy : `!(${truthy})`}) {`);
|
|
emitter.indent++;
|
|
if (isRefCounted(e.type)) emitter.releaseValue(name, e.type);
|
|
emitter.emitBranchInto(name, e.right);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type });
|
|
return { name, type: e.type };
|
|
}
|
|
case "ternary": {
|
|
// Exactly one arm evaluates. Each arm runs in its own frame: the
|
|
// chosen value's ownership moves to the result temp, everything
|
|
// else the arm allocated is released inside its branch.
|
|
const c = emitter.emitExpr(e.cond);
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.type, name)};`);
|
|
const emitArm = (arm: IrExpr) => {
|
|
emitter.indent++;
|
|
emitter.emitBranchInto(name, arm);
|
|
emitter.indent--;
|
|
};
|
|
emitter.line(`if (${c.name}) {`);
|
|
emitArm(e.then);
|
|
emitter.line(`} else {`);
|
|
emitArm(e.else_);
|
|
emitter.line(`}`);
|
|
if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type });
|
|
return { name, type: e.type };
|
|
}
|
|
case "optChain": {
|
|
// `a?.b` / `f?.()`: the nullish test inverted. The receiver
|
|
// evaluates once into an ordinary frame temp (borrowed — the frame
|
|
// releases it at statement end). On a unit tag the result is the
|
|
// interned undefined arm and the body never runs (argument side
|
|
// effects included); otherwise the narrowed payload fills the bind
|
|
// temp (+1, frame-owned, NULL-initialized so the unit path's frame
|
|
// release is a no-op) and the body reads it through chainRecv.
|
|
// Island-handle chain: the nullish test asks the engine value; the
|
|
// unit path result is the engine's undefined (+1 cell), the body
|
|
// runs lazily over the bound handle otherwise.
|
|
if (e.receiver.type.kind === "jsval") {
|
|
const r = emitter.emitExpr(e.receiver);
|
|
const bind = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.type, bind)} = NULL;`);
|
|
emitter.currentFrame().push({ name: bind, type: e.type });
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.type, name)};`);
|
|
emitter.line(`if (scr_jsval_is_nullish(${r.name})) {`);
|
|
emitter.indent++;
|
|
emitter.line(`${name} = scr_jsval_undefined();`);
|
|
emitter.indent--;
|
|
emitter.line(`} else {`);
|
|
emitter.indent++;
|
|
emitter.line(`${bind} = scr_jsval_retain(${r.name});`);
|
|
emitter.chainTemps.set(e.id, { name: bind, type: e.type });
|
|
emitter.emitBranchInto(name, e.body);
|
|
emitter.chainTemps.delete(e.id);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
emitter.currentFrame().push({ name, type: e.type });
|
|
return { name, type: e.type };
|
|
}
|
|
// A dyn (dyn) receiver — the `rawName?.match(re)` step: the nullish
|
|
// test reads the node's kind tag; the unit path is the undefined
|
|
// dyn singleton (dyn results) or nothing (void bodies), the body
|
|
// runs over the bound receiver otherwise (the validated dynamic
|
|
// dispatch, its result converted back into the checked-dynamic tree by the
|
|
// frontend's dynFrom wrap).
|
|
if (e.receiver.type.kind === "dyn") {
|
|
const r = emitter.emitExpr(e.receiver);
|
|
const test = `(${r.name}->kind == SCR_DYN_UNDEF || ${r.name}->kind == SCR_DYN_NULL)`;
|
|
const bind = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.receiver.type, bind)} = NULL;`);
|
|
emitter.currentFrame().push({ name: bind, type: e.receiver.type });
|
|
if (e.type.kind === "void") {
|
|
emitter.line(`if (!${test}) {`);
|
|
emitter.indent++;
|
|
emitter.line(`${bind} = scr_dyn_retain(${r.name});`);
|
|
emitter.chainTemps.set(e.id, { name: bind, type: e.receiver.type });
|
|
emitter.frames.push([]);
|
|
emitter.emitExpr(e.body);
|
|
emitter.releaseFrame(emitter.frames.pop()!);
|
|
emitter.chainTemps.delete(e.id);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
if (e.type.kind !== "dyn") throw new InternalCompilerError("emitter bug: dyn optChain result kind");
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.type, name)};`);
|
|
emitter.line(`if (${test}) {`);
|
|
emitter.indent++;
|
|
emitter.line(`${name} = scr_dyn_retain(scr_dyn_undefined());`);
|
|
emitter.indent--;
|
|
emitter.line(`} else {`);
|
|
emitter.indent++;
|
|
emitter.line(`${bind} = scr_dyn_retain(${r.name});`);
|
|
emitter.chainTemps.set(e.id, { name: bind, type: e.receiver.type });
|
|
emitter.emitBranchInto(name, e.body);
|
|
emitter.chainTemps.delete(e.id);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
emitter.currentFrame().push({ name, type: e.type });
|
|
return { name, type: e.type };
|
|
}
|
|
if (e.receiver.type.kind !== "union") throw new InternalCompilerError("emitter bug: optChain receiver is not a union");
|
|
const def = emitter.unionsById.get(e.receiver.type.unionId);
|
|
if (!def) throw new InternalCompilerError(`emitter bug: optChain of unknown union ${e.receiver.type.unionId}`);
|
|
const unitTags: number[] = def.arms.flatMap((a, i): number[] => (isUnitType(a) ? [i] : []));
|
|
const narrowIdx = def.arms.findIndex((a) => !isUnitType(a));
|
|
if (unitTags.length === 0 || narrowIdx < 0) throw new InternalCompilerError("emitter bug: optChain union arms");
|
|
const multiple = def.arms.length - unitTags.length > 1;
|
|
const narrowed = multiple ? e.receiver.type : def.arms[narrowIdx]!;
|
|
const r = emitter.emitExpr(e.receiver);
|
|
const bind = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(narrowed, bind)} = ${isRefCounted(narrowed) ? "NULL" : "0"};`);
|
|
if (isRefCounted(narrowed)) emitter.currentFrame().push({ name: bind, type: narrowed });
|
|
const test = unitTags.map((t) => `${r.name}->tag == ${t}`).join(" || ");
|
|
const extract = multiple
|
|
? retainCallC(narrowed, r.name)
|
|
: narrowed.kind === "f64"
|
|
? `scr_union_get_f64(${r.name})`
|
|
: narrowed.kind === "bool"
|
|
? `scr_union_get_bool(${r.name})`
|
|
: retainCallC(narrowed, `(${cType(narrowed).trim()})scr_union_peek(${r.name})`);
|
|
if (e.type.kind === "void") {
|
|
// Statement form (cb?.()): no result value at all.
|
|
emitter.line(`if (!(${test})) {`);
|
|
emitter.indent++;
|
|
emitter.line(`${bind} = ${extract};`);
|
|
emitter.chainTemps.set(e.id, { name: bind, type: narrowed });
|
|
emitter.frames.push([]);
|
|
emitter.emitExpr(e.body);
|
|
emitter.releaseFrame(emitter.frames.pop()!);
|
|
emitter.chainTemps.delete(e.id);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
// A dyn-typed chain (`pricing?.[key]` over an unknown-valued index
|
|
// signature): the unit path is the undefined dyn value — dyn
|
|
// represents undefined directly, no union wrapper exists.
|
|
if (e.type.kind === "dyn") {
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.type, name)};`);
|
|
emitter.line(`if (${test}) {`);
|
|
emitter.indent++;
|
|
emitter.line(`${name} = scr_dyn_retain(scr_dyn_undefined());`);
|
|
emitter.indent--;
|
|
emitter.line(`} else {`);
|
|
emitter.indent++;
|
|
emitter.line(`${bind} = ${extract};`);
|
|
emitter.chainTemps.set(e.id, { name: bind, type: narrowed });
|
|
emitter.emitBranchInto(name, e.body);
|
|
emitter.chainTemps.delete(e.id);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
emitter.currentFrame().push({ name, type: e.type });
|
|
return { name, type: e.type };
|
|
}
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: optChain result is not a union");
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
if (undefTag < 0) throw new InternalCompilerError("emitter bug: optChain result lacks its undefined arm");
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.type, name)};`);
|
|
emitter.line(`if (${test}) {`);
|
|
emitter.indent++;
|
|
emitter.line(`${name} = ${emitter.unitInstanceRef(e.type.unionId, undefTag)};`);
|
|
emitter.indent--;
|
|
emitter.line(`} else {`);
|
|
emitter.indent++;
|
|
emitter.line(`${bind} = ${extract};`);
|
|
emitter.chainTemps.set(e.id, { name: bind, type: narrowed });
|
|
emitter.emitBranchInto(name, e.body);
|
|
emitter.chainTemps.delete(e.id);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
emitter.currentFrame().push({ name, type: e.type });
|
|
return { name, type: e.type };
|
|
}
|
|
case "chainRecv": {
|
|
const bound = emitter.chainTemps.get(e.id);
|
|
if (!bound) throw new InternalCompilerError(`emitter bug: chainRecv "${e.id}" outside its chain`);
|
|
return emitter.newTemp(
|
|
e.type,
|
|
isRefCounted(e.type) ? retainCallC(e.type, bound.name) : bound.name,
|
|
);
|
|
}
|
|
case "orDefault": {
|
|
// `u || d` narrowed to the single non-unit arm: nullish's dance
|
|
// with the per-union TRUTHY helper as the test — truthy extracts
|
|
// the arm (+1 for ref kinds), falsy releases and runs the default.
|
|
if (e.left.type.kind !== "union") throw new InternalCompilerError("emitter bug: orDefault left is not a union");
|
|
const l = emitter.emitExpr(e.left);
|
|
emitter.moveTemp(l);
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.type, name)};`);
|
|
emitter.line(`if (${emitter.unionTruthyHelper(e.left.type.unionId)}(${l.name})) {`);
|
|
emitter.indent++;
|
|
if (e.retag !== undefined) {
|
|
// The helper CONSUMES the left box (callees own their params), so
|
|
// no release on this side — the falsy side still owns and frees it.
|
|
// An unwind here produces the NULL dummy and never reaches the
|
|
// frame push below, so the slot needs no early registration.
|
|
emitter.line(`${name} = ${emitter.callTargetC(e.retag)}(${l.name});`);
|
|
if (emitter.mayThrow.has(e.retag)) emitter.emitPendingCheck();
|
|
} else {
|
|
const arm = e.type;
|
|
const read =
|
|
arm.kind === "f64"
|
|
? `scr_union_get_f64(${l.name})`
|
|
: arm.kind === "bool"
|
|
? `scr_union_get_bool(${l.name})`
|
|
: retainCallC(arm, `(${cType(arm).trim()})scr_union_peek(${l.name})`);
|
|
emitter.line(`${name} = ${read};`);
|
|
emitter.releaseValue(l.name, e.left.type);
|
|
}
|
|
emitter.indent--;
|
|
emitter.line(`} else {`);
|
|
emitter.indent++;
|
|
emitter.releaseValue(l.name, e.left.type);
|
|
emitter.emitBranchInto(name, e.right);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type });
|
|
return { name, type: e.type };
|
|
}
|
|
case "nullish": {
|
|
// `a ?? b`: logical's move/release dance with the left's runtime
|
|
// TAG against its unit arms as the test. The left evaluates exactly
|
|
// once (ownership moves out of the frame, manually managed below);
|
|
// the right runs lazily in its own branch. Pass-through shape: the
|
|
// result IS the left box. Narrowed shape: the single non-unit arm's
|
|
// payload is extracted unionNarrow-style (+1 for ref kinds, the
|
|
// checker proved the tag) and the left box is released.
|
|
if (e.left.type.kind === "jsval") {
|
|
// `a ?? b` on an island value: the engine's nullish test; the
|
|
// right runs lazily in its branch (already jsval-typed).
|
|
const l = emitter.emitExpr(e.left);
|
|
emitter.moveTemp(l);
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.type, name)};`);
|
|
emitter.line(`if (scr_jsval_is_nullish(${l.name})) {`);
|
|
emitter.indent++;
|
|
emitter.releaseValue(l.name, e.left.type);
|
|
emitter.emitBranchInto(name, e.right);
|
|
emitter.indent--;
|
|
emitter.line(`} else {`);
|
|
emitter.indent++;
|
|
emitter.line(`${name} = ${l.name};`);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type });
|
|
return { name, type: e.type };
|
|
}
|
|
if (e.left.type.kind === "dyn") {
|
|
// `a ?? b` on a checked-dynamic left: the runtime kind decides
|
|
// (UNDEF/NULL take the default; a wrapped island value asks the
|
|
// engine); the right runs lazily in its branch (already dyn).
|
|
const l = emitter.emitExpr(e.left);
|
|
emitter.moveTemp(l);
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.type, name)};`);
|
|
emitter.line(`if (scr_dyn_is_nullish(${l.name})) {`);
|
|
emitter.indent++;
|
|
emitter.releaseValue(l.name, e.left.type);
|
|
emitter.emitBranchInto(name, e.right);
|
|
emitter.indent--;
|
|
emitter.line(`} else {`);
|
|
emitter.indent++;
|
|
emitter.line(`${name} = ${l.name};`);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type });
|
|
return { name, type: e.type };
|
|
}
|
|
if (e.left.type.kind !== "union") throw new InternalCompilerError("emitter bug: nullish left is not a union");
|
|
const def = emitter.unionsById.get(e.left.type.unionId);
|
|
if (!def) throw new InternalCompilerError(`emitter bug: nullish of unknown union ${e.left.type.unionId}`);
|
|
const unitTags: number[] = def.arms.flatMap((a, i): number[] => (isUnitType(a) ? [i] : []));
|
|
if (unitTags.length === 0) throw new InternalCompilerError("emitter bug: nullish union lacks unit arms");
|
|
const l = emitter.emitExpr(e.left);
|
|
emitter.moveTemp(l);
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.type, name)};`);
|
|
const test = unitTags.map((t) => `${l.name}->tag == ${t}`).join(" || ");
|
|
emitter.line(`if (${test}) {`);
|
|
emitter.indent++;
|
|
emitter.releaseValue(l.name, e.left.type);
|
|
emitter.emitBranchInto(name, e.right);
|
|
emitter.indent--;
|
|
emitter.line(`} else {`);
|
|
emitter.indent++;
|
|
if (typeEquals(e.type, e.left.type)) {
|
|
emitter.line(`${name} = ${l.name};`);
|
|
} else {
|
|
const arm = e.type;
|
|
const read =
|
|
arm.kind === "f64"
|
|
? `scr_union_get_f64(${l.name})`
|
|
: arm.kind === "bool"
|
|
? `scr_union_get_bool(${l.name})`
|
|
: retainCallC(arm, `(${cType(arm).trim()})scr_union_peek(${l.name})`);
|
|
emitter.line(`${name} = ${read};`);
|
|
emitter.releaseValue(l.name, e.left.type);
|
|
}
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type });
|
|
return { name, type: e.type };
|
|
}
|
|
default: {
|
|
const _exhaustive: never = e;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
function emitStringExpr(
|
|
emitter: CEmitter,
|
|
e: ExprOf<"strConcat" | "strEq" | "strCmp" | "toString" | "strIntrinsic" | "regexLit" | "templateStrings" | "regexIntrinsic">,
|
|
): Temp {
|
|
switch (e.kind) {
|
|
case "strConcat": {
|
|
const l = emitter.emitExpr(e.left);
|
|
const r = emitter.emitExpr(e.right);
|
|
return emitter.newTemp(e.type, `scr_str_concat(${l.name}, ${r.name})`);
|
|
}
|
|
case "strEq": {
|
|
const l = emitter.emitExpr(e.left);
|
|
const r = emitter.emitExpr(e.right);
|
|
return emitter.newTemp(e.type, `${e.negated ? "!" : ""}scr_str_eq(${l.name}, ${r.name})`);
|
|
}
|
|
case "strCmp": {
|
|
const l = emitter.emitExpr(e.left);
|
|
const r = emitter.emitExpr(e.right);
|
|
const fn = e.utf16 === true ? "scr_str_cmp_u16" : "scr_str_cmp";
|
|
return emitter.newTemp(e.type, `${fn}(${l.name}, ${r.name}) ${e.op} 0`);
|
|
}
|
|
case "toString": {
|
|
const v = emitter.emitExpr(e.operand);
|
|
if (v.type.kind === "union") {
|
|
// The ARM value's ToString via the per-union interned helper
|
|
// (unit arms are interned literals, string arms retain the
|
|
// payload, f64/bool arms format). Box borrowed; result +1.
|
|
return emitter.fallibleTemp(e.type, `${emitter.unionToStrHelper(v.type.unionId)}(${v.name})`);
|
|
}
|
|
if (v.type.kind === "caught") {
|
|
// String(e) over the exception snapshot. Box borrowed; result +1.
|
|
return emitter.fallibleTemp(e.type, `scr_caught_to_string(${v.name})`);
|
|
}
|
|
if (v.type.kind === "dyn") {
|
|
// String(unknown): dispatch over the dyn kind (dynToStrHelper —
|
|
// Node's String() incl. arrays-join and "[object Object]").
|
|
return emitter.fallibleTemp(e.type, `${emitter.dynToStrHelper()}(${v.name})`);
|
|
}
|
|
if (v.type.kind === "record") {
|
|
// String(record) / `${record}`: Object.prototype.toString's
|
|
// constant. The operand temp was emitted above (effects and RC
|
|
// ride the frame like any other statement temp); the result is
|
|
// the interned literal, retained like a strLit.
|
|
const sym = emitter.internLiteral("[object Object]");
|
|
return emitter.newTemp(e.type, retainCallC(e.type, `(ScrStr *)&${sym}`));
|
|
}
|
|
const fn = v.type.kind === "bool" ? "scr_bool_to_scrstr" : "scr_f64_to_scrstr";
|
|
return emitter.newTemp(e.type, `${fn}(${v.name})`);
|
|
}
|
|
case "strIntrinsic": {
|
|
// Receiver and string arguments are owned temps in the current
|
|
// frame; every scr_str_* method BORROWS them (frame release at
|
|
// statement end still applies). String-returning methods hand back
|
|
// a +1 reference, which newTemp registers in the frame like any
|
|
// other owned string temp. Omitted optional args get the C-side
|
|
// defaults from docs/ir.md: indexOf position 0, slice start 0,
|
|
// slice end INFINITY (math.h is always included).
|
|
const r = emitter.emitExpr(e.receiver);
|
|
const args = e.args.map((a) => emitter.emitExpr(a));
|
|
const method = e.method;
|
|
switch (method) {
|
|
case "length":
|
|
return emitter.newTemp(e.type, `scr_str_utf16_len(${r.name})`);
|
|
case "charCodeAt":
|
|
return emitter.newTemp(e.type, `scr_str_char_code_at(${r.name}, ${args[0]!.name})`);
|
|
case "charAt":
|
|
return emitter.newTemp(e.type, `scr_str_char_at(${r.name}, ${args[0]!.name})`);
|
|
case "indexOf":
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_str_index_of(${r.name}, ${args[0]!.name}, ${args[1]?.name ?? "0"})`,
|
|
);
|
|
case "includes":
|
|
// The position form is indexOf's clamp exactly (the spec
|
|
// routes both through StringIndexOf): found ⇔ index != -1.
|
|
if (args[1]) {
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_str_index_of(${r.name}, ${args[0]!.name}, ${args[1].name}) != -1`,
|
|
);
|
|
}
|
|
return emitter.newTemp(e.type, `scr_str_includes(${r.name}, ${args[0]!.name})`);
|
|
case "startsWith":
|
|
return emitter.newTemp(e.type, args[1]
|
|
? `scr_str_starts_with_from(${r.name}, ${args[0]!.name}, ${args[1].name})`
|
|
: `scr_str_starts_with(${r.name}, ${args[0]!.name})`);
|
|
case "endsWith":
|
|
return emitter.newTemp(e.type, args[1]
|
|
? `scr_str_ends_with_from(${r.name}, ${args[0]!.name}, ${args[1].name})`
|
|
: `scr_str_ends_with(${r.name}, ${args[0]!.name})`);
|
|
case "slice":
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_str_slice(${r.name}, ${args[0]?.name ?? "0"}, ${args[1]?.name ?? "INFINITY"})`,
|
|
);
|
|
case "substring":
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_str_substring(${r.name}, ${args[0]!.name}, ${args[1]?.name ?? "INFINITY"})`,
|
|
);
|
|
case "repeat":
|
|
return emitter.newTemp(e.type, `scr_str_repeat(${r.name}, ${args[0]!.name})`);
|
|
case "trim":
|
|
return emitter.newTemp(e.type, `scr_str_trim(${r.name})`);
|
|
case "trimStart":
|
|
return emitter.newTemp(e.type, `scr_str_trim_start(${r.name})`);
|
|
case "trimEnd":
|
|
return emitter.newTemp(e.type, `scr_str_trim_end(${r.name})`);
|
|
case "split":
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_str_split_limit(${r.name}, ${args[0]!.name}, ${args[1]!.name})`,
|
|
);
|
|
case "padStart":
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_str_pad_start(${r.name}, ${args[0]!.name}, ${args[1]!.name})`,
|
|
);
|
|
case "padEnd":
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_str_pad_end(${r.name}, ${args[0]!.name}, ${args[1]!.name})`,
|
|
);
|
|
// The lre-backed pair (scr_regex.c): the IR's presence flips the
|
|
// regex link switch, so the symbols always resolve.
|
|
case "toLowerCase":
|
|
return emitter.newTemp(e.type, `scr_str_to_lower(${r.name})`);
|
|
case "toUpperCase":
|
|
return emitter.newTemp(e.type, `scr_str_to_upper(${r.name})`);
|
|
// The well-formedness pair: no-ops over well-formed storage
|
|
// (constant true / retained identity; scr_string.c).
|
|
case "isWellFormed":
|
|
return emitter.newTemp(e.type, `scr_str_is_well_formed(${r.name})`);
|
|
case "toWellFormed":
|
|
return emitter.newTemp(e.type, `scr_str_to_well_formed(${r.name})`);
|
|
case "cpAt":
|
|
return emitter.newTemp(e.type, `scr_str_cp_at(${r.name}, ${args[0]!.name})`);
|
|
default: {
|
|
const _exhaustive: never = method;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
case "regexLit": {
|
|
// One immortal static per (pattern, flags) pair; the +1 retain is a
|
|
// no-op on immortals but keeps the owned-temps discipline uniform.
|
|
// Pattern/flags strings are interned NOW so the literal table is
|
|
// complete when emit() assembles the file.
|
|
const key = `${e.flags}/${e.pattern}`;
|
|
let sym = emitter.regexInstances.get(key);
|
|
if (!sym) {
|
|
sym = `sc_re_${emitter.regexInstances.size}`;
|
|
emitter.regexInstances.set(key, sym);
|
|
emitter.internLiteral(e.pattern);
|
|
emitter.internLiteral(e.flags);
|
|
}
|
|
return emitter.newTemp(e.type, `scr_regex_retain(&${sym})`);
|
|
}
|
|
case "templateStrings": {
|
|
// One immortal static string array per template SITE (the key);
|
|
// the +1 retain is a no-op on immortals but keeps the owned-temps
|
|
// discipline uniform. Cooked strings intern NOW so the literal
|
|
// table is complete when emit() assembles the file.
|
|
let inst = emitter.templateStringsInstances.get(e.key);
|
|
if (!inst) {
|
|
inst = { sym: `sc_tsa_${emitter.templateStringsInstances.size}`, cooked: e.cooked };
|
|
emitter.templateStringsInstances.set(e.key, inst);
|
|
for (const s of e.cooked) emitter.internLiteral(s);
|
|
}
|
|
return emitter.newTemp(e.type, `scr_arr_retain(&${inst.sym})`);
|
|
}
|
|
case "regexIntrinsic": {
|
|
// Receiver and args are borrowed frame temps; string/array results
|
|
// come back +1 and join the frame via newTemp. replaceAll and split
|
|
// may THROW (catchable) — fallibleTemp emits the pending check.
|
|
const r = emitter.emitExpr(e.receiver);
|
|
const args = e.args.map((a) => emitter.emitExpr(a));
|
|
const method = e.method;
|
|
switch (method) {
|
|
case "test":
|
|
return emitter.newTemp(e.type, `scr_regex_test(${r.name}, ${args[0]!.name})`);
|
|
case "match": {
|
|
// +1 string[] or NULL from the runtime; the `string[] | null`
|
|
// union wraps type-directedly, the process.envGet convention.
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: match result not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const arrTag = def ? def.arms.findIndex((a) => a.kind === "array") : -1;
|
|
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
|
if (arrTag < 0 || nullTag < 0) {
|
|
throw new InternalCompilerError("emitter bug: match union lacks its arms");
|
|
}
|
|
const m = emitter.newTemp(arrayOf(STRING), `scr_regex_match(${r.name}, ${args[0]!.name})`);
|
|
emitter.moveTemp(m); // moves into the union box when present; NULL otherwise
|
|
const present = `scr_union_new_ref(${arrTag}, ${m.name}, &scr_arr_retain_v, &scr_arr_release_v, NULL)`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, nullTag);
|
|
return emitter.newTemp(e.type, `${m.name} ? ${present} : ${absent}`);
|
|
}
|
|
case "matchAll":
|
|
// Every match drained eagerly into a fresh +1 string[][];
|
|
// throws Node's TypeError on a non-global regex (catchable —
|
|
// fallibleTemp's pending check).
|
|
return emitter.fallibleTemp(e.type, `scr_regex_match_all(${r.name}, ${args[0]!.name})`);
|
|
case "matchAllInto":
|
|
// matchAll's companion-index form: args[1] (a number[]) also
|
|
// receives each match's UTF-16 start index.
|
|
return emitter.fallibleTemp(
|
|
e.type,
|
|
`scr_regex_match_all_into(${r.name}, ${args[0]!.name}, ${args[1]!.name})`,
|
|
);
|
|
case "search":
|
|
// First-match index or -1 — a plain double; never throws.
|
|
return emitter.newTemp(e.type, `scr_regex_search(${r.name}, ${args[0]!.name})`);
|
|
case "source":
|
|
return emitter.newTemp(e.type, `scr_regex_source(${r.name})`);
|
|
case "flags":
|
|
return emitter.newTemp(e.type, `scr_regex_flags(${r.name})`);
|
|
case "replace":
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_regex_replace(${r.name}, ${args[0]!.name}, ${args[1]!.name})`,
|
|
);
|
|
case "replaceAll":
|
|
return emitter.fallibleTemp(
|
|
e.type,
|
|
`scr_regex_replace_all(${r.name}, ${args[0]!.name}, ${args[1]!.name})`,
|
|
);
|
|
case "split":
|
|
return emitter.fallibleTemp(
|
|
e.type,
|
|
`scr_regex_split_limit(${r.name}, ${args[0]!.name}, ${args[1]!.name})`,
|
|
);
|
|
default: {
|
|
const _exhaustive: never = method;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
default: {
|
|
const _exhaustive: never = e;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
function emitContainerExpr(
|
|
emitter: CEmitter,
|
|
e: ExprOf<"arrayLit" | "arrayNewLen" | "arrayGet" | "arrayHas" | "arrayState" | "arrIntrinsic" | "bytesNew" | "bytesIntrinsic" | "mapNew" | "mapIntrinsic" | "setIntrinsic" | "setNew">,
|
|
): Temp {
|
|
switch (e.kind) {
|
|
case "arrayLit": {
|
|
// Allocate, then push each element in order. Ownership of refcounted
|
|
// plain elements moves into the array (interned literals are
|
|
// immortal, so the array's eventual release of them is a no-op).
|
|
// SPREAD positions hold a same-typed source array (borrowed): its
|
|
// elements copy in — _get_ref returns +1 and _push_ref takes
|
|
// ownership, so the copy loop is RC-balanced; the length is
|
|
// snapshotted before the loop.
|
|
if (e.type.kind !== "array") throw new InternalCompilerError("emitter bug: arrayLit of non-array type");
|
|
const elem = e.type.elem;
|
|
const arr = emitter.newTemp(e.type, emitter.arrNewC(elem, e.elems.length));
|
|
const acc = elemAccess(elem);
|
|
const spreadSet = new Set(e.spreads ?? []);
|
|
e.elems.forEach((el, i) => {
|
|
const v = emitter.emitExpr(el);
|
|
if (spreadSet.has(i)) {
|
|
emitter.line(`scr_arr_push_spread(${arr.name}, ${v.name});`);
|
|
return;
|
|
}
|
|
if (acc === "ref") emitter.moveTemp(v);
|
|
emitter.line(`scr_arr_push_${acc}(${arr.name}, ${v.name});`);
|
|
});
|
|
return arr;
|
|
}
|
|
case "arrayNewLen": {
|
|
// Mapper-less Array.from({ length: n }): a length-n array of
|
|
// ABSENT slots — the interned undefined arm for unions carrying
|
|
// one (immortal: pushing owes no retain), NULL for every other
|
|
// ref element kind (assign before reading — SEMANTICS.md 46). The
|
|
// `i <= n - 1` bound is ToLength for the lengths that terminate:
|
|
// fractions truncate, negative/NaN produce an empty array.
|
|
if (e.type.kind !== "array") throw new InternalCompilerError("emitter bug: arrayNewLen of non-array type");
|
|
const elem = e.type.elem;
|
|
const n = emitter.emitExpr(e.length);
|
|
const arr = emitter.newTemp(e.type, emitter.arrNewC(elem, 0));
|
|
let fill = "NULL";
|
|
if (elem.kind === "union") {
|
|
const def = emitter.unionsById.get(elem.unionId);
|
|
const tag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1;
|
|
if (tag >= 0) fill = emitter.unitInstanceRef(elem.unionId, tag);
|
|
}
|
|
const i = `sc_i${emitter.tempCounter++}`;
|
|
emitter.line(`for (double ${i} = 0; ${i} <= ${n.name} - 1; ${i} += 1) {`);
|
|
emitter.indent++;
|
|
emitter.line(`scr_arr_push_${elemAccess(elem)}(${arr.name}, ${fill});`);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
return arr;
|
|
}
|
|
case "arrayGet": {
|
|
const arr = emitter.emitExpr(e.arr);
|
|
const idx = emitter.emitExpr(e.index);
|
|
if (e.arr.type.kind !== "array") throw new InternalCompilerError("emitter bug: arrayGet on non-array");
|
|
// Ref-element reads return +1 (the runtime retains); newTemp
|
|
// registers the owned temp in the frame like any other.
|
|
const acc = elemAccess(e.arr.type.elem);
|
|
return emitter.newTemp(e.type, `scr_arr_get_${acc}(${arr.name}, ${idx.name})`);
|
|
}
|
|
case "arrayHas": {
|
|
const arr = emitter.emitExpr(e.arr);
|
|
const idx = emitter.emitExpr(e.index);
|
|
if (e.arr.type.kind !== "array") throw new InternalCompilerError("emitter bug: arrayHas on non-array");
|
|
return emitter.newTemp(e.type, `scr_arr_has(${arr.name}, ${idx.name})`);
|
|
}
|
|
case "arrayState": {
|
|
const arr = emitter.emitExpr(e.arr);
|
|
const idx = emitter.emitExpr(e.index);
|
|
if (e.arr.type.kind !== "array") throw new InternalCompilerError("emitter bug: arrayState on non-array");
|
|
return emitter.newTemp(e.type, `scr_arr_state(${arr.name}, ${idx.name})`);
|
|
}
|
|
case "arrIntrinsic": {
|
|
// getNumber/indexEq produce scalars without invoking user code. A
|
|
// stable binding can own its receiver until that lookup finishes.
|
|
const r = e.method === "getNumber" || e.method === "indexEq"
|
|
? emitStableReceiver(emitter, e.receiver, e.args)
|
|
: emitter.emitExpr(e.receiver);
|
|
if (e.receiver.type.kind !== "array") throw new InternalCompilerError("emitter bug: arrIntrinsic on non-array");
|
|
const acc = elemAccess(e.receiver.type.elem);
|
|
const method = e.method;
|
|
switch (method) {
|
|
case "length":
|
|
return emitter.newTemp(e.type, `scr_arr_len(${r.name})`);
|
|
case "getNumber": {
|
|
const index = emitter.emitExpr(e.args[0]!);
|
|
const table = e.receiver.kind === "varRef" ? emitter.constantNumericTables.get(e.receiver.localId) : undefined;
|
|
return emitter.newTemp(e.type, `${table ? `${table.symbol}_get` : "scr_arr_get_number"}(${r.name}, ${index.name})`);
|
|
}
|
|
case "nextPresent": {
|
|
const start = emitter.emitExpr(e.args[0]!);
|
|
return emitter.newTemp(e.type, `scr_arr_next_present(${r.name}, ${start.name})`);
|
|
}
|
|
case "indexEq": {
|
|
const index = emitter.emitExpr(e.args[0]!);
|
|
const other = emitStableReceiver(emitter, e.args[1]!, [e.args[2]!]);
|
|
const otherIndex = emitter.emitExpr(e.args[2]!);
|
|
return emitter.newTemp(e.type, `scr_arr_index_eq(${r.name}, ${index.name}, ${other.name}, ${otherIndex.name})`);
|
|
}
|
|
case "push": {
|
|
// Variadic like JS: every argument evaluates first (left to
|
|
// right — an argument reading the array sees the pre-push
|
|
// state), then each appends in order. Ownership of refcounted
|
|
// arguments moves into the array; the result is the new length
|
|
// (f64) — the last push's return, or the unchanged length for
|
|
// Node's no-op zero-argument call.
|
|
const vs = e.args.map((a) => emitter.emitExpr(a));
|
|
if (acc === "ref") vs.forEach((v) => emitter.moveTemp(v));
|
|
for (let i = 0; i < vs.length - 1; i++) {
|
|
emitter.line(`scr_arr_push_${acc}(${r.name}, ${vs[i]!.name});`);
|
|
}
|
|
const last = vs[vs.length - 1];
|
|
return last
|
|
? emitter.newTemp(e.type, `scr_arr_push_${acc}(${r.name}, ${last.name})`)
|
|
: emitter.newTemp(e.type, `scr_arr_len(${r.name})`);
|
|
}
|
|
case "pushSpread": {
|
|
// `a.push(...src)`: append src's indexed state in order while
|
|
// preserving holes; the runtime snapshots self-spread and
|
|
// retains copied refs. Result: the new length.
|
|
const src = emitter.emitExpr(e.args[0]!);
|
|
return emitter.newTemp(e.type, `scr_arr_push_spread(${r.name}, ${src.name})`);
|
|
}
|
|
case "concatSpread": {
|
|
const src = emitter.emitExpr(e.args[0]!);
|
|
return emitter.newTemp(e.type, `scr_arr_concat_copy(${r.name}, ${src.name})`);
|
|
}
|
|
case "unshift": {
|
|
// Like push, every argument evaluates before mutation. Apply
|
|
// them from right to left so their final front order remains
|
|
// the source order. Ref ownership moves into the array.
|
|
const vs = e.args.map((a) => emitter.emitExpr(a));
|
|
if (acc === "ref") vs.forEach((v) => emitter.moveTemp(v));
|
|
let last: Temp | undefined;
|
|
for (let i = vs.length - 1; i >= 0; i--) {
|
|
last = emitter.newTemp(e.type, `scr_arr_unshift_${acc}(${r.name}, ${vs[i]!.name})`);
|
|
}
|
|
return last ?? emitter.newTemp(e.type, `scr_arr_len(${r.name})`);
|
|
}
|
|
case "unshiftSpread": {
|
|
// The runtime snapshots and retains the borrowed source,
|
|
// including the aliasing `a.unshift(...a)` case.
|
|
const src = emitter.emitExpr(e.args[0]!);
|
|
return emitter.newTemp(e.type, `scr_arr_unshift_spread(${r.name}, ${src.name})`);
|
|
}
|
|
case "pop":
|
|
case "shift": {
|
|
// Move the slot once, preserving undefined and holes as values
|
|
// while still applying the method's length/index mutation.
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: array removal result is not a union");
|
|
const elemT = e.receiver.type.elem;
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const tag = def ? def.arms.findIndex((arm) => typeEquals(arm, elemT)) : -1;
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
const sameUnion = elemT.kind === "union" && typeEquals(elemT, e.type);
|
|
if ((!sameUnion && tag < 0) || undefTag < 0) throw new InternalCompilerError("emitter bug: array removal union lacks its arms");
|
|
const state = `sc_remove_state${emitter.tempCounter++}`;
|
|
const slot = `sc_remove_slot${emitter.tempCounter++}`;
|
|
emitter.line(`uint64_t ${slot} = 0;`);
|
|
emitter.line(`uint8_t ${state} = scr_arr_${method}_state(${r.name}, &${slot});`);
|
|
let present: string;
|
|
if (elemT.kind === "f64") {
|
|
const number = `sc_remove_number${emitter.tempCounter++}`;
|
|
emitter.line(`double ${number}; memcpy(&${number}, &${slot}, sizeof ${number});`);
|
|
present = `scr_union_new_f64(${tag}, ${number})`;
|
|
} else if (elemT.kind === "bool") {
|
|
present = `scr_union_new_bool(${tag}, ${slot} != 0)`;
|
|
} else {
|
|
const ref = `sc_remove_ref${emitter.tempCounter++}`;
|
|
emitter.line(`void *${ref}; memcpy(&${ref}, &${slot}, sizeof ${ref});`);
|
|
const rc = vAdapters(elemT);
|
|
present = sameUnion ? `(ScrUnion *)${ref}` : `scr_union_new_ref(${tag}, ${ref}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(elemT)})`;
|
|
}
|
|
return emitter.newTemp(e.type, `${state} == SCR_ARR_VALUE ? ${present} : ${emitter.unitInstanceRef(e.type.unionId, undefTag)}`);
|
|
}
|
|
case "indexOf": {
|
|
// The needle is BORROWED (released with this statement's frame);
|
|
// the ref variant dispatches on the array's element kind
|
|
// (strings by content, arrays by pointer). Strict equality.
|
|
const v = emitter.emitExpr(e.args[0]!);
|
|
return emitter.newTemp(e.type, `scr_arr_index_of_${acc}(${r.name}, ${v.name})`);
|
|
}
|
|
case "includes": {
|
|
// Borrowed needle, SameValueZero (NaN matches NaN).
|
|
const v = emitter.emitExpr(e.args[0]!);
|
|
return emitter.newTemp(e.type, `scr_arr_includes_${acc}(${r.name}, ${v.name})`);
|
|
}
|
|
case "join": {
|
|
// Separator borrowed; the result is an owned (+1) string.
|
|
// Union elements route through the per-union join walker
|
|
// (nullish arms print empty, exactly Array.prototype.join).
|
|
const sep = emitter.emitExpr(e.args[0]!);
|
|
if (e.receiver.type.kind === "array" && e.receiver.type.elem.kind === "union") {
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`${emitter.unionJoinHelper(e.receiver.type.elem.unionId)}(${r.name}, ${sep.name})`,
|
|
);
|
|
}
|
|
return emitter.newTemp(e.type, `scr_arr_join(${r.name}, ${sep.name})`);
|
|
}
|
|
case "slice": {
|
|
// Receiver borrowed; the result is a fresh +1 shallow copy
|
|
// (ref elements retained). Omitted indices get the JS
|
|
// defaults, the string-slice convention.
|
|
const start = e.args[0] ? emitter.emitExpr(e.args[0]).name : "0";
|
|
const end = e.args[1] ? emitter.emitExpr(e.args[1]).name : "INFINITY";
|
|
return emitter.newTemp(e.type, `scr_arr_slice(${r.name}, ${start}, ${end})`);
|
|
}
|
|
case "toReversed":
|
|
return emitter.newTemp(e.type, `scr_arr_to_reversed(${r.name})`);
|
|
case "reverse":
|
|
// Mutates in place and returns a retained reference to the
|
|
// receiver, preserving Array.prototype.reverse identity.
|
|
return emitter.newTemp(e.type, `scr_arr_reverse(${r.name})`);
|
|
case "toSpliced": {
|
|
const start = emitter.emitExpr(e.args[0]!);
|
|
const count = emitter.emitExpr(e.args[1]!);
|
|
const items = emitter.emitExpr(e.args[2]!);
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_arr_to_spliced(${r.name}, ${start.name}, ${count.name}, ${items.name})`,
|
|
);
|
|
}
|
|
case "with": {
|
|
const index = emitter.emitExpr(e.args[0]!);
|
|
const value = emitter.emitExpr(e.args[1]!);
|
|
const out = emitter.newTemp(
|
|
e.type,
|
|
`scr_arr_with_${acc}(${r.name}, ${index.name}, ${value.name})`,
|
|
);
|
|
emitter.emitPendingCheck();
|
|
return out;
|
|
}
|
|
case "withUndefined": {
|
|
const index = emitter.emitExpr(e.args[0]!);
|
|
const out = emitter.newTemp(
|
|
e.type,
|
|
`scr_arr_with_undefined(${r.name}, ${index.name})`,
|
|
);
|
|
emitter.emitPendingCheck();
|
|
return out;
|
|
}
|
|
case "splice": {
|
|
// The removal splice: the removed elements come back as a
|
|
// fresh +1 array, their ownership MOVED out of the receiver
|
|
// (no retain/release churn). An omitted count removes to the
|
|
// end (+Infinity, the slice convention).
|
|
const start = emitter.emitExpr(e.args[0]!);
|
|
const cnt = e.args[1] ? emitter.emitExpr(e.args[1]).name : "INFINITY";
|
|
return emitter.newTemp(e.type, `scr_arr_splice(${r.name}, ${start.name}, ${cnt})`);
|
|
}
|
|
case "spliceInsert": {
|
|
const start = emitter.emitExpr(e.args[0]!);
|
|
const count = emitter.emitExpr(e.args[1]!);
|
|
const items = emitter.emitExpr(e.args[2]!);
|
|
return emitter.newTemp(e.type, `scr_arr_splice_insert(${r.name}, ${start.name}, ${count.name}, ${items.name})`);
|
|
}
|
|
case "flatCopy":
|
|
case "flatOne": {
|
|
const out = emitter.emitExpr(e.args[0]!);
|
|
return emitter.newTemp(e.type, `scr_arr_flat_copy(${r.name}, ${out.name}, ${method === "flatOne" ? "true" : "false"})`);
|
|
}
|
|
default: {
|
|
const _exhaustive: never = method;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
case "bytesNew": {
|
|
// Typed-array/Buffer construction; the SOURCE's static type picks
|
|
// the runtime entry (see the node doc). The source is borrowed;
|
|
// every form hands back +1. Length and checked-input forms can
|
|
// throw; check after the result joins its ownership frame.
|
|
if (e.type.kind !== "bytes") throw new InternalCompilerError("emitter bug: bytesNew of non-bytes type");
|
|
const kind = bytesElemKindC(e.type.elem);
|
|
if (!e.source) return emitter.newTemp(e.type, `scr_bytes_new(${kind}, 0)`);
|
|
const src = emitter.emitExpr(e.source);
|
|
if (e.source.type.kind === "f64") {
|
|
const t = emitter.newTemp(e.type, `scr_bytes_new(${kind}, ${src.name})`);
|
|
emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
if (e.source.type.kind === "bytes") {
|
|
return emitter.newTemp(e.type, `scr_bytes_convert(${kind}, ${src.name})`);
|
|
}
|
|
if (e.source.type.kind === "dyn") {
|
|
const t = emitter.newTemp(e.type, `scr_bytes_from_dyn(${kind}, ${src.name}, ${e.from ? "true" : "false"})`);
|
|
emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
if (e.source.type.kind === "array") {
|
|
return emitter.newTemp(e.type, `scr_bytes_from_arr(${kind}, ${src.name})`);
|
|
}
|
|
throw new InternalCompilerError(`emitter bug: bytesNew source of kind ${e.source.type.kind}`);
|
|
}
|
|
case "bytesIntrinsic": {
|
|
// Receiver and args are borrowed frame temps; string/bytes results
|
|
// come back +1. The MAY_THROW methods get the standard pending
|
|
// check after their temp joins its frame.
|
|
//
|
|
// The numeric families first: their kind token (args[0], always a
|
|
// strLit — validated) maps to the runtime tag at COMPILE time and
|
|
// never emits as a value; the remaining args are plain f64s.
|
|
if (e.method === "readNum" || e.method === "writeNum" || e.method === "readNumVar" || e.method === "writeNumVar") {
|
|
const tok = e.args[0]!;
|
|
if (tok.kind !== "strLit") throw new InternalCompilerError(`emitter bug: bytesIntrinsic ${e.method} kind must be a strLit`);
|
|
const r0 = emitter.emitExpr(e.receiver);
|
|
const rest = e.args.slice(1).map((a) => emitter.emitExpr(a));
|
|
let call: string;
|
|
if (e.method === "readNum" || e.method === "writeNum") {
|
|
const spec = BYTES_NUM_KIND_C[tok.value];
|
|
if (!spec) throw new InternalCompilerError(`emitter bug: bytes numeric kind '${tok.value}'`);
|
|
call =
|
|
e.method === "readNum"
|
|
? `scr_bytes_read_num(${r0.name}, ${rest[0]!.name}, ${spec.kind}, ${spec.le})`
|
|
: `scr_bytes_write_num(${r0.name}, ${rest[0]!.name}, ${rest[1]!.name}, ${spec.kind}, ${spec.le})`;
|
|
} else {
|
|
const spec = BYTES_NUM_VAR_C[tok.value];
|
|
if (!spec) throw new InternalCompilerError(`emitter bug: bytes variable-width kind '${tok.value}'`);
|
|
call =
|
|
e.method === "readNumVar"
|
|
? `scr_bytes_read_var(${r0.name}, ${rest[0]!.name}, ${rest[1]!.name}, ${spec.sign}, ${spec.le})`
|
|
: `scr_bytes_write_var(${r0.name}, ${rest[0]!.name}, ${rest[1]!.name}, ${rest[2]!.name}, ${spec.sign}, ${spec.le})`;
|
|
}
|
|
const t = emitter.newTemp(e.type, call);
|
|
emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
const method = e.method;
|
|
const directElementAccess = method === "length" || method === "byteLength" || method === "get";
|
|
const r = directElementAccess
|
|
? emitStableReceiver(emitter, e.receiver, e.args)
|
|
: emitter.emitExpr(e.receiver);
|
|
const integerIndex = method === "get" ? emitter.integerLoopIndex(e.args[0]!) : null;
|
|
const args = integerIndex === null ? e.args.map((a) => emitter.emitExpr(a)) : [];
|
|
switch (method) {
|
|
case "length":
|
|
return emitter.newTemp(e.type, `(double)${r.name}->len`);
|
|
case "byteLength":
|
|
if (e.receiver.type.kind !== "bytes") {
|
|
throw new InternalCompilerError("emitter bug: bytesIntrinsic byteLength on non-bytes");
|
|
}
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`(double)(${r.name}->len * ${e.receiver.type.elem === "u8" ? "1" : e.receiver.type.elem === "f64" ? "8" : "4"})`,
|
|
);
|
|
case "get":
|
|
// Any invalid index traps (the array runtime's discipline).
|
|
if (e.receiver.type.kind !== "bytes") {
|
|
throw new InternalCompilerError("emitter bug: bytesIntrinsic get on non-bytes");
|
|
}
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`${emitter.bytesElementHelper("get", e.receiver.type.elem, integerIndex !== null)}(${r.name}, ${integerIndex ?? args[0]!.name})`,
|
|
);
|
|
case "slice":
|
|
// Omitted relative indices default like string slice: start 0,
|
|
// end +Infinity (math.h is always included). Fresh copy, +1.
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_bytes_slice(${r.name}, ${args[0]?.name ?? "0"}, ${args[1]?.name ?? "INFINITY"})`,
|
|
);
|
|
case "subarray":
|
|
// Same defaults; the result is a +1 VIEW aliasing the
|
|
// receiver's storage (subarray / Buffer's slice).
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_bytes_subarray(${r.name}, ${args[0]?.name ?? "0"}, ${args[1]?.name ?? "INFINITY"})`,
|
|
);
|
|
case "toReversed":
|
|
return emitter.newTemp(e.type, `scr_bytes_to_reversed(${r.name})`);
|
|
case "copyWithin":
|
|
return emitter.newTemp(e.type, `scr_bytes_copy_within(${r.name}, ${args[0]!.name}, ${args[1]!.name}, ${args[2]!.name})`);
|
|
case "with": {
|
|
const out = emitter.newTemp(
|
|
e.type,
|
|
`scr_bytes_with(${r.name}, ${args[0]!.name}, ${args[1]!.name})`,
|
|
);
|
|
emitter.emitPendingCheck();
|
|
return out;
|
|
}
|
|
case "join":
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_bytes_join(${r.name}, ${args[0]!.name})`,
|
|
);
|
|
case "toArray":
|
|
return emitter.newTemp(e.type, `scr_bytes_to_arr(${r.name})`);
|
|
case "setFrom":
|
|
case "setFromDyn": {
|
|
// dst.set(src, offset?) — void; throws Node's RangeError on
|
|
// overflow (may-throw seed).
|
|
emitter.line(
|
|
`${method === "setFromDyn" ? "scr_bytes_set_from_dyn" : "scr_bytes_set_from"}(${r.name}, ${args[0]!.name}, ${args[1]?.name ?? "0"});${emitter.srcComment(e.loc)}`,
|
|
);
|
|
emitter.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "toString":
|
|
case "toStringVar": {
|
|
// The encoding arg is always present (the frontend completes
|
|
// an omitted one to "utf8"). The literal path is already
|
|
// canonical and cannot throw; toStringVar validates a runtime
|
|
// encoding and may throw ERR_UNKNOWN_ENCODING. Both return a
|
|
// +1 string. Range forms: [enc, start] decodes to the buffer's
|
|
// end; [enc, start, end] clamps the explicit end (negatives
|
|
// empty, Node's slice-then-decode).
|
|
const checked = method === "toStringVar";
|
|
const stem = checked ? "scr_bytes_to_str_checked" : "scr_bytes_to_str";
|
|
let out: Temp;
|
|
if (args.length === 3) {
|
|
out = emitter.newTemp(e.type, `${stem}_range(${r.name}, ${args[0]!.name}, ${args[1]!.name}, ${args[2]!.name})`);
|
|
} else if (args.length === 2) {
|
|
out = emitter.newTemp(e.type, `${stem}_range(${r.name}, ${args[0]!.name}, ${args[1]!.name}, (double)${r.name}->len)`);
|
|
} else {
|
|
out = emitter.newTemp(e.type, `${stem}(${r.name}, ${args[0]!.name})`);
|
|
}
|
|
if (checked) emitter.emitPendingCheck();
|
|
return out;
|
|
}
|
|
case "equals":
|
|
return emitter.newTemp(e.type, `scr_bytes_equals(${r.name}, ${args[0]!.name})`);
|
|
case "compareBuf": {
|
|
// nargs = the PRESENT index args (omitted ones skip Node's
|
|
// validation); the 0 placeholders are never read past nargs.
|
|
const n = e.args.length - 1;
|
|
const idx = [1, 2, 3, 4].map((i: number) => args[i]?.name ?? "0");
|
|
const t = emitter.newTemp(
|
|
e.type,
|
|
`scr_bytes_compare(${r.name}, ${args[0]!.name}, ${n}, ${idx.join(", ")})`,
|
|
);
|
|
emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
case "indexOf":
|
|
case "lastIndexOf":
|
|
case "includes": {
|
|
// args = [needle, align, byteOffset?]; an omitted byteOffset
|
|
// is NaN — the runtime's search-everything default.
|
|
const fwd = method !== "lastIndexOf";
|
|
const call = `scr_bytes_index_of(${r.name}, ${args[0]!.name}, ${args[2]?.name ?? "NAN"}, ${args[1]!.name}, ${fwd})`;
|
|
return emitter.newTemp(e.type, method === "includes" ? `(${call} != -1)` : call);
|
|
}
|
|
case "indexOfNum":
|
|
case "lastIndexOfNum":
|
|
case "includesNum": {
|
|
const fwd = method !== "lastIndexOfNum";
|
|
const call = `scr_bytes_index_of_num(${r.name}, ${args[0]!.name}, ${args[1]?.name ?? "NAN"}, ${fwd})`;
|
|
return emitter.newTemp(e.type, method === "includesNum" ? `(${call} != -1)` : call);
|
|
}
|
|
case "fill":
|
|
case "fillNum": {
|
|
const fn = method === "fill" ? "scr_bytes_fill" : "scr_bytes_fill_num";
|
|
const n = e.args.length - 1;
|
|
const t = emitter.newTemp(
|
|
e.type,
|
|
`${fn}(${r.name}, ${args[0]!.name}, ${n}, ${args[1]?.name ?? "0"}, ${args[2]?.name ?? "0"})`,
|
|
);
|
|
emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
case "fillElem":
|
|
// Per-element TypedArray fill (non-u8): slice-style index
|
|
// defaults, never throws; the receiver comes back +1.
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_bytes_fill_elem(${r.name}, ${args[0]!.name}, ${args[1]?.name ?? "0"}, ${args[2]?.name ?? "INFINITY"})`,
|
|
);
|
|
case "fillStr": {
|
|
const n = e.args.length - 2;
|
|
const t = emitter.newTemp(
|
|
e.type,
|
|
`scr_bytes_fill_str(${r.name}, ${args[0]!.name}, ${args[1]!.name}, ${n}, ` +
|
|
`${args[2]?.name ?? "0"}, ${args[3]?.name ?? "0"})`,
|
|
);
|
|
emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
case "copy": {
|
|
const n = e.args.length - 1;
|
|
const t = emitter.newTemp(
|
|
e.type,
|
|
`scr_bytes_copy_into(${r.name}, ${args[0]!.name}, ${n}, ` +
|
|
`${args[1]?.name ?? "0"}, ${args[2]?.name ?? "0"}, ${args[3]?.name ?? "0"})`,
|
|
);
|
|
emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
case "swap16":
|
|
case "swap32":
|
|
case "swap64": {
|
|
const w = method === "swap16" ? 2 : method === "swap32" ? 4 : 8;
|
|
const t = emitter.newTemp(e.type, `scr_bytes_swap(${r.name}, ${w})`);
|
|
emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
case "writeStr": {
|
|
const t = emitter.newTemp(
|
|
e.type,
|
|
`scr_bytes_write_str(${r.name}, ${args[0]!.name}, ${args[1]!.name}, ${args[2]!.name}, ` +
|
|
`${args[3]?.name ?? "0"}, ${args[3] ? "true" : "false"})`,
|
|
);
|
|
emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
case "byteOffset":
|
|
// 0 for owners, the view's offset into its owner for a
|
|
// DataView. Never throws.
|
|
return emitter.newTemp(e.type, `scr_bytes_byte_offset(${r.name})`);
|
|
case "buffer":
|
|
return emitter.newTemp(e.type, `scr_array_buffer_from_bytes(${r.name})`);
|
|
case "dataViewNew": {
|
|
// new DataView(x.buffer, byteOffset?, byteLength?) — receiver
|
|
// is x itself (the frontend peeled `.buffer`). Omitted offset
|
|
// defaults to 0; the has_len flag keeps an omitted length
|
|
// distinct from every numeric value (the default is "to the
|
|
// end of the buffer"). Throws Node's RangeErrors; +1 view.
|
|
const t = emitter.newTemp(
|
|
e.type,
|
|
`scr_dataview_new(${r.name}, ${args[0]?.name ?? "0"}, ` +
|
|
`${args[1] ? "true" : "false"}, ${args[1]?.name ?? "0"})`,
|
|
);
|
|
emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
case "dvGetUint8":
|
|
case "dvGetInt8":
|
|
case "dvGetUint16":
|
|
case "dvGetInt16":
|
|
case "dvGetUint32":
|
|
case "dvGetInt32":
|
|
case "dvGetFloat32":
|
|
case "dvGetFloat64":
|
|
case "dvGetBigUint64Number":
|
|
case "dvGetBigInt64Number": {
|
|
// DataView getters: an omitted littleEndian is big-endian
|
|
// (the JS default). Throw Node's constant RangeError on a bad
|
|
// offset.
|
|
const kind = DV_GET_KIND_C[method];
|
|
const t = emitter.newTemp(
|
|
e.type,
|
|
`scr_dataview_get(${r.name}, ${args[0]!.name}, ${kind}, ${args[1]?.name ?? "false"})`,
|
|
);
|
|
emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
case "dvSetUint8":
|
|
case "dvSetInt8":
|
|
case "dvSetUint16":
|
|
case "dvSetInt16":
|
|
case "dvSetUint32":
|
|
case "dvSetInt32":
|
|
case "dvSetFloat32":
|
|
case "dvSetFloat64": {
|
|
// DataView setters: [offset, value, littleEndian?] — void;
|
|
// throw the getters' constant RangeError on a bad offset.
|
|
const kind = DV_SET_KIND_C[method];
|
|
emitter.line(
|
|
`scr_dataview_set(${r.name}, ${args[0]!.name}, ${args[1]!.name}, ${kind}, ` +
|
|
`${args[2]?.name ?? "false"});${emitter.srcComment(e.loc)}`,
|
|
);
|
|
emitter.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
default: {
|
|
const _exhaustive: never = method;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
case "mapNew": {
|
|
// Each reference side supplies its own RC adapters. Either trace
|
|
// adapter requires a collector header: keys as well as values can
|
|
// hold a path back to the map.
|
|
if (e.type.kind !== "map") throw new InternalCompilerError("emitter bug: mapNew of non-map type");
|
|
const value = e.type.value;
|
|
const rc = isRefCounted(value) ? vAdapters(value) : null;
|
|
const keyRc = mapKeyAccess(e.type.key) === "ref" ? vAdapters(e.type.key) : null;
|
|
const args = `${mapKeyKindC(e.type.key)}, ${mapValKindC(value)}, `;
|
|
const valueArgs = `${rc ? `&${rc.retain}` : "NULL"}, ${rc ? `&${rc.release}` : "NULL"}, ${emitter.traceArgC(value)}`;
|
|
const m = emitter.newTemp(
|
|
e.type,
|
|
keyRc
|
|
? `scr_map_new_typed(${args}&${keyRc.retain}, &${keyRc.release}, ${emitter.traceArgC(e.type.key)}, ${valueArgs})`
|
|
: `scr_map_new(${args}${valueArgs})`,
|
|
);
|
|
// Seeded construction: set() each pair in source order — exactly
|
|
// the statements the user would write on an empty map, so a
|
|
// repeated key overwrites (the runtime releases the old value).
|
|
for (const pair of e.seed ?? []) {
|
|
const k = emitter.emitExpr(pair.key);
|
|
const v = emitter.emitExpr(pair.value);
|
|
if (elemAccess(value) === "ref") emitter.moveTemp(v); // value MOVES in, like mapIntrinsic set
|
|
emitter.line(
|
|
`scr_map_set_${mapKeyAccess(e.type.key)}_${elemAccess(value)}(${m.name}, ${k.name}, ${v.name});${emitter.srcComment(e.loc)}`,
|
|
);
|
|
}
|
|
return m;
|
|
}
|
|
case "mapIntrinsic":
|
|
case "setIntrinsic":
|
|
return emitMapLikeIntrinsic(emitter, e);
|
|
case "setNew": {
|
|
// Empty set: the map runtime with the element as the KEY and the
|
|
// value slot pinned to the scalar kind (every stored value is 0.0,
|
|
// never read back). Identity elements supply key RC/trace adapters;
|
|
// scalar and string elements use the lean constructor.
|
|
if (e.type.kind !== "set") throw new InternalCompilerError("emitter bug: setNew of non-set type");
|
|
// Reference elements (identity hashing) carry their RC adapters
|
|
// at construction — the scr_arr_new_ref technique.
|
|
const elemAcc = mapKeyAccess(e.type.elem);
|
|
const rcAdapters = elemAcc === "ref" ? vAdapters(e.type.elem) : null;
|
|
const s = emitter.newTemp(
|
|
e.type,
|
|
rcAdapters
|
|
? `scr_map_new_typed(${mapKeyKindC(e.type.elem)}, SCR_MAP_VAL_F64, &${rcAdapters.retain}, &${rcAdapters.release}, ${emitter.traceArgC(e.type.elem)}, NULL, NULL, NULL)`
|
|
: `scr_map_new(${mapKeyKindC(e.type.elem)}, SCR_MAP_VAL_F64, NULL, NULL, NULL)`,
|
|
);
|
|
// Seeded construction (`new Set(values)`): one borrowed T[] whose
|
|
// elements add() in order — the runtime helper walks the array
|
|
// (duplicates overwrite in place, insertion position preserved).
|
|
if (e.seed) {
|
|
const arr = emitter.emitExpr(e.seed);
|
|
emitter.line(`scr_set_add_all(${s.name}, ${arr.name});${emitter.srcComment(e.loc)}`);
|
|
}
|
|
return s;
|
|
}
|
|
default: {
|
|
const _exhaustive: never = e;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
function emitCallExpr(
|
|
emitter: CEmitter,
|
|
e: ExprOf<"call" | "ffiCall" | "closure" | "callValue" | "selfRef" | "new" | "classRef" | "newValue" | "instanceOfValue" | "promiseVoidWiden" | "upcast" | "downcast" | "instanceOf" | "virtualCall">,
|
|
): Temp {
|
|
switch (e.kind) {
|
|
case "call": {
|
|
const args = e.args.map((a) => emitter.emitExpr(a));
|
|
// Callees own their params: ownership of refcounted args moves.
|
|
for (const a of args) emitter.moveTemp(a);
|
|
// Async callee: the spawn wrapper runs the body eagerly to its
|
|
// first suspension and returns the promise (+1). The call itself
|
|
// never unwinds — rejections surface at await.
|
|
const call = `${emitter.callTargetC(e.callee)}(${args.map((a) => a.name).join(", ")})`;
|
|
if (e.type.kind === "void") {
|
|
emitter.line(`${call};${emitter.srcComment(e.loc)}`);
|
|
if (emitter.mayThrow.has(e.callee)) emitter.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
const t = emitter.newTemp(e.type, call);
|
|
// The check runs AFTER the result temp joins the frame: an unwind
|
|
// releases it (the dummy is NULL for refcounted returns).
|
|
if (emitter.mayThrow.has(e.callee)) emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
case "ffiCall": {
|
|
// LIBRARY mode: every ffiCall is a profile-declared host-callback
|
|
// channel (the library lane loads no native-FFI manifest). The
|
|
// dispatch fetches the slot's registered pointer — or delivers the
|
|
// channel's unregistered-call trap through the funnel (SC4025) —
|
|
// then brackets only the typed indirect call, opaque context first.
|
|
// The bracket makes callback-time ABI re-entry a deterministic
|
|
// SC4026 trap before an inner entry can mutate runtime state.
|
|
// Marshalling matches the native ffiCall's value classes exactly:
|
|
// buffers are borrowed (ptr, len) for the call's duration, the
|
|
// u8/u32/i32 plumbing classes ride JS's ToUint32/ToInt32, and a
|
|
// scalar return converts to f64 exactly. The host cannot raise a
|
|
// scriptc exception, so no pending check follows.
|
|
const libCb = emitter.mod.lib?.callbacks?.find((c) => c.name === e.import);
|
|
if (libCb !== undefined) {
|
|
const cbArgs = e.args.map((arg) => emitter.emitExpr(arg));
|
|
const natTypes: string[] = ["void *"];
|
|
const natArgs: string[] = [];
|
|
libCb.params.forEach((cls, i) => {
|
|
const arg = cbArgs[i]!;
|
|
const native = (): string => `sc_t${emitter.tempCounter++}`;
|
|
switch (cls) {
|
|
case "f64": {
|
|
const value = native();
|
|
emitter.line(`double ${value} = ${arg.name};`);
|
|
natTypes.push("double");
|
|
natArgs.push(value);
|
|
break;
|
|
}
|
|
case "bool": {
|
|
const value = native();
|
|
emitter.line(`uint8_t ${value} = (uint8_t)(${arg.name} ? 1 : 0);`);
|
|
natTypes.push("uint8_t");
|
|
natArgs.push(value);
|
|
break;
|
|
}
|
|
case "u8": {
|
|
const value = native();
|
|
emitter.line(`uint8_t ${value} = (uint8_t)(uint32_t)scr_bit_ushr(${arg.name}, 0.0);`);
|
|
natTypes.push("uint8_t");
|
|
natArgs.push(value);
|
|
break;
|
|
}
|
|
case "u32": {
|
|
const value = native();
|
|
emitter.line(`uint32_t ${value} = (uint32_t)scr_bit_ushr(${arg.name}, 0.0);`);
|
|
natTypes.push("uint32_t");
|
|
natArgs.push(value);
|
|
break;
|
|
}
|
|
case "i32": {
|
|
const value = native();
|
|
emitter.line(`int32_t ${value} = (int32_t)scr_bit_or(${arg.name}, 0.0);`);
|
|
natTypes.push("int32_t");
|
|
natArgs.push(value);
|
|
break;
|
|
}
|
|
case "string":
|
|
case "bytes": {
|
|
const ptr = native();
|
|
const len = native();
|
|
emitter.line(`const uint8_t *${ptr} = (const uint8_t *)${arg.name}->data;`);
|
|
emitter.line(`size_t ${len} = ${arg.name}->len;`);
|
|
natTypes.push("const uint8_t *", "size_t");
|
|
natArgs.push(ptr, len);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
const retC =
|
|
libCb.returns === "void" ? "void"
|
|
: libCb.returns === "f64" ? "double"
|
|
: libCb.returns === "bool" || libCb.returns === "u8" ? "uint8_t"
|
|
: libCb.returns === "u32" ? "uint32_t"
|
|
: "int32_t";
|
|
const trapLit = cStringLiteral(Buffer.from(libCb.unregisteredTrap, "utf8"));
|
|
// Materialize the pointer and context before the callback-active
|
|
// bracket. This keeps the existing SC4025 fetch path outside the
|
|
// bracket and avoids C argument evaluation-order ambiguity.
|
|
const fn = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${retC} (*${fn})(${natTypes.join(", ")}) = (${retC} (*)(${natTypes.join(", ")}))scr_library_cb_require(${libCb.slot}, ${trapLit});`);
|
|
const ctx = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`void *${ctx} = scr_library_cb_ctx(${libCb.slot});`);
|
|
const call = `${fn}(${[ctx, ...natArgs].join(", ")})`;
|
|
switch (libCb.returns) {
|
|
case "void":
|
|
emitter.line(`scr_library_callback_begin();`);
|
|
emitter.line(`${call};${emitter.srcComment(e.loc)}`);
|
|
emitter.line(`scr_library_callback_end();`);
|
|
return { name: "", type: e.type };
|
|
case "f64": {
|
|
const raw = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`scr_library_callback_begin();`);
|
|
emitter.line(`${retC} ${raw} = ${call};${emitter.srcComment(e.loc)}`);
|
|
emitter.line(`scr_library_callback_end();`);
|
|
return emitter.newTemp(e.type, raw);
|
|
}
|
|
case "bool":
|
|
{
|
|
const raw = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`scr_library_callback_begin();`);
|
|
emitter.line(`${retC} ${raw} = ${call};${emitter.srcComment(e.loc)}`);
|
|
emitter.line(`scr_library_callback_end();`);
|
|
return emitter.newTemp(e.type, `(${raw} != 0)`);
|
|
}
|
|
default: { // u8/u32/i32 — exact widenings back to f64
|
|
const raw = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`scr_library_callback_begin();`);
|
|
emitter.line(`${retC} ${raw} = ${call};${emitter.srcComment(e.loc)}`);
|
|
emitter.line(`scr_library_callback_end();`);
|
|
return emitter.newTemp(e.type, `(double)${raw}`);
|
|
}
|
|
}
|
|
}
|
|
const entry = emitter.ffiByName.get(e.import);
|
|
if (!entry) throw new InternalCompilerError(`emitter bug: unknown FFI import ${e.import}`);
|
|
const args = e.args.map((arg) => emitter.emitExpr(arg));
|
|
const sourceArgs = new Map<number, Temp>();
|
|
const callbackArgs = new Map<string, Temp>();
|
|
let sourceIndex = 0;
|
|
entry.params.forEach((param, abiIndex) => {
|
|
if (isFfiContextParam(param)) return;
|
|
const arg = args[sourceIndex++]!;
|
|
sourceArgs.set(abiIndex, arg);
|
|
if (isFfiCallbackParam(param)) callbackArgs.set(param.callback.id, arg);
|
|
if (isFfiReleaseParam(param)) callbackArgs.set(param.callback.release, arg);
|
|
});
|
|
|
|
const { registrations: retainedRegistrations, releases: retainedReleases } =
|
|
collectFfiRetainedOps<Temp>(entry, callbackArgs, (binding, id) => emitter.ffiCallbackAdapter(binding, id));
|
|
|
|
// Pin before registration. Raw retained descriptors are native
|
|
// singletons: the incoming closure is pinned (and an EMPTY slot
|
|
// armed) before the native set call, but a replaced registration
|
|
// stays live and dispatching until the call returns — a native
|
|
// setter may flush the outgoing callback one last time mid-replace.
|
|
// scr_ffi_commit_slot below repoints the slot and retires the
|
|
// superseded pins after the call.
|
|
for (const registration of retainedRegistrations) {
|
|
if (registration.global !== null) {
|
|
emitter.line(`scr_ffi_retain_slot(&${registration.table}, &${registration.global}, ${registration.callback.name});`);
|
|
} else if (registration.foreign) {
|
|
emitter.line(`scr_ffi_retain_foreign(&${registration.table}, ${registration.callback.name});`);
|
|
} else {
|
|
emitter.line(`scr_ffi_retain(&${registration.table}, ${registration.callback.name});`);
|
|
}
|
|
}
|
|
// Validate releases BEFORE the native removal call runs: a bogus
|
|
// release traps without native code observing any side effect. The
|
|
// registration itself is unpinned only after the call returns.
|
|
for (const release of retainedReleases) {
|
|
emitter.line(`scr_ffi_require${release.foreign ? "_foreign" : ""}(&${release.table}, ${release.callback.name});`);
|
|
}
|
|
|
|
// Raw C callback pointers carry no userdata. For the documented
|
|
// call-scoped/same-thread policy, lend each one a distinct TLS
|
|
// slot for the dynamic extent of this native call. Save/restore
|
|
// makes nested and reentrant calls stack correctly.
|
|
const rawContexts: { tls: string; previous: Temp }[] = [];
|
|
for (const param of entry.params) {
|
|
if (!isFfiCallbackParam(param)) continue;
|
|
const adapter = emitter.ffiCallbackAdapter(entry.name, param.callback.id);
|
|
if (adapter.tls === null) continue;
|
|
const callback = callbackArgs.get(param.callback.id)!;
|
|
const previous = emitter.newBorrowedTemp(callback.type, adapter.tls);
|
|
emitter.line(`${adapter.tls} = ${callback.name};`);
|
|
rawContexts.push({ tls: adapter.tls, previous });
|
|
}
|
|
const nativeArgs: string[] = [];
|
|
entry.params.forEach((param, i) => {
|
|
if (isFfiCallbackParam(param)) {
|
|
const adapter = emitter.ffiCallbackAdapter(entry.name, param.callback.id);
|
|
nativeArgs.push(`&${adapter.symbol}`);
|
|
return;
|
|
}
|
|
if (isFfiReleaseParam(param)) {
|
|
const { binding, id } = parseFfiCallbackKey(param.callback.release);
|
|
const adapter = emitter.ffiCallbackAdapter(binding, id);
|
|
nativeArgs.push(`&${adapter.symbol}`);
|
|
return;
|
|
}
|
|
if (isFfiContextParam(param)) {
|
|
const callback = callbackArgs.get(param.context);
|
|
if (!callback) throw new InternalCompilerError(`emitter bug: FFI context '${param.context}' has no callback arg`);
|
|
nativeArgs.push(`(void *)${callback.name}`);
|
|
return;
|
|
}
|
|
const arg = sourceArgs.get(i)!;
|
|
switch (param) {
|
|
case "mutable-bytes":
|
|
nativeArgs.push(`(uint8_t *)${arg.name}->data`, `${arg.name}->len`);
|
|
break;
|
|
case "f64":
|
|
nativeArgs.push(arg.name);
|
|
break;
|
|
case "f32":
|
|
nativeArgs.push(`(float)${arg.name}`);
|
|
break;
|
|
case "i8":
|
|
case "i16": {
|
|
const bits = param === "i8" ? 8 : 16;
|
|
const coerced = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`uint32_t ${coerced} = (uint32_t)scr_bit_ushr(${arg.name}, 0.0) & ${(2 ** bits) - 1}u;`);
|
|
nativeArgs.push(`(int${bits}_t)(${coerced} < ${2 ** (bits - 1)}u ? (int32_t)${coerced} : (int32_t)${coerced} - ${2 ** bits})`);
|
|
break;
|
|
}
|
|
case "u16":
|
|
nativeArgs.push(`(uint16_t)(uint32_t)scr_bit_ushr(${arg.name}, 0.0)`);
|
|
break;
|
|
case "bool":
|
|
nativeArgs.push(`(uint8_t)(${arg.name} ? 1 : 0)`);
|
|
break;
|
|
case "u8":
|
|
nativeArgs.push(`(uint8_t)(uint32_t)scr_bit_ushr(${arg.name}, 0.0)`);
|
|
break;
|
|
case "u32":
|
|
nativeArgs.push(`(uint32_t)scr_bit_ushr(${arg.name}, 0.0)`);
|
|
break;
|
|
case "i32":
|
|
nativeArgs.push(`(int32_t)scr_bit_or(${arg.name}, 0.0)`);
|
|
break;
|
|
case "string":
|
|
nativeArgs.push(`(const uint8_t *)${arg.name}->data`, `${arg.name}->len`);
|
|
break;
|
|
case "bytes":
|
|
nativeArgs.push(`(const uint8_t *)${arg.name}->data`, `${arg.name}->len`);
|
|
break;
|
|
}
|
|
});
|
|
const call = `${entry.symbol}(${nativeArgs.join(", ")})`;
|
|
const restoreRawContexts = (): void => {
|
|
for (let i = rawContexts.length - 1; i >= 0; i--) {
|
|
const saved = rawContexts[i]!;
|
|
emitter.line(`${saved.tls} = ${saved.previous.name};`);
|
|
}
|
|
};
|
|
const finishRetainedReleases = (): void => {
|
|
// Commit raw replacements first (repoint the slot, retire the
|
|
// superseded pins), then unpin explicit releases — the runtime
|
|
// disarms the slot itself when the released closure holds it.
|
|
for (const registration of retainedRegistrations) {
|
|
if (registration.global !== null) {
|
|
emitter.line(`scr_ffi_commit_slot(&${registration.table}, ${registration.callback.name});`);
|
|
}
|
|
}
|
|
for (const release of retainedReleases) {
|
|
emitter.line(`scr_ffi_release${release.foreign ? "_foreign" : ""}(&${release.table}, ${release.callback.name});`);
|
|
}
|
|
};
|
|
const callbacksMayThrow = callbackArgs.size > 0 || emitter.ffiHasRetainedCallback;
|
|
switch (entry.returns) {
|
|
case "void":
|
|
emitter.line(`${call};${emitter.srcComment(e.loc)}`);
|
|
restoreRawContexts();
|
|
finishRetainedReleases();
|
|
if (callbacksMayThrow) emitter.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
case "f64": {
|
|
const result = emitter.newTemp(e.type, call);
|
|
restoreRawContexts();
|
|
finishRetainedReleases();
|
|
if (callbacksMayThrow) emitter.emitPendingCheck();
|
|
return result;
|
|
}
|
|
case "bool": {
|
|
const result = emitter.newTemp(e.type, `(${call} != 0)`);
|
|
restoreRawContexts();
|
|
finishRetainedReleases();
|
|
if (callbacksMayThrow) emitter.emitPendingCheck();
|
|
return result;
|
|
}
|
|
case "u8":
|
|
case "f32":
|
|
case "i8":
|
|
case "u16":
|
|
case "i16":
|
|
case "u32":
|
|
case "i32": {
|
|
const result = emitter.newTemp(e.type, `(double)${call}`);
|
|
restoreRawContexts();
|
|
finishRetainedReleases();
|
|
if (callbacksMayThrow) emitter.emitPendingCheck();
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
case "closure": {
|
|
const target = emitter.fnByName.get(e.fnName);
|
|
if (!target) throw new InternalCompilerError(`emitter bug: closure over unknown function ${e.fnName}`);
|
|
if (target.captures === undefined) {
|
|
// Declared function as a value: the interned immortal closure —
|
|
// every mention yields the same pointer, so `f === f` is true.
|
|
emitter.fnValues.add(e.fnName);
|
|
return emitter.newTemp(
|
|
e.type,
|
|
retainCallC(e.type, `(ScrClosure *)&${mangleFnClosure(e.fnName)}`),
|
|
);
|
|
}
|
|
const t = emitter.newTemp(
|
|
e.type,
|
|
`scr_closure_new((void *)&${emitter.callTargetC(e.fnName)}, ${e.captures.length})`,
|
|
);
|
|
e.captures.forEach((localId, i) => {
|
|
emitter.line(`${t.name}->caps[${i}] = scr_box_retain(${mangleLocal(localId)});`);
|
|
});
|
|
return t;
|
|
}
|
|
case "callValue": {
|
|
const callee = emitter.emitExpr(e.callee);
|
|
const receiver = e.receiver === undefined ? null : emitter.emitExpr(e.receiver);
|
|
const args = e.args.map((a) => emitter.emitExpr(a));
|
|
for (const a of args) emitter.moveTemp(a); // callee owns its params
|
|
if (e.callee.type.kind !== "func") throw new InternalCompilerError("emitter bug: callValue on non-func");
|
|
const cast = cFnPtrCast(e.callee.type);
|
|
const argList = [callee.name, ...args.map((a) => a.name)].join(", ");
|
|
const call = `(${cast}${callee.name}->fn)(${argList})`;
|
|
emitter.line(`scr_dyn_this_push_dyn(${receiver?.name ?? "NULL"});`);
|
|
if (e.type.kind === "void") {
|
|
emitter.line(`${call};${emitter.srcComment(e.loc)}`);
|
|
emitter.line("scr_dyn_this_pop();");
|
|
if (emitter.indirectMayThrow) emitter.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
const t = emitter.newTemp(e.type, call);
|
|
emitter.line("scr_dyn_this_pop();");
|
|
if (emitter.indirectMayThrow) emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
case "selfRef":
|
|
// The running closure itself (env is borrowed; the result is owned).
|
|
return emitter.newTemp(e.type, retainCallC(e.type, "sc_env"));
|
|
case "new": {
|
|
// Allocate (fields zeroed), then run the ctor. The ctor owns and
|
|
// releases its `this` param like any callee, so it receives a +1
|
|
// distinct from the one this expression returns.
|
|
const t = emitter.newTemp(e.type, `${mangleClassNew(e.className)}()`);
|
|
const args = e.args.map((a) => emitter.emitExpr(a));
|
|
for (const a of args) emitter.moveTemp(a);
|
|
const ctorArgs = [`${mangleClassRetain(e.className)}(${t.name})`, ...args.map((a) => a.name)];
|
|
emitter.line(`${mangleFunction(`%${e.className}.constructor`)}(${ctorArgs.join(", ")});${emitter.srcComment(e.loc)}`);
|
|
// A throwing constructor unwinds like any call; the half-built
|
|
// object is in this frame and releases with it.
|
|
if (emitter.mayThrow.has(`%${e.className}.constructor`)) emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
case "classRef": {
|
|
// The class itself as a value: the immortal class object's
|
|
// address. The +1 retain is a no-op on immortals but keeps the
|
|
// owned-temps discipline uniform (the regexLit pattern).
|
|
const sym = emitter.classObjSym(e.className);
|
|
return emitter.newTemp(e.type, `scr_classobj_retain(&${sym})`);
|
|
}
|
|
case "newValue": {
|
|
// Construction through a class VALUE: call the class object's
|
|
// construct thunk. Every value legally in the slot shares the
|
|
// static class's constructor ABI (the frontend's flow rule), so
|
|
// the cast spells exactly that signature; the thunk returns the
|
|
// +1 object as void * and the site reinterprets to the static
|
|
// class (a runtime descendant is the ordinary upcast story).
|
|
if (e.callee.type.kind !== "classval") {
|
|
throw new InternalCompilerError("emitter bug: newValue on non-classval callee");
|
|
}
|
|
const cls = e.callee.type.className;
|
|
const ctor = emitter.fnByName.get(`%${cls}.constructor`);
|
|
if (!ctor) throw new InternalCompilerError(`emitter bug: newValue on ${cls} without a constructor`);
|
|
const callee = emitter.emitExpr(e.callee);
|
|
const args = e.args.map((a) => emitter.emitExpr(a));
|
|
for (const a of args) emitter.moveTemp(a); // the constructor owns its params
|
|
const paramTypes = ctor.params.slice(1).map((p) => cType(p.type).trim());
|
|
const cast = `(void *(*)(${paramTypes.join(", ") || "void"}))`;
|
|
const call = `(${cast}${callee.name}->ctor)(${args.map((a) => a.name).join(", ")})`;
|
|
const t = emitter.newTemp(e.type, `(${cType(e.type).trim()})${call}`);
|
|
if (newValueMayThrow(cls, emitter.classMeta.get(cls), emitter.mayThrow)) emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
case "instanceOfValue": {
|
|
// The interval check with the target loaded from the class object
|
|
// (same numbering the vtables carry). Frontend guarantees both
|
|
// sides are hierarchy members, so the operand has a vt word.
|
|
const v = emitter.emitExpr(e.value);
|
|
const target = emitter.emitExpr(e.classValue);
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`${v.name}->vt->pre >= ${target.name}->pre && ${v.name}->vt->pre <= ${target.name}->post`,
|
|
);
|
|
}
|
|
case "promiseVoidWiden": {
|
|
// One ScrPromise* either way — ownership transfers, type-only.
|
|
const v = emitter.emitExpr(e.value);
|
|
emitter.moveTemp(v);
|
|
return emitter.newTemp(e.type, v.name);
|
|
}
|
|
case "upcast":
|
|
case "downcast": {
|
|
// Prefix layout: both directions are pointer reinterprets of the
|
|
// SAME object — no RC traffic, ownership transfers from the
|
|
// operand temp to the result temp (the operand is struck so the
|
|
// one +1 releases exactly once). Classval upcasts are the same
|
|
// pointer with only the static type changing (one ScrClassObj
|
|
// struct covers every class), so the cast is a no-op spelling.
|
|
const v = emitter.emitExpr(e.value);
|
|
emitter.moveTemp(v);
|
|
return emitter.newTemp(e.type, `(${cType(e.type).trim()})${v.name}`);
|
|
}
|
|
case "instanceOf": {
|
|
// O(1) preorder-interval test against the vtable the object
|
|
// carries; the target's interval is a compile-time constant.
|
|
const v = emitter.emitExpr(e.value);
|
|
const target = emitter.classMeta.get(e.className);
|
|
if (!target) throw new InternalCompilerError(`emitter bug: instanceOf against unknown class ${e.className}`);
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`${v.name}->vt->pre >= ${target.pre} && ${v.name}->vt->pre <= ${target.post}`,
|
|
);
|
|
}
|
|
case "virtualCall": {
|
|
// Dispatch through the receiver's vtable: the slot lives on the
|
|
// method's root-most declaring class (an ancestor of the static
|
|
// class), whose signature the slot pointer carries — the receiver
|
|
// upcasts to it, another prefix-layout reinterpret.
|
|
const meta = emitter.classMeta.get(e.className);
|
|
if (!meta) throw new InternalCompilerError(`emitter bug: virtualCall on unknown class ${e.className}`);
|
|
const slot = meta.root.slots.find(
|
|
(sl) =>
|
|
sl.method === e.method && sl.declarer.pre <= meta.pre && meta.pre <= sl.declarer.post,
|
|
);
|
|
if (!slot) throw new InternalCompilerError(`emitter bug: no vtable slot for ${e.className}.${e.method}`);
|
|
const args = e.args.map((a) => emitter.emitExpr(a));
|
|
for (const a of args) emitter.moveTemp(a); // callees own their params
|
|
const recv = args[0]!.name;
|
|
const recvArg =
|
|
slot.declarer === meta ? recv : `(${mangleClassStruct(slot.declarer.def.name)} *)${recv}`;
|
|
const vtt = mangleVtStruct(meta.root.def.name);
|
|
const argList = [recvArg, ...args.slice(1).map((a) => a.name)].join(", ");
|
|
const call = `((const ${vtt} *)${recv}->vt)->${slot.member}(${argList})`;
|
|
if (e.type.kind === "void") {
|
|
emitter.line(`${call};${emitter.srcComment(e.loc)}`);
|
|
if (emitter.mayThrowMethods.has(e.method)) emitter.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
const t = emitter.newTemp(e.type, call);
|
|
if (emitter.mayThrowMethods.has(e.method)) emitter.emitPendingCheck();
|
|
return t;
|
|
}
|
|
default: {
|
|
const _exhaustive: never = e;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
function emitRecordExpr(
|
|
emitter: CEmitter,
|
|
e: ExprOf<"fieldGet" | "recordGet" | "recordLit" | "recordClone" | "recordKeyGet" | "recordOvfKeys" | "recordOvfHas">,
|
|
): Temp {
|
|
switch (e.kind) {
|
|
case "fieldGet":
|
|
case "recordGet": {
|
|
const obj = emitter.emitExpr(e.obj);
|
|
// Runtime error classes use ScrError's own member names.
|
|
const member =
|
|
e.kind === "fieldGet" && RUNTIME_ERROR_CLASSES.has(e.className)
|
|
? e.field
|
|
: mangleField(e.field);
|
|
const field = `${obj.name}->${member}`;
|
|
return emitter.newTemp(e.type, isRefCounted(e.type) ? retainCallC(e.type, field) : field);
|
|
}
|
|
case "recordLit": {
|
|
// Allocate (fields zeroed), then write each field IN SOURCE ORDER —
|
|
// JS evaluates property values in source order. Ownership of
|
|
// refcounted values moves in; the struct is fresh, so there is
|
|
// never an old value to release. OVERFLOW entries (undeclared keys
|
|
// of an index-signature shape) insert into the shape's overflow
|
|
// map in the same interleaved order — the map takes ownership.
|
|
if (e.type.kind !== "record") throw new InternalCompilerError("emitter bug: recordLit of non-record type");
|
|
const rec = emitter.newTemp(e.type, `${mangleRecordNew(e.type.shapeId)}()`);
|
|
for (const f of e.fields) {
|
|
if (f.drop) {
|
|
// A mapping-dropped field (the PromiseSettledResult subset):
|
|
// the initializer runs in its source-order slot — effects and
|
|
// throws included — and the result (if any) releases with the
|
|
// statement frame instead of storing.
|
|
emitter.emitExpr(f.value);
|
|
continue;
|
|
}
|
|
const v = emitter.emitExpr(f.value);
|
|
if (f.overflow) {
|
|
const lit = emitter.internLiteral(f.name);
|
|
const acc =
|
|
v.type.kind === "f64" ? "f64" : v.type.kind === "bool" ? "bool" : "ref";
|
|
if (acc === "ref") emitter.moveTemp(v);
|
|
emitter.line(
|
|
`scr_map_set_str_${acc}(${rec.name}->${OVERFLOW_MEMBER}, (ScrStr *)&${lit}, ${v.name});`,
|
|
);
|
|
continue;
|
|
}
|
|
if (isRefCounted(v.type)) emitter.moveTemp(v);
|
|
emitter.line(`${rec.name}->${mangleField(f.name)} = ${v.name};`);
|
|
}
|
|
return rec;
|
|
}
|
|
case "recordClone": {
|
|
if (e.type.kind !== "record") throw new InternalCompilerError("emitter bug: recordClone of non-record type");
|
|
emitter.recordCloneShapes.add(e.type.shapeId);
|
|
// Source first, then each override in source order. The helper
|
|
// returns a fully retained owned clone; replacement unlinks before
|
|
// releasing the copied value, matching recordSet's cycle discipline.
|
|
const source = emitter.emitExpr(e.source);
|
|
const rec = emitter.newTemp(e.type, `${mangleRecordClone(e.type.shapeId)}(${source.name})`);
|
|
for (const f of e.overrides) {
|
|
const v = emitter.emitExpr(f.value);
|
|
const field = `${rec.name}->${mangleField(f.name)}`;
|
|
if (isRefCounted(v.type)) {
|
|
emitter.moveTemp(v);
|
|
const old = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(v.type, old)} = ${field};`);
|
|
emitter.line(`${field} = ${v.name};`);
|
|
emitter.releaseValue(old, v.type);
|
|
} else {
|
|
emitter.line(`${field} = ${v.name};`);
|
|
}
|
|
}
|
|
return rec;
|
|
}
|
|
case "recordKeyGet": {
|
|
// Dynamic-keyed record read through the per-(shape, result type)
|
|
// helper — declared-field string switch, then the overflow map.
|
|
// Never throws (a smuggled miss traps in the helper).
|
|
const obj = emitter.emitExpr(e.obj);
|
|
const key = emitter.emitExpr(e.key);
|
|
const helper = emitter.recordKeyGetHelper(e.shapeId, e.type, e.overflowOnly === true);
|
|
return emitter.newTemp(e.type, `${helper}(${obj.name}, ${key.name})`);
|
|
}
|
|
case "recordOvfHas": {
|
|
const obj = emitter.emitExpr(e.obj);
|
|
const key = emitter.emitExpr(e.key);
|
|
return emitter.newTemp(e.type, `scr_map_has_str(${obj.name}->${OVERFLOW_MEMBER}, ${key.name})`);
|
|
}
|
|
case "recordOvfKeys": {
|
|
// The overflow map's live keys in JS own-key order — a fresh
|
|
// string[] snapshot (+1); the record is borrowed.
|
|
const obj = emitter.emitExpr(e.obj);
|
|
return emitter.newTemp(e.type, `scr_map_keys_js_order(${obj.name}->${OVERFLOW_MEMBER})`);
|
|
}
|
|
default: {
|
|
const _exhaustive: never = e;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
function emitDynamicExpr(
|
|
emitter: CEmitter,
|
|
e: ExprOf<"dynFrom" | "dynFromJsval" | "dynCall" | "dynInvoke" | "dynArrLit" | "dynObjLit" | "unionWrap" | "unionNarrow" | "unionDisc" | "unionKeyGet" | "unionIsTag" | "dynKeyGet" | "dynHasKey" | "dynScalarEq" | "dynTest" | "unionEq" | "unionFuncEq" | "caughtTest" | "caughtCheck" | "caughtNarrow" | "caughtToDyn">,
|
|
): Temp {
|
|
switch (e.kind) {
|
|
case "dynFrom": {
|
|
// Static value → fresh dyn tree (+1) through the interned per-type
|
|
// converter; the operand stays borrowed (frame-released as usual).
|
|
// Bare unit literals (an `undefined`/`null` stored under an
|
|
// `unknown` index signature) are the dyn unit values directly.
|
|
if (e.value.kind === "unitLit") {
|
|
return emitter.newTemp(
|
|
e.type,
|
|
e.value.unit === "undefined"
|
|
? "scr_dyn_retain(scr_dyn_undefined())"
|
|
: "scr_dyn_new_null()",
|
|
);
|
|
}
|
|
const v = emitter.emitExpr(e.value);
|
|
const identityRef =
|
|
isDynTypedRefType(v.type) ||
|
|
(v.type.kind === "union" &&
|
|
(emitter.unionsById.get(v.type.unionId)?.arms.some(isDynTypedRefType) ?? false));
|
|
if (e.liveRef || identityRef) {
|
|
if (v.type.kind === "union") {
|
|
const adapter = liveDynUnionRefAdapter(emitter, v.type);
|
|
return emitter.newTemp(e.type, `${adapter}(${v.name})`);
|
|
}
|
|
const adapter = liveDynRefAdapter(emitter, v.type);
|
|
const rc = vAdapters(v.type);
|
|
const key = typeKey(v.type);
|
|
const keyLit = cStringLiteral(Buffer.from(key, "utf8"));
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_dyn_new_typed_ref(${v.name}, &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(key, "utf8")}, &${adapter.snapshot}, ${adapter.commit})`,
|
|
);
|
|
}
|
|
if (v.type.kind === "func") {
|
|
// A closure boxes as the checked-dynamic tree's function kind: retained closure +
|
|
// the per-signature call thunk + the interned signature key. The
|
|
// best-effort name rides along for inspect/error rendering (NULL
|
|
// when the lowering had none).
|
|
const name =
|
|
e.fnName !== undefined && e.fnName !== ""
|
|
? cStringLiteral(Buffer.from(e.fnName, "utf8"))
|
|
: "NULL";
|
|
return emitter.newTemp(e.type, `${emitter.dynFuncBoxHelper(v.type)}(${v.name}, ${name})`);
|
|
}
|
|
return emitter.newTemp(e.type, `${emitter.toDynHelper(v.type)}(${v.name})`);
|
|
}
|
|
case "dynFromJsval": {
|
|
// Island value → dyn: the by-reference wrap (scr_dyn_from_jsval
|
|
// retains the cell in; engine scalars normalize to native dyn
|
|
// kinds at wrap time). Operand borrowed, result +1, never throws.
|
|
const v = emitter.emitExpr(e.value);
|
|
return emitter.newTemp(e.type, `scr_dyn_from_jsval(${v.name})`);
|
|
}
|
|
case "dynCall": {
|
|
// Calling a dyn value: args are already dyn (the lowering boxed or
|
|
// converted them); everything is BORROWED by scr_dyn_call — the
|
|
// boxed thunk builds its own typed copies — so the temps release
|
|
// with the frame as usual. The callee's source spelling rides
|
|
// along for Node's "<name> is not a function" TypeError.
|
|
const callee = emitter.emitExpr(e.callee);
|
|
const receiver = e.receiver === undefined ? null : emitter.emitExpr(e.receiver);
|
|
const invoke = (call: string): Temp => {
|
|
emitter.line(`scr_dyn_this_push_dyn(${receiver?.name ?? "NULL"});`);
|
|
const result = emitter.newTemp(e.type, call);
|
|
emitter.line("scr_dyn_this_pop();");
|
|
emitter.emitPendingCheck();
|
|
return result;
|
|
};
|
|
const what = cStringLiteral(Buffer.from(e.calleeName, "utf8"));
|
|
if (e.spreads !== undefined && e.spreads.length > 0) {
|
|
// The RUNTIME-ARITY form (`f(...args)`): one fresh dyn array
|
|
// collects the arguments left-to-right — plain args move in
|
|
// (push takes ownership), spread args FLATTEN (push_spread
|
|
// retains elements in and throws V8's spread-call TypeError for
|
|
// non-iterable dyn kinds, checked per spread — JS's
|
|
// ArgumentListEvaluation order) — then apply calls through the
|
|
// array's elements (borrowed, exactly scr_dyn_call).
|
|
const spreadAt = new Map(e.spreads.map((s) => [s.arg, s.what]));
|
|
const pack = emitter.newTemp(DYN, "scr_dyn_new_arr()");
|
|
e.args.forEach((a, i) => {
|
|
const v = emitter.emitExpr(a);
|
|
const spreadWhat = spreadAt.get(i);
|
|
if (spreadWhat !== undefined) {
|
|
const w = cStringLiteral(Buffer.from(spreadWhat, "utf8"));
|
|
emitter.line(`scr_dyn_arr_push_spread(${pack.name}, ${v.name}, ${w});`);
|
|
emitter.emitPendingCheck();
|
|
} else {
|
|
emitter.moveTemp(v);
|
|
emitter.line(`scr_dyn_arr_push(${pack.name}, ${v.name});`);
|
|
}
|
|
});
|
|
return invoke(`scr_dyn_apply(${callee.name}, ${pack.name}, ${what})`);
|
|
}
|
|
const args = e.args.map((a) => emitter.emitExpr(a));
|
|
let argsExpr = "NULL";
|
|
if (args.length > 0) {
|
|
const arr = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`ScrDyn *${arr}[${args.length}] = { ${args.map((a) => a.name).join(", ")} };`);
|
|
argsExpr = arr;
|
|
}
|
|
return invoke(`scr_dyn_call(${callee.name}, ${argsExpr}, ${args.length}, ${what})`);
|
|
}
|
|
case "dynInvoke": {
|
|
// Prototype-method dispatch on a dyn receiver: everything is
|
|
// BORROWED by scr_dyn_invoke (temps release with the frame); the
|
|
// result is owned and may ride a pending exception.
|
|
const recv = emitter.emitExpr(e.recv);
|
|
const args = e.args.map((a) => emitter.emitExpr(a));
|
|
let argsExpr = "NULL";
|
|
if (args.length > 0) {
|
|
const arr = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`ScrDyn *${arr}[${args.length}] = { ${args.map((a) => a.name).join(", ")} };`);
|
|
argsExpr = arr;
|
|
}
|
|
const method = cStringLiteral(Buffer.from(e.method, "utf8"));
|
|
const what = cStringLiteral(Buffer.from(e.calleeName, "utf8"));
|
|
return emitter.fallibleTemp(
|
|
e.type,
|
|
`scr_dyn_invoke(${recv.name}, ${method}, ${argsExpr}, ${args.length}, ${what})`,
|
|
);
|
|
}
|
|
case "dynArrLit": {
|
|
// A dyn array built element-by-element: ownership of each dyn
|
|
// element MOVES into the array (scr_dyn_arr_push's contract).
|
|
const arr = emitter.newTemp(e.type, "scr_dyn_new_arr()");
|
|
for (const el of e.elems) {
|
|
const v = emitter.emitExpr(el);
|
|
emitter.moveTemp(v);
|
|
emitter.line(`scr_dyn_arr_push(${arr.name}, ${v.name});`);
|
|
}
|
|
return arr;
|
|
}
|
|
case "dynObjLit": {
|
|
// A dyn object built member-by-member: key then value, source
|
|
// order (JS's literal evaluation). scr_dyn_key_set BORROWS all
|
|
// three (the member retains the value in), so key/value temps
|
|
// release with the frame as usual; the receiver is a fresh OBJ,
|
|
// so the non-object throw paths are unreachable here.
|
|
const obj = emitter.newTemp(e.type, "scr_dyn_new_obj()");
|
|
for (const f of e.fields ?? []) {
|
|
const k = emitter.emitExpr(f.key);
|
|
const v = emitter.emitExpr(f.value);
|
|
emitter.line(`scr_dyn_key_set(${obj.name}, ${k.name}, ${v.name});`);
|
|
}
|
|
return obj;
|
|
}
|
|
case "unionWrap": {
|
|
// Construct a fresh immutable tagged box. Ownership of a refcounted
|
|
// payload MOVES into the union (which stores the arm's _v RC entry
|
|
// points so a generic release can free it); scalars ride the slot.
|
|
// Unit arms (undefined/null) carry NO payload: every wrap yields
|
|
// THE interned immortal instance for this (union, tag) — no
|
|
// allocation, and the frame's release is a no-op (rc == SIZE_MAX,
|
|
// which the RC entry points and the cycle collector both skip).
|
|
const arm = e.value.type;
|
|
if (isUnitType(arm)) {
|
|
return emitter.newTemp(e.type, emitter.unitInstanceRef(e.unionId, e.tag));
|
|
}
|
|
// A VOID payload (a void call wrapping into an undefined arm):
|
|
// evaluate for effects, produce the interned unit instance.
|
|
if (arm.kind === "void") {
|
|
emitter.emitExpr(e.value);
|
|
return emitter.newTemp(e.type, emitter.unitInstanceRef(e.unionId, e.tag));
|
|
}
|
|
const v = emitter.emitExpr(e.value);
|
|
if (isRefCounted(arm)) {
|
|
emitter.moveTemp(v);
|
|
const rc = vAdapters(arm);
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_union_new_ref(${e.tag}, ${v.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(arm)})`,
|
|
);
|
|
}
|
|
if (arm.kind === "f64") return emitter.newTemp(e.type, `scr_union_new_f64(${e.tag}, ${v.name})`);
|
|
if (arm.kind === "bool") return emitter.newTemp(e.type, `scr_union_new_bool(${e.tag}, ${v.name})`);
|
|
throw new InternalCompilerError(`emitter bug: unionWrap of ${arm.kind}`);
|
|
}
|
|
case "unionNarrow": {
|
|
// Tag-UNCHECKED payload extraction: the frontend emits this only
|
|
// where tsc's control-flow narrowing proved the tag (docs/ir.md).
|
|
// Ref payloads come out +1 via the arm's CONCRETE retain (statically
|
|
// known here — no need for the stored fn ptr); the union temp itself
|
|
// releases with this statement's frame as usual.
|
|
const u = emitter.emitExpr(e.value);
|
|
const arm = e.type;
|
|
if (isUnitType(arm)) throw new InternalCompilerError(`emitter bug: unionNarrow to unit arm ${arm.kind}`);
|
|
if (arm.kind === "f64") return emitter.newTemp(arm, `scr_union_get_f64(${u.name})`);
|
|
if (arm.kind === "bool") return emitter.newTemp(arm, `scr_union_get_bool(${u.name})`);
|
|
const payload = `(${cType(arm).trim()})scr_union_peek(${u.name})`;
|
|
return emitter.newTemp(arm, retainCallC(arm, payload));
|
|
}
|
|
case "unionDisc": {
|
|
// Shared-field read `r.kind` / `spec.config`: switch on the runtime
|
|
// tag and read the (same-typed) field from the concretely-cast
|
|
// payload. Ref-counted results come out retained (+1), owned by
|
|
// this frame.
|
|
const u = emitter.emitExpr(e.value);
|
|
const def = emitter.unionsById.get(e.unionId);
|
|
if (!def) throw new InternalCompilerError(`emitter bug: unionDisc of unknown union ${e.unionId}`);
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.type, name)};`);
|
|
emitter.line(`switch (${u.name}->tag) {`);
|
|
emitter.indent++;
|
|
def.arms.forEach((arm, i) => {
|
|
if (arm.kind !== "record" && arm.kind !== "object") {
|
|
throw new InternalCompilerError(`emitter bug: unionDisc arm of kind ${arm.kind}`);
|
|
}
|
|
// cType names the arm's struct (incl. the runtime ScrError for
|
|
// builtin error arms, whose members are unmangled).
|
|
const member =
|
|
arm.kind === "object" && RUNTIME_ERROR_CLASSES.has(arm.className)
|
|
? e.field
|
|
: mangleField(e.field);
|
|
const read = `((${cType(arm).trim()})scr_union_peek(${u.name}))->${member}`;
|
|
const value = isRefCounted(e.type) ? retainCallC(e.type, read) : read;
|
|
emitter.line(`case ${i}: ${name} = ${value}; break;`);
|
|
});
|
|
emitter.line(`default: scr_trap("scriptc: internal error: invalid union tag\\n");`);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type });
|
|
return { name, type: e.type };
|
|
}
|
|
case "unionKeyGet": {
|
|
// The unionDisc generalization: switch on the runtime tag; each arm
|
|
// answers at the JOIN type — a declared field reads its slot
|
|
// (wrapping an arm-typed answer into the join), an index-signature
|
|
// arm goes through the shared keyed-read helper (owned result,
|
|
// missing-key policy included), and a unit arm answers the interned
|
|
// undefined arm (the optional-chain tail's short-circuit value).
|
|
const u = emitter.emitExpr(e.value);
|
|
const k = emitter.emitExpr(e.key);
|
|
const def = emitter.unionsById.get(e.unionId);
|
|
if (!def) throw new InternalCompilerError(`emitter bug: unionKeyGet of unknown union ${e.unionId}`);
|
|
const resultDef = e.type.kind === "union" ? emitter.unionsById.get(e.type.unionId) : undefined;
|
|
const literal = e.key.kind === "strLit" ? e.key.value : null;
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.type, name)};`);
|
|
emitter.line(`switch (${u.name}->tag) {`);
|
|
emitter.indent++;
|
|
def.arms.forEach((arm, i) => {
|
|
if (isUnitType(arm)) {
|
|
const tag = resultDef?.arms.findIndex((a) => a.kind === "undefinedT") ?? -1;
|
|
if (tag < 0 || e.type.kind !== "union") {
|
|
throw new InternalCompilerError("emitter bug: unionKeyGet unit arm without an undefined result arm");
|
|
}
|
|
emitter.line(`case ${i}: ${name} = ${emitter.unitInstanceRef(e.type.unionId, tag)}; break;`);
|
|
return;
|
|
}
|
|
if (arm.kind === "array") {
|
|
// A NUMBER-keyed element read (the chain-tail `u?.split(":")[0]`
|
|
// form): the runtime getter answers owned (+1 for ref
|
|
// elements); invalid indices trap (divergence 4). The result
|
|
// wraps into the join when unit arms widened it.
|
|
const read = `scr_arr_get_${elemAccess(arm.elem)}((${cType(arm).trim()})scr_union_peek(${u.name}), ${k.name})`;
|
|
if (typeEquals(arm.elem, e.type)) {
|
|
emitter.line(`case ${i}: ${name} = ${read}; break;`);
|
|
return;
|
|
}
|
|
if (arm.elem.kind === "union" && e.type.kind === "union") {
|
|
const helper = unionWidenHelper(emitter, arm.elem.unionId, e.type.unionId);
|
|
emitter.line(`case ${i}: { ScrUnion *hit = ${read}; ${name} = ${helper}(hit); scr_union_release(hit); break; }`);
|
|
return;
|
|
}
|
|
const tag = resultDef?.arms.findIndex((a) => typeEquals(a, arm.elem)) ?? -1;
|
|
if (tag < 0 || e.type.kind !== "union" || isUnitType(arm.elem)) {
|
|
throw new InternalCompilerError(`emitter bug: unionKeyGet element ${arm.elem.kind} outside the join`);
|
|
}
|
|
const wrapped =
|
|
arm.elem.kind === "f64"
|
|
? `scr_union_new_f64(${tag}, ${read})`
|
|
: arm.elem.kind === "bool"
|
|
? `scr_union_new_bool(${tag}, ${read})`
|
|
: (() => {
|
|
const rc = vAdapters(arm.elem);
|
|
// The element read is already owned (+1) — ownership
|
|
// MOVES into the union box, no extra retain.
|
|
return `scr_union_new_ref(${tag}, ${read}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(arm.elem)})`;
|
|
})();
|
|
emitter.line(`case ${i}: ${name} = ${wrapped}; break;`);
|
|
return;
|
|
}
|
|
if (arm.kind !== "record") throw new InternalCompilerError(`emitter bug: unionKeyGet arm of kind ${arm.kind}`);
|
|
const shape = emitter.recordsById.get(arm.shapeId);
|
|
if (!shape) throw new InternalCompilerError(`emitter bug: unionKeyGet arm of unknown shape ${arm.shapeId}`);
|
|
const declared = literal !== null ? shape.fields.find((f) => f.name === literal) : undefined;
|
|
if (declared) {
|
|
const ft = declared.type;
|
|
const read = `((${cType(arm).trim()})scr_union_peek(${u.name}))->${mangleField(declared.name)}`;
|
|
if (typeEquals(ft, e.type)) {
|
|
emitter.line(`case ${i}: ${name} = ${isRefCounted(ft) ? retainCallC(ft, read) : read}; break;`);
|
|
return;
|
|
}
|
|
if (ft.kind === "union" && e.type.kind === "union") {
|
|
const helper = unionWidenHelper(emitter, ft.unionId, e.type.unionId);
|
|
emitter.line(`case ${i}: ${name} = ${helper}(${read}); break;`);
|
|
return;
|
|
}
|
|
const tag = resultDef?.arms.findIndex((a) => typeEquals(a, ft)) ?? -1;
|
|
if (tag < 0 || e.type.kind !== "union" || isUnitType(ft)) {
|
|
throw new InternalCompilerError(`emitter bug: unionKeyGet arm answer ${ft.kind} outside the join`);
|
|
}
|
|
const wrapped =
|
|
ft.kind === "f64"
|
|
? `scr_union_new_f64(${tag}, ${read})`
|
|
: ft.kind === "bool"
|
|
? `scr_union_new_bool(${tag}, ${read})`
|
|
: (() => {
|
|
const rc = vAdapters(ft);
|
|
return `scr_union_new_ref(${tag}, ${retainCallC(ft, read)}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(ft)})`;
|
|
})();
|
|
emitter.line(`case ${i}: ${name} = ${wrapped}; break;`);
|
|
return;
|
|
}
|
|
// Index-signature arm (or declared-only shape under a runtime
|
|
// key): the per-(shape, join type) keyed-read helper — a literal
|
|
// key naming no declared field touches only the overflow map.
|
|
const helper = emitter.recordKeyGetHelper(arm.shapeId, e.type, literal !== null && !!shape.indexValue);
|
|
emitter.line(`case ${i}: ${name} = ${helper}((${cType(arm).trim()})scr_union_peek(${u.name}), ${k.name}); break;`);
|
|
});
|
|
emitter.line(`default: scr_trap("scriptc: internal error: invalid union tag\\n");`);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type });
|
|
return { name, type: e.type };
|
|
}
|
|
case "unionIsTag": {
|
|
// A pure tag compare — the box is borrowed, no payload is touched.
|
|
const u = emitter.emitExpr(e.value);
|
|
return emitter.newTemp(e.type, `${u.name}->tag ${e.negated ? "!=" : "=="} ${e.tag}`);
|
|
}
|
|
case "dynKeyGet": {
|
|
// Keyed read on the checked-dynamic tree through the one interned helper — the
|
|
// non-optional form throws JS's TypeError on an undefined/null
|
|
// receiver, and HANDLE receivers can throw the loud unmodeled-
|
|
// property ladder on EITHER form, so both ride a fallible temp;
|
|
// the result is owned (+1).
|
|
const d = emitter.emitExpr(e.value);
|
|
const k = emitter.emitExpr(e.key);
|
|
const helper = dynKeyGetHelper(emitter);
|
|
const call = `${helper}(${d.name}, ${k.name}, ${e.optional ? "true" : "false"})`;
|
|
return emitter.fallibleTemp(e.type, call);
|
|
}
|
|
case "dynHasKey": {
|
|
const value = emitter.emitExpr(e.value);
|
|
const key = emitter.emitExpr({ kind: "strLit", value: e.key, type: STRING, loc: e.loc });
|
|
const test = `scr_dyn_has_key(${value.name}, ${key.name})`;
|
|
return emitter.fallibleTemp(e.type, e.negated ? `!${test}` : test);
|
|
}
|
|
case "dynScalarEq": {
|
|
// dyn vs scalar strict equality: kind test + payload compare.
|
|
// Operands emit in SOURCE order (JS evaluation order); the dyn
|
|
// side is found by type. Both borrowed, no allocation.
|
|
const l = emitter.emitExpr(e.left);
|
|
const r = emitter.emitExpr(e.right);
|
|
const [d, s, st] = e.left.type.kind === "dyn" ? [l, r, e.right.type] : [r, l, e.left.type];
|
|
const test =
|
|
st.kind === "dyn"
|
|
? // dyn vs dyn: whole-dyn strict equality (scalars by value,
|
|
// units by kind, reference kinds by node identity).
|
|
`scr_dyn_strict_eq(${l.name}, ${r.name})`
|
|
: st.kind === "string"
|
|
? `(${d.name}->kind == SCR_DYN_STR && scr_str_eq(${d.name}->v.str, ${s.name}))`
|
|
: st.kind === "f64"
|
|
? `(${d.name}->kind == SCR_DYN_NUM && ${d.name}->v.num == ${s.name})`
|
|
: `(${d.name}->kind == SCR_DYN_BOOL && ${d.name}->v.b == ${s.name})`;
|
|
return emitter.newTemp(e.type, e.negated ? `!${test}` : test);
|
|
}
|
|
case "dynTest": {
|
|
// A pure kind compare on the dyn node — borrowed; only the truthy
|
|
// form also reads a scalar payload. ISLAND-held nodes (the jsval
|
|
// kind — engine objects/arrays/functions only, scalars normalize
|
|
// at wrap time) route the tests that depend on the engine's
|
|
// answer through the scr_dyn_isl_* helpers (false on every other
|
|
// kind, so the calls stay unconditional); narrowing never changes
|
|
// representation (SEMANTICS.md).
|
|
const d = emitter.emitExpr(e.value);
|
|
if (e.test === "buffer") {
|
|
const test = `(${d.name}->kind == SCR_DYN_BYTES && ${d.name}->buffer)`;
|
|
return emitter.newTemp(e.type, e.negated ? `!${test}` : test);
|
|
}
|
|
const test =
|
|
e.test === "nullish"
|
|
? `(${d.name}->kind == SCR_DYN_UNDEF || ${d.name}->kind == SCR_DYN_NULL)`
|
|
: e.test === "object"
|
|
? // `typeof v === "object"`: objects, arrays, bytes, native
|
|
// handles, promises, AND null — engine-held objects by the
|
|
// engine's own typeof.
|
|
`(${d.name}->kind == SCR_DYN_OBJ || ${d.name}->kind == SCR_DYN_ARR || ${d.name}->kind == SCR_DYN_BYTES || ${d.name}->kind == SCR_DYN_HANDLE || ${d.name}->kind == SCR_DYN_PROMISE || ${d.name}->kind == SCR_DYN_PROXY || ${d.name}->kind == SCR_DYN_NULL || scr_dyn_isl_typeof_is(${d.name}, "object"))`
|
|
: e.test === "truthy"
|
|
? // Runtime ToBoolean includes typed-reference capsules and
|
|
// keeps this backend in lockstep with LLVM.
|
|
`scr_dyn_truthy(${d.name})`
|
|
: e.test === "error"
|
|
? // `u instanceof Error`: the checked-dynamic tree's error encoding — an
|
|
// object carrying the reserved "%error" marker key
|
|
// (built by caughtToDyn for Error payloads) — or a real
|
|
// engine Error held by reference.
|
|
`((${d.name}->kind == SCR_DYN_OBJ && scr_dyn_obj_get(${d.name}, "%error", 6) != NULL) || scr_dyn_isl_is_error(${d.name}))`
|
|
: e.test === "array"
|
|
? // Array.isArray: the checked-dynamic tree's array kind, or the engine's
|
|
// own answer for an engine-held value.
|
|
`(${d.name}->kind == SCR_DYN_ARR || scr_dyn_isl_is_array(${d.name}))`
|
|
: e.test === "function"
|
|
? `(${d.name}->kind == SCR_DYN_FUNC || scr_dyn_isl_typeof_is(${d.name}, "function"))`
|
|
: `${d.name}->kind == ${
|
|
{ string: "SCR_DYN_STR", number: "SCR_DYN_NUM", boolean: "SCR_DYN_BOOL", undefined: "SCR_DYN_UNDEF", null: "SCR_DYN_NULL", bytes: "SCR_DYN_BYTES" }[e.test]
|
|
}`;
|
|
return emitter.newTemp(e.type, e.negated ? `!(${test})` : test);
|
|
}
|
|
case "unionEq": {
|
|
// Strict equality (or Object.is's SameValue — the f64 arm's
|
|
// compare is the one difference) of the ARM values via the
|
|
// per-union helper (tag compare + per-arm payload compare). Both
|
|
// boxes are borrowed.
|
|
const l = emitter.emitExpr(e.left);
|
|
const r = emitter.emitExpr(e.right);
|
|
const call = `${emitter.unionEqHelper(e.unionId, e.sameValue)}(${l.name}, ${r.name})`;
|
|
return emitter.newTemp(e.type, e.negated ? `!${call}` : call);
|
|
}
|
|
case "unionFuncEq": {
|
|
const u = emitter.emitExpr(e.union);
|
|
const f = emitter.emitExpr(e.func);
|
|
const test = `(${u.name}->tag == ${e.tag} && scr_union_peek(${u.name}) == ${f.name})`;
|
|
return emitter.newTemp(e.type, e.negated ? `!${test}` : test);
|
|
}
|
|
case "caughtTest": {
|
|
// Kind-tag tests read the snapshot directly; instanceof compares an
|
|
// OBJ payload's vtable preorder against the class's compile-time
|
|
// interval (false for every other payload kind). Box borrowed.
|
|
const c = emitter.emitExpr(e.value);
|
|
if (e.test === "instanceof") {
|
|
const target = emitter.classMeta.get(e.className!);
|
|
if (!target) throw new InternalCompilerError(`emitter bug: caughtTest against unknown class ${e.className}`);
|
|
const test = `scr_caught_instanceof(${c.name}, ${target.pre}, ${target.post})`;
|
|
return emitter.newTemp(e.type, e.negated ? `!${test}` : test);
|
|
}
|
|
if (e.test === "object") {
|
|
const test = `scr_caught_is_object(${c.name})`;
|
|
return emitter.newTemp(e.type, e.negated ? `!${test}` : test);
|
|
}
|
|
const tag = { string: "SCR_EXC_STR", number: "SCR_EXC_F64", boolean: "SCR_EXC_BOOL" }[e.test];
|
|
return emitter.newTemp(e.type, `${c.name}->kind ${e.negated ? "!=" : "=="} ${tag}`);
|
|
}
|
|
case "caughtCheck": {
|
|
// Checked payload extraction (`e as C`): instanceof match extracts
|
|
// +1, anything else throws the catchable TypeError — the result
|
|
// joins the frame BEFORE the pending check so an unwind releases
|
|
// the NULL dummy harmlessly. Box borrowed.
|
|
const c = emitter.emitExpr(e.value);
|
|
const target = emitter.classMeta.get(e.className);
|
|
if (!target) throw new InternalCompilerError(`emitter bug: caughtCheck against unknown class ${e.className}`);
|
|
const display = e.className.startsWith("%") ? e.className.slice(1) : e.className;
|
|
return emitter.fallibleTemp(
|
|
e.type,
|
|
`(${cType(e.type).trim()})scr_caught_check_obj(${c.name}, ${target.pre}, ${target.post}, ${cStringLiteral(Buffer.from(display, "utf8"))})`,
|
|
);
|
|
}
|
|
case "caughtNarrow": {
|
|
// Checker-trusted extraction (the matching caughtTest was proven by
|
|
// tsc's narrowing): scalars read the snapshot's slots, refcounted
|
|
// payloads come out retained (+1). Box borrowed.
|
|
const c = emitter.emitExpr(e.value);
|
|
if (e.type.kind === "f64") return emitter.newTemp(e.type, `${c.name}->f64`);
|
|
if (e.type.kind === "bool") return emitter.newTemp(e.type, `${c.name}->b`);
|
|
if (e.type.kind === "string") {
|
|
return emitter.newTemp(e.type, `scr_str_retain((ScrStr *)${c.name}->payload)`);
|
|
}
|
|
if (e.type.kind === "object") {
|
|
return emitter.newTemp(e.type, `(${cType(e.type).trim()})${c.name}->retain_fn(${c.name}->payload)`);
|
|
}
|
|
throw new InternalCompilerError(`emitter bug: caughtNarrow to ${e.type.kind}`);
|
|
}
|
|
case "caughtToDyn": {
|
|
// The caught snapshot converting to a dyn value (an unknown
|
|
// slot) — the interned runtime-kind dispatch. Box borrowed; the
|
|
// result is a fresh tree (+1). Never throws.
|
|
const c = emitter.emitExpr(e.value);
|
|
return emitter.newTemp(e.type, `${emitter.caughtToDynHelper()}(${c.name})`);
|
|
}
|
|
default: {
|
|
const _exhaustive: never = e;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
function emitIntrinsicExpr(
|
|
emitter: CEmitter,
|
|
e: ExprOf<"intrinsic">,
|
|
): Temp {
|
|
switch (e.kind) {
|
|
case "intrinsic": {
|
|
if (e.name === "module.await") {
|
|
// The module evaluator's dependency wait: park only while the
|
|
// promise is pending. A settled dependency continues in this
|
|
// turn (no JavaScript-await hop); a rejection rethrows.
|
|
const p = emitter.emitExpr(e.args[0]!);
|
|
emitter.line(`scr_module_await(${p.name});${emitter.srcComment(e.loc)}`);
|
|
emitter.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
if (e.name === "promise.all") {
|
|
// The runtime countdown combinator: a pre-sized values array
|
|
// (filled per INPUT index by the per-kind store helper as
|
|
// entries fulfill) plus one subscription per entry — settled
|
|
// entries settle inline, the first rejection wins, later
|
|
// rejections count as handled. A void inner type passes no
|
|
// values array and the result fulfills void.
|
|
if (e.type.kind !== "promise") throw new InternalCompilerError("emitter bug: promise.all type");
|
|
const entries = e.args[0]!;
|
|
if (entries.type.kind !== "array" || entries.type.elem.kind !== "promise") {
|
|
throw new InternalCompilerError("emitter bug: promise.all argument");
|
|
}
|
|
const ps = emitter.emitExpr(entries);
|
|
if (e.type.inner.kind === "void") {
|
|
return emitter.newTemp(e.type, `scr_promise_all(${ps.name}, NULL, NULL)`);
|
|
}
|
|
if (e.type.inner.kind !== "array") throw new InternalCompilerError("emitter bug: promise.all result");
|
|
const elem = e.type.inner.elem;
|
|
const store =
|
|
elem.kind === "f64"
|
|
? "scr_promise_all_store_f64"
|
|
: elem.kind === "bool"
|
|
? "scr_promise_all_store_bool"
|
|
: elem.kind === "string"
|
|
? "scr_promise_all_store_str"
|
|
: "scr_promise_all_store_ref";
|
|
// Entry array and values array both stay frame-owned; the
|
|
// combinator BORROWS them and retains what it keeps (the race
|
|
// convention).
|
|
const vals = emitter.newTemp(e.type.inner, emitter.arrNewC(elem, `(size_t)scr_arr_len(${ps.name})`));
|
|
return emitter.newTemp(e.type, `scr_promise_all(${ps.name}, ${vals.name}, &${store})`);
|
|
}
|
|
if (e.name === "promise.reject") {
|
|
// A fresh promise rejected through the exception cell: the
|
|
// %Error-rooted reason moves in as the cell's OBJ payload
|
|
// (exactly the thrown-Error representation — catch instanceof
|
|
// and the uncaught printer see a thrown Error), and
|
|
// reject_pending moves the cell into the promise, wakes
|
|
// nothing (no waiters can exist yet), and enters the unhandled
|
|
// ledger until an await/then observes it. The cell is consumed
|
|
// immediately, so no pending check runs in between.
|
|
if (e.type.kind !== "promise") throw new InternalCompilerError("emitter bug: promise.reject type");
|
|
const reason = emitter.emitExpr(e.args[0]!);
|
|
const t = e.args[0]!.type;
|
|
if (t.kind !== "object" && t.kind !== "dyn") {
|
|
throw new InternalCompilerError("emitter bug: promise.reject reason");
|
|
}
|
|
const p = emitter.newTemp(e.type, `scr_promise_new()`);
|
|
emitter.moveTemp(reason); // the cell takes ownership
|
|
const rc = vAdapters(t);
|
|
if (t.kind === "dyn") {
|
|
// The thrown-dyn representation (REF + dyn adapters): catch
|
|
// bindings and the unhandled dispatch see the dyn value
|
|
// itself — identity preserved.
|
|
emitter.line(
|
|
`scr_throw_ref_classified(${reason.name}, &${rc.retain}, &${rc.release}, NULL, scr_dyn_is_object(${reason.name}));${emitter.srcComment(e.loc)}`,
|
|
);
|
|
} else {
|
|
emitter.line(
|
|
`scr_throw_obj(${reason.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`,
|
|
);
|
|
}
|
|
emitter.line(`scr_promise_reject_pending(${p.name});`);
|
|
return p;
|
|
}
|
|
if (e.name === "promise.resolve") {
|
|
// A fresh promise fulfilled immediately: void/f64/bool by
|
|
// value, strings and refs MOVE in (fulfill takes the +1 — the
|
|
// moveTemp keeps the frame from double-releasing), matching the
|
|
// async-return trampoline's fulfill exactly. No waiters exist
|
|
// yet, so the wake is a no-op.
|
|
if (e.type.kind !== "promise") throw new InternalCompilerError("emitter bug: promise.resolve type");
|
|
const p = emitter.newTemp(e.type, `scr_promise_new()`);
|
|
if (e.args.length === 0) {
|
|
emitter.line(`scr_promise_fulfill_void(${p.name});${emitter.srcComment(e.loc)}`);
|
|
return p;
|
|
}
|
|
const v = emitter.emitExpr(e.args[0]!);
|
|
const t = e.args[0]!.type;
|
|
switch (t.kind) {
|
|
case "f64":
|
|
case "date":
|
|
emitter.line(`scr_promise_fulfill_f64(${p.name}, ${v.name});${emitter.srcComment(e.loc)}`);
|
|
break;
|
|
case "bool":
|
|
emitter.line(`scr_promise_fulfill_bool(${p.name}, ${v.name});${emitter.srcComment(e.loc)}`);
|
|
break;
|
|
case "string":
|
|
emitter.moveTemp(v);
|
|
emitter.line(`scr_promise_fulfill_str(${p.name}, ${v.name});${emitter.srcComment(e.loc)}`);
|
|
break;
|
|
default: {
|
|
const rc = vAdapters(t);
|
|
emitter.moveTemp(v);
|
|
emitter.line(
|
|
`scr_promise_fulfill_ref(${p.name}, ${v.name}, ${rc.retain}, ${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`,
|
|
);
|
|
}
|
|
}
|
|
return p;
|
|
}
|
|
if (e.name === "promise.race") {
|
|
// A fresh result promise + one race_add per entry: settled
|
|
// entries settle it immediately (first add wins), pending ones
|
|
// park a callback waiter. The interned adapter converts each
|
|
// entry's payload to the result's inner type; entry temps stay
|
|
// frame-owned (race_add retains what it keeps).
|
|
if (e.type.kind !== "promise") throw new InternalCompilerError("emitter bug: promise.race type");
|
|
const result = emitter.newTemp(e.type, `scr_promise_new()`);
|
|
for (const entry of e.args) {
|
|
if (entry.type.kind !== "promise") throw new InternalCompilerError("emitter bug: promise.race entry");
|
|
const p = emitter.emitExpr(entry);
|
|
const adapter = emitter.raceAdapterFor(entry.type.inner, e.type.inner);
|
|
emitter.line(`scr_promise_race_add(${result.name}, ${p.name}, &${adapter});${emitter.srcComment(e.loc)}`);
|
|
}
|
|
return result;
|
|
}
|
|
// console.log and its stderr twin console.error share the ScrLogArg
|
|
// packing; only the runtime entry point (stream) differs.
|
|
const args = e.args.map((a) => emitter.emitExpr(a));
|
|
const arr = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`ScrLogArg ${arr}[${Math.max(args.length, 1)}];`);
|
|
args.forEach((a, i) => {
|
|
switch (a.type.kind) {
|
|
case "f64":
|
|
emitter.line(`${arr}[${i}].tag = SCR_ARG_F64; ${arr}[${i}].v.f = ${a.name};`);
|
|
break;
|
|
case "string":
|
|
emitter.line(`${arr}[${i}].tag = SCR_ARG_STR; ${arr}[${i}].v.s = ${a.name};`);
|
|
break;
|
|
case "bool":
|
|
emitter.line(`${arr}[${i}].tag = SCR_ARG_BOOL; ${arr}[${i}].v.b = ${a.name};`);
|
|
break;
|
|
default:
|
|
throw new InternalCompilerError(`${e.name} arg of type ${a.type.kind}`);
|
|
}
|
|
});
|
|
const consoleFn = e.name === "console.error" ? "scr_console_error" : "scr_console_log";
|
|
emitter.line(`${consoleFn}(${args.length}, ${arr});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
default: {
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
function emitSerializationExpr(
|
|
emitter: CEmitter,
|
|
e: ExprOf<"jsonStringify" | "dynCheck">,
|
|
): Temp {
|
|
switch (e.kind) {
|
|
case "jsonStringify": {
|
|
// Type-directed serialization: the STATIC type picks an emitted
|
|
// serializer (interned per type) — no dyn, no runtime dispatch. The
|
|
// value temp is BORROWED (released with this statement's frame);
|
|
// the result string is owned (+1). Throws only over CYCLE-CAPABLE
|
|
// types (recursive records — the circular-structure TypeError) and
|
|
// dyn roots; everything else keeps the throw-free path.
|
|
const v = emitter.emitExpr(e.value);
|
|
// A dyn root: the runtime's dyn walker (scr_dyn_format_j — the %j
|
|
// serializer IS JSON.stringify over the checked-dynamic tree: number/string/bool/
|
|
// null/array/object exact, dropped members omitted, a dropped ROOT
|
|
// becomes the text "undefined", a runtime handle inside the tree
|
|
// throws) — fallible, so the pending-exception check runs.
|
|
const compact =
|
|
e.value.type.kind === "dyn"
|
|
? emitter.fallibleTemp(e.type, `scr_dyn_format_j(${v.name})`)
|
|
: (() => {
|
|
const helper = emitter.jsonWriteHelper(e.value.type);
|
|
const buf = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`ScrJsonBuf ${buf}; scr_jb_init(&${buf});${emitter.srcComment(e.loc)}`);
|
|
emitter.line(`${helper}(&${buf}, ${v.name});`);
|
|
const t = emitter.newTemp(e.type, `scr_jb_finish(&${buf})`);
|
|
// A cycle-capable root can throw the circular-structure
|
|
// TypeError mid-walk: finish still runs (frees the buffer,
|
|
// the partial string joins the frame and releases on
|
|
// unwind), then the pending check unwinds.
|
|
if (emitter.traceAdapterC(e.value.type) !== null) emitter.emitPendingCheck();
|
|
return t;
|
|
})();
|
|
// A pretty-print form (`stringify(v, null, 2)`): the frontend
|
|
// resolved the space to a compile-time indent string (Node's
|
|
// clamp/truncate rules) riding as an extra property; the interned
|
|
// re-indenter rewrites the compact text with Node's gap algorithm.
|
|
// Compact temp stays frame-owned; the pretty string is a fresh +1.
|
|
const indent = (e as { indent?: string }).indent;
|
|
if (!indent) return compact;
|
|
const bytes = Buffer.from(indent, "utf8");
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`${emitter.jsonIndentHelper()}(${compact.name}, ${cStringLiteral(bytes)}, ${bytes.length})`,
|
|
);
|
|
}
|
|
case "dynCheck": {
|
|
// The dynamic boundary: validate the checked-dynamic tree against the target type
|
|
// and BUILD the typed value (+1) — or throw a catchable
|
|
// TypeError-shaped, path-annotated string. The dyn temp is BORROWED;
|
|
// the result joins the frame BEFORE the pending check so an unwind
|
|
// releases the dummy (NULL for refcounted targets) harmlessly.
|
|
const dyn = emitter.emitExpr(e.value);
|
|
const helper = emitter.dynCheckHelper(e.type);
|
|
return emitter.fallibleTemp(e.type, `${helper}(${dyn.name}, NULL)`);
|
|
}
|
|
default: {
|
|
const _exhaustive: never = e;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
function emitAsyncExpr(
|
|
emitter: CEmitter,
|
|
e: ExprOf<"yieldExpr" | "genResume" | "awaitExpr" | "awaitUnionExpr" | "newPromise" | "promiseWithResolvers">,
|
|
): Temp {
|
|
switch (e.kind) {
|
|
case "yieldExpr": {
|
|
// Park the operand in the generator's OUT slot (moved in, typed by
|
|
// the function's yield channel) and switch back to the resumer.
|
|
// Control returns at the next resume — possibly with an injected
|
|
// .throw payload or the GENRET sentinel pending, hence the check.
|
|
// The result is the .next(v) argument, moved out of the IN slot.
|
|
const gen = emitter.currentGenerator;
|
|
if (!gen) throw new InternalCompilerError("emitter bug: yieldExpr outside a generator body");
|
|
if (e.value === null) throw new InternalCompilerError("emitter bug: yieldExpr with no operand (frontend fills undefined)");
|
|
const v = emitter.emitExpr(e.value);
|
|
const yt = e.value.type;
|
|
if (e.awaited) emitter.line(`scr_async_gen_hop_done();`);
|
|
if (yt.kind === "f64" || yt.kind === "date") {
|
|
emitter.line(`scr_gen_yield_f64(${v.name});${emitter.srcComment(e.loc)}`);
|
|
} else if (yt.kind === "bool") {
|
|
emitter.line(`scr_gen_yield_bool(${v.name});${emitter.srcComment(e.loc)}`);
|
|
} else {
|
|
emitter.moveTemp(v); // the OUT slot takes ownership
|
|
emitter.line(`scr_gen_yield_ref(${v.name}, ${vAdapters(yt).release});${emitter.srcComment(e.loc)}`);
|
|
}
|
|
emitter.emitPendingCheck();
|
|
switch (e.type.kind) {
|
|
case "void":
|
|
// An undefined next-channel: nothing to read (the frontend
|
|
// fences value-position yields on this channel).
|
|
return { name: "", type: e.type };
|
|
case "f64":
|
|
case "date":
|
|
return emitter.newTemp(e.type, `scr_gen_take_in_f64()`);
|
|
case "bool":
|
|
return emitter.newTemp(e.type, `scr_gen_take_in_bool()`);
|
|
default:
|
|
// Refcounted channels (dyn included): the slot's +1 moves out.
|
|
return emitter.newTemp(e.type, `(${cType(e.type).trim()})scr_gen_take_in_ref()`);
|
|
}
|
|
}
|
|
case "genResume": {
|
|
// One consumer resume: park the sent value (typed per mode), hop
|
|
// into the fiber, propagate a body exception (pending check), and
|
|
// build the IteratorResult record through the interned helper.
|
|
const genT = e.gen.type;
|
|
if (genT.kind !== "generator") throw new InternalCompilerError("emitter bug: genResume on a non-generator");
|
|
const resultT = genT.async ? (e.type.kind === "promise" ? e.type.inner : null) : e.type;
|
|
if (resultT?.kind !== "record") throw new InternalCompilerError("emitter bug: genResume result is not an IteratorResult record");
|
|
const g = emitter.emitExpr(e.gen); // borrowed for the calls below
|
|
const sendArg = (store: (a: Temp) => string): void => {
|
|
const a = emitter.emitExpr(e.arg!);
|
|
if (isRefCounted(e.arg!.type)) emitter.moveTemp(a); // the slot takes ownership
|
|
emitter.line(store(a));
|
|
};
|
|
if (genT.async) {
|
|
let call: string;
|
|
if (e.mode === "next") {
|
|
if (e.arg === null) {
|
|
if (genT.nextT.kind === "dyn") {
|
|
call = `scr_async_gen_next_ref(${g.name}, scr_dyn_retain(scr_dyn_undefined()), scr_dyn_release_v)`;
|
|
} else {
|
|
call = `scr_async_gen_next_none(${g.name})`;
|
|
}
|
|
} else {
|
|
const a = emitter.emitExpr(e.arg);
|
|
const t = e.arg.type;
|
|
if (isRefCounted(t)) emitter.moveTemp(a);
|
|
call = t.kind === "f64" || t.kind === "date"
|
|
? `scr_async_gen_next_f64(${g.name}, ${a.name})`
|
|
: t.kind === "bool"
|
|
? `scr_async_gen_next_bool(${g.name}, ${a.name})`
|
|
: `scr_async_gen_next_ref(${g.name}, ${a.name}, ${vAdapters(t).release})`;
|
|
}
|
|
} else if (e.mode === "return") {
|
|
if (e.arg === null) {
|
|
call = `scr_async_gen_return_none(${g.name})`;
|
|
} else {
|
|
const a = emitter.emitExpr(e.arg);
|
|
const t = e.arg.type;
|
|
if (isRefCounted(t)) emitter.moveTemp(a);
|
|
call = t.kind === "f64" || t.kind === "date"
|
|
? `scr_async_gen_return_f64(${g.name}, ${a.name})`
|
|
: t.kind === "bool"
|
|
? `scr_async_gen_return_bool(${g.name}, ${a.name})`
|
|
: `scr_async_gen_return_ref(${g.name}, ${a.name}, ${vAdapters(t).release})`;
|
|
}
|
|
} else {
|
|
if (e.arg === null) throw new InternalCompilerError("emitter bug: async genResume throw with no payload");
|
|
const a = emitter.emitExpr(e.arg);
|
|
const t = e.arg.type;
|
|
if (isRefCounted(t)) emitter.moveTemp(a);
|
|
if (t.kind === "date") {
|
|
throw new InternalCompilerError("emitter bug: Date async-generator throw reached backend");
|
|
} else if (t.kind === "f64") {
|
|
emitter.line(`scr_throw_f64(${a.name});${emitter.srcComment(e.loc)}`);
|
|
} else if (t.kind === "bool") {
|
|
emitter.line(`scr_throw_bool(${a.name});${emitter.srcComment(e.loc)}`);
|
|
} else if (t.kind === "string") {
|
|
emitter.line(`scr_throw_str(${a.name});${emitter.srcComment(e.loc)}`);
|
|
} else if (t.kind === "object" && emitter.classMeta.get(t.className)?.hierarchy) {
|
|
const rc = vAdapters(t);
|
|
emitter.line(`scr_throw_obj(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`);
|
|
} else if (t.kind === "symbol" || t.kind === "bigint" || t.kind === "func" || t.kind === "classval") {
|
|
const rc = vAdapters(t);
|
|
emitter.line(`scr_throw_primitive_ref(${a.name}, &${rc.retain}, &${rc.release}, NULL);${emitter.srcComment(e.loc)}`);
|
|
} else if (t.kind === "dyn" || t.kind === "jsval") {
|
|
const rc = vAdapters(t);
|
|
const test = t.kind === "dyn" ? "scr_dyn_is_object" : "scr_jsval_is_object";
|
|
emitter.line(`scr_throw_ref_classified(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)}, ${test}(${a.name}));${emitter.srcComment(e.loc)}`);
|
|
} else {
|
|
const rc = vAdapters(t);
|
|
emitter.line(`scr_throw_ref(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`);
|
|
}
|
|
call = `scr_async_gen_throw(${g.name})`;
|
|
}
|
|
return emitter.newTemp(e.type, call);
|
|
}
|
|
if (e.mode === "next") {
|
|
if (e.arg === null) {
|
|
// Valueless resume: dyn channels read JS's undefined; unit
|
|
// channels have nothing to read.
|
|
if (genT.nextT.kind === "dyn") {
|
|
emitter.line(`scr_gen_in_ref(${g.name}, scr_dyn_retain(scr_dyn_undefined()), scr_dyn_release_v);${emitter.srcComment(e.loc)}`);
|
|
} else {
|
|
emitter.line(`scr_gen_in_none(${g.name});${emitter.srcComment(e.loc)}`);
|
|
}
|
|
} else {
|
|
const nt = e.arg.type;
|
|
sendArg((a) =>
|
|
nt.kind === "f64" || nt.kind === "date" ? `scr_gen_in_f64(${g.name}, ${a.name});${emitter.srcComment(e.loc)}`
|
|
: nt.kind === "bool" ? `scr_gen_in_bool(${g.name}, ${a.name});${emitter.srcComment(e.loc)}`
|
|
: `scr_gen_in_ref(${g.name}, ${a.name}, ${vAdapters(nt).release});${emitter.srcComment(e.loc)}`);
|
|
}
|
|
emitter.line(`scr_gen_resume(${g.name});`);
|
|
} else if (e.mode === "return") {
|
|
if (e.arg === null) {
|
|
emitter.line(`scr_gen_ret_none(${g.name});${emitter.srcComment(e.loc)}`);
|
|
} else {
|
|
const rt = e.arg.type;
|
|
sendArg((a) =>
|
|
rt.kind === "f64" || rt.kind === "date" ? `scr_gen_ret_f64(${g.name}, ${a.name});${emitter.srcComment(e.loc)}`
|
|
: rt.kind === "bool" ? `scr_gen_ret_bool(${g.name}, ${a.name});${emitter.srcComment(e.loc)}`
|
|
: `scr_gen_ret_ref(${g.name}, ${a.name}, ${vAdapters(rt).release});${emitter.srcComment(e.loc)}`);
|
|
}
|
|
emitter.line(`scr_gen_resume_return(${g.name});`);
|
|
} else {
|
|
// .throw(e): park the payload in the CALLER's cell (the throw
|
|
// statement's exact kind dispatch), then resume — the runtime
|
|
// moves it into the fiber, or leaves it pending (non-suspended
|
|
// generators: the .throw call itself throws at the check below).
|
|
if (e.arg === null) throw new InternalCompilerError("emitter bug: genResume throw with no payload");
|
|
const a = emitter.emitExpr(e.arg);
|
|
const t = e.arg.type;
|
|
if (isRefCounted(t)) emitter.moveTemp(a); // the cell takes ownership
|
|
if (t.kind === "date") {
|
|
throw new InternalCompilerError("emitter bug: Date generator throw reached backend");
|
|
} else if (t.kind === "f64") {
|
|
emitter.line(`scr_throw_f64(${a.name});${emitter.srcComment(e.loc)}`);
|
|
} else if (t.kind === "bool") {
|
|
emitter.line(`scr_throw_bool(${a.name});${emitter.srcComment(e.loc)}`);
|
|
} else if (t.kind === "string") {
|
|
emitter.line(`scr_throw_str(${a.name});${emitter.srcComment(e.loc)}`);
|
|
} else if (t.kind === "object" && emitter.classMeta.get(t.className)?.hierarchy) {
|
|
const rc = vAdapters(t);
|
|
emitter.line(`scr_throw_obj(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`);
|
|
} else if (t.kind === "symbol" || t.kind === "bigint" || t.kind === "func" || t.kind === "classval") {
|
|
const rc = vAdapters(t);
|
|
emitter.line(`scr_throw_primitive_ref(${a.name}, &${rc.retain}, &${rc.release}, NULL);${emitter.srcComment(e.loc)}`);
|
|
} else if (t.kind === "dyn" || t.kind === "jsval") {
|
|
const rc = vAdapters(t);
|
|
const test = t.kind === "dyn" ? "scr_dyn_is_object" : "scr_jsval_is_object";
|
|
emitter.line(`scr_throw_ref_classified(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)}, ${test}(${a.name}));${emitter.srcComment(e.loc)}`);
|
|
} else {
|
|
const rc = vAdapters(t);
|
|
emitter.line(`scr_throw_ref(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`);
|
|
}
|
|
emitter.line(`scr_gen_resume_throw(${g.name});`);
|
|
}
|
|
const helper = genResultThunkFor(emitter, genT, resultT);
|
|
// The record builds before the check so an unwind (a propagated
|
|
// body exception) releases it as the frame's never-read dummy.
|
|
return emitter.fallibleTemp(e.type, `${helper}(${g.name})`);
|
|
}
|
|
case "awaitExpr": {
|
|
// Parks the fiber until the promise settles; rejected promises
|
|
// re-throw here (hence the pending check). Promise temp borrowed;
|
|
// refcounted results arrive +1 and join the frame pre-check so an
|
|
// unwind releases the dummy (NULL) harmlessly.
|
|
const pr = emitter.emitExpr(e.value);
|
|
let read: string;
|
|
switch (e.type.kind) {
|
|
case "f64":
|
|
case "date":
|
|
read = `scr_await_f64(${pr.name})`;
|
|
break;
|
|
case "bool":
|
|
read = `scr_await_bool(${pr.name})`;
|
|
break;
|
|
case "string":
|
|
read = `scr_await_str(${pr.name})`;
|
|
break;
|
|
case "void": {
|
|
emitter.line(`scr_await_void(${pr.name});${emitter.srcComment(e.loc)}`);
|
|
emitter.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "dyn":
|
|
// The checked-dynamic tree-crossing await: void fulfillments (a boxed
|
|
// promise<void> that skipped its adapter) answer the
|
|
// undefined VALUE, never NULL.
|
|
read = `scr_await_dyn(${pr.name})`;
|
|
break;
|
|
default:
|
|
read = `(${cType(e.type).trim()})scr_await_ref(${pr.name})`;
|
|
}
|
|
return emitter.fallibleTemp(e.type, read);
|
|
}
|
|
case "awaitUnionExpr": {
|
|
// Await of a promise-or-absent union: the promise arm awaits like
|
|
// awaitExpr (parks, re-throws rejections); a unit arm takes exactly
|
|
// one microtask hop (JS: await of a non-thenable) and yields
|
|
// itself. The union temp is borrowed; a value-carrying result joins
|
|
// the frame BEFORE the pending check so an unwind releases the
|
|
// dummy (a union box holding the never-read NULL payload, or NULL)
|
|
// harmlessly.
|
|
if (e.value.type.kind !== "union") {
|
|
throw new InternalCompilerError("emitter bug: awaitUnion of a non-union");
|
|
}
|
|
const def = emitter.unionsById.get(e.value.type.unionId);
|
|
const promiseArm = def?.arms[e.promiseTag];
|
|
if (!def || promiseArm?.kind !== "promise") {
|
|
throw new InternalCompilerError("emitter bug: awaitUnion arm is not a promise");
|
|
}
|
|
const inner = promiseArm.inner;
|
|
const u = emitter.emitExpr(e.value);
|
|
const peek = `(ScrPromise *)scr_union_peek(${u.name})`;
|
|
if (e.type.kind === "void") {
|
|
emitter.line(
|
|
`if (${u.name}->tag == ${e.promiseTag}) scr_await_void(${peek}); else scr_await_hop();${emitter.srcComment(e.loc)}`,
|
|
);
|
|
emitter.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
if (e.type.kind !== "union") {
|
|
throw new InternalCompilerError("emitter bug: awaitUnion result is neither void nor a union");
|
|
}
|
|
const resDef = emitter.unionsById.get(e.type.unionId);
|
|
if (!resDef) throw new InternalCompilerError("emitter bug: awaitUnion result union unknown");
|
|
const resTagOf = (arm: (typeof resDef.arms)[number]): number => {
|
|
const tag = resDef.arms.findIndex((a) => typeEquals(a, arm));
|
|
if (tag < 0) throw new InternalCompilerError("emitter bug: awaitUnion result arm missing");
|
|
return tag;
|
|
};
|
|
const innerTag = resTagOf(inner);
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.type, name)} = NULL;${emitter.srcComment(e.loc)}`);
|
|
emitter.currentFrame().push({ name, type: e.type });
|
|
emitter.line(`if (${u.name}->tag == ${e.promiseTag}) {`);
|
|
emitter.indent++;
|
|
let wrap: string;
|
|
switch (inner.kind) {
|
|
case "f64":
|
|
wrap = `scr_union_new_f64(${innerTag}, scr_await_f64(${peek}))`;
|
|
break;
|
|
case "bool":
|
|
wrap = `scr_union_new_bool(${innerTag}, scr_await_bool(${peek}))`;
|
|
break;
|
|
case "string":
|
|
wrap = `scr_union_new_ref(${innerTag}, scr_await_str(${peek}), scr_str_retain_v, scr_str_release_v, NULL)`;
|
|
break;
|
|
default: {
|
|
const v = vAdapters(inner);
|
|
wrap = `scr_union_new_ref(${innerTag}, scr_await_ref(${peek}), ${v.retain}, ${v.release}, ${emitter.traceArgC(inner)})`;
|
|
}
|
|
}
|
|
emitter.line(`${name} = ${wrap};`);
|
|
emitter.indent--;
|
|
emitter.line(`} else {`);
|
|
emitter.indent++;
|
|
emitter.line(`scr_await_hop();`);
|
|
const unitTags: number[] = def.arms.flatMap((a, i): number[] => (isUnitType(a) ? [i] : []));
|
|
if (unitTags.length === 1) {
|
|
emitter.line(`${name} = ${emitter.unitInstanceRef(e.type.unionId, resTagOf(def.arms[unitTags[0]!]!))};`);
|
|
} else {
|
|
emitter.line(`switch (${u.name}->tag) {`);
|
|
for (const t of unitTags) {
|
|
emitter.line(`case ${t}: ${name} = ${emitter.unitInstanceRef(e.type.unionId, resTagOf(def.arms[t]!))}; break;`);
|
|
}
|
|
emitter.line(`default: break;`);
|
|
emitter.line(`}`);
|
|
}
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
emitter.emitPendingCheck();
|
|
return { name, type: e.type };
|
|
}
|
|
case "newPromise": {
|
|
// Pending promise + resolve closure, executor run synchronously
|
|
// (its throw rejects — handled inside the runtime helper, so no
|
|
// pending check here). Executor/resolve temps are frame-owned.
|
|
if (e.type.kind !== "promise") throw new InternalCompilerError("emitter bug: newPromise type");
|
|
const inner = e.type.inner;
|
|
const p = emitter.newTemp(e.type, `scr_promise_new()`);
|
|
// Zero-param executor: no resolve exists — a forever-pending
|
|
// promise unless the executor throws (which rejects it).
|
|
if (e.executor.type.kind === "func" && e.executor.type.params.length === 0) {
|
|
const exec0 = emitter.emitExpr(e.executor);
|
|
emitter.line(
|
|
`scr_promise_run_executor0(${p.name}, ${exec0.name});${emitter.srcComment(e.loc)}`,
|
|
);
|
|
return p;
|
|
}
|
|
let mk: string;
|
|
switch (inner.kind) {
|
|
case "f64":
|
|
case "date":
|
|
mk = `scr_make_resolve(${p.name}, 0)`;
|
|
break;
|
|
case "bool":
|
|
mk = `scr_make_resolve(${p.name}, 1)`;
|
|
break;
|
|
case "string":
|
|
mk = `scr_make_resolve(${p.name}, 2)`;
|
|
break;
|
|
case "void":
|
|
mk = `scr_make_resolve(${p.name}, 3)`;
|
|
break;
|
|
default:
|
|
mk = `scr_make_resolve_fn(${p.name}, (void *)&${emitter.resolveThunkFor(inner)})`;
|
|
}
|
|
const resolve = emitter.newTemp(
|
|
{ kind: "func", params: inner.kind === "void" ? [] : [inner], ret: { kind: "void" } },
|
|
mk,
|
|
);
|
|
// Two-param executor: reject is a runtime-provided closure rejecting
|
|
// the promise with its Error reason (OBJ payload — catch instanceof
|
|
// and the uncaught printer see exactly a thrown Error). First settle
|
|
// wins in the runtime; both closures' +1 move into the call.
|
|
if (e.executor.type.kind === "func" && e.executor.type.params.length === 2) {
|
|
const reject = emitter.newTemp(
|
|
{ kind: "func", params: [{ kind: "object", className: "%Error" }], ret: { kind: "void" } },
|
|
`scr_make_reject(${p.name})`,
|
|
);
|
|
const exec2 = emitter.emitExpr(e.executor);
|
|
emitter.moveTemp(resolve);
|
|
emitter.moveTemp(reject);
|
|
emitter.line(
|
|
`scr_promise_run_executor2(${p.name}, ${exec2.name}, ${resolve.name}, ${reject.name});${emitter.srcComment(e.loc)}`,
|
|
);
|
|
return p;
|
|
}
|
|
const exec = emitter.emitExpr(e.executor);
|
|
// The executor is a compiled closure and OWNS its params (it
|
|
// releases them on exit) — resolve's +1 moves into the call. The
|
|
// executor closure itself is borrowed (frame-released here).
|
|
emitter.moveTemp(resolve);
|
|
emitter.line(
|
|
`scr_promise_run_executor(${p.name}, ${exec.name}, ${resolve.name});${emitter.srcComment(e.loc)}`,
|
|
);
|
|
return p;
|
|
}
|
|
case "promiseWithResolvers": {
|
|
// The newPromise pieces without an executor: a pending promise,
|
|
// its runtime resolve closure (typed per the inner kind), and
|
|
// the reject closure, written into the fresh record. Closure +1s
|
|
// move into the record's fields; never throws.
|
|
if (e.type.kind !== "record") throw new InternalCompilerError("emitter bug: promiseWithResolvers type");
|
|
const shape = emitter.recordsById.get(e.type.shapeId);
|
|
const promT = shape?.fields.find((f) => f.name === "promise")?.type;
|
|
const resolveT = shape?.fields.find((f) => f.name === "resolve")?.type;
|
|
const rejectT = shape?.fields.find((f) => f.name === "reject")?.type;
|
|
if (!shape || promT?.kind !== "promise" || !resolveT || !rejectT) {
|
|
throw new InternalCompilerError("emitter bug: promiseWithResolvers record shape");
|
|
}
|
|
const inner = promT.inner;
|
|
const p = emitter.newTemp(promT, `scr_promise_new()`);
|
|
let mk: string;
|
|
switch (inner.kind) {
|
|
case "f64":
|
|
case "date":
|
|
mk = `scr_make_resolve(${p.name}, 0)`;
|
|
break;
|
|
case "bool":
|
|
mk = `scr_make_resolve(${p.name}, 1)`;
|
|
break;
|
|
case "string":
|
|
mk = `scr_make_resolve(${p.name}, 2)`;
|
|
break;
|
|
case "void":
|
|
mk = `scr_make_resolve(${p.name}, 3)`;
|
|
break;
|
|
default:
|
|
mk = `scr_make_resolve_fn(${p.name}, (void *)&${emitter.resolveThunkFor(inner)})`;
|
|
}
|
|
const resolve = emitter.newTemp(resolveT, mk);
|
|
const reject = emitter.newTemp(rejectT, `scr_make_reject(${p.name})`);
|
|
const rec = emitter.newTemp(e.type, `${mangleRecordNew(e.type.shapeId)}()`);
|
|
// The promise's +1 moves into the record; the record's own read
|
|
// of it at the call site retains per field access as usual.
|
|
emitter.moveTemp(p);
|
|
emitter.moveTemp(resolve);
|
|
emitter.moveTemp(reject);
|
|
emitter.line(`${rec.name}->${mangleField("promise")} = ${p.name};`);
|
|
emitter.line(`${rec.name}->${mangleField("resolve")} = ${resolve.name};`);
|
|
emitter.line(`${rec.name}->${mangleField("reject")} = ${reject.name};`);
|
|
return rec;
|
|
}
|
|
default: {
|
|
const _exhaustive: never = e;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
function emitJsInteropExpr(
|
|
emitter: CEmitter,
|
|
e: ExprOf<"jsMarshal" | "jsOp" | "jsExit" | "jsBridgePromise">,
|
|
): Temp {
|
|
switch (e.kind) {
|
|
case "jsMarshal": {
|
|
// Static → island (--dynamic only). Primitives by value; JSON-safe
|
|
// composites deep-copy through the emitted type-directed serializer
|
|
// and the engine's JSON parser (documented aliasing divergence).
|
|
// Operand borrowed; result +1. from_json cannot fail on this
|
|
// machine-produced JSON but reports engine surprises via NULL +
|
|
// pending — check like a may-throw so the dummy unwinds cleanly.
|
|
const v = emitter.emitExpr(e.value);
|
|
switch (e.value.type.kind) {
|
|
case "f64":
|
|
return emitter.newTemp(e.type, `scr_jsval_from_f64(${v.name})`);
|
|
case "bool":
|
|
return emitter.newTemp(e.type, `scr_jsval_from_bool(${v.name})`);
|
|
case "string":
|
|
return emitter.newTemp(e.type, `scr_jsval_from_str(${v.name})`);
|
|
case "dyn":
|
|
// A CHECKED-DYNAMIC (dyn) value entering the island: deep
|
|
// copy, data kinds only — boxed functions/handles/promises
|
|
// throw the catchable TypeError in the runtime.
|
|
return emitter.fallibleTemp(e.type, `scr_jsval_from_dyn(${v.name})`);
|
|
case "bytes":
|
|
// A typed array crossing IN: an engine typed array of the same
|
|
// element kind — a COPY (the boundary's copy stance).
|
|
return emitter.fallibleTemp(e.type, `scr_jsval_from_bytes(${v.name})`);
|
|
case "url":
|
|
// A URL crossing IN: an engine URL instance built from href.
|
|
return emitter.fallibleTemp(e.type, `scr_jsval_from_url(${v.name})`);
|
|
case "promise": {
|
|
// A STATIC promise crossing IN: a real engine thenable
|
|
// settled when the scriptc promise settles (the async-
|
|
// callback return bridge). from_promise takes ownership of a
|
|
// +1 — retain past the borrowed frame temp.
|
|
const tag = islandPromisePayloadTag(e.value.type.inner);
|
|
if (!tag) throw new InternalCompilerError("emitter bug: jsMarshal of a promise outside the bridge payload domain");
|
|
const tagC = {
|
|
void: "SCR_ISLP_VOID", f64: "SCR_ISLP_F64", bool: "SCR_ISLP_BOOL",
|
|
string: "SCR_ISLP_STR", jsval: "SCR_ISLP_JSVAL", jsvalArr: "SCR_ISLP_JSVAL_ARR",
|
|
}[tag];
|
|
return emitter.fallibleTemp(e.type, `scr_jsval_from_promise(scr_promise_retain(${v.name}), ${tagC})`);
|
|
}
|
|
case "func": {
|
|
// A closure entering the island as a host function (the
|
|
// package-callback pattern). from_closure retains the closure;
|
|
// the engine's finalizer releases it at teardown — which runs
|
|
// before the RC audit. The per-signature adapter gives the
|
|
// runtime one uniform call shape over the closure ABI: the
|
|
// interned (arity, return) adapters for the all-'any' shape,
|
|
// or a typed adapter converting each incoming argument to the
|
|
// param's static type through the exit machinery.
|
|
const fn = e.value.type;
|
|
const adapter = canMarshalFuncIntoIsland(fn)
|
|
? emitter.islandAdapter(
|
|
fn.params.length,
|
|
fn.ret.kind as "void" | "jsval" | "f64" | "bool" | "string",
|
|
)
|
|
: emitter.islandTypedAdapter(fn);
|
|
// ISLAND-REST closures encode a NEGATIVE arity: the wrapper
|
|
// pads the leading declared params and hands the trailing
|
|
// slot the ENGINE array of the surplus arguments.
|
|
const arity = fn.rest === true && fn.restAbi === "jsval" ? -fn.params.length : fn.params.length;
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_jsval_from_closure(${v.name}, ${arity}, ${adapter})`,
|
|
);
|
|
}
|
|
default: {
|
|
const helper = emitter.jsonWriteHelper(e.value.type);
|
|
const buf = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`ScrJsonBuf ${buf}; scr_jb_init(&${buf});${emitter.srcComment(e.loc)}`);
|
|
emitter.line(`${helper}(&${buf}, ${v.name});`);
|
|
const json = emitter.newTemp(STRING, `scr_jb_finish(&${buf})`);
|
|
return emitter.fallibleTemp(e.type, `scr_jsval_from_json(${json.name})`);
|
|
}
|
|
}
|
|
}
|
|
case "jsOp": {
|
|
// Island operation: JS semantics via the engine (prelude helper
|
|
// closures), never C reimplementations. jsval args are borrowed
|
|
// frame temps; jsval/string results +1. Engine exceptions bridge
|
|
// into the cell — pending checks after every fallible op.
|
|
const args = e.args.map((a) => emitter.emitExpr(a));
|
|
const a = (i: number) => args[i]!.name;
|
|
const nameSym = () => `(ScrStr *)&${emitter.internLiteral(e.name!)}`;
|
|
const finishFallible = (call: string): Temp => emitter.fallibleTemp(e.type, call);
|
|
const argPack = (list: string[]): string => {
|
|
if (list.length === 0) return "NULL";
|
|
const arr = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`ScrJsval *${arr}[] = { ${list.join(", ")} };`);
|
|
return arr;
|
|
};
|
|
switch (e.op) {
|
|
case "add": case "sub": case "mul": case "div": case "mod": case "pow": {
|
|
const c = `SCR_JSOP_${e.op.toUpperCase()}`;
|
|
return finishFallible(`scr_jsval_binop(${c}, ${a(0)}, ${a(1)})`);
|
|
}
|
|
case "lt": case "le": case "gt": case "ge": case "eq": case "neq": {
|
|
const c = `SCR_JSOP_${e.op.toUpperCase()}`;
|
|
return finishFallible(`(scr_jsval_cmp(${c}, ${a(0)}, ${a(1)}) == 1)`);
|
|
}
|
|
case "instanceOf":
|
|
return finishFallible(`(scr_jsval_instance_of(${a(0)}, ${a(1)}) == 1)`);
|
|
case "neg":
|
|
return finishFallible(`scr_jsval_neg(${a(0)})`);
|
|
case "plus":
|
|
return finishFallible(`scr_jsval_plus(${a(0)})`);
|
|
case "truthy":
|
|
return emitter.newTemp(e.type, `(scr_jsval_truthy(${a(0)}) != 0)`);
|
|
case "not":
|
|
return emitter.newTemp(e.type, `(scr_jsval_truthy(${a(0)}) == 0)`);
|
|
case "typeof":
|
|
return emitter.newTemp(e.type, `scr_jsval_typeof(${a(0)})`);
|
|
case "toStr":
|
|
return finishFallible(`scr_jsval_to_str(${a(0)})`);
|
|
case "getProp":
|
|
return finishFallible(`scr_jsval_get_prop(${a(0)}, ${nameSym()})`);
|
|
case "globalGet":
|
|
return finishFallible(`scr_jsval_global_get(${nameSym()})`);
|
|
case "setProp":
|
|
emitter.line(`scr_jsval_set_prop(${a(0)}, ${nameSym()}, ${a(1)});${emitter.srcComment(e.loc)}`);
|
|
emitter.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
case "getIdx":
|
|
return finishFallible(`scr_jsval_get_idx(${a(0)}, ${a(1)})`);
|
|
case "iterNew":
|
|
return finishFallible(`scr_jsval_iter_new(${a(0)})`);
|
|
case "setIdx":
|
|
emitter.line(`scr_jsval_set_idx(${a(0)}, ${a(1)}, ${a(2)});${emitter.srcComment(e.loc)}`);
|
|
emitter.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
case "optCallMethod": {
|
|
const pack = argPack(args.slice(1).map((x) => x.name));
|
|
return finishFallible(
|
|
`scr_jsval_opt_call_method(${a(0)}, ${nameSym()}, ${args.length - 1}, ${pack})`,
|
|
);
|
|
}
|
|
case "callMethod": {
|
|
const pack = argPack(args.slice(1).map((x) => x.name));
|
|
return finishFallible(
|
|
`scr_jsval_call_method(${a(0)}, ${nameSym()}, ${args.length - 1}, ${pack})`,
|
|
);
|
|
}
|
|
case "callFnThis": {
|
|
const pack = argPack(args.slice(2).map((x) => x.name));
|
|
return finishFallible(
|
|
`scr_jsval_call_this(${a(0)}, ${a(1)}, ${args.length - 2}, ${pack})`,
|
|
);
|
|
}
|
|
case "callFn": {
|
|
const pack = argPack(args.slice(1).map((x) => x.name));
|
|
return finishFallible(`scr_jsval_call(${a(0)}, ${args.length - 1}, ${pack})`);
|
|
}
|
|
case "callSpread":
|
|
// Spread application (`f(...pre, ...s)`): the prelude helper's
|
|
// real spread syntax — iterator protocols are the engine's
|
|
// own, the guards front-run V8's spread-call TypeError texts
|
|
// (the name literal is the spread expression's spelling).
|
|
return finishFallible(`scr_jsval_call_spread(${a(0)}, ${a(1)}, ${a(2)}, ${nameSym()})`);
|
|
case "construct": {
|
|
// `new X(...)` on an island callee: JS_CallConstructor.
|
|
const pack = argPack(args.slice(1).map((x) => x.name));
|
|
return finishFallible(`scr_jsval_construct(${a(0)}, ${args.length - 1}, ${pack})`);
|
|
}
|
|
case "objLit": {
|
|
const pack = argPack(args.map((x) => x.name));
|
|
return emitter.newTemp(e.type, `scr_jsval_obj_lit(${args.length / 2}, ${pack})`);
|
|
}
|
|
case "tplStrings": {
|
|
const pack = argPack(args.map((x) => x.name));
|
|
return emitter.newTemp(e.type, `scr_jsval_tpl_strings(${args.length / 2}, ${pack})`);
|
|
}
|
|
case "objSpread":
|
|
// Spread completion: engine CopyDataProperties (getters can
|
|
// throw — fallible); answers the target (+1).
|
|
return finishFallible(`scr_jsval_obj_spread(${a(0)}, ${a(1)})`);
|
|
case "defineGetter":
|
|
// Getter completion for an island literal: defines key (a(1))
|
|
// on obj (a(0)) as an engine getter invoking a(2); answers the
|
|
// object (+1) for chaining.
|
|
return emitter.newTemp(e.type, `scr_jsval_define_getter(${a(0)}, ${a(1)}, ${a(2)})`);
|
|
case "arrLit": {
|
|
const pack = argPack(args.map((x) => x.name));
|
|
return emitter.newTemp(e.type, `scr_jsval_arr_lit(${args.length}, ${pack})`);
|
|
}
|
|
case "undefLit":
|
|
return emitter.newTemp(e.type, `scr_jsval_undefined()`);
|
|
case "nullLit":
|
|
return emitter.newTemp(e.type, `scr_jsval_null()`);
|
|
default: {
|
|
const _exhaustive: never = e.op;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
case "jsExit": {
|
|
// Island → static validated exit. Primitives extract strictly (no
|
|
// coercion — a non-number refuses to exit as number); composites
|
|
// round-trip engine JSON.stringify → json.parse → the existing
|
|
// dynCheck walker, inheriting its width tolerance and path-annotated
|
|
// failures. Every step is a may-throw with the standard pending
|
|
// discipline; intermediate temps are frame-owned.
|
|
const v = emitter.emitExpr(e.value);
|
|
switch (e.type.kind) {
|
|
case "f64":
|
|
case "bool": {
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
const ctype = e.type.kind === "f64" ? "double" : "bool";
|
|
const fn = e.type.kind === "f64" ? "scr_jsval_exit_f64" : "scr_jsval_exit_bool";
|
|
emitter.line(`${ctype} ${name} = 0;${emitter.srcComment(e.loc)}`);
|
|
emitter.line(`${fn}(${v.name}, &${name});`);
|
|
emitter.emitPendingCheck();
|
|
return { name, type: e.type };
|
|
}
|
|
case "string":
|
|
return emitter.fallibleTemp(e.type, `scr_jsval_exit_str(${v.name})`);
|
|
case "bytes":
|
|
// Uint8Array exit: kind-checked, copied out (+1) — engine
|
|
// Buffers pass (they ARE Uint8Arrays). The frontend only
|
|
// emits u8 targets (canExitIslandToType).
|
|
return emitter.fallibleTemp(e.type, `scr_jsval_exit_bytes(${v.name})`);
|
|
default: {
|
|
// `any[]`-declared slot: the engine array exits Array.isArray-
|
|
// gated, elements BY REFERENCE (identity crosses; the spine is
|
|
// a snapshot copy). JSON-safe element types keep the round
|
|
// trip below.
|
|
if (e.type.kind === "array" && e.type.elem.kind === "jsval") {
|
|
return emitter.fallibleTemp(e.type, `scr_jsval_exit_jsval_arr(${v.name})`);
|
|
}
|
|
// An undefined-armed union target: the engine's undefined takes
|
|
// the undefined arm FIRST — JSON cannot spell it (to_json would
|
|
// refuse the exit) — then null and data ride the round trip
|
|
// into the union's dynCheck like any composite (or, for the
|
|
// `any[] | undefined` defaulted-parameter spelling, the
|
|
// jsval-element array exit wrapped into the data arm).
|
|
const undefTag = e.type.kind === "union" ? undefinedArmTag(e.type, emitter.unionsById) : -1;
|
|
if (e.type.kind === "union" && undefTag >= 0) {
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${cDecl(e.type, name)};`);
|
|
emitter.line(`if (scr_jsval_is_undefined(${v.name})) {`);
|
|
emitter.indent++;
|
|
emitter.line(`${name} = ${emitter.unitInstanceRef(e.type.unionId, undefTag)};`);
|
|
emitter.indent--;
|
|
emitter.line(`} else {`);
|
|
emitter.indent++;
|
|
emitter.frames.push([]);
|
|
const unionDef = emitter.unionsById.get(e.type.unionId);
|
|
const dataArms = unionDef ? unionDef.arms.flatMap((a, i) => (isUnitType(a) ? [] : [{ a, i }])) : [];
|
|
const jsvalArr = dataArms.length === 1 && dataArms[0]!.a.kind === "array" && dataArms[0]!.a.elem.kind === "jsval" ? dataArms[0]! : null;
|
|
if (jsvalArr) {
|
|
// The `any[] | undefined` defaulted-parameter spelling:
|
|
// the engine array exits BY REFERENCE into the data arm.
|
|
const arr = emitter.fallibleTemp(jsvalArr.a, `scr_jsval_exit_jsval_arr(${v.name})`);
|
|
emitter.moveTemp(arr);
|
|
emitter.line(`${name} = scr_union_new_ref(${jsvalArr.i}, ${arr.name}, &scr_arr_retain_v, &scr_arr_release_v, NULL);`);
|
|
} else {
|
|
const json = emitter.fallibleTemp(STRING, `scr_jsval_to_json(${v.name})`);
|
|
const dom = emitter.fallibleTemp(DYN, `scr_json_parse(${json.name})`);
|
|
const out = emitter.fallibleTemp(e.type, `${emitter.dynCheckHelper(e.type)}(${dom.name}, NULL)`);
|
|
emitter.moveTemp(out);
|
|
emitter.line(`${name} = ${out.name};`);
|
|
}
|
|
emitter.releaseFrame(emitter.frames.pop()!);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
emitter.currentFrame().push({ name, type: e.type });
|
|
return { name, type: e.type };
|
|
}
|
|
const json = emitter.fallibleTemp(STRING, `scr_jsval_to_json(${v.name})`);
|
|
const dom = emitter.fallibleTemp(DYN, `scr_json_parse(${json.name})`);
|
|
return emitter.fallibleTemp(e.type, `${emitter.dynCheckHelper(e.type)}(${dom.name}, NULL)`);
|
|
}
|
|
}
|
|
}
|
|
case "jsBridgePromise": {
|
|
// Island → static promise bridge: a fresh pending ScrPromise the
|
|
// engine promise settles (fulfillment = retained jsval cell or
|
|
// void; rejection = the bridged reason). Operand borrowed; the +1
|
|
// promise joins the frame. Fails only on an engine-level surprise
|
|
// minting the subscription — pending check like other island ops.
|
|
const v = emitter.emitExpr(e.value);
|
|
const payload =
|
|
e.type.kind === "promise" && e.type.inner.kind === "void"
|
|
? "SCR_ISLP_VOID"
|
|
: e.type.kind === "promise" && e.type.inner.kind === "array" && e.type.inner.elem.kind === "jsval"
|
|
? "SCR_ISLP_JSVAL_ARR" // `any[]` fulfillment: the Array.isArray-gated by-reference exit at settle
|
|
: "SCR_ISLP_JSVAL";
|
|
return emitter.fallibleTemp(e.type, `scr_jsval_bridge_promise(${v.name}, ${payload})`);
|
|
}
|
|
default: {
|
|
const _exhaustive: never = e;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
type LibCallExpr = ExprOf<"libCall">;
|
|
type LibCallPrefixOf<T extends string> = T extends `${infer Prefix}.${string}`
|
|
? Prefix
|
|
: never;
|
|
type LibCallPrefix = LibCallPrefixOf<IrLibFn>;
|
|
|
|
interface LibCallState {
|
|
emitter: CEmitter;
|
|
e: LibCallExpr;
|
|
args: Temp[];
|
|
arg: (index: number) => string;
|
|
finish: (call: string) => Temp;
|
|
}
|
|
|
|
function emitWebLibCall(state: LibCallState): Temp {
|
|
const { e, emitter, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
case "fetch.start":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_fetch_static(${arg(0)}, ${arg(1)})`);
|
|
case "fetch.responseNew":
|
|
return finish(`scr_fetch_response_new(${arg(0)}, ${arg(1)})`);
|
|
case "fetch.responseJson":
|
|
return finish(`scr_fetch_response_json(${arg(0)})`);
|
|
case "fetch.responseText":
|
|
case "fetch.responseBytes": {
|
|
if (e.type.kind !== "promise") {
|
|
throw new InternalCompilerError(`emitter bug: ${fn} result`);
|
|
}
|
|
const runtimeFn =
|
|
fn === "fetch.responseText"
|
|
? "scr_fetch_response_text"
|
|
: "scr_fetch_response_bytes";
|
|
const source = emitter.newTemp(
|
|
{ kind: "promise", inner: DYN },
|
|
`${runtimeFn}(${arg(0)})`,
|
|
);
|
|
const result = emitter.newTemp(e.type, `scr_promise_new()`);
|
|
const adapter = dynPromiseAdapter(emitter, e.type.inner);
|
|
emitter.line(
|
|
`scr_promise_race_add(${result.name}, ${source.name}, &${adapter});${emitter.srcComment(e.loc)}`,
|
|
);
|
|
return result;
|
|
}
|
|
case "fetch.abortControllerNew":
|
|
return finish(`scr_fetch_abort_controller_new()`);
|
|
case "fetch.abortTimeout":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_fetch_abort_timeout(${arg(0)})`);
|
|
case "fetch.abortNow":
|
|
return finish(`scr_fetch_abort_now(${arg(0)})`);
|
|
case "fetch.abortAny":
|
|
return finish(`scr_fetch_abort_any(${arg(0)})`);
|
|
case "fetch.streamNew":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_fetch_stream_new(${arg(0)})`);
|
|
case "fetch.streamFrom":
|
|
emitter.usesTimers = true;
|
|
if (e.args[0]!.type.kind === "array") {
|
|
const adapter = streamFromArrayAdapter(emitter, e.args[0]!.type);
|
|
return finish(
|
|
`scr_fetch_stream_from_array(${arg(0)}, &${adapter})`,
|
|
);
|
|
}
|
|
if (e.args[0]!.type.kind === "bytes") {
|
|
return finish(`scr_fetch_stream_from_bytes(${arg(0)})`);
|
|
}
|
|
if (e.args[0]!.type.kind === "string") {
|
|
return finish(`scr_fetch_stream_from_string(${arg(0)})`);
|
|
}
|
|
return finish(`scr_fetch_stream_from(${arg(0)})`);
|
|
case "fetch.readerRead": {
|
|
if (e.type.kind !== "promise" || e.type.inner.kind !== "record") {
|
|
throw new InternalCompilerError("emitter bug: fetch.readerRead result");
|
|
}
|
|
const source = emitter.newTemp(
|
|
{ kind: "promise", inner: DYN },
|
|
`scr_fetch_reader_read(${arg(0)})`,
|
|
);
|
|
const result = emitter.newTemp(e.type, `scr_promise_new()`);
|
|
const adapter = dynPromiseAdapter(emitter, e.type.inner);
|
|
emitter.line(
|
|
`scr_promise_race_add(${result.name}, ${source.name}, &${adapter});${emitter.srcComment(e.loc)}`,
|
|
);
|
|
return result;
|
|
}
|
|
case "island.eval":
|
|
// --dynamic builds only (the frontend fences the intrinsic, so
|
|
// scr_island_eval is always linked when this emits). Borrows
|
|
// the source; returns +1 String(result), or bridges the island
|
|
// exception into the cell (may-throw seed set).
|
|
return finish(`scr_island_eval(${arg(0)})`);
|
|
case "island.import":
|
|
// --dynamic builds only. Loads an EMBEDDED npm module (main
|
|
// registered the table before %main ran) through the island's
|
|
// module loader — cached by the engine, so one evaluation per
|
|
// module — and takes one export as an owned jsval. A package's
|
|
// top-level throw bridges catchably (may-throw seed set), and a
|
|
// MISSING named export throws Node's link-time SyntaxError
|
|
// naming the written specifier (arg 2).
|
|
return finish(`scr_jsval_import(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "island.importDyn":
|
|
// --dynamic builds only. Dynamic import(): the island's module
|
|
// system answers an ENGINE promise of the namespace — load and
|
|
// evaluation failures REJECT it (Node's shape), so the call
|
|
// itself never throws; the frontend's jsBridgePromise carries
|
|
// the settlement across. +1 jsval holding the engine promise.
|
|
return finish(`scr_jsval_import_dyn(${arg(0)})`);
|
|
case "island.castFail":
|
|
// The deferred boundary failure: the island value was
|
|
// evaluated (arg 0 — its side effects are real), the throw is
|
|
// unconditional (catchable TypeError naming the target type),
|
|
// and the typed dummy is a NULL promise the pending check
|
|
// abandons (releases are NULL-tolerant).
|
|
return finish(`(scr_jsval_cast_fail(${arg(0)}, ${arg(1)}), NULL)`);
|
|
case "json.parseReviver":
|
|
return finish(`scr_json_parse_reviver(${arg(0)}, ${arg(1)})`);
|
|
case "json.stringifyReplacer":
|
|
return finish(`scr_json_stringify_replacer(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "json.parse":
|
|
// Borrows the text; returns +1 on a fresh dyn, or throws a
|
|
// catchable SyntaxError-shaped string (may-throw seed set).
|
|
return finish(`scr_json_parse(${arg(0)})`);
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: web libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitDynamicLibCall(state: LibCallState): Temp {
|
|
const { e, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
case "dyn.defineProps":
|
|
// Object.defineProperties over dyn values: both borrowed,
|
|
// result the target (+1); throws catchably (may-throw seed).
|
|
return finish(`scr_dyn_define_props(${arg(0)}, ${arg(1)})`);
|
|
case "dyn.defineProperty":
|
|
return finish(`scr_dyn_define_property(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "dyn.getOwnPropertyDescriptor":
|
|
return finish(`scr_dyn_get_own_property_descriptor(${arg(0)}, ${arg(1)})`);
|
|
case "dyn.hasKey":
|
|
// `k in v` with a runtime key: the dyn presence answer (both
|
|
// borrowed, no allocation, never throws).
|
|
return finish(`scr_dyn_has_key(${arg(0)}, ${arg(1)})`);
|
|
case "dyn.keySet":
|
|
// Keyed write on a dyn receiver: all three borrowed (the
|
|
// member retains the value in); throws Node's TypeErrors on
|
|
// non-object receivers (may-throw seed set).
|
|
return finish(`scr_dyn_key_set(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "dyn.keySetComputed":
|
|
return finish(`scr_dyn_key_set_computed(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "dyn.keyDelete":
|
|
return finish(`scr_dyn_key_delete(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "dyn.globalSymbolGet":
|
|
return finish(`scr_dyn_global_symbol_get(${arg(0)})`);
|
|
case "dyn.globalSymbolSet":
|
|
return finish(`scr_dyn_global_symbol_set(${arg(0)}, ${arg(1)})`);
|
|
case "dyn.globalSymbolHas":
|
|
return finish(`scr_dyn_global_symbol_has(${arg(0)})`);
|
|
case "dyn.globalSymbolDelete":
|
|
return finish(`scr_dyn_global_symbol_delete(${arg(0)})`);
|
|
case "dyn.typedRefIs":
|
|
return finish(`scr_dyn_typed_ref_is_key(${arg(0)}, ${arg(1)})`);
|
|
case "dyn.iterPack":
|
|
// Destructuring/for-of pack over a dyn source: both borrowed,
|
|
// fresh array +1; throws V8's not-iterable TypeError on
|
|
// non-iterable dyn kinds and drains wrapped engine values
|
|
// through the engine's protocol (may-throw seed set).
|
|
return finish(`scr_dyn_iter_pack(${arg(0)}, ${arg(1)})`);
|
|
case "dyn.arrLen":
|
|
// The for-of pack's length (borrowed; never throws).
|
|
return finish(`scr_dyn_arr_len(${arg(0)})`);
|
|
case "dyn.arrAt":
|
|
// The for-of pack's element at index (+1; never throws — the
|
|
// undefined singleton past the end).
|
|
return finish(`scr_dyn_arr_at(${arg(0)}, ${arg(1)})`);
|
|
case "dyn.typeof":
|
|
// Bare typeof on a dyn value: the dyn kind's JS answer (+1).
|
|
return finish(`scr_dyn_typeof(${arg(0)})`);
|
|
case "dyn.objectTag":
|
|
return finish(`scr_dyn_object_tag(${arg(0)})`);
|
|
case "dyn.toString":
|
|
// Receiver-kind-dispatched toString (+1); throws Node's
|
|
// TypeError on undefined/null and the "is not a function"
|
|
// TypeError on null-prototype dictionaries (may-throw seed
|
|
// set) — args[2] carries the call's source spelling.
|
|
return finish(`scr_dyn_to_string_method(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "dyn.toStringCoerce":
|
|
// +1 string or NULL with the exception pending (user
|
|
// toString/valueOf throws propagate). Borrows the dyn.
|
|
return finish(`scr_dyn_string_coerce_js(${arg(0)})`);
|
|
case "dyn.toNumberCoerce":
|
|
// JS ToNumber (number-hint valueOf/toString protocol). A
|
|
// thrown hook leaves the exception pending; the may-throw
|
|
// epilogue abandons the NaN dummy.
|
|
return finish(`scr_dyn_number_coerce(${arg(0)})`);
|
|
case "dyn.add":
|
|
return finish(`scr_dyn_add(${arg(0)}, ${arg(1)})`);
|
|
case "dyn.proxyNew":
|
|
return finish(`scr_dyn_proxy_new(${arg(0)}, ${arg(1)})`);
|
|
case "global.native":
|
|
return finish(`scr_global_native(${arg(0)})`);
|
|
case "global.undefRead":
|
|
// A declare-d const nothing defines: Node's catchable
|
|
// ReferenceError at the access (always throws — the typed
|
|
// dummy is abandoned by the pending check's unwind; releases
|
|
// are NULL-tolerant). Borrows the name string.
|
|
return finish(
|
|
`(scr_undef_global_read(${arg(0)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
|
|
);
|
|
case "dyn.this":
|
|
return finish(`scr_dyn_this_get()`);
|
|
case "arrayBuffer.new":
|
|
return finish(`scr_array_buffer_new(${arg(0)})`);
|
|
case "arrayBuffer.is":
|
|
return finish(`scr_array_buffer_is(${arg(0)})`);
|
|
case "arrayBuffer.isView":
|
|
return finish(`scr_array_buffer_is_view(${arg(0)})`);
|
|
case "arrayBuffer.byteLengthGetter":
|
|
return finish(`scr_array_buffer_byte_length_getter()`);
|
|
case "arrayBuffer.byteLengthDescriptor":
|
|
return finish(`scr_array_buffer_byte_length_descriptor(${arg(0)})`);
|
|
case "arrayBuffer.viewU8":
|
|
return finish(`scr_array_buffer_view_u8(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "arrayBuffer.viewU32":
|
|
return finish(`scr_array_buffer_view_u32(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "arrayBuffer.viewI32":
|
|
return finish(`scr_array_buffer_view_i32(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "arrayBuffer.viewF32":
|
|
return finish(`scr_array_buffer_view_f32(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "arrayBuffer.viewF64":
|
|
return finish(`scr_array_buffer_view_f64(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "arrayBuffer.viewDV":
|
|
return finish(`scr_array_buffer_view_dv(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "dyn.objKeys":
|
|
return finish(`scr_dyn_obj_keys(${arg(0)})`);
|
|
case "dyn.forInKeys":
|
|
return finish(`scr_dyn_for_in_keys(${arg(0)})`);
|
|
case "dyn.assign":
|
|
// Object.assign over dyn values: own members copy, the target
|
|
// returns (+1); non-object receivers throw like Node.
|
|
return finish(`scr_dyn_assign(${arg(0)}, ${arg(1)})`);
|
|
case "dyn.packPush":
|
|
// Variadic Object.assign's source pack: a plain source
|
|
// retains in (both args borrowed). Never throws.
|
|
return finish(`scr_dyn_pack_push(${arg(0)}, ${arg(1)})`);
|
|
case "dyn.packPushSpread":
|
|
// A spread source flattens through the spread-call walk —
|
|
// V8's exact TypeError texts (may-throw seed set); the string
|
|
// spells the spread expression for the nullish form.
|
|
return finish(`scr_dyn_pack_push_spread(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "dyn.packPushSpreadIter":
|
|
// The non-last/multi-spread positions: V8's iterator-protocol
|
|
// failure texts describe the value (may-throw seed set).
|
|
return finish(`scr_dyn_pack_push_spread_iter(${arg(0)}, ${arg(1)})`);
|
|
case "dyn.assignAll":
|
|
// The flattened pack copies onto the target left to right;
|
|
// the target returns (+1). Nullish targets throw like Node.
|
|
return finish(`scr_dyn_assign_all(${arg(0)}, ${arg(1)})`);
|
|
case "dyn.objCreateNullProto":
|
|
// Object.create(null): the fresh null-prototype dictionary
|
|
// (+1). Never throws.
|
|
return finish(`scr_dyn_new_obj_null_proto()`);
|
|
case "dyn.hasOwn":
|
|
// Object.hasOwn over a dyn receiver (throws on nullish, like
|
|
// Node's ToObject).
|
|
return finish(`scr_dyn_has_own(${arg(0)}, ${arg(1)})`);
|
|
case "dyn.objValues":
|
|
return finish(`scr_dyn_obj_values(${arg(0)})`);
|
|
case "dyn.objEntries":
|
|
return finish(`scr_dyn_obj_entries(${arg(0)})`);
|
|
case "dyn.errInstanceof":
|
|
// The from_error cache resolves the dyn value to its runtime
|
|
// error; the class's stamped interval answers. Never throws.
|
|
return finish(`scr_dyn_err_instanceof(${arg(0)}, ${arg(1)})`);
|
|
case "dyn.structuredClone":
|
|
// Deep dyn clone (+1); option/DataClone/cycle errors throw
|
|
// (may-throw seed set). Both args borrowed.
|
|
return finish(`scr_structured_clone(${arg(0)}, ${arg(1)})`);
|
|
case "dyn.cloneMissing":
|
|
// Always throws ERR_MISSING_ARGS; the result never exists.
|
|
return finish(`scr_structured_clone_missing()`);
|
|
case "dyn.cloneTransferFail":
|
|
// Always throws DataCloneError; the result never exists.
|
|
return finish(`scr_structured_clone_transfer_fail()`);
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: dynamic libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitFilesystemLibCall(state: LibCallState): Temp {
|
|
const { e, emitter, args, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
case "fs.readFileSync":
|
|
// args[1] is the (always-"utf8") encoding: evaluated for
|
|
// JS-exact side-effect order, ignored by the runtime.
|
|
return finish(`scr_fs_read_file(${arg(0)})`);
|
|
case "fs.readFileSyncBuf":
|
|
return finish(`scr_fs_read_file_bytes(${arg(0)})`);
|
|
case "fs.readFileSyncDyn":
|
|
return finish(`scr_fs_read_file_sync_dyn(${arg(0)}, ${arg(1)})`);
|
|
case "fs.writeFileSync":
|
|
return finish(`scr_fs_write_file(${arg(0)}, ${arg(1)})`);
|
|
case "fs.appendFileSync":
|
|
return finish(`scr_fs_append_file(${arg(0)}, ${arg(1)})`);
|
|
case "fs.existsSync":
|
|
return finish(`scr_fs_exists(${arg(0)})`);
|
|
case "fs.mkdirSync":
|
|
return finish(`scr_fs_mkdir(${arg(0)})`);
|
|
case "fs.rmSync":
|
|
return finish(`scr_fs_rm(${arg(0)})`);
|
|
case "fs.rmdirSync":
|
|
return finish(`scr_fs_rmdir(${arg(0)})`);
|
|
case "fs.readdirSync":
|
|
return finish(`scr_fs_readdir(${arg(0)})`);
|
|
case "fs.readdirTypesSync":
|
|
case "fsp.readdirTypes": {
|
|
// Dirent rows assembled inline from one scandir snapshot
|
|
// (scr_lib.c) — the os.networkInterfaces pattern, flat. The
|
|
// frontend/validator pinned the shape ({%dtype, name,
|
|
// parentPath}); lookups here only guard emitter bugs. The sync
|
|
// form checks the snapshot's scandir error immediately. The
|
|
// promise form lets NULL flow through the null-tolerant count/
|
|
// free accessors, builds an empty dummy array, and moves it to
|
|
// settled_ref: the pending exception becomes the rejection and
|
|
// settled_ref releases that dummy.
|
|
const promiseForm = e.fn === "fsp.readdirTypes";
|
|
const arrayT = promiseForm && e.type.kind === "promise" ? e.type.inner : e.type;
|
|
if (arrayT.kind !== "array" || arrayT.elem.kind !== "record") {
|
|
throw new InternalCompilerError(`emitter bug: ${e.fn} result is not a Dirent record array`);
|
|
}
|
|
const recT = arrayT.elem;
|
|
const snap = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`ScrScandir *${snap} = scr_fs_scandir(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
if (!promiseForm) emitter.emitPendingCheck();
|
|
const out = emitter.newTemp(arrayT, emitter.arrNewC(recT, `scr_fs_scandir_count(${snap})`));
|
|
const i = `sc_t${emitter.tempCounter++}`;
|
|
const n = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`for (size_t ${i} = 0, ${n} = scr_fs_scandir_count(${snap}); ${i} < ${n}; ${i}++) {`);
|
|
emitter.indent++;
|
|
const row = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`${mangleRecordStruct(recT.shapeId)} *${row} = ${mangleRecordNew(recT.shapeId)}();`);
|
|
emitter.line(`${row}->${mangleField("%dtype")} = scr_fs_scandir_type(${snap}, ${i});`);
|
|
emitter.line(`${row}->${mangleField("name")} = scr_fs_scandir_name(${snap}, ${i});`);
|
|
emitter.line(`${row}->${mangleField("parentPath")} = ${retainCallC({ kind: "string" }, arg(0))};`);
|
|
emitter.line(`scr_arr_push_ref(${out.name}, ${row});`); // push takes ownership of the row
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
emitter.line(`scr_fs_scandir_free(${snap});`);
|
|
if (promiseForm) {
|
|
const rc = vAdapters(arrayT);
|
|
emitter.moveTemp(out); // promise fulfillment owns the result array
|
|
return finish(
|
|
`scr_promise_settled_ref(${out.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(arrayT)})`,
|
|
);
|
|
}
|
|
return out;
|
|
}
|
|
// Stats (scr_lib.c): statSync throws like the other sync fs
|
|
// calls; the getters are pure reads.
|
|
case "fs.openSync":
|
|
// Throws Node-shaped fs errors (may-throw seed set); the fd
|
|
// comes back as f64.
|
|
return finish(`scr_fs_open(${arg(0)}, ${arg(1)})`);
|
|
case "fs.openNumericSync":
|
|
return finish(`scr_fs_open_numeric(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "fs.fstatSync":
|
|
return finish(`scr_fs_fstat(${arg(0)})`);
|
|
case "fs.fchmodSync":
|
|
return finish(`scr_fs_fchmod(${arg(0)}, ${arg(1)})`);
|
|
case "fs.fsyncSync":
|
|
return finish(`scr_fs_fsync(${arg(0)})`);
|
|
case "fs.linkSync":
|
|
return finish(`scr_fs_link(${arg(0)}, ${arg(1)})`);
|
|
case "fs.readSync":
|
|
return finish(`scr_fs_read_sync(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`);
|
|
case "fs.writeSync":
|
|
return finish(`scr_fs_write_sync(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`);
|
|
case "fs.writeStrSync":
|
|
return finish(`scr_fs_write_str_sync(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`);
|
|
case "fs.closeSync":
|
|
return finish(`scr_fs_close(${arg(0)})`);
|
|
case "fs.watch":
|
|
// Throws Node-shaped fs errors when the path won't open
|
|
// (may-throw seed set). An open watcher holds the loop:
|
|
// usesTimers.
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_fs_watch(${arg(0)}, NULL, NULL)`);
|
|
case "fs.watchCb": {
|
|
// The callback MOVES into the watcher's registry; the adapter
|
|
// is runtime-provided per listener shape (zero-param, or the
|
|
// eventType string).
|
|
emitter.usesTimers = true;
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: fs.watchCb callback not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = cbT.params.length === 0 ? "scr_watch_thunk0" : "scr_watch_thunk_event";
|
|
return finish(`scr_fs_watch(${arg(0)}, ${cb.name}, &${adapter})`);
|
|
}
|
|
case "watcher.close":
|
|
emitter.line(`scr_watcher_close(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "fs.statSync":
|
|
return finish(`scr_fs_stat(${arg(0)})`);
|
|
case "stats.isSymbolicLink":
|
|
return finish(`scr_stats_is_symlink(${arg(0)})`);
|
|
case "stats.blocks":
|
|
return finish(`scr_stats_blocks(${arg(0)})`);
|
|
case "stats.nlink":
|
|
return finish(`scr_stats_nlink(${arg(0)})`);
|
|
case "stats.atimeMs":
|
|
return finish(`scr_stats_atime_ms(${arg(0)})`);
|
|
case "stats.mtimeMs":
|
|
return finish(`scr_stats_mtime_ms(${arg(0)})`);
|
|
case "stats.ctimeMs":
|
|
return finish(`scr_stats_ctime_ms(${arg(0)})`);
|
|
case "stats.isFile":
|
|
return finish(`scr_stats_is_file(${arg(0)})`);
|
|
case "stats.isDirectory":
|
|
return finish(`scr_stats_is_dir(${arg(0)})`);
|
|
case "stats.size":
|
|
return finish(`scr_stats_size(${arg(0)})`);
|
|
case "stats.dev":
|
|
return finish(`scr_stats_dev(${arg(0)})`);
|
|
case "stats.ino":
|
|
return finish(`scr_stats_ino(${arg(0)})`);
|
|
case "fs.toUnixTimestamp":
|
|
return finish(`scr_fs_to_unix_timestamp(${arg(0)})`);
|
|
// The fs argument-validation ladders: the always-throw Chk
|
|
// forms (validation error or the trailing fence) take the
|
|
// error.nodeThrow dummy pattern; mkdtempSyncChk and the lchmod
|
|
// pair answer real results on a validated pass.
|
|
case "fs.existsChk":
|
|
emitter.usesTimers = true; // the scheduled answer holds the loop open
|
|
return finish(`scr_fs_exists_async(${arg(0)}, ${arg(1)})`);
|
|
case "fs.renameCb": {
|
|
// The callback MOVES into the scheduled operation. Its
|
|
// adapter constructs the program-specific Error | null union
|
|
// (or dyn error/null for the JS lane) when the timer fires.
|
|
emitter.usesTimers = true;
|
|
const cbT = e.args[2]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: fs.rename callback not a func");
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = emitter.fsRenameThunkFor(cbT);
|
|
emitter.line(`scr_fs_rename_async(${arg(0)}, ${arg(1)}, ${cb.name}, &${adapter});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "fs.mkdtempChk":
|
|
return finish(
|
|
`(scr_fs_mkdtemp_chk(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
|
|
);
|
|
case "fs.mkdtempSyncChk":
|
|
return finish(`scr_fs_mkdtemp_sync_chk(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "fs.readFileChk":
|
|
return finish(
|
|
`(scr_fs_read_file_chk(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
|
|
);
|
|
case "fs.opendirChk":
|
|
return finish(
|
|
`(scr_fs_opendir_chk(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
|
|
);
|
|
case "fs.watchFileChk":
|
|
return finish(
|
|
`(scr_fs_watch_file_chk(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
|
|
);
|
|
case "fs.lchmodChk":
|
|
return finish(
|
|
`(scr_fs_lchmod_chk(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
|
|
);
|
|
case "fs.lchmodSyncChk":
|
|
return finish(`scr_fs_lchmod_sync_chk(${arg(0)}, ${arg(1)})`);
|
|
case "fsp.lchmodChk":
|
|
return finish(`scr_fsp_lchmod_chk(${arg(0)}, ${arg(1)})`);
|
|
case "fs.readChk":
|
|
return finish(
|
|
`(scr_fs_read_chk(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
|
|
);
|
|
case "fs.streamOptsChk":
|
|
return finish(
|
|
`(scr_fs_stream_opts_chk(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
|
|
);
|
|
// The fs Buffer forms (scr_bytes_io.c): the sync forms throw
|
|
// like the utf8 forms (may-throw seed set); the promise form
|
|
// rejects instead.
|
|
case "fs.readFileSyncBytes":
|
|
return finish(`scr_fs_read_file_bytes(${arg(0)})`);
|
|
case "fs.writeFileSyncBytes":
|
|
return finish(`scr_fs_write_file_bytes(${arg(0)}, ${arg(1)})`);
|
|
case "fs.appendFileSyncBytes":
|
|
return finish(`scr_fs_append_file_bytes(${arg(0)}, ${arg(1)})`);
|
|
case "fsp.readFileBytes":
|
|
return finish(`scr_fsp_read_file_bytes(${arg(0)})`);
|
|
// zlib (scr_zlib.c — linked only when these appear on the IR):
|
|
// inflate throws on corrupt input (may-throw seed set).
|
|
case "zlib.deflateSync":
|
|
return finish(`scr_zlib_deflate(${arg(0)})`);
|
|
case "zlib.inflateSync":
|
|
return finish(`scr_zlib_inflate(${arg(0)})`);
|
|
case "zlib.deflateRawSync":
|
|
return finish(`scr_zlib_deflate_mode(${arg(0)}, 1.0, -1.0)`);
|
|
case "zlib.deflateLevelSync":
|
|
return finish(`scr_zlib_deflate_mode(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "zlib.inflateRawSync":
|
|
return finish(`scr_zlib_inflate_mode(${arg(0)}, 1.0)`);
|
|
case "zlib.gzipSync":
|
|
return finish(`scr_zlib_deflate_mode(${arg(0)}, 2.0, -1.0)`);
|
|
case "zlib.gunzipSync":
|
|
return finish(`scr_zlib_inflate_mode(${arg(0)}, 2.0)`);
|
|
case "zlib.unzipSync":
|
|
return finish(`scr_zlib_inflate_mode(${arg(0)}, 3.0)`);
|
|
case "zlib.crc32":
|
|
return finish(`scr_zlib_crc32(${arg(0)}, ${arg(1)})`);
|
|
case "zlib.deflateCb":
|
|
case "zlib.inflateCb":
|
|
case "zlib.deflateRawCb":
|
|
case "zlib.inflateRawCb":
|
|
case "zlib.gzipCb":
|
|
case "zlib.gunzipCb":
|
|
case "zlib.unzipCb": {
|
|
emitter.usesTimers = true;
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: zlib callback not a func");
|
|
const callback = args[1]!;
|
|
emitter.moveTemp(callback);
|
|
const adapter = emitter.zlibBytesThunkFor(cbT);
|
|
const compressing = fn === "zlib.deflateCb" || fn === "zlib.deflateRawCb" || fn === "zlib.gzipCb";
|
|
const mode = fn === "zlib.deflateRawCb" || fn === "zlib.inflateRawCb"
|
|
? 1
|
|
: fn === "zlib.gzipCb" || fn === "zlib.gunzipCb"
|
|
? 2
|
|
: fn === "zlib.unzipCb"
|
|
? 3
|
|
: 0;
|
|
emitter.line(`scr_zlib_codec_async(${arg(0)}, ${mode}.0, ${compressing ? "true" : "false"}, ${callback.name}, &${adapter});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "fsp.readFile":
|
|
return finish(`scr_fsp_read_file(${arg(0)})`);
|
|
case "fsp.writeFile":
|
|
return finish(`scr_fsp_write_file(${arg(0)}, ${arg(1)})`);
|
|
case "fsp.writeFileMode":
|
|
return finish(`scr_fsp_write_file_mode(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "fsp.mkdir":
|
|
return finish(`scr_fsp_mkdir(${arg(0)})`);
|
|
case "fsp.mkdirMode":
|
|
return finish(`scr_fsp_mkdir_mode(${arg(0)}, ${arg(1)})`);
|
|
case "fsp.mkdirRecursive":
|
|
return finish(`scr_fsp_mkdir_recursive(${arg(0)})`);
|
|
case "fsp.mkdirRecursiveMode":
|
|
return finish(`scr_fsp_mkdir_recursive_mode(${arg(0)}, ${arg(1)})`);
|
|
case "fsp.unlink":
|
|
return finish(`scr_fsp_unlink(${arg(0)})`);
|
|
case "fsp.chmod":
|
|
return finish(`scr_fsp_chmod(${arg(0)}, ${arg(1)})`);
|
|
case "fsp.rename":
|
|
return finish(`scr_fsp_rename(${arg(0)}, ${arg(1)})`);
|
|
case "fsp.readdir":
|
|
return finish(`scr_fsp_readdir(${arg(0)})`);
|
|
case "fsp.rm":
|
|
return finish(`scr_fsp_rm(${arg(0)})`);
|
|
case "fsp.stat":
|
|
return finish(`scr_fsp_stat(${arg(0)})`);
|
|
case "fsp.realpath":
|
|
return finish(`scr_fsp_realpath(${arg(0)})`);
|
|
case "fsp.lstat":
|
|
return finish(`scr_fsp_lstat(${arg(0)})`);
|
|
case "fsp.open":
|
|
return finish(`scr_fsp_open(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "fileHandle.fd":
|
|
return finish(`scr_file_handle_fd(${arg(0)})`);
|
|
case "fileHandle.close":
|
|
return finish(`scr_file_handle_close_promise(${arg(0)})`);
|
|
case "fileHandle.readFile":
|
|
return finish(`scr_file_handle_read_file_promise(${arg(0)}, ${arg(1)})`);
|
|
case "fileHandle.readFileBytes":
|
|
return finish(`scr_file_handle_read_file_bytes_promise(${arg(0)}, ${arg(1)})`);
|
|
case "fileHandle.writeFile":
|
|
return finish(`scr_file_handle_write_file_promise(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "fileHandle.writeFileBytes":
|
|
return finish(`scr_file_handle_write_file_bytes_promise(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "fileHandle.stat":
|
|
return finish(`scr_file_handle_stat_promise(${arg(0)})`);
|
|
case "fileHandle.read":
|
|
case "fileHandle.writeBytes":
|
|
case "fileHandle.writeStr": {
|
|
if (e.type.kind !== "promise" || e.type.inner.kind !== "record") {
|
|
throw new InternalCompilerError(`emitter bug: ${e.fn} result`);
|
|
}
|
|
const inner = e.type.inner;
|
|
const countFn = e.fn === "fileHandle.read"
|
|
? `scr_file_handle_read(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`
|
|
: e.fn === "fileHandle.writeBytes"
|
|
? `scr_file_handle_write_bytes(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`
|
|
: `scr_file_handle_write_str(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`;
|
|
const count = emitter.newTemp(F64, countFn);
|
|
const row = emitter.newTemp(inner, `${mangleRecordNew(inner.shapeId)}()`);
|
|
const countField = e.fn === "fileHandle.read" ? "bytesRead" : "bytesWritten";
|
|
emitter.line(`${row.name}->${mangleField(countField)} = ${count.name};`);
|
|
emitter.line(`${row.name}->${mangleField("buffer")} = ${retainCallC(e.args[1]!.type, arg(1))};`);
|
|
const rc = vAdapters(inner);
|
|
emitter.moveTemp(row); // promise fulfillment owns the result record
|
|
return finish(`scr_promise_settled_ref(${row.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(inner)})`);
|
|
}
|
|
case "fs.realpathSync":
|
|
return finish(`scr_fs_realpath(${arg(0)})`);
|
|
case "fs.realpathNativeSync":
|
|
return finish(`scr_fs_realpath_promise(${arg(0)})`);
|
|
// The fs option forms (scr_lib.c) — all in the may-throw seed,
|
|
// like the rest of sync fs.
|
|
case "fs.mkdirRecursiveSync":
|
|
return finish(`scr_fs_mkdir_recursive(${arg(0)})`);
|
|
case "fs.rmOptsSync":
|
|
return finish(`scr_fs_rm_opts(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "fs.rmRetrySync":
|
|
return finish(`scr_fs_rm_opts_retry(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`);
|
|
case "fs.mkdtempSync":
|
|
return finish(`scr_fs_mkdtemp(${arg(0)})`);
|
|
case "fs.accessSync":
|
|
return finish(`scr_fs_access(${arg(0)}, ${arg(1)})`);
|
|
// The wider sync fs slice (scr_lib.c): syscall wrappers with
|
|
// Node's errno message shapes, `.code` stamped like the rest.
|
|
case "fs.unlinkSync":
|
|
return finish(`scr_fs_unlink(${arg(0)})`);
|
|
case "fs.chmodSync":
|
|
return finish(`scr_fs_chmod(${arg(0)}, ${arg(1)})`);
|
|
case "fs.chownSync":
|
|
return finish(`scr_fs_chown(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "fs.copyFileSync":
|
|
return finish(`scr_fs_copyfile(${arg(0)}, ${arg(1)})`);
|
|
case "fs.renameSync":
|
|
return finish(`scr_fs_rename(${arg(0)}, ${arg(1)})`);
|
|
case "fs.lstatSync":
|
|
return finish(`scr_fs_lstat(${arg(0)})`);
|
|
case "fs.writeFileModeSync":
|
|
return finish(`scr_fs_write_file_mode(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "fs.mkdirModeSync":
|
|
return finish(`scr_fs_mkdir_mode(${arg(0)}, ${arg(1)})`);
|
|
case "fs.mkdirRecursiveModeSync":
|
|
return finish(`scr_fs_mkdir_recursive_mode(${arg(0)}, ${arg(1)})`);
|
|
// Atomics.wait — the synchronous-sleep idiom (scr_lib.c): a
|
|
// real nanosleep; +1 string result; never throws.
|
|
case "atomics.wait":
|
|
return finish(`scr_atomics_wait(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`);
|
|
case "fs.readFdSync":
|
|
// args[1] is the (always-"utf8") encoding: evaluated for
|
|
// JS-exact side-effect order, ignored by the runtime.
|
|
return finish(`scr_fs_read_fd(${arg(0)})`);
|
|
case "fs.readFdSyncBytes":
|
|
return finish(`scr_fs_read_fd_bytes(${arg(0)})`);
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: filesystem libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitPathUrlLibCall(state: LibCallState): Temp {
|
|
const { e, emitter, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
// node:path (scr_path.c): pure string algorithms, POSIX rules.
|
|
// join/resolve borrow ONE packed string[] (the frontend built it);
|
|
// results are always +1 fresh strings. None of these throw.
|
|
case "path.join":
|
|
return finish(`scr_path_join(${arg(0)})`);
|
|
case "path.resolve":
|
|
return finish(`scr_path_resolve(${arg(0)})`);
|
|
case "path.normalize":
|
|
return finish(`scr_path_normalize(${arg(0)})`);
|
|
case "path.dirname":
|
|
return finish(`scr_path_dirname(${arg(0)})`);
|
|
case "path.basename":
|
|
return finish(`scr_path_basename(${arg(0)}, ${arg(1)})`);
|
|
case "path.extname":
|
|
return finish(`scr_path_extname(${arg(0)})`);
|
|
case "path.isAbsolute":
|
|
return finish(`scr_path_is_absolute(${arg(0)})`);
|
|
case "path.relative":
|
|
return finish(`scr_path_relative(${arg(0)}, ${arg(1)})`);
|
|
case "path.toNamespacedPath":
|
|
return finish(`scr_path_to_namespaced_path(${arg(0)})`);
|
|
// The win32 family (Node v24's path.win32 in scr_path.c): the
|
|
// path.win32 namespace anywhere, and the bare module's binding
|
|
// under a win32 target.
|
|
case "path.win32Join":
|
|
return finish(`scr_path_win32_join(${arg(0)})`);
|
|
case "path.win32Resolve":
|
|
return finish(`scr_path_win32_resolve(${arg(0)})`);
|
|
case "path.win32Normalize":
|
|
return finish(`scr_path_win32_normalize(${arg(0)})`);
|
|
case "path.win32Dirname":
|
|
return finish(`scr_path_win32_dirname(${arg(0)})`);
|
|
case "path.win32Basename":
|
|
return finish(`scr_path_win32_basename(${arg(0)}, ${arg(1)})`);
|
|
case "path.win32Extname":
|
|
return finish(`scr_path_win32_extname(${arg(0)})`);
|
|
case "path.win32IsAbsolute":
|
|
return finish(`scr_path_win32_is_absolute(${arg(0)})`);
|
|
case "path.win32Relative":
|
|
return finish(`scr_path_win32_relative(${arg(0)}, ${arg(1)})`);
|
|
case "path.win32ToNamespacedPath":
|
|
return finish(`scr_path_win32_to_namespaced_path(${arg(0)})`);
|
|
case "os.homedir":
|
|
return finish(`scr_os_homedir()`);
|
|
case "os.type":
|
|
return finish(`scr_os_type()`);
|
|
case "os.totalmem":
|
|
return finish(`scr_os_totalmem()`);
|
|
case "os.release":
|
|
return finish(`scr_os_release()`);
|
|
case "os.userName":
|
|
return finish(`scr_os_user_name()`);
|
|
case "os.userShell":
|
|
return finish(`scr_os_user_shell()`);
|
|
case "os.userHomedir":
|
|
return finish(`scr_os_user_homedir()`);
|
|
case "os.tmpdir":
|
|
return finish(`scr_os_tmpdir()`);
|
|
case "os.networkInterfaces": {
|
|
// The Dict<NetworkInterfaceInfo[]> record, built inline from a
|
|
// getifaddrs(3) snapshot (scr_lib.c) — the dgram.address
|
|
// pattern scaled up. Every shape/union/tag below comes from
|
|
// the call's own type; the frontend verified the structure
|
|
// (lowerOsNetworkInterfacesCall), so lookups here only guard
|
|
// against emitter bugs. Rows append to their interface's
|
|
// bucket in snapshot order; a first row makes the bucket (a
|
|
// fresh Info[] wrapped into the `Info[] | undefined` union arm
|
|
// the overflow map stores).
|
|
if (e.type.kind !== "record") throw new InternalCompilerError("emitter bug: networkInterfaces result is not a record");
|
|
const dictShape = emitter.recordsById.get(e.type.shapeId);
|
|
const iv = dictShape?.indexValue;
|
|
if (!dictShape || iv?.kind !== "union") throw new InternalCompilerError("emitter bug: networkInterfaces dict shape");
|
|
const ivDef = emitter.unionsById.get(iv.unionId);
|
|
const arrTag = ivDef?.arms.findIndex((a) => a.kind === "array") ?? -1;
|
|
const arrT = ivDef?.arms[arrTag];
|
|
if (arrT?.kind !== "array" || arrT.elem.kind !== "union") throw new InternalCompilerError("emitter bug: networkInterfaces bucket type");
|
|
const infoT = arrT.elem;
|
|
const infoDef = emitter.unionsById.get(infoT.unionId);
|
|
if (!infoDef || infoDef.arms.length !== 2) throw new InternalCompilerError("emitter bug: networkInterfaces Info union");
|
|
const tag6 = infoDef.arms.findIndex(
|
|
(a) => a.kind === "record" && emitter.recordsById.get(a.shapeId)?.fields.find((f) => f.name === "scopeid")?.type.kind === "f64",
|
|
);
|
|
const tag4 = 1 - tag6;
|
|
const armShape = (tag: number): { t: IrType & { kind: "record" }; shape: IrRecordShape } => {
|
|
const t = infoDef.arms[tag];
|
|
if (t?.kind !== "record") throw new InternalCompilerError("emitter bug: networkInterfaces Info arm");
|
|
const shape = emitter.recordsById.get(t.shapeId);
|
|
if (!shape) throw new InternalCompilerError("emitter bug: networkInterfaces Info shape");
|
|
return { t, shape };
|
|
};
|
|
const dict = emitter.newTemp(e.type, `${mangleRecordNew(e.type.shapeId)}()`);
|
|
const snap = `sc_t${emitter.tempCounter++}`;
|
|
const i = `sc_t${emitter.tempCounter++}`;
|
|
const n = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`ScrIfaddrs *${snap} = scr_os_ifaddrs();${emitter.srcComment(e.loc)}`);
|
|
emitter.line(`for (size_t ${i} = 0, ${n} = scr_os_ifaddrs_count(${snap}); ${i} < ${n}; ${i}++) {`);
|
|
emitter.indent++;
|
|
const row = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`ScrUnion *${row};`);
|
|
const emitRow = (tag: number, v6: boolean): void => {
|
|
const { t, shape } = armShape(tag);
|
|
const cidrT = shape.fields.find((f) => f.name === "cidr")?.type;
|
|
const cidrDef = cidrT?.kind === "union" ? emitter.unionsById.get(cidrT.unionId) : undefined;
|
|
if (cidrT?.kind !== "union" || !cidrDef) throw new InternalCompilerError("emitter bug: networkInterfaces cidr type");
|
|
const cidrStrTag = cidrDef.arms.findIndex((a) => a.kind === "string");
|
|
const cidrNullTag = cidrDef.arms.findIndex((a) => a.kind === "nullT");
|
|
const r = `sc_t${emitter.tempCounter++}`;
|
|
const struct = mangleRecordStruct(t.shapeId);
|
|
emitter.line(`${struct} *${r} = ${mangleRecordNew(t.shapeId)}();`);
|
|
emitter.line(`${r}->${mangleField("address")} = scr_os_ifaddrs_address(${snap}, ${i});`);
|
|
emitter.line(`${r}->${mangleField("netmask")} = scr_os_ifaddrs_netmask(${snap}, ${i});`);
|
|
emitter.line(`${r}->${mangleField("family")} = scr_os_ifaddrs_family(${snap}, ${i});`);
|
|
emitter.line(`${r}->${mangleField("mac")} = scr_os_ifaddrs_mac(${snap}, ${i});`);
|
|
emitter.line(`${r}->${mangleField("internal")} = scr_os_ifaddrs_internal(${snap}, ${i});`);
|
|
const cs = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`ScrStr *${cs} = scr_os_ifaddrs_cidr(${snap}, ${i});`);
|
|
emitter.line(
|
|
`${r}->${mangleField("cidr")} = ${cs} ? scr_union_new_ref(${cidrStrTag}, ${cs}, &scr_str_retain_v, &scr_str_release_v, NULL) : scr_union_retain(${emitter.unitInstanceRef(cidrT.unionId, cidrNullTag)});`,
|
|
);
|
|
if (v6) {
|
|
emitter.line(`${r}->${mangleField("scopeid")} = scr_os_ifaddrs_scopeid(${snap}, ${i});`);
|
|
} else {
|
|
const st = shape.fields.find((f) => f.name === "scopeid")?.type;
|
|
if (st?.kind !== "union") throw new InternalCompilerError("emitter bug: networkInterfaces IPv4 scopeid type");
|
|
const undefTag = undefinedArmTag(st, emitter.unionsById);
|
|
emitter.line(`${r}->${mangleField("scopeid")} = scr_union_retain(${emitter.unitInstanceRef(st.unionId, undefTag)});`);
|
|
}
|
|
const rc = vAdapters(t);
|
|
emitter.line(`${row} = scr_union_new_ref(${tag}, ${r}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});`);
|
|
};
|
|
emitter.line(`if (scr_os_ifaddrs_ipv6(${snap}, ${i})) {`);
|
|
emitter.indent++;
|
|
emitRow(tag6, true);
|
|
emitter.indent--;
|
|
emitter.line(`} else {`);
|
|
emitter.indent++;
|
|
emitRow(tag4, false);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
const name = `sc_t${emitter.tempCounter++}`;
|
|
const cell = `sc_t${emitter.tempCounter++}`;
|
|
const rows = `sc_t${emitter.tempCounter++}`;
|
|
emitter.line(`ScrStr *${name} = scr_os_ifaddrs_name(${snap}, ${i});`);
|
|
emitter.line(`ScrUnion *${cell} = scr_map_get_str_ref(${dict.name}->${OVERFLOW_MEMBER}, ${name});`);
|
|
emitter.line(`ScrArr *${rows};`);
|
|
emitter.line(`if (${cell}) {`);
|
|
emitter.indent++;
|
|
emitter.line(`${rows} = scr_arr_retain((ScrArr *)scr_union_peek(${cell}));`);
|
|
emitter.line(`scr_union_release(${cell});`);
|
|
emitter.indent--;
|
|
emitter.line(`} else {`);
|
|
emitter.indent++;
|
|
emitter.line(`${rows} = ${emitter.arrNewC(infoT, 1)};`);
|
|
const arrRc = vAdapters(arrT);
|
|
emitter.line(
|
|
`scr_map_set_str_ref(${dict.name}->${OVERFLOW_MEMBER}, ${name}, scr_union_new_ref(${arrTag}, scr_arr_retain(${rows}), &${arrRc.retain}, &${arrRc.release}, ${emitter.traceArgC(arrT)}));`,
|
|
);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
emitter.line(`scr_arr_push_ref(${rows}, ${row});`); // push takes ownership of the row
|
|
emitter.line(`scr_arr_release(${rows});`);
|
|
emitter.line(`scr_str_release(${name});`);
|
|
emitter.indent--;
|
|
emitter.line(`}`);
|
|
emitter.line(`scr_os_ifaddrs_free(${snap});`);
|
|
return dict;
|
|
}
|
|
case "url.new":
|
|
return finish(`scr_url_new(${arg(0)})`);
|
|
case "url.newBase":
|
|
return finish(`scr_url_new_base(${arg(0)}, ${arg(1)})`);
|
|
case "url.protocol":
|
|
return finish(`scr_url_protocol(${arg(0)})`);
|
|
case "url.origin":
|
|
return finish(`scr_url_origin(${arg(0)})`);
|
|
case "url.username":
|
|
return finish(`scr_url_username(${arg(0)})`);
|
|
case "url.password":
|
|
return finish(`scr_url_password(${arg(0)})`);
|
|
case "url.host":
|
|
return finish(`scr_url_host(${arg(0)})`);
|
|
case "url.hostname":
|
|
return finish(`scr_url_hostname(${arg(0)})`);
|
|
case "url.port":
|
|
return finish(`scr_url_port(${arg(0)})`);
|
|
case "url.pathname":
|
|
return finish(`scr_url_pathname(${arg(0)})`);
|
|
case "url.href":
|
|
return finish(`scr_url_href(${arg(0)})`);
|
|
case "url.fileURLToPathUrl":
|
|
return finish(`scr_url_to_path(${arg(0)})`);
|
|
case "url.fileURLToPathStr":
|
|
return finish(`scr_url_str_to_path(${arg(0)})`);
|
|
case "url.pathToFileURL":
|
|
// The win32-target flavor is the SAME entry point (the bridge
|
|
// dispatches by _WIN32) — the distinct name exists for the
|
|
// may-throw seed (the win32 arm's UNC TypeErrors).
|
|
case "url.pathToFileURLWin32":
|
|
return finish(`scr_url_from_path(${arg(0)})`);
|
|
// URLSearchParams (scr_url.c — always linked with the url unit).
|
|
// Constructions come back +1; sp.fromPairs throws Node's
|
|
// ERR_INVALID_TUPLE catchably (may-throw seed set). Mutators are
|
|
// void borrowed-receiver calls; a live view's URL query updates
|
|
// inside the runtime.
|
|
case "sp.new":
|
|
return finish(`scr_sp_new()`);
|
|
case "sp.parse":
|
|
return finish(`scr_sp_parse(${arg(0)})`);
|
|
case "sp.copy":
|
|
return finish(`scr_sp_copy(${arg(0)})`);
|
|
case "sp.fromPairs":
|
|
return finish(`scr_sp_from_pairs(${arg(0)})`);
|
|
case "sp.with":
|
|
return finish(`scr_sp_with(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "url.searchParams":
|
|
return finish(`scr_url_search_params(${arg(0)})`);
|
|
case "url.search":
|
|
return finish(`scr_url_search(${arg(0)})`);
|
|
case "url.hash":
|
|
return finish(`scr_url_hash(${arg(0)})`);
|
|
case "sp.get": {
|
|
// `string | null` — the sym.desc pattern with a null arm: the
|
|
// runtime answers a +1 string or NULL.
|
|
if (e.type.kind !== "union") {
|
|
throw new InternalCompilerError("emitter bug: sp.get result is not a union");
|
|
}
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
|
|
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
|
if (strTag < 0 || nullTag < 0) {
|
|
throw new InternalCompilerError("emitter bug: sp.get union lacks its arms");
|
|
}
|
|
const raw = emitter.newTemp(STRING, `scr_sp_get(${arg(0)}, ${arg(1)})`);
|
|
emitter.moveTemp(raw); // ownership passes into the union arm below
|
|
const present = `scr_union_new_ref(${strTag}, ${raw.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, nullTag);
|
|
return emitter.newTemp(e.type, `${raw.name} != NULL ? ${present} : ${absent}`);
|
|
}
|
|
case "sp.getAll":
|
|
return finish(`scr_sp_get_all(${arg(0)}, ${arg(1)})`);
|
|
case "sp.append":
|
|
return finish(`scr_sp_append(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "sp.set":
|
|
return finish(`scr_sp_set(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "sp.delete":
|
|
return finish(`scr_sp_delete(${arg(0)}, ${arg(1)})`);
|
|
case "sp.deleteValue":
|
|
return finish(`scr_sp_delete_value(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "sp.has":
|
|
return finish(`scr_sp_has(${arg(0)}, ${arg(1)})`);
|
|
case "sp.hasValue":
|
|
return finish(`scr_sp_has_value(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "sp.sort":
|
|
return finish(`scr_sp_sort(${arg(0)})`);
|
|
case "sp.size":
|
|
return finish(`scr_sp_size(${arg(0)})`);
|
|
case "sp.toString":
|
|
return finish(`scr_sp_to_string(${arg(0)})`);
|
|
case "sp.keyAt":
|
|
return finish(`scr_sp_key_at(${arg(0)}, ${arg(1)})`);
|
|
case "sp.valAt":
|
|
return finish(`scr_sp_val_at(${arg(0)}, ${arg(1)})`);
|
|
// node:querystring (scr_qs.c — linked exactly when parse/
|
|
// stringify/unescape appear, moduleUsesQs). escape IS the
|
|
// component encoder (Node's qsEscape set equals
|
|
// encodeURIComponent's), so it emits the always-linked codec
|
|
// and never pulls the unit. Borrow; string results +1; no throw.
|
|
case "qs.escape":
|
|
return finish(`scr_str_encode_uri_component(${arg(0)})`);
|
|
case "qs.unescape":
|
|
return finish(`scr_qs_unescape(${arg(0)})`);
|
|
case "qs.stringify":
|
|
return finish(`scr_qs_stringify(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "qs.parse": {
|
|
// The ParsedUrlQuery dictionary: a fresh pure-index-signature
|
|
// record whose overflow map the runtime scan fills
|
|
// (scr_qs_parse_into groups repeats into string[] buckets).
|
|
// The frontend verified the shape (lowerQuerystringParseCall);
|
|
// lookups here only guard emitter bugs. Args: qs, sep, eq,
|
|
// maxKeys.
|
|
if (e.type.kind !== "record") throw new InternalCompilerError("emitter bug: qs.parse result is not a record");
|
|
const dictShape = emitter.recordsById.get(e.type.shapeId);
|
|
const iv = dictShape?.indexValue;
|
|
if (!dictShape || iv?.kind !== "union") throw new InternalCompilerError("emitter bug: qs.parse dict shape");
|
|
const ivDef = emitter.unionsById.get(iv.unionId);
|
|
const strTag = ivDef?.arms.findIndex((a) => a.kind === "string") ?? -1;
|
|
const arrTag = ivDef?.arms.findIndex((a) => a.kind === "array") ?? -1;
|
|
if (strTag < 0 || arrTag < 0) throw new InternalCompilerError("emitter bug: qs.parse index union lacks its arms");
|
|
const dict = emitter.newTemp(e.type, `${mangleRecordNew(e.type.shapeId)}()`);
|
|
emitter.line(
|
|
`scr_qs_parse_into(${dict.name}->${OVERFLOW_MEMBER}, ${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${strTag}, ${arrTag});${emitter.srcComment(e.loc)}`,
|
|
);
|
|
return dict;
|
|
}
|
|
case "sp.finished":
|
|
// The stream/promises form: a +1 pending promise the terminal
|
|
// watcher settles — no callback, no cleanup exposure.
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_sp_finished((ScrStream *)${arg(0)})`);
|
|
case "sp.pipeline": {
|
|
// pipeline(count, s1..sn) settling a void promise; the stream
|
|
// list rides the callback form's compound literal.
|
|
emitter.usesTimers = true;
|
|
const countArg = e.args[0]!;
|
|
if (countArg.kind !== "numLit") throw new InternalCompilerError(`emitter bug: ${fn} count not a literal`);
|
|
const n = countArg.value;
|
|
const list = Array.from({ length: n }, (_, i) => `(ScrStream *)${arg(1 + i)}`).join(", ");
|
|
return finish(`scr_sp_pipeline(${n}, (ScrStream *[]){ ${list} })`);
|
|
}
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: pathUrl libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitPrimitiveLibCall(state: LibCallState): Temp {
|
|
const { e, emitter, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
// Math.max/min over one spread number[] — the JS fold in C
|
|
// (NaN poisons, ±0 order, empty → ∓Infinity). Borrows; no throw.
|
|
case "math.maxArr":
|
|
return finish(`scr_math_max_arr(${arg(0)})`);
|
|
case "math.minArr":
|
|
return finish(`scr_math_min_arr(${arg(0)})`);
|
|
case "math.hypotArr":
|
|
return finish(`scr_math_hypot_arr(${arg(0)})`);
|
|
// Math.floor/trunc/ceil — the C functions ARE the JS operations
|
|
// (math.h is always included). Borrow nothing; no throw.
|
|
case "math.floor":
|
|
return finish(`floor(${arg(0)})`);
|
|
case "math.trunc":
|
|
return finish(`trunc(${arg(0)})`);
|
|
case "math.ceil":
|
|
return finish(`ceil(${arg(0)})`);
|
|
case "math.sin":
|
|
return finish(`sin(${arg(0)})`);
|
|
case "math.sinh":
|
|
return finish(`sinh(${arg(0)})`);
|
|
case "math.cos":
|
|
return finish(`cos(${arg(0)})`);
|
|
case "math.cosh":
|
|
return finish(`cosh(${arg(0)})`);
|
|
case "math.tan":
|
|
return finish(`tan(${arg(0)})`);
|
|
case "math.tanh":
|
|
return finish(`tanh(${arg(0)})`);
|
|
case "math.asin":
|
|
return finish(`asin(${arg(0)})`);
|
|
case "math.asinh":
|
|
return finish(`asinh(${arg(0)})`);
|
|
case "math.acos":
|
|
return finish(`acos(${arg(0)})`);
|
|
case "math.acosh":
|
|
return finish(`acosh(${arg(0)})`);
|
|
case "math.atan":
|
|
return finish(`atan(${arg(0)})`);
|
|
case "math.atanh":
|
|
return finish(`atanh(${arg(0)})`);
|
|
case "math.cbrt":
|
|
return finish(`cbrt(${arg(0)})`);
|
|
case "math.clz32":
|
|
return finish(`scr_math_clz32(${arg(0)})`);
|
|
case "math.sign":
|
|
// Each argument was evaluated into a temp. Returning that temp
|
|
// for unordered/zero inputs preserves NaN and the sign of zero.
|
|
return finish(`(${arg(0)} > 0.0 ? 1.0 : (${arg(0)} < 0.0 ? -1.0 : ${arg(0)}))`);
|
|
case "math.exp":
|
|
return finish(`exp(${arg(0)})`);
|
|
case "math.expm1":
|
|
return finish(`expm1(${arg(0)})`);
|
|
case "math.fround":
|
|
return finish(`scr_math_fround(${arg(0)})`);
|
|
case "math.sqrt":
|
|
return finish(`sqrt(${arg(0)})`);
|
|
case "math.log":
|
|
return finish(`log(${arg(0)})`);
|
|
case "math.log1p":
|
|
return finish(`log1p(${arg(0)})`);
|
|
case "math.log2":
|
|
return finish(`log2(${arg(0)})`);
|
|
case "math.log10":
|
|
return finish(`log10(${arg(0)})`);
|
|
case "math.atan2":
|
|
return finish(`atan2(${arg(0)}, ${arg(1)})`);
|
|
case "math.pow":
|
|
return finish(`scr_math_pow(${arg(0)}, ${arg(1)})`);
|
|
case "math.imul":
|
|
return finish(`scr_math_imul(${arg(0)}, ${arg(1)})`);
|
|
// Math.abs — C fabs IS the JS operation. Math.round — the JS
|
|
// half-toward-+Infinity rule (scr_lib.c; C round() differs on
|
|
// halves and naive floor(x+0.5) drifts at the epsilon boundary).
|
|
case "math.abs":
|
|
return finish(`fabs(${arg(0)})`);
|
|
case "math.round":
|
|
return finish(`scr_math_round(${arg(0)})`);
|
|
// The scalar Math.min/max (scr_lib.c — fmin/fmax drop NaN, so
|
|
// these are the JS folds) and Math.random (arc4random-backed
|
|
// uniform [0,1), SEMANTICS.md 62). Borrow nothing; no throw.
|
|
case "math.min":
|
|
return finish(`scr_math_min(${arg(0)}, ${arg(1)})`);
|
|
case "math.max":
|
|
return finish(`scr_math_max(${arg(0)}, ${arg(1)})`);
|
|
case "math.random":
|
|
return finish(`scr_math_random()`);
|
|
// The static global parsers/tests (scr_string.c). Borrow; no throw.
|
|
case "num.parseInt":
|
|
return finish(`scr_parse_int(${arg(0)}, ${arg(1)})`);
|
|
case "num.parseFloat":
|
|
return finish(`scr_parse_float(${arg(0)})`);
|
|
case "num.fromString":
|
|
return finish(`scr_string_to_number(${arg(0)})`);
|
|
case "num.isNaN":
|
|
return finish(`(bool)isnan(${arg(0)})`);
|
|
// The URI codecs (scr_string.c). Borrow; results +1. decode
|
|
// throws the spec's catchable URIError on bad hex or invalid
|
|
// UTF-8 octets (may-throw seed set); the encoders never throw.
|
|
case "str.encodeUriComponent":
|
|
return finish(`scr_str_encode_uri_component(${arg(0)})`);
|
|
case "str.decodeUriComponent":
|
|
return finish(`scr_str_decode_uri_component(${arg(0)})`);
|
|
case "str.encodeUri":
|
|
return finish(`scr_encode_uri(${arg(0)})`);
|
|
// RegExp.escape (scr_string.c): total per-code-point escape —
|
|
// borrows, +1 result, never throws.
|
|
case "regexp.escape":
|
|
return finish(`scr_regexp_escape(${arg(0)})`);
|
|
// The WHATWG base64 globals (scr_string.c): dyn arg borrowed
|
|
// (WebIDL ToString in the runtime), +1 string result; malformed
|
|
// input throws the catchable DOMException InvalidCharacterError
|
|
// (may-throw seed set). The zero-argument form always throws
|
|
// Node's TypeError [ERR_MISSING_ARGS].
|
|
case "str.atob":
|
|
return finish(`scr_atob(${arg(0)})`);
|
|
case "str.btoa":
|
|
return finish(`scr_btoa(${arg(0)})`);
|
|
case "str.b64Missing":
|
|
return finish(`scr_b64_missing_arg()`);
|
|
// Number.prototype formatters (scr_lib.c). Borrow nothing;
|
|
// successful results +1; explicit toFixed may throw RangeError.
|
|
case "num.toExponential":
|
|
return finish(`scr_num_to_exponential(${arg(0)})`);
|
|
case "num.toFixed0":
|
|
return finish(`scr_num_to_fixed0(${arg(0)})`);
|
|
case "num.toFixed":
|
|
return finish(`scr_num_to_fixed(${arg(0)}, ${arg(1)})`);
|
|
// Object.is over two numbers — SameValue on doubles. No throw.
|
|
case "num.sameValue":
|
|
return finish(`scr_num_same_value(${arg(0)}, ${arg(1)})`);
|
|
// Intl.NumberFormat("en-US").format / toLocaleString("en-US")
|
|
// with default options (scr_lib.c). +1 string; no throw.
|
|
case "intl.segmenterNew":
|
|
return finish(`scr_intl_segmenter_new()`);
|
|
case "intl.numFormatEnUs":
|
|
return finish(`scr_intl_num_format_en_us(${arg(0)})`);
|
|
// The URL surface (scr_url.c): construction and the two
|
|
// fileURLToPath receiver forms throw catchable TypeErrors
|
|
// (may-throw seed set); the getters are pure reads. Receivers
|
|
// and string args are borrowed; every result is +1.
|
|
// ES symbols (scr_symbol.c — linked exactly when these appear,
|
|
// moduleUsesSymbol). Fresh identities (+1); Symbol.for answers
|
|
// +1 on the registry's interned per-key symbol. None throw.
|
|
case "sym.new":
|
|
return finish(`scr_sym_new(${arg(0)})`);
|
|
case "sym.newAnon":
|
|
return finish(`scr_sym_new(NULL)`);
|
|
case "sym.for":
|
|
return finish(`scr_sym_for(${arg(0)})`);
|
|
case "sym.toString":
|
|
return finish(`scr_sym_to_string(${arg(0)})`);
|
|
case "bigint.parse":
|
|
return finish(`scr_bigint_parse(${arg(0)})`);
|
|
case "bigint.fromF64":
|
|
return finish(`scr_bigint_from_f64(${arg(0)})`);
|
|
case "bigint.neg":
|
|
return finish(`scr_bigint_neg(${arg(0)})`);
|
|
case "bigint.not":
|
|
return finish(`scr_bigint_not(${arg(0)})`);
|
|
case "bigint.add":
|
|
return finish(`scr_bigint_add(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.sub":
|
|
return finish(`scr_bigint_sub(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.mul":
|
|
return finish(`scr_bigint_mul(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.div":
|
|
return finish(`scr_bigint_div(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.mod":
|
|
return finish(`scr_bigint_mod(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.pow":
|
|
return finish(`scr_bigint_pow(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.and":
|
|
return finish(`scr_bigint_and(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.or":
|
|
return finish(`scr_bigint_or(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.xor":
|
|
return finish(`scr_bigint_xor(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.shl":
|
|
return finish(`scr_bigint_shl(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.shr":
|
|
return finish(`scr_bigint_shr(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.eq":
|
|
return finish(`scr_bigint_eq(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.eqString":
|
|
return finish(`scr_bigint_eq_string(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.cmp":
|
|
return finish(`scr_bigint_cmp_f64(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.cmpNumber":
|
|
return finish(`scr_bigint_cmp_number(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.truthy":
|
|
return finish(`scr_bigint_truthy(${arg(0)})`);
|
|
case "bigint.toString":
|
|
return finish(`scr_bigint_to_string(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.inspect":
|
|
return finish(`scr_bigint_inspect(${arg(0)})`);
|
|
case "bigint.toF64":
|
|
return finish(`scr_bigint_to_f64(${arg(0)})`);
|
|
case "bigint.asUintN":
|
|
return finish(`scr_bigint_as_uint_n(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.asIntN":
|
|
return finish(`scr_bigint_as_int_n(${arg(0)}, ${arg(1)})`);
|
|
case "bigint.bufferRead":
|
|
return finish(`scr_bigint_buffer_read(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`);
|
|
case "bigint.bufferWrite":
|
|
return finish(`scr_bigint_buffer_write(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`);
|
|
case "bigint.dataViewGet":
|
|
return finish(`scr_bigint_dataview_get(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`);
|
|
case "bigint.dataViewSet":
|
|
return finish(`scr_bigint_dataview_set(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`);
|
|
case "sym.desc":
|
|
case "sym.keyFor": {
|
|
// `string | undefined` — the child.stdout pattern with a
|
|
// string arm: the runtime answers a +1 string or NULL.
|
|
if (e.type.kind !== "union") {
|
|
throw new InternalCompilerError(`emitter bug: ${e.fn} result is not a union`);
|
|
}
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
|
|
const undefTag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1;
|
|
if (strTag < 0 || undefTag < 0) {
|
|
throw new InternalCompilerError(`emitter bug: ${e.fn} union lacks its arms`);
|
|
}
|
|
const get = e.fn === "sym.desc" ? "scr_sym_desc" : "scr_sym_key_for";
|
|
const raw = emitter.newTemp(STRING, `${get}(${arg(0)})`);
|
|
emitter.moveTemp(raw); // ownership passes into the union arm below
|
|
const present = `scr_union_new_ref(${strTag}, ${raw.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, undefTag);
|
|
return emitter.newTemp(e.type, `${raw.name} != NULL ? ${present} : ${absent}`);
|
|
}
|
|
case "perf.now":
|
|
return finish(`scr_perf_now()`);
|
|
// The Number statics (scr_lib.c): pure f64 → bool, never throw.
|
|
case "number.isFinite":
|
|
return finish(`scr_num_is_finite(${arg(0)})`);
|
|
case "number.isNaN":
|
|
return finish(`scr_num_is_nan(${arg(0)})`);
|
|
case "number.isInteger":
|
|
return finish(`scr_num_is_integer(${arg(0)})`);
|
|
case "number.isSafeInteger":
|
|
return finish(`scr_num_is_safe_integer(${arg(0)})`);
|
|
case "date.now":
|
|
// Node's integer milliseconds since epoch. Never throws.
|
|
return finish(`scr_date_now()`);
|
|
case "date.newNow":
|
|
return finish(`scr_date_now()`);
|
|
case "date.newMs":
|
|
return finish(`scr_date_new_ms(${arg(0)})`);
|
|
case "date.newString":
|
|
return finish(`scr_date_parse_get_time(${arg(0)})`);
|
|
case "date.getTime":
|
|
case "date.valueOf":
|
|
return finish(`${arg(0)}`);
|
|
case "date.toISOString":
|
|
case "date.toISOStringValue":
|
|
// +1 string, or Node's "Invalid time value" RangeError
|
|
// (may-throw seed set).
|
|
return finish(`scr_date_to_iso(${arg(0)})`);
|
|
case "date.parse":
|
|
case "date.parseGetTime":
|
|
// The bounded date-string parse (X509 validity + ECMA format);
|
|
// NaN elsewhere. Never throws.
|
|
return finish(`scr_date_parse_get_time(${arg(0)})`);
|
|
case "date.utc":
|
|
// MakeDay/MakeTime/TimeClip over seven completed number
|
|
// arguments; NaN outside the time range. Never throws.
|
|
return finish(
|
|
`scr_date_utc(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)})`,
|
|
);
|
|
case "date.getFullYear":
|
|
return finish(`scr_date_get_full_year(${arg(0)}, false)`);
|
|
case "date.getUTCFullYear":
|
|
return finish(`scr_date_get_full_year(${arg(0)}, true)`);
|
|
case "date.getMonth":
|
|
return finish(`scr_date_get_month(${arg(0)}, false)`);
|
|
case "date.getUTCMonth":
|
|
return finish(`scr_date_get_month(${arg(0)}, true)`);
|
|
case "date.getDate":
|
|
return finish(`scr_date_get_date(${arg(0)}, false)`);
|
|
case "date.getUTCDate":
|
|
return finish(`scr_date_get_date(${arg(0)}, true)`);
|
|
case "date.getDay":
|
|
return finish(`scr_date_get_day(${arg(0)}, false)`);
|
|
case "date.getUTCDay":
|
|
return finish(`scr_date_get_day(${arg(0)}, true)`);
|
|
case "date.getHours":
|
|
return finish(`scr_date_get_hours(${arg(0)}, false)`);
|
|
case "date.getUTCHours":
|
|
return finish(`scr_date_get_hours(${arg(0)}, true)`);
|
|
case "date.getMinutes":
|
|
return finish(`scr_date_get_minutes(${arg(0)}, false)`);
|
|
case "date.getUTCMinutes":
|
|
return finish(`scr_date_get_minutes(${arg(0)}, true)`);
|
|
case "date.getSeconds":
|
|
return finish(`scr_date_get_seconds(${arg(0)}, false)`);
|
|
case "date.getUTCSeconds":
|
|
return finish(`scr_date_get_seconds(${arg(0)}, true)`);
|
|
case "date.getMilliseconds":
|
|
case "date.getUTCMilliseconds":
|
|
return finish(`scr_date_get_milliseconds(${arg(0)})`);
|
|
case "date.getTimezoneOffset":
|
|
return finish(`scr_date_get_timezone_offset(${arg(0)})`);
|
|
case "text.decode":
|
|
// WHATWG utf-8 decode with the leading BOM stripped
|
|
// (scr_bytes.c). Borrowed bytes; +1 string; never throws.
|
|
return finish(`scr_text_decode(${arg(0)})`);
|
|
case "text.decodeLegacy":
|
|
// Compile-time-labeled WHATWG legacy decode (scr_bytes.c).
|
|
// Borrowed bytes + numeric encoding id; +1 string.
|
|
return finish(`scr_text_decode_legacy(${arg(0)}, ${arg(1)})`);
|
|
case "string.fromCharCode":
|
|
// One packed f64[] (the frontend built it) or one bytes value
|
|
// (the spread-typed-array form); +1 string.
|
|
return finish(
|
|
e.args[0]!.type.kind === "bytes"
|
|
? `scr_str_from_char_code_bytes(${arg(0)})`
|
|
: `scr_str_from_char_code(${arg(0)})`,
|
|
);
|
|
case "string.fromCodePoint":
|
|
return finish(e.args[0]!.type.kind === "bytes"
|
|
? `scr_str_from_code_point_bytes(${arg(0)})`
|
|
: `scr_str_from_code_point(${arg(0)})`);
|
|
case "string.lastIndexOf":
|
|
return finish(`scr_str_last_index_of(${arg(0)}, ${arg(1)})`);
|
|
case "string.lastIndexOfFrom":
|
|
return finish(`scr_str_last_index_of_from(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "string.raw":
|
|
// Raw literals + pre-stringified substitutions; +1 string.
|
|
return finish(`scr_str_raw(${arg(0)}, ${arg(1)})`);
|
|
case "class.name":
|
|
// `X.name` through a class value: the class object's stored
|
|
// .name string (+1 — a no-op retain on the interned immortal).
|
|
return finish(`scr_classobj_name(${arg(0)})`);
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: primitive libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitUtilLibCall(state: LibCallState): Temp {
|
|
const { e, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
// node:util.parseArgs (scr_util.c): checked-dynamic config in,
|
|
// checked-dynamic result out; may throw a coded TypeError.
|
|
case "util.parseArgs":
|
|
return finish(`scr_util_parse_args(${arg(0)})`);
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: util libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitCryptoBytesLibCall(state: LibCallState): Temp {
|
|
const { e, emitter, args, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
case "crypto.x509Fingerprint":
|
|
// Throws Node's PEM error on unparseable input (may-throw).
|
|
return finish(`scr_crypto_x509_fingerprint(${arg(0)})`);
|
|
case "crypto.x509FingerprintStr":
|
|
return finish(`scr_crypto_x509_fingerprint_str(${arg(0)})`);
|
|
case "crypto.x509ValidFrom":
|
|
// The Validity walk shares the fingerprint's PEM contract
|
|
// (may-throw); the string is Node's ASN1_TIME_print shape.
|
|
return finish(`scr_crypto_x509_valid_from(${arg(0)})`);
|
|
case "crypto.x509ValidFromStr":
|
|
return finish(`scr_crypto_x509_valid_from_str(${arg(0)})`);
|
|
case "crypto.x509ValidTo":
|
|
return finish(`scr_crypto_x509_valid_to(${arg(0)})`);
|
|
case "crypto.x509ValidToStr":
|
|
return finish(`scr_crypto_x509_valid_to_str(${arg(0)})`);
|
|
// fs/promises: already-settled promises — failures REJECT (the
|
|
// runtime moves the pending exception into the promise), so no
|
|
// pending check here; the await re-throws catchably. args[1] of
|
|
// readFile is the (always-"utf8") encoding, evaluated for
|
|
// JS-exact side-effect order and ignored by the runtime.
|
|
case "crypto.randomUUID":
|
|
return finish(`scr_crypto_random_uuid()`);
|
|
case "crypto.randomBytesToString":
|
|
// May throw Node's RangeError (may-throw seed set).
|
|
return finish(`scr_crypto_random_string(${arg(0)}, ${arg(1)})`);
|
|
case "crypto.randomBytes":
|
|
// A real u8 Buffer (+1); same RangeError as the composed form.
|
|
return finish(`scr_crypto_random_bytes(${arg(0)})`);
|
|
case "crypto.hashDigestStr":
|
|
return finish(`scr_crypto_hash_digest_str(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "crypto.hashDigestBytes":
|
|
return finish(`scr_crypto_hash_digest_bytes(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "crypto.hashNew":
|
|
return finish(`scr_crypto_hash_new(${arg(0)})`);
|
|
case "crypto.hmacNewStr":
|
|
return finish(`scr_crypto_hmac_new_str(${arg(0)}, ${arg(1)})`);
|
|
case "crypto.hmacNewBytes":
|
|
return finish(`scr_crypto_hmac_new_bytes(${arg(0)}, ${arg(1)})`);
|
|
case "crypto.hashUpdateStr":
|
|
case "crypto.hmacUpdateStr":
|
|
return finish(`scr_crypto_hash_update_str(${arg(0)}, ${arg(1)})`);
|
|
case "crypto.hashUpdateBytes":
|
|
case "crypto.hmacUpdateBytes":
|
|
return finish(`scr_crypto_hash_update_bytes(${arg(0)}, ${arg(1)})`);
|
|
case "crypto.hashCopy":
|
|
return finish(`scr_crypto_hash_copy(${arg(0)})`);
|
|
case "crypto.hashDigestString":
|
|
case "crypto.hmacDigestString":
|
|
return finish(`scr_crypto_hash_digest_string(${arg(0)}, ${arg(1)})`);
|
|
case "crypto.hashDigestBuffer":
|
|
case "crypto.hmacDigestBuffer":
|
|
return finish(`scr_crypto_hash_digest_buffer(${arg(0)})`);
|
|
case "crypto.timingSafeEqual":
|
|
return finish(`scr_crypto_timing_safe_equal(${arg(0)}, ${arg(1)})`);
|
|
case "crypto.randomFill":
|
|
return finish(`scr_crypto_random_fill(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "crypto.randomFillRest":
|
|
return finish(`scr_crypto_random_fill_rest(${arg(0)}, ${arg(1)})`);
|
|
case "crypto.randomInt":
|
|
return finish(`scr_crypto_random_int(${arg(0)}, ${arg(1)})`);
|
|
case "crypto.pbkdf2":
|
|
return finish(`scr_crypto_pbkdf2(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`);
|
|
case "crypto.randomBytesCb":
|
|
case "crypto.pbkdf2Cb": {
|
|
emitter.usesTimers = true;
|
|
const cbIndex = fn === "crypto.randomBytesCb" ? 1 : 5;
|
|
const cbT = e.args[cbIndex]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: crypto callback not a func");
|
|
const callback = args[cbIndex]!;
|
|
emitter.moveTemp(callback);
|
|
const adapter = emitter.cryptoBytesThunkFor(cbT);
|
|
if (fn === "crypto.randomBytesCb") {
|
|
emitter.line(`scr_crypto_random_bytes_async(${arg(0)}, ${callback.name}, &${adapter});${emitter.srcComment(e.loc)}`);
|
|
} else {
|
|
emitter.line(`scr_crypto_pbkdf2_async(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${callback.name}, &${adapter});${emitter.srcComment(e.loc)}`);
|
|
}
|
|
if (MAY_THROW_LIB_FNS.has(fn)) emitter.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
// The Buffer statics (scr_bytes.c): fromStr decodes Node-
|
|
// leniently (never throws), concat copies its borrowed list.
|
|
case "buffer.fromStr":
|
|
return finish(`scr_bytes_from_str(${arg(0)}, ${arg(1)})`);
|
|
case "buffer.brand":
|
|
return finish(`scr_bytes_as_buffer(${arg(0)})`);
|
|
case "buffer.fromDyn":
|
|
return finish(`scr_buffer_from_dyn(${arg(0)}, ${arg(1)})`);
|
|
case "buffer.concat":
|
|
return finish(`scr_bytes_concat(${arg(0)})`);
|
|
case "buffer.concatLen":
|
|
return finish(`scr_bytes_concat_len(${arg(0)}, ${arg(1)})`);
|
|
case "buffer.byteLenStr":
|
|
return finish(`scr_bytes_byte_length_str(${arg(0)}, ${arg(1)})`);
|
|
case "buffer.isEncoding":
|
|
return finish(`scr_bytes_is_encoding(${arg(0)})`);
|
|
// The checked-dynamic compare/equals validators
|
|
// (scr_bytes_io.c): Node's argument ladders throw catchably
|
|
// (may-throw seed set); all dyn args borrowed.
|
|
case "buffer.compareChk":
|
|
return finish(`scr_buffer_compare_chk(${arg(0)}, ${arg(1)})`);
|
|
case "bytes.equalsChk":
|
|
return finish(`scr_bytes_equals_chk(${arg(0)}, ${arg(1)})`);
|
|
case "bytes.compareChk":
|
|
return finish(`scr_bytes_compare_chk(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`);
|
|
case "buffer.newStringFail":
|
|
return finish(`scr_buffer_new_string_fail(${arg(0)})`);
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: cryptoBytes libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitChildProcessLibCall(state: LibCallState): Temp {
|
|
const { e, emitter, args, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
// child_process.spawnSync (scr_child.c): blocks until the child
|
|
// is reaped — NEVER throws (spawn failure is data: status null,
|
|
// empty outputs). cmd and args are borrowed; the result is +1.
|
|
case "cp.spawnSync":
|
|
return finish(`scr_spawn_sync(${arg(0)}, ${arg(1)})`);
|
|
case "cp.spawnSyncOpts":
|
|
// The options form: timeout/killSignal/stdio modes. Same
|
|
// never-throws contract — timeout and spawn failure are data
|
|
// on the result (error/signal), exactly Node's spawnSync.
|
|
return finish(
|
|
`scr_spawn_sync_opts(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)})`,
|
|
);
|
|
case "cp.spawnSyncStdioStr":
|
|
// The runtime maps the type-proven "pipe"/"ignore"/"inherit"
|
|
// string to the three modes; same never-throws contract.
|
|
return finish(
|
|
`scr_spawn_sync_stdio_str(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`,
|
|
);
|
|
case "cp.execSync":
|
|
// Throws Node's exec errors; the +1 stdout result must join
|
|
// the frame BEFORE the pending check (fallibleTemp) so an
|
|
// unwind releases the NULL dummy harmlessly.
|
|
return emitter.fallibleTemp(
|
|
e.type,
|
|
`scr_exec_sync(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, ${arg(8)}, ${arg(9)}, ${arg(10)})`,
|
|
);
|
|
case "cp.execCapture":
|
|
// The promisified-execFile core: throws Node's ASYNC exec
|
|
// errors (the interned async helper's fiber makes them the
|
|
// rejection); +1 spawnRes result, same fallible discipline.
|
|
return emitter.fallibleTemp(
|
|
e.type,
|
|
`scr_exec_capture(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`,
|
|
);
|
|
case "spawnRes.status": {
|
|
// The `number | null` union, constructed type-directedly like
|
|
// process.envGet: the exit code wraps the f64 arm; signal
|
|
// death and spawn failure yield the interned null-arm
|
|
// instance (the runtime exposes a has_status flag + code).
|
|
if (e.type.kind !== "union") {
|
|
throw new InternalCompilerError("emitter bug: spawnRes.status result is not a union");
|
|
}
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const f64Tag = def ? def.arms.findIndex((a) => a.kind === "f64") : -1;
|
|
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
|
if (f64Tag < 0 || nullTag < 0) {
|
|
throw new InternalCompilerError("emitter bug: spawnRes.status union lacks its arms");
|
|
}
|
|
const present = `scr_union_new_f64(${f64Tag}, scr_spawn_res_status(${arg(0)}))`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, nullTag);
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`scr_spawn_res_has_status(${arg(0)}) ? ${present} : ${absent}`,
|
|
);
|
|
}
|
|
case "spawnRes.stdout":
|
|
return finish(`scr_spawn_res_stdout(${arg(0)})`);
|
|
case "spawnRes.stderr":
|
|
return finish(`scr_spawn_res_stderr(${arg(0)})`);
|
|
case "spawnRes.signal": {
|
|
// The `string | null` union (the termination signal's name,
|
|
// null for a normal exit or spawn failure) — the has/get pair
|
|
// wrapped type-directedly like spawnRes.status.
|
|
if (e.type.kind !== "union") {
|
|
throw new InternalCompilerError("emitter bug: spawnRes.signal result is not a union");
|
|
}
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
|
|
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
|
if (strTag < 0 || nullTag < 0) {
|
|
throw new InternalCompilerError("emitter bug: spawnRes.signal union lacks its arms");
|
|
}
|
|
const sv = emitter.newTemp(
|
|
STRING,
|
|
`scr_spawn_res_has_signal(${arg(0)}) ? scr_spawn_res_signal(${arg(0)}) : NULL`,
|
|
);
|
|
emitter.moveTemp(sv); // moves into the box when present; NULL otherwise
|
|
const present = `scr_union_new_ref(${strTag}, ${sv.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, nullTag);
|
|
return emitter.newTemp(e.type, `${sv.name} ? ${present} : ${absent}`);
|
|
}
|
|
case "spawnRes.error": {
|
|
// The `Error | undefined` union, the envGet convention: a
|
|
// spawn failure hands back a fresh +1 %Error (ownership moves
|
|
// into the union box); otherwise the interned undefined arm.
|
|
if (e.type.kind !== "union") {
|
|
throw new InternalCompilerError("emitter bug: spawnRes.error result is not a union");
|
|
}
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const errTag = def ? def.arms.findIndex((a) => a.kind === "object" && a.className === "%Error") : -1;
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
if (errTag < 0 || undefTag < 0) {
|
|
throw new InternalCompilerError("emitter bug: spawnRes.error union lacks its arms");
|
|
}
|
|
const errT: IrType = { kind: "object", className: "%Error" };
|
|
const ev = emitter.newTemp(errT, `scr_spawn_res_error(${arg(0)})`);
|
|
emitter.moveTemp(ev); // moves into the box when present; NULL otherwise
|
|
const present = `scr_union_new_ref(${errTag}, ${ev.name}, &scr_error_retain_v, &scr_error_release_v, ${emitter.traceArgC(errT)})`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, undefTag);
|
|
return emitter.newTemp(e.type, `${ev.name} ? ${present} : ${absent}`);
|
|
}
|
|
case "child.pid":
|
|
case "child.exitCode": {
|
|
// pid: `number | undefined` (undefined = spawn failure);
|
|
// exitCode: `number | null` (null while running / signal
|
|
// death) — the spawnRes.status construction over the has/get
|
|
// runtime pairs.
|
|
if (e.type.kind !== "union") {
|
|
throw new InternalCompilerError(`emitter bug: ${e.fn} result is not a union`);
|
|
}
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const wantUnit = e.fn === "child.pid" ? "undefinedT" : "nullT";
|
|
const f64Tag = def ? def.arms.findIndex((a) => a.kind === "f64") : -1;
|
|
const unitTag = def ? def.arms.findIndex((a) => a.kind === wantUnit) : -1;
|
|
if (f64Tag < 0 || unitTag < 0) {
|
|
throw new InternalCompilerError(`emitter bug: ${e.fn} union lacks its arms`);
|
|
}
|
|
const has = e.fn === "child.pid" ? "scr_child_has_pid" : "scr_child_has_exit_code";
|
|
const get = e.fn === "child.pid" ? "scr_child_pid" : "scr_child_exit_code";
|
|
const present = `scr_union_new_f64(${f64Tag}, ${get}(${arg(0)}))`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, unitTag);
|
|
return emitter.newTemp(e.type, `${has}(${arg(0)}) ? ${present} : ${absent}`);
|
|
}
|
|
case "child.stdin":
|
|
case "child.stdout":
|
|
case "child.stderr": {
|
|
// `Writable | null` / `Readable | null` — the child.pid
|
|
// pattern with a REF arm: the runtime answers a +1 stream
|
|
// handle or NULL (not piped).
|
|
if (e.type.kind !== "union") {
|
|
throw new InternalCompilerError(`emitter bug: ${e.fn} result is not a union`);
|
|
}
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const writer = e.fn === "child.stdin";
|
|
const streamTag = def ? def.arms.findIndex((a) => a.kind === (writer ? "childWriter" : "childStream")) : -1;
|
|
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
|
if (streamTag < 0 || nullTag < 0) {
|
|
throw new InternalCompilerError(`emitter bug: ${e.fn} union lacks its arms`);
|
|
}
|
|
const get = writer ? "scr_child_stdin" : e.fn === "child.stdout" ? "scr_child_stdout" : "scr_child_stderr";
|
|
const rawType = writer ? CHILDWRITER_T : CHILDSTREAM_T;
|
|
const raw = emitter.newTemp(rawType, `${get}(${arg(0)})`);
|
|
emitter.moveTemp(raw); // ownership passes into the union arm below
|
|
const stem = writer ? "scr_child_writer" : "scr_child_stream";
|
|
const present = `scr_union_new_ref(${streamTag}, ${raw.name}, &${stem}_retain_v, &${stem}_release_v, NULL)`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, nullTag);
|
|
return emitter.newTemp(e.type, `${raw.name} != NULL ? ${present} : ${absent}`);
|
|
}
|
|
case "writer.writeString":
|
|
return finish(`scr_child_writer_write_string(${arg(0)}, ${arg(1)})`);
|
|
case "writer.writeBytes":
|
|
return finish(`scr_child_writer_write_bytes(${arg(0)}, ${arg(1)})`);
|
|
case "writer.end":
|
|
return finish(`scr_child_writer_end(${arg(0)})`);
|
|
case "writer.destroy":
|
|
return finish(`scr_child_writer_destroy(${arg(0)})`);
|
|
case "writer.writable":
|
|
return finish(`scr_child_writer_writable(${arg(0)})`);
|
|
case "writer.onDrain":
|
|
case "writer.onFinish": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const fnName = fn === "writer.onDrain" ? "scr_child_writer_on_drain" : "scr_child_writer_on_finish";
|
|
emitter.line(`${fnName}(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "writer.onError": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: writer.onError callback not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error";
|
|
emitter.line(`scr_child_writer_on_error(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "procStream.write":
|
|
// The receiver IS the fd scalar; dispatches onto the exact
|
|
// promptly-submitted stdout/stderr paths (ordering identical).
|
|
return finish(`scr_proc_stream_write(${arg(0)}, ${arg(1)})`);
|
|
case "child.killed":
|
|
return finish(`scr_child_killed(${arg(0)})`);
|
|
case "child.kill":
|
|
// Throws the Unknown-signal TypeError on bad names (may-throw).
|
|
return finish(`scr_child_kill(${arg(0)}, ${arg(1)})`);
|
|
case "child.killNum":
|
|
return finish(`scr_child_kill_num(${arg(0)}, ${arg(1)})`);
|
|
case "child.unref":
|
|
return finish(`scr_child_unref(${arg(0)})`);
|
|
// child_process.spawn (scr_child.c + the event loop): the child
|
|
// starts NOW (posix_spawnp); the loop reaps it and fires its
|
|
// listeners. Never throws — spawn failure defers to "error".
|
|
case "cp.spawn":
|
|
emitter.usesTimers = true; // the loop must run to reap the child
|
|
return finish(`scr_spawn(${arg(0)}, ${arg(1)})`);
|
|
case "cp.fork":
|
|
emitter.usesTimers = true;
|
|
return finish(
|
|
`scr_fork(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)})`,
|
|
);
|
|
case "cp.spawnOpts":
|
|
// The options form: per-slot stdio (ignore/inherit/fd — the
|
|
// fds dup2 into the child), detached (setsid), env
|
|
// replacement, cwd. Same loop/reap story as cp.spawn.
|
|
emitter.usesTimers = true;
|
|
return finish(
|
|
`scr_spawn_opts(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, ${arg(8)}, ${arg(9)}, ${arg(10)}, ${arg(11)})`,
|
|
);
|
|
case "cp.execFile": {
|
|
const callbackArg = e.args[2];
|
|
const cb = args[2];
|
|
if (!callbackArg || !cb) throw new InternalCompilerError("emitter bug: execFile callback missing");
|
|
const cbT = callbackArg.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: execFile callback not a func");
|
|
emitter.moveTemp(cb);
|
|
const adapter = emitter.execFileThunkFor(cbT);
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_exec_file(${arg(0)}, ${arg(1)}, ${cb.name}, &${adapter})`);
|
|
}
|
|
case "child.onExit":
|
|
case "child.onClose": {
|
|
// The callback MOVES into the child's registry; the third
|
|
// ingredient is the ADAPTER — emitted per callback shape,
|
|
// because the `number | null` union's tags are program data
|
|
// (a zero-param listener gets the runtime's ignoring thunk).
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} callback not a func`);
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 0
|
|
? "scr_child_exit_thunk0"
|
|
: cbT.params.length === 1
|
|
? emitter.childExitThunkFor(cbT.params[0]!)
|
|
: emitter.childExitSignalThunkFor(cbT.params[0]!, cbT.params[1]!);
|
|
emitter.line(
|
|
`${e.fn === "child.onClose" ? "scr_child_on_close" : "scr_child_on_exit"}(${arg(0)}, ${cb.name}, &${adapter});${emitter.srcComment(e.loc)}`,
|
|
);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "child.onError": {
|
|
// Both error-listener shapes have runtime-provided adapters
|
|
// (constructing the %Error instance needs no program types).
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: child.onError callback not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error";
|
|
emitter.line(
|
|
`scr_child_on_error(${arg(0)}, ${cb.name}, &${adapter});${emitter.srcComment(e.loc)}`,
|
|
);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "child.connected":
|
|
return finish(`scr_child_ipc_connected(${arg(0)})`);
|
|
case "child.send":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_child_ipc_send(${arg(0)}, ${arg(1)})`);
|
|
case "child.sendCb": {
|
|
const cbT = e.args[2]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: child.send callback not a func");
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_child_ipc_send_cb(${arg(0)}, ${arg(1)}, ${cb.name}, &${emitter.ipcSendThunkFor(cbT)})`);
|
|
}
|
|
case "child.disconnect":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_child_ipc_disconnect(${arg(0)})`);
|
|
case "child.onMessage": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: child message listener not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.usesTimers = true;
|
|
emitter.line(`scr_child_ipc_on_message(${arg(0)}, ${cb.name}, &${emitter.ipcMessageThunkFor(cbT)}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "child.onDisconnect": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: child disconnect listener not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.usesTimers = true;
|
|
emitter.line(`scr_child_ipc_on_disconnect(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: childProcess libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitNetworkLibCall(state: LibCallState): Temp {
|
|
const { e, emitter, args, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
// node:net (scr_net.c + the loop's net hook — linked only when
|
|
// these appear on the IR). Receivers and data are borrowed;
|
|
// CALLBACKS MOVE into the handle's registry (released at
|
|
// settlement). listen/connect make the loop live: usesTimers.
|
|
case "net.createServer":
|
|
return finish(`scr_net_create_server(NULL, NULL)`);
|
|
case "net.createServerCb": {
|
|
const cbT = e.args[0]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: net.createServerCb handler not a func");
|
|
const cb = args[0]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = cbT.params.length === 0 ? "scr_net_conn_thunk0" : "scr_net_conn_thunk_sock";
|
|
return finish(`scr_net_create_server(${cb.name}, &${adapter})`);
|
|
}
|
|
case "net.listen":
|
|
emitter.usesTimers = true; // a listening server holds the loop open
|
|
emitter.line(`scr_net_listen(${arg(0)}, ${arg(1)}, NULL);${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.listenCb": {
|
|
emitter.usesTimers = true;
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_net_listen(${arg(0)}, ${arg(1)}, ${cb.name});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "net.listenOpts":
|
|
emitter.usesTimers = true;
|
|
emitter.line(`scr_net_listen_opts(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, NULL);${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.listenOptsReusePort":
|
|
emitter.usesTimers = true;
|
|
emitter.line(`scr_net_listen_opts_reuse_port(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, NULL);${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.listenOptsCb": {
|
|
emitter.usesTimers = true;
|
|
// The callback slot may be the `(() => void) | undefined`
|
|
// optional-binding union: unwrap to a nullable closure (the
|
|
// createSecureServerSni pattern).
|
|
const cbT = e.args[4]!.type;
|
|
let cbExpr: string;
|
|
if (cbT.kind === "func") {
|
|
const cb = args[4]!;
|
|
emitter.moveTemp(cb);
|
|
cbExpr = cb.name;
|
|
} else {
|
|
if (cbT.kind !== "union") throw new InternalCompilerError("emitter bug: net.listenOptsCb callback shape");
|
|
const def = emitter.unionsById.get(cbT.unionId);
|
|
const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1;
|
|
if (funcTag < 0) throw new InternalCompilerError("emitter bug: net.listenOptsCb union lacks its func arm");
|
|
const u = args[4]!;
|
|
const t = emitter.newTemp(
|
|
def!.arms[funcTag]!,
|
|
`${u.name}->tag == ${funcTag} ? scr_closure_retain((ScrClosure *)scr_union_peek(${u.name})) : NULL`,
|
|
);
|
|
emitter.moveTemp(t);
|
|
cbExpr = t.name;
|
|
}
|
|
emitter.line(`scr_net_listen_opts(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${cbExpr});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "net.listenOptsReusePortCb": {
|
|
emitter.usesTimers = true;
|
|
// The callback slot follows reusePort in the additive ABI and
|
|
// may be the `(() => void) | undefined` optional-binding union.
|
|
const cbT = e.args[5]!.type;
|
|
let cbExpr: string;
|
|
if (cbT.kind === "func") {
|
|
const cb = args[5]!;
|
|
emitter.moveTemp(cb);
|
|
cbExpr = cb.name;
|
|
} else {
|
|
if (cbT.kind !== "union") throw new InternalCompilerError("emitter bug: net.listenOptsReusePortCb callback shape");
|
|
const def = emitter.unionsById.get(cbT.unionId);
|
|
const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1;
|
|
if (funcTag < 0) throw new InternalCompilerError("emitter bug: net.listenOptsReusePortCb union lacks its func arm");
|
|
const u = args[5]!;
|
|
const t = emitter.newTemp(
|
|
def!.arms[funcTag]!,
|
|
`${u.name}->tag == ${funcTag} ? scr_closure_retain((ScrClosure *)scr_union_peek(${u.name})) : NULL`,
|
|
);
|
|
emitter.moveTemp(t);
|
|
cbExpr = t.name;
|
|
}
|
|
emitter.line(`scr_net_listen_opts_reuse_port(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${cbExpr});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "net.serverPort":
|
|
return finish(`scr_net_server_port(${arg(0)})`);
|
|
case "net.serverListening":
|
|
return finish(`scr_net_server_listening(${arg(0)})`);
|
|
case "net.serverAddress": {
|
|
// The AddressInfo record from the three runtime reads (the
|
|
// dgram.address materialization; none of these throw).
|
|
if (e.type.kind !== "record") throw new InternalCompilerError("emitter bug: net.serverAddress result is not a record");
|
|
const ip = emitter.newTemp(STRING, `scr_net_server_addr_ip(${arg(0)})`);
|
|
const rec = emitter.newTemp(e.type, `${mangleRecordNew(e.type.shapeId)}()`);
|
|
emitter.moveTemp(ip);
|
|
emitter.line(`${rec.name}->${mangleField("address")} = ${ip.name};`);
|
|
emitter.line(`${rec.name}->${mangleField("family")} = scr_net_server_addr_family(${arg(0)});`);
|
|
emitter.line(`${rec.name}->${mangleField("port")} = scr_net_server_port(${arg(0)});`);
|
|
return rec;
|
|
}
|
|
case "net.serverClose":
|
|
emitter.line(`scr_net_server_close(${arg(0)}, NULL);${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.serverCloseCb": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_net_server_close(${arg(0)}, ${cb.name});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "net.serverCloseBind": {
|
|
// The bound REAL close as a value: an emitted adapter behind
|
|
// a fresh closure whose one env slot holds the +1 server.
|
|
if (e.type.kind !== "func") throw new InternalCompilerError("emitter bug: net.serverCloseBind result not a func");
|
|
const fnSym = emitter.closeBindThunkFor(e.type.params[0]!, e.type.ret.kind === "netServer");
|
|
const bound = emitter.newTemp(e.type, `scr_closure_new((void *)&${fnSym}, 1)`);
|
|
emitter.line(`${bound.name}->caps[0] = scr_box_new_obj(&scr_net_server_retain_v, &scr_net_server_release_v, NULL);`);
|
|
emitter.line(`scr_box_set_ref(${bound.name}->caps[0], scr_net_server_retain(${arg(0)}));`);
|
|
return bound;
|
|
}
|
|
case "net.serverSetCloseOverride": {
|
|
// The override MOVES into the server's slot behind the
|
|
// emitted zero-arg wrapper (the runtime can't build the
|
|
// callback union — tags are program data).
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: close override not a func");
|
|
const wrapSym = emitter.closeOverrideWrapFor(cbT.params[0]!, cbT.ret.kind === "netServer");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb); // ownership moves into the wrapper's env box
|
|
const wrap = emitter.newTemp(e.args[1]!.type, `scr_closure_new((void *)&${wrapSym}, 1)`);
|
|
emitter.line(`${wrap.name}->caps[0] = scr_box_new(SCR_BOX_FUNC);`);
|
|
emitter.line(`scr_box_set_ref(${wrap.name}->caps[0], ${cb.name});`);
|
|
emitter.moveTemp(wrap); // and the wrapper moves into the server's slot
|
|
emitter.line(`scr_net_server_set_close_override(${arg(0)}, ${wrap.name});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "net.serverOnError":
|
|
case "net.sockOnError": {
|
|
// The child %Error adapters fit exactly (message → %Error).
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} callback not a func`);
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error";
|
|
const fn = e.fn === "net.serverOnError" ? "scr_net_server_on_error" : "scr_net_sock_on_error";
|
|
emitter.line(`${fn}(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "net.serverOnClose": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_net_server_on_close(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "net.serverOnConnection":
|
|
case "net.serverOnSecureConnection": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} callback not a func`);
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = cbT.params.length === 0 ? "scr_net_conn_thunk0" : "scr_net_conn_thunk_sock";
|
|
const entry = e.fn === "net.serverOnConnection" ? "scr_net_server_on_connection" : "scr_net_server_on_secure_connection";
|
|
emitter.line(`${entry}(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "net.connect":
|
|
emitter.usesTimers = true; // a connecting/open socket holds the loop open
|
|
return finish(`scr_net_connect(${arg(0)}, ${arg(1)}, NULL)`);
|
|
case "net.connectAttempt":
|
|
// The validated autoSelectFamilyAttemptTimeout form: Node's
|
|
// range ladder runs first, the dial follows.
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_net_connect_attempt(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "net.connectOptsChk":
|
|
// The runtime option-bag ladder (always throws — a validation
|
|
// error or the trailing fence; the error.nodeThrow dummy).
|
|
return finish(
|
|
`(scr_net_connect_opts_chk(${arg(0)}, ${arg(1)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
|
|
);
|
|
case "net.connectCb": {
|
|
emitter.usesTimers = true;
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
return finish(`scr_net_connect(${arg(0)}, ${arg(1)}, ${cb.name})`);
|
|
}
|
|
case "net.connectLookup": {
|
|
// The caller-resolver dial: the runtime invokes the lookup
|
|
// closure with (hostname, dyn undefined, answer-closure); the
|
|
// answer closure's fn is the emitted per-shape thunk (its
|
|
// union tag and record field are program data — the SNI
|
|
// pattern). The lookup moves in.
|
|
emitter.usesTimers = true;
|
|
const lookupT = e.args[2]!.type;
|
|
if (lookupT.kind !== "func" || lookupT.params[2]?.kind !== "func") {
|
|
throw new InternalCompilerError("emitter bug: net.connectLookup resolver shape (frontend must fence)");
|
|
}
|
|
const thunk = emitter.netLookupAnswerThunkFor(lookupT.params[2]);
|
|
const lookup = args[2]!;
|
|
emitter.moveTemp(lookup);
|
|
return finish(`scr_net_connect_lookup(${arg(0)}, ${arg(1)}, ${lookup.name}, (void *)&${thunk})`);
|
|
}
|
|
case "net.sockWrite":
|
|
emitter.line(`scr_net_sock_write_str(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.sockWriteBytes":
|
|
emitter.line(`scr_net_sock_write_bytes(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.sockEnd":
|
|
emitter.line(`scr_net_sock_end(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.sockEndStr":
|
|
emitter.line(`scr_net_sock_end_str(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.sockEndBytes":
|
|
emitter.line(`scr_net_sock_end_bytes(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.sockWriteDyn":
|
|
emitter.line(`scr_net_sock_write_dynv(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.sockEndDyn":
|
|
emitter.line(`scr_net_sock_end_dynv(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.sockDestroy":
|
|
emitter.line(`scr_net_sock_destroy(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.sockPause":
|
|
return finish(`scr_net_sock_pause(${arg(0)})`);
|
|
case "net.sockResume":
|
|
return finish(`scr_net_sock_resume(${arg(0)})`);
|
|
case "net.sockSetNoDelay":
|
|
return finish(`scr_net_sock_set_nodelay(${arg(0)}, ${arg(1)})`);
|
|
case "net.sockDestroySoon":
|
|
emitter.line(`scr_net_sock_destroy_soon(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.sockBytesWritten":
|
|
return finish(`scr_net_sock_bytes_written(${arg(0)})`);
|
|
case "net.sockReadable":
|
|
return finish(`scr_net_sock_readable(${arg(0)})`);
|
|
case "net.sockOnFinish": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_net_sock_on_finish(${arg(0)}, ${cb.name});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "net.sockPipe":
|
|
emitter.line(`scr_net_sock_pipe(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.sockOnData": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: net.sockOnData callback not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 0 ? "scr_net_data_thunk0"
|
|
: cbT.params[0]!.kind === "dyn" ? "scr_net_data_thunk_dyn"
|
|
: cbT.params[0]!.kind === "string" ? "scr_net_data_thunk_str"
|
|
: "scr_net_data_thunk_bytes";
|
|
emitter.line(`scr_net_sock_on_data(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "net.sockOnEnd":
|
|
case "net.sockOnClose":
|
|
case "net.sockOnConnect": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const fn = e.fn === "net.sockOnEnd" ? "scr_net_sock_on_end"
|
|
: e.fn === "net.sockOnClose" ? "scr_net_sock_on_close"
|
|
: "scr_net_sock_on_connect";
|
|
emitter.line(`${fn}(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
// node:dgram + node:dns (scr_dgram.c + the loop's dgram hook —
|
|
// linked only when these appear on the IR). The net discipline:
|
|
// receivers/data borrowed, CALLBACKS MOVE into the handle's
|
|
// registry; bind/connect/send make the loop live (usesTimers).
|
|
case "dgram.createSocket":
|
|
return finish(`scr_dgram_create(${arg(0)})`);
|
|
case "dgram.bind":
|
|
emitter.usesTimers = true; // a bound socket holds the loop open
|
|
return finish(`scr_dgram_bind(${arg(0)}, ${arg(1)}, ${arg(2)}, NULL)`);
|
|
case "dgram.bindCb": {
|
|
emitter.usesTimers = true;
|
|
const cb = args[3]!;
|
|
emitter.moveTemp(cb);
|
|
return finish(`scr_dgram_bind(${arg(0)}, ${arg(1)}, ${arg(2)}, ${cb.name})`);
|
|
}
|
|
case "dgram.connect":
|
|
emitter.usesTimers = true; // a connected socket holds the loop open
|
|
return finish(`scr_dgram_connect(${arg(0)}, ${arg(1)}, ${arg(2)}, NULL)`);
|
|
case "dgram.connectCb": {
|
|
emitter.usesTimers = true;
|
|
const cb = args[3]!;
|
|
emitter.moveTemp(cb);
|
|
return finish(`scr_dgram_connect(${arg(0)}, ${arg(1)}, ${arg(2)}, ${cb.name})`);
|
|
}
|
|
case "dgram.sendStr":
|
|
emitter.usesTimers = true; // send implicit-binds; the socket stays open
|
|
return finish(`scr_dgram_send_str(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`);
|
|
case "dgram.sendBytes":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_dgram_send_bytes(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`);
|
|
case "dgram.sendChk":
|
|
emitter.usesTimers = true; // a validated send implicit-binds
|
|
return finish(
|
|
`scr_dgram_send_chk(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)})`,
|
|
);
|
|
case "dgram.address": {
|
|
// The AddressInfo record, built here from runtime parts (the
|
|
// frontend pinned the {address, family, port} shape). The
|
|
// address read is the fallible one — Node's "Not running"
|
|
// throw for a never-bound socket; family/port never throw
|
|
// once it passed.
|
|
if (e.type.kind !== "record") throw new InternalCompilerError("emitter bug: dgram.address result is not a record");
|
|
const ip = emitter.fallibleTemp(STRING, `scr_dgram_addr_ip(${arg(0)})`);
|
|
const rec = emitter.newTemp(e.type, `${mangleRecordNew(e.type.shapeId)}()`);
|
|
emitter.moveTemp(ip);
|
|
emitter.line(`${rec.name}->${mangleField("address")} = ${ip.name};`);
|
|
emitter.line(`${rec.name}->${mangleField("family")} = scr_dgram_addr_family(${arg(0)});`);
|
|
emitter.line(`${rec.name}->${mangleField("port")} = scr_dgram_addr_port(${arg(0)});`);
|
|
return rec;
|
|
}
|
|
case "dgram.close":
|
|
return finish(`scr_dgram_close(${arg(0)}, NULL)`);
|
|
case "dgram.closeCb": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
return finish(`scr_dgram_close(${arg(0)}, ${cb.name})`);
|
|
}
|
|
case "dgram.unref":
|
|
emitter.line(`scr_dgram_unref(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "dgram.ref":
|
|
emitter.line(`scr_dgram_ref(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "dgram.onMessage": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: dgram.onMessage callback not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 0 ? "scr_dgram_msg_thunk0"
|
|
: cbT.params.length === 1 ? "scr_dgram_msg_thunk1"
|
|
: emitter.dgramMsgThunkFor(cbT.params[1]!);
|
|
emitter.line(`scr_dgram_on_message(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "dgram.onError": {
|
|
// The child %Error adapters fit exactly (message → %Error).
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: dgram.onError callback not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error";
|
|
emitter.line(`scr_dgram_on_error(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "dgram.onListening":
|
|
case "dgram.onClose":
|
|
case "dgram.onConnect": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const fn = e.fn === "dgram.onListening" ? "scr_dgram_on_listening"
|
|
: e.fn === "dgram.onClose" ? "scr_dgram_on_close"
|
|
: "scr_dgram_on_connect";
|
|
emitter.line(`${fn}(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "dns.lookup": {
|
|
// getaddrinfo runs NOW; the callback (moved in) fires at the
|
|
// next loop turn through its per-shape adapter.
|
|
emitter.usesTimers = true; // the pending callback holds the loop open
|
|
const cbT = e.args[2]!.type;
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = emitter.dnsLookupThunkFor(cbT);
|
|
emitter.line(`scr_dns_lookup(${arg(0)}, ${arg(1)}, ${cb.name}, &${adapter});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "net.sockEncrypted": {
|
|
// boolean | undefined: the true arm iff the socket carries a
|
|
// TLS transport; plain sockets answer the undefined arm (Node
|
|
// types `encrypted` on TLSSocket only).
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: net.sockEncrypted result is not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const boolTag = def ? def.arms.findIndex((a) => a.kind === "bool") : -1;
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
if (boolTag < 0 || undefTag < 0) {
|
|
throw new InternalCompilerError("emitter bug: net.sockEncrypted union lacks its arms");
|
|
}
|
|
const w = emitter.newTemp(BOOL, `scr_net_sock_encrypted(${arg(0)})`);
|
|
const present = `scr_union_new_bool(${boolTag}, true)`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, undefTag);
|
|
return emitter.newTemp(e.type, `${w.name} ? ${present} : ${absent}`);
|
|
}
|
|
case "net.sockDestroyed":
|
|
return finish(`scr_net_sock_destroyed(${arg(0)})`);
|
|
case "net.sockWritable":
|
|
return finish(`scr_net_sock_writable(${arg(0)})`);
|
|
case "net.sockPipeRes":
|
|
emitter.line(`scr_http_sock_pipe_res(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.serverOnListening": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_net_server_on_listening(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "net.sockSetEncoding":
|
|
return finish(`scr_net_sock_set_encoding(${arg(0)}, ${arg(1)})`);
|
|
case "net.sockSetTimeout":
|
|
emitter.line(`scr_net_sock_set_timeout(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.sockOnTimeout": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_net_sock_on_timeout(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "net.sockOnReadable": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_net_sock_on_readable(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "net.sockRead": {
|
|
// Buffer | null, type-directed like http.reqHeader: NULL (not
|
|
// enough buffered) takes the null arm.
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: net.sockRead result is not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes" && a.elem === "u8") : -1;
|
|
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
|
if (bytesTag < 0 || nullTag < 0) {
|
|
throw new InternalCompilerError("emitter bug: net.sockRead union lacks its arms");
|
|
}
|
|
const b = emitter.newTemp(BYTES_U8, `scr_net_sock_read_bytes(${arg(0)}, ${arg(1)})`);
|
|
emitter.moveTemp(b); // moves into the box when present; NULL otherwise
|
|
const present = `scr_union_new_ref(${bytesTag}, ${b.name}, &scr_bytes_retain_v, &scr_bytes_release_v, NULL)`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, nullTag);
|
|
return emitter.newTemp(e.type, `${b.name} ? ${present} : ${absent}`);
|
|
}
|
|
case "net.sockUnshift":
|
|
emitter.line(`scr_net_sock_unshift_bytes(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.serverEmitConnection":
|
|
emitter.line(`scr_net_server_emit_connection(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "net.sockRemoteAddress": {
|
|
// string | undefined, type-directed like http.reqHeader: NULL
|
|
// (closed socket) takes the undefined arm.
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: net.sockRemoteAddress result is not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
if (strTag < 0 || undefTag < 0) {
|
|
throw new InternalCompilerError("emitter bug: net.sockRemoteAddress union lacks its arms");
|
|
}
|
|
const s = emitter.newTemp(STRING, `scr_net_sock_remote_address(${arg(0)})`);
|
|
emitter.moveTemp(s);
|
|
const present = `scr_union_new_ref(${strTag}, ${s.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, undefTag);
|
|
return emitter.newTemp(e.type, `${s.name} ? ${present} : ${absent}`);
|
|
}
|
|
case "net.getAutoSelTimeout":
|
|
return finish(`scr_net_get_autosel_timeout()`);
|
|
case "net.setAutoSelTimeout":
|
|
return finish(`scr_net_set_autosel_timeout(${arg(0)})`);
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: network libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitTestLibCall(state: LibCallState): Temp {
|
|
const { e, emitter, args, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
// node:test (scr_test.c — linked only when these appear on the
|
|
// IR; moduleUsesNodeTest is the switch). Strings borrowed,
|
|
// callbacks MOVE. Registrations keep the loop-run emitted
|
|
// (usesTimers): the runner fiber drains after main returns.
|
|
case "test.register": {
|
|
emitter.usesTimers = true;
|
|
const cb = args[3]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_test_register(${arg(0)}, ${arg(1)}, ${arg(2)}, ${cb.name}, ${arg(4)}, ${arg(5)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "test.registerEmpty":
|
|
emitter.usesTimers = true;
|
|
emitter.line(`scr_test_register(${arg(0)}, ${arg(1)}, ${arg(2)}, NULL, ${arg(3)}, ${arg(4)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "test.suite": {
|
|
emitter.usesTimers = true;
|
|
const cb = args[3]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_test_suite(${arg(0)}, ${arg(1)}, ${arg(2)}, ${cb.name}, ${arg(4)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "test.hook": {
|
|
emitter.usesTimers = true;
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_test_hook(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "test.sub": {
|
|
// Runs the subtest INLINE on the runner fiber; the settled
|
|
// promise (+1) is the await's operand.
|
|
const cb = args[4]!;
|
|
emitter.moveTemp(cb);
|
|
return finish(`scr_test_sub(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${cb.name}, ${arg(5)}, ${arg(6)})`);
|
|
}
|
|
case "test.subEmpty":
|
|
// Fn-less subtest: the settled promise is discarded here (the
|
|
// lowering types it void — nothing consumes it).
|
|
emitter.line(`scr_promise_release(scr_test_sub(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, NULL, 0, ${arg(4)}));${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "test.ctxSkip":
|
|
emitter.line(`scr_test_ctx_skip(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "test.ctxTodo":
|
|
emitter.line(`scr_test_ctx_todo(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "test.ctxDiagnostic":
|
|
emitter.line(`scr_test_ctx_diagnostic(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "test.ctxName":
|
|
return finish(`scr_test_ctx_name(${arg(0)})`);
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: test libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitHttpLibCall(state: LibCallState): Temp {
|
|
const { e, emitter, args, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
// node:http, the server slice (scr_http.c over scr_net.c).
|
|
case "http.createServer": {
|
|
emitter.usesTimers = true;
|
|
const cbT = e.args[0]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.createServer handler not a func");
|
|
const cb = args[0]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 2 ? "scr_http_handler_thunk2"
|
|
: cbT.params.length === 1 ? "scr_http_handler_thunk1"
|
|
: "scr_http_handler_thunk0";
|
|
return finish(`scr_http_create_server(${cb.name}, &${adapter})`);
|
|
}
|
|
case "http.reqUrl":
|
|
return finish(`scr_http_req_url(${arg(0)})`);
|
|
case "http.reqMethod":
|
|
return finish(`scr_http_req_method(${arg(0)})`);
|
|
case "http.reqHttpVersion":
|
|
return finish(`scr_http_req_http_version(${arg(0)})`);
|
|
case "http.reqHttpVersionMajor":
|
|
return finish(`scr_http_req_http_version_major(${arg(0)})`);
|
|
case "http.reqHttpVersionMinor":
|
|
return finish(`scr_http_req_http_version_minor(${arg(0)})`);
|
|
case "http.reqAborted":
|
|
return finish(`scr_http_req_aborted_flag(${arg(0)})`);
|
|
case "http.reqComplete":
|
|
return finish(`scr_http_req_complete(${arg(0)})`);
|
|
case "http.reqHeader":
|
|
case "http.reqTrailer": {
|
|
// string|undefined, type-directed exactly like process.envGet:
|
|
// the runtime answers +1 or NULL; NULL takes the undefined arm.
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: http.reqHeader result is not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
if (strTag < 0 || undefTag < 0) {
|
|
throw new InternalCompilerError("emitter bug: http.reqHeader union lacks its arms");
|
|
}
|
|
const entry = e.fn === "http.reqHeader" ? "scr_http_req_header" : "scr_http_req_trailer";
|
|
const s = emitter.newTemp(STRING, `${entry}(${arg(0)}, ${arg(1)})`);
|
|
emitter.moveTemp(s); // moves into the box when present; NULL otherwise
|
|
const present = `scr_union_new_ref(${strTag}, ${s.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, undefTag);
|
|
return emitter.newTemp(e.type, `${s.name} ? ${present} : ${absent}`);
|
|
}
|
|
case "http.reqHeaderValues":
|
|
case "http.reqTrailerValues": {
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: distinct header result is not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const arrTag = def ? def.arms.findIndex((a) => a.kind === "array") : -1;
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
if (arrTag < 0 || undefTag < 0) throw new InternalCompilerError("emitter bug: distinct header union lacks its arms");
|
|
const entry = e.fn === "http.reqHeaderValues" ? "scr_http_req_header_values" : "scr_http_req_trailer_values";
|
|
const values = emitter.newTemp(arrayOf(STRING), `${entry}(${arg(0)}, ${arg(1)})`);
|
|
emitter.moveTemp(values);
|
|
const present = `scr_union_new_ref(${arrTag}, ${values.name}, &scr_arr_retain_v, &scr_arr_release_v, NULL)`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, undefTag);
|
|
return emitter.newTemp(e.type, `${values.name} ? ${present} : ${absent}`);
|
|
}
|
|
case "http.reqOnData": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.reqOnData callback not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 0 ? "scr_net_data_thunk0"
|
|
: cbT.params[0]!.kind === "dyn" ? "scr_net_data_thunk_dyn"
|
|
: cbT.params[0]!.kind === "string" ? "scr_net_data_thunk_str"
|
|
: "scr_net_data_thunk_bytes";
|
|
emitter.line(`scr_http_req_on_data(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.reqOnEnd": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_http_req_on_end(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.resSetHeader":
|
|
emitter.line(`scr_http_res_set_header(${arg(0)}, ${arg(1)}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.validateHeaderName":
|
|
emitter.line(`scr_http_validate_header_name(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.validateHeaderValue":
|
|
emitter.line(`scr_http_validate_header_value(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.resWriteHead":
|
|
return finish(`scr_http_res_write_head(${arg(0)}, ${arg(1)})`);
|
|
case "http.resWriteHeadN":
|
|
return finish(`scr_http_res_write_head_n(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`);
|
|
case "http.resWriteContinue":
|
|
return finish(`scr_http_res_write_continue(${arg(0)})`);
|
|
case "http.resWriteProcessing":
|
|
return finish(`scr_http_res_write_processing(${arg(0)})`);
|
|
case "http.resWriteEarlyHints":
|
|
return finish(`scr_http_res_write_early_hints(${arg(0)}, ${arg(1)})`);
|
|
case "http.resWrite":
|
|
return finish(`scr_http_res_write_str(${arg(0)}, ${arg(1)})`);
|
|
case "http.resWriteBytes":
|
|
return finish(`scr_http_res_write_bytes(${arg(0)}, ${arg(1)})`);
|
|
case "http.resEnd":
|
|
return finish(`scr_http_res_end(${arg(0)})`);
|
|
case "http.resEndStr":
|
|
return finish(`scr_http_res_end_str(${arg(0)}, ${arg(1)})`);
|
|
case "http.resEndBytes":
|
|
return finish(`scr_http_res_end_bytes(${arg(0)}, ${arg(1)})`);
|
|
case "http.resWriteDyn":
|
|
return finish(`scr_http_res_write_dynv(${arg(0)}, ${arg(1)})`);
|
|
case "http.resEndDyn":
|
|
return finish(`scr_http_res_end_dynv(${arg(0)}, ${arg(1)})`);
|
|
case "http.resHeadersSent":
|
|
return finish(`scr_http_res_headers_sent(${arg(0)})`);
|
|
case "http.resWritableEnded":
|
|
return finish(`scr_http_res_writable_ended(${arg(0)})`);
|
|
case "http.resFlushHeaders":
|
|
return finish(`scr_http_res_flush_headers(${arg(0)})`);
|
|
case "http.resAddTrailers":
|
|
return finish(`scr_http_res_add_trailers(${arg(0)}, ${arg(1)})`);
|
|
case "http.resCork":
|
|
emitter.line(`scr_http_res_cork(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.resUncork":
|
|
return finish(`scr_http_res_uncork(${arg(0)})`);
|
|
case "http.resWritableCorked":
|
|
return finish(`scr_http_res_writable_corked(${arg(0)})`);
|
|
// The server-surface member follow-ups.
|
|
case "http.reqStatusCode": {
|
|
// number | undefined, type-directed like process.columns: the
|
|
// runtime answers a negative status for server requests.
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: http.reqStatusCode result is not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const f64Tag = def ? def.arms.findIndex((a) => a.kind === "f64") : -1;
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
if (f64Tag < 0 || undefTag < 0) {
|
|
throw new InternalCompilerError("emitter bug: http.reqStatusCode union lacks its arms");
|
|
}
|
|
const w = emitter.newTemp(F64, `scr_http_req_status(${arg(0)})`);
|
|
const present = `scr_union_new_f64(${f64Tag}, ${w.name})`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, undefTag);
|
|
return emitter.newTemp(e.type, `${w.name} >= 0 ? ${present} : ${absent}`);
|
|
}
|
|
case "http.reqSocket":
|
|
return finish(`scr_http_req_socket(${arg(0)})`);
|
|
case "http.reqH2Stream": {
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: http.reqH2Stream result is not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const streamTag = def ? def.arms.findIndex((a) => a.kind === "http2Stream") : -1;
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
if (streamTag < 0 || undefTag < 0) throw new InternalCompilerError("emitter bug: http.reqH2Stream union lacks its arms");
|
|
const st = emitter.newTemp({ kind: "http2Stream" }, `scr_http_req_h2_stream(${arg(0)})`);
|
|
emitter.moveTemp(st);
|
|
const present = `scr_union_new_ref(${streamTag}, ${st.name}, &scr_http2_stream_retain_v, &scr_http2_stream_release_v, NULL)`;
|
|
return emitter.newTemp(e.type, `${st.name} ? ${present} : ${emitter.unitInstanceRef(e.type.unionId, undefTag)}`);
|
|
}
|
|
case "http.reqH2StreamOrThrow":
|
|
return finish(`scr_http_req_h2_stream_or_throw(${arg(0)}, ${arg(1)})`);
|
|
case "http.reqRawHeaders":
|
|
return finish(`scr_http_req_raw_headers(${arg(0)})`);
|
|
case "http.reqRawTrailers":
|
|
return finish(`scr_http_req_raw_trailers(${arg(0)})`);
|
|
case "http.reqHeaderPairs":
|
|
return finish(`scr_http_req_header_pairs(${arg(0)})`);
|
|
case "http.reqTrailerPairs":
|
|
return finish(`scr_http_req_trailer_pairs(${arg(0)})`);
|
|
case "http.reqStatusMessage": {
|
|
// string | undefined: a reason phrase on client responses,
|
|
// NULL (the undefined arm) on server requests — the
|
|
// sockRemoteAddress shape.
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: http.reqStatusMessage result is not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
if (strTag < 0 || undefTag < 0) {
|
|
throw new InternalCompilerError("emitter bug: http.reqStatusMessage union lacks its arms");
|
|
}
|
|
const m = emitter.newTemp(STRING, `scr_http_req_status_message(${arg(0)})`);
|
|
emitter.moveTemp(m);
|
|
const present = `scr_union_new_ref(${strTag}, ${m.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, undefTag);
|
|
return emitter.newTemp(e.type, `${m.name} ? ${present} : ${absent}`);
|
|
}
|
|
case "http.serverOnUpgrade":
|
|
case "http.clientOnUpgrade": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} listener not a func`);
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 3 ? "scr_http_upgrade_thunk3"
|
|
: cbT.params.length === 2 ? "scr_http_upgrade_thunk2"
|
|
: cbT.params.length === 1 ? "scr_http_upgrade_thunk1"
|
|
: "scr_http_upgrade_thunk0";
|
|
const entry = e.fn === "http.serverOnUpgrade"
|
|
? "scr_http_server_on_upgrade"
|
|
: "scr_http_client_on_upgrade";
|
|
emitter.line(`${entry}(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.serverOnConnect": {
|
|
// The CONNECT handover: the upgrade adapters fit the plain
|
|
// socket-param shapes; a UNION socket slot (the h2 compat
|
|
// listener) takes the emitted per-shape wrapper — the arm's
|
|
// tag is program data.
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.serverOnConnect listener not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const p1 = cbT.params[1];
|
|
const adapter =
|
|
p1 !== undefined && p1.kind === "union"
|
|
? emitter.connectSockThunkFor(cbT)
|
|
: cbT.params.length === 3 ? "scr_http_upgrade_thunk3"
|
|
: cbT.params.length === 2 ? "scr_http_upgrade_thunk2"
|
|
: cbT.params.length === 1 ? "scr_http_upgrade_thunk1"
|
|
: "scr_http_upgrade_thunk0";
|
|
const h2Def = p1?.kind === "union" ? emitter.unionsById.get(p1.unionId) : undefined;
|
|
const h2Adapter = cbT.params.length >= 2
|
|
? h2Def?.arms.some((arm) => arm.kind === "httpRes") ? `&${emitter.connectResThunkFor(cbT)}` : "NULL"
|
|
: cbT.params.length === 1 ? "&scr_http_handler_thunk1" : "&scr_http_handler_thunk0";
|
|
emitter.line(`scr_http_server_on_connect(${arg(0)}, ${cb.name}, &${adapter}, ${h2Adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.reqPipeRes":
|
|
emitter.line(`scr_http_req_pipe_res(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.reqPipeClient":
|
|
emitter.line(`scr_http_req_pipe_client(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.reqPipeSock":
|
|
emitter.line(`scr_http_req_pipe_sock(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.reqResume":
|
|
emitter.line(`scr_http_req_resume(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.reqDestroy":
|
|
emitter.line(`scr_http_req_destroy(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.reqOnError": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.reqOnError callback not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error";
|
|
emitter.line(`scr_http_req_on_error(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.reqOnClose": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_http_req_on_close(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.reqOnAborted": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_http_req_on_aborted(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.resDestroy":
|
|
emitter.line(`scr_http_res_destroy(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.resOnClose": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_http_res_on_close(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.createServerEmpty":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_http_create_server(NULL, NULL)`);
|
|
case "http.serverJoinDupHeaders":
|
|
emitter.line(`scr_http_server_join_duplicate_headers(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.serverAllowMissingHostHeader":
|
|
emitter.line(`scr_http_server_allow_missing_host_header(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.serverSetTimeout":
|
|
emitter.line(`scr_net_server_set_timeout_plain(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.serverCloseAllConnections":
|
|
case "http.serverCloseIdleConnections": {
|
|
const entry = e.fn === "http.serverCloseAllConnections"
|
|
? "scr_net_server_close_all_connections" : "scr_net_server_close_idle_connections";
|
|
emitter.line(`${entry}(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.serverOnTimeout":
|
|
case "http.serverSetTimeoutCb": {
|
|
const cbIndex = e.fn === "http.serverSetTimeoutCb" ? 2 : 1;
|
|
const cbT = e.args[cbIndex]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} callback not a func`);
|
|
const cb = args[cbIndex]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = cbT.params.length === 0 ? "scr_net_conn_thunk0" : "scr_net_conn_thunk_sock";
|
|
const entry = e.fn === "http.serverOnTimeout" ? "scr_net_server_on_timeout" : "scr_net_server_set_timeout_cb";
|
|
const callArgs = e.fn === "http.serverOnTimeout"
|
|
? `${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)}`
|
|
: `${arg(0)}, ${arg(1)}, ${cb.name}, &${adapter}`;
|
|
emitter.line(`${entry}(${callArgs});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.serverTimeoutGet":
|
|
return finish(`scr_net_server_timeout_get(${arg(0)}, ${arg(1)})`);
|
|
case "http.serverTimeoutSet":
|
|
emitter.line(`scr_net_server_timeout_set(${arg(0)}, ${arg(1)}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.serverTimeoutOptionSet":
|
|
return finish(`scr_net_server_timeout_option_set(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "http.resStatusGet":
|
|
return finish(`scr_http_res_status_get(${arg(0)})`);
|
|
case "http.resStatusSet":
|
|
emitter.line(`scr_http_res_status_set(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.resStatusMsgGet": {
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: http.resStatusMsgGet result is not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
if (strTag < 0 || undefTag < 0) throw new InternalCompilerError("emitter bug: http.resStatusMsgGet union lacks its arms");
|
|
const msg = emitter.newTemp(STRING, `scr_http_res_status_msg_get(${arg(0)})`);
|
|
emitter.moveTemp(msg);
|
|
const present = `scr_union_new_ref(${strTag}, ${msg.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`;
|
|
return emitter.newTemp(e.type, `${msg.name} ? ${present} : ${emitter.unitInstanceRef(e.type.unionId, undefTag)}`);
|
|
}
|
|
case "http.resStatusMsgSet":
|
|
emitter.line(`scr_http_res_status_msg_set(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.resRequest":
|
|
return finish(`scr_http_res_request(${arg(0)})`);
|
|
case "http.resSocket": {
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: http.resSocket result is not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const socketTag = def ? def.arms.findIndex((a) => a.kind === "netSocket") : -1;
|
|
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
|
if (socketTag < 0 || nullTag < 0) throw new InternalCompilerError("emitter bug: http.resSocket union lacks its arms");
|
|
const sock = emitter.newTemp(NETSOCKET_T, `scr_http_res_socket(${arg(0)})`);
|
|
emitter.moveTemp(sock);
|
|
const present = `scr_union_new_ref(${socketTag}, ${sock.name}, &scr_net_sock_retain_v, &scr_net_sock_release_v, NULL)`;
|
|
return emitter.newTemp(e.type, `${sock.name} ? ${present} : ${emitter.unitInstanceRef(e.type.unionId, nullTag)}`);
|
|
}
|
|
case "http.resWritableFinished":
|
|
return finish(`scr_http_res_writable_finished(${arg(0)})`);
|
|
case "http.resSendDateGet":
|
|
return finish(`scr_http_res_send_date(${arg(0)})`);
|
|
case "http.resSendDateSet":
|
|
emitter.line(`scr_http_res_set_send_date(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.resStrictContentLengthGet":
|
|
return finish(`scr_http_res_strict_content_length(${arg(0)})`);
|
|
case "http.resStrictContentLengthSet":
|
|
emitter.line(`scr_http_res_set_strict_content_length(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.resSetTimeout":
|
|
emitter.line(`scr_http_res_set_timeout_plain(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.resSetTimeoutCb": {
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_http_res_set_timeout(${arg(0)}, ${arg(1)}, ${cb.name});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.resGetHeader":
|
|
case "http.clientGetHeader": {
|
|
// string|undefined, exactly the http.reqHeader emission.
|
|
if (e.type.kind !== "union") throw new InternalCompilerError(`emitter bug: ${e.fn} result is not a union`);
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
if (strTag < 0 || undefTag < 0) {
|
|
throw new InternalCompilerError("emitter bug: http.resGetHeader union lacks its arms");
|
|
}
|
|
const entry = e.fn === "http.resGetHeader" ? "scr_http_res_get_header" : "scr_http_client_get_header";
|
|
const s = emitter.newTemp(STRING, `${entry}(${arg(0)}, ${arg(1)})`);
|
|
emitter.moveTemp(s); // moves into the box when present; NULL otherwise
|
|
const present = `scr_union_new_ref(${strTag}, ${s.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, undefTag);
|
|
return emitter.newTemp(e.type, `${s.name} ? ${present} : ${absent}`);
|
|
}
|
|
case "http.resGetHeaderNames":
|
|
return finish(`scr_http_res_get_header_names(${arg(0)})`);
|
|
case "http.resGetRawHeaderNames":
|
|
return finish(`scr_http_res_get_raw_header_names(${arg(0)})`);
|
|
case "http.resGetHeaders":
|
|
return finish(`scr_http_res_get_headers(${arg(0)})`);
|
|
case "http.resHasHeader":
|
|
return finish(`scr_http_res_has_header_named(${arg(0)}, ${arg(1)})`);
|
|
case "http.resRemoveHeader":
|
|
emitter.line(`scr_http_res_remove_header(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.resOnFinish": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_http_res_on_finish(${arg(0)}, ${cb.name});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.resWriteHeadPairs":
|
|
return finish(`scr_http_res_write_head_pairs(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "http.resWriteHeadDyn":
|
|
return finish(`scr_http_res_write_head_dyn(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "http.reqSetEncoding":
|
|
return finish(`scr_http_req_set_encoding(${arg(0)}, ${arg(1)})`);
|
|
case "http.reqSetTimeout":
|
|
return finish(`scr_http_req_set_timeout_plain(${arg(0)}, ${arg(1)})`);
|
|
case "http.reqSetTimeoutCb": {
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_http_req_set_timeout(${arg(0)}, ${arg(1)}, ${cb.name});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
// node:http, the client slice (scr_http.c over the net client).
|
|
case "http.request":
|
|
case "http.requestCb": {
|
|
emitter.usesTimers = true; // an in-flight request holds the loop open
|
|
let cbExpr = "NULL";
|
|
let adapter = "NULL";
|
|
if (e.fn === "http.requestCb") {
|
|
const cbT = e.args[7]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.requestCb callback not a func");
|
|
const cb = args[7]!;
|
|
emitter.moveTemp(cb);
|
|
cbExpr = cb.name;
|
|
adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res";
|
|
}
|
|
return finish(
|
|
`scr_http_request(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${cbExpr}, ${adapter})`,
|
|
);
|
|
}
|
|
case "http.agentNew":
|
|
emitter.usesTimers = true; // queued dials hold the loop open
|
|
return finish(
|
|
`scr_http_agent_new(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)})`,
|
|
);
|
|
case "http.requestAgent":
|
|
case "http.requestAgentCb": {
|
|
emitter.usesTimers = true;
|
|
let cbExpr = "NULL";
|
|
let adapter = "NULL";
|
|
if (e.fn === "http.requestAgentCb") {
|
|
const cbT = e.args[8]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.requestAgentCb callback not a func");
|
|
const cb = args[8]!;
|
|
emitter.moveTemp(cb);
|
|
cbExpr = cb.name;
|
|
adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res";
|
|
}
|
|
return finish(
|
|
`scr_http_request_agent(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, ${cbExpr}, ${adapter})`,
|
|
);
|
|
}
|
|
case "http.requestUrl":
|
|
case "http.requestUrlCb":
|
|
case "https.requestUrl":
|
|
case "https.requestUrlCb": {
|
|
emitter.usesTimers = true; // an in-flight request holds the loop open
|
|
const tls = e.fn.startsWith("https.");
|
|
let cbExpr = "NULL";
|
|
let adapter = "NULL";
|
|
if (e.fn.endsWith("Cb")) {
|
|
const cbT = e.args[3]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} callback not a func`);
|
|
const cb = args[3]!;
|
|
emitter.moveTemp(cb);
|
|
cbExpr = cb.name;
|
|
adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res";
|
|
}
|
|
const entry = tls ? "scr_https_request_url" : "scr_http_request_url";
|
|
return finish(
|
|
`${entry}(${arg(0)}, ${arg(1)}, ${arg(2)}, ${cbExpr}, ${adapter})`,
|
|
);
|
|
}
|
|
case "http.requestConn":
|
|
case "http.requestConnCb": {
|
|
emitter.usesTimers = true;
|
|
let cbExpr = "NULL";
|
|
let adapter = "NULL";
|
|
if (e.fn === "http.requestConnCb") {
|
|
const cbT = e.args[6]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.requestConnCb callback not a func");
|
|
const cb = args[6]!;
|
|
emitter.moveTemp(cb);
|
|
cbExpr = cb.name;
|
|
adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res";
|
|
}
|
|
const dial = args[0]!;
|
|
emitter.moveTemp(dial);
|
|
return finish(
|
|
`scr_http_request_conn(${dial.name}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${cbExpr}, ${adapter})`,
|
|
);
|
|
}
|
|
case "https.createServer": {
|
|
emitter.usesTimers = true;
|
|
const cbT = e.args[2]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: https.createServer handler not a func");
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 2 ? "scr_http_handler_thunk2"
|
|
: cbT.params.length === 1 ? "scr_http_handler_thunk1"
|
|
: "scr_http_handler_thunk0";
|
|
return finish(
|
|
`scr_https_create_server((const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len, ${cb.name}, &${adapter})`,
|
|
);
|
|
}
|
|
case "https.createServerDyn":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_https_create_server_dyn(${arg(0)}, NULL, NULL)`);
|
|
case "https.createServerDynCb": {
|
|
emitter.usesTimers = true;
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: https.createServerDynCb handler not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 2 ? "scr_http_handler_thunk2"
|
|
: cbT.params.length === 1 ? "scr_http_handler_thunk1"
|
|
: "scr_http_handler_thunk0";
|
|
return finish(`scr_https_create_server_dyn(${arg(0)}, ${cb.name}, &${adapter})`);
|
|
}
|
|
case "http.serverOnRequest": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.serverOnRequest handler not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 2 ? "scr_http_handler_thunk2"
|
|
: cbT.params.length === 1 ? "scr_http_handler_thunk1"
|
|
: "scr_http_handler_thunk0";
|
|
emitter.line(`scr_http_server_on_request(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "https.request":
|
|
case "https.requestCb": {
|
|
emitter.usesTimers = true; // an in-flight request holds the loop open
|
|
let cbExpr = "NULL";
|
|
let adapter = "NULL";
|
|
if (e.fn === "https.requestCb") {
|
|
const cbT = e.args[9]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: https.requestCb callback not a func");
|
|
const cb = args[9]!;
|
|
emitter.moveTemp(cb);
|
|
cbExpr = cb.name;
|
|
adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res";
|
|
}
|
|
return finish(
|
|
`scr_https_request(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, (const char *)${arg(8)}->data, ${arg(8)}->len, ${cbExpr}, ${adapter})`,
|
|
);
|
|
}
|
|
case "https.requestAgent":
|
|
case "https.requestAgentCb": {
|
|
emitter.usesTimers = true; // an in-flight request holds the loop open
|
|
let cbExpr = "NULL";
|
|
let adapter = "NULL";
|
|
if (e.fn === "https.requestAgentCb") {
|
|
const cbT = e.args[10]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: https.requestAgentCb callback not a func");
|
|
const cb = args[10]!;
|
|
emitter.moveTemp(cb);
|
|
cbExpr = cb.name;
|
|
adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res";
|
|
}
|
|
return finish(
|
|
`scr_https_request_agent(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, (const char *)${arg(8)}->data, ${arg(8)}->len, ${arg(9)}, ${cbExpr}, ${adapter})`,
|
|
);
|
|
}
|
|
case "https.requestFn":
|
|
case "https.requestFnCb": {
|
|
// The requestFn binding's runtime dial: arg 0 picks the client
|
|
// — a C ternary between the two real entry points over the
|
|
// SAME evaluated argument temps (only one side runs; the cb
|
|
// moves into whichever). The https row's reject/ca args are
|
|
// simply unused on the plain side, like Node's http.request
|
|
// with TLS options.
|
|
emitter.usesTimers = true; // an in-flight request holds the loop open
|
|
let cbExpr = "NULL";
|
|
let adapter = "NULL";
|
|
if (e.fn === "https.requestFnCb") {
|
|
const cbT = e.args[10]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: https.requestFnCb callback not a func");
|
|
const cb = args[10]!;
|
|
emitter.moveTemp(cb);
|
|
cbExpr = cb.name;
|
|
adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res";
|
|
}
|
|
return finish(
|
|
`(${arg(0)} ? scr_https_request(${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, ${arg(8)}, (const char *)${arg(9)}->data, ${arg(9)}->len, ${cbExpr}, ${adapter}) : scr_http_request_ex(${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, ${cbExpr}, ${adapter}, 80, NULL, NULL))`,
|
|
);
|
|
}
|
|
case "http.clientWrite":
|
|
return finish(`scr_http_client_write_str(${arg(0)}, ${arg(1)})`);
|
|
case "http.clientSetHeader":
|
|
return finish(`scr_http_client_set_header(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "http.clientHasHeader":
|
|
return finish(`scr_http_client_has_header_named(${arg(0)}, ${arg(1)})`);
|
|
case "http.clientRemoveHeader":
|
|
return finish(`scr_http_client_remove_header(${arg(0)}, ${arg(1)})`);
|
|
case "http.clientGetHeaderNames":
|
|
return finish(`scr_http_client_get_header_names(${arg(0)})`);
|
|
case "http.clientGetRawHeaderNames":
|
|
return finish(`scr_http_client_get_raw_header_names(${arg(0)})`);
|
|
case "http.clientGetHeaders":
|
|
return finish(`scr_http_client_get_headers(${arg(0)})`);
|
|
case "http.clientWriteBytes":
|
|
return finish(`scr_http_client_write_bytes(${arg(0)}, ${arg(1)})`);
|
|
case "http.clientEnd":
|
|
return finish(`scr_http_client_end(${arg(0)})`);
|
|
case "http.clientEndStr":
|
|
return finish(`scr_http_client_end_str(${arg(0)}, ${arg(1)})`);
|
|
case "http.clientEndBytes":
|
|
return finish(`scr_http_client_end_bytes(${arg(0)}, ${arg(1)})`);
|
|
case "http.clientWriteDyn":
|
|
return finish(`scr_http_client_write_dynv(${arg(0)}, ${arg(1)})`);
|
|
case "http.clientEndDyn":
|
|
return finish(`scr_http_client_end_dynv(${arg(0)}, ${arg(1)})`);
|
|
case "http.clientFlushHeaders":
|
|
return finish(`scr_http_client_flush_headers(${arg(0)})`);
|
|
case "http.clientAddTrailers":
|
|
return finish(`scr_http_client_add_trailers(${arg(0)}, ${arg(1)})`);
|
|
case "http.clientCork":
|
|
emitter.line(`scr_http_client_cork(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.clientUncork":
|
|
return finish(`scr_http_client_uncork(${arg(0)})`);
|
|
case "http.clientWritableCorked":
|
|
return finish(`scr_http_client_writable_corked(${arg(0)})`);
|
|
case "http.clientMethod":
|
|
return finish(`scr_http_client_method(${arg(0)})`);
|
|
case "http.clientPath":
|
|
return finish(`scr_http_client_path(${arg(0)})`);
|
|
case "http.clientHost":
|
|
return finish(`scr_http_client_host(${arg(0)})`);
|
|
case "http.clientProtocol":
|
|
return finish(`scr_http_client_protocol(${arg(0)})`);
|
|
case "http.clientHeadersSent":
|
|
return finish(`scr_http_client_headers_sent(${arg(0)})`);
|
|
case "http.clientWritableEnded":
|
|
return finish(`scr_http_client_writable_ended(${arg(0)})`);
|
|
case "http.clientWritableFinished":
|
|
return finish(`scr_http_client_writable_finished(${arg(0)})`);
|
|
case "http.clientSocket":
|
|
return finish(`scr_http_client_socket(${arg(0)})`);
|
|
case "http.clientReusedSocket":
|
|
return finish(`scr_http_client_reused_socket(${arg(0)})`);
|
|
case "http.clientSetNoDelay":
|
|
return finish(`scr_http_client_set_nodelay(${arg(0)}, ${arg(1)})`);
|
|
case "http.clientSetSocketKeepAlive":
|
|
return finish(`scr_http_client_set_socket_keepalive(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "http.clientSetTimeout":
|
|
return finish(`scr_http_client_set_timeout(${arg(0)}, ${arg(1)})`);
|
|
case "http.clientSetTimeoutCb": {
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_http_client_set_timeout_cb(${arg(0)}, ${arg(1)}, ${cb.name});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.statusCodes":
|
|
return finish("scr_http_status_codes()");
|
|
case "http.methods":
|
|
return finish("scr_http_methods()");
|
|
case "http.clientDestroy":
|
|
emitter.line(`scr_http_client_destroy(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.clientAbort":
|
|
emitter.line(`scr_http_client_abort(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http.clientAborted":
|
|
return finish(`scr_http_client_aborted(${arg(0)})`);
|
|
case "http.clientDestroyed":
|
|
return finish(`scr_http_client_destroyed(${arg(0)})`);
|
|
case "http.clientOnResponse": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.clientOnResponse callback not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = cbT.params.length === 0 ? "scr_http_resp_thunk0" : "scr_http_resp_thunk_res";
|
|
emitter.line(`scr_http_client_on_response(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.clientOnSocket": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.clientOnSocket callback not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = cbT.params.length === 0 ? "scr_net_conn_thunk0" : "scr_net_conn_thunk_sock";
|
|
emitter.line(`scr_http_client_on_socket(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.clientOnError": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.clientOnError callback not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error";
|
|
emitter.line(`scr_http_client_on_error(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http.clientOnTimeout":
|
|
case "http.clientOnClose":
|
|
case "http.clientOnFinish":
|
|
case "http.clientOnAbort": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const fn = e.fn === "http.clientOnTimeout" ? "scr_http_client_on_timeout"
|
|
: e.fn === "http.clientOnClose" ? "scr_http_client_on_close"
|
|
: e.fn === "http.clientOnAbort" ? "scr_http_client_on_abort" : "scr_http_client_on_finish";
|
|
emitter.line(`${fn}(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: http libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitTlsLibCall(state: LibCallState): Temp {
|
|
const { e, emitter, args, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
// node:tls + node:https (scr_tls.c over scr_net.c/scr_http.c).
|
|
// cert/key/ca PEM arguments are strings OR Buffers — both carry
|
|
// data+len, so one emission shape serves both (the cast covers
|
|
// ScrBytes' uint8_t*).
|
|
case "tls.createServer":
|
|
case "tls.createServerCb": {
|
|
emitter.usesTimers = true;
|
|
let cbExpr = "NULL";
|
|
let adapter = "NULL";
|
|
if (e.fn === "tls.createServerCb") {
|
|
const cbT = e.args[2]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: tls.createServerCb handler not a func");
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
cbExpr = cb.name;
|
|
adapter = cbT.params.length === 0 ? "&scr_net_conn_thunk0" : "&scr_net_conn_thunk_sock";
|
|
}
|
|
return finish(
|
|
`scr_tls_create_server((const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len, ${cbExpr}, ${adapter})`,
|
|
);
|
|
}
|
|
// The RUNTIME options records (divergence 66's stance): the dyn
|
|
// walks throw the catchable fence for out-of-bounds members.
|
|
case "tls.pemDyn":
|
|
return finish(`scr_tls_pem_from_dyn(${arg(0)}, ${arg(1)}->data)`);
|
|
case "tls.createServerDyn":
|
|
case "tls.createServerDynCb": {
|
|
emitter.usesTimers = true;
|
|
let cbExpr = "NULL";
|
|
let adapter = "NULL";
|
|
if (e.fn === "tls.createServerDynCb") {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: tls.createServerDynCb handler not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
cbExpr = cb.name;
|
|
adapter = cbT.params.length === 0 ? "&scr_net_conn_thunk0" : "&scr_net_conn_thunk_sock";
|
|
}
|
|
return finish(`scr_tls_create_server_dyn(${arg(0)}, ${cbExpr}, ${adapter})`);
|
|
}
|
|
// The TLSSocket member surface on the socket kind.
|
|
case "tls.sockAuthorized":
|
|
return finish(`scr_tls_sock_authorized(${arg(0)})`);
|
|
case "tls.sockAuthError": {
|
|
// string | null: the verify-failure code string, or the null
|
|
// arm when authorized / never verified (Node's value shape).
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: tls.sockAuthError result is not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
|
|
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
|
if (strTag < 0 || nullTag < 0) {
|
|
throw new InternalCompilerError("emitter bug: tls.sockAuthError union lacks its arms");
|
|
}
|
|
const s = emitter.newTemp(STRING, `scr_tls_sock_auth_error(${arg(0)})`);
|
|
emitter.moveTemp(s);
|
|
const present = `scr_union_new_ref(${strTag}, ${s.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, nullTag);
|
|
return emitter.newTemp(e.type, `${s.name} ? ${present} : ${absent}`);
|
|
}
|
|
case "tls.sockOnSecureConnect": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_tls_sock_on_secure_connect(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "tls.sockOnSession": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: tls.sockOnSession callback not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 0 ? "scr_net_data_thunk0"
|
|
: cbT.params[0]!.kind === "dyn" ? "scr_net_data_thunk_dyn"
|
|
: "scr_net_data_thunk_bytes";
|
|
emitter.line(`scr_tls_sock_on_session(${arg(0)}, ${cb.name}, (void *)&${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
// tls.connect (the TLS client socket): the callback moves in and
|
|
// fires post-handshake — the conn list's secureConnect timing.
|
|
case "tls.connect":
|
|
case "tls.connectCb": {
|
|
emitter.usesTimers = true;
|
|
let cbExpr = "NULL";
|
|
if (e.fn === "tls.connectCb") {
|
|
const cb = args[3]!;
|
|
emitter.moveTemp(cb);
|
|
cbExpr = cb.name;
|
|
}
|
|
return finish(
|
|
`scr_tls_connect_dyn(${arg(0)}, ${arg(1)}, ${arg(2)}, ${cbExpr})`,
|
|
);
|
|
}
|
|
case "tls.createSecureContext":
|
|
// Parses the PEM pair into the opaque SecureContext handle the
|
|
// SNI answer serves; bad material is the construction-time
|
|
// print-and-die trap, like createServer's.
|
|
return finish(
|
|
`scr_tls_create_secure_context((const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len)`,
|
|
);
|
|
case "tls.createSecureContextDyn":
|
|
// The runtime option-bag form: Node's typed validations, then
|
|
// the pem walk (throws catchably on both ladders).
|
|
return finish(`scr_tls_create_secure_context_dyn(${arg(0)})`);
|
|
case "tls.caCertsChk":
|
|
// Always throws (validation error or the trailing fence).
|
|
return finish(
|
|
`(scr_tls_ca_certs_chk(${arg(0)}, ${arg(1)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
|
|
);
|
|
// The CA-store introspection unit (scr_tls_ca.c): cached
|
|
// per-type PEM string arrays (+1 retained answers), and the
|
|
// default-set replacement. get/set are may-throw seeds.
|
|
case "tlsca.get":
|
|
return finish(`scr_tls_ca_get(${arg(0)})`);
|
|
case "tlsca.root":
|
|
return finish(`scr_tls_ca_root()`);
|
|
case "tlsca.set":
|
|
return finish(`scr_tls_ca_set_default(${arg(0)})`);
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: tls libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitHttp2LibCall(state: LibCallState): Temp {
|
|
const { e, emitter, args, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
case "http2.createSecureServer":
|
|
emitter.usesTimers = true;
|
|
return finish(
|
|
`scr_http2_create_secure_server_allow_http1((const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len, NULL, NULL, NULL, NULL, ${arg(2)})`,
|
|
);
|
|
case "http2.createSecureServerH2":
|
|
// The REAL h2-over-TLS server (no allowHTTP1): ALPN advertises
|
|
// h2 alone and the h2 session attaches at establishment —
|
|
// 'stream'/'session' listeners, exactly the h2c surface.
|
|
emitter.usesTimers = true;
|
|
return finish(
|
|
`scr_http2_create_secure_server((const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len, ${arg(2)})`,
|
|
);
|
|
case "http2.createSecureServerReq":
|
|
case "http2.createSecureServerH2Req": {
|
|
// createSecureServer(options, handler): the eager COMPAT
|
|
// handler is the first 'request' listener on either flavor
|
|
// (the createServerReq adapter family).
|
|
emitter.usesTimers = true;
|
|
const cbT = e.args[2]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} handler not a func`);
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 2 ? "scr_http_handler_thunk2"
|
|
: cbT.params.length === 1 ? "scr_http_handler_thunk1"
|
|
: "scr_http_handler_thunk0";
|
|
const certKey = `(const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len`;
|
|
return finish(
|
|
fn === "http2.createSecureServerReq"
|
|
? `scr_http2_create_secure_server_allow_http1(${certKey}, ${cb.name}, &${adapter}, NULL, NULL, ${arg(3)})`
|
|
: `scr_http2_create_secure_server_req(${certKey}, ${cb.name}, &${adapter}, ${arg(3)})`,
|
|
);
|
|
}
|
|
case "http2.createSecureServerDyn":
|
|
// The runtime options record: allowHTTP1/cert/key read at
|
|
// runtime (may-throw — the divergence-66 walk).
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_http2_create_secure_server_dyn(${arg(0)}, NULL, NULL)`);
|
|
case "http2.createSecureServerDynCb": {
|
|
emitter.usesTimers = true;
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: createSecureServerDynCb handler not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 2 ? "scr_http_handler_thunk2"
|
|
: cbT.params.length === 1 ? "scr_http_handler_thunk1"
|
|
: "scr_http_handler_thunk0";
|
|
return finish(`scr_http2_create_secure_server_dyn(${arg(0)}, ${cb.name}, &${adapter})`);
|
|
}
|
|
case "http2.createSecureServerSni": {
|
|
// The SNI form: the runtime parses each connection's
|
|
// ClientHello for server_name BEFORE the handshake, calls the
|
|
// JS SNICallback with (servername, answer-closure), and
|
|
// resumes when the answer arrives. The answer closure's fn is
|
|
// the emitted per-shape thunk (its unions' tags are program
|
|
// data). A union-typed arg is the conditional-spread spelling
|
|
// — the undefined arm passes NULL, exactly the no-SNI server.
|
|
emitter.usesTimers = true;
|
|
const sniT = e.args[2]!.type;
|
|
let cbFuncT: IrType;
|
|
let cbExpr: string;
|
|
if (sniT.kind === "func") {
|
|
cbFuncT = sniT;
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
cbExpr = cb.name;
|
|
} else {
|
|
if (sniT.kind !== "union") throw new InternalCompilerError("emitter bug: createSecureServerSni callback shape");
|
|
const def = emitter.unionsById.get(sniT.unionId);
|
|
const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1;
|
|
const funcArm = def?.arms[funcTag];
|
|
if (funcTag < 0 || !funcArm) throw new InternalCompilerError("emitter bug: createSecureServerSni union lacks its func arm");
|
|
cbFuncT = funcArm;
|
|
const u = args[2]!;
|
|
const t = emitter.newTemp(
|
|
funcArm,
|
|
`${u.name}->tag == ${funcTag} ? scr_closure_retain((ScrClosure *)scr_union_peek(${u.name})) : NULL`,
|
|
);
|
|
emitter.moveTemp(t);
|
|
cbExpr = t.name;
|
|
}
|
|
if (cbFuncT.kind !== "func" || cbFuncT.params[1]?.kind !== "func") {
|
|
throw new InternalCompilerError("emitter bug: createSecureServerSni callback shape (frontend must fence)");
|
|
}
|
|
const answer = emitter.sniAnswerThunkFor(cbFuncT.params[1]);
|
|
return finish(
|
|
`scr_http2_create_secure_server_allow_http1((const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len, NULL, NULL, ${cbExpr}, (void *)&${answer}, ${arg(3)})`,
|
|
);
|
|
}
|
|
case "http2.serverOnSessionError": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: sessionError listener not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = cbT.params.length === 2
|
|
? "scr_http2_session_error_thunk2"
|
|
: cbT.params.length === 1
|
|
? "scr_http2_session_error_thunk1"
|
|
: "scr_http2_session_error_thunk0";
|
|
emitter.line(`scr_http2_server_on_session_error(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http2.streamNoop":
|
|
// `req.stream?.on(...)`: stream is undefined on every
|
|
// connection the allowHTTP1 lowering accepts — the optional
|
|
// chain short-circuits and nothing runs.
|
|
return { name: "", type: e.type };
|
|
case "http2.streamUndefCall":
|
|
// The unguarded form: Node's member read on undefined — the
|
|
// exact catchable TypeError (pending check via the may-throw
|
|
// seed set). Borrows the member-name string.
|
|
return finish(`scr_http2_stream_undef_call(${arg(0)})`);
|
|
// ── the REAL h2c surface (scr_http2.c) ─────────────────────
|
|
// Event registrations take the EMITTED adapter closure (built
|
|
// by the frontend — it owns arity and headers-record building),
|
|
// so the runtime thunk is a fixed passthrough per payload shape.
|
|
case "http2.createServer":
|
|
emitter.usesTimers = true; // a listening server holds the loop open
|
|
return finish(`scr_http2_create_server()`);
|
|
case "http2.createServerReq": {
|
|
// createServer(handler): the eager COMPAT handler — the first
|
|
// 'request' listener, the http.createServer adapter family.
|
|
emitter.usesTimers = true;
|
|
const cbT = e.args[0]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http2.createServerReq handler not a func");
|
|
const cb = args[0]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 2 ? "scr_http_handler_thunk2"
|
|
: cbT.params.length === 1 ? "scr_http_handler_thunk1"
|
|
: "scr_http_handler_thunk0";
|
|
return finish(`scr_http2_create_server_req(${cb.name}, &${adapter})`);
|
|
}
|
|
case "http2.serverOnStream":
|
|
case "http2.sessionOnStream": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const fn = e.fn === "http2.serverOnStream" ? "scr_http2_server_on_stream" : "scr_http2_session_on_stream";
|
|
emitter.line(`${fn}(${arg(0)}, ${cb.name}, &scr_http2_stream_thunk, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http2.serverOnSession": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: serverOnSession listener not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = cbT.params.length === 0 ? "scr_http2_session_thunk0" : "scr_http2_session_thunk";
|
|
emitter.line(`scr_http2_server_on_session(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http2.sessionOnConnect": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: sessionOnConnect listener not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length >= 2 ? "scr_http2_connect_thunk2"
|
|
: cbT.params.length === 1 ? "scr_http2_connect_thunk1"
|
|
: "scr_http2_connect_thunk0";
|
|
emitter.line(`scr_http2_session_on_connect(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http2.connect":
|
|
emitter.usesTimers = true; // an open session holds the loop open
|
|
return finish(
|
|
`scr_http2_connect(${arg(0)}, ${arg(1)}, (const char *)${arg(2)}->data, ${arg(2)}->len, NULL, NULL)`,
|
|
);
|
|
case "http2.connectCb": {
|
|
emitter.usesTimers = true;
|
|
const cbT = e.args[3]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http2.connectCb listener not a func");
|
|
const cb = args[3]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length >= 2 ? "scr_http2_connect_thunk2"
|
|
: cbT.params.length === 1 ? "scr_http2_connect_thunk1"
|
|
: "scr_http2_connect_thunk0";
|
|
return finish(
|
|
`scr_http2_connect(${arg(0)}, ${arg(1)}, (const char *)${arg(2)}->data, ${arg(2)}->len, ${cb.name}, &${adapter})`,
|
|
);
|
|
}
|
|
case "http2.sessionRequest":
|
|
return finish(`scr_http2_session_request(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "http2.sessionClose":
|
|
emitter.line(`scr_http2_session_close(${arg(0)}, NULL);${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.sessionCloseCb": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_http2_session_close(${arg(0)}, ${cb.name});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http2.sessionDestroy":
|
|
emitter.line(`scr_http2_session_destroy(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.sessionOnClose": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_http2_session_on_close(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http2.sessionOnError":
|
|
case "http2.streamOnError": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} listener not a func`);
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error";
|
|
const fn = e.fn === "http2.sessionOnError" ? "scr_http2_session_on_error" : "scr_http2_stream_on_error";
|
|
emitter.line(`${fn}(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http2.sessionOnGoaway": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: sessionOnGoaway listener not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length >= 2 ? "scr_http2_goaway_thunk2"
|
|
: cbT.params.length === 1 ? "scr_http2_goaway_thunk1"
|
|
: "scr_http2_goaway_thunk0";
|
|
emitter.line(`scr_http2_session_on_goaway(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http2.sessionClosed":
|
|
return finish(`scr_http2_session_closed(${arg(0)})`);
|
|
case "http2.sessionDestroyed":
|
|
return finish(`scr_http2_session_destroyed(${arg(0)})`);
|
|
case "http2.sessionEncrypted":
|
|
return finish(`scr_http2_session_encrypted(${arg(0)})`);
|
|
case "http2.sessionType":
|
|
return finish(`scr_http2_session_type(${arg(0)})`);
|
|
case "http2.sessionAlpn":
|
|
return finish(`scr_http2_session_alpn(${arg(0)})`);
|
|
case "http2.sessionSocket":
|
|
return finish(`scr_http2_session_socket(${arg(0)})`);
|
|
case "http2.streamRespond":
|
|
emitter.line(`scr_http2_stream_respond(${arg(0)}, ${arg(1)}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.streamWrite":
|
|
emitter.line(`scr_http2_stream_write_str(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.streamWriteBytes":
|
|
emitter.line(`scr_http2_stream_write_bytes(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.streamEnd":
|
|
emitter.line(`scr_http2_stream_end(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.streamEndStr":
|
|
emitter.line(`scr_http2_stream_end_str(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.streamEndBytes":
|
|
emitter.line(`scr_http2_stream_end_bytes(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.streamClose":
|
|
emitter.line(`scr_http2_stream_close(${arg(0)}, ${arg(1)}, NULL);${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.streamCloseCb": {
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_http2_stream_close(${arg(0)}, ${arg(1)}, ${cb.name});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http2.streamDestroy":
|
|
emitter.line(`scr_http2_stream_destroy(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.sessionSettings0":
|
|
emitter.line(`scr_http2_session_settings(${arg(0)}, NULL, NULL, NULL);${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.sessionSettings":
|
|
emitter.line(`scr_http2_session_settings(${arg(0)}, ${arg(1)}, NULL, NULL);${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.sessionSettingsDynCb":
|
|
emitter.line(`scr_http2_session_settings_dyncb(${arg(0)}, ${arg(1)}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.sessionSettingsCb0": {
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_http2_session_settings(${arg(0)}, ${arg(1)}, ${cb.name}, &scr_http2_settings_thunk0);${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http2.sessionOnSettingsDyn":
|
|
emitter.line(`scr_http2_session_on_settings_dyn(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.sessionOnSettings0": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_http2_session_on_settings(${arg(0)}, ${cb.name}, &scr_http2_settings_thunk0, ${arg(2)}, ${arg(3)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http2.sessionSettingsGet":
|
|
return finish(`scr_http2_session_settings_get(${arg(0)}, ${arg(1)})`);
|
|
case "http2.sessionPendingSettingsAck":
|
|
return finish(`scr_http2_session_pending_settings_ack(${arg(0)})`);
|
|
case "http2.getDefaultSettings":
|
|
return finish(`scr_http2_get_default_settings()`);
|
|
case "http2.streamSetEncoding":
|
|
emitter.line(`scr_http2_stream_set_encoding(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.streamSetEncodingRet":
|
|
// the chaining spelling: same write, answers the receiver +1
|
|
return finish(`scr_http2_stream_set_encoding_ret(${arg(0)}, ${arg(1)})`);
|
|
case "http2.streamResume":
|
|
emitter.line(`scr_http2_stream_resume(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.streamPause":
|
|
emitter.line(`scr_http2_stream_pause(${arg(0)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
case "http2.streamOnData": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: streamOnData callback not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 0 ? "scr_net_data_thunk0"
|
|
: cbT.params[0]!.kind === "dyn" ? "scr_net_data_thunk_dyn"
|
|
: cbT.params[0]!.kind === "string" ? "scr_net_data_thunk_str"
|
|
: "scr_net_data_thunk_bytes";
|
|
emitter.line(`scr_http2_stream_on_data(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http2.streamOnEnd":
|
|
case "http2.streamOnClose":
|
|
case "http2.streamOnAborted": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const fn = e.fn === "http2.streamOnEnd" ? "scr_http2_stream_on_end"
|
|
: e.fn === "http2.streamOnClose" ? "scr_http2_stream_on_close"
|
|
: "scr_http2_stream_on_aborted";
|
|
emitter.line(`${fn}(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http2.streamOnResponse": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_http2_stream_on_response(${arg(0)}, ${cb.name}, &scr_http2_resp_thunk, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "http2.streamId":
|
|
return finish(`scr_http2_stream_id(${arg(0)})`);
|
|
case "http2.streamRstCode":
|
|
return finish(`scr_http2_stream_rst_code(${arg(0)})`);
|
|
case "http2.streamDestroyed":
|
|
return finish(`scr_http2_stream_destroyed(${arg(0)})`);
|
|
case "http2.streamClosed":
|
|
return finish(`scr_http2_stream_closed(${arg(0)})`);
|
|
case "http2.streamAborted":
|
|
return finish(`scr_http2_stream_aborted(${arg(0)})`);
|
|
case "http2.streamPending":
|
|
return finish(`scr_http2_stream_pending(${arg(0)})`);
|
|
case "http2.streamSession":
|
|
return finish(`scr_http2_stream_session(${arg(0)})`);
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: http2 libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitAsyncContextLibCall(state: LibCallState): Temp {
|
|
const { e, emitter, args, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
case "tp.setTimeout":
|
|
return finish(`scr_tp_set_timeout(${arg(0)})`);
|
|
case "tp.setImmediate":
|
|
return finish(`scr_tp_set_immediate()`);
|
|
case "dc.channel":
|
|
return finish(`scr_dc_channel(${arg(0)})`);
|
|
case "dc.subscribe":
|
|
return finish(`scr_dc_subscribe(${arg(0)}, ${arg(1)})`);
|
|
case "dc.unsubscribe":
|
|
return finish(`scr_dc_unsubscribe(${arg(0)}, ${arg(1)})`);
|
|
case "dc.hasSubscribers":
|
|
return finish(`scr_dc_has_subscribers(${arg(0)})`);
|
|
case "dc.publish":
|
|
return finish(`scr_dc_publish(${arg(0)}, ${arg(1)})`);
|
|
case "dc.chanSubscribe":
|
|
return finish(`scr_dc_chan_subscribe(${arg(0)}, ${arg(1)})`);
|
|
case "dc.chanUnsubscribe":
|
|
return finish(`scr_dc_chan_unsubscribe(${arg(0)}, ${arg(1)})`);
|
|
case "dc.chanHasSubscribers":
|
|
return finish(`scr_dc_chan_has_subscribers(${arg(0)})`);
|
|
case "dc.chanName":
|
|
return finish(`scr_dc_chan_name(${arg(0)})`);
|
|
case "timers.setImmediateFnValue":
|
|
// Calling the minted value schedules a real immediate — the
|
|
// loop must run to fire it.
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_set_immediate_dyn_value()`);
|
|
case "timers.immediatePromise":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_immediate_promise()`);
|
|
case "dc.tracingChannel":
|
|
return finish(`scr_dc_tracing_channel(${arg(0)})`);
|
|
case "dc.tracingChannelOf":
|
|
return finish(`scr_dc_tracing_channel_of(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`);
|
|
case "dc.tcChannel":
|
|
return finish(`scr_dc_tc_channel(${arg(0)}, ${arg(1)})`);
|
|
case "dc.tcHasSubscribers":
|
|
return finish(`scr_dc_tc_has_subscribers(${arg(0)})`);
|
|
case "dc.tcSubscribe":
|
|
return finish(`scr_dc_tc_subscribe(${arg(0)}, ${arg(1)})`);
|
|
case "dc.tcUnsubscribe":
|
|
return finish(`scr_dc_tc_unsubscribe(${arg(0)}, ${arg(1)})`);
|
|
case "dc.tcTraceSync":
|
|
return finish(`scr_dc_tc_trace_sync(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`);
|
|
case "dc.tcTraceCallback":
|
|
return finish(`scr_dc_tc_trace_callback(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`);
|
|
case "async.hop":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_await_hop()`);
|
|
case "async.awaitDyn":
|
|
emitter.usesTimers = true; // the hop rides the loop
|
|
return finish(`scr_await_dyn_value(${arg(0)})`);
|
|
case "als.new":
|
|
return finish(`scr_als_new()`);
|
|
case "als.get":
|
|
return finish(`scr_als_get(${arg(0)})`);
|
|
case "als.run":
|
|
return finish(`scr_als_run(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`);
|
|
case "als.exitRun":
|
|
return finish(`scr_als_exit_run(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "als.enterWith":
|
|
return finish(`scr_als_enter_with(${arg(0)}, ${arg(1)})`);
|
|
case "als.disable":
|
|
return finish(`scr_als_disable(${arg(0)})`);
|
|
case "dc.chanBindStore":
|
|
return finish(`scr_dc_chan_bind_store(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "dc.chanUnbindStore":
|
|
return finish(`scr_dc_chan_unbind_store(${arg(0)}, ${arg(1)})`);
|
|
case "dc.chanRunStores":
|
|
return finish(`scr_dc_chan_run_stores(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`);
|
|
case "dc.tcTracePromise":
|
|
// The reaction fiber needs the loop to drain it.
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_dc_tc_trace_promise(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`);
|
|
case "timers.setTimeout": {
|
|
// The loop owns the callback until it fires.
|
|
emitter.usesTimers = true;
|
|
const cb = args[0]!;
|
|
emitter.moveTemp(cb);
|
|
return finish(`scr_set_timeout(${cb.name}, ${arg(1)})`);
|
|
}
|
|
case "timers.setInterval": {
|
|
// The loop owns the callback until clearInterval; the f64
|
|
// handle comes back. A live interval keeps the loop alive.
|
|
emitter.usesTimers = true;
|
|
const cb = args[0]!;
|
|
emitter.moveTemp(cb);
|
|
return finish(`scr_set_interval(${cb.name}, ${arg(1)})`);
|
|
}
|
|
case "timers.clearInterval":
|
|
return finish(`scr_clear_interval(${arg(0)})`);
|
|
case "timers.setTimeoutHandle": {
|
|
emitter.usesTimers = true;
|
|
const cb = args[0]!;
|
|
emitter.moveTemp(cb);
|
|
return finish(`scr_set_timeout_handle(${cb.name}, ${arg(1)})`);
|
|
}
|
|
case "timers.clearTimeout":
|
|
return finish(`scr_clear_interval(${arg(0)})`);
|
|
case "timers.unref":
|
|
// Returns the handle for chaining (comma expr: bookkeep, yield).
|
|
return emitter.newTemp(e.type, `(scr_timer_unref(${arg(0)}), ${arg(0)})`);
|
|
case "timers.ref":
|
|
return emitter.newTemp(e.type, `(scr_timer_ref(${arg(0)}), ${arg(0)})`);
|
|
case "timers.hasRef":
|
|
return finish(`scr_timer_has_ref(${arg(0)})`);
|
|
case "timers.refresh":
|
|
return emitter.newTemp(e.type, `(scr_timer_refresh(${arg(0)}), ${arg(0)})`);
|
|
case "timers.setImmediate": {
|
|
// The loop owns the callback until the check phase fires it.
|
|
emitter.usesTimers = true;
|
|
const cb = args[0]!;
|
|
emitter.moveTemp(cb);
|
|
return finish(`scr_set_immediate(${cb.name})`);
|
|
}
|
|
case "timers.queueMicrotask": {
|
|
// The envelope owns the callback until the drain runs it; the
|
|
// loop must run so a main-queued microtask fires after %main.
|
|
emitter.usesTimers = true;
|
|
const cb = args[0]!;
|
|
emitter.moveTemp(cb);
|
|
return finish(`scr_queue_microtask(${cb.name})`);
|
|
}
|
|
case "timers.queueMicrotaskDyn":
|
|
// Borrowed dyn; a non-function throws ERR_INVALID_ARG_TYPE
|
|
// synchronously (may-throw seed set).
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_queue_microtask_dyn(${arg(0)})`);
|
|
case "timers.clearImmediate":
|
|
return finish(`scr_clear_immediate(${arg(0)})`);
|
|
case "timers.immediateUnref":
|
|
return emitter.newTemp(e.type, `(scr_immediate_unref(${arg(0)}), ${arg(0)})`);
|
|
case "timers.immediateRef":
|
|
return emitter.newTemp(e.type, `(scr_immediate_ref(${arg(0)}), ${arg(0)})`);
|
|
case "timers.immediateHasRef":
|
|
return finish(`scr_immediate_has_ref(${arg(0)})`);
|
|
case "timers.clearNoop":
|
|
// clearTimeout(null) and friends: Node silently ignores
|
|
// non-handles — nothing runs.
|
|
return { name: "", type: e.type };
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: asyncContext libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitProcessLibCall(state: LibCallState): Temp {
|
|
const { e, emitter, args, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
case "process.stdoutWriteBytes":
|
|
return finish(`scr_process_stdout_write_bytes(${arg(0)}, ${arg(1)})`);
|
|
case "process.stderrWriteBytes":
|
|
return finish(`scr_process_stderr_write_bytes(${arg(0)}, ${arg(1)})`);
|
|
case "process.stdoutWriteBytesCb":
|
|
case "process.stderrWriteBytesCb": {
|
|
// Submit the bytes only after every call argument evaluated,
|
|
// then move the completion callback onto the next-tick queue.
|
|
// The shared error-first adapter materializes success `null`.
|
|
emitter.usesTimers = true;
|
|
const cbT = e.args[2]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: process write callback not a func");
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = emitter.fsRenameThunkFor(cbT);
|
|
const write = e.fn === "process.stdoutWriteBytesCb"
|
|
? "scr_process_stdout_write_bytes"
|
|
: "scr_process_stderr_write_bytes";
|
|
const out = emitter.newTemp(e.type, `${write}(${arg(0)}, ${arg(1)})`);
|
|
emitter.line(`scr_process_write_callback(${cb.name}, &${adapter});${emitter.srcComment(e.loc)}`);
|
|
return out;
|
|
}
|
|
case "process.onWarning":
|
|
return finish(`scr_process_on_warning(${arg(0)})`);
|
|
case "process.offWarning":
|
|
return finish(`scr_process_off_warning(${arg(0)})`);
|
|
case "process.emitWarning":
|
|
return finish(`scr_process_emit_warning(${arg(0)})`);
|
|
case "process.onUnhandledRejection":
|
|
// The completed-checkpoint report dispatches the listeners.
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_process_on_unhandled_rejection(${arg(0)}, ${arg(1)})`);
|
|
case "process.offUnhandledRejection":
|
|
return finish(`scr_process_off_unhandled_rejection(${arg(0)})`);
|
|
case "process.onRejectionHandled":
|
|
// Fires after a reported promise gains a handler — the loop
|
|
// must be live for the checkpoint report to run.
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_process_on_rejection_handled(${arg(0)}, ${arg(1)})`);
|
|
case "process.offRejectionHandled":
|
|
return finish(`scr_process_off_rejection_handled(${arg(0)})`);
|
|
case "process.argv":
|
|
return finish(`scr_process_argv()`);
|
|
case "process.platform":
|
|
return finish(`scr_process_platform()`);
|
|
case "process.cwd":
|
|
return finish(`scr_process_cwd()`);
|
|
case "process.stdoutWrite":
|
|
return finish(`scr_process_stdout_write(${arg(0)})`);
|
|
case "process.stderrWrite":
|
|
return finish(`scr_process_stderr_write(${arg(0)})`);
|
|
case "process.envGet": {
|
|
// getenv(3): +1 fresh string, or NULL when unset. The union
|
|
// construction is type-directed HERE — the runtime knows no
|
|
// tags: present wraps the string arm (ownership of the fresh
|
|
// string MOVES into the box), absent yields the interned
|
|
// immortal undefined-arm instance (releases no-op on it).
|
|
if (e.type.kind !== "union") {
|
|
throw new InternalCompilerError("emitter bug: process.envGet result is not a union");
|
|
}
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
if (strTag < 0 || undefTag < 0) {
|
|
throw new InternalCompilerError("emitter bug: process.envGet union lacks its arms");
|
|
}
|
|
const s = emitter.newTemp(STRING, `scr_env_get(${arg(0)})`);
|
|
emitter.moveTemp(s); // moves into the box when present; NULL otherwise
|
|
const present = `scr_union_new_ref(${strTag}, ${s.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, undefTag);
|
|
return emitter.newTemp(e.type, `${s.name} ? ${present} : ${absent}`);
|
|
}
|
|
case "process.envSet":
|
|
return finish(`scr_env_set(${arg(0)}, ${arg(1)})`);
|
|
case "process.envUnset":
|
|
return finish(`scr_env_unset(${arg(0)})`);
|
|
case "process.envPairs":
|
|
return finish(`scr_env_pairs()`);
|
|
case "process.pid":
|
|
return finish(`scr_process_pid()`);
|
|
case "process.getuid":
|
|
return finish(`scr_process_getuid()`);
|
|
case "process.uptime":
|
|
return finish(`scr_process_uptime()`);
|
|
case "module.registryInit":
|
|
return finish(`scr_module_registry_init(${arg(0)})`);
|
|
case "module.define":
|
|
return finish(`scr_module_define(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`);
|
|
case "module.enter":
|
|
return finish(`scr_module_enter(${arg(0)})`);
|
|
case "module.link":
|
|
return finish(`scr_module_link(${arg(0)}, ${arg(1)})`);
|
|
case "module.finish":
|
|
return finish(`scr_module_finish(${arg(0)})`);
|
|
case "module.fail":
|
|
return finish(`scr_module_fail(${arg(0)})`);
|
|
case "module.filename":
|
|
return finish(`scr_module_filename(${arg(0)})`);
|
|
case "module.id":
|
|
return finish(`scr_module_id(${arg(0)})`);
|
|
case "module.path":
|
|
return finish(`scr_module_path(${arg(0)})`);
|
|
case "module.paths":
|
|
return finish(`scr_module_paths(${arg(0)})`);
|
|
case "module.children":
|
|
return finish(`scr_module_children(${arg(0)})`);
|
|
case "module.parent":
|
|
return finish(`scr_module_parent(${arg(0)})`);
|
|
case "module.loaded":
|
|
return finish(`scr_module_loaded(${arg(0)})`);
|
|
case "module.cacheGet":
|
|
return finish(`scr_module_cache_get(${arg(0)})`);
|
|
case "module.cacheHas":
|
|
return finish(`scr_module_cache_has(${arg(0)})`);
|
|
case "module.cacheKeys":
|
|
return finish(`scr_module_cache_keys()`);
|
|
case "process.availableMemory":
|
|
return finish(`scr_available_memory()`);
|
|
case "process.constrainedMemory":
|
|
return finish(`scr_constrained_memory()`);
|
|
case "process.cpuUser":
|
|
return finish(`scr_cpu_user()`);
|
|
case "process.cpuSystem":
|
|
return finish(`scr_cpu_system()`);
|
|
case "process.cpuUserDiff":
|
|
return finish(`scr_cpu_user_diff(${arg(0)})`);
|
|
case "process.cpuSystemDiff":
|
|
return finish(`scr_cpu_system_diff(${arg(0)})`);
|
|
case "process.threadCpuUser":
|
|
return finish(`scr_thread_cpu_user()`);
|
|
case "process.threadCpuSystem":
|
|
return finish(`scr_thread_cpu_system()`);
|
|
case "process.threadCpuUserDiff":
|
|
return finish(`scr_thread_cpu_user_diff(${arg(0)})`);
|
|
case "process.threadCpuSystemDiff":
|
|
return finish(`scr_thread_cpu_system_diff(${arg(0)})`);
|
|
case "process.cpuPrevValidate":
|
|
// MAY THROW: negative/non-finite prev fields raise Node's
|
|
// ERR_INVALID_ARG_VALUE RangeError (finish emits the check).
|
|
return finish(`scr_cpu_prev_validate(${arg(0)}, ${arg(1)})`);
|
|
case "process.rusage":
|
|
return finish(`scr_process_rusage(${arg(0)})`);
|
|
case "process.activeResources":
|
|
// The loop's bookkeeping needs the loop linked.
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_active_resources()`);
|
|
case "process.getgid":
|
|
return finish(`scr_process_getgid()`);
|
|
case "process.execPath":
|
|
return finish(`scr_process_exec_path()`);
|
|
case "process.forkTarget":
|
|
return finish(`scr_process_fork_target(${arg(0)})`);
|
|
case "process.connected":
|
|
return finish(`scr_process_ipc_connected()`);
|
|
case "process.send":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_process_ipc_send(${arg(0)})`);
|
|
case "process.sendCb": {
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: process.send callback not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_process_ipc_send_cb(${arg(0)}, ${cb.name}, &${emitter.ipcSendThunkFor(cbT)})`);
|
|
}
|
|
case "process.disconnect":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_process_ipc_disconnect()`);
|
|
case "process.onMessage": {
|
|
const cbT = e.args[0]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: process message listener not a func");
|
|
const cb = args[0]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.usesTimers = true;
|
|
emitter.line(`scr_process_ipc_on_message(${cb.name}, &${emitter.ipcMessageThunkFor(cbT)}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "process.onDisconnect": {
|
|
const cbT = e.args[0]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: process disconnect listener not a func");
|
|
const cb = args[0]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.usesTimers = true;
|
|
emitter.line(`scr_process_ipc_on_disconnect(${cb.name}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "process.arch":
|
|
return finish(`scr_process_arch()`);
|
|
case "process.versions":
|
|
return finish(`scr_process_versions()`);
|
|
case "process.builtinId":
|
|
return finish(`scr_process_builtin_id(${arg(0)}, ${arg(1)})`);
|
|
case "process.builtinModule":
|
|
return finish(`scr_process_builtin_module(${arg(0)}, ${arg(1)})`);
|
|
case "process.builtinUnsupported":
|
|
return finish(`scr_process_builtin_unsupported(${arg(0)}, ${arg(1)})`);
|
|
case "process.versionsNode":
|
|
return finish(`scr_process_versions_node()`);
|
|
case "process.versionsOpenssl":
|
|
return finish(`scr_process_versions_openssl()`);
|
|
case "process.umask":
|
|
return finish(`scr_process_umask(${arg(0)})`);
|
|
case "process.chdir":
|
|
return finish(`scr_process_chdir(${arg(0)})`);
|
|
case "process.exiting":
|
|
return finish(`scr_process_exiting()`);
|
|
case "process.kill":
|
|
// Throws Node's kill errors (bad pid TypeError, unknown-signal
|
|
// TypeError, ESRCH/EPERM Error); the pending check comes from
|
|
// the may-throw set.
|
|
return finish(`scr_process_kill_named(${arg(0)}, ${arg(1)})`);
|
|
case "process.killNum":
|
|
return finish(`scr_process_kill(${arg(0)}, ${arg(1)})`);
|
|
case "process.exit":
|
|
// Flushes stdout and _Exit()s — never returns (exit handlers,
|
|
// including the RC audit, deliberately do not run).
|
|
return finish(`scr_process_exit(${arg(0)})`);
|
|
case "process.setExitCode":
|
|
return finish(`scr_process_set_exit_code(${arg(0)})`);
|
|
case "process.currentExitCode":
|
|
return finish(`scr_process_exit_code_or_zero()`);
|
|
case "process.nextTick": {
|
|
// The tick queue owns the callback until the drain fires it;
|
|
// ticks run before promise jobs at every loop checkpoint and
|
|
// keep the loop alive while queued, so main runs the loop.
|
|
emitter.usesTimers = true;
|
|
const cb = args[0]!;
|
|
emitter.moveTemp(cb);
|
|
return finish(`scr_next_tick(${cb.name})`);
|
|
}
|
|
case "process.onSignal": {
|
|
// The registry owns the callback (zero-param — frontend-pinned)
|
|
// until off/once removes it. The loop dispatches deliveries.
|
|
emitter.usesTimers = true;
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
return finish(`scr_signal_on(${arg(0)}, ${cb.name}, ${arg(2)})`);
|
|
}
|
|
case "process.offSignal":
|
|
// Borrowed callback: removal is by pointer identity.
|
|
return finish(`scr_signal_off(${arg(0)}, ${arg(1)})`);
|
|
case "process.onExit": {
|
|
// Runtime adapters cover both shapes (the code is a plain
|
|
// double); the registry owns the callback.
|
|
const cbT = e.args[0]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: process.onExit callback not a func");
|
|
const cb = args[0]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter = cbT.params.length === 0 ? "scr_exit_thunk0" : "scr_exit_thunk_code";
|
|
emitter.line(`scr_process_on_exit(${cb.name}, &${adapter}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "process.offExit":
|
|
return finish(`scr_process_off_exit(${arg(0)})`);
|
|
case "stdin.onData": {
|
|
// A data listener is a consumer: the loop watches fd 0 and
|
|
// stays alive until EOF, so main must run it.
|
|
emitter.usesTimers = true;
|
|
const cbT = e.args[0]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: stdin.onData callback not a func");
|
|
const cb = args[0]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 0 ? "scr_stdin_data_thunk0" : "scr_stdin_data_thunk_bytes";
|
|
emitter.line(`scr_stdin_on_data(${cb.name}, &${adapter}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "stdin.onEnd": {
|
|
emitter.usesTimers = true;
|
|
const cb = args[0]!;
|
|
emitter.moveTemp(cb);
|
|
return finish(`scr_stdin_on_end(${cb.name}, ${arg(1)})`);
|
|
}
|
|
case "stdin.onError": {
|
|
// The child error adapters fit exactly (message → %Error).
|
|
emitter.usesTimers = true;
|
|
const cbT = e.args[0]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: stdin.onError callback not a func");
|
|
const cb = args[0]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error";
|
|
emitter.line(`scr_stdin_on_error(${cb.name}, &${adapter}, ${arg(1)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "stdin.nextChunk":
|
|
// +1 promise of the next chunk (empty = EOF); the await parks
|
|
// the fiber while the loop watches fd 0.
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_stdin_next_chunk()`);
|
|
case "process.isTTY":
|
|
return finish(`scr_process_is_tty(${arg(0)})`);
|
|
case "process.columns":
|
|
case "process.rows": {
|
|
// ioctl(TIOCGWINSZ): a non-negative dimension wraps the f64 arm;
|
|
// a non-TTY stream (or an ioctl refusal) comes back negative
|
|
// and yields the interned undefined-arm instance — Node's
|
|
// missing geometry. Type-directed union construction, like
|
|
// process.envGet.
|
|
if (e.type.kind !== "union") {
|
|
throw new InternalCompilerError(`emitter bug: ${e.fn} result is not a union`);
|
|
}
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const f64Tag = def ? def.arms.findIndex((a) => a.kind === "f64") : -1;
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
if (f64Tag < 0 || undefTag < 0) {
|
|
throw new InternalCompilerError(`emitter bug: ${e.fn} union lacks its arms`);
|
|
}
|
|
const w = emitter.newTemp(F64, `scr_process_${e.fn === "process.rows" ? "rows" : "columns"}(${arg(0)})`);
|
|
const present = `scr_union_new_f64(${f64Tag}, ${w.name})`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, undefTag);
|
|
return emitter.newTemp(e.type, `${w.name} >= 0 ? ${present} : ${absent}`);
|
|
}
|
|
case "process.stdinDestroy":
|
|
// A deliberate no-op (SEMANTICS.md).
|
|
return finish(`scr_process_stdin_destroy()`);
|
|
case "process.stdinSetRawMode":
|
|
// TTY: termios raw mode (libuv's UV_TTY_MODE_RAW set). Non-TTY:
|
|
// Node's exact catchable TypeError (the may-throw seed).
|
|
return finish(`scr_process_stdin_set_raw_mode(${arg(0)})`);
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: process libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitErrorsEventsLibCall(state: LibCallState): Temp {
|
|
const { e, emitter, args, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
case "error.nodeThrow":
|
|
// The compiler-resolved Node-parity throw (always throws —
|
|
// the typed dummy is abandoned by the pending check's
|
|
// unwind; releases are NULL-tolerant). Borrows both strings.
|
|
return finish(
|
|
`(scr_throw_node_coded(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
|
|
);
|
|
case "error.argTypeThrow":
|
|
// Always throws with the runtime-rendered Received tail (the
|
|
// error.nodeThrow dummy pattern). Borrows all three.
|
|
return finish(
|
|
`(scr_throw_arg_type(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
|
|
);
|
|
case "error.propTypeThrow":
|
|
// The property flavor ("The \"options.x\" property must be
|
|
// ...") — same always-throw dummy pattern.
|
|
return finish(
|
|
`(scr_throw_prop_type(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
|
|
);
|
|
case "error.newOptions":
|
|
case "error.new": {
|
|
// Which builtin the runtime constructs is named by the RESULT
|
|
// type. Arguments are borrowed; raw message conversion may throw.
|
|
if (e.type.kind !== "object") throw new InternalCompilerError("emitter bug: error.new result is not a class");
|
|
const rec = RUNTIME_ERROR_CLASSES.get(e.type.className);
|
|
if (!rec) throw new InternalCompilerError(`emitter bug: error.new of ${e.type.className}`);
|
|
return finish(fn === "error.newOptions"
|
|
? `scr_error_new_options(${rec.kind}, ${arg(0)}, ${arg(1)})`
|
|
: `scr_error_new(${rec.kind}, ${arg(0)})`);
|
|
}
|
|
case "error.ctorOptions":
|
|
case "error.ctor": {
|
|
// super(message) into the builtin base: stamps name/message on
|
|
// the receiver (borrowed, like the message). The RECEIVER'S
|
|
// static class names which builtin name to stamp.
|
|
const recvT = e.args[0]!.type;
|
|
if (recvT.kind !== "object") throw new InternalCompilerError("emitter bug: error.ctor receiver is not a class");
|
|
const rec = RUNTIME_ERROR_CLASSES.get(recvT.className);
|
|
if (!rec) throw new InternalCompilerError(`emitter bug: error.ctor on ${recvT.className}`);
|
|
return finish(fn === "error.ctorOptions"
|
|
? `scr_error_init_options(${arg(0)}, ${rec.kind}, ${arg(1)}, ${arg(2)})`
|
|
: `scr_error_init(${arg(0)}, ${rec.kind}, ${arg(1)})`);
|
|
}
|
|
case "error.cause":
|
|
return finish(`scr_error_cause((ScrError *)${arg(0)})`);
|
|
case "error.hasCause":
|
|
return finish(`scr_error_has_cause((ScrError *)${arg(0)})`);
|
|
case "error.setCause":
|
|
return finish(`scr_error_set_cause((ScrError *)${arg(0)}, ${arg(1)})`);
|
|
case "error.deleteCause":
|
|
return finish(`scr_error_delete_cause((ScrError *)${arg(0)})`);
|
|
case "error.toString":
|
|
// Borrowed receiver; +1 "name: message" (Node's toString rules).
|
|
return finish(`scr_error_to_string((ScrError *)${arg(0)})`);
|
|
case "error.newDom":
|
|
// new DOMException(message?, nameOrOptions?) — both dyn args
|
|
// borrowed (WebIDL resolution runs in the runtime); +1
|
|
// %DOMException. Never throws.
|
|
return finish(`scr_domex_new(${arg(0)}, ${arg(1)})`);
|
|
case "error.domCode":
|
|
// The WebIDL legacy numeric code (0 off-table). Borrowed.
|
|
return finish(`scr_domex_code(${arg(0)})`);
|
|
case "error.domHasCause":
|
|
// `'cause' in e` — the options form's own-property record.
|
|
return finish(`scr_domex_has_cause(${arg(0)})`);
|
|
case "error.domCause":
|
|
// +1 cause dyn value (the dyn undefined when absent).
|
|
return finish(`scr_domex_cause(${arg(0)})`);
|
|
case "error.domClone":
|
|
// WebIDL serialization: name/message copy, code re-derives.
|
|
// Borrowed receiver + borrowed options; +1 %DOMException.
|
|
// Option errors throw (may-throw seed set).
|
|
return finish(`scr_domex_clone(${arg(0)}, ${arg(1)})`);
|
|
case "regex.new":
|
|
// Eager compile: bad patterns/flags throw catchable
|
|
// SyntaxError (may-throw seed set). Borrowed strings; +1.
|
|
return finish(`scr_regex_new(${arg(0)}, ${arg(1)})`);
|
|
// node:events EventEmitter (scr_events_emitter.c — linked
|
|
// exactly when these appear, moduleUsesEmitter). Receivers are
|
|
// borrowed and reinterpret to the runtime's ScrEmitter prefix;
|
|
// the chaining forms answer the receiver +1 (Node's `return
|
|
// this`), cast back to the receiver's static struct.
|
|
case "emitter.new":
|
|
// +1 bare emitter, collector-headered (the registry owns
|
|
// listener closures — unconditionally cycle-capable).
|
|
return finish(`scr_emitter_new()`);
|
|
case "emitter.ctor":
|
|
// super() into the prefix: the allocation already initialized
|
|
// it (registry NULL, display name stamped by the newFn).
|
|
return finish(`scr_emitter_init(${arg(0)})`);
|
|
case "emitter.on": {
|
|
// (recv, name, cb /moves/, once, prepend): the listener rides
|
|
// the registry with its emitted per-signature invoke adapter.
|
|
// May-throw: 'newListener' meta listeners run inside.
|
|
const cbT = e.args[2]!.type;
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
const thunk = emitter.emitterInvokeThunkFor(cbT);
|
|
return finish(
|
|
`(${cType(e.type).trim()})scr_emitter_on((ScrEmitter *)${arg(0)}, ${arg(1)}, ${cb.name}, &${thunk}, ${arg(3)}, ${arg(4)})`,
|
|
);
|
|
}
|
|
case "emitter.off":
|
|
// (recv, name, cb /borrowed — identity match/). May-throw:
|
|
// 'removeListener' meta listeners run inside.
|
|
return finish(
|
|
`(${cType(e.type).trim()})scr_emitter_off((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)})`,
|
|
);
|
|
case "emitter.checkListener":
|
|
// Node's registration-family listener type check: a
|
|
// non-function dyn throws the catchable ERR_INVALID_ARG_TYPE
|
|
// TypeError (may-throw seed).
|
|
return finish(`scr_emitter_check_listener(${arg(0)})`);
|
|
case "emitter.onDyn": {
|
|
// (recv, name, cb /borrowed dyn — the identity/, adapter
|
|
// /moves/, once, prepend): the ADAPTER (the frontend's
|
|
// dynCheck value — it boxes the tuple to dyn and calls the
|
|
// original) rides the registry with its own invoke thunk; the
|
|
// runtime keeps the original as the entry's identity.
|
|
const adT = e.args[3]!.type;
|
|
const adapter = args[3]!;
|
|
emitter.moveTemp(adapter);
|
|
const dynThunk = emitter.emitterInvokeThunkFor(adT);
|
|
return finish(
|
|
`(${cType(e.type).trim()})scr_emitter_on_dyn((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)}, ${adapter.name}, &${dynThunk}, ${arg(4)}, ${arg(5)})`,
|
|
);
|
|
}
|
|
case "emitter.onFlex":
|
|
return finish(
|
|
`(${cType(e.type).trim()})scr_emitter_on_flex((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`,
|
|
);
|
|
case "emitter.onData": {
|
|
// The stream-'data' registration: same runtime entry as
|
|
// emitter.on, but the DATA invoke thunk (the two-slot payload
|
|
// ABI — see scr_stream_emit_data).
|
|
const cbT = e.args[2]!.type;
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
const thunk = emitter.streamDataThunkFor(cbT);
|
|
return finish(
|
|
`(${cType(e.type).trim()})scr_emitter_on((ScrEmitter *)${arg(0)}, ${arg(1)}, ${cb.name}, &${thunk}, ${arg(3)}, ${arg(4)})`,
|
|
);
|
|
}
|
|
case "emitter.onDataDyn": {
|
|
// The dyn-listener flavor: the frontend's adapter takes ONE
|
|
// dyn parameter; the data thunk boxes the payload by tag.
|
|
const adT = e.args[3]!.type;
|
|
const adapter = args[3]!;
|
|
emitter.moveTemp(adapter);
|
|
const dynThunk = emitter.streamDataThunkFor(adT);
|
|
return finish(
|
|
`(${cType(e.type).trim()})scr_emitter_on_dyn((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)}, ${adapter.name}, &${dynThunk}, ${arg(4)}, ${arg(5)})`,
|
|
);
|
|
}
|
|
case "emitter.emitData": {
|
|
// A user emit('data', chunk) on a stream-rooted receiver:
|
|
// fill the matching payload slot, NULL the other.
|
|
const chunkT = e.args[2]!.type;
|
|
const both =
|
|
chunkT.kind === "string"
|
|
? `(ScrBytes *)NULL, ${arg(2)}`
|
|
: `${arg(2)}, (ScrStr *)NULL`;
|
|
return finish(`scr_emitter_emit((ScrEmitter *)${arg(0)}, ${arg(1)}, ${both})`);
|
|
}
|
|
case "emitter.offDyn":
|
|
// (recv, name, cb /borrowed dyn/): identity is the dyn box's
|
|
// underlying closure. May-throw ('removeListener' meta).
|
|
return finish(
|
|
`(${cType(e.type).trim()})scr_emitter_off_dyn((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)})`,
|
|
);
|
|
case "emitter.removeAll":
|
|
return finish(
|
|
`(${cType(e.type).trim()})scr_emitter_remove_all((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)})`,
|
|
);
|
|
case "emitter.emit":
|
|
// The variadic dispatch: the event's unified tuple rides the
|
|
// C variadic tail, every argument borrowed (the invoke
|
|
// adapters retain each callee's +1). May-throw (listeners).
|
|
return finish(
|
|
`scr_emitter_emit((ScrEmitter *)${arg(0)}, ${arg(1)}${e.args.slice(2).map((_, i) => `, ${arg(i + 2)}`).join("")})`,
|
|
);
|
|
case "emitter.emitFlex": {
|
|
const count = e.args.length - 2;
|
|
const payload = count === 0
|
|
? "NULL"
|
|
: `(ScrDyn *const[]){${e.args.slice(2).map((_, i) => arg(i + 2)).join(", ")}}`;
|
|
return finish(
|
|
`scr_emitter_emit_flex((ScrEmitter *)${arg(0)}, ${arg(1)}, ${payload}, ${count})`,
|
|
);
|
|
}
|
|
case "emitter.emitError":
|
|
// emit('error', err): unhandled ⇒ the runtime throws the
|
|
// payload through the exception cell (may-throw seed).
|
|
return finish(
|
|
`scr_emitter_emit_error((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)})`,
|
|
);
|
|
case "emitter.count":
|
|
return finish(`scr_emitter_listener_count((ScrEmitter *)${arg(0)}, ${arg(1)})`);
|
|
case "emitter.countFn":
|
|
return finish(`scr_emitter_listener_count_fn((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "emitter.names":
|
|
// +1 string[] in first-registration order.
|
|
return finish(`scr_emitter_event_names((ScrEmitter *)${arg(0)})`);
|
|
case "emitter.listeners":
|
|
// +1 closure array (originals — once wrappers are runtime-
|
|
// internal; SEMANTICS.md documents the rawListeners stance).
|
|
return finish(`scr_emitter_listeners((ScrEmitter *)${arg(0)}, ${arg(1)})`);
|
|
case "emitter.setMax":
|
|
return finish(
|
|
`(${cType(e.type).trim()})scr_emitter_set_max((ScrEmitter *)${arg(0)}, ${arg(1)})`,
|
|
);
|
|
case "emitter.setMaxChk":
|
|
// The checked-dynamic ladder (may throw); +1 receiver back.
|
|
return finish(
|
|
`(${cType(e.type).trim()})scr_emitter_set_max_chk((ScrEmitter *)${arg(0)}, ${arg(1)})`,
|
|
);
|
|
case "emitter.getMax":
|
|
return finish(`scr_emitter_get_max((ScrEmitter *)${arg(0)})`);
|
|
case "emitter.setDefaultMax":
|
|
return finish(`scr_emitter_set_default_max(${arg(0)})`);
|
|
case "emitter.setDefaultMaxChk":
|
|
return finish(`scr_emitter_set_default_max_chk(${arg(0)}, ${arg(1)})`);
|
|
case "emitter.getDefaultMax":
|
|
return finish(`scr_emitter_get_default_max()`);
|
|
case "error.code": {
|
|
// `string | undefined`, constructed type-directedly like
|
|
// process.envGet: a stamped code wraps the string arm (+1
|
|
// moves into the box); absent yields the interned
|
|
// undefined-arm instance. The receiver may be a user subclass
|
|
// struct — the code slot sits in its ScrError prefix, so the
|
|
// cast is the ordinary upcast reinterpret.
|
|
if (e.type.kind !== "union") {
|
|
throw new InternalCompilerError("emitter bug: error.code result is not a union");
|
|
}
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
|
|
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
|
if (strTag < 0 || undefTag < 0) {
|
|
throw new InternalCompilerError("emitter bug: error.code union lacks its arms");
|
|
}
|
|
const s = emitter.newTemp(STRING, `scr_error_code((ScrError *)${arg(0)})`);
|
|
emitter.moveTemp(s); // moves into the box when present; NULL otherwise
|
|
const present = `scr_union_new_ref(${strTag}, ${s.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`;
|
|
const absent = emitter.unitInstanceRef(e.type.unionId, undefTag);
|
|
return emitter.newTemp(e.type, `${s.name} ? ${present} : ${absent}`);
|
|
}
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: errorsEvents libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitStreamLibCall(state: LibCallState): Temp {
|
|
const { e, emitter, args, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
case "stream.onData": {
|
|
// The callback MOVES into the stream's listener registry; the
|
|
// adapter is per callback shape — runtime-provided for the
|
|
// zero-param and Buffer forms, emitted per union for the
|
|
// `Buffer | string` chunk (the chunk wraps at its Buffer arm).
|
|
emitter.usesTimers = true; // a flowing stream holds the loop
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: stream.onData callback not a func");
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
const param = cbT.params[0];
|
|
const adapter =
|
|
param === undefined
|
|
? "scr_child_stream_thunk0"
|
|
: param.kind === "union"
|
|
? emitter.childDataThunkFor(param)
|
|
: "scr_child_stream_thunk_bytes";
|
|
emitter.line(
|
|
`scr_child_stream_on_data(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`,
|
|
);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "stream.onDataStr": {
|
|
emitter.usesTimers = true;
|
|
const cbT = e.args[1]!.type;
|
|
if (cbT.kind !== "func" || cbT.params[0]?.kind !== "string") {
|
|
throw new InternalCompilerError("emitter bug: stream.onDataStr callback not a string func");
|
|
}
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(
|
|
`scr_child_stream_on_data_str(${arg(0)}, ${cb.name}, &scr_child_stream_thunk_str, ${arg(2)});${emitter.srcComment(e.loc)}`,
|
|
);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "stream.onEnd": {
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_child_stream_on_end(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "stream.childSetEncoding":
|
|
return finish(`scr_child_stream_set_encoding(${arg(0)}, ${arg(1)})`);
|
|
// node:stream (scr_stream.c — linked exactly when these appear,
|
|
// moduleUsesStream). Receivers reinterpret to the shared
|
|
// ScrStream layout (the ScrEmitter prefix plus the stream-state
|
|
// pointer); chaining forms answer the receiver +1 cast back to
|
|
// its static class. Any operation that can run user code holds
|
|
// the loop open (deferred next-tick emissions): usesTimers.
|
|
case "readable.new":
|
|
case "writable.new":
|
|
case "duplex.new":
|
|
case "transform.new":
|
|
case "passthrough.new":
|
|
case "readable.init":
|
|
case "writable.init":
|
|
case "duplex.init":
|
|
case "transform.init":
|
|
case "passthrough.init": {
|
|
emitter.usesTimers = true;
|
|
// Head args then flags then the PRESENT callbacks in canonical
|
|
// order — the flags literal names which; absent ones emit NULL.
|
|
// The .init forms (a subclass constructor's super(options))
|
|
// carry the BORROWED receiver at arg 0 and shift everything by
|
|
// one; scr_stream_init_* fills the already-allocated struct.
|
|
const base = fn.slice(0, fn.indexOf("."));
|
|
const isInit = fn.endsWith(".init");
|
|
const off = isInit ? 1 : 0;
|
|
const duplexShape = base !== "readable" && base !== "writable";
|
|
const headLen = duplexShape ? 8 : 4;
|
|
const flagsArg = e.args[off + headLen - 1]!;
|
|
if (flagsArg.kind !== "numLit") throw new InternalCompilerError(`emitter bug: ${fn} flags not a literal`);
|
|
const flags = flagsArg.value;
|
|
const canonical: { name: string; kind: "r" | "w" | "f" | "d" | "t" | "l" }[] =
|
|
base === "readable"
|
|
? [{ name: "read", kind: "r" }, { name: "destroy", kind: "d" }]
|
|
: base === "writable"
|
|
? [{ name: "write", kind: "w" }, { name: "final", kind: "f" }, { name: "destroy", kind: "d" }]
|
|
: base === "duplex"
|
|
? [{ name: "read", kind: "r" }, { name: "write", kind: "w" }, { name: "final", kind: "f" }, { name: "destroy", kind: "d" }]
|
|
: [{ name: "transform", kind: "t" }, { name: "flush", kind: "l" }, { name: "destroy", kind: "d" }];
|
|
const cbArgs: string[] = [];
|
|
let at = off + headLen;
|
|
for (let i = 0; i < canonical.length; i++) {
|
|
if ((flags & (1 << i)) === 0) {
|
|
cbArgs.push("NULL", "NULL");
|
|
continue;
|
|
}
|
|
const cb = args[at]!;
|
|
const cbT = e.args[at]!.type;
|
|
emitter.moveTemp(cb); // the callback closure MOVES into the stream
|
|
cbArgs.push(cb.name, `&${emitter.streamCbThunkFor(canonical[i]!.kind, cbT)}`);
|
|
at++;
|
|
}
|
|
const entry = isInit ? `scr_stream_init_${base}` : `scr_stream_new_${base}`;
|
|
const headIdx = duplexShape ? [0, 1, 2, 3, 4, 5, 6] : [0, 1, 2];
|
|
const head = headIdx.map((i) => arg(off + i)).join(", ");
|
|
if (isInit) {
|
|
return finish(`${entry}((ScrStream *)${arg(0)}, ${head}, ${cbArgs.join(", ")})`);
|
|
}
|
|
return finish(`(${cType(e.type).trim()})${entry}(${head}, ${cbArgs.join(", ")})`);
|
|
}
|
|
case "stream.setRead":
|
|
case "stream.setWrite":
|
|
case "stream.setFinal":
|
|
case "stream.setDestroy":
|
|
case "stream.setTransform":
|
|
case "stream.setFlush": {
|
|
// The underscore-method assignment surface: the runtime slot
|
|
// swaps its closure (+1 moves in) and invoke thunk — the next
|
|
// _read/_write/... dispatch uses it, Node's timing.
|
|
emitter.usesTimers = true;
|
|
const kindOf: Record<string, "r" | "w" | "f" | "d" | "t" | "l"> = {
|
|
"stream.setRead": "r", "stream.setWrite": "w", "stream.setFinal": "f",
|
|
"stream.setDestroy": "d", "stream.setTransform": "t", "stream.setFlush": "l",
|
|
};
|
|
const symOf: Record<string, string> = {
|
|
"stream.setRead": "scr_stream_set_read", "stream.setWrite": "scr_stream_set_write",
|
|
"stream.setFinal": "scr_stream_set_final", "stream.setDestroy": "scr_stream_set_destroy",
|
|
"stream.setTransform": "scr_stream_set_transform", "stream.setFlush": "scr_stream_set_flush",
|
|
};
|
|
const cb = args[1]!;
|
|
const cbT = e.args[1]!.type;
|
|
emitter.moveTemp(cb); // the callback closure MOVES into the stream
|
|
return finish(`${symOf[fn]!}((ScrStream *)${arg(0)}, ${cb.name}, &${emitter.streamCbThunkFor(kindOf[fn]!, cbT)})`);
|
|
}
|
|
case "stream.finished":
|
|
case "stream.finishedDyn": {
|
|
// finished(s, cb): the +1 cleanup closure answers. Typed
|
|
// callbacks ride the "e" thunk (this + Error|null prefix);
|
|
// dyn values ride the runtime's own inv.
|
|
emitter.usesTimers = true;
|
|
if (fn === "stream.finishedDyn") {
|
|
return finish(`(${cType(e.type).trim()})scr_stream_finished_dyn((ScrStream *)${arg(0)}, ${arg(1)})`);
|
|
}
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb); // the watcher closure MOVES into the stream
|
|
const thunk = emitter.streamCbThunkFor("e", e.args[1]!.type);
|
|
return finish(`(${cType(e.type).trim()})scr_stream_finished((ScrStream *)${arg(0)}, ${cb.name}, &${thunk})`);
|
|
}
|
|
case "stream.pipeline":
|
|
case "stream.pipelineDyn": {
|
|
// pipeline(count, s1..sn, cb): the destination answers +1.
|
|
emitter.usesTimers = true;
|
|
const countArg = e.args[0]!;
|
|
if (countArg.kind !== "numLit") throw new InternalCompilerError(`emitter bug: ${fn} count not a literal`);
|
|
const n = countArg.value;
|
|
const list = Array.from({ length: n }, (_, i) => `(ScrStream *)${arg(1 + i)}`).join(", ");
|
|
if (fn === "stream.pipelineDyn") {
|
|
return finish(
|
|
`(${cType(e.type).trim()})scr_stream_pipeline_dyn(${n}, (ScrStream *[]){ ${list} }, ${arg(1 + n)})`,
|
|
);
|
|
}
|
|
const cb = args[1 + n]!;
|
|
emitter.moveTemp(cb);
|
|
const thunk = emitter.streamCbThunkFor("e", e.args[1 + n]!.type);
|
|
return finish(
|
|
`(${cType(e.type).trim()})scr_stream_pipeline(${n}, (ScrStream *[]){ ${list} }, ${cb.name}, &${thunk})`,
|
|
);
|
|
}
|
|
case "sc.text":
|
|
// stream/consumers: +1 pending promises the accumulate-and-
|
|
// settle machinery resolves or rejects (the stream's
|
|
// error / premature close / json's SyntaxError).
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_sc_text((ScrStream *)${arg(0)})`);
|
|
case "sc.json":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_sc_json((ScrStream *)${arg(0)})`);
|
|
case "sc.buffer":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_sc_buffer((ScrStream *)${arg(0)})`);
|
|
case "readable.newDyn":
|
|
case "writable.newDyn":
|
|
case "duplex.newDyn":
|
|
case "transform.newDyn":
|
|
case "passthrough.newDyn": {
|
|
// The dyn-options constructor: the runtime walks the record
|
|
// (borrowed). MAY THROW (NULL result with the exception
|
|
// pending — the seed set carries these).
|
|
emitter.usesTimers = true;
|
|
const base = fn.slice(0, fn.indexOf("."));
|
|
return finish(`(${cType(e.type).trim()})scr_stream_new_${base}_dyn(${arg(0)})`);
|
|
}
|
|
case "readable.initDyn":
|
|
case "writable.initDyn":
|
|
case "duplex.initDyn":
|
|
case "transform.initDyn":
|
|
case "passthrough.initDyn": {
|
|
// The dyn-options super(options): borrowed receiver + record,
|
|
// then the FALLBACK underscore-method wrappers in canonical
|
|
// order (flags names which; absent ones emit NULL — exactly
|
|
// the .init callback ABI; wrappers MOVE).
|
|
emitter.usesTimers = true;
|
|
const base = fn.slice(0, fn.indexOf("."));
|
|
const flagsArg = e.args[2]!;
|
|
if (flagsArg.kind !== "numLit") throw new InternalCompilerError(`emitter bug: ${fn} flags not a literal`);
|
|
const flags = flagsArg.value;
|
|
const canonical: { name: string; kind: "r" | "w" | "f" | "d" | "t" | "l" }[] =
|
|
base === "readable"
|
|
? [{ name: "read", kind: "r" }, { name: "destroy", kind: "d" }]
|
|
: base === "writable"
|
|
? [{ name: "write", kind: "w" }, { name: "final", kind: "f" }, { name: "destroy", kind: "d" }]
|
|
: base === "duplex"
|
|
? [{ name: "read", kind: "r" }, { name: "write", kind: "w" }, { name: "final", kind: "f" }, { name: "destroy", kind: "d" }]
|
|
: [{ name: "transform", kind: "t" }, { name: "flush", kind: "l" }, { name: "destroy", kind: "d" }];
|
|
const cbArgs: string[] = [];
|
|
let at = 3;
|
|
for (let i = 0; i < canonical.length; i++) {
|
|
if ((flags & (1 << i)) === 0) {
|
|
cbArgs.push("NULL", "NULL");
|
|
continue;
|
|
}
|
|
const cb = args[at]!;
|
|
const cbT = e.args[at]!.type;
|
|
emitter.moveTemp(cb);
|
|
cbArgs.push(cb.name, `&${emitter.streamCbThunkFor(canonical[i]!.kind, cbT)}`);
|
|
at++;
|
|
}
|
|
return finish(`scr_stream_init_${base}_dyn((ScrStream *)${arg(0)}, ${arg(1)}, ${cbArgs.join(", ")})`);
|
|
}
|
|
case "readable.push":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_stream_push((ScrStream *)${arg(0)}, ${arg(1)})`);
|
|
case "readable.pushStr":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_stream_push_str((ScrStream *)${arg(0)}, ${arg(1)})`);
|
|
case "readable.pushStrEnc":
|
|
// push(chunk, enc) with a literal non-utf8 encoding.
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_stream_push_str_enc((ScrStream *)${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "readable.pushEncoding":
|
|
// The defaultEncoding option's push side (chaining, +1).
|
|
emitter.usesTimers = true;
|
|
return finish(`(${cType(e.type).trim()})scr_stream_set_push_encoding((ScrStream *)${arg(0)}, ${arg(1)})`);
|
|
case "readable.pushNull":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_stream_push_null((ScrStream *)${arg(0)})`);
|
|
case "readable.pushU":
|
|
case "writable.writeU": {
|
|
// Union-typed chunk: dispatch by tag (bytes / string / null
|
|
// arms — the frontend admitted exactly those).
|
|
emitter.usesTimers = true;
|
|
const t = e.args[1]!.type;
|
|
if (t.kind !== "union") throw new InternalCompilerError(`emitter bug: ${fn} chunk not a union`);
|
|
const def = emitter.unionsById.get(t.unionId);
|
|
if (!def) throw new InternalCompilerError(`emitter bug: ${fn} union unknown`);
|
|
const bytesTag = def.arms.findIndex((a) => a.kind === "bytes");
|
|
const strTag = def.arms.findIndex((a) => a.kind === "string");
|
|
const nullTag = def.arms.findIndex((a) => a.kind === "nullT");
|
|
const u = arg(1);
|
|
const recv = `(ScrStream *)${arg(0)}`;
|
|
const pushing = fn === "readable.pushU";
|
|
const onBytes = pushing
|
|
? `scr_stream_push(${recv}, (ScrBytes *)scr_union_peek(${u}))`
|
|
: `scr_stream_write(${recv}, (ScrBytes *)scr_union_peek(${u}), NULL)`;
|
|
const onStr = pushing
|
|
? `scr_stream_push_str(${recv}, (ScrStr *)scr_union_peek(${u}))`
|
|
: `scr_stream_write_str(${recv}, (ScrStr *)scr_union_peek(${u}), NULL)`;
|
|
const onNull = pushing
|
|
? `scr_stream_push_null(${recv})`
|
|
: `scr_stream_write_null(${recv})`;
|
|
const present: { tag: number; expr: string }[] = [
|
|
...(nullTag >= 0 ? [{ tag: nullTag, expr: onNull }] : []),
|
|
...(strTag >= 0 ? [{ tag: strTag, expr: onStr }] : []),
|
|
...(bytesTag >= 0 ? [{ tag: bytesTag, expr: onBytes }] : []),
|
|
];
|
|
if (present.length === 0) throw new InternalCompilerError(`emitter bug: ${fn} union lacks its arms`);
|
|
const tail = present[present.length - 1]!.expr;
|
|
const chain = present.slice(0, -1).map((a) => `${u}->tag == ${a.tag} ? ${a.expr}`);
|
|
return finish(chain.length > 0 ? `(${chain.join(" : ")} : ${tail})` : tail);
|
|
}
|
|
case "readable.pushDyn":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_stream_push_dyn((ScrStream *)${arg(0)}, ${arg(1)})`);
|
|
case "readable.unshift":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_stream_unshift((ScrStream *)${arg(0)}, ${arg(1)})`);
|
|
case "readable.unshiftStr":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_stream_unshift_str((ScrStream *)${arg(0)}, ${arg(1)})`);
|
|
case "readable.read": {
|
|
// +1 Buffer or NULL → the `Buffer | null` union, constructed
|
|
// type-directedly (the error.code pattern).
|
|
emitter.usesTimers = true;
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: readable.read result is not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes") : -1;
|
|
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
|
if (bytesTag < 0 || nullTag < 0) throw new InternalCompilerError("emitter bug: readable.read union lacks its arms");
|
|
const b = emitter.newTemp(bytesOf("u8"), `scr_stream_read((ScrStream *)${arg(0)}, ${arg(1)})`);
|
|
emitter.emitPendingCheck();
|
|
emitter.moveTemp(b); // moves into the union arm when present
|
|
const present = `scr_union_new_ref(${bytesTag}, ${b.name}, &scr_bytes_retain_v, &scr_bytes_release_v, NULL)`;
|
|
return emitter.newTemp(e.type, `${b.name} ? ${present} : scr_union_retain(${emitter.unitInstanceRef(e.type.unionId, nullTag)})`);
|
|
}
|
|
case "readable.pause":
|
|
return finish(`(${cType(e.type).trim()})scr_stream_pause((ScrStream *)${arg(0)})`);
|
|
case "readable.setEncoding":
|
|
emitter.usesTimers = true;
|
|
return finish(`(${cType(e.type).trim()})scr_stream_set_encoding((ScrStream *)${arg(0)}, ${arg(1)})`);
|
|
case "readable.nextChunk":
|
|
// +1 promise; may run the user _read synchronously (may-throw).
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_stream_next_chunk((ScrStream *)${arg(0)})`);
|
|
case "readable.nextChunkDyn":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_stream_next_chunk_dyn((ScrStream *)${arg(0)})`);
|
|
case "readable.fromArr":
|
|
emitter.usesTimers = true;
|
|
return finish(`(${cType(e.type).trim()})scr_stream_from_arr(${arg(0)}, ${arg(1)})`);
|
|
case "readable.resume":
|
|
emitter.usesTimers = true;
|
|
return finish(`(${cType(e.type).trim()})scr_stream_resume((ScrStream *)${arg(0)})`);
|
|
case "readable.isPaused":
|
|
return finish(`scr_stream_is_paused((ScrStream *)${arg(0)})`);
|
|
case "readable.pipe":
|
|
emitter.usesTimers = true;
|
|
return finish(`(${cType(e.type).trim()})scr_stream_pipe((ScrStream *)${arg(0)}, (ScrStream *)${arg(1)}, ${arg(2)})`);
|
|
case "readable.unpipe":
|
|
emitter.usesTimers = true;
|
|
return finish(
|
|
e.args.length > 1
|
|
? `(${cType(e.type).trim()})scr_stream_unpipe((ScrStream *)${arg(0)}, (ScrStream *)${arg(1)})`
|
|
: `(${cType(e.type).trim()})scr_stream_unpipe((ScrStream *)${arg(0)}, NULL)`,
|
|
);
|
|
case "readable.flowing": {
|
|
// -1 (null: never kicked) / 0 / 1 → the `boolean | null` union.
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: readable.flowing result is not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const boolTag = def ? def.arms.findIndex((a) => a.kind === "bool") : -1;
|
|
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
|
if (boolTag < 0 || nullTag < 0) throw new InternalCompilerError("emitter bug: readable.flowing union lacks its arms");
|
|
const f = emitter.newTemp({ kind: "f64" }, `scr_stream_flowing((ScrStream *)${arg(0)})`);
|
|
return emitter.newTemp(
|
|
e.type,
|
|
`${f.name} < 0 ? scr_union_retain(${emitter.unitInstanceRef(e.type.unionId, nullTag)}) : scr_union_new_bool(${boolTag}, ${f.name} > 0)`,
|
|
);
|
|
}
|
|
case "writable.writeDyn":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_stream_write_dyn((ScrStream *)${arg(0)}, ${arg(1)}, NULL)`);
|
|
case "writable.write":
|
|
case "writable.writeStr": {
|
|
emitter.usesTimers = true;
|
|
const entry = fn === "writable.write" ? "scr_stream_write" : "scr_stream_write_str";
|
|
if (e.args.length > 2) {
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb); // the user's completion callback MOVES
|
|
return finish(`${entry}((ScrStream *)${arg(0)}, ${arg(1)}, ${cb.name})`);
|
|
}
|
|
return finish(`${entry}((ScrStream *)${arg(0)}, ${arg(1)}, NULL)`);
|
|
}
|
|
case "writable.end": {
|
|
// (recv, flags[, chunk][, cb]) — flags: 1 bytes chunk, 2 string
|
|
// chunk, 4 callback.
|
|
emitter.usesTimers = true;
|
|
const flagsArg = e.args[1]!;
|
|
if (flagsArg.kind !== "numLit") throw new InternalCompilerError("emitter bug: writable.end flags not a literal");
|
|
const flags = flagsArg.value;
|
|
let at = 2;
|
|
let chunkB = "NULL";
|
|
let chunkS = "NULL";
|
|
let chunkD: string | null = null;
|
|
if (flags & 1) chunkB = arg(at++);
|
|
else if (flags & 2) chunkS = arg(at++);
|
|
else if (flags & 8) chunkD = arg(at++); // dyn chunk: write first
|
|
let cbName = "NULL";
|
|
if (flags & 4) {
|
|
const cb = args[at]!;
|
|
emitter.moveTemp(cb);
|
|
cbName = cb.name;
|
|
}
|
|
const endCall = `(${cType(e.type).trim()})scr_stream_end((ScrStream *)${arg(0)}, ${chunkB}, ${chunkS}, ${cbName})`;
|
|
if (chunkD !== null) {
|
|
// end(chunk) with a checked-dynamic chunk: write, then end
|
|
// (exactly Node's end(chunk) decomposition).
|
|
return finish(`(scr_stream_write_dyn((ScrStream *)${arg(0)}, ${chunkD}, NULL), ${endCall})`);
|
|
}
|
|
return finish(endCall);
|
|
}
|
|
case "writable.cork":
|
|
return finish(`scr_stream_cork((ScrStream *)${arg(0)})`);
|
|
case "writable.uncork":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_stream_uncork((ScrStream *)${arg(0)})`);
|
|
case "stream.destroy":
|
|
emitter.usesTimers = true;
|
|
return finish(`(${cType(e.type).trim()})scr_stream_destroy((ScrStream *)${arg(0)}, NULL)`);
|
|
case "stream.destroyErr":
|
|
emitter.usesTimers = true;
|
|
return finish(`(${cType(e.type).trim()})scr_stream_destroy((ScrStream *)${arg(0)}, ${arg(1)})`);
|
|
case "stream.iteratorClose":
|
|
emitter.usesTimers = true;
|
|
return finish(`(${cType(e.type).trim()})scr_stream_iterator_close((ScrStream *)${arg(0)})`);
|
|
case "stream.prop": {
|
|
// The property NAME is a compile-time literal; args[1]'s
|
|
// emitted temp is unused (released with the statement's frame).
|
|
const nameArg = e.args[1]!;
|
|
if (nameArg.kind !== "strLit") throw new InternalCompilerError("emitter bug: stream.prop name not a literal");
|
|
const c = `scr_stream_prop((ScrStream *)${arg(0)}, "${nameArg.value}")`;
|
|
return finish(e.type.kind === "bool" ? `(${c} != 0)` : c);
|
|
}
|
|
case "stream.errored": {
|
|
// +1 error or NULL → the `Error | null` union (error.code's
|
|
// construction pattern).
|
|
if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: stream.errored result is not a union");
|
|
const def = emitter.unionsById.get(e.type.unionId);
|
|
const errTag = def ? def.arms.findIndex((a) => a.kind === "object") : -1;
|
|
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
|
if (errTag < 0 || nullTag < 0) throw new InternalCompilerError("emitter bug: stream.errored union lacks its arms");
|
|
const er = emitter.newTemp({ kind: "object", className: "%Error" }, `scr_stream_errored((ScrStream *)${arg(0)})`);
|
|
emitter.moveTemp(er);
|
|
const present = `scr_union_new_ref(${errTag}, ${er.name}, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg())`;
|
|
return emitter.newTemp(e.type, `${er.name} ? ${present} : scr_union_retain(${emitter.unitInstanceRef(e.type.unionId, nullTag)})`);
|
|
}
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: stream libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitAssertInspectLibCall(state: LibCallState): Temp {
|
|
const { e, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
case "assert.expectsErrDyn":
|
|
return finish(`scr_assert_expects_err_dyn(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`);
|
|
// node:assert (scr_assert.c; match in scr_regex.c): all args
|
|
// borrowed; failures throw the catchable AssertionError (the
|
|
// may-throw seed runs the pending check after each).
|
|
case "assert.ok":
|
|
return finish(`scr_assert_ok(${arg(0)}, ${arg(1)})`);
|
|
case "assert.eqF64":
|
|
return finish(`scr_assert_eq_f64(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`);
|
|
case "assert.eqBigInt":
|
|
return finish(`scr_assert_eq_bigint(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`);
|
|
case "assert.eqStr":
|
|
return finish(`scr_assert_eq_str(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`);
|
|
case "assert.eqBool":
|
|
return finish(`scr_assert_eq_bool(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`);
|
|
case "assert.eqSym":
|
|
// Symbol strict equality: pointer identity, v24's
|
|
// "Symbol(desc)" stacked-diff messages (scr_symbol.c).
|
|
return finish(`scr_assert_eq_sym(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`);
|
|
case "assert.eqDyn":
|
|
// The quartet over checked-dynamic operands: SameValue /
|
|
// dyn-walk deep equality, assertion_error.js messages.
|
|
return finish(`scr_assert_eq_dyn(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`);
|
|
case "assert.looseResult":
|
|
return finish(`scr_assert_loose_result(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`);
|
|
case "assert.deepResult":
|
|
return finish(`scr_assert_deep_result(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`);
|
|
case "assert.deqEnter":
|
|
return finish(`scr_assert_deq_enter(${arg(0)}, ${arg(1)})`);
|
|
case "assert.deqLeave":
|
|
return finish(`scr_assert_deq_leave()`);
|
|
case "assert.sameValue":
|
|
return finish(`scr_assert_same_value_f64(${arg(0)}, ${arg(1)})`);
|
|
case "assert.match":
|
|
return finish(`scr_assert_match(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`);
|
|
case "assert.throwsNone":
|
|
return finish(`scr_assert_throws_none(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`);
|
|
case "assert.throwsMismatch":
|
|
return finish(`scr_assert_throws_mismatch(${arg(0)}, (ScrError *)${arg(1)}, ${arg(2)}, ${arg(3)})`);
|
|
case "assert.throwsRegex":
|
|
// assert.throws(fn, /re/): Node tests String(error)
|
|
// (scr_regex.c — the regex argument keeps the link switch on).
|
|
return finish(`scr_assert_throws_regex(${arg(0)}, (ScrError *)${arg(1)}, ${arg(2)}, ${arg(3)})`);
|
|
// The throws(fn, {shape}) accumulator: begin/slot calls never
|
|
// throw; shapeEnd throws the byte-exact Comparison diff on
|
|
// mismatch.
|
|
case "assert.shapeBegin":
|
|
return finish(`scr_assert_shape_begin((ScrError *)${arg(0)})`);
|
|
case "assert.shapeStr":
|
|
return finish(`scr_assert_shape_str(${arg(0)}, ${arg(1)})`);
|
|
case "assert.shapeRe":
|
|
return finish(`scr_assert_shape_re(${arg(0)}, ${arg(1)})`);
|
|
case "assert.shapeEnd":
|
|
return finish(`scr_assert_shape_end(${arg(0)}, ${arg(1)})`);
|
|
case "assert.regexErrTest":
|
|
return finish(`scr_assert_regex_err_test(${arg(0)}, (ScrError *)${arg(1)})`);
|
|
case "assert.unwantedRejection":
|
|
return finish(`scr_assert_unwanted_rejection((ScrError *)${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
// assert.ifError's per-type throws (always throw — Node rejects
|
|
// falsy values too; null/undefined never reach the runtime).
|
|
case "assert.ifErrorErr":
|
|
return finish(`scr_assert_iferror_err((ScrError *)${arg(0)})`);
|
|
case "assert.ifErrorF64":
|
|
return finish(`scr_assert_iferror_f64(${arg(0)})`);
|
|
case "assert.ifErrorStr":
|
|
return finish(`scr_assert_iferror_str(${arg(0)})`);
|
|
case "assert.ifErrorBool":
|
|
return finish(`scr_assert_iferror_bool(${arg(0)})`);
|
|
case "assert.ifErrorDyn":
|
|
// The checked-dynamic argument (test/common's mustSucceed):
|
|
// units pass quietly, %error-marked objects throw with the
|
|
// error's message, everything else with the inspection.
|
|
return finish(`scr_assert_iferror_dyn(${arg(0)})`);
|
|
case "assert.refEqBytes":
|
|
// strictEqual/notStrictEqual over bytes: pointer identity,
|
|
// Node's object-comparison headers (brands_eq is the static
|
|
// brand agreement — it shapes only the failure header).
|
|
return finish(`scr_assert_ref_eq_bytes(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`);
|
|
case "assert.refEqFn":
|
|
// strictEqual/notStrictEqual over functions: pointer identity,
|
|
// the object-comparison headers.
|
|
return finish(`scr_assert_ref_eq_fn(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`);
|
|
case "assert.bytesDeepEq":
|
|
// deepStrictEqual's verdict: static brand agreement AND
|
|
// content equality. Never throws.
|
|
return finish(`scr_assert_bytes_deep_eq(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
// util.inspect (scr_inspect.c): all args borrowed; string
|
|
// results +1. Only insp.jsval throws (composite island values —
|
|
// the may-throw seed runs the pending check).
|
|
case "insp.f64":
|
|
return finish(`scr_insp_f64(${arg(0)})`);
|
|
case "insp.jsonDyn":
|
|
return finish(`scr_dyn_format_j(${arg(0)})`);
|
|
case "insp.str":
|
|
return finish(`scr_insp_str(${arg(0)})`);
|
|
case "insp.regex":
|
|
return finish(`scr_insp_regex(${arg(0)})`);
|
|
case "insp.buffer":
|
|
return finish(`scr_insp_buffer(${arg(0)})`);
|
|
case "insp.error":
|
|
// The receiver may be a user subclass struct — its ScrError
|
|
// prefix carries the name/message/code slots (the error.code
|
|
// precedent).
|
|
return finish(`scr_insp_error((ScrError *)${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "insp.dyn":
|
|
return finish(`scr_insp_dyn(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "insp.dynS":
|
|
return finish(`scr_insp_dyn_s(${arg(0)}, ${arg(1)})`);
|
|
case "insp.jsval":
|
|
return finish(`scr_insp_jsval(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "insp.begin":
|
|
return finish(`scr_insp_begin(${arg(0)})`);
|
|
// Circular references over cycle-capable composites: check/push/
|
|
// wrap drive Node's seen/circular machinery (scr_inspect.c).
|
|
case "insp.circCheck":
|
|
return finish(`scr_insp_circ_check(${arg(0)})`);
|
|
case "insp.seenPush":
|
|
return finish(`scr_insp_seen_push(${arg(0)})`);
|
|
case "insp.refWrap":
|
|
return finish(`scr_insp_ref_wrap(${arg(0)}, ${arg(1)})`);
|
|
case "insp.circular":
|
|
return finish(`scr_insp_circular(${arg(0)})`);
|
|
case "insp.entry":
|
|
return finish(`scr_insp_entry(${arg(0)}, ${arg(1)})`);
|
|
case "insp.key":
|
|
return finish(`scr_insp_key(${arg(0)})`);
|
|
case "insp.moreItems":
|
|
return finish(`scr_insp_more_items(${arg(0)})`);
|
|
case "insp.end":
|
|
return finish(`scr_insp_end(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`);
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: assertInspect libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitIoLibCall(state: LibCallState): Temp {
|
|
const { e, emitter, args, arg, finish } = state;
|
|
const fn = e.fn;
|
|
switch (fn) {
|
|
// node:readline (scr_readline.c, the events gate): an OPEN
|
|
// interface is a stdin consumer — the loop must run.
|
|
case "rl.create":
|
|
emitter.usesTimers = true;
|
|
return finish(`scr_rl_create()`);
|
|
case "rl.question": {
|
|
emitter.usesTimers = true;
|
|
const cbT = e.args[2]!.type;
|
|
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: rl.question callback not a func");
|
|
const cb = args[2]!;
|
|
emitter.moveTemp(cb);
|
|
const adapter =
|
|
cbT.params.length === 0 ? "scr_rl_answer_thunk0" : "scr_rl_answer_thunk_str";
|
|
// Throws Node's use-after-close error; the pending check comes
|
|
// from the may-throw seed.
|
|
emitter.line(`scr_rl_question(${arg(0)}, ${arg(1)}, ${cb.name}, &${adapter});${emitter.srcComment(e.loc)}`);
|
|
emitter.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
case "rl.close":
|
|
return finish(`scr_rl_close(${arg(0)})`);
|
|
case "rl.onClose": {
|
|
emitter.usesTimers = true;
|
|
const cb = args[1]!;
|
|
emitter.moveTemp(cb);
|
|
emitter.line(`scr_rl_on_close(${arg(0)}, ${cb.name});${emitter.srcComment(e.loc)}`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
// The StringDecoder trio (scr_bytes.c): pure functions over the
|
|
// canonical encoding name + packed-f64 pending state; never
|
|
// throw.
|
|
case "strdec.write":
|
|
return finish(`scr_strdec_write(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "strdec.next":
|
|
return finish(`scr_strdec_next(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
|
case "strdec.end":
|
|
return finish(`scr_strdec_end(${arg(0)}, ${arg(1)})`);
|
|
default:
|
|
throw new InternalCompilerError(`emitter bug: io libCall dispatch for ${fn}`);
|
|
}
|
|
}
|
|
|
|
function emitLibCallExpr(emitter: CEmitter, e: LibCallExpr): Temp {
|
|
const packed = e.args[0];
|
|
if (e.fn === "string.fromCharCode" && packed?.kind === "arrayLit" &&
|
|
packed.elems.length === 1 && !packed.spreads?.length) {
|
|
// The literal is only the variadic ABI pack, never an observable
|
|
// array. Evaluate the scalar once before bypassing that allocation.
|
|
const code = emitter.emitExpr(packed.elems[0]!);
|
|
return emitter.newTemp(e.type, `scr_str_from_char_code_one(${code.name})`);
|
|
}
|
|
// Standard-library call. Args are BORROWED (owned temps of the
|
|
// current frame, released at statement end); refcounted results come
|
|
// back +1 (process.argv: +1 on the runtime's ONE interned array —
|
|
// JS identity — everything else fresh). Throwing members (the
|
|
// may-throw seed set) get the standard pending check, emitted after
|
|
// a result temp joins its frame so an unwind releases it.
|
|
const args = e.args.map((a) => emitter.emitExpr(a));
|
|
const arg = (i: number) => args[i]!.name;
|
|
const finish = (call: string): Temp => {
|
|
if (e.type.kind === "void") {
|
|
emitter.line(`${call};${emitter.srcComment(e.loc)}`);
|
|
if (MAY_THROW_LIB_FNS.has(e.fn)) emitter.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
const t = emitter.newTemp(e.type, call);
|
|
if (MAY_THROW_LIB_FNS.has(e.fn)) emitter.emitPendingCheck();
|
|
return t;
|
|
};
|
|
const fn = e.fn;
|
|
const state: LibCallState = { emitter, e, args, arg, finish };
|
|
const prefix = fn.slice(0, fn.indexOf(".")) as LibCallPrefix;
|
|
switch (prefix) {
|
|
case "fetch":
|
|
case "island":
|
|
case "json":
|
|
return emitWebLibCall(state);
|
|
case "arrayBuffer":
|
|
case "dyn":
|
|
case "global":
|
|
return emitDynamicLibCall(state);
|
|
case "fs":
|
|
case "fsp":
|
|
case "fileHandle":
|
|
case "watcher":
|
|
case "stats":
|
|
case "zlib":
|
|
case "atomics":
|
|
return emitFilesystemLibCall(state);
|
|
case "path":
|
|
case "os":
|
|
case "url":
|
|
case "sp":
|
|
case "qs":
|
|
return emitPathUrlLibCall(state);
|
|
case "math":
|
|
case "num":
|
|
case "str":
|
|
case "regexp":
|
|
case "intl":
|
|
case "sym":
|
|
case "bigint":
|
|
case "perf":
|
|
case "number":
|
|
case "date":
|
|
case "text":
|
|
case "string":
|
|
case "class":
|
|
return emitPrimitiveLibCall(state);
|
|
case "util":
|
|
return emitUtilLibCall(state);
|
|
case "crypto":
|
|
case "buffer":
|
|
case "bytes":
|
|
return emitCryptoBytesLibCall(state);
|
|
case "cp":
|
|
case "spawnRes":
|
|
case "child":
|
|
case "writer":
|
|
case "procStream":
|
|
return emitChildProcessLibCall(state);
|
|
case "net":
|
|
case "dgram":
|
|
case "dns":
|
|
return emitNetworkLibCall(state);
|
|
case "test":
|
|
return emitTestLibCall(state);
|
|
case "http":
|
|
case "https":
|
|
return emitHttpLibCall(state);
|
|
case "tls":
|
|
case "tlsca":
|
|
return emitTlsLibCall(state);
|
|
case "http2":
|
|
return emitHttp2LibCall(state);
|
|
case "tp":
|
|
case "dc":
|
|
case "timers":
|
|
case "async":
|
|
case "als":
|
|
return emitAsyncContextLibCall(state);
|
|
case "process":
|
|
case "stdin":
|
|
case "module":
|
|
return emitProcessLibCall(state);
|
|
case "error":
|
|
case "regex":
|
|
case "emitter":
|
|
return emitErrorsEventsLibCall(state);
|
|
case "stream":
|
|
case "readable":
|
|
case "writable":
|
|
case "duplex":
|
|
case "transform":
|
|
case "passthrough":
|
|
case "sc":
|
|
return emitStreamLibCall(state);
|
|
case "assert":
|
|
case "insp":
|
|
return emitAssertInspectLibCall(state);
|
|
case "rl":
|
|
case "strdec":
|
|
return emitIoLibCall(state);
|
|
default: {
|
|
const _exhaustive: never = prefix;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
export function emitExpr(emitter: CEmitter, e: IrExpr): Temp {
|
|
switch (e.kind) {
|
|
case "numLit":
|
|
case "boolLit":
|
|
case "strLit":
|
|
case "moduleNsRef":
|
|
case "unitLit":
|
|
case "varRef":
|
|
return emitLiteralExpr(emitter, e);
|
|
case "bin":
|
|
case "unary":
|
|
case "incDec":
|
|
case "fieldIncDec":
|
|
case "assignExpr":
|
|
case "seqExpr":
|
|
return emitOperatorExpr(emitter, e);
|
|
case "dynDestrCheck":
|
|
case "dynIterN":
|
|
case "toBool":
|
|
case "logical":
|
|
case "ternary":
|
|
case "optChain":
|
|
case "chainRecv":
|
|
case "orDefault":
|
|
case "nullish":
|
|
return emitControlExpr(emitter, e);
|
|
case "strConcat":
|
|
case "strEq":
|
|
case "strCmp":
|
|
case "toString":
|
|
case "strIntrinsic":
|
|
case "regexLit":
|
|
case "templateStrings":
|
|
case "regexIntrinsic":
|
|
return emitStringExpr(emitter, e);
|
|
case "arrayLit":
|
|
case "arrayNewLen":
|
|
case "arrayGet":
|
|
case "arrayHas":
|
|
case "arrayState":
|
|
case "arrIntrinsic":
|
|
case "bytesNew":
|
|
case "bytesIntrinsic":
|
|
case "mapNew":
|
|
case "mapIntrinsic":
|
|
case "setIntrinsic":
|
|
case "setNew":
|
|
return emitContainerExpr(emitter, e);
|
|
case "call":
|
|
case "ffiCall":
|
|
case "closure":
|
|
case "callValue":
|
|
case "selfRef":
|
|
case "new":
|
|
case "classRef":
|
|
case "newValue":
|
|
case "instanceOfValue":
|
|
case "promiseVoidWiden":
|
|
case "upcast":
|
|
case "downcast":
|
|
case "instanceOf":
|
|
case "virtualCall":
|
|
return emitCallExpr(emitter, e);
|
|
case "fieldGet":
|
|
case "recordGet":
|
|
case "recordLit":
|
|
case "recordClone":
|
|
case "recordKeyGet":
|
|
case "recordOvfKeys":
|
|
case "recordOvfHas":
|
|
return emitRecordExpr(emitter, e);
|
|
case "dynFrom":
|
|
case "dynFromJsval":
|
|
case "dynCall":
|
|
case "dynInvoke":
|
|
case "dynArrLit":
|
|
case "dynObjLit":
|
|
case "unionWrap":
|
|
case "unionNarrow":
|
|
case "unionDisc":
|
|
case "unionKeyGet":
|
|
case "unionIsTag":
|
|
case "dynKeyGet":
|
|
case "dynHasKey":
|
|
case "dynScalarEq":
|
|
case "dynTest":
|
|
case "unionEq":
|
|
case "unionFuncEq":
|
|
case "caughtTest":
|
|
case "caughtCheck":
|
|
case "caughtNarrow":
|
|
case "caughtToDyn":
|
|
return emitDynamicExpr(emitter, e);
|
|
case "intrinsic":
|
|
return emitIntrinsicExpr(emitter, e);
|
|
case "jsonStringify":
|
|
case "dynCheck":
|
|
return emitSerializationExpr(emitter, e);
|
|
case "yieldExpr":
|
|
case "genResume":
|
|
case "awaitExpr":
|
|
case "awaitUnionExpr":
|
|
case "newPromise":
|
|
case "promiseWithResolvers":
|
|
return emitAsyncExpr(emitter, e);
|
|
case "jsMarshal":
|
|
case "jsOp":
|
|
case "jsExit":
|
|
case "jsBridgePromise":
|
|
return emitJsInteropExpr(emitter, e);
|
|
case "libCall":
|
|
return emitLibCallExpr(emitter, e);
|
|
default: {
|
|
const _exhaustive: never = e;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|