High performance native Lua compiler

clx is an ahead-of-time (AOT) native compiler that compiles Lua 5.5 source code into optimized machine code. By eliminating the need for a runtime interpreter or bytecode virtual machine, clx provides predictable execution time, minimal latency, and a reduced resource footprint.

clx transpiles Lua source code into optimized C++ code, which is then compiled using the system's C++ compiler to produce native machine code. This architecture provides performance up to 60x faster than standard Lua interpreters, thanks to static analysis and high performance optimizations.

clx generates standalone binaries with zero external dependencies. The entire toolchain is fully open source and released under the permissive MIT License.

Due to the static compilation model inherent to an AOT compiler, load(), dofile(), loadfile(), string.dump(), and the , debug library are not available. Dynamic code loading requires a runtime interpreter, and debug introspection requires runtime metadata that AOT compilation does not preserve.

Multiplatform
Lua compiler
C++20
Backend
MIT
Open Source License
Lua 5.5
Compatibility

Features

Native AOT
Compile Lua directly to optimized C++20 code. No interpreter layer, no bytecode overhead.
Zero Dependencies
Binaries with no runtime requirements. Ship a single file, run anywhere.
Lua 5.5 runtime
Lua 5.5 syntax and standard modules. (load/dofile/loadfile and debug lib excluded — AOT limitations)
Modern garbage collector
Incremental, shadow-stack-based Mark-and-Sweep collector.
Cross-Platform
Available on Linux, macOS, and Windows.
Optimizations
Optimizer using static analysis to speedup program execution.
C++20 Backend
SROA, SIMD vectorization, CPU cache friendly optimizations...
Extendable C++ API
Build third party modules using the clx C++ library

Installation

clx is available as source code (build from any platform) and as pre-built binaries for Linux (x86_64), macOS (ARM64), and Windows (x86_64) from GitHub Releases, built automatically via CI. Linux binaries require glibc ≥ 2.39. This page explains how to build and install the clx binary and runtime library.

shmacOS / Linux
$ git clone https://github.com/samyeyo/clx $ cd clx $ ./build.sh install # clx compiler installed to /usr/local/bin # libclx.a library installed to /usr/local/lib
CMDWindows
> git clone https://github.com/samyeyo/clx > cd clx > build.bat install # clx.exe compiler installed to clx\bin # clx.lib and clx_size.lib installed to clx\lib

Verify your installation:

shVerify
$ clx --version clx 0.1.0 MIT License - Copyright (c) 2026 Tine Samir

Benchmarks

Performance comparison against popular Lua runtimes. The speedup is relative to Lua 5.5 interpreter. Benchmarks may vary depending on the C++ toolchain and environment.

Runtime Time Speedup

Getting Started

Build clx

Clone the repository and build from source:

shBuild
$ git clone https://github.com/samyeyo/clx $ cd clx $ ./build.sh # Debug build: ./build.sh debug # Install: ./build.sh install

Alternatively, build manually with CMake:

$ mkdir build $ cd build $ cmake .. -DCMAKE_BUILD_TYPE=Release $ cmake --build .

Compile Your First Program

Create hello.lua:

luahello.lua
print("Hello, World!")

Compile and run:

$ ./build/clx hello.lua $ ./hello Hello, World!

Your Second Program

Let's try something more interesting:

luafib.lua
-- fib.lua function fib(n) if n <= 1 then return n end return fib(n - 1) + fib(n - 2) end print("Fibonacci(20) = " .. fib(20))

Compile it with --fast flag for better performances:

$ ./build/clx --fast fib.lua $ ./fib Fibonacci(20) = 6765

Language Features

clx supports most Lua 5.5 features including variables, control flow, functions, tables, metatables, coroutines, standard libraries, and bitwise operations. Due to the AOT compilation model, load(), dofile(), loadfile(), string.dump(), and the debug library are not available.

Variables and Types

lua
-- Numbers local x = 42 local pi = 3.14159 -- Strings local greeting = "Hello" local name = 'World' -- Booleans local flag = true -- Tables local t = { a = 1, b = 2 } local arr = { 1, 2, 3 } -- Functions local function add(a, b) return a + b end -- Closures local function counter() local n = 0 return function() n = n + 1 return n end end

Control Flow

lua
-- If/else if x > 10 then print("big") elseif x > 5 then print("medium") else print("small") end -- While loop while x > 0 do print(x) x = x - 1 end -- For loop (numeric) for i = 1, 10 do print(i) end -- For loop (generic) for k, v in pairs(t) do print(k, v) end -- Repeat/until repeat x = x - 1 until x == 0

Functions

lua
-- Basic function function greet(name) return "Hello, " .. name end -- Multiple return values function divmod(a, b) return math.floor(a / b), a % b end -- Variadic function sum(...) local total = 0 for i = 1, select("#", ...) do total = total + select(i, ...) end return total end -- Method syntax local obj = { value = 10 } function obj:double() self.value = self.value * 2 end

Tables and Metatables

lua
-- Table with methods local vector = { x = 0, y = 0, add = function(self, other) return { x = self.x + other.x, y = self.y + other.y } end, __tostring = function(self) return "(" .. self.x .. "," .. self.y .. ")" end } -- Metatable for operator overloading setmetatable(vector, { __add = function(a, b) return a:add(b) end }) local v1 = { x = 1, y = 2 } local v2 = { x = 3, y = 4 } local v3 = v1 + v2 -- Uses __add

Coroutines

lua
-- Producer/consumer with coroutines local function producer(max) for i = 1, max do coroutine.yield(i) end end local function consumer() local co = coroutine.create(producer) while true do local status, value = coroutine.resume(co) if not status or value == nil then break end print("Received: " .. value) end end consumer()

String Module

