Lua Compiler for Native Standalone Executables

Welcome to clx — a native Lua compiler that turns your Lua scripts into fast, standalone executables. Normal AOT builds need no interpreter or runtime dependency; use --dynamic when the application must load Lua source at runtime.

# Write a Lua program echo 'print("Hello from clx!")' > hello.lua # Compile to a native binary clx hello.lua # Run it — the normal AOT binary has no runtime dependency ./hello
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
Normal AOT binaries are self-contained. --dynamic additionally links the embedded Lua VM for runtime loading.
Lua 5.5 runtime
Lua 5.5 language and clx standard modules. Dynamic chunks use the embedded VM's libraries and require; load/loadfile/dofile require --dynamic.
Modern garbage collector
Mark-and-sweep collector with reusable worklists and explicit incremental GC options.
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.3.0 MIT License - Copyright (c) 2026 Tine Samir

Benchmarks

Performance comparison against Lua 5.5 and LuaJIT. The speedup is relative to Lua 5.5. Results are averages from 10 runs using hyperfine on a single CPU; they vary with 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 # Build and install: ./build.sh install # Clean build directory: ./build.sh clean

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. load(), loadfile(), and dofile() are registered only for programs compiled with --dynamic; they execute source in the embedded Lua VM and are absent from ordinary AOT and --minimal builds. The AOT runtime does not provide string.dump() or debug directly; the dynamic VM provides its own library behavior.

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 Dynamic Lua, Architecture, Optimizations, Runtime, and CLI documentation.

Dynamic Lua

Enable Dynamic Lua

Normal clx builds compile Lua ahead of time into native C++. The --dynamic switch adds the embedded Lua VM and bridge needed to load or execute Lua source at runtime.

shBuild and compile
$ cmake -S . -B build $ cmake --build build $ ./build/clx main.lua --dynamic --output myapp $ ./myapp

On POSIX the generated program links libclx_lua.a; on Windows it links clx_lua.lib. Do not combine --minimal with dynamic loading: minimal builds skip the standard-library setup that registers the loader functions.

Runtime Loading APIs

With --dynamic, clx registers the following globals:

local chunk, err = load(source [, chunkname [, mode [, env]]]) local chunk, err = loadfile([filename [, mode [, env]]]) local result = dofile([filename])
  • load compiles a source string in the embedded VM and returns a callable clx function value. The current bridge does not implement the reader-function form.
  • loadfile reads and compiles a file without executing it.
  • dofile loads a file and immediately calls the resulting chunk.

VM Libraries and require

Runtime-loaded chunks execute in the embedded VM, not in the clx AOT runtime. They use the VM's own string, math, table, os, io, utf8, coroutine, package, require, base functions, and VM debug library. The VM initializes that library, but the default dynamic environment does not copy a debug global into loaded chunks.

The package systems are separate: AOT require uses clx's package runtime, while a dynamic chunk uses Lua VM package.searchers, package.loaded, and package.preload. AOT modules are not automatically inserted into the VM's package.preload.

Environments and Boundary Values

  • The default VM environment exposes selected VM globals and bridge callbacks for missing names in the clx global table.
  • A custom environment passed to load or loadfile is a converted clx table; VM libraries must be provided explicitly when needed.
  • Nil, booleans, numbers, and strings are copied by value. Functions are wrapped across the bridge.
  • Clx tables and userdata become VM-side proxy userdata (clx_proxy and clx_userdata), not raw native objects. VM tables passed to clx are converted and tracked writes can be copied back.
  • Ordinary VM userdata and lightuserdata are VM-local and convert to nil when passed to clx; VM threads are explicitly rejected. Only recognized bridge proxies have special handling.

Coroutines and Limitations

Coroutines are runtime-local. Keep creation, resume, yield, and close within either clx or the embedded VM; clx and VM coroutines cannot be passed or resumed across the boundary, and a yield cannot cross it. The bridge also rejects calling a VM function from inside a clx coroutine.

Dynamic loading is unavailable without --dynamic, loader registration is skipped by --minimal, and the two runtimes do not share table identity, userdata layout, garbage collection, or package caches. The AOT runtime does not provide string.dump; that API belongs to the embedded VM path.

For the complete API behavior and current limitations, see doc/dyamic-lua.md.

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 --dynamic Link the embedded Lua 5.5 VM and enable load, loadfile, and dofile --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: fixed at build time via CMake Executable: <name> (no extension) Object: <name>.o | Static: <name>.a # Windows: Compiler: fixed at build time via CMake Executable: <name>.exe Object: <name>.obj | Static: <name>.lib

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 read — compiler fixed at build time)

Exit Codes

0 Success 1 Usage or compilation error

Build with CMake

If building from source:

$ mkdir build $ cd build $ cmake .. -DCMAKE_BUILD_TYPE=Release $ cmake --build . $ ./clx --help

Architecture

Overview

