Acore language

[ AxiomCore · Documentation ]

Syntax

Lexical rules, keywords, properties, declarations and authoring shortcuts for Acore source files.

This page lists the rules for writing an Acore source file: the three source shapes, comments, identifiers, keywords, properties, declarations and the shorthand forms that the compiler expands for you. Examples were checked with Axiom CLI 0.149.1.

Source shapes

The nearest AxiomDeps.toml decides which grammar a file uses.

ShapeSelected byTypical sourceSeparators
General moduleNo type in the manifestname = value, name { ... }Newlines or spaces. No commas between object members. Commas are optional in lists.
Backend declarationstype = "backend"project(id: "x", version: "1"), model Task { title: String }Named arguments separated by commas. Commas are required between list items and arguments; a trailing comma is allowed.
Acore UItype = "frontend"app, page, component, view { ... }See Acore UI. ; is allowed between model fields.

Database schemas use the offline database compiler; see Database contracts. Package projects are described in Packages. General modules and backend declarations reject ; (Invalid character in source or AXB001).

Comments

FormGeneral moduleBackend declarationsAcore UI
// line commentYesYesYes
/* block comment */No (Unexpected token)No (AXB001)Yes, and blocks can nest
Acore excerpt
// Contract policy shared by all endpoints.
policies(cache: CachePolicy(strategy: CacheStrategy.networkFirst, ttlSeconds: 60))

Module declaration

A dotted module name gives a file a stable source identity:

Acore excerpt
module commerce.orders

General modules and backend sources may omit it. An Acore UI file must declare module <name>.ui: a missing declaration is AUI001 and a name without the .ui suffix is AUI002.

Identifiers

Identifiers start with an ASCII letter or underscore and continue with letters, digits or underscores. Names are case-sensitive. A dot is member access, except inside a module name. In general modules, a property whose name starts with _ is evaluated but left out of rendered output.

Keywords

The general grammar reserves these words and symbols.

KeywordUse
null, true, falseLiterals
moduleModule name
import, amends, useLoad source, compose source, consume a compiled contract or export
readRead an environment value or external property
class, extends, typealias, functionDeclare a class, its parent, a type alias, a function signature
local, hidden, fixed, const, abstract, open, externalModifiers
this, outer, superScope references
if, else, let, throwExpressions
extendPromote an extracted model: extend Models.Name { ... }
AnyThe top type
is, as, trace, import*, read*Reserved. The 0.149.1 parser rejects them with Unexpected token.

Backend words such as project, model, endpoint, action and managed are declarations, not reserved words, so ordinary properties such as audience = "mobile" remain valid. Run axiom explain --schema for the full declaration catalog, or see Backend declarations.

Properties in general modules

A property can have a value, a type, or both. A name followed by a block amends or creates an object.

config.acore
name = "orders"
port: Int = 8080
optionalLabel: String? = null
headers {
  contentType = "application/json"
  ["x-request-id"] = "example"
}
codes = [502, 503, 504]
names = ["tasks" "users"]
  • name: Type with no value declares an abstract property.
  • Bracketed keys such as ["x-request-id"] are evaluated, so any expression that produces a string works: { [headerName] = "1" }.
  • An object that contains only bare values (xs { 1 2 3 }) renders as a list.
  • There is no new keyword. Write RetryPolicy { maxAttempts = 4 } to create a typed object.

Strings and numbers

FormGeneral moduleBackend and UI
"text"Yes. A backslash escape such as \" or \n is accepted, but the general evaluator keeps it verbatim in the value.Yes
"""multi-line"""YesNo
#"raw text"#YesNo
$"Task {id}"NoYes, in managed and UI expressions. See Authoring shortcuts.
42, -7Signed 64-bit integerTyped by the declaration (Int32, Int64)
0.25, -0.564-bit float. Digits are required on both sides of the point; there is no exponent form.Typed by the declaration
250ms, 2s, 5m, 1h, 1dNo duration literalDuration. Write the unsigned whole number and unit with no space.

Annotations and modifiers

Annotations come before a member and may carry an object body. Modifiers come before properties, classes, type aliases, functions or modules.

Acore excerpt
@deprecated { reason = "Use currentEndpoint" }
hidden oldEndpoint = "legacy"

open class BasePolicy {
  enabled: Boolean = true
}
ModifierEffect in 0.149.1
hidden, localThe value can be referenced but is left out of rendered output. A hidden class is also left out.
open, abstract, fixed, constParsed and recorded. Their effect depends on the compiler profile.
externalDeclares a member whose implementation a trusted host must bind.

An annotation is data for tooling and has no effect unless the active profile recognizes it. Do not use modifiers or annotations as a security boundary.

Declarations

A backend source is a list of named declarations. Arguments are named, and there is no new keyword.

backend/main.acore
project(id: "orders-api", version: "1.0.0")
policies(cache: CachePolicy(strategy: CacheStrategy.cacheFirst, ttlSeconds: 300))
model Index {
  names: List<String>
  counts: Map<String, Int64>
}
variant default(include: ["*"])
endpoint index(method: GET, path: "/index") {
  response(type: Models.Index)
  cache(strategy: CacheStrategy.cacheFirst, ttl: 5m)
}

