internal/core/adt: clean up Builtin and Validator semantics

- A Builtin now no longer validates
- Instead, the evaluator explicitly converts Builtins to
   BuiltinValidators when appropriate
- Builtins now have the "func" type. This is because their
   return type is not known (depends on how it is used).
- Builtin implementations may now return *Bottom,
   insta-promoting them to validators (not strictly necessary,
   but they make no sense as a builtin per se).

Fixes #603

Change-Id: Id72d7088fa1cea71b0b606ca7252399bea3518c1
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/7884
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/builtin_test.go b/cue/builtin_test.go
index c8698cd..679d493 100644
--- a/cue/builtin_test.go
+++ b/cue/builtin_test.go
@@ -104,7 +104,9 @@
 		`{a:1}`,
 	}, {
 		test("struct", `struct.MinFields(2) & {a: 1}`),
-		`_|_(invalid value {a:1} (does not satisfy struct.MinFields(2)))`,
+		// TODO: original value may be better.
+		// `_|_(invalid value {a:1} (does not satisfy struct.MinFields(2)))`,
+		`_|_(struct has 1 fields < MinFields(2))`,
 	}, {
 		test("time", `time.Time & "1937-01-01T12:00:27.87+00:20"`),
 		`"1937-01-01T12:00:27.87+00:20"`,
diff --git a/cue/testdata/builtins/validators.txtar b/cue/testdata/builtins/validators.txtar
new file mode 100644
index 0000000..bafa49e
--- /dev/null
+++ b/cue/testdata/builtins/validators.txtar
@@ -0,0 +1,273 @@
+-- in.cue --
+import (
+    "struct"
+    "encoding/json"
+)
+
+
+// non-monotonic builtins must fail with an "incomplete" error if there
+// is a possibility the constraint can get resolved by becoming more specific.
+incompleteError1: {
+    MyType: {
+            kv: struct.MinFields(1)
+    }
+
+    foo: MyType & {
+            kv: joel: "testing"
+    }
+}
+
+incompleteError2: {
+    MyType: {
+            kv: [string]: string
+            kv: struct.MinFields(1)
+    }
+
+    foo: MyType & {
+            kv: joel: "testing"
+    }
+}
+
+incompleteError3: {
+    t: string
+    t: json.Validate(string)
+}
+
+uniqueConstrains1: {
+    t: string
+    t: json.Validate(string)
+    t: json.Validate(string)
+}
+
+uniqueConstrains2: {
+    t: struct.MaxFields(1)
+    t: struct.MaxFields(1)
+}
+
+violation: {
+    #MyType: {
+            kv: [string]: string
+            kv: struct.MinFields(1)
+    }
+
+    foo: #MyType & {
+            kv: joel: "testing"
+            kv: tony: "testing"
+    }
+}
+
+conjuncts: {
+    kv: struct.MinFields(1)
+    kv: struct.MaxFields(3)
+}
+
+// TODO: stripe off conflicting pairs
+// conflicting: {
+//     kv: struct.MinFields(3)
+//     kv: struct.MaxFields(1)
+// }
+
+// Builtins with bool return that can be used as validator.
+
+bareBuiltin: {
+    a: json.Valid
+    a: json.Valid
+}
+
+bareBuiltinCheck: {
+    a: json.Valid
+    a: "3"
+}
+
+builtinValidator: {
+    a: json.Valid()
+    a: json.Valid()
+}
+
+builtinValidatorCheck: {
+    a: json.Valid()
+    a: "3"
+}
+
+callOfCallToValidator: {
+    a: json.Valid
+    b: a()
+    e: b() // not allowed
+    e: "5"
+}
+
+validatorAsFunction: {
+    a: json.Valid
+    b: a("3")
+    c: json.Valid("3")
+}
+
+-- out/eval --
+Errors:
+callOfCallToValidator.e: cannot call previously called validator b:
+    ./in.cue:94:8
+
+Result:
+(_|_){
+  // [eval]
+  incompleteError1: (struct){
+    MyType: (struct){
+      kv: (struct){ struct.MinFields(1) }
+    }
+    foo: (struct){
+      kv: (struct){
+        joel: (string){ "testing" }
+      }
+    }
+  }
+  incompleteError2: (struct){
+    MyType: (struct){
+      kv: (_|_){
+        // [incomplete] struct has 0 fields < MinFields(1)
+      }
+    }
+    foo: (struct){
+      kv: (struct){
+        joel: (string){ "testing" }
+      }
+    }
+  }
+  incompleteError3: (struct){
+    t: (string){ &("encoding/json".Validate(string), string) }
+  }
+  uniqueConstrains1: (struct){
+    t: (string){ &("encoding/json".Validate(string), string) }
+  }
+  uniqueConstrains2: (struct){
+    t: (struct){ struct.MaxFields(1) }
+  }
+  violation: (struct){
+    #MyType: (#struct){
+      kv: (_|_){
+        // [incomplete] struct has 0 fields < MinFields(1)
+      }
+    }
+    foo: (#struct){
+      kv: (#struct){
+        joel: (string){ "testing" }
+        tony: (string){ "testing" }
+      }
+    }
+  }
+  conjuncts: (struct){
+    kv: (struct){ &(struct.MinFields(1), struct.MaxFields(3)) }
+  }
+  bareBuiltin: (struct){
+    a: ((string|bytes)){ "encoding/json".Valid() }
+  }
+  bareBuiltinCheck: (struct){
+    a: (string){ "3" }
+  }
+  builtinValidator: (struct){
+    a: ((string|bytes)){ "encoding/json".Valid() }
+  }
+  builtinValidatorCheck: (struct){
+    a: (string){ "3" }
+  }
+  callOfCallToValidator: (_|_){
+    // [eval]
+    a: ((string|bytes)){ "encoding/json".Valid() }
+    b: ((string|bytes)){ "encoding/json".Valid() }
+    e: (_|_){
+      // [eval] callOfCallToValidator.e: cannot call previously called validator b:
+      //     ./in.cue:94:8
+    }
+  }
+  validatorAsFunction: (struct){
+    a: ((string|bytes)){ "encoding/json".Valid() }
+    b: (bool){ true }
+    c: (bool){ true }
+  }
+}
+-- out/compile --
+--- in.cue
+{
+  incompleteError1: {
+    MyType: {
+      kv: 〈import;struct〉.MinFields(1)
+    }
+    foo: (〈0;MyType〉 & {
+      kv: {
+        joel: "testing"
+      }
+    })
+  }
+  incompleteError2: {
+    MyType: {
+      kv: {
+        [string]: string
+      }
+      kv: 〈import;struct〉.MinFields(1)
+    }
+    foo: (〈0;MyType〉 & {
+      kv: {
+        joel: "testing"
+      }
+    })
+  }
+  incompleteError3: {
+    t: string
+    t: 〈import;"encoding/json"〉.Validate(string)
+  }
+  uniqueConstrains1: {
+    t: string
+    t: 〈import;"encoding/json"〉.Validate(string)
+    t: 〈import;"encoding/json"〉.Validate(string)
+  }
+  uniqueConstrains2: {
+    t: 〈import;struct〉.MaxFields(1)
+    t: 〈import;struct〉.MaxFields(1)
+  }
+  violation: {
+    #MyType: {
+      kv: {
+        [string]: string
+      }
+      kv: 〈import;struct〉.MinFields(1)
+    }
+    foo: (〈0;#MyType〉 & {
+      kv: {
+        joel: "testing"
+      }
+      kv: {
+        tony: "testing"
+      }
+    })
+  }
+  conjuncts: {
+    kv: 〈import;struct〉.MinFields(1)
+    kv: 〈import;struct〉.MaxFields(3)
+  }
+  bareBuiltin: {
+    a: 〈import;"encoding/json"〉.Valid
+    a: 〈import;"encoding/json"〉.Valid
+  }
+  bareBuiltinCheck: {
+    a: 〈import;"encoding/json"〉.Valid
+    a: "3"
+  }
+  builtinValidator: {
+    a: 〈import;"encoding/json"〉.Valid()
+    a: 〈import;"encoding/json"〉.Valid()
+  }
+  builtinValidatorCheck: {
+    a: 〈import;"encoding/json"〉.Valid()
+    a: "3"
+  }
+  callOfCallToValidator: {
+    a: 〈import;"encoding/json"〉.Valid
+    b: 〈0;a〉()
+    e: 〈0;b〉()
+    e: "5"
+  }
+  validatorAsFunction: {
+    a: 〈import;"encoding/json"〉.Valid
+    b: 〈0;a〉("3")
+    c: 〈import;"encoding/json"〉.Valid("3")
+  }
+}
diff --git a/cue/testdata/eval/issue545.txtar b/cue/testdata/eval/issue545.txtar
index 1a069ec..7e6fa60 100644
--- a/cue/testdata/eval/issue545.txtar
+++ b/cue/testdata/eval/issue545.txtar
@@ -69,9 +69,9 @@
     a: (string){ =~"foo" }
     b: (string){ =~"foo" }
     c: (string){ =~"foo" }
