An embeddable, sandbox-first Lua 5.4 runtime for Go applications, with a small set of experimental 5.5 features. Pure Go, zero dependencies, no cgo.
Good fits include plugin systems, user scripting, game logic, automation, and controlled configuration runtimes.
- Full Lua 5.4 language support (with experimental 5.5 features)
- Coroutines with yield/resume
- Complete metamethod system (
__index,__newindex,__call,__close,__eq,__lt,__le,__add,__concat,__len,__tostring, etc.) - Module system (
require,package.loaded,package.preload,package.searchers,package.searchpath) - Pattern matching (
string.match,string.gsub,string.find,string.gmatch) - Binary data packing (
string.pack,string.unpack,string.packsize) - Function serialization (
string.dump/loadround-trip, Lua 5.4.8 binary format compatible) - Go-style glob matching (
glob.match,glob.match_words,glob.match_named) <const>and<close>variable attributes with to-be-closed support- Bitwise operators (
&,|,~,<<,>>) andbit32compat library - Integer division (
//) and full integer/float numeric model - UTF-8 library (
utf8.char,utf8.codepoint,utf8.codes,utf8.len,utf8.offset) - Deterministic
math.randomper VM instance (seeding one VM does not affect others) - Full debug library (
debug.getinfo,debug.traceback,debug.sethook,debug.getlocal,debug.setlocal,debug.getupvalue,debug.setupvalue,debug.upvalueid,debug.upvaluejoin) - Go interop (call Lua from Go, expose Go functions to Lua)
- Sandboxed code loading via
LuaCodeProvider(controlsdofile,loadfile, andrequire) - Sandboxed IO via
LuaIoProvider(includesJailedIoProviderfor read-only, directory-confined access) - Sandboxed OS via
LuaOsProvider(includesDefaultOsProviderwith optional env filtering) - Capability-gated channels for Go↔Lua message passing via
LuaChanProvider - Millisecond-precision timing via
LuaTimeProvider(time.now,time.since,time.tick) - Optional HTTP client module (
http.get,http.post,http.fetch) with automatic JSON coercion - Modern process execution via
LuaProcessProvider(exec.run,exec.spawnwith streaming I/O, stdin, kill, timed waits) - Output interception via
LuaPrintProvider(redirectprint()/warn()to logging, per-VM warn isolation) - Context cancellation and execution limits (call depth, stack, instructions)
- Source-level header directive parser (
directives.Parse) for embedder metadata like-- @tick 30s(non-standard, encoded as ordinary comments so source remains portable to reference Lua) - No cgo, no C dependencies, no shared object (.so/.dll) loading
- Single static binary when compiled
GoLua is available in two variants, published as separate Go module versions:
v1 — Lua 5.4.8 (master) |
v2 — Lua 5.5.0 (lua_5_5_0) |
|
|---|---|---|
| Import | github.com/iceisfun/golua |
github.com/iceisfun/golua/v2 |
| Install | go get github.com/iceisfun/golua |
go get github.com/iceisfun/golua/v2 |
| Lua compat | PUC-Rio 5.4.8 | PUC-Rio 5.5.0 |
| Binary format | Lua 5.4 bytecode | Lua 5.5 bytecode |
- Battle-tested language spec with the widest ecosystem of Lua libraries and documentation
- Binary chunk format matches PUC-Rio 5.4.8
- Stable API — no breaking changes planned
- Best choice when you need maximum compatibility with existing Lua code
- Latest language features:
globaldeclarations, named varargs (... name),local<const>/local<close>prefix syntax,table.create,debug.getinfoextraargsfield - Stricter compile-time checking (undeclared globals are errors unless opted out)
- Removed deprecated math functions (
atan2,cosh,sinh,tanh,log10,pow) LUA_PATH_5_5/LUA_CPATH_5_5env var precedence for module paths- Best choice for new projects that don't need to load existing 5.4 bytecode or libraries that depend on removed APIs
Both versions share the same provider architecture, sandbox model, and Go interop patterns. Code using the embedding API migrates between versions with only import path changes.
# Lua 5.4.8
go get github.com/iceisfun/golua
# Lua 5.5.0
go get github.com/iceisfun/golua/v2# Lua 5.4.8
go install github.com/iceisfun/golua/cmd/lua@latest
go install github.com/iceisfun/golua/cmd/luac@latest
# Lua 5.5.0
go install github.com/iceisfun/golua/v2/cmd/lua@latest
go install github.com/iceisfun/golua/v2/cmd/luac@latestGoLua is pure Go and does not require cgo.
package main
import (
"fmt"
"log"
"github.com/iceisfun/golua/compiler"
"github.com/iceisfun/golua/parser"
"github.com/iceisfun/golua/stdlib"
"github.com/iceisfun/golua/vm"
)
func main() {
source := `return 1 + 2`
block, err := parser.Parse("example", source)
if err != nil {
log.Fatal(err)
}
proto, err := compiler.Compile("example", block)
if err != nil {
log.Fatal(err)
}
v := vm.New()
stdlib.Open(v)
results, err := v.Run(proto)
if err != nil {
log.Fatal(err)
}
fmt.Println(results[0].AsInt())
}Core embedding flow: parser.Parse -> compiler.Compile -> vm.New -> stdlib.Open -> v.Run.
vm.New() defaults to context.Background(). Use vm.WithContext(ctx) to pass a custom context. Call v.Close(ctx) when done to shut down any providers that implement Shutdownable.
go run ./cmd/lua script.lua
go run ./cmd/lua -e "print(1 + 1)"
go run ./examples/basic
go test ./...
go test ./tests/...GoLua is organized into layered packages with no circular dependencies:
Source → Lexer → Parser → AST → Compiler → Proto (bytecode)
↓
VM (executes bytecode)
↑ ↑
stdlib Providers
| Package | Purpose |
|---|---|
lexer |
Tokenizes Lua source into a stream of tokens |
parser |
Parses tokens into an AST |
ast |
Abstract syntax tree node definitions |
compiler |
Compiles AST into Lua 5.4 bytecode (Proto) |
vm |
Executes bytecode, manages stack/coroutines, defines Value and LuaTable |
stdlib |
Registers standard library functions (string, math, table, etc.) |
check |
Static diagnostics for editor integration |
glob |
Go-style pattern matching (non-standard extension) |
stdlib/http |
Optional HTTP client module (non-standard extension) |
Provider interfaces (vm package) control host-system access:
| Interface | Controls | Implementation |
|---|---|---|
LuaCodeProvider |
dofile, loadfile, require file searcher |
DirCodeProvider |
LuaIoProvider |
io.* file operations |
JailedIoProvider, FullIoProvider |
LuaOsProvider |
os.* core (clock, time, date, getenv, setlocale) |
DefaultOsProvider |
LuaExecProvider |
os.execute command execution |
DefaultExecProvider |
LuaExitHandler |
os.exit VM termination |
DefaultExitHandler |
LuaDebugProvider |
debug.* capability gating |
DefaultDebugProvider |
LuaChanProvider |
chan.* Go↔Lua channels |
DefaultChanProvider |
LuaTimeProvider |
time.* millisecond timing |
DefaultTimeProvider |
LuaPrintProvider |
print()/warn() output routing |
DefaultPrintProvider |
LuaProcessProvider |
exec.* process spawning and streaming |
DefaultProcessProvider |
LuaLoadLibProvider |
package.loadlib native module hook |
custom host implementation |
All provider interface methods receive ctx context.Context as their first parameter, carrying the VM's context for cancellation and deadline propagation. Providers may optionally implement Initializable or Shutdownable (called when set on a VM / by vm.Close(ctx)) for lifecycle management.
See the examples/ directory for complete examples:
- basic - minimal Lua execution from Go
- expose_go - expose Go functions and tables to Lua
- call_lua - call Lua functions from Go and read results back
- capture_output - capture
print()output in-memory - print_provider - route
print()andwarn()through host logging
- code_provider - sandboxed file/module loading with
LuaCodeProvider - jailed_io - confined filesystem and OS access
- table -
LuaTableinterop and deterministic iteration - chan - Go<->Lua channels with
chan.select - glob - Go-style pattern matching from Go and Lua
- exec - process execution, streaming I/O, stdin, and timeouts
- time - millisecond timing with
time.now,time.since,time.tick, andtime.once - http - optional HTTP client module with a dedicated example runner
- check - diagnostics as JSON for editor integrations
- editor - Monaco editor with live diagnostics and sandboxed execution
- editor_advanced - browser IDE with completion, hover, diagnostics, and execution
- expose_object - Go-backed objects with an explicit adapter layer
- context - context cancellation stops a runaway Lua script
- directives - parse
@-prefixed metadata from a Lua source header (non-standard, source-level) - directive_loader - directive-driven script loader: scan a directory, honor
@disabled/@scope/@tick, then run
v := vm.New()
stdlib.Open(v)
// Register a Go function
v.SetGlobal("add", vm.NewNativeFunc(func(v *vm.VM) int {
a := v.Get(1).AsInt()
b := v.Get(2).AsInt()
v.Set(0, vm.NewInt(a + b))
return 1 // number of return values
}))// Get a Lua function
fn := v.GetGlobal("myFunction")
// Call it with arguments
results, err := v.ProtectedCall(fn, []vm.Value{
vm.NewInt(10),
vm.NewString("hello"),
})Implement LuaCodeProvider to control what Lua can load. All provider interface methods take ctx context.Context as their first parameter:
type MyProvider struct{}
func (p *MyProvider) LoadChunk(ctx context.Context, name string, caller *vm.LuaCallerContext) ([]byte, string, error) {
// Validate and load the requested chunk
// Return source, display name, and error
}
func (p *MyProvider) Capabilities(ctx context.Context) vm.LuaLoaderCaps {
return vm.LuaLoaderCaps{
AllowDofile: true,
AllowLoadfile: true,
}
}
// Use it
v := vm.New()
if err := v.SetCodeProvider(&MyProvider{}); err != nil {
log.Fatal(err)
}
stdlib.Open(v)package.loadlib is always exposed but returns Lua's standard "absent" failure
triple by default. Standard C Lua modules (.so/.dll) are compiled against the
PUC-Rio C API (lua_State*, lua_push*, etc.) and cannot be loaded directly
into GoLua. Setting a LuaLoadLibProvider lets the host implement its own native
module strategy — for example, mapping module names to Go-implemented bindings or
using cgo to bridge platform-specific libraries.
type MyLoadLibProvider struct{}
func (p *MyLoadLibProvider) LoadLib(ctx context.Context, path, init string, caller *vm.LuaCallerContext) (vm.NativeFunc, string, string) {
// The host decides how to interpret path and init — they need not
// refer to actual .so/.dll files. Return errmsg + where to deny loading.
// where is one of "open", "init", or "absent".
if path == "mylib" && init == "luaopen_mylib" {
return func(v *vm.VM) int {
lib := vm.NewEmptyTable()
lib.SetString("greet", vm.NewNativeFunc(func(v *vm.VM) int {
v.Set(0, vm.NewString("hello from Go"))
return 1
}))
v.Set(0, vm.NewTable(lib))
return 1
}, "", ""
}
return nil, path + ": module not available", "absent"
}
v := vm.New()
if err := v.SetLoadLibProvider(&MyLoadLibProvider{}); err != nil {
log.Fatal(err)
}
stdlib.Open(v)v := vm.New()
// Read-only file access, confined to a directory
if err := v.SetIoProvider(vm.NewJailedIoProvider("/path/to/allowed/dir")); err != nil {
log.Fatal(err)
}
// OS functions (clock, time, date, getenv, setlocale)
if err := v.SetOsProvider(vm.NewDefaultOsProvider()); err != nil {
log.Fatal(err)
}
// Command execution (os.execute)
if err := v.SetExecProvider(vm.NewDefaultExecProvider()); err != nil {
log.Fatal(err)
}
// VM termination (os.exit)
if err := v.SetExitHandler(vm.NewDefaultExitHandler()); err != nil {
log.Fatal(err)
}
// Or restrict which env vars are visible
if err := v.SetOsProvider(vm.NewFilteredOsProvider(func(name string) bool {
return name == "USER" || name == "HOME"
})); err != nil {
log.Fatal(err)
}
stdlib.Open(v)The exec module provides modern process control beyond os.execute:
v := vm.New()
if err := v.SetProcessProvider(vm.NewDefaultProcessProvider()); err != nil {
log.Fatal(err)
}
stdlib.Open(v)-- Simple run with captured output
local result = exec.run("ls", "-al")
print(result.stdout)
-- Spawn for streaming and interaction
local p = exec.spawn("sort")
p:write("banana\napple\n")
p:close_stdin()
for line in p:readlines() do print(line) end
p:wait()
-- Shell mode with pipes
local r = exec.run_shell("cat /etc/hosts | grep localhost")See docs/exec.md for the full API reference.
The full Lua 5.4 debug library, gated by LuaDebugProvider capabilities:
v := vm.New()
if err := v.SetDebugProvider(vm.NewDefaultDebugProvider()); err != nil {
log.Fatal(err)
}
stdlib.Open(v)
// Lua now has: debug.getinfo, debug.traceback, debug.sethook, debug.gethook,
// debug.getlocal, debug.setlocal, debug.getupvalue, debug.setupvalue,
// debug.upvalueid, debug.upvaluejoin, debug.getmetatable, debug.setmetatable,
// debug.getregistryIndividual capabilities can be enabled/disabled via LuaDebugCaps fields (e.g. AllowSetHook, AllowSetLocal, AllowUpvalueJoin).
Route print() and warn() output through your logging infrastructure:
type LoggingProvider struct{ Name string }
func (p *LoggingProvider) Print(ctx context.Context, msg string) {
log.Printf("[%s] %s", p.Name, msg)
}
func (p *LoggingProvider) Warn(ctx context.Context, msg string) {
log.Printf("[%s] WARN: %s", p.Name, msg)
}
v := vm.New()
if err := v.SetPrintProvider(&LoggingProvider{Name: "inventory.lua"}); err != nil {
log.Fatal(err)
}
stdlib.Open(v)Without a provider, print() writes to stdout (or the WithCaptureOutput buffer) and warn() writes to stderr — matching standard Lua behavior.
The warn("@on")/warn("@off") flag is per-VM, so disabling warnings in one VM does not affect others.
Stop runaway scripts with cooperative context cancellation and execution limits:
// Cancel infinite loops via context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
v := vm.New(vm.WithContext(ctx))
stdlib.Open(v)
// Or use setters
v.SetContext(ctx)
// Limit call depth, stack size, and instructions
v.SetLimits(vm.Limits{
MaxCallDepth: 200, // Prevent deep recursion
MaxStackSlots: 10000, // Bound memory growth
MaxInstructions: 1000000, // Bound CPU (checkpoint visits)
MaxMetaDepth: 500, // Limit __index/__newindex chain depth (default: 2000)
CompilerLimits: compiler.CompilerLimits{
MaxVars: 200, // Max locals per function (default: 200)
MaxRegs: 249, // Max registers per function (default: 249, hard cap: 249)
MaxUpvals: 255, // Max upvalues per function (default: 255, hard cap: 255)
},
})
// Or configure metatable depth independently
v = vm.New(vm.WithMaxMetaDepth(500))
v.SetMaxMetaDepth(500) // runtime setter equivalentThe VM checks for cancellation at backedges (loop iterations), function calls, and tail calls. No per-instruction overhead is added unless MaxInstructions is set. Context and limits are inherited by coroutine VMs. Errors from limits are catchable by pcall.
MaxMetaDepth bounds the length of __index and __newindex table-to-table chains to prevent infinite loops from metatable cycles. The default is 2000, matching Lua 5.4's MAXTAGLOOP. A value of 0 means "use the default". Function metamethods are not affected by this limit.
CompilerLimits enforces Lua 5.4 compile-time limits on locals, registers, and upvalues per function. These apply to load() and dofile() calls within the VM. Zero values use Lua 5.4 defaults. Limits can also be passed directly to compiler.Compile() via compiler.WithLimits().
Channels provide safe, capability-gated communication between Go and running Lua scripts. No goroutines or shared memory are exposed to Lua.
// Create a provider and channel on the Go side
provider := vm.NewDefaultChanProvider()
events := provider.NewChannel(0) // unbuffered
// Set up the VM
v := vm.New()
if err := v.SetChanProvider(provider); err != nil {
log.Fatal(err)
}
stdlib.Open(v)
// Pass the channel into Lua
v.SetGlobal("events", stdlib.WrapChannel(v, events))
// Go goroutine sends events
go func() {
events.Send(nil, vm.NewString("hello from Go"))
events.Close()
}()
v.Run(proto) // Lua reads with events:recv()Lua API:
| Function | Description |
|---|---|
chan.make(size?) |
Create a new channel (0 = unbuffered) |
chan.select(ch1, ..., timeout?) |
Receive from any ready channel; returns idx, val, ok |
ch:send(val) |
Blocking send (panics on interrupt or closed channel) |
ch:recv() |
Blocking receive; returns val, ok (ok=false when closed and drained) |
ch:close() |
Close the channel (panics if already closed) |
ch:try_send(val) |
Non-blocking send; returns bool |
ch:try_recv() |
Non-blocking receive; returns val, ok, received |
chan.select returns a 1-based index of the channel that fired, or 0 on timeout. Blocking operations respect context cancellation and call CheckInterrupt() after waking.
The chan table is absent by default (chan == nil). It only appears when the host sets a LuaChanProvider before calling stdlib.Open(). Channels from different providers are rejected by chan.select (VM boundary safety). The convenience function stdlib.ProvideChan(v) sets up a DefaultChanProvider and opens the module in one call.
Millisecond-precision timing for benchmarking and periodic triggers:
v := vm.New()
if err := v.SetTimeProvider(vm.NewDefaultTimeProvider()); err != nil {
log.Fatal(err)
}
stdlib.Open(v)local start = time.now() -- current time in ms
-- ... work ...
print(time.since(start) .. "ms") -- elapsed ms
-- periodic trigger: true once per interval, false otherwise
for i = 1, math.huge do
if time.tick(1000) then print("once per second") end
end
-- explicit key (shared across callsites)
if time.tick("heartbeat", 500) then send_heartbeat() end
-- one-time initialization guard
if time.once() then load_resources() end| Function | Description |
|---|---|
time.now() |
Current time in milliseconds (integer) |
time.since(t) |
Milliseconds elapsed since t |
time.tick([name,] ms) |
Returns true once per ms interval, false otherwise |
time.once([name]) |
Returns true on the first call for a given key, false on all subsequent calls |
time.tick and time.once are GoLua extensions (not part of standard Lua). When name is omitted, both functions auto-key by callsite — the VM inspects the calling function's source file and line number (source:line) so each call location gets independent state. Pass an explicit name string to share state across call locations. The time table is absent by default and only appears when the host sets a LuaTimeProvider.
A common embedder pattern is annotating Lua scripts with host-meaningful metadata in their header — scheduler intervals, scope names, enable/disable flags, registration hints. The directives sub-package factors that out into a single source-level parser:
import "github.com/iceisfun/golua/directives"
f, _ := directives.Parse(source)
if f.Has("disabled") { return }
tick, _ := f.Get("tick") // "30s"
scope, _ := f.Get("scope") // "alias_expander"
imports := f.Lookup("import") // ["shared/util", "shared/log"]-- @tick 30s
-- @scope alias_expander
-- @disabled
-- @import shared/util
-- @import shared/log
local function run() return 42 end
return run()| Method | Description |
|---|---|
Parse(src) |
Returns *File with the parsed header (always non-nil) |
(*File) Get(k) |
Last value for key k (last-wins for repeated keys); flags return ("", true) |
(*File) Has(k) |
Whether key k was present at least once |
(*File) Lookup(k) |
Every value for key k, in source order |
(*File) Keys() |
Distinct keys in first-occurrence order |
(*File) All() |
Range-over-func iterator over every (key, value) pair, including duplicates |
Header directives (-- @key value) are a golua-specific extension for embedders. They are not part of the Lua language as specified by Lua 5.4 / Lua 5.5; the reference Lua interpreter sees them as ordinary comments. This is by design:
- Reference Lua executes the same source unchanged. A
.luafile with a directive header runs identically underlua/lua5.5.0and under GoLua. - The lexer, parser, compiler, and VM are unaffected. Directives are scanned by a standalone source-level parser; there is no grammar change, no new tokens, no new AST nodes, and no bytecode change.
- Stripped / source-less execution is unaffected. Directives never enter the bytecode pipeline; a precompiled
*compiler.Protocarries no directive data.
The directives package has no opinion about which keys are valid — @tick, @scope, @disabled, @import are embedder conventions, not GoLua features. The parser identifies header-position directives only (a contiguous prefix of shebang, blank lines, and -- short comments); long comments (--[[ ]]) terminate the header without being scanned.
See examples/directives for a minimal API demo and examples/directive_loader for the realistic embedder pattern of scanning a directory of scripts and applying policy from their headers.
Tables implement the LuaTable interface, which is the contract used by the VM and stdlib:
type LuaTable interface {
Get(key Value) Value
Set(key Value, val Value) error
Delete(key Value) error
Next(key Value) (nextKey Value, val Value, err error)
Len() int
Metatable() LuaTable
SetMetatable(mt LuaTable)
IsThread() bool // Whether this table represents a coroutine thread
VMRef() *VM // Coroutine VM reference, or nil
}LuaTable.Get() performs raw access (equivalent to Lua's rawget()) — it does not invoke __index metamethods. To get Lua-style indexing that walks the __index chain, use v.TableGet(tbl, key) or v.TableGetInt(tbl, n) instead. Similarly, v.SetIndexValue() respects __newindex. This distinction matters when reading from class instances created via setmetatable.
The default *Table implementation uses an ordered keys slice for the hash part, so next()/pairs() iteration is deterministic (insertion-ordered). This avoids the non-deterministic range map behavior in Go. Mutation during iteration is not safe; inserting or deleting keys may skip entries or produce duplicates. Tables have no implicit thread safety -- concurrent read+write requires external synchronization.
stdlib.Open(v) registers all standard modules. Capability-gated modules only appear when their provider is set.
Standard Lua modules are available by default. GoLua-specific extensions and host-facing capabilities include glob, chan, time, exec, and the separate stdlib/http module.
| Module | Requires Provider | Description |
|---|---|---|
string |
No | Pattern matching, formatting, byte manipulation, pack/unpack, dump |
math |
No | Math functions with per-VM deterministic random |
table |
No | Table manipulation (sort, concat, insert, remove, move, pack, unpack) |
coroutine |
No | Coroutine creation and control |
utf8 |
No | UTF-8 encoding/decoding (strict mode) |
bit32 |
No | Lua 5.2 bitwise compat library |
glob |
No | Case-insensitive Go-style pattern matching (match, match_words, match_named) |
io |
LuaIoProvider |
File I/O (absent by default) |
os |
LuaOsProvider |
OS core: clock, time, date, difftime, getenv, setlocale (execute/exit/rename also require their own providers) |
package |
No* | Module system: require, package.loaded, package.preload, package.searchers, searchpath |
debug |
LuaDebugProvider |
Full Lua 5.4 debug library: getinfo, hooks, locals, upvalues (absent by default) |
chan |
LuaChanProvider |
Go↔Lua message passing channels (absent by default) |
time |
LuaTimeProvider |
Millisecond timing: now, since, periodic tick (absent by default) |
exec |
LuaProcessProvider |
Process execution: run, spawn, streaming I/O, stdin, kill (docs) |
http |
Separate module | HTTP client: get, post, put, patch, delete, fetch (docs) |
* package.loadlib is nil by default; set LuaLoadLibProvider to provide host-defined native module loading.
GoLua is sandboxed by default. The VM starts with no access to the host system. Capabilities are granted explicitly by the host via providers.
A fresh vm.New() instance has no file, network, process, or debug access unless the host opts in.
┌───────────────────────────────────────────────────────────────────────────────────────┐
│ Host (Go) │
│ │
│ ┌────────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ ┌────────────┐ ┌──────────┐ │
│ │CodeProvider│ │IoProvider│ │OsProvider│ │DebugProvider│ │ChanProvider│ │ Time │ │
│ │ (optional) │ │(optional)│ │(optional)│ │ (optional) │ │ (optional) │ │(optional)│ │
│ │ │ │ │ │ExecProv. │ │ │ │ │ │ │ │
│ │ │ │ │ │ExitHndlr │ │ │ │ │ │ │ │
│ │ │ │ │ │ProcessP. │ │ │ │ │ │ │ │
│ └─────┬──────┘ └────┬─────┘ └────┬─────┘ └──────┬──────┘ └─────┬──────┘ └────┬─────┘ │
│ │ │ │ │ │ │ │
│ ┌─────▼─────────────▼────────────▼──────────────▼──────────────▼─────────────▼──┐ │
│ │ VM (sandbox) │ │
│ │ │ │
│ │ string, math, table, coroutine, utf8, bit32, glob, package │ │
│ │ io*, os*, debug*, chan*, time*, exec* (* = provider-gated) │ │
│ │ require → CodeProvider (Lua file searcher) │ │
│ │ print/warn → PrintProvider (or stdout/stderr fallback) │ │
│ └───────────────────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────────────────────┘
- No filesystem access unless explicitly provided
- No OS access unless explicitly provided
- No native code loading (C Lua modules are incompatible; host can provide Go-native bindings via
LuaLoadLibProvider) - No ambient authority
The default IO provider (JailedIoProvider) enforces:
- root confinement
- read-only access
- path traversal prevention
go test ./... # All tests
go test ./tests/... # Lua script tests only
go test -v ./tests/... # Verbose output├── ast/ # Abstract syntax tree definitions
├── check/ # Lua source diagnostics for editor integration
├── compiler/ # Bytecode compiler
├── docs/ # Additional documentation
├── glob/ # Go-style glob matching package, diverges from standard lua
├── lexer/ # Lexical analyzer
├── parser/ # Lua parser
├── stdlib/ # Standard library implementation
├── token/ # Token definitions
├── vm/ # Virtual machine
├── tests/ # Lua test files
├── examples/ # Usage examples
└── cmd/
├── ast/ # AST printer/debugger CLI
├── lua/ # Lua interpreter CLI
└── luac/ # Bytecode compiler CLI
# Run a Lua script
go run ./cmd/lua script.lua
# Execute inline code
go run ./cmd/lua -e "print(1 + 1)"
# Run with a 500ms execution timeout
go run ./cmd/lua --timeout 500 script.lua
# Pass arguments to a script (available as `arg[1]`, `arg[2]`, ...)
go run ./cmd/lua script.lua foo bar
# Run with test mode (enables debug provider, jailed IO, code provider)
go run ./cmd/lua --test script.lua
# Compile and show bytecode
go run ./cmd/luac script.luaCurrent compatibility notes and intentional boundaries:
- The CLI enables a practical host-facing set of providers by default:
DefaultOsProvider,FullIoProvider,DirCodeProvider,DefaultExecProvider,DefaultExitHandler, andDefaultDebugProvider.--testswitches to a more test-like environment withJailedIoProvider,DirCodeProvider, andDefaultDebugProvider, without enablingos.executeoros.exit. - No C module loading — standard C Lua modules (.so/.dll) are compiled against the PUC-Rio C API and are not compatible with GoLua's VM.
package.loadlibis nil by default; setting aLuaLoadLibProviderlets the host provide Go-native bindings or cgo-bridged libraries under the same API surface.requireloads Lua modules viaLuaCodeProvider. - No
io.stdin/io.stdout/io.stderrin the library by default (the CLI atcmd/luaprovides full stdio via its environment, butvm.New()does not to maintain the sandbox) - No
io.writeinJailedIoProvider(read-only by design; useFullIoProviderfor read-write access) - Binary chunk format is compatible with Lua 5.4.8 —
load(string.dump(f))round-tripping works, and chunks dumped by GoLua can be loaded by reference Lua 5.4.8 and vice versa. However, bytecode details may differ between compilers. os.setlocaleonly supports the"C"locale — Go has no native locale support. Queries return"C"and setting any other locale returnsnil.- GC behavior differs from C Lua — GoLua delegates garbage collection entirely to Go's runtime GC.
collectgarbage("collect")triggersruntime.GC()but Go's GC timing is non-deterministic, so tests that depend on exact finalization order or count may not pass. - VM instances are isolated but not safe for concurrent mutation from multiple goroutines without external synchronization.
PRs welcome. See CONTRIBUTING.md for guidelines. Run go test ./... before submitting.
MIT
