Skip to content

Releases

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 13 Oct 23:03
fcc0632

An embedding release. A script can call functions its host provides (D35),
be reloaded while the service runs (D36), and be read whole into a host's own
types. One behaviour change: read_file now reads a relative path from the
manifest's folder (D37) - a run from the manifest's own folder is unaffected.

Changed

  • read_file reads a relative path from the manifest's folder (D37), as an
    import does - not from wherever the process was started. The same manifest
    now gives the same result from any directory, and a service embedding it
    through Script finds the files beside it. A function reads from the folder
    of the module that defined it. What may be read is unchanged: the granted
    roots stay relative to the current directory, and a manifest outside them is
    E0311, with a help line naming the folder and the flag that grants it. A
    run from the manifest's own folder behaves as before.

Added

  • Script::output and ScriptBuilder::env_values: a whole manifest, read
    into your own types.
    output() returns what aura eval prints, as any
    Deserialize type. env_values([(name, value)]) grants exactly those
    variables with exactly those values; env() never falls back to the process,
    so a host's environment cannot leak in. Every example in the repository is
    read this way in a test and compared with the output the CLI is pinned to.
  • Three examples read from Rust, as programs to copy from:
    cargo run --example read_device, read_k8s_deploy and
    read_service_catalog, with the structs aura types --lang rust writes for
    their schemas - checked in a test to be what the generator writes today.
  • Float.round(digits). (640.0 * 2.0 + 9.5) * 0.92 is
    1186.3400000000001, and there was no way to show it as 1186.34. Rounding
    is of the value the Float holds, so 1.005.round(2) is 1.0 on every
    machine; an exact binary tie goes to the even digit, as in IEEE 754. The
    result stays a Float, and digits is an Int from 0 to 15 (E0306
    otherwise).
  • A script can be reloaded while the service runs (D36). Script::reload()
    loads the files again; ScriptBuilder::watch(interval, on_reload) does it when
    their content changes - a touch loads nothing, and a file still being written
    is not loaded until it has been left alone for the settle period (two
    intervals and at least a second, or .settle(d)). A reload is a full load
    with the first load's grants and its aura.lock, frozen: it can never get more
    than the first load was given. Every export keeps its kind and number of
    arguments, or the reload is E0011 and the old version keeps serving; a bound
    Function follows its name. Calls in flight finish on the version they began
    on. on_reload is required, so a failed automatic reload cannot pass
    unnoticed. Script::version() returns the generation and a content hash.
  • A script can call functions its host provides (D35). The script imports
    them - import "host:geo" as geo - and the host registers them with
    ScriptBuilder::pure or ScriptBuilder::effect. A pure function is the
    host's promise of determinism and leaves the load hermetic; an effect is a
    grant, like allow_env. Host functions reach the root module only, unless
    grant_to_imports opens one to imports. Every use is checked at load: a
    module or function not provided is E0007, a wrong number of arguments
    E0009. At the call, wrong types are E0009, an Err from the host is
    E0008, and a host function that calls back into the script gets E0010.
    Each call costs 100 steps of the budget (host_call_steps). aura eval has
    no host, so a host: import there is E0007; aura check lists what the
    script needs, and the language server explains the import line.

v0.3.1

Choose a tag to compare

@github-actions github-actions released this 12 Oct 15:01
4a26a71

A data-integrity release: a JSON object could be read as a number. No new
API, nothing to change in a manifest.

Fixed

  • A JSON object whose key was $serde_json::private::Number was read as a
    number.
    {"$serde_json::private::Number": "123"} came back from
    parse_json() as the integer 123, with no error. The key is serde_json's own
    internal marker: Aura read numbers through its arbitrary_precision mode,
    which represents a number as an object with exactly that key, and so could
    not tell the two apart. Present since 0.2.0; found by the nightly
    ClusterFuzzLite run, which read such a key from YAML, wrote JSON and failed to
    read it back. Numbers are now read with float_roundtrip instead, which keeps
    the precision fix from 0.2.0 – the same digits still mean the same number in
    every format – without a marker that data can collide with.

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 12 Oct 12:29
ef107a5