-    d: (string){ time.Time }
+    d: (string){ time.Time() }
     e: (string){ time.Time() }
-    f: (string){ time.Time }
+    f: (string){ time.Time() }
   }
 }
 -- out/compile --
diff --git a/cue/testdata/resolve/011_bounds.txtar b/cue/testdata/resolve/011_bounds.txtar
index 6fca110..d489811 100644
--- a/cue/testdata/resolve/011_bounds.txtar
+++ b/cue/testdata/resolve/011_bounds.txtar
@@ -138,10 +138,10 @@
 }
 -- out/eval --
 Errors:
-e1: conflicting values null and !=null (mismatched types null and (bool|string|bytes|list|struct|number)):
+e1: conflicting values null and !=null (mismatched types null and (bool|string|bytes|func|list|struct|number)):
     ./in.cue:40:5
     ./in.cue:40:12
-e2: conflicting values !=null and null (mismatched types (bool|string|bytes|list|struct|number) and null):
+e2: conflicting values !=null and null (mismatched types (bool|string|bytes|func|list|struct|number) and null):
     ./in.cue:41:5
     ./in.cue:41:14
 e5: incompatible bounds >1 and <0:
@@ -200,12 +200,12 @@
   s23e: (number){ &(>0.0, <2.0) }
   s30: (int){ &(>0, int) }
   e1: (_|_){
-    // [eval] e1: conflicting values null and !=null (mismatched types null and (bool|string|bytes|list|struct|number)):
+    // [eval] e1: conflicting values null and !=null (mismatched types null and (bool|string|bytes|func|list|struct|number)):
     //     ./in.cue:40:5
     //     ./in.cue:40:12
   }
   e2: (_|_){
-    // [eval] e2: conflicting values !=null and null (mismatched types (bool|string|bytes|list|struct|number) and null):
+    // [eval] e2: conflicting values !=null and null (mismatched types (bool|string|bytes|func|list|struct|number) and null):
     //     ./in.cue:41:5
     //     ./in.cue:41:14
   }
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index d471164..d9d1865 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -792,7 +792,7 @@
 	case cue.CallOp:
 		name := fmt.Sprint(a[0])
 		switch name {
-		case "list.UniqueItems":
+		case "list.UniqueItems", "list.UniqueItems()":
 			b.checkArgs(a, 0)
 			b.setFilter("Schema", "uniqueItems", ast.NewBool(true))
 			return
diff --git a/encoding/openapi/types.go b/encoding/openapi/types.go
index ccc237a..7306660 100644
--- a/encoding/openapi/types.go
+++ b/encoding/openapi/types.go
@@ -36,6 +36,7 @@
 
 	"bytes": "binary",
 
+	"time.Time()":                "date-time",
 	"time.Time":                  "date-time",
 	`time.Format ("2006-01-02")`: "date",
 
diff --git a/internal/core/adt/adt.go b/internal/core/adt/adt.go
index 13038a5..d746190 100644
--- a/internal/core/adt/adt.go
+++ b/internal/core/adt/adt.go
@@ -162,8 +162,7 @@
 func (*Disjunction) Concreteness() Concreteness { return Constraint }
 func (*BoundValue) Concreteness() Concreteness  { return Constraint }
 
-// Constraint only applies if Builtin is used as constraint.
-func (*Builtin) Concreteness() Concreteness          { return Constraint }
+func (*Builtin) Concreteness() Concreteness          { return Concrete }
 func (*BuiltinValidator) Concreteness() Concreteness { return Constraint }
 
 // Value and Expr
diff --git a/internal/core/adt/expr.go b/internal/core/adt/expr.go
index 16a7ec9..fa791d9 100644
--- a/internal/core/adt/expr.go
+++ b/internal/core/adt/expr.go
@@ -898,6 +898,33 @@
 
 func (x *CallExpr) evaluate(c *OpContext) Value {
 	fun := c.value(x.Fun)
+	var b *Builtin
+	switch f := fun.(type) {
+	case *Builtin:
+		b = f
+
+	case *BuiltinValidator:
+		// We allow a validator that takes no arguments accept the validated
+		// value to be called with zero arguments.
+		switch {
+		case f.Src != nil:
+			c.addErrf(0, pos(x.Fun),
+				"cannot call previously called validator %s", c.Str(x.Fun))
+
+		case f.Builtin.IsValidator(len(x.Args)):
+			v := *f
+			v.Src = x
+			return &v
+
+		default:
+			b = f.Builtin
+		}
+
+	default:
+		c.addErrf(0, pos(x.Fun), "cannot call non-function %s (type %s)",
+			c.Str(x.Fun), kind(fun))
+		return nil
+	}
 	args := []Value{}
 	for i, a := range x.Args {
 		expr := c.value(a)
@@ -921,13 +948,10 @@
 	if c.HasErr() {
 		return nil
 	}
-	b, _ := fun.(*Builtin)
-	if b == nil {
-		c.addErrf(0, pos(x.Fun), "cannot call non-function %s (type %s)",
-			c.Str(x.Fun), kind(fun))
-		return nil
+	if b.IsValidator(len(args)) {
+		return &BuiltinValidator{x, b, args}
 	}
-	result := b.call(c, x.Src, args)
+	result := b.call(c, pos(x), args)
 	if result == nil {
 		return nil
 	}
@@ -943,8 +967,6 @@
 
 	Package Feature
 	Name    string
-	// REMOVE: for legacy
-	Const string
 }
 
 type Param struct {
@@ -970,22 +992,19 @@
 
 // Kind here represents the case where Builtin is used as a Validator.
 func (x *Builtin) Kind() Kind {
-	if len(x.Params) == 0 {
-		return BottomKind
-	}
-	return x.Params[0].Kind()
+	return FuncKind
 }
 
-func (x *Builtin) validate(c *OpContext, v Value) *Bottom {
-	if x.Result != BoolKind {
-		return c.NewErrf(
-			"invalid validator %s: not a bool return", x.Name)
+func (x *Builtin) BareValidator() *BuiltinValidator {
+	if len(x.Params) != 1 ||
+		(x.Result != BoolKind && x.Result != BottomKind) {
+		return nil
 	}
-	if len(x.Params) != 1 {
-		return c.NewErrf(
-			"invalid validator %s: may only have one validator to be used without call", x.Name)
-	}
-	return validateWithBuiltin(c, nil, x, []Value{v})
+	return &BuiltinValidator{Builtin: x}
+}
+
+func (x *Builtin) IsValidator(numArgs int) bool {
+	return len(x.Params)-1 == numArgs && x.Result&^BoolKind == 0
 }
 
 func bottom(v Value) *Bottom {
@@ -996,23 +1015,20 @@
 	return b
 }
 
-func (x *Builtin) call(c *OpContext, call *ast.CallExpr, args []Value) Expr {
-	if len(x.Params)-1 == len(args) && x.Result == BoolKind {
-		// We have a custom builtin
-		return &BuiltinValidator{call, x, args}
-	}
+func (x *Builtin) call(c *OpContext, p token.Pos, args []Value) Expr {
+	fun := x // right now always x.
 	if len(args) > len(x.Params) {
-		c.addErrf(0, call.Rparen,
+		c.addErrf(0, p,
 			"too many arguments in call to %s (have %d, want %d)",
-			call.Fun, len(args), len(x.Params))
+			fun, len(args), len(x.Params))
 		return nil
 	}
 	for i := len(args); i < len(x.Params); i++ {
 		v := x.Params[i].Default()
 		if v == nil {
-			c.addErrf(0, call.Rparen,
+			c.addErrf(0, p,
 				"not enough arguments in call to %s (have %d, want %d)",
-				call.Fun, len(args), len(x.Params))
+				fun, len(args), len(x.Params))
 			return nil
 		}
 		args = append(args, v)
@@ -1030,7 +1046,7 @@
 				}
 				c.addErrf(code, pos(a),
 					"cannot use %s (type %s) as %s in argument %d to %s",
-					a, k, x.Params[i].Kind(), i+1, call.Fun)
+					a, k, x.Params[i].Kind(), i+1, fun)
 				return nil
 			}
 		}
@@ -1046,16 +1062,23 @@
 //    strings.MinRunes(4)
 //
 type BuiltinValidator struct {
-	Src     *ast.CallExpr
+	Src     *CallExpr
 	Builtin *Builtin
 	Args    []Value // any but the first value
 }
 
 func (x *BuiltinValidator) Source() ast.Node {
 	if x.Src == nil {
-		return nil
+		return x.Builtin.Source()
 	}
-	return x.Src
+	return x.Src.Source()
+}
+
+func (x *BuiltinValidator) Pos() token.Pos {
+	if src := x.Source(); src != nil {
+		return src.Pos()
+	}
+	return token.NoPos
 }
 
 func (x *BuiltinValidator) Kind() Kind {
@@ -1066,10 +1089,11 @@
 	args := make([]Value, len(x.Args)+1)
 	args[0] = v
 	copy(args[1:], x.Args)
-	return validateWithBuiltin(c, x.Src, x.Builtin, args)
+
+	return validateWithBuiltin(c, x.Pos(), x.Builtin, args)
 }
 
-func validateWithBuiltin(c *OpContext, src *ast.CallExpr, b *Builtin, args []Value) *Bottom {
+func validateWithBuiltin(c *OpContext, src token.Pos, b *Builtin, args []Value) *Bottom {
 	res := b.call(c, src, args)
 	switch v := res.(type) {
 	case nil:
diff --git a/internal/core/adt/kind.go b/internal/core/adt/kind.go
index 5715693..1c3bd7e 100644
--- a/internal/core/adt/kind.go
+++ b/internal/core/adt/kind.go
@@ -63,6 +63,7 @@
 	FloatKind
 	StringKind
 	BytesKind
+	FuncKind
 	ListKind
 	StructKind
 
@@ -162,6 +163,7 @@
 	FloatKind:   "float",
 	StringKind:  "string",
 	BytesKind:   "bytes",
+	FuncKind:    "func",
 	StructKind:  "struct",
 	ListKind:    "list",
 	_numberKind: "number",
@@ -175,6 +177,7 @@
 	FloatKind:   "float",
 	StringKind:  "string",
 	BytesKind:   "bytes",
+	FuncKind:    "_",
 	StructKind:  "{...}",
 	ListKind:    "[...]",
 	_numberKind: "number",
diff --git a/internal/core/adt/simplify.go b/internal/core/adt/simplify.go
index 9869ce8..c58ad47 100644
--- a/internal/core/adt/simplify.go
+++ b/internal/core/adt/simplify.go
@@ -204,24 +204,8 @@
 // now.
 func SimplifyValidator(ctx *OpContext, v, w Validator) Validator {
 	switch x := v.(type) {
-	case *Builtin:
-		switch y := w.(type) {
-		case *Builtin:
-			if x == y {
-				return x
-			}
-
-		case *BuiltinValidator:
-			if y.Builtin == x && len(y.Args) == 0 {
-				return x
-			}
-		}
-
 	case *BuiltinValidator:
 		switch y := w.(type) {
-		case *Builtin:
-			return SimplifyValidator(ctx, y, x)
-
 		case *BuiltinValidator:
 			if x == y {
 				return x
diff --git a/internal/core/eval/eval.go b/internal/core/eval/eval.go
index 84333de..0602a4d 100644
--- a/internal/core/eval/eval.go
+++ b/internal/core/eval/eval.go
@@ -554,7 +554,7 @@
 				}
 			}
 			for _, v := range n.checks {
-				// TODO(errors): make Validate return boolean and generate
+				// TODO(errors): make Validate return bottom and generate
 				// optimized conflict message. Also track and inject IDs
 				// to determine origin location.s
 				if b := ctx.Validate(v, n.node); b != nil {
@@ -1459,9 +1459,15 @@
 		// TODO: Use the Closer to close other fields as well?
 	}
 
-	if b, ok := v.(*adt.Bottom); ok {
+	switch b := v.(type) {
+	case *adt.Bottom:
 		n.addBottom(b)
 		return
+	case *adt.Builtin:
+		if v := b.BareValidator(); v != nil {
+			n.addValueConjunct(env, v, id)
+			return
+		}
 	}
 
 	if !n.updateNodeType(v.Kind(), v, id) {
@@ -1552,12 +1558,13 @@
 				return
 			}
 		}
+		n.updateNodeType(x.Kind(), x, id)
 		n.checks = append(n.checks, x)
 
 	case *adt.Vertex:
 	// handled above.
 
-	case adt.Value: // *NullLit, *BoolLit, *NumLit, *StringLit, *BytesLit
+	case adt.Value: // *NullLit, *BoolLit, *NumLit, *StringLit, *BytesLit, *Builtin
 		if y := n.scalar; y != nil {
 			if b, ok := adt.BinOp(ctx, adt.EqualOp, x, y).(*adt.Bool); !ok || !b.B {
 				n.addConflict(x, y, x.Kind(), y.Kind(), n.scalarID, id)
diff --git a/pkg/gen/gen.go b/pkg/gen/gen.go
index ba2b9bd..145fd4e 100644
--- a/pkg/gen/gen.go
+++ b/pkg/gen/gen.go
@@ -314,8 +314,10 @@
 		fmt.Fprintf(g.w, "{Kind: %s},\n", k)
 	}
 	fmt.Fprintf(g.w, "\n},\n")
-	result := g.goToCUE(x.Type.Results.List[0].Type)
-	fmt.Fprintf(g.w, "Result: %s,\n", result)
+
+	expr := x.Type.Results.List[0].Type
+	fmt.Fprintf(g.w, "Result: %s,\n", g.goToCUE(expr))
+
 	argList := strings.Join(args, ", ")
 	valList := strings.Join(vals, ", ")
 	init := ""
@@ -351,6 +353,8 @@
 		return "bigFloat"
 	case "big.Rat":
 		return "bigRat"
+	case "adt.Bottom":
+		return "error"
 	case "internal.Decimal":
 		return "decimal"
 	case "[]*internal.Decimal":
diff --git a/pkg/internal/builtin.go b/pkg/internal/builtin.go
index 29c94e7..bbd0ccb 100644
--- a/pkg/internal/builtin.go
+++ b/pkg/internal/builtin.go
@@ -148,6 +148,9 @@
 		}()
 		b.Func(c)
 		switch v := c.Ret.(type) {
+		case nil:
+			// Validators may return a nil in case validation passes.
+			return nil
 		case adt.Value:
 			return v
 		case bottomer:
diff --git a/pkg/math/testdata/round.txtar b/pkg/math/testdata/round.txtar
index fa3b04e..ae9093c 100644
--- a/pkg/math/testdata/round.txtar
+++ b/pkg/math/testdata/round.txtar
@@ -36,7 +36,7 @@
 Errors:
 error in call to math.MultipleOf: division by zero
 floorE1: too many arguments in call to math.Floor (have 2, want 1):
-    ./in.cue:17:25
+    ./in.cue:17:10
 floorE2: cannot use "foo" (type string) as number in argument 1 to math.Floor:
     ./in.cue:18:21
 
diff --git a/pkg/struct/pkg.go b/pkg/struct/pkg.go
index 9807c35..f7bf56e 100644
--- a/pkg/struct/pkg.go
+++ b/pkg/struct/pkg.go
@@ -23,11 +23,11 @@
 			{Kind: adt.StructKind},
 			{Kind: adt.IntKind},
 		},
-		Result: adt.BoolKind,
+		Result: adt.BottomKind,
 		Func: func(c *internal.CallCtxt) {
 			object, n := c.Struct(0), c.Int(1)
 			if c.Do() {
-				c.Ret, c.Err = MinFields(object, n)
+				c.Ret = MinFields(object, n)
 			}
 		},
 	}, {
diff --git a/pkg/struct/struct.go b/pkg/struct/struct.go
index 00094e1..d9b034e 100644
--- a/pkg/struct/struct.go
+++ b/pkg/struct/struct.go
@@ -17,22 +17,33 @@
 
 import (
 	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/errors"
+	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal/core/adt"
 )
 
 // MinFields validates the minimum number of fields that are part of a struct.
+// It can only be used as a validator, for instance `MinFields(3)`.
 //
 // Only fields that are part of the data model count. This excludes hidden
 // fields, optional fields, and definitions.
-func MinFields(object *cue.Struct, n int) (bool, error) {
+func MinFields(object *cue.Struct, n int) *adt.Bottom {
 	iter := object.Fields(cue.Hidden(false), cue.Optional(false))
 	count := 0
 	for iter.Next() {
 		count++
 	}
-	return count >= n, nil
+	if count < n {
+		return &adt.Bottom{
+			Code: adt.IncompleteError, // could still be resolved
+			Err:  errors.Newf(token.NoPos, "struct has %d fields < MinFields(%d)", count, n),
+		}
+	}
+	return nil
 }
 
 // MaxFields validates the maximum number of fields that are part of a struct.
+// It can only be used as a validator, for instance `MaxFields(3)`.
 //
 // Only fields that are part of the data model count. This excludes hidden
 // fields, optional fields, and definitions.
@@ -42,5 +53,6 @@
 	for iter.Next() {
 		count++
 	}
+	// permanent error is okay here.
 	return count <= n, nil
 }
diff --git a/pkg/struct/testdata/gen.txtar b/pkg/struct/testdata/gen.txtar
index 83c1382..1ef363b 100644
--- a/pkg/struct/testdata/gen.txtar
+++ b/pkg/struct/testdata/gen.txtar
@@ -13,17 +13,19 @@
 t1: conflicting values struct.MinFields(0) and "" (mismatched types struct and string):
     ./in.cue:3:5
     ./in.cue:3:27
-t3: invalid value {a:1} (does not satisfy struct.MinFields(2)):
-    ./in.cue:5:5
 t4: invalid value {a:1} (does not satisfy struct.MaxFields(0)):
     ./in.cue:6:5
 
 Result:
+import "struct"
+
 t1: _|_ // t1: conflicting values struct.MinFields(0) and "" (mismatched types struct and string)
 t2: {
 	a: 1
 }
-t3: _|_ // t3: invalid value {a:1} (does not satisfy struct.MinFields(2))
+t3: struct.MinFields(2) & {
+	a: 1
+}
 t4: _|_ // t4: invalid value {a:1} (does not satisfy struct.MaxFields(0))
 t5: {
 	a: 1