We would like to allow developers to use async/await on the inside (Node.js) of Lumi programs. We now support (don't error on) usage of async/await inside runtime callbacks in Lumi programs. If await is used during deployment, it will trigger an error. Also adds support for try/catch in LumiJS, as these are used more heavily in async/await code. Since we target Node.js environments without native support for async/await, we also emit runtime helpers to support TS transpilation of async/await for Node.js pre-7.6.
2345 lines
84 KiB
Go
2345 lines
84 KiB
Go
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
|
|
|
package eval
|
|
|
|
import (
|
|
"math"
|
|
"reflect"
|
|
"sort"
|
|
|
|
"github.com/golang/glog"
|
|
|
|
"github.com/pulumi/lumi/pkg/compiler/ast"
|
|
"github.com/pulumi/lumi/pkg/compiler/binder"
|
|
"github.com/pulumi/lumi/pkg/compiler/core"
|
|
"github.com/pulumi/lumi/pkg/compiler/errors"
|
|
"github.com/pulumi/lumi/pkg/compiler/symbols"
|
|
"github.com/pulumi/lumi/pkg/compiler/types"
|
|
"github.com/pulumi/lumi/pkg/diag"
|
|
"github.com/pulumi/lumi/pkg/eval/rt"
|
|
"github.com/pulumi/lumi/pkg/tokens"
|
|
"github.com/pulumi/lumi/pkg/util/contract"
|
|
)
|
|
|
|
// Interpreter can evaluate compiled LumiPacks.
|
|
type Interpreter interface {
|
|
core.Phase
|
|
|
|
Ctx() *binder.Context // the binding context object.
|
|
|
|
// EvaluatePackage performs evaluation on the given blueprint package.
|
|
EvaluatePackage(pkg *symbols.Package, args core.Args) (*rt.Object, *rt.Unwind)
|
|
// EvaluateModule performs evaluation on the given module's entrypoint function.
|
|
EvaluateModule(mod *symbols.Module, args core.Args) (*rt.Object, *rt.Unwind)
|
|
// EvaluateFunction performs an evaluation of the given function, using the provided arguments.
|
|
EvaluateFunction(fnc symbols.Function, this *rt.Object, args core.Args) (*rt.Object, *rt.Unwind)
|
|
|
|
// LoadLocation loads a location by symbol; lval controls whether it is an l-value or just a value.
|
|
LoadLocation(tree diag.Diagable, sym symbols.Symbol, this *rt.Object, lval bool) (*Location, *rt.Unwind)
|
|
|
|
// UnhandledException triggers the unhandled exception logic.
|
|
UnhandledException(tree diag.Diagable, ex *rt.Exception)
|
|
}
|
|
|
|
// New creates an interpreter that can be used to evaluate LumiPacks.
|
|
func New(ctx *binder.Context, hooks Hooks) Interpreter {
|
|
e := &evaluator{
|
|
ctx: ctx,
|
|
hooks: hooks,
|
|
modules: make(moduleMap),
|
|
statics: make(staticMap),
|
|
protos: make(prototypeMap),
|
|
modinits: make(modinitMap),
|
|
classinits: make(classinitMap),
|
|
}
|
|
newLocalScope(&e.locals, true, ctx.Scope)
|
|
contract.Assert(e.locals != nil)
|
|
return e
|
|
}
|
|
|
|
type evaluator struct {
|
|
fnc symbols.Function // the function currently under evaluation.
|
|
ctx *binder.Context // the binding context with type and symbol information.
|
|
hooks Hooks // callbacks that hook into interpreter events.
|
|
modules moduleMap // the current module objects for all modules.
|
|
statics staticMap // the object values for all static variable symbols.
|
|
protos prototypeMap // the current "prototype" objects for all classes.
|
|
stack *rt.StackFrame // a stack of frames to keep track of calls.
|
|
locals rt.Environment // variable values corresponding to the current lexical scope.
|
|
modinits modinitMap // a map of which modules have been initialized already.
|
|
classinits classinitMap // a map of which classes have been initialized already.
|
|
}
|
|
|
|
type moduleMap map[*symbols.Module]*rt.Object
|
|
type staticMap map[*symbols.Class]*rt.PropertyMap
|
|
type prototypeMap map[symbols.Type]*rt.Object
|
|
type modinitMap map[*symbols.Module]bool
|
|
type classinitMap map[*symbols.Class]bool
|
|
|
|
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.
|
|
func (e *evaluator) EvaluatePackage(pkg *symbols.Package, args core.Args) (*rt.Object, *rt.Unwind) {
|
|
glog.Infof("Evaluating package '%v'", pkg.Name())
|
|
if e.hooks != nil {
|
|
uw, leave := e.hooks.OnEnterPackage(pkg)
|
|
if uw != nil {
|
|
return nil, uw
|
|
} else if leave != nil {
|
|
defer leave()
|
|
}
|
|
}
|
|
|
|
if glog.V(2) {
|
|
defer glog.V(2).Infof("Evaluation of package '%v' completed w/ %v warnings and %v errors",
|
|
pkg.Name(), e.Diag().Warnings(), e.Diag().Errors())
|
|
}
|
|
|
|
// Search the package for a default module to evaluate.
|
|
var ret *rt.Object
|
|
var uw *rt.Unwind
|
|
defmod := pkg.Default()
|
|
if defmod == nil {
|
|
e.Diag().Errorf(errors.ErrorPackageHasNoDefaultModule.At(pkg.Tree()), pkg.Name())
|
|
} else {
|
|
ret, uw = e.EvaluateModule(defmod, args)
|
|
}
|
|
return ret, uw
|
|
}
|
|
|
|
// EvaluateModule performs evaluation on the given module's entrypoint function.
|
|
func (e *evaluator) EvaluateModule(mod *symbols.Module, args core.Args) (*rt.Object, *rt.Unwind) {
|
|
glog.Infof("Evaluating module '%v'", mod.Token())
|
|
if e.hooks != nil {
|
|
uw, leave := e.hooks.OnEnterModule(mod)
|
|
if uw != nil {
|
|
return nil, uw
|
|
} else if leave != nil {
|
|
defer leave()
|
|
}
|
|
}
|
|
|
|
if glog.V(2) {
|
|
defer glog.V(2).Infof("Evaluation of module '%v' completed w/ %v warnings and %v errors",
|
|
mod.Token(), e.Diag().Warnings(), e.Diag().Errors())
|
|
}
|
|
|
|
// Fetch the module's entrypoint function, erroring out if it doesn't have one.
|
|
var ret *rt.Object
|
|
var uw *rt.Unwind
|
|
hadEntry := false
|
|
if entry, has := mod.Members[tokens.EntryPointFunction]; has {
|
|
if entryfnc, ok := entry.(symbols.Function); ok {
|
|
ret, uw = e.EvaluateFunction(entryfnc, nil, args)
|
|
hadEntry = true
|
|
}
|
|
}
|
|
|
|
if !hadEntry {
|
|
e.Diag().Errorf(errors.ErrorModuleHasNoEntryPoint.At(mod.Tree()), mod.Name())
|
|
}
|
|
|
|
return ret, uw
|
|
}
|
|
|
|
// EvaluateFunction performs an evaluation of the given function, using the provided arguments.
|
|
func (e *evaluator) EvaluateFunction(fnc symbols.Function, this *rt.Object, args core.Args) (*rt.Object, *rt.Unwind) {
|
|
glog.Infof("Evaluating function '%v'", fnc.Token())
|
|
if glog.V(2) {
|
|
defer glog.V(2).Infof("Evaluation of function '%v' completed w/ %v warnings and %v errors",
|
|
fnc.Token(), e.Diag().Warnings(), e.Diag().Errors())
|
|
}
|
|
|
|
// Call the pre-start hook if registered.
|
|
if e.hooks != nil {
|
|
if uw := e.hooks.OnStart(); uw != nil {
|
|
return nil, uw
|
|
}
|
|
}
|
|
|
|
// Ensure all exit paths do the right thing (dumping, exceptions, hooks).
|
|
var uw *rt.Unwind
|
|
defer (func() {
|
|
// Dump the evaluation state at log-level 5, if it is enabled.
|
|
e.dumpEvalState(5)
|
|
|
|
// If the call had a throw unwind, then we have an unhandled exception.
|
|
if uw != nil && uw.Throw() {
|
|
e.UnhandledException(fnc.Tree(), uw.Thrown())
|
|
}
|
|
|
|
// Make sure to invoke the done hook.
|
|
if e.hooks != nil {
|
|
if doneuw := e.hooks.OnDone(uw); doneuw != nil {
|
|
uw = doneuw
|
|
}
|
|
}
|
|
})()
|
|
|
|
// Ensure that initializers have been run.
|
|
switch f := fnc.(type) {
|
|
case *symbols.ClassMethod:
|
|
if uw = e.ensureClassInit(f.Parent); uw != nil {
|
|
return nil, uw
|
|
}
|
|
case *symbols.ModuleMethod:
|
|
if uw = e.ensureModuleInit(f.Parent); uw != nil {
|
|
return nil, uw
|
|
}
|
|
default:
|
|
contract.Failf("Unrecognized function evaluation type: %v", reflect.TypeOf(f))
|
|
}
|
|
|
|
// First, validate any arguments, and turn them into real runtime *rt.Objects.
|
|
var argos []*rt.Object
|
|
params := fnc.Function().GetParameters()
|
|
if params == nil {
|
|
if len(args) != 0 {
|
|
e.Diag().Errorf(errors.ErrorFunctionArgMismatch.At(fnc.Tree()), 0, len(args))
|
|
}
|
|
} else {
|
|
if len(*params) != len(args) {
|
|
e.Diag().Errorf(errors.ErrorFunctionArgMismatch.At(fnc.Tree()), 0, len(args))
|
|
}
|
|
|
|
ptys := fnc.Signature().Parameters
|
|
found := make(map[tokens.Name]bool)
|
|
for i, param := range *params {
|
|
pname := param.Name.Ident
|
|
if arg, has := args[pname]; has {
|
|
found[pname] = true
|
|
argo := rt.NewConstantObject(arg)
|
|
if !types.CanConvert(argo.Type(), ptys[i]) {
|
|
e.Diag().Errorf(errors.ErrorFunctionArgIncorrectType.At(fnc.Tree()), ptys[i], argo.Type())
|
|
break
|
|
}
|
|
argos = append(argos, argo)
|
|
} else {
|
|
e.Diag().Errorf(errors.ErrorFunctionArgNotFound.At(fnc.Tree()), param.Name)
|
|
}
|
|
}
|
|
for arg := range args {
|
|
if !found[arg] {
|
|
e.Diag().Errorf(errors.ErrorFunctionArgUnknown.At(fnc.Tree()), arg)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the arguments bound correctly, actually perform the invocation.
|
|
var ret *rt.Object
|
|
if e.Diag().Success() {
|
|
ret, uw = e.evalCallSymbol(fnc.Tree(), fnc, this, argos...)
|
|
}
|
|
return ret, uw
|
|
}
|
|
|
|
// Utility functions
|
|
|
|
// dumpEvalState logs the evaluator's current state at the given log-level.
|
|
func (e *evaluator) dumpEvalState(v glog.Level) {
|
|
if glog.V(v) {
|
|
glog.V(v).Infof("Evaluator state dump:")
|
|
glog.V(v).Infof("=====================")
|
|
|
|
// Print all initialized modules in alphabetical order.
|
|
modtoks := make([]string, 0, len(e.modinits))
|
|
for mod := range e.modinits {
|
|
modtoks = append(modtoks, string(mod.Token()))
|
|
}
|
|
sort.Strings(modtoks)
|
|
for _, mod := range modtoks {
|
|
glog.V(v).Infof("Module init: %v", mod)
|
|
}
|
|
|
|
// Print all initialized classes in alphabetical order.
|
|
classtoks := make([]string, 0, len(e.classinits))
|
|
for class := range e.classinits {
|
|
classtoks = append(classtoks, string(class.Token()))
|
|
}
|
|
sort.Strings(classtoks)
|
|
for _, class := range classtoks {
|
|
glog.V(v).Infof("Class init: %v", class)
|
|
}
|
|
}
|
|
}
|
|
|
|
// initProperty initializes a property entry in the given map, using an optional `this` pointer for member functions.
|
|
// It returns the resulting pointer along with a boolean to indicate whether the property was left unfrozen.
|
|
func (e *evaluator) initProperty(this *rt.Object, properties *rt.PropertyMap,
|
|
key rt.PropertyKey, sym symbols.Symbol) (*rt.Pointer, bool) {
|
|
// First, ensure we've swapped in an intrinsic if available.
|
|
contract.Assert(sym != nil)
|
|
sym = MaybeIntrinsic(sym)
|
|
|
|
switch m := sym.(type) {
|
|
case symbols.Function:
|
|
// A function results in a closure object referring to `this`, if any.
|
|
obj := rt.NewFunctionObjectFromSymbol(m, this)
|
|
// TODO[pulumi/lumi#56]: all methods are readonly; consider permitting JS-style overwriting of them.
|
|
return properties.InitAddr(key, obj, true, nil, nil), false
|
|
case symbols.Variable:
|
|
// A variable could have a default object; if so, use that; otherwise, null will be substituted automatically.
|
|
var obj *rt.Object
|
|
if m.Default() != nil {
|
|
// If the variable has a default object, substitute it.
|
|
obj = rt.NewConstantObject(*m.Default())
|
|
} else if this != nil && m.Computed() {
|
|
// If the variable is a computed one -- that is, assignments may come from outside the system at a later
|
|
// date and we want to be able to speculate beyond it -- then stick a computed value in the slot.
|
|
obj = rt.NewComputedObject(m.Type(), false, []*rt.Object{this})
|
|
} else {
|
|
obj = rt.Null
|
|
}
|
|
var thist symbols.Type
|
|
if this != nil {
|
|
thist = this.Type()
|
|
}
|
|
glog.V(9).Infof("Initializing property '%v' (t=%v) for obj type '%v': %v", key, m.Type(), thist, obj)
|
|
|
|
// If this is a class property, it may have a getter and/or setter; flow them.
|
|
var get symbols.Function
|
|
var set symbols.Function
|
|
if prop, isprop := m.(*symbols.ClassProperty); isprop {
|
|
get = prop.Getter()
|
|
set = prop.Setter()
|
|
}
|
|
contract.Assertf(obj == nil || (get == nil && set == nil), "Inits, getters, and setters cannot be mixed")
|
|
|
|
// Finally allocate the slot and return the resulting pointer to that slot.
|
|
ptr := properties.InitAddr(key, obj, false, get, set)
|
|
return ptr, m.Readonly()
|
|
case *symbols.Module:
|
|
// A module resolves to its module object.
|
|
modobj := e.getModuleObject(m)
|
|
return properties.InitAddr(key, modobj, false, nil, nil), false
|
|
case *symbols.Class:
|
|
// A class resolves to its prototype object.
|
|
proto := e.getPrototypeObject(m)
|
|
return properties.InitAddr(key, proto, false, nil, nil), false
|
|
default:
|
|
contract.Failf("Unrecognized property '%v' symbol type: %v", key, reflect.TypeOf(sym))
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
// ensureClassInit ensures that the target's class initializer has been run.
|
|
func (e *evaluator) ensureClassInit(class *symbols.Class) *rt.Unwind {
|
|
already := e.classinits[class]
|
|
e.classinits[class] = true // set true before running, in case of cycles.
|
|
|
|
if !already {
|
|
// First ensure the module initializer has run.
|
|
if uw := e.ensureModuleInit(class.Parent); uw != nil {
|
|
return uw
|
|
}
|
|
|
|
// Next ensure that the base class's statics are initialized.
|
|
if class.Extends != nil {
|
|
if extends, isclass := class.Extends.(*symbols.Class); isclass {
|
|
e.ensureClassInit(extends)
|
|
}
|
|
}
|
|
|
|
// Now populate this class's statics with all of the static members.
|
|
var readonlines []*rt.Pointer
|
|
statics := e.getClassStatics(class)
|
|
var current symbols.Type = class
|
|
for current != nil {
|
|
members := current.TypeMembers()
|
|
for _, name := range symbols.StableClassMemberMap(members) {
|
|
if member := members[name]; member.Static() {
|
|
key := rt.PropertyKey(name)
|
|
if ptr, readonly := e.initProperty(nil, statics, key, member); readonly {
|
|
// Readonly properties are unfrozen during initialization; afterwards, they will be frozen.
|
|
readonlines = append(readonlines, ptr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keep going up the type hierarchy.
|
|
current = current.Base()
|
|
}
|
|
|
|
// Next, run the class if it has an initializer.
|
|
if init := class.GetInit(); init != nil {
|
|
glog.V(7).Infof("Initializing class: %v", class)
|
|
contract.Assert(len(init.Sig.Parameters) == 0)
|
|
contract.Assert(init.Sig.Return == nil)
|
|
ret, uw := e.evalCallSymbol(class.Tree(), init, nil)
|
|
if uw != nil {
|
|
return uw
|
|
}
|
|
contract.Assert(ret == nil)
|
|
} else {
|
|
glog.V(7).Infof("Class has no initializer: %v", class)
|
|
}
|
|
|
|
// Now, finally, ensure that all readonly class statics are frozen.
|
|
for _, readonly := range readonlines {
|
|
readonly.Freeze() // ensure this cannot be written post-initialization.
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ensureModuleInit ensures that the target's module initializer has been run. It also evaluates dependency module
|
|
// initializers, assuming they have been declared. If they have not, those will run when we access them.
|
|
func (e *evaluator) ensureModuleInit(mod *symbols.Module) *rt.Unwind {
|
|
already := e.modinits[mod]
|
|
e.modinits[mod] = true // set true before running, in case of cycles.
|
|
|
|
if !already {
|
|
// Populate all properties in this module, even if they will be empty for now.
|
|
var readonlines []*rt.Pointer
|
|
globals := e.getModuleGlobals(mod)
|
|
for _, name := range symbols.StableModuleMemberMap(mod.Members) {
|
|
key := rt.PropertyKey(name)
|
|
member := mod.Members[name]
|
|
if ptr, readonly := e.initProperty(nil, globals.Properties(), key, member); readonly {
|
|
// If this property was left unfrozen, be sure to remember it for freezing after we're done.
|
|
readonlines = append(readonlines, ptr)
|
|
}
|
|
}
|
|
|
|
// Next, run the module initializer if it has one.
|
|
if init := mod.GetInit(); init != nil {
|
|
glog.V(7).Infof("Initializing module: %v", mod)
|
|
contract.Assert(len(init.Sig.Parameters) == 0)
|
|
contract.Assert(init.Sig.Return == nil)
|
|
ret, uw := e.evalCallSymbol(mod.Tree(), init, nil)
|
|
if uw != nil {
|
|
return uw
|
|
}
|
|
contract.Assert(ret == nil)
|
|
} else {
|
|
glog.V(7).Infof("Module has no initializer: %v", mod)
|
|
}
|
|
|
|
// Ensure that all readonly module properties are frozen.
|
|
for _, readonly := range readonlines {
|
|
readonly.Freeze() // ensure this is never written to after initialization.
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getModuleGlobals returns a globals table for the given module, lazily initializing if needed.
|
|
func (e *evaluator) getModuleGlobals(module *symbols.Module) *rt.Object {
|
|
// If there's an existing globals object, return it.
|
|
if globals, has := e.modules[module]; has {
|
|
return globals
|
|
}
|
|
|
|
// Otherwise, we need to create a fresh module object. This is laid out in a very specific way to facilitate
|
|
// information hiding. There are two objects: 1) the super (prototype) object, containing the full set of exported
|
|
// variables, facilitating dynamic access; and 2) the child object, whose map contains all the globals, etc. that
|
|
// are not exported for use outside of the module itself. We access one or the other as appropriate.
|
|
|
|
// First, create the super object, with all of the exports.
|
|
proto := rt.NewObject(symbols.NewModuleType(module), nil, nil, nil)
|
|
props := proto.Properties()
|
|
for _, name := range symbols.StableModuleExportMap(module.Exports) {
|
|
key := rt.PropertyKey(name)
|
|
if props.GetAddr(key) == nil {
|
|
exp := module.Exports[name]
|
|
e.initProperty(proto, props, key, e.chaseExports(exp))
|
|
}
|
|
}
|
|
|
|
// Next, create the childmost object and link its parent to the proto.
|
|
globals := rt.NewObject(symbols.NewModuleType(module), nil, nil, proto)
|
|
|
|
// Memoize the result and return it.
|
|
e.modules[module] = globals
|
|
return globals
|
|
}
|
|
|
|
// getClassStatics returns a statics table for the given class, lazily initializing if needed.
|
|
func (e *evaluator) getClassStatics(class *symbols.Class) *rt.PropertyMap {
|
|
statics, has := e.statics[class]
|
|
if !has {
|
|
// TODO[pulumi/lumi#176]: merge the class statics representation with the prototype object representation.
|
|
statics = rt.NewPropertyMap()
|
|
e.statics[class] = statics
|
|
}
|
|
return statics
|
|
}
|
|
|
|
// getModuleObject returns a module object for the given module. This object contains all exported members through
|
|
// properties. This is a mutable object, and so is cached and reused for subsequent lookups.
|
|
func (e *evaluator) getModuleObject(m *symbols.Module) *rt.Object {
|
|
// To get the module object, simply fetch the globals. The super proto class of the globals will contain only the
|
|
// exported variables, which is suitable for returning to code on the import side.
|
|
globals := e.getModuleGlobals(m)
|
|
contract.Assert(globals != nil)
|
|
contract.Assert(globals.Proto() != nil)
|
|
return globals.Proto()
|
|
}
|
|
|
|
// getPrototypeObject returns the prototype for a given type. The prototype is a mutable object, and so it is cached,
|
|
// and reused for subsequent lookups. This means that mutations in the prototype are lasting and visible for all later
|
|
// uses. This is similar to ECMAScript behavior; see http://www.ecma-international.org/ecma-262/6.0/#sec-objects.
|
|
// TODO[pulumi/lumi#70]: technically this should be gotten from the constructor function object; we will need to
|
|
// rewire things a bit, depending on how serious we are about ECMAScript compliance, especially dynamic scenarios.
|
|
func (e *evaluator) getPrototypeObject(t symbols.Type) *rt.Object {
|
|
// If there is already a proto for this type, use it.
|
|
if proto, has := e.protos[t]; has {
|
|
return proto
|
|
}
|
|
|
|
// If not, we need to create a new one. First, fetch the base if there is one.
|
|
var base *rt.Object
|
|
if t.Base() != nil {
|
|
base = e.getPrototypeObject(t.Base())
|
|
}
|
|
|
|
// Now populate the prototype object with all members.
|
|
proto := rt.NewObject(symbols.NewPrototypeType(t), nil, nil, base)
|
|
e.addPrototypeObjectMembers(proto, t.TypeMembers(), true)
|
|
|
|
// For any interfaces implemented by the type, type also ensure to add these as though they were members.
|
|
if class, isclass := t.(*symbols.Class); isclass {
|
|
for _, impl := range class.Implements {
|
|
e.addPrototypeObjectInterfaceMembers(proto, impl)
|
|
}
|
|
}
|
|
|
|
e.protos[t] = proto
|
|
return proto
|
|
}
|
|
|
|
// addPrototypeObjectMembers adds a bag of members to a prototype (during initialization).
|
|
func (e *evaluator) addPrototypeObjectMembers(proto *rt.Object, members symbols.ClassMemberMap, must bool) {
|
|
properties := proto.Properties()
|
|
for _, name := range symbols.StableClassMemberMap(members) {
|
|
if member := members[name]; !member.Static() {
|
|
key := rt.PropertyKey(name)
|
|
if must || properties.GetAddr(key) == nil {
|
|
e.initProperty(proto, properties, key, member)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// addPrototypeObjectInterfaceMembers ensures that interface members exist on the prototype (during initialization).
|
|
func (e *evaluator) addPrototypeObjectInterfaceMembers(proto *rt.Object, impl symbols.Type) {
|
|
// Add members from this type, but only so long as they don't already exist.
|
|
e.addPrototypeObjectMembers(proto, impl.TypeMembers(), false)
|
|
|
|
// Now do the same for any base classes.
|
|
if base := impl.Base(); base != nil {
|
|
e.addPrototypeObjectInterfaceMembers(proto, base)
|
|
}
|
|
if class, isclass := impl.(*symbols.Class); isclass {
|
|
for _, classimpl := range class.Implements {
|
|
e.addPrototypeObjectInterfaceMembers(proto, classimpl)
|
|
}
|
|
}
|
|
}
|
|
|
|
// newObject allocates a fresh object of the given type, wired up to its prototype.
|
|
func (e *evaluator) newObject(t symbols.Type) *rt.Object {
|
|
// First, fetch the prototype chain for this object. This is required to implement property chaining.
|
|
proto := e.getPrototypeObject(t)
|
|
|
|
// Now create an empty object of the desired type. Subsequent operations will do the right thing with it. E.g.,
|
|
// overwriting a property will add a new entry to the object's map; reading will search the prototpe chain; etc.
|
|
return rt.NewObject(t, nil, nil, proto)
|
|
}
|
|
|
|
// issueUnhandledException issues an unhandled exception error using the given diagnostic and unwind information.
|
|
func (e *evaluator) UnhandledException(tree diag.Diagable, ex *rt.Exception) {
|
|
// Produce a message with the exception text plus stack trace.
|
|
msg := ex.Message(e.Diag())
|
|
|
|
// Now simply output the error with the message plus stack trace.
|
|
e.Diag().Errorf(errors.ErrorUnhandledException.At(tree), msg)
|
|
}
|
|
|
|
// rejectComputed checks an object's value and, if it's computed and its value isn't known, returns an exception unwind.
|
|
func (e *evaluator) rejectComputed(tree diag.Diagable, obj *rt.Object) *rt.Unwind {
|
|
if obj != nil && obj.Type().Computed() {
|
|
// TODO[pulumi/lumi#170]: support multi-stage planning that speculates beyond conditionals.
|
|
return e.NewUnexpectedComputedValueException(tree, obj)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pushModuleScope establishes a new module-wide scope. It returns a function that restores the prior value.
|
|
func (e *evaluator) pushModuleScope(m *symbols.Module) func() {
|
|
return e.ctx.PushModule(m)
|
|
}
|
|
|
|
// pushClassScope establishes a new class-wide scope. This also establishes the right module context. If the object
|
|
// argument is non-nil, instance methods are also populated. It returns a function that restores the prior value.
|
|
func (e *evaluator) pushClassScope(c *symbols.Class, obj *rt.Object) func() {
|
|
contract.Assert(obj == nil || types.CanConvert(obj.Type(), c))
|
|
return e.ctx.PushClass(c)
|
|
}
|
|
|
|
// pushScope pushes a new local and context scope. The activation 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 *rt.StackFrame, activation bool) {
|
|
if frame != nil {
|
|
frame.Parent = e.stack // remember the parent so we can pop.
|
|
e.stack = frame // install this as the current frame.
|
|
}
|
|
e.locals.Push(activation) // pushing the local scope also updates the context scope.
|
|
}
|
|
|
|
// popScope pops the current local and context scopes.
|
|
func (e *evaluator) popScope(frame *rt.StackFrame) {
|
|
if frame != nil {
|
|
contract.Assert(e.stack == frame)
|
|
e.stack = e.stack.Parent
|
|
}
|
|
e.locals.Pop() // popping the local scope also updates the context scope.
|
|
}
|
|
|
|
// Functions
|
|
|
|
func (e *evaluator) evalCall(node diag.Diagable,
|
|
fnc ast.Function, sym symbols.Function, sig *symbols.FunctionType, shareActivation bool,
|
|
this *rt.Object, args ...*rt.Object) (*rt.Object, *rt.Unwind) {
|
|
var label string
|
|
if sym == nil {
|
|
label = "lambda"
|
|
} else {
|
|
label = string(sym.Token())
|
|
}
|
|
glog.V(7).Infof("Evaluating call to fnc %v; this=%v args=%v", label, this != nil, len(args))
|
|
if glog.V(9) {
|
|
for i, arg := range args {
|
|
glog.V(9).Infof("Call argument #%v=%v", i, arg)
|
|
}
|
|
}
|
|
|
|
// First check the this pointer, since it might throw before the call even happens.
|
|
intrinsic := false
|
|
var thisVariable *symbols.LocalVariable
|
|
var superVariable *symbols.LocalVariable
|
|
if sym != nil {
|
|
for fsym, done := sym, false; !done; {
|
|
contract.Assert(fsym != nil)
|
|
// Set up the appropriate this/super variables, and also ensure that we enter the right module/class
|
|
// context (otherwise module-sensitive binding won't work).
|
|
switch f := fsym.(type) {
|
|
case *symbols.ClassMethod:
|
|
if f.Static() {
|
|
if this != nil {
|
|
// A non-nil `this` is okay if we loaded this function from a prototype object.
|
|
prototy, isproto := this.Type().(*symbols.PrototypeType)
|
|
contract.Assert(isproto)
|
|
contract.Assert(prototy.Type == f.Parent)
|
|
}
|
|
} else {
|
|
contract.Assertf(this != nil, "Expect non-nil this to invoke '%v'", f)
|
|
if uw := e.checkThis(node, this); uw != nil {
|
|
return nil, uw
|
|
}
|
|
thisVariable = f.Parent.This
|
|
superVariable = f.Parent.Super
|
|
}
|
|
|
|
popModule := e.pushModuleScope(f.Parent.Parent)
|
|
defer popModule()
|
|
popClass := e.pushClassScope(f.Parent, this)
|
|
defer popClass()
|
|
done = true
|
|
|
|
case *symbols.ModuleMethod:
|
|
if this != nil {
|
|
// A non-nil `this` is okay if we loaded this function from a module object. Because modules can
|
|
// re-export members from other modules, we cannot require that the type's parent matches.
|
|
_, ismod := this.Type().(*symbols.ModuleType)
|
|
contract.Assert(ismod)
|
|
this = nil // the this parameter isn't required during invocation.
|
|
}
|
|
|
|
popModule := e.pushModuleScope(f.Parent)
|
|
defer popModule()
|
|
done = true
|
|
|
|
case *rt.Intrinsic:
|
|
intrinsic = true
|
|
fsym = f.UnderlyingSymbol() // swap in the underlying symbol for purposes of this/super/scoping.
|
|
if fsym == nil { // Builtin intrinsics may not have an underlying symbol.
|
|
done = true
|
|
}
|
|
|
|
default:
|
|
contract.Failf("Unrecognized function type during call: %v", reflect.TypeOf(fsym))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save the prior func symbol, set the new one, and restore upon exit.
|
|
prior := sym
|
|
e.fnc = sym
|
|
defer func() { e.fnc = prior }()
|
|
|
|
// Set up a new lexical scope "activation frame" in which we can bind the parameters; restore it upon exit.
|
|
frame := &rt.StackFrame{Node: fnc, Func: sym, Caller: node}
|
|
e.pushScope(frame, !shareActivation)
|
|
defer e.popScope(frame)
|
|
|
|
// If the target is an instance method, the "this" and "super" variables must be bound to values.
|
|
if thisVariable != nil {
|
|
contract.Assert(this != nil)
|
|
e.locals.Register(thisVariable)
|
|
e.locals.InitValueAddr(thisVariable, rt.NewPointer(this, true, nil, nil))
|
|
}
|
|
if superVariable != nil {
|
|
contract.Assert(this != nil)
|
|
e.locals.Register(superVariable)
|
|
e.locals.InitValueAddr(superVariable, rt.NewPointer(this, true, nil, nil))
|
|
}
|
|
|
|
// Ensure that the arguments line up to the parameter slots and add them to the frame.
|
|
if fnc != nil {
|
|
params := fnc.GetParameters()
|
|
if params == nil {
|
|
contract.Assert(len(args) == 0)
|
|
} else {
|
|
contract.Assertf(len(args) == len(*params),
|
|
"Expected argc %v == paramc %v in call to '%v'", len(args), len(*params), label)
|
|
for i, param := range *params {
|
|
sym := e.ctx.RequireVariable(param).(*symbols.LocalVariable)
|
|
e.locals.Register(sym)
|
|
arg := args[i]
|
|
contract.Assertf(arg != nil, "Did not expect nil argument slot (#%v)", i)
|
|
contract.Assertf(types.CanConvert(arg.Type(), sym.Type()),
|
|
"Arg %v of type %v cannot convert to the expected type %v", arg.Type(), sym.Type())
|
|
e.locals.SetValue(sym, arg)
|
|
}
|
|
}
|
|
}
|
|
|
|
var uw *rt.Unwind
|
|
|
|
// Invoke the hooks if available.
|
|
if e.hooks != nil {
|
|
var leave func()
|
|
if uw, leave = e.hooks.OnEnterFunction(sym, args); leave != nil {
|
|
defer leave()
|
|
}
|
|
}
|
|
|
|
// Assuming the hook didn't perform its own logic, we can now perform the real invocation; for intrinsics, just run
|
|
// the code; for all others, interpret the body.
|
|
if uw == nil {
|
|
if intrinsic {
|
|
isym := sym.(*rt.Intrinsic)
|
|
invoker := GetIntrinsicInvoker(isym)
|
|
uw = invoker(isym, e, this, args)
|
|
} else {
|
|
uw = e.evalStatement(fnc.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.
|
|
retty := sig.Return
|
|
if uw != nil {
|
|
if uw.Throw() {
|
|
glog.V(7).Infof("Evaluated call to fnc %v (sym %v); unhandled exception: %v",
|
|
fnc, sym, uw.Thrown().Obj)
|
|
return nil, uw
|
|
} else if uw.Cancel() {
|
|
glog.V(7).Infof("Evaluated call to fnc %v (sym %v) ended in cancellation", fnc, sym)
|
|
return nil, uw
|
|
}
|
|
|
|
contract.Assert(uw.Return()) // break/continue not expected.
|
|
ret := uw.Returned()
|
|
contract.Assert((retty == nil) == (ret == nil))
|
|
contract.Assert(ret == nil || types.CanConvert(ret.Type(), retty))
|
|
if glog.V(7) {
|
|
glog.V(7).Infof("Evaluated call to fnc %v; return=%v", label, ret)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// An absence of a return is okay for void- or dynamic-returning functions.
|
|
contract.Assert(retty == nil || retty == types.Dynamic)
|
|
glog.V(7).Infof("Evaluated call to fnc %v; return=<nil>", label)
|
|
return nil, nil
|
|
}
|
|
|
|
func (e *evaluator) evalCallSymbol(node diag.Diagable, fnc symbols.Function,
|
|
this *rt.Object, args ...*rt.Object) (*rt.Object, *rt.Unwind) {
|
|
return e.evalCall(node, fnc.Function(), fnc, fnc.Signature(), false, this, args...)
|
|
}
|
|
|
|
func (e *evaluator) evalCallFuncStub(node diag.Diagable,
|
|
fnc rt.FuncStub, args ...*rt.Object) (*rt.Object, *rt.Unwind) {
|
|
// If there is an environment for the call stub, restore it before invoking and then put it back.
|
|
var shareActivation bool
|
|
if fnc.Env != nil {
|
|
restore := e.locals.Swap(fnc.Env)
|
|
defer restore()
|
|
shareActivation = true
|
|
}
|
|
return e.evalCall(node, fnc.Func, fnc.Sym, fnc.Sig, shareActivation, fnc.This, args...)
|
|
}
|
|
|
|
// Statements
|
|
|
|
func (e *evaluator) evalStatement(node ast.Statement) *rt.Unwind {
|
|
contract.Assert(node != nil)
|
|
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 rt.Unwind info.
|
|
switch n := node.(type) {
|
|
case *ast.Import:
|
|
return e.evalImport(n)
|
|
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.SwitchStatement:
|
|
return e.evalSwitchStatement(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.ForStatement:
|
|
return e.evalForStatement(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) evalImport(node *ast.Import) *rt.Unwind {
|
|
// Ensure the target module has been initialized.
|
|
imptok := node.Referent
|
|
sym := e.ctx.LookupSymbol(imptok, imptok.Tok, true)
|
|
contract.Assert(sym != nil)
|
|
|
|
var mod *symbols.Module
|
|
switch s := sym.(type) {
|
|
case *symbols.Module:
|
|
mod = s
|
|
case symbols.ModuleMember:
|
|
mod = s.MemberParent()
|
|
default:
|
|
contract.Failf("Unrecognized import symbol: %v", reflect.TypeOf(sym))
|
|
}
|
|
if uw := e.ensureModuleInit(mod); uw != nil {
|
|
return uw
|
|
}
|
|
|
|
// If a name was requested, bind it dynamically to an object with this import's exports.
|
|
// TODO[pulumi/lumi#176]: a more elegant way to do this might be to bind it statically, however that's complex
|
|
// because then it would potentially require a module property versus local variable distinction.
|
|
if node.Name != nil {
|
|
key := node.Name.Ident
|
|
addr := e.getDynamicNameAddr(key, true)
|
|
modobj := e.getModuleObject(mod)
|
|
addr.Set(modobj)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *evaluator) evalBlock(node *ast.Block) *rt.Unwind {
|
|
// Push a scope at the start, and pop it at afterwards; both for the symbol context and local variable values.
|
|
e.pushScope(nil, false)
|
|
defer e.popScope(nil)
|
|
|
|
for _, stmt := range node.Statements {
|
|
if uw := e.evalStatement(stmt); uw != nil {
|
|
return uw
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *evaluator) evalLocalVariableDeclaration(node *ast.LocalVariableDeclaration) *rt.Unwind {
|
|
// Populate the variable in the scope.
|
|
loc := node.Local
|
|
sym := e.ctx.RequireVariable(loc).(*symbols.LocalVariable)
|
|
e.locals.Register(sym)
|
|
|
|
// If there is a default value, set it now.
|
|
if loc.Default != nil {
|
|
obj := rt.NewConstantObject(*loc.Default)
|
|
e.locals.SetValue(sym, obj)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *evaluator) evalTryCatchFinally(node *ast.TryCatchFinally) *rt.Unwind {
|
|
// First, execute the try part.
|
|
uw := e.evalStatement(node.TryClause)
|
|
if uw != nil && uw.Throw() {
|
|
// The try block threw something; see if there is a handler that covers this.
|
|
thrown := uw.Thrown().Obj
|
|
if node.CatchClauses != nil {
|
|
for _, catch := range *node.CatchClauses {
|
|
ex := e.ctx.RequireVariable(catch.Exception).(*symbols.LocalVariable)
|
|
exty := ex.Type()
|
|
if types.CanConvert(thrown.Type(), exty) {
|
|
// This type matched, so this handler will catch the exception. Set the exception variable,
|
|
// evaluate the block, and swap the rt.Unwind information ("handling" the in-flight exception).
|
|
e.pushScope(nil, false)
|
|
e.locals.SetValue(ex, thrown)
|
|
uw = e.evalStatement(catch.Body)
|
|
e.popScope(nil)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// No matter the rt.Unwind instructions, be sure to invoke the finally part.
|
|
if node.FinallyClause != nil {
|
|
uwf := e.evalStatement(*node.FinallyClause)
|
|
|
|
// Any rt.Unwind information from the finally block overrides the try rt.Unwind that was in flight.
|
|
if uwf != nil {
|
|
uw = uwf
|
|
}
|
|
}
|
|
|
|
return uw
|
|
}
|
|
|
|
func (e *evaluator) evalBreakStatement(node *ast.BreakStatement) *rt.Unwind {
|
|
var label *tokens.Name
|
|
if node.Label != nil {
|
|
label = &node.Label.Ident
|
|
}
|
|
return rt.NewBreakUnwind(label)
|
|
}
|
|
|
|
func (e *evaluator) evalContinueStatement(node *ast.ContinueStatement) *rt.Unwind {
|
|
var label *tokens.Name
|
|
if node.Label != nil {
|
|
label = &node.Label.Ident
|
|
}
|
|
return rt.NewContinueUnwind(label)
|
|
}
|
|
|
|
func (e *evaluator) evalIfStatement(node *ast.IfStatement) *rt.Unwind {
|
|
// Evaluate the branches explicitly based on the result of the condition node.
|
|
cond, uw := e.requireExpressionValue(node.Condition)
|
|
if uw != nil {
|
|
return uw
|
|
}
|
|
if cond.BoolValue() {
|
|
return e.evalStatement(node.Consequent)
|
|
} else if node.Alternate != nil {
|
|
return e.evalStatement(*node.Alternate)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *evaluator) evalSwitchStatement(node *ast.SwitchStatement) *rt.Unwind {
|
|
// First evaluate the expression we are switching on.
|
|
expr, uw := e.evalExpression(node.Expression)
|
|
if uw != nil {
|
|
return uw
|
|
}
|
|
|
|
// Next try to find a match; do this by walking all cases, in order, and checking for strict equality.
|
|
fallen := false
|
|
for _, caseNode := range node.Cases {
|
|
var match bool
|
|
if fallen {
|
|
// A fallthrough automatically executes the body without evaluating the clause.
|
|
match = true
|
|
} else if caseNode.Clause == nil {
|
|
// A default style clause always matches.
|
|
match = true
|
|
} else {
|
|
// Otherwise, evaluate the expression, and check for equality.
|
|
clause, uw2 := e.evalExpression(*caseNode.Clause)
|
|
if uw2 != nil {
|
|
return uw2
|
|
}
|
|
match = e.evalBinaryOperatorEquals(expr, clause)
|
|
}
|
|
|
|
// If we got a match, execute the clause.
|
|
if match {
|
|
if uw = e.evalStatement(caseNode.Consequent); uw != nil {
|
|
if uw.Break() && uw.Label() == nil {
|
|
// A simple break from this case.
|
|
break
|
|
} else {
|
|
// Anything else, get out of dodge.
|
|
return uw
|
|
}
|
|
}
|
|
|
|
// If we didn't encounter a break, we will fall through to the next case.
|
|
fallen = true
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *evaluator) evalLabeledStatement(node *ast.LabeledStatement) *rt.Unwind {
|
|
// Evaluate the underlying statement; if it is breaking or continuing to this label, stop the rt.Unwind.
|
|
uw := e.evalStatement(node.Statement)
|
|
if uw != nil && uw.Label() != nil && *uw.Label() == node.Label.Ident {
|
|
contract.Assert(uw.Continue() || uw.Break())
|
|
// TODO[pulumi/lumi#214]: perform correct break/continue behavior when the label is affixed to a loop.
|
|
uw = nil
|
|
}
|
|
return uw
|
|
}
|
|
|
|
func (e *evaluator) evalReturnStatement(node *ast.ReturnStatement) *rt.Unwind {
|
|
var ret *rt.Object
|
|
if node.Expression != nil {
|
|
var uw *rt.Unwind
|
|
if ret, uw = e.evalExpression(*node.Expression); uw != nil {
|
|
// If the expression caused an rt.Unwind, propagate that and ignore the returned object.
|
|
return uw
|
|
}
|
|
}
|
|
return rt.NewReturnUnwind(ret)
|
|
}
|
|
|
|
func (e *evaluator) evalThrowStatement(node *ast.ThrowStatement) *rt.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 rt.NewThrowUnwind(thrown, node, e.stack)
|
|
}
|
|
|
|
func (e *evaluator) evalLoop(condition *ast.Expression, body ast.Statement, post *ast.Statement) *rt.Unwind {
|
|
// So long as the condition evaluates to true, keep on visiting the body.
|
|
for {
|
|
var test *rt.Object
|
|
if condition != nil {
|
|
var uw *rt.Unwind
|
|
if test, uw = e.requireExpressionValue(*condition); uw != nil {
|
|
return uw
|
|
}
|
|
}
|
|
if test == nil || test.BoolValue() {
|
|
if uw := e.evalStatement(body); uw != nil {
|
|
if uw.Continue() {
|
|
contract.Assertf(uw.Label() == nil, "Labeled continue not yet supported")
|
|
continue
|
|
} else if uw.Break() {
|
|
contract.Assertf(uw.Label() == nil, "Labeled break not yet supported")
|
|
break
|
|
} else {
|
|
// If it's not a continue or break, return it right away.
|
|
return uw
|
|
}
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
|
|
// Before looping around again, run the post statement, if there is one.
|
|
if post != nil {
|
|
if uw := e.evalStatement(*post); uw != nil {
|
|
return uw
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *evaluator) evalWhileStatement(node *ast.WhileStatement) *rt.Unwind {
|
|
// Just run the loop.
|
|
return e.evalLoop(node.Condition, node.Body, nil)
|
|
}
|
|
|
|
func (e *evaluator) evalForStatement(node *ast.ForStatement) *rt.Unwind {
|
|
// Now run the initialization code.
|
|
if node.Init != nil {
|
|
if uw := e.evalStatement(*node.Init); uw != nil {
|
|
return uw
|
|
}
|
|
}
|
|
|
|
// Now actually run the loop and post logic.
|
|
return e.evalLoop(node.Condition, node.Body, node.Post)
|
|
}
|
|
|
|
func (e *evaluator) evalMultiStatement(node *ast.MultiStatement) *rt.Unwind {
|
|
for _, stmt := range node.Statements {
|
|
if uw := e.evalStatement(stmt); uw != nil {
|
|
return uw
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *evaluator) evalExpressionStatement(node *ast.ExpressionStatement) *rt.Unwind {
|
|
// Just evaluate the expression, drop its object on the floor, and propagate its rt.Unwind information.
|
|
_, uw := e.evalExpression(node.Expression)
|
|
return uw
|
|
}
|
|
|
|
// Expressions
|
|
|
|
func (e *evaluator) evalExpression(node ast.Expression) (*rt.Object, *rt.Unwind) {
|
|
contract.Assert(node != nil)
|
|
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 rt.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.TryLoadDynamicExpression:
|
|
return e.evalTryLoadDynamicExpression(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
|
|
}
|
|
}
|
|
|
|
// requireExpressionValue evaluates an expression and ensures that it isn't latent; that is, it has a concrete value.
|
|
// If it is latent, the function returns a non-nil exception unwind object.
|
|
func (e *evaluator) requireExpressionValue(node ast.Expression) (*rt.Object, *rt.Unwind) {
|
|
// TODO[pulumi/lumi#170]: eventually we should audit all uses of this routine and, for most if not all of them,
|
|
// make them work. Doing so requires a fair bit of machinery around stepwise application of deployments.
|
|
obj, uw := e.evalExpression(node)
|
|
if uw != nil {
|
|
return nil, uw
|
|
} else if uw = e.rejectComputed(node, obj); uw != nil {
|
|
return nil, uw
|
|
}
|
|
return obj, nil
|
|
}
|
|
|
|
// evalLValueExpression evaluates an expression for use as an l-value; in particular, this loads the target as a
|
|
// pointer/reference object, rather than as an ordinary value, so that it can be used in an assignment. This is only
|
|
// valid on the subset of AST nodes that are legal l-values (very few of them, it turns out).
|
|
func (e *evaluator) evalLValueExpression(node ast.Expression) (*Location, *rt.Unwind) {
|
|
switch n := node.(type) {
|
|
case *ast.LoadLocationExpression:
|
|
return e.evalLoadLocation(n, true)
|
|
case *ast.LoadDynamicExpression:
|
|
return e.evalLoadDynamic(n, true)
|
|
case *ast.TryLoadDynamicExpression:
|
|
return e.evalTryLoadDynamic(n, true)
|
|
case *ast.UnaryOperatorExpression:
|
|
contract.Assert(n.Operator == ast.OpDereference)
|
|
obj, uw := e.evalUnaryOperatorExpressionFor(n, true)
|
|
return &Location{e: e, obj: obj}, uw
|
|
default:
|
|
contract.Failf("Unrecognized l-value expression type: %v", node.GetKind())
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
func (e *evaluator) evalNullLiteral(node *ast.NullLiteral) (*rt.Object, *rt.Unwind) {
|
|
contract.Ignore(node) // no need for the node, all nulls are the same.
|
|
return rt.Null, nil
|
|
}
|
|
|
|
func (e *evaluator) evalBoolLiteral(node *ast.BoolLiteral) (*rt.Object, *rt.Unwind) {
|
|
return rt.Bools[node.Value], nil
|
|
}
|
|
|
|
func (e *evaluator) evalNumberLiteral(node *ast.NumberLiteral) (*rt.Object, *rt.Unwind) {
|
|
return rt.NewNumberObject(node.Value), nil
|
|
}
|
|
|
|
func (e *evaluator) evalStringLiteral(node *ast.StringLiteral) (*rt.Object, *rt.Unwind) {
|
|
return rt.NewStringObject(node.Value), nil
|
|
}
|
|
|
|
func (e *evaluator) evalArrayLiteral(node *ast.ArrayLiteral) (*rt.Object, *rt.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 []*rt.Pointer
|
|
|
|
// 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
|
|
} else if sze.Type().Computed() {
|
|
// If the array size isn't known, then we will propagate a latent value in its place.
|
|
return rt.NewComputedObject(ty, true, sze.ComputedValue().Sources), nil
|
|
}
|
|
sz := int(sze.NumberValue())
|
|
if sz < 0 {
|
|
// If the size is less than zero, raise a new error.
|
|
return nil, e.NewNegativeArrayLengthException(*node.Size)
|
|
}
|
|
arr = make([]*rt.Pointer, sz)
|
|
}
|
|
|
|
// Allocate a new array object.
|
|
arrobj := rt.NewArrayObject(ty.Element, &arr)
|
|
|
|
// 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([]*rt.Pointer, 0, len(*node.Elements))
|
|
} else if len(*node.Elements) > *sz {
|
|
// The element count exceeds the size; raise an error.
|
|
return nil, e.NewIncorrectArrayElementCountException(node, *sz, len(*node.Elements))
|
|
}
|
|
|
|
for i, elem := range *node.Elements {
|
|
elemobj, uw := e.evalExpression(elem)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
elemptr := rt.NewPointer(elemobj, false, nil, nil)
|
|
if sz == nil {
|
|
arr = append(arr, elemptr)
|
|
} else {
|
|
arr[i] = elemptr
|
|
}
|
|
}
|
|
}
|
|
|
|
return arrobj, nil
|
|
}
|
|
|
|
func (e *evaluator) evalObjectLiteral(node *ast.ObjectLiteral) (*rt.Object, *rt.Unwind) {
|
|
ty := e.ctx.Types[node]
|
|
|
|
// Allocate a new object of the right type, containing all of the properties pre-populated.
|
|
obj := e.newObject(ty)
|
|
|
|
if node.Properties != nil {
|
|
// The binder already checked that the properties are legal, so we will simply store them as values.
|
|
for _, init := range *node.Properties {
|
|
// For dynamic types, we simply store the values in the bag of properties. For all other types, we actually
|
|
// require that the token be a class member token that references a valid property. Note that we evaluate
|
|
// the LHS before the RHS, so that evaluation order for computed properties is correct.
|
|
var addr *rt.Pointer
|
|
var property rt.PropertyKey
|
|
switch p := init.(type) {
|
|
case *ast.ObjectLiteralNamedProperty:
|
|
id := p.Property.Tok
|
|
if ty == types.Dynamic {
|
|
property = rt.PropertyKey(id)
|
|
addr = obj.GetPropertyAddr(property, true, true)
|
|
} else {
|
|
contract.Assert(id.HasClassMember())
|
|
member := tokens.ClassMember(id).Name()
|
|
property = rt.PropertyKey(member.Name())
|
|
addr = obj.GetPropertyAddr(property, true, true)
|
|
}
|
|
case *ast.ObjectLiteralComputedProperty:
|
|
name, uw := e.evalExpression(p.Property)
|
|
if uw != nil {
|
|
return nil, uw
|
|
} else if name.Type().Computed() {
|
|
// If we are setting a property on the object, and that property's name is dynamically computed
|
|
// using a string whose value is not known, then we can't let the object be seen in a concrete
|
|
// state. Doing so would mean we can't assure latent propagation to operations on such properties.
|
|
return rt.NewComputedObject(ty, true, name.ComputedValue().Sources), nil
|
|
}
|
|
property = rt.PropertyKey(name.StringValue())
|
|
addr = obj.GetPropertyAddr(property, true, true)
|
|
}
|
|
|
|
// Evaluate and set the property.
|
|
val, uw := e.evalExpression(init.Val())
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
contract.Assert(val != nil)
|
|
addr.Set(val)
|
|
}
|
|
}
|
|
|
|
// Ensure we freeze anything that must be frozen.
|
|
obj.FreezeReadonlyProperties()
|
|
|
|
return obj, nil
|
|
}
|
|
|
|
// getObjectOrSuperProperty loads a property pointer from an object using the given property key. It understands how
|
|
// to determine whether this is a `super` load, and bind it, and will adjust the resulting pointer accordingly.
|
|
func (e *evaluator) getObjectOrSuperProperty(
|
|
obj *rt.Object, objexpr ast.Expression, k rt.PropertyKey, init bool, forWrite bool) (*rt.Pointer, *rt.Unwind) {
|
|
// Ensure the object's class has been initialized.
|
|
if obj != nil {
|
|
if class, isclass := obj.Type().(*symbols.Class); isclass {
|
|
if uw := e.ensureClassInit(class); uw != nil {
|
|
return nil, uw
|
|
}
|
|
}
|
|
}
|
|
|
|
// If this member is being accessed using "super", we need to start our property search from the
|
|
// superclass prototype, and not the object itself, so that we find the right value.
|
|
var super *rt.Object
|
|
if objexpr != nil {
|
|
if ldloc, isldloc := objexpr.(*ast.LoadLocationExpression); isldloc {
|
|
if ldloc.Name.Tok == tokens.Token(tokens.SuperVariable) {
|
|
contract.Assert(ldloc.Object == nil)
|
|
// The super expression's type will resolve to the parent class; use that for lookups.
|
|
super = e.getPrototypeObject(e.ctx.RequireType(objexpr))
|
|
}
|
|
}
|
|
}
|
|
|
|
// If a superclass, use the right prototype.
|
|
if super != nil {
|
|
// Make sure to call GetPropertyAddrForThis to ensure the this parameter is adjusted to the object.
|
|
return super.GetPropertyAddrForThis(obj, k, init, forWrite), nil
|
|
}
|
|
|
|
// Otherwise, simply fetch the property from the object directly.
|
|
return obj.GetPropertyAddr(k, init, forWrite), nil
|
|
}
|
|
|
|
func (e *evaluator) evalLoadLocationExpression(node *ast.LoadLocationExpression) (*rt.Object, *rt.Unwind) {
|
|
loc, uw := e.evalLoadLocation(node, false)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
return loc.Read(node)
|
|
}
|
|
|
|
func (e *evaluator) newLocation(node diag.Diagable, sym symbols.Symbol,
|
|
pv *rt.Pointer, ty symbols.Type, this *rt.Object, lval bool) *Location {
|
|
// If this is an l-value, return a pointer to the object; otherwise, return the raw object.
|
|
var obj *rt.Object
|
|
if lval {
|
|
obj = rt.NewPointerObject(ty, pv)
|
|
} else {
|
|
obj = pv.Obj()
|
|
}
|
|
contract.Assert(obj != nil)
|
|
|
|
if glog.V(9) {
|
|
var thist symbols.Type
|
|
if this != nil {
|
|
thist = this.Type()
|
|
}
|
|
glog.V(9).Infof("Loaded location of type '%v' from symbol '%v': lval=%v current=%v this=%v",
|
|
ty.Token(), sym.Token(), lval, pv.Obj(), thist)
|
|
}
|
|
|
|
return &Location{
|
|
e: e,
|
|
this: this,
|
|
name: sym.Name(),
|
|
lval: lval,
|
|
obj: obj,
|
|
getter: pv.Getter(),
|
|
setter: pv.Setter(),
|
|
}
|
|
}
|
|
|
|
type Location struct {
|
|
e *evaluator // the evaluator that produced this location.
|
|
this *rt.Object // the target object, if any.
|
|
name tokens.Name // the simple name of the variable.
|
|
lval bool // whether the result is an lval.
|
|
obj *rt.Object // the resulting object (pointer if lval, object otherwise).
|
|
getter symbols.Function // the getter function, if any.
|
|
setter symbols.Function // the setter function, if any.
|
|
}
|
|
|
|
func (loc *Location) Get(node diag.Diagable) (*rt.Object, *rt.Unwind) {
|
|
if loc.getter != nil {
|
|
// If there is a getter, invoke it.
|
|
contract.Assert(loc.this != nil)
|
|
return loc.e.evalCallSymbol(node, loc.getter, loc.this)
|
|
}
|
|
|
|
// Otherwise, just return the object directly.
|
|
return loc.obj, nil
|
|
}
|
|
|
|
func (loc *Location) Set(node diag.Diagable, val *rt.Object) *rt.Unwind {
|
|
if loc.setter != nil {
|
|
// If the location has a setter, use that for the assignment.
|
|
contract.Assert(loc.this != nil)
|
|
if _, uw := loc.e.evalCallSymbol(node, loc.setter, loc.this, val); uw != nil {
|
|
return uw
|
|
}
|
|
} else {
|
|
// Otherwise, perform a straightforward assignment, invoking the variable assignment if necessary.
|
|
ptr := loc.obj.PointerValue()
|
|
if ptr.Readonly() {
|
|
// If the pointer is readonly, however, we will disallow the assignment.
|
|
loc.e.Diag().Errorf(errors.ErrorIllegalReadonlyLValue.At(node))
|
|
} else {
|
|
ptr.Set(val)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (loc *Location) Read(node diag.Diagable) (*rt.Object, *rt.Unwind) {
|
|
if loc.getter != nil {
|
|
// If the location has a getter, use that for the assignment.
|
|
contract.Assert(loc.this != nil)
|
|
return loc.e.evalCallSymbol(node, loc.getter, loc.this)
|
|
}
|
|
// Otherwise, just return the object directly.
|
|
contract.Assertf(loc.obj != nil, "Unexpected nil object from location ready by %v", node)
|
|
return loc.obj, nil
|
|
}
|
|
|
|
// 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, *rt.Unwind) {
|
|
// If there's a target object, evaluate it.
|
|
var this *rt.Object
|
|
var thisexpr ast.Expression
|
|
if node.Object != nil {
|
|
thisexpr = *node.Object
|
|
var uw *rt.Unwind
|
|
if this, uw = e.evalExpression(thisexpr); uw != nil {
|
|
return nil, uw
|
|
}
|
|
}
|
|
|
|
// Create a pointer to the target location.
|
|
var pv *rt.Pointer
|
|
var ty symbols.Type
|
|
var sym symbols.Symbol
|
|
tok := node.Name.Tok
|
|
if tok.Simple() {
|
|
// If there is no object and the name is simple, it refers to a local variable in the current scope.
|
|
// For more "sophisticated" lookups, in the hierarchy of scopes, a load dynamic operation must be utilized.
|
|
contract.Assert(this == nil)
|
|
loc := e.locals.Lookup(tok.Name())
|
|
contract.Assert(loc != nil)
|
|
pv = e.locals.GetValueAddr(loc, true)
|
|
ty = loc.Type()
|
|
sym = loc
|
|
} else {
|
|
sym = e.ctx.LookupSymbol(node.Name, tok, false)
|
|
var uw *rt.Unwind
|
|
pv, ty, uw = e.evalLoadSymbolLocation(node, sym, this, thisexpr, lval)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
}
|
|
|
|
contract.Assert(pv != nil)
|
|
contract.Assert(pv.Obj() != nil)
|
|
contract.Assert(ty != nil)
|
|
contract.Assert(sym != nil)
|
|
return e.newLocation(node, sym, pv, ty, this, lval), nil
|
|
}
|
|
|
|
// chaseExports walks the referent chain of an export until it hits a real symbol, and then returns that.
|
|
func (e *evaluator) chaseExports(sym symbols.Symbol) symbols.Symbol {
|
|
for {
|
|
export, isexport := sym.(*symbols.Export)
|
|
if !isexport {
|
|
break
|
|
}
|
|
// Simply chase the referent symbol until we bottom out on something useful.
|
|
contract.Assertf(export.Referent != sym, "Unexpected self-referential export token")
|
|
sym = export.Referent
|
|
contract.Assertf(sym != nil, "Expected export '%v' to resolve to a token", export.Node.Referent.Tok)
|
|
}
|
|
return sym
|
|
}
|
|
|
|
func (e *evaluator) evalLoadSymbolLocation(node diag.Diagable, sym symbols.Symbol,
|
|
this *rt.Object, thisexpr ast.Expression, lval bool) (*rt.Pointer, symbols.Type, *rt.Unwind) {
|
|
contract.Assert(sym != nil) // don't issue errors; we shouldn't ever get here if verification failed.
|
|
sym = e.chaseExports(sym) // chase the export down until we get a real symbol.
|
|
|
|
// Look up the symbol property in the right place. Note that because this is a static load, we intentionally
|
|
// do not perform any lazily initialization of missing property slots; they must exist. But we still need to
|
|
// load from the object in case one of the properties was overwritten. The sole exception is for `dynamic`.
|
|
var pv *rt.Pointer
|
|
var ty symbols.Type
|
|
switch s := sym.(type) {
|
|
case symbols.ClassMember:
|
|
// Consult either the statics map or the object's property based on the kind of symbol. Note that we do
|
|
// this even for class functions so that in case they are replaced or overridden in derived types, we get
|
|
// the expected "virtual" dispatch behavior. The one special case is constructors, where we intentionally
|
|
// return a statically resolved symbol (since they aren't stored as properties and to support `super`).
|
|
k := rt.PropertyKey(sym.Name())
|
|
if s.Static() {
|
|
contract.Assert(this == nil)
|
|
// Ensure the class is initialized.
|
|
class := s.MemberParent()
|
|
if uw := e.ensureClassInit(class); uw != nil {
|
|
return nil, nil, uw
|
|
}
|
|
|
|
// Now fetch the statics and lookup the property.
|
|
statics := e.getClassStatics(class)
|
|
pv = statics.GetAddr(k)
|
|
contract.Assertf(pv != nil, "Missing static class member '%v'", s.Token())
|
|
contract.Assertf(pv.Getter() == nil, "Static class property getters unexpected (%v)", s)
|
|
contract.Assertf(pv.Setter() == nil, "Static class property setters unexpected (%v)", s)
|
|
} else {
|
|
contract.Assert(this != nil)
|
|
if uw := e.checkThis(node, this); uw != nil {
|
|
return nil, nil, uw
|
|
}
|
|
dynload := this.Type() == types.Dynamic
|
|
var uw *rt.Unwind
|
|
if pv, uw = e.getObjectOrSuperProperty(this, thisexpr, k, dynload, lval); uw != nil {
|
|
return nil, nil, uw
|
|
}
|
|
contract.Assertf(pv != nil, "Missing instance class member '%v'", s.Token())
|
|
}
|
|
ty = s.Type()
|
|
case symbols.ModuleMemberProperty:
|
|
module := s.MemberParent()
|
|
|
|
// Ensure this module has been initialized.
|
|
if uw := e.ensureModuleInit(module); uw != nil {
|
|
return nil, nil, uw
|
|
}
|
|
|
|
// Search the globals table for this module's members.
|
|
contract.Assert(this == nil)
|
|
k := rt.PropertyKey(s.Name())
|
|
globals := e.getModuleGlobals(module)
|
|
pv = globals.Properties().GetAddr(k)
|
|
contract.Assertf(pv != nil, "Missing module member '%v'", s.Token())
|
|
contract.Assertf(pv.Getter() == nil, "Module property getters unexpected (%v)", s)
|
|
contract.Assertf(pv.Setter() == nil, "Module property setters unexpected (%v)", s)
|
|
ty = s.MemberType()
|
|
default:
|
|
contract.Failf("Unexpected symbol token '%v' kind during load expression: %v",
|
|
sym.Token(), reflect.TypeOf(sym))
|
|
}
|
|
contract.Assert(pv != nil)
|
|
contract.Assert(ty != nil)
|
|
|
|
return pv, ty, nil
|
|
}
|
|
|
|
func (e *evaluator) LoadLocation(tree diag.Diagable, sym symbols.Symbol, this *rt.Object,
|
|
lval bool) (*Location, *rt.Unwind) {
|
|
pv, ty, uw := e.evalLoadSymbolLocation(tree, sym, this, nil, lval)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
return e.newLocation(tree, sym, pv, ty, this, lval), 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) *rt.Unwind {
|
|
contract.Assert(this != nil) // binder should catch cases where this isn't true
|
|
if this.Type() == types.Null {
|
|
return e.NewNullObjectException(node)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *evaluator) evalLoadDynamicExpression(node *ast.LoadDynamicExpression) (*rt.Object, *rt.Unwind) {
|
|
loc, uw := e.evalLoadDynamic(node, false)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
return loc.Read(node)
|
|
}
|
|
|
|
func (e *evaluator) evalLoadDynamic(node *ast.LoadDynamicExpression, lval bool) (*Location, *rt.Unwind) {
|
|
return e.evalLoadDynamicCore(node, node.Object, node.Name, false, lval)
|
|
}
|
|
|
|
func (e *evaluator) evalTryLoadDynamicExpression(node *ast.TryLoadDynamicExpression) (*rt.Object, *rt.Unwind) {
|
|
loc, uw := e.evalTryLoadDynamic(node, false)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
return loc.Read(node)
|
|
}
|
|
|
|
func (e *evaluator) evalTryLoadDynamic(node *ast.TryLoadDynamicExpression, lval bool) (*Location, *rt.Unwind) {
|
|
return e.evalLoadDynamicCore(node, node.Object, node.Name, true, lval)
|
|
}
|
|
|
|
func (e *evaluator) evalLoadDynamicCore(node ast.Node, objexpr *ast.Expression, nameexpr ast.Expression,
|
|
try bool, lval bool) (*Location, *rt.Unwind) {
|
|
// Evaluate the object and then the property expression.
|
|
var uw *rt.Unwind
|
|
var this *rt.Object
|
|
var thisexpr ast.Expression
|
|
if objexpr != nil {
|
|
thisexpr = *objexpr
|
|
if this, uw = e.evalExpression(thisexpr); uw != nil {
|
|
return nil, uw
|
|
}
|
|
|
|
// Check that the object isn't null; if it is, raise an exception.
|
|
if uw = e.checkThis(node, this); uw != nil {
|
|
return nil, uw
|
|
}
|
|
|
|
}
|
|
var name *rt.Object
|
|
if name, uw = e.evalExpression(nameexpr); uw != nil {
|
|
return nil, uw
|
|
}
|
|
|
|
var pv *rt.Pointer
|
|
var key tokens.Name
|
|
if (this != nil && this.Type().Computed()) || name.Type().Computed() {
|
|
// If the object or name are latent, we can't possibly proceed, because we do not know what to lookup.
|
|
var comps []*rt.Object
|
|
if this.Type().Computed() {
|
|
comps = append(comps, this.ComputedValue().Sources...)
|
|
}
|
|
if name.Type().Computed() {
|
|
comps = append(comps, name.ComputedValue().Sources...)
|
|
}
|
|
lat := rt.NewComputedObject(types.Dynamic, true, comps)
|
|
pv = rt.NewPointer(lat, false, nil, nil)
|
|
} else {
|
|
// Otherwise, go ahead and search the object for a property with the given name.
|
|
if name.Type() == types.Number {
|
|
_, isarr := this.Type().(*symbols.ArrayType)
|
|
contract.Assertf(isarr, "Expected an array for numeric dynamic load index")
|
|
ix := int(name.NumberValue())
|
|
arrv := this.ArrayValue()
|
|
// TODO[pulumi/lumi#70]: Although storing arrays as arrays is fine for many circumstances, there are two
|
|
// situations that could cause us troubles with ECMAScript compliance. First, negative indices are fine in
|
|
// ECMAScript. Second, sparse arrays can be represented more efficiently as a "bag of properties" than as a
|
|
// true array that needs to be resized (possibly growing to become enormous in memory usage).
|
|
// TODO[pulumi/lumi#70]: We are emulating "ECMAScript-like" array accesses, where -- just like ordinary
|
|
// property accesses below -- we will permit indexes that we've never seen before. Out of bounds should
|
|
// yield `undefined`, rather than the usual case of throwing an exception, for example. And such
|
|
// assignments are to be permitted. This will cause troubles down the road when we do other languages that
|
|
// reject out of bounds accesses e.g. Python. An alternative approach would be to require ECMAScript to
|
|
// use a runtime library anytime an array is accessed, translating exceptions like this into `undefined`s.
|
|
if ix >= len(*arrv) && (lval || try) {
|
|
newarr := make([]*rt.Pointer, ix+1)
|
|
copy(*arrv, newarr)
|
|
*arrv = newarr
|
|
}
|
|
if ix < len(*arrv) {
|
|
pv = (*arrv)[ix]
|
|
if pv == nil && (lval || try) {
|
|
nul := rt.Null
|
|
pv = rt.NewPointer(nul, false, nil, nil)
|
|
(*arrv)[ix] = pv
|
|
}
|
|
}
|
|
} else {
|
|
contract.Assertf(name.Type() == types.String, "Expected dynamic load name to be a string")
|
|
key = tokens.Name(name.StringValue())
|
|
if thisexpr == nil {
|
|
pv = e.getDynamicNameAddr(key, lval)
|
|
} else {
|
|
var uw *rt.Unwind
|
|
if pv, uw = e.getObjectOrSuperProperty(
|
|
this, thisexpr, rt.PropertyKey(key), lval || try, lval); uw != nil {
|
|
return nil, uw
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if pv == nil && try {
|
|
// If this is a try load and we couldn't find the name,
|
|
glog.V(9).Infof("Attempted to dynamically load location %v; failed -- returning null", key)
|
|
pv = rt.NewPointer(rt.Null, false, nil, nil)
|
|
}
|
|
|
|
if pv == nil {
|
|
// If the result is nil, then the name is not defined. In the event of a try load, we will have substituted a
|
|
// null already above, so if we got here, we need to propagate an exception.
|
|
contract.Assert(!try)
|
|
contract.Assert(!lval)
|
|
return nil, e.NewNameNotDefinedException(node, key)
|
|
}
|
|
|
|
// 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 {
|
|
obj = rt.NewPointerObject(types.Dynamic, pv)
|
|
} else {
|
|
obj = pv.Obj()
|
|
}
|
|
contract.Assert(obj != nil)
|
|
|
|
return &Location{
|
|
e: e,
|
|
this: this,
|
|
name: key,
|
|
lval: lval,
|
|
obj: obj,
|
|
getter: pv.Getter(),
|
|
setter: pv.Setter(),
|
|
}, nil
|
|
}
|
|
|
|
func getDynamicNameAddrCore(locals rt.Environment, globals *rt.Object, key tokens.Name) *rt.Pointer {
|
|
if loc := locals.Lookup(key); loc != nil {
|
|
return locals.GetValueAddr(loc, true) // create a slot, we know the declaration exists.
|
|
}
|
|
// If it didn't exist in the lexical scope, check the module's globals.
|
|
pkey := rt.PropertyKey(key)
|
|
return globals.Properties().GetAddr(pkey) // look for a global by this name, but don't allocate one.
|
|
}
|
|
|
|
func (e *evaluator) getDynamicNameAddr(key tokens.Name, lval bool) *rt.Pointer {
|
|
globals := e.getModuleGlobals(e.ctx.Currmodule)
|
|
pv := getDynamicNameAddrCore(e.locals, globals, key)
|
|
|
|
// If not found and this is the target of a load, allocate a slot.
|
|
if pv == nil && lval {
|
|
if e.fnc != nil && e.fnc.SpecialModInit() && e.locals.Activation() {
|
|
pkey := rt.PropertyKey(key)
|
|
pv = globals.Properties().GetInitAddr(pkey)
|
|
} else {
|
|
loc := symbols.NewSpecialVariableSym(key, types.Dynamic)
|
|
e.locals.MustRegister(loc)
|
|
pv = e.locals.GetValueAddr(loc, true)
|
|
}
|
|
}
|
|
|
|
return pv
|
|
}
|
|
|
|
func (e *evaluator) evalNewExpression(node *ast.NewExpression) (*rt.Object, *rt.Unwind) {
|
|
// Fetch the type of this expression; that's the kind of object we are allocating.
|
|
ty := e.ctx.RequireType(node)
|
|
// Now actually perform the new operation.
|
|
return e.evalNew(node, ty, node.Arguments)
|
|
}
|
|
|
|
// evalNew performs a new operation on the given type with the given arguments.
|
|
func (e *evaluator) evalNew(node diag.Diagable, t symbols.Type, args *[]*ast.CallArgument) (*rt.Object, *rt.Unwind) {
|
|
// TODO[pulumi/lumi#176]: if a dynamic invoke, we want a runtime exceptions, not assertions, for failures below.
|
|
|
|
// Create a object of the right type, containing all of the properties pre-populated.
|
|
obj := e.newObject(t)
|
|
|
|
// Evaluate the arguments in order.
|
|
var argobjs []*rt.Object
|
|
if args != nil {
|
|
for _, arg := range *args {
|
|
argobj, uw := e.evalExpression(arg.Expr)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
argobjs = append(argobjs, argobj)
|
|
}
|
|
}
|
|
|
|
// See if there is a constructor or if this is a record. If not, just return a fresh object.
|
|
if ctor := t.Ctor(); ctor != nil {
|
|
contract.Assertf(ctor.Signature().Return == nil || ctor.Signature().Return == types.Dynamic,
|
|
"Expected ctor %v to have a nil (or dynamic) return; got %v", ctor, ctor.Signature().Return)
|
|
// Now dispatch the function call using the fresh object as the constructor's `this` argument.
|
|
if _, uw := e.evalCallSymbol(node, ctor, obj, argobjs...); uw != nil {
|
|
return nil, uw
|
|
}
|
|
} else if args != nil && t.Record() {
|
|
// If this is a record type, we can still initialize it much like an object literal, using named properties.
|
|
// This only works provided the arguments are named and each one maps to a primary property on the type.
|
|
for i, arg := range *args {
|
|
contract.Assertf(arg.Name != nil, "Expected only named args for new of a record type")
|
|
id := tokens.ClassMemberName(arg.Name.Ident)
|
|
contract.Assertf(t.TypeMembers()[id] != nil, "Expected named arg %v to match a type member", id)
|
|
contract.Assertf(t.TypeMembers()[id].Primary(), "Expected named arg %v to match a primary member", id)
|
|
val := argobjs[i]
|
|
prop := rt.PropertyKey(id)
|
|
addr := obj.GetPropertyAddr(prop, true, true)
|
|
addr.Set(val)
|
|
}
|
|
} else {
|
|
contract.Assertf(args == nil || len(*args) == 0,
|
|
"No constructor found for non-record type %v, yet the new expression had %v args", t, len(*args))
|
|
}
|
|
|
|
// If there are post-construction hooks, run them right now. Note that we intentionally invoke this after
|
|
// construction but before object freezing, in case the hook wants to mutate readonly properties. In a sense, this
|
|
// hook becomes an extension of the constructor itself.
|
|
if e.hooks != nil {
|
|
if uw := e.hooks.OnObjectInit(node, obj); uw != nil {
|
|
return nil, uw
|
|
}
|
|
}
|
|
|
|
// Finally, ensure that all readonly properties are frozen now.
|
|
obj.FreezeReadonlyProperties()
|
|
|
|
return obj, nil
|
|
}
|
|
|
|
func (e *evaluator) evalInvokeFunctionExpression(node *ast.InvokeFunctionExpression) (*rt.Object, *rt.Unwind) {
|
|
// Evaluate the function that we are meant to invoke. Note that at the moment we reject latent types; we could
|
|
// simply propagate a latent value with the expected return type, however that would risk covering up code paths
|
|
// that contain conditionals, something that we don't permit until pulumi/lumi#170 is handled.
|
|
fncobj, uw := e.requireExpressionValue(node.Function)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
|
|
// See if this is a dynamic invocation -- these are validated differently.
|
|
dynamic := (e.ctx.RequireType(node.Function) == types.Dynamic)
|
|
|
|
// Ensure that this actually led to a function; this is guaranteed by the binder.
|
|
var fnc rt.FuncStub
|
|
switch t := fncobj.Type().(type) {
|
|
case *symbols.FunctionType:
|
|
fnc = fncobj.FunctionValue()
|
|
case *symbols.PrototypeType:
|
|
contract.Assertf(dynamic, "Prototype invocation is only valid for dynamic invokes")
|
|
// For dynamic invokes, we permit invocation of class prototypes (a "new").
|
|
ot := t.Type // this is the type of object to create.
|
|
return e.evalNew(node, ot, node.Arguments)
|
|
default:
|
|
// If dynamic, raise an exception; otherwise, assert, since the IL was malformed and should have been caught
|
|
// during binding/verification time.
|
|
if dynamic {
|
|
return nil, e.NewIllegalInvokeTargetException(node.Function, t)
|
|
}
|
|
contract.Failf("Expected function expression to yield a function type")
|
|
}
|
|
|
|
// Now evaluate the arguments to the function, in order.
|
|
var args []*rt.Object
|
|
if node.Arguments != nil {
|
|
for _, arg := range *node.Arguments {
|
|
argobj, uw := e.evalExpression(arg.Expr)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
contract.Assertf(argobj != nil, "Unexpected nil argument expression: %v", arg.Expr)
|
|
args = append(args, argobj)
|
|
}
|
|
}
|
|
|
|
// Finally, actually dispatch the call; this will create the activation frame, etc. for us.
|
|
return e.evalCallFuncStub(node, fnc, args...)
|
|
}
|
|
|
|
func (e *evaluator) evalLambdaExpression(node *ast.LambdaExpression) (*rt.Object, *rt.Unwind) {
|
|
// To create a lambda object we will simply produce a function object that can invoke it. Lambdas also uniquely
|
|
// capture the current environment, including the this variable.
|
|
sig := e.ctx.RequireType(node).(*symbols.FunctionType)
|
|
moduleObject := e.getModuleGlobals(e.ctx.Currmodule)
|
|
obj := rt.NewFunctionObjectFromLambda(node, sig, e.locals, moduleObject)
|
|
return obj, nil
|
|
}
|
|
|
|
func (e *evaluator) evalUnaryOperatorExpression(node *ast.UnaryOperatorExpression) (*rt.Object, *rt.Unwind) {
|
|
return e.evalUnaryOperatorExpressionFor(node, false)
|
|
}
|
|
|
|
func (e *evaluator) evalUnaryOperatorExpressionFor(node *ast.UnaryOperatorExpression,
|
|
lval bool) (*rt.Object, *rt.Unwind) {
|
|
contract.Assertf(!lval || node.Operator == ast.OpDereference, "Only dereference unary ops support l-values")
|
|
|
|
// Evaluate the operand and prepare to use it.
|
|
var opand *rt.Object
|
|
var opandloc *Location
|
|
op := node.Operator
|
|
if op == ast.OpAddressof || op == ast.OpPlusPlus || op == ast.OpMinusMinus {
|
|
// These operators require an l-value; so we bind the expression a bit differently.
|
|
loc, uw := e.evalLValueExpression(node.Operand)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
opand, uw = loc.Read(node)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
opandloc = loc
|
|
} else {
|
|
// Otherwise, we just need to evaluate the operand as usual.
|
|
var uw *rt.Unwind
|
|
if opand, uw = e.evalExpression(node.Operand); uw != nil {
|
|
return nil, uw
|
|
}
|
|
}
|
|
|
|
// See if the operand is a computed type. If yes, treat it differently.
|
|
if opand.Type().Computed() {
|
|
comps := opand.ComputedValue().Sources
|
|
switch op {
|
|
case ast.OpDereference:
|
|
// The target is a pointer; return the underlying pointer element.
|
|
pt := opand.Type().(*symbols.ComputedType).Element
|
|
et := pt.(*symbols.PointerType).Element
|
|
return rt.NewComputedObject(et, true, comps), nil
|
|
case ast.OpAddressof:
|
|
// The target is a pointer; return the actual pointer.
|
|
pt := opand.Type().(*symbols.ComputedType).Element
|
|
return rt.NewComputedObject(pt, true, comps), nil
|
|
case ast.OpLogicalNot:
|
|
// The target is a boolean; propagate a latent boolean.
|
|
return rt.NewComputedObject(types.Bool, true, comps), nil
|
|
case ast.OpUnaryPlus, ast.OpUnaryMinus, ast.OpBitwiseNot,
|
|
ast.OpPlusPlus, ast.OpMinusMinus:
|
|
// All these operators deal with numbers; so, propagate a latent number.
|
|
return rt.NewComputedObject(types.Number, true, comps), nil
|
|
default:
|
|
contract.Failf("Unrecognized unary operator: %v", op)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
// The value is known; switch on the operator and perform its specific operation.
|
|
switch op {
|
|
case ast.OpDereference:
|
|
// The target is a pointer. If this is for an l-value, just return it as-is; otherwise, dereference it.
|
|
ptr := opand.PointerValue()
|
|
contract.Assert(ptr != nil)
|
|
if lval {
|
|
return opand, nil
|
|
}
|
|
return ptr.Obj(), nil
|
|
case ast.OpAddressof:
|
|
// The target is an l-value, load its address.
|
|
contract.Assert(opand.PointerValue() != nil)
|
|
return opand, nil
|
|
case ast.OpUnaryPlus:
|
|
// The target is a number; simply fetch it (asserting its value), and + it.
|
|
return rt.NewNumberObject(+opand.NumberValue()), nil
|
|
case ast.OpUnaryMinus:
|
|
// The target is a number; simply fetch it (asserting its value), and - it.
|
|
return rt.NewNumberObject(-opand.NumberValue()), nil
|
|
case ast.OpLogicalNot:
|
|
// The target is a boolean; simply fetch it (asserting its value), and ! it.
|
|
return rt.Bools[!opand.BoolValue()], nil
|
|
case ast.OpBitwiseNot:
|
|
// The target is a number; simply fetch it (asserting its value), and ^ it (similar to C's ~ operator).
|
|
return rt.NewNumberObject(float64(^int64(opand.NumberValue()))), nil
|
|
case ast.OpPlusPlus:
|
|
// The target is an l-value; we must load it, ++ it, and return the appropriate prefix/postfix value.
|
|
ptr := opand.PointerValue()
|
|
old := ptr.Obj()
|
|
val := old.NumberValue()
|
|
new := rt.NewNumberObject(val + 1)
|
|
if uw := opandloc.Set(node.Operand, new); uw != nil {
|
|
return nil, uw
|
|
}
|
|
if node.Postfix {
|
|
return old, nil
|
|
}
|
|
return new, nil
|
|
case ast.OpMinusMinus:
|
|
// The target is an l-value; we must load it, -- it, and return the appropriate prefix/postfix value.
|
|
ptr := opand.PointerValue()
|
|
old := ptr.Obj()
|
|
val := old.NumberValue()
|
|
new := rt.NewNumberObject(val - 1)
|
|
if uw := opandloc.Set(node.Operand, new); uw != nil {
|
|
return nil, uw
|
|
}
|
|
if node.Postfix {
|
|
return old, nil
|
|
}
|
|
return new, nil
|
|
default:
|
|
contract.Failf("Unrecognized unary operator: %v", op)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
func (e *evaluator) evalBinaryOperatorExpression(node *ast.BinaryOperatorExpression) (*rt.Object, *rt.Unwind) {
|
|
// Evaluate the operands and prepare to use them. First left, then right.
|
|
var lhs *rt.Object
|
|
var lhsloc *Location
|
|
op := node.Operator
|
|
if ast.IsAssignmentBinaryOperator(op) {
|
|
// If an assignment, we must produce an l-value.
|
|
var uw *rt.Unwind
|
|
if lhsloc, uw = e.evalLValueExpression(node.Left); uw != nil {
|
|
return nil, uw
|
|
}
|
|
lhs, uw = lhsloc.Read(node)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
} else {
|
|
// Else we will just be evaluating an expression to use its value.
|
|
var uw *rt.Unwind
|
|
if lhs, uw = e.evalExpression(node.Left); uw != nil {
|
|
return nil, uw
|
|
}
|
|
}
|
|
|
|
// For the logical && and ||, we will only evaluate the rhs it if the lhs was true.
|
|
if ast.IsConditionalBinaryOperator(op) {
|
|
if uw := e.rejectComputed(node.Left, lhs); uw != nil {
|
|
return nil, uw
|
|
}
|
|
switch op {
|
|
case ast.OpLogicalAnd:
|
|
if lhs.BoolValue() {
|
|
return e.evalExpression(node.Right)
|
|
}
|
|
return rt.False, nil
|
|
case ast.OpLogicalOr:
|
|
if !lhs.BoolValue() {
|
|
return e.evalExpression(node.Right)
|
|
}
|
|
return rt.True, nil
|
|
}
|
|
}
|
|
|
|
// Otherwise, just evaluate the rhs and prepare to evaluate the operator.
|
|
rhs, uw := e.evalExpression(node.Right)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
|
|
// If the operator involves computed operations, we must accumulate a list of the sources, so we can propagate.
|
|
var comps []*rt.Object
|
|
if lhs.Type().Computed() && op != ast.OpAssign {
|
|
// Note that a LHS that is computed doesn't matter for straight assignment, because we never read it.
|
|
lhssrc := lhs.ComputedValue().Sources
|
|
contract.Assert(len(lhssrc) > 0)
|
|
comps = append(comps, lhssrc...)
|
|
}
|
|
if rhs.Type().Computed() {
|
|
rhssrc := rhs.ComputedValue().Sources
|
|
contract.Assert(len(rhssrc) > 0)
|
|
comps = append(comps, rhssrc...)
|
|
}
|
|
|
|
// Switch on operator to perform the operator's effects.
|
|
// TODO[pulumi/lumi#176]: anywhere there is type coercion to/from float64/int64/etc., we should be skeptical.
|
|
// Because our numeric type system is float64-based -- i.e., "JSON-like" -- we often find ourselves doing
|
|
// operations on floats that honestly don't make sense (like shifting, masking, and whatnot). If there is a
|
|
// type coercion, Golang (rightfully) doesn't support an operator on numbers of that type. I suspect we'll
|
|
// eventually want to consider integer types in LumiIL, and/or verify that numbers aren't outside of the legal
|
|
// range as part of verification, and then push the responsibility for presenting valid LumiIL with any required
|
|
// conversions bacp up to the compilers (compile-time, runtime, or othwerwise, per the language semantics).
|
|
if ast.IsArithmeticBinaryOperator(op) {
|
|
if len(comps) > 0 {
|
|
// If the arithmetic operator uses a computed value, propagate a computed of that type. In the case of
|
|
// string concatenation, the result will be a computed string; for all other cases, a number.
|
|
if op == ast.OpAdd && types.CanConvert(rhs.Type(), types.String) {
|
|
return rt.NewComputedObject(types.String, true, comps), nil
|
|
}
|
|
return rt.NewComputedObject(types.Number, true, comps), nil
|
|
}
|
|
|
|
switch op {
|
|
case ast.OpAdd:
|
|
// If the lhs/rhs are strings, concatenate them; if numbers, + them.
|
|
if types.CanConvert(rhs.Type(), types.String) {
|
|
return rt.NewStringObject(lhs.StringValue() + rhs.StringValue()), nil
|
|
}
|
|
return rt.NewNumberObject(lhs.NumberValue() + rhs.NumberValue()), nil
|
|
case ast.OpSubtract:
|
|
// Both targets are numbers; fetch them (asserting their types), and - them.
|
|
return rt.NewNumberObject(lhs.NumberValue() - rhs.NumberValue()), nil
|
|
case ast.OpMultiply:
|
|
// Both targets are numbers; fetch them (asserting their types), and * them.
|
|
return rt.NewNumberObject(lhs.NumberValue() * rhs.NumberValue()), nil
|
|
case ast.OpDivide:
|
|
// Both targets are numbers; fetch them (asserting their types), and / them.
|
|
return rt.NewNumberObject(lhs.NumberValue() / rhs.NumberValue()), nil
|
|
case ast.OpRemainder:
|
|
// Both targets are numbers; fetch them (asserting their types), and % them.
|
|
return rt.NewNumberObject(float64(int64(lhs.NumberValue()) % int64(rhs.NumberValue()))), nil
|
|
case ast.OpExponentiate:
|
|
// Both targets are numbers; fetch them (asserting their types), and raise lhs to rhs's power.
|
|
return rt.NewNumberObject(math.Pow(lhs.NumberValue(), rhs.NumberValue())), nil
|
|
default:
|
|
contract.Failf("Unrecognized binary arithmetic operator: %v", op)
|
|
return nil, nil
|
|
}
|
|
} else if ast.IsBitwiseBinaryOperator(op) {
|
|
if len(comps) > 0 {
|
|
// If the binary operator uses a computed value, propagate a computed number.
|
|
return rt.NewComputedObject(types.Number, true, comps), nil
|
|
}
|
|
|
|
switch op {
|
|
// TODO[pulumi/lumi#176]: the ECMAScript specification for bitwise operators is a fair bit more complicated than
|
|
// these; for instance, shifts mask out all but the least significant 5 bits of the rhs. If we don't do it
|
|
// here, LumiJS should; e.g. see https://www.ecma-international.org/ecma-262/7.0/#sec-left-shift-operator.
|
|
case ast.OpBitwiseShiftLeft:
|
|
// Both targets are numbers; fetch them (asserting their types), and << them.
|
|
// TODO[pulumi/lumi#176]: issue an error if rhs is negative.
|
|
return rt.NewNumberObject(float64(int64(lhs.NumberValue()) << uint(rhs.NumberValue()))), nil
|
|
case ast.OpBitwiseShiftRight:
|
|
// Both targets are numbers; fetch them (asserting their types), and >> them.
|
|
// TODO[pulumi/lumi#176]: issue an error if rhs is negative.
|
|
return rt.NewNumberObject(float64(int64(lhs.NumberValue()) >> uint(rhs.NumberValue()))), nil
|
|
case ast.OpBitwiseAnd:
|
|
// Both targets are numbers; fetch them (asserting their types), and & them.
|
|
return rt.NewNumberObject(float64(int64(lhs.NumberValue()) & int64(rhs.NumberValue()))), nil
|
|
case ast.OpBitwiseOr:
|
|
// Both targets are numbers; fetch them (asserting their types), and | them.
|
|
return rt.NewNumberObject(float64(int64(lhs.NumberValue()) | int64(rhs.NumberValue()))), nil
|
|
case ast.OpBitwiseXor:
|
|
// Both targets are numbers; fetch them (asserting their types), and ^ them.
|
|
return rt.NewNumberObject(float64(int64(lhs.NumberValue()) ^ int64(rhs.NumberValue()))), nil
|
|
default:
|
|
contract.Failf("Unrecognized binary bitwise operator: %v", op)
|
|
return nil, nil
|
|
}
|
|
} else if ast.IsAssignmentBinaryOperator(op) {
|
|
// If the assignment entails computed properties, we still want to proceed with the assignment. In general,
|
|
// we simply propagate the RHS as-is, although in the cases of operators, we must check the contents.
|
|
if op == ast.OpAssign {
|
|
// The target is an l-value; just overwrite its value, and yield the new value as the result.
|
|
if uw := lhsloc.Set(node.Left, rhs); uw != nil {
|
|
return nil, uw
|
|
}
|
|
return rhs, nil
|
|
}
|
|
var val *rt.Object
|
|
ptr := lhs.PointerValue()
|
|
if len(comps) > 0 {
|
|
if op == ast.OpAssignSum && types.CanConvert(ptr.Obj().Type(), types.String) {
|
|
val = rt.NewComputedObject(types.String, true, comps)
|
|
} else {
|
|
val = rt.NewComputedObject(types.Number, true, comps)
|
|
}
|
|
} else {
|
|
switch op {
|
|
case ast.OpAssignSum:
|
|
if types.CanConvert(ptr.Obj().Type(), types.String) {
|
|
// If the lhs/rhs are strings, just concatenate += and yield the new value as a result.
|
|
val = rt.NewStringObject(ptr.Obj().StringValue() + rhs.StringValue())
|
|
} else {
|
|
// Otherwise, the target is a numeric l-value; just += to it, and yield the new value as the result.
|
|
val = rt.NewNumberObject(ptr.Obj().NumberValue() + rhs.NumberValue())
|
|
}
|
|
case ast.OpAssignDifference:
|
|
// The target is a numeric l-value; just -= rhs to it, and yield the new value as the result.
|
|
val = rt.NewNumberObject(ptr.Obj().NumberValue() - rhs.NumberValue())
|
|
case ast.OpAssignProduct:
|
|
// The target is a numeric l-value; just *= rhs to it, and yield the new value as the result.
|
|
val = rt.NewNumberObject(ptr.Obj().NumberValue() * rhs.NumberValue())
|
|
case ast.OpAssignQuotient:
|
|
// The target is a numeric l-value; just /= rhs to it, and yield the new value as the result.
|
|
val = rt.NewNumberObject(ptr.Obj().NumberValue() / rhs.NumberValue())
|
|
case ast.OpAssignRemainder:
|
|
// The target is a numeric l-value; just %= rhs to it, and yield the new value as the result.
|
|
val = rt.NewNumberObject(float64(int64(ptr.Obj().NumberValue()) % int64(rhs.NumberValue())))
|
|
case ast.OpAssignExponentiation:
|
|
// The target is a numeric l-value; just raise to rhs as a power, and yield the new value as the result.
|
|
val = rt.NewNumberObject(math.Pow(ptr.Obj().NumberValue(), rhs.NumberValue()))
|
|
case ast.OpAssignBitwiseShiftLeft:
|
|
// The target is a numeric l-value; just <<= rhs to it, and yield the new value as the result.
|
|
val = rt.NewNumberObject(float64(int64(ptr.Obj().NumberValue()) << uint(rhs.NumberValue())))
|
|
case ast.OpAssignBitwiseShiftRight:
|
|
// The target is a numeric l-value; just >>= rhs to it, and yield the new value as the result.
|
|
val = rt.NewNumberObject(float64(int64(ptr.Obj().NumberValue()) >> uint(rhs.NumberValue())))
|
|
case ast.OpAssignBitwiseAnd:
|
|
// The target is a numeric l-value; just &= rhs to it, and yield the new value as the result.
|
|
val = rt.NewNumberObject(float64(int64(ptr.Obj().NumberValue()) & int64(rhs.NumberValue())))
|
|
case ast.OpAssignBitwiseOr:
|
|
// The target is a numeric l-value; just |= rhs to it, and yield the new value as the result.
|
|
val = rt.NewNumberObject(float64(int64(ptr.Obj().NumberValue()) | int64(rhs.NumberValue())))
|
|
case ast.OpAssignBitwiseXor:
|
|
// The target is a numeric l-value; just ^= rhs to it, and yield the new value as the result.
|
|
val = rt.NewNumberObject(float64(int64(ptr.Obj().NumberValue()) ^ int64(rhs.NumberValue())))
|
|
default:
|
|
contract.Failf("Unrecognized binary assignment operator: %v", op)
|
|
return nil, nil
|
|
}
|
|
}
|
|
if uw := lhsloc.Set(node.Left, val); uw != nil {
|
|
return nil, uw
|
|
}
|
|
return val, nil
|
|
} else if ast.IsRelationalBinaryOperator(op) {
|
|
// All relational operators produce bools, so if it's computed, we can quickly return a computed bool.
|
|
if len(comps) > 0 {
|
|
return rt.NewComputedObject(types.Bool, true, comps), nil
|
|
}
|
|
|
|
switch op {
|
|
case ast.OpLt:
|
|
// The targets are numbers; just compare them with < and yield the boolean result.
|
|
return rt.Bools[lhs.NumberValue() < rhs.NumberValue()], nil
|
|
case ast.OpLtEquals:
|
|
// The targets are numbers; just compare them with <= and yield the boolean result.
|
|
return rt.Bools[lhs.NumberValue() <= rhs.NumberValue()], nil
|
|
case ast.OpGt:
|
|
// The targets are numbers; just compare them with > and yield the boolean result.
|
|
return rt.Bools[lhs.NumberValue() > rhs.NumberValue()], nil
|
|
case ast.OpGtEquals:
|
|
// The targets are numbers; just compare them with >= and yield the boolean result.
|
|
return rt.Bools[lhs.NumberValue() >= rhs.NumberValue()], nil
|
|
case ast.OpEquals:
|
|
// Equality checking handles many object types, so defer to a helper for it.
|
|
return rt.Bools[e.evalBinaryOperatorEquals(lhs, rhs)], nil
|
|
case ast.OpNotEquals:
|
|
// Just return the inverse of what the operator equals function itself returns.
|
|
return rt.Bools[!e.evalBinaryOperatorEquals(lhs, rhs)], nil
|
|
default:
|
|
contract.Failf("Unrecognized relational binary operator: %v", op)
|
|
return nil, nil
|
|
}
|
|
}
|
|
contract.Failf("Unrecognized binary operator: %v", op)
|
|
return nil, nil
|
|
}
|
|
|
|
func (e *evaluator) evalBinaryOperatorEquals(lhs *rt.Object, rhs *rt.Object) bool {
|
|
if lhs == rhs {
|
|
return true
|
|
}
|
|
if lhs.Type() == types.Bool && rhs.Type() == types.Bool {
|
|
return lhs.BoolValue() == rhs.BoolValue()
|
|
}
|
|
if lhs.Type() == types.Number && rhs.Type() == types.Number {
|
|
return lhs.NumberValue() == rhs.NumberValue()
|
|
}
|
|
if lhs.Type() == types.String && rhs.Type() == types.String {
|
|
return lhs.StringValue() == rhs.StringValue()
|
|
}
|
|
if lhs.Type() == types.Null && rhs.Type() == types.Null {
|
|
return true // all nulls are equal.
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (e *evaluator) evalCastExpression(node *ast.CastExpression) (*rt.Object, *rt.Unwind) {
|
|
// Evaluate the underlying expression.
|
|
obj, uw := e.evalExpression(node.Expression)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
|
|
// All bad static casts have been rejected, so we now need to check the runtime types.
|
|
from := obj.Type()
|
|
to := e.ctx.RequireType(node)
|
|
if !types.CanConvert(from, to) {
|
|
return nil, e.NewInvalidCastException(node, from, to)
|
|
}
|
|
|
|
return obj, nil
|
|
}
|
|
|
|
func (e *evaluator) evalIsInstExpression(node *ast.IsInstExpression) (*rt.Object, *rt.Unwind) {
|
|
// Evaluate the underlying expression.
|
|
obj, uw := e.evalExpression(node.Expression)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
|
|
// Now check the type and produce a bool object indicating whether the type is good.
|
|
from := obj.Type()
|
|
to := e.ctx.LookupType(node.Type)
|
|
isinst := types.CanConvert(from, to)
|
|
return rt.Bools[isinst], nil
|
|
}
|
|
|
|
func (e *evaluator) evalTypeOfExpression(node *ast.TypeOfExpression) (*rt.Object, *rt.Unwind) {
|
|
// Evaluate the underlying expression.
|
|
obj, uw := e.evalExpression(node.Expression)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
|
|
// Now just return the underlying type token for the object as a string.
|
|
tok := obj.Type().Token()
|
|
return rt.NewStringObject(string(tok)), nil
|
|
}
|
|
|
|
func (e *evaluator) evalConditionalExpression(node *ast.ConditionalExpression) (*rt.Object, *rt.Unwind) {
|
|
// Evaluate the branches explicitly based on the result of the condition node.
|
|
cond, uw := e.requireExpressionValue(node.Condition)
|
|
if uw != nil {
|
|
return nil, uw
|
|
}
|
|
if cond.BoolValue() {
|
|
return e.evalExpression(node.Consequent)
|
|
}
|
|
return e.evalExpression(node.Alternate)
|
|
}
|
|
|
|
func (e *evaluator) evalSequenceExpression(node *ast.SequenceExpression) (*rt.Object, *rt.Unwind) {
|
|
// Evaluate the sequence's prelude and then, afterwards, evaluate to its value. If an unwind happens anywhere
|
|
// during the prelude, we will abruptly terminate the sequence and return it.
|
|
for _, prelnode := range node.Prelude {
|
|
switch n := prelnode.(type) {
|
|
case ast.Expression:
|
|
if _, uw := e.evalExpression(n); uw != nil {
|
|
return nil, uw
|
|
}
|
|
case ast.Statement:
|
|
if uw := e.evalStatement(n); uw != nil {
|
|
return nil, uw
|
|
}
|
|
}
|
|
}
|
|
return e.evalExpression(node.Value)
|
|
}
|