tools/trim: implement using adt

The data structures (adt) of the new evaluator make
implementing trim a lot easier than it was before.

The new evaluator tracks all conjuncts in a Vertex.
It now also marks whether a value originated from
a comprehension, definition, embedding, etc. This
means that the trim implemenation no longer needs to
maintain parallel data structures, which is hard and
error prone.

It also allows the right level of pre-evaluation,
so that the right values can be used to determine
whether a field is subsumed. This was hard in the
old implementation.

Ultimately it would be good if these kind of tools
could be build on a public API, but let's first try
to understand, building tools like trim, what would
be a good API.

Fixes #643
Fixes #626
Fixes #558
Closes #548

This fixes several other issues that were noted in
the tests and issues that were not explicitly marked
as issues.

Change-Id: I2a8cf6933288fbfa427792e536fbca030a04b3b2
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/8238
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Paul Jolly <paul@myitcv.org.uk>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/testdata/script/trim.txt b/cmd/cue/cmd/testdata/script/trim.txt
index b5f0cd5..78468eb 100644
--- a/cmd/cue/cmd/testdata/script/trim.txt
+++ b/cmd/cue/cmd/testdata/script/trim.txt
@@ -44,11 +44,9 @@
 	}
 
 	t: u: {
-		x: 5
 	}
 }
 
-// TODO: top-level fields are currently not removed.
 group: {
 	for k, v in foo {
 		comp: "\(k)": v
@@ -58,7 +56,7 @@
 		aa: 8 // new value
 	}
 
-	comp: baz: {} // removed: fully implied by comprehension above
+	comp: {} // TODO: remove: implied by comprehension above
 }
 -- trim/trim.cue --
 package trim
@@ -93,7 +91,7 @@
 	b: "foo"
 	c: 45
 	e: string
-	f: ">> here <<"  // TODO: remove
+	f: ">> here <<"
 
 	// 5 is an integer, so this can be removed.
 	n: int
@@ -121,7 +119,6 @@
 	}
 }
 
-// TODO: top-level fields are currently not removed.
 group: {
 	for k, v in foo {
 		comp: "\(k)": v
@@ -132,6 +129,6 @@
 		aa: 8 // new value
 	}
 
-	comp: baz: {} // removed: fully implied by comprehension above
+	comp: baz: {} // TODO: remove: implied by comprehension above
 }
 -- cue.mod --
diff --git a/doc/tutorial/kubernetes/quick/services/frontend/bartender/kube.cue b/doc/tutorial/kubernetes/quick/services/frontend/bartender/kube.cue
index 2173264..8570398 100644
--- a/doc/tutorial/kubernetes/quick/services/frontend/bartender/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/frontend/bartender/kube.cue
@@ -1,6 +1,5 @@
 package kube
 
