encoding/jsonschema: constraints don't imply types

This was a major bug based on the wrong assumption that
a JSON Schema constraint implies the type for that constraint.
Instead, the contraints only apply if a value is of a certain
type.

To keep diffs to a minimum, and to improve the overal output,
it now explicitly sorts literal
composit types (structs and lists) at the end of a series
of conjunctions.

Change-Id: Ice98a2bc00ae4a68170cd6cd726565a453b1187f
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/6302
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/testdata/script/def_jsonschema.txt b/cmd/cue/cmd/testdata/script/def_jsonschema.txt
index 612c064..9ae96d8 100644
--- a/cmd/cue/cmd/testdata/script/def_jsonschema.txt
+++ b/cmd/cue/cmd/testdata/script/def_jsonschema.txt
@@ -68,11 +68,13 @@
 age: twenty
 
 -- expect-stderr2 --
-age: conflicting values "twenty" and >=0 (mismatched types string and number):
+age: conflicting values "twenty" and (int & >=0) (mismatched types string and int):
     ./data.yaml:1:7
+    ./schema.json:18:15
     ./schema.json:19:18
 -- expect-stderr3 --
-age: conflicting values "twenty" and >=0 (mismatched types string and number):
+age: conflicting values "twenty" and (int & >=0) (mismatched types string and int):
     ./data.yaml:1:7
+    ./schema.json:18:15
     ./schema.json:19:18
 -- cue.mod --
diff --git a/cmd/cue/cmd/testdata/script/def_openapi.txt b/cmd/cue/cmd/testdata/script/def_openapi.txt
index e9a7a62..3e05937 100644
--- a/cmd/cue/cmd/testdata/script/def_openapi.txt
+++ b/cmd/cue/cmd/testdata/script/def_openapi.txt
@@ -217,7 +217,7 @@
 }
 #Foo: {
 	a: int
-	b: >=0 & <10
+	b: uint & <10
 	...
 }
 -- expect-cue2 --
@@ -231,7 +231,7 @@
 }
 #Foo: {
 	a: int
-	b: >=0 & <10
+	b: uint & <10
 	...
 }
 -- expect-cue2 --
@@ -245,7 +245,7 @@
 }
 #Foo: {
 	a: int
-	b: >=0 & <10
+	b: uint & <10
 	...
 }
 -- expect-cue3 --
@@ -262,11 +262,11 @@
 }
 #Foo: {
 	a: int
-	b: >=0 & <10
+	b: uint & <10
 	...
 }
 #Baz: {
 	a: int
-	b: >=0 & <10
+	b: uint & <10
 	...
 }
diff --git a/cmd/cue/cmd/testdata/script/import_auto.txt b/cmd/cue/cmd/testdata/script/import_auto.txt
index 6c9d655..785d242 100644
--- a/cmd/cue/cmd/testdata/script/import_auto.txt
+++ b/cmd/cue/cmd/testdata/script/import_auto.txt
@@ -13,7 +13,7 @@
 
 #Foo: {
 	a: int
-	b: >=0 & <10
+	b: int & >=0 & <10
 	...
 }
 #Bar: {
diff --git a/cue.mod/pkg/github.com/SchemaStore/schemastore/src/schemas/json/github-workflow.cue b/cue.mod/pkg/github.com/SchemaStore/schemastore/src/schemas/json/github-workflow.cue
index 9db3227..78fac7e 100644
--- a/cue.mod/pkg/github.com/SchemaStore/schemastore/src/schemas/json/github-workflow.cue
+++ b/cue.mod/pkg/github.com/SchemaStore/schemastore/src/schemas/json/github-workflow.cue
@@ -4,7 +4,7 @@
 
 #Workflow: {
 	@jsonschema(schema="http://json-schema.org/draft-07/schema")
-	number | null | bool | string | [...] | {
+	null | bool | number | string | [...] | {
 		// The name of your workflow. GitHub displays the names of your
 		// workflows on your repository's actions page. If you omit this
 		// field, GitHub sets the name to the workflow's filename.
@@ -237,10 +237,9 @@
 			// commit object. You can retrieve the full commit object using
 			// the REST API. For more information, see
 			// https://developer.github.com/v3/repos/commits/#get-a-single-commit.
-			push?: #ref &
-				{
-					{[=~"^(branche|tag|path)s(-ignore)?$" & !~"^()$"]: _}
-				}
+			push?: #ref & {
+				{[=~"^(branche|tag|path)s(-ignore)?$" & !~"^()$"]: _}
+			}
 
 			// Runs your workflow anytime a package is published or updated.
 			// For more information, see
@@ -289,7 +288,7 @@
 			// generate your cron syntax and confirm what time it will run.
 			// To help you get started, there is also a list of crontab guru
 			// examples (https://crontab.guru/examples.html).
-			schedule?: [...number | null | bool | string | [...] | {
+			schedule?: [...null | bool | number | string | [...] | {
 				cron?: =~"^(((\\d+,)+\\d+|((\\d+|\\*)\\/\\d+)|(\\d+-\\d+)|\\d+|\\*) ?){5,7}$"
 			}] & [_, ...]
 		}
@@ -320,7 +319,9 @@
 				// If you do not set a container, all steps will run directly on
 				// the host specified by runs-on unless a step refers to an
 				// action configured to run in a container.
-				container?: [string]: string | #container
+				container?: {
+					[string]: string | #container
+				}
 
 				// A map of default settings that will apply to all steps in the
 				// job.
@@ -338,7 +339,9 @@
 
 				// A map of outputs for a job. Job outputs are available to all
 				// downstream jobs that depend on this job.
-				outputs?: [string]: string
+				outputs?: {
+					[string]: string
+				}
 
 				// You can use the if conditional to prevent a job from running
 				// unless a condition is met. You can use any supported context
@@ -467,7 +470,6 @@
 						{[=~"^(in|ex)clude$" & !~"^()$"]: [...{
 											[string]: #configuration
 						}] & [_, ...]}
-
 						{[!~"^(in|ex)clude$" & !~"^()$"]: [...#configuration] & [_, ...]}
 					}
 
@@ -496,7 +498,9 @@
 				// is automatically mapped to the service name.
 				// When a step does not use a container action, you must access
 				// the service using localhost and bind the ports.
-				services?: [string]: #container
+				services?: {
+					[string]: #container
+				}
 			}}
 		}
 	}
