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) {