lang: Add internal, resource specific edges

This adds the ability to specify internal, resource specific edges, with
and without notifications. We use the special words: "Notify", "Before",
"Listen", and "Depend". They must have the first character capitalized.
They also support the "elvis" operator.
This commit is contained in:
James Shubin
2018-02-27 21:18:17 -05:00
parent 8e01b6db48
commit 3ad7097c8a
7 changed files with 649 additions and 175 deletions

View File

@@ -163,6 +163,45 @@ it evaluates to `true`, then the parameter will be used. If no `elvis` operator
is specified, then the parameter value will also be used. If the parameter is is specified, then the parameter value will also be used. If the parameter is
not specified, then it will obviously not be used. not specified, then it will obviously not be used.
Resources may also declare edges internally. The edges may point to or from
another resource, and may optionally include a notification. The four properties
are: `Before`, `Depend`, `Notify` and `Listen`. The first two represent normal
edge dependencies, and the second two are normal edge dependencies that also
send notifications. You may have multiples of these per resource, including
multiple `Depend` lines if necessary. Each of these properties also supports the
conditional inclusion `elvis` operator as well.
For example, you may write is:
```mcl
$b = true # for example purposes
if $b {
pkg "drbd" {
state => "installed",
# multiple properties may be used in the same resource
Before => File["/etc/drbd.conf"],
Before => Svc["drbd"],
}
}
file "/etc/drbd.conf" {
content => "some config",
Depend => $b ?: Pkg["drbd"],
Notify => Svc["drbd"],
}
svc "drbd" {
state => "running",
}
```
There are two unique properties about these edges that is different from what
you might expect from other automation software:
1. The ability to specify multiples of these properties allows you to avoid
having to manage arrays and conditional trees of these different dependencies.
2. The keywords all have the same length, which means your code lines up nicely.
#### Edge #### Edge
Edges express dependencies in the graph of resources which are output. They can Edges express dependencies in the graph of resources which are output. They can

18
examples/lang/edges1.mcl Normal file
View File

@@ -0,0 +1,18 @@
$b = true
if $b {
exec "exec0" {
cmd => "sleep 10s",
shell => "/bin/bash",
}
}
exec "exec1" {
cmd => "sleep 10s",
shell => "/bin/bash",
Depend => $b ?: Exec["exec0"],
Before => Exec["exec2"],
}
exec "exec2" {
cmd => "sleep 10s",
shell => "/bin/bash",
}

View File

