Skip to content

Grammar

vladyslav-kinzerskiy edited this page Jun 15, 2026 · 1 revision

The .art grammar

Reference for the artheia DSL. The grammar source of truth is the textX grammar under artheia/grammar/*.tx; this page summarizes it with authentic examples. When in doubt, read the .tx.

Code blocks use scala highlighting — .art isn't a known highlighter language, and Scala renders it cleanly.

File shape

Model:
    ('package' name=QualifiedName)?
    imports*=Import
    elements*=TopLevelElement

A file is an optional package line, then import lines, then top-level elements. Imports must precede every element — the grammar is package? imports* elements*, so an import after any declaration is a syntax error.

A package is split across two sibling files that the loader merges into one model: package.art (the schema — messages, enums, interfaces, nodes) and component.art (the wiring — compositions, clusters). They must declare the same package. The merger hoists import lines from both files to the top, so either file may carry imports.

File-name resolution priority for import X.Y.*: the resolver looks under the directory mapped from the FQN for system.artcluster.artpackage.artcomponent.art.

package system.services.exec
import system.supervisor.*           // imports first, always
message SupervisionEvent { }         // then declarations

Top-level elements

message · enum · interface · node · composition · cluster · bus · gateway_route.

message / enum (proto3-equivalent)

message LogRecord {
    string context
    uint32 level
    uint64 ts_ns
    string text
}

enum FgState { IDLE = 0  STARTING = 1  RUNNING = 2  STOPPING = 3 }

Scalar field types: int32 int64 sint32 sint64 uint32 uint64 fixed32 fixed64 sfixed32 sfixed64 float double bool string bytes. A field may also reference another message / enum by name, and may be repeated.

message Name { } is its own forward declaration — a message is never extern.

interface — two flavors

provides / requires on a port must match the interface flavor.

// pub/sub, fire-and-forget; one message type per `data`:
interface senderReceiver LogStream {
    data LogRecord record
}

// RPC, request/reply; operations with in/out/inout params:
interface clientServer ExecCtl {
    operation StartGroup(in r:StartGroupRequest) returns ExecEmpty
    operation StopGroup(in r:StopGroupRequest)   returns ExecEmpty
}

node — the thread

NodeDecl:
    extern?='extern'
    'node' kind=NodeKind name=ID ('prototype' base=[NodeDecl|FQN])? '{'
        (tipc=TipcAddress)?
        (requires_timers?='requires_timers')?
        ('reporting' '=' reporting=BoolLit)?
        ('tag' '=' tag=STRING)?
        ('config' config=[MessageDecl|FQN])?
        ('ports' '{' ports*=PortDecl '}')?
        ('params' '{' params*=NodeParam '}')?
        ('statem' '{' statem=StateMBody '}')?
    '}'
  • kind is atomic (a GenServer, or GenStateM if a statem block is present) or runnable (a GenRunnable free worker — no statem).
  • tipc type=0x… instance=0 — the network address. Grammar-optional but loader-REQUIRED for a real (non-extern) node, unless a prototype <Base> supplies it via inheritance.
  • requires_timers — the node uses process_timers() (send_after / cancel_timer); the flag lets main publish the process TimerService.
  • tag = "LOG" — short context id for log lines (defaults to the node name when omitted).
  • reporting = true|false — whether the supervisor watchdogs this node and can push heartbeat / trace / log-level config to it (defaults true).
  • prototype Base — attribute-REPLACEMENT inheritance (deliberately NOT extends): the derived node inherits the base's ports / statem / config / params / requires_timers unless it re-declares them (no field-level merge). The common case is overriding only tipc: node atomic FooZonal prototype Foo { tipc type=0x… instance=0 }.
  • config Msg — the node's etcd-backed config message type.

There is no kick_off (a node's post-construction startup is the OTP init(State&) callback) and no fallthrough (an unrouted inbound frame is dropped with a CRITICAL log in TipcMux; cross-node traffic is exclusively typed cast / call). Tracing has no annotation: every node is runtime-traceable, flipped by the supervisor.

node atomic TraceCollector {
    tipc type=0x80010013 instance=0
    tag = "LOG"
    ports {
        server   ctl_supdbg     provides TraceControl
        sender   stream_out     provides TraceRecordStream
        receiver in_records     requires TraceRecordSubmit
        client   to_supervisor  requires SupervisorControlIf
        sender   to_log         provides LogStream  best_effort
    }
}

ports

Four port kinds. provides / requires must match the interface flavor:

port interface direction
server X provides Iface clientServer this node answers RPCs
client X requires Iface clientServer this node calls RPCs
sender X provides Iface [reliable|best_effort] senderReceiver this node publishes
receiver X requires Iface [reliable|best_effort] senderReceiver this node subscribes

Reliability defaults to reliable; best_effort is fire-and-forget (used for log/trace fan-in where a drop must never block the app).

params (ROS2-style, etcd-backed)

params {
    publish_period_ms : uint32 = 10
    enabled           : bool   = true
    source_name       : string = "front-axle"
}

statem (gen_statem FSM)

A statem block on an atomic node makes it a GenStateM.

statem {
    states [ Idle, Running, Stopping ]
    initial Idle
    data SmContext
    on Idle:
        event StartReq  Running
    on Running:
        event StopReq  Stopping
        timeout  halt
}

Transition arrows are the Unicode (U+2192), not ->.

composition — the process

composition VehicleSystem {
    prototype SpeedPublisher    speed_pub
    prototype TorqueController  torque_ctrl
    connect speed_pub.out          to torque_ctrl.speed_in
    connect torque_ctrl.torque_out to actuator.torque_in
}
  • prototype NodeType name instantiates a node; an optional on process P is a process-affinity hint (consumed by the rig/manifest pipeline).
  • composition CompType name includes another composition.
  • connect a.port to b.port wires ports. A port ref is <prototype>.<port>; prototype names are globally unique in the model, so no member-prefix qualification is needed.

cluster — the distribution bundle

cluster Services {
    composition Com   com
    composition Log   log
    composition Per   per
    composition Sm    sm
    composition Ucm   ucm
}

Each composition T name member is one installable package keyed by name. connect lines inside a cluster are inter-process TIPC (vs the in-process wiring inside a composition).

bus / gateway_route (AUTOSAR PSP)

bus kcan kind = can
gateway_route SpeedPublisher {
    can id=0x42 bus=kcan dlc=8
    direction = in
}

gateway_route maps a node's PDU to a CAN id / FlexRay slot, with direction = in|out. These appear in the generated AUTOSAR mega-node specs (produced by gen-autosar-system / import-dbc / import-fibex), not in hand-written FC specs.

Forward declarations (extern)

Cross-file references resolve through the import + extern forward-decl pattern: declare the symbol locally with the extern keyword and an EMPTY body so the file parses standalone, and the import-following scope provider materializes the real definition from the imported package.

extern is the explicit forward-declaration marker (an empty body is not magic). It prefixes a node, composition, cluster, or interface (a message is never extern).

// system/system.art
package system
import system.services.*
import system.supervisor.*

extern cluster     Services   { }      // real def in system.services
extern composition Supervisor { }      // real def in system.supervisor
cluster Platform {
    composition Supervisor sup
}

The empty { } is still required (extern node atomic FlexRayIngress { }, extern interface senderReceiver EML_01_Iface { }).

Loader rules:

  • An extern decl MUST have an empty body — no tipc / ports on a node, no members on a composition / cluster / interface.
  • A non-extern node is loader-REQUIRED to have a tipc, unless a prototype <Base> supplies it.

Validate

artheia parse <file.art>

Resolves imports recursively and prints the merged tree, or exits non-zero on the first error. Common errors: an import placed after a declaration; a non-extern node missing its tipc; a non-empty extern body; typed as ->; a provides / requires pointing at the wrong interface flavor. The LSP gives the same diagnostics live, plus go-to-definition across the forward-decl boundary — see Editor Support.

Keyword reference

The structural keywords (as recognized by the LSP completion + grammar):

group keywords
file package import
data message enum
interface interface senderReceiver clientServer data operation returns in out inout
node node atomic runnable tipc type instance ports params config tag reporting requires_timers extern
ports sender receiver client server provides requires reliable best_effort
composition composition prototype connect to on process
cluster cluster
statem statem states initial event timeout after halt
bus / route bus gateway_route signal
literals true false
scalar types int32 int64 uint32 uint64 sint32 sint64 fixed32 fixed64 sfixed32 sfixed64 float double bool string bytes

artheia wiki

Install

pip install artheia

artheia · artheia-lsp · artheia-mcp

Related wikis

Clone this wiki locally