Parser.kt
src/jvmMain/kotlin/afield/lang/fieldml/parse/Parser.kt · 560 lines
package and imports — 43 lines, from line 1
package afield.lang.fieldml.parse
import afield.lang.fieldml.ast.BinaryOp
import afield.lang.fieldml.ast.ConstructorDecl
import afield.lang.fieldml.ast.EBinary
import afield.lang.fieldml.ast.EBool
import afield.lang.fieldml.ast.ECall
import afield.lang.fieldml.ast.EField
import afield.lang.fieldml.ast.EFloat
import afield.lang.fieldml.ast.EIf
import afield.lang.fieldml.ast.EInt
import afield.lang.fieldml.ast.ELambda
import afield.lang.fieldml.ast.ELet
import afield.lang.fieldml.ast.EList
import afield.lang.fieldml.ast.EMatch
import afield.lang.fieldml.ast.ENegate
import afield.lang.fieldml.ast.ERecord
import afield.lang.fieldml.ast.EString
import afield.lang.fieldml.ast.ETuple
import afield.lang.fieldml.ast.ETypeDecl
import afield.lang.fieldml.ast.EVar
import afield.lang.fieldml.ast.Expr
import afield.lang.fieldml.ast.MatchCase
import afield.lang.fieldml.ast.PBool
import afield.lang.fieldml.ast.PCons
import afield.lang.fieldml.ast.PConstructor
import afield.lang.fieldml.ast.PInt
import afield.lang.fieldml.ast.PList
import afield.lang.fieldml.ast.PString
import afield.lang.fieldml.ast.PTuple
import afield.lang.fieldml.ast.PVar
import afield.lang.fieldml.ast.PWildcard
import afield.lang.fieldml.ast.Pattern
import afield.lang.fieldml.ast.RecordField
import afield.lang.fieldml.ast.TypeDecl
import afield.lang.fieldml.ast.TypeRef
import afield.lang.fieldml.ast.TypeRefName
import afield.lang.fieldml.diag.CompileAbort
import afield.lang.fieldml.diag.Diagnostic
import afield.lang.fieldml.diag.Severity
import afield.lang.fieldml.diag.SourceRange
import afield.lang.fieldml.lex.Token
import afield.lang.fieldml.lex.TokenKind
FieldML's second stage: tokens in, the surface tree out, by recursive descent with precedence climbing for the operators.
There is no error recovery. The first token the parser cannot use throws, parse catches it, and the result is no tree and exactly one diagnostic. Where the lexer reports every character it could not read, the parser reports only the first thing it could not fit into the grammar.
Two places in the grammar are settled by columns rather than by tokens, and they are what lets a
box be written as a stack of bindings with no in and a record as a block with no separators.
The value of a top-level let ends at the next line that starts no further right than the let
itself, so a continuation has to be indented. A record field's value ends at }, at a , or
;, or at the next name = that starts a later line. Both are carried down into the expression
parser as predicates, so they apply at whatever depth the expression has reached rather than only
at the top.
Function application binds tighter than every operator, and prefix - binds looser than
application, so -f x negates the call. :: is the one right-associative operator. |> gets no
node of its own: a |> f is parsed as the call f a, which is why nothing after this stage has a
case for a pipe.
A program is a chain of let and type declarations ending in an expression. One that ends on a
binding instead evaluates to Unit, and a type declaration with nothing after it is an error
rather than a program.
data class ParseResult
The surface tree, or nothing and the reason why.
The two fields are not independent: the parser stops at its first complaint, so a null expr
comes with exactly one diagnostic and a non-null one comes with none.
data class ParseResult(
/**
* The tree, or null when parsing gave up. Non-null means the whole token list parsed, `EOF`
* included, so nothing was left over.
*/
val expr: Expr?,
/** Empty when [expr] is non-null, and one error long when it is null. */
val diagnostics: List<Diagnostic>,
)
class Parser(private val tokens: List<Token>)
Recursive descent over a finished token list.
The list is expected to end with EOF, as the lexer's does: reading past the end hands back that
last token again rather than failing, so a program that stops in the middle of a form gets a
diagnostic about what was missing instead of an index error.
class Parser(private val tokens: List<Token>) {
private var cursor = 0
private val diagnostics = mutableListOf<Diagnostic>()
fun parse(): ParseResult
Parse the token list as one FieldML program, reporting a failure rather than throwing it.
The whole list has to be consumed: a token left over after the program's final expression is an error, not a place to stop.
body — 456 lines, from line 104
fun parse(): ParseResult =
try {
val expr = parseTopLevelExpression()
expect(TokenKind.EOF, "Expected end of input")
ParseResult(expr, diagnostics)
} catch (abort: CompileAbort) {
diagnostics += abort.diagnostic
ParseResult(null, diagnostics)
}
private fun parseTopLevelExpression(): Expr {
if (check(TokenKind.TYPE)) return parseTypeDecl()
if (!check(TokenKind.LET)) return parseExpression(Stop.eof())
val start = expect(TokenKind.LET, "Expected 'let'")
val name = expect(TokenKind.IDENT, "Expected a binding name")
val parameters = mutableListOf<String>()
while (check(TokenKind.IDENT)) parameters += advance().text
expect(TokenKind.EQ, "Expected '=' in let binding")
val value = parseExpression(Stop.topLevelBinding(start) + Stop.until(TokenKind.IN))
if (match(TokenKind.IN)) {
val body = parseExpression(Stop.eof())
return ELet(name.text, parameters, value, body, SourceRange(start.range.start, body.range.end))
}
val body = if (check(TokenKind.EOF)) {
EVar("Unit", value.range)
} else {
parseTopLevelExpression()
}
return ELet(name.text, parameters, value, body, SourceRange(start.range.start, body.range.end))
}
private fun parseTypeDecl(): Expr {
val start = expect(TokenKind.TYPE, "Expected 'type'")
val name = expect(TokenKind.IDENT, "Expected a type name")
val parameters = mutableListOf<String>()
while (check(TokenKind.IDENT)) {
parameters += advance().text
}
expect(TokenKind.EQ, "Expected '=' in type declaration")
val constructors = mutableListOf<ConstructorDecl>()
do {
match(TokenKind.BAR)
constructors += parseConstructorDecl(start)
} while (check(TokenKind.BAR))
if (constructors.isEmpty()) abort("Expected at least one constructor", name.range)
val declRange = SourceRange(start.range.start, constructors.last().range.end)
val body = if (check(TokenKind.EOF)) {
abort("Type declarations must be followed by an expression", current().range)
} else {
parseTopLevelExpression()
}
return ETypeDecl(
TypeDecl(name.text, parameters, constructors, declRange),
body,
SourceRange(start.range.start, body.range.end),
)
}
private fun parseConstructorDecl(typeStart: Token): ConstructorDecl {
val name = expect(TokenKind.IDENT, "Expected a constructor name")
if (!name.text.first().isUpperCase()) {
abort("Constructor names must start with an uppercase letter", name.range)
}
val arguments = mutableListOf<TypeRef>()
while (!check(TokenKind.BAR) && !check(TokenKind.EOF) && !isTopLevelBoundary(typeStart)) {
arguments += parseTypeRef()
}
val end = arguments.lastOrNull()?.range ?: name.range
return ConstructorDecl(name.text, arguments, SourceRange(name.range.start, end.end))
}
private fun parseTypeRef(): TypeRef {
val name = expect(TokenKind.IDENT, "Expected a type name")
return TypeRefName(name.text, name.range)
}
private fun parseExpression(stop: Stop): Expr =
when {
stop.matches(current(), this) -> abort("Expected an expression", current().range)
check(TokenKind.LET) -> parseLet(stop)
check(TokenKind.IF) -> parseIf(stop)
check(TokenKind.FUN) -> parseLambda(stop)
check(TokenKind.MATCH) -> parseMatch(stop)
else -> parsePipe(stop)
}
private fun parsePipe(stop: Stop): Expr {
var left = parseBinary(0, stop)
while (!stop.matches(current(), this) && match(TokenKind.PIPE_FORWARD)) {
val right = parseBinary(0, stop)
left = ECall(right, left, SourceRange.spanning(left.range, right.range))
}
return left
}
private fun parseLet(stop: Stop): Expr {
val start = expect(TokenKind.LET, "Expected 'let'")
val name = expect(TokenKind.IDENT, "Expected a binding name")
val parameters = mutableListOf<String>()
while (check(TokenKind.IDENT)) parameters += advance().text
expect(TokenKind.EQ, "Expected '=' in let binding")
val value = parseExpression(stop + Stop.until(TokenKind.IN))
if (!match(TokenKind.IN)) {
abort("Expected 'in' after local let binding", current().range)
}
val body = parseExpression(stop)
return ELet(name.text, parameters, value, body, SourceRange(start.range.start, body.range.end))
}
private fun parseIf(stop: Stop): Expr {
val start = expect(TokenKind.IF, "Expected 'if'")
val condition = parseExpression(stop + Stop.until(TokenKind.THEN))
expect(TokenKind.THEN, "Expected 'then'")
val thenBranch = parseExpression(stop + Stop.until(TokenKind.ELSE))
expect(TokenKind.ELSE, "Expected 'else'")
val elseBranch = parseExpression(stop)
return EIf(condition, thenBranch, elseBranch, SourceRange(start.range.start, elseBranch.range.end))
}
private fun parseLambda(stop: Stop): Expr {
val start = expect(TokenKind.FUN, "Expected 'fun'")
val parameters = mutableListOf<String>()
parameters += expect(TokenKind.IDENT, "Expected a lambda parameter").text
while (check(TokenKind.IDENT)) parameters += advance().text
expect(TokenKind.ARROW, "Expected '->' after lambda parameters")
val body = parseExpression(stop)
return ELambda(parameters, body, SourceRange(start.range.start, body.range.end))
}
private fun parseMatch(stop: Stop): Expr {
val start = expect(TokenKind.MATCH, "Expected 'match'")
val scrutinee = parseExpression(stop + Stop.until(TokenKind.WITH))
expect(TokenKind.WITH, "Expected 'with'")
val cases = mutableListOf<MatchCase>()
while (!stop.matches(current(), this) && !check(TokenKind.EOF)) {
match(TokenKind.BAR)
val pattern = parsePattern()
expect(TokenKind.ARROW, "Expected '->' after match pattern")
val body = parseExpression(stop + Stop.until(TokenKind.BAR))
cases += MatchCase(pattern, body, SourceRange(pattern.range.start, body.range.end))
if (!check(TokenKind.BAR)) break
}
if (cases.isEmpty()) abort("Expected at least one match case", current().range)
return EMatch(scrutinee, cases, SourceRange(start.range.start, cases.last().range.end))
}
private fun parsePattern(): Pattern {
val head = parseAppliedPattern()
if (!match(TokenKind.COLONCOLON)) return head
// Right associative: `a :: b :: rest`.
val tail = parsePattern()
return PCons(head, tail, SourceRange(head.range.start, tail.range.end))
}
private fun parseAppliedPattern(): Pattern {
val first = parsePatternAtom()
if (first is PConstructor) {
val arguments = mutableListOf<Pattern>()
while (startsPatternAtom(current().kind)) {
arguments += parsePatternAtom()
}
if (arguments.isNotEmpty()) {
return PConstructor(first.name, arguments, SourceRange(first.range.start, arguments.last().range.end))
}
}
return first
}
private fun parsePatternAtom(): Pattern {
val token = current()
return when {
match(TokenKind.INT) -> PInt(token.text.toLong(), token.range)
match(TokenKind.TRUE) -> PBool(true, token.range)
match(TokenKind.FALSE) -> PBool(false, token.range)
match(TokenKind.STRING) -> PString(decodeString(token.text), token.range)
match(TokenKind.IDENT) -> when {
token.text == "_" -> PWildcard(token.range)
token.text.first().isUpperCase() -> PConstructor(token.text, emptyList(), token.range)
else -> PVar(token.text, token.range)
}
match(TokenKind.LPAREN) -> parseTuplePattern(token)
match(TokenKind.LBRACKET) -> parseListPattern(token)
else -> abort("Expected a pattern", token.range)
}
}
private fun parseTuplePattern(open: Token): Pattern {
val first = parsePattern()
if (!match(TokenKind.COMMA)) {
expect(TokenKind.RPAREN, "Expected ')' after pattern")
return first
}
val elements = mutableListOf(first)
do {
elements += parsePattern()
} while (match(TokenKind.COMMA))
val close = expect(TokenKind.RPAREN, "Expected ')' after tuple pattern")
return PTuple(elements, SourceRange(open.range.start, close.range.end))
}
private fun parseListPattern(open: Token): Pattern {
val elements = mutableListOf<Pattern>()
if (!check(TokenKind.RBRACKET)) {
do {
elements += parsePattern()
} while (match(TokenKind.COMMA))
}
val close = expect(TokenKind.RBRACKET, "Expected ']' after list pattern")
return PList(elements, SourceRange(open.range.start, close.range.end))
}
private fun parseBinary(minPrecedence: Int, stop: Stop): Expr {
var left = parseUnary(stop)
while (!stop.matches(current(), this)) {
val op = binaryOp(current().kind) ?: break
val precedence = precedence(op)
if (precedence < minPrecedence) break
advance()
// `::` is right associative so `a :: b :: rest` builds the list the way it reads.
val right = parseBinary(if (op == BinaryOp.CONS) precedence else precedence + 1, stop)
left = EBinary(op, left, right, SourceRange.spanning(left.range, right.range))
}
return left
}
private fun parseUnary(stop: Stop): Expr {
if (!stop.matches(current(), this) && check(TokenKind.MINUS)) {
val start = advance()
val value = parseUnary(stop)
return ENegate(value, SourceRange(start.range.start, value.range.end))
}
return parseApplication(stop)
}
private fun parseApplication(stop: Stop): Expr {
var expr = parsePostfix(stop)
while (!stop.matches(current(), this) && startsAtom(current().kind)) {
val argument = parsePostfix(stop)
expr = ECall(expr, argument, SourceRange.spanning(expr.range, argument.range))
}
return expr
}
private fun parsePostfix(stop: Stop): Expr {
var expr = parseAtom(stop)
while (!stop.matches(current(), this) && match(TokenKind.DOT)) {
val field = expect(TokenKind.IDENT, "Expected field name after '.'")
expr = EField(expr, field.text, SourceRange(expr.range.start, field.range.end))
}
return expr
}
private fun parseAtom(stop: Stop): Expr {
if (stop.matches(current(), this)) abort("Expected an expression", current().range)
val token = current()
return when {
match(TokenKind.INT) -> EInt(token.text.toLong(), token.range)
match(TokenKind.FLOAT) -> EFloat(token.text.toDouble(), token.range)
match(TokenKind.TRUE) -> EBool(true, token.range)
match(TokenKind.FALSE) -> EBool(false, token.range)
match(TokenKind.STRING) -> EString(decodeString(token.text), token.range)
match(TokenKind.IDENT) -> EVar(token.text, token.range)
match(TokenKind.LPAREN) -> parseParenthesizedOrTuple(token)
match(TokenKind.LBRACKET) -> parseList(token)
match(TokenKind.LBRACE) -> parseRecord(token)
else -> abort("Expected an expression", token.range)
}
}
private fun parseParenthesizedOrTuple(open: Token): Expr {
// `()` is the unit value. It had no spelling until the host bridge needed one: `hostCall`'s
// n-ary form reads its arguments out of a tuple, and a call with no arguments at all has to be
// writable as something.
if (check(TokenKind.RPAREN)) {
val close = advance()
return EVar("Unit", SourceRange(open.range.start, close.range.end))
}
val first = parseExpression(Stop.until(TokenKind.COMMA) + Stop.until(TokenKind.RPAREN))
if (!match(TokenKind.COMMA)) {
expect(TokenKind.RPAREN, "Expected ')'")
return first
}
val elements = mutableListOf(first)
do {
elements += parseExpression(Stop.until(TokenKind.COMMA) + Stop.until(TokenKind.RPAREN))
} while (match(TokenKind.COMMA))
val close = expect(TokenKind.RPAREN, "Expected ')' after tuple")
return ETuple(elements, SourceRange(open.range.start, close.range.end))
}
private fun parseList(open: Token): Expr {
val elements = mutableListOf<Expr>()
if (!check(TokenKind.RBRACKET)) {
do {
elements += parseExpression(Stop.until(TokenKind.COMMA) + Stop.until(TokenKind.RBRACKET))
} while (match(TokenKind.COMMA))
}
val close = expect(TokenKind.RBRACKET, "Expected ']' after list")
return EList(elements, SourceRange(open.range.start, close.range.end))
}
private fun parseRecord(open: Token): Expr {
val fields = mutableListOf<RecordField>()
while (!check(TokenKind.RBRACE)) {
val name = expect(TokenKind.IDENT, "Expected record field name")
expect(TokenKind.EQ, "Expected '=' after record field name")
val value = parseExpression(Stop.recordField(name.range.start.line))
fields += RecordField(name.text, value, SourceRange(name.range.start, value.range.end))
match(TokenKind.COMMA) || match(TokenKind.SEMICOLON)
}
val close = expect(TokenKind.RBRACE, "Expected '}'")
val duplicate = fields.groupBy { it.name }.values.firstOrNull { it.size > 1 }?.last()
if (duplicate != null) abort("Duplicate record field '${duplicate.name}'", duplicate.range)
return ERecord(fields, SourceRange(open.range.start, close.range.end))
}
private fun startsAtom(kind: TokenKind): Boolean =
kind in setOf(
TokenKind.IDENT,
TokenKind.INT,
TokenKind.FLOAT,
TokenKind.STRING,
TokenKind.TRUE,
TokenKind.FALSE,
TokenKind.LPAREN,
TokenKind.LBRACKET,
TokenKind.LBRACE,
TokenKind.MATCH,
)
private fun startsPatternAtom(kind: TokenKind): Boolean =
kind in setOf(
TokenKind.IDENT,
TokenKind.INT,
TokenKind.STRING,
TokenKind.TRUE,
TokenKind.FALSE,
TokenKind.LPAREN,
TokenKind.LBRACKET,
)
private fun binaryOp(kind: TokenKind): BinaryOp? =
when (kind) {
TokenKind.PLUS -> BinaryOp.ADD
TokenKind.PLUSPLUS -> BinaryOp.CONCAT
TokenKind.MINUS -> BinaryOp.SUB
TokenKind.STAR -> BinaryOp.MUL
TokenKind.SLASH -> BinaryOp.DIV
TokenKind.COLONCOLON -> BinaryOp.CONS
TokenKind.LT -> BinaryOp.LT
TokenKind.LTE -> BinaryOp.LTE
TokenKind.GT -> BinaryOp.GT
TokenKind.GTE -> BinaryOp.GTE
TokenKind.EQEQ -> BinaryOp.EQ
TokenKind.NEQ -> BinaryOp.NEQ
TokenKind.AMPAMP -> BinaryOp.AND
TokenKind.BARBAR -> BinaryOp.OR
else -> null
}
private fun precedence(op: BinaryOp): Int =
when (op) {
BinaryOp.MUL, BinaryOp.DIV -> 6
BinaryOp.ADD, BinaryOp.SUB, BinaryOp.CONCAT -> 5
BinaryOp.CONS -> 4
BinaryOp.LT, BinaryOp.LTE, BinaryOp.GT, BinaryOp.GTE, BinaryOp.EQ, BinaryOp.NEQ -> 3
BinaryOp.AND -> 2
BinaryOp.OR -> 1
}
private fun decodeString(text: String): String {
val raw = text.substring(1, text.length - 1)
val out = StringBuilder()
var i = 0
while (i < raw.length) {
val c = raw[i++]
if (c != '\\' || i >= raw.length) {
out.append(c)
} else {
out.append(
when (val escaped = raw[i++]) {
'n' -> '\n'
'r' -> '\r'
't' -> '\t'
'"' -> '"'
'\\' -> '\\'
else -> escaped
},
)
}
}
return out.toString()
}
private fun match(kind: TokenKind): Boolean {
if (!check(kind)) return false
advance()
return true
}
private fun expect(kind: TokenKind, message: String): Token {
if (check(kind)) return advance()
abort("$message, found ${current().kind}", current().range)
}
private fun abort(message: String, range: SourceRange): Nothing {
throw CompileAbort(Diagnostic(Severity.Error, message, range))
}
private fun check(kind: TokenKind): Boolean = current().kind == kind
private fun current(): Token = tokens[cursor.coerceAtMost(tokens.lastIndex)]
private fun peek(): Token = tokens[(cursor + 1).coerceAtMost(tokens.lastIndex)]
private fun advance(): Token = current().also { cursor++ }
private fun isTopLevelBoundary(start: Token): Boolean =
current().range.start.line > start.range.start.line &&
current().range.start.column <= start.range.start.column &&
current().kind !in setOf(TokenKind.BAR, TokenKind.EOF)
private data class Stop(
val predicates: List<(Token, Parser) -> Boolean>,
) {
fun matches(token: Token, parser: Parser): Boolean = predicates.any { it(token, parser) }
operator fun plus(other: Stop): Stop = Stop(predicates + other.predicates)
companion object {
fun eof(): Stop = until(TokenKind.EOF)
fun until(kind: TokenKind): Stop = Stop(listOf({ token, _ -> token.kind == kind }))
fun topLevelBinding(letToken: Token): Stop =
Stop(
listOf(
{ token, _ -> token.kind == TokenKind.EOF },
{ token, _ ->
token.range.start.line > letToken.range.start.line &&
token.range.start.column <= letToken.range.start.column
},
),
)
fun recordField(fieldLine: Int): Stop =
Stop(
listOf(
{ token, _ -> token.kind in setOf(TokenKind.RBRACE, TokenKind.COMMA, TokenKind.SEMICOLON) },
{ token, parser ->
token.kind == TokenKind.IDENT &&
parser.peek().kind == TokenKind.EQ &&
token.range.start.line > fieldLine
},
),
)
}
}
}