# aontu > aontu is a language for the ontology of a software system: the entities it contains, the types they must have, and the relations that must hold between them. The definition is machine-checkable, so a change proposed by a coding agent is admitted or refused with the offending line named. Every documentation page, concatenated. Documented against `aontu` 0.72.0. Source: https://github.com/aontu-lang/aontu. Index: https://aontu.dev/llms.txt Each page below is rendered from the engine repository, where its examples are executed by the test suite on every build. The executed use cases are deliberately NOT here. They are long-form records (verbatim CLI transcripts and findings), and folding them in would half again the size of this corpus with material a reader wants one at a time. They are indexed in https://aontu.dev/llms.txt, and each has its own markdown twin. --- # aontu documentation _What aontu is, how its documentation is organised, and a 30-second taste of unification._ Source: https://github.com/aontu-lang/aontu/blob/main/docs/index.md · Page: https://aontu.dev/docs aontu is a JSON structure **unifier**: a small language (a purpose-specific dialect inspired by [CUE](https://cuelang.org/)) and an engine that merges partial structures into one consistent result, or reports exactly where they conflict. The same source can describe data, the schema that constrains it, and the defaults that fill it in: all in one notation, all combined by a single operation: *unification*. This repository ships **two implementations kept in parity**: - **TypeScript** in [`ts/`](https://github.com/aontu-lang/aontu/blob/main/ts/): the canonical implementation, published to npm as [`aontu`](https://npmjs.com/package/aontu). - **Go** in [`go/`](https://github.com/aontu-lang/aontu/blob/main/go/): a port (`github.com/aontu-lang/aontu/go`) that mirrors the core semantics. Both are checked against one language-agnostic test suite in [`test/spec/`](https://github.com/aontu-lang/aontu/blob/main/test/spec/), and every example in these pages is executed by that machinery too: the outputs come from the engine, not from the author's memory. ## How this documentation is organised The documentation is split by **what you are trying to do** when you open it. Reach for the part that matches your need: | If you want to… | Read | |---------------------------------------------------------------|------| | **Learn** aontu from zero by building something, step by step | [Tutorials](/docs/tutorial) | | **Accomplish a specific task** you already have in mind | [How-to guides](https://github.com/aontu-lang/aontu/blob/main/docs/how-to/) | | **Look up** exact syntax, semantics, options, or API surface | [Language reference](/docs/reference-language) · [API reference](/docs/reference-api), with three supplements below | | **Understand unification itself**: meet, top, bottom, the lattice | [Unification](/docs/unification) | | **Understand** how and why the engine works the way it does | [Explanation](/docs/explanation) | | **See whole systems defined**, each with its checks runnable | [Use cases](/docs/use-cases) | There are four tutorials, each building one thing and showing the output of every step: a config that is schema, defaults, and data at once; the graph layer of identity, relations, and reachability; sharing a model as a versioned package; and computing source files from a model. The [index](/docs/tutorial) says what each one assumes. The how-to guides are one page per task, grouped six ways: run, embed and integrate; templates, defaults and composition; schemas and constraints; query, explain and change; validate and evolve; modules and multi-file. Three capabilities have doorways of their own: - **Declare and check relations.** Entities carry identity, the edges between them are declared in the model, and the engine checks both. The recipe is [check relations](/how-to/check-relations); the live version is [`use-cases/12-relations`](/use-cases/12-relations); the normative rules are under [Declared relations](/docs/reference-language#declared-relations). - **Write a recursive schema.** A schema can name itself, so trees and nested structures validate to any depth. The recipe is [define a recursive schema](/how-to/define-a-recursive-schema); the live version is [`use-cases/13-recursive-schema`](/use-cases/13-recursive-schema); the semantics are under [Recursive references](/docs/reference-language#recursive-references-fixpoints). - **Generate code from a model.** The field names, types and optionality a Go struct or a TypeScript interface needs are already in the model, and the unifier computes the file: a rule set over the records, `match` for the type mapping, a backtick string to carry the target text, and a component tree of files and lines that a generator runtime writes to disk and holds against its golden. The recipe is [generate code from a model](/how-to/generate-code); the live version, with three targets in one document and a check that both ports build identical trees, is [`use-cases/15-code-generation`](/use-cases/15-code-generation). The rules for the tree itself are the [Generation reference](/docs/reference-generation). Six reference sections sit beside the two above. Each is a surface that cuts across the language reference rather than a part of it, which is what the language reference, organised by topic, cannot show at once: - [Generation reference](/docs/reference-generation). The component tree: every component node, the props it carries, the children it admits, and what `aontu render` and `aontu trace` do with a tree. - [Functions reference](/docs/reference-functions). The call surface of every built-in: arity, argument modes, accepted kinds and result words, as one table and as slices through it. - [Error reference](/docs/reference-errors). Every registered error code, by class, with what raises it and what a report carries. - [Packages reference](/docs/reference-packages). The files the package system keeps, every field `pkg.aon` declares, the name rules, the caps, what an archive may hold, and every refusal code. - [Grammar reference](/docs/reference-grammar). The published grammar rule by rule, the spellings the parser accepts beyond it, and what holds the four grammar files to the engine. - [Agent and editor reference](/docs/reference-agents). Every door a machine comes in by, the one answer shape they share, what none of them does, and the posture each takes towards includes. Tooling: - [The `aontu` command](/docs/reference-api#command-line-interface). One binary, nineteen verbs, both implementations. Each verb has its own reference section: - validate: [`vet`](/docs/reference-api#aontu-vet), wrapped for CI as a [GitHub Action](https://github.com/aontu-lang/aontu/blob/main/vet-action/README.md) - evolve a schema: [`subsume`](/docs/reference-api#aontu-subsume), [`breaking`](/docs/reference-api#aontu-breaking) - ask and change: [`get`](/docs/reference-api#aontu-model-get), [`why`](/docs/reference-api#aontu-model-why), [`set`](/docs/reference-api#aontu-model-set), [`trim`](/docs/reference-api#aontu-trim) - identity and relations: [`relations`](/docs/reference-api#aontu-relations), [`reaches`](/docs/reference-api#aontu-reaches), [`view`](/docs/reference-api#aontu-view) - export and pin: [`jsonschema`](/docs/reference-api#aontu-jsonschema), [`hash`](/docs/reference-api#aontu-hash) - generate: [`template`](/docs/reference-api#aontu-template), [`trace`](/docs/reference-api#aontu-trace) (the guide: [Generate code from a model](/how-to/generate-code)) - depend and publish: [`sync`](/docs/reference-api#aontu-sync), [`add`, `get`, `remove`](/docs/reference-api#aontu-add-aontu-get-aontu-remove), [`why`](/docs/reference-api#aontu-why), [`publish`](/docs/reference-api#aontu-publish), [`pkg`](/docs/reference-api#aontu-pkg) - hand over: [`agentsmd`](/docs/reference-api#aontu-agentsmd) - keep in the agreed form: [`fmt`](/docs/reference-api#aontu-fmt) (the form: [The formatted form](/docs/reference-language#the-formatted-form); the guide: [Format a document](/how-to/format-a-document)) With no file at all, `aontu` starts a REPL. - [Language Server (LSP)](/docs/lsp). The `aontu-lsp` diagnostics server (TypeScript and Go), how to wire it into an editor, and the reusable LSP library API. - [The MCP server](/docs/reference-api#the-mcp-server). `aontu mcp`, a Model Context Protocol server over stdio, answering with the identical reports the CLI prints. For agents: - [The aontu skill](https://github.com/aontu-lang/aontu/blob/main/docs/skill/SKILL.md). An agent-facing teaching pack: the [grammar card](https://github.com/aontu-lang/aontu/blob/main/docs/skill/grammar-card.md), a [worked example ladder](https://github.com/aontu-lang/aontu/blob/main/docs/skill/examples.md) whose documents the test suite executes, and the [error-code index](https://github.com/aontu-lang/aontu/blob/main/docs/skill/error-codes.md). - [The published grammar](/docs/reference-language#the-published-grammar): [`grammar/aontu.abnf`](https://github.com/aontu-lang/aontu/blob/main/grammar/aontu.abnf) to read, with railroad diagrams; [`aontu.gbnf`](https://github.com/aontu-lang/aontu/blob/main/grammar/aontu.gbnf) and [`aontu.lark`](https://github.com/aontu-lang/aontu/blob/main/grammar/aontu.lark) for constrained decoding. Contract: - [The trust contract](/docs/trust). Hermeticity, termination, determinism, and sandboxing: what a host may rely on when evaluating an aontu document, and where each guarantee is conditional. For contributors: - [The style guide](https://github.com/aontu-lang/aontu/blob/main/docs/STYLE-GUIDE.md). How these pages are written: Diátaxis placement, the voice, the banned-phrase list, and the snippet directives under which every example runs. ### Why the split? The four kinds of document answer four different questions and are kept separate on purpose. A tutorial holds your hand and is allowed to omit detail; a how-to assumes you know the basics and just need the recipe; a reference is exhaustive and dry so you can trust it as the source of truth; an explanation is discursive and is the only place that argues about trade-offs. Mixing them (a reference that teaches, a tutorial that digresses into design rationale) serves none of those needs well, so each lives in its own file. The rule applies to the toolkit as much as to the language, which is why a verb can appear in all four kinds without any of them repeating another: met once, in passing, while a tutorial builds something; given as a recipe for one goal in a how-to guide; specified exhaustively (every flag, every exit code) in the API reference; and argued for, never merely listed, in the explanation. The use cases stand alongside as whole worked systems, each holding a `check.sh` that CI runs. ## A 30-second taste ```aontu # A schema, a default, and data — unified into one result. port: *8080|integer host: string host: "localhost" ``` Unifying the three lines above yields: ```json { "host": "localhost", "port": 8080 } ``` The `port` is constrained to be an `integer`, defaults to `8080`, and, because nothing overrode the default, `8080` is what comes out. `host` is constrained to a `string` and pinned to `"localhost"`. Conflicting facts (a second `port: "high"`, say, or a `port: 1.5`) are refused with a precise error rather than silently resolved: the preferred branch keeps the kind it names, which is [argued in the explanation](/docs/explanation#a-preference-is-gated-by-kind-not-by-family). Try it without writing a file: both implementations ship an `aontu` command that evaluates a file, reads stdin, or starts a REPL: ```sh $ echo 'port: *8080 | integer' | aontu { "port": 8080 } ``` Start with [build a config that checks itself](/docs/tutorial-config), the first of four [tutorials](/docs/tutorial). --- # Tutorials _The four tutorials, what each one builds, and which to read first._ Source: https://github.com/aontu-lang/aontu/blob/main/docs/tutorial.md · Page: https://aontu.dev/docs/tutorial Four tutorials, each building one thing from an empty directory and showing the engine's output at every step. Read them in this order if you are new; take one on its own if its subject is what you need. - [Build a config that checks itself](/docs/tutorial-config). A service config that is schema, defaults, and data in one document, validated against a second file with `aontu vet` and explained line by line with `aontu model why`. Assumes no aontu at all. - [Model the system, not the tree](/docs/tutorial-graph). The same config as a graph: two views of one service brought into contact, declared relations that refuse a cycle, and a schema as deep as its data. Assumes the first tutorial. - [Share a model as a package](/docs/tutorial-package). A schema published as a signed package into a directory, acquired by a second project, and pinned so that a change to what it means is refused. Assumes the first tutorial. - [From a model to a file tree](/docs/tutorial-generate). A TypeScript client computed from a model of its routes, as a tree of folders, files, and lines, written to disk with `aontu render` and held against the model by its check. Assumes the first tutorial. Every transcript on these pages is run by `ts/test/docs.test.ts`, so the output beside a command is what the engine printed. A tutorial is the wrong shape once you can name what you want. Go to the [how-to guides](https://github.com/aontu-lang/aontu/blob/main/docs/how-to/) for a task, the [language reference](/docs/reference-language) or the [API reference](/docs/reference-api) for a fact, [unification](/docs/unification) for the operation itself, and the [explanation](/docs/explanation) for why the engine works as it does. --- # Tutorial: build a config that checks itself _Learn aontu from zero by building a config that checks itself, step by step._ Source: https://github.com/aontu-lang/aontu/blob/main/docs/tutorial-config.md · Page: https://aontu.dev/docs/tutorial-config A service config needs values, constraints, and defaults. In aontu, you can write all three in one document and combine them through [unification](/docs/unification). Build a config, validate data against it, and find which statement set a value. Each step shows source and output checked by the test suite. For the reasoning behind the operation, read the [explanation](/docs/explanation). ## 1. Set up If aontu is already installed, skip to [§2](#2-objects-are-just-keys-and-values). ### TypeScript Install and build inside the repo's `ts/` directory: ```sh cd ts npm install npm run build # compiles src + test into dist/ and dist-test/ ``` A scratch `play.js` next to `ts/` is enough to call the library: ```js const { Aontu } = require('aontu') // when installed from npm // from inside this repo: require('./ts/dist/aontu') const aontu = new Aontu() console.log(aontu.generate('hello: world')) // { hello: 'world' } ``` `generate` takes aontu source text and returns a plain JavaScript value. That is the whole API surface you need today. ### Go The Go port lives in `go/`: ```sh cd go go test ./... # confirms the toolchain works ``` A scratch `main.go`: ```go package main import ( "fmt" aontu "github.com/aontu-lang/aontu/go" ) func main() { out, err := aontu.New().Generate("hello: world") fmt.Println(out, err) // map[hello:world] } ``` The two implementations accept the same source and produce the same shape; the parity is pinned by one test suite both implementations run. The rest of this page shows source and result: run them in whichever language you keep at hand. ### The `aontu` command Both implementations also ship an `aontu` command, and this page writes its CLI moments as transcripts of it. From a clone the command is `node ts/bin/aontu.js` (or `go run ./cmd/aontu` from inside `go/`); installed from npm (`npm i -g aontu`) it is plain `aontu`, which is how the transcripts spell it. Pipe a snippet in: ```sh $ echo 'a:1 b:$.a' | aontu { "a": 1, "b": 1 } ``` `b` followed a reference to `a` before printing: even a piped one-liner is fully evaluated. Run `aontu` with no file and you get a REPL; the [API reference](/docs/reference-api#command-line-interface) covers both. Two verbs help before the language does. `aontu init ` writes a model, an instance of it, and a `check.sh` that asks the four questions, which is a faster start than an empty file; `aontu explain ` prints the long form of a refusal, so a code in a message becomes a paragraph with examples. ## 2. Objects are just keys and values aontu source reads as relaxed JSON (it is parsed by [`@tabnas/jsonic`](https://github.com/tabnas/jsonic), so quotes, commas and braces are mostly optional). Plain data is legal on its own: ```aontu name: Mercury order: 1 rocky: true ``` → ```json { "name": "Mercury", "order": 1, "rocky": true } ``` No quotes on `Mercury`, no braces, no commas; the output is ordinary JSON all the same. Nesting repeats keys with a colon: `a:b:c:1` means `a: { b: { c: 1 } }`: ```aontu server: host: localhost server: port: 8080 ``` → ```json { "server": { "host": "localhost", "port": 8080 } } ``` The two `server:` lines did not collide. They merged, and that merge is your first unification. ## 3. Unification: combining facts Stating two things about the same place combines them. The explicit operator is `&`; between map keys it happens on its own: ```aontu server: { host: localhost } server: { port: 8080 } ``` → ```json { "server": { "host": "localhost", "port": 8080 } } ``` Where the statements agree, or one is more specific, unification keeps the combination. Where they disagree, we want an error, and we get one: ```aontu port: 8080 port: 9090 ``` → fails with: ``` [aontu/scalar_value]: Cannot unify values at path $.port Cannot unify value: 9090 with value: 8080 ``` (Trimmed; the real message also quotes both source lines, with a caret under each.) Combining information can only narrow toward a single answer or fail loudly. aontu never picks one fact over the other silently. ## 4. Types as values A bare type name is a value too: it means "any value of this kind". Unify it with a concrete value and the value wins, provided it fits: ```aontu port: integer port: 8080 ``` → ```json { "port": 8080 } ``` And when it does not fit: ```aontu port: integer port: "high" ``` → fails with: ``` [aontu/no_scalar_unify]: Cannot unify values at path $.port Cannot unify value: "high" with value: integer ``` The built-in kinds are `string`, `boolean`, `top` (the catch-all that admits anything), and the numeric family: `number` covers every numeric value, over its four leaves `integer`, `float`, `biginteger` and `bigdecimal`. Say `number` when you mean "some number", and name a leaf when you mean that leaf: `port: integer` will not accept `8080.5`. Your config now carries the first piece of its own schema. ## 5. Exact numbers with `0d` An ordinary aontu number, like an ordinary JSON number, is a binary floating-point value. For a port or a timeout that is fine. For money and for large identifiers it is not, because binary cannot represent every decimal: ```aontu total: 0.1 + 0.2 ``` → ```json { "total": 0.30000000000000004 } ``` Prefix the literals with `0d` and the arithmetic is exact, stored as decimal digits and computed without rounding: ```sh $ echo 'total: 0d0.1 + 0d0.2' | aontu { "total": 0.3 } ``` The prefix is source syntax only; the digits generate as an ordinary JSON number. Whole digits give a `biginteger` (`0d5`); a decimal point or an exponent gives a `bigdecimal` (`0d19.99`, `0d1e3`). Neither has a practical size limit. The same care shows up as a refusal. A literal that cannot be stored without silently becoming a different number is not stored: ```sh $ echo 'id: 9007199254740993' | aontu [aontu/lossy_integer_literal]: Cannot resolve value at path $.id This integer literal, 9007199254740993, is not exactly representable in binary64, so storing it would silently round it to a DIFFERENT number. aontu refuses rather than corrupts: write it as a `0d` literal to get the exact integer. ... $ echo $? 1 ``` That value is 2^53+1, just past where doubles start skipping whole numbers. Take the hint: ```sh $ echo 'id: 0d9007199254740993' | aontu { "id": 9007199254740993 } ``` The exact ID survives to the output. One thing to remember when you write the schema: an exact value is a `biginteger` or a `bigdecimal`, never an `integer`, so constrain it with the exact leaf (or with `number`, which admits any numeric leaf). ## 6. Defaults with `*` Mark a value as a **default** with `*`. A default is used only when nothing more specific is supplied: ```aontu port: *8080|integer ``` → ```json { "port": 8080 } ``` The `|` is **disjunction**, a choice between alternatives (`8080` or any `integer`), and the `*` says which branch to take when nothing forces the choice. Unify a concrete value on top and it wins: ```aontu port: *8080|integer port: 9090 ``` → ```json { "port": 9090 } ``` Now try a float: ```aontu port: *8080|integer port: 1.5 ``` → fails with: ``` [aontu/empty]: Cannot unify values at path $.port Empty disjunction. The disjunction has no valid alternatives. ``` An override must be admitted by one of the branches you wrote, and `1.5` is a `float`, which `integer` does not admit: why the gate is this strict is argued in the [explanation](/docs/explanation#a-preference-is-gated-by-kind-not-by-family). When any number should be able to win, say so in the branch: ```aontu port: *8080|number port: 1.5 ``` → ```json { "port": 1.5 } ``` `*value | kind` is the shape to remember: the kind you write is the kind you get. The full rule, including enums with defaults, is in the [language reference](/docs/reference-language#preference--default-). ## 7. References Pull a value from elsewhere in the document with a path; `$.` starts at the root: ```aontu defaults: timeout: 30 service: timeout: $.defaults.timeout ``` → ```json { "defaults": { "timeout": 30 }, "service": { "timeout": 30 } } ``` Change the default once and every reader of the path follows. A leading `.` is relative to the current object, and `key()` names the key a value is stored under: a compact way to give records their own name: ```aontu users: { alice:id:key() bob:id:key() } ``` → ```json { "users": { "alice": { "id": "alice" }, "bob": { "id": "bob" } } } ``` Each record read its own key. The [graph tutorial](/docs/tutorial-graph) picks this thread up again, where names become identities. ## 8. Templates with `&:` (spread) A `&:` entry inside a map is a **template** unified into every sibling key. Declare a shape once and it applies everywhere: ```aontu servers: { &: { region: *"us-east"|string active: *true|boolean } } servers: web: region: "eu-west" servers: db: {} ``` → ```json { "servers": { "db": { "active": true, "region": "us-east" }, "web": { "active": true, "region": "eu-west" } } } ``` `web` overrode the default region; `db` supplied nothing and took both defaults; the template itself does not appear in the output. Spreads work in lists too (`[&: {...}]`), and §11 leans on that form. ## 9. Functions aontu has a fixed set of built-in functions, and no user-defined ones. A few of the everyday ones: ```aontu web: region: "eu-west" name: upper(mercury) # -> "MERCURY" slug: lower(Mercury) # -> "mercury" label: a + b + c # -> "abc" copy: copy($.web) # deep copy of another node ``` → ```json { "copy": { "region": "eu-west" }, "label": "abc", "name": "MERCURY", "slug": "mercury", "web": { "region": "eu-west" } } ``` `+` concatenates strings and adds numbers; `upper` and `lower` double as ceiling and floor on numbers, and they keep exact numbers exact (`upper(0d1.1)` prints `2.0`, a `bigdecimal` ceiling). The rest are tabulated with tested examples in the [language reference](/docs/reference-language#functions): bounds, pattern, and length constraints; generators that build children; and `join`, which folds a bag into a line of text. You need none of them today. ## 10. Sealing a shape with `close` A map is **open** by default: unifying in extra keys is allowed, which is what lets separate files contribute separate keys. A schema usually wants the opposite. `close()` seals a shape: ```aontu point: close({ x:number y:number }) point: { x:1 y:2 } ``` → ```json { "point": { "x": 1, "y": 2 } } ``` Add a key the shape does not declare and the document refuses: ```aontu point: close({ x:number y:number }) point: { x:1 y:2 z:3 } ``` → fails with: ``` [aontu/closed]: Cannot resolve value at path $.point.z Cannot add to closed structure. ``` `open()` reverses it. How open a schema should be is a genuine design dial; [closed values](/docs/reference-language#closed-values-close--open) has the mechanics, and the [explanation](/docs/explanation#closed-world-validation-is-a-dial) the trade-off. ## 11. Putting it together Time to spend all of it. Here is a single document that is schema, defaults and data at once: save it as `config.aon`: ```aontu # --- schema + defaults (could live in its own file) --- service: close({ name: string host: *localhost|string port: *8080|integer rate: *0d0.01|bigdecimal tags: [&: string] }) # --- environment data merged on top --- service: { name:api port:9090 rate:0d0.025 tags: [public http] } ``` Run it: ```sh $ aontu config.aon { "service": { "host": "localhost", "name": "api", "port": 9090, "rate": 0.025, "tags": [ "public", "http" ] } } ``` Every default is `*value | kind`, the §6 shape, so `port: 9090.5` is refused rather than accepted. And `tags` says `[&: string]` where `[string]` might look like enough: a list literal is positional, so `[string]` constrains element 0 and waves `tags: [public, 7]` through; the [explanation](/docs/explanation#a-list-literal-is-positional) argues why it works that way. With those two spellings in place the schema constrains every field. Defaults filled `host`, the data supplied the rest, `rate` stayed exact from `0d0.025` to the printed `0.025`, and `close` kept stray keys out: one answer, assembled by unification, which would have failed loudly had anything conflicted. ## 12. Asking the document questions You have written something that says quite a lot. From here on, stop reading it and start asking it. `get` prints one slice of the answer: ```sh $ aontu model get '$.service.tags' config.aon [ "public", "http" ] ``` The path is the same `$.`-rooted path §7 used for references: quote it so the shell leaves the `$` alone. `why` is the verb to reach for when a value surprises you. It names every statement that contributed to a path, in source order: ```sh $ aontu model why '$.service.port' config.aon $.service.port = 9090 1. *8080|integer config.aon:5:9 2. 9090 config.aon:11:26 ``` Two contributions, and you wrote both: the default with its type, and the data that beat it, each with the line and column it came from. The first line is the *whole* written value: a contribution is a statement, so the `8080` and the `integer` arrive together, as the single value they were written as. Now ask about `host`, which nothing overrode: ```sh $ aontu model why '$.service.host' config.aon $.service.host = *"localhost"|string 1. *"localhost"|string config.aon:4:9 ``` One contribution, still wearing its `*`: the default answered only because nothing outranked it. `get` has `--keys`, `--types` and `--canon` views as well, and the rest of the verb surface is tabulated in the [API reference](/docs/reference-api#command-line-interface); [query a path](/how-to/query-a-path) and [explain a value](/how-to/explain-a-value) put these two to work. ## 13. Validating data with `aontu vet` Configuration rarely stays in one file: the schema is yours, the data arrives from somewhere else. Split `config.aon` at its comment. The schema half becomes `service.aon`: ```aontu service: close({ name: string host: *localhost|string port: *8080|integer rate: *0d0.01|bigdecimal tags: [&: string] }) ``` and the data half becomes `prod.aon`: ```aontu service: { name:api port:9090 rate:0d0.025 tags: [public http] } ``` `vet` asks whether a data document holds against a schema document: ```sh $ aontu vet service.aon prod.aon verdict: valid ``` Now a second environment arrives, `staging.aon`, written by someone else: ```aontu service: { name:search port:8100 tags: [internal 3] } ``` Vet it: ```sh $ aontu vet service.aon staging.aon verdict: invalid $.service.tags.1: no_scalar_unify [conflict] [aontu/no_scalar_unify]: Cannot unify values at path $.service.tags.1 data: staging.aon:1:50 (3) schema: service.aon:6:13 (string) $ echo $? 1 ``` Read the finding from the top. The path `$.service.tags.1` is exactly where the trouble is: element 1 of the list, the `3`. Then **two sites**, because a conflict is always between two statements and neither one owns the blame: `data` is what arrived (`3`, line 4, column 20 of `staging.aon`) and `schema` is what it had to [meet](/docs/unification) (`string`, line 6, column 16 of `service.aon`). Every finding is sited on both sides, so you never guess which file to open. And the exit code, `1`, is the verdict class: a CI job needs nothing else. The `3` was meant to be a tier name. Write `staging.aon` again, saying so: ```aontu service: { name:search port:8100 tags: [internal tier3] } ``` ```sh $ aontu vet service.aon staging.aon verdict: valid ``` Notice `staging.aon` never mentions `host` or `rate` and passes anyway: the schema's defaults stand in. To see what the service actually gets, unify the two files: a document that loads both is all it takes. Write `stack.aon`: ```aontu @"./service.aon" @"./staging.aon" ``` ```sh $ aontu stack.aon { "service": { "host": "localhost", "name": "search", "port": 8100, "rate": 0.01, "tags": [ "internal", "tier3" ] } } ``` `@"file"` loads a source file and unifies it in place, which is all "multi-file" means here; [split a model across files](/how-to/split-a-model-across-files) grows the idea into versioned, vendored dependencies. `vet` has a third verdict, and it is the one that stops the loop calling an unfinished document valid. Delete the `name` line from `staging.aon`: ```aontu service: { port:8100 tags: [internal tier3] } ``` ```sh $ aontu vet service.aon staging.aon verdict: incomplete $.service.name: mapval_required [incomplete] [aontu/mapval_required]: Cannot resolve value at path $.service.name schema: service.aon:2:9 (string) $ echo $? 3 ``` Nothing contradicts anything; the document is simply not finished yet. aontu has kept "wrong" and "unfinished" apart all through this page, and `vet` keeps them apart in its exit codes too: one code per verdict class, which is what makes it usable as a gate. The codes, the JSON and SARIF report forms and `--watch` are specified under [`aontu vet`](/docs/reference-api#aontu-vet), and [validate in CI](/how-to/validate-in-ci) is the recipe. That is the loop the whole verb surface exists for: **emit** a document, **vet** it against the truth it has to satisfy, and when it fails, let the two sites and `aontu model why` say where to **repair** it. You have now run it once by hand, which matters, because repair is the step you will eventually hand to a command: `aontu model set` rewrites overlay files under exactly these rules, and [change a pinned value](/how-to/change-a-pinned-value) shows what it will and will not touch. ## Where to go next Your config is a tree. Real systems are graphs: services that depend on each other, ownership that must not cycle, schemas that contain themselves. The [graph tutorial](/docs/tutorial-graph) is the second half of this one: identity, relations, reachability and recursion, on the engine you just used. When the document is ready to leave your repository, [share a model as a package](/docs/tutorial-package) pins it for whoever imports it, and [from a model to a file tree](/docs/tutorial-generate) computes source files from it. The [tutorials index](/docs/tutorial) lists all four. For a task you already have, go straight to its guide: - **Run, embed and integrate**: [call from TypeScript](/how-to/call-from-typescript), [call from Go](/how-to/call-from-go), [read a conflict error](/how-to/read-a-conflict-error) - **Templates, defaults and composition**: [provide defaults](/how-to/provide-defaults), [apply a template to many keys](/how-to/apply-a-template-to-many-keys) - **Schemas and constraints**: [constrain list elements](/how-to/constrain-list-elements), [make a field optional](/how-to/make-a-field-optional) - **Query, explain and change**: [query a path](/how-to/query-a-path), [explain a value](/how-to/explain-a-value) - **Validate and evolve**: [validate in CI](/how-to/validate-in-ci), [gate schema changes](/how-to/gate-schema-changes) - **Modules and multi-file**: [split a model across files](/how-to/split-a-model-across-files) Every rule and edge case is in the [language reference](/docs/reference-language); the command and both APIs are in the [API reference](/docs/reference-api); the reasons are in the [explanation](/docs/explanation). --- # Tutorial: model the system, not the tree _Model a system with identities, declared relations, and recursive schemas._ Source: https://github.com/aontu-lang/aontu/blob/main/docs/tutorial-graph.md · Page: https://aontu.dev/docs/tutorial-graph At 04:11 the ledger database went down, and at 04:12 the payments service went with it: a dependency that was real in production and recorded nowhere. Every config file involved was valid all night. The [first tutorial](/docs/tutorial-config) left you with a config that is schema, defaults and data in one document, describing each service completely, and each service alone. Nothing in it can say "these two files describe the same service", or "this feeds that, never in a loop", or "a chain of these, as long as it needs to be". This tutorial adds that layer: bringing two views into contact, declared relations (`rel` and the graph atoms), and recursive schemas. Each is a statement the engine checks: "payments depends on ledger" becomes something that can refuse. Run snippets as before (from a clone, `node ts/bin/aontu.js` stands in for `aontu`). Every output here is the engine's own. ## 1. Two views of one service The catalog team records what `payments` *is*; the platform team records what actually *runs*. Unification is path-aligned, so a claim at `$.catalog.payments` never [meets](/docs/unification) a claim at `$.deploy.eu1.payments`: a forked tier stays confident in both files, and one of them is wrong. Bringing the two into contact is a reference. Here are the two views, trimmed from [use-case 01](/use-cases/01-service-catalog) and inlined into one document: ```aontu catalog: payments: { owner:"team-payments" tier:1 } deploy: eu1: payments: $.catalog.payments & { image: "acme/payments:2.14.1" replicas: 6 } ``` → ```json {"catalog": {"payments": {"owner": "team-payments", "tier": 1}}, "deploy": {"eu1": {"payments": {"image": "acme/payments:2.14.1", "owner": "team-payments", "replicas": 6, "tier": 1}}}} ``` The deploy view now holds the whole service, and the two claims about `tier` have met. A disagreement between them is a located error rather than a silent fork: ```sh $ echo 'catalog: pay: {tier:1} deploy: pay: $.catalog.pay & {tier:2}' | aontu [aontu/scalar_value]: Cannot unify values at path $.deploy.pay.tier ... $ echo $? 1 ``` A reference is **directional**: `deploy` is narrowed by the catalog, and the catalog is not changed by the deploy. That direction is deliberate. The alternative (a global name both files declare, with every node carrying it merged into every other) reads well until you mount the same model twice, at which point the two instances are one entity and the second one's overrides are contradictions. The [reference](/docs/reference-language#linking-the-tree-is-the-namespace) has the argument; the practical consequence is that a model file is reusable, which §2 relies on. ## 2. Relations: an edge you can trust Now the connections. We model an ETL pipeline of three jobs (extract feeds transform, transform feeds load), trimmed from [use-case 12](/use-cases/12-relations), where the four-job original lives under test. The vocabulary sits in `spec.aon` (`hide()` marks it schema: present for unification, absent from output): ```aontu spec: hide({ Job: { kind:job feeds?:rel($.spec.JobShape) } JobShape: kind: job }) ``` `rel(t)` turns the field's strings into **tree addresses**: each must name a node in this evaluation, and the type `t` flows into every target, so an edge landing on something that is not a job refuses at the edge. The `?` makes the key [optional](/docs/reference-language#optional-keys-): a job with nothing downstream writes nothing. (`JobShape` is a thin stand-in: a self-typed `rel($.spec.Job)` inside `Job` is still in design.) The topology sits in `pipeline.aon`: plain lists of addresses, one of which carries a typo: ```aontu pipeline: jobs: { &: $.spec.Job } pipeline: jobs: extract: feeds: [path($.pipeline.jobs.tranform)] pipeline: jobs: transform: feeds: [path($.pipeline.jobs.load)] pipeline: jobs: load: {} ``` A two-line root, `model.aon`, joins them: ```aontu @"./spec.aon" @"./pipeline.aon" ``` ```sh $ aontu model.aon [aontu/rel_unresolved]: Cannot refer value at path $.pipeline.jobs.extract.feeds.0 ... $ echo $? 1 ``` Without `rel()`, that address is a perfectly good string and this pipeline silently loses everything downstream of extract. With it, the typo is a located refusal, and existence is decided inside the evaluation: an address resolves, or the document refuses. Correct the line in `pipeline.aon`: ```aontu pipeline: jobs: { &: $.spec.Job } pipeline: jobs: extract: feeds: [path($.pipeline.jobs.transform)] pipeline: jobs: transform: feeds: [path($.pipeline.jobs.load)] pipeline: jobs: load: {} ``` ```sh $ aontu model.aon ... "feeds": [ "$.pipeline.jobs.transform" ], ... ``` ### A constraint held on every address: `re()` Acme's convention says a job may only feed another **job**: not a raw dump, which lives elsewhere in the tree. The addresses say where each target is, so the convention is a rule about the address. Write it into the declaration, in `spec.aon`: ```aontu spec: hide({ Job: { kind: job feeds?: rel($.spec.JobShape) & re("^\\$\\.pipeline\\.jobs\\.") } JobShape: kind: job }) ``` A constraint beside `rel()` constrains the **address string**, and it is held onto every element of the list. To watch it work, write a change request: a file that includes the model and layers a delta on at the path it applies to. Propose a new edge, `raw.aon`: ```aontu @"./model.aon" pipeline: { dumps: raw: kind: job jobs: extract: feeds: [ path($.pipeline.jobs.transform) path($.pipeline.dumps.raw) ] } ``` ```sh $ aontu raw.aon [aontu/constraint]: Cannot unify values at path $.pipeline.jobs.extract.feeds.1 ... Cannot unify value: re("^\\$\\.pipeline\\.jobs\\.") with value: path($.pipeline.dumps.raw) ... $ echo $? 1 ``` `$.pipeline.dumps.raw` resolves, and it is even shaped like a job, but it is not *under* `jobs`, the held `re()` refuses the address, and the edge never exists. Conventions like this usually live in a wiki. This one is schema. ### The graph atoms: `acyclic()` and `inverse()` Two facts about the pipeline concern the whole edge set: no job may feed itself at any remove, and every fed job should name its feeders back. Declare both at the field, and give `fedBy` the same checked treatment, in `spec.aon`: ```aontu spec: hide({ Job: { kind: job feeds?: rel($.spec.JobShape) & acyclic() & inverse(fedBy) fedBy?: rel($.spec.JobShape) } JobShape: kind: job }) ``` The `feeds?:` line is now the entire relation: checked addresses, endpoint type, naming rule, acyclicity, inverse. During unification the atoms only *register* the declaration: one more edge can flip either property, so the verdict waits for generation, where every edge is known ([the rule](/docs/reference-language#declared-relations)). And `inverse(fedBy)` checks the mirror rather than writing it for you, so the data states both directions, in `pipeline.aon`: ```aontu pipeline: jobs: { &: $.spec.Job } pipeline: jobs: extract: feeds: [path($.pipeline.jobs.transform)] pipeline: jobs: transform: { fedBy: [path($.pipeline.jobs.extract)] feeds: [path($.pipeline.jobs.load)] } pipeline: jobs: load: fedBy: [path($.pipeline.jobs.transform)] ``` ```sh $ aontu model.aon { "pipeline": { "jobs": { "extract": { "feeds": [ "$.pipeline.jobs.transform" ], "kind": "job" }, ... "transform": { "fedBy": [ "$.pipeline.jobs.extract" ], "feeds": [ "$.pipeline.jobs.load" ], "kind": "job" } } } } $ aontu relations model.aon verdict: pass ``` The links generate as the plain strings the author wrote; the machinery stays in the hidden vocabulary, and the data keeps its JSON shape. The second command, the `relations` verb, reports the same verdict without generating anything. ### Refusing a cycle A change request makes load feed extract, and its author is careful: the inverse entry is dutifully written too. Save it as `cycle.aon`: ```aontu @"./model.aon" pipeline: jobs: load: feeds: [path($.pipeline.jobs.extract)] pipeline: jobs: extract: fedBy: [path($.pipeline.jobs.load)] ``` ```sh $ aontu cycle.aon [aontu/relation_cycle]: Cannot relate value at path $.pipeline.jobs.extract.feeds ... $ echo $? 1 $ aontu relations cycle.aon verdict: fail $.pipeline.jobs.extract.feeds.0 feeds: cycle $.pipeline.jobs.extract -> $.pipeline.jobs.transform -> $.pipeline.jobs.load -> $.pipeline.jobs.extract $ echo $? 1 ``` The care did not help, which is the point: acyclicity is a property of the whole graph, and no local diligence satisfies it. Generation refuses at an edge on the loop; the verb names the loop itself, closing back on the first node. ### Refusing a missing inverse A new job taps the transform output, and nobody records the feeder on its `fedBy`. The `change` list restates `job_load` because lists unify positionally (the first tutorial's §11 rule). Save it as `metrics.aon`: ```aontu @"./model.aon" pipeline: jobs: { metrics: fedBy: [] transform: feeds: [path($.pipeline.jobs.load) path($.pipeline.jobs.metrics)] } ``` ```sh $ aontu relations metrics.aon verdict: fail $.pipeline.jobs.transform.feeds.1 feeds: $.pipeline.jobs.metrics does not list $.pipeline.jobs.transform under fedBy $ echo $? 1 ``` The exact missing entry is named, at the position that states the edge. Add `$.pipeline.jobs.transform` to the new job's `fedBy` and the model passes. ### Reachability If extract dies, does load stop? That question needs the closure of the edge set, and it has its own verb: ```sh $ aontu reaches $.pipeline.jobs.extract $.pipeline.jobs.load --relation feeds model.aon verdict: reaches $.pipeline.jobs.extract -> $.pipeline.jobs.transform -> $.pipeline.jobs.load $ aontu reaches $.pipeline.jobs.load $.pipeline.jobs.extract --relation feeds model.aon verdict: unreachable $.pipeline.jobs.load does not reach $.pipeline.jobs.extract $ echo $? 1 ``` The answer is the path, because the path is what an operator acts on; a "no" carries none and exits `1`, so a policy like "nothing in the public tier may reach the ledger" can be a CI gate. One caution: this model writes both directions, so leaving off `--relation` lets load "reach" extract by walking `fedBy` upstream. A directional question names its relation. The [check-relations](/how-to/check-relations) and [query-reachability](/how-to/query-reachability) guides turn these verbs into CI recipes; the [API reference](/docs/reference-api#aontu-relations) lists every flag and exit code. ## 3. A schema as deep as the data An approval chain is a step that may be followed by another step, and the depth belongs to each policy: a schema that hard-codes three levels is wrong the day someone needs four. Here is the whole vocabulary, trimmed from [use-case 13](/use-cases/13-recursive-schema), as `schema.aon`: ```aontu spec: hide({ Step: { approver: string & re("^[a-z]+@acme[.]example$") decision: *pending|pending|approved|rejected then?: $.spec.Step } }) ``` Look at the last field. `$.spec.Step`, written *inside* `Step`, is a reference to the value being defined, and it simply means the fixpoint: a `Step` whose tail is a `Step`, by this very definition, all the way down. No marker, no depth parameter, no unrolled copies. Write a three-level chain against it, as `chain.aon`: ```aontu @"./schema.aon" payments: $.spec.Step & { approver: "lead@acme.example" decision: approved then: { approver: "cfo@acme.example" decision: approved then: approver: "audit@acme.example" } } ``` ```sh $ aontu chain.aon { "payments": { "approver": "lead@acme.example", "decision": "approved", "then": { "approver": "cfo@acme.example", "decision": "approved", "then": { "approver": "audit@acme.example", "decision": "pending" } } } } ``` The leaf never states a `decision`, and the ranked default supplied `"pending"` three levels down, from a schema one reference deep. Mechanically, the self-reference leaves a *residual* (a value still waiting for information) at each `then`, and the residual expands one level per meet with concrete data: the checks descend exactly as far as the chain does, and stop. Data is finite, so evaluation terminates. ### The guard is the `?` The engine never analyses a schema for well-foundedness; the data decides, and what ends the expansion is the `?` on `then?:`, where the data stops, the optional key drops. Spell the tail required and no finite chain can satisfy it. Try it, as `strict.aon`: ```aontu strict: hide({ Step: { approver:string then:$.strict.Step } }) doc: $.strict.Step & { approver: "lead@acme.example" then: approver: "cfo@acme.example" } ``` ```sh $ aontu strict.aon [aontu/recursion_unexpanded]: Cannot recurse value at path $.doc.then.then ... $ echo $? 1 ``` The schema itself is accepted (it is a perfectly good fixpoint), but every step demands a next step, so generation refuses at `$.doc.then.then`, the exact position where this document ran out. A required tail is a schema for documents that cannot exist. ### Vetting a chain you did not write The chains that matter arrive from outside (an approval tool, an agent) as plain JSON. Here is one with no aontu syntax in it at all, `review.json`: ```json { "approver": "lead@acme.example", "decision": "approved", "then": { "approver": "cfo@acme.example", "decision": "pending" } } ``` `vet --at` anchors the check at the recursive definition: ```sh $ aontu vet --at '$.spec.Step' schema.aon review.json verdict: valid ``` Now a chain that smuggles in an outside approver one level down, as `outside.json`: ```json { "approver": "lead@acme.example", "decision": "approved", "then": { "approver": "EXTERNAL@other.example", "decision": "maybe" } } ``` ```sh $ aontu vet --at '$.spec.Step' schema.aon outside.json verdict: invalid $.spec.Step.then.approver: constraint [conflict] [aontu/constraint]: Cannot unify values at path $.spec.Step.then.approver expected: re("^[a-z]+@acme[.]example$") actual: "EXTERNAL@other.example" data: outside.json:5:17 ("EXTERNAL@other.example") schema: schema.aon:3:24 (re("^[a-z]+@acme[.]example$")) ... $ echo $? 1 ``` The finding is one level down, sited on both sides (the data's line in `outside.json`, the schema's line in `schema.aon`), and the invented `decision` is refused right beside it. The depth cost nothing to write, and there is no blind spot at level fifty either! The [define-a-recursive-schema](/how-to/define-a-recursive-schema) guide adds what a vocabulary needs next: canonical form, and the hash that pins a recursive schema as one string. ## Where to go next You can now hand the engine plain-looking files and get back refusals for a forked fact, a dangling name, a dependency loop, an unmirrored edge, and a malformed chain at any depth: a decent set of questions to have asked about payments and ledger before 04:11. - The live models this page trimmed: [use-case 12, relations](/use-cases/12-relations) and [use-case 13, recursive schema](/use-cases/13-recursive-schema), each with a `check.sh` driving every refusal shown here and more. - Gating a repo on the checks: [check relations](/how-to/check-relations), [query reachability](/how-to/query-reachability), [define a recursive schema](/how-to/define-a-recursive-schema). - The rules in full: [linking](/docs/reference-language#linking-the-tree-is-the-namespace), [checked links](/docs/reference-language#checked-links-refert), [declared relations](/docs/reference-language#declared-relations), [recursive references](/docs/reference-language#recursive-references-fixpoints). - Why a checked link refuses where `owl:sameAs` silently merged, and why the graph checks are verbs: the [explanation](/docs/explanation). - The other tutorials: [share a model as a package](/docs/tutorial-package) gives this model a version and a hash for another project to import, [from a model to a file tree](/docs/tutorial-generate) computes source files from it, and the [tutorials index](/docs/tutorial) lists all four. --- # Tutorial: share a model as a package _Publish a schema as a package, acquire it from another project, and watch the pin refuse a change to what it means._ Source: https://github.com/aontu-lang/aontu/blob/main/docs/tutorial-package.md · Page: https://aontu.dev/docs/tutorial-package A schema two projects need gets copied into both, and from that moment there are two schemas. This tutorial takes one model out of its repository and gives it a version, a signature and a hash, so the second project imports it instead of copying it, and a later change to what it means is refused rather than absorbed. Everything here runs on one machine, with no network: the package repository is a directory. The one step that needs a listening process is described in [§9](#9-where-the-network-comes-in) and left to its guide. Commands are written as `aontu`; from a clone, `node ts/bin/aontu.js` stands in. Every result on this page is the engine's own. ## 1. The model two projects need Money has two fields and one rule that should exist in exactly one place. Save this as `rates/rates.aon`: ```aontu currency: string & re("^[A-Z]{3}$") amount: bigdecimal ``` `re` bounds a string by a pattern, and `bigdecimal` is the exact decimal leaf from [§5 of the first tutorial](/docs/tutorial-config#5-exact-numbers-with-0d): a price is not a `float`. A document of your own can be checked against it. Save one as `price.aon`: ```aontu currency: "eur" amount: 0d19.99 ``` and [vet](/docs/reference-api#aontu-vet) it: ```sh $ aontu vet rates/rates.aon price.aon verdict: invalid $.currency: constraint [conflict] [aontu/constraint]: Cannot unify values at path $.currency expected: re("^[A-Z]{3}$") actual: "eur" data: price.aon:1:11 ("eur") schema: rates/rates.aon:1:20 (re("^[A-Z]{3}$")) $ echo $? 1 ``` That is the value of the rule and the reason to share it: a lowercase currency code is caught in one line, in whichever project asks. Fix the data, in `price.aon`: ```aontu currency: "EUR" amount: 0d19.99 ``` ```sh $ aontu vet rates/rates.aon price.aon verdict: valid ``` ## 2. Make it a package A **module** is what an import names. A **package** is what you publish: a versioned archive of one or more modules. Both words are about to appear, and they are not synonyms. A package declares itself in a `pkg.aon` beside its sources. Write `rates/pkg.aon`: ```aontu pkg: { path:"corp.example/schemas/rates" version:"1.0.0" main:"rates.aon" } publish: public ``` Four facts, and no more: the `path` importers will write, the `version` this release carries, the `main` file an import of the bare path resolves to, and `publish: public`, which is the package saying it may leave the machine. The path's first segment carries a dot, which is what separates a module import from a file path. ## 3. Publish it into a directory A publish is signed, so there is a key first. `aontu pkg keygen` mints one and prints the public half, which is the id a consumer names: ```sh $ aontu pkg keygen key.pem ... ``` Keep `key.pem` where a build job can read it and nobody else can. Now publish, into a directory called `repo`. Without `--yes` every check runs and nothing is sent: ```sh $ aontu publish --key key.pem --to repo rates verdict: dry-run corp.example/schemas/rates 1.0.0 public archive: sha256:171e30a516146c364a20ddb7291cf9c9856fecb6b6223f5f29e8cdfb63096dcf (2 files, 353 bytes) module: corp.example/schemas/rates rates.aon aon1-vO_nsrLAdRBPpq8Vi8G_FgDDMWmoVGRjm6eZThS0QHA file: pkg.aon sha256:0f2e3cb9456aede3ec8e9fa99e5a1dd2c4bb5376b7ca23871e2f6aa9516773c0 92 file: rates.aon sha256:fed42fa726c3df4155c84dd35e690383b8a1a07d7f05a0544168a7f487c524d2 55 ... dry run: nothing sent (add --yes) ``` Two of those lines are the ones to keep your eye on. `archive` is the digest of the bytes; `module` is the **canon-hash**, `aon1-…`, which is what the module MEANS after evaluation. They answer different questions, and both are about to be pinned. Send it: ```sh $ aontu publish --yes --key key.pem --to repo rates verdict: sent ... sent ``` `repo` now holds the layout a consumer reads: ``` repo/ pkg/corp.example/schemas/rates/ @latest @v/ list 1.0.0.zip the archive 1.0.0.manifest what it holds, file by file, and what it means 1.0.0.sig the proof, over the manifest advisory/corp.example/schemas/rates.aon ``` Nothing in that directory was evaluated by the repository, and a consumer trusts none of it for more than availability: every pin is recomputed on the way in. ## 4. Take it on, in another project The consumer is a second project, beside the first. Its `checkout/pkg.aon` names itself and what it depends on: ```aontu pkg: { path:"corp.example/checkout" main:"main.aon" } dep: "corp.example/schemas/rates": v: "1.0.0" ``` The `v` is a **minimum**, not a pin: the lockfile pins, and §5 writes it. The entry document imports the module and adds its own facts. Write `checkout/main.aon`: ```aontu price: @"corp.example/schemas/rates" price: { currency:"EUR" amount:0d19.99 } ``` Those two lines are what the exercise is for: the schema arrives by import and the data meets it in the [ordinary way](/docs/unification). Nothing has been received yet, though, and `aontu pkg tidy`, the resolve step that fetches nothing, says so: ```sh $ aontu pkg tidy checkout verdict: missing corp.example/schemas/rates: not fetched (run: aontu sync) $ echo $? 1 ``` No lockfile was written: a partial lock would claim a closure that was never resolved. The hint names the arrangement in [§9](#9-where-the-network-comes-in), where a repository answers; here §5 puts the copy in place by hand instead. ## 5. Vendor the tree, and lock what it means The consumer's copy of a package lives in its own project, under `aontu_meta/vendor//`, one directory per path segment. Put the package's source tree there. Its `checkout/aontu_meta/vendor/corp.example/schemas/rates/pkg.aon`: ```aontu pkg: { path:"corp.example/schemas/rates" version:"1.0.0" main:"rates.aon" } publish: public ``` and its `checkout/aontu_meta/vendor/corp.example/schemas/rates/rates.aon`: ```aontu currency: string & re("^[A-Z]{3}$") amount: bigdecimal ``` That is a copy and nothing more: `cp -r` of the published tree. §9 is where a repository does the copying for you. Either way, `sync` is what turns a copy into a dependency: ```sh $ aontu sync checkout verdict: ok corp.example/schemas/rates 1.0.0 aon1-vO_nsrLAdRBPpq8Vi8G_FgDDMWmoVGRjm6eZThS0QHA ``` That hash is the one the publish printed. `sync` resolved the closure, evaluated the module on its own, and wrote one canonical line to `checkout/aontu_meta/pkg-lock.aon`: ``` # pkg-lock.aon (generated by `aontu sync`; do not edit) {"lock":{"corp.example/schemas/rates":{"archive":"sha256:171e30a516146c364a20ddb7291cf9c9856fecb6b6223f5f29e8cdfb63096dcf","canon":"aon1-vO_nsrLAdRBPpq8Vi8G_FgDDMWmoVGRjm6eZThS0QHA","v":"1.0.0"}}} ``` One line, JSON-parseable, and diffable like code. Commit it together with `aontu_meta/vendor/`: that pair is the build, on a machine with no network. ## 6. Evaluate with nothing but the project ```sh $ aontu checkout/main.aon { "price": { "amount": 19.99, "currency": "EUR" } } ``` The import resolved from the vendor tree, the pattern held, and `0d19.99` reached the output exactly. Ask why the package is in the closure and the answer is a path of requirements: ```sh $ aontu why corp.example/schemas/rates checkout verdict: ok corp.example/checkout -> corp.example/schemas/rates ``` One hop, because the project asked for it directly. A package pulled in by a dependency of a dependency prints the chain that reaches it. ## 7. The pin earns its keep Here is the part a copied file cannot do. Someone relaxes the vendored schema, perhaps to get a build through. Rewrite `checkout/aontu_meta/vendor/corp.example/schemas/rates/rates.aon`: ```aontu currency: string amount: bigdecimal ``` The data still satisfies it, so nothing about the document is wrong. Evaluate anyway: ```sh $ aontu checkout/main.aon module integrity: corp.example/schemas/rates expected aon1-vO_nsrLAdRBPpq8Vi8G_FgDDMWmoVGRjm6eZThS0QHA got aon1-wixJdyL2g90c1HaWaoBKBpHA6da7Hn-MEikz6LW-BVI $ echo $? 1 ``` Both hashes are named: the meaning that was reviewed, and the meaning the store holds now. `aontu pkg verify` asks the same question of the bytes first, and never reaches the meaning here because the archive digest already disagrees: ```sh $ aontu pkg verify checkout verdict: mismatch corp.example/schemas/rates: pinned archive sha256:171e30a516146c364a20ddb7291cf9c9856fecb6b6223f5f29e8cdfb63096dcf but the store holds sha256:62be1cd19a5db54c374a3b0539ec0973ae66bbb0b5d2411f28fbba3eb7a1e716 $ echo $? 1 ``` That is the command for a build job: it recomputes every pin, writes nothing, and exits `1` on any disagreement. Put `checkout/aontu_meta/vendor/corp.example/schemas/rates/rates.aon` back: ```aontu currency: string & re("^[A-Z]{3}$") amount: bigdecimal ``` ```sh $ aontu pkg verify checkout verdict: ok corp.example/schemas/rates: verified ``` Be precise about what the canon-hash protects. It is taken over what the module MEANS, so comments, whitespace, and a rewrite that canons to the same value all keep it, deliberately. The bytes are the `archive` pin's job, and it is the stricter of the two: a comment added to a vendored file keeps the meaning and still fails `aontu pkg verify`, which is why a vendored tree is re-vendored rather than edited. ## 8. The next version is gated on this one Back in the package. Currencies differ in how many decimal places they carry, so `rates.aon` gains a field. Rewrite `rates/rates.aon`: ```aontu currency: string & re("^[A-Z]{3}$") amount: bigdecimal precision?: integer ``` The `?` makes the key optional, so a document that never mentions it still holds. Bump the version in `rates/pkg.aon`: ```aontu pkg: { path:"corp.example/schemas/rates" version:"1.0.1" main:"rates.aon" } publish: public ``` and publish. `publish` fetches the highest version the repository already holds and compares the two: ```sh $ aontu publish --yes --key key.pem --to repo rates verdict: sent corp.example/schemas/rates 1.0.1 public ... against: corp.example/schemas/rates 1.0.0 ... sent ``` `against` names what it was gated on. Now make the same field required, which is the edit that feels like a tidy-up and is not. Rewrite `rates/rates.aon`: ```aontu currency: string & re("^[A-Z]{3}$") amount: bigdecimal precision: integer ``` with `rates/pkg.aon` at a new minor version: ```aontu pkg: { path:"corp.example/schemas/rates" version:"1.1.0" main:"rates.aon" } publish: public ``` ```sh $ aontu publish --yes --key key.pem --to repo rates verdict: breaking corp.example/schemas/rates 1.1.0 public ... $.precision: the general value requires this key; the specific value makes it optional, so instances without it are admitted ... $ echo $? 1 ``` Refused, at the key it refuses on, and nothing was written. The two names in that message are the sides of the question: the candidate is the general value, the version already published is the specific one, and a release ships only when the candidate admits every instance its predecessor did. Every 1.0.1 consumer with no `precision` would stop evaluating, and no version number lifts the gate: a break ships as a new package path, and [modules](/docs/reference-language#modules) has the `moved` declaration that retires the old one. Meanwhile `checkout` is untouched at 1.0.0, because its lockfile says so. `aontu get corp.example/schemas/rates@1.0.1` is how it moves, when it chooses to, and it needs a repository to reach: the arrangement in [§9](#9-where-the-network-comes-in), or a fresh vendored copy and `aontu sync`. ## 9. Where the network comes in Every command above ran against a directory, which is why this page needs none. A colleague cannot read your directory, though, so the arrangement between two machines serves it instead: `aontu pkg serve repo` serves the same bytes over HTTP on a loopback address and runs until interrupted, which is the one thing a transcript cannot show. A consumer then names the base and the signer it accepts, in its own `pkg.aon`: ```aontu pkg: { path:"corp.example/checkout" main:"main.aon" } dep: "corp.example/schemas/rates": v: "1.0.0" repo: base: ["http://127.0.0.1:8017"] repo: trust: "corp.example/*": { signer:"ed25519:…" inclusion:none } ``` and `aontu sync` fetches the archive, the manifest and the proof, checks the proof, then the bytes, then the meaning, and refuses at the first that does not hold. What it writes is the lockfile and the vendor tree from §5, with one pin more: `manifest`, the digest of what the publisher signed. The rest of the page is unchanged, which is the reason it was worth learning in this order. Evaluation never reaches the network in either arrangement. An `@"…"` import resolves from the vendor tree and a local cache, and from nowhere else. ## Where to go next Your model is now a thing with a version, and the two questions that follow are what to pin it against and who may move it: - The recipes, each one task: [publish a package](/how-to/publish-a-package) with a served repository and a trust stanza, [vendor a dependency closure](/how-to/vendor-a-dependency-closure) for the frozen CI form, [vendor a module by hand](/how-to/vendor-by-hand) for a package that was never published, and [gate schema changes](/how-to/gate-schema-changes) for the compatibility query §8 ran, asked of two documents rather than two versions. - The rules in full: [`aontu sync`](/docs/reference-api#aontu-sync), [`aontu publish`](/docs/reference-api#aontu-publish), [`aontu pkg`](/docs/reference-api#aontu-pkg), and [modules](/docs/reference-language#modules) for how an import resolves. - The live version, cold start through tamper, trust confinement and the publish gate, with a `check.sh` that drives all of it: [use-cases/11-shared-modules](/use-cases/11-shared-modules). - The other tutorials: [from a model to a file tree](/docs/tutorial-generate) computes source files from the model you just shared, and the [tutorials index](/docs/tutorial) lists all four. --- # Tutorial: from a model to a file tree _Compute a TypeScript client from a model of its routes, then write the file tree with aontu render._ Source: https://github.com/aontu-lang/aontu/blob/main/docs/tutorial-generate.md · Page: https://aontu.dev/docs/tutorial-generate A client library for an HTTP API says the same thing three times: once in a route table, once in a function per route, once in the types. Two of those are derivable from the first, and a model that already holds the route table already holds the file. This tutorial computes a small TypeScript client from a model of its routes. The result is a **component tree**: an ordinary aontu value whose nodes are a project, a folder, files, and lines. The tree is checkable on its own, and [§6](#6-write-the-files) writes it to disk with `aontu render`. Commands are written as `aontu`; from a clone, `node ts/bin/aontu.js` stands in. Every result on this page is the engine's own. ## 1. The model Two routes are enough to show the shape. Save this as `api.aon`: ```aontu routes: [ { name:"list" verb:GET path:"/orders" } { name:"create" verb:POST path:"/orders" } ] ``` ```sh $ aontu api.aon { "routes": [ { "name": "list", "path": "/orders", "verb": "GET" }, { "name": "create", "path": "/orders", "verb": "POST" } ] } ``` Plain data, and deliberately so. A model worth generating from is one the rest of the project can also validate, query and diff, which is what the [first tutorial](/docs/tutorial-config) built. ## 2. A file is a value `file(name, children)` names a file and holds its lines. Write `client.aon`, which loads the model and describes one file: ```aontu @"./api.aon" out: file("client.ts", ["export const timeout = 5000"]) ``` `@"./api.aon"` loads a source file and unifies it in place, so `$.routes` is in scope here. The file itself is at `$.out`, and the verb that reads a value is `get`: ```sh $ aontu model get '$.out' client.aon { "children": [ { "children": [], "cmp": "Line", "props": { "src": "export const timeout = 5000" } } ], "cmp": "File", "props": { "name": "client.ts" } } ``` Three keys make up every node of the tree. `cmp` is what kind of node it is, `props` carries what that kind needs, and `children` holds what sits inside it. The bare string became a `Line`, which is the node that writes its own newline, so nothing in the source spells one. ## 3. One line per record A generator is a **rule set**. `emit(selection, table)` visits every node of a selection, takes the first template whose `match` the node unifies with, and instantiates that template's `body` against the node, where `.name` is that node's `name`. A list is visited in its own order, which is why the route table is a list: a map is visited in code-point order of its keys, whatever order it was written in. Rewrite `client.aon`: ```aontu @"./api.aon" %route = emit(_, { match: verb: GET body: [`export const ` + .name + ` = () => get("` + .path + `")`] }) out: file("client.ts", emit($.routes, %route)) ``` Two things about the spelling. A backtick string may hold a literal newline and a `"` without escaping it, which is what makes it the way to carry another language's text. And `%route` is a named table, declared once and called by name, so a document can hold several generators without nesting them. Run it: ```sh $ aontu model get '$.out' client.aon ... [aontu/emit_none]: Cannot resolve value at path $.out.0 ... $ echo $? 4 ``` The refusal is the lesson. `create` is a `POST`, the only template matches `GET`, and a generator that silently dropped the route would have produced a client missing a function. `message` is the headline; the node it could not place and the matches it tried are in the finding's `hint`, which `--format json` carries and running the document prints in full: ```sh $ aontu client.aon ... node unifies with; the node {"name":"create","path":"/orders","verb":"POST"} unified with none of {"verb":"GET"}. ... $ echo $? 1 ``` ## 4. A second template, and the dispatch Give `POST` a template of its own. A table may be a list of them, tried in the order written. Rewrite `client.aon`: ```aontu @"./api.aon" %route = emit(_, [ { match: verb: GET body: [`export const ` + .name + ` = () => get("` + .path + `")`] } { match: verb: POST body: [`export const ` + .name + ` = (b) => post("` + .path + `", b)`] } ]) out: file("client.ts", emit($.routes, %route)) ``` ```sh $ aontu model get '$.out' client.aon { "children": [ { "children": [], "cmp": "Line", "props": { "src": "export const list = () => get(\"/orders\")" } }, { "children": [], "cmp": "Line", "props": { "src": "export const create = (b) => post(\"/orders\", b)" } } ], "cmp": "File", "props": { "name": "client.ts" } } ``` Two lines, in the model's order, each taking the body its `verb` selected. The one to ask about later is which rule wrote which line, and `aontu trace` answers it, one row per piece: ```sh $ aontu trace client.aon client.ts $.children.0 $.routes.0 $.%route#0 client.ts $.children.1 $.routes.1 $.%route#1 ``` The file it reached, the node in the tree, the model record that matched, and the rule: `%route#0` is the `GET` template and `#1` the `POST` one. A line nobody generated has no row, which is worth remembering when a report looks short. ## 5. A folder, and a project A client is more than one file, and files live in directories. `folder(name, children)` is a directory, `project(dir, children)` is the root and names the output directory, and both hold children the same way a file holds lines. Rewrite `client.aon` a last time: ```aontu @"./api.aon" %route = emit(_, [ { match: verb: GET body: [`export const ` + .name + ` = () => get("` + .path + `")`] } { match: verb: POST body: [`export const ` + .name + ` = (b) => post("` + .path + `", b)`] } ]) out: project("build", [ folder("src", [ file("client.ts", [ `import { get, post } from "./http"` emit($.routes, %route) ]) file("index.ts", each(pick($.routes, name), `export { ` + _ + ` } from "./client"`)) ]) ]) ``` Two new shapes in there. The `import` line is a plain string beside an `emit` call, and the call **splices**: its pieces join the list rather than nesting inside it, so a body can mix written lines with generated ones. And `index.ts` uses a different generator: `pick` projects one field out of every record, and `each` makes one element per member, so the re-exports come out in the model's order. `pack` would key them by data and sort the keys, which is right for a map and wrong for a file. ```sh $ aontu model get '$.out' client.aon { "children": [ { "children": [ { "children": [ { "children": [], "cmp": "Line", "props": { "src": "import { get, post } from \"./http\"" } }, { "children": [], "cmp": "Line", "props": { "src": "export const list = () => get(\"/orders\")" } }, { "children": [], "cmp": "Line", "props": { "src": "export const create = (b) => post(\"/orders\", b)" } } ], "cmp": "File", "props": { "name": "client.ts" } }, { "children": [ { "children": [], "cmp": "Line", "props": { "src": "export { list } from \"./client\"" } }, { "children": [], "cmp": "Line", "props": { "src": "export { create } from \"./client\"" } } ], "cmp": "File", "props": { "name": "index.ts" } } ], "cmp": "Folder", "props": { "name": "src" } } ], "cmp": "Project", "props": { "folder": "build" } } ``` A `Project` whose `folder` is `build`, holding a `Folder` named `src`, holding two `File` nodes, each holding its lines. Nothing in that tree is a template waiting to be filled or a string waiting to be split: it says which files exist, in which directories, with which lines, in which order. And because it is an ordinary value, `get` reads any node of it. Read one leaf: ```sh $ aontu model get '$.out.children.0.children.1.props.name' client.aon "index.ts" ``` `aontu vet` can check the tree against a schema, and the rule behind one generated line is what [§4](#4-a-second-template-and-the-dispatch)'s `aontu trace` names. ## 6. Write the files `aontu render` writes the tree. It hands it to a **generator runtime**, [jostraca](https://github.com/jostraca/jostraca), which both implementations depend on; each node's `cmp` is the component name that runtime looks up, which is what those keys in the JSON above are for. The path argument is the root the tree is written under, and the `Project`'s own `folder` names the directory inside it: ```sh $ aontu render --at '$.out' client.aon . ``` It prints nothing, and `build/src/client.ts` and `build/src/index.ts` are on disk. `--check` writes nothing and compares instead: ```sh $ aontu render --check --at '$.out' client.aon . $ echo $? 0 ``` Hand-edit `build/src/client.ts`: ```ts export const oops = 1 ``` and the check names the file and refuses: ```sh $ aontu render --check --at '$.out' client.aon . content: build/src/client.ts $ echo $? 1 ``` That is the form for CI: commit the generated files beside the model, and an edit to one of them is a red build rather than a quiet divergence from the model that produced it. The tree is still a value either way, which is why [§5](#5-a-folder-and-a-project) could read a leaf of it with `aontu model get` before anything was written. The rest is a pipe, and it is a recipe rather than a lesson: [generate code from a model](/how-to/generate-code) has the command that hands `aontu model get` output to a runtime, the `--folder` form that compares instead of writing (a generated file edited by hand is then a red build), and the way to write the generator in the target's own syntax so that `gofmt` and an editor can read it. ## Where to go next You have a model that computes its own source files, and the questions that follow are what else the model should hold: - The recipes: [generate code from a model](/how-to/generate-code) end to end with goldens, [seal generated children deeply](/how-to/seal-generated-children) when a generated shape must stay closed, and [keep schema out of the output](/how-to/keep-schema-out-of-output) for the helper fields a generator needs and a file does not. - The rules in full: [transforming with `emit`](/docs/reference-language#transforming-emit) for dispatch order, splicing and recursion, [`each`](/docs/reference-language#each-the-order-preserving-map) and its `_ & …` idiom, and [generation](/docs/reference-language#generation) for the ten component functions and what each node means for the output. [`aontu trace`](/docs/reference-api#aontu-trace) is specified with the other verbs. - The live version: three targets in one document, committed goldens, and a check that both implementations answer byte-identical files, in [use-cases/15-code-generation](/use-cases/15-code-generation). - The other tutorials: [share a model as a package](/docs/tutorial-package) gives the model a version and a hash so another project can import it, and the [tutorials index](/docs/tutorial) lists all four. --- # Language reference _The core language, exhaustively: syntax, semantics, the unification lattice, and every operator._ Source: https://github.com/aontu-lang/aontu/blob/main/docs/reference-language.md · Page: https://aontu.dev/docs/reference-language Complete, exhaustive description of the aontu language: lexical structure, every value form and operator, evaluation order, the canonical form, and generation rules. Behaviour stated here is verified by the shared [`test/spec/*.tsv`](https://github.com/aontu-lang/aontu/blob/main/test/spec/) suite and holds in both the TypeScript and Go implementations unless a difference is called out. For the public programming interface see the [API reference](/docs/reference-api). For the reasoning behind the model see the [Explanation](/docs/explanation). ## Contents - [Lexical structure](#lexical-structure) - [The value lattice](#the-value-lattice) - [Scalars](#scalars) - [Scalar kinds (types)](#scalar-kinds-types) - [Maps](#maps) - [Lists](#lists) - [Container kinds: `map()` and `list()`](#container-kinds-map-and-list) - [Conjunction `&`](#conjunction-) - [Disjunction `|`](#disjunction-) - [Preference / default `*`](#preference--default-) - [Optional keys `?`](#optional-keys-) - [Spreads `&:`](#spreads-) - [Generating children: `pack` and `each`](#generating-children-pack-and-each) - [Making elements: `each`](#each-the-order-preserving-map) - [The `_ & …` idiom](#the-_---idiom-construction-and-bound) - [Selecting: `filter` and `match`](#selecting-filter-and-match) - [The placeholder `_`](#the-placeholder-_) - [Transforming: `emit`](#transforming-emit) - [References and paths](#references-and-paths) - [Recursive references (fixpoints)](#recursive-references-fixpoints) - [Variables `$name`](#variables-name) - [Aliases `%`](#aliases-) - [The `+` operator and grouping](#the--operator-and-grouping) - [Functions](#functions) - [Arithmetic: `add` `sub` `mul` `div` `mod` `rem`](#arithmetic-add-sub-mul-div-mod-rem) - [Projecting fields: `pick`](#projecting-fields-pick) - [Optional input: `maybe`](#optional-input-maybe) - [Ordering: `sort`](#ordering-sort) - [Aggregating: `sum` `least` `greatest`](#aggregating-sum-least-greatest) - [Folding to a string: `join`](#folding-to-a-string-join) - [Text: `esc` `usc` `rep` `split`](#text-esc-usc-rep-split) - [Linking: the tree is the namespace](#linking-the-tree-is-the-namespace) - [First-class paths: `path(p?)`](#first-class-paths-pathp) - [Checked links: `refer(t?)`](#checked-links-refert) - [Declared relations](#declared-relations) - [Marks: `type` and `hide`](#marks-type-and-hide) - [Closed values: `close` / `open`](#closed-values-close--open) - [Source loading `@"…"`](#source-loading-) - [Text: `.txt` and `--text-ext`](#text-txt-and---text-ext) - [Operator precedence](#operator-precedence) - [Canonical form](#canonical-form) - [The formatted form](#the-formatted-form) - [The published grammar](#the-published-grammar) - [Generation](#generation) - [Subsumption](#subsumption) - [Errors](#errors) - [Grammars: `abnf()` and `parse()`](#grammars-abnf-and-parse) - [A grammar reads better in backticks](#a-grammar-reads-better-in-backticks) - [A grammar can say what it builds](#a-grammar-can-say-what-it-builds) - [Shaping an unannotated tree](#shaping-an-unannotated-tree) - [The constraint algebra](#the-constraint-algebra) - [Named constraint aliases](#named-constraint-aliases) --- ## Lexical structure aontu source is parsed by [`@tabnas/jsonic`](https://github.com/tabnas/jsonic) with aontu-specific plugins, so the surface syntax is "relaxed JSON". - **Whitespace** separates tokens; newlines and commas are interchangeable separators. `a:1 b:2`, `a:1, b:2`, and `a:1\nb:2` are equivalent. - **Comments** begin with `#` and run to end of line. A file of only comments unifies to `{}`. - **Bare strings** need no quotes (`name: Mercury`), and may hold letters, digits, `-` and `_`, and nothing else. So `owner: team-payments`, `on: 2026-09-05` and `id: user_42` are bare strings. Every other punctuation character is either syntax, where the grammar gives it a meaning, or an error where it does not: `x=y`, `6/2`, `50%` and `>10` are refused with `[aontu/bare_punct]`, which names the character, rather than read as strings. Quote with `"…"`, `'…'` or `` `…` `` to include spaces or any other character (`name: "hi there"`, `ratio: "6/2"`). All three are the same kind of value; only what they may contain differs. - **Keys** follow the same rule: bare when they hold only letters, digits, `-` and `_` (`host`, `a-b`), quoted otherwise. - **Backtick strings may span lines.** `"…"` and `'…'` refuse a literal newline; `` `…` `` accepts one, so a backtick string carries several lines of text as one scalar. This is what lets a document hold a block of another language: a shell script, a template, a fragment of generated source. Escapes are processed in all three forms, so `\t` is a tab and `` \` `` is a literal backtick. A **literal** control character in the source is refused (`[aontu/unprintable]`), including a literal tab: write `\t`. - **Numbers** come in two families. A plain JSON number (`1`, `1.5`, `1e3`) is stored as an IEEE-754 double and takes `integer` or `float` kind; a `0d`-prefixed literal (`0d5`, `0d0.1`) is stored *exactly*, with no binary rounding anywhere, and takes `biginteger` or `bigdecimal` kind. Which of the four a literal takes is decided by its source text, never by its magnitude; the rule is stated in full under [Scalar kinds](#scalar-kinds-types). - **Exact literals** are written `0d` (or `0D`) followed by digits. Digits alone give a biginteger (`0d123`); adding a `.` or an exponent gives a bigdecimal (`0d0.1`, `0d1e3`). The grammar is `0[dD] digits [ "." digits ] [ (e|E) [+-] digits ]`. The sign goes *before* the prefix (`-0d5`, never `0d-5`) and a marker with no digit after it is not a literal at all: `0d` is the bare string `"0d"`, and `0d.5` reads as member access on that string. - **Other numeric forms.** Hexadecimal (`0x1f`), octal (`0o17`) and binary (`0b1010`) literals use lower-case prefixes, and belong to the plain family, not the exact one. (Only the exact marker also accepts its letter in upper case: `0D12` is a literal, `0X1F` is the bare string `"0X1F"`.) `_` may separate digits (`1_000_000`, `0d1_000`), but only singly and only *between* digits: a run that breaks the rule is not a number at all, so `1__0` is the string `"1__0"`, not `10`. - **A number that cannot be stored exactly is refused.** An integer literal the double format would silently round is a located error naming the `0d` escape, not an approximation: see [Exact or refused](#exact-or-refused-lossy-literals). - **Booleans** are `true` / `false`; **null** is `null`. A backtick string is how a document holds a block of another language. Here `greet.aon` carries a shell script as one value: ```aon greet: `#!/bin/sh echo "hi" ` tab: `x\ty` ``` ```sh $ aontu -c greet.aon {"greet":"#!/bin/sh\necho \"hi\"\n","tab":"x\ty"} $ echo $? 0 ``` The relaxed forms combine in one document: ```aon a: 1 b: 2 c: Mercury d: "hi there" ``` ```json {"a":1,"b":2,"c":"Mercury","d":"hi there"} ``` ## The value lattice Every aontu value is a point in a lattice ordered from most general to most specific:
top string number boolean null path() integer float biginteger bigdecimal nil
The value lattice: top at the join; string, number, boolean and null under it; path() under string; integer, float, biginteger and bigdecimal under number; nil at the meet, below every kind.
The engine draws this figure itself: it is [`aontu view lattice`](/docs/reference-api#aontu-view) over a document with no values in it. Run the same verb over your own document and each node carries a count of the values that landed there. - **`top`** is the unit: unifying anything with `top` yields the other value. It is what an unconstrained field is. - **`nil`** (bottom) is the result of a failed unification. It carries an error message and cannot be generated. - **Unification** of two values is their *greatest lower bound*: the most general value at least as specific as both. If none exists, the result is `nil`. This ordering is why unification is order-independent and idempotent: `a & b` equals `b & a`, and `a & a` equals `a`. ## Scalars | Form | Example source | Generates | |-------------|----------------|-----------| | integer | `a:1` | `1` | | negative | `a:-5` | `-5` | | float | `a:1.5` | `1.5` | | biginteger | `a:0d5` | `5` | | bigdecimal | `a:0d0.1` | `0.1` | | bare string | `a:hello` | `"hello"` | | quoted str | `a:"hi there"` | `"hi there"` | | boolean | `a:true` | `true` | | null | `a:null` | `null` | Two scalars unify only if they are of the same kind *and* equal (`1 & 1` → `1`, `foo & foo` → `"foo"`); otherwise the result is a conflict (`1 & 2` → error, and so is `1 & 1.0`). ## Scalar kinds (types) A bare kind name is a *type*: the set of all scalars of that kind. | Kind | Matches | |--------------|------------------------------------------------| | `string` | any string | | `number` | any numeric value: the supertype over the four leaves below | | `integer` | any value of *integer kind* (below) | | `float` | any value of *float kind* (below) | | `biginteger` | any value of *biginteger kind* (below) | | `bigdecimal` | any value of *bigdecimal kind* (below) | | `boolean` | `true` or `false` | | `top` | any value at all | The path kind is spelled `path()` rather than a bare word, and sits under `string`: see [First-class paths](#first-class-paths-pathp). The container kinds are `map()` and `list()`: see [Container kinds](#container-kinds-map-and-list). ### The four numeric leaves Every numeric value carries a **kind**, fixed when the value is built, and it is the kind (not the magnitude) that decides what the value unifies with. There are four numeric kinds, and `number` is not one of them: `number` names the whole family and nothing else, so no value ever has `number` kind. ``` number (a pure supertype — no value has this kind) ├── integer a double, whole, in the int64 window 1 1e3 ├── float any other double 1.5 1e21 ├── biginteger exact, whole, unbounded 0d5 0d1_000 └── bigdecimal exact, with a point or an exponent 0d0.1 0d1e3 ``` The two upper leaves hold IEEE-754 doubles (every value a plain JSON number can hold exactly) and the source rule below decides which of them a literal joins. The two lower leaves are reached only by writing `0d`, and hold their digits *exactly*: no binary rounding, and no precision limit but the [exactness budget](#the-exactness-budget). The four leaves are **disjoint**. No value belongs to two of them, and values of different leaves never unify however equal they look: `1 & 1.0`, `5 & 0d5` and `0d5 & 0d5.0` are all conflicts. A cross-leaf result would have to pick a kind, and either choice would make `&` asymmetric in kind. **Leaf by source.** Which leaf a literal lands in is decided by how it is written, never by how large it is. A literal *without* the `0d` prefix has **integer** kind if, and only if, all three of these hold: 1. its source text contains no `.`; 2. its value is integral (no fractional part); 3. its value lies within the int64 range, that is `-9223372036854775808 ≤ n < 9223372036854775808`. Anything else has **float** kind. The upper bound is *exclusive* because these values are doubles and 2^63−1 cannot be represented in one: it rounds up to 2^63, and so falls outside the range. A literal *with* the `0d` prefix has **bigdecimal** kind if its source contains a `.` or an exponent, and **biginteger** kind otherwise. ``` 1 → integer (no '.', integral, in range) 1e3 → integer (1000 — an exponent is not a '.') 9007199254740992 → integer 1.0 → float (rule 1: the source has a '.') 1.5 → float (rules 1 and 2) 1e21 → float (rule 3: beyond int64) 100000000000000000000 → float (rule 3) 0d5 → biginteger (0d, digits only) 0d1_000 → biginteger 0d0.1 → bigdecimal (0d with a '.') 0d1e3 → bigdecimal (0d with an exponent) ``` The two families nearly mirror each other, with one asymmetry: a `.` splits the leaf in both, but an exponent splits it only in the `0d` family: `1e3` is an integer, `0d1e3` a bigdecimal. **Canon rendering.** Canon renders a number so that reparsing it yields the same kind again, which takes three markers: - an integer-kind value renders plainly: `1000`; - a float-kind value always carries a fraction or an exponent, so a `.0` suffix is appended when the shortest rendering has neither: `1.0`, `100000000000000000000.0`; - an exact value carries the `0d` marker, with any sign in front of it: `0d5`, `-0d5`, `0d0.1`. Because `0d` names the *family* and not the leaf, one more marker is needed to tell the two exact leaves apart, and it is the same `.0` device: **an integral bigdecimal always renders with one decimal place.** So `0d1e3` canons as `0d1000.0` while the biginteger `0d1000` canons as `0d1000`. Without that, `canon(0d1e3)` would reparse as a biginteger: a different lattice point, since the leaves are disjoint. Exact values render in plain form at every magnitude, never in scientific notation, and **one value has exactly one rendering**: scale is presentation, not identity, so `0d0.10`, `0d0.1` and `0d1e-1` all parse to the same value and all canon as `0d0.1`. Edge cases: - The same rules apply wherever a numeric value is built (a parsed literal, a `$var` binding, a raw value handed to the API) so a given number never has two different kinds depending on where it came from. Where there is no source text, condition 1 is vacuous and conditions 2 and 3 decide. - A literal that overflows the double range entirely (`1e999`) is not a number at all; it is an error. One that *underflows* to exactly zero (`1e-400`) is integer-kind `0`. - Negative zero never survives, in any leaf: `-0.0` normalises to `0.0`, `-0d0` to `0d0`, and `-0d0.0` to `0d0.0`, in canon and in generated output alike. - aontu has no negative literals: `-` is a prefix operator applied to a positive literal. The int64 *minimum* therefore cannot be written as an integer-kind literal: `-9223372036854775808` negates the float-kind literal `9223372036854775808` and stays float kind. Write it `-0d9223372036854775808` to hold it exactly, as a biginteger. ### Exact or refused: lossy literals An integer literal is stored only if the double format holds it *exactly*. One that would be silently rounded is a located error instead, and the message names the fix: write it with `0d`. The input that triggers this rule is ordinary JSON: for example, a 64-bit record ID in a dump from an API. `id: 9007199254740993` is 2^53+1, the first whole number a double cannot hold. Storing it anyway would yield 9007199254740992, a different ID, with nothing said about it. aontu refuses: ```sh $ echo 'id: 9007199254740993' | aontu [aontu/lossy_integer_literal]: Cannot resolve value at path $.id This integer literal, 9007199254740993, is not exactly representable in binary64, so storing it would silently round it to a DIFFERENT number. aontu refuses rather than corrupts: write it as a `0d` literal to get the exact integer. ... $ echo $? 1 ``` (That is the TypeScript wording; Go phrases the same refusal differently. Both name the `0d` escape.) Take the escape and the document works again, exactly: in generated output and in canonical form: ```sh $ echo 'id: 0d9007199254740993' | aontu { "id": 9007199254740993 } $ echo 'id: 0d9007199254740993' | aontu -c {"id":0d9007199254740993} ``` One consequence to plan for: the rescued value has **biginteger** kind, not `integer`, so a schema constraining it must say `biginteger` (or the family, `number`). `id: integer` would now conflict. ``` id:0d9007199254740993 & biginteger → {"id":0d9007199254740993} id:0d9007199254740993 & number → {"id":0d9007199254740993} id:0d9007199254740993 & integer → error ``` **The rule is exactness, not magnitude.** A shorter literal can be refused while a much longer one is fine, because what matters is whether the exact value happens to be a double: ``` 9007199254740992 → integer (2^53, exactly representable) 9007199254740993 → error (2^53+1 is not) 100000000000000000000 → float (10^20 — far larger, still exact) 0x7fffffffffffffff → error (2^63−1 rounds up to 2^63) 0x8000000000000000 → float (2^63 itself is a power of two) ``` The refusal covers every integer-literal form, decimal and base-prefixed alike, and it happens at parse time, so a lossy literal never reaches unification. ### The exactness budget The exact leaves have no precision limit in the ordinary sense (a biginteger is as wide as its digits) but a bigdecimal is bounded, so that a short source cannot demand unbounded work. The bound is one a document can rely on: > A bigdecimal may carry **at most 4096 coefficient digits** and an > **absolute scale of at most 4096**. The *coefficient* is the significant digits with the point removed; the *scale* is where the point sits among them, which for a literal is its fraction digits minus its exponent. So `0d1.5e-4095` has coefficient 2 and scale 4096, and is the last value of its shape that fits. Both halves are checked independently, on literals (against the source as written, before normalisation) and on every computed result. Exceeding either is a located error: *"This exact decimal exceeds the exactness budget"*. aontu has no rounding mode and no precision context, so a value beyond the budget is refused rather than approximated. ``` 0d1e-4096 → 0d0.000…0001 (scale 4096 — inside) 0d1e-4097 → error (scale 4097 — outside) 0d1e4097 → error (the bound is two-sided) 0d1e1000000000 → error (refused before rendering it) 0d1e-4000 + 0d1e4000 → error (the exact sum needs 8001 digits) ``` `biginteger` has no scale and no coefficient bound: a whole number of ten thousand digits is an ordinary value. ### Unification rules - **kind & matching scalar → the scalar.** `number & 2` → `2`; `string & hello` → `"hello"`; `1 & integer` → `1`; `0d1.5 & bigdecimal` → `0d1.5`. - **kind & non-matching scalar → conflict.** `1 & string` → error; `1.0 & integer` → error (`1.0` is float kind whatever its value), and so are `1e21 & integer`, `0d5 & integer` and `1 & biginteger`. - **kind & kind:** equal kinds unify to themselves; `number & ` → that leaf (`number & integer` → `integer`, `number & bigdecimal` → `bigdecimal`); two distinct leaves conflict, as do unrelated kinds. - **scalar & scalar:** two concrete numbers are the same only when kind *and* value match. So `1 & 1.0` is a conflict, and `1|1.0` is a real two-branch disjunction: `(1|1.0) & 1.0` selects the float. Value comparison for the exact leaves is over the number, not its spelling: `0d1.5 & 0d1.50` is `0d1.5`. No operator or function narrows a kind: see [`+`](#the--operator-and-grouping) and [`upper()`/`lower()`](#functions). The int64 window, the `.0` canon suffix and the `0d` marker are stated in [the four numeric leaves](#the-four-numeric-leaves) and [Canonical form](#canonical-form). ## Maps A map is an unordered set of key/value pairs. Braces are optional at the top level. - **Literal:** `a:{b:1,c:2}` → `{"a":{"b":1,"c":2}}`. - **Implicit nesting:** a chain of colons builds nested maps: `a:b:c:1` → `{"a":{"b":{"c":1}}}`. - **Duplicate-key merge:** stating a key twice unifies the two values. `a:{b:1}, a:{c:2}` → `{"a":{"b":1,"c":2}}`. The merge recurses through nesting: ```aon a: b: c: 1 a: b: d: 2 a: e: 3 ``` ```json {"a":{"b":{"c":1,"d":2},"e":3}} ``` Maps are **open** by default (extra keys may be unified in) until sealed with [`close`](#closed-values-close--open). ## Lists A list is an ordered sequence. - **Literal:** `a:[1,2,3]` → `{"a":[1,2,3]}`. Elements may be whitespace-separated: `[1 2 3]`. - **Mixed / nested / of maps:** `[1,two,true]`, `[[1,2],[3,4]]`, `[{x:1},{y:2}]` all work. - **A pair is a single-key map element:** `[a:1, b:2]` is `[{a:1}, {b:2}]`: the braces are optional for a one-key map in list position, and the two spellings are the same document. An optional pair carries its `?` into the element (`[a?:1]` is `[{a?:1}]`), a numeric key is a key of the element map and never an index into the list (`[0:1]` is `[{"0":1}]`), and a chain nests (`[a:b:1]` is `[{a:{b:1}}]`). - Lists unify element-by-element by position (and support `&:` spreads, below). The pair form reads naturally for ordered records: ```aon routes: [get:"/health" post:"/orders"] ``` ```json { "routes": [ { "get": "/health" }, { "post": "/orders" } ] } ``` ## Container kinds: `map()` and `list()` `{}` and `[]` are the container *units*: each admits any value of its shape, and generates empty when nothing else arrives. `map()` and `list()` are the container *kinds*: each admits exactly the same values and defaults to nothing, as `string` does. The kind is the spelling of "a map must be supplied here": an unmet unit silently manufactures its empty value, an unmet kind refuses to generate. ```aon required: map() & { a:1 } ``` ```json { "required": {"a": 1} } ``` The contrast, unmet: ```sh $ echo 'y: {}' | aontu -c {"y":{}} $ echo 'y: map()' | aontu [aontu/mapval_no_gen]: Cannot resolve value at path $.y ... $ echo $? 1 ``` A kind mismatch refuses with the unit's own codes (`[aontu/map]`, `[aontu/list]`): `map() & [1]` is the same fact `{} & [1]` reports. Neither function takes arguments: element constraints belong to the spreads (`{&: V}`, `[&: V]`). The kinds settle inside `type()` bodies, [meet](/docs/unification) the unit literals (`map() & {}` is `{}`: an explicitly supplied empty map satisfies the kind), and subsume their containers (`map()` subsumes `{a:1}`). Pinned by [`test/spec/containerkind.tsv`](https://github.com/aontu-lang/aontu/blob/main/test/spec/containerkind.tsv). ## Conjunction `&` `a & b` is the explicit unification of `a` and `b`: the same operation that merges duplicate map keys. ```aon a: 1 & integer b: { x:1 } & { y:2 } c: { x:p:1 } & { x:q:2 } ``` ```json {"a":1,"b":{"x":1,"y":2},"c":{"x":{"p":1,"q":2}}} ``` Two kinds meet to the narrower kind and stay a kind: `number & integer` canons as `integer` and does not generate on its own. Conjunction is commutative, associative, and idempotent. It **distributes over disjunction**: `x & (a|b)` tries `x` against each alternative. ## Disjunction `|` `a | b` is a choice of alternatives. It is kept open until something selects a branch. ``` a:1|2 → canon {"a":1|2} a:string|number → canon {"a":string|number} a:1|2|3 → canon {"a":1|2|3} ``` Unifying a concrete value selects the matching branch (others become nil and drop out): ```aon a: 2 a: 1|2 b: 2 b: string|number ``` ```json {"a":2,"b":2} ``` `&` binds tighter than `|`, so `c & b | a` parses as `(c & b) | a`. **An unresolved disjunction has no value**. More than one alternative still admitted means the truth is not yet settled, so generation refuses with `disjunct_no_gen`, class `incomplete`: the same class a bare `string` residue answers: ``` a:1|2 → [aontu/disjunct_no_gen] at $.a a:{x:1}|{y:2} → [aontu/disjunct_no_gen] at $.a ``` Two things resolve it: a value that selects an alternative, or a preference saying which one holds when nothing else does (below). Alternatives that are the *same value* collapse first, so `1|1` and `{a:1}|{a:1}` each generate that one value: sameness is structural for maps and lists (container kind, closedness, marks, optional keys, then the children). An optional key whose value is an unresolved disjunction is dropped rather than reported, as every other unresolved optional is. ## Preference / default `*` `*x` marks `x` as **preferred** (a default). In a disjunction the preferred branch is chosen unless unification forces another. ```aon a: *1|number b: *5 c: *green|string d: *1|number d: 2 ``` ```json {"a":1,"b":5,"c":"green","d":2} ``` The preference survives in canonical form (`a` above canons as `{"a":*1|number}`) because a default is constraint information, not a resolved value. Defaults propagate through nesting and spreads. `pref(x)` is the function form of `*x` (canon `*x`). Preferences can be ranked (a `*` of a `*` outranks a single `*`); the lowest rank wins when two preferred values meet. A ranked preference meets its peers exactly as rank 1 does: the **rank-uniform meet**: `a:**1.5 & float` is `1.5` just as `a:*1.5 & float` is, and `**2|integer` met by a bare `integer` keeps its default. Overriding a default is judged in two steps, and they are the two arms of the disjunction `*x` stands for: `*x & peer` is `(x & peer) | (super(x) & peer)`. **The preferred value answers first.** A peer it still admits leaves the preference standing, narrowed to what survived: `a:*1.5 & float` and `a:*1.5 & number` are both `1.5`, `a:*8080 & min(1024)` is still `*8080`, and `a:*integer & 7` is `*7`. **Otherwise its type answers, and that is the override.** `a:*8080 & 9090` is `9090`: `8080` cannot admit it, `integer` can. When neither arm admits the peer, nothing is left of the disjunction and the refusal is `empty`: `a:*2 & 3.0`, `a:*2.2 & 3` and `a:*1.5 & integer` are all errors, because the numeric leaves are disjoint. The type is `super(x)`, so the rule reaches every kind of default: `super(integer)` is `number`, so `a:*integer & 7` narrows and `a:*integer & "s"` refuses. **Two defaults of the same rank must agree.** `a:*1` beside `a:*7` is `pref_rank_clash`, in that spelling and in `a:*1|*7`: the disagreement is between the DEFAULTS, and the fix is to rank one of them (`**`). Compatible defaults fold: `a:*1` beside `a:*integer` is `*1`. **A preference conjoined with a disjunction names an alternative**: `(A|B) & *A` is `*A|B`, the same value the direct spelling denotes, so the two ways of writing an enum-with-default agree. ```aon a: ("1.0"|"1.1") & *"1.0" ``` ```json {"a":"1.0"} ``` The canon is `{"a":*"1.0"|"1.1"}`. A preference that names no alternative is dropped (it has nothing to prefer) so `("1.0"|"1.1") & *"2.0"` canons as `"1.0"|"1.1"`. The default-validity lint below is what reports that shape. **A preference inside a disjunction is gated by admission**: an override must be admitted by the disjunction itself: by at least one alternative, or by the preferred value. A preferred branch contributes exactly its own value to the admitted set, so `*'auto' | 'literal' | 'data'` is a true **enum with a default**: unset generates `"auto"`, `'literal'` and `'data'` override, and anything else is the empty disjunction (`[aontu/empty]`). A wider alternative admits a wider override (`*8080 | integer` accepts any integer), and a constraint alternative is consulted rather than bypassed (`*8080 | (integer & min(1024) & max(65535))` refuses `80` and accepts `2048`; `*8080 | (integer & neq(80))` refuses `80`). A deliberately open default states its openness: `*x | top` admits every override. The gate covers scalar preferred values: the same boundary as the kind gate above. ```aon a: *8080|integer a: 9090 b: *8080|number b: 1.5 c: *8080|string c: 8080 ``` ```json {"a":9090,"b":1.5,"c":8080} ``` An alternative admits `a`'s override (same leaf); the `number` branch admits `b`'s float; the preferred value admits itself at `c`. An override nothing admits is the empty disjunction: ```sh $ echo 'k: *auto | literal | data k: autoo' | aontu [aontu/empty]: Cannot unify values at path $.k ... $ echo $? 1 ``` The refusals follow the same rule at every width: `*8080 | integer` met by `1.5` is `[aontu/empty]` (the other numeric leaf), and `*8080 | (integer & neq(80))` met by `80` is refused because the exclusion is consulted, not bypassed. A document that wants an open override says so by writing the open branch explicitly, `*x | top`. **A structural default is gated too**, by the same rule as every other: the peer must pass `super(x)`, and `super({x:1})` is `{x:integer}`. A map default therefore MERGES with a map that adds a key (the preferred value itself admits it) and refuses a value of another kind outright: ```aon a: *{ x:1 } a: y: 2 b: *{ x:1 } b: x: 2 ``` ```json {"a":{"x":1,"y":2},"b":{"x":2}} ``` `a` keeps its `x` default and gains `y`; `b`'s `x` is overridden, because `{x:1}` cannot admit `{x:2}` but its type can. A peer of another kind (`a: "s"`) refuses, as the scalar case always did. A document that wants a structural default any peer may replace says so by writing the open branch explicitly, `*{x:1} | top`. Writing `a:{x:*1}` rather than `a:*{x:1}` is still the clearer spelling when you mean "a map whose `x` defaults to 1", and it is what `pref({x:1})` produces. Pinned by the `pref-struct-*` rows in [`test/spec/pref.tsv`](https://github.com/aontu-lang/aontu/blob/main/test/spec/pref.tsv). ## Optional keys `?` A key suffixed with `?` is optional. If it never receives a concrete value, it is **dropped from the generated output** instead of erroring. ```aon x?: number y: Y a: {y?:number, z:2} a: {} b: {y?:number, z:2} b: {y:11} c: {y?:number, z:*3} c: {y:11} ``` ```json {"a":{"z":2},"b":{"y":11,"z":2},"c":{"y":11,"z":3},"y":"Y"} ``` The unresolved `x?` is dropped, `b`'s filled `y` is kept, and `c`'s default still applies beside the filled key. Optionality survives references: a referenced map drops its unresolved optional keys too. ## Spreads `&:` A `&:` entry is a **template** unified into every other entry of its map or list. The template itself is not emitted: ```aon c: { &: { x:2 } y:k:3 z:k:4 } ``` ```json {"c":{"y":{"k":3,"x":2},"z":{"k":4,"x":2}}} ``` A template may be a kind (`&: string`), a constraint map (`&: {x:number}`), a referenced value (`&: $.tmpl`), or carry a per-child overridable default (`&: x: *1|number`). A template that names each child uses `key()`: ```aon a: b: { &: { name:key() } c: {} d: {} } ``` ```json {"a":{"b":{"c":{"name":"c"},"d":{"name":"d"}}}} ``` Other forms: - **Implicit / cross-statement:** `a:b:{} a:&:{x:1}` → `{"a":{"b":{"x":1}}}`. - **Top-level:** `a:{} &:{x:1}` → `{"a":{"x":1}}` (applied to every root key). - **Lists:** the spread applies to every element, and canon keeps the spread entry (`[&:{"x":1},{"y":1,"x":1},…]`): ```aon l: [&: { x:1 } y:1 y:2] ``` ```json {"l":[{"y":1,"x":1},{"y":2,"x":1}]} ``` **Several templates apply independently, per child.** When one bag accumulates more than one `&:` template (consecutive spreads, spreads from different statements, templates arriving by reference through a conjunction or an id-merge) every child meets the combined constraint of all of them, and only that: children never meet each other's data through the templates, whatever mix of literal values, kinds, references, defaults or `key()` the templates carry. A key one template requires is required at every child; a default one template carries defaults (and stays overridable) per child. ```aon w: &: {p: integer} w: &: {r: integer} w: x: {p:1, r:5} w: y: {p:2, r:6} ``` ```json {"w":{"x":{"p":1,"r":5},"y":{"p":2,"r":6}}} ``` ## Generating children: `pack` and `each` A spread constrains children that already exist. `pack` and `each` **make** them, from data that is already in the model, so the list of names and the children built from it cannot drift apart: ```aon names: [web auth billing] deploy: close(pack($.names, { image: "acme/" + key() + ":1.4.2" replicas: *2|integer port: *8080|integer })) deploy: billing: replicas: 4 # an override composes as usual ``` ```json {"names": ["web", "auth", "billing"], "deploy": { "web": {"image": "acme/web:1.4.2", "replicas": 2, "port": 8080}, "auth": {"image": "acme/auth:1.4.2", "replicas": 2, "port": 8080}, "billing": {"image": "acme/billing:1.4.2", "replicas": 4, "port": 8080}}} ``` `pack(data, tmpl)` makes one **keyed child** per child of `data`. The keys are **data, never position**: for a list, the strings themselves (a non-string element is an error, `pack_key`); for a map, its keys. Each generated child is `tmpl` cloned at that destination, so `key()` and relative references inside the template answer for the child rather than for the call. Duplicate keys are not an error: the colliding children unify, exactly as duplicate source keys merge. **Instantiation is per destination, to the leaves**. The clone a destination receives is a *full instance*: nothing in it (not a call's arguments, not a preference's inner value, not an operator's operands) is shared with the template or with any sibling destination, and every path inside it is the destination's. So `close({name: key()})`, `**key(1) | string` and `.a + 1` inside a template all answer per child, in expressions and call arguments as much as in bare positions; the first child's resolution can never answer for the others. The same rule instantiates a `filter` condition per trial and a spread constraint (`&:`) per application. `each(data, tmpl)` makes one **list element** per child of `data`. It is documented in full [below](#each-the-order-preserving-map); what matters here is that the same `_` that binds the source child also lets it be kept, so `each(d, _ & t)` is every member of `d` met with `t`, and `each(d, _)` is a map's children as a list. The order is fixed: source order for a list, sorted-key order for a map. ```aon ports: { http:80 https:443 } open: each($.ports, _ & integer) names: each({ b:2 a:1 }, _) ``` ```json {"ports":{"http":80,"https":443},"open":[80,443],"names":[1,2]} ``` Once fired, generated children are **ordinary children**: a destination `&:` spread applies to them, `close()` seals the generated shape, references reach into them, and a template may itself contain a generator. Both **wait for the model to settle** before they fire, and fire exactly once. A generator's data can still be merged into by a sibling statement, an include or a spread after it first looks complete, and children generated from a half-merged bag would be missing. The data argument's **snapshot waits for the source too**: a reference like `pack($.ports, …)` copies its target only once the target has finished resolving in the tree, so a source augmented by a spread (even one injecting relative references (`ports: &: {port: .containerPort}`)) reaches the generator with those references already answered at the source. Until it fires, a generator canons as its own call (`pack($.n,…)` with the data reference still standing) which reparses to the same value. Neither can recurse. Both iterate a finite bag that already exists, so the number of children either can produce is fixed by the data: evaluation still terminates by construction. ### `each`, the order-preserving map `each(data, tmpl)` makes one **list element** per child of `data`, being `tmpl` instantiated at that position with `_` bound to the source child. Written plainly it **replaces** rather than meets: the element is the template and nothing else, which is what makes it a construction. Mentioning the hole keeps the child; that is the [`_ & …` idiom](#the-_---idiom-construction-and-bound) below. ```aon names: [web auth billing] files: each($.names, { path:_ + ".ts" }) consts: each($.names, upper(_)) tag: join(each(split("index-build", "-"), upper(_)), "_") ``` ```json {"names": ["web", "auth", "billing"], "files": [{"path": "web.ts"}, {"path": "auth.ts"}, {"path": "billing.ts"}], "consts": ["WEB", "AUTH", "BILLING"], "tag": "INDEX_BUILD"} ``` The order is the data's (source order for a list, sorted-key order for a map) through the one rule every bag reader uses, and a hidden child or an unfilled optional is skipped as generation would skip it. That order is why the list generator is a built-in at all: `pick(pack(d, {f: t}), f)` maps too, but through a map, so it re-sorts to code-point order, and the fields of a struct, the imports of a file or an index in the model's order would come out alphabetised. With `split` and `join` it closes the name-derivation chain, as `tag` shows. Like `pack`, it waits for the model to settle and fires once; a `_` inside its template is its own to bind, never an enclosing generator's. ### The `_ & …` idiom: construction and bound `_` inside a generator's template binds the **source child**. Whether that child survives into the element is decided by one thing: whether the template mentions the hole. ```aon ports: [containerPort:80 containerPort:443] plain: each($.ports, { protocol:TCP }) bound: each($.ports, _ & { protocol:TCP }) ``` ```json {"ports": [{"containerPort": 80}, {"containerPort": 443}], "plain": [{"protocol": "TCP"}, {"protocol": "TCP"}], "bound": [{"containerPort": 80, "protocol": "TCP"}, {"containerPort": 443, "protocol": "TCP"}]} ``` `plain` **replaces**: the element is the template, and the port numbers are gone. `bound` **meets**: `_ & {protocol:TCP}` is the child unified with the template, so each entry keeps its `containerPort` and gains a `protocol`. One generator, two jobs, and the `_` says which. Three shapes cover most uses: | written | the element is | use it for | |---|---|---| | `each(d, t)` | `t`, instantiated | building new records from data | | `each(d, _ & t)` | the child, met with `t` | constraining or extending members | | `each(d, _)` | the child itself | a map's values as a list | **`each(d, _ & t)` is a bound**, so everything a meet does applies: a kind checks the members, a constraint atom bounds them, and a preference supplies a default the member may override. ```aon ports: [8080 443] checked: each($.ports, _ & integer) m: { b:2 a:1 } vals: each($.m, _) ``` ```json {"ports": [8080, 443], "checked": [8080, 443], "m": {"a": 1, "b": 2}, "vals": [1, 2]} ``` **`each(d, _)` is the map-to-list conversion**: the template is the hole and nothing else, so every member arrives unchanged, in sorted-key order for a map and source order for a list. The same `_` binds in a `pack` template, a `filter` condition and an `emit` body, and it always names the value that construct is working on. Two rules govern it: - **The hole belongs to the nearest enclosing generator.** In `pack($.m, {inner: each(_, _)})` the first `_` is the *pack's* source child, because a generator's data argument is not a binding position, and the second is the `each`'s own. - **A spread has no hole.** `&: {n: _}` leaves `_` unfilled; inside a spread, name the child's fields with a relative reference (`.k`) and its key with `key()`. A meet cannot select, so `_` does not reach into the child: asking for one of its fields with `each($.lines, _ & _.amount)` asks for something that is both the whole record and one of its fields. Use [`pick`](#projecting-fields-pick) to project a field. ## Selecting: `filter` and `match` `filter(data, cond)` keeps the children of `data` that **already satisfy** `cond` (keys preserved for a map, order for a list) and drops the rest silently: ```aon services: { web: { debug:true port:80 } auth:port:81 } debugged: filter($.services, { debug:true }) sidecars: pack($.debugged, { image:"acme/debug:1.0" }) ``` ```json {"services": {"web": {"debug": true, "port": 80}, "auth": {"port": 81}}, "debugged": {"web": {"debug": true, "port": 80}}, "sidecars": {"web": {"image": "acme/debug:1.0"}}} ``` "Already satisfies" means the meet **changes nothing**: `cond` adds no information the child did not have. Mere unifiability would not do: a map is open, so `{port:81}` unifies with `{debug:true}` by *gaining* the key, and a filter that kept everything that could be made to match would keep everything. The condition is an ordinary value, so the constraint atoms compose with it: `filter($.deploy, {replicas:min(3)})`. `match(v, p1, r1, …, d?)` is a **bounded conditional**. The first pattern in argument order that `v` **already satisfies** selects its result, which is the answer; a trailing argument (the one that makes the argument count even) is the default: ```aon tier: large size: match($.tier, small, { cpu:1 }, large, { cpu:8 }, { cpu:2 }) ``` ```json {"tier":"large","size":{"cpu":8}} ``` A pattern is held to the same "already satisfies" rule as `filter`'s condition above, so kinds and atoms work as patterns (`match(x, integer, …, string, …)`, `match(n, min(0), …)`) while a pattern naming a key the value lacks does not match by gaining it. There are no guards, no comparisons beyond the atoms, and no fallthrough. **No match and no default is an error** naming the patterns that were tried, not an empty answer: a default is how a document says the rest was meant to be allowed. An unselected result is never evaluated, so a broken arm nobody takes is not an error the document has to carry. **A defaulted scrutinee matches as the value it generates**. A settled scrutinee that carries an effective default (a preference, or a disjunction holding one) is tested as the innermost preferred value, not as the still-open preference. So with `side_effect: *readonly | write | destructive`, the derivation `match(.side_effect, destructive, true, false)` answers `false` when `side_effect` is unset (the effective value is `"readonly"`), and `true` only when it is genuinely `destructive`. Before this rule a pattern could *select* an arm by overriding the default, deriving a value that contradicted the one generated beside it. A pref-free open disjunction still matches by plain unifiability. Both wait for the model to settle before they answer, for the reason `pack` and `each` do: a bag that is still being merged into is the wrong bag to take a subset of, and a scrutinee that is still being narrowed can match an earlier arm than the one it will end up matching. **A `match` does not fire on an unfilled hole.** `match(_, …)` outside a generator's template never answers: the peer that would fill the hole is not also checked against the arm the fill selects (see [The placeholder `_`](#the-placeholder-_)), so a `match` written as a schema would accept every document it was asked about. The call stands unresolved instead, and a `vet` run says so. Inside a generator's template the hole is the source child, the scrutinee is a value by the time the match runs, and the form works as documented above. ## The placeholder `_` A bare `_` is a **hole**: a call holding one waits, and whatever the call is unified with fills it. ```aon greeting: upper(_) & hello x: {&: {m: _ + 2}} x: a: m: 1 ``` ```json {"greeting":"HELLO","x":{"a":{"m":3}}} ``` The peer goes **into** the call and is not also a constraint on the way out: `upper(_) & hello` is `"HELLO"`, not `"HELLO" & "hello"`. Two holes meeting is an error: neither has a value to fill the other. `match` is the one call a peer does not fill, because the arm it would select is not then checked against that peer: see [Selecting](#selecting-filter-and-match). Inside a generator's template, `_` is the **source child** the generated one is being made from: ```aon ports: { http:80 https:443 } open: pack($.ports, { port:_ name:key() }) ``` ```json {"ports": {"http": 80, "https": 443}, "open": {"http": {"name": "http", "port": 80}, "https": {"name": "https", "port": 443}}} ``` A hole belongs to its **nearest enclosing generator**: an outer generator's fill pass never reaches into a nested generator's template (or a `filter`'s condition), so in `pack($.envs, {services: pack($.fleet, {v: _})})` the inner `_` is the fleet entry, not the env. A hole in a generator's *data* argument is not a binding position, so it is still the outer generator's to fill: `pack($.m, {inner: each(_, _)})` iterates the outer source child. A generator whose data is a hole is filled by its **peer**, exactly as any other call is (`["a"] & pack(_, {x:1})` packs the list) which is what lets a rule table be named (see [Transforming](#transforming-emit)). And wrapping a generator in a call (`close(pack(d, _ & t))`) does not expose the template's hole to the wrapper's peers: an overlay statement merges with the generated children, never with the template. For a `pack` over a list of names, `_` and `key()` are the same thing: the name is the key. Over a map they differ: `key()` is the key, `_` is the value. In a `filter` condition, `_` is the child being tested. A hole is not a function parameter: it cannot be named, passed, or partially applied, and there is no way to write one that is not already inside a call. Unfilled at generation it is an error, exactly as `top` is. A bare `_` is a hole, pinned by `test/spec/place.tsv`. Quoted `"_"` is that string, any longer bare word containing it (`_b`) is ordinary text, and `_` as a **key** is a key. ## Transforming: `emit` `emit(select, table)` applies a **rule table** to a selection of nodes. For every node, in order, the first template whose `match` the node already satisfies is taken, and its `body` is instantiated against that node. The answer is one flat list of pieces: ```aon services: [{ kind:sqs pin:"srv:a" } { kind:http path:"/a" }] lines: emit($.services, [ { match:kind:sqs body: ["listen(" + .pin + ")"] } { match:kind:http body: ["serve(" + .path + ")"] } ]) ``` ```json {"services": [{"kind": "sqs", "pin": "srv:a"}, {"kind": "http", "path": "/a"}], "lines": ["listen(srv:a)", "serve(/a)"]} ``` A **table is a list of templates**, tried in order, and each template is a map naming both a `match` and a `body`. A table of one may be written as the template map itself. Both keys are required: a template with no pattern would claim every node by accident, and one with no body would claim a node and emit nothing. **The body is a list, and the result is flat.** A body element that is itself a list splices into the answer rather than nesting, which is what lets one dispatch compose into another. **Two things inside a body name the matched node**: `_` is the node, and a *relative* reference is a field of it: `.pin` is that node's `pin`. An absolute reference (`$.x`) is untouched and still reads the document root. A relative reference the node cannot answer is an error (`emit_ref`) reported against the node, not a miss somewhere else: inside a body, only a chain of plain names is a field, so a parent step has no answer at a node that is an origin rather than a position. **An empty selection emits nothing**, and that is the whole conditional mechanism: there is no `when` directive because there is nothing for one to do. A dispatch over a `filter` that selects nothing contributes nothing: ```aon services: [{ name:web logs: [] }] lines: emit($.services, { match: name: string body: [ "start " + .name emit(filter(.logs, { level:debug }), { match: level: debug body: ["debug on"] }) ] }) ``` ```json {"services": [{"name": "web", "logs": []}], "lines": ["start web"]} ``` **No match is an error** (`emit_none`), naming the patterns that were tried, rather than an empty answer or a copy of the node. A template whose `match` is `any`, written last, is how a document says the rest of the selection was meant to be allowed. ### A named table A table written as an ordinary field is evaluated where it sits, so the relative references in its bodies resolve there and miss. The position that holds a table unevaluated is the one position the language never drives: a call's template argument. Write the table as an `emit` whose **selection is a hole**, and it is a rule set waiting for its nodes: ```aon %wire = emit(_, { match:pin:string body: ["client(" + .pin + ")"] }) listen: [pin:"srv:a"] client: [pin:"srv:b"] a: emit($.listen, %wire) b: $.client & %wire ``` ```json {"listen": [{"pin": "srv:a"}], "client": [{"pin": "srv:b"}], "a": ["client(srv:a)"], "b": ["client(srv:b)"]} ``` Passing the nodes by call and by meet are the same dispatch. A named table is also how one rule set serves two outputs: naming a value is something the language already does, so no keyword is needed for it. ### Recursion, and what bounds it A named table may name **itself**, which is how a rule set walks a nested structure into nested output: ```aon tree: [{ name:a kids: [{ name:b kids: [] }] }] %walk = emit(_, { match: name: string body: ["<" + .name + ">" emit(.kids, %walk) ""] }) out: emit($.tree, %walk) ``` ```json {"tree": [{"name": "a", "kids": [{"name": "b", "kids": []}]}], "out": ["", "", "", ""]} ``` `emit` is the one form here that recurses, and what bounds it is the **selection**: each dispatch descends into a finite bag that already exists in the model, and a selection that empties emits nothing. A rule set that walks into itself without descending is refused as a spent depth budget, like any other runaway descent. Like the other combinators, `emit` waits for the model to settle before it fires: a selection that is still being merged into is the wrong set of nodes to dispatch over. Until it fires it canons as its own call. ### Replacing text in a body: `replace` and `esc` A body line is target text, and a value reaches it through a **`replace`** map rather than a hole: each key is an exact string the body already holds as ordinary text, and its value is evaluated against the matched node. Every value is **escaped** by the template's `esc` convention: the C/JSON escape when the key is absent; `sq` for a single-quoted literal; `sql`, `shell`, `xml`, `uri` or `regex` by name; and `none` for a value that is not going into a literal at all: ```aon services: [{ name:"o'brien" pin:"srv:a" }] lines: emit($.services, { match: name: string esc: sq replace: { NAME:.name PIN:.pin } body: ["seneca.client({type:'sqs',pin:'PIN'})" "await getSeneca('NAME')"] }) ``` ```json {"services": [{"name": "o'brien", "pin": "srv:a"}], "lines": ["seneca.client({type:'sqs',pin:'srv:a'})", "await getSeneca('o\\'brien')"]} ``` There is no delimiter to collide with the target's own syntax, so a deployment template's `${self:provider.stage}` and a backtick string survive untouched. Three rules bound the substitution: a line is scanned once, left to right, taking the longest key at each position; a substituted value is never scanned again, so no value can introduce a key; and a template's replacements touch its own literal lines only: a piece spliced in from a nested dispatch carries that template's replacements and is finished. A number or a boolean value spells itself, as it does after `+`; a map, a list or a null is refused (`replace_value`). Two checks run on the template before any node is visited: a key inside another key is ambiguous whatever the order (`replace_overlap`), and a key the body's literal lines do not hold means the template has drifted from its map (`replace_unused`). ## References and paths A reference resolves to the value at another location, then unifies in place. | Syntax | Meaning | Example | |-----------|------------------------------------------------------|---------| | `$.a.b` | absolute path from the document root | `a:1 b:$.a` → `b:1` | | `.a.b` | path relative to the current map | `z:x:{a:62} z:y:.x.a` → `y:62` | | `$.a.1` | list index: a segment is numeric **only** as a plain decimal integer | `a:[10,20,30] b:$.a.1` → `b:20` | **Numeric segments are plain decimal integers, and nothing else is.** `$.a.1` indexes a list and reaches the key `1`. Every other numeric spelling (hex, `0d`, `_` separators, an exponent) addresses the key spelled **exactly that way**, because that is what the spelling already produces on the key side: `a:{0x0:1}` generates `{"0x0":1}`, not `{"0":1}`, so `$.a.0x0` finds it and `$.a.0` does not. In a path the dot is always the **separator**, never a decimal point. That is why `$.a.1.0` is the two segments `1` and `0` (how a nested list index is written (`a:[[1,2],[3,4]] b:$.a.1.0` → `b:3`)) rather than a key spelled `1.0`. References compose with unification and each other: cross-references, chains, and a referenced map met with extra keys: ```aon a: { x:1 y:$.b.x } b: { x:2 y:$.a.x } c: v: $.d.v d: v: 99 q: a: x: 1 w: b: $.q.a & { y:2 z:3 } ``` ```json {"a": {"x": 1, "y": 2}, "b": {"x": 2, "y": 1}, "c": {"v": 99}, "d": {"v": 99}, "q": {"a": {"x": 1}}, "w": {"b": {"x": 1, "y": 2, "z": 3}}} ``` An unresolvable path is an error: `a:$.nope` → `Cannot resolve value: $.nope`. ### Recursive references (fixpoints) A reference to a value **inside that value** is the fixpoint, not an error. `$.schema.Step` written inside `Step` means "a `Step`, by this very definition", and the schema applies at every depth of the data: ```aon schema: hide({ Step: { label:string then?:$.schema.Step } }) doc: $.schema.Step & { label:"start" then:label:"finish" } ``` ```json {"doc": {"label": "start", "then": {"label": "finish"}}} ``` The recursive position expands **one level per meet with concrete data**, so the checks descend exactly as far as the data does and no further. Data is finite, so evaluation terminates; the depth budget is the backstop (`recursion_budget`). **Guardedness is emergent: the data decides, never a static analysis.** Under an optional key (`then?:`) the chain ends where the data ends. A ranked default works the same way: ```aon schema: hide({ Node: { v:integer next: *null|$.schema.Node } }) doc: $.schema.Node & { v:1 next:v:2 } ``` ```json {"doc": {"v": 1, "next": {"v": 2, "next": null}}} ``` A **required** recursive position that never meets data refuses at generation, at the exact place no finite document can fill: ``` schema: hide({Step: {label: string, then: $.schema.Step}}) doc: $.schema.Step & {label: "start"} → [aontu/recursion_unexpanded]: Cannot recurse value at path $.doc.then ``` In [canonical form](#canonical-form) and the `aon1-` hash the recursion stays **symbolic**: the instance unrolls to its data and then says `$.schema.Step`; the definition stays one reference deep. A recursive schema's canon is finite, reparses to itself, and its hash pins the mu-form: one string for an infinitely deep type: ``` {"doc":{"label":"start","then"?:{"label":"finish","then"?:$.schema.Step}}, "schema":{"Step":{"label":string,"then"?:$.schema.Step}}} ``` Mutual recursion (`A` referencing `B` referencing `A`) works the same way, and so does a recursive [alias](#aliases-), which is enough to write the JSON value space in one line: ```aon %json = null|boolean|number|string|[&: %json]|{ &: %json } x: %json & { a: [1 "two" b:true] } ``` ```json {"x": {"a": [1, "two", {"b": true}]}} ``` [Subsumption](#subsumption) over an unexpanded recursive position answers `undecided` rather than guessing. The degenerate self-reference with no structure at all (`a: $.a`) is a residual that can never expand: its canon is exactly `{"a":$.a}` and generation refuses with `recursion_unexpanded`. A cycle THROUGH other values (`a:$.b b:$.a`) is still `path_cycle`: two references chasing each other name no definition at all. For the recipe form see [Define a recursive schema](/how-to/define-a-recursive-schema); the live version, with its checks, is [use-cases/13-recursive-schema](/use-cases/13-recursive-schema). ## Variables `$name` `$name` (a bare name with no leading dot) is never resolved from the document. The calling program supplies it (see [API reference](/docs/reference-api#variables)). The shared test set binds `foo=11`, `bar="hello"`, `flag=true`, `obj={x:1}`: ``` a:$foo → {"a":11} a:$bar → {"a":"hello"} a:$obj → {"a":{"x":1}} a:$foo & number → {"a":11} (variables unify like values) ``` An unknown variable is a `Cannot resolve` error. ## Aliases `%` An **alias** is a name for a value, written with a leading `%`. `%name = value` at the top level of a file declares one; `%name` in value position uses it. The `=` is the declaration operator, and it is an operator nowhere else: `foo = 1` without the sigil is not a declaration, and neither it nor `a: x=y` is a value: a `=` outside a declaration is punctuation outside its syntax, refused with `[aontu/bare_punct]` (see [Lexical structure](#lexical-structure)). Unlike a [reference](#references-and-paths), which spells a path into the tree, an alias names the value directly and belongs to no path: ```aon %port = integer & min(1) & max(65535) listen: %port listen: 8080 admin: %port admin: 443 ``` ```json { "listen": 8080, "admin": 443 } ``` **The declaration is not part of the document.** It does not generate, it is not a key `close()` counts, and it does not appear in canon, so the file above and the file with `integer & min(1) & max(65535)` written out at both keys are the same document and produce the same [`aon1-` hash](#canonical-form). That is the whole of what an alias is: a name for a value, and nothing else. **An alias key declares a value and creates a field.** `%name: value` is shorthand for `name: %name = value`. The field keeps the value at its written position, and the alias belongs to the file. This form works at the root, inside nested maps, and in list elements. Alias declarations require a map-root document; a root list with declarations is refused with `alias_not_toplevel`. Wrap that list in a field. Quoting the key, `"%name": value`, creates an ordinary key with the sigil in its name. ```aon schema: type({ %row:name:string }) item: %row & { name:example } ``` ```json { "item": { "name": "example" } } ``` **Inside a spread template.** `{&: {a: %D}}` does not resolve the reference when it is written (a template applies to children that have not arrived) so the reference stands in the evaluated document. Canon spells it as the value it names, at any depth: `%u = integer` with `t: {&: %u}` canons as `{"t":{&:integer}}`, and the file produces the same [`aon1-` hash](#canonical-form) as the file with `integer` written in the template. A template that reads its own position, such as `%row = {name: key()}`, canons as the template (`{"name":key()}`), not as what `key()` answered at the declaration. One reference keeps its name: a recursive alias's reference to itself inside its own template (`%json = null | boolean | number | string | [&: %json] | {&: %json}`), which no finite text can write out. Such a document generates and hashes, and its canon is the same in both implementations, but the canon does not reparse on its own. **An alias is not a path segment.** `$.%foo` is refused, at any depth: the alias namespace and the path namespace are disjoint, and an alias is reached by writing `%foo` and only that. **A declaration sits at the root of the document.** A nested `x: { %a = 1 }` is refused: `%a` resolves from the root, so a nested declaration would be erased from the output (it *is* a declaration) and still unreachable by any reference (it is *not* at the root): a name that exists nowhere. Where the declaration *lands* is what decides this, not where it was written, which is what makes the two include shapes differ: - `a: @"./f.aon"` is **refused** if `f.aon` declares an alias. The declaration is at the root of its own file but not of the document, and left writable a `%b` in the *including* file is what `f.aon`'s own `%b` would reach. - `@"./f.aon"` spliced at the root is **accepted**. There is one root map, so there is no second scope for a name to leak out of, and the declaration is a declaration of that one document. **A declaration may also prefix a value**, and written that way it is accepted wherever it sits. `%name = ` in front of any value, to the right of a colon or as a list element, declares the name for the document and leaves the value alone: ```aon x: %a = 1 y: %a ``` ```json { "x": 1, "y": 1 } ``` This is what a key declaration cannot do, and the reason both forms exist. A key leaves a field behind: `%a: 1` names the value and emits `a` beside it. A prefix leaves the document as it was, so a shape can be named exactly where it is used without a key appearing to say so. The value stays where it was written and at its own path, so `x` above is still `1`, and nothing else is added. Either form binds the name for the file rather than for the place it sits, so either resolves from anywhere, and two declarations of one name unify whichever form each was written in and wherever each sat. **A name belongs to the file that declares it.** An include carries a file's *values* across the boundary and never its names, in either direction: an included file cannot see a name the including file declared, the including file cannot see a name the included file declared, and two files that declare one name hold two names that never meet. A reference resolves where it was written rather than where it lands, so a spread template written in one file still names its own file's declaration when it is instantiated against another file's data. A name crosses where both files say so, and nowhere else: the declaring file publishes it with [`export`](#publishing-a-name-export), and the using file asks for it by name with [the destructure](#taking-a-name-the-destructure). **The `%` is part of the name.** A quoted `"%a"` is an ordinary key or string, and a `%` anywhere but on an alias name is refused like any other stray punctuation (`b: 50%` is `[aontu/bare_punct]`; write `"50%"`): ```aon a: "%foo" b: "50%" ``` ```json { "a": "%foo", "b": "50%" } ``` An alias resolves exactly the way a path reference does, which is where its properties come from rather than from rules of its own: - **Order is irrelevant**: a use may precede its declaration. - **An alias may name another alias**, and a cycle is refused. So is a cycle that runs through the document (`%a = $.x` with `x: %a`), because there is one reference graph, not two. - **Two declarations of one name unify**, exactly as two statements for one key do: `%n = 1` with `%n = integer` is `1`, and `%n = 1` with `%n = 2` is a conflict. - **A use of an undeclared name is refused**, naming the name. **Expansion is bounded by size.** A name built from other aliases expands to the product of what they hold, so a file that fits on a screen can describe a document that does not fit in memory: twenty declarations of the shape `%a20 = [%a19, %a19]` reach a million nodes. The expanded size is counted before evaluation and refused over `trust.budget.alias` with the code `alias_budget`. Expansion always terminates, whatever the budget, because an alias takes no parameters, a cycle is refused, and a file declares finitely many names: the bound is about size alone, so raising it is the repair where the document is meant and the machine can hold the result. Aliases are not passed to generated children: a spread template sees the *expansion*, so children are constrained by the value and acquire no name. ```aon %row = { kind:string id:integer } table: { &: %row a: { kind:user id:1 } b: { kind:user id:2 } } ``` ```json { "table": { "a": { "kind": "user", "id": 1 }, "b": { "kind": "user", "id": 2 } } } ``` ### What a finding says about a name A value that arrives through a name has two places: where the source writes it, and where the document asks for it. A finding names both. The frames give the value, what it met, and the reference that carried it: ```sh $ aontu conflict.aon [aontu/scalar_value]: Cannot unify values at path $.a ... Cannot unify value: 1 with value: 2 --> conflict.aon:1:6 1 | %p = 1 ^ value was: 1 Value arrived through %p --> conflict.aon:2:4 2 | a: %p ^ used %p here ``` A value that arrives by an ordinary path reference has no name to blame, so a finding adds no such frame. ### The shorthand: `{ %a %b }` In value position a set of names stands for the map that binds each one under its own name: `{ %a %b }` is `{ a: %a, b: %b }`, key without the sigil and value with it. ```aon %kind = "user" %limit = 10 defaults: { %kind %limit } ``` ```json {"defaults":{"kind":"user","limit":10}} ``` The sigil is what makes the sugar unambiguous, so `{ a, b }` stays the parse error it has always been, and a set may still separate its names with commas. Canon expands the shorthand, so a document written short and the same document written long are one `aon1-` digest, which is what makes this sugar rather than a second way to say something else. A rename needs no shorthand, because `a: %b` already spells it. ### Publishing a name: `export` `export({ %a, %b })` declares which of a file's names another file may take. It is a declaration and not a value, so a file generates the same document with it as without it: ```aon %port = integer & min(1) & max(65535) export({ %port }) listen: %port listen: 8080 ``` ```json { "listen": 8080 } ``` It takes a set of alias names and nothing else. Every other argument is refused with `export_arg`: `export({ port })` names a key, which already crosses the boundary as a value; `export(%port)` names an alias but not a set; and `export({%})` is the wildcard, which belongs on the taking side. A name a file declares and does not export stays that file's own. ### Taking a name: the destructure `{ %a } = @"./f.aon"` places `f.aon`'s values exactly as `@"./f.aon"` places them, and also binds `%a` in the taking file's scope. There is no `import` verb: the include already crosses the boundary for values, and the pattern on its left crosses it for names. Write the publishing file as `types.aon`: ```aon %uint8 = integer & min(0) & max(255) export({ %uint8 }) defaults: retries: 3 ``` and take its name from `main.aon`: ```aon { %uint8 } = @"./types.aon" level: %uint8 level: 200 ``` ```sh $ aontu -c main.aon {"defaults":{"retries":3},"level":200} ``` The file's values arrive whether or not a name is asked for, which is what makes the pattern additive rather than a filter. `{%}` takes every name the other file exports, and only those: the publishing file chose the set. Asking for a name that file does not export is refused with `import_not_exported`, which names the name; the destructure asked, so the refusal stands whether or not anything goes on to use the name. A name that arrives this way meets a local declaration of the same name rather than replacing it, exactly as two declarations in one file meet. **A file publishes what it declares.** A name that merely arrived in a file through an include belongs to the file that wrote it, so re-exporting it is refused: publishing someone else's private name is not a file's to do. **Rename what you take with `%local: %remote`.** Both sides carry the sigil, because both are names; the left is what this file calls it and the right is what the other file publishes. Two files publishing one name is the case it answers, and nothing else does. **A destructure may also sit under a key.** The values land where the head stands and the names it binds are the document's, so a file can be mounted at a path and still be taken from. Both forms read the same `types.aon`: ```aon %uint8 = integer & min(0) & max(255) export({ %uint8 }) defaults: retries: 3 ``` `rename.aon` takes `%uint8` under a name of its own: ```aon { %port: %uint8 } = @"./types.aon" listen: %port listen: 200 ``` ```sh $ aontu -c rename.aon {"defaults":{"retries":3},"listen":200} ``` and `mount.aon` puts the same file's values under `svc`: ```aon svc: { %uint8 } = @"./types.aon" level: %uint8 level: 200 ``` ```sh $ aontu -c mount.aon {"level":200,"svc":{"defaults":{"retries":3}}} ``` The other file's own declarations come up to the document root with its values. Without that a file that uses the name it publishes could not be mounted at all, since an alias resolves from the root and its declaration would have landed under the key. **A wrapped root still publishes.** `open(...)` and `copy(...)` hold the document as their one argument, so such a file publishes what its map declares. One limit: the wrapper's argument may not use the name the file publishes, because the declaration rises to the taking document's root, out of the argument's reach. That include fails with `conjunct`. `export` does not rename: a file publishes what it has, and the taking file renames what it takes, so `export({ %a: %b })` is refused with `export_arg`. ## The `+` operator and grouping `+` adds numbers and concatenates strings; it chains left-to-right. Parentheses group sub-expressions and a leading unary `+` is allowed. ```aon a: 1 + 2 b: 1 + 2 + 3 c: 1.5 + 2 d: p + q e: p + q + r f: (1 + 2) g: ( + 3 + 4) h: i: j: 10 + 5 ``` ```json {"a":3,"b":6,"c":3.5,"d":"pq","e":"pqr","f":3,"g":7,"h":{"i":{"j":15}}} ``` **Result kind: the exact ladder.** `+` never introduces a kind narrower than its operands, and it never demotes. The three exact leaves form a ladder, ``` integer < biginteger < bigdecimal ``` and a sum of exact operands takes the **widest** leaf present and is computed exactly. `float` is not on that ladder: it keeps its classic contagion with `integer` alone. ``` x:1+2 → integer 3 canon {"x":3} x:1+2.0 → float 3 canon {"x":3.0} x:1.5+1.5 → float 3 canon {"x":3.0} x:1+0d2 → biginteger 3 canon {"x":0d3} x:0d2+0d3 → biginteger 5 canon {"x":0d5} x:1+0d0.5 → bigdecimal 1.5 canon {"x":0d1.5} x:0d2+0d0.5 → bigdecimal 2.5 canon {"x":0d2.5} x:(1+2) & integer → {"x":3} x:(1.5+1.5) & integer → error (the sum is float kind) x:(1+0d2) & integer → error (the sum is a biginteger) ``` The widest operand anywhere in a chain decides, whichever end it arrives at: `x:1+2+0d3` → `0d6`. A `*`-preferred operand contributes its preferred value's kind. Results never demote, so a biginteger sum that would fit an `integer` stays a biginteger, and an integral bigdecimal sum stays a bigdecimal: `x:(0d0.5+0d0.5)&0d1.0` is `0d1.0`, while `& 0d1` is a conflict. **Exact arithmetic is exact.** Adding bigdecimals aligns the scales and adds; nothing is rounded and no precision context is consulted, so the answers are the ones decimal arithmetic gives on paper: ``` x:0d0.1+0d0.2 → {"x":0d0.3} (binary64: 0.30000000000000004) x:0d0.1+0d0.2+0d0.3 → {"x":0d0.6} (binary64: 0.6000000000000001) x:0d1.23+0d4.567 → {"x":0d5.797} ``` The same sums, run through the CLI: ```sh $ echo 'x: 0d0.1 + 0d0.2' | aontu { "x": 0.3 } $ echo 'x: 0d0.1 + 0d0.2 + 0d0.3' | aontu { "x": 0.6 } ``` A sum too wide to hold is refused, never approximated: see [the exactness budget](#the-exactness-budget). **Float and exact never mix.** An exact value never silently becomes a binary float, in either operand order. There is no promotion for this pair; it is a hard error. ``` x:1.0+0d2 → error (a float and a biginteger cannot mix) x:0d0.5+1.0 → error (the same refusal, operands the other way round) ``` Parentheses only decide *where* the refusal happens: `x:(1+0d2)+1.0` and `x:(1+2.0)+0d3` both fail. **Integer sums are exact too.** `integer + integer` is computed exactly, and the answer must then satisfy the same storage contract its operands did: integral, inside the int64 window, *and* exactly representable as a double. A sum that fails any of the three is a located error naming the `0d` escape, rather than a rounded value: ``` x:4503599627370496+4503599627370496 → {"x":9007199254740992} (2^53) x:9007199254740992+2 → {"x":9007199254740994} x:9007199254740992+1 → error: … not exactly representable x:9007199254740992+0d1 → {"x":0d9007199254740993} (the escape) x:4611686018427387904+4611686018427387904 → error (2^63, past int64) ``` **String concatenation renders digits, not kinds.** A `+` with a string operand concatenates, and the numeric side contributes its plain digits with **no `0d` marker**: the marker is canon decoration, and it never leaks into a string. ```aon a: q + 0d5 b: q + 0d0.1 c: 0d5 + q d: q + 0d1e3 e: q + 0d1000 ``` ```json {"a":"q5","b":"q0.1","c":"5q","d":"q1000.0","e":"q1000"} ``` The digits are the value's own rendering minus the marker, so the integral bigdecimal at `d` keeps its one decimal place while the biginteger at `e` does not. The plain family is unchanged and still coerces with JavaScript rules, which drop a trailing `.0`: `x:a+1.0` → `"a1"`, not `"a1.0"`. **Two lists concatenate.** A `+` whose operands are both lists answers one list: the left's elements, then the right's, each cloned into its new index. An empty operand contributes nothing. This is how a document assembles a list from a written head and a computed tail: ```aon a: [1] + [2] b: [] + [2] c: ["x"] + each(["y"], _) ``` ```json {"a":[1,2],"b":[2],"c":["x","y"]} ``` A list with a scalar is not a sum and is refused, in either order. **A sum of an absence is absent.** `maybe()` travels through `+` the way it travels through a call, on either side and whatever the other operand is, so an optional tail needs no guard: ```sh $ echo 'a: 1 b: [1] + maybe($.gone) c: "x" + maybe($.gone)' | aontu { "a": 1 } ``` See [Optional input: `maybe`](#optional-input-maybe). Unary `-` negates a numeric operand exactly. It binds tighter than `+`, `&` and `|` (`-1 & integer` is `(-1) & integer`) and, like `+`, never narrows the kind and never yields `-0`. ## Functions aontu provides a fixed set of built-in functions. There are no user-defined functions. This alphabetical index lists every built-in; the links lead to its detailed behaviour and examples. The argument modes describe how a call uses its arguments: `template` is instantiated for a selected value, `trial` supplies a condition, `projector` names a field or index, `capture` preserves a path's spelling, and `text` supplies literal text. An unmarked argument supplies a value. For collection operations, compare [pack and each](#generating-children-pack-and-each), [the `_ & …` idiom](#the-_---idiom-construction-and-bound), [filter and match](#selecting-filter-and-match), [pick](#projecting-fields-pick), and [emit](#transforming-emit). `pack` and `each` construct collections; `filter` selects members; `pick` projects a field; `emit` applies a rule table and flattens its output. ### `abnf(g: string) : string` Compile an RFC 5234 ABNF grammar and answer its source, so a parser is an ordinary string. A grammar that does not compile is refused here, once, rather than at every site that parses with it. See [grammars](#grammars-abnf-and-parse). Example: `G: abnf("v = 1*DIGIT")` ### `above(n: number|string) : constraint` Constrain a numeric or string value to be strictly greater than a bound. See [bounds](#the-constraint-algebra). Example: `integer & above(0)` ### `acyclic() : constraint` Require the edges of a declared relation to contain no cycle. See [declared relations](#declared-relations). Example: `rel() & acyclic()` ### `add(a: number, b: number) : number` Add two numbers under the [number-tower rules](#arithmetic-add-sub-mul-div-mod-rem). Example: `add(2, 3)` → `5` ### `below(n: number|string) : constraint` Constrain a numeric or string value to be strictly less than a bound. See [bounds](#the-constraint-algebra). Example: `integer & below(10)` ### `close(m: any) : any` Seal a map/list against extra keys. Example: see [closed values](#closed-values-close--open) ### `content(spec: string|map) : map` A text node of the [component tree](#generation): a span of target text, added with no newline of its own, which is the whole difference from `line`. A bare string fills `src`, and an empty span is a value rather than a mistake. Example: `content("export const N = 1\n")` ### `copy(v: any) : any` Deep copy of a value or referenced node; clears `type`/`hide` marks. Example: `copy({a:1,b:2})`→`{a:1,b:2}`; `copy($.x)` ### `copyfiles(spec: string|map) : map` A copy node of the [component tree](#generation): files copied verbatim from `from` into the output. Named `copyfiles` because `copy` already copies a VALUE. Example: `copyfiles("assets")` ### `deprecate(v: any, r?: map) : any` Mark `x` deprecated; unifies exactly as `x`, and the record `m` (`{msg?, use?, since?}`, all strings; `use` is a path spelled as a string) rides the result through meets, reference clones and spread applications. Canon renders the call back; generation is unchanged. The point-of-use surfaces: a vet `deprecated` warning, the LSP Deprecated tag, and `aontu breaking --allow-deprecated-removal`. Example: `port: deprecate(*8080|integer, {msg:"renamed", use:"$.listen", since:"2.0.0"})` ### `div(a: number, b: number) : number` Divide two numbers; integer division truncates towards zero. See [arithmetic and refusals](#arithmetic-add-sub-mul-div-mod-rem). Example: `div(7, 2)` → `3` ### `each(d: map|list, template t: any) : list` Construct one list element per source child by instantiating a template with `_` bound to that child. See [form](#each-the-order-preserving-map). Example: `each([a, b], upper(_))` → `["A", "B"]` ### `emit(s: map|list, template t: map|list) : list` One flat list of pieces from a selection and a rule table: for each node, the first template whose `match` it already satisfies, its `body` instantiated at that node. See [Transforming](#transforming-emit). Example: `lines: emit($.services, {match:{pin:string}, body:[.pin]})` ### `esc(s: string, variant?: string) : string` Escape a string using a named convention; the default is JSON-style double-quoted text. See [escaping](#escs-variant-and-uscs-variant). Example: `esc("", xml)` ### `file(spec: string|map, children?: list) : map` A file node of the [component tree](#generation), named by `name` and holding content, lines, fragments, injections, and copies: one file of the output. Wherever `line` is admitted a bare string stands for it, which is what a template body line becomes. Example: `file("index.ts", ["export {}\n"])` ### `filter(d: map|list, trial c: any) : map|list` The children of `d` that ALREADY satisfy `c`: the meet with `c` changes nothing. Keys kept for a map, order for a list; the rest are dropped, not refused. See [Selecting](#selecting-filter-and-match). Example: `debugged: filter($.services, {debug:true})` ### `folder(spec: string|map, children?: list) : map` A folder node of the [component tree](#generation), named by `name` and holding folders, files, and copies: one directory of the output. Example: `folder("src", [file("index.ts")])` ### `fragment(spec: string|map, children?: list) : map` A fragment node of the [component tree](#generation): a file read from `from`, reaching the output with its `<[SLOT]>` markers filled by the slots beneath it. Example: `fragment("head.ts", [slot("body")])` ### `greatest(d: map|list) : number` Return the greatest numeric member, preserving its kind. An empty collection is refused. See [aggregates](#aggregating-sum-least-greatest). Example: `greatest([2, 7, 4])` → `7` ### `hide(v: any) : any` Mark `x` as hidden. Example: `hide(world) & string`→`"world"` ### `inject(spec: string|map, children?: list) : map` An injection node of the [component tree](#generation): a body written between markers in an output file that already exists. Example: `inject("routes", [line("app.use(r)")])` ### `inverse(projector k: string) : constraint` Require every edge of a declared relation to have a corresponding edge under the named inverse. See [declared relations](#declared-relations). Example: `rel() & inverse(usedBy)` ### `join(d: map|list, sep?: string) : string` Join collection members as text, with an optional separator. See [join](#folding-to-a-string-join). Example: `join([a, b], ", ")` → `"a, b"` ### `key(up?: integer|biginteger) : string` The ancestor key `n` levels up (`0` = own key, default `1` = parent). `n` must be an **integer** (`integer` or `biginteger`); anything else is an error. A level beyond the top of the path yields `""`. Example: at `a:b:c`: `key()`→`"b"`, `key(0)`→`"c"`, `key(2)`→`"a"`, `key(2.0)`→error ### `least(d: map|list) : number` Return the least numeric member, preserving its kind. An empty collection is refused. See [aggregates](#aggregating-sum-least-greatest). Example: `least([2, 7, 4])` → `2` ### `length(n: number|constraint) : constraint` Constrain a string length or collection size. See [length semantics](#length-semantics). Example: `list() & length(min(1))` ### `line(spec: string|map) : map` A text node of the [component tree](#generation): a span of target text with a newline added, which is the whole difference from `content`. An empty span is a blank line. Example: `line("import fs from 'fs'")`; `line("")` is a blank line ### `list() : list` The list **kind**: admits any list, defaults to nothing. Example: `y: list() & [1]`→`[1]` ### `listitems(spec: map, children?: list) : map` A repetition node of the [component tree](#generation), over the list at `item`: its children are written once for each member. The bag is required and must be a list: a missing one would render nothing, silently. Example: `listitems({item: $.rows}, [line("x")])` ### `lower(s: string|number, start?: integer|biginteger, len?: integer|biginteger) : string` Lowercase a string, or a run of it; **floor** of a number, keeping the argument's kind. The range is `upper`'s; see [`upper`](#uppers-stringnumber-start-integerbiginteger-len-integerbiginteger--string). Example: `lower(ABC)`→`"abc"`, `lower("FOO",1,-1)`→`"Foo"`, `lower("FOOBAR",-3,-1)`→`"fooBAR"`, `lower(1.9)`→ float `1` ### `map() : map` The map **kind**: admits any map, defaults to nothing. See [Container kinds](#container-kinds-map-and-list). Example: `y: map() & {a:1}`→`{a:1}`; `y: map()`→ error ### `match(s: any, ...pr: (trial any, any), dflt?: any) : any` The result of the first pattern `v` already satisfies; a trailing argument is the default. No match and no default is an error naming the patterns tried. Example: `size: match($.tier, small, {cpu:1}, {cpu:2})` ### `max(n: number|string) : constraint` Constrain a numeric or string value to be at most the bound. See [bounds](#the-constraint-algebra). Example: `integer & max(10)` ### `maybe(v: any) : any` The value when it resolves, and **absence** when the only thing wrong is that it is not there. See [Optional input](#optional-input-maybe). Example: `maybe($.gone)` generates nothing; `maybe($.here)` is `$.here` ### `min(n: number|string) : constraint` Constrain a numeric or string value to be at least the bound. See [bounds](#the-constraint-algebra). Example: `integer & min(0)` ### `mod(a: number, b: number) : number` Compute a modulo whose nonzero result follows the divisor's sign. See [arithmetic](#arithmetic-add-sub-mul-div-mod-rem). Example: `mod(-7, 3)` → `2` ### `move(v: any) : any` Resolve reference `p`, dropping unresolved optional keys. Example: `m:{x?:number,y:Y} n:move($.m)`→`n:{y:"Y"}` ### `mul(a: number, b: number) : number` Multiply two numbers under the [number-tower rules](#arithmetic-add-sub-mul-div-mod-rem). Example: `mul(2, 3)` → `6` ### `must(trial c: any, text msg: string) : constraint` Apply an evaluation-time condition with an author-supplied failure message. See [must](#band-b-must). Example: `must(min(1), "must be positive")` ### `neq(...vals: number|string) : constraint` Exclude the listed numeric or string values. See [constraint atoms](#the-constraint-algebra). Example: `string & neq("reserved")` ### `nom(name: string, style?: string|list, acronyms?: list) : string|map` One name in one spelling, or every spelling as a map when no style is named: `camel`, `dot`, `kebab`, `pascal`, `path`, `snake`, `text`, `title` and `upper`. An acronym list keeps `id` as `ID`. Example: `nom("planet_body", pascal)` → `"PlanetBody"` ### `open(m: any) : any` Reverse a `close`. Example: `open(close({x:1})) & {y:2}`→`{x:1,y:2}` ### `pack(d: map|list, template t: any) : map` One keyed child per child of `d`, each of them `t` cloned at that destination. Keys are the strings of a list, or the keys of a map. See [Generating children](#generating-children-pack-and-each). Example: `deploy: pack($.names, {replicas:*2|integer})` ### `parse(g: string, v?: string) : map|list|constraint` Parse a string under a grammar and answer what the grammar says it builds: the syntax tree, or the map or list a **value annotation** asks for. With no value, the grammar as a **constraint** on whatever meets it, answering that value unchanged. A failure to parse is a failure to unify. See [grammars](#grammars-abnf-and-parse). Example: `parse($.G, "12")` → `{rule:"v" src:"12" kids:[...]}`; `*"" | parse($.G)` ### `path(capture p?: path) : path` **capture** `p` as a path value: the spelling, never the resolution; with no argument, the path **kind**. See [First-class paths](#first-class-paths-pathp). Example: `dep: path(.auth)` generates `".auth"`; `host: path()` ### `pick(d: map|list, projector k: string|integer) : any` Project one field or index from every collection member into a list. See [pick](#projecting-fields-pick). Example: `pick([{n:a}, {n:b}], n)` → `["a", "b"]` ### `pref(v: any) : any` Mark `x` as preferred (same as `*x`). Example: `pref(1)` canon `*1`; `pref(2),x:3`→`3` ### `project(spec?: string|map, children?: list) : map` The root node of the [component tree](#generation); its `folder` is the output directory and is the one prop that is not required. Example: `project("./build", [folder("src")])` ### `re(text p: string) : constraint` Constrain a string to match a portable regular expression. See [patterns](#re-and-the-portable-pattern-subset). Example: `string & re("^[a-z]+$")` ### `refer(template t?: any) : constraint` Constrain a field to a **path value whose address resolves**; `t`, if given, is unified into the target. The field keeps the address. See [Checked links](#checked-links-refert). Example: `dependsOn: [&: refer($.aontu.System.Service), path($.services.auth)]` ### `rel(template t?: any) : constraint` Declare a field as a relation and optionally constrain its targets. See [declared relations](#declared-relations). Example: `dependsOn: rel() & [path($.auth)]` ### `rem(a: number, b: number) : number` Compute the remainder of truncating division. See [arithmetic](#arithmetic-add-sub-mul-div-mod-rem). Example: `rem(-7, 3)` → `-1` ### `rep(s: string, text p: string, text sub: string) : string` Replace every pattern match in a string. See [replacement syntax](#reps-pattern-sub). Example: `rep("a1b2", "[0-9]", "_")` ### `slot(spec: string|map, children?: list) : map` A slot node of the [component tree](#generation), beneath a fragment: the body that fills the marker of that name. Example: `slot("body", [line("return 1")])` ### `sort(d: map|list, projector k?: string|integer, dir?: string) : list` Order a collection's members into a list, by a projected field or by the members themselves. See [Ordering](#ordering-sort). Example: `sort([3, 1, 2])` → `[1, 2, 3]` ### `split(s: string, sep: string|constraint) : list` Split a string using a literal separator or a pattern constraint. See [split](#splits-sep). Example: `split("a,b", ",")` → `["a", "b"]` ### `sub(a: number, b: number) : number` Subtract the second number from the first. See [arithmetic](#arithmetic-add-sub-mul-div-mod-rem). Example: `sub(7, 2)` → `5` ### `sum(d: map|list) : number` Add the numeric members of a collection; an empty collection sums to zero. See [aggregates](#aggregating-sum-least-greatest). Example: `sum([2, 3])` → `5` ### `super(t: any) : any` The immediate parent type of `x`, structurally: a scalar's kind, a kind's parent, a container of its children's parents. Example: `super(1)` → `integer`, `super(integer)` → `number`, `super({a:1})` → `{a:integer}` ### `translate(s: string, from: string, to?: string) : string` Map the characters of `s` from one set to another. A range expands (`a-z`), a short `to` pads with its last character, and an omitted `to` deletes every character named in `from`. Example: `translate("a-b-c", "-", "_")` → `"a_b_c"` ### `type(t: any) : any` Mark `x` as a type/schema value. Example: `type(1) & number`→`1` ### `unique(projector k?: string) : constraint` Require distinct members, optionally comparing a named field. See [unique semantics](#unique-semantics). Example: `list() & unique(id)` ### `upper(s: string|number, start?: integer|biginteger, len?: integer|biginteger) : string` Uppercase a string, or a run of it; **ceiling** of a number, keeping the argument's kind. `start` is a boundary. Zero or positive, the run begins there and reaches forward; negative, it counts from the end and the run stops there, the character it lands on being the first one left alone. `len` is how many characters; `-1`, which is also the default, is the whole source. Both ends clamp, so a run past either end does as much as exists. Indices are code points. A range on a number is refused. Example: `upper(abc)`→`"ABC"`, `upper("foo",0,1)`→`"Foo"`, `upper("foo",1)`→`"fOO"`, `upper("foo",-1,2)`→`"FOo"`, `upper(1.1)`→ float `2` ### `usc(s: string, variant?: string) : string` Decode text escaped with the named convention, refusing malformed input. See [escaping](#escs-variant-and-uscs-variant). Example: `usc(esc("", xml), xml)` → `""` ### Parent types `super(x)` answers the immediate parent type of its **argument**. For a concrete scalar that is the scalar's kind, and for a kind it is the kind's own parent: `number` sits above the four numeric leaves, so the numeric ladder has a real middle rung. For structured arguments, `super` descends: a map lifts to the map of its values' parents (key optionality, closedness and any `&:` spread carried over, the spread template lifted), a list lifts element by element, a preference lifts to its value's parent, a disjunction lifts arm by arm, and a constraint lifts to the kind it constrains: its absorbed leaf kind when it has one, otherwise the domain its atoms compare in. ```sh $ echo 'a: super(1) b: super(1.5) c: super(integer) d: super(number)' | aontu -c {"a":integer,"b":float,"c":number,"d":top} $ echo 'e: super({port: 8080, name?: web}) f: super([1, on])' | aontu -c {"e":{"name"?:string,"port":integer},"f":[integer,string]} $ echo 'g: super(*8080) h: super(1|2) i: super(min(3)) j: super(integer & min(3))' | aontu -c {"g":integer,"h":integer,"i":number,"j":integer} $ echo $? 0 ``` The result is a type, so it constrains: lifting an example produces a schema the example itself satisfies: ```sh $ echo 'x: super({a:1}) & {a: 7}' | aontu { "x": { "a": 7 } } $ echo 'x: super({a:1}) & {a: 7.5}' | aontu [aontu/no_scalar_unify]: Cannot unify values at path $.x.a ... $ echo $? 1 ``` The answer is `top` only where `top` is the immediate parent: the root kinds (`number`, `string`, `boolean`), `top` itself, a disjunction with an arm that lifts to `top`, and a constraint that admits several container kinds (`length(n)` constrains strings, lists and maps alike). Two edges are pinned in `test/spec/super.tsv`: a recursion residual met by `super` stays a symbolic call (the finite spelling of a lift that is itself recursive) which generation refuses like any unresolved call, and `super(null)` answers the null kind, which canon prints as `null`, the same spelling as the value. ### Rounding numbers `upper()` and `lower()` round a number without narrowing it: the result carries the *argument's* kind, so `upper(2)` is an integer `2` (and unifies with `integer`) while `upper(1.1)` is a float `2` (and does not). On the exact leaves they are exact ceiling and floor: no binary arithmetic is involved, and the kind still survives: ``` x:upper(0d1.1) → {"x":0d2.0} x:upper(-0d1.5) → {"x":-0d1.0} x:lower(0d1.9) → {"x":0d1.0} x:lower(-0d1.5) → {"x":-0d2.0} x:upper(0d5) → {"x":0d5} (a biginteger is already integral) x:upper(0d1.1) & bigdecimal → {"x":0d2.0} x:upper(0d1.1) & biginteger → error (rounding does not change the leaf) ``` A bigdecimal result is still a bigdecimal, so it keeps the one decimal place its leaf always renders, even when the value is whole. ### Composing calls Functions compose with operators, references, list elements, and the preference mark: ```aon a: upper(abc) + def b: lower(1.1) + 2 c: foo d: upper($.c) e: [lower(A) lower(B)] f: *upper(foo) ``` ```json {"a":"ABCdef","b":3,"c":"foo","d":"FOO","e":["a","b"],"f":"FOO"} ``` ## Arithmetic: `add` `sub` `mul` `div` `mod` `rem` Maths beyond `+` is spelled with **functions**. The tokens `-` `*` `/` `%` stay reserved for the language's own use, so there is no infix arithmetic to learn beyond `+` and unary `-`: ```aon replicas: mul($.base.replicas, 2) spare: sub($.quota.cpu, $.used.cpu) shards: div($.total, $.per_shard) ``` Each takes exactly **two operands**, and both must be numbers. That is what distinguishes `add` from `+`: the operator is polymorphic and will happily concatenate, so a Kubernetes quantity written `"500m" + "500m"` is the string `"500m500m"` and nothing complains. `add("500m","500m")` is an error, because a function named for a numeric operation has no business inventing a string. ```aon a: add(1, 2) b: sub(10, 3) c: mul(6, 7) ``` ```json {"a":3,"b":7,"c":42} ``` A non-number operand is an `invalid-arg` error whatever its shape: `add("a","b")`, `add(true,1)` and `sub(integer,1)` are all refused. **Kind follows the operands** (R5, and the same [exact ladder](#the-four-numeric-leaves) `+` uses): integer with integer is an integer, anything with a float is a float, and a mixed exact operation promotes to the widest leaf and never demotes. ``` x:mul(2,3) → {"x":6} integer x:mul(2,1.5) → {"x":3.0} float — never narrowed to integer 3 x:add(1,0d2) → {"x":0d3} biginteger, the wider operand x:mul(2,0d1.5) → {"x":0d3.0} bigdecimal x:add(1.0,0d2) → error, exact_float_mix — as with `+` ``` **Integer division truncates toward zero**, and `rem` and `mod` differ only in whose sign the answer follows: `rem`'s the dividend's, `mod`'s the divisor's. That is the whole reason both exist: ```aon a: div(7, 2) b: div(-7, 2) c: rem(-7, 2) d: mod(-7, 2) e: rem(7, -2) f: mod(7, -2) ``` ```json {"a":3,"b":-3,"c":-1,"d":1,"e":1,"f":-1} ``` `b` is `-3`, not `-4`: truncation, not flooring. Three things are refused rather than answered, each because the answer would be a value aontu cannot carry: - **A zero divisor**, in every leaf including floats. A JSON superset has no notation for an infinity, so there is nothing `div(7,0)` could return (`divide_by_zero`). - **A non-finite float result**: `mul(1.0e200,1.0e200)` overflows binary64 (`float_overflow`). The same check governs `+`. - **`div`, `mod` or `rem` over a bigdecimal.** One third has no finite decimal form, so exact decimal division either rounds (the one thing that leaf exists to prevent) or refuses (`inexact_divide`). Scale to integers first, which is how money should be carried anyway (minor units as an integer), or use floats if an approximation is acceptable. Note `0d10` is a *biginteger*, not a decimal, so `div(0d10,0d4)` is `0d2`; it is `0d10.0` that is refused. An exact result that will not store is refused too, exactly as a sum is (`inexact_integer_sum`): `mul(4503599627370496,4503599627370496)` is an error rather than a rounded answer, and `0d` operands compute it exactly. ## Projecting fields: `pick` `pick(data, key)` returns a list containing the named field from each member of a map or list. Use it to turn records into the values that an aggregate or a string join needs: ```aon lines: [amountCents:1200 amountCents:450] amounts: pick($.lines, amountCents) total: sum($.amounts) ``` ```json {"amounts":[1200,450],"lines":[{"amountCents":1200},{"amountCents":450}],"total":1650} ``` `amountCents` is a field name supplied to the projector argument. The bare word and the quoted string `"amountCents"` name the same key. The result preserves each selected value's kind and structure; picking a map-valued field returns that map as one element, without flattening it. ### Order and list indexes A list is visited in source order. A map is visited in sorted-key order, and its keys do not appear in the resulting list. For members that are lists, supply a zero-based integer index: ```aon records: { z:name:last a:name:first } names: pick($.records, name) first: pick([[9 8] [7 6]], 0) empty: pick([], name) ``` ```json {"empty":[],"first":[9,7],"names":["first","last"],"records":{"a":{"name":"first"},"z":{"name":"last"}}} ``` The empty collection returns an empty list. As with `each`, hidden or type-marked collection members and unfilled optional members are skipped. This selection happens before `pick` reads the requested field. ### Missing fields and invalid arguments Every selected member must contain the requested field or index. A missing key, an out-of-range index, or a scalar member is `pick_key`. The call refuses the projection instead of returning a shorter list: ```sh $ echo 'x: pick([{a:1}, {b:2}], a)' | aontu [aontu/pick_key]: Cannot pick value at path $.x ... $ echo $? 1 ``` A non-collection input is `aggregate_data`. The key must be a string name or an `integer` index; a float such as `0.0`, a kind, or a list is `invalid-arg`. A missing argument is `func_arity`. A projector names one key, not a dotted path expression. Project twice to select through two levels: ```aon records: [address:city:Dublin address:city:Cork] cities: pick(pick($.records, address), city) ``` ```json {"cities":["Dublin","Cork"],"records":[{"address":{"city":"Dublin"}},{"address":{"city":"Cork"}}]} ``` ### Choose projection or construction Use `pick(records, name)` to extract a field. Use [`each`](#each-the-order-preserving-map) when each output element needs an expression or a new structure. The bound spelling `each(records, _ & t)` unifies each source member with a template; it preserves that member's information rather than extracting one field from it. Compose the resulting list with [sum](#aggregating-sum-least-greatest) for a total or [join](#folding-to-a-string-join) for a line of text. ## Optional input: `maybe` A path that names nothing is `no_path`, and that is right: a typo should be loud. It leaves a document that reads **optional** input with nothing to say, though, because the miss refuses the whole call. `maybe(v)` is the value when it resolves, and **absence** when the only thing wrong is that it is not there. **Absence generates nothing**, at a required key as readily as at an optional one, and from a list without leaving a hole. That is the whole difference from `top`, which is not generable and refuses with `mapval_no_gen`. ```sh $ echo 'a: 1 b: maybe($.gone) c: [1, maybe($.gone), 2]' | aontu { "a": 1, "c": [ 1, 2 ] } ``` **A call on an absent argument is no call.** Absence travels through every built-in, in any argument position, and through [`+`](#the--operator-and-grouping), so a transform written against optional input needs no guard around it. ```sh $ echo 'a: 1 b: each(maybe($.tags), {t:_}) c: join(maybe($.tags), "-")' | aontu -c {"a":1,"b":maybe(),"c":maybe()} ``` **Absence is the unit of `&`**, on either side, so meeting it with a constraint leaves the constraint: ```sh $ echo 'a: 1 & maybe($.gone) b: maybe($.gone) & 2' | aontu -c {"a":1,"b":2} ``` **Only a missing referent is forgiven.** A conflict inside the argument is the document's own bug and is reported where it happened, not swallowed: ```sh $ echo 'b: maybe(1 & 2)' | aontu [aontu/scalar_value]: Cannot unify values at path $.b ... $ echo $? 1 ``` **It waits for the model.** A reference that has not resolved yet is not a reference to nothing, so `maybe` fires only once the document has settled, the way [`each`](#each-the-order-preserving-map) and [`pack`](#generating-children-pack-and-each) do. A forward reference therefore answers the value: ```sh $ echo 'b: maybe($.x) x: 1' | aontu -c {"b":1,"x":1} ``` **It cannot make a containing map vanish.** Absence travels through a call and out of a list element, not out of a map that still has other keys: `{k:"frag", n: emit(maybe($.tags), t)}` drops `n` and keeps a `{k:"frag"}` behind. Write the whole element as the optional thing, not one of its fields. **A constrained list refuses it.** Absence leaves a plain list without a hole, but a list carrying a spread meets every element against the spread's template, and absence is not a member that template admits: ```sh $ echo 'x: ["a", maybe($.gone)]' | aontu -c {"x":["a",maybe()]} $ echo 'x: [&: string] x: ["a", maybe($.gone)]' | aontu [aontu/listval_no_gen]: Cannot resolve value at path $.x.1 ... $ echo $? 1 ``` So an optional member of a list a schema constrains is written as an optional KEY of the map that holds it, or the spread is dropped from the list. ## Ordering: `sort` Generation supplies two orders, and neither is the one a report or a rendered file wants: a map generates in **sorted-key** order and a list in **source** order. `sort(data)` is the third. **It answers a list, from either container.** A map has no order of its own to be put in, which is the reason `Semver` is a list as well. ```sh $ echo 'a: sort([3, 1, 2]) b: sort({x:"c", y:"a"})' | aontu -c {"a":[1,2,3],"b":["a","c"]} ``` **The second argument projects**, exactly as `pick`'s does: a key name for a map member, an index for a list member. ```sh $ echo 'a: sort([{n:"b"}, {n:"a"}], n)' | aontu -c {"a":[{"n":"a"},{"n":"b"}]} ``` **The third is `asc` or `desc`**, and omitting it is `asc`. The projector comes first, so a keyless descending sort writes the empty projector, which means the member itself. ```sh $ echo 'a: sort([{n:1}, {n:3}], n, desc) b: sort([1, 3, 2], "", desc)' | aontu -c {"a":[{"n":3},{"n":1}],"b":[3,2,1]} ``` **Equal keys keep source order**, in both directions. The source position breaks every tie, which makes the order a total one, so the two implementations answer the same list whatever their own sort does with equals. ```sh $ echo 'a: sort([{k:1,v:"a"}, {k:1,v:"b"}, {k:0,v:"c"}], k, desc)' | aontu -c {"a":[{"k":1,"v":"a"},{"k":1,"v":"b"},{"k":0,"v":"c"}]} ``` **There are two orders and no third.** Numbers compare through the exact comparator, never through binary64, so a bigdecimal and an integer in one bag order by their values. Text compares by code point. A bag that mixes the two, or that holds a boolean, a null or a container, has no order to be put in and is refused (`sort_domain`). A member with no key to order by is `sort_key`, for the reason [`pick`](#projecting-fields-pick) refuses one: a shorter list is a different answer. A direction naming no direction is `sort_dir`. ```sh $ echo 'a: sort([0d9007199254740993, 9007199254740992])' | aontu -c {"a":[9007199254740992,0d9007199254740993]} ``` **A sort sees the members generation emits**, the rule every bag reader follows: a `hide()`- or `type()`-marked child is not one, and neither is an optional key that generates nothing. Composed with [`pick`](#projecting-fields-pick) it turns a bag of records into an ordered line of source, and with [`join`](#folding-to-a-string-join) into the text of one: ```sh $ echo 'cols: [{n:"id"}, {n:"age"}] sql: join(pick(sort($.cols, n), n), ", ")' | aontu -c {"cols":[{"n":"id"},{"n":"age"}],"sql":"age, id"} ``` ## Aggregating: `sum` `least` `greatest` `length()` counts a bag; these three fold one. Each takes a **single bag** (a list or a map) and walks the children the model already holds: ```aon lines: [1200 450 3000] total: sum($.lines) lowest: least($.lines) peak: greatest($.lines) hourly: { p50:12 p95:40 p99:91 } spike: greatest($.hourly) ``` ```json {"lines": [1200, 450, 3000], "total": 4650, "lowest": 450, "peak": 3000, "hourly": {"p50": 12, "p95": 40, "p99": 91}, "spike": 91} ``` A map is folded in **sorted-key order** and a list in source order, which is `each`'s rule; for these three it changes nothing, since every operation is commutative, but it is stated so that it cannot drift. They are named `least` and `greatest` rather than `min` and `max` because those two are already the constraint atoms for a lower and an upper *bound*: `min(3)` means "at least 3", which is a statement about a value, while `least($.xs)` picks an element out of a set. Two different things do not share a spelling. **`sum` folds with `add`**, so the whole [number tower](#arithmetic-add-sub-mul-div-mod-rem) comes with it: a bag of integers sums to an integer, one float among them makes the total a float, `0d` members keep it exact, and a total that will not store is refused rather than rounded. ``` x:sum([1,2,3]) → {"x":6} integer x:sum([1,2.5]) → {"x":3.5} float, by contagion x:sum([0d1.5,0d2.5]) → {"x":0d4.0} exact x:sum([]) → {"x":0} ``` **`sum([])` is `0`, and `least([])` is an error.** Addition has an identity, so the empty sum has an answer; comparison has none, and answering with a zero or an infinity would be inventing a value the data does not contain (`aggregate_empty`). `least` and `greatest` return **one of the elements**, so the answer keeps that element's own kind, and they compare with the tower's exact comparator rather than through binary64: `0d9007199254740993` and `9007199254740992` share a float image but are correctly ordered here. A value that is not a bag is `aggregate_data`; a member that is not a number is `invalid-arg`, reported against the aggregate the author wrote rather than against the `add` inside it. There is no `fold` combinator and will not be one: a fold takes a function, and this language has no user functions to give it. These three are total because the bag is finite, the operation is fixed, and each child is visited once: the same argument that makes `each` safe. ## Folding to a string: `join` `join(coll, sep?)` folds a bag into one string: every member rendered as text, with `sep` between them. It is the counterpart of `sum`: one takes a bag to a number, the other to a string. ```sh $ echo 'ports: [8080, 443] addr: join($.ports, "-")' | aontu -c {"addr":"8080-443","ports":[8080,443]} ``` **The separator defaults to the empty string**, so `join(coll)` is concatenation. That is why there is no `concat` and no `lines`: with a separator argument, one function covers both. ```sh $ echo 'a: join([x, y, z]) b: join([x, y, z], ", ")' | aontu -c {"a":"xyz","b":"x, y, z"} ``` **A fold sees the members generation emits.** `join`, and with it `each`, `emit`, `filter`, `pack`, `pick` and the aggregates, read a bag's *members*: a `hide()`- or `type()`-marked child is not one, and an optional key whose value generates nothing is not one, so a value the document withholds from its output never reaches a string or a total the document computes. Canon still shows the whole document; the fold does not. ```sh $ echo 'm: {a: "keep", b: hide("SECRET")} s: join($.m, "-")' | aontu -c {"m":{"a":"keep","b":"SECRET"},"s":"keep"} ``` A reference still lifts a hidden bag: `each($.schema.entities, _)` under `schema: hide({…})` sees every entity, because there the mark belongs to the schema, not to any one entity. **`join` folds with `+`**, exactly as `sum` folds with `add`. The number-to-text rule is therefore `+`'s own and not a second one: no `0d` marker, no `.0` float suffix, and the exact digits of a big integer. ```sh $ echo 'a: join([1, 2.0, 0d0.5, true], "|")' | aontu -c {"a":"1|2|0.5|true"} ``` **`join([])` is `""`**, concatenation's identity: the parallel of `sum([]) == 0`, and the opposite of `least([])`, which refuses because comparison has no identity to answer with. A map folds in **sorted-key order** and a list in source order, which is `each`'s rule and `pick`'s. For a generated file this matters: list order is *source* order, so a list is what a transform should build its lines in. ```sh $ echo 'm: {b: B, a: A} x: join($.m, ",")' | aontu -c {"m":{"a":"A","b":"B"},"x":"A,B"} ``` Composed with `pick`, it is the step that turns a bag of records into a line of source: ```sh $ echo 'cols: [{n: id}, {n: email}] sql: join(pick($.cols, n), ", ")' | aontu -c {"cols":[{"n":"id"},{"n":"email"}],"sql":"id, email"} ``` **A member that is settled but not text is an error** (`join_member`), raised at the member rather than at generation. `+` with a string on the left *residuates* on a map or a null rather than refusing, so folding blindly would report the failure late and name the whole call instead of the member that caused it. ```sh $ echo 'a: join([{x: 1}], ",")' | aontu [aontu/join_member]: Cannot join value at path $.a ... $ echo $? 1 ``` **A member that is merely unresolved is not an error at all.** The call stays residual and generation reports ordinary incompleteness, so `join` can be written in a schema over data that has not arrived: ```sh $ echo 'names: [string] line: join($.names, ",")' | aontu -c {"line":join([string],","),"names":[string]} ``` The separator must be a **string**. A number would render perfectly well through `+` and is still refused: the separator is not a member of the fold but the parameter naming the text between members, and `join(x, 5)` is far likelier a mistake than an intent (`invalid-arg`). A value that is not a bag is `aggregate_data`, as it is for the aggregates. ## Text: `esc` `usc` `rep` `split` Four ordinary string functions. They return values and compose with `+`, and they know nothing about generation, but they are what a generator needs, because a generator interpolates values into literals and derives names from data. ### `esc(s, variant?)` and `usc(s, variant?)` `esc` makes a string safe to place inside a literal; `usc` reads it back out. **A variant names a convention, not a language**: several languages share one convention, and one language has several: a C-family literal escapes differently in each quote, and SQL spells a literal one way and an identifier another. | variant | convention | |---------|------------| | *(none)* | C / JSON, double-quoted: TypeScript, JavaScript, Java, C, C++, C#, Go, Rust, Swift, Kotlin, Scala and JSON itself | | `sq` | single-quoted C-family | | `sql` | standard SQL, which doubles the quote | | `shell` | POSIX single-quote | | `xml` | the five entities; covers HTML | | `uri` | percent-encoding, RFC 3986 | | `regex` | the metacharacters the pattern subset admits | ```aon plain: esc("plain text") inner: esc("it\'s", sq) table: esc("o\'brien", sql) markup: esc("&", xml) address: esc("a b/c", uri) pattern: esc("a.b", regex) ``` ```json {"plain": "plain text", "inner": "it\\'s", "table": "o''brien", "markup": "<a>&", "address": "a%20b%2Fc", "pattern": "a\\.b"} ``` **Escaping a value that was already safe changes nothing**, which is what makes it cheap enough to do by default. An unknown variant is refused at the call (`esc_variant`) rather than passed through, so a new convention arrives by name rather than by a silent change in what an existing one does. **`usc` is the left inverse, and it is partial.** `usc(esc(s))` is `s` for every `s` and every convention. The other direction does not hold: several spellings escape to one value, so `esc(usc(t))` is `t` only for canonically escaped `t`. Text with no original (a truncated code-point escape, an escape the convention does not define, a lone quote where the convention doubles it) is refused (`usc_malformed`) rather than answered with a different string. ### `rep(s, pattern, sub)` Every match of `pattern` in `s` replaced by `sub`. The pattern is the **same portable subset [`re`](#re-and-the-portable-pattern-subset) takes**, so a document has one regexp language rather than two. The substitution is `$1` to `$9` for the numbered groups, `$&` for the whole match and `$$` for a literal `$`. ```aon day: rep("2026-09-04", "([0-9]+)-([0-9]+)-([0-9]+)", "$3/$2/$1") words: rep("aim:ingest,process:episode", "[,:]", " ") ``` ```json {"day": "04/09/2026", "words": "aim ingest process episode"} ``` **It replaces every match**: a replace-the-first default silently does the wrong thing in a generator that normalises separators, and anchoring the pattern is how a document asks for one. A `$` naming nothing, or a group the pattern has not got, is refused (`rep_sub`) rather than expanded to the empty string: a file written with a hole in it and no complaint is the failure that refusal exists to close. ### `split(s, sep)` The fields of `s`. **A plain string separator is a literal and an `re(…)` argument is a pattern**: the asymmetry with `rep` is deliberate, since splitting is usually on a literal, and it removes the trap where `split(v, ".")` silently cuts between every character. ```aon fields: split("a,,b", ",") chars: split("abc", "") runs: split("a1b22c", re("[0-9]+")) whole: split("abc", ",") ``` ```json {"fields": ["a", "", "b"], "chars": ["a", "b", "c"], "runs": ["a", "b", "c"], "whole": ["abc"]} ``` Empty fields are **preserved**, so `join` is the inverse: `join(split(s, sep), sep)` is `s`. An empty separator yields the code points, and a separator that does not occur yields the whole string as one field. ## Linking: the tree is the namespace A document is a tree, and its only names are tree paths. That is deliberate, and it is the whole of the addressing story: there is no second namespace, no registry of declared names, and nothing a document can say that makes two positions one node. Two consequences follow, and both are what the design is for. **A model can be instantiated more than once.** Mount the same file at two paths and you get two independent nodes, each with its own values. Write the model as `model.aon`: ```aon auth: { port:80 region: *"eu"|string } billing: dep: refer() & path(..auth) ``` and mount it twice from `main.aon`: ```aon tenantA: m: @"./model.aon" tenantB: { m:@"./model.aon" m:auth:region:"us" } ``` Each instance resolves its own internal link inside itself, and the per-tenant override is an ordinary narrowing rather than a contradiction. A global name on `auth` would have made the two instances one entity and the second override an error, which is why there are no global names. **Bringing two descriptions into contact is something you write.** Unification is path-aligned, so a catalog file and a deploy file that describe the same real-world thing at different paths do not meet on their own. Point one at the other and they do: ```aon catalog: payments: { owner:"team-pay" tier:1 } deploy: eu1: payments: $.catalog.payments & { replicas:3 tier:2 } ``` The two `tier` values now meet, and disagree, so the run fails at the site that says so. A reference is directional (`deploy` is narrowed, `catalog` is not) and that directionality is what keeps two unrelated models from silently merging because they happened to choose the same word. ## First-class paths: `path(p?)` `path(p)` **captures** the path expression `p` as a value: the spelling, never the resolution. A plain reference resolves; a capture is the address itself, as data. ```aon a: b: 1 emb: $.a.b # a reference: the value at the path cap: path($.a.b) # a capture: the path itself ``` ```json { "a": {"b": 1}, "cap": "$.a.b", "emb": 1 } ``` This is the one non-strict argument position in the language: every other call reads its argument's value, `path(p)` reads its spelling. The captured spelling is the address grammar `refer` reads (`$.a.b` from the document root, `.b` from the sibling scope, one more leading dot per parent step) and a bare dotted argument is relative (`path(q.r)` captures `.q.r`). A bare string is **never** a path: the call's own argument is the one conversion the language has. A string *literal* argument is address text (`path("$.a")` is the capture `path($.a)`), and text with no anchor is **relative**: `path("auth")` is `path(.auth)`, the address the raw spelling captures. A *computed* argument (an expression, a reference to a string) evaluates first, and the result converts by the same grammar, which is what makes an address buildable: ```aon names: { web: {} db: {} } accounts: pack($.names, { for:refer() & path("$.names." + key()) }) ``` ```json { "accounts": { "db": {"for": "$.names.db"}, "web": {"for": "$.names.web"} }, "names": { "db": {}, "web": {} } } ``` Text that spells no address even once anchored (an empty string, an empty segment (`"a..b"`), a broken `$` spelling) refuses at the call (`path_address`); a number or a container argument is refused as `invalid-arg`. `path()` with no argument is the path **kind**: the set of all path values. It sits under `string` in the kind lattice, so `string` admits a path value and the string constraints apply to spellings, but the kind does **not** promote: `path() & "$.a"` refuses (`no_scalar_unify`) exactly as `integer & "x"` does, because outside the `path(...)` call a string never becomes a path. Everything else about a path value is what scalars already do, made precise by three rules: 1. **Meets are syntactic, by the prefix rule.** Two path values meet when one spells a *prefix* of the other (the same anchor, the shorter path's segments starting the longer path) and the result is the **longer**: a path can always be told more precisely. `path($.a) & path($.a.b)` is `path($.a.b)`; incomparable spellings (`path($.a) & path($.b)`, or different anchors) refuse (`scalar_value`); and a path value refuses a plain string *literal* (`path($.a) & "$.a"` is `scalar_kind`) exactly as the number tower's leaves refuse each other. Subsumption follows the meet: a prefix subsumes its extensions. 2. **A path value is data.** `path($.nope)` generates `"$.nope"`: existence is `refer`'s contract, not the value's, so a document may address things outside this evaluation. `path(p) & refer()` is the checked link: see [Checked links](#checked-links-refert). 3. **Generation and canon.** A path value generates as its address string; its canonical form is the call (`path($.a.b)`), which reparses to the same value: the call form is the literal syntax for this kind. The kind settles inside `type()` bodies, which a `refer` cannot (see [Checked links](#checked-links-refert)), so a vocabulary can declare a path-valued field for the data to meet: ```aon Service: type({ host:path() }) db: $.Service & { host:path($.hosts.h1) } hosts: h1: {} ``` ```json { "db": {"host": "$.hosts.h1"}, "hosts": {"h1": {}} } ``` Pinned by [`test/spec/path.tsv`](https://github.com/aontu-lang/aontu/blob/main/test/spec/path.tsv). ## Checked links: `refer(t?)` A reference (`$.a.b`) resolves by *cloning* its target into place, so `dependsOn: [$.services.auth]` generates a full copy of the auth node where the author meant a name. A bare string generates the name and checks nothing. `refer` is the third option: the field keeps the address string, and the language checks it. ```aon services: auth: { kind:service port:8080 } services: billing: dependsOn: [&: refer({ kind:service }) path($.services.auth)] ``` ```json {"services": { "auth": {"kind": "service", "port": 8080}, "billing": {"dependsOn": ["$.services.auth"]}}} ``` The list spread applies `refer` to every element, so `dependsOn` generates a list of **addresses**, checked. `refer(t)` says three things about the string it constrains: 1. It must be a **tree address**. 2. The address must **resolve** in this evaluation. 3. If `t` is given, `t` is unified **into** the target. ### Addresses An address is a path, in the two spellings a reference already uses: ``` $.services.auth from the document root .auth beside the link itself ..auth one level up from there ``` `$.a.b` is absolute. A leading `.` reads the link's own sibling scope, and every further dot is one step up: the same reduction a relative reference performs. `$` alone is not an address: the whole document has no enclosing position, so nothing could be written back into it. Relative addressing is what makes a model reusable. A link written `..auth` means a different node from each position the model is mounted at, so the same file instantiated twice gives two self-contained instances. Only a [path value](#first-class-paths-pathp) can be an address: a bare string never is (`refer() & "$.a"` refuses (`refer_address`)) and `path("...")` is the one conversion. A second path peer refines the address by the prefix rule (`refer() & path($.a) & path($.a.b)` links to `$.a.b`), and a relative address that climbs off the top of the tree is refused outright: no later pass can grow a tree upwards. ### Existence is decided, not deferred A `refer` **residuates**: a target may be introduced by a later conjunct, include or spread, so the constraint retries each pass exactly as a forward reference does. But within one evaluation the document-set is fixed, so existence *is* decidable: an address that still names nothing at the last pass is a located error (`refer_unresolved`), not something to check later. ### Constraints flow through links `refer(t)` does not merely *test* the target against `t`; it unifies `t` into it, at the position the address names: ```aon a: p: 1 b: refer({ r:3 }) & path($.a) ``` ```json {"a": {"p":1, "r":3}, "b": "$.a"} ``` Referring to something as a `Service` makes it one, and if it cannot be, the conflict is an ordinary located error. Check-only semantics would be non-monotone (true, then false as the target grows), and the lattice guarantee is that more information never falsifies what has already been observed. Constraints written *alongside* a refer constrain the **link**, not the target: `refer() & string & re("auth$") & path($.services.auth)` checks the address itself. They are held until the address arrives, and then meet it. ### The argument is a template, not an address `refer(t)` takes the value the **target** must satisfy. The address comes from the `path()` beside it, never from the argument, so `refer(key())` does not mean "link to the node this key names". It means "the target must unify with whatever `key()` answers here", and `key()` answers with a *string*, so the link is constrained to a target that is that string. At the root of a document `key()` is `""`, which leaves `refer("")`: a link with no address, which cannot generate. ``` link: refer(key()) → [aontu/mapval_no_gen] at $.link value was: refer("") ``` **A key does not survive a reference.** `key()` is path-dependent (it answers for the destination it lands at) and a reference is a new destination, so referring to a field whose value came from `key()` re-fires it at the referring site rather than carrying the target's key across. There is no built-in that takes a `path()` value and yields its last segment. Generate the link and the name together instead, from the one place the key is already in hand. Inside a `pack` template `key()` is the child's own key, so it can build the address and stand as a value at the same time: ```aon services: { auth:port:8080 billing:port:9090 } names: [auth billing] links: pack($.names, { to:refer() & path("$.services." + key()) name:key() }) ``` ```json {"services": {"auth": {"port": 8080}, "billing": {"port": 9090}}, "names": ["auth", "billing"], "links": {"auth": {"to": "$.services.auth", "name": "auth"}, "billing": {"to": "$.services.billing", "name": "billing"}}} ``` `to` is checked (a name with no service refuses) and `name` is the same key as an ordinary string. ### The bundled vocabularies **Five vocabularies ship with the engine**, served from it rather than from disk, and **every one of them is named under `aontu:`**. That is the whole rule: a language-supplied schema has one spelling, and the scheme is what stops a file on disk from standing in front of it. | name | what it is | |---|---| | `aontu:system` | ports, components and services: [below](#the-aontusystem-vocabulary) | | `aontu:view` | the schema for one declaration of a [view document](/docs/reference-api#aontu-view), `$.aontu.View.Figure`, which types every option the verb reads so a typo is refused at evaluation | | `aontu:profile` | a language declared as data, which `template` and `fmt` read through `--profile` | | `aontu:lang/text` | the text profile | | `aontu:lang/markdown` | the markdown profile | The last three are described [after the system vocabulary](#the-aontu-models). ### The `aontu:` models A name that begins `aontu:` is a **language-supplied model**, and it resolves from the engine's own table and nowhere else: the memory, module, file and package legs are never asked, so no file can shadow one, and a name the engine does not serve is refused naming the set rather than looked for on disk. **Everything an `aontu:` model defines lands under the single root key `aontu`**, so including one never takes a name a document wants: | include | defines | |---|---| | `@"aontu:system"` | `$.aontu.System.Port`, `.Component`, `.Service`, `.Semver` | | `@"aontu:view"` | `$.aontu.View.Figure` | | `@"aontu:profile"` | `$.aontu.Profile`, `$.aontu.Lang` | One key is reserved instead of six, it is named for the language rather than for a domain, and `$.aontu` anywhere tells a reader at once that the subtree is not the document's own. **A path part that names a type is CamelCase.** That is why every bundled key above is capitalised, and why the members under them (`Port`, `Service`, `Figure`) always were: the case of a segment says what kind of thing it names. It is a **convention and only a convention**: the engine does not check it, `aontu vet` says nothing about a lowercase `type()`, and a document is free to ignore it. The bundled models follow it so there is one worked example to copy. The SCHEME name is unaffected and stays lowercase: `@"aontu:system"` loads the model, `$.aontu.System` is where its content lands, and a source name is not a path. Write this as `models.aon`: ```aon @"aontu:profile" aontu: Lang: { lang:"ocaml" template: { marker:"(*-" close:"*)" ext: [ml] } } ``` ```sh $ aontu models.aon { "aontu": { "Lang": { "indent": { "unit": " ", "width": 2 }, "lang": "ocaml", "template": { "close": "*)", "ext": [ "ml" ], "marker": "(*-" } } } } ``` A name the engine does not serve is refused, and the refusal names the set. Write this as `nope.aon`: ```aon @"aontu:nope" ``` ```sh $ aontu nope.aon source not found: aontu:nope (the language-supplied models are aontu:lang/markdown, aontu:lang/text, aontu:profile, aontu:system, aontu:view) $ echo $? 1 ``` **`aontu:profile`** is the schema of a language profile: a language declared as data, which `aontu template` and `aontu fmt` read through `--profile`. It carries the language's `lang`, an `indent`, optionally the comment forms, and the `template` block below. A profile is data and only data. Its root is not `type()`-marked, because a profile is read through generation; `$.aontu.Profile` names the schema with `type()`, so naming it neither generates it nor asks a document to fill it, and `$.aontu.Lang` is where one lands. **A profile is where a language is configured.** Its `template` block names the marker a generator written in that language carries and the extensions that marker belongs to, so `aontu template` and `aontu fmt` read one file rather than repeating a `--marker` flag. A marker carries its own closer after a space where the opener does not imply one, which is what reaches a block comment the engine has never seen: ```aon aontu: Lang: template: { marker:"(*-" close:"*)" ext: ["ml" "mli"] } ``` **`aontu:lang/text`** and **`aontu:lang/markdown`** are the two bundled profiles. `text` is `lang: "text"` and an indent of two spaces, and nothing else; `markdown` is that plus what markdown has of its own, the HTML comment form and the template marker its files write, ``. A language the bundled pair does not cover writes its own profile and passes it with `--profile`. Every bundled model is **experimental** until the vocabulary can be versioned by canon-hash. ### The `aontu:system` vocabulary Ports, components and relations need no syntax: they are schemas, and one set of them ships with the engine. Write this as `system.aon`: ```aon @"aontu:system" services: { auth: $.aontu.System.Service & { ports: http: protocol: http dependedOnBy: rel() & [path($.services.billing)] } billing: $.aontu.System.Service & { dependsOn: rel($.aontu.System.Service) & inverse(dependedOnBy) & acyclic() & [ path($.services.auth) ] } } ``` ```sh $ aontu system.aon { "aontu": { "System": {} }, "services": { "auth": { "dependedOnBy": [ "$.services.billing" ], "kind": "service", ... ``` | Schema | Says | |--------|------| | `$.aontu.System.Port` | one end of a connection: `direction` (default `in`) and an optional `protocol` | | `$.aontu.System.Component` | a node with `ports`, each of which is a `Port` | | `$.aontu.System.Service` | a Component whose `kind` is `service` | | `$.aontu.System.Semver` | a version as an ordered tuple, `[major minor patch pre-release build]`, with the tail defaulted: `[1]` is `[1 0 0 "" ""]` | **`Semver` is a list, not a string and not a map.** A version is compared rather than read, and comparison runs component by component from the left, an order a list has and the other two do not: `"1.10.0"` sorts below `"1.9.0"` as text, and a map has no order of its own to compare along. **The tail is defaulted**, so a version may be written as short as it is meant: `[1]` is `[1 0 0 "" ""]`, and `[1 2]` is `[1 2 0 "" ""]`. The arity is five, so a sixth element is refused (`[aontu/constraint]`). Write this as `version.aon`: ```aon @"aontu:system" v: $.aontu.System.Semver & [1] pre: $.aontu.System.Semver & [1 2 3 "alpha.1"] ``` ```sh $ aontu version.aon { "aontu": { "System": {} }, "pre": [ 1, 2, 3, "alpha.1", "" ], "v": [ 1, 0, 0, "", "" ] } ``` The two string parts differ in three ways: - **Both are checked by grammar, not by pattern.** [semver.org 2.0.0](https://semver.org) spells each as dot-separated identifiers, which as a pattern is a quantified group holding a quantifier, the one shape `re()` refuses outright (`constraint_pattern`, for backtracking exponentially; see [The constraint algebra](#the-constraint-algebra)). The vocabulary carries an ABNF grammar for each instead and applies it with `parse()`, so `"beta_1"`, `"alpha..1"` and `"01"` are all refused (`[aontu/empty]`) where an alphabet pattern admitted the last two. The two grammars are members of the model in their own right, `semverPreRelease` and `semverBuild`, hidden so a schema's grammar does not generate into the document it checks, and lower-case because the case of a bundled key says whether it names a type. See [grammars](#grammars-abnf-and-parse). - **The two grammars differ where the spec does.** A wholly numeric pre-release identifier may not carry a leading zero, because pre-releases are compared numerically; a build identifier may, because build metadata is never compared. So `[1 0 0 "01"]` is refused, `[1 0 0 "0alpha"]` stands, and so does `[1 0 0 "" "001"]`. - **Build metadata comes last.** The spec says it MUST be ignored when determining precedence, and last is the one position where a comparison that walks the tuple from the left can stop before it without leaving a hole. Leading zeroes need no rule in the numeric parts: they are integers, and `01` is not a distinct integer literal, so the spec's "MUST NOT contain leading zeroes" is impossible to write there rather than merely forbidden. `@"aontu:system"` is **bundled with the engine** (no filesystem, no package resolution) so it resolves under every include capability except `'none'`, which denies every include by definition. It is **experimental** until the vocabulary can be versioned by canon-hash. Two of its behaviours are the language rather than the vocabulary: - **A preferred member is one enum member, with the default role.** `direction: *in | out | inout` is a true enum-with-default under the admission gate: unset generates `in`, `out` and `inout` override, and any other value is refused (`[aontu/empty]`). A vocabulary that wants an open field says so with a `| top` (or `| string`) branch. - **`Service` is written out rather than as `$.aontu.System.Component & {kind: service}`.** A reference from one member of an included file to another does not survive the include, so each schema states itself; `$.aontu.System.Component & $.aontu.System.Service` still meets exactly as you would expect. Everything here is ordinary unification, so an author who wants a different vocabulary writes one the same way, and nothing in the language knows these names. ### Declared relations A relation is declared AT ITS FIELD: `rel(t)` says the field's strings are tree addresses and flows `t` into every target, and the two GRAPH ATOMS declare the properties that hold over the whole edge set: ```aon a: dependsOn: rel() & inverse(usedBy) & acyclic() & [path($.b)] b: usedBy: rel() & [path($.a)] ``` ```json {"a": {"dependsOn": ["$.b"]}, "b": {"usedBy": ["$.a"]}} ``` - **`acyclic()`**: the edges under this relation must have no cycle. The error names the nodes the cycle runs through, closing back on the first. - **`inverse()`**: for each `a --dependsOn--> b`, `b` must carry `a` under ``, as an edge of that relation. The error names the exact missing entry. Writing the inverse **for** you is generation, not validation, and is not done here. - The `target` half of the old declaration is `rel(t)` itself: the type flows into each far end at the site, so a conflict or a hole is an ordinary located evaluation error. The atoms are **lattice-inert, deliberately.** Both properties are global and non-monotone: an acyclic graph becomes cyclic when one more edge unifies in, and an inverse that is present becomes absent when the far side is narrowed. The lattice guarantee is that more information never falsifies what has already been observed, so a constraint that could be true and then false is not one the lattice may hold. During unification the atoms only REGISTER the declaration (the predicate is the key they sit on) and ride the field's value; the verdict lands at GENERATION (where no more information can arrive) as a located `relation_cycle` or `relation_inverse_missing` at the offending edge, exactly as an unmet sizing atom refuses. `aontu relations ` reports the same findings without generating, and the library exposes `relationCheck(src)`. The closure question (does `a` reach `b` at any remove?) is a separate verb, [`aontu reaches`](/docs/reference-api#aontu-reaches). There is no reserved `relations:` key: a document that writes one has written ordinary data. The tree is user space at every level. For the working recipes see [Check relations](/how-to/check-relations) and [Query reachability](/how-to/query-reachability); the live version, with its checks, is [use-cases/12-relations](/use-cases/12-relations). ## Marks: `type` and `hide` Marks are boolean flags carried on a value (set by `type()` / `hide()`, or propagated by conjunction): - A **type**-marked value is schema/metadata. - A **hide**-marked value is intentionally excluded from output. In both cases, **a map field whose value is type- or hide-marked is omitted when the enclosing map is generated**, while still participating in unification. A bare marked value at the top level still generates (`type(1) & number`→`1`). `copy()` clears both marks, making the result emittable again: ```aon x: type({}) x: y: 1 a: copy($.x) ``` ```json {"a":{"y":1}} ``` **A mark belongs to the field its wrapper was written at**. A reference to a `type()`/`hide()`-marked value copies the value with the marks cleared, and that holds however the wrapper resolves: a reference that lands on a still-unresolved `type()`/`hide()` call waits for it to resolve at its *own* field rather than copying the call, so the marks can never be re-stamped at the referring site. In particular `m: hide(pack(...))` hides the field `m` exactly as `hide({literal map})` does (the generated children stay usable downstream (`out: pack($.m, {got:_})` emits their values)) and a `type()`-marked alias referenced inside another `type()` body constrains the referring field without suppressing its emission. ## Closed values: `close` / `open` A **closed** map or list refuses any key/element not already present. Narrowing an existing key is fine, and `open` lifts the seal: ```aon a: close({ x:1 }) & { x:number } b: open(close({ x:1 })) & { y:2 } c: close(42) ``` ```json {"a":{"x":1},"b":{"x":1,"y":2},"c":42} ``` `close` on a scalar is a no-op (`c` above), and `close($.x)` closes a referenced node. Adding a key or extending a list is refused: ``` close({x:1}) & {y:2} → error: closed close([1,2]) & [3,4,5] → error: closed ``` ## Source loading `@"…"` `@"path"` loads and parses another source file, then unifies the result in place, so external files merge like any other value. Source files use the `.aon` extension (preferred) or `.aontu`. When the path has no extension, those two are tried in turn, so `@"foo"` resolves `foo.aon` then `foo.aontu`. **The extension decides what the file is**, and it says which of three things: | extension | what it is | |---|---| | `.aon`, `.aontu` | **aontu source**: the language, with everything in it | | `.json`, `.jsonld`, `.jsonc`, `.json5`, `.jsonic`, `.jsc`, `.toml`, `.yaml`, `.yml`, `.ini` | **configuration data**, read by that format's own parser | | `.txt`, and whatever `--text-ext` names | **text**: the file's bytes, as one string | | anything else | refused, by name | Every one of those formats maps onto JSON, which is why one word covers them: a `.toml` file is a map of scalars, lists and maps, and so is the `.aon` file that unifies with it. What a data format does not get is the language: a `&` in a YAML file is a YAML anchor, not a spread key, because the YAML parser reads it, not this one. Write `vocab.jsonld`: ```json {"name": "aontu", "tags": ["config", "types"]} ``` and load it from `main.aon`: ```aon schema: @"./vocab.jsonld" ``` ```sh $ aontu main.aon { "schema": { "name": "aontu", "tags": [ "config", "types" ] } } ``` ### Text: `.txt` and `--text-ext` A `.txt` file is read as **one string**. Nothing parses it, so nothing in it can mean anything, which is what makes it the safe third category. Write `notes.txt`: ``` Deploy freezes over the holiday period. ``` and load it as a value in `main.aon`: ```aon notes: @"./notes.txt" ``` ```sh $ aontu -c main.aon {"notes":"Deploy freezes over the holiday period.\n"} ``` The result is an ordinary string, so the language's string operations reach it and a schema can constrain it: `notes: string & length(1)` holds, and `upper(@"./notes.txt")` uppercases the file. **Other extensions need an allowance.** `--text-ext md,sql` reads those as text too, for a project that keeps its prose in `.md` or its queries in `.sql`. Every verb but `help`, `explain`, `init`, `lsp` and `mcp` takes it, and the dots are optional (`--text-ext .md`). Two limits: an extension the table already names keeps its meaning, so `--text-ext toml` does not re-read TOML as a string; and `.js` stays refused however the flag is spelled. A config file in any of those formats reads the same way. Write `server.toml`: ```toml port = 8080 hosts = ["a", "b"] ``` and hold it to a schema in `main.aon`: ```aon port: integer hosts: [string] @"./server.toml" ``` ```sh $ aontu main.aon { "hosts": [ "a", "b" ], "port": 8080 } ``` **A format's own semantics are the ones that apply.** INI has no types, so `port=8080` read from a `.ini` is the string `"8080"`, and a schema wanting a number has to say so. A malformed config file refuses the whole document rather than becoming an empty value under the key that included it. Every other extension (and a name with no extension at all) is refused by name rather than guessed at. Put rows in `rows.csv`: ``` port,host 8080,local ``` and ask for it in `main.aon`: ```aon rows: @"./rows.csv" ``` ```sh $ aontu main.aon include not readable: ./rows.csv (extension: .csv) $ echo $? 1 ``` A guess would be worse than the refusal, and it was: read as text, a vocabulary became a string that a schema then validated nothing against; read as aontu, prose became a parse error at a line nobody wrote. Both exited 0. Reading a file as text is a category the table now *names* (that is what `.txt` is) and the difference is that it is stated rather than a fallback for whatever the table failed to recognise. ``` @"./foo.aon" → {"f":11} (top level) a:@"./foo.aon" → {"a":{"f":11}} (nested) car:@"./car.aon" car:{wheels:4} → merges loaded + local @"foo" → {"f":11} (implicit .aon/.aontu) ``` To see the merge, write `foo.aon`: ```aon f: 11 ``` a second file, `car.aon`: ```aon doors: 2 ``` and an entry file, `main.aon`, loading both: ```aon @"./foo.aon" car: @"./car.aon" car: wheels: 4 ``` ```sh $ aontu main.aon { "car": { "doors": 2, "wheels": 4 }, "f": 11 } ``` A **relative** path resolves against a configurable base directory: the `aontu` CLI sets it to the entry file's directory, and the Go API exposes it via `NewWithBase` (the TypeScript API via the `path` option). A relative load *inside* a loaded file resolves against **that file's own directory**, so a chain of files (a → b → c) each resolves relative to itself. Absolute paths ignore the base. Resolution tries, in order, an in-memory resolver, the filesystem, then package resolution (see [API reference](/docs/reference-api#aontuoptions)). A conflict between a loaded value and a local one is a normal unification error. ### Modules An import whose path is **domain-shaped** is a module import rather than a file path. A local file says so with a `./`, `../` or `/` prefix: ``` service: @"corp.example/schemas/service" frozen: @"corp.example/schemas/service#aon1-4vJemVYtWFR2mQeN…" legacy: @"alias:legacy" local: @"./fragment.aon" ``` **Every reference says what it is.** The first segment of a package path contains a dot and the path carries no version: compatibility is computed at publish, so the major left the name. `alias:` names an alias the project's package file declares, and resolves by lookup, never by shape. A bare reference whose last segment carries an extension the include table knows is refused with `module_local` and the message `local files need a ./ prefix`, because `config.json` routes here now and was a file before. **Shape routes; validity refuses.** A path that routes here becomes a directory on every platform the toolchain runs on, so it is checked before anything is built from it: no element may be empty, begin or end with `.`, or be a reserved device name (`nul`, `con`, `com1`…), and the path is bounded in length and element count. ``` module path: corp.example/../schemas (an element begins or ends with ".") ``` **Uppercase is escaped on disk.** `corp.example/Widgets` and `corp.example/widgets` are two identities and, on a case-insensitive filesystem, one directory, so an uppercase letter is written `!`+lowercase in every store. The written path stays the identity. **Evaluation never touches the network.** A module resolves from local stores only: `aontu_meta/vendor/` in the project that declares `pkg.aon`, and in every project enclosing it, then the user cache under `aontu/pkg`, which is consulted only when the expected canon-hash is known, because that hash is its key. A module in neither store names the step that fixes it: ``` module not fetched: corp.example/schemas/service (run: aontu sync) ``` **A package that moved refuses.** A package's own file may declare `moved: `; an import of the old path is refused with `module_moved`, naming the destination, and nothing follows it. A name that came to mean something else without saying so would be the failure the naming convention exists to prevent. **The package file and the lockfile are ordinary aontu.** `pkg.aon` declares the package's own path, entry and version, what it depends on, and whether it may be published: ```aon pkg: { path:"corp.example/schemas/service" version:"1.4.2" main:"service.aon" } dep: "corp.example/schemas/common": v: "1.0.0" publish: public ``` `aontu_meta/pkg-lock.aon` is machine-written in **canonical form**: one line, sorted keys, diffable, and (its leaves being scalars) valid JSON. Each entry carries three pins with distinct roles: `archive` certifies *these are the bytes*, `manifest` certifies *this is what the publisher signed*, and `canon` certifies *this is the meaning that was reviewed*: ```aon {"lock":{"corp.example/schemas/service":{"archive":"sha256:9127…","canon":"aon1-4vJe…","manifest":"sha256:f72c…","v":"1.4.2"}}} ``` Only the canon pin can be checked by evaluation alone, and it is the one an import checks: by unifying the module **standalone** and comparing its [canon-hash](#canonical-form): ``` module integrity: corp.example/schemas/service expected aon1-4vJe… got aon1-9kQz… ``` The pin survives comments, whitespace, formatting and refactoring; it breaks on any semantic change in the module's transitive closure. An inline `#aon1-…` fragment is the same check without a lockfile: the degenerate mode for single-file and agent-sandbox use. The other two pins belong to the tooling: `aontu sync` and `aontu pkg verify` check the bytes before the meaning. Under a **root** trust capability (`docs/trust.md`) the user cache is not consulted at all: a confined evaluation sees the project's own `aontu_meta/vendor/` and nothing else, which is what confinement means. **A vendored package is a project inside a project.** It carries its own `pkg.aon`, and its imports resolve from its own directory and then from every project enclosing it, which is where `sync` put its dependencies. The vendor tree is flat: a dependency of a dependency sits beside its dependant, never inside it. **The lockfile is maintained by tooling, not by hand.** `aontu sync` walks the closure and resolves it by **minimum version selection**: every package is taken at the highest of the minima anyone asked for, and never higher, so the answer is reproducible and adding one dependency cannot move another. It fetches what no store holds, verifying the proof, the bytes and the meaning in that order, writes the lockfile, materialises the vendor tree and verifies every pin. The verbs, their flags and the repository they read from are in the [API reference](/docs/reference-api#aontu-sync). ## Operator precedence From tightest to loosest binding (higher binding power binds first): | Operator | Form | Notes | |---------------------|-------------|-------| | `$` (variable/abs) | prefix | tightest | | `.` (path) | prefix/infix | | | `*` (preference) | prefix | | | `-` / `+` (unary) | prefix | `-1 & integer` ≡ `(-1) & integer` | | `+` (add/concat) | infix | | | `&` (conjunction) | infix | binds tighter than `\|` | | `\|` (disjunction) | infix | loosest | So `c & b | a` ≡ `(c & b) | a` and `*1 | number` ≡ `(*1) | number`. Parentheses override precedence and also serve as function-call syntax. ## Canonical form `unify(src).canon` (TS) / `Unify(src).Canon()` (Go) renders a unified value as **reparseable source text**. Unlike generation it preserves constraints, defaults, and open disjunctions. Rules: - Maps render as `{"k":v,…}` with **quoted keys**, no spaces: `{"a":{"b":1,"c":2}}`. Lists as `[v,…]`. - Strings are quoted (`"hello"`); numbers, booleans and `null` render literally; `top` renders as `top`. - **Numbers render so that canon reparses to the same kind.** An integer-kind value renders plainly (`1000`). A float-kind value always carries a fraction or an exponent, so a `.0` suffix is appended when the shortest rendering has neither: ``` 1.0 → 1.0 1e21 → 1e+21 (already exponential) 0.0 → 0.0 0.000001 → 0.000001 (already fractional) 1e20 → 100000000000000000000.0 ``` This applies to **canon only**. String concatenation is unaffected: `a+1.0` is still `"a1"`. - **Exact values carry the `0d` marker**, with any sign in front of it, in plain form at every magnitude: never scientific. An integral bigdecimal keeps one decimal place, which is what distinguishes it from the biginteger of the same value: ``` 0d5 → 0d5 0d1000 → 0d1000 (biginteger) -0d5 → -0d5 0d1e3 → 0d1000.0 (bigdecimal) 0d0.10 → 0d0.1 0d1e-1 → 0d0.1 (one value, one rendering) ``` Here too the marker is canon decoration only: `q+0d5` is `"q5"`. - Negative zero never appears: it normalises to `0` (integer), `0.0` (float), `0d0` (biginteger) or `0d0.0` (bigdecimal), in canon and in generated output alike. - Kinds render lowercase: `number`, `integer`, `float`, `biginteger`, `bigdecimal`, `string`, `boolean`. - Conjunction: `a&b` (for example `number&"A"`). Disjunction: `a|b` (for example `1|2`, `string|number`). Preference: `*x` (for example `*1|number`). - Spreads keep the `&:` entry: `{&:{"x":2},"y":{…}}`. ## The formatted form `aontu fmt` writes a document in one agreed form, in the tradition of `gofmt`, so that layout is never argued about and a diff shows only what changed. The form is a spelling of the document and not a change to it: what the formatter writes evaluates to the same value, has the same canon-hash, and is a fixed point of the formatter. The verb is in the [API reference](/docs/reference-api#aontu-fmt); how to run it on a file or gate a repository is a [how-to](/how-to/format-a-document). This section is the form. **Lines.** Two spaces per level of indentation, never a tab. Line endings are `LF`, no line ends in whitespace, and the file ends in one newline. A packing budget of 80 columns decides between two legal spellings of a value, one line or several, and nothing else: the formatter never breaks a line. A string 200 columns wide stays 200 columns wide, and an expression the author wrote on one line stays on it however wide it is. **Pairs.** A pair is `key: value`, no space before the colon and one after, and the key, the colon and the value are never on different lines. At statement level every pair has its own line, so `a: 1 b: 2` on one line becomes two. Inside an inline container the colon is tight, `{ a:1 b:2 }`, and the space between pairs is what separates them. An optional key keeps its marker tight, `port?: integer`; a spread is `&: value`; an alias declaration is `%Name = value`. **Braces are for shape, not for nesting.** A pair whose value is a map holding exactly one entry is written as a chain: `a: {b: 1}` is `a: b: 1`, recursively, and the root map has no braces at all. A one-key map in list position is a pair element, `[a:1 b:2]` for `[{a:1}, {b:2}]`. A map whose only entry is a spread keeps its braces, `a: { &: integer }`, because the braces are what say "a map shape": a spread alone reads as a constraint on `a` rather than on its members. A map that is an operand or an argument keeps its braces too, `a: { b:1 } & T`, `s: close({ a:1 })`: those are expressions, and inside them the rules apply again to each entry. **Repeat the prefix.** A pair in statement position whose value is a plain map is laid out in this order: on one line, `key: { a:v b:v }`, when that fits the budget and the map holds no comment and no value that spans lines; else as one statement per entry, each carrying the key again, when every entry is a one-liner that way: ```aon server: host: "0.0.0.0" server: port: 8080 server: tls: { enabled:true cert:"/etc/tls/edge/cert.pem" } ``` and otherwise as a braced block, `key: {` on the pair's line, each entry a statement one level in, and `}` alone on its line. The repeat is legal because a key written twice is a meet, and the meet of two maps with disjoint keys is their union: the three statements above and `server: { host: "0.0.0.0", port: 8080, tls: { ... } }` are one document, with one canon-hash. It applies recursively, so a nested map that fits stays on its line and one that does not is descended into under the longer prefix, `a: b: c: 1` / `a: b: d: 2`; there is no cap on how many statements a map becomes. **The descent stops at a record.** A record is a map of several entries, every one of them a value rather than another map: a field, an error, a row. The prefix reaches through a map that holds maps, because those keys are a path and a line carrying all of them says where it is; where the descent reaches a record instead, that map is written as a braced block under the prefix rather than dissolved into it, because its keys are what the thing IS and repeating the prefix in front of each of them says nothing: ```aon entity: planet: table: "planets" entity: planet: field: id: { name: "id" json: "id" kind: "string" required: true pk: true write: true fk: false } ``` A one-entry map is a chain at every width and is not a record; nor is a map holding a spread. The block replaces a descent and never rescues one: where the deeper repeat could not have been written anyway (a list too wide under the longer prefix, a value spanning lines) the statement is a braced block by the rule above, exactly as it was before this. And the statement's own map is not reached by a descent, so a flat `server: host:` / `server: port:` is written as the repeat it has always been, and so is a record a chain leads to and nothing else does. Merging goes the other way too: adjacent statements naming one key are one map to the formatter, which then lays that map out by the same procedure, so `s: a: 1` / `s: b: 2` is written `s: { a:1 b:2 }`. Only adjacent statements merge; a `server:` line, something else, then another `server:` line stays as it is, because the formatter never reorders. A statement's trailing comment travels onto the entry it stood beside; comments and blank lines between merged statements stay between the entries. The rule touches nothing but a plain map in statement position. A map wrapped in a call is an expression, and splitting it changes the document: `s: close({a: 1})` / `s: close({b: 2})` does not evaluate where `s: close({a: 1, b: 2})` does. The same holds for an operand of `&` or `|`, a list element, a map with a comment on its opening line or as its last entry, and a map holding two spreads, which the engine keeps as a conjunction. Every repeat and every merge is checked with the engine before it is written, the two spellings evaluated in isolation and compared, and a rewrite the engine evaluates differently is not made: the statement keeps its braces. **Containers.** A map whose one-line spelling fits is written on one line, padded inside the braces and with the colons tight; a list is not padded: `limits: { rps:100 burst:200 }`, `ports: [80 443 8080]`, `routes: [get:"/health" post:"/orders"]`. A container goes to several lines when it does not fit, when it holds a comment, or when an element is itself several lines. A list then puts each element on its own line one level in, with the closing bracket alone on a line; a map in statement position repeats or blocks as above; a map in expression position is a braced block, `{` at the end of the line that opens it and `}` alone, which is the ordinary spelling of a constrained map: ```aon CatalogEntry: $.aontu.System.Service & { owner: %Owner tier: 1|2|3 dependsOn?: rel($.aontu.System.Service) & %CatalogAddr & acyclic() & inverse(dependedOnBy) } ``` That third line is 83 columns where it sits, and stays so. Empty containers are `{}` and `[]`, always inline. **Separators.** No commas between pairs or between elements: a newline or a space separates, and commas on input are dropped, trailing ones included. Inside a call's argument list the author's separators are kept, a comma or a space, with one space after a comma, because the parser reads a run of arguments such as `must((v) => 0 <= v, "…")` exactly as it reads `match(.t, "string", "x")`. **Comments.** Every `#` comment is kept, its text untouched. A comment on its own line attaches to the statement that follows it and is indented to that statement's level; a blank line between the two stays. A trailing comment stays on its line, one space after the last token, and trailing comments are not aligned into a column. A comment inside a container puts the container on several lines, which is the only way the comment keeps its place; a comment on the line that opens a block stays there, `server: { # what the edge sees`. **Blank lines.** A blank line is a paragraph break the author chose, and the formatter keeps it: any run of blank lines becomes one. None at the start or end of a block, none at the start of the file, and one at the end, which is the final newline. **Keys and strings.** A key is bare when it can be: a quoted key whose text is a legal bare key, `[A-Za-z_][A-Za-z0-9_]*`, is written bare, so `"host": 1` becomes `host: 1`. Quoting that means something is never touched: `"a?": 1` is a key named `a?`, where `a?: 1` is an optional `a`. A single-quoted string becomes double-quoted, `'plain'` to `"plain"`, unless it holds a double quote; a backtick string is verbatim, newlines and indentation included; a string's content is never changed; and a bare string stays bare, a quoted one quoted. **Numbers.** A number's source text is copied exactly. `1`, `1.0`, `0d1` and `0d1.0` are four kinds, and `1_000`, `0x1f` and `1e3` are spellings the author chose. **Operators and calls.** Binary operators are spaced, `a & b`, `a | b`, `a + b`; a preference is tight, `*8080 | 9090`; a call is `name(arg, arg)` with no space before the parenthesis and none inside it, and an empty argument list is `name()`. References and paths are copied as written. Parentheses are the author's: the formatter neither adds nor removes a grouping parenthesis. A line break the author put inside an expression is kept, at its operator, which then leads its continuation line one level in: ```aon out: `a` + .b + match(.t, "string", `TEXT`) ``` A call that does not fit on its line hugs its last argument to the parentheses when that argument is a container, an unbroken expression that ends in one, or a call whose own last argument hugs, which is the schema idiom `type(close({` … `}))`; otherwise the arguments go one per line, one level in, with the closing parenthesis alone. Arguments that hold no container stay on one line however wide it is: a scalar is no narrower on a line of its own. **The root, and what never changes.** The root map has no braces. Includes and alias declarations are statements like any other, kept where the author put them and in that order. The formatter never reorders a key, an element, an include or a declaration; never renames a key; never introduces an alias; never resolves an include or reads a file it was not given; never changes a number, a string's content or a parenthesis; and never breaks a line. ## The published grammar Canon is the shape a grammar can be written for (every key quoted, one spelling per construct) and [`grammar/aontu.abnf`](https://github.com/aontu-lang/aontu/blob/main/grammar/aontu.abnf) is that grammar, in RFC 5234 notation with RFC 7405's case-sensitive `%s"…"` literals. The same rules are published for two machine consumers as [`aontu.gbnf`](https://github.com/aontu-lang/aontu/blob/main/grammar/aontu.gbnf) and [`aontu.lark`](https://github.com/aontu-lang/aontu/blob/main/grammar/aontu.lark); this is the form to read. It is the **emission surface**: what a document should be allowed to write, a superset of JSON plus the operators, constraints and marks canon emits. It is **conservative by construction** (it may accept less than the parser does, never more) and it makes two deliberate exclusions. `@"…"` includes are absent, because a generated document should describe values rather than reach for files. So are unquoted keys and the other spellings the parser tolerates, because canon does not emit them. The grammar is executed, not merely published: `ts/test/grammar.test.ts` reads the file, interprets it, and requires it to accept **every canonical-form output in the shared spec suite** (several hundred of them) and to refuse the excluded forms. A rule the engine has outgrown fails the suite. ### How a value composes Whitespace is permitted between every element and is not drawn; the `ws` rule in the grammar text carries it. Each track is one rule, and a box in one is a link to its own track.
rootvaluevaluedisjunctdisjunctconjunct|conjunctconjunctprefixed&prefixedprefixed*prefixedsumsumatom+atomatommaplistfuncrefkindplacescalar(value)map{entry,entry}entryspreadpairspread&:valuepairstring?:valuelist[element,element]elementspreadvaluefuncname(value,value)nameone of the builtin functionsref$.segment.segmentsegmentALPHADIGIT_place_
Railroad diagram of the aontu grammar's structural rules: a value is a disjunction of conjunctions of prefixed sums, and an atom is a map, list, function call, reference, kind, placeholder, scalar or parenthesised value.
### How one is spelled The scalar forms, the character rules behind a string, the four numeric spellings, and whitespace itself.
kindbigintegerbigdecimalbooleanfloatintegernumberstringtopnilscalarstringexactnumbertruefalsenullstringDQUOTEcharDQUOTEcharunescaped\escapeunescaped%x0-!#-[]-%x10FFFFescapeDQUOTE\/bfnrtuhexhexhexhexhexDIGITA-Fa-fexact-0ddigits.digitsexponentnumber-digits.digitsexponentexponenteE-+digitsdigitsDIGITws %x9%xA%xDALPHAA-Za-zDIGIT0-9DQUOTE"
Railroad diagram of the aontu grammar's lexical rules: the kind names, the scalar forms, a string as a quoted run of escaped or unescaped characters, the exact 0d literal, the plain number with its optional fraction and exponent, and whitespace.
The function-name rule is drawn as one node rather than as a fan of alternatives; the names are in the grammar text and in [Functions](#functions), with what each one means. ## Generation This section is about producing a **value** from a model. Producing target-language **source** from one is a different thing with the same name: see [Generate code from a model](/how-to/generate-code). The ten component functions (`project`, `folder`, `file`, `content`, `line`, `fragment`, `slot`, `inject`, `copyfiles`, and `listitems`) answer a **component tree**, which generates as any other value does. Each node's `cmp` key is the component name a generator runtime looks up: `Project`, `CopyFiles`, `ListItems`, and the rest. [jostraca](https://github.com/jostraca/jostraca) is one such runtime, and reads the tree directly. `generate` / `Generate` produces a native value (JSON-compatible) and requires the model to be **fully concrete**: - Disjunctions must be resolved to a single branch; a `*`-preferred branch is generated as that value. - Unresolved **optional** keys are dropped. - **type/hide**-marked map fields are omitted. - An unresolved **type**, an unresolved **conjunction**, a **nil**, or `top` cannot be generated and raises an error. **Exact values generate exactly.** The `0d` marker is source syntax and does not survive into output; the digits do, all of them. A JSON number is arbitrary-precision text, so nothing is lost on the way out: ``` x:0d9007199254740993 → {"x": 9007199254740993} x:0d0.1+0d0.2 → {"x": 0.3} a:0d1000 b:0d1e3 → {"a": 1000, "b": 1000.0} ``` The last line, run through the CLI's exact emitter: ```sh $ echo 'a: 0d1000 b: 0d1e3' | aontu { "a": 1000, "b": 1000.0 } ``` That is the leaf distinction reaching the output: a biginteger emits `1000`, and the integral bigdecimal beside it emits `1000.0`, because that trailing place is part of a bigdecimal's own digits. The plain family behaves the other way: an integral float loses its point, so `b:2.0` generates `2`. The native values follow: `bigint` and `Decimal` in TypeScript, `*big.Int` and `*aontu.Decimal` in Go, each carrying the exact value. TypeScript's `JSON.stringify` cannot serialise a `bigint`, so the library exports its own exact emitter (`exactJSON`): the one the `aontu` command uses. Object key order is not significant in generated output, and within the plain family neither is numeric kind. Between the exact leaves it *is* significant, as the `1000` / `1000.0` pair shows, which is why the shared suite pins those cases byte for byte rather than structurally. ## Subsumption `A ⊒ B` ("A subsumes B") holds when **every instance the specific value B admits, the general value A admits too**. It is the lattice's own order, asked as a first-class query: `subsume(general, specific)` in both engines, running after evaluation on finished trees, never mutating them. The verdict is three-valued plus `error`: `subsumes`, `does_not_subsume` (with the failing path and both sides' canons as the witness), `undecided` (always with a `sub_*` reason code, never silently), and `error` for a source that does not stand up on its own. Findings reuse the validation verb's report object with class `compat`; every code is registered in `test/spec/errcodes.tsv`, and the whole behaviour is pinned by `test/spec/subsume.tsv` in both engines. **Soundness before completeness.** Where a rule cannot decide, the answer folds toward `does_not_subsume` or `undecided`, never toward "compatible": a gate that wrongly reports "breaking" costs a second look, one that wrongly reports "compatible" ships the break. ### Profiles | Profile | Compares | |---------|----------| | `values` | admitted value sets only | | `defaults` (the default) | value sets, plus every effective default the specific side declares must survive into the general side unchanged | | `gen` | `defaults`, plus the `type`/`hide` marks on corresponding nodes (they change the output shape) | An **effective default** is a preference's own value, or, in a disjunction holding several preferences, the value of the lowest-ranked one (generation picks the lowest rank: `a:**1|*2` generates `2`). Equal-rank preferences that disagree make the effective default indeterminate (`sub_default_indeterminate`, undecided). Adding a default where none existed is compatible; changing or removing one is `compat_default_changed`: previously generable documents materialise differently or become incomplete. ### Rules, by value former | A (general) | B (specific) | A ⊒ B | |-------------|--------------|-------| | `top` | anything | yes | | preference `*x` |: | compares as what it admits (its superior type); its default value is the profiles' business, not the value set's | | unresolved residue (reference, variable, unreduced conjunct or function) on either side |: | `undecided` (`sub_unresolved`): there is no admitted set to compare | | anything | disjunction | every specific alternative must be admitted by A; a concrete failing alternative is a witness (`compat_narrowed`), a non-concrete one is `undecided` (`sub_disjunct_distribution`) | | disjunction | non-disjunction | some general alternative must admit B member-wise; failure with concrete B is a witness, otherwise `undecided` (`sub_disjunct_distribution`): member-wise failure is not proof, the distribution case | | scalar kind | scalar kind or scalar | the general kind admits the specific kind (`number ⊒ integer`) or the scalar's kind; distinct leaves are disjoint | | scalar kind | constraint residual | the kind covers the residual's domain: `number` admits any numeric residual, a numeric leaf kind admits a residual pinned to that leaf, `string` admits any pattern residual | | constraint residual | constraint residual | per the constraint algebra's own [subsumption table](#subsumption-1); a `must` on the general side is `undecided` (`sub_evaluate_only`) | | constraint residual | scalar | membership, with `must` again `undecided`; `unique()` and `length` demands admit no scalar | | concrete scalar | concrete scalar | identity: a concrete value subsumes only itself (kind included) | | map | map | see below; anything else is `compat_narrowed` | | list | list | element-wise by position, with the same required/optional shape as maps | There is no nil rule: an error-free evaluated document carries no nil (failing disjunct members are discarded and every other nil collects an error), and a source that does not stand alone answers `error` before the walk begins. ### Maps, lists, closedness, optionality, spreads - Every **required** key of the general side must be present and required in the specific side, and subsume; a missing or optional-ised key is `compat_required_added` (instances without it are admitted by the specific side but refused by the general). - An **optional** key (`k?:`) of the general side compares only when the specific side has it; the specific side making a general optional key required merely narrows, which is compatible. - A **closed** general bag (`close(…)`) requires the specific side to be closed and inside its declared key set; an open specific side, or a surplus key, is `compat_narrowed`. - A **spread** template (`&:`) on the general side governs the specific side's surplus keys and its template (a missing specific template compares as `top`, so a general-only template does not subsume an open specific bag). A specific-only template narrows the specific side and refuses nothing. A **path-dependent** template (one whose meaning depends on where it lands: `key()`, a reference) cannot be compared structurally: `sub_path_dependent_spread`, undecided. - Under the `gen` profile, `type`/`hide` marks must agree on corresponding nodes (`compat_marks_changed`). The `at` option anchors both documents at one path before comparing (the validation verb's `--at`); a path missing from either side is an `error` verdict. ### Default validity The relation also powers an advisory lint: the validation verb reports a `pref_not_instance` finding (severity `warning`, class `compat`) when a disjunction's effective default is not an instance of any **remaining** alternative. Under the admission gate this is no longer a soundness hole (the preferred branch contributes its own value to the admitted set, so `level: *wran | info | warn | debug` is a well-defined enum `{wran, info, warn, debug}` defaulting to `wran`) but that spelling is also exactly the shape of a *typo'd* default (`*warn` was probably meant), which nothing at meet time can distinguish. The warning flags the boundary: a default drawn from the written alternatives (`*8080 | integer`) is silent; a default that widens them is what the warning reports. Repeating the branch (`*warn | warn | error`) states "the default is a first-class member", silences the lint, and enforces the same admitted set. ## Errors Failures surface as messages (thrown as `AontuError` in TS, returned as `error` in Go): | Situation | Message (contains) | |------------------------|--------------------| | scalar conflict | `Cannot unify value: 2 with value: 1` | | kind conflict | `Cannot unify value: string with value: 1` | | cross-leaf conflict | `different kinds cannot unify` (`1 & 1.0`, `5 & 0d5`) | | nested conflict | reports the clashing leaf values | | unresolved reference | `Cannot resolve value: $.nope` | | unknown variable | `Cannot resolve …` | | extra key on closed | `closed` | | lossy integer literal | `not exactly representable`, plus the `0d` hint | | inexact integer sum | `exactly representable`, plus `0d` | | float mixed with exact | `cannot mix` (naming both leaves) | | over the exact budget | `exceeds the exactness budget`, `at most 4096` | | conflict marker left in | `conflict marker was found` (code `merge_conflict`) | | wrong argument count | `takes exactly one argument, but was given 2` (code `func_arity`) | | key or element with no value | `written with no value after the colon` (code `elided_value`) | **Every built-in has a fixed arity, checked at parse.** Nearly all take exactly one argument; the two exceptions are `key`, which takes none or one (how many levels up the path to read: none means the parent), and `neq`, which takes one or more exclusions. A wrong count is a mistake in the source and is refused before anything is evaluated. **An elided value is refused.** A key, element or spread written with nothing after its colon (`a:`, `a?:`, `[,]`, `[1,,2]`, `x:$obj&:`) is a mistake in the source rather than a null: writing it as a null made the mistake indistinguishable from a deliberate `a:null`. The error names the key or index, not the container, except for a spread, which has no key of its own and so refuses the map it belongs to. Three things that look similar are not elisions and keep working: an explicit `a:null`, a colon chain (`a: b:1`, whose value is the nested pair), and a trailing comma (`[1,]`, `{a:1,}`). A comma group and a written list are different counts: `upper("a","b")` is two arguments and is refused, while `upper(["a","b"])` is one: a list, which `upper` then refuses for its kind rather than its count. A **version-control conflict marker** is refused before the parse, with a code of its own, `merge_conflict`. A marker line would otherwise fall to the bare-string rule (`<` and `=` are punctuation outside any syntax) and be refused as a stray character, which says nothing about the merge that left it there. The match is git's exact shape: seven `<`, `=` or `>` at the start of a line, followed by the end of the line or a space before the branch label. A document may still write those characters anywhere else in a quoted string (`a:"<<<<<<<"`); a bare `a:<<<<<<` is refused, but as `bare_punct`, never as a conflict. In conflict messages the operand later in the source is named first ("…value: `` with value: ``") so the two sites are distinguishable. ## Grammars: `abnf()` and `parse()` `re()` is deliberately small: the portable pattern subset both engines agree on. Real formats are published as **grammars** rather than as regexes, and transcribing one into that subset is at best lossy. `abnf()` takes the grammar as written. **`abnf(g)` compiles an RFC 5234 grammar and answers its source**, so a parser is an ordinary string that canons, hashes and unifies like any other. The compile is what the call is for: a grammar that does not compile is refused where it is DECLARED, once, rather than at every site that parses with it. Write this as `grammar.aon`: ```aon G: abnf("v = n \".\" n\nn = 1*d\nd = %x30-39\n") a: parse($.G, "1.2") ``` ```sh $ aontu grammar.aon { "G": "v = n \".\" n\nn = 1*d\nd = %x30-39\n", "a": { "kids": [ { "kids": [], "rule": "d", "src": "1" }, { "kids": [ { "kids": [], "rule": "d", "src": "2" } ], "rule": "n", "src": "2" } ], "rule": "v", "src": "1.2" } } ``` **`parse(g, v)` answers the syntax tree** as ordinary maps and lists: `rule` names the production that matched, `src` the text it matched, and `kids` its children. `kids` is always present and always a list, so a schema written against the tree need not ask whether a leaf has the key. **A failure to parse is a failure to unify.** The call answers a refusal (`parse_failed`), so a field is refused rather than set to a value meaning "no". That is what lets a grammar act as a check: ```aon G: abnf("v = 1*d\nd = %x30-39\n") ok: parse($.G, "12") # the tree no: parse($.G, "x") # [aontu/parse_failed] ``` **`parse(g)` with no value is the grammar as a constraint**, which is what a schema position wants: there is no value there yet to hand the call. It is value-preserving, like every other atom in [the constraint algebra](#the-constraint-algebra): it admits a string the grammar accepts and answers that string, so it stays idempotent and order-independent under a meet, and a default can sit beside it. Write this as `check.aon`: ```aon G: abnf("v = 1*d\nd = %x30-39\n") tag: *""|parse($.G) ver: (*""|parse($.G)) & "12" ``` ```sh $ aontu check.aon { "G": "v = 1*d\nd = %x30-39\n", "tag": "", "ver": "12" } ``` `tag` takes its default because nothing met it; `ver` was written with `"12"`, the grammar accepts it, and the field keeps the string it was given. Written with `"xy"` instead, both branches fail and the disjunction is empty (`[aontu/empty]`). The tree is what the two-argument form is for: a constraint that also rewrote its value would have to carry the grammar that produced it for a second meet to mean anything, and nothing needs that yet. `aontu:system`'s `Semver` is the worked use: see [The `aontu:system` vocabulary](#the-aontusystem-vocabulary). Four things govern a grammar: - **Whitespace is not skipped.** The grammars run here describe strings with no spaces in them, so `1 . 2` does not parse as `1.2`. - **The empty string parses under no grammar.** The host engine answers an empty tree for empty input, which would make `parse(g, "")` succeed everywhere; it is refused instead. - **The parse is bounded** at 100 000 steps. A grammar needing more is refused rather than run, for the reason `re()` refuses a pattern that backtracks exponentially. - **`src` is the text the rule matched**, assembled from what the grammar consumed rather than sliced out of the input. - **A character class must not contain a literal used elsewhere.** Write `digit = "0" / positive-digit`, never `digit = %x30-39`, when `"0"` also appears on its own. Where a class overlaps a literal the class wins, and the literal's alternative silently becomes unreachable, so a grammar that looks right refuses input it names. The overlap rule is worth a moment, because a grammar that breaks it looks correct and fails on ordinary input. The no-leading-zero rule of a semantic version needs `"0"` as an alternative of its own: ```abnf numeric-identifier = "0" / positive-digit *digit positive-digit = %x31-39 digit = "0" / positive-digit ``` `digit` is spelled `"0" / positive-digit` rather than `%x30-39` so that a `0` is always the same token wherever it appears. Spell it as the class and `numeric-identifier`'s `"0"` branch is never reached, so `1.0.0` stops parsing while `1.2.3` still does. ### A grammar reads better in backticks A backtick string spans lines, so a grammar can be written as a grammar rather than as a run of escapes. Write this as `media.aon`: ```aon G: abnf( ` media = "@" type "/" sub type = 1*ALPHA sub = 1*ALPHA ALPHA = %x61-7A ` ) ok: "@text/plain" & parse($.G) ``` ```sh $ aontu media.aon { "G": "\nmedia = \"@\" type \"/\" sub\ntype = 1*ALPHA\nsub = 1*ALPHA\nALPHA = %x61-7A\n", "ok": "@text/plain" } ``` The leading newline is part of the string and costs nothing: a grammar is a list of rules, and ABNF ignores a blank line. The bundled [`aontu:system`](#the-aontusystem-vocabulary) model still spells its two grammars with `\n` escapes, because its text is held in a raw string literal in each port and a raw string cannot contain a backtick. ### A grammar can say what it builds The tree is the default, not the only answer. A **value annotation**, a trailing comment on a production, says what that rule should build instead. Write this as `build.aon`: ```aon G: hide( abnf( ` ver = maj "." min "." pat ; @object maj min pat maj = 1*DIGIT min = 1*DIGIT pat = 1*DIGIT DIGIT = %x30-39 ` ) ) v: parse($.G, "1.2.30") ``` ```sh $ aontu build.aon { "v": { "maj": "1", "min": "2", "pat": "30" } } ``` Nothing in `1.2.30` spells `maj`. The keys come from the comment, and `; @object` names one member per part of the rule that produces a value: a rule reference, a group or a repetition. A literal produces nothing and is never named, which is why `"."` is not a member and three references take three names. **`; @array` names nothing** and takes every part that produces a value as an element, in order. Shapes compose, because a part whose own rule is annotated is assigned whole. Write this as `list.aon`: ```aon G: hide( abnf( ` list = "[" entry *( "," entry ) "]" ; @array entry = key "=" val ; @object key val key = 1*ALPHA val = 1*DIGIT ALPHA = %x61-7A DIGIT = %x30-39 ` ) ) entries: parse($.G, "[width=10,height=20]") ``` ```sh $ aontu list.aon { "entries": [ { "key": "width", "val": "10" }, { "key": "height", "val": "20" } ] } ``` A repetition contributes one element per item, so a list comes out a list rather than the run's matched text. Five things to know before writing one: - **It is about the output, never the language.** A comment is the one place in RFC 5234 that carries no meaning of its own, so delete every annotation and the same inputs parse. You get the tree back. - **Every leaf is still text.** The annotation chooses the container, and there is no scalar form, so `"30"` is a string and stays one. - **The leading fold is answered, not removed.** Naming a member keeps it, so the `"v"` the next section needs is unnecessary here. Where the fold would erase a member's own built value the compile is REFUSED, with a diagnostic naming the rule and what to write instead. - **A rule that builds a value contributes no text** to whatever contains it. Mixing the two is supported; just do not read `src` on a node that contains an annotated rule. - **The refusals are deliberate.** More than one alternative, a member count that does not match the parts, a duplicate member name, and a leading member whose own rule builds a value are all refused where the grammar is declared rather than built into a differently shaped value. **Either builder nests inside the other.** An `@array` is a member of an `@object`, an element of another `@array`, or an object's only member, and answers the same value in both engines. ### Shaping an unannotated tree The answer is the RAW tree, so a document that wants natural structure builds it with the language's own verbs. Three do the work: [`pick`](#projecting-fields-pick) projects one field of every child, [`filter`](#selecting-filter-and-match) selects children by rule, and [`join`](#folding-to-a-string-join) folds a one-element selection back to a scalar. `hide()` keeps the grammar and the tree out of the generated document. Write this as `shape.aon`: ```aon G: hide( abnf( ` ver = "v" maj "." min "." pat maj = 1*DIGIT min = 1*DIGIT pat = 1*DIGIT DIGIT = %x30-39 ` ) ) t: hide(parse($.G, "v1.2.30")) parts: pick($.t.kids, src) names: pick($.t.kids, rule) minor: join(pick(filter($.t.kids, { rule:"min" }), src)) whole: $.t.src ``` ```sh $ aontu shape.aon { "minor": "2", "names": [ "maj", "min", "pat" ], "parts": [ "1", "2", "30" ], "whole": "v1.2.30" } ``` **A leading field loses its name**, and that is the one shape rule a grammar author has to know. The compiler folds a production's first element into the parent's node, so `ver = maj "." min "." pat` answers a first child named `DIGIT` where the version above answers `maj`. The fix is the `"v"` above: give the production a leading terminal and every field keeps its name. Both engines do this identically, so it is a property of the grammar compiler rather than a difference between the ports. Both limits belong to the tree, and the annotation above answers the first: name the members and the leading field keeps its name with no terminal in front of it. The second it does not answer. Every leaf is the **text** the rule matched, so `"30"` is a string under either spelling, and nothing turns it into `30`. ## The constraint algebra > All nine atoms (the bounds `min`/`max`/`above`/`below`, the > exclusion `neq`, the pattern `re`, the sizing atoms `length` and > `unique`, and the evaluate-only `must`) are implemented in both > engines over the four-leaf number tower, pinned by the > [`test/spec/constraint-*.tsv`](https://github.com/aontu-lang/aontu/blob/main/test/spec/) suites. Violations > raise the registered `constraint` code, and a pattern outside the > portable subset raises `constraint_pattern`. Known limit: a > preference meeting a constraint in a CONJUNCT (`min(1024) & *8080`) > does not resolve to the default: use the disjunct form > (`*8080 | (integer & min(1024))`). Under the admission gate > the disjunct form also ENFORCES on override: an > out-of-bound peer is refused rather than silently bypassing the > constraint branch, so the recommended spelling both defaults and > validates. ### Vocabulary Nine builtins join the function registry. Eight are **Band A**: full lattice citizens with defined meet, emptiness, subsumption, and canonical form. One is **Band B**: evaluate-only, and reported as such. There is no new grammar: atoms are ordinary functions. | Atom | Band | Meaning | |------|------|---------| | `min(n: number\|string) : constraint` | A | value ≥ x (numeric, or string with lexical order) | | `max(n: number\|string) : constraint` | A | value ≤ x | | `above(n: number\|string) : constraint` | A | value > x | | `below(n: number\|string) : constraint` | A | value < x | | `neq(...vals: number\|string) : constraint` | A | value is none of the listed scalars (leaf-aware) | | `re(text p: string) : constraint` | A | string matches pattern p (unanchored, portable subset) | | `length(n: number\|constraint) : constraint` | A | length/count satisfies integer constraint c | | `unique(projector k?: string) : constraint` | A | members pairwise distinct (list elements, map values) | | `must(trial c: any, text msg: string) : constraint` | B | evaluate-only check with an author message | ### Bounds and the number tower Three rulings, each forced by the tower's disjoint leaves (`integer`, `float`, `biginteger`, `bigdecimal` under the pure supertype `number`): 1. **Order is a property of the number line, not the leaf.** A numeric bound constrains the value's mathematical position and is satisfied by ANY numeric leaf at an admissible position: `min(0) & 0d5` is `0d5`, `above(1) & 1.5` is `1.5`. Comparison is exact across leaves: every binary64 is exactly a rational, so a `float` compares with an exact decimal without rounding, in both implementations. A numeric bound implies the kind `number` (the supertype); it never narrows the peer's leaf. 2. **Endpoints keep their written leaf.** Canon round-trips kind (rule R4), so `min(1)`, `min(1.0)` and `min(0d1)` are distinct canonical texts denoting the same bound point. When two endpoints at the SAME point meet (`min(1) & min(1.0)`), the survivor is the one whose leaf sits lowest in the tower order `integer < float < biginteger < bigdecimal`: a deterministic choice both implementations make identically. 3. **`neq` excludes by scalar identity (leaf and value**) because that is what scalar identity means in the lattice (`1 & 1.0` is a conflict; `1|1.0` keeps both alternatives). `neq(1)` excludes the integer `1` and admits the float `1.0`. To exclude a point on the whole number line, list its leaves: `neq(1, 1.0)` (the exact leaves are opt-in, so `0d`-free documents need only these two). String bounds (`min("a")`) use lexical code-point order and imply `string`. Mixing domains in one meet (`min(0) & min("a")`) is empty and yields nil. ### The meet `atom & atom` (same domain) is symbolic: decided at schema-composition time, before any data arrives: | Meet | Result | |------|--------| | interval & interval | intersection: `min(0) & min(5)` → `min(5)`; `min(2) & max(10) & max(7)` → `min(2)&max(7)` | | `neq` & `neq` | exclusion-set union, arguments sorted | | `re` & `re` | regex-set accumulation (patterns sorted; never simplified) | | `length(c1)` & `length(c2)` | `length(c1 & c2)`: the count atom reuses the numeric algebra recursively | | bound & kind | domain narrowing: `integer & min(0)` keeps both (interval gains the integral-domain flag); `number & min(0)` keeps `min(0)` (already implied); `string & min(0)` → nil | | bound & concrete scalar | membership by exact comparison → the scalar, or a two-site nil | | bound & `must` | both kept; `must` stays opaque | Meets are commutative and idempotent by construction (normalisation, not term order, defines the result) so the lattice guarantee is preserved. ### Emptiness Decided **eagerly at unification time** where it is exact, and never guessed where it is not: - Empty interval: `min(5) & max(3)` → nil, both sites reported. - Integral gap: an integral-domain interval containing no integral value: `integer & above(1) & below(2)` → nil. (Applies when the domain is narrowed by `integer` or `biginteger`.) - Point deletion **requires a narrowed leaf**: `min(3) & max(3)` admits the point 3 in any numeric leaf, so `neq(3)` (which excludes only the integer `3`) does NOT empty it, but `integer & min(3) & max(3) & neq(3)` → nil. This is the tower re-derivation of the pre-tower example, and the spec rows pin both directions. - `length(c)` is empty iff `c & integer & min(0)` is. - Regex emptiness is deliberately approximate: distinct `re` atoms accumulate and are never declared empty: sound (no false conflicts), incomplete (some contradictions surface only against data). ### Subsumption *The `subsume` query implements this table in both engines (its per-former rules are in [Subsumption](#subsumption) above). One mapping to note: the query answers the `must` row's "never" as `undecided` with reason `sub_evaluate_only`: the admitted set is opaque, which is undecided rather than refused.* `A ⊒ B` ("A subsumes B", B is an instance of A) holds when **every value B admits, A admits too**. It is the lattice's own order, and for this algebra it is decided per atom family rather than by search. Three properties make it useful: it is reflexive (`A ⊒ A`), transitive, and `A ⊒ B` exactly when `A & B` is `B`, so an implementation has a free cross-check against the meet table. **Soundness before completeness.** Where a rule below cannot decide, the answer is **not subsumed**, never a guess. That direction is the safe one for the `subsume` query built on it: a compatibility check that wrongly reports "breaking" costs a reviewer a second look, while one that wrongly reports "compatible" ships the break. Two rules are approximate in this sense and are marked; the rest are exact. | A (general) | B (specific) | A ⊒ B when | |-------------|--------------|------------| | no kind | any | always: an unnarrowed residual admits every leaf its domain has | | `number` | any numeric leaf, or a numeric residual | always: the supertype admits every leaf | | leaf `k` | leaf `k'` | `k == k'`; distinct leaves are disjoint, so neither subsumes the other | | interval | interval | A's interval contains B's: A's lower endpoint is at or below B's, A's upper at or above, and where endpoints coincide A's may not be the open one | | interval | concrete scalar | the scalar is admitted by A (the membership rule of the meet) | | no bound on a side | any | an absent endpoint is ±∞ and contains everything | | `neq(S)` | `neq(T)` | `S ⊆ T`: excluding *fewer* values is more general. `neq(1) ⊒ neq(1,2)` | | `neq(S)` | concrete scalar | the scalar is in neither S nor excluded by A's other atoms | | `re(P)` | `re(Q)` | **approximate**: `P ⊆ Q` as a *set of pattern strings*. Adding a pattern narrows, so `re("a") ⊒ re("a")&re("b")` | | `length(c)` | `length(d)` | `c ⊒ d`, recursively: the count atom reuses this same table over the integer domain | | absent `length`/`unique` | present | always: an unsized residual admits every size | | `unique(k)` | `unique()` | always (reflexive); nothing else subsumes or is subsumed by it | | `must(f)` | anything | **never**: a Band B predicate is opaque, so A's admitted set is unknown | | anything | `must(…)` | decided by A's other atoms alone; an extra `must` on B can only narrow B | | anything | nil (empty) | always: the empty set is an instance of everything | A whole residual subsumes another when **every** row above holds for the corresponding atom families, and the domains agree (a numeric residual never subsumes a string one, or a container one). **Why the two approximations are where they are.** `re` compares patterns as *text* because deciding that `^a` admits everything `^ab` admits is regex containment, which this algebra deliberately does not do: the same ruling that stops two `re` atoms being declared empty at composition time. `must` is opaque by construction: that is what Band B *means*. In both cases the answer is "not subsumed", so the error is always toward reporting a difference that is not there. **Normalisation makes the spelling irrelevant.** Subsumption is decided over the *normalised* residual, so two spellings of one constraint subsume each other in both directions. `min(0)&max(10)` and `max(10)&min(0)` normalise identically, and the canonical atom order below is what makes that true by construction rather than by a special case. ### Endpoint tightening: lazy endpoints, eager emptiness The pre-tower draft left open whether `integer & above(0.5)` should rewrite to `integer&min(1)`. **Decided: no endpoint rewriting.** Under the tower, a synthesised endpoint must be given a leaf the author never wrote (`1`? `0d1`?), and that invented spelling leaks into canonical text and, later, canon hashes. Emptiness needs no synthesis, so the algebra keeps *eager emptiness* (the composition-time contradiction detection that is the point of Band A) with *lazy endpoints* (canon stays what was written, normalised only by the meet rules above). ### Canonical form A residual constraint renders as its normalised atoms joined by `&` in a fixed order (**kind, lower bound (`min`/`above`), upper bound (`max`/`below`), `neq` (arguments sorted), `re` (patterns sorted), `length`, `unique`, `must`**) no spaces, reparseable, endpoint leaves preserved: ```aon a: integer & max(10) & min(0) & min(2) # canon: {"a":integer&min(2)&max(10)} ``` `parse(canon(v)) == v` holds for every atom and every normalisation rule: the reparse produces a conjunct of atoms that normalises back to the identical residual. Spec rows pin a round-trip and an order-independence case (`min(0)&max(10)` vs `max(10)&min(0)` → identical canon) for each rule. Two renderings follow from that round trip rather than from taste: - **`length`'s argument renders unabridged**, implied parts and all: `length(3)` canonicalises to `length(integer&min(3)&max(3))`, because that *is* the residual the count must satisfy (`length(c)` always meets `integer & min(0)`; see [`length` semantics](#length-semantics)). Abbreviating it would mean a second set of rules for when the implied parts may be dropped, and canon is a normal form ([`aontu hash`](/docs/reference-api#aontu-hash) digests it) not a pretty-printer. - **A bare domain is spelled out when nothing implies it.** An order atom's argument names its own domain, so `min(2)` need not say `number`. A sizing residual carries no order, so `string & length(3)` renders as `string&length(...)`: drop the `string` and the reparse would admit lists and maps of three members too. ### `re` and the portable pattern subset `re(p)` admits a string matching `p`. Matching is **unanchored** in both implementations, so `re("el")` admits `"hello"`; anchor with `^` and `$` to constrain the whole string. The string kind is implied, so `string & re("x")` canonicalises to `re("x")`: the same rule that makes `number & min(0)` canonicalise to `min(0)`. A pattern must mean the same thing in both implementations **and cost about the same to evaluate**, and the two host regex engines guarantee neither: TypeScript compiles with JavaScript's backtracking `RegExp`, Go with RE2: a different language, in a different complexity class, over a different alphabet. aontu therefore **defines** the pattern language and rewrites your pattern into a form neither engine can read two ways. Only the rewritten form reaches a host engine. **What `re` accepts** | | | |---|---| | literals | `a`, and `\` before any of `. \ + * ? ( ) [ ] { } \| ^ $ /` to mean it literally; `\xHH` | | classes | `[abc]`, `[^abc]`, `[a-z]`; `\-` inside a class for a literal hyphen | | abbreviations | `\d \D \w \W \s \S` and `.` | | repetition | `*` `+` `?` `{n}` `{n,}` `{n,m}` with every count **1000 or less**, and the lazy forms `*?` `+?` `??` | | grouping | `(…)`, `(?:…)`, alternation `a|b` | | anchors | `^` `$` `\A` `\z` `\b` `\B` | | control | `\t \n \r \f \v` | **aontu defines the abbreviations**, and inherits neither host's: | written | means | |---|---| | `\d` / `\D` | `[0-9]` / `[^0-9]` | | `\w` / `\W` | `[0-9A-Za-z_]` / `[^0-9A-Za-z_]` | | `\s` / `\S` | `[ \t\n\r\f\v]` / `[^ \t\n\r\f\v]` | | `.` | `[^\n]` | | `\A` / `\z` | `^` / `$` | These are the small ASCII sets deliberately. **`\s` is those six characters only**: it does *not* match U+00A0 or the other Unicode spaces, though JavaScript's `\s` does, because a non-breaking space in a config value is a mistake worth catching rather than a space worth accepting in silence. Matching counts **code points**, not UTF-16 code units, in both implementations. **What `re` refuses**, and why rewriting cannot help: | Construct | Why | |-----------|-----| | backreferences `\1`–`\9`, `\k` | RE2 has no equivalent, and a pattern using one is not a regular expression at all | | lookaround `(?=)` `(?!)` `(?<=)` `(?` in RE2 and `(?` in JavaScript; inline flags change the meaning of everything after them | | `\p{…}`, `\x{…}`, `\u`, `\Z` | spelled differently, or read as a literal by one engine | | POSIX classes `[[:alpha:]]` | RE2 only | | empty classes `[]`, `[^]` | a never-matching class in JavaScript, a parse error in RE2 | | a repeat count above **1000** (`a{1001}`, `a{2,1001}`) | RE2 refuses to compile it and JavaScript accepts it, so the same schema was valid in one implementation and not the other. The bound is **aontu's**, checked in the normaliser before either engine sees the pattern, which is why the refusal is the same in both | | a quantifier applied to `^`, `$`, `\b` or `\B` | there is nothing to repeat: JavaScript under the `u` flag calls it a syntax error, RE2 quantifies the assertion and matches | | a `{` that opens no counted quantifier (`x{y}`), or a `}` that closes none | JavaScript reads each as a lone quantifier bracket and refuses; RE2 reads both as literals | | a quantifier on a group containing a quantifier or an alternation | **cost, not meaning**: see below | The last one is different in kind. `(a+)+$` against twenty-nine `a`s and a `!` takes **45 seconds** in JavaScript and 0.065s under RE2, growing exponentially; a regex match is counted by no evaluator budget ([the trust contract](/docs/trust), clause 2), so without this rule an untrusted schema could stall the TypeScript evaluator indefinitely. Rewriting cannot fix a complexity difference, so this one is refused rather than normalised. `(?:a|b)+` is caught by it too, though it is safe: deciding that two alternation branches cannot both match is real work. Write `[ab]+`. Unquantified groups, top-level alternation, `(?:ab)+`, `(a)(b)` and `(a)+` all pass, and a quantifier inside a character class is a literal character (`[a+]+` is fine). The refusal message names the offending construct *and* restates this whole table, so an author never has to find this page to recover. Patterns **accumulate** and are never simplified: `re("x") & re("a")` keeps both (sorted by pattern text in canon), and a value must match every one. Two `re` atoms are never declared empty at composition time, because deciding that one pattern excludes another is regex containment, which this algebra deliberately does not do. A contradiction between patterns therefore surfaces against data, not against the schema. Canon renders the pattern **as written**, never the rewritten form: canon round-trips source, and the semantic hash ([`aontu hash`](/docs/reference-api#aontu-hash)) is taken over canon. ### `length` semantics `length` applies to strings, lists, and maps, with the domain fixed by the peer: - **strings**: length in **Unicode code points**: not UTF-16 code units (TS's native count) and not bytes (Go's): `length(1) & "𝄞"` holds, in both implementations. Astral-plane rows are part of the spec suite, not an implementation accident. - **lists**: element count. **maps**: entry count. Its argument is any integer-domain constraint: `length(3)` means exactly 3; `length(min(2) & max(5))` means between 2 and 5. Every argument meets `integer & min(0)` (a count is a non-negative whole number) which is what makes `length(max(-1))` and `length(1.5)` empty on their own, and what canon renders. Like every other atom's argument, it **residuates** until it settles: `length($.n)` waits for `$.n`, then checks the count. Only a *settled* argument of the wrong shape (a string, a boolean, a contradictory kind) is refused. A sizing residual has **no domain of its own** (a count says nothing about what is counted) so meeting a kind *sets* one rather than merely agreeing with it. `string & length(3)` is a three-character string, and `number & length(3)` is empty, because a number has neither a length nor members. `min(2) & unique()` and `re("^a") & unique()` are empty for the same reason. **`length` counts what generates.** An optional key that never resolves is dropped at generation, so it does not count. The constraint is a claim about the data, and the data is what comes out: ```aon a: string & length(3) a: abc b: length(1) & { x:1 y?:number } ``` ```json {"a":"abc","b":{"x":1}} ``` `b` holds because the generated value is `{"x":1}`: one member. **When the count is decided.** An optional key **survives unification carrying its unresolved value** (`{x:1, y?:number}` canonicalises as `{"x":1,"y"?:number}`) and is dropped only in generation (`BagVal.gen`). It is tempting to conclude that the count is therefore unknowable until generation, and that `length` must wait for a drop. It must not: nothing in the fixpoint performs that drop, so an atom waiting for it waits forever. The count is knowable earlier, because *whether a member will generate* is decided before generation runs. A member is skipped by generation when it carries a `type` or `hide` mark, or when it is an optional key whose value cannot generate. So: - **Every optional child settled**: this includes `{x:1, y?:number}`, where the map converges immediately and `y` holds an unresolved kind. The count is known, and `length` decides at composition time like every other atom, `length(1) & {x:1, y?:number}` included. - **Some optional child still converging**: `{x:1, y?:$.z}` before `z` resolves, where the child's fate genuinely is not yet decided. `length` **residuates**: it stays in place and is retried, exactly as an arithmetic operator with a non-concrete operand does. So `length` is eager in the ordinary case and defers only where the answer is not yet determined, which is the same discipline every other deferring value in the language follows. What is never deferred is the atom's own arithmetic: `length(min(5) & max(3))` is empty at composition time whatever map it meets, because the inner interval is empty on its own. ### Sizing atoms fold last There is one more rule the sizing atoms need, and it is not shared with the order atoms: **`length` and `unique` are the last terms of a conjunct to fold.** An order atom may decide the moment it meets a scalar, because meeting further scalars can only narrow: `min(2) & 1 & 2` is a conflict however it is grouped. A sizing atom cannot, because meeting further containers *grows* the member set: ```aon a: length(2) a: { x:1 y:2 } ``` ```json {"a":{"x":1,"y":2}} ``` Layering fragments like this is the point of the language, and an atom that folded early would count `{x:1}` alone and refuse it. So the two kinds of atom take different slots in the conjunct sort order (`cjo`): the order atoms fold before containers, the sizing atoms after every value that could contribute a member. The size is then read once, from the merged container. Written order does not matter (`a: {x:1} a: {y:2} a: length(2)` is the same value) which is the property the sort order exists to guarantee. **`must` folds last for the same reason**, and the slot is named for what the three atoms share rather than for sizing alone: `length`, `unique` and `must` all need the *whole* value. An evaluate-only check run against the first fragment would refuse `a: must(length(2),m)` / `a: {x:1}` / `a: {y:2}` on a count of one, exactly as an early-folding `length` would. **And "last" reaches past the document.** Sorting the atom to the end of its conjunct is only half the rule, because a container can settle in one document and still gain members from another: the data half of a [`vet`](/docs/reference-api#aontu-vet) meet, an [`@` include](#source-loading-), a later [`pack`](#generating-children-pack-and-each). An atom that decided when its own conjunct settled decided too early there, and `vet` then reported `valid` for data the evaluator refuses. So a sizing verdict is taken only when **more members cannot change it**: members accumulate under unification, they are never removed: | reading | permanent? | what happens | |---|---|---| | an upper bound **violated** | yes: more members only add | refuse now | | an upper bound **satisfied** | no | the atom stays on the value | | a lower bound **satisfied** | yes | that reading is spent | | a lower bound **violated** | no | the atom stays on the value | | a **duplicate** found | yes | refuse now | | distinctness **so far** | no | the atom stays on the value | Anything provisional **residuates**, exactly as an atom over a container that has not settled does, and is decided at **generation**, which is where nothing more can arrive. So `length(min(1)) & {&: {r: integer}}` no longer refuses the schema it was written for, and `length(max(2)) & {&: {r: integer}}` no longer passes three records. A residuated atom is visible in [canon](#canonical-form), which is the correct rendering: the value really does still carry the constraint. ### `unique` semantics `unique()` holds when the members of a container are **pairwise distinct**, compared by **canonical form**: two members are the same member exactly when their canons are equal. Canon is the right yardstick because it is already this language's normal form for "the same value": `ConstraintVal.same` compares canons, and `DisjunctVal` deduplicates members that way. It is deterministic, byte-identical across the two implementations (every `canon` spec row pins that), and it is defined for *every* value, which scalar identity is not. For scalar members it reduces exactly to scalar identity (leaf *and* value) because canon round-trips kind: `1` and `1.0` canon differently, so `[1, 1.0]` is distinct under the number tower, exactly as `1 & 1.0` is a conflict. For **container** members it gives structural equality without a separate rule: `[{x:1},{x:1}]` is not unique, because both elements canon as `{"x":1}`, and `[{x:1},{x:2}]` is. It applies to two shapes: - **lists**: the elements are pairwise distinct. - **maps**: the entry *values* are pairwise distinct. (Keys are distinct by construction, so there is nothing to check there.) Any other peer (a string, a number, a boolean, `null`) is a domain conflict: no scalar has members. The members it does compare are the members that *generate*, the same set `length` counts, so a `hide`n entry and a dropped optional are not members here either. **`unique(k)` is uniqueness by projection.** "No two services share a port" compares one field of each member rather than the whole member, and the atom's single argument is that projector: the arity was reserved for it, and is now spent: ```aon services: unique(port) & { api: { port:8080 name:"api" } auth: { port:8443 name:"auth" } } ``` ```json {"services": { "api": {"port": 8080, "name": "api"}, "auth": {"port": 8443, "name": "auth"}}} ``` A member with no such key **fails** rather than being skipped: distinctness that cannot be shown is distinctness the collection does not have, and skipping would let one keyless record hide a duplicate. A member that is not a map fails for the same reason: it has no key to project. `unique(a) & unique(b)` demands **both**; the keys accumulate rather than the later one replacing the earlier, since each names a different axis of distinctness and dropping either would silently weaken the constraint. Canon renders them sorted after the bare atom (`unique()&unique("a")&unique("b")`), so two documents saying the same thing render the same string. In subsumption, a general `unique(k)` needs the same key on the specific side (distinctness on `port` says nothing about distinctness on `name`) while a specific that adds a key still subsumes, because more distinctness is narrower. ### Cross-field bounds and residuation An atom whose argument contains an unresolved reference, or whose peer is not yet concrete, **residuates**: no error, stays in place, re-evaluated on later fixpoint passes. Atoms only ever suspend or intersect (never force evaluation) so evaluation order cannot change results. ```aon scaling: floor: 2 scaling: ceiling: 10 scaling: target: integer & min($.scaling.floor) & max($.scaling.ceiling) # target normalises to integer&min(2)&max(10) once floor/ceiling resolve ``` A residual that survives to generation is an error, exactly like an unresolved kind today; exhaustion of the pass budget while residuals are still refining is `budget_passes` ([the trust contract](/docs/trust), clause 2). ### Band B: `must` `must(c, msg)` wraps any aontu value as an evaluate-only check: it residuates until its peer is concrete, then requires the peer to unify with `c`; on failure the author's message is attached to the nil (`NilVal.details`). `must` never participates in emptiness or subsumption, and any report including one states that the check was evaluate-only: the channel for domain rules beyond the algebra. ### Errors A constraint violation is an ordinary two-site nil in the existing message family (`Cannot unify value: 99999 with value: max(65535)`), with machine-readable `details`: the failing atom, the normalised admissible interval/sets, and any `must` message. Codes ride the [error-code registry](https://github.com/aontu-lang/aontu/blob/main/test/spec/errcodes.tsv); rendering into reports belongs to [`aontu vet`](/docs/reference-api#aontu-vet). ### Named constraint aliases The algebra has no `int8`, `uint16` or `port` keyword, and does not need one. A constraint is an ordinary value, so a name for one is an ordinary field, and a `type()`-marked block gives you a library of them that unifies like everything else and emits nothing. This section names constraints by their **path** (`$.type.port`). For the name-only spelling, `%port`, see [Aliases `%`](#aliases-); the two are the same idea reached two ways, and a `%` alias may hold a constraint just as a `type()` field can: ```aon type: type({}) type: { uint8: integer & min(0) & max(255) int8: integer & min(-128) & max(127) port: integer & min(1) & max(65535) } listen: $.type.port listen: 8080 ``` ```json { "listen": 8080 } ``` Three properties make this work, and all three are rules stated elsewhere in this document rather than anything special to constraints: - **The block is schema, so it does not generate.** `type()` marks its value as metadata, and a map field whose value is type-marked is omitted from the enclosing map ([Marks](#marks-type-and-hide)). The aliases are present for unification and absent from output. - **A reference copies with the marks cleared.** `$.type.port` lands on a type-marked value and yields an unmarked one, so `listen` emits normally. - **The alias is a constraint, not a value**, so it meets the concrete value at the referring field exactly as if it had been written there. The key name is not reserved: `type` above is a field called `type` that happens to be `type()`-marked. `defs`, `schema` or anything else reads the same to the engine. An out-of-range value is refused at the field that holds it. Write this as `uint8.aon`: ```aon type: type({}) type: uint8: integer & min(0) & max(255) a: $.type.uint8 a: 300 ``` ```sh $ aontu uint8.aon [aontu/constraint]: Cannot unify values at path $.a ... $ echo $? 1 ``` `300` does not satisfy `max(255)`. **Name the kind as well as the bounds.** `min(0) & max(255)` alone is a bound on *numbers*, so `1.5` satisfies it; a sized integer is `integer & min(0) & max(255)`. This is the one mistake the idiom invites, and the reason the aliases above all lead with `integer`: ```aon loose: type({}) loose: byteish: min(0) & max(255) a: $.loose.byteish a: 1.5 ``` ```json { "a": 1.5 } ``` Because an alias is a value, the aliases compose: one can be written in terms of another, and a reference to an alias may be met with further constraints at the point of use. ```aon type: type({}) type: { n:integer & min(0) u8:$.type.n & max(255) } small: $.type.u8 & max(15) small: 12 ``` ```json { "small": 12 } ``` `u8` is written in terms of `n`, and `small` narrows `u8` again where it is used. Nothing here is special to constraints: it is the meet, applied to values that happen to be constraints. A value that violates the composition is refused against the whole residual, not against whichever atom noticed first: ``` Cannot unify value: 20 with value: integer&min(0)&max(15) ``` `max(255)` is absent because `max(15)` subsumes it, and `integer` and `min(0)` are present because `20` still has to satisfy them. That normalised form is what `vet --format json` reports as `expected`, and what the value's [canon](#canonical-form) states. --- # Generation reference _The component tree: every component node, its props and the children it admits, and what aontu render and aontu trace do with it._ Source: https://github.com/aontu-lang/aontu/blob/main/docs/reference-generation.md · Page: https://aontu.dev/docs/reference-generation A component tree is the value a generator answers: nested nodes that name every output file, every folder above it, and every span of text inside it. This page is normative for the tree's shape, for each component's props and admitted children, and for what each verb writes when handed one. The constructs that *compute* a tree are specified in the language reference and not here: [Generating children: `pack` and `each`](/docs/reference-language#generating-children-pack-and-each), [Selecting: `filter` and `match`](/docs/reference-language#selecting-filter-and-match), [The placeholder `_`](/docs/reference-language#the-placeholder-_), [Transforming: `emit`](/docs/reference-language#transforming-emit), and [Generation](/docs/reference-language#generation) for what `generate` requires of a model. Each component function's one-entry summary is in that page's [Functions](/docs/reference-language#functions) index, and the arity and argument modes of every name are in [The call surface](/docs/reference-functions#the-call-surface). The option lists and synopses for the verbs belong to the API reference, under [`aontu render`](/docs/reference-api#aontu-render) and [`aontu trace`](/docs/reference-api#aontu-trace), and the error codes named below to [The codes](/docs/reference-errors#the-codes). Behaviour stated here is pinned by [`test/spec/cmp.tsv`](https://github.com/aontu-lang/aontu/blob/main/test/spec/cmp.tsv) and [`test/spec/trace.tsv`](https://github.com/aontu-lang/aontu/blob/main/test/spec/trace.tsv), which both implementations run. ## Contents - [The component tree](#the-component-tree) - [The components](#the-components) - [What `aontu render` writes](#what-aontu-render-writes) - [What `aontu trace` reports](#what-aontu-trace-reports) - [Order and determinism](#order-and-determinism) - [Related](#related) --- ## The component tree Every component function answers a map of exactly three keys: `cmp`, the string a generator runtime looks the component up by; `props`, a map holding that component's declared props; and `children`, a list of further nodes. A node built by a component function is [closed](/docs/reference-language#closed-values-close--open), so a fourth key is a `closed` refusal rather than an ignored field: ```aon out: file("a.txt", ["x" ["y" "z"]]) ``` ```json { "out": { "children": [ { "children": [], "cmp": "Line", "props": { "src": "x" } }, { "children": [], "cmp": "Line", "props": { "src": "y" } }, { "children": [], "cmp": "Line", "props": { "src": "z" } } ], "cmp": "File", "props": { "name": "a.txt" } } } ``` **A bare string child is a `line`**, terminator included, which is what a template body line desugars to; an explicit `content()` is still a span with no terminator (`cmp-bare-string-child`, `cmp-bare-string-spliced`). The conversion happens only where `line` is an admitted child, so `file`, `fragment`, `slot`, `inject`, and `listitems` take a bare string and `folder` and `project` refuse one (`cmp-bare-string-needs-line`, `cmp-bare-string-not-in-project`). The children argument is flattened in place, recursively (`cmp-children-splice`), under the rule [Transforming: `emit`](/docs/reference-language#transforming-emit) states for a body. A node is an ordinary map addressed by path, printing its keys in the [order every value prints in](#order-and-determinism). Write a `tree.aon`: ```aon out: project(".", [folder("src", [file("main.js", ["const x = 1"])])]) ``` and address a node inside it by path: ```sh $ aontu model get '$.out.children.0.cmp' tree.aon "Folder" $ aontu model get '$.out.children.0.children.0.children.0' tree.aon { "children": [], "cmp": "Line", "props": { "src": "const x = 1" } } ``` [Generate code from a model](/how-to/generate-code) reads a whole tree with the same verb. **The prop schema is enforced by the component function, not by the tree.** A hand-written map carrying a `cmp` key that names a component is admitted as a child, and its props are never checked against the schema below: an undeclared prop reaches the runtime, where eight of the ten components drop it in silence. A prop a component does not declare is refused at the call. ## The components Ten functions build nodes. A leaf handed a children list is a `func_arity` error ([Class `parse`](/docs/reference-errors#class-parse)) rather than a bad argument (`cmp-leaf-takes-no-children`). The shorthand column names the prop a bare string spec sets. | signature | node | shorthand | children | props | |---|---|---|---|---| | `project(spec?: string\|map, children?: list) : map` | `Project` | `folder`, optional | `project`, `folder`, `file`, `copyfiles` | `name`, `folder` | | `folder(spec: string\|map, children?: list) : map` | `Folder` | `name` | `folder`, `file`, `copyfiles` | `name` | | `file(spec: string\|map, children?: list) : map` | `File` | `name` | `content`, `line`, `fragment`, `inject`, `listitems`, `copyfiles` | `name`, `exclude`, `mode` | | `content(spec: string\|map) : map` | `Content` | `src`, a span | none | `arg`, `src`, `name`, `indent`, `extra`, `replace`, `raw` | | `line(spec: string\|map) : map` | `Line` | `src`, a span | none | `arg`, `src`, `name`, `indent`, `extra`, `replace`, `raw` | | `fragment(spec: string\|map, children?: list) : map` | `Fragment` | `from` | `slot`, `content`, `line`, `listitems` | `from`, `indent`, `replace`, `eject` | | `slot(spec: string\|map, children?: list) : map` | `Slot` | `name` | `content`, `line`, `fragment`, `listitems` | `name` | | `inject(spec: string\|map, children?: list) : map` | `Inject` | `name` | `content`, `line`, `listitems` | `name`, `markers`, `exclude` | | `copyfiles(spec: string\|map) : map` | `CopyFiles` | `from` | none | `from`, `to`, `replace`, `exclude` | | `listitems(spec: map, children?: list) : map` | `ListItems` | none | `content`, `line`, `fragment` | `item`, `line`, `indent` | Every bad call answers the same error code, `invalid-arg` ([Class `conflict`](/docs/reference-errors#class-conflict)). The message's attempt word is the offending name rather than a fixed verb, and it is one of five: - the prop that is not declared; - the text prop's own name, where that prop is required and missing, or is present and is not a non-empty string, so `project({folder: 1})` and `project({folder: ""})` both answer `Cannot folder`; - `item`, where a `listitems` bag is absent or is not a list, so `listitems({})` and `listitems({item: "a"})` both answer `Cannot item`; - `children`, for a child the component does not admit; - `spec`, where the argument is not a form the component's spec takes, which covers the string `listitems("a")`. Where more than one prop is wrong, the one named is the **first written**, not the first in sorted order, and both ports walk the written key list to keep that promise (`cmp-props-names-first-written`). A child the component does not admit: ```sh $ echo 'out: folder("src", [line("x")])' | aontu [aontu/invalid-arg]: Cannot children values at path $.out ... $ echo $? 1 ``` What each component adds to the table: - **`project`**: the one component whose text prop is optional, so `project()` answers a node with empty props. A `folder` written as anything but a non-empty string is refused by that name (`cmp-project-folder-kind`, `cmp-project-folder-not-empty`). - **`folder`**: `name` is required and non-empty (`cmp-folder-needs-a-name`, `cmp-folder-name-not-empty`). - **`file`**: `name` is required and may hold `/`, and the folders on the way are made. `exclude: true` leaves the file alone when it is already there, and is the only form each runtime honours: a path or a list of paths is matched against the COMPONENT path rather than the output path, and the Go runtime skips nothing for either. `mode` is the permission bits as a number: `mode: 493` and `mode: 0o755` are the same value, both spellings being [numeric literals](/docs/reference-language#lexical-structure), and `test/spec/cmp.tsv` writes `mode: 420` for `0o644`. - **`content`**: the text prop may be the empty string, so `content("")` is a node where `folder("")` is a refusal. The text is taken from `arg`, then `src`, then a string child, and a node holding none of the three writes nothing. `indent` is a count of spaces or a literal prefix. - **`line`**: the same seven props as `content`, from the same alias. The name is also a `listitems` prop, which is a different thing spelled the same way. - **`fragment`**: `from` resolves against the output folder. `eject` names a start and end marker pair, and only the region between them is read. The source file must exist. `fragment` and `copyfiles` are the two components whose props the runtime validates against a closed set, so an undeclared prop on a hand-written `Fragment` or `CopyFiles` node is refused by name at render time rather than dropped, and `eject` is refused unless it is a list of two markers: `eject: true` builds a node (`cmp-props-fragment-all`) and does not render. - **`slot`**: `name` is required and non-empty, and fills the `<[SLOT:name]>` marker of the enclosing `fragment`. The unnamed `<[SLOT]>` marker takes that fragment's non-`slot` children instead, so a bare string, `content`, `line`, or `listitems` written directly under a `fragment` lands there; a `fragment` carrying such children whose source has no unnamed marker is a refusal at exit 2. - **`inject`**: the default marker pair is `#--START--#` followed by a newline, and a newline followed by `#--END--#`. A `markers` pair with exactly one empty member is refused at render, and both empty reads as unset. `inject` rewrites a file that already exists and never creates one, so a missing target is a refusal at exit 2. - **`copyfiles`**: `from` resolves against the process working directory. `replace` substitutes in copied text, and a binary file is copied through unchanged. This `replace`, here and in the span set, is a substitution the runtime applies, and not [`emit`'s `replace` key](/docs/reference-language#replacing-text-in-a-body-replace-and-esc). - **`listitems`**: the one component with no text prop, so it takes no bare string spec (`cmp-listitems-takes-no-string`), and the one with a bag: `item` must be present and must be a list, whatever else it holds (`cmp-listitems-needs-item`, `cmp-listitems-item-is-a-list`). `line` here is a prop and not the component of that name. ## What `aontu render` writes Per node, on disk: | node | effect | |---|---| | `Project` | Joins `folder` under the run's output folder. `name` writes nothing. | | `Folder` | One or more path segments below the enclosing folder, so `folder("a/b")` is two of them. | | `File` | One output file at `name` below the enclosing folder, with `mode` and `exclude` applied. | | `Content` | A span of text, with no terminator. | | `Line` | The same span, with a newline after it. | | `Fragment` | The file at `from`, resolved against the output folder, with its markers filled by the slots beneath. | | `Slot` | One marker of the enclosing `Fragment`. | | `Inject` | The region between a marker pair, in a file that already exists. | | `CopyFiles` | A copy of `from`, resolved against the process working directory, at `to` below the enclosing folder. | | `ListItems` | Its children once per element of `item`, then a blank line unless `line: false`. | Where a root lands is stated under [`aontu render`](/docs/reference-api#aontu-render). One segment of the path is the tree's own: a `Project`'s `folder` is joined under `` as a further segment, so `project("pkg", [file("a.txt")])` against `build2` writes `build2/pkg/a.txt`, and the project's `name` adds nothing to the path. **Text reaches the file verbatim.** `render` sets `raw` on every `Content` and `Line` node, so the substitution the runtime would otherwise apply to a span does not run, and a `$$…$$` sequence in a shell script, a doc comment, or a regex is written as it stands. Two props in the span set follow from that: `extra` and `replace` are inert unless the node writes `raw: false` itself. `indent` is placement rather than substitution and applies either way. **The write is not atomic.** The runtime writes as it walks, so a refusal part way through leaves the files written before it on disk. Two generators in one set claiming a single output path are refused that way, at exit 2, with the first file already written. `--check` writes nothing, and its flag entry is under [`aontu render`](/docs/reference-api#aontu-render). What it holds is the tree's own surface: the files the generator emits and not the directory, so a file that stops being generated is not reported. It answers the question `render` answers, so it reports no difference for a file `render` leaves alone: a `file` carrying `exclude: true` is reported neither for its bytes nor for its mode. The one difference it still reports for such a file is `missing`, because `exclude` is consulted only when the target is already there, and `render` writes an absent one. A reported path is the one the generator names, relative to ``, rather than the path the command was given. Write a `gen.aon` answering a project of one file: ```aon out: project(".", [folder("src", [file("main.js", ["const x = 1" ""])])]) ``` and a `build/src/main.js` holding different bytes: ```text const x = 2 ``` ```sh $ aontu render --check gen.aon build content: src/main.js $ echo $? 1 ``` The same edit under `exclude: true` is not a difference, because `render` does not make it. Write a generator that excludes its one file, as `gen.aon`: ```aon out: project(".", [file({ name:"keep.txt" exclude:true }, ["generated"])]) ``` and a hand-written `build/keep.txt`: ```text hand written ``` `render` leaves those bytes where they are, and the check then reports nothing: ```sh $ aontu render gen.aon build $ aontu render --check gen.aon build $ echo $? 0 ``` Exit codes for the verb are listed under [`aontu render`](/docs/reference-api#aontu-render), and the five values the engine uses under [Exit codes](/docs/reference-errors#exit-codes). Two of them are decided by the tree rather than by the command: | refusal | exit | |---|---| | the tree root is a `File` with no `name` | 4 | | the tree is refused before any write: an absolute or climbing `Project` folder, or a `props` that is not a map | 4 | A climbing `Project` folder is refused while the tree is still data, at exit 4; a climbing `File` or `Folder` name is refused by the write, at exit 2. The nameless-`File` guard reads the tree root alone, so a hand-written nameless `File` nested inside a `Project` renders and writes a file called `undefined`. ## What `aontu trace` reports `trace` reports one row per piece an [`emit`](/docs/reference-language#transforming-emit) rule stamped, attributed to the file it reached. The text form is four tab-separated columns, in this order: 1. `file`, the `name` prop of the innermost enclosing `File` node. 2. `at`, the address of the stamped piece in the document. 3. `node`, the address of the model node the dispatch matched, and the empty string where the selection was written inline at the call and so has no address (`test/spec/trace.tsv`). 4. `rule`, the rule table's address, `#`, and the template's index, addressed as [`aontu trace`](/docs/reference-api#aontu-trace) states. `--format json` answers one object under a `trace` key, whose entries carry the same four fields keyed `at`, `file`, `node`, and `rule`. Both ports print the four in those two orders. Write a `gen.aon` whose fields come from a rule set: ```aon fields: [n:"id" n:"name"] %field = emit(_, { match:n:string body: [line(" " + .n + ": string")] }) out: file("t.ts", ["type T = {" emit($.fields, %field) "}"]) ``` Ask what wrote each line: ```sh $ aontu trace gen.aon t.ts $.children.1 $.fields.0 $.%field#0 t.ts $.children.2 $.fields.1 $.%field#0 $ aontu trace --format json gen.aon {"trace":[{"at":"$.children.1","file":"t.ts","node":"$.fields.0","rule":"$.%field#0"},{"at":"$.children.2","file":"t.ts","node":"$.fields.1","rule":"$.%field#0"}]} ``` Four rules decide what has no row: - A piece no rule stamped, which covers every hand-written child (`trace-no-rule-no-entry`). - A piece under no `File` node at all, and a piece under a `File`-shaped map whose `props` is not a map, which names no file. - The descendants of a stamped piece. Only the top level of a spliced result is stamped, so a rule whose body is `[file(.n + ".txt", [line(.n)])]` puts a row on the `File` node and none on the `Line` inside it. - Anything outside the anchor, which is `$.out` unless `--at` names another path. The `file` column is the innermost enclosing `File`, by longest matching address prefix rather than by first match, and a `File` node that a rule stamped itself gets a row naming itself. `trace` reads a `` whose name does not end in `.aon` as a generator in the target's own syntax, desugared by its marker. That includes a `.aontu` file, which [`aontu render`](/docs/reference-api#aontu-render) and [`aontu fmt`](/docs/reference-api#aontu-fmt) both read as plain aontu; traced, it has no `$.out` and answers `no_path` ([Class `reference`](/docs/reference-errors#class-reference)) at exit 4. Exit codes are `0` for a report, empty or not, `2` for usage or I/O, and `4` where the document does not stand up or `--at` names nothing. ## Order and determinism `children` is a list and keeps document order. Each spliced child is checked against the same parent's admitted children (`cmp-splice-refuses-inside`). A node's own three keys print in code-point order, `children`, `cmp`, then `props`, and a props map prints its keys sorted the same way, whatever order they were written in: `cmp-map-spec` writes `file({name: "a.ts", mode: 420})` and pins the printed props as `mode` then `name`. List elements print in index order. `emit` visits its selection in the [order every bag reader uses](/docs/reference-language#generating-children-pack-and-each). `pack` answers a map, and no component accepts a map as `children`, so a `pack` result reaches a `file` as `Cannot children`. `each` and `emit` answer lists and feed `children` directly. [Generate code from a model](/how-to/generate-code) makes the practical point about which to reach for. Where rules nest, the innermost owns its pieces. A nested `emit` flattens its result into the parent's piece list in place, and the stamp is applied only where a piece carries none, so an outer rule never overwrites an inner rule's attribution. The stamps themselves are opt in: they are built only for a run that asks for them, which `trace` does and ordinary evaluation does not, so no stamp appears in the tree `generate` or `model get` answers. Both implementations promise the same tree and the same report, under [Behavioural parity](/docs/reference-api#behavioural-parity), and the Go component table mirrors the TypeScript one entry for entry. What `render` writes is one step further out: the two ports call two separate builds of the generator runtime, and it is the goldens in [`use-cases/15-code-generation/`](/use-cases/15-code-generation), held by `render --check`, that hold the bytes to each other. ## Related - [Language reference, Generation](/docs/reference-language#generation). What `generate` requires of a model, and the per-function index entry for each of the ten components. - [The call surface](/docs/reference-functions#the-call-surface). The arity, argument modes, and result word of every declared name. - [The codes](/docs/reference-errors#the-codes). `invalid-arg`, `func_arity`, and `no_path` by class, with the exit code each run ends on. - [`aontu render`](/docs/reference-api#aontu-render). The flags, the synopsis, where a root lands, and the groups `--format json` sorts written files into. - [Generate code from a model](/how-to/generate-code). The worked recipe: a rule set over the records, a tree of files and lines, and the bytes held against goldens. - [Trust and determinism](/docs/trust#clause-4-sandboxing). Why nothing in the engine writes a file, and which tree-shape refusals stand before `render` hands the tree on. --- # Functions reference _The call surface of all 64 built-ins: arity, argument modes, accepted kinds and result words, as one table and as slices through it._ Source: https://github.com/aontu-lang/aontu/blob/main/docs/reference-functions.md · Page: https://aontu.dev/docs/reference-functions aontu has 64 built-in functions and no user-defined ones. The name set is closed: `test/spec/signature.tsv` declares one line per built-in, both implementations carry a copy of that file inlined at build time, and a name the engine does not hold is refused while the document is parsed. This page is normative for the call surface. It states how many arguments each name takes, which mode each argument slot is read in, which kinds a slot admits, the result word the declaration gives, and what a call is refused for. It tabulates that surface, then slices it by argument mode, by result word, by rest slot, and by optional slot. It says nothing about what any function means. What each function means, with an example, is the language reference's [Functions](/docs/reference-language#functions) index: one entry per built-in, keyed by its whole signature. Nothing here repeats it. Each refusal code named below, with its class and the version it was registered at, is the [errors reference](/docs/reference-errors#the-codes), which is normative for the registry and for the shape of a report. ## Contents - [How a call is checked](#how-a-call-is-checked) - [Argument modes](#argument-modes) - [The call surface](#the-call-surface) - [Slices](#slices) - [Related](#related) --- ## How a call is checked The arity is decided at parse, the kinds at evaluation, and the two answer different codes. The permitted count comes from the declaration: a required slot raises the minimum, an optional slot raises the maximum only, and a rest slot leaves the maximum open while counting its group's length toward the minimum. The value builder compares that interval against the number of comma-separated terms the author wrote and, on a miss, puts a nil carrying `func_arity` where the call was. Its class is `parse`. No argument has been evaluated at that point, so the same broken reference is invisible while the count is wrong and reported once the count is right: ```sh $ echo 'a: add($.nope)' | aontu [aontu/func_arity]: Cannot resolve value at path $.a ... add takes exactly two arguments, but was given 1. ... $ echo $? 1 $ echo 'a: add(1, $.nope)' | aontu [aontu/no_path]: Cannot resolve value at path $.a ... $ echo $? 1 ``` The wording in that message is rendered from the interval and collapses cases, so it is not itself the interval: `rep` takes three arguments and prints `rep takes exactly one argument`, `project` takes none to two and prints `project takes no arguments or one`, and `match`'s floor of three is invisible in `match takes one or more arguments`. The bound is the arity column of [the call surface](#the-call-surface). The count is of written terms, so a list literal counts as one: `add([1 2])` misses `add`'s interval of two, while `neq([1 2])` meets `neq`'s minimum of one ([Errors](/docs/reference-language#errors)). Kinds are checked at evaluation, after every argument has settled and before the function resolves. The signature gate walks the declared slots, stops at a rest slot, and reads only a slot whose mode is `value` and whose declared type words are all scalar kinds: `string`, `number`, `integer`, `float`, `biginteger`, `bigdecimal`, `boolean`, and `path`. A slot declared `any`, `map`, `list`, or `constraint` is skipped, and so is an argument that is absent, nil, or unsettled. What the gate admits is a concrete scalar whose leaf kind is or sits below one of the declared words, so `number` admits every numeric leaf and `string` admits a path, while a scalar kind written in place of a value is refused. A miss is `func_arg`, class `conflict`, and the message prints the whole declaration and names the slot by number and by name: ```sh $ echo 'a: upper(true)' | aontu [aontu/func_arg]: Cannot unify values at path $.a ... argument 1 (`s`) was `true`. ... $ echo $? 1 ``` The declared words are read as kinds, so a numeric leaf fits `number` and a path fits `string`: ```aon n: add(1.5, 2) s: upper(path($.n)) ``` ```json { "n": 3.5, "s": "$.N" } ``` `upper` declares `s: string|number`, and the path was admitted as a string. Eighteen names have a gated slot. The declaration alone would gate 23: `key` is exempt by name and answers `key_level` for a bad argument, and `min`, `max`, `above`, and `below` are constraint atoms that never reach the gate, so a bad argument to one of those is `invalid-arg`. One further boundary sits inside `parse`, whose one-argument form is the constraint form and [meets](/docs/unification) its peer instead of resolving: so `parse(1)` is `mapval_no_gen` and `parse(1, "x")` is `func_arg`. Absence is decided before the signature gate: the first absent argument drops the whole call, and `maybe` is the only built-in that forgives it ([Optional input: `maybe`](/docs/reference-language#optional-input-maybe)). The order a call is refused in, first to last: 1. `func_arity`, from the written count, at parse. 2. Whatever refuses while the value-mode arguments are driven. A broken reference is `no_path`. 3. Absence, which drops the call. 4. `func_arg`, from the signature gate. 5. The function's own reading of its slots, which is `invalid-arg` or a code of that verb's own. There is no declaration form for a function anywhere in the grammar: the `name` rule enumerates the built-in names and the `func` rule admits no other ([The published grammar](/docs/reference-language#the-published-grammar)). A name the engine does not hold answers `unknown_function`, class `reference`, also at parse, and a call cannot be applied to a further argument list: ```sh $ echo 'a: nonesuch(1)' | aontu [aontu/unknown_function]: Cannot resolve value at path $.a ... $ echo $? 1 $ echo 'a: add(1)("x")' | aontu [aontu/unknown_function]: Cannot resolve value at path $.a ... $ echo $? 1 ``` A name in a value position is an ordinary string, because only `name(` builds a call ([Lexical structure](/docs/reference-language#lexical-structure)): ```aon a: upper ``` ```json { "a": "upper" } ``` An alias ([Aliases `%`](/docs/reference-language#aliases-)) may hold a call, which is how a named constraint and a named `emit` table are written ([Named constraint aliases](/docs/reference-language#named-constraint-aliases)), and it still cannot be a function name: `%Up = upper` followed by `%Up("x")` is `unknown_function`. ```aon %U = upper("x") n: %U ``` ```json { "n": "X" } ``` There is no partial application: an under-supplied call is `func_arity` at parse ([Limitations and trade-offs](/docs/explanation#limitations-and-trade-offs)). A parse-time nil is reported where the value is reached rather than where it was built, so a miss inside a losing disjunct branch is silent: ```aon a: *1|add(1) ``` ```json { "a": 1 } ``` ## Argument modes A declaration marks each slot with the mode it is read in and leaves `value` unmarked. The mode decides what the evaluator does with the slot before the function resolves, and which refusal a bad argument gets: the signature gate reads value-mode slots only, so in every other mode the refusal comes from the function's own reading of the settled argument. What each mode word means is the language reference's [Functions](/docs/reference-language#functions) index intro. The grammar of the declaration line is the header of `test/spec/signature.tsv`. An unmarked slot is driven: it is unified against [top](/docs/unification) before the call resolves, and the call resolves only once every value slot has settled. At least one value slot appears in 54 of the 64 names, and 46 of those carry no other mode. Three names have no slots at all (`acyclic`, `list`, and `map`), so 49 of the 64 use no mode but `value`, and 15 carry at least one slot in another mode. The five other modes, the slots that carry them, what the evaluator does with the slot, and where the semantics are specified: - `template`, five slots: `each`'s, `emit`'s, and `pack`'s second argument, and `refer`'s and `rel`'s only argument. `pack`, `each`, and `emit` instantiate the template per destination rather than driving it; `refer` and `rel` drive it at the call site. [Generating children: `pack` and `each`](/docs/reference-language#generating-children-pack-and-each), [Transforming: `emit`](/docs/reference-language#transforming-emit), [The placeholder `_`](/docs/reference-language#the-placeholder-_), [Checked links: `refer(t?)`](/docs/reference-language#checked-links-refert), and [The argument is a template, not an address](/docs/reference-language#the-argument-is-a-template-not-an-address). - `projector`, four slots: `pick`'s and `sort`'s second argument, and `inverse`'s and `unique`'s only argument. `pick` and `sort` never drive the slot (`test/spec/agg.tsv:115-122`, `test/spec/sort.tsv:70-72`); `inverse` and `unique` do, and then require the settled value to be a name (`test/spec/relation.tsv:112-113,170-174`, `test/spec/constraint-length.tsv:79-90`). [Projecting fields: `pick`](/docs/reference-language#projecting-fields-pick), [Ordering: `sort`](/docs/reference-language#ordering-sort), [`unique` semantics](/docs/reference-language#unique-semantics), and [Declared relations](/docs/reference-language#declared-relations). - `trial`, three slots: `must`'s check, `filter`'s condition, and the odd members of `match`'s rest group. The argument is unified against a candidate in a sandbox with an error sink of its own, and the sandbox is discarded afterwards, so a failed trial is a false answer rather than an error in the document. [Selecting: `filter` and `match`](/docs/reference-language#selecting-filter-and-match), [Band B: `must`](/docs/reference-language#band-b-must), and [Sizing atoms fold last](/docs/reference-language#sizing-atoms-fold-last). - `text`, four slots in three names: `re`'s pattern, `must`'s message, and `rep`'s pattern and substitution. The slot settles to a string like any other, and the settled string is then read as text in another notation rather than compared as a value. [`re` and the portable pattern subset](/docs/reference-language#re-and-the-portable-pattern-subset) and [`rep(s, pattern, sub)`](/docs/reference-language#reps-pattern-sub). - `capture`, one slot, `path`'s. The slot is read for its spelling and its value is never asked for, which is the one argument position the evaluator does not drive. [First-class paths: `path(p?)`](/docs/reference-language#first-class-paths-pathp). Five non-value slots answer a code of their own rather than `invalid-arg`: | name | slot | mode | code | |---|---|---|---| | `inverse` | `k` | `projector` | `inverse_name` | | `path` | `p` | `capture` | `path_address` | | `re` | `p` | `text` | `constraint_pattern` | | `rep` | `p` | `text` | `rep_pattern` | | `rep` | `sub` | `text` | `rep_sub` | Every other non-value slot answers `invalid-arg`, `path`'s included where the argument is neither address text nor a reference. One call carries a code from each side of the gate: `test/spec/str.tsv:132-134` pins `rep(1, "a", "b")` as `func_arg` from slot 1, which is value mode, and `rep("a", 1, "b")` and `rep("a", "a", 1)` as `invalid-arg` from slots 2 and 3, which are text mode. ## The call surface One row per declared name, in alphabetical order. The signature cell is the declaration line from `test/spec/signature.tsv`, with the type alternation's `|` escaped for the cell. The arity is the interval the engine derives from that line, written as a count, a range, or a floor with `n` for a rest slot. The modes are those the name's slots use, and `none` where the name has no slots. The result is the declaration's result word. The table is generated from the engine's registry and gated against it: `ts/test/docs.test.ts` re-renders every signature printed on a documentation page and fails on a difference of one space. | signature | arity | modes | result | |---|---|---|---| | `abnf(g: string) : string` | `1` | `value` | `string` | | `above(n: number\|string) : constraint` | `1` | `value` | `constraint` | | `acyclic() : constraint` | `0` | none | `constraint` | | `add(a: number, b: number) : number` | `2` | `value` | `number` | | `below(n: number\|string) : constraint` | `1` | `value` | `constraint` | | `close(m: any) : any` | `1` | `value` | `any` | | `content(spec: string\|map) : map` | `1` | `value` | `map` | | `copy(v: any) : any` | `1` | `value` | `any` | | `copyfiles(spec: string\|map) : map` | `1` | `value` | `map` | | `deprecate(v: any, r?: map) : any` | `1..2` | `value` | `any` | | `div(a: number, b: number) : number` | `2` | `value` | `number` | | `each(d: map\|list, template t: any) : list` | `2` | `value`, `template` | `list` | | `emit(s: map\|list, template t: map\|list) : list` | `2` | `value`, `template` | `list` | | `esc(s: string, variant?: string) : string` | `1..2` | `value` | `string` | | `file(spec: string\|map, children?: list) : map` | `1..2` | `value` | `map` | | `filter(d: map\|list, trial c: any) : map\|list` | `2` | `value`, `trial` | `map\|list` | | `folder(spec: string\|map, children?: list) : map` | `1..2` | `value` | `map` | | `fragment(spec: string\|map, children?: list) : map` | `1..2` | `value` | `map` | | `greatest(d: map\|list) : number` | `1` | `value` | `number` | | `hide(v: any) : any` | `1` | `value` | `any` | | `inject(spec: string\|map, children?: list) : map` | `1..2` | `value` | `map` | | `inverse(projector k: string) : constraint` | `1` | `projector` | `constraint` | | `join(d: map\|list, sep?: string) : string` | `1..2` | `value` | `string` | | `key(up?: integer\|biginteger) : string` | `0..1` | `value` | `string` | | `least(d: map\|list) : number` | `1` | `value` | `number` | | `length(n: number\|constraint) : constraint` | `1` | `value` | `constraint` | | `line(spec: string\|map) : map` | `1` | `value` | `map` | | `list() : list` | `0` | none | `list` | | `listitems(spec: map, children?: list) : map` | `1..2` | `value` | `map` | | `lower(s: string\|number, start?: integer\|biginteger, len?: integer\|biginteger) : string` | `1..3` | `value` | `string` | | `map() : map` | `0` | none | `map` | | `match(s: any, ...pr: (trial any, any), dflt?: any) : any` | `3..n` | `value`, `trial` | `any` | | `max(n: number\|string) : constraint` | `1` | `value` | `constraint` | | `maybe(v: any) : any` | `1` | `value` | `any` | | `min(n: number\|string) : constraint` | `1` | `value` | `constraint` | | `mod(a: number, b: number) : number` | `2` | `value` | `number` | | `move(v: any) : any` | `1` | `value` | `any` | | `mul(a: number, b: number) : number` | `2` | `value` | `number` | | `must(trial c: any, text msg: string) : constraint` | `2` | `trial`, `text` | `constraint` | | `neq(...vals: number\|string) : constraint` | `1..n` | `value` | `constraint` | | `nom(name: string, style?: string\|list, acronyms?: list) : string\|map` | `1..3` | `value` | `string\|map` | | `open(m: any) : any` | `1` | `value` | `any` | | `pack(d: map\|list, template t: any) : map` | `2` | `value`, `template` | `map` | | `parse(g: string, v?: string) : map\|list\|constraint` | `1..2` | `value` | `map\|list\|constraint` | | `path(capture p?: path) : path` | `0..1` | `capture` | `path` | | `pick(d: map\|list, projector k: string\|integer) : any` | `2` | `value`, `projector` | `any` | | `pref(v: any) : any` | `1` | `value` | `any` | | `project(spec?: string\|map, children?: list) : map` | `0..2` | `value` | `map` | | `re(text p: string) : constraint` | `1` | `text` | `constraint` | | `refer(template t?: any) : constraint` | `0..1` | `template` | `constraint` | | `rel(template t?: any) : constraint` | `0..1` | `template` | `constraint` | | `rem(a: number, b: number) : number` | `2` | `value` | `number` | | `rep(s: string, text p: string, text sub: string) : string` | `3` | `value`, `text` | `string` | | `slot(spec: string\|map, children?: list) : map` | `1..2` | `value` | `map` | | `sort(d: map\|list, projector k?: string\|integer, dir?: string) : list` | `1..3` | `value`, `projector` | `list` | | `split(s: string, sep: string\|constraint) : list` | `2` | `value` | `list` | | `sub(a: number, b: number) : number` | `2` | `value` | `number` | | `sum(d: map\|list) : number` | `1` | `value` | `number` | | `super(t: any) : any` | `1` | `value` | `any` | | `translate(s: string, from: string, to?: string) : string` | `2..3` | `value` | `string` | | `type(t: any) : any` | `1` | `value` | `any` | | `unique(projector k?: string) : constraint` | `0..1` | `projector` | `constraint` | | `upper(s: string\|number, start?: integer\|biginteger, len?: integer\|biginteger) : string` | `1..3` | `value` | `string` | | `usc(s: string, variant?: string) : string` | `1..2` | `value` | `string` | A result word of `constraint` marks a residual whose meet depends on the peer it lands beside. `any` is the result word where the declaration cannot name the result: ten wrappers that answer whatever they were given, plus `pick`, which answers a projection out of every child, and `match`, which answers one of its pattern results. ## Slices The same 64 names, cut by result word, by rest slot, and by optional slot. Every count below is over the whole surface. The mode slice is [Argument modes](#argument-modes). The ten result words, and the names under each: | result | count | names | |---|---|---| | `any` | 12 | `close`, `copy`, `deprecate`, `hide`, `match`, `maybe`, `move`, `open`, `pick`, `pref`, `super`, `type` | | `constraint` | 13 | `above`, `acyclic`, `below`, `inverse`, `length`, `max`, `min`, `must`, `neq`, `re`, `refer`, `rel`, `unique` | | `list` | 5 | `each`, `emit`, `list`, `sort`, `split` | | `map` | 12 | `content`, `copyfiles`, `file`, `folder`, `fragment`, `inject`, `line`, `listitems`, `map`, `pack`, `project`, `slot` | | `map\|list` | 1 | `filter` | | `map\|list\|constraint` | 1 | `parse` | | `number` | 9 | `add`, `div`, `greatest`, `least`, `mod`, `mul`, `rem`, `sub`, `sum` | | `path` | 1 | `path` | | `string` | 9 | `abnf`, `esc`, `join`, `key`, `lower`, `rep`, `translate`, `upper`, `usc` | | `string\|map` | 1 | `nom` | The algebra of the thirteen that answer `constraint`, including which pairs have a meet and what each one is refused for, is [The constraint algebra](/docs/reference-language#the-constraint-algebra). Ten of the twelve that answer `map` are the component functions, written as a tree that a generator walks: the tree's shape and each component's props are the [generation reference](/docs/reference-generation), and what the tree is turned into is [Generation](/docs/reference-language#generation). The other two are `map()`, which is the map kind, and `pack`. Three of the nine that answer `string` are the text verbs, `esc`, `usc`, and `rep`, together with `split`, which answers `list`: [Text: `esc` `usc` `rep` `split`](/docs/reference-language#text-esc-usc-rep-split). The one name that answers `path` is `path` itself: [First-class paths: `path(p?)`](/docs/reference-language#first-class-paths-pathp). Two names take a rest slot, spelled `...` in the declaration. `neq` takes one or more single values and has a floor of one. `match` takes a repeating pair whose first member is the pattern and whose second is the answer, and the pair's length counts toward the floor, which is why the floor is three: a scrutinee, a pattern, and an answer. `match` is specified at [Selecting: `filter` and `match`](/docs/reference-language#selecting-filter-and-match), `neq` at [The constraint algebra](/docs/reference-language#the-constraint-algebra). Twenty-three names have at least one optional slot. `must` is the one name that carries two non-value modes, its check `trial` and its message `text`. `match` is the one name whose rest group pairs a `trial` member with a `value` one. ## Related - [Language reference: Functions](/docs/reference-language#functions). One entry per built-in, keyed by its signature: what the function means, and an example. - [Composing calls](/docs/reference-language#composing-calls). Where a call may be written, and what it may stand in for. - [Errors reference](/docs/reference-errors#the-codes). Every registered code, its class, the version it was registered at, and the shape of the report a refusal is carried in. - [Generation reference](/docs/reference-generation). The component tree the ten component functions build, and what each verb writes when handed one. - [Unification](/docs/unification). Meet, top, bottom, and residual, which the modes and result words above are stated in terms of. - [API reference](/docs/reference-api). Calling the engine, and reading the refusals it answers with, from TypeScript or Go. --- # Errors reference _Every registered error code, by class, with what raises it, how a report is shaped, and which exit code a verb answers with._ Source: https://github.com/aontu-lang/aontu/blob/main/docs/reference-errors.md · Page: https://aontu.dev/docs/reference-errors Every refusal the engine can make carries a **code**, and every code carries a **class**. The code names the condition that was refused; the class says what kind of thing went wrong, and so which repair applies. This page is normative for the registry: every registered code, its class, and the version it was first registered at. It is not normative for the report a code arrives in, nor for the failures themselves. [`aontu vet`](/docs/reference-api#aontu-vet) specifies the report: the fields a finding carries, what a site names, and the `--format json` object each verb answers. The language reference specifies each construct where that construct belongs, and states what refuses it there: see [Errors](/docs/reference-language#errors) for the message families, [Exact or refused: lossy literals](/docs/reference-language#exact-or-refused-lossy-literals) and [The exactness budget](/docs/reference-language#the-exactness-budget) for the numeric rules, and [Errors](/docs/reference-language#errors-1) under [The constraint algebra](/docs/reference-language#the-constraint-algebra) for a constraint violation. [`aontu explain`](/docs/reference-api#aontu-explain) answers one code from the same table a finding carries, and `aontu help codes` prints the agent-facing card of the classes, which is shorter than the table below and carries no counts. ## Contents - [Classes](#classes) - [Dynamic-prefix families](#dynamic-prefix-families) - [The registry in a finding](#the-registry-in-a-finding) - [Exit codes](#exit-codes) - [The codes](#the-codes) - [Class `parse`](#class-parse) - [Class `conflict`](#class-conflict) - [Class `incomplete`](#class-incomplete) - [Class `reference`](#class-reference) - [Class `compat`](#class-compat) - [Class `budget`](#class-budget) - [Class `internal`](#class-internal) - [Related](#related) --- ## Classes There are seven classes, and the registry holds **172** codes across them. | class | codes | what went wrong | |---|---|---| | `parse` | 53 | the text is not a document | | `conflict` | 55 | two values cannot both hold | | `incomplete` | 11 | nothing contradicts, but the value is not concrete | | `reference` | 27 | a name or path resolves to nothing | | `compat` | 13 | a change breaks an earlier version | | `budget` | 7 | evaluation hit a deterministic limit | | `internal` | 6 | the engine reached a state it should not reach | A class states which repair applies rather than where in the engine the failure arose. [`aontu explain`](/docs/reference-api#aontu-explain) answers one code with its class and the engine's own text for it. Two `conflict` codes are reported after unification rather than by it: `relation_cycle` and `relation_inverse_missing`, whose shape and timing are under [`aontu relations`](/docs/reference-api#aontu-relations). ## Dynamic-prefix families Five registry rows are prefixes rather than whole codes: `func:`, `op:`, `op[`, `ref[`, and `var[`. The engine appends a name or a value, so a reported code may be `func:upper` or `ref[$.x]`. The bare `func`, `op`, `ref`, and `var` are separate rows with their own classes, and are not the same codes as the families. Class lookup takes the exact registry entry first, then the registered prefix the code extends, and then `internal`, because an unregistered code is an engine defect rather than a user error. Hint lookup has no such fallback. It is exact, so a finding whose code extends a family carries no hint text although the prefix row has some, and [`aontu explain`](/docs/reference-api#aontu-explain) is the only place a prefix's text is answered. `func:upper` is not a registry row, and resolves through `func:`: ```sh $ aontu explain func:upper code: func:upper class: conflict Function error: ``` Both the class and the text are the prefix's. A finding carrying that code would take the class and carry no `hint` key. ## The registry in a finding Three of a finding's fields are read from the registry, and each carries a qualification the registry alone does not show. **Class.** A finding's `class` is the registered class, on every surface that reports one. A finding that mints its own code takes the class that code is registered with; a finding that repeats a code the engine raised takes the class from the same row, never one of its own. `message` is the code's one-line headline throughout; the wording of that line is each implementation's own, since the shared suite holds codes and classes rather than prose. **Hint text.** All 172 codes have hint text. `aontu explain --list` prints them one per line, with each code's class beside it; a code carrying no text would be marked `(no text)`, and none is. A finding carries that text under `hint` when it repeats a code the engine raised and the run asked for `--format json`: [`vet`](/docs/reference-api#aontu-vet), [`allow`](/docs/reference-api#aontu-allow), [`model get`](/docs/reference-api#aontu-model-get), [`model why`](/docs/reference-api#aontu-model-why), [`view`](/docs/reference-api#aontu-view), the library's `diff` ([TypeScript](/docs/reference-api#typescript-api), and no CLI verb of its own), and the [MCP tools](/docs/reference-api#the-mcp-server). A finding the report mints for itself carries its own message and no `hint`, and so does the bare command that evaluates a document. No text format prints hint text at all: [`aontu explain`](/docs/reference-api#aontu-explain) is the verb that answers one code's. **Severity.** The field's domain is `error`, `warning`, and `info`, and nothing in either implementation reports `info`. Five codes are always reported at `warning`: `deprecated`, `pref_not_instance`, `patch_not_editable`, `patch_ambiguous`, and `patch_span_mismatch`. Under [`aontu breaking`](/docs/reference-api#aontu-breaking) with `--allow-deprecated-removal`, any error finding at a path the prior version marked with [`deprecate`](/docs/reference-language#deprecatev-any-r-map--any) is reported at `warning` instead, so a `compat` code otherwise reported at `error` arrives at `warning` there. Class `incomplete` is the only class a verdict rule reads. Under `vet` a schema that does not stand up on its own is verdict `error` whatever the finding's class; otherwise any error outside class `incomplete` makes the verdict `invalid`, errors only of class `incomplete` make it `incomplete` unless `--partial` was passed, and a `warning` never moves it. ## Exit codes A code's class does not determine the exit code. `scalar_value`, class `conflict`, exits 1 where the contradiction is between schema and data, and 4 where it is inside the schema. Which values a verb answers, and what each one means for that verb, is in the API reference beside that verb's option list. [`aontu vet`](/docs/reference-api#aontu-vet), [`aontu subsume`](/docs/reference-api#aontu-subsume), [`aontu breaking`](/docs/reference-api#aontu-breaking), [`aontu view`](/docs/reference-api#aontu-view), [`aontu model set`](/docs/reference-api#aontu-model-set), and [`aontu fmt`](/docs/reference-api#aontu-fmt) each state their own table. `aontu view` is the one verb whose refusals split across two exits. Nine `view_*` codes are usage rather than the document's fault and exit 2: `view_kind_unknown`, `view_profile_unknown`, `view_rows_exceeded`, `view_at_required`, `view_sets_required`, `view_group_required`, `view_document_shape`, `view_style_profile`, and `view_style_unknown`. A view kind that names no kind at all is caught while the arguments are being read, so it exits 2 with no report at all. ## The codes The registry is [`test/spec/errcodes.tsv`](https://github.com/aontu-lang/aontu/blob/main/test/spec/errcodes.tsv), which both implementations run as part of the shared suite; the parity it holds them to is stated under [`aontu explain`](/docs/reference-api#aontu-explain). The tables below are that file, one section per class, alphabetical within each. A `since` column is the version line at which the code was first registered, and `0.51.0` is the registry's own inception version, so every code older than the registry carries it. Codes are append-only: a registered code is never renamed or reused, so a row stays registered after the condition it named stops arising. The parenthetical link on a row is the section that specifies the refusal; twenty rows have no such section and carry no link. ### Class `parse` | code | since | raised when | |---|---|---| | `abnf_grammar` | 0.63.0 | The grammar could not be compiled; `abnf()` takes RFC 5234 ABNF, with `=` and `/` rather than `::=`. ([Grammars: `abnf()` and `parse()`](/docs/reference-language#grammars-abnf-and-parse)) | | `alias_colon` | 0.58.0 | Reserved for the former rejection of alias keys; `%name: value` now creates a field and declares its alias. ([Aliases `%`](/docs/reference-language#aliases-)) | | `alias_in_path` | 0.53.0 | An alias name used as a segment of a path. ([Aliases `%`](/docs/reference-language#aliases-)) | | `alias_not_toplevel` | 0.53.0 | An alias declaration has no map root, or a key declaration sits below it. ([Aliases `%`](/docs/reference-language#aliases-)) | | `bare_punct` | 0.58.0 | A bare string holding a character outside letters, digits, `-`, and `_`. ([Errors](/docs/reference-language#errors)) | | `decimal_syntax` | 0.51.0 | A `0d` literal that is not a valid exact number. ([The four numeric leaves](/docs/reference-language#the-four-numeric-leaves)) | | `each_data` | 0.53.0 | The first argument to `each()` is not a bag, so it has no children to make elements from. ([Generating children: `pack` and `each`](/docs/reference-language#generating-children-pack-and-each)) | | `elided_value` | 0.53.0 | A key, element, or spread with no value. ([Errors](/docs/reference-language#errors)) | | `emit_body` | 0.57.0 | A template body in an `emit()` table is not a list. ([Transforming: `emit`](/docs/reference-language#transforming-emit)) | | `emit_data` | 0.57.0 | The first argument to `emit()` is not a bag, so the selection has no children to visit. ([Transforming: `emit`](/docs/reference-language#transforming-emit)) | | `emit_table` | 0.57.0 | The second argument to `emit()` is not a rule table. ([Transforming: `emit`](/docs/reference-language#transforming-emit)) | | `emit_template` | 0.57.0 | A template in an `emit()` table is not a rule naming both `match` and `body`. ([Transforming: `emit`](/docs/reference-language#transforming-emit)) | | `esc_variant` | 0.57.0 | `esc()`, `usc()`, or a template's `esc:` key was given a variant naming no convention. ([`esc(s, variant?)` and `usc(s, variant?)`](/docs/reference-language#escs-variant-and-uscs-variant)) | | `export_arg` | 0.69.0 | `export()` was given something other than a set of alias names. ([Publishing a name: `export`](/docs/reference-language#publishing-a-name-export)) | | `filter_data` | 0.53.0 | The first argument to `filter()` is not a bag. ([Selecting: `filter` and `match`](/docs/reference-language#selecting-filter-and-match)) | | `form_data` | 0.58.0 | The first argument to the list generator `form` is not a bag; `form` was renamed `each`, which answers `each_data`. | | `func_arity` | 0.53.0 | A call whose argument count is not the built-in's arity. ([Errors](/docs/reference-language#errors)) | | `include_denied` | 0.53.0 | An `@"..."` include refused by the active trust profile. ([Clause 1: hermeticity](/docs/trust#clause-1-hermeticity)) | | `include_extension` | 0.54.0 | An `@"..."` include naming a file whose extension the include table does not know. ([Source loading `@"…"`](/docs/reference-language#source-loading-)) | | `incomplete_expression` | 0.51.0 | An expression missing a term, grouping parentheses with nothing inside included. ([The `+` operator and grouping](/docs/reference-language#the--operator-and-grouping)) | | `inverse_name` | 0.53.0 | The argument to `inverse()` is not a relation name. ([Declared relations](/docs/reference-language#declared-relations)) | | `merge_conflict` | 0.53.0 | A version-control conflict marker left in the source. ([Errors](/docs/reference-language#errors)) | | `module_integrity` | 0.53.0 | A module resolved locally does not carry the meaning its canon-hash pin recorded. ([Modules](/docs/reference-language#modules)) | | `module_local` | 0.65.0 | A bare module reference whose last segment carries an extension the include table knows. ([Modules](/docs/reference-language#modules)) | | `module_missing` | 0.53.0 | A module import naming a package that is not in the project's local stores. ([Modules](/docs/reference-language#modules)) | | `module_moved` | 0.65.0 | A later version of the imported package declares a new path. ([Modules](/docs/reference-language#modules)) | | `module_path` | 0.54.0 | A domain-shaped module import whose path cannot be a directory on every platform the toolchain runs on. ([Modules](/docs/reference-language#modules)) | | `negative` | 0.51.0 | Unary minus applied to a non-numeric operand. ([The four numeric leaves](/docs/reference-language#the-four-numeric-leaves)) | | `not_number` | 0.51.0 | A numeric literal that reads as a non-finite value. ([The four numeric leaves](/docs/reference-language#the-four-numeric-leaves)) | | `pack_data` | 0.53.0 | The first argument to `pack()` is not a bag. ([Generating children: `pack` and `each`](/docs/reference-language#generating-children-pack-and-each)) | | `pack_key` | 0.53.0 | A list packed by `pack()` holds an element that is not a string. ([Generating children: `pack` and `each`](/docs/reference-language#generating-children-pack-and-each)) | | `parse` | 0.51.0 | The outer wrapper for a source that could not be turned into a document; the inner error carries the code that explains it. | | `parse_arg` | 0.63.0 | `parse()` was given something other than a grammar string and a text string. ([Grammars: `abnf()` and `parse()`](/docs/reference-language#grammars-abnf-and-parse)) | | `parse_bad_src` | 0.51.0 | The source handed in for parsing is not a non-empty string. | | `parse_unknown` | 0.51.0 | A parsed node of a kind the value builder has no case for. | | `patch_assignment` | 0.53.0 | A `set` argument that is not `=`. ([`aontu model set`](/docs/reference-api#aontu-model-set)) | | `path_address` | 0.54.0 | `path()` was given text that is not a tree address. ([First-class paths: `path(p?)`](/docs/reference-language#first-class-paths-pathp)) | | `pref_implicit_bag` | 0.53.0 | A preference mark written on a bare key rather than on a value. ([Preference / default `*`](/docs/reference-language#preference--default-)) | | `refer_address` | 0.53.0 | `refer()` was given something that is not a path value. ([Addresses](/docs/reference-language#addresses)) | | `rel_address` | 0.53.0 | A `rel()` field holds something other than path values. ([Declared relations](/docs/reference-language#declared-relations)) | | `render_path` | 0.58.0 | A unit path that is not relative, below the output directory, and distinct from every other unit's. ([What `aontu render` writes](/docs/reference-generation#what-aontu-render-writes)) | | `render_profile` | 0.58.0 | A declaration needing a lowering, under a profile whose language has none. ([What `aontu render` writes](/docs/reference-generation#what-aontu-render-writes)) | | `rep_pattern` | 0.57.0 | The pattern given to `rep()` is outside the portable subset `re()` takes. ([`rep(s, pattern, sub)`](/docs/reference-language#reps-pattern-sub)) | | `rep_sub` | 0.57.0 | The substitution given to `rep()` names a group the pattern does not have. ([`rep(s, pattern, sub)`](/docs/reference-language#reps-pattern-sub)) | | `replace_overlap` | 0.58.0 | Two keys of a template's `replace` map overlap, one inside the other. ([Replacing text in a body: `replace` and `esc`](/docs/reference-language#replacing-text-in-a-body-replace-and-esc)) | | `replace_unused` | 0.58.0 | A key of a template's `replace` map appears in none of the body's literal lines. ([Replacing text in a body: `replace` and `esc`](/docs/reference-language#replacing-text-in-a-body-replace-and-esc)) | | `reserved_key` | 0.69.0 | A source key beginning with the engine's reserved `\u0000aontu_` prefix, where the parser keeps a document's marks. ([Errors](/docs/reference-language#errors)) | | `sort_dir` | 0.63.0 | A sort direction other than `asc` or `desc`. ([Ordering: `sort`](/docs/reference-language#ordering-sort)) | | `split_sep` | 0.57.0 | The separator given to `split()` is neither a string nor a pattern. ([Text: `esc` `usc` `rep` `split`](/docs/reference-language#text-esc-usc-rep-split)) | | `syntax` | 0.51.0 | The parser refused the source text; the message is the parser's own, with the operator-character hint appended. ([Lexical structure](/docs/reference-language#lexical-structure)) | | `unify_no_src` | 0.51.0 | No source was handed in for unification. | | `usc_malformed` | 0.57.0 | `usc()` was given text the named convention could not have produced. ([`esc(s, variant?)` and `usc(s, variant?)`](/docs/reference-language#escs-variant-and-uscs-variant)) | | `view_line_break` | 0.54.0 | A label the figure would draw holds a line terminator. ([`aontu view`](/docs/reference-api#aontu-view)) | ### Class `conflict` | code | since | raised when | |---|---|---| | `aggregate_data` | 0.53.0 | An aggregate was given something other than a bag to fold. ([Aggregating: `sum` `least` `greatest`](/docs/reference-language#aggregating-sum-least-greatest)) | | `aggregate_empty` | 0.53.0 | `least` or `greatest` given an empty bag. ([Aggregating: `sum` `least` `greatest`](/docs/reference-language#aggregating-sum-least-greatest)) | | `arg` | 0.51.0 | A required argument is missing. | | `close` | 0.51.0 | The structure could not be closed. ([Closed values: `close` / `open`](/docs/reference-language#closed-values-close--open)) | | `closed` | 0.51.0 | A key or element added to a closed map or list. ([Errors](/docs/reference-language#errors)) | | `constraint` | 0.52.0 | The value does not satisfy the normalised residual the constraint reduced to. ([The constraint algebra](/docs/reference-language#the-constraint-algebra)) | | `constraint_pattern` | 0.53.0 | An `re()` pattern outside the supported subset. ([The constraint algebra](/docs/reference-language#the-constraint-algebra)) | | `decimal_budget` | 0.51.0 | An exact decimal past 4096 coefficient digits or an absolute scale of 4096. ([The exactness budget](/docs/reference-language#the-exactness-budget)) | | `divide_by_zero` | 0.53.0 | `div`, `mod`, or `rem` given a zero divisor. ([Arithmetic: `add` `sub` `mul` `div` `mod` `rem`](/docs/reference-language#arithmetic-add-sub-mul-div-mod-rem)) | | `emit_none` | 0.57.0 | No template matched a node, and the table has no catch-all. ([Transforming: `emit`](/docs/reference-language#transforming-emit)) | | `emit_ref` | 0.57.0 | A template body names a field the node it matched does not carry. ([Transforming: `emit`](/docs/reference-language#transforming-emit)) | | `empty` | 0.54.0 | A disjunction with no admitted alternative. ([Preference / default `*`](/docs/reference-language#preference--default-)) | | `empty-dist` | 0.54.0 | Every alternative of a distributed disjunction is refused. ([Disjunction `|`](/docs/reference-language#disjunction-)) | | `exact_float_mix` | 0.51.0 | An exact number combined with a binary float. ([The four numeric leaves](/docs/reference-language#the-four-numeric-leaves)) | | `float_overflow` | 0.53.0 | A result that is not a finite binary64 number. ([Arithmetic: `add` `sub` `mul` `div` `mod` `rem`](/docs/reference-language#arithmetic-add-sub-mul-div-mod-rem)) | | `func` | 0.51.0 | A function operation failed; the named function carries the detail. ([How a call is checked](/docs/reference-functions#how-a-call-is-checked)) | | `func:` | 0.51.0 | Dynamic-prefix family: a named function's own failure, the name appended (`func:upper`). | | `func_arg` | 0.55.0 | An argument does not fit the function's signature. ([How a call is checked](/docs/reference-functions#how-a-call-is-checked)) | | `inexact_divide` | 0.53.0 | An exact decimal operand given to `div`, `mod`, or `rem`; exact decimal division is not closed, so it is refused rather than rounded, while a `0d` biginteger divides. ([Arithmetic: `add` `sub` `mul` `div` `mod` `rem`](/docs/reference-language#arithmetic-add-sub-mul-div-mod-rem)) | | `inexact_integer_sum` | 0.51.0 | An `integer` result outside the integral, int64, exactly representable range. ([Arithmetic: `add` `sub` `mul` `div` `mod` `rem`](/docs/reference-language#arithmetic-add-sub-mul-div-mod-rem)) | | `invalid-arg` | 0.51.0 | An argument does not match the expected type or format. | | `join_member` | 0.54.0 | A member of the bag `join()` folds is not text and never will be. ([Folding to a string: `join`](/docs/reference-language#folding-to-a-string-join)) | | `key_level` | 0.51.0 | The argument to `key()` is not a level. ([How a call is checked](/docs/reference-functions#how-a-call-is-checked)) | | `list` | 0.51.0 | A list was expected and the value is of another kind. ([Container kinds: `map()` and `list()`](/docs/reference-language#container-kinds-map-and-list)) | | `list_length` | 0.53.0 | A literal list alternative admits only a list of its own length; a spread makes it take any length. ([Lists](/docs/reference-language#lists)) | | `literal_nil` | 0.51.0 | A literal nil met another value. ([The value lattice](/docs/reference-language#the-value-lattice)) | | `lossy_integer_literal` | 0.51.0 | An integer literal not exactly representable in binary64; the hint names the `0d` spelling. ([Exact or refused: lossy literals](/docs/reference-language#exact-or-refused-lossy-literals)) | | `make` | 0.51.0 | A value could not be constructed. | | `map` | 0.51.0 | A map was expected and the value is of another kind. ([Container kinds: `map()` and `list()`](/docs/reference-language#container-kinds-map-and-list)) | | `match_none` | 0.53.0 | No pattern matched, and `match` has no default. ([Selecting: `filter` and `match`](/docs/reference-language#selecting-filter-and-match)) | | `must` | 0.53.0 | The value fails an evaluate-only check written with `must()`; the author's message rides on the finding. ([Band B: `must`](/docs/reference-language#band-b-must)) | | `nil_gen` | 0.51.0 | A nil survived unification, and nil is not a literal value to generate. ([Generation](/docs/reference-language#generation)) | | `no_first_arg` | 0.51.0 | The function's first argument is missing. | | `no_scalar_unify` | 0.51.0 | Two scalar values of incompatible types. ([Unification rules](/docs/reference-language#unification-rules)) | | `not-scalar-type` | 0.51.0 | A scalar type was expected and the value is not one. ([Unification rules](/docs/reference-language#unification-rules)) | | `op` | 0.51.0 | An operator operation failed; the named operator carries the detail. | | `op:` | 0.51.0 | Dynamic-prefix family: a named operator's own failure, the name appended (`op:add`). | | `op[` | 0.51.0 | Dynamic-prefix family: an operator failure carrying the offending value (`op[1]`). | | `operate` | 0.51.0 | The operation could not be performed over the values given. | | `parse_failed` | 0.63.0 | The text does not parse under the grammar given, so the field is refused. ([Grammars: `abnf()` and `parse()`](/docs/reference-language#grammars-abnf-and-parse)) | | `pick_key` | 0.53.0 | A child of the bag has no key for `pick` to project. ([Missing fields and invalid arguments](/docs/reference-language#missing-fields-and-invalid-arguments)) | | `place_pair` | 0.53.0 | Two placeholders met, and neither has a value to fill the other. ([The placeholder `_`](/docs/reference-language#the-placeholder-_)) | | `pref_rank_clash` | 0.54.0 | Two defaults of the same rank disagree. ([Preference / default `*`](/docs/reference-language#preference--default-)) | | `relation_cycle` | 0.53.0 | A relation declared `acyclic()`, and its edges form a cycle. ([Declared relations](/docs/reference-language#declared-relations)) | | `relation_inverse_missing` | 0.53.0 | A relation declared `inverse(name)`, and an edge has no mirroring edge. ([Declared relations](/docs/reference-language#declared-relations)) | | `render_lang` | 0.58.0 | A text escape carrying verbatim syntax of a language other than the unit's. ([What `aontu render` writes](/docs/reference-generation#what-aontu-render-writes)) | | `render_strict` | 0.58.0 | An opaque escape, which the renderer cannot check, under strict rendering. ([What `aontu render` writes](/docs/reference-generation#what-aontu-render-writes)) | | `replace_value` | 0.58.0 | A replacement value is not text by the time the dispatch fires. ([Replacing text in a body: `replace` and `esc`](/docs/reference-language#replacing-text-in-a-body-replace-and-esc)) | | `resolve` | 0.51.0 | The value could not be resolved. | | `scalar-type` | 0.51.0 | Two scalar kinds where neither contains the other. ([Unification rules](/docs/reference-language#unification-rules)) | | `scalar_kind` | 0.51.0 | Two literal scalars of different kinds. ([Unification rules](/docs/reference-language#unification-rules)) | | `scalar_value` | 0.51.0 | Two literal scalars of the same kind that are not equal. ([Unification rules](/docs/reference-language#unification-rules)) | | `sort_domain` | 0.63.0 | A bag with no order to be put in. ([Ordering: `sort`](/docs/reference-language#ordering-sort)) | | `sort_key` | 0.63.0 | A child of the bag has no key to order by. ([Ordering: `sort`](/docs/reference-language#ordering-sort)) | | `unite` | 0.51.0 | Two values could not be united. | ### Class `incomplete` | code | since | raised when | |---|---|---| | `conjunct` | 0.51.0 | A conjunction has a term that could not be resolved. ([Conjunction `&`](/docs/reference-language#conjunction-)) | | `disjunct_no_gen` | 0.53.0 | More than one alternative is still admitted, so there is no single value to generate. ([Disjunction `|`](/docs/reference-language#disjunction-)) | | `listval_no_gen` | 0.51.0 | A list element survived unification as something other than a literal value. ([Generation](/docs/reference-language#generation)) | | `listval_required` | 0.51.0 | A required list element has no value. ([Optional keys `?`](/docs/reference-language#optional-keys-)) | | `listval_spread_required` | 0.51.0 | A key a spread requires has no value, in a list. ([Spreads `&:`](/docs/reference-language#spreads-)) | | `mapval_no_gen` | 0.51.0 | A map value survived unification as something other than a literal value. ([Optional input: `maybe`](/docs/reference-language#optional-input-maybe)) | | `mapval_required` | 0.51.0 | A required map value has no value. ([Optional keys `?`](/docs/reference-language#optional-keys-)) | | `mapval_spread_required` | 0.51.0 | A key a spread requires has no value, in a map. ([Spreads `&:`](/docs/reference-language#spreads-)) | | `no_gen` | 0.51.0 | A value survived unification as something other than a literal value. ([Generation](/docs/reference-language#generation)) | | `recursion_unexpanded` | 0.53.0 | A schema refers to itself, and no data reached the position to expand it against. ([Recursive references (fixpoints)](/docs/reference-language#recursive-references-fixpoints)) | | `required_listelem` | 0.51.0 | A non-optional list element has no value. ([Optional keys `?`](/docs/reference-language#optional-keys-)) | ### Class `reference` | code | since | raised when | |---|---|---| | `import_not_exported` | 0.69.0 | A destructure asked for a name the other file does not publish. ([Taking a name: the destructure](/docs/reference-language#taking-a-name-the-destructure)) | | `invalid_var_kind` | 0.51.0 | A variable's kind is not the kind the use expects. ([Variables `$name`](/docs/reference-language#variables-name)) | | `multisource_not_found` | 0.51.0 | An `aontu:` name that is not one of the language-supplied models; the message names the set. ([The `aontu:` models](/docs/reference-language#the-aontu-models)) | | `no_path` | 0.51.0 | A path reference resolves to nothing. ([Optional input: `maybe`](/docs/reference-language#optional-input-maybe)) | | `patch_ambiguous` | 0.53.0 | Two or more statements pin the path, so an in-place edit has no single place to write. ([`aontu model set`](/docs/reference-api#aontu-model-set)) | | `patch_not_editable` | 0.53.0 | An in-place edit found no single literal to rewrite, so the assignment was appended. ([`aontu model set`](/docs/reference-api#aontu-model-set)) | | `path_cycle` | 0.51.0 | A path reference closes a cycle. ([Recursive references (fixpoints)](/docs/reference-language#recursive-references-fixpoints)) | | `ref` | 0.51.0 | A reference could not be resolved to a value. ([References and paths](/docs/reference-language#references-and-paths)) | | `ref[` | 0.51.0 | Dynamic-prefix family: a reference failure carrying the address (`ref[$.x]`). ([References and paths](/docs/reference-language#references-and-paths)) | | `refer_unresolved` | 0.53.0 | A `refer()` address names no node in this evaluation. ([Existence is decided, not deferred](/docs/reference-language#existence-is-decided-not-deferred)) | | `rel_unresolved` | 0.53.0 | A `rel()` address names no node in this evaluation. ([Declared relations](/docs/reference-language#declared-relations)) | | `render_unit` | 0.58.0 | The unit asked for is not in the instance. ([What `aontu render` writes](/docs/reference-generation#what-aontu-render-writes)) | | `unknown_function` | 0.51.0 | A function name that resolves to no built-in. ([How a call is checked](/docs/reference-functions#how-a-call-is-checked)) | | `unknown_var` | 0.51.0 | A variable that has not been defined. ([Variables `$name`](/docs/reference-language#variables-name)) | | `var` | 0.51.0 | An unresolved variable reached generation. ([Variables `$name`](/docs/reference-language#variables-name)) | | `var[` | 0.51.0 | Dynamic-prefix family: a variable failure carrying the name (`var[$x]`). ([Variables `$name`](/docs/reference-language#variables-name)) | | `view_at_required` | 0.54.0 | The meet ladder draws the contributions at one path, and none was named. ([`aontu view`](/docs/reference-api#aontu-view)) | | `view_document_shape` | 0.54.0 | A figure in a view document does not name both its `kind` and its `out` file. ([`aontu view`](/docs/reference-api#aontu-view)) | | `view_group_required` | 0.54.0 | The layer diagram bands nodes by a field, and none was named. ([`aontu view`](/docs/reference-api#aontu-view)) | | `view_kind_unknown` | 0.54.0 | The figure kind is not one the verb draws; the note lists the kinds. ([`aontu view`](/docs/reference-api#aontu-view)) | | `view_profile_unknown` | 0.54.0 | The figure kind does not render into the profile asked for. ([`aontu view`](/docs/reference-api#aontu-view)) | | `view_relation_ambiguous` | 0.54.0 | The document has edges under several relations, and the figure draws one. ([`aontu view`](/docs/reference-api#aontu-view)) | | `view_relation_unknown` | 0.54.0 | The relation named to the view has no edges in this document. ([`aontu view`](/docs/reference-api#aontu-view)) | | `view_sets_required` | 0.54.0 | The set panel needs both the sets map and the member field, and one was missing. ([`aontu view`](/docs/reference-api#aontu-view)) | | `view_sets_shape` | 0.54.0 | The sets map or the universe does not have the shape the set panel reads. ([`aontu view`](/docs/reference-api#aontu-view)) | | `view_style_profile` | 0.55.0 | The style asked for is not the one that profile carries. ([`aontu view`](/docs/reference-api#aontu-view)) | | `view_style_unknown` | 0.55.0 | A style other than `none`, `ansi`, `css`, or the command line's `auto`. ([`aontu view`](/docs/reference-api#aontu-view)) | ### Class `compat` | code | since | raised when | |---|---|---| | `compat_default_changed` | 0.53.0 | The effective default changed, so a document generable before materialises differently or becomes incomplete. ([Profiles](/docs/reference-language#profiles)) | | `compat_marks_changed` | 0.53.0 | The marks on the two values differ. ([Maps, lists, closedness, optionality, spreads](/docs/reference-language#maps-lists-closedness-optionality-spreads)) | | `compat_narrowed` | 0.53.0 | The specific value admits something the general does not; the message names which comparison failed. ([Rules, by value former](/docs/reference-language#rules-by-value-former)) | | `compat_outcome_changed` | 0.65.0 | A position both versions resolve with nothing supplied, to different values. ([`aontu publish`](/docs/reference-api#aontu-publish)) | | `compat_required_added` | 0.53.0 | The general value requires a key the specific omits or makes optional. ([Maps, lists, closedness, optionality, spreads](/docs/reference-language#maps-lists-closedness-optionality-spreads)) | | `compat_undetermined` | 0.65.0 | A position the prior version resolved with nothing supplied, and nothing resolves now. ([`aontu publish`](/docs/reference-api#aontu-publish)) | | `deprecated` | 0.53.0 | A use of a value carrying a `deprecate` mark. ([`deprecate(v: any, r?: map) : any`](/docs/reference-language#deprecatev-any-r-map--any)) | | `pref_not_instance` | 0.53.0 | A disjunction's effective default is not an instance of any remaining alternative. ([Default validity](/docs/reference-language#default-validity)) | | `sub_default_indeterminate` | 0.53.0 | Equal-rank preferences disagree, so the effective default is not a single value. ([Profiles](/docs/reference-language#profiles)) | | `sub_disjunct_distribution` | 0.53.0 | A specific alternative is not admitted member-wise, and no concrete counterexample settles the distribution case. ([Rules, by value former](/docs/reference-language#rules-by-value-former)) | | `sub_evaluate_only` | 0.53.0 | An evaluate-only check makes the admitted set opaque. ([Rules, by value former](/docs/reference-language#rules-by-value-former)) | | `sub_path_dependent_spread` | 0.53.0 | A path-dependent spread template cannot be compared structurally. ([Maps, lists, closedness, optionality, spreads](/docs/reference-language#maps-lists-closedness-optionality-spreads)) | | `sub_unresolved` | 0.53.0 | Unresolved residue, or no subsumption rule covers the pair of value formers. ([Rules, by value former](/docs/reference-language#rules-by-value-former)) | ### Class `budget` | code | since | raised when | |---|---|---| | `alias_budget` | 0.69.0 | Alias expansion counted past the size budget before evaluation; expansion terminates whatever the budget, so this is about size. ([Aliases `%`](/docs/reference-language#aliases-)) | | `budget_passes` | 0.52.0 | The fixpoint pass budget was spent before the model converged; the hint names what was still refining. ([Cross-field bounds and residuation](/docs/reference-language#cross-field-bounds-and-residuation)) | | `max_depth` | 0.51.0 | Input nested deeper than the engine processes. ([Clause 2: termination](/docs/trust#clause-2-termination)) | | `module_depth` | 0.53.0 | Module verification nested past its depth, usually a vendor tree leading back to itself. ([Modules](/docs/reference-language#modules)) | | `recursion_budget` | 0.53.0 | A recursive schema expanded past the depth budget without meeting concrete data. ([Recursive references (fixpoints)](/docs/reference-language#recursive-references-fixpoints)) | | `unify_cycle` | 0.51.0 | A circular reference reached during unification. ([Clause 2: termination](/docs/trust#clause-2-termination)) | | `view_rows_exceeded` | 0.54.0 | The figure has more rows than the row cap allows; the figure is refused rather than trimmed. ([`aontu view`](/docs/reference-api#aontu-view)) | ### Class `internal` | code | since | raised when | |---|---|---| | `format_check` | 0.56.0 | The formatted text is not the same document, so nothing was written. ([`aontu fmt`](/docs/reference-api#aontu-fmt)) | | `internal` | 0.51.0 | An unexpected state during unification. | | `unify_failed` | 0.67.0 | A document does not evaluate, and the failure carries no code of its own. | | `patch_span_mismatch` | 0.53.0 | The overlay text does not hold the recorded source at the recorded span, so the span cannot be verified before writing. ([`aontu model set`](/docs/reference-api#aontu-model-set)) | | `unify_no_res` | 0.51.0 | Unification produced no result. | | `unknown_op` | 0.51.0 | An operator expression the evaluator has no rule for. | ## Related - [`aontu vet`](/docs/reference-api#aontu-vet) for the report these findings arrive in, the fields they carry, and its own exit table. - [`aontu explain`](/docs/reference-api#aontu-explain) for the verb that answers one code, and for what `--list` prints. - [Errors](/docs/reference-language#errors) in the language reference for the message families and the rules that refuse a document. - [Read a conflict error](/how-to/read-a-conflict-error) for reading a two-site conflict message operand by operand. - [Collect errors instead of throwing](/how-to/collect-errors) for gathering every finding in one pass from the embedded API. - [`test/spec/errcodes.tsv`](https://github.com/aontu-lang/aontu/blob/main/test/spec/errcodes.tsv) for the registry itself, which both implementations run. --- # Packages reference _The package system's artefacts: the files it keeps, every field pkg.aon declares, the name rules, the caps, the archive allowlist, and every refusal code._ Source: https://github.com/aontu-lang/aontu/blob/main/docs/reference-packages.md · Page: https://aontu.dev/docs/reference-packages A **module** is a document another document imports. A **package** is a module that was published: a tree of aontu source with a path, a version, a signed manifest, and an archive whose bytes one digest names. The language reference states how an import routes to one; this page states what the package system keeps on disk while it does so. This page is normative for the artefacts: the files the package system reads and writes, every field `pkg.aon` declares, the name and version rules, the caps every consumer applies, what an archive may hold, and every code a package operation refuses with. It states no verb's options and no verb's exit codes. The verbs are the API reference: [`aontu sync`](/docs/reference-api#aontu-sync) for the one that makes a project correct, [`aontu add`, `aontu get`, `aontu remove`](/docs/reference-api#aontu-add-aontu-get-aontu-remove) for changing what a project depends on, [`aontu publish`](/docs/reference-api#aontu-publish) for sending one out, and [`aontu pkg`](/docs/reference-api#aontu-pkg) for the remaining steps. [Modules](/docs/reference-language#modules) in the language reference states how a reference routes, and [`tutorial-package.md`](/docs/tutorial-package) walks the whole path once with a real package. ## Contents - [The files](#the-files) - [What pkg.aon declares](#what-pkgaon-declares) - [Names and versions](#names-and-versions) - [Where a module resolves from](#where-a-module-resolves-from) - [The caps](#the-caps) - [What an archive may hold](#what-an-archive-may-hold) - [Refusals](#refusals) - [Related](#related) --- ## The files Everything the package system keeps in a project is a file in it, and every one of those is ordinary text: nothing is hidden in a database, and nothing outside this table is consulted. The two stores outside a project are not text alone. The user cache and a repository directory hold the archives themselves, which are zips, beside the manifest and proof that travel with them. | path | written by | read by | commit it | |---|---|---|---| | `pkg.aon` | the author, and `add`, `get`, `remove` by the smallest text change | every package verb, and module resolution | yes | | `aontu_meta/pkg-lock.aon` | `sync`, `pkg tidy`, `pkg refreeze` | `sync --frozen`, `pkg verify`, `pkg tree`, `why`, and resolution for a pinned import | yes | | `aontu_meta/vendor//` | `sync`, `pkg vendor` | resolution, first of the stores | yes | | `aontu_meta/vendor//aontu_meta/` | `sync`, holding the manifest and proof as served | `pkg verify` | yes | | the user cache | `sync`, `add`, `get` | resolution, when the expected canon-hash is known | it is outside the project | | a repository directory | `publish --to ` | `pkg serve`, and any consumer whose `repo.base` names it | it is not part of a project | | the signing key | `pkg keygen`, once | `publish --key` | never | A project that has been synced evaluates from its own directory with no cache and no network, because the vendor tree is a real copy rather than a link. The lockfile and the vendor tree are written together and belong in one commit: a lockfile without the tree it pins names bytes the project does not have. ## What pkg.aon declares `pkg.aon` is an ordinary aontu document, evaluated as any other. The tools read the fields below and nothing else, so a field of the author's own is carried rather than refused. | field | value | default | read by | |---|---|---|---| | `pkg.path` | this package's path | none | resolution, `publish`, `why` | | `pkg.version` | this package's own version | none | `publish`, `pkg manifest` | | `pkg.main` | the entry document, a path inside the tree | `main.aon` | resolution, `publish` | | `dep."".v` | the minimum version of a dependency | none | `sync`, and every verb that resolves | | `dep."alias:".pkg` | the package an alias names | none | resolution of an `alias:` import | | `dep."alias:".v` | the minimum version taken under that alias | none | `sync` | | `publish` | `public`, to allow publication at all | `private` | `publish` | | `moved` | the package path that replaces this one | absent | resolution, `publish` | | `retract` | versions withdrawn, as a list of strings | empty | `publish --to`, which derives the advisory | | `repo.base` | repositories to read from, in order | none | every verb that fetches | | `repo.write` | the write path a publish sends to | none | `publish` | | `repo.private` | patterns never sent to a public base | none | every verb that fetches | | `repo.private_base` | repositories for a name on the private list | none | every verb that fetches | | `repo.trust."".signer` | `forge`, or `ed25519:` and a public key | the entry for `*`, which names the forge signer | acquisition | | `repo.trust."".inclusion` | `none`, to accept a key proof that carries no log inclusion | inclusion is required | acquisition | A consuming project declares what it depends on, and where those packages are read from: ```aon pkg: { path:"corp.example/checkout" main:"main.aon" } dep: "corp.example/schemas/service": v: "1.4.2" dep: "alias:legacy": { pkg:"corp.example/schemas/service" v:"1.2.0" } repo: base: ["https://pkg.aontu.dev"] repo: private: ["corp.example/*"] repo: private_base: ["https://pkg.corp.example"] repo: trust: "corp.example/*": { signer:"ed25519:q520" inclusion:none } ``` A package that is published declares its own version and says so out loud, and the last version of a path that has been superseded points at its replacement: ```aon pkg: { path:"corp.example/schemas/service" version:"1.4.2" main:"service.aon" } publish: public retract: ["1.4.1"] moved: "corp.example/schemas/service2" ``` Both fields are shown together above for the field's sake. A package declares `moved` when its path is finished, and a path that is finished gains no further versions. ## Names and versions A **package path** is a domain-shaped path: the first element contains a dot, and no element is a major version, because compatibility is computed when a package is published rather than declared in its name. A path that routes to a package becomes a directory on every platform the toolchain runs on, so the shape is bounded before anything is built from it. - At most **512** characters, and at most **32** elements. - No element is empty, and none begins or ends with `.`. - The part of an element before its first `.` is not a reserved device name. The set, matched without regard to case: `con`, `prn`, `aux`, `nul`, `com1` to `com9`, and `lpt1` to `lpt9`. - An uppercase letter is written as `!` and its lowercase form in every store, so two paths differing only in case stay two identities on a filesystem that would fold them into one. A **version** is three parts separated by `.`, each either `0` or a digit string with no leading zero. That is what a publish mints and what a manifest carries. The comparator reads a part a version omits as `0`, so `1.2` and `1.2.0` are one version to it, and a lockfile rewritten from either says the same thing. An **alias key** is `alias:` followed by the name the imports use. A **local** import carries `./`, `../` or `/`; a bare relative path whose last segment has an extension the include table knows is refused with `module_local` rather than read as a package. ## Where a module resolves from Resolution reads local stores only, in this order, and stops at the first that holds the module: 1. `aontu_meta/vendor/` of the project whose directory holds `pkg.aon`. 2. `aontu_meta/vendor/` of each project enclosing that one, outward, so a vendored module resolves its own dependencies from the tree that vendored it. 3. The user cache, which is keyed by canon-hash and so is consulted only where the expected hash is already known. The cache directory is the first of these that applies: | condition | directory | |---|---| | `XDG_CACHE_HOME` is set | `$XDG_CACHE_HOME/aontu/pkg` | | `HOME` is set | `$HOME/.cache/aontu/pkg` | | Windows, and `LOCALAPPDATA` is set | `%LOCALAPPDATA%\aontu\pkg` | | none of those | there is no cache, and the vendor tree is the only store | ## The caps Every cap is a fixed number, the same in both implementations, and each is refused by name so a report says which one was reached. | cap | value | refused as | |---|---|---| | a module path, in characters | 512 | `module_path` | | elements in a module path | 32 | `module_path` | | dependency nesting, in levels | 16 | `module_depth` | | packages in one closure | 1024 | `closure_too_large` | | an archive, compressed | 16777216 bytes | `archive_too_large` | | an archive, extracted | 67108864 bytes | `archive_bomb` | | files in an archive | 4096 | `archive_too_many_files` | | one file in an archive | 8388608 bytes | `archive_bomb` | The four archive caps are applied twice. A consumer applies them while acquiring, before a byte of the archive is parsed. A publish applies the same four before anything is minted, so a package that every consumer would refuse never leaves the machine; there the reasons are reported as what stopped the mint rather than as a refusal code. ## What an archive may hold The archive of a package holds source and the text that travels with it. The list is enumerated and refused by default, so a new extension is admitted by a change to the engine and never by a package. | group | admitted | |---|---| | aontu source | `aon`, `aontu` | | data the include table reads | `json`, `jsonld`, `jsonc`, `json5`, `jsonic`, `jsc`, `toml`, `yaml`, `yml`, `ini` | | text | `md`, `txt` | | by name, with no extension | `LICENSE`, `NOTICE` | No path element may begin with `.`, and nothing under `aontu_meta/` is sent: a lockfile and a vendor tree are the consuming project's, and a package that carried them would ship one project's resolution to another. Anything else in the tree is refused with `archive_entry_forbidden`, which names the file. ## Refusals A package operation refuses with a **code**, a message, and the package it was about. There are thirty codes. Six of them are also engine codes, raised while a document is evaluated, and those six are in the registry with a class and a registered-at version: see [the codes](/docs/reference-errors#the-codes) for `module_path`, `module_local`, `module_missing`, `module_moved`, `module_integrity` and `module_depth`. The other twenty-four name a refusal by a verb. They are not findings, carry no class, and arrive under `refusal` in a verb's `--format json` report. The step each code is raised at: **resolution** reads the local stores while a document is evaluated; **configuration** reads `repo` and the signing key; **acquisition** reads a repository, in the order version list, manifest, proof, archive; **publication** mints and sends. | code | raised at | what it means | |---|---|---| | `archive_bomb` | acquisition | a file, or the running total, is over the size cap once extracted | | `archive_digest_mismatch` | acquisition | the archive's digest is not the one the manifest declares | | `archive_entry_forbidden` | acquisition | the archive carries a file the allowlist does not admit | | `archive_not_canonical` | acquisition | the zip is not in the canonical form one tree has one digest under | | `archive_path_invalid` | acquisition | a path in the archive breaks the element rules | | `archive_too_large` | acquisition | the archive is over the compressed cap | | `archive_too_many_files` | acquisition | the archive is over the file-count cap | | `base_not_https` | configuration | a repository base is neither `https` nor `http` on a loopback host | | `closure_too_large` | acquisition | the closure is over the package cap | | `config_invalid` | configuration | a `repo.trust` entry is not a pattern, or names no signer | | `fetch_failed` | acquisition | no repository answered, or answered with a status, for a list, manifest or archive | | `file_manifest_mismatch` | acquisition | the archive holds a file the manifest does not list, or lacks one it does | | `inclusion_missing` | acquisition | the trust entry requires log inclusion, which a key proof does not carry | | `key_invalid` | configuration | the key file is not a PEM private key, or is not Ed25519 | | `list_rollback` | acquisition | a version seen before is absent from the list now served | | `manifest_invalid` | acquisition | the manifest is malformed, names another package or version, or declares a dependency without the package it names | | `module_depth` | resolution, acquisition | dependency nesting is past the level cap | | `module_integrity` | resolution, acquisition | the module's meaning is not the canon-hash that was pinned | | `module_local` | resolution | a local file was written without a `./` prefix | | `module_missing` | resolution | no store holds the module, or an alias is not declared | | `module_moved` | resolution, acquisition | the package declares `moved`, and nothing follows a move | | `module_path` | resolution | the path breaks a name rule or a length cap | | `not_public` | publication | the package does not declare `publish: public` | | `path_moved` | publication | the path is frozen by a `moved` declaration | | `private_name_public_path` | configuration | the name is on the private list and `repo.private_base` names no repository | | `proof_missing` | acquisition | no proof is served for the version | | `proof_signer_untrusted` | acquisition | the proof's signer is not the one the trust entry accepts for that name | | `response_mismatch` | acquisition | a served document is malformed, or names something other than what was asked for | | `tombstoned` | acquisition | the version was withdrawn by the repository | | `version_exists` | publication | the version was published before, and a version is never reusable | ## Related - [`aontu sync`](/docs/reference-api#aontu-sync) for the verb that makes a project correct, its report, and the order the checks run in. - [`aontu publish`](/docs/reference-api#aontu-publish) for what a publish sends, the compatibility gate, and the proof. - [`aontu pkg`](/docs/reference-api#aontu-pkg) for the individual operations, including `verify`, `vendor`, `tree` and `serve`. - [Modules](/docs/reference-language#modules) in the language reference for how a reference routes to a package, a file or an alias. - [The codes](/docs/reference-errors#the-codes) in the errors reference for the six codes that are also engine codes, with their classes. - [Vendor a dependency closure](/how-to/vendor-a-dependency-closure) and [publish a package](/how-to/publish-a-package) for the two jobs in recipe form. --- # Grammar reference _The published grammar rule by rule, the spellings the parser accepts beyond it, and the checks that hold the four grammar files to the engine._ Source: https://github.com/aontu-lang/aontu/blob/main/docs/reference-grammar.md · Page: https://aontu.dev/docs/reference-grammar One grammar, published four times. The rules below are the **emission surface**: what a document should be allowed to write, which is a superset of JSON plus the operators, constraints and marks the canonical form emits. The grammar is conservative by construction, so it accepts less than the parser does and never more. This page is normative for the published grammar as a set of rules: which rules there are, what each spells, which spellings the parser accepts beyond them, and what holds the four files to the engine. It does not draw the grammar: [The published grammar](/docs/reference-language#the-published-grammar) in the language reference carries the railroad diagrams and the status of the emission surface, and each construct is specified where the language reference defines it. A refusal while a document is read is class `parse`, and the codes are the [errors reference](/docs/reference-errors#class-parse). ## Contents - [The published files](#the-published-files) - [The rules](#the-rules) - [Two notation choices](#two-notation-choices) - [Where order matters](#where-order-matters) - [What the parser also accepts](#what-the-parser-also-accepts) - [What the grammar excludes](#what-the-grammar-excludes) - [Lexical sets](#lexical-sets) - [What holds the files to the engine](#what-holds-the-files-to-the-engine) - [Related](#related) --- ## The published files | file | notation | for | held to | |---|---|---|---| | [`grammar/aontu.abnf`](https://github.com/aontu-lang/aontu/blob/main/grammar/aontu.abnf) | RFC 5234, with RFC 7405 case-sensitive literals | a reader, and the railroad diagrams the language reference draws | interpreted and run against every canonical output of the shared spec suite; every rule reachable; the name set checked against the engine | | [`grammar/aontu.gbnf`](https://github.com/aontu-lang/aontu/blob/main/grammar/aontu.gbnf) | GBNF | a constrained decoder | interpreted and run against the same corpus; the name set checked against the engine | | [`grammar/aontu.lark`](https://github.com/aontu-lang/aontu/blob/main/grammar/aontu.lark) | Lark | a parser generator | rule names checked against the other two; the name set checked against the engine | | [`grammar/aontu.tmLanguage.json`](https://github.com/aontu-lang/aontu/blob/main/grammar/aontu.tmLanguage.json) | TextMate | editor highlighting | the name set checked against the engine, and the copy the editor extensions ship checked against this one | The first three are one grammar in three notations. Twenty-nine rules are named in all three, rule for rule and name for name. The ABNF carries four more, which the other two spell inline rather than name: `unescaped`, and RFC 5234's core rules `ALPHA`, `DIGIT` and `DQUOTE`, written out so that file stands alone. The fourth file is a highlighter rather than a parser, and shares only the built-in name set. ## The rules Thirty-three rules, in the order the file defines them. Every one is reachable from `root`, and a rule that stops being reachable fails the suite rather than sitting in the file. | rule | what it spells | |---|---| | `root` | a whole document: whitespace, one value, whitespace | | `value` | a value, which is a disjunction | | `disjunct` | alternatives, separated by the vertical bar | | `conjunct` | values that must all hold, separated by `&` | | `prefixed` | a `*` preference, or a sum | | `sum` | atoms joined by `+` | | `atom` | a map, a list, a call, a reference, a kind, the placeholder, a scalar, or a parenthesised value | | `map` | a brace-delimited bag of entries | | `entry` | a spread template, or a pair | | `spread` | `&`, `:` and the template every key of the bag must satisfy | | `pair` | a key, an optional `?`, `:`, and a value | | `list` | a bracket-delimited sequence of elements | | `element` | a spread template, or a value | | `func` | a built-in name applied to arguments | | `name` | the closed set of built-in names | | `ref` | a path reference, absolute from `$` or relative from `.` | | `segment` | one path segment: letters, digits and `_` | | `place` | the placeholder, `_`, bare | | `kind` | a kind name | | `scalar` | a string, an exact number, a number, `true`, `false` or `null` | | `string` | a double-quoted run of characters | | `char` | one character: unescaped, or `\` and an escape | | `unescaped` | every code point but the quote and the backslash | | `escape` | what may follow a backslash, including `u` and four hex digits | | `hex` | one hexadecimal digit | | `exact` | an exact literal: `0d`, digits, an optional fraction and exponent | | `number` | digits, an optional fraction and exponent | | `exponent` | `e` or `E`, an optional sign, and digits | | `digits` | one or more digits | | `ws` | the whitespace characters | | `ALPHA` | a letter | | `DIGIT` | a digit | | `DQUOTE` | the double-quote character | `root` is the start rule. Whitespace is permitted between every element and is carried by `ws` rather than drawn. ## Two notation choices **Every literal is case-sensitive**, spelled `%s"..."`. A plain `"..."` is case-insensitive in RFC 5234, and this language is not: `TRUE` is a bare word and `true` is a boolean. The reader refuses a bare quoted literal rather than guess which was meant, so a rule cannot acquire a case-insensitive literal by accident. **Alternation is ordered.** RFC 5234's `/` is formally unordered, and both consumers of the file take the first branch that matches. Where one spelling is a prefix of another the longer comes first, which is what makes the ordered reading and the unordered one accept the same language. ## Where order matters Ordered alternation has a consequence wherever one branch matches a prefix of what another would match: the longer branch has to come first. Every such place in the file, and what a swap would do: | earlier | later | what a swap would do | |---|---|---| | `exact` | `number` | `0d5` begins with a digit, so `number` would match `0` and leave `d5` unread | | `copyfiles` | `copy` | a call to `copyfiles` would be read as `copy` | | `listitems` | `list` | a call to `listitems` would be read as `list` | | `refer` | `re` | a call to `refer` would be read as `re` | | `rel` | `re` | a call to `rel` would be read as `re` | | `rem` | `re` | a call to `rem` would be read as `re` | | `rep` | `re` | a call to `rep` would be read as `re` | The first row is an ordering between two rules, in `scalar`. The rest are built-in names inside the `name` rule, and are every prefix pair that rule holds. ## What the parser also accepts The grammar describes what a document should write. The parser accepts these spellings too, and each means what the canonical form in the third column says. A model generating aontu should write the canonical spelling; a human reading a document may meet either. | spelling | written | means | |---|---|---| | an unquoted key | `a: 1` | `{"a":1}` | | a document with no outer braces | `a: 1 b: 2` | `{"a":1,"b":2}` | | a bare word as a string | `a: hello` | `{"a":"hello"}` | | a single-quoted string | `a: 'x'` | `{"a":"x"}` | | a trailing comma | `a: {b: 1,}` | `{"a":{"b":1}}` | | a path-flattened pair | `a: b: 1` | `{"a":{"b":1}}` | | a comment to end of line | `a: 1 # note` | `{"a":1}` | | an unquoted key the canonical form quotes | `a-b: 1` | `{"a-b":1}` | A newline separates entries as a comma does, and a backtick-quoted string reads as a double-quoted one. Two entries naming the same key meet rather than replace, which is the language's own rule and not a spelling: see [the unification rules](/docs/reference-language#unification-rules). ## What the grammar excludes Two forms are deliberately absent, and the suite requires the grammar to refuse them rather than merely omit them: - **`@"..."` includes.** A generated document should describe values rather than reach for files, so nothing a constrained decoder emits can read the filesystem. - **The tolerated spellings above.** The canonical form quotes every key and writes one spelling per construct, and one spelling is what a grammar is for. An excluded form is still valid aontu. The exclusion says what a generator should write, not what the engine reads. ## Lexical sets | set | members | |---|---| | whitespace | space, tab, carriage return, line feed | | an escape after `\` | `"`, `\`, `/`, `b`, `f`, `n`, `r`, `t`, and `u` with four hex digits | | unescaped | every code point except `"` and `\`, control characters included | | a hex digit | `0` to `9`, `A` to `F`, `a` to `f` | | a path segment | letters, digits and `_`, and never `-` | Control characters are admitted because the canonical form writes them escaped, and a grammar that refused them would refuse less than the parser accepts in the one direction this file may not take. A path segment has no `-` because a hyphen is not a bare-text character: `a:6-2` is a parse error rather than the key `6-2`, so admitting it in a segment would describe a language wider than the one the engine reads. ## What holds the files to the engine The grammar is executed rather than published and left alone. In both implementations' test suites: | check | what it proves | |---|---| | the reader parses the file | the notation is well formed, and no literal is case-insensitive | | every canonical output of the shared spec suite is accepted | the grammar has not fallen behind the engine | | every excluded form is refused | the exclusions are real, rather than an omission | | the three parser grammars name the same rules, less the four the ABNF writes out | one grammar, three notations | | every rule is reachable from `root` | no rule outlives the construct it spelled | | all four files name exactly the engine's built-ins | a function added or retired cannot leave a grammar behind | | the editor extensions' copy is the published file | an editor highlights what the engine reads | ## Related - [The published grammar](/docs/reference-language#the-published-grammar) in the language reference for the railroad diagrams and the status of the emission surface. - [The formatted form](/docs/reference-language#the-formatted-form) for what `aontu fmt` writes, which is the spelling a page shows. - [Class `parse`](/docs/reference-errors#class-parse) in the errors reference for every code a refusal while reading carries. - [Grammars: `abnf()` and `parse()`](/docs/reference-language#grammars-abnf-and-parse) for using a grammar of your own inside a document, which is a different thing from this one. - [The published grammar](/docs/reference-api#the-published-grammar) in the API reference for the files as an artefact of the distribution. --- # Agent and editor reference _Every door a machine reads a model through, the one answer shape they share, what none of them does, and the include posture each takes._ Source: https://github.com/aontu-lang/aontu/blob/main/docs/reference-agents.md · Page: https://aontu.dev/docs/reference-agents An editor and an agent read a model through the same doors, and there are more of them than the command line. This page is normative for that surface taken as a whole: which doors there are, what each answers, what none of them does, what posture each takes towards `@"..."` includes, and which implementation carries it. Each door's own specification is elsewhere and is linked from the table below. Nothing here repeats a tool list, an option or an exit table: the [API reference](/docs/reference-api#command-line-interface) specifies every verb, [the language server](/docs/lsp) specifies the protocol and the library behind it, and the [errors reference](/docs/reference-errors#exit-codes) specifies the exit codes and the codes a report carries. ## Contents - [The doors](#the-doors) - [The machine answer](#the-machine-answer) - [What none of them does](#what-none-of-them-does) - [Includes, by door](#includes-by-door) - [The teaching topics](#the-teaching-topics) - [Which implementation has it](#which-implementation-has-it) - [Related](#related) --- ## The doors | door | reached by | answers | specified in | |---|---|---|---| | the verbs | `aontu `, with `--format json` where a report is wanted | one report a verb | [command-line interface](/docs/reference-api#command-line-interface) | | the embedded API | a library call, in either implementation | the same reports, as values rather than text | [TypeScript API](/docs/reference-api#typescript-api), [Go API](/docs/reference-api#go-api) | | the language server | `aontu lsp`, or the `aontu-lsp` binary, over stdio | diagnostics, hover and completion as an editor asks for them | [the language server](/docs/lsp) | | the tool server | `aontu mcp`, or the `aontu-mcp` binary, over stdio | a tool for each report it serves, which is a subset of the verbs, under the same JSON contract | [the MCP server](/docs/reference-api#the-mcp-server) | | the teaching pack | `aontu help ` | the language, from inside the binary, with no network and no checkout | [`aontu help`](/docs/reference-api#aontu-help) | | the code index | `aontu explain `, or `--list` | what one refusal means, or every registered code with its class | [errors reference](/docs/reference-errors#the-codes) | | the starting documents | `aontu init [dir]` | a model, an instance of it, and the checks to run | [`aontu init`](/docs/reference-api#aontu-init) | | the prose entrypoint | `aontu agentsmd --write ` | the stanza naming a document's pin, root keys, shape and commands | [`aontu agentsmd`](/docs/reference-api#aontu-agentsmd) | | the structured-output bridge | `aontu jsonschema` | a JSON Schema for the document, and the list of what it could not say | [`aontu jsonschema`](/docs/reference-api#aontu-jsonschema) | | the published grammars | the files in [`grammar/`](https://github.com/aontu-lang/aontu/blob/main/grammar/) | the emission surface, for a constrained decoder or a highlighter | [grammar reference](/docs/reference-grammar) | | the editor plugins | [`editors/`](https://github.com/aontu-lang/aontu/blob/main/editors/) for Visual Studio Code, Emacs and Vim | the language server, wired to an editor | [editor configuration](/docs/lsp#editor-configuration) | A door is a way in, not a tier: the tool server answers with the report the verb prints, and the verb prints what the library returned. One question has one answer whichever door it arrives by, which is the property that makes a transcript in a document and a tool call in an agent the same evidence. ## The machine answer `--format json` is a per-verb option rather than a global one: `fmt`, `template` and `agentsmd` do not take it, and refuse it by name. Where a verb does answer JSON it answers one object, and every one of them opens with the same block: | key | carries | |---|---| | `aontu` | `verb` and `version`: which verb answered, and the version that answered it | `aontu trace --format json` is the one exception: it answers its record alone, under `trace`. **How an answer says whether it holds** is one of three shapes. The shape belongs to the verb rather than to the run, so a document that does not hold changes the values and never the keys: | shape | the answer carries | the verbs | |---|---|---| | `ok` and the answer beside it | `ok`, with `out` or `record` | the bare entry point, `model get`, `model why` | | a verdict word | `verdict`, with the verb's own fields | `vet`, `subsume`, `breaking`, `relations`, `reaches`, `trim`, `jsonschema`, `view`, `render --check`, `allow` | | the payload alone | neither: the object is the answer | `hash`, `help`, `explain` | **A refusal is an answer**, and the findings say what was refused. The key they arrive under is the verb's too: | findings under | the verbs | |---|---| | `findings` | the bare entry point, `model get`, `model why`, `vet`, `subsume`, `breaking`, `relations`, `allow` | | `errors` | `reaches` | | nothing: the verb reports none | `trim`, `jsonschema`, `view`, `render --check`, `hash`, `help`, `explain` | A verb that reports no findings says what it could not do in a field of its own instead: `view`'s `loss`, `jsonschema`'s `lossy` and `render --check`'s `drift`. A caller branches on `ok` or on `verdict` rather than parsing prose, and the exit code carries the same answer: the [errors reference](/docs/reference-errors#exit-codes) is normative for it. Each verb's own section names the fields beside these. The tool server keeps the same rule at the protocol level: a tool whose document does not hold answers with its own report and `isError: false`, because the report is the answer. `isError` is reserved for a call that could not be made at all, such as an unknown tool, a malformed argument, or a file argument the server does not serve. ## What none of them does These hold across every door, and each is the property an integrator needs before wiring one into something that runs unattended: - **The language server never writes a file.** It answers, and the editor owns the buffer. - **The tool server never writes a file.** `set` answers the new overlay text, and the caller owns the write. - **`init` never overwrites.** If any member of the trio already stands in the directory it refuses before writing any of them, so a refused run leaves nothing half written. - **`agentsmd --write` changes only what lies between its two markers.** Everything outside them is left exactly as it was, which is what makes it safe to re-run and safe to point at a file someone else writes prose in. - **A `--check` run writes nothing.** It compares and exits. - **Evaluation never reaches the network.** A module resolves from local stores only; the package verbs are the only ones that fetch, and they say so by name. ## Includes, by door `@"..."` reads a file while a document is evaluated, so every door that evaluates a document a caller supplied has a posture towards it. The postures are the [trust contract](/docs/trust); this is which door takes which by default. | door | default | how it is set | |---|---|---| | the verbs | `system`: an include reads what the process can read | `--trust none`, `--trust root[:dir]`, or `--include-root `, on every verb that reads a document; `help`, `explain` and `init` read none and refuse them, and `lsp` takes no arguments at all | | the embedded API | whatever the caller passes, and `system` where it passes nothing | the `trust` option of the call | | the language server | confined below the workspace folder the client named, and `system` where the client named none | `initializationOptions.aontu.trust.include`: `system`, `none`, a root, or an in-memory map | | the tool server | `none`: every include is denied | `--root `, which confines includes below the resolved root and serves the path arguments of every tool | A server that evaluates source from a caller is exactly the place an unconfined include is a mistake, which is why the tool server denies them until a root says otherwise, and why a path argument is refused until then too. ## The teaching topics `aontu help` lists what the binary carries, and `aontu help ` prints one. Each is staged into both implementations at build time from its source, and both suites hold the staged copy to that source byte for byte, so the answer travels with the binary rather than with a checkout. | topic | what it carries | |---|---| | `tasks` | the job-to-verb index: which verb answers the question you have | | `language` | the grammar card: everything the language spells, on one page | | `examples` | the ladder, from plain JSON upward, one addition at a time | | `codes` | what a refusal means, and what to do about it | | `grammar` | the published ABNF, for a parser or a constrained decoder | `aontu help` with no topic lists them, `aontu --help` documents the verbs and their flags, and `--format json` answers either as an object. ## Which implementation has it | door | TypeScript | Go | |---|---|---| | the verbs | yes | yes, less the tool server | | the embedded API | yes | yes | | the language server | yes | yes | | the tool server | yes | no: `aontu mcp` says so and exits `2` | | the teaching pack | yes | yes, from a committed copy of the staged pack | | the code index | yes | yes | | the starting documents | yes | yes | | the prose entrypoint | yes | yes | | the structured-output bridge | yes | yes | | the published grammars | shared: the files are the distribution's, not a port's | shared | | the editor plugins | either binary serves them | either binary serves them | The tool server is the one door with no Go half. Its role there is the embedded API, which carries the same calls. ## Related - [The language server](/docs/lsp) for the protocol surface, the library API in both implementations, and how diagnostics are computed. - [The MCP server](/docs/reference-api#the-mcp-server) for the tools, what each answers, and the confinement the `--root` posture adds. - [The trust contract](/docs/trust) for what an include may read, and what a profile confines. - [Exit codes](/docs/reference-errors#exit-codes) in the errors reference for the verdict every verb carries out of a run. - [Grammar reference](/docs/reference-grammar) for the published grammars and what holds them to the engine. - [Give an agent an entrypoint](/how-to/give-an-agent-an-entrypoint) and [wire your editor](/how-to/wire-your-editor) for the two jobs in recipe form. --- # API reference _The API and the command line: every verb, every flag, every exit code, and the MCP server._ Source: https://github.com/aontu-lang/aontu/blob/main/docs/reference-api.md · Page: https://aontu.dev/docs/reference-api The programming interfaces of both implementations, plus the command-line tool. For the language itself see the [Language reference](/docs/reference-language). ## Contents - [Command-line interface](#command-line-interface) - [Evaluation consumes the tree](#evaluation-consumes-the-tree) - [TypeScript API](#typescript-api) - [`Aontu`](#class-aontu) - [`AontuOptions`](#aontuoptions) - [`AontuContext`](#aontucontext) - [`Val`](#val-typescript) - [Exact numbers and `exactJSON`](#exact-numbers-and-exactjson) - [Variables](#variables) - [Exports](#exports) - [Go API](#go-api) - [`Aontu`](#type-aontu) - [`Val`](#val-go) - [Exact numbers in Go](#exact-numbers-in-go) - [`Ctx` and errors](#ctx-and-errors) - [Variables in Go](#variables-in-go) - [Behavioural parity](#behavioural-parity) --- ## Command-line interface Both implementations ship the same `aontu` command. It evaluates a source file (or stdin) and prints the result, or starts a REPL when run interactively with no file. The synopsis, from the command itself: ```sh $ aontu --help Usage: aontu [options] [file] aontu vet [options] [more-data...] aontu subsume [options] aontu breaking --against [options] aontu trim --check [options] aontu relations [options] aontu reaches [--relation ] [options] aontu view [options] ... aontu view --views [--check] [options] aontu jsonschema [--at ] [--strict] [options] aontu template [--resugar] [--check] [--marker ] [--profile ] aontu trace [--at ] [--format json] [--marker ] [--profile ] aontu render [--check] [--at ] [--format json] [--marker ] [--profile ] aontu hash [options] aontu sync [--frozen] [options] [dir] aontu add [@] [options] [dir] aontu get [@] [options] [dir] aontu remove [options] [dir] aontu why [options] [dir] aontu publish [--yes] [--to ] [options] [dir] aontu pkg tidy|verify|vendor|manifest|refreeze|tree|outdated|serve [options] [dir] aontu pkg keygen aontu model get [options] aontu model why [options] aontu model set =... --entry --overlay aontu allow --role [--at ] ... aontu agentsmd [--write ] [--depth ] aontu fmt [-w|-l|--check|-d|--lint] [--marker ] [--profile ] ... aontu help [topic] [--format text|json] aontu explain | --list [--format text|json] aontu init [dir] aontu lsp aontu mcp [--root ] Evaluate an aontu source file and print the result as JSON. With no file on an interactive terminal, start a REPL. With no file and piped input, read the source from stdin. ... ``` The elided remainder lists every option; the per-verb sections below carry the same lists. Three options apply to every verb that reads a document: `--trust ` (the include capability: `system`, `none`, or `root[:dir]`; see [`AontuOptions`](#aontuoptions) for what each admits), its shorthand `--include-root `, and `--text-ext [,]` (extensions an include additionally reads as text; repeatable, and a leading dot is optional). The last rides WITH the capability rather than beside it: both answer what an include may read. Five verbs take none of the three: [`help`](#aontu-help), [`explain`](#aontu-explain) and [`init`](#aontu-init), which read no document; [`lsp`](#aontu-lsp), which takes no arguments at all; and [the MCP server](#the-mcp-server), which confines with `--root ` instead. [`fmt`](#aontu-fmt) takes all three, and they govern the document it evaluates, which is the one `--profile` names rather than the one it formats. `sync`, `add`, `get` and `remove` take them and then refuse a `root` capability, because a confined run cannot reach the package cache they write. **Behaviour** - **File:** `aontu config.aontu` reads, unifies and prints the file. Relative `@"file"` loads inside it resolve against the file's own directory, so it works from any working directory. - **Exactly one file.** The bare form is `aontu [options] [file]`, singular, and a second filename is a usage error (exit 2) rather than a silent discard. This is what makes a MISTYPED VERB fail loudly: `vet2` matches no subcommand, so it falls through as a file name, and `aontu vet2 schema.aon good.json` is a usage error rather than a plausible pass: in the one place a tool loop reads the exit code to decide whether the data is good. A file genuinely named like a verb is still reachable as `./vet`. - **One mistyped verb is diagnosed too.** A single argument that cannot be read and is shaped like a bare word (no separator, no extension) is reported as a verb rather than as a missing file, with the nearest verb named: `aontu vett` answers ``aontu: `vett` is not a file, and not a verb this port knows`` and suggests `aontu vet`, at exit 2. A path-shaped argument (`./help`, `help.aon`, `/tmp/help`) keeps the file diagnosis and its exit 1, which is the same escape hatch the subcommand dispatch uses. - **Stdin:** `echo 'a:1 b:$.a' | aontu` reads source from the pipe. - **REPL:** `aontu` with no file on a terminal starts an interactive loop; each line is evaluated and printed. - Output is pretty-printed JSON by default, or canonical form with `--canon`. - **Exact numbers keep their digits.** A document using the `0d` exact literals prints them in full, at any magnitude: `x:0d9007199254740993` prints `9007199254740993`, not a rounded `…992`. The TypeScript CLI gets this from the library's [`exactJSON`](#exact-numbers-and-exactjson) export, the Go CLI from a `json.Encoder` over the [marshalling types](#exact-numbers-in-go), with HTML escaping **off** in both, so `<`, `>` and `&` stay literal and the two CLIs print the same bytes. - Results go to **stdout**; errors go to **stderr** with a non-zero exit status (`1` for an evaluation error, `2` for a bad option). - **`--format json` makes the answer an object**, so the default entry point reports like every other verb rather than like a stream of prose: `{aontu, findings, ok, out}` on **stdout**, whether the document evaluated or not. `out` is the text the default form prints (the generated JSON, or the canonical form under `--canon`), empty when it did not evaluate; `findings` then carries one finding with the `code` to hand to [`aontu explain`](#aontu-explain), its `class` from the registry, and the headline as `message`. The finding names no site and carries no hint: the frames under the headline are drawn for a person, and hint prose is deliberately outside cross-port parity. Exit codes are unchanged. ### `aontu vet` Validate data documents against a schema document. This is the emit → validate → repair loop's entry point: an agent writes a document, `vet` says what does not hold and where, and the exit code says which kind of "no" it was. ``` aontu vet [options] [more-data...] --at Validate against this path of the schema ($.a.b) --closed Refuse keys the anchor does not declare --partial Residue is reported but does not fail the run --max-errors Cap the finding list (default 20) --coverage Report what the check examined --strict-coverage --coverage, and exit 1 when the run was vacuous --coverage-at

Measure coverage under this path of the data only --format text (default), json or sarif --watch Re-run whenever a watched file changes ``` **Exit codes are verdict classes**, not a pass/fail bit, because the three ways to fail call for three different responses: | Exit | Verdict | Meaning | |------|---------|---------| | 0 | `valid` | the data unifies and is concrete (or `--partial`) | | 1 | `invalid` | the data does not hold: a contradiction it can never satisfy, or a document that would not parse | | 1 | `valid` | a VACUOUS run under `--strict-coverage`: the unification held and measured nothing, so the verdict stands and only the status says so (below) | | 2 |: | usage: a bad option, or a file that cannot be read | | 3 | `incomplete` | no contradiction, but the truth is not yet satisfied | | 4 | `error` | the run could not be set up from the schema side: an unusable schema, or an `--at` that names nothing: never the data's fault | Each data file is vetted separately, and the worst verdict wins: two data files are two candidates for the same truth, not one merged candidate. `--max-errors` caps the whole report, not each file, and says so with `truncated`. **A data file that will not parse is the data's fault**, and is reported as one `parse`-class finding with a site in that file: not as a broken schema. The distinction matters to the loop the verb exists for: exit 1 says "repair what you emitted", exit 4 says "the truth you were given is unusable, stop". #### What the check examined A check that examined **nothing** and a check that **passed** answer the same. That is not a bug in the unifier, which answered correctly about the document it was given, but it is a hole in a gate: the caller reads exit 0 and reports success. The usual cause is one construct. A schema written with the wildcard other tools use is not a wildcard here: ``` entity: { "*": { table: string } } ``` `"*"` is a key **named** `*`. It declares an entity called `*`, meets no data key, and constrains nothing, so data with `table: 42` vets `valid`. The template is [`&:`](/docs/reference-language), which meets every key of the map it sits in. `--coverage` adds a `coverage` object to the report, and the report is the answer: | field | is | |---|---| | `checked` | data **leaves** a schema declaration constrained | | `leaves` | data leaves in all, under `--coverage-at` when given | | `declared` | declarations the schema makes under the anchor: a map key, a list index, or a template, at every depth | | `unchecked` | the shallowest data paths no declaration constrained | | `unused` | the shallowest declarations no data path met | | `vacuous` | no data leaf was constrained, over a document that has leaves | **Leaves, not paths.** A leaf is where a value lives, and matching a container constrains no value: a schema saying only "there is a key called `entity`" has checked nothing, and `checked` is the number that says so. A document with no leaves is not vacuous either, because there was nothing to examine. The lists name the **shallowest** paths: a subtree nothing constrained is named once rather than once per leaf. The text form prints the first ten of each and counts the rest; the JSON form carries every one. `--strict-coverage` makes a vacuous run **exit 1**. The verdict word is unchanged (the unification really did hold), so nothing that passes today starts failing, and the reason goes to stderr while stdout stays a report contract. It is the flag a CI gate and an agent loop both want, and it implies `--coverage`, because a gate cannot fire on what was never measured. Across several data files the schema side is counted once and the data side adds up: a declaration one file exercised is not unused, and `vacuous` means no file constrained anything. The accounting is **structural**: what the schema declares about the data, rather than a reading of the meet. A meet-based reading would count a value the data supplied to itself as covered, which is the opposite of the question. It costs one extra evaluation of the data document, and only when asked for. **A parse failure is located.** Its single site carries the parser's own row and column, 1-based: the same position the human renderer draws its caret under. A document whose second line is `b: ]` reports `row: 2, col: 4`, in both ports. **A site has an extent, so a finding can be repaired.** Beside `row` and `col` a site carries `len` (the span in UTF-16 code units, the units `col` is already counted in) and `src`, the source text that span covers. Both are `-1` and `""` when unknown, and a consumer must not edit a site that says so. The extent is not optional detail, because **`value` is the canon and not the source text**. Vetting `port: 0x1F` reports `value: "31"` at column 7, so replacing `(col, value.length)` writes `port: 90001F`. With the span the edit is `(col, len)` (`(7, 4)`) and lands exactly on `0x1F`. `src` is what makes the span **verifiable**: read the document at `(row, col, len)`, compare it to `src`, and refuse if they differ. That check matters most where the span is correct but partial. A site names the token it points at, exactly as `row` and `col` always have, so a scalar reports its whole literal while a compound reports its opening token: `min(1)` reports `src: "min"`, a map reports `src: "{"`. Seeing `min` where it expected `min(1)`, a consumer refuses rather than replacing the name and orphaning the arguments. To see the site shape, pin `port` in a one-line `schema.aon`: ```aontu port: 8080 ``` and vet a `data.aon` that spells a different port in hex: ```aontu port: 0x1F ``` ```sh $ aontu vet --format json schema.aon data.aon { "aontu": { "verb": "vet", ... "sites": [ { "col": 7, "file": "data.aon", "len": 4, "role": "data", "row": 1, "src": "0x1F", "value": "31" }, ... $ echo $? 1 ``` `value` says `31`, `src` says `0x1F`, and `len` says the span is four code units: the three facts an editing consumer needs, together. `aontu model why` carries the same pair: each conjunct in the record has the `len` on its site and the contribution's own `src` beside its `canon`. **A site names the file whose text it excerpts**, which for a modular document is not the entry file. A constraint written in `lib/types.aon` and reached through `@"./lib/types.aon"` is reported at `lib/types.aon` with that file's row and column: never at the entry with the included file's coordinates, which is a real filename against a line it may not have. The name is the one the CALLER'S OWN spelling reaches: `vet contract.aon` names `types.aon`, `vet a/b/contract.aon` names `a/b/types.aon`, and an absolute entry keeps absolute includes. So a site can be opened from wherever the command was run, and a report stays repo-relative, which is what a SARIF upload needs. Identity is still the resolved path underneath: two documents loading one library by different relative spellings are one file, not two. **Every verdict carries its finding, `error` included.** A schema that does not stand up (a contradiction inside it, a document that will not parse, a merge marker) reports what failed and where, while the verdict stays `error`: whose fault it is and what the fault is are two separate answers, and the report gives both. Every site is in the schema (role `schema`), and a contradiction names both of its operands, exactly as one in the data would. A report that said only `error` was the one a repair loop could do nothing with. **`--at` takes a structural path**: map keys and list indices, the same thing a reference means by `$.a.b`, with an index spelled as a plain decimal integer. A path that names nothing is verdict `error`, carrying the same `no_path` finding `get` and `why` give, including the "did you mean" note when a near key exists. **Relative `@"file"` loads inside either document** resolve from that document's own directory, exactly as they do for `aontu `. **A finding names both sides.** Sites are labelled by provenance (`data` first, because that is the one to edit) rather than by the source-order heuristic a single-document error uses. Write a closed schema as `service.aon`: ```aontu service: close({ name:string port: *8080|integer replicas:integer }) ``` and a `deploy.json` with one mistyped key and one string where an integer belongs: ```json {"service": {"name": "checkout", "prot": 8080, "replicas": "3"}} ``` ```sh $ aontu vet service.aon deploy.json verdict: invalid $.service.prot: closed [conflict] [aontu/closed]: Cannot resolve value at path $.service.prot data: deploy.json:1:42 (8080) $.service.replicas: no_scalar_unify [conflict] [aontu/no_scalar_unify]: Cannot unify values at path $.service.replicas data: deploy.json:2:15 ("3") schema: service.aon:1:59 (integer) $ echo $? 1 ``` The `closed` finding has no schema site (there is no line that refuses `prot`, only a `close()` that never declared it) and the `no_scalar_unify` finding names both. `--format json` emits the same report as an object, with an `aontu` stanza naming the producer, so a report read from a pipe says which version and verb made it. Where the constraint algebra knows what would have unified, the finding carries it as `expected`/`actual`, and a `must()` check's author message rides along as `note`. **A finding carries the repair beside the diagnosis.** `message` is the headline and stays one line (that is what makes it comparable and greppable) so a finding also carries `hint`: the engine's own explanation of the failure class, with the offending values filled in. It is the text a human sees under the error frame, and for several codes it is the only place the FIX is written down. A lossy integer literal is the clearest case (abridged): ```json { "code": "lossy_integer_literal", "message": "[aontu/lossy_integer_literal]: Cannot resolve value at path $.port", "hint": "This integer literal, 9007199254740993, is not exactly representable in\nbinary64 ... write it as a `0d`\nliteral to get the exact integer." } ``` The field is absent, not empty, for a code that has no hint text. Hint prose, like `message`, is deliberately outside cross-port parity. **Colour is a decision about the destination.** Error frames are coloured for a terminal and plain everywhere else: `NO_COLOR` (set, to anything) turns colour off for every caller of the library, the command additionally turns it off when its own stderr is not a terminal, and `--jsonl` turns it off unconditionally: a JSONL answer is machine-read by definition. A piped report therefore never carries terminal control codes into a log, a CI annotation or a parser. `--format sarif` emits the report as SARIF 2.1.0, the interchange form CI systems ingest (GitHub code scanning upload, PR annotation): a minimal profile: one run, one result per finding, the data site as the primary location, the schema site under `relatedLocations`, and the whole native finding embedded in `properties`, so a SARIF consumer still holds the native contract. Severities map to SARIF levels (`info` → `note`). The renderer is library API in both ports (`sarifReport(report, version)` from `aontu`; `aontu.SarifReport` in Go), and its bytes are held to cross-port parity by the golden in [`test/spec/files/vet-sarif/`](https://github.com/aontu-lang/aontu/blob/main/test/spec/files/vet-sarif/README.md). A ready-made GitHub Action wrapping the verb ships in this repository: [`vet-action/`](https://github.com/aontu-lang/aontu/blob/main/vet-action/README.md). `--watch` re-runs the whole vet whenever a watched file (the schema or any data file) changes, streaming one report per run: non-incremental by design: parsed trees are single-use, so every run is a full re-parse and re-unify, bounded by the fixpoint's pass budget. A file that is briefly unreadable mid-save reports and keeps watching. #### Vetting a recursive schema A [recursive schema](/docs/reference-language#recursive-references-fixpoints) needs nothing extra from `vet`: the definition expands one level per [meet](/docs/unification) with concrete data, so the checks descend exactly as far as the data does, and a finding at depth is located there. The vocabulary below is a trimmed version of [use-cases/13-recursive-schema](/use-cases/13-recursive-schema). Write it as `chain.aon`: ```aontu spec: hide({ Step: { approver: string & re("^[a-z]+@acme[.]example$") decision: *pending|pending|approved|rejected then?: $.spec.Step } }) ``` `--at` anchors the run at the definition (`hide()` keeps `spec` out of generated output but not out of the path) so a data document is a candidate `Step`, not a candidate whole file. A two-level chain in `request.json` holds: ```json {"approver": "lead@acme.example", "decision": "approved", "then": {"approver": "cfo@acme.example"}} ``` ```sh $ aontu vet --at $.spec.Step chain.aon request.json verdict: valid ``` A chain whose third level breaks the `approver` pattern, as `request-deep.json` does, is refused **at that depth**: ```json {"approver": "lead@acme.example", "decision": "approved", "then": {"approver": "cfo@acme.example", "then": {"approver": "EXTERNAL@other.example"}}} ``` ```sh $ aontu vet --at $.spec.Step chain.aon request-deep.json verdict: invalid $.spec.Step.then.then.approver: constraint [conflict] [aontu/constraint]: Cannot unify values at path $.spec.Step.then.then.approver expected: re("^[a-z]+@acme[.]example$") actual: "EXTERNAL@other.example" data: request-deep.json:3:32 ("EXTERNAL@other.example") schema: chain.aon:3:24 (re("^[a-z]+@acme[.]example$")) $ echo $? 1 ``` The finding's path is the unrolled position (`$.spec.Step.then.then.approver`), while its schema site is the one `re()` the author wrote: the definition is written once and applies at every depth, and the report says both. For the recipe form see [Define a recursive schema](/how-to/define-a-recursive-schema). ### `aontu subsume` The subsumption query as a command ([docs/reference-language.md, "Subsumption"](/docs/reference-language#subsumption)): does the general document admit every instance the specific one admits? ``` aontu subsume [--profile values|defaults|gen] [--at ] [--format text|json] ``` The exit code is the verdict class: `0` subsumes, `1` does not subsume (the findings carry the witness: path, codes, both sites), `3` undecided (always with a `sub_*` reason), `4` a document that does not stand up on its own, `2` usage. The report reuses vet's finding object and renderers, class `compat`. **An unexpanded recursive position is `undecided`, never guessed.** A [recursive reference](/docs/reference-language#recursive-references-fixpoints) expands only against concrete data, and subsumption compares two documents with no data on either side, so at the recursive position there is no rule to apply, and the query says so rather than answering from hope. Write a `general.aon`: ```aontu spec: hide({ Step: { label:string then?:$.spec.Step } }) doc: $.spec.Step ``` and a `specific.aon` whose step recurses into a DIFFERENT definition: ```aontu spec: hide({ Step: { label:"start" then?:$.spec.Other } Other:label:string }) doc: $.spec.Step ``` ```sh $ aontu subsume general.aon specific.aon verdict: undecided $.spec.Step.then: sub_unresolved [compat] no subsumption rule covers this pair of value formers expected: $.spec.Step actual: {"label":string} general: general.aon:1:41 ($.spec.Step) specific: specific.aon:1:63 ({"label":string}) $.doc: sub_unresolved [compat] no subsumption rule covers this pair of value formers expected: $.spec.Step actual: {"label":"start","then"?:{"label":string}} general: general.aon:2:6 ($.spec.Step) specific: specific.aon:1:20 ({"label":"start","then"?:{"label":string}}) $ echo $? 3 ``` **The same recursion on both sides is decided**, and decided by identity: a document that recurses, declares a relation or shares a template by reference admits itself, because two values with the same **hash form** are the same value. The rule runs only where the answer would otherwise be `undecided`, so it narrows nothing else, and without it a contract could not be gated against its own earlier version at all. This is the verdict [`breaking`](#aontu-breaking) fails on by default: a gate that cannot decide a recursive contract reports `undecided` and stops, and `--allow-undecided` is the deliberate downgrade. ### `aontu breaking` The evolution gate built on the same query: compare a document against its own earlier versions. ``` aontu breaking --against [--at ] [--mode backward|forward|full] [--allow-undecided] [--format text|json] ``` - `--against` takes a file path or `git#`, and is repeatable. A `git#` spelling is the old version of the **whole tree**, not of the entry file alone: the revision's includable sources (`.aon`, `.aontu`, `.jsonic`, `.json`) are materialised into a temporary directory by shelling out to git (no embedded git) and the old document is evaluated from there, so a change inside an `@"…"`-included file is part of the comparison. The temporary tree is removed when the run ends. Sources outside the revision (package includes under `node_modules`, the bundled `aontu:system`) resolve as they always do; their versions travel with the lockfile rather than with this comparison. A file the revision does not carry is a usage failure naming it, not a comparison against nothing. - `--at ` compares that path of **both** versions, the same anchor [`subsume`](#aontu-subsume) takes, and findings are reported from it. A module's top level carries the things that are *supposed* to change between releases (the version string, the `aontu_policy` block) so the whole-document comparison answers about those rather than about the contract, and a release that bumps only its version self-breaks the gate. Anchoring at the contract is the fix; splitting the file was the workaround. - Modes: **backward** (the default) checks the new document subsumes the old: documents valid under v1 stay valid; **forward** checks the old subsumes the new; **full** checks both. - The document can declare its own promise: `aontu_policy: hide({compat: *backward | forward | full | none})`: `breaking` reads `$.aontu_policy.compat` from the new document, and `--mode` overrides it. `none` declares no promise: nothing is checked. - Exit codes mirror `subsume`'s: `0` compatible, `1` breaking, `3` undecided, `4` error, `2` usage. Undecided **fails** the gate by default (a gate that shrugs is not a gate) downgradable with `--allow-undecided`. - `--allow-deprecated-removal` downgrades a finding about a value the old version already `deprecate()`d to a warning (still reported, no longer failing): deprecate-then-remove is the supported rename path. ### `aontu trim` Report redundant map entries (entries whose removal leaves the evaluated result unchanged, the spread-implied case included) as paths. ``` aontu trim --check [--format text|json] ``` - The test is **evaluate-and-compare**: for each candidate entry the source is re-parsed, the entry deleted from the parsed tree, and the canon compared to the baseline. This covers everything the fixpoint can see (spread templates, references, duplicate-key merges), and a removal that *errors* is not redundant: the document does not stand up without that entry. - Candidates are map entries at every depth; **list elements are not candidates** (removing one shifts every later index: a different document, not the same one minus a redundancy). A child of a redundant parent is skipped: removing the parent already covers it. - `--check` is **required**: trim only reports, and `aontu trim f.aon` doing something other than trimming silently would be worse than saying so. It is not blocked on the machinery [`set --in-place`](#aontu-model-set) now has (a splice needs no format-preserving editor) but deleting an entry is a different edit from replacing one: a statement's span does not say which surrounding blank line or trailing comment went with it, and guessing wrong silently rewrites the file's shape. - Exit codes: `0` clean, `1` redundant entries found, `4` the document itself does not evaluate, `2` usage. - **A verdict of `error` says why.** A document that does not evaluate has no redundancy to report, but it does have a reason: the report carries `errors`, the engine's own first failure in the same finding shape [`vet`](#aontu-vet) reports in: code, class, path, sites with file, row, column and extent, and the repair `hint`. The field is present only on that verdict, and the text renderer prints the finding under the verdict line. ### `aontu relations` Run the [declared-relation](/docs/reference-language#declared-relations) checks (acyclicity and inverse consistency) over one finished model. ``` aontu relations [--format text|json] ``` The vocabulary is declared once, at the field; the model lists plain names. The files below are a trimmed version of [use-cases/12-relations](/use-cases/12-relations). Write the vocabulary as `spec.aon`: ```aontu spec: hide({ Service: { kind: service dependsOn?: rel($.spec.ServiceShape) & acyclic() & inverse(usedBy) usedBy?: rel($.spec.ServiceShape) } ServiceShape: kind: service }) ``` A model whose edges hold, `system.aon`, passes: ```aontu @"./spec.aon" services: { &: $.spec.Service } services: web: dependsOn: [path($.services.billing)] services: billing: { dependsOn: [path($.services.ledger)] usedBy: [path($.services.web)] } services: ledger: usedBy: [path($.services.billing)] ``` ```sh $ aontu relations system.aon verdict: pass ``` A `bad-system.aon` whose two services depend on each other, with neither inverse written out, fails on every count at once: ```aontu @"./spec.aon" services: { &: $.spec.Service } services: auth: dependsOn: [path($.services.billing)] services: billing: dependsOn: [path($.services.auth)] ``` ```sh $ aontu relations bad-system.aon verdict: fail $.services.auth.dependsOn.0 dependsOn: cycle $.services.auth -> $.services.billing -> $.services.auth $.services.auth.dependsOn.0 dependsOn: $.services.billing does not list $.services.auth under usedBy $.services.billing.dependsOn.0 dependsOn: $.services.auth does not list $.services.billing under usedBy $ echo $? 1 ``` - Relations are declared **at the field**, by [`rel(t)` and the graph atoms](/docs/reference-language#declared-relations): `acyclic()` and `inverse(name)` register the declaration during unification, and the verb reports the verdict over the finished model's edge set. There is no reserved `relations:` key: a document that writes one has written ordinary data. - **These are not lattice constraints, deliberately.** Both properties are global and non-monotone (one more edge makes an acyclic graph cyclic) so they are facts about a finished model rather than something unification may hold. Generation enforces the same verdict (a located `relation_cycle` / `relation_inverse_missing` at the offending edge); the verb reports it without generating. The [language reference](/docs/reference-language#declared-relations) states the rule; the [explanation](/docs/explanation#why-there-is-a-verb-surface) argues it. - A finding carries `at` (the position of the offending edge), `code` (`relation_cycle` or `relation_inverse_missing`), `relation`, and `detail`: for a cycle, the node paths it runs through in order; for a missing inverse, `[from, to, inverseName]`. Findings are **sorted by `at`**, so the report diffs cleanly. - **The endpoint type is `rel(t)`'s flow**: declared once on the field, it flows into each far end at the site, so a conflict or a hole is an ordinary located evaluation error rather than a report row, and a document with a wrong-typed far end answers `verdict: error` here, because it does not stand up at all. (The old `target:` declaration and its `relation_target_unmet` finding are retired with the `relations:` key.) - `--format json` wraps the same findings with the `aontu` producer block (`verb`, `version`) that every machine-readable report carries. - Exit codes: `0` `pass`, `1` `fail`, `4` `error` (the document does not evaluate), `2` usage. Note these are the verb's own three verdicts, not [`vet`](#aontu-vet)'s five classes: there is no schema on the other side of this question, so `incomplete` has nothing to mean. - **A verdict of `error` says why.** A document that does not stand up has no graph, so it has no relation findings, but the report carries `errors`, the engine's own first failure in the same finding shape [`vet`](#aontu-vet) reports in. `findings` stays the graph's own vocabulary; the two lists answer two different questions, and the `errors` field is present only on the `error` verdict. - The library form is `relationCheck(src)` in TypeScript and `Aontu.RelationCheck(src)` in Go, returning the identical `{verdict, findings}` record (plus `errors` on a failed run); the derived graph the checks run over is `result.graph` / `Aontu.Graph`, described under [the TypeScript API](#class-aontu). ### `aontu reaches` Ask whether one node **reaches** another over the link graph, at any remove. Endpoints are `$.dotted` node paths. ``` aontu reaches [--relation ] [--format text|json] ``` [`relations`](#aontu-relations) asks about the edge set as a whole. This asks the question that needs the **closure**: does anything `from` links to, at any remove, end up at `to`? That is the shape of every blast-radius question an operator asks ("if the billing database goes, what falls over?") and every containment question a policy asks ("nothing in the public tier may reach the ledger"), and neither can be put one edge at a time. Ask it of the `system.aon` model above: ```sh $ aontu reaches $.services.web $.services.ledger system.aon verdict: reaches $.services.web -> $.services.billing -> $.services.ledger $ echo $? 0 ``` - **The path is the answer**, not decoration: "yes" is worth little to an operator asking what a failure would take out, and the chain is what they act on. It is a **shortest** path, and among shortest ones the first in code-point order, so it is the same path in both ports. A `no` carries none: there is no evidence for a negative answer. - **Transitive, not reflexive-transitive.** `reaches a a` is true only when a path of one or more edges returns to `a`, which says the graph has a cycle through `a` rather than saying nothing. - `--relation ` follows only edges under that relation: the difference between "can this reach that at all" and "can it reach it *this way*". - A link into part of a node (`$.services.auth.ports.http`) reaches **that node**, not the one above it: reachability is between tree positions, and there is no declared boundary to widen it to. Same rule [`relations`](#aontu-relations) uses, and it has to be, or the two verbs would disagree about what an edge connects. - Exit codes: `0` `reaches`, `1` `unreachable`, `4` `error`, `2` usage. An unreachable pair is a **failed check**, not an error: the question was answered, and the answer was no. - **An endpoint that names no node is a refusal**, reported as `refer_unresolved` with the linked nodes listed: answering `no` would report a typo as a fact about the model. An endpoint that exists but has no edges is a perfectly good question with the answer `unreachable`. - Like acyclicity, this is a verb and **not a lattice constraint**: reachability is global and non-monotone, so a citizen asserting *non*-reachability could be true and then false as one more edge arrives. - The library form is `reachCheck(src, from, to, options?)` in TypeScript and `Aontu.Reach(src, from, to, options)` in Go, returning the identical `{verdict, path?}` record (plus `errors` on a failed run). In `system.aon` the `usedBy` inverses run the other way, so the ledger reaches the web service in general but not along `dependsOn`: ```sh $ aontu reaches $.services.ledger $.services.web system.aon verdict: reaches $.services.ledger -> $.services.billing -> $.services.web $ aontu reaches $.services.ledger $.services.web --relation dependsOn system.aon verdict: unreachable $.services.ledger does not reach $.services.web $ echo $? 1 ``` And an endpoint that names no node refuses rather than answering: ```sh $ aontu reaches $.services.web $.services.ledgr system.aon verdict: error $: refer_unresolved [reference] $.services.ledgr names no node in this document. note: nodes with links: $.services.billing, $.services.ledger, $.services.web $ echo $? 4 ``` ### `aontu view` Draw a **figure** of a finished model, as deterministic text a golden diff can check: no coordinate is computed, nodes and edges are sorted by code point, and both ports emit the same bytes. ``` aontu view [--as ] [--at ] [--out [--check]] [--strict] [--max-rows ] [--depth ] [--style ] [--format text|json] [options] ... ``` Ten kinds. Eight read a report the engine already produces; `doc` and `lattice` read the document itself, which is the one thing no report holds: | kind | draws | reads | profiles | |---|---|---|---| | `doc` | the document's own key tree, to a depth | the anchor walk, as `get --keys --types` reads it | `text`, `svg` | | `lattice` | the language's value lattice, with a count at every node the document reaches | the anchor walk, and each value's own kind | `text`, `svg` | | `tree` | the dependency tree of a relation: roots derived, repeats elided, cycles marked | the edge set | `text`, `svg` | | `matrix` | the dependency-structure matrix over one relation, in `canon` or `partition` order, with `--closure` | the edge set and the relation declarations | `text`, `svg` | | `graph` | the node-link drawing, grouped and labelled by fields of the nodes | the edge set, the declarations, the node values | `mermaid`, `dot`, `er` | | `layer` | the architecture layers: one band per value of `--group-by`, upward edges named | the edge set and the node values | `text`, `mermaid`, `svg` | | `sets` | the set-intersection panel over a family of sets (UpSet) | the generated value | `text`, `svg` | | `layers` | which document contributed which path | the provenance record | `text`, `svg` | | `ladder` | the meet ladder at one path: every contribution as a rung, in rank order | the [`why`](#aontu-model-why) record | `mermaid`, `dot` | | `poset` | the subsumption order over several documents | [`subsume`](#aontu-subsume), pairwise | `mermaid`, `dot` | The first profile listed is the kind's default; asking for another is a refusal (`view_profile_unknown`), because there is no text form of a node-link drawing and no Mermaid form of a matrix. `--as svg` draws the cell-based kinds (every kind whose text form is a grid of character cells) as a standalone SVG with the same geometry: 8 units per character and 20 per line, every coordinate a whole number, so no font is measured and both ports emit the same bytes. The figure carries its own style block (dropped by `--style none`, below); a host page sets the colours through CSS variables (`--av-ink`, `--av-muted`, `--av-bg`, `--av-rule`, `--av-rule-faint`, `--av-closure`, `--av-warn`, `--av-alert`, `--av-bar`), and the defaults stand where it sets none. The matrix fills a direct cell, tints a closure cell, marks an unmirrored edge, and rules the diagonal; the layers draw each upward edge as a dashed arrow; the panel draws its bars and dots. The node-link kinds stay Mermaid and DOT, whose renderers lay them out. Draw the `system.aon` model above over its `dependsOn` relation, first as the tree, then as the matrix in partition order: ```sh $ aontu view tree --relation dependsOn system.aon web └── billing └── ledger ``` ```sh $ aontu view matrix --relation dependsOn --order partition --closure system.aon 1 2 3 ledger 1 \ . . billing 2 X \ . web 3 + X \ # above-diagonal direct cells: 0 ``` A cell at (row, column) is set when the row depends on the column: `X` a direct edge, `!` a direct edge whose declared inverse is not written back, `+` reachable only transitively, `.` absent, `\` the diagonal. In partition order an acyclic relation is a lower triangle, and the footer's count of cells above the diagonal is the acyclicity proof in the picture's own shape; a cycle survives every ordering as an above-diagonal cell, and is reported as `cycle_block`. The same edges as a node-link drawing, in Mermaid: ```sh $ aontu view graph --relation dependsOn system.aon flowchart LR n_billing["billing"] n_ledger["ledger"] n_web["web"] n_billing -->|"dependsOn"| n_ledger n_web -->|"dependsOn"| n_billing ``` The `lattice` kind draws the LANGUAGE rather than the model. The shape is always the same ([the value lattice](/docs/reference-language#the-value-lattice), whatever the document holds) and what the document adds is a count at each node its own values landed on. Write `ports.aon`: ```aontu host: "0.0.0.0" port: 8080 tls: true timeout: 1.5 retries: integer backoff: integer & min(1) ``` ```sh $ aontu view lattice ports.aon top │ ┌────────────────────────────────┬────┴───────────────────────────┬─────────┐ string (1) number boolean (1) null │ │ │ │ ├─────────────┬────────────┬─────┴─────┬────────────┐ │ │ path() integer (2) float (1) biginteger bigdecimal │ │ │ │ │ │ │ │ │ └─────────────┴────────────┴──────────┬┴────────────┴─────────────┴─────────┘ nil ``` A concrete scalar sits at its kind (`8080` counts at `integer`) and a kind written as a schema sits *at* that node, which is why `retries: integer` is the second of the two. `backoff` is at neither: `integer & min(1)` is a region of the lattice rather than a point, so the figure declines to draw it anywhere and names it instead: ``` lattice_unplaced 1 $.backoff ``` An unresolved disjunction is unplaced for the same reason: `*8080 | 9090` is two places at once, and a node that claimed either would be claiming something the document does not say. - **The loss report.** Every run prints, on stderr, what the figure could not draw or drew differently from the model, one line per code with a count: `hidden_contribution` (an edge inside a `hide()` subtree, not drawn, because a committed figure discloses what it draws), `edges_in_disjunct` (a link under an unresolved disjunction, which is not a fact, so the figure reports it rather than picking an arm), `unresolved_field` (a node without a value for `--group-by` or `--label`), `lattice_unplaced` (a value at no single point of the lattice, named rather than drawn), `cycle_block`, `cols_elided`, and for the poset `order_undecided`, `order_maybe_equal` and `order_intransitive`. Any of these makes the verdict `lossy`, which `--strict` turns into exit `1`. Three codes are informational and leave the verdict `rendered`: `edges_deduped` (a model declaring each entity at two positions writes each edge twice), `inverse_suppressed` (a declared mirror, implied by the edge drawn) and `crossings` (a property of the emitted order). - **`--style `** says how a figure carries the MEANING of its marks. Every mark has a reason the extractor established (a direct cell, a closure cell, an unmirrored edge, an upward edge, a repeated subtree) and each profile has one way to show it: SGR escapes for `text`, CSS classes for `svg`. `auto`, the default, picks that mechanism where the destination can carry it: escapes only when **stdout** is a terminal and `NO_COLOR` is unset (stdout, because that is where the figure goes: the error frames' own colour decision is about stderr), and an SVG keeps the stylesheet that makes it standalone. `none` drops both: on `svg` the classes stay (they are structure, not style) and only the embedded stylesheet goes, which is what a host page wants once it has bound `--av-ink` and its kin and is embedding several figures. `ansi` and `css` name a mechanism outright, and asking for one on a profile that cannot carry it is a usage error (`view_style_profile`) rather than a silent no-op. Escapes are never written to a file: `--out` with `--style ansi` is refused, and `auto` resolves to no escapes there. Neither mechanism states a colour, and a figure still cannot name one: SGR 31 means the colour the reader's terminal calls red, and a CSS class states nothing at all. A hex triple in a figure stays refused, and so does `style` in a view document: a declaration says which projection, never how it looks. - **`--out ` and `--check`.** The figure is written to the file instead of stdout; with `--check` nothing is written and the exit is `1` when the file differs from what would be drawn, which is the CI gate for a committed figure. - **`--max-rows `** (default 60) is a refusal, exit `2`, not a truncation; the message names the narrowing options. - `doc`: the shape of the model, before any of its values mean anything. Every other kind here needs the document to HAVE something (links, contributions, peers) and draws nothing from one that does not; this draws what is in the document and how it is arranged, which is what a reader meeting a model wants first. `--at` names the subtree (default `$`) and `--depth ` how many levels of key below it (default 3). Map keys are in code-point order and list indices in order, exactly as `get --keys` lists them. A leaf carries its canon, cut at 32 characters: the kind of thing it is, not its value. A container the depth bound stops at carries the number of keys not drawn, and they are counted into the loss report as `depth_elided`: a tree that stopped without saying so would be the one thing a structural drawing must not be. - **`--at `** restricts the edge-derived kinds to nodes under the path, names the subtree `doc` draws, the provenance panel to paths under it, and names the path the ladder draws (required) and where the poset compares. - `tree`: `--relation` draws one relation; without it every relation is drawn, each branch naming its own. `--root ` (repeatable) draws one subtree; a root that is not a node of the drawn graph is refused (`refer_unresolved`). Roots are derived as the nodes nothing depends on; a shared subtree is expanded once and marked `(*)` after; a closing edge is `(cycle)`; labels are the shortest path suffix unique in the drawing. - `matrix`: `--relation` is required unless exactly one relation has edges (`view_relation_ambiguous` otherwise); `--order canon` (label order, the default) or `partition`; `--closure` marks the transitively reachable cells. Ten or more rows stack the index digits in the header. - `graph`: `--relation` (repeatable) keeps only those predicates; a declared inverse's mirror is suppressed and counted. `--group-by ` puts each node in a subgraph named by that field's value (ids `g0`, `g1` and so on, in label order); `--label ` labels the node with it, a number or boolean as its canon. Node ids encode the label injectively: `n_` + the name when it is an ASCII identifier, else `nq_` + the name with every other code point as `_` and its hex (`cust-1` is `nq_cust_2d1`, so a name such as `end` or `graph` can never collide with a keyword). Text is escaped per code point: Mermaid as numeric entities (`#34;` for `"`, `#124;` for `|`), DOT as `\"` and `\\`. A label holding a line terminator is refused (`view_line_break`). `--as er` draws Mermaid's `erDiagram`, every relationship many-to-many because the model states no cardinality. - `layer`: `--group-by ` (required, `view_group_required`) names each node's layer; bands are stacked in the partition order of the layer-level graph, reversed, so the layer nothing depends on is on top. `--layers a,b,c` fixes the order (top first) for a model whose upward edge makes the layer graph cyclic. The footer counts the relation's downward, sideways and upward edges, and names each edge `--edges` shows. `--edges upward|all|none` chooses which of them the figure draws over the bands: `upward` is the violations, and the default for the fixed grids (`text`, `svg`), because the bands already say which way the rest go; `all` draws the relation itself, which is what a reader tracing one module's dependencies wants, and is `mermaid`'s default since it lays edges out itself; `none` leaves the bands alone. In SVG an upward edge is dashed and alert-coloured, a downward one runs from the bottom of its box to the top of the one it names, and a sideways one dips below the boxes of its band. - `sets`: `--sets ` names a map whose keys are the sets, `--member ` the field holding each set's members (a list of strings), `--universe ` a map or list of every element, so the covered-by-nothing column exists. A member written as an address (`path($.perms.read)`) meets a universe map's key on that address and is shown by its shortest unique suffix. Columns are the exact membership signatures, by degree, then cardinality, then name; `--min-degree ` drops the low ones and `--max-cols ` elides the rest (`cols_elided`). Both need the document to generate. - `layers`: the same panel over the provenance record, sets being the documents and elements the paths each document wrote into. Files are shown relative to the entry document; `--min-size ` drops the small intersections. - `ladder`: the `why` record at `--at`, one rung per contribution, sorted by rank descending (weakest first, so the winner is the last rung before the value), then by site. - `poset`: several files; each pair is compared with `subsume` at `--at` under `--profile values|defaults|gen` (default `defaults`). Documents that subsume each other are one node, labelled `a = b`; an edge is a cover of the transitive closure, upward toward the more general document; an undecided pair with no proven order is a dashed edge labelled with the `sub_*` reason. Labels are the filenames without `.aon`. - Exit codes: `0` rendered or lossy, `1` a `--check` mismatch or lossy under `--strict`, `2` usage (an unknown kind or profile, a missing required option, `--max-rows` exceeded), `4` error (a document that does not stand up, a relation, root or path that names nothing). A document with nothing to draw renders an empty figure and exits `0`. In text form the figure is all that stdout carries, so a redirect is a golden file; `--format json` wraps the whole report as `{kind, verdict, text, loss}` under the usual `aontu` envelope, and a refusal carries `errors` in place of `text`. - The library form is `view(src, options)` in TypeScript (`viewTree` remains for the tree) and `Aontu.View(src, options)` in Go, returning the identical `{verdict, kind, text?, loss, errors?}` record; the poset's further documents ride `options.docs` as `{src, path?, name?}`. **The view document.** A projection that runs in CI belongs in a file. `--views ` names a map, in an ordinary document that includes the model, whose values declare figures: one evaluation, N figures, one exit code. The keys of a declaration are the view options (the flags without the dashes) and every declaration names its `kind` and the `out` file it draws into. `views` is the author's key; nothing in the engine knows the name, which is why the path is given. Write a `views.aon` beside the `system.aon` above: ```aon @"./system.aon" views: arch: { kind: matrix relation: dependsOn order: partition closure: true out: "arch.dsm.txt" } views: map: { kind: graph relation: dependsOn groupBy: owner as: mermaid out: "arch.mmd" } ``` ```sh $ aontu view --views '$.views' views.aon $ aontu view --views '$.views' --check views.aon $ echo $? 0 ``` Each `out` is resolved against the **view document's own directory**, so the gate passes from any working directory. Nothing is written unless every figure rendered: N figures of one model are only meaningful together, so a set whose third figure refuses leaves the first two off disk, and its exit code is the worst of the figures'. `--check` compares the whole set and names every difference; `--strict` turns any figure's loss into exit `1`. A declaration that names an option that is not one, gives a value of the wrong shape, or leaves out `kind` or `out` is `view_document_shape`, reported for every faulty declaration at once and before anything is drawn. The `poset` is refused there: it compares several documents and a view document declares figures of the one it includes. The library form is `viewSet(src, options)` in TypeScript and `Aontu.ViewSet(src, options)` in Go, returning `{verdict, views, errors?}` where each view is `{name, kind, out, verdict, text?, loss, errors?}`: the caller writes the files. `@"aontu:view"` is the bundled schema for a declaration, so the same mistakes are refused when the document is EVALUATED rather than when the verb reads it. Write a `views-typed.aon`: ```aon @"aontu:view" @"./system.aon" views: { &: $.aontu.View.Figure } & { arch: { kind: matrix relation: dependsOn order: partition closure: true out: "arch.dsm.txt" } } ``` ```sh $ aontu view --views '$.views' --check views-typed.aon $ echo $? 0 ``` `$.aontu.View.Figure` types every option, and a kind that is not a kind, an order that is not an order or a count below zero is an ordinary unification failure naming `aontu:view` as the other operand. It is optional: a view document that does not include it is read exactly the same way, and refused by `view_document_shape` instead. The source is served from the engine, as `aontu:system` is, so it needs no filesystem and resolves under every include capability but `'none'`. The use cases pin one figure of each kind as a golden: [01-service-catalog](/use-cases/01-service-catalog) the graph and the matrix, [04-schema-evolution](/use-cases/04-schema-evolution) the poset, [08-feature-flags](/use-cases/08-feature-flags) the ladder, [12-relations](/use-cases/12-relations) the graph and the ER diagram, and [16-module-deps](/use-cases/16-module-deps) the tree, the matrix and the layers; 16 also declares all seven of its figures in a `views.aon` that its `check.sh` gates in one run. ### `aontu jsonschema` Export a document as a **JSON Schema** (draft 2020-12), and say what could not be carried. ``` aontu jsonschema [--at ] [--strict] [--format text|json] ``` This is the interop bridge. Every major LLM provider's structured-output API constrains generation to JSON Schema and to nothing else, so the shape an enterprise actually deploys is: export the model, let the provider generate under it, then [`vet`](#aontu-vet) the result against the model itself: the schema narrows what is *produced*, the model decides what is *true*. An MCP tool's `inputSchema`, which the protocol requires to be JSON Schema, is the same export. **The schema goes to stdout and the losses to stderr**, so `aontu jsonschema x.aon > schema.json` writes a usable schema and still tells the reader what it left behind. Write a `contract.aon`: ```aontu spec: name: string & re("^[a-z][a-z0-9-]{2,39}$") spec: tier: *internal|standard|critical ``` ```sh $ aontu jsonschema --at spec contract.aon { "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { "name": { "pattern": "^[a-z][a-z0-9-]{2,39}$", "type": "string" }, "tier": { "default": "internal", "enum": [ "internal", "standard", "critical" ] } }, "required": [ "name", "tier" ], "type": "object" } ``` - It exports the **unified** value, not the parse: what a document MEANS is what a consumer should be constrained to. - `--at ` names the subtree to export: the same anchor [`vet --at`](#aontu-vet) takes, so `--at spec` means the same thing in both. - `--format json` prints the whole report (`schema`, `lossy`, `verdict`) under the usual `aontu: {version, verb}` envelope. - Exit codes: `0` exported, `1` lossy **under `--strict`**, `2` usage, `4` the document does not stand up on its own. Without `--strict` a lossy export is still an export and exits 0. **What crosses exactly.** Kinds become `type`; a concrete scalar becomes `const`; a disjunction of scalars becomes `enum`, and its preference becomes `default`; bounds become `minimum`/`maximum`, with the open endpoints as 2020-12's `exclusiveMinimum`/`exclusiveMaximum`; `re` becomes `pattern` (aontu's portable subset is a subset of ECMA-262, which is what JSON Schema reads, so no translation happens); `neq` becomes `not: {enum: …}`; `length` becomes `minLength`/`maxLength` on a string and `minItems`/`maxItems` otherwise; `unique()` becomes `uniqueItems`; an optional key is simply absent from `required`. A spread is `additionalProperties: