507 lines
10 KiB
C#
507 lines
10 KiB
C#
namespace Finn;
|
|
|
|
using Finn.AST;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
|
|
class Parser
|
|
{
|
|
private class ParseError : Exception { }
|
|
|
|
private readonly List<Token> tokens;
|
|
private int current = 0;
|
|
|
|
public Parser(List<Token> tokens)
|
|
{
|
|
this.tokens = tokens;
|
|
}
|
|
|
|
public Expr? parse()
|
|
{
|
|
try
|
|
{
|
|
return expression();
|
|
}
|
|
catch (ParseError)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Parsing primitives
|
|
|
|
private bool match(params TokenType[] types)
|
|
{
|
|
foreach (TokenType type in types)
|
|
{
|
|
if (check(type))
|
|
{
|
|
advance();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool check(params TokenType[] types)
|
|
{
|
|
if (isAtEnd()) return false;
|
|
return Array.IndexOf(types, peek().Type) >= 0;
|
|
}
|
|
|
|
private Token advance()
|
|
{
|
|
if (!isAtEnd()) current++;
|
|
return previous();
|
|
}
|
|
|
|
private bool isAtEnd()
|
|
{
|
|
return peek().Type == TokenType.EOF;
|
|
}
|
|
|
|
private Token peek()
|
|
{
|
|
return tokens[current];
|
|
}
|
|
|
|
private Token previous()
|
|
{
|
|
return tokens[current - 1];
|
|
}
|
|
|
|
private Token consume(string message, params TokenType[] types)
|
|
{
|
|
if (check(types)) return advance();
|
|
throw error(peek(), message);
|
|
}
|
|
|
|
private ParseError error(Token token, string message)
|
|
{
|
|
Program.error(token, message);
|
|
return new ParseError();
|
|
}
|
|
|
|
private void synchronize()
|
|
{
|
|
advance();
|
|
|
|
while (!isAtEnd())
|
|
{
|
|
switch (peek().Type)
|
|
{
|
|
case TokenType.Def:
|
|
case TokenType.Type:
|
|
return;
|
|
}
|
|
|
|
advance();
|
|
}
|
|
}
|
|
|
|
// Helpers
|
|
|
|
private Expr binaryLeft(Func<Expr> next, params TokenType[] types)
|
|
{
|
|
Expr expr = next();
|
|
|
|
while (match(types))
|
|
{
|
|
Token op = previous();
|
|
Expr right = next();
|
|
expr = new Binary(expr, op, right);
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
// Rules
|
|
|
|
private Expr expression()
|
|
{
|
|
var expr = equality();
|
|
while (match(TokenType.Semicolon))
|
|
{
|
|
var right = equality();
|
|
expr = new Sequence(expr, right);
|
|
}
|
|
return expr;
|
|
}
|
|
|
|
private Expr equality() =>
|
|
binaryLeft(comparison, TokenType.BangEqual, TokenType.DoubleEqual);
|
|
|
|
private Expr comparison() =>
|
|
binaryLeft(sum, TokenType.Greater, TokenType.GreaterEqual, TokenType.Less, TokenType.LessEqual);
|
|
|
|
private Expr sum() => binaryLeft(product, TokenType.Minus, TokenType.Plus);
|
|
|
|
private Expr product() => binaryLeft(concat, TokenType.Slash, TokenType.Asterisk);
|
|
|
|
private Expr concat() => binaryLeft(unary, TokenType.PlusPlus);
|
|
|
|
private Expr unary()
|
|
{
|
|
if (match(TokenType.Bang, TokenType.Minus))
|
|
{
|
|
Token op = previous();
|
|
Expr right = unary();
|
|
return new Unary(op, right);
|
|
}
|
|
return let();
|
|
}
|
|
|
|
private RecordPattern? recordPattern()
|
|
{
|
|
if (!match(TokenType.LBrace))
|
|
{
|
|
return null;
|
|
}
|
|
var lbrace = previous();
|
|
|
|
List<FieldPattern> fields = new List<FieldPattern>();
|
|
while (!check(TokenType.RBrace, TokenType.Pipe))
|
|
{
|
|
var fieldName = consume("Expect identifier as field name.", TokenType.Identifier, TokenType.QuotedIdentifier);
|
|
var pat = match(TokenType.Equal) ? pattern() : null;
|
|
fields.Add(new(fieldName, pat));
|
|
if (!match(TokenType.Comma))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
var restPattern = match(TokenType.Pipe) ? simplePattern() : null;
|
|
consume("Expect '}' at end of record pattern.", TokenType.RBrace);
|
|
return new(fields.ToArray(), restPattern, lbrace);
|
|
}
|
|
|
|
private VariantPattern? variantPattern()
|
|
{
|
|
if (!match(TokenType.Backtick))
|
|
{
|
|
return null;
|
|
}
|
|
var backtick = previous();
|
|
var tag = consume("Expect identifier as tag name.", TokenType.Identifier, TokenType.QuotedIdentifier);
|
|
if (!match(TokenType.LParen))
|
|
{
|
|
return new(tag, null, backtick);
|
|
}
|
|
Pattern argument = pattern();
|
|
consume("Expect ')' after variant argument.", TokenType.RParen);
|
|
return new(tag, argument, backtick);
|
|
}
|
|
|
|
private SimplePattern? simplePattern()
|
|
{
|
|
if (match(TokenType.Blank))
|
|
{
|
|
return new SimplePattern(null, previous());
|
|
}
|
|
|
|
return match(TokenType.Identifier, TokenType.QuotedIdentifier) ? new(previous(), previous()) : null;
|
|
}
|
|
|
|
private Pattern pattern()
|
|
{
|
|
Pattern? p;
|
|
if ((p = simplePattern()) != null)
|
|
{
|
|
return p;
|
|
}
|
|
if ((p = recordPattern()) != null)
|
|
{
|
|
return p;
|
|
}
|
|
if ((p = variantPattern()) != null)
|
|
{
|
|
return p;
|
|
}
|
|
if (check(TokenType.String))
|
|
{
|
|
throw error(peek(), "Cannot pattern-match string literals.");
|
|
}
|
|
if (check(TokenType.Number))
|
|
{
|
|
throw error(peek(), "Cannot pattern-match numbers.");
|
|
}
|
|
throw error(peek(), "Expect pattern.");
|
|
}
|
|
|
|
private Expr let()
|
|
{
|
|
if (!match(TokenType.Let))
|
|
{
|
|
return ifExpr();
|
|
}
|
|
var letToken = previous();
|
|
|
|
List<Binding> bindings = new List<Binding>();
|
|
bindings.Add(parseBinding());
|
|
while (match(TokenType.And))
|
|
{
|
|
bindings.Add(parseBinding());
|
|
}
|
|
consume("Expect 'in' after let-bindings.", TokenType.In);
|
|
Expr body = expression();
|
|
return new Let(bindings.ToArray(), body, letToken);
|
|
|
|
Binding parseBinding()
|
|
{
|
|
Pattern p = pattern();
|
|
switch (p)
|
|
{
|
|
case (SimplePattern(var funcName)) when funcName != null && match(TokenType.LParen):
|
|
List<Pattern> funcParams = new List<Pattern>();
|
|
while (!check(TokenType.RParen))
|
|
{
|
|
funcParams.Add(pattern());
|
|
if (!match(TokenType.Comma))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
consume("Expect ')' at end of parameters.", TokenType.RParen);
|
|
consume("Expect '=' after parameters.", TokenType.Equal);
|
|
return new FuncBinding(funcName, funcParams.ToArray(), expression());
|
|
default:
|
|
consume("Expect '=' after pattern.", TokenType.Equal);
|
|
return new VarBinding(p, expression());
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
private Expr ifExpr()
|
|
{
|
|
if (!match(TokenType.If))
|
|
{
|
|
return when();
|
|
}
|
|
var ifToken = previous();
|
|
Expr condition = expression();
|
|
consume("Expect 'then' after condition.", TokenType.Then);
|
|
Expr thenCase = expression();
|
|
consume("Expect 'else' after 'then' case.", TokenType.Else);
|
|
Expr elseCase = expression();
|
|
return new If(condition, thenCase, elseCase, ifToken);
|
|
}
|
|
|
|
private Expr when()
|
|
{
|
|
if (!match(TokenType.When))
|
|
{
|
|
return primary();
|
|
}
|
|
var whenToken = previous();
|
|
Expr head = expression();
|
|
consume("Expect 'is' after expression.", TokenType.Is);
|
|
|
|
List<VarBinding> cases = new List<VarBinding>();
|
|
cases.Add(parseCase());
|
|
while (match(TokenType.Comma))
|
|
{
|
|
cases.Add(parseCase());
|
|
}
|
|
return new When(head, cases.ToArray(), whenToken);
|
|
|
|
VarBinding parseCase()
|
|
{
|
|
Pattern pat = pattern();
|
|
consume("Expect '=>' after pattern.", TokenType.DoubleArrow);
|
|
Expr value = expression();
|
|
return new VarBinding(pat, value);
|
|
}
|
|
}
|
|
|
|
private Expr primary()
|
|
{
|
|
Expr expr = operand();
|
|
|
|
if (match(TokenType.Period))
|
|
{
|
|
var ident = consume("Expect identifier after dot.", TokenType.Identifier, TokenType.QuotedIdentifier);
|
|
return new Selector(expr, ident);
|
|
}
|
|
|
|
if (match(TokenType.LBracket))
|
|
{
|
|
var index = expression();
|
|
consume("Expect '[' after expression.", TokenType.RBracket);
|
|
return new Indexer(expr, index);
|
|
}
|
|
|
|
if (match(TokenType.LParen))
|
|
{
|
|
return finishCall(expr);
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
private Expr finishCall(Expr callee)
|
|
{
|
|
var lParen = previous();
|
|
var args = new List<Expr?>();
|
|
while (!check(TokenType.RParen))
|
|
{
|
|
if (match(TokenType.Blank))
|
|
{
|
|
args.Add(null);
|
|
}
|
|
else
|
|
{
|
|
args.Add(expression());
|
|
}
|
|
if (!match(TokenType.Comma))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
consume("Expect ')' after arguments.", TokenType.RParen);
|
|
|
|
const int MaxArgs = 25;
|
|
if (args.Count > MaxArgs)
|
|
{
|
|
error(peek(), $"Can't have more than {MaxArgs} arguments.");
|
|
}
|
|
return new Call(callee, args.ToArray(), lParen);
|
|
}
|
|
|
|
private Expr operand()
|
|
{
|
|
if (match(TokenType.Number, TokenType.String))
|
|
{
|
|
var token = previous();
|
|
object literal = token.Literal!;
|
|
return new Literal(previous().Literal!, token);
|
|
}
|
|
|
|
if (match(TokenType.Identifier, TokenType.QuotedIdentifier))
|
|
{
|
|
return new Variable(previous());
|
|
}
|
|
|
|
if (match(TokenType.LParen))
|
|
{
|
|
var lparen = previous();
|
|
Expr groupedExpr = expression();
|
|
consume("Expect ')' after expression.", TokenType.RParen);
|
|
return new Grouping(groupedExpr, lparen);
|
|
}
|
|
|
|
Expr? expr;
|
|
if ((expr = record()) != null)
|
|
{
|
|
return expr;
|
|
}
|
|
if ((expr = variant()) != null)
|
|
{
|
|
return expr;
|
|
}
|
|
if ((expr = list()) != null)
|
|
{
|
|
return expr;
|
|
}
|
|
|
|
throw error(peek(), "Expect expression.");
|
|
}
|
|
|
|
private Expr? list()
|
|
{
|
|
if (!match(TokenType.LBracket))
|
|
{
|
|
return null;
|
|
}
|
|
var lbracket = previous();
|
|
|
|
List<Expr> elements = new List<Expr>();
|
|
|
|
if (!match(TokenType.RBracket))
|
|
{
|
|
elements.Add(expression());
|
|
while (!match(TokenType.RBracket))
|
|
{
|
|
if (match(TokenType.Comma))
|
|
{
|
|
if (match(TokenType.RBracket))
|
|
{
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
elements.Add(expression());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw error(previous(), "Expect comma between list elements.");
|
|
}
|
|
}
|
|
}
|
|
return new List(elements.ToArray(), lbracket);
|
|
}
|
|
|
|
private Expr? variant()
|
|
{
|
|
if (!match(TokenType.Backtick))
|
|
{
|
|
return null;
|
|
}
|
|
var backtick = previous();
|
|
var tag = consume("Expect identifier as tag name.", TokenType.QuotedIdentifier, TokenType.Identifier);
|
|
Expr? argument = null;
|
|
if (match(TokenType.LParen))
|
|
{
|
|
if (!match(TokenType.RParen))
|
|
{
|
|
argument = expression();
|
|
consume("Expect ')' after variant argument.", TokenType.RParen);
|
|
}
|
|
}
|
|
return new Variant(tag, argument, backtick);
|
|
}
|
|
|
|
private Expr? record()
|
|
{
|
|
if (!match(TokenType.LBrace))
|
|
{
|
|
return null;
|
|
}
|
|
var lbrace = previous();
|
|
|
|
var extensions = parseFields(TokenType.RBrace, TokenType.Pipe);
|
|
BaseRecord? baseRecord = null;
|
|
|
|
if (match(TokenType.Pipe))
|
|
{
|
|
var baseExpr = expression();
|
|
var updates = match(TokenType.With) ? parseFields(TokenType.RBrace) : Array.Empty<Field>();
|
|
baseRecord = new(baseExpr, updates);
|
|
}
|
|
|
|
consume("Expect '}' at end of record literal.", TokenType.RBrace);
|
|
return new Record(extensions, baseRecord, lbrace);
|
|
|
|
Field[] parseFields(params TokenType[] endAt)
|
|
{
|
|
List<Field> fields = new List<Field>();
|
|
|
|
while (!check(endAt))
|
|
{
|
|
var fieldName = consume("Expect identifier as field name.", TokenType.Identifier, TokenType.QuotedIdentifier);
|
|
var value = match(TokenType.Equal) ? expression() : null;
|
|
fields.Add(new(fieldName, value));
|
|
if (!match(TokenType.Comma))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return fields.ToArray();
|
|
}
|
|
}
|
|
}
|