internal/core/eval: breadth-first disjunction resolution

This CL makes disjunction resolution polynomial instead
of exponential. More precisely, running time for a field
reduces from
   O(m)*O(c^d)
to
   O(m)+O(d*c),
where m is the number of non-disjunction conjuncts for a field,
d is the number of disjunctions, and c the maximum number of
disjuncts per disjunction.

So overall, this reduces the running time by improving the big O
runtime complexity as well as reducing the constant.

This change also fixes numerous issues:
- results properly reflect disjunction where this was previously
  not the case in some instances
- error messages that were previously dropped in some cases are
  now retained.
- this also means that the "empty disjunction" error message now
  properly reflect the error type, whereas previously this was
  sometimes lost.
- embedding of disjunctions with struct and non-struct types
  are now properly resolved.
- the scalar hack has been replaced with a more principled
  "non-default" erasure.

Fixes #565
 - was already at 10s for alpha6 on 16' Macbook Pro, but now
   down to 1.2s. (Orignally >10m).

Fixes #547

Change-Id: Ib050ba3f15e481391e4a60e6d3ab7c3f512643fc
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/8045
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/testdata/benchmarks/disjunction.txtar b/cue/testdata/benchmarks/disjunction.txtar
new file mode 100644
index 0000000..7124097
--- /dev/null
+++ b/cue/testdata/benchmarks/disjunction.txtar
@@ -0,0 +1,146 @@
+// Triggering recomputation of disjunctions on each branch results
+// in exponential run time. Ensure this does not happen.
+
+-- in.cue --
+x: a0: {}
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+x: [_]: f: *1 | int
+
+
+x: a0: {}
+-- out/compile --
+--- in.cue
+{
+  x: {
+    a0: {}
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    [_]: {
+      f: (*1|int)
+    }
+  }
+  x: {
+    a0: {}
+  }
+}
+-- out/eval --
+(struct){
+  x: (struct){
+    a0: (struct){
+      f: (int){ |(*(int){ 1 }, (int){ int }) }
+    }
+  }
+}
diff --git a/cue/testdata/comprehensions/errors.txtar b/cue/testdata/comprehensions/errors.txtar
index 483db98..85fa787 100644
--- a/cue/testdata/comprehensions/errors.txtar
+++ b/cue/testdata/comprehensions/errors.txtar
@@ -32,7 +32,9 @@
   // [eval]
   circularIf: (struct){
     #list: (#struct){
-      tail: (null){ null }
+      tail: ((null|struct)){ |(*(null){ null }, (#struct){
+          tail: (null){ null }
+        }) }
     }
   }
   circularFor: (_|_){
@@ -44,7 +46,7 @@
     }
   }
   userError: (_|_){
-    // [incomplete] userError.a: empty disjunction:
+    // [incomplete] userError: non-concrete value string in operand to !=:
     //     ./in.cue:21:8
     a: (string){ string }
   }
diff --git a/cue/testdata/comprehensions/issue436.txtar b/cue/testdata/comprehensions/issue436.txtar
index c206191..fcd8b64 100644
--- a/cue/testdata/comprehensions/issue436.txtar
+++ b/cue/testdata/comprehensions/issue436.txtar
@@ -26,7 +26,7 @@
     result: (string){ string }
   }
   a: (#struct){
-    val: (string){ "default" }
+    val: (string){ |(*(string){ "default" }, (string){ string }) }
     result: (string){ |(*(string){ "not-matched" }, (string){ string }) }
   }
   match: (#struct){
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 ea59006..89f4dd7 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
@@ -159,6 +159,10 @@
 }
 -- out/eval --
 Errors:
+xe1: 2 errors in empty disjunction:
+xe1: conflicting values 8 and 9:
+    ./in.cue:43:12
+    ./in.cue:48:6
 xe3: conflicting values 7 and 6:
     ./in.cue:45:6
     ./in.cue:45:10
@@ -191,7 +195,13 @@
   xd4: (int){ 9 }
   xd5: (int){ 10 }
   xe1: (_|_){
-    // [incomplete] xe1: empty disjunction
+    // [eval] xe1: 2 errors in empty disjunction:
+    // xe1: conflicting values 8 and 9:
+    //     ./in.cue:43:12
+    //     ./in.cue:48:6
+    // xe3: conflicting values 7 and 6:
+    //     ./in.cue:45:6
+    //     ./in.cue:45:10
   }
   xe2: (_|_){
     // [eval] xe3: conflicting values 7 and 6:
diff --git a/cue/testdata/cycle/052_resolved_self-reference_cycles_with_disjunction_with_defaults.txtar b/cue/testdata/cycle/052_resolved_self-reference_cycles_with_disjunction_with_defaults.txtar
index 46c6286..773bdca 100644
--- a/cue/testdata/cycle/052_resolved_self-reference_cycles_with_disjunction_with_defaults.txtar
+++ b/cue/testdata/cycle/052_resolved_self-reference_cycles_with_disjunction_with_defaults.txtar
@@ -129,6 +129,10 @@
 }
 -- out/eval --
 Errors:
+xe1: 2 errors in empty disjunction:
+xe1: conflicting values 8 and 9:
+    ./in.cue:36:14
+    ./in.cue:41:6
 xe3: conflicting values 7 and 6:
     ./in.cue:38:6
     ./in.cue:38:10
@@ -142,7 +146,7 @@
   xa4: (int){ 10 }
   xb1: (int){ 8 }
   xb2: (int){ 8 }
