A pragmatic-core Lua 5.4 interpreter written in Hoon, runnable on an Urbit ship.
It is a hand-written lexer → recursive-descent parser → tree-walking interpreter. Lua values, tables, and closures live in a purely-functional store that is threaded through evaluation, so closures capture upvalues correctly and tables are mutable — all without leaving Nock.
print("hello, " .. "urbit") --> hello, urbit
print(10 / 3) --> 3.333333333333333
local function fib(n)
if n < 2 then return n end
return fib(n-1) + fib(n-2)
end
print(fib(10)) --> 55desk/
lib/lua.hoon -- the interpreter (lexer + parser + evaluator)
mar/lua.hoon -- a %lua source mark, for storing .lua files in Clay
gen/lua.hoon -- the `+lua` generator: run a Lua string and print stdout
ted/lua.hoon -- a spider thread: run a Lua program that can do Urbit IO
sys.kelvin -- [%zuse 408]
examples/ -- sample Lua programs
On a running ship (real or fake), create a desk from this desk/ directory and
run the generator. For a fakeship:
> |new-desk %lua
> |mount %lua
Copy desk/lib/lua.hoon, desk/mar/lua.hoon, desk/gen/lua.hoon (and, if you
want to commit .lua files via the mark, your base ship's mar/mime.hoon and
mar/txt.hoon) into the mounted %lua desk, then:
> |commit %lua
> +lua 'print(1 + 2)'
3
The simplest path while developing is to drop the three files onto your %base
desk instead, so +lua runs live with no desk pinning:
> +lua 'local s=0 for i=1,100 do s=s+i end print(s)'
5050
run:lua is the library entrypoint: (run:lua src=@t) parses and evaluates a
Lua source cord and returns its stdout as a (list tape).
/ted/lua.hoon runs a Lua program as a spider
thread — and the program can perform real Urbit effects, written in Lua:
> -lua 'sys.flog("counting down...") for i=3,1,-1 do sys.flog(i.."...") sys.wait(1000) end return "liftoff at "..sys.now()'
counting down...
3...
2... (one real second passes between each line)
1...
[~zod ...] <- the thread result: "liftoff at <time>"
The sys table exposes strand effects:
| Lua call | effect |
|---|---|
sys.wait(ms) |
sleep ms milliseconds on the timer (behn) vane |
sys.flog(s) |
print a line to the dojo, live |
sys.now() |
read the current time (@da) back into Lua |
This is the coroutine machinery doing real work. The whole program runs as one
coroutine; each sys.* call is coroutine.yield(tag, …), which suspends the
interpreter from any depth (here, inside a for loop) and hands an effect
request to the Hoon strand. The strand performs it with strandio, then
resumes the program with the result — so sys.now() returns a value that
flowed back in from the runtime, and sys.wait genuinely parks the Lua program
across a timer event. Adding more effects (scry, poke, …) is a new sys.*
line in the prelude plus a branch in the strand driver. A pure program that
never calls sys.* just runs to completion and returns its stdout.
- numbers: integers (
@sd) and floats (@rd), with Lua's int/float promotion (+ - *stay integer;/and^produce floats;// %follow operands) - bitwise operators
& | ~ << >>and unary~(64-bit two's-complement) - strings with escapes, and
..concatenation;#length nil/ booleans /and/or/not(with short-circuit)- locals and globals, multiple assignment (
a, b = b, a) - functions, closures with mutable upvalues, recursion
- varargs (
...) and multiple return values - control flow:
if/elseif/else,while,repeat/until, numericfor, genericfor ... in,break,goto/::label:: - per-iteration loop-variable capture (Lua 5.4 semantics): closures made in different iterations capture distinct values
- tables: array + hash parts, constructors,
t[k]/t.k,#t - metatables / metamethods:
__index,__newindex,__add__sub__mul__div__mod__pow__idiv__unm,__eq__lt__le,__concat,__len,__call,__tostring;setmetatable/getmetatable - true coroutines (real suspension from any depth):
coroutine.create/resume/yield/status/running/isyieldable— see notes below - standard library:
print,type,tostring,tonumber,pairs,ipairs,next,select,assert,error,rawget/rawset/rawequal/rawlen;math.*(floor ceil abs sqrt max min pi);string.*(len sub upper lower rep, and full C-styleformat: flags, width,.precision,d i u o x X e E f g G c s q %);table.*(insert remove concat)
These are true coroutines, not a generator fake. yield suspends from any
depth — inside a loop, a nested call, an if — and resume re-enters exactly
where it left off, with values passed both ways. So all of these work as in
real Lua: infinite generators, resume-value passing, side effects interleaved
between resumes, and nested (asymmetric) coroutines.
-- yield from inside a loop; driven across resumes
local function gen(n) for i = 1, n do coroutine.yield(i) end end
local co = coroutine.create(gen)
print(coroutine.resume(co, 3)) --> true 1
print(coroutine.resume(co)) --> true 2
print(coroutine.resume(co)) --> true 3
print(coroutine.resume(co)) --> true (body returned)
print(coroutine.resume(co)) --> false cannot resume dead coroutine
-- resume args flow back as yield's return value
local c = coroutine.create(function() local x = coroutine.yield(10); return x + 1 end)
print(coroutine.resume(c)) --> true 10
print(coroutine.resume(c, 99)) --> true 100The main thread runs on the recursive tree-walker (fast, common case). A
coroutine body instead runs on a small CEK step machine: its continuation
is reified as a flat (list kframe) stored in the coroutine, so yield simply
parks that stack and returns to the resumer, and resume spins the machine
again from the saved state. Both evaluators share every leaf arm (arithmetic,
indexing, metamethods, builtins), so semantics stay identical. Sub-expressions
that provably contain no call skip the machine and run atomically, so a hot
yield-free loop body inside a coroutine keeps tree-walker speed.
coroutine.wrapis not provided (usecreate+resume).- You cannot
yieldacross a builtin or metamethod boundary — e.g. ayieldinside an__indexfunction, apcall, or atable.sortcomparator errors (yield outside coroutine). This mirrors Lua's historical "attempt to yield across a C-call boundary." - A runtime error inside a coroutine body propagates as a host crash rather
than being caught and returned as
false, msg. numeric forwith float bounds and ayieldin its body is unsupported (integer numeric-for, while, and generic-for all work).
- the coroutine cases above (wrap, yield-across-builtin, float-for yield)
gotointo the scope of a local is not statically rejected__indexchains /__eqcover the common cases; metamethods are not invoked bystring.format("%s", t)ortable.concat(they use rawtostring)
Tree-walking interpreter over a functional store, interpreted through Nock — so
expect scripting speeds, not LuaJIT. Timed with the runtime's own ~> %bout
hint on a fakeship, wrapping (run:lua src):
| benchmark | work | wall time | per unit |
|---|---|---|---|
| lex + parse + stdlib setup | — | 0.43 ms | fixed start |
s = s + i loop |
1,000,000 | 35.5 s | ~35 µs/iter |
Ackermann A(3,5) |
42,438 | 3.42 s | ~81 µs/call |
Ackermann A(3,6) |
172,233 | 14.1 s | ~82 µs/call |
Ackermann A(3,7) |
693,964 | 57.4 s | ~83 µs/call |
naive recursive fib(25) |
242,785 | 34.2 s | ~141 µs/call |
("work" = loop iterations, or the exact number of function calls made.)
The Ackermann rows are the interesting ones: per-call cost stays flat (~82 µs) from 10k to 700k calls. That's the point — a call that creates no closures or tables reclaims its cells on return, so deep/wide recursion runs in bounded memory and scales linearly instead of growing the store until the loom is exhausted. The same holds for loops (flat from 10⁵ to 10⁶ iterations).
See examples/ for runnable programs, including ackermann.lua and
an infinite coroutine prime generator (primes.lua).
Lua's AST is naturally a recursive type (expr contains expr). The vere build
this was developed against crashes its compiler (dig: over) when pattern-matching
(?- / ?=) over a directly self-recursive $%. To work around it, recursive
child positions in the expr / stmt / tfield molds are typed as * (raw
noun) so the declared types are non-recursive, and each evaluator arm ;;-clamps
a node back to its typed mold before dispatching. If your toolchain doesn't have
that bug, the molds can be written with ordinary self-reference.