# IR Serialization Format

## Why This Exists

The logical IR model (`{role, payload}` trees) needs a serialization format that is compact, unambiguous, trivially parseable, and native to the tree's own structure. JSON adds significant ceremony (braces, quotes, repeated key names) that doesn't serve the IR's consumers. This format encodes the same logical tree as a flat line-per-node text file where depth encodes parent-child relationships.

## Line Grammar

Every IR node serializes to exactly one line. There are two line forms:

```
<id> <depth>.<role>
<id> <depth>.<role>:<type>."<value>"
```

**Composite node** (no colon): the node's payload is the contiguous run of nodes at `depth+1` that follow it, up to the next node at `depth` or lower.

**Leaf node** (with colon): the node's payload is the typed scalar value after the colon. Leaf nodes have no children.

### Components

| Component | Format | Description |
| --- | --- | --- |
| `id` | `[a-zA-Z0-9_-]{5}` | Unique node identifier. 5 characters from a 64-character alphabet. |
| ` ` | Space | Separates ID from depth. |
| `depth` | Non-negative integer | Tree depth. `0` is the root. |
| `.` | Literal dot | Separates depth from role. |
| `role` | `[a-z][a-z0-9-]*` | Node role. Lowercase alphanumeric with hyphens. |
| `:` | Literal colon | Separates node identity from typed value. Present only on leaf nodes. |
| `type` | `[a-z][a-z0-9-]*` | Scalar type name (e.g., `string`, `int`, `bool`). |
| `.` | Literal dot | Separates type from value. |
| `"<value>"` | Double-quoted string | The scalar value. |

### Node IDs

5 characters from a 64-character alphabet: `a-z` (26) + `A-Z` (26) + `0-9` (10) + `_` (1) + `-` (1).

64^5 = 1,073,741,824 (~1 billion) unique identifiers per IR unit. IDs serve two purposes:

1. **Sibling disambiguation**: when multiple children share the same depth and role, the ID distinguishes them.
2. **Global addressability**: the ID space is large enough that IDs are unique across the entire IR unit, enabling flat references without path construction.

IDs lead the line so they're visually stable — the structural content (depth, role, value) can change without shifting ID column alignment.

IDs are generated during lowering and are unique within a single IR unit. Cross-unit references scope by unit ID (`file:ci.ops#a1b2`).

### Escaping

Values are double-quoted. The following escape sequences apply within quoted values:

| Sequence | Meaning |
| --- | --- |
| `\"` | Literal double quote |
| `\\` | Literal backslash |
| `\n` | Newline |
| `\t` | Tab |
| `\r` | Carriage return |

No other escaping is needed. Role names, type names, and IDs use restricted character sets that cannot conflict with delimiters.

### Scalar Arrays

Scalar arrays are composites. Each element is a child `item` node with a typed leaf payload:

```
a1b2_ 2.default
a1b3_ 3.item:string."linux"
a1b4_ 3.item:string."darwin"
```

This preserves the two-case invariant: every line is either composite (no colon) or leaf (with colon). No special array syntax.

## Tree Reconstruction

The depth integer encodes the tree structure. To reconstruct:

1. Maintain a stack of open nodes indexed by depth.
2. For each line, parse ID, depth, role, and optional type/value.
3. The node's parent is the most recent node at `depth - 1`.
4. The node's payload (if composite) is populated as subsequent lines at `depth + 1` are encountered.
5. When a line at depth `<= current` appears, all open nodes deeper than that depth are closed.

This is a single-pass, streaming parse with O(max_depth) stack space.

### Example

