Введение
Aura — конфигурационный язык, который компилируется в JSON/YAML/TOML. Он создан вокруг трёх обещаний:
- Детерминизм по построению. Два запуска дают побайтно одинаковый результат.
В языке нет
now(), случайности и неявного доступа к окружению — всё внешнее (файлы, переменные среды) приходит только через явные права, выданные при запуске. - Безопасность цепочки поставок. Импортированный пакет физически не может читать ваши файлы и переменные окружения — capability-модель в стиле Deno, но строже: даже вызов экспортированной функции пакета выполняется с правами пакета, а не вашими.
- Валидация до деплоя. Схемы с типами,
assert-инварианты, статический анализ мёртвого кода — ошибки конфигурации ловятся при сборке, а не в проде.
Как это выглядит
type Service
name: String
port: Int
end
base_port = 8000 # приватное вычисление
is_prod = env("APP_ENV", "dev") == "production"
api: new Service # свойство — попадает в вывод
name: "api"
port: base_port + 1
end
replicas: is_prod ? 3 : 1
assert base_port > 1024, "privileged ports are not allowed"
aura eval app.aura --allow-env=APP_ENV
Кому это нужно
- DevOps/SRE: манифесты k8s, CI-матрицы, конфиги сервисов — без копипасты
YAML и с проверкой схем до
kubectl apply. - Платформенным командам: публикуемые пакеты стандартов (
pub def/pub type) с версиями и integrity-локом. - Разработчикам приложений: одна
.aura-декларация → JSON/YAML/TOML для любого потребителя; для Rust — прямое встраивание библиотекой.
Где что лежать
- Эта книга — учебник и справочник для пользователя языка.
- SPEC.ru.md — формальная спецификация для разработчиков реализации.
- examples/ — работающие примеры с ожидаемыми выводами.
Основная версия книги — английская. Русская публикуется под
/book/ru/и является полным переводом; при расхождении верна английская.
Быстрый старт
Установка
Быстрее всего попробовать язык, ничего не устанавливая: playground запускает настоящий компилятор прямо в браузере.
Бинарник:
cargo install aura-lang
Либо готовая сборка со страницы
Releases — Linux (gnu и
статический musl), macOS (Intel и Apple silicon), Windows, к каждой .sha256.
На x86_64 Linux предпочтительна musl-сборка: она статическая, порога glibc нет.
В GitHub Actions:
- uses: aura-config/setup-aura@v1
- run: aura check deploy.aura --strict
Сборка из исходников:
git clone https://github.com/aura-config/aura-lang && cd aura-lang
cargo build --release
# бинарник: target/release/aura
Первый манифест
Создайте hello.aura:
app_name = "hello"
port = 8080
service:
name: app_name
url: "http://localhost:#{port}"
replicas: 2
end
$ aura eval hello.aura
{
"service": {
"name": "hello",
"url": "http://localhost:8080",
"replicas": 2
}
}
Обратите внимание: app_name и port (объявленные через =) в JSON не попали —
это приватные вычисления. В вывод идут только свойства (имя:) и блоки.
Подробнее — в следующей главе.
Три команды, которые нужны каждый день
aura eval app.aura # вычислить → JSON в stdout
aura check app.aura --strict # проверить без вычисления (линтер; для CI)
aura fmt app.aura # канонизировать отступы
Структура без боли
Aura не использует значимые отступы (травмы YAML лечатся здесь). Структуру
задают переносы строк и явное end:
server:
http:
port: 8080
timeouts:
read: "30s".parse_duration()
end
end
end
Отступы — только для людей; aura fmt приводит их к канону автоматически
и гарантированно не меняет смысл (поток токенов до/после идентичен).
Вычисления и вывод: = против :
Главное правило Aura, отличающее её от YAML/JSON (аналог locals vs outputs в Terraform):
| Форма | Смысл | В выводе? |
|---|---|---|
x = expr | приватная переменная | нет |
key: expr | свойство | да |
domain "name" ... end | блок | да (ключ = имя) |
base = 8000 # вычисление
port: base + 80 # вывод: {"port": 8080}
Благодаря этому анализ мёртвого кода точен: неиспользуемая =-переменная —
всегда настоящий мусор (W0501), а не «может, это чей-то вывод».
Иммутабельность
Повторное присваивание в одной области — ошибка:
x = 1
x = 2 # E0301: variable 'x' is immutable
Явное затенение
Переопределить внешнюю переменную во вложенном блоке можно только явно:
log_path = "/var/log/app.log"
domain "debug"
log_path = "/tmp/debug.log" # E0302: требуется shadow
shadow log_path = "/tmp/debug.log" # ок — намерение видно в diff'е
path: log_path # → "/tmp/debug.log"
end
«Почему в проде другой путь?» больше не тихий сюрприз — затенение всегда
видно по слову shadow.
Длинные числа
Разделяйте разряды через _ там, где это помогает глазу:
max_bytes: 10_000_000
retention: 2_592_000
port: 8_080
rate: 0.000_001 # в дробной части группы отсчитываются от точки
Это полностью необязательно: 2371933 и 2_371_933 — одно и то же число, и
никто не требует что-либо группировать.
Но если вы группируете, разделитель обязан разбивать по три, считая от
десятичной точки. Поэтому короткая группа оказывается в начале целой части и в
конце дробной: 8_080, 1.123_45.
Это строже, чем в Go, Ruby или Rust, где 2_3_7_1_9_3_3 допустимо. Причины две.
Разделитель, который может стоять где угодно, — это второй способ записать одно
и то же число, а язык тратит свои правила ровно на то, чтобы таких способов не
было. И он вводит в заблуждение: 1_0000_000 — это десять миллионов, но всякий,
кто читает по три, увидит куда большее число.
100_, 1__0 и 1_.5 — тоже ошибки: разделять там нечего.
Всё это существует потому, что длинные числа живут именно в конфигурации, а просчитанный ноль — та ошибка, которую больше ничто не поймает: значение корректно, тип верен, и сервис тихо получает в десять раз больше, чем должен.
Строки и интерполяция
name = "auth"
image: "company/#{name}:v#{1 + 1}" # → "company/auth:v2"
Внутри #{...} — обычный синтаксис выражений: кавычки вложенных строк
не экранируются ("#{list.join(", ")}" — валидно).
У null нет текстовой формы, поэтому его интерполяция — это E0324, а не
слово null:
host = env("HOST", null)
url: "https://#{host == null ? "localhost" : host}/api"
Проверка написана явно, потому что какой текст подставить вместо отсутствующего
значения — это решение о конфигурации. Если бы печаталось null, эти четыре
буквы попали бы в вывод, и тот, кто читает его дальше, увидел бы настройку, а не
её отсутствие. А обычная причина, по которой null оказывается в строке, — это
значение, потерянное выше по цепочке.
Доступ к данным
Точка — для полей, скобки — только для индексов списков:
version: cfg.package.version # обычные ключи
special: cfg."ключ с пробелом" # любой ключ — строкой
dynamic: cfg.envs."#{region}" # динамический ключ
first: apps[0].name # индекс (за границами — E0317)
optional: cfg.get("maybe", "default") # промах → default, не ошибка
Опечатка в имени поля — ошибка E0308 с точной позицией, а не молчаливый null.
Блоки и области видимости
Конфиг редко бывает плоским. В Aura есть две формы вложенности, и различаются они не выводом, а тем, что разрешено писать внутри.
| Форма | Что внутри | В JSON |
|---|---|---|
key: … end | только свойства — данные | вложенный объект |
domain "имя" … end | свойства и вычисления (=, shadow, assert, def) | объект под ключом-меткой |
Литерал объекта: просто данные
security:
tls: true
min_version: "1.3"
end
{ "security": { "tls": true, "min_version": "1.3" } }
Внутри допустимы только свойства. Нужны вычисления — берите domain.
В одну строку
Короткий объект помещается в одну строку: тот же блок, только с запятыми там, где были переводы строк (D38).
resources:
requests: cpu: "250m", memory: "512Mi" end
limits: cpu: "500m", memory: "1Gi" end
end
Запятая разделяет два поля и ничего больше - пропущенная или стоящая перед
end даёт E0205, и сообщение говорит, какая именно. aura fmt сохраняет ту
форму, которую вы выбрали. Два end в строке (a: b: 1 end end) допустимы, но
читаются тяжело: внешний уровень лучше записать блоком.
Внутри списка однострочной форме нужна схема, new Port name: "http", port: 80 end - там запятая разделяет ещё и элементы, а схема заодно проверит поля.
Через =
= привязывает объект ровно так же, как : его выводит (D10): блок тот же, но
в результат он не попадает.
defaults =
port: 8080
timeout: "30s"
end
limits = cpu: "500m", memory: "1Gi" end
server:
port: defaults.port
timeout: defaults.timeout
end
{ "server": { "port": 8080, "timeout": "30s" } }
Замените defaults = на defaults: - и объект попадёт в вывод; больше в нём
ничего не изменится. Если у блока после = забыт end, ошибка укажет на
строку с =.
domain — секция со своей областью видимости
Метка становится ключом, а тело — полноценная область: =, shadow, assert
работают:
domain "prod"
replicas: 3
region: "eu-central"
end
{ "prod": { "replicas": 3, "region": "eu-central" } }
Метка может быть выражением, не только строковым литералом.
Область видимости и shadow
Внешние переменные видны, но их перекрытие требует явного shadow (D7):
port = 80
domain "prod"
shadow port = 443 # без `shadow` — E0302
listen: port # 443
end
fallback: port # 80 — снаружи ничего не изменилось
Это прямой ответ на вопрос «почему в проде другой порт»: перекрытие видно в коде.
Списки объектов: map без лишних ключевых слов
Тело лямбды — тоже область видимости (D17), поэтому элементы списка описываются напрямую, включая приватные вычисления:
services: ["api", "web"].map (n, i) ->
base = 8000 # приватно: в JSON не попадёт
name: n
port: base + i
end
{
"services": [
{ "name": "api", "port": 8000 },
{ "name": "web", "port": 8001 }
]
}
Поле name здесь — обычное свойство, которое вы пишете сами. Раньше эту строку
неявно вставляло ключевое слово component; оно убрано в D17 — язык не
добавляет полей за вашей спиной.
Что выбрать
- Только данные →
key:…end. - Данные плюс вычисления →
domain. - Список объектов →
mapс телом-лямбдой; блок не нужен.
Общее правило: тела кода (def, лямбда, domain) — это области видимости;
литерал объекта — это данные.
Схемы и валидация
Схемы
type объявляет схему, new — инстанцирует с проверкой:
type Container
name: String
image: String
port: Int
end
main: new Container
name: "gateway"
image: "company/gateway:2.4.1"
port: 8080
end
Короткий экземпляр помещается в одну строку, с запятыми между полями (D38). Проверяется он ровно так же, как блочная форма:
sidecar: new Container name: "proxy", image: "envoy:1.30", port: 9901 end
Проверки при инстанцировании:
| Ситуация | Код | Когда |
|---|---|---|
| Отсутствует поле схемы | E0511 | всегда |
Неверный тип (Int vs String, Int vs Float) | E0512 | всегда |
| Лишнее поле, не объявленное в схеме | E0513 | только --strict |
Значение по умолчанию проверяется там, где схема объявлена, а не только там,
где используется. q: Int = "not an int" — это E0516 прямо на type, потому
что поле, которое вызывающие всегда задают явно, иначе никогда не дошло бы до
new со своим умолчанием, и объявление продолжало бы утверждать невозможное.
Судятся только литералы: что даст произвольное выражение, до запуска неизвестно,
а результат всё равно проверит new.
type Quota
# `Int? = null` — маркер допускает null, а умолчание делает поле необязательным.
# Один только маркер по-прежнему требовал бы поле при `new`.
limit: Int? = null
end
Типы полей: String, Int, Float, Bool, List, Object.
Int и Float — раздельные типы: лимиты памяти в байтах и 64-битные
идентификаторы не теряют точность (E0304 на переполнении, а не тихий wrap).
Поля, у которых может не быть значения
Поле хранит значение своего типа и ничего больше. Когда значения действительно
нет - безлимитная квота, срок, который никто не назначил, - пометьте поле ?:
type Plan
id: String
quota: Int?
end
unlimited: new Plan
id: "enterprise"
quota: null
end
Это нужно, чтобы покончить с сигнальными значениями. Без ? «безлимит»
пишут как -1 или 2147483647, и каждому приходится помнить, какое число что
значит и где. Сигнальное значение - законное значение своего типа, поэтому
неверное никто никогда не поймает.
? отвечает только на один вопрос: может ли значения не быть. Можно ли поле
опустить - отдельная задача = default, и они сочетаются:
| Объявление | Если опустить | Если написать null |
|---|---|---|
quota: Int | E0511 | E0512 |
quota: Int? | E0511 | можно |
quota: Int = 100 | будет 100 | E0512 |
quota: Int? = null | будет null | можно |
Nullable-поле по-прежнему проверяется по типу, когда значение в нём есть, а
aura types выпускает Option<i64>, number | null и *int64.
Единственное место, где ? может навредить
? расширяет то, что поле принимает, и именно поэтому на него стоит
посмотреть ещё раз, когда значение приходит извне:
quota: data.get("quota") # без запасного значения
Без ? отсутствующий ключ - это E0512, и прогон останавливается, так что
двое людей с разными входными файлами узнают об этом сразу. С ? та же разница
проходит как null, и они молча выкатывают разные конфигурации.
Это сочетание - W0513, а под --strict - ошибка:
[W0513] Warning: 'quota' is nullable and is filled from `get(key)`, so a
missing value becomes null instead of an error
Help: name the value that stands for absence: `get(key, fallback)`
— or drop the `?` so a missing one is reported
env(name) без запасного значения - тот же случай. Предупреждение просит
только назвать запасное значение: data.get("quota", 0) говорит, какое значение
означает отсутствие, и говорит это в манифесте, а не в чьей-то голове.
Списки с типом элементов
List сам по себе принимает любые элементы. Чтобы сказать, что в нём лежит,
пишите [T]:
type Service
name: String
tags: [String]
endpoints: [Endpoint]
end
Тип повторяет значение: [1, 2] - список, значит, его тип [Int]. Вложенность
получается без нового правила, [[Int]], и enum тоже годится как тип
элемента.
Неверный элемент называется с индексом, потому что «один из них неверный» - не ответ в списке из сорока:
[E0512] Error: endpoints[0] of schema Service expects Endpoint, got String
Это доходит и до хоста: aura types выпускает Vec<Endpoint>, Endpoint[] или
[]Endpoint, а не нетипизированный массив.
Голый List сохраняет прежний смысл, так что ничего написанного раньше менять
не нужно.
Поле с типом-схемой хранит то, что принял бы new
Поле с типом Port, [Port] или [[Port]] хранит значения, которые принял бы
new Port, откуда бы они ни пришли (D41). Элемент из JSON, YAML или от хоста
проверяется по тем же правилам, что и написанный через new: обязательные
поля, типы, enum и инварианты.
type Port
number: Int
protocol: String = "tcp"
assert number > 0, "port must be positive"
end
type Service
ports: [Port]
end
raw = read_file("ports.json").parse_json()
svc: new Service ports: raw end
Ошибка называет путь до неверного значения:
[E0515] Error: Port: port must be positive (in ports[2])
[E0511] Error: missing field 'port' required by schema Endpoint (in routes[1].target)
Как это работает и почему
- Структурно, а не номинально. Значение не помнит, какая схема его создала,
поэтому вопрос звучит «принял бы это
new Port?», а не «создано ли это черезnew Port?». Данные извне проходят, если они правильные, и их не нужно пересобирать поле за полем через.map. - Умолчания подставляются.
{"number": 80}в поле[Port]становится{"number": 80, "protocol": "tcp"}, в порядке полей схемы, ровно как сделал быnew. Одна схема даёт одну форму вывода, каким бы ни был источник, - на это опираютсяaura typesи хост, читающий результат. - Экземпляр, созданный через
new, проверяется ещё раз. Проверки чистые, а умолчания уже на месте, так что ничего не меняется; альтернатива - скрытая метка «уже проверено» - была бы состоянием, которого читатель не видит. - Лишние поля - как у
new: сохраняются, а под--strictдаютE0513. - Схема может содержать саму себя -
children: [Node]- на глубину до 32 уровней; глубже -E0208, а не падение, потому что данные, в отличие от кода, можно построить любой глубины.
До D41 такое поле принимало любой объект: без number или с number, который
нарушает инвариант. Манифест, который на это опирался, теперь остановится с
ошибкой, называющей неверный элемент.
Правила, которые схема утверждает о себе
Тип говорит, какой формы значения. Он может сказать и то, какими они должны быть:
type Plan
price_monthly: Int
price_yearly: Int
max_channels: Int
assert price_monthly >= 0, "price must not be negative"
assert max_channels > 0, "a plan needs at least one channel"
assert price_yearly <= price_monthly * 12, "yearly must not exceed twelve months"
end
Они выполняются при каждом new Plan, а провал - это E0515 с сообщением,
которое написала схема:
[E0515] Error: Plan: yearly must not exceed twelve months
Ради третьего это и стоит иметь: оно связывает два поля, чего не выразит никакое ограничение на отдельное поле. И правило порядка ему не нужно, потому что каждое поле заполнено и проверено по типу до того, как выполнится любой инвариант.
Почему бы просто не написать assert рядом с new
Потому что тогда это не часть типа. Он не переезжает вместе со схемой, так что импортёр получает поля без правил; инструменты его не видят; а следующий, кто создаст экземпляр схемы, просто его не напишет.
Поместите его в схему - и он действует везде, включая манифест автора, который вашего пакета никогда не читал:
import "./lib.aura" as lib
p: new lib.Port
number: 80 # E0515: Port: ports below 1025 are privileged
end
Что видит инвариант
Свои поля и ничего больше. Ни модуль, где объявлена схема, ни тот, где написан
new:
[E0504] Error: use of undefined variable 'limit'
Help: an invariant sees only this schema's fields (q), so it means the
same in every manifest that uses the schema; pass anything else
in as a field
Это сделано намеренно. Правило, которое могло бы читать переменную модуля, значило бы разное в каждом манифесте, импортирующем схему, - а инварианты существуют как раз чтобы решить эту проблему. Заодно из него недостижимо ничего эффектного, так что экземпляр валиден или невалиден одинаково для всех.
assert
Произвольные инварианты — statement assert:
admins = [187650342, 244179081]
assert admins.uniq().len() == admins.len(), "Duplicate admin ids"
assert admins.len() >= 1
Провал — E0530 с вашим сообщением и точной позицией. assert работает
и в --dry-run — тестовый прогон обязан находить отказ валидации.
fail в выражениях
Для валидации внутри выражения есть fail:
port: p > 0 && p < 65536 ? p : fail("invalid port #{p}")
Закрытые наборы: enum
Поле String принимает любую строку — опечатка в значении доезжает до прода.
enum задаёт закрытый набор допустимых строк:
enum Tier
"frontend"
"backend"
end
type Service
tier: Tier
end
svc: new Service
tier: "backand" # E0514 + подсказка «did you mean "backend"?»
end
Член enum — обычная строка: в JSON попадёт "backend", никакой обёртки не
появится. Это ограничение на значение, а не новый тип данных.
pub enum экспортируется importer’ам (D12). Члены резолвятся там, где объявлена
схема, поэтому импортированная схема проверяется против enum своего модуля —
даже если у вас в области видимости есть одноимённый.
Статический анализ
aura check (и автоматически перед каждым eval) находит проблемы до запуска:
E0504— использование необъявленной переменной (в том числе внутри#{...});W0501/W0502/W0503— неиспользуемые переменные / импорты / функции и типы;W0512— эффектный вызов (env,read_file) в импортированном модуле.
В --strict предупреждения становятся ошибками — режим для CI.
Функции и методы
def — функции-конструкторы
def возвращает объект, когда тело - свойства (и значение, когда тело им
кончается, - ниже):
def make_env(name, replicas, debug)
replicas: replicas
log_level: debug ? "debug" : "warn"
db_url: "postgres://db.#{name}.internal:5432/app"
end
environments:
dev: make_env("dev", 1, true)
prod: make_env("prod", 6, false)
end
Один def вместо трёх скопированных YAML-файлов — главный анти-копипаст
инструмент языка.
Функция, которая возвращает значение
Функция, тело которой кончается выражением, возвращает это выражение (D40).
Перед ним можно писать привязки и assert, так что шаг можно назвать:
def ports_of(services)
all = services.map (s, i) -> s.port end
assert all.len() > 0, "no services"
all.uniq().sort()
end
ports: ports_of(catalogue)
Многострочная лямбда подчиняется тому же правилу; однострочная,
(x) -> x * 2 end, и так была выражением.
Три случая и почему они такие
- Свойства или последнее значение - не то и другое сразу. Тело со
свойствами
key:, которое ещё и кончается выражением, - этоE0215. Вернуть значение значило бы потерять свойства, вернуть свойства - потерять значение, и в обоих случаях что-то пропадало бы бесследно. Сделайте значение свойством (value: ...) или уберите свойства. - Значение, которое никто не использует, - ошибка. Голое выражение в любом
другом месте - в начале файла, в
domain, до последней строки функции - этоE0214. В Aura нет побочных эффектов, так что значение, которое никуда не идёт, ничего не сделало: автор почти всегда имел в видуkey:(вывести),name =(использовать дальше) илиassert(проверить), и подсказка называет все три. До D40 такая строка вычислялась и молча выбрасывалась. - Только привязки и проверки - по-прежнему пустой объект. Тело из одних
=иassertвозвращает{}, как и раньше: ничто в нём не задумывалось результатом, значит, ничего и не выбрасывается.
Обе ошибки выдаёт aura check, поэтому редактор показывает их сразу, пока вы
печатаете.
Лямбды
double = (x) -> x * 2 end
up: ["a", "b"].map (s, i) -> s.upper() end # trailing-лямбда
Колбэк map/filter получает элемент и индекс; лишние параметры можно
не объявлять.
Методы
Вызываются через точку, образуют цепочки:
active: services.compact().uniq().map (s, i) -> s.upper() end
Полный список — в справочнике методов. Часто используемые:
- списки:
mapfiltercompact(убратьnull)uniqfirstlastjoin(sep)contains(x)get(i, default) - объекты:
merge(правый перекрывает)keysvaluescontains(key)get(key, default) - строки:
upperlowerlencontains(sub)+ парсеры форматов
Тернарный оператор
Единственная ветвящаяся конструкция языка:
mode: is_prod ? "webhook" : "long_polling"
Условие обязано быть Bool — «правдивости» (truthiness) в Aura нет,
E0306 на любом другом типе.
Модули и пакеты
Импорты
import "templates/defaults.aura" as defaults # файл, относительно текущего
import github/acme/aura-k8s@v1.2 as k8s # пакет из registry, версия обязательна
Циклические импорты обнаруживаются с полной цепочкой:
E0401: cyclic import: a.aura -> b.aura -> a.aura.
Что экспортирует модуль
Модуль отдаёт импортёру объект: свойства, блоки и pub-элементы:
# validators.aura
pub type Service # схема — часть API
name: String
port: Int
end
def valid_port(p) # приватный хелпер: импортёру невидим
ok: p > 0 && p < 65536
end
pub def service(name, port) # функция — часть API
name: name
port: valid_port(port).ok ? port : fail("invalid port #{port}")
end
# использование
import "validators.aura" as v
api: v.service("api", 8080)
worker: new v.Service
name: "worker"
port: 9000
end
Ключевая гарантия безопасности: экспортированная функция выполняется с правами своего модуля, а не вызывающего. Пакет не может «одолжить» ваши права на чтение файлов, заставив вас вызвать его функцию.
Установка пакетов: aura add
aura add github/acme/aura-k8s@v1.2.3 # из сети (точная версия)
aura add pkg/internal@v1.0.0 --from ./pkg.aura # из локального файла
aura add — единственное место, где Aura обращается к сети: пакет
скачивается, валидируется, кладётся в локальный кэш (~/.aura/registry)
и фиксируется в aura.lock с хешем целостности. eval всегда работает оффлайн.
aura.lock и –frozen
- лок хранит точную версию + integrity каждого пакета;
- хеш считается по потоку токенов, а не по байтам: переформатирование пакета или исправление опечатки в комментарии проверку не срабатывают, а переименование поля или изменение числа — срабатывает. Проверка целостности, которая срабатывает на безобидных правках, приучает обновлять лок не глядя, что её и обесценивает;
- запись
sha256-от старой версии по-прежнему проверяется и обновляется при первой возможности записи; - подмена содержимого пакета →
E0402 integrity mismatch; - в CI запускайте с
--frozen: резолв строго по локу, отсутствие записи — ошибкаE0403, лок не переписывается.
Диапазоны версий (@v1, @v1.2) резолвятся по локальному кэшу в максимальную
подходящую (v1.2 → 1.2.*).
Время без сюрпризов
now() не существует
Вызов now() или timestamp() — ошибка E0533. Это не пробел, а дизайн-решение
(D13): текущее время сделало бы конфиг невоспроизводимым — сломались бы
--dry-run-сравнения, кэши и сама идея «два прогона дают одинаковый результат».
Если время сборки действительно нужно — его передаёт хост:
built_at: env("BUILD_TIME", "unknown")
BUILD_TIME=$(date -u +%FT%TZ) aura eval app.aura --allow-env=BUILD_TIME
Решение о времени принимает и фиксирует запускающая сторона, конфиг остаётся чистой функцией от входов.
Длительности
Таймауты и TTL — строки с единицами d/h/m/s, парсятся в секунды (Int):
ttl = "1h30m".parse_duration() # → 5400
cache:
ttl_seconds: ttl
refresh_every: (ttl / 3).format_duration() # → "30m"
end
Дальше — обычная целочисленная арифметика: сложение, вычитание, «перевести
сутки в часы» и т.п. Некорректная строка — E0319.
Даты
RFC3339 ↔ epoch-секунды (UTC):
start = "2026-07-18T22:00:00Z".parse_datetime() # → epoch Int
window:
start: start
end: (start + "4h".parse_duration()).format_datetime()
# → "2026-07-19T02:00:00Z" — переход через полночь корректен
end
Поддерживаются смещения (+02:00 указывает на тот же момент времени)
и короткая форма даты ("2026-07-18" — полночь UTC). Некорректная дата — E0320.
Календарная арифметика высокого уровня («+3 месяца», рабочие дни, таймзоны) — намеренно не в ядре: это территория будущих пакетов сообщества поверх epoch-примитивов.
Модель безопасности
Aura исходит из того, что конфиги собирают из чужого кода (пакеты, шаблоны коллег, скопированные сниппеты), и строит защиту по построению, а не по договорённости.
Capability-модель
По умолчанию манифест не может ничего: ни читать файлы, ни переменные окружения. Права выдаются флагами запуска и не «протекают»:
| Флаг | Разрешает |
|---|---|
--allow-read=<dir> | read_file() внутри каталога; пути канонизируются, .. не сбегает (E0311). Относительный путь читается от папки самого манифеста (D37) |
--allow-env[=A,B] | env() для перечисленных переменных (без списка — все) |
--hermetic | I/O нет вовсе: env() и read_file() — E0505 в любом модуле; несовместим с --allow-* |
--allow-imports-io | распространить права корня на импортированные модули |
Вызов без прав — E0310 с подсказкой, какой флаг добавить.
Герметичный режим
--hermetic — та же шкала с другого конца: вместо выдачи прав он объявляет, что
прав нет вовсе. env() и read_file() становятся E0505 где угодно — и в корне,
и в импорте, — а сам флаг несовместим с любым --allow-*.
Отличие от простого «не выдавать права» — в том, когда узнаёшь. Запрещённый
вызов — это E0310 во время выполнения, на той ветке, которая исполнилась;
E0505 — ошибка анализа, поэтому aura check --hermetic доказывает свойство для
всего манифеста, ничего не вычисляя. Это и делает его пригодным для гейта в CI:
$ aura check --hermetic manifests/*.aura
CUE, KCL и Starlark герметичны по построению. В Aura это режим — так один репозиторий может держать и манифесты, которым нужно читать окружение, и те, которым нельзя, а CI может требовать строгий вид по каталогам.
Ограниченное вычисление
Герметичность доказывает, что манифест ничего не может взять. Но не доказывает,
что он закончится: три вложенных range(1000).map умещаются в 95 байт, проходят
check --hermetic и работали бы больше минуты. Поэтому у каждого вычисления есть
бюджет — у CLI, eval_file, песочницы и Script одинаково:
| Бюджет | Исчерпан | По умолчанию | Флаг |
|---|---|---|---|
| шаги — вычисленные выражения | E0325 | 10 млн | --max-steps=N |
| байты — каждое созданное значение и кадр | E0326 | 256 МиБ | --max-memory=MB |
Оба считаются, а не замеряются по времени, поэтому один и тот же манифест
останавливается в одной и той же точке на любой машине. Значения по умолчанию
примерно в тридцать тысяч раз больше, чем тратит самый тяжёлый пример в
репозитории; --stats печатает расход, чтобы бюджет выбирался по замеру.
У глубины свой предел, потому что шаги её не ограничивают: функция, вызывающая
себя несколько сотен раз, тратит мало шагов и очень много стека. Одно выражение
вкладывается не глубже 256 уровней - длинная цепочка вроде a + b + c + ...
засчитывает свою длину, - а вычисление во время работы вкладывается не глубже 256
уровней, причём рекурсивный вызов засчитывает выражения, через которые проходит.
За любым из пределов - ошибка, E0208 или E0399, а не переполнение стека (D42).
Как и бюджет, предел - это счёт, поэтому манифест останавливается в одной и той
же точке везде. Release-сборке на него хватает 512 КиБ стека, так что хост может
вызывать Script с любого потока, где стека не меньше, - у вторичного потока iOS
ровно 512 КиБ.
У значений тот же предел (D43). Цикл может построить глубокое значение без
всякой собственной вложенности - reduce, заворачивающий аккумулятор, добавляет
уровень на каждом шаге, - а любой обход значения, от вывода до освобождения
памяти, рекурсивен по уровням. Поэтому список или объект глубже 256 - это E0328
там, где его строят, а значение от хоста такой глубины отвергается на границе.
Разобранные JSON и YAML и так останавливаются на 128.
Изоляция импортов
Права принадлежат корневому манифесту. Импортированный модуль:
- не может вызывать
env()/read_file()— даже если у корня права есть; - не получает права через свои экспортированные функции: тело
pub defвыполняется с capability модуля-происхождения, а не вызывающего; - статически помечается предупреждением
W0512, если содержит эффектные вызовы.
$ aura eval main.aura --allow-read=.
[E0310] Error: imported module has no capability to call read_file()
╭─[ evil_dependency.aura:2:7 ]
Живой пример — examples/security_demo/.
Функции хоста и перезагрузка
Приложение, встраивающее Aura, может дать скрипту свои функции (D35) и перезагружать его на ходу (D36). Оба механизма подчинены той же модели:
- Функция хоста - это разрешение, которое приложение выдаёт по имени, по одной.
pure- обещание хоста, что функция детерминирована, - оставляет скрипт герметичным;effect- разрешение, как--allow-env. - Они доступны только корневому модулю. Импортированный пакет не может их
вызвать, если приложение не открыло ему эту функцию по имени (
E0310). - Каждое использование проверяется при загрузке, до любого вычисления:
непредоставленная функция -
E0007. Функция хоста не может вызвать скрипт обратно (E0010). - Перезагрузка - это полная загрузка с разрешениями первой и её
aura.lock, замороженным: правка скрипта не расширит его права, не подтянет незакреплённый пакет и не уберёт экспорт, к которому привязалось приложение (E0011). Недописанный файл не загружается, пока его не оставят в покое на период тишины.
Supply chain
- Версия пакета обязательна в самом импорте:
import github/acme/pkg@v1.2 as p. aura.lockфиксирует точную версию и хеш потока токенов модуля; подмена —E0402. Хеш по токенам, а не по байтам, означает, что переформатирование или правка комментария проверку не срабатывают, а любое изменение, способное повлиять на поведение, — срабатывает. Lock, записанный старой версией, содержит байтовыйsha256-; он по-прежнему проверяется и обновляется на первом же запуске без--frozen.--frozen(режим CI) запрещает резолв мимо лока (E0403).- Сеть существует только в
aura add; вычисление всегда оффлайн.
Детерминизм
now()не существует (E0533) — время передаёт хост черезenv.- Порядок ключей вывода = порядок объявления; два прогона побайтно идентичны.
--dry-runвыполняет всё вычисление, но не пишет ни JSON, ни лок, и отчитывается о каждом чтении:[dry-run] read: ./Cargo.toml.
Форматы: TOML, JSON, YAML
Чтение
Три парсера — методы строки; результат — обычные объекты/списки Aura:
cargo = read_file("./Cargo.toml").parse_toml()
team = read_file("./team.json").parse_json()
lint = read_file("./.rules.yaml").parse_yaml()
service:
name: cargo.package.name
owner: team.teams[0].lead
end
Целые числа всех форматов приходят как Int (без потери точности),
ошибки парсинга — E0314 с сообщением исходной библиотеки.
Запись
Из CLI — флагом:
aura eval app.aura --format json # по умолчанию, pretty
aura eval app.aura --format json-flat # плоские ключи: a.b.c = 1
aura eval app.aura --format yaml
aura eval app.aura --format toml # требует объект на верхнем уровне
Изнутри языка — методами. У каждого формата их два, и выбор — про то, кто будет читать результат:
configmap:
# Читает программа: компактная форма, потому что переносы строк всё равно
# были бы экранированы на входе.
"app-config.yaml": settings.to_yaml()
# Читает человек — в `kubectl get cm -o yaml` или в diff: развёрнутая.
"reference.yaml": settings.to_yaml_pretty()
end
to_json / to_yaml / to_toml дают самую компактную форму, которую допускает
формат; to_json_pretty / to_yaml_pretty / to_toml_pretty — форму для
человека. Компактный YAML — это flow-стиль, {a: 1, xs: [1, 2]}, обычный YAML,
а не диалект. Компактный TOML держит массивы в одну строку.
У CLI наоборот: --format всегда пишет развёрнутую форму, потому что производит
документ, а не строку, которая пойдёт внутрь документа.
json-flat склеивает вложенные ключи точкой, и это однозначно лишь пока сама точка
не встречается внутри ключа. Ключи, написанные на Aura, — идентификаторы, поэтому
не встречается; но parse_json и родственные возвращают то, что было во входных
данных, и тогда {"a": {"b": 1}} и {"a.b": 2} оба претендуют на ключ a.b. Это
E0604, а не молчаливая перезапись одного значения другим. Пустой объект
сплющивается в себя (a.b: {}) — он лист, как и пустой список.
Ограничения TOML честно превращаются в ошибки E0603: нет null,
на верхнем уровне обязан быть объект.
Aura как конвертер
Поскольку язык читает и пишет все три формата, миграция — однострочник:
# convert.aura
config: read_file("./legacy.toml").parse_toml()
aura eval convert.aura --allow-read=. --format yaml
В отличие от yq/jq, по пути доступны схемы, assert и merge
нескольких источников — конвертация с гарантиями.
Встраивание и вызов из других языков
Rust: библиотека
[dependencies]
# `default-features = false` отключает фичу `cli`, а с ней clap, ariadne и ureq —
# библиотеке они не нужны. Замер на пустом проекте: 38 пакетов вместо 96. Фича
# включена по умолчанию только чтобы `cargo install aura-lang` давал бинарник
# `aura`.
aura-lang = { version = "0.4", default-features = false }
#![allow(unused)]
fn main() {
use aura_lang::facade::{eval_file, EvalOptions};
let opts = EvalOptions {
strict: true,
allow_read: vec!["config/".into()],
..Default::default()
};
let out = eval_file("config/app.aura".as_ref(), &opts)?;
let cfg: MyConfig = serde_json::from_value(out.json)?; // сразу в свои структуры
for w in &out.warnings {
log::warn!("{w}"); // Display: warning[W..]: ... at file:line:col
}
if let Some(lock) = out.updated_lockfile {
std::fs::write("config/aura.lock", lock)?; // писать или нет — решает хост
}
}
Права задаёт хост-приложение — конфиг не получает больше, чем вы разрешили.
Диагностики приходят структурированными (code, severity, file, line,
column, help) — рендерите в свой лог как удобно.
В async-контексте заворачивайте eval_file в spawn_blocking: он читает файлы и
вычисляет синхронно.
Чтение всего манифеста в свои типы
Когда манифест и есть конфигурация, читайте его целиком, сразу в свои
структуры - без JSON посередине и с тем же результатом, что печатает aura eval:
#![allow(unused)]
fn main() {
#[derive(serde::Deserialize)]
struct Device { board: Board, sensors: Vec<Sensor>, /* ... */ }
let device: Device = Script::builder()
.file("device.aura")
.allow_read("config") // файлы, которые он читает, рядом с ним
.env_values([("APP_ENV", "production")]) // ровно эти, и никакие другие
.load()?
.output()?;
}
env_values разрешает названные переменные с заданными значениями; env() берёт
их оттуда и никогда - из процесса, поэтому окружение самого сервиса не утечёт в
манифест, а тест может дать ему любое окружение, не трогая окружение процесса.
aura types --lang rust генерирует структуры для схем, объявленных в манифесте.
Каждый пример в репозитории читается так в тесте и сравнивается с выводом, на
котором зафиксирован CLI; cargo run --example read_device (а также
read_k8s_deploy, read_service_catalog) показывает это целиком.
Вызов скрипта: Script
eval_file возвращает JSON, а JSON не умеет хранить функции. Но pub def в Aura —
это значение, поэтому правила могут лежать в .aura-файле, а приложение —
вызывать их на каждый запрос, событие или порцию, меняя поведение правкой файла,
а не пересборкой. Для этого есть тип Script (D33).
#![allow(unused)]
fn main() {
use aura_lang::{Budget, Script};
#[derive(serde::Serialize)]
struct Event { device: String, temp_c: f64 }
#[derive(serde::Deserialize)]
struct Summary { total: u64, alerts: u64 }
let rules = Script::builder()
.file("telemetry.aura")
.budget(Budget::per_call().steps(5_000_000).memory_mb(32))
.load()?; // разбор, анализ, доказательство герметичности — один раз
let summarize = rules.function::<(&[Event],), Summary>("summarize")?; // имя, тип, арность — проверены сейчас
for chunk in events.chunks(10_000) {
let s = summarize.call((chunk,))?; // один вызов, один бюджет, ничего не остаётся в памяти
}
}
Что гарантируется — в том порядке, в каком решение ставит приоритеты:
- Герметичность по умолчанию. Без выданных прав
loadстатически доказывает, что скрипт не читает ни файлов, ни переменных, ни часов, и падает сE0505, если мог бы, — в том числе на ветке, которая никогда не выполнится.allow_envиallow_readвыдают ровно то же, что--allow-envи--allow-read, и только корневому модулю. - У каждого вызова бюджет — в шагах и байтах, но не во времени: иначе
результат зависел бы от машины. Убежавший вызов заканчивается с
E0325илиE0326, а не вешает хост. Значение по умолчанию конечно. - Память на вызов постоянна. Каждый вызов идёт в своём интерпретаторе и освобождает созданное, когда возвращается.
- Граница проверяется. Аргументы проходят через
serde; целое за пределамиi64или NaN — этоE0005, а не молча другой вход, а результат, который не подходит под ваш тип, —E0006с путём внутри значения. Script—Send + Sync. Один экземпляр обслуживает все потоки.
Замерено на одном ядре: вызов стоит около 1,2 мкс, а правило над событиями
обрабатывает около 0,9 млн событий в секунду при вызове на каждое событие и
1,3 млн — порциями (cargo bench --bench script). cargo run --example embedding
запускает пример выше целиком.
Перезагрузка правил без перезапуска сервиса
Правила меняются чаще, чем сервис, который их исполняет. Скрипт, загруженный из файла, можно загрузить заново без перезапуска (D36):
#![allow(unused)]
fn main() {
use std::time::Duration;
let rules = Script::builder()
.file("routing.aura")
.watch(Duration::from_secs(2), |result| match result {
Ok(v) => log::info!("правила: поколение {} ({})", v.generation, v.hash),
Err(e) => log::error!("правила не перезагружены, работают старые: {e}"),
})
.load()?;
rules.reload()?; // или перезагрузить сейчас, с watch или без него
}
- Перезагрузка - это загрузка. Она проходит все проверки первой загрузки с
теми же разрешениями: скрипт не станет менее герметичным, не дотянется до
функции хоста, которую вы не дали, и не подтянет пакет, не закреплённый в
aura.lock, - lock читается один раз, а перезагрузка разрешает импорты как--frozen. - Экспорт - это контракт. Каждый экспорт сохраняет вид и число аргументов,
иначе перезагрузка -
E0011, и продолжает работать старая версия. Новые экспорты разрешены. ПривязаннаяFunctionследует за своим именем. - Никогда не половина версии. Вызов заканчивается на той версии, на которой начался; старая версия освобождается, когда вернулся последний такой вызов.
- Решает содержимое.
touchничего не загружает. При слежении изменение загружается, только когда файлы не трогали в течение периода тишины - двух интервалов и не меньше секунды, или.settle(d), - поэтому недописанный файл не загрузится наполовину. Писатель, застрявший посреди записи дольше этого, всё же может быть пойман: выкладывайте новый файл и переименовывайте его поверх старого, как делают редакторы, а для медленного сетевого диска увеличьте.settle. - Без тихих отказов. Обработчик, который требует
watch, узнаёт о каждой автоматической перезагрузке - новая версия или ошибка, - а об одном и том же сломанном содержимом один раз. version()называет поколение и хэш всех файлов - для ваших логов.
cargo run --example checkout собирает всё вместе: правила риска интернет-магазина,
которым сервис даёт таблицу GeoIP, курсы валют и базу споров, перезагрузка по
правке - и отказ в правке, которая переименовывает то, что вызывает сервис, или
тянется к секрету, которого ей не давали.
Продвинутое: скрипт вызывает хост
Некоторым правилам нужны данные, которые есть только у приложения: в каком регионе адрес, включена ли функция для клиента. Вместо того чтобы вычислять все такие значения заранее, хост может дать скрипту именованные функции (D35). Это продвинутое использование: прочитайте гарантии ниже, прежде чем на него опираться.
import "host:geo" as geo
pub def route(ip)
region = geo.region(ip)
backend: region == null ? "global" : "#{region}-cluster"
end
#![allow(unused)]
fn main() {
let rules = Script::builder()
.file("routing.aura")
.pure("geo.region", move |ip: String| geoip.get(&ip).cloned()) // Option: None - это null
.effect("flags.enabled", move |flag: String, customer: String| {
flags.enabled(&flag, &customer) // Result: Err - это E0008
})
.load()?;
}
pure и effect - методы Rust API, а не слова языка. Часть имени до точки -
модуль, который импортирует скрипт.
pure- ваше обещание, и язык не может его проверить. Чистая функция должна давать один ответ на одни аргументы: таблица, поставляемая с бинарником, формула. Загрузка остаётся герметичной. Всё, что читает внешний мир - база, сервис флагов, часы, - этоeffect: его регистрация - разрешение, какallow_env, и загрузка перестаёт быть герметичной.- Только корневой модуль. Пакет, который импортирует скрипт, не может
вызывать функции хоста;
grant_to_imports("geo.region")открывает импортам одну функцию. - Проверка при загрузке. Непредоставленный модуль или функция -
E0007, вызов с неверным числом аргументов -E0009, в том числе в ветках, которые не выполнятся. Типы аргументов проверяются при вызове, тоже какE0009. - Бюджет. Вызов хоста стоит 100 шагов (
host_call_stepsменяет это). Время внутри вашей функции бюджету не видно: ограничивайте его сами. - Без обратного вызова. Функция хоста, вызвавшая скрипт, получает
E0010.
У aura eval хоста нет, поэтому импорт host: там останавливается с E0007;
aura check перечисляет, что нужно скрипту от приложения, которое его запустит.
cargo run --example host_functions запускает полный пример.
Любой язык: subprocess
CLI-контракт стабилен и является API: JSON в stdout, диагностики с кодами
E0xxx/W0xxx в stderr, exit-коды 0 (успех) / 1 (диагностики) /
2 (I/O, аргументы).
import json, subprocess
r = subprocess.run(["aura", "eval", "app.aura", "--frozen"],
capture_output=True, text=True)
if r.returncode != 0:
raise RuntimeError(r.stderr)
config = json.loads(r.stdout)
Рекомендации для прода:
--frozen— зависимости строго поaura.lock;- права минимальные и явные (
--allow-read,--allow-env=ИМЕНА); --format yaml|toml, если потребителю удобнее.
Мобильные и браузерные приложения
Правильный паттерн — вычисление на сервере/CI:
configs.aura ──aura eval──▶ config.json ──▶ CDN / бандл
устройство читает готовый JSON штатным парсером платформы
Приложения — потребители результата Aura; валидация уже прошла в CI.
Почему обёртки, а не нативные реализации
Для YAML в каждом языке есть свой парсер, и возникает вопрос, почему у Aura не
так. Потому что YAML — формат данных: его порт это парсер, байты → данные.
Aura — вычисляемый язык (функции, импорты, схемы, capability, детерминизм): её
порт это интерпретатор, который обязан побайтово совпадать с этим. Иначе
один и тот же манифест даст разные значения в CI и в сервисе — то есть в
проде. Расхождение возникает не на экзотике: наивная реализация на
encoding/json в Go выдаёт 1 там, где Aura выдаёт 1.0, и
1000000000000000000 там, где 1e+18.
Поэтому реализация одна, а языки получают её через:
- Вычисление на этапе сборки — основной путь. Рантайм Aura внутри вашего
языка не нужен вовсе, а
aura typesвдобавок сгенерирует типы под ваш JSON. - Обёртки вокруг того же ядра — в роадмапе, по приоритету: WASM/npm
(закрывает Node, браузер и playground одним артефактом), затем
wazeroдля Go (чистый Go, без cgo) и wasmtime для Python; PyO3/UniFFI — по мере спроса.
Ядро уже собирается под wasm32-unknown-unknown; это проверяется в CI на
каждом коммите.
CLI
aura eval <file.aura> [--strict] [--dry-run] [--frozen]
[--allow-read=<dir>]... [--allow-env[=A,B]] [--allow-imports-io]
[--hermetic] [--max-steps=N] [--max-memory=MB] [--stats]
[--format json|json-flat|yaml|toml] [-o out] [--registry-dir=<dir>]
aura check <file.aura> [--strict] [--hermetic]
aura docs --agent [-o <file>]
aura fmt <files...> [--check]
aura types <file.aura> --lang rust|ts|go [--out <file>]
aura add <path>@vX.Y.Z [--from <file>] [--registry-dir=<dir>]
aura --version
eval
Вычисляет манифест со всеми импортами и печатает результат.
| Флаг | Действие |
|---|---|
--strict | предупреждения анализа (всех модулей) блокируют; лишние поля схем — ошибка E0513 |
--dry-run | полное вычисление, но ни вывод, ни aura.lock не записываются; отчёт [dry-run] read/would write |
--frozen | зависимости строго по aura.lock (E0403 при расхождении), лок не переписывается — режим CI |
--allow-read=<dir> | право на read_file() внутри каталога (повторяемый). Относительный путь в read_file() читается от папки самого манифеста, поэтому вывод не зависит от того, откуда запущен aura (D37) |
--allow-env[=A,B] | право на env(): список имён либо все |
--hermetic | никакого I/O: env() и read_file() — это E0505 где угодно. Несовместим с флагами --allow-* |
--allow-imports-io | выдать импортам права корня |
--max-steps=N | сколько выражений вычисление может выполнить, прежде чем остановится с E0325. По умолчанию десять миллионов |
--max-memory=MB | сколько мебибайт значений оно может создать, прежде чем остановится с E0326. По умолчанию 256 |
--stats | напечатать в stderr, сколько шагов и байтов израсходовано |
--format | json (по умолчанию) / json-flat / yaml / toml |
-o, --output <file> | записать в файл вместо stdout |
--registry-dir=<dir> | каталог кэша пакетов (по умолчанию ~/.aura/registry) |
check
Только lex + parse + статический анализ — быстрый гейт для pre-commit и CI.
--hermetic дополнительно требует, чтобы манифест не выполнял ввода-вывода:
env() и read_file() где угодно в нём или в том, что он импортирует, - это
E0505. Решается статически, поэтому check отвечает без вычисления - в том
числе для веток, которые этот запуск не прошёл бы:
$ aura check --hermetic deploy.aura
[E0505] Error: env() is not allowed in hermetic mode
╭─[ deploy.aura:4:11 ]
Он нужен, когда вывод манифеста не должен зависеть ни от чего, кроме его текста:
гейт в CI на каталог манифестов или сборка, которую хочется воспроизвести на
другой машине. eval --hermetic требует того же и подходит, когда манифесту нет
доверия.
Скрипт, написанный для приложения с функциями хоста (D35), импортирует их как
import "host:geo" as geo. У CLI хоста нет, поэтому eval останавливается с
E0007, а check перечисляет, что скрипту нужно от приложения, которое его
запустит, и завершается с кодом 0:
$ aura check routing.aura
note: provided by the application at run time, this script calls:
geo.region (1 argument)
flags.enabled (2 arguments)
ok: routing.aura
docs
aura docs --agent печатает полный справочник по языку — синтаксис, стандартную
библиотеку, все коды диагностик, — собранный из определений самого компилятора.
Это около четырёх тысяч токенов: достаточно мало, чтобы отдать ИИ-ассистенту
целиком.
Смысл печатать его из бинарника, а не давать ссылку на страницу, — в версиях:
вывод описывает тот aura, который его произвёл, и не может разойтись с ним, как
это бывает с размещённым в сети документом.
Полезнее всего дать ассистенту забрать справку самому. Одна строка в вашем
AGENTS.md, CLAUDE.md или аналоге:
Before writing or editing .aura files, run `aura docs --agent` for the complete
language reference.
Либо закоммитить — для ассистентов, которые читают файлы, но не запускают команды:
$ aura docs --agent -o AURA.md
Тот же текст публикуется по адресу /llms.txt для инструментов, которые ходят по сети.
fmt
Канонический форматтер: отступы (2 пробела/уровень по токен-глубине), внутристрочные
пробелы, колоночное выравнивание прогонов name = value, key: value и арок cond
вместе с хвостовыми комментариями. Строки и содержимое block strings не трогаются;
поток токенов гарантированно не меняется, форматирование идемпотентно.
--check — только проверить (exit 1 при отличиях).
types
Генерация типов host-языка из объявлений type и enum:
aura types config.aura --lang ts
Шесть встроенных типов полей отображаются так:
| Aura | Rust | TypeScript | Go |
|---|---|---|---|
String | String | string | string |
Int | i64 | number | int64 |
Float | f64 | number | float64 |
Bool | bool | boolean | bool |
List | Vec<serde_json::Value> | unknown[] | []any |
Object | serde_json::Map<String, serde_json::Value> | Record<string, unknown> | map[string]any |
String — единственное имя, которое язык принимает для текстового поля; Str не
существует.
Обратите внимание на две последние строки. List и Object в схеме не несут типа
элемента, поэтому и сгенерированный код не может: получается последовательность
или отображение нетипизированных значений. Где форма важна, объявите type для
элемента и держите в поле его.
Одна схема и валидирует конфиг, и типизирует сервис, который читает JSON. Enum
превращается в юнион строковых литералов (TS), enum с #[serde(rename)] (Rust)
или тип-строку с типизированными константами (Go). Опциональные поля (D15) —
обязательные в выводе: вычисление всегда вставляет дефолт. Только парсинг —
манифест не вычисляется, capability не задействованы. --out пишет в файл.
Вывод сразу канонический для форматтера host-языка: rustfmt --check, gofmt -l
и prettier --check на сгенерированных файлах молчат. Сгенерированное можно
коммитить в репозиторий сервиса без форматного шума в диффах.
add
Установка пакета: скачивание (github/<owner>/<repo> → package.aura тега
vX.Y.Z; точная версия обязательна), валидация, запись в кэш, фиксация
в ./aura.lock с хешем целостности. --from <file> — установка из локального файла.
Единственная сетевая команда.
Exit-коды
| Код | Смысл |
|---|---|
| 0 | успех |
| 1 | диагностики (ошибки языка, провал assert, блокирующий strict) |
| 2 | ошибки I/O и аргументов |
Ключевые слова
В Aura шестнадцать зарезервированных слов. Ни одним из них нельзя назвать
привязку, функцию, схему или ключ без кавычек: type = 1 - синтаксическая
ошибка. Ключ, который должен быть одним из этих слов, пишется строкой и так же
читается (D11): "type": "service", затем o."type".
| Слово | Что делает | Пример | Решение |
|---|---|---|---|
import | Подключает другой модуль, пакет из реестра с закреплённой версией или модуль, который даёт хост | import "./lib.aura" as lib | D8, D12, D35 |
as | Даёт имя тому, что подключил import; только после импорта | import org/net@v1.2 as net | D12 |
type | Объявляет схему: поля с типами, умолчаниями и инвариантами. Поле с типом-схемой хранит только то, что принял бы её new | type Port … end | D15, D26, D27, D28, D41 |
enum | Объявляет закрытый набор строк для поля схемы | enum Tier … end | D18 |
def | Объявляет именованную функцию. Возвращает объект из своих свойств или последнее выражение, если тело им кончается | def labels(team) … end | D17, D40 |
end | Закрывает любой блок: объекты, def, type, enum, domain, cond, new, многострочную лямбду и строку text | tls: enabled: true end | D3, D38 |
domain | Именованный раздел со своей областью видимости: метка становится ключом, а в теле можно вычислять | domain "production" … end | D17 |
new | Создаёт экземпляр схемы и проверяет его | new Port number: 443 end | D4, D38 |
assert | Проверка, которая останавливает вычисление с сообщением; внутри type - инвариант, который переезжает вместе со схемой | assert replicas > 0, "need one" | D5, D28 |
shadow | Заново привязывает имя из внешней области - без него это E0302 | shadow port = 9090 | D7 |
pub | Делает def, type или enum доступным для импорта; свойства видны всегда | pub def service(name) | D12 |
cond | Выбор из нескольких значений, побеждает первая истинная ветка | cond … else -> "dev" … end | D14 |
else | Последняя ветка cond, обязательна, чтобы у каждого пути было значение | else -> "dev" | D14 |
true | Логическая истина. «Истинности» нет: условие, которое не Bool, - это E0306 | tls: true | - |
false | Логическая ложь | debug: false | - |
null | Отсутствие значения. Поле схемы принимает его, только если тип помечен ?, и оно никогда не выводится текстом | quota: Int? = null | D27, D31 |
Не зарезервировано
| Слово | Что делает | Пример | Решение |
|---|---|---|---|
text | Открывает блочную строку, но только там, где ожидается значение, - key: text или x = text в конце строки. В остальных местах это обычное имя, так что поле с именем text продолжает работать | body: text … end | D16 |
Имена типов - String, Int, Float, Bool, List, Object - тоже не
ключевые слова: это имена, которые знает проверка схем. Решения из последнего
столбца - в журнале решений спецификации.
Встроенные методы
Методы вызываются через точку и составляются в цепочки. Каждая сигнатура ниже сверена с реализацией, а не переписана по памяти.
String
| Метод | Результат | Примечания |
|---|---|---|
upper() / lower() | String | "auth".upper() → "AUTH" |
len() | Int | считает символы: "héllo".len() → 5 |
trim() | String | убирает пробельные символы по краям |
split(sep) | List | "a,b".split(",") → ["a", "b"] |
replace(from, to) | String | все вхождения |
starts_with(prefix) / ends_with(suffix) | Bool | |
contains(substr) | Bool | "hello".contains("ell") → true |
to_int() / to_float() | Int / Float | некорректное число — ошибка, а не null |
slice(start, end) | String | подстрока по индексу символа, end не включается, индексы зажимаются |
to_str() | String | тождество, чтобы не разрывать цепочку |
parse_json() / parse_yaml() / parse_toml() | Object | целые становятся Int; сбой — E0314 |
| ↳ точность | порядок ключей — как в документе; YAML << применяется; дата TOML становится String | |
parse_duration() | Int (секунды) | "1h30m" → 5400; единицы d/h/m/s; E0319 |
parse_datetime() | Int (epoch UTC) | RFC 3339 или YYYY-MM-DD; смещения ±HH:MM; E0320 |
sha256() | String | hex-дайджест в нижнем регистре |
base64() / base64_decode() | String | стандартный алфавит с дополнением; неверный вход — E0321 |
Int
| Метод | Результат | Примечания |
|---|---|---|
abs() | Int | |
to_float() | Float | расширение; Float.to_int() нет — ему пришлось бы округлять |
to_str() | String | |
format_duration() | String | 5400 → "1h30m"; 0 → "0s" |
format_datetime() | String | RFC 3339 UTC: 946684800 → "2000-01-01T00:00:00Z" |
Float
| Метод | Результат |
|---|---|
abs() | Float |
round(digits) | Float |
to_str() | String |
Bool
| Метод | Результат |
|---|---|
to_str() | String |
List
| Метод | Результат | Примечания |
|---|---|---|
len() | Int | |
first() / last() | элемент | пустой список — E0317 |
get(i) / get(i, default) | элемент | промах даёт null или указанный default |
contains(item) | Bool | структурное равенство |
compact() | List | убирает null |
uniq() | List | дедупликация, порядок первых вхождений |
map (x, i) -> ... end | List | колбэк получает элемент и его индекс |
filter (x, i) -> Bool end | List | не-Bool из колбэка — E0306 |
join(sep) | String | только скаляры; контейнеры — E0307 |
sort() | List | |
reverse() | List | |
sum() | Int | |
min() / max() | элемент | |
flatten() | List | на один уровень: [[1], [2, 3]] → [1, 2, 3] |
to_object() | Object | собирает объект из пар { key, value }; повтор ключа — E0323 |
reduce(init) (acc, e) -> … end | Any | левая свёртка; начальное значение обязательно |
any (e, i) -> Bool end | Bool | хотя бы один подходит; для пустого списка — false |
all (e, i) -> Bool end | Bool | подходят все; для пустого списка — true |
find(default) (e, i) -> Bool end | Any | первое совпадение или default |
index_of(value, default) | Int | индекс первого равного элемента или default; default обязан быть Int |
group_by (e, i) -> key end | Object | элементы по ключу-строке, ключи в порядке первого появления (D39) |
sort_by (e, i) -> key end | List | по возрастанию и устойчиво; ключи как у sort() (D39) |
flat_map (e, i) -> [...] end | List | map, затем списки склеиваются на один уровень; не-список из лямбды - E0306 (D39) |
zip(other) | List | пары { left, right }; списки разной длины - E0327 (D39) |
slice(start, end) | List | end не включается: [1, 2, 3].slice(1, 3) → [2, 3] |
to_json() / to_yaml() / to_toml() | String | компактная форма, для встраивания; to_toml на списке — E0603 |
to_json_pretty() / to_yaml_pretty() / to_toml_pretty() | String | форма для человека, для файла, который попадает в diff |
Object
| Метод | Результат | Примечания |
|---|---|---|
len() | Int | |
keys() / values() | List | порядок объявления |
contains(key) | Bool | |
get(key, default) | значение | промах даёт default |
entries() | List | объекты { key, value } в порядке объявления; обратное к to_object() |
merge(other) | Object | ключи правой стороны побеждают |
to_json() / to_yaml() / to_toml() | String | компактная форма, для встраивания |
to_json_pretty() / to_yaml_pretty() / to_toml_pretty() | String | форма для человека, для файла, который попадает в diff |
Операторы, которые строят значения
Почти все операторы числовые. Два — нет, и только у них результат может быть больше того, что пришло на вход.
| Выражение | Результат | Примечания |
|---|---|---|
"a" + "b" | String | склеивает две строки; интерполяция "#{a}#{b}" делает то же самое |
[1] + [2, 3] | List | склеивает два списка; операнды не меняются |
+ никогда не преобразует. "port " + 8080 — это E0306, и диагностика
называет сторону, на которой нужно вызвать .to_str(). Склейка ограничена
1 000 000 элементов списка и 16 МиБ строки, сообщается как E0322.
Глобальные функции
| Функция | Требуемое право | Поведение |
|---|---|---|
range(n) | — | [0, 1, … n-1] |
env(name, default) | --allow-env | переменная окружения; отсутствует → default |
read_file(path) | --allow-read | содержимое файла строкой; относительный путь - от папки манифеста (D37) |
fail(message) | — | останавливает выполнение с E0531 и вашим сообщением |
Вызов метода имеет приоритет над одноимённым полем объекта, в котором лежит
функция. Экспортированные функции пакета вызываются тем же синтаксисом:
pkg.fn(...).
Коды ошибок и предупреждений
Каждая диагностика Aura имеет стабильный код — по нему удобно искать в CI-логах
и ссылаться в обсуждениях. E — ошибка, W — предупреждение (в --strict
предупреждения блокируют).
Хост (E00xx)
Возникают на границе между манифестом и приложением, которое его встраивает.
У E0001-E0006 нет позиции в исходнике, потому что разбор ещё не начинался;
E0007-E0009 указывают на вызов в скрипте (D35).
| Код | Значение | Как исправить |
|---|---|---|
| E0001 | хост не смог открыть файл либо aura.lock нечитаем | в сообщении — исходная ошибка ввода-вывода |
| E0002 | скрипт ничего не экспортирует под этим именем | проверьте имя; экспортом являются только свойства : и pub def |
| E0003 | экспорт — не функция | получите его через get, а не function или call |
| E0004 | у функции другое число аргументов | проверяется при привязке функции, до любого вызова |
| E0005 | у аргумента нет точного значения в Aura | целое за пределами i64, NaN или бесконечность, байтовая строка; отвергается, а не приводится |
| E0006 | результат не подходит под тип хоста | сообщение называет путь внутри значения, где не совпало |
| E0007 | скрипт импортирует или вызывает функцию хоста, которую хост не предоставил | модуль host: предоставляет приложение во время выполнения, поэтому aura eval останавливается здесь; aura check перечисляет, что нужно скрипту |
| E0008 | функция хоста вернула ошибку | сообщение - от приложения; скрипт не может её обработать |
| E0009 | функция хоста вызвана с неверным числом аргументов (при загрузке) или типами (при вызове) либо вернула значение, которого нет в Aura | сообщение называет функцию |
| E0010 | скрипт вызван изнутри одной из его же функций хоста | запрещено (D35): верните значение скрипту |
| E0011 | перезагрузка изменила бы экспорт скрипта: экспорт удалён, либо изменился его вид или число аргументов | хост мог его привязать, поэтому продолжает работать старая версия (D36); чтобы изменить экспорт, перезапустите приложение |
Лексика (E01xx)
| Код | Смысл | Как чинить |
|---|---|---|
| E0101 | после десятичной точки нет цифры (12.) | 12.0 либо 12 |
| E0102 | незакрытая строка до конца строки | строки в Aura однострочные; закройте " |
| E0103 | целочисленный литерал не помещается в i64 | |
| E0104 | registry-импорт без версии | import a/b@v1 as x — версия обязательна (D8) |
| E0105 | неожиданный символ | |
| E0106 | незакрытая интерполяция #{ | |
| E0107 | незакрытая блочная строка | закрыть end на отступе открывающей строки (D16) |
| E0108 | разделитель разрядов стоит неверно | _ группирует по три от точки: 10_000_000, 0.000_001 — но не 100_, 1__0 и не 2_3_7_1_9_3_3 |
Синтаксис (E02xx)
| Код | Смысл | Как чинить |
|---|---|---|
| E0200 | ожидался конкретный токен | текст ошибки называет какой |
| E0201 | инлайн-блок из v1.1 (metrics port: 9090) | объектный блок: metrics: … end (D3) |
| E0203 | не хватает end / ] | |
| E0204 | ожидалось выражение | |
| E0205 | statement’ы разделяются переносами строк | |
| E0206 | pub не перед def/type/enum | свойства экспортируются сами; = всегда приватен |
| E0207 | у cond нет ветки else | добавить else -> …: каждая ветвь обязана дать значение (D14) |
| E0208 | слишком глубокая вложенность | предел рекурсии, из-за которого глубоко вложенный ввод — ошибка, а не переполнение стека; цепочка вида a + b + c засчитывает свою длину (D42) |
| E0211 | член enum — не строка в кавычках | члены перечисляются строками, по одной на строку (D18) |
| E0212 | дубликат члена enum | каждый член указывается один раз |
| E0213 | enum без членов | пустой набор нельзя удовлетворить |
| E0214 | значение вычислено и выброшено | голое выражение не в конце функции никуда не идёт; напишите key:, name = или assert (D40) |
| E0215 | в теле есть свойства, и оно же кончается значением | функция возвращает одно или другое; сделайте значение свойством или уберите свойства (D40) |
Выполнение (E03xx)
| Код | Смысл | Как чинить |
|---|---|---|
| E0301 | повторное присваивание — значения иммутабельны | новое имя либо вложенный scope |
| E0302 | затенение внешней переменной без маркера | shadow x = ... (D7) |
| W0303 | shadow ничего не затеняет | уберите shadow |
| E0304 | переполнение целочисленной арифметики | Float либо пересмотрите величины |
| E0305 | деление на ноль | |
| E0306 | неподходящий тип операнда | условия — строго Bool; «правдивости» нет |
| E0307 | в интерполяции/join не скаляр | сериализуйте явно: .to_json() |
| E0308 | неизвестное поле объекта | опечатка? .get(key, default) для опциональных |
| E0309 | неизвестный метод | список — в справочнике методов; поля-функции пакетов вызываются так же |
| E0310 | нет прав (capability) | подсказка в ошибке называет нужный флаг (D1) |
| E0311 | путь вне разрешённых --allow-read каталогов | |
| E0312 | функции передано меньше аргументов, чем параметров | |
| E0313 | ошибка чтения файла (I/O) | |
| E0314 | некорректный TOML/JSON/YAML при парсинге | |
| E0315 | методу (map/filter) не передана лямбда | |
| E0316 | некорректное выражение внутри #{...} | |
| E0317 | индекс вне границ списка / first()/last() на пустом | .get(i, default) |
| E0318 | obj["key"] — скобки на объектах запрещены | точечная форма: obj."key" (D11) |
| E0319 | некорректная длительность | формат "1h30m", единицы d/h/m/s |
| E0320 | некорректная дата | RFC3339: "2026-07-18T12:00:00Z" |
| E0321 | некорректный base64 | base64_decode() на входе, который не является base64 |
| E0322 | склеенный список или строка превышает предел размера | + удваивает: потолок 1M элементов / 16 MiB |
| E0323 | to_object() получил один и тот же ключ дважды | молчаливая перезапись зависела бы от порядка элементов; уберите дубли заранее |
| E0324 | null выводится как текст | интерполяция и to_str() обязаны совпадать; скажите, что печатать в пустом случае (D31) |
| E0325 | вычисление исчерпало бюджет шагов | --max-steps его увеличивает, --stats показывает расход (D34); в Script бюджет — на вызов (D33) |
| E0326 | вычисление исчерпало бюджет памяти | --max-memory его увеличивает; считается каждое созданное значение и кадр (D34) |
| E0327 | zip() получил списки разной длины | молча отброшенный хвост унёс бы часть конфигурации; сначала обрежьте длинный через slice (D39) |
| E0328 | значение вложено глубже 256 списков и объектов | почти всегда цикл, который на каждом шаге заворачивает результат; стройте уровни рядом, а не друг в друге (D43) |
| E0398 | внутреннее: окружение замыкания было освобождено | ошибка в Aura — пришлите манифест в issue |
| E0399 | вычисление вложено глубже 256 уровней | почти всегда рекурсивная функция без условия остановки; map, filter и reduce обходят список без вложенности (D42) |
| E0533 | now()/timestamp() не существуют | время передаёт хост: env("BUILD_TIME", ...) (D13) |
Модули и пакеты (E04xx)
| Код | Смысл | Как чинить |
|---|---|---|
| E0401 | циклический импорт (цепочка в сообщении) | разорвите цикл: общий код — в третий модуль |
| E0402 | integrity-хэш пакета не совпал с aura.lock | содержимое пакета изменилось — разберитесь почему |
| E0403 | --frozen: записи нет в aura.lock | aura add локально, лок — в коммит |
| E0404 | модуль не найден / не резолвится | путь, версия, --registry-dir |
| E0410 | импорт передан интерпретатору без загрузчика модулей | встречается только при прямом встраивании вычислителя; используйте facade::eval_file/eval_source, которые грузят модули |
Анализ и валидация (E05xx / W05xx)
| Код | Смысл | Как чинить |
|---|---|---|
| W0501 | неиспользуемая переменная | мёртвый код — удалите |
| W0502 | неиспользуемый импорт | |
| W0503 | неиспользуемая функция/тип | для API пакета — пометьте pub |
| E0504 | необъявленная переменная | объявление должно предшествовать использованию |
| E0505 | env() или read_file() при --hermetic | передать значение снаружи либо снять флаг и выдать право |
| E0511 | отсутствует поле, требуемое схемой | |
| E0512 | тип поля не совпал со схемой | Int и Float — разные типы |
| E0513 | лишнее поле (только --strict) | уберите поле либо добавьте в схему |
| E0514 | значение не входит в enum | подсказка предлагает ближайший член и перечисляет набор (D18) |
| E0515 | инвариант схемы не выполняется для этого экземпляра | собственный assert схемы; показано его же сообщение |
| E0516 | значение по умолчанию, которое поле принять не может | проверяется там, где схема объявлена, а не там, где используется |
| W0512 | эффектный вызов в импортированном модуле | у импортов нет I/O-прав; пересмотрите дизайн пакета |
| W0513 | нулевое поле заполняется из get(key) или env(name) без значения по умолчанию | отсутствие тихо станет null; задайте значение по умолчанию или уберите ? |
| E0530 | провал assert | сообщение — ваше собственное |
| E0531 | вызван fail(...) |
Сериализация (E06xx)
| Код | Смысл | Как чинить |
|---|---|---|
| E0601 | функция/схема внутри выводимого дерева | путь ключа — в сообщении; на верхнем уровне pub-элементы исключаются сами |
| E0602 | не-конечный Float (NaN/Inf) | |
| E0603 | ограничение формата (TOML: нет null, корень — объект) | |
| E0604 | два ключа сплющиваются в один ключ json-flat | ключ с точкой неотличим от вложенного пути; взять --format json либо переименовать ключ |