From e7991adfaa306145f2f5dbd2cd92dfd9b866255b Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Wed, 29 Dec 2021 12:32:53 -0700 Subject: [PATCH] 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. --- README.md | 44 +++++-- default.nix | 25 ++++ gg/decoder.go | 36 +++--- gg/decoder_test.go | 96 ++++++++------ gg/gg.go | 304 +++----------------------------------------- gg/gg_test.go | 112 ---------------- go.mod | 8 +- graph/graph.go | 284 +++++++++++++++++++++++++++++++++++++++++ graph/graph_test.go | 115 +++++++++++++++++ vm/op.go | 35 ++--- vm/scope.go | 9 +- vm/vm.go | 80 +++++++++++- 12 files changed, 655 insertions(+), 493 deletions(-) create mode 100644 default.nix delete mode 100644 gg/gg_test.go create mode 100644 graph/graph.go create mode 100644 graph/graph_test.go diff --git a/README.md b/README.md index fd77177..d1f6440 100644 --- a/README.md +++ b/README.md @@ -3,26 +3,42 @@ Fibonacci function in ginger: ``` -fib { - decr { out add(in, -1) } +fib = { + decr = { out = add < (in; -1;); }; - out { - n 0(in), - a 1(in), - b 2(in), + out = { + n = 0 < in; + a = 1 < in; + b = 2 < in; - out if( - zero?(n), - a, - recur(decr(n), b, add(a,b)) - ) + out < if < ( + zero? < n; + a; + recur < (decr < n; b; add < (a;b;); ); + ); - }(in, 0, 1) -} + } < (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! diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..87fc74d --- /dev/null +++ b/default.nix @@ -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 + ]; + }; + +} diff --git a/gg/decoder.go b/gg/decoder.go index 46a6189..b803331 100644 --- a/gg/decoder.go +++ b/gg/decoder.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) } diff --git a/gg/decoder_test.go b/gg/decoder_test.go index e3212cb..e7290f1 100644 --- a/gg/decoder_test.go +++ b/gg/decoder_test.go @@ -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) }) } diff --git a/gg/gg.go b/gg/gg.go index 7d767dc..35fbecf 100644 --- a/gg/gg.go +++ b/gg/gg.go @@ -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 "" - 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") + return "" } } - -//////////////////////////////////////////////////////////////////////////////// - -// 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 -} diff --git a/gg/gg_test.go b/gg/gg_test.go deleted file mode 100644 index 069c0e4..0000000 --- a/gg/gg_test.go +++ /dev/null @@ -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)) - }) - } -} diff --git a/go.mod b/go.mod index 6431356..58fcd77 100644 --- a/go.mod +++ b/go.mod @@ -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 +) diff --git a/graph/graph.go b/graph/graph.go new file mode 100644 index 0000000..a8c1589 --- /dev/null +++ b/graph/graph.go @@ -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 +} diff --git a/graph/graph_test.go b/graph/graph_test.go new file mode 100644 index 0000000..1adc502 --- /dev/null +++ b/graph/graph_test.go @@ -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)) + }) + } +} diff --git a/vm/op.go b/vm/op.go index cacdf19..8cc61c2 100644 --- a/vm/op.go +++ b/vm/op.go @@ -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) } diff --git a/vm/scope.go b/vm/scope.go index aca1ef7..f2f5f34 100644 --- a/vm/scope.go +++ b/vm/scope.go @@ -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, diff --git a/vm/vm.go b/vm/vm.go index 27f22f4..73511f7 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -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 "" + + 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) }