pkg/tool: add constraints for tasks

Updates #39

Change-Id: Ied624912324b773b56ff4f2a1bf85a88c22ecc45
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/1924
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cmd/cue/cmd/custom.go b/cmd/cue/cmd/custom.go
index 48c9f7c..8e53f17 100644
--- a/cmd/cue/cmd/custom.go
+++ b/cmd/cue/cmd/custom.go
@@ -29,6 +29,7 @@
 	"sync"
 
 	"cuelang.org/go/cue"
+	"cuelang.org/go/internal"
 	itask "cuelang.org/go/internal/task"
 	_ "cuelang.org/go/pkg/tool/cli" // Register tasks
 	_ "cuelang.org/go/pkg/tool/exec"
@@ -251,15 +252,34 @@
 	dep   map[*task]bool
 }
 
+var oldKinds = map[string]string{
+	"exec":       "tool/exec.Run",
+	"http":       "tool/http.Do",
+	"print":      "tool/cli.Print",
+	"testserver": "cmd/cue/cmd.Test",
+}
+
 func newTask(index int, name string, v cue.Value) (*task, error) {
+	// Lookup kind for backwards compatibility.
+	// TODO: consider at some point whether kind can be removed.
 	kind, err := v.Lookup("kind").String()
 	if err != nil {
 		return nil, err
 	}
+	if k, ok := oldKinds[kind]; ok {
+		kind = k
+	}
 	rf := itask.Lookup(kind)
 	if rf == nil {
 		return nil, fmt.Errorf("runner of kind %q not found", kind)
 	}
+
+	// Verify entry against template.
+	v = internal.UnifyBuiltin(v, kind).(cue.Value)
+	if err := v.Err(); err != nil {
+		return nil, err
+	}
+
 	runner, err := rf(v)
 	if err != nil {
 		return nil, err
@@ -278,7 +298,7 @@
 }
 
 func init() {
-	itask.Register("testserver", newTestServerCmd)
+	itask.Register("cmd/cue/cmd.Test", newTestServerCmd)
 }
 
 var testOnce sync.Once
diff --git a/cmd/cue/cmd/testdata/tasks/cmd_baddisplay.out b/cmd/cue/cmd/testdata/tasks/cmd_baddisplay.out
index 08dafc3..ebb7a3f 100644
--- a/cmd/cue/cmd/testdata/tasks/cmd_baddisplay.out
+++ b/cmd/cue/cmd/testdata/tasks/cmd_baddisplay.out
@@ -1,3 +1,4 @@
-not of right kind (number vs string):
+unsupported op &(number, (string)*):
     $CWD/testdata/tasks/task_tool.cue:29:9
+    tool/cli:4:10
     
diff --git a/cue/builtin.go b/cue/builtin.go
index 0751b1c..3fb7c0c 100644
--- a/cue/builtin.go
+++ b/cue/builtin.go
@@ -23,9 +23,11 @@
 	"math/big"
 	"path"
 	"sort"
+	"strings"
 
 	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/parser"
+	"cuelang.org/go/internal"
 )
 
 // A builtin is a builtin function or constant.
@@ -250,6 +252,26 @@
 	return p
 }
 
+func init() {
+	internal.UnifyBuiltin = func(val interface{}, kind string) interface{} {
+		v := val.(Value)
+		ctx := v.ctx()
+
+		p := strings.Split(kind, ".")
+		pkg, name := p[0], p[1]
+		s := getBuiltinPkg(ctx, pkg)
+		if s == nil {
+			return v
+		}
+		a := s.lookup(ctx, ctx.label(name, false))
+		if a.v == nil {
+			return v
+		}
+
+		return v.Unify(newValueRoot(ctx, a.v.evalPartial(ctx)))
+	}
+}
+
 // do returns whether the call should be done.
 func (c *callCtxt) do() bool {
 	return c.err == nil
diff --git a/cue/builtins.go b/cue/builtins.go
index 93e8c9e..a8cb704 100644
--- a/cue/builtins.go
+++ b/cue/builtins.go
@@ -466,9 +466,6 @@
 			},
 		}},
 	},
-	"list": &builtinPkg{
-		native: []*builtin{{}},
-	},
 	"math": &builtinPkg{
 		native: []*builtin{{
 			Name:  "MaxExp",
@@ -1297,18 +1294,6 @@
 			},
 		}},
 	},
