Parser Context
Parser context lets you carry 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], Ctx must extend ParserCtx, and the macro that builds the parse tables also builds a factory of the initial context from its constructor defaults -- which only works for a case class whose fields all have default values. At runtime, parse() calls that factory once to create the initial context and passes it to the first reduction. An assignment to a context field in a rule body is rewritten into a copy, which the parser passes to the reductions that follow, as a lexer does with its context.
ParserCtx.Empty (Default)
When you extend Parser without a type parameter, the parser uses ParserCtx.Empty. No context definition is needed:
import halotukozak.alpaca.*
val BrainLexer = lexer:
case "\\+" => Token["inc"]
case "-" => Token["dec"]
case name @ "[A-Za-z]+" => Token["functionName"](name)
case "\\(" => Token["functionOpen"]
case "\\)" => Token["functionClose"]
case "!" => Token["functionCall"]
case "\\s+" => Token.Ignored
enum BrainAST:
case Root(ops: List[BrainAST])
case FunctionDef(name: String, ops: List[BrainAST])
case FunctionCall(name: String)
case Inc, Dec
import halotukozak.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 halotukozak.alpaca.*
case class BrainParserCtx(
functions: Set[String] = Set.empty,
) extends ParserCtx
Two rules apply:
- Must be a
case class-- the initial context is built from the case class constructor (otherwise:... should be a case class.). - All fields must have default values -- the initial context is constructed from constructor defaults.
The fields can be vals holding immutable values: assigning one in a rule body (ctx.functions += name) changes the context for the reductions that follow. A var field, or a mutable collection, is changed in place instead.
Accessing Context in Rule Bodies
The ctx identifier is available inside every production's body, typed as your specific ParserCtx subtype:
import halotukozak.alpaca.*
val BrainLexer = lexer:
case "\\+" => Token["inc"]
case "-" => Token["dec"]
case name @ "[A-Za-z]+" => Token["functionName"](name)
case "\\(" => Token["functionOpen"]
case "\\)" => Token["functionClose"]
case "!" => Token["functionCall"]
case "\\s+" => Token.Ignored
enum BrainAST:
case Root(ops: List[BrainAST])
case FunctionDef(name: String, ops: List[BrainAST])
case FunctionCall(name: String)
case Inc, Dec
import halotukozak.alpaca.*
case class BrainParserCtx(
functions: Set[String] = 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(name.value), s"Function ${name.value} is already defined")
ctx.functions += name.value
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, so it sees the assignment of an earlier FunctionDef.
Positional Info from Lexemes, Not ctx
ParserCtx and LexerCtx are independent: the parser context has no column or line. Positions come from the lexemes a rule binds -- name.column and name.line after BrainLexer.functionName(name), when the lexer context tracks them (see Lexeme Bindings).
See Parser for grammar rules and Extractors for EBNF operators.
