diff --git a/docs/language-guide.md b/docs/language-guide.md index 81311276..ffdace5f 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -402,6 +402,38 @@ 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. +A class names can contain colons to indicate it is nested inside of the class in +the same scope which is named with the prefix indicated by colon separation. +Instead of needing to repeatedly indent the child classes, we can instead prefix +them at the definition site (where created with the class keyword) with the name +of the parent class, followed by a colon, to get the desired embedded sugar. + +For example, instead of writing: + +```mcl +class base() { + class inner() { + class deepest() { + } + } +} +``` + +You can instead write: + +```mcl +class base() { +} +class base:inner() { +} +class base:inner:deepest() { +} +``` + +Of course, you can only access any of the inner classes by first including +(with the include keyword) a parent class, and then subsequently including the +inner one. + #### Include The `include` statement causes the previously defined class to produce the @@ -553,6 +585,10 @@ and can be used for other scenarios in which one statement or expression would be better represented by a larger AST. Most nodes in the AST simply return their own node address, and do not modify the AST. +This stage also implements the class nesting when it finds class names with +colons that should be nested inside of a base class. Currently this does modify +the AST for efficiency and simplicity. + #### Scope propagation Scope propagation passes the parent scope (starting with the top-level, built-in diff --git a/examples/lang/class-include-nested0.mcl b/examples/lang/class-include-nested0.mcl new file mode 100644 index 00000000..8955becf --- /dev/null +++ b/examples/lang/class-include-nested0.mcl @@ -0,0 +1,19 @@ +$top = "top-level" +class base($s) { + test "middle " + $s {} + $middle = "inside base" +} + +# syntactic sugar for the equivalent of defining a class `inner` inside of base. +class base:inner($s) { + test "inner " + $s {} + + $last = "i am inner and i can see " + $middle +} + +include base("world") as b1 +include b1.inner("hello") as b2 # inner comes out of `base` + +test $top {} +test $b1.middle {} +test $b2.last {} diff --git a/examples/lang/class-include-nested1.mcl b/examples/lang/class-include-nested1.mcl new file mode 100644 index 00000000..8d2df250 --- /dev/null +++ b/examples/lang/class-include-nested1.mcl @@ -0,0 +1,36 @@ +$top = "top-level" +class base($s) { + test "middle " + $s {} + $middle = "inside base" +} + +# syntactic sugar for the equivalent of defining a class `inner` inside of base. +class base:inner1($s) { + test "inner1 " + $s {} + + $last = "i am inner1 and i can see " + $middle +} + +class base:inner2($s) { + test "inner2 " + $s {} + + $last = "i am inner2 and i can see " + $middle +} + +# three deep! +class base:inner1:deep($s, $b) { + test "deep is " + $s {} + + $end = "i am deep and i can see " + $middle + " and last says " + $last +} + +include base("world") as b0 +include b0.inner1("hello") as b1 # inner comes out of `base` +include b0.inner2("hello") as b2 # inner comes out of `base` +include b1.deep("deep", true) as d # deep comes out of `inner1` + +test $top {} +test $b0.middle {} +test $b1.last {} +test $b2.last {} +test $d.end {} diff --git a/lang/ast/structs.go b/lang/ast/structs.go index f52998d0..0d6906bf 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -2921,7 +2921,67 @@ func (obj *StmtProg) Init(data *interfaces.Data) error { // 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 particular implementation can currently modify the source AST in-place, +// and then finally return a copy. This isn't ideal, but it is much more optimal +// as it avoids a lot of copying, and the code is simpler. If we need our AST to +// be static, then we can improve this. func (obj *StmtProg) Interpolate() (interfaces.Stmt, error) { + // First, make a list of class name to class pointer. + classes := make(map[string]*StmtClass) + for _, x := range obj.Body { + stmt, ok := x.(*StmtClass) + if !ok { + continue + } + if _, exists := classes[stmt.Name]; exists { + return nil, fmt.Errorf("duplicate class name of: `%s`", stmt.Name) + } + // if it contains a colon we could skip it (perf busy work) + //if strings.Contains(stmt.Name, interfaces.ClassSep) { + // continue + //} + classes[stmt.Name] = stmt + } + + // Now, loop through (in reverse so that remove will work without + // breaking the index offset) the body and pull any colon prefixed class + // into the base class that it belongs inside. We also rename it to pop + // off the front prefix name once it's inside the new base class. This + // is all syntactic sugar to implement the class child nesting. + for i := len(obj.Body) - 1; i >= 0; i-- { // reverse order for remove + stmt, ok := obj.Body[i].(*StmtClass) + if !ok || stmt.Name == "" { + continue + } + + // equivalent to: strings.Contains(stmt.Name, interfaces.ClassSep) + split := strings.Split(stmt.Name, interfaces.ClassSep) + if len(split) == 0 || len(split) == 1 { + continue + } + if split[0] == "" { // prefix, eg: `:foo:bar` + return nil, fmt.Errorf("class name prefix is empty") + } + + class, exists := classes[split[0]] + if !exists { + continue + } + prog, ok := class.Body.(*StmtProg) // probably a *StmtProg + if !ok { + // TODO: print warning or error? + continue + } + + // It's not ideal to modify things here, but we do since it's so + // much easier and faster to do it like this. We can use copies + // if it turns out we need to preserve the original input AST. + stmt.Name = strings.Join(split[1:], interfaces.ClassSep) // new name w/o prefix + prog.Body = append(prog.Body, stmt) // append it to child body + obj.Body = append(obj.Body[:i], obj.Body[i+1:]...) // remove it (from the end) + } + + // Now perform the normal recursive interpolation calls. body := []interfaces.Stmt{} for _, x := range obj.Body { interpolated, err := x.Interpolate() diff --git a/lang/interfaces/const.go b/lang/interfaces/const.go index 45761c49..66eac257 100644 --- a/lang/interfaces/const.go +++ b/lang/interfaces/const.go @@ -23,6 +23,10 @@ const ( // It is also used for variable scope separation such as `$foo.bar.baz`. ModuleSep = "." + // ClassSep is the character used for the class embedding separation. + // For example when defining `class base:inner` this is the char used. + ClassSep = ":" + // VarPrefix is the prefix character that precedes the variables // identifier. For example, `$foo` or for a lambda, `$fn(42)`. It is // also used with `ModuleSep` for scoped variables like `$foo.bar.baz`. diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-nested0.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-nested0.txtar new file mode 100644 index 00000000..c5fd2215 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-nested0.txtar @@ -0,0 +1,26 @@ +-- main.mcl -- +$top = "top-level" +class base($s) { + test "middle " + $s {} + $middle = "inside base" +} + +# syntactic sugar for the equivalent of defining a class `inner` inside of base. +class base:inner($s) { + test "inner " + $s {} + + $last = "i am inner and i can see " + $middle +} + +include base("world") as b1 +include b1.inner("hello") as b2 # inner comes out of `base` + +test $top {} +test $b1.middle {} +test $b2.last {} +-- OUTPUT -- +Vertex: test[inner hello] +Vertex: test[middle world] +Vertex: test[top-level] +Vertex: test[inside base] +Vertex: test[i am inner and i can see inside base] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-nested1.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-nested1.txtar new file mode 100644 index 00000000..9cc313d2 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-nested1.txtar @@ -0,0 +1,47 @@ +-- main.mcl -- +$top = "top-level" +class base($s) { + test "middle " + $s {} + $middle = "inside base" +} + +# syntactic sugar for the equivalent of defining a class `inner` inside of base. +class base:inner1($s) { + test "inner1 " + $s {} + + $last = "i am inner1 and i can see " + $middle +} + +class base:inner2($s) { + test "inner2 " + $s {} + + $last = "i am inner2 and i can see " + $middle +} + +# three deep! +class base:inner1:deep($s, $b) { + test "deep is " + $s {} + + $end = "i am deep and i can see " + $middle + " and last says " + $last +} + +include base("world") as b0 +include b0.inner1("hello") as b1 # inner comes out of `base` +include b0.inner2("hello") as b2 # inner comes out of `base` +include b1.deep("deep", true) as d # deep comes out of `inner1` + +test $top {} +test $b0.middle {} +test $b1.last {} +test $b2.last {} +test $d.end {} +-- OUTPUT -- +Vertex: test[deep is deep] +Vertex: test[i am deep and i can see inside base and last says i am inner1 and i can see inside base] +Vertex: test[i am inner1 and i can see inside base] +Vertex: test[i am inner2 and i can see inside base] +Vertex: test[inner1 hello] +Vertex: test[inner2 hello] +Vertex: test[inside base] +Vertex: test[middle world] +Vertex: test[top-level] diff --git a/lang/parser/lexer.nex b/lang/parser/lexer.nex index 1a9d1580..d69dd968 100644 --- a/lang/parser/lexer.nex +++ b/lang/parser/lexer.nex @@ -62,6 +62,10 @@ /:/ { yylex.pos(lval) // our pos lval.str = yylex.Text() + // sanity check... these should be the same! + if x, y := lval.str, interfaces.ClassSep; x != y { + panic(fmt.Sprintf("COLON does not match ClassSep (%s != %s)", x, y)) + } return COLON } /;/ { diff --git a/lang/parser/parser.y b/lang/parser/parser.y index cef04b26..0f24f20a 100644 --- a/lang/parser/parser.y +++ b/lang/parser/parser.y @@ -257,7 +257,7 @@ stmt: } } // `class name { }` -| CLASS_IDENTIFIER IDENTIFIER OPEN_CURLY prog CLOSE_CURLY +| CLASS_IDENTIFIER colon_identifier OPEN_CURLY prog CLOSE_CURLY { posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtClass{ @@ -268,7 +268,7 @@ stmt: } // `class name() { }` // `class name(, ) { }` -| CLASS_IDENTIFIER IDENTIFIER OPEN_PAREN args CLOSE_PAREN OPEN_CURLY prog CLOSE_CURLY +| CLASS_IDENTIFIER colon_identifier OPEN_PAREN args CLOSE_PAREN OPEN_CURLY prog CLOSE_CURLY { posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtClass{ @@ -1408,7 +1408,7 @@ colon_identifier: posLast(yylex, yyDollar) // our pos $$.str = $1.str } - // eg: `foo:bar` (used in `docker:image`) + // eg: `foo:bar` (used in `docker:image` or `class base:inner:deeper`) | colon_identifier COLON IDENTIFIER { posLast(yylex, yyDollar) // our pos