diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index 41f691259..82eb5262d 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -11,9 +11,9 @@ import ( "github.com/marapongo/mu/pkg/compiler/binder" "github.com/marapongo/mu/pkg/compiler/core" "github.com/marapongo/mu/pkg/compiler/errors" - "github.com/marapongo/mu/pkg/compiler/eval" "github.com/marapongo/mu/pkg/compiler/metadata" "github.com/marapongo/mu/pkg/diag" + "github.com/marapongo/mu/pkg/eval" "github.com/marapongo/mu/pkg/graph" "github.com/marapongo/mu/pkg/graph/graphgen" "github.com/marapongo/mu/pkg/pack" diff --git a/pkg/compiler/eval/alloc.go b/pkg/compiler/eval/alloc.go deleted file mode 100644 index d32c3c1d3..000000000 --- a/pkg/compiler/eval/alloc.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2016 Marapongo, Inc. All rights reserved. - -package eval - -import ( - "fmt" - - "github.com/golang/glog" - - "github.com/marapongo/mu/pkg/compiler/symbols" - "github.com/marapongo/mu/pkg/compiler/types" - "github.com/marapongo/mu/pkg/util/contract" -) - -// Allocator is a factory for creating objects. -type Allocator struct { - hooks InterpreterHooks // an optional set of allocation lifetime callback hooks. -} - -// NewAllocator allocates a fresh allocator instance. -func NewAllocator(hooks InterpreterHooks) *Allocator { - return &Allocator{hooks: hooks} -} - -// newObject is used internally for all allocator-driven object allocation, so that hooks can be run appropriately. -func (a *Allocator) newObject(t symbols.Type, v interface{}, props Properties) *Object { - o := NewObject(t, nil, props) - if a.hooks != nil { - a.hooks.OnNewObject(o) - } - return o -} - -// New creates a new empty object of the given type. -func (a *Allocator) New(t symbols.Type) *Object { - return a.newObject(t, nil, nil) -} - -// NewPrimitive creates a new primitive object with the given primitive type. -func (a *Allocator) NewPrimitive(t symbols.Type, v interface{}) *Object { - if glog.V(9) { - glog.V(9).Infof("New primitive object: t=%v; v=%v", t, v) - } - return a.newObject(t, v, nil) -} - -// NewBool creates a new primitive number object. -func (a *Allocator) NewBool(v bool) *Object { - return a.NewPrimitive(types.Bool, v) -} - -// NewNumber creates a new primitive number object. -func (a *Allocator) NewNumber(v float64) *Object { - return a.NewPrimitive(types.Number, v) -} - -// NewString creates a new primitive number object. -func (a *Allocator) NewString(v string) *Object { - return a.NewPrimitive(types.String, v) -} - -// NewFunction creates a new function object that can be invoked, with the given symbol. -func (a *Allocator) NewFunction(fnc symbols.Function, this *Object) *Object { - stub := funcStub{Func: fnc, This: this} - return a.newObject(fnc.FuncType(), stub, nil) -} - -// funcStub is a stub that captures a symbol plus an optional instance 'this' object. -type funcStub struct { - Func symbols.Function - This *Object -} - -// NewPointerObject allocates a new pointer-like object that wraps the given reference. -func (a *Allocator) NewPointer(t symbols.Type, ptr *Pointer) *Object { - contract.Require(ptr != nil, "ptr") - ptrt := symbols.NewPointerType(t) - return a.NewPrimitive(ptrt, ptr) -} - -// NewError creates a new exception with the given message. -func (a *Allocator) NewError(message string, args ...interface{}) *Object { - // TODO: capture a stack trace. - return a.NewPrimitive(types.Error, fmt.Sprintf(message, args...)) -} - -// NewConstant returns a new object with the right type and value, based on some constant data. -func (a *Allocator) NewConstant(v interface{}) *Object { - if v == nil { - return a.NewPrimitive(types.Null, nil) - } - switch data := v.(type) { - case bool: - return a.NewPrimitive(types.Bool, data) - case string: - return a.NewPrimitive(types.String, data) - case float64: - return a.NewPrimitive(types.Number, data) - default: - // TODO: we could support more here (essentially, anything that is JSON serializable). - contract.Failf("Unrecognized constant data literal: %v", data) - return nil - } -} diff --git a/pkg/compiler/symbols/class.go b/pkg/compiler/symbols/class.go index e07a861ea..b04cd4008 100644 --- a/pkg/compiler/symbols/class.go +++ b/pkg/compiler/symbols/class.go @@ -122,6 +122,7 @@ func (node *ClassProperty) Token() tokens.Token { func (node *ClassProperty) Tree() diag.Diagable { return node.Node } func (node *ClassProperty) classMember() {} func (node *ClassProperty) Optional() bool { return node.Node.Optional != nil && *node.Node.Optional } +func (node *ClassProperty) Readonly() bool { return node.Node.Readonly != nil && *node.Node.Readonly } func (node *ClassProperty) Static() bool { return node.Node.Static != nil && *node.Node.Static } func (node *ClassProperty) Default() *interface{} { return node.Node.Default } func (node *ClassProperty) Type() Type { return node.Ty } diff --git a/pkg/compiler/symbols/module.go b/pkg/compiler/symbols/module.go index a2a964eea..ec2ad4207 100644 --- a/pkg/compiler/symbols/module.go +++ b/pkg/compiler/symbols/module.go @@ -124,6 +124,7 @@ func (node *ModuleProperty) Token() tokens.Token { func (node *ModuleProperty) Tree() diag.Diagable { return node.Node } func (node *ModuleProperty) moduleMember() {} func (node *ModuleProperty) MemberNode() ast.ModuleMember { return node.Node } +func (node *ModuleProperty) Readonly() bool { return node.Node.Readonly != nil && *node.Node.Readonly } func (node *ModuleProperty) Type() Type { return node.Ty } func (node *ModuleProperty) VarNode() ast.Variable { return node.Node } func (node *ModuleProperty) String() string { return string(node.Name()) } diff --git a/pkg/compiler/symbols/vars.go b/pkg/compiler/symbols/vars.go index 416fe2d09..c3544d084 100644 --- a/pkg/compiler/symbols/vars.go +++ b/pkg/compiler/symbols/vars.go @@ -31,6 +31,7 @@ func (node *LocalVariable) symbol() {} func (node *LocalVariable) Name() tokens.Name { return node.Nm } func (node *LocalVariable) Token() tokens.Token { return tokens.Token(node.Name()) } func (node *LocalVariable) Tree() diag.Diagable { return node.Node } +func (node *LocalVariable) Readonly() bool { return node.Node.Readonly != nil && *node.Node.Readonly } func (node *LocalVariable) Type() Type { return node.Ty } func (node *LocalVariable) VarNode() ast.Variable { return node.Node } func (node *LocalVariable) String() string { return string(node.Name()) } diff --git a/pkg/eval/alloc.go b/pkg/eval/alloc.go new file mode 100644 index 000000000..88226e973 --- /dev/null +++ b/pkg/eval/alloc.go @@ -0,0 +1,89 @@ +// Copyright 2016 Marapongo, Inc. All rights reserved. + +package eval + +import ( + "github.com/marapongo/mu/pkg/compiler/symbols" + "github.com/marapongo/mu/pkg/compiler/types" + "github.com/marapongo/mu/pkg/eval/rt" +) + +// Allocator is a factory for creating objects. +type Allocator struct { + hooks InterpreterHooks // an optional set of allocation lifetime callback hooks. +} + +// NewAllocator allocates a fresh allocator instance. +func NewAllocator(hooks InterpreterHooks) *Allocator { + return &Allocator{hooks: hooks} +} + +// onNewObject is invoked for each allocation and emits an appropriate event. +func (a *Allocator) onNewObject(o *rt.Object) { + if a.hooks != nil { + a.hooks.OnNewObject(o) + } +} + +// New creates a new empty object of the given type. +func (a *Allocator) New(t symbols.Type) *rt.Object { + obj := rt.NewObject(t, nil, nil) + a.onNewObject(obj) + return obj +} + +// 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) + 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) + 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) + 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) + a.onNewObject(obj) + return obj +} + +// NewFunction creates a new function object that can be invoked, with the given symbol. +func (a *Allocator) NewFunction(fnc symbols.Function, this *rt.Object) *rt.Object { + obj := rt.NewFunctionObject(fnc, this) + a.onNewObject(obj) + return obj +} + +// NewPointerObject allocates a new pointer-like object that wraps the given reference. +func (a *Allocator) NewPointer(t symbols.Type, ptr *rt.Pointer) *rt.Object { + obj := rt.NewPointerObject(t, ptr) + a.onNewObject(obj) + return obj +} + +// NewError creates a new exception with the given message. +func (a *Allocator) NewError(message string, args ...interface{}) *rt.Object { + obj := rt.NewErrorObject(message, args...) + a.onNewObject(obj) + return obj +} + +// NewConstant returns a new object with the right type and value, based on some constant data. +func (a *Allocator) NewConstant(v interface{}) *rt.Object { + obj := rt.NewConstantObject(v) + a.onNewObject(obj) + return obj +} diff --git a/pkg/compiler/eval/eval.go b/pkg/eval/eval.go similarity index 93% rename from pkg/compiler/eval/eval.go rename to pkg/eval/eval.go index ba793a35e..aebafaac4 100644 --- a/pkg/compiler/eval/eval.go +++ b/pkg/eval/eval.go @@ -15,6 +15,7 @@ import ( "github.com/marapongo/mu/pkg/compiler/symbols" "github.com/marapongo/mu/pkg/compiler/types" "github.com/marapongo/mu/pkg/diag" + "github.com/marapongo/mu/pkg/eval/rt" "github.com/marapongo/mu/pkg/graph" "github.com/marapongo/mu/pkg/tokens" "github.com/marapongo/mu/pkg/util/contract" @@ -31,19 +32,19 @@ type Interpreter interface { // EvaluateModule performs evaluation on the given module's entrypoint function. EvaluateModule(mod *symbols.Module, args core.Args) graph.Graph // EvaluateFunction performs an evaluation of the given function, using the provided arguments, returning its graph. - EvaluateFunction(fnc symbols.Function, this *Object, args core.Args) graph.Graph + EvaluateFunction(fnc symbols.Function, this *rt.Object, args core.Args) graph.Graph } // InterpreterHooks is a set of callbacks that can be used to hook into interesting interpreter events. type InterpreterHooks interface { - OnNewObject(o *Object) // invoked whenever an object is created. - OnAssignProperty(o *Object, prop string, old, nw *Object) // invoked whenever a property is (re)assigned. - OnEnterPackage(pkg *symbols.Package) // invoked whenever we enter a new package. - OnLeavePackage(pkg *symbols.Package) // invoked whenever we leave a package. - OnEnterModule(mod *symbols.Module) // invoked whenever we enter a new module. - OnLeaveModule(mod *symbols.Module) // invoked whenever we leave a module. - OnEnterFunction(fnc symbols.Function) // invoked whenever we enter a new function. - OnLeaveFunction(fnc symbols.Function) // invoked whenever we leave a function. + OnNewObject(o *rt.Object) // invoked whenever an object is created. + OnAssignProperty(o *rt.Object, prop string, old, nw *rt.Object) // invoked whenever a property is (re)assigned. + OnEnterPackage(pkg *symbols.Package) // invoked whenever we enter a new package. + OnLeavePackage(pkg *symbols.Package) // invoked whenever we leave a package. + OnEnterModule(mod *symbols.Module) // invoked whenever we enter a new module. + OnLeaveModule(mod *symbols.Module) // invoked whenever we leave a module. + OnEnterFunction(fnc symbols.Function) // invoked whenever we enter a new function. + OnLeaveFunction(fnc symbols.Function) // invoked whenever we leave a function. } // New creates an interpreter that can be used to evaluate MuPackages. @@ -72,7 +73,7 @@ type evaluator struct { classinits classinitMap // a map of which classes have been initialized already. } -type globalMap map[symbols.Variable]*Pointer +type globalMap map[symbols.Variable]*rt.Pointer type modinitMap map[*symbols.Module]bool type classinitMap map[*symbols.Class]bool @@ -134,7 +135,7 @@ func (e *evaluator) EvaluateModule(mod *symbols.Module, args core.Args) graph.Gr } // EvaluateFunction performs an evaluation of the given function, using the provided arguments, returning its graph. -func (e *evaluator) EvaluateFunction(fnc symbols.Function, this *Object, args core.Args) graph.Graph { +func (e *evaluator) EvaluateFunction(fnc symbols.Function, this *rt.Object, args core.Args) graph.Graph { glog.Infof("Evaluating function '%v'", fnc.Token()) if e.hooks != nil { e.hooks.OnEnterFunction(fnc) @@ -158,8 +159,8 @@ func (e *evaluator) EvaluateFunction(fnc symbols.Function, this *Object, args co contract.Failf("Unrecognized function evaluation type: %v", reflect.TypeOf(f)) } - // First, validate any arguments, and turn them into real runtime *Objects. - var argos []*Object + // First, validate any arguments, and turn them into real runtime *rt.Objects. + var argos []*rt.Object params := fnc.FuncNode().GetParameters() if params == nil { if len(args) != 0 { @@ -312,7 +313,7 @@ func (e *evaluator) popScope() { // Functions -func (e *evaluator) evalCall(fnc symbols.Function, this *Object, args ...*Object) (*Object, *Unwind) { +func (e *evaluator) evalCall(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)) // Save the prior func, set the new one, and restore upon exit. @@ -337,10 +338,10 @@ func (e *evaluator) evalCall(fnc symbols.Function, this *Object, args ...*Object 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, &Pointer{obj: this, readonly: true}) + 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, &Pointer{obj: this, readonly: true}) + 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") @@ -538,7 +539,7 @@ func (e *evaluator) evalLabeledStatement(node *ast.LabeledStatement) *Unwind { } func (e *evaluator) evalReturnStatement(node *ast.ReturnStatement) *Unwind { - var ret *Object + var ret *rt.Object if node.Expression != nil { var uw *Unwind if ret, uw = e.evalExpression(*node.Expression); uw != nil { @@ -605,7 +606,7 @@ func (e *evaluator) evalExpressionStatement(node *ast.ExpressionStatement) *Unwi // Expressions -func (e *evaluator) evalExpression(node ast.Expression) (*Object, *Unwind) { +func (e *evaluator) evalExpression(node ast.Expression) (*rt.Object, *Unwind) { // Simply switch on the node type and dispatch to the specific function, returning the object and Unwind info. switch n := node.(type) { case *ast.NullLiteral: @@ -653,7 +654,7 @@ func (e *evaluator) evalExpression(node ast.Expression) (*Object, *Unwind) { // 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) (*Object, *Unwind) { +func (e *evaluator) evalLValueExpression(node ast.Expression) (*rt.Object, *Unwind) { switch n := node.(type) { case *ast.LoadLocationExpression: return e.evalLoadLocationExpressionFor(n, true) @@ -666,29 +667,29 @@ func (e *evaluator) evalLValueExpression(node ast.Expression) (*Object, *Unwind) } } -func (e *evaluator) evalNullLiteral(node *ast.NullLiteral) (*Object, *Unwind) { +func (e *evaluator) evalNullLiteral(node *ast.NullLiteral) (*rt.Object, *Unwind) { return e.alloc.NewPrimitive(types.Null, nil), nil } -func (e *evaluator) evalBoolLiteral(node *ast.BoolLiteral) (*Object, *Unwind) { +func (e *evaluator) evalBoolLiteral(node *ast.BoolLiteral) (*rt.Object, *Unwind) { return e.alloc.NewPrimitive(types.Bool, node.Value), nil } -func (e *evaluator) evalNumberLiteral(node *ast.NumberLiteral) (*Object, *Unwind) { +func (e *evaluator) evalNumberLiteral(node *ast.NumberLiteral) (*rt.Object, *Unwind) { return e.alloc.NewPrimitive(types.Number, node.Value), nil } -func (e *evaluator) evalStringLiteral(node *ast.StringLiteral) (*Object, *Unwind) { +func (e *evaluator) evalStringLiteral(node *ast.StringLiteral) (*rt.Object, *Unwind) { return e.alloc.NewPrimitive(types.String, node.Value), nil } -func (e *evaluator) evalArrayLiteral(node *ast.ArrayLiteral) (*Object, *Unwind) { +func (e *evaluator) evalArrayLiteral(node *ast.ArrayLiteral) (*rt.Object, *Unwind) { // Fetch this expression type and assert that it's an array. ty := e.ctx.RequireType(node).(*symbols.ArrayType) // Now create the array data. var sz *int - var arr []Value + var arr []rt.Value // If there's a node size, ensure it's a number, and initialize the array. if node.Size != nil { @@ -702,7 +703,7 @@ func (e *evaluator) evalArrayLiteral(node *ast.ArrayLiteral) (*Object, *Unwind) // If the size is less than zero, raise a new error. return nil, NewThrowUnwind(e.alloc.NewError("Invalid array size (must be >= 0)")) } - arr = make([]Value, sz) + arr = make([]rt.Value, sz) } // If there are elements, place them into the array. This has two behaviors: @@ -711,7 +712,7 @@ func (e *evaluator) evalArrayLiteral(node *ast.ArrayLiteral) (*Object, *Unwind) if node.Elements != nil { if sz == nil { // Right-size the array. - arr = make([]Value, 0, len(*node.Elements)) + 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( @@ -736,7 +737,7 @@ func (e *evaluator) evalArrayLiteral(node *ast.ArrayLiteral) (*Object, *Unwind) return e.alloc.NewPrimitive(ty, arr), nil } -func (e *evaluator) evalObjectLiteral(node *ast.ObjectLiteral) (*Object, *Unwind) { +func (e *evaluator) evalObjectLiteral(node *ast.ObjectLiteral) (*rt.Object, *Unwind) { obj := e.alloc.New(e.ctx.Types[node]) if node.Properties != nil { @@ -754,13 +755,13 @@ func (e *evaluator) evalObjectLiteral(node *ast.ObjectLiteral) (*Object, *Unwind return obj, nil } -func (e *evaluator) evalLoadLocationExpression(node *ast.LoadLocationExpression) (*Object, *Unwind) { +func (e *evaluator) evalLoadLocationExpression(node *ast.LoadLocationExpression) (*rt.Object, *Unwind) { return e.evalLoadLocationExpressionFor(node, false) } -func (e *evaluator) evalLoadLocationExpressionFor(node *ast.LoadLocationExpression, lval bool) (*Object, *Unwind) { +func (e *evaluator) evalLoadLocationExpressionFor(node *ast.LoadLocationExpression, lval bool) (*rt.Object, *Unwind) { // If there's a 'this', evaluate it. - var this *Object + var this *rt.Object if node.Object != nil { var uw *Unwind if this, uw = e.evalExpression(*node.Object); uw != nil { @@ -769,7 +770,7 @@ func (e *evaluator) evalLoadLocationExpressionFor(node *ast.LoadLocationExpressi } // Create a pointer to the target location. - var pv *Pointer + var pv *rt.Pointer var ty symbols.Type tok := node.Name.Tok if this == nil && tok.Simple() { @@ -790,17 +791,14 @@ func (e *evaluator) evalLoadLocationExpressionFor(node *ast.LoadLocationExpressi // 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 = &Pointer{ - obj: e.alloc.NewFunction(s, this), - readonly: true, - } + 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 = &Pointer{} + ref = rt.NewPointer(nil, s.Readonly()) e.globals[s] = ref } pv = ref @@ -809,10 +807,7 @@ func (e *evaluator) evalLoadLocationExpressionFor(node *ast.LoadLocationExpressi // 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 = &Pointer{ - obj: e.alloc.NewFunction(s, nil), - readonly: true, - } + pv = rt.NewPointer(e.alloc.NewFunction(s, nil), true) ty = s.Type default: contract.Failf("Unexpected symbol token kind during load expression: %v", tok) @@ -830,18 +825,18 @@ func (e *evaluator) evalLoadLocationExpressionFor(node *ast.LoadLocationExpressi return pv.Obj(), nil } -func (e *evaluator) evalLoadDynamicExpression(node *ast.LoadDynamicExpression) (*Object, *Unwind) { +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 } -func (e *evaluator) evalNewExpression(node *ast.NewExpression) (*Object, *Unwind) { +func (e *evaluator) evalNewExpression(node *ast.NewExpression) (*rt.Object, *Unwind) { // TODO: create a new object and invoke its constructor. contract.Failf("Evaluation of %v nodes not yet implemented", reflect.TypeOf(node)) return nil, nil } -func (e *evaluator) evalInvokeFunctionExpression(node *ast.InvokeFunctionExpression) (*Object, *Unwind) { +func (e *evaluator) evalInvokeFunctionExpression(node *ast.InvokeFunctionExpression) (*rt.Object, *Unwind) { // Evaluate the function that we are meant to invoke. fncobj, uw := e.evalExpression(node.Function) if uw != nil { @@ -849,7 +844,7 @@ func (e *evaluator) evalInvokeFunctionExpression(node *ast.InvokeFunctionExpress } // Ensure that this actually led to a function; this is guaranteed by the binder. - var fnc funcStub + var fnc rt.FuncStub switch fncobj.Type().(type) { case *symbols.FunctionType: fnc = fncobj.FunctionValue() @@ -859,7 +854,7 @@ func (e *evaluator) evalInvokeFunctionExpression(node *ast.InvokeFunctionExpress } // Now evaluate the arguments to the function, in order. - var args []*Object + var args []*rt.Object if node.Arguments != nil { for _, arg := range *node.Arguments { argobj, uw := e.evalExpression(arg) @@ -874,21 +869,21 @@ func (e *evaluator) evalInvokeFunctionExpression(node *ast.InvokeFunctionExpress return e.evalCall(fnc.Func, fnc.This, args...) } -func (e *evaluator) evalLambdaExpression(node *ast.LambdaExpression) (*Object, *Unwind) { +func (e *evaluator) evalLambdaExpression(node *ast.LambdaExpression) (*rt.Object, *Unwind) { // TODO: create the lambda object that can be invoked at runtime. contract.Failf("Evaluation of %v nodes not yet implemented", reflect.TypeOf(node)) return nil, nil } -func (e *evaluator) evalUnaryOperatorExpression(node *ast.UnaryOperatorExpression) (*Object, *Unwind) { +func (e *evaluator) evalUnaryOperatorExpression(node *ast.UnaryOperatorExpression) (*rt.Object, *Unwind) { return e.evalUnaryOperatorExpressionFor(node, false) } -func (e *evaluator) evalUnaryOperatorExpressionFor(node *ast.UnaryOperatorExpression, lval bool) (*Object, *Unwind) { +func (e *evaluator) evalUnaryOperatorExpressionFor(node *ast.UnaryOperatorExpression, lval bool) (*rt.Object, *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 *Object + var opand *rt.Object if node.Operator == ast.OpAddressof || node.Operator == ast.OpPlusPlus || node.Operator == ast.OpMinusMinus { // These operators require an l-value; so we bind the expression a bit differently. @@ -960,9 +955,9 @@ func (e *evaluator) evalUnaryOperatorExpressionFor(node *ast.UnaryOperatorExpres } } -func (e *evaluator) evalBinaryOperatorExpression(node *ast.BinaryOperatorExpression) (*Object, *Unwind) { +func (e *evaluator) evalBinaryOperatorExpression(node *ast.BinaryOperatorExpression) (*rt.Object, *Unwind) { // Evaluate the operands and prepare to use them. First left, then right. - var lhs *Object + var lhs *rt.Object if isBinaryAssignmentOperator(node.Operator) { var uw *Unwind if lhs, uw = e.evalLValueExpression(node.Left); uw != nil { @@ -1153,7 +1148,7 @@ func isBinaryAssignmentOperator(op ast.BinaryOperator) bool { } } -func (e *evaluator) evalBinaryOperatorEquals(lhs *Object, rhs *Object) bool { +func (e *evaluator) evalBinaryOperatorEquals(lhs *rt.Object, rhs *rt.Object) bool { if lhs == rhs { return true } @@ -1172,22 +1167,22 @@ func (e *evaluator) evalBinaryOperatorEquals(lhs *Object, rhs *Object) bool { return false } -func (e *evaluator) evalCastExpression(node *ast.CastExpression) (*Object, *Unwind) { +func (e *evaluator) evalCastExpression(node *ast.CastExpression) (*rt.Object, *Unwind) { contract.Failf("Evaluation of %v nodes not yet implemented", reflect.TypeOf(node)) return nil, nil } -func (e *evaluator) evalIsInstExpression(node *ast.IsInstExpression) (*Object, *Unwind) { +func (e *evaluator) evalIsInstExpression(node *ast.IsInstExpression) (*rt.Object, *Unwind) { contract.Failf("Evaluation of %v nodes not yet implemented", reflect.TypeOf(node)) return nil, nil } -func (e *evaluator) evalTypeOfExpression(node *ast.TypeOfExpression) (*Object, *Unwind) { +func (e *evaluator) evalTypeOfExpression(node *ast.TypeOfExpression) (*rt.Object, *Unwind) { contract.Failf("Evaluation of %v nodes not yet implemented", reflect.TypeOf(node)) return nil, nil } -func (e *evaluator) evalConditionalExpression(node *ast.ConditionalExpression) (*Object, *Unwind) { +func (e *evaluator) evalConditionalExpression(node *ast.ConditionalExpression) (*rt.Object, *Unwind) { // Evaluate the branches explicitly based on the result of the condition node. cond, uw := e.evalExpression(node.Condition) if uw != nil { @@ -1199,9 +1194,9 @@ func (e *evaluator) evalConditionalExpression(node *ast.ConditionalExpression) ( return e.evalExpression(node.Alternate) } -func (e *evaluator) evalSequenceExpression(node *ast.SequenceExpression) (*Object, *Unwind) { +func (e *evaluator) evalSequenceExpression(node *ast.SequenceExpression) (*rt.Object, *Unwind) { // Simply walk through the sequence and return the last object. - var obj *Object + var obj *rt.Object contract.Assert(len(node.Expressions) > 0) for _, expr := range node.Expressions { var uw *Unwind diff --git a/pkg/compiler/eval/object.go b/pkg/eval/rt/object.go similarity index 60% rename from pkg/compiler/eval/object.go rename to pkg/eval/rt/object.go index 7072f2977..9e0b56970 100644 --- a/pkg/compiler/eval/object.go +++ b/pkg/eval/rt/object.go @@ -1,6 +1,6 @@ // Copyright 2016 Marapongo, Inc. All rights reserved. -package eval +package rt import ( "fmt" @@ -62,9 +62,9 @@ func (o *Object) StringValue() string { } // FunctionValue asserts that the target is a reference and returns its value. -func (o *Object) FunctionValue() funcStub { +func (o *Object) FunctionValue() FuncStub { contract.Assertf(o.value != nil, "Expected Function object to carry a Value; got nil") - r, ok := o.value.(funcStub) + r, ok := o.value.(FuncStub) contract.Assertf(ok, "Expected Function object's Value to be a Function") return r } @@ -80,12 +80,35 @@ func (o *Object) PointerValue() *Pointer { // GetPropertyAddr returns the reference to an object's property, lazily initializing if 'init' is true, or // returning nil otherwise. func (o *Object) GetPropertyAddr(nm tokens.Name, init bool) *Pointer { - ref, has := o.properties[nm] - if !has { - ref = &Pointer{} - o.properties[nm] = ref + ptr, hasprop := o.properties[nm] + if !hasprop { + // Look up the property definition (if any) in the members list, to seed a default value. + var obj *Object + var readonly bool + if class, isclass := o.t.(*symbols.Class); isclass { + if member, hasmember := class.Members[tokens.ClassMemberName(nm)]; hasmember { + switch m := member.(type) { + case *symbols.ClassProperty: + if m.Default() != nil { + obj = NewConstantObject(*m.Default()) + } + readonly = m.Readonly() + case *symbols.ClassMethod: + if m.Static() { + obj = NewFunctionObject(m, nil) + } else { + obj = NewFunctionObject(m, o) + } + readonly = true // TODO[marapongo/mu#56]: consider permitting JS-style overwriting of methods. + default: + contract.Failf("Unexpected member type: %v", member) + } + } + } + ptr = NewPointer(obj, readonly) + o.properties[nm] = ptr } - return ref + return ptr } // String can be used to print the contents of an object; it tries to be smart about the display. @@ -134,3 +157,67 @@ func (o *Object) String() string { return "obj{type=" + o.t.Token().String() + ",props={" + p + "}}" } } + +// NewPrimitiveObject creates a new primitive object with the given primitive type. +func NewPrimitiveObject(t symbols.Type, v interface{}) *Object { + return NewObject(t, v, nil) +} + +// NewBoolObject creates a new primitive number object. +func NewBoolObject(v bool) *Object { + return NewPrimitiveObject(types.Bool, v) +} + +// NewNumber creates a new primitive number object. +func NewNumber(v float64) *Object { + return NewPrimitiveObject(types.Number, v) +} + +// NewString creates a new primitive number object. +func NewString(v string) *Object { + return NewPrimitiveObject(types.String, v) +} + +// NewFunctionObject creates a new function object that can be invoked, with the given symbol. +func NewFunctionObject(fnc symbols.Function, this *Object) *Object { + stub := FuncStub{Func: fnc, This: this} + return NewObject(fnc.FuncType(), stub, nil) +} + +// NewPointerObject allocates a new pointer-like object that wraps the given reference. +func NewPointerObject(t symbols.Type, ptr *Pointer) *Object { + contract.Require(ptr != nil, "ptr") + ptrt := symbols.NewPointerType(t) + return NewPrimitiveObject(ptrt, ptr) +} + +// NewErrorObject creates a new exception with the given message. +func NewErrorObject(message string, args ...interface{}) *Object { + // TODO: capture a stack trace. + return NewPrimitiveObject(types.Error, fmt.Sprintf(message, args...)) +} + +// NewConstantObject returns a new object with the right type and value, based on some constant data. +func NewConstantObject(v interface{}) *Object { + if v == nil { + return NewPrimitiveObject(types.Null, nil) + } + switch data := v.(type) { + case bool: + return NewPrimitiveObject(types.Bool, data) + case string: + return NewPrimitiveObject(types.String, data) + case float64: + return NewPrimitiveObject(types.Number, data) + default: + // TODO: we could support more here (essentially, anything that is JSON serializable). + contract.Failf("Unrecognized constant data literal: %v", data) + return nil + } +} + +// FuncStub is a stub that captures a symbol plus an optional instance 'this' object. +type FuncStub struct { + Func symbols.Function + This *Object +} diff --git a/pkg/compiler/eval/pointer.go b/pkg/eval/rt/pointer.go similarity index 88% rename from pkg/compiler/eval/pointer.go rename to pkg/eval/rt/pointer.go index 5ee59699b..6a402bdc1 100644 --- a/pkg/compiler/eval/pointer.go +++ b/pkg/eval/rt/pointer.go @@ -1,6 +1,6 @@ // Copyright 2016 Marapongo, Inc. All rights reserved. -package eval +package rt import ( "fmt" @@ -16,6 +16,10 @@ type Pointer struct { var _ fmt.Stringer = (*Pointer)(nil) +func NewPointer(obj *Object, readonly bool) *Pointer { + return &Pointer{obj: obj, readonly: readonly} +} + func (ptr *Pointer) Readonly() bool { return ptr.readonly } func (ptr *Pointer) Obj() *Object { return ptr.obj } diff --git a/pkg/compiler/eval/scope.go b/pkg/eval/scope.go similarity index 89% rename from pkg/compiler/eval/scope.go rename to pkg/eval/scope.go index 478c017a5..005c156b9 100644 --- a/pkg/compiler/eval/scope.go +++ b/pkg/eval/scope.go @@ -6,6 +6,7 @@ import ( "github.com/marapongo/mu/pkg/compiler/binder" "github.com/marapongo/mu/pkg/compiler/symbols" "github.com/marapongo/mu/pkg/compiler/types" + "github.com/marapongo/mu/pkg/eval/rt" "github.com/marapongo/mu/pkg/util/contract" ) @@ -19,7 +20,7 @@ type localScope struct { } // valueMap maps local variables to their current known object value (if any). -type valueMap map[*symbols.LocalVariable]*Pointer +type valueMap map[*symbols.LocalVariable]*rt.Pointer func newLocalScope(slot **localScope, frame bool, lex *binder.Scope) *localScope { s := &localScope{ @@ -45,7 +46,7 @@ func (s *localScope) Pop() { } // GetValue returns the object value for the given symbol. -func (s *localScope) GetValue(sym *symbols.LocalVariable) *Object { +func (s *localScope) GetValue(sym *symbols.LocalVariable) *rt.Object { if ref := s.GetValueAddr(sym, false); ref != nil { return ref.Obj() } @@ -54,17 +55,17 @@ func (s *localScope) GetValue(sym *symbols.LocalVariable) *Object { // GetValueAddr returns a reference to the object for the given symbol. If init is true, and the value doesn't // exist, a new slot will be allocated. Otherwise, the return value is nil. -func (s *localScope) GetValueAddr(sym *symbols.LocalVariable, init bool) *Pointer { +func (s *localScope) GetValueAddr(sym *symbols.LocalVariable, init bool) *rt.Pointer { return s.lookupValueAddr(sym, nil, init) } // InitValue registers a reference for a local variable, and asserts that none previously existed. -func (s *localScope) InitValueAddr(sym *symbols.LocalVariable, ref *Pointer) { +func (s *localScope) InitValueAddr(sym *symbols.LocalVariable, ref *rt.Pointer) { s.lookupValueAddr(sym, ref, false) } // lookupValueAddr is used to lookup and initialize references using a single, shared routine. -func (s *localScope) lookupValueAddr(sym *symbols.LocalVariable, place *Pointer, init bool) *Pointer { +func (s *localScope) lookupValueAddr(sym *symbols.LocalVariable, place *rt.Pointer, init bool) *rt.Pointer { // To get a value's reference, we must first find the position in the shadowed frames, so that its lifetime equals // the actual local variable symbol's lifetime. This ensures that once that frame is popped, so too is any value // associated with it; and similarly, that its value won't be popped until the frame containing the variable is. @@ -94,7 +95,7 @@ outer: return place } if init { - ref := &Pointer{} + ref := rt.NewPointer(nil, sym.Readonly()) s.Values[sym] = ref return ref } @@ -102,7 +103,7 @@ outer: } // SetValue overwrites the current value, or adds a new entry, for the given symbol. -func (s *localScope) SetValue(sym *symbols.LocalVariable, obj *Object) { +func (s *localScope) SetValue(sym *symbols.LocalVariable, obj *rt.Object) { contract.Assert(obj == nil || types.CanConvert(obj.Type(), sym.Type())) ptr := s.GetValueAddr(sym, true) ptr.Set(obj) diff --git a/pkg/compiler/eval/unwind.go b/pkg/eval/unwind.go similarity index 78% rename from pkg/compiler/eval/unwind.go rename to pkg/eval/unwind.go index 1e9eec872..e3ff8ec83 100644 --- a/pkg/compiler/eval/unwind.go +++ b/pkg/eval/unwind.go @@ -3,6 +3,7 @@ package eval import ( + "github.com/marapongo/mu/pkg/eval/rt" "github.com/marapongo/mu/pkg/tokens" "github.com/marapongo/mu/pkg/util/contract" ) @@ -11,8 +12,8 @@ import ( type Unwind struct { kind unwindKind // the kind of the unwind. label *tokens.Name // a label being sought (valid only on break/continue). - returned *Object // an object being returned (valid only on return). - thrown *Object // an exception object being thrown (valid only on throw). + returned *rt.Object // an object being returned (valid only on return). + thrown *rt.Object // an exception object being thrown (valid only on throw). } // unwindKind is the kind of unwind being performed. @@ -27,8 +28,8 @@ const ( func NewBreakUnwind(label *tokens.Name) *Unwind { return &Unwind{kind: breakUnwind, label: label} } func NewContinueUnwind(label *tokens.Name) *Unwind { return &Unwind{kind: continueUnwind, label: label} } -func NewReturnUnwind(ret *Object) *Unwind { return &Unwind{kind: returnUnwind, returned: ret} } -func NewThrowUnwind(thrown *Object) *Unwind { return &Unwind{kind: throwUnwind, thrown: thrown} } +func NewReturnUnwind(ret *rt.Object) *Unwind { return &Unwind{kind: returnUnwind, returned: ret} } +func NewThrowUnwind(thrown *rt.Object) *Unwind { return &Unwind{kind: throwUnwind, thrown: thrown} } func (uw *Unwind) Break() bool { return uw.kind == breakUnwind } func (uw *Unwind) Continue() bool { return uw.kind == continueUnwind } @@ -40,12 +41,12 @@ func (uw *Unwind) Label() *tokens.Name { return uw.label } -func (uw *Unwind) Returned() *Object { +func (uw *Unwind) Returned() *rt.Object { contract.Assert(uw.Return()) return uw.returned } -func (uw *Unwind) Thrown() *Object { +func (uw *Unwind) Thrown() *rt.Object { contract.Assert(uw.Throw()) return uw.thrown } diff --git a/pkg/graph/graphgen/generator.go b/pkg/graph/graphgen/generator.go index a82651634..2a19a3790 100644 --- a/pkg/graph/graphgen/generator.go +++ b/pkg/graph/graphgen/generator.go @@ -5,10 +5,11 @@ package graphgen import ( "github.com/marapongo/mu/pkg/compiler/core" - "github.com/marapongo/mu/pkg/compiler/eval" "github.com/marapongo/mu/pkg/compiler/symbols" "github.com/marapongo/mu/pkg/compiler/types" "github.com/marapongo/mu/pkg/compiler/types/predef" + "github.com/marapongo/mu/pkg/eval" + "github.com/marapongo/mu/pkg/eval/rt" "github.com/marapongo/mu/pkg/graph" "github.com/marapongo/mu/pkg/util/contract" ) @@ -33,20 +34,43 @@ type generator struct { } // objectSet is a set of object pointers; each entry has a ref-count to track how many occurrences it contains. -type objectSet map[*eval.Object]int +type objectSet map[*rt.Object]int // dependsSet is a map of object pointers to the objectSet containing the set of objects each such object depends upon. -type dependsSet map[*eval.Object]objectSet +type dependsSet map[*rt.Object]objectSet var _ Generator = (*generator)(nil) // Graph takes the information recorded thus far and produces a new MuGL graph from it. func (g *generator) Graph() graph.Graph { - return nil + // First create vertices for all objects. + verts := make(map[*rt.Object]*objectVertex) + for o := range g.res { + verts[o] = newObjectVertex(o) + } + + // Now create edges connecting all vertices along dependency lines. + // TODO: detect and issue errors about cycles. + for o, deps := range g.res { + for dep := range deps { + verts[o].AddEdge(verts[dep]) + } + } + + // Finally, find all vertices that do not have any incoming edges, and consider them roots. + var roots []graph.Vertex + for _, vert := range verts { + if len(vert.Ins()) == 0 { + roots = append(roots, vert) + } + } + contract.Assert(len(roots) > 0) // should have been detected with the DAG algorithm above. + + return newObjectGraph(roots) } // OnNewObject is called whenever a new object has been allocated. -func (g *generator) OnNewObject(o *eval.Object) { +func (g *generator) OnNewObject(o *rt.Object) { contract.Assert(o != nil) // We only care about subclasses of the mu.Resource type; all others are "just" data/computation. if types.HasBaseName(o.Type(), predef.MuResourceClass) { @@ -59,7 +83,7 @@ func (g *generator) OnNewObject(o *eval.Object) { } // OnAssignProperty is called whenever a property has been (re)assigned; it receives both the old and new values. -func (g *generator) OnAssignProperty(o *eval.Object, prop string, old *eval.Object, nw *eval.Object) { +func (g *generator) OnAssignProperty(o *rt.Object, prop string, old *rt.Object, nw *rt.Object) { contract.Assert(o != nil) // If the target of the assignment is a resource, we need to track dependencies. @@ -72,12 +96,13 @@ func (g *generator) OnAssignProperty(o *eval.Object, prop string, old *eval.Obje c, has := deps[old] contract.Assertf(has, "Expected old resource property to exist in dependency map") contract.Assertf(c > 0, "Expected old resource property ref-count to be > 0 in dependency map") - deps[old]-- + deps[old] = c - 1 } // If the new object is a resource, add a ref-count (or a whole new entry if needed). if nw != nil && types.HasBaseName(nw.Type(), predef.MuResourceClass) { if c, has := deps[nw]; has { + contract.Assertf(c >= 0, "Expected old resource property ref-count to be >= 0 in dependency map") deps[nw] = c + 1 } else { deps[nw] = 1 diff --git a/pkg/graph/graphgen/graph.go b/pkg/graph/graphgen/graph.go new file mode 100644 index 000000000..ce4a8fcda --- /dev/null +++ b/pkg/graph/graphgen/graph.go @@ -0,0 +1,42 @@ +// Copyright 2016 Marapongo, Inc. All rights reserved. + +package graphgen + +import ( + "github.com/marapongo/mu/pkg/eval/rt" + "github.com/marapongo/mu/pkg/graph" +) + +type objectGraph struct { + roots []graph.Vertex +} + +var _ graph.Graph = (*objectGraph)(nil) + +func newObjectGraph(roots []graph.Vertex) *objectGraph { + return &objectGraph{roots: roots} +} + +func (v *objectGraph) Roots() []graph.Vertex { return v.roots } + +type objectVertex struct { + obj *rt.Object // this vertex's object. + ins []graph.Vertex // edges connecting from other vertices into this vertex. + outs []graph.Vertex // edges connecting this vertex to other vertices. +} + +var _ graph.Vertex = (*objectVertex)(nil) + +func newObjectVertex(obj *rt.Object) *objectVertex { + return &objectVertex{obj: obj} +} + +func (v *objectVertex) Obj() *rt.Object { return v.obj } +func (v *objectVertex) Ins() []graph.Vertex { return v.ins } +func (v *objectVertex) Outs() []graph.Vertex { return v.outs } + +// AddEdge creates an edge connecting the receiver vertex to the argument vertex. +func (v *objectVertex) AddEdge(to *objectVertex) { + v.outs = append(v.outs, to) // outgoing from this vertex to the other. + to.ins = append(to.ins, v) // incoming from this vertex to the other. +} diff --git a/pkg/graph/vertex.go b/pkg/graph/vertex.go index 9181df48f..58efd0ee6 100644 --- a/pkg/graph/vertex.go +++ b/pkg/graph/vertex.go @@ -3,20 +3,12 @@ package graph import ( - "github.com/marapongo/mu/pkg/tokens" + "github.com/marapongo/mu/pkg/eval/rt" ) // Vertex is a single vertex within an overall MuGL graph. type Vertex interface { - Type() tokens.Type // the type of the node. - Properties() map[tokens.Name]VertexProperty // a complete set of properties, known and unknown. - Edges() []Vertex // other nodes that this node depends upon. -} - -// VertexProperty represents a single property associated with a node. -type VertexProperty interface { - Name() tokens.Variable // the property's name. - Type() tokens.Type // the type of this property's value. - Value() *interface{} // the value of this property, or nil if unknown. - Computed() bool // true if this property's value is unknown because it will be computed. + Obj() *rt.Object // the vertex's object. + Ins() []Vertex // incoming edges from other vertices within the graph to this vertex. + Outs() []Vertex // outgoing edges from this vertex to other vertices within the graph. }