diff --git a/Interpreter.cs b/Interpreter.cs index af71bbc..0a0766d 100644 --- a/Interpreter.cs +++ b/Interpreter.cs @@ -64,6 +64,20 @@ public class Env } } } + + public Env Ancestor(int distance) + { + Env? curr = this; + for (int i = 0; i < distance; i++) + { + curr = curr.enclosing; + if (curr is null) + { + throw new IndexOutOfRangeException(); + } + } + return curr; + } } public interface Callable @@ -302,14 +316,16 @@ public class Interpreter : AST.IExprVisitor binding.Params[i].accept((arguments[i], env), new PatternBinder()); } return interpreter.evaluate(env, binding.Value); - // throw new NotImplementedException(); } } protected internal readonly Env Globals = new Env(); + private readonly Resolver Resolver; - public Interpreter() + public Interpreter(Resolver resolver) { + Resolver = resolver; + Globals["clock"] = new NativeFunction((args) => { return (double)DateTimeOffset.Now.ToUnixTimeSeconds(); @@ -413,14 +429,14 @@ public class Interpreter : AST.IExprVisitor case TokenType.BangEqual: return Variant.FromBool((left, right) switch { - (String l, String r) => l != r, + (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, + (string l, string r) => l == r, (double l, double r) => l == r, _ => throw new ArgumentException(), }); @@ -453,7 +469,15 @@ public class Interpreter : AST.IExprVisitor public object visitVariableExpr(Env env, AST.Variable expr) { - return env[expr.Value]; + var name = (string)expr.Value.Literal!; + if (Resolver.TryResolve(expr, out var distance)) + { + return env.Ancestor(distance)[expr.Value]; + } + else + { + return Globals[expr.Value]; + } } public object visitIfExpr(Env env, AST.If expr) @@ -499,9 +523,11 @@ public class Interpreter : AST.IExprVisitor } catch (Exception e) { - var start = e is PatternMismatchException ? - ((PatternMismatchException)e).Start : - binding.Start; + var start = e switch + { + PatternMismatchException pme => pme.Start, + _ => binding.Start, + }; throw new RuntimeError(start, e.Message); } break; diff --git a/Program.cs b/Program.cs index c49f851..e472bac 100644 --- a/Program.cs +++ b/Program.cs @@ -7,7 +7,8 @@ namespace Finn; class Program { - private static readonly Interpreter interpreter = new Interpreter(); + private static readonly Resolver resolver = new Resolver(); + private static readonly Interpreter interpreter = new Interpreter(resolver); static bool hadError = false; static bool hadRuntimeError = true; @@ -41,16 +42,8 @@ class Program { var scanner = new Scanner(src); List tokens = scanner.scanTokens(); - // Console.WriteLine("TOKENS\n======="); - // foreach (var token in tokens) - // { - // Console.WriteLine(token); - // } Parser parser = new Parser(tokens); Expr? expression = parser.parse(); - // Console.WriteLine("\nAST\n======="); - // Console.WriteLine(expression); - // Console.WriteLine(); if (hadError) { @@ -58,6 +51,13 @@ class Program return; } + resolver.Add(expression!); + if (hadError) + { + hadError = false; + return; + } + interpreter.Interpret(expression!); } diff --git a/Resolver.cs b/Resolver.cs index a8924c7..d7f453a 100644 --- a/Resolver.cs +++ b/Resolver.cs @@ -8,250 +8,267 @@ using ScopeStack = System.Collections.Immutable.ImmutableStack; using System.Collections.Generic; -class Resolver : IExprVisitor +public class Resolver { - private class NameCollection - { - public ImmutableHashSet Names { get; private set; } = ImmutableHashSet.Empty; - - public void Add(Token name) - { - var value = (string)name.Literal!; - if (Names.Contains(value)) - { - Program.error(name.Start, $"Duplicate name declared in scope: {name.Lexeme}"); - } - else - { - Names = Names.Add(value); - } - } - } - - private class PatternNameCollector : IPatternVisitor - { - public readonly static PatternNameCollector Instance = new(); - private static readonly ValueTuple Void = ValueTuple.Create(); - - private PatternNameCollector() { } - - public ValueTuple visitFieldPatternPattern(NameCollection names, FieldPattern field) - { - if (field.Pattern is null) - { - names.Add(field.Name); - } - else - { - field.Pattern.accept(names, this); - } - return Void; - } - - public ValueTuple visitRecordPatternPattern(NameCollection names, RecordPattern record) - { - record.Rest?.accept(names, this); - foreach (var field in record.Fields) - { - field.accept(names, this); - } - return Void; - } - - public ValueTuple visitSimplePatternPattern(NameCollection names, SimplePattern simple) - { - if (simple.Identifier is not null) - { - names.Add(simple.Identifier); - } - return Void; - } - - public ValueTuple visitVariantPatternPattern(NameCollection names, VariantPattern variant) - { - variant.Argument?.accept(names, this); - return Void; - } - } - private static readonly Resolutions EmptyResolutions = Resolutions.Empty.WithComparers(ReferenceEqualityComparer.Instance); - public static readonly Resolver Instance = new Resolver(); - private Resolver() { } - - private static Resolutions AddRange(Resolutions r1, Resolutions r2) => r1.AddRange(r2); - - private Resolutions Resolve(ScopeStack scopes, Expr expr) + private Resolutions Resolutions = EmptyResolutions; + public bool TryResolve(Expr expr, out int distance) { - return expr.accept(scopes, this); + return Resolutions.TryGetValue(expr, out distance); } - private Resolutions Resolve(ScopeStack scopes, ImmutableHashSet newScope, Expr value) + public void Add(Expr expr) { - return Resolve(scopes.Push(newScope), value); + var newResolutions = ResolverBuilder.Instance.Resolve(ScopeStack.Empty, expr); + Resolutions = Resolutions.AddRange(newResolutions); } - public Resolutions visitBinaryExpr(ScopeStack scopes, Binary expr) + private class ResolverBuilder : IExprVisitor { - var left = Resolve(scopes, expr.Left); - var right = Resolve(scopes, expr.Right); - return left.AddRange(right); - } - - public Resolutions visitCallExpr(ScopeStack scopes, Call expr) - { - return (from arg in expr.Arguments - where arg is not null - select Resolve(scopes, arg)) - .Aggregate(Resolve(scopes, expr.Left), AddRange); - } - - public Resolutions visitGroupingExpr(ScopeStack scopes, Grouping expr) - { - return Resolve(scopes, expr.Expression); - } - - public Resolutions visitIfExpr(ScopeStack scopes, If expr) - { - var condition = Resolve(scopes, expr.Condition); - var _then = Resolve(scopes, expr.Then); - var _else = Resolve(scopes, expr.Else); - return condition.AddRange(_then).AddRange(_else); - } - - public Resolutions visitIndexerExpr(ScopeStack scopes, Indexer expr) - { - var left = Resolve(scopes, expr.Left); - var index = Resolve(scopes, expr.Index); - return left.AddRange(index); - } - - public Resolutions visitLetExpr(ScopeStack scopes, Let expr) - { - var names = new NameCollection(); - foreach (var binding in expr.Bindings) + private class NameCollection { - switch (binding) + public ImmutableHashSet Names { get; private set; } = ImmutableHashSet.Empty; + + public void Add(Token name) { - case FuncBinding fb: - names.Add(fb.Name); - break; - case VarBinding vb: - vb.Pattern.accept(names, PatternNameCollector.Instance); - break; - default: - throw new Exception("wtf there are no other binding types"); - } - } - - var newScopes = scopes.Push(names.Names); - - // TODO Put the static constructivity check here. - // For now, just check that a definition only uses names that come before it. - // Note that this will preclude using shadowed variables in definitions. - - return (from binding in expr.Bindings - select (binding switch + var value = (string)name.Literal!; + if (Names.Contains(value)) { - FuncBinding fb => ResolveFunction(newScopes, fb), - VarBinding vb => Resolve(newScopes, vb.Value), - _ => throw new Exception("wtf there are no other binding types"), - })) - .Aggregate(Resolve(newScopes, expr.Body), AddRange); - - } - - private Resolutions ResolveFunction(ScopeStack scopes, FuncBinding fb) - { - NameCollection names = new(); - foreach (var param in fb.Params) - { - param.accept(names, PatternNameCollector.Instance); - } - return Resolve(scopes, names.Names, fb.Value); - } - - public Resolutions visitListExpr(ScopeStack scopes, List expr) - { - return (from element in expr.Elements - select Resolve(scopes, element)) - .Aggregate(EmptyResolutions, AddRange); - } - - public Resolutions visitLiteralExpr(ScopeStack scopes, Literal expr) - { - return EmptyResolutions; - } - - public Resolutions visitRecordExpr(ScopeStack scopes, Record expr) - { - var resolutions = (from extension in expr.Extensions - select Resolve(scopes, extension.Value)) - .Aggregate(EmptyResolutions, AddRange); - if (expr.Base is not null) - { - resolutions = resolutions.AddRange(Resolve(scopes, expr.Base.Value)); - resolutions = (from update in expr.Base.Updates - select Resolve(scopes, update.Value)) - .Aggregate(resolutions, AddRange); - } - - return resolutions; - } - - public Resolutions visitSelectorExpr(ScopeStack scopes, Selector expr) - { - return Resolve(scopes, expr.Left); - } - - public Resolutions visitSequenceExpr(ScopeStack scopes, Sequence expr) - { - return AddRange(Resolve(scopes, expr.Left), Resolve(scopes, expr.Right)); - } - - public Resolutions visitUnaryExpr(ScopeStack scopes, Unary expr) - { - return Resolve(scopes, expr.Right); - } - - public Resolutions visitVariableExpr(ScopeStack scopes, Variable expr) - { - int depth = 0; - while (!scopes.IsEmpty) - { - scopes = scopes.Pop(out var scope); - if (scope.Contains((string)expr.Value.Literal!)) - { - return EmptyResolutions.Add(expr, depth); + Program.error(name.Start, $"Duplicate name declared in scope: {name.Lexeme}"); + } + else + { + Names = Names.Add(value); + } } - depth++; } - // At this point, it must be a global. I don't like that we leave that resolution for runtime. - // TODO Don't leave that resolution for runtime. - // But I think that can wait until I make the resolver nameless. - return EmptyResolutions.Add(expr, depth); - } - public Resolutions visitVariantExpr(ScopeStack scopes, Variant expr) - { - if (expr.Argument is not null) + private class PatternNameCollector : IPatternVisitor { - return Resolve(scopes, expr.Argument); + public readonly static PatternNameCollector Instance = new(); + private static readonly ValueTuple Void = ValueTuple.Create(); + + private PatternNameCollector() { } + + public ValueTuple visitFieldPatternPattern(NameCollection names, FieldPattern field) + { + if (field.Pattern is null) + { + names.Add(field.Name); + } + else + { + field.Pattern.accept(names, this); + } + return Void; + } + + public ValueTuple visitRecordPatternPattern(NameCollection names, RecordPattern record) + { + record.Rest?.accept(names, this); + foreach (var field in record.Fields) + { + field.accept(names, this); + } + return Void; + } + + public ValueTuple visitSimplePatternPattern(NameCollection names, SimplePattern simple) + { + if (simple.Identifier is not null) + { + names.Add(simple.Identifier); + } + return Void; + } + + public ValueTuple visitVariantPatternPattern(NameCollection names, VariantPattern variant) + { + variant.Argument?.accept(names, this); + return Void; + } } - return EmptyResolutions; - } - public Resolutions visitWhenExpr(ScopeStack scopes, When expr) - { - return (from _case in expr.Cases - select ResolveCase(_case)) - .Aggregate(Resolve(scopes, expr.Head), AddRange); + public static readonly ResolverBuilder Instance = new ResolverBuilder(); - Resolutions ResolveCase(VarBinding _case) + private ResolverBuilder() { } + + private static Resolutions AddRange(Resolutions r1, Resolutions r2) => r1.AddRange(r2); + + public Resolutions Resolve(ScopeStack scopes, Expr expr) + { + return expr.accept(scopes, this); + } + + private Resolutions Resolve(ScopeStack scopes, ImmutableHashSet newScope, Expr value) + { + return Resolve(scopes.Push(newScope), value); + } + + public Resolutions visitBinaryExpr(ScopeStack scopes, Binary expr) + { + var left = Resolve(scopes, expr.Left); + var right = Resolve(scopes, expr.Right); + return left.AddRange(right); + } + + public Resolutions visitCallExpr(ScopeStack scopes, Call expr) + { + return (from arg in expr.Arguments + where arg is not null + select Resolve(scopes, arg)) + .Aggregate(Resolve(scopes, expr.Left), AddRange); + } + + public Resolutions visitGroupingExpr(ScopeStack scopes, Grouping expr) + { + return Resolve(scopes, expr.Expression); + } + + public Resolutions visitIfExpr(ScopeStack scopes, If expr) + { + var condition = Resolve(scopes, expr.Condition); + var _then = Resolve(scopes, expr.Then); + var _else = Resolve(scopes, expr.Else); + return condition.AddRange(_then).AddRange(_else); + } + + public Resolutions visitIndexerExpr(ScopeStack scopes, Indexer expr) + { + var left = Resolve(scopes, expr.Left); + var index = Resolve(scopes, expr.Index); + return left.AddRange(index); + } + + public Resolutions visitLetExpr(ScopeStack scopes, Let expr) { var names = new NameCollection(); - _case.Pattern.accept(names, PatternNameCollector.Instance); - return Resolve(scopes, names.Names, _case.Value); + foreach (var binding in expr.Bindings) + { + switch (binding) + { + case FuncBinding fb: + names.Add(fb.Name); + break; + case VarBinding vb: + vb.Pattern.accept(names, PatternNameCollector.Instance); + break; + default: + throw new Exception("wtf there are no other binding types"); + } + } + + var newScopes = scopes.Push(names.Names); + + // TODO Put the static constructivity check here. + // For now, just check that a definition only uses names that come before it. + // Note that this will preclude using shadowed variables in definitions. + + return (from binding in expr.Bindings + select (binding switch + { + FuncBinding fb => ResolveFunction(newScopes, fb), + VarBinding vb => Resolve(newScopes, vb.Value), + _ => throw new Exception("wtf there are no other binding types"), + })) + .Aggregate(Resolve(newScopes, expr.Body), AddRange); + + } + + private Resolutions ResolveFunction(ScopeStack scopes, FuncBinding fb) + { + NameCollection names = new(); + foreach (var param in fb.Params) + { + param.accept(names, PatternNameCollector.Instance); + } + return Resolve(scopes, names.Names, fb.Value); + } + + public Resolutions visitListExpr(ScopeStack scopes, List expr) + { + return (from element in expr.Elements + select Resolve(scopes, element)) + .Aggregate(EmptyResolutions, AddRange); + } + + public Resolutions visitLiteralExpr(ScopeStack scopes, Literal expr) + { + return EmptyResolutions; + } + + public Resolutions visitRecordExpr(ScopeStack scopes, Record expr) + { + var resolutions = (from extension in expr.Extensions + select Resolve(scopes, extension.Value)) + .Aggregate(EmptyResolutions, AddRange); + if (expr.Base is not null) + { + resolutions = resolutions.AddRange(Resolve(scopes, expr.Base.Value)); + resolutions = (from update in expr.Base.Updates + select Resolve(scopes, update.Value)) + .Aggregate(resolutions, AddRange); + } + + return resolutions; + } + + public Resolutions visitSelectorExpr(ScopeStack scopes, Selector expr) + { + return Resolve(scopes, expr.Left); + } + + public Resolutions visitSequenceExpr(ScopeStack scopes, Sequence expr) + { + return AddRange(Resolve(scopes, expr.Left), Resolve(scopes, expr.Right)); + } + + public Resolutions visitUnaryExpr(ScopeStack scopes, Unary expr) + { + return Resolve(scopes, expr.Right); + } + + public Resolutions visitVariableExpr(ScopeStack scopes, Variable expr) + { + int depth = 0; + while (!scopes.IsEmpty) + { + scopes = scopes.Pop(out var scope); + if (scope.Contains((string)expr.Value.Literal!)) + { + return EmptyResolutions.Add(expr, depth); + } + depth++; + } + // At this point, it must be a global. I don't like that we leave that resolution for runtime. + // TODO Don't leave that resolution for runtime. + // But I think that can wait until I make the resolver nameless. + return EmptyResolutions.Add(expr, depth); + } + + public Resolutions visitVariantExpr(ScopeStack scopes, Variant expr) + { + if (expr.Argument is not null) + { + return Resolve(scopes, expr.Argument); + } + return EmptyResolutions; + } + + public Resolutions visitWhenExpr(ScopeStack scopes, When expr) + { + return (from _case in expr.Cases + select ResolveCase(_case)) + .Aggregate(Resolve(scopes, expr.Head), AddRange); + + Resolutions ResolveCase(VarBinding _case) + { + var names = new NameCollection(); + _case.Pattern.accept(names, PatternNameCollector.Instance); + return Resolve(scopes, names.Names, _case.Value); + } } } -} \ No newline at end of file +} + diff --git a/TODO.txt b/TODO.txt index 83ddf0c..98e8986 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,4 +1,3 @@ -Wire up resolver to interpreter Move let-expr resolvability check to Resolver Remove dynamic type checks (checkXXX methods) from interpreter Use numerical indices in resolutions