| // 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" |
| "cuelang.org/go/internal/core/adt" |
| "cuelang.org/go/internal/core/debug" |
| "cuelang.org/go/internal/core/runtime" |
| "cuelang.org/go/internal/core/subsume" |
| ) |
| |
| // 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.Instance, cfg *Config) error { |
| 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{}, |
| exclude: map[ast.Node]bool{}, |
| debug: Debug, |
| w: os.Stderr, |
| } |
| |
| 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.Expr().Source(); src != nil { |
| t.remove[src] = true |
| if t.debug { |
| t.logf("removing %s", debug.NodeString(t.ctx, c.Expr(), nil)) |
| } |
| } |
| } |
| |
| func (t *trimmer) markKeep(c adt.Conjunct) { |
| if isDom, _ := isDominator(c); isDom { |
| return |
| } |
| if src := c.Expr().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 c.Field().(type) { |
| case *adt.OptionalField: // bulk constraints handled elsewhere. |
| return true, false |
| } |
| 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.Expr() |
| 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{} |
| 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.Expr().(adt.Resolver); ok { |
| x, _ := t.ctx.Resolve(c.Env, r) |
| // Even if this is not a dominator now, descendants will be. |
| if 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) |
| } |
| } |
| }() |
| |
| 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 |
| } |