Rename Operation to Function, plus some cleanup

This commit is contained in:
Brian Picciano 2023-10-16 17:15:51 +02:00
parent 7d0fcbf28a
commit 21c91731e9
5 changed files with 248 additions and 319 deletions

235
vm/function.go Normal file
View File

@ -0,0 +1,235 @@
package vm
import (
"fmt"
"github.com/mediocregopher/ginger/gg"
"github.com/mediocregopher/ginger/graph"
)
// Function is an entity which accepts an argument Value and performs some
// internal processing on that argument to return a resultant Value.
type Function interface {
Perform(Value) Value
}
// FunctionFunc is a function which implements the Function interface.
type FunctionFunc func(Value) Value
// Perform calls the underlying FunctionFunc directly.
func (f FunctionFunc) Perform(arg Value) Value {
return f(arg)
}
// Identity returns an Function which always returns the given Value,
// regardless of the input argument.
//
// TODO this might not be the right name
func Identity(val Value) Function {
return FunctionFunc(func(Value) Value {
return val
})
}
type graphFn struct {
*gg.Graph
scope Scope
}
var (
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)}
)
// FunctionFromGraph wraps the given Graph such that it can be used as an
// Function. The given Scope determines what values outside of the Graph are
// available for use within the Function.
func FunctionFromGraph(g *gg.Graph, scope Scope) (Function, error) {
// edgeFn is distinct from a generic Function in that the Value passed into
// Perform will _always_ be the value of "in" for the overall Function.
//
// edgeFns will wrap each other, passing "in" downwards to the leaf edgeFns.
type edgeFn Function
var compileEdge func(*gg.OpenEdge) (edgeFn, error)
// TODO memoize?
valToEdgeFn := func(val Value) (edgeFn, error) {
if val.Name == nil {
return edgeFn(Identity(val)), nil
}
name := *val.Name
if val.Equal(valNameIn) {
return edgeFn(FunctionFunc(func(inArg Value) Value {
return inArg
})), nil
}
// 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 edgeFn(Identity(val)), nil
} else if l != 1 {
return nil, fmt.Errorf("resolved name %q to %d input edges, rather than one", name, l)
}
edge := edgesIn[0]
return compileEdge(edge)
}
// "out" resolves to more than a static value, treat the graph as a full
// operation.
// thisFn is used to support recur. It will get filled in with the Function
// which is returned by this function, once that Function is created.
thisFn := new(Function)
compileEdge = func(edge *gg.OpenEdge) (edgeFn, error) {
return graph.MapReduce[gg.Value, gg.Value, edgeFn](
edge,
func(ggVal gg.Value) (edgeFn, error) {
return valToEdgeFn(Value{Value: ggVal})
},
func(ggEdgeVal gg.Value, inEdgeFns []edgeFn) (edgeFn, error) {
if ggEdgeVal.Equal(valNameIf.Value) {
if len(inEdgeFns) != 3 {
return nil, fmt.Errorf("'if' requires a 3-tuple argument")
}
return edgeFn(FunctionFunc(func(inArg Value) Value {
if pred := inEdgeFns[0].Perform(inArg); pred.Equal(valNumberZero) {
return inEdgeFns[2].Perform(inArg)
}
return inEdgeFns[1].Perform(inArg)
})), nil
}
// "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.
inEdgeFn := inEdgeFns[0]
if len(inEdgeFns) > 1 {
inEdgeFn = edgeFn(FunctionFunc(func(inArg Value) Value {
tupVals := make([]Value, len(inEdgeFns))
for i := range inEdgeFns {
tupVals[i] = inEdgeFns[i].Perform(inArg)
}
return Tuple(tupVals...)
}))
}
edgeVal := Value{Value: ggEdgeVal}
if edgeVal.IsZero() {
return inEdgeFn, nil
}
if edgeVal.Equal(valNameRecur) {
return edgeFn(FunctionFunc(func(inArg Value) Value {
return (*thisFn).Perform(inEdgeFn.Perform(inArg))
})), nil
}
if edgeVal.Graph != nil {
opFromGraph, err := FunctionFromGraph(
edgeVal.Graph,
scope.NewScope(),
)
if err != nil {
return nil, fmt.Errorf("compiling graph to operation: %w", err)
}
edgeVal = Value{Function: opFromGraph}
}
// The Function is known at compile-time, so we can wrap it
// directly into an edgeVal using the existing inEdgeFn as the
// input.
if edgeVal.Function != nil {
return edgeFn(FunctionFunc(func(inArg Value) Value {
return edgeVal.Function.Perform(inEdgeFn.Perform(inArg))
})), nil
}
// the edgeVal is not an Function at compile time, and so
// it must become one at runtime. We must resolve edgeVal to an
// edgeFn as well (edgeEdgeFn), and then at runtime that is
// given the inArg and (hopefully) the resultant Function is
// called.
edgeEdgeFn, err := valToEdgeFn(edgeVal)
if err != nil {
return nil, err
}
return edgeFn(FunctionFunc(func(inArg Value) Value {
runtimeEdgeVal := edgeEdgeFn.Perform(inArg)
if runtimeEdgeVal.Graph != nil {
runtimeFn, err := FunctionFromGraph(
runtimeEdgeVal.Graph,
scope.NewScope(),
)
if err != nil {
panic(fmt.Sprintf("compiling graph to operation: %v", err))
}
runtimeEdgeVal = Value{Function: runtimeFn}
}
if runtimeEdgeVal.Function == nil {
panic("edge value must be an operation")
}
return runtimeEdgeVal.Function.Perform(inEdgeFn.Perform(inArg))
})), nil
},
)
}
graphFn, err := valToEdgeFn(valNameOut)
if err != nil {
return nil, err
}
*thisFn = Function(graphFn)
return Function(graphFn), nil
}

