diff --git a/lang/ast/structs.go b/lang/ast/structs.go index e17f4696a4..dcd65e99fa 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -32,7 +32,9 @@ import ( engineUtil "github.com/purpleidea/mgmt/engine/util" "github.com/purpleidea/mgmt/lang/funcs" "github.com/purpleidea/mgmt/lang/funcs/core" + "github.com/purpleidea/mgmt/lang/funcs/ref" "github.com/purpleidea/mgmt/lang/funcs/structs" + "github.com/purpleidea/mgmt/lang/funcs/txn" "github.com/purpleidea/mgmt/lang/inputs" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" @@ -148,6 +150,16 @@ const ( // ErrNoStoredScope is an error that tells us we can't get a scope here. ErrNoStoredScope = interfaces.Error("scope is not stored in this node") + // ErrCantSpeculate is an error that explains that we can't speculate + // when trying to run the Value() method of Expr. This can be useful if + // we want to distinguish between "something is broken" and "I can't + // produce a value at this time" which can be identified and we can + // continue. If we don't get this error, then it's okay to shut + // everything down. + // XXX: SAM HERE IS A SENTINEL ERROR IN CASE YOU WANT TO USE IT. I THINK + // IT MIGHT ACTUALLY NOT BE NECESSARY, BUT LET'S SEE IF IT'S USEFUL... + ErrCantSpeculate = interfaces.Error("can't speculate for value") + // ErrFuncPointerNil is an error that explains the function pointer for // table lookup is missing. If this happens, it's most likely a // programming error. @@ -5173,6 +5185,10 @@ func (obj *ExprBool) SetScope(scope *interfaces.Scope, sctx map[string]interface return nil } +func (obj *ExprBool) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + return nil +} + // SetType will make no changes if called here. It will error if anything other // than a Bool is passed in, and doesn't need to be called for this expr to // work. @@ -5353,6 +5369,10 @@ func (obj *ExprStr) SetScope(scope *interfaces.Scope, sctx map[string]interfaces return nil } +func (obj *ExprStr) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + return nil +} + // SetType will make no changes if called here. It will error if anything other // than an Str is passed in, and doesn't need to be called for this expr to // work. @@ -5483,6 +5503,10 @@ func (obj *ExprInt) SetScope(scope *interfaces.Scope, sctx map[string]interfaces return nil } +func (obj *ExprInt) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + return nil +} + // SetType will make no changes if called here. It will error if anything other // than an Int is passed in, and doesn't need to be called for this expr to // work. @@ -5615,6 +5639,10 @@ func (obj *ExprFloat) SetScope(scope *interfaces.Scope, sctx map[string]interfac return nil } +func (obj *ExprFloat) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + return nil +} + // SetType will make no changes if called here. It will error if anything other // than a Float is passed in, and doesn't need to be called for this expr to // work. @@ -5828,6 +5856,15 @@ func (obj *ExprList) SetScope(scope *interfaces.Scope, sctx map[string]interface return nil } +func (obj *ExprList) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + for _, x := range obj.Elements { + if err := x.CheckParamScope(freeVars); err != nil { + return err + } + } + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -6260,6 +6297,18 @@ func (obj *ExprMap) SetScope(scope *interfaces.Scope, sctx map[string]interfaces return nil } +func (obj *ExprMap) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + for _, x := range obj.KVs { + if err := x.Key.CheckParamScope(freeVars); err != nil { + return err + } + if err := x.Val.CheckParamScope(freeVars); err != nil { + return err + } + } + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -6732,6 +6781,15 @@ func (obj *ExprStruct) SetScope(scope *interfaces.Scope, sctx map[string]interfa return nil } +func (obj *ExprStruct) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + for _, x := range obj.Fields { + if err := x.Value.CheckParamScope(freeVars); err != nil { + return err + } + } + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -7209,7 +7267,6 @@ func (obj *ExprFunc) Copy() (interfaces.Expr, error) { // Ordering returns a graph of the scope ordering that represents the data flow. // This can be used in SetScope so that it knows the correct order to run it in. -// XXX: do we need to add ordering around named args, eg: obj.Args Name strings? func (obj *ExprFunc) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { graph, err := pgraph.NewGraph("ordering") if err != nil { @@ -7237,7 +7294,6 @@ func (obj *ExprFunc) Ordering(produces map[string]interfaces.Node) (*pgraph.Grap cons := make(map[interfaces.Node]string) - // XXX: do we need ordering for other aspects of ExprFunc ? if obj.Body != nil { g, c, err := obj.Body.Ordering(newProduces) if err != nil { @@ -7303,6 +7359,23 @@ func (obj *ExprFunc) SetScope(scope *interfaces.Scope, sctx map[string]interface return nil } +func (obj *ExprFunc) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + if obj.Body != nil { + newFreeVars := make(map[interfaces.Expr]struct{}) + for k, v := range freeVars { + newFreeVars[k] = v + } + for _, param := range obj.params { + newFreeVars[param] = struct{}{} + } + + if err := obj.Body.CheckParamScope(newFreeVars); err != nil { + return err + } + } + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -7614,7 +7687,7 @@ func (obj *ExprFunc) Graph(env map[string]interfaces.Func) (*pgraph.Graph, inter var funcValueFunc interfaces.Func if obj.Body != nil { funcValueFunc = structs.FuncValueToConstFunc(&full.FuncValue{ - V: func(innerTxn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { + Timeful: func(innerTxn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { // Extend the environment with the arguments. extendedEnv := make(map[string]interfaces.Func) for k, v := range env { @@ -7643,8 +7716,10 @@ func (obj *ExprFunc) Graph(env map[string]interfaces.Func) (*pgraph.Graph, inter // an output value, but we need to construct a node which takes no // inputs and produces a FuncValue, so we need to wrap it. + // TODO: if the builtin function is known to be timeless, use Timeless + // instead of Timeful funcValueFunc = structs.FuncValueToConstFunc(&full.FuncValue{ - V: func(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { + Timeful: func(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { // Copy obj.function so that the underlying ExprFunc.function gets // refreshed with a new ExprFunc.Function() call. Otherwise, multiple // calls to this function will share the same Func. @@ -7714,12 +7789,66 @@ func (obj *ExprFunc) SetValue(value types.Value) error { // This might get called speculatively (early) during unification to learn more. // This particular value is always known since it is a constant. func (obj *ExprFunc) Value() (types.Value, error) { - panic("ExprFunc does not store its latest value because resources don't yet have function fields.") - //// TODO: implement speculative value lookup (if not already sufficient) - //return &full.FuncValue{ - // V: obj.V, - // T: obj.typ, - //}, nil + if obj.Body != nil { + // We can only return a Value if we know the value of all the ExprParams. + // We don't have an environment, so this is only possible if there are no + // ExprParams at all. + if err := obj.CheckParamScope(make(map[interfaces.Expr]struct{})); err != nil { + // return the sentinel value + return nil, ErrCantSpeculate + } + + return &full.FuncValue{ + Timeful: func(innerTxn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { + // There are no ExprParams, so we start with the empty environment. + // Extend that environment with the arguments. + extendedEnv := make(map[string]interfaces.Func) + for i, arg := range obj.Args { + extendedEnv[arg.Name] = args[i] + } + + // Create a subgraph from the lambda's body, instantiating the + // lambda's parameters with the args and the other variables + // with the nodes in the captured environment. + subgraph, bodyFunc, err := obj.Body.Graph(extendedEnv) + if err != nil { + return nil, errwrap.Wrapf(err, "could not create the lambda body's subgraph") + } + + innerTxn.AddGraph(subgraph) + + return bodyFunc, nil + }, + T: obj.typ, + }, nil + } else if obj.Function != nil { + // TODO: if the builtin is timeless, use SimpleFnToFuncValue instead of + // FuncToFullFuncValue + + // Copy obj.function so that the underlying ExprFunc.function gets + // refreshed with a new ExprFunc.Function() call. Otherwise, multiple + // calls to this function will share the same Func. + exprCopy, err := obj.Copy() + if err != nil { + return nil, errwrap.Wrapf(err, "could not copy expression") + } + funcExprCopy, ok := exprCopy.(*ExprFunc) + if !ok { + // programming error + return nil, errwrap.Wrapf(err, "ExprFunc.Copy() does not produce an ExprFunc") + } + valueTransformingFunc := funcExprCopy.function + return structs.FuncToFullFuncValue(obj.Title, valueTransformingFunc, obj.typ), nil + } + // else if /* len(obj.Values) > 0 */ + panic("what to do here") + // polymorphic case: figure out which one has the correct type and wrap + // it in a full.FuncValue. + + return &full.FuncValue{ + //V: obj.V, // XXX ??? + T: obj.typ, + }, nil } // ExprCall is a representation of a function call. This does not represent the @@ -8022,6 +8151,22 @@ func (obj *ExprCall) SetScope(scope *interfaces.Scope, sctx map[string]interface return nil } +func (obj *ExprCall) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + if obj.expr != nil { + if err := obj.expr.CheckParamScope(freeVars); err != nil { + return err + } + } + + for _, x := range obj.Args { + if err := x.CheckParamScope(freeVars); err != nil { + return err + } + } + + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -8553,9 +8698,50 @@ func (obj *ExprCall) Graph(env map[string]interfaces.Func) (*pgraph.Graph, inter graph.AddVertex(paramFunc) funcValueFunc = paramFunc } else { - // The function being called is a top-level definition. The parameters which - // are visible at this use site must not be visible at the definition site, - // so we pass an empty environment. + // The function being called is a top-level definition. + + // Optimization: in the common case in which the function is + // statically-known, generate a single static sub-graph. + exprValue, err := obj.expr.Value() + if err == nil { + exprFuncValue, ok := exprValue.(*full.FuncValue) + if ok { + txn := (&txn.GraphTxn{ + GraphAPI: (&txn.Graph{ + Debug: obj.data.Debug, + Logf: func(format string, v ...interface{}) { + obj.data.Logf(format, v...) + }, + }).Init(), + Lock: func() {}, + Unlock: func() {}, + RefCount: (&ref.Count{}).Init(), + }).Init() + args := []interfaces.Func{} + for i, arg := range obj.Args { // []interfaces.Expr + g, f, err := arg.Graph(env) + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not get graph for arg %d", i) + } + args = append(args, f) + txn.AddGraph(g) + } + outputFunc, err := structs.CallFuncValue(exprFuncValue, txn, args) + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not construct the static graph for a function call") + } + + txn.Commit() + return txn.Graph(), outputFunc, nil + } + } + + // Otherwise, generate a CallFunc node, which will dynamically + // re-create the sub-graph as needed. + + // The parameters which are visible at this use site must not + // be visible at the definition site, so we pass an empty + // environment. emptyEnv := map[string]interfaces.Func{} exprGraph, topLevelFunc, err := obj.expr.Graph(emptyEnv) if err != nil { @@ -8613,10 +8799,31 @@ func (obj *ExprCall) SetValue(value types.Value) error { // This particular implementation of the function returns the previously stored // and cached value as received by SetValue. func (obj *ExprCall) Value() (types.Value, error) { - if obj.V == nil { + if obj.expr == nil { return nil, fmt.Errorf("func value does not yet exist") } - return obj.V, nil + + // speculatively call Value() on obj.expr and each arg. + value, err := obj.expr.Value() // speculative + if err != nil { + return nil, err + } + + funcValue, ok := value.(*full.FuncValue) + if !ok { + return nil, fmt.Errorf("not a func value") + } + + args := []types.Value{} + for _, arg := range obj.Args { // []interfaces.Expr + a, err := arg.Value() // speculative + if err != nil { + return nil, err + } + args = append(args, a) + } + + return structs.CallTimelessFuncValue(funcValue, args) // speculative } // ExprVar is a representation of a variable lookup. It returns the expression @@ -8741,6 +8948,14 @@ func (obj *ExprVar) SetScope(scope *interfaces.Scope, sctx map[string]interfaces return nil } +func (obj *ExprVar) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + target := obj.scope.Variables[obj.Name] + if err := target.CheckParamScope(freeVars); err != nil { + return err + } + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -8953,6 +9168,14 @@ func (obj *ExprParam) SetScope(scope *interfaces.Scope, sctx map[string]interfac return nil } +func (obj *ExprParam) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + if _, exists := freeVars[obj]; !exists { + return fmt.Errorf("the body uses parameter $%s", obj.Name) + } + + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -9085,6 +9308,10 @@ func (obj *ExprPoly) SetScope(scope *interfaces.Scope, sctx map[string]interface panic("ExprPoly.SetScope(): should not happen, ExprVar should replace ExprPoly with a copy of its definition before calling SetScope") } +func (obj *ExprPoly) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + panic("ExprPoly.CheckParamScope(): should not happen, ExprVar should replace ExprPoly with a copy of its definition before calling SetScope") +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -9242,6 +9469,10 @@ func (obj *ExprTopLevel) SetScope(scope *interfaces.Scope, sctx map[string]inter return obj.Definition.SetScope(obj.CapturedScope, make(map[string]interfaces.Expr)) } +func (obj *ExprTopLevel) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + return obj.Definition.CheckParamScope(freeVars) +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -9422,6 +9653,10 @@ func (obj *ExprSingleton) SetScope(scope *interfaces.Scope, sctx map[string]inte return obj.Definition.SetScope(scope, sctx) } +func (obj *ExprSingleton) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + return obj.Definition.CheckParamScope(freeVars) +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -9709,6 +9944,19 @@ func (obj *ExprIf) SetScope(scope *interfaces.Scope, sctx map[string]interfaces. return obj.Condition.SetScope(scope, sctx) } +func (obj *ExprIf) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + if err := obj.Condition.CheckParamScope(freeVars); err != nil { + return err + } + if err := obj.ThenBranch.CheckParamScope(freeVars); err != nil { + return err + } + if err := obj.ElseBranch.CheckParamScope(freeVars); err != nil { + return err + } + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't diff --git a/lang/funcs/core/iter/map_func.go b/lang/funcs/core/iter/map_func.go index d733f7b561..6ffaf97149 100644 --- a/lang/funcs/core/iter/map_func.go +++ b/lang/funcs/core/iter/map_func.go @@ -794,7 +794,7 @@ func (obj *MapFunc) replaceSubGraph(subgraphInput interfaces.Func) error { ) obj.init.Txn.AddVertex(inputElemFunc) - outputElemFunc, err := obj.lastFuncValue.Call(obj.init.Txn, []interfaces.Func{inputElemFunc}) + outputElemFunc, err := structs.CallFuncValue(obj.lastFuncValue, obj.init.Txn, []interfaces.Func{inputElemFunc}) if err != nil { return errwrap.Wrapf(err, "could not call obj.lastFuncValue.Call()") } diff --git a/lang/funcs/facts/facts.go b/lang/funcs/facts/facts.go index 0e1eb65718..5057691078 100644 --- a/lang/funcs/facts/facts.go +++ b/lang/funcs/facts/facts.go @@ -86,3 +86,16 @@ type Fact interface { Init(*Init) error Stream(context.Context) error } + +// CallableFunc is a function that can be called statically if we want to do it +// speculatively or from a resource. +type CallableFact interface { + Fact // implement everything in Fact but add the additional requirements + + // Call this function with the input args and return the value if it is + // possible to do so at this time. To transform from the single value, + // graph representation of the callable values into a linear, standard + // args list for use here, you can use the StructToCallableArgs + // function. + Call() (types.Value, error) +} diff --git a/lang/funcs/facts/func.go b/lang/funcs/facts/func.go index b4c3c56ec7..6e9eed3851 100644 --- a/lang/funcs/facts/func.go +++ b/lang/funcs/facts/func.go @@ -79,3 +79,15 @@ func (obj *FactFunc) Init(init *interfaces.Init) error { func (obj *FactFunc) Stream(ctx context.Context) error { return obj.Fact.Stream(ctx) } + +// Call means this function implements the CallableFunc interface and can be +// called statically if we want to do it speculatively or from a resource. +func (obj *FactFunc) Call() (types.Value, error) { + + callableFact, ok := obj.Fact.(CallableFact) + if !ok { + return nil, fmt.Errorf("fact is not a CallableFact") + } + + return callableFact.Call() +} diff --git a/lang/funcs/simple/simple.go b/lang/funcs/simple/simple.go index 7ab5d266d4..4d93a50ad4 100644 --- a/lang/funcs/simple/simple.go +++ b/lang/funcs/simple/simple.go @@ -153,15 +153,18 @@ func (obj *WrappedFunc) Stream(ctx context.Context) error { obj.last = input // store for next } - values := []types.Value{} + // Use the existing implementation instead of this one + // which can't handle this case at the moment. + //args, err := interfaces.StructToCallableArgs(input) + args := []types.Value{} for _, name := range obj.Fn.Type().Ord { x := input.Struct()[name] - values = append(values, x) + args = append(args, x) } - result, err := obj.Fn.Call(values) // (Value, error) + result, err := obj.Call(args) if err != nil { - return errwrap.Wrapf(err, "simple function errored") + return err } // TODO: do we want obj.result to be a pointer instead? @@ -184,3 +187,13 @@ func (obj *WrappedFunc) Stream(ctx context.Context) error { } } } + +// Call means this function implements the CallableFunc interface and can be +// called statically if we want to do it speculatively or from a resource. +func (obj *WrappedFunc) Call(args []types.Value) (types.Value, error) { + result, err := obj.Fn.Call(args) // (Value, error) + if err != nil { + return nil, errwrap.Wrapf(err, "simple function errored") + } + return result, err +} diff --git a/lang/funcs/simplepoly/simplepoly.go b/lang/funcs/simplepoly/simplepoly.go index 1c0d237b47..df3d9a31be 100644 --- a/lang/funcs/simplepoly/simplepoly.go +++ b/lang/funcs/simplepoly/simplepoly.go @@ -581,24 +581,18 @@ func (obj *WrappedFunc) Stream(ctx context.Context) error { obj.last = input // store for next } - values := []types.Value{} + // Use the existing implementation instead of this one + // which can't handle this case at the moment. + //args, err := interfaces.StructToCallableArgs(input) + args := []types.Value{} for _, name := range obj.fn.Type().Ord { x := input.Struct()[name] - values = append(values, x) + args = append(args, x) } - if obj.init.Debug { - obj.init.Logf("Calling function with: %+v", values) - } - result, err := obj.fn.Call(values) // (Value, error) + result, err := obj.Call(args) if err != nil { - if obj.init.Debug { - obj.init.Logf("Function returned error: %+v", err) - } - return errwrap.Wrapf(err, "simple poly function errored") - } - if obj.init.Debug { - obj.init.Logf("Function returned with: %+v", result) + return err } // TODO: do we want obj.result to be a pointer instead? @@ -621,3 +615,13 @@ func (obj *WrappedFunc) Stream(ctx context.Context) error { } } } + +// Call means this function implements the CallableFunc interface and can be +// called statically if we want to do it speculatively or from a resource. +func (obj *WrappedFunc) Call(args []types.Value) (types.Value, error) { + result, err := obj.fn.Call(args) // (Value, error) + if err != nil { + return nil, errwrap.Wrapf(err, "simple poly function errored") + } + return result, err +} diff --git a/lang/funcs/structs/call.go b/lang/funcs/structs/call.go index a6ebbf4e29..202050d615 100644 --- a/lang/funcs/structs/call.go +++ b/lang/funcs/structs/call.go @@ -211,7 +211,7 @@ func (obj *CallFunc) replaceSubGraph(newFuncValue *full.FuncValue) error { // methods called on it. Nothing else. It will _not_ call Commit or // Reverse. It adds to the graph, and our Commit and Reverse operations // are the ones that actually make the change. - outputFunc, err := newFuncValue.Call(obj.init.Txn, obj.ArgVertices) + outputFunc, err := CallFuncValue(newFuncValue, obj.init.Txn, obj.ArgVertices) if err != nil { return errwrap.Wrapf(err, "could not call newFuncValue.Call()") } diff --git a/lang/funcs/structs/const.go b/lang/funcs/structs/const.go index 34da9ae3df..970f5f5641 100644 --- a/lang/funcs/structs/const.go +++ b/lang/funcs/structs/const.go @@ -90,3 +90,19 @@ func (obj *ConstFunc) Stream(ctx context.Context) error { close(obj.init.Output) // signal that we're done sending return nil } + +// Call means this function implements the CallableFunc interface and can be +// called statically if we want to do it speculatively or from a resource. +func (obj *ConstFunc) Call(args []types.Value) (types.Value, error) { + if obj.Info() == nil { + return nil, fmt.Errorf("info is empty") + } + if obj.Info().Sig == nil { + return nil, fmt.Errorf("sig is empty") + } + if i, j := len(args), len(obj.Info().Sig.Ord); i != j { + return nil, fmt.Errorf("arg length doesn't match, got %d, exp: %d", i, j) + } + + return obj.Value, nil +} diff --git a/lang/funcs/structs/if.go b/lang/funcs/structs/if.go index 7ad33d161a..ee0bf00e44 100644 --- a/lang/funcs/structs/if.go +++ b/lang/funcs/structs/if.go @@ -103,12 +103,13 @@ func (obj *IfFunc) Stream(ctx context.Context) error { } obj.last = input // store for next - var result types.Value - - if input.Struct()["c"].Bool() { - result = input.Struct()["a"] // true branch - } else { - result = input.Struct()["b"] // false branch + args, err := interfaces.StructToCallableArgs(input) // []types.Value, error + if err != nil { + return err + } + result, err := obj.Call(args) + if err != nil { + return err } // skip sending an update... @@ -129,3 +130,22 @@ func (obj *IfFunc) Stream(ctx context.Context) error { } } } + +// Call means this function implements the CallableFunc interface and can be +// called statically if we want to do it speculatively or from a resource. +func (obj *IfFunc) Call(args []types.Value) (types.Value, error) { + if obj.Info() == nil { + return nil, fmt.Errorf("info is empty") + } + if obj.Info().Sig == nil { + return nil, fmt.Errorf("sig is empty") + } + if i, j := len(args), len(obj.Info().Sig.Ord); i != j { + return nil, fmt.Errorf("arg length doesn't match, got %d, exp: %d", i, j) + } + + if args[0].Bool() { // condition + return args[1], nil // true branch + } + return args[2], nil // false branch +} diff --git a/lang/funcs/structs/util.go b/lang/funcs/structs/util.go index 60f60b83e8..0288a9a21d 100644 --- a/lang/funcs/structs/util.go +++ b/lang/funcs/structs/util.go @@ -18,6 +18,8 @@ package structs import ( + "fmt" + "github.com/purpleidea/mgmt/lang/funcs/simple" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" @@ -51,18 +53,9 @@ func SimpleFnToDirectFunc(name string, fv *types.FuncValue) interfaces.Func { // *full.FuncValue. func SimpleFnToFuncValue(name string, fv *types.FuncValue) *full.FuncValue { return &full.FuncValue{ - V: func(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { - wrappedFunc := SimpleFnToDirectFunc(name, fv) - txn.AddVertex(wrappedFunc) - for i, arg := range args { - argName := fv.T.Ord[i] - txn.AddEdge(arg, wrappedFunc, &interfaces.FuncEdge{ - Args: []string{argName}, - }) - } - return wrappedFunc, nil - }, - T: fv.T, + Name: &name, + Timeless: fv, + T: fv.T, } } @@ -72,3 +65,50 @@ func SimpleFnToFuncValue(name string, fv *types.FuncValue) *full.FuncValue { func SimpleFnToConstFunc(name string, fv *types.FuncValue) interfaces.Func { return FuncValueToConstFunc(SimpleFnToFuncValue(name, fv)) } + +// FuncToFullFuncValue creates a *full.FuncValue which adds the given +// interfaces.Func to the graph. Note that this means the *full.FuncValue +// can only be called once. +func FuncToFullFuncValue(name string, valueTransformingFunc interfaces.Func, typ *types.Type) *full.FuncValue { + return &full.FuncValue{ + Name: &name, + Timeful: func(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { + for i, arg := range args { + argName := typ.Ord[i] + txn.AddEdge(arg, valueTransformingFunc, &interfaces.FuncEdge{ + Args: []string{argName}, + }) + } + return valueTransformingFunc, nil + }, + T: typ, + } +} + +// Call calls the function with the provided txn and args. +func CallFuncValue(obj *full.FuncValue, txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { + if obj.Timeful != nil { + return obj.Timeful(txn, args) + } + + wrappedFunc := SimpleFnToDirectFunc(*obj.Name, obj.Timeless) + txn.AddVertex(wrappedFunc) + for i, arg := range args { + argName := obj.T.Ord[i] + txn.AddEdge(arg, wrappedFunc, &interfaces.FuncEdge{ + Args: []string{argName}, + }) + } + return wrappedFunc, nil +} + +// Speculatively call the function with the provided arguments. +// Only makes sense if the function is timeless (produces a single Value, not a +// stream of values). +func CallTimelessFuncValue(obj *full.FuncValue, args []types.Value) (types.Value, error) { + if obj.Timeless != nil { + return obj.Timeless.V(args) + } + + return nil, fmt.Errorf("cannot call CallIfTimeless on a Timeful function") +} diff --git a/lang/interfaces/ast.go b/lang/interfaces/ast.go index c4749c8250..3ebb2d0d25 100644 --- a/lang/interfaces/ast.go +++ b/lang/interfaces/ast.go @@ -109,6 +109,10 @@ type Expr interface { // SetScope sets the scope here and propagates it downwards. SetScope(*Scope, map[string]Expr) error + // Ensure that only the specified ExprParams are free in this expression. + // XXX: JAMES: REPLACE WITH WITH A CALL TO APPLY() AFTER EVERYTHING WORKS. + CheckParamScope(map[Expr]struct{}) error + // SetType sets the type definitively, and errors if it is incompatible. SetType(*types.Type) error diff --git a/lang/interfaces/func.go b/lang/interfaces/func.go index d79ed888b2..4d60d208ea 100644 --- a/lang/interfaces/func.go +++ b/lang/interfaces/func.go @@ -190,6 +190,19 @@ type OldPolyFunc interface { Build(*types.Type) (*types.Type, error) } +// CallableFunc is a function that can be called statically if we want to do it +// speculatively or from a resource. +type CallableFunc interface { + Func // implement everything in Func but add the additional requirements + + // Call this function with the input args and return the value if it is + // possible to do so at this time. To transform from the single value, + // graph representation of the callable values into a linear, standard + // args list for use here, you can use the StructToCallableArgs + // function. + Call(args []types.Value) (types.Value, error) +} + // NamedArgsFunc is a function that uses non-standard function arg names. If you // don't implement this, then the argnames (if specified) must correspond to the // a, b, c...z, aa, ab...az, ba...bz, and so on sequence. @@ -344,3 +357,33 @@ type Txn interface { // committed. Graph() *pgraph.Graph } + +// StructToCallableArgs transforms the single value, graph representation of the +// callable values into a linear, standard args list. +func StructToCallableArgs(st types.Value) ([]types.Value, error) { + if st == nil { + return nil, fmt.Errorf("empty struct") + } + typ := st.Type() + if typ == nil { + return nil, fmt.Errorf("empty type") + } + if kind := typ.Kind; kind != types.KindStruct { + return nil, fmt.Errorf("incorrect kind, got: %s", kind) + } + structValues := st.Struct() // map[string]types.Value + if structValues == nil { + return nil, fmt.Errorf("empty values") + } + + args := []types.Value{} + for i, x := range typ.Ord { // in the correct order + v, exists := structValues[x] + if !exists { + return nil, fmt.Errorf("invalid input value at %d", i) + } + + args = append(args, v) + } + return args, nil +} diff --git a/lang/interfaces/structs.go b/lang/interfaces/structs.go index 6e5875a1ee..d3aa2a1443 100644 --- a/lang/interfaces/structs.go +++ b/lang/interfaces/structs.go @@ -78,6 +78,8 @@ func (obj *ExprAny) Ordering(produces map[string]Node) (*pgraph.Graph, map[Node] // does not need to know about the parent scope. func (obj *ExprAny) SetScope(*Scope, map[string]Expr) error { return nil } +func (obj *ExprAny) CheckParamScope(freeVars map[Expr]struct{}) error { return nil } + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't diff --git a/lang/interpret_test/TestAstFunc1/optimized-higher-order-function.txtar b/lang/interpret_test/TestAstFunc1/optimized-higher-order-function.txtar new file mode 100644 index 0000000000..e90a553c06 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/optimized-higher-order-function.txtar @@ -0,0 +1,7 @@ +func apply($f, $x) { + $f($x) +} +$add1 = fn($x) { + $x + 1 +} +$z = apply($add, 1) diff --git a/lang/types/full/full.go b/lang/types/full/full.go index beb24ce12f..d28d032d9d 100644 --- a/lang/types/full/full.go +++ b/lang/types/full/full.go @@ -41,22 +41,10 @@ import ( // went horribly wrong. (Think, an internal panic.) type FuncValue struct { types.Base - V func(interfaces.Txn, []interfaces.Func) (interfaces.Func, error) - T *types.Type // contains ordered field types, arg names are a bonus part -} - -// NewFunc creates a new function with the specified type. -func NewFunc(t *types.Type) *FuncValue { - if t.Kind != types.KindFunc { - return nil // sanity check - } - v := func(interfaces.Txn, []interfaces.Func) (interfaces.Func, error) { - return nil, fmt.Errorf("nil function") // TODO: is this correct? - } - return &FuncValue{ - V: v, - T: t, - } + Name *string + Timeful func(interfaces.Txn, []interfaces.Func) (interfaces.Func, error) + Timeless *types.FuncValue + T *types.Type // contains ordered field types, arg names are a bonus part } // String returns a visual representation of this value. @@ -125,20 +113,3 @@ func (obj *FuncValue) Value() interface{} { //val := reflect.MakeFunc(typ, fn) //return val.Interface() } - -// Func represents the value of this type as a function if it is one. If this is -// not a function, then this panics. -func (obj *FuncValue) Func() interface{} { - return obj.V -} - -// Set sets the function value to be a new function. -func (obj *FuncValue) Set(fn func(interfaces.Txn, []interfaces.Func) (interfaces.Func, error)) error { // TODO: change method name? - obj.V = fn - return nil // TODO: can we do any sort of checking here? -} - -// Call calls the function with the provided txn and args. -func (obj *FuncValue) Call(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { - return obj.V(txn, args) -}