416 lines
8.7 KiB
C#
416 lines
8.7 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Collections.Generic;
|
|
using Finn.AST;
|
|
|
|
namespace Finn;
|
|
|
|
class Program
|
|
{
|
|
private static readonly Resolver resolver = new Resolver();
|
|
private static readonly Interpreter interpreter = new Interpreter(resolver);
|
|
static bool hadError = false;
|
|
static bool hadRuntimeError = true;
|
|
|
|
static void Main(string[] args)
|
|
{
|
|
if (args.Length > 1)
|
|
{
|
|
Console.WriteLine("Usage: finn [script]");
|
|
Environment.Exit(64);
|
|
}
|
|
else if (args.Length == 1)
|
|
{
|
|
runFile(args[0]);
|
|
}
|
|
else
|
|
{
|
|
runPrompt();
|
|
}
|
|
}
|
|
|
|
static void runFile(string path)
|
|
{
|
|
var src = File.ReadAllText(path);
|
|
run(src);
|
|
|
|
if (hadError) Environment.Exit(65);
|
|
if (hadRuntimeError) Environment.Exit(70);
|
|
}
|
|
|
|
static void run(string src)
|
|
{
|
|
var scanner = new Scanner(src);
|
|
List<Token> tokens = scanner.scanTokens();
|
|
Parser parser = new Parser(tokens);
|
|
Expr? expression = parser.parse();
|
|
|
|
if (hadError)
|
|
{
|
|
hadError = false;
|
|
return;
|
|
}
|
|
|
|
resolver.Add(expression!);
|
|
if (hadError)
|
|
{
|
|
hadError = false;
|
|
return;
|
|
}
|
|
|
|
interpreter.Interpret(expression!);
|
|
}
|
|
|
|
static void runPrompt()
|
|
{
|
|
var input = new BufferedStream(Console.OpenStandardInput());
|
|
var reader = new StreamReader(input);
|
|
|
|
while (true)
|
|
{
|
|
Console.Write("> ");
|
|
var line = reader.ReadLine();
|
|
if (line == null) break;
|
|
run(line);
|
|
}
|
|
}
|
|
|
|
public static void error(Position position, String message)
|
|
{
|
|
report(position, "", message);
|
|
}
|
|
|
|
public static void error(Token token, String message)
|
|
{
|
|
if (token.Type == TokenType.EOF)
|
|
{
|
|
report(token.Start, " at end", message);
|
|
}
|
|
else
|
|
{
|
|
report(token.Start, " at '" + token.Lexeme + "'", message);
|
|
}
|
|
}
|
|
|
|
public static void runtimeError(RuntimeError err)
|
|
{
|
|
Console.Error.WriteLine($"{err.Message}\n[{err.Token.Start}]");
|
|
hadRuntimeError = true;
|
|
}
|
|
|
|
static void report(Position position, String where, String message)
|
|
{
|
|
Console.WriteLine($"[{position}] Error{where}: {message}");
|
|
hadError = true;
|
|
}
|
|
}
|
|
|
|
public enum TokenType
|
|
{
|
|
LParen, RParen,
|
|
LBrace, RBrace,
|
|
LBracket, RBracket,
|
|
Less, Greater,
|
|
Pipe,
|
|
Colon,
|
|
Tick,
|
|
Backtick,
|
|
At,
|
|
Comma,
|
|
Semicolon,
|
|
Period,
|
|
Equal,
|
|
Plus, Minus, Asterisk, Slash,
|
|
PlusPlus,
|
|
DoubleEqual,
|
|
Bang, BangEqual,
|
|
LessEqual, GreaterEqual,
|
|
SingleArrow,
|
|
DoubleArrow,
|
|
If, Then, Else,
|
|
When,
|
|
Is,
|
|
Let,
|
|
And,
|
|
In,
|
|
With,
|
|
Fn,
|
|
Type,
|
|
Alias,
|
|
Recurse,
|
|
Def,
|
|
Blank,
|
|
Identifier, QuotedIdentifier, Number, String,
|
|
EOF
|
|
}
|
|
|
|
public record struct Position(int Offset, int Line, int Column)
|
|
{
|
|
public override string ToString()
|
|
{
|
|
return $"{this.Line}:{this.Column}";
|
|
}
|
|
|
|
public static Position operator ++(Position p)
|
|
{
|
|
return p.Next();
|
|
}
|
|
|
|
public Position Next()
|
|
{
|
|
return this with { Offset = Offset + 1, Column = Column + 1 };
|
|
}
|
|
|
|
public Position NewLine()
|
|
{
|
|
return this with { Line = Line + 1, Column = 1 };
|
|
}
|
|
}
|
|
|
|
public record Token(TokenType Type, String Lexeme, Object? Literal, Position Start, Position End);
|
|
|
|
class Scanner
|
|
{
|
|
private readonly String source;
|
|
private readonly List<Token> tokens = new List<Token>();
|
|
private Position start = new(0, 1, 1);
|
|
private Position current = new(0, 1, 1);
|
|
|
|
private static readonly Dictionary<String, TokenType> keywords = new Dictionary<string, TokenType>()
|
|
{
|
|
{"def", TokenType.Def},
|
|
{"if", TokenType.If},
|
|
{"then", TokenType.Then},
|
|
{"else", TokenType.Else},
|
|
{"when", TokenType.When},
|
|
{"is", TokenType.Is},
|
|
{"let", TokenType.Let},
|
|
{"and", TokenType.And},
|
|
{"in", TokenType.In},
|
|
{"with", TokenType.With},
|
|
{"fn", TokenType.Fn},
|
|
{"type", TokenType.Type},
|
|
{"alias", TokenType.Alias},
|
|
{"recurse", TokenType.Recurse},
|
|
{"_", TokenType.Blank},
|
|
};
|
|
|
|
public Scanner(String source)
|
|
{
|
|
this.source = source;
|
|
}
|
|
|
|
private bool isAtEnd()
|
|
{
|
|
return current.Offset >= source.Length;
|
|
}
|
|
|
|
private char advance()
|
|
{
|
|
return source[(current++).Offset];
|
|
}
|
|
|
|
private void addToken(TokenType type)
|
|
{
|
|
addToken(type, null);
|
|
}
|
|
|
|
private string lexeme
|
|
{
|
|
get => source.Substring(start.Offset, current.Offset - start.Offset);
|
|
}
|
|
|
|
private void addToken(TokenType type, Object? literal)
|
|
{
|
|
tokens.Add(new Token(type, lexeme, literal, start, current));
|
|
}
|
|
|
|
private bool match(char expected)
|
|
{
|
|
if (isAtEnd()) return false;
|
|
if (source[current.Offset] != expected) return false;
|
|
|
|
current++;
|
|
return true;
|
|
}
|
|
|
|
private bool match(Predicate<char> pred)
|
|
{
|
|
if (isAtEnd()) return false;
|
|
if (!pred(source[current.Offset])) return false;
|
|
|
|
current++;
|
|
return true;
|
|
}
|
|
|
|
private char? peek()
|
|
{
|
|
if (isAtEnd()) return null;
|
|
return source[current.Offset];
|
|
}
|
|
|
|
private string? _stringLiteral(string errorName)
|
|
{
|
|
var valueStart = current.Offset;
|
|
while (peek() != '"' && !isAtEnd())
|
|
{
|
|
if (peek() == '\n')
|
|
current = current.NewLine();
|
|
advance();
|
|
}
|
|
|
|
if (isAtEnd())
|
|
{
|
|
Program.error(current, $"Unterminated {errorName}.");
|
|
return null;
|
|
}
|
|
|
|
// The closing ".
|
|
advance();
|
|
|
|
// Trim the closing quote.
|
|
return source.Substring(valueStart, current.Offset - valueStart - 1);
|
|
}
|
|
|
|
private void quotedIdentifier()
|
|
{
|
|
if (!match('"'))
|
|
{
|
|
Program.error(current, "Expect \" after @.");
|
|
}
|
|
var value = _stringLiteral("quoted identifier");
|
|
if (value == null) return;
|
|
addToken(TokenType.QuotedIdentifier, value);
|
|
}
|
|
|
|
private void stringLiteral()
|
|
{
|
|
var value = _stringLiteral("string");
|
|
if (value == null) return;
|
|
addToken(TokenType.String, value);
|
|
}
|
|
|
|
private bool isDigitOrSeparator(char c)
|
|
{
|
|
return Char.IsAsciiDigit(c) || c == '_';
|
|
}
|
|
|
|
private char? peekNext()
|
|
{
|
|
if (current.Offset + 1 >= source.Length) return null;
|
|
return source[current.Offset + 1];
|
|
}
|
|
private void numberLiteral()
|
|
{
|
|
while (match(isDigitOrSeparator)) ;
|
|
|
|
// Look for a fractional part.
|
|
if (peek() == '.' && isDigitOrSeparator(peekNext() ?? '\0'))
|
|
{
|
|
match('.');
|
|
while (match(isDigitOrSeparator)) ;
|
|
}
|
|
double value = Double.Parse(source.Substring(start.Offset, current.Offset - start.Offset));
|
|
addToken(TokenType.Number, value);
|
|
}
|
|
|
|
private bool isIdentifierStartChar(char c)
|
|
{
|
|
return Char.IsAsciiLetter(c) || c == '_';
|
|
}
|
|
|
|
private bool isIdentifierChar(char c)
|
|
{
|
|
return isIdentifierStartChar(c) || Char.IsAsciiDigit(c);
|
|
}
|
|
|
|
private void identifier()
|
|
{
|
|
while (match(isIdentifierChar)) ;
|
|
String text = source.Substring(start.Offset, current.Offset - start.Offset);
|
|
TokenType type;
|
|
var isKeyword = keywords.TryGetValue(text, out type);
|
|
addToken(isKeyword ? type : TokenType.Identifier, text);
|
|
}
|
|
|
|
private void scanToken()
|
|
{
|
|
char c = advance();
|
|
switch (c)
|
|
{
|
|
case '(': addToken(TokenType.LParen); break;
|
|
case ')': addToken(TokenType.RParen); break;
|
|
case '{': addToken(TokenType.LBrace); break;
|
|
case '}': addToken(TokenType.RBrace); break;
|
|
case '[': addToken(TokenType.LBracket); break;
|
|
case ']': addToken(TokenType.RBracket); break;
|
|
case ':': addToken(TokenType.Colon); break;
|
|
case '\'': addToken(TokenType.Tick); break;
|
|
case '`': addToken(TokenType.Backtick); break;
|
|
case ',': addToken(TokenType.Comma); break;
|
|
case ';': addToken(TokenType.Semicolon); break;
|
|
case '.': addToken(TokenType.Period); break;
|
|
case '*': addToken(TokenType.Asterisk); break;
|
|
case '/': addToken(TokenType.Slash); break;
|
|
case '|': addToken(TokenType.Pipe); break;
|
|
case '-':
|
|
addToken(match('>') ? TokenType.SingleArrow : TokenType.Minus);
|
|
break;
|
|
case '!':
|
|
addToken(match('=') ? TokenType.BangEqual : TokenType.Bang);
|
|
break;
|
|
case '+':
|
|
addToken(match('+') ? TokenType.PlusPlus : TokenType.Plus);
|
|
break;
|
|
case '=':
|
|
addToken(match('=') ? TokenType.DoubleEqual :
|
|
match('>') ? TokenType.DoubleArrow :
|
|
TokenType.Equal);
|
|
break;
|
|
case '<':
|
|
addToken(match('=') ? TokenType.LessEqual : TokenType.Less);
|
|
break;
|
|
case '>':
|
|
addToken(match('=') ? TokenType.GreaterEqual : TokenType.Greater);
|
|
break;
|
|
case '@': quotedIdentifier(); break;
|
|
case '"': stringLiteral(); break;
|
|
case '#':
|
|
while (peek() != '\n' && !isAtEnd()) advance();
|
|
break;
|
|
case ' ':
|
|
case '\r':
|
|
case '\t':
|
|
break;
|
|
case '\n':
|
|
current = current.NewLine();
|
|
break;
|
|
default:
|
|
if (Char.IsAsciiDigit(c))
|
|
{
|
|
numberLiteral();
|
|
}
|
|
else if (isIdentifierStartChar(c))
|
|
{
|
|
identifier();
|
|
}
|
|
else
|
|
{
|
|
Program.error(current, "Unexpected character.");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
public List<Token> scanTokens()
|
|
{
|
|
while (!isAtEnd())
|
|
{
|
|
start = current;
|
|
scanToken();
|
|
}
|
|
|
|
tokens.Add(new Token(TokenType.EOF, "", null, current, current));
|
|
return tokens;
|
|
}
|
|
}
|