import {
convertTargetName,
literalIndex,
resolvePackedValue,
stripOuterParens,
wrapExpression,
} from "./high-level-utils.js";
import { warnStackUnderflow } from "./high-level-state.js";
export function tryCollectionExpression(state, instruction) {
const mnemonic = instruction.opcode.mnemonic;
if (mnemonic === "NEWARRAY0") {
const temp = `t${state.nextTempId}`;
state.nextTempId += 1;
state.statements.push(`let ${temp} = [];`);
state.stack.push(temp);
return true;
}
if (mnemonic === "NEWARRAY_T") {
if (!ensureStack(state, instruction, 1)) return true;
const size = stripOuterParens(state.stack.pop());
const targetName = convertTargetName(instruction.operand);
const typeText =
targetName !== null ? `"${targetName}"` : formatTypeByte(instruction.operand);
const temp = `t${state.nextTempId}`;
state.nextTempId += 1;
state.statements.push(`let ${temp} = new_array_t(${size}, ${typeText});`);
state.stack.push(temp);
return true;
}
if (mnemonic === "NEWMAP") {
const temp = `t${state.nextTempId}`;
state.nextTempId += 1;
state.statements.push(`let ${temp} = Map();`);
state.stack.push(temp);
return true;
}
if (mnemonic === "NEWSTRUCT0") {
const temp = `t${state.nextTempId}`;
state.nextTempId += 1;
state.statements.push(`let ${temp} = Struct();`);
state.stack.push(temp);
return true;
}
if (mnemonic === "NEWSTRUCT") {
if (!ensureStack(state, instruction, 1)) return true;
const value = stripOuterParens(state.stack.pop());
const temp = `t${state.nextTempId}`;
state.nextTempId += 1;
state.statements.push(`let ${temp} = new_struct(${value});`);
state.stack.push(temp);
return true;
}
if (mnemonic === "PACK" || mnemonic === "PACKMAP" || mnemonic === "PACKSTRUCT") {
return emitPackExpression(state, mnemonic, stripOuterParens);
}
if (mnemonic === "PICKITEM") {
if (!ensureStack(state, instruction, 2)) return true;
const [target, index] = popArgs(state, 2);
state.stack.push(`${wrapExpression(target)}[${index}]`);
return true;
}
if (mnemonic === "HASKEY") {
if (!ensureStack(state, instruction, 2)) return true;
const [target, key] = popArgs(state, 2);
state.stack.push(`has_key(${wrapExpression(target)}, ${key})`);
return true;
}
if (mnemonic === "SIZE") {
if (!ensureStack(state, instruction, 1)) return true;
const target = stripOuterParens(state.stack.pop());
state.stack.push(`len(${wrapExpression(target)})`);
return true;
}
if (mnemonic === "KEYS") {
if (!ensureStack(state, instruction, 1)) return true;
const target = stripOuterParens(state.stack.pop());
state.stack.push(`keys(${wrapExpression(target)})`);
return true;
}
if (mnemonic === "VALUES") {
if (!ensureStack(state, instruction, 1)) return true;
const target = stripOuterParens(state.stack.pop());
state.stack.push(`values(${wrapExpression(target)})`);
return true;
}
if (mnemonic === "POPITEM") {
if (!ensureStack(state, instruction, 1)) return true;
const target = stripOuterParens(state.stack.pop());
state.stack.push(`pop_item(${wrapExpression(target)})`);
return true;
}
if (mnemonic === "ISTYPE") {
if (!ensureStack(state, instruction, 1)) return true;
const value = stripOuterParens(state.stack.pop());
const targetName = convertTargetName(instruction.operand);
let expression;
if (targetName !== null) {
expression = `is_type_${targetName}(${value})`;
} else if (instruction.operand !== null) {
expression = `is_type(${formatCallArgument(value)}, ${formatTypeByte(instruction.operand)})`;
} else {
expression = `is_type(${formatCallArgument(value)})`;
}
const temp = `t${state.nextTempId}`;
state.nextTempId += 1;
state.statements.push(`let ${temp} = ${expression};`);
state.stack.push(temp);
return true;
}
if (mnemonic === "UNPACK") {
if (!ensureStack(state, instruction, 1)) return true;
const source = stripOuterParens(state.stack.pop());
const packed = resolvePackedValue(state, source);
if (!packed) {
const elementCount = inferUnpackElementCount(state, instruction);
const elementsTemp = `unpack(${source})`;
state.statements.push(`let t${state.nextTempId} = ${elementsTemp};`);
const unpackTemp = `t${state.nextTempId}`;
state.nextTempId += 1;
for (let index = 0; index < elementCount; index += 1) {
const itemTemp = `t${state.nextTempId}`;
state.nextTempId += 1;
state.statements.push(
`let ${itemTemp} = unpack_item(${unpackTemp}, ${index});`,
);
state.stack.push(itemTemp);
}
const countTemp = `t${state.nextTempId}`;
state.nextTempId += 1;
state.statements.push(`let ${countTemp} = len(${source});`);
state.stack.push(countTemp);
return true;
}
for (let i = packed.length - 1; i >= 0; i--) {
state.stack.push(packed[i]);
}
state.stack.push(`${packed.length}`);
return true;
}
return false;
}
export function tryCollectionStatement(state, instruction) {
const mnemonic = instruction.opcode.mnemonic;
if (mnemonic === "MEMCPY") {
if (!ensureStack(state, instruction, 5)) return true;
const args = popArgs(state, 5);
state.statements.push(`memcpy(${args.join(", ")});`);
return true;
}
if (mnemonic === "SETITEM") {
if (!ensureStack(state, instruction, 3)) return true;
const [target, index, value] = popArgs(state, 3);
state.statements.push(`${wrapExpression(target)}[${index}] = ${formatCallArgument(value)};`);
return true;
}
if (mnemonic === "APPEND") {
if (!ensureStack(state, instruction, 2)) return true;
const [target, value] = popArgs(state, 2);
state.statements.push(`append(${wrapExpression(target)}, ${value});`);
return true;
}
if (mnemonic === "REMOVE") {
if (!ensureStack(state, instruction, 2)) return true;
const [target, key] = popArgs(state, 2);
state.statements.push(`remove_item(${wrapExpression(target)}, ${key});`);
return true;
}
if (mnemonic === "CLEARITEMS") {
if (!ensureStack(state, instruction, 1)) return true;
const target = stripOuterParens(state.stack.pop());
state.statements.push(`clear_items(${wrapExpression(target)});`);
return true;
}
if (mnemonic === "REVERSEITEMS") {
if (!ensureStack(state, instruction, 1)) return true;
const target = stripOuterParens(state.stack.pop());
state.statements.push(`reverse_items(${wrapExpression(target)});`);
return true;
}
return false;
}
function ensureStack(state, instruction, needed) {
if (state.stack.length >= needed) return true;
warnStackUnderflow(state, instruction, needed);
return false;
}
function popArgs(state, count) {
const args = [];
for (let index = 0; index < count; index += 1) {
args.push(stripOuterParens(state.stack.pop()));
}
args.reverse();
return args;
}
function formatCallArgument(value) {
return /^-\d+$/u.test(value) ? `(${value})` : value;
}
function formatTypeByte(operand) {
const byte =
operand.kind === "U8" ? operand.value : operand.kind === "I8" ? operand.value & 0xff : null;
if (byte === null) {
return "unknown";
}
return `0x${byte.toString(16).padStart(2, "0").toUpperCase()}`;
}
const PACK_MAX_INLINE = 64;
function emitPackExpression(state, mnemonic, stripOuterParens) {
const countText = state.stack.pop();
const count = literalIndex(countText);
if (!Number.isFinite(count) || count < 0) {
state.stack.push(`pack_dynamic(${countText ?? "???"})`);
return true;
}
const isMap = mnemonic === "PACKMAP";
const unit = isMap ? 2 : 1;
const drainCount = Math.min(count, Math.floor(state.stack.length / unit), PACK_MAX_INLINE);
const rendered = [];
for (let index = 0; index < drainCount; index += 1) {
if (isMap) {
const key = stripOuterParens(state.stack.pop() ?? "???");
const value = stripOuterParens(state.stack.pop() ?? "???");
rendered.push(`${key}: ${value}`);
} else {
rendered.push(stripOuterParens(state.stack.pop() ?? "???"));
}
}
const remainder = count - drainCount;
if (remainder > 0) {
const noun = isMap
? remainder === 1 ? "entry" : "entries"
: remainder === 1 ? "element" : "elements";
rendered.push(`/* ${remainder} more ${noun} */`);
let excess = remainder * unit;
while (excess > 0 && state.stack.length > 0) {
state.stack.pop();
excess -= 1;
}
}
const expression = renderPackedExpression(mnemonic, rendered);
state.stack.push(expression);
if (!isMap) {
state.packedValuesByExpression.set(expression, [...rendered]);
}
return true;
}
function renderPackedExpression(mnemonic, elements) {
const body = elements.join(", ");
if (mnemonic === "PACKMAP") {
return `Map(${body})`;
}
if (mnemonic === "PACKSTRUCT") {
return `Struct(${body})`;
}
return `[${body}]`;
}
function inferUnpackElementCount(state, instruction) {
const DEFAULT_COUNT = 4;
if (!state.programIndexByOffset) {
const map = new Map();
for (let i = 0; i < state.program.length; i++) {
map.set(state.program[i].offset, i);
}
state.programIndexByOffset = map;
}
const unpackIndex = state.programIndexByOffset.get(instruction.offset);
if (unpackIndex === undefined) {
return DEFAULT_COUNT;
}
let cursor = unpackIndex + 1;
if (cursor >= state.program.length) {
return DEFAULT_COUNT;
}
if (state.program[cursor].opcode.mnemonic !== "DROP") {
return DEFAULT_COUNT;
}
cursor += 1;
let pops = 0;
while (
cursor < state.program.length &&
isSinglePopMnemonic(state.program[cursor].opcode.mnemonic)
) {
pops += 1;
cursor += 1;
}
if (pops === 0) {
return DEFAULT_COUNT;
}
const hasDupBefore =
unpackIndex > 0 && state.program[unpackIndex - 1].opcode.mnemonic === "DUP";
const count = hasDupBefore ? Math.max(0, pops - 1) : pops;
return count === 0 ? DEFAULT_COUNT : count;
}
function isSinglePopMnemonic(mnemonic) {
return (
mnemonic === "DROP" ||
mnemonic.startsWith("STLOC") ||
mnemonic.startsWith("STARG") ||
mnemonic.startsWith("STSFLD")
);
}