@@ -505,7 +509,7 @@
 
 	#name: =~"^[_a-zA-Z][a-zA-Z0-9_-]*$"
 
-	#env: [string]: number | bool | string
+	#env: [string]: bool | number | string
 
 	#architecture: "ARM32" | "x64" | "x86"
 
@@ -549,7 +553,7 @@
 		"working-directory"?: #["working-directory"]
 	}
 
-	#shell: string | ("bash" | "pwsh" | "python" | "sh" | "cmd" | "powershell")
+	#shell: (string | ("bash" | "pwsh" | "python" | "sh" | "cmd" | "powershell")) & string
 
 	#: "working-directory": string
 
diff --git a/encoding/jsonschema/constraints.go b/encoding/jsonschema/constraints.go
index d838d9c..89333f6 100644
--- a/encoding/jsonschema/constraints.go
+++ b/encoding/jsonschema/constraints.go
@@ -144,12 +144,11 @@
 		}
 		s.id = u
 
-		if s.obj == nil {
-			s.obj = &ast.StructLit{}
-		}
+		obj := s.object(n)
+
 		// TODO: handle the case where this is always defined and we don't want
 		// to include the default value.
-		s.obj.Elts = append(s.obj.Elts, &ast.Attribute{
+		obj.Elts = append(obj.Elts, &ast.Attribute{
 			Text: fmt.Sprintf("@jsonschema(id=%q)", u)})
 	}),
 
@@ -165,19 +164,27 @@
 			switch str {
 			case "null":
 				types |= cue.NullKind
+				s.setTypeUsed(n, nullType)
 				// TODO: handle OpenAPI restrictions.
 			case "boolean":
 				types |= cue.BoolKind
+				s.setTypeUsed(n, boolType)
 			case "string":
 				types |= cue.StringKind
+				s.setTypeUsed(n, stringType)
 			case "number":
 				types |= cue.NumberKind
+				s.setTypeUsed(n, numType)
 			case "integer":
 				types |= cue.IntKind
+				s.setTypeUsed(n, numType)
+				s.add(n, numType, ast.NewIdent("int"))
 			case "array":
 				types |= cue.ListKind
+				s.setTypeUsed(n, arrayType)
 			case "object":
 				types |= cue.StructKind
+				s.setTypeUsed(n, objectType)
 
 			default:
 				s.errf(n, "unknown type %q", n)
@@ -203,19 +210,17 @@
 		for _, x := range s.listItems("enum", n, true) {
 			a = append(a, s.value(x))
 		}
-		s.addConjunct(n, ast.NewBinExpr(token.OR, a...))
-		s.typeOptional = true
+		s.all.add(n, ast.NewBinExpr(token.OR, a...))
 	}),
 
 	p1d("const", 6, func(n cue.Value, s *state) {
-		s.addConjunct(n, s.value(n))
+		s.all.add(n, s.value(n))
 	}),
 
 	p1("default", func(n cue.Value, s *state) {
-		allowed, used := s.allowedTypes, s.usedTypes
-		s.default_ = s.value(n)
-		s.allowedTypes, s.usedTypes = allowed, used
-		// must validate that the default is subsumed by the normal value,
+		sc := *s
+		s.default_ = sc.value(n)
+		// TODO: must validate that the default is subsumed by the normal value,
 		// as CUE will otherwise broaden the accepted values with the default.
 		s.examples = append(s.examples, s.default_)
 	}),
@@ -268,7 +273,7 @@
 			expr = &ast.BadExpr{From: n.Pos()}
 		}
 
-		s.addConjunct(n, expr)
+		s.all.add(n, expr)
 	}),
 
 	// Combinators
@@ -302,7 +307,7 @@
 			}
 		}
 		if len(a) > 0 {
-			s.conjuncts = append(s.conjuncts, ast.NewBinExpr(token.AND, a...))
+			s.all.add(n, ast.NewBinExpr(token.AND, a...))
 		}
 	}),
 
