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.
This commit is contained in:
Brian Picciano 2021-12-29 12:32:53 -07:00
parent 6040abc836
commit e7991adfaa
12 changed files with 655 additions and 493 deletions

View File

@ -3,26 +3,42 @@
Fibonacci function in ginger: Fibonacci function in ginger:
``` ```
fib { fib = {
decr { out add(in, -1) } decr = { out = add < (in; -1;); };
out { out = {
n 0(in), n = 0 < in;
a 1(in), a = 1 < in;
b 2(in), b = 2 < in;
out if( out < if < (
zero?(n), zero? < n;
a, a;
recur(decr(n), b, add(a,b)) recur < (decr < n; b; add < (a;b;); );
) );
}(in, 0, 1) } < (in; 0; 1;);
} };
``` ```
Usage of the function to generate the 6th fibonnaci number: 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!

25
default.nix Normal file
View File

@ -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
];
};
}

View File

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"io" "io"
"strconv" "strconv"
"github.com/mediocregopher/ginger/graph"
) )
// Punctuations which are used in the gg file format. // Punctuations which are used in the gg file format.
@ -88,7 +90,7 @@ func (d *decoder) parseSingleValue(
func (d *decoder) parseOpenEdge( func (d *decoder) parseOpenEdge(
toks []LexerToken, toks []LexerToken,
) ( ) (
OpenEdge, []LexerToken, error, graph.OpenEdge[Value], []LexerToken, error,
) { ) {
if isPunct(toks[0], punctOpenTuple) { if isPunct(toks[0], punctOpenTuple) {
@ -111,31 +113,31 @@ func (d *decoder) parseOpenEdge(
} }
if err != nil { if err != nil {
return OpenEdge{}, nil, err return graph.OpenEdge[Value]{}, nil, err
} }
if termed { if termed {
return ValueOut(val, ZeroValue), toks, nil return graph.ValueOut[Value](val, ZeroValue), toks, nil
} }
opTok, toks := toks[0], toks[1:] opTok, toks := toks[0], toks[1:]
if !isPunct(opTok, punctOp) { 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 { 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) oe, toks, err := d.parseOpenEdge(toks)
if err != nil { 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 return oe, toks, nil
} }
@ -143,17 +145,17 @@ func (d *decoder) parseOpenEdge(
func (d *decoder) parseTuple( func (d *decoder) parseTuple(
toks []LexerToken, toks []LexerToken,
) ( ) (
OpenEdge, []LexerToken, error, graph.OpenEdge[Value], []LexerToken, error,
) { ) {
openTok, toks := toks[0], toks[1:] openTok, toks := toks[0], toks[1:]
var edges []OpenEdge var edges []graph.OpenEdge[Value]
for { for {
if len(toks) == 0 { 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) { } else if isPunct(toks[0], punctCloseTuple) {
toks = toks[1:] toks = toks[1:]
@ -161,14 +163,14 @@ func (d *decoder) parseTuple(
} }
var ( var (
oe OpenEdge oe graph.OpenEdge[Value]
err error err error
) )
oe, toks, err = d.parseOpenEdge(toks) oe, toks, err = d.parseOpenEdge(toks)
if err != nil { if err != nil {
return OpenEdge{}, nil, err return graph.OpenEdge[Value]{}, nil, err
} }
edges = append(edges, oe) edges = append(edges, oe)
@ -181,7 +183,7 @@ func (d *decoder) parseTuple(
toks = toks[1:] 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. // 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:] openTok, toks = toks[0], toks[1:]
} }
g := ZeroGraph g := new(graph.Graph[Value])
for { for {
@ -252,7 +254,7 @@ func (d *decoder) parseGraphValue(
return val, toks, termed, nil 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 { if len(toks) == 0 {
return into, nil, nil 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 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 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 // 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, // 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. // 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{} decoder := &decoder{}
return decoder.decode(lexer) return decoder.decode(lexer)
} }

View File

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

304
gg/gg.go
View File

@ -3,7 +3,8 @@ package gg
import ( import (
"fmt" "fmt"
"strings"
"github.com/mediocregopher/ginger/graph"
) )
// ZeroValue is a Value with no fields set. // ZeroValue is a Value with no fields set.
@ -15,7 +16,7 @@ type Value struct {
// Only one of these fields may be set // Only one of these fields may be set
Name *string Name *string
Number *int64 Number *int64
Graph *Graph Graph *graph.Graph[Value]
// TODO coming soon! // TODO coming soon!
// String *string // String *string
@ -28,15 +29,22 @@ type Value struct {
// IsZero returns true if the Value is the zero value (none of the sub-value // IsZero returns true if the Value is the zero value (none of the sub-value
// fields are set). LexerToken is ignored for this check. // fields are set). LexerToken is ignored for this check.
func (v Value) IsZero() bool { func (v Value) IsZero() bool {
v.LexerToken = nil return v.Equal(ZeroValue)
return v == Value{}
} }
// Equal returns true if the passed in Value is equivalent. // Equal returns true if the passed in Value is equivalent, ignoring the
func (v Value) Equal(v2 Value) bool { // 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 { switch {
case v.IsZero() && v2.IsZero(): case v == ZeroValue && v2 == ZeroValue:
return true return true
case v.Name != nil && v2.Name != nil && *v.Name == *v2.Name: 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: case v.Number != nil && v2.Number != nil && *v.Number == *v2.Number:
return true 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 return true
default: default:
@ -57,9 +65,6 @@ func (v Value) String() string {
switch { switch {
case v.IsZero():
return "<zero>"
case v.Name != nil: case v.Name != nil:
return *v.Name return *v.Name
@ -70,281 +75,6 @@ func (v Value) String() string {
return v.Graph.String() return v.Graph.String()
default: default:
panic("unknown value kind") return "<zero>"
} }
} }
////////////////////////////////////////////////////////////////////////////////
// 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 true
}

View File

@ -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))
})
}
}

8
go.mod
View File

@ -1,5 +1,11 @@
module github.com/mediocregopher/ginger module github.com/mediocregopher/ginger
go 1.16 go 1.18
require github.com/stretchr/testify v1.7.0 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
)

284
graph/graph.go Normal file
View File

@ -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
}

115
graph/graph_test.go Normal file
View File

@ -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))
})
}
}

