JIT Compiler
Overview
TinyMUX 2.14 includes an optional just-in-time compiler that translates softcode into native machine code. The JIT is transparent to softcode authors: programs produce identical results whether interpreted or compiled. The compiler targets multiple platforms and applies several optimization passes to generate efficient code.
The JIT is enabled at build time with the --enable-jit configure flag.
Compilation Pipeline
When softcode is compiled, it passes through a multi-stage pipeline:
- AST – The softcode parser produces an abstract syntax tree from the source text.
- HIR – The AST is lowered to a high-level intermediate representation that expresses MUX operations as typed instructions.
- SSA – The HIR is converted to Static Single Assignment form, where every variable is assigned exactly once. This representation simplifies subsequent analysis and optimization.
- Optimize – Multiple optimization passes run over the SSA graph (see below).
- RV64 Codegen – The optimized SSA is lowered to RISC-V 64-bit instructions. RV64 serves as a portable, well-defined target that decouples optimization from platform-specific concerns.
- Dynamic Binary Translation – The RV64 instructions are translated to native machine code for the host platform at execution time.
Multi-Platform Backends
The dynamic binary translator produces native code for three calling conventions:
- x86-64 SysV – Used on Linux, FreeBSD, and macOS.
- x86-64 Win64 – Used on 64-bit Windows.
- AArch64 AAPCS64 – Used on ARM64 platforms.
This design allows the optimization passes and RV64 codegen to remain platform-independent while the final translation step adapts to the host ABI.
Optimization Passes
Compile-Time Constant Folding
The compiler evaluates constant expressions at compile time rather than at runtime. Over 35 built-in functions are eligible for constant folding. For example, add(2,3) is folded to 5 during compilation without generating any runtime instructions.
Peephole Optimization
A peephole pass examines small windows of consecutive instructions and applies local improvements:
- Redundant load-after-store elimination – When a value is stored and immediately loaded back, the redundant load is removed.
- Identity arithmetic removal – Operations that have no effect (such as adding zero or multiplying by one) are eliminated.
- Dead-code cleanup – Instructions whose results are never used are removed.
Superblock Optimization
Straight-line basic blocks with no intervening branches are merged into larger superblocks. This increases the scope available to the peephole optimizer and reduces branch overhead.
ECALL Argument Coercion
When compiled code calls back into the MUX engine (an ECALL), all arguments must be strings. The JIT automatically inserts ITOA (integer-to-ASCII) and FTOA (float-to-ASCII) coercion operations for non-string arguments, so the compiled code correctly interfaces with the string-based softcode engine.
Re-Entrant Execution
JIT-compiled code runs via a shared-heap dynamic binary translator. The execution environment is re-entrant, meaning compiled functions can call other compiled functions or invoke the interpreter without conflicting state. This is essential because softcode evaluation is inherently recursive – functions call functions, triggers fire during evaluation, and so on.
Code Cache
Compiled native code is cached in the SQLite database. Each cached entry is tagged with a JIT_COMPILER_VERSION value. When the compiler is updated and the version changes, stale cache entries are automatically invalidated and the affected softcode is recompiled on next execution.
Lua Bytecode
The JIT pipeline also handles Lua 5.4 bytecode. All 83 Lua opcodes are supported, allowing Lua scripts invoked via @lua or lua() to benefit from the same compilation and optimization passes as native softcode.
Monitoring and Benchmarking
Two built-in tools assist with JIT performance analysis:
- jitstats() – Returns statistics about the JIT compiler’s activity: number of compilations, cache hits, total compiled functions, and related counters.
- astbench() – Benchmarks a softcode expression by running it repeatedly under both the interpreter and the JIT compiler, reporting comparative timings.
Related Topics
jitstats(), astbench(), Lua Scripting, SQLite Storage Backend.