Implementation of a super basic vm

The vm does what it needs to do (evaluate the result of passing an input
to an operatio, where the input and the operation themselves may have
sub-inputs/operations to evaluate), with many caveats/misgivings.
This commit is contained in:
Brian Picciano 2021-12-28 13:18:01 -07:00
parent ec3218e2d0
commit 6040abc836
6 changed files with 408 additions and 7 deletions

View File

@ -94,6 +94,15 @@ func (oe OpenEdge) String() string {
return fmt.Sprintf("%s(%s, %s)", vertexType, oe.fromV.String(), oe.edgeVal.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. // EdgeValue returns the Value which lies on the edge itself.
func (oe OpenEdge) EdgeValue() Value { func (oe OpenEdge) EdgeValue() Value {
return oe.edgeVal return oe.edgeVal
@ -274,14 +283,16 @@ func (g *Graph) String() string {
return fmt.Sprintf("graph(%s)", strings.Join(strs, ", ")) 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 { for _, valIn := range g.valIns {
if valIn.val.Equal(val) { 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 // 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. // be created in this step.
func (g *Graph) AddValueIn(oe OpenEdge, val Value) *Graph { 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) { if existingOE.equal(oe) {
return g return g
} }
} }
valIn = valIn.cp() // ValueIns returns a copy of edges, so we're ok to modify it.
valIn.edges = append(valIn.edges, oe) edges = append(edges, oe)
valIn := graphValueIn{val: val, edges: edges}
g = g.cp() g = g.cp()

108
vm/op.go Normal file
View File

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

178
vm/scope.go Normal file
View File

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

33
vm/scope_global.go Normal file
View File

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

41
vm/vm.go Normal file
View File

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

29
vm/vm_test.go Normal file
View File

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