# Writing a generator
This page walks you through building a plugin generator in Python that
emits Lua. The same pattern works in any language that can read JSON
on stdin and write JSON on stdout.
The full source of this example is short — about 60 lines.
## Goal
Given:
```primate
// constants/limits.prim
duration TIMEOUT = 30s
u32 MAX_RETRIES = 5
```
Emit:
```lua
-- generated by primate-lua
local M = {}
M.TIMEOUT = 30 -- seconds
M.MAX_RETRIES = 5
return M
```
## Step 1: read stdin, write stdout
```python
#!/usr/bin/env python3
# scripts/primate_lua.py
import json
import sys
req = json.load(sys.stdin)
out_lines = ["-- generated by primate-lua", "local M = {}"]
for module in req["modules"]:
for c in module["constants"]:
name = c["name"]
v = c["value"]
if "nanoseconds" in v:
secs = v["nanoseconds"] / 1_000_000_000
out_lines.append(f"M.{name} = {secs} -- seconds")
else:
out_lines.append(f"M.{name} = {json.dumps(v)}")
out_lines.append("return M")
resp = {
"files": [{
"path": req["outputPath"],
"content": "\n".join(out_lines) + "\n",
"mappings": [],
}],
"errors": [],
}
json.dump(resp, sys.stdout)
```
## Step 2: wire it into `primate.toml`
```toml
[generators.lua]
output = "scripts/generated/constants.lua"
command = ["python3", "scripts/primate_lua.py"]
```
## Step 3: run it
```bash
primate build
# generated scripts/generated/constants.lua
```
## What just happened
primate parsed `.prim` files, built the IR, then for the `[generators.lua]`
target it spawned `python3 scripts/primate_lua.py`. It piped a JSON
request to the plugin's stdin (with all modules, enums, and aliases),
read the JSON response from stdout, and wrote each file in `files` to
disk.
## Reading types
The example only handles primitive values for brevity. A real generator
needs to recognize the type tags:
```python
def render_type(t):
kind = t["kind"]
if kind in ("i32", "i64", "u32", "u64", "f32", "f64"):
return "number"
if kind == "string":
return "string"
if kind == "duration":
return "number" # seconds
if kind == "bool":
return "boolean"
if kind == "array":
return f"{render_type(t['element'])}[]"
if kind == "fixed_array":
return f"{render_type(t['element'])}[{t['length']}]"
if kind == "map":
return f"map<{render_type(t['key'])}, {render_type(t['value'])}>"
if kind == "enum":
return t["name"]
if kind == "alias":
return t["name"]
raise ValueError(f"unknown type kind: {kind}")
```
## Generating sourcemaps
If you populate `mappings` in your response, the LSP can jump between
`.prim` source lines and your generated lines:
```python
mappings.append({
"symbol": f"{module['namespace']}::{c['name']}",
"line": current_line_number,
"column": 1,
})
```
`symbol` is the fully-qualified constant name; `line` is 1-based, `column`
is 1-based. Keeping these accurate makes go-to-definition from generated
code into `.prim` work.
## Returning errors
```python
resp = {
"files": [],
"errors": [{
"message": "could not represent f128 in Lua",
"source": c["source"], # passes through file/line/column
}],
}
```
primate surfaces these alongside its own diagnostics; a non-empty
`errors` array fails the build.
## Built-in generators are plugins
The Rust, TypeScript, and Python generators are linked into the
primate binary, but they implement exactly this protocol — the only
difference is that they don't fork a subprocess. Reading their
implementations (`src/generators/`) is the easiest reference for what
"correct" output looks like for each shape.
## See also
- [Protocol](./protocol.md) — complete JSON schema reference.