@@ -312,13 +317,11 @@
 		for _, v := range s.listItems("anyOf", n, false) {
 			x, sub := s.schemaState(v, s.allowedTypes, nil, true)
 			types |= sub.allowedTypes
-			if sub.hasConstraints() {
-				a = append(a, x)
-			}
+			a = append(a, x)
 		}
 		s.allowedTypes &= types
 		if len(a) > 0 {
-			s.conjuncts = append(s.conjuncts, ast.NewBinExpr(token.OR, a...))
+			s.all.add(n, ast.NewBinExpr(token.OR, a...))
 		}
 	}),
 
@@ -342,7 +345,7 @@
 		s.allowedTypes &= types
 		if len(a) > 0 && hasSome {
 			s.usedTypes = allTypes
-			s.conjuncts = append(s.conjuncts, ast.NewBinExpr(token.OR, a...))
+			s.all.add(n, ast.NewBinExpr(token.OR, a...))
 		}
 
 		// TODO: oneOf({a:x}, {b:y}, ..., not(anyOf({a:x}, {b:y}, ...))),
@@ -360,22 +363,21 @@
 			return
 		}
 		s.usedTypes |= cue.StringKind
-		s.addConjunct(n, &ast.UnaryExpr{Op: token.MAT, X: s.string(n)})
+		s.add(n, stringType, &ast.UnaryExpr{Op: token.MAT, X: s.string(n)})
 	}),
 
 	p1("minLength", func(n cue.Value, s *state) {
 		s.usedTypes |= cue.StringKind
 		min := s.number(n)
 		strings := s.addImport(n, "strings")
-		s.addConjunct(n, ast.NewCall(ast.NewSel(strings, "MinRunes"), min))
-
+		s.add(n, stringType, ast.NewCall(ast.NewSel(strings, "MinRunes"), min))
 	}),
 
 	p1("maxLength", func(n cue.Value, s *state) {
 		s.usedTypes |= cue.StringKind
 		max := s.number(n)
 		strings := s.addImport(n, "strings")
-		s.addConjunct(n, ast.NewCall(ast.NewSel(strings, "MaxRunes"), max))
+		s.add(n, stringType, ast.NewCall(ast.NewSel(strings, "MaxRunes"), max))
 	}),
 
 	p1d("contentMediaType", 7, func(n cue.Value, s *state) {
@@ -396,24 +398,24 @@
 
 	p1("minimum", func(n cue.Value, s *state) {
 		s.usedTypes |= cue.NumberKind
-		s.addConjunct(n, &ast.UnaryExpr{Op: token.GEQ, X: s.number(n)})
+		s.add(n, numType, &ast.UnaryExpr{Op: token.GEQ, X: s.number(n)})
 	}),
 
 	p1("exclusiveMinimum", func(n cue.Value, s *state) {
 		// TODO: should we support Draft 4 booleans?
 		s.usedTypes |= cue.NumberKind
-		s.addConjunct(n, &ast.UnaryExpr{Op: token.GTR, X: s.number(n)})
+		s.add(n, numType, &ast.UnaryExpr{Op: token.GTR, X: s.number(n)})
 	}),
 
 	p1("maximum", func(n cue.Value, s *state) {
 		s.usedTypes |= cue.NumberKind
-		s.addConjunct(n, &ast.UnaryExpr{Op: token.LEQ, X: s.number(n)})
+		s.add(n, numType, &ast.UnaryExpr{Op: token.LEQ, X: s.number(n)})
 	}),
 
 	p1("exclusiveMaximum", func(n cue.Value, s *state) {
 		// TODO: should we support Draft 4 booleans?
 		s.usedTypes |= cue.NumberKind
-		s.addConjunct(n, &ast.UnaryExpr{Op: token.LSS, X: s.number(n)})
+		s.add(n, numType, &ast.UnaryExpr{Op: token.LSS, X: s.number(n)})
 	}),
 
 	p1("multipleOf", func(n cue.Value, s *state) {
@@ -425,17 +427,15 @@
 			s.errf(n, `"multipleOf" value must be < 0; found %s`, n)
 		}
 		math := s.addImport(n, "math")
-		s.addConjunct(n, ast.NewCall(ast.NewSel(math, "MultipleOf"), multiple))
+		s.add(n, numType, ast.NewCall(ast.NewSel(math, "MultipleOf"), multiple))
 	}),
 
 	// Object constraints
 
 	p1("properties", func(n cue.Value, s *state) {
 		s.usedTypes |= cue.StructKind
+		obj := s.object(n)
 
-		if s.obj == nil {
-			s.obj = &ast.StructLit{}
-		}
 		if n.Kind() != cue.StructKind {
 			s.errf(n, `"properties" expected an object, found %v`, n.Kind)
 		}
@@ -447,7 +447,7 @@
 			f := &ast.Field{Label: name, Value: expr}
 			state.doc(f)
 			f.Optional = token.Blank.Pos()
-			if len(s.obj.Elts) > 0 && len(f.Comments()) > 0 {
+			if len(obj.Elts) > 0 && len(f.Comments()) > 0 {
 				// TODO: change formatter such that either a a NewSection on the
 				// field or doc comment will cause a new section.
 				ast.SetRelPos(f.Comments()[0], token.NewSection)
@@ -455,12 +455,12 @@
 			if state.deprecated {
 				switch expr.(type) {
 				case *ast.StructLit:
-					s.obj.Elts = append(s.obj.Elts, addTag(name, "deprecated", ""))
+					obj.Elts = append(obj.Elts, addTag(name, "deprecated", ""))
 				default:
 					f.Attrs = append(f.Attrs, internal.NewAttr("deprecated", ""))
 				}
 			}
-			s.obj.Elts = append(s.obj.Elts, f)
+			obj.Elts = append(obj.Elts, f)
 			s.setField(label{name: key}, f)
 		})
 	}),
@@ -473,15 +473,13 @@
 
 		s.usedTypes |= cue.StructKind
 
-		if s.obj == nil {
-			s.obj = &ast.StructLit{}
-			// TODO: detect that properties is defined somewhere.
-			// s.errf(n, `"required" without a "properties" field`)
-		}
+		// TODO: detect that properties is defined somewhere.
+		// s.errf(n, `"required" without a "properties" field`)
+		obj := s.object(n)
 
 		// Create field map
 		fields := map[string]*ast.Field{}
-		for _, d := range s.obj.Elts {
+		for _, d := range obj.Elts {
 			f, ok := d.(*ast.Field)
 			if !ok {
 				continue // Could be embedding? See cirrus.json
@@ -501,7 +499,7 @@
 					Value: ast.NewIdent("_"),
 				}
 				fields[str] = f
-				s.obj.Elts = append(s.obj.Elts, f)
+				obj.Elts = append(obj.Elts, f)
 				continue
 			}
 			if f.Optional == token.NoPos {
@@ -515,7 +513,8 @@
 		// [=~pattern]: _
 		if names, _ := s.schemaState(n, cue.StringKind, nil, false); !isAny(names) {
 			s.usedTypes |= cue.StructKind
-			s.addConjunct(n, ast.NewStruct(ast.NewList((names)), ast.NewIdent("_")))
+			x := ast.NewStruct(ast.NewList(names), ast.NewIdent("_"))
+			s.add(n, objectType, x)
 		}
 	}),
 
@@ -531,7 +530,8 @@
 		s.usedTypes |= cue.StructKind
 
 		pkg := s.addImport(n, "struct")
-		s.addConjunct(n, ast.NewCall(ast.NewSel(pkg, "MaxFields"), s.uint(n)))
+		x := ast.NewCall(ast.NewSel(pkg, "MaxFields"), s.uint(n))
+		s.add(n, objectType, x)
 	}),
 
 	p1("dependencies", func(n cue.Value, s *state) {
@@ -554,9 +554,7 @@
 		if n.Kind() != cue.StructKind {
 			s.errf(n, `value of "patternProperties" must be an an object, found %v`, n.Kind)
 		}
-		if s.obj == nil {
-			s.obj = &ast.StructLit{}
-		}
+		obj := s.object(n)
 		existing := excludeFields(s.obj.Elts)
 		s.processMap(n, func(key string, n cue.Value) {
 			// [!~(properties) & pattern]: schema
@@ -569,7 +567,7 @@
 				Value: s.schema(n),
 			}))
 			ast.SetRelPos(f, token.NewSection)
-			s.obj.Elts = append(s.obj.Elts, f)
+			obj.Elts = append(obj.Elts, f)
 		})
 	}),
 
