Evaluate arithmetic expressions
This commit is contained in:
parent
6459e39260
commit
b1607eebc6
289
Interpreter.cs
Normal file
289
Interpreter.cs
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
using AST = Finn.AST;
|
||||||
|
|
||||||
|
namespace Finn;
|
||||||
|
|
||||||
|
public class Interpreter : AST.IExprVisitor<object>
|
||||||
|
{
|
||||||
|
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<string, ImmutableStack<object>>.Empty,
|
||||||
|
};
|
||||||
|
public required ImmutableDictionary<string, ImmutableStack<object>> Fields { get; init; }
|
||||||
|
|
||||||
|
public Record Update(string name, object value)
|
||||||
|
{
|
||||||
|
ImmutableStack<object>? 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<object>? values;
|
||||||
|
if (!Fields.TryGetValue(name, out values))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"no such field: {name}");
|
||||||
|
}
|
||||||
|
return values.Peek();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Record Remove(string name)
|
||||||
|
{
|
||||||
|
ImmutableStack<object>? 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<object>.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 <true, false>");
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
15
Program.cs
15
Program.cs
@ -7,7 +7,9 @@ namespace Finn;
|
|||||||
|
|
||||||
class Program
|
class Program
|
||||||
{
|
{
|
||||||
|
private static readonly Interpreter interpreter = new Interpreter();
|
||||||
static bool hadError = false;
|
static bool hadError = false;
|
||||||
|
static bool hadRuntimeError = true;
|
||||||
|
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
@ -30,6 +32,9 @@ class Program
|
|||||||
{
|
{
|
||||||
var src = File.ReadAllText(path);
|
var src = File.ReadAllText(path);
|
||||||
run(src);
|
run(src);
|
||||||
|
|
||||||
|
if (hadError) Environment.Exit(65);
|
||||||
|
if (hadRuntimeError) Environment.Exit(70);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void run(string src)
|
static void run(string src)
|
||||||
@ -39,13 +44,13 @@ class Program
|
|||||||
Parser parser = new Parser(tokens);
|
Parser parser = new Parser(tokens);
|
||||||
Expr? expression = parser.parse();
|
Expr? expression = parser.parse();
|
||||||
|
|
||||||
if (hadError || expression == null)
|
if (hadError)
|
||||||
{
|
{
|
||||||
hadError = false;
|
hadError = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine(expression);
|
interpreter.Interpret(expression!);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void runPrompt()
|
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)
|
static void report(int line, String where, String message)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[line {line}] Error{where}: {message}");
|
Console.WriteLine($"[line {line}] Error{where}: {message}");
|
||||||
|
Loading…
Reference in New Issue
Block a user