Complete refactor vm to not be as stupid

This commit is the result of many days of picking vm apart and putting
it back together again. The result is an implementation which separates
compile and runtime into separate steps, and which functions (more)
correctly in the face of recursion.

Pretty much all aspects of vm have been modified or deleted, so it's not
even really worth it to describe specific changes. Just pretend this is
the original implementaiton and the old one was never done.
rust
Brian Picciano 2 years ago
parent ebf57591a8
commit 2be865181d
  1. 4
      README.md
  2. 2
      cmd/eval/main.go
  3. 10
      gg/gg.go
  4. 274
      vm/op.go
  5. 163
      vm/scope.go
  6. 52
      vm/scope_global.go
  7. 19
      vm/vm.go
  8. 2
      vm/vm_test.go

@ -28,6 +28,4 @@ An example program which computes the Nth fibonacci number can be found at
go run ./cmd/eval/main.go "$(cat examples/fib.gg)" 5
```
Where you can replace `5` with any number. The vm has only been given enough
capability to run this program as a demo, and is extremely poorly optimized (as
will be evident if you input any large number). Further work is obviously TODO.
Where you can replace `5` with any number.

@ -29,7 +29,7 @@ func main() {
res, err := vm.EvaluateSource(
bytes.NewBufferString(opSrc),
inVal,
vm.Value{Value: inVal},
vm.GlobalScope,
)

@ -26,6 +26,16 @@ type Value struct {
LexerToken *LexerToken
}
// Name returns a name Value.
func Name(name string) Value {
return Value{Name: &name}
}
// Number returns a number Value.
func Number(n int64) Value {
return Value{Number: &n}
}
// IsZero returns true if the Value is the zero value (none of the sub-value
// fields are set). LexerToken is ignored for this check.
func (v Value) IsZero() bool {

@ -1,109 +1,235 @@
package vm
import (
"fmt"
"github.com/mediocregopher/ginger/gg"
"github.com/mediocregopher/ginger/graph"
)
// Operation is an entity which accepts an argument Value and performs some
// internal processing on that argument to return a resultant Value.
type Operation interface {
Perform(Value) Value
}
// OperationFunc is a function which implements the Operation interface.
type OperationFunc func(Value) Value
// Perform calls the underlying OperationFunc directly.
func (f OperationFunc) Perform(arg Value) Value {
return f(arg)
}
// Identity returns an Operation which always returns the given Value,
// regardless of the input argument.
//
// TODO this might not be the right name
func Identity(val Value) Operation {
return OperationFunc(func(Value) Value {
return val
})
}
type graphOp struct {
*gg.Graph
scope Scope
}
var (
outVal = nameVal("out")
valNameIn = Value{Value: gg.Name("in")}
valNameOut = Value{Value: gg.Name("out")}
valNameIf = Value{Value: gg.Name("if")}
valNameRecur = Value{Value: gg.Name("recur")}
valNumberZero = Value{Value: gg.Number(0)}
)
// 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)
// OperationFromGraph wraps the given Graph such that it can be used as an
// Operation. The given Scope determines what values outside of the Graph are
// available for use within the Operation.
func OperationFromGraph(g *gg.Graph, scope Scope) (Operation, error) {
func valThunk(val Value) Thunk {
return func() (Value, error) { return val, nil }
}
// edgeOp is distinct from a generic Operation in that the Value passed into
// Perform will _always_ be the value of "in" for the overall Operation.
//
// edgeOps will wrap each other, passing "in" downwards to the leaf edgeOps.
type edgeOp Operation
// 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 {
var compileEdge func(*gg.OpenEdge) (edgeOp, error)
if len(args) == 1 {
return args[0]
}
// TODO memoize?
valToEdgeOp := func(val Value) (edgeOp, error) {
return func() (Value, error) {
if val.Name == nil {
return edgeOp(Identity(val)), nil
}
var (
err error
tupVals = make([]Value, len(args))
)
name := *val.Name
if val.Equal(valNameIn) {
return edgeOp(OperationFunc(func(inArg Value) Value {
return inArg
})), nil
}
for i := range args {
if tupVals[i], err = args[i](); err != nil {
return ZeroValue, err
// TODO intercept if and recur?
edgesIn := g.ValueIns(val.Value)
if l := len(edgesIn); l == 0 {
val, err := scope.Resolve(name)
if err != nil {
return nil, fmt.Errorf("resolving name %q from the outer scope: %w", name, err)
}
return edgeOp(Identity(val)), nil
} else if l != 1 {
return nil, fmt.Errorf("resolved name %q to %d input edges, rather than one", name, l)
}
return Value{Tuple: tupVals}, nil
edge := edgesIn[0]
return compileEdge(edge)
}
}
// 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.
//
// The Operation passed into Perform is the Operation which is calling the
// Perform. It may be nil.
type Operation interface {
Perform([]Thunk, Operation) (Thunk, error)
}
// "out" resolves to more than a static value, treat the graph as a full
// operation.
func preEvalValOp(fn func(Value) (Value, error)) Operation {
// thisOp is used to support recur. It will get filled in with the Operation
// which is returned by this function, once that Operation is created.
thisOp := new(Operation)
return OperationFunc(func(args []Thunk, _ Operation) (Thunk, error) {
compileEdge = func(edge *gg.OpenEdge) (edgeOp, error) {
return func() (Value, error) {
return graph.MapReduce[gg.Value, gg.Value, edgeOp](
edge,
func(ggVal gg.Value) (edgeOp, error) {
return valToEdgeOp(Value{Value: ggVal})
},
func(ggEdgeVal gg.Value, inEdgeOps []edgeOp) (edgeOp, error) {
val, err := evalThunks(args)()
if ggEdgeVal.Equal(valNameIf.Value) {
if err != nil {
return ZeroValue, err
}
if len(inEdgeOps) != 3 {
return nil, fmt.Errorf("'if' requires a 3-tuple argument")
}
return fn(val)
return edgeOp(OperationFunc(func(inArg Value) Value {
}, nil
if pred := inEdgeOps[0].Perform(inArg); pred.Equal(valNumberZero) {
return inEdgeOps[2].Perform(inArg)
}
})
}
return inEdgeOps[1].Perform(inArg)
type graphOp struct {
*gg.Graph
scope Scope
}
})), nil
}
// OperationFromGraph wraps the given Graph such that it can be used as an
// operation.
//
// 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,
scope: scope,
// "if" statements (above) are the only case where we want the
// input edges to remain separated, otherwise they should always
// be combined into a single edge whose value is a tuple. Do
// that here.
inEdgeOp := inEdgeOps[0]
if len(inEdgeOps) > 1 {
inEdgeOp = edgeOp(OperationFunc(func(inArg Value) Value {
tupVals := make([]Value, len(inEdgeOps))
for i := range inEdgeOps {
tupVals[i] = inEdgeOps[i].Perform(inArg)
}
return Tuple(tupVals...)
}))
}
edgeVal := Value{Value: ggEdgeVal}
if edgeVal.IsZero() {
return inEdgeOp, nil
}
if edgeVal.Equal(valNameRecur) {
return edgeOp(OperationFunc(func(inArg Value) Value {
return (*thisOp).Perform(inEdgeOp.Perform(inArg))
})), nil
}
if edgeVal.Graph != nil {
opFromGraph, err := OperationFromGraph(
edgeVal.Graph,
scope.NewScope(),
)
if err != nil {
return nil, fmt.Errorf("compiling graph to operation: %w", err)
}
edgeVal = Value{Operation: opFromGraph}
}
// The Operation is known at compile-time, so we can wrap it
// directly into an edgeVal using the existing inEdgeOp as the
// input.
if edgeVal.Operation != nil {
return edgeOp(OperationFunc(func(inArg Value) Value {
return edgeVal.Operation.Perform(inEdgeOp.Perform(inArg))
})), nil
}
// the edgeVal is not an Operation at compile time, and so
// it must become one at runtime. We must resolve edgeVal to an
// edgeOp as well (edgeEdgeOp), and then at runtime that is
// given the inArg and (hopefully) the resultant Operation is
// called.
edgeEdgeOp, err := valToEdgeOp(edgeVal)
if err != nil {
return nil, err
}
return edgeOp(OperationFunc(func(inArg Value) Value {
runtimeEdgeVal := edgeEdgeOp.Perform(inArg)
if runtimeEdgeVal.Graph != nil {
runtimeOp, err := OperationFromGraph(
runtimeEdgeVal.Graph,
scope.NewScope(),
)
if err != nil {
panic(fmt.Sprintf("compiling graph to operation: %v", err))
}
runtimeEdgeVal = Value{Operation: runtimeOp}
}
if runtimeEdgeVal.Operation == nil {
panic("edge value must be an operation")
}
return runtimeEdgeVal.Operation.Perform(inEdgeOp.Perform(inArg))
})), nil
},
)
}
}
func (g *graphOp) Perform(args []Thunk, _ Operation) (Thunk, error) {
return ScopeFromGraph(
g.Graph,
evalThunks(args),
g.scope,
g,
).Evaluate(outVal)
}
graphOp, err := valToEdgeOp(valNameOut)
// OperationFunc is a function which implements the Operation interface.
type OperationFunc func([]Thunk, Operation) (Thunk, error)
if err != nil {
return nil, err
}
// Perform calls the underlying OperationFunc directly.
func (f OperationFunc) Perform(args []Thunk, op Operation) (Thunk, error) {
return f(args, op)
*thisOp = Operation(graphOp)
return Operation(graphOp), nil
}

@ -4,108 +4,35 @@ 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
// by which to obtain those values, and allows for the evaluation of a name to
// its value.
// Scope encapsulates a set of name->Value mappings.
type Scope interface {
// Evaluate accepts a name Value and returns a Thunk which will return the
// real Value which that name points to.
Evaluate(Value) (Thunk, error)
// Resolve accepts a name and returns an Value.
Resolve(string) (Value, error)
// NewScope returns a new Scope which sub-operations within this Scope
// should use for themselves.
NewScope() Scope
}
// 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.
//
// The Operation is the Operation which is evaluating the edge, if any.
func EvaluateEdge(edge *gg.OpenEdge, scope Scope, op Operation) (Value, error) {
thunk, err := graph.MapReduce[gg.Value, gg.Value, Thunk](
edge,
func(ggVal gg.Value) (Thunk, error) {
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, op)
},
)
if err != nil {
return ZeroValue, err
}
return thunk()
}
// 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) (Thunk, error) {
// Resolve uses the given name as a key into the ScopeMap map, and
// returns the Operation held there for the key, if any.
func (m ScopeMap) Resolve(name string) (Value, error) {
if nameVal.Name == nil {
return nil, fmt.Errorf("value %v is not a name value", nameVal)
}
val, ok := m[*nameVal.Name]
v, ok := m[name]
if !ok {
return nil, fmt.Errorf("%q not defined", *nameVal.Name)
return Value{}, fmt.Errorf("%q not defined", name)
}
return valThunk(val), nil
return v, nil
}
// NewScope returns the ScopeMap as-is.
@ -113,65 +40,79 @@ func (m ScopeMap) NewScope() Scope {
return m
}
type scopeWith struct {
Scope // parent
name string
val Value
}
// ScopeWith returns a copy of the given Scope, except that evaluating the given
// name will always return the given Value.
func ScopeWith(scope Scope, name string, val Value) Scope {
return &scopeWith{
Scope: scope,
name: name,
val: val,
}
}
func (s *scopeWith) Resolve(name string) (Value, error) {
if name == s.name {
return s.val, nil
}
return s.Scope.Resolve(name)
}
type graphScope struct {
*gg.Graph
in Thunk
parent Scope
op Operation
}
// 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.
/*
TODO I don't think this is actually necessary
// ScopeFromGraph returns a Scope which will use the given Graph for name
// resolution.
//
// 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.
// When a name is resolved, 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
// compiled into an Operation and returned.
//
// 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 resolve 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, in Thunk, parent Scope, op Operation) Scope {
func ScopeFromGraph(g *gg.Graph, parent Scope) Scope {
return &graphScope{
Graph: g,
in: in,
parent: parent,
op: op,
}
}
func (g *graphScope) Evaluate(nameVal Value) (Thunk, error) {
func (g *graphScope) Resolve(name string) (Value, error) {
if nameVal.Name == nil {
return nil, fmt.Errorf("value %v is not a name value", nameVal)
}
if *nameVal.Name == "in" {
return g.in, nil
}
var ggNameVal gg.Value
ggNameVal.Name = &name
edgesIn := g.ValueIns(nameVal.Value)
log.Printf("resolving %q", name)
edgesIn := g.ValueIns(ggNameVal)
if l := len(edgesIn); l == 0 && g.parent != nil {
return g.parent.Evaluate(nameVal)
return g.parent.Resolve(name)
} else if l != 1 {
return nil, fmt.Errorf(
"%q must have exactly one input edge, found %d input edges",
*nameVal.Name, l,
name, l,
)
}
return func() (Value, error) {
return EvaluateEdge(edgesIn[0], g, g.op)
}, nil
return CompileEdge(edgesIn[0], g)
}
func (g *graphScope) NewScope() Scope {
@ -182,3 +123,5 @@ func (g *graphScope) NewScope() Scope {
return g.parent
}
*/

@ -6,11 +6,23 @@ import (
"github.com/mediocregopher/ginger/gg"
)
func globalOp(fn func(Value) (Value, error)) Value {
return Value{
Operation: OperationFunc(func(in Value) Value {
res, err := fn(in)
if err != nil {
panic(err)
}
return res
}),
}
}
// 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) {
"add": globalOp(func(val Value) (Value, error) {
var sum int64
@ -25,17 +37,17 @@ var GlobalScope = ScopeMap{
return Value{Value: gg.Value{Number: &sum}}, nil
})},
}),
"tupEl": Value{Operation: preEvalValOp(func(val Value) (Value, error) {
"tupEl": globalOp(func(val Value) (Value, error) {
tup, i := val.Tuple[0], val.Tuple[1]
return tup.Tuple[int(*i.Number)], nil
})},
}),
"isZero": Value{Operation: preEvalValOp(func(val Value) (Value, error) {
"isZero": globalOp(func(val Value) (Value, error) {
if *val.Number == 0 {
one := int64(1)
@ -45,33 +57,5 @@ var GlobalScope = ScopeMap{
zero := int64(0)
return Value{Value: gg.Value{Number: &zero}}, nil
})},
"if": Value{Operation: OperationFunc(func(args []Thunk, _ Operation) (Thunk, error) {
b := args[0]
onTrue := args[1]
onFalse := args[2]
return func() (Value, error) {
bVal, err := b()
if err != nil {
return ZeroValue, err
}
if *bVal.Number == 0 {
return onFalse()
}
return onTrue()
}, nil
})},
"recur": Value{Operation: OperationFunc(func(args []Thunk, op Operation) (Thunk, error) {
return op.Perform(args, op)
})},
}),
}

@ -22,6 +22,12 @@ type Value struct {
Tuple []Value
}
// Tuple returns a tuple Value comprising the given Values. Calling Tuple with
// no arguments returns ZeroValue.
func Tuple(vals ...Value) Value {
return Value{Tuple: vals}
}
// IsZero returns true if the Value is the zero value (aka the 0-tuple).
// LexerToken (within the gg.Value) is ignored for this check.
func (v Value) IsZero() bool {
@ -105,7 +111,7 @@ func nameVal(n string) Value {
//
// 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) {
func EvaluateSource(opSrc io.Reader, input Value, scope Scope) (Value, error) {
lexer := gg.NewLexer(opSrc)
g, err := gg.DecodeLexer(lexer)
@ -113,16 +119,11 @@ func EvaluateSource(opSrc io.Reader, input gg.Value, scope Scope) (Value, error)
return Value{}, err
}
op := OperationFromGraph(g, scope.NewScope())
thunk, err := op.Perform(
[]Thunk{valThunk(Value{Value: input})},
nil,
)
op, err := OperationFromGraph(g, scope.NewScope())
if err != nil {
return ZeroValue, err
return Value{}, err
}
return thunk()
return op.Perform(input), nil
}

@ -20,7 +20,7 @@ func TestVM(t *testing.T) {
val, err := EvaluateSource(
bytes.NewBufferString(src),
gg.Value{Number: &in},
Value{Value: gg.Number(in)},
GlobalScope,
)

Loading…
Cancel
Save