@@ -584,24 +582,21 @@
 		case cue.StructKind:
 			s.usedTypes |= cue.StructKind
 			s.closeStruct = true
-			if s.obj == nil {
-				s.obj = &ast.StructLit{}
-			}
-			if len(s.obj.Elts) == 0 {
-				s.obj.Elts = append(s.obj.Elts, &ast.Field{
+			obj := s.object(n)
+			if len(obj.Elts) == 0 {
+				obj.Elts = append(obj.Elts, &ast.Field{
 					Label: ast.NewList(ast.NewIdent("string")),
 					Value: s.schema(n),
 				})
 				return
 			}
 			// [!~(properties|patternProperties)]: schema
-			existing := append(s.patterns, excludeFields(s.obj.Elts))
+			existing := append(s.patterns, excludeFields(obj.Elts))
 			f := internal.EmbedStruct(ast.NewStruct(&ast.Field{
 				Label: ast.NewList(ast.NewBinExpr(token.AND, existing...)),
 				Value: s.schema(n),
 			}))
-			ast.SetRelPos(f, token.NewSection)
-			s.obj.Elts = append(s.obj.Elts, f)
+			obj.Elts = append(obj.Elts, f)
 
 		default:
 			s.errf(n, `value of "additionalProperties" must be an object or boolean`)
@@ -616,7 +611,7 @@
 		case cue.StructKind:
 			elem := s.schema(n)
 			ast.SetRelPos(elem, token.NoRelPos)
-			s.addConjunct(n, ast.NewList(&ast.Ellipsis{Type: elem}))
+			s.add(n, arrayType, ast.NewList(&ast.Ellipsis{Type: elem}))
 
 		case cue.ListKind:
 			var a []ast.Expr
@@ -626,7 +621,7 @@
 				a = append(a, v)
 			}
 			s.list = ast.NewList(a...)
-			s.addConjunct(n, s.list)
+			s.add(n, arrayType, s.list)
 
 		default:
 			s.errf(n, `value of "items" must be an object or array`)
@@ -655,7 +650,8 @@
 		list := s.addImport(n, "list")
 		// TODO: Passing non-concrete values is not yet supported in CUE.
 		if x := s.schema(n); !isAny(x) {
-			s.addConjunct(n, ast.NewCall(ast.NewSel(list, "Contains"), clearPos(x)))
+			x := ast.NewCall(ast.NewSel(list, "Contains"), clearPos(x))
+			s.add(n, arrayType, x)
 		}
 	}),
 
