internal/core/adt: fix bug that left errors of incompleted expressions unreported
This also requires simplification of BoundExpr to simulate
complete cycle handling.
Fixes #475
Fixes #680
Fixes #684
Change-Id: I906d8cc6e3689abb74c7c1ebc2ba96ecd98aaf4c
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/8323
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/testdata/builtins/incomplete.txtar b/cue/testdata/builtins/incomplete.txtar
index 76954a4..f8d346f 100644
--- a/cue/testdata/builtins/incomplete.txtar
+++ b/cue/testdata/builtins/incomplete.txtar
@@ -79,9 +79,13 @@
// [incomplete] list2.#Sub: undefined field b:
// ./in.cue:30:13
}
- Out2: (list){
+ Out2: (_|_){
+ // [incomplete] list2.#Sub: undefined field b:
+ // ./in.cue:30:13
}
- Out3: (list){
+ Out3: (_|_){
+ // [incomplete] list2.#Sub: undefined field b:
+ // ./in.cue:30:13
}
_Top: (_|_){
// [incomplete] list2.#Sub: undefined field b:
diff --git a/cue/testdata/cycle/051_resolved_self-reference_cycles_with_disjunction.txtar b/cue/testdata/cycle/051_resolved_self-reference_cycles_with_disjunction.txtar
index af4e0c3..2d2d32d 100644
--- a/cue/testdata/cycle/051_resolved_self-reference_cycles_with_disjunction.txtar
+++ b/cue/testdata/cycle/051_resolved_self-reference_cycles_with_disjunction.txtar
@@ -186,7 +186,10 @@
}
xc1: (int){ |((int){ 8 }, (int){ 9 }) }
xc2: (int){ 8 }
- xc3: (int){ 6 }
+ xc3: (_|_){
+ // [incomplete] xc3: unresolved disjunction 8 | 9 (type int):
+ // ./in.cue:29:10
+ }
xc4: (int){ 9 }
xc5: (int){ 10 }
xd1: (int){ 8 }
@@ -235,5 +238,8 @@
}
z1: (int){ |((int){ 11 }, (int){ 13 }) }
z2: (int){ 10 }
- z3: (int){ 8 }
+ z3: (_|_){
+ // [incomplete] z3: unresolved disjunction 11 | 13 (type int):
+ // ./in.cue:58:5
+ }
}
diff --git a/cue/testdata/eval/bounds.txtar b/cue/testdata/eval/bounds.txtar
index 8ca60f3..45fa97b 100644
--- a/cue/testdata/eval/bounds.txtar
+++ b/cue/testdata/eval/bounds.txtar
@@ -10,8 +10,67 @@
b5: >=21 & >20
b6: int & >5 & <= 6
+simplifyExpr: {
+ less1: <(<3)
+ less2: <(<=3)
+ less3: <=(<3)
+ less4: <=(<=3)
+ less5: <(!=3)
+ less6: <=(!=3)
+
+ gtr1: >(>3)
+ gtr2: >(>=3)
+ gtr3: >=(>3)
+ gtr4: >=(>=3)
+ gtr5: >(!=3)
+ gtr6: >=(!=3)
+
+ lg1: <(>3)
+ lg2: <(>=3)
+ lg3: <=(>3)
+ lg4: <=(>=3)
+
+ gl1: >(<3)
+ gl2: >(<=3)
+ gl3: >=(<3)
+ gl4: >=(<=3)
+
+ ne1: !=(!=3)
+ ne2: !=(<3)
+ ne3: !=(<=3)
+ ne4: !=(>3)
+ ne5: !=(>=3)
+
+ s: string
+ n: number
+ i: int
+ f: float
+ b: bytes
+ basic1: <(i)
+ basic2: >(n)
+ basic3: >=(s)
+ basic4: <=(f)
+ basic5: <=(b)
+
+ // Do NOT interpret this the same as `!= type`.
+ bne1: !=(s)
+ bne2: !=(n)
+ bne3: !=(n)
+ bne4: !=(i)
+ bne5: !=(b)
+
+ e1: <(=~"foo")
+ e2: >(null)
+}
+
-- out/eval --
-(struct){
+Errors:
+simplifyExpr.e2: cannot use null for bound >:
+ ./in.cue:62:11
+
+Result:
+(_|_){
+ // [eval]
x0: (int){ 5 }
x1: (int){ 30 }
b0: (number){ &(>0, <5) }
@@ -21,6 +80,72 @@
b4: (number){ >20 }
b5: (number){ >=21 }
b6: (int){ 6 }
+ simplifyExpr: (_|_){
+ // [eval]
+ less1: (number){ <3 }
+ less2: (number){ <3 }
+ less3: (number){ <3 }
+ less4: (number){ <=3 }
+ less5: (number){ number }
+ less6: (number){ number }
+ gtr1: (number){ >3 }
+ gtr2: (number){ >3 }
+ gtr3: (number){ >3 }
+ gtr4: (number){ >=3 }
+ gtr5: (number){ number }
+ gtr6: (number){ number }
+ lg1: (number){ number }
+ lg2: (number){ number }
+ lg3: (number){ number }
+ lg4: (number){ number }
+ gl1: (number){ number }
+ gl2: (number){ number }
+ gl3: (number){ number }
+ gl4: (number){ number }
+ ne1: (int){ 3 }
+ ne2: (number){ >=3 }
+ ne3: (number){ >3 }
+ ne4: (number){ <=3 }
+ ne5: (number){ <3 }
+ s: (string){ string }
+ n: (number){ number }
+ i: (int){ int }
+ f: (float){ float }
+ b: (bytes){ bytes }
+ basic1: (int){ int }
+ basic2: (number){ number }
+ basic3: (string){ string }
+ basic4: (float){ float }
+ basic5: (bytes){ bytes }
+ bne1: (_|_){
+ // [incomplete] simplifyExpr.bne1: non-concrete value s for bound !=:
+ // ./in.cue:55:11
+ }
+ bne2: (_|_){
+ // [incomplete] simplifyExpr.bne2: non-concrete value n for bound !=:
+ // ./in.cue:56:11
+ }
+ bne3: (_|_){
+ // [incomplete] simplifyExpr.bne3: non-concrete value n for bound !=:
+ // ./in.cue:57:11
+ }
+ bne4: (_|_){
+ // [incomplete] simplifyExpr.bne4: non-concrete value i for bound !=:
+ // ./in.cue:58:11
+ }
+ bne5: (_|_){
+ // [incomplete] simplifyExpr.bne5: non-concrete value b for bound !=:
+ // ./in.cue:59:11
+ }
+ e1: (_|_){
+ // [incomplete] simplifyExpr.e1: non-concrete value =~"foo" for bound <:
+ // ./in.cue:61:9
+ }
+ e2: (_|_){
+ // [eval] simplifyExpr.e2: cannot use null for bound >:
+ // ./in.cue:62:11
+ }
+ }
}
-- out/compile --
--- in.cue
@@ -34,4 +159,48 @@
b4: (>=20 & >20)
b5: (>=21 & >20)
b6: ((int & >5) & <=6)
+ simplifyExpr: {
+ less1: <<3
+ less2: <<=3
+ less3: <=<3
+ less4: <=<=3
+ less5: <!=3
+ less6: <=!=3
+ gtr1: >>3
+ gtr2: >>=3
+ gtr3: >=>3
+ gtr4: >=>=3
+ gtr5: >!=3
+ gtr6: >=!=3
+ lg1: <>3
+ lg2: <>=3
+ lg3: <=>3
+ lg4: <=>=3
+ gl1: ><3
+ gl2: ><=3
+ gl3: >=<3
+ gl4: >=<=3
+ ne1: !=!=3
+ ne2: !=<3
+ ne3: !=<=3
+ ne4: !=>3
+ ne5: !=>=3
+ s: string
+ n: number
+ i: int
+ f: float
+ b: bytes
+ basic1: <〈0;i〉
+ basic2: >〈0;n〉
+ basic3: >=〈0;s〉
+ basic4: <=〈0;f〉
+ basic5: <=〈0;b〉
+ bne1: !=〈0;s〉
+ bne2: !=〈0;n〉
+ bne3: !=〈0;n〉
+ bne4: !=〈0;i〉
+ bne5: !=〈0;b〉
+ e1: <=~"foo"
+ e2: >null
+ }
}
diff --git a/cue/testdata/eval/errunifiy.txtar b/cue/testdata/eval/errunifiy.txtar
index bcb09aa..f818cbb 100644
--- a/cue/testdata/eval/errunifiy.txtar
+++ b/cue/testdata/eval/errunifiy.txtar
@@ -15,7 +15,10 @@
Result:
(_|_){
// [user]
- a: (string){ "t" }
+ a: (_|_){
+ // [incomplete] empty list in call to or:
+ // ./in.cue:1:4
+ }
b: (_|_){
// [user] explicit error (_|_ literal) in source:
// ./in.cue:4:4
diff --git a/cue/testdata/eval/incomplete.txtar b/cue/testdata/eval/incomplete.txtar
index 2fb31ea..daa5c6e 100644
--- a/cue/testdata/eval/incomplete.txtar
+++ b/cue/testdata/eval/incomplete.txtar
@@ -1,11 +1,12 @@
-These should all be an incomplete errors.
+These should all be incomplete errors.
-- in.cue --
s: string
e1: s + s
-e2: >"bar" & s // TODO
-e3: >s & "foo" // TODO
+e2: >"bar" & s // okay
+e3: >s & "foo" // okay
+e3b: >s
e4: >e1 & s
e5: <e5 & s
@@ -22,10 +23,13 @@
a: "golang/go:1.13.5"
}
+issue680: (>10 * 2) & 0
+
+
-- out/eval --
Errors:
permanentlyIncompleteOperands.a: invalid operand string ('+' requires concrete value):
- ./in.cue:18:8
+ ./in.cue:19:8
Result:
(_|_){
@@ -37,39 +41,44 @@
}
e2: (string){ >"bar" }
e3: (string){ "foo" }
+ e3b: (string){ string }
e4: (_|_){
// [incomplete] e1: non-concrete value string in operand to +:
// ./in.cue:3:5
}
e5: (_|_){
// [cycle] cycle error:
- // ./in.cue:8:6
+ // ./in.cue:9:6
}
E: (struct){
a: (_|_){
// [cycle] cycle error:
- // ./in.cue:11:6
+ // ./in.cue:12:6
}
b: (_|_){
// [cycle] cycle error:
- // ./in.cue:11:6
- // cycle error:
// ./in.cue:12:6
+ // cycle error:
+ // ./in.cue:13:6
}
c: (_|_){
// [cycle] cycle error:
- // ./in.cue:11:6
- // cycle error:
// ./in.cue:12:6
+ // cycle error:
+ // ./in.cue:13:6
}
}
permanentlyIncompleteOperands: (_|_){
// [eval]
a: (_|_){
// [eval] permanentlyIncompleteOperands.a: invalid operand string ('+' requires concrete value):
- // ./in.cue:18:8
+ // ./in.cue:19:8
}
}
+ issue680: (_|_){
+ // [incomplete] issue680: non-concrete value >10 in operand to *:
+ // ./in.cue:23:12
+ }
}
-- out/compile --
--- in.cue
@@ -78,6 +87,7 @@
e1: (〈0;s〉 + 〈0;s〉)
e2: (>"bar" & 〈0;s〉)
e3: (>〈0;s〉 & "foo")
+ e3b: >〈0;s〉
e4: (>〈0;e1〉 & 〈0;s〉)
e5: (<〈0;e5〉 & 〈0;s〉)
E: {
@@ -89,4 +99,5 @@
a: ((string + ":") + string)
a: "golang/go:1.13.5"
}
+ issue680: ((>10 * 2) & 0)
}
diff --git a/cue/testdata/export/009.txtar b/cue/testdata/export/009.txtar
index a6f04b7..7ab8e78 100644
--- a/cue/testdata/export/009.txtar
+++ b/cue/testdata/export/009.txtar
@@ -81,15 +81,21 @@
3: (int){ int }
4: (int){ int }
}
- b: (list){
+ b: (_|_){
+ // [incomplete] b: non-concrete value <=5 in operand to *:
+ // ./in.cue:4:5
0: (int){ 1 }
1: (int){ 2 }
}
- c: (list){
+ c: (_|_){
+ // [incomplete] c: non-concrete value >=3 & <=5 in operand to *:
+ // ./in.cue:6:5
0: (int){ 1 }
1: (int){ 2 }
}
- d: (list){
+ d: (_|_){
+ // [incomplete] d: non-concrete value >=2 in operand to *:
+ // ./in.cue:8:5
0: (int){ 1 }
1: (int){ 2 }
}
diff --git a/cue/testdata/export/010.txtar b/cue/testdata/export/010.txtar
index be462a4..bbaaec3 100644
--- a/cue/testdata/export/010.txtar
+++ b/cue/testdata/export/010.txtar
@@ -82,15 +82,21 @@
3: (int){ int }
4: (int){ int }
}
- b: (list){
+ b: (_|_){
+ // [incomplete] b: non-concrete value <=5 in operand to *:
+ // ./in.cue:4:5
0: (int){ 1 }
1: (int){ 2 }
}
- c: (list){
+ c: (_|_){
+ // [incomplete] c: non-concrete value >=3 & <=5 in operand to *:
+ // ./in.cue:6:5
0: (int){ 1 }
1: (int){ 2 }
}
- d: (list){
+ d: (_|_){
+ // [incomplete] d: non-concrete value >=2 in operand to *:
+ // ./in.cue:8:5
0: (int){ 1 }
1: (int){ 2 }
}
diff --git a/internal/core/adt/eval.go b/internal/core/adt/eval.go
index e42a930..d360086 100644
--- a/internal/core/adt/eval.go
+++ b/internal/core/adt/eval.go
@@ -308,6 +308,7 @@
n.expandDisjuncts(disState, n, maybeDefault, false)
for _, d := range n.disjuncts {
+ d.finalizeIncomplete()
d.free()
}
@@ -351,6 +352,10 @@
// to not confuse memory management.
v.state = n
+ // We don't do this in postDisjuncts, as it should only be done after
+ // completing all disjunctions.
+ n.finalizeIncomplete()
+
if state != Finalized {
return
}
@@ -371,6 +376,17 @@
}
}
+// finalizeIncomplete collects all uncompleted expressions and adds them as
+// errors. As disjuncts are always evaluated with Finalized, care should be
+// taken to only call this after all disjunctions in a path have been completed.
+func (n *nodeContext) finalizeIncomplete() {
+ if !n.done() {
+ if err := n.incompleteErrors(); err != nil {
+ n.node.BaseValue = err
+ }
+ }
+}
+
func (n *nodeContext) doNotify() {
if n.errs == nil || len(n.notify) == 0 {
return
@@ -389,11 +405,6 @@
n.notify = n.notify[:0]
}
-func isStruct(v *Vertex) bool {
- _, ok := v.BaseValue.(*StructMarker)
- return ok
-}
-
func (n *nodeContext) postDisjunct(state VertexStatus) {
ctx := n.ctx
@@ -426,6 +437,16 @@
n.node.BaseValue = nil
}
}
+ // TODO: this ideally should be done here. However, doing so causes
+ // a somewhat more aggressive cutoff in disjunction cycles, which cause
+ // some incompatibilities. Fix in another CL.
+ //
+ // else if !n.done() {
+ // n.expandOne()
+ // if err := n.incompleteErrors(); err != nil {
+ // n.node.BaseValue = err
+ // }
+ // }
// We are no longer evaluating.
// n.node.UpdateStatus(Partial)
diff --git a/internal/core/adt/expr.go b/internal/core/adt/expr.go
index 00a871e..4551069 100644
--- a/internal/core/adt/expr.go
+++ b/internal/core/adt/expr.go
@@ -452,15 +452,110 @@
}
func (x *BoundExpr) evaluate(ctx *OpContext) Value {
+ v := ctx.value(x.Expr)
+ if isError(v) {
+ return v
+ }
+
+ switch k := v.Kind(); k {
+ case IntKind, FloatKind, NumKind, StringKind, BytesKind:
+ case NullKind:
+ if x.Op != NotEqualOp {
+ err := ctx.NewPosf(pos(x.Expr),
+ "cannot use null for bound %s", x.Op)
+ return &Bottom{Err: err}
+ }
+ default:
+ mask := IntKind | FloatKind | NumKind | StringKind | BytesKind
+ if x.Op == NotEqualOp {
+ mask |= NullKind
+ }
+ if k&mask != 0 {
+ ctx.addErrf(IncompleteError, ctx.pos(),
+ "non-concrete value %s for bound %s", ctx.Str(x.Expr), x.Op)
+ return nil
+ }
+ err := ctx.NewPosf(pos(x.Expr),
+ "invalid value %s (type %s) for bound %s",
+ ctx.Str(v), k, x.Op)
+ return &Bottom{Err: err}
+ }
+
if v, ok := x.Expr.(Value); ok {
if v == nil || v.Concreteness() > Concrete {
return ctx.NewErrf("bound has fixed non-concrete value")
}
return &BoundValue{x.Src, x.Op, v}
}
- v := ctx.value(x.Expr)
- if isError(v) {
- return v
+
+ // This simplifies boundary expressions. It is an alternative to an
+ // evaluation strategy that makes nodes increasingly more specific.
+ //
+ // For instance, a completely different implementation would be to allow
+ // the precense of a concrete value to ignore incomplete errors.
+ //
+ // TODO: consider an alternative approach.
+ switch y := v.(type) {
+ case *BoundValue:
+ switch {
+ case y.Op == NotEqualOp:
+ switch x.Op {
+ case LessEqualOp, LessThanOp, GreaterEqualOp, GreaterThanOp:
+ // <(!=3) => number
+ // Smaller than an arbitrarily large number is any number.
+ return &BasicType{K: y.Kind()}
+ case NotEqualOp:
+ // !=(!=3) ==> 3
+ // Not a value that is anything but a given value is that
+ // given value.
+ return y.Value
+ }
+
+ case x.Op == NotEqualOp:
+ // Invert if applicable.
+ switch y.Op {
+ case LessEqualOp:
+ return &BoundValue{x.Src, GreaterThanOp, y.Value}
+ case LessThanOp:
+ return &BoundValue{x.Src, GreaterEqualOp, y.Value}
+ case GreaterEqualOp:
+ return &BoundValue{x.Src, LessThanOp, y.Value}
+ case GreaterThanOp:
+ return &BoundValue{x.Src, LessEqualOp, y.Value}
+ }
+
+ case (x.Op == LessThanOp || x.Op == LessEqualOp) &&
+ (y.Op == GreaterThanOp || y.Op == GreaterEqualOp),
+ (x.Op == GreaterThanOp || x.Op == GreaterEqualOp) &&
+ (y.Op == LessThanOp || y.Op == LessEqualOp):
+ // <(>=3)
+ // Something smaller than an arbitrarily large number is any number.
+ return &BasicType{K: y.Kind()}
+
+ case x.Op == LessThanOp &&
+ (y.Op == LessEqualOp || y.Op == LessThanOp),
+ x.Op == GreaterThanOp &&
+ (y.Op == GreaterEqualOp || y.Op == GreaterThanOp):
+ // <(<=x) => <x
+ // <(<x) => <x
+ // Less than something that is less or equal to x is less than x.
+ return &BoundValue{x.Src, x.Op, y.Value}
+
+ case x.Op == LessEqualOp &&
+ (y.Op == LessEqualOp || y.Op == LessThanOp),
+ x.Op == GreaterEqualOp &&
+ (y.Op == GreaterEqualOp || y.Op == GreaterThanOp):
+ // <=(<x) => <x
+ // <=(<=x) => <=x
+ // Less or equal than something that is less than x is less than x.
+ return y
+ }
+
+ case *BasicType:
+ switch x.Op {
+ case LessEqualOp, LessThanOp, GreaterEqualOp, GreaterThanOp:
+ return y
+ }
}
if v.Concreteness() > Concrete {
ctx.addErrf(IncompleteError, ctx.pos(),