Begin doing evaluation

This change checks in an enormously rudimentary interpreter.  There is
a lot left to do, as evidenced by the countless TODOs scattered throughout
pkg/compiler/eval/eval.go.  Nevertheless, the scaffolding and some important
pieces are included in this change.

In particular, to evaluate a package, we must locate its entrypoint and then,
using the results of binding, systematically walk the full AST.  As we do
so, we will assert that aspects of the AST match the expected shape,
including symbols and their types, and produce value objects for expressions.

An *unwind structure is used for returns, throws, breaks, and continues (both
labeled and unlabeled).  Each statement or expression visitation may optionally
return one and its presence indicates that control flow transfer is occurring.
The visitation logic then needs to propagate this; usually just by bailing out
of the current context immediately, but sometimes -- as is the case with
TryCatchBlock statements, for instance -- the unwind is manipulated in more
"interesting" ways.

An *Object structure is used for expressions yielding values.  This is a
"runtime object" in our system and is comprised of three things: 1) a Type
(essentially its v-table), 2) an optional data pointer (for tagged primitives),
and 3) an optional bag of properties (for complex object property values).
I am still on the fence about whether to unify the data representations.

The hokiest aspect of this change is the scoping and value management.  I am
trying to avoid needing to implement any sort of "garbage collection", which
means our bag-of-values approach will not work.  Instead, we will need to
arrange for scopes to be erected and discarded in the correct order during
evaluation.  I will probably tackle that next, along with fleshing out the
many missing statement and expression cases (...and tests, of course).
This commit is contained in:
joeduffy 2017-01-25 10:13:06 -08:00
parent 7c1010ba72
commit 5bdb535c54
10 changed files with 563 additions and 15 deletions

View file

@ -54,8 +54,8 @@ const TryCatchFinallyKind NodeKind = "TryCatchFinally"
type TryCatchBlock struct {
NodeValue
Block *Block `json:"block"`
Exception *LocalVariable `json:"exception,omitempty"`
Block *Block `json:"block"`
}
var _ Node = (*TryCatchBlock)(nil)
@ -124,7 +124,7 @@ const ReturnStatementKind NodeKind = "ReturnStatement"
// ThrowStatement maps to raising an exception, usually `throw`, in the source language.
type ThrowStatement struct {
StatementNode
Expression *Expression `json:"expression,omitempty"`
Expression Expression `json:"expression"`
}
var _ Node = (*ThrowStatement)(nil)

View file

