diff --git a/Interpreter.cs b/Interpreter.cs new file mode 100644 index 0000000..987f9c5 --- /dev/null +++ b/Interpreter.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Immutable; + +using AST = Finn.AST; + +namespace Finn; + +public class Interpreter : AST.IExprVisitor +{ + public class RuntimeError : Exception + { + public readonly Token Token; + + internal RuntimeError(Token token, String message) : base(message) + { + Token = token; + } + } + + private static void checkTypesEqual(object a, object b) + { + var aType = a.GetType(); + var bType = b.GetType(); + if (aType != bType) + { + throw new Exception("Type mismatch: {aType} != {bType}."); + } + } + + private class Record + { + public static Record Empty = new Record + { + Fields = ImmutableDictionary>.Empty, + }; + public required ImmutableDictionary> Fields { get; init; } + + public Record Update(string name, object value) + { + ImmutableStack? values; + if (!Fields.TryGetValue(name, out values)) + { + throw new ArgumentException($"no such field: {name}"); + } + checkTypesEqual(value, values.Peek()); + return new Record { Fields = Fields.SetItem(name, values!.Pop().Push(value)) }; + } + + public object Get(string name) + { + ImmutableStack? values; + if (!Fields.TryGetValue(name, out values)) + { + throw new ArgumentException($"no such field: {name}"); + } + return values.Peek(); + } + + public Record Remove(string name) + { + ImmutableStack? values; + if (!Fields.TryGetValue(name, out values)) + { + throw new ArgumentException($"no such field: {name}"); + } + var value = values.Peek(); + var popped = values.Pop(); + if (popped.IsEmpty) + { + return new Record { Fields = Fields.Remove(name) }; + } + return new Record { Fields = Fields.SetItem(name, popped) }; + } + + public Record Extend(string name, object value) + { + var values = Fields.GetValueOrDefault(name, ImmutableStack.Empty); + return new Record { Fields = Fields.SetItem(name, values.Push(value)) }; + } + } + + private record Variant(string Tag, object? Value) + { + public static readonly Variant True = new Variant("true", null); + public static readonly Variant False = new Variant("false", null); + public static Variant FromBool(bool b) + { + return b ? True : False; + } + public bool IsEmpty { get { return Value == null; } } + + public override string ToString() + { + if (Value == null) + { + return $"`{Tag}"; + } + else + { + return $"`{Tag}({Value})"; + } + } + } + + public void Interpret(AST.Expr expression) + { + try + { + var value = evaluate(expression); + Console.WriteLine(value); + } + catch (RuntimeError err) + { + Program.runtimeError(err); + } + } + + private object evaluate(AST.Expr expr) + { + return expr.accept(this); + } + + private void checkNumberOperand(Token op, Object operand) + { + if (operand is double) return; + throw new RuntimeError(op, "Operand must be a number."); + } + + private void checkNumberOperands(Token op, object left, object right) + { + if (left is double && right is double) return; + throw new RuntimeError(op, "Operands must be numbers."); + } + + private void checkStringOperands(Token op, object left, object right) + { + if (left is string && right is string) return; + throw new RuntimeError(op, "Operands must be strings."); + } + + public object visitBinaryExpr(AST.Binary expr) + { + var left = evaluate(expr.Left); + var right = evaluate(expr.Right); + + switch (expr.Op.type) + { + case TokenType.Minus: + checkNumberOperands(expr.Op, left, right); + return (double)left - (double)right; + case TokenType.Plus: + checkNumberOperands(expr.Op, left, right); + return (double)left + (double)right; + case TokenType.Slash: + checkNumberOperands(expr.Op, left, right); + return (double)left / (double)right; + case TokenType.Asterisk: + checkNumberOperands(expr.Op, left, right); + return (double)left * (double)right; + + case TokenType.PlusPlus: + checkStringOperands(expr.Op, left, right); + return (string)left + (string)right; + + case TokenType.Greater: + checkNumberOperands(expr.Op, left, right); + return Variant.FromBool((double)left > (double)right); + case TokenType.GreaterEqual: + checkNumberOperands(expr.Op, left, right); + return Variant.FromBool((double)left >= (double)right); + case TokenType.Less: + checkNumberOperands(expr.Op, left, right); + return Variant.FromBool((double)left < (double)right); + case TokenType.LessEqual: + checkNumberOperands(expr.Op, left, right); + return Variant.FromBool((double)left <= (double)right); + + case TokenType.BangEqual: + return Variant.FromBool((left, right) switch + { + (String l, String r) => l != r, + (double l, double r) => l != r, + _ => throw new ArgumentException(), + }); + case TokenType.DoubleEqual: + return Variant.FromBool((left, right) switch + { + (String l, String r) => l == r, + (double l, double r) => l == r, + _ => throw new ArgumentException(), + }); + } + + throw new ArgumentException($"bad binary op: {expr.Op}"); + } + + public object visitCallExpr(AST.Call expr) + { + throw new System.NotImplementedException(); + } + + public object visitGroupingExpr(AST.Grouping expr) + { + return evaluate(expr.Expression); + throw new System.NotImplementedException(); + } + + public object visitIdentifierExpr(AST.Identifier expr) + { + throw new System.NotImplementedException(); + } + + public object visitIfExpr(AST.If expr) + { + throw new System.NotImplementedException(); + } + + public object visitIndexerExpr(AST.Indexer expr) + { + throw new System.NotImplementedException(); + } + + public object visitLetExpr(AST.Let expr) + { + throw new System.NotImplementedException(); + } + + public object visitListExpr(AST.List expr) + { + throw new System.NotImplementedException(); + } + + public object visitLiteralExpr(AST.Literal expr) + { + return expr.Value; + } + + public object visitRecordExpr(AST.Record expr) + { + throw new System.NotImplementedException(); + } + + public object visitSelectorExpr(AST.Selector expr) + { + throw new System.NotImplementedException(); + } + + public object visitSequenceExpr(AST.Sequence expr) + { + throw new System.NotImplementedException(); + } + + public object visitUnaryExpr(AST.Unary expr) + { + object right = evaluate(expr.Right); + switch (expr.Op.type) + { + case TokenType.Minus: + checkNumberOperand(expr.Op, right); + return -(double)right; + case TokenType.Bang: + if (right is Variant v) + { + if (v == Variant.True) + { + return Variant.False; + } + else if (v == Variant.False) + { + return Variant.True; + } + } + throw new RuntimeError(expr.Op, "Boolean must be "); + default: + // Unreachable + throw new Exception($"bad unary op: {expr.Op}"); + } + } + + public object visitVariantExpr(AST.Variant expr) + { + throw new System.NotImplementedException(); + } + + public object visitWhenExpr(AST.When expr) + { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index da2f8e1..6ed5463 100644 --- a/Program.cs +++ b/Program.cs @@ -7,7 +7,9 @@ namespace Finn; class Program { + private static readonly Interpreter interpreter = new Interpreter(); static bool hadError = false; + static bool hadRuntimeError = true; static void Main(string[] args) { @@ -30,6 +32,9 @@ class Program { var src = File.ReadAllText(path); run(src); + + if (hadError) Environment.Exit(65); + if (hadRuntimeError) Environment.Exit(70); } static void run(string src) @@ -39,13 +44,13 @@ class Program Parser parser = new Parser(tokens); Expr? expression = parser.parse(); - if (hadError || expression == null) + if (hadError) { hadError = false; return; } - Console.WriteLine(expression); + interpreter.Interpret(expression!); } static void runPrompt() @@ -79,6 +84,12 @@ class Program } } + 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}");