using System; using System.Collections.Generic; 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 List { public required ImmutableList Items { get; init; } public static List Empty = new List { Items = ImmutableList.Empty }; public object this[double i] { get { return Items[(int)i]; } } public bool IsEmpty { get { return Items.IsEmpty; } } public List Add(object item) { return new List { Items = Items.Add(item) }; } public override string ToString() { System.IO.StringWriter sw = new System.IO.StringWriter(); sw.Write("["); foreach (var item in this.Items) { sw.Write($" {item},"); } sw.Write(" ]"); return sw.ToString(); } } private class Record { public static Record Empty = new Record { Fields = ImmutableSortedDictionary>.Empty, }; public required ImmutableSortedDictionary> 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)) }; } public override string ToString() { System.IO.StringWriter sw = new System.IO.StringWriter(); sw.Write("{"); foreach ((var label, var values) in this.Fields) { foreach (var value in values) { sw.Write($" {label} = {value},"); } } sw.Write(" }"); return sw.ToString(); } } 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 List checkListOperand(Token op, Object operand) { if (operand is List l) return l; throw new RuntimeError(op, "Operand must be a record."); } private Record checkRecordOperand(Token op, Object operand) { if (operand is Record r) return r; throw new RuntimeError(op, "Operand must be a record."); } private double checkNumberOperand(Token op, Object operand) { if (operand is double d) return d; 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."); } private Variant checkBoolOperand(Token op, object operand) { if (operand is Variant v) { if (v == Variant.True || v == Variant.False) return v; } throw new RuntimeError(op, "Operand must be ."); } 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); } public object visitIdentifierExpr(AST.Identifier expr) { throw new System.NotImplementedException(); } public object visitIfExpr(AST.If expr) { var cond = evaluate(expr.Condition); // TODO Maybe I should token info in the AST. var vb = checkBoolOperand(new Token(TokenType.If, "if", null, 1), cond); if (vb == Variant.True) { return evaluate(expr.Then); } else { return evaluate(expr.Else); } } public object visitIndexerExpr(AST.Indexer expr) { // TODO use real token var tok = new Token(TokenType.LBracket, "[", null, 1); var left = checkListOperand(tok, evaluate(expr.Left)); var index = checkNumberOperand(tok, evaluate(expr.Index)); try { var item = left[index]; return new Variant("some", item); } catch { return new Variant("nothing", null); } } public object visitLetExpr(AST.Let expr) { throw new System.NotImplementedException(); } public object visitListExpr(AST.List expr) { // TODO use real token var tok = new Token(TokenType.LBracket, "[", null, 1); List l = List.Empty; foreach (var itemExpr in expr.Elements) { var item = evaluate(itemExpr); if (!l.IsEmpty) { try { checkTypesEqual(l[0], item); } catch { throw new RuntimeError(tok, "List items must all have same type."); } } l = l.Add(item); } return l; } public object visitLiteralExpr(AST.Literal expr) { return expr.Value; } public object visitRecordExpr(AST.Record expr) { // TODO use real token Token tok = new Token(TokenType.LBrace, "{", null, 1); Record rec = Record.Empty; if (expr.Base != null) { var baseRecValue = evaluate(expr.Base.Value); if (baseRecValue is not Record) { throw new RuntimeError(tok, "Base record must be a record."); } var baseRec = (Record)baseRecValue; // Updates HashSet updateLabels = new HashSet(); foreach (AST.Field update in expr.Base.Updates) { var label = update.Name.Value; if (updateLabels.Contains(label)) { throw new RuntimeError(tok, "Record updates must be to unique fields."); } updateLabels.Add(label); if (update.Value == null) throw new NotImplementedException(); try { baseRec = baseRec.Update(label, evaluate(update.Value)); } catch { throw new RuntimeError(tok, "Field update must have same type as previous value."); } } rec = baseRec; } // Extensions HashSet extLabels = new HashSet(); foreach (AST.Field extension in expr.Extensions) { var label = extension.Name.Value; if (extLabels.Contains(label)) { throw new RuntimeError(tok, "Record extensions must have unique field names."); } extLabels.Add(label); if (extension.Value == null) throw new NotImplementedException(); rec = rec.Extend(label, evaluate(extension.Value)); } return rec; } public object visitSelectorExpr(AST.Selector expr) { var left = evaluate(expr.Left); // TODO Use real token. var tok = new Token(TokenType.Period, ".", null, 1); var r = checkRecordOperand(tok, left); try { return r.Get(expr.FieldName.Value); } catch { throw new RuntimeError(tok, "Operand must have selected field."); } } public object visitSequenceExpr(AST.Sequence expr) { evaluate(expr.Left); return evaluate(expr.Right); } 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 (checkBoolOperand(expr.Op, right) == Variant.True) { return Variant.False; } else { return Variant.True; } default: // Unreachable throw new Exception($"bad unary op: {expr.Op}"); } } public object visitVariantExpr(AST.Variant expr) { return new Variant(expr.Tag.Value, expr.Argument); } public object visitWhenExpr(AST.When expr) { throw new System.NotImplementedException(); } }