// Copyright 2022 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 export import ( "fmt" "strconv" "strings" "cuelang.org/go/cue/ast" "cuelang.org/go/cue/token" "cuelang.org/go/internal/core/adt" "cuelang.org/go/internal/core/dep" ) // This file contains the algorithm to contain a self-contained CUE file. // TODO: // - Handle below edge cases where a reference directly references the root // of the exported main tree. // - Inline smallish structs that themselves do not have outside // references. // - Overall better inlining. // - Consider a shorthand for the `let X = { #x: foo }` annotation. Possibly // allow `#{}`, or allow "let definitions", like `let #X = {}`. // - Make doc comment of root the file comment. // initPivotter initializes a selfContainedCloser if either a subtree // is exported or imports need to be removed. It will not initialize one if // neither is the case. func (e *exporter) initPivotter(v *adt.Vertex) { s := &pivotter{} e.pivotter = s s.x = e s.depsMap = map[*adt.Vertex]*depData{} s.refMap = map[adt.Resolver]*refData{} s.linkDependencies(v) } func (e *exporter) completePivot(f *ast.File) { s := e.pivotter if s == nil || f == nil { return } for _, d := range s.deps { if !d.isExternalRoot() { continue } s.addExternal(d) } f.Decls = append(f.Decls, s.decls...) } // A pivotter pivots a graph around a new root. // // Given a Vertex that itself is not the root of a configuration, the pivotter // recomputes the configuration to have that node as a root instead. // // TODO: although this is currently part of Package export, it could be its own // package down the line, if there is a proven need for it. type pivotter struct { x *exporter deps []*depData depsMap map[*adt.Vertex]*depData refs []*refData refMap map[adt.Resolver]*refData decls []ast.Decl } type depData struct { parent *depData dstNode *adt.Vertex dstImport *adt.ImportReference ident adt.Feature path []adt.Feature useCount int // Other reference using this vertex included bool needTopLevel bool } // isExternalRoot reports whether d is an external node (a node referenced // outside main exported tree) that has no further parent nodes that are // referenced. func (d *depData) isExternalRoot() bool { return d.ident != 0 } func (d *depData) usageCount() int { return getParent(d).useCount } type refData struct { dst *depData } func (v *depData) node() *adt.Vertex { return v.dstNode } func (p *pivotter) linkDependencies(v *adt.Vertex) { p.markDeps(v, nil) // Explicitly add the root of the configuration. p.markIncluded(v) // Link one parent up for _, d := range p.depsMap { p.markParentsPass1(d) } // Get transitive closure of parents. for _, d := range p.depsMap { if d.parent != nil { d.parent = getParent(d) d.parent.useCount++ } } // Compute the paths for the parent nodes. for _, d := range p.deps { if d.parent == nil { p.makeParentPath(d) } } } func getParent(d *depData) *depData { for ; d.parent != nil; d = d.parent { } return d } func (p *pivotter) markDeps(v *adt.Vertex, pkg *adt.ImportReference) { // TODO: sweep all child nodes and mark as no need for recursive checks. cfg := &dep.Config{ Descend: true, Pkg: pkg, } dep.Visit(cfg, p.x.ctx, v, func(d dep.Dependency) error { node := d.Node switch { case p.refMap[d.Reference] != nil: // Already done. return nil case d.Import() != nil: // Only record nodes within import if we want to expand imports. if !p.x.cfg.InlineImports { return nil } // Never resolve core packages. Reasons: // - most of them are builtins // - they are available anyway // - some of the types have special meaning, which would be lost // by rewriting to their underlying type. // TODO: support marking non-CUE packages as "special". This could // be done, for instance, by marking them as "core" in the runtime // and using a Runtime method to determine whether something is // a core package, rather than relying on the presence of a dot. path := d.Import().ImportPath.StringValue(p.x.ctx) if !strings.ContainsRune(path, '.') { return nil } case node.IsUnprocessed(): // This may happen for DynamicReferences. return nil } data, ok := p.depsMap[node] if !ok { data = &depData{ dstNode: node, dstImport: d.Import(), } p.depsMap[node] = data p.deps = append(p.deps, data) } data.useCount++ ref := &refData{dst: data} p.refs = append(p.refs, ref) p.refMap[d.Reference] = ref if !ok { d.Recurse() } return nil }) } // markIncluded marks all referred nodes that are within the normal tree to be // exported. func (p *pivotter) markIncluded(v *adt.Vertex) { d, ok := p.depsMap[v] if !ok { d = &depData{dstNode: v} p.depsMap[v] = d } d.included = true for _, a := range v.Arcs { p.markIncluded(a) } } // markParentPass1 marks the furthest ancestor node for which there is a // dependency as its parent. Only dependencies that do not have a parent // will be assigned to hidden reference. func (p *pivotter) markParentsPass1(d *depData) { for n := d.node().Parent; n != nil; n = n.Parent { if v, ok := p.depsMap[n]; ok { d.parent = v } } } func (p *pivotter) makeParentPath(d *depData) { if d.parent != nil { panic("not a parent") } if d.included || d.isExternalRoot() { return } var f adt.Feature if path := d.dstNode.Path(); len(path) > 0 { f = path[len(path)-1] } else if imp := d.dstImport; imp != nil { f = imp.Label } else { // This may legitimately happen for internal vertices, such as // comprehension scopes. return } var str string if f.IsInt() { str = fmt.Sprintf("Index%d", f.Index()) } else { str = f.IdentString(p.x.ctx) str = strings.TrimLeft(str, "_#") str = strings.ToUpper(str) } uf, _ := p.x.uniqueFeature(str) d.path = []adt.Feature{uf} d.ident = uf // Make it a definition if we need it. if d.dstNode.IsRecursivelyClosed() { d.path = append(d.path, adt.MakeIdentLabel(p.x.ctx, "#x", "")) } } // makeAlternativeReference computes the alternative path for the reference. func (p *pivotter) makeAlternativeReference(ref *refData, r adt.Resolver) ast.Expr { d := ref.dst // Determine if the reference can be inline. var path []adt.Feature if d.parent == nil { // Get canonical vertexData. path = d.path } else { pathLen, pkgRef := relPathLength(r) path = d.node().Path() count := d.stepsToParent() switch { case ref.dst.included: // Inside main tree. if count > pathLen { // Cannot refer to root, so cannot use >= return nil } case pkgRef: default: // Inside hoisted value. if count >= pathLen { return nil } } path = path[len(path)-count:] path = append(d.parent.path, path...) } if len(path) == 0 { path = append(path, p.x.ctx.StringLabel("ROOT")) } var x ast.Expr = p.x.ident(path[0]) for _, f := range path[1:] { if f.IsInt() { x = &ast.IndexExpr{ X: x, Index: ast.NewLit(token.INT, strconv.Itoa(f.Index())), } } else { x = &ast.SelectorExpr{ X: x, Sel: p.x.stringLabel(f), } } } return x } func (d *depData) stepsToParent() int { parent := d.parent.node() count := 0 for p := d.node(); p != parent; p = p.Parent { if p == nil { break } count++ } return count } func relPathLength(r adt.Resolver) (length int, newRoot bool) { for { var expr adt.Expr switch x := r.(type) { case *adt.FieldReference, *adt.DynamicReference, *adt.LetReference, *adt.ValueReference: length++ case *adt.ImportReference: // This reference indicates a different vertex as root, but doesn't // increase the path length. return length, true case *adt.SelectorExpr: length++ expr = x.X case *adt.IndexExpr: length++ expr = x.X } switch x := expr.(type) { case nil: return length, false case adt.Resolver: r = x default: panic("unreachable") } } } // refExpr returns a substituted expression for a given reference, or nil if // there are no changes. This function implements most of the policy to decide // when an expression can be inlined. func (p *pivotter) refExpr(r adt.Resolver) ast.Expr { ref, ok := p.refMap[r] if !ok { return nil } dst := ref.dst n := dst.node() // Inline value, but only when this may not lead to an exponential // expansion. We allow inlining when a value is only used once, or when // it is a simple concrete scalar value. switch { case dst.included: // Keep references that point inside the hoisted vertex. // TODO: force hoisting. This would be akin to taking the interpretation // that references that initially point outside the included vertex // are external inputs too, even if they eventually point inside. case p.x.inDefinition == 0 && n.IsRecursivelyClosed(): // We need to wrap the value in a definition. case dst.usageCount() == 0: // The root value itself. case n.IsErr(): // Don't simplify for errors to make the position of the error clearer. case !n.IsConcrete() && p.x.inExpression > 0: // Don't simplify an expression that is known will fail. case dst.usageCount() == 1 && p.x.inExpression == 0: // Used only once. fallthrough case n.IsConcrete() && len(n.Arcs) == 0: // Simple scalar value. return p.x.expr(nil, n) } if r := p.makeAlternativeReference(ref, r); r != nil { dst.needTopLevel = true return r } return nil } // addExternal converts a vertex for an external reference. func (p *pivotter) addExternal(d *depData) { if !d.needTopLevel { return } expr := p.x.expr(nil, d.node()) if len(d.path) > 1 { expr = ast.NewStruct(&ast.Field{ Label: p.x.stringLabel(d.path[1]), Value: expr, }) } let := &ast.LetClause{ Ident: p.x.ident(d.path[0]), Expr: expr, } ast.SetRelPos(let, token.NewSection) path := p.x.ctx.PathToString(d.node().Path()) var msg string if d.dstImport == nil { msg = fmt.Sprintf("//cue:path: %s", path) } else { pkg := d.dstImport.ImportPath.SelectorString(p.x.ctx) if path == "" { msg = fmt.Sprintf("//cue:path: %s", pkg) } else { msg = fmt.Sprintf("//cue:path: %s.%s", pkg, path) } } cg := &ast.CommentGroup{ Doc: true, List: []*ast.Comment{{Text: msg}}, } ast.SetRelPos(cg, token.NewSection) ast.AddComment(let, cg) p.decls = append(p.decls, let) }