Wasm AOT applications#

The browser package runs LuaJIT inside a retained v86 guest. Select browser services with the target's host:

app = {
   kind = "bundle", entries = {"main"}, sources = {"src"},
   output = "dist/app.lua", host = "browser",
   aot = "require-wasm",
}

scripts/browser-app . app dist/browser packages the program, verified guest, independent Wasm kernels, worker entry, and matching sources and notices. Linux builds the pinned guest; other hosts set NUPP_BROWSER_GUEST_DIR to a verified source-built guest package. Install the editors/playground Node dependencies before packaging. The Wasm kernels are compiled and linked by nupp itself; no Emscripten is needed.

Independent Wasm kernels#

Pure kernels exchange exact scalar slots and bounded copied spans with their own Wasm memory. Struct field offsets are converted between guest and Wasm layouts. The transfer batch limit is 2 MiB and kernel memory is capped at 64 MiB. This boundary is best for substantial kernels rather than tiny calls. emit-wasm exports kernels while leaving ordinary Lua bodies active; require-wasm installs their generated wrappers.

An artifact manifest under the target's AOT output names each content-addressed module, registrar, unit identity, feature tier, target, and bridge entries. The browser packager copies those modules as verified assets. Guest FFI cannot load a Wasm side module directly.

Guest-native AOT#

Functions that construct Lua tables or strings use the guest's Lua C API and must be compiled for the guest rather than as independent kernels:

app = {
   kind = "bundle", entries = {"main"}, sources = {"src"},
   output = "dist/app.lua", host = "browser",
   aot = "require", aotTarget = "i686-unknown-linux-gnu",
   aotFeatures = {maximum = "baseline"},
}

nupp compiles and links the guest's library itself, with the AOT runtime linked in, because the guest has no native provider to hand it one. The modeled triple describes the layout; the library is built for the guest's musl, not glibc. The packager verifies i386 ELF identity and installs adjacent shared libraries before application or worker startup. Native libraries are limited to one MiB combined and share the seven-MiB startup budget with the application.

Guest-native AOT executes through x86 emulation. It is not an independent Wasm kernel or a promise of equivalent speed. require-wasm rejects Lua-C-API builders; use native require or ordinary LuaJIT with aot = "off" for them.

Browser package#

The destination contains a manifest and entry module beside independently cacheable assets:

dist/browser/nupp-browser-app.mjs
dist/browser/nupp-browser-app.json
dist/browser/nupp-audio.mjs
dist/browser/nupp-audio-worklet.mjs
dist/browser/app-<digest>.lua
dist/browser/guest/<build-key>/guest-manifest.json
dist/browser/aot/<unit>.<digest>.wasm
dist/browser/native/<digest>/<library>.so
dist/browser/worker-lane.mjs

A page starts the application by calling run() from nupp-browser-app.mjs; importing the module starts nothing, so the page passes its options in that one call:

const application = await import("./nupp-browser-app.mjs");
const result = await application.run({limits: {perRun: {deadlineMs: 60000}}});

run settles with the application's result and may be called once. Its limits are the only bound on how long the application runs. cancel() asks a running application to stop, and close() terminates its Worker. A page that hosts the runtime itself calls runPackagedNuppLuaJITApp() from app-runtime.mjs instead. The application may return no value or one JSON-compatible value.

run's host option answers the application's nupp.host requests, by kind, on the page or in the application's Worker, and the entry's push sends it inbound messages; nupp-audio.mjs plays an outbound stream of samples. See Host requests. Worker tasks use a bounded pool of guest lanes and the same verified manifest.

Browser facades select implementations for HTTP, files, suspension, time, random bytes, UUIDs, WebGPU, and worker tasks. Effects cross a bounded protocol; ordinary Lua and AOT kernels stay within the guest or kernel until they request a service. The page answers each request when it settles rather than when the rest of its batch does, so a short sleep shipped beside a task scope's deadline timer ends on time. Only a request that lends guest memory is answered in the frame that carried it.

Limits#

Wasm AOT is not a whole-language Nupp-to-Wasm lowering. General Nupp emits LuaJIT and runs in the guest; only admitted kernels lower through LLVM to Wasm. Independent kernels cannot use raw FFI, arbitrary C interop, or guest-native modules. Guest-native AOT cannot be loaded as an independent Wasm kernel.

Pure Lua dependencies work when selected by the target. Browser files are application-scoped; arbitrary host paths and processes remain unavailable. HTTP accepts absolute http and https URIs, and a request body or response is bounded by what one turn may carry.

A packaged page application runs under limits, two tables of positive integers. perTurn bounds one turn, which on the page is one guest frame and its response, and on a worker lane is one task:

perTurn key Default Bounds
maxEffects 256 requests one turn carries
maxEffectBytes 4 MiB bytes of the turn's effect frames
maxResponseBytes 8 MiB bytes of the turn's responses, or of the result
computeMs 30,000 guest work from a response to its next frame, or from boot to the first

perRun takes maxEffects, maxEffectBytes, maxResponseBytes and deadlineMs, counted over the whole run; the deadline starts once the guest is ready. It is unbounded unless a limit is named, so a frame loop runs for as long as the page keeps it. A package whose program uses worker tasks raises perTurn.maxEffects to 262,144 and both byte limits to 256 MiB, because a lane's turn is a whole task.

Past a limit the run fails with "browser application exceeded limits.perTurn.maxEffects (256)", naming the limit and its value; a guest past computeMs is terminated rather than awaited. runPackagedNuppLuaJITApp(url, {limits}) layers its tables over the manifest's key by key, so {perRun: {deadlineMs: 60000}} bounds a run without restating the rest. A key outside the two tables is refused.