lua
-- Basic operations local s = "Hello, World!" print(string.len(s)) -- 13 print(string.sub(s, 1, 5)) -- Hello print(string.upper(s)) -- HELLO, WORLD! print(string.lower(s)) -- hello, world! print(string.reverse(s)) -- !dlroW ,olleH -- Character conversion print(string.byte("A")) -- 65 print(string.char(65, 66, 67)) -- ABC -- Repetition print(string.rep("ab", 3)) -- ababab print(string.rep("x", 5, "-")) -- x-x-x-x-x -- Format print(string.format("Pi: %.2f", 3.14159)) -- Pi: 3.14 -- Pattern matching local start, finish = string.find("hello world", "world") print(start, finish) -- 7 11 local match = string.match("hello world", "(%a+)") print(match) -- hello for word in string.gmatch("hello world from lua", "%a+") do print(word) end local result, count = string.gsub("hello world", "world", "lua") print(result, count) -- hello lua, 1

Bitwise Operations

lua
-- Bitwise AND, OR, XOR print(0xFF & 0x0F) -- 15 print(0xF0 | 0x0F) -- 255 print(0xFF ~ 0xF0) -- 15 -- Bitwise shifts print(1 << 8) -- 256 print(256 >> 4) -- 16 -- Bitwise NOT print(~0) -- -1

Performance Tips

Use local variables, prefer numeric for loops, and avoid mixing types for optimal performance.

luaTips
-- Good: local variables are faster local function compute() local result = 0 for i = 1, 1000 do local temp = i * 2 result = result + temp end return result end -- Prefer numeric for loops for i = 1, 1000000 do -- body end -- Avoid mixed types: 1 + "2" is slower than 1 + 2

Common Issues

Debugging Compilation Errors

If you get a C++ compilation error, you can see the generated code:

$ clx script.lua --cpp # This creates script.cpp — examine it to see what's being generated

Understanding Runtime Errors

Runtime errors show the Lua line where the error occurred:

Error: script.lua:10: attempt to perform arithmetic on a number value

The format is filename:line: message.

Next Steps

Read the Architecture, Optimizations, Runtime, and CLI documentation.

CLI Reference

Usage

$ clx [options] <file.lua> [<compiler-options>]

Options starting with - that are not recognized by clx are automatically passed through to the C++ compiler.

Build Mode

--executable Compile to executable (default) --object Compile to object file (.o/.obj) --static Compile to static module (.a/.lib)

Output Options

--output <name> Specify output file name

Compilation Options

--debug Enable debug symbols, disable optimizations; #line directives map debugger views to Lua source --size Optimize for size (default) --fast Optimize for speed --cpp Generate C++ source files, don't compile --minimal Exclude non-essential modules (string, table, io, os, math, utf8, coroutine); keeps base + package --modules <list> Precompiled modules to link (comma-separated)

Optimization Flags

Default flags are only applied when no compiler options are provided by the user.

Size Mode (--size, default)
# gcc/clang: -Os -flto=auto -fno-rtti -fvisibility=hidden -ffunction-sections -fdata-sections -Wl,--gc-sections -s # MSVC: /O1 /GL /GR- /MD /EHsc /GS- /fp:fast /Gw /Gy /link /OPT:REF /OPT:ICF

DCE flags (-ffunction-sections -fdata-sections -Wl,--gc-sections) are added only for executables.

Fast Mode (--fast)
# gcc/clang: -O3 -flto=auto -fno-rtti -fvisibility=hidden -ffunction-sections -fdata-sections -Wl,--gc-sections -s # MSVC: /O2 /Ot /GL /GR- /MD /EHsc /GS- /fp:fast /Gw /Gy /link /OPT:REF /OPT:ICF

--size (default) links against libclx_size.a and reduces binary size by 36–40% vs --fast. Compute-heavy code can be 80–320% slower; table/IO-heavy code sees negligible difference. Use --fast when throughput matters more than size.

Debug Mode (--debug)
# gcc/clang: -O0 -g # MSVC: /Od /Zi /MDd /EHsc

If you provide any compiler options, no default flags are added — you get full control.

Platform-Specific

# Linux/macOS: Compiler: clang++ (falls back to g++) Executable: <name> (no extension) Object: <name>.o | Static: <name>.a | Dynmod: <name>.so # Windows: Compiler: clang++ (falls back to g++, then cl MSVC) Executable: <name>.exe Object: <name>.obj | Static: <name>.lib | Dynmod: <name>.dll

Examples

$ clx script.lua # Compile to executable $ clx script.lua --output myapp # Custom output name $ clx script.lua --size # Optimize for size (default) $ clx script.lua --fast # Optimize for speed $ clx script.lua --debug # Debug build — Lua source lines mapped via #line directives (GDB/LLDB) $ clx script.lua --cpp # Keep generated C++ source $ clx script.lua -O2 # Custom optimization flags $ clx script.lua --object # Object file output $ clx script.lua --static # Static library

Environment Variables

clx respects these environment variables:

CXX (Not currently read — compiler auto-detected)

Exit Codes

0 Success 1 Usage or compilation error 2 No C++ compiler found

Build with CMake

If building from source:

$ mkdir build $ cd build $ cmake .. $ make $ ./clx --help

Architecture

Overview

clx transpiles Lua source code into optimized C++, then compiles with the system C++ compiler to produce native machine code. This architecture provides performance up to 60x faster than standard Lua interpreters.

Compiler Pipeline

PipelineCompilation Stages
Lua Source
Lexer
Token Stream ▼
Parser
Abstract Syntax Tree ▼
Optimizer
Annotated AST ▼
Codegen
C++ Source ▼
C++ Compiler
(gcc / clang / cl)
Runtime (libclx.a)
Native Binary

Components

CLI — Handles argument parsing, file I/O, and invokes the C++ compiler. Auto-detects gcc, clang, or MSVC. Enables DCE via -ffunction-sections -Wl,--gc-sections (gcc/clang) or /Gy /link /OPT:REF /OPT:ICF (MSVC).

Lexer — Converts source to token stream (keywords, identifiers, literals, operators, delimiters).

Parser — Recursive descent parser builds an AST with statement, expression, block, and function nodes.

AST Nodes — Core node types: Block, Identifier, BinaryOp, UnaryOp, FunctionDef, TableConstructor, ForStatement, WhileStatement, IfStatement, CallExpression.

