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".
- 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.
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.
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.luais required at runtime. Thetests/directory and benchmark files are for development only.
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)
- Complete Lua 5.1 Language Support:
- Control flow:
if/elseif/else,while,repeat/until, numericfor, genericfor,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 sandboxedgetfenv/setfenvsupport for interpreted closures.
- Control flow:
- Built-in Sandbox Quotas & Safety:
- Configurable compile-time limits (
maxSourceBytes,maxCodeSlots) and runtime budgets (maxInstructions,maxCallDepth,maxStack,maxArgs,maxReturns). - Blocks
getfenv/setfenvon native C/host functions andsetfenv(0, ...), including when they are reached through guestpcall/xpcall, so untrusted code cannot escape into the host_G.
- Configurable compile-time limits (
- Source-Line Tracebacks:
- Syntax errors report exact line numbers and nearby tokens.
- Runtime errors are rewritten to
sourceName:line: messagewith a Lua-stylestack traceback:across interpreted frames, including function names,main chunk, anonymous<source:line>labels and synthetic frames for inlined leaf functions. - Guest
pcall/xpcallaround 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 constantmaxStackfootprint.
- 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), numericforbodies (includings = s * k,s = s % kands = s .. k), per-iteration closures, table constructors, andipairs/pairsgenericforloops 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/ortest family each cost one opcode instead of six to eighteen.
- Fuses local moves, constant loads, binary arithmetic, compound assignments (
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, 25If env is omitted (nil), GoogiVM creates a fresh empty table {} — it never exposes the host _G by default.
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, errwhereerris a human-readable syntax or option validation error.
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. |
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.
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 svs5.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.
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.csvIt 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.
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.
luajit -joff benchmarks/benchreal.lua # local game-profile proxy
luajit benchmarks/benchreal.lua # JIT profileMedians 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.
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.
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.
See TECHNICAL.md for a deep dive into the compiler, bytecode format, and VM internals.
- Lexer & Single-Pass Parser:
- Scans tokens on demand (
tk,tv,tp,pos) without building a token list or AST. - Tracks newline offsets in
line_startsand resolves source lines via binary search (line_of).
- Scans tokens on demand (
- hybrid Stack + Register Bytecode:
- Locals live in fixed frame-relative registers
S[base + slot - 1]. - Temporary expression values use the top of
Sabovebase + maxreg - 1. - Captured locals are automatically promoted to heap cells (
{ v = val }) by backpatchingNOP -> CELLandGETLOCAL/SETLOCAL -> GETCELL/SETCELL.
- Locals live in fixed frame-relative registers
- 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
pcby4 * 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
forloops intoTFOR_1andTFOR_2with direct in-VMipairsandnextiteration.
- Explicit Call Frames & Frame Pool:
- Custom-to-custom function calls (
CALLD) push a lightweight frame ontoframes[](reusing pooled frame tables fromfree_frames) and jump topc = proto.entryinside the samewhile trueloop - avoiding host Lua C-stack recursion. - Tail calls (
return f(...)) overwrite the current frame in place.
- Custom-to-custom function calls (
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.luaExpected 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.
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
MIT License. See LICENSE.