Alpaca
A type-safe lexer and parser library for Scala 3, featuring compile-time validation and a pattern-matching DSL.
Features
- Type-safe lexer and parser — catch errors at compile time with Scala 3's type system
- Pattern-matching DSL — define lexers and parsers using intuitive
casesyntax - Compile-time validation — regex patterns and grammar rules are checked during compilation
- Macro-based code generation — Scala 3 macros generate efficient tokenizers and parse tables
- Context-aware — lexical and parsing contexts with type-safe state management
- LALR(1) parsing — automatic parse table generation with conflict detection
- Cross-platform — runs on the JVM, Scala.js, and Scala Native
Installation
Mill
Add Alpaca as a dependency in your build.mill:
//| mill-version: 1.3.1
//| mill-jvm-version: 21
import mill._
import mill.scalalib._
object myproject extends ScalaModule {
def scalaVersion = "3.9.0"
def scalacOptions = Seq("-Yretain-trees")
def mvnDeps = Seq(
mvn"com.halotukozak::alpaca::1.0.0-RC5"
)
}
SBT
Add Alpaca to your build.sbt:
libraryDependencies += "com.halotukozak" %% "alpaca" % "1.0.0-RC5"
Make sure you're using Scala 3.9.0 or later and enable the required compiler flag (Alpaca 0.3.2 and earlier also need -experimental):
scalaVersion := "3.9.0"
scalacOptions ++= Seq("-Yretain-trees")
Scala CLI
Use Alpaca directly in your Scala CLI scripts:
//> using scala "3.9.0"
//> using dep "com.halotukozak::alpaca:1.0.0-RC5"
//> using options "-Yretain-trees"
import halotukozak.alpaca.*
// Your code here
Scala.js and Scala Native
Alpaca is also published for Scala.js 1 and Scala Native 0.5 (built with Scala.js 1.22.0 and Scala Native 0.5.12). The same dependency works there, with %%% in sbt:
libraryDependencies += "com.halotukozak" %%% "alpaca" % "1.0.0-RC5"
In Mill, the mvn"com.halotukozak::alpaca::1.0.0-RC5" line above picks the right artifact in a ScalaJSModule or ScalaNativeModule. In Scala CLI, add //> using platform js (or native) and write the dependency with two colons before the version: //> using dep "com.halotukozak::alpaca::1.0.0-RC5". The -Yretain-trees flag is needed on every platform.
Quick Start
Creating a Lexer
Define a lexer using pattern matching with regex patterns:
import halotukozak.alpaca.*
val MyLexer = lexer:
case num @ "[0-9]+" => Token["NUM"](num.toDouble)
case "\\+" => Token["PLUS"]
case "-" => Token["MINUS"]
case "\\*" => Token["STAR"]
case "/" => Token["SLASH"]
case "\\(" => Token["LP"]
case "\\)" => Token["RP"]
case "\\s+" => Token.Ignored
Creating a Parser
Define a parser by extending the Parser class and defining grammar rules:
import halotukozak.alpaca.*
val MyLexer = lexer:
case num @ "[0-9]+" => Token["NUM"](num.toDouble)
case "\\+" => Token["PLUS"]
case "-" => Token["MINUS"]
case "\\*" => Token["STAR"]
case "/" => Token["SLASH"]
case "\\(" => Token["LP"]
case "\\)" => Token["RP"]
case "\\s+" => Token.Ignored
import halotukozak.alpaca.*
object MyParser extends Parser:
val root: Rule[Double] = rule:
case Expr(e) => e
val Expr: Rule[Double] = rule(
{ case (Expr(l), MyLexer.PLUS(_), Term(r)) => l + r },
{ case (Expr(l), MyLexer.MINUS(_), Term(r)) => l - r },
{ case Term(t) => t },
)
val Term: Rule[Double] = rule(
{ case (Term(l), MyLexer.STAR(_), Factor(r)) => l * r },
{ case (Term(l), MyLexer.SLASH(_), Factor(r)) => l / r },
{ case Factor(f) => f },
)
val Factor: Rule[Double] = rule(
{ case MyLexer.NUM(n) => n.value },
{ case (MyLexer.LP(_), Expr(e), MyLexer.RP(_)) => e },
)
Parsing Input
import halotukozak.alpaca.*
val MyLexer = lexer:
case num @ "[0-9]+" => Token["NUM"](num.toDouble)
case "\\+" => Token["PLUS"]
case "-" => Token["MINUS"]
case "\\*" => Token["STAR"]
case "/" => Token["SLASH"]
case "\\(" => Token["LP"]
case "\\)" => Token["RP"]
case "\\s+" => Token.Ignored
import halotukozak.alpaca.*
object MyParser extends Parser:
val root: Rule[Double] = rule:
case Expr(e) => e
val Expr: Rule[Double] = rule(
{ case (Expr(l), MyLexer.PLUS(_), Term(r)) => l + r },
{ case (Expr(l), MyLexer.MINUS(_), Term(r)) => l - r },
{ case Term(t) => t },
)
val Term: Rule[Double] = rule(
{ case (Term(l), MyLexer.STAR(_), Factor(r)) => l * r },
{ case (Term(l), MyLexer.SLASH(_), Factor(r)) => l / r },
{ case Factor(f) => f },
)
val Factor: Rule[Double] = rule(
{ case MyLexer.NUM(n) => n.value },
{ case (MyLexer.LP(_), Expr(e), MyLexer.RP(_)) => e },
)
import halotukozak.alpaca.*
val input = "2 + 3 * 4"
val lexemes = MyLexer.tokenize(input).getOrThrow
val result = MyParser.parse(lexemes).getOrThrow
println(result) // 14.0
What's Next
- Getting Started — build a BrainFuck interpreter step by step
- Lexer — the full lexer DSL reference
- Parser — grammar rules, EBNF operators, conflict resolution
- Tooling — debug output and the IntelliJ plugin
- Cookbook — complete examples: an expression evaluator, a JSON parser, contextual lexing
- Theory — formal foundations: finite automata, LR parsing, parse tables
