Skip to content
MistGoPublic

About

Pure-Lua Lua 5.1 compiler + bytecode VM for sandboxed hosts without loadstring/FFI. Single file, no dependencies.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

GoogiVM

🚀 About the Project

This project is a successful experiment in AI-accelerated "vibe-coding," built under expert human direction and backed by deep Lua knowledge.

Every file here was generated by reasoning LLM working under strict guidance. However, unlike completely unmonitored AI projects, the codebase has been partially reviewed, guided, and successfully tested by a human "expert".

🧠 The AI Synergy

  • DeepSeek V4.1 Flash 1017 (~90% of the core) Laid the entire foundation and generated a fully working prototype on the very first try. It was successfully stress-tested inside a highly restrictive in-game sandbox environment.
  • Gemini 4 Argon (Optimization & Benchmarks) Brought in selectively for deep logic optimization and benchmark calibration. While too expensive for continuous development, its incredible intelligence helped this runtime leap ahead in initial performance benchmarks.

⚠️ Disclaimer & Production Use

As an AI-generated codebase, it has been partially reviewed but has not undergone a line-by-line formal security audit, independent verification, or a long-term maintenance commitment.

  • License: Offered as-is under the MIT license, with no warranty of any kind.
  • Best For: Gaming, experimentation, and sandboxed environments.
  • Production: For critical systems, authentication boundaries, or anything handling real user data or money, traditional upstream Lua or LuaJIT remain recommended.

A pure-Lua Lua 5.1 source compiler and bytecode VM for sandboxed hosts where loadstring is unavailable.

Lua Tests License: MIT Generated by AI

custom_loadstring_full.lua is GoogiVM: a single-file Lua 5.1 compiler, peephole optimizer, and register/stack virtual machine written in 100% pure Lua. It requires zero external dependencies and never touches load, loadstring, FFI, debug, io, or os.

It is designed for game modding environments (such as Scrap Mechanic / ScrapComputers), embedded scripting engines, and restricted sandboxes that need to compile and run Lua 5.1 source strings safely and quickly.

Single-file deployment: Only custom_loadstring_full.lua is required at runtime. The tests/ directory and benchmark files are for development only.


Why GoogiVM?

Most pure-Lua interpreters (such as FiOne / LBI) are bytecode-only interpreters: to run Lua source code, you must first run a separate pure-Lua lexer/parser/compiler (LuaZ/LuaX/LuaY/LuaK/LuaU from Yueliang) to serialize Lua 5.1 bytecode, and then deserialize and interpret it in FiOne.

GoogiVM replaces that entire multi-file chain with a single integrated pipeline:

Lua 5.1 Source
  -> Streaming Lexer & Single-Pass Compiler (no AST allocation)
  -> Per-Function Prototype + Line Table
  -> Linear Peephole Superinstruction Optimizer (Opcodes 52-208)
  -> Explicit-Frame Stack/Register VM (Balanced Opcode Dispatch Tree, Frame Pooling, Tail-Call Optimization)

Key Features

  • Complete Lua 5.1 Language Support:
    • Control flow: if/elseif/else, while, repeat/until, numeric for, generic for, break, do/end, return.
    • Functions & closures: lexical scoping, mutable upvalues (heap-cell backpatching), varargs (...), method syntax (obj:method()), and frame-reusing tail calls (return f(...)).
    • Tables & metamethods: array/hash/nested constructors, multi-return tail expansion ({a, f()}), and full host metamethod support (__index, __newindex, __add..__pow, __eq, __lt, __le, __concat, __unm, __call, __len).
    • Environments: isolated per-chunk env, plus sandboxed getfenv/setfenv support for interpreted closures.
  • Built-in Sandbox Quotas & Safety:
    • Configurable compile-time limits (maxSourceBytes, maxCodeSlots) and runtime budgets (maxInstructions, maxCallDepth, maxStack, maxArgs, maxReturns).
    • Blocks getfenv/setfenv on native C/host functions and setfenv(0, ...), including when they are reached through guest pcall/xpcall, so untrusted code cannot escape into the host _G.
  • Source-Line Tracebacks:
    • Syntax errors report exact line numbers and nearby tokens.
    • Runtime errors are rewritten to sourceName:line: message with a Lua-style stack traceback: across interpreted frames, including function names, main chunk, anonymous <source:line> labels and synthetic frames for inlined leaf functions.
    • Guest pcall/xpcall around interpreted closures keep the same sandbox format: host file paths and internal VM line numbers are never exposed.
  • Bounded Stack for Tail Calls:
    • return f(...) reuses the current frame and its original stack region, so even millions of tail calls stay within a constant maxStack footprint.
  • Superinstruction Peephole Optimizer (131 Fused Opcodes + Leaf Inlining):
    • Fuses local moves, constant loads, binary arithmetic, compound assignments (s = s + i, s = s + t[i], s = (s + i) % k, t[i % k] = v), multiple-assignment swaps (a, b = b, a), table reads/writes (t[k] = v, t.field = v), short-circuit comparisons (and/or), conditional branches (if a < b, if x % d == 0), numeric for bodies (including s = s * k, s = s % k and s = s .. k), per-iteration closures, table constructors, and ipairs/pairs generic for loops that run the iterator and the loop step inside one dispatch (TFOR_1/TFOR_2) instead of a dispatch-heavy sequence.
    • Opcode families share a single opcode with a kind stored in a dead slot, so the dispatch tree stays small: all twelve comparison branches, all seven compound assignments, and the whole and/or test family each cost one opcode instead of six to eighteen.

