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 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 tokens = new List(); private Position start = new(0, 1, 1); private Position current = new(0, 1, 1); private static readonly Dictionary keywords = new Dictionary() { {"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 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 scanTokens() { while (!isAtEnd()) { start = current; scanToken(); } tokens.Add(new Token(TokenType.EOF, "", null, current)); return tokens; } }