Skip to main content

Bytecode VM & Performance

Uddin-Lang uses a register-based bytecode virtual machine as its default execution engine. This page explains the VM architecture, available opcodes, and the performance APIs introduced in Phase 2.

Architecture Overview

Source (.din)

▼ Tokenizer + Parser
AST (Abstract Syntax Tree)

▼ Compiler
Bytecode ([]Instruction)

▼ VM (register-based)
Result

Why a Bytecode VM?

The previous tree-walker interpreter recursively traversed the AST on every execution. The bytecode VM compiles the AST once into a compact array of 4-byte instructions, then executes them in a tight loop. This eliminates recursive function call overhead and improves CPU cache locality.

Typical speedup vs. tree-walker: 5–15× on numeric/loop-heavy scripts.

VM Design

  • Register-based — each frame has a fixed register array (regs []Value). No operand stack.
  • 4-byte instructions — each Instruction is {Op uint8, Dst uint8, Src1 uint8, Src2 uint8}.
  • Frame stack — function calls push a vmFrame; returns pop it.
  • Try stack — exception handlers use a separate tryStack for OP_TRY/OP_END_TRY.

Switching Between Engines

// Default: VM enabled
config := uddin.DefaultConfig()
config.VMEnabled = true // default

// Use tree-walker (legacy)
config.VMEnabled = false

Opcode Reference

All opcodes fit in a uint8. Instructions are 4 bytes: Op | Dst | Src1 | Src2.

Load / Store

OpcodeEncodingDescription
LOAD_CONSTDst = Constants[Src1<<8|Src2]Load constant from pool into register
LOAD_VARDst = locals[Src1]Load local variable into register
STORE_VARlocals[Dst] = regs[Src1]Store register into local variable
LOAD_UPVALDst = closure.Upvalues[Src1]Load captured variable (closure)
STORE_UPVALclosure.Upvalues[Dst] = regs[Src1]Store into captured variable

Arithmetic — Generic (any Value)

OpcodeOperation
ADDregs[Dst] = regs[Src1] + regs[Src2]
SUBregs[Dst] = regs[Src1] - regs[Src2]
MULregs[Dst] = regs[Src1] * regs[Src2]
DIVregs[Dst] = regs[Src1] / regs[Src2]
MODregs[Dst] = regs[Src1] % regs[Src2]
POWregs[Dst] = regs[Src1] ** regs[Src2]
NEGregs[Dst] = -regs[Src1]

Arithmetic — Typed (int operands, no boxing)

OpcodeOperation
ADD_INTInteger add, skips type assertion overhead
SUB_INTInteger subtract
MUL_INTInteger multiply
DIV_INTInteger divide
MOD_INTInteger modulo

The compiler emits typed variants when both operands are known integers at compile time.

Comparison

OpcodeOperation
EQregs[Dst] = regs[Src1] == regs[Src2]
NEQregs[Dst] = regs[Src1] != regs[Src2]
LTLess than
LTELess than or equal
GTGreater than
GTEGreater than or equal
INMembership test (in operator)

Logic

OpcodeOperation
ANDLogical and (short-circuit)
ORLogical or (short-circuit)
NOTLogical not
XORLogical exclusive or

Control Flow

OpcodeEncodingDescription
JUMPpc += int16(Dst<<8|Src2)Unconditional relative jump
JUMP_FALSEJump if !IsTruthy(regs[Src1])Conditional branch (false)
JUMP_TRUEJump if IsTruthy(regs[Src1])Conditional branch (true)
RETURNReturn regs[Src1]Return from current function

Jump offsets are signed 16-bit integers encoded in Dst:Src2 fields.

Collections

OpcodeDescription
MAKE_ARRAYBuild []Value from Src2 registers starting at Src1
MAKE_MAPBuild map[string]Value from Src2 key-value pairs starting at Src1
SUBSCRIPTregs[Dst] = regs[Src1][regs[Src2]] — array/map index
SET_INDEXregs[Dst][regs[Src1]] = regs[Src2] — array/map assignment

Functions