clx transpiles Lua source code into optimized C++, then compiles it with the system C++ compiler to produce native machine code. The normal execution path is entirely AOT. The optional --dynamic link path adds a separate embedded Lua VM for runtime-loaded source; it does not replace the AOT runtime.

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)
--dynamic: libclx_lua.a / clx_lua.lib
Link-time optional VM + bridge ▼
Native Binary

Components

CLI — Handles argument parsing, file I/O, and invokes the C++ compiler. The compiler is fixed at build time via CMake (same compiler that built clx) to ensure ABI compatibility. 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-paths and parameter promotion, numeric record-field inference, variable scope resolution, table purity analysis, constant folding preparation, table version tracking for inline-cache invalidation, and yields_number analysis for numeric loops.

Code Generator — Produces C++ code with native numeric fast-paths, dynamic fallbacks, loop transformations, [[likely]] hints, per-LTable 4-entry inline caches, StringBuilder concatenation, and baked string hashes.

Runtime Library — Implements Lua semantics through the AOT runtime: values, GC, tables, metamethods, arithmetic, standard libraries, coroutines, and the clx package system. The optional runtime/vm/ tree contains the separate embedded Lua VM and bridge.

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) │ └── clx_simd.h # SIMD helpers for runtime type-array scans ├── src/ │ ├── clx.cpp # Compiler driver / CLI │ ├── syntax/ │ │ ├── lexer.h/cpp # Tokenizer/scanner │ │ ├── parser.h/cpp # Recursive descent parser │ │ ├── nodes.h # AST node definitions │ ├── 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 │ └── vm/ # Optional embedded Lua VM and bridge │ ├── dynamic_vm.cpp # VM lifetime and environment │ ├── load_builtin.cpp # load/loadfile/dofile registration │ ├── vm_convert.cpp # clx ↔ VM conversion │ ├── vm_native_bridge.cpp # Calls across the boundary │ └── lua/ # Vendored Lua VM sources ├── 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 and selected AOT libraries) → Execution (native code with numeric fast-paths) → Dynamic fallback to the LValue representation when types are unknown → Cleanup (GC). With --dynamic, loaded chunks execute separately in the embedded VM.

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.

Per-LTable Inline Cache

Each LTable embeds a 4-entry InlineCache for string-keyed table access. The cache stores key payload, entry index, and table hash_version. It is valid when the key and version match; structural changes (inserts, deletes, and rehashes) invalidate it, while value-only updates preserve read-then-write hits. The cache occupies 64 bytes per LTable.

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().

Table Version Tracking

Tables track hash_version (increments on structural changes: inserts, deletes, rehash) and array_version (increments on array-size changes). The inline cache checks hash_version to detect stale entries after table mutations.

SIMD Runtime Scans

ValueType is a uint8_t enum, so 16 type tags fit in a single 128-bit register. The runtime uses SSE2 (x64) or NEON (ARM64) to accelerate hot type-array scans: rawlen() (finds first nil in array), next() (finds first non-nil), table_concat validation (validates String/Double/Int64), and GC mark loop (skips nil entries). Scalar fallback handles non-16-byte-aligned remainders.

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 value representation (separate ValueType tag + 8-byte payload). The runtime LValue arithmetic functions dispatch to native double or int64 operations internally. At the codegen level, integer literals and variables provably holding integers are emitted as C++ int64_t, while float literals and mixed expressions use native double:

lua
-- Integer literal: local i = 1 + 2 -- Compiled as: int64_t arithmetic via LValue -- Float literal: local f = 1.5 + 2.3 -- Compiled as: double arithmetic via LValue -- 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

Int64 Overflow Safety

Integer arithmetic (add, sub, mul) with two Int64 operands uses native C++ integer operations for speed. Each operation checks for overflow before returning:

  • add: Detects same-sign inputs producing a different-sign result
  • sub: Detects different-sign inputs where the result sign matches the subtrahend
  • mul: Verifies result / operand == other_operand, with special-case for INT64_MIN * -1

On overflow, the result is promoted to double (matching Lua 5.5 semantics). mod, idiv, pow, and unm follow their Lua numeric semantics for integer and floating-point inputs.

Non-Fast Function Parameter Numeric Promotion

Functions with mixed parameter types (e.g., decode(str, pos, ...) where str is a string but pos is a number) cannot be fast functions — not all parameters are numeric. But every pos + 1, pos - 1 still went through clx::add/clx::sub runtime calls.

The optimizer detects parameters used in arithmetic operations (directly or through call chains) and marks them as numeric. The codegen emits them as double l_param = args[i].as_number() instead of clx::LValue l_param = args[i], enabling native arithmetic even in non-fast functions.

Function Parameter Numeric-Record Array Inference

When a function receives a table as a parameter (e.g., advance(bodies, dt) where bodies is an array of record-like tables), the optimizer cannot prove that bodies[i].x yields a number without tracing the pattern.

