# Extensions: adding a language without touching core stupidtools is deliberately not a one-trick C compiler. The core binary knows only about a generic extension ABI (`src/ext/abi.h`); the C/C++ languages are themselves just the first modules over it (`src/ext/lang_c.c`, `src/ext/lang_cpp.c`). Any other language — Fortran, Rust, D, a toy DSL — is added by dropping a Lua module into an extension directory. No core changes, no recompiling. The worked example this document walks through is [`examples/ext_fortran.lua`](../examples/ext_fortran.lua), a complete third-party Fortran stub. Read that file first; it is heavily commented. --- ## 1. Where extensions live (discovery) Extension discovery (`src/ext/discovery.h`) looks in this order: 1. **`--ext-dir DIR`** — repeatable command-line flag (`stupidtools --ext-dir DIR [--ext-dir DIR2 ...] `). 2. **`STUPIDTOOLS_EXT`** — colon-separated directory list (`STUPIDTOOLS_EXT=/a:/b stupidtools `); empty entries are skipped. 3. **User directory** — `$XDG_DATA_HOME/stupidtools/ext`, falling back to `$HOME/.local/share/stupidtools/ext` when `XDG_DATA_HOME` is unset or empty. Skipped entirely when neither variable yields a path. 4. **Builtin directory** — `builtin-ext/` relative to the working directory (compile-time configurable via `-DSTUPIDTOOLS_BUILTIN_EXT_DIR` for installed layouts). Scanning rules: only **regular files ending in `.lua`** are loaded (dotfiles excluded); load order within a directory is bytewise- lexicographic; a missing or empty directory is a silent no-op; an empty `.lua` file is a clean no-op. Discovery **fail-fasts**: the first module that fails to load (syntax error, runtime error, sandbox violation, unreadable file) aborts with `extension load failed: : `, and later modules are not attempted. Each loaded module logs one line built from its **actual** registrations: ``` loaded extension: /tmp/extdemo/ext_fortran.lua — checks: 1 (fortran_compiler_flag) languages: 1 (fortran) ``` ## 2. The sandbox contract Extension code runs in a stripped Lua 5.4 runtime (`src/ext/lua.h`). The module is **declarative** — it registers capabilities; it cannot execute or touch the outside world: | Capability | Status | | --- | --- | | `st.register_language`, `st.register_check` | the **only** interface | | base / table / string / curated `math` / hand-built `os` | available | | `os.execute`, `os.exit`, `os.remove`, `os.rename` | **present but blocked** — calling them raises `sandbox blocked: ...` and fails discovery | | `dofile`, `loadfile`, `require`, `package`, `io`, `debug`, `coroutine` | **nil** — their C libraries are not even linked | A module that violates the sandbox is rejected with a clear load error; the binary never runs untrusted program execution. ## 3. The registration API Two functions on the `st` table. Call shapes (exact — see `src/ext/api.h` and `src/ext/lua.c`): ```lua st.register_language(name, [spec_table]) st.register_check(name, { kind = "compile"|"link"|"run", source = "", link = { "-lfoo", ... } }) ``` - **`register_language(name [, table])`** — registers a language *name*. The optional second argument, if present, must be a table; v1 records the name only (the table's contents are reserved for future language detection). - **`register_check(name, spec)`** — registers a custom check. `kind` is the probe **mode** (compile / link / run), `source` is the C snippet the probe compiles, and `link` is an optional list of extra argv elements for the link step. The check's *name* becomes a new check kind a build file can reference, exactly like the builtin `header` kind. - Names must be non-empty strings with no NUL bytes and must not collide with an already-registered name (builtin or earlier module). The Lua tables are materialized into a `struct st_lua_probe_spec` (`src/ext/api.h`): all fields strdup'd, owned by the runtime's registry, freed with the runtime. ## 4. v1 limitations (documented, deliberate) - **Custom checks compile C snippets against the C toolchain.** The `source` in a probe spec is a *C* snippet driven by the detected C compiler (`st_lua_probe_run` in `src/ext/api.h`). There is no generic per-language probe runner yet; the Fortran example's check is a C probe whose name says "fortran". (This is why the example keeps its source libm-free and trivially true.) - **Lua language detection is a stub.** `register_language` records the name only; nothing yet detects a `fortran` compiler on the host, and the DSL has no language-scoped feature resolution wired to it. The table argument exists so a later release can add `register_language("fortran", { detect = ... })` without breaking existing modules. - **The bridge maps every Lua check to the `"c"` language** internally (`ST_LUA_CHECK_LANGUAGE` in `src/ext/api.c`) — again, v1 custom checks are C snippets, by design. ## 5. Step-by-step: adding a new language Copy this recipe for any language, not just Fortran. 1. **Write a `.lua` module.** Model it on `examples/ext_fortran.lua`: ```lua -- mylang.lua st.register_language("mylang", { kind = "compile" }) st.register_check("mylang_compiler_flag", { kind = "compile", source = "int main(void) { return 0; }", link = { "-lmylang" }, }) ``` Only `st.*` calls are allowed; no `os.execute`, no `io`, no `require` — the sandbox enforces it. 2. **Drop it in a discovery directory.** Either: ```sh mkdir -p /tmp/extdemo cp mylang.lua /tmp/extdemo/ ./src/stupidtools --ext-dir /tmp/extdemo ``` or: ```sh STUPIDTOOLS_EXT=/tmp/extdemo ./src/stupidtools ``` or install it to `$XDG_DATA_HOME/stupidtools/ext/` (default user dir). 3. **Check the load line.** You should see your module named with its actual registrations: ``` loaded extension: /tmp/extdemo/mylang.lua — checks: 1 (mylang_compiler_flag) languages: 1 (mylang) ``` A malformed module aborts discovery with `extension load failed: : :: ` — fix and rerun. 4. **Reference the new check kind from a build file.** Once a module has registered `mylang_compiler_flag`, a `feature` block in the KDL build file can name that kind like any builtin check kind. (Full fixture- driven resolution lands with the buildfile pipeline; discovery + registration is what this version proves.) That is the whole contract: **discover → sandbox → register → reference.** Languages are data to core, and the builtin C/C++ modules are just the first extensions.