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.
This commit is contained in:
Brian Picciano 2021-12-30 14:00:04 -07:00
parent 223b7f93a5
commit 3a2423a937
3 changed files with 144 additions and 141 deletions

125
vm/op.go
View File

@ -2,70 +2,73 @@ package vm
import ( import (
"github.com/mediocregopher/ginger/gg" "github.com/mediocregopher/ginger/gg"
"github.com/mediocregopher/ginger/graph"
) )
var ( var (
inVal = nameVal("in")
outVal = nameVal("out") outVal = nameVal("out")
) )
// Operation is an entity which can accept a single argument (the OpenEdge), // Thunk is returned from the performance of an Operation. When called it will
// perform some internal processing on that argument, and return a resultant // return the result of that Operation having been called with the particular
// Value. // arguments which were passed in.
// type Thunk func() (Value, error)
// The Scope passed into Perform can be used to Evaluate the OpenEdge, as
// needed. 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 { type Operation interface {
Perform(*gg.OpenEdge, Scope) (Value, error) Perform([]Thunk) (Thunk, 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(args []Thunk) (Thunk, error) {
edgeVal, err := EvaluateEdge(edge, scope) return func() (Value, error) {
if err != nil { val, err := evalThunks(args)()
return Value{}, err
}
return fn(edgeVal) if err != nil {
}) return ZeroValue, err
}
// 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)
} }
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 { type graphOp struct {
@ -76,10 +79,9 @@ type graphOp struct {
// OperationFromGraph wraps the given Graph such that it can be used as an // OperationFromGraph wraps the given Graph such that it can be used as an
// operation. // operation.
// //
// When Perform is called the passed in OpenEdge is set to the "in" name value // The Thunk returned by Perform will evaluate the passed in Thunks, and set
// of the given Graph, then that resultant graph and the given parent Scope are // them to the "in" name value of the given Graph. The "out" name value is
// used to construct a new Scope. The "out" name value is Evaluated on that // Evaluated using the given Scope to obtain a resultant Value.
// Scope to obtain a resultant Value.
func OperationFromGraph(g *gg.Graph, scope Scope) Operation { func OperationFromGraph(g *gg.Graph, scope Scope) Operation {
return &graphOp{ return &graphOp{
Graph: g, 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) { func (g *graphOp) Perform(args []Thunk) (Thunk, error) {
return ScopeFromGraph(
return preEvalEdgeOp(func(edge *gg.OpenEdge) (Value, error) { g.Graph,
evalThunks(args),
scope = ScopeFromGraph( g.scope,
g.Graph.AddValueIn(inVal.Value, edge), ).Evaluate(outVal)
g.scope,
)
return scope.Evaluate(outVal)
}).Perform(edge, scope)
} }
// 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([]Thunk) (Thunk, 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(args []Thunk) (Thunk, error) {
return f(edge, scope) return f(args)
} }

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
@ -11,86 +12,76 @@ import (
// its value. // its value.
type Scope interface { type Scope interface {
// Evaluate accepts a name Value and returns the real Value which that name // Evaluate accepts a name Value and returns a Thunk which will return the
// points to. // real Value which that name points to.
Evaluate(Value) (Value, error) Evaluate(Value) (Thunk, error)
// NewScope returns a new Scope which sub-operations within this Scope // NewScope returns a new Scope which sub-operations within this Scope
// should use for themselves. // should use for themselves.
NewScope() Scope 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, // 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 *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() { val := Value{Value: ggVal}
return edgeToValue(edge, scope)
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) return thunk()
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. // 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 // Evaluate uses the given name Value as a key into the ScopeMap map, and
// returns the Value held there for the key, if any. // 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 { 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] val, ok := m[*nameVal.Name]
if !ok { 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. // NewScope returns the ScopeMap as-is.
@ -122,6 +113,7 @@ func (m ScopeMap) NewScope() Scope {
type graphScope struct { type graphScope struct {
*gg.Graph *gg.Graph
in Thunk
parent Scope parent Scope
} }
@ -132,23 +124,31 @@ type graphScope struct {
// be followed, with edge values being evaluated to Operations, until a Value // be followed, with edge values being evaluated to Operations, until a Value
// can be obtained. // 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 // 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 // used to evaluate that name. If the parent Scope is nil then an error is
// returned. // returned.
// //
// 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 *gg.Graph, in Thunk, parent Scope) Scope {
return &graphScope{ return &graphScope{
Graph: g, Graph: g,
in: in,
parent: parent, parent: parent,
} }
} }
func (g *graphScope) Evaluate(nameVal Value) (Value, error) { func (g *graphScope) Evaluate(nameVal Value) (Thunk, error) {
if nameVal.Name == nil { 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) edgesIn := g.ValueIns(nameVal.Value)
@ -159,13 +159,13 @@ func (g *graphScope) Evaluate(nameVal Value) (Value, error) {
} else if l != 1 { } else if l != 1 {
return Value{}, fmt.Errorf( return nil, fmt.Errorf(
"%q must have exactly one input edge, found %d input edges", "%q must have exactly one input edge, found %d input edges",
*nameVal.Name, l, *nameVal.Name, l,
) )
} }
return EvaluateEdge(edgesIn[0], g) return func() (Value, error) { return EvaluateEdge(edgesIn[0], g) }, nil
} }
func (g *graphScope) NewScope() Scope { func (g *graphScope) NewScope() Scope {

View File

@ -115,5 +115,13 @@ 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(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()
} }