A security and embedding release. Script (D33) lets a Rust host call into
Aura safely; every evaluation – the CLI, the facade, the playground and
Script – now has a budget (D34). The budget closes a hole in 0.2.0, where a
95-byte manifest that passes check --hermetic could run for over a minute and
hang a CI job that evaluated it.

Changed – may need an edit

  • Every evaluation has a finite budget by default: 10 million steps and
    256 MiB, exhausted as E0325 / E0326. Measured against every example in
    the repository, the heaviest spends 302 steps and 17 KB. A manifest that
    legitimately needs more passes --max-steps / --max-memory; --stats
    shows what it spends.

Added

  • Every evaluation has a budget, not only Script (D34). aura eval,
    eval_file and the browser playground used to evaluate with no limit, and
    aura eval runs third-party packages: a 95-byte manifest that passes
    check --hermetic ran for over a minute, which in CI is a build that hangs
    until its job times out. The default is the same everywhere – 10 million steps
    and 256 MiB, exhausted as E0325 / E0326 – and was measured rather than
    guessed: the heaviest example in the repository spends 302 steps and 17 KB.
    --max-steps and --max-memory change it, and --stats prints what an
    evaluation spent; Evaluated::usage reports the same to a host. A manifest
    that legitimately needs more now has to say so.

  • Script: calling into Aura from Rust (D33). A host loads a script once and
    calls its pub def functions as often as it likes, from as many threads as it
    likes, with host types crossing through serde:

    let rules = Script::builder().file("rules.aura").load()?;
    let route: Route = rules.call("route", (path, method))?;

    The route this replaces drove the compiler's internals by hand and went around
    the facade, so it had no capability model, no analysis and no diagnostics.
    Measuring it found two defects that ruled it out for a long-running process:
    every call kept about 2 KB alive forever, and a 95-byte manifest that passes
    check --hermetic ran for over a minute.

    • Hermetic by default: with no grants, load proves the script reads no
      file, variable or clock (E0505). Grants mirror the CLI's flags.
    • A budget on every call, in steps and bytes – never in time, which would
      make the outcome depend on the machine. E0325 / E0326; the default is
      finite.
    • A checked boundary: a value Aura cannot hold exactly is E0005, and a
      result of the wrong shape is E0006 with the path inside it. Binding a
      function checks its name, kind and arity once (E0002–E0004).
    • Send + Sync, so one instance serves every thread.

    Measured on one core: about 1.2 µs a call, against 3.8 µs on the old route;
    0.9 million events a second called per event, 1.3 million in batches.
    Tested before it was implemented – tests/script_api.rs was written against
    the designed API – and fuzzed by the new fuzz_script target.

Changed

  • A function call releases its frames when nothing can refer to them. Every
    call kept its environment until the whole evaluation ended, so xs.map(...)
    over a long list held a frame per element – measured at gigabytes for one
    ordinary-looking expression. Only a function value refers to an environment,
    so a call that created none releases its frames on return. The evaluator
    benchmarks are unchanged or faster.
  • Environments hold their bindings in an RwLock, and the source arena in a
    Mutex, so values can be shared across threads. Measured: no slowdown. Miri
    passes on the arena under both Stacked and Tree Borrows.
  • import "./lib.aura" resolves from in-memory sources as it does from
    disk. The in-memory resolver looked the path up verbatim, so the same manifest
    ran from a directory and failed with E0404 from memory.

Fixed

  • A lambda parameter declared inside #{...} pointed at the wrong text.
    Found by the first nightly ClusterFuzzLite run. Declarations made inside an
    interpolation were recorded relative to the interpolated text rather than the
    file, and searched for among the file's tokens, where the whole string is one
    token. The language server clamped the position, so it did not crash – but
    go-to-definition jumped elsewhere, and a rename would have rewritten unrelated
    bytes. Plain ASCII hid it; a multibyte character before the interpolation
    made it visible.

Security

  • rustls 0.23.42 → 0.23.45, the TLS stack aura add uses to fetch packages –
    the only place Aura touches the network.

Removed

  • examples/scripting.rs, the sketch that called a pub def through the
    compiler's internals. Script supersedes it; cargo run --example embedding
    shows the supported way.

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 11 Oct 07:27
0200e0e

I design Aura and make every decision about it; this release was built together
with Claude, an AI model by Anthropic, which wrote the code and the tests that keep
the documentation honest. How Aura is built tells
the longer story.

