internal/core/eval: add more positions for closedness errors

- Keep track of position stack in context.
- Copy the positions on first use.

Change-Id: Ibaab87557353484d62277cd08f7f2f7fc619f9c9
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/6682
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
diff --git a/cue/testdata/definitions/026_combined_definitions.txtar b/cue/testdata/definitions/026_combined_definitions.txtar
index 3cea8a6..5da4669 100644
--- a/cue/testdata/definitions/026_combined_definitions.txtar
+++ b/cue/testdata/definitions/026_combined_definitions.txtar
@@ -118,8 +118,15 @@
 }
 -- out/eval --
 Errors:
-#D4.env: field `b` not allowed
-d1.env: field `c` not allowed
+#D4.env: field `b` not allowed:
+    ./in.cue:27:7
+    ./in.cue:30:1
+    ./in.cue:30:6
+d1.env: field `c` not allowed:
+    ./in.cue:2:1
+    ./in.cue:3:7
+    ./in.cue:4:7
+    ./in.cue:9:17
 
 Result:
 (_|_){
@@ -137,7 +144,11 @@
   d1: (_|_){
     // [eval]
     env: (_|_){
-      // [eval] d1.env: field `c` not allowed
+      // [eval] d1.env: field `c` not allowed:
+      //     ./in.cue:2:1
+      //     ./in.cue:3:7
+      //     ./in.cue:4:7
+      //     ./in.cue:9:17
       a: (string){ "A" }
       b: (string){ "B" }
       c: (string){ "C" }
@@ -160,7 +171,10 @@
   #D4: (_|_){
     // [eval]
     env: (_|_){
-      // [eval] #D4.env: field `b` not allowed
+      // [eval] #D4.env: field `b` not allowed:
+      //     ./in.cue:27:7
+      //     ./in.cue:30:1
+      //     ./in.cue:30:6
       a: (int){ int }
       b: (int){ int }
     }
diff --git a/cue/testdata/definitions/032_definitions_with_embedding.txtar b/cue/testdata/definitions/032_definitions_with_embedding.txtar
index a60d57a..8727fdf 100644
--- a/cue/testdata/definitions/032_definitions_with_embedding.txtar
+++ b/cue/testdata/definitions/032_definitions_with_embedding.txtar
@@ -81,7 +81,11 @@
 }
 -- out/eval --
 Errors:
-#e1.a: field `d` not allowed
+#e1.a: field `d` not allowed:
+    ./in.cue:1:1
+    ./in.cue:2:5
+    ./in.cue:7:5
+    ./in.cue:12:15
 
 Result:
 (_|_){
@@ -101,7 +105,11 @@
   #e1: (_|_){
     // [eval]
     a: (_|_){
-      // [eval] #e1.a: field `d` not allowed
+      // [eval] #e1.a: field `d` not allowed:
+      //     ./in.cue:1:1
+      //     ./in.cue:2:5
+      //     ./in.cue:7:5
+      //     ./in.cue:12:15
       b: (int){ int }
       c: (int){ int }
       d: (int){ 4 }
diff --git "a/cue/testdata/definitions/033_Issue_\043153.txtar" "b/cue/testdata/definitions/033_Issue_\043153.txtar"
index 3b99002..d1bee19 100644
--- "a/cue/testdata/definitions/033_Issue_\043153.txtar"
+++ "b/cue/testdata/definitions/033_Issue_\043153.txtar"
@@ -62,7 +62,11 @@
 }
 -- out/eval --
 Errors:
-listOfCloseds.0: field `b` not allowed
+listOfCloseds.0: field `b` not allowed:
+    ./in.cue:5:1
+    ./in.cue:5:10
+    ./in.cue:14:18
+    ./in.cue:15:20
 
 Result:
 (_|_){
@@ -70,7 +74,11 @@
   listOfCloseds: (_|_){
     // [eval]
     0: (_|_){
-      // [eval] listOfCloseds.0: field `b` not allowed
+      // [eval] listOfCloseds.0: field `b` not allowed:
+      //     ./in.cue:5:1
+      //     ./in.cue:5:10
+      //     ./in.cue:14:18
+      //     ./in.cue:15:20
       a: (int){ |(*(int){ 0 }, (int){ int }) }
       b: (int){ 2 }
     }
@@ -79,7 +87,7 @@
     listOfCloseds: (list){
     }
   }
-  #Closed: (struct){
+  #Closed: (#struct){
     a: (int){ |(*(int){ 0 }, (int){ int }) }
   }
   Junk: (struct){
diff --git a/cue/testdata/definitions/037_closing_with_comprehensions.txtar b/cue/testdata/definitions/037_closing_with_comprehensions.txtar
index 25fc9b3..e71e890 100644
--- a/cue/testdata/definitions/037_closing_with_comprehensions.txtar
+++ b/cue/testdata/definitions/037_closing_with_comprehensions.txtar
@@ -105,8 +105,15 @@
 -- out/eval --
 Errors:
 #D: cannot mix bulk optional fields with dynamic fields, embeddings, or comprehensions within the same struct
-#E: field `f3` not allowed
-a: field `f3` not allowed
+#E: field `f3` not allowed:
+    ./in.cue:1:1
+    ./in.cue:1:5
+    ./in.cue:27:10
+    ./in.cue:28:24
+a: field `f3` not allowed:
+    ./in.cue:1:1
+    ./in.cue:1:5
+    ./in.cue:4:10
 
 Result:
 (_|_){
@@ -126,13 +133,20 @@
     f1: (int){ int }
   }
   #E: (_|_){
-    // [eval] #E: field `f3` not allowed
+    // [eval] #E: field `f3` not allowed:
+    //     ./in.cue:1:1
+    //     ./in.cue:1:5
+    //     ./in.cue:27:10
+    //     ./in.cue:28:24
     f1: (int){ int }
     f2: (int){ int }
     f3: (int){ int }
   }
   a: (_|_){
-    // [eval] a: field `f3` not allowed
+    // [eval] a: field `f3` not allowed:
+    //     ./in.cue:1:1
+    //     ./in.cue:1:5
+    //     ./in.cue:4:10
     f1: (int){ int }
     f2: (int){ int }
     f3: (int){ int }
diff --git a/cue/testdata/definitions/037_conjunction_of_optional_sets.txtar b/cue/testdata/definitions/037_conjunction_of_optional_sets.txtar
index d3a9543..db72e06 100644
--- a/cue/testdata/definitions/037_conjunction_of_optional_sets.txtar
+++ b/cue/testdata/definitions/037_conjunction_of_optional_sets.txtar
@@ -52,8 +52,19 @@
 }
 -- out/eval --
 Errors:
-c: field `aaa` not allowed
-d: field `aaa` not allowed
+c: field `aaa` not allowed:
+    ./in.cue:1:1
+    ./in.cue:1:5
+    ./in.cue:4:1
+    ./in.cue:4:5
+    ./in.cue:9:10
+d: field `aaa` not allowed:
+    ./in.cue:1:1
+    ./in.cue:1:5
+    ./in.cue:4:1
+    ./in.cue:4:5
+    ./in.cue:11:5
+    ./in.cue:12:9
 
 Result:
 (_|_){
@@ -65,13 +76,24 @@
   #C: (#struct){
   }
   c: (_|_){
-    // [eval] c: field `aaa` not allowed
+    // [eval] c: field `aaa` not allowed:
+    //     ./in.cue:1:1
+    //     ./in.cue:1:5
+    //     ./in.cue:4:1
+    //     ./in.cue:4:5
+    //     ./in.cue:9:10
     aaa: (int){ 3 }
   }
   #D: (#struct){
   }
   d: (_|_){
-    // [eval] d: field `aaa` not allowed
+    // [eval] d: field `aaa` not allowed:
+    //     ./in.cue:1:1
+    //     ./in.cue:1:5
+    //     ./in.cue:4:1
+    //     ./in.cue:4:5
+    //     ./in.cue:11:5
+    //     ./in.cue:12:9
     aaa: (int){ 3 }
   }
 }
diff --git a/cue/testdata/definitions/038_continue_recursive_closing_for_optionals.txtar b/cue/testdata/definitions/038_continue_recursive_closing_for_optionals.txtar
index 9c16b91..af888a7 100644
--- a/cue/testdata/definitions/038_continue_recursive_closing_for_optionals.txtar
+++ b/cue/testdata/definitions/038_continue_recursive_closing_for_optionals.txtar
@@ -38,7 +38,10 @@
 }
 -- out/eval --
 Errors:
