mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
1351 lines
57 KiB
TypeScript
1351 lines
57 KiB
TypeScript
import { arrayClassDescriptor } from "./classes.js";
|
|
import { emitBigIntWrap } from "./bigint-wrap.js";
|
|
import { emitBorrowedInput, emitBorrowedInputs } from "./borrowed-inputs.js";
|
|
import { borrowsIteratorInputs, borrowsJsonInputs } from "./checked-value-lifetimes.js";
|
|
import { emitTlsLibCall } from "./lib-tls.js";
|
|
import { emitHttp2LibCall } from "./lib-http2.js";
|
|
import { emitDatagramLibCall } from "./lib-datagram.js";
|
|
import { emitTestLibCall } from "./lib-test.js";
|
|
import { FN_ATTRS, arrNewCall, traceArg, vAdapters } from "./shapes.js";
|
|
/* Focused LLVM library-call emission extracted from emitter.ts. */
|
|
import { InternalCompilerError } from "../../errors.js";
|
|
|
|
import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
|
|
import { BYTES_ELEMENT_SIZE, F64, type IrExpr, type IrType } from "../../ir/ir.js";
|
|
import { LlvmUnsupportedError } from "./unsupported.js";
|
|
import { emitJsLibCall } from "./jsval.js";
|
|
import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js";
|
|
import {
|
|
ASSERT_VERDICT_LIB_FNS,
|
|
LIB_FN_SYMS,
|
|
PURE_MATH_LIB_FNS,
|
|
mockTimerSym,
|
|
USES_TIMERS_LIB_FNS,
|
|
} from "./lib-shared.js";
|
|
import { isDynUndefined } from "../../ir/runtime-features.js";
|
|
import { DYN_KIND } from "./dyn.js";
|
|
import { caBytes } from "./lib-abi.js";
|
|
import { emitInlineAtomicsOp } from "./expr-bytes.js";
|
|
import { regexLiteralCaptures } from "./regex-matchers.js";
|
|
import { registerReplay } from "./emit-journal.js";
|
|
|
|
/** ScrDyn.v.typed_ref.type_key_len: the low 27 bits of its bit-field word. */
|
|
const TYPE_KEY_LEN_MASK = (1 << 27) - 1;
|
|
|
|
export function emitAssertInspectLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
|
const B = host.B;
|
|
if (e.fn === "assert.shapeStr" || e.fn === "assert.shapeRe") {
|
|
// The throws(fn, {shape}) accumulator's slot writers: the key is a
|
|
// C int (the generic path would pass a double through the ABI —
|
|
// fptosi here, exactly the C prototype's implicit conversion).
|
|
// Never throw.
|
|
const key = host.emitExpr(e.args[0]!);
|
|
const v = host.emitExpr(e.args[1]!);
|
|
const sym = e.fn === "assert.shapeStr" ? "scr_assert_shape_str" : "scr_assert_shape_re";
|
|
host.declare(`declare void @${sym}(i32, ptr)`);
|
|
const k32 = B.tmp();
|
|
B.line(`${k32} = fptosi double ${key.name} to i32`);
|
|
B.line(`call void @${sym}(i32 ${k32}, ptr ${v.name})`);
|
|
return { name: "", type: e.type };
|
|
}
|
|
return host.emitGenericLibCall(e);
|
|
}
|
|
|
|
export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
|
const B = host.B;
|
|
if (e.fn === "rl.create" || e.fn === "rl.createSignal") {
|
|
// readline interface handles are runtime IDs (doubles); an open
|
|
// interface on stdin holds the loop. The mode literal says which
|
|
// optional arguments follow (lowerReadlineCreateCall): a stream input
|
|
// or output goes through scr_stream.c's bridge.
|
|
host.usesTimers = true;
|
|
const modeArg = e.args[0]!;
|
|
if (modeArg.kind !== "numLit") {
|
|
throw new InternalCompilerError("llvm emitter bug: rl.create mode is not a literal");
|
|
}
|
|
const mode = modeArg.value;
|
|
let i = 1;
|
|
const input = mode & 1 ? host.emitExpr(e.args[i++]!).name : "null";
|
|
const output = mode & 2 ? host.emitExpr(e.args[i++]!).name : "null";
|
|
const crlf = host.emitExpr(e.args[i++]!).name;
|
|
const prompt = mode & 16 ? host.emitExpr(e.args[i++]!).name : "null";
|
|
const signal = mode & 8 ? host.emitExpr(e.args[i++]!).name : "null";
|
|
let add = "null";
|
|
if (mode & 8) {
|
|
host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`);
|
|
add = "@scr_events_add_abort_listener";
|
|
}
|
|
const stdout = mode & 4 ? "true" : "false";
|
|
const t = B.tmp();
|
|
if (mode & 3) {
|
|
host.declare(`declare double @scr_stream_rl_open(ptr, ptr, i1, double, ptr, ptr, ptr)`);
|
|
B.line(
|
|
`${t} = call double @scr_stream_rl_open(ptr ${input}, ptr ${output}, i1 ${stdout}, double ${crlf}, ptr ${prompt}, ptr ${signal}, ptr ${add})`,
|
|
);
|
|
} else {
|
|
host.declare(`declare double @scr_rl_open(ptr, ptr, ptr, i1, double, ptr, ptr, ptr)`);
|
|
B.line(
|
|
`${t} = call double @scr_rl_open(ptr null, ptr null, ptr null, i1 ${stdout}, double ${crlf}, ptr ${prompt}, ptr ${signal}, ptr ${add})`,
|
|
);
|
|
}
|
|
// A signal that is not an AbortSignal throws ERR_INVALID_ARG_TYPE.
|
|
if (mode & 8) host.emitPendingCheck();
|
|
// readline/promises interfaces are instances of that module's class.
|
|
if (mode & 32) {
|
|
host.declare(`declare void @scr_rl_mark_promises(double)`);
|
|
B.line(`call void @scr_rl_mark_promises(double ${t})`);
|
|
}
|
|
return { name: t, type: e.type };
|
|
}
|
|
if (e.fn === "rl.question" || e.fn === "rl.questionDyn") {
|
|
// The answer callback MOVES into the interface's registry; throws
|
|
// Node's use-after-close error (the may-throw seed), and with a
|
|
// checked-dynamic query that is not a string, ERR_INVALID_ARG_TYPE.
|
|
host.usesTimers = true;
|
|
const fn = e.fn === "rl.question" ? "scr_rl_question" : "scr_rl_question_dyn";
|
|
const cbT = e.args[2]!.type;
|
|
if (cbT.kind !== "func")
|
|
throw new InternalCompilerError("llvm emitter bug: rl.question callback not a func");
|
|
const args = e.args.map((a) => host.emitExpr(a));
|
|
host.toHeap(args[2]!);
|
|
const adapter =
|
|
cbT.params.length === 0
|
|
? "scr_rl_answer_thunk0"
|
|
: cbT.params[0]!.kind === "dyn"
|
|
? "scr_rl_answer_thunk_dyn"
|
|
: "scr_rl_answer_thunk_str";
|
|
host.declare(`declare void @${adapter}(ptr, ptr)`);
|
|
host.declare(`declare void @${fn}(double, ptr, ptr, ptr)`);
|
|
B.line(
|
|
`call void @${fn}(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name}, ptr @${adapter})`,
|
|
);
|
|
host.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
if (
|
|
e.fn === "rl.questionAsync" ||
|
|
e.fn === "rl.questionAsyncSignal" ||
|
|
e.fn === "rl.questionAsyncDyn" ||
|
|
e.fn === "rl.questionAsyncSignalDyn"
|
|
) {
|
|
// The promise question never throws: closed interfaces, aborted
|
|
// signals and queries that are not strings reject the answered
|
|
// promise (+1).
|
|
host.usesTimers = true;
|
|
const args = e.args.map((a) => host.emitExpr(a));
|
|
const dyn = e.fn === "rl.questionAsyncDyn" || e.fn === "rl.questionAsyncSignalDyn";
|
|
const fn = dyn ? "scr_rl_question_promise_dyn" : "scr_rl_question_promise";
|
|
let signal = "null";
|
|
let add = "null";
|
|
if (e.fn === "rl.questionAsyncSignal" || e.fn === "rl.questionAsyncSignalDyn") {
|
|
signal = args[2]!.name;
|
|
host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`);
|
|
add = "@scr_events_add_abort_listener";
|
|
}
|
|
host.declare(`declare ptr @${fn}(double, ptr, ptr, ptr)`);
|
|
const t = B.tmp();
|
|
B.line(
|
|
`${t} = call ptr @${fn}(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${signal}, ptr ${add})`,
|
|
);
|
|
return host.own({ name: t, type: e.type });
|
|
}
|
|
if (e.fn === "rl.keypressStdin") {
|
|
// emitKeypressEvents(process.stdin): no stream side
|
|
host.usesTimers = true;
|
|
const args = e.args.map((a) => host.emitExpr(a));
|
|
host.declare(`declare void @scr_rl_keypress_events(ptr, ptr, double, double)`);
|
|
B.line(
|
|
`call void @scr_rl_keypress_events(ptr null, ptr null, double ${args[0]!.name}, double ${args[1]!.name})`,
|
|
);
|
|
return { name: "", type: e.type };
|
|
}
|
|
if (e.fn === "rl.terminal") {
|
|
// A completer's errors print through the inspect unit, which links
|
|
// only when there is a completer: its inspect function rides along.
|
|
const args = e.args.map((a) => host.emitExpr(a));
|
|
let inspect = "null";
|
|
if (!isDynUndefined(e.args[3]!)) {
|
|
host.declare(`declare ptr @scr_insp_dyn(ptr, double, double)`);
|
|
inspect = "@scr_insp_dyn";
|
|
}
|
|
host.declare(`declare void @scr_rl_terminal(double, double, i1 zeroext, ptr, i1 zeroext, ptr)`);
|
|
B.line(
|
|
`call void @scr_rl_terminal(double ${args[0]!.name}, double ${args[1]!.name}, i1 zeroext ${args[2]!.name}, ptr ${args[3]!.name}, i1 zeroext ${args[4]!.name}, ptr ${inspect})`,
|
|
);
|
|
return { name: "", type: e.type };
|
|
}
|
|
if (e.fn === "rl.on") {
|
|
// The listener MOVES into the interface's registry; the adapter is
|
|
// picked by event and parameter count ('line': the text, 'error': the
|
|
// error, 'history': the history array, the rest none).
|
|
host.usesTimers = true;
|
|
const cbT = e.args[2]!.type;
|
|
const evArg = e.args[1]!;
|
|
if (cbT.kind !== "func" || evArg.kind !== "numLit") {
|
|
throw new InternalCompilerError("llvm emitter bug: rl.on operands");
|
|
}
|
|
const id = host.emitExpr(e.args[0]!);
|
|
const ev = host.emitExpr(evArg);
|
|
const cb = host.emitExpr(e.args[2]!);
|
|
const once = host.emitExpr(e.args[3]!);
|
|
host.toHeap(cb);
|
|
let adapter = "null";
|
|
if (evArg.value === 0 || evArg.value === 4) {
|
|
const param = cbT.params[0];
|
|
const kind =
|
|
param === undefined ? "0" : param.kind === "dyn" ? "_dyn" : evArg.value === 0 ? "_str" : "";
|
|
const name = evArg.value === 0 ? `scr_rl_answer_thunk${kind}` : `scr_rl_error_thunk${kind}`;
|
|
host.declare(`declare void @${name}(ptr, ptr)`);
|
|
adapter = `@${name}`;
|
|
} else if (evArg.value === 5 && cbT.params.length === 1) {
|
|
const name =
|
|
cbT.params[0]!.kind === "dyn" ? "scr_rl_history_thunk_dyn" : "scr_rl_history_thunk_arr";
|
|
host.declare(`declare void @${name}(ptr, ptr)`);
|
|
adapter = `@${name}`;
|
|
}
|
|
host.declare(`declare void @scr_rl_on(double, double, ptr, ptr, i1)`);
|
|
B.line(
|
|
`call void @scr_rl_on(double ${id.name}, double ${ev.name}, ptr ${cb.name}, ptr ${adapter}, i1 ${once.name})`,
|
|
);
|
|
return { name: "", type: e.type };
|
|
}
|
|
return host.emitGenericLibCall(e);
|
|
}
|
|
|
|
export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
|
const B = host.B;
|
|
if (host.coroAwait && e.fn === "async.hop") {
|
|
host.emitCoroAwait(null);
|
|
return { name: "", type: e.type };
|
|
}
|
|
if (host.coroAwait && e.fn === "async.awaitDyn") {
|
|
const value = host.emitExpr(e.args[0]!);
|
|
host.declare(`declare ptr @scr_dyn_promise_of(ptr)`);
|
|
const promise = B.tmp();
|
|
const isPromise = B.tmp();
|
|
const wait = B.newLabel("await.dyn.promise");
|
|
const hop = B.newLabel("await.dyn.value");
|
|
const ready = B.newLabel("await.dyn.ready");
|
|
B.line(`${promise} = call ptr @scr_dyn_promise_of(ptr ${value.name})`);
|
|
B.line(`${isPromise} = icmp ne ptr ${promise}, null`);
|
|
B.condBr(isPromise, wait, hop);
|
|
B.startBlock(wait);
|
|
host.emitCoroAwait(promise);
|
|
B.br(ready);
|
|
B.startBlock(hop);
|
|
host.emitCoroAwait(null);
|
|
B.br(ready);
|
|
B.startBlock(ready);
|
|
const extract = host.wasi ? "scr_await_dyn_value_settled" : "scr_coro_await_dyn_value";
|
|
host.declare(`declare ptr @${extract}(ptr)`);
|
|
const result = B.tmp();
|
|
B.line(`${result} = call ptr @${extract}(ptr ${value.name})`);
|
|
const output = host.own({ name: result, type: e.type });
|
|
host.emitPendingCheck();
|
|
return output;
|
|
}
|
|
if (e.fn === "promise.allTuple") return emitPromiseAllTuple(host, e);
|
|
if (e.fn === "promise.fulfilledValue") {
|
|
// The payload of a fulfilled promise (borrowed operand): the scalar
|
|
// kinds that lower to a double (numbers, Dates, process streams) and
|
|
// booleans by value, strings and references +1.
|
|
const promise = host.emitExpr(e.args[0]!);
|
|
const out = B.tmp();
|
|
const ty = host.llType(e.type);
|
|
if (ty === "double") {
|
|
host.declare(`declare double @scr_promise_payload_f64(ptr)`);
|
|
B.line(`${out} = call double @scr_promise_payload_f64(ptr ${promise.name})`);
|
|
return { name: out, type: e.type };
|
|
}
|
|
if (ty === "i1") {
|
|
host.declare(`declare zeroext i1 @scr_promise_payload_bool(ptr)`);
|
|
B.line(`${out} = call zeroext i1 @scr_promise_payload_bool(ptr ${promise.name})`);
|
|
return { name: out, type: e.type };
|
|
}
|
|
if (e.type.kind === "string") {
|
|
host.declare(`declare ptr @scr_promise_payload_str(ptr)`);
|
|
B.line(`${out} = call ptr @scr_promise_payload_str(ptr ${promise.name}) ; +1`);
|
|
return host.own({ name: out, type: e.type });
|
|
}
|
|
host.declare(`declare ptr @scr_promise_payload_ref(ptr)`);
|
|
B.line(`${out} = call ptr @scr_promise_payload_ref(ptr ${promise.name}) ; +1`);
|
|
return host.own({ name: out, type: e.type });
|
|
}
|
|
if (e.fn === "record.sourceSpread") {
|
|
const built = host.emitReadReceiver(e.args[0]!);
|
|
const value = host.emitReadReceiver(e.args[1]!);
|
|
const type = e.args[0]!.type;
|
|
if (type.kind !== "record")
|
|
throw new InternalCompilerError("llvm emitter bug: record.sourceSpread of a non-record");
|
|
host.dyn.emitRecordSourceSpread(B, built.name, value.name, type.shapeId);
|
|
return { name: "", type: e.type };
|
|
}
|
|
if (e.fn === "array.speciesCreate") {
|
|
// The receiver is borrowed; the call answers a new reference.
|
|
const receiver = emitBorrowedInput(host, e.args[0]!);
|
|
const length = host.emitExpr(e.args[1]!);
|
|
host.declare(`declare ptr @scr_arr_species_create(ptr, double)`);
|
|
const out = B.tmp();
|
|
B.line(
|
|
`${out} = call ptr @scr_arr_species_create(ptr ${receiver.name}, double ${length.name})`,
|
|
);
|
|
const value = host.own({ name: out, type: e.type });
|
|
host.emitPendingCheck();
|
|
return value;
|
|
}
|
|
if (e.fn === "array.speciesFinish") {
|
|
// The instance is borrowed; the method's result moves into the call,
|
|
// which answers it or the instance that holds its elements.
|
|
const instance = emitBorrowedInput(host, e.args[0]!);
|
|
const result = host.emitExpr(e.args[1]!);
|
|
host.toHeap(result);
|
|
const setLength = host.emitExpr(e.args[2]!);
|
|
host.declare(`declare ptr @scr_arr_species_finish(ptr, ptr, i1 zeroext)`);
|
|
const out = B.tmp();
|
|
B.line(
|
|
`${out} = call ptr @scr_arr_species_finish(ptr ${instance.name}, ptr ${result.name}, i1 zeroext ${setLength.name})`,
|
|
);
|
|
const value = host.own({ name: out, type: e.type });
|
|
host.emitPendingCheck();
|
|
return value;
|
|
}
|
|
if (e.fn === "array.speciesAdopt") {
|
|
// The receiver is borrowed; the method's result moves into the call,
|
|
// which answers it or the species instance that replaces it.
|
|
const receiver = emitBorrowedInput(host, e.args[0]!);
|
|
const result = host.emitExpr(e.args[1]!);
|
|
host.toHeap(result);
|
|
const length = host.emitExpr(e.args[2]!);
|
|
const setLength = host.emitExpr(e.args[3]!);
|
|
host.declare(`declare ptr @scr_arr_species_adopt(ptr, ptr, double, i1 zeroext)`);
|
|
const out = B.tmp();
|
|
B.line(
|
|
`${out} = call ptr @scr_arr_species_adopt(ptr ${receiver.name}, ptr ${result.name}, double ${length.name}, i1 zeroext ${setLength.name})`,
|
|
);
|
|
const value = host.own({ name: out, type: e.type });
|
|
host.emitPendingCheck();
|
|
return value;
|
|
}
|
|
if (e.fn === "array.dynIsClass") {
|
|
const className = e.args[1]!.kind === "strLit" ? e.args[1]!.value : null;
|
|
if (className === null)
|
|
throw new InternalCompilerError("llvm emitter bug: array.dynIsClass needs a class name");
|
|
const value = emitBorrowedInput(host, e.args[0]!);
|
|
host.declare(`declare zeroext i1 @scr_dyn_is_array_class(ptr, ptr)`);
|
|
const out = B.tmp();
|
|
B.line(
|
|
`${out} = call zeroext i1 @scr_dyn_is_array_class(ptr ${value.name}, ptr ${arrayClassDescriptor(className)})`,
|
|
);
|
|
return { name: out, type: e.type };
|
|
}
|
|
if (e.fn === "array.classConstructAdopt") {
|
|
const className = e.args[0]!.kind === "strLit" ? e.args[0]!.value : null;
|
|
if (className === null)
|
|
throw new InternalCompilerError(
|
|
"llvm emitter bug: array.classConstructAdopt needs a class name",
|
|
);
|
|
const result = host.emitExpr(e.args[1]!);
|
|
host.toHeap(result);
|
|
const length = host.emitExpr(e.args[2]!);
|
|
host.declare(`declare ptr @scr_arr_class_construct_adopt(ptr, ptr, double)`);
|
|
const out = B.tmp();
|
|
B.line(
|
|
`${out} = call ptr @scr_arr_class_construct_adopt(ptr ${arrayClassDescriptor(className)}, ptr ${result.name}, double ${length.name})`,
|
|
);
|
|
const value = host.own({ name: out, type: e.type });
|
|
host.emitPendingCheck();
|
|
return value;
|
|
}
|
|
if (e.fn === "dyn.typedRefIs" || e.fn === "dyn.typedRefKeyIs") {
|
|
const literal = e.args[1]!.kind === "strLit" ? e.args[1]!.value : null;
|
|
if (e.fn === "dyn.typedRefKeyIs" && literal === null)
|
|
throw new InternalCompilerError("llvm emitter bug: dyn.typedRefKeyIs needs a literal key");
|
|
// A literal brand cannot replace the receiver while being evaluated.
|
|
// Generated class dispatch uses these probes repeatedly on one local.
|
|
const value = literal !== null ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!);
|
|
const key = e.fn === "dyn.typedRefIs" ? host.emitExpr(e.args[1]!) : null;
|
|
// Expose the capsule tag to LLVM so class dispatch can skip the whole
|
|
// chain of brand comparisons for ordinary checked-dynamic values.
|
|
// Keep the runtime's null handling and evaluate both operands once.
|
|
const slot = B.slot();
|
|
B.entryAllocas.push(`${slot} = alloca i1`);
|
|
B.line(`store i1 false, ptr ${slot}`);
|
|
const present = B.tmp();
|
|
const inspect = B.newLabel("brand.inspect");
|
|
const capsule = B.newLabel("brand.capsule");
|
|
const done = B.newLabel("brand.done");
|
|
B.line(`${present} = icmp ne ptr ${value.name}, null`);
|
|
B.condBr(present, inspect, done);
|
|
B.startBlock(inspect);
|
|
const kind = host.dynKind(value.name);
|
|
const typed = B.tmp();
|
|
B.line(`${typed} = icmp eq i32 ${kind}, ${DYN_KIND.TYPED_REF}`);
|
|
B.condBr(typed, capsule, done);
|
|
B.startBlock(capsule);
|
|
// Capsules boxed by this program share its key constant, so a pointer
|
|
// match answers without comparing names.
|
|
if (literal !== null) {
|
|
const keySlot = B.tmp(),
|
|
keyPtr = B.tmp(),
|
|
same = B.tmp();
|
|
B.line(
|
|
`${keySlot} = getelementptr inbounds i8, ptr ${value.name}, i64 ${host.abiOffset(40, 28)} ; ->v.typed_ref.type_key`,
|
|
);
|
|
B.line(`${keyPtr} = load ptr, ptr ${keySlot}`);
|
|
B.line(`${same} = icmp eq ptr ${keyPtr}, ${host.cstr(literal)}`);
|
|
if (e.fn === "dyn.typedRefKeyIs") {
|
|
B.line(`store i1 ${same}, ptr ${slot}`);
|
|
B.br(done);
|
|
} else {
|
|
const compare = B.newLabel("brand.compare");
|
|
B.line(`store i1 true, ptr ${slot}`);
|
|
B.condBr(same, done, compare);
|
|
B.startBlock(compare);
|
|
// A capsule with another key pointer usually belongs to another
|
|
// class: reject it on its key length and last eight bytes before
|
|
// the out-of-line name comparison, so a class table's chain of
|
|
// probes costs a few instructions per class it skips.
|
|
B.line(`store i1 false, ptr ${slot}`);
|
|
const bytes = Buffer.from(literal, "utf8");
|
|
const lenSlot = B.tmp(),
|
|
lenWord = B.tmp(),
|
|
len = B.tmp(),
|
|
lenEq = B.tmp();
|
|
B.line(
|
|
`${lenSlot} = getelementptr inbounds i8, ptr ${value.name}, i64 ${host.abiOffset(88, 52)} ; ->v.typed_ref.type_key_len`,
|
|
);
|
|
B.line(`${lenWord} = load i32, ptr ${lenSlot}`);
|
|
B.line(`${len} = and i32 ${lenWord}, ${TYPE_KEY_LEN_MASK}`);
|
|
B.line(`${lenEq} = icmp eq i32 ${len}, ${bytes.length}`);
|
|
const full = B.newLabel("brand.full");
|
|
if (bytes.length >= 8) {
|
|
const tail = B.newLabel("brand.tail");
|
|
B.condBr(lenEq, tail, done);
|
|
B.startBlock(tail);
|
|
const tailPtr = B.tmp(),
|
|
word = B.tmp(),
|
|
wordEq = B.tmp();
|
|
B.line(`${tailPtr} = getelementptr inbounds i8, ptr ${keyPtr}, i64 ${bytes.length - 8}`);
|
|
B.line(`${word} = load i64, ptr ${tailPtr}, align 1`);
|
|
B.line(`${wordEq} = icmp eq i64 ${word}, ${bytes.readBigInt64LE(bytes.length - 8)}`);
|
|
B.condBr(wordEq, full, done);
|
|
} else {
|
|
B.condBr(lenEq, full, done);
|
|
}
|
|
B.startBlock(full);
|
|
}
|
|
}
|
|
if (e.fn === "dyn.typedRefIs") {
|
|
host.declare("declare zeroext i1 @scr_dyn_typed_ref_is_key(ptr, ptr) memory(read)");
|
|
const matches = B.tmp();
|
|
B.line(
|
|
`${matches} = call zeroext i1 @scr_dyn_typed_ref_is_key(ptr ${value.name}, ptr ${key!.name})`,
|
|
);
|
|
B.line(`store i1 ${matches}, ptr ${slot}`);
|
|
B.br(done);
|
|
}
|
|
B.startBlock(done);
|
|
const result = B.tmp();
|
|
B.line(`${result} = load i1, ptr ${slot}`);
|
|
return { name: result, type: e.type };
|
|
}
|
|
if (e.fn === "dyn.classIs") {
|
|
// Only a class constructor's function value can match. Expose the
|
|
// function tag to LLVM so a chain of constructor probes (a static
|
|
// property bag's dispatch) costs one test for every other value, and
|
|
// borrow the receiver across a literal key as the capsule probes do.
|
|
const literal = e.args[1]!.kind === "strLit";
|
|
const value = literal ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!);
|
|
const key = host.emitExpr(e.args[1]!);
|
|
const slot = B.slot();
|
|
B.entryAllocas.push(`${slot} = alloca i1`);
|
|
B.line(`store i1 false, ptr ${slot}`);
|
|
const present = B.tmp();
|
|
const inspect = B.newLabel("ctor.inspect");
|
|
const compare = B.newLabel("ctor.compare");
|
|
const done = B.newLabel("ctor.done");
|
|
B.line(`${present} = icmp ne ptr ${value.name}, null`);
|
|
B.condBr(present, inspect, done);
|
|
B.startBlock(inspect);
|
|
const kind = host.dynKind(value.name);
|
|
const fn = B.tmp();
|
|
B.line(`${fn} = icmp eq i32 ${kind}, ${DYN_KIND.FUNC}`);
|
|
B.condBr(fn, compare, done);
|
|
B.startBlock(compare);
|
|
host.declare("declare zeroext i1 @scr_dyn_class_is_key(ptr, ptr)");
|
|
const matches = B.tmp();
|
|
B.line(
|
|
`${matches} = call zeroext i1 @scr_dyn_class_is_key(ptr ${value.name}, ptr ${key.name})`,
|
|
);
|
|
B.line(`store i1 ${matches}, ptr ${slot}`);
|
|
B.br(done);
|
|
B.startBlock(done);
|
|
const result = B.tmp();
|
|
B.line(`${result} = load i1, ptr ${slot}`);
|
|
return { name: result, type: e.type };
|
|
}
|
|
if (e.fn === "crypto.randomFillCb" || e.fn === "crypto.randomFillCbDyn") {
|
|
host.usesTimers = true;
|
|
const args = e.args.map((arg) => host.emitExpr(arg));
|
|
const cbT = e.args[4]!.type;
|
|
if (cbT.kind !== "func")
|
|
throw new InternalCompilerError("llvm emitter bug: randomFill callback not a func");
|
|
host.toHeap(args[4]!);
|
|
const dyn = e.fn === "crypto.randomFillCbDyn";
|
|
const adapter = host.cryptoBytesThunkFor(cbT, false, dyn ? "dyn" : "view");
|
|
const symbol = dyn ? "scr_crypto_random_fill_async_dyn" : "scr_crypto_random_fill_async";
|
|
const position = dyn ? "ptr" : "double";
|
|
host.declare(`declare void @${symbol}(ptr, ${position}, ${position}, double, ptr, ptr)`);
|
|
B.line(
|
|
`call void @${symbol}(ptr ${args[0]!.name}, ${position} ${args[1]!.name}, ${position} ${args[2]!.name}, double ${args[3]!.name}, ptr ${args[4]!.name}, ptr @${adapter})`,
|
|
);
|
|
host.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
if (e.fn === "crypto.generateKeyCb") {
|
|
host.usesTimers = true;
|
|
const args = e.args.map((arg) => host.emitExpr(arg));
|
|
const cbT = e.args[2]!.type;
|
|
if (cbT.kind !== "func")
|
|
throw new InternalCompilerError("llvm emitter bug: generateKey callback not a func");
|
|
host.toHeap(args[2]!);
|
|
const adapter = host.cryptoBytesThunkFor(cbT, false, "dyn");
|
|
host.declare(`declare void @scr_crypto_generate_key_async(ptr, ptr, ptr, ptr)`);
|
|
B.line(
|
|
`call void @scr_crypto_generate_key_async(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name}, ptr @${adapter})`,
|
|
);
|
|
host.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
if (e.fn === "crypto.randomIntCb") {
|
|
host.usesTimers = true;
|
|
const args = e.args.map((arg) => host.emitExpr(arg));
|
|
const cbT = e.args[3]!.type;
|
|
if (cbT.kind !== "func")
|
|
throw new InternalCompilerError("llvm emitter bug: randomInt callback not a func");
|
|
host.toHeap(args[3]!);
|
|
const adapter = host.cryptoIntThunkFor(cbT);
|
|
host.declare(`declare void @scr_crypto_random_int_async(double, double, double, ptr, ptr)`);
|
|
B.line(
|
|
`call void @scr_crypto_random_int_async(double ${args[0]!.name}, double ${args[1]!.name}, double ${args[2]!.name}, ptr ${args[3]!.name}, ptr @${adapter})`,
|
|
);
|
|
host.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
if (e.fn.startsWith("crypto.") && e.fn.includes("Promise")) {
|
|
// util.promisify over the callback forms: the callback form runs with
|
|
// a runtime callback that owns the +1 promise and settles it in the
|
|
// promise's value representation; an argument error the callback form
|
|
// threw moves into the promise as its rejection.
|
|
host.usesTimers = true;
|
|
if (e.type.kind !== "promise")
|
|
throw new InternalCompilerError("llvm emitter bug: crypto promise result not a promise");
|
|
const inner = e.type.inner;
|
|
const dyn = inner.kind === "dyn";
|
|
const args = e.args.map((arg) => host.emitExpr(arg));
|
|
const settle =
|
|
e.fn === "crypto.randomIntPromise"
|
|
? dyn
|
|
? "scr_crypto_promise_settle_number_dyn"
|
|
: "scr_crypto_promise_settle_number"
|
|
: e.fn === "crypto.hkdfPromise"
|
|
? "scr_crypto_promise_settle_array_buffer"
|
|
: e.fn === "crypto.generateKeyPromise" || e.fn === "crypto.randomFillPromiseDyn"
|
|
? "scr_crypto_promise_settle_dyn"
|
|
: !dyn
|
|
? "scr_crypto_promise_settle_bytes"
|
|
: e.fn === "crypto.randomFillPromise"
|
|
? "scr_crypto_promise_settle_view_dyn"
|
|
: "scr_crypto_promise_settle_buffer_dyn";
|
|
host.declare(`declare ptr @scr_promise_new()`);
|
|
host.declare(`declare ptr @scr_crypto_promise_callback(ptr)`);
|
|
host.declare(`declare void @scr_crypto_promise_reject_thrown(ptr)`);
|
|
host.declare(
|
|
e.fn === "crypto.randomIntPromise"
|
|
? `declare void @${settle}(ptr, double)`
|
|
: `declare void @${settle}(ptr, ptr, ptr)`,
|
|
);
|
|
const promise = B.tmp();
|
|
const cb = B.tmp();
|
|
B.line(`${promise} = call ptr @scr_promise_new()`);
|
|
B.line(`${cb} = call ptr @scr_crypto_promise_callback(ptr ${promise})`);
|
|
const a = (i: number, type = "ptr") => `${type} ${args[i]!.name}`;
|
|
const call = (symbol: string, params: string[], values: string[]) => {
|
|
host.declare(`declare void @${symbol}(${[...params, "ptr", "ptr"].join(", ")})`);
|
|
B.line(`call void @${symbol}(${[...values, `ptr ${cb}`, `ptr @${settle}`].join(", ")})`);
|
|
};
|
|
if (e.fn === "crypto.randomBytesPromise") {
|
|
call("scr_crypto_random_bytes_async", ["double"], [a(0, "double")]);
|
|
} else if (e.fn === "crypto.pbkdf2Promise") {
|
|
call(
|
|
"scr_crypto_pbkdf2_async",
|
|
["ptr", "ptr", "double", "double", "ptr"],
|
|
[a(0), a(1), a(2, "double"), a(3, "double"), a(4)],
|
|
);
|
|
} else if (e.fn === "crypto.scryptPromise") {
|
|
call(
|
|
"scr_crypto_scrypt_async",
|
|
["ptr", "ptr", "double", "ptr"],
|
|
[a(0), a(1), a(2, "double"), a(3)],
|
|
);
|
|
} else if (e.fn === "crypto.hkdfPromise") {
|
|
call(
|
|
"scr_crypto_hkdf_async",
|
|
["ptr", "ptr", "ptr", "ptr", "double"],
|
|
[a(0), a(1), a(2), a(3), a(4, "double")],
|
|
);
|
|
} else if (e.fn === "crypto.randomFillPromise") {
|
|
call(
|
|
"scr_crypto_random_fill_async",
|
|
["ptr", "double", "double", "double"],
|
|
[a(0), a(1, "double"), a(2, "double"), a(3, "double")],
|
|
);
|
|
} else if (e.fn === "crypto.randomFillPromiseDyn") {
|
|
call(
|
|
"scr_crypto_random_fill_async_dyn",
|
|
["ptr", "ptr", "ptr", "double"],
|
|
[a(0), a(1), a(2), a(3, "double")],
|
|
);
|
|
} else if (e.fn === "crypto.randomIntPromise") {
|
|
call(
|
|
"scr_crypto_random_int_async",
|
|
["double", "double", "double"],
|
|
[a(0, "double"), a(1, "double"), a(2, "double")],
|
|
);
|
|
} else if (e.fn === "crypto.generateKeyPromise") {
|
|
call("scr_crypto_generate_key_async", ["ptr", "ptr"], [a(0), a(1)]);
|
|
} else {
|
|
throw new InternalCompilerError(`llvm emitter bug: unknown crypto promise ${e.fn}`);
|
|
}
|
|
B.line(`call void @scr_crypto_promise_reject_thrown(ptr ${promise})`);
|
|
return host.own({ name: promise, type: e.type });
|
|
}
|
|
if (
|
|
e.fn === "crypto.randomBytesCb" ||
|
|
e.fn === "crypto.pbkdf2Cb" ||
|
|
e.fn === "crypto.hkdfCb" ||
|
|
e.fn === "crypto.scryptCb"
|
|
) {
|
|
host.usesTimers = true;
|
|
const args = e.args.map((arg) => host.emitExpr(arg));
|
|
const cbIndex = e.fn === "crypto.randomBytesCb" ? 1 : e.fn === "crypto.scryptCb" ? 4 : 5;
|
|
const cbT = e.args[cbIndex]!.type;
|
|
if (cbT.kind !== "func")
|
|
throw new InternalCompilerError("llvm emitter bug: crypto callback not a func");
|
|
host.toHeap(args[cbIndex]!);
|
|
const adapter = host.cryptoBytesThunkFor(cbT, e.fn === "crypto.hkdfCb");
|
|
if (e.fn === "crypto.randomBytesCb") {
|
|
host.declare(`declare void @scr_crypto_random_bytes_async(double, ptr, ptr)`);
|
|
B.line(
|
|
`call void @scr_crypto_random_bytes_async(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr @${adapter})`,
|
|
);
|
|
} else if (e.fn === "crypto.hkdfCb") {
|
|
host.declare(`declare void @scr_crypto_hkdf_async(ptr, ptr, ptr, ptr, double, ptr, ptr)`);
|
|
B.line(
|
|
`call void @scr_crypto_hkdf_async(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name}, ptr ${args[3]!.name}, double ${args[4]!.name}, ptr ${args[5]!.name}, ptr @${adapter})`,
|
|
);
|
|
} else if (e.fn === "crypto.scryptCb") {
|
|
host.declare(`declare void @scr_crypto_scrypt_async(ptr, ptr, double, ptr, ptr, ptr)`);
|
|
B.line(
|
|
`call void @scr_crypto_scrypt_async(ptr ${args[0]!.name}, ptr ${args[1]!.name}, double ${args[2]!.name}, ptr ${args[3]!.name}, ptr ${args[4]!.name}, ptr @${adapter})`,
|
|
);
|
|
} else {
|
|
host.declare(
|
|
`declare void @scr_crypto_pbkdf2_async(ptr, ptr, double, double, ptr, ptr, ptr)`,
|
|
);
|
|
B.line(
|
|
`call void @scr_crypto_pbkdf2_async(ptr ${args[0]!.name}, ptr ${args[1]!.name}, double ${args[2]!.name}, double ${args[3]!.name}, ptr ${args[4]!.name}, ptr ${args[5]!.name}, ptr @${adapter})`,
|
|
);
|
|
}
|
|
host.emitPendingCheck();
|
|
return { name: "", type: e.type };
|
|
}
|
|
if (
|
|
e.fn === "http.request" ||
|
|
e.fn === "http.requestCb" ||
|
|
e.fn === "http.requestUrl" ||
|
|
e.fn === "http.requestUrlCb" ||
|
|
e.fn === "http.requestAgent" ||
|
|
e.fn === "http.requestAgentCb" ||
|
|
e.fn === "https.request" ||
|
|
e.fn === "https.requestCb" ||
|
|
e.fn === "https.requestUrl" ||
|
|
e.fn === "https.requestUrlCb"
|
|
) {
|
|
// The https URL row is the http one with the TLS entry point — same
|
|
// three arguments, same response-callback adapter. The https options
|
|
// row is wider: rejectUnauthorized stays an i1, while its ScrStr or
|
|
// ScrBytes CA value expands to the runtime's raw pointer + length
|
|
// (caBytes: a null pointer for none).
|
|
const isTls = e.fn.startsWith("https.");
|
|
const isUrl = e.fn.includes("requestUrl");
|
|
const isTlsOptions = isTls && !isUrl;
|
|
const isAgent = e.fn.startsWith("http.requestAgent");
|
|
const cbIdx = isUrl ? 3 : isTlsOptions ? 9 : isAgent ? 8 : 7;
|
|
const hasCb = e.fn.endsWith("Cb");
|
|
const args = e.args.map((a) => host.emitExpr(a));
|
|
let cb = "null";
|
|
let adapter = "null";
|
|
if (hasCb) {
|
|
const cbT = e.args[cbIdx]!.type;
|
|
if (cbT.kind !== "func")
|
|
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`);
|
|
host.toHeap(args[cbIdx]!);
|
|
cb = args[cbIdx]!.name;
|
|
const sym = cbT.params.length === 0 ? "scr_http_resp_thunk0" : "scr_http_resp_thunk_res";
|
|
host.declare(`declare void @${sym}(ptr, ptr)`);
|
|
adapter = `@${sym}`;
|
|
}
|
|
const head = args.slice(0, cbIdx);
|
|
const entry = isTlsOptions
|
|
? "scr_https_request"
|
|
: isTls
|
|
? "scr_https_request_url"
|
|
: isUrl
|
|
? "scr_http_request_url"
|
|
: isAgent
|
|
? "scr_http_request_agent"
|
|
: "scr_http_request";
|
|
let callArgs = head.map((a) => `${host.llType(a.type)} ${a.name}`);
|
|
if (isTlsOptions) {
|
|
callArgs = [
|
|
...callArgs.slice(0, 8),
|
|
...caBytes(host, args[8]!).map((arg) => `${arg.type} ${arg.name}`),
|
|
];
|
|
host.declare(
|
|
`declare ptr @scr_https_request(ptr, double, ptr, ptr, double, ptr, i1 zeroext, i1 zeroext, ptr, ${host.sizeType}, ptr, ptr)`,
|
|
);
|
|
} else {
|
|
const decls = head.map((a) =>
|
|
host.llType(a.type) === "i1" ? "i1 zeroext" : host.llType(a.type),
|
|
);
|
|
host.declare(`declare ptr @${entry}(${[...decls, "ptr", "ptr"].join(", ")})`);
|
|
}
|
|
const t = B.tmp();
|
|
B.line(`${t} = call ptr @${entry}(${[...callArgs, `ptr ${cb}`, `ptr ${adapter}`].join(", ")})`);
|
|
const out = host.own({ name: t, type: e.type });
|
|
if (MAY_THROW_LIB_FNS.has(e.fn)) host.emitPendingCheck();
|
|
return out;
|
|
}
|
|
if (MAY_THROW_LIB_FNS.has(e.fn) && LIB_FN_SYMS[e.fn] === undefined) {
|
|
throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
|
|
}
|
|
// A setter's write: the time value into the Date object (ScrDate.ms).
|
|
if (e.fn === "date.store") {
|
|
const date = emitBorrowedInput(host, e.args[0]!).name;
|
|
const ms = host.emitExpr(e.args[1]!).name;
|
|
const field = B.tmp();
|
|
B.line(`${field} = getelementptr inbounds i8, ptr ${date}, i64 8 ; ->ms`);
|
|
B.line(`store double ${ms}, ptr ${field}`);
|
|
return { name: ms, type: e.type };
|
|
}
|
|
// queueMicrotask's checked-dynamic form carries the AsyncLocalStorage
|
|
// context like the static one (lib-process.ts).
|
|
const sym =
|
|
mockTimerSym(host, e) ??
|
|
(e.fn === "timers.queueMicrotaskDyn" && host.schedulesInContext
|
|
? "scr_queue_microtask_dyn_als"
|
|
: // Programs with constructor-function streams unbox untyped pipe
|
|
// destinations and pipeline stages through scr_dyn_builtin_stream.c,
|
|
// which also takes those streams.
|
|
e.fn === "stream.unboxDyn" && host.usesDynBuiltinStream
|
|
? "scr_dyn_builtin_stream_unbox"
|
|
: e.fn === "stream.unboxDynStage" && host.usesDynBuiltinStream
|
|
? "scr_dyn_builtin_stream_unbox_stage"
|
|
: // for await over a checked-dynamic value iterates a native
|
|
// stream behind it (scr_dyn_for_await_stream.c)
|
|
e.fn === "dyn.asyncIteratorOpen" && host.dynForAwaitStreams
|
|
? "scr_stream_dyn_async_iterator_open"
|
|
: (e.fn === "dyn.keySet" || e.fn === "dyn.keySetComputed") &&
|
|
host.sloppyDynWrites.has(`${e.loc.file}\0${e.loc.start}\0${e.loc.end}`)
|
|
? `${LIB_FN_SYMS[e.fn]}_sloppy`
|
|
: LIB_FN_SYMS[e.fn]);
|
|
if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
|
|
// The runtime's Date functions take and answer time values: a Date
|
|
// operand passes its time value, read at once from the borrowed object,
|
|
// and a Date result is a new Date object.
|
|
const args = READ_ONLY_STRING_LIB_FNS.has(e.fn)
|
|
? emitBorrowedInputs(host, e.args)
|
|
: e.args.map((a) =>
|
|
a.type.kind === "date"
|
|
? { name: emitDateTimeArg(host, a), type: F64 }
|
|
: borrowsJsonInputs(e.fn) || borrowsIteratorInputs(e.fn)
|
|
? emitBorrowedInput(host, a)
|
|
: host.emitExpr(a),
|
|
);
|
|
// getTime and valueOf answer the time value the argument already is.
|
|
if ((e.fn === "date.getTime" || e.fn === "date.valueOf") && e.args[0]?.type.kind === "date")
|
|
return { name: args[0]!.name, type: e.type };
|
|
// The deep comparisons' number test: computed here, with no runtime call.
|
|
if (e.fn === "assert.sameValue")
|
|
return { name: emitSameValueF64(host, args[0]!.name, args[1]!.name), type: e.type };
|
|
const callArgs = args;
|
|
if (e.type.kind === "date") {
|
|
const ms = emitSymbolLibCall(host, { ...e, type: F64 }, sym, callArgs);
|
|
host.declare(`declare ptr @scr_date_new(double)`);
|
|
const date = B.tmp();
|
|
B.line(`${date} = call ptr @scr_date_new(double ${ms.name})`);
|
|
return host.own({ name: date, type: e.type });
|
|
}
|
|
return emitSymbolLibCall(host, e, sym, callArgs);
|
|
}
|
|
|
|
/** The time value of a Date argument. A Date that a runtime Date function
|
|
* produces for this argument alone (`new Date(ms).getTime()`, a
|
|
* time-value local's read) passes the function's time value and creates
|
|
* no Date object. */
|
|
function emitDateTimeArg(host: LlvmEmitterContext, date: IrExpr): string {
|
|
// A time-value local's read is its time value itself.
|
|
if (date.kind === "libCall" && date.fn === "date.fromTime")
|
|
return host.emitExpr(date.args[0]!).name;
|
|
if (date.kind === "libCall" && date.fn.startsWith("date."))
|
|
return emitGenericLibCall(host, { ...date, type: F64 }).name;
|
|
return emitDateTime(host, emitBorrowedInput(host, date).name);
|
|
}
|
|
|
|
/** A Date object's time value (ScrDate.ms). */
|
|
function emitDateTime(host: LlvmEmitterContext, date: string): string {
|
|
const B = host.B;
|
|
const field = B.tmp();
|
|
const ms = B.tmp();
|
|
B.line(`${field} = getelementptr inbounds i8, ptr ${date}, i64 8 ; ->ms`);
|
|
B.line(`${ms} = load double, ptr ${field}`);
|
|
return ms;
|
|
}
|
|
|
|
/** The call of runtime function `sym` with evaluated arguments. */
|
|
function emitSymbolLibCall(
|
|
host: LlvmEmitterContext,
|
|
e: LibCallExpr,
|
|
sym: string,
|
|
args: readonly LlValue[],
|
|
): LlValue {
|
|
const B = host.B;
|
|
const argDecls = args.map((a) => {
|
|
const ty = host.llType(a.type);
|
|
return ty === "i1" ? "i1 zeroext" : ty;
|
|
});
|
|
const retTy = host.llType(e.type);
|
|
const retDecl = retTy === "i1" ? "zeroext i1" : retTy;
|
|
host.declare(`declare ${retDecl} @${sym}(${argDecls.join(", ")})`);
|
|
const argList = args.map((a, index) => `${argDecls[index]} ${a.name}`).join(", ");
|
|
if (retTy === "void") {
|
|
const failing = emitAssertFailing(host, e.fn, args);
|
|
let passed: string | null = null;
|
|
if (failing !== null) {
|
|
const fail = B.newLabel("assert.fail");
|
|
passed = B.newLabel("assert.pass");
|
|
B.condBr(failing, fail, passed);
|
|
B.startBlock(fail);
|
|
}
|
|
// An Assert instance's options apply to this call only: set after the
|
|
// arguments are evaluated, consumed by a failure, cleared by a pass.
|
|
const options =
|
|
e.assertOptions === undefined
|
|
? 0
|
|
: (e.assertOptions.diff === "full" ? 1 : 0) | (e.assertOptions.skipPrototype ? 2 : 0);
|
|
if (options !== 0) {
|
|
host.declare("declare void @scr_assert_set_options(i32)");
|
|
B.line(`call void @scr_assert_set_options(i32 ${options})`);
|
|
}
|
|
B.line(`call void @${sym}(${argList})`);
|
|
if (options !== 0 && failing === null) {
|
|
host.declare("declare void @scr_assert_clear_options()");
|
|
B.line("call void @scr_assert_clear_options()");
|
|
}
|
|
if (MAY_THROW_LIB_FNS.has(e.fn)) host.emitPendingCheck();
|
|
if (passed !== null) {
|
|
B.br(passed);
|
|
B.startBlock(passed);
|
|
}
|
|
return { name: "", type: e.type };
|
|
}
|
|
const t = B.tmp();
|
|
const attrs = PURE_MATH_LIB_FNS.has(e.fn) ? " nounwind willreturn memory(none)" : "";
|
|
B.line(`${t} = call ${retDecl} @${sym}(${argList})${attrs}`);
|
|
// The result joins its frame BEFORE the pending check so an unwind
|
|
// releases the dummy (NULL for refcounted returns) harmlessly.
|
|
const out = host.own({ name: t, type: e.type });
|
|
if (MAY_THROW_LIB_FNS.has(e.fn)) host.emitPendingCheck();
|
|
return out;
|
|
}
|
|
|
|
/** SameValue over doubles, as scr_assert_same_value_f64 computes it: equal
|
|
* bits, or both NaN. +0 and -0 differ. */
|
|
function emitSameValueF64(host: LlvmEmitterContext, a: string, b: string): string {
|
|
const B = host.B;
|
|
const bitsA = B.tmp();
|
|
const bitsB = B.tmp();
|
|
const equalBits = B.tmp();
|
|
const nanA = B.tmp();
|
|
const nanB = B.tmp();
|
|
const bothNan = B.tmp();
|
|
const same = B.tmp();
|
|
B.line(`${bitsA} = bitcast double ${a} to i64`);
|
|
B.line(`${bitsB} = bitcast double ${b} to i64`);
|
|
B.line(`${equalBits} = icmp eq i64 ${bitsA}, ${bitsB}`);
|
|
B.line(`${nanA} = fcmp uno double ${a}, ${a}`);
|
|
B.line(`${nanB} = fcmp uno double ${b}, ${b}`);
|
|
B.line(`${bothNan} = and i1 ${nanA}, ${nanB}`);
|
|
B.line(`${same} = or i1 ${equalBits}, ${bothNan}`);
|
|
return same;
|
|
}
|
|
|
|
/** The failure test of an assertion call that returns at once when the
|
|
* assertion passes, or null for other calls. Result calls carry their
|
|
* verdict; strictEqual over numbers and booleans compares here with the
|
|
* runtime's test: SameValue (equal bits, or both NaN) and `==`. */
|
|
function emitAssertFailing(
|
|
host: LlvmEmitterContext,
|
|
fn: string,
|
|
args: readonly LlValue[],
|
|
): string | null {
|
|
const B = host.B;
|
|
const negatedIndex = ASSERT_VERDICT_LIB_FNS.get(fn);
|
|
if (negatedIndex !== undefined) {
|
|
const failing = B.tmp();
|
|
const verdict = args[0]!.name;
|
|
if (negatedIndex === null) B.line(`${failing} = xor i1 ${verdict}, true`);
|
|
else B.line(`${failing} = icmp eq i1 ${verdict}, ${args[negatedIndex]!.name}`);
|
|
return failing;
|
|
}
|
|
if (fn !== "assert.eqF64" && fn !== "assert.eqBool") return null;
|
|
const [a, b, negated] = [args[0]!.name, args[1]!.name, args[2]!.name];
|
|
let same: string;
|
|
if (fn === "assert.eqBool") {
|
|
same = B.tmp();
|
|
B.line(`${same} = icmp eq i1 ${a}, ${b}`);
|
|
} else {
|
|
same = emitSameValueF64(host, a, b);
|
|
}
|
|
const failing = B.tmp();
|
|
B.line(`${failing} = icmp eq i1 ${same}, ${negated}`);
|
|
return failing;
|
|
}
|
|
|
|
/** Runtime string functions that only read their string arguments and keep
|
|
* no reference to them: their operands are lent (a comparator's
|
|
* `a.name.localeCompare(b.name)` reads both fields without retaining). */
|
|
const READ_ONLY_STRING_LIB_FNS: ReadonlySet<string> = new Set(["str.localeCompare"]);
|
|
|
|
/** The most parameters scr_regex_replace_fn calls a replacer with
|
|
* (SCR_REPLACER_MAX_ARITY), the first bit of its string-or-undefined
|
|
* parameters (SCR_REPLACER_OPTIONAL), and its flag for a checked-dynamic
|
|
* result (SCR_REPLACER_BOXED_RESULT). */
|
|
const REPLACER_MAX_ARITY = 6;
|
|
const REPLACER_OPTIONAL = 2 ** 8;
|
|
const REPLACER_BOXED_RESULT = 2 ** 31;
|
|
|
|
/** `string | undefined` held as the string pointer, NULL for undefined. */
|
|
function optionalStringType(host: LlvmEmitterContext, type: IrType): boolean {
|
|
if (type.kind !== "union") return false;
|
|
const nullable = host.nullableUnions.get(type.unionId);
|
|
if (nullable?.repr !== "single" || nullable.arm.kind !== "string" || nullable.nullTag >= 0)
|
|
return false;
|
|
return host.unionsById.get(type.unionId)?.arms[nullable.unitTag]?.kind === "undefinedT";
|
|
}
|
|
|
|
/** The parameter count and kinds (scr_regex_replace_fn's `kinds`) of a
|
|
* replace callback the runtime can call through the closure ABI, or null
|
|
* when it needs the dyn box. A checked-dynamic parameter receives whatever
|
|
* Node passes at its position. Each typed parameter must receive a value of
|
|
* its declared kind in every match: the match string at 0; past 0 the
|
|
* pattern must be a literal whose captures are known, so position i is a
|
|
* capture (a string when every match sets it, otherwise `string |
|
|
* undefined`), the offset (a number) right after the captures, or the
|
|
* subject (a string) after that. The result is a string or a
|
|
* checked-dynamic value. */
|
|
function typedReplacerShape(
|
|
host: LlvmEmitterContext,
|
|
callback: IrType,
|
|
pattern: IrExpr,
|
|
): { arity: number; kinds: number } | null {
|
|
if (callback.kind !== "func" || callback.rest || callback.argumentsAll) return null;
|
|
if (callback.ret.kind !== "string" && callback.ret.kind !== "dyn") return null;
|
|
const params = callback.params;
|
|
if (params.length > REPLACER_MAX_ARITY) return null;
|
|
let kinds = callback.ret.kind === "dyn" ? REPLACER_BOXED_RESULT : 0;
|
|
let captures: { count: number; always: boolean[] } | null | undefined;
|
|
for (let i = 0; i < params.length; i++) {
|
|
const kind = params[i]!.kind;
|
|
if (kind === "dyn") {
|
|
kinds += 2 ** i;
|
|
continue;
|
|
}
|
|
const optional = optionalStringType(host, params[i]!);
|
|
if (i === 0) {
|
|
if (kind !== "string") return null;
|
|
continue;
|
|
}
|
|
if (pattern.kind !== "regexLit") return null;
|
|
captures ??= regexLiteralCaptures(pattern.pattern, pattern.flags);
|
|
if (!captures) return null;
|
|
const ok =
|
|
i < captures.count
|
|
? (kind === "string" && captures.always[i] === true) || optional
|
|
: i === captures.count
|
|
? kind === "f64"
|
|
: i === captures.count + 1 && kind === "string";
|
|
if (!ok) return null;
|
|
if (optional) kinds += REPLACER_OPTIONAL * 2 ** i;
|
|
}
|
|
return { arity: params.length, kinds };
|
|
}
|
|
|
|
/** `s.replace(re, fn)` / `replaceAll` with a typed callback the runtime can
|
|
* call directly: the dyn box the IR wraps it in is never observable (the
|
|
* runtime only calls it, with `this` undefined), so the closure goes to
|
|
* scr_regex_replace_fn as it is. Null keeps the generic path. */
|
|
function emitTypedRegexReplace(host: LlvmEmitterContext, e: LibCallExpr): LlValue | null {
|
|
const [subject, pattern, callback, all] = e.args;
|
|
if (!subject || !pattern || callback?.kind !== "dynFrom" || all?.kind !== "boolLit") return null;
|
|
const shape = typedReplacerShape(host, callback.value.type, pattern);
|
|
if (shape === null) return null;
|
|
const B = host.B;
|
|
const s = host.emitExpr(subject);
|
|
const re = host.emitExpr(pattern);
|
|
const fn = host.emitExpr(callback.value);
|
|
host.declare(`declare ptr @scr_regex_replace_fn(ptr, ptr, ptr, i32, i32, i1 zeroext)`);
|
|
const result = B.tmp();
|
|
B.line(
|
|
`${result} = call ptr @scr_regex_replace_fn(ptr ${s.name}, ptr ${re.name}, ptr ${fn.name}, i32 ${shape.arity}, i32 ${shape.kinds | 0}, i1 zeroext ${all.value})`,
|
|
);
|
|
const output = host.own({ name: result, type: e.type });
|
|
host.emitPendingCheck();
|
|
return output;
|
|
}
|
|
|
|
export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
|
if (USES_TIMERS_LIB_FNS.has(e.fn)) host.usesTimers = true;
|
|
if (e.fn === "regex.replaceCallback") {
|
|
const typed = emitTypedRegexReplace(host, e);
|
|
if (typed) return typed;
|
|
}
|
|
if (e.fn === "process.nextTick") return host.emitAsyncContextLibCall(e);
|
|
if (
|
|
e.fn === "sp.pipeline" ||
|
|
e.fn.startsWith("sp.pipelineStages") ||
|
|
e.fn.startsWith("sp.pipelineOpts") ||
|
|
e.fn.startsWith("sp.finishedOpts")
|
|
)
|
|
return host.emitStreamLibCall(e);
|
|
if (e.fn === "bigint.parse" && e.args[0]?.kind === "strLit") {
|
|
// Literals (`123n`) and constant BigInt("...") texts are immortal
|
|
// statics instead of a parse at every evaluation.
|
|
const sym = host.internBigIntLiteral(e.args[0].value);
|
|
if (sym) return host.own({ name: host.retainFrame(sym, e.type), type: e.type });
|
|
}
|
|
if (e.fn.startsWith("bigint.")) {
|
|
// Small-value operator trees run in 64-bit registers (bigint-wrap.ts).
|
|
const fused = emitBigIntWrap(host, e);
|
|
if (fused) return fused;
|
|
}
|
|
// timers/promises arms the timer heap or the immediate queue, so the
|
|
// loop runs even when nothing awaits the promise: a pending ref'd timer
|
|
// keeps the process alive, as in Node.
|
|
if (e.fn.startsWith("tp.")) host.usesTimers = true;
|
|
if (e.fn === "atomics.op") {
|
|
const inline = emitInlineAtomicsOp(host, e);
|
|
if (inline) return inline;
|
|
}
|
|
if (e.fn === "bytes.isAbsent") {
|
|
// A borrowed read: retaining an absent Buffer would dereference NULL.
|
|
const value = host.emitReadReceiver(e.args[0]!);
|
|
const t = host.B.tmp();
|
|
host.B.line(`${t} = icmp eq ptr ${value.name}, null`);
|
|
return { name: t, type: e.type };
|
|
}
|
|
if (e.fn === "bytes.absent") return host.own({ name: "null", type: e.type });
|
|
if (e.fn === "bytes.disjoint") {
|
|
// [data, data + len * size) of each view, compared as addresses. A view
|
|
// that is not there (a binding the nest never assigned, NULL) answers
|
|
// false without reading its header, so the caller keeps its loop.
|
|
const B = host.B;
|
|
const values = e.args.map((arg) => ({ arg, value: host.emitReadReceiver(arg) }));
|
|
const present = values.map(({ value }) => {
|
|
const t = B.tmp();
|
|
B.line(`${t} = icmp ne ptr ${value.name}, null`);
|
|
return t;
|
|
});
|
|
const both = B.tmp();
|
|
B.line(`${both} = and i1 ${present[0]}, ${present[1]}`);
|
|
const compare = B.newLabel("disjoint.compare"),
|
|
done = B.newLabel("disjoint.done");
|
|
const from = B.currentLabel();
|
|
B.condBr(both, compare, done);
|
|
B.startBlock(compare);
|
|
const range = (arg: IrExpr, pointer: string): { start: string; end: string } => {
|
|
const size = arg.type.kind === "bytes" ? BYTES_ELEMENT_SIZE[arg.type.elem] : 1;
|
|
const dataSlot = B.tmp(),
|
|
data = B.tmp(),
|
|
lenSlot = B.tmp(),
|
|
len = B.tmp(),
|
|
bytes = B.tmp(),
|
|
start = B.tmp(),
|
|
end = B.tmp();
|
|
B.line(`${dataSlot} = getelementptr inbounds %ScrBytes, ptr ${pointer}, i64 0, i32 3`);
|
|
B.line(`${data} = load ptr, ptr ${dataSlot}`);
|
|
B.line(`${lenSlot} = getelementptr inbounds %ScrBytes, ptr ${pointer}, i64 0, i32 1`);
|
|
B.line(`${len} = load ${host.sizeType}, ptr ${lenSlot}`);
|
|
B.line(`${bytes} = mul ${host.sizeType} ${len}, ${size}`);
|
|
B.line(`${start} = ptrtoint ptr ${data} to ${host.sizeType}`);
|
|
B.line(`${end} = add ${host.sizeType} ${start}, ${bytes}`);
|
|
return { start, end };
|
|
};
|
|
const left = range(values[0]!.arg, values[0]!.value.name),
|
|
right = range(values[1]!.arg, values[1]!.value.name);
|
|
const before = B.tmp(),
|
|
after = B.tmp(),
|
|
apart = B.tmp(),
|
|
t = B.tmp();
|
|
B.line(`${before} = icmp ule ${host.sizeType} ${left.end}, ${right.start}`);
|
|
B.line(`${after} = icmp ule ${host.sizeType} ${right.end}, ${left.start}`);
|
|
B.line(`${apart} = or i1 ${before}, ${after}`);
|
|
const compared = B.currentLabel();
|
|
B.br(done);
|
|
B.startBlock(done);
|
|
B.line(`${t} = phi i1 [ false, %${from} ], [ ${apart}, %${compared} ]`);
|
|
return { name: t, type: e.type };
|
|
}
|
|
if (e.fn === "record.isFrozen") {
|
|
// The frozen bit of the record's rc word (scr_rc.h SCR_RC_FROZEN) is
|
|
// its second-highest bit; an immortal word sets the highest one too.
|
|
const value = host.emitReadReceiver(e.args[0]!);
|
|
const S = host.sizeType;
|
|
const rc = host.B.tmp(),
|
|
top = host.B.tmp(),
|
|
t = host.B.tmp();
|
|
host.B.line(`${rc} = load ${S}, ptr ${value.name}`);
|
|
host.B.line(`${top} = lshr ${S} ${rc}, ${S === "i64" ? 62 : 30}`);
|
|
host.B.line(`${t} = icmp eq ${S} ${top}, 1`);
|
|
return { name: t, type: e.type };
|
|
}
|
|
const prefix = e.fn.slice(0, e.fn.indexOf(".")) as LibCallPrefix;
|
|
// events.once builds typed listener adapters beside the emitter calls.
|
|
if (e.fn === "events.once" || e.fn === "events.onceSignal")
|
|
return host.emitErrorsEventsLibCall(e);
|
|
switch (prefix) {
|
|
case "fetch":
|
|
case "abort":
|
|
case "json":
|
|
return host.emitWebLibCall(e);
|
|
case "dyn":
|
|
case "global":
|
|
return host.emitDynamicLibCall(e);
|
|
case "js":
|
|
return emitJsLibCall(host, e);
|
|
case "fs":
|
|
case "fsp":
|
|
case "fileHandle":
|
|
case "dir":
|
|
case "watcher":
|
|
case "stats":
|
|
case "zlib":
|
|
case "atomics":
|
|
return host.emitFilesystemLibCall(e);
|
|
case "path":
|
|
case "os":
|
|
case "url":
|
|
case "sp":
|
|
case "qs":
|
|
return host.emitPathUrlLibCall(e);
|
|
case "math":
|
|
case "num":
|
|
case "str":
|
|
case "regexp":
|
|
case "intl":
|
|
case "sym":
|
|
case "perf":
|
|
case "number":
|
|
case "date":
|
|
case "text":
|
|
case "string":
|
|
case "class":
|
|
return host.emitPrimitiveLibCall(e);
|
|
case "cp":
|
|
case "spawnRes":
|
|
case "child":
|
|
case "writer":
|
|
case "procStream":
|
|
return host.emitChildProcessLibCall(e);
|
|
case "tp":
|
|
case "dc":
|
|
case "timers":
|
|
case "async":
|
|
case "als":
|
|
return host.emitAsyncContextLibCall(e);
|
|
case "process":
|
|
case "stdin":
|
|
return host.emitProcessLibCall(e);
|
|
case "module":
|
|
case "worker":
|
|
return host.emitGenericLibCall(e);
|
|
case "error":
|
|
case "regex":
|
|
case "emitter":
|
|
return host.emitErrorsEventsLibCall(e);
|
|
case "stream":
|
|
case "readable":
|
|
case "writable":
|
|
case "duplex":
|
|
case "transform":
|
|
case "passthrough":
|
|
case "sc":
|
|
return host.emitStreamLibCall(e);
|
|
case "dgram":
|
|
case "dns":
|
|
return emitDatagramLibCall(host, e);
|
|
case "net":
|
|
case "http":
|
|
case "https":
|
|
return host.emitNetworkHttpLibCall(e);
|
|
case "assert":
|
|
case "insp":
|
|
return host.emitAssertInspectLibCall(e);
|
|
case "rl":
|
|
case "strdec":
|
|
return host.emitIoLibCall(e);
|
|
case "console":
|
|
// The tail of a console lowering whose effects sit in its
|
|
// statements: nothing runs.
|
|
if (e.fn === "console.noop") return { name: "", type: e.type };
|
|
return host.emitGenericLibCall(e);
|
|
case "weakMap":
|
|
case "weakSet":
|
|
case "finalizationRegistry":
|
|
case "ffi":
|
|
case "sharedArrayBuffer":
|
|
case "arrayBuffer":
|
|
case "util":
|
|
case "bigint":
|
|
case "crypto":
|
|
case "webcrypto":
|
|
case "buffer":
|
|
case "bytes":
|
|
case "tlsca":
|
|
case "event":
|
|
case "events":
|
|
case "blob":
|
|
case "wasm":
|
|
case "array":
|
|
if (e.fn === "array.foreignThrow") return emitArrayForeignThrow(host, e);
|
|
return host.emitGenericLibCall(e);
|
|
case "collection":
|
|
case "record":
|
|
case "disposable":
|
|
case "punycode":
|
|
case "constants":
|
|
case "promise":
|
|
case "navigator":
|
|
case "mime":
|
|
return host.emitGenericLibCall(e);
|
|
case "nodeIsland":
|
|
return host.emitGenericLibCall(e);
|
|
case "test":
|
|
return emitTestLibCall(host, e);
|
|
case "tls":
|
|
return emitTlsLibCall(host, e);
|
|
case "http2":
|
|
return emitHttp2LibCall(host, e);
|
|
default: {
|
|
const _exhaustive: never = prefix;
|
|
void _exhaustive;
|
|
throw new InternalCompilerError("unreachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
/** `array.foreignThrow`: the checked conversion's TypeError for a foreign
|
|
* element read into a typed result. Always throws; the result is a dummy
|
|
* of the replaced read's type (the error.argTypeThrow pattern). */
|
|
function emitArrayForeignThrow(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
|
const B = host.B;
|
|
const arr = host.emitExpr(e.args[0]!);
|
|
const index = host.emitExpr(e.args[1]!);
|
|
const want = host.emitExpr(e.args[2]!);
|
|
host.declare(`declare void @scr_arr_foreign_throw(ptr, double, ptr)`);
|
|
B.line(
|
|
`call void @scr_arr_foreign_throw(ptr ${arr.name}, double ${index.name}, ptr ${want.name})`,
|
|
);
|
|
const ty = host.llType(e.type);
|
|
if (ty === "void") {
|
|
host.emitAlwaysPendingUnwind();
|
|
return { name: "", type: e.type };
|
|
}
|
|
const dummy = ty === "double" ? "0.0" : ty === "i1" ? "false" : "null";
|
|
const out = host.own({ name: dummy, type: e.type });
|
|
host.emitAlwaysPendingUnwind();
|
|
return out;
|
|
}
|
|
|
|
/** The settlement adapter of Promise.all's tuple form for the builder
|
|
* `fnName`, which fulfills the result with the builder's tuple. */
|
|
function allTupleAdapterFor(host: LlvmEmitterContext, fnName: string, tuple: IrType): string {
|
|
const key = `allTuple:${fnName}`;
|
|
let adapter = host.resolveThunks.get(key);
|
|
if (adapter !== undefined) return adapter;
|
|
adapter = host.contentSymbol("sc_all_tuple_", key);
|
|
host.resolveThunks.set(key, adapter);
|
|
host.journal?.allocated(adapter, "allTupleAdapterFor", [fnName, tuple]);
|
|
const rc = vAdapters(host.shapeHost, tuple);
|
|
host.declare(`declare ptr @scr_promise_payload_ref(ptr)`);
|
|
host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
|
|
host.resolveThunkDefs.push(
|
|
`define internal void @${adapter}(ptr %dst, ptr %src) ${FN_ATTRS} { ; Promise.all tuple`,
|
|
`entry:`,
|
|
` %entries = call ptr @scr_promise_payload_ref(ptr %src) ; +1, moves into the builder`,
|
|
` %tuple = call ptr @${host.callTarget(fnName)}(ptr %entries)`,
|
|
` call void @scr_promise_fulfill_ref(ptr %dst, ptr %tuple, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, tuple)})`,
|
|
` ret void`,
|
|
`}`,
|
|
``,
|
|
);
|
|
host.journal?.built();
|
|
return adapter;
|
|
}
|
|
|
|
registerReplay("allTupleAdapterFor", (host, args) =>
|
|
allTupleAdapterFor(host, args[0] as string, args[1] as IrType),
|
|
);
|
|
|
|
/** Promise.all's tuple form (promise.allTuple): the combinator over the
|
|
* entries stores each fulfilled entry promise, and a settlement adapter on
|
|
* the combinator calls the builder with them and fulfills the result with
|
|
* the tuple inside the combinator's settlement; a rejection settles the
|
|
* result the same way. The combinator itself counts as handled. */
|
|
function emitPromiseAllTuple(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
|
const B = host.B;
|
|
const build = e.args[1]!;
|
|
if (build.kind !== "closure" || e.type.kind !== "promise")
|
|
throw new InternalCompilerError("llvm emitter bug: promise.allTuple shape");
|
|
const entriesT = e.args[0]!.type;
|
|
const entries = host.emitExpr(e.args[0]!);
|
|
const adapter = allTupleAdapterFor(host, build.fnName, e.type.inner);
|
|
host.declare(`declare ptr @scr_promise_all(ptr, ptr, ptr)`);
|
|
host.declare(`declare void @scr_promise_all_store_promise(ptr, double, ptr)`);
|
|
host.declare(`declare ptr @scr_promise_new()`);
|
|
host.declare(`declare void @scr_promise_race_add(ptr, ptr, ptr)`);
|
|
host.declare(`declare void @scr_promise_mark_handled(ptr)`);
|
|
host.declare(`declare void @scr_promise_release(ptr)`);
|
|
host.declare(`declare double @scr_arr_len(ptr)`);
|
|
if (entriesT.kind !== "array")
|
|
throw new InternalCompilerError("llvm emitter bug: promise.allTuple entries");
|
|
const len = B.tmp();
|
|
const cap = B.tmp();
|
|
B.line(`${len} = call double @scr_arr_len(ptr ${entries.name})`);
|
|
B.line(`${cap} = fptoui double ${len} to ${host.sizeType}`);
|
|
const values = B.tmp();
|
|
B.line(`${values} = ${arrNewCall(host.shapeHost, entriesT.elem, cap)}`);
|
|
host.own({ name: values, type: entriesT });
|
|
const all = B.tmp();
|
|
B.line(
|
|
`${all} = call ptr @scr_promise_all(ptr ${entries.name}, ptr ${values}, ptr @scr_promise_all_store_promise)`,
|
|
);
|
|
B.line(`call void @scr_promise_mark_handled(ptr ${all})`);
|
|
const result = B.tmp();
|
|
B.line(`${result} = call ptr @scr_promise_new()`);
|
|
B.line(`call void @scr_promise_race_add(ptr ${result}, ptr ${all}, ptr @${adapter})`);
|
|
B.line(`call void @scr_promise_release(ptr ${all})`);
|
|
return host.own({ name: result, type: e.type });
|
|
}
|