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 }