diff --git a/pkg/compiler/binder/stmtexpr.go b/pkg/compiler/binder/stmtexpr.go index f44d30752..222d46f9d 100644 --- a/pkg/compiler/binder/stmtexpr.go +++ b/pkg/compiler/binder/stmtexpr.go @@ -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 diff --git a/pkg/compiler/errors/eval.go b/pkg/compiler/errors/eval.go index 88a0d6358..839a42fc6 100644 --- a/pkg/compiler/errors/eval.go +++ b/pkg/compiler/errors/eval.go @@ -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") ) diff --git a/pkg/eval/alloc.go b/pkg/eval/alloc.go index 4d5e87323..35f26689b 100644 --- a/pkg/eval/alloc.go +++ b/pkg/eval/alloc.go @@ -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 } diff --git a/pkg/eval/eval.go b/pkg/eval/eval.go index 26dea1a93..b18f79870 100644 --- a/pkg/eval/eval.go +++ b/pkg/eval/eval.go @@ -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) { diff --git a/pkg/eval/exceptions.go b/pkg/eval/exceptions.go new file mode 100644 index 000000000..899a8173e --- /dev/null +++ b/pkg/eval/exceptions.go @@ -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) +} diff --git a/pkg/eval/rt/object.go b/pkg/eval/rt/object.go index 79f8ab0da..6c24a2003 100644 --- a/pkg/eval/rt/object.go +++ b/pkg/eval/rt/object.go @@ -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) }