-service: {}
 deployment: bartender: spec: template: spec: containers: [{
 	image: "gcr.io/myproj/bartender:v0.1.34"
 	args: [
diff --git a/doc/tutorial/kubernetes/quick/services/frontend/breaddispatcher/kube.cue b/doc/tutorial/kubernetes/quick/services/frontend/breaddispatcher/kube.cue
index 18bbcfd..6e06a6a 100644
--- a/doc/tutorial/kubernetes/quick/services/frontend/breaddispatcher/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/frontend/breaddispatcher/kube.cue
@@ -1,6 +1,5 @@
 package kube
 
-service: {}
 deployment: breaddispatcher: spec: template: spec: containers: [{
 	image: "gcr.io/myproj/breaddispatcher:v0.3.24"
 	args: [
diff --git a/doc/tutorial/kubernetes/quick/services/frontend/host/kube.cue b/doc/tutorial/kubernetes/quick/services/frontend/host/kube.cue
index 727349d..e58c822 100644
--- a/doc/tutorial/kubernetes/quick/services/frontend/host/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/frontend/host/kube.cue
@@ -1,6 +1,5 @@
 package kube
 
-service: {}
 deployment: host: spec: {
 	replicas: 2
 	template: spec: containers: [{
diff --git a/doc/tutorial/kubernetes/quick/services/frontend/maitred/kube.cue b/doc/tutorial/kubernetes/quick/services/frontend/maitred/kube.cue
index 8659935..4829e95 100644
--- a/doc/tutorial/kubernetes/quick/services/frontend/maitred/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/frontend/maitred/kube.cue
@@ -1,6 +1,5 @@
 package kube
 
-service: {}
 deployment: maitred: spec: template: spec: containers: [{
 	image: "gcr.io/myproj/maitred:v0.0.4"
 	args: [
diff --git a/doc/tutorial/kubernetes/quick/services/frontend/waiter/kube.cue b/doc/tutorial/kubernetes/quick/services/frontend/waiter/kube.cue
index 5c30c2e..7a2c1de 100644
--- a/doc/tutorial/kubernetes/quick/services/frontend/waiter/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/frontend/waiter/kube.cue
@@ -1,6 +1,5 @@
 package kube
 
-service: {}
 deployment: waiter: spec: {
 	replicas: 5
 	template: spec: containers: [{
diff --git a/doc/tutorial/kubernetes/quick/services/infra/download/kube.cue b/doc/tutorial/kubernetes/quick/services/infra/download/kube.cue
index a54a89b..a7860ae 100644
--- a/doc/tutorial/kubernetes/quick/services/infra/download/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/infra/download/kube.cue
@@ -1,6 +1,5 @@
 package kube
 
-service: {}
 deployment: download: spec: template: spec: containers: [{
 	image: "gcr.io/myproj/download:v0.0.2"
 	ports: [{
diff --git a/doc/tutorial/kubernetes/quick/services/infra/updater/kube.cue b/doc/tutorial/kubernetes/quick/services/infra/updater/kube.cue
index e72b31e..ca7b848 100644
--- a/doc/tutorial/kubernetes/quick/services/infra/updater/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/infra/updater/kube.cue
@@ -1,6 +1,5 @@
 package kube
 
-service: {}
 deployment: updater: spec: template: spec: {
 	volumes: [{
 		name: "secret-updater"
diff --git a/doc/tutorial/kubernetes/quick/services/kitchen/caller/kube.cue b/doc/tutorial/kubernetes/quick/services/kitchen/caller/kube.cue
index 779f801..ddd728b 100644
--- a/doc/tutorial/kubernetes/quick/services/kitchen/caller/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/kitchen/caller/kube.cue
@@ -1,6 +1,5 @@
 package kube
 
-service: {}
 deployment: caller: spec: {
 	replicas: 3
 	template: spec: {
diff --git a/doc/tutorial/kubernetes/quick/services/kitchen/dishwasher/kube.cue b/doc/tutorial/kubernetes/quick/services/kitchen/dishwasher/kube.cue
index 8b37f43..49430a4 100644
--- a/doc/tutorial/kubernetes/quick/services/kitchen/dishwasher/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/kitchen/dishwasher/kube.cue
@@ -1,6 +1,5 @@
 package kube
 
-service: {}
 deployment: dishwasher: spec: {
 	replicas: 5
 	template: spec: {
diff --git a/doc/tutorial/kubernetes/quick/services/kitchen/expiditer/kube.cue b/doc/tutorial/kubernetes/quick/services/kitchen/expiditer/kube.cue
index 689e480..c152000 100644
--- a/doc/tutorial/kubernetes/quick/services/kitchen/expiditer/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/kitchen/expiditer/kube.cue
@@ -1,6 +1,5 @@
 package kube
 
-service: {}
 deployment: expiditer: spec: template: spec: containers: [{
 	image: "gcr.io/myproj/expiditer:v0.5.34"
 	args: [
diff --git a/doc/tutorial/kubernetes/quick/services/kitchen/headchef/kube.cue b/doc/tutorial/kubernetes/quick/services/kitchen/headchef/kube.cue
index fd00407..983d689 100644
--- a/doc/tutorial/kubernetes/quick/services/kitchen/headchef/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/kitchen/headchef/kube.cue
@@ -1,6 +1,5 @@
 package kube
 
-service: {}
 deployment: headchef: spec: template: spec: containers: [{
 	image: "gcr.io/myproj/headchef:v0.2.16"
 	volumeMounts: [{
diff --git a/doc/tutorial/kubernetes/quick/services/kitchen/linecook/kube.cue b/doc/tutorial/kubernetes/quick/services/kitchen/linecook/kube.cue
index 862e094..179b993 100644
--- a/doc/tutorial/kubernetes/quick/services/kitchen/linecook/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/kitchen/linecook/kube.cue
@@ -1,6 +1,5 @@
 package kube
 
-service: {}
 deployment: linecook: spec: template: spec: {
 	volumes: [{
 	}, {
diff --git a/doc/tutorial/kubernetes/quick/services/kitchen/pastrychef/kube.cue b/doc/tutorial/kubernetes/quick/services/kitchen/pastrychef/kube.cue
index 612e679..3810eda 100644
--- a/doc/tutorial/kubernetes/quick/services/kitchen/pastrychef/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/kitchen/pastrychef/kube.cue
@@ -1,6 +1,5 @@
 package kube
 
-service: {}
 deployment: pastrychef: spec: template: spec: {
 	volumes: [{
 	}, {
diff --git a/doc/tutorial/kubernetes/quick/services/kitchen/souschef/kube.cue b/doc/tutorial/kubernetes/quick/services/kitchen/souschef/kube.cue
index d39e055..3d035f4 100644
--- a/doc/tutorial/kubernetes/quick/services/kitchen/souschef/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/kitchen/souschef/kube.cue
@@ -1,6 +1,5 @@
 package kube
 
-service: {}
 deployment: souschef: spec: template: spec: containers: [{
 	image: "gcr.io/myproj/souschef:v0.5.3"
 }]
diff --git a/doc/tutorial/kubernetes/quick/services/proxy/authproxy/service.cue b/doc/tutorial/kubernetes/quick/services/proxy/authproxy/service.cue
index eb6a6e0..f84e1e4 100644
--- a/doc/tutorial/kubernetes/quick/services/proxy/authproxy/service.cue
+++ b/doc/tutorial/kubernetes/quick/services/proxy/authproxy/service.cue
@@ -1,3 +1 @@
 package kube
-
-service: {}
diff --git a/internal/core/adt/closed.go b/internal/core/adt/closed.go
index 6e978f3..acf50ac 100644
--- a/internal/core/adt/closed.go
+++ b/internal/core/adt/closed.go
@@ -96,6 +96,34 @@
 	IsClosed bool
 }
 
+func (c CloseInfo) Location() Node {
+	if c.closeInfo == nil {
+		return nil
+	}
+	return c.closeInfo.location
+}
+
+func (c CloseInfo) SpanMask() SpanType {
+	if c.closeInfo == nil {
+		return 0
+	}
+	return c.span
+}
+
+func (c CloseInfo) RootSpanType() SpanType {
+	if c.closeInfo == nil {
+		return 0
+	}
+	return c.root
+}
+
+func (c CloseInfo) IsInOneOf(t SpanType) bool {
+	if c.closeInfo == nil {
+		return false
+	}
+	return c.span&t != 0
+}
+
 // TODO(perf): remove: error positions should always be computed on demand
 // in dedicated error types.
 func (c *CloseInfo) AddPositions(ctx *OpContext) {
@@ -118,6 +146,7 @@
 		parent:   c.closeInfo,
 		location: x,
 		mode:     closeEmbed,
+		root:     EmbeddingSpan,
 		span:     span | EmbeddingSpan,
 	}
 	return c
@@ -152,6 +181,7 @@
 	c.closeInfo = &closeInfo{
 		parent:   c.closeInfo,
 		location: x,
+		root:     t,
 		span:     span | t,
 	}
 	return c
@@ -169,6 +199,7 @@
 	}
 	if isDef {
 		c.mode = closeDef
+		c.closeInfo.root = DefinitionSpan
 		c.closeInfo.span |= DefinitionSpan
 	}
 	return c
@@ -228,6 +259,7 @@
 	//  - it is a sibling of a new definition.
 	noCheck bool // don't process for inclusion info
 
+	root SpanType
 	span SpanType
 }
 
diff --git a/tools/trim/testdata/definitions.txtar b/tools/trim/testdata/definitions.txtar
new file mode 100644
index 0000000..d376569
--- /dev/null
+++ b/tools/trim/testdata/definitions.txtar
@@ -0,0 +1,20 @@
+#Issue #558
+
+-- in.cue --
+#Foo: {
+	bar: int
+	baz: 2
+}
+foo: #Foo & {
+	bar: 1
+	baz: 2
+}
+-- out/trim --
+== in.cue
+#Foo: {
+	bar: int
+	baz: 2
+}
+foo: #Foo & {
+	bar: 1
+}
diff --git a/tools/trim/trim.go b/tools/trim/trim.go
index 4af47a4..17e164c 100644
--- a/tools/trim/trim.go
+++ b/tools/trim/trim.go
@@ -12,21 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// Package trim removes definitions that may be inferred from
-// templates.
+// Package trim removes fields that may be inferred from another mixed in value
+// that "dominates" it. For instance, a value that is merged in from a
+// definition is considered to dominate a value from a regular struct that
+// mixes in this definition. Values derived from constraints and comprehensions
+// can also dominate other fields.
 //
-// A field, struct, or list is removed if it is implied by a constraint, such
-// as from an optional field matching a required field, a list type value,
-// a comprehension or any other implied content. It will modify the files in place.
+// A value A is considered to be implied by a value B if A subsumes the default
+// value of B. For instance, if a definition defines a field `a: *1 | int` and
+// mixed in with a struct that defines a field `a: 1 | 2`, then the latter can
+// be removed because a definition field dominates a regular field and because
+// the latter subsumes the default value of the former.
 //
-// Limitations
-//
-// Removal is on a best effort basis. Some caveats:
-// - Fields in implied content may refer to fields within the struct in which
-//   they are included, but are only resolved on a best-effort basis.
-// - Disjunctions that contain structs in implied content cannot be used to
-//   remove fields.
-// - There is currently no verification step: manual verification is required.
 //
 // Examples:
 //
@@ -44,11 +41,7 @@
 //
 // Results in:
 //
-// 	light: [string]: {
-// 		room:          string
-// 		brightnessOff: *0.0 | >=0 & <=100.0
-// 		brightnessOn:  *100.0 | >=0 & <=100.0
-// 	}
+// 	// Unmodified: light: [string]: { ... }
 //
 // 	light: ceiling50: {
 // 		room: "MasterBedroom"
@@ -57,25 +50,15 @@
 package trim
 
 import (
-	"bytes"
-	"fmt"
-	"strconv"
-	"strings"
-
-	"cuelang.org/go/cue/ast"
-	"cuelang.org/go/cue/token"
-	"cuelang.org/go/internal"
-
-	// "cuelang.org/go/cue"
 	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/ast/astutil"
+	"cuelang.org/go/internal"
+	"cuelang.org/go/internal/core/adt"
+	"cuelang.org/go/internal/core/runtime"
+	"cuelang.org/go/internal/core/subsume"
 )
 
-type Runtime = cue.Runtime
-
-// TODO:
-// - remove the limitations mentioned in the documentation
-// - implement verification post-processing as extra safety
-
 // Config configures trim options.
 type Config struct {
 	Trace bool
@@ -86,443 +69,141 @@
 // Trimming is done on a best-effort basis and only when the removed field
 // is clearly implied by another field, rather than equal sibling fields.
 func Files(files []*ast.File, inst *cue.Instance, cfg *Config) error {
-	gen := newTrimSet(cfg)
-	gen.files = files
-	for _, f := range files {
-		gen.markNodes(f)
+	rx, vx := internal.CoreValue(inst.Value())
+	r := rx.(*runtime.Runtime)
+	v := vx.(*adt.Vertex)
+
+	t := &trimmer{
+		Config: *cfg,
+		ctx:    adt.NewContext(r, v),
+		remove: map[ast.Node]bool{},
 	}
 
-	root := inst.Lookup()
-	rm := gen.trim("root", root, cue.Value{}, root)
+	t.findSubordinates(v)
 
+	// Remove subordinate values from files.
 	for _, f := range files {
-		f.Decls = gen.trimDecls(f.Decls, rm, cue.Value{}, true)
+		astutil.Apply(f, func(c astutil.Cursor) bool {
+			if f, ok := c.Node().(*ast.Field); ok && t.remove[f.Value] {
+				c.Delete()
+			}
+			return true
+		}, nil)
 	}
+
 	return nil
 }
 
-type trimSet struct {
-	cfg       Config
-	files     []*ast.File
-	stack     []string
-	exclude   map[ast.Node]bool // don't remove fields marked here
-	alwaysGen map[ast.Node]bool // node is always from a generated source
-	fromComp  map[ast.Node]bool // node originated from a comprehension
+type trimmer struct {
+	Config
+
+	ctx    *adt.OpContext
+	remove map[ast.Node]bool
 }
 
-func newTrimSet(cfg *Config) *trimSet {
-	if cfg == nil {
-		cfg = &Config{}
-	}
-	return &trimSet{
-		cfg:       *cfg,
-		exclude:   map[ast.Node]bool{},
-		alwaysGen: map[ast.Node]bool{},
-		fromComp:  map[ast.Node]bool{},
+func (t *trimmer) markRemove(c adt.Conjunct) {
+	if src := c.Expr().Source(); src != nil {
+		t.remove[src] = true
 	}
 }
 
-func (t *trimSet) path() string {
-	return strings.Join(t.stack[1:], " ")
+const dominatorNode = adt.ComprehensionSpan | adt.DefinitionSpan | adt.ConstraintSpan
+
+func isDominator(c adt.Conjunct) bool {
+	return c.CloseInfo.IsInOneOf(dominatorNode)
 }
 
-func (t *trimSet) traceMsg(msg string) {
-	if t.cfg.Trace {
-		fmt.Print(t.path())
-		msg = strings.TrimRight(msg, "\n")
-		msg = strings.Replace(msg, "\n", "\n    ", -1)
-		fmt.Printf(": %s\n", msg)
-	}
-}
-
-func (t *trimSet) markNodes(n ast.Node) {
-	ast.Walk(n, nil, func(n ast.Node) {
-		switch x := n.(type) {
-		case *ast.Ident:
-			if x.Node != nil {
-				t.exclude[x.Node] = true
-			}
-			if x.Scope != nil {
-				t.exclude[x.Scope] = true
-			}
-
-		case *ast.ListLit:
-			_, e := internal.ListEllipsis(x)
-			if e != nil && e.Type != nil {
-				t.markAlwaysGen(e.Type, false)
-			}
-
-		case *ast.Field:
-			switch x.Label.(type) {
-			case *ast.TemplateLabel, *ast.ListLit:
-				t.markAlwaysGen(x.Value, false)
-			}
-
-		case *ast.ListComprehension, *ast.Comprehension:
-			t.markAlwaysGen(x, true)
-		}
-	})
-}
-
-func (t *trimSet) markAlwaysGen(first ast.Node, isComp bool) {
-	ast.Walk(first, func(n ast.Node) bool {
-		if t.alwaysGen[n] {
-			return false
-		}
-		t.alwaysGen[n] = true
-		if isComp {
-			t.fromComp[n] = true
-		}
-		if x, ok := n.(*ast.Ident); ok && n != first {
-			// Also mark any value used within a constraint from an optional field.
-			if x.Node != nil {
-				// fmt.Println("MARKED", internal.DebugStr(x.Node),
-				// "by", internal.DebugStr(first))
-				// t.markAlwaysGen(x.Node)
-			}
-		}
-		return true
-	}, nil)
-}
-
-func (t *trimSet) canRemove(n ast.Node) bool {
-	return !t.exclude[n] && !t.alwaysGen[n]
-}
-
-func isDisjunctionOfStruct(n ast.Node) bool {
-	switch x := n.(type) {
-	case *ast.BinaryExpr:
-		if x.Op == token.OR {
-			return hasStruct(x.X) || hasStruct(x.Y)
-		}
-	}
-	return false
-}
-
-func hasStruct(n ast.Node) bool {
-	hasStruct := false
-	ast.Walk(n, func(n ast.Node) bool {
-		if _, ok := n.(*ast.StructLit); ok {
-			hasStruct = true
-		}
-		return !hasStruct
-	}, nil)
-	return hasStruct
-}
-
-// trim strips fields from structs that would otherwise be generated by implied
-// content, such as optional fields turned required, comprehensions, and list types.
-//
-// The algorithm walks the tree with two values in parallel: one for the full
-// configuration, and one for implied content. For each node in the tree it
-// determines the value of the implied content and that of the full value
-// and strips any of the non-implied fields if it subsumes the implied ones.
-//
-// There are a few gotchas:
-// - Fields in the implied content may refer to fields in the complete config.
-//   To support this, incomplete fields are detected and evaluated within the
-//   configuration.
-// - Values of optional fields are instantiated as a result of the declaration
-//   of concrete sibling fields. Such fields should not be removed even if the
-//   instantiated constraint completely subsumes such fields as the reason to
-//   apply the optional constraint will disappear with it.
-// - As the parallel structure is different, it may resolve to different
-//   default values. There is no support yet for selecting defaults of a value
-//   based on another value without doing a full unification. So for now we
-//   skip any disjunction containing structs.
-//
-//		v      the current value
-//		m      the "mixed-in" values
-//		scope  in which to evaluate expressions
-//		rmSet  nodes in v that may be removed by the caller
-func (t *trimSet) trim(label string, v, m, scope cue.Value) (rmSet []ast.Node) {
-	saved := t.stack
-	t.stack = append(t.stack, label)
-	defer func() { t.stack = saved }()
-
-	vSplit := v.Split()
-
-	// At the moment disjunctions of structs are not supported. Detect them and
-	// punt.
-	// TODO: support disjunctions.
-	mSplit := m.Split()
-	for _, v := range mSplit {
-		if isDisjunctionOfStruct(v.Source()) {
-			return
-		}
-	}
-
-	// Collect generated nodes.
-	// Only keep the good parts of the optional constraint.
-	// Incoming structs may be incomplete resulting in errors. It is safe
-	// to ignore these. If there is an actual error, it will manifest in
-	// the evaluation of v.
-	in := cue.Value{}
-	gen := []ast.Node{}
-	for _, v := range mSplit {
-		// TODO: consider resolving incomplete values within the current
-		// scope, as we do for fields.
-		if v.Exists() {
-			in = in.UnifyAccept(v, m)
-		}
-		gen = append(gen, v.Source())
-	}
-
-	accept := v
-	comp := in
-
-	// Identify generated components and unify them with the mixin value.
-	exists := false
-	for _, v := range vSplit {
-		src := v.Source()
-		alwaysGen := t.alwaysGen[src]
-		inNodes := inNodes(gen, src)
-		fromComp := t.fromComp[src]
-		if fromComp {
-			gen = append(gen, src)
-			comp = comp.UnifyAccept(v, accept)
-			continue
-		}
-		if !(alwaysGen || inNodes) {
-			continue
-		}
-
-		if !v.IsConcrete() {
-			// The template has an expression that cannot be fully
-			// resolved. Attempt to complete the expression by
-			// evaluting it within the struct to which the template
-			// is applied.
-			expr := internal.ToExpr(v.Syntax())
-
-			// TODO: this only resolves references contained in scope.
-			v = internal.EvalExpr(scope, expr).(cue.Value)
-		}
-
-		if w := in.UnifyAccept(v, accept); w.Err() == nil {
-			in = w
-		}
-		// One of the sources of this struct is generated. That means
-		// we can safely delete a non-generated version.
-		exists = true
-		gen = append(gen, src)
-	}
-
-	switch v.Kind() {
-	case cue.StructKind:
-		// Build map of mixin fields.
-		valueMap := map[key]cue.Value{}
-		for mIter, _ := in.Fields(cue.All(), cue.Optional(false)); mIter.Next(); {
-			valueMap[iterKey(mIter)] = mIter.Value()
-		}
-
-		fn := v.Template()
-
-		// Process fields.
-		rm := []ast.Node{}
-		for iter, _ := v.Fields(cue.All()); iter.Next(); {
-			mSub := valueMap[iterKey(iter)]
-			if fn != nil {
-				label := iter.Label()
-				w := fn(label)
-				mSub = mSub.Unify(w)
-			}
-
-			label := iter.Label()
-			removed := t.trim(label, iter.Value(), mSub, v)
-			rm = append(rm, removed...)
-		}
-
-		canRemove := fn == nil
-		for _, v := range vSplit {
-			src := v.Source()
-			if t.fromComp[src] {
-				canRemove = true
-			}
-		}
-
-		// Remove fields from source.
-		for _, v := range vSplit {
-			if src := v.Source(); !t.alwaysGen[src] {
-				switch x := src.(type) {
-				case *ast.File:
-					// TODO: use in instead?
-					x.Decls = t.trimDecls(x.Decls, rm, m, canRemove)
-
-				case *ast.StructLit:
-					x.Elts = t.trimDecls(x.Elts, rm, m, canRemove)
-					exists = exists || m.Exists()
-					if len(x.Elts) == 0 && exists && t.canRemove(src) && !inNodes(gen, src) {
-						rmSet = append(rmSet, src)
-					}
-
-				default:
-					// Currently the ast.Files are not associated with the
-					// nodes. We detect this case here and iterate over all
-					// files.
-					// TODO: fix this. See cue/instance.go.
-					if len(t.stack) == 1 {
-						for _, x := range t.files {
-							x.Decls = t.trimDecls(x.Decls, rm, m, canRemove)
-						}
-					}
-				}
-			}
-		}
-
-		if t.cfg.Trace {
-			w := &bytes.Buffer{}
-			fmt.Fprintln(w)
-			fmt.Fprintln(w, "value:    ", v)
-			if in.Exists() {
-				fmt.Fprintln(w, "mixed in: ", in)
-			}
-			for _, v := range vSplit {
-				status := "[]"
-				src := v.Source()
-				if inNodes(rmSet, src) {
-					status = "[re]"
-				} else if t.alwaysGen[src] {
-					status = "[i]"
-				}
-				fmt.Fprintf(w, "    %4s %v: %v %T\n", status, v.Pos(), internal.DebugStr(src), src)
-			}
-
-			t.traceMsg(w.String())
-		}
-
-	case cue.ListKind:
-		mIter, _ := m.List()
-		elem, hasElem := m.Elem()
-		i := 0
-		rmElem := []ast.Node{}
-		allowRemove := true
-		for iter, _ := v.List(); iter.Next(); i++ {
-			var m cue.Value
-			if mIter.Next() {
-				m = mIter.Value()
-			} else if hasElem {
-				m = elem
-				allowRemove = false
-			} else {
-				allowRemove = false
-				break
-			}
-			rm := t.trim(strconv.Itoa(i), iter.Value(), m, scope)
-			rmElem = append(rmElem, rm...)
-		}
-
-		// Signal the removal of lists of which all elements have been marked
-		// for removal.
-		if allowRemove {
-			for _, v := range vSplit {
-				if src := v.Source(); !t.alwaysGen[src] {
-					l, ok := src.(*ast.ListLit)
-					if !ok {
-						break
-					}
-					rmList := true
-					iter, _ := v.List()
-					for i := 0; i < len(l.Elts) && iter.Next(); i++ {
-						if !inNodes(rmElem, l.Elts[i]) {
-							rmList = false
-							break
-						}
-					}
-					if rmList && m.Exists() && t.canRemove(src) && !inNodes(gen, src) {
-						rmSet = append(rmSet, src)
-					}
-				}
-			}
-		}
-		fallthrough
-
-	default:
-		// Mark any subsumed part that is covered by generated config.
-		in, _ := in.Default()
-		if in.Err() == nil && v.Subsume(in) == nil {
-			for _, v := range vSplit {
-				src := v.Source()
-				if t.canRemove(src) && !inNodes(gen, src) &&
-					exists {
-					rmSet = append(rmSet, src)
-				}
-			}
-		}
-
-		if t.cfg.Trace {
-			w := &bytes.Buffer{}
-			if len(rmSet) > 0 {
-				fmt.Fprint(w, "field: SUBSUMED\n")
-			} else {
-				fmt.Fprint(w, "field: \n")
-			}
-			fmt.Fprintln(w, "value:    ", v)
-			if in.Exists() {
-				fmt.Fprintln(w, "mixed in: ", in)
-			}
-			for _, v := range vSplit {
-				status := "["
-				if inNodes(gen, v.Source()) {
-					status += "i"
-				}
-				if inNodes(rmSet, v.Source()) {
-					status += "r"
-				}
-				status += "]"
-				src := v.Source()
-				fmt.Fprintf(w, "   %4s %v: %v\n", status, v.Pos(), internal.DebugStr(src))
-			}
-
-			t.traceMsg(w.String())
-		}
-	}
-
-	if comp.Exists() {
-		if v.Subsume(comp) == nil {
-			for _, v := range vSplit {
-				src := v.Source()
-				if !inNodes(gen, src) {
-					rmSet = append(rmSet, src)
-				}
-			}
-		}
-	}
-
-	return rmSet
-}
-
-func (t *trimSet) trimDecls(decls []ast.Decl, rm []ast.Node, m cue.Value, allow bool) []ast.Decl {
-	a := make([]ast.Decl, 0, len(decls))
-
-	for _, d := range decls {
-		if f, ok := d.(*ast.Field); ok {
-			label, _, err := ast.LabelName(f.Label)
-			v := m.Lookup(label)
-			inNodes := inNodes(rm, f.Value)
-			ok := allow || v.Exists()
-			if err == nil && inNodes && ok {
-				continue
-			}
-		}
-		a = append(a, d)
-	}
-	return a
-}
-
-func inNodes(a []ast.Node, n ast.Node) bool {
-	for _, e := range a {
-		if e == n {
+// Roots of constraints are not allowed to strip conjuncts by
+// themselves as it will eliminate the reason for the trigger.
+func allowRemove(v *adt.Vertex) bool {
+	for _, c := range v.Conjuncts {
+		if isDominator(c) &&
+			(c.CloseInfo.Location() != c.Expr() ||
+				c.CloseInfo.RootSpanType() != adt.ConstraintSpan) {
 			return true
 		}
 	}
 	return false
 }
 
-type key struct {
-	label  string
-	hidden bool
-}
+// A parent may be removed if there is not a `no` and there is at least one
+// `yes`. A `yes` is proves that there is at least one node that is not a
+// dominator node and that we are not removing nodes from a declaration of a
+// dominator itself.
+const (
+	no = 1 << iota
+	maybe
+	yes
+)
 
-func iterKey(v *cue.Iterator) key {
-	return key{v.Label(), v.IsHidden()}
+func (t *trimmer) findSubordinates(v *adt.Vertex) int {
+	// TODO(structure sharing): do not descend into vertices whose parent is not
+	// equal to the parent. This is not relevant at this time, but may be so in
+	// the future.
+
+	if len(v.Arcs) > 0 {
+		var match int
+		for _, a := range v.Arcs {
+			match |= t.findSubordinates(a)
+		}
+
+		// This also skips embedded scalars if not all fields are removed. In
+		// this case we need to preserve the scalar to keep the type of the
+		// struct intact, which might as well be done by not removing the scalar
+		// type.
+		if match&yes == 0 || match&no != 0 {
+			return match
+		}
+	}
+
+	if !allowRemove(v) {
+		return no
+	}
+
+	switch v.BaseValue.(type) {
+	case *adt.StructMarker, *adt.ListMarker:
+		// Rely on previous processing of the Arcs and the fact that we take the
+		// default value to check dominator subsumption, meaning that we don't
+		// have to check additional optional constraints to pass subsumption.
+
+	default:
+		doms := &adt.Vertex{}
+		hasSubs := false
+
+		for _, c := range v.Conjuncts {
+			if isDominator(c) {
+				doms.AddConjunct(c)
+			} else {
+				hasSubs = true
+			}
+		}
+
+		if !hasSubs {
+			return maybe // only if there are siblings to be removed.
+		}
+
+		doms.Finalize(t.ctx)
+		doms = doms.Default()
+
+		// This is not necessary, but seems like it may result in more
+		// user-friendly semantics.
+		if doms.IsErr() || v.IsErr() {
+			return no
+		}
+
+		// TODO: since we take v, instead of the unification of subordinate
+		// values, it should suffice to take equality here:
+		//    doms ⊑ subs  ==> doms == subs&doms
+		if err := subsume.Value(t.ctx, v, doms); err != nil {
+			return no
+		}
+	}
+
+	for _, c := range v.Conjuncts {
+		if !isDominator(c) {
+			t.markRemove(c)
+		}
+	}
+
+	return yes
 }
diff --git a/tools/trim/trim_test.go b/tools/trim/trim_test.go
index 04de0c2..167192a 100644
--- a/tools/trim/trim_test.go
+++ b/tools/trim/trim_test.go
@@ -79,6 +79,7 @@
 		c: b: 3
 
 		z: {
+
 			a: b: 3
 			for k, v in a {
 				c: "\(k)": v
@@ -92,6 +93,7 @@
 }
 
 z: {
+
 	a: b: 3
 	for k, v in a {
 		c: "\(k)": v
@@ -135,7 +137,7 @@
 		x: >=5 & <=8 & int
 	}
 
-	t: u: { x: 5 } // TODO: should be t: u: {}
+	t: u: { x: 5 }
 }
 
 group: {
@@ -157,7 +159,7 @@
 		x: >=5 & <=8 & int
 	}
 
-	t: u: {x: 5}
+	t: u: {}
 }
 
 group: {
@@ -184,6 +186,22 @@
 }
 `,
 	}, {
+		name: "list removal",
+		in: `
+		service: [string]: {
+			ports: [{a: 1}, {a: 1}, ...{ extra: 3 }]
+		}
+		service: a: {
+			ports: [{a: 1}, {a: 1,}]
+		}
+		`,
+		out: `service: [string]: {
+	ports: [{a: 1}, {a: 1}, ...{extra: 3}]
+}
+service: a: {
+}
+`,
+	}, {
 		name: "do not overmark comprehension",
 		in: `
 foo: multipath: {
@@ -213,36 +231,25 @@
 }
 `,
 	}, {
-		name: "list removal",
+		name: "remove implied interpolations",
 		in: `
-	service: [string]: {
-		ports: [{a: 1}, {a: 1}, ...{ extra: 3 }]
-	}
-	service: a: {
-		ports: [{a: 1}, {a: 1, extra: 3}, {}, { extra: 3 }]
-	}
-`,
-		out: `service: [string]: {
-	ports: [{a: 1}, {a: 1}, ...{extra: 3}]
+				foo: [string]: {
+					a: string
+					b: "--\(a)--"
+				}
+				foo: entry: {
+					a: "insert"
+					b: "--insert--"
+				}
+				`,
+		out: `foo: [string]: {
+	a: string
+	b: "--\(a)--"
 }
-service: a: {
-	ports: [{}, {extra: 3}, {}, {}]
+foo: entry: {
+	a: "insert"
 }
 `,
-		// }, {
-		// TODO: This used to work.
-		// 	name: "remove implied interpolations",
-		// 	in: `
-		// 		foo: [string]: {
-		// 			a: string
-		// 			b: "--\(a)--"
-		// 		}
-		// 		foo: entry: {
-		// 			a: "insert"
-		// 			b: "--insert--"
-		// 		}
-		// 		`,
-		// 	out: ``,
 	}}
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
@@ -250,7 +257,7 @@
 			if err != nil {
 				t.Fatal(err)
 			}
-			r := Runtime{}
+			r := cue.Runtime{}
 			inst, err := r.CompileFile(f)
 			if err != nil {
 				t.Fatal(err)