@@ -671,7 +667,7 @@
 		for ; p > 0; p-- {
 			a = append(a, ast.NewIdent("_"))
 		}
-		s.addConjunct(n, ast.NewList(append(a, &ast.Ellipsis{})...))
+		s.add(n, arrayType, ast.NewList(append(a, &ast.Ellipsis{})...))
 
 		// TODO: use this once constraint resolution is properly implemented.
 		// list := s.addImport(n, "list")
@@ -681,14 +677,16 @@
 	p1("maxItems", func(n cue.Value, s *state) {
 		s.usedTypes |= cue.ListKind
 		list := s.addImport(n, "list")
-		s.addConjunct(n, ast.NewCall(ast.NewSel(list, "MaxItems"), clearPos(s.uint(n))))
+		x := ast.NewCall(ast.NewSel(list, "MaxItems"), clearPos(s.uint(n)))
+		s.add(n, arrayType, x)
+
 	}),
 
 	p1("uniqueItems", func(n cue.Value, s *state) {
 		s.usedTypes |= cue.ListKind
 		if s.boolValue(n) {
 			list := s.addImport(n, "list")
-			s.addConjunct(n, ast.NewCall(ast.NewSel(list, "UniqueItems")))
+			s.add(n, arrayType, ast.NewCall(ast.NewSel(list, "UniqueItems")))
 		}
 	}),
 }
