From 2899bc234aa12ac029a5922afe2fc87056b29e57 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Wed, 5 Mar 2025 13:18:35 -0500 Subject: [PATCH] lang: Add a forkv loop statement for iterating over a map This adds a forkv statement which is used to iterate over a map with a body of statements. This is an important data transformation tool which should be used sparingly, but is important to have. An import statement inside of a forkv loop is not currently supported. We have a simple hack to detect the obvious cases, but more deeply nested scenarios probably won't be caught, and you'll get an obscure error message if you try to do this. This was incredibly challenging to get right, and it's all thanks to Sam for his brilliance. Note, I couldn't think of a better keyword that "forkv" but suggestions are welcome if you think you have a better idea. Other ideas were formap and foreach, but neither got me very excited. --- docs/language-guide.md | 9 + examples/lang/forkv.mcl | 21 + lang/ast/structs.go | 524 +++++++++++++++++- lang/funcs/structs/forkv.go | 314 +++++++++++ .../TestAstFunc2/stmtforforkv00.txtar | 31 ++ .../TestAstFunc2/stmtforkv00.txtar | 13 + .../TestAstFunc2/stmtforkv01.txtar | 15 + .../TestAstFunc2/stmtforkv02.txtar | 13 + .../TestAstFunc2/stmtforkv03.txtar | 14 + .../TestAstFunc2/stmtforkv04.txtar | 16 + .../TestAstFunc2/stmtforkv05.txtar | 16 + .../TestAstFunc2/stmtforkv06.txtar | 16 + .../TestAstFunc2/stmtforkv07.txtar | 17 + .../TestAstFunc2/stmtforkv08.txtar | 20 + .../TestAstFunc2/stmtforkv09.txtar | 22 + .../TestAstFunc2/stmtforkv10.txtar | 21 + .../TestAstFunc2/stmtforkv11.txtar | 21 + .../TestAstFunc2/stmtforkv12.txtar | 21 + .../TestAstFunc2/stmtforkv13.txtar | 21 + .../TestAstFunc2/stmtforkv14.txtar | 23 + .../TestAstFunc2/stmtforkv15.txtar | 23 + .../TestAstFunc2/stmtforkv16.txtar | 22 + .../TestAstFunc2/stmtforkv17.txtar | 22 + .../TestAstFunc2/stmtforkv18.txtar | 22 + .../TestAstFunc2/stmtforkv19.txtar | 22 + .../TestAstFunc2/stmtforkv20.txtar | 24 + .../TestAstFunc2/stmtforkv21.txtar | 24 + .../TestAstFunc2/stmtforkv22.txtar | 24 + .../TestAstFunc2/stmtforkv23.txtar | 31 ++ .../TestAstFunc2/stmtforkv24.txtar | 17 + .../TestAstFunc2/stmtforkv25.txtar | 17 + .../TestAstFunc2/stmtforkv26.txtar | 41 ++ .../TestAstFunc2/stmtforkv27.txtar | 38 ++ .../TestAstFunc2/stmtforkv28.txtar | 19 + .../TestAstFunc2/stmtforkv29.txtar | 22 + .../TestAstFunc2/stmtforkv30.txtar | 23 + .../TestAstFunc2/stmtforkv31.txtar | 24 + .../TestAstFunc2/stmtforkv32.txtar | 24 + .../TestAstFunc2/stmtforkv33.txtar | 24 + .../TestAstFunc2/stmtforkv34.txtar | 23 + .../TestAstFunc2/stmtforkv35.txtar | 32 ++ .../TestAstFunc2/stmtforkv36.txtar | 29 + .../TestAstFunc2/stmtforkv37.txtar | 28 + .../TestAstFunc2/stmtforkv38.txtar | 21 + .../TestAstFunc2/stmtforkv39.txtar | 21 + .../TestAstFunc2/stmtforkv40.txtar | 10 + .../TestAstFunc2/stmtforkv41.txtar | 24 + .../TestAstFunc2/stmtforkv42.txtar | 27 + .../TestAstFunc2/stmtforkv43.txtar | 22 + lang/parser/lexer.nex | 5 + lang/parser/parser.y | 14 +- 51 files changed, 1885 insertions(+), 2 deletions(-) create mode 100644 examples/lang/forkv.mcl create mode 100644 lang/funcs/structs/forkv.go create mode 100644 lang/interpret_test/TestAstFunc2/stmtforforkv00.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv00.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv01.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv02.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv03.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv04.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv05.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv06.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv07.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv08.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv09.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv10.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv11.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv12.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv13.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv14.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv15.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv16.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv17.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv18.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv19.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv20.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv21.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv22.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv23.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv24.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv25.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv26.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv27.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv28.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv29.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv30.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv31.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv32.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv33.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv34.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv35.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv36.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv37.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv38.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv39.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv40.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv41.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv42.txtar create mode 100644 lang/interpret_test/TestAstFunc2/stmtforkv43.txtar diff --git a/docs/language-guide.md b/docs/language-guide.md index 9728c608..e77d134f 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -109,6 +109,15 @@ expression } ``` +- **forkv**: loop over a map with a body of statements + + ```mcl + $map = {0 => "a", 1 => "b", 2 => "c",} + forkv $key, $val in $map { + # some statements go here + } + ``` + - **resource**: produces a resource ```mcl diff --git a/examples/lang/forkv.mcl b/examples/lang/forkv.mcl new file mode 100644 index 00000000..4be5509b --- /dev/null +++ b/examples/lang/forkv.mcl @@ -0,0 +1,21 @@ +import "datetime" +import "fmt" +import "math" + +$now = datetime.now() +$num = if math.mod($now, 2) == 0 { + 13 +} else { + $now +} + +# NOTE: the graph doesn't get regenerated just because a map value changed! +$m = {"foo" => 42, "bar" => $num,} + +forkv $k, $v in $m { + print ["${k}",] { + msg => fmt.printf("hello %s; count: %d", $k, $v), + + Meta:autogroup => true, + } +} diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 36ffd376..218ab146 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -3505,6 +3505,509 @@ func (obj *StmtFor) Output(table map[interfaces.Func]types.Value) (*interfaces.O }, nil } +// StmtForKV represents an iteration over a map. The body contains statements. +type StmtForKV struct { + Textarea + data *interfaces.Data + scope *interfaces.Scope // store for referencing this later + + Key string // no $ prefix + Val string // no $ prefix + + TypeKey *types.Type + TypeVal *types.Type + + keyParam *ExprParam + valParam *ExprParam + + Expr interfaces.Expr + exprPtr interfaces.Func // ptr for table lookup + Body interfaces.Stmt // optional, but usually present + + iterBody map[types.Value]interfaces.Stmt +} + +// String returns a short representation of this statement. +func (obj *StmtForKV) String() string { + // TODO: improve/change this if needed + s := fmt.Sprintf("forkv($%s, $%s)", obj.Key, obj.Val) + s += fmt.Sprintf(" in %s", obj.Expr.String()) + if obj.Body != nil { + s += fmt.Sprintf(" { %s }", obj.Body.String()) + } + return s +} + +// Apply is a general purpose iterator method that operates on any AST node. It +// is not used as the primary AST traversal function because it is less readable +// and easy to reason about than manually implementing traversal for each node. +// Nevertheless, it is a useful facility for operations that might only apply to +// a select number of node types, since they won't need extra noop iterators... +func (obj *StmtForKV) Apply(fn func(interfaces.Node) error) error { + if err := obj.Expr.Apply(fn); err != nil { + return err + } + if obj.Body != nil { + if err := obj.Body.Apply(fn); err != nil { + return err + } + } + return fn(obj) +} + +// Init initializes this branch of the AST, and returns an error if it fails to +// validate. +func (obj *StmtForKV) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + + obj.iterBody = make(map[types.Value]interfaces.Stmt) + + if err := obj.Expr.Init(data); err != nil { + return err + } + if obj.Body != nil { + if err := obj.Body.Init(data); err != nil { + return err + } + } + // XXX: remove this check if we can! + for _, stmt := range obj.Body.(*StmtProg).Body { + if _, ok := stmt.(*StmtImport); !ok { + continue + } + return fmt.Errorf("a StmtImport can't be contained inside a StmtForKV") + } + return nil +} + +// 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. +func (obj *StmtForKV) Interpolate() (interfaces.Stmt, error) { + expr, err := obj.Expr.Interpolate() + if err != nil { + return nil, errwrap.Wrapf(err, "could not interpolate Expr") + } + var body interfaces.Stmt + if obj.Body != nil { + body, err = obj.Body.Interpolate() + if err != nil { + return nil, errwrap.Wrapf(err, "could not interpolate Body") + } + } + return &StmtForKV{ + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, // XXX: Should we copy/include this here? + + Key: obj.Key, + Val: obj.Val, + + TypeKey: obj.TypeKey, + TypeVal: obj.TypeVal, + + keyParam: obj.keyParam, // XXX: Should we copy/include this here? + valParam: obj.valParam, // XXX: Should we copy/include this here? + + Expr: expr, + exprPtr: obj.exprPtr, // XXX: Should we copy/include this here? + Body: body, + + iterBody: obj.iterBody, // XXX: Should we copy/include this here? + }, nil +} + +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *StmtForKV) Copy() (interfaces.Stmt, error) { + copied := false + expr, err := obj.Expr.Copy() + if err != nil { + return nil, errwrap.Wrapf(err, "could not copy Expr") + } + if expr != obj.Expr { // must have been copied, or pointer would be same + copied = true + } + + var body interfaces.Stmt + if obj.Body != nil { + body, err = obj.Body.Copy() + if err != nil { + return nil, errwrap.Wrapf(err, "could not copy Body") + } + if body != obj.Body { + copied = true + } + } + + if !copied { // it's static + return obj, nil + } + return &StmtForKV{ + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, // XXX: Should we copy/include this here? + + Key: obj.Key, + Val: obj.Val, + + TypeKey: obj.TypeKey, + TypeVal: obj.TypeVal, + + keyParam: obj.keyParam, // XXX: Should we copy/include this here? + valParam: obj.valParam, // XXX: Should we copy/include this here? + + Expr: expr, + exprPtr: obj.exprPtr, // XXX: Should we copy/include this here? + Body: body, + + iterBody: obj.iterBody, // XXX: Should we copy/include this here? + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *StmtForKV) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, err + } + graph.AddVertex(obj) + + // Additional constraints: We know the condition has to be satisfied + // before this for statement itself can be used, since we depend on that + // value. + edge := &pgraph.SimpleEdge{Name: "stmtforkvexpr1"} + graph.AddEdge(obj.Expr, obj, edge) // prod -> cons + + cons := make(map[interfaces.Node]string) + + g, c, err := obj.Expr.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtforkvexpr2"} + graph.AddEdge(n, k, edge) + } + + if obj.Body == nil { // return early + return graph, cons, nil + } + + // additional constraints... + edge1 := &pgraph.SimpleEdge{Name: "stmtforkvbodyexpr"} + graph.AddEdge(obj.Expr, obj.Body, edge1) // prod -> cons + edge2 := &pgraph.SimpleEdge{Name: "stmtforkvbody1"} + graph.AddEdge(obj.Body, obj, edge2) // prod -> cons + + nodes := []interfaces.Stmt{obj.Body} // XXX: are there more to add? + + for _, node := range nodes { // "dry" + g, c, err := node.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtforkvbody2"} + graph.AddEdge(n, k, edge) + } + } + + return graph, cons, nil +} + +// SetScope stores the scope for later use in this resource and its children, +// which it propagates this downwards to. +func (obj *StmtForKV) SetScope(scope *interfaces.Scope) error { + if scope == nil { + scope = interfaces.EmptyScope() + } + obj.scope = scope // store for later + + if err := obj.Expr.SetScope(scope, map[string]interfaces.Expr{}); err != nil { // XXX: empty sctx? + return err + } + + if obj.Body == nil { // no loop body, we're done early + return nil + } + + // We need to build the two ExprParam's here, and those will contain the + // type unification variables, so we might as well populate those parts + // now, rather than waiting for the subsequent TypeCheck step. + + typExprKey := obj.TypeKey + if obj.TypeKey == nil { + typExprKey = &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?1 + } + } + obj.keyParam = newExprParam( + obj.Key, + typExprKey, + ) + + typExprVal := obj.TypeVal + if obj.TypeVal == nil { + typExprVal = &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?1 + } + } + obj.valParam = newExprParam( + obj.Val, + typExprVal, + ) + + newScope := scope.Copy() + newScope.Iterated = true // important! + newScope.Variables[obj.Key] = obj.keyParam + newScope.Variables[obj.Val] = obj.valParam + + return obj.Body.SetScope(newScope) +} + +// TypeCheck returns the list of invariants that this node produces. It does so +// recursively on any children elements that exist in the AST, and returns the +// collection to the caller. It calls TypeCheck for child statements, and +// Infer/Check for child expressions. +func (obj *StmtForKV) TypeCheck() ([]*interfaces.UnificationInvariant, error) { + // Don't call obj.Expr.Check here! + typ, invariants, err := obj.Expr.Infer() + if err != nil { + return nil, err + } + + // The type unification variables get created in SetScope! (If needed!) + typExprKey := obj.keyParam.typ + typExprVal := obj.valParam.typ + + typExpr := &types.Type{ + Kind: types.KindMap, + Key: typExprKey, + Val: typExprVal, + } + + invar := &interfaces.UnificationInvariant{ + Node: obj, + Expr: obj.Expr, + Expect: typExpr, // the map + Actual: typ, + } + invariants = append(invariants, invar) + + // The following two invariants are needed to ensure the ExprParam's are + // added to the unification solver so that we actually benefit from that + // relationship and solution! + invarKey := &interfaces.UnificationInvariant{ + Node: obj, + Expr: obj.keyParam, + Expect: typExprKey, // the map key type + Actual: typExprKey, // not necessarily an int! + } + invariants = append(invariants, invarKey) + + invarVal := &interfaces.UnificationInvariant{ + Node: obj, + Expr: obj.valParam, + Expect: typExprVal, // the map val type + Actual: typExprVal, + } + invariants = append(invariants, invarVal) + + if obj.Body != nil { + invars, err := obj.Body.TypeCheck() + if err != nil { + return nil, err + } + invariants = append(invariants, invars...) + } + + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. This particular for statement has lots of complex magic to +// make it all work. +func (obj *StmtForKV) Graph(env *interfaces.Env) (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("forkv") + if err != nil { + return nil, err + } + + g, f, err := obj.Expr.Graph(env) + if err != nil { + return nil, err + } + graph.AddGraph(g) + obj.exprPtr = f + + if obj.Body == nil { // no loop body, we're done early + return graph, nil + } + + mutex := &sync.Mutex{} + + // This gets called once per iteration, each time the map changes. + setOnIterBody := func(innerTxn interfaces.Txn, ptr types.Value, key, val interfaces.Func) error { + // Extend the environment with the two loop variables. + extendedEnv := env.Copy() + + // calling convention + extendedEnv.Variables[obj.keyParam.envKey] = &interfaces.FuncSingleton{ + MakeFunc: func() (*pgraph.Graph, interfaces.Func, error) { + f := key + g, err := pgraph.NewGraph("g") + if err != nil { + return nil, nil, err + } + g.AddVertex(f) + return g, f, nil + }, + } + + // XXX: create the function in ForKVFunc instead? + extendedEnv.Variables[obj.valParam.envKey] = &interfaces.FuncSingleton{ // XXX: We could set this one statically + MakeFunc: func() (*pgraph.Graph, interfaces.Func, error) { + f := val + g, err := pgraph.NewGraph("g") + if err != nil { + return nil, nil, err + } + g.AddVertex(f) + return g, f, nil + }, + } + + // NOTE: We previously considered doing a "copy singletons" here + // instead, but decided we didn't need it after all. + body, err := obj.Body.Copy() + if err != nil { + return err + } + + mutex.Lock() + obj.iterBody[ptr] = body + // XXX: Do we fake our map by giving each key an index too? + // NOTE: We can't do append since the key might not be an int. + //obj.iterBody = append(obj.iterBody, body) // not possible + mutex.Unlock() + + // Create a subgraph from the lambda's body, instantiating the + // lambda's parameters with the args and the other variables + // with the nodes in the captured environment. + subgraph, err := body.Graph(extendedEnv) + if err != nil { + return errwrap.Wrapf(err, "could not create the lambda body's subgraph") + } + + innerTxn.AddGraph(subgraph) + + // We don't need an output func because body.Graph is a + // statement and it doesn't return an interfaces.Func, + // only the expression versions return those! + return nil + } + + // Add a vertex for the map passing itself. + edgeName := structs.ForKVFuncArgNameMap + forKVFunc := &structs.ForKVFunc{ + KeyType: obj.keyParam.typ, + ValType: obj.valParam.typ, + + EdgeName: edgeName, + + SetOnIterBody: setOnIterBody, + ClearIterBody: func(length int) { // XXX: use length? + mutex.Lock() + obj.iterBody = map[types.Value]interfaces.Stmt{} + mutex.Unlock() + }, + } + graph.AddVertex(forKVFunc) + graph.AddEdge(f, forKVFunc, &interfaces.FuncEdge{ + Args: []string{edgeName}, + }) + + return graph, nil +} + +// Output returns the output that this "program" produces. This output is what +// is used to build the output graph. This only exists for statements. The +// analogous function for expressions is Value. Those Value functions might get +// called by this Output function if they are needed to produce the output. +func (obj *StmtForKV) Output(table map[interfaces.Func]types.Value) (*interfaces.Output, error) { + if obj.exprPtr == nil { + return nil, ErrFuncPointerNil + } + expr, exists := table[obj.exprPtr] + if !exists { + return nil, ErrTableNoValue + } + + if obj.Body == nil { // logically body is optional + return &interfaces.Output{}, nil // XXX: test this doesn't panic anything + } + + resources := []engine.Res{} + edges := []*interfaces.Edge{} + + m := expr.Map() // must not panic! + + for key := range m { + // key and val are both an mcl types.Value + // XXX: Do we need a mutex around this iterBody access? + if _, exists := obj.iterBody[key]; !exists { + // programming error + return nil, fmt.Errorf("programming error on key: %s", key) + } + output, err := obj.iterBody[key].Output(table) + if err != nil { + return nil, err + } + + if output != nil { + resources = append(resources, output.Resources...) + edges = append(edges, output.Edges...) + } + } + + return &interfaces.Output{ + Resources: resources, + Edges: edges, + }, nil +} + // StmtProg represents a list of stmt's. This usually occurs at the top-level of // any program, and often within an if stmt. It also contains the logic so that // the bind statement's are correctly applied in this scope, and irrespective of @@ -3804,6 +4307,25 @@ func (obj *StmtProg) Ordering(produces map[string]interfaces.Node) (*pgraph.Grap // } // prod[uid2] = stmt // store //} + //if stmt, ok := x.(*StmtForKV); ok { + // if stmt.Key == "" { + // return nil, nil, fmt.Errorf("missing index name") + // } + // uid1 := varOrderingPrefix + stmt.Key // ordering id + // if n, exists := prod[uid1]; exists { + // return nil, nil, fmt.Errorf("duplicate assignment to `%s`, have: %s", uid1, n) + // } + // prod[uid1] = stmt // store + // + // if stmt.Val == "" { + // return nil, nil, fmt.Errorf("missing val name") + // } + // uid2 := varOrderingPrefix + stmt.Val // ordering id + // if n, exists := prod[uid2]; exists { + // return nil, nil, fmt.Errorf("duplicate assignment to `%s`, have: %s", uid2, n) + // } + // prod[uid2] = stmt // store + //} } newProduces := CopyNodeMapping(produces) // don't modify the input map! @@ -5060,7 +5582,7 @@ func (obj *StmtProg) Output(table map[interfaces.Func]types.Value) (*interfaces. func (obj *StmtProg) IsModuleUnsafe() error { // TODO: rename this function? for _, x := range obj.Body { // stmt's allowed: import, bind, func, class - // stmt's not-allowed: for, if, include, res, edge + // stmt's not-allowed: for, forkv, if, include, res, edge switch x.(type) { case *StmtImport: case *StmtBind: diff --git a/lang/funcs/structs/forkv.go b/lang/funcs/structs/forkv.go new file mode 100644 index 00000000..6380bcf7 --- /dev/null +++ b/lang/funcs/structs/forkv.go @@ -0,0 +1,314 @@ +// Mgmt +// Copyright (C) James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this program, or any covered work, by linking or combining it +// with embedded mcl code and modules (and that the embedded mcl code and +// modules which link with this program, contain a copy of their source code in +// the authoritative form) containing parts covered by the terms of any other +// license, the licensors of this program grant you additional permission to +// convey the resulting work. Furthermore, the licensors of this program grant +// the original author, James Shubin, additional permission to update this +// additional permission if he deems it necessary to achieve the goals of this +// additional permission. + +package structs + +import ( + "context" + "fmt" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/util/errwrap" +) + +const ( + // ForKVFuncName is the unique name identifier for this function. + ForKVFuncName = "forkv" + + // ForKVFuncArgNameMap is the name for the edge which connects the input + // map to CallFunc. + ForKVFuncArgNameMap = "map" +) + +// ForKVFunc receives a map from upstream. We iterate over the received map to +// build a subgraph that processes each key and val, and in doing so we get a +// larger function graph. This is rebuilt as necessary if the input map changes. +type ForKVFunc struct { + KeyType *types.Type + ValType *types.Type + + EdgeName string // name of the edge used + + SetOnIterBody func(innerTxn interfaces.Txn, ptr types.Value, key, val interfaces.Func) error + ClearIterBody func(length int) + + init *interfaces.Init + + lastForKVMap types.Value // remember the last value + lastInputMapLength int // remember the last input map length +} + +// String returns a simple name for this function. This is needed so this struct +// can satisfy the pgraph.Vertex interface. +func (obj *ForKVFunc) String() string { + return ForKVFuncName +} + +// Validate makes sure we've built our struct properly. +func (obj *ForKVFunc) Validate() error { + if obj.KeyType == nil { + return fmt.Errorf("must specify a type") + } + if obj.ValType == nil { + return fmt.Errorf("must specify a type") + } + + // TODO: maybe we can remove this if we use this for core functions... + if obj.EdgeName == "" { + return fmt.Errorf("must specify an edge name") + } + + return nil +} + +// Info returns some static info about itself. +func (obj *ForKVFunc) Info() *interfaces.Info { + var typ *types.Type + + if obj.KeyType != nil && obj.ValType != nil { // don't panic if called speculatively + // XXX: Improve function engine so it can return no value? + //typ = types.NewType(fmt.Sprintf("func(%s map{%s: %s})", obj.EdgeName, obj.KeyType, obj.ValType)) // returns nothing + // XXX: Temporary float type to prove we're dropping the output since we don't use it. + typ = types.NewType(fmt.Sprintf("func(%s map{%s: %s}) float", obj.EdgeName, obj.KeyType, obj.ValType)) + } + + return &interfaces.Info{ + Pure: true, + Memo: false, // TODO: ??? + Sig: typ, + Err: obj.Validate(), + } +} + +// Init runs some startup code for this composite function. +func (obj *ForKVFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.lastForKVMap = nil + obj.lastInputMapLength = -1 + return nil +} + +// Stream takes an input struct in the format as described in the Func and Graph +// methods of the Expr, and returns the actual expected value as a stream based +// on the changing inputs to that value. +func (obj *ForKVFunc) Stream(ctx context.Context) error { + defer close(obj.init.Output) // the sender closes + + // A Func to send input maps to the subgraph. The Txn.Erase() call + // ensures that this Func is not removed when the subgraph is recreated, + // so that the function graph can propagate the last map we received to + // the subgraph. + inputChan := make(chan types.Value) + subgraphInput := &ChannelBasedSourceFunc{ + Name: "subgraphInput", + Source: obj, + Chan: inputChan, + Type: obj.mapType(), + } + obj.init.Txn.AddVertex(subgraphInput) + if err := obj.init.Txn.Commit(); err != nil { + return errwrap.Wrapf(err, "commit error in Stream") + } + obj.init.Txn.Erase() // prevent the next Reverse() from removing subgraphInput + defer func() { + close(inputChan) + obj.init.Txn.Reverse() + obj.init.Txn.DeleteVertex(subgraphInput) + obj.init.Txn.Commit() + }() + + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + obj.init.Input = nil // block looping back here + //canReceiveMoreMapValues = false + // We don't ever shutdown here, since even if we + // don't get more maps, that last map value is + // still propagating inside of the subgraph and + // so we don't want to shutdown since that would + // reverse the txn which we only do at the very + // end on graph shutdown. + continue + } + + forKVMap, exists := input.Struct()[obj.EdgeName] + if !exists { + return fmt.Errorf("programming error, can't find edge") + } + + // It's important to have this compare step to avoid + // redundant graph replacements which slow things down, + // but also cause the engine to lock, which can preempt + // the process scheduler, which can cause duplicate or + // unnecessary re-sending of values here, which causes + // the whole process to repeat ad-nauseum. + n := len(forKVMap.Map()) + + // If the keys are the same, that's enough! We don't + // need to rebuild the graph unless any of the keys + // change, since those are our unique identifiers into + // the whole loop. As a result, we don't compare between + // the entire two map, since while we could rebuild the + // graph on any change, it's easier to leave it as is + // and simply push new values down the already built + // graph if any value changes. + if obj.lastInputMapLength != n || obj.cmpMapKeys(forKVMap) != nil { + // TODO: Technically we only need to save keys! + obj.lastForKVMap = forKVMap + obj.lastInputMapLength = n + // replaceSubGraph uses the above two values + if err := obj.replaceSubGraph(subgraphInput); err != nil { + return errwrap.Wrapf(err, "could not replace subgraph") + } + } + + // send the new input map to the subgraph + select { + case inputChan <- forKVMap: + case <-ctx.Done(): + return nil + } + + case <-ctx.Done(): + return nil + } + + select { + case obj.init.Output <- &types.FloatValue{ + V: 42.0, // XXX: temporary + }: + case <-ctx.Done(): + return nil + } + } +} + +func (obj *ForKVFunc) replaceSubGraph(subgraphInput interfaces.Func) error { + // delete the old subgraph + if err := obj.init.Txn.Reverse(); err != nil { + return errwrap.Wrapf(err, "could not Reverse") + } + + obj.ClearIterBody(obj.lastInputMapLength) // XXX: pass in size? + + forKVMap := obj.lastForKVMap.Map() + // XXX: Should we loop in a deterministic order? + // XXX: Should our type support the new iterator pattern? + for k := range forKVMap { + ptr := k + argNameKey := "forkvInputMapKey" + argNameVal := "forkvInputMapVal" + + // the key + inputElemFuncKey := SimpleFnToDirectFunc( + fmt.Sprintf("forkvInputElemKey[%v]", ptr), + &types.FuncValue{ + V: func(_ context.Context, args []types.Value) (types.Value, error) { + if len(args) != 1 { + return nil, fmt.Errorf("inputElemFuncKey: expected a single argument") + } + //arg := args[0] + //m, ok := arg.(*types.MapValue) + //if !ok { + // return nil, fmt.Errorf("inputElemFuncKey: expected a MapValue argument") + //} + // XXX: If we had some sort of index fn? + //return m.Map().Index(?), nil + return k, nil + }, + T: types.NewType(fmt.Sprintf("func(%s %s) %s", argNameKey, obj.mapType(), obj.KeyType)), + }, + ) + obj.init.Txn.AddVertex(inputElemFuncKey) + + obj.init.Txn.AddEdge(subgraphInput, inputElemFuncKey, &interfaces.FuncEdge{ + Args: []string{argNameKey}, + }) + + // the val + inputElemFuncVal := SimpleFnToDirectFunc( + fmt.Sprintf("forkvInputElemVal[%v]", ptr), + &types.FuncValue{ + V: func(_ context.Context, args []types.Value) (types.Value, error) { + if len(args) != 1 { + return nil, fmt.Errorf("inputElemFuncVal: expected a single argument") + } + //return v, nil // If we always rebuild the map. + arg := args[0] + m, ok := arg.(*types.MapValue) + if !ok { + return nil, fmt.Errorf("inputElemFuncVal: expected a MapValue argument") + } + return m.Map()[ptr], nil + }, + T: types.NewType(fmt.Sprintf("func(%s %s) %s", argNameVal, obj.mapType(), obj.ValType)), + }, + ) + obj.init.Txn.AddVertex(inputElemFuncVal) + + obj.init.Txn.AddEdge(subgraphInput, inputElemFuncVal, &interfaces.FuncEdge{ + Args: []string{argNameVal}, + }) + + if err := obj.SetOnIterBody(obj.init.Txn, ptr, inputElemFuncKey, inputElemFuncVal); err != nil { + return errwrap.Wrapf(err, "could not call SetOnIterBody()") + } + } + + return obj.init.Txn.Commit() +} + +func (obj *ForKVFunc) mapType() *types.Type { + return types.NewType(fmt.Sprintf("map{%s: %s}", obj.KeyType, obj.ValType)) +} + +// cmpMapKeys compares the input map with the cached private lastForKVMap field. +// If either are nil, or if the keys of the maps are not identical, then this +// errors. +func (obj *ForKVFunc) cmpMapKeys(m types.Value) error { + if obj.lastForKVMap == nil || m == nil { + return fmt.Errorf("got a nil map") + } + + m1 := obj.lastForKVMap.Map() + m2 := m.(*types.MapValue) // must not panic! + if len(m1) != len(m.Map()) { + return fmt.Errorf("lengths differ") + } + + for k := range m1 { + if _, exists := m2.Lookup(k); !exists { + return fmt.Errorf("key not found") + } + } + + return nil +} diff --git a/lang/interpret_test/TestAstFunc2/stmtforforkv00.txtar b/lang/interpret_test/TestAstFunc2/stmtforforkv00.txtar new file mode 100644 index 00000000..56553002 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforforkv00.txtar @@ -0,0 +1,31 @@ +-- main.mcl -- +import "fmt" + +$map0 = {0 => "a", 1 => "b", 2 => "c",} +$map1 = {0 => "d", 1 => "e", 2 => "f",} +$map2 = {0 => "g", 1 => "h", 2 => "i",} +$map3 = {0 => "j", 1 => "k", 2 => "l",} + +$list = [$map0, $map1, $map2, $map3,] + +for $index, $value in $list { + forkv $key, $val in $value { + + $s = fmt.printf("%s is %d", $val, $key+$index) + test [$s,] {} + } +} + +-- OUTPUT -- +Vertex: test[a is 0] +Vertex: test[b is 1] +Vertex: test[c is 2] +Vertex: test[d is 1] +Vertex: test[e is 2] +Vertex: test[f is 3] +Vertex: test[g is 2] +Vertex: test[h is 3] +Vertex: test[i is 4] +Vertex: test[j is 3] +Vertex: test[k is 4] +Vertex: test[l is 5] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv00.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv00.txtar new file mode 100644 index 00000000..29ff2e5e --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv00.txtar @@ -0,0 +1,13 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +forkv $key, $val in $map { + test [$val,] {} +} + +-- OUTPUT -- +Vertex: test[a] +Vertex: test[b] +Vertex: test[c] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv01.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv01.txtar new file mode 100644 index 00000000..7a1a8f5e --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv01.txtar @@ -0,0 +1,15 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +forkv $key, $val in $map { + $s = $val # our first major bug was triggered by this! + test [$s,] {} +} + +# The buggy version would return "test[a]" three times! +-- OUTPUT -- +Vertex: test[a] +Vertex: test[b] +Vertex: test[c] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv02.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv02.txtar new file mode 100644 index 00000000..25a764bb --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv02.txtar @@ -0,0 +1,13 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +forkv $key, $val in $map { + test [fmt.printf("%s is %d", $val, $key),] {} +} + +-- OUTPUT -- +Vertex: test[a is 0] +Vertex: test[b is 1] +Vertex: test[c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv03.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv03.txtar new file mode 100644 index 00000000..e9c1183b --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv03.txtar @@ -0,0 +1,14 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +forkv $key, $val in $map { + $s = fmt.printf("%s is %d", $val, $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[a is 0] +Vertex: test[b is 1] +Vertex: test[c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv04.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv04.txtar new file mode 100644 index 00000000..85a6c2b0 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv04.txtar @@ -0,0 +1,16 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$s = "nope" # should be out of scope + +forkv $key, $val in $map { + $s = fmt.printf("%s is %d", $val, $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[a is 0] +Vertex: test[b is 1] +Vertex: test[c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv05.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv05.txtar new file mode 100644 index 00000000..b589fee5 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv05.txtar @@ -0,0 +1,16 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$val = "nope" # should be out of scope + +forkv $key, $val in $map { + $s = fmt.printf("%s is %d", $val, $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[a is 0] +Vertex: test[b is 1] +Vertex: test[c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv06.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv06.txtar new file mode 100644 index 00000000..a18a0f23 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv06.txtar @@ -0,0 +1,16 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$val = 42 # should be out of scope (also not the same type) + +forkv $key, $val in $map { + $s = fmt.printf("%s is %d", $val, $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[a is 0] +Vertex: test[b is 1] +Vertex: test[c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv07.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv07.txtar new file mode 100644 index 00000000..29a5d424 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv07.txtar @@ -0,0 +1,17 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + $key = 42 + $s = fmt.printf("%s is %d", $val, $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[a is 42] +Vertex: test[b is 42] +Vertex: test[c is 42] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv08.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv08.txtar new file mode 100644 index 00000000..e730e090 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv08.txtar @@ -0,0 +1,20 @@ +-- main.mcl -- +import "fmt" +import "math" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + $key = if math.mod($key, 2) == 0 { + $key + } else { + 42 + } + $s = fmt.printf("%s is %d", $val, $key) + test [$s,] {} +} + +-- OUTPUT -- +# err: errSetScope: recursive reference while setting scope: not a dag diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv09.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv09.txtar new file mode 100644 index 00000000..fa7ff9b3 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv09.txtar @@ -0,0 +1,22 @@ +-- main.mcl -- +import "fmt" +import "math" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + $newkey = if math.mod($key, 2) == 0 { + $key + } else { + 42 + } + $s = fmt.printf("%s is %d", $val, $newkey) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[a is 0] +Vertex: test[b is 42] +Vertex: test[c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv10.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv10.txtar new file mode 100644 index 00000000..aa445960 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv10.txtar @@ -0,0 +1,21 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + $fn = func($x) { + "hello " + $x + } + + $s = fmt.printf("%s is %d", $fn($val), $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a is 0] +Vertex: test[hello b is 1] +Vertex: test[hello c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv11.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv11.txtar new file mode 100644 index 00000000..9dbd7c0d --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv11.txtar @@ -0,0 +1,21 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + func fn($x) { + "hello " + $x + } + + $s = fmt.printf("%s is %d", fn($val), $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a is 0] +Vertex: test[hello b is 1] +Vertex: test[hello c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv12.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv12.txtar new file mode 100644 index 00000000..13c68df5 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv12.txtar @@ -0,0 +1,21 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + $fn = func($x) { + "hello " + $val + } + + $s = fmt.printf("%s is %d", $fn("whatever"), $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a is 0] +Vertex: test[hello b is 1] +Vertex: test[hello c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv13.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv13.txtar new file mode 100644 index 00000000..775226d3 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv13.txtar @@ -0,0 +1,21 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + func fn($x) { + "hello " + $val + } + + $s = fmt.printf("%s is %d", fn("whatever"), $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a is 0] +Vertex: test[hello b is 1] +Vertex: test[hello c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv14.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv14.txtar new file mode 100644 index 00000000..21251599 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv14.txtar @@ -0,0 +1,23 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + $fn = func($x) { + "hello" + $value + } + + $value = " " + $val + + $s = fmt.printf("%s is %d", $fn("whatever"), $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a is 0] +Vertex: test[hello b is 1] +Vertex: test[hello c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv15.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv15.txtar new file mode 100644 index 00000000..545abedd --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv15.txtar @@ -0,0 +1,23 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + func fn($x) { + "hello" + $value + } + + $value = " " + $val + + $s = fmt.printf("%s is %d", fn("whatever"), $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a is 0] +Vertex: test[hello b is 1] +Vertex: test[hello c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv16.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv16.txtar new file mode 100644 index 00000000..f30f67aa --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv16.txtar @@ -0,0 +1,22 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + class foo($x) { + $result = "hello " + $x + } + include foo($val) as included + + $s = fmt.printf("%s is %d", $included.result, $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a is 0] +Vertex: test[hello b is 1] +Vertex: test[hello c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv17.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv17.txtar new file mode 100644 index 00000000..45fd1a2a --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv17.txtar @@ -0,0 +1,22 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$word = "hello" + +forkv $key, $val in $map { + + class foo($x) { + $result = $word + " " + $x + } + include foo($val) as included + + $s = fmt.printf("%s is %d", $included.result, $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a is 0] +Vertex: test[hello b is 1] +Vertex: test[hello c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv18.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv18.txtar new file mode 100644 index 00000000..5d8938be --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv18.txtar @@ -0,0 +1,22 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + class foo($x) { + $result = "hello " + $val + } + include foo("whatever") as included + + $s = fmt.printf("%s is %d", $included.result, $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a is 0] +Vertex: test[hello b is 1] +Vertex: test[hello c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv19.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv19.txtar new file mode 100644 index 00000000..09146c6c --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv19.txtar @@ -0,0 +1,22 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$word = "hello" + +forkv $key, $val in $map { + + class foo($x) { + $result = $word + " " + $val + } + include foo("whatever") as included + + $s = fmt.printf("%s is %d", $included.result, $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a is 0] +Vertex: test[hello b is 1] +Vertex: test[hello c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv20.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv20.txtar new file mode 100644 index 00000000..55704d64 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv20.txtar @@ -0,0 +1,24 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + class foo($x) { + $result = "hello" + $value + } + include foo("whatever") as included + + $value = " " + $val + + $s = fmt.printf("%s is %d", $included.result, $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a is 0] +Vertex: test[hello b is 1] +Vertex: test[hello c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv21.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv21.txtar new file mode 100644 index 00000000..c7c77030 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv21.txtar @@ -0,0 +1,24 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$word = "hello" + +forkv $key, $val in $map { + + class foo($x) { + $result = $word + $value + } + include foo("whatever") as included + + $value = " " + $val + + $s = fmt.printf("%s is %d", $included.result, $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a is 0] +Vertex: test[hello b is 1] +Vertex: test[hello c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv22.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv22.txtar new file mode 100644 index 00000000..32dcaf29 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv22.txtar @@ -0,0 +1,24 @@ +-- main.mcl -- +import "fmt" + +$map1 = {0 => "a", 1 => "b", 2 => "c",} +$map2 = {"x" => 42, "y" => 13, "z" => -4,} + +forkv $key1, $val1 in $map1 { + forkv $key2, $val2 in $map2 { + + $s = fmt.printf("%s is %d", $val1, $val2) + test [$s,] {} + } +} + +-- OUTPUT -- +Vertex: test[a is 42] +Vertex: test[b is 42] +Vertex: test[c is 42] +Vertex: test[a is 13] +Vertex: test[b is 13] +Vertex: test[c is 13] +Vertex: test[a is -4] +Vertex: test[b is -4] +Vertex: test[c is -4] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv23.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv23.txtar new file mode 100644 index 00000000..9f83e4e1 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv23.txtar @@ -0,0 +1,31 @@ +-- main.mcl -- +import "fmt" + +$map0 = {0 => "a", 1 => "b", 2 => "c",} +$map1 = {0 => "d", 1 => "e", 2 => "f",} +$map2 = {0 => "g", 1 => "h", 2 => "i",} +$map3 = {0 => "j", 1 => "k", 2 => "l",} + +$map = {0 => $map0, 1 => $map1, 2 => $map2, 3 => $map3,} + +forkv $key, $val in $map { + forkv $i, $v in $val { + + $s = fmt.printf("%s is %d", $v, $i+$key) + test [$s,] {} + } +} + +-- OUTPUT -- +Vertex: test[a is 0] +Vertex: test[b is 1] +Vertex: test[c is 2] +Vertex: test[d is 1] +Vertex: test[e is 2] +Vertex: test[f is 3] +Vertex: test[g is 2] +Vertex: test[h is 3] +Vertex: test[i is 4] +Vertex: test[j is 3] +Vertex: test[k is 4] +Vertex: test[l is 5] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv24.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv24.txtar new file mode 100644 index 00000000..5afef44e --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv24.txtar @@ -0,0 +1,17 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a",} + +forkv $key, $val in $map { + + $fn = func() { + "hello " + $val + } + + $s = $fn() + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv25.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv25.txtar new file mode 100644 index 00000000..5bb46be7 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv25.txtar @@ -0,0 +1,17 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a",} + +forkv $key, $val in $map { + + func fn() { + "hello " + $val + } + + $s = fn() + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv26.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv26.txtar new file mode 100644 index 00000000..8ae4a896 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv26.txtar @@ -0,0 +1,41 @@ +-- main.mcl -- +import "fmt" + +$map1 = {0 => "a", 1 => "b", 2 => "c",} +$map2 = {0 => "x", 1 => "y", 2 => "z",} + +$word = "hello" + +forkv $key1, $val1 in $map1 { + forkv $key2, $val2 in $map2 { + + class foo($x, $y) { + $result = "hello " + $x + $y + $val1 + $val2 + $result1 = $x + $val1 + $result2 = $y + $val2 + } + include foo($val1, $val2) as included + + $s = fmt.printf("%s is {%d,%d}", $included.result, $key1, $key2) + $s1 = fmt.printf("one: %s", $included.result1) + $s2 = fmt.printf("two: %s", $included.result2) + test [$s, $s1, $s2,] {} + } +} + +-- OUTPUT -- +Vertex: test[hello axax is {0,0}] +Vertex: test[hello ayay is {0,1}] +Vertex: test[hello azaz is {0,2}] +Vertex: test[hello bxbx is {1,0}] +Vertex: test[hello byby is {1,1}] +Vertex: test[hello bzbz is {1,2}] +Vertex: test[hello cxcx is {2,0}] +Vertex: test[hello cycy is {2,1}] +Vertex: test[hello czcz is {2,2}] +Vertex: test[one: aa] +Vertex: test[one: bb] +Vertex: test[one: cc] +Vertex: test[two: xx] +Vertex: test[two: yy] +Vertex: test[two: zz] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv27.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv27.txtar new file mode 100644 index 00000000..1ade0651 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv27.txtar @@ -0,0 +1,38 @@ +-- main.mcl -- +import "fmt" + +$map1 = {0 => "a", 1 => "b", 2 => "c",} +$map2 = {0 => "x", 1 => "y", 2 => "z",} + +$word = "hello" + +forkv $key1, $val1 in $map1 { + + class foo($x, $y) { + $result = "hello " + $x + $y + $val1 + $result1 = $x + $val1 + } + + forkv $key2, $val2 in $map2 { + + include foo($val1, $val2) as included + + $s = fmt.printf("%s is {%d,%d}", $included.result, $key1, $key2) + $s1 = fmt.printf("one: %s", $included.result1) + test [$s, $s1,] {} + } +} + +-- OUTPUT -- +Vertex: test[hello axa is {0,0}] +Vertex: test[hello aya is {0,1}] +Vertex: test[hello aza is {0,2}] +Vertex: test[hello bxb is {1,0}] +Vertex: test[hello byb is {1,1}] +Vertex: test[hello bzb is {1,2}] +Vertex: test[hello cxc is {2,0}] +Vertex: test[hello cyc is {2,1}] +Vertex: test[hello czc is {2,2}] +Vertex: test[one: aa] +Vertex: test[one: bb] +Vertex: test[one: cc] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv28.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv28.txtar new file mode 100644 index 00000000..1d2273c2 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv28.txtar @@ -0,0 +1,19 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +forkv $i, $x in $map { + + func foo($y) { + "hello" + $x + $y + } + + $s = foo($x) + + test [$s,] {} +} +-- OUTPUT -- +Vertex: test[helloaa] +Vertex: test[hellobb] +Vertex: test[hellocc] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv29.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv29.txtar new file mode 100644 index 00000000..6e63f286 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv29.txtar @@ -0,0 +1,22 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + class foo() { + test [$val + fmt.printf("%d", $key),] {} + } + include foo() # as included + + #$s = fmt.printf("%s is %d", $included.result, $key) + #test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[a0] +Vertex: test[b1] +Vertex: test[c2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv30.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv30.txtar new file mode 100644 index 00000000..1e905f3e --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv30.txtar @@ -0,0 +1,23 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + class foo($x) { + $result = "hello " + $x + test [$result,] {} + } + include foo($val) # as included + + #$s = fmt.printf("%s is %d", $included.result, $key) + #test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a] +Vertex: test[hello b] +Vertex: test[hello c] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv31.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv31.txtar new file mode 100644 index 00000000..978bbf40 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv31.txtar @@ -0,0 +1,24 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + class foo($x) { + func result($s) { + $s + $x + $val + } + } + include foo($val) as included + + $s = fmt.printf("%s is %d", included.result($val), $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[aaa is 0] +Vertex: test[bbb is 1] +Vertex: test[ccc is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv32.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv32.txtar new file mode 100644 index 00000000..cb3c01ff --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv32.txtar @@ -0,0 +1,24 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + class foo($x) { + $result = "hello" + $x + $val + } + include foo($val) as thing + $result = "please" + + # XXX: add $thing.some_func and so on... add more tests says sam. + $s = fmt.printf("%s is %d is %s", $thing.result, $key, $result) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[helloaa is 0 is please] +Vertex: test[hellobb is 1 is please] +Vertex: test[hellocc is 2 is please] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv33.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv33.txtar new file mode 100644 index 00000000..a87051aa --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv33.txtar @@ -0,0 +1,24 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + class foo($x) { + $result = func($s) { + $s + $x + $val + } + } + include foo($val) as included + + $s = fmt.printf("%s is %d", $included.result($val), $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[aaa is 0] +Vertex: test[bbb is 1] +Vertex: test[ccc is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv34.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv34.txtar new file mode 100644 index 00000000..df187101 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv34.txtar @@ -0,0 +1,23 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + class foo() { + $result = "hello " + $val + fmt.printf("%d", $key) + test [$result,] {} + } + include foo() # as included + + #$s = fmt.printf("%s is %d", $included.result, $key) + #test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a0] +Vertex: test[hello b1] +Vertex: test[hello c2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv35.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv35.txtar new file mode 100644 index 00000000..f0eb3d0c --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv35.txtar @@ -0,0 +1,32 @@ +-- main.mcl -- +import "fmt" + +$map1 = {0 => "a", 1 => "b", 2 => "c",} +$map2 = {0 => "x", 1 => "y", 2 => "z",} + +$word = "hello" + +class foo($x, $y) { + $result = "hello " + $x + $y +} + +forkv $key1, $val1 in $map1 { + forkv $key2, $val2 in $map2 { + + include foo($val1, $val2) as included + + $s = fmt.printf("%s is {%d,%d}", $included.result, $key1, $key2) + test [$s,] {} + } +} + +-- OUTPUT -- +Vertex: test[hello ax is {0,0}] +Vertex: test[hello ay is {0,1}] +Vertex: test[hello az is {0,2}] +Vertex: test[hello bx is {1,0}] +Vertex: test[hello by is {1,1}] +Vertex: test[hello bz is {1,2}] +Vertex: test[hello cx is {2,0}] +Vertex: test[hello cy is {2,1}] +Vertex: test[hello cz is {2,2}] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv36.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv36.txtar new file mode 100644 index 00000000..d18500ac --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv36.txtar @@ -0,0 +1,29 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +#$key = 42 # should be out of scope + +forkv $key, $val in $map { + + class foo($x) { + #$result = "hello" + $x + $val # harder + #$result = $val # works + #$result = $x # works + $resultx = "hello" + $x # harder + #$result = "hello" + $val # harder + #$result = $x + $val # harder + } + include foo($val)# as included + $result = "please" + + # XXX: add $included.some_func and so on... add more tests says sam. + $s = fmt.printf("%s is %d is %s", $val, $key, $result) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[a is 0 is please] +Vertex: test[b is 1 is please] +Vertex: test[c is 2 is please] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv37.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv37.txtar new file mode 100644 index 00000000..c25a6537 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv37.txtar @@ -0,0 +1,28 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +$key = 42 # should be out of scope + +forkv $key, $val in $map { + + class foo($x) { + $result = func($y1) { + "hello" + $x + $val + $y1 + } + } + include foo($val) as thing + $result = func($y2) { + "please" + $y2 + } + + # XXX: add $thing.some_func and so on... add more tests says sam. + $s = fmt.printf("%s is %d is %s", $thing.result("!"), $key, $result("!")) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[helloaa! is 0 is please!] +Vertex: test[hellobb! is 1 is please!] +Vertex: test[hellocc! is 2 is please!] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv38.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv38.txtar new file mode 100644 index 00000000..34f7c073 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv38.txtar @@ -0,0 +1,21 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +class foo($x) { + $result = "hello " + $x +} + +forkv $key, $val in $map { + + include foo($val) as included + + $s = fmt.printf("%s is %d", $included.result, $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello a is 0] +Vertex: test[hello b is 1] +Vertex: test[hello c is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv39.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv39.txtar new file mode 100644 index 00000000..6d8508fd --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv39.txtar @@ -0,0 +1,21 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +class foo($x) { + $result = "hello " + fmt.printf("%d", $x) +} + +forkv $key, $val in $map { + + include foo($key) as included + + $s = fmt.printf("%s is %d", $included.result, $key) + test [$s,] {} +} + +-- OUTPUT -- +Vertex: test[hello 0 is 0] +Vertex: test[hello 1 is 1] +Vertex: test[hello 2 is 2] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv40.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv40.txtar new file mode 100644 index 00000000..134153c3 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv40.txtar @@ -0,0 +1,10 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +forkv $key, $val in $map { + $foo = $key # does nothing +} + +-- OUTPUT -- diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv41.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv41.txtar new file mode 100644 index 00000000..de91038f --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv41.txtar @@ -0,0 +1,24 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +import "thing.mcl" # works + +forkv $key, $val in $map { + # The semantics are that only one copy of an import is needed... Not one per iteration. + # XXX: Error: could not find `inside` in env for ExprIterated + # XXX: I added a hack to catch this obvious case + #import "thing.mcl" # XXX: doesn't work :( + + $x = 42 + $thing.inside + + $s = fmt.printf("%s is %d = %d", $val, $key, $x) + test [$s,] {} +} +-- thing.mcl -- +$inside = 13 +-- OUTPUT -- +Vertex: test[a is 0 = 55] +Vertex: test[b is 1 = 55] +Vertex: test[c is 2 = 55] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv42.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv42.txtar new file mode 100644 index 00000000..d6f484cc --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv42.txtar @@ -0,0 +1,27 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +import "thing.mcl" # works + +forkv $key, $val in $map { + # The semantics are that only one copy of an import is needed... Not one per iteration. + # XXX: Error: could not find `inside` in env for ExprIterated + class foo($y) { + #import "thing.mcl" # XXX: doesn't work :( + $out = $y + 7 + $thing.inside + } + include foo($key) as usefoo + + $x = 42 + $usefoo.out + + $s = fmt.printf("%s is %d = %d", $val, $key, $x) + test [$s,] {} +} +-- thing.mcl -- +$inside = 13 +-- OUTPUT -- +Vertex: test[a is 0 = 62] +Vertex: test[b is 1 = 63] +Vertex: test[c is 2 = 64] diff --git a/lang/interpret_test/TestAstFunc2/stmtforkv43.txtar b/lang/interpret_test/TestAstFunc2/stmtforkv43.txtar new file mode 100644 index 00000000..0f3c4f92 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtforkv43.txtar @@ -0,0 +1,22 @@ +-- main.mcl -- +import "fmt" + +$map = {0 => "a", 1 => "b", 2 => "c",} + +#import "thing.mcl" # works + +forkv $key, $val in $map { + # The semantics are that only one copy of an import is needed... Not one per iteration. + # XXX: Error: could not find `inside` in env for ExprIterated + # XXX: We don't want this to be an error, but it is for now. + import "thing.mcl" # XXX: doesn't work :( + + $x = 42 + $thing.inside + + $s = fmt.printf("%s is %d = %d", $val, $key, $x) + test [$s,] {} +} +-- thing.mcl -- +$inside = 13 +-- OUTPUT -- +# err: errInit: a StmtImport can't be contained inside a StmtForKV diff --git a/lang/parser/lexer.nex b/lang/parser/lexer.nex index 5078a94b..015e5a16 100644 --- a/lang/parser/lexer.nex +++ b/lang/parser/lexer.nex @@ -159,6 +159,11 @@ lval.str = yylex.Text() return FOR } +/forkv/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return FORKV + } /\->/ { yylex.pos(lval) // our pos lval.str = yylex.Text() diff --git a/lang/parser/parser.y b/lang/parser/parser.y index d77b4ff8..d3fb6b01 100644 --- a/lang/parser/parser.y +++ b/lang/parser/parser.y @@ -92,7 +92,7 @@ func init() { %token OPEN_CURLY CLOSE_CURLY %token OPEN_PAREN CLOSE_PAREN %token OPEN_BRACK CLOSE_BRACK -%token IF ELSE FOR +%token IF ELSE FOR FORKV %token BOOL STRING INTEGER FLOAT %token EQUALS DOLLAR %token COMMA COLON SEMICOLON @@ -228,6 +228,18 @@ stmt: } locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } + // iterate over maps + // `forkv $key, $val in $map { }` +| FORKV var_identifier COMMA var_identifier IN expr OPEN_CURLY prog CLOSE_CURLY + { + $$.stmt = &ast.StmtForKV{ + Key: $2.str, // no $ prefix + Val: $4.str, // no $ prefix + Expr: $6.expr, // XXX: name this Map ? + Body: $8.stmt, + } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) + } // this is the named version, iow, a user-defined function (statement) // `func name() { }` // `func name() { }`