diff --git a/gg/gg.go b/gg/gg.go index 78a014b..7d767dc 100644 --- a/gg/gg.go +++ b/gg/gg.go @@ -94,6 +94,15 @@ func (oe OpenEdge) String() string { 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 @@ -274,14 +283,16 @@ func (g *Graph) String() string { return fmt.Sprintf("graph(%s)", strings.Join(strs, ", ")) } -func (g *Graph) valIn(val Value) graphValueIn { +// 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 + return valIn.cp().edges } } - return graphValueIn{val: val} + return nil } // AddValueIn takes a OpenEdge and connects it to the Value Vertex containing @@ -290,16 +301,17 @@ func (g *Graph) valIn(val Value) graphValueIn { // be created in this step. func (g *Graph) AddValueIn(oe OpenEdge, val Value) *Graph { - valIn := g.valIn(val) + edges := g.ValueIns(val) - for _, existingOE := range valIn.edges { + for _, existingOE := range edges { if existingOE.equal(oe) { return g } } - valIn = valIn.cp() - valIn.edges = append(valIn.edges, oe) + // 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() diff --git a/vm/op.go b/vm/op.go new file mode 100644 index 0000000..cacdf19 --- /dev/null +++ b/vm/op.go @@ -0,0 +1,108 @@ +package vm + +import "github.com/mediocregopher/ginger/gg" + +var ( + inVal = nameVal("in") + outVal = nameVal("out") +) + +// Operation is an entity which can accept a single argument (the OpenEdge), +// perform some internal processing on that argument, and return a resultant +// Value. +// +// The Scope passed into Perform can be used to Evaluate the OpenEdge, as +// needed. +type Operation interface { + Perform(gg.OpenEdge, Scope) (Value, error) +} + +func preEvalValOp(fn func(Value) (Value, error)) Operation { + + return OperationFunc(func(edge gg.OpenEdge, scope Scope) (Value, error) { + + edgeVal, err := EvaluateEdge(edge, scope) + + if err != nil { + return Value{}, err + } + + return fn(edgeVal) + }) +} + +// 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 +// 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 { + + return preEvalValOp(func(val Value) (Value, error) { + + var edge gg.OpenEdge + + if len(val.Tuple) > 0 { + + tupEdges := make([]gg.OpenEdge, len(val.Tuple)) + + for i := range val.Tuple { + tupEdges[i] = gg.ValueOut(val.Tuple[i].Value, gg.ZeroValue) + } + + edge = gg.TupleOut(tupEdges, gg.ZeroValue) + + } else { + + edge = gg.ValueOut(val.Value, gg.ZeroValue) + + } + + return fn(edge) + }) + +} + +type graphOp struct { + *gg.Graph + scope Scope +} + +// OperationFromGraph wraps the given Graph such that it can be used as an +// operation. +// +// When Perform is called the passed in OpenEdge is set to the "in" name value +// 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 { + return &graphOp{ + Graph: g, + scope: scope, + } +} + +func (g *graphOp) Perform(edge gg.OpenEdge, scope Scope) (Value, error) { + + return preEvalEdgeOp(func(edge gg.OpenEdge) (Value, error) { + + scope = ScopeFromGraph( + g.Graph.AddValueIn(edge, inVal.Value), + g.scope, + ) + + return scope.Evaluate(outVal) + + }).Perform(edge, scope) + +} + +// OperationFunc is a function which implements the Operation interface. +type OperationFunc func(gg.OpenEdge, Scope) (Value, error) + +// Perform calls the underlying OperationFunc directly. +func (f OperationFunc) Perform(edge gg.OpenEdge, scope Scope) (Value, error) { + return f(edge, scope) +} diff --git a/vm/scope.go b/vm/scope.go new file mode 100644 index 0000000..aca1ef7 --- /dev/null +++ b/vm/scope.go @@ -0,0 +1,178 @@ +package vm + +import ( + "fmt" + + "github.com/mediocregopher/ginger/gg" +) + +// Scope encapsulates a set of names and the values they indicate, or the means +// by which to obtain those values, and allows for the evaluation of a name to +// its value. +type Scope interface { + + // Evaluate accepts a name Value and returns the real Value which that name + // points to. + Evaluate(Value) (Value, error) + + // NewScope returns a new Scope which sub-operations within this Scope + // should use for themselves. + NewScope() Scope +} + +// edgeToValue ignores the edgeValue, it only evaluates the edge's vertex as a +// Value. +func edgeToValue(edge gg.OpenEdge, scope Scope) (Value, error) { + + if ggVal, ok := edge.FromValue(); ok { + + val := Value{Value: ggVal} + + if val.Name != nil { + return scope.Evaluate(val) + } + + return val, nil + } + + var tupVal Value + + tup, _ := edge.FromTuple() + + for _, tupEdge := range tup { + + val, err := EvaluateEdge(tupEdge, scope) + + if err != nil { + return Value{}, err + } + + tupVal.Tuple = append(tupVal.Tuple, val) + } + + if len(tupVal.Tuple) == 1 { + return tupVal.Tuple[0], nil + } + + return tupVal, nil +} + +// 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) { + + edgeVal := Value{Value: edge.EdgeValue()} + + if edgeVal.IsZero() { + return edgeToValue(edge, scope) + } + + edge = edge.WithEdgeValue(gg.ZeroValue) + + if edgeVal.Name != nil { + + var err error + + if edgeVal, err = scope.Evaluate(edgeVal); err != nil { + return Value{}, err + } + } + + if edgeVal.Graph != nil { + + edgeVal = Value{ + Operation: OperationFromGraph(edgeVal.Graph, scope.NewScope()), + } + } + + if edgeVal.Operation == nil { + return Value{}, fmt.Errorf("edge value must be an operation") + } + + return edgeVal.Operation.Perform(edge, scope) +} + +// ScopeMap implements the Scope interface. +type ScopeMap map[string]Value + +var _ Scope = ScopeMap{} + +// Evaluate uses the given name Value as a key into the ScopeMap map, and +// returns the Value held there for the key, if any. +func (m ScopeMap) Evaluate(nameVal Value) (Value, error) { + + if nameVal.Name == nil { + return Value{}, fmt.Errorf("value %v is not a name value", nameVal) + } + + val, ok := m[*nameVal.Name] + + if !ok { + return Value{}, fmt.Errorf("%q not defined", *nameVal.Name) + } + + return val, nil +} + +// NewScope returns the ScopeMap as-is. +func (m ScopeMap) NewScope() Scope { + return m +} + +type graphScope struct { + *gg.Graph + parent Scope +} + +// ScopeFromGraph returns a Scope which will use the given Graph for evaluation. +// +// When a name is evaluated, that name will be looked up in the Graph. The +// name's vertex must have only a single OpenEdge leading to it. That edge will +// be followed, with edge values being evaluated to Operations, until a Value +// can be obtained. +// +// If a name does not appear in the Graph, then the given parent Scope will be +// used to evaluate that name. If the parent Scope is nil then an error is +// returned. +// +// NewScope will return the parent scope, if one is given, or an empty ScopeMap +// if not. +func ScopeFromGraph(g *gg.Graph, parent Scope) Scope { + return &graphScope{ + Graph: g, + parent: parent, + } +} + +func (g *graphScope) Evaluate(nameVal Value) (Value, error) { + + if nameVal.Name == nil { + return Value{}, fmt.Errorf("value %v is not a name value", nameVal) + } + + edgesIn := g.ValueIns(nameVal.Value) + + if l := len(edgesIn); l == 0 && g.parent != nil { + + return g.parent.Evaluate(nameVal) + + } else if l != 1 { + + return Value{}, fmt.Errorf( + "%q must have exactly one input edge, found %d input edges", + *nameVal.Name, l, + ) + } + + return EvaluateEdge(edgesIn[0], g) +} + +func (g *graphScope) NewScope() Scope { + + if g.parent == nil { + return ScopeMap{} + } + + return g.parent +} diff --git a/vm/scope_global.go b/vm/scope_global.go new file mode 100644 index 0000000..a516113 --- /dev/null +++ b/vm/scope_global.go @@ -0,0 +1,33 @@ +package vm + +import ( + "fmt" + + "github.com/mediocregopher/ginger/gg" +) + +// GlobalScope contains operations and values which are available from within +// any operation in a ginger program. +var GlobalScope = ScopeMap{ + + "add": Value{Operation: preEvalValOp(func(val Value) (Value, error) { + + if len(val.Tuple) == 0 { + return Value{}, fmt.Errorf("add requires a non-zero tuple of numbers as an argument") + } + + var sum int64 + + for _, tupVal := range val.Tuple { + + if tupVal.Number == nil { + return Value{}, fmt.Errorf("add requires a non-zero tuple of numbers as an argument") + } + + sum += *tupVal.Number + } + + return Value{Value: gg.Value{Number: &sum}}, nil + + })}, +} diff --git a/vm/vm.go b/vm/vm.go new file mode 100644 index 0000000..27f22f4 --- /dev/null +++ b/vm/vm.go @@ -0,0 +1,41 @@ +// Package vm implements the execution of gg.Graphs as programs. +package vm + +import ( + "io" + + "github.com/mediocregopher/ginger/gg" +) + +// Value extends a gg.Value to include Operations and Tuples as a possible +// types. +type Value struct { + gg.Value + + Operation + Tuple []Value +} + +func nameVal(n string) Value { + var val Value + val.Name = &n + return val +} + +// EvaluateSource reads and parses the io.Reader as an operation, input is used +// as the argument to the operation, and the resultant value is returned. +// +// scope contains pre-defined operations and values which are available during +// the evaluation. +func EvaluateSource(opSrc io.Reader, input gg.Value, scope Scope) (Value, error) { + lexer := gg.NewLexer(opSrc) + + g, err := gg.DecodeLexer(lexer) + if err != nil { + return Value{}, err + } + + op := OperationFromGraph(g, scope.NewScope()) + + return op.Perform(gg.ValueOut(input, gg.ZeroValue), scope) +} diff --git a/vm/vm_test.go b/vm/vm_test.go new file mode 100644 index 0000000..70adc3c --- /dev/null +++ b/vm/vm_test.go @@ -0,0 +1,29 @@ +package vm + +import ( + "bytes" + "testing" + + "github.com/mediocregopher/ginger/gg" + "github.com/stretchr/testify/assert" +) + +func TestVM(t *testing.T) { + + src := ` + incr = { out = add < (1; in;); }; + + out = incr < incr < in; + ` + + var in int64 = 5 + + val, err := EvaluateSource( + bytes.NewBufferString(src), + gg.Value{Number: &in}, + GlobalScope, + ) + + assert.NoError(t, err) + assert.Equal(t, in+2, *val.Number) +}