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 (
"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)
}

View File

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

View File

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