The minor number moves because behaviour moved. Four changes reject or
re-render something 0.1.1 accepted, so a manifest that worked before may need
one edit — each is listed under Changed below with what to do about it.

Everything else is addition: typed list elements, nullable fields, schema
invariants, folds and predicates, + on strings and lists, digit separators,
substrings, widening, and a compact/spelled-out pair for every serialisation
format. Three reproducibility defects were also fixed, all of which could
silently produce a wrong configuration: a YAML merge key that was not applied,
TOML that came back alphabetised, and the same digits meaning different numbers
depending on which format carried them.

Changed — may need an edit

  • A schema's default value is now checked where the schema is declared
    (E0516). q: Int = "not an int" used to pass check --strict with nothing
    to report but an unused type; the mismatch waited for new, and a field that
    callers always supply explicitly never reaches new with its default at all —
    so a schema could carry an impossible value indefinitely while the declaration
    claimed otherwise. Only literal defaults are judged, because what an arbitrary
    expression evaluates to is not knowable before it runs.

    A declaration that was already wrong will now say so. q: Int = null wants
    q: Int? = null: the marker admits null and the default makes the field
    optional — the marker alone still requires it at new.

  • Interpolating a null is now E0324 instead of printing the four letters
    null (D31). Where a value can be absent, say what stands in for it:
    "#{x == null ? "none" : x}", or xs.compact().join(", ") for a list.

  • to_yaml() and to_toml() return the compact form (D32). If the string
    is written to a file a person reads, use to_yaml_pretty() /
    to_toml_pretty(). The CLI is unaffected: --format still writes the
    spelled-out form.

  • parse_toml() preserves the document's key order instead of sorting it.
    Output that pinned the alphabetised order will differ; the new order is the
    one the input file was written in.