-a.v: field `b` not allowed
+a.v: field `b` not allowed:
+    ./in.cue:1:1
+    ./in.cue:2:12
+    ./in.cue:5:5
 
 Result:
 (_|_){
@@ -48,7 +51,10 @@
   a: (_|_){
     // [eval]
     v: (_|_){
-      // [eval] a.v: field `b` not allowed
+      // [eval] a.v: field `b` not allowed:
+      //     ./in.cue:1:1
+      //     ./in.cue:2:12
+      //     ./in.cue:5:5
       b: (int){ int }
       a: (int){ int }
     }
diff --git a/cue/testdata/eval/closed_disjunction.txtar b/cue/testdata/eval/closed_disjunction.txtar
index 3691706..78e6872 100644
--- a/cue/testdata/eval/closed_disjunction.txtar
+++ b/cue/testdata/eval/closed_disjunction.txtar
@@ -15,7 +15,10 @@
 }
 -- out/eval --
 Errors:
-b: field `d` not allowed
+b: field `d` not allowed:
+    ./in.cue:1:5
+    ./in.cue:3:35
+    ./in.cue:11:9
 
 Result:
 (_|_){
@@ -27,7 +30,10 @@
     c: (int){ 3 }
   }
   b: (_|_){
-    // [eval] b: field `d` not allowed
+    // [eval] b: field `d` not allowed:
+    //     ./in.cue:1:5
+    //     ./in.cue:3:35
+    //     ./in.cue:11:9
     c: (int){ 3 }
     d: (int){ 4 }
   }
diff --git a/cue/testdata/eval/closedness.txtar b/cue/testdata/eval/closedness.txtar
index 8371eca..2023486 100644
--- a/cue/testdata/eval/closedness.txtar
+++ b/cue/testdata/eval/closedness.txtar
@@ -18,7 +18,11 @@
 }
 -- out/eval --
 Errors:
