finn-lang/Program.cs

352 lines
7.2 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
{
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<Token> tokens = scanner.scanTokens();
2023-06-28 18:12:14 +00:00
Parser parser = new Parser(tokens);
Expr? expression = parser.parse();
2023-06-08 13:26:41 +00:00
2023-07-02 17:38:48 +00:00
if (hadError || expression == null)
{
hadError = false;
return;
}
2023-06-28 18:12:14 +00:00
Console.WriteLine(new ASTPrinter().print(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);
}
}
public static void error(int line, String message)
{
report(line, "", message);
}
2023-06-28 18:12:14 +00:00
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);
}
}
2023-06-08 13:26:41 +00:00
static void report(int line, String where, String message)
{
Console.WriteLine($"[line {line}] Error{where}: {message}");
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, Number, String,
EOF
}
2023-06-08 14:16:32 +00:00
public record Token(TokenType type, String lexeme, Object? literal, int line);
2023-06-08 13:26:41 +00:00
class Scanner
{
private readonly String source;
private readonly List<Token> tokens = new List<Token>();
private int start = 0;
private int current = 0;
private int line = 1;
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()
{
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<char> 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'))
{
2023-06-08 14:00:26 +00:00
match('.');
2023-06-08 13:26:41 +00:00
while (match(isDigitOrSeparator)) ;
}
2023-06-08 14:00:26 +00:00
double value = Double.Parse(source.Substring(start, current - start));
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)) ;
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;
2023-06-08 13:26:41 +00:00
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 '"': 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<Token> scanTokens()
{
while (!isAtEnd())
{
start = current;
scanToken();
}
tokens.Add(new Token(TokenType.EOF, "", null, line));
return tokens;
}
}