Skip to content

Commit

Permalink
internal/core/dep: first stab at precise dependency analyzer
Browse files Browse the repository at this point in the history
Change-Id: I0bf5de9a1b2421ddffa4108c6dbffd38dae2cfe0
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/7604
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
  • Loading branch information
mpvl committed Nov 16, 2020
1 parent dbfa73b commit c60e115
Show file tree
Hide file tree
Showing 14 changed files with 633 additions and 7 deletions.
8 changes: 8 additions & 0 deletions internal/core/adt/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,14 @@ func (c *OpContext) Resolve(env *Environment, r Resolver) (*Vertex, *Bottom) {
return nil, arc.ChildErrors
}

for {
x, ok := arc.Value.(*Vertex)
if !ok {
break
}
arc = x
}

return arc, err
}

Expand Down
311 changes: 311 additions & 0 deletions internal/core/dep/dep.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
// Copyright 2020 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 dep analyzes dependencies between values.
package dep

import (
"errors"

"cuelang.org/go/internal/core/adt"
)

// A Dependency is a reference and the node that reference resolves to.
type Dependency struct {
// Node is the referenced node.
Node *adt.Vertex

// Reference is the expression that referenced the node.
Reference adt.Resolver

top bool
}

// Import returns the import reference or nil if the reference was within
// the same package as the visited Vertex.
func (d *Dependency) Import() *adt.ImportReference {
x, _ := d.Reference.(adt.Expr)
return importRef(x)
}

// IsRoot reports whether the dependency is referenced by the root of the
// original Vertex passed to any of the Visit* functions, and not one of its
// descendent arcs. This always returns true for Visit().
func (d *Dependency) IsRoot() bool {
return d.top
}

func (d *Dependency) Path() []adt.Feature {
return nil
}

func importRef(r adt.Expr) *adt.ImportReference {
switch x := r.(type) {
case *adt.ImportReference:
return x
case *adt.SelectorExpr:
return importRef(x.X)
case *adt.IndexExpr:
return importRef(x.X)
}
return nil
}

// VisitFunc is used for reporting dependencies.
type VisitFunc func(Dependency) error

// Visit calls f for all vertices referenced by the conjuncts of n without
// descending into the elements of list or fields of structs. Only references
// that do not refer to the conjuncts of n itself are reported.
func Visit(c *adt.OpContext, n *adt.Vertex, f VisitFunc) error {
return visit(c, n, f, false, true)
}

// VisitAll calls f for all vertices referenced by the conjuncts of n including
// those of descendant fields and elements. Only references that do not refer to
// the conjuncts of n itself are reported.
func VisitAll(c *adt.OpContext, n *adt.Vertex, f VisitFunc) error {
return visit(c, n, f, true, true)
}

var empty *adt.Vertex

func init() {
empty = &adt.Vertex{}
empty.UpdateStatus(adt.Finalized)
}

func visit(c *adt.OpContext, n *adt.Vertex, f VisitFunc, all, top bool) (err error) {
if c == nil {
panic("nil context")
}
v := visitor{
ctxt: c,
visit: f,
node: n,
all: all,
top: top,
}

defer func() {
switch x := recover(); x {
case nil:
case aborted:
err = v.err
default:
panic(x)
}
}()

for _, x := range n.Conjuncts {
v.markExpr(x.Env, x.Expr())
}

return nil
}

var aborted = errors.New("aborted")

type visitor struct {
ctxt *adt.OpContext
visit VisitFunc
node *adt.Vertex
err error
all bool
top bool
}

// TODO: factor out the below logic as either a low-level dependency analyzer or
// some walk functionality.