Pass 3 in the optimizer detects local bi = bodies[i] where bodies is a function parameter, then traces field accesses (bi.x, bi.y) in BinaryOp nodes to infer numeric fields. Both the parameter name and the local name are registered in g_numeric_table_fields so yields_number() proves field reads yield numbers.

Local Variable Optimization

Local variables that hold numbers are stored as unboxed C++ int64_t or double:

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

Inline Caching

Each LTable embeds a 4-entry InlineCache. On repeated access to the same string key on the same table, the cache skips the hash probe entirely. hash_version guards detect stale entries after structural table mutations. Read-then-write patterns still hit because only inserts/deletes/rehash invalidate the cache — value updates don't.

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 a 16-byte LValue (8-byte payload + separate ValueType tag) to represent all Lua values with distinct types for Number (double), Integer (native int64), TAG_ISTR inline strings (≤6 bytes, no heap allocation), and pointers for tables, functions, threads, and userdata. Integer arithmetic uses native operations without floating-point conversion. Overflow in add, sub, mul promotes to double (matching Lua 5.5 semantics).

Value Layout16-byte LValue (payload + type tag)
Double (Numbers) 64-bit IEEE 754 floating-point in TValue payload
Integer 64-bit signed integer in TValue payload
Pointer (GC Objects) 64-bit heap address in TValue payload (tables, closures, threads, userdata)
Inline String (TAG_ISTR) Small strings up to 6 bytes are stored inline in the 8-byte payload without heap allocation
Special System Literals nil, true, false — stored as distinct ValueType tags

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, and gsub with pattern matching support.

Coroutine Library — create, resume, yield, status, wrap, isyieldable, and close using OS-level fibers/context. On macOS Apple Silicon, it uses hand-written ARM64 assembly (src/runtime/coro_switch_aarch64.s) instead of the buggy Darwin ucontext shim.

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 __close __metatable

Per-LTable Inline Cache

cppInlineCache structure (embedded in LTable)
struct InlineCache { uint64_t key_payload; // Raw key payload for fast comparison uint32_t entry_idx; // Index into hash entries array uint32_t table_ver; // Snapshot of hash_version at populate time };

4-entry per-LTable cache for string-keyed table access. A hit requires a matching key payload, the current hash_version, and a non-nil cached entry. Structural changes (inserts, deletes, and rehashes) invalidate the cache; value-only updates do not. The cache occupies 64 bytes per LTable.

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
hash_version, array_version8 bytes
inline cache (4 × 16B)64 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 Nil clx::boolean(b); // Returns Boolean from bool b clx::number(d); // Returns Double from double d clx::integer(i); // Returns Int64 from int64_t i clx::string(L, s); // Interned string; ≤6 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 UserData

ValueType Short Aliases

cppclx::Nil, clx::String, etc.
// ValueType is a scoped enum. Short aliases in the clx namespace: clx::Nil // ValueType::Nil (0) clx::Boolean // ValueType::Boolean (1) clx::Int64 // ValueType::Int64 (2) clx::Double // ValueType::Double (3) clx::String // ValueType::String (4) clx::Table // ValueType::Table (5) clx::Function // ValueType::Function (6) clx::UserData // ValueType::UserData (7) clx::Thread // ValueType::Thread (8)

Type Queries

cppCheck LValue types
clx::type_of(v); // Returns ValueType for value v clx::is_nil(v); // True if Nil clx::is_bool(v); // True if Boolean clx::is_number(v); // True if Double or Int64 clx::is_integer(v); // True if Int64 clx::is_string(v); // True if string (inline or interned) clx::is_table(v); // True if Table clx::is_function(v); // True if Function clx::is_thread(v); // True if Thread clx::is_userdata(v); // True if 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 // 16-byte value (TValue payload + ValueType tag) 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 (≤6 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 AOT modules in this build are registered in clx's package.preload and loaded via the AOT require. This does not populate the embedded VM's package registry.

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 --dynamic Link the embedded Lua 5.5 VM and enable load, loadfile, and dofile --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
debugAOT: ❌ / VM: ✅ with --dynamic

Conditional and Unsupported Features

Runtime loading requires --dynamic and executes in the embedded Lua VM. The clx AOT runtime does not provide these features directly:

FeatureReason
load()Requires --dynamic; the current bridge accepts source strings, not reader functions
loadfile()Requires --dynamic; compiles a file in the embedded Lua VM
dofile()Requires --dynamic; loads and executes a file through the bridge
string.dump()Not provided by the clx AOT runtime; available in the embedded VM path
debug libraryNot provided as a clx AOT global; embedded VM behavior applies to dynamic code

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 directly by the clx C++ API: lua_load and luaL_loadfile (use the optional --dynamic VM bridge for runtime loading), lua_dump and a shared bytecode format, lua_pushcclosure and the upvalue API, lua_newstate, lua_gc, and luaL_Buffer.