tinymux/mux/modules/engine/jit_lua.cpp
Stephen Dennis 70384ff2b2 fix(lua/jit): load tier2 on the Lua path so ^ can compile (#1561)
softcode entry points already call tier2_lazy_init; the Lua compile path
did not. With jit_eval_brackets 0 (required for Lua JIT under #1326)
softcode never hits jit_eval, so softlib.rv64 stayed unloaded and every
tier2_sym_addr("pow") decline made runtime ^ vacuous.

tier2_ensure() from compile_lua_bytecode loads the blob once. Stacked on
#1488 so integral ^ results render as "4.0"/"8.0" rather than "4"/"8".
EXEC pins for return 3.0, a+b, and runtime ^ keep both regressions closed.
2026-07-27 13:51:32 +00:00

412 lines
14 KiB
C++

/*! \file jit_lua.cpp
* \brief CJITCompile COM class — Lua bytecode → native JIT compilation.
*
* Implements mux_IJITCompile. Deserializes Lua 5.4 bytecode,
* lowers through HIR/RV64/x86-64 pipeline, caches compiled programs.
*/
#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 <atomic>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <unordered_map>
#include <vector>
#include <string>
// ---------------------------------------------------------------
// Compile cache
// ---------------------------------------------------------------
static std::unordered_map<uint64_t, compiled_program> s_lua_cache;
static std::atomic<uint64_t> s_next_key{1};
// ---------------------------------------------------------------
// Statistics
// ---------------------------------------------------------------
struct lua_jit_stats {
uint64_t compile_ok;
uint64_t compile_fail;
uint64_t run_ok;
uint64_t run_fail;
uint64_t cache_hits;
uint64_t invalidations;
};
static lua_jit_stats s_lua_jit_stats = {};
// Published to jitstats() (#1316). These counters are the only way to tell
// a Lua JIT that runs from one that compiles and then silently falls back.
//
void jit_lua_get_stats(lua_jit_counters *out) {
if (nullptr == out) return;
out->compile_ok = s_lua_jit_stats.compile_ok;
out->compile_fail = s_lua_jit_stats.compile_fail;
out->run_ok = s_lua_jit_stats.run_ok;
out->run_fail = s_lua_jit_stats.run_fail;
out->cache_hits = s_lua_jit_stats.cache_hits;
out->invalidations = s_lua_jit_stats.invalidations;
}
void jit_lua_reset_stats(void) {
s_lua_jit_stats = {};
}
void jit_lua_clear_cache(void) {
const size_t n = s_lua_cache.size();
s_lua_cache.clear();
// Count each dropped entry as an invalidation so jitstats() shows the
// flush happened; operators comparing before/after can see the drop.
//
s_lua_jit_stats.invalidations += n;
}
// ---------------------------------------------------------------
// Compile a Lua bytecode blob to a compiled_program.
// ---------------------------------------------------------------
static bool compile_lua_bytecode(const uint8_t *data, size_t len,
compiled_program *out) {
// TINYMUX_DUMP_HIR covered only the softcode JIT, so nothing on this path
// was visible: neither the block layout nor, more basically, whether a
// chunk compiled at all. A declined chunk runs on the interpreter and so
// agrees with it trivially, which reads as a pass when comparing the two
// — the reason the declines below are reported and not just the dumps.
//
const char *dump_env = getenv("TINYMUX_DUMP_HIR");
bool bDump = (dump_env && *dump_env != '0');
// Softcode entry points load the Tier 2 blob; the Lua path used not to.
// With jit_eval_brackets 0 (required for Lua JIT under #1326) softcode
// never hits jit_eval, so the blob stayed unloaded and every `^` that
// needs tier2_sym_addr("pow") declined at lowering (#1561). Ensure once
// here so native FP lowers can resolve symbols; missing softlib still
// leaves s_tier2.loaded false and those ops decline cleanly.
//
tier2_ensure();
// Deserialize.
lua_bc_chunk chunk;
if (!lua_bc_load(data, len, &chunk)) {
if (bDump) {
printf("\n--- Lua JIT: declined, bytecode failed to load ---\n");
}
return false;
}
// Fast eligibility check — reject before allocating HIR/RV64 state.
lua_bc_reject reason = lua_bc_eligible(&chunk.main);
if (reason != LUA_BC_ELIGIBLE) {
if (bDump) {
printf("\n--- Lua JIT: %s declined by eligibility: %s ---\n",
chunk.main.source.c_str(), lua_bc_reject_name(reason));
}
return false;
}
// Create HIR program and RV64 compiler state.
hir_program *h = new hir_program;
h->init();
rv_compiler rc_state;
// Lower Lua bytecode to HIR.
int result = hir_lower_lua_proto(*h, rc_state, &chunk.main);
if (result < 0) {
if (bDump) {
printf("\n--- Lua JIT: %s declined by lowering"
" (%d bytecode insns) ---\n",
chunk.main.source.c_str(),
static_cast<int>(chunk.main.code.size()));
}
delete h;
return false;
}
// The Lua path never consulted h->overflowed (#1501). It checked only the
// -1 returned by hir_lower_lua_proto, which catches a refusal the lowering
// propagated all the way out and misses one a consumer swallowed -- and
// then ran SSA, the optimizer and codegen over a program whose lowering had
// stopped partway. compile_expression has had this check since #859; the
// Lua entry point is the same pipeline and needs it too, plus the
// consumer-side refused_index() half.
//
if (h->overflowed || h->refused_index()) {
if (bDump) {
printf("\n--- Lua JIT: %s declined by overflow/refused index"
" (%d bytecode insns) ---\n",
chunk.main.source.c_str(),
static_cast<int>(chunk.main.code.size()));
}
delete h;
return false;
}
if (bDump) {
printf("\n--- Lua JIT Compilation: %s (%d bytecode insns) ---\n",
chunk.main.source.c_str(),
static_cast<int>(chunk.main.code.size()));
printf("Phase 1: HIR Lowering\n");
hir_dump(*h);
}
// Always build block ranges (block_last starts at -1 until CFG is
// computed). Without this, single-block Lua programs skip every HIR
// insn in hir_codegen and leave final_out=0 (#1309 empty folds).
//
hir_build_cfg(*h);
if (bDump) {
printf("Phase 2: CFG (%d blocks)\n", h->n_blocks);
hir_dump(*h);
}
if (h->n_blocks > 1) {
hir_ssa_construct(*h);
if (bDump) {
printf("Phase 2b: SSA Construction\n");
hir_dump(*h);
}
hir_optimize(*h);
} else {
// Single block: just constant folding.
hir_const_fold(*h);
}
if (bDump) {
printf("Phase 3: %s\n",
h->n_blocks > 1 ? "SSA Optimization" : "Constant Folding");
hir_dump(*h);
}
// SSA construction and the optimizer can exhaust capacity themselves, after
// the check above has already run -- the same reason compile_expression
// re-checks between phases (#1149).
//
if (h->overflowed || h->refused_index()) {
delete h;
return false;
}
// Code generation: HIR → RV64.
hir_codegen(*h, rc_state);
// Copy code into guest memory at code_base — same as softcode
// compile_expression. Without this, code lives only in rc_state.code
// while memory's code region stays zero; compact then materializes
// zeros at entry_pc and the DBT spins forever at guest PC 0
// (unknown opcode → exit_with_pc(same) → #1309 hang).
//
if (rc_state.code.size() * 4 > rv_compiler::CODE_LIMIT) {
delete h;
return false;
}
for (size_t i = 0; i < rc_state.code.size(); i++) {
memcpy(rc_state.memory.data() + rc_state.code_base + i * 4,
&rc_state.code[i], 4);
}
if (rc_state.out_exhausted || rc_state.pool_exhausted) {
delete h;
return false;
}
// Build compiled_program output.
// Include rc_state.needs_jit: codegen may force runtime for results that
// live only in registers (ITOA path), even when lowering saw no ecalls.
//
out->memory = std::move(rc_state.memory);
out->memory_size = rv_compiler::MEM_SIZE;
out->out_addr = rc_state.final_out;
out->out_used = rc_state.out_pool;
out->entry_pc = rc_state.code_base;
out->code_size = rc_state.code.size() * 4;
out->str_pool_end = rc_state.str_pool;
out->fargs_pool_end = rc_state.fargs_pool;
out->out_pool_end = rc_state.out_pool;
out->ok = true;
out->folds = h->folds;
out->ecalls = h->ecalls;
out->tier2_calls = 0;
out->native_ops = h->native_ops;
out->needs_jit = h->needs_jit || rc_state.needs_jit;
// Classify CARGS/SUBST refs (mux.args[N] → emit_sref) so
// run_cached_program populates the guest slots this program reads.
// Same pass as softcode compile_expression.
//
out->subst_mask = 0;
out->cargs_used = 0;
for (uint64_t a : h->sref_addrs) {
if ( a >= rv_compiler::CARGS_BASE
&& a < rv_compiler::CARGS_BASE
+ static_cast<uint64_t>(rv_compiler::MAX_CARGS)
* rv_compiler::CARGS_SLOT) {
int idx = static_cast<int>(
(a - rv_compiler::CARGS_BASE) / rv_compiler::CARGS_SLOT);
if (idx + 1 > out->cargs_used) {
out->cargs_used = idx + 1;
}
} else if ( a >= rv_compiler::SUBST_BASE
&& a < rv_compiler::SUBST_BASE
+ static_cast<uint64_t>(rv_compiler::SUBST_COUNT)
* rv_compiler::SUBST_SLOT) {
int slot = static_cast<int>(
(a - rv_compiler::SUBST_BASE) / rv_compiler::SUBST_SLOT);
out->subst_mask |= (UINT64_C(1) << slot);
}
}
delete h;
return true;
}
// ---------------------------------------------------------------
// CJITCompile COM class
// ---------------------------------------------------------------
class CJITCompile : public mux_IJITCompile
{
public:
CJITCompile(void) : m_cRef(1) {}
virtual ~CJITCompile() {}
// mux_IUnknown
MUX_RESULT QueryInterface(MUX_IID iid, void **ppv) override {
if (mux_IID_IUnknown == iid) {
*ppv = static_cast<mux_IUnknown *>(static_cast<mux_IJITCompile *>(this));
} else if (IID_IJITCompile == iid) {
*ppv = static_cast<mux_IJITCompile *>(this);
} else {
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
AddRef();
return MUX_S_OK;
}
uint32_t AddRef(void) override { return ++m_cRef; }
uint32_t Release(void) override {
uint32_t n = --m_cRef;
if (0 == n) delete this;
return n;
}
// mux_IJITCompile
MUX_RESULT CompileLuaBytecode(const uint8_t *pData, size_t nData,
uint64_t *pKey) override
{
if (nullptr == pData || nullptr == pKey) return MUX_E_INVALIDARG;
// Persistent cache key: "lua:" + sha1(bytecodes).
std::string cache_key = "lua:" + jit_sha1_hex(pData, nData);
// Check SQLite cache first.
compiled_program prog;
bool from_cache = jit_load_from_sqlite(cache_key, prog);
if (!from_cache) {
// Cache miss — compile from bytecodes.
if (!compile_lua_bytecode(pData, nData, &prog)) {
s_lua_jit_stats.compile_fail++;
*pKey = 0;
return MUX_E_FAIL;
}
// Persist to SQLite while prog.memory still exists.
jit_store_to_sqlite(cache_key, prog);
// Compact: extract blobs, release 4MB memory.
jit_compact_program(prog);
}
// Drain before inserting, not after: a flush deferred from earlier in
// this command would otherwise wipe the program we just compiled and
// hand back a key that no longer resolves.
//
jit_flush_pending_caches();
uint64_t key = s_next_key.fetch_add(1, std::memory_order_relaxed);
s_lua_cache[key] = std::move(prog);
*pKey = key;
s_lua_jit_stats.compile_ok++;
return MUX_S_OK;
}
MUX_RESULT RunCompiled(uint64_t key,
dbref executor, dbref caller, dbref enactor,
const UTF8 *pArgs[], int nArgs,
UTF8 *pResult, size_t nResultMax, size_t *pnResultLen,
void *pLuaState) override
{
// Apply a deferred flush before taking a pointer into the cache:
// &it->second below is held by run_cached_program for the whole
// execution, and a flush arriving mid-run would free it (#1316).
// A drop here is a miss, and the caller falls back to the Lua VM.
//
jit_flush_pending_caches();
auto it = s_lua_cache.find(key);
if (it == s_lua_cache.end()) return MUX_E_NOTFOUND;
s_lua_jit_stats.cache_hits++;
// On a wall-clock alarm mid-run, run_cached_program writes
// "#-1 CPU LIMITED" into pResult and returns true (handled) rather
// than false. That is deliberate: a false here returns MUX_E_FAIL,
// and the Lua caller responds by re-running the whole chunk in the
// Lua VM -- expensive work we must NOT do once the command is already
// over its CPU budget. So an alarm surfaces as a successful
// CPU-LIMITED result, not a failover.
//
bool ok = run_cached_program(&it->second, executor, caller, enactor,
pResult, nResultMax, pArgs, nArgs,
EV_FCHECK | EV_EVAL, pLuaState);
if (!ok) {
s_lua_jit_stats.run_fail++;
return MUX_E_FAIL;
}
if (pnResultLen) {
*pnResultLen = strlen(reinterpret_cast<const char *>(pResult));
}
s_lua_jit_stats.run_ok++;
return MUX_S_OK;
}
MUX_RESULT IsCompiled(uint64_t key, bool *pCompiled) override {
if (nullptr == pCompiled) return MUX_E_INVALIDARG;
*pCompiled = (s_lua_cache.find(key) != s_lua_cache.end());
return MUX_S_OK;
}
MUX_RESULT Invalidate(uint64_t key) override {
auto it = s_lua_cache.find(key);
if (it != s_lua_cache.end()) {
s_lua_cache.erase(it);
s_lua_jit_stats.invalidations++;
}
return MUX_S_OK;
}
private:
uint32_t m_cRef;
};
// ---------------------------------------------------------------
// Factory creation function — called from engine_com.cpp.
// ---------------------------------------------------------------
MUX_RESULT jit_compile_create_instance(MUX_IID iid, void **ppv) {
CJITCompile *pObj = nullptr;
try { pObj = new CJITCompile; } catch (...) { ; }
if (nullptr == pObj) return MUX_E_OUTOFMEMORY;
MUX_RESULT mr = pObj->QueryInterface(iid, ppv);
pObj->Release();
return mr;
}