From c182d71c080f34f3837d67b888da8e90b7695e9e Mon Sep 17 00:00:00 2001 From: joeduffy Date: Fri, 20 Jan 2017 14:04:13 -0800 Subject: [PATCH] Begin binding function bodies This change begins to bind function bodies. This must be done as a second pass over the AST, because dependencies between modules, and even intra-module dependencies, might refer to top-level symbols like types, variables, and functions, and so must be established first. At the moment, the only node kind we handle is ast.Block, which merely pushes and pops lexical scopes; however, the next step is to implement the AST node-specific visitation logic for all statement and expression nodes. I've also rearranged how Scopes work to be a little easier to use. The Scope type now remembers the **Scope slot in which it is rooted, so that we can simply call Push and Pop on Scopes and have the right thing happen. --- pkg/compiler/ast/walk.go | 21 +++++++++--- pkg/compiler/binder/binder.go | 38 ++++++++++------------ pkg/compiler/binder/class.go | 12 +++++++ pkg/compiler/binder/function.go | 57 +++++++++++++++++++++++++++++++++ pkg/compiler/binder/legacy.go | 6 ---- pkg/compiler/binder/module.go | 24 ++++++++++---- pkg/compiler/binder/package.go | 35 +++++++++++++++++--- pkg/compiler/binder/scope.go | 25 +++++++++++---- pkg/compiler/symbols/class.go | 1 + pkg/compiler/symbols/type.go | 20 ++++++------ pkg/compiler/symbols/vars.go | 26 +++++++++++++++ 11 files changed, 205 insertions(+), 60 deletions(-) create mode 100644 pkg/compiler/binder/function.go create mode 100644 pkg/compiler/symbols/vars.go diff --git a/pkg/compiler/ast/walk.go b/pkg/compiler/ast/walk.go index 701a836ea..cf411e678 100644 --- a/pkg/compiler/ast/walk.go +++ b/pkg/compiler/ast/walk.go @@ -13,8 +13,8 @@ type Visitor interface { // which nodes are visitied is specific to the specific visitation API being used. Visit(node Node) Visitor - // PostVisit is invoked after visitation of a given node. - PostVisit(node Node) + // After is invoked after visitation of a given node. + After(node Node) } // Walk visits an AST node and all of its children. It walks the AST in depth-first order. A pre- and/or @@ -174,7 +174,7 @@ func Walk(v Visitor, node Node) { } // Finally let the visitor know that we are done processing this node. - v.PostVisit(node) + v.After(node) } // Inspector is a very simple Visitor implementation; it simply returns true to continue visitation, or false to stop. @@ -187,5 +187,18 @@ func (insp Inspector) Visit(node Node) Visitor { return nil } -func (insp Inspector) PostVisit(node Node) { +func (insp Inspector) After(node Node) { + // nothing to do. +} + +// AfterInspector is a very simple Visitor implementation; it simply runs after visitation has occurred on nodes. +type AfterInspector func(Node) + +func (insp AfterInspector) Visit(node Node) Visitor { + // nothing to do. + return insp +} + +func (insp AfterInspector) After(node Node) { + insp(node) } diff --git a/pkg/compiler/binder/binder.go b/pkg/compiler/binder/binder.go index 55f209d8b..961bbf543 100644 --- a/pkg/compiler/binder/binder.go +++ b/pkg/compiler/binder/binder.go @@ -3,13 +3,14 @@ package binder import ( + "github.com/golang/glog" + "github.com/marapongo/mu/pkg/compiler/ast" "github.com/marapongo/mu/pkg/compiler/core" "github.com/marapongo/mu/pkg/compiler/metadata" "github.com/marapongo/mu/pkg/compiler/symbols" "github.com/marapongo/mu/pkg/diag" "github.com/marapongo/mu/pkg/pack" - "github.com/marapongo/mu/pkg/util/contract" "github.com/marapongo/mu/pkg/workspace" ) @@ -38,8 +39,8 @@ func New(w workspace.W, ctx *core.Context, reader metadata.Reader) Binder { types: make(TypeMap), } - // Create a global scope and populate it with all of the predefined type names. - b.PushScope() + // Create a global scope and populate it with all of the predefined type names. This one's never popped. + NewScope(&b.scope) for _, prim := range symbols.Primitives { b.scope.MustRegister(prim) } @@ -59,26 +60,14 @@ func (b *binder) Diag() diag.Sink { return b.ctx.Diag } -// PushScope creates a new scope with an empty symbol table parented to the existing one. -func (b *binder) PushScope() { - b.scope = NewScope(b.ctx, b.scope) -} - -// PopScope replaces the current scope with its parent. -func (b *binder) PopScope() { - contract.Assertf(b.scope != nil, "Unexpected empty binding scope during PopScope") - b.scope = b.scope.parent -} - // registerFunctionType understands how to turn any function node into a type, and adds it to the type table. This // works for any kind of function-like AST node: module property, class property, or lambda. func (b *binder) registerFunctionType(node ast.Function) { // Make a function type and inject it into the type table. - var params *[]symbols.Type + var params []symbols.Type np := node.GetParameters() if np != nil { - *params = make([]symbols.Type, len(*np)) - for i, param := range *np { + for _, param := range *np { var ptysym symbols.Type // If there was an explicit type, look it up. @@ -91,17 +80,21 @@ func (b *binder) registerFunctionType(node ast.Function) { ptysym = symbols.AnyType } - (*params)[i] = ptysym + params = append(params, ptysym) } } - var ret *symbols.Type + var ret symbols.Type nr := node.GetReturnType() if nr != nil { - *ret = b.scope.LookupType(*nr) + ret = b.scope.LookupType(*nr) } - b.types[node] = symbols.NewFunctionType(params, ret) + tysym := symbols.NewFunctionType(params, ret) + if glog.V(7) { + glog.V(7).Infof("Registered function type: '%v' => %v", node.GetName().Ident, tysym.Name()) + } + b.types[node] = tysym } // registerVariableType understands how to turn any variable node into a type, and adds it to the type table. This @@ -120,5 +113,8 @@ func (b *binder) registerVariableType(node ast.Variable) { tysym = symbols.AnyType } + if glog.V(7) { + glog.V(7).Infof("Registered variable type: '%v' => %v", node.GetName().Ident, tysym.Name()) + } b.types[node] = tysym } diff --git a/pkg/compiler/binder/class.go b/pkg/compiler/binder/class.go index 242156297..e534a6097 100644 --- a/pkg/compiler/binder/class.go +++ b/pkg/compiler/binder/class.go @@ -3,12 +3,16 @@ package binder import ( + "github.com/golang/glog" + "github.com/marapongo/mu/pkg/compiler/ast" "github.com/marapongo/mu/pkg/compiler/symbols" "github.com/marapongo/mu/pkg/util/contract" ) func (b *binder) bindClass(node *ast.Class, parent *symbols.Module) *symbols.Class { + glog.V(3).Infof("Binding module '%v' class '%v'", parent.Name(), node.Name.Ident) + // Bind base type tokens to actual symbols. var extends *symbols.Type if node.Extends != nil { @@ -53,12 +57,16 @@ func (b *binder) bindClassMember(node ast.ClassMember, parent *symbols.Class) sy } func (b *binder) bindClassProperty(node *ast.ClassProperty, parent *symbols.Class) *symbols.ClassProperty { + glog.V(3).Infof("Binding class '%v' property '%v'", parent.Name(), node.Name.Ident) + // Look up this node's type and inject it into the type table. b.registerVariableType(node) return symbols.NewClassPropertySym(node, parent) } func (b *binder) bindClassMethod(node *ast.ClassMethod, parent *symbols.Class) *symbols.ClassMethod { + glog.V(3).Infof("Binding class '%v' method '%v'", parent.Name(), node.Name.Ident) + // Make a function type out of this method and inject it into the type table. b.registerFunctionType(node) @@ -66,3 +74,7 @@ func (b *binder) bindClassMethod(node *ast.ClassMethod, parent *symbols.Class) * // top-level symbols into the type table, we would potentially encounter missing intra-module symbols. return symbols.NewClassMethodSym(node, parent) } + +func (b *binder) bindClassMethodBody(method *symbols.ClassMethod) { + glog.V(3).Infof("Binding class method '%v' body", method.Token()) +} diff --git a/pkg/compiler/binder/function.go b/pkg/compiler/binder/function.go new file mode 100644 index 000000000..4688813c6 --- /dev/null +++ b/pkg/compiler/binder/function.go @@ -0,0 +1,57 @@ +// Copyright 2016 Marapongo, Inc. All rights reserved. + +package binder + +import ( + "github.com/marapongo/mu/pkg/compiler/ast" + "github.com/marapongo/mu/pkg/compiler/symbols" +) + +// bindFunctionBody binds a function body, including a scope, its parameters, and its expressions and statements. +func (b *binder) bindFunctionBody(node ast.Function) { + // Enter a new scope, bind the parameters, and then bind the body using a visitor. + scope := b.scope.Push() + defer scope.Pop() + params := node.GetParameters() + if params != nil { + for _, param := range *params { + // Register this variable's type and associate its name with the identifier. + b.registerVariableType(param) + b.scope.MustRegister(symbols.NewLocalVariableSym(param)) + } + } + + body := node.GetBody() + if body != nil { + v := &astBinder{b} + ast.Walk(v, body) + } +} + +// astBinder is an AST visitor implementation that understands how to deal with all sorts of node types. It +// does not visit children, however, as it relies on the depth-first order walk supplied by the AST package. The +// overall purpose of this is to perform validation, and record types and symbols that're needed during evaluation. +type astBinder struct { + b *binder +} + +var _ ast.Visitor = (*astBinder)(nil) + +func (a *astBinder) Visit(node ast.Node) ast.Visitor { + switch node.(type) { + case *ast.Block: + // Entering a new block requires a fresh lexical scope. + a.b.scope.Push() + } + // TODO: bind the other node kinds. + return a +} + +func (a *astBinder) After(node ast.Node) { + switch node.(type) { + case *ast.Block: + // Exiting a block restores the prior lexical context. + a.b.scope.Pop() + } + // TODO: bind the other node kinds. +} diff --git a/pkg/compiler/binder/legacy.go b/pkg/compiler/binder/legacy.go index c66b21f34..e70a52804 100644 --- a/pkg/compiler/binder/legacy.go +++ b/pkg/compiler/binder/legacy.go @@ -31,9 +31,6 @@ func (b *binder) PrepareStack(stack *ast.Stack) []pack.PackageURLString { stack.Name, b.Diag().Warnings(), b.Diag().Errors()) } - // Push a new scope for this binding pass. - b.PushScope() - // Now perform a phase1 walk of the tree, preparing it for subsequent binding. This must be done as a // separate phase because we won't know what to stick into the symbol table until after this first walk. phase := newBinderPreparePhase(b, stack) @@ -66,9 +63,6 @@ func (b *binder) ValidateStack(stack *ast.Stack) { stack.Name, b.Diag().Warnings(), b.Diag().Errors()) } - // Restore the original scope after this binding pass. - defer b.PopScope() - // Now perform the final validation of the AST. There's nothing to return, it just may issue errors. phase := newBinderValidatePhase(b) v := core.NewInOrderVisitor(phase, nil) diff --git a/pkg/compiler/binder/module.go b/pkg/compiler/binder/module.go index a37cfda2a..72b540969 100644 --- a/pkg/compiler/binder/module.go +++ b/pkg/compiler/binder/module.go @@ -11,7 +11,7 @@ import ( ) func (b *binder) bindModule(node *ast.Module, parent *symbols.Package) *symbols.Module { - glog.V(3).Infof("Binding package %v module %v", parent.Name, node.Name) + glog.V(3).Infof("Binding package '%v' module '%v'", parent.Name(), node.Name.Ident) // First create a module symbol with empty members, so we can use it as a parent below. module := symbols.NewModuleSym(node, parent) @@ -26,15 +26,14 @@ func (b *binder) bindModule(node *ast.Module, parent *symbols.Package) *symbols. // TODO: add the imports to the symbol table. } - // Next, bind each member at the symbolic level; in particular, we do not yet bind bodies of methods. + // Next, bind each member and add it to the module's map. if node.Members != nil { - for memtok, member := range *node.Members { - module.Members[memtok] = b.bindModuleMember(member, module) + // First bind members and add them to the symbol table. Note that this does not bind bodies just yet. The + // reason is that module members may freely reference one another, so we must do this in a second pass. + for nm, member := range *node.Members { + module.Members[nm] = b.bindModuleMember(member, module) } } - // TODO: add these to the the symbol table for binding bodies. - - // TODO: bind the bodies. return module } @@ -56,6 +55,8 @@ func (b *binder) bindModuleMember(node ast.ModuleMember, parent *symbols.Module) } func (b *binder) bindExport(node *ast.Export, parent *symbols.Module) *symbols.Export { + glog.V(3).Infof("Binding module '%v' export '%v'", parent.Name(), node.Name.Ident) + // To bind an export, simply look up the referent symbol and associate this name with it. refsym := b.scope.Lookup(node.Referent) if refsym == nil { @@ -66,12 +67,16 @@ func (b *binder) bindExport(node *ast.Export, parent *symbols.Module) *symbols.E } func (b *binder) bindModuleProperty(node *ast.ModuleProperty, parent *symbols.Module) *symbols.ModuleProperty { + glog.V(3).Infof("Binding module '%v' property '%v'", parent.Name(), node.Name.Ident) + // Look up this node's type and inject it into the type table. b.registerVariableType(node) return symbols.NewModulePropertySym(node, parent) } func (b *binder) bindModuleMethod(node *ast.ModuleMethod, parent *symbols.Module) *symbols.ModuleMethod { + glog.V(3).Infof("Binding module '%v' method '%v'", parent.Name(), node.Name.Ident) + // Make a function type out of this method and inject it into the type table. b.registerFunctionType(node) @@ -79,3 +84,8 @@ func (b *binder) bindModuleMethod(node *ast.ModuleMethod, parent *symbols.Module // top-level symbols into the type table, we would potentially encounter missing intra-module symbols. return symbols.NewModuleMethodSym(node, parent) } + +func (b *binder) bindModuleMethodBody(method *symbols.ModuleMethod) { + glog.V(3).Infof("Binding module method '%v' body", method.Token()) + b.bindFunctionBody(method.Node) +} diff --git a/pkg/compiler/binder/package.go b/pkg/compiler/binder/package.go index e03f7081b..50957e5ab 100644 --- a/pkg/compiler/binder/package.go +++ b/pkg/compiler/binder/package.go @@ -21,11 +21,16 @@ func (b *binder) BindPackage(pkg *pack.Package) *symbols.Package { // Resolve all package dependencies. b.resolvePackageDeps(pkgsym) - // TODO: add these packages and their modules into the symbol table. - // Now bind all of the package's modules (if any). + // Now bind all of the package's modules (if any). This pass does not yet actually bind bodies. b.bindPackageModules(pkgsym) + // TODO: fix up the symbol table. + + // Finally, bind all of the package's method bodies. This second pass is required to ensure that inter-module + // dependencies can resolve to symbols, after reaching the symbol-level fixed point above. + b.bindPackageMethodBodies(pkgsym) + return pkgsym } @@ -41,7 +46,7 @@ func (b *binder) resolvePackageDeps(pkg *symbols.Package) { if err != nil { b.Diag().Errorf(errors.ErrorPackageURLMalformed, depurl, err) } else { - glog.V(3).Infof("Resolving package %v dependency name=%v, url=%v", pkg.Name, dep.Name, dep.URL()) + glog.V(3).Infof("Resolving package '%v' dependency name=%v, url=%v", pkg.Name(), dep.Name, dep.URL()) if depsym := b.resolveDep(dep); depsym != nil { pkg.Dependencies[dep.Name] = depsym } @@ -72,7 +77,7 @@ func (b *binder) resolveDep(dep pack.PackageURL) *symbols.Package { for _, loc := range b.w.DepCandidates(dep) { // See if this candidate actually exists. isMufile := workspace.IsMufile(loc, b.Diag()) - glog.V(5).Infof("Probing for dependency %v at %v: %v", dep, loc, isMufile) + glog.V(5).Infof("Probing for dependency '%v' at '%v': isMufile=%v", dep, loc, isMufile) // If it does, go ahead and read it in, and bind it (recursively). if isMufile { @@ -110,3 +115,25 @@ func (b *binder) bindPackageModules(pkg *symbols.Package) { } } } + +// bindPackageMethodBodies binds all method bodies, in a second pass, after binding all symbol-level information. +func (b *binder) bindPackageMethodBodies(pkg *symbols.Package) { + contract.Require(pkg != nil, "pkg") + + // Just dig through, find all ModuleMethod and ClassMethod symbols, and bind them. + for _, module := range pkg.Modules { + for _, member := range module.Members { + switch m := member.(type) { + case *symbols.ModuleMethod: + b.bindModuleMethodBody(m) + case *symbols.Class: + for _, cmember := range m.Members { + switch cm := cmember.(type) { + case *symbols.ClassMethod: + b.bindClassMethodBody(cm) + } + } + } + } + } +} diff --git a/pkg/compiler/binder/scope.go b/pkg/compiler/binder/scope.go index d2bc971d8..35652ad0d 100644 --- a/pkg/compiler/binder/scope.go +++ b/pkg/compiler/binder/scope.go @@ -3,7 +3,6 @@ package binder import ( - "github.com/marapongo/mu/pkg/compiler/core" "github.com/marapongo/mu/pkg/compiler/symbols" "github.com/marapongo/mu/pkg/tokens" "github.com/marapongo/mu/pkg/util/contract" @@ -11,7 +10,7 @@ import ( // Scope enables lookups and symbols to obey traditional language scoping rules. type Scope struct { - ctx *core.Context + slot **Scope parent *Scope symtbl SymbolTable } @@ -19,13 +18,25 @@ type Scope struct { // SymbolTable is a mapping from symbol token to an actual symbol object representing it. type SymbolTable map[tokens.Token]symbols.Symbol -// NewScope allocates and returns a fresh scope with the optional parent scope attached to it. -func NewScope(ctx *core.Context, parent *Scope) *Scope { - return &Scope{ - ctx: ctx, - parent: parent, +// NewScope allocates and returns a fresh scope using the given slot, populating it. +func NewScope(slot **Scope) *Scope { + scope := &Scope{ + slot: slot, + parent: *slot, symtbl: make(SymbolTable), } + *slot = scope + return scope +} + +// Push creates a new scope with an empty symbol table parented to the existing one. +func (s *Scope) Push() *Scope { + return NewScope(s.slot) +} + +// Pop restores the prior scope into the underlying slot, tossing away the current symbol table. +func (s *Scope) Pop() { + *s.slot = s.parent } // Lookup finds a symbol registered underneath the given token, issuing an error and returning nil if not found. diff --git a/pkg/compiler/symbols/class.go b/pkg/compiler/symbols/class.go index 730b205d9..f8b343e3c 100644 --- a/pkg/compiler/symbols/class.go +++ b/pkg/compiler/symbols/class.go @@ -42,6 +42,7 @@ func NewClassSym(node *ast.Class, parent *Module, extends *Type, implements *Typ Parent: parent, Extends: extends, Implements: implements, + Members: make(ClassMemberMap), } } diff --git a/pkg/compiler/symbols/type.go b/pkg/compiler/symbols/type.go index 9c5d5cc03..9ac7b441b 100644 --- a/pkg/compiler/symbols/type.go +++ b/pkg/compiler/symbols/type.go @@ -74,24 +74,22 @@ func MapTypeToken(key Type, elem Type) tokens.Type { } // FunctionTypeToken creates a new function type token from parameter and return types. -func FunctionTypeToken(params *[]Type, ret *Type) tokens.Type { +func FunctionTypeToken(params []Type, ret Type) tokens.Type { // TODO: consider caching this to avoid creating needless strings. // Stringify the parameters (if any). sparams := "" - if params != nil { - for i, param := range *params { - if i > 0 { - sparams += TypeDecorsFunctionParamSep - } - sparams += string(param.Token()) + for i, param := range params { + if i > 0 { + sparams += TypeDecorsFunctionParamSep } + sparams += string(param.Token()) } // Stringify the return type (if any). sret := "" if ret != nil { - sret = string((*ret).Token()) + sret = string(ret.Token()) } return tokens.Type(fmt.Sprintf(TypeDecorsFunction, sparams, sret)) @@ -136,8 +134,8 @@ func NewMapType(key Type, elem Type) *MapType { // FunctionType is an invocable type, representing a signature with optional parameters and a return type. type FunctionType struct { - Parameters *[]Type - Return *Type + Parameters []Type // an array of optional parameter types. + Return Type // a return type, or nil if "void". } var _ Symbol = (*FunctionType)(nil) @@ -151,6 +149,6 @@ func (node *FunctionType) Token() tokens.Token { } func (node *FunctionType) Tree() diag.Diagable { return nil } -func NewFunctionType(params *[]Type, ret *Type) *FunctionType { +func NewFunctionType(params []Type, ret Type) *FunctionType { return &FunctionType{params, ret} } diff --git a/pkg/compiler/symbols/vars.go b/pkg/compiler/symbols/vars.go new file mode 100644 index 000000000..3e12d99d2 --- /dev/null +++ b/pkg/compiler/symbols/vars.go @@ -0,0 +1,26 @@ +// Copyright 2016 Marapongo, Inc. All rights reserved. + +package symbols + +import ( + "github.com/marapongo/mu/pkg/compiler/ast" + "github.com/marapongo/mu/pkg/diag" + "github.com/marapongo/mu/pkg/tokens" +) + +// LocalVariable is a fully bound local variable symbol. +type LocalVariable struct { + Node *ast.LocalVariable +} + +var _ Symbol = (*LocalVariable)(nil) + +func (node *LocalVariable) symbol() {} +func (node *LocalVariable) Name() tokens.Name { return node.Node.Name.Ident } +func (node *LocalVariable) Token() tokens.Token { return tokens.Token(node.Name()) } +func (node *LocalVariable) Tree() diag.Diagable { return node.Node } + +// NewLocalVariableSym returns a new LocalVariable symbol associated with the given AST node. +func NewLocalVariableSym(node *ast.LocalVariable) *LocalVariable { + return &LocalVariable{Node: node} +}