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
|
||||
{
|
||||
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}");
|
||||
|
Loading…
Reference in New Issue
Block a user