Documentation
Design and reference documentation for rilua, a Lua 5.1.1 interpreter in Rust.
Foundation
- architecture.md – Design principles, module structure, key decisions
- use-cases.md – WoW ecosystem and general embedding use cases
- references.md – Studied implementations and what we learned from each
API
- api.md – Public API: Lua struct, IntoLua/FromLua, handle types, embedding examples
- future-api.md – Planned API enhancements: closure-based functions, UserData trait, container conversions
Implementation
- features.md – Feature coverage and compatibility notes
- stdlib.md – All 9 standard libraries, function lists, implementation notes
- wasm.md – WebAssembly target: library availability, platform stubs, building for the browser
Quality
- testing.md – Unit tests, integration tests, PUC-Rio suite, behavioral equivalence
- performance.md – Profiling, benchmarks, regression gate, optimization history
Architecture Overview
rilua is a from-scratch implementation of Lua 5.1.1 in Rust. The goal is behavioral equivalence with the PUC-Rio reference interpreter — executed Lua code must produce identical results. Internal architecture is free to diverge where Rust idioms offer better safety, clarity, or modularity.
Design Principles
- Behavioral equivalence — Lua code produces the same results as PUC-Rio Lua 5.1.1. Observable behavior (output, errors, GC API returns, weak table clearing, finalizer execution) must match.
- Idiomatic Rust — Use Rust’s type system, ownership, and error
handling. Minimize unsafe code. Prefer enums over tagged unions,
Resultover longjmp, traits over function pointers. - Zero external dependencies — Only Rust’s standard library. All data structures, algorithms, and patterns are self-contained.
- Modular design — Clear module boundaries. The compiler does not depend on the VM. The GC does not depend on the compiler. Standard library functions are isolated from core VM logic.
- Spec-driven testing — Test against the Lua 5.1.1 specification and PUC-Rio’s official test suite. Unit tests for internals, integration tests for language semantics.
Pipeline
Source Code
|
v
[Lexer] src/compiler/lexer.rs
| tokens
v
[Parser] src/compiler/parser.rs
| AST
v
[Compiler] src/compiler/codegen.rs
| Proto (bytecode + constants + nested protos)
v
[VM] src/vm/
| execution
v
Output / Side Effects
Unlike PUC-Rio’s single-pass compiler that emits bytecode during parsing, rilua uses an explicit AST intermediate representation. This follows the approach used by Luau (Roblox’s Lua 5.1-compatible scripting language).
Benefits of the AST phase:
- Separation between parsing and code generation
- Each phase is independently testable
- Future optimizations (constant folding, dead code elimination) can operate on the AST without modifying the parser
- Easier to understand and debug than interleaved parse-and-emit
Module Structure
src/
lib.rs Public API (Lua struct, traits, types)
error.rs Error types (syntax, runtime, argument)
conversion.rs IntoLua/FromLua trait implementations
handles.rs Table/Function/Thread/AnyUserData handle types
platform.rs Centralized FFI declarations (raw extern "C")
bin/
rilua.rs Standalone interpreter (matches lua.c)
riluac.rs Bytecode compiler/lister (matches luac)
compiler/
mod.rs Compiler module root
lexer.rs Tokenizer (source -> tokens)
token.rs Token types
parser.rs Parser (tokens -> AST)
ast.rs AST node types
codegen.rs Code generator (AST -> Proto)
vm/
mod.rs VM module root
state.rs Lua state (the main VM struct)
execute.rs Instruction dispatch loop
instructions.rs Opcode definitions (Rust enums)
proto.rs Function prototype (bytecode container)
value.rs Value representation (Val enum)
gc/
mod.rs GC module root
arena.rs Generational arena (typed Vec storage)
collector.rs Mark-sweep collector
trace.rs Trace trait for marking reachable objects
table.rs Table implementation (array + hash parts)
string.rs String interning
closure.rs Closures and upvalues
callinfo.rs Call stack (CallInfo chain)
metatable.rs Metamethod dispatch
debug_info.rs Debug info and variable name resolution
dump.rs Binary chunk serialization (string.dump)
undump.rs Binary chunk deserialization (loadstring)
listing.rs Bytecode listing (riluac -l output)
stdlib/
mod.rs Standard library registration
base.rs Base library (print, assert, type, etc.)
coroutine.rs Coroutine library
string.rs String library
table.rs Table library
math.rs Math library
io.rs I/O library
os.rs OS library
debug.rs Debug library
package.rs Package/module library
testlib.rs T test module (PUC-Rio ltests.c equivalent)
Key Architectural Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Compilation pipeline | Lexer -> Parser -> AST -> Compiler | Separation of concerns, testability |
| Instruction set | PUC-Rio’s 38 opcodes as Rust enums | Behavioral equivalence, type safety |
| Value representation | Rust enum (Val) | Type safety, pattern matching |
| Garbage collection | Arena with generational indices | Zero unsafe, mark-sweep |
| Tables | Array + hash dual representation | Performance, PUC-Rio compatibility |
| Strings | Interned with cached hash | Pointer equality, O(1) comparison |
| Closures and upvalues | Open/closed upvalue model | PUC-Rio semantics |
| Error handling | Result-based | Idiomatic Rust, no longjmp |
| Public API | Trait-based, Rust-idiomatic | Ergonomic embedding (api.md) |
| Standard library | Modular, per-library files | Independent testing, optional loading (stdlib.md) |
| Call stack | Dynamic CallInfo array | Separate from value stack, index-based |
| Metatables | PUC-Rio 5.1.1 dispatch semantics | 17 metamethods, type coercion rules |
| Coroutines | Threads with shared GC heap | Independent stacks, cooperative multithreading |
| Testing strategy | Spec-driven, multi-layer | Correctness assurance (testing.md) |
| Platform abstraction | Centralized FFI with WASM stubs | Cross-platform without conditional code in consumers (wasm.md) |
Platform Support
All C FFI is centralized in src/platform.rs. See wasm.md
for WASM-specific stubs and library availability.
Supported targets:
| Target | Status | Notes |
|---|---|---|
x86_64-unknown-linux-gnu | Full | Primary development platform |
x86_64-apple-darwin / aarch64-apple-darwin | Full | macOS (Intel + Apple Silicon) |
x86_64-pc-windows-msvc | Full | MSVC toolchain, links ucrt |
wasm32-unknown-unknown | Core | No I/O/OS; see wasm.md |
Reference Implementations
See references.md for a classification of all studied implementations and what we learned from each.
Use Cases
Overview
rilua targets two audiences: the World of Warcraft emulation community and Rust developers who need an embeddable Lua 5.1.1 interpreter. The WoW use cases drive the project’s priorities. General embedding is a secondary benefit of the architecture.
Primary: World of Warcraft
Addon development and testing
Run and test WoW addons outside the game client. Addon authors can validate Lua logic, check for errors, and iterate without launching WoW. This requires:
- Lua 5.1.1 behavioral equivalence (the client’s Lua version)
- WoW-specific global aliases (
strtrim,strsplit,strjoin,wipe,tinsert,tremove, degree-based trig functions) - WoW-specific string.format positional arguments (
%2$d) - TOC file parsing for addon manifest loading
- Stub implementations of the WoW API (frame system, events, CVars)
The last two items are out of scope for rilua itself but enabled by the embedding API. A separate WoW environment layer (analogous to wowbench) can be built on top.
Server-side scripting
Private server emulators (CMaNGOS, TrinityCore, AzerothCore) embed a Lua interpreter for scripted content: boss encounters, quests, NPC behavior, world events. rilua can serve as an alternative to the bundled Lua or Eluna scripting engines. This requires:
- The embedding API (
Lua::new(),register_function,UserData) - Reliable error handling (scripts must not crash the server)
- Controlled GC (server processes are long-lived)
Client Lua environment emulation
Reproduce the WoW client’s Lua sandbox for compatibility testing and emulation research. The WoW client runs a modified Lua 5.1.1 with:
- Restricted stdlib: no
iolibrary, noos.execute, no binary module loading, limiteddebuglibrary - Taint system: tracks whether code originated from Blizzard (“secure”) or addons (“insecure”). Tainted code cannot perform protected actions (casting spells, using items). Taint propagates through reads, writes, and function calls.
- Modified GC defaults:
gcpause=110(more aggressive than the standard 200),maxcstack=4096(doubled from standard 2048) - Removed Lua 5.0 compatibility: no
LUA_COMPAT_VARARG,LUA_COMPAT_MOD,LUA_COMPAT_LSTR,LUA_COMPAT_GFIND - Bitwise operations library:
bit.band,bit.bor,bit.bxor,bit.bnot,bit.lshift,bit.rshift,bit.arshift,bit.mod(32-bit integer operations) - UTF-8 BOM handling: automatically strips byte order marks
The taint system is a significant extension. It requires per-object and per-value taint tracking with propagation modes (read, write, both, disabled). This is a post-1.0 feature, documented separately in elune’s implementation.
Addon compatibility testing
Automated verification that addons behave correctly. A test harness loads addons via their TOC manifests and runs them against a mock WoW environment, checking for:
- Runtime errors
- Taint violations
- Correct event handling
- Saved variable serialization
This is the wowbench use case. rilua provides the interpreter; the test harness and WoW API stubs are a separate layer.
Secondary: General Lua 5.1.1
Embedded scripting for Rust applications
The trait-based API (IntoLua/FromLua, UserData, Function)
makes rilua usable as a general-purpose embedded scripting language
for Rust programs. Use cases include:
- Game engines (configuration, modding, entity scripting)
- Command-line tools with user-configurable behavior
- Applications that need a sandboxed extension language
- No transitive dependencies (see architecture.md)
Lua 5.1.1 conformance reference
The PUC-Rio C source (lua-5.1.1) is the authoritative
implementation but difficult to read. rilua provides an alternative
reference implementation in Rust with:
- Named types and enums instead of tagged unions and macros
- Explicit control flow instead of longjmp-based error handling
- Pattern matching instead of switch-case fallthrough
- Module boundaries instead of translation-unit scoping
Useful for anyone studying Lua internals or building their own implementation.
Educational use
rilua demonstrates language implementation techniques in a real project:
- Lexing and tokenization
- Recursive descent parsing with Pratt operator precedence
- AST-based compilation to register bytecode
- Register allocation and jump backpatching
- Mark-sweep garbage collection with weak references
- Closure capture with open/closed upvalue conversion
- Coroutines via separate thread stacks
The design documentation in docs/ covers each subsystem with
algorithms sourced from PUC-Rio’s C code and translated into Rust
patterns.
Out of Scope
These are explicitly not goals:
- General-purpose Lua replacement: Luau, LuaJIT, and PUC-Rio 5.4 serve this better
- Performance-critical production scripting: no JIT, no optimized GC; if throughput matters, use LuaJIT
- Lua 5.2+ features:
goto, integer subtype, bitwise operators (native), generalizedfor,_ENV– all out of scope - Binary compatibility with PUC-Rio: the C ABI,
lua_State*layout, and bytecode format are intentionally different
Reference Implementations
Classification of implementations studied during architecture design. Each was examined for its approach to compilation, VM design, memory management, and API surface.
Tier 1 — Primary References
PUC-Rio Lua 5.1.1 (C)
- Source:
./lua-5.1.1/(vendored in repo; seeAGENTS.mdfor setup) - Role: The specification. All behavioral questions are answered here.
- Architecture: 17,654 lines. Single-pass recursive descent compiler emitting 38 register-based opcodes (u32 packed). CallInfo chain for call stack. Incremental tri-color mark-sweep GC with write barriers. Array+hash tables. Interned strings with cached hash. longjmp/setjmp error handling. Stack-based C API.
- What we take: Opcode semantics, GC behavioral contract, weak table
semantics, finalizer protocol,
collectgarbage()API, standard library behavior, error messages. - What we change: Single-pass compilation (we use AST), longjmp (we use Result), C API (we use traits), raw pointers (we use arenas).
Luau (C++)
- GitHub: https://github.com/luau-lang/luau
- Role: Architecture reference. Lua 5.1-compatible scripting language.
- Architecture: Lexer -> Parser -> AST -> Compiler -> VM. 83 register-based opcodes (count evolves with development). Incremental tri-color GC. CallInfo chain. Array+hash tables. String interning. No longjmp (C++ exceptions).
- What we take: AST-based pipeline design, separation of compiler phases, Proto as non-GC value (owned by closures), CallInfo chain pattern, incremental GC debt model.
- What we change: Opcode count (we use PUC-Rio’s 38, not Luau’s 83), language extensions (we implement standard Lua 5.1.1 only), native codegen (out of scope).
Tier 2 — Selective References
tsuki (Rust, Lua 5.4)
- GitHub: https://github.com/nickmass/tsuki
- Role: Proves Result-based error handling works for a Lua VM.
- Architecture: c2rust translation of Lua 5.4. PUC-Rio file naming (llex.rs, lparser.rs, lcode.rs). 83 opcodes. Incremental GC via raw pointers. Heavy use of unsafe code. Memory-safe public API over unsafe internals.
- What we take: Result-based error propagation pattern, memory-safe public API design, packed u32 instruction format for serialization.
- What we avoid: c2rust code style, heavy unsafe, wrong Lua version (5.4 vs 5.1.1).
full-moon (Rust, parser only)
- GitHub: https://github.com/Kampfkarren/full-moon
- Role: Best Rust patterns for Lua parsing.
- Architecture: Hand-written recursive descent parser. Lossless AST preserving whitespace and comments. Error recovery with partial AST. Multi-dialect support (5.1-5.4, Luau) via feature flags. Sealed traits, visitor pattern, builder pattern.
- What we take: Recursive descent parsing patterns, AST node design idioms, error recovery approach, sealed trait pattern.
- What we avoid: Lossless parsing (unnecessary for a VM), multi-dialect support (we only target 5.1.1), external dependencies.
mlua (Rust, FFI wrapper)
- GitHub: https://github.com/mlua-rs/mlua
- Role: API design reference for embedding Lua in Rust.
- Architecture: FFI wrapper around C Lua. Memory-safe Rust API over
unsafe C bindings. Trait-based type conversions (
IntoLua,FromLua). Registry-based lifetime management. Scoped values. UserData trait. - What we take: Trait-based API design (
IntoLua/FromLuapattern), UserData trait approach, scope-based lifetime management, error type design. - Not applicable: FFI wrapping (we implement from scratch), C Lua compilation, vendored builds.
lua-rs / CppCXY (Rust, Lua 5.5)
- GitHub: https://github.com/CppCXY/lua-rs
- Role: Architecture comparison. Full Lua 5.5 port to Rust with pointer-based VM design.
- Architecture: 64k lines across workspace (luars, luars-derive, luars_interpreter, luars_wasm). Faithful port of C Lua’s architecture: pointer-based VM dispatch, 86 Lua 5.5 opcodes, tri-color incremental + generational GC, 16-byte TValue (union + type tag), Brent’s collision tables, interned strings, pointer-based upvalues. 399 unsafe blocks. Multi-crate workspace with proc-macro derive crate. 28/30 official Lua 5.5 tests pass. External dependencies: ahash, rand, chrono, itoa, smol_str, syn/quote (macros).
- What we can study: Async/await coroutine bridging (novel feature), proc-macro derive for UserData, generational GC implementation, Lua 5.5 opcode set, lightweight 1-byte error enum with message stored in VM, platform abstraction layer design, WASM support approach.
- Key differences from rilua: Targets Lua 5.5 (not 5.1.1), uses raw pointers and union types (C-style, 399 unsafe blocks vs rilua’s arena-based zero-unsafe GC), has external dependencies (ahash, rand, chrono, smol_str vs rilua’s zero-dependency policy), pointer-based upvalues (vs rilua’s arena indices), larger scope (64k LOC vs rilua’s ~17k), proc-macro crate for ergonomic API.
Tier 3 — Limited Relevance
lua-in-rust (Rust, Lua 5.1.1)
- GitHub: https://github.com/cjneidhart/lua-in-rust
- Role: Previous base for rilua. Studied for lessons learned.
- Architecture: Single-pass compiler, stack-based VM (~40 custom opcodes), stop-the-world mark-sweep GC with raw pointers, string interning, HashMap-only tables. ~5,000 lines. ~40% complete.
- Lessons: Stack-based VM diverges too far from PUC-Rio’s register-based design. Single-pass compilation makes the compiler hard to test and extend. Raw pointer GC works but is fragile. String interning with pointer equality is the right approach.
lua-rs / lonng (Rust, compiler only)
- GitHub: https://github.com/lonng/lua-rs
- Role: Shows AST-to-bytecode compilation for Lua 5.1.
- Architecture: Multi-pass with explicit AST. Scanner -> Parser -> AST -> Compiler -> FunctionProto. No VM. Broken build. Zero unsafe.
- Useful for: AST node design, constant folding patterns, debug info tracking. Not useful for VM or runtime design.
lua-rs (Rust, full interpreter, CppCXY)
- Local path:
~/Repos/github.com/CppCXY/lua-rs - GitHub: https://github.com/CppCXY/lua-rs
- Role: Performance comparison target. One-to-one C Lua 5.5 port.
- Architecture: Direct port of PUC-Rio’s C source to Rust. Same pipeline (Lexer -> Parser -> Code Generator -> Bytecode -> VM), 86 opcodes, tri-color incremental/generational GC, string interning. ~60K lines, ~400 unsafe blocks for GC pointer ops. 5 runtime deps (ahash, rand, chrono, itoa, smol_str). Requires nightly Rust.
- Useful for: Benchmarking (runs ~1.2x PUC-Rio on micro-benchmarks), comparing pure-Rust Lua implementation approaches and the safety-vs-performance trade-off.
- Differences: Targets Lua 5.5 (not 5.1.1), uses
unsafeextensively, requires nightly. Seedocs/src/comparison.md.
coppermoon (Rust, mlua wrapper)
- GitHub: https://github.com/coppermoondev/coppermoon
- Role: None for VM/compiler work.
- Architecture: Node.js-like runtime wrapping mlua (C Lua 5.4 FFI). Not a from-scratch implementation. Provides batteries-included stdlib (HTTP, SQLite, WebSocket, etc.) over the C VM.
- Not applicable: Everything. This wraps C Lua, not implements it.
hematita (Rust, Lua 5.3)
- GitHub: https://github.com/danii/hematita
- Role: Reference for hardened Lua interpreter patterns in Rust.
- Architecture: From-scratch Lua interpreter in Rust targeting 5.3. Focuses on safety and correctness. Useful for comparing implementation approaches for parsing, value representation, and standard library.
- What we take: Comparison point for implementation patterns and edge case handling.
- Differences: Targets Lua 5.3 (not 5.1.1), different design goals.
Public API
Decision
Rust-idiomatic, trait-based API inspired by mlua’s design. Not a 1:1 mirror of the Lua C API.
Overview
PUC-Rio Lua exposes a stack-based C API where values are pushed and popped from a virtual stack. This works well in C but is unergonomic in Rust – it lacks type safety, requires manual stack management, and does not leverage Rust’s trait system. rilua uses traits for type conversion, methods for common operations, and the type system for safety instead.
See future-api.md for planned ergonomic improvements.
Core Type: Lua
/// A Lua interpreter instance.
///
/// Owns the full VM state including value stack, call stack, GC heap,
/// and global table. All interaction with Lua values goes through this
/// struct.
pub struct Lua {
// Internal state (not public)
}
impl Lua {
/// Create a new Lua state with all standard libraries loaded.
pub fn new() -> LuaResult<Self> { ... }
/// Create a new Lua state without any libraries.
pub fn new_empty() -> Self { ... }
/// Create a new Lua state with selected standard libraries.
///
/// ```ignore
/// let lua = Lua::new_with(StdLib::BASE | StdLib::STRING)?;
/// ```
pub fn new_with(libs: StdLib) -> LuaResult<Self> { ... }
// -- Execution --
/// Compile and execute a Lua source string.
/// Chunk name is set to `"=(string)"`.
pub fn exec(&mut self, source: &str) -> LuaResult<()> { ... }
/// Compile and execute Lua source bytes with a given chunk name.
/// Source is `&[u8]` because Lua files may contain arbitrary bytes.
pub fn exec_bytes(
&mut self, source: &[u8], name: &str,
) -> LuaResult<()> { ... }
/// Read a file and execute its contents as a Lua chunk.
/// Chunk name is set to `@<path>`.
pub fn exec_file(&mut self, path: &str) -> LuaResult<()> { ... }
/// Compile a Lua source string and return a function handle.
/// Chunk name is set to `"=(string)"`.
pub fn load(&mut self, source: &str) -> LuaResult<Function> { ... }
/// Compile Lua source bytes (or load a binary chunk) and return
/// a function handle.
pub fn load_bytes(
&mut self, source: &[u8], name: &str,
) -> LuaResult<Function> { ... }
/// Read a file (or stdin if `None`) and compile it, returning a
/// function handle. Handles shebang lines in executable scripts.
pub fn load_file(
&mut self, path: Option<&str>,
) -> LuaResult<Function> { ... }
// -- Globals --
/// Get a global variable, converting via FromLua.
/// Takes `&mut self` because looking up a string key may intern
/// the name.
pub fn global<V: FromLua>(
&mut self, name: &str,
) -> LuaResult<V> { ... }
/// Set a global variable from a Rust value, converting via IntoLua.
pub fn set_global<V: IntoLua>(
&mut self, name: &str, value: V,
) -> LuaResult<()> { ... }
// -- Object creation --
/// Allocate a new empty table and return a handle.
pub fn create_table(&mut self) -> Table { ... }
/// Intern a byte string via the GC string table.
pub fn create_string(&mut self, s: &[u8]) -> Val { ... }
/// Register a Rust function as a global Lua function.
/// See [Implementing Native Functions](#implementing-native-functions)
/// for the `RustFn` signature and usage.
pub fn register_function(
&mut self, name: &str, func: RustFn,
) -> LuaResult<()> { ... }
// -- Userdata creation --
/// Create a new userdata containing `data` with no metatable.
pub fn create_userdata<T: Any>(&mut self, data: T) -> AnyUserData { ... }
/// Create a new userdata with a named, registry-cached metatable.
/// The metatable is stored in the registry under `type_name` and
/// reused for subsequent calls with the same name.
pub fn create_typed_userdata<T: Any>(
&mut self, data: T, type_name: &str,
) -> LuaResult<AnyUserData> { ... }
/// Create or retrieve a named metatable for a userdata type.
pub fn create_userdata_metatable(
&mut self, type_name: &str,
) -> LuaResult<Table> { ... }
// -- Calling functions --
/// Call a loaded Function handle with arguments and collect results.
///
/// Arguments and results are raw `Val` values. For type-safe
/// conversions, use `IntoLua`/`FromLua` on individual values.
pub fn call_function(
&mut self, func: &Function, args: &[Val],
) -> LuaResult<Vec<Val>> { ... }
/// Call a loaded Function handle, appending a stack traceback
/// on error. Used by the CLI to match PUC-Rio's `docall` pattern.
pub fn call_function_traced(
&mut self, func: &Function, args: &[Val],
) -> LuaResult<Vec<Val>> { ... }
// -- Table operations --
/// Raw set on a table handle (no metamethod dispatch).
pub fn table_raw_set(
&mut self, table: &Table, key: Val, value: Val,
) -> LuaResult<()> { ... }
// -- GC control --
/// Run a full garbage collection cycle.
pub fn gc_collect(&mut self) -> LuaResult<()> { ... }
/// Get current memory usage in bytes.
pub fn gc_count(&self) -> usize { ... }
/// Stop automatic garbage collection.
pub fn gc_stop(&mut self) { ... }
/// Restart automatic garbage collection.
pub fn gc_restart(&mut self) { ... }
/// Perform an incremental GC step. Returns true if a cycle completed.
pub fn gc_step(&mut self, step_size: i64) -> LuaResult<bool> { ... }
/// Set GC pause parameter (percentage). Returns the previous value.
pub fn gc_set_pause(&mut self, pause: u32) -> u32 { ... }
/// Set GC step multiplier. Returns the previous value.
pub fn gc_set_step_multiplier(&mut self, stepmul: u32) -> u32 { ... }
}
Selective Library Loading
bitflags! {
pub struct StdLib: u16 {
const BASE = 0x0001;
const PACKAGE = 0x0002;
const TABLE = 0x0004;
const IO = 0x0008;
const OS = 0x0010;
const STRING = 0x0020;
const MATH = 0x0040;
const DEBUG = 0x0080;
const COROUTINE = 0x0100;
const ALL = 0x01FF;
}
}
Use Lua::new_with(StdLib::BASE | StdLib::STRING) to load only
selected libraries. This enables sandboxing by excluding dangerous
libraries (IO, OS, debug).
Conversion Traits
IntoLua / FromLua
/// Convert a Rust value into a Lua value.
pub trait IntoLua {
fn into_lua(self, lua: &mut Lua) -> LuaResult<Val>;
}
/// Convert a Lua value into a Rust value.
pub trait FromLua: Sized {
fn from_lua(val: Val, lua: &Lua) -> LuaResult<Self>;
}
Implemented for:
| Rust Type | Lua Type | Direction |
|---|---|---|
() | nil | both |
bool | boolean | both |
f64, f32 | number | both |
i8..i64, u8..u64 | number (with range check) | both |
String | string | both |
&str | string | IntoLua only |
&[u8] | string | IntoLua only |
Vec<u8> | string (raw bytes) | FromLua only |
Val | (any) | both (passthrough) |
Table | table | both |
Function | function | both |
Thread | thread | both |
AnyUserData | userdata | both |
IntoLuaMulti / FromLuaMulti
For functions with multiple arguments or return values:
pub trait IntoLuaMulti {
fn into_lua_multi(self, lua: &mut Lua) -> LuaResult<Vec<Val>>;
}
pub trait FromLuaMulti: Sized {
fn from_lua_multi(values: &[Val], lua: &Lua) -> LuaResult<Self>;
}
Implemented for Vec<Val> (variable-length) and () (zero values).
Handle Types
Table
pub struct Table(/* GcRef<table::Table> */);
impl Table {
/// Get a value by key (raw, no metamethods).
pub fn raw_get(
&self, state: &LuaState, key: Val,
) -> LuaResult<Val> { ... }
/// Set a value by key (raw, no metamethods).
pub fn raw_set(
&self, state: &mut LuaState, key: Val, value: Val,
) -> LuaResult<()> { ... }
/// Raw length (no `__len` metamethod).
pub fn raw_len(&self, state: &LuaState) -> i64 { ... }
/// Set or clear the metatable.
pub fn set_metatable(
&self, state: &mut LuaState, mt: Option<Table>,
) -> LuaResult<()> { ... }
/// Get the underlying GcRef.
pub fn gc_ref(self) -> GcRef<table::Table> { ... }
}
Note: Table handle methods take &LuaState (the internal VM state),
not &Lua. This is because handles are used both from the public API
and from stdlib internals. Use lua.state() / lua.state_mut() (which
are pub(crate)) to get the state reference, or use
Lua::table_raw_set() for the public-facing convenience method.
Function
pub struct Function(/* GcRef<Closure> */);
impl Function {
/// Get the underlying GcRef.
pub fn gc_ref(self) -> GcRef<Closure> { ... }
}
Call a Function via Lua::call_function() or Lua::call_function_traced().
Thread
pub struct Thread(/* GcRef<LuaThread> */);
impl Thread {
/// Get the status of this coroutine thread.
pub fn status(&self, state: &LuaState) -> ThreadStatus { ... }
/// Get the underlying GcRef.
pub fn gc_ref(self) -> GcRef<LuaThread> { ... }
}
pub enum ThreadStatus {
Initial, // loaded, not yet started
Running, // currently executing
Suspended, // yielded, waiting to be resumed
Normal, // resumed another coroutine, waiting
Dead, // finished or errored
}
AnyUserData
pub struct AnyUserData(/* GcRef<Userdata> */);
impl AnyUserData {
/// Borrow the inner data as `&T`.
/// Returns None if the type doesn't match or the userdata was collected.
pub fn borrow<'a, T: Any>(
&self, state: &'a LuaState,
) -> Option<&'a T> { ... }
/// Borrow the inner data as `&mut T`.
pub fn borrow_mut<'a, T: Any>(
&self, state: &'a mut LuaState,
) -> Option<&'a mut T> { ... }
/// Set or clear the metatable.
pub fn set_metatable(
&self, state: &mut LuaState, mt: Option<Table>,
) -> LuaResult<()> { ... }
/// Get the metatable, if set.
pub fn metatable(&self, state: &LuaState) -> Option<Table> { ... }
/// Get the underlying GcRef.
pub fn gc_ref(self) -> GcRef<Userdata> { ... }
}
Embedding Example
Mirrored at examples/embedding.rs.
use rilua::{Lua, LuaApiMut, StdLib, Val};
fn main() -> rilua::LuaResult<()> {
let mut lua = Lua::new_with(StdLib::ALL)?;
// Execute Lua code
lua.exec(r#"
x = 1 + 2
msg = string.format("x = %d", x)
"#)?;
// Read Lua globals from Rust
let x: f64 = lua.global("x")?;
assert_eq!(x, 3.0);
let msg: String = lua.global("msg")?;
assert_eq!(msg, "x = 3");
// Set Lua globals from Rust
lua.set_global("greeting", "hello from Rust")?;
lua.exec("print(greeting)")?;
// Load and call a function
let func = lua.load("return 1 + 2")?;
let results = lua.call_function(&func, &[])?;
assert_eq!(results, vec![Val::Num(3.0)]);
Ok(())
}
Implementing Native Functions
Native functions use the low-level stack-based API via LuaState.
This is the same API used by the standard library implementation.
Mirrored at examples/native_function.rs.
use rilua::vm::state::LuaState;
use rilua::LuaResult;
use rilua::{Lua, LuaApiMut, RustFn};
/// A native function that adds two numbers.
/// Arguments are on the stack at indices base..top.
/// Returns the number of results pushed.
fn my_add(state: &mut LuaState) -> LuaResult<u32> {
let a = state.check_number(1)?;
let b = state.check_number(2)?;
state.push_number(a + b);
Ok(1)
}
fn main() -> rilua::LuaResult<()> {
let mut lua = Lua::new()?;
let f: RustFn = my_add;
lua.register_function("my_add", f)?;
lua.exec("print(my_add(10, 20))")?; // prints 30
Ok(())
}
The RustFn type is fn(&mut LuaState) -> LuaResult<u32>. It takes
a function pointer, not a closure. For stateful native functions, use
RustClosure upvalues (see future-api.md for a
planned closure-based alternative).
Internal Stack Model
Internally, rilua uses a virtual stack similar to PUC-Rio’s C API for stdlib function implementation. Understanding this model is necessary for implementing stdlib functions and the debug library.
Stack Index Addressing
Stack indices address values relative to the current call frame:
| Index type | Range | Resolution |
|---|---|---|
| Positive | 1, 2, 3, … | Base-relative: base + index - 1 |
| Negative | -1, -2, -3, … | Top-relative: top + index |
| Pseudo-index | special constants | Registry, globals, environment, upvalues |
Pseudo-indices provide access to non-stack locations:
| Pseudo-index | Target |
|---|---|
REGISTRY_INDEX | Global registry table (shared across all threads) |
GLOBALS_INDEX | Current thread’s global table |
ENVIRON_INDEX | Current function’s environment table |
| Upvalue indices | C closure upvalue slots (one per captured value) |
Push/Get Protocol
Push operations write to stack[top] then increment top.
Operations that allocate GC objects (strings, tables, closures)
trigger a GC check. Operations on immediate values (nil, boolean,
number) do not.
Get operations read from the addressed stack slot. Most are non-mutating. Exception: number-to-string conversion mutates the stack slot in place and triggers a GC check (string allocation).
C closure upvalues are stored as values directly inside the
closure struct (not as UpVal objects like Lua closures). When
creating a C closure with n upvalues, the top n stack values
are popped and copied into the closure’s upvalue array.
Table Operations
With metamethods (gettable/settable): invoke the full
__index/__newindex chain (up to 100 iterations). gettable
takes the key from the stack top, replaces it with the result.
settable takes key and value from the top, pops both.
Without metamethods (rawget/rawset): bypass the metamethod
chain. Call luaH_get/luaH_set directly. The raw variants require
the target to be a table (not just anything with a metatable).
Integer shortcuts (rawgeti/rawseti): take the integer key
as a parameter instead of from the stack.
Call Protocol
- Push the function onto the stack.
- Push arguments in order (first argument pushed first).
- Call with
(nargs, nresults). - The function and all arguments are removed.
nresultsresults are pushed (excess discarded, missing padded with nil).MULTRET(-1) means push all results.
Protected calls return a status code. On error, the function and arguments are replaced by a single error message on the stack. An optional error handler function is specified by stack index (converted to a stack offset internally because the stack may be reallocated).
Registry and Environments
The registry is a global table stored in the shared state. It is accessible via the registry pseudo-index. C libraries use it to store private data (metatables, module state, references) that should not be accessible from Lua code.
Function environments: every closure (C or Lua) has an associated environment table. For Lua closures, the environment is the table used for global variable lookups. The environment is captured from the creating function when a new closure is created.
Thread environments: each thread has its own global table. The main thread’s global table is the default environment for new closures.
Reference System
The reference system provides a way to store Lua values in a table (typically the registry) and retrieve them later by integer handle. It is a free-list allocator using integer keys:
ref(table)pops a value from the stack, stores it at an integer key. If there is a free slot (from a previousunref), it reuses it. Otherwise appends at#table + 1.unref(table, ref)adds the slot back to the free list.table[0]stores the free list head. Each free slot stores the index of the next free slot as its value.- Special constants:
REF_NIL(-1) for nil values (never stored),NO_REF(-2) for invalid references.
Library Registration Protocol
Each standard library is loaded by pushing its opener function, pushing the library name as argument, then calling. The opener:
- Creates a table for the library (or reuses an existing one from
_LOADED). - Registers all functions via closure creation and field assignment.
- Stores the table in both
_LOADED[name]and as a global.
The base library uses an empty name and registers directly into the global table.
GC Handle Safety
Values on the Lua stack (between stack[0] and stack[top-1]) are
marked as reachable during GC traversal. This is the primary
mechanism for protecting values from collection.
Stack traversal during GC:
- Mark the thread’s global table.
- Mark all values from
stack[0]tostack[top-1]. - Nil out slots from
topto the maximumci.topacross all call frames (clears stale references).
GC checks (checkGC) run after operations that allocate GC
objects. The check occurs before the allocation in the API function,
which is safe because the new object does not exist yet. After
allocation, the object is immediately placed on the stack (making it
reachable).
Write barriers maintain the tri-color invariant during incremental GC:
- Forward barrier: when a black object (already traversed) gets a white value (not yet traversed) stored into it, the white value is immediately marked.
- Back barrier (for tables): the table is re-grayed so it will be re-traversed.
C closure upvalues and table entries both require write barriers when mutated through the API.
Future API Enhancements
Planned ergonomic improvements for the rilua embedding API. These are
not yet implemented. See docs/api.md for the current API.
Closure-Based Function Creation
Currently, native functions must be declared as fn pointers
(fn(&mut LuaState) -> LuaResult<u32>). This prevents capturing
state. A closure-based API would allow registering Rust closures
directly:
impl Lua {
/// Create a Lua-callable function from a Rust closure.
pub fn create_function<F>(&mut self, func: F) -> LuaResult<Function>
where
F: Fn(&mut Lua) -> LuaResult<u32> + 'static,
{ ... }
}
This requires storing a Box<dyn Fn> inside the closure rather than
a bare function pointer. The current RustClosure struct uses upvalue
slots for state, which works but is less ergonomic for embedders.
Trait-Based Call and Resume
Function calling and coroutine resuming currently use raw Val slices.
Trait-based versions would automatically convert arguments and results:
impl Lua {
/// Call a Lua function with trait-based argument/result conversion.
pub fn call<A, R>(&mut self, func: Function, args: A) -> LuaResult<R>
where
A: IntoLuaMulti,
R: FromLuaMulti,
{ ... }
/// Protected call -- catches Lua errors.
pub fn pcall<A, R>(
&mut self, func: Function, args: A,
) -> std::result::Result<R, LuaError>
where
A: IntoLuaMulti,
R: FromLuaMulti,
{ ... }
/// Create a new coroutine from a function.
pub fn create_thread(
&mut self, func: Function,
) -> LuaResult<Thread> { ... }
}
impl Thread {
pub fn resume<A: IntoLuaMulti, R: FromLuaMulti>(
&self, lua: &mut Lua, args: A,
) -> LuaResult<R> { ... }
}
impl Function {
pub fn call<A: IntoLuaMulti, R: FromLuaMulti>(
&self, lua: &mut Lua, args: A,
) -> LuaResult<R> { ... }
}
This depends on tuple impls for IntoLuaMulti / FromLuaMulti
(see below).
Tuple Multi-Value Conversions
Currently IntoLuaMulti and FromLuaMulti are only implemented for
Vec<Val> and (). Tuple impls would enable ergonomic multi-argument
and multi-return patterns:
// Implemented for tuples up to reasonable arity:
impl<A: IntoLua, B: IntoLua> IntoLuaMulti for (A, B) { ... }
impl<A: FromLua, B: FromLua> FromLuaMulti for (A, B) { ... }
// ... up to 8 or 12 elements
This enables patterns like:
let (name, age): (String, f64) = lua.call(func, ("query", 42))?;
Container Conversions
Additional IntoLua / FromLua implementations for standard Rust
container types:
| Rust Type | Lua Type | Notes |
|---|---|---|
Vec<T> | table (sequence) | Keys are 1-indexed integers |
HashMap<K, V> | table | Key and value types must implement traits |
Option<T> | T or nil | None maps to nil, Some(v) maps to v |
impl<T: IntoLua> IntoLua for Vec<T> { ... }
impl<T: FromLua> FromLua for Vec<T> { ... }
impl<K: IntoLua + Eq + Hash, V: IntoLua> IntoLua for HashMap<K, V> { ... }
impl<K: FromLua + Eq + Hash, V: FromLua> FromLua for HashMap<K, V> { ... }
impl<T: IntoLua> IntoLua for Option<T> { ... }
impl<T: FromLua> FromLua for Option<T> { ... }
Trait-Based Table Access
Table handles currently only support raw access with Val keys.
Trait-based accessors would add type conversion and metamethod support:
impl Table {
/// Get with metamethods and type conversion.
pub fn get<K: IntoLua, V: FromLua>(
&self, lua: &mut Lua, key: K,
) -> LuaResult<V> { ... }
/// Set with metamethods and type conversion.
pub fn set<K: IntoLua, V: IntoLua>(
&self, lua: &mut Lua, key: K, value: V,
) -> LuaResult<()> { ... }
/// Length with `__len` metamethod.
pub fn len(&self, lua: &mut Lua) -> LuaResult<i64> { ... }
/// Iterate key-value pairs (wraps `next()`).
pub fn pairs<K: FromLua, V: FromLua>(
&self, lua: &mut Lua,
) -> TablePairs<K, V> { ... }
/// Get the next key-value pair after `key`.
pub fn next<K: IntoLua, NK: FromLua, NV: FromLua>(
&self, lua: &mut Lua, key: K,
) -> LuaResult<Option<(NK, NV)>> { ... }
}
Note: the current raw methods take &LuaState. The trait-based
versions would take &mut Lua to support string interning and
metamethod dispatch.
UserData Trait
The current userdata system uses Box<dyn Any> with manual metatable
construction. An mlua-style UserData trait would allow declarative
method and field registration:
pub trait UserData {
fn add_methods(methods: &mut UserDataMethods<Self>);
fn add_fields(fields: &mut UserDataFields<Self>);
}
struct UserDataMethods<T> { ... }
impl<T> UserDataMethods<T> {
pub fn add_method<M, A, R>(&mut self, name: &str, method: M)
where
M: Fn(&T, &mut Lua, A) -> LuaResult<R>,
A: FromLuaMulti,
R: IntoLuaMulti,
{ ... }
pub fn add_method_mut<M, A, R>(&mut self, name: &str, method: M)
where
M: FnMut(&mut T, &mut Lua, A) -> LuaResult<R>,
A: FromLuaMulti,
R: IntoLuaMulti,
{ ... }
}
Usage:
struct Player { name: String, health: f64 }
impl UserData for Player {
fn add_methods(methods: &mut UserDataMethods<Self>) {
methods.add_method("name", |p, _, ()| Ok(p.name.clone()));
methods.add_method_mut("heal", |p, _, amount: f64| {
p.health += amount;
Ok(())
});
}
}
This depends on closure-based function creation and tuple multi-value conversions.
Native Function Helpers
Currently, native functions interact with the stack directly via
LuaState methods (check_number, check_string, etc.). Higher-level
helpers on Lua would provide trait-based argument extraction:
impl Lua {
/// Get argument at 1-based index, converting via FromLua.
/// Raises an argument error if the value is missing or wrong type.
pub fn check_arg<V: FromLua>(&self, index: u32) -> LuaResult<V> { ... }
/// Push a return value onto the stack, converting via IntoLua.
pub fn push<V: IntoLua>(&mut self, value: V) -> LuaResult<()> { ... }
}
This would enable:
lua.register_function("greet", |lua| {
let name: String = lua.check_arg(1)?;
lua.push(format!("Hello, {name}!"))?;
Ok(1)
});
Note: this also depends on closure-based function creation to accept
a closure that captures &mut Lua instead of &mut LuaState.
Priority
These enhancements improve the embedding experience but are not required for Lua 5.1.1 compatibility. They should be implemented after the PUC-Rio test suite passes (Phase 9e).
Suggested implementation order:
- Container conversions (
Vec<T>,HashMap,Option<T>) – standalone, no dependencies - Tuple
IntoLuaMulti/FromLuaMulti– standalone, enables later items - Trait-based table access (
Table::get<K,V>,Table::set<K,V>) - Closure-based function creation (
create_function) - Trait-based call/resume (
Lua::call<A,R>,Thread::resume<A,R>) - Native function helpers (
check_arg,push) - UserData trait (depends on 2, 4, 5)
Lua Feature Matrix (5.1 – 5.5)
Cross-version feature reference for the Lua programming language. Each feature shows the version in which it was introduced, and when it was deprecated or removed.
rilua targets Lua 5.1.1 (the version embedded in the World of Warcraft game client). Features from later versions are documented for reference.
Legend
Version columns:
- ✓ – Available (standard, documented in reference manual)
- D – Deprecated (per reference manual or available only via compatibility flags)
- (empty) – Not present (not yet introduced, or removed)
rilua column:
- ✓ – Fully implemented
- ~ – Partially implemented (known limitations)
- ✗ – Not implemented
1. Types and Values
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
| nil | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| boolean | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Only false and nil are falsy |
| number (float only) | ✓ | ✓ | ✓ | All numbers are f64 in 5.1–5.2 | |||
| number (integer subtype) | ✓ | ✓ | ✓ | ✗ | 64-bit integers + 64-bit floats | ||
| string | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Immutable, 8-bit clean byte sequences |
| function | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | First-class values |
| table | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Associative arrays |
| userdata (full) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Arbitrary host data with metatables |
| userdata (light) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Raw pointer, no metatable |
| thread | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Coroutine execution context |
| Reference semantics | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Tables, functions, threads, userdata |
| Automatic string-number coercion | ✓ | ✓ | ✓ | D | D | ✓ | Restricted in 5.4+ (moved to string metamethods) |
2. Lexical Elements
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
| 21 reserved keywords | ✓ | ✓ | and break do else elseif end false for function if in local nil not or repeat return then true until while | ||||
| 22 reserved keywords | ✓ | ✓ | ✓ | ✗ | 5.2+ adds goto to the 5.1 set | ||
| 23 reserved keywords | ✓ | ✗ | 5.5 adds global; effectively 22 when LUA_COMPAT_GLOBAL is on (default) | ||||
global keyword | ✓ | ✗ | Conditionally reserved; unreserved by default via LUA_COMPAT_GLOBAL | ||||
Short strings ("..." / '...') | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
Long strings ([[...]]) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | With levels: [=[...]=] |
Escape: \a \b \f \n \r \t \v \\ \" \' | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
Escape: \ddd (decimal byte) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
Escape: \xHH (hex byte) | ✓ | ✓ | ✓ | ✓ | ✗ | 5.2 feature | |
Escape: \z (skip whitespace) | ✓ | ✓ | ✓ | ✓ | ✗ | 5.2 feature | |
Escape: \u{XXXX} (Unicode) | ✓ | ✓ | ✓ | ✗ | UTF-8 encoding of codepoint | ||
| Decimal numeric literals | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 42, 3.14, 1e10 |
| Hexadecimal integer literals | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 0xff |
| Hexadecimal float literals | ✓ | ✓ | ✓ | ✓ | ✗ | 0x1.Bp10 | |
Line comments (--) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
Block comments (--[[...]]) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
Shebang line (#!) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | First line only |
Empty statement (;) | ✓ | ✓ | ✓ | ✓ | ✓ | 5.1 allows ; as separator only, not as statement |
3. Variables and Scope
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
| Global variables | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Stored in environment table |
| Local variables | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Lexically scoped |
| Upvalues (closures) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Inner functions capture outer locals |
Function environments (setfenv/getfenv) | ✓ | ✓ | Replaced by _ENV in 5.2 | ||||
_ENV variable | ✓ | ✓ | ✓ | ✓ | ✗ | Free names translated to _ENV.var | |
_G global table | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
local x <const> | ✓ | ✓ | ✗ | Compile-time constant local | |||
local x <close> | ✓ | ✓ | ✗ | Calls __close on scope exit | |||
global declarations | ✓ | ✗ | global x, global *, global x <const> | ||||
| Named vararg tables | ✓ | ✗ | Named access to varargs |
4. Statements and Control Flow
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
do ... end blocks | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Explicit scope |
| Assignment (single and multiple) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | x, y = y, x |
if ... elseif ... else ... end | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
while ... do ... end | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Pre-test loop |
repeat ... until | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Post-test loop; body visible to condition |
Numeric for | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | for i = start, limit, step do |
Generic for | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | for k, v in iterator do |
Generic for closing value | ✓ | ✓ | ✗ | 4th variable: to-be-closed | |||
Read-only for loop variables | ✓ | ✗ | Control variable in generic for is read-only | ||||
break | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Must be last statement in 5.1 |
break anywhere in block | ✓ | ✓ | ✓ | ✓ | ✗ | 5.1 requires do break end workaround | |
goto and ::label:: | ✓ | ✓ | ✓ | ✓ | ✗ | Cannot jump into or across local scope | |
return | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | With optional expression list |
| Function calls as statements | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
5. Expressions and Operators
5.1 Arithmetic
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
+ - * / % ^ (unary -) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
Floor division // | ✓ | ✓ | ✓ | ✗ | Rounds quotient toward negative infinity |
5.2 Bitwise
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
& (AND) | ✓ | ✓ | ✓ | ✗ | Language-level operator (not bit32 library) | ||
| (OR) | ✓ | ✓ | ✓ | ✗ | |||
~ (XOR / unary NOT) | ✓ | ✓ | ✓ | ✗ | Binary XOR and unary NOT | ||
<< >> (shifts) | ✓ | ✓ | ✓ | ✗ |
5.3 Relational
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
== ~= < > <= >= | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
5.4 Logical
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
and or not | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Short-circuit evaluation |
5.5 Other
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
.. (concatenation) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
# (length) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
Table constructors {} | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Array, record, mixed |
| Parenthesized expressions | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Truncate multi-return to single value |
6. Functions
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
| Named definition | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | function f() end |
| Local definition | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | local function f() end |
| Anonymous (lambda) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | function() end |
Method definition (:) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Implicit self parameter |
| Dotted names | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | function a.b.c() end |
| No-parenthesis call | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Single string or table arg |
Method call (:) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | obj:method(args) |
| Multiple return values | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
Varargs (...) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Proper tail calls | ✓ | ✓ | ✓ | ✓ | ✓ | ~ | Known bug with return-from-C |
| Closures (upvalues) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| C/Rust functions | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Native host functions |
| Light C functions | ✓ | ✓ | ✓ | ✓ | ✗ | No allocation overhead |
7. Metatables and Metamethods
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
getmetatable / setmetatable | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
__index (read) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Table or function |
__newindex (write) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Table or function |
__call | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Call non-function values |
__add __sub __mul __div __mod __pow __unm | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Arithmetic metamethods |
__idiv | ✓ | ✓ | ✓ | ✗ | Floor division metamethod | ||
__band __bor __bxor __bnot __shl __shr | ✓ | ✓ | ✓ | ✗ | Bitwise metamethods | ||
__eq | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.1-5.2: same type and same metamethod required; 5.3+: checks each operand’s metatable independently |
__lt __le | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
__concat | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
__len | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+ honors __len for tables |
__tostring | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
__metatable | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Protect metatable from access |
__mode (weak tables) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | "k", "v", or "kv" |
__gc (userdata finalizer) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
__gc (table finalizer) | ✓ | ✓ | ✓ | ✓ | ✗ | Extended to tables in 5.2 | |
__pairs | ✓ | ✓ | ✓ | ✓ | ✗ | Added in 5.2; present in source and manual index through 5.5 | |
__ipairs | ✓ | D | ✗ | Added in 5.2; deprecated in 5.3 (LUA_COMPAT_IPAIRS), removed in 5.4 | |||
__close | ✓ | ✓ | ✗ | For to-be-closed variables |
8. Garbage Collection
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
| Incremental mark-sweep | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Tri-color with write barriers |
| Generational mode (experimental) | ✓ | ✗ | Removed in 5.3 as experimental | ||||
| Generational mode (stable) | ✓ | ✓ | ✗ | Re-introduced in 5.4 | |||
| Incremental major collections | ✓ | ✗ | Major GC done incrementally in 5.5 | ||||
collectgarbage("collect") | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
collectgarbage("stop"/"restart") | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
collectgarbage("count") | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2 returns 2 values (kbytes, remainder); 5.1 and 5.3+ return single value |
collectgarbage("step") | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
collectgarbage("setpause"/"setstepmul") | ✓ | ✓ | ✓ | ✓ | ✓ | Replaced by collectgarbage("param") in 5.5 | |
collectgarbage("param") | ✓ | ✗ | Unified parameter access: ("param", name [, value]) | ||||
collectgarbage("isrunning") | ✓ | ✓ | ✓ | ✓ | ✗ | ||
collectgarbage("incremental") | ✓ | ✓ | ✓ | ✗ | Switch to incremental mode; 5.4+ accepts parameters | ||
collectgarbage("generational") | ✓ | ✓ | ✓ | ✗ | Switch to generational mode; 5.4+ accepts parameters | ||
Weak tables (__mode) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Ephemeron tables | ✓ | ✓ | ✓ | ✓ | ✗ | Weak keys with value dependency | |
| Emergency GC | ✓ | ✓ | ✓ | ✓ | ✗ | Runs GC on allocation failure | |
| Userdata finalizers | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Via __gc metamethod |
| Table finalizers | ✓ | ✓ | ✓ | ✓ | ✗ | Extended __gc to tables | |
gcinfo() | D | ✓ | Deprecated in 5.1; removed in 5.2 |
9. Coroutines
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
coroutine.create | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
coroutine.resume | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
coroutine.yield | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
coroutine.wrap | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Returns iterator function |
coroutine.status | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
coroutine.running | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+ returns 2 values (thread, is-main) |
coroutine.isyieldable | ✓ | ✓ | ✓ | ✗ | |||
coroutine.close | ✓ | ✓ | ✗ | Close coroutine and its to-be-closed vars | |||
Yieldable pcall/xpcall | ✓ | ✓ | ✓ | ✓ | ✗ | Coroutines can yield across protected calls | |
| Yieldable metamethods | ✓ | ✓ | ✓ | ✓ | ✗ |
10. Error Handling
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
error(msg [, level]) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
pcall(f, ...) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
xpcall(f, msgh) | ✓ | ✓ | 5.1 signature: 2 args only | ||||
xpcall(f, msgh, ...) | ✓ | ✓ | ✓ | ✓ | ✗ | 5.2+ passes extra args to f | |
| Error objects (any value) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
warn(msg) | ✓ | ✓ | ✗ | Non-fatal warning system |
11. Standard Library: Base Functions
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
assert | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
collectgarbage | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Options vary by version |
dofile | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
error | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
getmetatable | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
ipairs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.3+ uses lua_geti (respects __index); 5.2 __ipairs metamethod separate |
load | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+ accepts string and mode/env args |
loadfile | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+ accepts mode/env args |
next | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
pairs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
pcall | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
print | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.4+ calls __tostring directly |
rawequal | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
rawget | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
rawset | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
rawlen | ✓ | ✓ | ✓ | ✓ | ✗ | Length without __len | |
select | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
setmetatable | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
tonumber | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
tostring | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
type | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
xpcall | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Signature changed in 5.2 (see sec. 10) |
_G | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
_VERSION | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | "Lua 5.x" |
warn | ✓ | ✓ | ✗ | Non-fatal warnings | |||
unpack (global) | ✓ | D | D | ✓ | Moved to table.unpack in 5.2; 5.3 via LUA_COMPAT_UNPACK | ||
loadstring | ✓ | D | D | ✓ | Use load in 5.2+; 5.3 via LUA_COMPAT_LOADSTRING | ||
getfenv | ✓ | ✓ | Removed in 5.2 (replaced by _ENV) | ||||
setfenv | ✓ | ✓ | Removed in 5.2 (replaced by _ENV) | ||||
module | ✓ | D | D | ✓ | Deprecated in 5.2; 5.3 via LUA_COMPAT_MODULE | ||
newproxy | ✓ | ✓ | Undocumented in 5.1; removed in 5.2 | ||||
gcinfo | D | ✓ | Deprecated since 5.0; removed in 5.2 |
12. Standard Library: String
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
string.byte | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
string.char | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
string.dump | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.3+ adds strip parameter |
string.find | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
string.format | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+: %s calls __tostring via luaL_tolstring; 5.4+: %p specifier |
string.gmatch | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.4+ adds optional init parameter |
string.gsub | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
string.len | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
string.lower | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
string.match | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
string.rep | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+ adds optional sep parameter |
string.reverse | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
string.sub | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
string.upper | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
string.pack | ✓ | ✓ | ✓ | ✗ | Binary data packing | ||
string.unpack | ✓ | ✓ | ✓ | ✗ | Binary data unpacking | ||
string.packsize | ✓ | ✓ | ✓ | ✗ | Packed size calculation | ||
string.gfind | D | ✓ | Deprecated alias for gmatch; removed in 5.2 | ||||
String metatable (__index) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Method syntax: s:upper() |
Frontier pattern (%f) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Implemented in 5.1 (undocumented); documented in 5.2 |
Pattern class %g | ✓ | ✓ | ✓ | ✓ | ✗ | Printable characters (except space) | |
\0 in patterns | ✓ | ✓ | ✓ | ✓ | ✗ | Null bytes in patterns |
13. Standard Library: Table
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
table.concat | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
table.insert | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+ stricter argument checking |
table.remove | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+ stricter argument checking |
table.sort | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
table.unpack | ✓ | ✓ | ✓ | ✓ | ✗ | Replaces global unpack from 5.1 | |
table.pack | ✓ | ✓ | ✓ | ✓ | ✗ | Returns table with .n field | |
table.move | ✓ | ✓ | ✓ | ✗ | Move elements between positions/tables | ||
table.create | ✓ | ✗ | Pre-allocate table with size hints | ||||
table.maxn | ✓ | D | D | ✓ | Largest positive numeric key; 5.3 via LUA_COMPAT_MAXN | ||
table.foreach | D | ✓ | Deprecated since 5.0 | ||||
table.foreachi | D | ✓ | Deprecated since 5.0 | ||||
table.getn | D | ✓ | Use # operator instead | ||||
table.setn | D | ✓ | No replacement; removed in 5.2 |
14. Standard Library: Math
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
math.abs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.acos | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.asin | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.atan | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.3+ accepts 2 args (replaces atan2) |
math.ceil | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.cos | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.deg | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.exp | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.floor | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.fmod | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.log | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+ adds optional base parameter |
math.max | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.min | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.modf | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.rad | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.random | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.4 switches to xoshiro256** RNG |
math.randomseed | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.sin | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.sqrt | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.tan | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.huge | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Positive infinity |
math.pi | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
math.maxinteger | ✓ | ✓ | ✓ | ✗ | Largest integer value | ||
math.mininteger | ✓ | ✓ | ✓ | ✗ | Smallest integer value | ||
math.tointeger | ✓ | ✓ | ✓ | ✗ | Convert float to integer if fits | ||
math.type | ✓ | ✓ | ✓ | ✗ | "integer", "float", or false | ||
math.ult | ✓ | ✓ | ✓ | ✗ | Unsigned integer less-than comparison | ||
math.atan2 | ✓ | ✓ | D | D | D | ✓ | Use math.atan(y, x) in 5.3+; 5.3+ via LUA_COMPAT_MATHLIB |
math.cosh | ✓ | ✓ | D | D | D | ✓ | 5.3+ via LUA_COMPAT_MATHLIB |
math.sinh | ✓ | ✓ | D | D | D | ✓ | 5.3+ via LUA_COMPAT_MATHLIB |
math.tanh | ✓ | ✓ | D | D | D | ✓ | 5.3+ via LUA_COMPAT_MATHLIB |
math.pow | ✓ | ✓ | D | D | D | ✓ | Use x ^ y operator; 5.3+ via LUA_COMPAT_MATHLIB |
math.frexp | ✓ | ✓ | D | D | ✓ | ✓ | Restored as standard in 5.5; 5.3-5.4 via LUA_COMPAT_MATHLIB |
math.ldexp | ✓ | ✓ | D | D | ✓ | ✓ | Restored as standard in 5.5; 5.3-5.4 via LUA_COMPAT_MATHLIB |
math.log10 | ✓ | D | D | D | D | ✓ | Use math.log(x, 10); 5.3+ via LUA_COMPAT_MATHLIB |
math.mod | D | ✓ | Alias for fmod; deprecated since 5.0 |
15. Standard Library: I/O
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
io.close | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
io.flush | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
io.input | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
io.lines | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+ accepts read format options |
io.open | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
io.output | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
io.popen | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
io.read | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+ adds "*L" format (line with newline) |
io.tmpfile | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
io.type | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
io.write | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
io.stdin / io.stdout / io.stderr | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
file:close | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+: pipe close returns exit status |
file:flush | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
file:lines | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+ accepts format options |
file:read | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
file:seek | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
file:setvbuf | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
file:write | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+: returns file handle (for chaining) |
16. Standard Library: OS
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
os.clock | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
os.date | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
os.difftime | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
os.execute | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+ returns true/nil, reason, code |
os.exit | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+ adds optional close parameter |
os.getenv | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
os.remove | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
os.rename | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
os.setlocale | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
os.time | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
os.tmpname | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
17. Standard Library: Debug
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
debug.debug | ✓ | ✓ | ✓ | ✓ | ✓ | ~ | Stub |
debug.gethook | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Returns hook function, mask string, count |
debug.getinfo | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+ adds nparams, isvararg, istailcall |
debug.getlocal | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 5.2+ accesses vararg info |
debug.getmetatable | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
debug.getregistry | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
debug.getupvalue | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
debug.sethook | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Line, call, return, and count hooks |
debug.setlocal | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
debug.setmetatable | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
debug.setupvalue | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
debug.traceback | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
debug.upvalueid | ✓ | ✓ | ✓ | ✓ | ✗ | Unique ID for upvalue | |
debug.upvaluejoin | ✓ | ✓ | ✓ | ✓ | ✗ | Make upvalues share | |
debug.getuservalue | ✓ | ✓ | ✓ | ✓ | ✗ | 5.4+ supports multiple user values | |
debug.setuservalue | ✓ | ✓ | ✓ | ✓ | ✗ | 5.4+ supports multiple user values | |
debug.setcstacklimit | ✓ | ✗ | Added in 5.4; removed in 5.5 | ||||
debug.getfenv | ✓ | ✓ | Removed with environment model in 5.2 | ||||
debug.setfenv | ✓ | ✓ | Removed with environment model in 5.2 |
18. Standard Library: Package
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
require | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
package.config | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
package.cpath | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
package.loaded | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
package.loadlib | ✓ | ✓ | ✓ | ✓ | ✓ | ~ | Loads rilua-native modules (not PUC-Rio C modules); requires dynmod feature |
package.path | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
package.preload | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
package.searchpath | ✓ | ✓ | ✓ | ✓ | ✗ | ||
package.searchers | ✓ | ✓ | ✓ | ✓ | ✗ | Replaces package.loaders | |
package.loaders | ✓ | D | D | ✓ | Renamed to package.searchers in 5.2; 5.3 via LUA_COMPAT_LOADERS | ||
package.seeall | ✓ | D | D | ✓ | Deprecated in 5.2; 5.3 via LUA_COMPAT_MODULE | ||
module | ✓ | D | D | ✓ | Deprecated in 5.2; 5.3 via LUA_COMPAT_MODULE |
19. Standard Library: bit32
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
bit32.arshift | ✓ | D | ✗ | Arithmetic right shift | |||
bit32.band | ✓ | D | ✗ | Bitwise AND | |||
bit32.bnot | ✓ | D | ✗ | Bitwise NOT | |||
bit32.bor | ✓ | D | ✗ | Bitwise OR | |||
bit32.btest | ✓ | D | ✗ | Test bits | |||
bit32.bxor | ✓ | D | ✗ | Bitwise XOR | |||
bit32.extract | ✓ | D | ✗ | Extract bits | |||
bit32.replace | ✓ | D | ✗ | Replace bits | |||
bit32.lrotate | ✓ | D | ✗ | Left rotate | |||
bit32.lshift | ✓ | D | ✗ | Left shift | |||
bit32.rrotate | ✓ | D | ✗ | Right rotate | |||
bit32.rshift | ✓ | D | ✗ | Right shift |
20. Standard Library: UTF-8
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
utf8.char | ✓ | ✓ | ✓ | ✗ | Create UTF-8 string from codepoints | ||
utf8.charpattern | ✓ | ✓ | ✓ | ✗ | Pattern matching one UTF-8 character | ||
utf8.codes | ✓ | ✓ | ✓ | ✗ | Iterator over codepoints | ||
utf8.codepoint | ✓ | ✓ | ✓ | ✗ | Get codepoints from string | ||
utf8.len | ✓ | ✓ | ✓ | ✗ | Count UTF-8 characters | ||
utf8.offset | ✓ | ✓ | ✓ | ✗ | 5.5: also returns final position |
21. rilua: Interpreter CLI
Reproduces the PUC-Rio Lua 5.1.1 standalone interpreter (lua.c).
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
rilua [options] [script [args]] | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
Option -e stat | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Execute string |
Option -i | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Interactive mode after script |
Option -l name | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Require library |
Option -v | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Version info |
Option -- | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Stop option handling |
Option - | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Execute stdin |
Option -E | ✓ | ✓ | ✓ | ✓ | ✗ | Ignore environment variables | |
Option -W | ✓ | ✓ | ✗ | Turn on warnings | |||
LUA_INIT env var | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Execute string or @filename |
arg table | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | arg[0] is script name |
| REPL / interactive mode | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | _PROMPT / _PROMPT2 globals |
REPL =expr shorthand | ✓ | ✓ | ✓ | ✓ | ✓ | Calculator mode added in 5.3 alongside; =expr removed in 5.5 |
22. rilua: Bytecode Compiler CLI
Reproduces the PUC-Rio luac bytecode compiler/lister.
| Feature | 5.1 | 5.2 | 5.3 | 5.4 | 5.5 | rilua | Notes |
|---|---|---|---|---|---|---|---|
riluac [options] [files] | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
Option -l | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | List bytecode |
Option -l -l | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | List with constants and locals |
Option -p | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Parse only (syntax check) |
Option -o file | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Write binary output |
Option -s | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Strip debug info |
Option -v | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Version info |
23. rilua: Rust Embedding API
These features are rilua-specific (not part of the Lua language standard) but correspond to the C API provided by PUC-Rio Lua.
| Feature | rilua | Notes |
|---|---|---|
Lua struct (state ownership) | ✓ | Main VM state |
IntoLua / FromLua traits | ✓ | Type-safe value conversion |
Table handle | ✓ | Read/write table fields |
Function handle | ✓ | Call Lua functions from Rust |
Thread handle | ✓ | Coroutine manipulation |
AnyUserData handle | ✓ | Typed Rust data in Lua |
StdLib bitflags | ✓ | Selective library loading |
LuaResult<T> error handling | ✓ | Result-based (no longjmp) |
Lua::load() / Lua::load_bytes() | ✓ | Load and compile chunks |
Lua::gc_collect() etc. | ✓ | GC control from Rust |
| Registry table | ✓ | Global storage for host |
Version Evolution Summary
Lua 5.1 (2006) – rilua target
The baseline. All numbers are f64. Function environments via
setfenv/getfenv. Module system via module() and package.loaders.
Pattern matching (not regex). Incremental GC. Coroutines. 21 keywords.
Lua 5.2 (2011)
Replaced function environments with _ENV lexical scoping. Added
goto/labels, bit32 library, table.pack/table.unpack, rawlen,
yieldable pcall/xpcall, ephemeron tables, table finalizers (__gc),
hex string escapes, hexadecimal floats, package.searchpath. Documented
frontier patterns (%f, already implemented in 5.1). Removed
setfenv/getfenv, newproxy. Deprecated module, loadstring,
global unpack, table.maxn, math.log10.
Lua 5.3 (2015)
Added 64-bit integer subtype alongside floats. Introduced native bitwise
operators (&, |, ~, <<, >>), floor division (//), utf8
library, string.pack/unpack/packsize, table.move,
math.tointeger/math.type/math.maxinteger/math.mininteger,
coroutine.isyieldable, math.ult, \u{XXXX} string escapes,
string.dump strip parameter. Changed ipairs to use lua_geti
(respects __index metamethods). Deprecated bit32 library,
math.atan2, math.cosh/sinh/tanh, math.pow,
math.frexp/ldexp, math.log10. Deprecated __ipairs metamethod.
Removed experimental generational GC.
Lua 5.4 (2020)
Added to-be-closed variables (<close>), const locals (<const>),
generic for closing value (4th variable), stable generational GC,
warn() function and -W CLI flag, coroutine.close, __close
metamethod, string.gmatch init parameter, string.format %p,
xoshiro256** RNG, debug.setcstacklimit, multiple user values for
userdata. Relaxed __eq to check each operand’s metatable independently
(no longer requires same metamethod). Removed bit32 library and
__ipairs metamethod. Deprecated math functions (atan2, cosh, sinh,
tanh, pow, frexp, ldexp, log10) remain behind
LUA_COMPAT_MATHLIB (under LUA_COMPAT_5_3). Restricted implicit
string-to-number coercion (moved to string metatable metamethods).
Lua 5.5 (2025)
Added global keyword for explicit global declarations, named vararg
tables, read-only for-loop variables, table.create, incremental major
GC collections, compact arrays (60% less memory for large arrays),
decimal float printing, collectgarbage("param") unified parameter API.
Enhanced utf8.offset to return final position. Restored math.frexp
and math.ldexp as standard functions (no longer behind compat flag).
Removed debug.setcstacklimit, =expr REPL shorthand,
collectgarbage("setpause"/"setstepmul").
Corrections from Previous Versions
This matrix corrects version attributions found during verification against the Lua reference manuals and PUC-Rio source code.
Round 1 corrections (version attribution errors)
- Bitwise operators (
&,|,~,<<,>>) are language-level features added in 5.3, not 5.2. Thebit32library (a function library, not operators) was added in 5.2. rawlenwas added in 5.2, not 5.3.table.movewas added in 5.3, not 5.2.coroutine.isyieldablewas added in 5.3, not 5.2.\xHHand\zstring escapes were added in 5.2, not 5.1.- Empty statement (
;as a statement) was added in 5.2. In 5.1,;is only a statement separator. math.log10was deprecated in 5.2 (withmath.log(x, 10)as replacement), not 5.3.
Round 2 corrections (manual/source verification)
- Frontier pattern
%f: Was already implemented in 5.1.1 source code (lstrlib.clines 383–393) but undocumented. 5.2 officially documented it. The Lua 5.2 readme lists “frontier patterns” but this refers to documentation, not new code. rilua implements%f. __pairsmetamethod: Was not removed in 5.3. It remained functional through 5.3 (previously corrected from “removed in 5.3” to “removed in 5.4”; see Round 3 for further correction).__ipairsmetamethod: Was deprecated (not removed) in 5.3. The 5.3 manual says “its__ipairsmetamethod has been deprecated.” It was removed in 5.4.=exprREPL shorthand: Was not removed in 5.3. Calculator mode (addreturn) was added in 5.3, but=exprcoexisted through 5.4 with a “for compatibility with 5.2” comment.=exprwas finally removed in 5.5.
Round 3 corrections (source code and rilua verification)
\xHHand\zescape sequences: rilua column was ✓ but should be ✗. The rilua lexer (scan_short_string) only handles 5.1 escapes (\a \b \f \n \r \t \v \\ \" \' \<newline> \ddd). Previous note “rilua includes as extension” was incorrect.table.unpack: rilua column was ✓ but should be ✗. rilua only registers the globalunpack(5.1 standard).table.unpackis a 5.2+ feature not present in the table library.__pairsmetamethod: Was never removed from PUC-Rio source or manual index.lbaselib.c:luaB_pairsunconditionally checks__pairsin 5.2, 5.3, 5.4, and 5.5. Corrected from “removed in 5.4” to ✓ in all versions 5.2 through 5.5.string.format%sand__tostring: Started in 5.2 (whenluaL_tolstringwas introduced), not 5.3. The 5.1lstrlib.cusesluaL_checklstring(no metamethod); 5.2+ usesluaL_tolstring.- Reserved keyword count: 5.1 has 21 keywords. 5.2+ has 22 (adds
goto). Split into separate rows to avoid implying 5.2+ also has 21. - Math compat functions (
atan2,cosh,sinh,tanh,pow,frexp,ldexp,log10): Available in 5.4 behindLUA_COMPAT_MATHLIB(viaLUA_COMPAT_5_3). Marked D instead of empty. math.frexpandmath.ldexp: Restored as standard functions in 5.5 (always available, not behind any compat flag). Marked ✓ in 5.5.globalkeyword: Conditionally reserved in 5.5. Unreserved by default whenLUA_COMPAT_GLOBALis defined (which it is by default).__eqnote: Removed “5.2+ requires same type and same metamethod” – this restriction applies to all versions including 5.1. The 5.1 source (get_compTM) already requires both operands to share the same metamethod function.collectgarbage("count")note: Clarified that 5.2 is the outlier returning 2 values (kbytes + remainder). Both 5.1 and 5.3+ return a single combined value.table.insert/table.removestricter checking: Changed “5.3+” to “5.2+”. Position bounds validation vialuaL_argcheckwas introduced in 5.2, not 5.3.
Round 4 Corrections
debug.setcstacklimit: Changed 5.5 from ✓ to empty. This function was added in 5.4 but removed in 5.5 (not present in 5.5dblib[]).package.loaders: Changed 5.3 from empty to D. Available in 5.3 viaLUA_COMPAT_LOADERS(underLUA_COMPAT_5_1).moduleandpackage.seeall: Changed 5.3 from empty to D. Available in 5.3 viaLUA_COMPAT_MODULE(underLUA_COMPAT_5_1).- Version Evolution Summary (5.4): Changed “Removed
__pairs/__ipairsmetamethods” to “Removed__ipairsmetamethod”. The__pairsmetamethod was never removed (present unconditionally inlbaselib.cthrough 5.5). - Version Evolution Summary (5.4): Changed “Removed … deprecated math
functions” to “Moved … behind
LUA_COMPAT_MATHLIB”. The table data correctly shows D (not empty) for these functions in 5.4, so “Removed” was contradictory.
Round 5 Corrections
xpcall(f, msgh, ...): Changed rilua column from ✓ to ✗. The rilua implementation matches the 5.1 signature (2 args only); extra arguments are not forwarded tof(state.top = func_pos + 1in base.rs).- Legend: Refined D definition from “available via compatibility flags,
not recommended” to “per reference manual or available only via
compatibility flags”. In 5.1, functions like
gcinfo,table.foreach,table.foreachi,table.getnare deprecated per the reference manual but unconditionally registered (no compat flag). The original wording only described the later-version compat-flag pattern.
Round 6 Corrections
collectgarbage("incremental"): Added ✓ for 5.2. Present inopts[]asLUA_GCINC(value 11) for switching from generational to incremental mode. Removed in 5.3 (with experimental generational GC), re-added in 5.4.collectgarbage("generational"): Added ✓ for 5.2. Present inopts[]asLUA_GCGEN(value 10). Same lifecycle as"incremental".module(section 11): Changed 5.3 from empty to D for consistency with section 18 (which was already corrected in Round 4). Available in 5.3 viaLUA_COMPAT_MODULEunderLUA_COMPAT_5_1.
Round 7 Corrections (Lua 5.3 focused)
unpack(global): Changed 5.3 from empty to D. Available in 5.3 viaLUA_COMPAT_UNPACK(underLUA_COMPAT_5_1).loadstring: Changed 5.3 from empty to D. Available in 5.3 viaLUA_COMPAT_LOADSTRING(underLUA_COMPAT_5_1).table.maxn: Changed 5.3 from empty to D. Available in 5.3 viaLUA_COMPAT_MAXN(underLUA_COMPAT_5_2).- Math compat function notes: Changed “5.4-5.5 via
LUA_COMPAT_MATHLIB” to “5.3+ viaLUA_COMPAT_MATHLIB” formath.atan2,math.cosh,math.sinh,math.tanh,math.pow,math.log10. The flag exists in 5.3 underLUA_COMPAT_5_2, same mechanism as 5.4’sLUA_COMPAT_5_3. math.frexp/math.ldexpnotes: Changed “5.4 viaLUA_COMPAT_MATHLIB” to “5.3-5.4 viaLUA_COMPAT_MATHLIB”. Same compat mechanism applies in 5.3.ipairsnote: Changed “5.3+ respects metamethods” to clarify it useslua_geti(which respects__index), distinct from the 5.2__ipairsmetamethod.- Added
math.ultrow: New in 5.3 (unsigned integer less-than). Present unconditionally inmathlib[]in 5.3+. - 5.3 version summary: Added
math.ult,string.dumpstrip parameter,ipairsmetamethod behavioral change, andmath.log10deprecation.
Round 8 Corrections (Lua 5.4 focused)
__eqnote: Changed “Same type and same metamethod required (all versions)” to distinguish 5.1-5.2 behavior (requires same metamethod viaget_compTM) from 5.3+ behavior (checks each operand’s metatable independently vialuaT_gettmbyobj).- 5.4 version summary: Added
math.log10to the list of deprecated math functions behindLUA_COMPAT_MATHLIB(was omitted while the other 7 functions were listed). Addeddebug.setcstacklimit, multiple user values for userdata, genericforclosing value,-WCLI flag, and__eqrelaxation. Reworded “Moved … behindLUA_COMPAT_MATHLIB” to “remain behindLUA_COMPAT_MATHLIB(underLUA_COMPAT_5_3)” since these were already behind the flag in 5.3.
Round 9 Corrections (Lua 5.5 focused)
collectgarbage("setpause"/"setstepmul"): Changed 5.5 from ✓ to empty. These options were removed in 5.5, replaced by the unifiedcollectgarbage("param", name [, value])API (LUA_GCPARAM). Theopts[]array in 5.5’slbaselib.cno longer contains"setpause"or"setstepmul".- Added
collectgarbage("param")row: New in 5.5 only. Provides unified get/set for GC parameters:"pause","stepmul","stepsize","minormul","majorminor","minormajor". - Reserved keyword count for 5.5: Changed “22 reserved keywords” from
✓ to empty for 5.5. Added new “23 reserved keywords” row for 5.5.
The 5.5
llex.henum lists 23 reserved words (includingglobal);LUA_COMPAT_GLOBAL(on by default) unreservesglobal, making the effective count 22, but the manual lists 23. - 5.5 version summary: Added restorations (
math.frexp/math.ldexp), removals (debug.setcstacklimit,=exprREPL shorthand,collectgarbage("setpause"/"setstepmul")), and the newcollectgarbage("param")API.
Standard Library
Decision
Modular implementation, one file per library, matching PUC-Rio Lua 5.1.1 standard library behavior.
Overview
Lua 5.1.1 ships with 9 standard libraries (base, coroutine, string,
table, math, io, os, debug, package). The coroutine library is
registered as part of luaopen_base() but occupies its own namespace. Each library is a
collection of functions registered in a table (or as globals for the
base library). Libraries can be loaded selectively — an embedded Lua
environment may omit io and os for sandboxing.
Libraries
Base Library (stdlib/base.rs)
Global functions not in any table.
| Function | Status | Notes |
|---|---|---|
assert | Required | Error with optional message |
collectgarbage | Required | 7 options |
dofile | Required | Load and execute file |
error | Required | Throw error object at level |
getfenv | Required | Get function environment |
getmetatable | Required | Get metatable (respects __metatable) |
ipairs | Required | Integer key iterator |
load | Required | Load chunk from function |
loadfile | Required | Load chunk from file |
loadstring | Required | Load chunk from string |
gcinfo | Required | Deprecated GC info (returns KB used) |
next | Required | Table traversal |
pairs | Required | Generic table iterator |
pcall | Required | Protected call |
print | Required | Print to stdout (uses tostring) |
rawequal | Required | Equality without metamethods |
rawget | Required | Table access without metamethods |
rawset | Required | Table assignment without metamethods |
_G | Required | Global table reference |
select | Required | select(n, ...) or select('#', ...) |
setfenv | Required | Set function environment |
setmetatable | Required | Set metatable (respects __metatable) |
tonumber | Required | Convert to number (with base) |
tostring | Required | Convert to string (uses __tostring) |
type | Required | Type name as string |
unpack | Required | Table to multiple values |
xpcall | Required | Protected call with error handler |
_VERSION | Required | "Lua 5.1" |
newproxy | Optional | Undocumented, creates proxy userdata |
Base Library Behavioral Notes
assert(v [, message]) — if v is nil or false, calls
error(message). Default message is "assertion failed!". Returns
all arguments on success.
error(message [, level]) — level 0 means no position prefix.
Level 1 (default) prefixes with the current function’s location.
Level 2 uses the caller’s location, etc. If message is not a string,
no position prefix is added.
getfenv(f) — f can be a function or a number (stack level).
Level 0 returns the thread environment. Level 1 (default) returns
the current function’s environment. getfenv(0) differs from
getfenv() (the latter defaults to level 1).
getmetatable(object) — if the metatable has a __metatable
field, returns that field’s value instead of the actual metatable.
This protects metatables from user inspection.
ipairs(t) — returns an iterator function, the table, and 0.
The iterator returns index, value pairs starting at 1 until
t[index] is nil. Uses raw access (no metamethods).
pcall(f, ...) — calls f(...) in protected mode. Returns
true, results... on success or false, error on failure. The
error object can be any type, not just strings.
select(index, ...) — if index is "#", returns the count of
remaining arguments. If index is negative, counts from the end.
Error: "index out of range" if the resulting position is < 1.
setfenv(f, table) — level 0 changes the thread environment
(returns nothing). Cannot change environments of C functions (error:
"'setfenv' cannot change environment of given object").
setmetatable(table, metatable) — first arg must be a table
(not userdata). If the existing metatable has a __metatable field,
error: "cannot change a protected metatable".
tonumber(e [, base]) — base 10 uses lua_isnumber (handles
strings, hex 0xff, whitespace). Other bases (2-36) use unsigned
integer conversion only. Returns nil on failure.
tostring(e) — checks __tostring metamethod first. Without
metamethod: numbers use "%.14g" format, booleans produce
"true"/"false", nil produces "nil", other types produce
"typename: pointer".
unpack(list [, i [, j]]) — i defaults to 1, j defaults to
#list. Returns list[i] through list[j] using raw access. Error:
"table too big to unpack" if the range exceeds stack space.
xpcall(f, err) — calls f() with zero arguments (extra args
are discarded). The error handler receives the original error object
and its return value becomes the error returned by xpcall.
Coroutine Library (stdlib/base.rs)
| Function | Notes |
|---|---|
coroutine.create | Create coroutine from function |
coroutine.resume | Resume suspended coroutine |
coroutine.running | Return running coroutine (returns nothing if main thread) |
coroutine.status | Return status string (running/suspended/normal/dead) |
coroutine.wrap | Create coroutine as iterator function |
coroutine.yield | Suspend execution, return values to resume |
String Library (stdlib/string.rs)
Registered as the string table and as the string metatable’s
__index.
| Function | Notes |
|---|---|
string.byte | Character codes |
string.char | Characters from codes |
string.dump | Dump function bytecode |
string.find | Pattern matching search |
string.format | Formatted string output |
string.gmatch | Global pattern match iterator |
string.gsub | Global pattern substitution |
string.len | String length |
string.lower | Lowercase conversion |
string.match | Pattern match extraction |
string.rep | String repetition |
string.reverse | String reversal |
string.sub | Substring extraction |
string.upper | Uppercase conversion |
string.gfind | Deprecated alias for gmatch (works by default; raises error only if LUA_COMPAT_GFIND is undefined) |
Lua 5.1 patterns are NOT regular expressions. They support character
classes (%a, %d, %w, etc.), anchors (^, $), quantifiers
(*, +, -, ?), captures, and backreferences (%1 through
%9 to match a previous capture). They do not support alternation.
String Library Behavioral Notes
string.byte(s [, i [, j]]) — i defaults to 1, j defaults
to i. Negative positions count from end. Returns one integer per
byte (0-255). Stack check: "string slice too long".
string.char(...) — each argument must be 0-255 (error:
"invalid value"). No arguments returns empty string.
string.dump(function) — function must be a Lua function, not a
C function (error: "unable to dump given function").
string.find(s, pattern [, init [, plain]]) — init defaults
to 1 (negative counts from end). Plain mode does literal substring
search. Returns start, end, captures... on match, nil on failure.
Positions are 1-based.
string.format(formatstring, ...) — specifiers: c d i o u x X e E f g G q s and %%. Flags: - + (space) # 0. Width/precision
max 2 digits each. %q produces a Lua-readable quoted string
(escapes ", \, newlines, \r, \0). %s with no precision and
string >= 100 chars pushes directly (no truncation).
string.gmatch(s, pattern) — returns an iterator. Each call
returns the next match’s captures. Empty matches advance by 1
character to prevent infinite loops.
string.gsub(s, pattern, repl [, n]) — replacement can be
string (%0=match, %1-%9=captures, %%=literal %), function
(called with captures; falsy return keeps original), or table
(first capture as key; falsy result keeps original). Returns the
result string and substitution count.
string.sub(s, i [, j]) — j defaults to -1 (end of string).
Negative positions count from end. Returns empty string if
start > end.
Pattern Language Specification
Character classes (from match_class in lstrlib.c):
| Class | Matches | Negation |
|---|---|---|
%a | letters (isalpha) | %A |
%c | control characters (iscntrl) | %C |
%d | digits (isdigit) | %D |
%l | lowercase letters (islower) | %L |
%p | punctuation (ispunct) | %P |
%s | whitespace (isspace) | %S |
%u | uppercase letters (isupper) | %U |
%w | alphanumeric (isalnum) | %W |
%x | hex digits (isxdigit) | %X |
%z | the null byte (\0) | %Z |
%. | literal . (any % + non-letter = literal) | — |
Bracket classes: [abc] matches any of a, b, c. [^abc]
negated. [a-z] ranges. % classes work inside brackets.
Single character matchers: . matches any character. %x
matches a class. [...] matches a bracket class. Anything else
matches literally.
Quantifiers:
| Quantifier | Meaning | Strategy |
|---|---|---|
* | 0 or more | Greedy (max first, backtrack) |
+ | 1 or more | Greedy |
- | 0 or more | Lazy (min first, extend) |
? | 0 or 1 | Greedy |
Anchors: ^ at pattern start anchors to beginning. $ at
pattern end anchors to end. Elsewhere they are literal.
Captures: (...) captures matched text. () captures the
position (1-based integer) instead of text. Maximum 32 captures
(LUA_MAXCAPTURES). Backreferences: %1 through %9 match the
same text as the corresponding capture.
Special patterns: %bxy matches balanced delimiters (e.g.,
%b() matches balanced parentheses). %f[set] is a frontier
pattern — matches a position where the previous character does not
match [set] and the current character does. At string start, the
“previous character” is \0.
Error conditions: "malformed pattern (ends with '%%')",
"malformed pattern (missing ']')", "invalid capture index",
"unfinished capture", "invalid pattern capture",
"too many captures", "missing '[' after '%%f' in pattern".
Table Library (stdlib/table.rs)
| Function | Notes |
|---|---|
table.concat | Concatenate array elements |
table.insert | Insert element at position |
table.maxn | Maximum positive numeric key |
table.remove | Remove element at position |
table.sort | In-place sort |
table.foreach | Deprecated: iterate table (use pairs) |
table.foreachi | Deprecated: iterate array (use ipairs) |
table.getn | Deprecated: table length (use # operator) |
table.setn | Deprecated: raises error in 5.1.1 |
Table Library Behavioral Notes
table.concat(list [, sep [, i [, j]]]) — sep defaults to "",
i defaults to 1, j defaults to #list. Each element must be a
string or number (error: "table contains non-strings"). Uses raw
access. Returns empty string if i > j.
table.insert(list, [pos,] value) — 2 args appends at end. 3
args inserts at pos, shifting elements up. Error: "wrong number of arguments to 'insert'" for other counts.
table.maxn(list) — scans ALL keys (both parts) via next().
Returns the largest positive numeric key (including non-integer keys
like 1.5). Returns 0 if no positive numeric keys exist.
table.remove(list [, pos]) — pos defaults to #list (remove
from end). Shifts elements down, sets last element to nil. Returns
the removed element, or nothing if the table was empty.
table.sort(list [, comp]) — Quicksort with median-of-three
pivot. Tail recursion on the larger partition. Default comparison uses
< (invokes __lt metamethods). Error: "invalid order function for sorting" if the comparison is inconsistent (e.g., NaN values break
strict weak ordering). Not stable.
table.setn(table, n) — error: "'setn' is obsolete".
Math Library (stdlib/math.rs)
| Function | Notes |
|---|---|
math.abs | Absolute value |
math.acos | Arc cosine |
math.asin | Arc sine |
math.atan | Arc tangent |
math.atan2 | Two-argument arc tangent |
math.ceil | Ceiling |
math.cos | Cosine |
math.cosh | Hyperbolic cosine |
math.deg | Radians to degrees |
math.exp | Exponential |
math.floor | Floor |
math.fmod | Float modulo |
math.frexp | Decompose float |
math.huge | Infinity constant |
math.ldexp | Scale by power of 2 |
math.log | Natural logarithm |
math.log10 | Base-10 logarithm |
math.max | Maximum |
math.min | Minimum |
math.mod | Deprecated alias for fmod (enabled by default via LUA_COMPAT_MOD) |
math.modf | Integer and fractional parts |
math.pi | Pi constant |
math.pow | Power |
math.rad | Degrees to radians |
math.random | Random number |
math.randomseed | Set random seed |
math.sin | Sine |
math.sinh | Hyperbolic sine |
math.sqrt | Square root |
math.tan | Tangent |
math.tanh | Hyperbolic tangent |
Math Library Behavioral Notes
All single-argument functions use f64 methods from Rust’s standard
library. Each takes one number argument and returns one number.
math.random([m [, n]]) — 0 args: float in [0, 1). 1 arg:
integer in [1, m]. 2 args: integer in [m, n]. Error: "interval is empty" if the range is invalid. Uses C rand() equivalent
(deterministic for a given seed).
math.randomseed(x) — seeds the random generator. Argument
must be convertible to integer.
math.min(...) / math.max(...) — requires at least 1 argument.
NaN asymmetry: if NaN is the first argument, it is returned. If NaN
appears later, it is skipped (since NaN < x and NaN > x are
both false).
math.frexp(x) — returns 2 values: mantissa m and integer
exponent e where x = m * 2^e and 0.5 <= |m| < 1.
math.modf(x) — returns 2 values: integer part and fractional
part.
math.log(x) — natural logarithm only (no base parameter in
5.1). Base-10 is math.log10.
Lua % vs math.fmod: the Lua % operator uses
a - floor(a/b)*b (result has same sign as b), while math.fmod
uses C fmod (result has same sign as a). Example: -1 % 5 is
4 in Lua but math.fmod(-1, 5) is -1.
I/O Library (stdlib/io.rs)
| Function | Notes |
|---|---|
io.close | Close file |
io.flush | Flush output |
io.input | Set/get default input |
io.lines | Line iterator |
io.open | Open file |
io.output | Set/get default output |
io.popen | Open process (platform-dependent) |
io.read | Read from default input |
io.tmpfile | Create temporary file |
io.type | Check file handle type |
io.write | Write to default output |
| File methods | :close, :flush, :lines, :read, :seek, :setvbuf, :write |
io.stdin | Standard input file handle |
io.stdout | Standard output file handle |
io.stderr | Standard error file handle |
OS Library (stdlib/os.rs)
| Function | Notes |
|---|---|
os.clock | CPU time |
os.date | Date formatting |
os.difftime | Time difference |
os.execute | Run shell command |
os.exit | Exit process |
os.getenv | Environment variable |
os.remove | Delete file |
os.rename | Rename file |
os.setlocale | Set locale |
os.time | Current time |
os.tmpname | Temporary file name |
Debug Library (stdlib/debug.rs)
| Function | Notes |
|---|---|
debug.debug | Interactive debug prompt |
debug.getfenv | Get environment |
debug.gethook | Get hook function |
debug.getinfo | Function information |
debug.getlocal | Local variable value |
debug.getmetatable | Raw metatable |
debug.getregistry | Registry table |
debug.getupvalue | Upvalue value |
debug.setfenv | Set environment |
debug.sethook | Set hook function |
debug.setlocal | Set local variable |
debug.setmetatable | Set metatable |
debug.setupvalue | Set upvalue |
debug.traceback | Stack traceback |
Package Library (stdlib/package.rs)
| Function/Field | Notes |
|---|---|
require | Module loader (registered as global) |
module | Create module (registered as global) |
package.config | Directory/path separator configuration string |
package.cpath | C module search path |
package.loaded | Cache of loaded modules |
package.loaders | Ordered list of module searchers |
package.loadlib | Load native module (see Native Module Loading) |
package.path | Lua module search path |
package.preload | Pre-registered module loaders |
package.seeall | Set module environment to globals |
Native Module Loading
PUC-Rio Lua’s package.loadlib loads C modules via lua_CFunction
(int (*)(lua_State *)). rilua cannot load PUC-Rio C modules because:
- Rust has no stable ABI. The internal types (
LuaState,Val) change layout between compiler versions. - rilua’s function signature (
fn(&mut LuaState) -> LuaResult<u32>) differs from PUC-Rio’sextern "C" fn(*mut lua_State) -> c_int. - Building a C API compatibility shim (
lua.h-compatible) would require reimplementing the entire PUC-Rio stack API (~120 functions) withextern "C"wrappers, plus maintaining ABI stability guarantees that Rust does not provide.
Instead, rilua defines its own native module ABI. Modules are Rust
cdylib crates compiled against the same rilua version and rustc
version as the host. This is gated behind the dynmod Cargo feature
(default off). Without the feature, package.loadlib returns
(nil, msg, "absent").
When dynmod is enabled:
package.loadlib(path, funcname)loads a shared library, validates aRILUA_MODULE_INFOdescriptor (magic bytes, version, struct sizes), and looks up the named entry point.- The C module loaders (
package.loaders[3]and[4]) searchpackage.cpathfor modules namedrilua_open_<modname>. - Library handles are stored as userdata with a
__gcmetamethod that callsdlclose/FreeLibraryon collection.
See src/dynmod.rs for the ABI contract and examples/native_module/
for a working example.
Loading
Libraries are loaded via Lua::new() (all standard libraries) or
selectively via Lua::new_with(StdLib).
Mirrored at examples/selective_stdlib.rs.
use rilua::{Lua, StdLib};
fn main() -> rilua::LuaResult<()> {
let mut lua = Lua::new_with(
StdLib::BASE | StdLib::STRING | StdLib::TABLE | StdLib::MATH,
)?;
// io, os, debug, package omitted (sandboxed)
lua.exec(r#"print(string.upper("ok"))"#)?;
Ok(())
}
Error Message Formats
All luaL_error messages are prefixed by source location
("source:line: " or empty string).
Argument errors: "bad argument #N to 'funcname' (message)".
For methods, narg is decremented by 1 (implicit self). If narg
becomes 0: "calling 'name' on bad self (message)".
Type errors: "bad argument #N to 'funcname' (expected expected, got actual)".
Key format constants:
| Constant | Value |
|---|---|
LUA_MAXCAPTURES | 32 |
LUA_NUMBER_FMT | "%.14g" |
Implementation Priority
- Base library (with coroutine) — required for any Lua program
- String library — heavily used, pattern matching is complex
- Table library — common operations
- Math library — straightforward wrappers around
f64methods - I/O library — file operations
- OS library — system operations
- Package library — module system
- Debug library — introspection, lowest priority
WebAssembly Support
rilua compiles to wasm32-unknown-unknown for running Lua 5.1.1 code
in the browser or other WebAssembly runtimes.
How It Works
When targeting wasm32, src/platform.rs swaps every extern "C"
function for a pure-Rust stub. Only the platform layer changes; the
VM, compiler, and core standard libraries are unmodified.
platform.rs
|
+-- #[cfg(not(target_arch = "wasm32"))] -> extern "C" { ... }
|
+-- #[cfg(target_arch = "wasm32")] -> wasm_stubs module
Stubs and Replacements
| C Function | WASM Replacement |
|---|---|
strtod | f64::from_str() (ASCII decimal only) |
localeconv | Static LConv with decimal_point = '.' |
strcoll | Byte-wise comparison (no locale) |
setlocale | No-op (returns "C") |
strftime | Returns 0 (no formatting) |
localtime_r / gmtime_r | Returns false (no time conversion) |
mktime | Returns -1 |
clock | Returns -1 |
time(NULL) | Returns 0 |
isalpha, tolower, etc. | ASCII-only Rust equivalents |
| FILE* operations | Return null/error values |
popen / pclose | Return null/-1 |
signal | No-op (SIGINT handling disabled) |
Locale Differences
On native platforms, rilua uses strtod and localeconv for
locale-aware number parsing (matching PUC-Rio behavior where 3,14
parses as a number in locales using comma as decimal separator).
On WASM, number parsing is ASCII-only with . as the decimal point.
This matches the "C" locale and is correct for all standard Lua
programs.
Standard Library Availability
Libraries that need filesystem or process access are still loadable on WASM but their functions return errors. Libraries that are pure computation work without restrictions.
| Library | WASM Status | Notes |
|---|---|---|
base | Full | All 29 functions work |
string | Full | All 14 functions work, pattern matching included |
table | Full | All 9 functions work |
math | Full | All 28 functions work |
coroutine | Full | All 6 functions work |
debug | Full | All 14 functions work (no filesystem dependency) |
io | Errors | File operations return nil, "not supported" |
os | Partial | os.clock, os.date, os.time return defaults; os.execute, os.remove, os.rename, os.tmpname error |
package | Limited | require works for preloaded modules; file-based loading fails |
Sandboxed Loading
For WASM builds, load only the libraries that work:
use rilua::{Lua, StdLib};
let libs = StdLib::BASE | StdLib::STRING | StdLib::TABLE
| StdLib::MATH | StdLib::COROUTINE;
let mut lua = Lua::new_with(libs)?;
Building for WASM
Prerequisites
- Rust 1.92+ with the
wasm32-unknown-unknowntarget - wasm-pack (for browser builds)
rustup target add wasm32-unknown-unknown
As a Library Dependency
Add rilua to your WASM crate’s Cargo.toml:
[dependencies]
rilua = "0.1"
wasm-bindgen = "0.2"
[lib]
crate-type = ["cdylib"]
Example lib.rs:
use wasm_bindgen::prelude::*;
use rilua::{Lua, StdLib};
#[wasm_bindgen]
pub fn eval_lua(code: &str) -> String {
let libs = StdLib::BASE | StdLib::STRING | StdLib::TABLE
| StdLib::MATH | StdLib::COROUTINE;
let mut lua = match Lua::new_with(libs) {
Ok(l) => l,
Err(e) => return format!("init error: {e}"),
};
match lua.exec(code) {
Ok(()) => String::new(),
Err(e) => format!("{e}"),
}
}
Build with wasm-pack:
wasm-pack build --target web
Browser Demo
A working browser demo is in examples/wasm-demo/. It provides a
textarea for Lua code and renders output in the page. See
examples/wasm-demo/README.md for build and serve instructions.
The demo replaces print with a version that writes to a thread-local
String buffer (stdout does not exist in WASM), then returns the
captured output after execution.
Feature Interactions
| Feature | WASM Behavior |
|---|---|
dynmod | Disabled (no shared library loading on WASM) |
send | Works (GcRef indices are just u32 values) |
| SIGINT | No-op (no signal handling on WASM) |
Limitations
See the Standard Library Availability table and Locale Differences section above for specifics. In summary: no filesystem, no process control, no locale, no clock, ASCII-only number parsing.
print writes to stdout, which does not exist in WASM. Override it
to capture output (as the browser demo does).
Testing Strategy
Decision
Spec-driven, multi-layer testing. Unit tests for internals, oracle comparison for behavioral equivalence, integration tests for language semantics, PUC-Rio official test suite as the compatibility target.
Current: 1325 tests (609 unit, 431 integration, 277 oracle, 5 proptest, 3 lua51).
With dynmod feature: 1333 tests (611 unit, 6 dynmod, 431 integration,
277 oracle, 5 proptest, 3 lua51). All 5 layers are active.
Test Layers
Layer 1: Unit Tests
In-module #[cfg(test)] blocks testing internal components in
isolation. Every implementation chunk includes unit tests.
Lexer tests (src/compiler/lexer.rs):
- Token type recognition for all keywords and symbols
- Number literal parsing (decimal, hex, float, exponent)
- String literal parsing (escapes, long brackets)
- Comment handling (single-line, long comments)
- Error cases (unterminated strings, invalid escapes)
- Source position tracking
Parser tests (src/compiler/parser.rs):
- Expression parsing with operator precedence
- Statement parsing for each statement type
- Block and scope handling
- Error recovery and error messages
- AST structure verification
Compiler tests (src/compiler/codegen.rs):
- Instruction emission for each AST node type
- Register allocation
- Constant pool management
- Upvalue resolution
- Jump backpatching
VM tests (src/vm/):
- Instruction dispatch correctness
- Stack manipulation
- GC arena allocation and collection
- Table operations (get, set, resize)
- String interning
- Closure creation and upvalue management
Layer 2: Oracle Comparison Tests
Oracle comparison tests run the same Lua code in both rilua and PUC-Rio Lua 5.1.1, comparing output to verify behavioral equivalence. This catches divergences that unit tests and integration tests might miss.
Reference Binaries
- lua (interpreter):
./lua-5.1.1/src/lua - luac (compiler/lister):
./lua-5.1.1/src/luac
Both are built from the official PUC-Rio Lua 5.1.1 tarball. See
AGENTS.md for download, verification, and build instructions.
The lua binary path is configured via the LUA_REFERENCE_BIN
environment variable (defaults to ./lua-5.1.1/src/lua). Tests
that require the reference binary skip gracefully if it is not
available.
Oracle Test Framework
Test helpers in tests/helpers/:
// tests/helpers/oracle.rs
/// Run code in PUC-Rio Lua 5.1.1 and return (stdout, stderr, exit_code).
fn run_reference(code: &str) -> (String, String, i32);
/// Run code in rilua and return (stdout, stderr).
fn run_rilua(code: &str) -> (String, String);
/// Assert rilua produces the expected stdout for the given code.
fn assert_output(code: &str, expected: &str);
/// Run in both interpreters, assert stdout matches.
fn assert_matches_reference(code: &str);
Usage in tests:
#[test]
fn arithmetic_matches_reference() {
assert_matches_reference("print(1 + 2)");
assert_matches_reference("print(2 ^ 10)");
assert_matches_reference("print(10 % 3)");
assert_matches_reference("print(-7 % 3)"); // floor modulo
}
Bytecode Comparison
After the compiler is implemented, bytecode comparison tests compile
Lua snippets with both rilua and luac -l, then compare instruction
output. This verifies the compiler produces correct bytecode before
the VM exists.
/// Compile code with rilua and return a formatted instruction listing.
fn compile_rilua(code: &str) -> String;
/// Compile code with PUC-Rio luac -l and return the listing.
fn compile_reference(code: &str) -> String;
/// Assert both compilers produce equivalent bytecode.
fn assert_bytecode_matches(code: &str);
Bytecode comparison checks instruction opcodes and operands. It does not compare constant pool ordering or debug info formatting, since these may differ between implementations without affecting semantics.
When Each Test Category Activates
| Category | Activates after | Mechanism |
|---|---|---|
| Unit tests | Phase 0 (skeleton) | cargo test --lib |
| Bytecode comparison | Phase 2 (compiler) | Compare rilua compiler output with luac -l |
| Oracle comparison | Phase 3 + print | Compare rilua output with lua -e |
Integration .lua tests | Phase 3 + assert | cargo test --test integration |
| PUC-Rio test suite | Phase 5a (base lib) | cargo test --test lua51 |
Layer 3: Integration Tests
Lua scripts in tests/ that exercise language features through the
full pipeline. Each test uses assert() to validate behavior.
Organization mirrors the Lua 5.1 Reference Manual. Language tests cover Chapter 2 (“The Language”), standard library tests cover Chapter 5 (“Standard Libraries”).
Language tests (Chapter 2):
| File | Section | Description |
|---|---|---|
lexical.lua | 2.1 | Lexical conventions (keywords, names, strings, numbers, comments) |
types.lua | 2.2 | Values and types, coercion (2.2.1) |
variables.lua | 2.3 | Global, local, and table field variables |
statements.lua | 2.4 | Chunks, blocks, assignment, control structures, for loops, local declarations |
expressions.lua | 2.5 | Arithmetic, relational, logical operators, concatenation, length, precedence, table constructors, function calls, function definitions |
visibility.lua | 2.6 | Lexical scoping, upvalues, closures |
errors.lua | 2.7 | error(), pcall, xpcall, error objects, stack traces |
metatables.lua | 2.8 | Metamethods for arithmetic, comparison, indexing, call, concatenation, length |
environments.lua | 2.9 | Function environments, setfenv, getfenv |
gc.lua | 2.10 | Garbage collection, finalizers (2.10.1), weak tables (2.10.2) |
coroutines.lua | 2.11 | create, resume, yield, wrap, status, error propagation |
Standard library tests (Chapter 5):
| File | Section | Description |
|---|---|---|
stdlib-base.lua | 5.1 | Base library (assert, type, tonumber, tostring, select, unpack, etc.) |
stdlib-package.lua | 5.3 | Package/module library (require, module, loaders, etc.) |
stdlib-string.lua | 5.4 | String library (find, format, gmatch, gsub, etc.) |
stdlib-table.lua | 5.5 | Table library (concat, insert, remove, sort, maxn) |
stdlib-math.lua | 5.6 | Math library (abs, floor, ceil, random, sin, cos, etc.) |
stdlib-io.lua | 5.7 | I/O library (open, read, write, lines, etc.) |
stdlib-os.lua | 5.8 | OS library (clock, date, time, execute, etc.) |
stdlib-debug.lua | 5.9 | Debug library (getinfo, getlocal, sethook, traceback, etc.) |
Test infrastructure files: tests/helpers/mod.rs (shared utilities),
tests/helpers/oracle.rs (PUC-Rio comparison), tests/integration.rs
(runner), tests/lua51.rs (PUC-Rio suite runner).
Layer 4: PUC-Rio Official Test Suite
The PUC-Rio Lua 5.1.1 test suite (./lua-5.1-tests/) is the
compatibility target. These are verbatim test files from the
official Lua test tarball. See AGENTS.md for download instructions.
Official Running Modes
Per lua.org/tests/, the test suite has three running modes:
- Portable mode:
lua -e"_U=true" all.lua– skips system-dependent tests and memory-intensive operations. - Full mode:
lua all.lua– tests every corner of the language. Requires compiled C libraries inlibs/subdirectory. - Internal mode: Recompile Lua with
ltests.c/ltests.hto enable the T (testC) library for internal VM tests.
What all.lua Does
The all.lua runner is not a simple file list. It modifies the
runtime environment before running each test:
- Sets GC parameters:
collectgarbage("setstepmul", 180)andsetpause(190). - Redefines
dofileto round-trip every test file throughstring.dump+loadstring, implicitly testing binary chunk serialization and deserialization. - Wraps
big.luaincoroutine.wrap(the file yields values). calls.luaexpects adeepvariable set bymain.lua.- Sets a
debug.sethookcall/return hook during cleanup (line 118).
How rilua Runs These Tests
rilua does not run all.lua directly. Instead, each test file
is executed individually using two approaches:
Important: Tests must be run from the lua-5.1-tests/ directory.
Several tests depend on relative paths: attrib.lua creates files
in libs/, math.lua and verybig.lua require checktable.lua
via LUA_PATH, and file tests reference paths relative to the test
directory. Running from the project root will cause false failures.
Individual file execution (primary):
# Run from the test directory (required)
cd lua-5.1-tests
mkdir -p libs
# Run a single test
RILUA_TEST_LIB=1 LUA_PATH="?;./?.lua" ../target/release/rilua <test>.lua
# Run all tests
for f in *.lua; do
[ "$f" = "all.lua" ] && continue
echo -n "$(basename $f .lua): "
timeout 30 env RILUA_TEST_LIB=1 LUA_PATH="?;./?.lua" \
../target/release/rilua "$f" >/dev/null 2>&1 && echo "PASS" || echo "FAIL"
done
Comparison script (scripts/compare.sh):
# Compare all test files between PUC-Rio and rilua
scripts/compare.sh ./lua-5.1.1/src/lua ./target/release/rilua
The comparison script runs each .lua file individually (except
all.lua) with a 10-second timeout, reporting PASS/FAIL/TIMEOUT
for both interpreters. This differs from all.lua in that:
- No
string.dump/loadstringround-trip (tests run directly). - No GC parameter tuning.
- No inter-test state (
big.luaruns standalone, not in a coroutine;calls.luaruns withoutdeepfrommain.lua). - Each test gets a fresh interpreter state.
rilua also passes all.lua directly (see
What all.lua Does for its additional
requirements).
T Module
rilua implements PUC-Rio’s internal test library (T global),
the Rust equivalent of ltests.c. Activate it with the
RILUA_TEST_LIB=1 environment variable. 25 functions are
registered:
| Function | Description |
|---|---|
T.querytab | Returns (array size, hash size) for a table |
T.hash | Returns hash-part index for a key in a table |
T.int2fb | Converts integer to float-byte encoding |
T.log2 | Returns floor(log2(x)) |
T.listcode | Returns list of opcodes for a function |
T.setyhook | Sets yield-on-hook for a coroutine thread |
T.resume | Resumes a coroutine (no arguments) |
T.d2s | Converts f64 to 8-byte native-endian string |
T.s2d | Converts 8-byte native-endian string to f64 |
T.testC | C API mini-interpreter (28 commands) |
T.newuserdata | Create userdata with given byte size |
T.udataval | Return unique integer ID for userdata |
T.pushuserdata | Find/create userdata by its ID |
T.ref | Store object in registry, return integer key |
T.unref | Remove registry entry |
T.getref | Get value from registry by key |
T.upvalue | Get/set upvalue n of closure f |
T.checkmemory | No-op stub (GC consistency check) |
T.gsub | String substitution |
T.doonnewstack | Run code in a new coroutine |
T.newstate | Create independent Lua state |
T.closestate | Close a state created by newstate |
T.doremote | Execute code string in remote state |
T.loadlib | Load standard libraries into remote state |
T.totalmem | Get/set memory limit (OOM simulation) |
Four tests (api.lua, checktable.lua, closure.lua, code.lua)
use T extensively. When T is nil, guarded sections (if T then ... end) are skipped. All four pass with RILUA_TEST_LIB=1.
Test Files
| Test File | Area |
|---|---|
all.lua | Test runner (chains all tests with dump/undump) |
api.lua | C API interactions (requires T.testC) |
attrib.lua | require/package system, assignments, operators |
big.lua | String overflow, large line counts, table constructs |
calls.lua | Function calls and returns |
checktable.lua | Table invariant checker (utility functions only) |
closure.lua | Closures, upvalues, and coroutines |
code.lua | Code generation, optimizations (uses T.listcode) |
constructs.lua | Syntax, operator priority, language constructs |
db.lua | Debug library |
errors.lua | Error handling |
events.lua | Metatables and metamethods |
files.lua | I/O library |
gc.lua | Garbage collection |
literals.lua | Scanner/lexer and literal parsing |
locals.lua | Local variables |
main.lua | Standalone interpreter (lua.c) options |
math.lua | Math library |
nextvar.lua | Tables, next(), size operator, for loops |
pm.lua | Pattern matching |
sort.lua | table.sort |
strings.lua | String library |
vararg.lua | Vararg functions |
verybig.lua | Very large programs |
Current Status
All 23 files pass: api, attrib, big, calls, checktable, closure, code, constructs, db, errors, events, files, gc, literals, locals, main, math, nextvar, pm, sort, strings, vararg, verybig.
The all.lua runner also passes (see
What all.lua Does for its additional
requirements beyond individual file execution).
Compatibility flags: The PUC-Rio test suite was written with
default compat options enabled (e.g., LUA_COMPAT_VARARG enables
the arg table in vararg functions). WoW’s Lua disables some of
these. Tests that depend on compat options may need conditional
handling.
Layer 5: Behavioral Equivalence Tests
Tests that specifically verify behavioral equivalence with PUC-Rio Lua 5.1.1. These test edge cases where implementations commonly diverge:
- Numeric formatting (
tostring(0.1),string.format("%g", 0.1)) - Error message wording (programs may match on error strings)
- GC behavior (
collectgarbagereturn values) - Weak table clearing timing
- Finalizer execution order
- String-to-number coercion edge cases
- Integer overflow behavior (all numbers are f64)
- Modulo with negative operands (floor division)
- Concatenation type coercion
These tests use the oracle comparison framework to run each case in both rilua and PUC-Rio, comparing exact output. They are the last line of defense before the PUC-Rio test suite.
Test Workflow
Test-Driven Development
New features are implemented test-first where possible:
- Write a Lua test script that exercises the feature.
- Run the test against PUC-Rio Lua 5.1.1 to verify expected behavior.
- Implement the feature in rilua.
- Run the test and fix until it passes.
- Run the oracle comparison to verify matching output.
- Run the full test suite to check for regressions.
This ensures every feature is validated against the reference implementation.
Quality Gate
Every commit must pass:
cargo fmt -- --check && \
cargo clippy --all-targets && \
cargo test && \
cargo doc --no-deps
This ensures:
- Consistent formatting
- No lint warnings
- All tests pass (unit, integration, oracle, PUC-Rio suite)
- Documentation builds without errors
Coverage Tracking
Test coverage is measured by:
- Feature coverage – which Lua 5.1.1 features are implemented and tested (tracked in CHANGELOG.md).
- PUC-Rio test suite progress – 23 of 23 official test files passing (tracked in CI).
- Oracle comparison count – number of Lua snippets verified against PUC-Rio output.
- Code coverage –
cargo-tarpaulinorllvm-covfor line coverage metrics (informational, not a gate).
Performance
Performance characteristics, benchmarks against PUC-Rio Lua 5.1.1, and optimization history.
Goal: PUC-Rio Parity
The target is matching PUC-Rio Lua 5.1.1 (compiled with -O2) on the
official test suite. PUC-Rio Lua is written in C and represents the
performance floor for a Lua 5.1 implementation.
Benchmark Environment
| Property | Value |
|---|---|
| CPU | AMD Ryzen 7 8840U w/ Radeon 780M Graphics |
| OS | Fedora Linux 43 (kernel 6.18) |
| Rust | Edition 2024, --release profile |
| PUC-Rio | Lua 5.1.1, compiled with gcc -O2 -DLUA_USE_LINUX |
| Runs | 10 per test, median reported |
| Date | 2026-02-23 |
Per-Test Results (ms, median of 10 runs)
Tests from the PUC-Rio test suite run
individually. main.lua and big.lua are excluded: main.lua tests
CLI features via os.execute (environment-dependent), and big.lua
requires a coroutine wrapper set by all.lua.
| Test | PUC-Rio | rilua | Ratio |
|---|---|---|---|
| gc.lua | 70 | 85 | 1.21x |
| db.lua | 16 | 30 | 1.88x |
| calls.lua | 7 | 9 | 1.29x |
| strings.lua | 3 | 3 | 1.00x |
| literals.lua | 3 | 3 | 1.00x |
| attrib.lua | 4 | 4 | 1.00x |
| locals.lua | 4 | 6 | 1.50x |
| constructs.lua | 252 | 583 | 2.31x |
| code.lua | 2 | 2 | 1.00x |
| nextvar.lua | 13 | 28 | 2.15x |
| pm.lua | 11 | 11 | 1.00x |
| api.lua | 3 | 3 | 1.00x |
| events.lua | 3 | 3 | 1.00x |
| vararg.lua | 2 | 2 | 1.00x |
| closure.lua | 5 | 8 | 1.60x |
| errors.lua | 135 | 148 | 1.10x |
| math.lua | 5 | 6 | 1.20x |
| sort.lua | 55 | 98 | 1.78x |
| verybig.lua | 115 | 217 | 1.89x |
| files.lua | 12 | 13 | 1.08x |
| Sum | 720 | 1262 | 1.75x |
Interpretation
rilua is 1.75x slower than PUC-Rio Lua overall. Most tests are within 1.0-1.5x. Four tests account for the majority of the gap:
- constructs.lua (2.31x, +331ms): heavy control-flow constructs, deeply nested loops and conditionals. This test stresses the VM dispatch loop.
- nextvar.lua (2.15x, +15ms): table iteration (
next,pairs), global table manipulation. Stresses table hash traversal. - verybig.lua (1.89x, +102ms): large function compilation and execution with many locals and upvalues.
- db.lua (1.88x, +14ms): debug library operations,
getinfo,getlocal, hook management. - sort.lua (1.78x, +43ms):
table.sortwith comparison callbacks. Function call overhead per comparison.
Tests at or near parity (1.0-1.1x): strings.lua, literals.lua,
attrib.lua, code.lua, pm.lua, api.lua, events.lua,
vararg.lua, files.lua.
Combined Runner
bench-all.lua runs all 20 standalone tests sequentially in a single
interpreter session (like all.lua but without main.lua/big.lua
and without the dump/undump dofile override).
| Runner | PUC-Rio | rilua | Ratio |
|---|---|---|---|
| bench-all.lua | 792 | 1529 | 1.93x |
The combined runner is slower than the sum of individual tests (1.93x vs 1.75x). Running all tests in a single interpreter session accumulates more live objects across test boundaries, increasing GC work per cycle.
Reproducing
Build both interpreters and run the benchmark script:
# Build PUC-Rio Lua 5.1.1
cd lua-5.1.1 && make linux && cd ..
# Build rilua
cargo build --release
# Run benchmarks (default: 10 runs per test)
./scripts/benchmark-tests.sh [runs]
Optimization History
Starting from ~15.4s on the full suite, four optimization phases reduced runtime to ~2.6s (83% total reduction).
Phase 1: Lexer and Parser (~7% improvement)
- Keyword lookup:
matchdispatch replacing binary search on sorted array - Parser advance:
mem::replacereplacingToken::clone - Lexer: fast-path byte-slice scanning for common characters
- GC traverse: zero-allocation indexed access for tables and closures
Phase 2: Constant Pool (~68% reduction)
- Hash-based constant pool deduplication replacing O(n) linear scan
- Mirrors PUC-Rio’s
addkapproach usingluaH_setonfs->h ConstantKeyenum:Num(u64)/Bool(bool)/Str(Vec<u8>)- 15.4s -> 4.9s
Phase 3: GC and VM Inlining (~12% reduction)
#[inline]on hot GC arena and collector methodssweep_partial: direct assignment replacingmem::replaceon dead pathGCSWEEPMAX: 40 -> 80 to amortize dispatch overheadtraverse_thread: indexed access replacingVecclone allocationCallInfo.is_luacache: eliminates arena lookups in traceback- 4.9s -> 4.3s
Phase 4: SoA Sweep Layout (~46% reduction)
- Parallel
Vec<u8>color array (Structure-of-Arrays layout) - Sweep reads 1 byte per slot instead of loading full
Entry<T>(~72 bytes for tables) - Iterator-based sweep: eliminates per-access bounds checks
- 4.9s -> 2.6s (10-run median)
Profiling
Requirements
- Linux with
perfinstalled (linux-tools-commonor equivalent) cargo-flamegraph:cargo install flamegraph
Generating Flamegraphs
Build with debug symbols in release mode (already configured in
Cargo.toml via [profile.release] debug = true if needed):
# Profile a specific test file
cargo flamegraph -- -e "dofile('lua-5.1-tests/constructs.lua')"
# Profile the full test suite
cd lua-5.1-tests
RILUA_TEST_LIB=1 cargo flamegraph -- all.lua
Flamegraph SVGs are interactive. Open them in a browser to click-zoom into specific call stacks and search for function names.
Generated flamegraphs go in flamegraphs/ (gitignored).
Using perf Directly
cargo build --release
perf record -g --call-graph dwarf target/release/rilua lua-5.1-tests/constructs.lua
perf report
Benchmarks
Criterion Microbenchmarks
benches/interpreter.rs contains criterion benchmarks covering:
- State creation: empty, base libs, full stdlib
- Compilation: minimal, loops, functions, tables
- VM execution: arithmetic loops, fibonacci, string concat, tables, closures, metatable dispatch
- GC: full collect, allocation churn, incremental stepping
- String interning: unique strings, dedup hits
- Table operations: integer keys, string keys, mixed Lua ops
- End-to-end: compile+run, coroutine cycles
Run with:
cargo bench
Results go to target/criterion/. Use --save-baseline and
--baseline flags to compare across changes.
PUC-Rio Full Suite Benchmark
The primary wall-clock benchmark:
cargo build --release
./scripts/bench-puc-rio.sh [binary] [runs]
Arguments:
binary: path to rilua binary (default:target/release/rilua)runs: number of runs (default: 5)
Output: min, median, and max times. Prints median to stdout.
Regression Gate
scripts/perf-gate.sh compares the current build against the stored
baseline with a configurable threshold (default 5%).
./scripts/perf-gate.sh [baseline_ms] [threshold_pct]
If no arguments are given, reads .perf-baseline and uses 5%.
The script:
- Builds release
- Runs
bench-puc-rio.shwith 5 iterations - Compares median against
baseline + baseline * threshold / 100 - Exits 0 (pass) or 1 (regression detected)
After a confirmed improvement, update the baseline:
./scripts/bench-puc-rio.sh > .perf-baseline
Optimization Priorities
Based on the per-test benchmarks, these areas offer the largest potential gains, ordered by impact:
1. VM Dispatch (constructs.lua: 2.31x, +331ms)
constructs.lua is the heaviest test and the largest absolute gap.
It exercises the main execute() loop with deeply nested control flow.
- Instruction dispatch: the
match-based dispatch inexecute()is the hot path. Layout optimization, opcode reordering to improve branch prediction, and reducing per-instruction overhead would have the highest impact. - FORPREP/FORLOOP specialization: integer-only fast path for
numeric
forloops when bounds are integers.
2. Table Operations (nextvar.lua: 2.15x, sort.lua: 1.78x)
- Hash traversal:
next()andpairs()iteration speed.nextvar.luahammers these. - Comparison callback overhead:
sort.luacalls a Lua comparison function per element pair. Reducing function call setup/teardown cost would help.
3. Compilation (verybig.lua: 1.89x, +102ms)
- AST allocation: heap-allocated AST nodes dropped after
compilation. A pool or arena built from
Vec-based storage could reduce allocation pressure. - Constant folding: limited constant folding during compilation could reduce VM work for arithmetic-heavy code.
4. GC Under Sustained Load (bench-all.lua: 1.93x)
The combined runner is 10% slower relative to PUC-Rio than the sum of individual tests (1.93x vs 1.75x). This indicates GC overhead grows disproportionately with accumulated state. Incremental GC tuning and sweep efficiency under high object counts are the targets here.
5. Lower-Priority Opportunities
- String concatenation: batching consecutive
CONCAToperations to reduce intermediate allocations. - Generational GC: nursery for young objects, tenured for survivors. Would reduce per-cycle work for allocation-heavy programs.
- Hash function: alternative hash functions could reduce collision rates for specific workloads.
Comparison with Other Implementations
How rilua compares to PUC-Rio Lua, mlua, Luau, and lua-rs in architecture, performance, API design, and trade-offs.
Overview
| rilua | PUC-Rio Lua | mlua | Luau | lua-rs | |
|---|---|---|---|---|---|
| Language | Rust | C | Rust (FFI to C) | C++ | Rust |
| Lua Version | 5.1.1 | 5.1 - 5.5 | 5.1 - 5.5, Luau | 5.1 derivative | 5.5 |
| Type | Native interpreter | Reference impl | Binding layer | Native interpreter | Native interpreter |
| License | MIT | MIT | MIT | MIT | MIT |
| Dependencies | 0 | 0 (libc) | 7+ runtime | 0 (C++ stdlib) | 5 runtime |
| WASM | wasm32-unknown-unknown | No | wasm32-unknown-emscripten | No | wasm32-unknown-unknown |
| Unsafe Code | 0 in core VM/GC | N/A (C) | Extensive (FFI) | N/A (C++) | ~400 blocks |
| Nightly Rust | No | N/A | No | N/A | Yes |
Architecture
rilua
Pipeline: Lexer -> Parser -> AST -> Compiler -> Bytecode -> VM.
38 register-based opcodes matching PUC-Rio’s instruction set. Values
are a Rust enum (Val) with GcRef indices into typed arenas. Arena-based
incremental mark-sweep GC with generational indices. No unsafe code in
the VM, compiler, or GC. Errors propagate via Result<T, LuaError> –
no setjmp/longjmp, no panics in library code.
Protos (compiled function bodies) are reference-counted (Rc<Proto>)
rather than GC-managed, reducing GC traversal cost for immutable data.
PUC-Rio Lua
The reference implementation and baseline for all Lua interpreters.
Same 38-opcode register-based VM. Values are tagged unions (TValue)
with a mark-sweep GC using linked lists of GCObject pointers. Error
handling uses setjmp/longjmp. Written in ANSI C for portability.
PUC-Rio’s GC walks linked lists of heap-allocated objects. Each object carries color bits inline. The design prioritizes simplicity and portability over cache locality.
mlua
mlua is not a Lua implementation. It is a Rust binding layer over PUC-Rio’s C implementation (or LuaJIT, or Luau). The actual Lua execution happens in C/C++ code linked via FFI.
Every C API call that can trigger a Lua error is wrapped in
lua_pcall to prevent longjmp from unwinding Rust stack frames.
The library states it contains “a huge amount of unsafe code” to
bridge the C/Rust boundary. Users do not write unsafe in normal
usage, but the FFI boundary is inherently fragile.
When using the vendored feature, mlua compiles PUC-Rio’s C source
(or Luau’s C++ source) from lua-src-rs during build. This requires
a C/C++ compiler in the toolchain.
Luau
Roblox’s derivative of Lua 5.1 with a rewritten interpreter, optional native code generation (x64/ARM64), and a gradual type system. The bytecode format differs from PUC-Rio. The interpreter uses inline caching for table field access.
Luau removes several Lua 5.1 features for sandboxing: string.dump,
loadstring with bytecode, __gc metamethods, setfenv/getfenv.
It adds buffer (typed byte arrays), table.freeze, table.clone,
string interpolation, compound assignments, and continue.
lua-rs
CppCXY/lua-rs is an almost
one-to-one port of PUC-Rio’s C Lua 5.5 source to Rust. The module
structure mirrors the C codebase directly: lparser.c becomes
compiler/parser, lgc.c becomes gc/, ltable.c becomes
lua_value/lua_table/, and so on.
Pipeline: Lexer -> Parser -> Code Generator -> Bytecode -> VM (same as C Lua, no intermediate AST).
86 opcodes matching Lua 5.5’s instruction set with multiple formats
(iABC, iABx, iAsBx, iAx, isJ). Tri-color incremental and
generational GC ported from lgc.c, including object aging
(NEW/SURVIVAL/OLD), ephemeron cleanup, and __gc finalizers. String
interning with short/long string distinction (threshold: 40 bytes).
The project uses unsafe extensively (~400 blocks) for GC object
pointer manipulation, upvalue handling, and table internals – the
same operations that are pointer-based in C. No external C FFI; all
unsafe is internal Rust. Requires nightly Rust for
unchecked_shifts and other unstable features.
Runtime dependencies: ahash, rand, chrono, itoa, smol_str.
Optional: serde/serde_json for JSON support.
Performance
Relative Speed
| Implementation | vs PUC-Rio 5.1 (interpreted) | Notes |
|---|---|---|
| PUC-Rio Lua 5.1 | 1.0x (baseline) | C, -O2 |
| rilua | ~1.7x slower | Pure Rust, --release, 0 unsafe |
| lua-rs | ~1.2x slower | Rust, --release, nightly, ~400 unsafe |
| mlua | ~1.0x (wraps PUC-Rio) | FFI overhead at boundaries only |
| Luau (interpreted) | Faster than PUC-Rio | Optimized dispatch, inline caching |
| Luau (native codegen) | 1.5-2.5x faster than Luau interpreted | x64/ARM64 only |
Measured on AMD Ryzen 7 8840U, release builds, median of 10 runs. Sum of 20 individual PUC-Rio test files: PUC-Rio 696ms, rilua 1167ms, mlua 211ms (8 tests that passed).
rilua’s overhead comes from four areas: VM dispatch loop
(constructs.lua 2.26x), table hash traversal (nextvar.lua 2.0x),
compilation cost (verybig.lua 1.87x), and function call overhead
in sorting callbacks (sort.lua 1.76x). Tests that do not stress
these paths run at or near parity.
lua-rs’s closer-to-C performance reflects its one-to-one port
approach: the same pointer-based GC object layout, string interning
with flat bucket arrays, and unchecked bit shifts in the instruction
decoder. The unsafe blocks map to operations that are pointer
arithmetic in the original C. This preserves C Lua’s performance
characteristics at the cost of Rust’s safety guarantees in those
paths.
mlua adds minimal overhead because execution happens in PUC-Rio’s C VM. The FFI crossing cost exists at every Rust<->Lua boundary call but is small relative to VM execution time. Micro-benchmarks confirm mlua matches PUC-Rio within noise (1.0-1.2x).
Luau’s interpreter is faster than PUC-Rio through instruction-level optimizations, inline caching, and tuned memory allocation. With native code generation enabled, compute-heavy code sees an additional 1.5-2.5x speedup.
Where rilua is Competitive
For workloads dominated by string operations, pattern matching, file I/O, and simple control flow, rilua matches PUC-Rio. The overhead is concentrated in tight loops with many VM dispatch cycles, table iteration, and deep function call chains.
For embedding scenarios where Lua execution is a small fraction of total runtime (e.g., configuration evaluation, scripting hooks), the 1.7x factor is unlikely to be noticeable.
Micro-Benchmarks (All Implementations)
Minimal Lua scripts that run on incomplete implementations too:
| Test | PUC-Rio | rilua | lua-rs | mlua | lua-in-rust |
|---|---|---|---|---|---|
| fib.lua (recursive fib(35)) | 670ms | 1751ms (2.61x) | 900ms (1.34x) | 652ms (1.01x) | 3784ms (5.85x) |
| loop.lua (1M iterations) | 6ms | 15ms (2.50x) | 7ms (1.17x) | 7ms (1.17x) | 22ms (3.67x) |
| tables.lua (100K insert+read) | 5ms | 9ms (1.80x) | 6ms (1.20x) | 5ms (1.00x) | 23ms (4.60x) |
| closures.lua (500K calls) | 14ms | 45ms (3.21x) | 19ms (1.36x) | 13ms (1.00x) | — |
| nested_loops.lua (1Mx1K) | 11ms | 28ms (2.55x) | 9ms (0.82x) | 11ms (1.22x) | 34ms (3.78x) |
Ratios are vs PUC-Rio. lua-in-rust could not run closures.lua
(runtime crash on upvalue access). lua-rs requires nightly Rust.
Benchmark script and runner: scripts/benchmark-implementations.sh.
Rust API
rilua
Trait-based API inspired by mlua’s design:
use rilua::vm::state::LuaState;
use rilua::{Lua, LuaApiMut, LuaResult};
let mut lua = Lua::new()?;
lua.set_global("x", 42)?;
let val: i32 = lua.global("x")?;
// Register a Rust function -- function pointer, not a closure.
fn add(state: &mut LuaState) -> LuaResult<u32> {
let a = state.check_number(1)?;
let b = state.check_number(2)?;
state.push_number(a + b);
Ok(1)
}
lua.register_function("add", add)?;
// UserData
lua.create_typed_userdata::<MyType>(value)?;
Key characteristics:
- Handle types (
Table,Function,Thread,AnyUserData) areCopy– they are u32 indices into GC arenas IntoLua/FromLuatraits for type conversionRustFnisfn(&mut LuaState) -> LuaResult<u32>(push returns, return count)- GC control:
gc_collect(),gc_stop(),gc_step(), etc. - No lifetime parameters on handles – arena indices remain valid until the Lua state is dropped or GC collects the object
mlua
More feature-rich API with closures, async, scoped borrows:
use mlua::prelude::*;
let lua = Lua::new();
lua.globals().set("x", 42)?;
let val: i32 = lua.globals().get("x")?;
// Closure-based function creation
let add = lua.create_function(|_, (a, b): (f64, f64)| {
Ok(a + b)
})?;
lua.globals().set("add", add)?;
// UserData via derive or trait impl
impl UserData for MyType {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("foo", |_, this, ()| Ok(this.value));
}
}
Key characteristics:
- Closures as Lua functions (captures state, not just
fnpointers) UserDatatrait with method/field registrationScopefor non-'staticborrows in callbacks- Async function support (
create_async_function) - Serde integration (
serialize/deserializeLua values) RegistryKeyfor persistent references across calls- Module authoring (
#[mlua::lua_module]proc macro)
lua-rs
Feature-rich API closer to mlua’s level, with closures, async, derive macros, and table builders:
use luars::{LuaVM, Stdlib, LuaValue, TableBuilder};
use luars::lua_vm::SafeOption;
let mut vm = LuaVM::new(SafeOption::default());
vm.open_stdlib(Stdlib::All)?;
// Execute and get results
let results = vm.execute("return 1 + 2")?;
// Register a Rust function
vm.register_function("add", |state| {
let a = state.get_arg(1).unwrap();
let b = state.get_arg(2).unwrap();
// ...
Ok(1)
})?;
// UserData via derive macro
#[derive(LuaUserData)]
struct Point { x: f64, y: f64 }
vm.register_type_of::<Point>("Point")?;
// Async support
vm.register_async("fetch", |args| async move {
Ok(vec![AsyncReturnValue::string("result".into())])
})?;
Key characteristics:
LuaVMas the main state objectLuaValueenum for Lua valuesTableBuilderfor fluent table constructionregister_type_of/register_enumfor exposing Rust typesregister_asyncwith coroutine-based bridgingLuaError(1-byte enum) with optionalLuaFullError(rich messages)- Optional Serde support for JSON conversion
- Requires nightly Rust
API Comparison
| Feature | rilua | mlua | lua-rs |
|---|---|---|---|
| Function registration | fn pointers | Closures (captures) | Closures (captures) |
| UserData | create_typed_userdata | UserData trait + derive | LuaUserData derive |
| Async | No | Yes (async feature) | Yes (coroutine bridging) |
| Serde | No | Yes (serde feature) | Yes (serde feature) |
| Scoped borrows | No | Yes (Scope) | No |
| Module authoring | No | Yes (proc macro) | No |
| Handle model | Copy indices | Reference-counted | Value-based |
| Error type | LuaError enum | mlua::Error enum | LuaError / LuaFullError |
| Dependencies | 0 | 7+ | 5 |
| C compiler needed | No | Yes (vendored) or system Lua | No |
| Nightly Rust | No | No | Yes |
rilua’s API is intentionally smaller. It covers the embedding use case (create state, load code, call functions, exchange data) without the framework features mlua and lua-rs provide. The trade-off is fewer capabilities but zero external dependencies and no C toolchain requirement.
lua-rs sits between rilua and mlua in API richness: it offers closures, async, derive macros, and Serde – features rilua lacks – but without mlua’s scoped borrows or module authoring support. Unlike mlua, it requires no C compiler but does require nightly Rust.
Safety
Memory Safety
| rilua | mlua | lua-rs | Luau | |
|---|---|---|---|---|
| Unsafe in core | None | Extensive (FFI) | ~400 blocks | N/A (C++) |
| User-facing unsafe | None | None (normal usage) | None | N/A |
| Error model | Result<T> | Result<T> (wraps longjmp) | Result<T> | longjmp (C++) |
| GC safety | Generational indices | C GC + prevent-collection guards | GC object pointers (unsafe) | C++ GC |
| Use-after-free | Impossible (index validation) | Possible if guards misused | Possible (unsafe internals) | Possible (C++) |
rilua’s arena-based GC uses generational indices: each arena slot has
a generation counter incremented on free. A GcRef stores both the
slot index and the generation it was created with. Accessing a freed
slot returns an error rather than corrupted data. This provides
use-after-free protection without unsafe code.
lua-rs’s GC is a direct port of C Lua’s lgc.c. GC objects are
heap-allocated and managed through raw pointers, mirroring the C
implementation’s GCObject* linked lists. The ~400 unsafe blocks
are concentrated in GC traversal, object dereferencing, string
interning, and table internals. No external C FFI is involved – all
unsafe is internal pointer manipulation for performance parity
with C. The user-facing API does not expose unsafe.
mlua wraps every error-capable C API call in lua_pcall to catch
longjmp. This prevents Rust stack unwinding but adds overhead and
complexity. The library acknowledges the approach cannot guarantee
100% safety due to the fundamental tension between longjmp and
Rust’s ownership model.
Sandboxing
Luau has the strongest sandboxing story: no bytecode loading, no
__gc metamethods, restricted collectgarbage, per-script global
isolation (safeenv), and a VM interrupt mechanism for terminating
runaway scripts.
rilua and PUC-Rio Lua 5.1 expose string.dump, loadstring with
bytecode, __gc metamethods, and setfenv/getfenv. These are
faithful to the Lua 5.1 specification but provide less isolation
than Luau.
Send/Sync (Thread Safety)
| rilua | mlua | lua-rs | |
|---|---|---|---|
| Default | !Send, !Sync | !Send, !Sync | !Send, !Sync |
| With feature | Send (feature = send) | Send + Sync (feature = send) | No Send support |
| Mechanism | GcRef is u32 (trivially Send) | Reentrant mutex around VM | N/A |
| Overhead | None (index-based handles) | Lock acquisition on every access | N/A |
| Constraint | UserData: Send required | UserData: Send required | N/A |
rilua’s send feature works because GcRef<T> values are plain
u32 indices – they contain no pointers and are trivially Send.
The Lua struct gets unsafe impl Send gated on the feature flag.
There is no synchronization overhead.
mlua’s send feature wraps the Lua VM in a parking_lot reentrant
mutex, making it Send + Sync. Every VM access acquires the lock.
This is correct but adds per-operation overhead.
lua-rs does not provide a Send feature. Its GC uses raw pointers
internally, which would require additional safety analysis to make
Send-compatible.
Neither rilua nor mlua makes concurrent access safe without external
synchronization. Both assume single-threaded access to the Lua
state and use Send to allow moving the state between threads.
Feature Differences
What rilua Has That Others Do Not
- Zero dependencies: no C compiler, no system libraries, no
pkg-config, no nightly Rust - No unsafe in core: the VM, GC, compiler, and stdlib contain
zero
unsafeblocks - Behavioral equivalence with PUC-Rio 5.1: bytecode-compatible, same 38 opcodes, same GC states, same stdlib edge cases
Sendsupport: feature-gatedSendfor multi-threaded embedding (no overhead, no mutex)
What lua-rs Has That rilua Does Not
- Lua 5.5: latest language features (generational GC modes,
bitwise ops, integers,
goto, UTF-8 library) - Closer-to-C performance: ~1.2x PUC-Rio vs rilua’s ~1.7x
- Closure-based function creation: captures arbitrary state
- Async support: coroutine-based async Rust function bridging
- Serde integration: Lua to/from JSON
- UserData derive macro:
#[derive(LuaUserData)]with#[lua_methods] - Published crate: available on crates.io
What mlua Has That rilua and lua-rs Do Not
- Multiple Lua versions: 5.1 through 5.5, LuaJIT, Luau
- Scoped borrows: non-
'staticreferences in callbacks - Module authoring: build
.so/.dllloadable by Lua - PUC-Rio C performance: execution at native C speed
Send + Sync: mutex-based thread safety (with overhead)
What Luau Has That the Rest Lack
- Gradual type system: optional type annotations with inference
- Native code generation: AOT compilation for x64/ARM64
buffertype: typed byte array operations- String interpolation: backtick template literals
table.freeze/table.clone: immutable tables, shallow copy- Compound assignments:
+=,-=,*=,/=,..= continuestatement: in loops- Sandbox isolation: per-script globals, VM interrupts
When to Use What
Use rilua When
- You need Lua 5.1.1 behavioral equivalence (WoW addon compatibility, legacy Lua code)
- You want zero external dependencies and no C toolchain
- You are targeting
wasm32-unknown-unknown - Memory safety guarantees in the interpreter matter (no unsafe in core)
- You need
Sendsupport for multi-threaded embedding - The embedding scenario has modest performance requirements (scripting hooks, configuration, game logic at moderate scale)
Use lua-rs When
- You want Lua 5.5 features in a pure-Rust interpreter
- Performance close to C Lua matters and you accept internal
unsafe - You want async Rust function support and Serde integration
- You want derive macros for UserData
- Your build environment supports nightly Rust
- You do not need
Send/Syncthread safety
Use mlua When
- You need production-grade Lua at C execution speed
- You want async Rust integration with Lua coroutines
- You need Serde serialization of Lua values
- You are building a Lua module (
.so/.dll) rather than embedding - You need multiple Lua version support (5.1 through 5.5)
- Your build environment has a C compiler available
Use Luau When
- You need sandboxed execution of untrusted scripts
- Performance is critical (native codegen for hot paths)
- You want gradual typing for large Lua codebases
- You need Roblox ecosystem compatibility
- You can accept the divergence from standard Lua 5.1 semantics
(
setfenv/getfenvremoved, no__gc, nostring.dump)
Use PUC-Rio Lua Directly When
- You are writing C/C++ and do not need Rust integration
- You need the absolute reference behavior
- You are extending Lua at the C API level with existing C libraries