From 281805311c79be18e653e2238f876197db58c171 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Wed, 25 Jan 2017 18:38:53 -0800 Subject: [PATCH] Perform package and module evaluation This change looks up the main module from a package, and that module's entrypoint, when performing evaluation. In any case, the arguments are validated and bound to the resulting function's parameters. --- cmd/compile.go | 13 ++-- pkg/compiler/ast/definitions.go | 1 + pkg/compiler/compiler.go | 4 +- pkg/compiler/core/opts.go | 8 +- pkg/compiler/errors/eval.go | 8 +- pkg/compiler/eval/eval.go | 119 ++++++++++++++++++++++++------ pkg/compiler/symbols/module.go | 1 + pkg/tokens/constants.go | 17 ++++- tools/mujs/lib/ast/definitions.ts | 1 + 9 files changed, 136 insertions(+), 36 deletions(-) diff --git a/cmd/compile.go b/cmd/compile.go index 442045a86..6d4c181a5 100644 --- a/cmd/compile.go +++ b/cmd/compile.go @@ -12,6 +12,7 @@ import ( "github.com/marapongo/mu/pkg/compiler" "github.com/marapongo/mu/pkg/compiler/core" "github.com/marapongo/mu/pkg/graph" + "github.com/marapongo/mu/pkg/tokens" "github.com/marapongo/mu/pkg/util/contract" ) @@ -81,8 +82,8 @@ func newCompileCmd() *cobra.Command { // dashdashArgsToMap is a simple args parser that places incoming key/value pairs into a map. These are then used // during MuPackage compilation as inputs to the main entrypoint function. // TODO: this is fairly rudimentary; we eventually want to support arrays, maps, and complex types. -func dashdashArgsToMap(args []string) map[string]interface{} { - mapped := make(map[string]interface{}) +func dashdashArgsToMap(args []string) core.Args { + mapped := make(core.Args) for i := 0; i < len(args); i++ { arg := args[i] @@ -98,18 +99,18 @@ func dashdashArgsToMap(args []string) map[string]interface{} { // Now find a k=v, and split the k/v part. if eq := strings.IndexByte(arg, '='); eq != -1 { // For --k=v, simply store v underneath k's entry. - mapped[arg[:eq]] = arg[eq+1:] + mapped[tokens.Name(arg[:eq])] = arg[eq+1:] } else { if i+1 < len(args) && args[i+1][0] != '-' { // If the next arg doesn't start with '-' (i.e., another flag) use its value. - mapped[arg] = args[i+1] + mapped[tokens.Name(arg)] = args[i+1] i++ } else if arg[0:3] == "no-" { // For --no-k style args, strip off the no- prefix and store false underneath k. - mapped[arg[3:]] = false + mapped[tokens.Name(arg[3:])] = false } else { // For all other --k args, assume this is a boolean flag, and set the value of k to true. - mapped[arg] = true + mapped[tokens.Name(arg)] = true } } } diff --git a/pkg/compiler/ast/definitions.go b/pkg/compiler/ast/definitions.go index c2a460573..f94a8e56e 100644 --- a/pkg/compiler/ast/definitions.go +++ b/pkg/compiler/ast/definitions.go @@ -31,6 +31,7 @@ func (node *DefinitionNode) GetDescription() *string { return node.Description } // Module contains members, including variables, functions, and/or classes. type Module struct { DefinitionNode + Default bool `json:"default,omitempty"` Imports *[]*ModuleToken `json:"imports,omitempty"` Members *ModuleMembers `json:"members,omitempty"` } diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index 3491475c0..8e93f921f 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -149,11 +149,11 @@ func (c *compiler) CompilePackage(pkg *pack.Package) graph.Graph { // Afterwards, we can safely evaluate the MuIL entrypoint, using our MuIL AST interpreter. b := binder.New(c.w, c.ctx, c.reader) - b.BindPackage(pkg) + pkgsym := b.BindPackage(pkg) if !c.Diag().Success() { return nil } e := eval.New(b.Ctx()) - return e.EvaluatePackage(pkg) + return e.EvaluatePackage(pkgsym, c.ctx.Opts.Args) } diff --git a/pkg/compiler/core/opts.go b/pkg/compiler/core/opts.go index 5b739b1de..d45f49e42 100644 --- a/pkg/compiler/core/opts.go +++ b/pkg/compiler/core/opts.go @@ -4,12 +4,16 @@ package core import ( "github.com/marapongo/mu/pkg/diag" + "github.com/marapongo/mu/pkg/tokens" ) +// Args are a set of command line arguments supplied to a blueprint. +type Args map[tokens.Name]interface{} + // Options contains all of the settings a user can use to control the compiler's behavior. type Options struct { - Diag diag.Sink // a sink to use for all diagnostics. - Args map[string]interface{} // optional blueprint arguments passed at the CLI. + Diag diag.Sink // a sink to use for all diagnostics. + Args Args // optional blueprint arguments passed at the CLI. } // DefaultOptions returns the default set of compiler options. diff --git a/pkg/compiler/errors/eval.go b/pkg/compiler/errors/eval.go index c48a4048e..421e262a1 100644 --- a/pkg/compiler/errors/eval.go +++ b/pkg/compiler/errors/eval.go @@ -4,5 +4,11 @@ package errors // Eval errors are in the [1000,2000) range. var ( - ErrorUnhandledException = newError(1000, "An unhandled exception terminated the program: %v") + ErrorUnhandledException = newError(1000, "An unhandled exception terminated the program: %v") + ErrorPackageHasNoDefaultModule = newError(1001, "Package '%v' is missing a default module") + ErrorModuleHasNoEntryPoint = newError(1002, "Module '%v' is missing an entrypoint function") + ErrorFunctionArgMismatch = newError(1003, "Function expected %v arguments, but only got %v") + ErrorFunctionArgIncorrectType = newError(1004, "Function argument has an incorrect type; expected %v, got %v") + ErrorFunctionArgNotFound = newError(1005, "Function argument '%v' was not supplied") + ErrorFunctionArgUnknown = newError(1006, "Function argument '%v' was not recognized") ) diff --git a/pkg/compiler/eval/eval.go b/pkg/compiler/eval/eval.go index ad89aa458..38778d9a6 100644 --- a/pkg/compiler/eval/eval.go +++ b/pkg/compiler/eval/eval.go @@ -3,6 +3,8 @@ package eval import ( + "github.com/golang/glog" + "github.com/marapongo/mu/pkg/compiler/ast" "github.com/marapongo/mu/pkg/compiler/binder" "github.com/marapongo/mu/pkg/compiler/core" @@ -11,7 +13,6 @@ import ( "github.com/marapongo/mu/pkg/compiler/types" "github.com/marapongo/mu/pkg/diag" "github.com/marapongo/mu/pkg/graph" - "github.com/marapongo/mu/pkg/pack" "github.com/marapongo/mu/pkg/tokens" "github.com/marapongo/mu/pkg/util/contract" ) @@ -22,10 +23,12 @@ type Interpreter interface { Ctx() *binder.Context // the binding context object. - // EvaluatePackage performs evaluation on the given blueprint package, starting in its entrypoint. - EvaluatePackage(pkg *pack.Package) graph.Graph + // EvaluatePackage performs evaluation on the given blueprint package. + EvaluatePackage(pkg *symbols.Package, args core.Args) graph.Graph + // EvaluateModule performs evaluation on the given module's entrypoint function. + EvaluateModule(mod *symbols.Module, args core.Args) graph.Graph // EvaluateFunction performs an evaluation of the given function, using the provided arguments, returning its graph. - EvaluateFunction(fnc symbols.Function, args ...interface{}) graph.Graph + EvaluateFunction(fnc symbols.Function, args core.Args) graph.Graph } // New creates an interpreter that can be used to evaluate MuPackages. @@ -52,32 +55,100 @@ var _ Interpreter = (*evaluator)(nil) func (e *evaluator) Ctx() *binder.Context { return e.ctx } func (e *evaluator) Diag() diag.Sink { return e.ctx.Diag } -// EvaluatePackage performs evaluation on the given blueprint package, starting in its entrypoint. -func (e *evaluator) EvaluatePackage(pkg *pack.Package) graph.Graph { - // TODO: find the entrypoint. - // TODO: pair up the ctx args, if any, with the entrypoint's parameters. - // TODO: visit the function. +// EvaluatePackage performs evaluation on the given blueprint package. +func (e *evaluator) EvaluatePackage(pkg *symbols.Package, args core.Args) graph.Graph { + glog.Infof("Evaluating package '%v'", pkg.Name) + if glog.V(2) { + defer glog.V(2).Infof("Evaluation of package '%v' completed w/ %v warnings and %v errors", + pkg.Name, e.Diag().Warnings(), e.Diag().Errors()) + } + + // Search the package for a default module "index" to evaluate. + for _, mod := range pkg.Modules { + if mod.Default() { + return e.EvaluateModule(mod, args) + } + } + + e.Diag().Errorf(errors.ErrorPackageHasNoDefaultModule.At(pkg.Tree()), pkg.Name) + return nil +} + +// EvaluateModule performs evaluation on the given module's entrypoint function. +func (e *evaluator) EvaluateModule(mod *symbols.Module, args core.Args) graph.Graph { + glog.Infof("Evaluating module '%v'", mod.Token()) + if glog.V(2) { + defer glog.V(2).Infof("Evaluation of module '%v' completed w/ %v warnings and %v errors", + mod.Token(), e.Diag().Warnings(), e.Diag().Errors()) + } + + // Fetch the module's entrypoint function, erroring out if it doesn't have one. + if ep, has := mod.Members[tokens.EntryPointFunction]; has { + if epfnc, ok := ep.(symbols.Function); ok { + return e.EvaluateFunction(epfnc, args) + } + } + + e.Diag().Errorf(errors.ErrorModuleHasNoEntryPoint.At(mod.Tree()), mod.Name) return nil } // EvaluateFunction performs an evaluation of the given function, using the provided arguments, returning its graph. -func (e *evaluator) EvaluateFunction(fnc symbols.Function, args ...interface{}) graph.Graph { - // First, turn the arguments into real runtime *Objects. - argObjs := make([]*Object, len(args)) - for i, arg := range args { - argObjs[i] = NewConstantObject(arg) +func (e *evaluator) EvaluateFunction(fnc symbols.Function, args core.Args) graph.Graph { + glog.Infof("Evaluating function '%v'", fnc.Token()) + if glog.V(2) { + defer glog.V(2).Infof("Evaluation of function '%v' completed w/ %v warnings and %v errors", + fnc.Token(), e.Diag().Warnings(), e.Diag().Errors()) } - // Next, make the call. - _, uw := e.evalCall(fnc, argObjs...) - if uw != nil { - // If the call had an unwind out of it, then presumably we have an unhandled exception. - contract.Assert(!uw.Break) - contract.Assert(!uw.Continue) - contract.Assert(!uw.Return) - contract.Assert(uw.Throw) - // TODO: ideally we would have a stack trace to show here. - e.Diag().Errorf(errors.ErrorUnhandledException, uw.Thrown.Data.(string)) + // First, validate any arguments, and turn them into real runtime *Objects. + var argos []*Object + params := fnc.FuncNode().GetParameters() + if params == nil { + if len(args) != 0 { + e.Diag().Errorf(errors.ErrorFunctionArgMismatch.At(fnc.Tree()), 0, len(args)) + } + } else { + if len(*params) != len(args) { + e.Diag().Errorf(errors.ErrorFunctionArgMismatch.At(fnc.Tree()), 0, len(args)) + } + + ptys := fnc.FuncType().Parameters + found := make(map[tokens.Name]bool) + for i, param := range *params { + pname := param.Name.Ident + if arg, has := args[pname]; has { + found[pname] = true + argo := NewConstantObject(arg) + if types.CanConvert(argo.Type, ptys[i]) { + argos = append(argos, argo) + } else { + e.Diag().Errorf(errors.ErrorFunctionArgIncorrectType.At(fnc.Tree()), ptys[i], argo.Type) + return nil + } + } else { + e.Diag().Errorf(errors.ErrorFunctionArgNotFound.At(fnc.Tree()), param.Name) + } + } + for arg := range args { + if !found[arg] { + e.Diag().Errorf(errors.ErrorFunctionArgUnknown.At(fnc.Tree()), arg) + } + } + } + + if e.Diag().Success() { + // If the arguments bound correctly, make the call. + _, uw := e.evalCall(fnc, argos...) + if uw != nil { + // If the call had an unwind out of it, then presumably we have an unhandled exception. + contract.Assert(!uw.Break) + contract.Assert(!uw.Continue) + contract.Assert(!uw.Return) + contract.Assert(uw.Throw) + // TODO: ideally we would have a stack trace to show here. + e.Diag().Errorf(errors.ErrorUnhandledException, uw.Thrown.Data.(string)) + } } // TODO: turn the returned object into a graph. diff --git a/pkg/compiler/symbols/module.go b/pkg/compiler/symbols/module.go index 2eedaf15e..d92243743 100644 --- a/pkg/compiler/symbols/module.go +++ b/pkg/compiler/symbols/module.go @@ -19,6 +19,7 @@ type Module struct { var _ Symbol = (*Module)(nil) func (node *Module) symbol() {} +func (node *Module) Default() bool { return node.Node.Default } func (node *Module) Name() tokens.Name { return node.Node.Name.Ident } func (node *Module) Token() tokens.Token { return tokens.Token( diff --git a/pkg/tokens/constants.go b/pkg/tokens/constants.go index 1bd3f7d99..2770a0ea6 100644 --- a/pkg/tokens/constants.go +++ b/pkg/tokens/constants.go @@ -2,16 +2,31 @@ package tokens -// Accessibility modifiers. +// Module accessibility. type Accessibility string // accessibility modifiers common to all. const ( PublicAccessibility Accessibility = "public" PrivateAccessibility Accessibility = "private" ) +// Class member accessibility. type ClassMemberAccessibility Accessibility // accessibility modifiers for class members. const ( PublicClassAccessibility ClassMemberAccessibility = "public" PrivateClassAccessibility ClassMemberAccessibility = "private" ProtectedClassAccessibility ClassMemberAccessibility = "protected" ) + +// Special variable tokens. +const ( + ThisVariable Name = ".this" // the current object (for class methods). + SuperVariable Name = ".super" // the parent class object (for class methods). +) + +// Special function tokens. +const ( + EntryPointFunction ModuleMemberName = ".main" // the special package entrypoint function. + ModuleInitializerFunction ModuleMemberName = ".init" // the special module initialization function. + ClassConstructorFunction ClassMemberName = ".ctor" // the special class instance constructor function. + ClassInitializerFunction ClassMemberName = ".init" // the special class initialization function. +) diff --git a/tools/mujs/lib/ast/definitions.ts b/tools/mujs/lib/ast/definitions.ts index c277c8fb0..349cdb459 100644 --- a/tools/mujs/lib/ast/definitions.ts +++ b/tools/mujs/lib/ast/definitions.ts @@ -19,6 +19,7 @@ export interface Definition extends Node { // A module contains members, including variables, functions, and/or classes. export interface Module extends Definition { kind: ModuleKind; + default?: boolean; // true if this is the package entrypoint (just one). imports?: ModuleToken[]; // an ordered list of import modules to initialize. members?: ModuleMembers; // a list of members (both private and public, exported ones). }