blob: 17e164c9b9619137681cc317be68cbba89dc8081 [file] [log] [blame]
// 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 (
"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/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{},
}
t.findSubordinates(v)
// 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] {
c.Delete()
}
return true
}, nil)
}
return nil
}
type trimmer struct {
Config
ctx *adt.OpContext
remove map[ast.Node]bool
}
func (t *trimmer) markRemove(c adt.Conjunct) {
if src := c.Expr().Source(); src != nil {
t.remove[src] = true
}
}
const dominatorNode = adt.ComprehensionSpan | adt.DefinitionSpan | adt.ConstraintSpan
func isDominator(c adt.Conjunct) bool {
return c.CloseInfo.IsInOneOf(dominatorNode)
}
// Roots of constraints are not allowed to strip conjuncts by
// themselves as it will eliminate the reason for the trigger.
func allowRemove(v *adt.Vertex) bool {
for _, c := range v.Conjuncts {
if isDominator(c) &&
(c.CloseInfo.Location() != c.Expr() ||
c.CloseInfo.RootSpanType() != adt.ConstraintSpan) {
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
)
func (t *trimmer) findSubordinates(v *adt.Vertex) int {
// 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 {
match |= t.findSubordinates(a)
}
// 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 !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:
doms := &adt.Vertex{}
hasSubs := false
for _, c := range v.Conjuncts {
if isDominator(c) {
doms.AddConjunct(c)
} else {
hasSubs = true
}
}
if !hasSubs {
return maybe // only if there are siblings to be removed.
}
doms.Finalize(t.ctx)
doms = doms.Default()
// This is not necessary, but seems like it may result in more
// user-friendly semantics.
if doms.IsErr() || 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 {
if !isDominator(c) {
t.markRemove(c)
}
}
return yes
}