internal/core/eval: hoist vertex processing logic

This will allows making Vertex processing more uniform
and to treat a Vertex as a "soft link". This in turn can fix
several bugs.

Change-Id: Ie4b9a231f0de0a8596f9d1a6d0de2cb1c1ffbd79
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/7744
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
diff --git a/internal/core/eval/eval.go b/internal/core/eval/eval.go
index d323938..3fa7a60 100644
--- a/internal/core/eval/eval.go
+++ b/internal/core/eval/eval.go
@@ -1138,7 +1138,6 @@
 		}
 	}()
 
-outer:
 	switch x := v.Expr().(type) {
 	case adt.Resolver:
 		arc, err := ctx.Resolve(v.Env, x)
@@ -1155,106 +1154,7 @@
 			break
 		}
 
-		// We need to ensure that each arc is only unified once (or at least) a
-		// bounded time, witch each conjunct. Comprehensions, for instance, may
-		// distribute a value across many values that get unified back into the
-		// same value. If such a value is a disjunction, than a disjunction of N
-		// disjuncts will result in a factor N more unifications for each
-		// occurrence of such value, resulting in exponential running time. This
-		// is especially common values that are used as a type.
-		//
-		// However, unification is idempotent, so each such conjunct only needs
-		// to be unified once. This cache checks for this and prevents an
-		// exponential blowup in such case.
-		//
-		// TODO(perf): this cache ensures the conjuncts of an arc at most once
-		// per ID. However, we really need to add the conjuncts of an arc only
-		// once total, and then add the close information once per close ID
-		// (pointer can probably be shared). Aside from being more performant,
-		// this is probably the best way to guarantee that conjunctions are
-		// linear in this case.
-		if n.arcMap == nil {
-			n.arcMap = map[arcKey]bool{}
-		}
-		if n.arcMap[arcKey{arc, v.CloseID}] {
-			return
-		}
-		n.arcMap[arcKey{arc, v.CloseID}] = true
-
-		// Pass detection of structural cycles from parent to children.
-		cyclic := false
-		if v.Env != nil {
-			// If a reference is in a tainted set, so is the value it refers to.
-			cyclic = v.Env.Cyclic
-		}
-
-		status := arc.Status()
-
-		switch status {
-		case adt.Evaluating:
-			// Reference cycle detected. We have reached a fixed point and
-			// adding conjuncts at this point will not change the value. Also,
-			// continuing to pursue this value will result in an infinite loop.
-
-			// TODO: add a mechanism so that the computation will only have to
-			// be done once?
-
-			if arc == n.node {
-				// TODO: we could use node sharing here. This may avoid an
-				// exponential blowup during evaluation, like is possible with
-				// YAML.
-				break outer
-			}
-
-		case adt.EvaluatingArcs:
-			// Structural cycle detected. Continue evaluation as usual, but
-			// keep track of whether any other conjuncts without structural
-			// cycles are added. If not, evaluation of child arcs will end
-			// with this node.
-
-			// For the purpose of determining whether at least one non-cyclic
-			// conjuncts exists, we consider all conjuncts of a cyclic conjuncts
-			// also cyclic.
-
-			cyclic = true
-			n.hasCycle = true
-
-			// As the adt.EvaluatingArcs mechanism bypasses the self-reference
-			// mechanism, we need to separately keep track of it here.
-			// If this (originally) is a self-reference node, adding them
-			// will result in recursively adding the same reference. For this
-			// we also mark the node as evaluating.
-			if arc.SelfCount > 0 {
-				break outer
-			}
-
-			// This count is added for values that are directly added below.
-			// The count is handled separately for delayed values.
-			arc.SelfCount++
-			defer func() { arc.SelfCount-- }()
-		}
-
-		if isDef(v.Expr()) { // should test whether closed, not isDef?
-			c := closedInfo(n.node)
-			closeID = c.InsertDefinition(v.CloseID, x)
-			n.needClose = true // TODO: is this still necessary?
-		}
-
-		// TODO: uncommenting the following almost works, but causes some
-		// faulty results in complex cycle handling between disjunctions.
-		// The reason is that disjunctions must be eliminated if checks in
-		// values on which they depend fail.
-		ctx.Unify(ctx, arc, adt.Finalized)
-
-		for _, c := range arc.Conjuncts {
-			var a []*adt.Vertex
-			if v.Env != nil {
-				a = v.Env.Deref
-			}
-			c = updateCyclic(c, cyclic, arc, a)
-			c.CloseID = closeID
-			n.addExprConjunct(c)
-		}
+		n.addVertexConjuncts(v.Env, v.CloseID, v.Expr(), arc)
 
 	case adt.Evaluator:
 		// adt.Interpolation, adt.UnaryExpr, adt.BinaryExpr, adt.CallExpr
