mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
#1866's proven-integral fast path upgrades tonumber(arg) to CALL_INT when the argument is an all-digit SCONST. But Lua 5.4 returns a FLOAT, not an integer, when a decimal literal overflows int64: tonumber("9223372036854775808") -> 9.2e18 (INT64_MAX + 1) tonumber("99999999999999999999999999") -> 1e26 For those, CALL_INT sees lua_isinteger == false and post-entry declines loud (#-1 LUA ERROR) where the interpreter answers the float — a compiled-vs-interpreter divergence, exactly the class the seam corpus guards, newly introduced by the fast path. lua_tonumber_arg_is_integral now bounds the significant-digit count: INT64_MAX has 19 digits, so <= 18 significant digits always fits and is guaranteed integral; 19+ falls back to CALL_VAL, which preserves the typed float. Conservative (some in-range 19-digit values take the slower CALL_VAL path) but sound — same significant-digit bound as the CIDR prefix guard. tonumber("17") still executes native. tests/luajit gains two EXEC pins (INT64_MAX+1 and a 26-digit literal); both FAIL against the pre-fix engine (post-entry decline) and pass after. Harness: 228 chunks, 0 divergences, 0 exec_post_entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3751 lines
160 KiB
C++
3751 lines
160 KiB
C++
/*! \file hir_lower_lua.cpp
|
|
* \brief Lua 5.4 bytecode → HIR lowering.
|
|
*
|
|
* Two-pass approach:
|
|
* Pass 1: scan for basic block boundaries (branch targets).
|
|
* Pass 2: walk opcodes, emit HIR instructions.
|
|
*
|
|
* Lua register map: lua_reg[i] holds the current HIR value number
|
|
* for Lua register i. Updated on each register write.
|
|
*
|
|
* Unsupported opcodes → return -1 (caller falls back to Lua VM).
|
|
*/
|
|
|
|
#include "copyright.h"
|
|
#include "autoconf.h"
|
|
#include "config.h"
|
|
#include "externs.h"
|
|
|
|
#include "dbt_compile.h"
|
|
#include "engine_api.h"
|
|
#include "lua_bytecode.h"
|
|
#include "hir_lower_lua.h"
|
|
|
|
#include <cstring>
|
|
#include <cstdio>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <map>
|
|
#include <set>
|
|
#include <vector>
|
|
#include <string>
|
|
|
|
// Maximum Lua registers we track.
|
|
static constexpr int MAX_LUA_REGS = 256;
|
|
|
|
// Q-register slots for Lua for-loop variables.
|
|
// Reuses compiler-internal slots 10-12 (same as softcode iter()).
|
|
// Safe because Lua lowering never calls the softcode iter() path.
|
|
//
|
|
static constexpr int QREG_LUA_IDX = 10; // loop index variable
|
|
static constexpr int QREG_LUA_BUDGET = 12; // back-edge iteration budget
|
|
|
|
// Maximum inline depth for nested lowering.
|
|
static constexpr int MAX_INLINE_DEPTH = 4;
|
|
|
|
// Maximum code size we attempt to JIT (prevents runaway compile time).
|
|
static constexpr int MAX_LUA_CODE_SIZE = 1024;
|
|
|
|
// Maximum Lua stack size (register pressure bound).
|
|
static constexpr int MAX_LUA_STACK = 64;
|
|
|
|
// Maximum parameters.
|
|
static constexpr int MAX_LUA_PARAMS = 8;
|
|
|
|
// ---------------------------------------------------------------
|
|
// Back-edge iteration budget.
|
|
//
|
|
// Emits HIR to decrement QREG_LUA_BUDGET at each back-edge.
|
|
// When the counter reaches zero, the combined condition forces
|
|
// loop exit — same model as MUSHcode's func_invk_lim. The
|
|
// budget is initialized to lua_instruction_limit (default 100K)
|
|
// at function entry.
|
|
//
|
|
// The per-iteration cost is: LOAD_Q, SUB, STORE_Q, GT, BAND —
|
|
// five integer ops, no ECALL. At GHz speed this is negligible.
|
|
//
|
|
// cond: the original loop condition (>=0 for FORLOOP/TFORLOOP),
|
|
// or -1 for unconditional back-edges (JMP).
|
|
// Returns: combined condition value.
|
|
// ---------------------------------------------------------------
|
|
|
|
static int emit_budget_check(hir_program &h, int cond, int amount) {
|
|
int budget = h.emit(HIR_LOAD_Q, TY_INT, -1, -1, QREG_LUA_BUDGET);
|
|
if (budget < 0) return cond;
|
|
// Decrement by the loop body's bytecode length, not by 1: the
|
|
// interpreter's hook counts INSTRUCTIONS, and a per-edge tick would
|
|
// let a compiled loop run body-length times longer than the VM
|
|
// before tripping the same limit.
|
|
if (amount < 1) amount = 1;
|
|
int amt = h.emit(HIR_ICONST, TY_INT, -1, -1, amount);
|
|
int new_budget = h.emit(HIR_SUB, TY_INT, budget, amt);
|
|
h.emit(HIR_STORE_Q, TY_VOID, new_budget, -1, QREG_LUA_BUDGET);
|
|
|
|
int zero_val = h.emit(HIR_ICONST, TY_INT, -1, -1, 0);
|
|
int budget_ok = h.emit(HIR_GT, TY_INT, new_budget, zero_val);
|
|
|
|
if (cond >= 0) {
|
|
return h.emit(HIR_BAND, TY_INT, cond, budget_ok);
|
|
}
|
|
return budget_ok;
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Rejection reason names (for diagnostics).
|
|
// ---------------------------------------------------------------
|
|
|
|
const char *lua_bc_reject_name(lua_bc_reject reason) {
|
|
switch (reason) {
|
|
case LUA_BC_ELIGIBLE: return "eligible";
|
|
case LUA_BC_EMPTY: return "empty proto";
|
|
case LUA_BC_TOO_LARGE: return "code or stack too large";
|
|
case LUA_BC_TOO_MANY_PARAMS: return "too many parameters";
|
|
case LUA_BC_HAS_CLOSURE: return "contains OP_CLOSURE";
|
|
case LUA_BC_HAS_VARARG: return "contains OP_VARARG";
|
|
case LUA_BC_HAS_TBC: return "contains OP_TBC";
|
|
case LUA_BC_HAS_TAILCALL: return "contains OP_TAILCALL";
|
|
case LUA_BC_HAS_NESTED_PROTOS: return "has nested protos";
|
|
case LUA_BC_UNSUPPORTED_OP: return "unsupported opcode";
|
|
case LUA_BC_HAS_NON_INT_CONST: return "non-integer float constant";
|
|
case LUA_BC_HAS_LOOP: return "backward branch (unbounded on host)";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Eligibility pre-filter.
|
|
//
|
|
// This is a fast O(n) scan over the proto's instruction stream
|
|
// that rejects protos we know we cannot compile. It runs before
|
|
// any HIR allocation so the reject path is cheap.
|
|
//
|
|
// The supported opcode set must exactly match what
|
|
// hir_lower_lua_proto() handles in its switch statement.
|
|
// ---------------------------------------------------------------
|
|
|
|
lua_bc_reject lua_bc_eligible(const lua_bc_proto *proto) {
|
|
if (nullptr == proto) return LUA_BC_EMPTY;
|
|
|
|
int n = static_cast<int>(proto->code.size());
|
|
if (n == 0) return LUA_BC_EMPTY;
|
|
|
|
// --- Header checks ---
|
|
|
|
if (n > MAX_LUA_CODE_SIZE) return LUA_BC_TOO_LARGE;
|
|
if (proto->maxstacksize > MAX_LUA_STACK) return LUA_BC_TOO_LARGE;
|
|
if (proto->numparams > MAX_LUA_PARAMS) return LUA_BC_TOO_MANY_PARAMS;
|
|
|
|
// Nested protos mean OP_CLOSURE will appear. Reject early without
|
|
// scanning — the bytecode can't reference protos that don't exist.
|
|
if (!proto->protos.empty()) return LUA_BC_HAS_NESTED_PROTOS;
|
|
|
|
// --- Opcode scan ---
|
|
//
|
|
// We maintain a whitelist of opcodes the lowering handles.
|
|
// Anything outside the whitelist → reject.
|
|
//
|
|
// Note: VARARGPREP is harmless (adjusts stack for main chunks)
|
|
// and is treated as a no-op. OP_VARARG is the actual vararg
|
|
// access instruction and is rejected.
|
|
|
|
for (int pc = 0; pc < n; pc++) {
|
|
int op = proto->code[pc].opcode();
|
|
switch (op) {
|
|
|
|
// Data movement — handled.
|
|
case OP_LUA_MOVE:
|
|
case OP_LUA_LOADI:
|
|
case OP_LUA_LOADF:
|
|
case OP_LUA_LOADK:
|
|
case OP_LUA_LOADKX:
|
|
case OP_LUA_LOADFALSE:
|
|
case OP_LUA_LFALSESKIP:
|
|
case OP_LUA_LOADTRUE:
|
|
case OP_LUA_LOADNIL:
|
|
break;
|
|
|
|
// Arithmetic — handled (integer and float).
|
|
case OP_LUA_ADD:
|
|
case OP_LUA_SUB:
|
|
case OP_LUA_MUL:
|
|
case OP_LUA_DIV:
|
|
case OP_LUA_IDIV:
|
|
case OP_LUA_MOD:
|
|
case OP_LUA_UNM:
|
|
case OP_LUA_NOT:
|
|
case OP_LUA_BNOT:
|
|
case OP_LUA_LEN:
|
|
case OP_LUA_CONCAT:
|
|
case OP_LUA_ADDI:
|
|
case OP_LUA_ADDK:
|
|
case OP_LUA_SUBK:
|
|
case OP_LUA_MULK:
|
|
case OP_LUA_DIVK:
|
|
case OP_LUA_IDIVK:
|
|
case OP_LUA_MODK:
|
|
case OP_LUA_BAND:
|
|
case OP_LUA_BOR:
|
|
case OP_LUA_BXOR:
|
|
case OP_LUA_SHL:
|
|
case OP_LUA_SHR:
|
|
case OP_LUA_SHRI:
|
|
case OP_LUA_SHLI:
|
|
case OP_LUA_BANDK:
|
|
case OP_LUA_BORK:
|
|
case OP_LUA_BXORK:
|
|
case OP_LUA_POW:
|
|
case OP_LUA_POWK:
|
|
break;
|
|
|
|
// Metamethod companions — skipped as no-ops.
|
|
case OP_LUA_MMBIN:
|
|
case OP_LUA_MMBINI:
|
|
case OP_LUA_MMBINK:
|
|
break;
|
|
|
|
// Table operations — handled via ECALL back to Lua VM.
|
|
case OP_LUA_NEWTABLE:
|
|
case OP_LUA_GETTABLE:
|
|
case OP_LUA_GETTABI:
|
|
case OP_LUA_GETFIELD:
|
|
case OP_LUA_SETTABLE:
|
|
case OP_LUA_SETTABI:
|
|
case OP_LUA_SETFIELD:
|
|
case OP_LUA_SETLIST:
|
|
break;
|
|
|
|
// Comparisons — handled.
|
|
case OP_LUA_EQK:
|
|
case OP_LUA_EQ:
|
|
case OP_LUA_LT:
|
|
case OP_LUA_LE:
|
|
case OP_LUA_EQI:
|
|
case OP_LUA_LTI:
|
|
case OP_LUA_LEI:
|
|
case OP_LUA_GTI:
|
|
case OP_LUA_GEI:
|
|
case OP_LUA_TEST:
|
|
case OP_LUA_TESTSET:
|
|
break;
|
|
|
|
// Control flow — handled.
|
|
case OP_LUA_JMP:
|
|
case OP_LUA_FORPREP:
|
|
case OP_LUA_FORLOOP:
|
|
break;
|
|
|
|
// Return — handled.
|
|
case OP_LUA_RETURN:
|
|
case OP_LUA_RETURN0:
|
|
case OP_LUA_RETURN1:
|
|
break;
|
|
|
|
// Table/global access and function calls — handled. TAILCALL is
|
|
// lowered as CALL-then-RETURN: the frame-reuse the real mechanism
|
|
// exists for does not apply when the callee runs via an ECALL doing
|
|
// its own pcall, and the chunk-level pcall takes one result either
|
|
// way. A k flag (upvalues to close) declines in the lowering.
|
|
case OP_LUA_GETTABUP:
|
|
case OP_LUA_SETTABUP:
|
|
case OP_LUA_GETUPVAL:
|
|
case OP_LUA_SETUPVAL:
|
|
case OP_LUA_SELF:
|
|
case OP_LUA_CALL:
|
|
case OP_LUA_TAILCALL:
|
|
break;
|
|
|
|
// Generic for-loop — handled via ECALL.
|
|
case OP_LUA_TFORPREP:
|
|
case OP_LUA_TFORCALL:
|
|
case OP_LUA_TFORLOOP:
|
|
break;
|
|
|
|
// Harmless no-ops.
|
|
case OP_LUA_VARARGPREP:
|
|
case OP_LUA_EXTRAARG:
|
|
case OP_LUA_CLOSE: // no open upvalues without closures
|
|
break;
|
|
|
|
// --- Hard rejects ---
|
|
|
|
case OP_LUA_CLOSURE:
|
|
return LUA_BC_HAS_CLOSURE;
|
|
|
|
case OP_LUA_VARARG:
|
|
return LUA_BC_HAS_VARARG;
|
|
|
|
case OP_LUA_TBC:
|
|
return LUA_BC_HAS_TBC;
|
|
|
|
// --- Everything else is unsupported ---
|
|
default:
|
|
return LUA_BC_UNSUPPORTED_OP;
|
|
}
|
|
}
|
|
|
|
// Backward branches (#1326, partially lifted by #1732).
|
|
//
|
|
// Instruction and memory limits are enforced by CLuaMod::InsnCountHook,
|
|
// which is a Lua VM hook: it does not exist on the compiled path, so an
|
|
// unbudgeted compiled loop runs unbounded (TC009's original symptom).
|
|
//
|
|
// Numeric for loops (OP_FORLOOP) now compile under a back-edge budget
|
|
// whose exhaustion ABORTS the run via ECALL_LUA_LIMITED -- the runner
|
|
// fails over to the interpreter, which re-runs the chunk and raises its
|
|
// own "instruction limit exceeded" through the hook, so the error
|
|
// surface is the interpreter's verbatim. The re-run is what shapes the
|
|
// restrictions below: a loop proto must be RERUN-SAFE, so anything that
|
|
// could reach outside the chunk is rejected -- calls (a rebound global
|
|
// is arbitrary effectful code), SETTABUP (global writes), SELF (method
|
|
// dispatch). Chunk-local table stores are fine: TryJIT's stack
|
|
// save/restore discards them with the chunk. TESTSET is rejected
|
|
// because it writes a register from inside a terminator, which the
|
|
// store-at-write q-register routing in the lowering cannot see. The
|
|
// stack cap is the q-register file: loop-carried Lua registers map onto
|
|
// q-regs 0..9.
|
|
//
|
|
// while/repeat (backward OP_JMP) and generic for (TFORLOOP) still
|
|
// reject: the JMP shape needs the same budget wiring on a less regular
|
|
// structure, and TFOR's iterator call is the dead named bridge.
|
|
//
|
|
bool has_back_edge = false;
|
|
for (int pc = 0; pc < n; pc++) {
|
|
const lua_bc_instruction &insn = proto->code[pc];
|
|
switch (insn.opcode()) {
|
|
// while/repeat loop back through a backward JMP; same budget,
|
|
// same rerun-safety restrictions below as the numeric for.
|
|
case OP_LUA_JMP:
|
|
if (pc + 1 + insn.sJ() <= pc) has_back_edge = true;
|
|
break;
|
|
|
|
case OP_LUA_FORLOOP:
|
|
has_back_edge = true;
|
|
break;
|
|
|
|
// Backward by construction; the iterator protocol is unsupported.
|
|
case OP_LUA_TFORLOOP:
|
|
return LUA_BC_HAS_LOOP;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (has_back_edge) {
|
|
if (proto->maxstacksize > 10) return LUA_BC_HAS_LOOP;
|
|
for (int pc = 0; pc < n; pc++) {
|
|
switch (proto->code[pc].opcode()) {
|
|
case OP_LUA_CALL:
|
|
case OP_LUA_TAILCALL:
|
|
case OP_LUA_SELF:
|
|
case OP_LUA_SETTABUP:
|
|
case OP_LUA_TESTSET:
|
|
return LUA_BC_HAS_LOOP;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return LUA_BC_ELIGIBLE;
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Pass 1: find basic block boundaries
|
|
// ---------------------------------------------------------------
|
|
|
|
// The block leaders a single instruction induces, written into out[] (at most
|
|
// two). find_block_starts() and lua_bool_fuse_at() both consult this, so the
|
|
// two passes cannot disagree about where the blocks are -- a disagreement is
|
|
// what #1421 was: the lowering skipped past a leader the CFG had recorded.
|
|
static int insn_leaders(const lua_bc_proto *proto, int pc, int n, int out[2]) {
|
|
const lua_bc_instruction &insn = proto->code[pc];
|
|
int cnt = 0;
|
|
|
|
switch (insn.opcode()) {
|
|
case OP_LUA_JMP:
|
|
out[cnt++] = pc + 1 + insn.sJ();
|
|
out[cnt++] = pc + 1;
|
|
break;
|
|
// Lua 5.4 numeric for: both operands are UNSIGNED Bx with an implicit
|
|
// direction -- FORPREP skips FORWARD past the whole loop (Bx+1) when
|
|
// the trip count is zero and otherwise falls into the body; FORLOOP
|
|
// jumps BACK by Bx to the body. The 5.3-era sBx read produced targets
|
|
// tens of thousands of instructions away, which is why the (then
|
|
// unreachable) lowering declined the moment the eligibility reject was
|
|
// lifted (#1732).
|
|
case OP_LUA_FORPREP:
|
|
out[cnt++] = pc + 1 + insn.Bx() + 1; // zero-trip skip target
|
|
out[cnt++] = pc + 1; // body
|
|
break;
|
|
case OP_LUA_FORLOOP:
|
|
out[cnt++] = pc + 1 - insn.Bx(); // body (back edge)
|
|
out[cnt++] = pc + 1; // exit
|
|
break;
|
|
case OP_LUA_TFORPREP:
|
|
case OP_LUA_TFORLOOP:
|
|
// Rejected by eligibility; offsets kept only for the leader map.
|
|
out[cnt++] = pc + 1 + insn.sBx();
|
|
out[cnt++] = pc + 1;
|
|
break;
|
|
case OP_LUA_EQ:
|
|
case OP_LUA_LT:
|
|
case OP_LUA_LE:
|
|
case OP_LUA_EQI:
|
|
case OP_LUA_LTI:
|
|
case OP_LUA_LEI:
|
|
case OP_LUA_GTI:
|
|
case OP_LUA_GEI:
|
|
case OP_LUA_TEST:
|
|
case OP_LUA_TESTSET:
|
|
// "if (cond ~= k) then pc++" -- the skip lands at pc+2, and the JMP
|
|
// it skipped is its own block.
|
|
out[cnt++] = pc + 1;
|
|
out[cnt++] = pc + 2;
|
|
break;
|
|
case OP_LUA_EQK:
|
|
// #1761: EQK is "if ((R[A]==K[B]) ~= k) then pc++" -- one-instruction
|
|
// skip, NOT the EQ+JMP fuse. pc+1 is the skipped insn (often JMP),
|
|
// pc+2 is the fall-through after the skip. Omitting these leaders
|
|
// left false_target in the SAME block as the EQK, so BRC's false
|
|
// edge was a self-loop (dispatch-limit hang) and the fall-through
|
|
// return was unreachable. Masked by interpreter re-run until
|
|
// Phase 4 removed it.
|
|
out[cnt++] = pc + 1;
|
|
out[cnt++] = pc + 2;
|
|
break;
|
|
case OP_LUA_LFALSESKIP:
|
|
// "R[A] := false; pc++". The skip is control flow, not a linear
|
|
// step: the instruction it jumps over belongs to the other path.
|
|
// Lowering it as a bare pc++ swallowed that leader whole (#1421).
|
|
out[cnt++] = pc + 1;
|
|
out[cnt++] = pc + 2;
|
|
break;
|
|
case OP_LUA_RETURN:
|
|
case OP_LUA_RETURN0:
|
|
case OP_LUA_RETURN1:
|
|
// Do NOT mark pc+1 as a leader. Lua always appends a trailing
|
|
// return after an explicit one; treating it as a new block made
|
|
// every returning chunk multi_block (budget STORE_Q + SSA) and
|
|
// contributed to the #1309 hang class on otherwise linear code.
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Drop out-of-range targets; malformed bytecode is declined elsewhere.
|
|
int keep = 0;
|
|
for (int i = 0; i < cnt; i++) {
|
|
if (out[i] > 0 && out[i] < n) out[keep++] = out[i];
|
|
}
|
|
return keep;
|
|
}
|
|
|
|
// Is `pc` the head of the four-instruction idiom Lua emits to materialize a
|
|
// condition as a value (lcode.c exp2reg / code_loadbool)?
|
|
//
|
|
// pc : <test> if (cond ~= k) then pc++
|
|
// pc+1 : JMP -> pc+3
|
|
// pc+2 : LFALSESKIP A R[A] := false; pc++ (skips pc+3)
|
|
// pc+3 : LOADTRUE A R[A] := true
|
|
//
|
|
// The whole run is just R[A] = (cond == k) with no control flow, so fusing
|
|
// it to a bare comparison is both correct and branchless. A compound
|
|
// condition (`a<b and c<d`) patches its own jump list into pc+2/pc+3, and
|
|
// then the run is a real join and must not be fused -- so require that
|
|
// nothing outside the run enters it.
|
|
//
|
|
// Only the comparison opcodes are fused. TEST/TESTSET are excluded: TESTSET
|
|
// also copies R[B] into R[A] on the taken path, so its value is not simply
|
|
// the branch condition.
|
|
static bool lua_bool_fuse_at(const lua_bc_proto *proto, int pc, int n,
|
|
int *dst_reg) {
|
|
switch (proto->code[pc].opcode()) {
|
|
case OP_LUA_EQ: case OP_LUA_LT: case OP_LUA_LE:
|
|
case OP_LUA_EQK: // same condjump+LFALSESKIP/LOADTRUE shape as EQ (#1764)
|
|
case OP_LUA_EQI: case OP_LUA_LTI: case OP_LUA_LEI:
|
|
case OP_LUA_GTI: case OP_LUA_GEI:
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
if (pc + 3 >= n) return false;
|
|
|
|
const lua_bc_instruction &jmp = proto->code[pc + 1];
|
|
const lua_bc_instruction &lfs = proto->code[pc + 2];
|
|
const lua_bc_instruction <r = proto->code[pc + 3];
|
|
if (jmp.opcode() != OP_LUA_JMP) return false;
|
|
if (pc + 2 + jmp.sJ() != pc + 3) return false;
|
|
if (lfs.opcode() != OP_LUA_LFALSESKIP) return false;
|
|
if (ltr.opcode() != OP_LUA_LOADTRUE) return false;
|
|
if (lfs.A() != ltr.A()) return false;
|
|
|
|
// No entry into pc+1 .. pc+3 from outside the run itself.
|
|
for (int j = 0; j < n; j++) {
|
|
if (j >= pc && j <= pc + 2) continue; // the run's own branches
|
|
int tgt[2];
|
|
int cnt = insn_leaders(proto, j, n, tgt);
|
|
for (int i = 0; i < cnt; i++) {
|
|
if (tgt[i] >= pc + 1 && tgt[i] <= pc + 3) return false;
|
|
}
|
|
}
|
|
|
|
*dst_reg = lfs.A();
|
|
return true;
|
|
}
|
|
|
|
static void find_block_starts(const lua_bc_proto *proto,
|
|
std::vector<bool> &is_leader) {
|
|
int n = static_cast<int>(proto->code.size());
|
|
is_leader.assign(n, false);
|
|
if (n > 0) is_leader[0] = true;
|
|
|
|
for (int pc = 0; pc < n; pc++) {
|
|
int dst;
|
|
if (lua_bool_fuse_at(proto, pc, n, &dst)) {
|
|
// Fused to a value in pass 2 -- the run has no control flow, so
|
|
// it must not induce leaders here either.
|
|
pc += 3;
|
|
continue;
|
|
}
|
|
int tgt[2];
|
|
int cnt = insn_leaders(proto, pc, n, tgt);
|
|
for (int i = 0; i < cnt; i++) is_leader[tgt[i]] = true;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Block mapping
|
|
// ---------------------------------------------------------------
|
|
|
|
static int assign_blocks(const std::vector<bool> &is_leader,
|
|
std::vector<int> &pc_to_block, int n) {
|
|
int block_count = 0;
|
|
pc_to_block.resize(n, -1);
|
|
for (int pc = 0; pc < n; pc++) {
|
|
if (is_leader[pc]) block_count++;
|
|
pc_to_block[pc] = block_count - 1;
|
|
}
|
|
return block_count;
|
|
}
|
|
|
|
// The mux.* SENTINEL: `mux` and `mux.args` are lowered as SCONSTs holding
|
|
// their own NAMES, not as Lua values. The separate provenance bit is
|
|
// essential: `"mux.args"` is also perfectly ordinary Lua string text and
|
|
// must never enter the CARGS fast path just because its bytes match.
|
|
//
|
|
static inline bool lua_is_mux_sentinel(const hir_program &h, int v) {
|
|
return v >= 0 && h.kind[v] == HIR_SCONST && h.lua_mux_sentinel[v];
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Helper: coerce a return value to TY_STRING for HIR_RET.
|
|
//
|
|
// Known ICONST/FCONST fold to SCONST digit strings so pure `return 42`
|
|
// can take the folded (needs_jit=false) path. Runtime ITOA/FTOA is only
|
|
// used when the value is not a compile-time constant (#1309).
|
|
// ---------------------------------------------------------------
|
|
|
|
// Render a double the way Lua's own tostring does (lobject.c tostringbuff).
|
|
// Lua formats with LUA_NUMBER_FMT -- "%.14g" for the double build -- and then
|
|
// appends ".0" to anything that came out looking like an integer, so a float
|
|
// whose value happens to be integral prints as "3.0" and stays distinguishable
|
|
// from the integer 3. The compiled path used "%.17g" and never appended,
|
|
// so every integral float lost its subtype and every other float printed more
|
|
// digits than the interpreter (#1488).
|
|
//
|
|
void lua_format_double(double d, char *buf, size_t sz) {
|
|
mux_snprintf(reinterpret_cast<UTF8 *>(buf), sz, T("%.14g"), d);
|
|
if (buf[strspn(buf, "-0123456789")] == '\0') {
|
|
size_t len = strlen(buf);
|
|
if (len + 3 <= sz) {
|
|
buf[len] = '.';
|
|
buf[len + 1] = '0';
|
|
buf[len + 2] = '\0';
|
|
}
|
|
}
|
|
}
|
|
|
|
static int return_as_string(hir_program &h, rv_compiler &rc, int rv) {
|
|
if (rv < 0) return -1;
|
|
// A mux table sentinel is not the string containing its name.
|
|
if (lua_is_mux_sentinel(h, rv)) return -1;
|
|
// A CALL_VAL result is a live Lua value on the stack: marshal with
|
|
// fun_lua rules at the softcode boundary (#1764 shape 2). Other
|
|
// handles (tables, callables) still must not escape as decimal indices.
|
|
if (h.ty[rv] == TY_LUA_HANDLE) {
|
|
if (h.kind[rv] == HIR_LUA_CALL_VAL) {
|
|
h.needs_jit = true;
|
|
return h.emit(HIR_LUA_MARSHAL, TY_STRING, rv);
|
|
}
|
|
return -1;
|
|
}
|
|
if (h.ty[rv] == TY_STRING) {
|
|
return rv;
|
|
}
|
|
if (h.ty[rv] == TY_INT) {
|
|
if (h.kind[rv] == HIR_ICONST) {
|
|
char buf[32];
|
|
mux_snprintf(reinterpret_cast<UTF8 *>(buf), sizeof(buf), T("%lld"),
|
|
static_cast<long long>(h.val[rv]));
|
|
uint64_t addr = rc.pool_str(buf, strlen(buf));
|
|
return h.emit_sconst(addr, buf);
|
|
}
|
|
// Runtime ITOA has no sval. Without needs_jit the folded path
|
|
// would return that empty string -- `return not a` answered ""
|
|
// while the interpreter answered "0"/"1".
|
|
h.needs_jit = true;
|
|
return h.emit(HIR_ITOA, TY_STRING, rv);
|
|
}
|
|
if (h.ty[rv] == TY_FLOAT) {
|
|
if (h.kind[rv] == HIR_FCONST) {
|
|
char buf[64];
|
|
lua_format_double(h.fval[rv], buf, sizeof(buf));
|
|
uint64_t addr = rc.pool_str(buf, strlen(buf));
|
|
return h.emit_sconst(addr, buf);
|
|
}
|
|
// Lua float, so Lua's rendering -- not HIR_FTOA, which formats the
|
|
// MUX way and would drop the ".0" at run time just as the fold used
|
|
// to at compile time (#1488).
|
|
h.needs_jit = true;
|
|
return h.emit(HIR_LUA_FTOA, TY_STRING, rv);
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
// The return half of a lowered OP_TAILCALL: `return f(...)` is the call the
|
|
// OP_LUA_CALL case just emitted, then this. One helper because the call
|
|
// body has two successful exits (the direct ECALL path and the general
|
|
// path) and both must finish the same way.
|
|
//
|
|
static int lua_tailcall_ret(hir_program &h, rv_compiler &rc, int v,
|
|
int &result_val) {
|
|
int rv = return_as_string(h, rc, v);
|
|
if (rv < 0) return -1;
|
|
h.emit(HIR_RET, TY_VOID, rv);
|
|
if (result_val < 0) {
|
|
result_val = rv;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Helper: load a Lua constant into HIR.
|
|
// ---------------------------------------------------------------
|
|
|
|
static int emit_lua_constant(hir_program &h, rv_compiler &rc,
|
|
const lua_bc_constant &k) {
|
|
switch (k.type) {
|
|
case LUA_BC_TNIL:
|
|
return h.emit_sconst(rc.pool_str("", 0), "");
|
|
case LUA_BC_TFALSE:
|
|
return h.emit_iconst(0);
|
|
case LUA_BC_TTRUE:
|
|
return h.emit_iconst(1);
|
|
case LUA_BC_TINT:
|
|
return h.emit_iconst(k.ival);
|
|
case LUA_BC_TFLOAT:
|
|
// A Lua float stays a float even when its value is integral. This
|
|
// used to demote 2.0 to ICONST "for compatibility with integer
|
|
// arithmetic", but Lua 5.4's integer/float distinction is observable
|
|
// -- tostring(2.0) is "2.0", math.type(2.0) is "float", and a float
|
|
// operand makes the whole expression float. Demoting it made
|
|
// `a * 1.0` an integer multiply and `a + 0.0` print "3" (#1488).
|
|
return h.emit_fconst(k.fval);
|
|
case LUA_BC_TSHRSTR:
|
|
case LUA_BC_TLNGSTR: {
|
|
uint64_t addr = rc.pool_str(k.sval.c_str(), k.sval.size());
|
|
return h.emit_sconst(addr, k.sval);
|
|
}
|
|
default:
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Helper: promote an operand to TY_FLOAT if needed.
|
|
// Returns the (possibly new) HIR value index, or -1 on error.
|
|
// ---------------------------------------------------------------
|
|
|
|
// A CALL_STR result is a Lua value marshalled to text the way fun_lua
|
|
// renders a chunk return (nil→"", bool→"0"/"1", else tolstring). That is
|
|
// correct only where the value leaves Lua. Inside the chunk the Lua type
|
|
// is gone: truthiness, ==, and arithmetic then run under softcode / string
|
|
// rules and answer wrongly for values that are truthy in Lua but MUSH-falsy
|
|
// as text ("0", "", integer 0), or for bool/nil that only look right by
|
|
// coincidence of the marshal. #1764: treat the producer as provenance and
|
|
// refuse to consume it under Lua semantics -- the interpreter answers.
|
|
//
|
|
// The mux.* sentinel fiction is what buys the
|
|
// native CARGS fast path -- GETTABI on the "mux.args" sentinel becomes an
|
|
// ALOAD with no ECALL -- and it is sound only while the sentinel is
|
|
// consumed AS a sentinel. The moment one reaches an operation that treats
|
|
// it as a value, the program is working with the string "mux.args".
|
|
//
|
|
// It had leaked into five of them (#1795), every one executing and silent:
|
|
// type() said "string", tostring() said "mux.args", concat spliced the
|
|
// name in where Lua raises, == compared it to the literal text, and #mux
|
|
// answered 3 -- #1424's shape (a length taken over the NAME of a thing)
|
|
// on a different value class.
|
|
//
|
|
// Same rule as lua_is_marshalled_str and lua_is_nil below: a
|
|
// representation that lies about its type must be refused by every
|
|
// consumer that would believe it.
|
|
static inline bool lua_is_marshalled_str(const hir_program &h, int v) {
|
|
return v >= 0 && h.kind[v] == HIR_LUA_CALL_STR;
|
|
}
|
|
|
|
// Lua truthiness provenance. HIR collapses false and integer 0 to the
|
|
// same ICONST 0, and nil and "" to the same empty SCONST. HIR_BOOL is
|
|
// integer SNEZ (softcode truthiness), so `local a=0 if a` answered falsy
|
|
// while Lua requires truthy. Tags restore the distinction at TEST/NOT:
|
|
//
|
|
// VALUE — numbers, real strings, floats: always truthy in Lua
|
|
// BOOL — 0/1 with boolean semantics (LOADTRUE/FALSE, comparisons)
|
|
// NIL — always falsy (LOADNIL, or a known-absent plain-table read)
|
|
// UNKNOWN — tag lost or never known (loop-carried LOAD_Q, …). TEST/NOT
|
|
// must decline rather than default to VALUE: the VALUE default
|
|
// is the unsafe direction (lost BOOL/NIL → silent truthy, the
|
|
// bug class #1765 fixed, re-entering through the back door).
|
|
// #1768.
|
|
//
|
|
enum lua_truth : uint8_t {
|
|
LUA_TRUTH_VALUE = 0,
|
|
LUA_TRUTH_BOOL = 1,
|
|
LUA_TRUTH_NIL = 2,
|
|
LUA_TRUTH_UNKNOWN = 3,
|
|
};
|
|
|
|
typedef std::map<int, lua_truth> lua_truth_map;
|
|
|
|
static void lua_truth_set(lua_truth_map &m, int v, lua_truth t) {
|
|
if (v >= 0) {
|
|
m[v] = t;
|
|
}
|
|
}
|
|
|
|
// Tag a HIR value produced from a Lua pool constant. TNIL/TFALSE/TTRUE
|
|
// share HIR representations with "" / 0 / 1; without the tag, EQK and
|
|
// TEST invent softcode semantics (nil == "" was true compiled).
|
|
//
|
|
static void lua_truth_tag_constant(lua_truth_map &truth,
|
|
const lua_bc_constant &k, int v) {
|
|
if (v < 0) {
|
|
return;
|
|
}
|
|
switch (k.type) {
|
|
case LUA_BC_TNIL:
|
|
lua_truth_set(truth, v, LUA_TRUTH_NIL);
|
|
break;
|
|
case LUA_BC_TFALSE:
|
|
case LUA_BC_TTRUE:
|
|
lua_truth_set(truth, v, LUA_TRUTH_BOOL);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Lua TYPE classes, for equality. Lua's == is false across types --
|
|
// 0 ~= false, "5" ~= 5, nil ~= "" -- but HIR represents false and 0 as
|
|
// the same ICONST, nil and "" as the same empty SCONST, and coerces
|
|
// strings to numbers when comparing. Equality must therefore compare
|
|
// types before representations.
|
|
//
|
|
enum lua_type_class {
|
|
LUA_TC_NIL,
|
|
LUA_TC_BOOL,
|
|
LUA_TC_NUMBER,
|
|
LUA_TC_STRING,
|
|
LUA_TC_OTHER, // handles etc. -- refused before this matters
|
|
};
|
|
|
|
static lua_type_class lua_type_class_of_const(const lua_bc_constant &k) {
|
|
switch (k.type) {
|
|
case LUA_BC_TNIL: return LUA_TC_NIL;
|
|
case LUA_BC_TFALSE:
|
|
case LUA_BC_TTRUE: return LUA_TC_BOOL;
|
|
case LUA_BC_TINT:
|
|
case LUA_BC_TFLOAT: return LUA_TC_NUMBER;
|
|
case LUA_BC_TSHRSTR:
|
|
case LUA_BC_TLNGSTR: return LUA_TC_STRING;
|
|
default: return LUA_TC_OTHER;
|
|
}
|
|
}
|
|
|
|
static lua_truth lua_truth_of(const hir_program &h, const lua_truth_map &m,
|
|
int v) {
|
|
if (v < 0) {
|
|
return LUA_TRUTH_UNKNOWN;
|
|
}
|
|
lua_truth_map::const_iterator it = m.find(v);
|
|
if (it != m.end()) {
|
|
return it->second;
|
|
}
|
|
// Comparison / NOT / BOOL results are 0/1 booleans even without a mark.
|
|
switch (h.kind[v]) {
|
|
case HIR_EQ: case HIR_NE: case HIR_LT: case HIR_LE:
|
|
case HIR_GT: case HIR_GE:
|
|
case HIR_FEQ: case HIR_FLT: case HIR_FLE:
|
|
case HIR_BOOL: case HIR_NOT:
|
|
return LUA_TRUTH_BOOL;
|
|
// Loop-carried q-register reloads (#1732): the value came from a
|
|
// store in another block; its truth class was not carried with it.
|
|
case HIR_LOAD_Q:
|
|
return LUA_TRUTH_UNKNOWN;
|
|
default:
|
|
// Untagged ICONST/arith/SCONST: VALUE is correct for numbers and
|
|
// real strings. Producers of BOOL/NIL must tag explicitly.
|
|
return LUA_TRUTH_VALUE;
|
|
}
|
|
}
|
|
|
|
// A value KNOWN to be Lua nil at lowering: LOADNIL, or a known-absent
|
|
// read of a plain table with a closed key set. nil is representable in
|
|
// HIR only as the empty SCONST, which is also a real "" -- the truth tag
|
|
// is what tells them apart, so every consumer Lua would reject on nil
|
|
// must consult it here.
|
|
//
|
|
// Producers are easy to get right and consumers are easy to forget: the
|
|
// first cut of this change taught `"a" .. t[2]` to answer "a" where Lua
|
|
// raises, `#t[2]` to answer 0, and tostring(t[2]) to answer "" instead of
|
|
// "nil". A new value class needs a consumer audit, not just correct
|
|
// producers (#1751 review note).
|
|
//
|
|
static inline bool lua_is_nil(const hir_program &h, const lua_truth_map &m,
|
|
int v) {
|
|
return lua_truth_of(h, m, v) == LUA_TRUTH_NIL;
|
|
}
|
|
|
|
// Lua type class of a lowered VALUE. The truth tag carries exactly the
|
|
// distinctions HIR erases; everything else follows the HIR type.
|
|
//
|
|
static lua_type_class lua_type_class_of_value(const hir_program &h,
|
|
const lua_truth_map &m,
|
|
int v) {
|
|
if (v < 0) return LUA_TC_OTHER;
|
|
// A mux.* sentinel is really a TABLE; classifying it by its SCONST
|
|
// representation would compare it against the literal "mux.args".
|
|
if (lua_is_mux_sentinel(h, v)) return LUA_TC_OTHER;
|
|
const lua_truth t = lua_truth_of(h, m, v);
|
|
if (t == LUA_TRUTH_NIL) return LUA_TC_NIL;
|
|
if (t == LUA_TRUTH_BOOL) return LUA_TC_BOOL;
|
|
if (t == LUA_TRUTH_UNKNOWN) return LUA_TC_OTHER; // caller declines
|
|
switch (h.ty[v]) {
|
|
case TY_INT:
|
|
case TY_FLOAT: return LUA_TC_NUMBER;
|
|
case TY_STRING: return LUA_TC_STRING;
|
|
default: return LUA_TC_OTHER;
|
|
}
|
|
}
|
|
|
|
// Encode a non-handle operand for HIR_LUA_EQ (kind in val): 0=int, 1=str,
|
|
// 3=nil, 4=bool. Returns the rhs HIR value index, or -1 if unencodable.
|
|
// #1835: NIL/BOOL must not collapse to kind 1/0 via empty SCONST / ICONST.
|
|
//
|
|
static int lua_eq_kind_of_value(hir_program &h, const lua_truth_map &m,
|
|
int v, int *kind_out) {
|
|
const lua_truth tr = lua_truth_of(h, m, v);
|
|
if (tr == LUA_TRUTH_NIL) {
|
|
*kind_out = 3;
|
|
return h.emit_iconst(0);
|
|
}
|
|
if (tr == LUA_TRUTH_BOOL) {
|
|
*kind_out = 4;
|
|
return v;
|
|
}
|
|
if (h.kind[v] == HIR_SCONST) {
|
|
*kind_out = 1;
|
|
return v;
|
|
}
|
|
if (h.ty[v] == TY_INT) {
|
|
*kind_out = 0;
|
|
return v;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static int promote_to_float(hir_program &h, const lua_truth_map &truth, int v) {
|
|
if (v < 0) return -1;
|
|
if (lua_is_marshalled_str(h, v)) return -1;
|
|
if (lua_is_mux_sentinel(h, v)) return -1;
|
|
// nil is not a number; ATOI("") would invent 0.
|
|
if (lua_truth_of(h, truth, v) == LUA_TRUTH_NIL) return -1;
|
|
if (h.ty[v] == TY_FLOAT) return v;
|
|
if (h.ty[v] == TY_INT) {
|
|
return h.emit(HIR_ITOF, TY_FLOAT, v);
|
|
}
|
|
if (h.ty[v] == TY_STRING) {
|
|
int as_int = h.emit(HIR_ATOI, TY_INT, v);
|
|
if (as_int < 0) return -1;
|
|
return h.emit(HIR_ITOF, TY_FLOAT, as_int);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Helper: promote an operand to TY_INT if needed.
|
|
// TY_STRING → HIR_ATOI. TY_INT passes through.
|
|
// TY_FLOAT returns -1 (use promote_to_float instead).
|
|
// ---------------------------------------------------------------
|
|
|
|
static int promote_to_int(hir_program &h, const lua_truth_map &truth, int v) {
|
|
if (v < 0) return -1;
|
|
if (lua_is_marshalled_str(h, v)) return -1;
|
|
if (lua_is_mux_sentinel(h, v)) return -1;
|
|
if (lua_truth_of(h, truth, v) == LUA_TRUTH_NIL) return -1;
|
|
if (h.ty[v] == TY_INT) return v;
|
|
if (h.ty[v] == TY_STRING) return h.emit(HIR_ATOI, TY_INT, v);
|
|
return -1;
|
|
}
|
|
|
|
// Returns true if either operand is TY_FLOAT (i.e., need float arithmetic).
|
|
//
|
|
static bool either_float(hir_program &h, int a, int b) {
|
|
return (a >= 0 && h.ty[a] == TY_FLOAT)
|
|
|| (b >= 0 && h.ty[b] == TY_FLOAT);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Helper: emit a comparison + branch pattern.
|
|
// Many Lua comparison opcodes share the same structure:
|
|
// compare → optional negate (k bit) → read JMP → emit BRC
|
|
// ---------------------------------------------------------------
|
|
|
|
static inline bool lua_reg_in_range(int idx); // defined with pass 2
|
|
|
|
// A Lua handle is a reference into the VM, not a value (#1579). Arithmetic,
|
|
// comparison, length, concatenation and returning are all illegal on one, so
|
|
// decline the chunk rather than let a stack index flow on as though it were
|
|
// the thing it points at -- which is what made `#t` answer 22 (#1424).
|
|
//
|
|
// Declining here is strictly better than the run-time bail that #1518's
|
|
// fail-closed intercept produces today: it costs no compile-and-run, and it
|
|
// does not depend on the bridge ECALL names staying unimplemented.
|
|
//
|
|
// What a handle refers to -- its REFERENT -- tracked per HIR value while
|
|
// lowering. The type system says "handle"; this says handle to WHAT, which
|
|
// is the question two decision sites had each been answering by inspecting
|
|
// provenance:
|
|
//
|
|
// * OP_LUA_GETFIELD chose GETFIELD_REF vs GETFIELD_INT by whether the
|
|
// table handle's producing instruction was GETGLOBAL, and
|
|
// * OP_LUA_CALL chose CALL_INT vs CALL_STR by walking back to the SCONST
|
|
// key that named the callee and consulting a whitelist -- which then had
|
|
// to gate BOTH branches (d5e5e86e0), or a name skipping one could fall
|
|
// through and claim the other's result type.
|
|
//
|
|
// Now the claim is made once, where the handle is created, and the decision
|
|
// sites read it. A claim is ELIGIBILITY, not soundness: every ECALL
|
|
// verifies at runtime (lua_isfunction before calling, lua_isinteger and
|
|
// LUA_TSTRING on results) and declines to the interpreter on a miss, so a
|
|
// wrong claim costs a bail, never a wrong answer. That is what lets
|
|
// TY_STRING be the open default for a name nothing here knows -- a
|
|
// game-defined global that does return a string compiles and runs, and one
|
|
// that does not declines exactly where it always did.
|
|
//
|
|
// A library member that is a VALUE rather than a function -- math.pi,
|
|
// math.maxinteger. Reading one takes the value directly (GETFIELD_INT /
|
|
// GETFIELD_FLT on the library table) instead of a reference nothing could
|
|
// consume. Function members deliberately do NOT appear here: their return
|
|
// claims stay in lua_call_claim, so each fact lives once.
|
|
//
|
|
struct lua_lib_value {
|
|
const char *name;
|
|
hir_type ty; // TY_INT or TY_FLOAT
|
|
};
|
|
|
|
static const lua_lib_value k_lua_math_values[] = {
|
|
{"maxinteger", TY_INT},
|
|
{"mininteger", TY_INT},
|
|
{"pi", TY_FLOAT},
|
|
{"huge", TY_FLOAT},
|
|
{nullptr, TY_VOID},
|
|
};
|
|
|
|
struct lua_referent {
|
|
// Field reads: false means members are values (GETFIELD_INT -- the
|
|
// NEWTABLE shape), true means members are references (GETFIELD_REF --
|
|
// the library shape). GETGLOBAL results are the only handles whose
|
|
// fields are taken by reference, same as the provenance test chose.
|
|
bool fields_are_refs = false;
|
|
|
|
// Calls: may one be attempted, and what does it claim to return?
|
|
// TY_INT and TY_STRING are the two marshallings that exist; a handle
|
|
// that never received a callable claim declines the call at compile
|
|
// time, which is where a NEWTABLE or a nested-field handle lands.
|
|
bool callable = false;
|
|
hir_type returns = TY_VOID;
|
|
|
|
// Callee name when known at the GETGLOBAL/GETFIELD site (e.g. "tonumber").
|
|
// Used for call-site result specialisation (#1866 fast path): a fixed
|
|
// returns claim cannot express "int or float depending on the arg".
|
|
//
|
|
std::string call_name;
|
|
|
|
// The callee may have side effects the player can observe
|
|
// (mux.notify/pemit/set/eval). Recorded for diagnostics and for
|
|
// any future purity analysis; it is NOT a compile-time refuse.
|
|
// #1751 Phase 4 deleted post-entry re-run, so an effect delivered
|
|
// compiled is not re-delivered by a silent interpreter retry — the
|
|
// reason #1750 made these ineligible. Both routes share the same
|
|
// bridge C functions under the same permissions now.
|
|
bool effectful = false;
|
|
|
|
// Known VALUE members, for a recognized standard-library table; null
|
|
// for everything else. Like every claim here it is eligibility only:
|
|
// a game that rebinds math.pi to a string declines at the runtime
|
|
// check, not answers wrongly.
|
|
const lua_lib_value *values = nullptr;
|
|
|
|
// PROVEN plain: created by NEWTABLE in this chunk and never passed as
|
|
// a call argument since. Only such a table may take the typed
|
|
// integer-keyed fast reads (GETI / GETFIELD_INT / GETFIELD_FLT):
|
|
// their ok=0 claim-miss path continues with 0, which is sound only
|
|
// when no metamethod can fire and no non-integer value can have been
|
|
// stored -- both guaranteed by construction here (compiled stores are
|
|
// integer-only, plain tables have no __index) and by NOTHING for a
|
|
// handle from anywhere else. #1751's Phase 1 review proved the miss:
|
|
// an interpreter-installed __index on a global made compiled `T[1]`
|
|
// answer 0 where the interpreter answered "s", silently. Escaping
|
|
// as a call argument clears the proof: the callee can setmetatable.
|
|
bool plain_proven = false;
|
|
|
|
// Closed integer-key set for a plain table: every compiled store used
|
|
// a constant key recorded in int_keys. A dynamic-key store clears
|
|
// keys_closed. When closed, a GETI of a constant key not in the set
|
|
// is known-absent at lowering — emit nil, no post-entry GETI_INT
|
|
// miss (the residual loud site for `return t[2]` after `t[1]=5`).
|
|
bool keys_closed = false;
|
|
std::set<int64_t> int_keys;
|
|
};
|
|
|
|
static hir_type lua_lib_value_type(const lua_referent &t,
|
|
const std::string &key) {
|
|
if (nullptr == t.values) return TY_VOID;
|
|
for (const lua_lib_value *v = t.values; v->name != nullptr; v++) {
|
|
if (key == v->name) return v->ty;
|
|
}
|
|
return TY_VOID;
|
|
}
|
|
|
|
typedef std::map<int, lua_referent> lua_ref_map;
|
|
|
|
static lua_referent lua_referent_of(const lua_ref_map &m, int v) {
|
|
lua_ref_map::const_iterator it = m.find(v);
|
|
return (it == m.end()) ? lua_referent() : it->second;
|
|
}
|
|
|
|
// Record a constant integer-key store on a plain table. No-op if the
|
|
// handle is not plain or keys are already open.
|
|
//
|
|
static void lua_note_int_key_store(lua_ref_map &m, int tbl, int64_t key) {
|
|
lua_ref_map::iterator it = m.find(tbl);
|
|
if (it == m.end() || !it->second.plain_proven) {
|
|
return;
|
|
}
|
|
if (!it->second.keys_closed) {
|
|
return;
|
|
}
|
|
it->second.int_keys.insert(key);
|
|
}
|
|
|
|
// A non-constant integer key store: the closed set is no longer complete.
|
|
//
|
|
static void lua_note_dynamic_int_key_store(lua_ref_map &m, int tbl) {
|
|
lua_ref_map::iterator it = m.find(tbl);
|
|
if (it == m.end() || !it->second.plain_proven) {
|
|
return;
|
|
}
|
|
it->second.keys_closed = false;
|
|
it->second.int_keys.clear();
|
|
}
|
|
|
|
// The standard-library knowledge, in one place: what does calling NAME
|
|
// return? HIR result types are static and Lua's are not -- math.max(3,9)
|
|
// and tostring(42) take the same argument shapes and return different
|
|
// types -- so the name is the only thing that carries it, and this list is
|
|
// a claim about the standard library. Names claiming TY_INT stay few and
|
|
// deliberate; everything else claims TY_STRING, the open default the
|
|
// runtime check makes safe.
|
|
//
|
|
static hir_type lua_call_claim(const std::string &name) {
|
|
// #1866: tonumber is NOT on this list. Lua 5.4 returns an integer
|
|
// for integral inputs and a float for non-integral ones; claiming
|
|
// TY_INT always emitted CALL_INT, and ECALL_LUA_CALL_INT declines
|
|
// when lua_isinteger is false -- a post-entry residual after the
|
|
// function has already run. Default claim routes CALL_VAL; the
|
|
// call site upgrades to CALL_INT when the argument is a proven
|
|
// integer (HIR INT or integral string constant) so the hot path
|
|
// stays native.
|
|
//
|
|
static const char *kIntReturning[] = {
|
|
"floor", "ceil", "max", "min", "abs", "tointeger",
|
|
"len", "byte", "maxinteger", "mininteger",
|
|
};
|
|
for (const char *n : kIntReturning) {
|
|
if (name == n) return TY_INT;
|
|
}
|
|
// FLOAT-returning stdlib names. No CALL_FLT marshalling exists, so
|
|
// a FLOAT claim makes the call ineligible at lowering and the
|
|
// interpreter answers -- silently, with the right subtype. Before
|
|
// this list, sqrt claimed the STRING default and its miss was a
|
|
// post-entry fail (smoke TC046 under Phase 0).
|
|
//
|
|
// tonumber is also not here: a FLOAT claim would decline the whole
|
|
// call, including the common tonumber("17") integer case.
|
|
static const char *kFloatReturning[] = {
|
|
"sqrt", "exp", "log", "sin", "cos", "tan",
|
|
"asin", "acos", "atan", "fmod", "rad", "deg",
|
|
"random", // float in the no-argument form
|
|
};
|
|
for (const char *n : kFloatReturning) {
|
|
if (name == n) return TY_FLOAT;
|
|
}
|
|
return TY_STRING;
|
|
}
|
|
|
|
// #1866: when is tonumber(arg) guaranteed to return a Lua integer?
|
|
// - HIR integer (tonumber(3) → integer)
|
|
// - SCONST of optional '-' + digits only, small enough to fit int64
|
|
// (tonumber("17") → integer; tonumber("3.0") / "3.5" / "1e2" stay on
|
|
// CALL_VAL, and an overflowing literal must too -- see below)
|
|
// Runtime strings and floats take CALL_VAL.
|
|
//
|
|
static bool lua_tonumber_arg_is_integral(const hir_program &h, int areg) {
|
|
if (areg < 0) {
|
|
return false;
|
|
}
|
|
if (h.ty[areg] == TY_INT) {
|
|
return true;
|
|
}
|
|
if (h.kind[areg] != HIR_SCONST) {
|
|
return false;
|
|
}
|
|
const std::string &s = h.sval[areg];
|
|
if (s.empty()) {
|
|
return false;
|
|
}
|
|
size_t i = 0;
|
|
if (s[0] == '-' || s[0] == '+') {
|
|
i = 1;
|
|
if (i >= s.size()) {
|
|
return false;
|
|
}
|
|
}
|
|
const size_t start = i;
|
|
for (; i < s.size(); i++) {
|
|
if (s[i] < '0' || s[i] > '9') {
|
|
return false;
|
|
}
|
|
}
|
|
// All digits -- but Lua 5.4 returns a FLOAT when an all-digit literal
|
|
// overflows int64 (tonumber("9223372036854775808") -> 9.2e18), so
|
|
// claiming CALL_INT here would post-entry decline where the interpreter
|
|
// answers a float. INT64_MAX has 19 digits; <= 18 significant digits
|
|
// always fits. Bound conservatively rather than parse (#1866 review).
|
|
//
|
|
size_t z = start;
|
|
while (z < s.size() && s[z] == '0') {
|
|
z++;
|
|
}
|
|
if (s.size() - z > 18) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static inline bool lua_is_handle(const hir_program &h, int v) {
|
|
return v >= 0 && h.ty[v] == TY_LUA_HANDLE;
|
|
}
|
|
|
|
static int emit_cmp_branch(hir_program &h, int cmp, int k_bit,
|
|
const lua_bc_proto *proto, int &pc,
|
|
const std::vector<int> &pc_to_block,
|
|
int cur_hir_block, int n,
|
|
int *lua_reg, bool multi_block) {
|
|
if (cmp < 0) return -1;
|
|
// Lua's conditional ops are "if (cond ~= k) then pc++", and that pc++
|
|
// skips the JMP which follows. So the JMP is taken exactly when
|
|
// cond == k, and falling through to pc+2 is the cond != k case.
|
|
// true_target below is the JMP's destination, so the branch condition
|
|
// must be (cond == k): negate when k is 0, not when it is 1 (#1486).
|
|
//
|
|
// EQK uses the same condjump+JMP shape as EQ/EQI (luaK_codeeq →
|
|
// condjump), so it shares this polarity. An older lowering treated
|
|
// EQK as a bare skip to pc+1/pc+2 with inverted k; that path is gone.
|
|
if (!k_bit) {
|
|
cmp = h.emit(HIR_NOT, TY_INT, cmp);
|
|
if (cmp < 0) return -1;
|
|
}
|
|
|
|
// The condition used as a *value* rather than as a branch: `cmp` already
|
|
// is (cond == k), which is exactly what the LFALSESKIP/LOADTRUE pair
|
|
// computes, so drop the whole run and keep the comparison (#1421).
|
|
int dst;
|
|
if (lua_reg != nullptr && lua_bool_fuse_at(proto, pc, n, &dst)) {
|
|
if (!lua_reg_in_range(dst)) return -1;
|
|
lua_reg[dst] = cmp;
|
|
pc += 2; // LFALSESKIP and LOADTRUE; the caller steps over the JMP
|
|
return 0;
|
|
}
|
|
|
|
// Only a real branch needs more than one block. This guard sits after the
|
|
// fuse on purpose: fusing removes the chunk's only branch, so `return a<b`
|
|
// is single-block by construction and would otherwise decline here.
|
|
if (!multi_block) return -1;
|
|
if (pc + 1 >= n) return -1;
|
|
const lua_bc_instruction &jmp_insn = proto->code[pc + 1];
|
|
if (jmp_insn.opcode() != OP_LUA_JMP) return -1;
|
|
int true_target = pc + 2 + jmp_insn.sJ();
|
|
int false_target = pc + 2;
|
|
int true_blk = (true_target >= 0 && true_target < n) ? pc_to_block[true_target] : -1;
|
|
int false_blk = (false_target >= 0 && false_target < n) ? pc_to_block[false_target] : -1;
|
|
if (true_blk < 0 || false_blk < 0) return -1;
|
|
h.emit(HIR_BRC, TY_VOID, cmp, false_blk, true_blk);
|
|
h.add_edge(cur_hir_block, true_blk);
|
|
h.add_edge(cur_hir_block, false_blk);
|
|
return 0; // success
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Pass 2: emit HIR
|
|
// ---------------------------------------------------------------
|
|
|
|
// Defensive bound for composite register indices (A + offset). Bare A/B/C
|
|
// operands are 8-bit (< MAX_LUA_REGS == 256) and always index lua_reg[]
|
|
// safely, but ranges — LOADNIL's R(A)..R(A+B), CONCAT/SETLIST/CALL argument
|
|
// and result runs, and the TFOR result registers R(A+4)..R(A+3+C) — can
|
|
// reach ~R(A+4+255) with crafted operands, past the end of lua_reg[].
|
|
// Well-formed Lua 5.4 compiler output keeps every register < maxstacksize
|
|
// (<= MAX_LUA_STACK == 64), so this guard only fires on malformed bytecode;
|
|
// bail to the Lua VM (return -1) rather than overrun the map. (Crafted
|
|
// bytecode can't reach this lowering through the text-only sandbox today,
|
|
// but the translation must stay memory-safe regardless.)
|
|
//
|
|
static inline bool lua_reg_in_range(int idx) {
|
|
return idx >= 0 && idx < MAX_LUA_REGS;
|
|
}
|
|
|
|
int hir_lower_lua_proto(hir_program &h, rv_compiler &rc,
|
|
const lua_bc_proto *proto) {
|
|
if (nullptr == proto) return -1;
|
|
|
|
int n = static_cast<int>(proto->code.size());
|
|
if (n == 0) return -1;
|
|
|
|
// Pass 1: find block boundaries.
|
|
std::vector<bool> is_leader;
|
|
find_block_starts(proto, is_leader);
|
|
std::vector<int> pc_to_block;
|
|
int num_blocks = assign_blocks(is_leader, pc_to_block, n);
|
|
|
|
bool multi_block = (num_blocks > 1);
|
|
|
|
// Allocate HIR blocks.
|
|
if (multi_block) {
|
|
for (int b = 1; b < num_blocks; b++) {
|
|
int nb = h.new_block();
|
|
if (nb < 0) return -1;
|
|
}
|
|
}
|
|
|
|
// Lua register → HIR value map.
|
|
int lua_reg[MAX_LUA_REGS];
|
|
memset(lua_reg, -1, sizeof(lua_reg));
|
|
|
|
// HIR value → referent claim, for TY_LUA_HANDLE values. Keyed by HIR
|
|
// value id, so it is indifferent to blocks and to which Lua register a
|
|
// handle currently sits in. A handle with no entry gets the default:
|
|
// fields are values, calls decline.
|
|
lua_ref_map lua_ref;
|
|
|
|
// HIR value → Lua truth class (see lua_truth). Distinguishes false
|
|
// from integer 0 and nil from "" so TEST/NOT follow Lua, not HIR_BOOL.
|
|
lua_truth_map truth_tag;
|
|
|
|
// Loop-carried value routing (#1732). A plain HIR value crosses a
|
|
// block boundary only under dominance, and the transition below drops
|
|
// everything else -- which is correct for diamonds and fatal for
|
|
// loops: the accumulator in `for i=1,4 do s=s+i end` is written in the
|
|
// body and read by the next iteration. Loop protos therefore route
|
|
// Lua registers through q-registers (reg r → qreg r), the one kind of
|
|
// traffic hir_ssa_construct PHI-converts -- the same road the numeric
|
|
// for's own index already takes via QREG_LUA_IDX.
|
|
//
|
|
// Backing rule: a register joins the backed set only when the ENTRY
|
|
// block stores it (every path executes the entry block, so every later
|
|
// LOAD_Q is dominated by a store), or when FORLOOP itself stores the
|
|
// loop variable (every reader is dominated by the latch). A register
|
|
// first written elsewhere stays plain and keeps today's drop-then-
|
|
// decline behavior: reloading it would read whatever the surrounding
|
|
// command left in the MUSH %q register on paths that never stored it.
|
|
// Backed stores are integers only; a backed register going non-int
|
|
// declines the chunk rather than leaving a stale int in the qreg.
|
|
// The loop VARIABLE needs its backing declared up front: the body is
|
|
// lowered BEFORE the FORLOOP that writes R(A)/R(A+3) (linear pc
|
|
// order), so without the pre-scan the body's read of the loop var
|
|
// found -1 and declined. Sound because every path into the body --
|
|
// and into the exit block -- passes the latch, whose STORE_Qs
|
|
// dominate every reload.
|
|
bool proto_has_loop = false;
|
|
bool forloop_backed[10] = { false, false, false, false, false,
|
|
false, false, false, false, false };
|
|
for (int i = 0; i < n; i++) {
|
|
const int sop = proto->code[i].opcode();
|
|
if (sop == OP_LUA_FORLOOP) {
|
|
proto_has_loop = true;
|
|
int fa = proto->code[i].A();
|
|
// Only the VISIBLE index (A+3) is materialized; R(A) is 5.4's
|
|
// internal counter and nothing here ever produces it.
|
|
if (fa + 3 < 10) {
|
|
forloop_backed[fa + 3] = true;
|
|
}
|
|
} else if (sop == OP_LUA_JMP
|
|
&& i + 1 + proto->code[i].sJ() <= i) {
|
|
// while/repeat: a backward JMP makes this a loop proto too.
|
|
proto_has_loop = true;
|
|
}
|
|
}
|
|
bool qreg_backed[10] = { false, false, false, false, false,
|
|
false, false, false, false, false };
|
|
bool entry_backing_sealed = false;
|
|
int limited_blk = -1;
|
|
// Register state at the moment the entry block was left. FORLOOP's
|
|
// static-bounds test reads its ICONSTs from here: by the latch,
|
|
// lua_reg[] holds LOAD_Q reloads, and a reload is one iteration
|
|
// fresher than an entry constant but buries the constness.
|
|
int entry_final[MAX_LUA_REGS];
|
|
memset(entry_final, -1, sizeof(entry_final));
|
|
|
|
|
|
// Snapshot of lua_reg as it stood on entry to the current block, so a
|
|
// block transition can tell which registers this block wrote. See the
|
|
// dominance note at the transition below (#1422).
|
|
int blk_entry_reg[MAX_LUA_REGS];
|
|
memcpy(blk_entry_reg, lua_reg, sizeof(blk_entry_reg));
|
|
|
|
int cur_hir_block = 0;
|
|
h.cur_block = 0;
|
|
int result_val = -1;
|
|
|
|
// The shared bail block: its ECALL aborts the whole run to the
|
|
// interpreter. Two producers branch here -- back-edge budget
|
|
// exhaustion, and FORPREP's runtime bounds guard -- and both bails
|
|
// are rerun-safe: loop protos exclude persistent effects, and the
|
|
// bounds guard runs before any body effect exists at all.
|
|
auto ensure_limited_blk = [&]() -> bool {
|
|
if (limited_blk >= 0) return true;
|
|
limited_blk = h.new_block();
|
|
if (limited_blk < 0) return false;
|
|
int save_blk = h.cur_block;
|
|
h.cur_block = limited_blk;
|
|
h.emit(HIR_LUA_LIMITED, TY_VOID);
|
|
int dead = h.emit_sconst(rc.pool_str("", 0), "");
|
|
if (dead < 0) return false;
|
|
h.emit(HIR_RET, TY_VOID, dead);
|
|
h.cur_block = save_blk;
|
|
return true;
|
|
};
|
|
|
|
// Back-edge budget guard, shared by FORLOOP and backward JMP so the
|
|
// two cannot drift (#1457's lesson): decrement by the loop body's
|
|
// instruction count, and branch to the shared limited block on
|
|
// exhaustion rather than exiting the loop with a wrong partial
|
|
// result (#1732). Leaves the current block set to a fresh
|
|
// continuation block for the caller's own terminator. Returns
|
|
// false on emission failure.
|
|
auto emit_backedge_guard = [&](int body_len) -> bool {
|
|
int budget_ok = emit_budget_check(h, -1, body_len);
|
|
if (budget_ok < 0) return false;
|
|
if (!ensure_limited_blk()) return false;
|
|
int cont_blk = h.new_block();
|
|
if (cont_blk < 0) return false;
|
|
h.emit(HIR_BRC, TY_VOID, budget_ok, limited_blk, cont_blk);
|
|
h.add_edge(cur_hir_block, limited_blk);
|
|
h.add_edge(cur_hir_block, cont_blk);
|
|
h.cur_block = cont_blk;
|
|
cur_hir_block = cont_blk;
|
|
return true;
|
|
};
|
|
|
|
// Entry-backing seal: decide, once, which registers the q-reg
|
|
// machinery may reload (see the backing rule above), and freeze
|
|
// entry_final. Called from the first block transition -- or from
|
|
// FORPREP's runtime-bounds path, which SPLITS the entry block with
|
|
// branches and must finalize entry state before the split so the
|
|
// next transition's drop-compare does not mistake entry writes for
|
|
// block-local ones.
|
|
auto seal_entry_backing = [&]() {
|
|
if (entry_backing_sealed) return;
|
|
for (int r = 0; r < 10 && r < MAX_LUA_REGS; r++) {
|
|
if (qreg_backed[r]
|
|
&& (lua_reg[r] < 0 || h.ty[lua_reg[r]] != TY_INT)) {
|
|
qreg_backed[r] = false;
|
|
}
|
|
}
|
|
memcpy(entry_final, lua_reg, sizeof(entry_final));
|
|
entry_backing_sealed = true;
|
|
};
|
|
|
|
// Initialize back-edge budget counter for loop DoS protection.
|
|
// Uses the same limit as the Lua interpreter's instruction hook.
|
|
// Only needed for multi-block programs (which can have loops).
|
|
//
|
|
// Read at RUN time via a dedicated ECALL, never baked as an ICONST of
|
|
// mudconf.lua_instruction_limit (#1745). A compiled program is cached
|
|
// in memory and persisted in code_cache, so a baked value is the limit
|
|
// that happened to be configured at compile time, forever -- @admin
|
|
// changes reported Set. and changed nothing, which is #1613's bug
|
|
// arriving on the compiled path. The test-config runtime-bounds case
|
|
// is what caught it, the first time the default-on flip put the
|
|
// compiled path in its way.
|
|
if (multi_block) {
|
|
int budget_init = h.emit(HIR_LUA_INSN_BUDGET, TY_INT);
|
|
h.emit(HIR_STORE_Q, TY_VOID, budget_init, -1, QREG_LUA_BUDGET);
|
|
}
|
|
|
|
// Pinned table tracking for array optimization.
|
|
// When a for-loop body accesses t[i] where i is the loop variable,
|
|
// we pin the table's array into guest memory before the loop.
|
|
int pinned_tbl_reg = -1; // Lua register of pinned table (-1 = none)
|
|
int pinned_count_val = -1; // HIR value holding element count
|
|
|
|
for (int pc = 0; pc < n; pc++) {
|
|
// Switch blocks if this PC is a leader.
|
|
if (is_leader[pc] && pc > 0) {
|
|
int new_block = pc_to_block[pc];
|
|
if (new_block != cur_hir_block) {
|
|
if (h.n_insns > 0) {
|
|
hir_kind last = h.kind[h.n_insns - 1];
|
|
if (last != HIR_BR && last != HIR_BRC && last != HIR_RET) {
|
|
h.emit(HIR_BR, TY_VOID, -1, -1, new_block);
|
|
h.add_edge(cur_hir_block, new_block);
|
|
}
|
|
}
|
|
// A plain HIR value is usable across blocks only where its
|
|
// defining block dominates the use. Nothing merges values
|
|
// at a join: hir_ssa_construct() inserts PHIs only for
|
|
// q-register traffic, which is exactly why the numeric for
|
|
// loop routes its index through STORE_Q/LOAD_Q (see the
|
|
// FORPREP comment below). The entry block dominates every
|
|
// reachable block; no other block here is known to. So
|
|
// drop any register this block wrote before leaving it --
|
|
// the "< 0" guards then decline the chunk instead of
|
|
// compiling a wrong answer (#1422).
|
|
//
|
|
// Without this, the last-lowered write simply won. A
|
|
// not-taken branch's assignment leaked past the join
|
|
// (`if x>2 then t=2 end` yielded 2 for x=1), and a loop
|
|
// body's update was invisible to the next iteration
|
|
// (`while i<10 do i=i+1 end` yielded 0, not 10).
|
|
if (cur_hir_block != 0) {
|
|
for (int r = 0; r < MAX_LUA_REGS; r++) {
|
|
if (lua_reg[r] != blk_entry_reg[r]) {
|
|
lua_reg[r] = -1;
|
|
}
|
|
}
|
|
}
|
|
if (proto_has_loop) {
|
|
// Leaving the entry block for the first time: seal
|
|
// the backed set (see seal_entry_backing; FORPREP's
|
|
// runtime-bounds path may have sealed already).
|
|
seal_entry_backing();
|
|
// Every backed register enters the new block through
|
|
// its qreg; SSA turns the loads into PHIs over the
|
|
// stores on each incoming path. ALWAYS -- an entry
|
|
// value dominates every block, but dominance is
|
|
// availability, not currency: inside the loop the
|
|
// entry constant is one iteration stale, which made
|
|
// `s=s+i` compute 0+i forever. FORLOOP gets the
|
|
// ICONSTs its static-bounds test needs from
|
|
// entry_final[], not from these reloads.
|
|
cur_hir_block = new_block;
|
|
h.cur_block = new_block;
|
|
for (int r = 0; r < 10 && r < MAX_LUA_REGS; r++) {
|
|
if (!qreg_backed[r] && !forloop_backed[r]) continue;
|
|
int lv = h.emit(HIR_LOAD_Q, TY_INT, -1, -1, r);
|
|
if (lv < 0) return -1;
|
|
h.known_int[lv] = true;
|
|
// Truth class did not cross the block boundary
|
|
// with the integer payload (#1768).
|
|
lua_truth_set(truth_tag, lv, LUA_TRUTH_UNKNOWN);
|
|
lua_reg[r] = lv;
|
|
}
|
|
memcpy(blk_entry_reg, lua_reg, sizeof(blk_entry_reg));
|
|
} else {
|
|
memcpy(blk_entry_reg, lua_reg, sizeof(blk_entry_reg));
|
|
cur_hir_block = new_block;
|
|
h.cur_block = new_block;
|
|
}
|
|
}
|
|
}
|
|
|
|
const lua_bc_instruction &insn = proto->code[pc];
|
|
int op = insn.opcode();
|
|
int A = insn.A();
|
|
|
|
// Snapshot for the store-at-write hook below (loop protos only).
|
|
int pre_reg[MAX_LUA_REGS];
|
|
if (proto_has_loop) {
|
|
memcpy(pre_reg, lua_reg, sizeof(pre_reg));
|
|
}
|
|
|
|
switch (op) {
|
|
|
|
// ---- Data movement ----
|
|
|
|
case OP_LUA_MOVE:
|
|
if (lua_reg[insn.B()] < 0) return -1;
|
|
lua_reg[A] = lua_reg[insn.B()];
|
|
break;
|
|
|
|
case OP_LUA_LOADI:
|
|
lua_reg[A] = h.emit_iconst(insn.sBx());
|
|
if (lua_reg[A] < 0) return -1;
|
|
break;
|
|
|
|
// LOADF loads a *float* whose value is the signed immediate. It
|
|
// carried an integer immediate, and the lowering took it at face
|
|
// value and emitted an integer constant -- so `return 3.0` produced
|
|
// the integer 3, and every Lua constant expression that folds to an
|
|
// integral float (`4/2`, `2^3`, `7.0//2.0`, `1e3`, `-3.0`) lost its
|
|
// float subtype before the JIT ever did any arithmetic. It also made
|
|
// `a * 1.0` an integer multiply (#1488).
|
|
case OP_LUA_LOADF:
|
|
lua_reg[A] = h.emit_fconst(static_cast<double>(insn.sBx()));
|
|
if (lua_reg[A] < 0) return -1;
|
|
break;
|
|
|
|
case OP_LUA_LOADK: {
|
|
int kidx = insn.Bx();
|
|
if (kidx < 0 || kidx >= static_cast<int>(proto->constants.size()))
|
|
return -1;
|
|
const lua_bc_constant &kc = proto->constants[kidx];
|
|
lua_reg[A] = emit_lua_constant(h, rc, kc);
|
|
if (lua_reg[A] < 0) return -1;
|
|
lua_truth_tag_constant(truth_tag, kc, lua_reg[A]);
|
|
break;
|
|
}
|
|
|
|
case OP_LUA_LOADKX: {
|
|
// Extended constant: index is in the following EXTRAARG instruction.
|
|
if (pc + 1 >= n) return -1;
|
|
int kidx = proto->code[pc + 1].Ax();
|
|
if (kidx < 0 || kidx >= static_cast<int>(proto->constants.size()))
|
|
return -1;
|
|
const lua_bc_constant &kc = proto->constants[kidx];
|
|
lua_reg[A] = emit_lua_constant(h, rc, kc);
|
|
if (lua_reg[A] < 0) return -1;
|
|
lua_truth_tag_constant(truth_tag, kc, lua_reg[A]);
|
|
pc++; // skip EXTRAARG
|
|
break;
|
|
}
|
|
|
|
case OP_LUA_LOADFALSE:
|
|
lua_reg[A] = h.emit_iconst(0);
|
|
if (lua_reg[A] < 0) return -1;
|
|
// Boolean false, not integer 0 — same ICONST, different TEST.
|
|
lua_truth_set(truth_tag, lua_reg[A], LUA_TRUTH_BOOL);
|
|
break;
|
|
|
|
// "R[A] := false; pc++". When this pairs with LOADTRUE purely to
|
|
// turn a condition into a value, the run is fused away before we get
|
|
// here (lua_bool_fuse_at). What is left is a genuine two-way join,
|
|
// so the skip has to be an explicit branch. Lowering it as a linear
|
|
// pc++ stepped over a block leader: the skipped path's entire body
|
|
// was emitted into this block and the other block was left empty, so
|
|
// the chunk returned the false arm's value -- or, once every RET got
|
|
// its own output slot, nothing at all (#1421).
|
|
case OP_LUA_LFALSESKIP: {
|
|
lua_reg[A] = h.emit_iconst(0);
|
|
if (lua_reg[A] < 0) return -1;
|
|
lua_truth_set(truth_tag, lua_reg[A], LUA_TRUTH_BOOL);
|
|
if (!multi_block) return -1;
|
|
int target = pc + 2;
|
|
int target_blk = (target > 0 && target < n) ? pc_to_block[target] : -1;
|
|
if (target_blk < 0) return -1;
|
|
h.emit(HIR_BR, TY_VOID, -1, -1, target_blk);
|
|
h.add_edge(cur_hir_block, target_blk);
|
|
break;
|
|
}
|
|
|
|
case OP_LUA_LOADTRUE:
|
|
lua_reg[A] = h.emit_iconst(1);
|
|
if (lua_reg[A] < 0) return -1;
|
|
lua_truth_set(truth_tag, lua_reg[A], LUA_TRUTH_BOOL);
|
|
break;
|
|
|
|
case OP_LUA_LOADNIL:
|
|
for (int i = A; i <= A + insn.B(); i++) {
|
|
if (!lua_reg_in_range(i)) return -1;
|
|
// Empty SCONST is also the representation of "": only the
|
|
// NIL tag makes TEST falsy here and leaves literal "" truthy.
|
|
lua_reg[i] = h.emit_sconst(rc.pool_str("", 0), "");
|
|
if (lua_reg[i] < 0) return -1;
|
|
lua_truth_set(truth_tag, lua_reg[i], LUA_TRUTH_NIL);
|
|
}
|
|
break;
|
|
|
|
// ---- Integer arithmetic ----
|
|
|
|
#define ARITH_RR(HIR_INT_OP, HIR_FP_OP, MMOP) \
|
|
{ \
|
|
int rb = lua_reg[insn.B()]; \
|
|
int rc_val = lua_reg[insn.C()]; \
|
|
if (rb < 0 || rc_val < 0) return -1; \
|
|
if (lua_is_handle(h, rb) || lua_is_handle(h, rc_val)) return -1; \
|
|
if (either_float(h, rb, rc_val)) { \
|
|
rb = promote_to_float(h, truth_tag, rb); \
|
|
rc_val = promote_to_float(h, truth_tag, rc_val); \
|
|
if (rb < 0 || rc_val < 0) return -1; \
|
|
lua_reg[A] = h.emit(HIR_FP_OP, TY_FLOAT, rb, rc_val); \
|
|
} else if (h.ty[rb] == TY_INT && h.ty[rc_val] == TY_INT) { \
|
|
lua_reg[A] = h.emit(HIR_INT_OP, TY_INT, rb, rc_val); \
|
|
} else { \
|
|
rb = promote_to_int(h, truth_tag, rb); \
|
|
rc_val = promote_to_int(h, truth_tag, rc_val); \
|
|
if (rb < 0 || rc_val < 0) return -1; \
|
|
lua_reg[A] = h.emit(HIR_INT_OP, TY_INT, rb, rc_val); \
|
|
} \
|
|
if (lua_reg[A] < 0) return -1; \
|
|
h.native_ops++; \
|
|
/* Do NOT pc++ past MMBIN here: the for-loop already advances
|
|
* pc, so an extra increment skips the following RETURN and
|
|
* leaves the chunk without a proper HIR_RET (#1309 hang).
|
|
* MMBIN* cases below are intentional no-ops. */ \
|
|
(void)MMOP; \
|
|
break; \
|
|
}
|
|
|
|
case OP_LUA_ADD: ARITH_RR(HIR_ADD, HIR_FADD, OP_LUA_MMBIN)
|
|
case OP_LUA_SUB: ARITH_RR(HIR_SUB, HIR_FSUB, OP_LUA_MMBIN)
|
|
case OP_LUA_MUL: ARITH_RR(HIR_MUL, HIR_FMUL, OP_LUA_MMBIN)
|
|
case OP_LUA_IDIV: ARITH_RR(HIR_DIV, HIR_DIV, OP_LUA_MMBIN) // IDIV always integer
|
|
case OP_LUA_MOD: ARITH_RR(HIR_REM, HIR_REM, OP_LUA_MMBIN) // MOD always integer
|
|
#undef ARITH_RR
|
|
|
|
// Lua `/` (OP_DIV) always produces a float result.
|
|
case OP_LUA_DIV: {
|
|
int rb = lua_reg[insn.B()];
|
|
int rc_val = lua_reg[insn.C()];
|
|
if (rb < 0 || rc_val < 0) return -1;
|
|
rb = promote_to_float(h, truth_tag, rb);
|
|
rc_val = promote_to_float(h, truth_tag, rc_val);
|
|
if (rb < 0 || rc_val < 0) return -1;
|
|
lua_reg[A] = h.emit(HIR_FDIV, TY_FLOAT, rb, rc_val);
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.native_ops++;
|
|
// MMBIN follows as a no-op case — do not double-advance pc.
|
|
break;
|
|
}
|
|
|
|
// Lua `^` (OP_POW) always produces a float. Native FCALL2 to the
|
|
// No HAVE_IEEE_FP_SNAN guard here, deliberately (#1556).
|
|
//
|
|
// Softcode POWER declines the native path when that macro is undefined
|
|
// (hir_lower.cpp) because fun_power has a *MUX output convention* on
|
|
// such builds: a negative base yields the literal string "Ind" rather
|
|
// than whatever the FP library produces. That is softcode's answer
|
|
// format, not a trap-avoidance measure.
|
|
//
|
|
// Lua has no such convention. luai_numpow (lua54/llimits.h) is
|
|
// `(b == 2) ? a*a : pow(a, b)` on every platform, so the Lua
|
|
// *interpreter* calls pow() directly whether or not the macro is
|
|
// defined. Mirroring softcode's guard here would therefore make the
|
|
// compiled path diverge from the Lua interpreter, which is the opposite
|
|
// of what the guard achieves for softcode.
|
|
//
|
|
// Measured on a build with HAVE_IEEE_FP_SNAN forced off: softcode
|
|
// power(-2,0.5) answers "Ind" while Lua (-2)^0.5 answers "nan", in the
|
|
// interpreter, with no JIT involved. The two languages disagree by
|
|
// design and the compiled path must follow Lua, not softcode.
|
|
//
|
|
// tier-2 `pow` blob — same path softcode power() uses — not the
|
|
// string-bridge ECALL __LUA_POW. That ECALL read both args with
|
|
// atof(farg_cstr()), but HIR_CALL marshals TY_FLOAT as the raw
|
|
// double storage address; low bytes are 0x00 so atof sees "" → 0
|
|
// and every runtime ^ became pow(0,0) == 1 (#1538). Constant ^
|
|
// never hit it (Lua folds those to LOADF).
|
|
case OP_LUA_POW: {
|
|
int rb = lua_reg[insn.B()];
|
|
int rc_val = lua_reg[insn.C()];
|
|
if (rb < 0 || rc_val < 0) return -1;
|
|
rb = promote_to_float(h, truth_tag, rb);
|
|
rc_val = promote_to_float(h, truth_tag, rc_val);
|
|
if (rb < 0 || rc_val < 0) return -1;
|
|
uint64_t addr = tier2_sym_addr("pow");
|
|
if (!addr) return -1;
|
|
lua_reg[A] = h.emit(HIR_FCALL2, TY_FLOAT, rb, rc_val,
|
|
static_cast<int64_t>(addr));
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.func_idx[lua_reg[A]] = FMATH_POW;
|
|
h.native_ops++;
|
|
break;
|
|
}
|
|
|
|
case OP_LUA_UNM: {
|
|
int rb = lua_reg[insn.B()];
|
|
if (rb < 0) return -1;
|
|
if (h.ty[rb] == TY_FLOAT) {
|
|
lua_reg[A] = h.emit(HIR_FNEG, TY_FLOAT, rb);
|
|
} else if (h.ty[rb] == TY_INT) {
|
|
lua_reg[A] = h.emit(HIR_NEG, TY_INT, rb);
|
|
} else if (h.ty[rb] == TY_STRING) {
|
|
rb = promote_to_int(h, truth_tag, rb);
|
|
if (rb < 0) return -1;
|
|
lua_reg[A] = h.emit(HIR_NEG, TY_INT, rb);
|
|
} else {
|
|
return -1;
|
|
}
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.native_ops++;
|
|
break;
|
|
}
|
|
|
|
// ---- Bitwise operations ----
|
|
|
|
#define BITOP_RR(HIR_OP) \
|
|
{ \
|
|
int rb = lua_reg[insn.B()]; \
|
|
int rc_val = lua_reg[insn.C()]; \
|
|
if (rb < 0 || rc_val < 0) return -1; \
|
|
if (lua_is_handle(h, rb) || lua_is_handle(h, rc_val)) return -1; \
|
|
rb = promote_to_int(h, truth_tag, rb); \
|
|
rc_val = promote_to_int(h, truth_tag, rc_val); \
|
|
if (rb < 0 || rc_val < 0) return -1; \
|
|
lua_reg[A] = h.emit(HIR_OP, TY_INT, rb, rc_val); \
|
|
if (lua_reg[A] < 0) return -1; \
|
|
h.native_ops++; \
|
|
/* MMBIN is a no-op case — do not double-advance pc (#1309). */ \
|
|
break; \
|
|
}
|
|
|
|
case OP_LUA_BAND: BITOP_RR(HIR_BAND)
|
|
case OP_LUA_BOR: BITOP_RR(HIR_BOR)
|
|
case OP_LUA_BXOR: BITOP_RR(HIR_BXOR)
|
|
case OP_LUA_SHL: BITOP_RR(HIR_SHL)
|
|
case OP_LUA_SHR: BITOP_RR(HIR_SHR)
|
|
#undef BITOP_RR
|
|
|
|
case OP_LUA_BNOT: {
|
|
int rb = lua_reg[insn.B()];
|
|
if (rb < 0) return -1;
|
|
rb = promote_to_int(h, truth_tag, rb);
|
|
if (rb < 0) return -1;
|
|
lua_reg[A] = h.emit(HIR_BNOT, TY_INT, rb);
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.native_ops++;
|
|
break;
|
|
}
|
|
|
|
// SHRI/SHLI: shift by immediate (sC field).
|
|
case OP_LUA_SHRI: {
|
|
int rb = lua_reg[insn.B()];
|
|
if (rb < 0) return -1;
|
|
rb = promote_to_int(h, truth_tag, rb);
|
|
if (rb < 0) return -1;
|
|
int imm = h.emit_iconst(insn.sC());
|
|
if (imm < 0) return -1;
|
|
lua_reg[A] = h.emit(HIR_SHR, TY_INT, rb, imm);
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.native_ops++;
|
|
break;
|
|
}
|
|
|
|
case OP_LUA_SHLI: {
|
|
int rb = lua_reg[insn.B()];
|
|
if (rb < 0) return -1;
|
|
rb = promote_to_int(h, truth_tag, rb);
|
|
if (rb < 0) return -1;
|
|
int imm = h.emit_iconst(insn.sC());
|
|
if (imm < 0) return -1;
|
|
lua_reg[A] = h.emit(HIR_SHL, TY_INT, rb, imm);
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.native_ops++;
|
|
break;
|
|
}
|
|
|
|
// Bitwise with constant (BANDK/BORK/BXORK).
|
|
#define BITOP_RK(HIR_OP) \
|
|
{ \
|
|
int rb = lua_reg[insn.B()]; \
|
|
if (rb < 0) return -1; \
|
|
if (lua_is_handle(h, rb)) return -1; \
|
|
rb = promote_to_int(h, truth_tag, rb); \
|
|
if (rb < 0) return -1; \
|
|
int kidx = insn.C(); \
|
|
if (kidx < 0 || kidx >= static_cast<int>(proto->constants.size())) \
|
|
return -1; \
|
|
int kval = emit_lua_constant(h, rc, proto->constants[kidx]); \
|
|
if (kval < 0 || h.ty[kval] != TY_INT) return -1; \
|
|
lua_reg[A] = h.emit(HIR_OP, TY_INT, rb, kval); \
|
|
if (lua_reg[A] < 0) return -1; \
|
|
h.native_ops++; \
|
|
break; \
|
|
}
|
|
|
|
case OP_LUA_BANDK: BITOP_RK(HIR_BAND)
|
|
case OP_LUA_BORK: BITOP_RK(HIR_BOR)
|
|
case OP_LUA_BXORK: BITOP_RK(HIR_BXOR)
|
|
#undef BITOP_RK
|
|
|
|
// ---- Logical NOT ----
|
|
|
|
case OP_LUA_NOT: {
|
|
int rb = lua_reg[insn.B()];
|
|
if (rb < 0) return -1;
|
|
// Marshalled CALL_STR text is not a Lua value: not("0") and
|
|
// not(false→"0") disagree, and the type is gone (#1764).
|
|
if (lua_is_marshalled_str(h, rb)) return -1;
|
|
// CALL_VAL handle: ask the VM (only nil/false are falsy).
|
|
if (lua_is_handle(h, rb) && h.kind[rb] == HIR_LUA_CALL_VAL) {
|
|
int tb = h.emit(HIR_LUA_TOBOOL, TY_INT, rb);
|
|
if (tb < 0) return -1;
|
|
lua_reg[A] = h.emit(HIR_NOT, TY_INT, tb);
|
|
if (lua_reg[A] < 0) return -1;
|
|
lua_truth_set(truth_tag, lua_reg[A], LUA_TRUTH_BOOL);
|
|
h.ecalls++;
|
|
h.native_ops++;
|
|
break;
|
|
}
|
|
if (lua_is_handle(h, rb)) return -1;
|
|
// NOT in Lua: false and nil → true (1), everything else → false (0).
|
|
// HIR_NOT is integer zero-test — correct only for BOOL tags.
|
|
// VALUE (including integer 0 and "") is always truthy → NOT 0.
|
|
// NIL → NOT 1. UNKNOWN (tag lost) declines rather than lie (#1768).
|
|
// Result is itself a boolean.
|
|
const lua_truth tr = lua_truth_of(h, truth_tag, rb);
|
|
if (tr == LUA_TRUTH_UNKNOWN) {
|
|
return -1;
|
|
}
|
|
if (tr == LUA_TRUTH_NIL) {
|
|
lua_reg[A] = h.emit_iconst(1);
|
|
} else if (tr == LUA_TRUTH_VALUE) {
|
|
lua_reg[A] = h.emit_iconst(0);
|
|
} else if (h.ty[rb] == TY_INT) {
|
|
// BOOL: 0 → 1, nonzero → 0.
|
|
if (h.kind[rb] == HIR_ICONST) {
|
|
lua_reg[A] = h.emit_iconst(h.val[rb] == 0 ? 1 : 0);
|
|
} else {
|
|
lua_reg[A] = h.emit(HIR_NOT, TY_INT, rb);
|
|
h.native_ops++;
|
|
}
|
|
} else {
|
|
return -1;
|
|
}
|
|
if (lua_reg[A] < 0) return -1;
|
|
lua_truth_set(truth_tag, lua_reg[A], LUA_TRUTH_BOOL);
|
|
break;
|
|
}
|
|
|
|
// ---- String length (ECALL back to Lua VM) ----
|
|
|
|
case OP_LUA_LEN: {
|
|
int rb = lua_reg[insn.B()];
|
|
if (rb < 0) return -1;
|
|
// Lua raises "attempt to get length of a nil value"; the
|
|
// empty SCONST would measure 0 instead.
|
|
if (lua_is_nil(h, truth_tag, rb)) return -1;
|
|
// #mux answered 3 -- strlen of the sentinel's NAME. #1424's
|
|
// shape on a different value class (#1795). The mux.args
|
|
// sentinel has its own arity path further down; this refuses
|
|
// the rest.
|
|
if (lua_is_mux_sentinel(h, rb) && h.sval[rb] != "mux.args") {
|
|
return -1;
|
|
}
|
|
// `#` on a VM reference is what returned 22 for a three-element
|
|
// table: the stack index, measured as though it were the value
|
|
// (#1424, #1579). Declining kept it correct; asking the VM
|
|
// makes it fast as well, and the index never leaves a register
|
|
// where something could measure it as text.
|
|
if (lua_is_handle(h, rb)) {
|
|
lua_reg[A] = h.emit(HIR_LUA_LEN, TY_INT, rb);
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.known_int[lua_reg[A]] = true;
|
|
h.ecalls++;
|
|
break;
|
|
}
|
|
|
|
// A mux.* table is carried through lowering as an SCONST holding
|
|
// its NAME -- "mux.args" is a sentinel, not text the program can
|
|
// see. It is not a handle, and it IS TY_STRING, so both guards
|
|
// above wave it through and a naive STRLEN would measure the
|
|
// sentinel: `#mux.args` answered 8 (strlen "mux.args") where the
|
|
// interpreter answers the argument count (TC020 under #1326).
|
|
//
|
|
// `#mux.args` is the call's ncargs. Softcode already parks that
|
|
// count in SUBST_NCARGS for `%+`; reuse it so nested production
|
|
// brackets can compile this shape instead of declining forever.
|
|
//
|
|
if ( lua_is_mux_sentinel(h, rb)
|
|
&& h.kind[rb] == HIR_SCONST
|
|
&& h.sval[rb] == "mux.args") {
|
|
uint64_t addr = rv_compiler::SUBST_BASE
|
|
+ static_cast<uint64_t>(rv_compiler::SUBST_NCARGS)
|
|
* rv_compiler::SUBST_SLOT;
|
|
h.needs_jit = true;
|
|
int sref = h.emit_sref(addr);
|
|
if (sref < 0) {
|
|
return -1;
|
|
}
|
|
lua_reg[A] = h.emit(HIR_ATOI, TY_INT, sref);
|
|
if (lua_reg[A] < 0) {
|
|
return -1;
|
|
}
|
|
h.known_int[lua_reg[A]] = true;
|
|
break;
|
|
}
|
|
// Other mux.* sentinels still have no length semantics here.
|
|
//
|
|
if (lua_is_mux_sentinel(h, rb)) {
|
|
return -1;
|
|
}
|
|
|
|
// For TY_STRING: emit strlen-like ECALL.
|
|
// For other types: would need lua_State to call __len metamethod.
|
|
if (h.ty[rb] == TY_STRING) {
|
|
// Use engine API STRLEN function if available.
|
|
int fidx = engine_api_lookup("STRLEN");
|
|
if (fidx > 0) {
|
|
int args[] = { rb };
|
|
lua_reg[A] = h.emit_call(TY_STRING, fidx, args, 1);
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.known_int[lua_reg[A]] = true;
|
|
h.ecalls++;
|
|
} else {
|
|
return -1;
|
|
}
|
|
} else {
|
|
return -1; // Table/userdata length needs lua_State.
|
|
}
|
|
break;
|
|
}
|
|
|
|
// ---- String concatenation ----
|
|
|
|
case OP_LUA_CONCAT: {
|
|
// OP_CONCAT A B: concatenate B values starting at R(A),
|
|
// result in R(A).
|
|
int nvals = insn.B();
|
|
if (nvals < 1) return -1;
|
|
// Concatenating a handle would splice a stack index into the
|
|
// text (#1579).
|
|
for (int ci = 0; ci < nvals; ci++) {
|
|
if (!lua_reg_in_range(A + ci)) return -1;
|
|
if (lua_is_handle(h, lua_reg[A + ci])) return -1;
|
|
// A sentinel is a table in Lua; concatenating it raises,
|
|
// and splicing its NAME in would answer "xmux.args".
|
|
if (lua_is_mux_sentinel(h, lua_reg[A + ci])) return -1;
|
|
// Lua raises "attempt to concatenate a nil value"; the
|
|
// empty SCONST would splice in as "".
|
|
if (lua_is_nil(h, truth_tag, lua_reg[A + ci])) return -1;
|
|
}
|
|
if (nvals == 1) {
|
|
// Single value — no-op (just ensure it's a string).
|
|
int rv = lua_reg[A];
|
|
if (rv < 0) return -1;
|
|
if (h.ty[rv] == TY_INT) {
|
|
lua_reg[A] = h.emit(HIR_ITOA, TY_STRING, rv);
|
|
} else if (h.ty[rv] == TY_FLOAT) {
|
|
lua_reg[A] = h.emit(HIR_FTOA, TY_STRING, rv);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Convert all operands to strings, then emit HIR_STRCAT.
|
|
std::vector<int> str_args;
|
|
for (int j = 0; j < nvals; j++) {
|
|
if (!lua_reg_in_range(A + j)) return -1;
|
|
int rv = lua_reg[A + j];
|
|
if (rv < 0) return -1;
|
|
if (h.ty[rv] == TY_INT) {
|
|
rv = h.emit(HIR_ITOA, TY_STRING, rv);
|
|
if (rv < 0) return -1;
|
|
} else if (h.ty[rv] == TY_FLOAT) {
|
|
rv = h.emit(HIR_FTOA, TY_STRING, rv);
|
|
if (rv < 0) return -1;
|
|
}
|
|
str_args.push_back(rv);
|
|
}
|
|
|
|
lua_reg[A] = h.emit_strcat(str_args.data(),
|
|
static_cast<int>(str_args.size()));
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.ecalls++;
|
|
break;
|
|
}
|
|
|
|
// ---- Table operations (ECALL back to Lua VM) ----
|
|
//
|
|
// Tables live on the Lua stack, referenced by stack index.
|
|
// NEWTABLE creates a table and returns its stack index (TY_INT).
|
|
// GETI/SETI/GETFIELD/SETFIELD operate via ECALL, marshalling
|
|
// values between guest memory and the Lua stack.
|
|
|
|
case OP_LUA_NEWTABLE: {
|
|
// A = dest register, B = array hint, C = hash hint.
|
|
// Extra size info may be in a following EXTRAARG instruction.
|
|
int narr = insn.B();
|
|
int nrec = insn.C();
|
|
// Emit ECALL_LUA_NEWTABLE: a0=narr, a1=nrec → a0=stack_idx.
|
|
int v_narr = h.emit_iconst(narr);
|
|
int v_nrec = h.emit_iconst(nrec);
|
|
if (v_narr < 0 || v_nrec < 0) return -1;
|
|
|
|
// Dedicated opcode, not a named HIR_CALL. The named form went
|
|
// through an ECALL that marshalled the stack index as a decimal
|
|
// string; nothing ever completed through it and it is gone
|
|
// (#1519). This keeps the index in a register, typed.
|
|
lua_reg[A] = h.emit(HIR_LUA_NEWTABLE, TY_LUA_HANDLE,
|
|
v_narr, v_nrec);
|
|
if (lua_reg[A] < 0) return -1;
|
|
// Mark this as known-integer (it's a stack index).
|
|
h.known_int[lua_reg[A]] = true;
|
|
// A table born here is PROVEN plain until it escapes as a
|
|
// call argument; see lua_referent::plain_proven. keys_closed
|
|
// starts true with an empty int_keys set — every constant-key
|
|
// store is recorded until a dynamic-key store opens it.
|
|
{
|
|
lua_referent nt;
|
|
nt.plain_proven = true;
|
|
nt.keys_closed = true;
|
|
lua_ref[lua_reg[A]] = nt;
|
|
}
|
|
h.ecalls++;
|
|
break;
|
|
}
|
|
|
|
case OP_LUA_GETTABI: {
|
|
// A = dest, B = table register, C = integer key.
|
|
int tbl = lua_reg[insn.B()];
|
|
if (tbl < 0) return -1;
|
|
|
|
// mux.args[N] (1-based) → softcode CARGS slot N-1. The mux.*
|
|
// bridge lowers `mux`/`args` to SCONST sentinels rather than a
|
|
// live Lua table; treating those as stack indices for
|
|
// HIR_LUA_GETI caused runaway DBT dispatch (#1309).
|
|
//
|
|
if (lua_is_mux_sentinel(h, tbl)) {
|
|
if (h.sval[tbl] == "mux.args") {
|
|
int key = insn.C();
|
|
if (key < 1 || key > rv_compiler::MAX_CARGS) {
|
|
return -1;
|
|
}
|
|
uint64_t carg_addr = rv_compiler::CARGS_BASE
|
|
+ static_cast<uint64_t>(key - 1)
|
|
* rv_compiler::CARGS_SLOT;
|
|
h.needs_jit = true;
|
|
lua_reg[A] = h.emit_sref(carg_addr);
|
|
if (lua_reg[A] < 0) return -1;
|
|
break;
|
|
}
|
|
// Other mux.* tables are not indexable on the JIT path yet.
|
|
if (h.sval[tbl].rfind("mux.", 0) == 0) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// Typed fast read requires the PLAIN PROOF (#1751 Phase 1):
|
|
// GETI's integer claim is sound only for a table this chunk
|
|
// built and never let escape. Any other handle -- a global,
|
|
// a member, an escaped local -- may carry a metatable or
|
|
// non-integer values, and the claim-miss path would continue
|
|
// with 0: a silent wrong answer, proved in review. Chunk is
|
|
// ineligible instead; the interpreter answers.
|
|
const lua_referent tref = lua_referent_of(lua_ref, tbl);
|
|
if (!tref.plain_proven) {
|
|
return -1;
|
|
}
|
|
|
|
const int64_t ikey = static_cast<int64_t>(insn.C());
|
|
// Known-absent under a closed key set: the integer slot cannot
|
|
// carry nil, and continuing with 0 was a silent wrong answer.
|
|
// Emit LOADNIL's representation at lowering — no post-entry
|
|
// GETI_INT miss.
|
|
if (tref.keys_closed
|
|
&& tref.int_keys.find(ikey) == tref.int_keys.end()) {
|
|
lua_reg[A] = h.emit_sconst(rc.pool_str("", 0), "");
|
|
if (lua_reg[A] < 0) return -1;
|
|
lua_truth_set(truth_tag, lua_reg[A], LUA_TRUTH_NIL);
|
|
break;
|
|
}
|
|
|
|
int key = h.emit_iconst(insn.C());
|
|
if (key < 0) return -1;
|
|
|
|
// Use integer fast-path: returns TY_INT directly, no string.
|
|
lua_reg[A] = h.emit(HIR_LUA_GETI, TY_INT, tbl, key);
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.ecalls++;
|
|
break;
|
|
}
|
|
|
|
case OP_LUA_SETTABI: {
|
|
// A = table register, B = integer key, C = value register --
|
|
// or, when k is set, a CONSTANT index rather than a register.
|
|
// Reading lua_reg[C] in that case yields -1 and the chunk
|
|
// declines, which is why `t[1]=5` did: the 5 is a constant.
|
|
int tbl = lua_reg[A];
|
|
if (tbl < 0) return -1;
|
|
int val;
|
|
if (insn.k()) {
|
|
if (insn.C() < 0
|
|
|| insn.C() >= static_cast<int>(proto->constants.size())) {
|
|
return -1;
|
|
}
|
|
const lua_bc_constant &kv = proto->constants[insn.C()];
|
|
if (kv.type != LUA_BC_TINT) return -1; // ints only, as below
|
|
val = h.emit_iconst(kv.ival);
|
|
} else {
|
|
val = lua_reg[insn.C()];
|
|
}
|
|
if (val < 0) return -1;
|
|
|
|
// Integer values only. The dedicated ECALL carries the value
|
|
// in a register (a2), so there is nowhere for a string to ride;
|
|
// the named form it replaces stringified everything and never
|
|
// completed (#1519). Decline the rest rather than invent a
|
|
// marshalling for it -- the interpreter answers, correctly.
|
|
if (h.ty[val] != TY_INT) return -1;
|
|
if (lua_is_handle(h, val)) return -1;
|
|
|
|
int key = h.emit_iconst(insn.B());
|
|
if (key < 0) return -1;
|
|
|
|
// In a loop proto the run may be re-run on the interpreter
|
|
// after budget exhaustion, so stores must be chunk-local: a
|
|
// store into a global-shaped table would happen twice (#1732).
|
|
if (proto_has_loop
|
|
&& lua_referent_of(lua_ref, tbl).fields_are_refs) {
|
|
return -1;
|
|
}
|
|
|
|
// Third operand rides in val[]; see hir_val_operand() in hir.h,
|
|
// which the liveness walker consults so the register holding the
|
|
// stored value is not recycled before the ECALL reads it.
|
|
if (h.emit(HIR_LUA_SETI, TY_VOID, tbl, key, val) < 0) return -1;
|
|
// SETTABI's key is always the constant insn.B().
|
|
lua_note_int_key_store(lua_ref, tbl, static_cast<int64_t>(insn.B()));
|
|
h.ecalls++;
|
|
break;
|
|
}
|
|
|
|
case OP_LUA_SETLIST: {
|
|
// A = table register, B = number of values, k+C = offset.
|
|
// Values are in R(A+1)..R(A+B).
|
|
int tbl = lua_reg[A];
|
|
if (tbl < 0) return -1;
|
|
// Same rerun-safety rule as SETTABI (#1732).
|
|
if (proto_has_loop
|
|
&& lua_referent_of(lua_ref, tbl).fields_are_refs) {
|
|
return -1;
|
|
}
|
|
int nvals = insn.B();
|
|
int offset = insn.C();
|
|
// k flag indicates extra offset from following EXTRAARG.
|
|
if (insn.k() && pc + 1 < n) {
|
|
offset += proto->code[pc + 1].Ax() * (1 << 8);
|
|
// Don't skip EXTRAARG here — it will be skipped as
|
|
// unsupported if we don't handle it, but SETLIST
|
|
// consumed the info.
|
|
}
|
|
|
|
for (int j = 1; j <= nvals; j++) {
|
|
if (!lua_reg_in_range(A + j)) return -1;
|
|
int val = lua_reg[A + j];
|
|
if (val < 0) return -1;
|
|
|
|
// Integer elements only, as for OP_LUA_SETTABI: the
|
|
// dedicated ECALL carries the value in a register, so there
|
|
// is nowhere for a string to ride. A constructor holding
|
|
// anything else declines and the interpreter answers.
|
|
if (h.ty[val] != TY_INT) return -1;
|
|
if (lua_is_handle(h, val)) return -1;
|
|
|
|
int key = h.emit_iconst(offset + j);
|
|
if (key < 0) return -1;
|
|
|
|
// Third operand rides in val[]; hir_val_operand() in hir.h
|
|
// is what keeps the liveness walker from recycling the
|
|
// register before the ECALL reads it.
|
|
if (h.emit(HIR_LUA_SETI, TY_VOID, tbl, key, val) < 0) {
|
|
return -1;
|
|
}
|
|
lua_note_int_key_store(lua_ref, tbl,
|
|
static_cast<int64_t>(offset + j));
|
|
h.ecalls++;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// GETTABLE: A = dest, B = table register, C = key register.
|
|
case OP_LUA_GETTABLE: {
|
|
int tbl = lua_reg[insn.B()];
|
|
int key = lua_reg[insn.C()];
|
|
if (tbl < 0 || key < 0) return -1;
|
|
|
|
// mux.args[k] with compile-time integer key → CARGS (see GETTABI).
|
|
//
|
|
if ( lua_is_mux_sentinel(h, tbl)
|
|
&& h.kind[tbl] == HIR_SCONST
|
|
&& h.sval[tbl] == "mux.args"
|
|
&& h.kind[key] == HIR_ICONST) {
|
|
int k = static_cast<int>(h.val[key]);
|
|
if (k < 1 || k > rv_compiler::MAX_CARGS) {
|
|
return -1;
|
|
}
|
|
uint64_t carg_addr = rv_compiler::CARGS_BASE
|
|
+ static_cast<uint64_t>(k - 1) * rv_compiler::CARGS_SLOT;
|
|
h.needs_jit = true;
|
|
lua_reg[A] = h.emit_sref(carg_addr);
|
|
if (lua_reg[A] < 0) return -1;
|
|
break;
|
|
}
|
|
|
|
if (h.ty[key] == TY_INT && pinned_tbl_reg >= 0
|
|
&& insn.B() == pinned_tbl_reg) {
|
|
// Pinned array: native memory load, no ECALL.
|
|
lua_reg[A] = h.emit(HIR_LUA_ALOAD, TY_INT, key, -1,
|
|
static_cast<int64_t>(rv_compiler::LUA_ARRAY_BASE));
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.native_ops++;
|
|
h.ecalls--; // replaces an ECALL
|
|
} else if (h.ty[key] == TY_INT) {
|
|
// Same plain-proof rule as GETTABI.
|
|
const lua_referent tref = lua_referent_of(lua_ref, tbl);
|
|
if (!tref.plain_proven) {
|
|
return -1;
|
|
}
|
|
// Constant key under a closed set: known-absent → nil.
|
|
if (h.kind[key] == HIR_ICONST
|
|
&& tref.keys_closed
|
|
&& tref.int_keys.find(h.val[key]) == tref.int_keys.end()) {
|
|
lua_reg[A] = h.emit_sconst(rc.pool_str("", 0), "");
|
|
if (lua_reg[A] < 0) return -1;
|
|
lua_truth_set(truth_tag, lua_reg[A], LUA_TRUTH_NIL);
|
|
break;
|
|
}
|
|
// Integer key: use fast-path ECALL, returns TY_INT.
|
|
lua_reg[A] = h.emit(HIR_LUA_GETI, TY_INT, tbl, key);
|
|
if (lua_reg[A] < 0) return -1;
|
|
} else {
|
|
// String/float key: use general ECALL path.
|
|
if (h.ty[key] == TY_FLOAT) {
|
|
key = h.emit(HIR_FTOA, TY_STRING, key);
|
|
if (key < 0) return -1;
|
|
}
|
|
// No handler exists for a general (string/float key)
|
|
// table read; emitting the named ECALL could only fail
|
|
// loudly post-entry. Ineligible at lowering instead
|
|
// (#1751 rule 1): the interpreter answers.
|
|
return -1;
|
|
}
|
|
h.ecalls++;
|
|
break;
|
|
}
|
|
|
|
// GETFIELD: A = dest, B = table register, C = key constant index.
|
|
// (General case — not the mux.* bridge pattern, which is handled
|
|
// separately via GETTABUP+GETFIELD.)
|
|
case OP_LUA_GETFIELD: {
|
|
int table_reg = lua_reg[insn.B()];
|
|
if (table_reg < 0) return -1;
|
|
int kidx = insn.C();
|
|
if (kidx < 0 || kidx >= static_cast<int>(proto->constants.size()))
|
|
return -1;
|
|
const lua_bc_constant &k = proto->constants[kidx];
|
|
if (k.type != LUA_BC_TSHRSTR && k.type != LUA_BC_TLNGSTR)
|
|
return -1;
|
|
|
|
// mux.* routing. ONLY mux.args stays on the SCONST sentinel:
|
|
// GETTABI on the "mux.args" marker is the native CARGS/ALOAD
|
|
// fast path. Every other mux member goes through the REAL
|
|
// global table, so a compiled mux.eval(...) pcalls the same
|
|
// bridge C function the interpreter calls -- semantics correct
|
|
// by construction. The old path mapped the NAME onto the
|
|
// softcode function table instead (mux.eval -> softcode
|
|
// eval(obj,attr), mux.name -> name() wanting a "#dbref"), which
|
|
// default-on exposed the day smoke first ran it compiled
|
|
// (#1745: TC013/TC014).
|
|
const bool mux_sentinel =
|
|
lua_is_mux_sentinel(h, table_reg)
|
|
&& h.sval[table_reg] == "mux";
|
|
if (mux_sentinel && k.sval == "args") {
|
|
std::string name = "mux." + k.sval;
|
|
uint64_t addr = rc.pool_str(name.c_str(), name.size());
|
|
lua_reg[A] = h.emit_sconst(addr, name);
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.lua_mux_sentinel[lua_reg[A]] = true;
|
|
} else {
|
|
if (mux_sentinel) {
|
|
// Materialize the real global in place of the marker.
|
|
uint64_t mk = rc.pool_str("mux", 3);
|
|
int mkey = h.emit_sconst(mk, "mux");
|
|
if (mkey < 0) return -1;
|
|
int mh = h.emit(HIR_LUA_GETGLOBAL, TY_LUA_HANDLE, mkey);
|
|
if (mh < 0) return -1;
|
|
lua_referent g;
|
|
g.fields_are_refs = true; // members are functions
|
|
g.callable = false; // the table itself is not
|
|
lua_ref[mh] = g;
|
|
h.ecalls++;
|
|
table_reg = mh;
|
|
}
|
|
// General table field access via the dedicated ECALL. The
|
|
// key travels as an ADDRESS into the program's own string
|
|
// pool; only the integer value comes back, in a register.
|
|
// Non-integer fields decline inside the handler.
|
|
if (!lua_is_handle(h, table_reg)) return -1;
|
|
uint64_t key_addr = rc.pool_str(k.sval.c_str(), k.sval.size());
|
|
int key_val = h.emit_sconst(key_addr, k.sval);
|
|
if (key_val < 0) return -1;
|
|
// Which variant depends on what the table IS -- its
|
|
// referent. A library table's members are functions, so
|
|
// take a reference; a data table's members are values, so
|
|
// take the value. The claim was recorded where the handle
|
|
// was created; a member reference gets its own claim here,
|
|
// from the member's name, so a later call reads it instead
|
|
// of walking back to this key.
|
|
const lua_referent tref = lua_referent_of(lua_ref, table_reg);
|
|
const hir_type vty = lua_lib_value_type(tref, k.sval);
|
|
if (TY_VOID != vty) {
|
|
// A known VALUE member of a library table -- math.pi,
|
|
// math.maxinteger -- so take the value itself; a
|
|
// reference would be a handle nothing downstream can
|
|
// consume. The runtime check keeps a rebound member
|
|
// honest: wrong type, decline.
|
|
lua_reg[A] = h.emit(
|
|
(TY_FLOAT == vty) ? HIR_LUA_GETFIELD_FLT
|
|
: HIR_LUA_GETFIELD,
|
|
vty, table_reg, key_val);
|
|
if (lua_reg[A] < 0) return -1;
|
|
if (TY_INT == vty) {
|
|
h.known_int[lua_reg[A]] = true;
|
|
}
|
|
h.ecalls++;
|
|
break;
|
|
}
|
|
// The typed value read (GETFIELD_INT) needs the PLAIN
|
|
// PROOF exactly as GETI does; a reference read does not
|
|
// (a handle result carries no value claim to miss).
|
|
if (!tref.fields_are_refs
|
|
&& !tref.plain_proven) {
|
|
return -1;
|
|
}
|
|
lua_reg[A] = h.emit(
|
|
tref.fields_are_refs ? HIR_LUA_GETFIELD_REF
|
|
: HIR_LUA_GETFIELD,
|
|
tref.fields_are_refs ? TY_LUA_HANDLE : TY_INT,
|
|
table_reg, key_val);
|
|
if (lua_reg[A] < 0) return -1;
|
|
if (tref.fields_are_refs) {
|
|
lua_referent m;
|
|
m.callable = true;
|
|
m.returns = lua_call_claim(k.sval);
|
|
m.call_name = k.sval;
|
|
// Bridge members that act on the world; see the
|
|
// effectful field's comment. eval is on the list
|
|
// because it runs arbitrary softcode -- pemit inside
|
|
// a mux.eval doubled exactly like a direct pemit in
|
|
// the adversarial probe.
|
|
m.effectful = (k.sval == "notify" || k.sval == "pemit"
|
|
|| k.sval == "set" || k.sval == "eval");
|
|
lua_ref[lua_reg[A]] = m;
|
|
}
|
|
h.known_int[lua_reg[A]] = true;
|
|
h.ecalls++;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// SETTABLE: A = table register, B = key register, C = value register.
|
|
case OP_LUA_SETTABLE: {
|
|
int tbl = lua_reg[A];
|
|
int key = lua_reg[insn.B()];
|
|
int val = lua_reg[insn.C()];
|
|
if (tbl < 0 || key < 0 || val < 0) return -1;
|
|
if (h.ty[key] == TY_INT) {
|
|
key = h.emit(HIR_ITOA, TY_STRING, key);
|
|
if (key < 0) return -1;
|
|
}
|
|
if (h.ty[val] == TY_INT) {
|
|
val = h.emit(HIR_ITOA, TY_STRING, val);
|
|
if (val < 0) return -1;
|
|
} else if (h.ty[val] == TY_FLOAT) {
|
|
val = h.emit(HIR_FTOA, TY_STRING, val);
|
|
if (val < 0) return -1;
|
|
}
|
|
// No handler exists for a runtime-keyed store; the named
|
|
// ECALL could only fail loudly post-entry -- which is what
|
|
// turned the plain `t[i]=v` loop into a loud diverge under
|
|
// Phase 0. Ineligible at lowering instead (#1751 rule 1).
|
|
return -1;
|
|
}
|
|
|
|
// SETFIELD: A = table register, B = key constant index, C = value register.
|
|
case OP_LUA_SETFIELD: {
|
|
int tbl = lua_reg[A];
|
|
if (tbl < 0) return -1;
|
|
// C is a CONSTANT index, not a register, when k is set -- the
|
|
// same shape that made `t[1]=5` decline on OP_LUA_SETTABI.
|
|
int val;
|
|
if (insn.k()) {
|
|
if (insn.C() < 0
|
|
|| insn.C() >= static_cast<int>(proto->constants.size())) {
|
|
return -1;
|
|
}
|
|
const lua_bc_constant &kv = proto->constants[insn.C()];
|
|
if (kv.type != LUA_BC_TINT) return -1;
|
|
val = h.emit_iconst(kv.ival);
|
|
} else {
|
|
val = lua_reg[insn.C()];
|
|
}
|
|
if (val < 0) return -1;
|
|
int kidx = insn.B();
|
|
if (kidx < 0 || kidx >= static_cast<int>(proto->constants.size()))
|
|
return -1;
|
|
const lua_bc_constant &k = proto->constants[kidx];
|
|
if (k.type != LUA_BC_TSHRSTR && k.type != LUA_BC_TLNGSTR)
|
|
return -1;
|
|
// Integer values only, as for the integer-keyed stores: the
|
|
// ECALL carries the value in a register.
|
|
if (h.ty[val] != TY_INT) return -1;
|
|
if (lua_is_handle(h, val)) return -1;
|
|
if (!lua_is_handle(h, tbl)) return -1;
|
|
|
|
// Same rerun-safety rule as SETTABI (#1732).
|
|
if (proto_has_loop
|
|
&& lua_referent_of(lua_ref, tbl).fields_are_refs) {
|
|
return -1;
|
|
}
|
|
|
|
uint64_t key_addr = rc.pool_str(k.sval.c_str(), k.sval.size());
|
|
int key_val = h.emit_sconst(key_addr, k.sval);
|
|
if (key_val < 0) return -1;
|
|
// Value rides in val[]; hir_val_operand() knows about SETFIELD
|
|
// as well as SETI, which is what keeps the register alive.
|
|
if (h.emit(HIR_LUA_SETFIELD, TY_VOID, tbl, key_val, val) < 0) {
|
|
return -1;
|
|
}
|
|
h.ecalls++;
|
|
break;
|
|
}
|
|
|
|
// ---- Immediate arithmetic ----
|
|
|
|
case OP_LUA_ADDI: {
|
|
int rb = lua_reg[insn.B()];
|
|
if (rb < 0) return -1;
|
|
// promote_to_int already refuses CALL_STR; guard the float/int
|
|
// arms too so a marshalled result cannot fall into ITOF/ADD.
|
|
if (lua_is_marshalled_str(h, rb)) return -1;
|
|
if (h.ty[rb] == TY_FLOAT) {
|
|
int imm_val = h.emit_fconst(static_cast<double>(insn.sC()));
|
|
if (imm_val < 0) return -1;
|
|
lua_reg[A] = h.emit(HIR_FADD, TY_FLOAT, rb, imm_val);
|
|
} else if (h.ty[rb] == TY_INT) {
|
|
int imm_val = h.emit_iconst(insn.sC());
|
|
if (imm_val < 0) return -1;
|
|
lua_reg[A] = h.emit(HIR_ADD, TY_INT, rb, imm_val);
|
|
} else if (h.ty[rb] == TY_STRING) {
|
|
rb = promote_to_int(h, truth_tag, rb);
|
|
if (rb < 0) return -1;
|
|
int imm_val = h.emit_iconst(insn.sC());
|
|
if (imm_val < 0) return -1;
|
|
lua_reg[A] = h.emit(HIR_ADD, TY_INT, rb, imm_val);
|
|
} else {
|
|
return -1;
|
|
}
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.native_ops++;
|
|
break;
|
|
}
|
|
|
|
// ---- Constant arithmetic ----
|
|
|
|
#define ARITH_RK(HIR_INT_OP, HIR_FP_OP) \
|
|
{ \
|
|
int rb = lua_reg[insn.B()]; \
|
|
if (rb < 0) return -1; \
|
|
if (lua_is_handle(h, rb)) return -1; \
|
|
int kidx = insn.C(); \
|
|
if (kidx < 0 || kidx >= static_cast<int>(proto->constants.size())) \
|
|
return -1; \
|
|
int kval = emit_lua_constant(h, rc, proto->constants[kidx]); \
|
|
if (kval < 0) return -1; \
|
|
if (either_float(h, rb, kval)) { \
|
|
rb = promote_to_float(h, truth_tag, rb); \
|
|
kval = promote_to_float(h, truth_tag, kval); \
|
|
if (rb < 0 || kval < 0) return -1; \
|
|
lua_reg[A] = h.emit(HIR_FP_OP, TY_FLOAT, rb, kval); \
|
|
} else if (h.ty[rb] == TY_INT && h.ty[kval] == TY_INT) { \
|
|
lua_reg[A] = h.emit(HIR_INT_OP, TY_INT, rb, kval); \
|
|
} else { \
|
|
rb = promote_to_int(h, truth_tag, rb); \
|
|
kval = promote_to_int(h, truth_tag, kval); \
|
|
if (rb < 0 || kval < 0) return -1; \
|
|
lua_reg[A] = h.emit(HIR_INT_OP, TY_INT, rb, kval); \
|
|
} \
|
|
if (lua_reg[A] < 0) return -1; \
|
|
h.native_ops++; \
|
|
break; \
|
|
}
|
|
|
|
case OP_LUA_ADDK: ARITH_RK(HIR_ADD, HIR_FADD)
|
|
case OP_LUA_SUBK: ARITH_RK(HIR_SUB, HIR_FSUB)
|
|
case OP_LUA_MULK: ARITH_RK(HIR_MUL, HIR_FMUL)
|
|
case OP_LUA_IDIVK: ARITH_RK(HIR_DIV, HIR_DIV)
|
|
case OP_LUA_MODK: ARITH_RK(HIR_REM, HIR_REM)
|
|
#undef ARITH_RK
|
|
|
|
// DIVK: Lua `/` with constant — always float.
|
|
case OP_LUA_DIVK: {
|
|
int rb = lua_reg[insn.B()];
|
|
if (rb < 0) return -1;
|
|
int kidx = insn.C();
|
|
if (kidx < 0 || kidx >= static_cast<int>(proto->constants.size()))
|
|
return -1;
|
|
int kval = emit_lua_constant(h, rc, proto->constants[kidx]);
|
|
if (kval < 0) return -1;
|
|
rb = promote_to_float(h, truth_tag, rb);
|
|
kval = promote_to_float(h, truth_tag, kval);
|
|
if (rb < 0 || kval < 0) return -1;
|
|
lua_reg[A] = h.emit(HIR_FDIV, TY_FLOAT, rb, kval);
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.native_ops++;
|
|
break;
|
|
}
|
|
|
|
// POWK: exponentiation with constant K — always float. Same
|
|
// native FCALL2 as OP_POW (#1538); do not string-bridge.
|
|
// Same reasoning as OP_LUA_POW above: no HAVE_IEEE_FP_SNAN guard,
|
|
// because Lua's interpreter has no "Ind" convention to match (#1556).
|
|
case OP_LUA_POWK: {
|
|
int rb = lua_reg[insn.B()];
|
|
if (rb < 0) return -1;
|
|
int kidx = insn.C();
|
|
if (kidx < 0 || kidx >= static_cast<int>(proto->constants.size()))
|
|
return -1;
|
|
int kval = emit_lua_constant(h, rc, proto->constants[kidx]);
|
|
if (kval < 0) return -1;
|
|
rb = promote_to_float(h, truth_tag, rb);
|
|
kval = promote_to_float(h, truth_tag, kval);
|
|
if (rb < 0 || kval < 0) return -1;
|
|
uint64_t addr = tier2_sym_addr("pow");
|
|
if (!addr) return -1;
|
|
lua_reg[A] = h.emit(HIR_FCALL2, TY_FLOAT, rb, kval,
|
|
static_cast<int64_t>(addr));
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.func_idx[lua_reg[A]] = FMATH_POW;
|
|
h.native_ops++;
|
|
break;
|
|
}
|
|
|
|
// ---- Comparisons ----
|
|
// All share: compare → optional negate → JMP → BRC
|
|
|
|
// CMP_RR: == / order on a marshalled CALL_STR result compares text, not
|
|
// the Lua value (0 == "0", false == "0", ...). Refuse those (#1764).
|
|
// Stack handles (CALL_VAL and any TY_LUA_HANDLE) use the VM for EQ only
|
|
// (HIR_LUA_EQ); order comparisons still decline.
|
|
//
|
|
// #1835: EQK/EQI already type-class gate and encode NIL/BOOL kinds; the
|
|
// register-register path did neither — mixed types ATOI-coerced to true,
|
|
// and a NIL-tagged empty SCONST was sent as kind 1 (string "").
|
|
//
|
|
#define CMP_RR(HIR_INT_OP, HIR_FP_OP) \
|
|
{ \
|
|
int rb = lua_reg[A]; \
|
|
int rc_val = lua_reg[insn.B()]; \
|
|
if (rb < 0 || rc_val < 0) return -1; \
|
|
if (lua_is_handle(h, rb) || lua_is_handle(h, rc_val)) { \
|
|
if ((HIR_INT_OP) != HIR_EQ) return -1; \
|
|
int lhs = rb, rhs = rc_val; \
|
|
int kind = 2; \
|
|
if (lua_is_handle(h, rb) && lua_is_handle(h, rc_val)) { \
|
|
kind = 2; \
|
|
} else if (lua_is_handle(h, rb)) { \
|
|
lhs = rb; \
|
|
rhs = lua_eq_kind_of_value(h, truth_tag, rc_val, &kind); \
|
|
if (rhs < 0) return -1; \
|
|
} else if (lua_is_handle(h, rc_val)) { \
|
|
lhs = rc_val; \
|
|
rhs = lua_eq_kind_of_value(h, truth_tag, rb, &kind); \
|
|
if (rhs < 0) return -1; \
|
|
} else { \
|
|
return -1; \
|
|
} \
|
|
int cmp = h.emit(HIR_LUA_EQ, TY_INT, lhs, rhs, kind); \
|
|
if (cmp < 0) return -1; \
|
|
h.ecalls++; \
|
|
if (emit_cmp_branch(h, cmp, insn.k(), proto, pc, pc_to_block, \
|
|
cur_hir_block, n, lua_reg, multi_block) < 0) \
|
|
return -1; \
|
|
pc++; \
|
|
break; \
|
|
} \
|
|
if (lua_is_marshalled_str(h, rb) || lua_is_marshalled_str(h, rc_val)) \
|
|
return -1; \
|
|
if (lua_is_mux_sentinel(h, rb) || lua_is_mux_sentinel(h, rc_val)) \
|
|
return -1; \
|
|
/* Type-class gate (#1835 / #1770): Lua == is false across */ \
|
|
/* types; order raises on mixed types. HIR erases BOOL/0 and */ \
|
|
/* NIL/"" and the old promote_to_int path made "5"==5 true. */ \
|
|
const lua_type_class ltc = \
|
|
lua_type_class_of_value(h, truth_tag, rb); \
|
|
const lua_type_class rtc = \
|
|
lua_type_class_of_value(h, truth_tag, rc_val); \
|
|
if (ltc == LUA_TC_OTHER || rtc == LUA_TC_OTHER) { \
|
|
return -1; \
|
|
} \
|
|
if ((HIR_INT_OP) == HIR_EQ) { \
|
|
if (ltc != rtc || ltc == LUA_TC_NIL) { \
|
|
int cmp = h.emit_iconst( \
|
|
(ltc == LUA_TC_NIL && rtc == LUA_TC_NIL) ? 1 : 0); \
|
|
if (cmp < 0) return -1; \
|
|
h.native_ops++; \
|
|
if (emit_cmp_branch(h, cmp, insn.k(), proto, pc, \
|
|
pc_to_block, cur_hir_block, n, \
|
|
lua_reg, multi_block) < 0) \
|
|
return -1; \
|
|
pc++; \
|
|
break; \
|
|
} \
|
|
} else { \
|
|
/* Order: interpreter raises on mixed / non-orderable. */ \
|
|
if (ltc != rtc) return -1; \
|
|
if (ltc != LUA_TC_NUMBER && ltc != LUA_TC_STRING) return -1; \
|
|
} \
|
|
int cmp; \
|
|
if (either_float(h, rb, rc_val)) { \
|
|
rb = promote_to_float(h, truth_tag, rb); \
|
|
rc_val = promote_to_float(h, truth_tag, rc_val); \
|
|
if (rb < 0 || rc_val < 0) return -1; \
|
|
cmp = h.emit(HIR_FP_OP, TY_INT, rb, rc_val); \
|
|
} else if (h.ty[rb] == TY_INT && h.ty[rc_val] == TY_INT) { \
|
|
cmp = h.emit(HIR_INT_OP, TY_INT, rb, rc_val); \
|
|
} else if (h.ty[rb] == TY_STRING && h.ty[rc_val] == TY_STRING) { \
|
|
int sc = h.emit(HIR_STRCMP, TY_INT, rb, rc_val); \
|
|
if (sc < 0) return -1; \
|
|
int zero = h.emit_iconst(0); \
|
|
cmp = h.emit(HIR_INT_OP, TY_INT, sc, zero); \
|
|
} else { \
|
|
/* Same type class but HIR types differ (e.g. int vs float */ \
|
|
/* already handled). Decline rather than cross-coerce. */ \
|
|
return -1; \
|
|
} \
|
|
h.native_ops++; \
|
|
if (emit_cmp_branch(h, cmp, insn.k(), proto, pc, pc_to_block, \
|
|
cur_hir_block, n, lua_reg, multi_block) < 0) \
|
|
return -1; \
|
|
pc++; \
|
|
break; \
|
|
}
|
|
|
|
#define CMP_RI(HIR_INT_OP, HIR_FP_OP) \
|
|
{ \
|
|
int rb = lua_reg[A]; \
|
|
if (rb < 0) return -1; \
|
|
if (lua_is_handle(h, rb)) return -1; \
|
|
if (lua_is_nil(h, truth_tag, rb)) return -1; \
|
|
if (lua_is_mux_sentinel(h, rb)) return -1; \
|
|
int cmp; \
|
|
if (h.ty[rb] == TY_FLOAT) { \
|
|
int fimm = h.emit_fconst(static_cast<double>(insn.sB())); \
|
|
if (fimm < 0) return -1; \
|
|
cmp = h.emit(HIR_FP_OP, TY_INT, rb, fimm); \
|
|
} else if (h.ty[rb] == TY_INT) { \
|
|
int imm_val = h.emit_iconst(insn.sB()); \
|
|
if (imm_val < 0) return -1; \
|
|
cmp = h.emit(HIR_INT_OP, TY_INT, rb, imm_val); \
|
|
} else if (h.ty[rb] == TY_STRING) { \
|
|
rb = promote_to_int(h, truth_tag, rb); \
|
|
if (rb < 0) return -1; \
|
|
int imm_val = h.emit_iconst(insn.sB()); \
|
|
if (imm_val < 0) return -1; \
|
|
cmp = h.emit(HIR_INT_OP, TY_INT, rb, imm_val); \
|
|
} else { \
|
|
return -1; \
|
|
} \
|
|
h.native_ops++; \
|
|
if (emit_cmp_branch(h, cmp, insn.k(), proto, pc, pc_to_block, \
|
|
cur_hir_block, n, lua_reg, multi_block) < 0) \
|
|
return -1; \
|
|
pc++; \
|
|
break; \
|
|
}
|
|
|
|
case OP_LUA_EQ: CMP_RR(HIR_EQ, HIR_FEQ)
|
|
case OP_LUA_LT: CMP_RR(HIR_LT, HIR_FLT)
|
|
case OP_LUA_LE: CMP_RR(HIR_LE, HIR_FLE)
|
|
|
|
// EQK: equality with constant from pool.
|
|
//
|
|
// Lua's condjump always pairs EQK with a following JMP (same shape
|
|
// as EQ/EQI). Value materialisation is EQK+JMP+LFALSESKIP+LOADTRUE
|
|
// and is fused via emit_cmp_branch / lua_bool_fuse_at. Older code
|
|
// treated EQK as a bare one-instruction skip to pc+1/pc+2 and
|
|
// declined the fuse shape, so `return x == "0"` never compiled
|
|
// once x was a CALL_VAL handle (#1764 residual).
|
|
//
|
|
case OP_LUA_EQK: {
|
|
int rb = lua_reg[A];
|
|
if (rb < 0) return -1;
|
|
// See CMP_RR: marshalled CALL_STR is text, not a Lua value (#1764).
|
|
if (lua_is_marshalled_str(h, rb)) return -1;
|
|
int kidx = insn.B();
|
|
if (kidx < 0 || kidx >= static_cast<int>(proto->constants.size()))
|
|
return -1;
|
|
const lua_bc_constant &kconst = proto->constants[kidx];
|
|
// Stack-handle left-hand side (CALL_VAL etc.): ask the VM so type
|
|
// distinctions survive (tostring(0) == "0" true; tonumber("17")
|
|
// == "17" false). Match on TY_LUA_HANDLE, not kind==CALL_VAL:
|
|
// SSA copies / reloads keep the type but not the producer kind.
|
|
//
|
|
if (lua_is_handle(h, rb)) {
|
|
int kind = -1;
|
|
int rhs = -1;
|
|
if (kconst.type == LUA_BC_TNIL) {
|
|
kind = 3;
|
|
rhs = h.emit_iconst(0);
|
|
} else if (kconst.type == LUA_BC_TFALSE) {
|
|
kind = 4;
|
|
rhs = h.emit_iconst(0);
|
|
} else if (kconst.type == LUA_BC_TTRUE) {
|
|
kind = 4;
|
|
rhs = h.emit_iconst(1);
|
|
} else if (kconst.type == LUA_BC_TINT) {
|
|
kind = 0;
|
|
rhs = h.emit_iconst(kconst.ival);
|
|
} else if (kconst.type == LUA_BC_TFLOAT) {
|
|
// Float constant kind not wired for HIR_LUA_EQ yet.
|
|
return -1;
|
|
} else if ( kconst.type == LUA_BC_TSHRSTR
|
|
|| kconst.type == LUA_BC_TLNGSTR) {
|
|
kind = 1;
|
|
rhs = emit_lua_constant(h, rc, kconst);
|
|
} else {
|
|
return -1;
|
|
}
|
|
if (rhs < 0) return -1;
|
|
int cmp = h.emit(HIR_LUA_EQ, TY_INT, rb, rhs, kind);
|
|
if (cmp < 0) return -1;
|
|
h.ecalls++;
|
|
// emit_cmp_branch owns k-bit polarity (EQ convention) and
|
|
// the LFALSESKIP/LOADTRUE fuse for `return x == K`.
|
|
if (emit_cmp_branch(h, cmp, insn.k(), proto, pc, pc_to_block,
|
|
cur_hir_block, n, lua_reg, multi_block) < 0)
|
|
return -1;
|
|
pc++;
|
|
break;
|
|
}
|
|
// nil is the empty SCONST in HIR, which is also a real "".
|
|
// EQK's string path would make nil == "" true. Resolve against
|
|
// the pool type (and the lhs tag) before emitting STRCMP:
|
|
// nil == nil → true
|
|
// nil == anything else (incl. "") → false
|
|
// Same for false vs integer 0 once BOOL is tagged on constants.
|
|
// Lua == is false across TYPES, and HIR erases the ones that
|
|
// matter: false and 0 are the same ICONST, nil and "" the same
|
|
// empty SCONST, and the numeric path would coerce "5" to 5.
|
|
// So compare type classes first; a mismatch is constant false
|
|
// whatever the representations say. (The nil-only version of
|
|
// this check still let `0 == false`, `1 == true` and
|
|
// `"5" == 5` answer true -- measured.)
|
|
const lua_type_class lhs_tc =
|
|
lua_type_class_of_value(h, truth_tag, rb);
|
|
const lua_type_class rhs_tc = lua_type_class_of_const(kconst);
|
|
if (lhs_tc == LUA_TC_OTHER || rhs_tc == LUA_TC_OTHER) {
|
|
return -1;
|
|
}
|
|
const bool lhs_nil = (lhs_tc == LUA_TC_NIL);
|
|
const bool rhs_nil = (rhs_tc == LUA_TC_NIL);
|
|
if (lhs_tc != rhs_tc || lhs_nil) {
|
|
// Different types -> false. Same type and both nil ->
|
|
// true (nil has exactly one value).
|
|
int cmp = h.emit_iconst((lhs_nil && rhs_nil) ? 1 : 0);
|
|
if (cmp < 0) return -1;
|
|
h.native_ops++;
|
|
if (emit_cmp_branch(h, cmp, insn.k(), proto, pc, pc_to_block,
|
|
cur_hir_block, n, lua_reg, multi_block) < 0)
|
|
return -1;
|
|
pc++;
|
|
break;
|
|
}
|
|
int kval = emit_lua_constant(h, rc, kconst);
|
|
if (kval < 0) return -1;
|
|
lua_truth_tag_constant(truth_tag, kconst, kval);
|
|
int cmp;
|
|
if (either_float(h, rb, kval)) {
|
|
rb = promote_to_float(h, truth_tag, rb);
|
|
kval = promote_to_float(h, truth_tag, kval);
|
|
if (rb < 0 || kval < 0) return -1;
|
|
cmp = h.emit(HIR_FEQ, TY_INT, rb, kval);
|
|
} else if (h.ty[rb] == TY_INT && h.ty[kval] == TY_INT) {
|
|
cmp = h.emit(HIR_EQ, TY_INT, rb, kval);
|
|
} else if (h.ty[rb] == TY_STRING && h.ty[kval] == TY_STRING) {
|
|
int sc = h.emit(HIR_STRCMP, TY_INT, rb, kval);
|
|
if (sc < 0) return -1;
|
|
int zero = h.emit_iconst(0);
|
|
cmp = h.emit(HIR_EQ, TY_INT, sc, zero);
|
|
} else {
|
|
rb = promote_to_int(h, truth_tag, rb);
|
|
kval = promote_to_int(h, truth_tag, kval);
|
|
if (rb < 0 || kval < 0) return -1;
|
|
cmp = h.emit(HIR_EQ, TY_INT, rb, kval);
|
|
}
|
|
if (cmp < 0) return -1;
|
|
h.native_ops++;
|
|
if (emit_cmp_branch(h, cmp, insn.k(), proto, pc, pc_to_block,
|
|
cur_hir_block, n, lua_reg, multi_block) < 0)
|
|
return -1;
|
|
pc++;
|
|
break;
|
|
}
|
|
|
|
// EQI is equality against a small integer immediate -- a NUMBER.
|
|
// Lua == is false across types, so a non-number lhs answers false
|
|
// rather than coercing (`local a=false if a==0` answered true;
|
|
// `local a="5" if a==5` likewise). Order comparisons (LTI/LEI/
|
|
// GTI/GEI) are different: Lua RAISES on mismatched types, so they
|
|
// keep declining via CMP_RI's guards.
|
|
case OP_LUA_EQI: {
|
|
int rb = lua_reg[A];
|
|
if (rb < 0) return -1;
|
|
if (lua_is_handle(h, rb)) return -1;
|
|
if (lua_is_marshalled_str(h, rb)) return -1;
|
|
const lua_type_class lhs_tc =
|
|
lua_type_class_of_value(h, truth_tag, rb);
|
|
if (lhs_tc == LUA_TC_OTHER) return -1;
|
|
if (lhs_tc != LUA_TC_NUMBER) {
|
|
int cmp = h.emit_iconst(0);
|
|
if (cmp < 0) return -1;
|
|
if (emit_cmp_branch(h, cmp, insn.k(), proto, pc, pc_to_block,
|
|
cur_hir_block, n, nullptr,
|
|
multi_block) < 0) {
|
|
return -1;
|
|
}
|
|
pc++;
|
|
break;
|
|
}
|
|
CMP_RI(HIR_EQ, HIR_FEQ)
|
|
}
|
|
case OP_LUA_LTI: CMP_RI(HIR_LT, HIR_FLT)
|
|
case OP_LUA_LEI: CMP_RI(HIR_LE, HIR_FLE)
|
|
|
|
// For GT/GE with floats, we only have FLT/FLE.
|
|
// GT(a, b) = FLT(b, a), GE(a, b) = FLE(b, a) — swap operands.
|
|
case OP_LUA_GTI: {
|
|
int rb = lua_reg[A];
|
|
if (rb < 0) return -1;
|
|
// nil has no order; empty SCONST would promote to 0.
|
|
if (lua_is_nil(h, truth_tag, rb)) return -1;
|
|
int cmp;
|
|
if (h.ty[rb] == TY_FLOAT) {
|
|
int fimm = h.emit_fconst(static_cast<double>(insn.sB()));
|
|
if (fimm < 0) return -1;
|
|
cmp = h.emit(HIR_FLT, TY_INT, fimm, rb); // swapped
|
|
} else if (h.ty[rb] == TY_INT) {
|
|
int imm_val = h.emit_iconst(insn.sB());
|
|
if (imm_val < 0) return -1;
|
|
cmp = h.emit(HIR_GT, TY_INT, rb, imm_val);
|
|
} else if (h.ty[rb] == TY_STRING) {
|
|
rb = promote_to_int(h, truth_tag, rb);
|
|
if (rb < 0) return -1;
|
|
int imm_val = h.emit_iconst(insn.sB());
|
|
if (imm_val < 0) return -1;
|
|
cmp = h.emit(HIR_GT, TY_INT, rb, imm_val);
|
|
} else {
|
|
return -1;
|
|
}
|
|
h.native_ops++;
|
|
if (emit_cmp_branch(h, cmp, insn.k(), proto, pc, pc_to_block,
|
|
cur_hir_block, n, lua_reg, multi_block) < 0)
|
|
return -1;
|
|
pc++;
|
|
break;
|
|
}
|
|
case OP_LUA_GEI: {
|
|
int rb = lua_reg[A];
|
|
if (rb < 0) return -1;
|
|
if (lua_is_nil(h, truth_tag, rb)) return -1;
|
|
int cmp;
|
|
if (h.ty[rb] == TY_FLOAT) {
|
|
int fimm = h.emit_fconst(static_cast<double>(insn.sB()));
|
|
if (fimm < 0) return -1;
|
|
cmp = h.emit(HIR_FLE, TY_INT, fimm, rb); // swapped
|
|
} else if (h.ty[rb] == TY_INT) {
|
|
int imm_val = h.emit_iconst(insn.sB());
|
|
if (imm_val < 0) return -1;
|
|
cmp = h.emit(HIR_GE, TY_INT, rb, imm_val);
|
|
} else if (h.ty[rb] == TY_STRING) {
|
|
rb = promote_to_int(h, truth_tag, rb);
|
|
if (rb < 0) return -1;
|
|
int imm_val = h.emit_iconst(insn.sB());
|
|
if (imm_val < 0) return -1;
|
|
cmp = h.emit(HIR_GE, TY_INT, rb, imm_val);
|
|
} else {
|
|
return -1;
|
|
}
|
|
h.native_ops++;
|
|
if (emit_cmp_branch(h, cmp, insn.k(), proto, pc, pc_to_block,
|
|
cur_hir_block, n, lua_reg, multi_block) < 0)
|
|
return -1;
|
|
pc++;
|
|
break;
|
|
}
|
|
#undef CMP_RR
|
|
#undef CMP_RI
|
|
|
|
case OP_LUA_TEST: {
|
|
int rb = lua_reg[A];
|
|
if (rb < 0) return -1;
|
|
// HIR_BOOL is integer SNEZ / softcode truthiness. A CALL_STR
|
|
// result is type-erased text: "0", "" and integer-0-as-text are
|
|
// all truthy in Lua and falsy under that test (#1764). Decline
|
|
// rather than lie; CALL_VAL uses the VM's truthiness instead.
|
|
if (lua_is_marshalled_str(h, rb)) return -1;
|
|
int cmp;
|
|
if (lua_is_handle(h, rb) && h.kind[rb] == HIR_LUA_CALL_VAL) {
|
|
cmp = h.emit(HIR_LUA_TOBOOL, TY_INT, rb);
|
|
if (cmp < 0) return -1;
|
|
h.ecalls++;
|
|
} else if (lua_is_handle(h, rb)) {
|
|
return -1;
|
|
} else {
|
|
// Lua: only nil and false are falsy. VALUE (0, 0.0, "") is
|
|
// always truthy; BOOL uses HIR_BOOL; NIL is always falsy.
|
|
// UNKNOWN declines — do not invent VALUE (#1768).
|
|
const lua_truth tr = lua_truth_of(h, truth_tag, rb);
|
|
if (tr == LUA_TRUTH_UNKNOWN) {
|
|
return -1;
|
|
}
|
|
if (tr == LUA_TRUTH_NIL) {
|
|
cmp = h.emit_iconst(0);
|
|
} else if (tr == LUA_TRUTH_VALUE) {
|
|
cmp = h.emit_iconst(1);
|
|
} else if (h.ty[rb] == TY_INT) {
|
|
if (h.kind[rb] == HIR_ICONST) {
|
|
cmp = h.emit_iconst(h.val[rb] != 0 ? 1 : 0);
|
|
} else {
|
|
cmp = h.emit(HIR_BOOL, TY_INT, rb);
|
|
h.native_ops++;
|
|
}
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
if (cmp < 0) return -1;
|
|
if (emit_cmp_branch(h, cmp, insn.k(), proto, pc, pc_to_block,
|
|
cur_hir_block, n, nullptr, multi_block) < 0)
|
|
return -1;
|
|
pc++;
|
|
break;
|
|
}
|
|
|
|
case OP_LUA_TESTSET: {
|
|
int rb = lua_reg[insn.B()];
|
|
if (rb < 0) return -1;
|
|
if (lua_is_marshalled_str(h, rb)) return -1;
|
|
int cmp;
|
|
if (lua_is_handle(h, rb) && h.kind[rb] == HIR_LUA_CALL_VAL) {
|
|
cmp = h.emit(HIR_LUA_TOBOOL, TY_INT, rb);
|
|
if (cmp < 0) return -1;
|
|
h.ecalls++;
|
|
} else if (lua_is_handle(h, rb)) {
|
|
return -1;
|
|
} else {
|
|
const lua_truth tr = lua_truth_of(h, truth_tag, rb);
|
|
if (tr == LUA_TRUTH_UNKNOWN) {
|
|
return -1;
|
|
}
|
|
if (tr == LUA_TRUTH_NIL) {
|
|
cmp = h.emit_iconst(0);
|
|
} else if (tr == LUA_TRUTH_VALUE) {
|
|
cmp = h.emit_iconst(1);
|
|
} else if (h.ty[rb] == TY_INT) {
|
|
if (h.kind[rb] == HIR_ICONST) {
|
|
cmp = h.emit_iconst(h.val[rb] != 0 ? 1 : 0);
|
|
} else {
|
|
cmp = h.emit(HIR_BOOL, TY_INT, rb);
|
|
h.native_ops++;
|
|
}
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
if (cmp < 0) return -1;
|
|
lua_reg[A] = rb; // Simplified: always copy.
|
|
if (emit_cmp_branch(h, cmp, insn.k(), proto, pc, pc_to_block,
|
|
cur_hir_block, n, nullptr, multi_block) < 0)
|
|
return -1;
|
|
pc++;
|
|
break;
|
|
}
|
|
|
|
// ---- Control flow ----
|
|
|
|
case OP_LUA_JMP: {
|
|
int target = pc + 1 + insn.sJ();
|
|
if (!multi_block) return -1;
|
|
int target_blk = (target >= 0 && target < n) ? pc_to_block[target] : -1;
|
|
if (target_blk < 0) return -1;
|
|
|
|
// Back edge (while/repeat): the budget guard, then the jump.
|
|
// The shape this replaces folded exhaustion into an exit to
|
|
// the fall-through -- leaving the loop early and CONTINUING
|
|
// with a wrong partial result, the exact defect the FORLOOP
|
|
// budget had (#1732). The guard aborts to the interpreter
|
|
// instead.
|
|
if (target <= pc) {
|
|
if (!emit_backedge_guard(pc - target + 1)) return -1;
|
|
}
|
|
|
|
h.emit(HIR_BR, TY_VOID, -1, -1, target_blk);
|
|
h.add_edge(cur_hir_block, target_blk);
|
|
break;
|
|
}
|
|
|
|
// ---- Numeric for loop ----
|
|
//
|
|
// Lua 5.4 for-loop registers:
|
|
// R(A) = internal counter (never materialized here)
|
|
// R(A+1) = limit
|
|
// R(A+2) = step
|
|
// R(A+3) = exposed index (visible in body)
|
|
//
|
|
// 5.4 semantics, not 5.3's: FORPREP sets R(A+3)=init and FALLS
|
|
// INTO the body (jumping forward past FORLOOP only when the trip
|
|
// count is zero); FORLOOP steps the index and jumps BACK on
|
|
// continue. The 5.3-shaped lowering this replaces (init-step
|
|
// pre-subtraction, sBx offsets) was written against the wrong VM
|
|
// and had never executed behind the #1326 reject.
|
|
//
|
|
// STATIC BOUNDS ONLY in this first cut: init, limit and step must
|
|
// all be integer constants, so the trip direction, the zero-trip
|
|
// decision, and freedom from wraparound are compile-time facts --
|
|
// 5.4's own counter model exists precisely because a naive
|
|
// idx<=limit test misbehaves at the integer edge, and declining
|
|
// the edge is cheaper than reproducing the counter. The index
|
|
// rides QREG_LUA_IDX so hir_ssa_construct() gives it a real PHI.
|
|
|
|
case OP_LUA_FORPREP: {
|
|
if (!multi_block) return -1;
|
|
if (!lua_reg_in_range(A + 3)) return -1;
|
|
int init = lua_reg[A];
|
|
int limit = lua_reg[A + 1];
|
|
int step = lua_reg[A + 2];
|
|
if (init < 0 || limit < 0 || step < 0) return -1;
|
|
// The STEP must be a constant either way: it fixes the trip
|
|
// direction, and with it which comparison FORLOOP emits.
|
|
// step 0 is a runtime error; the interpreter raises it.
|
|
if (h.kind[step] != HIR_ICONST) return -1;
|
|
const int64_t vs = h.val[step];
|
|
if (0 == vs) return -1;
|
|
// Stay far from the int64 edge so idx+step cannot wrap.
|
|
const int64_t kEdge = INT64_C(1) << 62;
|
|
if (vs > kEdge || vs < -kEdge) return -1;
|
|
|
|
int body_target = pc + 1;
|
|
int skip_target = pc + 1 + insn.Bx() + 1;
|
|
int body_blk = (body_target < n) ? pc_to_block[body_target] : -1;
|
|
int skip_blk = (skip_target < n) ? pc_to_block[skip_target] : -1;
|
|
if (body_blk < 0 || skip_blk < 0) return -1;
|
|
if (A + 3 >= 10) return -1;
|
|
|
|
if (h.kind[init] == HIR_ICONST
|
|
&& h.kind[limit] == HIR_ICONST) {
|
|
// STATIC bounds: trip direction, zero-trip, and freedom
|
|
// from wraparound are compile-time facts, and the entry
|
|
// block stays whole.
|
|
const int64_t vi = h.val[init];
|
|
const int64_t vl = h.val[limit];
|
|
if (vi > kEdge || vi < -kEdge || vl > kEdge
|
|
|| vl < -kEdge) {
|
|
return -1;
|
|
}
|
|
// The body's first pass reads the index before any
|
|
// FORLOOP runs, so it must be stored on the entry side.
|
|
h.emit(HIR_STORE_Q, TY_VOID, init, -1, QREG_LUA_IDX);
|
|
h.emit(HIR_STORE_Q, TY_VOID, init, -1, A + 3);
|
|
const bool zero_trip = (vs > 0) ? (vi > vl) : (vi < vl);
|
|
int target_blk = zero_trip ? skip_blk : body_blk;
|
|
h.emit(HIR_BR, TY_VOID, -1, -1, target_blk);
|
|
h.add_edge(cur_hir_block, target_blk);
|
|
break;
|
|
}
|
|
|
|
// RUNTIME bounds (#1732): `for i=1,n`. The zero-trip test
|
|
// and the wraparound guard become branches. A value outside
|
|
// the int64 safety margin bails to the limited block -- a
|
|
// decline, not an error: the interpreter re-runs the chunk
|
|
// with its own counter model, and nothing has executed yet,
|
|
// so the bail is rerun-safe by position alone.
|
|
if (h.ty[init] != TY_INT || lua_is_handle(h, init)) return -1;
|
|
if (h.ty[limit] != TY_INT || lua_is_handle(h, limit)) return -1;
|
|
|
|
// This path SPLITS the entry block, so entry state must be
|
|
// finalized first: seal the backing, and re-snapshot
|
|
// blk_entry_reg so the next transition's drop-compare sees
|
|
// the split blocks as the same logical entry -- its values
|
|
// dominate the body exactly as block 0's do.
|
|
seal_entry_backing();
|
|
memcpy(blk_entry_reg, lua_reg, sizeof(blk_entry_reg));
|
|
|
|
int e_hi = h.emit_iconst(kEdge);
|
|
int e_lo = h.emit_iconst(-kEdge);
|
|
if (e_hi < 0 || e_lo < 0) return -1;
|
|
int b1 = h.emit(HIR_LT, TY_INT, init, e_hi);
|
|
int b2 = h.emit(HIR_GT, TY_INT, init, e_lo);
|
|
int b3 = h.emit(HIR_LT, TY_INT, limit, e_hi);
|
|
int b4 = h.emit(HIR_GT, TY_INT, limit, e_lo);
|
|
int b12 = h.emit(HIR_BAND, TY_INT, b1, b2);
|
|
int b34 = h.emit(HIR_BAND, TY_INT, b3, b4);
|
|
int bounds_ok = h.emit(HIR_BAND, TY_INT, b12, b34);
|
|
if (bounds_ok < 0) return -1;
|
|
|
|
h.emit(HIR_STORE_Q, TY_VOID, init, -1, QREG_LUA_IDX);
|
|
h.emit(HIR_STORE_Q, TY_VOID, init, -1, A + 3);
|
|
|
|
if (!ensure_limited_blk()) return -1;
|
|
int cont_blk = h.new_block();
|
|
if (cont_blk < 0) return -1;
|
|
h.emit(HIR_BRC, TY_VOID, bounds_ok, limited_blk, cont_blk);
|
|
h.add_edge(cur_hir_block, limited_blk);
|
|
h.add_edge(cur_hir_block, cont_blk);
|
|
h.cur_block = cont_blk;
|
|
cur_hir_block = cont_blk;
|
|
|
|
// Runtime zero-trip: run the body iff init is on the limit's
|
|
// side of the direction the constant step fixes.
|
|
int cond_run = h.emit((vs > 0) ? HIR_LE : HIR_GE,
|
|
TY_INT, init, limit);
|
|
if (cond_run < 0) return -1;
|
|
h.emit(HIR_BRC, TY_VOID, cond_run, skip_blk, body_blk);
|
|
h.add_edge(cur_hir_block, body_blk);
|
|
h.add_edge(cur_hir_block, skip_blk);
|
|
break;
|
|
}
|
|
|
|
case OP_LUA_FORLOOP: {
|
|
if (!multi_block) return -1;
|
|
if (!lua_reg_in_range(A + 3)) return -1;
|
|
// Bounds come from entry_final[], the register state frozen
|
|
// at the entry block's exit: lua_reg[] holds LOAD_Q reloads
|
|
// by now, and the static-bounds test below needs to SEE the
|
|
// ICONSTs FORPREP already vetted.
|
|
int step = entry_final[A + 2];
|
|
int limit = entry_final[A + 1];
|
|
if (step < 0 || limit < 0) return -1;
|
|
|
|
// Load current index from q-register (becomes PHI after SSA).
|
|
int idx = h.emit(HIR_LOAD_Q, TY_INT, -1, -1, QREG_LUA_IDX);
|
|
// Loop index is always a number, but the reload has no truth
|
|
// tag by construction — mark UNKNOWN so a future if-on-idx
|
|
// cannot invent VALUE after a lost BOOL (#1768).
|
|
if (idx >= 0) {
|
|
lua_truth_set(truth_tag, idx, LUA_TRUTH_UNKNOWN);
|
|
}
|
|
if (idx < 0) return -1;
|
|
|
|
// Increment: index = index + step.
|
|
int new_idx = h.emit(HIR_ADD, TY_INT, idx, step);
|
|
if (new_idx < 0) return -1;
|
|
h.native_ops++;
|
|
|
|
// Store updated index back to q-register.
|
|
h.emit(HIR_STORE_Q, TY_VOID, new_idx, -1, QREG_LUA_IDX);
|
|
|
|
// Expose the new index to subsequent instructions and back it
|
|
// in the visible register's own qreg. FORLOOP is the only
|
|
// terminator that writes a register, so the store-at-write
|
|
// hook cannot see this; every reader -- the body, the exit
|
|
// block -- is dominated by this latch, and the first pass
|
|
// reads the STORE_Q FORPREP emitted, so the backing the
|
|
// pre-scan declared is stored on every path (#1732).
|
|
lua_reg[A + 3] = new_idx;
|
|
if (A + 3 >= 10) return -1;
|
|
h.emit(HIR_STORE_Q, TY_VOID, new_idx, -1, A + 3);
|
|
|
|
// Continue test. The step is an ICONST -- FORPREP declined
|
|
// anything else -- so the direction is static; FORPREP's edge
|
|
// bound is what keeps new_idx from wrapping first.
|
|
if (h.kind[step] != HIR_ICONST) return -1;
|
|
int cmp = h.emit((h.val[step] > 0) ? HIR_LE : HIR_GE,
|
|
TY_INT, new_idx, limit);
|
|
if (cmp < 0) return -1;
|
|
h.native_ops++;
|
|
|
|
// Back-edge budget (#1732); see emit_backedge_guard. The
|
|
// body is Bx instructions plus this FORLOOP.
|
|
if (!emit_backedge_guard(insn.Bx() + 1)) return -1;
|
|
|
|
// Branch: if true, loop back; else fall through. 5.4 encodes
|
|
// the back edge as an unsigned Bx: pc -= Bx.
|
|
int loop_target = pc + 1 - insn.Bx();
|
|
int exit_target = pc + 1;
|
|
int loop_blk = (loop_target >= 0 && loop_target < n) ? pc_to_block[loop_target] : -1;
|
|
int exit_blk = (exit_target >= 0 && exit_target < n) ? pc_to_block[exit_target] : -1;
|
|
if (loop_blk < 0 || exit_blk < 0) return -1;
|
|
h.emit(HIR_BRC, TY_VOID, cmp, exit_blk, loop_blk);
|
|
h.add_edge(cur_hir_block, loop_blk);
|
|
h.add_edge(cur_hir_block, exit_blk);
|
|
break;
|
|
}
|
|
|
|
// ---- Return ----
|
|
//
|
|
// Lua always appends a trailing OP_RETURN / RETURN0 after an
|
|
// explicit return (and after the last statement of a chunk).
|
|
// That second return is dead once we have already emitted HIR_RET
|
|
// for the real value. Updating result_val from it overwrote a
|
|
// correct "hello" / 42 with an empty SCONST, so folded Lua JIT
|
|
// programs always produced empty strings (#1309).
|
|
//
|
|
// Emit HIR_RET for every opcode (keeps block structure), but only
|
|
// the first return value becomes h.result.
|
|
|
|
case OP_LUA_RETURN0: {
|
|
int rv = h.emit_sconst(rc.pool_str("", 0), "");
|
|
if (rv < 0) return -1;
|
|
h.emit(HIR_RET, TY_VOID, rv);
|
|
if (result_val < 0) {
|
|
result_val = rv;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case OP_LUA_RETURN1: {
|
|
int rv = return_as_string(h, rc, lua_reg[A]);
|
|
if (rv < 0) return -1;
|
|
h.emit(HIR_RET, TY_VOID, rv);
|
|
if (result_val < 0) {
|
|
result_val = rv;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case OP_LUA_RETURN: {
|
|
int nret = insn.B() - 1;
|
|
if (nret < 0) {
|
|
// B == 0 is "return all values from A up" (in-top). The
|
|
// one shape that produces it here is the dead trailing
|
|
// return Lua appends after OP_TAILCALL, whose lowering
|
|
// already emitted the real HIR_RET and claimed result_val;
|
|
// give it RETURN0's shape so the block still terminates.
|
|
// Any other multret return stays declined.
|
|
if (0 == pc
|
|
|| OP_LUA_TAILCALL != proto->code[pc - 1].opcode()) {
|
|
return -1;
|
|
}
|
|
int dead = h.emit_sconst(rc.pool_str("", 0), "");
|
|
if (dead < 0) return -1;
|
|
h.emit(HIR_RET, TY_VOID, dead);
|
|
if (result_val < 0) {
|
|
result_val = dead;
|
|
}
|
|
break;
|
|
}
|
|
int rv;
|
|
if (nret == 0) {
|
|
rv = h.emit_sconst(rc.pool_str("", 0), "");
|
|
if (rv < 0) return -1;
|
|
} else {
|
|
rv = return_as_string(h, rc, lua_reg[A]);
|
|
if (rv < 0) return -1;
|
|
}
|
|
h.emit(HIR_RET, TY_VOID, rv);
|
|
if (result_val < 0) {
|
|
result_val = rv;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// ---- Upvalue access ----
|
|
// We reject nested protos, so the only upvalue is _ENV (index 0).
|
|
|
|
case OP_LUA_GETUPVAL: {
|
|
// A = dest, B = upvalue index.
|
|
// For the main chunk, upvalue 0 = _ENV (global table).
|
|
if (insn.B() != 0) return -1; // Non-_ENV upvalue.
|
|
// Push _ENV onto Lua stack via __lua_getglobal equivalent.
|
|
// Actually, just reject — GETUPVAL on _ENV is rare; scripts
|
|
// use GETTABUP for _ENV[key] access which is already handled.
|
|
// If someone does `local g = _ENV`, they get GETUPVAL.
|
|
std::string name("__lua_getenv");
|
|
int args[1];
|
|
int dummy = h.emit_iconst(0);
|
|
args[0] = dummy;
|
|
lua_reg[A] = h.emit_call(TY_STRING, 0, args, 1, &name);
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.ecalls++;
|
|
break;
|
|
}
|
|
|
|
case OP_LUA_SETUPVAL: {
|
|
// A = source register, B = upvalue index.
|
|
// Setting _ENV is unusual and dangerous. Reject.
|
|
return -1;
|
|
}
|
|
|
|
// ---- Global access, method calls, and function calls ----
|
|
|
|
case OP_LUA_GETTABUP: {
|
|
if (insn.B() != 0) return -1; // Only _ENV (upvalue 0).
|
|
int kidx = insn.C();
|
|
if (kidx < 0 || kidx >= static_cast<int>(proto->constants.size()))
|
|
return -1;
|
|
const lua_bc_constant &k = proto->constants[kidx];
|
|
if (k.type != LUA_BC_TSHRSTR && k.type != LUA_BC_TLNGSTR)
|
|
return -1;
|
|
|
|
if (k.sval == "mux") {
|
|
// mux.* bridge pattern — sentinel for GETFIELD+CALL.
|
|
uint64_t addr = rc.pool_str("mux", 3);
|
|
lua_reg[A] = h.emit_sconst(addr, "mux");
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.lua_mux_sentinel[lua_reg[A]] = true;
|
|
} else {
|
|
// General global access: _ENV[key] via ECALL.
|
|
// Pushes table/function onto Lua stack, returns stack
|
|
// index as string. Lua stack cleanup is handled by
|
|
// TryJIT's save/restore around RunCompiled.
|
|
uint64_t key_addr = rc.pool_str(k.sval.c_str(), k.sval.size());
|
|
int key_val = h.emit_sconst(key_addr, k.sval);
|
|
if (key_val < 0) return -1;
|
|
lua_reg[A] = h.emit(HIR_LUA_GETGLOBAL, TY_LUA_HANDLE,
|
|
key_val);
|
|
if (lua_reg[A] < 0) return -1;
|
|
// A global's referent is not knowable here -- `math` is a
|
|
// table, `tostring` is a function, and a game can rebind
|
|
// either -- so claim both capabilities and let each use's
|
|
// runtime check settle it: field reads take references,
|
|
// calls are allowed with the result type the name claims.
|
|
lua_referent g;
|
|
g.fields_are_refs = true;
|
|
g.callable = true;
|
|
g.returns = lua_call_claim(k.sval);
|
|
g.call_name = k.sval;
|
|
if (k.sval == "math") {
|
|
g.values = k_lua_math_values;
|
|
}
|
|
lua_ref[lua_reg[A]] = g;
|
|
h.known_int[lua_reg[A]] = true;
|
|
h.ecalls++;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// SETTABUP: set _ENV[key] = value.
|
|
case OP_LUA_SETTABUP: {
|
|
if (insn.A() != 0) return -1; // Only _ENV.
|
|
int kidx = insn.B();
|
|
if (kidx < 0 || kidx >= static_cast<int>(proto->constants.size()))
|
|
return -1;
|
|
const lua_bc_constant &k = proto->constants[kidx];
|
|
if (k.type != LUA_BC_TSHRSTR && k.type != LUA_BC_TLNGSTR)
|
|
return -1;
|
|
int val = lua_reg[insn.C()];
|
|
if (val < 0) return -1;
|
|
if (h.ty[val] == TY_INT) {
|
|
val = h.emit(HIR_ITOA, TY_STRING, val);
|
|
if (val < 0) return -1;
|
|
} else if (h.ty[val] == TY_FLOAT) {
|
|
val = h.emit(HIR_FTOA, TY_STRING, val);
|
|
if (val < 0) return -1;
|
|
}
|
|
uint64_t key_addr = rc.pool_str(k.sval.c_str(), k.sval.size());
|
|
int key_val = h.emit_sconst(key_addr, k.sval);
|
|
if (key_val < 0) return -1;
|
|
// Global writes have no handler; ineligible at lowering
|
|
// (#1751 rule 1) rather than a guaranteed post-entry fail.
|
|
return -1;
|
|
}
|
|
|
|
// SELF: A = dest, B = table register, C = method key constant.
|
|
// R(A+1) := R(B); R(A) := R(B)[K(C)]
|
|
case OP_LUA_SELF: {
|
|
if (!lua_reg_in_range(A + 1)) return -1;
|
|
int tbl = lua_reg[insn.B()];
|
|
if (tbl < 0) return -1;
|
|
// Copy table to R(A+1) for method call.
|
|
lua_reg[A + 1] = tbl;
|
|
// Load method: t[key].
|
|
int kidx = insn.C();
|
|
if (kidx < 0 || kidx >= static_cast<int>(proto->constants.size()))
|
|
return -1;
|
|
const lua_bc_constant &k = proto->constants[kidx];
|
|
if (k.type != LUA_BC_TSHRSTR && k.type != LUA_BC_TLNGSTR)
|
|
return -1;
|
|
uint64_t key_addr = rc.pool_str(k.sval.c_str(), k.sval.size());
|
|
int key_val = h.emit_sconst(key_addr, k.sval);
|
|
if (key_val < 0) return -1;
|
|
// Method dispatch has no handler; ineligible at lowering
|
|
// (#1751 rule 1).
|
|
return -1;
|
|
}
|
|
|
|
case OP_LUA_TAILCALL:
|
|
// `return f(...)`: the call below, then the return the helper
|
|
// emits at each successful exit. The real tail-call mechanism
|
|
// reuses the caller's frame; nothing here does -- the callee
|
|
// runs via an ECALL doing its own pcall -- and the chunk-level
|
|
// pcall asks for one result either way, so a plain call
|
|
// observes the same thing. k set means upvalues to close,
|
|
// which the CLOSURE reject should make impossible; decline
|
|
// rather than assume. C is frame correction for the frame
|
|
// reuse that is not happening.
|
|
if (insn.k()) return -1;
|
|
// fall through
|
|
case OP_LUA_CALL: {
|
|
int func_reg = lua_reg[A];
|
|
if (func_reg < 0) return -1;
|
|
|
|
int nargs = insn.B() - 1;
|
|
// TAILCALL has no C-encoded result count: it returns what the
|
|
// callee returns, of which the chunk boundary keeps one.
|
|
int nresults = (OP_LUA_TAILCALL == op) ? 1 : insn.C() - 1;
|
|
if (nargs < 0) return -1; // Variable args not supported.
|
|
|
|
// Direct call on a handle with a callable claim. CALL_INT and
|
|
// CALL_STR share one argument encoding (nargs, argkind bits,
|
|
// arg registers) and differ only in how the result comes back:
|
|
// in a register, or marshalled into an output slot whose SIZE
|
|
// the ECALL is told rather than assumes (#1679). Which one to
|
|
// emit is the handle's claimed result type, recorded where the
|
|
// handle was created -- one claim, so the two variants cannot
|
|
// disagree about a name the way the twin gated branches this
|
|
// replaces could (d5e5e86e0).
|
|
//
|
|
// Arguments may be integers, CONSTANT strings, floats --
|
|
// constant or runtime -- or Lua HANDLES, with TWO kind bits
|
|
// per argument telling codegen and the handler what each
|
|
// register carries (0 integer, 1 string address, 2 double as
|
|
// raw bits over the FMV.X.D lane, 3 stack reference). Floats
|
|
// travel honestly rather than as rendered text because
|
|
// coercion would lie to a type-sensitive callee:
|
|
// math.type("3.0") is nil, not "float". A handle argument is
|
|
// the index for a lua_pushvalue -- the one use of a handle
|
|
// that is ABOUT the thing it points at (#1579), which is what
|
|
// table.insert(t,4) needs. A runtime string argument would
|
|
// need its own guest buffer and is left for when something
|
|
// needs it.
|
|
//
|
|
// nresults == 0 is a call FOR the effect -- table.insert --
|
|
// and takes CALL_VOID: no result register, no result-type
|
|
// claim to check, and the destination Lua registers become
|
|
// dead exactly as the VM's would.
|
|
const lua_referent fref = lua_referent_of(lua_ref, func_reg);
|
|
// A FLOAT-returning claim has no marshalling; ineligible at
|
|
// lowering, interpreter answers with the right subtype.
|
|
if (fref.callable && TY_FLOAT == fref.returns) {
|
|
return -1;
|
|
}
|
|
// Effectful callees (mux.pemit/set/eval/…) are eligible: the
|
|
// compiled path runs the same bridge C functions as the
|
|
// interpreter. Pre-Phase-4 they were ineligible because a
|
|
// later runtime decline re-ran the chunk and doubled effects.
|
|
// The string form keeps its historical one-argument floor; the
|
|
// integer form and the effect-only form allow zero.
|
|
const int min_args =
|
|
(0 == nresults || TY_INT == fref.returns) ? 0 : 1;
|
|
if (fref.callable
|
|
&& (0 == nresults || 1 == nresults)
|
|
&& nargs >= min_args && nargs <= 3) {
|
|
int cargs[3] = { -1, -1, -1 };
|
|
int kinds = 0;
|
|
bool ok = true;
|
|
for (int i = 0; i < nargs && ok; i++) {
|
|
if (!lua_reg_in_range(A + 1 + i)) { ok = false; break; }
|
|
int areg = lua_reg[A + 1 + i];
|
|
if (areg < 0) { ok = false; break; }
|
|
// nil as an argument would arrive as "" -- tostring(nil)
|
|
// is "nil", not "". No kind encodes nil, so decline.
|
|
if (lua_is_nil(h, truth_tag, areg)) { ok = false; break; }
|
|
// A sentinel would travel as the string "mux.args";
|
|
// type() answered "string", tostring() the name.
|
|
if (lua_is_mux_sentinel(h, areg)) { ok = false; break; }
|
|
if (lua_is_handle(h, areg)) {
|
|
kinds |= (3 << (2 * i)); // stack reference
|
|
// The callee may setmetatable the table: the
|
|
// plain proof does not survive an escape.
|
|
lua_ref[areg].plain_proven = false;
|
|
} else if (h.ty[areg] == TY_INT) {
|
|
// integer: kind 0
|
|
} else if (h.kind[areg] == HIR_SCONST) {
|
|
kinds |= (1 << (2 * i)); // string address
|
|
} else if (h.ty[areg] == TY_FLOAT) {
|
|
kinds |= (2 << (2 * i)); // double, raw bits
|
|
} else {
|
|
ok = false; break;
|
|
}
|
|
cargs[i] = areg;
|
|
}
|
|
if (ok) {
|
|
// Arguments ride the carg[] list; val[] carries only
|
|
// the kind bits.
|
|
//
|
|
// #1866: tonumber is dynamic int|float. Upgrade to
|
|
// CALL_INT only when the arg proves integral so the
|
|
// common tonumber("17") / tonumber(n) path stays
|
|
// native; non-integral and runtime strings keep
|
|
// CALL_VAL (no post-entry residual on floats).
|
|
//
|
|
hir_type result_ty = fref.returns;
|
|
if (fref.call_name == "tonumber" && 1 == nargs
|
|
&& lua_tonumber_arg_is_integral(h, cargs[0])) {
|
|
result_ty = TY_INT;
|
|
}
|
|
if (0 == nresults) {
|
|
if (h.emit_lua_call(HIR_LUA_CALL_VOID, TY_VOID,
|
|
func_reg, cargs, nargs, kinds) < 0) {
|
|
return -1;
|
|
}
|
|
lua_reg[A] = -1;
|
|
} else if (TY_INT == result_ty) {
|
|
lua_reg[A] = h.emit_lua_call(HIR_LUA_CALL_INT,
|
|
TY_INT, func_reg, cargs, nargs, kinds);
|
|
if (lua_reg[A] < 0) return -1;
|
|
h.known_int[lua_reg[A]] = true;
|
|
} else {
|
|
// Default claim: keep the Lua value on the VM stack
|
|
// as a handle. Marshal only at chunk return
|
|
// (HIR_LUA_MARSHAL). Consumers use Lua semantics
|
|
// (TOBOOL, pushvalue args) instead of softcode text.
|
|
lua_reg[A] = h.emit_lua_call(HIR_LUA_CALL_VAL,
|
|
TY_LUA_HANDLE, func_reg, cargs, nargs, kinds);
|
|
if (lua_reg[A] < 0) return -1;
|
|
}
|
|
h.ecalls++;
|
|
for (int i = A + 1; i < A + 1 + nargs; i++) {
|
|
if (lua_reg_in_range(i)) lua_reg[i] = -1;
|
|
}
|
|
if (OP_LUA_TAILCALL == op
|
|
&& lua_tailcall_ret(h, rc, lua_reg[A],
|
|
result_val) < 0) {
|
|
return -1;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// A call the typed CALL_INT/STR/VOID path cannot encode --
|
|
// arity above three, argument shapes outside the kind
|
|
// encoding, an unclaimed callee -- has no honest compiled
|
|
// form. The original Phase 2 draft resurrected the named
|
|
// __lua_call here with the handle ITOA'd through guest
|
|
// memory and every argument STRINGIFIED; review measured a
|
|
// table argument arriving in string.format as its stack
|
|
// index rendered in decimal (#1424's exact resurrection) and
|
|
// the plan itself names string lies a non-goal. Until a
|
|
// TYPED general-call encoding exists, these shapes are
|
|
// ineligible at lowering (#1751 rule 1): the interpreter
|
|
// answers, silently and correctly.
|
|
return -1;
|
|
if (OP_LUA_TAILCALL == op
|
|
&& lua_tailcall_ret(h, rc, lua_reg[A], result_val) < 0) {
|
|
return -1;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// ---- Generic for-loop (TFOR) ----
|
|
//
|
|
// R(A) = iterator function (Lua stack ref)
|
|
// R(A+1) = invariant state (Lua stack ref)
|
|
// R(A+2) = control variable
|
|
// R(A+3) = to-be-closed (not used without TBC)
|
|
// R(A+4)... = iterator results (key, value, ...)
|
|
|
|
case OP_LUA_TFORPREP: {
|
|
// Jump forward to TFORLOOP for initial nil check.
|
|
if (!multi_block) return -1;
|
|
int target = pc + 1 + insn.sBx();
|
|
int target_blk = (target >= 0 && target < n) ? pc_to_block[target] : -1;
|
|
if (target_blk < 0) return -1;
|
|
h.emit(HIR_BR, TY_VOID, -1, -1, target_blk);
|
|
h.add_edge(cur_hir_block, target_blk);
|
|
break;
|
|
}
|
|
|
|
case OP_LUA_TFORCALL: {
|
|
// Call iterator: R(A+4),...,R(A+3+C) = R(A)(R(A+1), R(A+2))
|
|
if (!lua_reg_in_range(A + 4)) return -1;
|
|
int iter_func = lua_reg[A];
|
|
int iter_state = lua_reg[A + 1];
|
|
int iter_control = lua_reg[A + 2];
|
|
if (iter_func < 0 || iter_state < 0) return -1;
|
|
|
|
// Control variable might be nil (empty string) on first call.
|
|
if (iter_control < 0) {
|
|
iter_control = h.emit_sconst(rc.pool_str("", 0), "");
|
|
if (iter_control < 0) return -1;
|
|
}
|
|
|
|
// Convert control to string if needed.
|
|
if (h.ty[iter_control] == TY_INT) {
|
|
iter_control = h.emit(HIR_ITOA, TY_STRING, iter_control);
|
|
if (iter_control < 0) return -1;
|
|
}
|
|
|
|
// TFOR is rejected at eligibility; if that ever lifts, the
|
|
// iterator call needs a real typed encoding, not the dead
|
|
// named bridge. Ineligible (#1751 rule 1).
|
|
return -1;
|
|
}
|
|
|
|
case OP_LUA_TFORLOOP: {
|
|
// if R(A+4) ~= nil then R(A+2) = R(A+4); jump back
|
|
if (!multi_block) return -1;
|
|
if (!lua_reg_in_range(A + 4)) return -1;
|
|
int first_result = lua_reg[A + 4];
|
|
if (first_result < 0) return -1;
|
|
|
|
// Check if first result is empty (nil in string form).
|
|
// Use STRLEN-like check: if the string is empty, done.
|
|
int fidx = engine_api_lookup("STRLEN");
|
|
int len_val;
|
|
if (fidx > 0) {
|
|
int args[] = { first_result };
|
|
len_val = h.emit_call(TY_STRING, fidx, args, 1);
|
|
if (len_val < 0) return -1;
|
|
h.known_int[len_val] = true;
|
|
} else {
|
|
return -1;
|
|
}
|
|
|
|
// ATOI the length, check if > 0.
|
|
int len_int = h.emit(HIR_ATOI, TY_INT, len_val);
|
|
if (len_int < 0) return -1;
|
|
int zero = h.emit_iconst(0);
|
|
int cmp = h.emit(HIR_GT, TY_INT, len_int, zero);
|
|
if (cmp < 0) return -1;
|
|
|
|
// Back-edge budget check. (TFOR protos are rejected at
|
|
// eligibility; this fold-into-the-condition shape is the one
|
|
// #1732 replaced elsewhere and must be reworked like FORLOOP
|
|
// if TFOR is ever lifted.)
|
|
cmp = emit_budget_check(h, cmp, 1);
|
|
|
|
// If non-nil: set control = first_result, loop back.
|
|
int loop_target = pc + 1 + insn.sBx();
|
|
int exit_target = pc + 1;
|
|
int loop_blk = (loop_target >= 0 && loop_target < n) ? pc_to_block[loop_target] : -1;
|
|
int exit_blk = (exit_target >= 0 && exit_target < n) ? pc_to_block[exit_target] : -1;
|
|
if (loop_blk < 0 || exit_blk < 0) return -1;
|
|
|
|
// Update control variable.
|
|
lua_reg[A + 2] = first_result;
|
|
|
|
h.emit(HIR_BRC, TY_VOID, cmp, exit_blk, loop_blk);
|
|
h.add_edge(cur_hir_block, loop_blk);
|
|
h.add_edge(cur_hir_block, exit_blk);
|
|
break;
|
|
}
|
|
|
|
// ---- No-op instructions ----
|
|
case OP_LUA_CLOSE: // No open upvalues without closures.
|
|
case OP_LUA_VARARGPREP:
|
|
case OP_LUA_EXTRAARG:
|
|
case OP_LUA_MMBIN:
|
|
case OP_LUA_MMBINI:
|
|
case OP_LUA_MMBINK:
|
|
break;
|
|
|
|
// ---- Unsupported opcodes ----
|
|
default:
|
|
return -1;
|
|
}
|
|
|
|
// Store-at-write (#1732): mirror this instruction's register
|
|
// writes into the q-registers, so the value crosses the next
|
|
// block boundary as PHI-convertible q-reg traffic. Terminator
|
|
// opcodes are skipped -- their block is already closed, and the
|
|
// only terminator that writes a register, FORLOOP, stores its own
|
|
// writes inside its case. Comparisons that FUSE (lua_bool_fuse)
|
|
// also skip; their write stays plain and declines on a later
|
|
// cross-block read rather than answering wrongly.
|
|
if (proto_has_loop) {
|
|
bool is_terminator;
|
|
switch (op) {
|
|
case OP_LUA_JMP: case OP_LUA_FORPREP: case OP_LUA_FORLOOP:
|
|
case OP_LUA_RETURN: case OP_LUA_RETURN0: case OP_LUA_RETURN1:
|
|
case OP_LUA_EQ: case OP_LUA_EQK: case OP_LUA_EQI:
|
|
case OP_LUA_LT: case OP_LUA_LE: case OP_LUA_LTI:
|
|
case OP_LUA_LEI: case OP_LUA_GTI: case OP_LUA_GEI:
|
|
case OP_LUA_TEST:
|
|
is_terminator = true;
|
|
break;
|
|
default:
|
|
is_terminator = false;
|
|
break;
|
|
}
|
|
if (!is_terminator) {
|
|
for (int r = 0; r < 10 && r < MAX_LUA_REGS; r++) {
|
|
if (lua_reg[r] == pre_reg[r] || lua_reg[r] < 0) {
|
|
continue;
|
|
}
|
|
const bool is_int =
|
|
(h.ty[lua_reg[r]] == TY_INT)
|
|
&& !lua_is_handle(h, lua_reg[r]);
|
|
if (0 == cur_hir_block && !entry_backing_sealed) {
|
|
if (is_int) {
|
|
h.emit(HIR_STORE_Q, TY_VOID, lua_reg[r], -1, r);
|
|
qreg_backed[r] = true;
|
|
} else if (forloop_backed[r]) {
|
|
// The loop variable's register holding a
|
|
// non-int before the loop would leave the
|
|
// reload machinery a stale value; decline.
|
|
return -1;
|
|
}
|
|
} else if (qreg_backed[r] || forloop_backed[r]) {
|
|
// A backed register going non-int would leave a
|
|
// stale integer for the next reload to resurrect.
|
|
if (!is_int) return -1;
|
|
h.emit(HIR_STORE_Q, TY_VOID, lua_reg[r], -1, r);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (result_val < 0) return -1;
|
|
|
|
// Every block a branch can reach must have been lowered into. A
|
|
// reachable-but-empty block means some opcode's control flow was modelled
|
|
// as a linear step and swallowed a block leader whole: the skipped path's
|
|
// body ends up in the wrong block, and the block the branch actually
|
|
// targets is a hole. That was #1421 (OP_LFALSESKIP), and the same shape
|
|
// is available to any future opcode that advances pc by hand. Catching
|
|
// it here makes the whole class decline instead of silently answering
|
|
// with the other arm's value (#1501).
|
|
{
|
|
bool has_insn[HIR_MAX_BLOCKS];
|
|
memset(has_insn, 0, sizeof(has_insn));
|
|
for (int i = 0; i < h.n_insns; i++) {
|
|
int b = h.blk[i];
|
|
if (b >= 0 && b < HIR_MAX_BLOCKS) has_insn[b] = true;
|
|
}
|
|
for (int b = 0; b < h.n_blocks && b < HIR_MAX_BLOCKS; b++) {
|
|
if (!has_insn[b]) continue; // b unreachable itself; harmless
|
|
for (int s = 0; s < h.block_nsucc[b]; s++) {
|
|
int t = h.block_succ[b][s];
|
|
if (t >= 0 && t < h.n_blocks && !has_insn[t]) return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
h.result = result_val;
|
|
// ecalls/native_ops force a runtime path. Also keep needs_jit if
|
|
// lowering already set it (mux.args → CARGS srefs have no ecall/native
|
|
// count but must not take the folded path with empty sval) (#1309).
|
|
//
|
|
if (h.ecalls > 0 || h.native_ops > 0) {
|
|
h.needs_jit = true;
|
|
} else if (!h.sref_addrs.empty()) {
|
|
h.needs_jit = true;
|
|
}
|
|
|
|
return result_val;
|
|
}
|