# Wasm AOT applications A Lua 5.1 application can run inside Nupp's Wasm host while selected `@aot` functions run as WebAssembly side modules in the same linear memory. Use `nupp.mem.span` and `nupp.mem.array` when Lua and compiled code need zero-copy arrays of Nupp structs: ```nupp local span = nupp.mem.span local array = nupp.mem.array local struct Sample value: float end @aot(vectorize = false) local function double(exclusive out: span.WriteSpan): nil for index = 1, #out do out[index].value = out[index].value * 2 end end ``` The rest of the program remains Lua 5.1. The annotated body is replaced by one Lua C-closure call into compiled Wasm; its loop performs direct loads and stores without a Lua or JavaScript call per element. ## Wasm storage The Lua 5.1 target fixes portable calling conventions. During module initialization, `representation.cstorage` selects the compatible Wasm implementation when its host is present. That implementation supplies physical storage, exact integers, reference-valued structs, and transfer leases together. Lua receives no numeric address and cannot construct a pointer from an integer. The application target selects Lua 5.1 and requires Wasm AOT: ```lua app = { kind = "bundle", entries = {"main"}, output = "dist/app.lua", outDir = "build/app", dialect = "lua51", aot = "require-wasm", } ``` `require-wasm` fixes the AOT target to `wasm32-unknown-emscripten`. The default feature tier is scalar; `aotFeatures = "simd128"` selects Wasm SIMD and narrows the set of hosts that may load the result. ## Lua values An admitted function that returns a fresh table or string uses the `lua-builder` entry mode. Its side-module closure receives the embedded VM's `lua_State`, keeps unfinished values rooted on that stack, and returns an ordinary Lua value: ```nupp @aot(vectorize = false) local function summary(name: string, count: integer): {[string]: any} return {name = name, count = count, ready = true} end ``` This is the same source and builder subset used by native AOT. The compiler chooses the entry mode; the application still selects only `require-wasm`. Fresh numeric tables, rooted `string.byte` and `string.sub` calls, and ordinary append-only concatenation lower through the same entry. Specialized streaming parsers may still use [`nupp.codec.valuebuilder`](https://github.com/nupp-lang/nupp/blob/main/src/nupp/codec/valuebuilder.nupp) inside that boundary. ## Struct arrays `array.new` takes a struct value as its checked type witness and returns zeroed storage in the host's linear memory: ```nupp local samples = array.new(new Sample(), 128) local writable = samples:write() writable[1] = new Sample(3) drop writable local readable = samples:read() print(#readable, readable[1].value) ``` Indexing and length are Nupp operations. Lua 5.1 output lowers them to checked `get`, `set`, and `count` access, so it does not rely on table `__len` support. A writable span is affine and holds an exclusive borrow until it is passed to a kernel, dropped, or discharged at a scope boundary. The provider uses the explicit wasm32 layout for booleans, floats, numbers, integers, fixed-width integers, fixed arrays, and nested structs. Every side module registers the size, alignment, and field offsets its C compiler used. The generated Lua wrapper compares those values with the checked provider's layout before publishing the kernel. ## Artifacts Emscripten 6.0.8 compiles the verified C rendering as a retained side module. Name another compiler with `NUPP_WASM_CC` when `emcc` is not on `PATH`: ```bash NUPP_WASM_CC=/opt/emsdk/upstream/emscripten/emcc nupp build --target app ``` A bundle target writes one transportable group: ```text dist/app.lua dist/aot/units.json dist/aot/src/main.scalar-.wasm ``` The manifest names each side module, registrar, unit identity, tier, and target. Content-addressed filenames let a host cache the Lua VM, Lua bundle, and kernels independently. `emit-wasm` writes and packages the same Wasm artifacts but keeps the ordinary Lua bodies active; `require-wasm` installs the compiled wrappers. ## Browser package `scripts/browser-app` builds a Lua 5.1 bundle and writes everything a static server needs. Name the project, target, and destination: ```bash NUPP_WASM_CC=/opt/emsdk/upstream/emscripten/emcc \ NUPP_LUA51_SOURCE=/opt/src/lua-5.1.5/src \ scripts/browser-app . app dist/browser ``` The destination contains one manifest and entry module beside independently cacheable assets: ```text dist/browser/nupp-browser-app.mjs dist/browser/nupp-browser-app.json dist/browser/app-.lua dist/browser/nupp-app-.mjs dist/browser/nupp-app-.wasm dist/browser/aot/..wasm dist/browser/worker-lane.mjs ``` `worker-lane.mjs` is packaged only where the application reached [worker tasks](../../runtime/concurrency/workers.md), and the manifest names it under `workers` when it did. An HTML module can start the application by importing the entry: ```js const application = await import("./nupp-browser-app.mjs"); const result = await application.ready; console.log(result); ``` The entry creates a module Worker and starts one application. A bundle may return no value or one JSON string; `ready` answers the decoded value. Call `application.cancel()` while `ready` is pending to resume the Lua cleanup path with cancellation, and call `application.close()` when the Worker is no longer needed. The command builds the reusable host under `build/wasm-app-runtime` once and reuses it for later applications. Set `NUPP_BROWSER_RUNTIME` to a separately built runtime package to share the same host across projects. The package includes Lua's copyright notice and records the exact Emscripten version, digests, and byte sizes of the host assets. Tagged releases publish the same package as `nupp-browser-runtime.tar.gz`. The browser loader verifies the Lua bundle, host Wasm, and side-module bytes before execution. Chromium acceptance runs plain Lua 5.1, scalar struct AOT, SIMD struct AOT, the browser platform providers, cancellation, runtime errors, missing side modules, and worker tasks in their scalar and SIMD packages through an HTTP server. ## Browser platform services Browser facades select catalog-backed implementations for HTTP, URI, suspension, time, random bytes, UUIDs, and persistent string storage when required. SHA-256 and HMAC-SHA256 are ordinary portable functions: ```nupp local crypto = nupp.crypto local storage = nupp.io.storage local time = nupp.time time.sleep(10) local token = crypto.randomBytes(32) storage.set("session", token) local restored = storage.get("session") print(restored and #restored or 0) ``` A browser target uses ordinary entry modules: ```lua app = { kind = "bundle", entries = {"main"}, dialect = "lua51", } ``` Facades resolve compatible providers while requiring their modules. The checked Lua provider suspends the application and sends one effect to the Worker. The Worker uses `fetch`, `setTimeout`, Worker clocks, Web Crypto, or IndexedDB and resumes Lua with the result. Pure Lua work and AOT kernels do not cross the effect boundary. The host also supplies `host.workers`, so a browser application runs [worker tasks](../../runtime/concurrency/workers.md) on a bounded pool of lane Workers. Each lane boots this same verified manifest in its own Lua 5.1 Wasm state, including the packaged AOT side modules, and receives work through the same effect framing. Neither Wasm threads nor shared memory is involved, so a page serving these assets needs no cross-origin isolation headers. SHA-256, HMAC-SHA256, and UUIDs retain the standard Nupp APIs: ```nupp local digest = nupp.digest local mac = nupp.mac print(digest.hexDigest("sha256", "payload")) print(mac.hexDigest("hmac-sha256", "key", "payload")) print(nupp.uuid.v4(), nupp.uuid.v7()) ``` Persistent storage maps string keys to string values. Each application package uses an IndexedDB database derived from its content digest; callers embedding the runtime can provide another database name or a storage adapter. ## WebGPU The browser GPU provider can run admitted `@aot(target = "gpu")` map kernels through WebGPU while keeping their storage in Wasm memory. See [gpu.md](gpu.md#browser-gpu-kernels) for the generated WGSL profile and the browser provider's convenience operation. ## Application host `runtime/wasm/build-app-host.sh` builds official Lua 5.1 with the memory bridge and dynamic linker. It takes the output module and an official Lua source directory: ```bash EMCC=emcc runtime/wasm/build-app-host.sh \ dist/nupp-app.mjs /opt/src/lua-5.1.5/src ``` The generated ES module runs in Node, a Worker, or a browser. Load the artifact manifest, then pass its modules and Lua bundle through `runtime/wasm/app-runtime.mjs`; the runtime allocates a private stack for every retained side module, calls its registrar against the host's one `lua_State`, and only then starts the bundle. The host exposes start, resume, cancellation, status, and result operations. A suspended application yields one protocol string; a completed application returns no value or one structured-result string. The Worker owns effect dispatch and sends each response through the resume operation. The host caps linear memory at 256 MiB. Packaged applications default to 256 effects, 4 MiB requests, 8 MiB responses, 1 MiB storage values, and a 30-second cooperative deadline. The entry module also owns a hard Worker deadline; terminating the Worker stops code that never reaches a suspension point. Applications may embed the C host and reproduce that order directly. The binding ABI is the Lua 5.1 C API plus `nupp_wasm_pointer_address`; there is no virtual filesystem or JavaScript kernel trampoline. ## Limits Wasm AOT is not a whole-language Nupp-to-Wasm lowering. General Nupp lowers to Lua 5.1 and runs on the embedded VM, while admitted pointer kernels and Lua-building entries lower from the verified AOT IR through C to Wasm. Pointer kernels use `nupp.mem.span` spans. A `lua-builder` entry instead receives the embedded VM's `lua_State` and constructs fresh tables or strings through the public Lua 5.1 API, with the same admitted subset and rooting rules as a native AOT builder. Ordinary `cstorage` and typed span references use the target-compatible storage provider. Raw `ffi`, arbitrary `cinterop`, and native Lua modules remain unavailable. Pure Lua dependencies work when included by the target. The artifact catalog supplies compatible platform providers. Wasm storage does not supply filesystem, process, foreign C interoperability, or arbitrary third-party services. Browser HTTP accepts `http` and `https` absolute URIs. String and narrow `http.Reader` upload sources are accepted; reader uploads are collected before Fetch begins. Declared upload lengths are checked. File uploads require a file host and remain unavailable in browsers. Neither request nor response streaming is advertised. Fetch response chunks are bounded by `maxBytes` and the host byte limit, then copied directly into one writable Wasm lease. That allocation is transferred to an ordinary `io.Reader`; creating the reader does not copy it or create a Lua string. An explicit `read` materializes its returned string; `readSpan`, `readInto`, and `transferTo` use bulk memory operations. Fetch takes one owned copy of request bytes, so asynchronous memory growth cannot change its input. Host transfers use no base64. Cancellation and teardown release leases and retained response chunks. The portable URI provider covers the public absolute-URI suite and basic relative resolution; it does not implement the native provider's complete URI normalization surface. Lua 5.1 cannot yield through an arbitrary C function. Nupp's Lua 5.1 cleanup lowering uses a coroutine trampoline, so owned scopes can suspend and still drop their resources after completion, failure, or cancellation. A suspending call must remain outside a call to a C function such as `assert(client:send(request))`; branch on the returned error before calling `assert` instead. ::: seealso - [ahead-of-time.md](index.md) for the admitted kernel subset and numeric guarantees - [portable-compiler.md](../../projects/portability/compiler.md) for the separate compiler bundle used by the playground - [NEP 13](../../../neps/0013-lua-in-wasm-aot.md) for the binding decision :::