finn-lang/Program.cs

416 lines
8.8 KiB
C#

using System;
using System.IO;
using System.Collections.Generic;
using Finn.AST;
namespace Finn;
class Program
{
private static readonly Interpreter interpreter = new Interpreter();
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();
Console.WriteLine("TOKENS\n=======");
foreach (var token in tokens)
{
Console.WriteLine(token);
}
Parser parser = new Parser(tokens);
Expr? expression = parser.parse();
Console.WriteLine("\nAST\n=======");
Console.WriteLine(expression);
Console.WriteLine();
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.Position, " at end", message);
}
else
{
report(token.Position, " at '" + token.Lexeme + "'", message);
}
}
public static void runtimeError(RuntimeError err)
{
Console.Error.WriteLine($"{err.Message}\n[{err.Token.Position}]");
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 Position);
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));
}
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));
return tokens;
}
}