"""
**********************************************************************

  A RapydScript to JavaScript compiler.
  https://github.com/atsepkov/RapydScript

  -------------------------------- (C) ---------------------------------

                       Author: Alexander Tsepkov
                         <atsepkov@pyjeon.com>
                         http://www.pyjeon.com

  Distributed under BSD license:
    Copyright 2013 (c) Alexander Tsepkov <atsepkov@pyjeon.com>

 **********************************************************************
"""

from utils import noop, RAPYD_PREFIX
from tokenizer import PRECEDENCE
import stream
import ast

Stream = stream.Stream

# -----[ code generators ]-----
(def():
    # -----[ utils ]-----
    SPECIAL_METHODS = {
        "bind": "ՐՏ_bind",
        "rebind_all": "ՐՏ_rebindAll",
        "bool": "!!",
        "float": "parseFloat",
        "int": "parseInt",
        "mixin": "ՐՏ_mixin",
        "merge": "ՐՏ_merge",
        "print": "ՐՏ_print",
        "eslice": "ՐՏ_eslice",
        "type": "ՐՏ_type",
    }

    def unify(output, assign, *args):
        # unifies multiple operations to look like part of a single mechanism to JavaScript
        # this logic is used to accomplish an intelligent caching routine, where the base operation is assumed
        # to be primary, that gets cached into a variable reused by the decorator functions
        # in this case we'll be returning (tmp = ...constructor..., modifier1(tmp), modifier2(tmp), ..., tmp)
        args = args.filter(def(i): return i is not None;)
        return def(baseFn):
            if args.length:
                return def():
                    tmp = output.newTemp()
                    if assign: output.assign(assign)
                    output.with_parens(def():
                        output.assign(tmp)
                        baseFn()
                        output.comma()
                        for arg in args:
                            if arg not in [None, undefined]:
                                arg.call(output, tmp)
                                output.comma()
                        # for safety, output the temp var last, in case the last operation failed to return the object itself
                        output.print(tmp)
                    )
                    if assign: output.semicolon() # TODO: eventually this will need to be removed, once we start chaining this with other ops
            else: return def(): baseFn()

    def DEFPRINT(nodetype, generator):
        nodetype.prototype._codegen = generator

    ast.Node.prototype.print = def(stream, force_parens):
        self = this
        generator = self._codegen
        stream.push_node(self)
        if force_parens or self.needs_parens(stream):
            stream.with_parens(def():
                self.add_comments(stream)
                self.add_source_map(stream)
                generator(self, stream)
            )
        else:
            self.add_comments(stream)
            self.add_source_map(stream)
            generator(self, stream)
        stream.pop_node()

    ast.Node.prototype.print_to_string = def(options):
        s = Stream(options)
        this.print(s)
        return s.get()

    # -----[ comments ]-----
    ast.Node.prototype.add_comments = def(output):
        c = output.option("comments")
        self = this
        if c:
            start = self.start
            if start and not start._comments_dumped:
                start._comments_dumped = True
                comments = start.comments_before
                # XXX: ugly fix for https://github.com/mishoo/RapydScript2/issues/112
                #      if this node is `return` or `throw`, we cannot allow comments before
                #      the returned or thrown value.
                if isinstance(self, ast.Exit) and self.value and self.value.start.comments_before and self.value.start.comments_before.length > 0:
                    comments = (comments or []).concat(self.value.start.comments_before)
                    self.value.start.comments_before = []

                if c.test:
                    comments = comments.filter(def(comment):
                        return c.test(comment.value)
                    )
                elif JS('typeof c') is "function":
                    comments = comments.filter(def(comment):
                        return c(self, comment)
                    )

                for c in comments:
                    if c.type is "comment:line":
                        output.print("//" + c.value + "\n")
                        output.indent()
                    elif c.type is "comment:multiline":
                        output.print("/*" + c.value + "*/")
                        if start.newline_before:
                            output.print("\n")
                            output.indent()
                        else:
                            output.space()

    # -----[ PARENTHESES ]-----
    def PARENS(nodetype, func):
        nodetype.prototype.needs_parens = func

    PARENS(ast.Node, def():
        return False
    )
    # a function expression needs parens around it when it's provably
    # the first token to appear in a statement.
    PARENS(ast.Function, def(output):
        return first_in_statement(output)
    )
    # same goes for an object literal, because otherwise it would be
    # interpreted as a block of code.
    PARENS(ast.ObjectLiteral, def(output):
        return first_in_statement(output)
    )
    PARENS(ast.Unary, def(output):
        p = output.parent()
        return isinstance(p, ast.PropAccess) and p.expression is this
    )
    PARENS(ast.Seq, def(output):
        p = output.parent()
        return isinstance(p, ast.Unary) or isinstance(p, ast.VarDef) or isinstance(p, ast.Dot) or isinstance(p, ast.ObjectProperty) or isinstance(p, ast.Conditional)
    )
    PARENS(ast.Range, def(output):
        return False
    )
    PARENS(ast.Binary, def(output):
        p = output.parent()
        # (foo && bar)()
        if isinstance(p, ast.BaseCall) and p.expression is this:
            return True

        # typeof (foo && bar)
        if isinstance(p, ast.Unary):
            return True

        # (foo && bar)["prop"], (foo && bar).prop
        if isinstance(p, ast.PropAccess) and p.expression is this:
            return True

        # this deals with precedence: 3 * (2 + 1)
        if isinstance(p, ast.Binary):
            po = p.operator
            pp = PRECEDENCE[po]
            so = this.operator
            sp = PRECEDENCE[so]
            if pp > sp or pp is sp and this is p.right and not (so is po and (so is "*" or so is "&&" or so is "||")):
                return True
    )
    PARENS(ast.PropAccess, def(output):
        p = output.parent()
        if isinstance(p, ast.New) and p.expression is this:
            # i.e. new (foo.bar().baz)
            #
            # if there's one call into this subtree, then we need
            # parens around it too, otherwise the call will be
            # interpreted as passing the arguments to the upper New
            # expression.
            try:
                this.walk(ast.TreeWalker(def(node):
                    if isinstance(node, ast.BaseCall):
                        raise p
                ))
            except as ex:
                if ex is not p:
                    raise ex
                return True
    )
    PARENS(ast.BaseCall, def(output):
        p = output.parent()
        return isinstance(p, ast.New) and p.expression is this
    )
    PARENS(ast.New, def(output):
        p = output.parent()
        if no_constructor_parens(this, output) and (isinstance(p, ast.PropAccess) \
                or isinstance(p, ast.BaseCall) and p.expression is this):
            # (new foo)(bar)
            return True
    )
    PARENS(ast.Yield, def(output):
        # (yield some).a
        return this.parens
    )
    PARENS(ast.Number, def(output):
        p = output.parent()
        if this.getValue() < 0 and isinstance(p, ast.PropAccess) and p.expression is this:
            return True
    )
    PARENS(ast.NotANumber, def(output):
        p = output.parent()
        if isinstance(p, ast.PropAccess) and p.expression is this:
            return True
    )
    def assign_and_conditional_paren_rules(output):
        p = output.parent()
        # !(a = false) → true
        if isinstance(p, ast.Unary):
            return True

        # 1 + (a = 2) + 3 → 6, side effect setting a = 2
        if isinstance(p, ast.Binary) and not (isinstance(p, ast.Assign)):
            return True

        # (a = func)() —or— new (a = Object)()
        if isinstance(p, ast.BaseCall) and p.expression is this:
            return True

        # (a = foo) ? bar : baz
        if isinstance(p, ast.Conditional) and p.condition is this:
            return True

        # (a = foo)["prop"] —or— (a = foo).prop
        if isinstance(p, ast.PropAccess) and p.expression is this:
            return True

    PARENS(ast.Assign, assign_and_conditional_paren_rules)
    PARENS(ast.Conditional, assign_and_conditional_paren_rules)
    # -----[ PRINTERS ]-----
    DEFPRINT(ast.Directive, def(self, output):
        output.print_string(self.value)
        output.semicolon()
    )
    DEFPRINT(ast.Debugger, def(self, output):
        output.print("debugger")
        output.semicolon()
    )
    # -----[ statements ]-----
    def display_body(body, is_toplevel, output, with_return):
        # one-statement function aka lambda
        if with_return:
            output.indent()
            output.print("return")
            output.space()
            body[0].print(output)
            output.newline()
            return

        last = body.length - 1
        body.forEach(def(stmt, i):
            if not (isinstance(stmt, ast.EmptyStatement)) and not (isinstance(stmt, ast.Definitions)):
                output.indent()
                stmt.print(output)
                if not (i is last and is_toplevel):
                    output.newline()
        )

    def bind_methods(methods, output):
        # bind the methods
        for arg in dir(methods):
            output.indent()
            output.print("this.")
            output.assign(arg)
            output.print("ՐՏ_bind")
            output.with_parens(def():
                output.print("this." + arg)
                output.comma()
                output.print("this")
            )
            output.end_statement()

    def write_imports(module_, output):

        def sort_imports(mod, imp_sorted, importing, done):
            # returns sorted array of  module_ids
            imp_sorted = imp_sorted  or []
            importing = importing or  {}
            done = done or  {}

            def push_key(k):
                if not done[k]:
                    imp_sorted.push(k)
                    done[k] = True

            def sort_imp(a,b):
                a = module_.imports[a].import_order
                b = module_.imports[b].import_order
                return a < b ? -1 : a > b ? 1 : 0

            if importing[mod.module_id] or done[mod.module_id]: return
            importing[mod.module_id] = True
            # process imports
            imp_keys = Object.keys(mod.depends_on).sort(sort_imp)
            imp_keys.forEach(def (mod_id):
                # if import foo.bar.c -> push all foo submodules first
                pack_id = mod_id.split('.')[0]
                # may be own submodule
                if pack_id != mod.module_id:
                    sort_imports(module_.imports[pack_id], imp_sorted, importing, done)
                    push_key(mod_id);
            )
            # process self submodules
            if mod.submodules.length:
                mod.submodules.sort(sort_imp)
                mod.submodules.forEach(def (sub_key):
                    sort_imports(module_.imports[sub_key], imp_sorted, importing, done)
                    push_key(sub_key);
                )
                push_key(mod.module_id)

            return imp_sorted

        imports = sort_imports(module_).map(def(mid): return module_.imports[mid];)
        imports.push(module_.imports['__main__'])

        if imports.length > 1:
            output.indent()
            output.spaced('var ՐՏ_modules', '=', '{};')
            output.newline()

        # Declare all variable names exported from the modules as global symbols
        nonlocalvars = {}
        for module_ in imports:
            for name in module_.nonlocalvars:
                nonlocalvars[name] = True
        nonlocalvars = Object.getOwnPropertyNames(nonlocalvars).join(', ')
        if nonlocalvars.length:
            output.indent()
            output.print('var ' + nonlocalvars)
            output.end_statement()

        # Create the module objects
        for module_ in imports:
            if module_.module_id is not '__main__':
                output.indent()
                output.assign('ՐՏ_modules["' + module_.module_id + '"]')
                output.print('{}')
                output.end_statement()

        # Output module code
        for module_ in imports:
            if module_.module_id is not '__main__':
                print_module(module_, output)

    def write_main_name(output):
        if output.option('write_name'):
            output.newline()
            output.indent()
            output.spaced('var __name__', '=', '"__main__"')
            output.end_statement()

    def display_complex_body(node, is_toplevel, output):
        output.startLocalBuffer()
        offset = 0 # argument offset, applies if this is a method of a class

        # if this is a method, add 'var self = this'
        # delay self = this in ES6 constructor until super() call, if parent
        # if isinstance(node, ast.Method) and not node.static and not (output.option('es6') and isinstance(node, ast.Constructor) and node.callsSuper):
        needsSuper = False
        delaySelfAssignment = False
        if output.option('es6') and isinstance(node, ast.Constructor) and node.parent:
            if node.callsSuper:
                delaySelfAssignment = True
            else:
                needsSuper = True
        if (isinstance(node, ast.Method) or isinstance(node, ast.ObjectGetter) or isinstance(node, ast.ObjectSetter)) \
              and not node.static:
            offset += 1
            if not delaySelfAssignment:
                if needsSuper:
                    output.indent()
                    output.print('super()')
                    output.end_statement()
                output.indent()
                output.spaced("var", node.argnames[0], "=", "this")
                output.end_statement()


        if isinstance(node, ast.Scope):
            # if function takes any arguments
            if node.argnames:
                # If *args is set, pass remainder of arguments to it
                if node.argnames.starargs:
                    output.indent()
                    output.spaced("var", node.argnames.starargs, '=', "[].slice.call")
                    output.with_parens(def():
                        output.print("arguments")
                        output.comma()
                        output.print(node.argnames.length - offset)
                    )
                    output.end_statement()

                # initialize default function arguments, if any
                if not output.option('es6'):    # in es 6 the logic is now declared in the header, not the body
                    for arg in dir(node.argnames.defaults):
                        # typeof no longer needed with strict mode
                        # it also seems that it's no longer popular to assume that some troll overrode your undefined variable :)
                        output.indent()
                        output.spaced(arg, '=', arg, '===', 'void 0', '?')
                        output.space()
                        force_statement(node.argnames.defaults[arg], output)
                        output.space()
                        output.colon()
                        output.print(arg)
                        output.end_statement()

            # rebind parent's methods and bind own methods
            # for now we'll make a naive assumption that a function
            # named __init__ will only occur inside a class
            if output.option("auto_bind") and node.name and node.name.name is "__init__":
                output.indent()
                output.print("ՐՏ_rebindAll")
                output.with_parens(def():
                    output.print("this")
                    output.comma()
                    output.print("true")
                )
                output.end_statement()
                bind_methods(node.bound, output)

            declare_vars(node.localvars, output)

        elif isinstance(node, ast.Except):
            if node.argname:
                output.indent()
                output.print("var ")
                output.assign(node.argname)
                output.print("ՐՏ_Exception")
                output.end_statement()

        # insert 'return' (if there is no one) into one-statement function
        # if statement starts immediately after ':' and if it is in parens or Array or ObjectLiteral
        stmt = (
            isinstance(node, ast.Lambda) and node.body.length == 1
            and not node.body[0].start.newline_before
            and isinstance(node.body[0], ast.SimpleStatement)
            and node.body[0].body
        )
        with_return = stmt and (
            node.body[0].start.value is '('
            or isinstance(node.body[0].body, ast.Array)
            or isinstance(node.body[0].body, ast.ObjectLiteral)
            or isinstance(node.body[0].body, ast.Dot)
            or isinstance(node.body[0].body, ast.PropAccess)
        )
        display_body(node.body, is_toplevel, output, with_return)
        output.endLocalBuffer()

    def declare_vars(vars, output):
        # declare all variables as local, unless explictly set otherwise
        if vars.length:
            output.indent()
            output.print("var ")
            vars.forEach(def(arg, i):
                if i:
                    output.comma()

                arg.print(output)
            )
            output.end_statement()

    def declare_submodules(module_id,  submodules, output):
        seen = {}
        output.newline()
        for sub_module_id in submodules:
            if not seen.hasOwnProperty(sub_module_id):
                seen[sub_module_id] = True
                key = sub_module_id.split('.')[-1]
                output.indent()
                output.print('ՐՏ_modules["' + module_id + '"]["' + key + '"] = ')
                output.print('ՐՏ_modules["' + sub_module_id + '"]')
                output.end_statement()

    def declare_exports(module_id, exports, output):
        output.newline()
        seen = {}
        for symbol in exports:
            if not seen[symbol.name]:
                seen[symbol.name] = True
                output.indent()
                output.assign('ՐՏ_modules["' + module_id + '"]["' + symbol.name + '"]')
                output.print(symbol.name)
                output.end_statement()

    def unpack_tuple(tuple, output, in_statement):
        tuple.elements.forEach(def(elem, i):
            output.indent()
            output.assign(elem)
            output.print(output.prevTemp("upk"))
            output.with_square(def():
                output.print(i)
            )
            if not in_statement or i < tuple.elements.length - 1:
                output.end_statement()
        )

    def cacheBubble(operand, output):
        # helper function for caching expensive function calls and complex operations that may happen inside the comparison
        if not (isinstance(operand, ast.SymbolRef) or isinstance(operand, ast.SymbolClassRef)):
            # looks like operand is doing some calculation, let's cache it for performance
            tmp = output.newTemp()
            output.with_parens(def():
                output.spaced(tmp, '=', operand)
            )
            return { # a little trick to allow us to call print consistently from outside
                print: def(output): output.print(tmp)
            }
        operand.print(output)
        return operand

    ast.StatementWithBody.prototype._do_print_body = def(output):
        force_statement(this.body, output)

    DEFPRINT(ast.Statement, def(self, output):
        self.body.print(output)
        output.semicolon()
    )
    BASELIB = {}
    DEFPRINT(ast.TopLevel, def(self, output):
        is_main = output.is_main()

        nonlocal BASELIB
        BASELIB = self.baselib

        if output.option("private_scope") and is_main:
            write_imports(self, output)
            output.newline()
            output.with_parens(def():
                output.print("function()")
                output.with_block(def():
                    write_main_name(output)
                    output.newline()
                    display_complex_body(self, True, output)
                    output.newline()
                )
            )
            output.print("();")
            output.newline()
        else:
            if is_main:
                write_imports(self, output)
                write_main_name(output)

            if self.strict:
                declare_vars(self.localvars, output)

            display_body(self.body, True, output)

        if is_main:
            output.startLocalBuffer()
            Object.keys(BASELIB).filter(def(a): return self.baselib[a] > 0;).forEach(def(key):
                output.print_baselib(key)
            )
            output.endLocalBuffer(True)

    )
    def print_module(self, output):
        output.newline()
        output.indent()
        output.with_parens(def():
            output.print("function()")
            output.with_block(def():
                # dump the logic of this module
                output.indent()
                output.assign('var __name__')
                output.print('"' + self.module_id + '"')
                output.end_statement()
                declare_submodules(self.module_id, self.submodules, output)
                declare_vars(self.localvars, output)
                display_body(self.body, True, output)
                declare_exports(self.module_id, self.exports, output)
            )
        )
        output.print("()")
        output.end_statement()

    DEFPRINT(ast.Splat, def(self, output):
        if output.import(self.module.name):
            display_body(self.body.body, True, output)
            output.newline()
    )

    DEFPRINT(ast.Imports, def(container, output):
        seen = {}
        def add_aname(aname, key, from_import):
            seen_key = key + (from_import ? '.' + from_import : '')
            if seen[aname]:
                if seen[aname] == seen_key:
                    return
                else:
                    tmp = aname  + ' : '  + [seen[aname], seen_key].join(', ')
                    raise Error('Something went wrong, 2 imports with the same name detected: ' + tmp)
            seen[aname] = seen_key
            output.assign('var ' + aname)
            output.print('ՐՏ_modules["' + key + '"]')
            if from_import:
                output.print('.' + from_import)
            output.end_statement()
            output.indent()

        for self in container.imports:
            if isinstance(self, ast.Splat):
                output.import(self.module.name)
            if self.argnames:
                # A from import
                for argname in self.argnames:
                    alias = (argname.alias) ? argname.alias.name : argname.name
                    add_aname(alias, self.key, argname.name)
            else:
                if self.alias:
                    add_aname(self.alias.name, self.key, False)
                else:
                    bound_name = self.key.split('.', 1)[0]
                    add_aname(bound_name, bound_name, False)

    )
    DEFPRINT(ast.LabeledStatement, def(self, output):
        self.label.print(output)
        output.colon()
        self.body.print(output)
    )
    DEFPRINT(ast.SimpleStatement, def(self, output):
        if not (isinstance(self.body, ast.EmptyStatement)):
            self.body.print(output)
            output.semicolon()
    )
    def print_bracketed(node, output, complex):
        if node.body.length:
            output.with_block(def():
                if complex:
                    display_complex_body(node, False, output)
                else:
                    display_body(node.body, False, output)
            )
        else:
            output.print("{}")

    DEFPRINT(ast.BlockStatement, def(self, output):
        print_bracketed(self, output)
    )

    DEFPRINT(ast.EmptyStatement, def(self, output):
        pass
    )

    DEFPRINT(ast.Do, def(self, output):
        output.print("do")
        output.space()
        self._do_print_body(output)
        output.space()
        output.print("while")
        output.space()
        output.with_parens(def():
            self.condition.print(output)
        )
        output.semicolon()
    )

    DEFPRINT(ast.While, def(self, output):
        output.print("while")
        output.space()
        output.with_parens(def():
            self.condition.print(output)
        )
        output.space()
        self._do_print_body(output)
    )

    def is_simple_for_in(self):
        # return true if this loop can be simplified into a basic for (i in j) loop
        if isinstance(self.object, ast.BaseCall)
        and isinstance(self.object.expression, ast.SymbolRef)
        and self.object.expression.name is "dir" and self.object.args.length is 1:
            return True
        return False

    def is_simple_for(self):
        # returns true if this loop can be simplified into a basic for(i=n;i<h;i++) loop
        if isinstance(self.object, ast.BaseCall)
        and isinstance(self.object.expression, ast.SymbolRef)
        and self.object.expression.name is "range"
        and not (isinstance(self.init, ast.Array))
        and (
            self.object.args.length < 3 or (
                isinstance(self.object.args[-1][0], ast.Number)
                or isinstance(self.object.args[-1][0], ast.Unary)
                    and self.object.args[-1][0].operator is "-"
                    and isinstance(self.object.args[-1][0].expression, ast.Number)
            )
        ):
            return True
        return False

    ast.ForIn.prototype._do_print_body = def(output):
        self = this
        output.with_block(def():
            if not (is_simple_for(self) or is_simple_for_in(self)):
                # if we're using multiple iterators, unpack them
                output.indent()
                iterator = output.prevTemp('itr') # declared in DEFPRINT
                index = output.prevTemp('idx')    # declared in DEFPRINT
                if isinstance(self.init, ast.Array):
                    if output.option('es6'):
                        output.with_square(def():
                            self.init.elements.forEach(def(element, index):
                                if (index): output.comma()
                                element.print(output)
                            )
                        )
                        output.space()
                        output.print('=')
                        output.space()
                        output.print(iterator + "[" + index + "];")
                        output.newline()
                    else:
                        unpack = output.newTemp('upk')
                        output.assign(unpack)
                        output.print(iterator + "[" + index + "];")
                        output.newline()
                        unpack_tuple(self.init, output)
                else:
                    output.assign(self.init)
                    output.print(iterator + "[" + index + "];")
                    output.newline()

            self.body.body.forEach(def(stmt, i):
                output.indent()
                stmt.print(output)
                output.newline()
            )
        )

    DEFPRINT(ast.ForIn, def(self, output):
        if is_simple_for(self):
            # optimize range() into a simple for loop
            increment = None
            args = self.object.args
            tmp_ = args.length
            if tmp_ is 1:
                start = 0
                end = args[0]
            elif tmp_ is 2:
                start = args[0]
                end = args[1]
            elif tmp_ is 3:
                start = args[0]
                end = args[1]
                increment = args[2]

            output.print("for")
            output.space()
            output.with_parens(def():
                output.assign(self.init)
                start.print ? start.print(output) : output.print(start)
                output.semicolon()
                output.space()
                self.init.print(output)
                output.space()
                isinstance(increment, ast.Unary) ? output.print(">") : output.print("<")
                output.space()
                end.print(output)
                output.semicolon()
                output.space()
                self.init.print(output)
                if increment and (not (isinstance(increment, ast.Unary)) or increment.expression.value is not "1"):
                    if isinstance(increment, ast.Unary):
                        output.print("-=")
                        increment.expression.print(output)
                    else:
                        output.print("+=")
                        increment.print(output)
                else:
                    if isinstance(increment, ast.Unary):
                        output.print("--")
                    else:
                        output.print("++")
            )
        elif is_simple_for_in(self):
            # optimize dir() into a simple for-in loop
            output.print("for")
            output.space()
            output.with_parens(def():
                output.spaced(self.init, 'in', self.object.args[0])
            )
        else:
            # regular loop
            iterator = output.newTemp('itr')
            output.assign(iterator)
            output.print("ՐՏ_Iterable")
            output.with_parens(def():
                self.object.print(output)
            )
            output.end_statement()

            output.indent()
            output.print("for")
            output.space()
            output.with_parens(def():
                index = output.newTemp('idx')
                output.assign(index)
                output.print("0")
                output.semicolon()
                output.space()

                output.spaced(index, "<", iterator + ".length")
                output.semicolon()
                output.space()
                output.print(index + "++")
            )

        output.space()
        self._do_print_body(output)
    )

    ast.ForJS.prototype._do_print_body = def(output):
        self = this
        output.with_block(def():
            self.body.body.forEach(def(stmt, i):
                output.indent()
                stmt.print(output)
                output.newline()
            )
        )

    DEFPRINT(ast.ForJS, def(self, output):
        output.print("for")
        output.space()
        output.with_parens(def():
            self.condition.print(output)
        )
        output.space()
        self._do_print_body(output)
    )
    DEFPRINT(ast.ListComprehension, def(self, output):
        # this logic is shares by list and dict comprehensions, since the difference in output
        # isn't big
        constructor = {
            ListComprehension: '[]',
            DictComprehension: '{}'
        }[type(self)]

        # create temp vars, but tell the output that we'll manage their declaration ourselves
        # technically we could have the compiler handle it by creating a new buffer, but it isn't worth the overhead
        iterator = output.newTemp("itr", False)
        index = output.newTemp("idx", False)
        result = RAPYD_PREFIX + "res"

        if isinstance(self, ast.DictComprehension):
            add_entry = def():
                output.indent()
                output.print(result)
                output.with_square(def():
                    self.statement.print(output)
                )
                output.assign('')
                self.value_statement.print(output)
                output.end_statement()
        else:
            #list comprehension
            add_entry = def():
                output.indent()
                output.print(result + ".push")
                output.with_parens(def():
                    self.statement.print(output)
                )
                output.end_statement()

        output.with_parens(def():
            output.print("function")
            output.print("()")
            output.space()
            output.with_block(def():
                output.indent()

                # handle declarations for temporary variables
                output.print('var ' + index)
                output.comma()
                output.assign(iterator)
                output.print("ՐՏ_Iterable")
                output.with_parens(def():
                    self.object.print(output)
                )
                output.comma()
                output.assign(result)
                output.print(constructor)
                # make sure to locally scope loop variables
                if isinstance(self.init, ast.Array):
                    self.init.elements.forEach(def(i):
                        output.comma()
                        i.print(output)
                    )
                else:
                    output.comma()
                    self.init.print(output)

                output.semicolon()
                output.newline()
                output.indent()
                output.print("for")
                output.space()
                output.with_parens(def():
                    output.spaced(index, "=", "0")
                    output.semicolon()
                    output.space()

                    output.spaced(index, "<", iterator + ".length")
                    output.semicolon()
                    output.space()

                    output.print(index + "++")
                )
                output.space()
                output.with_block(def():
                    output.indent()
                    if isinstance(self.init, ast.Array):
                        if output.option('es6'):
                            # in ES6, unpacking is no longer required
                            output.with_square(def():
                                self.init.elements.forEach(def(element, index):
                                    if (index): output.comma()
                                    element.print(output)
                                )
                            )
                            output.space()
                            output.print('=')
                            output.space()
                        else:
                            # unpacking
                            output.assign(output.newTemp('upk'))
                        output.print(iterator + "[" + index + "];")
                        output.newline()
                        if not output.option('es6'):
                            unpack_tuple(self.init, output)
                    else:
                        output.assign(self.init)
                        output.print(iterator + "[" + index + "];")
                        output.newline()

                    if self.condition:
                        output.indent()
                        output.print("if")
                        output.space()
                        output.with_parens(def():
                            self.condition.print(output)
                        )
                        output.space()
                        output.with_block(def():
                            add_entry()
                        )
                        output.newline()
                    else:
                        add_entry()
                )
                output.newline()
                output.indent()
                output.print("return " + result)
                output.end_statement()
            )
        )
        output.print("()")
    )
    DEFPRINT(ast.With, def(self, output):
        output.print("with")
        output.space()
        output.with_parens(def():
            self.expression.print(output)
        )
        output.space()
        self._do_print_body(output)
    )


    # -----[ functions ]-----
    def decorate(decorators, output, internalsub):
        # build a decoration chain for the function
        pos = 0
        wrap = def():
            nonlocal pos
            if pos < decorators.length:
                decorators[pos].expression.print(output)
                pos += 1
                output.with_parens(def():
                    wrap()
                )
            else:
                internalsub()
        wrap()

    def decorated(decorators, output):
        # a version of the above function that can be called as a decorator
        return def(baseFn):
            return def():
                decorate(decorators, output, baseFn)

    ast.Lambda.prototype._do_print = def(output):
        self = this

        def addDecorators():
            # decorate the class
            if self.decorators and self.decorators.length:
                return def(obj):
                    output = this
                    output.assign(obj)
                    decorate(self.decorators, output, def(): output.print(obj);)
            return None

        def addDocstring():
            if self.docstring:
                return def(obj):
                    output = this
                    output.addProperty('__doc__', self.docstring).call(output, obj)
            return None

        # label the variable correctly depending on context
        # FIXME: this is bug-prone, need var to be conditional on whether this is a standalone statement
        name = None
        is_like_method = (
             isinstance(self, ast.Method)
             or isinstance(self, ast.ObjectGetter)
             or isinstance(self, ast.ObjectSetter)
        )
        if self.name and not is_like_method:
            name = 'var ' + self.name.name
            # if first_in_statement(output):
            #     name = 'var ' + name

        def maybe_weird_name(internalsub):
            if self.name and (self.name.js_reserved or self.name.start.type in ["string", "num"]):
                name = self.name.name
                # wrap the method in special decorator that
                # saves the method name into method.__name__
                return def():
                    output.print("ՐՏ_with__name__")
                    output.with_parens(def():
                        internalsub(False)
                        output.comma()
                        output.print_string(name)
                    )
            else:
                return internalsub


        @unify(output, name, addDecorators(), addDocstring())
        @maybe_weird_name
        def internalsub(print_name = True):
            output.print("function")
            if self.generator:
                output.print("*")
            if print_name and self.name:
                output.space()
                self.name.print(output)

            output.with_parens(def():
                self.argnames.forEach(def(arg, i):
                    if is_like_method:
                        if i is 0:
                            return
                        i-=1
                    if i: output.comma()
                    arg.print(output)
                    if output.option('es6') and self.argnames.defaults[arg.name]:
                        # in ES6 default arguments can now be part of the argument signature
                        output.print('=')
                        self.argnames.defaults[arg.name].print(output)
                )

                # handle kwargs hook, if there is a kwargs decorator
                if self.kwargs:
                    if self.argnames.length:
                        output.comma()
                    # it's a generic argument name, we'll rename it in function body
                    output.print('ՐՏ_kw')
            )
            output.space()
            print_bracketed(self, output, True)
        internalsub()

    DEFPRINT(ast.Lambda, def(self, output):
        self._do_print(output)
    )

    # -----[ classes ]-----
    ast.Class.prototype._do_print = def(output):
        self = this
        if self.external:
            return

        def addDecorators():
            # decorate the class with class decorators
            if self.decorators and self.decorators.length:
                return def(obj):
                    output = this
                    output.assign(obj)
                    decorate(self.decorators, output, def(): output.print(obj);)
            return None

        def getES6DecoratedMethods():
            decorated_methods = []
            for stmt in self.body:
                if isinstance(stmt, ast.Lambda) and stmt.decorators and stmt.decorators.length:
                    decorated_methods.push(stmt)
            if decorated_methods.length:
                return decorated_methods
            return None

        # label the variable correctly depending on context
        # FIXME: this is bug-prone, need var to be conditional on whether this is a standalone statement
        name = None
        if self.name:
            name = 'var ' + self.name.name
            # if first_in_statement(output):
            #     name = 'var ' + name

        def outputEs6():
            def addClassVariablesAndMethDecorators(decorated_methods):
                # these were somehow omitted from ES6 standard, so we'll have to handle them ourselves, the messy way
                properties = {}
                # class vars should be evaluated before Object.defineProperties(...)
                # order of vars is important! so we need an Array of {name: ... , value: ...}
                class_vars = []
                if self.docstring:
                    properties['__doc__'] = def(output): output.print_string(self.docstring)
                self.body.forEach(def(stmt, i):
                    if isinstance(stmt, ast.SimpleStatement) and isinstance(stmt.body, ast.Assign) and stmt.body.operator is '=':
                        # naively assume left side will not be an expression
                        properties[stmt.body.left.name] = def(output):
                            output.print(stmt.body.left.name)
                            output.newline()
                        class_vars.push({
                            name: stmt.body.left.name,
                            value: def(output): stmt.body.right.print(output)
                        })
                )
                if Object.keys(properties).length or (decorated_methods and decorated_methods.length):
                    def_class_vars_and_decorators = def(obj):
                        output = this
                        static_decorations = {}
                        if decorated_methods:
                            for stmt in decorated_methods:
                                def print_name(output, stmt_):
                                      pref = obj + (stmt_.static ? '' : '.prototype')
                                      output.print(pref)
                                      if stmt_.name.start.type in ["string", "num"]:
                                          output.with_square(def(): stmt_.name.print(output);)
                                      else:
                                          output.print('.'+ stmt_.name.name)

                                decoration = {
                                    'value' : ( def(_stmt):
                                            return def(output):
                                                decorate(_stmt.decorators, output, def(): print_name(output, _stmt););
                                    )(stmt),
                                    'attrs' : {'enumerable': 'false'},
                                    'name': stmt.name
                                }
                                if stmt.static:
                                    static_decorations[stmt.name.name] = decoration
                                else:
                                    properties[stmt.name.name] = decoration
                        if Object.keys(properties).length:
                            output.addProperties('prototype', properties).call(output, obj)
                            output.end_statement()
                        if Object.keys(static_decorations).length:
                            output.indent()
                            output.addProperties(None, static_decorations).call(output, obj)
                            output.end_statement()
                    def_class_vars_and_decorators= output.with_class_vars_init(class_vars, def_class_vars_and_decorators)
                    return def_class_vars_and_decorators
                return None

            # new ES6 classes
            # @decorated(self.decorators, output)
            @unify(output, name, addDecorators(), addClassVariablesAndMethDecorators(getES6DecoratedMethods()))
            def generateClass():
                output.print('class')

                # with ES6, class names are optional, just like functions we can have anonymous classes
                if self.name:
                    output.space()
                    self.name.print(output)

                # inheritance, if any
                if self.parent:
                    output.space()
                    output.print('extends')
                    output.space()
                    self.parent.print(output)

                output.space()
                output.with_block(def():

                    # build constructor
                    # in ES5 we created a sub-constructor called __init__ to call from the main constructor, the intention was to
                    # circumvent a limitation which I no longer remember, may have been something to do with existence of `this` in
                    # constructor context. Let's see if we can circumvent that with ES6 classes without the hack.

                    # add methods
                    # TODO: for now assume everything we see in class body is a method, we'll handle class variables later
                    # in a special way since ES6 dropped the ball on their support
                    self.body.forEach(def(stmt, i):
                        if isinstance(stmt, ast.Lambda):
                            output.indent()
                            if stmt.static:
                                output.print('static')
                                output.space()
                            if stmt.name.name is '__init__':
                                output.print('constructor')
                            else:
                                if isinstance(stmt, ast.ObjectGetter):
                                    output.print('get ')
                                elif isinstance(stmt, ast.ObjectSetter):
                                    output.print('set ')
                                elif stmt.generator:
                                    output.print('*')
                                stmt.name.print(output)
                            output.space()

                            # argument list
                            output.with_parens(def():
                                stmt.argnames.forEach(def(arg, i):
                                    # only strip first argument if the method isn't static
                                    if stmt.name.name in self.static: i += 1
                                    if i > 1: output.comma()
                                    if i: arg.print(output)
                                    if stmt.argnames.defaults[arg.name]:
                                        # in ES6 default arguments can be part of the signature instead of being in body
                                        output.print('=')
                                        stmt.argnames.defaults[arg.name].print(output)
                                )

                                # handle kwargs hook, if there is a kwargs decorator
                                if self.kwargs:
                                    if self.argnames.length: output.comma()
                                    # it's a generic argument name, we'll rename it in function body
                                    output.print('ՐՏ_kw')
                            )
                            output.space()
                            print_bracketed(stmt, output, True)
                            output.newline()
                    )
                )
            return generateClass

        def outputEs5():
            # original ES5 class generation logic
            def define_method(stmt):
                return def(output):
                    name = stmt.name.name
                    def internalsub(print_meth_name = True):
                        output.print("function")
                        if print_meth_name:
                            output.space()
                            output.print(name)
                        output.with_parens(def():
                            stmt.argnames.forEach(def(arg, i):
                                # only strip first argument if the method isn't static
                                if name in self.static: i += 1
                                if i > 1: output.comma()
                                if i: arg.print(output)
                            )

                            # handle kwargs hook, if there is a kwargs decorator
                            if self.kwargs:
                                if self.argnames.length:
                                    output.comma()
                                # it's a generic argument name, we'll rename it in function body
                                output.print('ՐՏ_kw')
                        )
                        print_bracketed(stmt, output, True)

                    if stmt.name.js_reserved or stmt.name.start.type in ["string", "num"]:
                        # wrap the method in special decorator that
                        # saves the method name into method.__name__
                        fn_def = internalsub
                        internalsub = def():
                            output.print("ՐՏ_with__name__")
                            output.with_parens(def():
                                fn_def(False)
                                output.comma()
                                output.print_string(name)
                            )
                    # decorate the method
                    if stmt.decorators and stmt.decorators.length:
                        decorate(stmt.decorators, output, internalsub)
                    else:
                        internalsub()
                    output.newline()

            def addInheritance():
                # inheritance, if parent exists
                if self.parent:
                    return def(obj):
                        output = this
                        output.print("ՐՏ_extends")
                        output.with_parens(def():
                            output.print(obj)
                            output.comma()
                            self.parent.print(output)
                        )
                return None

            def addMethods():
                # actual methods, and class variables
                methodsAndVars = {}
                staticMethods = {}
                # class vars should be evaluated before Object.defineProperties(...)
                # order of vars is important! so we need an Array of {name: ... , value: ...}
                class_vars = []
                if self.docstring:
                    methodsAndVars['__doc__'] = def(output): output.print_string(self.docstring)
                self.body.forEach(def(stmt, i):
                    if isinstance(stmt, ast.Method):
                        meth_hash = stmt.static ? staticMethods : methodsAndVars
                        meth_hash[stmt.name.name] ={value: define_method(stmt), name: stmt.name}
                    elif isinstance(stmt, ast.SimpleStatement) and isinstance(stmt.body, ast.Assign) and stmt.body.operator is '=':
                        # class variable
                        methodsAndVars[stmt.body.left.name] = def(output):
                            output.print(stmt.body.left.name)
                        class_vars.push({   name: stmt.body.left.name,
                                            value: def(output): stmt.body.right.print(output);})
                    elif isinstance(stmt, ast.Class):
                        console.error('Nested classes aren\'t supported yet')
                )

                methodAndOutput = None
                if Object.keys(methodsAndVars).length:
                    methodAndVarOutput = output.addProperties('prototype', methodsAndVars)
                    if class_vars.length:
                        methodAndVarOutput = output.with_class_vars_init(class_vars, methodAndVarOutput)

                staticMethodOutput = None
                if Object.keys(staticMethods).length:
                    staticMethodOutput = output.addProperties(None, staticMethods)

                return methodAndVarOutput, staticMethodOutput
            methodsAndVars, staticmethods = addMethods()

            # generate constructor
            @unify(output, name, addInheritance(), addDecorators(), methodsAndVars, staticmethods)
            def generateClass():
                if self.init or self.parent or self.statements.length:
                    output.print("function")
                    output.space()
                    self.name.print(output)
                    output.print("()")
                    output.space()
                    output.with_block(def():

                        bind_methods(self.bound, output)

                        # constructor
                        if self.init or self.parent:
                            output.indent()
                            cname = self.init ? self.name : self.parent
                            cname.print(output)
                            output.print(".prototype.__init__.apply")
                            output.with_parens(def():
                                output.print("this")
                                output.comma()
                                output.print("arguments")
                            )
                            output.end_statement()
                    )
                else:
                    # no init method or parent, create empty init
                    output.print("function")
                    output.space()
                    self.name.print(output)
                    output.print("()")
                    output.space()
                    output.with_block(def():
                        bind_methods(self.bound, output)
                    )
            return generateClass

        if output.option('es6'):
            generateClass = outputEs6()
        else:
            generateClass = outputEs5()
        # ignition
        generateClass()

    DEFPRINT(ast.Class, def(self, output):
        self._do_print(output)
    )

    DEFPRINT(ast.SymbolClassRef, def(self, output):
        self.class.print(output)
        output.print('.prototype.' + self.name)
    )

    # -----[ exits ]-----
    ast.Exit.prototype._do_print = def(output, kind):
        self = this
        output.print(kind)
        if self.value:
            output.space()
            self.value.print(output)
        if not self.yield_request:
            output.semicolon()

    DEFPRINT(ast.Return, def(self, output):
        self._do_print(output, "return")
    )
    DEFPRINT(ast.Yield, def(self, output):
        kind = "yield"
        if self.yield_from:
            kind = "yield*"
        self._do_print(output, kind)
    )
    DEFPRINT(ast.Throw, def(self, output):
        self._do_print(output, "throw")
    )

    # -----[ loop control ]-----
    ast.LoopControl.prototype._do_print = def(output, kind):
        output.print(kind)
        if this.label:
            output.space()
            this.label.print(output)
        output.semicolon()

    DEFPRINT(ast.Break, def(self, output):
        self._do_print(output, "break")
    )
    DEFPRINT(ast.Continue, def(self, output):
        self._do_print(output, "continue")
    )

    # -----[ if ]-----
    def make_then(self, output):
        if output.option("bracketize"):
            make_block(self.body, output)
            return

        # The squeezer replaces "block"-s that contain only a single
        # statement with the statement itself; technically, the AST
        # is correct, but this can create problems when we output an
        # IF having an ELSE clause where the THEN clause ends in an
        # IF *without* an ELSE block (then the outer ELSE would refer
        # to the inner IF).  This function checks for this case and
        # adds the block brackets if needed.
        if not self.body:
            return output.force_semicolon()

        body = self.body
        while True:
            if isinstance(body, ast.If):
                if not body.alternative:
                    make_block(self.body, output)
                    return

                body = body.alternative
            elif isinstance(body, ast.StatementWithBody):
                body = body.body
            else:
                break

        force_statement(self.body, output)

    DEFPRINT(ast.If, def(self, output):
        output.print("if")
        output.space()
        output.with_parens(def():
            self.condition.print(output)
        )
        output.space()
        if self.alternative:
            make_then(self, output)
            output.space()
            output.print("else")
            output.space()
            force_statement(self.alternative, output)
        else:
            self._do_print_body(output)

    )

    # -----[ switch ]-----
    DEFPRINT(ast.Switch, def(self, output):
        output.print("switch")
        output.space()
        output.with_parens(def():
            self.expression.print(output)
        )
        output.space()
        if self.body.length > 0:
            output.with_block(def():
                self.body.forEach(def(stmt, i):
                    if i:
                        output.newline()

                    output.indent(True)
                    stmt.print(output)
                )
            )
        else:
            output.print("{}")
    )
    ast.SwitchBranch.prototype._do_print_body = def(output):
        if this.body.length > 0:
            output.newline()
            this.body.forEach(def(stmt):
                output.indent()
                stmt.print(output)
                output.newline()
            )

    DEFPRINT(ast.Default, def(self, output):
        output.print("default:")
        self._do_print_body(output)
    )
    DEFPRINT(ast.Case, def(self, output):
        output.print("case")
        output.space()
        self.expression.print(output)
        output.print(":")
        self._do_print_body(output)
    )

    # -----[ exceptions ]-----
    DEFPRINT(ast.Try, def(self, output):
        output.print("try")
        output.space()
        print_bracketed(self, output)
        if self.bcatch:
            output.space()
            self.bcatch.print(output)

        if self.bfinally:
            output.space()
            self.bfinally.print(output)
    )
    DEFPRINT(ast.Catch, def(self, output):
        output.print("catch")
        output.space()
        output.with_parens(def():
            output.print("ՐՏ_Exception")
        )
        output.space()
        if self.body.length > 1 or self.body[0].errors.length:
            output.with_block(def():
                output.indent()
                no_default = True
                self.body.forEach(def(exception, i):
                    if i:
                        output.print("else ")

                    if exception.errors.length:
                        output.print("if")
                        output.space()
                        output.with_parens(def():
                            exception.errors.forEach(def(err, i):
                                if i:
                                    output.newline()
                                    output.indent()
                                    output.print("||")
                                    output.space()

                                output.spaced("ՐՏ_Exception", "instanceof", err)
                            )
                        )
                        output.space()
                    else:
                        no_default = False
                    print_bracketed(exception, output, True)
                    output.space()
                )
                if no_default:
                    output.print("else")
                    output.space()
                    output.with_block(def():
                        output.indent()
                        output.spaced("throw", "ՐՏ_Exception")
                        output.end_statement()
                    )
                output.newline()
            )
        else:
            print_bracketed(self.body[0], output, True)
    )
    DEFPRINT(ast.Finally, def(self, output):
        output.print("finally")
        output.space()
        print_bracketed(self, output)
    )

    # -----[ var/const ]-----
    ast.Definitions.prototype._do_print = def(output, kind):
        output.print(kind)
        output.space()
        this.definitions.forEach(def(def_, i):
            if i:
                output.comma()
            def_.print(output)
        )
        p = output.parent()
        in_for = isinstance(p, ast.ForIn)
        avoid_semicolon = in_for and p.init is this
        if not avoid_semicolon:
            output.semicolon()

    DEFPRINT(ast.Var, def(self, output):
        self._do_print(output, "var")
    )
    DEFPRINT(ast.Const, def(self, output):
        self._do_print(output, "const")
    )
    def parenthesize_for_noin(node, output, noin):
        if not noin:
            node.print(output)
        else:
            try:
                # need to take some precautions here:
                node.walk(ast.TreeWalker(def(node):
                    if isinstance(node, ast.Binary) and node.operator is "in":
                        raise output
                ))
                node.print(output)
            except as ex:
                if ex is not output:
                    raise ex
                node.print(output, True)

    DEFPRINT(ast.VarDef, def(self, output):
        self.name.print(output)
        if self.value:
            output.assign("")
            parenthesize_for_noin(self.value, output, isinstance(output.parent(1), ast.ForIn))
    )

    # -----[ other expressions ]-----
    CREATION = [] # FIXME: a hack, tracks variables that class is being assigned to for handling
                  # kwargs on constructor gracefully, a better approach is to split this function
                  # into ast.Call (normal functions), ast.ClassCall (class functions), and
                  # ast.New (constructors), remove DEFPRINT for ast.BaseCall altogether, add
                  # 'obj' property to ast.New and have it track the object it's assigned to. Then
                  # use that property instead of this global stack. This would also have the benefit
                  # of minimizing clutter that this function is currently dealing with.
    DEFPRINT(ast.BaseCall, def(self, output):

        # logic for defining the format of a function call (class, static, normal, decorated)
        selfArg = None
        def call_format():
            if isinstance(self, ast.ClassCall):
                # class methods are called through the prototype unless static
                if self.static:
                    self.class.print(output)
                    output.print("." + self.method)
                elif output.option('es6') and self.super:
                    nonlocal selfArg
                    output.print('super')
                    if self.method is not 'constructor': output.print("." + self.method)
                    selfArg = self.args.shift()
                else:
                    self.class.print(output)
                    output.print(".prototype." + self.method + ".call")
            else:
                # regular function call
                rename = self.expression.name in SPECIAL_METHODS ? SPECIAL_METHODS[self.expression.name] : undefined
                if rename:
                    # this is a special baselib function
                    output.print(rename)
                else:
                    self.expression.print(output)

        if isinstance(self, ast.New):
            # FIXME: a hack to avoid building a new type of AST object for the
            # special case of kwargs-based constructor
            object = CREATION.pop()

            if no_constructor_parens(self, output):
                call_format()
                return

        # determine if we'll be using kwargs, since there are two possible triggers for them
        has_kwarg_items = self.args.kwarg_items and self.args.kwarg_items.length
        has_kwarg_formals = self.args.kwargs and self.args.kwargs.length
        has_kwargs = has_kwarg_items or has_kwarg_formals

        if self.args.starargs or has_kwargs:
            obj = isinstance(self, ast.New) ? object : (self.expression.expression ? self.expression.expression : ast.This())
            if output.option('es6'):
                # starargs in ES6 work very similar to Python, the only complication is kwargs
                if has_kwargs:
                    # **kwargs requires currying
                    output.print('kwargs')
                    output.with_parens(def():
                        call_format()
                    )
                else:
                    # *args, format doesn't change
                    call_format()
            else:
                if isinstance(self, ast.New):
                    # class constructor
                    # we need to declare the class a bit differently with *args and **kwargs
                    call_format()
                    output.semicolon()
                    output.newline()
                    output.indent()
                    if has_kwargs:
                        # **kwargs requires currying
                        output.print('kwargs')
                        output.with_parens(def():
                            object.print(output)
                            output.print('.__init__')
                        )
                    else:
                        # *args only requires that we call apply on __init__ rather than the object itself
                        object.print(output)
                        output.print('.__init__')
                elif has_kwargs:
                    # regular **kwargs call
                    output.print('kwargs')
                    output.with_parens(def(): call_format();)
                else:
                    # *args, the format of the prefix doesn't change
                    call_format()
        else:
            # regular call without *args or **kwargs
            call_format()

        # helper function for generating/dumping **kwargs argument
        output_kwargs = def():
            # items represented as **kw
            if has_kwarg_items:
                self.args.kwarg_items.forEach(def(kwname, i):
                    if i > 0:
                        output.print(',')
                        output.space()
                    kwname.print(output)
                )
                if has_kwarg_formals:
                    output.print(',')
                    output.space()

            # implicit kwargs from positional arguments
            if has_kwarg_formals:
                output.print('{')
                self.args.kwargs.forEach(def(pair, i):
                    if i: output.comma()
                    pair[0].print(output)
                    output.print(':')
                    output.space()
                    pair[1].print(output)
                )
                output.print('}')

        if output.option('es6') and self.args.starargs:
            # TODO: current "Pythonic" limtation forces the user to put *args as a last argument, in ES6 such limitation
            # does not exist, therefore for now we'll make the output oblivious to it and in the future will remove
            # the limitation from parser as well. Moreover we'll also need to remove the limitation of being limited
            # to a single *args argument, since it also does not exist in ES6
            output.with_parens(def():
                self.args.forEach(def(expr, i):
                    if i: output.comma()
                    if self.args.starargs and i is self.args.length - 1:
                        output.print('...')
                    expr.print(output)
                )
            )
        elif self.args.starargs:
            # in ES5 *args logic requires unique compilation via apply() method
            # **kwargs always needs to be the last argument
            output.print(".apply")
            output.with_parens(def():
                obj.print(output)
                output.comma()

                if self.args.length > 1:
                    # start with basic arguments
                    output.with_square(def():
                        self.args[:-1].forEach(def(expr, i):
                            if i: output.comma()
                            expr.print(output)
                        )
                    )
                else:
                    # no basic args, just start with *args
                    self.args[0].print(output)

                # proceed to **kwargs
                if has_kwargs or self.args.length > 1:
                    output.print('.concat')
                    output.with_parens(def():
                        # add *args if we didn't yet
                        if self.args.length > 1:
                            self.args[-1].print(output)
                            if has_kwargs:
                                output.comma()
                        output_kwargs()
                    )
            )
        elif has_kwargs and (isinstance(self, ast.New) or self.expression and self.expression.expression):
            # **kwargs for class methods require .call() notation since currying loses track of the context
            output.print('.call')
            output.with_parens(def():
                obj.print(output)
                for arg in self.args:
                    output.comma()
                    arg.print(output)
                output.comma()
                output_kwargs()
            )
        else:
            # regular function call
            output.with_parens(def():
                self.args.forEach(def(expr, i):
                    if i:
                        output.comma()
                    expr.print(output)
                )

                # kwargs as hash
                if has_kwargs:
                    if self.args.length:
                        output.comma()
                    output_kwargs()
            )

        if output.option('es6') and (
                    (isinstance(self, ast.ClassCall) and self.super)
                 or (isinstance(self, ast.Call)      and self.expression.name is 'super') ):
            # in ES6, move "self = this" definition after the super() call
            output.end_statement()
            output.indent()
            output.spaced("var", selfArg or self.expression.selfArg, "=", "this")
    )
    DEFPRINT(ast.New, def(self, output):
        output.print("new")
        output.space()
        ast.BaseCall.prototype._codegen(self, output)
    )
    ast.Seq.prototype._do_print = def(output):
        self = this
        p = output.parent()
        print_seq = def():
            self.car.print(output)
            if self.cdr:
                output.comma()
                if output.should_break():
                    output.newline()
                    output.indent()
                self.cdr.print(output)

        # this will effectively convert tuples to arrays
        if isinstance(p, ast.Binary)
        or isinstance(p, ast.Return)
        or (isinstance(p, ast.Yield) and not p.yield_request)
        or isinstance(p, ast.Array)
        or isinstance(p, ast.BaseCall)
        or isinstance(p, ast.SimpleStatement):
            output.with_square(print_seq)
        else:
            print_seq()

    DEFPRINT(ast.Seq, def(self, output):
        self._do_print(output)
    )
    DEFPRINT(ast.Dot, def(self, output):
        expr = self.expression
        expr.print(output)
        if isinstance(expr, ast.Number) and expr.getValue() >= 0:
            if not /[xa-f.]/i.test(output.last()):
                output.print(".")
        output.print(".")
        # the name after dot would be mapped about here.
        output.add_mapping(self.end)
        output.print_name(self.property)
    )
    DEFPRINT(ast.Sub, def(self, output):
        self.expression.print(output)
        output.print("[")
        # parse negative constants into len-constant
        if isinstance(self.property, ast.Unary) and self.property.operator is "-"
        and isinstance(self.property.expression, ast.Number):
            # TODO: this might parse incorrectly if expression is a
            # function call that might not return the same result
            # when called repeatedly. We might eventually want to
            # save the return to a temporary variable and use that
            # instead if expression is a function. Or we could just
            # throw an error if negative indices are used with a
            # type that's not ast.SymbolVar
            self.expression.print(output)
            output.print(".length")

        self.property.print(output)
        output.print("]")
    )
    DEFPRINT(ast.Slice, def(self, output):
        # splice assignment via pythonic array[start:end]
        output.print('[].splice.apply')
        output.with_parens(def():
            self.expression.print(output)
            output.comma()
            output.with_square(def():
                self.property.print(output)
                output.comma()
                self.property2.print(output)
                output.print('-')
                self.property.print(output)
            )
            output.print('.concat')
            output.with_parens(def():
                self.assignment.print(output)
            )
        )
    )
    DEFPRINT(ast.UnaryPrefix, def(self, output):
        op = self.operator
        if op is "*":
            if output.option('es6'):
                op = "..."
            else:
                # FIXME: we're technically printing incorrect thing in ES5 but only kwargs code relies on this anyway and
                # that portion doesn't activate in ES5
                op = ''
        output.print(op)
        if /^[a-z]/i.test(op):
            output.space()
        self.expression.print(output)
    )
    DEFPRINT(ast.UnaryPostfix, def(self, output):
        self.expression.print(output)
        output.print(self.operator)
    )
    DEFPRINT(ast.Binary, def(self, output):
        comparators = {
            "<": True,
            ">": True,
            "<=": True,
            ">=": True,
            "==": True,
            "!=": True
        }
        function_ops = {
            "in": "ՐՏ_in",
            "**": "Math.pow",
            "//": "Math.floor"
        }

        # handle strict equality gracefully
        # TODO: this may be dead code, look into it later
        normalize = def(op):
            if op is '==': return '==='
            elif op is '!=': return '!=='
            return op

        if self.operator in function_ops:
            # TODO: possible unroll optimization for short 'in' sequences
            output.print(function_ops[self.operator])
            output.with_parens(def():
                self.left.print(output)
                if self.operator is "//":
                    output.space()
                    output.print('/')
                    output.space()
                else:
                    output.comma()
                self.right.print(output)
            )
        elif comparators[self.operator] and isinstance(self.left, ast.Binary) and comparators[self.left.operator]:
            operator = normalize(self.operator)

            if isinstance(self.left.right, ast.Symbol):
                # left side compares against a regular variable,
                # no caching needed
                self.left.print(output)
                leftvar = self.left.right.name
            else:
                # some logic is being performed, let's cache it
                self.left.left.print(output)
                output.space()
                output.print(self.left.operator)
                output.space()
                output.with_parens(def():
                    nonlocal leftvar
                    leftvar = output.newTemp()
                    output.assign(leftvar)
                    self.left.right.print(output)
                )

            output.space()
            output.spaced('&&', leftvar, operator, self.right)
        else:
            output.spaced(self.left, normalize(self.operator), self.right)
    )

    DEFPRINT(ast.DeepEquality, def(self, output):
        # a fast implementation of deep equality
        # TODO: this does not handle comparators logic yet

        primitives = ['Boolean', 'String', 'Number']
        if self.left.computedType in primitives or self.right.computedType in primitives:
            # type-inferred optimization
            self.left.print(output)
            output.space()
            self.operator is '==' ? output.print('===') : output.print('!==')
            output.space()
            self.right.print(output)
        else:
            output.with_parens(def():
                left = cacheBubble(self.left, output)
                if self.operator is '==':
                    # positive case, eliminate truthy immutable comparisons
                    output.space()
                    output.spaced('===')
                    output.space()
                    right = cacheBubble(self.right, output)
                    output.space()

                    # negative case, eliminate falsy immutable comparisons
                    output.spaced('||', 'typeof', left, '===', '"object"')
                    output.space()

                    # deep case
                    output.print('&&')
                    output.space()
                    output.print('ՐՏ_eq')
                    output.with_parens(def():
                        left.print(output)
                        output.comma()
                        right.print(output)
                    )
                else:
                    # negative case, eliminate truthy immutable comparisons
                    output.space()
                    output.spaced('!==')
                    output.space()
                    right = cacheBubble(self.right, output)
                    output.space()

                    # negative case, eliminate falsy immutable comparisons
                    output.print('&&')
                    output.space()
                    output.with_parens(def():
                        output.spaced('typeof', left, '!==', '"object"')
                        output.space()

                        # deep case
                        output.print('||')
                        output.space()
                        output.print('!ՐՏ_eq')
                        output.with_parens(def():
                            left.print(output)
                            output.comma()
                            right.print(output)
                        )
                    )
            )
    )
    DEFPRINT(ast.Assign, def(self, output):
        if isinstance(self.right, ast.Number) and self.right.value is 1 and self.operator in ["+=", "-="]:
            # optimization
            # NOTE: it's important that we use the prefix, not the suffix which returns pre-increment value
            output.print(self.operator is "+=" ? "++" : "--")
            self.left.print(output)
        else:
            if self.operator is '//=':
                output.assign(self.left)
                output.print('Math.floor')
                output.with_parens(def():
                    self.left.print(output)
                    output.space()
                    output.print('/')
                    output.space()
                    self.right.print(output)
                )
                return
            if isinstance(self.left, ast.Array):
                if output.option('es6'):
                    output.with_square(def():
                        self.left.elements.forEach(def(element, index):
                            if (index): output.comma()
                            element.print(output)
                        )
                    )
                else:
                    output.print(output.newTemp('upk'))
            else:
                self.left.print(output)

            output.space()
            output.print(self.operator)
            output.space()
            if isinstance(self.right, ast.New):
                # FIXME: a hack, see declaration for full explanation
                CREATION.push(self.left)
            self.right.print(output)
            if isinstance(self.left, ast.Array):
                if not output.option('es6'):
                    output.end_statement()
                    unpack_tuple(self.left, output, True)
    )
    DEFPRINT(ast.Conditional, def(self, output):
        self.condition.print(output)
        output.space()
        output.print("?")
        output.space()
        self.consequent.print(output)
        output.space()
        output.colon()
        self.alternative.print(output)
    )

    # -----[ literals ]-----
    DEFPRINT(ast.Array, def(self, output):
        output.with_square(def():
            array = self.elements
            len_ = array.length
            if len_ > 0:
                output.space()
            array.forEach(def(exp, i):
                if i:
                    output.comma()
                exp.print(output)
            )
            if len_ > 0:
                output.space()
        )
    )
    DEFPRINT(ast.Range, def(self, output):
        # FIXME: make use of computedValue which we'll add later
        indexes = []
        for element in [self.left, self.right]:
            if isinstance(element, ast.UnaryPrefix) and element.operator is '-' and isinstance(element.expression, ast.Number):
                indexes.push(parseFloat('-' + element.expression.value))
            elif isinstance(element, ast.Number):
                indexes.push(parseFloat(element.value))
            else:
                indexes.push(None)

        if indexes[0] and indexes[1] and Math.abs(indexes[1] - indexes[0]) < 50:
            # optimization
            start = indexes[0]
            end = indexes[1]
            step = start < end ? 1 : -1
            if self.operator is "to": end += step/1e6
            output.with_square(def():
                for i in range(start, end, step):
                    if i is not start: output.comma()
                    output.print(i)
            )
        else:
            output.print('range')
            output.with_parens(def():
                self.left.print(output)
                output.comma()
                if self.operator is "to":
                    output.spaced(self.left, '<', self.right, '?', self.right, '+', 1e-6, ':', self.right, '-', 1e-6)
                else:
                    self.right.print(output)
                output.comma()
                output.spaced(self.left, '<', self.right, '?', '1', ':', '-1')
            )
    )


    DEFPRINT(ast.ObjectLiteral, def(self, output):
        if self.properties.length > 0:
            properties = {}
            for p in self.properties:
                v = p.value
                key = p.key
                if (v_key = isinstance(v, ast.ObjectGetter) ? 'get' \
                      : isinstance(v, ast.ObjectSetter) ? 'set' : False):
                    h_ = def(v):
                         return def(output): v.print(output);
                    if not (props = properties[key.name]):
                        props = properties[key.name] = {
                            'name': key,
                            'attrs' : {'enumerable': 'true'},
                        }
                    props[v_key] = h_(v)
            add_props = None
            if Object.keys(properties).length:
                add_props = def(obj):
                    output.addProperties(None, properties).call(output, obj)
                    output.end_statement()
                    #output.indent()
                add_props = output.with_class_vars_init([], add_props)

            @unify(output, None, add_props)
            def inner():
                output.with_block(def():
                    j = 0
                    self.properties.forEach(def(prop, i):
                        nonlocal j
                        if not (isinstance(prop.value, ast.ObjectGetter) or isinstance(prop.value, ast.ObjectSetter)):
                            if j:
                                output.print(",")
                                output.newline()
                            j += 1
                            output.indent()
                            prop.print(output)
                    )
                    output.newline()
                )
            inner()
        else:
            output.print("{}")
    )
    DEFPRINT(ast.ObjectKeyVal, def(self, output):
        if isinstance(self.key, ast.Identifier)
        or isinstance(self.key, ast.String)
        or isinstance(self.key, ast.Number)
        or isinstance(self.key, ast.Boolean)
        or isinstance(self.key, ast.SymbolDefun):
            # default key
            self.key.print(output)
        else:
            # computed key
            output.with_square(def():
                self.key.print(output)
            )
        output.colon()
        self.value.print(output)
    )

    ast.Symbol.prototype.definition = def(): return this.thedef

    DEFPRINT(ast.Symbol, def(self, output):
        if self.start and self.start.type is "string":
            output.print_string(self.name)
            return
        def_ = self.definition()
        output.print_name((def_ ? def_.mangled_name or def_.name : self.name))
    )
    DEFPRINT(ast.Undefined, def(self, output):
        output.print("void 0")
    )
    DEFPRINT(ast.Hole, noop)
    DEFPRINT(ast.Infinity, def(self, output):
        output.print("1/0")
    )
    DEFPRINT(ast.NotANumber, def(self, output):
        output.print("0/0")
    )
    DEFPRINT(ast.This, def(self, output):
        output.print("this")
    )
    DEFPRINT(ast.Constant, def(self, output):
        output.print(self.getValue())
    )
    DEFPRINT(ast.String, def(self, output):
        if self.modifier in 'fF':
            # string template, ES6
            output.print('`')
            output.print_string(self.getValue(), False)
            output.print('`')
        else:
            # regular string
            output.print_string(self.getValue())
    )
    DEFPRINT(ast.Verbatim, def(self, output):
        output.print(self.getValue())
    )
    DEFPRINT(ast.Number, def(self, output):
        output.print(make_num(self.getValue()))
    )
    DEFPRINT(ast.RegExp, def(self, output):
        str_ = self.getValue().toString()
        if output.option("ascii_only"):
            str_ = output.to_ascii(str_)
        output.print(str_)
        p = output.parent()
        if isinstance(p, ast.Binary) and /^in/.test(p.operator) and p.left is self:
            output.print(" ")
    )

    def force_statement(stat, output):
        # print a statement even if there is nothing to print, forcing an appropriate "empty" indicator
        if output.option("bracketize"):
            if not stat or isinstance(stat, ast.EmptyStatement):
                output.print("{}")
            elif isinstance(stat, ast.BlockStatement):
                stat.print(output)
            else:
                output.with_block(def():
                    output.indent()
                    stat.print(output)
                    output.newline()
                )
        else:
            if not stat or isinstance(stat, ast.EmptyStatement):
                output.force_semicolon()
            else:
                stat.print(output)

    def first_in_statement(output):
        # return true if the node at the top of the stack (that means the
        # innermost node in the current output) is lexically the first in
        # a statement.
        processed = output.stack()
        i = processed.length
        node = processed[i -= 1]
        prev = processed[i -= 1]
        while i > 0:
            if isinstance(prev, ast.Statement) and prev.body is node:
                return True
            if isinstance(prev, ast.Seq) and prev.car is node
            or isinstance(prev, ast.BaseCall) and prev.expression is node
            or isinstance(prev, ast.Dot) and prev.expression is node
            or isinstance(prev, ast.Sub) and prev.expression is node
            or isinstance(prev, ast.Conditional) and prev.condition is node
            or isinstance(prev, ast.Binary) and prev.left is node
            or isinstance(prev, ast.UnaryPostfix) and prev.expression is node:
                node = prev
                prev = processed[i -= 1]
            else:
                return False

    def no_constructor_parens(self, output):
        # self should be ast.New, decide if we want to show parens or not.
        return self.args.length is 0 and not output.option("beautify")

    def best_of(choices):
        best = choices[0]
        len_ = best.length
        for i in range(1, choices.length):
            if choices[i].length < len_:
                best = choices[i]
                len_ = best.length
        return best

    def make_num(num):
        # convert number into best possible numeric representation
        str_ = num.toString(10)
        choices = [ str_.replace(/^0\./, ".").replace("e+", "e") ]
        match = None

        if Math.floor(num) is num:
            if num >= 0:
                choices.push("0x" + num.toString(16).toLowerCase(), # probably pointless
                "0" + num.toString(8))
            else:
                choices.push("-0x" + (-num).toString(16).toLowerCase(), # probably pointless
                "-0" + (-num).toString(8))

            if match = /^(.*?)(0+)$/.exec(num):
                choices.push(match[1] + "e" + match[2].length)

        elif match = /^0?\.(0+)(.*)$/.exec(num):
            choices.push(match[2] + "e-" + (match[1].length + match[2].length), str_.substr(str_.indexOf(".")))

        return best_of(choices)

    def make_block(stmt, output):
        if isinstance(stmt, ast.BlockStatement):
            stmt.print(output)
            return

        output.with_block(def():
            output.indent()
            stmt.print(output)
            output.newline()
        )

    # -----[ source map generators ]-----
    def DEFMAP(nodetype, generator):
        nodetype.prototype.add_source_map = def(stream): generator(this, stream)

    # We could easily add info for ALL nodes, but it seems to me that
    # would be quite wasteful, hence this noop in the base class.
    DEFMAP(ast.Node, noop)
    def basic_sourcemap_gen(self, output):
        output.add_mapping(self.start)

    # XXX: I'm not exactly sure if we need it for all of these nodes,
    # or if we should add even more.
    DEFMAP(ast.Directive, basic_sourcemap_gen)
    DEFMAP(ast.Debugger, basic_sourcemap_gen)
    DEFMAP(ast.Symbol, basic_sourcemap_gen)
    DEFMAP(ast.Jump, basic_sourcemap_gen)
    DEFMAP(ast.StatementWithBody, basic_sourcemap_gen)
    DEFMAP(ast.LabeledStatement, noop)
    # since the label symbol will mark it
    DEFMAP(ast.Lambda, basic_sourcemap_gen)
    DEFMAP(ast.Switch, basic_sourcemap_gen)
    DEFMAP(ast.SwitchBranch, basic_sourcemap_gen)
    DEFMAP(ast.BlockStatement, basic_sourcemap_gen)
    DEFMAP(ast.TopLevel, noop)
    DEFMAP(ast.New, basic_sourcemap_gen)
    DEFMAP(ast.Try, basic_sourcemap_gen)
    DEFMAP(ast.Catch, basic_sourcemap_gen)
    DEFMAP(ast.Finally, basic_sourcemap_gen)
    DEFMAP(ast.Definitions, basic_sourcemap_gen)
    DEFMAP(ast.Constant, basic_sourcemap_gen)
    DEFMAP(ast.ObjectProperty, def(self, output):
        output.add_mapping(self.start, self.key)
    )
)()
