This commit is contained in:
Brandon Dyck 2023-06-08 07:26:41 -06:00
commit c991893f76
5 changed files with 420 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
bin/
obj/

24
.vscode/launch.json vendored Normal file
View File

@ -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"
}
]
}

41
.vscode/tasks.json vendored Normal file
View File

@ -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"
}
]
}

345
Program.cs Normal file
View File

@ -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<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,
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'))
{
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<Token> scanTokens()
{
while (!isAtEnd())
{
start = current;
scanToken();
}
tokens.Add(new Token(TokenType.EOF, "", null, line));
return tokens;
}
}

8
finn.csproj Normal file
View File

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Finn</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>