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.
This commit is contained in:
joeduffy 2017-01-20 14:04:13 -08:00
parent 3f8a317724
commit c182d71c08
11 changed files with 205 additions and 60 deletions

View file

@ -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)
}

View file

@ -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
}

View file

@ -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())
}

View file

@ -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.
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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)
}
}
}
}
}
}

View file

@ -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.

View file

@ -42,6 +42,7 @@ func NewClassSym(node *ast.Class, parent *Module, extends *Type, implements *Typ
Parent: parent,
Extends: extends,
Implements: implements,
Members: make(ClassMemberMap),
}
}

View file

@ -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}
}

View file

@ -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}
}