Optimizer — Analyzes the AST and annotates nodes with optimization hints: Numeric fast-path (direct C++ arithmetic), variable scope resolution, table purity analysis, constant folding preparation, shape version tracking for inline cache invalidation, yields_number analysis for numeric for loops.

Code Generator — Produces C++ code with fast-path for numeric operations, slow-path for dynamic operations, loop transformation, [[likely]] branch prediction hints, per-call-site CacheSlot inline caching, StringBuilder-based concatenation, and wyhash-based string hashing.

Runtime Library — Implements Lua semantics: core VM (GC, tables, metamethods, arithmetic, bitwise ops), base library, math library, coroutines, module loading, and string library.

Project Layout

Directoryclx Source Tree
clx/ ├── CMakeLists.txt # Build configuration ├── build.sh / build.bat # Convenience build scripts ├── include/ │ ├── clx.h # Public C++ API (value ctors, type queries, helpers) │ └── clx_runtime.h # Internal runtime (types, GC, tables, inline ops) ├── src/ │ ├── clx.cpp # Compiler driver / CLI │ ├── syntax/ │ │ ├── lexer.h/cpp # Tokenizer/scanner │ │ ├── parser.h/cpp # Recursive descent parser │ │ ├── nodes.h # AST node definitions │ │ └── visitor.h/cpp # AST visitor pattern │ ├── optimizer/ │ │ ├── optimizer.h # Optimization passes │ │ └── optimizer.cpp # Optimization implementation │ ├── codegen/ │ │ ├── codegen.h # Code generator interface │ │ └── codegen.cpp # C++ code emission (calls optimizer internally) │ └── runtime/ # Runtime library (libclx.a) │ ├── runtime.cpp # VM core (GC, types, state, metamethods) │ ├── base.cpp # Base module (print, error, type, pcall, etc.) │ ├── table.cpp # Table module (insert, remove, concat, sort, etc.) │ ├── math.cpp # Math module │ ├── strings.cpp # String module │ ├── coroutine.cpp # Coroutine module │ ├── io.cpp # I/O module │ ├── os.cpp # OS module │ ├── utf8.cpp # UTF-8 module │ ├── package.cpp # Package/module system │ └── openlibs.cpp # Standard modules loader ├── tests/ # End-to-end test suite ├── examples/ # Example clx projects using the C++ embedding API │ ├── mandelbrot/ # Mandelbrot viewer │ ├── pong/ # Pong game │ └── sokol/ # Sokol graphics module for clx ├── benchmarks/ # Performance benchmarks with comparisons └── doc/ # Comprehensive documentation

Data Flow

Compilation: Lua source → Lexing → Parsing → AST → Optimization → Annotated AST → Codegen → C++ source → Compilation → Native binary

Runtime: Initialization (LState, standard libraries) → Execution (compiled code with Numeric fast-path) → Fallback (Lua value representation for dynamic types) → Cleanup (GC)

Key Runtime Components

StringPool

Open-addressed hash map for string interning. Each slot owns a baked allocation: [uint32_t hash][uint32_t len][char data...\0]. LValue stores a pointer to the char data (8 bytes past alloc start). One probe on hit, no std::string, no side map, no double lookup. Supports intern_preallocated() for zero-allocation string concatenation.

wyhash

Fast, high-quality hash function used for table keys and string interning. Uses compile-time constant secrets with 128-bit multiply for excellent avalanche. For interned strings, the hash is baked into the allocation header, making lvalue_hash() a single 4-byte load.

For strings ≤8 bytes, swar_hash_8() replaces wyhash_str — loads all bytes into one register with a single memcpy and mixes via wyhash64. Used consistently for both TAG_ISTR inline strings and short interned strings so cross-type hash compatibility is maintained.

CacheSlot Inline Caching

Per-call-site cache slots for string-keyed table access. Each access site in the source gets one CacheSlot that caches the last table pointer and value. Uses shape_version to detect stale cached values after table writes. Only caches non-GC values to avoid dangling pointers after collection. States: valid/invalid based on table pointer and shape version match.

StringBuilder

O(n) string concatenation that avoids the O(n²) quadratic blow-up of repeated s = s .. part patterns. Uses inline storage for up to 8 parts, grows to heap allocation when needed. Produces a single interned string with baked hash on to_string().

Shape Version Tracking

Tables track a shape_version that increments on every write. CacheSlots check the version to detect stale cached values, preventing incorrect reads after table mutations.

Optimizations

Constant Folding

clx emits arithmetic expressions and delegates to the C++ compiler, which performs constant folding at compile time:

lua
local x = 1 + 2 + 3 -- Compiled as: local x = 6 local y = "hello" .. " " .. "world" -- Compiled as: "hello world"

Numeric Fast-Path

clx distinguishes between Integer and Number types in its nan-boxed value representation. The runtime LValue arithmetic functions dispatch to native double or int64 operations internally.

lua
-- Numeric literal: local i = 1 + 2 -- Compiled as: double arithmetic via LValue -- Float literal: local f = 1.5 + 2.3 -- Same LValue arithmetic path -- Mixed (promotes to double): local m = 1 + 2.5 -- Integer converted to double

Direct Arithmetic Fast-Path

When all operands are known to be numeric, clx generates direct C++ arithmetic instead of dynamic LValue dispatch:

lua
-- Slow path (dynamic): local result = a + b -- Uses clx::LValue arithmetic with type checks -- Fast path (numeric): local result = a + b -- When known numeric, uses add directly

Local Variable Optimization

Local variables that hold numbers are stored as unboxed C++ doubles:

lua
local function sum(n) local total = 0 -- Stored as double, not LValue for i = 1, n do total = total + i -- Direct double arithmetic end return total end

Inline Caching

Each string-keyed table access site gets a dedicated CacheSlot. On repeated access to the same table key, the cache skips the hash probe entirely. Shape version guards detect stale cached values after table mutations. Only non-GC values are cached to avoid dangling pointers.

