From 3a2423a9371f112aa747931a9c7e10209490809d Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Thu, 30 Dec 2021 14:00:04 -0700 Subject: [PATCH] Refactor vm to use MapReduce and Thunks The new code is much simpler, and is able to handle more cases than before, such as the `if` operation. --- vm/op.go | 125 +++++++++++++++++++++---------------------- vm/scope.go | 150 ++++++++++++++++++++++++++-------------------------- vm/vm.go | 10 +++- 3 files changed, 144 insertions(+), 141 deletions(-) diff --git a/vm/op.go b/vm/op.go index b50a5f1..cd366ff 100644 --- a/vm/op.go +++ b/vm/op.go @@ -2,70 +2,73 @@ package vm import ( "github.com/mediocregopher/ginger/gg" - "github.com/mediocregopher/ginger/graph" ) 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. +// Thunk is returned from the performance of an Operation. When called it will +// return the result of that Operation having been called with the particular +// arguments which were passed in. +type Thunk func() (Value, error) + +func valThunk(val Value) Thunk { + return func() (Value, error) { return val, nil } +} + +// evalThunks is used to coalesce the results of multiple Thunks into a single +// Thunk which will return a tuple Value. As a special case, if only one Thunk +// is given then it is returned directly (1-tuple is equivalent to its only +// element). +func evalThunks(args []Thunk) Thunk { + + if len(args) == 1 { + return args[0] + } + + return func() (Value, error) { + + var ( + err error + tupVals = make([]Value, len(args)) + ) + + for i := range args { + if tupVals[i], err = args[i](); err != nil { + return ZeroValue, err + } + } + + return Value{Tuple: tupVals}, nil + } +} + + +// Operation is an entity which can accept one or more arguments (each not +// having been evaluated yet) and return a Thunk which will perform some internal processing on those +// arguments and return a resultant Value. type Operation interface { - Perform(*gg.OpenEdge, Scope) (Value, error) + Perform([]Thunk) (Thunk, error) } func preEvalValOp(fn func(Value) (Value, error)) Operation { - return OperationFunc(func(edge *gg.OpenEdge, scope Scope) (Value, error) { + return OperationFunc(func(args []Thunk) (Thunk, error) { - edgeVal, err := EvaluateEdge(edge, scope) + return func() (Value, error) { - if err != nil { - return Value{}, err - } + val, err := evalThunks(args)() - 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] = graph.ValueOut[gg.Value](gg.ZeroValue, val.Tuple[i].Value) + if err != nil { + return ZeroValue, err } - edge = graph.TupleOut[gg.Value](gg.ZeroValue, tupEdges...) + return fn(val) - } else { + }, nil - edge = graph.ValueOut[gg.Value](gg.ZeroValue, val.Value) - - } - - return fn(edge) }) - } type graphOp struct { @@ -76,10 +79,9 @@ type graphOp struct { // 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. +// The Thunk returned by Perform will evaluate the passed in Thunks, and set +// them to the "in" name value of the given Graph. The "out" name value is +// Evaluated using the given Scope to obtain a resultant Value. func OperationFromGraph(g *gg.Graph, scope Scope) Operation { return &graphOp{ Graph: g, @@ -87,25 +89,18 @@ func OperationFromGraph(g *gg.Graph, scope Scope) Operation { } } -func (g *graphOp) Perform(edge *gg.OpenEdge, scope Scope) (Value, error) { - - return preEvalEdgeOp(func(edge *gg.OpenEdge) (Value, error) { - - scope = ScopeFromGraph( - g.Graph.AddValueIn(inVal.Value, edge), - g.scope, - ) - - return scope.Evaluate(outVal) - - }).Perform(edge, scope) - +func (g *graphOp) Perform(args []Thunk) (Thunk, error) { + return ScopeFromGraph( + g.Graph, + evalThunks(args), + g.scope, + ).Evaluate(outVal) } // OperationFunc is a function which implements the Operation interface. -type OperationFunc func(*gg.OpenEdge, Scope) (Value, error) +type OperationFunc func([]Thunk) (Thunk, error) // Perform calls the underlying OperationFunc directly. -func (f OperationFunc) Perform(edge *gg.OpenEdge, scope Scope) (Value, error) { - return f(edge, scope) +func (f OperationFunc) Perform(args []Thunk) (Thunk, error) { + return f(args) } diff --git a/vm/scope.go b/vm/scope.go index fe23bc1..dbd67bd 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 @@ -11,86 +12,76 @@ import ( // its value. type Scope interface { - // Evaluate accepts a name Value and returns the real Value which that name - // points to. - Evaluate(Value) (Value, error) + // Evaluate accepts a name Value and returns a Thunk which will return the + // real Value which that name points to. + Evaluate(Value) (Thunk, 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()} + thunk, err := graph.MapReduce[gg.Value, gg.Value, Thunk]( + edge, + func(ggVal gg.Value) (Thunk, error) { - if edgeVal.IsZero() { - return edgeToValue(edge, scope) + val := Value{Value: ggVal} + + if val.Name != nil { + return scope.Evaluate(val) + } + + return valThunk(val), nil + + }, + func(ggEdgeVal gg.Value, args []Thunk) (Thunk, error) { + + if ggEdgeVal.IsZero() { + return evalThunks(args), nil + } + + edgeVal := Value{Value: ggEdgeVal} + + if edgeVal.Name != nil { + + nameThunk, err := scope.Evaluate(edgeVal) + + if err != nil { + return nil, err + + } else if edgeVal, err = nameThunk(); err != nil { + return nil, err + } + } + + if edgeVal.Graph != nil { + + edgeVal = Value{ + Operation: OperationFromGraph( + edgeVal.Graph, + scope.NewScope(), + ), + } + } + + if edgeVal.Operation == nil { + return nil, fmt.Errorf("edge value must be an operation") + } + + return edgeVal.Operation.Perform(args) + }, + ) + + if err != nil { + return ZeroValue, err } - 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) + return thunk() } // ScopeMap implements the Scope interface. @@ -100,19 +91,19 @@ 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) { +func (m ScopeMap) Evaluate(nameVal Value) (Thunk, error) { if nameVal.Name == nil { - return Value{}, fmt.Errorf("value %v is not a name value", nameVal) + return nil, 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 nil, fmt.Errorf("%q not defined", *nameVal.Name) } - return val, nil + return valThunk(val), nil } // NewScope returns the ScopeMap as-is. @@ -122,6 +113,7 @@ func (m ScopeMap) NewScope() Scope { type graphScope struct { *gg.Graph + in Thunk parent Scope } @@ -132,23 +124,31 @@ type graphScope struct { // be followed, with edge values being evaluated to Operations, until a Value // can be obtained. // +// As a special case, if the name "in" is evaluated, either directly or as part +// of an outer evaluation, then the given Thunk is used to evaluate the Value. +// // 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 { +func ScopeFromGraph(g *gg.Graph, in Thunk, parent Scope) Scope { return &graphScope{ Graph: g, + in: in, parent: parent, } } -func (g *graphScope) Evaluate(nameVal Value) (Value, error) { +func (g *graphScope) Evaluate(nameVal Value) (Thunk, error) { if nameVal.Name == nil { - return Value{}, fmt.Errorf("value %v is not a name value", nameVal) + return nil, fmt.Errorf("value %v is not a name value", nameVal) + } + + if *nameVal.Name == "in" { + return g.in, nil } edgesIn := g.ValueIns(nameVal.Value) @@ -159,13 +159,13 @@ func (g *graphScope) Evaluate(nameVal Value) (Value, error) { } else if l != 1 { - return Value{}, fmt.Errorf( + return nil, fmt.Errorf( "%q must have exactly one input edge, found %d input edges", *nameVal.Name, l, ) } - return EvaluateEdge(edgesIn[0], g) + return func() (Value, error) { return EvaluateEdge(edgesIn[0], g) }, nil } func (g *graphScope) NewScope() Scope { diff --git a/vm/vm.go b/vm/vm.go index 9c6bf98..d6f8a35 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -115,5 +115,13 @@ func EvaluateSource(opSrc io.Reader, input gg.Value, scope Scope) (Value, error) op := OperationFromGraph(g, scope.NewScope()) - return op.Perform(graph.ValueOut[gg.Value](gg.ZeroValue, input), scope) + thunk, err := op.Perform([]Thunk{ + func() (Value, error) { return Value{Value: input}, nil }, + }) + + if err != nil { + return ZeroValue, err + } + + return thunk() }