-  xb3: (int){ 6 }
+  xb3: (int){ |(*(int){ 6 }, (int){ 9 }) }
   xb4: (_|_){
     // [cycle] cycle error:
     //     ./in.cue:14:6
@@ -158,7 +162,13 @@
   xd4: (int){ 9 }
   xd5: (int){ 10 }
   xe1: (_|_){
-    // [incomplete] xe1: empty disjunction
+    // [eval] xe1: 2 errors in empty disjunction:
+    // xe1: conflicting values 8 and 9:
+    //     ./in.cue:36:14
+    //     ./in.cue:41:6
+    // xe3: conflicting values 7 and 6:
+    //     ./in.cue:38:6
+    //     ./in.cue:38:10
   }
   xe2: (_|_){
     // [eval] xe3: conflicting values 7 and 6:
diff --git a/cue/testdata/cycle/issue429.txtar b/cue/testdata/cycle/issue429.txtar
index 9a816c9..876994b 100644
--- a/cue/testdata/cycle/issue429.txtar
+++ b/cue/testdata/cycle/issue429.txtar
@@ -52,8 +52,22 @@
 
 -- out/eval --
 Errors:
-er3.min: 1 errors in empty disjunction:
-es3.max: 1 errors in empty disjunction:
+er3.min: 2 errors in empty disjunction:
+er3.min: conflicting values 1 and 5:
+    ./in.cue:28:11
+    ./in.cue:43:6
+    ./in.cue:44:10
+es3.max: 3 errors in empty disjunction:
+es3.max: conflicting values 1 and 5:
+    ./in.cue:4:19
+    ./in.cue:5:18
+    ./in.cue:20:6
+    ./in.cue:22:10
+es3.max: conflicting values 10 and 5:
+    ./in.cue:5:18
+    ./in.cue:20:6
+    ./in.cue:21:10
+    ./in.cue:22:10
 es3.max: invalid value 5 (out of bound >10):
     ./in.cue:5:10
     ./in.cue:22:10
@@ -94,7 +108,17 @@
     res: (int){ |(*(int){ 0 }, (int){ &(>=0, int) }) }
     min: (int){ 10 }
     max: (_|_){
-      // [eval] es3.max: 1 errors in empty disjunction:
+      // [eval] es3.max: 3 errors in empty disjunction:
+      // es3.max: conflicting values 1 and 5:
+      //     ./in.cue:4:19
+      //     ./in.cue:5:18
+      //     ./in.cue:20:6
+      //     ./in.cue:22:10
+      // es3.max: conflicting values 10 and 5:
+      //     ./in.cue:5:18
+      //     ./in.cue:20:6
+      //     ./in.cue:21:10
+      //     ./in.cue:22:10
       // es3.max: invalid value 5 (out of bound >10):
       //     ./in.cue:5:10
       //     ./in.cue:22:10
@@ -123,13 +147,21 @@
   er3: (_|_){
     // [eval]
     min: (_|_){
-      // [eval] er3.min: 1 errors in empty disjunction:
+      // [eval] er3.min: 2 errors in empty disjunction:
+      // er3.min: conflicting values 1 and 5:
+      //     ./in.cue:28:11
+      //     ./in.cue:43:6
+      //     ./in.cue:44:10
       // er3.min: invalid value 5 (out of bound <5):
       //     ./in.cue:29:10
       //     ./in.cue:44:10
     }
     max: (_|_){
-      // [eval] er3.min: 1 errors in empty disjunction:
+      // [eval] er3.min: 2 errors in empty disjunction:
+      // er3.min: conflicting values 1 and 5:
+      //     ./in.cue:28:11
+      //     ./in.cue:43:6
+      //     ./in.cue:44:10
       // er3.min: invalid value 5 (out of bound <5):
       //     ./in.cue:29:10
       //     ./in.cue:44:10
diff --git a/cue/testdata/cycle/structural.txtar b/cue/testdata/cycle/structural.txtar
index 924e72b..4a19c49 100644
--- a/cue/testdata/cycle/structural.txtar
+++ b/cue/testdata/cycle/structural.txtar
@@ -409,7 +409,6 @@
 c1.a.c.c: structural cycle
 d1.a.b.c.d.t: structural cycle
 d1.r: structural cycle
-d2.a.b.c.d.t: structural cycle
 e1.a.c: structural cycle
 e1.b.c: structural cycle
 e2.a.c: structural cycle
@@ -469,11 +468,9 @@
 p5.#T.a.0.link: structural cycle
 p6.#U.#T.a.0.link: structural cycle
 z1.z.g.h: structural cycle
-b4.x.y.0: structural cycle:
+b4.x.y.0.0: structural cycle:
     ./in.cue:53:8
-d2.a.b.c.d.t: structural cycle:
-    ./in.cue:254:8
-d2.r: structural cycle:
+d2.r.c.d.t: structural cycle:
     ./in.cue:254:8
 0: structural cycle:
     ./in.cue:264:19
@@ -564,7 +561,7 @@
   b4: (_|_){
     // [structural cycle]
     b: (_|_){
-      // [structural cycle] b4.x.y.0: structural cycle:
+      // [structural cycle] b4.x.y.0.0: structural cycle:
       //     ./in.cue:53:8
       0: (int){ 1 }
     }
@@ -589,7 +586,9 @@
     x: (struct){
       y: (struct){
         a: (#list){
-          0: (int){ int }
+          0: ((int|list)){ |((#list){
+              0: (int){ int }
+            }, (int){ int }) }
         }
       }
     }
@@ -699,13 +698,19 @@
   }
   b11: (struct){
     #list: (#struct){
-      tail: (null){ null }
+      tail: ((null|struct)){ |(*(null){ null }, (#struct){
+          tail: (null){ null }
+        }) }
     }
   }
   b12: (struct){
     #list: (#struct){
       value: (int){ int }
-      tail: (null){ null }
+      tail: ((null|struct)){ |(*(null){ null }, (#struct){
+          value: (int){ int }
+          tail: (null){ null }
+          sum: (int){ int }
+        }) }
       sum: (int){ int }
     }
     list1: (#struct){
@@ -1048,18 +1053,28 @@
   d2: (_|_){
     // [structural cycle]
     x: (_|_){
-      // [structural cycle] d2.a.b.c.d.t: structural cycle:
+      // [structural cycle] d2.r.c.d.t: structural cycle:
       //     ./in.cue:254:8
     }
     r: (_|_){
-      // [structural cycle] d2.r: structural cycle:
-      //     ./in.cue:254:8
-      c: (_|_){// {
-        //   d: {
-        //     h: int
-        //     t: 〈4;r〉
-        //   }
-        // }
+      // [structural cycle]
+      c: (_|_){
+        // [structural cycle]
+        d: (_|_){
+          // [structural cycle]
+          h: (int){ int }
+          t: (_|_){
+            // [structural cycle] d2.r.c.d.t: structural cycle:
+            //     ./in.cue:254:8
+            c: (_|_){// {
+              //   d: {
+              //     h: int
+              //     t: 〈4;r〉
+              //   }
+              // }
+            }
+          }
+        }
       }
     }
     a: (_|_){
@@ -1072,14 +1087,7 @@
             // [structural cycle]
             h: (int){ int }
             t: (_|_){
-              // [structural cycle] d2.a.b.c.d.t: structural cycle
-              c: (_|_){// {
-                //   d: {
-                //     h: int
-                //     t: 〈4;r〉
-                //   }
-                // }
-              }
+              // [structural cycle]
             }
           }
         }
@@ -1104,7 +1112,7 @@
         // [structural cycle] 0: structural cycle:
         //     ./in.cue:264:19
       }
-      i: (int){ 1 }
+      i: (int){ |(*(int){ 1 }, (int){ int }) }
     }
     x: (_|_){
       // [structural cycle] 0: structural cycle:
diff --git a/cue/testdata/disjunctions/embed.txtar b/cue/testdata/disjunctions/embed.txtar
new file mode 100644
index 0000000..72de3c1
--- /dev/null
+++ b/cue/testdata/disjunctions/embed.txtar
@@ -0,0 +1,131 @@
+-- in.cue --
+// Given the existence of this field, embedDefault is a struct. This
+// means that embedding this conjunction into `embedDefault` itself should
+// make it resolve to `{a: 2}`.
+
+default: {
+    y: *1 | {a: 2}
+    y
+}
+
+unambiguous: {
+    y: 1 | {a: 2}
+    y
+}
+
+forDefault: {
+    y: *1 | {a: 2}
+    for x in [1] {y}
+}
+
+// Carry over default to first disjunct.
+openDefault: {
+    #y: *1 | {a: 2}
+    #y
+}
+
+openAmbiguous: {
+    #y: 1 | {a: 2}
+    #y
+}
+
+forceStruct: {
+    #y: 1 | {a: 2}
+    #y
+    {}
+}
+
+-- out/eval --
+(struct){
+  default: (struct){
+    y: ((int|struct)){ |(*(int){ 1 }, (struct){
+        a: (int){ 2 }
+      }) }
+    a: (int){ 2 }
+  }
+  unambiguous: (struct){
+    y: ((int|struct)){ |((int){ 1 }, (struct){
+        a: (int){ 2 }
+      }) }
+    a: (int){ 2 }
+  }
+  forDefault: (struct){
+    y: ((int|struct)){ |(*(int){ 1 }, (struct){
+        a: (int){ 2 }
+      }) }
+    a: (int){ 2 }
+  }
+  openDefault: ((int|struct)){ |(*(int){
+      1
+      #y: ((int|struct)){ |(*(int){ 1 }, (#struct){
+          a: (int){ 2 }
+        }) }
+    }, (#struct){
+      #y: ((int|struct)){ |(*(int){ 1 }, (#struct){
+          a: (int){ 2 }
+        }) }
+      a: (int){ 2 }
+    }) }
+  openAmbiguous: ((int|struct)){ |((int){
+      1
+      #y: ((int|struct)){ |((int){ 1 }, (#struct){
+          a: (int){ 2 }
+        }) }
+    }, (#struct){
+      #y: ((int|struct)){ |((int){ 1 }, (#struct){
+          a: (int){ 2 }
+        }) }
+      a: (int){ 2 }
+    }) }
+  forceStruct: (#struct){
+    #y: ((int|struct)){ |((int){ 1 }, (#struct){
+        a: (int){ 2 }
+      }) }
+    a: (int){ 2 }
+  }
+}
+-- out/compile --
+--- in.cue
+{
+  default: {
+    y: (*1|{
+      a: 2
+    })
+    〈0;y〉
+  }
+  unambiguous: {
+    y: (1|{
+      a: 2
+    })
+    〈0;y〉
+  }
+  forDefault: {
+    y: (*1|{
+      a: 2
+    })
+    for _, x in [
+      1,
+    ] {
+      〈2;y〉
+    }
+  }
+  openDefault: {
+    #y: (*1|{
+      a: 2
+    })
+    〈0;#y〉
+  }
+  openAmbiguous: {
+    #y: (1|{
+      a: 2
+    })
+    〈0;#y〉
+  }
+  forceStruct: {
+    #y: (1|{
+      a: 2
+    })
+    〈0;#y〉
+    {}
+  }
+}
diff --git a/cue/testdata/disjunctions/errors.txtar b/cue/testdata/disjunctions/errors.txtar
index f4f9365..983c466 100644
--- a/cue/testdata/disjunctions/errors.txtar
+++ b/cue/testdata/disjunctions/errors.txtar
@@ -124,7 +124,7 @@
     }
   }
   explicitDefaultError: (_|_){
-    // [incomplete] explicitDefaultError.a: empty disjunction:
+    // [incomplete] explicitDefaultError: non-concrete value string in operand to !=:
     //     ./in.cue:30:8
     a: (string){ string }
   }
