Parser Context
Parser context lets you carry mutable state through parsing reductions. Stateless parsers use ParserCtx.Empty by default; custom contexts carry domain-specific state like symbol tables, error accumulators, or any state that evolves as productions are reduced.
Compile-time processing: When you define
Parser[Ctx], the Alpaca macro verifies thatCtxextendsParserCtx, hasCopyablederived, and generates the appropriate context threading code. At runtime, the initial context is created viaEmpty[Ctx](using default constructor values) and the same object is passed to every rule reduction in a singleparse()call.
ParserCtx.Empty (Default)
When you extend Parser without a type parameter, the parser uses ParserCtx.Empty internally. No context definition is needed — this is the right choice for parsers that only care about the parsed value, not accumulated state.
import alpaca.*
object CalcParser extends Parser: // uses ParserCtx.Empty by default
val Expr: Rule[Int] = rule(
{ case (Expr(a), CalcLexer.PLUS(_), Expr(b)) => a + b },
{ case CalcLexer.NUMBER(n) => n.value },
)
val root: Rule[Int] = rule:
case Expr(e) => e
val (_, result) = CalcParser.parse(CalcLexer.tokenize("1 + 2").lexemes)
// result: Int | Null -- 3 for valid input, null for invalid input
// ctx is ParserCtx.Empty -- can be discarded with _
Custom Parser Context
To carry state across reductions, define a case class extending ParserCtx with derives Copyable:
import alpaca.*
import scala.collection.mutable
case class CalcContext(
names: mutable.Map[String, Int] = mutable.Map.empty,
errors: mutable.ListBuffer[(tpe: String, value: Any, line: Int)] = mutable.ListBuffer.empty,
) extends ParserCtx derives Copyable
Four rules apply to every custom parser context:
- Must be a
case class—Copyable.derivedrequiresMirror.ProductOf, which only case classes provide. - All fields must have default values —
Empty[Ctx]constructs the initial context from constructor defaults. Fields without defaults cause a compile error. derives Copyableis required —Parser[Ctx]requires an implicitCopyable[Ctx]. Without it, the compiler reports an implicit not found error.- Mutable collections are
val; other mutable fields arevar— mutate the collection contents, not the reference.
Accessing Context in Rule Bodies
The ctx identifier is available inside every rule { case ... } body and resolves to the current context instance, typed as your specific ParserCtx subtype:
import alpaca.*
object CalcParser extends Parser[CalcContext]:
val Expr: Rule[Int] = rule(
{ case CalcLexer.NUMBER(n) => n.value },
{ case CalcLexer.ID(id) =>
ctx.names.getOrElse( // ctx is CalcContext here, not just ParserCtx
id.value, {
ctx.errors.append(("undefined", id, id.line))
0
},
)
},
)
val Statement: Rule[Unit | Int] = rule(
{ case (CalcLexer.ID(id), CalcLexer.ASSIGN(_), Expr(expr)) =>
ctx.names(id.value) = expr }, // mutates the shared context
{ case Expr(expr) => expr },
)
val root = rule:
case Statement(stmt) => stmt
ctx is @compileTimeOnly — it is only valid syntactically inside rule bodies.
Getting Positional Info from the Lexeme, Not ctx
ParserCtx and LexerCtx are completely independent. The parser context has no text, position, or line fields. To access positional information in parser rule bodies, use the fields on the Lexeme binding — not ctx.
Every terminal binding (e.g., id in CalcLexer.ID(id)) is a Lexeme object carrying a snapshot of the lexer context at match time. The available fields are:
| Field | Type | Description |
|---|---|---|
binding.value |
Token-specific | The extracted semantic value (e.g., Int for a number token) |
binding.text |
String |
The raw matched characters |
binding.position |
Int |
Character position from the lexer context snapshot |
binding.line |
Int |
Line number from the lexer context snapshot |
The position and line fields are available because the lexer uses LexerCtx.Default, which includes PositionTracking and LineTracking. If your lexer uses a custom context without those traits, the corresponding fields will not be present on the lexeme. See Lexer Context for details on which traits provide which fields.
See Extractors for the full Lexeme field reference and how custom lexer context fields appear in bindings. See Parser for grammar rules and EBNF operators.
