internal/core/adt: fix performance issue with disjunctions
Fixes a performance TODO, which was a deliberate regression
to deal with a semantic bug.
Fixes #651
Change-Id: I9d95dc9d250064e79552425331912f58175cc094
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/8282
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/testdata/benchmarks/dedupelem.txtar b/cue/testdata/benchmarks/dedupelem.txtar
index b83f06c..ca1ed5b 100644
--- a/cue/testdata/benchmarks/dedupelem.txtar
+++ b/cue/testdata/benchmarks/dedupelem.txtar
@@ -19,7 +19,7 @@
foo.0.type: conflicting values "int" and "float":
./in.cue:3:16
./in.cue:5:14
- ./in.cue:6:30
+ ./in.cue:5:30
foo.0.type: conflicting values "string" and "float":
./in.cue:5:14
./in.cue:10:14
@@ -41,7 +41,7 @@
// foo.0.type: conflicting values "int" and "float":
// ./in.cue:3:16
// ./in.cue:5:14
- // ./in.cue:6:30
+ // ./in.cue:5:30
// foo.0.type: conflicting values "string" and "float":
// ./in.cue:5:14
// ./in.cue:10:14
diff --git a/cue/testdata/benchmarks/mergeddisjunction.txtar b/cue/testdata/benchmarks/mergeddisjunction.txtar
new file mode 100644
index 0000000..56ae636
--- /dev/null
+++ b/cue/testdata/benchmarks/mergeddisjunction.txtar
@@ -0,0 +1,223 @@
+// Here one comprehension creates a number of distinct values, each of which
+// with a disjunction, then another comprehension maps them back to the
+// same value, creating a large number of disjunctions.
+//
+// The disjunctions cannot be identified as equal in the general case. If no
+// care is taken, disjunction elimination will be exponential, causing over
+// a billion disjuncts to process in the below example. With the proper
+// optimizations, there is a small, constant number of disjunction ops per
+// disjunct.
+//
+// Issue #651
+-- in.cue --
+list: [
+ 0, 1, 2, 3, 4, 5, 6, 7, 8,
+ 9, 10, 11, 12, 13, 14, 15, 16,
+ 17, 18, 19, 20, 21, 22, 23, 24,
+ 25, 26, 27, 28, 29, 30,
+]
+a: [X=string]: text: *"default" | string
+
+a: {
+ for i in list {
+ "\(i)": text: string
+ }
+}
+
+b: {
+ for x in a {
+ "\(x.text)": { text: x.text }
+ }
+}
+-- out/eval --
+(struct){
+ list: (#list){
+ 0: (int){ 0 }
+ 1: (int){ 1 }
+ 2: (int){ 2 }
+ 3: (int){ 3 }
+ 4: (int){ 4 }
+ 5: (int){ 5 }
+ 6: (int){ 6 }
+ 7: (int){ 7 }
+ 8: (int){ 8 }
+ 9: (int){ 9 }
+ 10: (int){ 10 }
+ 11: (int){ 11 }
+ 12: (int){ 12 }
+ 13: (int){ 13 }
+ 14: (int){ 14 }
+ 15: (int){ 15 }
+ 16: (int){ 16 }
+ 17: (int){ 17 }
+ 18: (int){ 18 }
+ 19: (int){ 19 }
+ 20: (int){ 20 }
+ 21: (int){ 21 }
+ 22: (int){ 22 }
+ 23: (int){ 23 }
+ 24: (int){ 24 }
+ 25: (int){ 25 }
+ 26: (int){ 26 }
+ 27: (int){ 27 }
+ 28: (int){ 28 }
+ 29: (int){ 29 }
+ 30: (int){ 30 }
+ }
+ a: (struct){
+ "0": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "1": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "2": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "3": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "4": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "5": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "6": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "7": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "8": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "9": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "10": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "11": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "12": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "13": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "14": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "15": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "16": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "17": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "18": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "19": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "20": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "21": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "22": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "23": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "24": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "25": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "26": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "27": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "28": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "29": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ "30": (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ }
+ b: (struct){
+ default: (struct){
+ text: (string){ |(*(string){ "default" }, (string){ string }) }
+ }
+ }
+}
+-- out/compile --
+--- in.cue
+{
+ list: [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ ]
+ a: {
+ [string]: {
+ text: (*"default"|string)
+ }
+ }
+ a: {
+ for _, i in 〈1;list〉 {
+ "\(〈1;i〉)": {
+ text: string
+ }
+ }
+ }
+ b: {
+ for _, x in 〈1;a〉 {
+ "\(〈1;x〉.text)": {
+ text: 〈2;x〉.text
+ }
+ }
+ }
+}
diff --git a/cue/testdata/disjunctions/elimination.txtar b/cue/testdata/disjunctions/elimination.txtar
new file mode 100644
index 0000000..b33f099
--- /dev/null
+++ b/cue/testdata/disjunctions/elimination.txtar
@@ -0,0 +1,788 @@
+// Ensure that disjunction elimination is not done prematurely.
+
+// Issue #651
+
+-- in.cue --
+import "struct"
+
+// Closedness checks should not be triggered too early, or with an improper
+// subset of StructInfos. Blindly finalizing partial disjuncts will end up
+// doing a closedness check with not all embeddings present, which can lead to
+// a field being rejected prematurely.
+//
+// It should be recursively disabled.
+disambiguateClosed: {
+ b: #Def & a
+ a: #Def
+ #Def: { {x: true} | {y: true} }
+}
+
+// Checks should never be disabled for field matching.
+alwaysCheckMatchers1: {
+ b: {[=~"^xxxx$"]: int} | null
+ b: {c: string} | null
+ b: c: "yyyyy"
+}
+
+alwaysCheckPatterns2: {
+ a: #X
+ a: b
+
+ b: #X
+ b: c: "yyyyy"
+
+ #X: string | {
+ c: string
+ {[=~"^xxxx$"]: int}
+ }
+}
+
+nestedNonMonotonic: resolved: n1: {
+ x: { a: struct.MinFields(2) } | null
+ x: { a: c: 1 } | null
+ x: { a: d: 1 } | null
+}
+
+nestedNonMonotonic: resolved: n2: {
+ x: { a: b: struct.MinFields(2) } | null
+ x: { a: b: c: 1 } | null
+ x: { a: b: d: 1 } | null
+}
+
+nestedNonMonotonic: eliminated: n1: p1: {
+ x: { a: struct.MaxFields(1) } | null
+ x: { a: c: 1 } | null
+ x: { a: d: 1 } | null
+}
+
+nestedNonMonotonic: eliminated: n1: p2: {
+ x: { a: c: 1 } | null
+ x: { a: struct.MaxFields(1) } | null
+ x: { a: d: 1 } | null
+}
+
+nestedNonMonotonic: eliminated: n1: p2: {
+ x: { a: c: 1 } | null
+ x: { a: d: 1 } | null
+ x: { a: struct.MaxFields(1) } | null
+}
+
+nestedNonMonotonic: eliminated: n2: p1: {
+ x: { a: b: struct.MaxFields(1) } | null
+ x: { a: b: c: 1 } | null
+ x: { a: b: d: 1 } | null
+}
+
+nestedNonMonotonic: eliminated: n2: p2: {
+ x: { a: b: c: 1 } | null
+ x: { a: b: struct.MaxFields(1) } | null
+ x: { a: b: d: 1 } | null
+}
+
+nestedNonMonotonic: eliminated: n2: p2: {
+ x: { a: b: c: 1 } | null
+ x: { a: b: d: 1 } | null
+ x: { a: b: struct.MaxFields(1) } | null
+}
+
+nestedNonMonotonic: incomplete: a: n1: p1: {
+ x: { a: struct.MinFields(2) } | null
+ x: { a: c: 1 } | null
+}
+
+nestedNonMonotonic: incomplete: a: n1: p2: {
+ x: { a: c: 1 } | null
+ x: { a: struct.MinFields(2) } | null
+}
+
+nestedNonMonotonic: incomplete: a: n2: p1: {
+ x: { a: b: struct.MinFields(2) } | null
+ x: { a: b: c: 1 } | null
+}
+
+nestedNonMonotonic: incomplete: a: n2: p2: {
+ x: { a: b: c: 1 } | null
+ x: { a: b: struct.MinFields(2) } | null
+}
+
+
+nestedNonMonotonic: incomplete: b: n1: p1: {
+ x: { a: struct.MinFields(3) } | null
+ x: { a: c: 1 } | null
+ x: { a: d: 1 } | null
+}
+
+nestedNonMonotonic: incomplete: b: n1: p2: {
+ x: { a: c: 1 } | null
+ x: { a: struct.MinFields(3) } | null
+ x: { a: d: 1 } | null
+}
+
+nestedNonMonotonic: incomplete: b: n1: p3: {
+ x: { a: c: 1 } | null
+ x: { a: d: 1 } | null
+ x: { a: struct.MinFields(3) } | null
+}
+
+nestedNonMonotonic: incomplete: b: n2: p1: {
+ x: { a: b: struct.MinFields(3) } | null
+ x: { a: b: c: 1 } | null
+ x: { a: b: d: 1 } | null
+}
+
+nestedNonMonotonic: incomplete: b: n2: p1: {
+ x: { a: b: c: 1 } | null
+ x: { a: b: struct.MinFields(3) } | null
+ x: { a: b: d: 1 } | null
+}
+
+nestedNonMonotonic: incomplete: b: n2: p1: {
+ x: { a: b: c: 1 } | null
+ x: { a: b: d: 1 } | null
+ x: { a: b: struct.MinFields(3) } | null
+}
+
+-- out/eval --
+(struct){
+ disambiguateClosed: (struct){
+ b: (struct){ |((#struct){
+ x: (bool){ true }
+ }, (#struct){
+ y: (bool){ true }
+ }) }
+ a: (struct){ |((#struct){
+ x: (bool){ true }
+ }, (#struct){
+ y: (bool){ true }
+ }) }
+ #Def: (struct){ |((#struct){
+ x: (bool){ true }
+ }, (#struct){
+ y: (bool){ true }
+ }) }
+ }
+ alwaysCheckMatchers1: (struct){
+ b: (struct){
+ c: (string){ "yyyyy" }
+ }
+ }
+ alwaysCheckPatterns2: (struct){
+ a: (#struct){
+ c: (string){ "yyyyy" }
+ }
+ b: (#struct){
+ c: (string){ "yyyyy" }
+ }
+ #X: ((string|struct)){ |((string){ string }, (#struct){
+ c: (string){ string }
+ }) }
+ }
+ nestedNonMonotonic: (struct){
+ resolved: (struct){
+ n1: (struct){
+ x: ((null|struct)){ |((struct){
+ a: (struct){
+ c: (int){ 1 }
+ d: (int){ 1 }
+ }
+ }, (null){ null }) }
+ }
+ n2: (struct){
+ x: ((null|struct)){ |((struct){
+ a: (struct){
+ b: (struct){
+ c: (int){ 1 }
+ d: (int){ 1 }
+ }
+ }
+ }, (null){ null }) }
+ }
+ }
+ eliminated: (struct){
+ n1: (struct){
+ p1: (struct){
+ x: (null){ null }
+ }
+ p2: (struct){
+ x: (null){ null }
+ }
+ }
+ n2: (struct){
+ p1: (struct){
+ x: (null){ null }
+ }
+ p2: (struct){
+ x: (null){ null }
+ }
+ }
+ }
+ incomplete: (struct){
+ a: (struct){
+ n1: (struct){
+ p1: (struct){
+ x: ((null|struct)){ |((struct){
+ a: (_|_){
+ // [incomplete] nestedNonMonotonic.incomplete.a.n1.p1.x.a: invalid value {c:1} (does not satisfy struct.MinFields(2)): len(fields) < MinFields(2) (1 < 2):
+ // ./in.cue:84:13
+ // ./in.cue:84:30
+ c: (int){ 1 }
+ }
+ }, (null){ null }) }
+ }
+ p2: (struct){
+ x: ((null|struct)){ |((struct){
+ a: (_|_){
+ // [incomplete] nestedNonMonotonic.incomplete.a.n1.p2.x.a: invalid value {c:1} (does not satisfy struct.MinFields(2)): len(fields) < MinFields(2) (1 < 2):
+ // ./in.cue:90:13
+ // ./in.cue:90:30
+ c: (int){ 1 }
+ }
+ }, (null){ null }) }
+ }
+ }
+ n2: (struct){
+ p1: (struct){
+ x: ((null|struct)){ |((struct){
+ a: (struct){
+ b: (_|_){
+ // [incomplete] nestedNonMonotonic.incomplete.a.n2.p1.x.a.b: invalid value {c:1} (does not satisfy struct.MinFields(2)): len(fields) < MinFields(2) (1 < 2):
+ // ./in.cue:94:16
+ // ./in.cue:94:33
+ c: (int){ 1 }
+ }
+ }
+ }, (null){ null }) }
+ }
+ p2: (struct){
+ x: ((null|struct)){ |((struct){
+ a: (struct){
+ b: (_|_){
+ // [incomplete] nestedNonMonotonic.incomplete.a.n2.p2.x.a.b: invalid value {c:1} (does not satisfy struct.MinFields(2)): len(fields) < MinFields(2) (1 < 2):
+ // ./in.cue:100:16
+ // ./in.cue:100:33
+ c: (int){ 1 }
+ }
+ }
+ }, (null){ null }) }
+ }
+ }
+ }
+ b: (struct){
+ n1: (struct){
+ p1: (struct){
+ x: ((null|struct)){ |((struct){
+ a: (_|_){
+ // [incomplete] nestedNonMonotonic.incomplete.b.n1.p1.x.a: invalid value {c:1,d:1} (does not satisfy struct.MinFields(3)): len(fields) < MinFields(3) (2 < 3):
+ // ./in.cue:105:13
+ // ./in.cue:105:30
+ c: (int){ 1 }
+ d: (int){ 1 }
+ }
+ }, (null){ null }) }
+ }
+ p2: (struct){
+ x: ((null|struct)){ |((struct){
+ a: (_|_){
+ // [incomplete] nestedNonMonotonic.incomplete.b.n1.p2.x.a: invalid value {c:1,d:1} (does not satisfy struct.MinFields(3)): len(fields) < MinFields(3) (2 < 3):
+ // ./in.cue:112:13
+ // ./in.cue:112:30
+ c: (int){ 1 }
+ d: (int){ 1 }
+ }
+ }, (null){ null }) }
+ }
+ p3: (struct){
+ x: ((null|struct)){ |((struct){
+ a: (_|_){
+ // [incomplete] nestedNonMonotonic.incomplete.b.n1.p3.x.a: invalid value {c:1,d:1} (does not satisfy struct.MinFields(3)): len(fields) < MinFields(3) (2 < 3):
+ // ./in.cue:119:13
+ // ./in.cue:119:30
+ c: (int){ 1 }
+ d: (int){ 1 }
+ }
+ }, (null){ null }) }
+ }
+ }
+ n2: (struct){
+ p1: (struct){
+ x: ((null|struct)){ |((struct){
+ a: (struct){
+ b: (_|_){
+ // [incomplete] nestedNonMonotonic.incomplete.b.n2.p1.x.a.b: invalid value {c:1,d:1} (does not satisfy struct.MinFields(3)): len(fields) < MinFields(3) (2 < 3):
+ // ./in.cue:137:16
+ // ./in.cue:137:33
+ c: (int){ 1 }
+ d: (int){ 1 }
+ }
+ }
+ }, (null){ null }) }
+ }
+ }
+ }
+ }
+ }
+}
+-- out/compile --
+--- in.cue
+{
+ disambiguateClosed: {
+ b: (〈0;#Def〉 & 〈0;a〉)
+ a: 〈0;#Def〉
+ #Def: {
+ ({
+ x: true
+ }|{
+ y: true
+ })
+ }
+ }
+ alwaysCheckMatchers1: {
+ b: ({
+ [=~"^xxxx$"]: int
+ }|null)
+ b: ({
+ c: string
+ }|null)
+ b: {
+ c: "yyyyy"
+ }
+ }
+ alwaysCheckPatterns2: {
+ a: 〈0;#X〉
+ a: 〈0;b〉
+ b: 〈0;#X〉
+ b: {
+ c: "yyyyy"
+ }
+ #X: (string|{
+ c: string
+ {
+ [=~"^xxxx$"]: int
+ }
+ })
+ }
+ nestedNonMonotonic: {
+ resolved: {
+ n1: {
+ x: ({
+ a: 〈import;struct〉.MinFields(2)
+ }|null)
+ x: ({
+ a: {
+ c: 1
+ }
+ }|null)
+ x: ({
+ a: {
+ d: 1
+ }
+ }|null)
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ resolved: {
+ n2: {
+ x: ({
+ a: {
+ b: 〈import;struct〉.MinFields(2)
+ }
+ }|null)
+ x: ({
+ a: {
+ b: {
+ c: 1
+ }
+ }
+ }|null)
+ x: ({
+ a: {
+ b: {
+ d: 1
+ }
+ }
+ }|null)
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ eliminated: {
+ n1: {
+ p1: {
+ x: ({
+ a: 〈import;struct〉.MaxFields(1)
+ }|null)
+ x: ({
+ a: {
+ c: 1
+ }
+ }|null)
+ x: ({
+ a: {
+ d: 1
+ }
+ }|null)
+ }
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ eliminated: {
+ n1: {
+ p2: {
+ x: ({
+ a: {
+ c: 1
+ }
+ }|null)
+ x: ({
+ a: 〈import;struct〉.MaxFields(1)
+ }|null)
+ x: ({
+ a: {
+ d: 1
+ }
+ }|null)
+ }
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ eliminated: {
+ n1: {
+ p2: {
+ x: ({
+ a: {
+ c: 1
+ }
+ }|null)
+ x: ({
+ a: {
+ d: 1
+ }
+ }|null)
+ x: ({
+ a: 〈import;struct〉.MaxFields(1)
+ }|null)
+ }
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ eliminated: {
+ n2: {
+ p1: {
+ x: ({
+ a: {
+ b: 〈import;struct〉.MaxFields(1)
+ }
+ }|null)
+ x: ({
+ a: {
+ b: {
+ c: 1
+ }
+ }
+ }|null)
+ x: ({
+ a: {
+ b: {
+ d: 1
+ }
+ }
+ }|null)
+ }
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ eliminated: {
+ n2: {
+ p2: {
+ x: ({
+ a: {
+ b: {
+ c: 1
+ }
+ }
+ }|null)
+ x: ({
+ a: {
+ b: 〈import;struct〉.MaxFields(1)
+ }
+ }|null)
+ x: ({
+ a: {
+ b: {
+ d: 1
+ }
+ }
+ }|null)
+ }
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ eliminated: {
+ n2: {
+ p2: {
+ x: ({
+ a: {
+ b: {
+ c: 1
+ }
+ }
+ }|null)
+ x: ({
+ a: {
+ b: {
+ d: 1
+ }
+ }
+ }|null)
+ x: ({
+ a: {
+ b: 〈import;struct〉.MaxFields(1)
+ }
+ }|null)
+ }
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ incomplete: {
+ a: {
+ n1: {
+ p1: {
+ x: ({
+ a: 〈import;struct〉.MinFields(2)
+ }|null)
+ x: ({
+ a: {
+ c: 1
+ }
+ }|null)
+ }
+ }
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ incomplete: {
+ a: {
+ n1: {
+ p2: {
+ x: ({
+ a: {
+ c: 1
+ }
+ }|null)
+ x: ({
+ a: 〈import;struct〉.MinFields(2)
+ }|null)
+ }
+ }
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ incomplete: {
+ a: {
+ n2: {
+ p1: {
+ x: ({
+ a: {
+ b: 〈import;struct〉.MinFields(2)
+ }
+ }|null)
+ x: ({
+ a: {
+ b: {
+ c: 1
+ }
+ }
+ }|null)
+ }
+ }
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ incomplete: {
+ a: {
+ n2: {
+ p2: {
+ x: ({
+ a: {
+ b: {
+ c: 1
+ }
+ }
+ }|null)
+ x: ({
+ a: {
+ b: 〈import;struct〉.MinFields(2)
+ }
+ }|null)
+ }
+ }
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ incomplete: {
+ b: {
+ n1: {
+ p1: {
+ x: ({
+ a: 〈import;struct〉.MinFields(3)
+ }|null)
+ x: ({
+ a: {
+ c: 1
+ }
+ }|null)
+ x: ({
+ a: {
+ d: 1
+ }
+ }|null)
+ }
+ }
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ incomplete: {
+ b: {
+ n1: {
+ p2: {
+ x: ({
+ a: {
+ c: 1
+ }
+ }|null)
+ x: ({
+ a: 〈import;struct〉.MinFields(3)
+ }|null)
+ x: ({
+ a: {
+ d: 1
+ }
+ }|null)
+ }
+ }
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ incomplete: {
+ b: {
+ n1: {
+ p3: {
+ x: ({
+ a: {
+ c: 1
+ }
+ }|null)
+ x: ({
+ a: {
+ d: 1
+ }
+ }|null)
+ x: ({
+ a: 〈import;struct〉.MinFields(3)
+ }|null)
+ }
+ }
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ incomplete: {
+ b: {
+ n2: {
+ p1: {
+ x: ({
+ a: {
+ b: 〈import;struct〉.MinFields(3)
+ }
+ }|null)
+ x: ({
+ a: {
+ b: {
+ c: 1
+ }
+ }
+ }|null)
+ x: ({
+ a: {
+ b: {
+ d: 1
+ }
+ }
+ }|null)
+ }
+ }
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ incomplete: {
+ b: {
+ n2: {
+ p1: {
+ x: ({
+ a: {
+ b: {
+ c: 1
+ }
+ }
+ }|null)
+ x: ({
+ a: {
+ b: 〈import;struct〉.MinFields(3)
+ }
+ }|null)
+ x: ({
+ a: {
+ b: {
+ d: 1
+ }
+ }
+ }|null)
+ }
+ }
+ }
+ }
+ }
+ nestedNonMonotonic: {
+ incomplete: {
+ b: {
+ n2: {
+ p1: {
+ x: ({
+ a: {
+ b: {
+ c: 1
+ }
+ }
+ }|null)
+ x: ({
+ a: {
+ b: {
+ d: 1
+ }
+ }
+ }|null)
+ x: ({
+ a: {
+ b: 〈import;struct〉.MinFields(3)
+ }
+ }|null)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/internal/core/adt/context.go b/internal/core/adt/context.go
index e1c4f9b..4a9ce1d 100644
--- a/internal/core/adt/context.go
+++ b/internal/core/adt/context.go
@@ -177,6 +177,26 @@
generation int
closed map[*closeInfo]*closeStats
todo *closeStats
+
+ // inDisjunct indicates that non-monotonic checks should be skipped.
+ // This is used if we want to do some extra work to eliminate disjunctions
+ // early. The result of unificantion should be thrown away if this check is
+ // used.
+ //
+ // TODO: replace this with a mechanism to determine the correct set (per
+ // conjunct) of StructInfos to include in closedness checking.
+ inDisjunct int
+
+ // inConstaint overrides inDisjunct as field matching should always be
+ // enabled.
+ inConstraint int
+}
+
+func (n *nodeContext) skipNonMonotonicChecks() bool {
+ if n.ctx.inConstraint > 0 {
+ return false
+ }
+ return n.ctx.inDisjunct > 0
}
// Impl is for internal use only. This will go.
diff --git a/internal/core/adt/disjunct.go b/internal/core/adt/disjunct.go
index d7b8f2f..7f3475f 100644
--- a/internal/core/adt/disjunct.go
+++ b/internal/core/adt/disjunct.go
@@ -141,7 +141,8 @@
m := *n
n.postDisjunct(state)
- if n.hasErr() {
+ switch {
+ case n.hasErr():
// TODO: consider finalizing the node thusly:
// if recursive {
// n.node.Finalize(n.ctx)
@@ -157,6 +158,9 @@
// Perhaps introduce an Err() method.
err = x.ChildErrors
}
+ if err.IsIncomplete() {
+ break
+ }
if err != nil {
parent.disjunctErrs = append(parent.disjunctErrs, err)
}
@@ -178,7 +182,8 @@
case len(n.disjunctions) > 0:
// Process full disjuncts to ensure that erroneous disjuncts are
- // eliminated.
+ // eliminated as early as possible.
+ state = Finalized
n.disjuncts = append(n.disjuncts, n)
@@ -187,16 +192,10 @@
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 = Partial
+ skipNonMonotonicChecks := i+1 < len(n.disjunctions)
+ if skipNonMonotonicChecks {
+ n.ctx.inDisjunct++
}
- // TODO(perf): ideally always finalize. See comment below
- // state = Finalized
for _, dn := range a {
switch {
@@ -231,6 +230,10 @@
}
}
+ if skipNonMonotonicChecks {
+ n.ctx.inDisjunct--
+ }
+
if len(n.disjuncts) == 0 {
n.makeError()
}
@@ -268,12 +271,6 @@
outer:
for _, d := range n.disjuncts {
for _, v := range p.disjuncts {
- // TODO(perf): not checking equality here may lead to polynomial
- // blowup. Doesn't seem to affect large configurations, though,
- // where this could matter at the moment.
- if state != Finalized {
- break
- }
if Equal(n.ctx, &v.result, &d.result) {
if d.defaultMode == isDefault {
v.defaultMode = isDefault
diff --git a/internal/core/adt/eval.go b/internal/core/adt/eval.go
index 9f7d233..d09a5c2 100644
--- a/internal/core/adt/eval.go
+++ b/internal/core/adt/eval.go
@@ -201,14 +201,33 @@
v.Closed = true
}
- ignore := false
if v.Parent != nil {
if v.Parent.Closed {
v.Closed = true
}
}
- if !v.Label.IsInt() && v.Parent != nil && !ignore && state == Finalized {
+ // TODO(perf): ideally we should always perform a closedness check if
+ // state is Finalized. This is currently not possible when computing a
+ // partial disjunction as the closedness information is not yet
+ // complete, possibly leading to a disjunct to be rejected prematurely.
+ // It is probably possible to fix this if we could add StructInfo
+ // structures demarked per conjunct.
+ //
+ // In practice this should not be a problem: when disjuncts originate
+ // from the same disjunct, they will have the same StructInfos, and thus
+ // Equal is able to equate them even in the precense of optional field.
+ // In general, combining any limited set of disjuncts will soon reach
+ // a fixed point where duplicate elements can be eliminated this way.
+ //
+ // Note that not checking closedness is irrelevant for disjunctions of
+ // scalars. This means it also doesn't hurt performance where structs
+ // have a discriminator field (e.g. Kubernetes). We should take care,
+ // though, that any potential performance issues are eliminated for
+ // Protobuf-like oneOf fields.
+ ignore := state != Finalized || n.skipNonMonotonicChecks()
+
+ if !v.Label.IsInt() && v.Parent != nil && !ignore {
// Visit arcs recursively to validate and compute error.
if _, err := verifyArc2(c, v.Label, v, v.Closed); err != nil {
// Record error in child node to allow recording multiple
@@ -474,7 +493,9 @@
}
}
// MOVE BELOW
- if v := n.node.Value(); v != nil && IsConcrete(v) {
+ // TODO(perf): only delay processing of actual non-monotonic checks.
+ skip := n.skipNonMonotonicChecks()
+ if v := n.node.Value(); v != nil && IsConcrete(v) && !skip {
for _, v := range n.checks {
// TODO(errors): make Validate return bottom and generate
// optimized conflict message. Also track and inject IDs
diff --git a/internal/core/adt/optional.go b/internal/core/adt/optional.go
index c648431..05568bd 100644
--- a/internal/core/adt/optional.go
+++ b/internal/core/adt/optional.go
@@ -88,7 +88,11 @@
if m == nil {
return false
}
- return m.Match(c, env, f)
+
+ c.inConstraint++
+ ret := m.Match(c, env, f)
+ c.inConstraint--
+ return ret
}
func getMatcher(c *OpContext, env *Environment, v Value) fieldMatcher {