diff --git a/encoding/jsonschema/decode.go b/encoding/jsonschema/decode.go
index 20ffc73..4be4718 100644
--- a/encoding/jsonschema/decode.go
+++ b/encoding/jsonschema/decode.go
@@ -20,8 +20,8 @@
 
 import (
 	"fmt"
-	"math/bits"
 	"net/url"
+	"sort"
 	"strings"
 
 	"cuelang.org/go/cue"
@@ -225,6 +225,85 @@
 
 // const draftCutoff = 5
 
+type coreType int
+
+const (
+	nullType coreType = iota
+	boolType
+	numType
+	stringType
+	arrayType
+	objectType
+
+	numCoreTypes
+)
+
+var coreToCUE = []cue.Kind{
+	nullType:   cue.NullKind,
+	boolType:   cue.BoolKind,
+	numType:    cue.FloatKind,
+	stringType: cue.StringKind,
+	arrayType:  cue.ListKind,
+	objectType: cue.StructKind,
+}
+
+func kindToAST(k cue.Kind) ast.Expr {
+	switch k {
+	case cue.NullKind:
+		// TODO: handle OpenAPI restrictions.
+		return ast.NewNull()
+	case cue.BoolKind:
+		return ast.NewIdent("bool")
+	case cue.FloatKind:
+		return ast.NewIdent("number")
+	case cue.StringKind:
+		return ast.NewIdent("string")
+	case cue.ListKind:
+		return ast.NewList(&ast.Ellipsis{})
+	case cue.StructKind:
+		return ast.NewStruct(&ast.Ellipsis{})
+	}
+	return nil
+}
+
+var coreTypeName = []string{
+	nullType:   "null",
+	boolType:   "bool",
+	numType:    "number",
+	stringType: "string",
+	arrayType:  "array",
+	objectType: "object",
+}
+
+type constraintInfo struct {
+	// typ is an identifier for the root type, if present.
+	// This can be omitted if there are constraints.
+	typ         ast.Expr
+	constraints []ast.Expr
+}
+
+func (c *constraintInfo) setTypeUsed(n cue.Value, t coreType) {
+	c.typ = kindToAST(coreToCUE[t])
+	setPos(c.typ, n)
+	ast.SetRelPos(c.typ, token.NoRelPos)
+}
+
+func (c *constraintInfo) add(n cue.Value, x ast.Expr) {
+	if !isAny(x) {
+		setPos(x, n)
+		ast.SetRelPos(x, token.NoRelPos)
+		c.constraints = append(c.constraints, x)
+	}
+}
+
+func (s *state) add(n cue.Value, t coreType, x ast.Expr) {
+	s.types[t].add(n, x)
+}
+
+func (s *state) setTypeUsed(n cue.Value, t coreType) {
+	s.types[t].setTypeUsed(n, t)
+}
+
 type state struct {
 	*decoder
 
@@ -240,7 +319,10 @@
 
 	pos cue.Value
 
-	typeOptional bool
+	// The constraints in types represent disjunctions per type.
+	types [numCoreTypes]constraintInfo
+	all   constraintInfo // values and oneOf etc.
+
 	usedTypes    cue.Kind
 	allowedTypes cue.Kind
 
@@ -254,8 +336,6 @@
 
 	definitions []ast.Decl
 
-	conjuncts []ast.Expr
-
 	// Used for inserting definitions, properties, etc.
 	hasSelfReference bool
 	obj              *ast.StructLit
@@ -279,9 +359,24 @@
 	refs  []*ast.Ident
 }
 
+func (s *state) object(n cue.Value) *ast.StructLit {
+	if s.obj == nil {
+		s.obj = &ast.StructLit{}
+		s.add(n, objectType, s.obj)
+	}
+	return s.obj
+}
+
 func (s *state) hasConstraints() bool {
-	return len(s.conjuncts) > 0 ||
-		len(s.patterns) > 0 ||
+	if len(s.all.constraints) > 0 {
+		return true
+	}
+	for _, t := range s.types {
+		if len(t.constraints) > 0 {
+			return true
+		}
+	}
+	return len(s.patterns) > 0 ||
 		s.title != "" ||
 		s.description != "" ||
 		s.obj != nil
@@ -295,59 +390,77 @@
 	conjuncts := []ast.Expr{}
 	disjuncts := []ast.Expr{}
 
-	add := func(e ast.Expr) {
-		disjuncts = append(disjuncts, e) // TODO: use setPos
-	}
-
 	types := s.allowedTypes &^ s.usedTypes
 	if types == allTypes {
-		add(ast.NewIdent("_"))
+		disjuncts = append(disjuncts, ast.NewIdent("_"))
 		types = 0
 	}
-	if types&cue.FloatKind != 0 {
-		add(ast.NewIdent("number"))
-		types &^= cue.IntKind
-	}
-	for types != 0 {
-		k := cue.Kind(1 << uint(bits.TrailingZeros(uint(types))))
-		types &^= k
 
-		switch k {
-		case cue.NullKind:
-			// TODO: handle OpenAPI restrictions.
-			add(ast.NewNull())
-		case cue.BoolKind:
-			add(ast.NewIdent("bool"))
-		case cue.StringKind:
-			add(ast.NewIdent("string"))
-		case cue.IntKind:
-			add(ast.NewIdent("int"))
-		case cue.ListKind:
-			add(ast.NewList(&ast.Ellipsis{}))
-		case cue.StructKind:
-			add(ast.NewStruct(&ast.Ellipsis{}))
+	// Sort literal structs and list last for nicer formatting.
+	sort.SliceStable(s.types[arrayType].constraints, func(i, j int) bool {
+		_, ok := s.types[arrayType].constraints[i].(*ast.ListLit)
+		return !ok
+	})
+	sort.SliceStable(s.types[objectType].constraints, func(i, j int) bool {
+		_, ok := s.types[objectType].constraints[i].(*ast.StructLit)
+		return !ok
+	})
+
+	for i, t := range s.types {
+		k := coreToCUE[i]
+		isAllowed := s.allowedTypes&k != 0
+		if len(t.constraints) > 0 {
+			if t.typ == nil && !isAllowed {
+				for _, c := range t.constraints {
+					s.addErr(errors.Newf(c.Pos(),
+						"constraint not allowed because type %s is excluded",
+						coreTypeName[i],
+					))
+				}
+				continue
+			}
+			x := ast.NewBinExpr(token.AND, t.constraints...)
+			disjuncts = append(disjuncts, x)
+		} else if s.usedTypes&k != 0 {
+			continue
+		} else if t.typ != nil {
+			if !isAllowed {
+				s.addErr(errors.Newf(t.typ.Pos(),
+					"constraint not allowed because type %s is excluded",
+					coreTypeName[i],
+				))
+				continue
+			}
+			disjuncts = append(disjuncts, t.typ)
+		} else if types&k != 0 {
+			x := kindToAST(k)
+			if x != nil {
+				disjuncts = append(disjuncts, x)
+			}
 		}
 	}
 
-	conjuncts = append(conjuncts, s.conjuncts...)
+	conjuncts = append(conjuncts, s.all.constraints...)
 
-	if s.obj != nil {
+	obj := s.obj
+	if obj == nil {
+		obj, _ = s.types[objectType].typ.(*ast.StructLit)
+	}
+	if obj != nil {
 		// TODO: may need to explicitly close.
 		if !s.closeStruct {
-			s.obj.Elts = append(s.obj.Elts, &ast.Ellipsis{})
+			obj.Elts = append(obj.Elts, &ast.Ellipsis{})
 		}
-		conjuncts = append(conjuncts, s.obj)
 	}
 
-	if len(conjuncts) > 0 {
-		disjuncts = append(disjuncts,
-			ast.NewBinExpr(token.AND, conjuncts...))
+	if len(disjuncts) > 0 {
+		conjuncts = append(conjuncts, ast.NewBinExpr(token.OR, disjuncts...))
 	}
 
-	if len(disjuncts) == 0 {
+	if len(conjuncts) == 0 {
 		e = &ast.BottomLit{}
 	} else {
-		e = ast.NewBinExpr(token.OR, disjuncts...)
+		e = ast.NewBinExpr(token.AND, conjuncts...)
 	}
 
 	if s.default_ != nil {
@@ -406,14 +519,6 @@
 	}
 }
 
-func (s *state) addConjunct(n cue.Value, e ast.Expr) {
-	if !isAny(e) {
-		ast.SetPos(e, n.Pos())
-		ast.SetRelPos(e, token.NoRelPos)
-		s.conjuncts = append(s.conjuncts, e)
-	}
-}
-
 func (s *state) schema(n cue.Value, idRef ...label) ast.Expr {
 	expr, _ := s.schemaState(n, allTypes, idRef, false)
 	// TODO: report unused doc.
diff --git a/encoding/jsonschema/decode_test.go b/encoding/jsonschema/decode_test.go
index 39c09f4..adc5bd6 100644
--- a/encoding/jsonschema/decode_test.go
+++ b/encoding/jsonschema/decode_test.go
@@ -69,6 +69,7 @@
 			var in *cue.Instance
 			var out, errout []byte
 			outIndex := -1
+			errIndex := -1
 
 			for i, f := range a.Files {
 				switch path.Ext(f.Name) {
@@ -81,22 +82,31 @@
 					outIndex = i
 				case ".err":
 					errout = f.Data
+					errIndex = i
 				}
 			}
 			if err != nil {
 				t.Fatal(err)
 			}
 
+			updated := false
+
 			expr, err := Extract(in, cfg)
-			if err != nil && errout == nil {
-				t.Fatal(errors.Details(err, nil))
-			}
-			got := []byte(nil)
 			if err != nil {
-				got = []byte(err.Error())
-			}
-			if !cmp.Equal(errout, got) {
-				t.Error(cmp.Diff(string(got), string(errout)))
+				got := []byte(errors.Details(err, nil))
+
+				got = bytes.TrimSpace(got)
+				errout = bytes.TrimSpace(errout)
+
+				switch {
+				case !cmp.Equal(errout, got):
+					if *update {
+						a.Files[errIndex].Data = got
+						updated = true
+						break
+					}
+					t.Error(cmp.Diff(string(got), string(errout)))
+				}
 			}
 
 			if expr != nil {
@@ -115,19 +125,24 @@
 				b = bytes.TrimSpace(b)
 				out = bytes.TrimSpace(out)
 
-				if !cmp.Equal(b, out) {
+				switch {
+				case !cmp.Equal(b, out):
 					if *update {
+						updated = true
 						a.Files[outIndex].Data = b
-						b = txtar.Format(a)
-						err = ioutil.WriteFile(fullpath, b, 0644)
-						if err != nil {
-							t.Fatal(err)
-						}
-						return
+						break
 					}
 					t.Error(cmp.Diff(string(out), string(b)))
 				}
 			}
+
+			if updated {
+				b := txtar.Format(a)
+				err = ioutil.WriteFile(fullpath, b, 0644)
+				if err != nil {
+					t.Fatal(err)
+				}
+			}
 		})
 		return nil
 	})
diff --git a/encoding/jsonschema/testdata/emptyanyof.txtar b/encoding/jsonschema/testdata/emptyanyof.txtar
new file mode 100644
index 0000000..0313e6d
--- /dev/null
+++ b/encoding/jsonschema/testdata/emptyanyof.txtar
@@ -0,0 +1,26 @@
+-- emptyanyof.json --
+{
+ "$defs": {
+   "shell": {
+      "description": "Specify a shell.",
+      "type": "string",
+      "anyOf": [
+        {
+        },
+        {
+          "enum": [
+            "bash",
+            "sh",
+            "cmd",
+            "powershell"
+          ]
+        }
+      ]
+   }
+ }
+}
+
+-- out.cue --
+_
+
+#shell: (string | ("bash" | "sh" | "cmd" | "powershell")) & string
diff --git a/encoding/jsonschema/testdata/err.txtar b/encoding/jsonschema/testdata/err.txtar
new file mode 100644
index 0000000..2175de8
--- /dev/null
+++ b/encoding/jsonschema/testdata/err.txtar
@@ -0,0 +1,20 @@
+-- type.json --
+{
+  "type": "object",
+
+  "properties": {
+    "multi": {
+        "type": [ "integer" ],
+        "minimum": 2,
+        "maximum": 3,
+        "maxLength": 5
+    }
+  },
+  "additionalProperties": false
+}
+
+-- out.err --
+constraint not allowed because type string is excluded:
+    type.json:9:22
+-- out.cue --
+multi?: int & >=2 & <=3
diff --git a/encoding/jsonschema/testdata/list.txtar b/encoding/jsonschema/testdata/list.txtar
index 3f7bd0a..c48c862 100644
--- a/encoding/jsonschema/testdata/list.txtar
+++ b/encoding/jsonschema/testdata/list.txtar
@@ -41,5 +41,5 @@
 foo?: [...string]
 tuple?: [string, int, 2]
 has?:  list.Contains(3)
-size?: [_, _, _, ...] & list.MaxItems(9) & list.UniqueItems()
+size?: list.UniqueItems() & list.MaxItems(9) & [_, _, _, ...]
 additional?: [int, int, ...string]
diff --git a/encoding/jsonschema/testdata/num.txtar b/encoding/jsonschema/testdata/num.txtar
index 1968c3a..cbbd984 100644
--- a/encoding/jsonschema/testdata/num.txtar
+++ b/encoding/jsonschema/testdata/num.txtar
@@ -13,10 +13,16 @@
         "maximum": 3
     },
     "exclusive": {
-        "type": "number",
+        "type": "integer",
         "exclusiveMinimum": 2,
         "exclusiveMaximum": 3
     },
+    "multi": {
+        "type": [ "integer", "string" ],
+        "minimum": 2,
+        "maximum": 3,
+        "maxLength": 5
+    },
     "cents": {
       "type": "number",
       "multipleOf": 0.05
@@ -26,10 +32,14 @@
 }
 
 -- out.cue --
-import "math"
+import (
+	"strings"
+	"math"
+)
 
 constant?:  2
 several?:   1 | 2 | 3 | 4
 inclusive?: >=2 & <=3
-exclusive?: >2 & <3
+exclusive?: int & >2 & <3
+multi?:     int & >=2 & <=3 | strings.MaxRunes(5)
 cents?:     math.MultipleOf(0.05)
diff --git a/encoding/jsonschema/testdata/object.txtar b/encoding/jsonschema/testdata/object.txtar
index 9f773e1..1e6644e 100644
--- a/encoding/jsonschema/testdata/object.txtar
+++ b/encoding/jsonschema/testdata/object.txtar
@@ -53,6 +53,15 @@
         "^\\P{Lo}": { "type": "integer" }
       },
       "additionalProperties": { "type": "string" }
+    },
+    "multi": {
+      "type": [ "object", "number" ],
+      "properties": {
+        "foo": { "type": "number" },
+        "bar": { "type": "number" }
+      },
+      "maxProperties": 5,
+      "minimum": 7
     }
   },
   "additionalProperties": false
