---
title: "15. Code generation"
description: "The model as the source of the code: Go, TypeScript and SQL from one catalogue, each over a slice"
source: "https://aontu.dev/use-cases/15-code-generation/"
---

# 15\. Code generation

The model as the source of the code: Go, TypeScript and SQL from one catalogue, each over a slice

Exercises list-spread + `pick` line building, `join` file assembly, backtick target text, `match` type mapping, both-ports byte parity

Rendered from [`use-cases/15-code-generation/README.md`](https://github.com/aontu-lang/aontu/blob/main/use-cases/15-code-generation/README.md) in the engine repository. The models, the expected output and the `check.sh` that drives the CLI over all of it are in [`use-cases/15-code-generation/`](https://github.com/aontu-lang/aontu/tree/main/use-cases/15-code-generation/).

**One model, three generated files, each over a different slice of it, one document.** Go structs, TypeScript interfaces and SQL DDL: every file is computed by the unifier from [`use-cases/15-code-generation/model.aon`](https://github.com/aontu-lang/aontu/blob/main/use-cases/15-code-generation/model.aon) and nothing else. Each generator is a rule set (`emit`) whose children fill one `file(...)` of a component tree; [`use-cases/15-code-generation/all.aon`](https://github.com/aontu-lang/aontu/blob/main/use-cases/15-code-generation/all.aon) holds the three files, and `aontu render` writes the bytes through jostraca. The goldens under `expected/` are held by `aontu render --check`. Nothing outside the model decides layout, ordering, or separators, and nothing in the engine writes a file.

The model tree: the record definitions every generated target is written from

## What is generated

| generator | reads | file | golden |
| --- | --- | --- | --- |
| [`use-cases/15-code-generation/gen-go.aon`](https://github.com/aontu-lang/aontu/blob/main/use-cases/15-code-generation/gen-go.aon) | `name`, `fields.n`, `fields.t`, `fields.go` | `types.go`, Go structs with JSON tags | [`use-cases/15-code-generation/expected/types.go`](https://github.com/aontu-lang/aontu/blob/main/use-cases/15-code-generation/expected/types.go) |
| [`use-cases/15-code-generation/gen-ts.aon`](https://github.com/aontu-lang/aontu/blob/main/use-cases/15-code-generation/gen-ts.aon) | `name`, `fields.n`, `fields.t`, `fields.req` | `types.ts`, TypeScript interfaces | [`use-cases/15-code-generation/expected/types.ts`](https://github.com/aontu-lang/aontu/blob/main/use-cases/15-code-generation/expected/types.ts) |
| [`use-cases/15-code-generation/gen-sql.aon`](https://github.com/aontu-lang/aontu/blob/main/use-cases/15-code-generation/gen-sql.aon) | `sql`, `fields.sql`, `fields.t`, `fields.req` | `schema.sql`, `CREATE TABLE` statements | [`use-cases/15-code-generation/expected/schema.sql`](https://github.com/aontu-lang/aontu/blob/main/use-cases/15-code-generation/expected/schema.sql) |

A component tree is data: `aontu model get out all.aon` prints it, and `aontu render` hands it to jostraca, which writes the bytes. `check.sh` holds the goldens to what `render --check` answers, and holds both ports to the same tree.

The Go generator never reads `sql`; the SQL generator never reads `go`; the TypeScript generator reads neither and keeps the wire name in `n`. The SQL generator quotes every identifier, because `order` is a reserved word and a generator that emits it bare produces a file that does not parse: always quoting is simpler than carrying a word list.

## The model tree

`model.aon` is the whole input to the generators: a list of records, each with a name, a SQL table name, and its fields. Everything the Go, TypeScript and SQL targets emit is computed from these three keys, so the model tree is also the list of things a target must handle.

```plaintext
$
└── records
    ├── 0
    │   ├── fields (3)
    │   ├── name "Customer"
    │   └── sql "customer"
    └── 1
        ├── fields (3)
        ├── name "Order"
        └── sql "order_line"
```

`aontu view doc --depth 3 model.aon` draws it, and `check.sh` pins it with `--out --check`. A key with `(n)` after it is a container the depth bound stopped at, and `n` is how many keys are not drawn; a leaf carries its canon, which is the kind of thing it is rather than its value.

## The shape of a generator

```aon
%go_field = emit(_, {
  match: n: string
  body: [
    line(
      "\t" + .go + " " + match(.t, "string", "string", "integer", "int64")
      + ` \`json:"` + .n + `"\``
    )
  ]
})

%go_record = emit(_, {
  match: name: string
  body: [
    line("")
    line("type " + .name + " struct {")
    emit(.fields, %go_field)
    line("}")
  ]
})

go: file("types.go", [
  line("// Code generated by aontu from model.aon. DO NOT EDIT.")
  line("")
  line("package acme")
  emit($.records, %go_record)
])
```

Each line of that shape is a decision:

-   **A rule set walks the records in source order.** `emit` visits the selection in the order the model wrote it, which is what a file needs; `pack` would key by data and emit the records alphabetically, silently wrong output for a file.
-   **Lines, not text.** A record contributes a blank line, a head, its fields and a tail; a field contributes one line. A `Line` writes its own terminator, so no generator spells a newline; the indent is the target’s own, `"\t"` in the Go generator’s strings and two spaces in the TypeScript one, because aontu knows no languages and has no view about how either indents.
-   **The children are flat.** The nested dispatch `emit(.fields, %go_field)` splices its lines into the record’s, so one flat list reaches the file.
-   **The file is one value.** The banner, the package clause and the records are children of the same `file(...)`, in order, so the blank line each record opens with is what separates a struct from what came before.
-   **Names come from the model, not the generator.** `Email` and `credit_cents` are written in `model.aon` rather than computed: what a type is _called_ in a target is a fact about the model, not a rule in a template: the “symbol provider” split that code generators arrive at. Writing the name down also makes a collision a unification conflict rather than a broken identifier at emit time.
-   **The aliases carry the target’s name** (`%go_record`, `%ts_record`), because an alias declaration is a key of the document that includes the file, and `all.aon` includes all three. Distinct names prevent the declarations from conflicting.

`gen-sql.aon` folds its column lines with `join` and `,\n`, so the separator falls between them and the last column carries no trailing comma, and carries the block as ONE line whose text holds its own newlines: a line per column would need to know which one is last.

```sql
CREATE TABLE "customer" (
  "id" TEXT NOT NULL,
  "email" TEXT,
  "credit_cents" BIGINT
);
```

The column table is written inline rather than named, because a named table does not resolve as the argument of a call inside a body.

## What check.sh proves

A generator whose output merely looks right is a generator nobody trusts, so the checks compile the Go and parse the SQL rather than inspecting them.

1.  One run, three files: the document names `types.go`, `types.ts` and `schema.sql`, in that order.
2.  The three generated files match their goldens byte for byte, each behind its `Code generated by aontu from model.aon. DO NOT EDIT.` banner: a line of the file, not something a script puts in front of it.
3.  The generated Go compiles: `go build` in a scratch module, over the bytes jostraca wrote. `gofmt` reformats the output, aligning the struct tags: the generator’s job is correct code, and layout is the formatter’s.
4.  The generated SQL parses: SQLite executes it, the tables `customer` and `order_line` exist, `order_line` has exactly the columns `id`, `customer_id` and `total_cents`, and no column list ends in a comma.
5.  The slices are real: renaming a `go` field (`Email` to `EmailAddr`) in a copy of the model moves the Go file and leaves the TypeScript file byte-identical.
6.  Generation is deterministic: two runs of the same document are byte-identical.
7.  A file path that climbs out of the output directory is refused.
8.  The check is red when a golden is edited, and names the file.
9.  The Go port builds the same trees as the TypeScript engine for all three files: one model never becomes two different files depending on which engine ran.
10.  The model tree draws and is pinned, text and SVG.

Check 9 needs a Go toolchain, and skips with a note without one.

## Run

```sh
./check.sh          # ten checks, including both ports byte for byte
```