diff --git a/cue/testdata/disjunctions/specdeviation.txtar b/cue/testdata/disjunctions/specdeviation.txtar
index 180bf3b..4ebf0d4 100644
--- a/cue/testdata/disjunctions/specdeviation.txtar
+++ b/cue/testdata/disjunctions/specdeviation.txtar
@@ -40,12 +40,12 @@
   p: (int){ |(*(int){ 2 }, (int){ int }) }
   s1: (#struct){
     max: (number){ |(*(int){ 5 }, (number){ >5 }) }
-    res: (int){ 0 }
+    res: (int){ |(*(int){ 0 }, (int){ &(>=0, int) }) }
     min: (int){ 5 }
   }
   #Size: (#struct){
     max: (number){ |(*(int){ 1 }, (number){ >0 }, (number){ >1 }) }
-    res: (int){ 0 }
+    res: (int){ |(*(int){ 0 }, (int){ &(>=0, int) }) }
     min: (number){ |(*(int){ 1 }, (number){ >0 }) }
   }
 }
diff --git a/cue/testdata/eval/disjunctions.txtar b/cue/testdata/eval/disjunctions.txtar
index 45a4c19..fa6adb6 100644
--- a/cue/testdata/eval/disjunctions.txtar
+++ b/cue/testdata/eval/disjunctions.txtar
@@ -51,6 +51,50 @@
   b: {} & (int | {c: 1})
 }
 
+t10: {
+    schema: test
+    schema: string | {name: string}
+    #A: {string | {name: string}}
+
+    test: name: "Test"
+    test: #A
+}
+
+t10: {
+    schema: string | {name: string}
+    schema: test
+    #A: {string | {name: string}}
+
+    test: name: "Test"
+    test: #A
+}
+
+t10: {
+    #A: {string | {name: string}}
+
+    test: name: "Test"
+    test: #A
+
+    schema: string | {name: string}
+    schema: test
+}
+
+t11: {
+    a: #A
+    a: b
+
+    b: #A & ["b"]
+    #A: ["a" | "b"] | {}
+}
+
+t11: {
+    b: #A & ["b"]
+    #A: ["a" | "b"] | {}
+
+    a: b
+    a: #A
+}
+
 
 d100: {
   // Should we allow a selector to imply a struct or list? Would be convenient.
@@ -167,13 +211,36 @@
       c: (int){ 1 }
     }
   }
