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, function registries, or error accumulators.
Under the hood: context threading
When you define Parser[Ctx], the Alpaca macro verifies that Ctx extends ParserCtx and is a case class (Product). A Copyable instance is automatically provided for any ParserCtx & Product — no explicit derivation is needed. At runtime, the initial context is created via Empty[Ctx] (using constructor defaults) and the same object is passed to every rule reduction in a single parse() call.
ParserCtx.Empty (Default)
When you extend Parser without a type parameter, the parser uses ParserCtx.Empty. No context definition is needed:
import alpaca.*
object BrainParser extends Parser: // uses ParserCtx.Empty
val root: Rule[BrainAST] = rule:
case Operation.List(stmts) => BrainAST.Root(stmts)
val Operation: Rule[BrainAST] = rule(
{ case BrainLexer.inc(_) => BrainAST.Inc },
{ case BrainLexer.dec(_) => BrainAST.Dec },
// ...
)
Custom Parser Context
The BrainFuck> extension adds function definitions and calls. To track which functions have been defined (so we can reject calls to undefined functions), we use a custom parser context:
import alpaca.*
import scala.collection.mutable
case class BrainParserCtx(
functions: mutable.Set[String] = mutable.Set.empty,
) extends ParserCtx
Three rules apply:
- Must be a
case class-- the library automatically provides aCopyableinstance for anyParserCtx & Product. - All fields must have default values --
Empty[Ctx]constructs the initial context from constructor defaults. - 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, typed as your specific ParserCtx subtype:
import alpaca.*
import scala.collection.mutable
case class BrainParserCtx(
functions: mutable.Set[String] = mutable.Set.empty,
) extends ParserCtx
object BrainParser extends Parser[BrainParserCtx]:
val root: Rule[BrainAST] = rule:
case Operation.List(stmts) => BrainAST.Root(stmts)
val FunctionDef: Rule[BrainAST] = rule:
case (BrainLexer.functionName(name), BrainLexer.functionOpen(_),
Operation.List(ops), BrainLexer.functionClose(_)) =>
require(ctx.functions.add(name.value), s"Function ${name.value} is already defined")
BrainAST.FunctionDef(name.value, ops)
val FunctionCall: Rule[BrainAST] = rule:
case (BrainLexer.functionName(name), BrainLexer.functionCall(_)) =>
require(ctx.functions.contains(name.value), s"Function ${name.value} is not defined")
BrainAST.FunctionCall(name.value)
val Operation: Rule[BrainAST] = rule(
{ case BrainLexer.inc(_) => BrainAST.Inc },
{ case BrainLexer.dec(_) => BrainAST.Dec },
{ case FunctionDef(fdef) => fdef },
{ case FunctionCall(call) => call },
// ... other operations
)
FunctionDef adds the function name to ctx.functions. FunctionCall checks that the name exists. Both see the same context object.
Positional Info from Lexemes, Not ctx
ParserCtx and LexerCtx are independent. The parser context has no text, position, or line fields. To access positional information, use the Lexeme binding:
{ case BrainLexer.functionName(name) =>
val funcName = name.value // String -- the function name
val pos = name.position // Int -- 1-based column within the line
val ln = name.line // Int -- line number from lexer
// ...
}
The position and line fields come from the lexer context snapshot. They are available when the lexer uses LexerCtx.Default or a custom context with PositionTracking/LineTracking. See Lexer Context for details.
See Extractors for the full Lexeme field reference. See Parser for grammar rules and EBNF operators.
