lang: ast, interfaces, interpret: Change the Output sig

This plumbs through the new Output method signature that accepts a table
of function pointers to values and relies on the previous storing of the
function pointers to be used for the lookup right now. This has the
elegant side-effect that Output generation could run in parallel with
the graph engine, as the engine only needs to pause to take a snapshot
of the current values tables.

Co-authored-by: Samuel Gélineau <gelisam@gmail.com>
This commit is contained in:
James Shubin
2023-08-11 15:56:10 -04:00
parent 9175d26b3b
commit 31c7144fff
3 changed files with 102 additions and 59 deletions

View File

@@ -103,6 +103,15 @@ 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")
// ErrFuncPointerNil is an error that explains the function pointer for
// table lookup is missing. If this happens, it's most likely a
// programming error.
ErrFuncPointerNil = interfaces.Error("missing func pointer for table")
// ErrTableNoValue is an error that explains the table is missing a
// value. If this happens, it's most likely a programming error.
ErrTableNoValue = interfaces.Error("missing value in table")
)
var (
@@ -261,7 +270,7 @@ func (obj *StmtBind) Graph() (*pgraph.Graph, error) {
// Output for the bind statement produces no output. Any values of interest come
// from the use of the var which this binds the expression to.
func (obj *StmtBind) Output() (*interfaces.Output, error) {
func (obj *StmtBind) Output(map[interfaces.Func]types.Value) (*interfaces.Output, error) {
return interfaces.EmptyOutput(), nil
}
@@ -616,10 +625,13 @@ func (obj *StmtRes) Graph() (*pgraph.Graph, error) {
// analogous function for expressions is Value. Those Value functions might get
// called by this Output function if they are needed to produce the output. In
// the case of this resource statement, this is definitely the case.
func (obj *StmtRes) Output() (*interfaces.Output, error) {
nameValue, err := obj.Name.Value()
if err != nil {
return nil, err
func (obj *StmtRes) Output(table map[interfaces.Func]types.Value) (*interfaces.Output, error) {
if obj.namePtr == nil {
return nil, ErrFuncPointerNil
}
nameValue, exists := table[obj.namePtr]
if !exists {
return nil, ErrTableNoValue
}
names := []string{} // list of names to build
@@ -642,18 +654,18 @@ func (obj *StmtRes) Output() (*interfaces.Output, error) {
resources := []engine.Res{}
edges := []*interfaces.Edge{}
for _, name := range names {
res, err := obj.resource(name)
res, err := obj.resource(table, name)
if err != nil {
return nil, errwrap.Wrapf(err, "error building resource")
}
edgeList, err := obj.edges(name)
edgeList, err := obj.edges(table, name)
if err != nil {
return nil, errwrap.Wrapf(err, "error building edges")
}
edges = append(edges, edgeList...)
if err := obj.metaparams(res); err != nil { // set metaparams
if err := obj.metaparams(table, res); err != nil { // set metaparams
return nil, errwrap.Wrapf(err, "error building meta params")
}
resources = append(resources, res)
@@ -667,7 +679,7 @@ func (obj *StmtRes) Output() (*interfaces.Output, error) {
// resource is a helper function to generate the res that comes from this.
// TODO: it could memoize some of the work to avoid re-computation when looped
func (obj *StmtRes) resource(resName string) (engine.Res, error) {
func (obj *StmtRes) resource(table map[interfaces.Func]types.Value, resName string) (engine.Res, error) {
res, err := engine.NewNamedResource(obj.Kind, resName)
if err != nil {
return nil, errwrap.Wrapf(err, "cannot create resource kind `%s` with named `%s`", obj.Kind, resName)
@@ -692,9 +704,12 @@ func (obj *StmtRes) resource(resName string) (engine.Res, error) {
}
if x.Condition != nil {
b, err := x.Condition.Value()
if err != nil {
return nil, err
if x.conditionPtr == nil {
return nil, ErrFuncPointerNil
}
b, exists := table[x.conditionPtr]
if !exists {
return nil, ErrTableNoValue
}
if !b.Bool() { // if value exists, and is false, skip it
@@ -731,9 +746,12 @@ func (obj *StmtRes) resource(resName string) (engine.Res, error) {
return nil, errwrap.Wrapf(err, "resource field `%s` of type `%+v`, cannot take type `%+v", x.Field, t, typ)
}
fv, err := x.Value.Value() // Value method on Expr
if err != nil {
return nil, err
if x.valuePtr == nil {
return nil, ErrFuncPointerNil
}
fv, exists := table[x.valuePtr]
if !exists {
return nil, ErrTableNoValue
}
// mutate the struct field f with the mcl data in fv
@@ -746,7 +764,7 @@ func (obj *StmtRes) resource(resName string) (engine.Res, error) {
}
// edges is a helper function to generate the edges that come from the resource.
func (obj *StmtRes) edges(resName string) ([]*interfaces.Edge, error) {
func (obj *StmtRes) edges(table map[interfaces.Func]types.Value, resName string) ([]*interfaces.Edge, error) {
edges := []*interfaces.Edge{}
// to and from self, map of kind, name, notify
@@ -760,9 +778,12 @@ func (obj *StmtRes) edges(resName string) ([]*interfaces.Edge, error) {
}
if x.Condition != nil {
b, err := x.Condition.Value()
if err != nil {
return nil, err
if x.conditionPtr == nil {
return nil, ErrFuncPointerNil
}
b, exists := table[x.conditionPtr]
if !exists {
return nil, ErrTableNoValue
}
if !b.Bool() { // if value exists, and is false, skip it
@@ -770,9 +791,12 @@ func (obj *StmtRes) edges(resName string) ([]*interfaces.Edge, error) {
}
}
nameValue, err := x.EdgeHalf.Name.Value()
if err != nil {
return nil, err
if x.EdgeHalf.namePtr == nil {
return nil, ErrFuncPointerNil
}
nameValue, exists := table[x.EdgeHalf.namePtr]
if !exists {
return nil, ErrTableNoValue
}
// the edge name can be a single string or a list of strings...
@@ -874,7 +898,7 @@ func (obj *StmtRes) edges(resName string) ([]*interfaces.Edge, error) {
// metaparams is a helper function to set the metaparams that come from the
// resource on to the individual resource we're working on.
func (obj *StmtRes) metaparams(res engine.Res) error {
func (obj *StmtRes) metaparams(table map[interfaces.Func]types.Value, res engine.Res) error {
meta := engine.DefaultMetaParams.Copy() // defaults
var rm *engine.ReversibleMeta
@@ -897,9 +921,12 @@ func (obj *StmtRes) metaparams(res engine.Res) error {
}
if x.Condition != nil {
b, err := x.Condition.Value()
if err != nil {
return err
if x.conditionPtr == nil {
return ErrFuncPointerNil
}
b, exists := table[x.conditionPtr]
if !exists {
return ErrTableNoValue
}
if !b.Bool() { // if value exists, and is false, skip it
@@ -907,9 +934,12 @@ func (obj *StmtRes) metaparams(res engine.Res) error {
}
}
v, err := x.MetaExpr.Value()
if err != nil {
return err
if x.metaExprPtr == nil {
return ErrFuncPointerNil
}
v, exists := table[x.metaExprPtr]
if !exists {
return ErrTableNoValue
}
switch p := strings.ToLower(x.Property); p {
@@ -2168,13 +2198,17 @@ func (obj *StmtEdge) Graph() (*pgraph.Graph, error) {
// called by this Output function if they are needed to produce the output. In
// the case of this edge statement, this is definitely the case. This edge stmt
// returns output consisting of edges.
func (obj *StmtEdge) Output() (*interfaces.Output, error) {
func (obj *StmtEdge) Output(table map[interfaces.Func]types.Value) (*interfaces.Output, error) {
edges := []*interfaces.Edge{}
// EdgeHalfList goes in a chain, so we increment like i++ and not i+=2.
for i := 0; i < len(obj.EdgeHalfList)-1; i++ {
nameValue1, err := obj.EdgeHalfList[i].Name.Value()
if err != nil {
return nil, err
if obj.EdgeHalfList[i].namePtr == nil {
return nil, ErrFuncPointerNil
}
nameValue1, exists := table[obj.EdgeHalfList[i].namePtr]
if !exists {
return nil, ErrTableNoValue
}
// the edge name can be a single string or a list of strings...
@@ -2196,9 +2230,12 @@ func (obj *StmtEdge) Output() (*interfaces.Output, error) {
return nil, fmt.Errorf("unhandled resource name type: %+v", nameValue1.Type())
}
nameValue2, err := obj.EdgeHalfList[i+1].Name.Value()
if err != nil {
return nil, err
if obj.EdgeHalfList[i+1].namePtr == nil {
return nil, ErrFuncPointerNil
}
nameValue2, exists := table[obj.EdgeHalfList[i+1].namePtr]
if !exists {
return nil, ErrTableNoValue
}
names2 := []string{} // list of names to build
@@ -2714,20 +2751,24 @@ func (obj *StmtIf) Graph() (*pgraph.Graph, error) {
// is used to build the output graph. This only exists for statements. The
// analogous function for expressions is Value. Those Value functions might get
// called by this Output function if they are needed to produce the output.
func (obj *StmtIf) Output() (*interfaces.Output, error) {
b, err := obj.Condition.Value()
if err != nil {
return nil, err
func (obj *StmtIf) Output(table map[interfaces.Func]types.Value) (*interfaces.Output, error) {
if obj.conditionPtr == nil {
return nil, ErrFuncPointerNil
}
b, exists := table[obj.conditionPtr]
if !exists {
return nil, ErrTableNoValue
}
var output *interfaces.Output
var err error
if b.Bool() { // must not panic!
if obj.ThenBranch != nil { // logically then branch is optional
output, err = obj.ThenBranch.Output()
output, err = obj.ThenBranch.Output(table)
}
} else {
if obj.ElseBranch != nil { // else branch is optional
output, err = obj.ElseBranch.Output()
output, err = obj.ElseBranch.Output(table)
}
}
if err != nil {
@@ -3793,7 +3834,7 @@ func (obj *StmtProg) Graph() (*pgraph.Graph, error) {
// is used to build the output graph. This only exists for statements. The
// analogous function for expressions is Value. Those Value functions might get
// called by this Output function if they are needed to produce the output.
func (obj *StmtProg) Output() (*interfaces.Output, error) {
func (obj *StmtProg) Output(table map[interfaces.Func]types.Value) (*interfaces.Output, error) {
resources := []engine.Res{}
edges := []*interfaces.Edge{}
@@ -3813,7 +3854,7 @@ func (obj *StmtProg) Output() (*interfaces.Output, error) {
continue
}
output, err := stmt.Output()
output, err := stmt.Output(table)
if err != nil {
return nil, err
}
@@ -4009,7 +4050,7 @@ func (obj *StmtFunc) Graph() (*pgraph.Graph, error) {
// Output for the func statement produces no output. Any values of interest come
// from the use of the func which this binds the function to.
func (obj *StmtFunc) Output() (*interfaces.Output, error) {
func (obj *StmtFunc) Output(map[interfaces.Func]types.Value) (*interfaces.Output, error) {
return interfaces.EmptyOutput(), nil
}
@@ -4181,8 +4222,8 @@ func (obj *StmtClass) Graph() (*pgraph.Graph, error) {
// come from the use of the include which this binds the statements to. This is
// usually called from the parent in StmtProg, but it skips running it so that
// it can be called from the StmtInclude Output method.
func (obj *StmtClass) Output() (*interfaces.Output, error) {
return obj.Body.Output()
func (obj *StmtClass) Output(table map[interfaces.Func]types.Value) (*interfaces.Output, error) {
return obj.Body.Output(table)
}
// StmtInclude causes a user defined class to get used. It's effectively the way
@@ -4526,8 +4567,8 @@ func (obj *StmtInclude) Graph() (*pgraph.Graph, error) {
// called by this Output function if they are needed to produce the output. The
// ultimate source of this output comes from the previously defined StmtClass
// which should be found in our scope.
func (obj *StmtInclude) Output() (*interfaces.Output, error) {
return obj.class.Output()
func (obj *StmtInclude) Output(table map[interfaces.Func]types.Value) (*interfaces.Output, error) {
return obj.class.Output(table)
}
// StmtImport adds the exported scope definitions of a module into the current
@@ -4616,7 +4657,7 @@ func (obj *StmtImport) Graph() (*pgraph.Graph, error) {
// called by this Output function if they are needed to produce the output. This
// import statement itself produces no output, as it is only used to populate
// the scope so that others can use that to produce values and output.
func (obj *StmtImport) Output() (*interfaces.Output, error) {
func (obj *StmtImport) Output(map[interfaces.Func]types.Value) (*interfaces.Output, error) {
return interfaces.EmptyOutput(), nil
}
@@ -4701,7 +4742,7 @@ func (obj *StmtComment) Graph() (*pgraph.Graph, error) {
}
// Output for the comment statement produces no output.
func (obj *StmtComment) Output() (*interfaces.Output, error) {
func (obj *StmtComment) Output(map[interfaces.Func]types.Value) (*interfaces.Output, error) {
return interfaces.EmptyOutput(), nil
}

View File

@@ -76,8 +76,9 @@ type Stmt interface {
Graph() (*pgraph.Graph, error)
// Output returns the output that this "program" produces. This output
// is what is used to build the output graph.
Output() (*Output, error)
// is what is used to build the output graph. It requires the input
// table of values that are used to populate each function.
Output(map[Func]types.Value) (*Output, error)
}
// Expr represents an expression in the language. Expr implementations must have

View File

@@ -25,16 +25,17 @@ import (
"github.com/purpleidea/mgmt/engine"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/util/errwrap"
)
// Interpret runs the program and causes a graph generation as a side effect.
// You should not run this on the AST if you haven't previously run the function
// graph engine so that output values have been produced! Type unification is
// another important aspect which needs to have been completed.
func Interpret(ast interfaces.Stmt) (*pgraph.Graph, error) {
output, err := ast.Output() // contains resList, edgeList, etc...
// Interpret runs the program and outputs a generated resource graph. It
// requires an AST, and the table of values required to populate that AST. Type
// unification, and earlier steps should obviously be run first so that you can
// actually get a useful resource graph out of this instead of an error!
func Interpret(ast interfaces.Stmt, table map[interfaces.Func]types.Value) (*pgraph.Graph, error) {
output, err := ast.Output(table) // contains resList, edgeList, etc...
if err != nil {
return nil, err
}