# 0002: One Luau VM per addon, with a time limit instead of an instruction count

- **Date:** 2026-10-02
- **Status:** accepted (step 0.11)

## Context

BUILD.md §3.9 asks for a Luau sandbox with a **per-script memory limit** and an
**interrupt callback** that stops runaway loops, with addons loaded from a folder and
hot-reloaded.

What we found in mlua 0.12.1 (feature `luau`):
- `Lua::set_memory_limit` limits a **whole VM**, not one script inside it.
- Luau has no instruction-count hook (no `debug.sethook`). It has an **interrupt
  callback**, which Luau calls at loop back-edges and function calls. Returning an error
  from it raises a Lua error at that point.
- A Lua error can be caught by `pcall`, so a script could catch "out of time" and keep
  looping.
- mlua gives Luau a default `require` that **reads files from disk**.
- `Lua::sandbox(true)` makes the libraries and globals read-only; the script's own
  globals go into a private table on top.

## Decision

- **Each addon (a folder with `init.lua`) runs in its own Luau VM**, with its own memory
  limit (64 MB) and its own guard.
- **Time limit, not instruction count:** before every call into an addon, Fusion sets a
  deadline (250 ms for loading `init.lua`, 50 ms for one event across all of that
  addon's hooks). The interrupt callback reads the clock every 64th call. Once the
  deadline passes, the guard is **tripped**: every later interrupt and every API call in
  that VM fails, so `pcall` can't rescue the loop.
- An addon that runs out of time or memory is **stopped**: its VM is dropped (hooks and
  memory go with it), the player sees a plain message, and the other addons keep
  running. Saving the addon's files loads it again.
- `require`, `loadstring`, `getfenv` and `setfenv` are removed before sandboxing.
  Multi-file addons will get a `require` that only sees the addon's own folder.
- Lua never touches Godot directly: API calls queue **commands** (e.g. spawn a prop),
  which the `FusionLua` node turns into signals each frame. `fusion-lua` has no Godot
  types and is tested on its own.

## Consequences

- Fault isolation like a process boundary, inside one process: one broken addon can't
  freeze Fusion or break other addons. A test shows an endless loop wrapped in `pcall`
  is stopped within the 50 ms budget. In the playground the hitch is one frame of about
  90 ms, then everything carries on.
- Hot reload is simply dropping the VM and making a new one; nothing leaks between
  versions.
- Addons don't share globals (unlike Garry's Mod, where all addons share one state).
  Addons that need to talk to each other will do it through Fusion: events, and later
  shared values. That's in line with "Lua talks to blocks only through ports, actions and
  events".
- Each VM costs a little memory (a fresh Luau state is small, tens of KB). If Devices
  (step 4.4) create many small scripts, they can share one VM per scenario.
- Long-running C functions (e.g. a huge `string.rep`) aren't interrupted mid-call; the
  memory limit catches the big ones. Good enough for v0.
