github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/tools/trim/trim.go (about)

     1  // Copyright 2018 The CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package trim removes fields that may be inferred from another mixed in value
    16  // that "dominates" it. For instance, a value that is merged in from a
    17  // definition is considered to dominate a value from a regular struct that
    18  // mixes in this definition. Values derived from constraints and comprehensions
    19  // can also dominate other fields.
    20  //
    21  // A value A is considered to be implied by a value B if A subsumes the default
    22  // value of B. For instance, if a definition defines a field `a: *1 | int` and
    23  // mixed in with a struct that defines a field `a: 1 | 2`, then the latter can
    24  // be removed because a definition field dominates a regular field and because
    25  // the latter subsumes the default value of the former.
    26  //
    27  //
    28  // Examples:
    29  //
    30  // 	light: [string]: {
    31  // 		room:          string
    32  // 		brightnessOff: *0.0 | >=0 & <=100.0
    33  // 		brightnessOn:  *100.0 | >=0 & <=100.0
    34  // 	}
    35  //
    36  // 	light: ceiling50: {
    37  // 		room:          "MasterBedroom"
    38  // 		brightnessOff: 0.0    // this line
    39  // 		brightnessOn:  100.0  // and this line will be removed
    40  // 	}
    41  //
    42  // Results in:
    43  //
    44  // 	// Unmodified: light: [string]: { ... }
    45  //
    46  // 	light: ceiling50: {
    47  // 		room: "MasterBedroom"
    48  // 	}
    49  //
    50  package trim
    51  
    52  import (
    53  	"io"
    54  	"os"
    55  
    56  	"github.com/joomcode/cue/cue"
    57  	"github.com/joomcode/cue/cue/ast"
    58  	"github.com/joomcode/cue/cue/ast/astutil"
    59  	"github.com/joomcode/cue/internal/core/adt"
    60  	"github.com/joomcode/cue/internal/core/debug"
    61  	"github.com/joomcode/cue/internal/core/subsume"
    62  	"github.com/joomcode/cue/internal/core/walk"
    63  	"github.com/joomcode/cue/internal/value"
    64  )
    65  
    66  // Config configures trim options.
    67  type Config struct {
    68  	Trace bool
    69  }
    70  
    71  // Files trims fields in the given files that can be implied from other fields,
    72  // as can be derived from the evaluated values in inst.
    73  // Trimming is done on a best-effort basis and only when the removed field
    74  // is clearly implied by another field, rather than equal sibling fields.
    75  func Files(files []*ast.File, inst cue.InstanceOrValue, cfg *Config) error {
    76  	r, v := value.ToInternal(inst.Value())
    77  
    78  	t := &trimmer{
    79  		Config:  *cfg,
    80  		ctx:     adt.NewContext(r, v),
    81  		remove:  map[ast.Node]bool{},
    82  		exclude: map[ast.Node]bool{},
    83  		debug:   Debug,
    84  		w:       os.Stderr,
    85  	}
    86  
    87  	// Mark certain expressions as off limits.
    88  	// TODO: We could alternatively ensure that comprehensions unconditionally
    89  	// resolve.
    90  	visitor := &walk.Visitor{
    91  		Before: func(n adt.Node) bool {
    92  			switch x := n.(type) {
    93  			case *adt.StructLit:
    94  				// Structs with comprehensions may never be removed.
    95  				for _, d := range x.Decls {
    96  					switch d.(type) {
    97  					case *adt.Comprehension:
    98  						t.markKeep(x)
    99  					}
   100  				}
   101  			}
   102  			return true
   103  		},
   104  	}
   105  	for _, c := range v.Conjuncts {
   106  		visitor.Elem(c.Elem())
   107  	}
   108  
   109  	d, _, _, pickedDefault := t.addDominators(nil, v, false)
   110  	t.findSubordinates(d, v, pickedDefault)
   111  
   112  	// Remove subordinate values from files.
   113  	for _, f := range files {
   114  		astutil.Apply(f, func(c astutil.Cursor) bool {
   115  			if f, ok := c.Node().(*ast.Field); ok && t.remove[f.Value] && !t.exclude[f.Value] {
   116  				c.Delete()
   117  			}
   118  			return true
   119  		}, nil)
   120  		if err := astutil.Sanitize(f); err != nil {
   121  			return err
   122  		}
   123  	}
   124  
   125  	return nil
   126  }
   127  
   128  type trimmer struct {
   129  	Config
   130  
   131  	ctx     *adt.OpContext
   132  	remove  map[ast.Node]bool
   133  	exclude map[ast.Node]bool
   134  
   135  	debug  bool
   136  	indent int
   137  	w      io.Writer
   138  }
   139  
   140  var Debug bool = false
   141  
   142  func (t *trimmer) markRemove(c adt.Conjunct) {
   143  	if src := c.Elem().Source(); src != nil {
   144  		t.remove[src] = true
   145  		if t.debug {
   146  			t.logf("removing %s", debug.NodeString(t.ctx, c.Elem(), nil))
   147  		}
   148  	}
   149  }
   150  
   151  func (t *trimmer) markKeep(x adt.Expr) {
   152  	if src := x.Source(); src != nil {
   153  		t.exclude[src] = true
   154  		if t.debug {
   155  			t.logf("keeping")
   156  		}
   157  	}
   158  }
   159  
   160  const dominatorNode = adt.ComprehensionSpan | adt.DefinitionSpan | adt.ConstraintSpan
   161  
   162  // isDominator reports whether a node can remove other nodes.
   163  func isDominator(c adt.Conjunct) (ok, mayRemove bool) {
   164  	if !c.CloseInfo.IsInOneOf(dominatorNode) {
   165  		return false, false
   166  	}
   167  	switch c.Field().(type) {
   168  	case *adt.OptionalField: // bulk constraints handled elsewhere.
   169  		return true, false
   170  	}
   171  	return true, true
   172  }
   173  
   174  // Removable reports whether a non-dominator conjunct can be removed. This is
   175  // not the case if it has pattern constraints that could turn into dominator
   176  // nodes.
   177  func removable(c adt.Conjunct, v *adt.Vertex) bool {
   178  	return c.CloseInfo.FieldTypes&(adt.HasAdditional|adt.HasPattern) == 0
   179  }
   180  
   181  // Roots of constraints are not allowed to strip conjuncts by
   182  // themselves as it will eliminate the reason for the trigger.
   183  func (t *trimmer) allowRemove(v *adt.Vertex) bool {
   184  	for _, c := range v.Conjuncts {
   185  		_, allowRemove := isDominator(c)
   186  		loc := c.CloseInfo.Location() != c.Elem()
   187  		isSpan := c.CloseInfo.RootSpanType() != adt.ConstraintSpan
   188  		if allowRemove && (loc || isSpan) {
   189  			return true
   190  		}
   191  	}
   192  	return false
   193  }
   194  
   195  // A parent may be removed if there is not a `no` and there is at least one
   196  // `yes`. A `yes` is proves that there is at least one node that is not a
   197  // dominator node and that we are not removing nodes from a declaration of a
   198  // dominator itself.
   199  const (
   200  	no = 1 << iota
   201  	maybe
   202  	yes
   203  )
   204  
   205  // addDominators injects dominator values from v into d. If no default has
   206  // been selected from dominators so far, the values are erased. Otherwise,
   207  // both default and new values are merged.
   208  //
   209  // Erasing the previous values when there has been no default so far allows
   210  // interpolations, for instance, to be evaluated in the new context and
   211  // eliminated.
   212  //
   213  // Values are kept when there has been a default (current or ancestor) because
   214  // the current value may contain information that caused that default to be
   215  // selected and thus erasing it would cause that information to be lost.
   216  //
   217  // TODO:
   218  // In principle, information only needs to be kept for discriminator values, or
   219  // any value that was instrumental in selecting the default. This is currently
   220  // hard to do, however, so we just fall back to a stricter mode in the presence
   221  // of defaults.
   222  func (t *trimmer) addDominators(d, v *adt.Vertex, hasDisjunction bool) (doms *adt.Vertex, ambiguous, hasSubs, strict bool) {
   223  	strict = hasDisjunction
   224  	doms = &adt.Vertex{
   225  		Parent: v.Parent,
   226  		Label:  v.Label,
   227  	}
   228  	if d != nil && hasDisjunction {
   229  		doms.Conjuncts = append(doms.Conjuncts, d.Conjuncts...)
   230  	}
   231  
   232  	hasDoms := false
   233  	for _, c := range v.Conjuncts {
   234  		isDom, _ := isDominator(c)
   235  		switch {
   236  		case isDom:
   237  			doms.AddConjunct(c)
   238  		default:
   239  			if r, ok := c.Elem().(adt.Resolver); ok {
   240  				x, _ := t.ctx.Resolve(c.Env, r)
   241  				// Even if this is not a dominator now, descendants will be.
   242  				if x != nil && x.Label.IsDef() {
   243  					for _, c := range x.Conjuncts {
   244  						doms.AddConjunct(c)
   245  					}
   246  					break
   247  				}
   248  			}
   249  			hasSubs = true
   250  		}
   251  	}
   252  	doms.Finalize(t.ctx)
   253  
   254  	switch x := doms.Value().(type) {
   255  	case *adt.Disjunction:
   256  		switch x.NumDefaults {
   257  		case 1:
   258  			strict = true
   259  		default:
   260  			ambiguous = true
   261  		}
   262  	}
   263  
   264  	if doms = doms.Default(); doms.IsErr() {
   265  		ambiguous = true
   266  	}
   267  
   268  	_ = hasDoms
   269  	return doms, hasSubs, ambiguous, strict || ambiguous
   270  }
   271  
   272  func (t *trimmer) findSubordinates(doms, v *adt.Vertex, hasDisjunction bool) (result int) {
   273  	defer un(t.trace(v))
   274  	defer func() {
   275  		if result == no {
   276  			for _, c := range v.Conjuncts {
   277  				t.markKeep(c.Expr())
   278  			}
   279  		}
   280  	}()
   281  
   282  	doms, hasSubs, ambiguous, pickedDefault := t.addDominators(doms, v, hasDisjunction)
   283  
   284  	if ambiguous {
   285  		return no
   286  	}
   287  
   288  	// TODO(structure sharing): do not descend into vertices whose parent is not
   289  	// equal to the parent. This is not relevant at this time, but may be so in
   290  	// the future.
   291  
   292  	if len(v.Arcs) > 0 {
   293  		var match int
   294  		for _, a := range v.Arcs {
   295  			d := doms.Lookup(a.Label)
   296  			match |= t.findSubordinates(d, a, pickedDefault)
   297  		}
   298  
   299  		// This also skips embedded scalars if not all fields are removed. In
   300  		// this case we need to preserve the scalar to keep the type of the
   301  		// struct intact, which might as well be done by not removing the scalar
   302  		// type.
   303  		if match&yes == 0 || match&no != 0 {
   304  			return match
   305  		}
   306  	}
   307  
   308  	if !t.allowRemove(v) {
   309  		return no
   310  	}
   311  
   312  	switch v.BaseValue.(type) {
   313  	case *adt.StructMarker, *adt.ListMarker:
   314  		// Rely on previous processing of the Arcs and the fact that we take the
   315  		// default value to check dominator subsumption, meaning that we don't
   316  		// have to check additional optional constraints to pass subsumption.
   317  
   318  	default:
   319  		if !hasSubs {
   320  			return maybe
   321  		}
   322  
   323  		// This should normally not be necessary, as subsume should catch this.
   324  		// But as we already take the default value for doms, it doesn't hurt to
   325  		// do it.
   326  		v = v.Default()
   327  
   328  		// This is not necessary, but seems like it may result in more
   329  		// user-friendly semantics.
   330  		if v.IsErr() {
   331  			return no
   332  		}
   333  
   334  		// TODO: since we take v, instead of the unification of subordinate
   335  		// values, it should suffice to take equality here:
   336  		//    doms ⊑ subs  ==> doms == subs&doms
   337  		if err := subsume.Value(t.ctx, v, doms); err != nil {
   338  			return no
   339  		}
   340  	}
   341  
   342  	for _, c := range v.Conjuncts {
   343  		_, allowRemove := isDominator(c)
   344  		if !allowRemove && removable(c, v) {
   345  			t.markRemove(c)
   346  		}
   347  	}
   348  
   349  	return yes
   350  }