Skip to content

L1 Compiler

opencode-agent[bot] edited this page Sep 13, 2026 · 3 revisions

L1 Compiler Registers

L1 compiler register allocation and virtual stack management for fast JIT compilation.

Overview

The L1 compiler (org.jnode.vm.x86.compiler.l1a) uses a virtual stack (VirtualStack) and register pools (X86RegisterPool) to translate bytecode to native x86 code. Rather than immediately emitting instructions, values are held as Item objects on a virtual stack, keeping them in registers as long as possible. This delayed-emission strategy maximizes register utilization within basic blocks, trading aggressive optimization for fast compilation speed.

Key Components

Class / File Role
core/src/core/org/jnode/vm/x86/compiler/l1a/X86RegisterPool.java Register pool management for GPR and XMM registers
core/src/core/org/jnode/vm/x86/compiler/l1a/VirtualStack.java Virtual operand stack with delayed emission
core/src/core/org/jnode/vm/x86/compiler/l1a/Item.java Abstract base for stack values (constant, GPR, XMM, STACK, LOCAL)
core/src/core/org/jnode/vm/x86/compiler/l1a/WordItem.java Single-register items (INT, FLOAT, REFERENCE)
core/src/core/org/jnode/vm/x86/compiler/l1a/DoubleWordItem.java Two-register items (LONG, DOUBLE) in 32-bit mode

How It Works

Register Pool Architecture

The X86RegisterPool manages available x86 registers in groups. Each RegisterGroupUsage instance represents a group of aliases (e.g., EAX/RAX for different JVM types) and tracks ownership:

// GPRs32 pools: EAX, EDX, ECX, ESI, EBX (EDI reserved for statics)
public static final class GPRs32 extends X86RegisterPool {
    protected RegisterGroupUsage[] initialize() {
        return new RegisterGroupUsage[]{
            new RegisterGroupUsage(
                new RegisterEntry(X86Register.EAX, JvmType.INT),
                new RegisterEntry(X86Register.EAX, JvmType.REFERENCE),
                new RegisterEntry(X86Register.EAX, JvmType.FLOAT), false),
            // ... EDX, ECX, ESI, EBX
        };
    }
}

32-bit GPR priority (highest cost first): EBX < ESI < ECX < EDX < EAX
64-bit GPR priority (highest cost first): R15 < R14 < R13 < R12 < R10 < R9 < R8 < RSI < RBX < RCX < RDX < RAX

Item State Machine

Values transition through states tracked by Item.Kind:

CONSTANT ──load()──> GPR ──push()──> STACK
                      ↑
                      └──load()───┘
  • load(ec): Brings value into a GPR (requests from pool, spills if necessary)
  • push(ec): Emits code to push value onto the actual stack (releases GPR)
  • spill(ec, reg): Spills a GPR to make room (transfers to STACK kind)
  • release(ec): Frees resources without emission

Virtual Stack

The VirtualStack maintains the operand stack as Item objects:

void push(Item item) {
    if (tos == stack.length) growStack();
    stack[tos++] = item;
}

Item pop() {
    tos--;
    return stack[tos];
}

Type-safe pops validate JVM types:

final IntItem popInt() { return (IntItem) pop(JvmType.INT); }
final LongItem popLong() { return (LongItem) pop(JvmType.LONG); }
final RefItem popRef() { return (RefItem) pop(JvmType.REFERENCE); }
final FloatItem popFloat() { return (FloatItem) pop(JvmType.FLOAT); }
final DoubleItem popDouble() { return (DoubleItem) pop(JvmType.DOUBLE); }

Long/Double Handling

32-bit mode: LONG and DOUBLE occupy two registers (LSB + MSB). DoubleWordItem tracks both registers:

final X86Register.GPR32 getLsbRegister(EmitterContext ec) { return lsb; }
final X86Register.GPR32 getMsbRegister(EmitterContext ec) { return msb; }

64-bit mode: LONG and DOUBLE use single GPR64 registers (e.g., RAX).

XMM Register Pools

Floating-point values use SSE XMM registers:

Mode Registers
32-bit (XMMs32) XMM0–XMM7
64-bit (XMMs64) XMM0–XMM15

Special Register Contracts

Register Usage
EAX/RAX Return values (int/ref), implicit for add/sub operands
ECX Shift amount for ishr/lshr
EDX:EAXX 32-bit long return value
EDI Points to statics (reserved)
R12 Points to current VmProcessor (reserved)

Failed Compilation Recovery

The L1 compiler reuses X86BytecodeVisitor instances across method compilations via ThreadLocal. When a compilation fails (e.g., unresolvable class), registers can remain allocated and items can remain on the virtual stack. Without cleanup, the next method compilation would fail with Error("Register(s) in use") in X86RegisterPool.reset().

Recovery chain:

  1. NativeCodeCompiler.doCompile() tracks success via a boolean compiled flag. On failure, calls cleanupBytecodeVisitor(bcv) before releaseBytecodeVisitor(bcv).
  2. X86Level1ACompiler.cleanupBytecodeVisitor() overrides the hook. Unwraps the visitor chain (Optimizing → Verifying → X86BytecodeVisitor) and calls cleanupAfterFailedCompilation().
  3. X86BytecodeVisitor.cleanupAfterFailedCompilation() force-resets GPR pool, XMM pool, and virtual stack.
  4. X86RegisterPool.forceReset() releases each allocated register with a warning log, then resets the pool index (unlike reset() which throws on in-use registers).
Class Method Role
NativeCodeCompiler cleanupBytecodeVisitor() No-op hook for subclasses
X86Level1ACompiler cleanupBytecodeVisitor() Unwraps visitor chain, triggers cleanup
X86BytecodeVisitor cleanupAfterFailedCompilation() Resets all pools and vstack
X86RegisterPool forceReset() Releases allocated registers with warning

Gotchas

  • Basic block barriers: Virtual stack resets at each basic block start. All pending items flush to real stack, limiting register utilization across branches.

  • Aliasing restriction: Modifying a value on the vstack is forbidden. loadLocal() pins any aliased stack items into registers before store.

  • ECX for shifts: Non-constant shift amounts must be in ECX. The compiler explicitly requests ECX for variable shifts.

  • Register priority: Lower-indexed registers have higher allocation cost. EAX is cheapest in 32-bit mode, R15 is cheapest in 64-bit.

  • FPU stack discipline: FPU operations use a separate 8-slot FPUStack. Items must be on top before fxch/fstp operations.

  • 64-bit constants: 64-bit instructions still take 32-bit immediates. Loading large constants requires MOV to register first.

  • Constant-folding item leaks: The ItemFactory tracks item creation/release balance via CHECK_BALANCED_ITEM_FACTORY. When folding constants (e.g., ineg, i2l, lneg), the old constant must be released after creating the new constant. Prior to the fix, ineg, i2l, and lneg constant-folding paths leaked items, causing unbalanced warnings. The fix adds val.release(eContext) after ifac.createIConst() in each constant-folding branch. The warning now includes the leak count for diagnostics.

  • SSE constant folding for sub/mul/div: FPCompilerSSE previously folded all SSE binary operations as addition (fpv1 + fpv2). The fix dispatches by operation type (SSE_SUB, SSE_MUL, SSE_DIV, SSE_ADD) and releases both operand items after folding.

Related Pages

Clone this wiki locally