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 }