Support config maps

This change adds support for configuration maps.

This is a new feature that permits initialization code to come from markup,
after compilation, but before evaluation.  There is nothing special with this
code as it could have been authored by a user.  But it offers a convenient
way to specialize configuration settings per target husk, without needing
to write code to specialize each of those husks (which is needlessly complex).

For example, let's say we want to have two husks, one in AWS's us-west-1
region, and the other in us-east-2.  From the same source package, we can
just create two husks, let's say "prod-west" and "prod-east":

    prod-west.json:
    {
        "husk": "prod-west",
        "config": {
            "aws:config:region": "us-west-1"
        }
    }

    prod-east.json:
    {
        "husk": "prod-east",
        "config": {
            "aws:config:region": "us-east-2"
        }
    }

Now when we evaluate these packages, they will automatically poke the
right configuration variables in the AWS package *before* actually
evaluating the CocoJS package contents.  As a result, the static variable
"region" in the "aws:config" package will have the desired value.

This is obviously fairly general purpose, but will allow us to experiment
with different schemes and patterns.  Also, I need to whip up support
for secrets, but that is a task for another day (perhaps tomorrow).
This commit is contained in:
joeduffy 2017-02-27 19:43:54 -08:00
parent 2c6616831f
commit d91b04d8f4
9 changed files with 290 additions and 156 deletions

View file

