tinymux/mux/modules/engine/hir_opt.cpp
Stephen Dennis f9b80c88b2 fix(#2166): a single-input PHI is its input — emit_phi resolves it, superblock backstops
Diagnosed the mechanism the issue left open: superblock formation merges
a block with exactly one predecessor into that predecessor, rewrites
branch targets and PHI predecessor block IDs — but never converts the
now-degenerate PHI into a copy.  The result is a PHI living in a block
that lists itself as its predecessor and has no incoming edges; PHI
resolution places copies on incoming edges, of which there are none, so
the PHI's slot is never written.  Bare switch(x,default) through the
machinery failed at run time (jit_handled=0/N, interpreter silently
answered); inside iter() it ran and returned EMPTY with
jit_handled=10/10 — the wrong-value-counter-green class.

Three layers:
- emit_phi with exactly one input now returns the input value and
  builds no node — a single-input PHI IS its input, and no lowering can
  create one again.  nargs == 0 keeps building an empty node: that is
  SSA construction's reservation contract, filled during renaming (and
  renaming always produces >= 2 args, since dominance-frontier blocks
  have >= 2 preds).
- hir_superblock backstop: after merging, a PHI whose home block has no
  predecessors sets overflowed, so the compile declines visibly
  (jit_handled=0/N, AST answers) instead of silently corrupting.
  Unreachable today; it exists for the next pass that collapses an edge
  out from under a PHI.
- The #2165 straight-line special case for npairs == 0 is REMOVED: the
  one-armed switch(target,default) now flows through the block
  machinery deliberately, as the standing regression shape — switch_fn
  TC002 asserts its value parity and jit_handled=10/10 on the compiled
  route, which is exactly the pair of signals this bug split.

tests/hir/test_phi.cpp pins the emit_phi contract (1 input → value, no
node; 2 inputs → real PHI; 0 inputs → SSA reservation).  Verified live:
V1 probe gives fallback|fallback|1|d-0 d-1 d-2 with the iter forms
jit_handled=10/10.  Bare top-level switch declines the JIT with or
without this change (pre-existing, value correct via the interpreter).
Full suite 35/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 20:51:31 -06:00

1402 lines
52 KiB
C++

