Introducing Achronyme — a language for zero-knowledge proofs. Read the announcement arrow_right_alt

AST Reference

Achronyme abstract syntax tree: enums, fields, and span tracking.

Overview

  • Owned, hand-built AST. No rowan, no Rc, no interning at this layer.
  • File: crates/achronyme-parser/src/ast.rs
  • Every node carries a Span. Re-exported from crates/diagnostics/src/span.rs.
  • Each Expr carries an ExprId(u32) — dense, unique within a Program. Used by the resolver to attach SymbolId via a parallel HashMap<ExprId, SymbolId> instead of mutating the AST.
  • Synthetic compile-generated expressions use ExprId::SYNTHETIC = 0. Real parser-allocated IDs start at 1.
  • The AST is immutable after parsing. Lowering passes (ProveIR, bytecode) read the tree and emit new IRs; they never rewrite nodes.

Span / source position

pub struct Span {
    pub byte_start: usize,
    pub byte_end: usize,
    pub line_start: usize,
    pub col_start: usize,
    pub line_end: usize,
    pub col_end: usize,
}

pub struct SpanRange {
    pub start: Span,
    pub end: Span,
}

Spans are byte-offset based for slicing the original source and (line, col) based for human-facing diagnostics. SpanRange is used when a node’s span genuinely needs two anchors (e.g. an if/else whose else branch lives far from the head).

Top-level: Program

pub struct Program {
    pub stmts: Vec<Stmt>,
}

A Program is the unit of parsing. Each input file becomes one Program; module imports are resolved separately by walking Stmt::Import and re-parsing each referenced file into its own Program.

Stmt enum

From crates/achronyme-parser/src/ast.rs:52-141:

VariantFieldsPurpose
LetDeclname: String, type_ann: Option<TypeAnnotation>, value: Expr, span: SpanImmutable binding. let x = 1;
MutDeclname: String, type_ann: Option<TypeAnnotation>, value: Expr, span: SpanMutable binding. mut x = 1;
Assignmenttarget: Expr, value: Expr, span: Spantarget must be an lvalue (Ident / Index / DotAccess); validated at resolve time.
PublicDeclnames: Vec<InputDecl>, span: Spanpublic root, leaf; — circuit / prove input visibility.
WitnessDeclnames: Vec<InputDecl>, span: Spanwitness path; — same shape as PublicDecl, opposite visibility.
FnDeclname: String, params: Vec<TypedParam>, return_type: Option<TypeAnnotation>, body: Block, span: SpanTop-level function.
CircuitDeclname: String, params: Vec<TypedParam>, body: Block, span: Spancircuit MerkleProof(...) { ... }.
Printvalue: Expr, span: SpanVM-only side effect.
Returnvalue: Option<Expr>, span: SpanBare return; is allowed in nil-returning functions.
Breakspan: SpanLoop control.
Continuespan: SpanLoop control.
Importpath: String, alias: Option<String>, span: Spanimport "./foo.ach" as foo;
SelectiveImportnames: Vec<String>, path: String, span: Spanimport { a, b } from "./foo.ach";
Exportinner: Box<Stmt>, span: Spanexport fn foo() { ... }.
ExportListnames: Vec<String>, span: Spanexport { a, b };
ImportCircuitpath: String, alias: Option<String>, span: Spanimport circuit "./foo.circom" as Foo;
ExprExprBare expression as statement (foo();).
Errorspan: SpanRecovery placeholder; lowering passes skip silently.

Expr enum

From crates/achronyme-parser/src/ast.rs:188-331. Every variant carries id: ExprId and span: Span in addition to the listed fields.

Literals

VariantExtra fieldsNotes
Numbervalue: StringString-stored; parsed lazily by lowering. Enables arbitrary-length integers without committing to a numeric type at parse time.
FieldLitvalue: String, radix: FieldRadixradix ∈ {Decimal, Hex, Bin}.
BigIntLitvalue: String, width: u16, radix: BigIntRadixwidth is the bit width: 0i256_…width = 256.
Boolvalue: bool
StringLitvalue: StringUnescaped UTF-8.
NilVM only; rejected by ProveIR lowering.

Names + access

VariantExtra fieldsNotes
Identname: String
StaticAccesstype_name: String, member: StringType::MEMBER, e.g. Field::ZERO, Int::MAX, BigInt::from_bits.
Indexobject: Box<Expr>, index: Box<Expr>arr[i].
DotAccessobject: Box<Expr>, field: StringDesugars to method dispatch when followed by (...) (handled in postfix).
Callcallee: Box<Expr>, args: Vec<CallArg>Positional and keyword args mixed in args.

