From aae0e163503fd870b85d251c0cfd358c6b6a20bf Mon Sep 17 00:00:00 2001 From: James Shubin Date: Fri, 12 Jan 2024 17:49:53 -0500 Subject: [PATCH] lang: ast, parser, interfaces: Implementation of nested class sugar This implements a new type of syntactic sugar for the common pattern of a base class which returns a child class, and so on. 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: class base() { class inner() { class deepest() { } } } You can instead write: 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. --- docs/language-guide.md | 36 +++++++++++ examples/lang/class-include-nested0.mcl | 19 ++++++ examples/lang/class-include-nested1.mcl | 36 +++++++++++ lang/ast/structs.go | 60 +++++++++++++++++++ lang/interfaces/const.go | 4 ++ .../class-include-as-nested0.txtar | 26 ++++++++ .../class-include-as-nested1.txtar | 47 +++++++++++++++ lang/parser/lexer.nex | 4 ++ lang/parser/parser.y | 6 +- 9 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 examples/lang/class-include-nested0.mcl create mode 100644 examples/lang/class-include-nested1.mcl create mode 100644 lang/interpret_test/TestAstFunc2/class-include-as-nested0.txtar create mode 100644 lang/interpret_test/TestAstFunc2/class-include-as-nested1.txtar 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