-a.q: field `e` not allowed
+a.q: field `e` not allowed:
+    ./in.cue:1:1
+    ./in.cue:1:5
+    ./in.cue:6:8
+    ./in.cue:13:8
 
 Result:
 (_|_){
@@ -37,7 +41,11 @@
     // [eval]
     b: (int){ 3 }
     q: (_|_){
-      // [eval] a.q: field `e` not allowed
+      // [eval] a.q: field `e` not allowed:
+      //     ./in.cue:1:1
+      //     ./in.cue:1:5
+      //     ./in.cue:6:8
+      //     ./in.cue:13:8
       c: (int){ 2 }
       d: (int){ int }
       e: (int){ 43 }
diff --git a/cue/testdata/fulleval/035_optionals_with_label_filters.txtar b/cue/testdata/fulleval/035_optionals_with_label_filters.txtar
index 40eda99..9204d37 100644
--- a/cue/testdata/fulleval/035_optionals_with_label_filters.txtar
+++ b/cue/testdata/fulleval/035_optionals_with_label_filters.txtar
@@ -102,9 +102,21 @@
 }
 -- out/eval --
 Errors:
-jobs1: field `foo1` not allowed
-jobs3: field `fooTest1` not allowed
-jobs3.fooTest1: field `cmd` not allowed
+jobs1: field `foo1` not allowed:
+    ./in.cue:6:8
+    ./in.cue:7:2
+    ./in.cue:9:2
+    ./in.cue:16:8
+jobs3: field `fooTest1` not allowed:
+    ./in.cue:6:8
+    ./in.cue:7:2
+    ./in.cue:9:2
+    ./in.cue:22:8
+    ./in.cue:23:8
+jobs3.fooTest1: field `cmd` not allowed:
+    ./in.cue:2:7
+    ./in.cue:6:8
+    ./in.cue:23:18
 jobs2.fooTest.name: invalid value "badName" (out of bound =~"^test"):
     ./in.cue:9:22
 
@@ -124,7 +136,11 @@
     }
   }
   jobs1: (_|_){
-    // [eval] jobs1: field `foo1` not allowed
+    // [eval] jobs1: field `foo1` not allowed:
+    //     ./in.cue:6:8
+    //     ./in.cue:7:2
+    //     ./in.cue:9:2
+    //     ./in.cue:16:8
     foo1: (#struct){
     }
   }