Operators

VariantExtra fields
BinOpop: BinOp, lhs: Box<Expr>, rhs: Box<Expr>
UnaryOpop: UnaryOp, operand: Box<Expr>

Control + structure

VariantExtra fieldsNotes
Ifcondition: Box<Expr>, then_block: Block, else_branch: Option<ElseBranch>else_branch is either a block or another If, supporting else if chains.
Forvar: String, iterable: ForIterable, body: BlockSee ForIterable below.
Whilecondition: Box<Expr>, body: BlockUnrolled by the lowering layer when used inside a circuit body.
Foreverbody: Blockforever { ... } — VM-only; circuit lowering rejects it.
Blockblock: BlockBlock-as-expression; the last expression’s value is the block’s value if no trailing ;.
FnExprname: Option<String>, params: Vec<TypedParam>, return_type: Option<TypeAnnotation>, body: BlockFunction-as-value. name is for self-recursion.
Provename: Option<String>, body: Block, params: Vec<TypedParam>The new typed-parameter form; the legacy public-list form is rewritten into this shape during parsing.

Composite

VariantExtra fieldsNotes
Arrayelements: Vec<Expr>Static-size arrays in circuit mode.
Mappairs: Vec<(MapKey, Expr)>VM only; rejected by ProveIR.

Recovery

VariantExtra fieldsNotes
ErrorEmitted by parser sync points (synchronize() in parser/core.rs).

Operators

pub enum BinOp {
    Add, Sub, Mul, Div, Mod, Pow,
    Eq, Neq, Lt, Le, Gt, Ge,
    And, Or,
}

pub enum UnaryOp {
    Neg, Not,
}

BinOp covers all infix operators in the precedence table. There is no separate node for the ternary — it is desugared into If at parse time so downstream passes only see one conditional shape.

Auxiliary types

pub struct CallArg {
    pub name: Option<String>,   // None = positional, Some("x") = keyword
    pub value: Expr,
}

pub enum ElseBranch {
    Block(Block),
    If(Box<Expr>),    // chained else-if
}

pub enum ForIterable {
    Range { start: u64, end: u64 },              // 0..10
    ExprRange { start: u64, end: Box<Expr> },    // 0..n  (dynamic upper bound, circuit mode)
    Expr(Box<Expr>),                             // for x in arr
}

pub enum MapKey {
    Ident(String),
    StringLit(String),
}

pub enum FieldRadix {
    Decimal,
    Hex,
    Bin,
}

pub enum BigIntRadix {
    Decimal,
    Hex,
    Bin,
}

pub struct InputDecl {
    pub name: String,
    pub type_ann: Option<TypeAnnotation>,
}

pub struct TypedParam {
    pub name: String,
    pub type_ann: TypeAnnotation,
}

pub struct TypeAnnotation {
    pub visibility: Option<Visibility>,
    pub base: BaseType,
    pub array_size: Option<usize>,
}

pub enum Visibility {
    Public,
    Witness,
}

pub enum BaseType {
    Field,
    Bool,
    Int,
    String,
}

ForIterable::ExprRange is the variant introduced for circuit-mode parametric loop bounds: for i in 0..n where n is a template parameter or a compile-time-known var. The lowering pass evaluates end to a u64 before unrolling.

TypeAnnotation::array_size is Some(n) for fixed-size arrays (Field[8]) and None for Field[] in prove(...) parameters where the size is captured from the calling scope.

ExprId allocation

  • Dense u32 IDs, allocated by the parser as it walks the source.
  • The whole Program is the ID arena; cross-program comparisons are not meaningful (e.g. ExprId(42) in foo.ach and ExprId(42) in bar.ach are unrelated).
  • ExprId::SYNTHETIC = 0 is reserved for compiler-generated nodes (mainly inside the Circom lowering and the ProveIR compiler when synthesising auxiliary expressions during template instantiation).
  • The resolver pass annotates each ExprId with a SymbolId in SymbolTable. See crates/resolve/src/annotate.rs. The resolver does not mutate the AST — it builds a side table keyed by ExprId.
  • This shape (immutable AST + side-table annotations) is what makes the LSP cheap: the resolver runs in microseconds and discards its tables when the AST is invalidated.

Source files

ComponentFile
AST typescrates/achronyme-parser/src/ast.rs
Spancrates/diagnostics/src/span.rs
Parser entrycrates/achronyme-parser/src/lib.rs
Resolver annotationcrates/resolve/src/annotate.rs
Symbol tablecrates/resolve/src/table.rs

See Grammar & Lexer for the surface syntax. See Pipeline Overview for what happens after parsing.

Navigation