using System; using System.Collections.Immutable; using Finn; using Finn.AST; class Resolver : IExprVisitor>, ValueTuple> { 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 ValueTuple Void = ValueTuple.Create(); private readonly Interpreter Interpreter; Resolver(Interpreter interpreter) { Interpreter = interpreter; } private void Resolve(ImmutableStack> scopes, Expr expr) { expr.accept(scopes, this); } private void Resolve(ImmutableStack> scopes, ImmutableHashSet newScope, Expr value) { Resolve(scopes.Push(newScope), value); } public ValueTuple visitBinaryExpr(ImmutableStack> scopes, Binary expr) { Resolve(scopes, expr.Left); Resolve(scopes, expr.Right); return Void; } public ValueTuple visitCallExpr(ImmutableStack> scopes, Call expr) { Resolve(scopes, expr.Left); foreach (var arg in expr.Arguments) { if (arg is not null) { Resolve(scopes, arg); } } return Void; } public ValueTuple visitGroupingExpr(ImmutableStack> scopes, Grouping expr) { Resolve(scopes, expr.Expression); return Void; } public ValueTuple visitIfExpr(ImmutableStack> scopes, If expr) { Resolve(scopes, expr.Condition); Resolve(scopes, expr.Then); Resolve(scopes, expr.Else); return Void; } public ValueTuple visitIndexerExpr(ImmutableStack> scopes, Indexer expr) { Resolve(scopes, expr.Left); Resolve(scopes, expr.Index); return Void; } public ValueTuple visitLetExpr(ImmutableStack> scopes, Let expr) { var names = new NameCollection(); 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. foreach (var binding in expr.Bindings) { switch (binding) { case FuncBinding fb: ResolveFunction(newScopes, fb); break; case VarBinding vb: Resolve(newScopes, vb.Value); break; default: throw new Exception("wtf there are no other binding types"); } } Resolve(newScopes, expr.Body); return Void; } private void ResolveFunction(ImmutableStack> scopes, FuncBinding fb) { NameCollection names = new(); foreach (var param in fb.Params) { param.accept(names, PatternNameCollector.Instance); } Resolve(scopes, names.Names, fb.Value); } public ValueTuple visitListExpr(ImmutableStack> scopes, List expr) { foreach (var element in expr.Elements) { Resolve(scopes, element); } return Void; } public ValueTuple visitLiteralExpr(ImmutableStack> scopes, Literal expr) { return Void; } public ValueTuple visitRecordExpr(ImmutableStack> scopes, Record expr) { if (expr.Base is not null) { Resolve(scopes, expr.Base.Value); foreach (var update in expr.Base.Updates) { if (update.Value is not null) { Resolve(scopes, update.Value); } else { // TODO Resolve update.Name like a var expression. } } } foreach (var extension in expr.Extensions) { if (extension.Value is not null) { Resolve(scopes, extension.Value); } else { // TODO Resolve extension.Name like a var expression. } } return Void; } public ValueTuple visitSelectorExpr(ImmutableStack> scopes, Selector expr) { Resolve(scopes, expr.Left); return Void; } public ValueTuple visitSequenceExpr(ImmutableStack> scopes, Sequence expr) { Resolve(scopes, expr.Left); Resolve(scopes, expr.Right); return Void; } public ValueTuple visitUnaryExpr(ImmutableStack> scopes, Unary expr) { Resolve(scopes, expr.Right); return Void; } public ValueTuple visitVariableExpr(ImmutableStack> scopes, Variable expr) { ResolveLocal(scopes, expr, expr.Value); return Void; } private void ResolveLocal(ImmutableStack> scopes, Expr expr, Token name) { int depth = 0; while (!scopes.IsEmpty) { scopes = scopes.Pop(out var scope); if (scope.Contains((string)name.Literal!)) { Interpreter.Resolve(expr, depth); return; } 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. } public ValueTuple visitVariantExpr(ImmutableStack> scopes, Variant expr) { if (expr.Argument is not null) { Resolve(scopes, expr.Argument); } return Void; } public ValueTuple visitWhenExpr(ImmutableStack> scopes, When expr) { Resolve(scopes, expr.Head); foreach (var _case in expr.Cases) { var names = new NameCollection(); _case.Pattern.accept(names, PatternNameCollector.Instance); Resolve(scopes, names.Names, _case.Value); } return Void; } }