String Optimizations

StringPool — Open-addressed hash map for string interning with one-probe-on-hit, baked hashes, and no std::string overhead.

Baked Hashes — For interned strings, the wyhash is baked into the allocation header. Reading the hash costs a single 4-byte load.

StringBuilder — O(n) string concatenation that avoids O(n²) quadratic blow-up. Produces a single interned string with baked hash.

wyhash — Fast, high-quality hash with compile-time constant secrets and 128-bit multiply (__uint128_t or _umul128 on MSVC).

Pre-Allocated Interningintern_preallocated() adopts a pre-formatted buffer directly into the StringPool, cutting string concat from 3 heap allocations to 1 (or 0 on pool hit).

Code Generation Optimizations

Loop Transformations — Numeric for loops transform to C++ for loops. Generic for loops emit direct LCFunction pointer calls.

Branch Prediction Hints — Fast paths annotated with [[likely]] attributes.

Inlining — Small functions inlined at compile time. All arithmetic operators are CLX_INLINE with always_inline.

SIMD Vectorization — C++ compiler can vectorize simple loops with -O3 -march=native. Add -mavx2 manually for AVX2-specific builds.

Dead Code Elimination-ffunction-sections -fdata-sections -Wl,--gc-sections (gcc/clang) or /Gy /link /OPT:REF /OPT:ICF (MSVC).

Link-Time Optimizations

When using -flto (gcc/clang) or /GL (MSVC), the compiler can inline across translation units, eliminate dead code across the entire program, and perform whole-program analysis. Enabled by default in release mode.

Runtime Optimizations

Table Pre-sizing — Tables with known structure are pre-allocated to the correct size.

Table Layout — Cache-line-optimized layout: all gettable fields fit in one 64-byte cache line.

Upvalue Fast-Path — Closure variables that aren't captured are stored directly, avoiding heap allocation.

Metamethod Caching — Frequently used metamethod strings are pre-interned at initialization.

Length Operator — String length read from baked header, avoiding strlen.

Optimization Levels

Lua source-level debugging. The generated C++ contains #line directives mapping each statement to the original .lua file and line, so GDB, LLDB, or MSVC debugger can step through the script.

Debug Mode (--debug)
# gcc/clang: -O0 -g # MSVC: /Od /Zi /MDd /EHsc
Release Mode (default)
# gcc/clang: -O3 -flto=auto -fno-rtti -fvisibility=hidden # MSVC: /O2 /Ot /GL /GR- /MD /EHsc /GS- /fp:fast /Gw /Gy

Compiler Remarks

GCC/Clang (Linux/macOS)

Default release flags:

-O3 -flto=auto -fno-rtti -fvisibility=hidden -ffunction-sections -fdata-sections -Wl,--gc-sections -s -ldl

MSVC (Windows)

Default release flags:

/O2 /Ot /GL /GR- /MD /EHsc /GS- /fp:fast /Gw /Gy /link /OPT:REF /OPT:ICF

Key MSVC optimizations: /GL (Whole program optimization / LTO), /OPT:REF (Remove unused functions), /OPT:ICF (Identical COMDAT folding), /Gy (Function-level linking), /fp:fast (Fast floating-point semantics).

Cross-Platform Tips

Use -O3 -march=native -flto=auto on gcc/clang for maximum performance. On MSVC, /O2 is the primary optimization flag; /GL enables link-time optimization. Both compilers support SIMD vectorization when loops are simple enough. Dead code elimination requires function-level linking on both platforms.

Runtime

Value System

clx uses nan-boxing to represent all Lua values in 64 bits with distinct types for Number (double), Integer (native int64), TAG_ISTR inline strings (≤5 bytes, no heap allocation), and pointers for tables, functions, threads, and userdata.

Value Layout64-bit NAN-Boxed Representation
Double (Numbers) 64-bit IEEE 754 floating-point format
Integer 48-bit signed payload + type tag
Pointer (GC Objects) 62-bit heap address references + 2-bit object tag (tables, closures, threads)
Inline String (TAG_ISTR) 16-bit tag prefix (0xFFF9 + len) + 48-bit immediate character sequence (≤ 5 Bytes)
Special System Literals Dedicated bit configurations representing nil, true, and false

Garbage Collection

Stop-the-world mark-and-sweep collector: mark phase traverses reachable objects from roots, sweep phase deallocates unreachable objects. Finalizers (__gc) are called before collection. Uses a reusable worklist vector to avoid repeated allocations.

Standard Libraries

Base Library — print, error, assert, type, tostring, tonumber, pairs, ipairs, next, pcall, xpcall, select, collectgarbage, setmetatable, getmetatable, rawequal, rawget, rawset, rawlen, warn, _VERSION

Math Library — Uses set_lazy_funcs for lazy registration via constexpr LazyReg[]. Functions created as LCFunction closures on first access, then cached on the table.

String Library — len, sub, reverse, lower, upper, rep, byte, char, format, find, match, gmatch, gsub, pack, unpack, packsize with full pattern matching support.

Coroutine Library — create, resume, yield, status, wrap using OS-level fibers/ucontext.

Table Library — insert, remove, concat, sort, unpack, pack, move.

Metamethods

__add __sub __mul __div __mod __pow __unm __len __eq __lt __le __concat __index __newindex __call __tostring __gc __metatable

CacheSlot Inline Caching

cppCacheSlot structure
struct CacheSlot { uint64_t table_val; // Cached table pointer (raw bits) uint32_t shape_version; // Table shape version for staleness detection LValue cached; // Cached value (non-GC only) bool valid; // Cache validity flag };

Per-call-site cache for string-keyed table access. Valid when table pointer matches, shape version hasn't changed, and cached value is not a GC object (avoiding dangling pointers). States: valid/invalid based on table pointer and shape version match.

Closures and Upvalues

clx supports lexical scoping with full closure capture:

