# Aura 0.4.0 - language reference for coding assistants Generated by `aura docs --agent` from the compiler's own definitions, so it describes the binary that produced it. Regenerate rather than edit. Aura is a configuration language. A manifest evaluates to a value, which is exported as JSON, YAML or TOML. It is not general-purpose: there are no loops, no mutation, no I/O beyond two capability-gated calls, and no way to observe the clock. This document is the whole language. If something is not here, it does not exist — do not infer syntax from Python, Ruby, YAML or HCL, which Aura resembles in places and diverges from in others. ## The one rule to internalise `:` exports, `=` does not. ```aura port: 8080 # a property — appears in the output internal = "secret" # a binding — usable in this file, never exported ``` Every other question about what ends up in the output reduces to this. A `def` or `type` is private unless marked `pub`, and `pub` makes it importable — not exported into the JSON. ## Blocks, not braces Indentation is not significant. Blocks open with a name and close with `end`. ```aura api: port: 8080 tls: enabled: true end end ``` There is no `{}` object literal. An object comes from a block, a `def`, or `new`. Lists use `[]`. A short block may be written on one line, with commas between the fields and `end` on the same line. `=` takes a block too: it binds the object without outputting it. ```aura defaults = port: 8080 timeout: "30s" end limits = cpu: "500m", memory: "1Gi" end api: port: defaults.port tls: enabled: true, min_version: "1.3" end end ``` A one-line object without a schema is not allowed inside `[]`: write `new Schema a: 1, b: 2 end` there. ## Values The built-in type names a schema field may use are listed under **Keywords and types** below — take them from there rather than guessing, because the error for an invented one is `E0504: use of undefined variable`, which does not say "unknown type" and is easy to misread. A schema can state rules about its own values, checked on every `new` and carried with the schema into any manifest that imports it: ```aura type Plan price_monthly: Int price_yearly: Int assert price_yearly <= price_monthly * 12, "yearly exceeds twelve months" end ``` An invariant sees only that schema's fields — not the surrounding module — so it means the same thing everywhere. A failure is `E0515`. A field takes `null` only if declared `Int?`. Use it for a genuinely absent value instead of a sentinel like `-1`: ```aura type Plan quota: Int? end ``` `?` does not make the field optional — omitting it is still an error. That is what `= default` is for, and the two combine: `quota: Int? = null`. A list field says what it holds: `tags: [String]`, `endpoints: [Endpoint]`, nesting as `[[Int]]`, and an `enum` works as the element type. A wrong element is `E0512` reported with its index. Bare `List` still accepts anything. `slice(start, end)` takes a substring by **character** index, `end` exclusive: `topic.slice(0, topic.len() - 1)` drops a trailing character. `Int.to_float()` widens a number; there is no `Float.to_int()`. A long number may be grouped with `_`, between digits only: ```aura max_bytes: 10_000_000 port: 8_080 ``` It is optional and means nothing — `10_000` and `10000` are the same value. Where used it must group by threes, counted away from the decimal point, so the short group is at the front of an integer and the end of a fraction: `8_080`, `1.123_45`. `2_3_7_1_9_3_3`, `1_0000_000`, `100_` and `1__0` are all errors. `Int` and `Float` are distinct: passing `1` where `1.0` is required is `E0512`. Strings are single-line and double-quoted, with `#{...}` interpolation: ```aura name = "checkout" label: "svc-#{name}" ``` Every format has two spellings: `to_json()` / `to_yaml()` / `to_toml()` give the most compact form the format allows, for a string being embedded somewhere, and `to_json_pretty()` / `to_yaml_pretty()` / `to_toml_pretty()` give the form a person reads, for a file that gets committed and diffed. Compact YAML is flow style — `{a: 1, xs: [1, 2]}` — which is ordinary YAML. `null` has no text form: interpolating one is `E0324`, not the word `null`. Say what the empty case prints — `"#{x == null ? "none" : x}"` — or drop the empties before joining a list: `xs.compact().join(", ")`. Multi-line text uses a block string, which ends with `end` at the opener's indentation and interpolates the same way: ```aura config: text server { server_name #{name}.example.com; } end ``` Lists fold and answer questions. `any` and `all` are what an `assert` over a collection should say; `reduce` needs an explicit initial value: ```aura ports = [8080, 8081, 9090] assert ports.all (p, i) -> p > 1024 end, "ports must be unprivileged" total: ports.reduce(0) (acc, p) -> acc + p end ``` `find(default)` and `index_of(value, default)` both require a fallback. There is no `-1` and no `Null`: absent is an ordinary outcome, and it has to be named. An object is traversed by turning it into pairs and back. `entries()` yields `{ key, value }` objects in declaration order, and `to_object()` is the inverse: ```aura def limits() cpu: 2 mem: 8 end doubled: limits().entries().map (e, i) -> key: e.key value: e.value * 2 end.to_object() ``` `to_object()` is also the only way to build an object whose keys are computed, because a property key is a literal. A repeated key is `E0323`, not an overwrite. `+` joins two strings or two lists, and never converts between types: ```aura base = ["auth", "billing"] all: base + ["frontend"] tag: "svc-" + name ``` `"port " + 8080` is `E0306` — call `.to_str()` on the number, or interpolate. There is no `concat` method; `+` is the only way to join lists. Braces inside a block string are literal — only `#{` starts interpolation. This is what makes Aura usable for generating nginx configs, Dockerfiles and the like. ## Conditionals `cond` is an expression and every arm produces a value. The `else` arm is mandatory (E0207) — there is no path that yields nothing. ```aura environment = "staging" replicas: cond environment == "production" -> 6 environment == "staging" -> 2 else -> 1 end ``` The binding is named `environment`, not `env`: `env` is the built-in that reads an environment variable, and a binding of that name shadows it. There is no truthiness. A condition must be `Bool`, or it is E0306. ## Functions ```aura def service(name, port) kind: "Service" name: name port: port end api: service("checkout", 8080) ``` A `def` body is an object. Lambdas are `(a, b) -> expr`, used with list methods. Functions are private unless `pub`. ## Schemas and enums ```aura enum Tier "frontend" "backend" end type Service name: String port: Int tier: Tier replicas: Int = 1 # optional: a default makes it so end api: new Service name: "checkout" port: 8080 tier: "backend" end ``` A short instance fits on one line: `new Service name: "web", port: 80, tier: "frontend" end`. A field typed by a schema - `Port`, `[Port]`, `[[Port]]` - holds only what `new Port` would accept, including values parsed from JSON or YAML; their errors name the path, `ports[2].number`. A missing required field is E0511, a type mismatch E0512, a value outside an `enum` E0514. An unknown field is a warning, or E0513 under `--strict`. A default is evaluated at instantiation and cannot reference sibling fields — derive those in a `def` instead. ## Assertions ```aura assert replicas > 0, "replicas must be positive" ``` A failed assertion is E0530 and stops the build. `fail("...")` (E0531) aborts unconditionally, which is how you reject an unreachable branch. ## Modules ```aura import "shared.aura" as shared import github/acme/pkg@v1.2 as pkg port: shared.default_port ``` A registry import must carry a version. `aura.lock` pins the exact version and a hash of the module's token stream; a mismatch is E0402. `import "host:geo" as geo` is different: the functions come from the application that embeds the script, not from a file. Write one only when that application provides it - `aura eval` has no host and stops with E0007, and `aura check` lists what the script needs. ## Capabilities — the part most likely to surprise you A manifest can do nothing by default. Two calls touch the outside world, and both are granted per run on the command line: - `env("NAME", "default")` needs `--allow-env` or `--allow-env=NAME,OTHER` - `read_file("path")` needs `--allow-read=`; a relative path is read from the manifest's folder, as an import is - not from where `aura` was run Without the grant the call is E0310. With a grant that does not cover the path, E0311. **Grants belong to the root manifest and do not reach imports.** A module you import cannot call `env()` or `read_file()` even when the root holds those rights, and an exported function runs with the capabilities of the module it came from, not the caller's. Assume any code you did not write cannot perform I/O. `--hermetic` goes further: both calls become E0505 everywhere, and because that is an analysis error, `aura check --hermetic` proves a manifest performs no I/O without evaluating it. ## Determinism `now()` and `timestamp()` do not exist and cannot be added (E0533). The same manifest with the same inputs always produces byte-identical output. A build timestamp is an input: pass it as `env("BUILD_TIME", ...)`. Key order in the output is declaration order, not sorted. ## Common mistakes - Using `=` for something that should appear in the output. It will silently not be there. This is the single most common error. - Expecting `{}` to build an object, or `if`/`elif` to exist. Neither does. - Leaving a bare expression on a line of its own. It is E0214 unless it is the last line of a `def` or lambda, where it is the return value - and then the body cannot also have `key:` properties (E0215). - Leaving out the commas in a one-line object (`a: 1 b: 2 end`), or adding one before `end`. Both are E0205. - Omitting the `else` arm of a `cond`. - Assuming an imported package can read a file because the root was granted it. - Reaching for a loop. Use `.map`, `.filter` and the other list methods. - Writing `1` where a `Float` is required — the types are distinct. ## Command line ``` aura eval [--strict] [--frozen] [--hermetic] [--dry-run] [--allow-read=]... [--allow-env[=A,B]] [--allow-imports-io] [--format json|json-flat|yaml|toml] [-o ] aura check [--strict] [--hermetic] aura fmt [--check] aura types --lang rust|ts|go aura add @vX.Y.Z ``` Exit codes: `0` success, `1` diagnostics, `2` an I/O or argument error. ## Keywords and types The built-in type names - the complete set a schema field may use, besides a `type` or `enum` declared in the manifest itself: String, Int, Float, Bool, List, Object. All 16 reserved words: import, as, type, enum, def, end, domain, new, assert, shadow, pub, cond, else, true, false, null. Plus `text`, which is *not* reserved: it opens a block string only where a value is expected, and is an ordinary identifier everywhere else. ## Standard library Methods are called on a value (`"a".upper()`, `xs.map(f)`); builtins are global functions. ### String | Signature | | | --- | --- | | `upper() -> String` | Uppercase every character. | | `lower() -> String` | Lowercase every character. | | `len() -> Int` | Number of characters. | | `trim() -> String` | Remove leading and trailing whitespace. | | `split(sep) -> List` | Split on a non-empty separator into a list of strings. | | `replace(from, to) -> String` | Replace every occurrence of a substring. | | `starts_with(prefix) -> Bool` | True if the string begins with the prefix. | | `ends_with(suffix) -> Bool` | True if the string ends with the suffix. | | `contains(substr) -> Bool` | True if the substring occurs anywhere. | | `slice(start, end) -> String` | Substring by character index; end exclusive, indices clamped. | | `to_int() -> Int` | Parse an integer (E0314 on failure). | | `to_float() -> Float` | Parse a float (E0314 on failure). | | `to_str() -> String` | The string itself (universal to_str). | | `parse_json() -> Object` | Parse a JSON document into a value. | | `parse_yaml() -> Object` | Parse a YAML document into a value. | | `parse_toml() -> Object` | Parse a TOML document into a value. | | `parse_duration() -> Int` | Duration string (d/h/m/s) to seconds; E0319 on error. | | `parse_datetime() -> Int` | RFC3339 timestamp to epoch seconds UTC; E0320 on error. | | `sha256() -> String` | SHA-256 digest as lowercase hex (config checksums). | | `base64() -> String` | Standard base64 with padding (e.g. Kubernetes Secret data). | | `base64_decode() -> String` | Decode standard base64 to a String; E0321 if invalid. | ### Int | Signature | | | --- | --- | | `abs() -> Int` | Absolute value. | | `to_str() -> String` | Decimal string form. | | `to_float() -> Float` | Widen to Float. There is no Float -> Int: narrowing would have to round. | | `format_duration() -> String` | Seconds to a duration string (d/h/m/s). | | `format_datetime() -> String` | Epoch seconds to an RFC3339 UTC timestamp. | ### Float | Signature | | | --- | --- | | `abs() -> Float` | Absolute value. | | `round(digits) -> Float` | Nearest Float with that many decimal places (0 to 15); exact ties go to even. | | `to_str() -> String` | Decimal string form. | ### Bool | Signature | | | --- | --- | | `to_str() -> String` | "true" or "false". | ### List | Signature | | | --- | --- | | `len() -> Int` | Number of elements. | | `to_object() -> Object` | Build an Object from { key, value } pairs; a duplicate key is E0323. | | `reduce(init, fn) -> Any` | Left fold: reduce(init) (acc, elem) -> ... end. The init is required. | | `any(fn) -> Bool` | True if the predicate holds for some element; false for an empty list. | | `all(fn) -> Bool` | True if the predicate holds for every element; true for an empty list. | | `find(default, fn) -> Any` | First element matching the predicate, or the default. | | `group_by(fn) -> Object` | Object from each String key the lambda gives to its elements, keys in first-appearance order (D39). | | `sort_by(fn) -> List` | Ascending, stable sort by the key the lambda gives; keys follow sort() (D39). | | `flat_map(fn) -> List` | Map to lists and join them, one level; the lambda must return a List (D39). | | `zip(other) -> List` | { left, right } pairs of two lists of one length; unequal lengths are E0327 (D39). | | `index_of(value, default) -> Int` | Index of the first equal element, or the default (which must be an Int). | | `first() -> Any` | First element (E0317 if empty). | | `last() -> Any` | Last element (E0317 if empty). | | `get(index) -> Any` | Element at an Int index, or a default via .get(i, default). | | `contains(item) -> Bool` | True if the element is present. | | `compact() -> List` | Drop null elements. | | `uniq() -> List` | Remove duplicates, keeping first occurrence. | | `map(fn) -> List` | Apply (element, index) -> value to each element. | | `filter(fn) -> List` | Keep elements where (element, index) -> Bool is true. | | `join(sep) -> String` | Join scalar elements with a separator. | | `sort() -> List` | Ascending sort of mutually-comparable scalars. | | `reverse() -> List` | Elements in reverse order. | | `sum() -> Int` | Sum of numbers (Float if any Float). | | `min() -> Any` | Smallest element (E0317 if empty). | | `max() -> Any` | Largest element (E0317 if empty). | | `flatten() -> List` | Spread one level of nested lists. | | `slice(start, end) -> List` | Half-open sub-list; indices are clamped. | | `to_json() -> String` | Serialize to a compact JSON string, for embedding. | | `to_json_pretty() -> String` | Serialize to indented JSON, for a file a person reads. | | `to_yaml() -> String` | Serialize to a compact YAML string (flow style), for embedding. | | `to_yaml_pretty() -> String` | Serialize to block-style YAML, for a file a person reads. | | `to_toml() -> String` | Serialize to TOML with arrays on one line, for embedding. | | `to_toml_pretty() -> String` | Serialize to TOML with one array element per line. | ### Object | Signature | | | --- | --- | | `len() -> Int` | Number of keys. | | `keys() -> List` | Keys in declaration order. | | `values() -> List` | Values in declaration order. | | `contains(key) -> Bool` | True if the key is present. | | `get(key, default) -> Any` | Value for a key, or a default if absent. | | `entries() -> List` | Key/value pairs as { key, value } objects, in declaration order. | | `merge(other) -> Object` | Shallow merge; the argument wins on key conflicts. | | `to_json() -> String` | Serialize to a compact JSON string, for embedding. | | `to_json_pretty() -> String` | Serialize to indented JSON, for a file a person reads. | | `to_yaml() -> String` | Serialize to a compact YAML string (flow style), for embedding. | | `to_yaml_pretty() -> String` | Serialize to block-style YAML, for a file a person reads. | | `to_toml() -> String` | Serialize to TOML with arrays on one line, for embedding. | | `to_toml_pretty() -> String` | Serialize to TOML with one array element per line. | ### Builtins | Signature | | | --- | --- | | `range(n) -> List` | The list [0, 1, ..., n-1]. | | `env(name, default) -> String` | Environment variable value or a default (needs --allow-env). | | `read_file(path) -> String` | File contents as a string (needs --allow-read). | | `fail(message) -> Any` | Abort evaluation with a message (E0530). | ## Diagnostics Every code is stable. When one is reported, the fix is in this table rather than in guesswork. ### Host (E00xx) Raised at the boundary between a manifest and the application that embeds it. `E0001`-`E0006` carry no source position, because nothing was parsed yet; `E0007`-`E0009` point at the call in the script (D35). | Code | Meaning | How to fix it | | --- | --- | --- | | E0001 | the host could not open a file, or `aura.lock` is unreadable | the message carries the underlying I/O error | | E0002 | the script exports nothing by that name | check the name; only `:` properties and `pub def` are exports | | E0003 | the export is not a function | bind it with `get`, not `function` or `call` | | E0004 | the function takes a different number of arguments | checked when the function is bound, before any call | | E0005 | an argument has no exact Aura value | an integer past `i64`, a NaN or an infinity, a byte string; refused rather than coerced | | E0006 | the result does not match the host's type | the message names the path inside the value where it did not | | E0007 | the script imports or calls a host function the host did not provide | a `host:` module is provided by the application at run time, so `aura eval` stops here; `aura check` lists what the script needs | | E0008 | a host function returned an error | the message is the application's; the script cannot recover from it | | E0009 | a host function was called with the wrong number of arguments (at load) or the wrong types (at the call), or returned a value Aura cannot hold | the message names the function | | E0010 | a script was called from inside one of its own host functions | not allowed (D35): return a value to the script instead | | E0011 | a reload would change what the script exports: an export removed, or its kind or number of arguments changed | the host may have bound it, so the old version keeps serving (D36); restart to change the exports | ### Lexing (E01xx) | Code | Meaning | How to fix it | | --- | --- | --- | | E0101 | no digit after the decimal point (`12.`) | write `12.0` or `12` | | E0102 | a string left unclosed at end of line | Aura strings are single-line; close the `"` | | E0103 | an integer literal does not fit in i64 | | | E0104 | a registry import without a version | `import a/b@v1 as x` — the version is mandatory (D8) | | E0105 | unexpected character | | | E0106 | an unclosed `#{` interpolation | | | E0107 | an unterminated block string | close it with `end` at the opener's indentation (D16) | | E0108 | a digit separator is misplaced | `_` groups by threes away from the point: `10_000_000`, `0.000_001` — never `100_`, `1__0` or `2_3_7_1_9_3_3` | ### Syntax (E02xx) | Code | Meaning | How to fix it | | --- | --- | --- | | E0200 | a specific token was expected | the message names which | | E0201 | a v1.1 inline block (`metrics port: 9090`) | use an object block: `metrics:` … `end` (D3) | | E0203 | a missing `end` or `]` | | | E0204 | an expression was expected | | | E0205 | statements are separated by line breaks | | | E0206 | `pub` not immediately before `def`, `type` or `enum` | properties export themselves; `=` is always private | | E0207 | a `cond` with no `else` arm | add `else -> ...`: every branch must produce a value (D14) | | E0208 | nesting too deep | the recursion limit that makes deeply-nested input an error rather than a stack overflow; a chain like `a + b + c` counts its length (D42) | | E0211 | an `enum` member is not a quoted string | members are listed as strings, one per line (D18) | | E0212 | a duplicate `enum` member | list each member once | | E0213 | an `enum` with no members | an empty set cannot be satisfied | | E0214 | a value is computed and thrown away | a bare expression outside the end of a function goes nowhere; write `key:`, `name =` or `assert` (D40) | | E0215 | a body has properties and also ends in a value | a function returns one or the other; make the value a property, or drop the properties (D40) | ### Evaluation (E03xx) | Code | Meaning | How to fix it | | --- | --- | --- | | E0301 | assigned twice — values are immutable | use a new name, or a nested scope | | E0302 | shadowing an outer variable without the marker | `shadow x = ...` (D7) | | W0303 | `shadow` shadows nothing | drop the `shadow` | | E0304 | integer arithmetic overflowed | use `Float`, or reconsider the magnitudes | | E0305 | division by zero | | | E0306 | an operand of the wrong type | conditions must be `Bool`; there is no truthiness | | E0307 | a non-scalar in interpolation or `join` | serialise it explicitly with `.to_json()` | | E0308 | unknown object field | a typo? use `.get(key, default)` for optional ones | | E0309 | unknown method | see the method reference; a package's function fields are called the same way | | E0310 | missing capability | the error names the flag you need (D1) | | E0311 | a path outside the directories allowed by `--allow-read` | | | E0312 | fewer arguments passed than the function has parameters | | | E0313 | a file read failed (I/O) | | | E0314 | malformed TOML, JSON or YAML while parsing | | | E0315 | a method that needs a lambda (`map`, `filter`) did not get one | | | E0316 | a malformed expression inside `#{...}` | | | E0317 | a list index out of range, or `first()` / `last()` on an empty list | use `.get(i, default)` | | E0318 | `obj["key"]` — brackets are not allowed on objects | use the dotted form: `obj."key"` (D11) | | E0319 | a malformed duration | the format is `"1h30m"`, units `d/h/m/s` | | E0320 | a malformed date | RFC 3339: `"2026-07-18T12:00:00Z"` | | E0321 | malformed base64 | `base64_decode()` on input that is not base64 | | E0322 | a concatenated list or string exceeds its size limit | `+` doubles: 1M elements / 16 MiB is the ceiling | | E0323 | `to_object()` got the same key twice | a silent overwrite would depend on element order; deduplicate first | | E0324 | `null` rendered as text | interpolation and `to_str()` must agree; say what the empty case prints (D31) | | E0325 | an evaluation ran out of its step budget | `--max-steps` raises it, `--stats` shows what is spent (D34); in `Script`, the budget is per call (D33) | | E0326 | an evaluation ran out of its memory budget | `--max-memory` raises it; every value and frame created counts (D34) | | E0327 | `zip()` got lists of different lengths | a dropped tail would drop configuration silently; cut the longer one with `slice` first (D39) | | E0328 | a value nested more than 256 lists and objects deep | almost always a loop that wraps its result each turn; build the levels side by side instead (D43) | | E0398 | internal: a closure's environment was dropped | a bug in Aura — please report it with the manifest | | E0399 | evaluation nested more than 256 levels | almost always a recursive function without the case that stops it; `map`, `filter` and `reduce` walk a list without nesting (D42) | | E0533 | `now()` and `timestamp()` do not exist | the host passes time in: `env("BUILD_TIME", ...)` (D13) | ### Modules and packages (E04xx) | Code | Meaning | How to fix it | | --- | --- | --- | | E0401 | a cyclic import (the chain is in the message) | break the cycle: move shared code into a third module | | E0402 | a package's integrity hash does not match `aura.lock` | the package's contents changed — find out why | | E0403 | `--frozen`: no entry in `aura.lock` | run `aura add` locally and commit the lock | | E0404 | module not found, or does not resolve | check the path, the version, `--registry-dir` | | E0410 | an import the interpreter was handed without the module loader | only reachable when embedding the evaluator directly; use `facade::eval_file`/`eval_source`, which load modules | ### Analysis and validation (E05xx / W05xx) | Code | Meaning | How to fix it | | --- | --- | --- | | W0501 | an unused variable | dead code — delete it | | W0502 | an unused import | | | W0503 | an unused function or type | if it is a package's API, mark it `pub` | | E0504 | an undeclared variable | a declaration must precede its use | | E0505 | `env()` or `read_file()` under `--hermetic` | pass the value in from the host, or drop the flag and grant the capability | | E0511 | a field the schema requires is missing | | | E0512 | a field's type does not match the schema | `Int` and `Float` are different types | | E0513 | an extra field (`--strict` only) | remove the field, or add it to the schema | | E0514 | a value outside the `enum` | the hint suggests the nearest member and lists the set (D18) | | E0515 | a schema invariant does not hold for this instance | the schema's own `assert`; the message shown is the one it carries | | E0516 | a default its own field can never accept | checked where the schema is declared, not where it is used | | W0512 | an effectful call in an imported module | imports have no I/O rights; reconsider the package's design | | W0513 | a nullable field is filled from `get(key)` or `env(name)` with no fallback | absence becomes `null` silently; name the fallback, or drop the `?` | | E0530 | an `assert` failed | the message is your own | | E0531 | `fail(...)` was called | | ### Serialisation (E06xx) | Code | Meaning | How to fix it | | --- | --- | --- | | E0601 | a function or schema inside the exported tree | the key path is in the message; at the top level `pub` items are excluded automatically | | E0602 | a non-finite Float (NaN or Inf) | | | E0603 | a format limitation (TOML has no null; its root must be an object) | | | E0604 | two keys flatten to the same `json-flat` key | a key containing a dot is indistinguishable from a nested path; use `--format json`, or rename the key |