Implement function calls in the interpreter

This change adds support for function calls.  We do not yet wire it
into the various function call expressions, however, that should be
relatively trivial after this change.  (It is dependent on figuring
out the runtime representation of load location objects and subsequent
uses of them).  This change also updates the scoping logic to respect
"activation frame" style of lexical scoping, where an inner function
as a result of a call should not have access to the callee context.

This change also includes logic to detect unhandled exceptions and to
print an error as a result.  Eventually, we will want stack traces here.
This commit is contained in:
joeduffy 2017-01-25 15:04:47 -08:00
parent 86cb0367ef
commit cff883211b
9 changed files with 236 additions and 112 deletions

View file

@ -10,7 +10,7 @@ import (
// bindFunctionBody binds a function body, including a scope, its parameters, and its expressions and statements.
func (b *binder) bindFunctionBody(node ast.Function) {
// Enter a new scope, bind the parameters, and then bind the body using a visitor.
scope := b.ctx.Scope.Push()
scope := b.ctx.Scope.Push(true)
defer scope.Pop()
params := node.GetParameters()
if params != nil {

View file

@ -4,7 +4,6 @@ package binder
import (
"github.com/marapongo/mu/pkg/compiler/ast"
"github.com/marapongo/mu/pkg/compiler/core"
"github.com/marapongo/mu/pkg/compiler/errors"
"github.com/marapongo/mu/pkg/compiler/symbols"
"github.com/marapongo/mu/pkg/tokens"
@ -13,69 +12,74 @@ import (
// Scope facilitates storing information that obeys lexically nested scoping rules.
type Scope struct {
ctx *core.Context // the shared context object for errors, etc.
slot **Scope // the slot rooting the tree of current scopes for pushing/popping.
parent *Scope // the parent scope to restore upon pop, or nil if this is the top.
locals LocalMap // the current scope's locals map (name to local symbol).
Ctx *Context // the shared context object for errors, etc.
Slot **Scope // the slot rooting the tree of current scopes for pushing/popping.
Frame bool // if this scope represents the top of an activation frame.
Parent *Scope // the parent scope to restore upon pop, or nil if this is the top.
Locals LocalMap // the current scope's locals map (name to local symbol).
}
// LocalMap maps the name of locals to their corresponding symbols, for the few places we need name-based lookup.
type LocalMap map[tokens.Name]*symbols.LocalVariable
// NewScope allocates and returns a fresh scope using the given slot, populating it.
func NewScope(ctx *core.Context, slot **Scope) *Scope {
func NewScope(ctx *Context, frame bool) *Scope {
slot := &ctx.Scope
scope := &Scope{
ctx: ctx,
slot: slot,
parent: *slot,
locals: make(LocalMap),
Ctx: ctx,
Slot: slot,
Frame: frame,
Parent: *slot,
Locals: make(LocalMap),
}
*slot = scope
return scope
}
// Push creates a new scope with an empty symbol table parented to the existing one.
func (s *Scope) Push() *Scope {
return NewScope(s.ctx, s.slot)
func (s *Scope) Push(frame bool) *Scope {
return NewScope(s.Ctx, frame)
}
// Pop restores the prior scope into the underlying slot, tossing away the current symbol table.
func (s *Scope) Pop() {
contract.Assert(*s.slot == s)
*s.slot = s.parent
contract.Assert(*s.Slot == s)
*s.Slot = s.Parent
}
// Lookup finds a variable underneath the given name, issuing an error and returning nil if not found.
func (s *Scope) Lookup(nm tokens.Name) *symbols.LocalVariable {
for s != nil {
if sym, exists := s.locals[nm]; exists {
if sym, exists := s.Locals[nm]; exists {
contract.Assert(sym != nil)
return sym
}
// If not in this scope, keep looking at the ancestral chain, if one exists.
s = s.parent
if s.Frame {
s = nil
} else {
s = s.Parent
}
}
// TODO: issue an error about a missing symbol.
return nil
}
// Register registers a local variable with a given name; if it already exists, the function returns false.
func (s *Scope) Register(sym *symbols.LocalVariable) bool {
nm := sym.Name()
if _, exists := s.locals[nm]; exists {
if _, exists := s.Locals[nm]; exists {
// TODO: this won't catch "shadowing" for parent scopes; do we care about this?
return false
}
s.locals[nm] = sym
s.Locals[nm] = sym
return true
}
// TryRegister registers a local with the given name; if it already exists, a compiler error is emitted.
func (s *Scope) TryRegister(node ast.Node, sym *symbols.LocalVariable) {
if !s.Register(sym) {
s.ctx.Diag.Errorf(errors.ErrorSymbolAlreadyExists.At(node), sym.Name)
s.Ctx.Diag.Errorf(errors.ErrorSymbolAlreadyExists.At(node), sym.Name)
}
}

View file

@ -109,7 +109,7 @@ func (a *astBinder) After(node ast.Node) {
func (a *astBinder) visitBlock(node *ast.Block) {
// Entering a new block requires a fresh lexical scope.
a.b.ctx.Scope.Push()
a.b.ctx.Scope.Push(false)
}
func (a *astBinder) checkBlock(node *ast.Block) {

View file

@ -1,12 +0,0 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package errors
import (
"github.com/marapongo/mu/pkg/diag"
)
var ErrorUnrecognizedIntrinsic = &diag.Diag{
ID: 1000,
Message: "Intrinsic '%v' was not recognized; it may be unsupported for the target cloud architecture",
}

View file

@ -0,0 +1,12 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package errors
import (
"github.com/marapongo/mu/pkg/diag"
)
var ErrorUnhandledException = &diag.Diag{
ID: 1000,
Message: "An unhandled exception terminated the program: %v",
}

View file

@ -6,6 +6,7 @@ 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/errors"
"github.com/marapongo/mu/pkg/compiler/symbols"
"github.com/marapongo/mu/pkg/compiler/types"
"github.com/marapongo/mu/pkg/diag"
@ -23,6 +24,8 @@ type Interpreter interface {
// EvaluatePackage performs evaluation on the given blueprint package, starting in its entrypoint.
EvaluatePackage(pkg *pack.Package) graph.Graph
// EvaluateFunction performs an evaluation of the given function, using the provided arguments, returning its graph.
EvaluateFunction(fnc symbols.Function, args ...interface{}) graph.Graph
}
// New creates an interpreter that can be used to evaluate MuPackages.
@ -31,15 +34,15 @@ func New(ctx *binder.Context) Interpreter {
ctx: ctx,
globals: make(globalMap),
}
initLocalScope(&e.locals)
newLocalScope(&e.locals, true, ctx.Scope)
return e
}
type evaluator struct {
fnc *symbols.ModuleMethod // the function under evaluation.
ctx *binder.Context // the binding context with type and symbol information.
globals globalMap // the object values for global variable symbols.
locals *localScope // local variable values scoped by the lexical structure.
fnc symbols.Function // the function currently under evaluation.
ctx *binder.Context // the binding context with type and symbol information.
globals globalMap // the object values for global variable symbols.
locals *localScope // local variable values scoped by the lexical structure.
}
type globalMap map[symbols.Variable]*Object
@ -49,6 +52,7 @@ var _ Interpreter = (*evaluator)(nil)
func (e *evaluator) Ctx() *binder.Context { return e.ctx }
func (e *evaluator) Diag() diag.Sink { return e.ctx.Diag }
// EvaluatePackage performs evaluation on the given blueprint package, starting in its entrypoint.
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.
@ -56,22 +60,99 @@ func (e *evaluator) EvaluatePackage(pkg *pack.Package) graph.Graph {
return nil
}
// Utility functions
// EvaluateFunction performs an evaluation of the given function, using the provided arguments, returning its graph.
func (e *evaluator) EvaluateFunction(fnc symbols.Function, args ...interface{}) graph.Graph {
// First, turn the arguments into real runtime *Objects.
argObjs := make([]*Object, len(args))
for i, arg := range args {
argObjs[i] = NewConstantObject(arg)
}
func (e *evaluator) pushScope() {
e.locals.Push() // for local variables
e.ctx.Scope.Push() // for symbol bindings
// Next, make the call.
_, uw := e.evalCall(fnc, argObjs...)
if uw != nil {
// If the call had an unwind out of it, then presumably we have an unhandled exception.
contract.Assert(!uw.Break)
contract.Assert(!uw.Continue)
contract.Assert(!uw.Return)
contract.Assert(uw.Throw)
// TODO: ideally we would have a stack trace to show here.
e.Diag().Errorf(errors.ErrorUnhandledException, uw.Thrown.Data.(string))
}
// TODO: turn the returned object into a graph.
return nil
}
// Utility functions
// pushScope pushes a new local and context scope. The frame argument indicates whether this is an activation frame,
// meaning that searches for local variables will not probe into parent scopes (since they are inaccessible).
func (e *evaluator) pushScope(frame bool) {
e.locals.Push(frame) // pushing the local scope also updates the context scope.
}
// popScope pops the current local and context scopes.
func (e *evaluator) popScope() {
e.locals.Pop() // for local variables.
e.ctx.Scope.Pop() // for symbol bindings.
e.locals.Pop() // popping the local scope also updates the context scope.
}
// Functions
func (e *evaluator) evalCall(fnc symbols.Function, args ...*Object) (*Object, *Unwind) {
// Save the prior func, set the new one, and restore upon exit.
prior := fnc
e.fnc = fnc
defer func() { e.fnc = prior }()
// Set up a new lexical scope "activation frame" in which we can bind the parameters; restore it upon exit.
e.pushScope(true)
defer e.popScope()
// Ensure that the arguments line up to the parameter slots and add them to the frame.
node := fnc.FuncNode()
params := node.GetParameters()
if params == nil {
contract.Assert(len(args) == 0)
} else {
contract.Assert(len(args) == len(*params))
for i, param := range *params {
sym := e.ctx.RequireVariable(param).(*symbols.LocalVariable)
e.ctx.Scope.Register(sym)
arg := args[i]
contract.Assert(types.CanConvert(arg.Type, sym.Type()))
e.locals.SetValue(sym, arg)
}
}
// Now perform the invocation by visiting the body.
uw := e.evalBlock(node.GetBody())
// Check that the unwind is as expected. In particular:
// 1) no breaks or continues are expected;
// 2) any throw is treated as an unhandled exception that propagates to the caller.
// 3) any return is checked to be of the expected type, and returned as the result of the call.
contract.Assert(!uw.Break)
contract.Assert(!uw.Continue)
retty := fnc.Type().Return
if uw.Throw {
return nil, uw
} else if uw.Return {
ret := uw.Returned
contract.Assert((retty == nil) == (ret == nil))
contract.Assert(ret == nil || types.CanConvert(ret.Type, retty))
return ret, nil
} else {
// An absence of a return is okay for void-returning functions.
contract.Assert(retty == nil)
return nil, 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.
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)
@ -105,9 +186,9 @@ func (e *evaluator) evalStatement(node ast.Statement) *unwind {
}
}
func (e *evaluator) evalBlock(node *ast.Block) *unwind {
func (e *evaluator) evalBlock(node *ast.Block) *Unwind {
// Push a scope at the start, and pop it at afterwards; both for the symbol context and local variable values.
e.pushScope()
e.pushScope(false)
defer e.popScope()
for _, stmt := range node.Statements {
@ -119,7 +200,7 @@ func (e *evaluator) evalBlock(node *ast.Block) *unwind {
return nil
}
func (e *evaluator) evalLocalVariableDeclaration(node *ast.LocalVariableDeclaration) *unwind {
func (e *evaluator) evalLocalVariableDeclaration(node *ast.LocalVariableDeclaration) *Unwind {
// Populate the variable in the scope.
sym := e.ctx.RequireVariable(node.Local).(*symbols.LocalVariable)
e.ctx.Scope.Register(sym)
@ -133,7 +214,7 @@ func (e *evaluator) evalLocalVariableDeclaration(node *ast.LocalVariableDeclarat
return nil
}
func (e *evaluator) evalTryCatchFinally(node *ast.TryCatchFinally) *unwind {
func (e *evaluator) evalTryCatchFinally(node *ast.TryCatchFinally) *Unwind {
// First, execute the TryBlock.
uw := e.evalBlock(node.TryBlock)
if uw != nil && uw.Thrown != nil {
@ -145,8 +226,8 @@ func (e *evaluator) evalTryCatchFinally(node *ast.TryCatchFinally) *unwind {
contract.Assert(types.CanConvert(exty, types.Error))
if types.CanConvert(uw.Thrown.Type, exty) {
// This type matched, so this handler will catch the exception. Set the exception variable,
// evaluate the block, and swap the unwind information (thereby "handling" the in-flight exception).
e.pushScope()
// evaluate the block, and swap the Unwind information (thereby "handling" the in-flight exception).
e.pushScope(false)
e.locals.SetValue(ex, uw.Thrown)
uw = e.evalBlock(catch.Block)
e.popScope()
@ -156,11 +237,11 @@ func (e *evaluator) evalTryCatchFinally(node *ast.TryCatchFinally) *unwind {
}
}
// No matter the unwind instructions, be sure to invoke the FinallyBlock.
// 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.
// Any Unwind information from the finally block overrides the try Unwind that was in flight.
if uwf != nil {
uw = uwf
}
@ -169,7 +250,7 @@ func (e *evaluator) evalTryCatchFinally(node *ast.TryCatchFinally) *unwind {
return uw
}
func (e *evaluator) evalBreakStatement(node *ast.BreakStatement) *unwind {
func (e *evaluator) evalBreakStatement(node *ast.BreakStatement) *Unwind {
var label *tokens.Name
if node.Label != nil {
label = &node.Label.Ident
@ -177,7 +258,7 @@ func (e *evaluator) evalBreakStatement(node *ast.BreakStatement) *unwind {
return breakUnwind(label)
}
func (e *evaluator) evalContinueStatement(node *ast.ContinueStatement) *unwind {
func (e *evaluator) evalContinueStatement(node *ast.ContinueStatement) *Unwind {
var label *tokens.Name
if node.Label != nil {
label = &node.Label.Ident
@ -185,7 +266,7 @@ func (e *evaluator) evalContinueStatement(node *ast.ContinueStatement) *unwind {
return continueUnwind(label)
}
func (e *evaluator) evalIfStatement(node *ast.IfStatement) *unwind {
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 {
@ -199,8 +280,8 @@ func (e *evaluator) evalIfStatement(node *ast.IfStatement) *unwind {
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.
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)
@ -211,19 +292,19 @@ func (e *evaluator) evalLabeledStatement(node *ast.LabeledStatement) *unwind {
return uw
}
func (e *evaluator) evalReturnStatement(node *ast.ReturnStatement) *unwind {
func (e *evaluator) evalReturnStatement(node *ast.ReturnStatement) *Unwind {
var ret *Object
if node.Expression != nil {
var uw *unwind
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.
// 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 {
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.
@ -233,9 +314,9 @@ func (e *evaluator) evalThrowStatement(node *ast.ThrowStatement) *unwind {
return throwUnwind(thrown)
}
func (e *evaluator) evalWhileStatement(node *ast.WhileStatement) *unwind {
func (e *evaluator) evalWhileStatement(node *ast.WhileStatement) *Unwind {
// So long as the test evaluates to true, keep on visiting the body.
var uw *unwind
var uw *Unwind
for {
test, uw := e.evalExpression(node.Test)
if uw != nil {
@ -250,7 +331,7 @@ func (e *evaluator) evalWhileStatement(node *ast.WhileStatement) *unwind {
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.
// If it's not a continue or break, stash the Unwind away and return it.
uw = uws
break
}
@ -262,7 +343,7 @@ func (e *evaluator) evalWhileStatement(node *ast.WhileStatement) *unwind {
return uw // usually nil, unless a body statement threw/returned.
}
func (e *evaluator) evalMultiStatement(node *ast.MultiStatement) *unwind {
func (e *evaluator) evalMultiStatement(node *ast.MultiStatement) *Unwind {
for _, stmt := range node.Statements {
if uw := e.evalStatement(stmt); uw != nil {
return uw
@ -271,16 +352,16 @@ func (e *evaluator) evalMultiStatement(node *ast.MultiStatement) *unwind {
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.
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.
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)
@ -324,23 +405,23 @@ func (e *evaluator) evalExpression(node ast.Expression) (*Object, *unwind) {
}
}
func (e *evaluator) evalNullLiteral(node *ast.NullLiteral) (*Object, *unwind) {
func (e *evaluator) evalNullLiteral(node *ast.NullLiteral) (*Object, *Unwind) {
return NewPrimitiveObject(types.Null, nil), nil
}
func (e *evaluator) evalBoolLiteral(node *ast.BoolLiteral) (*Object, *unwind) {
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) {
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) {
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) {
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)
@ -394,7 +475,7 @@ func (e *evaluator) evalArrayLiteral(node *ast.ArrayLiteral) (*Object, *unwind)
return NewPrimitiveObject(ty, arr), nil
}
func (e *evaluator) evalObjectLiteral(node *ast.ObjectLiteral) (*Object, *unwind) {
func (e *evaluator) evalObjectLiteral(node *ast.ObjectLiteral) (*Object, *Unwind) {
obj := NewObject(e.ctx.Types[node])
if node.Properties != nil {
@ -412,53 +493,53 @@ func (e *evaluator) evalObjectLiteral(node *ast.ObjectLiteral) (*Object, *unwind
return obj, nil
}
func (e *evaluator) evalLoadLocationExpression(node *ast.LoadLocationExpression) (*Object, *unwind) {
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) {
func (e *evaluator) evalLoadDynamicExpression(node *ast.LoadDynamicExpression) (*Object, *Unwind) {
return nil, nil
}
func (e *evaluator) evalNewExpression(node *ast.NewExpression) (*Object, *unwind) {
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) {
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) {
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) {
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) {
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) {
func (e *evaluator) evalCastExpression(node *ast.CastExpression) (*Object, *Unwind) {
return nil, nil
}
func (e *evaluator) evalIsInstExpression(node *ast.IsInstExpression) (*Object, *unwind) {
func (e *evaluator) evalIsInstExpression(node *ast.IsInstExpression) (*Object, *Unwind) {
return nil, nil
}
func (e *evaluator) evalTypeOfExpression(node *ast.TypeOfExpression) (*Object, *unwind) {
func (e *evaluator) evalTypeOfExpression(node *ast.TypeOfExpression) (*Object, *Unwind) {
return nil, nil
}
func (e *evaluator) evalConditionalExpression(node *ast.ConditionalExpression) (*Object, *unwind) {
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 {
@ -471,14 +552,14 @@ func (e *evaluator) evalConditionalExpression(node *ast.ConditionalExpression) (
}
}
func (e *evaluator) evalSequenceExpression(node *ast.SequenceExpression) (*Object, *unwind) {
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
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.
// If the Unwind was non-nil, stop visiting the expressions and propagate it now.
return nil, uw
}
}

View file

@ -40,6 +40,7 @@ func NewPrimitiveObject(t symbols.Type, data interface{}) *Object {
// NewErrorObject creates a new exception with the given message.
func NewErrorObject(message string, args ...interface{}) *Object {
// TODO: capture a stack trace.
return NewPrimitiveObject(types.Error, fmt.Sprintf(message, args...))
}

View file

@ -3,6 +3,7 @@
package eval
import (
"github.com/marapongo/mu/pkg/compiler/binder"
"github.com/marapongo/mu/pkg/compiler/symbols"
"github.com/marapongo/mu/pkg/compiler/types"
"github.com/marapongo/mu/pkg/util/contract"
@ -10,36 +11,73 @@ import (
// localScope is a kind of scope that holds local variable values.
type localScope struct {
slot **localScope
parent *localScope
values valueMap
Slot **localScope
Parent *localScope // the parent to restore when popping this scope.
Frame bool // if a top-level frame, searches won't reach beyond this scope.
Lexical *binder.Scope // the binding scope tells us at what level, lexically, a local resides.
Values valueMap // the values map contains the value for a variable so long as it exists.
}
// valueMap maps local variables to their current known object value (if any).
type valueMap map[*symbols.LocalVariable]*Object
func initLocalScope(slot **localScope) *localScope {
func newLocalScope(slot **localScope, frame bool, lex *binder.Scope) *localScope {
return &localScope{
slot: slot,
parent: *slot,
values: make(valueMap),
Slot: slot,
Parent: *slot,
Frame: frame,
Lexical: lex,
Values: make(valueMap),
}
}
func (s *localScope) Push() *localScope {
return initLocalScope(s.slot)
func (s *localScope) Push(frame bool) *localScope {
lex := s.Lexical.Push(frame)
return newLocalScope(s.Slot, frame, lex)
}
func (s *localScope) Pop() {
contract.Assert(*s.slot == s)
*s.slot = s.parent
contract.Assert(*s.Slot == s)
s.Lexical.Pop()
*s.Slot = s.Parent
}
func (s *localScope) GetValue(sym *symbols.LocalVariable) *Object {
return s.values[sym]
for s != nil {
if val, has := s.Values[sym]; has {
return val
}
// Keep looking into parent scopes so long as we didn't hit the top of an activation frame.
if s.Frame {
s = nil
} else {
s = s.Parent
}
}
return nil
}
func (s *localScope) SetValue(sym *symbols.LocalVariable, obj *Object) {
contract.Assert(obj == nil || types.CanConvert(obj.Type, sym.Type()))
s.values[sym] = obj
// To set a value, we must first find the position in the shadowed frames, so that the value's lifetime is identical
// to the actual local variable symbol's lifetime. This ensures that once that frame is popped, so too is any value
// associated with it; and similarly, that its value won't be popped until the frame containing the variable is.
lex := s.Lexical
for {
for _, lexloc := range lex.Locals {
if lexloc == sym {
break
}
}
contract.Assert(!s.Frame)
contract.Assert(!lex.Frame)
s = s.Parent
lex = lex.Parent
contract.Assert(s.Lexical == lex)
}
contract.Assert(s != nil)
contract.Assert(lex != nil)
s.Values[sym] = obj
}

View file

@ -6,8 +6,8 @@ import (
"github.com/marapongo/mu/pkg/tokens"
)
// unwind instructs callers how to unwind the stack.
type unwind struct {
// 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.
@ -17,7 +17,7 @@ type unwind struct {
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} }
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} }