// Copyright 2018 The CUE Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // 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 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. // // Examples: // // light: [string]: { // room: string // brightnessOff: *0.0 | >=0 & <=100.0 // brightnessOn: *100.0 | >=0 & <=100.0 // } // // light: ceiling50: { // room: "MasterBedroom" // brightnessOff: 0.0 // this line // brightnessOn: 100.0 // and this line will be removed // } // // Results in: // // // Unmodified: light: [string]: { ... } // // light: ceiling50: { // room: "MasterBedroom" // } package trim import ( "io" "os" "cuelang.org/go/cue" "cuelang.org/go/cue/ast" "cuelang.org/go/cue/ast/astutil" "cuelang.org/go/internal/core/adt" "cuelang.org/go/internal/core/debug" "cuelang.org/go/internal/core/subsume" "cuelang.org/go/internal/core/walk" "cuelang.org/go/internal/value" ) // Config configures trim options. type Config struct { Trace bool } // Files trims fields in the given files that can be implied from other fields, // as can be derived from the evaluated values in inst. // 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.InstanceOrValue, cfg *Config) error { r, v := value.ToInternal(inst.Value()) t := &trimmer{ Config: *cfg, ctx: adt.NewContext(r, v), remove: map[ast.Node]bool{}, exclude: map[ast.Node]bool{}, debug: Debug, w: os.Stderr, } // Mark certain expressions as off limits. // TODO: We could alternatively ensure that comprehensions unconditionally // resolve. visitor := &walk.Visitor{ Before: func(n adt.Node) bool { switch x := n.(type) { case *adt.StructLit: // Structs with comprehensions may never be removed. for _, d := range x.Decls { switch d.(type) { case *adt.Comprehension: t.markKeep(x) } } } return true }, } for _, c := range v.Conjuncts { visitor.Elem(c.Elem()) } d, _, _, pickedDefault := t.addDominators(nil, v, false) t.findSubordinates(d, v, pickedDefault) // Remove subordinate values from files. for _, f := range files { astutil.Apply(f, func(c astutil.Cursor) bool { if f, ok := c.Node().(*ast.Field); ok && t.remove[f.Value] && !t.exclude[f.Value] { c.Delete() } return true }, nil) if err := astutil.Sanitize(f); err != nil { return err } } return nil } type trimmer struct { Config ctx *adt.OpContext remove map[ast.Node]bool exclude map[ast.Node]bool debug bool indent int w io.Writer } var Debug bool = false func (t *trimmer) markRemove(c adt.Conjunct) { if src := c.Elem().Source(); src != nil { t.remove[src] = true if t.debug { t.logf("removing %s", debug.NodeString(t.ctx, c.Elem(), nil)) } } } func (t *trimmer) markKeep(x adt.Expr) { if src := x.Source(); src != nil { t.exclude[src] = true if t.debug { t.logf("keeping") } } } const dominatorNode = adt.ComprehensionSpan | adt.DefinitionSpan | adt.ConstraintSpan // isDominator reports whether a node can remove other nodes. func isDominator(c adt.Conjunct) (ok, mayRemove bool) { if !c.CloseInfo.IsInOneOf(dominatorNode) { return false, false } switch f := c.Field().(type) { case *adt.Field: // bulk constraints handled elsewhere. return true, f.ArcType == adt.ArcMember } return true, true } // Removable reports whether a non-dominator conjunct can be removed. This is // not the case if it has pattern constraints that could turn into dominator // nodes. func removable(c adt.Conjunct, v *adt.Vertex) bool { return c.CloseInfo.FieldTypes&(adt.HasAdditional|adt.HasPattern) == 0 } // Roots of constraints are not allowed to strip conjuncts by // themselves as it will eliminate the reason for the trigger. func (t *trimmer) allowRemove(v *adt.Vertex) bool { for _, c := range v.Conjuncts { _, allowRemove := isDominator(c) loc := c.CloseInfo.Location() != c.Elem() isSpan := c.CloseInfo.RootSpanType() != adt.ConstraintSpan if allowRemove && (loc || isSpan) { return true } } return false } // 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 ) // addDominators injects dominator values from v into d. If no default has // been selected from dominators so far, the values are erased. Otherwise, // both default and new values are merged. // // Erasing the previous values when there has been no default so far allows // interpolations, for instance, to be evaluated in the new context and // eliminated. // // Values are kept when there has been a default (current or ancestor) because // the current value may contain information that caused that default to be // selected and thus erasing it would cause that information to be lost. // // TODO: // In principle, information only needs to be kept for discriminator values, or // any value that was instrumental in selecting the default. This is currently // hard to do, however, so we just fall back to a stricter mode in the presence // of defaults. func (t *trimmer) addDominators(d, v *adt.Vertex, hasDisjunction bool) (doms *adt.Vertex, ambiguous, hasSubs, strict bool) { strict = hasDisjunction doms = &adt.Vertex{ Parent: v.Parent, Label: v.Label, } if d != nil && hasDisjunction { doms.Conjuncts = append(doms.Conjuncts, d.Conjuncts...) } hasDoms := false for _, c := range v.Conjuncts { isDom, _ := isDominator(c) switch { case isDom: doms.AddConjunct(c) default: if r, ok := c.Elem().(adt.Resolver); ok { x, _ := t.ctx.Resolve(c, r) // Even if this is not a dominator now, descendants will be. if x != nil && x.Label.IsDef() { for _, c := range x.Conjuncts { doms.AddConjunct(c) } break } } hasSubs = true } } doms.Finalize(t.ctx) switch x := doms.Value().(type) { case *adt.Disjunction: switch x.NumDefaults { case 1: strict = true default: ambiguous = true } } if doms = doms.Default(); doms.IsErr() { ambiguous = true } _ = hasDoms return doms, hasSubs, ambiguous, strict || ambiguous } func (t *trimmer) findSubordinates(doms, v *adt.Vertex, hasDisjunction bool) (result int) { defer un(t.trace(v)) defer func() { if result == no { for _, c := range v.Conjuncts { t.markKeep(c.Expr()) } } }() doms, hasSubs, ambiguous, pickedDefault := t.addDominators(doms, v, hasDisjunction) if ambiguous { return no } // 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 { d := doms.Lookup(a.Label) match |= t.findSubordinates(d, a, pickedDefault) } // 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 !t.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: if !hasSubs { return maybe } // This should normally not be necessary, as subsume should catch this. // But as we already take the default value for doms, it doesn't hurt to // do it. v = v.Default() // This is not necessary, but seems like it may result in more // user-friendly semantics. if 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 { _, allowRemove := isDominator(c) if !allowRemove && removable(c, v) { t.markRemove(c) } } return yes }