@@ -59,8 +59,8 @@ func TestInterpolate0(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t1", V: "t1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "stringptr", Field: "stringptr",
Value: &ExprStr{ Value: &ExprStr{
V: "foo", V: "foo",
@@ -103,8 +103,8 @@ func TestInterpolate0(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t1", V: "t1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "stringptr", Field: "stringptr",
Value: fieldName, Value: fieldName,
}, },
@@ -190,8 +190,8 @@ func TestInterpolateBasicStmt(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t1", V: "t1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "stringptr", Field: "stringptr",
Value: &ExprStr{ Value: &ExprStr{
V: "foo", V: "foo",
@@ -208,8 +208,8 @@ func TestInterpolateBasicStmt(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t1", V: "t1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "stringptr", Field: "stringptr",
Value: &ExprStr{ Value: &ExprStr{
V: "foo", V: "foo",
@@ -234,8 +234,8 @@ func TestInterpolateBasicStmt(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t${blah}", V: "t${blah}",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "stringptr", Field: "stringptr",
Value: &ExprStr{ Value: &ExprStr{
V: "foo", V: "foo",
@@ -264,8 +264,8 @@ func TestInterpolateBasicStmt(t *testing.T) {
&StmtRes{ &StmtRes{
Kind: "test", Kind: "test",
Name: resName, Name: resName,
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "stringptr", Field: "stringptr",
Value: &ExprStr{ Value: &ExprStr{
V: "foo", V: "foo",
@@ -290,8 +290,8 @@ func TestInterpolateBasicStmt(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t${42}", // incorrect type V: "t${42}", // incorrect type
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "stringptr", Field: "stringptr",
Value: &ExprStr{ Value: &ExprStr{
V: "foo", V: "foo",
@@ -321,8 +321,8 @@ func TestInterpolateBasicStmt(t *testing.T) {
&StmtRes{ &StmtRes{
Kind: "test", Kind: "test",
Name: resName, Name: resName,
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "stringptr", Field: "stringptr",
Value: &ExprStr{ Value: &ExprStr{
V: "foo", V: "foo",

View File

@@ -320,8 +320,8 @@ func TestLexParse0(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t1", V: "t1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "int64ptr", Field: "int64ptr",
Value: &ExprCall{ Value: &ExprCall{
Name: operatorFuncName, Name: operatorFuncName,
@@ -361,8 +361,8 @@ func TestLexParse0(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t1", V: "t1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "float32", Field: "float32",
Value: &ExprCall{ Value: &ExprCall{
Name: operatorFuncName, Name: operatorFuncName,
@@ -413,8 +413,8 @@ func TestLexParse0(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t1", V: "t1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "int64ptr", Field: "int64ptr",
Value: &ExprCall{ Value: &ExprCall{
Name: operatorFuncName, Name: operatorFuncName,
@@ -465,8 +465,8 @@ func TestLexParse0(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t1", V: "t1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "int64ptr", Field: "int64ptr",
Value: &ExprCall{ Value: &ExprCall{
Name: operatorFuncName, Name: operatorFuncName,
@@ -517,8 +517,8 @@ func TestLexParse0(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t1", V: "t1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "int64ptr", Field: "int64ptr",
Value: &ExprCall{ Value: &ExprCall{
Name: operatorFuncName, Name: operatorFuncName,
@@ -569,8 +569,8 @@ func TestLexParse0(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t1", V: "t1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "boolptr", Field: "boolptr",
Value: &ExprCall{ Value: &ExprCall{
Name: operatorFuncName, Name: operatorFuncName,
@@ -621,8 +621,8 @@ func TestLexParse0(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t1", V: "t1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "boolptr", Field: "boolptr",
Value: &ExprCall{ Value: &ExprCall{
Name: operatorFuncName, Name: operatorFuncName,
@@ -673,8 +673,8 @@ func TestLexParse0(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t1", V: "t1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "boolptr", Field: "boolptr",
Value: &ExprCall{ Value: &ExprCall{
Name: operatorFuncName, Name: operatorFuncName,
@@ -722,8 +722,8 @@ func TestLexParse0(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t1", V: "t1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "boolptr", Field: "boolptr",
Value: &ExprCall{ Value: &ExprCall{
Name: operatorFuncName, Name: operatorFuncName,
@@ -774,8 +774,8 @@ func TestLexParse0(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t1", V: "t1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "int64ptr", Field: "int64ptr",
Value: &ExprInt{ Value: &ExprInt{
V: 42, V: 42,
@@ -788,8 +788,8 @@ func TestLexParse0(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "t2", V: "t2",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "int64ptr", Field: "int64ptr",
Value: &ExprInt{ Value: &ExprInt{
V: 13, V: 13,

View File

@@ -63,8 +63,9 @@ func init() {
structFields []*ExprStructField structFields []*ExprStructField
structField *ExprStructField structField *ExprStructField
resFields []*StmtResField resContents []StmtResContents // interface
resField *StmtResField resField *StmtResField
resEdge *StmtResEdge
edgeHalfList []*StmtEdgeHalf edgeHalfList []*StmtEdgeHalf
edgeHalf *StmtEdgeHalf edgeHalf *StmtEdgeHalf
@@ -667,7 +668,7 @@ resource:
$$.stmt = &StmtRes{ $$.stmt = &StmtRes{
Kind: $1.str, Kind: $1.str,
Name: $2.expr, Name: $2.expr,
Fields: $4.resFields, Contents: $4.resContents,
} }
} }
; ;
@@ -675,17 +676,27 @@ resource_body:
/* end of list */ /* end of list */
{ {
posLast(yylex, yyDollar) // our pos posLast(yylex, yyDollar) // our pos
$$.resFields = []*StmtResField{} $$.resContents = []StmtResContents{}
} }
| resource_body resource_field | resource_body resource_field
{ {
posLast(yylex, yyDollar) // our pos posLast(yylex, yyDollar) // our pos
$$.resFields = append($1.resFields, $2.resField) $$.resContents = append($1.resContents, $2.resField)
} }
| resource_body conditional_resource_field | resource_body conditional_resource_field
{ {
posLast(yylex, yyDollar) // our pos posLast(yylex, yyDollar) // our pos
$$.resFields = append($1.resFields, $2.resField) $$.resContents = append($1.resContents, $2.resField)
}
| resource_body resource_edge
{
posLast(yylex, yyDollar) // our pos
$$.resContents = append($1.resContents, $2.resEdge)
}
| resource_body conditional_resource_edge
{
posLast(yylex, yyDollar) // our pos
$$.resContents = append($1.resContents, $2.resEdge)
} }
; ;
resource_field: resource_field:
@@ -710,6 +721,29 @@ conditional_resource_field:
} }
} }
; ;
resource_edge:
// Before => Test["t1"],
CAPITALIZED_IDENTIFIER ROCKET edge_half COMMA
{
posLast(yylex, yyDollar) // our pos
$$.resEdge = &StmtResEdge{
Property: $1.str,
EdgeHalf: $3.edgeHalf,
}
}
;
conditional_resource_edge:
// Before => $present ?: Test["t1"],
CAPITALIZED_IDENTIFIER ROCKET expr ELVIS edge_half COMMA
{
posLast(yylex, yyDollar) // our pos
$$.resEdge = &StmtResEdge{
Property: $1.str,
EdgeHalf: $5.edgeHalf,
Condition: $3.expr,
}
}
;
edge: edge:
// TODO: we could technically prevent single edge_half pieces from being // TODO: we could technically prevent single edge_half pieces from being
// parsed, but it's probably more work than is necessary... // parsed, but it's probably more work than is necessary...

View File

@@ -35,6 +35,24 @@ import (
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
) )
const (
// EdgeNotify declares an edge a -> b, such that a notification occurs.
// This is most similar to "notify" in Puppet.
EdgeNotify = "notify"
// EdgeBefore declares an edge a -> b, such that no notification occurs.
// This is most similar to "before" in Puppet.
EdgeBefore = "before"
// EdgeListen declares an edge a <- b, such that a notification occurs.
// This is most similar to "subscribe" in Puppet.
EdgeListen = "listen"
// EdgeDepend declares an edge a <- b, such that no notification occurs.
// This is most similar to "require" in Puppet.
EdgeDepend = "depend"
)
// StmtBind is a representation of an assignment, which binds a variable to an // StmtBind is a representation of an assignment, which binds a variable to an
// expression. // expression.
type StmtBind struct { type StmtBind struct {
@@ -96,48 +114,38 @@ func (obj *StmtBind) Output() (*interfaces.Output, error) {
return (&interfaces.Output{}).Empty(), nil return (&interfaces.Output{}).Empty(), nil
} }
// StmtRes is a representation of a resource. // StmtRes is a representation of a resource and possibly some edges.
// TODO: consider expanding Name (if it's a list) to have this return a list of
// Res's in the Output function. Alternatively, it could be a map[name]struct{},
// or even a map[[]name]struct{}.
type StmtRes struct { type StmtRes struct {
Kind string // kind of resource, eg: pkg, file, svc, etc... Kind string // kind of resource, eg: pkg, file, svc, etc...
Name interfaces.Expr // unique name for the res of this kind Name interfaces.Expr // unique name for the res of this kind
Fields []*StmtResField // list of fields in parsed order Contents []StmtResContents // list of fields/edges in parsed order
} }
// Interpolate returns a new node (or itself) once it has been expanded. This // Interpolate returns a new node (or itself) once it has been expanded. This
// generally increases the size of the AST when it is used. It calls Interpolate // generally increases the size of the AST when it is used. It calls Interpolate
// on any child elements and builds the new node with those new node contents. // on any child elements and builds the new node with those new node contents.
// TODO: could we expand Name (if it's a list) to have this return a list of
// Res's ? We'd have to return a StmtProg containing those in its place...
func (obj *StmtRes) Interpolate() (interfaces.Stmt, error) { func (obj *StmtRes) Interpolate() (interfaces.Stmt, error) {
name, err := obj.Name.Interpolate() name, err := obj.Name.Interpolate()
if err != nil { if err != nil {
return nil, err return nil, err
} }
fields := []*StmtResField{} contents := []StmtResContents{}
for _, x := range obj.Fields { for _, x := range obj.Contents { // make sure we preserve ordering...
interpolated, err := x.Value.Interpolate() interpolated, err := x.Interpolate()
if err != nil { if err != nil {
return nil, err return nil, err
} }
var condition interfaces.Expr contents = append(contents, interpolated)
if x.Condition != nil {
condition, err = x.Condition.Interpolate()
if err != nil {
return nil, err
}
}
field := &StmtResField{
Field: x.Field,
Value: interpolated,
Condition: condition,
}
fields = append(fields, field)
} }
return &StmtRes{ return &StmtRes{
Kind: obj.Kind, Kind: obj.Kind,
Name: name, Name: name,
Fields: fields, Contents: contents,
}, nil }, nil
} }
@@ -147,15 +155,10 @@ func (obj *StmtRes) SetScope(scope *interfaces.Scope) error {
if err := obj.Name.SetScope(scope); err != nil { if err := obj.Name.SetScope(scope); err != nil {
return err return err
} }
for _, x := range obj.Fields { for _, x := range obj.Contents {
if err := x.Value.SetScope(scope); err != nil { if err := x.SetScope(scope); err != nil {
return err return err
} }
if x.Condition != nil {
if err := x.Condition.SetScope(scope); err != nil {
return err
}
}
} }
return nil return nil
} }
@@ -179,54 +182,13 @@ func (obj *StmtRes) Unify() ([]interfaces.Invariant, error) {
} }
invariants = append(invariants, invar) invariants = append(invariants, invar)
// collect all the invariants of each field // collect all the invariants of each field and edge
for _, x := range obj.Fields { for _, x := range obj.Contents {
invars, err := x.Value.Unify() invars, err := x.Unify(obj.Kind) // pass in the resource kind
if err != nil { if err != nil {
return nil, err return nil, err
} }
invariants = append(invariants, invars...) invariants = append(invariants, invars...)
// conditional expression might have some children invariants to share
if x.Condition != nil {
condition, err := x.Condition.Unify()
if err != nil {
return nil, err
}
invariants = append(invariants, condition...)
// the condition must ultimately be a boolean
conditionInvar := &unification.EqualsInvariant{
Expr: x.Condition,
Type: types.TypeBool,
}
invariants = append(invariants, conditionInvar)
}
}
typMap, err := resources.LangFieldNameToStructType(obj.Kind)
if err != nil {
return nil, err
}
for _, x := range obj.Fields {
field := strings.TrimSpace(x.Field)
if len(field) != len(x.Field) {
return nil, fmt.Errorf("field was wrapped in whitespace")
}
if len(strings.Fields(field)) != 1 {
return nil, fmt.Errorf("field was empty or contained spaces")
}
typ, exists := typMap[x.Field]
if !exists {
return nil, fmt.Errorf("could not determine type for `%s` field of `%s`", x.Field, obj.Kind)
}
invar := &unification.EqualsInvariant{
Expr: x.Value,
Type: typ,
}
invariants = append(invariants, invar)
} }
return invariants, nil return invariants, nil
@@ -253,20 +215,12 @@ func (obj *StmtRes) Graph() (*pgraph.Graph, error) {
} }
graph.AddGraph(g) graph.AddGraph(g)
for _, x := range obj.Fields { for _, x := range obj.Contents {
g, err := x.Value.Graph() g, err := x.Graph()
if err != nil { if err != nil {
return nil, err return nil, err
} }
graph.AddGraph(g) graph.AddGraph(g)
if x.Condition != nil {
g, err := x.Condition.Graph()
if err != nil {
return nil, err
}
graph.AddGraph(g)
}
} }
return graph, nil return graph, nil
@@ -277,12 +231,13 @@ func (obj *StmtRes) Graph() (*pgraph.Graph, error) {
// analogous function for expressions is Value. Those Value functions might get // 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 // 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. // the case of this resource statement, this is definitely the case.
// XXX: Add MetaParams... // XXX: Add MetaParams as a simple meta field with a struct of the right type...
func (obj *StmtRes) Output() (*interfaces.Output, error) { func (obj *StmtRes) Output() (*interfaces.Output, error) {
nameValue, err := obj.Name.Value() nameValue, err := obj.Name.Value()
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO: test for []str instead, and loop
name := nameValue.Str() // must not panic name := nameValue.Str() // must not panic
res, err := resources.NewNamedResource(obj.Kind, name) res, err := resources.NewNamedResource(obj.Kind, name)
@@ -302,7 +257,12 @@ func (obj *StmtRes) Output() (*interfaces.Output, error) {
ts := reflect.TypeOf(res).Elem() // pointer to struct, then struct ts := reflect.TypeOf(res).Elem() // pointer to struct, then struct
// FIXME: we could probably simplify this code... // FIXME: we could probably simplify this code...
for _, x := range obj.Fields { for _, line := range obj.Contents {
x, ok := line.(*StmtResField)
if !ok {
continue
}
if x.Condition != nil { if x.Condition != nil {
b, err := x.Condition.Value() b, err := x.Condition.Value()
if err != nil { if err != nil {
@@ -408,13 +368,141 @@ func (obj *StmtRes) Output() (*interfaces.Output, error) {
value.Elem().Set(valof) value.Elem().Set(valof)
} }
f.Set(value) // set it ! f.Set(value) // set it !
}
edges, err := obj.edges()
if err != nil {
return nil, errwrap.Wrapf(err, "error building edges")
} }
return &interfaces.Output{ return &interfaces.Output{
Resources: []resources.Res{res}, Resources: []resources.Res{res},
Edges: edges,
}, nil }, nil
} }
// edges is a helper function to generate the edges that come from the resource.
func (obj *StmtRes) edges() ([]*interfaces.Edge, error) {
edges := []*interfaces.Edge{}
// to and from self, map of kind, name, notify
var to = make(map[string]map[string]bool) // to this from self
var from = make(map[string]map[string]bool) // from this to self
for _, line := range obj.Contents {
x, ok := line.(*StmtResEdge)
if !ok {
continue
}
if x.Condition != nil {
b, err := x.Condition.Value()
if err != nil {
return nil, err
}
if !b.Bool() { // if value exists, and is false, skip it
continue
}
}
v, err := x.EdgeHalf.Name.Value()
if err != nil {
return nil, err
}
name := v.Str() // must not panic
kind := x.EdgeHalf.Kind
var notify bool
switch p := x.Property; p {
// a -> b
// a notify b
// a before b
case EdgeNotify:
notify = true
fallthrough
case EdgeBefore:
if m, exists := to[kind]; !exists {
to[kind] = make(map[string]bool)
} else if n, exists := m[name]; exists {
notify = notify || n // collate
}
to[kind][name] = notify // to this from self
// b -> a
// b listen a
// b depend a
case EdgeListen:
notify = true
fallthrough
case EdgeDepend:
if m, exists := from[kind]; !exists {
from[kind] = make(map[string]bool)
} else if n, exists := m[name]; exists {
notify = notify || n // collate
}
from[kind][name] = notify // from this to self
default:
return nil, fmt.Errorf("unknown property: %s", p)
}
}
// TODO: we could detect simple loops here (if `from` and `to` have the
// same entry) but we can leave this to the proper dag checker later on
v, err := obj.Name.Value()
if err != nil {
return nil, err
}
self := v.Str() // must not panic
for kind, x := range to { // to this from self
for name, notify := range x {
edge := &interfaces.Edge{
Kind1: obj.Kind,
Name1: self,
//Send: "",
Kind2: kind,
Name2: name,
//Recv: "",
Notify: notify,
}
edges = append(edges, edge)
}
}
for kind, x := range from { // from this to self
for name, notify := range x {
edge := &interfaces.Edge{
Kind1: kind,
Name1: name,
//Send: "",
Kind2: obj.Kind,
Name2: self,
//Recv: "",
Notify: notify,
}
edges = append(edges, edge)
}
}
return edges, nil
}
// StmtResContents is the interface that is met by the resource contents. Look
// closely for while it is similar to the Stmt interface, it is quite different.
type StmtResContents interface {
Interpolate() (StmtResContents, error) // different!
SetScope(*interfaces.Scope) error
Unify(kind string) ([]interfaces.Invariant, error) // different!
Graph() (*pgraph.Graph, error)
}
// StmtResField represents a single field in the parsed resource representation. // StmtResField represents a single field in the parsed resource representation.
// This does not satisfy the Stmt interface. // This does not satisfy the Stmt interface.
type StmtResField struct { type StmtResField struct {
@@ -423,6 +511,243 @@ type StmtResField struct {
Condition interfaces.Expr // the value will be used if nil or true Condition interfaces.Expr // the value will be used if nil or true
} }
// Interpolate returns a new node (or itself) once it has been expanded. This
// generally increases the size of the AST when it is used. It calls Interpolate
// on any child elements and builds the new node with those new node contents.
// This interpolate is different It is different from the interpolate found in
// the Expr and Stmt interfaces because it returns a different type as output.
func (obj *StmtResField) Interpolate() (StmtResContents, error) {
interpolated, err := obj.Value.Interpolate()
if err != nil {
return nil, err
}
var condition interfaces.Expr
if obj.Condition != nil {
condition, err = obj.Condition.Interpolate()
if err != nil {
return nil, err
}
}
return &StmtResField{
Field: obj.Field,
Value: interpolated,
Condition: condition,
}, nil
}
// SetScope stores the scope for later use in this resource and it's children,
// which it propagates this downwards to.
func (obj *StmtResField) SetScope(scope *interfaces.Scope) error {
if err := obj.Value.SetScope(scope); err != nil {
return err
}
if obj.Condition != nil {
if err := obj.Condition.SetScope(scope); err != nil {
return err
}
}
return nil
}
// Unify returns the list of invariants that this node produces. It recursively
// calls Unify on any children elements that exist in the AST, and returns the
// collection to the caller. It is different from the Unify found in the Expr
// and Stmt interfaces because it adds an input parameter.
func (obj *StmtResField) Unify(kind string) ([]interfaces.Invariant, error) {
var invariants []interfaces.Invariant
invars, err := obj.Value.Unify()
if err != nil {
return nil, err
}
invariants = append(invariants, invars...)
// conditional expression might have some children invariants to share
if obj.Condition != nil {
condition, err := obj.Condition.Unify()
if err != nil {
return nil, err
}
invariants = append(invariants, condition...)
// the condition must ultimately be a boolean
conditionInvar := &unification.EqualsInvariant{
Expr: obj.Condition,
Type: types.TypeBool,
}
invariants = append(invariants, conditionInvar)
}
// TODO: unfortunately this gets called separately for each field... if
// we could cache this, it might be worth looking into for performance!
typMap, err := resources.LangFieldNameToStructType(kind)
if err != nil {
return nil, err
}
field := strings.TrimSpace(obj.Field)
if len(field) != len(obj.Field) {
return nil, fmt.Errorf("field was wrapped in whitespace")
}
if len(strings.Fields(field)) != 1 {
return nil, fmt.Errorf("field was empty or contained spaces")
}
typ, exists := typMap[obj.Field]
if !exists {
return nil, fmt.Errorf("could not determine type for `%s` field of `%s`", obj.Field, kind)
}
invar := &unification.EqualsInvariant{
Expr: obj.Value,
Type: typ,
}
invariants = append(invariants, invar)
return invariants, nil
}
// Graph returns the reactive function graph which is expressed by this node. It
// includes any vertices produced by this node, and the appropriate edges to any
// vertices that are produced by its children. Nodes which fulfill the Expr
// interface directly produce vertices (and possible children) where as nodes
// that fulfill the Stmt interface do not produces vertices, where as their
// children might. It is interesting to note that nothing directly adds an edge
// to the resources created, but rather, once all the values (expressions) with
// no outgoing edges have produced at least a single value, then the resources
// know they're able to be built.
func (obj *StmtResField) Graph() (*pgraph.Graph, error) {
graph, err := pgraph.NewGraph("resfield")
if err != nil {
return nil, errwrap.Wrapf(err, "could not create graph")
}
g, err := obj.Value.Graph()
if err != nil {
return nil, err
}
graph.AddGraph(g)
if obj.Condition != nil {
g, err := obj.Condition.Graph()
if err != nil {
return nil, err
}
graph.AddGraph(g)
}
return graph, nil
}
// StmtResEdge represents a single edge property in the parsed resource
// representation. This does not satisfy the Stmt interface.
type StmtResEdge struct {
Property string // TODO: iota constant instead?
EdgeHalf *StmtEdgeHalf
Condition interfaces.Expr // the value will be used if nil or true
}
// Interpolate returns a new node (or itself) once it has been expanded. This
// generally increases the size of the AST when it is used. It calls Interpolate
// on any child elements and builds the new node with those new node contents.
// This interpolate is different It is different from the interpolate found in
// the Expr and Stmt interfaces because it returns a different type as output.
func (obj *StmtResEdge) Interpolate() (StmtResContents, error) {
interpolated, err := obj.EdgeHalf.Interpolate()
if err != nil {
return nil, err
}
var condition interfaces.Expr
if obj.Condition != nil {
condition, err = obj.Condition.Interpolate()
if err != nil {
return nil, err
}
}
return &StmtResEdge{
Property: obj.Property,
EdgeHalf: interpolated,
Condition: condition,
}, nil
}
// SetScope stores the scope for later use in this resource and it's children,
// which it propagates this downwards to.
func (obj *StmtResEdge) SetScope(scope *interfaces.Scope) error {
if err := obj.EdgeHalf.SetScope(scope); err != nil {
return err
}
if obj.Condition != nil {
if err := obj.Condition.SetScope(scope); err != nil {
return err
}
}
return nil
}
// Unify returns the list of invariants that this node produces. It recursively
// calls Unify on any children elements that exist in the AST, and returns the
// collection to the caller. It is different from the Unify found in the Expr
// and Stmt interfaces because it adds an input parameter.
func (obj *StmtResEdge) Unify(kind string) ([]interfaces.Invariant, error) {
var invariants []interfaces.Invariant
invars, err := obj.EdgeHalf.Unify()
if err != nil {
return nil, err
}
invariants = append(invariants, invars...)
// conditional expression might have some children invariants to share
if obj.Condition != nil {
condition, err := obj.Condition.Unify()
if err != nil {
return nil, err
}
invariants = append(invariants, condition...)
// the condition must ultimately be a boolean
conditionInvar := &unification.EqualsInvariant{
Expr: obj.Condition,
Type: types.TypeBool,
}
invariants = append(invariants, conditionInvar)
}
return invariants, nil
}
// Graph returns the reactive function graph which is expressed by this node. It
// includes any vertices produced by this node, and the appropriate edges to any
// vertices that are produced by its children. Nodes which fulfill the Expr
// interface directly produce vertices (and possible children) where as nodes
// that fulfill the Stmt interface do not produces vertices, where as their
// children might. It is interesting to note that nothing directly adds an edge
// to the resources created, but rather, once all the values (expressions) with
// no outgoing edges have produced at least a single value, then the resources
// know they're able to be built.
func (obj *StmtResEdge) Graph() (*pgraph.Graph, error) {
graph, err := pgraph.NewGraph("resedge")
if err != nil {
return nil, errwrap.Wrapf(err, "could not create graph")
}
g, err := obj.EdgeHalf.Graph()
if err != nil {
return nil, err
}
graph.AddGraph(g)
if obj.Condition != nil {
g, err := obj.Condition.Graph()
if err != nil {
return nil, err
}
graph.AddGraph(g)
}
return graph, nil
}
// StmtEdge is a representation of a dependency. It also supports send/recv. // StmtEdge is a representation of a dependency. It also supports send/recv.
// Edges represents that the first resource (Kind/Name) listed in the // Edges represents that the first resource (Kind/Name) listed in the
// EdgeHalfList should happen in the resource graph *before* the next resource // EdgeHalfList should happen in the resource graph *before* the next resource
@@ -448,16 +773,10 @@ type StmtEdge struct {
func (obj *StmtEdge) Interpolate() (interfaces.Stmt, error) { func (obj *StmtEdge) Interpolate() (interfaces.Stmt, error) {
edgeHalfList := []*StmtEdgeHalf{} edgeHalfList := []*StmtEdgeHalf{}
for _, x := range obj.EdgeHalfList { for _, x := range obj.EdgeHalfList {
name, err := x.Name.Interpolate() edgeHalf, err := x.Interpolate()
if err != nil { if err != nil {
return nil, err return nil, err
} }
edgeHalf := &StmtEdgeHalf{
Kind: x.Kind,
Name: name,
SendRecv: x.SendRecv,
}
edgeHalfList = append(edgeHalfList, edgeHalf) edgeHalfList = append(edgeHalfList, edgeHalf)
} }
@@ -471,7 +790,7 @@ func (obj *StmtEdge) Interpolate() (interfaces.Stmt, error) {
// which it propagates this downwards to. // which it propagates this downwards to.
func (obj *StmtEdge) SetScope(scope *interfaces.Scope) error { func (obj *StmtEdge) SetScope(scope *interfaces.Scope) error {
for _, x := range obj.EdgeHalfList { for _, x := range obj.EdgeHalfList {
if err := x.Name.SetScope(scope); err != nil { if err := x.SetScope(scope); err != nil {
return err return err
} }
} }
@@ -499,26 +818,15 @@ func (obj *StmtEdge) Unify() ([]interfaces.Invariant, error) {
} }
for _, x := range obj.EdgeHalfList { for _, x := range obj.EdgeHalfList {
if x.Kind == "" {
return nil, fmt.Errorf("missing resource kind in edge")
}
if x.SendRecv != "" && len(obj.EdgeHalfList) != 2 { if x.SendRecv != "" && len(obj.EdgeHalfList) != 2 {
return nil, fmt.Errorf("send/recv edges must come in pairs") return nil, fmt.Errorf("send/recv edges must come in pairs")
} }
invars, err := x.Name.Unify() invars, err := x.Unify()
if err != nil { if err != nil {
return nil, err return nil, err
} }
invariants = append(invariants, invars...) invariants = append(invariants, invars...)
// name must be a string
invar := &unification.EqualsInvariant{
Expr: x.Name,
Type: types.TypeStr,
}
invariants = append(invariants, invar)
} }
return invariants, nil return invariants, nil
@@ -540,7 +848,7 @@ func (obj *StmtEdge) Graph() (*pgraph.Graph, error) {
} }
for _, x := range obj.EdgeHalfList { for _, x := range obj.EdgeHalfList {
g, err := x.Name.Graph() g, err := x.Graph()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -599,6 +907,81 @@ type StmtEdgeHalf struct {
SendRecv string // name of field to send/recv from, empty to ignore SendRecv string // name of field to send/recv from, empty to ignore
} }
// Interpolate returns a new node (or itself) once it has been expanded. This
// generally increases the size of the AST when it is used. It calls Interpolate
// on any child elements and builds the new node with those new node contents.
// This interpolate is different It is different from the interpolate found in
// the Expr and Stmt interfaces because it returns a different type as output.
func (obj *StmtEdgeHalf) Interpolate() (*StmtEdgeHalf, error) {
name, err := obj.Name.Interpolate()
if err != nil {
return nil, err
}
edgeHalf := &StmtEdgeHalf{
Kind: obj.Kind,
Name: name,
SendRecv: obj.SendRecv,
}
return edgeHalf, nil
}
// SetScope stores the scope for later use in this resource and it's children,
// which it propagates this downwards to.
func (obj *StmtEdgeHalf) SetScope(scope *interfaces.Scope) error {
return obj.Name.SetScope(scope)
}
// Unify returns the list of invariants that this node produces. It recursively
// calls Unify on any children elements that exist in the AST, and returns the
// collection to the caller.
func (obj *StmtEdgeHalf) Unify() ([]interfaces.Invariant, error) {
var invariants []interfaces.Invariant
if obj.Kind == "" {
return nil, fmt.Errorf("missing resource kind in edge")
}
invars, err := obj.Name.Unify()
if err != nil {
return nil, err
}
invariants = append(invariants, invars...)
// name must be a string
invar := &unification.EqualsInvariant{
Expr: obj.Name,
Type: types.TypeStr,
}
invariants = append(invariants, invar)
return invariants, nil
}
// Graph returns the reactive function graph which is expressed by this node. It
// includes any vertices produced by this node, and the appropriate edges to any
// vertices that are produced by its children. Nodes which fulfill the Expr
// interface directly produce vertices (and possible children) where as nodes
// that fulfill the Stmt interface do not produces vertices, where as their
// children might. It is interesting to note that nothing directly adds an edge
// to the resources created, but rather, once all the values (expressions) with
// no outgoing edges have produced at least a single value, then the resources
// know they're able to be built.
func (obj *StmtEdgeHalf) Graph() (*pgraph.Graph, error) {
graph, err := pgraph.NewGraph("edgehalf")
if err != nil {
return nil, errwrap.Wrapf(err, "could not create graph")
}
g, err := obj.Name.Graph()
if err != nil {
return nil, err
}
graph.AddGraph(g)
return graph, nil
}
// StmtIf represents an if condition that contains between one and two branches // StmtIf represents an if condition that contains between one and two branches
// of statements to be executed based on the evaluation of the boolean condition // of statements to be executed based on the evaluation of the boolean condition
// over time. In particular, this is different from an ExprIf which returns a // over time. In particular, this is different from an ExprIf which returns a

View File

@@ -51,8 +51,8 @@ func TestUnification1(t *testing.T) {
&StmtRes{ &StmtRes{
Kind: "test", Kind: "test",
Name: &ExprStr{V: "t1"}, Name: &ExprStr{V: "t1"},
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "str", Field: "str",
Value: expr, Value: expr,
}, },
@@ -85,8 +85,8 @@ func TestUnification1(t *testing.T) {
&StmtRes{ &StmtRes{
Kind: "test", Kind: "test",
Name: &ExprStr{V: "test"}, Name: &ExprStr{V: "test"},
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "slicestring", Field: "slicestring",
Value: expr, Value: expr,
}, },
@@ -125,8 +125,8 @@ func TestUnification1(t *testing.T) {
&StmtRes{ &StmtRes{
Kind: "test", Kind: "test",
Name: &ExprStr{V: "test"}, Name: &ExprStr{V: "test"},
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "mapintfloat", Field: "mapintfloat",
Value: expr, Value: expr,
}, },
@@ -167,8 +167,8 @@ func TestUnification1(t *testing.T) {
&StmtRes{ &StmtRes{
Kind: "test", Kind: "test",
Name: &ExprStr{V: "test"}, Name: &ExprStr{V: "test"},
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "mixedstruct", Field: "mixedstruct",
Value: expr, Value: expr,
}, },
@@ -215,8 +215,8 @@ func TestUnification1(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "n1", V: "n1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "int64ptr", Field: "int64ptr",
Value: expr, // func Value: expr, // func
}, },
@@ -270,8 +270,8 @@ func TestUnification1(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "n1", V: "n1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "int64ptr", Field: "int64ptr",
Value: expr, Value: expr,
}, },
@@ -326,8 +326,8 @@ func TestUnification1(t *testing.T) {
Name: &ExprStr{ Name: &ExprStr{
V: "n1", V: "n1",
}, },
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "float32", Field: "float32",
Value: expr, Value: expr,
}, },
@@ -458,8 +458,8 @@ func TestUnification1(t *testing.T) {
&StmtRes{ &StmtRes{
Kind: "test", Kind: "test",
Name: &ExprStr{V: "t1"}, Name: &ExprStr{V: "t1"},
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "stringptr", Field: "stringptr",
Value: expr, Value: expr,
}, },
@@ -493,8 +493,8 @@ func TestUnification1(t *testing.T) {
&StmtRes{ &StmtRes{
Kind: "test", Kind: "test",
Name: &ExprStr{V: "t1"}, Name: &ExprStr{V: "t1"},
Fields: []*StmtResField{ Contents: []StmtResContents{
{ &StmtResField{
Field: "stringptr", Field: "stringptr",
Value: expr, Value: expr,
}, },