Added

  • Every format has a compact and a spelled-out spelling (D32). to_json(),
    to_yaml() and to_toml() now give the most compact form the format allows;
    to_json_pretty(), to_yaml_pretty() and to_toml_pretty() give the form a
    person reads.

    Before this each emitter had made its own choice and the three had never been
    compared against each other: JSON came out compact, YAML came out block style,
    TOML expanded its arrays one element per line. Nothing explained the
    difference, so the only way to know what a method returned was to run it.

    Compact is the default because a serialised string is usually embedded —
    in a field of another configuration, in an environment variable, in a command
    argument — where a newline has to be escaped and helps nobody. The spelled-out
    form is for a file that lands in a repository, where one line per document
    makes a diff useless. That is the whole rule, and the method set follows from
    it rather than having to be memorised.

    Compact YAML is flow style, {a: 1, xs: [1, 2]}, which is ordinary YAML and
    not a dialect of it. Compact TOML keeps arrays on one line; a TOML document is
    several lines whatever happens.

    This changes output. to_yaml() and to_toml() return different strings
    than before — to_json() was already compact. Across the whole example
    corpus exactly one pinned line moved, in the device showcase, which now
    demonstrates both spellings against the consumer each is for. The CLI is
    unaffected: --format yaml|toml still writes the spelled-out form, because it
    is producing a document for a person rather than a string for a manifest.

  • null has no text form (D31). "#{x}" printed the four letters null
    while x.to_str() was E0309, because to_str is not registered for Null.
    One spelling of "render this as text" succeeded where the other refused, which
    is the kind of divergence this language is meant not to have.

    It is closed downward: rendering a null as text is now E0324 in
    interpolation, in join, and in the message of assert, an invariant or
    fail(). Closing it upward — printing null everywhere — would have made the
    common case silent, because the usual reason a null reaches a string is a
    key that lost its value, and the word null in the output reads downstream as
    a setting rather than as a missing one. That is what W0513 already warns
    about.

    The diagnostic names the remedy for the site it came from, and each one is
    executed by a test: #{x == null ? "none" : x} in a string,
    xs.compact().join(", ") in a list.

  • String.slice(start, end) and Int.to_float() (D30). Both gaps were
    found by writing the device showcase, not by theory — the manifest carried a
    comment apologising for the workaround.

    Trimming a trailing separator used to go through
    split("/").filter(...).join("/"). It is now
    topic.slice(0, topic.len() - 1), and the showcase's output is byte-identical,
    which is what says the two were equivalent.

    slice counts characters, not bytes, because len() does. If they
    disagreed, s.slice(0, s.len()) would be wrong the moment a manifest carried
    anything outside ASCII — which configuration routinely does. Half-open, indices
    clamped, same shape as List.slice.

    Int.to_float() widens. Before it, an Int reached Float by being divided
    by 1.0: correct under D6, since Float is contagious, but it reads as a
    trick rather than as intent. There is deliberately no Float.to_int() —
    narrowing would have to choose a rounding nobody asked for, and a lossy
    conversion must not look like a change of spelling.

  • Digit separators in numbers (D29). max_bytes: 10_000_000, and in the
    fractional part too, where groups run away from the point: 0.000_001.

    Configuration is where long numbers actually live — byte limits, quotas,
    retention windows. Miscounting a zero is the kind of mistake nothing else
    catches: the value is valid, the type is right, and the service quietly gets
    ten times the memory it should.

    It is optional and carries no meaning: 2371933 and 2_371_933 are the same
    token, and nothing asks you to group anything.

    Where used, it must group by threes, counted away from the decimal point —
    so the short group sits at the front of an integer and the end of a fraction:
    8_080, 1.123_45. This is stricter than Go, Ruby and Rust, where
    2_3_7_1_9_3_3 is legal. A separator free to sit anywhere is a second way to
    write one number, and it misleads: 1_0000_000 is ten million, but a reader
    scanning for threes sees something far larger. The cost is Indian grouping
    (1_00_00_000), which has to be written without separators.

    100_, 1__0 and 1_.5 are the new E0108 as well. A leading _ is still
    an identifier, as it always was.

    Numbers written plainly stay zero-copy; only a literal that actually contains a
    separator allocates.

  • A schema can state rules about its own values (D28). assert inside a
    type body, checked on every new:

    type Plan
      price_monthly: Int
      price_yearly:  Int
      assert price_yearly <= price_monthly * 12, "yearly must not exceed twelve months"
    end
    

    Cross-field rules are the reason this is worth having — no per-field constraint
    can relate two values. They need no ordering or cycle analysis either, because
    every field is filled and type-checked before any invariant runs. That is what
    makes this feasible where the dependent defaults of D15 were not.

    It travels with the schema. An importer instantiating new pkg.Port gets
    the package's rules, in a manifest whose author never read them. Written beside
    the new instead, the rule is not part of the type: it does not move, tooling
    cannot see it, and the next person does not write it. That is the argument D18
    made for enum, applied to conditions.

    An invariant sees its own fields and nothing else — not the declaring
    module, not the instantiation site. A rule that could read a module variable
    would mean something different in each manifest that imported the schema, which
    is the problem being solved; it also puts everything effectful out of reach, so
    an instance is valid or invalid identically for everyone. An undefined name
    there says so and lists the fields available.

    A failure is the new E0515, naming the schema and carrying its message.

  • A field can say that it may have no value (D27). quota: Int? admits
    null; every other field still rejects it.

    This exists to kill the sentinel. Without it, "unlimited" gets written as -1
    or 2147483647, and everyone has to carry in their head which number means
    what and where. A sentinel is a valid value of its type, so nothing catches a
    wrong one — which makes it the worst kind of the implicit rule this language
    keeps removing.

    ? answers exactly one question: may the value be absent. Whether the field
    may be left out is the separate job of = default, and the two compose:

    Declaration Omitting it Writing null
    quota: Int E0511 E0512
    quota: Int? E0511 allowed
    quota: Int = 100 gives 100 E0512
    quota: Int? = null gives null allowed

    A nullable field is still required to be written, and its non-null values are
    still type-checked. Nullability belongs to the field rather than to the type,
    which makes ...

Read more

v0.1.1

Choose a tag to compare

@github-actions github-actions released this 01 Aug 23:52

A documentation and diagnostics release. No behaviour of the language itself
changed, and no manifest that worked on 0.1.0 needs touching.