Quick Start

Copy custom_loadstring_full.lua into your project and load it:

local custom_loadstring = dofile("custom_loadstring_full.lua")

local env = {
    ipairs = ipairs,
    pairs = pairs,
    next = next,
    select = select,
    tostring = tostring,
    tonumber = tonumber,
    type = type,
    pcall = pcall,
    xpcall = xpcall,
    assert = assert,
    error = error,
    math = math,
    string = string,
    table = table,
}

local chunk, err = custom_loadstring([[
    local out = {}
    for i = 1, 5 do
        out[i] = i * i
    end
    return table.concat(out, ", ")
]], env, {
    sourceName = "example.lua",
    maxInstructions = 1000000,
})

if not chunk then
    error("Compile error: " .. tostring(err))
end

print(chunk()) --> 1, 4, 9, 16, 25

If env is omitted (nil), GoogiVM creates a fresh empty table {} — it never exposes the host _G by default.


Public API

local chunk, err = custom_loadstring(source, env, options)
Parameter Type Description
source string Lua 5.1 source code to compile. Non-strings return nil, "script must be a string".
env table | nil Global environment table for the chunk (nil creates a new {}).
options table | nil Optional configuration and resource budgets (see below).

Return values:

  • Success: returns a callable Lua function chunk(...).
  • Failure: returns nil, err where err is a human-readable syntax or option validation error.

Options & Resource Limits

All limits are enabled by default and can be customized per chunk or disabled individually by passing math.huge:

Option Default Description
sourceName "<string>" Chunk name shown in runtime error messages and stack tracebacks.
maxSourceBytes 4,194,304 (4 MiB) Maximum allowed length of source in bytes.
maxCodeSlots 8,388,608 Maximum bytecode array numbers per compiled function prototype.
maxInstructions 50,000,000 Maximum VM instructions executed per top-level call to chunk(...).
maxCallDepth 10,000 Maximum nested interpreted call frames (tail calls reuse frames and do not count).
maxStack 1,000,000 Maximum VM register/expression stack index.
maxArgs 1,000 Maximum arguments passed to a function call.
maxReturns 1,000 Maximum return values produced by a function return or vararg expansion.

Host-dependent semantics

GoogiVM implements the Lua 5.1 language, but a few behaviors are defined by the host runtime and are inherited rather than re-implemented. They are listed below so that "Lua 5.1 compatible" is not read as "host-independent standard library":

Area Behavior
# on tables with holes Follows the host's length algorithm; it is not defined as "highest integer key".
__len Honored only when the host honors __len for tables (Lua 5.2+ do; LuaJIT in 5.1 mode and plain Lua 5.1 do not).
getfenv / setfenv Available only when the host provides them. Interpreted closures (including numeric levels) are fully supported; setfenv(0, ...) is rejected and native function environments are blocked.
Numeric formatting tostring(number) uses the host formatter.
Libraries Only what you place in env; GoogiVM never imports math, string, or table itself.
Metamethod dispatch Performed by host operators, so host-specific __eq/__lt/__le/__concat quirks apply.
Operator error text Produced by the host; GoogiVM only rewrites the location prefix to sourceName:line.
Restricted sandboxes GoogiVM needs host setmetatable and getmetatable at load time (weak caches and the __add fast path). Hosts that proxy them, such as Scrap Mechanic, must supply the proxied versions. rawget, rawset and rawequal are never used, so their absence is harmless.

