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.
This commit is contained in:
@@ -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
|
||||
|
||||
19
examples/lang/class-include-nested0.mcl
Normal file
19
examples/lang/class-include-nested0.mcl
Normal file
@@ -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 {}
|
||||
36
examples/lang/class-include-nested1.mcl
Normal file
36
examples/lang/class-include-nested1.mcl
Normal file
@@ -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 {}
|
||||
@@ -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()
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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
|
||||
}
|
||||
/;/ {
|
||||
|
||||
@@ -257,7 +257,7 @@ stmt:
|
||||
}
|
||||
}
|
||||
// `class name { <prog> }`
|
||||
| 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(<arg>) { <prog> }`
|
||||
// `class name(<arg>, <arg>) { <prog> }`
|
||||
| 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
|
||||
|
||||
Reference in New Issue
Block a user