The Lua backend emits a single uplox_<grammar>.lua module per grammar.
Targets Lua 5.3 and later (uses integer division and bitops where useful;
no LuaJIT-specific syntax).
For a grammar named calc:
local M = require('uplox_calc')
-- Construct a parser over an input string.
local parser = M.new(input)
-- Parse. Returns true on success; sets parser.root, parser.error.
if not parser:parse() then
error(parser.error)
end
local root = parser.root -- table with .kind, .is_terminal, .children, …
-- Symbolic enums (token / non-terminal indices).
M.TOK.NUMBER -- integer token kind
M.NT.EXPR -- integer non-terminal kind
M.token_name(kind) -- string
M.nt_name(kind) -- stringEach Node is a plain Lua table with these fields:
{
is_terminal = true|false,
kind = <integer>,
production = <integer or -1>,
line, column,
text = '<lexeme>', -- terminals only
children = { ... }, -- 1-indexed list of child nodes
}parser:set_token_filter(function(parser, kind, text, len)
-- Return a (possibly rewritten) terminal kind.
return kind
end)
parser:set_post_reduce(function(parser, prod, node)
-- Update host state visible to the next filter call.
end)The runtime invokes the filter on every freshly fetched lookahead and again after every reduction; the post-reduce hook fires before the filter re-runs. Same ordering and contract as the C and C++ backends.
Tokens declared with %balanced='<close>' in the grammar source come
through to the emitted scanner via a token_balanced Lua table (one
entry per token, holding the close-delimiter byte or 0). After the
DFA matches a balanced token, Parser:next_token() extends the
match by counting nested open/close pairs in the input until depth
returns to zero. An unmatched close sets parser.error to
"unterminated balanced token..." and aborts the scan; parser:parse()
then returns false and the host reads parser.error.
tests/test_gen_lua.py::test_lua_typedef_name_hack_end_to_end
implements the C typedef-name hack in Lua with closures over a shared
typedef table. Use it as the template for any feedback-style grammar.
M.new(input) returns a fresh parser table. There is no module-level
state mutated during parsing. Two grammars produce two distinct modules
under different require paths.
When the grammar carries v3 AST annotations, the Lua module exposes a typed AST API:
M.AST_KINDS— table mapping kind name -> integer ordinal.Parser:parse_ast()— callsparse()then builds and stores the AST root. Returns the root table on success,nilon failure (inspectparser.error).
Each AST node is a Lua table with kind = "KindName", pos = { start_line, start_column, end_line, end_column }, and one
entry per declared field. List fields are Lua arrays; token
fields are references to the parse-tree leaf tables (which
carry .text, .line, .column, .kind).
local M = require('uplox_calc')
local ast = M.new('(1 + 2) * 3'):parse_ast()
-- ast.kind == 'BinOp', ast.op.text == '*', ast.lhs.kind == 'BinOp'Grammars without v3 annotations emit byte-identical Lua to before — the surface is purely additive.
Same as C / C++: no semantic-action injection beyond the post-reduce hook (or the v3 auto-AST surface when annotations are present), no error recovery. Walk the parse tree.