If you need bit-identical behavior, run the same host version everywhere; GoogiVM does not ship a second standard library.

Performance Benchmarks

50-Case Head-to-Head Suite (BENCHMARK_50.md)

Across 50 diverse microbenchmarks comparing GoogiVM against upstream FiOne (SovereignSatellite/FiOne running precompiled luac5.1 bytecode). Every figure below is the median of 10 measured iterations per case, after 3 warm-up iterations, on the same machine:

  • 46 wins, 2 losses, 2 near-ties (within 5%).
  • 6.58x faster by total median execution time (0.7927 s vs 5.2156 s).
  • 2.78x geometric mean speedup across all 50 workloads.

Ten samples are used instead of three because small samples are outlier-sensitive: a single sample disrupted by the OS scheduler or the garbage collector can move a mean by tens of percent. Median, trimmed mean (fastest and slowest sample dropped) and plain mean agree within a few percent on this sample — 6.58x / 6.41x / 6.25x total, 2.78x / 2.87x / 2.92x geometric mean — so the result is stable. Allocation-bound cases still flip between wins and losses across repeated suite runs; the -joff game profile below is the stable target.

Absolute seconds move with host load, so they are not comparable across sessions: an earlier session measured the same code roughly 1.5x faster in absolute terms while the ratios stayed within a few percent. The ratio columns are the stable metric.

Per-category median ratios:

Category FiOne / GoogiVM Notes
Function calls 7.85x - 95.50x method_call_100k 7.85x, calls_threearg_500k 26.32x, calls_twoarg_1m 95.50x
Closures / upvalues 0.97x - 8.20x closure_upvalue_1m 8.20x, closures_create_100k 0.97x, closure_loop_capture_100k 1.49x
Recursion (incl. tail) 0.51x - 17.02x recursion_fact_10 0.51x, tail_recursion_1000 17.02x, mutual_recursion_1000 9.30x
ipairs / pairs / next 2.11x - 3.76x generic_next_100k 2.11x, generic_ipairs_50k 3.76x
Metatables 9.63x metatable_add_100k
Only non-wins 0.51x - 0.98x recursion_fact_10 0.51x, table_nested_50k 0.85x, closures_create_100k 0.97x, branch_and_or_100k 0.98x

The geometric mean of 2.78x is the honest aggregate: it is dominated by the sub-millisecond cases, which are the noisiest. The two losses and two ties are micro-timing-bound: recursion_fact_10 and table_nested_50k are only a few microseconds to a few milliseconds long, while closures_create_100k and branch_and_or_100k sit at parity. Leaf-function inlining is what lifts the call-heavy cases from single digits to double digits.

See BENCHMARK_50.md and BENCHMARK_50.csv for the per-case breakdown of the same 10-sample median.

Reproducing the suite

benchmarks/bench50_actual.lua regenerates both files. Run it from the repository root:

luajit benchmarks/bench50_actual.lua <path-to-FiOne/source.lua> <dir-with-luac5.1> [work-dir]

It needs a FiOne checkout and a Lua 5.1.5 compiler, because FiOne consumes luac5.1 bytecode. The three arguments can also come from the FIONE_PATH, LUAC_DIR and BENCH_DIR environment variables; the defaults are ./FiOne/source.lua, the current directory, and ./bench50-work.

Compilation happens outside the timer, so the numbers are execution time only. The script writes BENCHMARK_50.md, BENCHMARK_50.csv and an untracked BENCHMARK_50_raw.csv with every individual sample to the current directory. A full run takes about two and a half minutes.

A single run is a small sample, so the published figures come from 10 consecutive runs of that command (rename each BENCHMARK_50.csv to run1.csv … run10.csv), aggregated with benchmarks/median_report.lua:

luajit benchmarks/median_report.lua run1.csv … run10.csv

It takes the per-case median across runs, reports wins/losses/ties on a 5% tie band, the geometric mean, and writes the aggregated BENCHMARK_50.md and BENCHMARK_50.csv. Never run two suites at the same time — the CPU contention makes the numbers meaningless.

Note that this suite runs under LuaJIT with the JIT enabled. That is the right profile for comparing two interpreters on equal terms, but it is not the profile GoogiVM runs in inside Scrap Mechanic, where the JIT never compiles the VM's own dispatch loop. The real-world section below reports both the local -joff proxy and the actual in-game results.

