Implement dynamic loading

This change implements dynamic loads in the binder and evaluator.
This commit is contained in:
joeduffy 2017-02-12 08:22:44 -08:00
parent 93d9df1c4c
commit a16bb714e4
6 changed files with 239 additions and 64 deletions

View file

@ -81,7 +81,7 @@ func (a *astBinder) After(node ast.Node) {
case *ast.LoadLocationExpression:
a.checkLoadLocationExpression(n)
case *ast.LoadDynamicExpression:
a.b.ctx.RegisterType(n, types.Dynamic) // register as a dynamic type.
a.checkLoadDynamicExpression(n)
case *ast.NewExpression:
a.checkNewExpression(n)
case *ast.InvokeFunctionExpression:
@ -312,12 +312,25 @@ func (a *astBinder) checkLoadLocationExpression(node *ast.LoadLocationExpression
if sym == nil {
ty = types.Error // no symbol found, use an error type.
} else {
var usesThis bool
for ty == nil {
// Check that the this object is well-formed and extract the symbol.
switch s := sym.(type) {
case symbols.Function:
ty = s.FuncType() // use the func's type.
case symbols.Variable:
ty = s.Type() // use the variable's type.
case *symbols.LocalVariable:
ty = s.Type()
usesThis = false
case *symbols.ClassMethod:
ty = s.FuncType()
usesThis = !s.Static()
case *symbols.ClassProperty:
ty = s.Type()
usesThis = !s.Static()
case *symbols.ModuleMethod:
ty = s.FuncType()
usesThis = false
case *symbols.ModuleProperty:
ty = s.Type()
usesThis = false
case *symbols.Export:
// For exports, let's keep digging until we hit something concrete.
sym = s.Referent
@ -325,12 +338,36 @@ func (a *astBinder) checkLoadLocationExpression(node *ast.LoadLocationExpression
contract.Failf("Unrecognized load location token '%v' symbol type: %v", sym.Token(), reflect.TypeOf(s))
}
}
if usesThis {
if node.Object == nil {
a.b.Diag().Errorf(errors.ErrorExpectedObject.At(node))
}
} else if node.Object != nil {
a.b.Diag().Errorf(errors.ErrorUnexpectedObject.At(node))
}
}
// Register the type; not that, although this is a valid l-value, we do not create a pointer out of it implicitly.
a.b.ctx.RegisterType(node, ty)
}
func (a *astBinder) checkLoadDynamicExpression(node *ast.LoadDynamicExpression) {
// Ensure the object is non-nil.
if node.Object == nil {
a.b.Diag().Errorf(errors.ErrorExpectedObject.At(node))
}
// Now ensure that the right hand side is either a string or a dynamic.
name := a.b.ctx.RequireType(node.Name)
if name != types.Dynamic && !types.CanConvert(name, types.String) {
}
// No matter the outcome, a load dynamic always produces a dynamically typed thing.
a.b.ctx.RegisterType(node, types.Dynamic)
}
func (a *astBinder) checkNewExpression(node *ast.NewExpression) {
// Figure out which type we're instantiating.
var ty symbols.Type

View file

@ -13,4 +13,6 @@ var (
ErrorFunctionArgNotFound = newError(1006, "Function argument '%v' was not supplied")
ErrorFunctionArgUnknown = newError(1007, "Function argument '%v' was not recognized")
ErrorIllegalReadonlyLValue = newError(1008, "A readonly target cannot be used as an assignment target")
ErrorExpectedObject = newError(1009, "Expected an object target for this instance member load operation")
ErrorUnexpectedObject = newError(1010, "Unexpected object target for this static or module load operation")
)

View file

@ -4,7 +4,6 @@ package eval
import (
"github.com/marapongo/mu/pkg/compiler/symbols"
"github.com/marapongo/mu/pkg/compiler/types"
"github.com/marapongo/mu/pkg/eval/rt"
)
@ -34,28 +33,35 @@ func (a *Allocator) New(t symbols.Type) *rt.Object {
// NewPrimitive creates a new primitive object with the given primitive type.
func (a *Allocator) NewPrimitive(t symbols.Type, v interface{}) *rt.Object {
obj := rt.NewObject(t, v, nil)
obj := rt.NewPrimitiveObject(t, v)
a.onNewObject(obj)
return obj
}
// NewBool creates a new primitive number object.
func (a *Allocator) NewBool(v bool) *rt.Object {
obj := rt.NewPrimitiveObject(types.Bool, v)
obj := rt.NewBoolObject(v)
a.onNewObject(obj)
return obj
}
// NewNumber creates a new primitive number object.
func (a *Allocator) NewNumber(v float64) *rt.Object {
obj := rt.NewPrimitiveObject(types.Number, v)
obj := rt.NewNumberObject(v)
a.onNewObject(obj)
return obj
}
// NewNull creates a new null object.
func (a *Allocator) NewNull() *rt.Object {
obj := rt.NewNullObject()
a.onNewObject(obj)
return obj
}
// NewString creates a new primitive number object.
func (a *Allocator) NewString(v string) *rt.Object {
obj := rt.NewPrimitiveObject(types.String, v)
obj := rt.NewStringObject(v)
a.onNewObject(obj)
return obj
}

View file

@ -197,7 +197,7 @@ func (e *evaluator) EvaluateFunction(fnc symbols.Function, this *rt.Object, args
if e.Diag().Success() {
// If the arguments bound correctly, make the call.
_, uw := e.evalCall(fnc, this, argos...)
_, uw := e.evalCall(fnc.Tree(), fnc, this, argos...)
if uw != nil {
// If the call had an unwind out of it, then presumably we have an unhandled exception.
e.issueUnhandledException(uw, errors.ErrorUnhandledException.At(fnc.Tree()))
@ -244,7 +244,7 @@ func (e *evaluator) ensureClassInit(class *symbols.Class) {
glog.V(7).Infof("Initializing class: %v", class)
contract.Assert(len(init.Ty.Parameters) == 0)
contract.Assert(init.Ty.Return == nil)
ret, uw := e.evalCall(init, nil)
ret, uw := e.evalCall(class.Tree(), init, nil)
contract.Assert(ret == nil)
if uw != nil {
// Must be an unhandled exception; spew it as an error (but keep going).
@ -273,7 +273,7 @@ func (e *evaluator) ensureModuleInit(mod *symbols.Module) {
glog.V(7).Infof("Initializing module: %v", mod)
contract.Assert(len(init.Type.Parameters) == 0)
contract.Assert(init.Type.Return == nil)
ret, uw := e.evalCall(init, nil)
ret, uw := e.evalCall(mod.Tree(), init, nil)
contract.Assert(ret == nil)
if uw != nil {
// Must be an unhandled exception; spew it as an error (but keep going).
@ -290,7 +290,7 @@ func (e *evaluator) issueUnhandledException(uw *Unwind, err *diag.Diag, args ...
contract.Assert(uw.Throw())
var msg string
if thrown := uw.Thrown(); thrown != nil {
msg = thrown.StringValue()
msg = thrown.ExceptionMessage()
}
if msg == "" {
msg = "no details available"
@ -313,9 +313,27 @@ func (e *evaluator) popScope() {
// Functions
func (e *evaluator) evalCall(fnc symbols.Function, this *rt.Object, args ...*rt.Object) (*rt.Object, *Unwind) {
func (e *evaluator) evalCall(node diag.Diagable, fnc symbols.Function,
this *rt.Object, args ...*rt.Object) (*rt.Object, *Unwind) {
glog.V(7).Infof("Evaluating call to fnc %v; this=%v args=%v", fnc, this != nil, len(args))
// First check the this pointer, since it might throw before the call even happens.
var thisVariable *symbols.LocalVariable
var superVariable *symbols.LocalVariable
switch f := fnc.(type) {
case *symbols.ClassMethod:
if f.Static() {
contract.Assert(this == nil)
} else if uw := e.checkThis(node, this); uw != nil {
return nil, uw
} else {
thisVariable = f.Parent.This
superVariable = f.Parent.Super
}
default:
contract.Assert(this == nil)
}
// Save the prior func, set the new one, and restore upon exit.
prior := fnc
e.fnc = fnc
@ -332,33 +350,20 @@ func (e *evaluator) evalCall(fnc symbols.Function, this *rt.Object, args ...*rt.
}
// If the target is an instance method, the "this" and "super" variables must be bound to values.
if this != nil {
switch f := fnc.(type) {
case *symbols.ClassMethod:
contract.Assertf(!f.Static(),
"Static methods don't have 'this' arguments, but we got a non-nil one")
contract.Assertf(types.CanConvert(this.Type(), f.Parent),
"'this' argument was of the wrong type")
e.ctx.Scope.Register(f.Parent.This)
e.locals.InitValueAddr(f.Parent.This, rt.NewPointer(this, true))
if f.Parent.Super != nil {
e.ctx.Scope.Register(f.Parent.Super)
e.locals.InitValueAddr(f.Parent.Super, rt.NewPointer(this, true))
}
default:
contract.Failf("Only class methods should have 'this' arguments, but we got a non-nil one")
}
} else {
// If no this was supplied, we expect that this is either not a class method, or it is a static.
switch f := fnc.(type) {
case *symbols.ClassMethod:
contract.Assertf(f.Static(), "Instance methods require 'this' arguments, but we got nil")
}
if thisVariable != nil {
contract.Assert(this != nil)
e.ctx.Scope.Register(thisVariable)
e.locals.InitValueAddr(thisVariable, rt.NewPointer(this, true))
}
if superVariable != nil {
contract.Assert(this != nil)
e.ctx.Scope.Register(superVariable)
e.locals.InitValueAddr(superVariable, rt.NewPointer(this, true))
}
// Ensure that the arguments line up to the parameter slots and add them to the frame.
node := fnc.FuncNode()
params := node.GetParameters()
fnode := fnc.FuncNode()
params := fnode.GetParameters()
if params == nil {
contract.Assert(len(args) == 0)
} else {
@ -373,7 +378,7 @@ func (e *evaluator) evalCall(fnc symbols.Function, this *rt.Object, args ...*rt.
}
// Now perform the invocation by visiting the body.
uw := e.evalBlock(node.GetBody())
uw := e.evalBlock(fnode.GetBody())
// Check that the unwind is as expected. In particular:
// 1) no breaks or continues are expected;
@ -400,6 +405,10 @@ func (e *evaluator) evalCall(fnc symbols.Function, this *rt.Object, args ...*rt.
// Statements
func (e *evaluator) evalStatement(node ast.Statement) *Unwind {
if glog.V(7) {
glog.V(7).Infof("Evaluating statement: %v", reflect.TypeOf(node))
}
// Simply switch on the node type and dispatch to the specific function, returning the Unwind info.
switch n := node.(type) {
case *ast.Block:
@ -609,6 +618,10 @@ func (e *evaluator) evalExpressionStatement(node *ast.ExpressionStatement) *Unwi
// Expressions
func (e *evaluator) evalExpression(node ast.Expression) (*rt.Object, *Unwind) {
if glog.V(7) {
glog.V(7).Infof("Evaluating expression: %v", reflect.TypeOf(node))
}
// 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:
@ -660,6 +673,8 @@ func (e *evaluator) evalLValueExpression(node ast.Expression) (location, *Unwind
switch n := node.(type) {
case *ast.LoadLocationExpression:
return e.evalLoadLocation(n, true)
case *ast.LoadDynamicExpression:
return e.evalLoadDynamic(n, true)
case *ast.UnaryOperatorExpression:
contract.Assert(n.Operator == ast.OpDereference)
obj, uw := e.evalUnaryOperatorExpressionFor(n, true)
@ -671,19 +686,19 @@ func (e *evaluator) evalLValueExpression(node ast.Expression) (location, *Unwind
}
func (e *evaluator) evalNullLiteral(node *ast.NullLiteral) (*rt.Object, *Unwind) {
return e.alloc.NewPrimitive(types.Null, nil), nil
return e.alloc.NewNull(), nil
}
func (e *evaluator) evalBoolLiteral(node *ast.BoolLiteral) (*rt.Object, *Unwind) {
return e.alloc.NewPrimitive(types.Bool, node.Value), nil
return e.alloc.NewBool(node.Value), nil
}
func (e *evaluator) evalNumberLiteral(node *ast.NumberLiteral) (*rt.Object, *Unwind) {
return e.alloc.NewPrimitive(types.Number, node.Value), nil
return e.alloc.NewNumber(node.Value), nil
}
func (e *evaluator) evalStringLiteral(node *ast.StringLiteral) (*rt.Object, *Unwind) {
return e.alloc.NewPrimitive(types.String, node.Value), nil
return e.alloc.NewString(node.Value), nil
}
func (e *evaluator) evalArrayLiteral(node *ast.ArrayLiteral) (*rt.Object, *Unwind) {
@ -704,7 +719,7 @@ func (e *evaluator) evalArrayLiteral(node *ast.ArrayLiteral) (*rt.Object, *Unwin
sz := int(sze.NumberValue())
if sz < 0 {
// If the size is less than zero, raise a new error.
return nil, NewThrowUnwind(e.alloc.NewException("Invalid array size (must be >= 0)"))
return nil, NewThrowUnwind(e.NewNegativeArrayLengthException())
}
arr = make([]rt.Value, sz)
}
@ -718,9 +733,7 @@ func (e *evaluator) evalArrayLiteral(node *ast.ArrayLiteral) (*rt.Object, *Unwin
arr = make([]rt.Value, 0, len(*node.Elements))
} else if len(*node.Elements) > *sz {
// The element count exceeds the size; raise an error.
return nil, NewThrowUnwind(
e.alloc.NewException("Invalid number of array elements; expected <=%v, got %v",
*sz, len(*node.Elements)))
return nil, NewThrowUnwind(e.NewIncorrectArrayElementCountException(*sz, len(*node.Elements)))
}
for i, elem := range *node.Elements {
@ -782,10 +795,10 @@ type location struct {
Obj *rt.Object // the resulting object (pointer if lval, object otherwise).
}
// evalLoadLocationExpressionInfo evaluates and loads information about the target. It takes an lval bool which
// evalLoadLocation evaluates and loads information about the target. It takes an lval bool which
// determines whether the resulting location object is an lval (pointer) or regular object.
func (e *evaluator) evalLoadLocation(node *ast.LoadLocationExpression, lval bool) (location, *Unwind) {
// If there's a 'this', evaluate it.
// If there's a target object, evaluate it.
var this *rt.Object
if node.Object != nil {
var uw *Unwind
@ -799,8 +812,9 @@ func (e *evaluator) evalLoadLocation(node *ast.LoadLocationExpression, lval bool
var ty symbols.Type
var sym symbols.Symbol
tok := node.Name.Tok
if this == nil && tok.Simple() {
if tok.Simple() {
// If there is no object and the name is simple, it refers to a local variable in the current scope.
contract.Assert(this == nil)
loc := e.ctx.Scope.Lookup(tok.Name())
contract.Assert(loc != nil)
pv = e.locals.GetValueAddr(loc, true)
@ -812,19 +826,30 @@ func (e *evaluator) evalLoadLocation(node *ast.LoadLocationExpression, lval bool
contract.Assert(sym != nil) // don't issue errors; we shouldn't ever get here if verification failed.
switch s := sym.(type) {
case *symbols.ClassProperty:
// Ensure that a this is present if instance, and not if static.
if s.Static() {
contract.Assert(this == nil)
} else if uw := e.checkThis(node, this); uw != nil {
return location{}, uw
}
// Search the class's properties and, if not present, allocate a new one.
contract.Assert(this != nil)
pv = this.GetPropertyAddr(rt.PropertyKey(sym.Name()), true)
ty = s.Type()
case *symbols.ClassMethod:
// Ensure that a this is present if instance, and not if static.
if s.Static() {
contract.Assert(this == nil)
} else if uw := e.checkThis(node, this); uw != nil {
return location{}, uw
}
// Create a new readonly ref slot, pointing to the method, that will abandon if overwritten.
contract.Assert(this != nil)
// TODO[marapongo/mu#56]: consider permitting "dynamic" method overwriting.
pv = rt.NewPointer(e.alloc.NewFunction(s, this), true)
ty = s.Type()
case *symbols.ModuleProperty:
// Search the globals table and, if not present, allocate a new property.
contract.Assert(this == nil)
ref, has := e.globals[s]
if !has {
ref = rt.NewPointer(nil, s.Readonly())
@ -832,12 +857,17 @@ func (e *evaluator) evalLoadLocation(node *ast.LoadLocationExpression, lval bool
}
pv = ref
ty = s.Type()
if this != nil {
e.Diag().Errorf(errors.ErrorUnexpectedObject.At(node))
}
case *symbols.ModuleMethod:
// Create a new readonly ref slot, pointing to the method, that will abandon if overwritten.
contract.Assert(this == nil)
// TODO[marapongo/mu#56]: consider permitting "dynamic" method overwriting.
pv = rt.NewPointer(e.alloc.NewFunction(s, nil), true)
ty = s.Type
if this != nil {
e.Diag().Errorf(errors.ErrorUnexpectedObject.At(node))
}
case *symbols.Export:
// Simply chase the referent symbol until we bottom out on one of the above cases.
sym = s.Referent
@ -867,9 +897,62 @@ func (e *evaluator) evalLoadLocation(node *ast.LoadLocationExpression, lval bool
}, nil
}
// checkThis checks a this object, raising a runtime error if it is the runtime null value.
func (e *evaluator) checkThis(node diag.Diagable, this *rt.Object) *Unwind {
contract.Assert(this != nil) // binder should catch cases where this isn't true
if this.Type() == types.Null {
return NewThrowUnwind(e.NewNullObjectException())
}
return nil
}
func (e *evaluator) evalLoadDynamicExpression(node *ast.LoadDynamicExpression) (*rt.Object, *Unwind) {
contract.Failf("Evaluation of %v nodes not yet implemented", reflect.TypeOf(node))
return nil, nil
loc, uw := e.evalLoadDynamic(node, false)
return loc.Obj, uw
}
func (e *evaluator) evalLoadDynamic(node *ast.LoadDynamicExpression, lval bool) (location, *Unwind) {
var uw *Unwind
// Evaluate the object and then the property expression.
var this *rt.Object
if this, uw = e.evalExpression(node.Object); uw != nil {
return location{}, uw
}
var name *rt.Object
if name, uw = e.evalExpression(node.Name); uw != nil {
return location{}, uw
}
// Check that the object isn't null; if it is, raise an exception.
if uw = e.checkThis(node, this); uw != nil {
return location{}, uw
}
// Now go ahead and search the object for a property with the given name.
contract.Assertf(name.Type() == types.String, "At the moment, only string names are supported")
pv := this.GetPropertyAddr(rt.PropertyKey(name.String()), true)
// If this isn't for an l-value, return the raw object. Otherwise, make sure it's not readonly, and return it.
var obj *rt.Object
if lval {
// A readonly reference cannot be used as an l-value.
if pv.Readonly() {
e.Diag().Errorf(errors.ErrorIllegalReadonlyLValue.At(node))
}
obj = e.alloc.NewPointer(types.Dynamic, pv)
} else {
obj = pv.Obj()
}
contract.Assert(obj != nil)
// TODO: we need to produce a symbol (or something) for the graph hooks to use.
return location{
This: this,
Sym: nil,
Lval: lval,
Obj: obj,
}, nil
}
func (e *evaluator) evalNewExpression(node *ast.NewExpression) (*rt.Object, *Unwind) {
@ -900,7 +983,7 @@ func (e *evaluator) evalNewExpression(node *ast.NewExpression) (*rt.Object, *Unw
}
// Now dispatch the function call using the fresh object as the constructor's `this` argument.
if _, uw := e.evalCall(ctormeth, obj, args...); uw != nil {
if _, uw := e.evalCall(node, ctormeth, obj, args...); uw != nil {
return nil, uw
}
} else {
@ -944,7 +1027,7 @@ func (e *evaluator) evalInvokeFunctionExpression(node *ast.InvokeFunctionExpress
}
// Finally, actually dispatch the call; this will create the activation frame, etc. for us.
return e.evalCall(fnc.Func, fnc.This, args...)
return e.evalCall(node, fnc.Func, fnc.This, args...)
}
func (e *evaluator) evalLambdaExpression(node *ast.LambdaExpression) (*rt.Object, *Unwind) {

19
pkg/eval/exceptions.go Normal file
View file

@ -0,0 +1,19 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package eval
import (
"github.com/marapongo/mu/pkg/eval/rt"
)
func (e *evaluator) NewNullObjectException() *rt.Object {
return e.alloc.NewException("Target object is null")
}
func (e *evaluator) NewNegativeArrayLengthException() *rt.Object {
return e.alloc.NewException("Invalid array size (must be >= 0)")
}
func (e *evaluator) NewIncorrectArrayElementCountException(expect int, got int) *rt.Object {
return e.alloc.NewException("Invalid number of array elements; expected <=%v, got %v", expect, got)
}

View file

@ -81,6 +81,15 @@ func (o *Object) PointerValue() *Pointer {
return r
}
// ExceptionMessage asserts that the target is an exception and returns its message.
func (o *Object) ExceptionMessage() string {
contract.Assertf(o.t == types.Exception, "Expected object type to be Exception; got %v", o.t)
contract.Assertf(o.value != nil, "Expected Exception object to carry a Value; got nil")
s, ok := o.value.(string)
contract.Assertf(ok, "Expected Exception object's Value to be string")
return s
}
// GetPropertyAddr returns the reference to an object's property, lazily initializing if 'init' is true, or
// returning nil otherwise.
func (o *Object) GetPropertyAddr(key PropertyKey, init bool) *Pointer {
@ -109,6 +118,12 @@ func (o *Object) GetPropertyAddr(key PropertyKey, init bool) *Pointer {
}
}
}
// If no members was found, this is presumably a dynamic scenario. Initialize the slot to null.
if obj == nil {
obj = NewNullObject()
}
ptr = NewPointer(obj, readonly)
o.properties[key] = ptr
}
@ -167,18 +182,31 @@ func NewPrimitiveObject(t symbols.Type, v interface{}) *Object {
return NewObject(t, v, nil)
}
var trueObj *Object = NewPrimitiveObject(types.Bool, true)
var falseObj *Object = NewPrimitiveObject(types.Bool, false)
// NewBoolObject creates a new primitive number object.
func NewBoolObject(v bool) *Object {
return NewPrimitiveObject(types.Bool, v)
if v {
return trueObj
}
return falseObj
}
// NewNumber creates a new primitive number object.
func NewNumber(v float64) *Object {
// NewNumberObject creates a new primitive number object.
func NewNumberObject(v float64) *Object {
return NewPrimitiveObject(types.Number, v)
}
// NewString creates a new primitive number object.
func NewString(v string) *Object {
var nullObj *Object = NewPrimitiveObject(types.Null, nil)
// NewNullObject creates a new null object; null objects are not expected to have distinct identity.
func NewNullObject() *Object {
return nullObj
}
// NewStringObject creates a new primitive number object.
func NewStringObject(v string) *Object {
return NewPrimitiveObject(types.String, v)
}