Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

optimize the graph shape to not create sub-graphs when possible #724

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6bb5930
lang: interfaces Add CallableFunc interface
purpleidea Oct 3, 2023
f4df5a6
lang: funcs: structs: Add CallableFunc implementation for if func
purpleidea Dec 6, 2023
bf06110
lang: funcs: structs: Add CallableFunc implementation for const func
purpleidea Dec 6, 2023
321e89f
lang: funcs: Add CallableFunc implementation for simple and simplepoly
purpleidea Dec 6, 2023
a264977
lang: funcs: facts: Add CallableFunc implementation for fact
purpleidea Dec 6, 2023
d6d3bd5
pseudo code for the static graph optimization
gelisam Jan 20, 2024
debbf55
an example which the pseudo-code does _not_ support yet
gelisam Jan 20, 2024
eab4f87
pseudo code for ExprFunc.Value()
gelisam Jan 20, 2024
9506b02
XXX: WIP
purpleidea Jan 20, 2024
2df932a
XXX: WIP ExprFunc.Value
purpleidea Jan 20, 2024
aa43cbf
pseudo code for ExprCall.Value()
gelisam Jan 20, 2024
723d2f8
XXX: messy pseudo-code to fix-up more
purpleidea Jan 22, 2024
27f155f
XXX: lang: ast: Add Value speculation sentinel error
purpleidea Jan 22, 2024
8138b5c
fill in comment
gelisam Jan 24, 2024
b9b9796
remove outdated comment
gelisam Jan 24, 2024
29341a0
answer to XXX comment in commit description
gelisam Jan 24, 2024
94455ee
explain what to do in the obj.Values case
gelisam Jan 24, 2024
d4270af
yes, I did mean arg.Graph()
gelisam Jan 24, 2024
d29983e
better error message
gelisam Jan 24, 2024
899b5d5
reword comment in the hope it is clear this time
gelisam Jan 24, 2024
eba4f9d
CheckParamScope
gelisam Jan 24, 2024
9a4c1ad
start with the empty environment
gelisam Jan 24, 2024
42d529b
ExprAny.CheckParamScope()
gelisam Jan 24, 2024
0ad1df2
XXX fixes so we compile
purpleidea Jan 25, 2024
2c2df8f
XXX: COMPLETELY GUESSING, PROBABLY WRONG
purpleidea Jan 25, 2024
d61a5df
Revert "XXX: COMPLETELY GUESSING, PROBABLY WRONG"
gelisam Jan 29, 2024
7ad1382
remove unused helper function NewFunc
gelisam Jan 29, 2024
b957901
remove unused helper FuncValue.Func()
gelisam Jan 29, 2024
44e3c06
remove unused helper FuncValue.Set()
gelisam Jan 29, 2024
87c0967
support for Timeless FuncValues
gelisam Jan 29, 2024
2c41e67
fix callers
gelisam Feb 3, 2024
8caf0a7
missing txn.Commit()
gelisam Feb 4, 2024
e25c9a6
ExprCall.Value()
gelisam Feb 4, 2024
2852f2e
a regular error, not a panic
gelisam Feb 4, 2024
c56211f
copy obj.function to avoid sharing
gelisam Feb 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 263 additions & 15 deletions lang/ast/structs.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lang/funcs/core/iter/map_func.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()")
}
Expand Down
13 changes: 13 additions & 0 deletions lang/funcs/facts/facts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
12 changes: 12 additions & 0 deletions lang/funcs/facts/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
21 changes: 17 additions & 4 deletions lang/funcs/simple/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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
}
30 changes: 17 additions & 13 deletions lang/funcs/simplepoly/simplepoly.go
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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
}
2 changes: 1 addition & 1 deletion lang/funcs/structs/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()")
}
Expand Down
16 changes: 16 additions & 0 deletions lang/funcs/structs/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
32 changes: 26 additions & 6 deletions lang/funcs/structs/if.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Expand All @@ -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
}
64 changes: 52 additions & 12 deletions lang/funcs/structs/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -51,18 +53,9 @@ func SimpleFnToDirectFunc(name string, fv *types.FuncValue) interfaces.Func {
// *full.FuncValue.
func SimpleFnToFuncValue(name string, fv *types.FuncValue) *full.FuncValue {
return &full.FuncValue{
V: func(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) {
wrappedFunc := SimpleFnToDirectFunc(name, fv)
txn.AddVertex(wrappedFunc)
for i, arg := range args {
argName := fv.T.Ord[i]
txn.AddEdge(arg, wrappedFunc, &interfaces.FuncEdge{
Args: []string{argName},
})
}
return wrappedFunc, nil
},
T: fv.T,
Name: &name,
Timeless: fv,
T: fv.T,
}
}

Expand All @@ -72,3 +65,50 @@ func SimpleFnToFuncValue(name string, fv *types.FuncValue) *full.FuncValue {
func SimpleFnToConstFunc(name string, fv *types.FuncValue) interfaces.Func {
return FuncValueToConstFunc(SimpleFnToFuncValue(name, fv))
}

// FuncToFullFuncValue creates a *full.FuncValue which adds the given
// interfaces.Func to the graph. Note that this means the *full.FuncValue
// can only be called once.
func FuncToFullFuncValue(name string, valueTransformingFunc interfaces.Func, typ *types.Type) *full.FuncValue {
return &full.FuncValue{
Name: &name,
Timeful: func(txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) {
for i, arg := range args {
argName := typ.Ord[i]
txn.AddEdge(arg, valueTransformingFunc, &interfaces.FuncEdge{
Args: []string{argName},
})
}
return valueTransformingFunc, nil
},
T: typ,
}
}

// Call calls the function with the provided txn and args.
func CallFuncValue(obj *full.FuncValue, txn interfaces.Txn, args []interfaces.Func) (interfaces.Func, error) {
if obj.Timeful != nil {
return obj.Timeful(txn, args)
}

wrappedFunc := SimpleFnToDirectFunc(*obj.Name, obj.Timeless)
txn.AddVertex(wrappedFunc)
for i, arg := range args {
argName := obj.T.Ord[i]
txn.AddEdge(arg, wrappedFunc, &interfaces.FuncEdge{
Args: []string{argName},
})
}
return wrappedFunc, nil
}

// Speculatively call the function with the provided arguments.
// Only makes sense if the function is timeless (produces a single Value, not a
// stream of values).
func CallTimelessFuncValue(obj *full.FuncValue, args []types.Value) (types.Value, error) {
if obj.Timeless != nil {
return obj.Timeless.V(args)
}

return nil, fmt.Errorf("cannot call CallIfTimeless on a Timeful function")
}
4 changes: 4 additions & 0 deletions lang/interfaces/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ type Expr interface {
// SetScope sets the scope here and propagates it downwards.
SetScope(*Scope, map[string]Expr) error

// Ensure that only the specified ExprParams are free in this expression.
// XXX: JAMES: REPLACE WITH WITH A CALL TO APPLY() AFTER EVERYTHING WORKS.
CheckParamScope(map[Expr]struct{}) error

// SetType sets the type definitively, and errors if it is incompatible.
SetType(*types.Type) error

Expand Down
43 changes: 43 additions & 0 deletions lang/interfaces/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
2 changes: 2 additions & 0 deletions lang/interfaces/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
func apply($f, $x) {
$f($x)
}
$add1 = fn($x) {
$x + 1
}
$z = apply($add, 1)
Loading
Loading