From 8e01b6db48a1c54ad982cfa9dd0d06c2fc13bc6b Mon Sep 17 00:00:00 2001 From: James Shubin Date: Sun, 25 Feb 2018 20:46:30 -0500 Subject: [PATCH] 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. --- docs/language-guide.md | 40 ++++++++++++++++++++++++---- examples/lang/elvis0.mcl | 5 ++++ lang/lexer.nex | 5 ++++ lang/lexparse_test.go | 14 ++++++++++ lang/parser.y | 19 +++++++++++++- lang/structs.go | 57 +++++++++++++++++++++++++++++++++++++--- 6 files changed, 130 insertions(+), 10 deletions(-) create mode 100644 examples/lang/elvis0.mcl diff --git a/docs/language-guide.md b/docs/language-guide.md index 647ad57f..f858286d 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -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 expression - ``` + ```mcl if { } else { @@ -101,7 +101,7 @@ expression - **resource**: produces a resource - ``` + ```mcl file "/tmp/hello" { content => "world", mode => "o=rwx", @@ -110,7 +110,7 @@ expression - **edge**: produces an edge - ``` + ```mcl File["/tmp/hello"] -> Print["alert4"] ``` @@ -131,14 +131,44 @@ This section needs better documentation. #### 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 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: -``` +```mcl Pkg["drbd"] -> File["/etc/drbd.conf"] -> Svc["drbd"] ``` diff --git a/examples/lang/elvis0.mcl b/examples/lang/elvis0.mcl new file mode 100644 index 00000000..bfc916cd --- /dev/null +++ b/examples/lang/elvis0.mcl @@ -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", +} diff --git a/lang/lexer.nex b/lang/lexer.nex index 1367c6d2..5bb5d0de 100644 --- a/lang/lexer.nex +++ b/lang/lexer.nex @@ -39,6 +39,11 @@ lval.str = yylex.Text() return ELSE } +/\?:/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return ELVIS + } /=>/ { yylex.pos(lval) // our pos lval.str = yylex.Text() diff --git a/lang/lexparse_test.go b/lang/lexparse_test.go index 43fa1dc7..9c029e14 100644 --- a/lang/lexparse_test.go +++ b/lang/lexparse_test.go @@ -122,6 +122,20 @@ func TestLexParse0(t *testing.T) { //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 values = append(values, test{ diff --git a/lang/parser.y b/lang/parser.y index 616b932e..b9aea602 100644 --- a/lang/parser.y +++ b/lang/parser.y @@ -77,7 +77,7 @@ func init() { %token STRING BOOL INTEGER FLOAT %token EQUALS %token COMMA COLON SEMICOLON -%token ROCKET ARROW DOT +%token ELVIS ROCKET ARROW DOT %token STR_IDENTIFIER BOOL_IDENTIFIER INT_IDENTIFIER FLOAT_IDENTIFIER %token STRUCT_IDENTIFIER VARIANT_IDENTIFIER VAR_IDENTIFIER IDENTIFIER %token VAR_IDENTIFIER_HX CAPITALIZED_IDENTIFIER @@ -682,6 +682,11 @@ resource_body: posLast(yylex, yyDollar) // our pos $$.resFields = append($1.resFields, $2.resField) } +| resource_body conditional_resource_field + { + posLast(yylex, yyDollar) // our pos + $$.resFields = append($1.resFields, $2.resField) + } ; resource_field: 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: // TODO: we could technically prevent single edge_half pieces from being // parsed, but it's probably more work than is necessary... diff --git a/lang/structs.go b/lang/structs.go index 1f7efb09..4ae4cfcd 100644 --- a/lang/structs.go +++ b/lang/structs.go @@ -120,9 +120,17 @@ func (obj *StmtRes) Interpolate() (interfaces.Stmt, error) { if err != nil { 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: x.Field, - Value: interpolated, + Field: x.Field, + Value: interpolated, + Condition: condition, } fields = append(fields, field) } @@ -143,6 +151,11 @@ func (obj *StmtRes) SetScope(scope *interfaces.Scope) error { if err := x.Value.SetScope(scope); err != nil { return err } + if x.Condition != nil { + if err := x.Condition.SetScope(scope); err != nil { + return err + } + } } return nil } @@ -173,6 +186,22 @@ func (obj *StmtRes) Unify() ([]interfaces.Invariant, error) { return nil, err } 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) @@ -230,6 +259,14 @@ func (obj *StmtRes) Graph() (*pgraph.Graph, error) { return nil, err } graph.AddGraph(g) + + if x.Condition != nil { + g, err := x.Condition.Graph() + if err != nil { + return nil, err + } + graph.AddGraph(g) + } } return graph, nil @@ -266,6 +303,17 @@ func (obj *StmtRes) Output() (*interfaces.Output, error) { // FIXME: we could probably simplify this code... 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() if err != nil { 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. // This does not satisfy the Stmt interface. type StmtResField struct { - Field string - Value interfaces.Expr + Field string + 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.