Run Luau (Roblox Lua) in Your Browser with luau-wasm
luau-wasm packages the Luau compiler and virtual machine as a Python extension that can run inside Pyodide. The result is a small, focused bridge: load Python in the browser, install a WebAssembly wheel from PyPI, pass Luau source to Python, and receive captured print() output.
This tutorial follows the package maintainer’s public API and playground rather than claiming independent zbrandco testing. The project is at version 0.1a0 and marked Alpha on its PyPI project page, so treat it as an experiment or prototype dependency until its compatibility and release policy mature.
What luau-wasm Actually Provides
Luau is the gradually typed language derived from Lua and used by Roblox. The official Luau introduction explains its syntax, type annotations, and relationship to Lua. luau-wasm is not Roblox Studio and does not recreate Roblox engine APIs. It embeds the language compiler and VM in a CPython extension compiled for Pyodide.
The maintainer’s luau-wasm repository documents one main function: execute(source). Each call creates a fresh sandboxed Luau state, executes the supplied source, captures calls to print(), and returns that output as a Python string. run is an alias. Compile and runtime failures raise luau_wasm.LuauError.
That deliberately narrow API is enough for examples, teaching tools, syntax demonstrations, and browser-based experiments. It is not evidence that arbitrary Roblox projects, native modules, networking, persistent files, or game-engine services will work.
Prerequisites
Use a current browser capable of running the current Pyodide release. The package’s build targets Pyodide 314 and CPython 3.14, as shown in its published project configuration. Do not substitute an older Pyodide URL and assume the binary wheel will be compatible.
You also need a page served over HTTP or HTTPS. A local static server is sufficient during development. Serving the file avoids the origin and module-loading restrictions that commonly appear when a browser opens an HTML file directly from disk.
The workflow loads code from a CDN and installs a package from PyPI, so the browser must be allowed to reach both services. For a production site, pin the Pyodide version, apply an appropriate Content Security Policy, and review the dependency and hosting chain before deployment.
Step 1: Try the Maintainer’s Playground
The fastest orientation is the maintainer’s hosted Luau WASM playground. Its source is part of the same repository as the package. The page loads Pyodide, loads micropip, installs luau-wasm, and enables a text area that sends Luau source to execute().
Using the official playground first answers a useful diagnostic question: can the current public distribution load in your browser and network environment? If it cannot, inspect the browser console and network panel before copying the integration into another application.
The playground is a demonstration, not a service-level guarantee. Availability, CDN behavior, and alpha-package compatibility can change.
Step 2: Install the Wheel in Pyodide
In a Pyodide Python session, use the package’s documented installation sequence:
import micropip
await micropip.install("luau-wasm")
import luau_wasm
output = luau_wasm.execute('print("Hello from Luau")')
print(output)
micropip.install() is asynchronous and therefore needs await. Pyodide’s package-loading documentation says micropip can install pure-Python packages and compatible Emscripten or wasm32 binary wheels. It also validates PyPI wheel downloads against hash digests from the PyPI JSON API.
The package is not part of Pyodide’s built-in package set. Loading micropip alone does not install luau-wasm; the explicit install call is still required.
Step 3: Load Pyodide from a Web Page
The following page uses the versioned CDN path shown by the current Pyodide documentation. Keeping the version in the URL makes the dependency visible and avoids silently moving to a different runtime.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Luau in Pyodide</title>
</head>
<body>
<textarea id="source">print("Hello from Luau")</textarea>
<button id="run" disabled>Run</button>
<pre id="output">Loading…</pre>
<script src="https://cdn.jsdelivr.net/pyodide/v314.0.3/full/pyodide.js"></script>
<script>
const button = document.querySelector("#run");
const source = document.querySelector("#source");
const output = document.querySelector("#output");
let pyodide;
async function initialize() {
pyodide = await loadPyodide();
await pyodide.loadPackage("micropip");
await pyodide.runPythonAsync(`
import micropip
await micropip.install("luau-wasm")
import luau_wasm
`);
button.disabled = false;
output.textContent = "Ready";
}
button.addEventListener("click", () => {
try {
pyodide.globals.set("luau_source", source.value);
const result = pyodide.runPython(`
import luau_wasm
luau_wasm.execute(luau_source)
`);
output.textContent = result || "";
} catch (error) {
output.textContent = String(error.message || error);
}
});
initialize().catch((error) => {
output.textContent = "Initialization failed: " + String(error);
});
</script>
</body>
</html>
The page disables the Run button until installation completes. It also places the editor value into Pyodide’s global namespace rather than interpolating user text into a Python source string. That separation avoids quoting errors and makes the boundary between JavaScript, Python, and Luau easier to follow.
Keep Initialization Separate from Execution
Loading Pyodide and installing a wheel are setup operations, so they should not run again every time someone presses Run. The example keeps the initialized runtime in the pyodide variable and enables the button only after setup succeeds. A larger application can represent the same lifecycle with explicit loading, ready, and failed states.
That distinction also improves error reporting. A CDN failure, a package-installation failure, and an error in submitted Luau source are different problems. Show initialization failures before enabling the editor action; handle Luau compile or runtime failures around the individual execution. The Pyodide package-loading guide documents the separate loadPackage() and micropip.install() paths and explains which package formats the browser runtime can load.
For repeated production use, decide deliberately whether one page should share a single initialized runtime or give each editor its own worker. A shared runtime avoids repeated setup but creates a shared Python host boundary. Separate workers provide stronger operational isolation at the cost of additional startup and memory. Those are application-design tradeoffs, not capabilities promised by luau-wasm, so measure them in the actual product environment.
Step 4: Run Typed Luau Source
The embedded VM can compile Luau syntax, including type annotations. For example:
local function greeting(name: string): string
return `Hello, {name}`
end
print(greeting("browser"))
Put that source in the text area and select Run. The documented execute() behavior returns captured printed output. Because a new sandboxed state is created for each call, variables from one execution should not be treated as durable application state.
If persistence is required, keep the state in JavaScript or Python and explicitly pass the next Luau source or data into a later call. Do not assume that a global created by one execution will exist in another.
Step 5: Handle Compile and Runtime Errors
The package exposes LuauError for both compile and runtime failures. In Python, handle it explicitly:
import luau_wasm
try:
result = luau_wasm.execute('error("boom")')
except luau_wasm.LuauError as error:
result = f"Luau failed: {error}"
In the browser example, a Python exception crosses the Pyodide boundary and is handled by JavaScript’s catch block. Displaying the exception is useful in a private playground. In a public application, avoid exposing sensitive host details and apply normal output encoding.
Untrusted code deserves additional care even when the Luau state is described as sandboxed. Review the wrapper’s exposed libraries and limits, keep the page isolated from privileged application state, and consider running the Pyodide workload in a Web Worker so long-running source does not freeze the main interface.
The word “sandboxed” should not be read as a complete security assessment. The repository describes a fresh Luau state with selected libraries, but a host application still controls the surrounding Python and browser environment. Do not expose secrets through Pyodide globals, do not place privileged tokens in the page, and do not rely on client-side execution as an authorization boundary. If user-supplied programs can consume material CPU or memory, add application-level limits and a recovery path such as terminating and replacing the worker.
What Not to Claim
The current public API documentation does not advertise a typecheck() function, direct JavaScript calls from Luau, Roblox service APIs, a stable persistence layer, or published cross-browser performance guarantees. A tutorial should not invent those capabilities.
It should also avoid universal browser-version tables unless those versions come from reproducible compatibility data. Pyodide’s own support policy and the package’s wheel target are the relevant constraints. Measure startup time, memory, and execution behavior in the deployment environment if those properties matter to a product decision.
For a broader view of the architecture, see zbrandco’s report on the luau-wasm alpha release and our analysis of browser-local AI and WebAssembly workloads. Both provide context, while this page stays focused on the documented integration path.
Deployment Checklist
- Pin the Pyodide CDN version and retest before changing it.
- Keep
micropip.install("luau-wasm")inside an awaited initialization path. - Disable execution controls until the runtime and package are ready.
- Pass editor source through a Pyodide global rather than string interpolation.
- Catch initialization, compile, and runtime failures separately where practical.
- Treat every
execute()call as a fresh Luau state. - Review the alpha dependency, CDN, PyPI, CSP, and worker-isolation implications.
- Measure behavior in the browsers and devices your application actually supports.
Bottom Line
luau-wasm demonstrates a real and unusually direct packaging path: a compiled Luau VM distributed as a Pyodide-compatible wheel on PyPI and invoked through a minimal Python API. The maintainer’s repository and playground establish the installation and execution flow. They do not establish all of the extra benchmarks, compatibility guarantees, or Roblox integrations that an earlier version of this tutorial claimed.
Use it for learning and controlled prototypes, keep the version pinned, and base production decisions on your own documented requirements and measurements.
Sources
- luau-wasm on PyPI
- simonw/luau-wasm repository
- Pyodide package-loading documentation
- Official Luau introduction
