lang: Add a resource-specific elvis operator

This allows you to omit a resource parameter programmatically, and
avoids the need of an `undef` or `nil` in our language, which would
contribute to programming errors, crashes, and overall reduced safety.
This commit is contained in:
James Shubin
2018-02-25 20:46:30 -05:00
parent 67607eba8b
commit 8e01b6db48
6 changed files with 130 additions and 10 deletions

View File

@@ -90,7 +90,7 @@ There are a very small number of statements in our language. They include:
- **if**: produces up to one branch of statements based on a conditional - **if**: produces up to one branch of statements based on a conditional
expression expression
``` ```mcl
if <conditional> { if <conditional> {
<statements> <statements>
} else { } else {
@@ -101,7 +101,7 @@ expression
- **resource**: produces a resource - **resource**: produces a resource
``` ```mcl
file "/tmp/hello" { file "/tmp/hello" {
content => "world", content => "world",
mode => "o=rwx", mode => "o=rwx",
@@ -110,7 +110,7 @@ expression
- **edge**: produces an edge - **edge**: produces an edge
``` ```mcl
File["/tmp/hello"] -> Print["alert4"] File["/tmp/hello"] -> Print["alert4"]
``` ```
@@ -131,14 +131,44 @@ This section needs better documentation.
#### Resource #### Resource
This section needs better documentation. Resources express the idempotent workloads that we want to have apply on our
system. They correspond to vertices in a [graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph)
which represent the order in which their declared state is applied. You will
usually want to pass in a number of parameters and associated values to the
resource to control how it behaves. For example, setting the `content` parameter
of a `file` resource to the string `hello`, will cause the contents of that file
to contain the string `hello` after it has run.
For some parameters, there is a distinction between an unspecified parameter,
and a parameter with a `zero` value. For example, for the file resource, you
might choose to set the `content` parameter to be the empty string, which would
ensure that the file has a length of zero. Alternatively you might wish to not
specify the file contents at all, which would leave that property undefined. If
you omit listing a property, then it will be undefined. To control this property
programmatically, you need to specify an `is-defined` value, as well as the
value to use if that boolean is true. You can do this with the resource-specific
`elvis` operator.
```mcl
$b = true # change me to false and then try editing the file manually
file "/tmp/mgmt-elvis" {
content => $b ?: "hello world\n",
state => "exists",
}
```
This example is static, however you can imagine that the `$b` value might be
chosen in a programmatic way, even one in which that value varies over time. If
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
not specified, then it will obviously not be used.
#### 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
be chained as a pair, or in any greater number. For example, you may write: be chained as a pair, or in any greater number. For example, you may write:
``` ```mcl
Pkg["drbd"] -> File["/etc/drbd.conf"] -> Svc["drbd"] Pkg["drbd"] -> File["/etc/drbd.conf"] -> Svc["drbd"]
``` ```

5
examples/lang/elvis0.mcl Normal file
View File

@@ -0,0 +1,5 @@
$b = true # change me to false and then try editing the file manually
file "/tmp/mgmt-elvis" {
content => $b ?: "hello world\n",
state => "exists",
}

View File

@@ -39,6 +39,11 @@
lval.str = yylex.Text() lval.str = yylex.Text()
return ELSE return ELSE
} }
/\?:/ {
yylex.pos(lval) // our pos
lval.str = yylex.Text()
return ELVIS
}
/=>/ { /=>/ {
yylex.pos(lval) // our pos yylex.pos(lval) // our pos
lval.str = yylex.Text() lval.str = yylex.Text()

View File

@@ -122,6 +122,20 @@ func TestLexParse0(t *testing.T) {
//exp: ???, // FIXME: add the expected AST //exp: ???, // FIXME: add the expected AST
}) })
} }
{
values = append(values, test{
name: "one res with elvis",
code: `
test "t1" {
int16 => true ?: 42, # elvis operator
int32 => 42,
stringptr => false ?: "", # missing is not ""
}
`,
fail: false,
//exp: ???, // FIXME: add the expected AST
})
}
{ {
// TODO: skip trailing comma requirement on one-liners // TODO: skip trailing comma requirement on one-liners
values = append(values, test{ values = append(values, test{

View File

@@ -77,7 +77,7 @@ func init() {
%token STRING BOOL INTEGER FLOAT %token STRING BOOL INTEGER FLOAT
%token EQUALS %token EQUALS
%token COMMA COLON SEMICOLON %token COMMA COLON SEMICOLON
%token ROCKET ARROW DOT %token ELVIS ROCKET ARROW DOT
%token STR_IDENTIFIER BOOL_IDENTIFIER INT_IDENTIFIER FLOAT_IDENTIFIER %token STR_IDENTIFIER BOOL_IDENTIFIER INT_IDENTIFIER FLOAT_IDENTIFIER
%token STRUCT_IDENTIFIER VARIANT_IDENTIFIER VAR_IDENTIFIER IDENTIFIER %token STRUCT_IDENTIFIER VARIANT_IDENTIFIER VAR_IDENTIFIER IDENTIFIER
%token VAR_IDENTIFIER_HX CAPITALIZED_IDENTIFIER %token VAR_IDENTIFIER_HX CAPITALIZED_IDENTIFIER
@@ -682,6 +682,11 @@ resource_body:
posLast(yylex, yyDollar) // our pos posLast(yylex, yyDollar) // our pos
$$.resFields = append($1.resFields, $2.resField) $$.resFields = append($1.resFields, $2.resField)
} }
| resource_body conditional_resource_field
{
posLast(yylex, yyDollar) // our pos
$$.resFields = append($1.resFields, $2.resField)
}
; ;
resource_field: resource_field:
IDENTIFIER ROCKET expr COMMA IDENTIFIER ROCKET expr COMMA
@@ -693,6 +698,18 @@ resource_field:
} }
} }
; ;
conditional_resource_field:
// content => $present ?: "hello",
IDENTIFIER ROCKET expr ELVIS expr COMMA
{
posLast(yylex, yyDollar) // our pos
$$.resField = &StmtResField{
Field: $1.str,
Value: $5.expr,
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

@@ -120,9 +120,17 @@ func (obj *StmtRes) Interpolate() (interfaces.Stmt, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
var condition interfaces.Expr
if x.Condition != nil {
condition, err = x.Condition.Interpolate()
if err != nil {
return nil, err
}
}
field := &StmtResField{ field := &StmtResField{
Field: x.Field, Field: x.Field,
Value: interpolated, Value: interpolated,
Condition: condition,
} }
fields = append(fields, field) fields = append(fields, field)
} }
@@ -143,6 +151,11 @@ func (obj *StmtRes) SetScope(scope *interfaces.Scope) error {
if err := x.Value.SetScope(scope); err != nil { if err := x.Value.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
} }
@@ -173,6 +186,22 @@ func (obj *StmtRes) Unify() ([]interfaces.Invariant, error) {
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) typMap, err := resources.LangFieldNameToStructType(obj.Kind)
@@ -230,6 +259,14 @@ func (obj *StmtRes) Graph() (*pgraph.Graph, error) {
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
@@ -266,6 +303,17 @@ func (obj *StmtRes) Output() (*interfaces.Output, error) {
// FIXME: we could probably simplify this code... // FIXME: we could probably simplify this code...
for _, x := range obj.Fields { for _, x := range obj.Fields {
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
}
}
typ, err := x.Value.Type() typ, err := x.Value.Type()
if err != nil { if err != nil {
return nil, errwrap.Wrapf(err, "resource field `%s` did not return a type", x.Field) return nil, errwrap.Wrapf(err, "resource field `%s` did not return a type", x.Field)
@@ -370,8 +418,9 @@ func (obj *StmtRes) Output() (*interfaces.Output, 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 {
Field string Field string
Value interfaces.Expr Value interfaces.Expr
Condition interfaces.Expr // the value will be used if nil or true
} }
// StmtEdge is a representation of a dependency. It also supports send/recv. // StmtEdge is a representation of a dependency. It also supports send/recv.