@@ -1286,6 +1186,111 @@
 	}
 }
 
+func (n *nodeContext) addVertexConjuncts(env *adt.Environment, closeID adt.ID, x adt.Expr, arc *adt.Vertex) {
+
+	// We need to ensure that each arc is only unified once (or at least) a
+	// bounded time, witch each conjunct. Comprehensions, for instance, may
+	// distribute a value across many values that get unified back into the
+	// same value. If such a value is a disjunction, than a disjunction of N
+	// disjuncts will result in a factor N more unifications for each
+	// occurrence of such value, resulting in exponential running time. This
+	// is especially common values that are used as a type.
+	//
+	// However, unification is idempotent, so each such conjunct only needs
+	// to be unified once. This cache checks for this and prevents an
+	// exponential blowup in such case.
+	//
+	// TODO(perf): this cache ensures the conjuncts of an arc at most once
+	// per ID. However, we really need to add the conjuncts of an arc only
+	// once total, and then add the close information once per close ID
+	// (pointer can probably be shared). Aside from being more performant,
+	// this is probably the best way to guarantee that conjunctions are
+	// linear in this case.
+	if n.arcMap == nil {
+		n.arcMap = map[arcKey]bool{}
+	}
+	if n.arcMap[arcKey{arc, closeID}] {
+		return
+	}
+	n.arcMap[arcKey{arc, closeID}] = true
+
+	// Pass detection of structural cycles from parent to children.
+	cyclic := false
+	if env != nil {
+		// If a reference is in a tainted set, so is the value it refers to.
+		cyclic = env.Cyclic
+	}
+
+	status := arc.Status()
+
+	switch status {
+	case adt.Evaluating:
+		// Reference cycle detected. We have reached a fixed point and
+		// adding conjuncts at this point will not change the value. Also,
+		// continuing to pursue this value will result in an infinite loop.
+
+		// TODO: add a mechanism so that the computation will only have to
+		// be done once?
+
+		if arc == n.node {
+			// TODO: we could use node sharing here. This may avoid an
+			// exponential blowup during evaluation, like is possible with
+			// YAML.
+			return
+		}
+
+	case adt.EvaluatingArcs:
+		// Structural cycle detected. Continue evaluation as usual, but
+		// keep track of whether any other conjuncts without structural
+		// cycles are added. If not, evaluation of child arcs will end
+		// with this node.
+
+		// For the purpose of determining whether at least one non-cyclic
+		// conjuncts exists, we consider all conjuncts of a cyclic conjuncts
+		// also cyclic.
+
+		cyclic = true
+		n.hasCycle = true
+
+		// As the adt.EvaluatingArcs mechanism bypasses the self-reference
+		// mechanism, we need to separately keep track of it here.
+		// If this (originally) is a self-reference node, adding them
+		// will result in recursively adding the same reference. For this
+		// we also mark the node as evaluating.
+		if arc.SelfCount > 0 {
+			return
+		}
+
+		// This count is added for values that are directly added below.
+		// The count is handled separately for delayed values.
+		arc.SelfCount++
+		defer func() { arc.SelfCount-- }()
+	}
+
+	if isDef(x) { // should test whether closed, not isDef?
+		c := closedInfo(n.node)
+		closeID = c.InsertDefinition(closeID, x)
+		n.needClose = true // TODO: is this still necessary?
+	}
+
+	// TODO: uncommenting the following almost works, but causes some
+	// faulty results in complex cycle handling between disjunctions.
+	// The reason is that disjunctions must be eliminated if checks in
+	// values on which they depend fail.
+	ctx := n.ctx
+	ctx.Unify(ctx, arc, adt.Finalized)
+
+	for _, c := range arc.Conjuncts {
+		var a []*adt.Vertex
+		if env != nil {
+			a = env.Deref
+		}
+		c = updateCyclic(c, cyclic, arc, a)
+		c.CloseID = closeID
+		n.addExprConjunct(c)
+	}
+}
+
 // isDef reports whether an expressions is a reference that references a
 // definition anywhere in its selection path.
 //