-	"runtime": &builtinPkg{
-		native: []*builtin{{
-			Name:   "Path",
-			Params: []kind{},
-			Result: stringKind,
-			Func: func(c *callCtxt) {
-				c.ret = func() interface{} {
-					return ""
-				}()
-			},
-		}},
-	},
 	"strconv": &builtinPkg{
 		native: []*builtin{{
 			Name:   "Unquote",
@@ -1882,4 +1867,110 @@
 	Time: null | =~"^\("\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2]\\d|3[0-1])")T\("([0-1]\\d|2[0-3]):[0-5]\\d:[0-5]\\d")\("(.\\d{1,10})?")\("(Z|(-|\\+)\\d\\d:\\d\\d)")$"
 }`,
 	},
+	"tool": &builtinPkg{
+		native: []*builtin{{}},
+		cue: `{
+	Command: {
+		usage?: string
+		short?: string
+		long?:  string
+		var <name>: {
+			value:       _
+			description: "" | string
+		}
+		tasks <name>: Task
+	}
+	Task _kind: =~"\\."
+}`,
+	},
+	"tool/cli": &builtinPkg{
+		native: []*builtin{{}},
+		cue: `{
+	Print: {
+		_kind: "tool/cli.Print"
+		text:  string
+	}
+}`,
+	},
+	"tool/exec": &builtinPkg{
+		native: []*builtin{{}},
+		cue: `{
+	Run: {
+		_kind:    "tool/exec.Run"
+		cmd:      string | [string, ...string]
+		install?: string | [string, ...string]
+		env <Key>: string
+		stdout:  *null | string | bytes
+		stderr:  *null | string | bytes
+		stdin?:  string | bytes
+		success: bool
+	}
+	Env: {
+		_kind: "tool/exec.Env"
+		env <Name>: string | number
+	}
+}`,
+	},
+	"tool/file": &builtinPkg{
+		native: []*builtin{{}},
+		cue: `{
+	Read: {
+		_kind:    "tool/file.Read"
+		filename: !=""
+		contents: *bytes | string
+	}
+	Create: {
+		_kind:       "tool/file.Create"
+		filename:    !=""
+		contents:    bytes | string
+		permissions: int | *420
+		overwrite:   *false | true
+	}
+	Append: {
+		_kind:       "tool/file.Append"
+		filename:    !=""
+		contents:    bytes | string
+		permissions: int | *420
+	}
+	Glob: {
+		_kind: "tool/file.Glob"
+		glob:  !=""
+		files: [...string]
+	}
+}`,
+	},
+	"tool/http": &builtinPkg{
+		native: []*builtin{{}},
+		cue: `{
+	Get: Do & {
+		method: "GET"
+	}
+	Do: {
+		_kind:  "tool/http.Do"
+		method: string
+		response: {
+			body: *bytes | string
+			header <Name>:  string | [...string]
+			trailer <Name>: string | [...string]
+			status:     string
+			statusCode: int
+		}
+		url: string
+		request: {
+			body: *bytes | string
+			header <Name>:  string | [...string]
+			trailer <Name>: string | [...string]
+		}
+	}
+	Post: Do & {
+		method: "POST"
+	}
+	Put: Do & {
+		method: "PUT"
+	}
+	Delete: Do & {
+		method: "DELETE"
+	}
+}`,
+	},
 }
diff --git a/cue/gen.go b/cue/gen.go
index e5c2b26..f7a6824 100644
--- a/cue/gen.go
+++ b/cue/gen.go
@@ -203,8 +203,9 @@
 		return
 	}
 
+	n := instances[0].Value().Syntax(cue.Hidden(true), cue.Concrete(false))
 	var buf bytes.Buffer
-	if err := cueformat.Node(&buf, instances[0].Value().Syntax()); err != nil {
+	if err := cueformat.Node(&buf, n); err != nil {
 		log.Fatal(err)
 	}
 	body := buf.String()
@@ -250,13 +251,12 @@
 				fmt.Fprint(g.decls, "\n\n")
 				continue
 			case token.TYPE:
+				// TODO: support type declarations.
 				for _, spec := range x.Specs {
 					if ast.IsExported(spec.(*ast.TypeSpec).Name.Name) {
 						log.Fatal("type declarations not supported")
 					}
 				}
-				printer.Fprint(g.decls, g.fset, x)
-				fmt.Fprint(g.decls, "\n\n")
 				continue
 			default:
 				log.Fatalf("unexpected spec of type %s", x.Tok)
diff --git a/cue/resolve_test.go b/cue/resolve_test.go
index 6a0db16..8e6737b 100644
--- a/cue/resolve_test.go
+++ b/cue/resolve_test.go
@@ -1748,8 +1748,8 @@
 			result: [ v for _, v in service ]
 
 			service <Name>: {
-				type: "service"
 				name: *Name | string
+				type: "service"
 				port: *7080 | int
 			}
 			service foo: {}
@@ -1757,15 +1757,15 @@
 			service baz: { name: "foobar" }
 			`,
 		out: `<0>{result: [` +