@@ -140,9 +156,17 @@
     }
   }
   jobs3: (_|_){
-    // [eval] jobs3: field `fooTest1` not allowed
+    // [eval] jobs3: field `fooTest1` not allowed:
+    //     ./in.cue:6:8
+    //     ./in.cue:7:2
+    //     ./in.cue:9:2
+    //     ./in.cue:22:8
+    //     ./in.cue:23:8
     fooTest1: (_|_){
-      // [eval] jobs3.fooTest1: field `cmd` not allowed
+      // [eval] jobs3.fooTest1: field `cmd` not allowed:
+      //     ./in.cue:2:7
+      //     ./in.cue:6:8
+      //     ./in.cue:23:18
       name: (string){ "badName" }
       cmd: (string){ string }
     }
diff --git a/cue/testdata/resolve/025_definitions.txtar b/cue/testdata/resolve/025_definitions.txtar
index d2eab90..9bb0833 100644
--- a/cue/testdata/resolve/025_definitions.txtar
+++ b/cue/testdata/resolve/025_definitions.txtar
@@ -119,8 +119,14 @@
 }
 -- out/eval --
 Errors:
-foo: field `feild` not allowed
-foo1.recursive: field `feild` not allowed
+foo: field `feild` not allowed:
+    ./in.cue:1:1
+    ./in.cue:1:7
+    ./in.cue:13:6
+foo1.recursive: field `feild` not allowed:
+    ./in.cue:1:1
+    ./in.cue:3:13
+    ./in.cue:18:13
 
 Result:
 (_|_){
@@ -136,7 +142,10 @@
     field2: (string){ string }
   }
   foo: (_|_){
-    // [eval] foo: field `feild` not allowed
+    // [eval] foo: field `feild` not allowed:
+    //     ./in.cue:1:1
+    //     ./in.cue:1:7
+    //     ./in.cue:13:6
     field: (int){ int }
     recursive: (#struct){
       field: (string){ string }
@@ -147,7 +156,10 @@
     // [eval]
     field: (int){ 2 }
     recursive: (_|_){
-      // [eval] foo1.recursive: field `feild` not allowed
+      // [eval] foo1.recursive: field `feild` not allowed:
+      //     ./in.cue:1:1
+      //     ./in.cue:3:13
+      //     ./in.cue:18:13
       field: (string){ string }
       feild: (int){ 2 }
     }
diff --git a/cue/testdata/resolve/029_non-closed_definition_carries_over_closedness_to_enclosed_template.txtar b/cue/testdata/resolve/029_non-closed_definition_carries_over_closedness_to_enclosed_template.txtar
index 09f65bf..69be4d5 100644
--- a/cue/testdata/resolve/029_non-closed_definition_carries_over_closedness_to_enclosed_template.txtar
+++ b/cue/testdata/resolve/029_non-closed_definition_carries_over_closedness_to_enclosed_template.txtar
@@ -104,9 +104,18 @@
 }
 -- out/eval --
 Errors:
-a.v: field `b` not allowed
-b.w: field `c` not allowed
-c.w.0: field `d` not allowed
+a.v: field `b` not allowed:
+    ./in.cue:1:1
+    ./in.cue:2:12
+    ./in.cue:5:5
+b.w: field `c` not allowed:
+    ./in.cue:7:1
+    ./in.cue:8:23
+    ./in.cue:11:5
+c.w.0: field `d` not allowed:
+    ./in.cue:13:1
+    ./in.cue:14:13
+    ./in.cue:17:6
 
 Result:
 (_|_){
@@ -116,7 +125,10 @@
   a: (_|_){
     // [eval]
     v: (_|_){
-      // [eval] a.v: field `b` not allowed
+      // [eval] a.v: field `b` not allowed:
+      //     ./in.cue:1:1
+      //     ./in.cue:2:12
+      //     ./in.cue:5:5
       b: (int){ int }
       a: (int){ int }
     }
@@ -126,7 +138,10 @@
   b: (_|_){
     // [eval]
     w: (_|_){
-      // [eval] b.w: field `c` not allowed
+      // [eval] b.w: field `c` not allowed:
+      //     ./in.cue:7:1
+      //     ./in.cue:8:23
+      //     ./in.cue:11:5
       c: (int){ int }
       b: (int){ int }
     }
@@ -138,7 +153,10 @@
     w: (_|_){
       // [eval]
       0: (_|_){
-        // [eval] c.w.0: field `d` not allowed
+        // [eval] c.w.0: field `d` not allowed:
+        //     ./in.cue:13:1
+        //     ./in.cue:14:13
+        //     ./in.cue:17:6
         d: (int){ int }
         a: (int){ int }
       }
diff --git a/cue/testdata/resolve/030_definitions_with_disjunctions.txtar b/cue/testdata/resolve/030_definitions_with_disjunctions.txtar
index 84774b6..d21c4cd 100644
--- a/cue/testdata/resolve/030_definitions_with_disjunctions.txtar
+++ b/cue/testdata/resolve/030_definitions_with_disjunctions.txtar
@@ -61,7 +61,10 @@
 }
 -- out/eval --
 Errors:
-bar: field `c` not allowed
+bar: field `c` not allowed:
+    ./in.cue:1:7
+    ./in.cue:5:2
+    ./in.cue:12:6
 
 Result:
 (_|_){
@@ -78,7 +81,10 @@
     a: (int){ 1 }
   }
   bar: (_|_){
-    // [eval] bar: field `c` not allowed
+    // [eval] bar: field `c` not allowed:
+    //     ./in.cue:1:7
+    //     ./in.cue:5:2
+    //     ./in.cue:12:6
     field: (int){ int }
     c: (int){ 2 }
     b: (int){ 2 }
diff --git a/cue/testdata/resolve/035_excluded_embedding_from_closing.txtar b/cue/testdata/resolve/035_excluded_embedding_from_closing.txtar
index 4aa95d6..e719e07 100644
--- a/cue/testdata/resolve/035_excluded_embedding_from_closing.txtar
+++ b/cue/testdata/resolve/035_excluded_embedding_from_closing.txtar
@@ -64,8 +64,14 @@
 }
 -- out/eval --
 Errors:
-V.b: field `extra` not allowed
-V.c: field `e` not allowed
+V.b: field `extra` not allowed:
+    ./in.cue:1:5
+    ./in.cue:6:10
+    ./in.cue:11:5
+V.c: field `e` not allowed:
+    ./in.cue:1:5
+    ./in.cue:4:6
+    ./in.cue:10:5
 
 Result:
 (_|_){
@@ -84,7 +90,10 @@
   V: (_|_){
     // [eval]
     c: (_|_){
-      // [eval] V.c: field `e` not allowed
+      // [eval] V.c: field `e` not allowed:
+      //     ./in.cue:1:5
+      //     ./in.cue:4:6
+      //     ./in.cue:10:5
       d: (int){ int }
       e: (int){ int }
     }
@@ -92,7 +101,10 @@
       c: (int){ int }
     }
     b: (_|_){
-      // [eval] V.b: field `extra` not allowed
+      // [eval] V.b: field `extra` not allowed:
+      //     ./in.cue:1:5
+      //     ./in.cue:6:10
+      //     ./in.cue:11:5
       open: (int){ int }
       extra: (int){ int }
     }
diff --git a/internal/core/adt/context.go b/internal/core/adt/context.go
index 9d8f3af..1a26c5c 100644
--- a/internal/core/adt/context.go
+++ b/internal/core/adt/context.go
@@ -107,9 +107,10 @@
 type OpContext struct {
 	config
 
-	e    *Environment
-	src  ast.Node
-	errs *Bottom
+	e         *Environment
+	src       ast.Node
+	errs      *Bottom
+	positions []Node // keep track of error positions
 
 	// vertex is used to determine the path location in case of error. Turning
 	// this into a stack could also allow determining the cyclic path for
diff --git a/internal/core/adt/errors.go b/internal/core/adt/errors.go
index ec73924..4736dbb 100644
--- a/internal/core/adt/errors.go
+++ b/internal/core/adt/errors.go
@@ -203,9 +203,10 @@
 
 // A valueError is returned as a result of evaluating a value.
 type valueError struct {
-	r   Runtime
-	v   *Vertex
-	pos []token.Pos
+	r      Runtime
+	v      *Vertex
+	pos    token.Pos
+	auxpos []token.Pos
 	errors.Message
 }
 
@@ -213,17 +214,45 @@
 	return c.vertex
 }
 
+// MarkPositions marks the current position stack.
+func (c *OpContext) MarkPositions() int {
+	return len(c.positions)
+}
+
+// ReleasePositions sets the position state to one from a call to MarkPositions.
+func (c *OpContext) ReleasePositions(p int) {
+	c.positions = c.positions[:p]
+}
+
+func (c *OpContext) AddPosition(n Node) {
+	c.positions = append(c.positions, n)
+}
+
 func (c *OpContext) Newf(format string, args ...interface{}) *valueError {
 	return c.NewPosf(c.pos(), format, args...)
 }
 
-func (c *OpContext) NewPosf(pos token.Pos, format string, args ...interface{}) *valueError {
+func (c *OpContext) NewPosf(p token.Pos, format string, args ...interface{}) *valueError {
+	var a []token.Pos
+	if len(c.positions) > 0 {
+		a = make([]token.Pos, 0, len(c.positions))
+		for _, n := range c.positions {
+			if p := pos(n); p != token.NoPos {
+				a = append(a, p)
+			} else if v, ok := n.(*Vertex); ok {
+				for _, c := range v.Conjuncts {
+					if p := pos(c.x); p != token.NoPos {
+						a = append(a, p)
+					}
+				}
+			}
+		}
+	}
 	return &valueError{
-		r: c.Runtime,
-		v: c.errNode(),
-		// TODO: leave ni if it can be derived from the source and save an
-		// allocation.
-		pos:     []token.Pos{pos},
+		r:       c.Runtime,
+		v:       c.errNode(),
+		pos:     p,
+		auxpos:  a,
 		Message: errors.NewMessage(format, args),
 	}
 }
@@ -233,15 +262,11 @@
 }
 
 func (e *valueError) Position() token.Pos {
-	if len(e.pos) == 0 {
-		// TODO: retrieve from source
-		return token.NoPos
-	}
-	return e.pos[0]
+	return e.pos
 }
 
-func (e *valueError) InputPositions() []token.Pos {
-	return e.pos
+func (e *valueError) InputPositions() (a []token.Pos) {
+	return e.auxpos
 }
 
 func (e *valueError) Path() (a []string) {
diff --git a/internal/core/eval/closed.go b/internal/core/eval/closed.go
index 80f6ed8..5c9ae62 100644
--- a/internal/core/eval/closed.go
+++ b/internal/core/eval/closed.go
@@ -114,7 +114,7 @@
 // Acceptor. This could be implemented as an allocation-free wrapper type around
 // a Disjunction. This will require a bit more API cleaning, though.
 func newDisjunctionAcceptor(x *adt.Disjunction) adt.Acceptor {
-	tree := &CloseDef{}
+	tree := &CloseDef{Src: x}
 	sets := []fieldSet{}
 	for _, d := range x.Values {
 		if a, _ := d.Closed.(*acceptor); a != nil {
@@ -142,6 +142,7 @@
 // of the set of allowed fields in that case and the embeddings will not add
 // any value.
 type CloseDef struct {
+	Src   adt.Node // for error reporting
 	ID    uint32
 	IsAnd bool
 	List  []*CloseDef
@@ -216,7 +217,7 @@
 		return buf[0]
 	}
 
-	return &CloseDef{ID: c.ID, IsAnd: c.IsAnd, List: buf[:k]}
+	return &CloseDef{Src: c.Src, ID: c.ID, IsAnd: c.IsAnd, List: buf[:k]}
 }
 
 // UpdateReplace is called after evaluating a conjunct at the top of the arc
@@ -313,6 +314,8 @@
 // "and" semantics of conjunctions. It generates an error if a field is not
 // allowed.
 func (n *acceptor) verifyArcAllowed(ctx *adt.OpContext, f adt.Feature) *adt.Bottom {
+	defer ctx.ReleasePositions(ctx.MarkPositions())
+
 	// TODO(perf): this will also generate a message for f == 0. Do something
 	// more clever and only generate this when it is a user error.
 	filter := f.IsString() || f == adt.InvalidLabel
@@ -367,5 +370,20 @@
 			}
 		}
 	}
+	collectPositions(ctx, n.tree)
+	for _, o := range n.fields {
+		if o.pos != nil {
+			ctx.AddPosition(o.pos)
+		}
+	}
 	return false
 }
+
+func collectPositions(ctx *adt.OpContext, c *CloseDef) {
+	if c.Src != nil {
+		ctx.AddPosition(c.Src)
+	}
+	for _, x := range c.List {
+		collectPositions(ctx, x)
+	}
+}
diff --git a/internal/core/eval/eval.go b/internal/core/eval/eval.go
index 7cbe503..a912c4b 100644
--- a/internal/core/eval/eval.go
+++ b/internal/core/eval/eval.go
@@ -188,6 +188,7 @@
 				Parent: v.Parent,
 				Value:  &adt.StructMarker{},
 				Arcs:   arcs,
+				Closed: result.Closed,
 			}
 		}
 		// *v = save // DO NOT ADD.
@@ -551,6 +552,34 @@
 		}
 	}
 
+	n.updateClosedInfo()
+
+	if cyclic := n.hasCycle && !n.hasNonCycle; cyclic {
+		n.node.Value = adt.CombineErrors(nil, n.node.Value, &adt.Bottom{
+			Code:  adt.StructuralCycleError,
+			Err:   ctx.Newf("structural cycle"),
+			Value: n.node.Value,
+			// TODO: probably, this should have the referenced arc.
+		})
+	} else {
+		// Visit arcs recursively to validate and compute error.
+		for _, a := range n.node.Arcs {
+			// Call UpdateStatus here to be absolutely sure the status is set
+			// correctly and that we are not regressing.
+			n.node.UpdateStatus(adt.EvaluatingArcs)
+			n.eval.Unify(ctx, a, adt.Finalized)
+			if err, _ := a.Value.(*adt.Bottom); err != nil {
+				n.node.AddChildError(err)
+			}
+		}
+	}
+
+	n.node.UpdateStatus(adt.Finalized)
+}
+
+func (n *nodeContext) updateClosedInfo() {
+	ctx := n.ctx
+
 	var c *CloseDef
 	if a, _ := n.node.Closed.(*acceptor); a != nil {
 		c = a.tree
@@ -559,7 +588,7 @@
 
 	updated := updateClosed(c, n.replace)
 	if updated == nil && n.needClose {
-		updated = &CloseDef{}
+		updated = &CloseDef{Src: n.node}
 	}
 
 	// TODO retrieve from env.
@@ -584,8 +613,6 @@
 		n.node.Closed = m
 	}
 
-	cyclic := n.hasCycle && !n.hasNonCycle
-
 	// Visit arcs recursively to validate and compute error.
 	for _, a := range n.node.Arcs {
 		if updated != nil {
@@ -605,27 +632,7 @@
 			// or at least don't record recursive error.
 			// continue
 		}
-		// Call UpdateStatus here to be absolutely sure the status is set
-		// correctly and that we are not regressing.
-		if !cyclic {
-			n.node.UpdateStatus(adt.EvaluatingArcs)
-			n.eval.Unify(ctx, a, adt.Finalized)
-			if err, _ := a.Value.(*adt.Bottom); err != nil {
-				n.node.AddChildError(err)
-			}
-		}
 	}
-
-	if cyclic {
-		n.node.Value = adt.CombineErrors(nil, n.node.Value, &adt.Bottom{
-			Code:  adt.StructuralCycleError,
-			Err:   ctx.Newf("structural cycle"),
-			Value: n.node.Value,
-			// TODO: probably, this should have the referenced arc.
-		})
-	}
-
-	n.node.UpdateStatus(adt.Finalized)
 }
 
 // TODO: this is now a sentinel. Use a user-facing error that traces where
@@ -1112,7 +1119,7 @@
 	current, n.newClose = n.newClose, current
 
 	if current == nil {
-		current = &CloseDef{ID: id}
+		current = &CloseDef{ID: id, Src: arc}
 	}
 	n.addAnd(current)
 }
