cmd/cue/cmd: allow task hierarchy

Change-Id: I5a14d62126739e776061b36ad33aae5d54c64715
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4442
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/custom.go b/cmd/cue/cmd/custom.go
index 60a9576..39684db 100644
--- a/cmd/cue/cmd/custom.go
+++ b/cmd/cue/cmd/custom.go
@@ -43,7 +43,6 @@
 
 const (
 	commandSection = "command"
-	taskSection    = "task"
 )
 
 func lookupString(obj cue.Value, key, def string) string {
@@ -113,91 +112,112 @@
 	return sub, nil
 }
 
-type taskKey struct {
-	typ  string
+type customRunner struct {
 	name string
-	task string
+	root *cue.Instance
+
+	index map[taskKey]*task
 }
 
-func (k taskKey) keyForTask(taskName string) taskKey {
-	k.task = taskName
-	return k
+type taskKey string
+
+func (r *customRunner) keyForTask(t *task) taskKey {
+	a := []string{commandSection, r.name}
+	return keyForReference(append(a, t.path...)...)
 }
 
-func keyForReference(ref []string) (k taskKey) {
-	// command <command> task <task>
-	if len(ref) >= 4 && ref[2] == taskSection {
-		k.typ = ref[0]
-		k.name = ref[1]
-		k.task = ref[3]
-	}
-	return k
+func keyForReference(ref ...string) (k taskKey) {
+	return taskKey(strings.Join(ref, "\000"))
 }
 
-func (k taskKey) taskPath(task string) []string {
-	k.task = task
-	return []string{k.typ, k.name, taskSection, task}
+func (r *customRunner) taskPath(t *task) []string {
+	return append([]string{commandSection, r.name}, t.path...)
 }
 
-func (k *taskKey) lookupTasks(root *cue.Instance) cue.Value {
-	return root.Lookup(k.typ, k.name, taskSection)
+func (r *customRunner) lookupTasks() cue.Value {
+	return r.root.Lookup(commandSection, r.name)
 }
 
 func doTasks(cmd *Command, typ, command string, root *cue.Instance) error {
-	err := executeTasks(typ, command, root)
+	err := executeTasks(cmd, typ, command, root)
 	exitIfErr(cmd, root, err, true)
 	return err
 }
 
-func isTask(index map[taskKey]*task, root *cue.Instance, value cue.Value) (*task, bool) {
-	inst, path := value.Reference()
-	if path != nil && inst == root {
-		if task, ok := index[keyForReference(path)]; ok {
+func (r *customRunner) referredTask(ref cue.Value) (t *task, ok bool) {
+	inst, path := ref.Reference()
+	if path != nil && inst == r.root {
+		if task := r.findTask(path); task != nil {
 			return task, true
 		}
 	}
 	return nil, false
 }
 
+func (r *customRunner) findTask(ref []string) *task {
+	for ; len(ref) > 2; ref = ref[:len(ref)-1] {
+		if t := r.index[keyForReference(ref...)]; t != nil {
+			return t
+		}
+	}
+	return nil
+}
+
+func getTasks(q []*task, v cue.Value, stack []string) ([]*task, error) {
+	if v.Lookup("$id").Exists() || v.Lookup("kind").Exists() {
+		t, err := newTask(len(q), stack, v)
+		if err != nil {
+			return nil, err
+		}
+		return append(q, t), nil
+	}
+
+	for iter, _ := v.Fields(); iter.Next(); {
+		var err error
+		q, err = getTasks(q, iter.Value(), append(stack, iter.Label()))
+		if err != nil {
+			return nil, err
+		}
+	}
+	return q, nil
+}
+
 // executeTasks runs user-defined tasks as part of a user-defined command.
 //
 // All tasks are started at once, but will block until tasks that they depend
 // on will continue.
-func executeTasks(typ, command string, root *cue.Instance) (err error) {
-	spec := taskKey{typ, command, ""}
-	tasks := spec.lookupTasks(root)
-
-	index := map[taskKey]*task{}
+func executeTasks(cmd *Command, typ, command string, inst *cue.Instance) (err error) {
+	cr := &customRunner{
+		name:  command,
+		root:  inst,
+		index: map[taskKey]*task{},
+	}
+	tasks := cr.lookupTasks()
 
 	// Create task entries from spec.
-	queue := []*task{}
-	iter, err := tasks.Fields()
+	queue, err := getTasks(nil, tasks, nil)
 	if err != nil {
 		return err
 	}
-	for i := 0; iter.Next(); i++ {
-		t, err := newTask(i, iter.Label(), iter.Value())
-		if err != nil {
-			return err
-		}
-		queue = append(queue, t)
-		index[spec.keyForTask(iter.Label())] = t
+
+	for _, t := range queue {
+		cr.index[cr.keyForTask(t)] = t
 	}
 
 	// Mark dependencies for unresolved nodes.
 	for _, t := range queue {
-		task := tasks.Lookup(t.name)
+		task := tasks.Lookup(t.path...)
 
 		// Inject dependency in `$after` field
 		after := task.Lookup("$after")
 		if after.Err() == nil {
-			if dep, ok := isTask(index, root, after); ok {
+			if dep, ok := cr.referredTask(after); ok {
 				t.dep[dep] = true
 			}
 			iter, err := after.List()
 			if err == nil {
 				for iter.Next() {
-					if dep, ok := isTask(index, root, iter.Value()); ok {
+					if dep, ok := cr.referredTask(iter.Value()); ok {
 						t.dep[dep] = true
 					}
 				}
@@ -207,13 +227,13 @@
 		// Inject reverse dependency in `$before` field
 		before := task.Lookup("$before")
 		if before.Err() == nil {
-			if dep, ok := isTask(index, root, before); ok {
+			if dep, ok := cr.referredTask(before); ok {
 				dep.dep[t] = true
 			}
 			iter, err := before.List()
 			if err == nil {
 				for iter.Next() {
-					if dep, ok := isTask(index, root, iter.Value()); ok {
+					if dep, ok := cr.referredTask(iter.Value()); ok {
 						dep.dep[t] = true
 					}
 				}
@@ -227,9 +247,9 @@
 			if (after.Err() == nil && v.Equals(after)) || (before.Err() == nil && v.Equals(before)) {
 				return false
 			}
-			for _, r := range appendReferences(nil, root, v) {
-				if dep, ok := index[keyForReference(r)]; ok && t != dep {
-					v := root.Lookup(r...)
+			for _, r := range appendReferences(nil, cr.root, v) {
+				if dep := cr.findTask(r); dep != nil && t != dep {
+					v := cr.root.Lookup(r...)
 					if v.IsIncomplete() && v.Kind() != cue.StructKind {
 						t.dep[dep] = true
 					}
@@ -261,16 +281,16 @@
 			// code does not look up new strings in the index and that the
 			// full configuration, as used by the tasks, is pre-evaluated.
 			m.Lock()
-			obj := tasks.Lookup(t.name)
+			obj := tasks.Lookup(t.path...)
 			// NOTE: ignore the linter warning for the following line:
 			// itask.Context is an internal type and we want to break if any
 			// fields are added.
 			update, err := t.Run(&itask.Context{ctx, stdout, stderr}, obj)
 			if err == nil && update != nil {
-				root, err = root.Fill(update, spec.taskPath(t.name)...)
+				cr.root, err = cr.root.Fill(update, cr.taskPath(t)...)
 
 				if err == nil {
-					tasks = spec.lookupTasks(root)
+					tasks = cr.lookupTasks()
 				}
 			}
 			m.Unlock()
@@ -348,7 +368,7 @@
 	itask.Runner
 
 	index int
-	name  string
+	path  []string
 	done  chan error
 	dep   map[*task]bool
 }
@@ -360,7 +380,7 @@
 	"testserver": "cmd/cue/cmd.Test",
 }
 
-func newTask(index int, name string, v cue.Value) (*task, error) {
+func newTask(index int, path []string, v cue.Value) (*task, error) {
 	kind, err := v.Lookup("$id").String()
 	if err != nil {
 		// Lookup kind for backwards compatibility.
@@ -391,7 +411,7 @@
 	return &task{
 		Runner: runner,
 		index:  index,
-		name:   name,
+		path:   path,
 		done:   make(chan error),
 		dep:    make(map[*task]bool),
 	}, nil
diff --git a/cmd/cue/cmd/testdata/script/cmd_after.txt b/cmd/cue/cmd/testdata/script/cmd_after.txt
index 6849774..a2704c3 100644
--- a/cmd/cue/cmd/testdata/script/cmd_after.txt
+++ b/cmd/cue/cmd/testdata/script/cmd_after.txt
@@ -14,23 +14,21 @@
 )
 
 command: after: {
-	task: {
-		t1: exec.Run & {
-			cmd: ["sh", "-c", "sleep 2; date +%s"]
-			stdout: string
-		}
-		t2: exec.Run & {
-			cmd: ["sh", "-c", "date +%s"]
-			stdout: string
-			$after: task.t1
-		}
-		t3: exec.Run & {
-			cmd: ["sh", "-c", "a=\(strings.TrimSpace(task.t1.stdout));b=\(strings.TrimSpace(task.t2.stdout));if [ $a -le $b ]; then echo 'true'; fi"]
-			stdout: string
-		}
-		t4: cli.Print & {
-				text: task.t3.stdout
-		}
+	t1: exec.Run & {
+		cmd: ["sh", "-c", "sleep 2; date +%s"]
+		stdout: string
+	}
+	t2: exec.Run & {
+		cmd: ["sh", "-c", "date +%s"]
+		stdout: string
+		$after: t1
+	}
+	t3: exec.Run & {
+		cmd: ["sh", "-c", "a=\(strings.TrimSpace(t1.stdout));b=\(strings.TrimSpace(t2.stdout));if [ $a -le $b ]; then echo 'true'; fi"]
+		stdout: string
+	}
+	t4: cli.Print & {
+			text: t3.stdout
 	}
 }
 
diff --git a/cmd/cue/cmd/testdata/script/cmd_baddisplay.txt b/cmd/cue/cmd/testdata/script/cmd_baddisplay.txt
index a41547b..101c0d9 100644
--- a/cmd/cue/cmd/testdata/script/cmd_baddisplay.txt
+++ b/cmd/cue/cmd/testdata/script/cmd_baddisplay.txt
@@ -14,7 +14,7 @@
 package home
 
 command: baddisplay: {
-	task: display: {
+	display: {
 		kind: "print"
 		text: 42
 	}
diff --git a/cmd/cue/cmd/testdata/script/cmd_before.txt b/cmd/cue/cmd/testdata/script/cmd_before.txt
index 762509c..813bb77 100644
--- a/cmd/cue/cmd/testdata/script/cmd_before.txt
+++ b/cmd/cue/cmd/testdata/script/cmd_before.txt
@@ -14,28 +14,26 @@
 )
 
 command: before: {
-	task: {
-		t1: exec.Run & {
-			cmd: ["sh", "-c", "sleep 2; date +%s"]
-			stdout: string
-			$before: [task.t2, task.t3]
-		}
-		t2: exec.Run & {
-			cmd: ["sh", "-c", "date +%s"]
-			stdout: string
-			$before: task.t4
-		}
-		t3: exec.Run & {
-			cmd: ["sh", "-c", "date +%s"]
-			stdout: string
-		}
-		t4: exec.Run & {
-			cmd: ["sh", "-c", "a=\(strings.TrimSpace(task.t1.stdout));b=\(strings.TrimSpace(task.t2.stdout));if [ $a -le $b ]; then echo 'true'; fi"]
-			stdout: string
-		}
-		t5: cli.Print & {
-				text: task.t4.stdout
-		}
+	t1: exec.Run & {
+		cmd: ["sh", "-c", "sleep 2; date +%s"]
+		stdout: string
+		$before: [t2, t3]
+	}
+	t2: exec.Run & {
+		cmd: ["sh", "-c", "date +%s"]
+		stdout: string
+		$before: t4
+	}
+	t3: exec.Run & {
+		cmd: ["sh", "-c", "date +%s"]
+		stdout: string
+	}
+	t4: exec.Run & {
+		cmd: ["sh", "-c", "a=\(strings.TrimSpace(t1.stdout));b=\(strings.TrimSpace(t2.stdout));if [ $a -le $b ]; then echo 'true'; fi"]
+		stdout: string
+	}
+	t5: cli.Print & {
+			text: t4.stdout
 	}
 }
 
diff --git a/cmd/cue/cmd/testdata/script/cmd_dep.txt b/cmd/cue/cmd/testdata/script/cmd_dep.txt
index 67d3fe8..25484d7 100644
--- a/cmd/cue/cmd/testdata/script/cmd_dep.txt
+++ b/cmd/cue/cmd/testdata/script/cmd_dep.txt
@@ -19,17 +19,15 @@
 }
 
 command: do: {
-    task: name: exec.Run & {
-        cmd: "echo cue"
+    inputs: name: exec.Run & {
+        cmd:    "echo cue"
         stdout: string
     }
-    task: out: cli.Print & {
+    outputs: print: cli.Print & {
         text:  json.Marshal(foo & {
-            a :: strings.TrimSpace(task.name.stdout)
+            a :: strings.TrimSpace(inputs.name.stdout)
         })
     }
 }
 
--- task.cue --
-package home
 -- cue.mod --
diff --git a/cmd/cue/cmd/testdata/script/cmd_echo.txt b/cmd/cue/cmd/testdata/script/cmd_echo.txt
index a626358..4468d6c 100644
--- a/cmd/cue/cmd/testdata/script/cmd_echo.txt
+++ b/cmd/cue/cmd/testdata/script/cmd_echo.txt
@@ -13,7 +13,7 @@
 package hello
 
 command: echo: {
-    task: echo: {
+    echo: {
         kind:   "exec"
         cmd:    "echo \(message)"
         stdout: string
@@ -21,7 +21,7 @@
 
     task: display: {
         kind: "print"
-        text: task.echo.stdout
+        text: echo.stdout
     }
 }
 -- cue.mod --
diff --git a/cmd/cue/cmd/testdata/script/cmd_import.txt b/cmd/cue/cmd/testdata/script/cmd_import.txt
index d401652..0db42f7 100644
--- a/cmd/cue/cmd/testdata/script/cmd_import.txt
+++ b/cmd/cue/cmd/testdata/script/cmd_import.txt
@@ -9,10 +9,8 @@
 // missing imports
 
 command: pkg: {
-	task: {
-		t2: cli.Print & {
-			text: "Hello world!"
-		}
+	t: cli.Print & {
+		text: "Hello world!"
 	}
 }
 
diff --git a/cmd/cue/cmd/testdata/script/cmd_print.txt b/cmd/cue/cmd/testdata/script/cmd_print.txt
index 1f032b3..4fb822a 100644
--- a/cmd/cue/cmd/testdata/script/cmd_print.txt
+++ b/cmd/cue/cmd/testdata/script/cmd_print.txt
@@ -15,18 +15,16 @@
 )
 
 command: print: {
-	task: {
-		t1: exec.Run & {
-			cmd: ["sh", "-c", "sleep 1; echo t1"]
-			stdout: string
-		}
-		t2: exec.Run & {
-			cmd: ["sh", "-c", "sleep 1; echo t2"]
-			stdout: string
-		}
-		t3: cli.Print & {
-			text: (f & {arg: t1.stdout + t2.stdout}).result
-		}
+	runs: t1: exec.Run & {
+		cmd: ["sh", "-c", "sleep 1; echo t1"]
+		stdout: string
+	}
+	runs: t2: exec.Run & {
+		cmd: ["sh", "-c", "sleep 1; echo t2"]
+		stdout: string
+	}
+	print: cli.Print & {
+		text: (f & {arg: runs.t1.stdout + runs.t2.stdout}).result
 	}
 }
 
diff --git a/cmd/cue/cmd/testdata/script/cmd_ref.txt b/cmd/cue/cmd/testdata/script/cmd_ref.txt
index 456f992..8aa8eb2 100644
--- a/cmd/cue/cmd/testdata/script/cmd_ref.txt
+++ b/cmd/cue/cmd/testdata/script/cmd_ref.txt
@@ -13,15 +13,13 @@
 )
 
 command: ref: {
-	task: {
-		t1: exec.Run & {
-				ref: task.t1.stdout
-				cmd: ["sh", "-c", "echo hello"]
-				stdout: string
-		}
-		t2: cli.Print & {
-			text: task.t1.stdout
-		}
+	t1: exec.Run & {
+			ref: t1.stdout
+			cmd: ["sh", "-c", "echo hello"]
+			stdout: string
+	}
+	t2: cli.Print & {
+		text: t1.stdout
 	}
 }