blob: 2453fe1ef070ad92d1e356ac8a95a82cd007e8d9 [file] [log] [blame]
// Copyright 2018 The LUCI 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 graph
import (
"fmt"
"sort"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/starlark/builtins"
)
var (
// ErrFinalized is returned by Graph methods that modify the graph when they
// are used on a finalized graph.
ErrFinalized = errors.New("cannot modify a finalized graph")
// ErrNotFinalized is returned by Graph traversal/query methods when they are
// used with a non-finalized graph.
ErrNotFinalized = errors.New("cannot query a graph under construction")
)
// backtrace returns an error message annotated with a backtrace.
func backtrace(err error, t *builtins.CapturedStacktrace) string {
return t.String() + "Error: " + err.Error()
}
// NodeRedeclarationError is returned when a adding an existing node.
type NodeRedeclarationError struct {
Trace *builtins.CapturedStacktrace // where it is added second time
Previous *Node // the previously added node
}
// Error is part of 'error' interface.
func (e *NodeRedeclarationError) Error() string {
// TODO(vadimsh): Improve error messages.
return fmt.Sprintf("%s is redeclared, previous declaration:\n%s", e.Previous, e.Previous.Trace)
}
// Backtrace returns an error message with a backtrace where it happened.
func (e *NodeRedeclarationError) Backtrace() string {
return backtrace(e, e.Trace)
}
// EdgeRedeclarationError is returned when adding an edge that has exact same
// end points and a title as some existing edge.
//
// Edges that link same nodes (in same direction) and differ only in title are
// OK and do not cause this error.
//
// Nodes referenced by the edge may not be fully declared yet (i.e. they may not
// yet have properties or trace associated with them). They do have valid keys
// though.
type EdgeRedeclarationError struct {
Trace *builtins.CapturedStacktrace // where it is added second time
Previous *Edge // the previously added edge
}
// Error is part of 'error' interface.
func (e *EdgeRedeclarationError) Error() string {
// TODO(vadimsh): Improve error messages.
return fmt.Sprintf("relation %q between %s and %s is redeclared, previous declaration:\n%s",
e.Previous.Title, e.Previous.Parent, e.Previous.Child, e.Previous.Trace)
}
// Backtrace returns an error message with a backtrace where it happened.
func (e *EdgeRedeclarationError) Backtrace() string {
return backtrace(e, e.Trace)
}
// CycleError is returned when adding an edge that introduces a cycle.
//
// Nodes referenced by edges may not be fully declared yet (i.e. they may not
// yet have properties or trace associated with them). They do have valid keys
// though.
type CycleError struct {
Trace *builtins.CapturedStacktrace // where the edge is being added
Edge *Edge // an edge being added
Path []*Edge // the rest of the cycle
}
// Error is part of 'error' interface.
func (e *CycleError) Error() string {
// TODO(vadimsh): Improve error messages.
return fmt.Sprintf("relation %q between %s and %s introduces a cycle",
e.Edge.Title, e.Edge.Parent, e.Edge.Child)
}
// Backtrace returns an error message with a backtrace where it happened.
func (e *CycleError) Backtrace() string {
return backtrace(e, e.Trace)
}
// DanglingEdgeError is returned by Finalize if a graph has an edge whose parent
// or child (or both) haven't been declared by AddNode.
//
// Use Edge.(Parent|Child).Declared() to figure out which end is not defined.
type DanglingEdgeError struct {
Edge *Edge
}
// Error is part of 'error' interface.
func (e *DanglingEdgeError) Error() string {
rel := ""
if e.Edge.Title != "" {
rel = fmt.Sprintf(" in %q", e.Edge.Title)
}
// TODO(vadimsh): Improve error messages.
hasP := e.Edge.Parent.Declared()
hasC := e.Edge.Child.Declared()
switch {
case hasP && hasC:
// This should not happen.
return "incorrect DanglingEdgeError, the edge is fully connected"
case !hasP && hasC:
return fmt.Sprintf("%s%s refers to undefined %s",
e.Edge.Child, rel, e.Edge.Parent)
case hasP && !hasC:
return fmt.Sprintf("%s%s refers to undefined %s",
e.Edge.Parent, rel, e.Edge.Child)
default:
return fmt.Sprintf("relation %q: refers to %s and %s, neither is defined",
e.Edge.Title, e.Edge.Parent, e.Edge.Child)
}
}
// Backtrace returns an error message with a backtrace where it happened.
func (e *DanglingEdgeError) Backtrace() string {
return backtrace(e, e.Edge.Trace)
}
// Graph is a DAG of keyed nodes.
//
// It is initially in "under construction" state, in which callers can use
// AddNode and AddEdge (in any order) to build the graph, but can't yet query
// it.
//
// Once the construction is complete, the graph should be finalized via
// Finalize() call, which checks there are no dangling edges and freezes the
// graph (so AddNode/AddEdge return errors), making it queryable.
//
// Graph implements starlark.HasAttrs interface and have the following methods:
// * key(kind1: string, id1: string, kind2: string, id2: string, ...): graph.key
// * add_node(key: graph.key, props={}, idempotent=False, trace=stacktrace()): graph.node
// * add_edge(parent: graph.Key, child: graph.Key, title='', trace=stacktrace())
// * finalize(): []string
// * node(key: graph.key): graph.node
// * children(parent: graph.key, kind: string, order_by='key'): []graph.node
// * parents(child: graph.key, kind: string, order_by='key'): []graph.node
type Graph struct {
KeySet
nodes map[*Key]*Node // all declared and "predeclared" nodes
edges []*Edge // all defined edges, in their definition order
finalized bool // true if the graph is no longer mutable
}
// validateKey returns an error if the key is not from this graph.
func (g *Graph) validateKey(title string, k *Key) error {
if k.set != &g.KeySet {
return fmt.Errorf("bad %s: %s is from another graph", title, k)
}
return nil
}
// initNode either returns an existing *Node, or adds a new one.
//
// The newly added node is in "predeclared" state: it has no Props or Trace.
// A predeclared node is moved to the fully declared state by AddNode, this can
// happen only once for non-idempotent nodes or more than once for idempotent
// nodes, if each time their props dict is exactly same.
//
// Panics if the graph is finalized.
func (g *Graph) initNode(k *Key) *Node {
if g.finalized {
panic(ErrFinalized)
}
if n := g.nodes[k]; n != nil {
return n
}
if g.nodes == nil {
g.nodes = make(map[*Key]*Node, 1)
}
n := &Node{Key: k}
g.nodes[k] = n
return n
}
//// API to mutate the graph before it is finalized.
// AddNode adds a node to the graph.
//
// If such node already exists, either returns an error right away (if
// 'idempotent' is false), or verifies the existing node has also been marked
// as idempotent and has exact same props dict as being passed here.
//
// Trying to use AddNode after the graph has been finalized is an error.
//
// Freezes props.values() as a side effect.
func (g *Graph) AddNode(k *Key, props *starlark.Dict, idempotent bool, trace *builtins.CapturedStacktrace) (*Node, error) {
if g.finalized {
return nil, ErrFinalized
}
if err := g.validateKey("key", k); err != nil {
return nil, err
}
// Only string keys are allowed in 'props'.
for _, pk := range props.Keys() {
if _, ok := pk.(starlark.String); !ok {
return nil, fmt.Errorf("non-string key %s in 'props'", pk)
}
}
propsStruct := starlarkstruct.FromKeywords(starlark.String("props"), props.Items())
n := g.initNode(k)
if !n.Declared() {
n.declare(propsStruct, idempotent, trace)
return n, nil
}
// Only idempotent nodes can be redeclared, and only if all declarations
// are marked as idempotent and they specify exact same props.
if n.Idempotent && idempotent {
switch eq, err := starlark.Equal(propsStruct, n.Props); {
case err != nil:
return nil, err
case eq:
return n, nil
}
}
return nil, &NodeRedeclarationError{Trace: trace, Previous: n}
}
// AddEdge adds an edge to the graph.
//
// Trying to use AddEdge after the graph has been finalized is an error.
//
// Neither of the nodes have to exist yet: it is OK to declare nodes and edges
// in arbitrary order as long as at the end of the graph construction (when it
// is finalized) the graph is complete.
//
// Returns an error if there's already an edge between the given nodes with
// exact same title or the new edge introduces a cycle.
func (g *Graph) AddEdge(parent, child *Key, title string, trace *builtins.CapturedStacktrace) error {
if g.finalized {
return ErrFinalized
}
if err := g.validateKey("parent", parent); err != nil {
return err
}
if err := g.validateKey("child", child); err != nil {
return err
}
edge := &Edge{
Parent: g.initNode(parent),
Child: g.initNode(child),
Title: title,
Trace: trace,
}
// Have this exact edge already?
for _, e := range edge.Parent.children {
if e.Child == edge.Child && e.Title == title {
return &EdgeRedeclarationError{
Trace: trace,
Previous: e,
}
}
}
// The child may have the parent among its descendants already? Then adding
// the edge would introduce a cycle.
err := edge.Child.visitDescendants(nil, func(n *Node, path []*Edge) error {
if n == edge.Parent {
return &CycleError{
Trace: trace,
Edge: edge,
Path: append([]*Edge(nil), path...),
}
}
return nil
})
if err != nil {
return err
}
edge.Parent.children = append(edge.Parent.children, edge)
edge.Child.parents = append(edge.Child.parents, edge)
g.edges = append(g.edges, edge)
return nil
}
// Finalize finishes the graph construction by verifying there are no "dangling"
// edges: all edges ever added by AddEdge should connect nodes that were at some
// point defined by AddNode.
//
// Finalizing an already finalized graph is not an error.
//
// A finalized graph is immutable (and frozen in Starlark sense): all subsequent
// calls to AddNode/AddEdge return an error. Conversely, freezing the graph via
// Freeze() finalizes it, panicking if the finalization fails. Users of Graph
// are expected to finalize the graph themselves (checking errors) before
// Starlark tries to freeze it.
//
// Once finalized, the graph becomes queryable.
func (g *Graph) Finalize() (errs errors.MultiError) {
if g.finalized {
return
}
for _, e := range g.edges {
if !e.Parent.Declared() || !e.Child.Declared() {
errs = append(errs, &DanglingEdgeError{Edge: e})
}
}
g.finalized = len(errs) == 0
return
}
//// API to query the graph after it is finalized.
// Node returns a node by the key or (nil, nil) if there's no such node.
//
// Trying to use Node before the graph has been finalized is an error.
func (g *Graph) Node(k *Key) (*Node, error) {
if !g.finalized {
return nil, ErrNotFinalized
}
if err := g.validateKey("key", k); err != nil {
return nil, err
}
switch n := g.nodes[k]; {
case n == nil:
return nil, nil
case !n.Declared():
panic(fmt.Errorf("impossible not-yet-declared node in a finalized graph: %s", n.Key))
default:
return n, nil
}
}
// Children returns direct children of a node (given by its key) that have the
// given kind (based on the last component of their keys).
//
// The order of the result depends on a value of 'orderBy':
// 'key': nodes are ordered lexicographically by their keys (smaller first).
// 'exec': nodes are ordered by the order edges to them were defined during
// the execution (earlier first).
//
// Any other value of 'orderBy' causes an error.
//
// Trying to use Children before the graph has been finalized is an error.
func (g *Graph) Children(parent *Key, kind, orderBy string) ([]*Node, error) {
return g.sortedListing(parent, "parent", kind, orderBy, (*Node).listChildren)
}
// Parents returns direct parents of a node (given by its key) that have the
// given kind (based on the last component of their keys).
//
// The order of the result depends on a value of 'orderBy':
// 'key': nodes are ordered lexicographically by their keys (smaller first).
// 'exec': nodes are ordered by the order edges to them were defined during
// the execution (earlier first).
//
// Any other value of 'orderBy' causes an error.
//
// Trying to use Parents before the graph has been finalized is an error.
func (g *Graph) Parents(child *Key, kind, orderBy string) ([]*Node, error) {
return g.sortedListing(child, "child", kind, orderBy, (*Node).listParents)
}
// sortedListing is a common implementation of Children and Parents.
func (g *Graph) sortedListing(key *Key, attr, kind, orderBy string, cb func(n *Node) []*Node) ([]*Node, error) {
if !g.finalized {
return nil, ErrNotFinalized
}
if err := g.validateKey(attr, key); err != nil {
return nil, err
}
if orderBy != "key" && orderBy != "exec" {
return nil, fmt.Errorf("unknown order %q, expecting either \"key\" or \"exec\"", orderBy)
}
n := g.nodes[key]
if n == nil {
return nil, nil // no node at all -> no related nodes
}
// Filter relatives by kind.
var out []*Node
for _, relative := range cb(n) {
if relative.Key.Kind() == kind {
out = append(out, relative)
}
}
// Optionally sort. cb is supposed to return relatives in order they were
// defined which matches orderBy == "exec", so need to sort only if asked to
// order by key.
if orderBy == "key" {
sort.Slice(out, func(i, j int) bool { return out[i].Key.Less(out[j].Key) })
}
return out, nil
}
//// starlark.Value interface implementation.
// String is a part of starlark.Value interface
func (g *Graph) String() string { return "graph" }
// Type is a part of starlark.Value interface.
func (g *Graph) Type() string { return "graph" }
// Freeze is a part of starlark.Value interface.
//
// It finalizes the graph, panicking on errors. Users of Graph are expected to
// finalize the graph themselves (checking errors) before Starlark tries to
// freeze it.
func (g *Graph) Freeze() {
if err := g.Finalize(); err != nil {
panic(err)
}
}
// Truth is a part of starlark.Value interface.
func (g *Graph) Truth() starlark.Bool { return starlark.True }
// Hash is a part of starlark.Value interface.
func (g *Graph) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable") }
// AttrNames is a part of starlark.HasAttrs interface.
func (g *Graph) AttrNames() []string {
names := make([]string, 0, len(graphAttrs))
for k := range graphAttrs {
names = append(names, k)
}
sort.Strings(names)
return names
}
// Attr is a part of starlark.HasAttrs interface.
func (g *Graph) Attr(name string) (starlark.Value, error) {
impl, ok := graphAttrs[name]
if !ok {
return nil, nil // per Attr(...) contract
}
return impl.BindReceiver(g), nil
}
//// Starlark bindings for individual graph methods.
func nodesList(nodes []*Node) *starlark.List {
vals := make([]starlark.Value, len(nodes))
for i, n := range nodes {
vals[i] = n
}
return starlark.NewList(vals)
}
var graphAttrs = map[string]*starlark.Builtin{
// key(kind1: string, id1: string, kind2: string, id2: string, ...): graph.key
"key": starlark.NewBuiltin("key", func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if len(kwargs) != 0 {
return nil, fmt.Errorf("graph.key: not expecting keyword arguments")
}
pairs := make([]string, len(args))
for idx, arg := range args {
str, ok := arg.(starlark.String)
if !ok {
return nil, fmt.Errorf("graph.key: all arguments must be strings, arg #%d was %s", idx, arg.Type())
}
pairs[idx] = str.GoString()
}
return b.Receiver().(*Graph).Key(pairs...)
}),
// add_node(key: graph.key, props={}, idempotent=False, trace=stacktrace()): graph.node
"add_node": starlark.NewBuiltin("add_node", func(th *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var key *Key
var props *starlark.Dict
var idempotent starlark.Bool
var trace *builtins.CapturedStacktrace
err := starlark.UnpackArgs("add_node", args, kwargs,
"key", &key,
"props?", &props,
"idempotent?", &idempotent,
"trace?", &trace,
)
if err != nil {
return nil, err
}
if props == nil {
props = &starlark.Dict{}
}
if trace == nil {
var err error
if trace, err = builtins.CaptureStacktrace(th, 0); err != nil {
return nil, err
}
}
return b.Receiver().(*Graph).AddNode(key, props, bool(idempotent), trace)
}),
// add_edge(parent: graph.Key, child: graph.Key, title='', trace=stacktrace())
"add_edge": starlark.NewBuiltin("add_edge", func(th *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var parent *Key
var child *Key
var title starlark.String
var trace *builtins.CapturedStacktrace
err := starlark.UnpackArgs("add_edge", args, kwargs,
"parent", &parent,
"child", &child,
"title?", &title,
"trace?", &trace)
if err != nil {
return nil, err
}
if trace == nil {
var err error
if trace, err = builtins.CaptureStacktrace(th, 0); err != nil {
return nil, err
}
}
err = b.Receiver().(*Graph).AddEdge(parent, child, title.GoString(), trace)
return starlark.None, err
}),
// finalize(): []string
"finalize": starlark.NewBuiltin("finalize", func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if err := starlark.UnpackArgs("finalize", args, kwargs); err != nil {
return nil, err
}
errs := b.Receiver().(*Graph).Finalize()
out := make([]starlark.Value, len(errs))
for i, err := range errs {
out[i] = starlark.String(err.Error())
}
return starlark.NewList(out), nil
}),
// node(key: graph.key): graph.node
"node": starlark.NewBuiltin("node", func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var key *Key
if err := starlark.UnpackArgs("node", args, kwargs, "key", &key); err != nil {
return nil, err
}
if node, err := b.Receiver().(*Graph).Node(key); node != nil || err != nil {
return node, err
}
return starlark.None, nil
}),
// children(parent: graph.key, kind: string, order_by='key'): []graph.node
"children": starlark.NewBuiltin("children", func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var parent *Key
var kind starlark.String
var orderBy starlark.String = "key"
err := starlark.UnpackArgs("children", args, kwargs,
"parent", &parent,
"kind", &kind,
"order_by?", &orderBy)
if err != nil {
return nil, err
}
nodes, err := b.Receiver().(*Graph).Children(parent, kind.GoString(), orderBy.GoString())
if err != nil {
return nil, err
}
return nodesList(nodes), nil
}),
// parents(child: graph.key, kind: string, order_by='key'): []graph.node
"parents": starlark.NewBuiltin("parents", func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var child *Key
var kind starlark.String
var orderBy starlark.String = "key"
err := starlark.UnpackArgs("parents", args, kwargs,
"child", &child,
"kind", &kind,
"order_by?", &orderBy)
if err != nil {
return nil, err
}
nodes, err := b.Receiver().(*Graph).Parents(child, kind.GoString(), orderBy.GoString())
if err != nil {
return nil, err
}
return nodesList(nodes), nil
}),
}