diff --git a/README.md b/README.md index 493c5bf..f066586 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cmd/eval/main.go b/cmd/eval/main.go index 0d559ab..e9ff261 100644 --- a/cmd/eval/main.go +++ b/cmd/eval/main.go @@ -29,7 +29,7 @@ func main() { res, err := vm.EvaluateSource( bytes.NewBufferString(opSrc), - inVal, + vm.Value{Value: inVal}, vm.GlobalScope, ) diff --git a/gg/gg.go b/gg/gg.go index eb21cf1..99e7cc1 100644 --- a/gg/gg.go +++ b/gg/gg.go @@ -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 { diff --git a/vm/op.go b/vm/op.go index 3e8f011..5e3feff 100644 --- a/vm/op.go +++ b/vm/op.go @@ -1,75 +1,33 @@ package vm import ( + "fmt" + "github.com/mediocregopher/ginger/gg" + "github.com/mediocregopher/ginger/graph" ) -var ( - outVal = nameVal("out") -) - -// 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. -// -// The Operation passed into Perform is the Operation which is calling the -// Perform. It may be nil. +// 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([]Thunk, Operation) (Thunk, error) + Perform(Value) Value } -func preEvalValOp(fn func(Value) (Value, error)) Operation { +// OperationFunc is a function which implements the Operation interface. +type OperationFunc func(Value) Value - return OperationFunc(func(args []Thunk, _ Operation) (Thunk, error) { - - return func() (Value, error) { - - val, err := evalThunks(args)() - - if err != nil { - return ZeroValue, err - } - - return fn(val) - - }, nil +// 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 }) } @@ -78,32 +36,200 @@ type graphOp struct { 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 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, +// 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) } -} -func (g *graphOp) Perform(args []Thunk, _ Operation) (Thunk, error) { - return ScopeFromGraph( - g.Graph, - evalThunks(args), - g.scope, - g, - ).Evaluate(outVal) -} + // "out" resolves to more than a static value, treat the graph as a full + // operation. -// OperationFunc is a function which implements the Operation interface. -type OperationFunc func([]Thunk, Operation) (Thunk, error) + // 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) -// Perform calls the underlying OperationFunc directly. -func (f OperationFunc) Perform(args []Thunk, op Operation) (Thunk, error) { - return f(args, op) + 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 } diff --git a/vm/scope.go b/vm/scope.go index 8a619c0..37defab 100644 --- a/vm/scope.go +++ b/vm/scope.go @@ -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 graphScope struct { - *gg.Graph - in Thunk - parent Scope - op Operation +type scopeWith struct { + Scope // parent + name string + val Value } -// ScopeFromGraph returns a Scope which will use the given Graph for evaluation. +// 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 + 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 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. -// -// 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) - } + var ggNameVal gg.Value + ggNameVal.Name = &name - if *nameVal.Name == "in" { - return g.in, nil - } - - 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 } + +*/ diff --git a/vm/scope_global.go b/vm/scope_global.go index f8149c7..88ee86b 100644 --- a/vm/scope_global.go +++ b/vm/scope_global.go @@ -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) - })}, + }), } diff --git a/vm/vm.go b/vm/vm.go index 04710f7..99c31d5 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -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 } diff --git a/vm/vm_test.go b/vm/vm_test.go index 70adc3c..456c37d 100644 --- a/vm/vm_test.go +++ b/vm/vm_test.go @@ -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, )