-			`<1>{type: "service", name: "foo", port: 7080},` +
-			`<2>{type: "service", name: "bar", port: 8000},` +
-			`<3>{type: "service", name: "foobar", port: 7080}], ` +
+			`<1>{name: "foo", type: "service", port: 7080},` +
+			`<2>{name: "bar", type: "service", port: 8000},` +
+			`<3>{name: "foobar", type: "service", port: 7080}], ` +
 
 			`service: <4>{` +
-			`<>: <5>(Name: string)-><6>{type: "service", name: (*<5>.Name | string), port: (*7080 | int)}, ` +
-			`foo: <7>{type: "service", name: "foo", port: 7080}, ` +
-			`bar: <8>{type: "service", name: "bar", port: 8000}, ` +
-			`baz: <9>{type: "service", name: "foobar", port: 7080}}}`,
+			`<>: <5>(Name: string)-><6>{name: (*<5>.Name | string), type: "service", port: (*7080 | int)}, ` +
+			`foo: <7>{name: "foo", type: "service", port: 7080}, ` +
+			`bar: <8>{name: "bar", type: "service", port: 8000}, ` +
+			`baz: <9>{name: "foobar", type: "service", port: 7080}}}`,
 	}, {
 		desc: "resolutions in struct comprehension keys",
 		in: `
diff --git a/internal/internal.go b/internal/internal.go
index 88b4b76..6133bac 100644
--- a/internal/internal.go
+++ b/internal/internal.go
@@ -43,3 +43,6 @@
 // DropOptional is a blanket override of handling optional values during
 // compilation. TODO: should we make this a build option?
 var DropOptional bool
+
+// UnifyBuiltin returns the given Value unified with the given builtin template.
+var UnifyBuiltin func(v interface{}, kind string) interface{}
diff --git a/pkg/tool/cli/cli.cue b/pkg/tool/cli/cli.cue
new file mode 100644
index 0000000..986f43a
--- /dev/null
+++ b/pkg/tool/cli/cli.cue
@@ -0,0 +1,42 @@
+// Copyright 2018 The CUE Authors
+// 
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// 
+//     http://www.apache.org/licenses/LICENSE-2.0
+// 
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cli
+
+// Print sends text to the stdout of the current process.
+Print: {
+	kind: "tool/cli.Print"
+
+	// text is the text to be printed.
+	text: string
+}
+
+// TODO:
+// Ask prompts the current console with a message and waits for input.
+//
+// Example:
+//     task ask: cli.Ask({
+//         prompt:   "Are you okay?"
+//         repsonse: bool
+//     })
+// Ask: {
+//  kind: "tool/cli.Ask"
+
+//  // prompt sends this message to the output.
+//  prompt: string
+
+//  // response holds the user's response. If it is a boolean expression it
+//  // will interpret the answer using textual yes/ no.
+//  response: string | bool
+// }
diff --git a/pkg/tool/exec/exec.cue b/pkg/tool/exec/exec.cue
new file mode 100644
index 0000000..b286f17
--- /dev/null
+++ b/pkg/tool/exec/exec.cue
@@ -0,0 +1,53 @@
+// Copyright 2018 The CUE Authors
+// 
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// 
+//     http://www.apache.org/licenses/LICENSE-2.0
+// 
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package exec
+
+// Run executes the given shell command.
+Run: {
+	kind: "tool/exec.Run"
+
+	// cmd is the command to run.
+	cmd: string | [string, ...string]
+
+	// install is an optional command to install the binaries needed
+	// to run the command.
+	install?: string | [string, ...string]
+
+	// env defines the environment variables to use for this system.
+	env <Key>: string
+
+	// stdout captures the output from stdout if it is of type bytes or string.
+	// The default value of null indicates it is redirected to the stdout of the
+	// current process.
+	stdout: *null | string | bytes
+
+	// stderr is like stdout, but for errors.
+	stderr: *null | string | bytes
+
+	// stdin specifies the input for the process.
+	stdin?: string | bytes
+
+	// success is set to true when the process terminates with with a zero exit
+	// code or false otherwise. The user can explicitly specify the value
+	// force a fatal error if the desired success code is not reached.
+	success: bool
+}
+
+// Env collects the environment variables of the current process.
+Env: {
+	kind: "tool/exec.Env"
+
+	env <Name>: string | number
+}
diff --git a/pkg/tool/file/file.cue b/pkg/tool/file/file.cue
new file mode 100644
index 0000000..1275236
--- /dev/null
+++ b/pkg/tool/file/file.cue
@@ -0,0 +1,77 @@
+// Copyright 2018 The CUE Authors
+// 
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// 
+//     http://www.apache.org/licenses/LICENSE-2.0
+// 
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package os
+
+import "tool"
+
+// Read reads the contents of a file.
+Read: tool.Task & {
+	_kind: "tool/file.Read"
+
+	// filename names the file to read.
+	filename: string
+
+	// contents is the read contents. If the contents are constraint to bytes
+	// (the default), the file is read as is. If it is constraint to a string,
+	// the contents are checked to be valid UTF-8.
+	contents: *bytes | string
+
+	// if body is given, the file contents are parsed as JSON and unified with
+	// the specified CUE value.
+	body?: _
+}
+
+// Create writes contents to the given file.
+Create: tool.Task & {
+	_kind: "tool/file.Create"
+
+	// filename names the file to write.
+	filename: string
+
+	// permissions defines the permissions to use if the file does not yet exist.
+	permissions: int
+
+	// overwrite defines whether an existing file may be overwritten.
+	overwrite: *false | true
+
+	// contents specifies the bytes to be written.
+	contents: bytes | string
+}
+
+// Append writes contents to the given file.
+Append: tool.Task & {
+	// filename names the file to append.
+	filename: string
+
+	// permissions defines the permissions to use if the file does not yet exist.
+	permissions: int
+
+	// contents specifies the bytes to be written.
+	contents: bytes | string
+}
+
+Dir: tool.Task & {
+	_kind: "tool/file.Dir"
+
+	path: string
+	dir: [...string]
+}
+
+Glob: tool.Task & {
+	_kind: "tool/file.Glob"
+
+	glob: string
+	files <Filename>: string
+}
diff --git a/pkg/tool/http/http.cue b/pkg/tool/http/http.cue
new file mode 100644
index 0000000..04cee3d
--- /dev/null
+++ b/pkg/tool/http/http.cue
@@ -0,0 +1,54 @@
+// Copyright 2018 The CUE Authors
+// 
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// 
+//     http://www.apache.org/licenses/LICENSE-2.0
+// 
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package http
+
+Get:    Do & {method: "GET"}
+Post:   Do & {method: "POST"}
+Put:    Do & {method: "PUT"}
+Delete: Do & {method: "DELETE"}
+
+Do: {
+	kind: "tool/http.Do"
+
+	method: string
+	url:    string // TODO: make url.URL type
+
+	request: {
+		body: *bytes | string
+		header <Name>:  string | [...string]
+		trailer <Name>: string | [...string]
+	}
+	response: {
+		status:     string
+		statusCode: int
+
+		body: *bytes | string
+		header <Name>:  string | [...string]
+		trailer <Name>: string | [...string]
+	}
+}
+
+/* TODO: support serving once we have the cue serve command.
+Serve: {
+	port: int
+
+	cert: string
+	key:  string
+
+	handle <Pattern>: Message & {
+		pattern: Pattern
+	}
+}
+*/
diff --git a/pkg/tool/tool.cue b/pkg/tool/tool.cue
new file mode 100644
index 0000000..c9c1b11
--- /dev/null
+++ b/pkg/tool/tool.cue
@@ -0,0 +1,170 @@
+// Copyright 2018 The CUE Authors
+// 
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package tool defines statefull operation types for cue commands.
+//
+// This package is only visible in cue files with a _tool.cue or _tool_test.cue
+// ending.
+//
+// CUE configuration files are not influenced by and do not influence anything
+// outside the configuration itself: they are hermetic. Tools solve
+// two problems: allow outside values such as environment variables,
+// file or web contents, random generators etc. to influence configuration,
+// and allow configuration to be actionable from within the tooling itself.
+// Separating these concerns makes it clear to user when outside influences are
+// in play and the tool definition can be strict about what is allowed.
+//
+// Tools are defined in files ending with _tool.cue. These files have a
+// top-level map, "command", which defines all the tools made available through
+// the cue command.
+//
+//
+package tool
+
+// A Command specifies a user-defined command.
+Command: {
+	//
+	// Example:
+	//     mycmd [-n] names
+	usage?: string
+
+	// short is short description of what the command does.
+	short?: string
+
+	// long is a longer description that spans multiple lines and
+	// likely contain examples of usage of the command.
+	long?: string
+
+	// A var defines a value that can be set by the command line or an
+	// environment variable.
+	//
+	// Example:
+	//    var fast: {
+	//        description: "run faster than anyone"
+	//        value:       true | bool
+	//    }
+	//
+	var <name>: {
+		value:       _
+		description: "" | string
+	}
+
+	// tasks specifies the list of things to do to run command. Tasks are
+	// typically underspecified and completed by the particular internal
+	// handler that is running them. Task de
+	tasks <name>: Task
+
+	// TODO:
+	// timeout?: number // in seconds
+}
+
+// A task defines a step in the execution of a command, server, or fix
+// operation.
+Task: {
+	kind: =~#"\."#
+}
+
+// import "cue/tool"
+//
+// command <Name>: { // from "cue/tool".Command
+//  // usage gives a short usage pattern of the command.
+//  // Example:
+//  //    fmt [-n] [-x] [packages]
+//  usage: Name | string
+//
+//  // short gives a brief on-line description of the command.
+//  // Example:
+//  //    reformat package sources
+//  short: "" | string
+//
+//  // long gives a detailed description of the command, including a
+//  // description of flags usage and examples.
+//  long: "" | string
+//
+//  // A task defines a single action to be run as part of this command.
+//  // Each task can have inputs and outputs, depending on the type
+//  // task. The outputs are initially unspecified, but are filled out
+//  // by the tooling
+//  //
+//  task <Name>: { // from "cue/tool".Task
+//   // supported fields depend on type
+//  }
+//
+//  VarValue = string | bool | int | float | [...string|int|float]
+//
+//  // var declares values that can be set by command line flags or
+//  // environment variables.
+//  //
+//  // Example:
+//  //   // environment to run in
+//  //   var env: "test" | "prod"
+//  // The tool would print documentation of this flag as:
+//  //   Flags:
+//  //      --env string    environment to run in: test(default) or prod
+//  var <Name>: VarValue
+//
+//  // flag defines a command line flag.
+//  //
+//  // Example:
+//  //   var env: "test" | "prod"
+//  //
+//  //   // augment the flag information for var
+//  //   flag env: {
+//  //       shortFlag:   "e"
+//  //       description: "environment to run in"
+//  //   }
+//  //
+//  // The tool would print documentation of this flag as:
+//  //   Flags:
+//  // -e, --env string    environment to run in: test(default), staging, or prod
+//  //
+//  flag <Name>: { // from "cue/tool".Flag
+//   // value defines the possible values for this flag.
+//   // The default is string. Users can define default values by
+//   // using disjunctions.
+//   value: env[Name].value | VarValue
+//
+//   // name, if set, allows var to be set with the command-line flag
+//   // of the given name. null disables the command line flag.
+//   name: Name | null | string
+//
+//   // short defines an abbreviated version of the flag.
+//   // Disabled by default.
+//   short: null | string
+//  }
+//
+//  // populate flag with the default values for
+//  flag: { "\(k)": { value: v } | null for k, v in var }
+//
+//  // env defines environment variables. It is populated with values
+//  // for var.
+//  //
+//  // To specify a var without an equivalent environment variable,
+//  // either specify it as a flag directly or disable the equally
+//  // named env entry explicitly:
+//  //
+//  //     var foo: string
+//  //     env foo: null  // don't use environment variables for foo
+//  //
+//  env <Name>: {å
+//   // name defines the environment variable that sets this flag.
+//   name: "CUE_VAR_" + strings.Upper(Name) | string | null
+//
+//   // The value retrieved from the environment variable or null
+//   // if not set.
+//   value: null | string | bytes
+//  }
+//  env: { "\(k)": { value: v } | null for k, v in var }
+// }
+//