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