Fixed

  • The agent reference taught a type that does not exist. aura docs --agent
    and the published llms.txt listed Str among the built-in types and used it
    in a schema example. Str is the Rust variant name; the language accepts only
    String. An assistant following the reference wrote a field the parser rejects.

    Fixed at the class rather than the instance: BUILTIN_TYPE_NAMES is now the
    one list, the parser matches through it, the reference prints from it, and a
    test fails if Str reappears.

  • An unknown field type said "undefined variable". a: Str reported
    use of undefined variable 'Str' and pointed at the whole type block —
    wording that never mentions types, sending the reader to look for a missing
    binding. It now says unknown type 'Str', points at the type name, lists the
    six built-ins, and suggests the likely one.

    The suggestion needed more than edit distance: Str against String is three
    insertions, past the typo threshold, so the most probable mistake — an
    abbreviation of the real name — got no hint at all. A prefix relation is tried
    first now.

  • Referring to a property by name explained nothing. assert limits.max <= 1
    where limits: is a property reported an undefined variable. True, and
    useless: : exports without creating a name, and that is the language's
    central rule, so the diagnostic now teaches it instead of restating the
    symptom. It fires whichever order the reference and the property appear in.

Documentation

  • How to install Aura was documented nowhere. The quick start told the reader
    to clone the repository and build from source — the instruction from before
    crates.io, release binaries and setup-aura existed. Both READMEs and the book
    now lead with the playground, which needs no install at all.
  • The README is rebuilt around what a reader needs first, in both languages:
    installation up front, a sixty-second example, the language tour and the
    verification table folded away, and badges for crates.io and docs.rs.
  • The type mapping used by aura types is documented for all three targets,
    including the part that only showed up when generating: a schema's List and
    Object carry no element type, so neither can the generated code.
  • Embedding now says to turn default features off — cargo add aura-lang locks
    96 packages, and 38 without the cli feature the library never uses.
  • SPEC says String where it describes something a user sees, notably the
    E0512 wording. The lexer's TokenKind::Str keeps its name, which is correct.

Added

  • D19, open and not adopted: a host can already call a manifest's pub def,
    so rules can live in a .aura file and be executed by an application per
    request, with no intermediate JSON and no rebuild.
    cargo run --example scripting demonstrates it and asserts its own results.
    What is missing is ergonomics, and what is deliberately unresolved is the
    other direction — a script cannot call back into the host, and opening that has
    to be shaped like the capability model rather than bolted beside it.
  • The crate documentation now states which surface is supported: facade is
    meant to stay stable; the layers beneath are public for tools and may change
    while 0.x.

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 01:40

The first release. Aura compiles readable manifests to JSON, YAML or TOML,
with schemas, assertions that fail the build rather than the deploy, and a
capability model in which env() and read_file() are granted per run and the
grant does not reach imported modules.

Nothing shipped before this, so there is nothing to migrate from and no
compatibility section. While 0.x, minor releases may still change the
language.

Language

  • Deterministic evaluation. now() and randomness do not exist (D13); durations
    and dates are epoch integers, via parse_duration/parse_datetime and their
    formatters. The same manifest with the same inputs produces byte-identical
    output.
  • key: properties are exported, x = bindings are private (D10) — which is
    also what makes dead-code analysis sound.
  • Schemas with type checking, and optional fields through = default (D15).
  • enum (D18): a closed set of allowed strings, usable as a schema field
    type. A member is an ordinary String, so output is unchanged; a non-member is
    E0514, with a did-you-mean suggestion and the full member list. pub enum is
    exported, and members resolve where the schema is declared, so an imported
    schema validates against its own module's enum.
  • Immutability with an explicit shadow (D7). Int and Float are distinct
    types (D6).
  • Packages: pub def / pub type, versioned imports, and aura.lock (D8/D12).
    An exported function runs with its origin module's capabilities, so
    isolation cannot be borrowed by asking the caller to invoke it.
  • Multi-way cond (D14), range(n), and text … end block strings (D16), whose
    interiors interpolate with #{} while leaving braces alone — which is what
    makes generating nginx configs and Dockerfiles practical.
  • Every code body is a scope (D17): a def or lambda body takes statements
    (=, shadow, assert) exactly as a module does.
  • Sixty standard-library methods across String, Int, Float, Bool, List and
    Object.

