Make graph generic

The base graph implementation has been moved into its own package,
`graph`, and been made fully generic, ie the value on each vertex/edge
is a parameterized type. This will allow us to use the graph for both
syntax parsing (gg) and runtime evaluation (vm), with each use-case
being able to use slightly different Value types.
rust
Brian Picciano 2 years ago
parent 6040abc836
commit e7991adfaa
  1. 50
      README.md
  2. 25
      default.nix
  3. 36
      gg/decoder.go
  4. 96
      gg/decoder_test.go
  5. 304
      gg/gg.go
  6. 112
      gg/gg_test.go
  7. 8
      go.mod
  8. 284
      graph/graph.go
  9. 115
      graph/graph_test.go
  10. 35
      vm/op.go
  11. 9
      vm/scope.go
  12. 80
      vm/vm.go

@ -3,26 +3,42 @@
Fibonacci function in ginger:
```
fib {
decr { out add(in, -1) }
out {
n 0(in),
a 1(in),
b 2(in),
out if(
zero?(n),
a,
recur(decr(n), b, add(a,b))
)
}(in, 0, 1)
}
fib = {
decr = { out = add < (in; -1;); };
out = {
n = 0 < in;
a = 1 < in;
b = 2 < in;
out < if < (
zero? < n;
a;
recur < (decr < n; b; add < (a;b;); );
);
} < (in; 0; 1;);
};
```
Usage of the function to generate the 6th fibonnaci number:
```
fib(5)
fib < 5;
```
## Development
Current efforts on ginger are focused on a golang-based virtual machine, which
will then be used to bootstrap the language. go >=1.18 is required for this vm.
If you are on a linux-amd64 machine with nix installed, you can run:
```
nix-shell -A shell
```
from the repo root and you will be dropped into a shell with all dependencies
(including the correct go version) in your PATH, ready to use. This could
probably be expanded to other OSs/architectures easily, if you care to do so
please check out the `default.nix` file and submit a PR!

@ -0,0 +1,25 @@
{
pkgs ? import (fetchTarball {
name = "nixpkgs-21-11";
url = "https://github.com/NixOS/nixpkgs/archive/a7ecde854aee5c4c7cd6177f54a99d2c1ff28a31.tar.gz";
sha256 = "162dywda2dvfj1248afxc45kcrg83appjd0nmdb541hl7rnncf02";
}) { },
}: rec {
# https://go.dev/dl/#go1.18beta1
go = fetchTarball {
name = "go1.18beta1";
url = "https://go.dev/dl/go1.18beta1.linux-amd64.tar.gz";
sha256 = "09sb0viv1ybx6adgx4jym1sckdq3mpjkd6albj06hwnchj5rqn40";
};
shell = pkgs.mkShell {
name = "ginger-dev";
buildInputs = [
go
];
};
}

