From 37bb67dffd671861e647085d9f21c57ae0c7eba4 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Mon, 17 Mar 2025 02:31:01 -0400 Subject: [PATCH] lang: Improve graph shape with speculative execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most of the time, we don't need to have a dynamic call sub graph, since the actual function call could be represented statically as it originally was before lambda functions were implemented. Simplifying the graph shape has important performance benefits in terms of both keep the graph smaller (memory, etc) and in avoiding the need to run transactions at runtime (speed) to reshape the graph. Co-authored-by: Samuel Gélineau --- lang/ast/structs.go | 249 ++++++++++++++++-- lang/ast/util.go | 123 +++++++++ lang/core/collect.go | 8 + lang/core/concat.go | 6 + lang/core/contains.go | 6 + lang/core/convert/format_bool.go | 6 + lang/core/convert/parse_bool.go | 6 + lang/core/convert/to_float.go | 6 + lang/core/convert/to_int.go | 6 + lang/core/convert/to_str.go | 6 + lang/core/core_test.go | 4 +- lang/core/datetime/format.go | 6 + lang/core/datetime/hour.go | 6 + lang/core/datetime/now_fact.go | 2 + lang/core/datetime/print.go | 6 + lang/core/datetime/weekday.go | 6 + lang/core/deploy/abspath.go | 19 ++ lang/core/deploy/binary.go | 6 + lang/core/deploy/bootstrap_packages.go | 6 + lang/core/deploy/readfile.go | 22 ++ lang/core/deploy/readfileabs.go | 19 ++ lang/core/embedded/provisioner/provisioner.go | 6 + lang/core/example/answer.go | 6 + lang/core/example/errorbool.go | 6 + lang/core/example/flipflop_fact.go | 3 + lang/core/example/int2str.go | 6 + lang/core/example/nested/hello_func.go | 6 + lang/core/example/plus.go | 6 + lang/core/example/str2int.go | 6 + lang/core/example/vumeter.go | 7 +- lang/core/fmt/printf.go | 12 +- lang/core/golang/template.go | 10 +- lang/core/history.go | 2 + lang/core/iter/filter.go | 8 +- lang/core/iter/map.go | 8 +- lang/core/iter/range.go | 4 +- lang/core/len.go | 6 + lang/core/list/list_concat.go | 12 +- lang/core/list/list_lookup.go | 15 +- lang/core/list_lookup.go | 6 + lang/core/list_lookup_default.go | 6 + lang/core/local/pool.go | 21 +- lang/core/local/vardir.go | 21 +- lang/core/lookup.go | 10 +- lang/core/lookup_default.go | 10 +- lang/core/map/map_keys.go | 6 + lang/core/map/map_lookup.go | 14 +- lang/core/map/map_values.go | 6 + lang/core/map_lookup.go | 6 + lang/core/map_lookup_default.go | 6 + lang/core/math/fortytwo.go | 6 + lang/core/math/minus1.go | 6 + lang/core/math/mod.go | 6 + lang/core/math/pow.go | 6 + lang/core/math/sqrt.go | 6 + lang/core/net/cidr_to_ip.go | 30 +++ lang/core/net/macfmt.go | 18 ++ lang/core/net/macs.go | 6 + lang/core/net/url_parser.go | 6 + lang/core/os/args.go | 7 + lang/core/os/distro.go | 6 + lang/core/os/expand_home.go | 7 + lang/core/os/family.go | 28 ++ lang/core/os/is_virtual.go | 7 + lang/core/os/readfile.go | 5 + lang/core/os/system.go | 2 + lang/core/panic.go | 6 + lang/core/random1.go | 3 + lang/core/regexp/match.go | 6 + lang/core/strings/join_nonempty.go | 6 + lang/core/strings/pad.go | 12 + lang/core/strings/split.go | 6 + lang/core/strings/substring.go | 6 + lang/core/strings/to_lower.go | 6 + lang/core/struct_lookup.go | 7 +- lang/core/struct_lookup_optional.go | 7 +- lang/core/sys/cpucount_fact.go | 2 + lang/core/sys/env.go | 24 ++ lang/core/sys/load_fact.go | 2 + lang/core/sys/uptime_fact.go | 2 + lang/core/test/fastcount_fact.go | 5 + lang/core/test/oneinstance_fact.go | 24 ++ lang/core/util/hostname_mapper.go | 6 + lang/core/value/get.go | 8 + lang/core/world/collect/res.go | 8 + lang/core/world/exchange.go | 2 + lang/core/world/getval.go | 8 + lang/core/world/kvlookup.go | 8 + lang/core/world/schedule.go | 2 + lang/funcs/facts/facts.go | 15 ++ lang/funcs/facts/func.go | 6 +- lang/funcs/funcgen/fixtures/func_base.tpl | 42 +++ .../funcgen/templates/generated_funcs.go.tpl | 7 + lang/funcs/funcs.go | 10 + lang/funcs/multi/multi.go | 24 +- lang/funcs/operators/operators.go | 25 +- lang/funcs/simple/simple.go | 24 +- lang/funcs/structs/call.go | 2 +- lang/funcs/structs/util.go | 40 +++ lang/funcs/wrapped/wrapped.go | 41 ++- lang/interfaces/ast.go | 35 +++ lang/interfaces/func.go | 12 +- .../TestAstFunc1/changing-func.txtar | 18 +- .../TestAstFunc1/classes-polyfuncs.txtar | 13 +- .../TestAstFunc1/doubleclass.txtar | 69 ++--- .../TestAstFunc1/duplicate_resource.txtar | 5 +- .../TestAstFunc1/efficient-lambda.txtar | 17 +- lang/interpret_test/TestAstFunc1/graph5.txtar | 7 +- lang/interpret_test/TestAstFunc1/graph6.txtar | 14 +- lang/interpret_test/TestAstFunc1/graph7.txtar | 7 +- lang/interpret_test/TestAstFunc1/hello0.txtar | 6 +- .../TestAstFunc1/importscope0.txtar | 6 +- .../TestAstFunc1/importscope2.txtar | 6 +- .../TestAstFunc1/lambda-chained.txtar | 28 +- .../TestAstFunc1/module_search1.txtar | 47 ++-- .../TestAstFunc1/polydoubleinclude.txtar | 22 +- .../TestAstFunc1/recursive_module1.txtar | 47 ++-- .../TestAstFunc1/returned-func.txtar | 2 - .../TestAstFunc1/returned-lambda.txtar | 2 - lang/interpret_test/TestAstFunc1/shape0.txtar | 24 ++ lang/interpret_test/TestAstFunc1/shape1.txtar | 12 + lang/interpret_test/TestAstFunc1/shape2.txtar | 16 ++ lang/interpret_test/TestAstFunc1/shape3.txtar | 25 ++ lang/interpret_test/TestAstFunc1/shape4.txtar | 25 ++ lang/interpret_test/TestAstFunc1/shape5.txtar | 34 +++ lang/interpret_test/TestAstFunc1/shape6.txtar | 33 +++ lang/interpret_test/TestAstFunc1/shape7.txtar | 57 ++++ lang/interpret_test/TestAstFunc1/shape8.txtar | 38 +++ .../TestAstFunc1/simple-func1.txtar | 4 +- .../TestAstFunc1/simple-func2.txtar | 17 +- .../TestAstFunc1/simple-lambda1.txtar | 4 +- .../TestAstFunc1/simple-lambda2.txtar | 16 +- .../TestAstFunc1/slow_unification0.txtar | 47 ++-- .../TestAstFunc1/static-function0.txtar | 10 +- .../TestAstFunc1/static-function1.txtar | 42 ++- lang/parser/lexparse_test.go | 9 +- lang/parser/parser.y | 8 +- lang/types/full/full.go | 27 +- lang/types/value.go | 3 + 139 files changed, 1871 insertions(+), 262 deletions(-) create mode 100644 lang/interpret_test/TestAstFunc1/shape0.txtar create mode 100644 lang/interpret_test/TestAstFunc1/shape1.txtar create mode 100644 lang/interpret_test/TestAstFunc1/shape2.txtar create mode 100644 lang/interpret_test/TestAstFunc1/shape3.txtar create mode 100644 lang/interpret_test/TestAstFunc1/shape4.txtar create mode 100644 lang/interpret_test/TestAstFunc1/shape5.txtar create mode 100644 lang/interpret_test/TestAstFunc1/shape6.txtar create mode 100644 lang/interpret_test/TestAstFunc1/shape7.txtar create mode 100644 lang/interpret_test/TestAstFunc1/shape8.txtar diff --git a/lang/ast/structs.go b/lang/ast/structs.go index a02f8d74..f366ec23 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -33,6 +33,7 @@ package ast import ( "bytes" + "context" "fmt" "reflect" "sort" @@ -45,7 +46,9 @@ import ( "github.com/purpleidea/mgmt/lang/core" "github.com/purpleidea/mgmt/lang/embedded" "github.com/purpleidea/mgmt/lang/funcs" + "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" @@ -9474,7 +9477,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 { @@ -9502,7 +9504,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 { @@ -9851,6 +9852,11 @@ func (obj *ExprFunc) Graph(env *interfaces.Env) (*pgraph.Graph, interfaces.Func, var funcValueFunc interfaces.Func if obj.Body != nil { + f := func(ctx context.Context, args []types.Value) (types.Value, error) { + // XXX: Find a way to exercise this function if possible. + //return nil, funcs.ErrCantSpeculate + return nil, fmt.Errorf("not implemented") + } funcValueFunc = structs.FuncValueToConstFunc(&full.FuncValue{ V: func(innerTxn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { // Extend the environment with the arguments. @@ -9888,13 +9894,27 @@ func (obj *ExprFunc) Graph(env *interfaces.Env) (*pgraph.Graph, interfaces.Func, return bodyFunc, nil }, + F: f, T: obj.typ, }) } else if obj.Function != nil { + // Build this "callable" version in case it's available and we + // can use that directly. We don't need to copy it because we + // expect anything that is Callable to be stateless, and so it + // can use the same function call for every instantiation of it. + var fn interfaces.FuncSig + callableFunc, ok := obj.function.(interfaces.CallableFunc) + if ok { + // XXX: this might be dead code, how do we exercise it? + // If the function is callable then the surrounding + // ExprCall will produce a graph containing this func + // instead of calling ExprFunc.Graph(). + fn = callableFunc.Call + } + // obj.function is a node which transforms input values into // an output value, but we need to construct a node which takes no // inputs and produces a FuncValue, so we need to wrap it. - funcValueFunc = structs.FuncValueToConstFunc(&full.FuncValue{ V: func(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { // Copy obj.function so that the underlying ExprFunc.function gets @@ -9919,6 +9939,7 @@ func (obj *ExprFunc) Graph(env *interfaces.Env) (*pgraph.Graph, interfaces.Func, } return valueTransformingFunc, nil }, + F: fn, T: obj.typ, }) } else /* len(obj.Values) > 0 */ { @@ -9967,13 +9988,122 @@ func (obj *ExprFunc) SetValue(value types.Value) error { // This particular value is always known since it is a constant. func (obj *ExprFunc) Value() (types.Value, error) { // Don't panic because we call Value speculatively for partial values! - // XXX: Not implemented - return nil, fmt.Errorf("error: 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 + //return nil, fmt.Errorf("error: ExprFunc does not store its latest value because resources don't yet have function fields") + + 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. + // XXX: If we add in EnvValue as an arg, can we change this up? + if err := checkParamScope(obj, make(map[interfaces.Expr]struct{})); err != nil { + // return the sentinel value + return nil, funcs.ErrCantSpeculate + } + + f := func(ctx context.Context, args []types.Value) (types.Value, error) { + // TODO: make TestAstFunc1/shape8.txtar better... + //extendedValueEnv := interfaces.EmptyValueEnv() // TODO: add me? + //for _, x := range obj.Args { + // extendedValueEnv[???] = ??? + //} + + // XXX: Find a way to exercise this function if possible. + // chained-returned-funcs.txtar will error if we use: + //return nil, fmt.Errorf("not implemented") + return nil, funcs.ErrCantSpeculate + } + + return &full.FuncValue{ + V: 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 := interfaces.EmptyEnv() + //extendedEnv := make(map[string]interfaces.Func) + for i := range obj.Args { + if args[i] == nil { + // XXX: speculation error? + return nil, fmt.Errorf("programming error?") + } + if len(obj.params) <= i { + // XXX: speculation error? + return nil, fmt.Errorf("programming error?") + } + param := obj.params[i] + if param == nil || param.envKey == nil { + // XXX: speculation error? + return nil, fmt.Errorf("programming error?") + } + + extendedEnv.Variables[param.envKey] = &interfaces.FuncSingleton{ + MakeFunc: func() (*pgraph.Graph, interfaces.Func, error) { + f := args[i] + g, err := pgraph.NewGraph("g") + if err != nil { + return nil, nil, err + } + g.AddVertex(f) + return g, f, nil + }, + } + } + + // 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 + }, + F: f, + T: obj.typ, + }, nil + + } else if obj.Function != nil { + copyFunc := func() interfaces.Func { + copyableFunc, isCopyableFunc := obj.function.(interfaces.CopyableFunc) + if obj.function == nil || !isCopyableFunc { + return obj.Function() // force re-build a new pointer here! + } + + // is copyable! + return copyableFunc.Copy() + } + + // Instead of passing in the obj.function, we instead pass in a + // builder function so that this can use that inside of the + // *full.FuncValue implementation to make new functions when it + // gets called. We'll need more than one so they're not the same + // pointer! + return structs.FuncToFullFuncValue(copyFunc, obj.typ), nil + } + // else if /* len(obj.Values) > 0 */ + + // XXX: It's unclear if the below code in this function is correct or + // even tested. + + // polymorphic case: figure out which one has the correct type and wrap + // it in a full.FuncValue. + + index, err := langUtil.FnMatch(obj.typ, obj.Values) + if err != nil { + // programming error + // since type checking succeeded at this point, there should only be one match + return nil, errwrap.Wrapf(err, "multiple matches found") + } + + simpleFn := obj.Values[index] // *types.FuncValue + simpleFn.T = obj.typ // ensure the precise type is set/known + + return &full.FuncValue{ + V: nil, // XXX: do we need to implement this too? + F: simpleFn.V, // XXX: is this correct? + T: obj.typ, + }, nil } // ExprCall is a representation of a function call. This does not represent the @@ -10737,13 +10867,6 @@ func (obj *ExprCall) Graph(env *interfaces.Env) (*pgraph.Graph, interfaces.Func, return nil, nil, errwrap.Wrapf(err, "could not get the type of the function") } - // Find the vertex which produces the FuncValue. - g, funcValueFunc, err := obj.funcValueFunc(env) - if err != nil { - return nil, nil, err - } - graph.AddGraph(g) - // Loop over the arguments, add them to the graph, but do _not_ connect them // to the function vertex. Instead, each time the call vertex (which we // create below) receives a FuncValue from the function node, it creates the @@ -10758,6 +10881,50 @@ func (obj *ExprCall) Graph(env *interfaces.Env) (*pgraph.Graph, interfaces.Func, argFuncs = append(argFuncs, argFunc) } + // Speculate early, in an attempt to get a simpler graph shape. + //exprFunc, ok := obj.expr.(*ExprFunc) + // XXX: Does this need to be .Pure for it to be allowed? + //canSpeculate := !ok || exprFunc.function == nil || (exprFunc.function.Info().Fast && exprFunc.function.Info().Spec) + canSpeculate := true // XXX: use the magic Info fields? + exprValue, err := obj.expr.Value() + exprFuncValue, ok := exprValue.(*full.FuncValue) + if err == nil && ok && canSpeculate { + 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() + txn.AddGraph(graph) // add all of the graphs so far... + + outputFunc, err := exprFuncValue.CallWithFuncs(txn, argFuncs) + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not construct the static graph for a function call") + } + txn.AddVertex(outputFunc) + + if err := txn.Commit(); err != nil { // Must Commit after txn.AddGraph(...) + return nil, nil, err + } + + return txn.Graph(), outputFunc, nil + } else if err != nil && ok && canSpeculate && err != funcs.ErrCantSpeculate { + // This is a permanent error, not a temporary speculation error. + //return nil, nil, err // XXX: Consider adding this... + } + + // Find the vertex which produces the FuncValue. + g, funcValueFunc, err := obj.funcValueFunc(env) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) + // Add a vertex for the call itself. edgeName := structs.CallFuncArgNameFunction callFunc := &structs.CallFunc{ @@ -10867,7 +11034,7 @@ func (obj *ExprCall) SetValue(value types.Value) error { if err := obj.typ.Cmp(value.Type()); err != nil { return err } - obj.V = value + obj.V = value // XXX: is this useful or a good idea? return nil } @@ -10875,13 +11042,46 @@ func (obj *ExprCall) SetValue(value types.Value) error { // usually only be valid once the engine has run and values have been produced. // This might get called speculatively (early) during unification to learn more. // It is often unlikely that this kind of speculative execution finds something. -// This particular implementation of the function returns the previously stored -// and cached value as received by SetValue. +// This particular implementation will run a function if all of the needed +// values are known. This is necessary for getting the efficient graph shape of +// ExprCall. func (obj *ExprCall) Value() (types.Value, error) { - if obj.V == nil { + if obj.V != nil { // XXX: is this useful or a good idea? + return 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. + // XXX: Should we check obj.expr.(*ExprFunc).Info.Pure here ? + 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) + } + + // We now have a *full.FuncValue and a []types.Value. We can't call the + // existing: + // Call(..., []interfaces.Func) interfaces.Func` method on the + // FuncValue, we need a speculative: + // Call(..., []types.Value) types.Value + // method. + return funcValue.CallWithValues(context.TODO(), args) } // ExprVar is a representation of a variable lookup. It returns the expression @@ -11370,6 +11570,11 @@ func (obj *ExprParam) SetValue(value types.Value) error { // usually only be valid once the engine has run and values have been produced. // This might get called speculatively (early) during unification to learn more. func (obj *ExprParam) Value() (types.Value, error) { + // XXX: if value env is an arg in Expr.Value(...) + //value, exists := valueEnv[obj] + //if exists { + // return value, nil + //} return nil, fmt.Errorf("no value for ExprParam") } diff --git a/lang/ast/util.go b/lang/ast/util.go index 59fe658d..7507f973 100644 --- a/lang/ast/util.go +++ b/lang/ast/util.go @@ -321,6 +321,11 @@ func getScope(node interfaces.Expr) (*interfaces.Scope, error) { return expr.scope, nil case *ExprVar: return expr.scope, nil + //case *ExprParam: + //case *ExprIterated: + //case *ExprPoly: + //case *ExprTopLevel: + //case *ExprSingleton: case *ExprIf: return expr.scope, nil @@ -329,6 +334,124 @@ func getScope(node interfaces.Expr) (*interfaces.Scope, error) { } } +// CheckParamScope ensures that only the specified ExprParams are free in the +// expression. It is used for graph shape function speculation. This could have +// been an addition to the interfaces.Expr interface, but since it's mostly +// iteration, it felt cleaner like this. +// TODO: Can we replace this with a call to Apply instead. +func checkParamScope(node interfaces.Expr, freeVars map[interfaces.Expr]struct{}) error { + switch obj := node.(type) { + + case *ExprBool: + return nil + + case *ExprStr: + return nil + + case *ExprInt: + return nil + + case *ExprFloat: + return nil + + case *ExprList: + for _, x := range obj.Elements { + if err := checkParamScope(x, freeVars); err != nil { + return err + } + } + return nil + + case *ExprMap: + for _, x := range obj.KVs { + if err := checkParamScope(x.Key, freeVars); err != nil { + return err + } + if err := checkParamScope(x.Val, freeVars); err != nil { + return err + } + } + return nil + + case *ExprStruct: + for _, x := range obj.Fields { + if err := checkParamScope(x.Value, freeVars); err != nil { + return err + } + } + return nil + + case *ExprFunc: + 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 := checkParamScope(obj.Body, newFreeVars); err != nil { + return err + } + } + // XXX: Do we need to do anything for obj.Function ? + // XXX: Do we need to do anything for obj.Values ? + return nil + + case *ExprCall: + if obj.expr != nil { + if err := checkParamScope(obj.expr, freeVars); err != nil { + return err + } + } + for _, x := range obj.Args { + if err := checkParamScope(x, freeVars); err != nil { + return err + } + } + return nil + + case *ExprVar: + // XXX: is this still correct? + target := obj.scope.Variables[obj.Name] + return checkParamScope(target, freeVars) + + case *ExprParam: + if _, exists := freeVars[obj]; !exists { + return fmt.Errorf("the body uses parameter $%s", obj.Name) + } + return nil + + case *ExprIterated: + return checkParamScope(obj.Definition, freeVars) // XXX: is this what we want? + + case *ExprPoly: + panic("checkParamScope(ExprPoly): should not happen, ExprVar should replace ExprPoly with a copy of its definition before calling checkParamScope") + + case *ExprTopLevel: + return checkParamScope(obj.Definition, freeVars) + + case *ExprSingleton: + return checkParamScope(obj.Definition, freeVars) + + case *ExprIf: + if err := checkParamScope(obj.Condition, freeVars); err != nil { + return err + } + if err := checkParamScope(obj.ThenBranch, freeVars); err != nil { + return err + } + if err := checkParamScope(obj.ElseBranch, freeVars); err != nil { + return err + } + return nil + + default: + return fmt.Errorf("unexpected: %+v", node) + } +} + // trueCallee is a helper function because ExprTopLevel and ExprSingleton are // sometimes added around builtins. This makes it difficult for the type checker // to check if a particular builtin is the callee or not. This function removes diff --git a/lang/core/collect.go b/lang/core/collect.go index fb4085b0..8533aff7 100644 --- a/lang/core/collect.go +++ b/lang/core/collect.go @@ -282,6 +282,8 @@ func (obj *CollectFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: false, Memo: false, + Fast: false, + Spec: false, Sig: sig, Err: obj.Validate(), } @@ -389,6 +391,9 @@ func (obj *CollectFunc) Stream(ctx context.Context) error { // to do so at this time. This was previously getValue which gets the value // we're looking for. func (obj *CollectFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 2 { + return nil, fmt.Errorf("not enough args") + } kind := args[0].Str() if kind == "" { return nil, fmt.Errorf("resource kind is empty") @@ -448,6 +453,9 @@ func (obj *CollectFunc) Call(ctx context.Context, args []types.Value) (types.Val } } + if obj.init == nil { + return nil, funcs.ErrCantSpeculate + } resOutput, err := obj.init.World.ResCollect(ctx, filters) if err != nil { return nil, err diff --git a/lang/core/concat.go b/lang/core/concat.go index 749bdcbf..4c11d84c 100644 --- a/lang/core/concat.go +++ b/lang/core/concat.go @@ -44,6 +44,12 @@ const ( func init() { simple.Register(ConcatFuncName, &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a str, b str) str"), F: Concat, }) diff --git a/lang/core/contains.go b/lang/core/contains.go index cbe66fa5..944799f7 100644 --- a/lang/core/contains.go +++ b/lang/core/contains.go @@ -44,6 +44,12 @@ const ( func init() { simple.Register(ContainsFuncName, &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(needle ?1, haystack []?1) bool"), F: Contains, }) diff --git a/lang/core/convert/format_bool.go b/lang/core/convert/format_bool.go index b7000049..ae74df79 100644 --- a/lang/core/convert/format_bool.go +++ b/lang/core/convert/format_bool.go @@ -39,6 +39,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "format_bool", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a bool) str"), F: FormatBool, }) diff --git a/lang/core/convert/parse_bool.go b/lang/core/convert/parse_bool.go index eeff5f72..760e2bb0 100644 --- a/lang/core/convert/parse_bool.go +++ b/lang/core/convert/parse_bool.go @@ -40,6 +40,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "parse_bool", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a str) bool"), F: ParseBool, }) diff --git a/lang/core/convert/to_float.go b/lang/core/convert/to_float.go index 44cc3e36..627114db 100644 --- a/lang/core/convert/to_float.go +++ b/lang/core/convert/to_float.go @@ -38,6 +38,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "to_float", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a int) float"), F: ToFloat, }) diff --git a/lang/core/convert/to_int.go b/lang/core/convert/to_int.go index 9894a25a..0e1bb375 100644 --- a/lang/core/convert/to_int.go +++ b/lang/core/convert/to_int.go @@ -38,6 +38,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "to_int", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a float) int"), F: ToInt, }) diff --git a/lang/core/convert/to_str.go b/lang/core/convert/to_str.go index 494bbf6c..d08e9943 100644 --- a/lang/core/convert/to_str.go +++ b/lang/core/convert/to_str.go @@ -39,6 +39,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "int_to_str", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a int) str"), F: IntToStr, }) diff --git a/lang/core/core_test.go b/lang/core/core_test.go index e79f3211..e0929106 100644 --- a/lang/core/core_test.go +++ b/lang/core/core_test.go @@ -62,8 +62,8 @@ func PureFuncExec(handle interfaces.Func, args []types.Value) (types.Value, erro return nil, fmt.Errorf("func is not pure") } // if function is expensive to run, we won't run it provisionally - if info.Slow { - return nil, fmt.Errorf("func is slow") + if !info.Fast { + return nil, fmt.Errorf("func is not fast") } sig := handle.Info().Sig diff --git a/lang/core/datetime/format.go b/lang/core/datetime/format.go index e90ded82..f475cc99 100644 --- a/lang/core/datetime/format.go +++ b/lang/core/datetime/format.go @@ -40,6 +40,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "format", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a int, b str) str"), F: Format, }) diff --git a/lang/core/datetime/hour.go b/lang/core/datetime/hour.go index 4635dbe6..335555b7 100644 --- a/lang/core/datetime/hour.go +++ b/lang/core/datetime/hour.go @@ -40,6 +40,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "hour", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a int) int"), F: Hour, }) diff --git a/lang/core/datetime/now_fact.go b/lang/core/datetime/now_fact.go index 2b0e784f..0aa90c6c 100644 --- a/lang/core/datetime/now_fact.go +++ b/lang/core/datetime/now_fact.go @@ -67,6 +67,8 @@ func (obj *DateTimeFact) String() string { // Info returns some static info about itself. func (obj *DateTimeFact) Info() *facts.Info { return &facts.Info{ + Pure: false, + Memo: false, Output: types.NewType("int"), } } diff --git a/lang/core/datetime/print.go b/lang/core/datetime/print.go index 3b247437..c971b9c5 100644 --- a/lang/core/datetime/print.go +++ b/lang/core/datetime/print.go @@ -41,6 +41,12 @@ import ( func init() { // FIXME: consider renaming this to printf, and add in a format string? simple.ModuleRegister(ModuleName, "print", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a int) str"), F: Print, }) diff --git a/lang/core/datetime/weekday.go b/lang/core/datetime/weekday.go index e0520133..98903146 100644 --- a/lang/core/datetime/weekday.go +++ b/lang/core/datetime/weekday.go @@ -41,6 +41,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "weekday", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a int) str"), F: Weekday, }) diff --git a/lang/core/deploy/abspath.go b/lang/core/deploy/abspath.go index c68d751a..6f0f308d 100644 --- a/lang/core/deploy/abspath.go +++ b/lang/core/deploy/abspath.go @@ -51,6 +51,8 @@ func init() { funcs.ModuleRegister(ModuleName, AbsPathFuncName, func() interfaces.Func { return &AbsPathFunc{} }) // must register the func and name } +var _ interfaces.DataFunc = &AbsPathFunc{} + // AbsPathFunc is a function that returns the absolute, full path in the deploy // from an input path that is relative to the calling file. If you pass it an // empty string, you'll just get the absolute deploy directory path that you're @@ -96,6 +98,8 @@ func (obj *AbsPathFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: false, // maybe false because the file contents can change Memo: false, + Fast: false, + Spec: false, Sig: types.NewType(fmt.Sprintf("func(%s str) str", absPathArgNamePath)), } } @@ -166,11 +170,26 @@ func (obj *AbsPathFunc) Stream(ctx context.Context) error { } } +// Copy is implemented so that the obj.built value is not lost if we copy this +// function. +func (obj *AbsPathFunc) Copy() interfaces.Func { + return &AbsPathFunc{ + init: obj.init, // likely gets overwritten anyways + data: obj.data, // needed because we don't call SetData twice + } +} + // Call this function with the input args and return the value if it is possible // to do so at this time. func (obj *AbsPathFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 1 { + return nil, fmt.Errorf("not enough args") + } path := args[0].Str() + if obj.data == nil { + return nil, funcs.ErrCantSpeculate + } p := strings.TrimSuffix(obj.data.Base, "/") if p == obj.data.Base { // didn't trim, so we fail // programming error diff --git a/lang/core/deploy/binary.go b/lang/core/deploy/binary.go index f9141076..c1cc0da8 100644 --- a/lang/core/deploy/binary.go +++ b/lang/core/deploy/binary.go @@ -39,6 +39,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "binary_path", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: false, + Fast: true, + Spec: false, + }, T: types.NewType("func() str"), F: BinaryPath, }) diff --git a/lang/core/deploy/bootstrap_packages.go b/lang/core/deploy/bootstrap_packages.go index 4fb88708..8b537789 100644 --- a/lang/core/deploy/bootstrap_packages.go +++ b/lang/core/deploy/bootstrap_packages.go @@ -40,6 +40,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "bootstrap_packages", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(str) []str"), F: BootstrapPackages, }) diff --git a/lang/core/deploy/readfile.go b/lang/core/deploy/readfile.go index 3462ee72..515ae689 100644 --- a/lang/core/deploy/readfile.go +++ b/lang/core/deploy/readfile.go @@ -52,6 +52,8 @@ func init() { funcs.ModuleRegister(ModuleName, ReadFileFuncName, func() interfaces.Func { return &ReadFileFunc{} }) // must register the func and name } +var _ interfaces.DataFunc = &ReadFileFunc{} + // ReadFileFunc is a function that reads the full contents from a file in our // deploy. The file contents can only change with a new deploy, so this is // static. Please note that this is different from the readfile function in the @@ -97,6 +99,8 @@ func (obj *ReadFileFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: false, // maybe false because the file contents can change Memo: false, + Fast: false, + Spec: false, Sig: types.NewType(fmt.Sprintf("func(%s str) str", readFileArgNameFilename)), } } @@ -168,11 +172,26 @@ func (obj *ReadFileFunc) Stream(ctx context.Context) error { } } +// Copy is implemented so that the obj.built value is not lost if we copy this +// function. +func (obj *ReadFileFunc) Copy() interfaces.Func { + return &ReadFileFunc{ + init: obj.init, // likely gets overwritten anyways + data: obj.data, // needed because we don't call SetData twice + } +} + // Call this function with the input args and return the value if it is possible // to do so at this time. func (obj *ReadFileFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 1 { + return nil, fmt.Errorf("not enough args") + } filename := args[0].Str() + if obj.data == nil { + return nil, funcs.ErrCantSpeculate + } p := strings.TrimSuffix(obj.data.Base, "/") if p == obj.data.Base { // didn't trim, so we fail // programming error @@ -186,6 +205,9 @@ func (obj *ReadFileFunc) Call(ctx context.Context, args []types.Value) (types.Va } path += filename + if obj.init == nil || obj.data == nil { + return nil, funcs.ErrCantSpeculate + } fs, err := obj.init.World.Fs(obj.data.FsURI) // open the remote file system if err != nil { return nil, errwrap.Wrapf(err, "can't load code from file system `%s`", obj.data.FsURI) diff --git a/lang/core/deploy/readfileabs.go b/lang/core/deploy/readfileabs.go index 03d45f22..e8facab1 100644 --- a/lang/core/deploy/readfileabs.go +++ b/lang/core/deploy/readfileabs.go @@ -51,6 +51,8 @@ func init() { funcs.ModuleRegister(ModuleName, ReadFileAbsFuncName, func() interfaces.Func { return &ReadFileAbsFunc{} }) // must register the func and name } +var _ interfaces.DataFunc = &ReadFileAbsFunc{} + // ReadFileAbsFunc is a function that reads the full contents from a file in our // deploy. The file contents can only change with a new deploy, so this is // static. In particular, this takes an absolute path relative to the root @@ -97,6 +99,8 @@ func (obj *ReadFileAbsFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: false, // maybe false because the file contents can change Memo: false, + Fast: false, + Spec: false, Sig: types.NewType(fmt.Sprintf("func(%s str) str", readfileArgNameFilename)), } } @@ -168,11 +172,26 @@ func (obj *ReadFileAbsFunc) Stream(ctx context.Context) error { } } +// Copy is implemented so that the obj.built value is not lost if we copy this +// function. +func (obj *ReadFileAbsFunc) Copy() interfaces.Func { + return &ReadFileAbsFunc{ + init: obj.init, // likely gets overwritten anyways + data: obj.data, // needed because we don't call SetData twice + } +} + // Call this function with the input args and return the value if it is possible // to do so at this time. func (obj *ReadFileAbsFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 1 { + return nil, fmt.Errorf("not enough args") + } filename := args[0].Str() + if obj.init == nil || obj.data == nil { + return nil, funcs.ErrCantSpeculate + } fs, err := obj.init.World.Fs(obj.data.FsURI) // open the remote file system if err != nil { return nil, errwrap.Wrapf(err, "can't load code from file system `%s`", obj.data.FsURI) diff --git a/lang/core/embedded/provisioner/provisioner.go b/lang/core/embedded/provisioner/provisioner.go index 38adb815..1882e1db 100644 --- a/lang/core/embedded/provisioner/provisioner.go +++ b/lang/core/embedded/provisioner/provisioner.go @@ -524,6 +524,12 @@ func (obj *provisioner) Register(moduleName string) error { // Build a few separately... simple.ModuleRegister(moduleName, "cli_password", &simple.Scaffold{ + I: &simple.Info{ + Pure: false, + Memo: false, + Fast: false, + Spec: false, + }, T: types.NewType("func() str"), F: func(ctx context.Context, input []types.Value) (types.Value, error) { if obj.localArgs == nil { diff --git a/lang/core/example/answer.go b/lang/core/example/answer.go index 307ebe3e..f08cafdd 100644 --- a/lang/core/example/answer.go +++ b/lang/core/example/answer.go @@ -41,6 +41,12 @@ const Answer = 42 func init() { simple.ModuleRegister(ModuleName, "answer", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func() int"), F: TheAnswerToLifeTheUniverseAndEverything, }) diff --git a/lang/core/example/errorbool.go b/lang/core/example/errorbool.go index 61ae4170..b8785979 100644 --- a/lang/core/example/errorbool.go +++ b/lang/core/example/errorbool.go @@ -39,6 +39,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "errorbool", &simple.Scaffold{ + I: &simple.Info{ + Pure: false, // XXX: because it errors do we need to be be safer here? + Memo: false, + Fast: true, + Spec: false, + }, T: types.NewType("func(a bool) str"), F: ErrorBool, }) diff --git a/lang/core/example/flipflop_fact.go b/lang/core/example/flipflop_fact.go index 4d3e6500..bef1bbef 100644 --- a/lang/core/example/flipflop_fact.go +++ b/lang/core/example/flipflop_fact.go @@ -123,6 +123,9 @@ func (obj *FlipFlopFact) Stream(ctx context.Context) error { // Call this fact and return the value if it is possible to do so at this time. func (obj *FlipFlopFact) Call(ctx context.Context) (types.Value, error) { + if obj.mutex == nil { + return nil, facts.ErrCantSpeculate + } obj.mutex.Lock() // TODO: could be a read lock value := obj.value obj.mutex.Unlock() diff --git a/lang/core/example/int2str.go b/lang/core/example/int2str.go index a4fb0636..f9545fcf 100644 --- a/lang/core/example/int2str.go +++ b/lang/core/example/int2str.go @@ -39,6 +39,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "int2str", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a int) str"), F: Int2Str, }) diff --git a/lang/core/example/nested/hello_func.go b/lang/core/example/nested/hello_func.go index 4d47f7be..d042df68 100644 --- a/lang/core/example/nested/hello_func.go +++ b/lang/core/example/nested/hello_func.go @@ -39,6 +39,12 @@ import ( func init() { simple.ModuleRegister(coreexample.ModuleName+"/"+ModuleName, "hello", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func() str"), F: Hello, }) diff --git a/lang/core/example/plus.go b/lang/core/example/plus.go index 24bbc2f0..fb34bfe8 100644 --- a/lang/core/example/plus.go +++ b/lang/core/example/plus.go @@ -38,6 +38,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "plus", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(y str, z str) str"), F: Plus, }) diff --git a/lang/core/example/str2int.go b/lang/core/example/str2int.go index 90b2459c..cfee08c4 100644 --- a/lang/core/example/str2int.go +++ b/lang/core/example/str2int.go @@ -39,6 +39,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "str2int", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a str) int"), F: Str2Int, }) diff --git a/lang/core/example/vumeter.go b/lang/core/example/vumeter.go index dad1b02d..436d5eff 100644 --- a/lang/core/example/vumeter.go +++ b/lang/core/example/vumeter.go @@ -131,8 +131,10 @@ func (obj *VUMeterFunc) Validate() error { // Info returns some static info about itself. func (obj *VUMeterFunc) Info() *interfaces.Info { return &interfaces.Info{ - Pure: true, + Pure: false, Memo: false, + Fast: false, + Spec: false, Sig: types.NewType(fmt.Sprintf("func(%s str, %s int, %s float) str", vuMeterArgNameSymbol, vuMeterArgNameMultiplier, vuMeterArgNamePeak)), } } @@ -212,6 +214,9 @@ func (obj *VUMeterFunc) Stream(ctx context.Context) error { // Call this function with the input args and return the value if it is possible // to do so at this time. func (obj *VUMeterFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 3 { + return nil, fmt.Errorf("not enough args") + } symbol := args[0].Str() multiplier := args[1].Int() peak := args[2].Float() diff --git a/lang/core/fmt/printf.go b/lang/core/fmt/printf.go index 8d92dde2..9bb6ad3f 100644 --- a/lang/core/fmt/printf.go +++ b/lang/core/fmt/printf.go @@ -98,7 +98,10 @@ type PrintfFunc struct { // String returns a simple name for this function. This is needed so this struct // can satisfy the pgraph.Vertex interface. func (obj *PrintfFunc) String() string { - return fmt.Sprintf("%s@%p", PrintfFuncName, obj) // be more unique! + if obj.Type != nil { + return fmt.Sprintf("%s: %s", PrintfFuncName, obj.Type) + } + return PrintfFuncName } // ArgGen returns the Nth arg name for this function. @@ -295,7 +298,9 @@ func (obj *PrintfFunc) Info() *interfaces.Info { // getting them from FuncInfer, and not from here. (During unification!) return &interfaces.Info{ Pure: true, - Memo: false, + Memo: true, + Fast: true, + Spec: true, Sig: obj.Type, Err: obj.Validate(), } @@ -367,6 +372,9 @@ func (obj *PrintfFunc) Copy() interfaces.Func { // Call this function with the input args and return the value if it is possible // to do so at this time. func (obj *PrintfFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 1 { + return nil, fmt.Errorf("not enough args") + } format := args[0].Str() values := []types.Value{} diff --git a/lang/core/golang/template.go b/lang/core/golang/template.go index 05c37ce6..c583c814 100644 --- a/lang/core/golang/template.go +++ b/lang/core/golang/template.go @@ -202,8 +202,10 @@ func (obj *TemplateFunc) Info() *interfaces.Info { sig = obj.sig() // helper } return &interfaces.Info{ - Pure: true, + Pure: false, // contents of a template might not be pure Memo: false, + Fast: false, + Spec: false, Sig: sig, Err: obj.Validate(), } @@ -407,6 +409,9 @@ func (obj *TemplateFunc) Copy() interfaces.Func { // Call this function with the input args and return the value if it is possible // to do so at this time. func (obj *TemplateFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 1 { + return nil, fmt.Errorf("not enough args") + } tmpl := args[0].Str() var vars types.Value // nil @@ -414,6 +419,9 @@ func (obj *TemplateFunc) Call(ctx context.Context, args []types.Value) (types.Va vars = args[1] } + if obj.init == nil { + return nil, funcs.ErrCantSpeculate + } result, err := obj.run(ctx, tmpl, vars) if err != nil { return nil, err // no errwrap needed b/c helper func diff --git a/lang/core/history.go b/lang/core/history.go index 28e51024..ff04db75 100644 --- a/lang/core/history.go +++ b/lang/core/history.go @@ -158,6 +158,8 @@ func (obj *HistoryFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: false, // definitely false Memo: false, + Fast: false, + Spec: false, Sig: obj.sig(), // helper Err: obj.Validate(), } diff --git a/lang/core/iter/filter.go b/lang/core/iter/filter.go index fc771dad..17c72365 100644 --- a/lang/core/iter/filter.go +++ b/lang/core/iter/filter.go @@ -142,8 +142,10 @@ func (obj *FilterFunc) Validate() error { // will return correct data. func (obj *FilterFunc) Info() *interfaces.Info { return &interfaces.Info{ - Pure: false, // TODO: what if the input function isn't pure? + Pure: false, // XXX: what if the input function isn't pure? Memo: false, + Fast: false, + Spec: false, Sig: obj.sig(), // helper Err: obj.Validate(), } @@ -412,9 +414,9 @@ func (obj *FilterFunc) replaceSubGraph(subgraphInput interfaces.Func) error { ) obj.init.Txn.AddVertex(inputElemFunc) - outputElemFunc, err := obj.lastFuncValue.Call(obj.init.Txn, []interfaces.Func{inputElemFunc}) + outputElemFunc, err := obj.lastFuncValue.CallWithFuncs(obj.init.Txn, []interfaces.Func{inputElemFunc}) if err != nil { - return errwrap.Wrapf(err, "could not call obj.lastFuncValue.Call()") + return errwrap.Wrapf(err, "could not call obj.lastFuncValue.CallWithFuncs()") } obj.init.Txn.AddEdge(subgraphInput, inputElemFunc, &interfaces.FuncEdge{ diff --git a/lang/core/iter/map.go b/lang/core/iter/map.go index b03f39b0..62d015d7 100644 --- a/lang/core/iter/map.go +++ b/lang/core/iter/map.go @@ -204,8 +204,10 @@ func (obj *MapFunc) Validate() error { // will return correct data. func (obj *MapFunc) Info() *interfaces.Info { return &interfaces.Info{ - Pure: false, // TODO: what if the input function isn't pure? + Pure: false, // XXX: what if the input function isn't pure? Memo: false, + Fast: false, + Spec: false, Sig: obj.sig(), // helper Err: obj.Validate(), } @@ -439,9 +441,9 @@ 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 := obj.lastFuncValue.CallWithFuncs(obj.init.Txn, []interfaces.Func{inputElemFunc}) if err != nil { - return errwrap.Wrapf(err, "could not call obj.lastFuncValue.Call()") + return errwrap.Wrapf(err, "could not call obj.lastFuncValue.CallWithFuncs()") } obj.init.Txn.AddEdge(subgraphInput, inputElemFunc, &interfaces.FuncEdge{ diff --git a/lang/core/iter/range.go b/lang/core/iter/range.go index 1a016cfc..748f5989 100644 --- a/lang/core/iter/range.go +++ b/lang/core/iter/range.go @@ -161,7 +161,9 @@ func (obj *RangeFunc) Validate() error { func (obj *RangeFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: true, - Memo: false, + Memo: true, + Fast: true, + Spec: true, Sig: obj.Type, Err: obj.Validate(), } diff --git a/lang/core/len.go b/lang/core/len.go index 36a88296..22437f36 100644 --- a/lang/core/len.go +++ b/lang/core/len.go @@ -39,6 +39,12 @@ import ( func init() { simple.Register("len", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(?1) int"), // TODO: should we add support for struct or func lengths? C: simple.TypeMatch([]string{ diff --git a/lang/core/list/list_concat.go b/lang/core/list/list_concat.go index 3dc20256..bf01a5ae 100644 --- a/lang/core/list/list_concat.go +++ b/lang/core/list/list_concat.go @@ -158,6 +158,13 @@ func (obj *ListConcatFunc) Build(typ *types.Type) (*types.Type, error) { obj.Func = &wrapped.Func{ Name: obj.String(), + FuncInfo: &wrapped.Info{ + // TODO: dedup with below Info data + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, Type: typ, // .Copy(), } @@ -188,6 +195,7 @@ func (obj *ListConcatFunc) Copy() interfaces.Func { func (obj *ListConcatFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { values := []types.Value{} + // TODO: Could speculation pass in non-lists here and cause a panic? for _, x := range args { values = append(values, x.List()...) } @@ -217,7 +225,9 @@ func (obj *ListConcatFunc) Validate() error { func (obj *ListConcatFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: true, - Memo: false, + Memo: true, + Fast: true, + Spec: true, Sig: obj.sig(), // helper Err: obj.Validate(), } diff --git a/lang/core/list/list_lookup.go b/lang/core/list/list_lookup.go index bfef16d2..71b92bae 100644 --- a/lang/core/list/list_lookup.go +++ b/lang/core/list/list_lookup.go @@ -202,6 +202,13 @@ func (obj *ListLookupFunc) Build(typ *types.Type) (*types.Type, error) { obj.Func = &wrapped.Func{ Name: obj.String(), + FuncInfo: &wrapped.Info{ + // TODO: dedup with below Info data + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, Type: typ, // .Copy(), } @@ -230,6 +237,10 @@ func (obj *ListLookupFunc) Copy() interfaces.Func { // Call is the actual implementation here. This is used in lieu of the Stream // function which we'd have these contents within. func (obj *ListLookupFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 2 { + return nil, fmt.Errorf("not enough args") + } + // TODO: Could speculation pass in a non-list here and cause a panic? l := (args[0]).(*types.ListValue) index := args[1].Int() //zero := l.Type().Val.New() // the zero value @@ -273,7 +284,9 @@ func (obj *ListLookupFunc) Validate() error { func (obj *ListLookupFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: true, - Memo: false, + Memo: true, + Fast: true, + Spec: true, Sig: obj.sig(), // helper Err: obj.Validate(), } diff --git a/lang/core/list_lookup.go b/lang/core/list_lookup.go index bcd70948..f909ff48 100644 --- a/lang/core/list_lookup.go +++ b/lang/core/list_lookup.go @@ -45,6 +45,12 @@ const ( func init() { simple.Register(ListLookupFuncName, &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(list []?1, index int) ?1"), F: ListLookup, }) diff --git a/lang/core/list_lookup_default.go b/lang/core/list_lookup_default.go index 20335f9e..8d695677 100644 --- a/lang/core/list_lookup_default.go +++ b/lang/core/list_lookup_default.go @@ -45,6 +45,12 @@ const ( func init() { simple.Register(ListLookupDefaultFuncName, &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(list []?1, index int, default ?1) ?1"), F: ListLookupDefault, }) diff --git a/lang/core/local/pool.go b/lang/core/local/pool.go index 21e4d17c..79bb4365 100644 --- a/lang/core/local/pool.go +++ b/lang/core/local/pool.go @@ -54,6 +54,8 @@ func init() { funcs.ModuleRegister(ModuleName, PoolFuncName, func() interfaces.Func { return &PoolFunc{} }) // must register the func and name } +var _ interfaces.DataFunc = &PoolFunc{} + // PoolFunc is a function that returns a unique integer from a pool of numbers. // Within a given namespace, it returns the same integer for a given name. It is // a simple mechanism to allocate numbers to different inputs when we don't have @@ -93,8 +95,10 @@ func (obj *PoolFunc) Validate() error { // Info returns some static info about itself. func (obj *PoolFunc) Info() *interfaces.Info { return &interfaces.Info{ - Pure: true, + Pure: false, // depends on local API Memo: false, + Fast: false, + Spec: false, Sig: types.NewType(fmt.Sprintf("func(%s str, %s str) int", absPathArgNameNamespace, absPathArgNameUID)), // TODO: add an optional config arg //Sig: types.NewType(fmt.Sprintf("func(%s str, %s str, %s struct{}) int", absPathArgNameNamespace, absPathArgNameUID, absPathArgNameConfig)), @@ -153,15 +157,30 @@ func (obj *PoolFunc) Stream(ctx context.Context) error { } } +// Copy is implemented so that the obj.built value is not lost if we copy this +// function. +func (obj *PoolFunc) Copy() interfaces.Func { + return &PoolFunc{ + init: obj.init, // likely gets overwritten anyways + data: obj.data, // needed because we don't call SetData twice + } +} + // Call this function with the input args and return the value if it is possible // to do so at this time. func (obj *PoolFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 2 { + return nil, fmt.Errorf("not enough args") + } // Validation of these inputs happens in the Local API which does it. namespace := args[0].Str() uid := args[1].Str() // TODO: pass in config //config := args[2].???() + if obj.init == nil { + return nil, funcs.ErrCantSpeculate + } result, err := obj.init.Local.Pool(ctx, namespace, uid, nil) if err != nil { return nil, err diff --git a/lang/core/local/vardir.go b/lang/core/local/vardir.go index 07c02da8..675bb2ae 100644 --- a/lang/core/local/vardir.go +++ b/lang/core/local/vardir.go @@ -61,6 +61,8 @@ func init() { funcs.ModuleRegister(ModuleName, VarDirFuncName, func() interfaces.Func { return &VarDirFunc{} }) // must register the func and name } +var _ interfaces.DataFunc = &VarDirFunc{} + // VarDirFunc is a function that returns the absolute, full path in the deploy // from an input path that is relative to the calling file. If you pass it an // empty string, you'll just get the absolute deploy directory path that you're @@ -102,8 +104,10 @@ func (obj *VarDirFunc) Validate() error { // Info returns some static info about itself. func (obj *VarDirFunc) Info() *interfaces.Info { return &interfaces.Info{ - Pure: true, + Pure: false, // TODO: depends on runtime dir path Memo: false, + Fast: false, + Spec: false, Sig: types.NewType(fmt.Sprintf("func(%s str) str", absPathArgNamePath)), } } @@ -160,9 +164,21 @@ func (obj *VarDirFunc) Stream(ctx context.Context) error { } } +// Copy is implemented so that the obj.built value is not lost if we copy this +// function. +func (obj *VarDirFunc) Copy() interfaces.Func { + return &VarDirFunc{ + init: obj.init, // likely gets overwritten anyways + data: obj.data, // needed because we don't call SetData twice + } +} + // Call this function with the input args and return the value if it is possible // to do so at this time. func (obj *VarDirFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 1 { + return nil, fmt.Errorf("not enough args") + } reldir := args[0].Str() if strings.HasPrefix(reldir, "/") { return nil, fmt.Errorf("path must be relative") @@ -174,6 +190,9 @@ func (obj *VarDirFunc) Call(ctx context.Context, args []types.Value) (types.Valu p := fmt.Sprintf("%s/", path.Join(VarDirFunctionsPrefix, reldir)) + if obj.init == nil { + return nil, funcs.ErrCantSpeculate + } result, err := obj.init.Local.VarDir(ctx, p) if err != nil { return nil, err diff --git a/lang/core/lookup.go b/lang/core/lookup.go index 1fd24848..3f63e43a 100644 --- a/lang/core/lookup.go +++ b/lang/core/lookup.go @@ -148,6 +148,9 @@ func (obj *LookupFunc) FuncInfer(partialType *types.Type, partialValues []types. // runs. func (obj *LookupFunc) Build(typ *types.Type) (*types.Type, error) { // typ is the KindFunc signature we're trying to build... + if typ == nil { + return nil, fmt.Errorf("nil type") // happens b/c of Copy() + } if typ.Kind != types.KindFunc { return nil, fmt.Errorf("input type must be of kind func") } @@ -226,7 +229,9 @@ func (obj *LookupFunc) Info() *interfaces.Info { if obj.fn == nil { return &interfaces.Info{ Pure: true, - Memo: false, + Memo: true, + Fast: true, + Spec: true, Sig: types.NewType("func(?1, ?2) ?3"), // func kind Err: obj.Validate(), } @@ -253,6 +258,9 @@ func (obj *LookupFunc) Stream(ctx context.Context) error { // Call returns the result of this function. func (obj *LookupFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if obj.fn == nil { + return nil, funcs.ErrCantSpeculate + } cf, ok := obj.fn.(interfaces.CallableFunc) if !ok { // programming error diff --git a/lang/core/lookup_default.go b/lang/core/lookup_default.go index 113c34d3..7a78eef1 100644 --- a/lang/core/lookup_default.go +++ b/lang/core/lookup_default.go @@ -93,6 +93,9 @@ func (obj *LookupDefaultFunc) ArgGen(index int) (string, error) { // runs. func (obj *LookupDefaultFunc) Build(typ *types.Type) (*types.Type, error) { // typ is the KindFunc signature we're trying to build... + if typ == nil { + return nil, fmt.Errorf("nil type") // happens b/c of Copy() + } if typ.Kind != types.KindFunc { return nil, fmt.Errorf("input type must be of kind func") } @@ -172,7 +175,9 @@ func (obj *LookupDefaultFunc) Info() *interfaces.Info { if obj.fn == nil { return &interfaces.Info{ Pure: true, - Memo: false, + Memo: true, + Fast: true, + Spec: true, Sig: types.NewType("func(?1, ?2, ?3) ?3"), // func kind Err: obj.Validate(), } @@ -199,6 +204,9 @@ func (obj *LookupDefaultFunc) Stream(ctx context.Context) error { // Call returns the result of this function. func (obj *LookupDefaultFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if obj.fn == nil { + return nil, funcs.ErrCantSpeculate + } cf, ok := obj.fn.(interfaces.CallableFunc) if !ok { // programming error diff --git a/lang/core/map/map_keys.go b/lang/core/map/map_keys.go index f9f04870..ec325215 100644 --- a/lang/core/map/map_keys.go +++ b/lang/core/map/map_keys.go @@ -39,6 +39,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "keys", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, // TODO: Maybe saying ?0 could mean we don't care about that type? T: types.NewType("func(x map{?1: ?2}) []?1"), F: MapKeys, diff --git a/lang/core/map/map_lookup.go b/lang/core/map/map_lookup.go index fea955e6..bb98947a 100644 --- a/lang/core/map/map_lookup.go +++ b/lang/core/map/map_lookup.go @@ -204,6 +204,13 @@ func (obj *MapLookupFunc) Build(typ *types.Type) (*types.Type, error) { obj.Func = &wrapped.Func{ Name: obj.String(), + FuncInfo: &wrapped.Info{ + // TODO: dedup with below Info data + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, Type: typ, // .Copy(), } @@ -232,6 +239,9 @@ func (obj *MapLookupFunc) Copy() interfaces.Func { // Call is the actual implementation here. This is used in lieu of the Stream // function which we'd have these contents within. func (obj *MapLookupFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 2 { + return nil, fmt.Errorf("not enough args") + } m := (args[0]).(*types.MapValue) key := args[1] //zero := m.Type().Val.New() // the zero value @@ -266,7 +276,9 @@ func (obj *MapLookupFunc) Validate() error { func (obj *MapLookupFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: true, - Memo: false, + Memo: true, + Fast: true, + Spec: true, Sig: obj.sig(), // helper Err: obj.Validate(), } diff --git a/lang/core/map/map_values.go b/lang/core/map/map_values.go index 7c1df3e0..1ae19d7b 100644 --- a/lang/core/map/map_values.go +++ b/lang/core/map/map_values.go @@ -39,6 +39,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "values", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, // TODO: Maybe saying ?0 could mean we don't care about that type? T: types.NewType("func(x map{?1: ?2}) []?2"), F: MapValues, diff --git a/lang/core/map_lookup.go b/lang/core/map_lookup.go index 99933ad1..9077bcbb 100644 --- a/lang/core/map_lookup.go +++ b/lang/core/map_lookup.go @@ -43,6 +43,12 @@ const ( func init() { simple.Register(MapLookupFuncName, &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(map map{?1: ?2}, key ?1) ?2"), F: MapLookup, }) diff --git a/lang/core/map_lookup_default.go b/lang/core/map_lookup_default.go index f6032f41..f19cf7bd 100644 --- a/lang/core/map_lookup_default.go +++ b/lang/core/map_lookup_default.go @@ -43,6 +43,12 @@ const ( func init() { simple.Register(MapLookupDefaultFuncName, &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(map map{?1: ?2}, key ?1, default ?2) ?2"), F: MapLookupDefault, }) diff --git a/lang/core/math/fortytwo.go b/lang/core/math/fortytwo.go index ddd229bf..8d6b1d2d 100644 --- a/lang/core/math/fortytwo.go +++ b/lang/core/math/fortytwo.go @@ -42,6 +42,12 @@ func init() { typInt := types.NewType("func() int") typFloat := types.NewType("func() float") multi.ModuleRegister(ModuleName, "fortytwo", &multi.Scaffold{ + I: &multi.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func() ?1"), M: multi.TypeMatch(map[string]interfaces.FuncSig{ "func() int": fortyTwo(typInt), diff --git a/lang/core/math/minus1.go b/lang/core/math/minus1.go index 0eaaf414..0f364cb3 100644 --- a/lang/core/math/minus1.go +++ b/lang/core/math/minus1.go @@ -38,6 +38,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "minus1", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(x int) int"), F: Minus1, }) diff --git a/lang/core/math/mod.go b/lang/core/math/mod.go index bea43e9e..cca544c7 100644 --- a/lang/core/math/mod.go +++ b/lang/core/math/mod.go @@ -40,6 +40,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "mod", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(?1, ?1) ?1"), // all int or float C: simple.TypeMatch([]string{ "func(int, int) int", diff --git a/lang/core/math/pow.go b/lang/core/math/pow.go index b0e37f7a..6bd0b745 100644 --- a/lang/core/math/pow.go +++ b/lang/core/math/pow.go @@ -40,6 +40,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "pow", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(x float, y float) float"), F: Pow, }) diff --git a/lang/core/math/sqrt.go b/lang/core/math/sqrt.go index f6a67338..cf598522 100644 --- a/lang/core/math/sqrt.go +++ b/lang/core/math/sqrt.go @@ -40,6 +40,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "sqrt", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(x float) float"), F: Sqrt, }) diff --git a/lang/core/net/cidr_to_ip.go b/lang/core/net/cidr_to_ip.go index cde44be4..e0ec3086 100644 --- a/lang/core/net/cidr_to_ip.go +++ b/lang/core/net/cidr_to_ip.go @@ -44,22 +44,52 @@ import ( func init() { simple.ModuleRegister(ModuleName, "cidr_to_ip", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a str) str"), F: CidrToIP, }) simple.ModuleRegister(ModuleName, "cidr_to_prefix", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a str) str"), F: CidrToPrefix, }) simple.ModuleRegister(ModuleName, "cidr_to_mask", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a str) str"), F: CidrToMask, }) simple.ModuleRegister(ModuleName, "cidr_to_first", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a str) str"), F: CidrToFirst, }) simple.ModuleRegister(ModuleName, "cidr_to_last", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a str) str"), F: CidrToLast, }) diff --git a/lang/core/net/macfmt.go b/lang/core/net/macfmt.go index abb42559..145597a7 100644 --- a/lang/core/net/macfmt.go +++ b/lang/core/net/macfmt.go @@ -41,14 +41,32 @@ import ( func init() { simple.ModuleRegister(ModuleName, "is_mac", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a str) bool"), F: IsMac, }) simple.ModuleRegister(ModuleName, "macfmt", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a str) str"), F: MacFmt, }) simple.ModuleRegister(ModuleName, "oldmacfmt", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a str) str"), F: OldMacFmt, }) diff --git a/lang/core/net/macs.go b/lang/core/net/macs.go index 58bbfe20..d9c4e0ea 100644 --- a/lang/core/net/macs.go +++ b/lang/core/net/macs.go @@ -39,6 +39,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "macs", &simple.Scaffold{ + I: &simple.Info{ + Pure: false, + Memo: false, + Fast: false, + Spec: false, // might be different at real runtime + }, T: types.NewType("func() []str"), F: Macs, }) diff --git a/lang/core/net/url_parser.go b/lang/core/net/url_parser.go index 41b1917a..1f56d6a0 100644 --- a/lang/core/net/url_parser.go +++ b/lang/core/net/url_parser.go @@ -56,6 +56,12 @@ var urlParserReturnType = fmt.Sprintf( func init() { simple.ModuleRegister(ModuleName, "url_parser", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType(fmt.Sprintf("func(str) %s", urlParserReturnType)), F: URLParser, }) diff --git a/lang/core/os/args.go b/lang/core/os/args.go index ca0a0c3a..c2d4d0fa 100644 --- a/lang/core/os/args.go +++ b/lang/core/os/args.go @@ -39,6 +39,13 @@ import ( func init() { simple.ModuleRegister(ModuleName, "args", &simple.Scaffold{ + I: &simple.Info{ + // XXX: make sure nobody changes it at runtime + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func() []str"), F: Args, }) diff --git a/lang/core/os/distro.go b/lang/core/os/distro.go index 364c5999..3501079e 100644 --- a/lang/core/os/distro.go +++ b/lang/core/os/distro.go @@ -44,6 +44,12 @@ var typeParseDistroUID = types.NewType(fmt.Sprintf("func(str) %s", structDistroU func init() { simple.ModuleRegister(ModuleName, "parse_distro_uid", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: typeParseDistroUID, F: ParseDistroUID, }) diff --git a/lang/core/os/expand_home.go b/lang/core/os/expand_home.go index 20eeb088..6500f712 100644 --- a/lang/core/os/expand_home.go +++ b/lang/core/os/expand_home.go @@ -39,6 +39,13 @@ import ( func init() { simple.ModuleRegister(ModuleName, "expand_home", &simple.Scaffold{ + // These might change depending on user database... + I: &simple.Info{ + Pure: false, + Memo: false, + Fast: true, + Spec: false, + }, T: types.NewType("func(str) str"), F: ExpandHome, }) diff --git a/lang/core/os/family.go b/lang/core/os/family.go index 4cddc94d..54c9100a 100644 --- a/lang/core/os/family.go +++ b/lang/core/os/family.go @@ -40,6 +40,13 @@ import ( func init() { simple.ModuleRegister(ModuleName, "family", &simple.Scaffold{ + I: &simple.Info{ + // In theory these are dependent on runtime. + Pure: false, + Memo: false, + Fast: true, + Spec: false, + }, T: types.NewType("func() str"), F: Family, }) @@ -62,14 +69,35 @@ func init() { // TODO: Create a family method that will return a giant struct. simple.ModuleRegister(ModuleName, "is_family_redhat", &simple.Scaffold{ + I: &simple.Info{ + // In theory these are dependent on runtime. + Pure: false, + Memo: false, + Fast: true, + Spec: false, + }, T: types.NewType("func() bool"), F: IsFamilyRedHat, }) simple.ModuleRegister(ModuleName, "is_family_debian", &simple.Scaffold{ + I: &simple.Info{ + // In theory these are dependent on runtime. + Pure: false, + Memo: false, + Fast: true, + Spec: false, + }, T: types.NewType("func() bool"), F: IsFamilyDebian, }) simple.ModuleRegister(ModuleName, "is_family_archlinux", &simple.Scaffold{ + I: &simple.Info{ + // In theory these are dependent on runtime. + Pure: false, + Memo: false, + Fast: true, + Spec: false, + }, T: types.NewType("func() bool"), F: IsFamilyArchLinux, }) diff --git a/lang/core/os/is_virtual.go b/lang/core/os/is_virtual.go index 3ca7d852..3abf8271 100644 --- a/lang/core/os/is_virtual.go +++ b/lang/core/os/is_virtual.go @@ -77,6 +77,13 @@ var virtualizationVendorSlice = []string{ func init() { simple.ModuleRegister(ModuleName, "is_virtual", &simple.Scaffold{ + I: &simple.Info{ + // In theory these are dependent on runtime. + Pure: false, + Memo: false, + Fast: false, // network requests to API's? + Spec: false, + }, T: types.NewType("func() bool"), F: IsVirtual, }) diff --git a/lang/core/os/readfile.go b/lang/core/os/readfile.go index 2351d903..8bb06b47 100644 --- a/lang/core/os/readfile.go +++ b/lang/core/os/readfile.go @@ -97,6 +97,8 @@ func (obj *ReadFileFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: false, // maybe false because the file contents can change Memo: false, + Fast: false, + Spec: false, Sig: types.NewType(fmt.Sprintf("func(%s str) str", readFileArgNameFilename)), } } @@ -249,6 +251,9 @@ func (obj *ReadFileFunc) Stream(ctx context.Context) error { // Call this function with the input args and return the value if it is possible // to do so at this time. func (obj *ReadFileFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 1 { + return nil, fmt.Errorf("not enough args") + } filename := args[0].Str() // read file... diff --git a/lang/core/os/system.go b/lang/core/os/system.go index fca6a156..8d7a67f8 100644 --- a/lang/core/os/system.go +++ b/lang/core/os/system.go @@ -91,6 +91,8 @@ func (obj *SystemFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: false, // definitely false Memo: false, + Fast: false, + Spec: false, Sig: types.NewType(fmt.Sprintf("func(%s str) str", systemArgNameCmd)), Err: obj.Validate(), } diff --git a/lang/core/panic.go b/lang/core/panic.go index 6efdf6b7..bdf5384b 100644 --- a/lang/core/panic.go +++ b/lang/core/panic.go @@ -39,6 +39,12 @@ import ( func init() { simple.Register("panic", &simple.Scaffold{ + I: &simple.Info{ + Pure: false, // n/a + Memo: false, + Fast: true, + Spec: false, // important! + }, T: types.NewType("func(x ?1) bool"), // ?1 is bool or str C: simple.TypeMatch([]string{ "func(x bool) bool", diff --git a/lang/core/random1.go b/lang/core/random1.go index 7967665c..6d156cc7 100644 --- a/lang/core/random1.go +++ b/lang/core/random1.go @@ -96,6 +96,9 @@ func (obj *Random1Func) Validate() error { func (obj *Random1Func) Info() *interfaces.Info { return &interfaces.Info{ Pure: false, + Memo: false, + Fast: false, + Spec: false, Sig: types.NewType(fmt.Sprintf("func(%s int) str", random1ArgNameLength)), Err: obj.Validate(), } diff --git a/lang/core/regexp/match.go b/lang/core/regexp/match.go index 6a597bc7..90605a7a 100644 --- a/lang/core/regexp/match.go +++ b/lang/core/regexp/match.go @@ -40,6 +40,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "match", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: false, // TODO: should we consider this fast? + Spec: true, + }, T: types.NewType("func(pattern str, s str) bool"), F: Match, }) diff --git a/lang/core/strings/join_nonempty.go b/lang/core/strings/join_nonempty.go index d450b6b1..4ce94a62 100644 --- a/lang/core/strings/join_nonempty.go +++ b/lang/core/strings/join_nonempty.go @@ -39,6 +39,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "join_nonempty", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(s []str, sep str) str"), F: JoinNonempty, }) diff --git a/lang/core/strings/pad.go b/lang/core/strings/pad.go index 0c7dac76..e647308e 100644 --- a/lang/core/strings/pad.go +++ b/lang/core/strings/pad.go @@ -39,10 +39,22 @@ import ( func init() { simple.ModuleRegister(ModuleName, "left_pad", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(s str, pad str, len int) str"), F: LeftPad, }) simple.ModuleRegister(ModuleName, "right_pad", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(s str, pad str, len int) str"), F: RightPad, }) diff --git a/lang/core/strings/split.go b/lang/core/strings/split.go index 5070c17b..a8d72686 100644 --- a/lang/core/strings/split.go +++ b/lang/core/strings/split.go @@ -39,6 +39,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "split", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a str, b str) []str"), F: Split, }) diff --git a/lang/core/strings/substring.go b/lang/core/strings/substring.go index e92290f1..45004649 100644 --- a/lang/core/strings/substring.go +++ b/lang/core/strings/substring.go @@ -39,6 +39,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "substr", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(s str, low int, high int) str"), F: SubStr, }) diff --git a/lang/core/strings/to_lower.go b/lang/core/strings/to_lower.go index d8f88119..0b65b4dd 100644 --- a/lang/core/strings/to_lower.go +++ b/lang/core/strings/to_lower.go @@ -39,6 +39,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "to_lower", &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(a str) str"), F: ToLower, }) diff --git a/lang/core/struct_lookup.go b/lang/core/struct_lookup.go index 40875362..96ece315 100644 --- a/lang/core/struct_lookup.go +++ b/lang/core/struct_lookup.go @@ -269,7 +269,9 @@ func (obj *StructLookupFunc) Info() *interfaces.Info { } return &interfaces.Info{ Pure: true, - Memo: false, + Memo: true, + Fast: true, + Spec: true, Sig: sig, Err: obj.Validate(), } @@ -339,6 +341,9 @@ func (obj *StructLookupFunc) Stream(ctx context.Context) error { // Call returns the result of this function. func (obj *StructLookupFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 2 { + return nil, fmt.Errorf("not enough args") + } st := args[0].(*types.StructValue) field := args[1].Str() diff --git a/lang/core/struct_lookup_optional.go b/lang/core/struct_lookup_optional.go index 7f46d2fe..9f02c725 100644 --- a/lang/core/struct_lookup_optional.go +++ b/lang/core/struct_lookup_optional.go @@ -266,7 +266,9 @@ func (obj *StructLookupOptionalFunc) Info() *interfaces.Info { } return &interfaces.Info{ Pure: true, - Memo: false, + Memo: true, + Fast: true, + Spec: true, Sig: sig, Err: obj.Validate(), } @@ -345,6 +347,9 @@ func (obj *StructLookupOptionalFunc) Stream(ctx context.Context) error { // Call returns the result of this function. func (obj *StructLookupOptionalFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 3 { + return nil, fmt.Errorf("not enough args") + } st := args[0].(*types.StructValue) field := args[1].Str() optional := args[2] diff --git a/lang/core/sys/cpucount_fact.go b/lang/core/sys/cpucount_fact.go index 8fc9a2a8..a9ce32ff 100644 --- a/lang/core/sys/cpucount_fact.go +++ b/lang/core/sys/cpucount_fact.go @@ -76,6 +76,8 @@ func (obj *CPUCountFact) String() string { // Info returns static typing info about what the fact returns. func (obj *CPUCountFact) Info() *facts.Info { return &facts.Info{ + Pure: false, + Memo: false, Output: types.NewType("int"), } } diff --git a/lang/core/sys/env.go b/lang/core/sys/env.go index e1f2f86b..ef5a4231 100644 --- a/lang/core/sys/env.go +++ b/lang/core/sys/env.go @@ -40,18 +40,42 @@ import ( func init() { simple.ModuleRegister(ModuleName, "getenv", &simple.Scaffold{ + I: &simple.Info{ + Pure: false, + Memo: false, + Fast: true, + Spec: false, + }, T: types.NewType("func(str) str"), F: GetEnv, }) simple.ModuleRegister(ModuleName, "defaultenv", &simple.Scaffold{ + I: &simple.Info{ + Pure: false, + Memo: false, + Fast: true, + Spec: false, + }, T: types.NewType("func(str, str) str"), F: DefaultEnv, }) simple.ModuleRegister(ModuleName, "hasenv", &simple.Scaffold{ + I: &simple.Info{ + Pure: false, + Memo: false, + Fast: true, + Spec: false, + }, T: types.NewType("func(str) bool"), F: HasEnv, }) simple.ModuleRegister(ModuleName, "env", &simple.Scaffold{ + I: &simple.Info{ + Pure: false, + Memo: false, + Fast: true, + Spec: false, + }, T: types.NewType("func() map{str: str}"), F: Env, }) diff --git a/lang/core/sys/load_fact.go b/lang/core/sys/load_fact.go index 9e4c03d1..0d2bf78d 100644 --- a/lang/core/sys/load_fact.go +++ b/lang/core/sys/load_fact.go @@ -70,6 +70,8 @@ func (obj *LoadFact) String() string { // Info returns some static info about itself. func (obj *LoadFact) Info() *facts.Info { return &facts.Info{ + Pure: false, + Memo: false, Output: types.NewType(loadSignature), } } diff --git a/lang/core/sys/uptime_fact.go b/lang/core/sys/uptime_fact.go index 5a8ea21e..57064e30 100644 --- a/lang/core/sys/uptime_fact.go +++ b/lang/core/sys/uptime_fact.go @@ -62,6 +62,8 @@ func (obj *UptimeFact) String() string { // Info returns some static info about itself. func (obj *UptimeFact) Info() *facts.Info { return &facts.Info{ + Pure: false, + Memo: false, Output: types.TypeInt, } } diff --git a/lang/core/test/fastcount_fact.go b/lang/core/test/fastcount_fact.go index 3de528ca..3011ac3c 100644 --- a/lang/core/test/fastcount_fact.go +++ b/lang/core/test/fastcount_fact.go @@ -71,6 +71,8 @@ func (obj *FastCountFact) String() string { // Info returns some static info about itself. func (obj *FastCountFact) Info() *facts.Info { return &facts.Info{ + Pure: false, + Memo: false, Output: types.NewType("int"), } } @@ -108,6 +110,9 @@ func (obj *FastCountFact) Stream(ctx context.Context) error { // Call this fact and return the value if it is possible to do so at this time. func (obj *FastCountFact) Call(ctx context.Context) (types.Value, error) { + if obj.mutex == nil { + return nil, facts.ErrCantSpeculate + } obj.mutex.Lock() // TODO: could be a read lock count := obj.count obj.mutex.Unlock() diff --git a/lang/core/test/oneinstance_fact.go b/lang/core/test/oneinstance_fact.go index 827c9ee4..a41c6c8e 100644 --- a/lang/core/test/oneinstance_fact.go +++ b/lang/core/test/oneinstance_fact.go @@ -122,6 +122,12 @@ func init() { }) simple.ModuleRegister(ModuleName, OneInstanceBFuncName, &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: false, // don't break tests + }, T: types.NewType("func() str"), F: func(context.Context, []types.Value) (types.Value, error) { oneInstanceBMutex.Lock() @@ -135,6 +141,12 @@ func init() { D: &OneInstanceFact{}, }) simple.ModuleRegister(ModuleName, OneInstanceDFuncName, &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: false, // don't break tests + }, T: types.NewType("func() str"), F: func(context.Context, []types.Value) (types.Value, error) { oneInstanceDMutex.Lock() @@ -148,6 +160,12 @@ func init() { D: &OneInstanceFact{}, }) simple.ModuleRegister(ModuleName, OneInstanceFFuncName, &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: false, // don't break tests + }, T: types.NewType("func() str"), F: func(context.Context, []types.Value) (types.Value, error) { oneInstanceFMutex.Lock() @@ -161,6 +179,12 @@ func init() { D: &OneInstanceFact{}, }) simple.ModuleRegister(ModuleName, OneInstanceHFuncName, &simple.Scaffold{ + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: false, // don't break tests + }, T: types.NewType("func() str"), F: func(context.Context, []types.Value) (types.Value, error) { oneInstanceHMutex.Lock() diff --git a/lang/core/util/hostname_mapper.go b/lang/core/util/hostname_mapper.go index 53cb1e16..58923351 100644 --- a/lang/core/util/hostname_mapper.go +++ b/lang/core/util/hostname_mapper.go @@ -41,6 +41,12 @@ import ( func init() { simple.ModuleRegister(ModuleName, "hostname_mapper", &simple.Scaffold{ + I: &simple.Info{ + Pure: false, + Memo: false, + Fast: false, + Spec: false, + }, T: types.NewType("func(map{str:str}) str"), F: HostnameMapper, }) diff --git a/lang/core/value/get.go b/lang/core/value/get.go index 18f17249..608f4bfb 100644 --- a/lang/core/value/get.go +++ b/lang/core/value/get.go @@ -206,6 +206,8 @@ func (obj *GetFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: false, // definitely false Memo: false, + Fast: false, + Spec: false, Sig: sig, Err: obj.Validate(), } @@ -310,6 +312,9 @@ func (obj *GetFunc) Stream(ctx context.Context) error { // to do so at this time. This was previously getValue which gets the value // we're looking for. func (obj *GetFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 1 { + return nil, fmt.Errorf("not enough args") + } key := args[0].Str() typ, exists := obj.Info().Sig.Out.Map[getFieldNameValue] // type of value field @@ -326,6 +331,9 @@ func (obj *GetFunc) Call(ctx context.Context, args []types.Value) (types.Value, // step that might be needed if the value started out empty... // TODO: We could even add a stored: bool field in the returned struct! isReady := true // assume true + if obj.init == nil { + return nil, funcs.ErrCantSpeculate + } val, err := obj.init.Local.ValueGet(ctx, key) if err != nil { return nil, errwrap.Wrapf(err, "channel read failed on `%s`", key) diff --git a/lang/core/world/collect/res.go b/lang/core/world/collect/res.go index 315a909d..2088b39a 100644 --- a/lang/core/world/collect/res.go +++ b/lang/core/world/collect/res.go @@ -118,6 +118,8 @@ func (obj *ResFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: false, Memo: false, + Fast: false, + Spec: false, Sig: obj.sig(), Err: obj.Validate(), } @@ -225,6 +227,9 @@ func (obj *ResFunc) Stream(ctx context.Context) error { // to do so at this time. This was previously getValue which gets the value // we're looking for. func (obj *ResFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 1 { + return nil, fmt.Errorf("not enough args") + } kind := args[0].Str() if kind == "" { return nil, fmt.Errorf("resource kind is empty") @@ -241,6 +246,9 @@ func (obj *ResFunc) Call(ctx context.Context, args []types.Value) (types.Value, } filters = append(filters, filter) + if obj.init == nil { + return nil, funcs.ErrCantSpeculate + } resOutput, err := obj.init.World.ResCollect(ctx, filters) if err != nil { return nil, err diff --git a/lang/core/world/exchange.go b/lang/core/world/exchange.go index 11cb2336..dfa6ab5b 100644 --- a/lang/core/world/exchange.go +++ b/lang/core/world/exchange.go @@ -91,6 +91,8 @@ func (obj *ExchangeFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: false, // definitely false Memo: false, + Fast: false, + Spec: false, // TODO: do we want to allow this to be statically polymorphic, // and have value be any type we might want? // output is map of: hostname => value diff --git a/lang/core/world/getval.go b/lang/core/world/getval.go index b6b5d940..cf144557 100644 --- a/lang/core/world/getval.go +++ b/lang/core/world/getval.go @@ -97,6 +97,8 @@ func (obj *GetValFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: false, // definitely false Memo: false, + Fast: false, + Spec: false, // output is a struct with two fields: // value is the zero value if not exists. A bool for that in other field. Sig: types.NewType(fmt.Sprintf("func(%s str) struct{%s str; %s bool}", getValArgNameKey, getValFieldNameValue, getValFieldNameExists)), @@ -206,8 +208,14 @@ func (obj *GetValFunc) Stream(ctx context.Context) error { // to do so at this time. This was previously getValue which gets the value // we're looking for. func (obj *GetValFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 1 { + return nil, fmt.Errorf("not enough args") + } key := args[0].Str() exists := true // assume true + if obj.init == nil { + return nil, funcs.ErrCantSpeculate + } val, err := obj.init.World.StrGet(ctx, key) if err != nil && obj.init.World.StrIsNotExist(err) { exists = false // val doesn't exist diff --git a/lang/core/world/kvlookup.go b/lang/core/world/kvlookup.go index 728b0d53..c8c444bc 100644 --- a/lang/core/world/kvlookup.go +++ b/lang/core/world/kvlookup.go @@ -93,6 +93,8 @@ func (obj *KVLookupFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: false, // definitely false Memo: false, + Fast: false, + Spec: false, // output is map of: hostname => value Sig: types.NewType(fmt.Sprintf("func(%s str) map{str: str}", kvLookupArgNameNamespace)), Err: obj.Validate(), @@ -211,7 +213,13 @@ func (obj *KVLookupFunc) Stream(ctx context.Context) error { // to do so at this time. This was previously buildMap, which builds the result // map which we'll need. It uses struct variables. func (obj *KVLookupFunc) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if len(args) < 1 { + return nil, fmt.Errorf("not enough args") + } namespace := args[0].Str() + if obj.init == nil { + return nil, funcs.ErrCantSpeculate + } keyMap, err := obj.init.World.StrMapGet(ctx, namespace) if err != nil { return nil, errwrap.Wrapf(err, "channel read failed on `%s`", namespace) diff --git a/lang/core/world/schedule.go b/lang/core/world/schedule.go index 94961a40..452498d0 100644 --- a/lang/core/world/schedule.go +++ b/lang/core/world/schedule.go @@ -283,6 +283,8 @@ func (obj *ScheduleFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: false, // definitely false Memo: false, + Fast: false, + Spec: false, // output is list of hostnames chosen Sig: sig, // func kind Err: obj.Validate(), diff --git a/lang/funcs/facts/facts.go b/lang/funcs/facts/facts.go index 7a372fee..75056f1d 100644 --- a/lang/funcs/facts/facts.go +++ b/lang/funcs/facts/facts.go @@ -40,6 +40,17 @@ import ( "github.com/purpleidea/mgmt/lang/types" ) +const ( + // ErrCantSpeculate is an error that explains that we can't speculate + // when trying to Call a function. This often gets called by the Value() + // method of the Expr. This can be useful if we want to distinguish + // between "something is broken" and "I just can't produce a value at + // this time", which can be identified and skipped over. If it's the + // former, then it's okay to error early and shut everything down since + // we know this function is never going to work the way it's called. + ErrCantSpeculate = funcs.ErrCantSpeculate +) + // RegisteredFacts is a global map of all possible facts which can be used. You // should never touch this map directly. Use methods like Register instead. var RegisteredFacts = make(map[string]func() Fact) // must initialize @@ -79,6 +90,10 @@ func ModuleRegister(module, name string, fn func() Fact) { // used for static analysis and type checking. If you break this contract, you // might cause a panic. type Info struct { + Pure bool // is the function pure? (can it be memoized?) + Memo bool // should the function be memoized? (false if too much output) + Fast bool // is the function fast? (avoid speculative execution) + Spec bool // can we speculatively execute it? (true for most) Output *types.Type // output value type (must not change over time!) Err error // did this fact validate? } diff --git a/lang/funcs/facts/func.go b/lang/funcs/facts/func.go index 13495a3b..f1c7b53a 100644 --- a/lang/funcs/facts/func.go +++ b/lang/funcs/facts/func.go @@ -64,8 +64,10 @@ func (obj *FactFunc) Validate() error { // Info returns some static info about itself. func (obj *FactFunc) Info() *interfaces.Info { return &interfaces.Info{ - Pure: false, - Memo: false, + Pure: obj.Fact.Info().Pure, + Memo: obj.Fact.Info().Memo, + Fast: obj.Fact.Info().Fast, + Spec: obj.Fact.Info().Spec, Sig: &types.Type{ Kind: types.KindFunc, // if Ord or Map are nil, this will panic things! diff --git a/lang/funcs/funcgen/fixtures/func_base.tpl b/lang/funcs/funcgen/fixtures/func_base.tpl index db992dd3..77327695 100644 --- a/lang/funcs/funcgen/fixtures/func_base.tpl +++ b/lang/funcs/funcgen/fixtures/func_base.tpl @@ -40,26 +40,68 @@ import ( func init() { simple.ModuleRegister("golang/testpkg", "all_kind", &simple.Scaffold{ + // XXX: pull these from a database, remove the impure functions + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(x int, y str) float"), F: TestpkgAllKind, }) simple.ModuleRegister("golang/testpkg", "to_upper", &simple.Scaffold{ + // XXX: pull these from a database, remove the impure functions + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(s str) str"), F: TestpkgToUpper, }) simple.ModuleRegister("golang/testpkg", "max", &simple.Scaffold{ + // XXX: pull these from a database, remove the impure functions + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(x float, y float) float"), F: TestpkgMax, }) simple.ModuleRegister("golang/testpkg", "with_error", &simple.Scaffold{ + // XXX: pull these from a database, remove the impure functions + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(s str) str"), F: TestpkgWithError, }) simple.ModuleRegister("golang/testpkg", "with_int", &simple.Scaffold{ + // XXX: pull these from a database, remove the impure functions + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(s float, i int, x int, j int, k int, b bool, t str) str"), F: TestpkgWithInt, }) simple.ModuleRegister("golang/testpkg", "super_byte", &simple.Scaffold{ + // XXX: pull these from a database, remove the impure functions + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("func(s str, t str) str"), F: TestpkgSuperByte, }) diff --git a/lang/funcs/funcgen/templates/generated_funcs.go.tpl b/lang/funcs/funcgen/templates/generated_funcs.go.tpl index 4cca6bf8..6b44e0cb 100644 --- a/lang/funcs/funcgen/templates/generated_funcs.go.tpl +++ b/lang/funcs/funcgen/templates/generated_funcs.go.tpl @@ -40,6 +40,13 @@ import ( func init() { {{ range $i, $func := .Functions }} simple.ModuleRegister("{{$func.MgmtPackage}}", "{{$func.MclName}}", &simple.Scaffold{ + // XXX: pull these from a database, remove the impure functions + I: &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + }, T: types.NewType("{{$func.Signature}}"), F: {{$func.InternalName}}, }) diff --git a/lang/funcs/funcs.go b/lang/funcs/funcs.go index 183bf504..1a1e5c3d 100644 --- a/lang/funcs/funcs.go +++ b/lang/funcs/funcs.go @@ -38,6 +38,7 @@ import ( docsUtil "github.com/purpleidea/mgmt/docs/util" "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/util" ) const ( @@ -113,6 +114,15 @@ const ( // CollectFuncOutType is the expected return type, the data field is an // encoded resource blob. CollectFuncOutType = "[]" + CollectFuncOutStruct + + // ErrCantSpeculate is an error that explains that we can't speculate + // when trying to Call a function. This often gets called by the Value() + // method of the Expr. This can be useful if we want to distinguish + // between "something is broken" and "I just can't produce a value at + // this time", which can be identified and skipped over. If it's the + // former, then it's okay to error early and shut everything down since + // we know this function is never going to work the way it's called. + ErrCantSpeculate = util.Error("can't speculate") ) // registeredFuncs is a global map of all possible funcs which can be used. You diff --git a/lang/funcs/multi/multi.go b/lang/funcs/multi/multi.go index 08bc3940..93440e9a 100644 --- a/lang/funcs/multi/multi.go +++ b/lang/funcs/multi/multi.go @@ -45,9 +45,20 @@ import ( // RegisteredFuncs maps a function name to the corresponding function scaffold. var RegisteredFuncs = make(map[string]*Scaffold) // must initialize +// Info holds some information about this function. +type Info struct { + Pure bool // is the function pure? (can it be memoized?) + Memo bool // should the function be memoized? (false if too much output) + Fast bool // is the function slow? (avoid speculative execution) + Spec bool // can we speculatively execute it? (true for most) +} + // Scaffold holds the necessary data to build a (possibly polymorphic) function // with this API. type Scaffold struct { + // I is some general info about the function. + I *Info + // T is the type of the function. It can include unification variables. // At a minimum, this must be a `func(?1) ?2` as a naked `?1` is not // allowed. (TODO: Because of ArgGen.) @@ -102,12 +113,23 @@ func Register(name string, scaffold *Scaffold) { panic(fmt.Sprintf("could not locate function filename for %s", name)) } + funcInfo := &wrapped.Info{} + if scaffold.I != nil { + funcInfo = &wrapped.Info{ + Pure: scaffold.I.Pure, + Memo: scaffold.I.Memo, + Fast: scaffold.I.Fast, + Spec: scaffold.I.Spec, + } + } + // register a copy in the main function database funcs.Register(name, func() interfaces.Func { return &Func{ Metadata: metadata, WrappedFunc: &wrapped.Func{ - Name: name, + Name: name, + FuncInfo: funcInfo, // NOTE: It might be more correct to Copy here, // but we do the copy inside of ExprFunc.Copy() // instead, so that the same type can be unified diff --git a/lang/funcs/operators/operators.go b/lang/funcs/operators/operators.go index b0731421..954d34b4 100644 --- a/lang/funcs/operators/operators.go +++ b/lang/funcs/operators/operators.go @@ -56,7 +56,14 @@ const ( ) func init() { + info := &simple.Info{ + Pure: true, + Memo: true, + Fast: true, + Spec: true, + } RegisterOperator("+", &simple.Scaffold{ + I: info, T: types.NewType("func(?1, ?1) ?1"), C: simple.TypeMatch([]string{ "func(str, str) str", // concatenation @@ -91,6 +98,7 @@ func init() { }) RegisterOperator("-", &simple.Scaffold{ + I: info, T: types.NewType("func(?1, ?1) ?1"), C: simple.TypeMatch([]string{ "func(int, int) int", // subtraction @@ -115,6 +123,7 @@ func init() { }) RegisterOperator("*", &simple.Scaffold{ + I: info, T: types.NewType("func(?1, ?1) ?1"), C: simple.TypeMatch([]string{ "func(int, int) int", // multiplication @@ -141,6 +150,7 @@ func init() { // don't add: `func(int, float) float` or: `func(float, int) float` RegisterOperator("/", &simple.Scaffold{ + I: info, T: types.NewType("func(?1, ?1) float"), C: simple.TypeMatch([]string{ "func(int, int) float", // division @@ -173,6 +183,7 @@ func init() { }) RegisterOperator("==", &simple.Scaffold{ + I: info, T: types.NewType("func(?1, ?1) bool"), C: func(typ *types.Type) error { //if typ == nil { // happens within iter @@ -218,6 +229,7 @@ func init() { }) RegisterOperator("!=", &simple.Scaffold{ + I: info, T: types.NewType("func(?1, ?1) bool"), C: func(typ *types.Type) error { //if typ == nil { // happens within iter @@ -263,6 +275,7 @@ func init() { }) RegisterOperator("<", &simple.Scaffold{ + I: info, T: types.NewType("func(?1, ?1) bool"), C: simple.TypeMatch([]string{ "func(int, int) bool", // less-than @@ -288,6 +301,7 @@ func init() { }) RegisterOperator(">", &simple.Scaffold{ + I: info, T: types.NewType("func(?1, ?1) bool"), C: simple.TypeMatch([]string{ "func(int, int) bool", // greater-than @@ -313,6 +327,7 @@ func init() { }) RegisterOperator("<=", &simple.Scaffold{ + I: info, T: types.NewType("func(?1, ?1) bool"), C: simple.TypeMatch([]string{ "func(int, int) bool", // less-than-equal @@ -338,6 +353,7 @@ func init() { }) RegisterOperator(">=", &simple.Scaffold{ + I: info, T: types.NewType("func(?1, ?1) bool"), C: simple.TypeMatch([]string{ "func(int, int) bool", // greater-than-equal @@ -366,6 +382,7 @@ func init() { // TODO: is there a way for the engine to have // short-circuit operators, and does it matter? RegisterOperator("and", &simple.Scaffold{ + I: info, T: types.NewType("func(bool, bool) bool"), F: func(ctx context.Context, input []types.Value) (types.Value, error) { return &types.BoolValue{ @@ -376,6 +393,7 @@ func init() { // logical or RegisterOperator("or", &simple.Scaffold{ + I: info, T: types.NewType("func(bool, bool) bool"), F: func(ctx context.Context, input []types.Value) (types.Value, error) { return &types.BoolValue{ @@ -386,6 +404,7 @@ func init() { // logical not (unary operator) RegisterOperator("not", &simple.Scaffold{ + I: info, T: types.NewType("func(bool) bool"), F: func(ctx context.Context, input []types.Value) (types.Value, error) { return &types.BoolValue{ @@ -396,6 +415,7 @@ func init() { // pi operator (this is an easter egg to demo a zero arg operator) RegisterOperator("π", &simple.Scaffold{ + I: info, T: types.NewType("func() float"), F: func(ctx context.Context, input []types.Value) (types.Value, error) { return &types.FloatValue{ @@ -645,8 +665,11 @@ func (obj *OperatorFunc) Info() *interfaces.Info { // avoid an accidental return of unification variables when we should be // getting them from FuncInfer, and not from here. (During unification!) return &interfaces.Info{ + // XXX: get these from the scaffold Pure: true, - Memo: false, + Memo: true, + Fast: true, + Spec: true, Sig: obj.Type, // func kind, which includes operator arg as input Err: obj.Validate(), } diff --git a/lang/funcs/simple/simple.go b/lang/funcs/simple/simple.go index b6c7effb..eaf94d7b 100644 --- a/lang/funcs/simple/simple.go +++ b/lang/funcs/simple/simple.go @@ -54,9 +54,20 @@ const ( // RegisteredFuncs maps a function name to the corresponding function scaffold. var RegisteredFuncs = make(map[string]*Scaffold) // must initialize +// Info holds some information about this function. +type Info struct { + Pure bool // is the function pure? (can it be memoized?) + Memo bool // should the function be memoized? (false if too much output) + Fast bool // is the function slow? (avoid speculative execution) + Spec bool // can we speculatively execute it? (true for most) +} + // Scaffold holds the necessary data to build a (possibly polymorphic) function // with this API. type Scaffold struct { + // I is some general info about the function. + I *Info + // T is the type of the function. It can include unification variables. // At a minimum, this must be a `func(?1) ?2` as a naked `?1` is not // allowed. (TODO: Because of ArgGen.) @@ -124,12 +135,23 @@ func Register(name string, scaffold *Scaffold) { panic(fmt.Sprintf("could not locate function filename for %s", name)) } + funcInfo := &wrapped.Info{} + if scaffold.I != nil { + funcInfo = &wrapped.Info{ + Pure: scaffold.I.Pure, + Memo: scaffold.I.Memo, + Fast: scaffold.I.Fast, + Spec: scaffold.I.Spec, + } + } + // register a copy in the main function database funcs.Register(name, func() interfaces.Func { return &Func{ Metadata: metadata, WrappedFunc: &wrapped.Func{ - Name: name, + Name: name, + FuncInfo: funcInfo, // NOTE: It might be more correct to Copy here, // but we do the copy inside of ExprFunc.Copy() // instead, so that the same type can be unified diff --git a/lang/funcs/structs/call.go b/lang/funcs/structs/call.go index e471ace6..d0a1eb62 100644 --- a/lang/funcs/structs/call.go +++ b/lang/funcs/structs/call.go @@ -223,7 +223,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 := newFuncValue.CallWithFuncs(obj.init.Txn, obj.ArgVertices) if err != nil { return errwrap.Wrapf(err, "could not call newFuncValue.Call()") } diff --git a/lang/funcs/structs/util.go b/lang/funcs/structs/util.go index 932192b9..a68b25e1 100644 --- a/lang/funcs/structs/util.go +++ b/lang/funcs/structs/util.go @@ -74,6 +74,7 @@ func SimpleFnToFuncValue(name string, fv *types.FuncValue) *full.FuncValue { } return wrappedFunc, nil }, + F: nil, // unused T: fv.T, } } @@ -84,3 +85,42 @@ 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(makeFunc func() interfaces.Func, typ *types.Type) *full.FuncValue { + + v := func(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { + valueTransformingFunc := makeFunc() // do this once here + buildableFunc, ok := valueTransformingFunc.(interfaces.BuildableFunc) + if ok { + // Set the type in case it's not already done. + if _, err := buildableFunc.Build(typ); err != nil { + // programming error? + return nil, err + } + } + for i, arg := range args { + argName := typ.Ord[i] + txn.AddEdge(arg, valueTransformingFunc, &interfaces.FuncEdge{ + Args: []string{argName}, + }) + } + return valueTransformingFunc, nil + } + + var f interfaces.FuncSig + callableFunc, ok := makeFunc().(interfaces.CallableFunc) + if ok { + f = callableFunc.Call + } + + // This has the "V" implementation and the simpler "F" implementation + // which can occasionally be used if the interfaces.Func supports that! + return &full.FuncValue{ + V: v, + F: f, + T: typ, + } +} diff --git a/lang/funcs/wrapped/wrapped.go b/lang/funcs/wrapped/wrapped.go index 2c5f8eda..79a2796c 100644 --- a/lang/funcs/wrapped/wrapped.go +++ b/lang/funcs/wrapped/wrapped.go @@ -39,6 +39,14 @@ import ( var _ interfaces.Func = &Func{} // ensure it meets this expectation +// Info holds some information about this function. +type Info struct { + Pure bool // is the function pure? (can it be memoized?) + Memo bool // should the function be memoized? (false if too much output) + Fast bool // is the function slow? (avoid speculative execution) + Spec bool // can we speculatively execute it? (true for most) +} + // Func is a wrapped scaffolding function struct which fulfills the boiler-plate // for the function API, but that can run a very simple, static, pure, function. // It can be wrapped by other structs that support polymorphism in various ways. @@ -48,6 +56,9 @@ type Func struct { // Name is a unique string name for the function. Name string + // Info is some general info about the function. + FuncInfo *Info + // Type is the type of the function. It can include unification // variables when this struct is wrapped in one that can build this out. Type *types.Type @@ -64,7 +75,16 @@ type Func struct { // String returns a simple name for this function. This is needed so this struct // can satisfy the pgraph.Vertex interface. func (obj *Func) String() string { - return fmt.Sprintf("%s @ %p", obj.Name, obj) // be more unique! + //if obj.Fn != nil { // TODO: would this work and be useful? + // return fmt.Sprintf("%s: %s", obj.Name, obj.Fn) + //} + //if obj.Type != nil { // TODO: would this work and be useful? + // return fmt.Sprintf("%s: %s", obj.Name, obj.Type) + //} + if obj.Name == "" { + return "" + } + return obj.Name } // ArgGen returns the Nth arg name for this function. @@ -104,12 +124,21 @@ func (obj *Func) Info() *interfaces.Info { typ = obj.Fn.Type() } - return &interfaces.Info{ - Pure: true, - Memo: false, // TODO: should this be something we specify here? + info := &interfaces.Info{ + Pure: false, + Memo: false, + Fast: false, + Spec: false, Sig: typ, Err: obj.Validate(), } + if fi := obj.FuncInfo; fi != nil { + info.Pure = fi.Pure + info.Memo = fi.Memo + info.Fast = fi.Fast + info.Spec = fi.Spec + } + return info } // Init runs some startup code for this function. @@ -184,5 +213,9 @@ func (obj *Func) Stream(ctx context.Context) error { // Call this function with the input args and return the value if it is possible // to do so at this time. func (obj *Func) Call(ctx context.Context, args []types.Value) (types.Value, error) { + if obj.Fn == nil { + // happens with speculative graph shape code paths + return nil, fmt.Errorf("nil function") + } return obj.Fn.Call(ctx, args) // (Value, error) } diff --git a/lang/interfaces/ast.go b/lang/interfaces/ast.go index 55cd1171..4ad7335a 100644 --- a/lang/interfaces/ast.go +++ b/lang/interfaces/ast.go @@ -508,6 +508,41 @@ func (obj *FuncSingleton) GraphFunc() (*pgraph.Graph, Func, error) { return obj.g, obj.f, nil } +// ValueEnv is a future potential argument to the Value() method on Expr. +// XXX: Consider using this if we want to improve graph shape even more. +type ValueEnv struct { + Variables map[Expr]types.Value +} + +// EmptyValueEnv returns the zero, empty value for the value env, with all the +// internal maps initialized appropriately. +func EmptyValueEnv() *ValueEnv { + return &ValueEnv{ + Variables: make(map[Expr]types.Value), + } +} + +// Copy makes a copy of the ValueEnv struct. This ensures that if the internal +// maps are changed, it doesn't affect other copies of the ValueEnv. It does +// *not* copy or change the pointers contained within, since these are +// references, and we need those to be consistently pointing to the same things +// after copying. +func (obj *ValueEnv) Copy() *ValueEnv { + if obj == nil { // allow copying nil envs + return EmptyValueEnv() + } + + variables := make(map[Expr]types.Value) + + for k, v := range obj.Variables { // copy + variables[k] = v // we don't copy the values! + } + + return &ValueEnv{ + Variables: variables, + } +} + // Arg represents a name identifier for a func or class argument declaration and // is sometimes accompanied by a type. This does not satisfy the Expr interface. type Arg struct { diff --git a/lang/interfaces/func.go b/lang/interfaces/func.go index ed111776..7d8779ee 100644 --- a/lang/interfaces/func.go +++ b/lang/interfaces/func.go @@ -44,6 +44,10 @@ import ( // FuncSig is the simple signature that is used throughout our implementations. type FuncSig = func(context.Context, []types.Value) (types.Value, error) +// GraphSig is the simple signature that is used throughout our implementations. +// TODO: Rename this? +type GraphSig = func(Txn, []Func) (Func, error) + // Compile-time guarantee that *types.FuncValue accepts a func of type FuncSig. var _ = &types.FuncValue{V: FuncSig(nil)} @@ -53,7 +57,8 @@ var _ = &types.FuncValue{V: FuncSig(nil)} type Info struct { Pure bool // is the function pure? (can it be memoized?) Memo bool // should the function be memoized? (false if too much output) - Slow bool // is the function slow? (avoid speculative execution) + Fast bool // is the function slow? (avoid speculative execution) + Spec bool // can we speculatively execute it? (true for most) Sig *types.Type // the signature of the function, must be KindFunc Err error // is this a valid function, or was it created improperly? } @@ -116,6 +121,8 @@ type Func interface { // It must close the Output chan if it's done sending new values out. It // must send at least one value, or return an error. It may also return // an error at anytime if it can't continue. + // XXX: Remove this from here, it should appear as StreamableFunc and + // funcs should implement StreamableFunc or CallableFunc or maybe both. Stream(context.Context) error } @@ -137,6 +144,7 @@ type Func interface { // the InferableFunc interface as well. type BuildableFunc interface { Func // implement everything in Func but add the additional requirements + // XXX: Should this be CopyableFunc instead? // Build takes the known or unified type signature for this function and // finalizes this structure so that it is now determined, and ready to @@ -273,7 +281,7 @@ type FuncData struct { // special functions that are useful in core. // TODO: This could be replaced if a func ever needs a SetScope method... type DataFunc interface { - Func // implement everything in Func but add the additional requirements + CopyableFunc // implement everything, but make it also have Copy // SetData is used by the language to pass our function some code-level // context. diff --git a/lang/interpret_test/TestAstFunc1/changing-func.txtar b/lang/interpret_test/TestAstFunc1/changing-func.txtar index d482a522..859c76b8 100644 --- a/lang/interpret_test/TestAstFunc1/changing-func.txtar +++ b/lang/interpret_test/TestAstFunc1/changing-func.txtar @@ -21,15 +21,21 @@ $out2 = $fn2() test "${out1}" {} test "${out2}" {} -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: call -> call # fn -Edge: call -> call # fn +Edge: FuncValue -> if # a +Edge: FuncValue -> if # a +Edge: FuncValue -> if # b +Edge: FuncValue -> if # b +Edge: const: bool(false) -> if # c +Edge: const: bool(true) -> if # c +Edge: if -> call # fn +Edge: if -> call # fn +Vertex: FuncValue +Vertex: FuncValue Vertex: FuncValue Vertex: FuncValue -Vertex: call -Vertex: call Vertex: call Vertex: call Vertex: const: bool(false) Vertex: const: bool(true) +Vertex: if +Vertex: if diff --git a/lang/interpret_test/TestAstFunc1/classes-polyfuncs.txtar b/lang/interpret_test/TestAstFunc1/classes-polyfuncs.txtar index 496d052a..2e1272e7 100644 --- a/lang/interpret_test/TestAstFunc1/classes-polyfuncs.txtar +++ b/lang/interpret_test/TestAstFunc1/classes-polyfuncs.txtar @@ -6,17 +6,14 @@ class c1($b) { test [fmt.printf("len is: %d", len($b)),] {} # len is 4 } -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: call -> composite: []str # 0 +Edge: composite: []int -> len # 0 Edge: const: int(-37) -> composite: []int # 3 Edge: const: int(0) -> composite: []int # 2 Edge: const: int(13) -> composite: []int # 0 Edge: const: int(42) -> composite: []int # 1 -Vertex: FuncValue -Vertex: FuncValue -Vertex: call -Vertex: call +Edge: const: str("len is: %d") -> printf: func(format str, a int) str # format +Edge: len -> printf: func(format str, a int) str # a +Edge: printf: func(format str, a int) str -> composite: []str # 0 Vertex: composite: []int Vertex: composite: []str Vertex: const: int(-37) @@ -24,3 +21,5 @@ Vertex: const: int(0) Vertex: const: int(13) Vertex: const: int(42) Vertex: const: str("len is: %d") +Vertex: len +Vertex: printf: func(format str, a int) str diff --git a/lang/interpret_test/TestAstFunc1/doubleclass.txtar b/lang/interpret_test/TestAstFunc1/doubleclass.txtar index 39627921..0383ea4f 100644 --- a/lang/interpret_test/TestAstFunc1/doubleclass.txtar +++ b/lang/interpret_test/TestAstFunc1/doubleclass.txtar @@ -15,36 +15,42 @@ include foo(1) include foo(2) include foo(3) -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: call -> composite: []str # 0 -Edge: call -> composite: []str # 0 -Edge: call -> composite: []str # 0 -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call +Edge: _operator -> _operator # a +Edge: _operator -> _operator # a +Edge: _operator -> _operator # a +Edge: _operator -> printf: func(format str, a int, b int) str # b +Edge: _operator -> printf: func(format str, a int, b int) str # b +Edge: _operator -> printf: func(format str, a int, b int) str # b +Edge: const: int(1) -> printf: func(format str, a int, b int) str # a +Edge: const: int(13) -> _operator # b +Edge: const: int(13) -> _operator # b +Edge: const: int(13) -> _operator # b +Edge: const: int(2) -> printf: func(format str, a int, b int) str # a +Edge: const: int(3) -> printf: func(format str, a int, b int) str # a +Edge: const: int(4) -> _operator # b +Edge: const: int(4) -> _operator # b +Edge: const: int(4) -> _operator # b +Edge: const: int(42) -> _operator # a +Edge: const: int(42) -> _operator # a +Edge: const: int(42) -> _operator # a +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("test-%d-%d") -> printf: func(format str, a int, b int) str # format +Edge: const: str("test-%d-%d") -> printf: func(format str, a int, b int) str # format +Edge: const: str("test-%d-%d") -> printf: func(format str, a int, b int) str # format +Edge: printf: func(format str, a int, b int) str -> composite: []str # 0 +Edge: printf: func(format str, a int, b int) str -> composite: []str # 0 +Edge: printf: func(format str, a int, b int) str -> composite: []str # 0 +Vertex: _operator +Vertex: _operator +Vertex: _operator +Vertex: _operator +Vertex: _operator +Vertex: _operator Vertex: composite: []str Vertex: composite: []str Vertex: composite: []str @@ -65,3 +71,6 @@ Vertex: const: str("+") Vertex: const: str("test-%d-%d") Vertex: const: str("test-%d-%d") Vertex: const: str("test-%d-%d") +Vertex: printf: func(format str, a int, b int) str +Vertex: printf: func(format str, a int, b int) str +Vertex: printf: func(format str, a int, b int) str diff --git a/lang/interpret_test/TestAstFunc1/duplicate_resource.txtar b/lang/interpret_test/TestAstFunc1/duplicate_resource.txtar index 9b38787e..e042698e 100644 --- a/lang/interpret_test/TestAstFunc1/duplicate_resource.txtar +++ b/lang/interpret_test/TestAstFunc1/duplicate_resource.txtar @@ -17,9 +17,7 @@ pkg "cowsay" { state => "newest", } -- OUTPUT -- -Edge: FuncValue -> call # fn -Vertex: FuncValue -Vertex: call +Edge: const: str("hello world") -> printf: func(format str) str # format Vertex: const: str("/tmp/foo") Vertex: const: str("/tmp/foo") Vertex: const: str("cowsay") @@ -28,3 +26,4 @@ Vertex: const: str("hello world") Vertex: const: str("hello world") Vertex: const: str("installed") Vertex: const: str("newest") +Vertex: printf: func(format str) str diff --git a/lang/interpret_test/TestAstFunc1/efficient-lambda.txtar b/lang/interpret_test/TestAstFunc1/efficient-lambda.txtar index ddb6f00b..e7e2e795 100644 --- a/lang/interpret_test/TestAstFunc1/efficient-lambda.txtar +++ b/lang/interpret_test/TestAstFunc1/efficient-lambda.txtar @@ -10,10 +10,17 @@ $out2 = $prefixer("b") test "${out1}" {} # helloa test "${out2}" {} # hellob -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Vertex: FuncValue -Vertex: call -Vertex: call +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("a") -> _operator # b +Edge: const: str("b") -> _operator # b +Edge: const: str("hello") -> _operator # a +Edge: const: str("hello") -> _operator # a +Vertex: _operator +Vertex: _operator +Vertex: const: str("+") +Vertex: const: str("+") Vertex: const: str("a") Vertex: const: str("b") +Vertex: const: str("hello") +Vertex: const: str("hello") diff --git a/lang/interpret_test/TestAstFunc1/graph5.txtar b/lang/interpret_test/TestAstFunc1/graph5.txtar index 1d18f7c0..9ebc08cb 100644 --- a/lang/interpret_test/TestAstFunc1/graph5.txtar +++ b/lang/interpret_test/TestAstFunc1/graph5.txtar @@ -4,9 +4,10 @@ test "t" { int64ptr => 42 + 13, } -- OUTPUT -- -Edge: FuncValue -> call # fn -Vertex: FuncValue -Vertex: call +Edge: const: int(13) -> _operator # b +Edge: const: int(42) -> _operator # a +Edge: const: str("+") -> _operator # op +Vertex: _operator Vertex: const: int(13) Vertex: const: int(42) Vertex: const: str("+") diff --git a/lang/interpret_test/TestAstFunc1/graph6.txtar b/lang/interpret_test/TestAstFunc1/graph6.txtar index 6d97d88b..48515047 100644 --- a/lang/interpret_test/TestAstFunc1/graph6.txtar +++ b/lang/interpret_test/TestAstFunc1/graph6.txtar @@ -4,12 +4,14 @@ test "t" { int64ptr => 42 + 13 - 99, } -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Vertex: FuncValue -Vertex: FuncValue -Vertex: call -Vertex: call +Edge: _operator -> _operator # a +Edge: const: int(13) -> _operator # b +Edge: const: int(42) -> _operator # a +Edge: const: int(99) -> _operator # b +Edge: const: str("+") -> _operator # op +Edge: const: str("-") -> _operator # op +Vertex: _operator +Vertex: _operator Vertex: const: int(13) Vertex: const: int(42) Vertex: const: int(99) diff --git a/lang/interpret_test/TestAstFunc1/graph7.txtar b/lang/interpret_test/TestAstFunc1/graph7.txtar index 97677968..ef239ae7 100644 --- a/lang/interpret_test/TestAstFunc1/graph7.txtar +++ b/lang/interpret_test/TestAstFunc1/graph7.txtar @@ -8,9 +8,10 @@ if true { } $i = 13 -- OUTPUT -- -Edge: FuncValue -> call # fn -Vertex: FuncValue -Vertex: call +Edge: const: int(13) -> _operator # b +Edge: const: int(42) -> _operator # a +Edge: const: str("+") -> _operator # op +Vertex: _operator Vertex: const: bool(true) Vertex: const: int(13) Vertex: const: int(42) diff --git a/lang/interpret_test/TestAstFunc1/hello0.txtar b/lang/interpret_test/TestAstFunc1/hello0.txtar index 94440bf4..251aadd3 100644 --- a/lang/interpret_test/TestAstFunc1/hello0.txtar +++ b/lang/interpret_test/TestAstFunc1/hello0.txtar @@ -7,9 +7,9 @@ test "greeting" { anotherstr => fmt.printf("hello: %s", $s), } -- OUTPUT -- -Edge: FuncValue -> call # fn -Vertex: FuncValue -Vertex: call +Edge: const: str("hello: %s") -> printf: func(format str, a str) str # format +Edge: const: str("world") -> printf: func(format str, a str) str # a Vertex: const: str("greeting") Vertex: const: str("hello: %s") Vertex: const: str("world") +Vertex: printf: func(format str, a str) str diff --git a/lang/interpret_test/TestAstFunc1/importscope0.txtar b/lang/interpret_test/TestAstFunc1/importscope0.txtar index ec34957e..dc02cd8b 100644 --- a/lang/interpret_test/TestAstFunc1/importscope0.txtar +++ b/lang/interpret_test/TestAstFunc1/importscope0.txtar @@ -16,13 +16,11 @@ class xclass { } } -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: call -> if # c Edge: const: str("bbb") -> if # a Edge: const: str("ccc") -> if # b -Vertex: FuncValue -Vertex: call +Edge: os.is_family_debian -> if # c Vertex: const: str("bbb") Vertex: const: str("ccc") Vertex: const: str("hello") Vertex: if +Vertex: os.is_family_debian diff --git a/lang/interpret_test/TestAstFunc1/importscope2.txtar b/lang/interpret_test/TestAstFunc1/importscope2.txtar index 79acdda8..4a06ba4c 100644 --- a/lang/interpret_test/TestAstFunc1/importscope2.txtar +++ b/lang/interpret_test/TestAstFunc1/importscope2.txtar @@ -15,13 +15,11 @@ class xclass { } } -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: call -> if # c Edge: const: str("bbb") -> if # a Edge: const: str("ccc") -> if # b -Vertex: FuncValue -Vertex: call +Edge: os.is_family_debian -> if # c Vertex: const: str("bbb") Vertex: const: str("ccc") Vertex: const: str("hello") Vertex: if +Vertex: os.is_family_debian diff --git a/lang/interpret_test/TestAstFunc1/lambda-chained.txtar b/lang/interpret_test/TestAstFunc1/lambda-chained.txtar index 19da0824..f62f0452 100644 --- a/lang/interpret_test/TestAstFunc1/lambda-chained.txtar +++ b/lang/interpret_test/TestAstFunc1/lambda-chained.txtar @@ -12,9 +12,27 @@ $out2 = $prefixer($out1) test "${out1}" {} test "${out2}" {} -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Vertex: FuncValue -Vertex: call -Vertex: call +Edge: _operator -> _operator # a +Edge: _operator -> _operator # a +Edge: _operator -> _operator # b +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str(":") -> _operator # b +Edge: const: str(":") -> _operator # b +Edge: const: str("hello") -> _operator # a +Edge: const: str("hello") -> _operator # a +Edge: const: str("world") -> _operator # b +Vertex: _operator +Vertex: _operator +Vertex: _operator +Vertex: _operator +Vertex: const: str("+") +Vertex: const: str("+") +Vertex: const: str("+") +Vertex: const: str("+") +Vertex: const: str(":") +Vertex: const: str(":") +Vertex: const: str("hello") Vertex: const: str("world") diff --git a/lang/interpret_test/TestAstFunc1/module_search1.txtar b/lang/interpret_test/TestAstFunc1/module_search1.txtar index 3f3a97bd..4e3f770f 100644 --- a/lang/interpret_test/TestAstFunc1/module_search1.txtar +++ b/lang/interpret_test/TestAstFunc1/module_search1.txtar @@ -62,27 +62,29 @@ $ex1 = $example1.name $name = "i am github.com/purpleidea/mgmt-example2/ and i contain: " + $ex1 -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call +Edge: _operator -> printf: func(format str, a int) str # a +Edge: _operator -> printf: func(format str, a str) str # a +Edge: _operator -> printf: func(format str, a str, b str) str # a +Edge: _operator -> printf: func(format str, a str, b str) str # b +Edge: const: int(3) -> _operator # b +Edge: const: int(42) -> _operator # a +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("i am github.com/purpleidea/mgmt-example1/ and i contain: ") -> _operator # a +Edge: const: str("i am github.com/purpleidea/mgmt-example1/ and i contain: ") -> _operator # a +Edge: const: str("i imported local: %s") -> printf: func(format str, a str) str # format +Edge: const: str("i imported remote: %s and %s") -> printf: func(format str, a str, b str) str # format +Edge: const: str("the answer is: %d") -> printf: func(format str, a int) str # format +Edge: const: str("this is module mod1 which contains: ") -> _operator # a +Edge: const: str("this is the nested git module mod1") -> _operator # b +Edge: const: str("this is the nested git module mod1") -> _operator # b +Edge: const: str("this is the nested local module mod1") -> _operator # b +Vertex: _operator +Vertex: _operator +Vertex: _operator +Vertex: _operator Vertex: const: int(3) Vertex: const: int(42) Vertex: const: str("+") @@ -101,3 +103,6 @@ Vertex: const: str("this is module mod1 which contains: ") Vertex: const: str("this is the nested git module mod1") Vertex: const: str("this is the nested git module mod1") Vertex: const: str("this is the nested local module mod1") +Vertex: printf: func(format str, a int) str +Vertex: printf: func(format str, a str) str +Vertex: printf: func(format str, a str, b str) str diff --git a/lang/interpret_test/TestAstFunc1/polydoubleinclude.txtar b/lang/interpret_test/TestAstFunc1/polydoubleinclude.txtar index ca0da33c..d933b7f5 100644 --- a/lang/interpret_test/TestAstFunc1/polydoubleinclude.txtar +++ b/lang/interpret_test/TestAstFunc1/polydoubleinclude.txtar @@ -10,22 +10,16 @@ class c1($a, $b) { } } -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn +Edge: composite: []int -> len # 0 Edge: const: int(-37) -> composite: []int # 3 Edge: const: int(0) -> composite: []int # 2 Edge: const: int(13) -> composite: []int # 0 Edge: const: int(42) -> composite: []int # 1 -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: call -Vertex: call -Vertex: call -Vertex: call +Edge: const: str("hello") -> len # 0 +Edge: const: str("len is: %d") -> printf: func(format str, a int) str # format +Edge: const: str("len is: %d") -> printf: func(format str, a int) str # format +Edge: len -> printf: func(format str, a int) str # a +Edge: len -> printf: func(format str, a int) str # a Vertex: composite: []int Vertex: const: int(-37) Vertex: const: int(0) @@ -36,3 +30,7 @@ Vertex: const: str("len is: %d") Vertex: const: str("len is: %d") Vertex: const: str("t1") Vertex: const: str("t2") +Vertex: len +Vertex: len +Vertex: printf: func(format str, a int) str +Vertex: printf: func(format str, a int) str diff --git a/lang/interpret_test/TestAstFunc1/recursive_module1.txtar b/lang/interpret_test/TestAstFunc1/recursive_module1.txtar index fb306f56..2fc6f798 100644 --- a/lang/interpret_test/TestAstFunc1/recursive_module1.txtar +++ b/lang/interpret_test/TestAstFunc1/recursive_module1.txtar @@ -60,27 +60,29 @@ $ex1 = $example1.name $name = "i am github.com/purpleidea/mgmt-example2/ and i contain: " + $ex1 -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call +Edge: _operator -> printf: func(format str, a int) str # a +Edge: _operator -> printf: func(format str, a str) str # a +Edge: _operator -> printf: func(format str, a str, b str) str # a +Edge: _operator -> printf: func(format str, a str, b str) str # b +Edge: const: int(3) -> _operator # b +Edge: const: int(42) -> _operator # a +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("i am github.com/purpleidea/mgmt-example1/ and i contain: ") -> _operator # a +Edge: const: str("i am github.com/purpleidea/mgmt-example1/ and i contain: ") -> _operator # a +Edge: const: str("i imported local: %s") -> printf: func(format str, a str) str # format +Edge: const: str("i imported remote: %s and %s") -> printf: func(format str, a str, b str) str # format +Edge: const: str("the answer is: %d") -> printf: func(format str, a int) str # format +Edge: const: str("this is module mod1 which contains: ") -> _operator # a +Edge: const: str("this is the nested git module mod1") -> _operator # b +Edge: const: str("this is the nested git module mod1") -> _operator # b +Edge: const: str("this is the nested local module mod1") -> _operator # b +Vertex: _operator +Vertex: _operator +Vertex: _operator +Vertex: _operator Vertex: const: int(3) Vertex: const: int(42) Vertex: const: str("+") @@ -99,3 +101,6 @@ Vertex: const: str("this is module mod1 which contains: ") Vertex: const: str("this is the nested git module mod1") Vertex: const: str("this is the nested git module mod1") Vertex: const: str("this is the nested local module mod1") +Vertex: printf: func(format str, a int) str +Vertex: printf: func(format str, a str) str +Vertex: printf: func(format str, a str, b str) str diff --git a/lang/interpret_test/TestAstFunc1/returned-func.txtar b/lang/interpret_test/TestAstFunc1/returned-func.txtar index dfb42837..5b882b19 100644 --- a/lang/interpret_test/TestAstFunc1/returned-func.txtar +++ b/lang/interpret_test/TestAstFunc1/returned-func.txtar @@ -12,7 +12,5 @@ $out = $fn() test "${out}" {} -- OUTPUT -- Edge: FuncValue -> call # fn -Edge: call -> call # fn Vertex: FuncValue Vertex: call -Vertex: call diff --git a/lang/interpret_test/TestAstFunc1/returned-lambda.txtar b/lang/interpret_test/TestAstFunc1/returned-lambda.txtar index 4684f8f2..8e7ead2a 100644 --- a/lang/interpret_test/TestAstFunc1/returned-lambda.txtar +++ b/lang/interpret_test/TestAstFunc1/returned-lambda.txtar @@ -11,7 +11,5 @@ $out = $fn() test "${out}" {} -- OUTPUT -- Edge: FuncValue -> call # fn -Edge: call -> call # fn Vertex: FuncValue Vertex: call -Vertex: call diff --git a/lang/interpret_test/TestAstFunc1/shape0.txtar b/lang/interpret_test/TestAstFunc1/shape0.txtar new file mode 100644 index 00000000..cd8987df --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/shape0.txtar @@ -0,0 +1,24 @@ +-- main.mcl -- +# this was originally: optimized-higher-order-function.txtar +import "fmt" + +func apply($f, $x) { + $f($x) +} +$add1 = func($x) { + $x + 1 +} +$z = apply($add1, 1) + +test [fmt.printf("%d", $z),] {} +-- OUTPUT -- +Edge: FuncValue -> call # fn +Edge: call -> printf: func(format str, a int) str # a +Edge: const: str("%d") -> printf: func(format str, a int) str # format +Edge: printf: func(format str, a int) str -> composite: []str # 0 +Vertex: FuncValue +Vertex: call +Vertex: composite: []str +Vertex: const: int(1) +Vertex: const: str("%d") +Vertex: printf: func(format str, a int) str diff --git a/lang/interpret_test/TestAstFunc1/shape1.txtar b/lang/interpret_test/TestAstFunc1/shape1.txtar new file mode 100644 index 00000000..3ee4f976 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/shape1.txtar @@ -0,0 +1,12 @@ +-- main.mcl -- +print ["a" + "b",] {} +-- OUTPUT -- +Edge: _operator -> composite: []str # 0 +Edge: const: str("+") -> _operator # op +Edge: const: str("a") -> _operator # a +Edge: const: str("b") -> _operator # b +Vertex: _operator +Vertex: composite: []str +Vertex: const: str("+") +Vertex: const: str("a") +Vertex: const: str("b") diff --git a/lang/interpret_test/TestAstFunc1/shape2.txtar b/lang/interpret_test/TestAstFunc1/shape2.txtar new file mode 100644 index 00000000..42f9b54b --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/shape2.txtar @@ -0,0 +1,16 @@ +-- main.mcl -- +$lambda = func($x) { + $x + "!" +} + +test [$lambda("hello"),] {} +-- OUTPUT -- +Edge: _operator -> composite: []str # 0 +Edge: const: str("!") -> _operator # b +Edge: const: str("+") -> _operator # op +Edge: const: str("hello") -> _operator # a +Vertex: _operator +Vertex: composite: []str +Vertex: const: str("!") +Vertex: const: str("+") +Vertex: const: str("hello") diff --git a/lang/interpret_test/TestAstFunc1/shape3.txtar b/lang/interpret_test/TestAstFunc1/shape3.txtar new file mode 100644 index 00000000..c0ab33c9 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/shape3.txtar @@ -0,0 +1,25 @@ +-- main.mcl -- +$lambda1 = func($x) { + $x + "!" +} +$lambda2 = func($x) { + $x + "?" +} + +$lambda = if true { # must be a const, otherwise this is a dynamic graph + $lambda1 +} else { + $lambda2 +} + +test [$lambda("hello"),] {} +-- OUTPUT -- +Edge: _operator -> composite: []str # 0 +Edge: const: str("!") -> _operator # b +Edge: const: str("+") -> _operator # op +Edge: const: str("hello") -> _operator # a +Vertex: _operator +Vertex: composite: []str +Vertex: const: str("!") +Vertex: const: str("+") +Vertex: const: str("hello") diff --git a/lang/interpret_test/TestAstFunc1/shape4.txtar b/lang/interpret_test/TestAstFunc1/shape4.txtar new file mode 100644 index 00000000..a94b9fd5 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/shape4.txtar @@ -0,0 +1,25 @@ +-- main.mcl -- +$lambda1 = func($x) { + $x + "!" +} +$lambda2 = func($x) { + $x + "?" +} + +$lambda = if 10 > 0 { # must be a const, otherwise this is a dynamic graph + $lambda1 +} else { + $lambda2 +} + +test [$lambda("hello"),] {} +-- OUTPUT -- +Edge: _operator -> composite: []str # 0 +Edge: const: str("!") -> _operator # b +Edge: const: str("+") -> _operator # op +Edge: const: str("hello") -> _operator # a +Vertex: _operator +Vertex: composite: []str +Vertex: const: str("!") +Vertex: const: str("+") +Vertex: const: str("hello") diff --git a/lang/interpret_test/TestAstFunc1/shape5.txtar b/lang/interpret_test/TestAstFunc1/shape5.txtar new file mode 100644 index 00000000..f43fba03 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/shape5.txtar @@ -0,0 +1,34 @@ +-- main.mcl -- +import "datetime" +import "fmt" + +$lambda1 = func($x) { + $x + "!" +} +$lambda2 = func($x) { + $x + "?" +} + +$lambda = if 10 > 0 { # must be a const, otherwise this is a dynamic graph + $lambda1 +} else { + $lambda2 +} + +$s = fmt.printf("%d", datetime.now()) + +test [$lambda($s),] {} +-- OUTPUT -- +Edge: _operator -> composite: []str # 0 +Edge: const: str("!") -> _operator # b +Edge: const: str("%d") -> printf: func(format str, a int) str # format +Edge: const: str("+") -> _operator # op +Edge: now -> printf: func(format str, a int) str # a +Edge: printf: func(format str, a int) str -> _operator # a +Vertex: _operator +Vertex: composite: []str +Vertex: const: str("!") +Vertex: const: str("%d") +Vertex: const: str("+") +Vertex: now +Vertex: printf: func(format str, a int) str diff --git a/lang/interpret_test/TestAstFunc1/shape6.txtar b/lang/interpret_test/TestAstFunc1/shape6.txtar new file mode 100644 index 00000000..beaffabc --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/shape6.txtar @@ -0,0 +1,33 @@ +-- main.mcl -- +import "iter" +import "golang" + +$fn = func($x) { # notable because concrete type is fn(t1) t2, where t1 != t2 + len($x) +} + +$in1 = ["a", "bb", "ccc",] + +$out1 = iter.map($in1, $fn) + +$s = golang.template("out1: {{ . }}", $out1) + +test [$s,] {} +-- OUTPUT -- +Edge: FuncValue -> map # function +Edge: composite: []str -> map # inputs +Edge: const: str("a") -> composite: []str # 0 +Edge: const: str("bb") -> composite: []str # 1 +Edge: const: str("ccc") -> composite: []str # 2 +Edge: const: str("out1: {{ . }}") -> template # template +Edge: map -> template # vars +Edge: template -> composite: []str # 0 +Vertex: FuncValue +Vertex: composite: []str +Vertex: composite: []str +Vertex: const: str("a") +Vertex: const: str("bb") +Vertex: const: str("ccc") +Vertex: const: str("out1: {{ . }}") +Vertex: map +Vertex: template diff --git a/lang/interpret_test/TestAstFunc1/shape7.txtar b/lang/interpret_test/TestAstFunc1/shape7.txtar new file mode 100644 index 00000000..9c450e05 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/shape7.txtar @@ -0,0 +1,57 @@ +-- main.mcl -- +import "datetime" +import "iter" +import "golang" + +$lambda1 = func($x) { + $x + "!" +} +$lambda2 = func($x) { + $x + "?" +} + +$lambda = if 10 > 0 { # must be a const, otherwise this is a dynamic graph + $lambda1 +} else { + $lambda2 +} + +$fn = func($x) { # notable because concrete type is fn(t1) t2, where t1 != t2 + len($x) +} + +$in1 = ["a", "bb", "ccc", "dddd", "eeeee",] + +$out1 = iter.map($in1, $fn) + +$s = golang.template("out1: {{ . }}", $out1) + +test [$lambda($s),] {} +-- OUTPUT -- +Edge: FuncValue -> map # function +Edge: _operator -> composite: []str # 0 +Edge: composite: []str -> map # inputs +Edge: const: str("!") -> _operator # b +Edge: const: str("+") -> _operator # op +Edge: const: str("a") -> composite: []str # 0 +Edge: const: str("bb") -> composite: []str # 1 +Edge: const: str("ccc") -> composite: []str # 2 +Edge: const: str("dddd") -> composite: []str # 3 +Edge: const: str("eeeee") -> composite: []str # 4 +Edge: const: str("out1: {{ . }}") -> template # template +Edge: map -> template # vars +Edge: template -> _operator # a +Vertex: FuncValue +Vertex: _operator +Vertex: composite: []str +Vertex: composite: []str +Vertex: const: str("!") +Vertex: const: str("+") +Vertex: const: str("a") +Vertex: const: str("bb") +Vertex: const: str("ccc") +Vertex: const: str("dddd") +Vertex: const: str("eeeee") +Vertex: const: str("out1: {{ . }}") +Vertex: map +Vertex: template diff --git a/lang/interpret_test/TestAstFunc1/shape8.txtar b/lang/interpret_test/TestAstFunc1/shape8.txtar new file mode 100644 index 00000000..b2febe09 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/shape8.txtar @@ -0,0 +1,38 @@ +-- main.mcl -- +$gt = func($one, $two) { + $one > $two +} + +$lambda1 = func($x) { + $x + "!" +} +$lambda2 = func($x) { + $x + "?" +} + +$lambda = if $gt(10, 0) { # must be a const, otherwise this is a dynamic graph + $lambda1 +} else { + $lambda2 +} + +test [$lambda("hello"),] {} +-- OUTPUT -- +Edge: FuncValue -> if # a +Edge: FuncValue -> if # b +Edge: _operator -> if # c +Edge: call -> composite: []str # 0 +Edge: const: int(0) -> _operator # b +Edge: const: int(10) -> _operator # a +Edge: const: str(">") -> _operator # op +Edge: if -> call # fn +Vertex: FuncValue +Vertex: FuncValue +Vertex: _operator +Vertex: call +Vertex: composite: []str +Vertex: const: int(0) +Vertex: const: int(10) +Vertex: const: str(">") +Vertex: const: str("hello") +Vertex: if diff --git a/lang/interpret_test/TestAstFunc1/simple-func1.txtar b/lang/interpret_test/TestAstFunc1/simple-func1.txtar index db710415..07da063a 100644 --- a/lang/interpret_test/TestAstFunc1/simple-func1.txtar +++ b/lang/interpret_test/TestAstFunc1/simple-func1.txtar @@ -7,6 +7,4 @@ $out1 = answer() test "${out1}" {} -- OUTPUT -- -Edge: FuncValue -> call # fn -Vertex: FuncValue -Vertex: call +Vertex: const: str("the answer is 42") diff --git a/lang/interpret_test/TestAstFunc1/simple-func2.txtar b/lang/interpret_test/TestAstFunc1/simple-func2.txtar index 47ad52b2..a34772b9 100644 --- a/lang/interpret_test/TestAstFunc1/simple-func2.txtar +++ b/lang/interpret_test/TestAstFunc1/simple-func2.txtar @@ -8,15 +8,12 @@ $out2 = answer() test [$out1 + $out2,] {} -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: call -> composite: []str # 0 -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: call -Vertex: call -Vertex: call +Edge: _operator -> composite: []str # 0 +Edge: const: str("+") -> _operator # op +Edge: const: str("the answer is 42") -> _operator # a +Edge: const: str("the answer is 42") -> _operator # b +Vertex: _operator Vertex: composite: []str Vertex: const: str("+") +Vertex: const: str("the answer is 42") +Vertex: const: str("the answer is 42") diff --git a/lang/interpret_test/TestAstFunc1/simple-lambda1.txtar b/lang/interpret_test/TestAstFunc1/simple-lambda1.txtar index 7cb942fd..aa81961c 100644 --- a/lang/interpret_test/TestAstFunc1/simple-lambda1.txtar +++ b/lang/interpret_test/TestAstFunc1/simple-lambda1.txtar @@ -10,6 +10,4 @@ $out = $answer() test "${out}" {} -- OUTPUT -- -Edge: FuncValue -> call # fn -Vertex: FuncValue -Vertex: call +Vertex: const: str("the answer is 42") diff --git a/lang/interpret_test/TestAstFunc1/simple-lambda2.txtar b/lang/interpret_test/TestAstFunc1/simple-lambda2.txtar index 7509bf5d..31f94bfd 100644 --- a/lang/interpret_test/TestAstFunc1/simple-lambda2.txtar +++ b/lang/interpret_test/TestAstFunc1/simple-lambda2.txtar @@ -11,14 +11,12 @@ $out2 = $answer() test [$out1 + $out2,] {} -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: call -> composite: []str # 0 -Vertex: FuncValue -Vertex: FuncValue -Vertex: call -Vertex: call -Vertex: call +Edge: _operator -> composite: []str # 0 +Edge: const: str("+") -> _operator # op +Edge: const: str("the answer is 42") -> _operator # a +Edge: const: str("the answer is 42") -> _operator # b +Vertex: _operator Vertex: composite: []str Vertex: const: str("+") +Vertex: const: str("the answer is 42") +Vertex: const: str("the answer is 42") diff --git a/lang/interpret_test/TestAstFunc1/slow_unification0.txtar b/lang/interpret_test/TestAstFunc1/slow_unification0.txtar index 6aab423a..0313f0da 100644 --- a/lang/interpret_test/TestAstFunc1/slow_unification0.txtar +++ b/lang/interpret_test/TestAstFunc1/slow_unification0.txtar @@ -52,27 +52,31 @@ if $state == "three" { Exec["timer"] -> Kv["${ns}"] } -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call +Edge: _lookup_default -> _operator # a +Edge: _lookup_default -> _operator # a +Edge: _lookup_default -> _operator # a +Edge: _lookup_default -> _operator # a +Edge: _operator -> _operator # a +Edge: _operator -> _operator # b +Edge: const: str("") -> _lookup_default # indexorkey +Edge: const: str("==") -> _operator # op +Edge: const: str("==") -> _operator # op +Edge: const: str("==") -> _operator # op +Edge: const: str("==") -> _operator # op +Edge: const: str("default") -> _lookup_default # default +Edge: const: str("default") -> _operator # b +Edge: const: str("estate") -> kvlookup # namespace +Edge: const: str("one") -> _operator # b +Edge: const: str("or") -> _operator # op +Edge: const: str("three") -> _operator # b +Edge: const: str("two") -> _operator # b +Edge: kvlookup -> _lookup_default # listormap +Vertex: _lookup_default +Vertex: _operator +Vertex: _operator +Vertex: _operator +Vertex: _operator +Vertex: _operator Vertex: const: str("") Vertex: const: str("/tmp/mgmt/state") Vertex: const: str("/tmp/mgmt/state") @@ -103,3 +107,4 @@ Vertex: const: str("timer") Vertex: const: str("timer") Vertex: const: str("two") Vertex: const: str("two") +Vertex: kvlookup diff --git a/lang/interpret_test/TestAstFunc1/static-function0.txtar b/lang/interpret_test/TestAstFunc1/static-function0.txtar index a9d7523a..a7935028 100644 --- a/lang/interpret_test/TestAstFunc1/static-function0.txtar +++ b/lang/interpret_test/TestAstFunc1/static-function0.txtar @@ -16,13 +16,9 @@ test "greeting3" { anotherstr => $fn(), } -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Vertex: FuncValue -Vertex: call -Vertex: call -Vertex: call Vertex: const: str("greeting1") Vertex: const: str("greeting2") Vertex: const: str("greeting3") +Vertex: const: str("hello world") +Vertex: const: str("hello world") +Vertex: const: str("hello world") diff --git a/lang/interpret_test/TestAstFunc1/static-function1.txtar b/lang/interpret_test/TestAstFunc1/static-function1.txtar index 5e0916ce..1044e2fd 100644 --- a/lang/interpret_test/TestAstFunc1/static-function1.txtar +++ b/lang/interpret_test/TestAstFunc1/static-function1.txtar @@ -18,13 +18,41 @@ test "greeting3" { anotherstr => $fn(), } -- OUTPUT -- -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Vertex: FuncValue -Vertex: call -Vertex: call -Vertex: call +Edge: _operator -> _operator # a +Edge: _operator -> _operator # a +Edge: _operator -> _operator # a +Edge: const: str(" ") -> _operator # b +Edge: const: str(" ") -> _operator # b +Edge: const: str(" ") -> _operator # b +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("+") -> _operator # op +Edge: const: str("hello") -> _operator # a +Edge: const: str("hello") -> _operator # a +Edge: const: str("hello") -> _operator # a +Edge: const: str("world") -> _operator # b +Edge: const: str("world") -> _operator # b +Edge: const: str("world") -> _operator # b +Vertex: _operator +Vertex: _operator +Vertex: _operator +Vertex: _operator +Vertex: _operator +Vertex: _operator +Vertex: const: str(" ") +Vertex: const: str(" ") +Vertex: const: str(" ") +Vertex: const: str("+") +Vertex: const: str("+") +Vertex: const: str("+") +Vertex: const: str("+") +Vertex: const: str("+") +Vertex: const: str("+") Vertex: const: str("greeting1") Vertex: const: str("greeting2") Vertex: const: str("greeting3") +Vertex: const: str("hello") +Vertex: const: str("world") diff --git a/lang/parser/lexparse_test.go b/lang/parser/lexparse_test.go index 10c1bada..d3821f7b 100644 --- a/lang/parser/lexparse_test.go +++ b/lang/parser/lexparse_test.go @@ -1744,7 +1744,8 @@ func TestLexParse0(t *testing.T) { &ast.StmtFunc{ Name: "f1", Func: &ast.ExprFunc{ - Args: []*interfaces.Arg{}, + Title: "f1", + Args: []*interfaces.Arg{}, Body: &ast.ExprInt{ V: 42, }, @@ -1765,6 +1766,7 @@ func TestLexParse0(t *testing.T) { } { fn := &ast.ExprFunc{ + Title: "f2", Args: []*interfaces.Arg{}, Return: types.TypeInt, Body: &ast.ExprCall{ @@ -1809,6 +1811,7 @@ func TestLexParse0(t *testing.T) { } { fn := &ast.ExprFunc{ + Title: "f3", Args: []*interfaces.Arg{ { Name: "a", @@ -1860,6 +1863,7 @@ func TestLexParse0(t *testing.T) { } { fn := &ast.ExprFunc{ + Title: "f4", Args: []*interfaces.Arg{ { Name: "x", @@ -2048,7 +2052,8 @@ func TestLexParse0(t *testing.T) { Name: "funcgen", // This is the outer function... Func: &ast.ExprFunc{ - Args: []*interfaces.Arg{}, + Title: "funcgen", + Args: []*interfaces.Arg{}, // This is the inner function... Body: &ast.ExprFunc{ Args: []*interfaces.Arg{}, diff --git a/lang/parser/parser.y b/lang/parser/parser.y index 891f104f..e04eafc2 100644 --- a/lang/parser/parser.y +++ b/lang/parser/parser.y @@ -255,9 +255,10 @@ stmt: $$.stmt = &ast.StmtFunc{ Name: $2.str, Func: &ast.ExprFunc{ - Args: $4.args, - //Return: nil, - Body: $7.expr, + Title: $2.str, + Args: $4.args, + Return: nil, + Body: $7.expr, }, } locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) @@ -266,6 +267,7 @@ stmt: | FUNC_IDENTIFIER IDENTIFIER OPEN_PAREN args CLOSE_PAREN type OPEN_CURLY expr CLOSE_CURLY { fn := &ast.ExprFunc{ + Title: $2.str, Args: $4.args, Return: $6.typ, // return type is known Body: $8.expr, diff --git a/lang/types/full/full.go b/lang/types/full/full.go index b6500477..97966d07 100644 --- a/lang/types/full/full.go +++ b/lang/types/full/full.go @@ -30,6 +30,7 @@ package full import ( + "context" "fmt" "github.com/purpleidea/mgmt/lang/interfaces" @@ -49,11 +50,17 @@ import ( // particular output node, which the function returns so that the caller may // connect that output node to more nodes down the line. // +// That's all done with the V field. You call it using the CallWithFuncs method. +// This function also has a variant that takes a direct function implementation +// which it can get called if needed. That's done with the F field. You call it +// using the CallWithValues method. +// // The function can also return an error which could represent that something // went horribly wrong. (Think, an internal panic.) type FuncValue struct { types.Base - V func(interfaces.Txn, []interfaces.Func) (interfaces.Func, error) + V interfaces.GraphSig + F interfaces.FuncSig T *types.Type // contains ordered field types, arg names are a bonus part } @@ -65,8 +72,12 @@ func NewFunc(t *types.Type) *FuncValue { v := func(interfaces.Txn, []interfaces.Func) (interfaces.Func, error) { return nil, fmt.Errorf("nil function") // TODO: is this correct? } + f := func(context.Context, []types.Value) (types.Value, error) { + return nil, fmt.Errorf("nil function") // TODO: is this correct? + } return &FuncValue{ V: v, + F: f, T: t, } } @@ -145,12 +156,20 @@ func (obj *FuncValue) Func() interface{} { } // 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? +func (obj *FuncValue) Set(fn interfaces.GraphSig) 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) { +// CallWithValues calls the function with the provided value args. +func (obj *FuncValue) CallWithValues(ctx context.Context, args []types.Value) (types.Value, error) { + if obj.F == nil { + return nil, fmt.Errorf("nil function") // TODO: Should we use ErrCantSpeculate? + } + return obj.F(ctx, args) +} + +// CallWithFuncs calls the function with the provided txn and func args. +func (obj *FuncValue) CallWithFuncs(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { return obj.V(txn, args) } diff --git a/lang/types/value.go b/lang/types/value.go index 3c5892e9..8bb2f2c8 100644 --- a/lang/types/value.go +++ b/lang/types/value.go @@ -1321,6 +1321,9 @@ func (obj *FuncValue) Value() interface{} { // inappropriate input types, or if it returns an inappropriate output type. func (obj *FuncValue) Call(ctx context.Context, args []Value) (Value, error) { // cmp input args type to obj.T + if obj.T == nil { + return nil, fmt.Errorf("the type is nil") + } length := len(obj.T.Ord) if length != len(args) { return nil, fmt.Errorf("arg length of %d does not match expected of %d", len(args), length)