Add some intrinsics tests

This change adds some machinery to make it easier to write evaluator tests,
and also implements some tests for the lumi:runtime/dynamic:isFunction intrinsic.
This commit is contained in:
joeduffy 2017-05-23 08:03:14 -07:00
parent bad62854a9
commit 2bbc4739bd
5 changed files with 276 additions and 58 deletions

View file

@ -218,16 +218,16 @@ var _ Node = (*CallArgument)(nil)
const CallArgumentKind NodeKind = "CallArgument"
type callExpressionNode struct {
type CallExpressionNode struct {
ExpressionNode
Arguments *[]*CallArgument `json:"arguments,omitempty"`
}
func (node *callExpressionNode) GetArguments() *[]*CallArgument { return node.Arguments }
func (node *CallExpressionNode) GetArguments() *[]*CallArgument { return node.Arguments }
// NewExpression allocates a new object and calls its constructor.
type NewExpression struct {
callExpressionNode
CallExpressionNode
Type *TypeToken `json:"type"` // the object type to allocate.
}
@ -239,7 +239,7 @@ const NewExpressionKind NodeKind = "NewExpression"
// InvokeFunctionExpression invokes a target expression that must evaluate to a function.
type InvokeFunctionExpression struct {
callExpressionNode
CallExpressionNode
Function Expression `json:"function"` // a function to invoke (of a function type).
}

View file

@ -26,6 +26,8 @@ import (
)
func (b *binder) bindModuleDeclarations(node *ast.Module, parent *symbols.Package) *symbols.Module {
contract.Assert(node != nil)
contract.Assert(parent != nil)
glog.V(3).Infof("Binding package '%v' module '%v' decls", parent.Name(), node.Name.Ident)
// Create the module symbol and register it.

View file

@ -42,11 +42,11 @@ type Interpreter interface {
Ctx() *binder.Context // the binding context object.
// EvaluatePackage performs evaluation on the given blueprint package.
EvaluatePackage(pkg *symbols.Package, args core.Args)
EvaluatePackage(pkg *symbols.Package, args core.Args) (*rt.Object, *rt.Unwind)
// EvaluateModule performs evaluation on the given module's entrypoint function.
EvaluateModule(mod *symbols.Module, args core.Args)
EvaluateModule(mod *symbols.Module, args core.Args) (*rt.Object, *rt.Unwind)
// EvaluateFunction performs an evaluation of the given function, using the provided arguments.
EvaluateFunction(fnc symbols.Function, this *rt.Object, args core.Args)
EvaluateFunction(fnc symbols.Function, this *rt.Object, args core.Args) (*rt.Object, *rt.Unwind)
// LoadLocation loads a location by symbol; lval controls whether it is an l-value or just a value.
LoadLocation(tree diag.Diagable, sym symbols.Symbol, this *rt.Object, lval bool) *Location
@ -108,7 +108,7 @@ 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.
func (e *evaluator) EvaluatePackage(pkg *symbols.Package, args core.Args) {
func (e *evaluator) EvaluatePackage(pkg *symbols.Package, args core.Args) (*rt.Object, *rt.Unwind) {
glog.Infof("Evaluating package '%v'", pkg.Name())
if e.hooks != nil {
if leave := e.hooks.OnEnterPackage(pkg); leave != nil {
@ -122,16 +122,19 @@ func (e *evaluator) EvaluatePackage(pkg *symbols.Package, args core.Args) {
}
// Search the package for a default module to evaluate.
var ret *rt.Object
var uw *rt.Unwind
defmod := pkg.Default()
if defmod == nil {
e.Diag().Errorf(errors.ErrorPackageHasNoDefaultModule.At(pkg.Tree()), pkg.Name())
} else {
e.EvaluateModule(defmod, args)
ret, uw = e.EvaluateModule(defmod, args)
}
return ret, uw
}
// EvaluateModule performs evaluation on the given module's entrypoint function.
func (e *evaluator) EvaluateModule(mod *symbols.Module, args core.Args) {
func (e *evaluator) EvaluateModule(mod *symbols.Module, args core.Args) (*rt.Object, *rt.Unwind) {
glog.Infof("Evaluating module '%v'", mod.Token())
if e.hooks != nil {
if leave := e.hooks.OnEnterModule(mod); leave != nil {
@ -145,10 +148,12 @@ func (e *evaluator) EvaluateModule(mod *symbols.Module, args core.Args) {
}
// Fetch the module's entrypoint function, erroring out if it doesn't have one.
var ret *rt.Object
var uw *rt.Unwind
hadEntry := false
if entry, has := mod.Members[tokens.EntryPointFunction]; has {
if entryfnc, ok := entry.(symbols.Function); ok {
e.EvaluateFunction(entryfnc, nil, args)
ret, uw = e.EvaluateFunction(entryfnc, nil, args)
hadEntry = true
}
}
@ -156,10 +161,12 @@ func (e *evaluator) EvaluateModule(mod *symbols.Module, args core.Args) {
if !hadEntry {
e.Diag().Errorf(errors.ErrorModuleHasNoEntryPoint.At(mod.Tree()), mod.Name())
}
return ret, uw
}
// EvaluateFunction performs an evaluation of the given function, using the provided arguments, returning its graph.
func (e *evaluator) EvaluateFunction(fnc symbols.Function, this *rt.Object, args core.Args) {
func (e *evaluator) EvaluateFunction(fnc symbols.Function, this *rt.Object, args core.Args) (*rt.Object, *rt.Unwind) {
glog.Infof("Evaluating function '%v'", fnc.Token())
if e.hooks != nil {
if leave := e.hooks.OnEnterFunction(fnc); leave != nil {
@ -217,9 +224,11 @@ func (e *evaluator) EvaluateFunction(fnc symbols.Function, this *rt.Object, args
}
}
var ret *rt.Object
var uw *rt.Unwind
if e.Diag().Success() {
// If the arguments bound correctly, make the call.
if _, uw := e.evalCallSymbol(fnc.Tree(), fnc, this, argos...); uw != nil {
if ret, uw = e.evalCallSymbol(fnc.Tree(), fnc, this, argos...); uw != nil {
// If the call had an unwind out of it, then presumably we have an unhandled exception.
e.issueUnhandledException(uw, errors.ErrorUnhandledException.At(fnc.Tree()))
}
@ -227,6 +236,8 @@ func (e *evaluator) EvaluateFunction(fnc symbols.Function, this *rt.Object, args
// Dump the evaluation state at log-level 5, if it is enabled.
e.dumpEvalState(5)
return ret, uw
}
// Utility functions
@ -609,48 +620,54 @@ func (e *evaluator) evalCall(node diag.Diagable,
var thisVariable *symbols.LocalVariable
var superVariable *symbols.LocalVariable
if sym != nil {
// Set up the appropriate this/super variables, and also ensure that we enter the right module/class context
// (otherwise module-sensitive binding won't work).
switch f := sym.(type) {
case *symbols.ClassMethod:
if f.Static() {
for fsym, done := sym, false; !done; {
contract.Assert(fsym != nil)
// Set up the appropriate this/super variables, and also ensure that we enter the right module/class
// context (otherwise module-sensitive binding won't work).
switch f := fsym.(type) {
case *symbols.ClassMethod:
if f.Static() {
if this != nil {
// A non-nil `this` is okay if we loaded this function from a prototype object.
prototy, isproto := this.Type().(*symbols.PrototypeType)
contract.Assert(isproto)
contract.Assert(prototy.Type == f.Parent)
}
} else {
contract.Assertf(this != nil, "Expect non-nil this to invoke '%v'", f)
if uw := e.checkThis(node, this); uw != nil {
return nil, uw
}
thisVariable = f.Parent.This
superVariable = f.Parent.Super
}
popModule := e.pushModuleScope(f.Parent.Parent)
defer popModule()
popClass := e.pushClassScope(f.Parent, this)
defer popClass()
done = true
case *symbols.ModuleMethod:
if this != nil {
// A non-nil `this` is okay if we loaded this function from a prototype object.
prototy, isproto := this.Type().(*symbols.PrototypeType)
contract.Assert(isproto)
contract.Assert(prototy.Type == f.Parent)
// A non-nil `this` is okay if we loaded this function from a module object. Because modules can
// re-export members from other modules, we cannot require that the type's parent matches.
_, ismod := this.Type().(*symbols.ModuleType)
contract.Assert(ismod)
this = nil // the this parameter isn't required during invocation.
}
} else {
contract.Assertf(this != nil, "Expect non-nil this to invoke '%v'", f)
if uw := e.checkThis(node, this); uw != nil {
return nil, uw
}
thisVariable = f.Parent.This
superVariable = f.Parent.Super
popModule := e.pushModuleScope(f.Parent)
defer popModule()
done = true
case *Intrinsic:
intrinsic = true
fsym = f.Func // swap in the underlying symbol for purposes of this/super/scoping.
default:
contract.Failf("Unrecognized function type during call: %v", reflect.TypeOf(fnc))
}
popModule := e.pushModuleScope(f.Parent.Parent)
defer popModule()
popClass := e.pushClassScope(f.Parent, this)
defer popClass()
case *symbols.ModuleMethod:
if this != nil {
// A non-nil `this` is okay if we loaded this function from a module object. Note that because modules
// can re-export members from other modules, we cannot require that the type's parent matches.
_, ismod := this.Type().(*symbols.ModuleType)
contract.Assert(ismod)
}
popModule := e.pushModuleScope(f.Parent)
defer popModule()
case *Intrinsic:
contract.Assert(this == nil)
intrinsic = true
default:
contract.Failf("Unrecognized function type during call: %v", reflect.TypeOf(fnc))
}
}

View file

@ -41,8 +41,8 @@ func init() {
// Intrinsic is a special intrinsic function whose behavior is implemented by the runtime.
type Intrinsic struct {
Node diag.Diagable // the contextual node representing the place where this intrinsic got created.
Func ast.Function // the underlying function's node (before mapping to an intrinsic).
Node diag.Diagable // the contextual node representing the place where this intrinsic got created.
Func symbols.Function // the underlying function symbol (before mapping to an intrinsic).
Nm tokens.Name
Tok tokens.Token
Sig *symbols.FunctionType
@ -56,8 +56,8 @@ func (node *Intrinsic) Name() tokens.Name { return node.Nm }
func (node *Intrinsic) Token() tokens.Token { return node.Tok }
func (node *Intrinsic) Special() bool { return false }
func (node *Intrinsic) SpecialModInit() bool { return false }
func (node *Intrinsic) Tree() diag.Diagable { return node.Func }
func (node *Intrinsic) Function() ast.Function { return node.Func }
func (node *Intrinsic) Tree() diag.Diagable { return node.Func.Function() }
func (node *Intrinsic) Function() ast.Function { return node.Func.Function() }
func (node *Intrinsic) Signature() *symbols.FunctionType { return node.Sig }
func (node *Intrinsic) String() string { return string(node.Name()) }
@ -68,7 +68,7 @@ func (node *Intrinsic) Invoke(e *evaluator, this *rt.Object, args []*rt.Object)
}
// NewIntrinsic returns a new intrinsic function symbol with the given information.
func NewIntrinsic(tree diag.Diagable, fnc ast.Function, tok tokens.Token, nm tokens.Name,
func NewIntrinsic(tree diag.Diagable, fnc symbols.Function, tok tokens.Token, nm tokens.Name,
sig *symbols.FunctionType, invoker Invoker) *Intrinsic {
return &Intrinsic{
Node: tree,
@ -93,7 +93,7 @@ func MaybeIntrinsic(tree diag.Diagable, sym symbols.Symbol) symbols.Symbol {
if invoker, isintrinsic := Intrinsics[tok]; isintrinsic {
contract.Assertf(tok.HasModuleMember(), "only module member intrinsics currently supported")
name := tokens.Name(tokens.ModuleMember(tok).Name())
sym = NewIntrinsic(tree, s.Function(), tok, name, s.Signature(), invoker)
sym = NewIntrinsic(tree, s, tok, name, s.Signature(), invoker)
}
}
return sym

View file

@ -0,0 +1,199 @@
// Licensed to Pulumi Corporation ("Pulumi") under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// Pulumi licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package eval
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/pulumi/lumi/pkg/compiler/ast"
"github.com/pulumi/lumi/pkg/compiler/binder"
"github.com/pulumi/lumi/pkg/compiler/core"
"github.com/pulumi/lumi/pkg/compiler/metadata"
"github.com/pulumi/lumi/pkg/compiler/types"
"github.com/pulumi/lumi/pkg/pack"
"github.com/pulumi/lumi/pkg/tokens"
"github.com/pulumi/lumi/pkg/util/contract"
"github.com/pulumi/lumi/pkg/workspace"
)
// newTestEval makes an interpreter that can be used for testing purposes.
func newTestEval() (binder.Binder, Interpreter) {
pwd, err := os.Getwd()
contract.Assert(err == nil)
ctx := core.NewContext(pwd, core.DefaultSink(pwd), nil)
w, err := workspace.New(ctx)
contract.Assert(err == nil)
reader := metadata.NewReader(ctx)
b := binder.New(w, ctx, reader)
return b, New(b.Ctx(), nil)
}
var isFunctionIntrin = tokens.ModuleMember("lumi:runtime/dynamic:isFunction")
func makeIsFunctionExprAST(dynamic bool) ast.Expression {
if dynamic {
var loadLumiMod ast.Expression = &ast.LoadDynamicExpression{
Name: &ast.StringLiteral{
Value: isFunctionIntrin.Module().Name().String(),
},
}
return &ast.LoadDynamicExpression{
Object: &loadLumiMod,
Name: &ast.StringLiteral{
Value: isFunctionIntrin.Name().String(),
},
}
}
return &ast.LoadLocationExpression{
Name: &ast.Token{
Tok: tokens.Token(isFunctionIntrin),
},
}
}
func makeTestIsFunctionAST(dynamic bool, realFunc bool) *pack.Package {
// Make the function body.
var body []ast.Statement
// If an intrinsic, we need to import the module so that it's available dynamically through a name.
if dynamic {
body = append(body, &ast.Import{
Referent: &ast.Token{
Tok: tokens.Token(isFunctionIntrin.Module()),
},
Name: &ast.Identifier{
Ident: tokens.Name(isFunctionIntrin.Module().Name().String()),
},
})
}
var invokeArg *ast.CallArgument
if realFunc {
// for real functions, just pass the isFunction function object itself.
loadFuncExpr := makeIsFunctionExprAST(dynamic)
invokeArg = &ast.CallArgument{
Expr: loadFuncExpr,
}
} else {
// for others, just pass a null literal.
invokeArg = &ast.CallArgument{
Expr: &ast.NullLiteral{},
}
}
loadFuncExpr := makeIsFunctionExprAST(dynamic)
var invokeExpr ast.Expression = &ast.InvokeFunctionExpression{
CallExpressionNode: ast.CallExpressionNode{
Arguments: &[]*ast.CallArgument{invokeArg},
},
Function: loadFuncExpr,
}
body = append(body, &ast.ReturnStatement{
Expression: &invokeExpr,
})
// Now return a package with a default module and single entrypoint main function.
return &pack.Package{
Name: "testIsFunction",
Dependencies: &pack.Dependencies{
"lumi": "*",
},
Modules: &ast.Modules{
tokens.ModuleName(".default"): &ast.Module{
DefinitionNode: ast.DefinitionNode{
Name: &ast.Identifier{
Ident: tokens.Name(".default"),
},
},
Members: &ast.ModuleMembers{
tokens.ModuleMemberName(".main"): &ast.ModuleMethod{
FunctionNode: ast.FunctionNode{
ReturnType: &ast.TypeToken{
Tok: types.Bool.TypeToken(),
},
Body: &ast.Block{
Statements: body,
},
},
ModuleMemberNode: ast.ModuleMemberNode{
DefinitionNode: ast.DefinitionNode{
Name: &ast.Identifier{
Ident: tokens.Name(".main"),
},
},
},
},
},
},
},
}
}
// TestIsFunction verifies the `lumi:runtime/dynamic:isFunction` intrinsic.
func TestIsFunction(t *testing.T) {
// variant #1: invoke the function statically, passing a null literal; expect a false return.
{
b, e := newTestEval()
pack := makeTestIsFunctionAST(false, false)
sym := b.BindPackage(pack)
ret, uw := e.EvaluatePackage(sym, nil)
assert.True(t, b.Diag().Success(), "Expected a successful evaluation")
assert.Nil(t, uw, "Did not expect a out-of-the-ordinary unwind to occur (expected a return)")
assert.NotNil(t, ret, "Expected a non-nil return value")
assert.True(t, ret.IsBool(), "Expected a bool return value; got %v", ret.Type())
assert.Equal(t, ret.BoolValue(), false, "Expected a return value of false; got %v", ret.BoolValue())
}
// variant #2: invoke the function dynamically, passing a null literal; expect a false return.
{
b, e := newTestEval()
pack := makeTestIsFunctionAST(true, false)
sym := b.BindPackage(pack)
ret, uw := e.EvaluatePackage(sym, nil)
assert.True(t, b.Diag().Success(), "Expected a successful evaluation")
assert.Nil(t, uw, "Did not expect a out-of-the-ordinary unwind to occur (expected a return)")
assert.NotNil(t, ret, "Expected a non-nil return value")
assert.True(t, ret.IsBool(), "Expected a bool return value; got %v", ret.Type())
assert.Equal(t, ret.BoolValue(), false, "Expected a return value of false; got %v", ret.BoolValue())
}
// variant #3: invoke the function statically, passing a real function; expect a true return.
{
b, e := newTestEval()
pack := makeTestIsFunctionAST(false, true)
sym := b.BindPackage(pack)
ret, uw := e.EvaluatePackage(sym, nil)
assert.True(t, b.Diag().Success(), "Expected a successful evaluation")
assert.Nil(t, uw, "Did not expect a out-of-the-ordinary unwind to occur (expected a return)")
assert.NotNil(t, ret, "Expected a non-nil return value")
assert.True(t, ret.IsBool(), "Expected a bool return value; got %v", ret.Type())
assert.Equal(t, ret.BoolValue(), true, "Expected a return value of true; got %v", ret.BoolValue())
}
// variant #4: invoke the function dynamically, passing a real function; expect a true return.
{
b, e := newTestEval()
pack := makeTestIsFunctionAST(true, true)
sym := b.BindPackage(pack)
ret, uw := e.EvaluatePackage(sym, nil)
assert.True(t, b.Diag().Success(), "Expected a successful evaluation")
assert.Nil(t, uw, "Did not expect a out-of-the-ordinary unwind to occur (expected a return)")
assert.NotNil(t, ret, "Expected a non-nil return value")
assert.True(t, ret.IsBool(), "Expected a bool return value; got %v", ret.Type())
assert.Equal(t, ret.BoolValue(), true, "Expected a return value of true; got %v", ret.BoolValue())
}
}