2026-03-18 09:59:36 -06:00
|
|
|
/*! \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"
|
|
|
|
|
|
2026-06-05 08:40:57 -05:00
|
|
|
#include <atomic>
|
2026-03-18 09:59:36 -06:00
|
|
|
#include <cstring>
|
|
|
|
|
#include <cstdio>
|
feat(lua/jit): make the Lua compile path visible under TINYMUX_DUMP_HIR
TINYMUX_DUMP_HIR covered only the softcode JIT. On the Lua path nothing was
observable from outside: not the HIR, not the block layout, and not the more
basic question of whether a chunk compiled at all.
That last one is the expensive part. A declined chunk runs on the interpreter,
so it agrees with the interpreter trivially -- which reads as a pass when the
two are compared. Several numeric-for results I first read as "the compiled
path is correct" had simply never compiled. So this reports declines too, and
names the reason:
--- Lua JIT: @#1/T declined by eligibility: LUA_BC_HAS_CLOSURE ---
--- Lua JIT: @#1/T declined by lowering (10 bytecode insns) ---
and otherwise dumps each phase, matching what jit_compiler.cpp already does for
softcode:
--- Lua JIT Compilation: @#1/T (10 bytecode insns) ---
Phase 1: HIR Lowering
Phase 2: CFG (5 blocks)
Phase 2b: SSA Construction
Phase 3: SSA Optimization
Gated on the same environment variable, so it is silent unless asked for;
verified that an unset variable produces no output and smoke is unchanged
(1505/1505, 316/316 dispatched).
This immediately paid for itself on #1486, where the dump shows the branch and
the returned value disagreeing:
v4 = GT int v2, v3
v5 = BRC void v4 ? -> BLOCK 3 : BLOCK 2 BLOCK 2 = then, BLOCK 3 = else
Result: v7 v7 = SCONST "111", in BLOCK 2
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:21:57 -06:00
|
|
|
#include <cstdlib>
|
2026-03-18 09:59:36 -06:00
|
|
|
#include <unordered_map>
|
|
|
|
|
#include <vector>
|
|
|
|
|
#include <string>
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
// Compile cache
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
static std::unordered_map<uint64_t, compiled_program> s_lua_cache;
|
2026-06-05 08:40:57 -05:00
|
|
|
static std::atomic<uint64_t> s_next_key{1};
|
2026-03-18 09:59:36 -06:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
// 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;
|
2026-07-29 01:34:45 -06:00
|
|
|
// #1751 Phase 0: post-entry ECALL_DECLINE committed as loud error
|
|
|
|
|
// (no interpreter re-run). Correct long-term value is zero.
|
|
|
|
|
//
|
|
|
|
|
uint64_t post_entry_decline;
|
2026-03-18 09:59:36 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static lua_jit_stats s_lua_jit_stats = {};
|
|
|
|
|
|
2026-07-25 22:55:25 -06:00
|
|
|
// 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;
|
2026-07-29 01:34:45 -06:00
|
|
|
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;
|
|
|
|
|
out->post_entry_decline = s_lua_jit_stats.post_entry_decline;
|
2026-07-25 22:55:25 -06:00
|
|
|
}
|
|
|
|
|
|
2026-07-29 01:40:19 -06:00
|
|
|
void jit_lua_note_post_entry_decline(void) {
|
|
|
|
|
s_lua_jit_stats.post_entry_decline++;
|
|
|
|
|
s_lua_jit_stats.run_fail++;
|
|
|
|
|
// A successful RunCompiled may have already counted run_ok; reverse it
|
|
|
|
|
// when the outer path commits a post-entry fail instead.
|
|
|
|
|
//
|
|
|
|
|
if (s_lua_jit_stats.run_ok > 0) {
|
|
|
|
|
s_lua_jit_stats.run_ok--;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 22:55:25 -06:00
|
|
|
void jit_lua_reset_stats(void) {
|
|
|
|
|
s_lua_jit_stats = {};
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-26 15:16:07 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 09:59:36 -06:00
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
// Compile a Lua bytecode blob to a compiled_program.
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
static bool compile_lua_bytecode(const uint8_t *data, size_t len,
|
|
|
|
|
compiled_program *out) {
|
feat(lua/jit): make the Lua compile path visible under TINYMUX_DUMP_HIR
TINYMUX_DUMP_HIR covered only the softcode JIT. On the Lua path nothing was
observable from outside: not the HIR, not the block layout, and not the more
basic question of whether a chunk compiled at all.
That last one is the expensive part. A declined chunk runs on the interpreter,
so it agrees with the interpreter trivially -- which reads as a pass when the
two are compared. Several numeric-for results I first read as "the compiled
path is correct" had simply never compiled. So this reports declines too, and
names the reason:
--- Lua JIT: @#1/T declined by eligibility: LUA_BC_HAS_CLOSURE ---
--- Lua JIT: @#1/T declined by lowering (10 bytecode insns) ---
and otherwise dumps each phase, matching what jit_compiler.cpp already does for
softcode:
--- Lua JIT Compilation: @#1/T (10 bytecode insns) ---
Phase 1: HIR Lowering
Phase 2: CFG (5 blocks)
Phase 2b: SSA Construction
Phase 3: SSA Optimization
Gated on the same environment variable, so it is silent unless asked for;
verified that an unset variable produces no output and smoke is unchanged
(1505/1505, 316/316 dispatched).
This immediately paid for itself on #1486, where the dump shows the branch and
the returned value disagreeing:
v4 = GT int v2, v3
v5 = BRC void v4 ? -> BLOCK 3 : BLOCK 2 BLOCK 2 = then, BLOCK 3 = else
Result: v7 v7 = SCONST "111", in BLOCK 2
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:21:57 -06:00
|
|
|
// 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');
|
|
|
|
|
|
2026-07-27 13:51:32 +00:00
|
|
|
// 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();
|
|
|
|
|
|
2026-03-18 09:59:36 -06:00
|
|
|
// Deserialize.
|
|
|
|
|
lua_bc_chunk chunk;
|
|
|
|
|
if (!lua_bc_load(data, len, &chunk)) {
|
feat(lua/jit): make the Lua compile path visible under TINYMUX_DUMP_HIR
TINYMUX_DUMP_HIR covered only the softcode JIT. On the Lua path nothing was
observable from outside: not the HIR, not the block layout, and not the more
basic question of whether a chunk compiled at all.
That last one is the expensive part. A declined chunk runs on the interpreter,
so it agrees with the interpreter trivially -- which reads as a pass when the
two are compared. Several numeric-for results I first read as "the compiled
path is correct" had simply never compiled. So this reports declines too, and
names the reason:
--- Lua JIT: @#1/T declined by eligibility: LUA_BC_HAS_CLOSURE ---
--- Lua JIT: @#1/T declined by lowering (10 bytecode insns) ---
and otherwise dumps each phase, matching what jit_compiler.cpp already does for
softcode:
--- Lua JIT Compilation: @#1/T (10 bytecode insns) ---
Phase 1: HIR Lowering
Phase 2: CFG (5 blocks)
Phase 2b: SSA Construction
Phase 3: SSA Optimization
Gated on the same environment variable, so it is silent unless asked for;
verified that an unset variable produces no output and smoke is unchanged
(1505/1505, 316/316 dispatched).
This immediately paid for itself on #1486, where the dump shows the branch and
the returned value disagreeing:
v4 = GT int v2, v3
v5 = BRC void v4 ? -> BLOCK 3 : BLOCK 2 BLOCK 2 = then, BLOCK 3 = else
Result: v7 v7 = SCONST "111", in BLOCK 2
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:21:57 -06:00
|
|
|
if (bDump) {
|
|
|
|
|
printf("\n--- Lua JIT: declined, bytecode failed to load ---\n");
|
|
|
|
|
}
|
2026-03-18 09:59:36 -06:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 12:15:42 -06:00
|
|
|
// Fast eligibility check — reject before allocating HIR/RV64 state.
|
|
|
|
|
lua_bc_reject reason = lua_bc_eligible(&chunk.main);
|
|
|
|
|
if (reason != LUA_BC_ELIGIBLE) {
|
feat(lua/jit): make the Lua compile path visible under TINYMUX_DUMP_HIR
TINYMUX_DUMP_HIR covered only the softcode JIT. On the Lua path nothing was
observable from outside: not the HIR, not the block layout, and not the more
basic question of whether a chunk compiled at all.
That last one is the expensive part. A declined chunk runs on the interpreter,
so it agrees with the interpreter trivially -- which reads as a pass when the
two are compared. Several numeric-for results I first read as "the compiled
path is correct" had simply never compiled. So this reports declines too, and
names the reason:
--- Lua JIT: @#1/T declined by eligibility: LUA_BC_HAS_CLOSURE ---
--- Lua JIT: @#1/T declined by lowering (10 bytecode insns) ---
and otherwise dumps each phase, matching what jit_compiler.cpp already does for
softcode:
--- Lua JIT Compilation: @#1/T (10 bytecode insns) ---
Phase 1: HIR Lowering
Phase 2: CFG (5 blocks)
Phase 2b: SSA Construction
Phase 3: SSA Optimization
Gated on the same environment variable, so it is silent unless asked for;
verified that an unset variable produces no output and smoke is unchanged
(1505/1505, 316/316 dispatched).
This immediately paid for itself on #1486, where the dump shows the branch and
the returned value disagreeing:
v4 = GT int v2, v3
v5 = BRC void v4 ? -> BLOCK 3 : BLOCK 2 BLOCK 2 = then, BLOCK 3 = else
Result: v7 v7 = SCONST "111", in BLOCK 2
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:21:57 -06:00
|
|
|
if (bDump) {
|
|
|
|
|
printf("\n--- Lua JIT: %s declined by eligibility: %s ---\n",
|
|
|
|
|
chunk.main.source.c_str(), lua_bc_reject_name(reason));
|
|
|
|
|
}
|
2026-03-18 12:15:42 -06:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 09:59:36 -06:00
|
|
|
// 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) {
|
feat(lua/jit): make the Lua compile path visible under TINYMUX_DUMP_HIR
TINYMUX_DUMP_HIR covered only the softcode JIT. On the Lua path nothing was
observable from outside: not the HIR, not the block layout, and not the more
basic question of whether a chunk compiled at all.
That last one is the expensive part. A declined chunk runs on the interpreter,
so it agrees with the interpreter trivially -- which reads as a pass when the
two are compared. Several numeric-for results I first read as "the compiled
path is correct" had simply never compiled. So this reports declines too, and
names the reason:
--- Lua JIT: @#1/T declined by eligibility: LUA_BC_HAS_CLOSURE ---
--- Lua JIT: @#1/T declined by lowering (10 bytecode insns) ---
and otherwise dumps each phase, matching what jit_compiler.cpp already does for
softcode:
--- Lua JIT Compilation: @#1/T (10 bytecode insns) ---
Phase 1: HIR Lowering
Phase 2: CFG (5 blocks)
Phase 2b: SSA Construction
Phase 3: SSA Optimization
Gated on the same environment variable, so it is silent unless asked for;
verified that an unset variable produces no output and smoke is unchanged
(1505/1505, 316/316 dispatched).
This immediately paid for itself on #1486, where the dump shows the branch and
the returned value disagreeing:
v4 = GT int v2, v3
v5 = BRC void v4 ? -> BLOCK 3 : BLOCK 2 BLOCK 2 = then, BLOCK 3 = else
Result: v7 v7 = SCONST "111", in BLOCK 2
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:21:57 -06:00
|
|
|
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()));
|
|
|
|
|
}
|
2026-03-18 09:59:36 -06:00
|
|
|
delete h;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
fix(jit): make the HIR -1 sentinel safe in the subscript, not per site (#1501)
Three rounds of per-site guards have landed for the same defect class --
#1440, #1449, #1457/#1470 -- and each round found producers the previous
one had missed. The guards are invisible at the point of danger: nothing
about `h.ty[p]` says p might be -1, so a fast path added ahead of an
existing guard is unprotected by construction. #1470 found exactly that,
and near-identical loops 80 lines apart had also drifted (#1457).
Move the check into the subscript. The 16 per-instruction arrays become
hir_slot<T, N>, whose operator[] refuses an out-of-range index: the access
lands on a scratch element instead of past the array, and is recorded so
refused_index() makes the whole program unusable. 828 access sites across
five files are unchanged -- there is no memset or array decay anywhere in
the family, so the wrapper is a true drop-in.
Both halves are load-bearing, and measured separately. Making the
subscript merely *defined* without recording it would convert a sanitizer
report into a silently wrong compile, which is worse than the bug; that is
case C below. Recording without a scratch element would leave the out-of-
bounds access in place.
refused_index() is read wherever h.overflowed already is. The two mean
the same thing to a caller -- this program is not safe to use -- and
differ only in what noticed: overflowed is set by the producer of a -1, so
it catches capacity limits and the #1242 unknown-node refusal, while
refused_index() is set by the consumer, so it also catches a refusal no
producer flagged. That is the half the per-site guards kept missing.
Also closes a gap on the Lua entry point: compile_lua_bytecode never
checked h->overflowed at all. It checked only the -1 returned by
hir_lower_lua_proto, so a refusal a consumer swallowed left the flag set
and unread while SSA, the optimizer and codegen ran over a program whose
lowering had stopped partway. compile_expression has had that check since
#859; the Lua path is the same pipeline.
Negative controls, all with an unguarded consumer injected at the #1470
site (h.known_int[-1] = true; h.ty[-1] = TY_INT; then read it back):
A wrapper checks, refusal wired 0 UBSan reports compile_fail 0->1
B subscript passes through 2 UBSan reports (see note)
C wrapper checks, refusal unwired 0 UBSan reports compile_fail 0->0
B reports exactly the class #1501 names -- "index -1 out of bounds for
type 'bool [4096]'" and "'hir_type [4096]'". Its compile_fail also moved,
but by accident: the out-of-bounds write lands on the preceding slot's
`refused` byte and sets it, which is its own argument for the change. C
is the one that matters -- the compile completed and was used
(compile_ok 1->3) over a program with a refused index, with no sanitizer
signal to show it. In all three the answer stayed correct, because the
AST evaluator is the fallback.
Verification:
- make test passes, both smoke routes 1509/1509, 316/316 dispatched
- sanitizer tree (--enable-sanitizers, aarch64): test-jit-qreg,
test-jit-ifelse, test-lua-ecall and the smoke leg (1509/1509,
315/315) all clean -- zero sanitizer findings, zero "index -1 out of
bounds", which is the acceptance test #1501 asks for
- make test-asan itself cannot reach those legs on any tree; it stops at
leg 2 on a pre-existing link failure in tests/netaddr, filed as #1522
and reproduced on unmodified master
- no measurable compile-path cost: smoke 16s vs 17-18s baseline, i.e.
within run-to-run noise
hir_slot's element parameter is ElemT rather than T because mux_nls.h
defines a function-style macro T(x); a parameter named T turns `T()` into
`(reinterpret_cast<const UTF8 *>())`. Two static_asserts pin HIR_NOP and
TY_VOID as the zero values, since a refused read returning a live-looking
kind or type could steer a consumer down a real branch before
refused_index() is consulted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:41:23 -06:00
|
|
|
// 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()) {
|
2026-07-27 11:57:12 +00:00
|
|
|
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()));
|
|
|
|
|
}
|
fix(jit): make the HIR -1 sentinel safe in the subscript, not per site (#1501)
Three rounds of per-site guards have landed for the same defect class --
#1440, #1449, #1457/#1470 -- and each round found producers the previous
one had missed. The guards are invisible at the point of danger: nothing
about `h.ty[p]` says p might be -1, so a fast path added ahead of an
existing guard is unprotected by construction. #1470 found exactly that,
and near-identical loops 80 lines apart had also drifted (#1457).
Move the check into the subscript. The 16 per-instruction arrays become
hir_slot<T, N>, whose operator[] refuses an out-of-range index: the access
lands on a scratch element instead of past the array, and is recorded so
refused_index() makes the whole program unusable. 828 access sites across
five files are unchanged -- there is no memset or array decay anywhere in
the family, so the wrapper is a true drop-in.
Both halves are load-bearing, and measured separately. Making the
subscript merely *defined* without recording it would convert a sanitizer
report into a silently wrong compile, which is worse than the bug; that is
case C below. Recording without a scratch element would leave the out-of-
bounds access in place.
refused_index() is read wherever h.overflowed already is. The two mean
the same thing to a caller -- this program is not safe to use -- and
differ only in what noticed: overflowed is set by the producer of a -1, so
it catches capacity limits and the #1242 unknown-node refusal, while
refused_index() is set by the consumer, so it also catches a refusal no
producer flagged. That is the half the per-site guards kept missing.
Also closes a gap on the Lua entry point: compile_lua_bytecode never
checked h->overflowed at all. It checked only the -1 returned by
hir_lower_lua_proto, so a refusal a consumer swallowed left the flag set
and unread while SSA, the optimizer and codegen ran over a program whose
lowering had stopped partway. compile_expression has had that check since
#859; the Lua path is the same pipeline.
Negative controls, all with an unguarded consumer injected at the #1470
site (h.known_int[-1] = true; h.ty[-1] = TY_INT; then read it back):
A wrapper checks, refusal wired 0 UBSan reports compile_fail 0->1
B subscript passes through 2 UBSan reports (see note)
C wrapper checks, refusal unwired 0 UBSan reports compile_fail 0->0
B reports exactly the class #1501 names -- "index -1 out of bounds for
type 'bool [4096]'" and "'hir_type [4096]'". Its compile_fail also moved,
but by accident: the out-of-bounds write lands on the preceding slot's
`refused` byte and sets it, which is its own argument for the change. C
is the one that matters -- the compile completed and was used
(compile_ok 1->3) over a program with a refused index, with no sanitizer
signal to show it. In all three the answer stayed correct, because the
AST evaluator is the fallback.
Verification:
- make test passes, both smoke routes 1509/1509, 316/316 dispatched
- sanitizer tree (--enable-sanitizers, aarch64): test-jit-qreg,
test-jit-ifelse, test-lua-ecall and the smoke leg (1509/1509,
315/315) all clean -- zero sanitizer findings, zero "index -1 out of
bounds", which is the acceptance test #1501 asks for
- make test-asan itself cannot reach those legs on any tree; it stops at
leg 2 on a pre-existing link failure in tests/netaddr, filed as #1522
and reproduced on unmodified master
- no measurable compile-path cost: smoke 16s vs 17-18s baseline, i.e.
within run-to-run noise
hir_slot's element parameter is ElemT rather than T because mux_nls.h
defines a function-style macro T(x); a parameter named T turns `T()` into
`(reinterpret_cast<const UTF8 *>())`. Two static_asserts pin HIR_NOP and
TY_VOID as the zero values, since a refused read returning a live-looking
kind or type could steer a consumer down a real branch before
refused_index() is consulted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:41:23 -06:00
|
|
|
delete h;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
feat(lua/jit): make the Lua compile path visible under TINYMUX_DUMP_HIR
TINYMUX_DUMP_HIR covered only the softcode JIT. On the Lua path nothing was
observable from outside: not the HIR, not the block layout, and not the more
basic question of whether a chunk compiled at all.
That last one is the expensive part. A declined chunk runs on the interpreter,
so it agrees with the interpreter trivially -- which reads as a pass when the
two are compared. Several numeric-for results I first read as "the compiled
path is correct" had simply never compiled. So this reports declines too, and
names the reason:
--- Lua JIT: @#1/T declined by eligibility: LUA_BC_HAS_CLOSURE ---
--- Lua JIT: @#1/T declined by lowering (10 bytecode insns) ---
and otherwise dumps each phase, matching what jit_compiler.cpp already does for
softcode:
--- Lua JIT Compilation: @#1/T (10 bytecode insns) ---
Phase 1: HIR Lowering
Phase 2: CFG (5 blocks)
Phase 2b: SSA Construction
Phase 3: SSA Optimization
Gated on the same environment variable, so it is silent unless asked for;
verified that an unset variable produces no output and smoke is unchanged
(1505/1505, 316/316 dispatched).
This immediately paid for itself on #1486, where the dump shows the branch and
the returned value disagreeing:
v4 = GT int v2, v3
v5 = BRC void v4 ? -> BLOCK 3 : BLOCK 2 BLOCK 2 = then, BLOCK 3 = else
Result: v7 v7 = SCONST "111", in BLOCK 2
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:21:57 -06:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 23:08:40 -06:00
|
|
|
// 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);
|
feat(lua/jit): make the Lua compile path visible under TINYMUX_DUMP_HIR
TINYMUX_DUMP_HIR covered only the softcode JIT. On the Lua path nothing was
observable from outside: not the HIR, not the block layout, and not the more
basic question of whether a chunk compiled at all.
That last one is the expensive part. A declined chunk runs on the interpreter,
so it agrees with the interpreter trivially -- which reads as a pass when the
two are compared. Several numeric-for results I first read as "the compiled
path is correct" had simply never compiled. So this reports declines too, and
names the reason:
--- Lua JIT: @#1/T declined by eligibility: LUA_BC_HAS_CLOSURE ---
--- Lua JIT: @#1/T declined by lowering (10 bytecode insns) ---
and otherwise dumps each phase, matching what jit_compiler.cpp already does for
softcode:
--- Lua JIT Compilation: @#1/T (10 bytecode insns) ---
Phase 1: HIR Lowering
Phase 2: CFG (5 blocks)
Phase 2b: SSA Construction
Phase 3: SSA Optimization
Gated on the same environment variable, so it is silent unless asked for;
verified that an unset variable produces no output and smoke is unchanged
(1505/1505, 316/316 dispatched).
This immediately paid for itself on #1486, where the dump shows the branch and
the returned value disagreeing:
v4 = GT int v2, v3
v5 = BRC void v4 ? -> BLOCK 3 : BLOCK 2 BLOCK 2 = then, BLOCK 3 = else
Result: v7 v7 = SCONST "111", in BLOCK 2
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:21:57 -06:00
|
|
|
if (bDump) {
|
|
|
|
|
printf("Phase 2: CFG (%d blocks)\n", h->n_blocks);
|
|
|
|
|
hir_dump(*h);
|
|
|
|
|
}
|
2026-03-18 09:59:36 -06:00
|
|
|
if (h->n_blocks > 1) {
|
|
|
|
|
hir_ssa_construct(*h);
|
feat(lua/jit): make the Lua compile path visible under TINYMUX_DUMP_HIR
TINYMUX_DUMP_HIR covered only the softcode JIT. On the Lua path nothing was
observable from outside: not the HIR, not the block layout, and not the more
basic question of whether a chunk compiled at all.
That last one is the expensive part. A declined chunk runs on the interpreter,
so it agrees with the interpreter trivially -- which reads as a pass when the
two are compared. Several numeric-for results I first read as "the compiled
path is correct" had simply never compiled. So this reports declines too, and
names the reason:
--- Lua JIT: @#1/T declined by eligibility: LUA_BC_HAS_CLOSURE ---
--- Lua JIT: @#1/T declined by lowering (10 bytecode insns) ---
and otherwise dumps each phase, matching what jit_compiler.cpp already does for
softcode:
--- Lua JIT Compilation: @#1/T (10 bytecode insns) ---
Phase 1: HIR Lowering
Phase 2: CFG (5 blocks)
Phase 2b: SSA Construction
Phase 3: SSA Optimization
Gated on the same environment variable, so it is silent unless asked for;
verified that an unset variable produces no output and smoke is unchanged
(1505/1505, 316/316 dispatched).
This immediately paid for itself on #1486, where the dump shows the branch and
the returned value disagreeing:
v4 = GT int v2, v3
v5 = BRC void v4 ? -> BLOCK 3 : BLOCK 2 BLOCK 2 = then, BLOCK 3 = else
Result: v7 v7 = SCONST "111", in BLOCK 2
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:21:57 -06:00
|
|
|
if (bDump) {
|
|
|
|
|
printf("Phase 2b: SSA Construction\n");
|
|
|
|
|
hir_dump(*h);
|
|
|
|
|
}
|
2026-03-18 09:59:36 -06:00
|
|
|
hir_optimize(*h);
|
|
|
|
|
} else {
|
|
|
|
|
// Single block: just constant folding.
|
|
|
|
|
hir_const_fold(*h);
|
|
|
|
|
}
|
feat(lua/jit): make the Lua compile path visible under TINYMUX_DUMP_HIR
TINYMUX_DUMP_HIR covered only the softcode JIT. On the Lua path nothing was
observable from outside: not the HIR, not the block layout, and not the more
basic question of whether a chunk compiled at all.
That last one is the expensive part. A declined chunk runs on the interpreter,
so it agrees with the interpreter trivially -- which reads as a pass when the
two are compared. Several numeric-for results I first read as "the compiled
path is correct" had simply never compiled. So this reports declines too, and
names the reason:
--- Lua JIT: @#1/T declined by eligibility: LUA_BC_HAS_CLOSURE ---
--- Lua JIT: @#1/T declined by lowering (10 bytecode insns) ---
and otherwise dumps each phase, matching what jit_compiler.cpp already does for
softcode:
--- Lua JIT Compilation: @#1/T (10 bytecode insns) ---
Phase 1: HIR Lowering
Phase 2: CFG (5 blocks)
Phase 2b: SSA Construction
Phase 3: SSA Optimization
Gated on the same environment variable, so it is silent unless asked for;
verified that an unset variable produces no output and smoke is unchanged
(1505/1505, 316/316 dispatched).
This immediately paid for itself on #1486, where the dump shows the branch and
the returned value disagreeing:
v4 = GT int v2, v3
v5 = BRC void v4 ? -> BLOCK 3 : BLOCK 2 BLOCK 2 = then, BLOCK 3 = else
Result: v7 v7 = SCONST "111", in BLOCK 2
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:21:57 -06:00
|
|
|
if (bDump) {
|
|
|
|
|
printf("Phase 3: %s\n",
|
|
|
|
|
h->n_blocks > 1 ? "SSA Optimization" : "Constant Folding");
|
|
|
|
|
hir_dump(*h);
|
|
|
|
|
}
|
2026-03-18 09:59:36 -06:00
|
|
|
|
fix(jit): make the HIR -1 sentinel safe in the subscript, not per site (#1501)
Three rounds of per-site guards have landed for the same defect class --
#1440, #1449, #1457/#1470 -- and each round found producers the previous
one had missed. The guards are invisible at the point of danger: nothing
about `h.ty[p]` says p might be -1, so a fast path added ahead of an
existing guard is unprotected by construction. #1470 found exactly that,
and near-identical loops 80 lines apart had also drifted (#1457).
Move the check into the subscript. The 16 per-instruction arrays become
hir_slot<T, N>, whose operator[] refuses an out-of-range index: the access
lands on a scratch element instead of past the array, and is recorded so
refused_index() makes the whole program unusable. 828 access sites across
five files are unchanged -- there is no memset or array decay anywhere in
the family, so the wrapper is a true drop-in.
Both halves are load-bearing, and measured separately. Making the
subscript merely *defined* without recording it would convert a sanitizer
report into a silently wrong compile, which is worse than the bug; that is
case C below. Recording without a scratch element would leave the out-of-
bounds access in place.
refused_index() is read wherever h.overflowed already is. The two mean
the same thing to a caller -- this program is not safe to use -- and
differ only in what noticed: overflowed is set by the producer of a -1, so
it catches capacity limits and the #1242 unknown-node refusal, while
refused_index() is set by the consumer, so it also catches a refusal no
producer flagged. That is the half the per-site guards kept missing.
Also closes a gap on the Lua entry point: compile_lua_bytecode never
checked h->overflowed at all. It checked only the -1 returned by
hir_lower_lua_proto, so a refusal a consumer swallowed left the flag set
and unread while SSA, the optimizer and codegen ran over a program whose
lowering had stopped partway. compile_expression has had that check since
#859; the Lua path is the same pipeline.
Negative controls, all with an unguarded consumer injected at the #1470
site (h.known_int[-1] = true; h.ty[-1] = TY_INT; then read it back):
A wrapper checks, refusal wired 0 UBSan reports compile_fail 0->1
B subscript passes through 2 UBSan reports (see note)
C wrapper checks, refusal unwired 0 UBSan reports compile_fail 0->0
B reports exactly the class #1501 names -- "index -1 out of bounds for
type 'bool [4096]'" and "'hir_type [4096]'". Its compile_fail also moved,
but by accident: the out-of-bounds write lands on the preceding slot's
`refused` byte and sets it, which is its own argument for the change. C
is the one that matters -- the compile completed and was used
(compile_ok 1->3) over a program with a refused index, with no sanitizer
signal to show it. In all three the answer stayed correct, because the
AST evaluator is the fallback.
Verification:
- make test passes, both smoke routes 1509/1509, 316/316 dispatched
- sanitizer tree (--enable-sanitizers, aarch64): test-jit-qreg,
test-jit-ifelse, test-lua-ecall and the smoke leg (1509/1509,
315/315) all clean -- zero sanitizer findings, zero "index -1 out of
bounds", which is the acceptance test #1501 asks for
- make test-asan itself cannot reach those legs on any tree; it stops at
leg 2 on a pre-existing link failure in tests/netaddr, filed as #1522
and reproduced on unmodified master
- no measurable compile-path cost: smoke 16s vs 17-18s baseline, i.e.
within run-to-run noise
hir_slot's element parameter is ElemT rather than T because mux_nls.h
defines a function-style macro T(x); a parameter named T turns `T()` into
`(reinterpret_cast<const UTF8 *>())`. Two static_asserts pin HIR_NOP and
TY_VOID as the zero values, since a refused read returning a live-looking
kind or type could steer a consumer down a real branch before
refused_index() is consulted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:41:23 -06:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 09:59:36 -06:00
|
|
|
// Code generation: HIR → RV64.
|
|
|
|
|
hir_codegen(*h, rc_state);
|
|
|
|
|
|
2026-07-25 23:17:08 -06:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 09:59:36 -06:00
|
|
|
// Build compiled_program output.
|
2026-07-25 23:08:40 -06:00
|
|
|
// Include rc_state.needs_jit: codegen may force runtime for results that
|
|
|
|
|
// live only in registers (ITOA path), even when lowering saw no ecalls.
|
|
|
|
|
//
|
2026-03-18 09:59:36 -06:00
|
|
|
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;
|
2026-04-04 11:50:43 -06:00
|
|
|
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;
|
2026-03-18 09:59:36 -06:00
|
|
|
out->ok = true;
|
|
|
|
|
out->folds = h->folds;
|
|
|
|
|
out->ecalls = h->ecalls;
|
|
|
|
|
out->tier2_calls = 0;
|
|
|
|
|
out->native_ops = h->native_ops;
|
2026-07-25 23:08:40 -06:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-18 09:59:36 -06:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-03-21 18:21:14 -06:00
|
|
|
// Persistent cache key: "lua:" + sha1(bytecodes).
|
|
|
|
|
std::string cache_key = "lua:" + jit_sha1_hex(pData, nData);
|
|
|
|
|
|
|
|
|
|
// Check SQLite cache first.
|
2026-03-18 09:59:36 -06:00
|
|
|
compiled_program prog;
|
2026-03-21 18:21:14 -06:00
|
|
|
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;
|
|
|
|
|
}
|
2026-04-04 11:50:43 -06:00
|
|
|
// Persist to SQLite while prog.memory still exists.
|
2026-03-21 18:21:14 -06:00
|
|
|
jit_store_to_sqlite(cache_key, prog);
|
2026-04-04 11:50:43 -06:00
|
|
|
// Compact: extract blobs, release 4MB memory.
|
|
|
|
|
jit_compact_program(prog);
|
2026-03-18 09:59:36 -06:00
|
|
|
}
|
|
|
|
|
|
fix(jit): defer the in-memory flush until no program is running (#1316) (#1409)
jitstats(flush) returned an empty string instead of OK whenever the region
containing the call was JIT-compiled -- which, with jit_eval_brackets on by
default, is the documented usage from wizhelp.
s_compile_cache holds compiled_program by value and compile_cached() hands
back a pointer into it. run_cached_program() keeps that pointer across the
whole execution and harvests through it afterwards:
int rc = dbt_run(dbt, prog->entry_pc, ...);
...
uint64_t out_addr = resolve_runtime_out_addr(prog->out_addr, ...);
and jit_eval() reads prog->ecalls / prog->tier2_calls after that returns.
dbt_run is what issues ECALLs, and ECALL callees run the interpreter -- that
is how u() and ufuns work from compiled code -- so fun_jitstats could execute
with a live program on the stack. s_compile_cache.clear() then freed the
program mid-flight and the harvest read released memory. It surfaced as a
lost result rather than a crash, which is allocator luck, not safety.
s_run_cached_depth did not help: it refuses nested run_cached_program, but an
ECALL into the interpreter is a plain call, so the guard never fired.
The SQLite DELETE and the write-queue drop stay immediate -- neither frees
anything a running program points at. Only the in-memory caches wait, drained
at the top of compile_cached() and of the Lua RunCompiled/Compile entries:
those are the points where the caller does not yet hold a pointer, and depth
== 0 means no outer frame holds one either. Nothing can be served stale in
between, because every lookup goes through one of those drains.
jit_lua's s_lua_cache had the same shape -- RunCompiled passes &it->second to
run_cached_program -- so it drains on the same flag.
Verified on macOS arm64, both routes, same tree:
before direct= nested= warm= (empty, on the compiled route)
after direct=OK nested=OK tailmark warm=OK
and the flush still flushes: across it, cache_hit_mem is unmoved while
cache_miss rises by exactly the number of regions evaluated after, including
one whose text had just been compiled.
jitstats_fn.mux TC005 pins both halves. It fails on the unfixed engine with
the exact signature above, and asserts the counters so a flush that returned
OK without flushing would not pass. No tr.done handoff was added: the dolist
over tr.tc* already dispatches it, and chaining from tc004 ran it twice.
with fix 1487 passed, 0 failed, 314/314, both routes
without fix 1486 passed, 1 failed (TC005), 314/314
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:49:27 -06:00
|
|
|
// 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();
|
|
|
|
|
|
2026-06-05 08:40:57 -05:00
|
|
|
uint64_t key = s_next_key.fetch_add(1, std::memory_order_relaxed);
|
2026-03-18 09:59:36 -06:00
|
|
|
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,
|
2026-03-18 15:19:28 -06:00
|
|
|
UTF8 *pResult, size_t nResultMax, size_t *pnResultLen,
|
|
|
|
|
void *pLuaState) override
|
2026-03-18 09:59:36 -06:00
|
|
|
{
|
fix(jit): defer the in-memory flush until no program is running (#1316) (#1409)
jitstats(flush) returned an empty string instead of OK whenever the region
containing the call was JIT-compiled -- which, with jit_eval_brackets on by
default, is the documented usage from wizhelp.
s_compile_cache holds compiled_program by value and compile_cached() hands
back a pointer into it. run_cached_program() keeps that pointer across the
whole execution and harvests through it afterwards:
int rc = dbt_run(dbt, prog->entry_pc, ...);
...
uint64_t out_addr = resolve_runtime_out_addr(prog->out_addr, ...);
and jit_eval() reads prog->ecalls / prog->tier2_calls after that returns.
dbt_run is what issues ECALLs, and ECALL callees run the interpreter -- that
is how u() and ufuns work from compiled code -- so fun_jitstats could execute
with a live program on the stack. s_compile_cache.clear() then freed the
program mid-flight and the harvest read released memory. It surfaced as a
lost result rather than a crash, which is allocator luck, not safety.
s_run_cached_depth did not help: it refuses nested run_cached_program, but an
ECALL into the interpreter is a plain call, so the guard never fired.
The SQLite DELETE and the write-queue drop stay immediate -- neither frees
anything a running program points at. Only the in-memory caches wait, drained
at the top of compile_cached() and of the Lua RunCompiled/Compile entries:
those are the points where the caller does not yet hold a pointer, and depth
== 0 means no outer frame holds one either. Nothing can be served stale in
between, because every lookup goes through one of those drains.
jit_lua's s_lua_cache had the same shape -- RunCompiled passes &it->second to
run_cached_program -- so it drains on the same flag.
Verified on macOS arm64, both routes, same tree:
before direct= nested= warm= (empty, on the compiled route)
after direct=OK nested=OK tailmark warm=OK
and the flush still flushes: across it, cache_hit_mem is unmoved while
cache_miss rises by exactly the number of regions evaluated after, including
one whose text had just been compiled.
jitstats_fn.mux TC005 pins both halves. It fails on the unfixed engine with
the exact signature above, and asserts the counters so a flush that returned
OK without flushing would not pass. No tr.done handoff was added: the dolist
over tr.tc* already dispatches it, and chaining from tc004 ran it twice.
with fix 1487 passed, 0 failed, 314/314, both routes
without fix 1486 passed, 1 failed (TC005), 314/314
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:49:27 -06:00
|
|
|
// 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();
|
|
|
|
|
|
2026-03-18 09:59:36 -06:00
|
|
|
auto it = s_lua_cache.find(key);
|
|
|
|
|
if (it == s_lua_cache.end()) return MUX_E_NOTFOUND;
|
|
|
|
|
|
|
|
|
|
s_lua_jit_stats.cache_hits++;
|
|
|
|
|
|
2026-07-29 05:48:11 -06:00
|
|
|
// #1751 Phase 4: run_cached_program with a Lua state never returns
|
|
|
|
|
// false after entry (CPU LIMITED, LUA ERROR, residual POST-ENTRY,
|
|
|
|
|
// and generic RUN FAIL are all committed handled=true). A false
|
2026-07-31 07:12:16 -06:00
|
|
|
// here is only a pre-entry/setup failure — depth/watermarks,
|
|
|
|
|
// oversize carg (#1055), get_dbt. Nothing ran; the interpreter
|
|
|
|
|
// must answer (#1837). Do not commit #-1 LUA JIT RUN FAIL.
|
2026-07-24 00:15:09 -06:00
|
|
|
//
|
2026-03-18 09:59:36 -06:00
|
|
|
bool ok = run_cached_program(&it->second, executor, caller, enactor,
|
2026-03-18 15:19:28 -06:00
|
|
|
pResult, nResultMax, pArgs, nArgs,
|
|
|
|
|
EV_FCHECK | EV_EVAL, pLuaState);
|
2026-07-29 01:34:45 -06:00
|
|
|
|
|
|
|
|
if (ok && nullptr != pResult
|
|
|
|
|
&& 0 == strncmp(reinterpret_cast<const char *>(pResult),
|
|
|
|
|
"#-1 LUA JIT POST-ENTRY DECLINE", 30)) {
|
|
|
|
|
s_lua_jit_stats.post_entry_decline++;
|
|
|
|
|
s_lua_jit_stats.run_fail++;
|
|
|
|
|
if (pnResultLen) {
|
|
|
|
|
*pnResultLen = strlen(reinterpret_cast<const char *>(pResult));
|
|
|
|
|
}
|
|
|
|
|
return MUX_S_OK;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 09:59:36 -06:00
|
|
|
if (!ok) {
|
2026-07-31 07:12:16 -06:00
|
|
|
// Pre-entry only. Leave pResult empty; caller falls back.
|
2026-07-29 05:48:11 -06:00
|
|
|
//
|
2026-07-31 07:12:16 -06:00
|
|
|
return MUX_E_FAIL;
|
2026-03-18 09:59:36 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pnResultLen) {
|
|
|
|
|
*pnResultLen = strlen(reinterpret_cast<const char *>(pResult));
|
|
|
|
|
}
|
2026-07-29 05:48:11 -06:00
|
|
|
// Handled run (success or committed LUA ERROR / CPU LIMITED).
|
|
|
|
|
// POST-ENTRY arm above already counted run_fail.
|
|
|
|
|
//
|
2026-03-18 09:59:36 -06:00
|
|
|
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;
|
|
|
|
|
}
|