Skip to content
Sinfolke edited this page Oct 25, 2026 · 2 revisions

Your First Grammar

Let's build up an expression grammar piece by piece. By the end you'll have met most of ISPA's syntax. We'll go slowly and explain every symbol.

Step 1: a token

ID:
  @ ([a-zA-Z_][a-zA-Z0-9_]*)
  {@}
;
  • ID is written in UPPERCASE, so ISPA treats it as a token. The whole name must be uppercase, not just the first letter.
  • The pattern in the middle is a regular-expression-style match for identifiers.
  • @ means "capture this." Everything the grammar captures can end up in the AST.
  • {@} means "the AST node for this is whatever I captured." We captured one thing, so that one thing becomes the node.
  • ; ends the definition.

Step 2: a token with several parts

NUMBER:
  @ [0-9]+ ('.' @ [0-9]+)?
  @{decimal, floating}
;

Now there are two @s. Each one captures a piece, and the @{decimal, floating} block gives the captures names, in the order the @s appear. So decimal holds the digits before the dot, and floating holds the digits after it (the optional part).

These names end up as the field names of the AST node ISPA generates for the token, so they're worth choosing well.

Named captures follow the order of their @ captures in that production.

Step 3: a rule

Lowercase names are rules. They build structure out of tokens (and other rules).

expr:
    @ #logical
    {@}

    #logical:
        @ compare (@ LOGICAL_OP @ compare)*

        @{left, op, right}
    ;

    #compare:
        @ arithmetic (@ COMPARE_OP @ arithmetic)*

        @{first, op, sequence}
    ;

    #arithmetic:
        @ term (@ PLUS | MINUS @ term)*

        @{first, op, sequence}
    ;

    #term:
        @ value (@ MULTIPLE | DIVIDE | MODULO @ value)*

        @{first, op, sequence}
    ;

    #value:
        @ STRING | NUMBER | ID
        {@}
    ;

    #group:
        '(' @ expr ')'
        {@}
    ;
;

There's a lot going on, so let's unpack it:

  • #name: defines a nested rule. Nested rules live inside their parent, right after the parent's own content and data block. They're scoped to the parent, so logical, compare and friends belong to expr.
  • Nesting is how this grammar expresses operator precedence. logical is built from compare, which is built from arithmetic, then term, then value. Operators lower in the chain bind tighter. It's the classic technique, and nested rules keep it tidy and in one place.
  • @ #logical {@} says expr is a single captured logical, and that is its node.
  • @{first, op, sequence} names three captures: the first operand, the operator, and the repeated operand(s).

That's a complete little expression language. 🎉

ℹ️ The STRING token in the README's example embeds a rule inside a token ('${' @ expr '}'). Rules inside tokens aren't supported yet, so treat that part as a preview of where things are going.

Step 4: generate and use it

Run ISPA on your grammar (see Getting Started), then look at Generated Code to see the types you got, and Runtime API to see how to feed in text and get a tree back.

Clone this wiki locally