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(); Parser parser = new Parser(tokens); Expr? expression = parser.parse(); 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(int line, String message) { report(line, "", message); } public static void error(Token token, String message) { if (token.type == TokenType.EOF) { report(token.line, " at end", message); } else { report(token.line, " at '" + token.lexeme + "'", message); } } public static void runtimeError(Interpreter.RuntimeError err) { Console.Error.WriteLine($"{err.Message}\n[line {err.Token.line}]"); hadRuntimeError = true; } static void report(int line, String where, String message) { Console.WriteLine($"[line {line}] 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, Number, String, EOF } public record Token(TokenType type, String lexeme, Object? literal, int line); class Scanner { private readonly String source; private readonly List tokens = new List(); private int start = 0; private int current = 0; private int line = 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 >= source.Length; } private char advance() { return source[current++]; } private void addToken(TokenType type) { addToken(type, null); } private void addToken(TokenType type, Object? literal) { String text = source.Substring(start, current - start); tokens.Add(new Token(type, text, literal, line)); } private bool match(char expected) { if (isAtEnd()) return false; if (source[current] != expected) return false; current++; return true; } private bool match(Predicate pred) { if (isAtEnd()) return false; if (!pred(source[current])) return false; current++; return true; } private char? peek() { if (isAtEnd()) return null; return source[current]; } private void stringLiteral() { while (peek() != '"' && !isAtEnd()) { if (peek() == '\n') line++; advance(); } if (isAtEnd()) { Program.error(line, "Unterminated string."); return; } // The closing ". advance(); // Trim the surrounding quotes. String value = source.Substring(start + 1, current - start - 2); addToken(TokenType.String, value); } private bool isDigitOrSeparator(char c) { return Char.IsAsciiDigit(c) || c == '_'; } private char? peekNext() { if (current + 1 >= source.Length) return null; return source[current + 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, current - start)); 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, current - start); TokenType type; var isKeyword = keywords.TryGetValue(text, out type); addToken(isKeyword ? type : TokenType.Identifier); } 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.At); 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 '"': stringLiteral(); break; case '#': while (peek() != '\n' && !isAtEnd()) advance(); break; case ' ': case '\r': case '\t': break; case '\n': line++; break; default: if (Char.IsAsciiDigit(c)) { numberLiteral(); } else if (isIdentifierStartChar(c)) { identifier(); } else { Program.error(line, "Unexpected character."); } break; } } public List scanTokens() { while (!isAtEnd()) { start = current; scanToken(); } tokens.Add(new Token(TokenType.EOF, "", null, line)); return tokens; } }