func canonicalize(x interface{}) (interface{}, error) {
	js, err := json.Marshal(&x)
	if err != nil {
		return nil, err
	}
	var y interface{}
	if err = json.Unmarshal(js, &y); err != nil {
		return nil, err
	}
	return y, nil
}
canonicalize is an abomination
package ecmascript
import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"os"
	"time"
	"github.com/Comcast/sheens/core"
	"github.com/Comcast/sheens/match"
	"github.com/dop251/goja"
	"github.com/gorhill/cronexpr"
)
var (
	InterruptedMessage = "RuntimeError: timeout"
	Interrupted = errors.New(InterruptedMessage)
	IgnoreExit = false
)
func init() {
	core.DefaultInterpreters["ecmascript"] = NewInterpreter()
}
type Interpreter struct {
	Test bool
	Extended bool
}
func NewInterpreter() *Interpreter {
	return &Interpreter{}
}
func wrapSrc(src string) string {
	return fmt.Sprintf("(function() {\n%s\n}());\n", src)
}
func AsSource(src interface{}) (code string, err error) {
	switch vv := src.(type) {
	case string:
		code = vv
		return
	default:
		err = errors.New(fmt.Sprintf("bad ECMAScript source (%T)", src))
		return
	}
}
func (i *Interpreter) Compile(ctx context.Context, src interface{}) (interface{}, error) {
	code, err := AsSource(src)
	if err != nil {
		return nil, err
	}
	code = wrapSrc(code)
	obj, err := goja.Compile("", code, true)
	if err != nil {
		return nil, errors.New(err.Error() + ": " + code)
	}
	return obj, nil
}
func protest(o *goja.Runtime, x interface{}) {
	panic(o.ToValue(x))
}
func deepCopy(x interface{}) (interface{}, error) {
	return core.Canonicalize(x)
}
func (i *Interpreter) Exec(ctx context.Context, bs match.Bindings, props core.StepProps, src interface{}, compiled interface{}) (*core.Execution, error) {
	exe := core.NewExecution(nil)
	var p *goja.Program
	if compiled == nil {
		var err error
		if compiled, err = i.Compile(ctx, src); err != nil {
			return exe, err
		}
	}
	var is bool
	if p, is = compiled.(*goja.Program); !is {
		return exe, fmt.Errorf("ECMAScript bad compilation: %T %#v", compiled, compiled)
	}
	env := map[string]interface{}{
		"ctx": ctx,
	}
	if props == nil {
		env["props"] = map[string]interface{}{}
	} else {
		env["props"] = map[string]interface{}(props.Copy())
	}
	if bs != nil {
		x, err := deepCopy(bs)
		if err != nil {
			return nil, err
		}
		bsCopy, is := x.(map[string]interface{})
		if !is {
			return nil, fmt.Errorf("internal error: %#v copy failed; %s", bs, err)
		}
		env["bindings"] = bsCopy
	}
	o := goja.New()
	o.Set("_", env)
	env["out"] = func(x interface{}) interface{} {
		var err error
		switch vv := x.(type) {
		case goja.Value:
			x = vv.Export()
		}
		if x, err = core.Canonicalize(x); err != nil {
			panic(err)
		}
		exe.AddEmitted(x)
		return x
	}
	if i.Extended {
		env["randstr"] = func() interface{} {
			return core.Gensym(32)
		}
		env["cronNext"] = func(x interface{}) interface{} {
			switch vv := x.(type) {
			case goja.Value:
				x = vv.Export()
			}
			cronExpr, is := x.(string)
			if !is {
				protest(o, "not a string")
			}
			c, err := cronexpr.Parse(cronExpr)
			if err != nil {
				protest(o, err.Error())
			}
			return c.Next(time.Now()).UTC().Format(time.RFC3339Nano)
		}
		env["match"] = func(pat, mess, bs goja.Value) interface{} {
			var bindings match.Bindings
			if bs == nil {
				bindings = match.NewBindings()
			} else {
				x, err := canonicalize(bs.Export())
				if err != nil {
					panic(err)
				}
				var is bool
				m, is := x.(map[string]interface{})
				if !is {
					panic("bad bindings")
				}
				bindings = match.Bindings(m)
			}
			var (
				p   interface{}
				m   interface{}
				err error
			)
			if p, err = canonicalize(pat.Export()); err != nil {
				panic(err)
			}
			if m, err = canonicalize(mess.Export()); err != nil {
				panic(err)
			}
			bss, err := match.Match(p, m, bindings)
			if err != nil {
				panic(err)
			}
			var x interface{}
			if x, err = canonicalize(bss); err != nil {
				panic(err)
			}
			return x
		}
	}
	if i.Test {
		env["sleep"] = func(n interface{}) interface{} {
			switch vv := n.(type) {
			case goja.Value:
				n = vv.Export()
			}
			ms, is := n.(int64)
			if !is {
				panic(fmt.Sprintf("a %T is not an %T", n, ms))
			}
			time.Sleep(time.Duration(ms) * time.Millisecond)
			return nil
		}
		env["log"] = func(x interface{}) interface{} {
			switch vv := x.(type) {
			case goja.Value:
				x = vv.Export()
			}
			js, err := json.Marshal(&x)
			if err != nil {
				log.Println("goja.log (can't marshal: " + err.Error() + ")")
			} else {
				log.Println(string(js))
			}
			return x
		}
		env["exit"] = func(n interface{}, msg interface{}) interface{} {
			switch vv := msg.(type) {
			case goja.Value:
				msg = vv.Export()
			}
			s, is := msg.(string)
			if !is {
				panic("not a string")
			}
			switch vv := n.(type) {
			case goja.Value:
				n = vv.Export()
			}
			ec, is := n.(int64)
			if !is {
				panic(fmt.Sprintf("a %T is not an %T", n, ec))
			}
			log.Println(s)
			if !IgnoreExit {
				os.Exit(int(ec))
			}
			return msg
		}
	}
	ictx, cancel := context.WithCancel(ctx)
	go func() {
		<-ictx.Done()
		o.Interrupt(InterruptedMessage)
	}()
	v, err := RunProgram(o, p)
	cancel()
	if err != nil {
		if _, is := err.(*goja.InterruptedError); is {
			return nil, Interrupted
		}
		return nil, err
	}
	x := v.Export()
	var result match.Bindings
	switch vv := x.(type) {
	case *goja.InterruptedError:
		return nil, vv
	case map[string]interface{}:
		result = match.Bindings(vv)
	case match.Bindings:
		result = vv
	case nil:
	default:
		return nil, fmt.Errorf("%#v (%T) isn't Bindings", x, x)
	}
	exe.Bs = result
	return exe, nil
}