Real-world suite (local and in-game)

benchmarks/benchreal.lua runs 16 full workloads adapted from The Computer Language Benchmarks Game — nbody, spectral-norm, mandelbrot, binary-trees, fannkuch-redux, pidigits, fasta, k-nucleotide, sieve, queens, matmul, quicksort, wordfreq, oop, jsonencode, hanoi. These are complete programs rather than microbenchmarks, so they are much closer to what a computer program in a mod actually does. Every case returns a value that is checked against a known expectation, so a wrong result is reported instead of silently timed.

Local reference (LuaJIT, one machine)

luajit -joff benchmarks/benchreal.lua      # local game-profile proxy
luajit benchmarks/benchreal.lua            # JIT profile

Medians of three runs, seconds:

Engine Total
Native PUC Lua 5.1 (reference only) 0.467
GoogiVM, JIT enabled 1.351
GoogiVM, -joff (local game-profile proxy) 3.862
Upstream FiOne, -joff 7.938

Against FiOne that is 5.88x with the JIT on and 2.06x with the JIT off. The -joff figure is a local proxy, not the final game number: with the JIT, GoogiVM's dispatch loop becomes machine code; with -joff every VM instruction costs a host interpreter dispatch, while FiOne — already a pure interpreter — is less affected by the flag. The actual in-game result is higher.

In-game (Scrap Mechanic, SCALE=1)

Measured inside ScrapComputers computers with the same 16-task suite; each VM ran in the same world. Times are seconds.

Suite Cases GoogiVM ScrapComputers VM SComputers VM Googi vs SC Googi vs S
16 real-world, full 16 4.063 12.096 —* 2.98x —
16 real-world, common 15 3.830 11.430 9.606 2.98x 2.51x
50 micro, full 50 2.6849 10.1501 7.6978** 3.78x ≈2.87x

* SComputers' 16-task log contains 15 tasks because oop is missing. If oop is estimated at the ScrapComputers time of 0.666 s, the full total is ≈10.27 s and the ratio is ≈2.53x; this is an estimate, not a measurement.

** SComputers' 50-case run has 49/50 timed: metatable_add_100k fails because SComputers' setmetatable returns nil instead of the table, which is a sandbox limitation rather than a VM bug. On the 49 common cases the ratio is ≈2.94x. The GoogiVM and ScrapComputers 50-case runs used SAMPLES=2; SComputers used SAMPLES=5.

ScrapComputers' wordfreq result was wrong (20050000 instead of 19050000); GoogiVM and SComputers returned the expected value.

Against ScrapComputers the 16-task split is 9 wins, 5 losses, 2 ties: GoogiVM wins spectral-norm 6.96x, hanoi 3.88x, oop 2.86x, binary-trees 2.70x, jsonencode 2.37x, fasta 1.83x, sieve 1.29x, nbody 1.24x and matmul 1.12x; mandelbrot (1.02x) and quicksort (0.98x) are ties; the remaining five are losses (fannkuch-redux 0.72x, pidigits 0.75x, k-nucleotide 0.70x, queens 0.90x, wordfreq 0.75x).

The 50-case micro suite is call-heavy: 3.78x total against ScrapComputers, but the geometric mean is about 1.7x, and without the three call-heavy cases the ratio is about 1.94x. The 16-task real-world suite is the headline game number.


ScrapComputers library benchmark

benchmarks/benchsc.lua has two halves. The first is the classic synthetic suite borrowed from the 50-case head-to-head run — naive fib, plain calls, closure creation, upvalues, varargs, tail recursion, metatable adds, table reads/writes and constructors, while/repeat, branch and and/or chains, loop break, multi-assignment, concatenation, ipairs, pairs, prime counting — so a single run measures the interpreter itself. The second half measures the heavy pure-Lua libraries that ship with ScrapComputers and that computer programs actually call: AES-256 (CBC and ECB), SHA-256, MD5, Base64, Base91, LZ4, JSON encoding, QR-Code generation, the syntax highlighter, bit streams and vector maths. Everything runs inside the computer's own VM, so the times include interpreter overhead.

Paste it into a computer's Main.lua and run it. Each library is looked up as a plain global (aes256), under the documented sc.* namespace (sc.aes256), or in the addon registry (sm.scrapcomputers.aes256), so the same file also works in other computer mods that expose the ScrapComputers addon API. Libraries that the environment does not expose are skipped, so the file also runs on a plain Lua host as a harness smoke test. The SCALE constant at the top multiplies every workload size.