View File

@ -1,6 +1,9 @@
package vm package vm
import "github.com/mediocregopher/ginger/gg" import (
"github.com/mediocregopher/ginger/gg"
"github.com/mediocregopher/ginger/graph"
)
var ( var (
inVal = nameVal("in") inVal = nameVal("in")
@ -14,12 +17,12 @@ var (
// The Scope passed into Perform can be used to Evaluate the OpenEdge, as // The Scope passed into Perform can be used to Evaluate the OpenEdge, as
// needed. // needed.
type Operation interface { 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 { 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) 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 // 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 // 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. // value), we have to use a Tuple vertex instead.
// //
// This also doesn't yet support passing an operation as a value to another // This also doesn't yet support passing an operation as a value to another
// operation. // 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) { return preEvalValOp(func(val Value) (Value, error) {
var edge gg.OpenEdge var edge graph.OpenEdge[gg.Value]
if len(val.Tuple) > 0 { 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 { 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 { } 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 { type graphOp struct {
*gg.Graph *graph.Graph[gg.Value]
scope Scope scope Scope
} }
@ -77,16 +80,16 @@ type graphOp struct {
// of the given Graph, then that resultant graph and the given parent Scope are // 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 // used to construct a new Scope. The "out" name value is Evaluated on that
// Scope to obtain a resultant Value. // 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{ return &graphOp{
Graph: g, Graph: g,
scope: scope, 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( scope = ScopeFromGraph(
g.Graph.AddValueIn(edge, inVal.Value), 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. // 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. // 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) return f(edge, scope)
} }

View File

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

View File

@ -3,10 +3,16 @@ package vm
import ( import (
"io" "io"
"fmt"
"strings"
"github.com/mediocregopher/ginger/gg" "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 // Value extends a gg.Value to include Operations and Tuples as a possible
// types. // types.
type Value struct { type Value struct {
@ -16,6 +22,78 @@ type Value struct {
Tuple []Value 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 { func nameVal(n string) Value {
var val Value var val Value
val.Name = &n val.Name = &n
@ -37,5 +115,5 @@ func EvaluateSource(opSrc io.Reader, input gg.Value, scope Scope) (Value, error)
op := OperationFromGraph(g, scope.NewScope()) 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)
} }