Tooling

  • aura eval / check / fmt / types / add / docs, with --strict,
    --frozen, --hermetic and --dry-run. Rich diagnostics through ariadne;
    JSON, flattened JSON, YAML and TOML output.
  • aura fmt is a canonical formatter: indentation, intra-line spacing, and
    column-aligned runs of name = value, key: value and cond arms together
    with their trailing comments. Strings and block-string interiors are untouched,
    the token stream never changes, and formatting is idempotent — both fuzzed.
  • aura types generates host-language types from a manifest's type and
    enum declarations (--lang rust|ts|go), so one schema both validates the
    config and types the service consuming its JSON. Parsing only: no evaluation,
    no capabilities, deterministic output, already canonical for rustfmt,
    gofmt and prettier.
  • aura docs --agent prints the complete language reference — syntax,
    standard library, every diagnostic code — assembled from the compiler's own
    definitions, at roughly four thousand tokens. Also published as
    llms.txt.
  • Language server (aura-lsp): live diagnostics, context- and type-aware
    completion, hover, go-to-definition in-file and across modules, find
    references, document symbols, rename (F2), signature help, and
    formatting/format-on-save. Its completion database is built by evaluating an
    Aura manifest with Aura itself.
    • Rename rests on scope-precise resolution, so x in a lambda and x at the
      top level are never conflated and uses inside #{…} are found. It refuses,
      with a reason, rather than guessing: on a file with syntax errors, on
      anything that is not a binding, on a malformed name, on a name already bound
      in an overlapping scope, and on pub items whose importers it cannot see. A
      test renames every binding in examples/showcase and asserts the evaluated
      JSON stays byte-identical.
    • Inside new lib.Endpoint, a field typed by an imported pub enum offers
      exactly its members. Go-to-definition on a registry import opens the cached
      package through the same resolver evaluation uses, so it cannot land on a
      different file than the one that would be evaluated.
  • A browser playground — the real compiler as WebAssembly, multi-file, with
    aura fmt and diagnostics. Nothing is installed and nothing leaves the page.
  • Editor support: TextMate grammar for VS Code, plus Vim/Neovim and nano.
  • The library and the CLI are one publishable crate: cargo add aura-lang to
    embed, cargo install aura-lang for the tool. The cli feature is
    detachable, which is what keeps the library free of clap, ariadne and ureq and
    able to build for wasm.

Security and supply chain

  • --hermetic turns env() and read_file() into E0505 in every module.
    Because that is an analysis error, aura check --hermetic proves a manifest
    performs no I/O without evaluating it — including for branches a given run
    would not take.
  • Capability refusals distinguish their causes: E0310 when nothing was
    granted, E0311 when a grant exists but the path resolves outside it, with
    the allowed directories named.
  • aura.lock pins a package's exact version and a hash of its token stream
    rather than its bytes, so reformatting or editing a comment does not fire the
    integrity check while any change that can alter behaviour does. Locks written
    with the older byte hash still verify, and are upgraded in place.
  • Six coverage-guided fuzz targets: lexer, parser, pipeline, formatter, codegen
    and resolver.

Distribution

  • Binaries for six targets on a tag — Linux gnu and musl, aarch64 Linux, macOS
    Intel and Apple silicon, Windows — each with a .sha256.
  • aura-config/setup-aura@v1
    installs the CLI in GitHub Actions, verifying the checksum before unpacking.
    On x86_64 Linux it installs the musl build: the gnu build requires
    GLIBC_2.34 and does not start on Ubuntu 20.04, Debian 11, CentOS 8 or Amazon
    Linux 2, while the static build has no such floor and is measurably faster.
  • packaging/e2e.sh drives the built binary through the claims this
    documentation makes — exit codes, capability refusals, hermetic mode, output
    formats, and byte-identical output across two runs. It runs on three operating
    systems per push, and on a tag against the real artifacts inside five
    containers, including aarch64 under emulation.

Performance

Measured on x86_64 Linux; re-run cargo bench -p aura-lang on your own machine.

  • 258 MiB/s lexing, 178 MiB/s through the parser, 33 µs for a full
    lex-parse-evaluate of the reference manifest.
  • The binary is 3.5 MB, a 1.7 MB download, with link-time optimisation and no
    symbol table. LTO is not a size-against-speed trade here: it made the binary
    17% smaller and about 9% faster.