Expressions

The full expression tree as parsed. After semantic analysis the same expressions are lowered to IrExpr for code generation.

Expr

#![allow(unused)]
fn main() {
pub enum Expr {
    Literal {
        value: Literal,
        span: Span,
    },

    /// Unified invocation: struct instantiation or function call.
    /// Semantic analysis determines which based on the name.
    Invocation {
        path: Vec<Ident>,                 // Name/path being invoked
        type_args: Vec<Type>,             // Generic type arguments
        args: Vec<(Option<Ident>, Expr)>, // Named or positional args
        span: Span,
    },

    /// A call of the value of an expression: `make()(4)`.
    Call {
        callee: Box<Expr>,
        args: Vec<(Option<Ident>, Expr)>,
        span: Span,
    },

    EnumInstantiation {
        enum_name: Ident,        // `Status`, or `shapes::Status` as one name
        type_args: Vec<Type>,    // `Maybe<I32>.none` gives [I32]
        variant: Ident,
        data: Vec<(Ident, Expr)>,
        span: Span,
    },

    InferredEnumInstantiation {
        variant: Ident,
        data: Vec<(Ident, Expr)>,
        span: Span,
    },

    Array {
        elements: Vec<Expr>,
        span: Span,
    },

    Tuple {
        fields: Vec<(Ident, Expr)>,
        span: Span,
    },

    Reference {
        path: Vec<Ident>,
        span: Span,
    },

    BinaryOp {
        left: Box<Expr>,
        op: BinaryOperator,
        right: Box<Expr>,
        span: Span,
    },

    UnaryOp {
        op: UnaryOperator,
        operand: Box<Expr>,
        span: Span,
    },

    ForExpr {
        var: Ident,
        collection: Box<Expr>,
        body: Box<Expr>,
        span: Span,
    },

    IfExpr {
        condition: Box<Expr>,
        then_branch: Box<Expr>,
        else_branch: Option<Box<Expr>>,
        span: Span,
    },

    MatchExpr {
        scrutinee: Box<Expr>,
        arms: Vec<MatchArm>,
        span: Span,
    },

    Group {
        expr: Box<Expr>,
        span: Span,
    },

    DictLiteral {
        entries: Vec<(Expr, Expr)>,  // Key-value pairs
        span: Span,
    },

    DictAccess {
        dict: Box<Expr>,
        key: Box<Expr>,
        span: Span,
    },

    FieldAccess {
        object: Box<Expr>,
        field: Ident,
        span: Span,
    },

    ClosureExpr {
        params: Vec<ClosureParam>,
        return_type: Option<Type>,   // The parser always sets None
        body: Box<Expr>,
        span: Span,
    },

    LetExpr {
        mutable: bool,
        pattern: BindingPattern,
        ty: Option<Type>,
        value: Box<Expr>,
        body: Box<Expr>,
        span: Span,
    },

    MethodCall {
        receiver: Box<Expr>,
        method: Ident,
        type_args: Vec<Type>,              // `I32` in `Box<I32>.of(v: 3)`; empty on a value
        args: Vec<(Option<Ident>, Expr)>,  // Arguments with optional labels
        span: Span,
    },

    Block {
        statements: Vec<BlockStatement>,
        result: Box<Expr>,
        span: Span,
    },
}
}

Expr::span() returns the span of any variant.

Calls

The parser reads name(args) and a::b::name(args) as an Invocation. The semantic pass decides if the name is a struct or a function. The parser reads (args) after any other expression as a Call: make()(4), f(x)(3) and (g)(1) are calls of a value. A Call lowers to IrExpr::CallClosure.

Enum paths

EnumInstantiation.enum_name holds the whole path as one name, joined with ::: shapes::Status.active gives enum_name shapes::Status. type_args holds the type arguments written on the path, so Maybe<I32>.none gives [I32]. It is empty when the path writes none. Serialisation omits an empty type_args, and deserialisation reads a missing one as empty.

Field paths

The parser appends a field to a Reference path: user.name is a Reference with the path [user, name]. After any other expression, .field gives a FieldAccess, for example make().name.

if let

The parser changes if let x = value { a } else { b } into a MatchExpr on value with two arms: .some(x): a and .none: b. So the AST has no if let node.

Closures

A closure is (params) -> body. The parentheses are required, also for one parameter. The syntax has no place for a return type, so the parser always sets return_type to None. The semantic pass reads a return_type that a tool sets on a hand-built AST.

Value paths

The parser reads Name.variant and Name.variant(label: value) as an EnumInstantiation when Name starts with an uppercase letter. The same text is a field access or a method call when Name is a value, for example G.slice(start: 1, end: 3) on let G: String. The parser cannot tell the two apart. After the semantic analyzer builds the symbol table, it changes each EnumInstantiation whose name is not a type or a trait into a Reference path or a MethodCall. The code is in src/semantic/value_paths.rs.

BlockStatement

#![allow(unused)]
fn main() {
pub enum BlockStatement {
    Let {
        mutable: bool,
        pattern: BindingPattern,
        ty: Option<Type>,
        value: Expr,
        span: Span,
    },
    Assign {
        target: Expr,
        value: Expr,
        span: Span,
    },
    Expr(Expr),
}
}

ClosureParam

#![allow(unused)]
fn main() {
pub struct ClosureParam {
    pub convention: ParamConvention,  // Let (default), Mut, or Sink
    pub name: Ident,
    pub ty: Option<Type>,
    pub span: Span,
}
}

convention on a ClosureParam constrains the caller of the closure, not the closure itself. Sink means the caller gives up the argument on each invocation; Mut means the caller must pass a mutable binding.

Literal

#![allow(unused)]
fn main() {
pub enum Literal {
    String(String),
    /// Numeric literal: see `NumberLiteral` for the carried payload.
    Number(NumberLiteral),
    Boolean(bool),
    Nil,
}
}

NumberLiteral

Discriminated payload for a numeric literal: preserves the exact integer digits as i128 (so i64-and-narrower targets round-trip without precision loss) or the float bits as f64. Carries the optional source-level type suffix and the integer-vs-float source- syntax kind so later passes can pick the resolved primitive without re-running inference.

#![allow(unused)]
fn main() {
pub struct NumberLiteral {
    pub value: NumberValue,
    pub suffix: Option<NumericSuffix>,
    pub kind: NumberSourceKind,
}

pub enum NumberValue {
    Integer(i128),  // integer-syntax literals: 42, 1_000, 0xFF
    Float(f64),     // float-syntax literals: 3.14, 1e5
}

pub enum NumericSuffix {
    I32, I64, F32, F64,  // uppercase suffix: 42I64, 3.14F32
}

pub enum NumberSourceKind {
    Integer,  // unsuffixed default → I32
    Float,    // unsuffixed default → F64
}
}

BinaryOperator

#![allow(unused)]
fn main() {
pub enum BinaryOperator {
    // Arithmetic
    Add, Sub, Mul, Div, Mod,
    // Comparison
    Lt, Gt, Le, Ge, Eq, Ne,
    // Logical
    And, Or,
    // Range
    Range,  // ..
}
}

Operator precedence (higher binds tighter). All binary operators are left-associative:

PrecedenceOperators
11.method(...), call (...) (postfix)
10.field, [index] (postfix)
9-, ! (prefix)
6*, /, %
5+, -
4<, >, <=, >=
3==, !=
2&&
1||
0..

UnaryOperator

#![allow(unused)]
fn main() {
pub enum UnaryOperator {
    Neg,  // -x
    Not,  // !x
}
}