Changelog
View SourceAll notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Upgrading from a Luerl-based
0.xrelease? See Upgrading from 0.x below, or the full Migrating to 1.0 guide for step-by-step before/after code.
Upgrading from 0.x (Luerl-based versions)
1.0.0 replaces the Luerl backend with an Elixir-native Lua 5.3 VM, and
Luerl is no longer a runtime dependency. Most code built on the high-level
Lua API (Lua.new/1, Lua.eval!/2, Lua.set!/3, deflua,
Lua.load_api/2) keeps working unchanged. The breaking changes are all at
the value-encoding and error boundaries:
- Encoded table / userdata / function tags changed. Values that carried
Luerl's internal tags now use the new VM's representation: tables are
{:tref, integer()}(was:luerl.tref()), userdata is{:udref, integer()}(was:luerl.usdref()), and Elixir-defined Lua callables are{:native_func, fun}(was:luerl.erl_func()); compiled Lua functions are{:lua_closure, _, _}. If you pattern-matched the old tuples, update the patterns — better still, treat encoded refs as opaque and round-trip them throughLua.decode!/2. - MFA callback encoding was removed.
Lua.encode!/2no longer accepts the{module(), atom(), list()}MFA tuple form. Replace it with a function literal or adefluacallback. - Parser error messages have a new format. The Luerl-style
"Line 1: syntax error before: ';'"is gone; the native parser produces messages like"Expected expression", with rich structured data available viaLua.Parser.parse_structured/1. Assertions that string-matched the old wording need updating. - 64-bit integers wrap on overflow. Arithmetic and bitwise ops follow Lua 5.3 §3.4.1 (wrap-around at 2^63) instead of widening to bignums as Luerl did. Code depending on arbitrary-precision integer results will now see wrapped values.
- Chunks are self-contained.
Lua.Chunknow holds a compiled prototype and is reusable acrossLua.eval!/2calls; there is no separate load step. - Exceptions are public.
Lua.RuntimeExceptionandLua.CompilerExceptionare documented, so user code can rescue and pattern-match them.
Everything else — the default sandbox, _G/_ENV semantics, metatables, and
the standard-library surface — is compatible. The full breaking-change list
is in the 1.0.0-rc.0 entry below.
1.0.1 - 2026-07-16
Fixed
- Parser and lexer errors for non-ASCII source are now valid UTF-8. An
unexpected multibyte character (or a non-ASCII byte inside a comment, string,
or long string) was matched one byte at a time, so the error message kept
only the UTF-8 lead byte and produced a mangled, non-UTF-8 string. The lexer
now carries the full codepoint, and the message reads
Unexpected character: … (U+XXXX). A genuinely invalid UTF-8 lead byte is reported separately asInvalid byte 0xXXwith an encoding-check suggestion. - Every string field of
Lua.Parser.Error.to_map/1is now valid UTF-8 and safe toJason.encode!/1, even when the offending token is non-ASCII or malformed — invalid bytes are scrubbed viaString.replace_invalid/1(#395).
1.0.0 - 2026-07-15
The first stable release on the Elixir-native Lua 5.3 VM, culminating the
1.0.0-rc.0 through rc.3 series. Upgrading from the last public release,
0.4.0? See Migrating to 1.0 for the full
walkthrough. The changes below are those since rc.3.
Added
Lua.format_exception/1renders aLua.RuntimeExceptionorLua.CompilerExceptionas the rich, human-readable report — location, source context, stack trace, and suggestions — with ANSI color whenIO.ANSI.enabled?/0is true. This is the reportmix lua.evalprints (#393).Lua.RuntimeException.to_map/2andLua.CompilerException.to_map/1expose a wire-safe structured representation (no ANSI) for JSON payloads, structured logs, and UI-facing error reporting (#393).
Changed
- Elixir callbacks now receive the public
Lua.t(%Lua{}) as their state argument regardless of how they enter the VM. Previously a two-arity callback (fn args, state -> {results, state} end) got a%Lua{}when set at a path or loaded viadeflua/Lua.load_api/2, but the raw internalLua.VM.Statewhen it reached the VM as an encoded value — nested inside a value passed toLua.set!/3, or produced byLua.encode!/2. Those paths now wrap the state consistently and validate the callback's return value, so the same closure behaves identically everywhere. Code relying on the rc.3 raw-state behaviour (or a%Lua{state: raw}workaround) should drop the workaround (#379).
Changed (breaking, before 1.0 freeze)
- Encoding a bare Elixir struct now raises. Previously
Lua.encode!/2(andLua.set!/3) matched a struct as a plain map and silently encoded a Lua table carrying a"__struct__"key — a lossy, accidental conversion. Convert structs explicitly first (e.g.Map.from_struct/1), selecting the fields Lua needs. This lands before 1.0 so the plannedLua.Encoderprotocol (#341) can be added without breaking a behaviour people relied on. - Removed the
is_mfaguard fromLua.API. It was a Luerl-era compatibility shim that always returnedfalseand was imported into everyuse Lua.APImodule; the VM has no MFA references. Remove anywhen is_mfa(value)clauses (they never matched). - The public runtime exception is now solely
Lua.RuntimeException. The five internal VM error structs —Lua.VM.RuntimeError,Lua.VM.TypeError,Lua.VM.ArgumentError,Lua.VM.AssertionError,Lua.VM.InternalError— are no longer part of the public surface. They are wrapped intoLua.RuntimeExceptionbefore crossing any API boundary, so the whole public exception surface is justLua.RuntimeException(runtime failures) andLua.CompilerException(compile-time failures). To discriminate a runtime failure, read the wrapper's new:kindfield (:error | :type | :argument | :assertion | :internal); the underlying VM struct remains available on:original. - The public
{:error, _}-returning APIs now hand back the exception struct uniformly, instead of a pre-rendered message string, so callers own rendering (Exception.message/1) and can pattern-match the concrete error.Lua.call_function/3returns{:error, exception, lua}whereexceptionis always aLua.RuntimeException. The raised Lua value (error(42), a table,nil,false) is preserved on:value, matching whatpcallhands back inside Lua, with the category on:kindand the underlying VM struct on:original. Code matching on a string reason should switch to the exception (or callException.message/1on it).Lua.parse_chunk/1returns{:error, %Lua.CompilerException{}}instead of{:error, [String.t()]}. CallException.message/1to render the full, human-readable report; the:errorsfield carries the bare, ANSI-free messages for programmatic inspection.
- Lua exceptions render their message lazily at
Exception.message/1call time, so the:messagestruct field isnilon theLua.RuntimeExceptionwrapper when it wraps an internal VM exception (and on the internal VM structs themselves). Read the message throughException.message/1(the idiomatic accessor) rather than thee.messagefield.Exception.message/1returns a plain, single-line, ANSI-free string (safe forLoggerand error trackers); the rich, ANSI-gated report — location, source context, stack trace, and suggestions — is available viaLua.format_exception/1. LikewiseLua.CompilerException's:errorsfield now holds bare, ANSI-free messages rather than the fully formatted (and previously ANSI-colored) diagnostics (#384, #393).
Changed
Lua.new/1's:max_string_bytesnow accepts:infinityfor no limit, making it uniform with its siblings:max_call_depthand:max_instructions.Lua.CompilerExceptionno longer has a:statefield. It was never populated (alwaysnil);:errorscarries all the formatted diagnostics.
Fixed
- Lua exception messages no longer leak ANSI escape codes (and multi-line rich
rendering) into non-terminal sinks such as log files, container stdout, or a
Sentry/AppSignal title. Messages were formatted eagerly during VM execution —
where
IO.ANSI.enabled?/0is true — freezing escape codes into the struct that survived long after the TTY gate.Exception.message/1now returns a plain, ANSI-free, single-line message; the rich, ANSI-gated report lives inLua.format_exception/1, where the gate is evaluated at call time — where the report is actually written (#384, #393). Lua.eval!/3now accepts a:sourceoption when evaluating a pre-compiledLua.Chunk, matching the string-script clause. Previouslyeval!(lua, chunk, source: "x")raised viaKeyword.validate!. For a chunk the option is accepted but ignored — the chunk already carries the source name it was compiled with.Lua.encode!/2now maps Elixirnilto Luanilinstead of the string"nil". Previously top-levelnilfell into the atom-encoding head and became"nil", which is truthy in Lua — silently invertingif not value then ...checks and breakingreturn nil, "reason"error patterns. The round tripdecode!(encode!(nil))is now lossless, matching the existing behaviour fornilinside tables and function result lists (#374).string.repnow sizes its allocation to the actual result length instead of the raw repeat count, so a large count paired with an empty or short string (e.g.string.rep("", 1e9)) no longer over-allocates or trips the string-size guard spuriously. The guard still refuses genuinely oversized results (#376).
Documentation
- The Lua 5.3 official test suite now passes 20/29 files. The 9 excluded
files are deliberate, documented exclusions rather than open bugs:
- Filesystem / subprocess non-goals —
main,files,attrib,verybig(shell-out, file I/O, and filesystemrequire, which a sandboxed embedded VM does not support). - Capability non-goals —
coroutine,db(the full continuation/coroutine model and the fulldebuglibrary). - Perf-bound, revisit in 1.0.x —
big,closure(run past the suite timeout on the BEAM tuple-copy ceiling; the VM results are correct). - PUC error-wording divergence —
errors(our structured error messages diverge from PUC-Lua's exact strings).
- Filesystem / subprocess non-goals —
1.0.0-rc.3 - 2026-06-15
The fourth release candidate for 1.0.0. It builds on rc.2 with a
structured parse-error API for tooling, conformant goto/label on
both VM engines, several order-of-magnitude performance wins on the
table and recursive-call paths, and a batch of parser error-location
and protected-call error-value fixes. All public API changes are
additive — nothing from rc.2 is broken.
Added
Lua.Parser.parse_structured/1returns{:ok, Chunk.t()} | {:error, [%Lua.Parser.Error{}]}, exposing the parser's rich structured error data directly instead of as a pre-formatted ANSI string — a stable contract for editors, LSPs, and web frontends that render parse errors in their own UI.Lua.Parser.Error.to_map/1,2emits a JSON-serializable map with the same wire shape asLua.VM.ErrorFormatter.to_map/3(type,message,source,line,call_stack,source_context,suggestion,error_kind), so parse and runtime errors can flow through a single renderer; the^pointer column now lands on the real offending token (#363).Lua.new/1accepts:max_instructions(default:infinity), bounding the number of VM instructions a single evaluation may execute. Exceeding the budget raises a catchable"instruction budget exceeded"runtime error, giving a deterministic CPU bound without wrapping each call in a hostTaskplus wall-clock timeout. Enforced at loop back-edges and call boundaries on both the interpreter and compiled-dispatcher paths via a singleLua.VM.State.tick!/2call that is a true no-op at:infinity(no increment, no struct rebuild), so the default:infinitycarries no per-opcode cost; the budget is fresh per top-level evaluation and recoverable viapcall(#320).
Performance
- Register tuples are sized to an honest peak, with no slack buffer, on
both VM engines (#312, #324). Both the interpreter (
call_function/3, the:callopcode,call_value/5) and the dispatcher (init_callee_regs/4) used to over-allocate every call frame's register tuple — the interpreter with a+16buffer, the dispatcher with the+16slack #347 added. On call-dense, work-light code (naivefib(30)most visibly, ~25%+ slower than rc.0) that per-frame over-allocation dominated. Both buffers existed to mask a latent codegen bug:max_registerscould undercount the true register peak for some deeply-nested expression shapes. Codegen's newinstruction_peak/1backstop makesmax_registershonest (it counts every statically-fixed destination the emitted stream writes), so both engines now size to exactlymax(max_registers, param_count)and grow on demand only for runtime-dynamic writes (vararg spread, multi-return distribution). The interpreter path is ~26% faster onfib(25)(closing the dispatcher–interpreter gap to ~1.02×), andfib(30)on the dispatcher improves from ~1.32× slower than Luerl to ~1.11× (Apple M-series, drift-controlled), with no other workload slower. - Recursive-call path closes the fibonacci gap with Luerl (#360).
Two profiler-driven fixes: call frames defer name decoding (a flat
3-tuple
{source, line, name_hint}decoded lazily at the cold traceback/debug.getinforeaders instead of an eager 4-key map per call), and codegen reclaims the register window after single-result calls so sibling calls reuse freed registers (fibdrops frommax_registers = 10to6, shrinking every per-framesetelement).fib(30)goes from ~1.18× slower than Luerl to ~1.03× (within run-to-run noise) and total allocations fall ~26%. #tis now O(1) on tables with no holes (#350). Lua tables cache their array-sequence border, so the length operator no longer rescans the array part on every read. Append loops (t[#t+1]=v,table.insert(t, v)) collapse from O(n²) to O(n) — −98% at n=2000 (21.96 ms → 0.44 ms). The cache is only re-established when1..nis provably dense, so holey tables fall back to a correct scan.pairsover hash-keyed tables is O(n), not O(n²) (#349).lua_nextmemoizes the hash-key iteration order on the first step of an iteration, making each subsequentnextO(1) instead of rescanning from the front. Iterating a large string-keyed table improves −92.5% at n=2000 (11.43 ms → 0.86 ms), with the gap widening as n grows.- Bitwise opcodes (
band/bor/bxor/shl/shr) andset_listmulti-return tails now compile on the dispatcher (#347) instead of falling back to the interpreter, closing dispatcher coverage gaps. The micro-benchmark delta is noise-dominated; this is a coverage fix, not a measurable speedup.
Fixed
goto/labelare conformant on both VM engines (#364). The interpreter previously resolved labels with a forward-only scan, so backward jumps (manual loops),continue-style jumps out of anif, and break-style jumps out of a loop all raised "goto target not found". Labels are now resolved ahead of execution on both the interpreter (Lua.Compiler.GotoResolution) and the compiled dispatcher (Bytecode.resolve_gotos/2), closing open upvalues at the block-exit level per Lua 5.3 §3.3.4. (One dispatcher gap remains: short-circuitand/orstill falls back to the interpreter.)- Parse errors are reported at the real offending token (#357, #365,
#366). A syntax error deep inside a function-call argument list — e.g.
an unclosed
(several lines down — was blamed at the call's opening line instead of where the mistake actually is. The parser no longer swallows a committed deep error to "recover" a partial argument list, and position-independent error shapes (bare_expression,invalid_assign_target,unclosed_delimiter,unexpected_end) always propagate. Unclosed tables and empty bracket lists now blame the opening delimiter with an "add a closing X" suggestion, matching the convention calls and indexes already followed. Lua.call_function/3returns the terse Lua error value forArgumentError(#354). A missingerror_value/1clause letArgumentErrorfall through toException.message/1, embedding ANSI codes and theat <source>:<line>:header in the reason returned bycall_function/3andpcall/xpcall. It now returns the §6.1-faithful terse string (e.g."bad argument #1 to 'pairs' (table expected, got string)");call_function!/3remains the escape hatch carrying the structured exception.- Compiled-chunk errors attribute the correct source line (#355). The
dispatcher baked per-call source lines into the call opcodes, so native
raise sites (
pairs("asdf"),error("boom")) in compiled chunks now include the line in their §6.1 prefix instead of omitting it. The hot path is unchanged (same single tuple-read per call). - Freshly
required modules now appear inpairs(package.loaded)(#356).cache_module_result/3wroteTable.datadirectly, bypassing the iteration-order bookkeeping, so a required module was reachable by direct index but never enumerated. Routing the write throughTable.put/3fixes the enumeration. tostringon a function now returns afunction: 0x...address instead of the bare string"function", matching PUC-Lua'stostring(print)-style output (builtins render asfunction: builtin: 0x...). The address is a deterministic per-value pseudo-pointer. Table rendering (table: 0x...) is unchanged.type(f)still returns"function".
1.0.0-rc.2 - 2026-06-10
The third release candidate for 1.0.0. It builds on rc.1 with a major
table-storage performance win, two non-standard os epoch helpers, and
a batch of protected-call and error-value correctness fixes that bring
pcall / xpcall and Lua.call_function/3 in line with Lua 5.3 §6.1.
The public API is unchanged from rc.1.
Performance
- Split-storage tables (Erlang
:array+ map) — dense positive-integer keys (1..n) now route to an Erlang:arrayfor O(1) functional read/write and dense iteration ordering, while strings, sparse/non-positive integers, and other key types stay in the hash map with the existing iteration bookkeeping (#328). String-keyed reads (globals, fields, metatable lookups) are unchanged. Table-heavy workloads improve 28–37% at n=1000 (Apple M4,luachunk path): Build −36%, Iterate/Sum −36%, Map+Reduce −37%, Sort −28%. Build, Iterate, and Map+Reduce now beat Luerl; Sort closes most of the gap.
Added
os.time_ms()andos.time_us()— non-standard extensions returning the current epoch in milliseconds / microseconds, for programs that need sub-second precision (os.time()is unchanged and still returns whole seconds). Both are current-time-only and are documented as extensions not present in PUC-Lua (#340).- The
os.clock()monotonic origin is now seeded ininstall/1rather than lazily on the first call, so elapsed time is measured from a stable startup point instead of drifting to whenever a program first happened to callos.clock()(#340).
Fixed
deflua/2guarded heads register under their real name (#344). A guarded head with no state argument (deflua clamp(a) when is_integer(a)) was registered under the name:wheninstead ofclamp, making it uncallable from Lua (calling it raised an undefined-function error). The macro now unwraps the:whenAST node to reach the real name, matching thedeflua/3(state-arg) variant, which was never affected.Lua.call_function/3returns the terse Lua error value, not the terminal render (#336). Its{:error, reason, _}previously surfaced the terminal-formatted error string — ANSI escape codes, theat <source>:<line>:header, theSuggestion:block, stack-trace frames, and a doubledLua runtime error: … runtime error:prefix — where a programmatic value was expected.reasonis now exactly whatpcallhands back (§6.1): thesource:line:-prefixed message for string errors, and the raw value (table/number/nil/false) passed through verbatim for non-string error objects. Notereasonmay therefore now be a non-string Lua value. The raising variantLua.call_function!/3is unchanged — it still raises aLua.RuntimeExceptioncarrying the rich formatted render.pcallpasses the raised error value through as-is (#334). Per Lua 5.3 §6.1,error(value)raises an arbitrary Lua value andpcallreturns it verbatim as its second result — previously non-string values were stringified (error({code = 1})came back as"table: 0x..."). Structured error objects, numbers, booleans, andnilnow survivepcall/xpcall, and thexpcallmessage handler receives the untouched value. String messages gain the referencesource:line:position prefix (suppressed byerror(msg, 0)); notepcall's second result may therefore now be a non-string Lua value. Host-facingLua.VM.RuntimeErrorrendering is unchanged.- Protected calls no longer roll back heap effects (#331). When a function
called via
pcall/xpcall(orLua.call_function/3from Elixir) raised an error, mutations made before the error — global writes, table field updates, upvalue assignments, metatable changes — were silently discarded, diverging from reference Lua. VM exceptions now carry the raise-time state, and protected-call boundaries recover it: heap state is kept, control state (call stack, open upvalues) unwinds. Thexpcallmessage handler now also observes those mutations, matching PUC-Lua's handler semantics.
Known issues
- Deep recursion is ~25% slower than rc.0. Carried forward from
rc.1: the configurable call-depth limit (#283) adds per-call
bookkeeping that recursion-dense workloads (e.g. naive
fib(30)) pay in full. Workloads that do real work per call are unaffected or faster. This remains a deliberate safety/speed tradeoff for the RC and will be addressed before1.0.0final.
1.0.0-rc.1 - 2026-06-02
The second release candidate for 1.0.0. It builds on rc.0 with a new
os and utf8 standard library, richer debug and error introspection,
a sizeable string.format and table performance pass, and a batch of
correctness fixes around upvalue lifetimes and Lua 5.3 semantics. The
public API is unchanged from rc.0.
Performance
A focused pass on the two hottest stdlib areas and the VM dispatcher. Numbers are pre-compiled-chunk throughput vs. rc.0, full Benchee runs on the same machine (Luerl and PUC-Lua used as drift controls, ±3%):
string.formatrewritten around iolist accumulation — literal runs and padding are appended to an iolist instead of repeatedly concatenating binaries, format flags are parsed once and dispatched on the integer specifier, and float conversion goes throughio_lib.format(#299, #317, #319, #316). The literal-heavy path is +143% (now ~2.7× faster than Luerl); width-flagged specifiers +83%; many-specifier strings +26%.- Plain-table
table.sort/table.concatfast paths for the common array-like case, with batched write-back in the sort path (#299, #318).table.sortis +35% at n=1000;table.concat-based string building is +66%. - Expanded VM dispatcher coverage to closures, varargs, multiple
returns, numeric/generic loops,
selfmethod calls, concat, and table opcodes (#275, #277). Object-oriented method dispatch is +41% (now faster than Luerl). - Batched table-literal construction through
Table.put_many/2in a single pass instead of element-by-element (#321).
Added
osstandard library, sandboxed:time,clock,date,difftime,getenv(no-op), and friends, with host-affecting calls neutered (#289).utf8standard library, plus aligned integer-arithmetic error wording (#258).debugintrospection: upvalue name tracking withdebug.getupvalue/debug.setupvalue(#285), anddebug.getinfonow populatesname/namewhatfrom the call site (#290).- Position captures
()in patterns acrossfind/match/gmatch/gsub(#288). - Configurable maximum call depth to bound recursion (#283).
- Allocation-bomb DoS hardening with documented sandboxing limits (#305).
- Structured error data on runtime errors (#246); arithmetic and bitwise type errors now thread operand hints into the message (#270).
Changed
- Rendered errors now lead with the source location, and ANSI colour is gated on whether output is a TTY (#304).
- Broader Lua 5.3 official test suite coverage: triaged and promoted
literals/goto/events/nextvar, and narrowed the remaining skips (pm,gc,constructs) to precise sub-ranges (#251, #282, #287, #294, #295). - README rewritten for 1.0 positioning with a quickstart and tour (#298),
runnable embedding examples under
examples/(#300), and@spec/ type definitions across the public API (#301).
Fixed
- Open upvalues are now closed at the exit of
do,if,while,for, andrepeatblocks (#286, #303), and the caller'sopen_upvaluesare restored after a nested execution returns (#245). requireno longer leaks the loaded module'sopen_upvaluesmap back to the calling chunk. Loading a module whose body created closures over its own top-level locals could alias the caller's locals to stale inner upvalue cells, breaking real-world libraries (e.g.luassert.assertions,luassert.array,luassert.spy) that follow the patternlocal x = require(...)→ manylocal functiondefs →x:method(...). As a side effect,Lua.call_function/3(public API) now preserves the caller'sopen_upvaluesacross calls (#244).- Integer divide and modulo by zero now match PUC-Lua semantics (#292).
table.unpackrejects oversized ranges instead of attempting a huge allocation (#293).gsubvalidates its replacement string and value (#291).- A parenthesised call or vararg now adjusts to a single value (#278).
- Function-declaration head names resolve during scope analysis (#274).
obj:method(...)calls expand multiple values in the argument list (#248).require()converts dotted module names to path separators (#242).
Known issues
- Deep recursion is ~25% slower than rc.0. The configurable call-depth
limit (#283) adds per-call bookkeeping that recursion-dense workloads
(e.g. naive
fib(30)) pay in full. Workloads that do real work per call are unaffected or faster. This is a deliberate safety/speed tradeoff for the RC and will be addressed before1.0.0final.
1.0.0-rc.0 - 2026-05-26
This is the first release candidate for 1.0.0. The library has been
rewritten on a new Elixir-native Lua 5.3 virtual machine, and the public
API is intended to be stable. Please report any regressions before final.
Added
- New Elixir-native Lua 5.3 virtual machine: lexer, parser, compiler, and register-based executor, with no Erlang or C dependencies.
- Standard library:
string(includingstring.formatwidth/precision,string.pack/unpack/packsize, and the full pattern engine forfind/match/gmatch/gsub),table,math(includingmath.fmod),debug,iostubs (sandboxed),os(sandboxed),package/require. _Gglobal table and Lua 5.3_ENVsemantics for global access.- Full metamethod dispatch:
__index,__newindex,__call, plus the arithmetic, comparison (including~=via__eqand<=/>=falling back through__lt), length, concat, andtostringmetamethods. - Varargs (
...), multiple returns, genericfor,goto/label,break, protected calls (pcall,xpcall). userdatasupport for passing arbitrary Elixir terms across the boundary.- Beautiful Lua-style stack traces and error messages with source line
tracking. Every runtime error carries line and source info (#214, #215),
and
attempt to call/attempt to indexerrors name the offending callee/target (#228). Inspectprotocol support for VM values returned across theLua.eval!/2boundary via display structs for tables, closures, userdata, and native functions (#218).- Mix tasks:
mix lua.eval,mix lua.suite,mix lua.bench(#220). - Lua 5.3 official test suite integration with per-file rationale for
suite files that are deferred as intentional non-goals (
main.lua,files.lua,attrib.lua,verybig.lua— shell-out, file I/O, and filesystemrequiresemantics that conflict with a sandboxed embedded VM) (#216). - Benchmark harness comparing against Luerl and PUC-Lua, with quick mode
and multi-
ninputs (#230) and asetup_luaport.shhelper (#225).
Changed
- VM backend: Luerl is no longer a runtime dependency. The library now
runs on its own Elixir-native VM. Luerl is kept only as a
:benchmark-env dependency for performance comparison. - Encoded value tags now use the new VM's internal representation:
{:tref, integer()}for tables (replacing:luerl.tref()),{:udref, integer()}for userdata (replacing:luerl.usdref()),{:native_func, fun}for Elixir-defined Lua callables (replacing:luerl.erl_func()), and{:lua_closure, _, _}for compiled Lua functions. - Parser error messages have a new format. The old Luerl-style
"Line 1: syntax error before: ';'"is now produced by the new parser (e.g."Expected expression"); user-visible string contents differ. - Chunks no longer require a separate "load" step —
Lua.Chunknow holds a compiled prototype and is reusable acrossLua.eval!/2calls. - 64-bit integer arithmetic and bitwise ops wrap on overflow per Lua 5.3 §3.4.1, instead of widening to bignums (Luerl's behaviour).
Lua.RuntimeExceptionandLua.CompilerExceptionare now publicly documented; user code can pattern-match and rescue them.
Removed
- The
{module(), atom(), list()}MFA encoding form is no longer accepted byLua.encode!/2. Use a function literal or adefluacallback instead.
Performance
- Right-size register tuple allocations (#153).
- O(N²) → O(N) upvalue collection in the closure handler (#154).
- O(1) upvalue access by storing upvalues as a tuple (#155).
- Fully tail-recursive CPS executor with line tracking moved off the heap (#156).
- Fast-path the executor dispatch loop (#223).
- Fast-path
Numeric.to_signed_int64for in-range integers (#227).
Fixed
- 64-bit integer overflow wrapping for arithmetic and bitwise ops (#177).
- Empty/missing-key table reads now return
nilper Lua 5.3 §3.4.11 (#179, #200). - Long-string
[[ ... ]]lexer handles embedded]and bracket levels like[==[ ... ]==], includingmain.lua-style headers (#180). - Comment tokens no longer leak past the lexer in expression lists (#182).
- Stdlib modules are pre-populated in
package.loadedsorequire"io"resolves (#184); module sentinel is set before executing required modules (#191). - For-loop variable now binds per statement, fixing register reuse (#195).
- Closure-handler crash on missing upvalue cells in
get_open_upvalueandset_open_upvalue(#196). _ENVsemantics for global variable access (#197).- Hex literal and string coercion in bitwise ops (#198);
math.fmodimplemented forbitwise.luaverification (#199). - Function declaration assigned to in-scope local rather than shadowing it (#185).
- Multi-return expansion no longer overflows the register tuple (#189).
- Pattern engine threads VM state through
gsubcallbacks and preserves capture order (#188, #190). - Atom values encode to strings (#158).
- Files containing only comments load successfully.
- Unicode characters supported in Lua scripts.
pairssurvives mid-iteration deletion by tracking dead keys (#202).- Metamethod closures receive operands through varargs (#203).
- Float division by zero yields ±
math.hugeinstead of raising (#204);//and%with a float-zero divisor returninf/nan(#211). - Lexer treats vertical tab and form feed as whitespace (#206).
- Table-library functions (
insert,remove,concat, etc.) honor__index,__newindex, and__len(#208). - Numeric
forcoerces string control values per Lua 5.3 §3.3.5 (#209). iois now exposed as a table of sandboxed stubs (#210).~=routes through the__eqmetamethod (#212);<=/>=fall back through__ltper Lua 5.3 §3.4.4 (#213).- Parser threads position info through bare-expression and unexpected-end errors (#222).
- Internal Lua VM frames pruned from
Lua.RuntimeExceptionstacks by default; opt back in withLua.new(debug: true)(#221). - Line number attribution for the first line of a chunk (#240).
string.packno longer emits compile warnings (#224).
0.4.0 - 2025-12-06
Changed
- Upgrade to Luerl 1.5.1
Fixed
- Warnings on Elixir 1.19
0.3.0 - 2025-06-09
Added
- Guards for encoded Lua values in
defluafunctionsis_table/1is_userdata/1is_lua_func/1is_erl_func/1is_mfa/1
Fixed
defluafunction can now specify guards when using or not using state
0.2.1 - 2025-05-14
Added
Lua.encode_list!/2andLua.decode_list!/2for encoding and decoding function arguments and return values
Fixed
- Ensure that list return values are properly encoded
0.2.0 - 2025-05-14
Changed
- Any data returned from a
defluafunction, or a function set byLua.set!/3is now validated. If the data is not an identity value, or an encoded value, it will raise an exception. In the past,Luaand Luerl would happily accept bad values, causing downstream problems in the program. This led to unexpected behavior, where depending on if the data passed was decoded or not, the program would succeed or fail.
0.1.1 - 2025-05-13
Added
Lua.put_private/3,Lua.get_private/2,Lua.get_private!/2, andLua.delete_private/2for working with private state
0.1.0 - 2025-05-12
Fixed
- Errors now correctly propagate state updates
- Fixed version requirements issues, causing references to undefined
luerl_new - Allow Unicode characters to be used in Lua scripts
- Files with only comments can be loaded
Changed
- Upgrade to Luerl 1.4.1
- Tables must now be explicitly decoded when receiving as arguments
defluaand other Elixir callbacks