+  t10: (struct){
+    schema: (#struct){
+      name: (string){ "Test" }
+    }
+    #A: ((string|struct)){ |((string){ string }, (#struct){
+        name: (string){ string }
+      }) }
+    test: (#struct){
+      name: (string){ "Test" }
+    }
+  }
+  t11: (struct){
+    a: (#list){
+      0: (string){ "b" }
+    }
+    b: (#list){
+      0: (string){ "b" }
+    }
+    #A: ((list|struct)){ |((#list){
+        0: (string){ |((string){ "a" }, (string){ "b" }) }
+      }, (#struct){
+      }) }
+  }
   d100: (struct){
     i: ((null|struct)){ |((null){ null }, (struct){
         bar: (int){ 2 }
       }) }
     j: (_|_){
       // [incomplete] d100.j: unresolved disjunction null | {bar:2} (type (null|struct)):
-      //     ./in.cue:58:6
+      //     ./in.cue:102:6
     }
   }
 }
@@ -261,6 +328,71 @@
       c: 1
     }))
   }
+  t10: {
+    schema: 〈0;test〉
+    schema: (string|{
+      name: string
+    })
+    #A: {
+      (string|{
+        name: string
+      })
+    }
+    test: {
+      name: "Test"
+    }
+    test: 〈0;#A〉
+  }
+  t10: {
+    schema: (string|{
+      name: string
+    })
+    schema: 〈0;test〉
+    #A: {
+      (string|{
+        name: string
+      })
+    }
+    test: {
+      name: "Test"
+    }
+    test: 〈0;#A〉
+  }
+  t10: {
+    #A: {
+      (string|{
+        name: string
+      })
+    }
+    test: {
+      name: "Test"
+    }
+    test: 〈0;#A〉
+    schema: (string|{
+      name: string
+    })
+    schema: 〈0;test〉
+  }
+  t11: {
+    a: 〈0;#A〉
+    a: 〈0;b〉
+    b: (〈0;#A〉 & [
+      "b",
+    ])
+    #A: ([
+      ("a"|"b"),
+    ]|{})
+  }
+  t11: {
+    b: (〈0;#A〉 & [
+      "b",
+    ])
+    #A: ([
+      ("a"|"b"),
+    ]|{})
+    a: 〈0;b〉
+    a: 〈0;#A〉
+  }
   d100: {
     i: (null|{
       bar: 2
diff --git a/cue/testdata/fulleval/000_detect_conflicting_value.txtar b/cue/testdata/fulleval/000_detect_conflicting_value.txtar
index a4a9787..d79a7e3 100644
--- a/cue/testdata/fulleval/000_detect_conflicting_value.txtar
+++ b/cue/testdata/fulleval/000_detect_conflicting_value.txtar
@@ -1,5 +1,3 @@
-# DO NOT EDIT; generated by go run testdata/gen.go
-#
 #name: detect conflicting value
 #evalFull
 -- in.cue --
@@ -16,8 +14,25 @@
   a: (7080|int)
 }
 -- out/eval --
-(struct){
+Errors:
+a: 2 errors in empty disjunction:
+a: conflicting values 8000.9 and 7080 (mismatched types float and int):
+    ./in.cue:1:4
+    ./in.cue:2:4
+a: conflicting values 8000.9 and int (mismatched types float and int):
+    ./in.cue:1:4
+    ./in.cue:2:11
+
+Result:
+(_|_){
+  // [eval]
   a: (_|_){
-    // [incomplete] a: empty disjunction
+    // [eval] a: 2 errors in empty disjunction:
+    // a: conflicting values 8000.9 and 7080 (mismatched types float and int):
+    //     ./in.cue:1:4
+    //     ./in.cue:2:4
+    // a: conflicting values 8000.9 and int (mismatched types float and int):
+    //     ./in.cue:1:4
+    //     ./in.cue:2:11
   }
 }
diff --git a/cue/testdata/fulleval/041.txtar b/cue/testdata/fulleval/041.txtar
index 35b4e1c..062e156 100644
--- a/cue/testdata/fulleval/041.txtar
+++ b/cue/testdata/fulleval/041.txtar
@@ -37,7 +37,7 @@
 -- out/eval --
 (struct){
   t: (struct){
-    #ok: (bool){ true }
+    #ok: (bool){ |(*(bool){ true }, (bool){ bool }) }
     x: (int){ int }
   }
   s: (struct){
diff --git a/cue/testdata/fulleval/054_issue312.txtar b/cue/testdata/fulleval/054_issue312.txtar
index 20833cf..5269842 100644
--- a/cue/testdata/fulleval/054_issue312.txtar
+++ b/cue/testdata/fulleval/054_issue312.txtar
@@ -1,5 +1,3 @@
-# DO NOT EDIT; generated by go run testdata/gen.go
-#
 #name: issue312
 #evalFull
 -- in.cue --
@@ -37,4 +35,5 @@
   y: ((int|struct)){ |(*(int){ 1 }, (struct){
       a: (int){ 2 }
     }) }
+  a: (int){ 2 }
 }
diff --git a/cue/testdata/resolve/048_builtins.txtar b/cue/testdata/resolve/048_builtins.txtar
index 4a24b24..cc7e94b 100644
--- a/cue/testdata/resolve/048_builtins.txtar
+++ b/cue/testdata/resolve/048_builtins.txtar
@@ -46,6 +46,17 @@
 <0>{a1: <1>{a: (=~"oo" & =~"fo"), b: =~"oo", c: =~"fo"}, a2: <2>{a: "foo", b: =~"oo", c: =~"fo"}, a3: <3>{a: _|_((=~"oo" & "bar"):invalid value "bar" (does not match =~"oo")), b: =~"oo", c: =~"fo"}, o1: <4>{a: string, b: string, c: "bar"}, o2: <5>{a: "foo", b: string, c: "bar"}, o3: <6>{a: _|_(("baz" & "foo"):empty disjunction: conflicting values "baz" and "foo";("bar" & "foo"):empty disjunction: conflicting values "bar" and "foo"), b: "baz", c: "bar"}}
 -- out/eval --
 Errors:
+o3.a: 2 errors in empty disjunction:
+o3.a: conflicting values "bar" and "foo":
+    ./in.cue:10:12
+    ./in.cue:12:5
+    ./in.cue:15:5
+    ./in.cue:15:14
+o3.a: conflicting values "baz" and "foo":
+    ./in.cue:10:9
+    ./in.cue:15:5
+    ./in.cue:15:14
+    ./in.cue:15:24
 a3.a: invalid value "bar" (out of bound =~"oo"):
     ./in.cue:3:5
 a3.a: invalid value "bar" (out of bound =~"fo"):
@@ -85,9 +96,20 @@
     b: (string){ string }
     c: (string){ "bar" }
   }
-  o3: (struct){
+  o3: (_|_){
+    // [eval]
     a: (_|_){
-      // [incomplete] o3.a: empty disjunction
+      // [eval] o3.a: 2 errors in empty disjunction:
+      // o3.a: conflicting values "bar" and "foo":
+      //     ./in.cue:10:12
+      //     ./in.cue:12:5
+      //     ./in.cue:15:5
+      //     ./in.cue:15:14
+      // o3.a: conflicting values "baz" and "foo":
+      //     ./in.cue:10:9
+      //     ./in.cue:15:5
+      //     ./in.cue:15:14
+      //     ./in.cue:15:24
     }
     b: (string){ "baz" }
     c: (string){ "bar" }
diff --git a/internal/core/eval/disjunct.go b/internal/core/eval/disjunct.go
index b02d425..b2110aa 100644
--- a/internal/core/eval/disjunct.go
+++ b/internal/core/eval/disjunct.go
@@ -98,7 +98,7 @@
 }
 
 func (n *nodeContext) addDisjunction(env *adt.Environment, x *adt.DisjunctionExpr, cloneID adt.CloseInfo) {
-	a := []disjunct{}
+	a := make([]disjunct, 0, len(x.Values))
 
 	numDefaults := 0
 	for _, v := range x.Values {
@@ -118,7 +118,7 @@
 }
 
 func (n *nodeContext) addDisjunctionValue(env *adt.Environment, x *adt.Disjunction, cloneID adt.CloseInfo) {
-	a := []disjunct{}
+	a := make([]disjunct, 0, len(x.Values))
 
 	for i, v := range x.Values {
 		a = append(a, disjunct{v, i < x.NumDefaults})
@@ -128,182 +128,183 @@
 		envDisjunct{env, a, x.NumDefaults, cloneID})
 }
 
-func (n *nodeContext) updateResult(state adt.VertexStatus) {
-	n.postDisjunct(state)
+func (n *nodeContext) expandDisjuncts(
+	state adt.VertexStatus,
+	parent *nodeContext,
+	m defaultMode,
+	recursive bool) {
 
-	if n.hasErr() {
-		x := n.node
-		err, ok := x.BaseValue.(*adt.Bottom)
-		if !ok {
-			err = n.getErr()
-		}
-		if err == nil {
-			// TODO(disjuncts): Is this always correct? Especially for partial
-			// evaluation it is okay for child errors to have incomplete errors.
-			// Perhaps introduce an Err() method.
-			err = x.ChildErrors
-		}
-		if err != nil {
-			n.disjunctErrs = append(n.disjunctErrs, err)
-		}
-		return
+	n.eval.stats.DisjunctCount++
+
+	for n.expandOne() {
 	}
 
-	n.touched = true
-	d := &n.nodeShared.disjunct
+	// save node to snapShot in nodeContex
+	// save nodeContext.
 
-	result := *n.node
-	if result.BaseValue == nil {
-		result.BaseValue = n.getValidators()
-	}
-
-	for _, v := range d.Values {
-		if adt.Equal(n.ctx, v, &result) {
-			return
-		}
-	}
-
-	p := &result
-	d.Values = append(d.Values, p)
-
-	if n.done() {
-		n.nodeShared.isDone = true
-	}
-
-	if n.defaultMode == isDefault {
-		// Keep defaults sorted first.
-		i := d.NumDefaults
-		j := i + 1
-		copy(d.Values[j:], d.Values[i:])
-		d.Values[i] = p
-		d.NumDefaults = j
+	if recursive || len(n.disjunctions) > 0 {
+		n.snapshot = snapshotVertex(*n.node)
+	} else {
+		n.snapshot = *n.node
 	}
 
 	switch {
-	case !n.nodeShared.hasResult():
-
-	case n.nodeShared.isDefault() && n.defaultMode != isDefault:
-		return
-
-	case !n.nodeShared.isDefault() && n.defaultMode == isDefault:
-
-	default:
-		return // n.defaultMode == isDefault
-	}
-
-	n.nodeShared.setResult(n.node)
-
-	return
-}
-
-func (n *nodeContext) processDisjuncts(state adt.VertexStatus) {
-	n.processDisjunct(state, 0, len(n.disjunctions))
-
-	if n.nodeShared.hasResult() {
-		return // found something
-	}
-
-	if len(n.disjunctions) > 0 {
-		code := adt.IncompleteError
-
-		if len(n.disjunctErrs) > 0 {
-			code = adt.EvalError
-			for _, c := range n.disjunctErrs {
-				if c.Code > code {
-					code = c.Code
-				}
-			}
-		}
-
-		b := &adt.Bottom{
-			Code: code,
-			Err:  n.disjunctError(),
-		}
-		n.node.SetValue(n.ctx, adt.Finalized, b)
-	}
-}
-
-// TODO: move state to nodeShared.
-func (n *nodeContext) processDisjunct(state adt.VertexStatus, k, sub int) {
-	isSub := false
-	var d envDisjunct
-	switch {
-	case sub < len(n.disjunctions):
-		d = n.disjunctions[sub]
-		sub++
-		isSub = true
-
-	case k < len(n.disjunctions):
-		d = n.disjunctions[k]
-		k++
-
-	default:
-		n.updateResult(state)
-		return
-	}
-
-	// save current state of node and nodeContext
-	nSaved := snapshotVertex(n.node)
-	saved := *n
-
-	for i, v := range d.values {
-		n.eval.stats.DisjunctCount++
-
-		if i > 0 {
-			*n = saved
-			*(n.node) = nSaved
-			// restore state
-		}
-
-		// TODO: HACK ALERT: we ignore the default tags of the subexpression
-		// if we already have a scalar value and can no longer change the
-		// outcome.
-		// This is not conform the spec, but mimics the old implementation.
-		// It also results in nicer default semantics. Changing this will
-		// break existing CUE code in awkward ways.
-		// We probably should address this when we figure out how to change
-		// the spec to accommodate for this. For instance, we could say
-		// that if a disjunction only contributes a single disjunct to an
-		// end result, default information is ignored. Not the greatest
-		// definition, though.
-		// Another alternative might be to have a special builtin that
-		// mimics the good behavior.
-		// Note that the same result can be obtained in CUE by adding
-		// 0 to a referenced number (forces the default to be discarded).
-		wasScalar := n.scalar != nil // Hack line 1
-
-		c := adt.MakeConjunct(d.env, v.expr, d.cloneID)
-		n.addExprConjunct(c)
-
-		for n.expandOne() {
-		}
+	default: // len(n.disjunctions) == 0
+		m := *n
+		n.postDisjunct(state)
 
 		if n.hasErr() {
-			continue
-		}
-
-		var mode defaultMode
-		switch {
-		case d.numDefaults == 0:
-			mode = maybeDefault
-		case v.isDefault:
-			mode = isDefault
-		default:
-			mode = notDefault
-		}
-
-		if isSub {
-			if !wasScalar { // Hack line 2.
-				n.subMode = combineDefault(n.subMode, mode)
+			x := n.node
+			err, ok := x.BaseValue.(*adt.Bottom)
+			if !ok {
+				err = n.getErr()
 			}
-		} else if sub == len(n.disjunctions) {
-			n.defaultMode = combineDefault(n.defaultMode, n.subMode)
-			n.defaultMode = combineDefault(n.defaultMode, mode)
-			n.subMode = maybeDefault
+			if err == nil {
+				// TODO(disjuncts): Is this always correct? Especially for partial
+				// evaluation it is okay for child errors to have incomplete errors.
+				// Perhaps introduce an Err() method.
+				err = x.ChildErrors
+			}
+			if err != nil {
+				n.disjunctErrs = append(n.disjunctErrs, err)
+			}
+			if recursive || len(n.disjunctions) > 0 {
+				n.eval.freeNodeContext(n)
+			}
+			return
+		}
+		// TODO: clean up this mess:
+		n.touched = true  // move result setting here
+		result := *n.node // XXX: n.result = snapshotVertex(n.node)?
+
+		if result.BaseValue == nil {
+			result.BaseValue = n.getValidators()
 		}
 
-		n.processDisjunct(state, k, sub)
+		n.nodeShared.setResult(n.node)
+		if n.node.BaseValue == nil {
+			n.node.BaseValue = result.BaseValue
+		}
+		if state < adt.Finalized {
+			*n = m
+		}
+		n.result = result
+		n.disjuncts = append(n.disjuncts, n)
+
+	case len(n.disjunctions) > 0:
+
+		n.disjuncts = append(n.disjuncts, n)
+
+		for i, d := range n.disjunctions {
+			a := n.disjuncts
+			n.disjuncts = n.buffer[:0]
+			n.buffer = a[:0]
+
+			state := state
+			if i+1 < len(n.disjunctions) {
+				// If this is not the last disjunction, set it to
+				// partial evaluation. This will disable the closedness
+				// check and any other non-monotonic check that should
+				// not be done unless there is complete information.
+				state = adt.Partial
+			}
+
+			for _, dn := range a {
+				for _, v := range d.values {
+					cn := dn.clone()
+					*cn.node = snapshotVertex(dn.snapshot)
+
+					c := adt.MakeConjunct(d.env, v.expr, d.cloneID)
+					cn.addExprConjunct(c)
+
+					newMode := mode(d, v)
+
+					cn.expandDisjuncts(state, n, newMode, true)
+
+					cn.defaultMode = combineDefault(dn.defaultMode, newMode)
+				}
+			}
+
+			if i > 0 {
+				for _, d := range a {
+					n.eval.freeNodeContext(d)
+				}
+			}
+
+			if len(n.disjuncts) == 0 {
+				n.makeError()
+			}
+		}
+
+		// HACK alert: this replaces the hack of the previous algorithm with a
+		// slightly less worse hack: instead of dropping the default info when
+		// the value was scalar before, we drop this information when there
+		// is only one disjunct, while not discarding hard defaults.
+		// TODO: a more principled approach would be to recognize that there
+		// is only one default at a point where this does not break
+		// commutativity.
+		if len(n.disjuncts) == 1 && n.disjuncts[0].defaultMode != isDefault {
+			n.disjuncts[0].defaultMode = maybeDefault
+		}
 	}
+
+	// Compare to root, but add to this one.
+	// TODO: if only one value is left, set to maybeDefault.
+	switch p := parent; {
+	case p != nil:
+		k := 0
+	outer:
+		for _, d := range n.disjuncts {
+			for _, v := range p.disjuncts {
+				if adt.Equal(n.ctx, &v.result, &d.result) {
+					n.eval.freeNodeContext(n)
+					continue outer
+				}
+			}
+			n.disjuncts[k] = d
+			k++
+
+			d.defaultMode = combineDefault(m, d.defaultMode)
+		}
+
+		p.disjuncts = append(p.disjuncts, n.disjuncts[:k]...)
+		n.disjuncts = n.disjuncts[:0]
+
+	case n.done():
+		n.nodeShared.isDone = true
+	}
+}
+
+func (n *nodeShared) makeError() {
+	code := adt.IncompleteError
+
+	if len(n.disjunctErrs) > 0 {
+		code = adt.EvalError
+		for _, c := range n.disjunctErrs {
+			if c.Code > code {
+				code = c.Code
+			}
+		}
+	}
+
+	b := &adt.Bottom{
+		Code: code,
+		Err:  n.disjunctError(),
+	}
+	n.node.SetValue(n.ctx, adt.Finalized, b)
+}
+
+func mode(d envDisjunct, v disjunct) defaultMode {
+	var mode defaultMode
+	switch {
+	case d.numDefaults == 0:
+		mode = maybeDefault
+	case v.isDefault:
+		mode = isDefault
+	default:
+		mode = notDefault
+	}
+	return mode
 }
 
 // Clone makes a shallow copy of a Vertex. The purpose is to create different
@@ -316,28 +317,26 @@
 // longer needed and can become nil. All other fields can be copied shallowly.
 //
 // USE TO SAVE NODE BRANCH FOR DISJUNCTION, BUT BEFORE POSTDIJSUNCT.
-func snapshotVertex(v *adt.Vertex) adt.Vertex {
-	c := *v
-
-	if len(v.Arcs) > 0 {
-		c.Arcs = make([]*adt.Vertex, len(v.Arcs))
-		for i, arc := range v.Arcs {
+func snapshotVertex(v adt.Vertex) adt.Vertex {
+	if a := v.Arcs; len(a) > 0 {
+		v.Arcs = make([]*adt.Vertex, len(a))
+		for i, arc := range a {
 			// For child arcs, only Conjuncts are set and Arcs and
 			// Structs will be nil.
 			a := *arc
-			c.Arcs[i] = &a
+			v.Arcs[i] = &a
 
 			a.Conjuncts = make([]adt.Conjunct, len(arc.Conjuncts))
 			copy(a.Conjuncts, arc.Conjuncts)
 		}
 	}
 
-	if len(v.Structs) > 0 {
-		c.Structs = make([]*adt.StructInfo, len(v.Structs))
-		copy(c.Structs, v.Structs)
+	if a := v.Structs; len(a) > 0 {
+		v.Structs = make([]*adt.StructInfo, len(a))
+		copy(v.Structs, a)
 	}
 
-	return c
+	return v
 }
 
 // Default rules from spec:
@@ -403,13 +402,13 @@
 //
 // TODO(perf): the set of errors is now computed during evaluation. Eventually,
 // this could be done lazily.
-func (n *nodeContext) disjunctError() (errs errors.Error) {
+func (n *nodeShared) disjunctError() (errs errors.Error) {
 	ctx := n.ctx
 
 	disjuncts := selectErrors(n.disjunctErrs)
 
 	if disjuncts == nil {
-		errs = ctx.Newf("empty disjunction")
+		errs = ctx.Newf("empty disjunction") // XXX: add space to sort first
 	} else {
 		disjuncts = errors.Sanitize(disjuncts)
 		k := len(errors.Errors(disjuncts))
diff --git a/internal/core/eval/eval.go b/internal/core/eval/eval.go
index 74ce4d5..d5869f1 100644
--- a/internal/core/eval/eval.go
+++ b/internal/core/eval/eval.go
@@ -163,7 +163,7 @@
 
 		if s.result_.BaseValue != nil { // There is a complete result.
 			*v = s.result_
-			result = *v
+			// result = *v
 		} else if b, ok := v.BaseValue.(*adt.Bottom); ok {
 			*v = save
 			return b
@@ -174,7 +174,7 @@
 		switch {
 		case !s.touched:
 
-		case len(s.disjunct.Values) == 1 || s.disjunct.NumDefaults == 1:
+		case len(s.disjuncts) == 1 || s.numDefaults == 1:
 			// TODO: this seems unnecessary as long as we have a better way
 			// to handle incomplete, and perhaps referenced. nodes.
 			if c.IsTentative() && isStruct(v) {
@@ -235,7 +235,8 @@
 				return b
 			}
 			// TODO: Only use result when not a cycle.
-			v = s.result()
+			w := s.result()
+			v = &w
 		}
 		// TODO: Store if concrete and fully resolved.
 	}
@@ -245,6 +246,9 @@
 	// gets the concrete value.
 	//
 	if v.BaseValue == nil {
+		if result.BaseValue == nil {
+			panic("nil basevalue")
+		}
 		return &result
 	}
 	return v
@@ -275,10 +279,10 @@
 	defer e.freeSharedNode(n)
 
 	switch {
-	case len(n.disjunct.Values) == 1:
-		*v = *(n.disjunct.Values[0])
+	case len(n.disjuncts) == 1:
+		*v = n.disjuncts[0].result
 
-	case len(n.disjunct.Values) > 0:
+	case len(n.disjuncts) > 0:
 		d := n.createDisjunct()
 		v.BaseValue = d
 		// The conjuncts will have too much information. Better have no
@@ -307,7 +311,7 @@
 
 	default:
 		if r := n.result(); r.BaseValue != nil {
-			*v = *r
+			*v = r
 		}
 	}
 
@@ -337,7 +341,7 @@
 		}
 	}
 
-	if !v.Label.IsInt() && v.Parent != nil && !ignore {
+	if !v.Label.IsInt() && v.Parent != nil && !ignore && state == adt.Finalized {
 		// Visit arcs recursively to validate and compute error.
 		if _, err := verifyArc(c, v.Label, v, v.Closed); err != nil {
 			// Record error in child node to allow recording multiple
@@ -392,6 +396,7 @@
 		e.freeNodeContext(n)
 		return shared
 	}
+
 	if !n.done() && len(n.disjunctions) > 0 && isEvaluating(v) {
 		// We disallow entering computations of disjunctions with
 		// incomplete data.
@@ -403,15 +408,13 @@
 		return shared
 	}
 
-	n.processDisjuncts(state)
-
-	// Handle disjunctions. If there are no disjunctions, this call is
-	// equivalent to calling n.postDisjunct.
+	n.expandDisjuncts(state, nil, maybeDefault, false)
+	// TODO: reorganize nodeShared and nodeContext
+	n.nodeShared.disjuncts = append(n.nodeShared.disjuncts, n.disjuncts...)
 	if v.BaseValue == nil {
 		v.BaseValue = n.getValidators()
 	}
-
-	e.freeNodeContext(n)
+	// e.freeNodeContext(n) freed in freeSharedNode
 	return shared
 }
 
@@ -589,7 +592,7 @@
 			if state == adt.Finalized {
 				n.node.UpdateStatus(adt.EvaluatingArcs)
 			}
-			n.eval.Unify(ctx, a, adt.Finalized)
+			n.eval.Unify(ctx, a, state)
 			if err, _ := a.BaseValue.(*adt.Bottom); err != nil {
 				n.node.AddChildError(err)
 			}
@@ -625,8 +628,9 @@
 
 	// Disjunction handling
 	touched      bool
-	disjunct     adt.Disjunction
+	disjuncts    []*nodeContext
 	disjunctErrs []*adt.Bottom
+	numDefaults  int
 
 	result_ adt.Vertex
 	isDone  bool
@@ -642,7 +646,7 @@
 			ctx:  ctx,
 			node: node,
 
-			disjunct:     adt.Disjunction{Values: n.disjunct.Values[:0]},
+			disjuncts:    n.disjuncts[:0],
 			disjunctErrs: n.disjunctErrs[:0],
 		}
 
@@ -661,20 +665,33 @@
 	e.stats.Freed++
 	n.nextFree = e.freeListShared
 	e.freeListShared = n
-}
 
-func (n *nodeShared) createDisjunct() *adt.Disjunction {
-	a := make([]*adt.Vertex, len(n.disjunct.Values))
-	copy(a, n.disjunct.Values)
-	return &adt.Disjunction{
-		Values:      a,
-		NumDefaults: n.disjunct.NumDefaults,
+	for _, d := range n.disjuncts {
+		e.freeNodeContext(d)
 	}
 }
 
-func (n *nodeShared) result() *adt.Vertex {
-	x := n.result_
-	return &x
+func (n *nodeShared) createDisjunct() *adt.Disjunction {
+	a := make([]*adt.Vertex, len(n.disjuncts))
+	p := 0
+	for i, x := range n.disjuncts {
+		v := x.result
+		if x.defaultMode == isDefault {
+			a[i] = a[p]
+			a[p] = &v
+			p++
+		} else {
+			a[i] = &v
+		}
+	}
+	return &adt.Disjunction{
+		Values:      a,
+		NumDefaults: p,
+	}
+}
+
+func (n *nodeShared) result() adt.Vertex {
+	return n.result_
 }
 
 func (n *nodeShared) setResult(v *adt.Vertex) {
@@ -682,17 +699,13 @@
 }
 
 func (n *nodeShared) hasResult() bool {
-	return len(n.disjunct.Values) > 1
+	return len(n.disjuncts) > 0
 }
 
 func (n *nodeShared) done() bool {
 	return n.isDone
 }
 
-func (n *nodeShared) isDefault() bool {
-	return n.disjunct.NumDefaults > 0
-}
-
 type arcKey struct {
 	arc *adt.Vertex
 	id  adt.CloseInfo
@@ -707,7 +720,7 @@
 
 	*nodeShared
 
-	// TODO:
+	// TODO: (this is CL is first step)
 	// filter *adt.Vertex a subset of composite with concrete fields for
 	// bloom-like filtering of disjuncts. We should first verify, however,
 	// whether some breath-first search gives sufficient performance, as this
@@ -716,6 +729,13 @@
 
 	arcMap []arcKey
 
+	// snapshot holds the last value of the vertex before calling postDisjunct.
+	snapshot adt.Vertex
+
+	// Result holds the last evaluated value of the vertex after calling
+	// postDisjunct.
+	result adt.Vertex
+
 	// Current value (may be under construction)
 	scalar   adt.Value // TODO: use Value in node.
 	scalarID adt.CloseInfo
@@ -728,7 +748,6 @@
 	upperBound *adt.BoundValue // < or <=
 	checks     []adt.Validator // BuiltinValidator, other bound values.
 	errs       *adt.Bottom
-	incomplete *adt.Bottom
 
 	// Struct information
 	dynamicFields []envDynamic
@@ -736,20 +755,53 @@
 	forClauses    []envYield
 	aStruct       adt.Expr
 	aStructID     adt.CloseInfo
-	hasTop        bool
 
 	// Expression conjuncts
 	lists  []envList
 	vLists []*adt.Vertex
 	exprs  []envExpr
 
+	hasTop      bool
 	hasCycle    bool // has conjunct with structural cycle
 	hasNonCycle bool // has conjunct without structural cycle
 
 	// Disjunction handling
 	disjunctions []envDisjunct
 	defaultMode  defaultMode
-	subMode      defaultMode
+	disjuncts    []*nodeContext
+	buffer       []*nodeContext
+}
+
+func (n *nodeContext) clone() *nodeContext {
+	d := n.eval.newNodeContext(n.nodeShared)
+
+	d.scalar = n.scalar
+	d.scalarID = n.scalarID
+	d.kind = n.kind
+	d.kindExpr = n.kindExpr
+	d.kindID = n.kindID
+	d.aStruct = n.aStruct
+	d.aStructID = n.aStructID
+	d.hasTop = n.hasTop
+
+	d.lowerBound = n.lowerBound
+	d.upperBound = n.upperBound
+	d.errs = n.errs
+	d.hasTop = n.hasTop
+	d.hasCycle = n.hasCycle
+	d.hasNonCycle = n.hasNonCycle
+
+	// d.arcMap = append(d.arcMap, n.arcMap...) // XXX add?
+	d.checks = append(d.checks, n.checks...)
+	d.dynamicFields = append(d.dynamicFields, n.dynamicFields...)
+	d.ifClauses = append(d.ifClauses, n.ifClauses...)
+	d.forClauses = append(d.forClauses, n.forClauses...)
+	d.lists = append(d.lists, n.lists...)
+	d.vLists = append(d.vLists, n.vLists...)
+	d.exprs = append(d.exprs, n.exprs...)
+	// No need to clone d.disjunctions
+
+	return d
 }
 
 func (e *Evaluator) newNodeContext(shared *nodeShared) *nodeContext {
@@ -769,6 +821,8 @@
 			vLists:        n.vLists[:0],
 			exprs:         n.exprs[:0],
 			disjunctions:  n.disjunctions[:0],
+			disjuncts:     n.disjuncts[:0],
+			buffer:        n.buffer[:0],
 		}
 
 		return n
@@ -922,7 +976,7 @@
 }
 
 func (n *nodeContext) maybeSetCache() {
-	if n.node.Status() > adt.Evaluating { // n.node.Value != nil
+	if n.node.Status() > adt.Evaluating { // n.node.BaseValue != nil
 		return
 	}
 	if n.scalar != nil {
@@ -1300,7 +1354,7 @@
 		// TODO: evaluate value?
 		switch v := x.BaseValue.(type) {
 		default:
-			panic("invalid value")
+			panic(fmt.Sprintf("invalid type %T", x.BaseValue))
 
 		case *adt.ListMarker:
 			n.vLists = append(n.vLists, x)
diff --git a/pkg/path/testdata/error.txtar b/pkg/path/testdata/error.txtar
index f53048c..023994a 100644
--- a/pkg/path/testdata/error.txtar
+++ b/pkg/path/testdata/error.txtar
@@ -19,10 +19,12 @@
 joinErr: path.Join(["a", "b"], "foo")
 -- out/path --
 Errors:
+cannot use _|_(2 errors in empty disjunction: (and 2 more errors)) (type _|_) as string in argument 1 to path.Join: 2 errors in empty disjunction: (and 2 more errors):
+    ./in.cue:4:10
 joinErr: cannot use "foo" as *"unix" | "windows" | "plan9" | "aix" | "android" | "darwin" | "dragonfly" | "freebsd" | "hurd" | "illumos" | "ios" | "js" | "linux" | "nacl" | "netbsd" | "openbsd" | "solaris" | "zos" in argument 2 to path.Join:
     ./in.cue:4:32
 
 Result:
 joinOK:  "a/b"
-joinErr: _|_ // joinErr: cannot use "foo" as *"unix" | "windows" | "plan9" | "aix" | "android" | "darwin" | "dragonfly" | "freebsd" | "hurd" | "illumos" | "ios" | "js" | "linux" | "nacl" | "netbsd" | "openbsd" | "solaris" | "zos" in argument 2 to path.Join (and 1 more errors)
+joinErr: _|_ // joinErr: cannot use "foo" as *"unix" | "windows" | "plan9" | "aix" | "android" | "darwin" | "dragonfly" | "freebsd" | "hurd" | "illumos" | "ios" | "js" | "linux" | "nacl" | "netbsd" | "openbsd" | "solaris" | "zos" in argument 2 to path.Join (and 3 more errors)