Architecture Overview

See TECHNICAL.md for a deep dive into the compiler, bytecode format, and VM internals.

  1. Lexer & Single-Pass Parser:
    • Scans tokens on demand (tk, tv, tp, pos) without building a token list or AST.
    • Tracks newline offsets in line_starts and resolves source lines via binary search (line_of).
  2. hybrid Stack + Register Bytecode:
    • Locals live in fixed frame-relative registers S[base + slot - 1].
    • Temporary expression values use the top of S above base + maxreg - 1.
    • Captured locals are automatically promoted to heap cells ({ v = val }) by backpatching NOP -> CELL and GETLOCAL/SETLOCAL -> GETCELL/SETCELL.
  3. Peephole Superinstruction Optimizer (optimize_code):
    • Scans jump targets first so it never fuses across a branch destination.
    • Replaces multi-instruction sequences in place (marking trailing slots dead and advancing pc by 4 * len).
    • Fuses loop control (FORLOOP_MOV, FORLOOP_CELL) with hot loop bodies (FORLOOP_ADD_RR/RK, FORLOOP_SUB_RR/RK, FORLOOP_ASSIGN_RR/RK, FORLOOP_ADD_INDEX, FORLOOP_SETINDEX_RR/RK, FORLOOP_MOD_RR_EQ_K, FORLOOP_NEWTABLE_SETINDEX_R, FORLOOP_SETTRUE).
    • Fuses direct calls (CALL_U_RR2/RR1, CALL_R_RR2/RR1, INDEX_ADD_RK_CALL_U_RR2), arithmetic chains (ADD_RR_ADD_K, MUL_DIV_K, ADD_R_ADD_K, DIV_RET, ASSIGN_MUL3_ADD, ASSIGN_SUB_ADD, MUL_ADD_SETLOCAL, MUL_ADD_SETLOCAL_FORLOOP, DIV_MUL_ADD_SETLOCAL_FORLOOP, DIV_K, DIV_K_MUL_ADD_SETLOCAL_FORLOOP), indexed updates (SETINDEX_MUL3, SETINDEX_MUL2, INDEX_ADD_RK, INDEX_INDEX_RR), indexed comparisons (INDEX_CMP_JMPF, ADD_JMPF_K) and loop back-edges (JMPT_CMP_*).
    • Inlines small leaf functions (no upvalues, no branches or calls, one arithmetic return) directly at the call site; the caller keeps a real frame for inlined error tracebacks.
    • Fuses generic for loops into TFOR_1 and TFOR_2 with direct in-VM ipairs and next iteration.
  4. Explicit Call Frames & Frame Pool:
    • Custom-to-custom function calls (CALLD) push a lightweight frame onto frames[] (reusing pooled frame tables from free_frames) and jump to pc = proto.entry inside the same while true loop - avoiding host Lua C-stack recursion.
    • Tail calls (return f(...)) overwrite the current frame in place.

Running the Test Suite

Run from the repository root with LuaJIT (primary target) or Lua 5.4. The suite targets Lua 5.1 semantics; other 5.x hosts are untested:

luajit run_all_tests.lua
lua run_all_tests.lua

Expected output:

  • LuaJIT 2.1.1720049189: TOTAL: 18737 checks, 0 failed
  • Lua 5.4.6: TOTAL: 18713 checks, 0 failed

See tests/README.md for a description of all 17 test modules.


Repository Layout

custom_loadstring_full.lua   # GoogiVM (compiler, optimizer, and VM in one file)
README.md                    # Project overview, API, and benchmark summary
TECHNICAL.md                 # Internal architecture and opcode reference
BENCHMARK_50.md              # 50-case benchmark report (GoogiVM vs FiOne)
BENCHMARK_50.csv             # Per-case medians, bests and repetition counts
benchmarks/                  # Benchmark harnesses (50-case, real-world, ScrapComputers, median aggregator)
run_all_tests.lua            # Automated test runner
tests/                       # 17 test modules + test suite documentation
AGENTS.md                    # Guidelines and invariants for AI coding agents
LICENSE                      # MIT License

License

MIT License. See LICENSE.

About

Pure-Lua Lua 5.1 compiler + bytecode VM for sandboxed hosts without loadstring/FFI. Single file, no dependencies.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages