2023-06-08 13:26:41 +00:00
|
|
|
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<Token> 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,
|
2023-06-08 14:00:26 +00:00
|
|
|
PlusPlus,
|
2023-06-08 13:26:41 +00:00
|
|
|
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<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>()
|
|
|
|
{
|
|
|
|
{"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<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.Comma); break;
|
|
|
|
case '.': addToken(TokenType.Period); 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;
|
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 '<':
|
|
|
|
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<Token> scanTokens()
|
|
|
|
{
|
|
|
|
while (!isAtEnd())
|
|
|
|
{
|
|
|
|
start = current;
|
|
|
|
scanToken();
|
|
|
|
}
|
|
|
|
|
|
|
|
tokens.Add(new Token(TokenType.EOF, "", null, line));
|
|
|
|
return tokens;
|
|
|
|
}
|
|
|
|
}
|