// markExpr visits all nodes in an expression to mark dependencies.
func (c *visitor) markExpr(env *adt.Environment, expr adt.Expr) {
switch x := expr.(type) {
case nil:
case adt.Resolver:
c.markResolver(env, x)

case *adt.BinaryExpr:
c.markExpr(env, x.X)
c.markExpr(env, x.Y)

case *adt.UnaryExpr:
c.markExpr(env, x.X)

case *adt.Interpolation:
for i := 1; i < len(x.Parts); i += 2 {
c.markExpr(env, x.Parts[i])
}

case *adt.BoundExpr:
c.markExpr(env, x.Expr)

case *adt.CallExpr:
c.markExpr(env, x.Fun)
saved := c.all
c.all = true
for _, a := range x.Args {
c.markExpr(env, a)
}
c.all = saved

case *adt.DisjunctionExpr:
for _, d := range x.Values {
c.markExpr(env, d.Val)
}

case *adt.SliceExpr:
c.markExpr(env, x.X)
c.markExpr(env, x.Lo)
c.markExpr(env, x.Hi)
c.markExpr(env, x.Stride)

case *adt.ListLit:
for _, e := range x.Elems {
switch x := e.(type) {
case adt.Yielder:
c.markYielder(env, x)

case adt.Expr:
c.markSubExpr(env, x)

case *adt.Ellipsis:
if x.Value != nil {
c.markSubExpr(env, x.Value)
}
}
}

case *adt.StructLit:
env := &adt.Environment{Up: env, Vertex: empty}
for _, e := range x.Decls {
c.markDecl(env, e)
}
}
}

// markResolve resolves dependencies.
func (c *visitor) markResolver(env *adt.Environment, r adt.Resolver) {
switch x := r.(type) {
case nil:
case *adt.LetReference:
saved := c.ctxt.PushState(env, nil)
env := c.ctxt.Env(x.UpCount)
c.markExpr(env, x.X)
c.ctxt.PopState(saved)
return
}

if ref, _ := c.ctxt.Resolve(env, r); ref != nil {
if ref != c.node {
d := Dependency{
Node: ref,
Reference: r,
top: c.top,
}
if err := c.visit(d); err != nil {
c.err = err
panic(aborted)
}
}

return
}

// It is possible that a reference cannot be resolved because it is
// incomplete. In this case, we should check whether subexpressions of the
// reference can be resolved to mark those dependencies. For instance,
// prefix paths of selectors and the value or index of an index experssion
// may independently resolve to a valid dependency.

switch x := r.(type) {
case *adt.NodeLink:
panic("unreachable")

case *adt.IndexExpr:
c.markExpr(env, x.X)
c.markExpr(env, x.Index)

case *adt.SelectorExpr:
c.markExpr(env, x.X)
}
}

func (c *visitor) markSubExpr(env *adt.Environment, x adt.Expr) {
if c.all {
saved := c.top
c.top = false
c.markExpr(env, x)
c.top = saved
}
}

func (c *visitor) markDecl(env *adt.Environment, d adt.Decl) {
switch x := d.(type) {
case *adt.Field:
c.markSubExpr(env, x.Value)

case *adt.OptionalField:
// when dynamic, only continue if there is evidence of
// the field in the parallel actual evaluation.
c.markSubExpr(env, x.Value)

case *adt.BulkOptionalField:
c.markExpr(env, x.Filter)
// when dynamic, only continue if there is evidence of
// the field in the parallel actual evaluation.
c.markSubExpr(env, x.Value)

case *adt.DynamicField:
c.markExpr(env, x.Key)
// when dynamic, only continue if there is evidence of
// a matching field in the parallel actual evaluation.
c.markSubExpr(env, x.Value)

case adt.Yielder:
c.markYielder(env, x)

case adt.Expr:
c.markExpr(env, x)

case *adt.Ellipsis:
if x.Value != nil {
c.markSubExpr(env, x.Value)
}
}
}

func (c *visitor) markYielder(env *adt.Environment, y adt.Yielder) {
switch x := y.(type) {
case *adt.ForClause:
c.markExpr(env, x.Src)
env := &adt.Environment{Up: env, Vertex: empty}
c.markYielder(env, x.Dst)
// In dynamic mode, iterate over all actual value and
// evaluate.

case *adt.LetClause:
c.markExpr(env, x.Expr)
env := &adt.Environment{Up: env, Vertex: empty}
c.markYielder(env, x.Dst)

case *adt.IfClause:
c.markExpr(env, x.Condition)
// In dynamic mode, only continue if condition is true.
c.markYielder(env, x.Dst)

case *adt.ValueClause:
c.markExpr(env, x.StructLit)
}
}

0 comments on commit c60e115

Please sign in to comment.