From c62b8a5d4f1c78fa646ad3a6591ea0d4ee354116 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Tue, 12 Jun 2018 14:44:29 -0400 Subject: [PATCH] lang: Add class and include statements This adds support for the class definition statement and the include statement which produces the output from the corresponding class. The classes in this language support optional input parameters. In contrast with other tools, the class is *not* a singleton, although it can be used as one. Using include with equivalent input parameters will cause the class to act as a singleton, although it can also be used to produce distinct output. The output produced by including a class is actually a list of statements (a prog) which is ultimately a list of resources and edges. This is different from functions which produces values. --- docs/language-guide.md | 137 ++++++++++++- lang/interfaces/ast.go | 7 + lang/lang_test.go | 422 +++++++++++++++++++++++++++++++++++++++++ lang/lexer.nex | 10 + lang/lexparse_test.go | 190 +++++++++++++++++++ lang/parser.y | 77 ++++++++ lang/structs.go | 345 ++++++++++++++++++++++++++++++--- 7 files changed, 1160 insertions(+), 28 deletions(-) diff --git a/docs/language-guide.md b/docs/language-guide.md index 2d31ef4a..dbb75546 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -85,8 +85,9 @@ These docs will be expanded on when things are more certain to be stable. There are a very small number of statements in our language. They include: -- **bind**: bind's an expression to a variable within that scope +- **bind**: bind's an expression to a variable within that scope without output - eg: `$x = 42` + - **if**: produces up to one branch of statements based on a conditional expression @@ -114,6 +115,31 @@ expression File["/tmp/hello"] -> Print["alert4"] ``` +- **class**: bind's a list of statements to a class name in scope without output + + ```mcl + class foo { + # some statements go here + } + ``` + + or + + ```mcl + class bar($a, $b) { # a parameterized class + # some statements go here + } + ``` + +- **include**: include a particular class at this location producing output + + ```mcl + include foo + + include bar("hello", 42) + include bar("world", 13) # an include can be called multiple times + ``` + All statements produce _output_. Output consists of between zero and more `edges` and `resources`. A resource statement can produce a resource, whereas an `if` statement produces whatever the chosen branch produces. Ultimately the goal @@ -215,6 +241,82 @@ to express a relationship between three resources. The first character in the resource kind must be capitalized so that the parser can't ascertain unambiguously that we are referring to a dependency relationship. +#### Class + +A class is a grouping structure that bind's a list of statements to a name in +the scope where it is defined. It doesn't directly produce any output. To +produce output it must be called via the `include` statement. + +Defining classes follows the same scoping and shadowing rules that is applied to +the `bind` statement, although they exist in a separate namespace. In other +words you can have a variable named `foo` and a class named `foo` in the same +scope without any conflicts. + +Classes can be both parameterized or naked. If a parameterized class is defined, +then the argument types must be either specified manually, or inferred with the +type unification algorithm. One interesting property is that the same class +definition can be used with `include` via two different input signatures, +although in practice this is probably fairly rare. Some usage examples include: + +A naked class definition: + +```mcl +class foo { + # some statements go here +} +``` + +A parameterized class with both input types being inferred if possible: + +```mcl +class bar($a, $b) { + # some statements go here +} +``` + +A parameterized class with one type specified statically and one being inferred: + +```mcl +class baz($a str, $b) { + # some statements go here +} +``` + +Classes can also be nested within other classes. Here's a contrived example: + +```mcl +class c1($a, $b) { + # nested class definition + class c2($c) { + test $a { + stringptr => printf("%s is %d", $b, $c), + } + } + + if $a == "t1" { + include c2(42) + } +} +``` + +Defining polymorphic classes was considered but is not currently allowed at this +time. + +Recursive classes are not currently supported and it is not clear if they will +be in the future. Discussion about this topic is welcome on the mailing list. + +#### Include + +The `include` statement causes the previously defined class to produce the +contained output. This statement must be called with parameters if the named +class is defined with those. + +The defined class can be called as many times as you'd like either within the +same scope or within different scopes. If a class uses inferred type input +parameters, then the same class can even be called with different signatures. +Whether the output is useful and whether there is a unique type unification +solution is dependent on your code. + ### Stages The mgmt compiler runs in a number of stages. In order of execution they are: @@ -596,6 +698,39 @@ someListOfStrings := &types.ListValue{ If you don't build these properly, then you will cause a panic! Even empty lists have a type. +### Is the `class` statement a singleton? + +Not really, but practically it can be used as such. The `class` statement is not +a singleton since it can be called multiple times in different locations, and it +can also be parameterized and called multiple times (with `include`) using +different input parameters. The reason it can be used as such is that statement +output (from multple classes) that is compatible (and usually identical) will +be automatically collated and have the duplicates removed. In that way, you can +assume that an unparameterized class is always a singleton, and that +parameterized classes can often be singletons depending on their contents and if +they are called in an identical way or not. In reality the de-duplication +actually happens at the resource output level, so anything that produces +multiple compatible resources is allowed. + +### Are recursive `class` definitions supported? + +Recursive class definitions where the contents of a `class` contain a +self-referential `include`, either directly, or with indirection via any other +number of classes is not supported. It's not clear if it ever will be in the +future, unless we decide it's worth the extra complexity. The reason is that our +FRP actually generates a static graph which doesn't change unless the code does. +To support dynamic graphs would require our FRP to be a "higher-order" FRP, +instead of the simpler "first-order" FRP that it is now. You might want to +verify that I got the [nomenclature](https://github.com/gelisam/frp-zoo) +correct. If it turns out that there's an important advantage to supporting a +higher-order FRP in mgmt, then we can consider that in the future. + +I realized that recursion would require a static graph when I considered the +structure required for a simple recursive class definition. If some "depth" +value wasn't known statically by compile time, then there would be no way to +know how large the graph would grow, and furthermore, the graph would need to +change if that "depth" value changed. + ### I don't like the mgmt language, is there an alternative? Yes, the language is just one of the available "frontends" that passes a stream diff --git a/lang/interfaces/ast.go b/lang/interfaces/ast.go index f2ea4e26..81575b08 100644 --- a/lang/interfaces/ast.go +++ b/lang/interfaces/ast.go @@ -68,6 +68,7 @@ type Expr interface { type Scope struct { Variables map[string]Expr //Functions map[string]??? // TODO: do we want a separate namespace for user defined functions? + Classes map[string]Stmt } // Empty returns the zero, empty value for the scope, with all the internal @@ -76,6 +77,7 @@ func (obj *Scope) Empty() *Scope { return &Scope{ Variables: make(map[string]Expr), //Functions: ???, + Classes: make(map[string]Stmt), } } @@ -85,13 +87,18 @@ func (obj *Scope) Empty() *Scope { // we need those to be consistently pointing to the same things after copying. func (obj *Scope) Copy() *Scope { variables := make(map[string]Expr) + classes := make(map[string]Stmt) if obj != nil { // allow copying nil scopes for k, v := range obj.Variables { // copy variables[k] = v // we don't copy the expr's! } + for k, v := range obj.Classes { // copy + classes[k] = v // we don't copy the StmtClass! + } } return &Scope{ Variables: variables, + Classes: classes, } } diff --git a/lang/lang_test.go b/lang/lang_test.go index 469db498..a2a08382 100644 --- a/lang/lang_test.go +++ b/lang/lang_test.go @@ -378,6 +378,428 @@ func TestInterpretMany(t *testing.T) { graph: graph, }) } + { + graph, _ := pgraph.NewGraph("g") + r1, _ := engine.NewNamedResource("test", "t1") + x1 := r1.(*resources.TestRes) + s1 := "hello" + x1.StringPtr = &s1 + graph.AddVertex(x1) + values = append(values, test{ + name: "single include", + code: ` + class c1($a, $b) { + test $a { + stringptr => $b, + } + } + include c1("t1", "hello") + `, + fail: false, + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + r1, _ := engine.NewNamedResource("test", "t1") + r2, _ := engine.NewNamedResource("test", "t2") + x1 := r1.(*resources.TestRes) + x2 := r2.(*resources.TestRes) + s1, s2 := "hello", "world" + x1.StringPtr = &s1 + x2.StringPtr = &s2 + graph.AddVertex(x1, x2) + values = append(values, test{ + name: "double include", + code: ` + class c1($a, $b) { + test $a { + stringptr => $b, + } + } + include c1("t1", "hello") + include c1("t2", "world") + `, + fail: false, + graph: graph, + }) + } + { + //graph, _ := pgraph.NewGraph("g") + //r1, _ := engine.NewNamedResource("test", "t1") + //r2, _ := engine.NewNamedResource("test", "t2") + //x1 := r1.(*resources.TestRes) + //x2 := r2.(*resources.TestRes) + //s1, i2 := "hello", int64(42) + //x1.StringPtr = &s1 + //x2.Int64Ptr = &i2 + //graph.AddVertex(x1, x2) + values = append(values, test{ + name: "double include different types error", + code: ` + class c1($a, $b) { + if $a == "t1" { + test $a { + stringptr => $b, + } + } else { + test $a { + int64ptr => $b, + } + } + } + include c1("t1", "hello") + include c1("t2", 42) + `, + fail: true, // should not be able to type check this! + //graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + r1, _ := engine.NewNamedResource("test", "t1") + r2, _ := engine.NewNamedResource("test", "t2") + x1 := r1.(*resources.TestRes) + x2 := r2.(*resources.TestRes) + s1, s2 := "testing", "testing" + x1.StringPtr = &s1 + x2.StringPtr = &s2 + graph.AddVertex(x1, x2) + values = append(values, test{ + name: "double include different types allowed", + code: ` + class c1($a, $b) { + if $b == $b { # for example purposes + test $a { + stringptr => "testing", + } + } + } + include c1("t1", "hello") + include c1("t2", 42) # this has a different sig + `, + fail: false, + graph: graph, + }) + } + // TODO: add this test once printf supports %v + //{ + // graph, _ := pgraph.NewGraph("g") + // r1, _ := engine.NewNamedResource("test", "t1") + // r2, _ := engine.NewNamedResource("test", "t2") + // x1 := r1.(*resources.TestRes) + // x2 := r2.(*resources.TestRes) + // s1, s2 := "value is: hello", "value is: 42" + // x1.StringPtr = &s1 + // x2.StringPtr = &s2 + // graph.AddVertex(x1, x2) + // values = append(values, test{ + // name: "double include different printf types allowed", + // code: ` + // class c1($a, $b) { + // test $a { + // stringptr => printf("value is: %v", $b), + // } + // } + // include c1("t1", "hello") + // include c1("t2", 42) + // `, + // fail: false, + // graph: graph, + // }) + //} + { + graph, _ := pgraph.NewGraph("g") + r1, _ := engine.NewNamedResource("test", "t1") + r2, _ := engine.NewNamedResource("test", "t2") + x1 := r1.(*resources.TestRes) + x2 := r2.(*resources.TestRes) + s1, s2 := "hey", "hey" + x1.StringPtr = &s1 + x2.StringPtr = &s2 + graph.AddVertex(x1, x2) + values = append(values, test{ + name: "double include with variable in parent scope", + code: ` + $foo = "hey" + class c1($a) { + test $a { + stringptr => $foo, + } + } + include c1("t1") + include c1("t2") + `, + fail: false, + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + r1, _ := engine.NewNamedResource("test", "t1") + r2, _ := engine.NewNamedResource("test", "t2") + x1 := r1.(*resources.TestRes) + x2 := r2.(*resources.TestRes) + s1, s2 := "hey", "hey" + x1.StringPtr = &s1 + x2.StringPtr = &s2 + graph.AddVertex(x1, x2) + values = append(values, test{ + name: "double include with out of order variable in parent scope", + code: ` + include c1("t1") + include c1("t2") + class c1($a) { + test $a { + stringptr => $foo, + } + } + $foo = "hey" + `, + fail: false, + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + r1, _ := engine.NewNamedResource("test", "t1") + x1 := r1.(*resources.TestRes) + s1 := "hello" + x1.StringPtr = &s1 + graph.AddVertex(x1) + values = append(values, test{ + name: "duplicate include identical", + code: ` + include c1("t1", "hello") + class c1($a, $b) { + test $a { + stringptr => $b, + } + } + include c1("t1", "hello") # this is an identical dupe + `, + fail: false, + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + r1, _ := engine.NewNamedResource("test", "t1") + x1 := r1.(*resources.TestRes) + s1 := "hello" + x1.StringPtr = &s1 + graph.AddVertex(x1) + values = append(values, test{ + name: "duplicate include non-identical", + code: ` + include c1("t1", "hello") + class c1($a, $b) { + if $a == "t1" { + test $a { + stringptr => $b, + } + } else { + test "t1" { + stringptr => $b, + } + } + } + include c1("t?", "hello") # should cause an identical dupe + `, + fail: false, + graph: graph, + }) + } + { + values = append(values, test{ + name: "duplicate include incompatible", + code: ` + include c1("t1", "hello") + class c1($a, $b) { + test $a { + stringptr => $b, + } + } + include c1("t1", "world") # incompatible + `, + fail: true, // incompatible resources + }) + } + { + values = append(values, test{ + name: "class wrong number of args 1", + code: ` + include c1("hello") # missing second arg + class c1($a, $b) { + test $a { + stringptr => $b, + } + } + `, + fail: true, // but should NOT panic + }) + } + { + values = append(values, test{ + name: "class wrong number of args 2", + code: ` + include c1("hello", 42) # added second arg + class c1($a) { + test $a { + stringptr => "world", + } + } + `, + fail: true, // but should NOT panic + }) + } + { + graph, _ := pgraph.NewGraph("g") + r1, _ := engine.NewNamedResource("test", "t1") + r2, _ := engine.NewNamedResource("test", "t2") + x1 := r1.(*resources.TestRes) + x2 := r2.(*resources.TestRes) + s1, s2 := "hello is 42", "world is 13" + x1.StringPtr = &s1 + x2.StringPtr = &s2 + graph.AddVertex(x1, x2) + values = append(values, test{ + name: "nested classes 1", + code: ` + include c1("t1", "hello") # test["t1"] -> hello is 42 + include c1("t2", "world") # test["t2"] -> world is 13 + + class c1($a, $b) { + # nested class definition + class c2($c) { + test $a { + stringptr => printf("%s is %d", $b, $c), + } + } + + if $a == "t1" { + include c2(42) + } else { + include c2(13) + } + } + `, + fail: false, + graph: graph, + }) + } + { + values = append(values, test{ + name: "nested classes out of scope 1", + code: ` + include c1("t1", "hello") # test["t1"] -> hello is 42 + include c2(99) # out of scope + + class c1($a, $b) { + # nested class definition + class c2($c) { + test $a { + stringptr => printf("%s is %d", $b, $c), + } + } + + if $a == "t1" { + include c2(42) + } else { + include c2(13) + } + } + `, + fail: true, + }) + } + // TODO: recursive classes are not currently supported (should they be?) + //{ + // graph, _ := pgraph.NewGraph("g") + // values = append(values, test{ + // name: "recursive classes 0", + // code: ` + // include c1(0) # start at zero + // class c1($count) { + // if $count != 3 { + // include c1($count + 1) + // } + // } + // `, + // fail: false, + // graph: graph, // produces no output + // }) + //} + // TODO: recursive classes are not currently supported (should they be?) + //{ + // graph, _ := pgraph.NewGraph("g") + // r0, _ := engine.NewNamedResource("test", "done") + // x0 := r0.(*resources.TestRes) + // s0 := "count is 3" + // x0.StringPtr = &s0 + // graph.AddVertex(x0) + // values = append(values, test{ + // name: "recursive classes 1", + // code: ` + // $max = 3 + // include c1(0) # start at zero + // # test["done"] -> count is 3 + // class c1($count) { + // if $count == $max { + // test "done" { + // stringptr => printf("count is %d", $count), + // } + // } else { + // include c1($count + 1) + // } + // } + // `, + // fail: false, + // graph: graph, + // }) + //} + // TODO: recursive classes are not currently supported (should they be?) + //{ + // graph, _ := pgraph.NewGraph("g") + // r0, _ := engine.NewNamedResource("test", "zero") + // r1, _ := engine.NewNamedResource("test", "ix:1") + // r2, _ := engine.NewNamedResource("test", "ix:2") + // r3, _ := engine.NewNamedResource("test", "ix:3") + // x0 := r0.(*resources.TestRes) + // x1 := r1.(*resources.TestRes) + // x2 := r2.(*resources.TestRes) + // x3 := r3.(*resources.TestRes) + // s0, s1, s2, s3 := "count is 0", "count is 1", "count is 2", "count is 3" + // x0.StringPtr = &s0 + // x1.StringPtr = &s1 + // x2.StringPtr = &s2 + // x3.StringPtr = &s3 + // graph.AddVertex(x0, x1, x2, x3) + // values = append(values, test{ + // name: "recursive classes 2", + // code: ` + // include c1("ix", 3) + // # test["ix:3"] -> count is 3 + // # test["ix:2"] -> count is 2 + // # test["ix:1"] -> count is 1 + // # test["zero"] -> count is 0 + // class c1($name, $count) { + // if $count == 0 { + // test "zero" { + // stringptr => printf("count is %d", $count), + // } + // } else { + // include c1($name, $count - 1) + // test "${name}:${count}" { + // stringptr => printf("count is %d", $count), + // } + // } + // } + // `, + // fail: false, + // graph: graph, + // }) + //} for index, test := range values { // run all the tests name, code, fail, exp := test.name, test.code, test.fail, test.graph diff --git a/lang/lexer.nex b/lang/lexer.nex index 5bb5d0de..bef890cd 100644 --- a/lang/lexer.nex +++ b/lang/lexer.nex @@ -174,6 +174,16 @@ lval.str = yylex.Text() return STRUCT_IDENTIFIER } +/class/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return CLASS_IDENTIFIER + } +/include/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return INCLUDE_IDENTIFIER + } /variant/ { yylex.pos(lval) // our pos lval.str = yylex.Text() diff --git a/lang/lexparse_test.go b/lang/lexparse_test.go index 75152b1c..d8c5fde0 100644 --- a/lang/lexparse_test.go +++ b/lang/lexparse_test.go @@ -25,6 +25,7 @@ import ( "testing" "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" "github.com/davecgh/go-spew/spew" ) @@ -859,6 +860,195 @@ func TestLexParse0(t *testing.T) { fail: true, }) } + { + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtClass{ + Name: "c1", + Body: &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "t1", + }, + Contents: []StmtResContents{ + &StmtResField{ + Field: "stringptr", + Value: &ExprStr{ + V: "hello", + }, + }, + }, + }, + }, + }, + }, + &StmtInclude{ + Name: "c1", + }, + }, + } + values = append(values, test{ + name: "simple class 1", + code: ` + class c1 { + test "t1" { + stringptr => "hello", + } + } + include c1 + `, + fail: false, + exp: exp, + }) + } + { + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtClass{ + Name: "c1", + Args: []*Arg{ + { + Name: "a", + //Type: &types.Type{}, + }, + { + Name: "b", + //Type: &types.Type{}, + }, + }, + Body: &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprVar{ + Name: "a", + }, + Contents: []StmtResContents{ + &StmtResField{ + Field: "stringptr", + Value: &ExprVar{ + Name: "b", + }, + }, + }, + }, + }, + }, + }, + &StmtInclude{ + Name: "c1", + Args: []interfaces.Expr{ + &ExprStr{ + V: "t1", + }, + &ExprStr{ + V: "hello", + }, + }, + }, + &StmtInclude{ + Name: "c1", + Args: []interfaces.Expr{ + &ExprStr{ + V: "t2", + }, + &ExprStr{ + V: "world", + }, + }, + }, + }, + } + values = append(values, test{ + name: "simple class with args 1", + code: ` + class c1($a, $b) { + test $a { + stringptr => $b, + } + } + include c1("t1", "hello") + include c1("t2", "world") + `, + fail: false, + exp: exp, + }) + } + { + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtClass{ + Name: "c1", + Args: []*Arg{ + { + Name: "a", + Type: types.TypeStr, + }, + { + Name: "b", + //Type: &types.Type{}, + }, + }, + Body: &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprVar{ + Name: "a", + }, + Contents: []StmtResContents{ + &StmtResField{ + Field: "stringptr", + Value: &ExprVar{ + Name: "b", + }, + }, + }, + }, + }, + }, + }, + &StmtInclude{ + Name: "c1", + Args: []interfaces.Expr{ + &ExprStr{ + V: "t1", + }, + &ExprStr{ + V: "hello", + }, + }, + }, + &StmtInclude{ + Name: "c1", + Args: []interfaces.Expr{ + &ExprStr{ + V: "t2", + }, + &ExprStr{ + V: "world", + }, + }, + }, + }, + } + values = append(values, test{ + name: "simple class with typed args 1", + code: ` + class c1($a str, $b) { + test $a { + stringptr => $b, + } + } + include c1("t1", "hello") + include c1("t2", "world") + `, + fail: false, + exp: exp, + }) + } for index, test := range values { // run all the tests name, code, fail, exp := test.name, test.code, test.fail, test.exp diff --git a/lang/parser.y b/lang/parser.y index 18c3e581..556104ac 100644 --- a/lang/parser.y +++ b/lang/parser.y @@ -63,6 +63,9 @@ func init() { structFields []*ExprStructField structField *ExprStructField + args []*Arg + arg *Arg + resContents []StmtResContents // interface resField *StmtResField resEdge *StmtResEdge @@ -82,6 +85,7 @@ func init() { %token STR_IDENTIFIER BOOL_IDENTIFIER INT_IDENTIFIER FLOAT_IDENTIFIER %token STRUCT_IDENTIFIER VARIANT_IDENTIFIER VAR_IDENTIFIER IDENTIFIER %token VAR_IDENTIFIER_HX CAPITALIZED_IDENTIFIER +%token CLASS_IDENTIFIER INCLUDE_IDENTIFIER %token COMMENT ERROR // precedence table @@ -185,6 +189,44 @@ stmt: ElseBranch: $8.stmt, } } + // `class name { }` +| CLASS_IDENTIFIER IDENTIFIER OPEN_CURLY prog CLOSE_CURLY + { + posLast(yylex, yyDollar) // our pos + $$.stmt = &StmtClass{ + Name: $2.str, + Args: nil, + Body: $4.stmt, + } + } + // `class name() { }` + // `class name(, ) { }` +| CLASS_IDENTIFIER IDENTIFIER OPEN_PAREN args CLOSE_PAREN OPEN_CURLY prog CLOSE_CURLY + { + posLast(yylex, yyDollar) // our pos + $$.stmt = &StmtClass{ + Name: $2.str, + Args: $4.args, + Body: $7.stmt, + } + } + // `include name` +| INCLUDE_IDENTIFIER IDENTIFIER + { + posLast(yylex, yyDollar) // our pos + $$.stmt = &StmtInclude{ + Name: $2.str, + } + } + // `include name(...)` +| INCLUDE_IDENTIFIER IDENTIFIER OPEN_PAREN call_args CLOSE_PAREN + { + posLast(yylex, yyDollar) // our pos + $$.stmt = &StmtInclude{ + Name: $2.str, + Args: $4.exprs, + } + } /* // resource bind | rbind @@ -596,6 +638,7 @@ call: } ; // list order gets us the position of the arg, but named params would work too! +// this is also used by the include statement when the called class uses args! call_args: /* end of list */ { @@ -623,6 +666,40 @@ var: } } ; +args: + /* end of list */ + { + posLast(yylex, yyDollar) // our pos + $$.args = []*Arg{} + } +| args COMMA arg + { + posLast(yylex, yyDollar) // our pos + $$.args = append($1.args, $3.arg) + } +| arg + { + posLast(yylex, yyDollar) // our pos + $$.args = append([]*Arg{}, $1.arg) + } +; +arg: + // `$x` + VAR_IDENTIFIER + { + $$.arg = &Arg{ + Name: $1.str, + } + } + // `$x ` +| VAR_IDENTIFIER type + { + $$.arg = &Arg{ + Name: $1.str, + Type: $2.typ, + } + } +; bind: VAR_IDENTIFIER EQUALS expr { diff --git a/lang/structs.go b/lang/structs.go index a1f8284b..43524cb9 100644 --- a/lang/structs.go +++ b/lang/structs.go @@ -61,7 +61,7 @@ type StmtBind struct { Value interfaces.Expr } -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. func (obj *StmtBind) Interpolate() (interfaces.Stmt, error) { @@ -125,7 +125,7 @@ type StmtRes struct { 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 (aka a copy) 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. func (obj *StmtRes) Interpolate() (interfaces.Stmt, error) { @@ -512,7 +512,7 @@ type StmtResField struct { 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 +// Interpolate returns a new node (aka a copy) 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 @@ -647,7 +647,7 @@ type StmtResEdge struct { 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 +// Interpolate returns a new node (aka a copy) 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 @@ -764,7 +764,7 @@ type StmtEdge struct { Notify bool // specifies that this edge sends a notification as well } -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. // TODO: could we expand the Name's from the EdgeHalf (if they're lists) to have @@ -908,7 +908,7 @@ type StmtEdgeHalf struct { 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 +// Interpolate returns a new node (aka a copy) 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 @@ -983,7 +983,7 @@ type StmtIf struct { ElseBranch interfaces.Stmt // optional } -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. func (obj *StmtIf) Interpolate() (interfaces.Stmt, error) { @@ -1152,7 +1152,7 @@ type StmtProg struct { Prog []interfaces.Stmt } -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. func (obj *StmtProg) Interpolate() (interfaces.Stmt, error) { @@ -1194,6 +1194,24 @@ func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { newScope.Variables[bind.Ident] = bind.Value } + // now collect any classes + // TODO: if we ever allow poly classes, then group in lists by name + classes := make(map[string]struct{}) + for _, x := range obj.Prog { + class, ok := x.(*StmtClass) + if !ok { + continue + } + // check for duplicates *in this scope* + if _, exists := classes[class.Name]; exists { + return fmt.Errorf("class `%s` already exists in this scope", class.Name) + } + + classes[class.Name] = struct{}{} // mark as found in scope + // add to scope, (overwriting, aka shadowing is ok) + newScope.Classes[class.Name] = class + } + // now set the child scopes (even on bind...) for _, x := range obj.Prog { if err := x.SetScope(newScope); err != nil { @@ -1212,6 +1230,11 @@ func (obj *StmtProg) Unify() ([]interfaces.Invariant, error) { // collect all the invariants of each sub-expression for _, x := range obj.Prog { + // skip over *StmtClass here + if _, ok := x.(*StmtClass); ok { + continue + } + invars, err := x.Unify() if err != nil { return nil, err @@ -1236,6 +1259,11 @@ func (obj *StmtProg) Graph() (*pgraph.Graph, error) { // collect all graphs that need to be included for _, x := range obj.Prog { + // skip over *StmtClass here + if _, ok := x.(*StmtClass); ok { + continue + } + g, err := x.Graph() if err != nil { return nil, err @@ -1255,6 +1283,13 @@ func (obj *StmtProg) Output() (*interfaces.Output, error) { edges := []*interfaces.Edge{} for _, stmt := range obj.Prog { + // skip over *StmtClass here so its Output method can be used... + if _, ok := stmt.(*StmtClass); ok { + // don't read output from StmtClass, it + // gets consumed by StmtInclude instead + continue + } + output, err := stmt.Output() if err != nil { return nil, err @@ -1271,6 +1306,229 @@ func (obj *StmtProg) Output() (*interfaces.Output, error) { }, nil } +// StmtClass represents a user defined class. It's effectively a program body +// that can optionally take some parameterized inputs. +// TODO: We don't currently support defining polymorphic classes (eg: different +// signatures for the same class name) but it might be something to consider. +type StmtClass struct { + Name string + Args []*Arg + Body interfaces.Stmt // probably a *StmtProg +} + +// Interpolate returns a new node (aka a copy) 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. +func (obj *StmtClass) Interpolate() (interfaces.Stmt, error) { + interpolated, err := obj.Body.Interpolate() + if err != nil { + return nil, err + } + + args := obj.Args + if obj.Args == nil { + args = []*Arg{} + } + + return &StmtClass{ + Name: obj.Name, + Args: args, // ensure this has length == 0 instead of nil + Body: interpolated, + }, nil +} + +// SetScope sets the scope of the child expression bound to it. It seems this is +// necessary in order to reach this, in particular in situations when a bound +// expression points to a previously bound expression. +func (obj *StmtClass) SetScope(scope *interfaces.Scope) error { + return obj.Body.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 *StmtClass) Unify() ([]interfaces.Invariant, error) { + if obj.Name == "" { + return nil, fmt.Errorf("missing class name") + } + + // TODO: do we need to add anything else here because of the obj.Args ? + return obj.Body.Unify() +} + +// 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. This particular func statement adds its linked expression to +// the graph. +func (obj *StmtClass) Graph() (*pgraph.Graph, error) { + return obj.Body.Graph() +} + +// Output for the class statement produces no output. Any values of interest +// 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() +} + +// StmtInclude causes a user defined class to get used. It's effectively the way +// to call a class except that it produces output instead of a value. Most of +// the interesting logic for classes happens here or in StmtProg. +type StmtInclude struct { + class *StmtClass // copy of class that we're using + + Name string + Args []interfaces.Expr +} + +// Interpolate returns a new node (aka a copy) 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. +func (obj *StmtInclude) Interpolate() (interfaces.Stmt, error) { + args := []interfaces.Expr{} + if obj.Args != nil { + for _, x := range obj.Args { + interpolated, err := x.Interpolate() + if err != nil { + return nil, err + } + args = append(args, interpolated) + } + } + + return &StmtInclude{ + Name: obj.Name, + Args: args, + }, nil +} + +// SetScope stores the scope for use in this statement. +func (obj *StmtInclude) SetScope(scope *interfaces.Scope) error { + if scope == nil { + scope = scope.Empty() + } + + stmt, exists := scope.Classes[obj.Name] + if !exists { + return fmt.Errorf("class `%s` does not exist in this scope", obj.Name) + } + class, ok := stmt.(*StmtClass) + if !ok { + return fmt.Errorf("class scope of `%s` does not contain a class", obj.Name) + } + + // helper function to keep things more logical + cp := func(input *StmtClass) (*StmtClass, error) { + // TODO: should we have a dedicated copy method instead? because + // we want to copy some things, but not others like Expr I think + copied, err := input.Interpolate() // this sort of copies things + if err != nil { + return nil, errwrap.Wrapf(err, "could not copy class") + } + class, ok := copied.(*StmtClass) // convert it back again + if !ok { + return nil, fmt.Errorf("copied class named `%s` is not a class", obj.Name) + } + return class, nil + } + + copied, err := cp(class) // copy it for each use of the include + if err != nil { + return errwrap.Wrapf(err, "could not copy class") + } + obj.class = copied + + newScope := scope.Copy() + for i, arg := range obj.class.Args { // copy + newScope.Variables[arg.Name] = obj.Args[i] + } + if err := obj.class.SetScope(newScope); 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. +func (obj *StmtInclude) Unify() ([]interfaces.Invariant, error) { + if obj.Name == "" { + return nil, fmt.Errorf("missing include name") + } + + // is it even possible for the signatures to match? + if len(obj.class.Args) != len(obj.Args) { + return nil, fmt.Errorf("class `%s` expected %d args but got %d", obj.Name, len(obj.class.Args), len(obj.Args)) + } + + var invariants []interfaces.Invariant + + // do this here because we skip doing it in the StmtProg parent + invars, err := obj.class.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, invars...) + + // collect all the invariants of each sub-expression + for i, x := range obj.Args { + invars, err := x.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, invars...) + + // TODO: are additional invariants required? + // add invariants between the args and the class + if typ := obj.class.Args[i].Type; typ != nil { + invar := &unification.EqualsInvariant{ + Expr: obj.Args[i], + Type: typ, // type of arg + } + 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. This particular func statement adds its linked expression to +// the graph. +func (obj *StmtInclude) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("include") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + + g, err := obj.class.Graph() + if err != nil { + return nil, err + } + graph.AddGraph(g) + + return graph, nil +} + +// Output returns the output that this include produces. This output is what +// 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. 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() +} + // StmtComment is a representation of a comment. It is currently unused. It // probably makes sense to make a third kind of Node (not a Stmt or an Expr) so // that comments can still be part of the AST (for eventual automatic code @@ -1280,11 +1538,15 @@ type StmtComment struct { Value string } -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. // Here it simply returns itself, as no interpolation is possible. -func (obj *StmtComment) Interpolate() (interfaces.Stmt, error) { return obj, nil } +func (obj *StmtComment) Interpolate() (interfaces.Stmt, error) { + return &StmtComment{ + Value: obj.Value, + }, nil +} // SetScope does nothing for this struct, because it has no child nodes, and it // does not need to know about the parent scope. @@ -1324,11 +1586,15 @@ type ExprBool struct { // String returns a short representation of this expression. func (obj *ExprBool) String() string { return fmt.Sprintf("bool(%t)", obj.V) } -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. // Here it simply returns itself, as no interpolation is possible. -func (obj *ExprBool) Interpolate() (interfaces.Expr, error) { return obj, nil } +func (obj *ExprBool) Interpolate() (interfaces.Expr, error) { + return &ExprBool{ + V: obj.V, + }, nil +} // SetScope does nothing for this struct, because it has no child nodes, and it // does not need to know about the parent scope. @@ -1405,7 +1671,7 @@ type ExprStr struct { // String returns a short representation of this expression. func (obj *ExprStr) String() string { return fmt.Sprintf("str(%s)", obj.V) } -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. // Here it attempts to expand the string if there are any internal variables @@ -1424,7 +1690,9 @@ func (obj *ExprStr) Interpolate() (interfaces.Expr, error) { return nil, err } if result == nil { - return obj, nil // pass self through, no changes + return &ExprStr{ + V: obj.V, + }, nil } // we got something, overwrite the existing static str return result, nil // replacement @@ -1505,11 +1773,15 @@ type ExprInt struct { // String returns a short representation of this expression. func (obj *ExprInt) String() string { return fmt.Sprintf("int(%d)", obj.V) } -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. // Here it simply returns itself, as no interpolation is possible. -func (obj *ExprInt) Interpolate() (interfaces.Expr, error) { return obj, nil } +func (obj *ExprInt) Interpolate() (interfaces.Expr, error) { + return &ExprInt{ + V: obj.V, + }, nil +} // SetScope does nothing for this struct, because it has no child nodes, and it // does not need to know about the parent scope. @@ -1588,11 +1860,15 @@ func (obj *ExprFloat) String() string { return fmt.Sprintf("float(%g)", obj.V) // TODO: %f instead? } -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. // Here it simply returns itself, as no interpolation is possible. -func (obj *ExprFloat) Interpolate() (interfaces.Expr, error) { return obj, nil } +func (obj *ExprFloat) Interpolate() (interfaces.Expr, error) { + return &ExprFloat{ + V: obj.V, + }, nil +} // SetScope does nothing for this struct, because it has no child nodes, and it // does not need to know about the parent scope. @@ -1678,7 +1954,7 @@ func (obj *ExprList) String() string { return fmt.Sprintf("list(%s)", strings.Join(s, ", ")) } -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. func (obj *ExprList) Interpolate() (interfaces.Expr, error) { @@ -1923,7 +2199,7 @@ func (obj *ExprMap) String() string { return fmt.Sprintf("map(%s)", strings.Join(s, ", ")) } -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. func (obj *ExprMap) Interpolate() (interfaces.Expr, error) { @@ -2263,7 +2539,7 @@ func (obj *ExprStruct) String() string { return fmt.Sprintf("struct(%s)", strings.Join(s, "; ")) } -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. func (obj *ExprStruct) Interpolate() (interfaces.Expr, error) { @@ -2521,11 +2797,15 @@ type ExprFunc struct { // we have a better printable function value and put that here instead. func (obj *ExprFunc) String() string { return fmt.Sprintf("func(???)") } // TODO: print nicely -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. // Here it simply returns itself, as no interpolation is possible. -func (obj *ExprFunc) Interpolate() (interfaces.Expr, error) { return obj, nil } +func (obj *ExprFunc) Interpolate() (interfaces.Expr, error) { + return &ExprFunc{ + V: obj.V, + }, nil +} // SetScope does nothing for this struct, because it has no child nodes, and it // does not need to know about the parent scope. @@ -2677,7 +2957,7 @@ func (obj *ExprCall) buildFunc() (interfaces.Func, error) { return fn, nil } -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. func (obj *ExprCall) Interpolate() (interfaces.Expr, error) { @@ -3052,12 +3332,16 @@ type ExprVar struct { // String returns a short representation of this expression. func (obj *ExprVar) String() string { return fmt.Sprintf("var(%s)", obj.Name) } -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. // Here it returns itself, since variable names cannot be interpolated. We don't // support variable, variables or anything crazy like that. -func (obj *ExprVar) Interpolate() (interfaces.Expr, error) { return obj, nil } +func (obj *ExprVar) Interpolate() (interfaces.Expr, error) { + return &ExprVar{ + Name: obj.Name, + }, nil +} // SetScope stores the scope for use in this resource. func (obj *ExprVar) SetScope(scope *interfaces.Scope) error { @@ -3249,6 +3533,13 @@ func (obj *ExprVar) Value() (types.Value, error) { return expr.Value() // recurse } +// Arg represents a name identifier for a func or class argument declaration and +// is sometimes accompanied by a type. This does not satisfy the Expr interface. +type Arg struct { + Name string + Type *types.Type // nil if unspecified (needs to be solved for) +} + // ExprIf represents an if expression which *must* have both branches, and which // returns a value. As a result, it has a type. This is different from a StmtIf, // which does not need to have both branches, and which does not return a value. @@ -3265,7 +3556,7 @@ func (obj *ExprIf) String() string { return fmt.Sprintf("if(%s)", obj.Condition.String()) // TODO: improve this } -// Interpolate returns a new node (or itself) once it has been expanded. This +// Interpolate returns a new node (aka a copy) 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. func (obj *ExprIf) Interpolate() (interfaces.Expr, error) {