commit c991893f768f001d56228a3abbcdcb73bf3741a8 Author: Brandon Dyck Date: Thu Jun 8 07:26:41 2023 -0600 Lexer diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cbbd0b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..221b448 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/net7.0/finn.dll", + "args": [], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..0fa8371 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/finn.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/finn.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/finn.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..5e41e2d --- /dev/null +++ b/Program.cs @@ -0,0 +1,345 @@ +using System; +using System.IO; +using System.Collections; +using System.Collections.Generic; + +namespace Finn; + +class Program +{ + static bool hadError = false; + + 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); + } + + static void run(string src) + { + var scanner = new Scanner(src); + List tokens = scanner.scanTokens(); + + foreach (Token token in tokens) + { + Console.WriteLine(token); + } + } + + 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); + } + + static void report(int line, String where, String message) + { + Console.WriteLine($"[line {line}] Error{where}: {message}"); + hadError = true; + } +} + +enum TokenType +{ + LParen, RParen, + LBrace, RBrace, + LBracket, RBracket, + Less, Greater, + Pipe, + Colon, + Tick, + Backtick, + Comma, + Period, + Equal, + Plus, Minus, Asterisk, Slash, + DoubleEqual, + Bang, BangEqual, + LessEqual, GreaterEqual, + SingleArrow, + DoubleArrow, + PipeRight, + PipeLeft, + ComposeRight, + ComposeLeft, + When, + Is, + Let, + In, + With, + Fn, + Type, + Alias, + Recurse, + Blank, + Identifier, Number, String, + EOF +} + +class Token +{ + public readonly TokenType type; + public readonly String lexeme; + public readonly Object? literal; + public readonly int line; + + public Token(TokenType type, String lexeme, Object? literal, int line) + { + this.type = type; + this.lexeme = lexeme; + this.literal = literal; + this.line = line; + } + + public override string ToString() + { + return $"{type} {lexeme} {literal}"; + } +} + +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() + { + {"when", TokenType.When}, + {"is", TokenType.Is}, + {"let", TokenType.Let}, + {"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')) + { + while (match(isDigitOrSeparator)) ; + } + } + + 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.Comma); break; + case '.': addToken(TokenType.Period); break; + case '+': addToken(TokenType.Plus); break; + case '*': addToken(TokenType.Asterisk); break; + case '/': addToken(TokenType.Slash); break; + case '-': + addToken(match('>') ? TokenType.SingleArrow : TokenType.Minus); + break; + case '!': + addToken(match('=') ? TokenType.BangEqual : TokenType.Bang); + break; + case '=': + addToken(match('=') ? TokenType.DoubleEqual : + match('>') ? TokenType.DoubleArrow : + TokenType.Equal); + break; + case '<': + addToken(match('=') ? TokenType.LessEqual : + match('<') ? TokenType.ComposeLeft : + match('|') ? TokenType.PipeLeft : + TokenType.Less); + break; + case '>': + addToken(match('=') ? TokenType.GreaterEqual : + match('>') ? TokenType.ComposeRight : TokenType.Greater); + break; + case '|': + addToken(match('>') ? TokenType.PipeRight : TokenType.Pipe); + 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; + } +} diff --git a/finn.csproj b/finn.csproj new file mode 100644 index 0000000..d4cb908 --- /dev/null +++ b/finn.csproj @@ -0,0 +1,8 @@ + + + Exe + net7.0 + Finn + enable + + \ No newline at end of file