Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Plugin protocol

primate’s built-in generators (Rust, TypeScript, Python) are part of the binary, but they implement the same protocol every plugin does: JSON request on stdin, JSON response on stdout, errors on stderr.

You can plug in any executable on PATH (or a script path) as a generator by referencing it from primate.toml.

Wire format

primate spawns the plugin and writes a single JSON request to its stdin, then closes stdin. The plugin reads stdin, generates whatever it generates, and writes a single JSON response to stdout.

Request

{
  "version": 1,
  "outputPath": "scripts/generated/constants.lua",
  "options": { "naming": "camelCase" },
  "modules": [
    {
      "namespace": "limits",
      "sourceFile": "constants/limits.prim",
      "doc": null,
      "constants": [
        {
          "name": "TIMEOUT",
          "doc": "How long the gateway waits before bailing.",
          "type": { "kind": "duration" },
          "value": { "duration": { "nanoseconds": 30000000000 } },
          "source": { "file": "constants/limits.prim", "line": 4, "column": 10 }
        }
      ]
    }
  ],
  "enums": [...],
  "aliases": [...]
}

Top-level fields:

  • version — protocol version. Currently 1.
  • outputPath — the output value from the relevant [generators.*] section of primate.toml, relative to project root.
  • options — every other key from that [generators.*] section, passed through verbatim as a JSON object.
  • modules — one per namespace; carries that namespace’s constants.
  • enums, aliases — top-level enum and type-alias definitions.

Response

{
  "files": [
    {
      "path": "scripts/generated/constants.lua",
      "content": "-- generated by primate\n...",
      "mappings": [
        { "symbol": "limits::TIMEOUT", "line": 7, "column": 1 }
      ]
    }
  ],
  "errors": []
}
  • files — one or more output files. The plugin can emit multiple files; primate writes each. (Most plugins emit just one.)
  • mappings — symbol → generated-file location, used to populate the sourcemap so the LSP can jump from .prim to generated code.
  • errors — non-empty array signals failure. Each entry has message and an optional source (file/line/column).

Type and value JSON shapes

Type expressions are tagged unions:

{ "kind": "u32" }
{ "kind": "string" }
{ "kind": "array", "element": { "kind": "u32" } }
{ "kind": "fixed_array", "element": { "kind": "u32" }, "length": 3 }
{ "kind": "map", "key": { "kind": "string" }, "value": { "kind": "u32" } }
{ "kind": "tuple", "elements": [ ... ] }
{ "kind": "optional", "inner": { "kind": "string" } }
{ "kind": "enum", "name": "LogLevel", "namespace": "logging" }
{ "kind": "alias", "name": "Port", "namespace": "network" }

Values are untagged (each shape has a uniquely-typed payload):

8                                                      // integer
3.14                                                   // float
true                                                   // bool
"hello"                                                // string
{ "nanoseconds": 30000000000 }                         // duration
[1, 2, 3]                                              // array / fixed-array / tuple
{ "key": value, ... }                                  // map (untagged JSON object)
null                                                   // optional, none case
{ "variant": "Info", "value": 1 }                      // enum

Plugins typically deserialize into language-native types and re-serialize to the target syntax.

Wiring up a plugin

In primate.toml:

[[output]]
generator = "lua"
path      = "scripts/generated/constants.lua"
command   = "/usr/local/bin/primate-lua"        # absolute or on $PATH
options.naming = "camelCase"

Or, if your plugin is a script in the project:

[[output]]
generator = "lua"
path      = "scripts/generated/constants.lua"
command   = ["python", "scripts/primate_lua.py"]

path can be a single file or a directory — that’s up to your plugin. The plugin decides how many files to emit and what their relative paths are; it’s purely a convention between you and your generator.

primate runs the command with stdin/stdout/stderr piped, sends the request, waits for the response, and writes whatever the response says.

See also