diff --git a/docs/language-guide.md b/docs/language-guide.md index d914cd6a..81311276 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -414,6 +414,57 @@ 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. +Classes can be included under a new scoped prefix by using the `as` field and an +identifier. When used in this manner, the captured scope of the class at its +definition site are made available in the scope of the include. Variables, +functions, and child classes are all exported. + +Variables are available in the include scope: + +```mcl +import "fmt" + +class c1 { + test "t1" {} # gets pulled out + $x = "hello" # gets exported +} +include c1 as i1 + +test "print0" { + anotherstr => fmt.printf("%s", $i1.x), # hello + onlyshow => ["AnotherStr",], # displays nicer +} +``` + +Classes are also available in the new scope: + +```mcl +import "fmt" +class c1 { + test "t1" {} # gets pulled out + $x = "hello" # gets exported + + class c0 { + test "t2" {} + $x = "goodbye" + } +} +include c1 as i1 +include i1.c0 as i0 + +test "print0" { + anotherstr => fmt.printf("%s", $i1.x), # hello + onlyshow => ["AnotherStr",], # displays nicer +} +test "print1" { + anotherstr => fmt.printf("%s", $i0.x), # goodbye + onlyshow => ["AnotherStr",], # displays nicer +} +``` + +Of course classes can be parameterized too, and those variables specified during +the `include`. + #### Import The `import` statement imports a scope into the specified namespace. A scope can diff --git a/examples/lang/class-include-as0.mcl b/examples/lang/class-include-as0.mcl new file mode 100644 index 00000000..0b31738b --- /dev/null +++ b/examples/lang/class-include-as0.mcl @@ -0,0 +1,12 @@ +import "fmt" + +class c1 { + test "t1" {} # gets pulled out + $x = "hello" # gets exported +} +include c1 as i1 + +test "print0" { + anotherstr => fmt.printf("%s", $i1.x), # hello + onlyshow => ["AnotherStr",], # displays nicer +} diff --git a/examples/lang/class-include-as1.mcl b/examples/lang/class-include-as1.mcl new file mode 100644 index 00000000..350eba10 --- /dev/null +++ b/examples/lang/class-include-as1.mcl @@ -0,0 +1,21 @@ +import "fmt" +class c1 { + test "t1" {} # gets pulled out + $x = "hello" # gets exported + + class c0 { + test "t2" {} + $x = "goodbye" + } +} +include c1 as i1 +include i1.c0 as i0 + +test "print0" { + anotherstr => fmt.printf("%s", $i1.x), # hello + onlyshow => ["AnotherStr",], # displays nicer +} +test "print1" { + anotherstr => fmt.printf("%s", $i0.x), # goodbye + onlyshow => ["AnotherStr",], # displays nicer +} diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 5c737cc2..f52998d0 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -65,6 +65,46 @@ const ( // MetaField is the prefix used to specify a meta parameter for the res. MetaField = "meta" + // AllowBareClassIncluding specifies that a simple include without an + // `as` suffix, will be pulled in under the name of the included class. + // We want this on if it turns out to be common to pull in values from + // classes. + // + // If we allow bare including of classes, then we have to also prevent + // duplicate class inclusion for many cases. For example: + // + // class c1($s) { + // test $s {} + // $x = "${s}" + // } + // include c1("hey") + // include c1("there") + // test $x {} + // + // What value should $x have? We want to import two useful `test` + // resources, but with a bare import this makes `$x` ambiguous. We'd + // have to detect this and ensure this is a compile time error to use + // it. Being able to allow compatible, duplicate classes is a key + // important feature of the language, and as a result, enabling this + // would probably be disastrous. The fact that the import statement + // allows bare imports is an ergonomic consideration that is allowed + // because duplicate imports aren't essential. As an aside, the use of + // bare imports isn't recommended because it makes it more difficult to + // know where certain things are coming from. + AllowBareClassIncluding = false + + // AllowBareIncludes specifies that you're allowed to use an include + // which flattens the included scope on top of the current scope. This + // means includes of the form: `include foo as *`. These are unlikely to + // get enabled for many reasons. + AllowBareIncludes = false + + // AllowBareImports specifies that you're allowed to use an import which + // flattens the imported scope on top of the current scope. This means + // imports of the form: `import foo as *`. These are being provisionally + // enabled, despite being less explicit and harder to parse. + AllowBareImports = true + // AllowUserDefinedPolyFunc specifies if we allow user-defined // polymorphic functions or not. At the moment this is not implemented. // XXX: not implemented @@ -101,10 +141,9 @@ const ( // classOrderingPrefix is a magic prefix used for the Ordering graph. classOrderingPrefix = "class:" - // legacyProgSetScope enables an old version of the SetScope function - // in StmtProg. Use it for experimentation if you don't want to use the - // Ordering function for some reason. In general, this should be false! - legacyProgSetScope = false + // scopedOrderingPrefix is a magic prefix used for the Ordering graph. + // It is shared between imports and include as. + scopedOrderingPrefix = "scoped:" // ErrNoStoredScope is an error that tells us we can't get a scope here. ErrNoStoredScope = interfaces.Error("scope is not stored in this node") @@ -2949,17 +2988,43 @@ func (obj *StmtProg) Ordering(produces map[string]interfaces.Node) (*pgraph.Grap prod := make(map[string]interfaces.Node) for _, x := range obj.Body { - if stmt, ok := x.(*StmtClass); ok { + if stmt, ok := x.(*StmtImport); ok { if stmt.Name == "" { return nil, nil, fmt.Errorf("missing class name") } - uid := classOrderingPrefix + stmt.Name // ordering id + uid := scopedOrderingPrefix + stmt.Name // ordering id + + if stmt.Alias == interfaces.BareSymbol { + // XXX: I think we need to parse these first... + // XXX: Somehow make sure these appear at the + // top of the topo-sort for the StmtProg... + // XXX: Maybe add edges between StmtProg and me? + continue + } + + if stmt.Alias != "" { + uid = scopedOrderingPrefix + stmt.Alias // ordering id + } + n, exists := prod[uid] if exists { return nil, nil, fmt.Errorf("duplicate assignment to `%s`, have: %s", uid, n) } prod[uid] = stmt // store } + + if stmt, ok := x.(*StmtBind); ok { + if stmt.Ident == "" { + return nil, nil, fmt.Errorf("missing bind name") + } + uid := varOrderingPrefix + stmt.Ident // ordering id + n, exists := prod[uid] + if exists { + return nil, nil, fmt.Errorf("duplicate assignment to `%s`, have: %s", uid, n) + } + prod[uid] = stmt // store + } + if stmt, ok := x.(*StmtFunc); ok { if stmt.Name == "" { return nil, nil, fmt.Errorf("missing func name") @@ -2971,11 +3036,27 @@ func (obj *StmtProg) Ordering(produces map[string]interfaces.Node) (*pgraph.Grap } prod[uid] = stmt // store } - if stmt, ok := x.(*StmtBind); ok { - if stmt.Ident == "" { - return nil, nil, fmt.Errorf("missing bind name") + + if stmt, ok := x.(*StmtClass); ok { + if stmt.Name == "" { + return nil, nil, fmt.Errorf("missing class name") } - uid := varOrderingPrefix + stmt.Ident // ordering id + uid := classOrderingPrefix + stmt.Name // ordering id + n, exists := prod[uid] + if exists { + return nil, nil, fmt.Errorf("duplicate assignment to `%s`, have: %s", uid, n) + } + prod[uid] = stmt // store + } + + if stmt, ok := x.(*StmtInclude); ok { + if stmt.Name == "" { + return nil, nil, fmt.Errorf("missing include name") + } + if stmt.Alias == "" { // not consumed + continue + } + uid := scopedOrderingPrefix + stmt.Alias // ordering id n, exists := prod[uid] if exists { return nil, nil, fmt.Errorf("duplicate assignment to `%s`, have: %s", uid, n) @@ -3482,6 +3563,8 @@ func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { newVariables := make(map[string]string) newFunctions := make(map[string]string) newClasses := make(map[string]string) + // TODO: If we added .Ordering() for *StmtImport, we could combine this + // loop with the main nodeOrder sorted topological ordering loop below! for _, x := range obj.Body { imp, ok := x.(*StmtImport) if !ok { @@ -3518,7 +3601,10 @@ func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { // TODO: do this in a deterministic (sorted) order for name, x := range importedScope.Variables { newName := alias + interfaces.ModuleSep + name - if alias == "*" { + if alias == interfaces.BareSymbol { + if !AllowBareImports { + return fmt.Errorf("bare imports disabled at compile time for import of `%s`", imp.Name) + } newName = name } if previous, exists := newVariables[newName]; exists { @@ -3530,7 +3616,10 @@ func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { } for name, x := range importedScope.Functions { newName := alias + interfaces.ModuleSep + name - if alias == "*" { + if alias == interfaces.BareSymbol { + if !AllowBareImports { + return fmt.Errorf("bare imports disabled at compile time for import of `%s`", imp.Name) + } newName = name } if previous, exists := newFunctions[newName]; exists { @@ -3542,7 +3631,10 @@ func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { } for name, x := range importedScope.Classes { newName := alias + interfaces.ModuleSep + name - if alias == "*" { + if alias == interfaces.BareSymbol { + if !AllowBareImports { + return fmt.Errorf("bare imports disabled at compile time for import of `%s`", imp.Name) + } newName = name } if previous, exists := newClasses[newName]; exists { @@ -3558,147 +3650,6 @@ func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { aliases[alias] = struct{}{} } - // collect all the bind statements in the first pass - // this allows them to appear out of order in this scope - binds := make(map[string]struct{}) // bind existence in this scope - for _, x := range obj.Body { - bind, ok := x.(*StmtBind) - if !ok { - continue - } - // check for duplicates *in this scope* - if _, exists := binds[bind.Ident]; exists { - return fmt.Errorf("var `%s` already exists in this scope", bind.Ident) - } - - binds[bind.Ident] = struct{}{} // mark as found in scope - // add to scope, (overwriting, aka shadowing is ok) - newScope.Variables[bind.Ident] = &ExprTopLevel{ - Definition: &ExprSingleton{ - Definition: bind.Value, - - mutex: &sync.Mutex{}, // TODO: call Init instead - }, - CapturedScope: newScope, - } - if obj.data.Debug { // TODO: is this message ever useful? - obj.data.Logf("prog: set scope: bind collect: (%+v): %+v (%T) is %p", bind.Ident, bind.Value, bind.Value, bind.Value) - } - } - - // now collect all the functions, and group by name (if polyfunc is ok) - functions := make(map[string][]*StmtFunc) - for _, x := range obj.Body { - fn, ok := x.(*StmtFunc) - if !ok { - continue - } - - _, exists := functions[fn.Name] - if !exists { - functions[fn.Name] = []*StmtFunc{} // initialize - } - - // check for duplicates *in this scope* - if exists && !AllowUserDefinedPolyFunc { - return fmt.Errorf("func `%s` already exists in this scope", fn.Name) - } - - // collect functions (if multiple, this is a polyfunc) - functions[fn.Name] = append(functions[fn.Name], fn) - } - - for name, fnList := range functions { - if obj.data.Debug { // TODO: is this message ever useful? - obj.data.Logf("prog: set scope: collect: (%+v -> %d): %+v (%T)", name, len(fnList), fnList[0].Func, fnList[0].Func) - } - // add to scope, (overwriting, aka shadowing is ok) - if len(fnList) == 1 { - fn := fnList[0].Func // local reference to avoid changing it in the loop... - // add to scope, (overwriting, aka shadowing is ok) - newScope.Functions[name] = &ExprPoly{ // XXX: is this ExprPoly approach optimal? - Definition: &ExprTopLevel{ - Definition: fn, // store the *ExprFunc - CapturedScope: newScope, - }, - } - continue - } - - // build polyfunc's - // XXX: not implemented - return fmt.Errorf("user-defined polyfuncs of length %d are not supported", len(fnList)) - } - - // 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.Body { - 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 - } - - obj.scope = newScope // save a reference in case we're read by an import - - // This is the legacy variant of this function that doesn't allow - // out-of-order code. It also returns obscure error messages for some - // cases, such as double-recursion. It's left here for reference. - if legacyProgSetScope { - // first set the scope on the classes, since it gets used in include... - for _, stmt := range obj.Body { - //if _, ok := stmt.(*StmtClass); !ok { - // continue - //} - _, ok1 := stmt.(*StmtClass) - _, ok2 := stmt.(*StmtFunc) // TODO: is this correct? - _, ok3 := stmt.(*StmtBind) // TODO: is this correct? - if !ok1 && !ok2 && !ok3 { // if all are false, we skip - continue - } - - if obj.data.Debug { - obj.data.Logf("prog: set scope: pass 1: %+v", stmt) - } - if err := stmt.SetScope(newScope); err != nil { - return err - } - } - - // now set the child scopes... - for _, stmt := range obj.Body { - // NOTE: We used to skip over *StmtClass here for recursion... - // Skip over *StmtClass here, since we already did it above... - if _, ok := stmt.(*StmtClass); ok { - continue - } - if _, ok := stmt.(*StmtFunc); ok { // TODO: is this correct? - continue - } - if _, ok := stmt.(*StmtBind); ok { // TODO: is this correct? - continue - } - - if obj.data.Debug { - obj.data.Logf("prog: set scope: pass 2: %+v", stmt) - } - if err := stmt.SetScope(newScope); err != nil { - return err - } - } - - return nil - } - // TODO: this could be called once at the top-level, and then cached... // TODO: it currently gets called inside child programs, which is slow! orderingGraph, _, err := obj.Ordering(nil) // XXX: pass in globals from scope? @@ -3713,22 +3664,27 @@ func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { if err := orderingGraph.ExecGraphviz("/tmp/graphviz-ordering.dot"); err != nil { obj.data.Logf("graphviz: errored: %+v", err) } + //if err := orderingGraphFiltered.ExecGraphviz("/tmp/graphviz-ordering-filtered.dot"); err != nil { + // obj.data.Logf("graphviz: errored: %+v", err) + //} // Only generate the top-level one, to prevent overwriting this! orderingGraphSingleton = false } + //nodeOrder, err := orderingGraphFiltered.TopologicalSort() nodeOrder, err := orderingGraph.TopologicalSort() if err != nil { // TODO: print the cycle in a prettier way (with names?) if obj.data.Debug { obj.data.Logf("set scope: not a dag:\n%s", orderingGraph.Sprint()) + //obj.data.Logf("set scope: not a dag:\n%s", orderingGraphFiltered.Sprint()) } return errwrap.Wrapf(err, "recursive reference while setting scope") } // XXX: implement ValidTopoSortOrder! //topoSanity := (RequireTopologicalOrdering || TopologicalOrderingWarning) - //if topoSanity && !orderingGraph.ValidTopoSortOrder(nodeOrder) { + //if topoSanity && !orderingGraphFiltered.ValidTopoSortOrder(nodeOrder) { // msg := "code is out of order, you're insane!" // if TopologicalOrderingWarning { // obj.data.Logf(msg) @@ -3770,12 +3726,29 @@ func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { obj.data.Logf("prog: set scope: ordering: %+v", stmts) } + // Track all the bind statements, functions, and classes. This is used + // for duplicate checking. These might appear out-of-order as code, but + // are iterated in the topoligically sorted node order. When we collect + // all the functions, we group by name (if polyfunc is ok) and we also + // do something similar for classes. + // TODO: if we ever allow poly classes, then group in lists by name + binds := make(map[string]struct{}) // bind existence in this scope + functions := make(map[string][]*StmtFunc) + classes := make(map[string]struct{}) + //includes := make(map[string]struct{}) // duplicates are allowed + // Optimization: In addition to importantly skipping the parts of the // graph that don't belong in this StmtProg, this also causes // un-consumed statements to be skipped. As a result, this simplifies // the graph significantly in cases of unused code, because they're not // given a chance to SetScope even though they're in the StmtProg list. - for _, x := range nodeOrder { // these are in the correct order for SetScope + + // In the below loop which we iterate over in the correct scope order, + // we build up the scope (loopScope) as we go, so that subsequent uses + // of the scope include earlier definitions and scope additions. + loopScope := newScope.Copy() + funcCount := make(map[string]int) // count the occurrences of a func + for _, x := range nodeOrder { // these are in the correct order for SetScope stmt, ok := x.(interfaces.Stmt) if !ok { continue @@ -3787,13 +3760,212 @@ func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { // Skip any unwanted additions that we pulled in. continue } - if obj.data.Debug { - obj.data.Logf("prog: set scope: order: %+v", stmt) - } - if err := stmt.SetScope(newScope); err != nil { + + capturedScope := loopScope.Copy() + if err := stmt.SetScope(capturedScope); err != nil { return err } + + if bind, ok := x.(*StmtBind); ok { + // check for duplicates *in this scope* + if _, exists := binds[bind.Ident]; exists { + return fmt.Errorf("var `%s` already exists in this scope", bind.Ident) + } + + binds[bind.Ident] = struct{}{} // mark as found in scope + // add to scope, (overwriting, aka shadowing is ok) + loopScope.Variables[bind.Ident] = &ExprTopLevel{ + Definition: &ExprSingleton{ + Definition: bind.Value, + + mutex: &sync.Mutex{}, // TODO: call Init instead + }, + CapturedScope: capturedScope, + } + if obj.data.Debug { // TODO: is this message ever useful? + obj.data.Logf("prog: set scope: bind collect: (%+v): %+v (%T) is %p", bind.Ident, bind.Value, bind.Value, bind.Value) + } + + continue // optional + } + + if fn, ok := x.(*StmtFunc); ok { + _, exists := functions[fn.Name] + if !exists { + functions[fn.Name] = []*StmtFunc{} // initialize + } + + // check for duplicates *in this scope* + if exists && !AllowUserDefinedPolyFunc { + return fmt.Errorf("func `%s` already exists in this scope", fn.Name) + } + + count := 1 // XXX: number of overloaded definitions of the same name (get from ordering eventually) + funcCount[fn.Name]++ + + // collect functions (if multiple, this is a polyfunc) + functions[fn.Name] = append(functions[fn.Name], fn) + + if funcCount[fn.Name] < count { + continue // delay SetScope for later... + } + + fnList := functions[fn.Name] // []*StmtFunc + + if obj.data.Debug { // TODO: is this message ever useful? + obj.data.Logf("prog: set scope: collect: (%+v -> %d): %+v (%T)", fn.Name, len(fnList), fnList[0].Func, fnList[0].Func) + } + + // add to scope, (overwriting, aka shadowing is ok) + if len(fnList) == 1 { + f := fnList[0].Func // local reference to avoid changing it in the loop... + // add to scope, (overwriting, aka shadowing is ok) + loopScope.Functions[fn.Name] = &ExprPoly{ // XXX: is this ExprPoly approach optimal? + Definition: &ExprTopLevel{ + Definition: f, // store the *ExprFunc + CapturedScope: capturedScope, + }, + } + continue + } + + // build polyfunc's + // XXX: not implemented + return fmt.Errorf("user-defined polyfuncs of length %d are not supported", len(fnList)) + } + + if class, ok := x.(*StmtClass); ok { + // 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) + loopScope.Classes[class.Name] = class + + continue + } + + // now collect any include contents + if include, ok := x.(*StmtInclude); ok { + // We actually don't want to check for duplicates, that + // is allowed, if we `include foo as bar` twice it will + // currently not work, but if possible, we can allow it. + // check for duplicates *in this scope* + //if _, exists := includes[include.Name]; exists { + // return fmt.Errorf("include `%s` already exists in this scope", include.Name) + //} + + alias := "" + if AllowBareClassIncluding { + alias = include.Name // this is what we would call the include + } + if include.Alias != "" { // this is what the user decided as the name + alias = include.Alias // use alias if specified + } + if alias == "" { + continue // there isn't anything to do here + } + + // NOTE: This gets caught in ordering instead of here... + // deal with alias duplicates and * includes and so on... + if _, exists := aliases[alias]; exists { + // TODO: track separately to give a better error message here + return fmt.Errorf("import/include alias `%s` already exists in this scope", alias) + } + + if include.class == nil { + // programming error + return fmt.Errorf("programming error: class `%s` not found", include.Name) + } + // This includes any variable from the top-level scope + // that is visible (and captured) inside the class, and + // re-exported when included with `as`. This is the + // "tricky case", but it turns out it's better this way. + // Example: + // + // $x = "i am x" # i am now top-level + // class c1() { + // $whatever = fmt.printf("i can see: %s", $x) + // } + // include c1 as i1 + // test $i1.x {} # tricky + // test $i1.whatever {} # easy + // + // We want to allow the tricky case to prevent needing + // to write code like: `$x = $x` inside of class c1 to + // get the same effect. + + //includedScope := include.class.Body.scope // conceptually + prog, ok := include.class.Body.(*StmtProg) + if !ok { + return fmt.Errorf("programming error: prog not found in class Body") + } + // XXX: .Copy() ? + includedScope := prog.scope + + // read from stored scope which was previously saved in SetScope + // add to scope, (overwriting, aka shadowing is ok) + // rename scope values, adding the alias prefix + // check that we don't overwrite a new value from another include + // TODO: do this in a deterministic (sorted) order + for name, x := range includedScope.Variables { + newName := alias + interfaces.ModuleSep + name + if alias == interfaces.BareSymbol { // not supported by parser atm! + if !AllowBareIncludes { + return fmt.Errorf("bare includes disabled at compile time for include of `%s`", include.Name) + } + newName = name + } + if previous, exists := newVariables[newName]; exists { + // don't overwrite in same scope + return fmt.Errorf("can't squash variable `%s` from `%s` by include of `%s`", newName, previous, include.Name) + } + newVariables[newName] = include.Name + loopScope.Variables[newName] = x // merge + } + for name, x := range includedScope.Functions { + newName := alias + interfaces.ModuleSep + name + if alias == interfaces.BareSymbol { // not supported by parser atm! + if !AllowBareIncludes { + return fmt.Errorf("bare includes disabled at compile time for include of `%s`", include.Name) + } + newName = name + } + if previous, exists := newFunctions[newName]; exists { + // don't overwrite in same scope + return fmt.Errorf("can't squash function `%s` from `%s` by include of `%s`", newName, previous, include.Name) + } + newFunctions[newName] = include.Name + loopScope.Functions[newName] = x + } + for name, x := range includedScope.Classes { + newName := alias + interfaces.ModuleSep + name + if alias == interfaces.BareSymbol { // not supported by parser atm! + if !AllowBareIncludes { + return fmt.Errorf("bare includes disabled at compile time for include of `%s`", include.Name) + } + newName = name + } + if previous, exists := newClasses[newName]; exists { + // don't overwrite in same scope + return fmt.Errorf("can't squash class `%s` from `%s` by include of `%s`", newName, previous, include.Name) + } + newClasses[newName] = include.Name + loopScope.Classes[newName] = x + } + + // everything has been merged, move on to next include... + //includes[include.Name] = struct{}{} // don't mark as found in scope + aliases[alias] = struct{}{} // do track these as a bonus + } + } + + obj.scope = loopScope // save a reference in case we're read by an import + if obj.data.Debug { obj.data.Logf("prog: set scope: finished") } @@ -4254,7 +4426,13 @@ func (obj *StmtClass) SetScope(scope *interfaces.Scope) error { if scope == nil { scope = interfaces.EmptyScope() } + + // We want to capture what was in scope at the definition site of the + // class so that when we `include` the class, the body of the class is + // expanded with the variables which were in scope at the definition + // site and not the variables which were in scope at the include site. obj.scope = scope // store for later + return nil } @@ -4296,8 +4474,9 @@ type StmtInclude struct { class *StmtClass // copy of class that we're using orig *StmtInclude // original pointer to this - Name string - Args []interfaces.Expr + Name string + Args []interfaces.Expr + Alias string } // String returns a short representation of this statement. @@ -4368,9 +4547,10 @@ func (obj *StmtInclude) Interpolate() (interfaces.Stmt, error) { } return &StmtInclude{ //class: obj.class, // TODO: is this necessary? - orig: orig, - Name: obj.Name, - Args: args, + orig: orig, + Name: obj.Name, + Args: args, + Alias: obj.Alias, }, nil } @@ -4403,9 +4583,10 @@ func (obj *StmtInclude) Copy() (interfaces.Stmt, error) { } return &StmtInclude{ //class: obj.class, // TODO: is this necessary? - orig: orig, - Name: obj.Name, - Args: args, + orig: orig, + Name: obj.Name, + Args: args, + Alias: obj.Alias, }, nil } @@ -4422,17 +4603,34 @@ func (obj *StmtInclude) Ordering(produces map[string]interfaces.Node) (*pgraph.G if obj.Name == "" { return nil, nil, fmt.Errorf("missing class name") } - uid := classOrderingPrefix + obj.Name // ordering id - cons := make(map[interfaces.Node]string) - cons[obj] = uid + uid := classOrderingPrefix + obj.Name // ordering id node, exists := produces[uid] if exists { - edge := &pgraph.SimpleEdge{Name: "stmtinclude"} + edge := &pgraph.SimpleEdge{Name: "stmtinclude1"} graph.AddEdge(node, obj, edge) // prod -> cons } + // equivalent to: strings.Contains(obj.Name, interfaces.ModuleSep) + if split := strings.Split(obj.Name, interfaces.ModuleSep); len(split) > 1 { + // we contain a dot + uid = scopedOrderingPrefix + split[0] // just the first prefix + + // TODO: do we also want this second edge?? + node, exists := produces[uid] + if exists { + edge := &pgraph.SimpleEdge{Name: "stmtinclude2"} + graph.AddEdge(node, obj, edge) // prod -> cons + } + } + // It's okay to replace the normal `class` prefix, because we have the + // fancier `scoped:` prefix which matches more generally... + + // TODO: we _can_ produce two uid's here, is it okay we only offer one? + cons := make(map[interfaces.Node]string) + cons[obj] = uid + for _, node := range obj.Args { g, c, err := node.Ordering(produces) if err != nil { @@ -4545,7 +4743,6 @@ func (obj *StmtInclude) SetScope(scope *interfaces.Scope) error { }, CapturedScope: newScope, } - } // recursion detection @@ -4702,6 +4899,12 @@ func (obj *StmtImport) Ordering(produces map[string]interfaces.Node) (*pgraph.Gr } graph.AddVertex(obj) + // Since we always run the imports before anything else in the StmtProg, + // we don't need to do anything special in here. + // TODO: If this statement is true, add this in so that imports can be + // done in the same iteration through StmtProg in SetScope with all of + // the other statements. + cons := make(map[interfaces.Node]string) return graph, cons, nil } @@ -7580,15 +7783,31 @@ func (obj *ExprCall) Ordering(produces map[string]interfaces.Node) (*pgraph.Grap uid = varOrderingPrefix + obj.Name // ordering id } - cons := make(map[interfaces.Node]string) - cons[obj] = uid - node, exists := produces[uid] if exists { - edge := &pgraph.SimpleEdge{Name: "exprcallname"} + edge := &pgraph.SimpleEdge{Name: "exprcallname1"} graph.AddEdge(node, obj, edge) // prod -> cons } + // equivalent to: strings.Contains(obj.Name, interfaces.ModuleSep) + if split := strings.Split(obj.Name, interfaces.ModuleSep); len(split) > 1 { + // we contain a dot + uid = scopedOrderingPrefix + split[0] // just the first prefix + + // TODO: do we also want this second edge?? + node, exists := produces[uid] + if exists { + edge := &pgraph.SimpleEdge{Name: "exprcallname2"} + graph.AddEdge(node, obj, edge) // prod -> cons + } + } + // It's okay to replace the normal `func` or `var` prefix, because we + // have the fancier `scoped:` prefix which matches more generally... + + // TODO: we _can_ produce two uid's here, is it okay we only offer one? + cons := make(map[interfaces.Node]string) + cons[obj] = uid + for _, node := range obj.Args { g, c, err := node.Ordering(produces) if err != nil { @@ -8363,15 +8582,31 @@ func (obj *ExprVar) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph } uid := varOrderingPrefix + obj.Name // ordering id - cons := make(map[interfaces.Node]string) - cons[obj] = uid - node, exists := produces[uid] if exists { - edge := &pgraph.SimpleEdge{Name: "exprvar"} + edge := &pgraph.SimpleEdge{Name: "exprvar1"} graph.AddEdge(node, obj, edge) // prod -> cons } + // equivalent to: strings.Contains(obj.Name, interfaces.ModuleSep) + if split := strings.Split(obj.Name, interfaces.ModuleSep); len(split) > 1 { + // we contain a dot + uid = scopedOrderingPrefix + split[0] // just the first prefix + + // TODO: do we also want this second edge?? + node, exists := produces[uid] + if exists { + edge := &pgraph.SimpleEdge{Name: "exprvar2"} + graph.AddEdge(node, obj, edge) // prod -> cons + } + } + // It's okay to replace the normal `var` prefix, because we have the + // fancier `scoped:` prefix which matches more generally... + + // TODO: we _can_ produce two uid's here, is it okay we only offer one? + cons := make(map[interfaces.Node]string) + cons[obj] = uid + return graph, cons, nil } diff --git a/lang/interfaces/ast.go b/lang/interfaces/ast.go index ebb617f5..0dd0ec12 100644 --- a/lang/interfaces/ast.go +++ b/lang/interfaces/ast.go @@ -235,7 +235,7 @@ type Data struct { // from the variables, which could actually contain lambda functions. type Scope struct { Variables map[string]Expr - Functions map[string]Expr // the Expr will usually be an *ExprFunc + Functions map[string]Expr // the Expr will usually be an *ExprFunc (actually it's usually (or always) an *ExprSingleton, which wraps an *ExprFunc now) Classes map[string]Stmt Chain []Node // chain of previously seen node's diff --git a/lang/interfaces/const.go b/lang/interfaces/const.go index e92d2675..45761c49 100644 --- a/lang/interfaces/const.go +++ b/lang/interfaces/const.go @@ -28,6 +28,12 @@ const ( // also used with `ModuleSep` for scoped variables like `$foo.bar.baz`. VarPrefix = "$" + // BareSymbol is the character used primarily for imports to specify + // that we want to import the entire contents and flatten them into our + // current scope. It should probably be removed entirely to force + // explicit imports. + BareSymbol = "*" + // PanicResKind is the kind string used for the panic resource. PanicResKind = "_panic" ) diff --git a/lang/interpret_test/TestAstFunc1/graph14.txtar b/lang/interpret_test/TestAstFunc1/graph14.txtar index 270188fd..efb85dcf 100644 --- a/lang/interpret_test/TestAstFunc1/graph14.txtar +++ b/lang/interpret_test/TestAstFunc1/graph14.txtar @@ -4,4 +4,4 @@ $x = "wow" $x = 99 # woops, but also a change of type :P -- OUTPUT -- -# err: errSetScope: var `x` already exists in this scope +# err: errSetScope: could not generate ordering: duplicate assignment to `var:x`, have: bind(x) diff --git a/lang/interpret_test/TestAstFunc1/graph9.txtar b/lang/interpret_test/TestAstFunc1/graph9.txtar index 3fb117aa..7634713a 100644 --- a/lang/interpret_test/TestAstFunc1/graph9.txtar +++ b/lang/interpret_test/TestAstFunc1/graph9.txtar @@ -3,4 +3,4 @@ $x = "hello" $x = "world" # woops -- OUTPUT -- -# err: errSetScope: var `x` already exists in this scope +# err: errSetScope: could not generate ordering: duplicate assignment to `var:x`, have: bind(x) diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-class0.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-class0.txtar new file mode 100644 index 00000000..3844d77c --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-class0.txtar @@ -0,0 +1,30 @@ +-- main.mcl -- +class c1 { + test "t1" {} + $y = "hello" + class c0 { + test "t2" {} + $x = "goodbye" + } +} +include c1 as i1 # has $y +include i1.c0 as i0 # has $x ...and $y + +test $i0.x {} # ok +test $i1.y {} # ok +panic($i0.x != "goodbye") +panic($i1.y != "hello") + +# the really tricky case +# XXX: works atm, but not supported for now: could not set scope: variable i0.y not in scope +# We currently re-export anything in the parent scope as available from our +# current child scope, which makes this variable visible. Unfortunately, it does +# not have the correct dependency (edge) present in the Ordering system, so it +# is flaky depending on luck of the toposort. +#test $i0.y {} + +-- OUTPUT -- +Vertex: test[goodbye] +Vertex: test[hello] +Vertex: test[t1] +Vertex: test[t2] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-class1.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-class1.txtar new file mode 100644 index 00000000..94b164e3 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-class1.txtar @@ -0,0 +1,39 @@ +-- main.mcl -- +class c1($b) { + test "t1" {} + if $b { + test "t2" {} + } else { + test "t3" {} + } + class c0 { + test "t4" {} + if $b { + test "t5" {} + } else { + test "t6" {} + } + $x = if $b { + "hello" + } else { + "goodbye" + } + } +} +include c1(true) as i1 +include i1.c0 as i01 + +include c1(false) as i2 +include i2.c0 as i02 + +test $i01.x {} +test $i02.x {} +-- OUTPUT -- +Vertex: test[hello] +Vertex: test[goodbye] +Vertex: test[t1] +Vertex: test[t2] +Vertex: test[t3] +Vertex: test[t4] +Vertex: test[t5] +Vertex: test[t6] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-class2.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-class2.txtar new file mode 100644 index 00000000..156b8e74 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-class2.txtar @@ -0,0 +1,17 @@ +-- main.mcl -- +class c1($b) { + if $b { # scope doesn't leak up and out of `if` statement! + class inner() { + test "t1" {} + } + } else { + class inner() { + test "t2" {} + } + } +} + +include c1 as i1 +include i1.inner +-- OUTPUT -- +# err: errSetScope: class `c1` expected 1 args but got 0 diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-duplicate0.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-duplicate0.txtar new file mode 100644 index 00000000..4fb80527 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-duplicate0.txtar @@ -0,0 +1,11 @@ +-- main.mcl -- +class c1() { + $x = "got: ${s}" +} + +# TODO: can this be allowed? +include c1 as i1 +include c1 as i1 +test $i1.x {} +-- OUTPUT -- +# err: errSetScope: could not generate ordering: duplicate assignment to `scoped:i1`, have: include(c1) diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-duplicate1.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-duplicate1.txtar new file mode 100644 index 00000000..110971d4 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-duplicate1.txtar @@ -0,0 +1,11 @@ +-- main.mcl -- +class c1($s) { + $x = "got: ${s}" +} + +# TODO: can this be allowed? +include c1("hey") as i1 +include c1("hey") as i1 +test $i1.x {} +-- OUTPUT -- +# err: errSetScope: could not generate ordering: duplicate assignment to `scoped:i1`, have: include(c1) diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-duplicate2.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-duplicate2.txtar new file mode 100644 index 00000000..eaca6a9b --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-duplicate2.txtar @@ -0,0 +1,10 @@ +-- main.mcl -- +class c1($s) { + $x = "got: ${s}" +} + +include c1("hey") as i1 +include c1("there") as i1 +test $i1.x {} +-- OUTPUT -- +# err: errSetScope: could not generate ordering: duplicate assignment to `scoped:i1`, have: include(c1) diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-func-scope0.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-func-scope0.txtar new file mode 100644 index 00000000..75b5a0d0 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-func-scope0.txtar @@ -0,0 +1,14 @@ +-- main.mcl -- +class c1 { + $x = "outside" + test "t1" {} + func f1($x) { + "hello" + $x + } +} +include c1 as i1 + +test i1.f1("world") {} +-- OUTPUT -- +Vertex: test[helloworld] +Vertex: test[t1] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-func-scope1.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-func-scope1.txtar new file mode 100644 index 00000000..16c5e30e --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-func-scope1.txtar @@ -0,0 +1,14 @@ +-- main.mcl -- +class c1 { + $x = "world" + test "t1" {} + func f1($y) { + "hello" + $x + } +} +include c1 as i1 + +test i1.f1("whatever") {} +-- OUTPUT -- +Vertex: test[helloworld] +Vertex: test[t1] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-func0.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-func0.txtar new file mode 100644 index 00000000..1c102e11 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-func0.txtar @@ -0,0 +1,13 @@ +-- main.mcl -- +class c1 { + test "t1" {} + func f1() { + "hello" + } +} +include c1 as i1 + +test i1.f1() {} +-- OUTPUT -- +Vertex: test[hello] +Vertex: test[t1] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-func1.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-func1.txtar new file mode 100644 index 00000000..b7f7c102 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-func1.txtar @@ -0,0 +1,27 @@ +-- main.mcl -- +class c1($b) { + test "t1" {} + if $b { + test "t2" {} + } else { + test "t3" {} + } + func f1() { + if $b { + "hello" + } else { + "goodbye" + } + } +} +include c1(true) as i1 +include c1(false) as i2 + +test i1.f1() {} +test i2.f1() {} +-- OUTPUT -- +Vertex: test[hello] +Vertex: test[goodbye] +Vertex: test[t1] +Vertex: test[t2] +Vertex: test[t3] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-func2.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-func2.txtar new file mode 100644 index 00000000..4d146d40 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-func2.txtar @@ -0,0 +1,42 @@ +-- main.mcl -- +class c0($b) { + test "t1" {} + if $b { + test "t2" {} + } else { + test "t3" {} + } + func f0() { + if $b { + "hello" + } else { + "goodbye" + } + } +} +class c1($b) { + test "t4" {} + if $b { + test "t5" {} + } else { + test "t6" {} + } + include c0($b) as i0 + func f1() { i0.f0() } +} +include c1(true) as i1 +include c1(false) as i2 + +test i1.f1() {} +test i2.f1() {} +test i1.i0.f0() {} # I think these might work directly too. Do we want them to? +test i2.i0.f0() {} +-- OUTPUT -- +Vertex: test[goodbye] +Vertex: test[hello] +Vertex: test[t1] +Vertex: test[t2] +Vertex: test[t3] +Vertex: test[t4] +Vertex: test[t5] +Vertex: test[t6] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-import1.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-import1.txtar new file mode 100644 index 00000000..85362af6 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-import1.txtar @@ -0,0 +1,10 @@ +-- main.mcl -- +import "fmt" as i1 +class c1($s) { + $x = fmt.printf("got: %s", $s) +} + +include c1("hey") as i1 +test $i1.x {} +-- OUTPUT -- +# err: errSetScope: could not generate ordering: duplicate assignment to `scoped:i1`, have: import(fmt) diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-import2.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-import2.txtar new file mode 100644 index 00000000..a9500f81 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-import2.txtar @@ -0,0 +1,10 @@ +-- main.mcl -- +import "fmt" +class c1($s) { + $x = fmt.printf("got: %s", $s) +} + +include c1("hey") as fmt +test $fmt.x {} +-- OUTPUT -- +# err: errSetScope: could not generate ordering: duplicate assignment to `scoped:fmt`, have: import(fmt) diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-lambda-scope0.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-lambda-scope0.txtar new file mode 100644 index 00000000..89df2d7c --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-lambda-scope0.txtar @@ -0,0 +1,14 @@ +-- main.mcl -- +class c1 { + $x = "outside" + test "t1" {} + $f1 = func($x) { + "hello" + $x + } +} +include c1 as i1 + +test $i1.f1("world") {} +-- OUTPUT -- +Vertex: test[helloworld] +Vertex: test[t1] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-lambda-scope1.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-lambda-scope1.txtar new file mode 100644 index 00000000..4ab28c4a --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-lambda-scope1.txtar @@ -0,0 +1,14 @@ +-- main.mcl -- +class c1 { + $x = "world" + test "t1" {} + $f1 = func($y) { + "hello" + $x + } +} +include c1 as i1 + +test $i1.f1("whatever") {} +-- OUTPUT -- +Vertex: test[helloworld] +Vertex: test[t1] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-lambda0.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-lambda0.txtar new file mode 100644 index 00000000..fcc6a981 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-lambda0.txtar @@ -0,0 +1,13 @@ +-- main.mcl -- +class c1 { + test "t1" {} + $f1 = func() { + "hello" + } +} +include c1 as i1 + +test $i1.f1() {} +-- OUTPUT -- +Vertex: test[hello] +Vertex: test[t1] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-lambda1.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-lambda1.txtar new file mode 100644 index 00000000..921efaa1 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-lambda1.txtar @@ -0,0 +1,27 @@ +-- main.mcl -- +class c1($b) { + test "t1" {} + if $b { + test "t2" {} + } else { + test "t3" {} + } + $f1 = func() { + if $b { + "hello" + } else { + "goodbye" + } + } +} +include c1(true) as i1 +include c1(false) as i2 + +test $i1.f1() {} +test $i2.f1() {} +-- OUTPUT -- +Vertex: test[hello] +Vertex: test[goodbye] +Vertex: test[t1] +Vertex: test[t2] +Vertex: test[t3] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-lambda2.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-lambda2.txtar new file mode 100644 index 00000000..bac1f9ba --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-lambda2.txtar @@ -0,0 +1,44 @@ +-- main.mcl -- +class c0($b) { + test "t1" {} + if $b { + test "t2" {} + } else { + test "t3" {} + } + $f0 = func() { + if $b { + "hello" + } else { + "goodbye" + } + } + #$f0 = "hey" +} +class c1($b) { + test "t4" {} + if $b { + test "t5" {} + } else { + test "t6" {} + } + include c0($b) as i0 + $x = $i0.f0 +} +include c1(true) as i1 +include c1(false) as i2 + +test $i1.x() {} +test $i1.i0.f0() {} +test $i2.x() {} +test $i1.i0.f0() {} # I think these should work directly too. Do we want them to? +test $i2.i0.f0() {} +-- OUTPUT -- +Vertex: test[goodbye] +Vertex: test[hello] +Vertex: test[t1] +Vertex: test[t2] +Vertex: test[t3] +Vertex: test[t4] +Vertex: test[t5] +Vertex: test[t6] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-vars0.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-vars0.txtar new file mode 100644 index 00000000..a0a645ee --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-vars0.txtar @@ -0,0 +1,11 @@ +-- main.mcl -- +class c1 { + test "t1" {} + $x = "hello" +} +include c1 as i1 + +test $i1.x {} +-- OUTPUT -- +Vertex: test[hello] +Vertex: test[t1] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-vars1.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-vars1.txtar new file mode 100644 index 00000000..fb7034f4 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-vars1.txtar @@ -0,0 +1,25 @@ +-- main.mcl -- +class c1($b) { + test "t1" {} + if $b { + test "t2" {} + } else { + test "t3" {} + } + $x = if $b { + "hello" + } else { + "goodbye" + } +} +include c1(true) as i1 +include c1(false) as i2 + +test $i1.x {} +test $i2.x {} +-- OUTPUT -- +Vertex: test[hello] +Vertex: test[goodbye] +Vertex: test[t1] +Vertex: test[t2] +Vertex: test[t3] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-vars2.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-vars2.txtar new file mode 100644 index 00000000..6c5ef002 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-vars2.txtar @@ -0,0 +1,40 @@ +-- main.mcl -- +class c0($b) { + test "t1" {} + if $b { + test "t2" {} + } else { + test "t3" {} + } + $y = if $b { + "hello" + } else { + "goodbye" + } +} +class c1($b) { + test "t4" {} + if $b { + test "t5" {} + } else { + test "t6" {} + } + include c0($b) as i0 + $x = $i0.y +} +include c1(true) as i1 +include c1(false) as i2 + +test $i1.x {} +test $i2.x {} +test $i1.i0.y {} # I think these should work directly too. Do we want them to? +test $i2.i0.y {} +-- OUTPUT -- +Vertex: test[goodbye] +Vertex: test[hello] +Vertex: test[t1] +Vertex: test[t2] +Vertex: test[t3] +Vertex: test[t4] +Vertex: test[t5] +Vertex: test[t6] diff --git a/lang/interpret_test/TestAstFunc2/class-include-as-vars3.txtar b/lang/interpret_test/TestAstFunc2/class-include-as-vars3.txtar new file mode 100644 index 00000000..d51f1b48 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/class-include-as-vars3.txtar @@ -0,0 +1,8 @@ +-- main.mcl -- +$wat = "bad1" +class c1($wat) { + test $wat {} +} +include c1("hello") +-- OUTPUT -- +Vertex: test[hello] diff --git a/lang/interpret_test/TestAstFunc2/import-scope-classes1.txtar b/lang/interpret_test/TestAstFunc2/import-scope-classes1.txtar new file mode 100644 index 00000000..b648a751 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/import-scope-classes1.txtar @@ -0,0 +1,22 @@ +-- main.mcl -- + +class c1() { + $x = "i am x" +} + +class c2() { + include c1 as g1 + + #$y = "i am y" + $z = "i am y and " + $g1.x +} + +include c2 as f1 + +test $f1.z {} # yep +#test $f1.x {} # no +test $f1.g1.x {} # yep + +-- OUTPUT -- +Vertex: test[i am y and i am x] +Vertex: test[i am x] diff --git a/lang/interpret_test/TestAstFunc2/import-scope-classes2.txtar b/lang/interpret_test/TestAstFunc2/import-scope-classes2.txtar new file mode 100644 index 00000000..c1c626f5 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/import-scope-classes2.txtar @@ -0,0 +1,20 @@ +-- main.mcl -- +$x = "i am x" # i am top-level + +class c2() { + #$y = "i am y" + $z = "i am y and " + $x + + $newx = $x + " and hello" +} + +include c2 as f1 + +test $f1.z {} +test $f1.x {} # tricky +test $f1.newx {} + +-- OUTPUT -- +Vertex: test[i am x] +Vertex: test[i am x and hello] +Vertex: test[i am y and i am x] diff --git a/lang/interpret_test/TestAstFunc2/import-scope-classes3.txtar b/lang/interpret_test/TestAstFunc2/import-scope-classes3.txtar new file mode 100644 index 00000000..f1eb16f9 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/import-scope-classes3.txtar @@ -0,0 +1,20 @@ +-- main.mcl -- +$x = "i am x" # i am top-level + +class c2() { + $z = "i am y and " + $x + + # Since $x is pulled in from top-level automatically, we don't allow the + # re-definition by shadowing of the same variable. + $x = $x # not allowed + #$x = $x + "wow" # allowed? +} + +include c2 as f1 + +test $f1.z {} +test $f1.x {} # tricky +test $f1.newx {} + +-- OUTPUT -- +# err: errSetScope: recursive reference while setting scope: not a dag diff --git a/lang/interpret_test/TestAstFunc2/import-scope-classes4.txtar b/lang/interpret_test/TestAstFunc2/import-scope-classes4.txtar new file mode 100644 index 00000000..27bd4b09 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/import-scope-classes4.txtar @@ -0,0 +1,26 @@ +-- main.mcl -- +$x1 = "i am x1" # i am top-level +$x2 = "i am x2" # i am top-level + +class c2() { + $z = "i am y and " + $x1 + + $x1 = "hey" # shadow +} + +include c2 as f1 + +test $f1.z {} +test $f1.x1 {} + +# the really tricky case +# XXX: works atm, but not supported for now: could not set scope: variable f1.x2 not in scope +# We currently re-export anything in the parent scope as available from our +# current child scope, which makes this variable visible. Unfortunately, it does +# not have the correct dependency (edge) present in the Ordering system, so it +# is flaky depending on luck of the toposort. +#test $f1.x2 {} + +-- OUTPUT -- +Vertex: test[hey] +Vertex: test[i am y and hey] diff --git a/lang/interpret_test/TestAstFunc2/import-scope-classes5.txtar b/lang/interpret_test/TestAstFunc2/import-scope-classes5.txtar new file mode 100644 index 00000000..08826c65 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/import-scope-classes5.txtar @@ -0,0 +1,31 @@ +-- main.mcl -- +$x = "i am x" # i am top-level + +class c2() { + $y = "i am y and " + $x + + #$x = $x # might be allowed, i don't _really_ care, but i prefer not + + # We need to be able to re-define our new $x that shadows the parent $x, + # but it should be able to incorporate the parent (top-level) $x into + # the new value that this new $x here becomes. + # XXX: not supported for now: could not set scope: not a dag + # Sam suggested the RHS $x should have a special keyword to make it + # refer to the parent scope. I suggested when $x is also on the LHS, + # then this magic keyword should be implied. And only possible in the + # case for variables on the RHS that are the same var as on the LHS. + #$x = $x + " and this is shadowed" # this is important +} + +include c2 as f1 + +test $x {} +test $f1.y {} + +# the really tricky case +# XXX: not supported for now: could not set scope: not a dag +#test $f1.x {} + +-- OUTPUT -- +Vertex: test[i am x] +Vertex: test[i am y and i am x] diff --git a/lang/interpret_test/TestAstFunc2/import-scope-classes6.txtar b/lang/interpret_test/TestAstFunc2/import-scope-classes6.txtar new file mode 100644 index 00000000..7bedfa1a --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/import-scope-classes6.txtar @@ -0,0 +1,20 @@ +-- main.mcl -- +$x = "i am x" # i am top-level + +class c2() { + $z = "i am y and " + $x + + #$x = $x # not allowed + $tmpx = $x # this $x is actually from below, not from the parent scope! + #$x = $x + "wow" # allowed? + $x = $tmpx + "wow" # circular with itself ($x) in this scope! +} + +include c2 as f1 + +test $f1.z {} +test $f1.x {} # tricky +test $f1.newx {} + +-- OUTPUT -- +# err: errSetScope: recursive reference while setting scope: not a dag diff --git a/lang/interpret_test/TestAstFunc2/import-scope-vars1.txtar b/lang/interpret_test/TestAstFunc2/import-scope-vars1.txtar new file mode 100644 index 00000000..3b58162e --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/import-scope-vars1.txtar @@ -0,0 +1,15 @@ +-- metadata.yaml -- +#files: "files/" # these are some extra files we can use (is the default) +-- main.mcl -- +import "y.mcl" as g +test $g.y {} # should work +#test $g.x {} # should fail +test $g.f.x {} # should maybe work +-- x.mcl -- +$x = "this is x.mcl" +-- y.mcl -- +import "x.mcl" as f +$y = $f.x + " and this is y.mcl" +-- OUTPUT -- +Vertex: test[this is x.mcl] +Vertex: test[this is x.mcl and this is y.mcl] diff --git a/lang/interpret_test/TestAstFunc2/import-scope-vars2-fail.txtar b/lang/interpret_test/TestAstFunc2/import-scope-vars2-fail.txtar new file mode 100644 index 00000000..f543c45b --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/import-scope-vars2-fail.txtar @@ -0,0 +1,14 @@ +-- metadata.yaml -- +#files: "files/" # these are some extra files we can use (is the default) +-- main.mcl -- +import "y.mcl" as g +#test $g.y {} # should work +test $g.x {} # should fail +test $g.f.x {} # should maybe work +-- x.mcl -- +$x = "this is x.mcl" +-- y.mcl -- +import "x.mcl" as f +$y = $f.x + " and this is y.mcl" +-- OUTPUT -- +# err: errSetScope: variable g.x not in scope diff --git a/lang/interpret_test/TestAstFunc2/import-scope-vars2.txtar b/lang/interpret_test/TestAstFunc2/import-scope-vars2.txtar new file mode 100644 index 00000000..cb60b5d0 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/import-scope-vars2.txtar @@ -0,0 +1,15 @@ +-- metadata.yaml -- +#files: "files/" # these are some extra files we can use (is the default) +-- main.mcl -- +import "y.mcl" as g +test $g.y {} # should work +#test $g.x {} # should fail +test $g.f.x {} # should maybe work +-- x.mcl -- +$x = "this is x.mcl" +-- y.mcl -- +import "x.mcl" as f +$y = $f.x + " and this is y.mcl" +-- OUTPUT -- +Vertex: test[this is x.mcl and this is y.mcl] +Vertex: test[this is x.mcl] diff --git a/lang/interpret_test/TestAstFunc2/simple-scope-ordering1.txtar b/lang/interpret_test/TestAstFunc2/simple-scope-ordering1.txtar new file mode 100644 index 00000000..683685dd --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-scope-ordering1.txtar @@ -0,0 +1,7 @@ +-- main.mcl -- +# set scope ordering test +$x = "hey" +$y = $x +test $y {} +-- OUTPUT -- +Vertex: test[hey] diff --git a/lang/interpret_test/TestAstFunc2/simple-scope-ordering2.txtar b/lang/interpret_test/TestAstFunc2/simple-scope-ordering2.txtar new file mode 100644 index 00000000..a53a0e4c --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-scope-ordering2.txtar @@ -0,0 +1,7 @@ +-- main.mcl -- +# set scope ordering test +$y = $x +$x = "hey" +test $y {} +-- OUTPUT -- +Vertex: test[hey] diff --git a/lang/interpret_test/TestAstFunc2/simple-scope-ordering3.txtar b/lang/interpret_test/TestAstFunc2/simple-scope-ordering3.txtar new file mode 100644 index 00000000..867dbd5b --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-scope-ordering3.txtar @@ -0,0 +1,9 @@ +-- main.mcl -- +# reverse order +test $i0.f0 {} +include c0 as i0 +class c0() { + $f0 = "hello" +} +-- OUTPUT -- +Vertex: test[hello] diff --git a/lang/interpret_test/TestAstFunc2/simple-scope-ordering4.txtar b/lang/interpret_test/TestAstFunc2/simple-scope-ordering4.txtar new file mode 100644 index 00000000..2a9dfb46 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-scope-ordering4.txtar @@ -0,0 +1,9 @@ +-- main.mcl -- +# reverse order +test $i0.f0 {} +include c0("hello") as i0 +class c0($s) { + $f0 = $s +} +-- OUTPUT -- +Vertex: test[hello] diff --git a/lang/parser/lexer.nex b/lang/parser/lexer.nex index ca6e716f..2318b270 100644 --- a/lang/parser/lexer.nex +++ b/lang/parser/lexer.nex @@ -85,8 +85,14 @@ return MINUS } /\*/ { + // This is used as the multiplication symbol, but also + // (for now) the bare import feature, eg: `import as *`. yylex.pos(lval) // our pos lval.str = yylex.Text() + // sanity check... these should be the same! + if x, y := lval.str, interfaces.BareSymbol; x != y { + panic(fmt.Sprintf("MULTIPLY does not match BareSymbol (%s != %s)", x, y)) + } return MULTIPLY } /\// { diff --git a/lang/parser/lexparse_test.go b/lang/parser/lexparse_test.go index fde80de4..761a2128 100644 --- a/lang/parser/lexparse_test.go +++ b/lang/parser/lexparse_test.go @@ -1670,12 +1670,12 @@ func TestLexParse0(t *testing.T) { exp: exp, }) } - { + if ast.AllowBareImports { exp := &ast.StmtProg{ Body: []interfaces.Stmt{ &ast.StmtImport{ Name: "foo1", - Alias: "*", + Alias: interfaces.BareSymbol, }, }, } diff --git a/lang/parser/parser.y b/lang/parser/parser.y index 91215a57..3501032d 100644 --- a/lang/parser/parser.y +++ b/lang/parser/parser.y @@ -295,6 +295,27 @@ stmt: Args: $4.exprs, } } + // `include name as foo` + // TODO: should we support: `include name as *` +| INCLUDE_IDENTIFIER dotted_identifier AS_IDENTIFIER IDENTIFIER + { + posLast(yylex, yyDollar) // our pos + $$.stmt = &ast.StmtInclude{ + Name: $2.str, + Alias: $4.str, + } + } + // `include name(...) as foo` + // TODO: should we support: `include name(...) as *` +| INCLUDE_IDENTIFIER dotted_identifier OPEN_PAREN call_args CLOSE_PAREN AS_IDENTIFIER IDENTIFIER + { + posLast(yylex, yyDollar) // our pos + $$.stmt = &ast.StmtInclude{ + Name: $2.str, + Args: $4.exprs, + Alias: $7.str, + } + } // `import "name"` | IMPORT_IDENTIFIER STRING {