@ -32,7 +32,7 @@ func newEvalCmd() *cobra.Command {
"a path to a blueprint elsewhere can be provided as the [blueprint] argument.",
Run: func(cmd *cobra.Command, args []string) {
// Perform the compilation and, if non-nil is returned, output the graph.
if result := compile(cmd, args); result != nil {
if result := compile(cmd, args, nil); result != nil {
// Serialize that evaluation graph so that it's suitable for printing/serializing.
if dotOutput {
// Convert the output to a DOT file.

View file

@ -103,7 +103,7 @@ func create(husk tokens.QName) {
// compile just uses the standard logic to parse arguments, options, and to locate/compile a package. It returns the
// CocoGL graph that is produced, or nil if an error occurred (in which case, we would expect non-0 errors).
func compile(cmd *cobra.Command, args []string) *compileResult {
func compile(cmd *cobra.Command, args []string, config *resource.ConfigMap) *compileResult {
// If there's a --, we need to separate out the command args from the stack args.
flags := cmd.Flags()
dashdash := flags.ArgsLenAtDash()
@ -117,6 +117,12 @@ func compile(cmd *cobra.Command, args []string) *compileResult {
opts := core.DefaultOptions()
opts.Args = dashdashArgsToMap(packArgs)
// Create the preexec hook if the config map is non-nil.
var preexec compiler.Preexec
if config != nil {
preexec = config.ApplyConfig
}
// In the case of an argument, load that specific package and new up a compiler based on its base path.
// Otherwise, use the default workspace and package logic (which consults the current working directory).
var comp compiler.Compiler
@ -130,7 +136,7 @@ func compile(cmd *cobra.Command, args []string) *compileResult {
sink().Errorf(errors.ErrorCantCreateCompiler, err)
return nil
}
pkg, heap = comp.Compile()
pkg, heap = comp.Compile(preexec)
} else {
fn := args[0]
if pkgmeta := cmdutil.ReadPackageFromArg(fn); pkgmeta != nil {
@ -144,7 +150,7 @@ func compile(cmd *cobra.Command, args []string) *compileResult {
sink().Errorf(errors.ErrorCantReadPackage, fn, err)
return nil
}
pkg, heap = comp.CompilePackage(pkgmeta)
pkg, heap = comp.CompilePackage(pkgmeta, preexec)
}
}
return &compileResult{comp, pkg, heap}
@ -163,7 +169,7 @@ func plan(cmd *cobra.Command, info *huskCmdInfo, delete bool) *planResult {
var result *compileResult
if !delete {
// First, compile; if that yields errors or an empty heap, exit early.
if result = compile(cmd, info.args); result == nil || result.Heap == nil {
if result = compile(cmd, info.args, info.dep.Config); result == nil || result.Heap == nil {
return nil
}

View file

@ -13,6 +13,7 @@ import (
"github.com/pulumi/coconut/pkg/compiler/errors"
"github.com/pulumi/coconut/pkg/compiler/symbols"
"github.com/pulumi/coconut/pkg/compiler/types"
"github.com/pulumi/coconut/pkg/diag"
"github.com/pulumi/coconut/pkg/tokens"
"github.com/pulumi/coconut/pkg/util/contract"
)
@ -101,17 +102,17 @@ func (ctx *Context) LookupClassMember(t symbols.Type, clm tokens.ClassMemberName
// LookupSymbol performs a complex lookup for a complex token; if require is true, failed lookups will issue an
// error; and in any case, the AST node is used as the context for errors (lookup, accessibility, or otherwise).
func (ctx *Context) LookupSymbol(node ast.Node, tok tokens.Token, require bool) symbols.Symbol {
func (ctx *Context) LookupSymbol(node diag.Diagable, tok tokens.Token, require bool) symbols.Symbol {
return ctx.lookupSymbolCore(node, tok, require, true)
}
// LookupShallowSymbol is just like LookupSymbol, except that it will not resolve exports fully. As a result, the
// returned symbol may not be bound to the actual underlying class member, and may return a symbols.Export instead.
func (ctx *Context) LookupShallowSymbol(node ast.Node, tok tokens.Token, require bool) symbols.Symbol {
func (ctx *Context) LookupShallowSymbol(node diag.Diagable, tok tokens.Token, require bool) symbols.Symbol {
return ctx.lookupSymbolCore(node, tok, require, false)
}
func (ctx *Context) lookupSymbolCore(node ast.Node, tok tokens.Token,
func (ctx *Context) lookupSymbolCore(node diag.Diagable, tok tokens.Token,
require bool, resolveExports bool) symbols.Symbol {
contract.Require(node != nil, "Node")
@ -242,7 +243,7 @@ func (ctx *Context) LookupPackageSymbol(name tokens.PackageName) *symbols.Packag
}
// LookupTypeToken binds a type token to its symbol, creating elements if needed. The node context is used for errors.
func (ctx *Context) LookupTypeToken(node ast.Node, tok tokens.Type, require bool) symbols.Type {
func (ctx *Context) LookupTypeToken(node diag.Diagable, tok tokens.Type, require bool) symbols.Type {
contract.Require(node != nil, "node")
var ty symbols.Type // the type, if any.
@ -305,7 +306,7 @@ func (ctx *Context) LookupFunctionType(node ast.Function) *symbols.FunctionType
}
// lookupBasicType handles decorated types (pointers, arrays, maps, functions) and primitives.
func (ctx *Context) lookupBasicType(node ast.Node, tok tokens.Type, require bool) symbols.Type {
func (ctx *Context) lookupBasicType(node diag.Diagable, tok tokens.Type, require bool) symbols.Type {
contract.Require(node != nil, "node")
contract.Requiref(tok.Primitive() || tok.Decorated(), "tok", "Primitive() || Decorated()")
@ -358,7 +359,7 @@ func (ctx *Context) lookupBasicType(node ast.Node, tok tokens.Type, require bool
return types.Error
}
func (ctx *Context) checkClassVisibility(node ast.Node, class symbols.Type, member symbols.ClassMember) {
func (ctx *Context) checkClassVisibility(node diag.Diagable, class symbols.Type, member symbols.ClassMember) {
acc := member.MemberNode().GetAccess()
if acc == nil {
a := tokens.PrivateAccessibility // private is the default.

View file

@ -29,11 +29,11 @@ type Compiler interface {
Workspace() workspace.W // the workspace that this compielr is using.
// Compile detects a package from the workspace and compiles it into a graph.
Compile() (*symbols.Package, *heapstate.Heap)
Compile(preexec Preexec) (*symbols.Package, *heapstate.Heap)
// CompilePath compiles a package given its path into its graph form.
CompilePath(path string) (*symbols.Package, *heapstate.Heap)
CompilePath(path string, preexec Preexec) (*symbols.Package, *heapstate.Heap)
// CompilePackage compiles a given package object into its associated graph form.
CompilePackage(pkg *pack.Package) (*symbols.Package, *heapstate.Heap)
CompilePackage(pkg *pack.Package, preexec Preexec) (*symbols.Package, *heapstate.Heap)
// Verify detects a package from the workspace and validates its CocoIL contents.
Verify() bool
// VerifyPath verifies a package given its path, validating that its CocoIL contents are correct.
@ -42,6 +42,9 @@ type Compiler interface {
VerifyPackage(pkg *pack.Package) bool
}
// Preexec can be used to hook compilation after binding, but before evaluation, for any pre-evaluation steps.
type Preexec func(*binder.Context, *symbols.Package, eval.Interpreter)
// compiler is the canonical implementation of the Coconut compiler.
type compiler struct {
w workspace.W
@ -105,23 +108,23 @@ func (c *compiler) Diag() diag.Sink { return c.ctx.Diag }
func (c *compiler) Workspace() workspace.W { return c.w }
// Compile attempts to detect the package from the current working directory and, provided that succeeds, compiles it.
func (c *compiler) Compile() (*symbols.Package, *heapstate.Heap) {
func (c *compiler) Compile(preexec Preexec) (*symbols.Package, *heapstate.Heap) {
if path := c.detectPackage(); path != "" {
return c.CompilePath(path)
return c.CompilePath(path, preexec)
}
return nil, nil
}
// CompilePath loads a package at the given path and compiles it into a graph.
func (c *compiler) CompilePath(path string) (*symbols.Package, *heapstate.Heap) {
func (c *compiler) CompilePath(path string, preexec Preexec) (*symbols.Package, *heapstate.Heap) {
if pkg := c.readPackage(path); pkg != nil {
return c.CompilePackage(pkg)
return c.CompilePackage(pkg, preexec)
}
return nil, nil
}
// CompilePackage compiles the given package into a graph.
func (c *compiler) CompilePackage(pkg *pack.Package) (*symbols.Package, *heapstate.Heap) {
func (c *compiler) CompilePackage(pkg *pack.Package, preexec Preexec) (*symbols.Package, *heapstate.Heap) {
contract.Requiref(pkg != nil, "pkg", "!= nil")
glog.Infof("Compiling package '%v' (w=%v)", pkg.Name, c.w.Root())
if glog.V(2) {
@ -157,8 +160,16 @@ func (c *compiler) CompilePackage(pkg *pack.Package) (*symbols.Package, *heapsta
// Now, create the machinery we need to generate a graph.
gg := heapstate.New(c.ctx)
// Now, evaluate.
// Create a fresh evaluator; if there are pre-exec hooks, run them now.
e := eval.New(b.Ctx(), gg)
if preexec != nil {
preexec(b.Ctx(), pkgsym, e)
}
if !c.Diag().Success() {
return nil, nil // the preexec functions raised errors; quit.
}
// Go ahead and perform the evaluation.
e.EvaluatePackage(pkgsym, c.ctx.Opts.Args)
if !c.Diag().Success() {
return pkgsym, nil

View file

@ -12,4 +12,6 @@ var (
ErrorCantReadDeployment = newError(2005, "Could not read deployment file '%v': %v")
ErrorDuplicateMonikerNames = newError(2006, "Duplicate objects with the same name: %v")
ErrorInvalidHuskName = newError(2007, "Invalid husk '%v'; could not be found in the workspace")
ErrorIllegalConfigToken = newError(2008,
"Configs may only target module properties and class static properties; %v is neither")
)

View file

@ -34,6 +34,12 @@ type Interpreter interface {
EvaluateModule(mod *symbols.Module, args core.Args)
// EvaluateFunction performs an evaluation of the given function, using the provided arguments.
EvaluateFunction(fnc symbols.Function, this *rt.Object, args core.Args)
// 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
// NewConstantObject creates a new constant object in the context of this interpreter.
NewConstantObject(tree diag.Diagable, v interface{}) *rt.Object
}
// InterpreterHooks is a set of callbacks that can be used to hook into interesting interpreter events.
@ -470,6 +476,11 @@ func (e *evaluator) newObject(tree diag.Diagable, t symbols.Type) *rt.Object {
return e.alloc.New(tree, t, nil, proto)
}
// NewConstantObject is a public function that allocates an object with the given state.
func (e *evaluator) NewConstantObject(tree diag.Diagable, v interface{}) *rt.Object {
return e.alloc.NewConstant(tree, v)
}
// issueUnhandledException issues an unhandled exception error using the given diagnostic and unwind information.
func (e *evaluator) issueUnhandledException(uw *rt.Unwind, err *diag.Diag, args ...interface{}) {
contract.Assert(uw.Throw())
@ -979,7 +990,7 @@ func (e *evaluator) evalExpression(node ast.Expression) (*rt.Object, *rt.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) (location, *rt.Unwind) {
func (e *evaluator) evalLValueExpression(node ast.Expression) (*Location, *rt.Unwind) {
switch n := node.(type) {
case *ast.LoadLocationExpression:
return e.evalLoadLocation(n, true)
@ -988,10 +999,10 @@ func (e *evaluator) evalLValueExpression(node ast.Expression) (location, *rt.Unw
case *ast.UnaryOperatorExpression:
contract.Assert(n.Operator == ast.OpDereference)
obj, uw := e.evalUnaryOperatorExpressionFor(n, true)
return location{Obj: obj}, uw
return &Location{e: e, Obj: obj}, uw
default:
contract.Failf("Unrecognized l-value expression type: %v", node.GetKind())
return location{}, nil
return nil, nil
}
}
@ -1114,18 +1125,6 @@ func (e *evaluator) evalObjectLiteral(node *ast.ObjectLiteral) (*rt.Object, *rt.
return obj, nil
}
func (e *evaluator) evalLoadLocationExpression(node *ast.LoadLocationExpression) (*rt.Object, *rt.Unwind) {
loc, uw := e.evalLoadLocation(node, false)
return loc.Obj, uw
}
type location struct {
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).
}
// 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(
@ -1160,9 +1159,61 @@ func (e *evaluator) getObjectOrSuperProperty(
return obj.GetPropertyAddr(k, init, forWrite)
}
func (e *evaluator) evalLoadLocationExpression(node *ast.LoadLocationExpression) (*rt.Object, *rt.Unwind) {
loc, uw := e.evalLoadLocation(node, false)
return loc.Obj, uw
}
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 = e.alloc.NewPointer(node, ty, pv)
} else {
obj = pv.Obj()
}
if glog.V(9) {
glog.V(9).Infof("Loaded location of type '%v' from symbol '%v': lval=%v current=%v",
ty.Token(), sym.Token(), lval, pv.Obj())
}
return &Location{
e: e,
This: this,
Name: sym.Name(),
Lval: lval,
Obj: obj,
}
}
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).
}
func (loc *Location) Assign(node diag.Diagable, val *rt.Object) {
// Perform the assignment, but make sure to invoke the property assignment hook if necessary.
ptr := loc.Obj.PointerValue()
// If the pointer is readonly, however, we will disallow the assignment.
if ptr.Readonly() {
loc.e.Diag().Errorf(errors.ErrorIllegalReadonlyLValue.At(node))
} else {
if loc.e.hooks != nil {
loc.e.hooks.OnVariableAssign(node, loc.This, loc.Name, ptr.Obj(), val)
}
ptr.Set(val)
}
}
// 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) {
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
@ -1170,7 +1221,7 @@ func (e *evaluator) evalLoadLocation(node *ast.LoadLocationExpression, lval bool
thisexpr = *node.Object
var uw *rt.Unwind
if this, uw = e.evalExpression(thisexpr); uw != nil {
return location{}, uw
return nil, uw
}
}
@ -1189,87 +1240,88 @@ func (e *evaluator) evalLoadLocation(node *ast.LoadLocationExpression, lval bool
sym = loc
} else {
sym = e.ctx.LookupSymbol(node.Name, tok, false)
contract.Assert(sym != nil) // don't issue errors; we shouldn't ever get here if verification failed.
sym = MaybeIntrinsic(node, sym) // replace this with an intrinsic if it's recognized as one.
// If the symbol is an export, keep chasing down the referents until we hit a real 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)
var uw *rt.Unwind
pv, ty, uw = e.evalLoadSymbolLocation(node, sym, this, thisexpr, lval)
if uw != nil {
return nil, uw
}
}
// 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`.
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)
class := s.MemberParent()
e.ensureClassInit(class) // ensure the class is initialized.
statics := e.getClassStatics(class)
pv = statics.GetAddr(k, false)
} else {
contract.Assert(this != nil)
if uw := e.checkThis(node, this); uw != nil {
return location{}, uw
}
dynload := this.Type() == types.Dynamic
pv = e.getObjectOrSuperProperty(this, thisexpr, k, dynload, lval)
}
contract.Assertf(pv != nil, "Missing class member '%v' (static=%v)", s.Token(), s.Static())
ty = s.Type()
contract.Assert(ty != nil)
case symbols.ModuleMemberProperty:
module := s.MemberParent()
return e.newLocation(node, sym, pv, ty, this, lval), nil
}
// Ensure this module has been initialized.
e.ensureModuleInit(module)
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 = MaybeIntrinsic(node, sym) // replace this with an intrinsic if it's recognized as one.
// Search the globals table for this module's members.
// If the symbol is an export, keep chasing down the referents until we hit a real 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)
}
// 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)
k := rt.PropertyKey(s.Name())
globals := e.getModuleGlobals(module)
pv = globals.GetAddr(k, false)
contract.Assertf(pv != nil, "Missing module member '%v'", s.Token())
ty = s.MemberType()
contract.Assert(ty != nil)
default:
contract.Failf("Unexpected symbol token '%v' kind during load expression: %v",
tok, reflect.TypeOf(sym))
class := s.MemberParent()
e.ensureClassInit(class) // ensure the class is initialized.
statics := e.getClassStatics(class)
pv = statics.GetAddr(k, false)
} else {
contract.Assert(this != nil)
if uw := e.checkThis(node, this); uw != nil {
return nil, nil, uw
}
dynload := this.Type() == types.Dynamic
pv = e.getObjectOrSuperProperty(this, thisexpr, k, dynload, lval)
}
contract.Assertf(pv != nil, "Missing class member '%v' (static=%v)", s.Token(), s.Static())
ty = s.Type()
contract.Assert(ty != nil)
case symbols.ModuleMemberProperty:
module := s.MemberParent()
// Ensure this module has been initialized.
e.ensureModuleInit(module)
// Search the globals table for this module's members.
contract.Assert(this == nil)
k := rt.PropertyKey(s.Name())
globals := e.getModuleGlobals(module)
pv = globals.GetAddr(k, false)
contract.Assertf(pv != nil, "Missing module member '%v'", s.Token())
ty = s.MemberType()
contract.Assert(ty != nil)
default:
contract.Failf("Unexpected symbol token '%v' kind during load expression: %v",
sym.Token(), reflect.TypeOf(sym))
}
// If this is an l-value, return a pointer to the object; otherwise, return the raw object.
var obj *rt.Object
if lval {
obj = e.alloc.NewPointer(node, ty, pv)
} else {
obj = pv.Obj()
}
return pv, ty, nil
}
if glog.V(9) {
glog.V(9).Infof("Loaded location of type '%v' from symbol '%v': lval=%v current=%v",
ty.Token(), sym.Token(), lval, pv.Obj())
}
return location{
This: this,
Name: sym.Name(),
Lval: lval,
Obj: obj,
}, nil
func (e *evaluator) LoadLocation(tree diag.Diagable, sym symbols.Symbol, this *rt.Object, lval bool) *Location {
pv, ty, uw := e.evalLoadSymbolLocation(tree, sym, this, nil, lval)
contract.Assertf(uw == nil, "Unexpected unwind; possible nil 'this' for instance method")
return e.newLocation(tree, sym, pv, ty, this, lval)
}
// checkThis checks a this object, raising a runtime error if it is the runtime null value.
@ -1286,22 +1338,22 @@ func (e *evaluator) evalLoadDynamicExpression(node *ast.LoadDynamicExpression) (
return loc.Obj, uw
}
func (e *evaluator) evalLoadDynamic(node *ast.LoadDynamicExpression, lval bool) (location, *rt.Unwind) {
func (e *evaluator) evalLoadDynamic(node *ast.LoadDynamicExpression, lval bool) (*Location, *rt.Unwind) {
var uw *rt.Unwind
// Evaluate the object and then the property expression.
var this *rt.Object
if this, uw = e.evalExpression(node.Object); uw != nil {
return location{}, uw
return nil, uw
}
var name *rt.Object
if name, uw = e.evalExpression(node.Name); uw != nil {
return location{}, uw
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 location{}, uw
return nil, uw
}
// Now go ahead and search the object for a property with the given name.
@ -1351,7 +1403,8 @@ func (e *evaluator) evalLoadDynamic(node *ast.LoadDynamicExpression, lval bool)
}
contract.Assert(obj != nil)
return location{
return &Location{
e: e,
This: this,
Name: tokens.Name(key),
Lval: lval,
@ -1452,7 +1505,7 @@ func (e *evaluator) evalUnaryOperatorExpressionFor(node *ast.UnaryOperatorExpres
// Evaluate the operand and prepare to use it.
var opand *rt.Object
var opandloc *location
var opandloc *Location
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.
@ -1461,7 +1514,7 @@ func (e *evaluator) evalUnaryOperatorExpressionFor(node *ast.UnaryOperatorExpres
return nil, uw
}
opand = loc.Obj
opandloc = &loc
opandloc = loc
} else {
// Otherwise, we just need to evaluate the operand as usual.
var uw *rt.Unwind
@ -1502,7 +1555,7 @@ func (e *evaluator) evalUnaryOperatorExpressionFor(node *ast.UnaryOperatorExpres
old := ptr.Obj()
val := old.NumberValue()
new := e.alloc.NewNumber(node, val+1)
e.evalAssign(node.Operand, *opandloc, new)
opandloc.Assign(node.Operand, new)
if node.Postfix {
return old, nil
}
@ -1513,7 +1566,7 @@ func (e *evaluator) evalUnaryOperatorExpressionFor(node *ast.UnaryOperatorExpres
old := ptr.Obj()
val := old.NumberValue()
new := e.alloc.NewNumber(node, val-1)
e.evalAssign(node.Operand, *opandloc, new)
opandloc.Assign(node.Operand, new)
if node.Postfix {
return old, nil
}
@ -1527,14 +1580,13 @@ func (e *evaluator) evalUnaryOperatorExpressionFor(node *ast.UnaryOperatorExpres
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
var lhsloc *Location
if isBinaryAssignmentOperator(node.Operator) {
loc, uw := e.evalLValueExpression(node.Left)
if uw != nil {
var uw *rt.Unwind
if lhsloc, uw = e.evalLValueExpression(node.Left); uw != nil {
return nil, uw
}
lhs = loc.Obj
lhsloc = &loc
lhs = lhsloc.Obj
} else {
var uw *rt.Unwind
if lhs, uw = e.evalExpression(node.Left); uw != nil {
@ -1613,7 +1665,7 @@ func (e *evaluator) evalBinaryOperatorExpression(node *ast.BinaryOperatorExpress
// Assignment operators
case ast.OpAssign:
// The target is an l-value; just overwrite its value, and yield the new value as the result.
e.evalAssign(node.Left, *lhsloc, rhs)
lhsloc.Assign(node.Left, rhs)
return rhs, nil
case ast.OpAssignSum:
var val *rt.Object
@ -1625,67 +1677,67 @@ func (e *evaluator) evalBinaryOperatorExpression(node *ast.BinaryOperatorExpress
// Otherwise, the target is a numeric l-value; just += to it, and yield the new value as the result.
val = e.alloc.NewNumber(node, ptr.Obj().NumberValue()+rhs.NumberValue())
}
e.evalAssign(node.Left, *lhsloc, val)
lhsloc.Assign(node.Left, val)
return val, nil
case ast.OpAssignDifference:
// The target is a numeric l-value; just -= rhs to it, and yield the new value as the result.
ptr := lhs.PointerValue()
val := e.alloc.NewNumber(node, ptr.Obj().NumberValue()-rhs.NumberValue())
e.evalAssign(node.Left, *lhsloc, val)
lhsloc.Assign(node.Left, val)
return val, nil
case ast.OpAssignProduct:
// The target is a numeric l-value; just *= rhs to it, and yield the new value as the result.
ptr := lhs.PointerValue()
val := e.alloc.NewNumber(node, ptr.Obj().NumberValue()*rhs.NumberValue())
e.evalAssign(node.Left, *lhsloc, val)
lhsloc.Assign(node.Left, val)
return val, nil
case ast.OpAssignQuotient:
// The target is a numeric l-value; just /= rhs to it, and yield the new value as the result.
ptr := lhs.PointerValue()
val := e.alloc.NewNumber(node, ptr.Obj().NumberValue()/rhs.NumberValue())
e.evalAssign(node.Left, *lhsloc, val)
lhsloc.Assign(node.Left, val)
return val, nil
case ast.OpAssignRemainder:
// The target is a numeric l-value; just %= rhs to it, and yield the new value as the result.
ptr := lhs.PointerValue()
val := e.alloc.NewNumber(node, float64(int64(ptr.Obj().NumberValue())%int64(rhs.NumberValue())))
e.evalAssign(node.Left, *lhsloc, val)
lhsloc.Assign(node.Left, val)
return val, nil
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.
ptr := lhs.PointerValue()
val := e.alloc.NewNumber(node, math.Pow(ptr.Obj().NumberValue(), rhs.NumberValue()))
e.evalAssign(node.Left, *lhsloc, val)
lhsloc.Assign(node.Left, val)
return val, nil
case ast.OpAssignBitwiseShiftLeft:
// The target is a numeric l-value; just <<= rhs to it, and yield the new value as the result.
ptr := lhs.PointerValue()
val := e.alloc.NewNumber(node, float64(int64(ptr.Obj().NumberValue())<<uint(rhs.NumberValue())))
e.evalAssign(node.Left, *lhsloc, val)
lhsloc.Assign(node.Left, val)
return val, nil
case ast.OpAssignBitwiseShiftRight:
// The target is a numeric l-value; just >>= rhs to it, and yield the new value as the result.
ptr := lhs.PointerValue()
val := e.alloc.NewNumber(node, float64(int64(ptr.Obj().NumberValue())>>uint(rhs.NumberValue())))
e.evalAssign(node.Left, *lhsloc, val)
lhsloc.Assign(node.Left, val)
return val, nil
case ast.OpAssignBitwiseAnd:
// The target is a numeric l-value; just &= rhs to it, and yield the new value as the result.
ptr := lhs.PointerValue()
val := e.alloc.NewNumber(node, float64(int64(ptr.Obj().NumberValue())&int64(rhs.NumberValue())))
e.evalAssign(node.Left, *lhsloc, val)
lhsloc.Assign(node.Left, val)
return val, nil
case ast.OpAssignBitwiseOr:
// The target is a numeric l-value; just |= rhs to it, and yield the new value as the result.
ptr := lhs.PointerValue()
val := e.alloc.NewNumber(node, float64(int64(ptr.Obj().NumberValue())|int64(rhs.NumberValue())))
e.evalAssign(node.Left, *lhsloc, val)
lhsloc.Assign(node.Left, val)
return val, nil
case ast.OpAssignBitwiseXor:
// The target is a numeric l-value; just ^= rhs to it, and yield the new value as the result.
ptr := lhs.PointerValue()
val := e.alloc.NewNumber(node, float64(int64(ptr.Obj().NumberValue())^int64(rhs.NumberValue())))
e.evalAssign(node.Left, *lhsloc, val)
lhsloc.Assign(node.Left, val)
return val, nil
// Relational operators
@ -1725,21 +1777,6 @@ func isBinaryAssignmentOperator(op ast.BinaryOperator) bool {
}
}
func (e *evaluator) evalAssign(node ast.Node, loc location, val *rt.Object) {
// Perform the assignment, but make sure to invoke the property assignment hook if necessary.
ptr := loc.Obj.PointerValue()
// If the pointer is readonly, however, we will disallow the assignment.
if ptr.Readonly() {
e.Diag().Errorf(errors.ErrorIllegalReadonlyLValue.At(node))
} else {
if e.hooks != nil {
e.hooks.OnVariableAssign(node, loc.This, loc.Name, ptr.Obj(), val)
}
ptr.Set(val)
}
}
func (e *evaluator) evalBinaryOperatorEquals(lhs *rt.Object, rhs *rt.Object) bool {
if lhs == rhs {
return true

View file

@ -28,8 +28,8 @@ func init() {
// Intrinsic is a special intrinsic function whose behavior is implemented by the runtime.
type Intrinsic struct {
Node ast.Node // the contextual node representing the place where this intrinsic got created.
Func ast.Function // the underlying function's node (before mapping to an intrinsic).
Node diag.Diagable // the contextual node representing the place where this intrinsic got created.
Func ast.Function // the underlying function's node (before mapping to an intrinsic).
Nm tokens.Name
Tok tokens.Token
Sig *symbols.FunctionType
@ -54,10 +54,10 @@ func (node *Intrinsic) Invoke(e *evaluator, this *rt.Object, args []*rt.Object)
}
// newIntrinsic returns a new intrinsic function symbol with the given information.
func NewIntrinsic(node ast.Node, fnc ast.Function, tok tokens.Token, nm tokens.Name,
func NewIntrinsic(tree diag.Diagable, fnc ast.Function, tok tokens.Token, nm tokens.Name,
sig *symbols.FunctionType, invoker Invoker) *Intrinsic {
return &Intrinsic{
Node: node,
Node: tree,
Func: fnc,
Nm: nm,
Tok: tok,
@ -68,7 +68,7 @@ func NewIntrinsic(node ast.Node, fnc ast.Function, tok tokens.Token, nm tokens.N
// MaybeIntrinsic checks whether the given symbol is an intrinsic and, if so, swaps it out with the actual runtime
// implementation of that intrinsic. If the symbol is not an intrinsic, the original symbol is simply returned.
func MaybeIntrinsic(node ast.Node, sym symbols.Symbol) symbols.Symbol {
func MaybeIntrinsic(tree diag.Diagable, sym symbols.Symbol) symbols.Symbol {
switch s := sym.(type) {
case *Intrinsic:
// Already an intrinsic; do not swap it out.
@ -77,7 +77,7 @@ func MaybeIntrinsic(node ast.Node, sym symbols.Symbol) symbols.Symbol {
// cache these symbols because of the need to associate the AST node with the resulting symbol.
tok := s.Token()
if invoker, isintrinsic := Intrinsics[tok]; isintrinsic {
sym = NewIntrinsic(node, s.Function(), tok, tok.Name(), s.Signature(), invoker)
sym = NewIntrinsic(tree, s.Function(), tok, tok.Name(), s.Signature(), invoker)
}
}
return sym

76
pkg/resource/config.go Normal file
View file

@ -0,0 +1,76 @@
// Copyright 2016 Pulumi, Inc. All rights reserved.
package resource
import (
"sort"
"github.com/pulumi/coconut/pkg/compiler/binder"
"github.com/pulumi/coconut/pkg/compiler/errors"
"github.com/pulumi/coconut/pkg/compiler/symbols"
"github.com/pulumi/coconut/pkg/diag"
"github.com/pulumi/coconut/pkg/eval"
"github.com/pulumi/coconut/pkg/tokens"
)
// ConfigMap contains a mapping from variable token to the value to poke into that variable.
type ConfigMap map[tokens.ModuleMember]interface{}
// ApplyConfig applies the configuration map to an existing interpreter context. The map is simply a map of tokens --
// which must be globally settable variables (module properties or static class properties) -- to serializable constant
// values. The routine simply walks these tokens in sorted order, and assigns the constant objects. Note that, because
// we are accessing module and class members, this routine will also trigger the relevant initialization routines.
func (cfg *ConfigMap) ApplyConfig(ctx *binder.Context, pkg *symbols.Package, e eval.Interpreter) {
if cfg != nil {
// For each config entry, bind the token to its symbol, and then attempt to assign to it.
for _, tok := range stableConfigKeys(*cfg) {
// Bind to the symbol; if it returns nil, this means an error has resulted, and we can skip it.
var tree diag.Diagable = nil // there is no source info for this; eventually we may assign one.
if sym := ctx.LookupSymbol(tree, tokens.Token(tok), true); sym != nil {
switch s := sym.(type) {
case *symbols.ModuleProperty:
// ok
case *symbols.ClassProperty:
// class properties are ok, so long as they are static.
if !s.Static() {
ctx.Diag.Errorf(errors.ErrorIllegalConfigToken, tok)
return
}
default:
ctx.Diag.Errorf(errors.ErrorIllegalConfigToken, tok)
return
}
// Load up the location as an l-value; because we don't support instance properties, this is nil.
if loc := e.LoadLocation(tree, sym, nil, true); loc != nil {
// Allocate a new constant for the value we are about to assign, and assign it to the location.
v := (*cfg)[tok]
loc.Assign(tree, e.NewConstantObject(nil, v))
}
}
}
}
}
func stableConfigKeys(config ConfigMap) []tokens.ModuleMember {
sorted := make(configKeys, 0, len(config))
for key := range config {
sorted = append(sorted, key)
}
sort.Sort(sorted)
return sorted
}
type configKeys []tokens.ModuleMember
func (s configKeys) Len() int {
return len(s)
}
func (s configKeys) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s configKeys) Less(i, j int) bool {
return s[i] < s[j]
}

View file

@ -16,6 +16,7 @@ import (
// Deployment is a serialized deployment target plus a record of the latest deployment.
type Deployment struct {
Husk tokens.QName `json:"husk"` // the target environment name.
Config *ConfigMap `json:"config,omitempty"` // optional configuration key/values.
Latest *DeploymentRecord `json:"latest,omitempty"` // the latest/current deployment record.
}