Stream: show and tell

Topic: At the risk of heresy, here is... Roc on LuaJIT?!?!


view this post on Zulip Peter Marreck (Oct 07 2026 at 04:28):

Hah, amazing! Hey, they say the best programmers put out negative LOC many days! ;)

view this post on Zulip Peter Marreck (Oct 07 2026 at 04:47):

@Luke Boswell

Yeah, I didn't intend to upstream it (unlikely to be interest, but who knows). I just wanted to see what was possible to do. And yeah, this might be an example of how to lower from LIR to implement other possible backends.

And yes, regarding the platform/app abstraction:

A Roc platform (as you may know) is two halves: a Roc module (main.roc) that declares the app's entrypoints and the effects it may call ("hosted functions"), and a native host that implements those effects. I reused the compiler's half unchanged and replaced the host half with Lua.

Compiler side. roc build --target=luajit lowers the program exactly as the interpreter build does, down to ARC-complete LIR. The LIR already marks the platform boundary:

LuaEmitter emits a platform-mode chunk shaped like APP = function(hosted) ... return { rt = ..., entrypoints = {...} } end.

Binding by name, not position: Native Roc dispatches hosted calls by position in an alphabetically sorted function list. The Lua output binds each hosted procedure once at load with rt.hosted(host, symbol), which looks the function up by its symbol and fails loudly if the host lacks it. That whole class of silent mismatch can't happen here.

The host: A Lua file returning { run = function(app, argv) }. LuaHost.writeProgram concatenates a shebang, the app chunk, the host chunk and the line HOST.run(APP, arg) into one runnable Lua file. run then:

  1. builds the table of hosted functions
  2. calls app(hosted) to get the runtime and the entrypoints
  3. invokes an entrypoint through rt.call_entry, which converts a Roc crash into a structured error and a Lua stack overflow into native Roc's overflow report (always a tricky business, getting errors across this line; might be incomplete)
  4. maps the outcome to the same stdout, stderr and exit status the native host would give.

hosts/default.lua (about 30 lines) is the whole host for headerless apps, mirroring default_platform/linux_runtime.zig.

Where the host comes from:

Crossing the boundary: Values cross as Lua tables in a fixed layout:

The host-services interface (RocOps) maps as follows:

Effects. The basic-cli host is a port of its Rust host to LuaJIT's FFI over libc: files, directories, environment, processes, TCP sockets, SQLite and HTTP. Rust's buffering behavior is reproduced, and errno values are mapped to the same IOErr tags.

Verification. Each platform has a differential conformance suite that builds every app natively and for LuaJIT and requires identical stdout bytes, stderr bytes and exit status:

My own Lua platform (luajit_backend/platforms/lua) takes the opposite shape. It has one hosted function, Host.call!(op, request), carrying values encoded as LuaValue bytes, with the codec written once in Roc and once in Lua. Its native host embeds LuaJIT, so native and LuaJIT builds run the same host.lua for every effect.

view this post on Zulip Notification Bot (Oct 07 2026 at 04:49):

This topic was moved here from #show and tell > At the risk of heresy, here is... a sneak peek at a neat WIP by Peter Marreck.


Last updated: Oct 15 2026 at 17:56 UTC