@@ -1315,7 +1322,7 @@
 
 	var hasOther, hasBulk adt.Node
 
-	opt := fieldSet{env: childEnv}
+	opt := fieldSet{pos: s, env: childEnv}
 
 	for _, d := range s.Decls {
 		switch x := d.(type) {
@@ -1352,7 +1359,7 @@
 			current, n.newClose = n.newClose, current
 
 			if current == nil {
-				current = &CloseDef{ID: id} // TODO: isClosed?
+				current = &CloseDef{Src: s, ID: id} // TODO: isClosed?
 			} else {
 				// n.needClose = true
 			}
@@ -1678,7 +1685,7 @@
 			continue
 		}
 
-		f := fieldSet{env: l.env}
+		f := fieldSet{pos: l.list, env: l.env}
 		f.AddEllipsis(c, l.elipsis)
 
 		n.optionals = append(n.optionals, f)
diff --git a/internal/core/eval/optionals.go b/internal/core/eval/optionals.go
index 6b17a8d..3dc1884 100644
--- a/internal/core/eval/optionals.go
+++ b/internal/core/eval/optionals.go
@@ -21,6 +21,8 @@
 // fieldSet represents the fields for a single struct literal, along
 // the constraints of fields that may be added.
 type fieldSet struct {
+	pos adt.Node
+
 	// TODO: look at consecutive identical environments to figure out
 	// what belongs to same definition?
 	env *adt.Environment