@@ -68,7 +77,6 @@
 additional?: {
 	foo?: number
 	bar?: number
-
 	{[!~"^(foo|bar)$"]: string}
 }
 map?: [string]: string
@@ -94,6 +102,10 @@
 	{[=~"^\\P{Lu}" & !~"^(foo|bar)$"]: string}
 
 	{[=~"^\\P{Lo}" & !~"^(foo|bar)$"]: int}
-
 	{[!~"^\\P{Lu}" & !~"^\\P{Lo}" & !~"^(foo|bar)$"]: string}
 }
+multi?: >=7 | struct.MaxFields(5) & {
+	foo?: number
+	bar?: number
+	...
+}
diff --git a/encoding/jsonschema/testdata/refroot.txtar b/encoding/jsonschema/testdata/refroot.txtar
index c090506..6f33439 100644
--- a/encoding/jsonschema/testdata/refroot.txtar
+++ b/encoding/jsonschema/testdata/refroot.txtar
@@ -16,7 +16,7 @@
 _schema
 _schema: {
 	@jsonschema(schema="http://json-schema.org/draft-07/schema#")
-	number | null | bool | string | [...] | {
+	null | bool | number | string | [...] | {
 		@jsonschema(id="http://cuelang.org/go/encoding/openapi/testdata/order.json")
 		value?: _
 		next?:  _schema_1
diff --git a/encoding/jsonschema/testdata/refroot2.txtar b/encoding/jsonschema/testdata/refroot2.txtar
index 20e6361..e4a162c 100644
--- a/encoding/jsonschema/testdata/refroot2.txtar
+++ b/encoding/jsonschema/testdata/refroot2.txtar
@@ -14,7 +14,7 @@
 _schema
 _schema: {
 	@jsonschema(schema="http://json-schema.org/draft-07/schema#")
-	number | null | bool | string | [...] | {
+	null | bool | number | string | [...] | {
 		value?: _
 		next?:  _schema_1
 		...
diff --git a/encoding/jsonschema/testdata/typedis.txtar b/encoding/jsonschema/testdata/typedis.txtar
index 6bf10ac..d2a3aeb 100644
--- a/encoding/jsonschema/testdata/typedis.txtar
+++ b/encoding/jsonschema/testdata/typedis.txtar
@@ -34,8 +34,6 @@
         }
       ]
     },
-
-
     "empty": {
         "allOf": [
             { "type": "object" },
@@ -44,11 +42,14 @@
     }
   }
 }
+-- out.err --
+constraint not allowed because type string is excluded:
+    type.json:39:23
 -- out.cue --
 // Main schema
 intOrString1?: int | string
 intOrString2?: int | string
-intOrString3?: int | string
-disjunction?:  int | string | >=3
+intOrString3?: string | int
+disjunction?:  string | int | int & >=3
 empty?:        _|_
 ...
diff --git a/encoding/jsonschema/testdata/used.txtar b/encoding/jsonschema/testdata/used.txtar
new file mode 100644
index 0000000..b22c92f
--- /dev/null
+++ b/encoding/jsonschema/testdata/used.txtar
@@ -0,0 +1,48 @@
+-- used.json --
+{
+    "$defs": {
+        "enum": {
+            "type": "string",
+            "enum": [ "a", "b", "c" ]
+        },
+        "lists": {
+            "description": "Single item or lists of various lengths.",
+            "oneOf": [
+                {
+                    "type": "string",
+                    "enum": [ "a", "b", "c" ]
+                },
+                {
+                    "type": "array",
+                    "oneOf": [
+                        {
+                            "items": [ { "const": "X" } ]
+                        },
+                        {
+                            "items": [
+                                { "const": "X" },
+                                {
+                                    "type": "string",
+                                    "enum": [ "a", "b", "c" ]
+                                }
+                            ]
+                        },
+                        {
+                            "items": [
+                                { "const": "X" },
+                                { "enum": [ "d", "e", "f" ] }
+                            ]
+                        }
+                    ],
+                    "additionalItems": false
+                }
+            ]
+        }
+    }
+}
+-- out.cue --
+_
+
+#enum: "a" | "b" | "c"
+
+#lists: "a" | "b" | "c" | (["X"] | ["X", "a" | "b" | "c"] | ["X", "d" | "e" | "f"])