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:
James Shubin
2024-01-12 17:49:53 -05:00
parent 741a71b490
commit aae0e16350
9 changed files with 235 additions and 3 deletions

View File

@@ -402,6 +402,38 @@ time.
Recursive classes are not currently supported and it is not clear if they will 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. 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 #### Include
The `include` statement causes the previously defined class to produce the 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 be better represented by a larger AST. Most nodes in the AST simply return their
own node address, and do not modify the AST. 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
Scope propagation passes the parent scope (starting with the top-level, built-in Scope propagation passes the parent scope (starting with the top-level, built-in

View 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 {}

View 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 {}

View File

@@ -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 // 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 // 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. // 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) { 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{} body := []interfaces.Stmt{}
for _, x := range obj.Body { for _, x := range obj.Body {
interpolated, err := x.Interpolate() interpolated, err := x.Interpolate()

View File

@@ -23,6 +23,10 @@ const (
// It is also used for variable scope separation such as `$foo.bar.baz`. // It is also used for variable scope separation such as `$foo.bar.baz`.
ModuleSep = "." 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 // VarPrefix is the prefix character that precedes the variables
// identifier. For example, `$foo` or for a lambda, `$fn(42)`. It is // identifier. For example, `$foo` or for a lambda, `$fn(42)`. It is
// also used with `ModuleSep` for scoped variables like `$foo.bar.baz`. // also used with `ModuleSep` for scoped variables like `$foo.bar.baz`.

View File

@@ -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]

View File

@@ -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]

View File

@@ -62,6 +62,10 @@
/:/ { /:/ {
yylex.pos(lval) // our pos yylex.pos(lval) // our pos
lval.str = yylex.Text() 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 return COLON
} }
/;/ { /;/ {

View File

@@ -257,7 +257,7 @@ stmt:
} }
} }
// `class name { <prog> }` // `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 posLast(yylex, yyDollar) // our pos
$$.stmt = &ast.StmtClass{ $$.stmt = &ast.StmtClass{
@@ -268,7 +268,7 @@ stmt:
} }
// `class name(<arg>) { <prog> }` // `class name(<arg>) { <prog> }`
// `class name(<arg>, <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 posLast(yylex, yyDollar) // our pos
$$.stmt = &ast.StmtClass{ $$.stmt = &ast.StmtClass{
@@ -1408,7 +1408,7 @@ colon_identifier:
posLast(yylex, yyDollar) // our pos posLast(yylex, yyDollar) // our pos
$$.str = $1.str $$.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 | colon_identifier COLON IDENTIFIER
{ {
posLast(yylex, yyDollar) // our pos posLast(yylex, yyDollar) // our pos