@ -84,6 +84,9 @@ func Walk(v Visitor, node Node) {
if n.FinallyBlock != nil {
Walk(v, n.FinallyBlock)
}
case *TryCatchBlock:
Walk(v, n.Exception)
Walk(v, n.Block)
case *IfStatement:
Walk(v, n.Condition)
Walk(v, n.Consequent)
@ -97,9 +100,7 @@ func Walk(v Visitor, node Node) {
Walk(v, *n.Expression)
}
case *ThrowStatement:
if n.Expression != nil {
Walk(v, *n.Expression)
}
Walk(v, n.Expression)
case *WhileStatement:
Walk(v, n.Test)
Walk(v, n.Body)

View file

@ -22,8 +22,8 @@ import (
type Binder interface {
core.Phase
// BindCtx represents the contextual information resulting from binding.
BindCtx() *Context
// Ctx represents the contextual information resulting from binding.
Ctx() *Context
// BindPackages takes a package AST, resolves all dependencies and tokens inside of it, and returns a fully bound
// package symbol that can be used for semantic operations (like interpretation and evaluation).
@ -46,8 +46,8 @@ type binder struct {
reader metadata.Reader // a metadata reader (in case we encounter package references).
}
func (b *binder) BindCtx() *Context { return b.ctx }
func (b *binder) Diag() diag.Sink { return b.ctx.Diag }
func (b *binder) Ctx() *Context { return b.ctx }
func (b *binder) Diag() diag.Sink { return b.ctx.Diag }
// bindType binds a type token AST node to a symbol.
func (b *binder) bindType(node *ast.TypeToken) symbols.Type {

View file

@ -16,9 +16,10 @@ func (b *binder) bindFunctionBody(node ast.Function) {
if params != nil {
for _, param := range *params {
// Register this variable's type and associate its name with the identifier.
// TODO: stick this into the scope.
ty := b.bindType(param.Type)
b.ctx.Scope.TryRegister(param, symbols.NewLocalVariableSym(param, ty))
sym := symbols.NewLocalVariableSym(param, ty)
b.ctx.RegisterSymbol(param, sym)
b.ctx.Scope.TryRegister(param, sym) // TODO: figure out whether to keep this.
}
}

View file

@ -144,9 +144,10 @@ func (a *astBinder) checkIfStatement(node *ast.IfStatement) {
func (a *astBinder) visitLocalVariable(node *ast.LocalVariable) {
// Encountering a new local variable results in registering it; both to the type and symbol table.
// TODO: add to the symbol map?
ty := a.b.bindType(node.Type)
a.b.ctx.Scope.TryRegister(node, symbols.NewLocalVariableSym(node, ty))
sym := symbols.NewLocalVariableSym(node, ty)
a.b.ctx.RegisterSymbol(node, ty)
a.b.ctx.Scope.TryRegister(node, sym) // TODO: figure out whether to keep this.
}
func (a *astBinder) visitLabeledStatement(node *ast.LabeledStatement) {

440
pkg/compiler/eval/eval.go Normal file
View file

@ -0,0 +1,440 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package eval
import (
"github.com/marapongo/mu/pkg/compiler/ast"
"github.com/marapongo/mu/pkg/compiler/binder"
"github.com/marapongo/mu/pkg/compiler/core"
"github.com/marapongo/mu/pkg/compiler/symbols"
"github.com/marapongo/mu/pkg/compiler/types"
"github.com/marapongo/mu/pkg/diag"
"github.com/marapongo/mu/pkg/graph"
"github.com/marapongo/mu/pkg/pack"
"github.com/marapongo/mu/pkg/tokens"
"github.com/marapongo/mu/pkg/util/contract"
)
// Interpreter can evaluate compiled MuPackages.
type Interpreter interface {
core.Phase
Ctx() *binder.Context // the binding context object.
// EvaluatePackage performs evaluation on the given blueprint package, starting in its entrypoint.
EvaluatePackage(pkg *pack.Package) graph.Graph
}
// New creates an interpreter that can be used to evaluate MuPackages.
func New(ctx *binder.Context) Interpreter {
return &evaluator{ctx: ctx}
}
type evaluator struct {
fnc *symbols.ModuleMethod // the function under evaluation.
ctx *binder.Context // the binding context with type and symbol information.
variables map[symbols.Variable]*Object // the object values for variable symbols. TODO: make this scope-based.
}
var _ Interpreter = (*evaluator)(nil)
func (e *evaluator) Ctx() *binder.Context { return e.ctx }
func (e *evaluator) Diag() diag.Sink { return e.ctx.Diag }
func (e *evaluator) EvaluatePackage(pkg *pack.Package) graph.Graph {
// TODO: find the entrypoint.
// TODO: pair up the ctx args, if any, with the entrypoint's parameters.
// TODO: visit the function.
return nil
}
// Statements
func (e *evaluator) evalStatement(node ast.Statement) *unwind {
// Simply switch on the node type and dispatch to the specific function, returning the unwind info.
switch n := node.(type) {
case *ast.Block:
return e.evalBlock(n)
case *ast.LocalVariableDeclaration:
return e.evalLocalVariableDeclaration(n)
case *ast.TryCatchFinally:
return e.evalTryCatchFinally(n)
case *ast.BreakStatement:
return e.evalBreakStatement(n)
case *ast.ContinueStatement:
return e.evalContinueStatement(n)
case *ast.IfStatement:
return e.evalIfStatement(n)
case *ast.LabeledStatement:
return e.evalLabeledStatement(n)
case *ast.ReturnStatement:
return e.evalReturnStatement(n)
case *ast.ThrowStatement:
return e.evalThrowStatement(n)
case *ast.WhileStatement:
return e.evalWhileStatement(n)
case *ast.EmptyStatement:
return nil // nothing to do
case *ast.MultiStatement:
return e.evalMultiStatement(n)
case *ast.ExpressionStatement:
return e.evalExpressionStatement(n)
default:
contract.Failf("Unrecognized statement node kind: %v", node.GetKind())
return nil
}
}
func (e *evaluator) evalBlock(node *ast.Block) *unwind {
// TODO: erect a variable scope (and tear it down afterwards).
return nil
}
func (e *evaluator) evalLocalVariableDeclaration(node *ast.LocalVariableDeclaration) *unwind {
// If there is a default value, set it now.
if node.Local.Default != nil {
obj := NewConstantObject(*node.Local.Default)
sym := e.ctx.RequireVariable(node.Local)
contract.Assert(sym != nil)
contract.Assert(obj.Type == sym.Type())
e.variables[sym] = obj
}
return nil
}
func (e *evaluator) evalBreakStatement(node *ast.BreakStatement) *unwind {
var label *tokens.Name
if node.Label != nil {
label = &node.Label.Ident
}
return breakUnwind(label)
}
func (e *evaluator) evalContinueStatement(node *ast.ContinueStatement) *unwind {
var label *tokens.Name
if node.Label != nil {
label = &node.Label.Ident
}
return continueUnwind(label)
}
func (e *evaluator) evalTryCatchFinally(node *ast.TryCatchFinally) *unwind {
// First, execute the TryBlock.
uw := e.evalBlock(node.TryBlock)
if uw != nil && uw.Thrown != nil {
// The try block threw something; see if there is a handler that covers this.
if node.CatchBlocks != nil {
for _, catch := range *node.CatchBlocks {
ex := e.ctx.RequireVariable(catch.Exception).Type()
contract.Assert(types.CanConvert(ex, types.Error))
if types.CanConvert(uw.Thrown.Type, ex) {
// This type matched; set the exception type and swap the unwind information with the catch block's.
// This has the effect of "handling" the exception (i.e., only reraise if the handler does).
// TODO: set the value of the exception somehow.
uw = e.evalBlock(catch.Block)
break
}
}
}
}
// No matter the unwind instructions, be sure to invoke the FinallyBlock.
if node.FinallyBlock != nil {
uwf := e.evalBlock(node.FinallyBlock)
// Any unwind information from the finally block overrides the try unwind that was in flight.
if uwf != nil {
uw = uwf
}
}
return uw
}
func (e *evaluator) evalIfStatement(node *ast.IfStatement) *unwind {
// Evaluate the branches explicitly based on the result of the condition node.
cond, uw := e.evalExpression(node.Condition)
if uw != nil {
return uw
}
if cond.Bool() {
return e.evalStatement(node.Consequent)
} else if node.Alternate != nil {
return e.evalStatement(*node.Alternate)
}
return nil
}
func (e *evaluator) evalLabeledStatement(node *ast.LabeledStatement) *unwind {
// Evaluate the underlying statement; if it is breaking or continuing to this label, stop the unwind.
uw := e.evalStatement(node.Statement)
if uw != nil && uw.Label != nil && *uw.Label == node.Label.Ident {
contract.Assert(uw.Return == false)
contract.Assert(uw.Throw == false)
// TODO: perform correct break/continue behavior when the label is affixed to a loop.
uw = nil
}
return uw
}
func (e *evaluator) evalReturnStatement(node *ast.ReturnStatement) *unwind {
var ret *Object
if node.Expression != nil {
var uw *unwind
if ret, uw = e.evalExpression(*node.Expression); uw != nil {
// If the expression caused an unwind, propagate that and ignore the returned object.
return uw
}
}
return returnUnwind(ret)
}
func (e *evaluator) evalThrowStatement(node *ast.ThrowStatement) *unwind {
thrown, uw := e.evalExpression(node.Expression)
if uw != nil {
// If the throw expression itself threw an exception, propagate that instead.
return uw
}
contract.Assert(thrown != nil)
return throwUnwind(thrown)
}
func (e *evaluator) evalWhileStatement(node *ast.WhileStatement) *unwind {
// So long as the test evaluates to true, keep on visiting the body.
var uw *unwind
for {
test, uw := e.evalExpression(node.Test)
if uw != nil {
return uw
}
if test.Bool() {
if uws := e.evalStatement(node.Body); uw != nil {
if uws.Continue {
contract.Assertf(uws.Label == nil, "Labeled continue not yet supported")
continue
} else if uws.Break {
contract.Assertf(uws.Label == nil, "Labeled break not yet supported")
break
} else {
// If it's not a continue or break, stash the unwind away and return it.
uw = uws
}
}
} else {
break
}
}
return uw // usually nil, unless a body statement threw/returned.
}
func (e *evaluator) evalMultiStatement(node *ast.MultiStatement) *unwind {
for _, stmt := range node.Statements {
if uw := e.evalStatement(stmt); uw != nil {
return uw
}
}
return nil
}
func (e *evaluator) evalExpressionStatement(node *ast.ExpressionStatement) *unwind {
// Just evaluate the expression, drop its object on the floor, and propagate its unwind information.
_, uw := e.evalExpression(node.Expression)
return uw
}
// Expressions
func (e *evaluator) evalExpression(node ast.Expression) (*Object, *unwind) {
// Simply switch on the node type and dispatch to the specific function, returning the object and unwind info.
switch n := node.(type) {
case *ast.NullLiteral:
return e.evalNullLiteral(n)
case *ast.BoolLiteral:
return e.evalBoolLiteral(n)
case *ast.NumberLiteral:
return e.evalNumberLiteral(n)
case *ast.StringLiteral:
return e.evalStringLiteral(n)
case *ast.ArrayLiteral:
return e.evalArrayLiteral(n)
case *ast.ObjectLiteral:
return e.evalObjectLiteral(n)
case *ast.LoadLocationExpression:
return e.evalLoadLocationExpression(n)
case *ast.LoadDynamicExpression:
return e.evalLoadDynamicExpression(n)
case *ast.NewExpression:
return e.evalNewExpression(n)
case *ast.InvokeFunctionExpression:
return e.evalInvokeFunctionExpression(n)
case *ast.LambdaExpression:
return e.evalLambdaExpression(n)
case *ast.UnaryOperatorExpression:
return e.evalUnaryOperatorExpression(n)
case *ast.BinaryOperatorExpression:
return e.evalBinaryOperatorExpression(n)
case *ast.CastExpression:
return e.evalCastExpression(n)
case *ast.IsInstExpression:
return e.evalIsInstExpression(n)
case *ast.TypeOfExpression:
return e.evalTypeOfExpression(n)
case *ast.ConditionalExpression:
return e.evalConditionalExpression(n)
case *ast.SequenceExpression:
return e.evalSequenceExpression(n)
default:
contract.Failf("Unrecognized expression node kind: %v", node.GetKind())
return nil, nil
}
}
func (e *evaluator) evalNullLiteral(node *ast.NullLiteral) (*Object, *unwind) {
return NewPrimitiveObject(types.Null, nil), nil
}
func (e *evaluator) evalBoolLiteral(node *ast.BoolLiteral) (*Object, *unwind) {
return NewPrimitiveObject(types.Bool, node.Value), nil
}
func (e *evaluator) evalNumberLiteral(node *ast.NumberLiteral) (*Object, *unwind) {
return NewPrimitiveObject(types.Number, node.Value), nil
}
func (e *evaluator) evalStringLiteral(node *ast.StringLiteral) (*Object, *unwind) {
return NewPrimitiveObject(types.String, node.Value), nil
}
func (e *evaluator) evalArrayLiteral(node *ast.ArrayLiteral) (*Object, *unwind) {
// Fetch this expression type and assert that it's an array.
ty := e.ctx.RequireType(node).(*symbols.ArrayType)
// Now create the array data.
var sz *int
var arr []Data
// If there's a node size, ensure it's a number, and initialize the array.
if node.Size != nil {
sze, uw := e.evalExpression(*node.Size)
if uw != nil {
return nil, uw
}
// TODO: this really ought to be an int, not a float...
sz := int(sze.Number())
if sz < 0 {
// If the size is less than zero, raise a new error.
return nil, throwUnwind(NewErrorObject("Invalid array size (must be >= 0)"))
}
arr = make([]Data, sz)
}
// If there are elements, place them into the array. This has two behaviors:
// 1) if there is a size, there can be up to that number of elements, which are set;
// 2) if there is no size, all of the elements are appended to the array.
if node.Elements != nil {
if sz == nil {
// Right-size the array.
arr = make([]Data, 0, len(*node.Elements))
} else if len(*node.Elements) > *sz {
// The element count exceeds the size; raise an error.
return nil, throwUnwind(
NewErrorObject("Invalid number of array elements; expected <=%v, got %v",
*sz, len(*node.Elements)))
}
for i, elem := range *node.Elements {
expr, uw := e.evalExpression(elem)
if uw != nil {
return nil, uw
}
if sz == nil {
arr = append(arr, expr)
} else {
arr[i] = expr
}
}
}
// Finally wrap the array data in a literal object.
return NewPrimitiveObject(ty, arr), nil
}
func (e *evaluator) evalObjectLiteral(node *ast.ObjectLiteral) (*Object, *unwind) {
// TODO: create a new object value.
return nil, nil
}
func (e *evaluator) evalLoadLocationExpression(node *ast.LoadLocationExpression) (*Object, *unwind) {
// TODO: create a pointer to the given location.
return nil, nil
}
func (e *evaluator) evalLoadDynamicExpression(node *ast.LoadDynamicExpression) (*Object, *unwind) {
return nil, nil
}
func (e *evaluator) evalNewExpression(node *ast.NewExpression) (*Object, *unwind) {
// TODO: create a new object and invoke its constructor.
return nil, nil
}
func (e *evaluator) evalInvokeFunctionExpression(node *ast.InvokeFunctionExpression) (*Object, *unwind) {
// TODO: resolve the target to a function, set up an activation record, and invoke it.
return nil, nil
}
func (e *evaluator) evalLambdaExpression(node *ast.LambdaExpression) (*Object, *unwind) {
// TODO: create the lambda object that can be invoked at runtime.
return nil, nil
}
func (e *evaluator) evalUnaryOperatorExpression(node *ast.UnaryOperatorExpression) (*Object, *unwind) {
// TODO: perform the unary operator's behavior.
return nil, nil
}
func (e *evaluator) evalBinaryOperatorExpression(node *ast.BinaryOperatorExpression) (*Object, *unwind) {
// TODO: perform the binary operator's behavior.
return nil, nil
}
func (e *evaluator) evalCastExpression(node *ast.CastExpression) (*Object, *unwind) {
return nil, nil
}
func (e *evaluator) evalIsInstExpression(node *ast.IsInstExpression) (*Object, *unwind) {
return nil, nil
}
func (e *evaluator) evalTypeOfExpression(node *ast.TypeOfExpression) (*Object, *unwind) {
return nil, nil
}
func (e *evaluator) evalConditionalExpression(node *ast.ConditionalExpression) (*Object, *unwind) {
// Evaluate the branches explicitly based on the result of the condition node.
cond, uw := e.evalExpression(node.Condition)
if uw != nil {
return nil, uw
}
if cond.Bool() {
return e.evalExpression(node.Consequent)
} else {
return e.evalExpression(node.Alternate)
}
}
func (e *evaluator) evalSequenceExpression(node *ast.SequenceExpression) (*Object, *unwind) {
// Simply walk through the sequence and return the last object.
var obj *Object
contract.Assert(len(node.Expressions) > 0)
for _, expr := range node.Expressions {
var uw *unwind
if obj, uw = e.evalExpression(expr); uw != nil {
// If the unwind was non-nil, stop visiting the expressions and propagate it now.
return nil, uw
}
}
// Return the last expression's object.
return obj, nil
}

View file

@ -0,0 +1,80 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package eval
import (
"fmt"
"github.com/marapongo/mu/pkg/compiler/symbols"
"github.com/marapongo/mu/pkg/compiler/types"
"github.com/marapongo/mu/pkg/tokens"
"github.com/marapongo/mu/pkg/util/contract"
)
// Object is a value allocated and stored on the heap. In MuIL's interpreter, all values are heap allocated, since we
// are less concerned about performance of the evaluation (compared to the cost of provisioning cloud resources).
type Object struct {
Type symbols.Type // the runtime type of the object.
Data Data // any constant data associated with this object.
Properties Properties // the full set of known properties and their values.
}
type Data interface{} // literal object data.
type Properties map[tokens.Name]*Object // an object's properties.
func NewObject(t symbols.Type) *Object {
return &Object{
Type: t,
Properties: make(Properties),
}
}
// NewPrimitiveObject creates a new primitive object with the given primitive type.
func NewPrimitiveObject(t symbols.Type, data interface{}) *Object {
return &Object{
Type: t,
Data: data,
}
}
// NewErrorObject creates a new exception with the given message.
func NewErrorObject(message string, args ...interface{}) *Object {
return NewPrimitiveObject(types.Error, fmt.Sprintf(message, args...))
}
// NewConstantObject returns a new object with the right type and value, based on some constant data.
func NewConstantObject(v interface{}) *Object {
if v == nil {
return NewPrimitiveObject(types.Null, nil)
}
switch data := v.(type) {
case bool:
return NewPrimitiveObject(types.Bool, data)
case string:
return NewPrimitiveObject(types.String, data)
case float64:
return NewPrimitiveObject(types.Number, data)
default:
// TODO: we could support more here (essentially, anything that is JSON serializable).
contract.Failf("Unrecognized constant data literal: %v", data)
return nil
}
}
// Bool asserts that the target is a boolean literal and returns its value.
func (o *Object) Bool() bool {
contract.Assertf(o.Type == types.Bool, "Expected object type to be Bool; got %v", o.Type)
contract.Assertf(o.Data != nil, "Expected boolean literal to carry a Data payload; got nil")
b, ok := o.Data.(bool)
contract.Assertf(ok, "Expected a boolean literal value for condition expr; conversion failed")
return b
}
// Number asserts that the target is a numeric literal and returns its value.
func (o *Object) Number() float64 {
contract.Assertf(o.Type == types.Number, "Expected object type to be Number; got %v", o.Type)
contract.Assertf(o.Data != nil, "Expected boolean literal to carry a Data payload; got nil")
n, ok := o.Data.(float64)
contract.Assertf(ok, "Expected a boolean literal value for condition expr; conversion failed")
return n
}

View file

@ -0,0 +1,23 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package eval
import (
"github.com/marapongo/mu/pkg/tokens"
)
// unwind instructs callers how to unwind the stack.
type unwind struct {
Break bool // true if breaking.
Continue bool // true if continuing.
Label *tokens.Name // a label being sought.
Return bool // true if returning.
Returned *Object // an object being returned.
Throw bool // true if raising an exception.
Thrown *Object // an exception object being thrown.
}
func breakUnwind(label *tokens.Name) *unwind { return &unwind{Break: true, Label: label} }
func continueUnwind(label *tokens.Name) *unwind { return &unwind{Continue: true, Label: label} }
func returnUnwind(ret *Object) *unwind { return &unwind{Return: true, Returned: ret} }
func throwUnwind(thrown *Object) *unwind { return &unwind{Throw: true, Thrown: thrown} }

View file

@ -14,6 +14,7 @@ var (
Number = symbols.NewPrimitiveType("number")
String = symbols.NewPrimitiveType("string")
Null = symbols.NewPrimitiveType("null")
Error = symbols.NewPrimitiveType("error")
)
// Primitives contains a map of all primitive types, keyed by their token/name.
@ -23,6 +24,7 @@ var Primitives = map[tokens.TypeName]symbols.Type{
Number.Nm: Number,
String.Nm: String,
Null.Nm: Null,
Error.Nm: Error,
}
// Common weakly typed types.

View file

@ -93,8 +93,8 @@ export type ReturnStatementKind = "ReturnStatement";
// A `throw` statement to throw an exception object.
export interface ThrowStatement extends Statement {
kind: ThrowStatementKind;
expression?: Expression;
kind: ThrowStatementKind;
expression: Expression;
}
export const throwStatementKind = "ThrowStatement";
export type ThrowStatementKind = "ThrowStatement";