/*! \file hir_opt.cpp
* \brief SSA optimization passes for the HIR.
*
* Implements:
* - Constant folding: arithmetic/comparison/logic on ICONST → ICONST
* - ATOI of SCONST → ICONST (compile-time string→int)
* - Copy propagation: replace uses of COPY with source
* - Global Value Numbering (GVN): dominator-tree walk with scoped
* hash table — subsumes CSE, sees through COPYs
* - Dead code elimination: remove unused instructions
* - Algebraic simplification: x+0, x*1, x*0, etc.
*
* All passes operate on the HIR parallel arrays. No guest memory
* or rv_compiler access needed — folding produces ICONST only.
* ITOA folding (ICONST→SCONST) is deferred to codegen since it
* requires string pool allocation.
*
* For single-block programs (M1/M2), these passes clean up
* redundant type conversions and constant arithmetic. They become
* essential in M4+ when control flow introduces PHI nodes.
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
#include "hir.h"
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <unordered_map>
#include <vector>
#include "jit_tier1_stamp.h"
// Tier 1 build stamp for this unit (#2061). Folded into the persisted
// code_cache's staleness key so a codegen change here invalidates entries
// compiled by the previous build. Updates when THIS unit is recompiled,
// which is what makes it work under incremental make.
TIER1_STAMP_DEFINE(TIER1_STAMP_HIR_OPT);
// ---------------------------------------------------------------
// Value Numbering Key for CSE
// ---------------------------------------------------------------
struct ValueKey {
hir_kind kind;
hir_type ty;
int src1;
int src2;
int64_t val;
bool operator==(const ValueKey &other) const {
return kind == other.kind && ty == other.ty && src1 == other.src1
&& src2 == other.src2 && val == other.val;
}
};
struct ValueKeyHash {
size_t operator()(const ValueKey &k) const {
size_t h = std::hash<int>{}(static_cast<int>(k.kind));
h ^= std::hash<int>{}(static_cast<int>(k.ty)) + 0x9e3779b9 + (h << 6) + (h >> 2);
h ^= std::hash<int>{}(k.src1) + 0x9e3779b9 + (h << 6) + (h >> 2);
h ^= std::hash<int>{}(k.src2) + 0x9e3779b9 + (h << 6) + (h >> 2);
h ^= std::hash<int64_t>{}(k.val) + 0x9e3779b9 + (h << 6) + (h >> 2);
return h;
}
};
// ---------------------------------------------------------------
// Helper: parse string as int64 (mirrors mux_atol for consistency)
// ---------------------------------------------------------------
static bool parse_int(const std::string &s, int64_t &out) {
if (s.empty()) { out = 0; return true; }
const char *p = s.c_str();
bool neg = false;
if (*p == '-') { neg = true; p++; }
if (*p == '\0') { out = 0; return true; }
int64_t v = 0;
while (*p >= '0' && *p <= '9') {
v = v * 10 + (*p - '0');
p++;
}
if (*p != '\0') { out = 0; return true; } // non-numeric → 0 (MUX convention)
out = neg ? -v : v;
return true;
}
// ---------------------------------------------------------------
// Constant folding
//
// Single forward pass. For each instruction, if all operands are
// compile-time constants, replace with the computed result.
// ---------------------------------------------------------------
void hir_const_fold(hir_program &h) {
bool changed = true;
while (changed) {
changed = false;
for (int i = 0; i < h.n_insns; i++) {
int s1 = h.src1[i];
int s2 = h.src2[i];
switch (h.kind[i]) {
// ATOI of SCONST → ICONST.
// Skip runtime_ref slots (CARGS/SUBST / mux.args): empty sval
// would fold to 0 and poison arithmetic (#1309).
//
case HIR_ATOI:
if ( s1 >= 0
&& h.kind[s1] == HIR_SCONST
&& !h.runtime_ref[s1]) {
int64_t v;
parse_int(h.sval[s1], v);
h.kind[i] = HIR_ICONST;
h.ty[i] = TY_INT;
h.val[i] = v;
h.src1[i] = -1;
changed = true;
}
// ATOI of ICONST → identity (already int).
else if (s1 >= 0 && h.kind[s1] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.ty[i] = TY_INT;
h.val[i] = h.val[s1];
h.src1[i] = -1;
changed = true;
}
break;
// Binary arithmetic on two ICONSTs.
case HIR_ADD:
if (s1 >= 0 && s2 >= 0
&& h.kind[s1] == HIR_ICONST && h.kind[s2] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = h.val[s1] + h.val[s2];
h.src1[i] = h.src2[i] = -1;
changed = true;
}
// Algebraic: x + 0 = x.
else if (s2 >= 0 && h.kind[s2] == HIR_ICONST && h.val[s2] == 0) {
h.kind[i] = HIR_COPY;
h.src2[i] = -1;
changed = true;
}
// Algebraic: 0 + x = x.
else if (s1 >= 0 && h.kind[s1] == HIR_ICONST && h.val[s1] == 0) {
h.kind[i] = HIR_COPY;
h.src1[i] = s2;
h.src2[i] = -1;
changed = true;
}
break;
case HIR_SUB:
if (s1 >= 0 && s2 >= 0
&& h.kind[s1] == HIR_ICONST && h.kind[s2] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = h.val[s1] - h.val[s2];
h.src1[i] = h.src2[i] = -1;
changed = true;
}
// Algebraic: x - 0 = x.
else if (s2 >= 0 && h.kind[s2] == HIR_ICONST && h.val[s2] == 0) {
h.kind[i] = HIR_COPY;
h.src2[i] = -1;
changed = true;
}
break;
case HIR_MUL:
if (s1 >= 0 && s2 >= 0
&& h.kind[s1] == HIR_ICONST && h.kind[s2] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = h.val[s1] * h.val[s2];
h.src1[i] = h.src2[i] = -1;
changed = true;
}
// Algebraic: x * 0 = 0.
else if (s2 >= 0 && h.kind[s2] == HIR_ICONST && h.val[s2] == 0) {
h.kind[i] = HIR_ICONST;
h.val[i] = 0;
h.src1[i] = h.src2[i] = -1;
changed = true;
}
else if (s1 >= 0 && h.kind[s1] == HIR_ICONST && h.val[s1] == 0) {
h.kind[i] = HIR_ICONST;
h.val[i] = 0;
h.src1[i] = h.src2[i] = -1;
changed = true;
}
// Algebraic: x * 1 = x.
else if (s2 >= 0 && h.kind[s2] == HIR_ICONST && h.val[s2] == 1) {
h.kind[i] = HIR_COPY;
h.src2[i] = -1;
changed = true;
}
else if (s1 >= 0 && h.kind[s1] == HIR_ICONST && h.val[s1] == 1) {
h.kind[i] = HIR_COPY;
h.src1[i] = s2;
h.src2[i] = -1;
changed = true;
}
break;
case HIR_REM:
if (s1 >= 0 && s2 >= 0
&& h.kind[s1] == HIR_ICONST && h.kind[s2] == HIR_ICONST
&& h.val[s2] != 0) {
h.kind[i] = HIR_ICONST;
// Guard INT64_MIN % -1 — UB that traps on x86 (#805).
h.val[i] = (h.val[s1] == INT64_MIN && h.val[s2] == -1)
? 0 : h.val[s1] % h.val[s2];
h.src1[i] = h.src2[i] = -1;
changed = true;
}
break;
case HIR_DIV:
if (s1 >= 0 && s2 >= 0
&& h.kind[s1] == HIR_ICONST && h.kind[s2] == HIR_ICONST
&& h.val[s2] != 0) {
h.kind[i] = HIR_ICONST;
// Guard INT64_MIN / -1 — UB that traps on x86 (#805).
h.val[i] = (h.val[s1] == INT64_MIN && h.val[s2] == -1)
? INT64_MIN : h.val[s1] / h.val[s2];
h.src1[i] = h.src2[i] = -1;
changed = true;
}
break;
case HIR_SIGN:
if (s1 >= 0 && h.kind[s1] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = (h.val[s1] < 0) ? -1 : (h.val[s1] > 0) ? 1 : 0;
h.src1[i] = -1;
changed = true;
}
break;
case HIR_MAX:
if (s1 >= 0 && s2 >= 0
&& h.kind[s1] == HIR_ICONST && h.kind[s2] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = (h.val[s1] >= h.val[s2]) ? h.val[s1] : h.val[s2];
h.src1[i] = h.src2[i] = -1;
changed = true;
}
break;
case HIR_MIN:
if (s1 >= 0 && s2 >= 0
&& h.kind[s1] == HIR_ICONST && h.kind[s2] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = (h.val[s1] <= h.val[s2]) ? h.val[s1] : h.val[s2];
h.src1[i] = h.src2[i] = -1;
changed = true;
}
break;
// Comparisons on two ICONSTs.
case HIR_EQ:
if (s1 >= 0 && s2 >= 0
&& h.kind[s1] == HIR_ICONST && h.kind[s2] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = (h.val[s1] == h.val[s2]) ? 1 : 0;
h.src1[i] = h.src2[i] = -1;
changed = true;
}
break;
case HIR_NE:
if (s1 >= 0 && s2 >= 0
&& h.kind[s1] == HIR_ICONST && h.kind[s2] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = (h.val[s1] != h.val[s2]) ? 1 : 0;
h.src1[i] = h.src2[i] = -1;
changed = true;
}
break;
case HIR_LT:
if (s1 >= 0 && s2 >= 0
&& h.kind[s1] == HIR_ICONST && h.kind[s2] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = (h.val[s1] < h.val[s2]) ? 1 : 0;
h.src1[i] = h.src2[i] = -1;
changed = true;
}
break;
case HIR_LE:
if (s1 >= 0 && s2 >= 0
&& h.kind[s1] == HIR_ICONST && h.kind[s2] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = (h.val[s1] <= h.val[s2]) ? 1 : 0;
h.src1[i] = h.src2[i] = -1;
changed = true;
}
break;
case HIR_GT:
if (s1 >= 0 && s2 >= 0
&& h.kind[s1] == HIR_ICONST && h.kind[s2] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = (h.val[s1] > h.val[s2]) ? 1 : 0;
h.src1[i] = h.src2[i] = -1;
changed = true;
}
break;
case HIR_GE:
if (s1 >= 0 && s2 >= 0
&& h.kind[s1] == HIR_ICONST && h.kind[s2] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = (h.val[s1] >= h.val[s2]) ? 1 : 0;
h.src1[i] = h.src2[i] = -1;
changed = true;
}
break;
// NOT of ICONST.
case HIR_NOT:
if (s1 >= 0 && h.kind[s1] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = (h.val[s1] == 0) ? 1 : 0;
h.src1[i] = -1;
changed = true;
}
break;
// BOOL (t) of ICONST.
case HIR_BOOL:
if (s1 >= 0 && h.kind[s1] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = (h.val[s1] != 0) ? 1 : 0;
h.src1[i] = -1;
changed = true;
}
break;
// INC / DEC of ICONST.
// Signed overflow is C++ UB; softcode/runtime wrap via
// ADDI (two's complement). Fold with unsigned intermediate
// so INT64_MAX+1 / INT64_MIN-1 match wrap (#1259).
case HIR_INC:
if (s1 >= 0 && h.kind[s1] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = static_cast<int64_t>(
static_cast<uint64_t>(h.val[s1]) + 1u);
h.src1[i] = -1;
changed = true;
}
break;
case HIR_DEC:
if (s1 >= 0 && h.kind[s1] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = static_cast<int64_t>(
static_cast<uint64_t>(h.val[s1]) - 1u);
h.src1[i] = -1;
changed = true;
}
break;
// NEG of ICONST.
// -INT64_MIN is C++ UB (same class as #805 / #1150). Keep
// INT64_MIN to match two's-complement wrap from RV64 SUB
// dest,x0,rs and i64Division's INT64_MIN/-1 style. (#1258)
case HIR_NEG:
if (s1 >= 0 && h.kind[s1] == HIR_ICONST) {
h.kind[i] = HIR_ICONST;
h.val[i] = (h.val[s1] == INT64_MIN)
? INT64_MIN : -h.val[s1];
h.src1[i] = -1;
changed = true;
}
break;
// ---------------------------------------------------------------
// Float constant folding.
// ---------------------------------------------------------------
// ITOF of ICONST → FCONST.
case HIR_ITOF:
if (s1 >= 0 && h.kind[s1] == HIR_ICONST) {
double v = static_cast<double>(h.val[s1]);
h.kind[i] = HIR_FCONST;
h.ty[i] = TY_FLOAT;
h.fval[i] = v;
h.src1[i] = -1;
changed = true;
}
break;
// FTOI of FCONST → ICONST.
case HIR_FTOI:
if (s1 >= 0 && h.kind[s1] == HIR_FCONST) {
h.kind[i] = HIR_ICONST;
h.ty[i] = TY_INT;
h.val[i] = static_cast<int64_t>(h.fval[s1]);
h.src1[i] = -1;
changed = true;
}
break;
// Binary float arithmetic on two FCONSTs.
#define FOLD_FBINOP(HIR_OP, C_OP) \
case HIR_OP: \
if (s1 >= 0 && s2 >= 0 \
&& h.kind[s1] == HIR_FCONST && h.kind[s2] == HIR_FCONST) { \
h.kind[i] = HIR_FCONST; \
h.fval[i] = h.fval[s1] C_OP h.fval[s2]; \
h.src1[i] = h.src2[i] = -1; \
changed = true; \
} \
break;
FOLD_FBINOP(HIR_FADD, +)
FOLD_FBINOP(HIR_FSUB, -)
FOLD_FBINOP(HIR_FMUL, *)
FOLD_FBINOP(HIR_FDIV, /)
#undef FOLD_FBINOP
// Unary float ops on FCONST.
case HIR_FNEG:
if (s1 >= 0 && h.kind[s1] == HIR_FCONST) {
h.kind[i] = HIR_FCONST;
h.fval[i] = -h.fval[s1];
h.src1[i] = -1;
changed = true;
}
break;
case HIR_FSQRT:
if (s1 >= 0 && h.kind[s1] == HIR_FCONST) {
h.kind[i] = HIR_FCONST;
h.fval[i] = ::sqrt(h.fval[s1]);
h.src1[i] = -1;
changed = true;
}
break;
// Float comparisons on two FCONSTs → ICONST (0/1).
#define FOLD_FCMP(HIR_OP, C_OP) \
case HIR_OP: \
if (s1 >= 0 && s2 >= 0 \
&& h.kind[s1] == HIR_FCONST && h.kind[s2] == HIR_FCONST) { \
h.kind[i] = HIR_ICONST; \
h.ty[i] = TY_INT; \
h.val[i] = (h.fval[s1] C_OP h.fval[s2]) ? 1 : 0; \
h.src1[i] = h.src2[i] = -1; \
changed = true; \
} \
break;
FOLD_FCMP(HIR_FEQ, ==)
FOLD_FCMP(HIR_FLT, <)
FOLD_FCMP(HIR_FLE, <=)
#undef FOLD_FCMP
// FCALL1 on FCONST → evaluate libm at compile time.
case HIR_FCALL1:
if (s1 >= 0 && h.kind[s1] == HIR_FCONST) {
double a = h.fval[s1];
double r;
switch (h.func_idx[i]) {
case FMATH_SIN: r = ::sin(a); break;
case FMATH_COS: r = ::cos(a); break;
case FMATH_TAN: r = ::tan(a); break;
case FMATH_ASIN: r = ::asin(a); break;
case FMATH_ACOS: r = ::acos(a); break;
case FMATH_ATAN: r = ::atan(a); break;
case FMATH_EXP: r = ::exp(a); break;
case FMATH_LOG: r = ::log(a); break;
case FMATH_LOG10: r = ::log10(a); break;
case FMATH_SQRT: r = ::sqrt(a); break;
case FMATH_CEIL: r = ::ceil(a); break;
case FMATH_FLOOR: r = ::floor(a); break;
case FMATH_FABS: r = ::fabs(a); break;
default: goto no_fold;
}
h.kind[i] = HIR_FCONST;
h.fval[i] = r;
h.src1[i] = -1;
changed = true;
no_fold:;
}
break;
// FCALL2 on two FCONSTs → evaluate libm at compile time.
case HIR_FCALL2:
if (s1 >= 0 && s2 >= 0
&& h.kind[s1] == HIR_FCONST && h.kind[s2] == HIR_FCONST) {
double a = h.fval[s1];
double b = h.fval[s2];
double r;
switch (h.func_idx[i]) {
case FMATH_POW: r = ::pow(a, b); break;
case FMATH_ATAN2: r = ::atan2(a, b); break;
case FMATH_FMOD: r = ::fmod(a, b); break;
case FMATH_FMAX: r = ::fmax(a, b); break;
case FMATH_FMIN: r = ::fmin(a, b); break;
default: goto no_fold2;
}
h.kind[i] = HIR_FCONST;
h.fval[i] = r;
h.src1[i] = h.src2[i] = -1;
changed = true;
no_fold2:;
}
break;
default:
break;
}
}
}
}
// ---------------------------------------------------------------
// Copy propagation
//
// Replace all references to COPY instructions with their source.
// Iterates until no more changes (handles chains: COPY of COPY).
// ---------------------------------------------------------------
static int resolve_copy(hir_program &h, int i) {
while (i >= 0 && h.kind[i] == HIR_COPY && h.src1[i] >= 0) {
i = h.src1[i];
}
return i;
}
void hir_copy_prop(hir_program &h) {
bool changed = true;
while (changed) {
changed = false;
for (int i = 0; i < h.n_insns; i++) {
// Propagate through src1.
if (h.src1[i] >= 0) {
int r = resolve_copy(h, h.src1[i]);
if (r != h.src1[i]) {
h.src1[i] = r;
changed = true;
}
}
// Propagate through src2 (skip BRC — src2 is a block number).
if (h.src2[i] >= 0 && h.kind[i] != HIR_BRC) {
int r = resolve_copy(h, h.src2[i]);
if (r != h.src2[i]) {
h.src2[i] = r;
changed = true;
}
}
// Everything that is not src1/src2: the val[] slot and any
// argument list, walked through the operand accessors so this
// pass does not need to know which opcode keeps what where.
//
// hir_operand_set matters here as much as the walk does: the
// val[] slot is a plain index for SETI/SETFIELD but PACKED with
// nargs for CALL_INT, and the previous `h.val[i] = r` preserved
// neither.
// val[] and argument lists (CALL/STRCAT carg[], PHI pval[]).
//
for (int sl = HIR_SLOT_VAL; sl < hir_operand_count(h, i); sl++) {
int cur = hir_operand_get(h, i, sl);
if (cur < 0) continue;
int r = resolve_copy(h, cur);
if (r != cur) {
hir_operand_set(h, i, sl, r);
changed = true;
}
}
}
// Propagate through result.
if (h.result >= 0) {
int r = resolve_copy(h, h.result);
if (r != h.result) {
h.result = r;
changed = true;
}
}
}
}
// ---------------------------------------------------------------
// Dead code elimination
//
// Mark all instructions that contribute to the result or have
// side effects. Replace unmarked instructions with NOP.
// ---------------------------------------------------------------
static bool has_side_effects(hir_kind k) {
// HIR_LUA_SETI mutates table cells — DCE must keep stores even when
// the result (if any) is unused (#1145). HIR_LUA_CALL_VOID exists
// only for its effect (table.insert): it produces no value at all,
// so nothing downstream can keep it alive — this line is what does.
// HIR_CALL_T2I: APPEND_I mutates its accumulator in place and
// SPLIT_STEP writes its output slot — both exist for their effects
// even when the integer result goes unused (#2132).
return k == HIR_CALL || k == HIR_STRCAT || k == HIR_CALL_T2I
|| k == HIR_STORE_Q
|| k == HIR_SETQ_SYNC
|| k == HIR_LCTX_DEPTH || k == HIR_LCTX_ELEM || k == HIR_LCTX_INUM
|| k == HIR_LCTX_KEEP
|| k == HIR_LUA_SETI || k == HIR_LUA_SETFIELD
|| k == HIR_LUA_CALL_VOID || k == HIR_LUA_CALL_VAL
|| k == HIR_LUA_LIMITED
|| k == HIR_RET || k == HIR_BR || k == HIR_BRC;
}
void hir_dce(hir_program &h) {
bool used[HIR_MAX_INSNS];
memset(used, 0, sizeof(bool) * h.n_insns);
// Mark side-effectful instructions.
for (int i = 0; i < h.n_insns; i++) {
if (has_side_effects(h.kind[i])) {
used[i] = true;
}
}
// Mark the result.
if (h.result >= 0 && h.result < h.n_insns) {
used[h.result] = true;
}
// Propagate: mark operands of used instructions.
bool changed = true;
while (changed) {
changed = false;
for (int i = 0; i < h.n_insns; i++) {
if (!used[i]) continue;
// Every operand, whatever slot it lives in. BRC's src2 is a
// block number and the accessor already refuses it. PHI args
// are ARG slots via hir_operand_count.
//
for (int sl = 0; sl < hir_operand_count(h, i); sl++) {
int a = hir_operand_get(h, i, sl);
if (a >= 0 && !used[a]) {
used[a] = true;
changed = true;
}
}
}
}
// Replace dead instructions with NOP.
int eliminated = 0;
for (int i = 0; i < h.n_insns; i++) {
if (!used[i] && h.kind[i] != HIR_NOP) {
h.kind[i] = HIR_NOP;
h.src1[i] = h.src2[i] = -1;
eliminated++;
}
}
}
// ---------------------------------------------------------------
// Common Subexpression Elimination (CSE)
//
// Walk instructions in block order. For each pure instruction,
// check if an identical (kind, ty, src1, src2, val) exists earlier
// in a dominating block. If so, replace the duplicate with a COPY
// referencing the original. ECALLs and side-effecting instructions
// are never CSE candidates (they may produce different results on
// repeated calls, e.g., rand()).
// ---------------------------------------------------------------
static bool is_pure_op(hir_kind k) {
// Guest memory / table ops are NOT pure: SETQ_SYNC and ECALLs mutate
// qreg longbits and table cells between otherwise-identical loads
// (#1144 ALOAD/GETI; #1145 SETI must not CSE or be treated pure).
//
// Do NOT "restore" HIR_LUA_ALOAD here for performance. It looks like
// #1144 gave up real CSE -- emit_qreg_read() loads QREG_LONGBITS once
// per %q read, so strcat(%q0,%q1,%q2,%q3) emits four identical ALOADs
// of one address -- but GVN never collapsed them even when ALOAD was
// pure. Each carries its own ICONST 1 operand, HIR_ICONST is not
// value-numbered, so the ValueKeys differ and nothing matches. Making
// ALOAD pure again therefore changes neither the emitted HIR (measured:
// still 4 ALOADs post-opt) nor the runtime.
//
// Collapsing them needs HIR_ICONST value-numbered as well -- which does
// work, 4 ALOADs become 1 -- but that only pays off with ALOAD pure,
// and ALOAD cannot be pure without memory-dependence tracking: a naive
// "bump a generation counter on each clobber" scheme is unsound at
// merge blocks, because GVN walks the dominator tree and a clobber on
// a sibling path may be visited after the merge that it must invalidate.
switch (k) {
case HIR_ADD: case HIR_SUB: case HIR_MUL: case HIR_DIV:
case HIR_REM: case HIR_NEG: case HIR_SIGN:
case HIR_MAX: case HIR_MIN:
case HIR_EQ: case HIR_NE: case HIR_LT: case HIR_LE:
case HIR_GT: case HIR_GE:
case HIR_NOT: case HIR_BOOL:
case HIR_INC: case HIR_DEC:
case HIR_ATOI: case HIR_ITOA: case HIR_STRCMP:
case HIR_FADD: case HIR_FSUB: case HIR_FMUL: case HIR_FDIV:
case HIR_FNEG:
case HIR_FEQ: case HIR_FLT: case HIR_FLE:
case HIR_ITOF: case HIR_FTOI:
return true;
default:
return false;
}
}
// Keep old name as alias for LICM and any other consumer.
static bool is_cse_candidate(hir_kind k) { return is_pure_op(k); }
// ---------------------------------------------------------------
// Global Value Numbering (GVN)
//
// Dominator-tree walk with a scoped hash table. Strictly more
// powerful than the old CSE: uses value numbers of operands
// (not raw instruction indices) so it sees through COPYs, and
// the dominator-tree walk guarantees that only dominating values
// are visible — no per-candidate dominance check needed.
//
// Subsumes the old hir_cse().
// ---------------------------------------------------------------
// Resolve an instruction to its value number: chase COPY chains.
//
static int vn_resolve(const hir_program &h, const int *vn, int i) {
if (i < 0) return i;
// Chase COPYs to their source's value number.
int limit = 64;
while (i >= 0 && i < h.n_insns && h.kind[i] == HIR_COPY && --limit > 0) {
i = h.src1[i];
}
if (i >= 0 && i < h.n_insns) return vn[i];
return i;
}
// Scoped value table: entries added in a dominator subtree are
// removed when leaving. Each scope records what to undo.
//
struct GVNScope {
// Keys inserted in this scope that need removal on exit.
std::vector<ValueKey> inserted;
// Keys that were overwritten — restore on exit.
std::vector<std::pair<ValueKey, int>> overwritten;
};
static void gvn_process_block(
hir_program &h,
int b,
int *vn,
std::unordered_map<ValueKey, int, ValueKeyHash> &table)
{
GVNScope scope;
// Process all instructions in this block.
for (int i = 0; i < h.n_insns; i++) {
if (h.blk[i] != b) continue;
if (h.kind[i] == HIR_NOP) continue;
// Every instruction's value number defaults to itself.
vn[i] = i;
// COPYs just inherit their source's value number.
if (h.kind[i] == HIR_COPY) {
vn[i] = vn_resolve(h, vn, h.src1[i]);
continue;
}
// PHIs: value number is self (can't look through them here).
if (h.kind[i] == HIR_PHI) continue;
// Only pure operations participate in numbering.
if (!is_pure_op(h.kind[i])) continue;
// Build key using value-numbered operands.
int vn_src1 = vn_resolve(h, vn, h.src1[i]);
int vn_src2 = vn_resolve(h, vn, h.src2[i]);
ValueKey key = { h.kind[i], h.ty[i], vn_src1, vn_src2, h.val[i] };
// Normalize commutative operations.
if (h.kind[i] == HIR_ADD || h.kind[i] == HIR_MUL ||
h.kind[i] == HIR_EQ || h.kind[i] == HIR_NE ||
h.kind[i] == HIR_FADD || h.kind[i] == HIR_FMUL) {
if (key.src1 > key.src2) {
std::swap(key.src1, key.src2);
}
}
auto it = table.find(key);
if (it != table.end()) {
// Value already computed by a dominating instruction.
// Replace this instruction with a COPY.
vn[i] = it->second;
h.kind[i] = HIR_COPY;
h.src1[i] = it->second;
h.src2[i] = -1;
h.val[i] = 0;
} else {
// New value — add to table.
vn[i] = i;
table[key] = i;
scope.inserted.push_back(key);
}
}
// Recurse into dominated children.
for (int c = 0; c < h.n_blocks; c++) {
if (h.idom[c] == b && c != b) {
gvn_process_block(h, c, vn, table);
}
}
// Undo this scope: remove entries added in this block.
for (auto &key : scope.inserted) {
table.erase(key);
}
}
void hir_gvn(hir_program &h) {
int vn[HIR_MAX_INSNS];
for (int i = 0; i < h.n_insns; i++) {
vn[i] = i;
}
std::unordered_map<ValueKey, int, ValueKeyHash> table;
gvn_process_block(h, 0, vn, table);
}
// ---------------------------------------------------------------
// Loop-Invariant Code Motion (LICM)
//
// For each loop (identified by back-edges), move pure instructions
// whose operands are all defined outside the loop to the preheader.
// "Moving" an instruction means changing its block assignment.
// The preheader is the unique predecessor of the header that is NOT
// the latch (i.e., the entry block for our structured iter loops).
// ---------------------------------------------------------------
void hir_licm(hir_program &h) {
if (h.n_rpo == 0) return;
// Find back-edges and process each loop.
for (int latch = 0; latch < h.n_blocks; latch++) {
for (int s = 0; s < h.block_nsucc[latch]; s++) {
int header = h.block_succ[latch][s];
if (header < 0) continue;
if (h.rpo_pos[header] > h.rpo_pos[latch]) continue;
// Back-edge: latch → header.
// Find preheader: predecessor of header that is NOT in the loop.
int preheader = -1;
for (int p = 0; p < h.n_pred[header]; p++) {
int pred = h.pred_list[h.pred_base[header] + p];
if (h.rpo_pos[pred] < h.rpo_pos[header]) {
preheader = pred;
break;
}
}
if (preheader < 0) continue;
// Mark which blocks are in this loop.
//
// This is the NATURAL loop: header, plus every block that can
// reach the latch without passing through the header --
// computed by walking predecessors backwards from the latch.
//
// It used to be "RPO position between header and latch", which
// over-approximates: with two nested iter() loops, the inner
// loop's EXIT block sat between the inner header and latch in
// RPO without being part of the loop. Instructions in it
// whose operands were outer-loop values then looked
// loop-invariant and were "hoisted" -- see below for why any
// hoist that actually fires is fatal (#2072).
bool in_loop[HIR_MAX_BLOCKS];
memset(in_loop, 0, sizeof(in_loop));
in_loop[header] = true;
{
int work[HIR_MAX_BLOCKS];
int n_work = 0;
if (!in_loop[latch]) {
in_loop[latch] = true;
work[n_work++] = latch;
}
while (n_work > 0) {
int b = work[--n_work];
for (int p = 0; p < h.n_pred[b]; p++) {
int pred = h.pred_list[h.pred_base[b] + p];
if (pred < 0 || pred >= h.n_blocks) continue;
if (in_loop[pred]) continue;
in_loop[pred] = true;
work[n_work++] = pred;
}
}
}
// Iteratively hoist loop-invariant instructions.
bool moved = true;
while (moved) {
moved = false;
for (int i = 0; i < h.n_insns; i++) {
if (h.kind[i] == HIR_NOP) continue;
if (!in_loop[h.blk[i]]) continue;
if (!is_cse_candidate(h.kind[i])) continue;
// Check if all operands are loop-invariant
// (defined outside the loop or already hoisted).
auto is_invariant = [&](int v) -> bool {
if (v < 0) return true;
if (v >= h.n_insns) return true;
if (h.kind[v] == HIR_ICONST || h.kind[v] == HIR_SCONST)
return true;
return !in_loop[h.blk[v]];
};
if (!is_invariant(h.src1[i])) continue;
if (h.kind[i] != HIR_BRC && !is_invariant(h.src2[i]))
continue;
// Structural safety: a "hoist" here is only a blk[]
// retag, and everything downstream -- block ranges,
// program points, live intervals, and the codegen walk
// itself -- iterates block_first[b]..block_last[b] in
// INDEX order. An instruction whose index falls after
// the preheader's terminator can never be emitted at
// its new home: either it vanishes from codegen (the
// ranges exclude it -- loc stays 0 and its consumers
// read guest address 0), or with recomputed ranges it
// lands after the preheader's BR as dead bytes. Both
// corrupt silently. So only move an instruction that
// already sits inside the preheader's index range,
// before its terminator -- which a loop-body
// instruction, emitted after the preheader by
// construction, never does.
//
// In practice this makes LICM inert until the IR can
// express instruction motion. Inert is strictly
// better: the one program shape that ever made this
// fire -- nested iter() with the accumulator rework
// (#2072) -- got its append-length ITOA silently
// dropped from codegen, and every outer iteration
// appended at offset 0.
int pre_term = -1;
for (int t = h.block_first[preheader];
t <= h.block_last[preheader]; t++) {
if (h.blk[t] != preheader) continue;
if (h.kind[t] == HIR_BR || h.kind[t] == HIR_BRC) {
pre_term = t;
break;
}
}
if (pre_term < 0
|| i < h.block_first[preheader]
|| i >= pre_term) {
continue;
}
// Hoist: move to preheader.
h.blk[i] = preheader;
moved = true;
}
}
}
}
}
// ---------------------------------------------------------------
// Peephole optimization
//
// Pattern-based simplification on HIR instruction pairs/triples.
// Runs as part of the optimization loop since it creates COPYs
// that copy propagation and DCE can clean up.
//
// Patterns:
// ATOI(ITOA(x)) → COPY x (round-trip elimination)
// BOOL(BOOL(x)) → COPY x (idempotent)
// NOT(NOT(x)) → BOOL x (double negation → bool)
// BOOL(cmp) → COPY cmp (comparisons already return 0/1)
// NEG(NEG(x)) → COPY x (double negation)
// FNEG(FNEG(x)) → COPY x (double negation, float)
// ADD(x, NEG(y)) → SUB(x, y) (strength reduction)
// SUB(x, NEG(y)) → ADD(x, y) (strength reduction)
// MUL(x, 2^k) → SHL(x, k) (strength reduction)
// NOT(EQ(a,b)) → NE(a,b) (comparison inversion)
// NOT(NE(a,b)) → EQ(a,b)
// NOT(LT(a,b)) → GE(a,b)
// NOT(LE(a,b)) → GT(a,b)
// NOT(GT(a,b)) → LE(a,b)
// NOT(GE(a,b)) → LT(a,b)
// BRC(NOT(x), T, F) → BRC(x, F, T) (branch inversion)
// ---------------------------------------------------------------
// Helper: is this kind a comparison that always returns 0 or 1?
static bool is_cmp_kind(hir_kind k) {
switch (k) {
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_NOT: case HIR_BOOL:
return true;
default:
return false;
}
}
// Helper: is this an exact power of two? Returns the exponent, or -1.
static int log2_exact(int64_t v) {
if (v <= 0) return -1;
if (v & (v - 1)) return -1;
int k = 0;
while (v > 1) { v >>= 1; k++; }
return k;
}
void hir_peephole(hir_program &h) {
bool changed = true;
while (changed) {
changed = false;
for (int i = 0; i < h.n_insns; i++) {
int s1 = h.src1[i];
int s2 = h.src2[i];
switch (h.kind[i]) {
// ATOI(ITOA(x)) → COPY x.
// The round-trip string→int→string→int is identity on integers.
case HIR_ATOI:
if (s1 >= 0 && h.kind[s1] == HIR_ITOA) {
h.kind[i] = HIR_COPY;
h.src1[i] = h.src1[s1];
h.ty[i] = TY_INT;
changed = true;
}
break;
// BOOL(BOOL(x)) → COPY x. (idempotent: already 0/1)
// BOOL(cmp) → COPY cmp. (comparisons already return 0/1)
case HIR_BOOL:
if (s1 >= 0 && is_cmp_kind(h.kind[s1])) {
h.kind[i] = HIR_COPY;
changed = true;
}
break;
// NOT(NOT(x)) → BOOL(x). (double negation restores truth value)
// NOT(cmp) → inverted cmp.
case HIR_NOT:
if (s1 >= 0 && h.kind[s1] == HIR_NOT) {
h.kind[i] = HIR_BOOL;
h.src1[i] = h.src1[s1];
changed = true;
} else if (s1 >= 0) {
// Invert comparisons: NOT(EQ(a,b)) → NE(a,b), etc.
hir_kind inv = HIR_NOP;
switch (h.kind[s1]) {
case HIR_EQ: inv = HIR_NE; break;
case HIR_NE: inv = HIR_EQ; break;
case HIR_LT: inv = HIR_GE; break;
case HIR_LE: inv = HIR_GT; break;
case HIR_GT: inv = HIR_LE; break;
case HIR_GE: inv = HIR_LT; break;
default: break;
}
if (inv != HIR_NOP) {
h.kind[i] = inv;
h.src1[i] = h.src1[s1];
h.src2[i] = h.src2[s1];
changed = true;
}
}
break;
// NEG(NEG(x)) → COPY x.
case HIR_NEG:
if (s1 >= 0 && h.kind[s1] == HIR_NEG) {
h.kind[i] = HIR_COPY;
h.src1[i] = h.src1[s1];
changed = true;
}
break;
// FNEG(FNEG(x)) → COPY x.
case HIR_FNEG:
if (s1 >= 0 && h.kind[s1] == HIR_FNEG) {
h.kind[i] = HIR_COPY;
h.src1[i] = h.src1[s1];
h.ty[i] = TY_FLOAT;
changed = true;
}
break;
// ADD(x, NEG(y)) → SUB(x, y).
case HIR_ADD:
if (s2 >= 0 && h.kind[s2] == HIR_NEG) {
h.kind[i] = HIR_SUB;
h.src2[i] = h.src1[s2];
changed = true;
}
// NEG(x) + y → SUB(y, x).
else if (s1 >= 0 && h.kind[s1] == HIR_NEG) {
h.kind[i] = HIR_SUB;
h.src1[i] = s2;
h.src2[i] = h.src1[s1];
changed = true;
}
break;
// SUB(x, NEG(y)) → ADD(x, y).
case HIR_SUB:
if (s2 >= 0 && h.kind[s2] == HIR_NEG) {
h.kind[i] = HIR_ADD;
h.src2[i] = h.src1[s2];
changed = true;
}
break;
// Future: MUL(x, 2^k) → SHL(x, k) once use-count analysis
// is available to safely rewrite shared ICONSTs.
// BRC(NOT(x), T, F) → BRC(x, F, T). Branch inversion.
case HIR_BRC:
if (s1 >= 0 && h.kind[s1] == HIR_NOT) {
h.src1[i] = h.src1[s1];
// Swap true/false targets.
int true_blk = static_cast<int>(h.val[i]);
int false_blk = h.src2[i];
h.val[i] = false_blk;
h.src2[i] = true_blk;
changed = true;
}
break;
default:
break;
}
}
}
}
// ---------------------------------------------------------------
// Superblock formation (block merging)
//
// Merge pairs of basic blocks where block A has exactly one
// successor (block B) and block B has exactly one predecessor
// (block A). The unconditional branch from A to B becomes a NOP,
// and all instructions in B are reassigned to block A.
//
// This runs after SSA construction and before the main optimization
// loop. It reduces control flow overhead and enables better
// optimization across the merged region.
//
// After merging, the CFG is rebuilt to maintain consistency.
// ---------------------------------------------------------------
// Renumber blocks to eliminate gaps left by merging.
// Builds a compact mapping and rewrites all block references.
//
static void hir_renumber_blocks(hir_program &h) {
int remap[HIR_MAX_BLOCKS];
memset(remap, -1, sizeof(int) * h.n_blocks);
// Find which block IDs are still in use.
bool used[HIR_MAX_BLOCKS];
memset(used, 0, sizeof(bool) * h.n_blocks);
for (int i = 0; i < h.n_insns; i++) {
int b = h.blk[i];
if (b >= 0 && b < h.n_blocks && h.kind[i] != HIR_NOP) {
used[b] = true;
}
}
// Block 0 is always the entry — keep it even if empty.
used[0] = true;
// Build compact mapping.
int new_count = 0;
for (int b = 0; b < h.n_blocks; b++) {
if (used[b]) {
remap[b] = new_count++;
}
}
// If nothing changed, skip the rewrite.
if (new_count == h.n_blocks) return;
// Rewrite blk[] on all instructions.
for (int i = 0; i < h.n_insns; i++) {
int b = h.blk[i];
if (b >= 0 && b < h.n_blocks && remap[b] >= 0) {
h.blk[i] = remap[b];
}
}
// Rewrite branch targets.
for (int i = 0; i < h.n_insns; i++) {
if (h.kind[i] == HIR_BR) {
int t = static_cast<int>(h.val[i]);
if (t >= 0 && t < h.n_blocks && remap[t] >= 0) {
h.val[i] = remap[t];
}
} else if (h.kind[i] == HIR_BRC) {
int t = static_cast<int>(h.val[i]);
if (t >= 0 && t < h.n_blocks && remap[t] >= 0) {
h.val[i] = remap[t];
}
int f = h.src2[i];
if (f >= 0 && f < h.n_blocks && remap[f] >= 0) {
h.src2[i] = remap[f];
}
}
}
// Rewrite PHI predecessor block IDs.
for (int i = 0; i < h.n_insns; i++) {
if (h.kind[i] != HIR_PHI) continue;
int base = h.pbase[i];
for (int j = 0; j < h.pnargs[i]; j++) {
int b = h.pblk[base + j];
if (b >= 0 && b < h.n_blocks && remap[b] >= 0) {
h.pblk[base + j] = remap[b];
}
}
}
h.n_blocks = new_count;
h.cur_block = (h.cur_block >= 0 && h.cur_block < HIR_MAX_BLOCKS
&& remap[h.cur_block] >= 0) ? remap[h.cur_block] : 0;
}
void hir_superblock(hir_program &h) {
if (h.n_blocks <= 1) return;
// Rebuild CFG to get fresh pred/succ info.
hir_build_cfg(h);
bool merged_any = false;
// Iterate until no more merges are possible.
// Bounded by n_blocks: each merge eliminates one block.
bool progress = true;
int limit = h.n_blocks;
while (progress && limit-- > 0) {
progress = false;
for (int a = 0; a < h.n_blocks; a++) {
// Skip empty blocks (already merged away).
if (h.block_last[a] < h.block_first[a]) continue;
// Block A must have exactly one successor.
if (h.block_nsucc[a] != 1) continue;
int b = h.block_succ[a][0];
if (b < 0 || b >= h.n_blocks || b == a) continue;
// Block B must have exactly one predecessor.
if (h.n_pred[b] != 1) continue;
// Block B must not be the entry block.
if (b == 0) continue;
// Block B must not be empty.
if (h.block_last[b] < h.block_first[b]) continue;
// Block A must end with HIR_BR targeting B.
// Pre-SSA, block ranges are contiguous, so a simple
// reverse scan within the range is safe.
int term = -1;
for (int i = h.block_last[a]; i >= h.block_first[a]; i--) {
if (h.blk[i] != a) continue;
if (h.kind[i] == HIR_BR || h.kind[i] == HIR_BRC) {
term = i;
break;
}
}
if (term < 0 || h.kind[term] != HIR_BR) continue;
if (static_cast<int>(h.val[term]) != b) continue;
// Kill the branch instruction.
h.kind[term] = HIR_NOP;
h.src1[term] = h.src2[term] = -1;
// Reassign all instructions in B to block A.
for (int i = 0; i < h.n_insns; i++) {
if (h.blk[i] == b) {
h.blk[i] = a;
}
}
// Rewrite branch targets: anything targeting B now targets A.
for (int i = 0; i < h.n_insns; i++) {
if (h.kind[i] == HIR_BR && static_cast<int>(h.val[i]) == b) {
h.val[i] = a;
}
if (h.kind[i] == HIR_BRC) {
if (static_cast<int>(h.val[i]) == b) h.val[i] = a;
if (h.src2[i] == b) h.src2[i] = a;
}
}
// Rewrite PHI predecessor block IDs: any incoming edge
// recorded from B now comes from merged block A.
for (int i = 0; i < h.n_insns; i++) {
if (h.kind[i] != HIR_PHI) continue;
int base = h.pbase[i];
for (int j = 0; j < h.pnargs[i]; j++) {
if (h.pblk[base + j] == b) {
h.pblk[base + j] = a;
}
}
}
merged_any = true;
progress = true;
// Rebuild CFG after each merge to keep pred info fresh.
hir_build_cfg(h);
break; // restart scan with fresh CFG
}
}
if (merged_any) {
// Compact block numbering: eliminate gaps left by empty blocks.
hir_renumber_blocks(h);
hir_build_cfg(h);
// Ensure every leaf block has an explicit HIR_RET terminator.
// Codegen emits blocks in numeric order with implicit fallthrough;
// without an explicit exit, a leaf block that is no longer last
// in layout will fall through into the next block's code.
for (int b = 0; b < h.n_blocks; b++) {
if (h.block_nsucc[b] != 0) continue; // not a leaf
if (h.block_last[b] < h.block_first[b]) continue; // empty
// Check if block already has a terminator.
bool has_term = false;
for (int i = h.block_first[b]; i <= h.block_last[b]; i++) {
if (h.blk[i] == b && (h.kind[i] == HIR_RET
|| h.kind[i] == HIR_BR || h.kind[i] == HIR_BRC)) {
has_term = true;
break;
}
}
if (has_term) continue;
// Append HIR_RET to this block.
int saved = h.cur_block;
h.cur_block = b;
h.emit(HIR_RET, TY_VOID);
h.cur_block = saved;
}
// Rebuild CFG one final time with the new RET instructions.
hir_build_cfg(h);
// A PHI is only meaningful with incoming edges: codegen resolves
// PHIs by placing copies on each predecessor's branch, so a PHI
// whose home block has NO predecessors is never filled and its
// slot reads back empty — wrong value, jit_handled green
// (#2166). emit_phi refusing single-input PHIs makes this
// unreachable from every current lowering; this guard is the
// last-resort backstop for the next pass that collapses an edge
// out from under a PHI. overflowed is the established decline
// path (#859): the compile bails and the AST evaluator answers,
// visibly (jit_handled=0/N), instead of silently corrupting.
for (int i = 0; i < h.n_insns; i++) {
if (h.kind[i] != HIR_PHI) continue;
int b = h.blk[i];
if (b < 0 || b >= h.n_blocks || h.n_pred[b] == 0) {
h.overflowed = true;
return;
}
}
}
}
// ---------------------------------------------------------------
// Top-level optimization entry point
// ---------------------------------------------------------------
void hir_optimize(hir_program &h) {
// Superblock runs pre-SSA in the compile pipeline (jit_compiler.cpp),
// not here, because it requires contiguous block ranges.
// Run constant folding + peephole + copy prop + CSE + DCE.
// Iterate: folding can create new COPYs, peephole can expose
// new constant operands, copy prop chains, CSE replaces
// duplicates with COPYs, DCE can simplify the graph.
int prev_live = -1;
for (int pass = 0; pass < 4; pass++) {
hir_const_fold(h);
hir_peephole(h);
hir_copy_prop(h);
hir_gvn(h);
hir_dce(h);
// Count remaining live instructions to detect convergence.
int live = 0;
for (int i = 0; i < h.n_insns; i++) {
if (h.kind[i] != HIR_NOP) live++;
}
if (live == prev_live) break; // converged
prev_live = live;
}
// LICM runs once after the main optimization loop.
// It benefits from cleaned-up IR (constants folded, copies
// propagated, dead code eliminated).
if (h.n_blocks > 1) {
hir_licm(h);
}
}