From 6bb5930a575a47949e5b63f6809a94b250bc268e Mon Sep 17 00:00:00 2001 From: James Shubin Date: Tue, 3 Oct 2023 15:49:36 -0400 Subject: [PATCH 01/35] lang: interfaces Add CallableFunc interface Add a new interface for callable functions. --- lang/interfaces/func.go | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/lang/interfaces/func.go b/lang/interfaces/func.go index d79ed888b2..4d60d208ea 100644 --- a/lang/interfaces/func.go +++ b/lang/interfaces/func.go @@ -190,6 +190,19 @@ type OldPolyFunc interface { Build(*types.Type) (*types.Type, error) } +// CallableFunc is a function that can be called statically if we want to do it +// speculatively or from a resource. +type CallableFunc interface { + Func // implement everything in Func but add the additional requirements + + // Call this function with the input args and return the value if it is + // possible to do so at this time. To transform from the single value, + // graph representation of the callable values into a linear, standard + // args list for use here, you can use the StructToCallableArgs + // function. + Call(args []types.Value) (types.Value, error) +} + // NamedArgsFunc is a function that uses non-standard function arg names. If you // don't implement this, then the argnames (if specified) must correspond to the // a, b, c...z, aa, ab...az, ba...bz, and so on sequence. @@ -344,3 +357,33 @@ type Txn interface { // committed. Graph() *pgraph.Graph } + +// StructToCallableArgs transforms the single value, graph representation of the +// callable values into a linear, standard args list. +func StructToCallableArgs(st types.Value) ([]types.Value, error) { + if st == nil { + return nil, fmt.Errorf("empty struct") + } + typ := st.Type() + if typ == nil { + return nil, fmt.Errorf("empty type") + } + if kind := typ.Kind; kind != types.KindStruct { + return nil, fmt.Errorf("incorrect kind, got: %s", kind) + } + structValues := st.Struct() // map[string]types.Value + if structValues == nil { + return nil, fmt.Errorf("empty values") + } + + args := []types.Value{} + for i, x := range typ.Ord { // in the correct order + v, exists := structValues[x] + if !exists { + return nil, fmt.Errorf("invalid input value at %d", i) + } + + args = append(args, v) + } + return args, nil +} From f4df5a624373ad748f9fa4c6f9c6f535ffcbdc1f Mon Sep 17 00:00:00 2001 From: James Shubin Date: Wed, 6 Dec 2023 15:29:00 -0500 Subject: [PATCH 02/35] lang: funcs: structs: Add CallableFunc implementation for if func Add an initial implementation for the `if` function. --- lang/funcs/structs/if.go | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/lang/funcs/structs/if.go b/lang/funcs/structs/if.go index 7ad33d161a..ee0bf00e44 100644 --- a/lang/funcs/structs/if.go +++ b/lang/funcs/structs/if.go @@ -103,12 +103,13 @@ func (obj *IfFunc) Stream(ctx context.Context) error { } obj.last = input // store for next - var result types.Value - - if input.Struct()["c"].Bool() { - result = input.Struct()["a"] // true branch - } else { - result = input.Struct()["b"] // false branch + args, err := interfaces.StructToCallableArgs(input) // []types.Value, error + if err != nil { + return err + } + result, err := obj.Call(args) + if err != nil { + return err } // skip sending an update... @@ -129,3 +130,22 @@ func (obj *IfFunc) Stream(ctx context.Context) error { } } } + +// Call means this function implements the CallableFunc interface and can be +// called statically if we want to do it speculatively or from a resource. +func (obj *IfFunc) Call(args []types.Value) (types.Value, error) { + if obj.Info() == nil { + return nil, fmt.Errorf("info is empty") + } + if obj.Info().Sig == nil { + return nil, fmt.Errorf("sig is empty") + } + if i, j := len(args), len(obj.Info().Sig.Ord); i != j { + return nil, fmt.Errorf("arg length doesn't match, got %d, exp: %d", i, j) + } + + if args[0].Bool() { // condition + return args[1], nil // true branch + } + return args[2], nil // false branch +} From bf06110923ecf5d7a671674ededa1870448cb940 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Wed, 6 Dec 2023 15:59:50 -0500 Subject: [PATCH 03/35] lang: funcs: structs: Add CallableFunc implementation for const func Add an initial implementation for the `const` function. --- lang/funcs/structs/const.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lang/funcs/structs/const.go b/lang/funcs/structs/const.go index 34da9ae3df..970f5f5641 100644 --- a/lang/funcs/structs/const.go +++ b/lang/funcs/structs/const.go @@ -90,3 +90,19 @@ func (obj *ConstFunc) Stream(ctx context.Context) error { close(obj.init.Output) // signal that we're done sending return nil } + +// Call means this function implements the CallableFunc interface and can be +// called statically if we want to do it speculatively or from a resource. +func (obj *ConstFunc) Call(args []types.Value) (types.Value, error) { + if obj.Info() == nil { + return nil, fmt.Errorf("info is empty") + } + if obj.Info().Sig == nil { + return nil, fmt.Errorf("sig is empty") + } + if i, j := len(args), len(obj.Info().Sig.Ord); i != j { + return nil, fmt.Errorf("arg length doesn't match, got %d, exp: %d", i, j) + } + + return obj.Value, nil +} From 321e89f1b85d368f316581ce049fd6d3c70a9f3e Mon Sep 17 00:00:00 2001 From: James Shubin Date: Wed, 6 Dec 2023 16:12:22 -0500 Subject: [PATCH 04/35] lang: funcs: Add CallableFunc implementation for simple and simplepoly Add initial implementations for these wrapper functions. --- lang/funcs/simple/simple.go | 21 ++++++++++++++++---- lang/funcs/simplepoly/simplepoly.go | 30 ++++++++++++++++------------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/lang/funcs/simple/simple.go b/lang/funcs/simple/simple.go index 7ab5d266d4..4d93a50ad4 100644 --- a/lang/funcs/simple/simple.go +++ b/lang/funcs/simple/simple.go @@ -153,15 +153,18 @@ func (obj *WrappedFunc) Stream(ctx context.Context) error { obj.last = input // store for next } - values := []types.Value{} + // Use the existing implementation instead of this one + // which can't handle this case at the moment. + //args, err := interfaces.StructToCallableArgs(input) + args := []types.Value{} for _, name := range obj.Fn.Type().Ord { x := input.Struct()[name] - values = append(values, x) + args = append(args, x) } - result, err := obj.Fn.Call(values) // (Value, error) + result, err := obj.Call(args) if err != nil { - return errwrap.Wrapf(err, "simple function errored") + return err } // TODO: do we want obj.result to be a pointer instead? @@ -184,3 +187,13 @@ func (obj *WrappedFunc) Stream(ctx context.Context) error { } } } + +// Call means this function implements the CallableFunc interface and can be +// called statically if we want to do it speculatively or from a resource. +func (obj *WrappedFunc) Call(args []types.Value) (types.Value, error) { + result, err := obj.Fn.Call(args) // (Value, error) + if err != nil { + return nil, errwrap.Wrapf(err, "simple function errored") + } + return result, err +} diff --git a/lang/funcs/simplepoly/simplepoly.go b/lang/funcs/simplepoly/simplepoly.go index 1c0d237b47..df3d9a31be 100644 --- a/lang/funcs/simplepoly/simplepoly.go +++ b/lang/funcs/simplepoly/simplepoly.go @@ -581,24 +581,18 @@ func (obj *WrappedFunc) Stream(ctx context.Context) error { obj.last = input // store for next } - values := []types.Value{} + // Use the existing implementation instead of this one + // which can't handle this case at the moment. + //args, err := interfaces.StructToCallableArgs(input) + args := []types.Value{} for _, name := range obj.fn.Type().Ord { x := input.Struct()[name] - values = append(values, x) + args = append(args, x) } - if obj.init.Debug { - obj.init.Logf("Calling function with: %+v", values) - } - result, err := obj.fn.Call(values) // (Value, error) + result, err := obj.Call(args) if err != nil { - if obj.init.Debug { - obj.init.Logf("Function returned error: %+v", err) - } - return errwrap.Wrapf(err, "simple poly function errored") - } - if obj.init.Debug { - obj.init.Logf("Function returned with: %+v", result) + return err } // TODO: do we want obj.result to be a pointer instead? @@ -621,3 +615,13 @@ func (obj *WrappedFunc) Stream(ctx context.Context) error { } } } + +// Call means this function implements the CallableFunc interface and can be +// called statically if we want to do it speculatively or from a resource. +func (obj *WrappedFunc) Call(args []types.Value) (types.Value, error) { + result, err := obj.fn.Call(args) // (Value, error) + if err != nil { + return nil, errwrap.Wrapf(err, "simple poly function errored") + } + return result, err +} From a2649777307b267396a50c294894958d04d89453 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Wed, 6 Dec 2023 16:13:55 -0500 Subject: [PATCH 05/35] lang: funcs: facts: Add CallableFunc implementation for fact Add an initial implementation for this wrapper function. --- lang/funcs/facts/facts.go | 13 +++++++++++++ lang/funcs/facts/func.go | 12 ++++++++++++ 2 files changed, 25 insertions(+) diff --git a/lang/funcs/facts/facts.go b/lang/funcs/facts/facts.go index 0e1eb65718..5057691078 100644 --- a/lang/funcs/facts/facts.go +++ b/lang/funcs/facts/facts.go @@ -86,3 +86,16 @@ type Fact interface { Init(*Init) error Stream(context.Context) error } + +// CallableFunc is a function that can be called statically if we want to do it +// speculatively or from a resource. +type CallableFact interface { + Fact // implement everything in Fact but add the additional requirements + + // Call this function with the input args and return the value if it is + // possible to do so at this time. To transform from the single value, + // graph representation of the callable values into a linear, standard + // args list for use here, you can use the StructToCallableArgs + // function. + Call() (types.Value, error) +} diff --git a/lang/funcs/facts/func.go b/lang/funcs/facts/func.go index b4c3c56ec7..6e9eed3851 100644 --- a/lang/funcs/facts/func.go +++ b/lang/funcs/facts/func.go @@ -79,3 +79,15 @@ func (obj *FactFunc) Init(init *interfaces.Init) error { func (obj *FactFunc) Stream(ctx context.Context) error { return obj.Fact.Stream(ctx) } + +// Call means this function implements the CallableFunc interface and can be +// called statically if we want to do it speculatively or from a resource. +func (obj *FactFunc) Call() (types.Value, error) { + + callableFact, ok := obj.Fact.(CallableFact) + if !ok { + return nil, fmt.Errorf("fact is not a CallableFact") + } + + return callableFact.Call() +} From d6d3bd5a5c16092ea25149301892217bf21c342f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Sat, 20 Jan 2024 15:32:15 -0500 Subject: [PATCH 06/35] pseudo code for the static graph optimization --- lang/ast/structs.go | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index e17f4696a4..4f9a35fcef 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -8553,9 +8553,32 @@ func (obj *ExprCall) Graph(env map[string]interfaces.Func) (*pgraph.Graph, inter graph.AddVertex(paramFunc) funcValueFunc = paramFunc } else { - // The function being called is a top-level definition. The parameters which - // are visible at this use site must not be visible at the definition site, - // so we pass an empty environment. + // The function being called is a top-level definition. + + // Optimization: in the common case in which the function is + // statically-known, generate a single static sub-graph. + exprValue, err := obj.expr.Value() + if err == nil { + exprFuncValue, ok := exprValue.(*full.FuncValue) + if ok { + struct GraphTxn { + Graph pgraph.Graph + } + txn := GraphTxn() + outputFunc, err := exprFuncValue.Call(txn, []Func) + if err != nil { + return nil, nil, errwap.Wrapf(err, "could not construct the static graph for a function call") + } + return txn.Graph, outputFunc, nil + } + } + + // Otherwise, generate a CallFunc node, which will dynamically + // re-create the sub-graph as needed. + + // The parameters which are visible at this use site must not + // be visible at the definition site, so we pass an empty + // environment. emptyEnv := map[string]interfaces.Func{} exprGraph, topLevelFunc, err := obj.expr.Graph(emptyEnv) if err != nil { From debbf55a3317a3ddf14c02f3b569ccc3cf5ab8cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Sat, 20 Jan 2024 15:40:53 -0500 Subject: [PATCH 07/35] an example which the pseudo-code does _not_ support yet --- .../TestAstFunc1/optimized-higher-order-function.txtar | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 lang/interpret_test/TestAstFunc1/optimized-higher-order-function.txtar diff --git a/lang/interpret_test/TestAstFunc1/optimized-higher-order-function.txtar b/lang/interpret_test/TestAstFunc1/optimized-higher-order-function.txtar new file mode 100644 index 0000000000..e90a553c06 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/optimized-higher-order-function.txtar @@ -0,0 +1,7 @@ +func apply($f, $x) { + $f($x) +} +$add1 = fn($x) { + $x + 1 +} +$z = apply($add, 1) From eab4f87d3cc1ccfb85ac25d25619c5e07ed96abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Sat, 20 Jan 2024 16:12:46 -0500 Subject: [PATCH 08/35] pseudo code for ExprFunc.Value() --- lang/ast/structs.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 4f9a35fcef..c75a1adb16 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -7714,12 +7714,14 @@ func (obj *ExprFunc) SetValue(value types.Value) error { // This might get called speculatively (early) during unification to learn more. // This particular value is always known since it is a constant. func (obj *ExprFunc) Value() (types.Value, error) { - panic("ExprFunc does not store its latest value because resources don't yet have function fields.") - //// TODO: implement speculative value lookup (if not already sufficient) - //return &full.FuncValue{ - // V: obj.V, - // T: obj.typ, - //}, nil + // MCL case: copy the code from ExprFunc.Graph which creates this FuncValue + // builtin case: look up the full.FuncValue or the FuncValue somewhere? + // polymorphic case: figure out which one has the correct type and wrap + // it in a full.FuncValue. + return &full.FuncValue{ + V: obj.V, + T: obj.typ, + }, nil } // ExprCall is a representation of a function call. This does not represent the From 9506b02da9849038794c0a9a7d4a83b04259b859 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Sat, 20 Jan 2024 16:13:59 -0500 Subject: [PATCH 09/35] XXX: WIP --- lang/ast/structs.go | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index c75a1adb16..d9abb68ded 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -8556,31 +8556,48 @@ func (obj *ExprCall) Graph(env map[string]interfaces.Func) (*pgraph.Graph, inter funcValueFunc = paramFunc } else { // The function being called is a top-level definition. - + // Optimization: in the common case in which the function is // statically-known, generate a single static sub-graph. - exprValue, err := obj.expr.Value() - if err == nil { + exprValue, err := obj.expr.Value() + if err == nil { exprFuncValue, ok := exprValue.(*full.FuncValue) if ok { - struct GraphTxn { - Graph pgraph.Graph + +// XXX JAMES TODO +// return (&graphTxn{ +// Lock: obj.Lock, +// Unlock: obj.Unlock, +// GraphAPI: obj, +// RefCount: obj.refCount, // reference counting +// FreeFunc: free, +// }).init() + + txn := (&interfaces.GraphTxn{}).Init() // XXX no, we want a different implementation of Txn + args := []interfaces.Func{} + for _, x := range obj.Args { // []interfaces.Expr + g, f, err := obj.expr.Graph(env) + if err != nil { + return nil, nil, errwap.Wrapf(err, "could not even XXX") + } + args = append(args, f) + txn.AddGraph(g) } - txn := GraphTxn() - outputFunc, err := exprFuncValue.Call(txn, []Func) + outputFunc, err := exprFuncValue.Call(txn, args) if err != nil { return nil, nil, errwap.Wrapf(err, "could not construct the static graph for a function call") } - return txn.Graph, outputFunc, nil + + return txn.Graph(), outputFunc, nil } } // Otherwise, generate a CallFunc node, which will dynamically // re-create the sub-graph as needed. - - // The parameters which are visible at this use site must not - // be visible at the definition site, so we pass an empty - // environment. + + // The parameters which are visible at this use site must not + // be visible at the definition site, so we pass an empty + // environment. emptyEnv := map[string]interfaces.Func{} exprGraph, topLevelFunc, err := obj.expr.Graph(emptyEnv) if err != nil { From 2df932a8fae0052364a23b41043b9fc7f837fcf6 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Sat, 20 Jan 2024 16:20:53 -0500 Subject: [PATCH 10/35] XXX: WIP ExprFunc.Value --- lang/ast/structs.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index d9abb68ded..3a1b8e3688 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -7718,6 +7718,39 @@ func (obj *ExprFunc) Value() (types.Value, error) { // builtin case: look up the full.FuncValue or the FuncValue somewhere? // polymorphic case: figure out which one has the correct type and wrap // it in a full.FuncValue. + if obj.Body != nil { + env := make(map[string]interfaces.Func) XXX ??? + return &full.FuncValue{ + V: func(innerTxn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { + // Extend the environment with the arguments. + extendedEnv := make(map[string]interfaces.Func) + for k, v := range env { + extendedEnv[k] = v + } + for i, arg := range obj.Args { + extendedEnv[arg.Name] = args[i] + } + + // Create a subgraph from the lambda's body, instantiating the + // lambda's parameters with the args and the other variables + // with the nodes in the captured environment. + subgraph, bodyFunc, err := obj.Body.Graph(extendedEnv) + if err != nil { + return nil, errwrap.Wrapf(err, "could not create the lambda body's subgraph") + } + + innerTxn.AddGraph(subgraph) + + return bodyFunc, nil + }, + T: obj.typ, + }, nil + } else if obj.Function != nil { + + } else /* len(obj.Values) > 0 */ { + + } + return &full.FuncValue{ V: obj.V, T: obj.typ, From aa43cbf3cae5ae977e34ec312dd94a45e831e694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Sat, 20 Jan 2024 16:25:57 -0500 Subject: [PATCH 11/35] pseudo code for ExprCall.Value() --- lang/ast/structs.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 3a1b8e3688..ead4006faa 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -8691,6 +8691,12 @@ func (obj *ExprCall) Value() (types.Value, error) { if obj.V == nil { return nil, fmt.Errorf("func value does not yet exist") } + + // speculatively call Value() on obj.Expr and each arg. + // if all successful, we will have a full.FuncValue and a []Value. + // full.FuncValue _also_ needs a speculative Call([]Value) Value + // method, in addition to its existing Call([]Func) Func method. + // call it. return obj.V, nil } From 723d2f82776d3dac5517740ce63b4de2af6f6a91 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Mon, 22 Jan 2024 12:55:31 -0500 Subject: [PATCH 12/35] XXX: messy pseudo-code to fix-up more --- lang/ast/structs.go | 82 +++++++++++++++++++++++++------------- lang/funcs/structs/util.go | 16 ++++++++ 2 files changed, 71 insertions(+), 27 deletions(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index ead4006faa..90e4c122ee 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -33,6 +33,7 @@ import ( "github.com/purpleidea/mgmt/lang/funcs" "github.com/purpleidea/mgmt/lang/funcs/core" "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" @@ -7716,10 +7717,10 @@ func (obj *ExprFunc) SetValue(value types.Value) error { func (obj *ExprFunc) Value() (types.Value, error) { // MCL case: copy the code from ExprFunc.Graph which creates this FuncValue // builtin case: look up the full.FuncValue or the FuncValue somewhere? - // polymorphic case: figure out which one has the correct type and wrap - // it in a full.FuncValue. + // polymorphic case: figure out which one has the correct type and wrap + // it in a full.FuncValue. if obj.Body != nil { - env := make(map[string]interfaces.Func) XXX ??? + env := make(map[string]interfaces.Func) // XXX ??? SAM will decide what to do here return &full.FuncValue{ V: func(innerTxn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { // Extend the environment with the arguments. @@ -7745,14 +7746,17 @@ func (obj *ExprFunc) Value() (types.Value, error) { }, T: obj.typ, }, nil - } else if obj.Function != nil { - } else /* len(obj.Values) > 0 */ { + } else if obj.Function != nil { + // builtin case: look up the full.FuncValue or the FuncValue somewhere? + return structs.FuncToFullFuncValue(obj.function, obj.typ), nil } + // else if /* len(obj.Values) > 0 */ + panic("what to do here") return &full.FuncValue{ - V: obj.V, + //V: obj.V, // XXX ??? T: obj.typ, }, nil } @@ -8596,29 +8600,25 @@ func (obj *ExprCall) Graph(env map[string]interfaces.Func) (*pgraph.Graph, inter if err == nil { exprFuncValue, ok := exprValue.(*full.FuncValue) if ok { - -// XXX JAMES TODO -// return (&graphTxn{ -// Lock: obj.Lock, -// Unlock: obj.Unlock, -// GraphAPI: obj, -// RefCount: obj.refCount, // reference counting -// FreeFunc: free, -// }).init() - - txn := (&interfaces.GraphTxn{}).Init() // XXX no, we want a different implementation of Txn + txn := (&txn.Graph{ + Debug: obj.data.debug, + Logf: func(format string, v ...interface{}) { + obj.data.Logf(format, v...) + }, + }.Init()) args := []interfaces.Func{} - for _, x := range obj.Args { // []interfaces.Expr + for _, arg := range obj.Args { // []interfaces.Expr + _ = arg // XXX: ??? obj.expr or arg.Graph ? g, f, err := obj.expr.Graph(env) if err != nil { - return nil, nil, errwap.Wrapf(err, "could not even XXX") + return nil, nil, errwrap.Wrapf(err, "could not even XXX") } args = append(args, f) txn.AddGraph(g) } outputFunc, err := exprFuncValue.Call(txn, args) if err != nil { - return nil, nil, errwap.Wrapf(err, "could not construct the static graph for a function call") + return nil, nil, errwrap.Wrapf(err, "could not construct the static graph for a function call") } return txn.Graph(), outputFunc, nil @@ -8688,16 +8688,44 @@ func (obj *ExprCall) SetValue(value types.Value) error { // This particular implementation of the function returns the previously stored // and cached value as received by SetValue. func (obj *ExprCall) Value() (types.Value, error) { - if obj.V == nil { + if obj.expr == nil { return nil, fmt.Errorf("func value does not yet exist") } - // speculatively call Value() on obj.Expr and each arg. - // if all successful, we will have a full.FuncValue and a []Value. - // full.FuncValue _also_ needs a speculative Call([]Value) Value - // method, in addition to its existing Call([]Func) Func method. - // call it. - return obj.V, nil + // speculatively call Value() on obj.expr and each arg. + value, err := obj.expr.Value() // speculative + if err != nil { + return nil, err + } + + funcValue, ok := value.(*full.FuncValue) + if !ok { + return nil, fmt.Errorf("not a func value") + } + + args := []types.Value{} + for _, arg := range obj.Args { // []interfaces.Expr + a, err := arg.Value() // speculative + if err != nil { + return nil, err + } + args = append(args, a) + } + + //callable, ok := v.(*XXX) // XXX ??? + + // if all successful, we will have a full.FuncValue and a []Value. + // full.FuncValue _also_ needs a speculative Call([]Value) Value + // method, in addition to its existing Call([]Func) Func method. + // call it. + + //if !ok { + // return nil, fmt.Errorf("not callable") + //} + + //XXX: full: func (obj *FuncValue) Call(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) + + return funcValue.Call(args) // XXX ??? } // ExprVar is a representation of a variable lookup. It returns the expression diff --git a/lang/funcs/structs/util.go b/lang/funcs/structs/util.go index 60f60b83e8..670910cc81 100644 --- a/lang/funcs/structs/util.go +++ b/lang/funcs/structs/util.go @@ -72,3 +72,19 @@ func SimpleFnToFuncValue(name string, fv *types.FuncValue) *full.FuncValue { func SimpleFnToConstFunc(name string, fv *types.FuncValue) interfaces.Func { return FuncValueToConstFunc(SimpleFnToFuncValue(name, fv)) } + +// XXX sam comment here +func FuncToFullFuncValue(valueTransformingFunc interfaces.Func, typ *types.Type) *full.FuncValue { + return &full.FuncValue{ + V: func(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { + for i, arg := range args { + argName := typ.Ord[i] + txn.AddEdge(arg, valueTransformingFunc, &interfaces.FuncEdge{ + Args: []string{argName}, + }) + } + return valueTransformingFunc, nil + }, + T: typ, + } +} From 27f155fc64df49119264846da2af5e924ed04ca0 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Mon, 22 Jan 2024 15:59:30 -0500 Subject: [PATCH 13/35] XXX: lang: ast: Add Value speculation sentinel error If it's useful, we have it... XXX --- lang/ast/structs.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 90e4c122ee..210792623a 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -149,6 +149,16 @@ const ( // ErrNoStoredScope is an error that tells us we can't get a scope here. ErrNoStoredScope = interfaces.Error("scope is not stored in this node") + // ErrCantSpeculate is an error that explains that we can't speculate + // when trying to run the Value() method of Expr. This can be useful if + // we want to distinguish between "something is broken" and "I can't + // produce a value at this time" which can be identified and we can + // continue. If we don't get this error, then it's okay to shut + // everything down. + // XXX: SAM HERE IS A SENTINEL ERROR IN CASE YOU WANT TO USE IT. I THINK + // IT MIGHT ACTUALLY NOT BE NECESSARY, BUT LET'S SEE IF IT'S USEFUL... + ErrCantSpeculate = interfaces.Error("can't speculate for value") + // ErrFuncPointerNil is an error that explains the function pointer for // table lookup is missing. If this happens, it's most likely a // programming error. From 8138b5cae593e34eebc2b9a8ac50bda558802503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Tue, 23 Jan 2024 22:13:36 -0500 Subject: [PATCH 14/35] fill in comment --- lang/funcs/structs/util.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lang/funcs/structs/util.go b/lang/funcs/structs/util.go index 670910cc81..a9ebd9e1b3 100644 --- a/lang/funcs/structs/util.go +++ b/lang/funcs/structs/util.go @@ -73,7 +73,9 @@ func SimpleFnToConstFunc(name string, fv *types.FuncValue) interfaces.Func { return FuncValueToConstFunc(SimpleFnToFuncValue(name, fv)) } -// XXX sam comment here +// 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(valueTransformingFunc interfaces.Func, typ *types.Type) *full.FuncValue { return &full.FuncValue{ V: func(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { From b9b9796393ba8b351ecbbadf69ab355dff656cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Tue, 23 Jan 2024 22:53:03 -0500 Subject: [PATCH 15/35] remove outdated comment we do include obj.Args in the ordering logic now. --- lang/ast/structs.go | 1 - 1 file changed, 1 deletion(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 210792623a..af1c04b8b5 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -7220,7 +7220,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 { From 29341a0ad4424e23619e7a8b4443b85fd3ef9e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Tue, 23 Jan 2024 22:55:17 -0500 Subject: [PATCH 16/35] answer to XXX comment in commit description we cover the args and the body, that covers both the newly-bound variables and all the variable usages in a lambda. builtins don't need ordering arrows because builtins are available from the beginning, there is no question about whether they should be processed before or after a given Stmt. --- lang/ast/structs.go | 1 - 1 file changed, 1 deletion(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index af1c04b8b5..dbd06332c6 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -7247,7 +7247,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 { From 94455ee132396adbb0ebf1e0c9cc96b93b095e76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Tue, 23 Jan 2024 23:03:03 -0500 Subject: [PATCH 17/35] explain what to do in the obj.Values case --- lang/ast/structs.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index dbd06332c6..63f32bc826 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -7723,10 +7723,6 @@ func (obj *ExprFunc) SetValue(value types.Value) error { // This might get called speculatively (early) during unification to learn more. // This particular value is always known since it is a constant. func (obj *ExprFunc) Value() (types.Value, error) { - // MCL case: copy the code from ExprFunc.Graph which creates this FuncValue - // builtin case: look up the full.FuncValue or the FuncValue somewhere? - // polymorphic case: figure out which one has the correct type and wrap - // it in a full.FuncValue. if obj.Body != nil { env := make(map[string]interfaces.Func) // XXX ??? SAM will decide what to do here return &full.FuncValue{ @@ -7756,12 +7752,13 @@ func (obj *ExprFunc) Value() (types.Value, error) { }, nil } else if obj.Function != nil { - // builtin case: look up the full.FuncValue or the FuncValue somewhere? return structs.FuncToFullFuncValue(obj.function, obj.typ), nil } // else if /* len(obj.Values) > 0 */ panic("what to do here") + // polymorphic case: figure out which one has the correct type and wrap + // it in a full.FuncValue. return &full.FuncValue{ //V: obj.V, // XXX ??? From d4270af55609516ea5a783192906c804fed7ca8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Tue, 23 Jan 2024 23:11:33 -0500 Subject: [PATCH 18/35] yes, I did mean arg.Graph() --- lang/ast/structs.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 63f32bc826..df7a365c64 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -8613,8 +8613,7 @@ func (obj *ExprCall) Graph(env map[string]interfaces.Func) (*pgraph.Graph, inter }.Init()) args := []interfaces.Func{} for _, arg := range obj.Args { // []interfaces.Expr - _ = arg // XXX: ??? obj.expr or arg.Graph ? - g, f, err := obj.expr.Graph(env) + g, f, err := arg.Graph(env) if err != nil { return nil, nil, errwrap.Wrapf(err, "could not even XXX") } From d29983eb5cc79e9b4b099a0d94a630760bada13a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Tue, 23 Jan 2024 23:13:40 -0500 Subject: [PATCH 19/35] better error message --- lang/ast/structs.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index df7a365c64..0cb4d03f9f 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -8612,10 +8612,10 @@ func (obj *ExprCall) Graph(env map[string]interfaces.Func) (*pgraph.Graph, inter }, }.Init()) args := []interfaces.Func{} - for _, arg := range obj.Args { // []interfaces.Expr + for i, arg := range obj.Args { // []interfaces.Expr g, f, err := arg.Graph(env) if err != nil { - return nil, nil, errwrap.Wrapf(err, "could not even XXX") + return nil, nil, errwrap.Wrapf(err, "could not get graph for arg %d", i) } args = append(args, f) txn.AddGraph(g) From 899b5d55cc67bc1b5ece1250c43a11abf908939e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Tue, 23 Jan 2024 23:23:56 -0500 Subject: [PATCH 20/35] reword comment in the hope it is clear this time --- lang/ast/structs.go | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 0cb4d03f9f..2b25e80029 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -8716,20 +8716,11 @@ func (obj *ExprCall) Value() (types.Value, error) { args = append(args, a) } - //callable, ok := v.(*XXX) // XXX ??? - - // if all successful, we will have a full.FuncValue and a []Value. - // full.FuncValue _also_ needs a speculative Call([]Value) Value - // method, in addition to its existing Call([]Func) Func method. - // call it. - - //if !ok { - // return nil, fmt.Errorf("not callable") - //} - - //XXX: full: func (obj *FuncValue) Call(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) - - return funcValue.Call(args) // XXX ??? + // we now have a full.FuncValue and a []Value. We can't call the existing + // Call([]Func) Func + // method on the FuncValue, we need a speculative + // Call([]Value) Value + // method. so the next step is to implement that method. } // ExprVar is a representation of a variable lookup. It returns the expression From eba4f9d37ee8f9ffc8a65e64f5996029518c5cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Tue, 23 Jan 2024 23:53:02 -0500 Subject: [PATCH 21/35] CheckParamScope --- lang/ast/structs.go | 120 +++++++++++++++++++++++++++++++++++++++++ lang/interfaces/ast.go | 3 ++ 2 files changed, 123 insertions(+) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 2b25e80029..49ac7f2745 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -5184,6 +5184,10 @@ func (obj *ExprBool) SetScope(scope *interfaces.Scope, sctx map[string]interface return nil } +func (obj *ExprBool) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + return nil +} + // SetType will make no changes if called here. It will error if anything other // than a Bool is passed in, and doesn't need to be called for this expr to // work. @@ -5364,6 +5368,10 @@ func (obj *ExprStr) SetScope(scope *interfaces.Scope, sctx map[string]interfaces return nil } +func (obj *ExprStr) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + return nil +} + // SetType will make no changes if called here. It will error if anything other // than an Str is passed in, and doesn't need to be called for this expr to // work. @@ -5494,6 +5502,10 @@ func (obj *ExprInt) SetScope(scope *interfaces.Scope, sctx map[string]interfaces return nil } +func (obj *ExprInt) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + return nil +} + // SetType will make no changes if called here. It will error if anything other // than an Int is passed in, and doesn't need to be called for this expr to // work. @@ -5626,6 +5638,10 @@ func (obj *ExprFloat) SetScope(scope *interfaces.Scope, sctx map[string]interfac return nil } +func (obj *ExprFloat) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + return nil +} + // SetType will make no changes if called here. It will error if anything other // than a Float is passed in, and doesn't need to be called for this expr to // work. @@ -5839,6 +5855,15 @@ func (obj *ExprList) SetScope(scope *interfaces.Scope, sctx map[string]interface return nil } +func (obj *ExprList) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + for _, x := range obj.Elements { + if err := x.CheckParamScope(freeVars); err != nil { + return err + } + } + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -6271,6 +6296,18 @@ func (obj *ExprMap) SetScope(scope *interfaces.Scope, sctx map[string]interfaces return nil } +func (obj *ExprMap) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + for _, x := range obj.KVs { + if err := x.Key.CheckParamScope(freeVars); err != nil { + return err + } + if err := x.Val.CheckParamScope(freeVars); err != nil { + return err + } + } + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -6743,6 +6780,15 @@ func (obj *ExprStruct) SetScope(scope *interfaces.Scope, sctx map[string]interfa return nil } +func (obj *ExprStruct) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + for _, x := range obj.Fields { + if err := x.Value.CheckParamScope(freeVars); err != nil { + return err + } + } + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -7312,6 +7358,23 @@ func (obj *ExprFunc) SetScope(scope *interfaces.Scope, sctx map[string]interface return nil } +func (obj *ExprFunc) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + if obj.Body != nil { + newFreeVars := make(map[interfaces.Expr]struct{}) + for k, v := range freeVars { + newFreeVars[k] = v + } + for _, param := range obj.params { + newFreeVars[param] = struct{}{} + } + + if err := obj.Body.CheckParamScope(newFreeVars); err != nil { + return err + } + } + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -8066,6 +8129,22 @@ func (obj *ExprCall) SetScope(scope *interfaces.Scope, sctx map[string]interface return nil } +func (obj *ExprCall) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + if obj.expr != nil { + if err := obj.expr.CheckParamScope(freeVars); err != nil { + return err + } + } + + for _, x := range obj.Args { + if err := x.CheckParamScope(freeVars); err != nil { + return err + } + } + + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -8845,6 +8924,14 @@ func (obj *ExprVar) SetScope(scope *interfaces.Scope, sctx map[string]interfaces return nil } +func (obj *ExprVar) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + target := obj.scope.Variables[obj.Name] + if err := target.CheckParamScope(freeVars); err != nil { + return err + } + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -9057,6 +9144,14 @@ func (obj *ExprParam) SetScope(scope *interfaces.Scope, sctx map[string]interfac return nil } +func (obj *ExprParam) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + if _, exists := freeVars[obj]; !exists { + return fmt.Errorf("the body uses parameter $%s", obj.Name) + } + + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -9189,6 +9284,10 @@ func (obj *ExprPoly) SetScope(scope *interfaces.Scope, sctx map[string]interface panic("ExprPoly.SetScope(): should not happen, ExprVar should replace ExprPoly with a copy of its definition before calling SetScope") } +func (obj *ExprPoly) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + panic("ExprPoly.CheckParamScope(): should not happen, ExprVar should replace ExprPoly with a copy of its definition before calling SetScope") +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -9346,6 +9445,10 @@ func (obj *ExprTopLevel) SetScope(scope *interfaces.Scope, sctx map[string]inter return obj.Definition.SetScope(obj.CapturedScope, make(map[string]interfaces.Expr)) } +func (obj *ExprTopLevel) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + return obj.Definition.CheckParamScope(freeVars) +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -9526,6 +9629,10 @@ func (obj *ExprSingleton) SetScope(scope *interfaces.Scope, sctx map[string]inte return obj.Definition.SetScope(scope, sctx) } +func (obj *ExprSingleton) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + return obj.Definition.CheckParamScope(freeVars) +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't @@ -9813,6 +9920,19 @@ func (obj *ExprIf) SetScope(scope *interfaces.Scope, sctx map[string]interfaces. return obj.Condition.SetScope(scope, sctx) } +func (obj *ExprIf) CheckParamScope(freeVars map[interfaces.Expr]struct{}) error { + if err := obj.Condition.CheckParamScope(freeVars); err != nil { + return err + } + if err := obj.ThenBranch.CheckParamScope(freeVars); err != nil { + return err + } + if err := obj.ElseBranch.CheckParamScope(freeVars); err != nil { + return err + } + return nil +} + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't diff --git a/lang/interfaces/ast.go b/lang/interfaces/ast.go index c4749c8250..dde5f6dde2 100644 --- a/lang/interfaces/ast.go +++ b/lang/interfaces/ast.go @@ -109,6 +109,9 @@ type Expr interface { // SetScope sets the scope here and propagates it downwards. SetScope(*Scope, map[string]Expr) error + // Ensure that only the specified ExprParams are free in this expression. + CheckParamScope(map[Expr]struct{}) error + // SetType sets the type definitively, and errors if it is incompatible. SetType(*types.Type) error From 9a4c1adef431a25ca0fea69678bdb543d7d796bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Wed, 24 Jan 2024 00:06:51 -0500 Subject: [PATCH 22/35] start with the empty environment --- lang/ast/structs.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 49ac7f2745..62bf34a066 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -7787,14 +7787,19 @@ 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) { if obj.Body != nil { - env := make(map[string]interfaces.Func) // XXX ??? SAM will decide what to do here + // We can only return a Value if we know the value of all the ExprParams. + // We don't have an environment, so this is only possible if there are no + // ExprParams at all. + if err := obj.CheckParamScope(make(map[interfaces.Expr]struct{})); err != nil { + // return the sentinel value + return nil, ErrCantSpeculate + } + return &full.FuncValue{ V: func(innerTxn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { - // Extend the environment with the arguments. + // There are no ExprParams, so we start with the empty environment. + // Extend that environment with the arguments. extendedEnv := make(map[string]interfaces.Func) - for k, v := range env { - extendedEnv[k] = v - } for i, arg := range obj.Args { extendedEnv[arg.Name] = args[i] } @@ -7813,7 +7818,6 @@ func (obj *ExprFunc) Value() (types.Value, error) { }, T: obj.typ, }, nil - } else if obj.Function != nil { return structs.FuncToFullFuncValue(obj.function, obj.typ), nil From 42d529b93b043862f2d006ea178e99a641f66a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Wed, 24 Jan 2024 00:08:56 -0500 Subject: [PATCH 23/35] ExprAny.CheckParamScope() --- lang/interfaces/structs.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lang/interfaces/structs.go b/lang/interfaces/structs.go index 6e5875a1ee..d3aa2a1443 100644 --- a/lang/interfaces/structs.go +++ b/lang/interfaces/structs.go @@ -78,6 +78,8 @@ func (obj *ExprAny) Ordering(produces map[string]Node) (*pgraph.Graph, map[Node] // does not need to know about the parent scope. func (obj *ExprAny) SetScope(*Scope, map[string]Expr) error { return nil } +func (obj *ExprAny) CheckParamScope(freeVars map[Expr]struct{}) error { return nil } + // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during // parsing if a type is specified explicitly. Since types are static and don't From 0ad1df244e14ac9175c9cc323d0f2ccc8cef3ee4 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Wed, 24 Jan 2024 20:48:52 -0500 Subject: [PATCH 24/35] XXX fixes so we compile --- lang/ast/structs.go | 20 ++++++++++++++------ lang/interfaces/ast.go | 1 + 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 62bf34a066..314d39ee87 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -32,6 +32,7 @@ import ( engineUtil "github.com/purpleidea/mgmt/engine/util" "github.com/purpleidea/mgmt/lang/funcs" "github.com/purpleidea/mgmt/lang/funcs/core" + "github.com/purpleidea/mgmt/lang/funcs/ref" "github.com/purpleidea/mgmt/lang/funcs/structs" "github.com/purpleidea/mgmt/lang/funcs/txn" "github.com/purpleidea/mgmt/lang/inputs" @@ -8688,12 +8689,17 @@ func (obj *ExprCall) Graph(env map[string]interfaces.Func) (*pgraph.Graph, inter if err == nil { exprFuncValue, ok := exprValue.(*full.FuncValue) if ok { - txn := (&txn.Graph{ - Debug: obj.data.debug, - Logf: func(format string, v ...interface{}) { - obj.data.Logf(format, v...) - }, - }.Init()) + txn := (&txn.GraphTxn{ + GraphAPI: (&txn.Graph{ + Debug: obj.data.Debug, + Logf: func(format string, v ...interface{}) { + obj.data.Logf(format, v...) + }, + }).Init(), + Lock: func() {}, + Unlock: func() {}, + RefCount: (&ref.Count{}).Init(), + }).Init() args := []interfaces.Func{} for i, arg := range obj.Args { // []interfaces.Expr g, f, err := arg.Graph(env) @@ -8799,6 +8805,8 @@ func (obj *ExprCall) Value() (types.Value, error) { args = append(args, a) } + _ = funcValue + panic("IMPLEMENT THIS") // we now have a full.FuncValue and a []Value. We can't call the existing // Call([]Func) Func // method on the FuncValue, we need a speculative diff --git a/lang/interfaces/ast.go b/lang/interfaces/ast.go index dde5f6dde2..3ebb2d0d25 100644 --- a/lang/interfaces/ast.go +++ b/lang/interfaces/ast.go @@ -110,6 +110,7 @@ type Expr interface { SetScope(*Scope, map[string]Expr) error // Ensure that only the specified ExprParams are free in this expression. + // XXX: JAMES: REPLACE WITH WITH A CALL TO APPLY() AFTER EVERYTHING WORKS. CheckParamScope(map[Expr]struct{}) error // SetType sets the type definitively, and errors if it is incompatible. From 2c2df8fbbd83a3346276e0760e208d41ade94e40 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Wed, 24 Jan 2024 21:04:35 -0500 Subject: [PATCH 25/35] XXX: COMPLETELY GUESSING, PROBABLY WRONG --- lang/ast/structs.go | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 314d39ee87..0aa13be55b 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -8796,6 +8796,8 @@ func (obj *ExprCall) Value() (types.Value, error) { return nil, fmt.Errorf("not a func value") } + funcList := []interfaces.Func{} + args := []types.Value{} for _, arg := range obj.Args { // []interfaces.Expr a, err := arg.Value() // speculative @@ -8803,15 +8805,48 @@ func (obj *ExprCall) Value() (types.Value, error) { return nil, err } args = append(args, a) + + // XXX COMPLETELY GUESSING: + _, argFunc, err := arg.Graph(nil) // env XXX ??? + if err != nil { + return nil, err + } + funcList = append(funcList, argFunc) } - _ = funcValue - panic("IMPLEMENT THIS") // we now have a full.FuncValue and a []Value. We can't call the existing // Call([]Func) Func // method on the FuncValue, we need a speculative // Call([]Value) Value // method. so the next step is to implement that method. + // + // XXX: we do have `Call(args []types.Value) (types.Value, error)` + // XXX: which is a `CallableFunc` interface, but that's based off of Func... + // XXX: this is based off of full.FuncValue ... + // XXX: this is just a dimensional analysis attempt, probably wrong: + // XXX COMPLETELY GUESSING: + 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 := ??? // interfaces.Txn + //funcList := ??? // []interfaces.Func + f, err := funcValue.Call(txn, funcList) // (interfaces.Func, error) + if err != nil { + return nil, err + } + callableFunc, ok := f.(interfaces.CallableFunc) + if !ok { + return nil, fmt.Errorf("not a CallableFunc") + } + return callableFunc.Call(args) } // ExprVar is a representation of a variable lookup. It returns the expression From d61a5dfd7cfa3d84d7ea03d1232f9ee68997b76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Sun, 28 Jan 2024 23:44:48 -0500 Subject: [PATCH 26/35] Revert "XXX: COMPLETELY GUESSING, PROBABLY WRONG" This reverts commit 2c2df8fbbd83a3346276e0760e208d41ade94e40. It is indeed wrong. We need to implement a new method to full.FuncValue, not bend over backwards to reuse Func's method. --- lang/ast/structs.go | 39 ++------------------------------------- 1 file changed, 2 insertions(+), 37 deletions(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 0aa13be55b..314d39ee87 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -8796,8 +8796,6 @@ func (obj *ExprCall) Value() (types.Value, error) { return nil, fmt.Errorf("not a func value") } - funcList := []interfaces.Func{} - args := []types.Value{} for _, arg := range obj.Args { // []interfaces.Expr a, err := arg.Value() // speculative @@ -8805,48 +8803,15 @@ func (obj *ExprCall) Value() (types.Value, error) { return nil, err } args = append(args, a) - - // XXX COMPLETELY GUESSING: - _, argFunc, err := arg.Graph(nil) // env XXX ??? - if err != nil { - return nil, err - } - funcList = append(funcList, argFunc) } + _ = funcValue + panic("IMPLEMENT THIS") // we now have a full.FuncValue and a []Value. We can't call the existing // Call([]Func) Func // method on the FuncValue, we need a speculative // Call([]Value) Value // method. so the next step is to implement that method. - // - // XXX: we do have `Call(args []types.Value) (types.Value, error)` - // XXX: which is a `CallableFunc` interface, but that's based off of Func... - // XXX: this is based off of full.FuncValue ... - // XXX: this is just a dimensional analysis attempt, probably wrong: - // XXX COMPLETELY GUESSING: - 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 := ??? // interfaces.Txn - //funcList := ??? // []interfaces.Func - f, err := funcValue.Call(txn, funcList) // (interfaces.Func, error) - if err != nil { - return nil, err - } - callableFunc, ok := f.(interfaces.CallableFunc) - if !ok { - return nil, fmt.Errorf("not a CallableFunc") - } - return callableFunc.Call(args) } // ExprVar is a representation of a variable lookup. It returns the expression From 7ad138260c4408e538077cfb41c676cd96a3fe90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Mon, 29 Jan 2024 00:11:15 -0500 Subject: [PATCH 27/35] remove unused helper function NewFunc --- lang/types/full/full.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/lang/types/full/full.go b/lang/types/full/full.go index beb24ce12f..f562f34e37 100644 --- a/lang/types/full/full.go +++ b/lang/types/full/full.go @@ -45,20 +45,6 @@ type FuncValue struct { T *types.Type // contains ordered field types, arg names are a bonus part } -// NewFunc creates a new function with the specified type. -func NewFunc(t *types.Type) *FuncValue { - if t.Kind != types.KindFunc { - return nil // sanity check - } - v := func(interfaces.Txn, []interfaces.Func) (interfaces.Func, error) { - return nil, fmt.Errorf("nil function") // TODO: is this correct? - } - return &FuncValue{ - V: v, - T: t, - } -} - // String returns a visual representation of this value. func (obj *FuncValue) String() string { return fmt.Sprintf("func(%+v)", obj.T) // TODO: can't print obj.V w/o vet warning From b957901daa0bea99c03649fe57b1ad7aa0da22df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Mon, 29 Jan 2024 00:13:49 -0500 Subject: [PATCH 28/35] remove unused helper FuncValue.Func() --- lang/types/full/full.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lang/types/full/full.go b/lang/types/full/full.go index f562f34e37..3ffdb028d5 100644 --- a/lang/types/full/full.go +++ b/lang/types/full/full.go @@ -112,12 +112,6 @@ func (obj *FuncValue) Value() interface{} { //return val.Interface() } -// Func represents the value of this type as a function if it is one. If this is -// not a function, then this panics. -func (obj *FuncValue) Func() interface{} { - return obj.V -} - // Set sets the function value to be a new function. func (obj *FuncValue) Set(fn func(interfaces.Txn, []interfaces.Func) (interfaces.Func, error)) error { // TODO: change method name? obj.V = fn From 44e3c068b06e4b5a76c229f4ac6c9ca40d63e629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Mon, 29 Jan 2024 00:14:16 -0500 Subject: [PATCH 29/35] remove unused helper FuncValue.Set() --- lang/types/full/full.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lang/types/full/full.go b/lang/types/full/full.go index 3ffdb028d5..66c96277b5 100644 --- a/lang/types/full/full.go +++ b/lang/types/full/full.go @@ -112,12 +112,6 @@ func (obj *FuncValue) Value() interface{} { //return val.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? - obj.V = fn - return nil // TODO: can we do any sort of checking here? -} - // Call calls the function with the provided txn and args. func (obj *FuncValue) Call(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { return obj.V(txn, args) From 87c0967709a63b7e59db9186f182a876eca868c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Mon, 29 Jan 2024 00:43:28 -0500 Subject: [PATCH 30/35] support for Timeless FuncValues --- lang/funcs/structs/util.go | 48 +++++++++++++++++++++++++++----------- lang/types/full/full.go | 11 ++++----- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/lang/funcs/structs/util.go b/lang/funcs/structs/util.go index a9ebd9e1b3..baedae1dcb 100644 --- a/lang/funcs/structs/util.go +++ b/lang/funcs/structs/util.go @@ -51,18 +51,9 @@ func SimpleFnToDirectFunc(name string, fv *types.FuncValue) interfaces.Func { // *full.FuncValue. func SimpleFnToFuncValue(name string, fv *types.FuncValue) *full.FuncValue { return &full.FuncValue{ - V: func(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { - wrappedFunc := SimpleFnToDirectFunc(name, fv) - txn.AddVertex(wrappedFunc) - for i, arg := range args { - argName := fv.T.Ord[i] - txn.AddEdge(arg, wrappedFunc, &interfaces.FuncEdge{ - Args: []string{argName}, - }) - } - return wrappedFunc, nil - }, - T: fv.T, + Name: &name, + Timeless: fv, + T: fv.T, } } @@ -76,9 +67,10 @@ func SimpleFnToConstFunc(name string, fv *types.FuncValue) interfaces.Func { // 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(valueTransformingFunc interfaces.Func, typ *types.Type) *full.FuncValue { +func FuncToFullFuncValue(name string, valueTransformingFunc interfaces.Func, typ *types.Type) *full.FuncValue { return &full.FuncValue{ - V: func(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { + Name: &name, + Timeful: func(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { for i, arg := range args { argName := typ.Ord[i] txn.AddEdge(arg, valueTransformingFunc, &interfaces.FuncEdge{ @@ -90,3 +82,31 @@ func FuncToFullFuncValue(valueTransformingFunc interfaces.Func, typ *types.Type) T: typ, } } + +// Call calls the function with the provided txn and args. +func CallFuncValue(obj *full.FuncValue, txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { + if obj.Timeful != nil { + return obj.Timeful(txn, args) + } + + wrappedFunc := SimpleFnToDirectFunc(*obj.Name, obj.Timeless) + txn.AddVertex(wrappedFunc) + for i, arg := range args { + argName := obj.T.Ord[i] + txn.AddEdge(arg, wrappedFunc, &interfaces.FuncEdge{ + Args: []string{argName}, + }) + } + return wrappedFunc, nil +} + +// Speculatively call the function with the provided arguments. +// Only makes sense if the function is timeless (produces a single Value, not a +// stream of values). +func CallTimelessFuncValue(obj *full.FuncValue, args []types.Value) (types.Value, error) { + if obj.Timeless != nil { + return obj.Timeless.V(args) + } + + panic("cannot call CallIfTimeless on a Timeful function") +} diff --git a/lang/types/full/full.go b/lang/types/full/full.go index 66c96277b5..d28d032d9d 100644 --- a/lang/types/full/full.go +++ b/lang/types/full/full.go @@ -41,8 +41,10 @@ import ( // went horribly wrong. (Think, an internal panic.) type FuncValue struct { types.Base - V func(interfaces.Txn, []interfaces.Func) (interfaces.Func, error) - T *types.Type // contains ordered field types, arg names are a bonus part + Name *string + Timeful func(interfaces.Txn, []interfaces.Func) (interfaces.Func, error) + Timeless *types.FuncValue + T *types.Type // contains ordered field types, arg names are a bonus part } // String returns a visual representation of this value. @@ -111,8 +113,3 @@ func (obj *FuncValue) Value() interface{} { //val := reflect.MakeFunc(typ, fn) //return val.Interface() } - -// Call calls the function with the provided txn and args. -func (obj *FuncValue) Call(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { - return obj.V(txn, args) -} From 2c41e672652856286b9956f811bc93f8ccd997be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Sat, 3 Feb 2024 12:36:17 -0500 Subject: [PATCH 31/35] fix callers --- lang/ast/structs.go | 15 +++++++++------ lang/funcs/core/iter/map_func.go | 2 +- lang/funcs/structs/call.go | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 314d39ee87..73298633b0 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -7687,7 +7687,7 @@ func (obj *ExprFunc) Graph(env map[string]interfaces.Func) (*pgraph.Graph, inter var funcValueFunc interfaces.Func if obj.Body != nil { funcValueFunc = structs.FuncValueToConstFunc(&full.FuncValue{ - V: func(innerTxn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { + Timeful: func(innerTxn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { // Extend the environment with the arguments. extendedEnv := make(map[string]interfaces.Func) for k, v := range env { @@ -7716,8 +7716,10 @@ func (obj *ExprFunc) Graph(env map[string]interfaces.Func) (*pgraph.Graph, inter // an output value, but we need to construct a node which takes no // inputs and produces a FuncValue, so we need to wrap it. + // TODO: if the builtin function is known to be timeless, use Timeless + // instead of Timeful funcValueFunc = structs.FuncValueToConstFunc(&full.FuncValue{ - V: func(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { + Timeful: func(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { // Copy obj.function so that the underlying ExprFunc.function gets // refreshed with a new ExprFunc.Function() call. Otherwise, multiple // calls to this function will share the same Func. @@ -7797,7 +7799,7 @@ func (obj *ExprFunc) Value() (types.Value, error) { } return &full.FuncValue{ - V: func(innerTxn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { + Timeful: func(innerTxn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) { // There are no ExprParams, so we start with the empty environment. // Extend that environment with the arguments. extendedEnv := make(map[string]interfaces.Func) @@ -7820,8 +7822,9 @@ func (obj *ExprFunc) Value() (types.Value, error) { T: obj.typ, }, nil } else if obj.Function != nil { - return structs.FuncToFullFuncValue(obj.function, obj.typ), nil - + // TODO: if the builtin is timeless, use SimpleFnToFuncValue instead of + // FuncToFullFuncValue + return structs.FuncToFullFuncValue(obj.Title, obj.function, obj.typ), nil } // else if /* len(obj.Values) > 0 */ panic("what to do here") @@ -8709,7 +8712,7 @@ func (obj *ExprCall) Graph(env map[string]interfaces.Func) (*pgraph.Graph, inter args = append(args, f) txn.AddGraph(g) } - outputFunc, err := exprFuncValue.Call(txn, args) + outputFunc, err := structs.CallFuncValue(exprFuncValue, txn, args) if err != nil { return nil, nil, errwrap.Wrapf(err, "could not construct the static graph for a function call") } diff --git a/lang/funcs/core/iter/map_func.go b/lang/funcs/core/iter/map_func.go index d733f7b561..6ffaf97149 100644 --- a/lang/funcs/core/iter/map_func.go +++ b/lang/funcs/core/iter/map_func.go @@ -794,7 +794,7 @@ func (obj *MapFunc) replaceSubGraph(subgraphInput interfaces.Func) error { ) obj.init.Txn.AddVertex(inputElemFunc) - outputElemFunc, err := obj.lastFuncValue.Call(obj.init.Txn, []interfaces.Func{inputElemFunc}) + outputElemFunc, err := structs.CallFuncValue(obj.lastFuncValue, obj.init.Txn, []interfaces.Func{inputElemFunc}) if err != nil { return errwrap.Wrapf(err, "could not call obj.lastFuncValue.Call()") } diff --git a/lang/funcs/structs/call.go b/lang/funcs/structs/call.go index a6ebbf4e29..202050d615 100644 --- a/lang/funcs/structs/call.go +++ b/lang/funcs/structs/call.go @@ -211,7 +211,7 @@ func (obj *CallFunc) replaceSubGraph(newFuncValue *full.FuncValue) error { // methods called on it. Nothing else. It will _not_ call Commit or // Reverse. It adds to the graph, and our Commit and Reverse operations // are the ones that actually make the change. - outputFunc, err := newFuncValue.Call(obj.init.Txn, obj.ArgVertices) + outputFunc, err := CallFuncValue(newFuncValue, obj.init.Txn, obj.ArgVertices) if err != nil { return errwrap.Wrapf(err, "could not call newFuncValue.Call()") } From 8caf0a7aac60800b6fdcf4353ea4e4c4ed31ba44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Sat, 3 Feb 2024 20:13:10 -0500 Subject: [PATCH 32/35] missing txn.Commit() --- lang/ast/structs.go | 1 + 1 file changed, 1 insertion(+) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 73298633b0..fdf1b05ec9 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -8717,6 +8717,7 @@ func (obj *ExprCall) Graph(env map[string]interfaces.Func) (*pgraph.Graph, inter return nil, nil, errwrap.Wrapf(err, "could not construct the static graph for a function call") } + txn.Commit() return txn.Graph(), outputFunc, nil } } From e25c9a6c8a08914f894a894f1b501d39481612d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Sat, 3 Feb 2024 22:52:46 -0500 Subject: [PATCH 33/35] ExprCall.Value() --- lang/ast/structs.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index fdf1b05ec9..bea987a924 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -8809,13 +8809,7 @@ func (obj *ExprCall) Value() (types.Value, error) { args = append(args, a) } - _ = funcValue - panic("IMPLEMENT THIS") - // we now have a full.FuncValue and a []Value. We can't call the existing - // Call([]Func) Func - // method on the FuncValue, we need a speculative - // Call([]Value) Value - // method. so the next step is to implement that method. + return structs.CallTimelessFuncValue(funcValue, args) // speculative } // ExprVar is a representation of a variable lookup. It returns the expression From 2852f2e15960c7097c9d2110a6e24491805d9590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Sat, 3 Feb 2024 22:55:13 -0500 Subject: [PATCH 34/35] a regular error, not a panic --- lang/funcs/structs/util.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lang/funcs/structs/util.go b/lang/funcs/structs/util.go index baedae1dcb..0288a9a21d 100644 --- a/lang/funcs/structs/util.go +++ b/lang/funcs/structs/util.go @@ -18,6 +18,8 @@ package structs import ( + "fmt" + "github.com/purpleidea/mgmt/lang/funcs/simple" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" @@ -108,5 +110,5 @@ func CallTimelessFuncValue(obj *full.FuncValue, args []types.Value) (types.Value return obj.Timeless.V(args) } - panic("cannot call CallIfTimeless on a Timeful function") + return nil, fmt.Errorf("cannot call CallIfTimeless on a Timeful function") } From c56211f2b9d6d1fdea8b1cd6a7c611c17c27d2a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Mon, 5 Feb 2024 23:12:07 -0500 Subject: [PATCH 35/35] copy obj.function to avoid sharing --- lang/ast/structs.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index bea987a924..dcd65e99fa 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -7824,7 +7824,21 @@ func (obj *ExprFunc) Value() (types.Value, error) { } else if obj.Function != nil { // TODO: if the builtin is timeless, use SimpleFnToFuncValue instead of // FuncToFullFuncValue - return structs.FuncToFullFuncValue(obj.Title, obj.function, obj.typ), nil + + // Copy obj.function so that the underlying ExprFunc.function gets + // refreshed with a new ExprFunc.Function() call. Otherwise, multiple + // calls to this function will share the same Func. + exprCopy, err := obj.Copy() + if err != nil { + return nil, errwrap.Wrapf(err, "could not copy expression") + } + funcExprCopy, ok := exprCopy.(*ExprFunc) + if !ok { + // programming error + return nil, errwrap.Wrapf(err, "ExprFunc.Copy() does not produce an ExprFunc") + } + valueTransformingFunc := funcExprCopy.function + return structs.FuncToFullFuncValue(obj.Title, valueTransformingFunc, obj.typ), nil } // else if /* len(obj.Values) > 0 */ panic("what to do here")