@ -5,6 +5,8 @@ import (
"fmt"
"io"
"strconv"
"github.com/mediocregopher/ginger/graph"
)
// Punctuations which are used in the gg file format.
@ -88,7 +90,7 @@ func (d *decoder) parseSingleValue(
func (d *decoder) parseOpenEdge(
toks []LexerToken,
) (
OpenEdge, []LexerToken, error,
graph.OpenEdge[Value], []LexerToken, error,
) {
if isPunct(toks[0], punctOpenTuple) {
@ -111,31 +113,31 @@ func (d *decoder) parseOpenEdge(
}
if err != nil {
return OpenEdge{}, nil, err
return graph.OpenEdge[Value]{}, nil, err
}
if termed {
return ValueOut(val, ZeroValue), toks, nil
return graph.ValueOut[Value](val, ZeroValue), toks, nil
}
opTok, toks := toks[0], toks[1:]
if !isPunct(opTok, punctOp) {
return OpenEdge{}, nil, decoderErrf(opTok, "must be %q or %q", punctOp, punctTerm)
return graph.OpenEdge[Value]{}, nil, decoderErrf(opTok, "must be %q or %q", punctOp, punctTerm)
}
if len(toks) == 0 {
return OpenEdge{}, nil, decoderErrf(opTok, "%q cannot terminate an edge declaration", punctOp)
return graph.OpenEdge[Value]{}, nil, decoderErrf(opTok, "%q cannot terminate an edge declaration", punctOp)
}
oe, toks, err := d.parseOpenEdge(toks)
if err != nil {
return OpenEdge{}, nil, err
return graph.OpenEdge[Value]{}, nil, err
}
oe = TupleOut([]OpenEdge{oe}, val)
oe = graph.TupleOut[Value]([]graph.OpenEdge[Value]{oe}, val)
return oe, toks, nil
}
@ -143,17 +145,17 @@ func (d *decoder) parseOpenEdge(
func (d *decoder) parseTuple(
toks []LexerToken,
) (
OpenEdge, []LexerToken, error,
graph.OpenEdge[Value], []LexerToken, error,
) {
openTok, toks := toks[0], toks[1:]
var edges []OpenEdge
var edges []graph.OpenEdge[Value]
for {
if len(toks) == 0 {
return OpenEdge{}, nil, decoderErrf(openTok, "no matching %q", punctCloseTuple)
return graph.OpenEdge[Value]{}, nil, decoderErrf(openTok, "no matching %q", punctCloseTuple)
} else if isPunct(toks[0], punctCloseTuple) {
toks = toks[1:]
@ -161,14 +163,14 @@ func (d *decoder) parseTuple(
}
var (
oe OpenEdge
oe graph.OpenEdge[Value]
err error
)
oe, toks, err = d.parseOpenEdge(toks)
if err != nil {
return OpenEdge{}, nil, err
return graph.OpenEdge[Value]{}, nil, err
}
edges = append(edges, oe)
@ -181,7 +183,7 @@ func (d *decoder) parseTuple(
toks = toks[1:]
}
return TupleOut(edges, ZeroValue), toks, nil
return graph.TupleOut[Value](edges, ZeroValue), toks, nil
}
// returned boolean value indicates if the token following the graph is a term.
@ -201,7 +203,7 @@ func (d *decoder) parseGraphValue(
openTok, toks = toks[0], toks[1:]
}
g := ZeroGraph
g := new(graph.Graph[Value])
for {
@ -252,7 +254,7 @@ func (d *decoder) parseGraphValue(
return val, toks, termed, nil
}
func (d *decoder) parseValIn(into *Graph, toks []LexerToken) (*Graph, []LexerToken, error) {
func (d *decoder) parseValIn(into *graph.Graph[Value], toks []LexerToken) (*graph.Graph[Value], []LexerToken, error) {
if len(toks) == 0 {
return into, nil, nil
@ -283,7 +285,7 @@ func (d *decoder) parseValIn(into *Graph, toks []LexerToken) (*Graph, []LexerTok
return into.AddValueIn(oe, dstVal), toks, nil
}
func (d *decoder) decode(lexer Lexer) (*Graph, error) {
func (d *decoder) decode(lexer Lexer) (*graph.Graph[Value], error) {
var toks []LexerToken
@ -314,7 +316,7 @@ func (d *decoder) decode(lexer Lexer) (*Graph, error) {
// construct a Graph according to the rules of the gg file format. DecodeLexer
// will only return an error if there is a non-EOF file returned from the Lexer,
// or the tokens read cannot be used to construct a valid Graph.
func DecodeLexer(lexer Lexer) (*Graph, error) {
func DecodeLexer(lexer Lexer) (*graph.Graph[Value], error) {
decoder := &decoder{}
return decoder.decode(lexer)
}

@ -5,10 +5,14 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mediocregopher/ginger/graph"
)
func TestDecoder(t *testing.T) {
zeroGraph := new(graph.Graph[Value])
i := func(i int64) Value {
return Value{Number: &i}
}
@ -17,27 +21,37 @@ func TestDecoder(t *testing.T) {
return Value{Name: &n}
}
vOut := func(val, edgeVal Value) graph.OpenEdge[Value] {
return graph.ValueOut(val, edgeVal)
}
tOut := func(ins []graph.OpenEdge[Value], edgeVal Value) graph.OpenEdge[Value] {
return graph.TupleOut(ins, edgeVal)
}
type openEdge = graph.OpenEdge[Value]
tests := []struct {
in string
exp *Graph
exp *graph.Graph[Value]
}{
{
in: "",
exp: ZeroGraph,
exp: zeroGraph,
},
{
in: "out = 1;",
exp: ZeroGraph.AddValueIn(ValueOut(i(1), ZeroValue), n("out")),
exp: zeroGraph.AddValueIn(vOut(i(1), ZeroValue), n("out")),
},
{
in: "out = incr < 1;",
exp: ZeroGraph.AddValueIn(ValueOut(i(1), n("incr")), n("out")),
exp: zeroGraph.AddValueIn(vOut(i(1), n("incr")), n("out")),
},
{
in: "out = a < b < 1;",
exp: ZeroGraph.AddValueIn(
TupleOut(
[]OpenEdge{ValueOut(i(1), n("b"))},
exp: zeroGraph.AddValueIn(
tOut(
[]openEdge{vOut(i(1), n("b"))},
n("a"),
),
n("out"),
@ -45,14 +59,14 @@ func TestDecoder(t *testing.T) {
},
{
in: "out = a < b < (1; c < 2; d < e < 3;);",
exp: ZeroGraph.AddValueIn(
TupleOut(
[]OpenEdge{TupleOut(
[]OpenEdge{
ValueOut(i(1), ZeroValue),
ValueOut(i(2), n("c")),
TupleOut(
[]OpenEdge{ValueOut(i(3), n("e"))},
exp: zeroGraph.AddValueIn(
tOut(
[]openEdge{tOut(
[]openEdge{
vOut(i(1), ZeroValue),
vOut(i(2), n("c")),
tOut(
[]openEdge{vOut(i(3), n("e"))},
n("d"),
),
},
@ -65,15 +79,15 @@ func TestDecoder(t *testing.T) {
},
{
in: "out = a < b < (1; c < (d < 2; 3;); );",
exp: ZeroGraph.AddValueIn(
TupleOut(
[]OpenEdge{TupleOut(
[]OpenEdge{
ValueOut(i(1), ZeroValue),
TupleOut(
[]OpenEdge{
ValueOut(i(2), n("d")),
ValueOut(i(3), ZeroValue),
exp: zeroGraph.AddValueIn(
tOut(
[]openEdge{tOut(
[]openEdge{
vOut(i(1), ZeroValue),
tOut(
[]openEdge{
vOut(i(2), n("d")),
vOut(i(3), ZeroValue),
},
n("c"),
),
@ -87,14 +101,14 @@ func TestDecoder(t *testing.T) {
},
{
in: "out = { a = 1; b = c < d < 2; };",
exp: ZeroGraph.AddValueIn(
ValueOut(
Value{Graph: ZeroGraph.
AddValueIn(ValueOut(i(1), ZeroValue), n("a")).
exp: zeroGraph.AddValueIn(
vOut(
Value{Graph: zeroGraph.
AddValueIn(vOut(i(1), ZeroValue), n("a")).
AddValueIn(
TupleOut(
[]OpenEdge{
ValueOut(i(2), n("d")),
tOut(
[]openEdge{
vOut(i(2), n("d")),
},
n("c"),
),
@ -108,13 +122,13 @@ func TestDecoder(t *testing.T) {
},
{
in: "out = a < { b = 1; } < 2;",
exp: ZeroGraph.AddValueIn(
TupleOut(
[]OpenEdge{
ValueOut(
exp: zeroGraph.AddValueIn(
tOut(
[]openEdge{
vOut(
i(2),
Value{Graph: ZeroGraph.
AddValueIn(ValueOut(i(1), ZeroValue), n("b")),
Value{Graph: zeroGraph.
AddValueIn(vOut(i(1), ZeroValue), n("b")),
},
),
},
@ -125,9 +139,9 @@ func TestDecoder(t *testing.T) {
},
{
in: "a = 1; b = 2;",
exp: ZeroGraph.
AddValueIn(ValueOut(i(1), ZeroValue), n("a")).
AddValueIn(ValueOut(i(2), ZeroValue), n("b")),
exp: zeroGraph.
AddValueIn(vOut(i(1), ZeroValue), n("a")).
AddValueIn(vOut(i(2), ZeroValue), n("b")),
},
}
@ -139,7 +153,7 @@ func TestDecoder(t *testing.T) {
got, err := DecodeLexer(lexer)
assert.NoError(t, err)
assert.True(t, Equal(got, test.exp), "\nexp:%v\ngot:%v", test.exp, got)
assert.True(t, got.Equal(test.exp), "\nexp:%v\ngot:%v", test.exp, got)
})
}

@ -3,7 +3,8 @@ package gg
import (
"fmt"
"strings"
"github.com/mediocregopher/ginger/graph"
)
// ZeroValue is a Value with no fields set.
@ -15,7 +16,7 @@ type Value struct {
// Only one of these fields may be set
Name *string
Number *int64
Graph *Graph
Graph *graph.Graph[Value]
// TODO coming soon!
// String *string
@ -28,15 +29,22 @@ type Value struct {
// IsZero returns true if the Value is the zero value (none of the sub-value
// fields are set). LexerToken is ignored for this check.
func (v Value) IsZero() bool {
v.LexerToken = nil
return v == Value{}
return v.Equal(ZeroValue)
}
// Equal returns true if the passed in Value is equivalent.
func (v Value) Equal(v2 Value) bool {
// Equal returns true if the passed in Value is equivalent, ignoring the
// LexerToken on either Value.
//
// Will panic if the passed in v2 is not a Value from this package.
func (v Value) Equal(v2g graph.Value) bool {
v2 := v2g.(Value)
v.LexerToken, v2.LexerToken = nil, nil
switch {
case v.IsZero() && v2.IsZero():
case v == ZeroValue && v2 == ZeroValue:
return true
case v.Name != nil && v2.Name != nil && *v.Name == *v2.Name:
@ -45,7 +53,7 @@ func (v Value) Equal(v2 Value) bool {
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):
case v.Graph != nil && v2.Graph != nil && v.Graph.Equal(v2.Graph):
return true
default:
@ -57,9 +65,6 @@ func (v Value) String() string {
switch {
case v.IsZero():
return "<zero>"
case v.Name != nil:
return *v.Name
@ -70,281 +75,6 @@ func (v Value) String() string {
return v.Graph.String()
default:
panic("unknown value kind")
}
}
////////////////////////////////////////////////////////////////////////////////
// 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 vertex
edgeVal Value
}
func (oe OpenEdge) String() string {
vertexType := "tup"
if oe.fromV.val != nil {
vertexType = "val"
}
return fmt.Sprintf("%s(%s, %s)", vertexType, oe.fromV.String(), oe.edgeVal.String())
}
// WithEdgeValue returns a copy of the OpenEdge with the given Value replacing
// the previous edge value.
//
// NOTE I _think_ this can be factored out once Graph is genericized.
func (oe OpenEdge) WithEdgeValue(val Value) OpenEdge {
oe.edgeVal = val
return oe
}
// EdgeValue returns the Value which lies on the edge itself.
func (oe OpenEdge) EdgeValue() Value {
return oe.edgeVal
}
// FromValue returns the Value from which the OpenEdge was created via ValueOut,
// or false if it wasn't created via ValueOut.
func (oe OpenEdge) FromValue() (Value, bool) {
if oe.fromV.val == nil {
return ZeroValue, false
}
return *oe.fromV.val, true
}
// FromTuple returns the tuple of OpenEdges from which the OpenEdge was created
// via TupleOut, or false if it wasn't created via TupleOut.
func (oe OpenEdge) FromTuple() ([]OpenEdge, bool) {
if oe.fromV.val != nil {
return nil, false
}
return oe.fromV.tup, true
}
// 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.
func ValueOut(val, edgeVal Value) OpenEdge {
return OpenEdge{fromV: vertex{val: &val}, edgeVal: edgeVal}
}
// TupleOut creates an OpenEdge which, when used to construct a Graph,
// represents an edge (with edgeVal attached to it) coming from the
// TupleVertex comprised of the given ordered-set of input edges.
//
// If len(ins) == 1 && edgeVal.IsZero(), then that single OpenEdge is
// returned as-is.
func TupleOut(ins []OpenEdge, edgeVal Value) OpenEdge {
if len(ins) == 1 {
in := ins[0]
if edgeVal.IsZero() {
return in
}
if in.edgeVal.IsZero() {
in.edgeVal = edgeVal
return in
}
}
return OpenEdge{
fromV: vertex{tup: ins},
edgeVal: edgeVal,
}
}
func (oe OpenEdge) equal(oe2 OpenEdge) bool {
return oe.edgeVal.Equal(oe2.edgeVal) && oe.fromV.equal(oe2.fromV)
}
type vertex struct {
val *Value
tup []OpenEdge
}
func (v vertex) equal(v2 vertex) bool {
if v.val != nil {
return v2.val != nil && 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
}
func (v vertex) String() string {
if v.val != nil {
return v.val.String()
}
strs := make([]string, len(v.tup))
for i := range v.tup {
strs[i] = v.tup[i].String()
}
return fmt.Sprintf("[%s]", strings.Join(strs, ", "))
}
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
}
func (g *Graph) String() string {
var strs []string
for _, valIn := range g.valIns {
for _, oe := range valIn.edges {
strs = append(
strs,
fmt.Sprintf("valIn(%s, %s)", oe.String(), valIn.val.String()),
)
}
}
return fmt.Sprintf("graph(%s)", strings.Join(strs, ", "))
}
// ValueIns returns, if any, all OpenEdges which lead to the given Value in the
// Graph (ie, all those added via AddValueIn).
func (g *Graph) ValueIns(val Value) []OpenEdge {
for _, valIn := range g.valIns {
if valIn.val.Equal(val) {
return valIn.cp().edges
}
}
return nil
}
// 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 {
edges := g.ValueIns(val)
for _, existingOE := range edges {
if existingOE.equal(oe) {
return g
}
}
// ValueIns returns a copy of edges, so we're ok to modify it.
edges = append(edges, oe)
valIn := graphValueIn{val: val, edges: edges}
g = g.cp()
for i, existingValIn := range g.valIns {
if existingValIn.val.Equal(val) {
g.valIns[i] = valIn
return g
}
}
g.valIns = append(g.valIns, valIn)
return g
}
// Equal returns whether or not the two Graphs are equivalent in value.
func Equal(g1, g2 *Graph) bool {
if len(g1.valIns) != len(g2.valIns) {
return false
}
outer:
for _, valIn1 := range g1.valIns {
for _, valIn2 := range g2.valIns {
if valIn1.equal(valIn2) {
continue outer
}
}
return false
return "<zero>"
}
return true
}

@ -1,112 +0,0 @@
package gg
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
func TestEqual(t *testing.T) {
i := func(i int64) Value {
return Value{Number: &i}
}
n := func(n string) Value {
return Value{Name: &n}
}
tests := []struct {
a, b *Graph
exp bool
}{
{
a: ZeroGraph,
b: ZeroGraph,
exp: true,
},
{
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,
},
{
// tuple with no edge value and just a single input edge should be
// equivalent to just that edge.
a: ZeroGraph.AddValueIn(TupleOut([]OpenEdge{
ValueOut(i(1), n("ident")),
}, ZeroValue), n("out")),
b: ZeroGraph.AddValueIn(ValueOut(i(1), n("ident")), n("out")),
exp: true,
},
{
// tuple with an edge value and just a single input edge that has no
// edgeVal should be equivalent to just that edge with the tuple's
// edge value.
a: ZeroGraph.AddValueIn(TupleOut([]OpenEdge{
ValueOut(i(1), ZeroValue),
}, n("ident")), n("out")),
b: ZeroGraph.AddValueIn(ValueOut(i(1), n("ident")), n("out")),
exp: true,
},
{
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))
})
}
}

@ -1,5 +1,11 @@
module github.com/mediocregopher/ginger
go 1.16
go 1.18
require github.com/stretchr/testify v1.7.0
require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)

@ -0,0 +1,284 @@
// Package graph implements a generic directed graph type, with support for
// tuple vertices in addition to traditional "value" vertices.
package graph
import (
"fmt"
"strings"
)
// Value is any value which can be stored within a Graph.
type Value interface {
Equal(Value) bool
String() string
}
// 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[V Value] struct {
fromV vertex[V]
edgeVal V
}
func (oe OpenEdge[V]) equal(oe2 OpenEdge[V]) bool {
return oe.edgeVal.Equal(oe2.edgeVal) && oe.fromV.equal(oe2.fromV)
}
func (oe OpenEdge[V]) String() string {
vertexType := "tup"
if oe.fromV.val != nil {
vertexType = "val"
}
return fmt.Sprintf("%s(%s, %s)", vertexType, oe.fromV.String(), oe.edgeVal.String())
}
// WithEdgeValue returns a copy of the OpenEdge with the given Value replacing
// the previous edge value.
//
// NOTE I _think_ this can be factored out once Graph is genericized.
func (oe OpenEdge[V]) WithEdgeValue(val V) OpenEdge[V] {
oe.edgeVal = val
return oe
}
// EdgeValue returns the Value which lies on the edge itself.
func (oe OpenEdge[V]) EdgeValue() V {
return oe.edgeVal
}
// FromValue returns the Value from which the OpenEdge was created via ValueOut,
// or false if it wasn't created via ValueOut.
func (oe OpenEdge[V]) FromValue() (V, bool) {
if oe.fromV.val == nil {
var zero V
return zero, false
}
return *oe.fromV.val, true
}
// FromTuple returns the tuple of OpenEdges from which the OpenEdge was created
// via TupleOut, or false if it wasn't created via TupleOut.
func (oe OpenEdge[V]) FromTuple() ([]OpenEdge[V], bool) {
if oe.fromV.val != nil {
return nil, false
}
return oe.fromV.tup, true
}
// 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.
func ValueOut[V Value](val, edgeVal V) OpenEdge[V] {
return OpenEdge[V]{fromV: vertex[V]{val: &val}, edgeVal: edgeVal}
}
// TupleOut creates an OpenEdge which, when used to construct a Graph,
// represents an edge (with edgeVal attached to it) coming from the
// TupleVertex comprised of the given ordered-set of input edges.
//
// If len(ins) == 1 && edgeVal.IsZero(), then that single OpenEdge is
// returned as-is.
func TupleOut[V Value](ins []OpenEdge[V], edgeVal V) OpenEdge[V] {
if len(ins) == 1 {
in := ins[0]
var zero V
if edgeVal.Equal(zero) {
return in
}
if in.edgeVal.Equal(zero) {
in.edgeVal = edgeVal
return in
}
}
return OpenEdge[V]{
fromV: vertex[V]{tup: ins},
edgeVal: edgeVal,
}
}
type vertex[V Value] struct {
val *V
tup []OpenEdge[V]
}
func (v vertex[V]) equal(v2 vertex[V]) bool {
if v.val != nil {
return v2.val != nil && (*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
}
func (v vertex[V]) String() string {
if v.val != nil {
return (*v.val).String()
}
strs := make([]string, len(v.tup))
for i := range v.tup {
strs[i] = v.tup[i].String()
}
return fmt.Sprintf("[%s]", strings.Join(strs, ", "))
}
type graphValueIn[V Value] struct {
val V
edges []OpenEdge[V]
}
func (valIn graphValueIn[V]) cp() graphValueIn[V] {
cp := valIn
cp.edges = make([]OpenEdge[V], len(valIn.edges))
copy(cp.edges, valIn.edges)
return valIn
}
func (valIn graphValueIn[V]) equal(valIn2 graphValueIn[V]) 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[V Value] struct {
valIns []graphValueIn[V]
}
func (g *Graph[V]) cp() *Graph[V] {
cp := &Graph[V]{
valIns: make([]graphValueIn[V], len(g.valIns)),
}
copy(cp.valIns, g.valIns)
return cp
}
func (g *Graph[V]) String() string {
var strs []string
for _, valIn := range g.valIns {
for _, oe := range valIn.edges {
strs = append(
strs,
fmt.Sprintf("valIn(%s, %s)", oe.String(), valIn.val.String()),
)
}
}
return fmt.Sprintf("graph(%s)", strings.Join(strs, ", "))
}
// ValueIns returns, if any, all OpenEdges which lead to the given Value in the
// Graph (ie, all those added via AddValueIn).
func (g *Graph[V]) ValueIns(val Value) []OpenEdge[V] {
for _, valIn := range g.valIns {
if valIn.val.Equal(val) {
return valIn.cp().edges
}
}
return nil
}
// AddValueIn takes a OpenEdge and connects it to the Value vertex containing
// val, returning the new Graph which reflects that connection.
func (g *Graph[V]) AddValueIn(oe OpenEdge[V], val V) *Graph[V] {
edges := g.ValueIns(val)
for _, existingOE := range edges {
if existingOE.equal(oe) {
return g
}
}
// ValueIns returns a copy of edges, so we're ok to modify it.
edges = append(edges, oe)
valIn := graphValueIn[V]{val: val, edges: edges}
g = g.cp()
for i, existingValIn := range g.valIns {
if existingValIn.val.Equal(val) {
g.valIns[i] = valIn
return g
}
}
g.valIns = append(g.valIns, valIn)
return g
}
// Equal returns whether or not the two Graphs are equivalent in value.
func (g *Graph[V]) Equal(g2 *Graph[V]) bool {
if len(g.valIns) != len(g2.valIns) {
return false
}
outer:
for _, valIn := range g.valIns {
for _, valIn2 := range g2.valIns {
if valIn.equal(valIn2) {
continue outer
}
}
return false
}
return true
}

@ -0,0 +1,115 @@
package graph
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
type S string
func (s S) Equal(s2 Value) bool { return s == s2.(S) }
func (s S) String() string { return string(s) }
func TestEqual(t *testing.T) {
var (
zeroValue S
zeroGraph = new(Graph[S])
)
tests := []struct {
a, b *Graph[S]
exp bool
}{
{
a: zeroGraph,
b: zeroGraph,
exp: true,
},
{
a: zeroGraph,
b: zeroGraph.AddValueIn(ValueOut[S]("in", "incr"), "out"),
exp: false,
},
{
a: zeroGraph.AddValueIn(ValueOut[S]("in", "incr"), "out"),
b: zeroGraph.AddValueIn(ValueOut[S]("in", "incr"), "out"),
exp: true,
},
{
a: zeroGraph.AddValueIn(ValueOut[S]("in", "incr"), "out"),
b: zeroGraph.AddValueIn(TupleOut[S]([]OpenEdge[S]{
ValueOut[S]("in", "ident"),
ValueOut[S]("1", "ident"),
}, "add"), "out"),
exp: false,
},
{
// tuples are different order
a: zeroGraph.AddValueIn(TupleOut[S]([]OpenEdge[S]{
ValueOut[S]("1", "ident"),
ValueOut[S]("in", "ident"),
}, "add"), "out"),
b: zeroGraph.AddValueIn(TupleOut[S]([]OpenEdge[S]{
ValueOut[S]("in", "ident"),
ValueOut[S]("1", "ident"),
}, "add"), "out"),
exp: false,
},
{
// tuple with no edge value and just a single input edge should be
// equivalent to just that edge.
a: zeroGraph.AddValueIn(TupleOut[S]([]OpenEdge[S]{
ValueOut[S]("1", "ident"),
}, zeroValue), "out"),
b: zeroGraph.AddValueIn(ValueOut[S]("1", "ident"), "out"),
exp: true,
},
{
// tuple with an edge value and just a single input edge that has no
// edgeVal should be equivalent to just that edge with the tuple's
// edge value.
a: zeroGraph.AddValueIn(TupleOut[S]([]OpenEdge[S]{
ValueOut[S]("1", zeroValue),
}, "ident"), "out"),
b: zeroGraph.AddValueIn(ValueOut[S]("1", "ident"), "out"),
exp: true,
},
{
a: zeroGraph.
AddValueIn(ValueOut[S]("in", "incr"), "out").
AddValueIn(ValueOut[S]("in2", "incr2"), "out2"),
b: zeroGraph.
AddValueIn(ValueOut[S]("in", "incr"), "out"),
exp: false,
},
{
a: zeroGraph.
AddValueIn(ValueOut[S]("in", "incr"), "out").
AddValueIn(ValueOut[S]("in2", "incr2"), "out2"),
b: zeroGraph.
AddValueIn(ValueOut[S]("in", "incr"), "out").
AddValueIn(ValueOut[S]("in2", "incr2"), "out2"),
exp: true,
},
{
// order of value ins shouldn't matter
a: zeroGraph.
AddValueIn(ValueOut[S]("in", "incr"), "out").
AddValueIn(ValueOut[S]("in2", "incr2"), "out2"),
b: zeroGraph.
AddValueIn(ValueOut[S]("in2", "incr2"), "out2").
AddValueIn(ValueOut[S]("in", "incr"), "out"),
exp: true,
},
}
for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
assert.Equal(t, test.exp, test.a.Equal(test.b))
})
}
}

@ -1,6 +1,9 @@
package vm
import "github.com/mediocregopher/ginger/gg"
import (
"github.com/mediocregopher/ginger/gg"
"github.com/mediocregopher/ginger/graph"
)
var (
inVal = nameVal("in")
@ -14,12 +17,12 @@ var (
// The Scope passed into Perform can be used to Evaluate the OpenEdge, as
// needed.
type Operation interface {
Perform(gg.OpenEdge, Scope) (Value, error)
Perform(graph.OpenEdge[gg.Value], Scope) (Value, error)
}
func preEvalValOp(fn func(Value) (Value, error)) Operation {
return OperationFunc(func(edge gg.OpenEdge, scope Scope) (Value, error) {
return OperationFunc(func(edge graph.OpenEdge[gg.Value], scope Scope) (Value, error) {
edgeVal, err := EvaluateEdge(edge, scope)
@ -33,30 +36,30 @@ func preEvalValOp(fn func(Value) (Value, error)) Operation {
// NOTE this is a giant hack to get around the fact that we're not yet
// using a genericized Graph implementation, so when we do AddValueIn
// on a gg.Graph we can't use a Tuple value (because gg has no Tuple
// on a graph.Graph[gg.Value] we can't use a Tuple value (because gg has no Tuple
// value), we have to use a Tuple vertex instead.
//
// This also doesn't yet support passing an operation as a value to another
// operation.
func preEvalEdgeOp(fn func(gg.OpenEdge) (Value, error)) Operation {
func preEvalEdgeOp(fn func(graph.OpenEdge[gg.Value]) (Value, error)) Operation {
return preEvalValOp(func(val Value) (Value, error) {
var edge gg.OpenEdge
var edge graph.OpenEdge[gg.Value]
if len(val.Tuple) > 0 {
tupEdges := make([]gg.OpenEdge, len(val.Tuple))
tupEdges := make([]graph.OpenEdge[gg.Value], len(val.Tuple))
for i := range val.Tuple {
tupEdges[i] = gg.ValueOut(val.Tuple[i].Value, gg.ZeroValue)
tupEdges[i] = graph.ValueOut[gg.Value](val.Tuple[i].Value, gg.ZeroValue)
}
edge = gg.TupleOut(tupEdges, gg.ZeroValue)
edge = graph.TupleOut[gg.Value](tupEdges, gg.ZeroValue)
} else {
edge = gg.ValueOut(val.Value, gg.ZeroValue)
edge = graph.ValueOut[gg.Value](val.Value, gg.ZeroValue)
}
@ -66,7 +69,7 @@ func preEvalEdgeOp(fn func(gg.OpenEdge) (Value, error)) Operation {
}
type graphOp struct {
*gg.Graph
*graph.Graph[gg.Value]
scope Scope
}
@ -77,16 +80,16 @@ type graphOp struct {
// of the given Graph, then that resultant graph and the given parent Scope are
// used to construct a new Scope. The "out" name value is Evaluated on that
// Scope to obtain a resultant Value.
func OperationFromGraph(g *gg.Graph, scope Scope) Operation {
func OperationFromGraph(g *graph.Graph[gg.Value], scope Scope) Operation {
return &graphOp{
Graph: g,
scope: scope,
}
}
func (g *graphOp) Perform(edge gg.OpenEdge, scope Scope) (Value, error) {
func (g *graphOp) Perform(edge graph.OpenEdge[gg.Value], scope Scope) (Value, error) {
return preEvalEdgeOp(func(edge gg.OpenEdge) (Value, error) {
return preEvalEdgeOp(func(edge graph.OpenEdge[gg.Value]) (Value, error) {
scope = ScopeFromGraph(
g.Graph.AddValueIn(edge, inVal.Value),
@ -100,9 +103,9 @@ func (g *graphOp) Perform(edge gg.OpenEdge, scope Scope) (Value, error) {
}
// OperationFunc is a function which implements the Operation interface.
type OperationFunc func(gg.OpenEdge, Scope) (Value, error)
type OperationFunc func(graph.OpenEdge[gg.Value], Scope) (Value, error)
// Perform calls the underlying OperationFunc directly.
func (f OperationFunc) Perform(edge gg.OpenEdge, scope Scope) (Value, error) {
func (f OperationFunc) Perform(edge graph.OpenEdge[gg.Value], scope Scope) (Value, error) {
return f(edge, scope)
}

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/mediocregopher/ginger/gg"
"github.com/mediocregopher/ginger/graph"
)
// Scope encapsulates a set of names and the values they indicate, or the means
@ -22,7 +23,7 @@ type Scope interface {
// edgeToValue ignores the edgeValue, it only evaluates the edge's vertex as a
// Value.
func edgeToValue(edge gg.OpenEdge, scope Scope) (Value, error) {
func edgeToValue(edge graph.OpenEdge[gg.Value], scope Scope) (Value, error) {
if ggVal, ok := edge.FromValue(); ok {
@ -60,7 +61,7 @@ func edgeToValue(edge gg.OpenEdge, scope Scope) (Value, error) {
// EvaluateEdge will use the given Scope to evaluate the edge's ultimate Value,
// after passing all leaf vertices up the tree through all Operations found on
// edge values.
func EvaluateEdge(edge gg.OpenEdge, scope Scope) (Value, error) {
func EvaluateEdge(edge graph.OpenEdge[gg.Value], scope Scope) (Value, error) {
edgeVal := Value{Value: edge.EdgeValue()}
@ -121,7 +122,7 @@ func (m ScopeMap) NewScope() Scope {
}
type graphScope struct {
*gg.Graph
*graph.Graph[gg.Value]
parent Scope
}
@ -138,7 +139,7 @@ type graphScope struct {
//
// NewScope will return the parent scope, if one is given, or an empty ScopeMap
// if not.
func ScopeFromGraph(g *gg.Graph, parent Scope) Scope {
func ScopeFromGraph(g *graph.Graph[gg.Value], parent Scope) Scope {
return &graphScope{
Graph: g,
parent: parent,

@ -3,10 +3,16 @@ package vm
import (
"io"
"fmt"
"strings"
"github.com/mediocregopher/ginger/gg"
"github.com/mediocregopher/ginger/graph"
)
// ZeroValue is a Value with no fields set. It is equivalent to the 0-tuple.
var ZeroValue Value
// Value extends a gg.Value to include Operations and Tuples as a possible
// types.
type Value struct {
@ -16,6 +22,78 @@ type Value struct {
Tuple []Value
}
// IsZero returns true if the Value is the zero value (aka the 0-tuple).
// LexerToken (within the gg.Value) is ignored for this check.
func (v Value) IsZero() bool {
return v.Equal(ZeroValue)
}
// Equal returns true if the passed in Value is equivalent, ignoring the
// LexerToken on either Value.
//
// Will panic if the passed in v2 is not a Value from this package.
func (v Value) Equal(v2g graph.Value) bool {
v2 := v2g.(Value)
switch {
case !v.Value.IsZero() || !v2.Value.IsZero():
return v.Value.Equal(v2.Value)
case v.Operation != nil || v2.Operation != nil:
// for now we say that Operations can't be compared. This will probably
// get revisted later.
return false
case len(v.Tuple) == len(v2.Tuple):
for i := range v.Tuple {
if !v.Tuple[i].Equal(v2.Tuple[i]) {
return false
}
}
return true
default:
// if both were the zero value then both tuples would have the same
// length (0), which is covered by the previous check. So anything left
// over must be tuples with differing lengths.
return false
}
}
func (v Value) String() string {
switch {
case v.Operation != nil:
// We can try to get better strings for ops later
return "<op>"
case !v.Value.IsZero():
return v.Value.String()
default:
// we consider zero value to be the 0-tuple
strs := make([]string, len(v.Tuple))
for i := range v.Tuple {
strs[i] = v.Tuple[i].String()
}
return fmt.Sprintf("(%s)", strings.Join(strs, ", "))
}
}
func nameVal(n string) Value {
var val Value
val.Name = &n
@ -37,5 +115,5 @@ func EvaluateSource(opSrc io.Reader, input gg.Value, scope Scope) (Value, error)
op := OperationFromGraph(g, scope.NewScope())
return op.Perform(gg.ValueOut(input, gg.ZeroValue), scope)
return op.Perform(graph.ValueOut[gg.Value](input, gg.ZeroValue), scope)
}

Loading…
Cancel
Save