primate 0.1.1

A small DSL for cross-language constants. Write once, generate typed Rust, TypeScript, and Python.
Documentation
# 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.