finn-lang/Program.cs

416 lines
8.8 KiB
C#
Raw Normal View History

2023-06-08 13:26:41 +00:00
using System;
using System.IO;
using System.Collections.Generic;
2023-06-28 18:12:14 +00:00
using Finn.AST;
2023-06-08 13:26:41 +00:00
namespace Finn;
class Program
{
2023-07-06 07:11:00 +00:00
private static readonly Interpreter interpreter = new Interpreter();
2023-06-08 13:26:41 +00:00
static bool hadError = false;
2023-07-06 07:11:00 +00:00
static bool hadRuntimeError = true;
2023-06-08 13:26:41 +00:00
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);
2023-07-06 07:11:00 +00:00
if (hadError) Environment.Exit(65);
if (hadRuntimeError) Environment.Exit(70);
2023-06-08 13:26:41 +00:00
}
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);
}
2023-06-28 18:12:14 +00:00
Parser parser = new Parser(tokens);
Expr? expression = parser.parse();
Console.WriteLine("\nAST\n=======");
Console.WriteLine(expression);
Console.WriteLine();
2023-06-08 13:26:41 +00:00
2023-07-06 07:11:00 +00:00
if (hadError)
2023-07-02 17:38:48 +00:00
{
hadError = false;
return;
}
2023-06-28 18:12:14 +00:00
2023-07-06 07:11:00 +00:00
interpreter.Interpret(expression!);
2023-06-08 13:26:41 +00:00
}
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);
}
}
2023-07-17 01:47:42 +00:00
public static void error(Position position, String message)
2023-06-08 13:26:41 +00:00
{
2023-07-17 01:47:42 +00:00
report(position, "", message);
2023-06-08 13:26:41 +00:00
}
2023-06-28 18:12:14 +00:00
public static void error(Token token, String message)
{
2023-07-25 19:19:07 +00:00
if (token.Type == TokenType.EOF)
2023-06-28 18:12:14 +00:00
{
2023-07-25 19:19:07 +00:00
report(token.Position, " at end", message);
2023-06-28 18:12:14 +00:00
}
else
{
2023-07-25 19:19:07 +00:00
report(token.Position, " at '" + token.Lexeme + "'", message);
2023-06-28 18:12:14 +00:00
}
}
public static void runtimeError(RuntimeError err)
2023-07-06 07:11:00 +00:00
{
2023-07-25 19:19:07 +00:00
Console.Error.WriteLine($"{err.Message}\n[{err.Token.Position}]");
2023-07-06 07:11:00 +00:00
hadRuntimeError = true;
}
2023-07-17 01:47:42 +00:00
static void report(Position position, String where, String message)
2023-06-08 13:26:41 +00:00
{
2023-07-17 01:47:42 +00:00
Console.WriteLine($"[{position}] Error{where}: {message}");
2023-06-08 13:26:41 +00:00
hadError = true;
}
}
2023-06-08 14:16:32 +00:00
public enum TokenType
2023-06-08 13:26:41 +00:00
{
LParen, RParen,
LBrace, RBrace,
LBracket, RBracket,
Less, Greater,
Pipe,
Colon,
Tick,
Backtick,
At,
2023-06-08 13:26:41 +00:00
Comma,
2023-06-28 23:18:31 +00:00
Semicolon,
2023-06-08 13:26:41 +00:00
Period,
Equal,
Plus, Minus, Asterisk, Slash,
2023-06-08 14:00:26 +00:00
PlusPlus,
2023-06-08 13:26:41 +00:00
DoubleEqual,
Bang, BangEqual,
LessEqual, GreaterEqual,
SingleArrow,
DoubleArrow,
2023-06-28 22:26:08 +00:00
If, Then, Else,
2023-06-08 13:26:41 +00:00
When,
Is,
Let,
2023-06-28 21:53:04 +00:00
And,
2023-06-08 13:26:41 +00:00
In,
With,
Fn,
Type,
Alias,
Recurse,
2023-06-28 18:12:14 +00:00
Def,
2023-06-08 13:26:41 +00:00
Blank,
Identifier, QuotedIdentifier, Number, String,
2023-06-08 13:26:41 +00:00
EOF
}
2023-07-17 01:47:42 +00:00
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 };
}
}
2023-07-25 19:19:07 +00:00
public record Token(TokenType Type, String Lexeme, Object? Literal, Position Position);
2023-06-08 13:26:41 +00:00
class Scanner
{
private readonly String source;
private readonly List<Token> tokens = new List<Token>();
2023-07-17 01:47:42 +00:00
private Position start = new(0, 1, 1);
private Position current = new(0, 1, 1);
2023-06-08 13:26:41 +00:00
private static readonly Dictionary<String, TokenType> keywords = new Dictionary<string, TokenType>()
{
2023-06-28 18:12:14 +00:00
{"def", TokenType.Def},
2023-06-28 22:26:08 +00:00
{"if", TokenType.If},
{"then", TokenType.Then},
{"else", TokenType.Else},
2023-06-08 13:26:41 +00:00
{"when", TokenType.When},
{"is", TokenType.Is},
{"let", TokenType.Let},
2023-06-28 21:53:04 +00:00
{"and", TokenType.And},
2023-06-08 13:26:41 +00:00
{"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()
{
2023-07-17 01:47:42 +00:00
return current.Offset >= source.Length;
2023-06-08 13:26:41 +00:00
}
private char advance()
{
2023-07-17 01:47:42 +00:00
return source[(current++).Offset];
2023-06-08 13:26:41 +00:00
}
private void addToken(TokenType type)
{
addToken(type, null);
}
private string lexeme
{
get => source.Substring(start.Offset, current.Offset - start.Offset);
}
2023-06-08 13:26:41 +00:00
private void addToken(TokenType type, Object? literal)
{
tokens.Add(new Token(type, lexeme, literal, start));
2023-06-08 13:26:41 +00:00
}
private bool match(char expected)
{
if (isAtEnd()) return false;
2023-07-17 01:47:42 +00:00
if (source[current.Offset] != expected) return false;
2023-06-08 13:26:41 +00:00
current++;
return true;
}
private bool match(Predicate<char> pred)
{
if (isAtEnd()) return false;
2023-07-17 01:47:42 +00:00
if (!pred(source[current.Offset])) return false;
2023-06-08 13:26:41 +00:00
current++;
return true;
}
private char? peek()
{
if (isAtEnd()) return null;
2023-07-17 01:47:42 +00:00
return source[current.Offset];
2023-06-08 13:26:41 +00:00
}
private string? _stringLiteral(string errorName)
2023-06-08 13:26:41 +00:00
{
var valueStart = current.Offset;
2023-06-08 13:26:41 +00:00
while (peek() != '"' && !isAtEnd())
{
2023-07-17 01:47:42 +00:00
if (peek() == '\n')
current = current.NewLine();
2023-06-08 13:26:41 +00:00
advance();
}
if (isAtEnd())
{
Program.error(current, $"Unterminated {errorName}.");
return null;
2023-06-08 13:26:41 +00:00
}
// 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;
2023-06-08 13:26:41 +00:00
addToken(TokenType.String, value);
}
private bool isDigitOrSeparator(char c)
{
return Char.IsAsciiDigit(c) || c == '_';
}
private char? peekNext()
{
2023-07-17 01:47:42 +00:00
if (current.Offset + 1 >= source.Length) return null;
return source[current.Offset + 1];
2023-06-08 13:26:41 +00:00
}
private void numberLiteral()
{
while (match(isDigitOrSeparator)) ;
// Look for a fractional part.
if (peek() == '.' && isDigitOrSeparator(peekNext() ?? '\0'))
{
2023-06-08 14:00:26 +00:00
match('.');
2023-06-08 13:26:41 +00:00
while (match(isDigitOrSeparator)) ;
}
2023-07-17 01:47:42 +00:00
double value = Double.Parse(source.Substring(start.Offset, current.Offset - start.Offset));
2023-06-08 14:00:26 +00:00
addToken(TokenType.Number, value);
2023-06-08 13:26:41 +00:00
}
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)) ;
2023-07-17 01:47:42 +00:00
String text = source.Substring(start.Offset, current.Offset - start.Offset);
2023-06-08 13:26:41 +00:00
TokenType type;
var isKeyword = keywords.TryGetValue(text, out type);
addToken(isKeyword ? type : TokenType.Identifier, text);
2023-06-08 13:26:41 +00:00
}
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;
2023-06-28 23:18:31 +00:00
case ';': addToken(TokenType.Semicolon); break;
2023-06-08 13:26:41 +00:00
case '.': addToken(TokenType.Period); break;
case '*': addToken(TokenType.Asterisk); break;
case '/': addToken(TokenType.Slash); break;
2023-06-29 18:54:40 +00:00
case '|': addToken(TokenType.Pipe); break;
2023-06-08 13:26:41 +00:00
case '-':
addToken(match('>') ? TokenType.SingleArrow : TokenType.Minus);
break;
case '!':
addToken(match('=') ? TokenType.BangEqual : TokenType.Bang);
break;
2023-06-08 14:00:26 +00:00
case '+':
addToken(match('+') ? TokenType.PlusPlus : TokenType.Plus);
break;
2023-06-08 13:26:41 +00:00
case '=':
addToken(match('=') ? TokenType.DoubleEqual :
match('>') ? TokenType.DoubleArrow :
TokenType.Equal);
break;
case '<':
2023-06-29 18:54:40 +00:00
addToken(match('=') ? TokenType.LessEqual : TokenType.Less);
2023-06-08 13:26:41 +00:00
break;
case '>':
2023-06-29 18:54:40 +00:00
addToken(match('=') ? TokenType.GreaterEqual : TokenType.Greater);
2023-06-08 13:26:41 +00:00
break;
case '@': quotedIdentifier(); break;
2023-06-08 13:26:41 +00:00
case '"': stringLiteral(); break;
case '#':
while (peek() != '\n' && !isAtEnd()) advance();
break;
case ' ':
case '\r':
case '\t':
break;
case '\n':
2023-07-17 01:47:42 +00:00
current = current.NewLine();
2023-06-08 13:26:41 +00:00
break;
default:
if (Char.IsAsciiDigit(c))
{
numberLiteral();
}
else if (isIdentifierStartChar(c))
{
identifier();
}
else
{
2023-07-17 01:47:42 +00:00
Program.error(current, "Unexpected character.");
2023-06-08 13:26:41 +00:00
}
break;
}
}
public List<Token> scanTokens()
{
while (!isAtEnd())
{
start = current;
scanToken();
}
2023-07-17 01:47:42 +00:00
tokens.Add(new Token(TokenType.EOF, "", null, current));
2023-06-08 13:26:41 +00:00
return tokens;
}
}