235
vm/op.go
View File

@ -1,235 +0,0 @@
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 (
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)}
)
// 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) {
// 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
var compileEdge func(*gg.OpenEdge) (edgeOp, error)
// TODO memoize?
valToEdgeOp := func(val Value) (edgeOp, error) {
if val.Name == nil {
return edgeOp(Identity(val)), nil
}
name := *val.Name
if val.Equal(valNameIn) {
return edgeOp(OperationFunc(func(inArg Value) Value {
return inArg
})), nil
}
// 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)
}
edge := edgesIn[0]
return compileEdge(edge)
}
// "out" resolves to more than a static value, treat the graph as a full
// 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)
compileEdge = func(edge *gg.OpenEdge) (edgeOp, 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) {
if ggEdgeVal.Equal(valNameIf.Value) {
if len(inEdgeOps) != 3 {
return nil, fmt.Errorf("'if' requires a 3-tuple argument")
}
return edgeOp(OperationFunc(func(inArg Value) Value {
if pred := inEdgeOps[0].Perform(inArg); pred.Equal(valNumberZero) {
return inEdgeOps[2].Perform(inArg)
}
return inEdgeOps[1].Perform(inArg)
})), nil
}
// "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
},
)
}
graphOp, err := valToEdgeOp(valNameOut)
if err != nil {
return nil, err
}
*thisOp = Operation(graphOp)
return Operation(graphOp), nil
}

View File

@ -2,8 +2,6 @@ package vm
import (
"fmt"
"github.com/mediocregopher/ginger/gg"
)
// Scope encapsulates a set of name->Value mappings.
@ -62,66 +60,3 @@ func (s *scopeWith) Resolve(name string) (Value, error) {
}
return s.Scope.Resolve(name)
}
type graphScope struct {
*gg.Graph
parent Scope
}
/*
TODO I don't think this is actually necessary
// ScopeFromGraph returns a Scope which will use the given Graph for name
// resolution.
//
// 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 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, parent Scope) Scope {
return &graphScope{
Graph: g,
parent: parent,
}
}
func (g *graphScope) Resolve(name string) (Value, error) {
var ggNameVal gg.Value
ggNameVal.Name = &name
log.Printf("resolving %q", name)
edgesIn := g.ValueIns(ggNameVal)
if l := len(edgesIn); l == 0 && g.parent != nil {
return g.parent.Resolve(name)
} else if l != 1 {
return nil, fmt.Errorf(
"%q must have exactly one input edge, found %d input edges",
name, l,
)
}
return CompileEdge(edgesIn[0], g)
}
func (g *graphScope) NewScope() Scope {
if g.parent == nil {
return ScopeMap{}
}
return g.parent
}
*/

View File

@ -6,9 +6,9 @@ import (
"github.com/mediocregopher/ginger/gg"
)
func globalOp(fn func(Value) (Value, error)) Value {
func globalFn(fn func(Value) (Value, error)) Value {
return Value{
Operation: OperationFunc(func(in Value) Value {
Function: FunctionFunc(func(in Value) Value {
res, err := fn(in)
if err != nil {
panic(err)
@ -22,7 +22,7 @@ func globalOp(fn func(Value) (Value, error)) Value {
// any operation in a ginger program.
var GlobalScope = ScopeMap{
"add": globalOp(func(val Value) (Value, error) {
"add": globalFn(func(val Value) (Value, error) {
var sum int64
@ -39,7 +39,7 @@ var GlobalScope = ScopeMap{
}),
"tupEl": globalOp(func(val Value) (Value, error) {
"tupEl": globalFn(func(val Value) (Value, error) {
tup, i := val.Tuple[0], val.Tuple[1]
@ -47,7 +47,7 @@ var GlobalScope = ScopeMap{
}),
"isZero": globalOp(func(val Value) (Value, error) {
"isZero": globalFn(func(val Value) (Value, error) {
if *val.Number == 0 {
one := int64(1)

View File

@ -13,12 +13,12 @@ import (
// ZeroValue is a Value with no fields set. It is equivalent to the 0-tuple.
var ZeroValue Value
// Value extends a gg.Value to include Operations and Tuples as a possible
// Value extends a gg.Value to include Functions and Tuples as a possible
// types.
type Value struct {
gg.Value
Operation
Function
Tuple []Value
}
@ -47,8 +47,8 @@ func (v Value) Equal(v2g graph.Value) bool {
case !v.Value.IsZero() || !v2.Value.IsZero():
return v.Value.Equal(v2.Value)
case v.Operation != nil || v2.Operation != nil:
// for now we say that Operations can't be compared. This will probably
case v.Function != nil || v2.Function != nil:
// for now we say that Functions can't be compared. This will probably
// get revisted later.
return false
@ -76,10 +76,10 @@ func (v Value) String() string {
switch {
case v.Operation != nil:
case v.Function != nil:
// We can try to get better strings for ops later
return "<op>"
return "<fn>"
case !v.Value.IsZero():
return v.Value.String()
@ -100,12 +100,6 @@ func (v Value) String() string {
}
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.
//
@ -119,11 +113,11 @@ func EvaluateSource(opSrc io.Reader, input Value, scope Scope) (Value, error) {
return Value{}, err
}
op, err := OperationFromGraph(g, scope.NewScope())
fn, err := FunctionFromGraph(g, scope.NewScope())
if err != nil {
return Value{}, err
}
return op.Perform(input), nil
return fn.Perform(input), nil
}