```
R00t_ 0.file
Un1t_ 1.unit
xK_mQ 2.id:string."file:ci.ops"
Lp3qR 2.path:string."ci.ops"
Sr01_ 1.source
nS01_ 2.name:string."file:ci.ops"
oS01_ 2.origin:string."user"
pS01_ 2.path:string."ci.ops"
cS01_ 2.content:string."main @release {\n  test()\n}\n"
Sr02_ 1.source
nS02_ 2.name:string."pkg:shellops-std@1.0.0/log.ops"
oS02_ 2.origin:string."package"
pkS2_ 2.package:string."shellops-std"
vrS2_ 2.version:string."1.0.0"
ptS2_ 2.path:string."log.ops"
dgS2_ 2.digest:string."sha256:..."
Im01_ 1.import
nI01_ 2.name:string."log"
vI01_ 2.value:string."pkg:shellops-std@1.0.0/log.ops"
En01_ 1.entrypoint:string."release"
Ev01_ 1.env
eE01_ 2.entry
nE01_ 3.name:string."DEPLOY_TARGET"
tE01_ 3.type:string."string"
rE01_ 3.required:bool.true
sE01_ 3.source:string."#/sourcemap/env.DEPLOY_TARGET"
eE02_ 2.entry
nE02_ 3.name:string."TARGETS"
tE02_ 3.type
nT02_ 4.name:string."string"
lT02_ 4.list:bool.true
sT02_ 4.separator:string."space"
dE02_ 3.default
d1E02 4.item:string."linux"
d2E02 4.item:string."darwin"
eE03_ 2.entry
nE03_ 3.name:string."DEPLOY_TOKEN"
tE03_ 3.type:string."secret"
rE03_ 3.required:bool.true
sE03_ 3.source:string."#/sourcemap/env.DEPLOY_TOKEN"
Sc01_ 1.script
nSc1_ 2.name:string."bin/deploy"
vSc1_ 2.value:string."release"
Dc01_ 1.declaration
nDc1_ 2.name:string."notice"
tDc1_ 2.type:string."string"
vDc1_ 2.value:string."release started"
Pl01_ 1.pipeline
nPl1_ 2.name:string."release"
ePl1_ 2.entrypoint
sPl1_ 2.safety:string."remote"
cP01_ 2.call
tC01_ 3.target:string."test"
sC01_ 3.source:string."#/sourcemap/pipelines.release.0"
cP02_ 2.call
tC02_ 3.target:string."deploy"
aC02_ 3.args
nA01_ 4.named
nnA1_ 5.name:string."target"
vnA1_ 5.var:string."DEPLOY_TARGET"
dC02_ 3.need
vD01_ 4.var:string."DEPLOY_TOKEN"
xC02_ 3.export:string."VERSION"
sC02_ 3.source:string."#/sourcemap/pipelines.release.1"
Sm01_ 1.sourcemap
nSm1_ 2.node
nnS1_ 3.name:string."pipelines.release"
snS1_ 3.source:string."file:ci.ops"
spS1_ 3.span
stS1_ 4.start
s1S1_ 5.item:int.1
s2S1_ 5.item:int.1
s3S1_ 5.item:int.0
enS1_ 4.end
e1S1_ 5.item:int.4
e2S1_ 5.item:int.2
e3S1_ 5.item:int.44
```

This is the same IR as the target-shape example in [ir_schema.md](../ir_schema.md).

## Relationship to the Logical Model

The serialization format is a direct encoding of the `{role, payload}` tree described in [ir_schema.md](../ir_schema.md):

- Every node in the logical model maps to exactly one line.
- `role` maps to the role field.
- `id` provides stable node identity for addressability and sibling disambiguation.
- `name` in the logical model becomes a `name` child node in the serialization.
- Composite `payload` arrays map to children at `depth+1`.
- Leaf `payload` scalars map to the `:type."value"` suffix.
- The two-case dispatch (`scalar` vs `array of {role, payload}`) maps directly to "has colon" vs "no colon."

The generation contributor dispatch pattern works identically: `generate(node) → lookup(node.role)(children)` for composites, `emit(node.value)` for leaves.

## Tooling

The format is designed for machine consumption. Human inspection should use tooling built on top:

- **Pretty-printer**: loads IR, renders as an indented tree with names inlined.
- **Query CLI**: `ir find <role> [name]` — locates nodes by role and optional name.
- **Diff**: standard line-based diff shows structural IR changes.
- **Grep**: `grep '\.pipeline'` finds all pipeline nodes; depth prefix narrows scope.
- **Subtree extraction**: given a node's line, collect all subsequent lines with depth > node's depth.
- **ID lookup**: `grep '^cP02'` jumps directly to a node by ID.

## Types

Scalar types used in the `:type."value"` suffix:

| Type | Value format | Example |
| --- | --- | --- |
| `string` | Quoted text | `string."hello"` |
| `int` | Unquoted integer | `int.42` |
| `number` | Unquoted decimal | `number.3.14` |
| `bool` | `true` or `false` | `bool.true` |

Scalar arrays use composite nodes with `item` children — no special type syntax.

## Invariants

- Every line is exactly one node. No multi-line nodes.
- Depth `0` appears exactly once (the root `file` node).
- IDs are 5 characters from `[a-zA-Z0-9_-]`, unique within an IR unit.
- A node's children are the contiguous run of lines at `depth+1` following it, before the next line at `depth` or lower.
- Leaf nodes (with `:`) never have children.
- Composite nodes (without `:`) may have zero or more children.
- The format is a lossless encoding of the logical `{role, payload}` tree.

## Design References

- [ir_schema](../ir_schema.md) defines the logical IR model and payload-shape rule.
- [posix-emission](./posix-emission.md) defines the generation contributor dispatch pattern.
- [versioning-and-compatibility](./versioning-and-compatibility.md) defines schema versioning.