  • Local variables captured by inner functions become upvalues
  • Shared upvalues (multiple closures sharing the same captured variable)
  • Loop variable capture (closures created in a for loop each capture the correct iteration value)
  • Triple nesting and arbitrary capture depth
  • Tail call optimization (TCO) for recursive calls — no stack growth

Goto and Labels

Full goto / ::label:: support with proper lexical scoping:

  • Forward and backward jumps
  • Duplicate labels in different scopes resolve correctly
  • Goto can create loops (backward jumps)

Memory Layout

Table Layout

RegionContents
LHeader (metadata)
type, marked, nextGC metadata
Cache line 0 (64 bytes) — all gettable fields here
array pointer8 bytes
array_size, array_cap16 bytes
bucket, hash_size24 bytes
metatable, hash_count40 bytes
padding56 bytes (to align next cache line)
Parallel arrays (cache-line optimized)
keyshash_size × 8 bytes
valshash_size × 8 bytes
nextshash_size × 2 bytes
shape_version4 bytes
free_head2 bytes

String Layout

Interned strings are stored as:

4 bytes: baked wyhash 4 bytes: length char data: string content (null-terminated)

LValue stores a pointer to the char data (skipping the 8-byte header). Length at ptr[-4..ptr[-1] and hash at ptr[-8..ptr[-5]].

Pre-interned Metamethods

To avoid repeated string interning on every metamethod dispatch, clx pre-interns common metamethod strings at LState initialization: str_index, str_newindex, str_gc, str_call, str_close, str_pairs, str_tostring. These are stored directly in LState and used for fast metamethod lookup.

GC Options

The collectgarbage() function accepts these options:

"collect" Performs a full GC cycle (default) "stop" Stops automatic GC — runs only on explicit calls "restart" Restarts automatic GC "count" Returns total memory in use (Kbytes, fractional) "step" Performs a single GC sweep step; returns true if finished "isrunning" Returns true if the collector is running "incremental" Switches to incremental mode (already the default) "generational" Not supported — stays incremental "param" Gets/sets GC parameters (pause, stepmul, stepsize)

Lazy Function Registration

set_lazy_funcs(L, table, lazy_regs, count) attaches a __index metamethod that creates LCFunction closures lazily on first access and caches them on the table. Uses constexpr-friendly LazyReg arrays (raw function pointers, no std::function) so registration tables live in static read-only storage. Math library uses this pattern.

Performance Characteristics

Table access (key known): O(1) average Table access (cache hit): O(1) single pointer check Table iteration: O(n) String concatenation: O(n) per operation Numeric for loop: O(n) with SIMD Function call: O(1) setup + body GC pause: Incremental, 512-byte step budget String interning: O(1) average, one probe Pattern matching: O(n*m) worst case

C++ API

All functions are in namespace clx. Include <clx.h> to use the full API. Values are LValue objects, not stack indices.

Lifecycle

cppOpen and close a clx state
LState* L = open(argc, argv); // Create a clx VM state luastd_base(L); // Register base lib (print, type, pcall, ...) luastd_math(L); // Register math lib (sin, cos, floor, ...) luastd_string(L); // Register string lib (sub, match, format, ...) luastd_coroutine(L); // Register coroutine lib (create, resume, yield, close) close(L); // Free all memory — call exactly once

Value Constructors

cppFactory functions for creating LValues
clx::nil(); // Returns LType::Nil clx::boolean(b); // Returns LType::Bool from bool b clx::number(d); // Returns LType::Number from double d clx::integer(i); // Returns LType::Integer from int64_t i clx::string(L, s); // Interned string; ≤5 bytes uses TAG_ISTR inline clx::table(L, asize, hsize); // Creates a new table with pre-sized array/hash clx::cfunction(L, func); // Wraps a CFunctionType as an LCFunction closure clx::thread(LThread*); // Wraps an LThread pointer (coroutine) clx::lightuserdata(ptr); // Wraps a void* as LType::Userdata

Type Queries

cppCheck LValue types
clx::type_of(v); // Returns LType enum for value v clx::is_nil(v); // True if LType::Nil clx::is_bool(v); // True if LType::Bool clx::is_number(v); // True if LType::Number clx::is_integer(v); // True if LType::Integer clx::is_string(v); // True if string (inline or interned) clx::is_table(v); // True if LType::Table clx::is_function(v); // True if LType::Function clx::is_thread(v); // True if LType::Thread clx::is_userdata(v); // True if LType::Userdata clx::is_none(v); // Always false (no stack sentinel) clx::is_noneornil(v); // True if v is nil

Type Names

cppGet type name strings
clx::type_name(LType t); // e.g. "number", "string" clx::type_name(const LValue& v); // Overload — same lookup

State Queries

cppQuery VM state
clx::isyieldable(L); // True if current thread is not main clx::status(t); // THREAD_SUSPENDED(0), RUNNING(1), DEAD(2), NORMAL(3)

Lenient Conversions

Return a default value on failure (no exception):

cppSafe conversions with defaults
clx::to_number(v, def); // Convert to double, def on failure clx::to_integer(v, def); // Convert to int64, def on failure clx::to_numberx(v, &isnum); // double, *isnum = true/false clx::to_integerx(v, &isnum); // int64, *isnum = true/false clx::to_boolean(v); // Convert to bool clx::to_string(L, v, def); // Convert to const char*, def on failure clx::touserdata(v); // Convert to void* clx::tothread(v); // Convert to LThread* clx::topointer(v); // Convert to const void* clx::stringtonumber(L, s); // "3.14" → number(3.14), else nil

Strict Conversions

Throw LRuntimeException on type mismatch:

cppChecked conversions (throw on failure)
clx::check_number(L, v); // Throws if not a number clx::check_integer(L, v); // Throws if not an integer clx::check_string(L, v); // Throws if not a string clx::check_table(L, v); // Throws if not a table clx::check_function(L, v); // Throws if not a function

Checked Field Access

Validate that a field has the expected type:

cppField type validation
clx::check_field_integer(L, v, "field"); // Throws on type mismatch clx::check_field_number(L, v, "field"); clx::check_field_string(L, v, "field");

These produce messages like field 'x' (integer expected, got string).

Optional Conversions

Return default on nil, throw on type mismatch:

cppOptional conversions (nil-safe)
clx::opt_number(L, v, def); // Returns def if nil, throws on mismatch clx::opt_integer(L, v, def); // Returns def if nil, throws on mismatch clx::opt_string(L, v, def); // Returns def if nil, throws on mismatch

String Conversion (__tostring aware)

cppstring conversion with __tostring support
clx::tolstring(L, v); // Returns string LValue; respects __tostring

If v is already a string, returns v directly. If v has __tostring, calls it and returns the result. Otherwise falls back to to_string + intern.

Argument Validation

cppArgument checking helpers
clx::checktype(L, argnum, v, LType t); // Asserts v.type() == t clx::checkany(L, v); // No-op (accepts any type) clx::argcheck(L, cond, argnum, msg); // Asserts cond clx::argexpected(L, cond, argnum, v, wanted, msg); // Reports actual type on fail

Table Operations

cppTable read/write with and without metamethods
clx::get_field(L, table, "key"); // table.key — respects __index metamethod clx::set_field(L, table, "key", v); // table.key = v — respects __newindex clx::raw_get(L, table, key); // Bypasses __index — key: LValue, const char*, double, int64_t clx::raw_set(L, table, key, v); // Bypasses __newindex — same key type overloads clx::raw_get_i(L, table, idx); // Raw integer key access (1-based index) clx::raw_set_i(L, table, idx, v); // Raw integer key write (1-based index)

Module Registration

cppRegister C functions and create Lua modules
clx::set_function(L, t, "name", f); // Bind CFunctionType to table key clx::set_value(L, t, "name", v); // Bind an LValue to table key clx::set_functions(L, t, regs); // Bind array of LReg structs clx::new_lib(L, regs); // Create table + bind LReg[] (nullptr-terminated) clx::set_lazy_funcs(L, t, regs, n); // Lazy __index: creates LCFunction on first access

LReg struct

cpp
struct LReg { const char* name; CFunctionType func; // std::function<MultiValue(LState*, const LValue*, size_t)> };

LazyReg struct

cpp
struct LazyReg { const char* name; RawCFunction func; // raw function pointer — constexpr-friendly }; using RawCFunction = MultiValue(*)(LState*, const LValue*, size_t);

LazyReg uses raw function pointers instead of std::function, making the array constexpr. set_lazy_funcs stores the LazyReg* as light userdata on the metatable; the array must persist in static storage.

Globals & Metatables

cppGlobal access and metamethod helpers
clx::get_global(L, "name"); // Read global variable _G["name"] clx::set_global(L, "name", val); // Write global — LValue, double, int64_t, const char* clx::set_global(L, "pi", 3.14); // convenience: double auto-wrapped clx::set_global(L, "msg", "hi"); // convenience: string auto-interned clx::getmetatable(L, obj); // Returns metatable or nil (respects __metatable) clx::setmetatable(L, obj, mt); // Sets metatable (nil or table) clx::rawequal(a, b); // Equality without __eq metamethod clx::next(L, table, key); // Iterator — call with nil to start clx::getmetafield(L, obj, field); // Get metatable[field] or nil clx::callmeta(L, obj, event); // Call metatable[event](obj), returns true if called clx::len(L, v); // # operator, respects __len metamethod clx::rawlen(v); // Raw #, bypasses __len (string/table only) clx::concat(L, a, b); // .. operator, respects __concat metamethod

Table Iteration

cppRange-style iteration over table entries
for (auto it = clx::iterate(L, table); it; ++it) { auto [key, value] = *it; // structured binding (C++17) // use key, value }

Function Calls

cppDirect and protected calls
clx::call(L, func, args, n); // Direct call, throws on Lua error clx::call(L, func, arg1, arg2, ...);// Variadic — accepts LValue and native C++ types clx::pcall(L, func, args, n); // Protected call, returns {true, ...} or {false, err} clx::pcall(L, func, arg1, arg2, ...);// Variadic — accepts LValue and native C++ types

Error Helpers

cppError reporting (all [[noreturn]])
clx::error(L, msg); // Throw LRuntimeException with message clx::arg_error(L, n, expected); // "bad argument #N (expected expected)" clx::type_error(L, n, expected); // "bad argument #N (expected expected, got nil)"

Coroutines

cppCreate, resume, yield, and close coroutines
clx::create_thread(L, func, 1<<20); // Create coroutine with stack size clx::resume(L, th, args, n); // Resume suspended coroutine clx::yield(L, args, n); // Yield from current coroutine (non-main only) clx::close_thread(L, th); // Close a suspended coroutine

Core Types

cppFundamental clx types
LType // Tagged type enum (Number, Integer, String, Table, ...) LValue // 64-bit nan-boxed value (with TAG_ISTR for ≤5B strings) MultiValue // Multi-return container (.count, operator[]) LState // VM state (opaque pointer) LThread // Coroutine handle LTable // Table (raw gettable/settable) LCFunction // C function closure LReg // {name, CFunctionType} for module registration LazyReg // {name, RawCFunction} for constexpr lazy registration RawCFunction // Raw C function pointer type CFunctionType // std::function<MultiValue(LState*, const LValue*, size_t)> LRuntimeException // Thrown on Lua errors; .error_obj holds error LValue

LValue Raw Constructors

cpp
LValue(); // nil LValue(bool); LValue(double); LValue(int64_t); LValue(const char*); // raw interned string pointer LValue(LType, LHeader*); // GC object LValue::istr(s, len); // static — inline string (≤5 bytes, no interning)

MultiValue

cpp
struct MultiValue { size_t count; LValue operator[](size_t i) const; // access by index // construct from initializer list, single LValue, or array+size };

Thread Status Constants

THREAD_SUSPENDED = 0 THREAD_RUNNING = 1 THREAD_DEAD = 2 THREAD_NORMAL = 3

Module Registration

Each standard module has a luastd_* function that creates and sets the global table:

cppStandard libraries
void luastd_base(LState* L); // registers _G, print, pcall, type, error, ... void luastd_math(LState* L); // registers math table void luastd_string(LState* L); // registers string table void luastd_coroutine(LState* L); // registers coroutine table void openlibs(LState* L); // registers all standard libraries at once

Call individual luastd_* functions or openlibs(L) after open() before using Lua features.

Coroutine Example

cppCreate, resume, yield, and close coroutines
clx::LValue co_func = clx::cfunction(L, [](clx::LState* L2, const clx::LValue* a, size_t n) { double v = clx::check_number(L2, a[0]); clx::LValue y = clx::number(v * 2); auto resumed = clx::yield(L2, &y, 1); // yield twice the input double r = clx::check_number(L2, &resumed[0], 1); return {clx::number(r * 3)}; // triple the resume value }); clx::LValue th = clx::create_thread(L, co_func); clx::LValue arg = clx::number(10); auto r = clx::resume(L, th, &arg, 1); // {true, 20} clx::LValue arg2 = clx::number(7); auto r2 = clx::resume(L, th, &arg2, 1); // {true, 21}

Complete Module Example

cppFull C++ module with lazy registration
// Define a function that sums its arguments static clx::MultiValue add(clx::LState* L, const clx::LValue* args, size_t n) { double sum = 0; for (size_t i = 0; i < n; i++) sum += clx::check_number(L, args[i]); return {clx::number(sum)}; } // LazyReg array — constexpr-friendly, no std::function overhead static constexpr clx::LazyReg my_funcs[] = { {"add", add}, }; CLX_API clx::LValue luaopen_mylib(clx::LState* L) { clx::LValue t = clx::table(L); clx::set_lazy_funcs(L, t, my_funcs, 1); // Lazy registration clx::set_global(L, "mylib", t); return clx::LValue(); }

Compile into an object/library, then link with clx main.lua --modules mylib. clx's generated main() calls register_module("mylib", luaopen_mylib). The module becomes available via require("mylib") at runtime.

Modules

clx supports two ways to organize and load modules: Lua source modules compiled alongside your entry point (static preload), and statically linked C++ modules with --modules. Both are consumed via Lua's require().

Lua Source Modules

Pass multiple .lua files to clx — the first is the entry point, the rest become modules loadable via require:

$ clx main.lua mymodule.lua utils.lua --output myapp

Inside main.lua, require them by name (filename without .lua):

luamain.lua
local mymodule = require("mymodule") local utils = require("utils") mymodule.say_hello() utils.help()

How it works

clx compiles each .lua file into a function luaopen_<module> with extern linkage. The generated main() calls register_module() for non-entry modules — this stores the function in package.preload[name] without calling it:

// Generated main() — simplified main() { open(); openlibs(L); register_module("mymodule", luaopen_mymodule); // stored in preload, NOT called register_module("utils", luaopen_utils); // stored in preload, NOT called luaopen_main(L); // entry point runs immediately close(L); }

When Lua calls require("mymodule"):

  1. Checks package.loaded["mymodule"] — if present, returns it immediately
  2. Checks package.preload["mymodule"] — calls the registered function
  3. Stores the result in package.loaded["mymodule"] and returns it
  4. Subsequent calls return the cached value without re-executing

Linking

All builds link statically against libclx.a. No shared library is needed at runtime.

Module convention

A Lua source module should return a table (or any value) that becomes the result of require:

luamymodule.lua
local M = {} function M.say_hello() print("hello from mymodule") end return M

C++ Native Modules (Statically Linked)

You can link precompiled C++ code using --modules:

$ clx main.lua --modules my_native_mod

The function must use CLX_API (which provides extern linkage and proper symbol visibility):

CLX_API clx::LValue luaopen_my_native_mod(clx::LState* L);

The generated main() calls register_module, which stores the wrapper in package.preload — the function runs only on first require().

Writing a C++ native module

cppmy_native_mod.cpp
#include <clx.h> CLX_API clx::LValue luaopen_my_native_mod(clx::LState* L) { clx::LValue t = L->create_table(); clx::LTable* mod = static_cast<clx::LTable*>(t.as_pointer()); mod->bind(L, "add", [](clx::LState* L, const clx::LValue* args, size_t n) -> clx::MultiValue { double a = args[0].as_number(); double b = args[1].as_number(); return clx::MultiValue(clx::LValue(a + b)); }); return t; }

Compile to an object file:

# Linux/macOS (g++/clang++): $ g++ -c -std=c++20 -I./include my_native_mod.cpp -o my_native_mod.o # Windows (MSVC): > cl /c /std:c++20 /I.\include my_native_mod.cpp /Fomy_native_mod.obj

Then link with your Lua script. clx looks for my_native_mod.a (or .lib on Windows) in the current directory, then in <clx-install-dir>/lib/clx/, and on POSIX also in /usr/local/lib/clx/:

$ clx main.lua --modules my_native_mod

If your module depends on external libraries, pass link flags directly:

$ clx main.lua --modules my_native_mod -lm -lz

Compiling Lua to Libraries

Static Library

$ clx mylib.lua --static --output mylib # Produces libmylib.a (Linux/macOS) or mylib.lib (Windows)

Object File

$ clx mylib.lua --object --output mylib # Produces mylib.o (Linux/macOS) or mylib.obj (Windows)

All export luaopen_mylib. A host C++ program links against the static library:

cpphost.cpp
#include <clx.h> CLX_API clx::LValue luaopen_mylib(clx::LState* L); int main() { clx::LState* L = clx::open(); clx::openlibs(L); L->register_module("mylib", luaopen_mylib); clx::close(L); return 0; }

Combining All Approaches

$ clx main.lua utils.lua --modules native_processor --output app
luamain.lua
local utils = require("utils") -- Lua source module (preload) local proc = require("native_processor") -- C++ native module (preload) local extra = require("extra_plugin") -- Static module (preload)

All modules are registered in package.preload and loaded via require.

Options Reference

--modules <list> Comma-separated list of precompiled C++ modules --minimal Exclude non-essential modules (string, table, io, os, math, utf8, coroutine); keeps base + package --static Compile to static library (exports luaopen_*) --object Compile to object file (exports luaopen_*)

API Reference

cppModule registration
// Register a native module for lazy loading via require() // Stores a wrapper in package.preload[name]; does NOT call luaopen_* void LState::register_module(const std::string& name, LValue (*func)(LState*)); // Initialize clx runtime LState* open(int argc = 0, char* argv[] = nullptr); // Open optional standard libraries (string, table, io, os, math, utf8, coroutine) void openlibs(LState* L); // Destroy clx runtime void close(LState* L);

Lua 5.5 Compatibility

clx targets Lua 5.5 compatibility. The following table summarizes the current implementation status.

Core LanguageStatus
Variables
Arithmetic operators
Logical operators
Comparisons
Functions
Closures
_ENV
Varargs
Multiple returns
Local variables
Global variables
Control FlowStatus
if / elseif / else
while
repeat / until
numeric for
generic for
break
goto
labels
TablesStatus
Table constructors
Array part
Hash part
Mixed tables
Table iteration
MetatablesStatus
__index / __newindex
__add / __sub / __mul / __div / __mod / __pow / __unm
__len / __concat / __eq / __lt / __le
__call / __tostring
__ipairs / __pairs
CoroutinesStatus
coroutine.create / resume / yield
coroutine.status / wrap
Standard LibrariesStatus
base
math
string
table
coroutine
io
os
utf8
package
debug

Unsupported — AOT Limitations

Due to the static compilation model, the following features are intentionally unsupported:

FeatureReason
load()Runtime Lua code loading
loadfile()Runtime Lua code loading from file
dofile()Runtime Lua code execution from file
string.dump()Lua code compilation
debug libraryRuntime introspection

Migration Guide: Lua C API → clx C++ API

This guide is for developers who have written Lua binary modules using the Lua C API and want to port them to clx. clx does not expose a VM stack — instead, values are passed as clx::LValue objects directly.

Key Differences

Lua C APIclx C++ API
Stack-based (lua_push*, lua_to*)Value-based (clx::LValue, no stack)
lua_State* Lclx::LState* L
lua_pushnumber(L, 3.14)clx::number(3.14)
lua_tointeger(L, 1)args[0].as_integer()
luaL_checknumber(L, 1)clx::check_number(L, args[0])
lua_settop(L, 0)Not needed — no stack
lua_getfield(L, idx, "key")clx::get_field(L, table, "key")
lua_setfield(L, idx, "key")clx::set_field(L, table, "key", val)
lua_newtable(L)clx::table(L)
return count nreturn MultiValue(values...)
lua_error(L)throw clx::LRuntimeException(...)
lua_len(L, idx)clx::len(L, v)
lua_concat(L, n)clx::concat(L, a, b)
lua_pcall(L, nargs, nresults)clx::pcall(L, func, args, count)
lua_next(L, idx)clx::next(L, table, key)

Function Signature

Lua C API:

c
static int my_func(lua_State* L) { double x = luaL_checknumber(L, 1); double y = luaL_checknumber(L, 2); lua_pushnumber(L, x + y); return 1; }

clx C++ API:

cpp
static clx::MultiValue my_func(clx::LState* L, const clx::LValue* args, size_t count) { double x = clx::check_number(L, args[0]); double y = clx::check_number(L, args[1]); return clx::number(x + y); }

Module Registration

Lua C API:

c
static const struct luaL_Reg mylib[] = { {"add", my_add}, {"sub", my_sub}, {NULL, NULL} }; int luaopen_mylib(lua_State* L) { luaL_newlib(L, mylib); return 1; }

clx C++ API:

cpp
static constexpr clx::LazyReg my_funcs[] = { {"add", my_add}, {"sub", my_sub}, }; CLX_API clx::LValue luaopen_mylib(clx::LState* L) { clx::LValue t = clx::table(L); clx::set_lazy_funcs(L, t, my_funcs, 2); return t; }

Complete Migration Example

Before (Lua C API):

cvector.c
#include "lua.h" #include "lauxlib.h" static int vec_add(lua_State* L) { double x1 = luaL_checknumber(L, 1); double y1 = luaL_checknumber(L, 2); double x2 = luaL_checknumber(L, 3); double y2 = luaL_checknumber(L, 4); lua_pushnumber(L, x1 + x2); lua_pushnumber(L, y1 + y2); return 2; } static int vec_len(lua_State* L) { double x = luaL_checknumber(L, 1); double y = luaL_checknumber(L, 2); lua_pushnumber(L, sqrt(x*x + y*y)); return 1; } static const struct luaL_Reg vec_lib[] = { {"add", vec_add}, {"len", vec_len}, {NULL, NULL} }; int luaopen_vector(lua_State* L) { luaL_newlib(L, vec_lib); return 1; }

After (clx C++ API):

cppvector.cpp
#include static clx::MultiValue vec_add(clx::LState* L, const clx::LValue* args, size_t count) { double x1 = clx::check_number(L, args[0]); double y1 = clx::check_number(L, args[1]); double x2 = clx::check_number(L, args[2]); double y2 = clx::check_number(L, args[3]); return clx::MultiValue({clx::number(x1 + x2), clx::number(y1 + y2)}); } static clx::MultiValue vec_len(clx::LState* L, const clx::LValue* args, size_t count) { double x = clx::check_number(L, args[0]); double y = clx::check_number(L, args[1]); return clx::number(std::sqrt(x*x + y*y)); } static constexpr clx::LazyReg vec_funcs[] = { {"add", vec_add}, {"len", vec_len}, }; CLX_API clx::LValue luaopen_vector(clx::LState* L) { clx::LValue t = clx::table(L); clx::set_lazy_funcs(L, t, vec_funcs, 2); return t; }

Not supported in clx: lua_load, luaL_loadfile, luaL_loadstring, lua_dump, bytecode format, lua_pushcclosure, upvalue API, lua_newstate, lua_gc, luaL_Buffer.