Strip out most of the functionality of gg.Graph

`gg.Graph` has been reworked in its internal functionality, to more
closely match the capability of a purely stack-based implementation. The
current implementation is _very_ inefficient, however. Tests have been
deliberately left pretty sparse, as I expect the internals to continue
to change significantly.
This commit is contained in:
Brian Picciano 2021-12-27 10:11:07 -07:00
parent e4bdeb8797
commit 1e30ad6959
3 changed files with 242 additions and 1310 deletions

634
gg/gg.go
View File

@ -1,33 +1,32 @@
// Package gg implements ginger graph creation, traversal, and (de)serialization
package gg
import (
"crypto/rand"
"encoding/hex"
"fmt"
"strings"
)
// Value wraps a go value in a way such that it will be uniquely identified
// within any Graph and between Graphs. Use NewValue to create a Value instance.
// You can create an instance manually as long as ID is globally unique.
// Value represents a value being stored in a Graph. Exactly one field must be
// non-nil.
type Value struct {
ID string
V interface{}
Name *string
Number *int64
Graph *Graph
// TODO coming soon!
// String *string
}
// NewValue returns a Value instance wrapping any go value. The Value returned
// will be independent of the passed in go value. So if the same go value is
// passed in twice then the two returned Value instances will be treated as
// being different values by Graph.
func NewValue(V interface{}) Value {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return Value{
ID: hex.EncodeToString(b),
V: V,
// Equal returns true if the passed in Value is equivalent.
func (v Value) Equal(v2 Value) bool {
switch {
case v.Name != nil && v2.Name != nil && *v.Name == *v2.Name:
return true
case v.Number != nil && v2.Number != nil && *v.Number == *v2.Number:
return true
case v.Graph != nil && v2.Graph != nil && Equal(v.Graph, v2.Graph):
return true
default:
return false
}
}
@ -46,138 +45,15 @@ const (
TupleVertex VertexType = "tuple"
)
// Edge is a uni-directional connection between two vertices with an attribute
// value.
type Edge struct {
From *Vertex
Value Value
To *Vertex
}
// Vertex is a vertex in a Graph. No fields should be modified directly, only
// through method calls.
type Vertex struct {
ID string
VertexType
Value Value // Value is valid if-and-only-if VertexType is ValueVertex
In, Out []Edge
}
////////////////////////////////////////////////////////////////////////////////
// OpenEdge is an un-realized Edge which can't be used for anything except
// constructing graphs. It has no meaning on its own.
type OpenEdge struct {
// fromV will be the source vertex as-if the vertex (and any sub-vertices of
// it) doesn't already exist in the graph. If it or it's sub-vertices does
// already that will need to be taken into account when persisting into the
// graph
fromV vertex
val Value
}
func (oe OpenEdge) id() string {
return fmt.Sprintf("(%s,%s)", oe.fromV.id, oe.val.ID)
}
// vertex is a representation of a vertex in the graph. Each Graph contains a
// set of all the Value vertex instances it knows about. Each of these contains
// all the input OpenEdges which are known for it. So you can think of these
// "top-level" Value vertex instances as root nodes in a tree, and each OpenEdge
// as a branch.
//
// If a OpenEdge contains a fromV which is a Value that vertex won't have its in
// slice populated no matter what. If fromV is a Tuple it will be populated,
// with any sub-Value's not being populated and so-on recursively
//
// When a view is constructed in makeView these Value instances are deduplicated
// and the top-level one's in value is used to properly connect it.
type vertex struct {
id string
VertexType
val Value
in []OpenEdge
}
func (v vertex) cp() vertex {
cp := v
cp.in = make([]OpenEdge, len(v.in))
copy(cp.in, v.in)
return cp
}
func (v vertex) hasOpenEdge(oe OpenEdge) bool {
oeID := oe.id()
for _, in := range v.in {
if in.id() == oeID {
return true
}
}
return false
}
func (v vertex) cpAndDelOpenEdge(oe OpenEdge) (vertex, bool) {
oeID := oe.id()
for i, in := range v.in {
if in.id() == oeID {
v = v.cp()
v.in = append(v.in[:i], v.in[i+1:]...)
return v, true
}
}
return v, false
}
// Graph is a wrapper around a set of connected Vertices.
type Graph struct {
vM map[string]vertex // only contains value vertices
// generated by makeView on-demand
byVal map[string]*Vertex
all map[string]*Vertex
}
// ZeroGraph is the root empty graph, and is the base off which all graphs are
// built.
var ZeroGraph = &Graph{
vM: map[string]vertex{},
byVal: map[string]*Vertex{},
all: map[string]*Vertex{},
}
// this does _not_ copy the view, as it's assumed the only reason to copy a
// graph is to modify it anyway.
func (g *Graph) cp() *Graph {
cp := &Graph{
vM: make(map[string]vertex, len(g.vM)),
}
for vID, v := range g.vM {
cp.vM[vID] = v
}
return cp
}
////////////////////////////////////////////////////////////////////////////////
// Graph creation
func mkVertex(typ VertexType, val Value, ins ...OpenEdge) vertex {
v := vertex{VertexType: typ, in: ins}
switch typ {
case ValueVertex:
v.id = val.ID
v.val = val
case TupleVertex:
inIDs := make([]string, len(ins))
for i := range ins {
inIDs[i] = ins[i].id()
}
v.id = "[" + strings.Join(inIDs, ",") + "]"
default:
panic(fmt.Sprintf("unknown vertex type %q", typ))
}
return v
}
// ValueOut creates a OpenEdge which, when used to construct a Graph, represents
// an edge (with edgeVal attached to it) coming from the ValueVertex containing
// val.
@ -203,355 +79,169 @@ func TupleOut(ins []OpenEdge, edgeVal Value) OpenEdge {
}
}
func (oe OpenEdge) equal(oe2 OpenEdge) bool {
return oe.val.Equal(oe2.val) && oe.fromV.equal(oe2.fromV)
}
type vertex struct {
VertexType
val Value
tup []OpenEdge
}
func mkVertex(typ VertexType, val Value, tupIns ...OpenEdge) vertex {
return vertex{
VertexType: typ,
val: val,
tup: tupIns,
}
}
func (v vertex) equal(v2 vertex) bool {
if v.VertexType != v2.VertexType {
return false
}
if v.VertexType == ValueVertex {
return v.val.Equal(v2.val)
}
if len(v.tup) != len(v2.tup) {
return false
}
for i := range v.tup {
if !v.tup[i].equal(v2.tup[i]) {
return false
}
}
return true
}
type graphValueIn struct {
val Value
edges []OpenEdge
}
func (valIn graphValueIn) cp() graphValueIn {
cp := valIn
cp.edges = make([]OpenEdge, len(valIn.edges))
copy(cp.edges, valIn.edges)
return valIn
}
func (valIn graphValueIn) equal(valIn2 graphValueIn) bool {
if !valIn.val.Equal(valIn2.val) {
return false
}
if len(valIn.edges) != len(valIn2.edges) {
return false
}
outer:
for _, edge := range valIn.edges {
for _, edge2 := range valIn2.edges {
if edge.equal(edge2) {
continue outer
}
}
return false
}
return true
}
// Graph is an immutable container of a set of vertices. The Graph keeps track
// of all Values which terminate an OpenEdge (which may be a tree of Value/Tuple
// vertices).
//
// NOTE The current implementation of Graph is incredibly inefficient, there's
// lots of O(N) operations, unnecessary copying on changes, and duplicate data
// in memory.
type Graph struct {
valIns []graphValueIn
}
// ZeroGraph is the root empty graph, and is the base off which all graphs are
// built.
var ZeroGraph = &Graph{}
func (g *Graph) cp() *Graph {
cp := &Graph{
valIns: make([]graphValueIn, len(g.valIns)),
}
copy(cp.valIns, g.valIns)
return cp
}
////////////////////////////////////////////////////////////////////////////////
// Graph creation
func (g *Graph) valIn(val Value) graphValueIn {
for _, valIn := range g.valIns {
if valIn.val.Equal(val) {
return valIn
}
}
return graphValueIn{val: val}
}
// AddValueIn takes a OpenEdge and connects it to the Value Vertex containing
// val, returning the new Graph which reflects that connection. Any Vertices
// referenced within toe OpenEdge which do not yet exist in the Graph will also
// be created in this step.
func (g *Graph) AddValueIn(oe OpenEdge, val Value) *Graph {
to := mkVertex(ValueVertex, val)
toID := to.id
// if to is already in the graph, pull it out, as it might have existing in
// edges we want to keep
if exTo, ok := g.vM[toID]; ok {
to = exTo
valIn := g.valIn(val)
for _, existingOE := range valIn.edges {
if existingOE.equal(oe) {
return g
}
}
// if the incoming edge already exists in to then there's nothing to do
if to.hasOpenEdge(oe) {
return g
}
valIn = valIn.cp()
valIn.edges = append(valIn.edges, oe)
to = to.cp()
to.in = append(to.in, oe)
g = g.cp()
// starting with to (which we always overwrite) go through vM and
// recursively add in any vertices which aren't already there
var persist func(vertex)
persist = func(v vertex) {
if v.VertexType == ValueVertex {
vID := v.id
if _, ok := g.vM[vID]; !ok {
g.vM[vID] = v
}
} else {
for _, e := range v.in {
persist(e.fromV)
}
for i, existingValIn := range g.valIns {
if existingValIn.val.Equal(val) {
g.valIns[i] = valIn
return g
}
}
delete(g.vM, toID)
persist(to)
for _, e := range to.in {
persist(e.fromV)
}
g.valIns = append(g.valIns, valIn)
return g
}
// DelValueIn takes a OpenEdge and disconnects it from the Value Vertex
// containing val, returning the new Graph which reflects the disconnection. If
// the Value Vertex doesn't exist within the graph, or it doesn't have the given
// OpenEdge, no changes are made. Any vertices referenced by toe OpenEdge for
// which that edge is their only outgoing edge will be removed from the Graph.
func (g *Graph) DelValueIn(oe OpenEdge, val Value) *Graph {
to := mkVertex(ValueVertex, val)
toID := to.id
// pull to out of the graph. if it's not there then bail
var ok bool
if to, ok = g.vM[toID]; !ok {
return g
}
// get new copy of to without the half-edge, or return if the half-edge
// wasn't even in to
to, ok = to.cpAndDelOpenEdge(oe)
if !ok {
return g
}
g = g.cp()
g.vM[toID] = to
// connectedTo returns whether the vertex has any connections with the
// vertex of the given id, descending recursively
var connectedTo func(string, vertex) bool
connectedTo = func(vID string, curr vertex) bool {
for _, in := range curr.in {
if in.fromV.VertexType == ValueVertex && in.fromV.id == vID {
return true
} else if in.fromV.VertexType == TupleVertex && connectedTo(vID, in.fromV) {
return true
}
}
return false
}
// isOrphaned returns whether the given vertex has any connections to other
// nodes in the graph
isOrphaned := func(v vertex) bool {
vID := v.id
if v, ok := g.vM[vID]; ok && len(v.in) > 0 {
return false
}
for vID2, v2 := range g.vM {
if vID2 == vID {
continue
} else if connectedTo(vID, v2) {
return false
}
}
return true
}
// if to is orphaned get rid of it
if isOrphaned(to) {
delete(g.vM, toID)
}
// rmOrphaned descends down the given OpenEdge and removes any Value
// Vertices referenced in it which are now orphaned
var rmOrphaned func(OpenEdge)
rmOrphaned = func(oe OpenEdge) {
if oe.fromV.VertexType == ValueVertex && isOrphaned(oe.fromV) {
delete(g.vM, oe.fromV.id)
} else if oe.fromV.VertexType == TupleVertex {
for _, juncOe := range oe.fromV.in {
rmOrphaned(juncOe)
}
}
}
rmOrphaned(oe)
return g
}
// Union takes in another Graph and returns a new one which is the union of the
// two. Value vertices which are shared between the two will be merged so that
// the new vertex has the input edges of both.
//
// TODO it bothers me that the opposite of Disjoin is Union and not "Join".
func (g *Graph) Union(g2 *Graph) *Graph {
g = g.cp()
for vID, v2 := range g2.vM {
v, ok := g.vM[vID]
if !ok {
v = v2
} else {
for _, v2e := range v2.in {
if !v.hasOpenEdge(v2e) {
v.in = append(v.in, v2e)
}
}
}
g.vM[vID] = v
}
return g
}
// Disjoin splits the Graph into as many independently connected Graphs as it
// contains. Each Graph returned will have vertices connected only within itself
// and not across to the other Graphs, and the Union of all returned Graphs will
// be the original again.
//
// The order of the Graphs returned is not deterministic. (TODO booooooo)
//
// ZeroGraph.Disjoin() returns empty slice.
func (g *Graph) Disjoin() []*Graph {
m := map[string]*Graph{} // maps each id to the Graph it belongs to
mG := map[*Graph]struct{}{} // tracks unique Graphs created
var connectedTo func(vertex) *Graph
connectedTo = func(v vertex) *Graph {
if v.VertexType == ValueVertex {
if g := m[v.id]; g != nil {
return g
}
}
for _, oe := range v.in {
if g := connectedTo(oe.fromV); g != nil {
return g
}
}
return nil
}
// used upon finding out that previously-thought-to-be disconnected vertices
// aren't. Merges the two graphs they're connected into one and updates all
// state internal to this function accordingly.
rejoin := func(gDst, gSrc *Graph) {
for id, v := range gSrc.vM {
gDst.vM[id] = v
m[id] = gDst
}
delete(mG, gSrc)
}
var connectTo func(vertex, *Graph)
connectTo = func(v vertex, g *Graph) {
if v.VertexType == ValueVertex {
if g2, ok := m[v.id]; ok && g != g2 {
rejoin(g, g2)
}
m[v.id] = g
}
for _, oe := range v.in {
connectTo(oe.fromV, g)
}
}
for id, v := range g.vM {
gV := connectedTo(v)
// if gV is nil it means this vertex is part of a new Graph which
// nothing else has been connected to yet.
if gV == nil {
gV = ZeroGraph.cp()
mG[gV] = struct{}{}
}
gV.vM[id] = v
// do this no matter what, because we want to descend in to the in edges
// and mark all of those as being part of this graph too
connectTo(v, gV)
}
gg := make([]*Graph, 0, len(mG))
for g := range mG {
gg = append(gg, g)
}
return gg
}
////////////////////////////////////////////////////////////////////////////////
// Graph traversal
func (g *Graph) makeView() {
if g.byVal != nil {
return
}
g.byVal = make(map[string]*Vertex, len(g.vM))
g.all = map[string]*Vertex{}
var getV func(vertex, bool) *Vertex
getV = func(v vertex, top bool) *Vertex {
V, ok := g.all[v.id]
if !ok {
V = &Vertex{ID: v.id, VertexType: v.VertexType, Value: v.val}
g.all[v.id] = V
}
// we can be sure all Value vertices will be called with top==true at
// some point, so we only need to descend into the input edges if:
// * top is true
// * this is a tuple's first time being gotten
if !top && (ok || v.VertexType != TupleVertex) {
return V
}
V.In = make([]Edge, 0, len(v.in))
for i := range v.in {
fromV := getV(v.in[i].fromV, false)
e := Edge{From: fromV, Value: v.in[i].val, To: V}
fromV.Out = append(fromV.Out, e)
V.In = append(V.In, e)
}
if v.VertexType == ValueVertex {
g.byVal[v.val.ID] = V
}
return V
}
for _, v := range g.vM {
getV(v, true)
}
}
// ValueVertex returns the Value Vertex for the given value. If the Graph
// doesn't contain a vertex for the value then nil is returned.
func (g *Graph) ValueVertex(val Value) *Vertex {
g.makeView()
return g.byVal[val.ID]
}
// ValueVertices returns all Value Vertices in the Graph.
func (g *Graph) ValueVertices() []*Vertex {
g.makeView()
vv := make([]*Vertex, 0, len(g.byVal))
for _, v := range g.byVal {
vv = append(vv, v)
}
return vv
}
// Equal returns whether or not the two Graphs are equivalent in value.
func Equal(g1, g2 *Graph) bool {
if len(g1.vM) != len(g2.vM) {
if len(g1.valIns) != len(g2.valIns) {
return false
}
for v1ID, v1 := range g1.vM {
v2, ok := g2.vM[v1ID]
if !ok {
return false
}
// since the vertices are values we must make sure their input sets are
// the same (which is tricky since they're unordered, unlike a
// tuple's)
if len(v1.in) != len(v2.in) {
return false
}
for _, in := range v1.in {
if !v2.hasOpenEdge(in) {
return false
outer:
for _, valIn1 := range g1.valIns {
for _, valIn2 := range g2.valIns {
if valIn1.equal(valIn2) {
continue outer
}
}
return false
}
return true
}
// TODO Walk, but by edge
// TODO Walk, but without end. AKA FSM
// Iter will iterate through the Graph's vertices, calling the callback on every
// Vertex in the Graph once. The vertex order used is non-deterministic. If the
// callback returns false the iteration is stopped.
func (g *Graph) Iter(callback func(*Vertex) bool) {
g.makeView()
if len(g.byVal) == 0 {
return
}
seen := make(map[*Vertex]bool, len(g.byVal))
var innerWalk func(*Vertex) bool
innerWalk = func(v *Vertex) bool {
if seen[v] {
return true
} else if !callback(v) {
return false
}
seen[v] = true
for _, e := range v.In {
if !innerWalk(e.From) {
return false
}
}
return true
}
for _, v := range g.byVal {
if !innerWalk(v) {
return
}
}
}
// ByID returns all vertices indexed by their ID field.
func (g *Graph) ByID() map[string]*Vertex {
g.makeView()
return g.all
}

View File

@ -1,665 +1,93 @@
package gg
import (
"fmt"
"sort"
"strings"
. "testing"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
func edge(val Value, from *Vertex) Edge {
return Edge{Value: val, From: from}
}
func TestEqual(t *testing.T) {
func value(val Value, in ...Edge) *Vertex {
return &Vertex{
VertexType: ValueVertex,
Value: val,
In: in,
i := func(i int64) Value {
return Value{Number: &i}
}
}
func tuple(val Value, in ...Edge) Edge {
return Edge{
From: &Vertex{
VertexType: TupleVertex,
In: in,
n := func(n string) Value {
return Value{Name: &n}
}
tests := []struct {
a, b *Graph
exp bool
}{
{
a: ZeroGraph,
b: ZeroGraph,
exp: true,
},
Value: val,
}
}
func assertVertexEqual(t *T, exp, got *Vertex, msgAndArgs ...interface{}) bool {
var assertInner func(*Vertex, *Vertex, map[*Vertex]bool) bool
assertInner = func(exp, got *Vertex, m map[*Vertex]bool) bool {
// if got is already in m then we've already looked at it
if m[got] {
return true
}
m[got] = true
assert.Equal(t, exp.VertexType, got.VertexType, msgAndArgs...)
assert.Equal(t, exp.Value, got.Value, msgAndArgs...)
if !assert.Len(t, got.In, len(exp.In), msgAndArgs...) {
return false
}
for i := range exp.In {
assertInner(exp.In[i].From, got.In[i].From, m)
assert.Equal(t, exp.In[i].Value, got.In[i].Value, msgAndArgs...)
assert.Equal(t, got, got.In[i].To)
assert.Contains(t, got.In[i].From.Out, got.In[i])
}
return true
}
return assertInner(exp, got, map[*Vertex]bool{})
}
func assertIter(t *T, expVals, expJuncs int, g *Graph, msgAndArgs ...interface{}) {
seen := map[*Vertex]bool{}
var gotVals, gotJuncs int
g.Iter(func(v *Vertex) bool {
assert.NotContains(t, seen, v, msgAndArgs...)
seen[v] = true
if v.VertexType == ValueVertex {
gotVals++
} else {
gotJuncs++
}
return true
})
assert.Equal(t, expVals, gotVals, msgAndArgs...)
assert.Equal(t, expJuncs, gotJuncs, msgAndArgs...)
}
type graphTest struct {
name string
out func() *Graph
exp []*Vertex
numVals, numJuncs int
}
func mkTest(name string, out func() *Graph, numVals, numJuncs int, exp ...*Vertex) graphTest {
return graphTest{
name: name,
out: out,
exp: exp,
numVals: numVals, numJuncs: numJuncs,
}
}
func TestGraph(t *T) {
var (
v0 = NewValue("v0")
v1 = NewValue("v1")
v2 = NewValue("v2")
v3 = NewValue("v3")
e0 = NewValue("e0")
e00 = NewValue("e00")
e01 = NewValue("e01")
e1 = NewValue("e1")
e10 = NewValue("e10")
e11 = NewValue("e11")
e2 = NewValue("e2")
e20 = NewValue("e20")
e21 = NewValue("e21")
ej0 = NewValue("ej0")
ej1 = NewValue("ej1")
ej2 = NewValue("ej2")
)
tests := []graphTest{
mkTest(
"values-basic",
func() *Graph {
return ZeroGraph.AddValueIn(ValueOut(v0, e0), v1)
},
2, 0,
value(v0),
value(v1, edge(e0, value(v0))),
),
mkTest(
"values-2edges",
func() *Graph {
g0 := ZeroGraph.AddValueIn(ValueOut(v0, e0), v2)
return g0.AddValueIn(ValueOut(v1, e1), v2)
},
3, 0,
value(v0),
value(v1),
value(v2,
edge(e0, value(v0)),
edge(e1, value(v1)),
),
),
mkTest(
"values-separate",
func() *Graph {
g0 := ZeroGraph.AddValueIn(ValueOut(v0, e0), v1)
return g0.AddValueIn(ValueOut(v2, e2), v3)
},
4, 0,
value(v0),
value(v1, edge(e0, value(v0))),
value(v2),
value(v3, edge(e2, value(v2))),
),
mkTest(
"values-circular",
func() *Graph {
return ZeroGraph.AddValueIn(ValueOut(v0, e0), v0)
},
1, 0,
value(v0, edge(e0, value(v0))),
),
mkTest(
"values-circular2",
func() *Graph {
g0 := ZeroGraph.AddValueIn(ValueOut(v0, e0), v1)
return g0.AddValueIn(ValueOut(v1, e1), v0)
},
2, 0,
value(v0, edge(e1, value(v1, edge(e0, value(v0))))),
value(v1, edge(e0, value(v0, edge(e1, value(v1))))),
),
mkTest(
"values-circular3",
func() *Graph {
g0 := ZeroGraph.AddValueIn(ValueOut(v0, e0), v1)
g1 := g0.AddValueIn(ValueOut(v1, e1), v2)
return g1.AddValueIn(ValueOut(v2, e2), v1)
},
3, 0,
value(v0),
value(v1,
edge(e0, value(v0)),
edge(e2, value(v2, edge(e1, value(v1)))),
),
value(v2, edge(e1, value(v1,
edge(e0, value(v0)),
edge(e2, value(v2)),
))),
),
mkTest(
"tuple-basic",
func() *Graph {
e0 := ValueOut(v0, e0)
e1 := ValueOut(v1, e1)
ej0 := TupleOut([]OpenEdge{e0, e1}, ej0)
return ZeroGraph.AddValueIn(ej0, v2)
},
3, 1,
value(v0), value(v1),
value(v2, tuple(ej0,
edge(e0, value(v0)),
edge(e1, value(v1)),
)),
),
mkTest(
"tuple-basic2",
func() *Graph {
e00 := ValueOut(v0, e00)
e10 := ValueOut(v1, e10)
ej0 := TupleOut([]OpenEdge{e00, e10}, ej0)
e01 := ValueOut(v0, e01)
e11 := ValueOut(v1, e11)
ej1 := TupleOut([]OpenEdge{e01, e11}, ej1)
ej2 := TupleOut([]OpenEdge{ej0, ej1}, ej2)
return ZeroGraph.AddValueIn(ej2, v2)
},
3, 3,
value(v0), value(v1),
value(v2, tuple(ej2,
tuple(ej0,
edge(e00, value(v0)),
edge(e10, value(v1)),
),
tuple(ej1,
edge(e01, value(v0)),
edge(e11, value(v1)),
),
)),
),
mkTest(
"tuple-circular",
func() *Graph {
e0 := ValueOut(v0, e0)
e1 := ValueOut(v1, e1)
ej0 := TupleOut([]OpenEdge{e0, e1}, ej0)
g0 := ZeroGraph.AddValueIn(ej0, v2)
e20 := ValueOut(v2, e20)
g1 := g0.AddValueIn(e20, v0)
e21 := ValueOut(v2, e21)
return g1.AddValueIn(e21, v1)
},
3, 1,
value(v0, edge(e20, value(v2, tuple(ej0,
edge(e0, value(v0)),
edge(e1, value(v1, edge(e21, value(v2)))),
)))),
value(v1, edge(e21, value(v2, tuple(ej0,
edge(e0, value(v0, edge(e20, value(v2)))),
edge(e1, value(v1)),
)))),
value(v2, tuple(ej0,
edge(e0, value(v0, edge(e20, value(v2)))),
edge(e1, value(v1, edge(e21, value(v2)))),
)),
),
}
for i := range tests {
t.Logf("test[%d]:%q", i, tests[i].name)
out := tests[i].out()
for j, exp := range tests[i].exp {
msgAndArgs := []interface{}{
"tests[%d].name:%q exp[%d].val:%q",
i, tests[i].name, j, exp.Value.V.(string),
}
v := out.ValueVertex(exp.Value)
if !assert.NotNil(t, v, msgAndArgs...) {
continue
}
assertVertexEqual(t, exp, v, msgAndArgs...)
}
msgAndArgs := []interface{}{
"tests[%d].name:%q",
i, tests[i].name,
}
// sanity check that graphs are equal to themselves
assert.True(t, Equal(out, out), msgAndArgs...)
// test the Iter method in here too
assertIter(t, tests[i].numVals, tests[i].numJuncs, out, msgAndArgs...)
}
}
func TestGraphImmutability(t *T) {
v0 := NewValue("v0")
v1 := NewValue("v1")
e0 := NewValue("e0")
oe0 := ValueOut(v0, e0)
g0 := ZeroGraph.AddValueIn(oe0, v1)
assert.Nil(t, ZeroGraph.ValueVertex(v0))
assert.Nil(t, ZeroGraph.ValueVertex(v1))
assert.NotNil(t, g0.ValueVertex(v0))
assert.NotNil(t, g0.ValueVertex(v1))
// half-edges should be re-usable
v2 := NewValue("v2")
v3a, v3b := NewValue("v3a"), NewValue("v3b")
e1 := NewValue("e1")
oe1 := ValueOut(v2, e1)
g1a := g0.AddValueIn(oe1, v3a)
g1b := g0.AddValueIn(oe1, v3b)
assertVertexEqual(t, value(v3a, edge(e1, value(v2))), g1a.ValueVertex(v3a))
assert.Nil(t, g1a.ValueVertex(v3b))
assertVertexEqual(t, value(v3b, edge(e1, value(v2))), g1b.ValueVertex(v3b))
assert.Nil(t, g1b.ValueVertex(v3a))
// ... even re-usable twice in succession
v3 := NewValue("v3")
v4 := NewValue("v4")
g2 := g0.AddValueIn(oe1, v3).AddValueIn(oe1, v4)
assert.Nil(t, g2.ValueVertex(v3b))
assert.Nil(t, g2.ValueVertex(v3a))
assertVertexEqual(t, value(v3, edge(e1, value(v2))), g2.ValueVertex(v3))
assertVertexEqual(t, value(v4, edge(e1, value(v2))), g2.ValueVertex(v4))
}
func TestGraphDelValueIn(t *T) {
v0 := NewValue("v0")
v1 := NewValue("v1")
e0 := NewValue("e0")
{ // removing from null
g := ZeroGraph.DelValueIn(ValueOut(v0, e0), v1)
assert.True(t, Equal(ZeroGraph, g))
}
e1 := NewValue("e1")
{ // removing edge from vertex which doesn't have that edge
g0 := ZeroGraph.AddValueIn(ValueOut(v0, e0), v1)
g1 := g0.DelValueIn(ValueOut(v0, e1), v1)
assert.True(t, Equal(g0, g1))
}
{ // removing only edge
oe := ValueOut(v0, e0)
g0 := ZeroGraph.AddValueIn(oe, v1)
g1 := g0.DelValueIn(oe, v1)
assert.True(t, Equal(ZeroGraph, g1))
}
ej0 := NewValue("ej0")
v2 := NewValue("v2")
{ // removing only edge (tuple)
oe := TupleOut([]OpenEdge{
ValueOut(v0, e0),
ValueOut(v1, e1),
}, ej0)
g0 := ZeroGraph.AddValueIn(oe, v2)
g1 := g0.DelValueIn(oe, v2)
assert.True(t, Equal(ZeroGraph, g1))
}
{ // removing one of two edges
oe := ValueOut(v1, e0)
g0 := ZeroGraph.AddValueIn(ValueOut(v0, e0), v2)
g1 := g0.AddValueIn(oe, v2)
g2 := g1.DelValueIn(oe, v2)
assert.True(t, Equal(g0, g2))
assert.NotNil(t, g2.ValueVertex(v0))
assert.Nil(t, g2.ValueVertex(v1))
assert.NotNil(t, g2.ValueVertex(v2))
}
e2 := NewValue("e2")
eja, ejb := NewValue("eja"), NewValue("ejb")
v3 := NewValue("v3")
{ // removing one of two edges (tuple)
e0 := ValueOut(v0, e0)
e1 := ValueOut(v1, e1)
e2 := ValueOut(v2, e2)
oeA := TupleOut([]OpenEdge{e0, e1}, eja)
oeB := TupleOut([]OpenEdge{e1, e2}, ejb)
g0a := ZeroGraph.AddValueIn(oeA, v3)
g0b := ZeroGraph.AddValueIn(oeB, v3)
g1 := g0a.Union(g0b).DelValueIn(oeA, v3)
assert.True(t, Equal(g1, g0b))
assert.Nil(t, g1.ValueVertex(v0))
assert.NotNil(t, g1.ValueVertex(v1))
assert.NotNil(t, g1.ValueVertex(v2))
assert.NotNil(t, g1.ValueVertex(v3))
}
{ // removing one of two edges in circular graph
e0 := ValueOut(v0, e0)
e1 := ValueOut(v1, e1)
g0 := ZeroGraph.AddValueIn(e0, v1).AddValueIn(e1, v0)
g1 := g0.DelValueIn(e0, v1)
assert.True(t, Equal(ZeroGraph.AddValueIn(e1, v0), g1))
assert.NotNil(t, g1.ValueVertex(v0))
assert.NotNil(t, g1.ValueVertex(v1))
}
ej := NewValue("ej")
{ // removing to's only edge, sub-nodes have edge to each other
oej := TupleOut([]OpenEdge{
ValueOut(v0, ej0),
ValueOut(v1, ej0),
}, ej)
g0 := ZeroGraph.AddValueIn(oej, v2)
e0 := ValueOut(v0, e0)
g1 := g0.AddValueIn(e0, v1)
g2 := g1.DelValueIn(oej, v2)
assert.True(t, Equal(ZeroGraph.AddValueIn(e0, v1), g2))
assert.NotNil(t, g2.ValueVertex(v0))
assert.NotNil(t, g2.ValueVertex(v1))
assert.Nil(t, g2.ValueVertex(v2))
}
}
// deterministically hashes a Graph.
func graphStr(g *Graph) string {
var vStr func(vertex) string
var oeStr func(OpenEdge) string
vStr = func(v vertex) string {
if v.VertexType == ValueVertex {
return fmt.Sprintf("v:%q\n", v.val.V.(string))
}
s := fmt.Sprintf("j:%d\n", len(v.in))
ssOE := make([]string, len(v.in))
for i := range v.in {
ssOE[i] = oeStr(v.in[i])
}
sort.Strings(ssOE)
return s + strings.Join(ssOE, "")
}
oeStr = func(oe OpenEdge) string {
s := fmt.Sprintf("oe:%q\n", oe.val.V.(string))
return s + vStr(oe.fromV)
}
sVV := make([]string, 0, len(g.vM))
for _, v := range g.vM {
sVV = append(sVV, vStr(v))
}
sort.Strings(sVV)
return strings.Join(sVV, "")
}
func assertEqualSets(t *T, exp, got []*Graph) bool {
if !assert.Equal(t, len(exp), len(got)) {
return false
}
m := map[*Graph]string{}
for _, g := range exp {
m[g] = graphStr(g)
}
for _, g := range got {
m[g] = graphStr(g)
}
sort.Slice(exp, func(i, j int) bool {
return m[exp[i]] < m[exp[j]]
})
sort.Slice(got, func(i, j int) bool {
return m[got[i]] < m[got[j]]
})
b := true
for i := range exp {
b = b || assert.True(t, Equal(exp[i], got[i]), "i:%d exp:%q got:%q", i, m[exp[i]], m[got[i]])
}
return b
}
func TestGraphUnion(t *T) {
assertUnion := func(g1, g2 *Graph) *Graph {
ga := g1.Union(g2)
gb := g2.Union(g1)
assert.True(t, Equal(ga, gb))
return ga
}
assertDisjoin := func(g *Graph, exp ...*Graph) {
ggDisj := g.Disjoin()
assertEqualSets(t, exp, ggDisj)
}
v0 := NewValue("v0")
v1 := NewValue("v1")
e0 := NewValue("e0")
{ // Union with ZeroGraph
assert.True(t, Equal(ZeroGraph, ZeroGraph.Union(ZeroGraph)))
g := ZeroGraph.AddValueIn(ValueOut(v0, e0), v1)
assert.True(t, Equal(g, assertUnion(g, ZeroGraph)))
assertDisjoin(g, g)
}
v2 := NewValue("v2")
v3 := NewValue("v3")
e1 := NewValue("e1")
{ // Two disparate graphs union'd
g0 := ZeroGraph.AddValueIn(ValueOut(v0, e0), v1)
g1 := ZeroGraph.AddValueIn(ValueOut(v2, e1), v3)
g := assertUnion(g0, g1)
assertVertexEqual(t, value(v0), g.ValueVertex(v0))
assertVertexEqual(t, value(v1, edge(e0, value(v0))), g.ValueVertex(v1))
assertVertexEqual(t, value(v2), g.ValueVertex(v2))
assertVertexEqual(t, value(v3, edge(e1, value(v2))), g.ValueVertex(v3))
assertDisjoin(g, g0, g1)
}
va0, vb0 := NewValue("va0"), NewValue("vb0")
va1, vb1 := NewValue("va1"), NewValue("vb1")
va2, vb2 := NewValue("va2"), NewValue("vb2")
ea0, eb0 := NewValue("ea0"), NewValue("eb0")
ea1, eb1 := NewValue("ea1"), NewValue("eb1")
eaj, ebj := NewValue("eaj"), NewValue("ebj")
{ // Two disparate graphs with tuples
ga := ZeroGraph.AddValueIn(TupleOut([]OpenEdge{
ValueOut(va0, ea0),
ValueOut(va1, ea1),
}, eaj), va2)
gb := ZeroGraph.AddValueIn(TupleOut([]OpenEdge{
ValueOut(vb0, eb0),
ValueOut(vb1, eb1),
}, ebj), vb2)
g := assertUnion(ga, gb)
assertVertexEqual(t, value(va0), g.ValueVertex(va0))
assertVertexEqual(t, value(va1), g.ValueVertex(va1))
assertVertexEqual(t,
value(va2, tuple(eaj,
edge(ea0, value(va0)),
edge(ea1, value(va1)))),
g.ValueVertex(va2),
)
assertVertexEqual(t, value(vb0), g.ValueVertex(vb0))
assertVertexEqual(t, value(vb1), g.ValueVertex(vb1))
assertVertexEqual(t,
value(vb2, tuple(ebj,
edge(eb0, value(vb0)),
edge(eb1, value(vb1)))),
g.ValueVertex(vb2),
)
assertDisjoin(g, ga, gb)
}
{ // Two partially overlapping graphs
g0 := ZeroGraph.AddValueIn(ValueOut(v0, e0), v2)
g1 := ZeroGraph.AddValueIn(ValueOut(v1, e1), v2)
g := assertUnion(g0, g1)
assertVertexEqual(t, value(v0), g.ValueVertex(v0))
assertVertexEqual(t, value(v1), g.ValueVertex(v1))
assertVertexEqual(t,
value(v2,
edge(e0, value(v0)),
edge(e1, value(v1)),
),
g.ValueVertex(v2),
)
assertDisjoin(g, g)
}
ej0 := NewValue("ej0")
ej1 := NewValue("ej1")
{ // two partially overlapping graphs with tuples
g0 := ZeroGraph.AddValueIn(TupleOut([]OpenEdge{
ValueOut(v0, e0),
ValueOut(v1, e1),
}, ej0), v2)
g1 := ZeroGraph.AddValueIn(TupleOut([]OpenEdge{
ValueOut(v0, e0),
ValueOut(v1, e1),
}, ej1), v2)
g := assertUnion(g0, g1)
assertVertexEqual(t, value(v0), g.ValueVertex(v0))
assertVertexEqual(t, value(v1), g.ValueVertex(v1))
assertVertexEqual(t,
value(v2,
tuple(ej0, edge(e0, value(v0)), edge(e1, value(v1))),
tuple(ej1, edge(e0, value(v0)), edge(e1, value(v1))),
),
g.ValueVertex(v2),
)
assertDisjoin(g, g)
}
{ // Two equal graphs
g0 := ZeroGraph.AddValueIn(ValueOut(v0, e0), v1)
g := assertUnion(g0, g0)
assertVertexEqual(t, value(v0), g.ValueVertex(v0))
assertVertexEqual(t,
value(v1, edge(e0, value(v0))),
g.ValueVertex(v1),
)
}
{ // Two equal graphs with tuples
g0 := ZeroGraph.AddValueIn(TupleOut([]OpenEdge{
ValueOut(v0, e0),
ValueOut(v1, e1),
}, ej0), v2)
g := assertUnion(g0, g0)
assertVertexEqual(t, value(v0), g.ValueVertex(v0))
assertVertexEqual(t, value(v1), g.ValueVertex(v1))
assertVertexEqual(t,
value(v2,
tuple(ej0, edge(e0, value(v0)), edge(e1, value(v1))),
),
g.ValueVertex(v2),
)
}
}
func TestGraphEqual(t *T) {
assertEqual := func(g1, g2 *Graph) {
assert.True(t, Equal(g1, g2))
assert.True(t, Equal(g2, g1))
}
assertNotEqual := func(g1, g2 *Graph) {
assert.False(t, Equal(g1, g2))
assert.False(t, Equal(g2, g1))
}
assertEqual(ZeroGraph, ZeroGraph) // duh
v0 := NewValue("v0")
v1 := NewValue("v1")
v2 := NewValue("v2")
e0 := NewValue("e0")
e1 := NewValue("e1")
e1a, e1b := NewValue("e1a"), NewValue("e1b")
{
// graph is equal to itself, not to null
e0 := ValueOut(v0, e0)
g0 := ZeroGraph.AddValueIn(e0, v1)
assertNotEqual(g0, ZeroGraph)
assertEqual(g0, g0)
// adding the an existing edge again shouldn't do anything
assertEqual(g0, g0.AddValueIn(e0, v1))
// g1a and g1b have the same vertices, but the edges are different
g1a := g0.AddValueIn(ValueOut(v0, e1a), v2)
g1b := g0.AddValueIn(ValueOut(v0, e1b), v2)
assertNotEqual(g1a, g1b)
}
{ // equal construction should yield equality, even if out of order
ga := ZeroGraph.AddValueIn(ValueOut(v0, e0), v1)
ga = ga.AddValueIn(ValueOut(v1, e1), v2)
gb := ZeroGraph.AddValueIn(ValueOut(v1, e1), v2)
gb = gb.AddValueIn(ValueOut(v0, e0), v1)
assertEqual(ga, gb)
}
ej := NewValue("ej")
{ // tuple basic test
e0 := ValueOut(v0, e0)
e1 := ValueOut(v1, e1)
ga := ZeroGraph.AddValueIn(TupleOut([]OpenEdge{e0, e1}, ej), v2)
gb := ZeroGraph.AddValueIn(TupleOut([]OpenEdge{e1, e0}, ej), v2)
assertEqual(ga, ga)
assertNotEqual(ga, gb)
{
a: ZeroGraph,
b: ZeroGraph.AddValueIn(ValueOut(n("in"), n("incr")), n("out")),
exp: false,
},
{
a: ZeroGraph.AddValueIn(ValueOut(n("in"), n("incr")), n("out")),
b: ZeroGraph.AddValueIn(ValueOut(n("in"), n("incr")), n("out")),
exp: true,
},
{
a: ZeroGraph.AddValueIn(ValueOut(n("in"), n("incr")), n("out")),
b: ZeroGraph.AddValueIn(TupleOut([]OpenEdge{
ValueOut(n("in"), n("ident")),
ValueOut(i(1), n("ident")),
}, n("add")), n("out")),
exp: false,
},
{
// tuples are different order
a: ZeroGraph.AddValueIn(TupleOut([]OpenEdge{
ValueOut(i(1), n("ident")),
ValueOut(n("in"), n("ident")),
}, n("add")), n("out")),
b: ZeroGraph.AddValueIn(TupleOut([]OpenEdge{
ValueOut(n("in"), n("ident")),
ValueOut(i(1), n("ident")),
}, n("add")), n("out")),
exp: false,
},
{
a: ZeroGraph.
AddValueIn(ValueOut(n("in"), n("incr")), n("out")).
AddValueIn(ValueOut(n("in2"), n("incr2")), n("out2")),
b: ZeroGraph.
AddValueIn(ValueOut(n("in"), n("incr")), n("out")),
exp: false,
},
{
a: ZeroGraph.
AddValueIn(ValueOut(n("in"), n("incr")), n("out")).
AddValueIn(ValueOut(n("in2"), n("incr2")), n("out2")),
b: ZeroGraph.
AddValueIn(ValueOut(n("in"), n("incr")), n("out")).
AddValueIn(ValueOut(n("in2"), n("incr2")), n("out2")),
exp: true,
},
{
// order of value ins shouldn't matter
a: ZeroGraph.
AddValueIn(ValueOut(n("in"), n("incr")), n("out")).
AddValueIn(ValueOut(n("in2"), n("incr2")), n("out2")),
b: ZeroGraph.
AddValueIn(ValueOut(n("in2"), n("incr2")), n("out2")).
AddValueIn(ValueOut(n("in"), n("incr")), n("out")),
exp: true,
},
}
for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
assert.Equal(t, test.exp, Equal(test.a, test.b))
})
}
}

View File

@ -1,186 +0,0 @@
package gg
import (
"encoding/json"
"fmt"
)
type openEdgeJSON struct {
From vertexJSON `json:"from"`
ValueID string `json:"valueID"`
}
type vertexJSON struct {
Type VertexType `json:"type"`
ValueID string `json:"valueID,omitempty"`
In []openEdgeJSON `json:"in"`
}
type graphJSON struct {
Values map[string]json.RawMessage `json:"values"`
ValueVertices []vertexJSON `json:"valueVertices"`
}
// MarshalJSON implements the json.Marshaler interface for a Graph. All Values
// in the Graph will have json.Marshal called on them as-is in order to marshal
// them.
func (g *Graph) MarshalJSON() ([]byte, error) {
gJ := graphJSON{
Values: map[string]json.RawMessage{},
ValueVertices: make([]vertexJSON, 0, len(g.vM)),
}
withVal := func(val Value) (string, error) {
if _, ok := gJ.Values[val.ID]; !ok {
valJ, err := json.Marshal(val.V)
if err != nil {
return "", err
}
gJ.Values[val.ID] = json.RawMessage(valJ)
}
return val.ID, nil
}
// two locally defined, mutually recursive functions. This kind of thing
// could probably be abstracted out, I feel like it happens frequently with
// graph code.
var mkIns func([]OpenEdge) ([]openEdgeJSON, error)
var mkVert func(vertex) (vertexJSON, error)
mkIns = func(in []OpenEdge) ([]openEdgeJSON, error) {
inJ := make([]openEdgeJSON, len(in))
for i := range in {
valID, err := withVal(in[i].val)
if err != nil {
return nil, err
}
vJ, err := mkVert(in[i].fromV)
if err != nil {
return nil, err
}
inJ[i] = openEdgeJSON{From: vJ, ValueID: valID}
}
return inJ, nil
}
mkVert = func(v vertex) (vertexJSON, error) {
ins, err := mkIns(v.in)
if err != nil {
return vertexJSON{}, err
}
vJ := vertexJSON{
Type: v.VertexType,
In: ins,
}
if v.VertexType == ValueVertex {
valID, err := withVal(v.val)
if err != nil {
return vJ, err
}
vJ.ValueID = valID
}
return vJ, nil
}
for _, v := range g.vM {
vJ, err := mkVert(v)
if err != nil {
return nil, err
}
gJ.ValueVertices = append(gJ.ValueVertices, vJ)
}
return json.Marshal(gJ)
}
type jsonUnmarshaler struct {
g *Graph
fn func(json.RawMessage) (interface{}, error)
}
// JSONUnmarshaler returns a json.Unmarshaler instance which, when used, will
// unmarshal a json string into the Graph instance being called on here.
//
// The passed in function is used to unmarshal Values (used in both ValueVertex
// vertices and edges) from json strings into go values. The returned inteface{}
// should have already had the unmarshal from the given json string performed on
// it.
//
// The json.Unmarshaler returned can be used many times, but will reset the
// Graph completely before each use.
func (g *Graph) JSONUnmarshaler(fn func(json.RawMessage) (interface{}, error)) json.Unmarshaler {
return jsonUnmarshaler{g: g, fn: fn}
}
func (jm jsonUnmarshaler) UnmarshalJSON(b []byte) error {
*(jm.g) = Graph{}
jm.g.vM = map[string]vertex{}
var gJ graphJSON
if err := json.Unmarshal(b, &gJ); err != nil {
return err
}
vals := map[string]Value{}
getVal := func(valID string) (Value, error) {
if val, ok := vals[valID]; ok {
return val, nil
}
j, ok := gJ.Values[valID]
if !ok {
return Value{}, fmt.Errorf("unmarshaling malformed graph, value with ID %q not defined", valID)
}
V, err := jm.fn(j)
if err != nil {
return Value{}, err
}
val := Value{ID: valID, V: V}
vals[valID] = val
return val, nil
}
var mkIns func([]openEdgeJSON) ([]OpenEdge, error)
var mkVert func(vertexJSON) (vertex, error)
mkIns = func(inJ []openEdgeJSON) ([]OpenEdge, error) {
in := make([]OpenEdge, len(inJ))
for i := range inJ {
val, err := getVal(inJ[i].ValueID)
if err != nil {
return nil, err
}
v, err := mkVert(inJ[i].From)
if err != nil {
return nil, err
}
in[i] = OpenEdge{fromV: v, val: val}
}
return in, nil
}
mkVert = func(vJ vertexJSON) (vertex, error) {
ins, err := mkIns(vJ.In)
if err != nil {
return vertex{}, err
}
var val Value
if vJ.Type == ValueVertex {
if val, err = getVal(vJ.ValueID); err != nil {
return vertex{}, err
}
}
return mkVertex(vJ.Type, val, ins...), nil
}
for _, v := range gJ.ValueVertices {
v, err := mkVert(v)
if err != nil {
return err
}
jm.g.vM[v.id] = v
}
return nil
}