diff --git a/Resolver.cs b/Resolver.cs new file mode 100644 index 0000000..2df1884 --- /dev/null +++ b/Resolver.cs @@ -0,0 +1,297 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Finn; +using Finn.AST; + + + +class Resolver : IExprVisitor +{ + 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 readonly Interpreter Interpreter; + private readonly Stack> scopes = new(); + + Resolver(Interpreter interpreter) + { + Interpreter = interpreter; + } + + private void Resolve(ValueTuple context, Expr expr) + { + expr.accept(context, this); + } + + private void ResolveInNewScope(ValueTuple context, IReadOnlySet vars, Expr expr) + { + BeginScope(vars); + Resolve(context, expr); + EndScope(); + } + + private void BeginScope(IReadOnlySet vars) + { + scopes.Push(vars); + } + + private void EndScope() + { + scopes.Pop(); + } + + public ValueTuple visitBinaryExpr(ValueTuple context, Binary expr) + { + Resolve(context, expr.Left); + Resolve(context, expr.Right); + return context; + } + + public ValueTuple visitCallExpr(ValueTuple context, Call expr) + { + Resolve(context, expr.Left); + foreach (var arg in expr.Arguments) + { + if (arg is not null) + { + Resolve(context, arg); + } + } + return context; + } + + public ValueTuple visitGroupingExpr(ValueTuple context, Grouping expr) + { + Resolve(context, expr.Expression); + return context; + } + + public ValueTuple visitIfExpr(ValueTuple context, If expr) + { + Resolve(context, expr.Condition); + Resolve(context, expr.Then); + Resolve(context, expr.Else); + return context; + } + + public ValueTuple visitIndexerExpr(ValueTuple context, Indexer expr) + { + Resolve(context, expr.Left); + Resolve(context, expr.Index); + return context; + } + + public ValueTuple visitLetExpr(ValueTuple context, 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"); + } + } + + BeginScope(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(context, fb); + break; + case VarBinding vb: + Resolve(context, vb.Value); + break; + default: + throw new Exception("wtf there are no other binding types"); + } + } + Resolve(context, expr.Body); + + EndScope(); + return context; + } + + private void ResolveFunction(ValueTuple context, FuncBinding fb) + { + NameCollection names = new(); + foreach (var param in fb.Params) + { + param.accept(names, PatternNameCollector.Instance); + } + ResolveInNewScope(context, names.Names, fb.Value); + } + + public ValueTuple visitListExpr(ValueTuple context, List expr) + { + foreach (var element in expr.Elements) + { + Resolve(context, element); + } + return context; + } + + public ValueTuple visitLiteralExpr(ValueTuple context, Literal expr) + { + return context; + } + + public ValueTuple visitRecordExpr(ValueTuple context, Record expr) + { + if (expr.Base is not null) + { + Resolve(context, expr.Base.Value); + foreach (var update in expr.Base.Updates) + { + if (update.Value is not null) + { + Resolve(context, update.Value); + } + else + { + // TODO Resolve update.Name like a var expression. + } + } + } + foreach (var extension in expr.Extensions) + { + if (extension.Value is not null) + { + Resolve(context, extension.Value); + } + else + { + // TODO Resolve extension.Name like a var expression. + } + } + return context; + } + + public ValueTuple visitSelectorExpr(ValueTuple context, Selector expr) + { + Resolve(context, expr.Left); + return context; + } + + public ValueTuple visitSequenceExpr(ValueTuple context, Sequence expr) + { + Resolve(context, expr.Left); + Resolve(context, expr.Right); + return context; + } + + public ValueTuple visitUnaryExpr(ValueTuple context, Unary expr) + { + Resolve(context, expr.Right); + return context; + } + + public ValueTuple visitVariableExpr(ValueTuple context, Variable expr) + { + ResolveLocal(expr, expr.Value); + return context; + } + + private void ResolveLocal(Expr expr, Token name) + { + // TODO I can't peek past the top element of the stack (as is correct for a stack). + // Switch to an immutable stack so I can just pop stuff off, or switch to a list. + throw new NotImplementedException(); + } + + public ValueTuple visitVariantExpr(ValueTuple context, Variant expr) + { + if (expr.Argument is not null) + { + Resolve(context, expr.Argument); + } + return context; + } + + public ValueTuple visitWhenExpr(ValueTuple context, When expr) + { + Resolve(context, expr.Head); + foreach (var _case in expr.Cases) + { + var names = new NameCollection(); + _case.Pattern.accept(names, PatternNameCollector.Instance); + ResolveInNewScope(context, names.Names, _case.Value); + } + return context; + } +} \ No newline at end of file diff --git a/TODO.txt b/TODO.txt index 94df5db..3b9c649 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,8 +1,9 @@ -Allow variable defs to reference earlier ones in same binding list +Capitalize stuff to match common C# style +Replace `with` keyword with `but` Figure out multiple-binding let-expr semantics +Clean up visibility Inject error handling into parser and scanner Think about a way to hide record fields - Maybe a compile-time primitive for generating globally unique symbols? Would also need a syntax to use those symbols. - - Use @ as an operator to convert a comptime-known string to an identifier -Replace `with` keyword with `but` \ No newline at end of file + - Use @ as an operator to convert a comptime-known string to an identifier \ No newline at end of file