OpcodeDescription
MAKE_FUNCCreate closure from fn.SubFunctions[Src1<<8|Src2], capture upvalues
CALLCall regs[Src1] with Src2 args; result in regs[Dst]
CALL_BUILTINCall builtin via vmBuiltinTable[Src1<<8|Src2] (dispatcher chain)
CALL_BUILTIN_DIRECTCall builtin via vmBuiltinDirectTable[Src1<<8|Src2] (direct pointer)

Exception Handling

OpcodeEncodingDescription
TRYSrc1=errReg; Dst:Src2=jump offset to catch blockPush try handler
END_TRYPop innermost try handler (no error occurred)

CALL_BUILTIN vs CALL_BUILTIN_DIRECT

Uddin-Lang has two builtin call paths:

CALL_BUILTIN — routes through the 4-layer DispatchOrPanic chain:

OP_CALL_BUILTIN → vmBuiltinTable[idx].fn → DispatchOrPanic → metadata lookup → actual function

CALL_BUILTIN_DIRECT (Phase 2B) — direct function pointer, no dispatcher:

OP_CALL_BUILTIN_DIRECT → vmBuiltinDirectTable[idx](interp, pos, args)

The compiler automatically emits CALL_BUILTIN_DIRECT for builtins with a direct implementation. Supported builtins:

len, typeof, upper, lower, trim, contains, starts_with, ends_with, split, join, int, str, append, waf_cidr_match, waf_path_match


Disassembling Bytecode

Use interpreter.Disassemble() to inspect compiled bytecode:

prog, _ := uddin.ParseProgram([]byte(source))
fn, _ := interpreter.NewCompiler().Compile(prog)
fmt.Println(interpreter.Disassemble(fn))

Example output:

function "main" regs=4 consts=2
0000 LOAD_CONST dst=0 src1=0 src2=0 ; 1
0001 LOAD_CONST dst=1 src1=0 src2=1 ; 2
0002 ADD_INT dst=2 src1=0 src2=1
0003 STORE_VAR dst=3 src1=2 src2=0
0004 RETURN dst=0 src1=3 src2=0

Engine Performance API (Phase 2A)

For embedding uddin-lang in Go services with high request throughput, the Engine struct provides compile-once, run-many execution.

Engine.ExecuteProgram with Cache

engine := uddin.New(config)

// First call: compiles AST → bytecode, stores in programCache
stats, err := engine.ExecuteProgram(prog)

// Subsequent calls with the same *Program: uses cached bytecode
stats, err = engine.ExecuteProgram(prog)

The cache key is the *Program pointer. Compile the same source into the same *Program object and reuse it across calls.

VM Pool (sync.Pool)

Engine maintains a sync.Pool of *VM instances. Each call to ExecuteProgram gets a VM from the pool, resets it (clears frame stack, rebinds interpreter), and returns it after execution. This eliminates per-request VM allocation overhead.

CompiledProgram — Low-Level API

For maximum control, compile and cache manually:

// Compile once, providing variable names for pre-allocation
varNames := []string{"request", "config"}
sort.Strings(varNames)
cp, err := interpreter.CompileProgram(prog, varNames)

// Execute with a new config on each call
stats, err := interpreter.ExecuteCompiledVM(cp, config, nil)

// Or reuse a VM across calls (advanced)
vm := interpreter.NewVM(nil)
stats, err = interpreter.ExecuteCompiledVM(cp, config, vm)

Regex Pre-compilation

String literals passed as regex patterns are compiled to *coregex.Regexp at bytecode compile time. At runtime, no string-to-regex compilation occurs:

// Pattern "^\d+$" is pre-compiled once during bytecode compilation
if regex.is_match("^\d+$", input):
...
end

This is automatic — no code changes needed.


Performance Summary

FeatureGain
Bytecode VM vs tree-walker5–15× on numeric/loop-heavy code
Engine.programCacheEliminates recompile cost on repeated calls
Engine.vmPoolEliminates per-call VM allocation
CALL_BUILTIN_DIRECT~20–30% reduction in hot builtin call overhead
Regex pre-compilationEliminates string→regex parse on every call