Declarations refer to each other through typed namespaces instead of strings:

ReferencePoints to
Models.TaskA model, authored or extracted
Entities.task, Entities.TaskA promoted domain entity
Projections.name, Validators.name, Functions.nameDomain projection, validator, pure function
Actions.name, Resources.name, Failures.name, Jobs.name, Guards.nameServer-plan declarations
Configurations.name, Secrets.name, OperationPolicies.namePrivate setup and policies
Routes.nameAn endpoint from an amended source
Audience.web or Audience["web"]An audience listed in AxiomDeps.toml
.fieldA field of the model or entity in context, as in fields: [.id, .email]
Input.field, Input.path.id, Input.body.titleAction or endpoint input
CacheStrategy.cacheFirstAn enum member

Authoring shortcuts

Experimental axiom check 0.149.1 accepts these forms. They do not change older explicit source and do not authorize extra provider operations. Editor support needs a matching acore-lsp.

Endpoint types inferred from an action

Keep the action's input and output and each transport location explicit. The compiler derives a missing response type and parameter types from the resolved handle mapping, after imports and amendments.

backend/main.acore
project(id: "typed-route", version: "1.0.0")
model Selector { id: Int32(min: 1) }
model Task { title: String }

action read(kind: ActionKind.query, input: Models.Selector, output: Models.Task) {
  native(symbol: "tasks.read")
}
endpoint readTask(method: GET, path: "/tasks/{id}") {
  request { path("id") }
  handle(action: Actions.read, input: {id: Input.path.id})
}

This endpoint gets an Int32 path parameter and a Task response.

  • Model input validation stays authoritative, and explicit annotations are still checked.
  • Ambiguous bindings, unknown actions, missing fields and optional path inputs are rejected.
  • Optionality stays explicit: a query, header or body field is not guessed from its name.
  • A response can keep its projection, status or media type and omit only the type.
  • Endpoints without an action keep explicit response types. Without a handle, a parameter needs type:.
  • Inference never selects a larger public projection or audience.
  • A native action still needs a registered trusted implementation.

Record updates and interpolation

Managed action excerpt
let updated = {task with title: normalized}
let message = $"Task {task.id}: {updated.title} {{ready}}"
  • A record update evaluates the base once, then the replacements in source order. It is shallow and immutable, keeps omitted optional fields, and rejects unknown, duplicate or differently typed fields. Arbitrary JSON spreading and capability handles are not supported.
  • $"..." checks each placeholder as a pure expression. Placeholders accept String, Int and Bool; format optional, null and other values explicitly. {{ and }} produce literal braces, and ordinary JSON string escapes work.
  • Backend integers keep exact Int64 behavior; frontend integers keep the portable safe range. Output and work stay bounded.
  • Ordinary strings keep their meaning. The frontend interpolate and to_string functions remain separate, compatible forms. Interpolation is text formatting, not HTML escaping or SQL construction.

Shared pure functions

Put a library of pure functions inside the frontend workspace so the frontend's local import stays inside it, then import the function by name from both sides.

frontend/shared/title.acore
pure fn normalize(value: String): String = value.trim()
backend/main.acore
use function normalize from "../frontend/shared/title.acore"
frontend/main.acore
use function normalize from "./shared/title.acore"

The backend parses the library without executing it and selects the function plus the pure functions it calls from the same library. A library contains pure functions only: missing exports, resource declarations, ambient calls and recursion are rejected. Each consumer checks the library against its own type and numeric rules. To share models, validators or codecs where visibility or representation matters, use the verified contract descriptors instead.

Models derived from a database schema

backend/main.acore
derive model Task(
  schema: "../database/schema.acore",
  table: "main.tasks",
  fields: {id: "id", title: "title"}
) also entity
  • The schema's own manifest must select the database type, and its source declares a numeric semantic version.
  • The compiler reads an immutable snapshot of that source and creates a virtual model. No Task.acore file is generated.
  • Only the listed columns are used, so new private or tenant columns do not widen the API.
  • PostgreSQL and SQLite share one checked mapping. String bounds and Int16 limits are kept. UUID columns get a checked canonical text pattern. Exact PostgreSQL Int64 and SQLite integer columns become bounded canonical strings.
  • Decimal, date and enum columns, nullable columns and unsupported vendor or codec mappings need an explicit, reviewed model. They never silently become optional fields. Database defaults do not become API defaults.
  • Entity keys and audience projections follow the usual rules. The source does not infer SQL queries, grants, tenant claims or migration authorization; keep using Migrations and persistence. The derive CLI remains available for intentional exported models.

Editor support and generated files

The matching language server shows resolved endpoint and action signatures, closed failure sets, inferred resources, selected codecs and schema-field navigation. VS Code and Zed provide the acore-derive-model, acore-errors-infer, acore-record-update and acore-interpolate starters. Editing never resolves credentials or runs providers.

Generated output goes under the nearest manifest's .axiom/ directory by default. Dependency locks, stable identities and migration history keep their own version-control rules. An explicit export can include private companion files; keep those out of client distributions.

Failure inference with errors(infer) and --frozen-interface are described in Managed logic and concurrency.