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

Grammar Syntax

The reference page. If you'd rather learn by example, start with Your First Grammar.

Tokens and rules

Kind How to spot it What it's for
Token Name is entirely UPPERCASE (NUMBER, LOGICAL_OP) Lexer-level pieces: identifiers, numbers, operators
Rule Lowercase name (expr) Parser-level structure built from tokens and other rules
Nested rule Written as #name: inside another rule A rule scoped to its parent

Every definition ends with ;.

🚧 Not yet supported: using a rule inside a token. This is planned, see Roadmap and Status.

Capturing: @

@ marks the part of a production you want to capture. Anything you don't mark isn't captured, which keeps your AST small and your lexer fast.

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

The data block: {@}

{@} assigns one single AST node to the production. The value can be either:

  • @, meaning the first (and only) thing you captured, or
  • a Python-style expression.
ID:
  @ ([a-zA-Z_][a-zA-Z0-9_]*)
  {@}
;

Naming captures: @{...}

When a production captures several things, @{a, b, c} names them. Put @ before each element you want, then list the names in the block. The names become the fields of the generated AST node.

#compare:
    @ arithmetic (@ COMPARE_OP @ arithmetic)*
    @{first, op, sequence}
;

Note: @name will produce an ast node name with no control directed to @{}. @ name will direct. Single space matters here.

Nested rules: #name

#name: defines a rule that lives inside another rule. A nested rule can be regular or advanced.

A regular (non-advanced) rule can, in turn, contain more rules inside it. These go right after the rule's content, followed by its data block or its fail block.

Nesting is handy for things like precedence levels, where every level is a small rule that belongs to one parent. See the full expr example in Your First Grammar.

Advanced rules use braces and executable clauses. For example, COMMENT { match '#' [^\n\0]*; ctx.skip() } recognizes a comment and tells the lexer to discard it. See examples/yaml/grammar.isc for indentation handling with ctx.emit, shared state, and ctx.error. The embedded action language is a supported Python subset, not arbitrary Python.

Operators you've already seen

Syntax Meaning
'text' A literal string
[0-9], [^"] Character classes (and negated classes)
( ... ) Grouping
*, +, ? Zero or more, one or more, optional
| Alternation

Semantic actions

Grammars can include Python-style pseudocode for semantic actions. This is what makes context-sensitive lexing possible.

🚧 LR/LALR accept advanced rules containing ordinary match clauses, but reject embedded Python actions and conditional matches. LR(*) currently rejects builds. See Algorithms and Limitations.

For grammar modules, external parser imports, and standard library modules, see Modules and External Parsers. Counted repetition, modifiers, templates, and dynamic patterns are evolving features; the concept/ documents describe proposals and should not be read as a promise that every form lowers to generated code.

Fail blocks

Fail blocks define custom error messages and recovery. They're still limited, so they have their own page: Error Recovery.

Clone this wiki locally