define(
  ["../base","../exception","../utils","./code-gen","exports"],
  function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) {
    "use strict";
    var COMPILER_REVISION = __dependency1__.COMPILER_REVISION;
    var REVISION_CHANGES = __dependency1__.REVISION_CHANGES;
    var Exception = __dependency2__["default"];
    var isArray = __dependency3__.isArray;
    var CodeGen = __dependency4__["default"];

    function Literal(value) {
      this.value = value;
    }

    function JavaScriptCompiler() {}

    JavaScriptCompiler.prototype = {
      // PUBLIC API: You can override these methods in a subclass to provide
      // alternative compiled forms for name lookup and buffering semantics
      nameLookup: function(parent, name /* , type*/) {
        if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
          return [parent, ".", name];
        } else {
          return [parent, "['", name, "']"];
        }
      },
      depthedLookup: function(name) {
        return [this.aliasable('this.lookup'), '(depths, "', name, '")'];
      },

      compilerInfo: function() {
        var revision = COMPILER_REVISION,
            versions = REVISION_CHANGES[revision];
        return [revision, versions];
      },

      appendToBuffer: function(source, location, explicit) {
        // Force a source as this simplifies the merge logic.
        if (!isArray(source)) {
          source = [source];
        }
        source = this.source.wrap(source, location);

        if (this.environment.isSimple) {
          return ['return ', source, ';'];
        } else if (explicit) {
          // This is a case where the buffer operation occurs as a child of another
          // construct, generally braces. We have to explicitly output these buffer
          // operations to ensure that the emitted code goes in the correct location.
          return ['buffer += ', source, ';'];
        } else {
          source.appendToBuffer = true;
          return source;
        }
      },

      initializeBuffer: function() {
        return this.quotedString("");
      },
      // END PUBLIC API

      compile: function(environment, options, context, asObject) {
        this.environment = environment;
        this.options = options;
        this.stringParams = this.options.stringParams;
        this.trackIds = this.options.trackIds;
        this.precompile = !asObject;

        this.name = this.environment.name;
        this.isChild = !!context;
        this.context = context || {
          programs: [],
          environments: []
        };

        this.preamble();

        this.stackSlot = 0;
        this.stackVars = [];
        this.aliases = {};
        this.registers = { list: [] };
        this.hashes = [];
        this.compileStack = [];
        this.inlineStack = [];
        this.blockParams = [];

        this.compileChildren(environment, options);

        this.useDepths = this.useDepths || environment.useDepths || this.options.compat;
        this.useBlockParams = this.useBlockParams || environment.useBlockParams;

        var opcodes = environment.opcodes,
            opcode,
            firstLoc,
            i,
            l;

        for (i = 0, l = opcodes.length; i < l; i++) {
          opcode = opcodes[i];

          this.source.currentLocation = opcode.loc;
          firstLoc = firstLoc || opcode.loc;
          this[opcode.opcode].apply(this, opcode.args);
        }

        // Flush any trailing content that might be pending.
        this.source.currentLocation = firstLoc;
        this.pushSource('');

        /* istanbul ignore next */
        if (this.stackSlot || this.inlineStack.length || this.compileStack.length) {
          throw new Exception('Compile completed with content left on stack');
        }

        var fn = this.createFunctionContext(asObject);
        if (!this.isChild) {
          var ret = {
            compiler: this.compilerInfo(),
            main: fn
          };
          var programs = this.context.programs;
          for (i = 0, l = programs.length; i < l; i++) {
            if (programs[i]) {
              ret[i] = programs[i];
            }
          }

          if (this.environment.usePartial) {
            ret.usePartial = true;
          }
          if (this.options.data) {
            ret.useData = true;
          }
          if (this.useDepths) {
            ret.useDepths = true;
          }
          if (this.useBlockParams) {
            ret.useBlockParams = true;
          }
          if (this.options.compat) {
            ret.compat = true;
          }

          if (!asObject) {
            ret.compiler = JSON.stringify(ret.compiler);

            this.source.currentLocation = {start: {line: 1, column: 0}};
            ret = this.objectLiteral(ret);

            if (options.srcName) {
              ret = ret.toStringWithSourceMap({file: options.destName});
              ret.map = ret.map && ret.map.toString();
            } else {
              ret = ret.toString();
            }
          } else {
            ret.compilerOptions = this.options;
          }

          return ret;
        } else {
          return fn;
        }
      },

      preamble: function() {
        // track the last context pushed into place to allow skipping the
        // getContext opcode when it would be a noop
        this.lastContext = 0;
        this.source = new CodeGen(this.options.srcName);
      },

      createFunctionContext: function(asObject) {
        var varDeclarations = '';

        var locals = this.stackVars.concat(this.registers.list);
        if(locals.length > 0) {
          varDeclarations += ", " + locals.join(", ");
        }

        // Generate minimizer alias mappings
        //
        // When using true SourceNodes, this will update all references to the given alias
        // as the source nodes are reused in situ. For the non-source node compilation mode,
        // aliases will not be used, but this case is already being run on the client and
        // we aren't concern about minimizing the template size.
        var aliasCount = 0;
        for (var alias in this.aliases) {
          var node = this.aliases[alias];

          if (this.aliases.hasOwnProperty(alias) && node.children && node.referenceCount > 1) {
            varDeclarations += ', alias' + (++aliasCount) + '=' + alias;
            node.children[0] = 'alias' + aliasCount;
          }
        }

        var params = ["depth0", "helpers", "partials", "data"];

        if (this.useBlockParams || this.useDepths) {
          params.push('blockParams');
        }
        if (this.useDepths) {
          params.push('depths');
        }

        // Perform a second pass over the output to merge content when possible
        var source = this.mergeSource(varDeclarations);

        if (asObject) {
          params.push(source);

          return Function.apply(this, params);
        } else {
          return this.source.wrap(['function(', params.join(','), ') {\n  ', source, '}']);
        }
      },
      mergeSource: function(varDeclarations) {
        var isSimple = this.environment.isSimple,
            appendOnly = !this.forceBuffer,
            appendFirst,

            sourceSeen,
            bufferStart,
            bufferEnd;
        this.source.each(function(line) {
          if (line.appendToBuffer) {
            if (bufferStart) {
              line.prepend('  + ');
            } else {
              bufferStart = line;
            }
            bufferEnd = line;
          } else {
            if (bufferStart) {
              if (!sourceSeen) {
                appendFirst = true;
              } else {
                bufferStart.prepend('buffer += ');
              }
              bufferEnd.add(';');
              bufferStart = bufferEnd = undefined;
            }

            sourceSeen = true;
            if (!isSimple) {
              appendOnly = false;
            }
          }
        });


        if (appendOnly) {
          if (bufferStart) {
            bufferStart.prepend('return ');
            bufferEnd.add(';');
          } else if (!sourceSeen) {
            this.source.push('return "";');
          }
        } else {
          varDeclarations += ", buffer = " + (appendFirst ? '' : this.initializeBuffer());

          if (bufferStart) {
            bufferStart.prepend('return buffer + ');
            bufferEnd.add(';');
          } else {
            this.source.push('return buffer;');
          }
        }

        if (varDeclarations) {
          this.source.prepend('var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n'));
        }

        return this.source.merge();
      },

      // [blockValue]
      //
      // On stack, before: hash, inverse, program, value
      // On stack, after: return value of blockHelperMissing
      //
      // The purpose of this opcode is to take a block of the form
      // `{{#this.foo}}...{{/this.foo}}`, resolve the value of `foo`, and
      // replace it on the stack with the result of properly
      // invoking blockHelperMissing.
      blockValue: function(name) {
        var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'),
            params = [this.contextName(0)];
        this.setupHelperArgs(name, 0, params);

        var blockName = this.popStack();
        params.splice(1, 0, blockName);

        this.push(this.source.functionCall(blockHelperMissing, 'call', params));
      },

      // [ambiguousBlockValue]
      //
      // On stack, before: hash, inverse, program, value
      // Compiler value, before: lastHelper=value of last found helper, if any
      // On stack, after, if no lastHelper: same as [blockValue]
      // On stack, after, if lastHelper: value
      ambiguousBlockValue: function() {
        // We're being a bit cheeky and reusing the options value from the prior exec
        var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'),
            params = [this.contextName(0)];
        this.setupHelperArgs('', 0, params, true);

        this.flushInline();

        var current = this.topStack();
        params.splice(1, 0, current);

        this.pushSource([
            'if (!', this.lastHelper, ') { ',
              current, ' = ', this.source.functionCall(blockHelperMissing, 'call', params),
            '}']);
      },

      // [appendContent]
      //
      // On stack, before: ...
      // On stack, after: ...
      //
      // Appends the string value of `content` to the current buffer
      appendContent: function(content) {
        if (this.pendingContent) {
          content = this.pendingContent + content;
        } else {
          this.pendingLocation = this.source.currentLocation;
        }

        this.pendingContent = content;
      },

      // [append]
      //
      // On stack, before: value, ...
      // On stack, after: ...
      //
      // Coerces `value` to a String and appends it to the current buffer.
      //
      // If `value` is truthy, or 0, it is coerced into a string and appended
      // Otherwise, the empty string is appended
      append: function() {
        if (this.isInline()) {
          this.replaceStack(function(current) {
            return [' != null ? ', current, ' : ""'];
          });

          this.pushSource(this.appendToBuffer(this.popStack()));
        } else {
          var local = this.popStack();
          this.pushSource(['if (', local, ' != null) { ', this.appendToBuffer(local, undefined, true), ' }']);
          if (this.environment.isSimple) {
            this.pushSource(['else { ', this.appendToBuffer("''", undefined, true), ' }']);
          }
        }
      },

      // [appendEscaped]
      //
      // On stack, before: value, ...
      // On stack, after: ...
      //
      // Escape `value` and append it to the buffer
      appendEscaped: function() {
        this.pushSource(this.appendToBuffer(
            [this.aliasable('this.escapeExpression'), '(', this.popStack(), ')']));
      },

      // [getContext]
      //
      // On stack, before: ...
      // On stack, after: ...
      // Compiler value, after: lastContext=depth
      //
      // Set the value of the `lastContext` compiler value to the depth
      getContext: function(depth) {
        this.lastContext = depth;
      },

      // [pushContext]
      //
      // On stack, before: ...
      // On stack, after: currentContext, ...
      //
      // Pushes the value of the current context onto the stack.
      pushContext: function() {
        this.pushStackLiteral(this.contextName(this.lastContext));
      },

      // [lookupOnContext]
      //
      // On stack, before: ...
      // On stack, after: currentContext[name], ...
      //
      // Looks up the value of `name` on the current context and pushes
      // it onto the stack.
      lookupOnContext: function(parts, falsy, scoped) {
        var i = 0;

        if (!scoped && this.options.compat && !this.lastContext) {
          // The depthed query is expected to handle the undefined logic for the root level that
          // is implemented below, so we evaluate that directly in compat mode
          this.push(this.depthedLookup(parts[i++]));
        } else {
          this.pushContext();
        }

        this.resolvePath('context', parts, i, falsy);
      },

      // [lookupBlockParam]
      //
      // On stack, before: ...
      // On stack, after: blockParam[name], ...
      //
      // Looks up the value of `parts` on the given block param and pushes
      // it onto the stack.
      lookupBlockParam: function(blockParamId, parts) {
        this.useBlockParams = true;

        this.push(['blockParams[', blockParamId[0], '][', blockParamId[1], ']']);
        this.resolvePath('context', parts, 1);
      },

      // [lookupData]
      //
      // On stack, before: ...
      // On stack, after: data, ...
      //
      // Push the data lookup operator
      lookupData: function(depth, parts) {
        /*jshint -W083 */
        if (!depth) {
          this.pushStackLiteral('data');
        } else {
          this.pushStackLiteral('this.data(data, ' + depth + ')');
        }

        this.resolvePath('data', parts, 0, true);
      },

      resolvePath: function(type, parts, i, falsy) {
        /*jshint -W083 */
        if (this.options.strict || this.options.assumeObjects) {
          this.push(strictLookup(this.options.strict, this, parts, type));
          return;
        }

        var len = parts.length;
        for (; i < len; i++) {
          this.replaceStack(function(current) {
            var lookup = this.nameLookup(current, parts[i], type);
            // We want to ensure that zero and false are handled properly if the context (falsy flag)
            // needs to have the special handling for these values.
            if (!falsy) {
              return [' != null ? ', lookup, ' : ', current];
            } else {
              // Otherwise we can use generic falsy handling
              return [' && ', lookup];
            }
          });
        }
      },

      // [resolvePossibleLambda]
      //
      // On stack, before: value, ...
      // On stack, after: resolved value, ...
      //
      // If the `value` is a lambda, replace it on the stack by
      // the return value of the lambda
      resolvePossibleLambda: function() {
        this.push([this.aliasable('this.lambda'), '(', this.popStack(), ', ', this.contextName(0), ')']);
      },

      // [pushStringParam]
      //
      // On stack, before: ...
      // On stack, after: string, currentContext, ...
      //
      // This opcode is designed for use in string mode, which
      // provides the string value of a parameter along with its
      // depth rather than resolving it immediately.
      pushStringParam: function(string, type) {
        this.pushContext();
        this.pushString(type);

        // If it's a subexpression, the string result
        // will be pushed after this opcode.
        if (type !== 'SubExpression') {
          if (typeof string === 'string') {
            this.pushString(string);
          } else {
            this.pushStackLiteral(string);
          }
        }
      },

      emptyHash: function(omitEmpty) {
        if (this.trackIds) {
          this.push('{}'); // hashIds
        }
        if (this.stringParams) {
          this.push('{}'); // hashContexts
          this.push('{}'); // hashTypes
        }
        this.pushStackLiteral(omitEmpty ? 'undefined' : '{}');
      },
      pushHash: function() {
        if (this.hash) {
          this.hashes.push(this.hash);
        }
        this.hash = {values: [], types: [], contexts: [], ids: []};
      },
      popHash: function() {
        var hash = this.hash;
        this.hash = this.hashes.pop();

        if (this.trackIds) {
          this.push(this.objectLiteral(hash.ids));
        }
        if (this.stringParams) {
          this.push(this.objectLiteral(hash.contexts));
          this.push(this.objectLiteral(hash.types));
        }

        this.push(this.objectLiteral(hash.values));
      },

      // [pushString]
      //
      // On stack, before: ...
      // On stack, after: quotedString(string), ...
      //
      // Push a quoted version of `string` onto the stack
      pushString: function(string) {
        this.pushStackLiteral(this.quotedString(string));
      },

      // [pushLiteral]
      //
      // On stack, before: ...
      // On stack, after: value, ...
      //
      // Pushes a value onto the stack. This operation prevents
      // the compiler from creating a temporary variable to hold
      // it.
      pushLiteral: function(value) {
        this.pushStackLiteral(value);
      },

      // [pushProgram]
      //
      // On stack, before: ...
      // On stack, after: program(guid), ...
      //
      // Push a program expression onto the stack. This takes
      // a compile-time guid and converts it into a runtime-accessible
      // expression.
      pushProgram: function(guid) {
        if (guid != null) {
          this.pushStackLiteral(this.programExpression(guid));
        } else {
          this.pushStackLiteral(null);
        }
      },

      // [invokeHelper]
      //
      // On stack, before: hash, inverse, program, params..., ...
      // On stack, after: result of helper invocation
      //
      // Pops off the helper's parameters, invokes the helper,
      // and pushes the helper's return value onto the stack.
      //
      // If the helper is not found, `helperMissing` is called.
      invokeHelper: function(paramSize, name, isSimple) {
        var nonHelper = this.popStack();
        var helper = this.setupHelper(paramSize, name);
        var simple = isSimple ? [helper.name, ' || '] : '';

        var lookup = ['('].concat(simple, nonHelper);
        if (!this.options.strict) {
          lookup.push(' || ', this.aliasable('helpers.helperMissing'));
        }
        lookup.push(')');

        this.push(this.source.functionCall(lookup, 'call', helper.callParams));
      },

      // [invokeKnownHelper]
      //
      // On stack, before: hash, inverse, program, params..., ...
      // On stack, after: result of helper invocation
      //
      // This operation is used when the helper is known to exist,
      // so a `helperMissing` fallback is not required.
      invokeKnownHelper: function(paramSize, name) {
        var helper = this.setupHelper(paramSize, name);
        this.push(this.source.functionCall(helper.name, 'call', helper.callParams));
      },

      // [invokeAmbiguous]
      //
      // On stack, before: hash, inverse, program, params..., ...
      // On stack, after: result of disambiguation
      //
      // This operation is used when an expression like `{{foo}}`
      // is provided, but we don't know at compile-time whether it
      // is a helper or a path.
      //
      // This operation emits more code than the other options,
      // and can be avoided by passing the `knownHelpers` and
      // `knownHelpersOnly` flags at compile-time.
      invokeAmbiguous: function(name, helperCall) {
        this.useRegister('helper');

        var nonHelper = this.popStack();

        this.emptyHash();
        var helper = this.setupHelper(0, name, helperCall);

        var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');

        var lookup = ['(', '(helper = ', helperName, ' || ', nonHelper, ')'];
        if (!this.options.strict) {
          lookup[0] = '(helper = ';
          lookup.push(
            ' != null ? helper : ',
            this.aliasable('helpers.helperMissing')
          );
        }

        this.push([
            '(', lookup,
            (helper.paramsInit ? ['),(', helper.paramsInit] : []), '),',
            '(typeof helper === ', this.aliasable('"function"'), ' ? ',
            this.source.functionCall('helper','call', helper.callParams), ' : helper))'
        ]);
      },

      // [invokePartial]
      //
      // On stack, before: context, ...
      // On stack after: result of partial invocation
      //
      // This operation pops off a context, invokes a partial with that context,
      // and pushes the result of the invocation back.
      invokePartial: function(isDynamic, name, indent) {
        var params = [],
            options = this.setupParams(name, 1, params, false);

        if (isDynamic) {
          name = this.popStack();
          delete options.name;
        }

        if (indent) {
          options.indent = JSON.stringify(indent);
        }
        options.helpers = 'helpers';
        options.partials = 'partials';

        if (!isDynamic) {
          params.unshift(this.nameLookup('partials', name, 'partial'));
        } else {
          params.unshift(name);
        }

        if (this.options.compat) {
          options.depths = 'depths';
        }
        options = this.objectLiteral(options);
        params.push(options);

        this.push(this.source.functionCall('this.invokePartial', '', params));
      },

      // [assignToHash]
      //
      // On stack, before: value, ..., hash, ...
      // On stack, after: ..., hash, ...
      //
      // Pops a value off the stack and assigns it to the current hash
      assignToHash: function(key) {
        var value = this.popStack(),
            context,
            type,
            id;

        if (this.trackIds) {
          id = this.popStack();
        }
        if (this.stringParams) {
          type = this.popStack();
          context = this.popStack();
        }

        var hash = this.hash;
        if (context) {
          hash.contexts[key] = context;
        }
        if (type) {
          hash.types[key] = type;
        }
        if (id) {
          hash.ids[key] = id;
        }
        hash.values[key] = value;
      },

      pushId: function(type, name, child) {
        if (type === 'BlockParam') {
          this.pushStackLiteral(
              'blockParams[' + name[0] + '].path[' + name[1] + ']'
              + (child ? ' + ' + JSON.stringify('.' + child) : ''));
        } else if (type === 'PathExpression') {
          this.pushString(name);
        } else if (type === 'SubExpression') {
          this.pushStackLiteral('true');
        } else {
          this.pushStackLiteral('null');
        }
      },

      // HELPERS

      compiler: JavaScriptCompiler,

      compileChildren: function(environment, options) {
        var children = environment.children, child, compiler;

        for(var i=0, l=children.length; i<l; i++) {
          child = children[i];
          compiler = new this.compiler();

          var index = this.matchExistingProgram(child);

          if (index == null) {
            this.context.programs.push('');     // Placeholder to prevent name conflicts for nested children
            index = this.context.programs.length;
            child.index = index;
            child.name = 'program' + index;
            this.context.programs[index] = compiler.compile(child, options, this.context, !this.precompile);
            this.context.environments[index] = child;

            this.useDepths = this.useDepths || compiler.useDepths;
            this.useBlockParams = this.useBlockParams || compiler.useBlockParams;
          } else {
            child.index = index;
            child.name = 'program' + index;

            this.useDepths = this.useDepths || child.useDepths;
            this.useBlockParams = this.useBlockParams || child.useBlockParams;
          }
        }
      },
      matchExistingProgram: function(child) {
        for (var i = 0, len = this.context.environments.length; i < len; i++) {
          var environment = this.context.environments[i];
          if (environment && environment.equals(child)) {
            return i;
          }
        }
      },

      programExpression: function(guid) {
        var child = this.environment.children[guid],
            programParams = [child.index, 'data', child.blockParams];

        if (this.useBlockParams || this.useDepths) {
          programParams.push('blockParams');
        }
        if (this.useDepths) {
          programParams.push('depths');
        }

        return 'this.program(' + programParams.join(', ') + ')';
      },

      useRegister: function(name) {
        if(!this.registers[name]) {
          this.registers[name] = true;
          this.registers.list.push(name);
        }
      },

      push: function(expr) {
        if (!(expr instanceof Literal)) {
          expr = this.source.wrap(expr);
        }

        this.inlineStack.push(expr);
        return expr;
      },

      pushStackLiteral: function(item) {
        this.push(new Literal(item));
      },

      pushSource: function(source) {
        if (this.pendingContent) {
          this.source.push(
              this.appendToBuffer(this.source.quotedString(this.pendingContent), this.pendingLocation));
          this.pendingContent = undefined;
        }

        if (source) {
          this.source.push(source);
        }
      },

      replaceStack: function(callback) {
        var prefix = ['('],
            stack,
            createdStack,
            usedLiteral;

        /* istanbul ignore next */
        if (!this.isInline()) {
          throw new Exception('replaceStack on non-inline');
        }

        // We want to merge the inline statement into the replacement statement via ','
        var top = this.popStack(true);

        if (top instanceof Literal) {
          // Literals do not need to be inlined
          stack = [top.value];
          prefix = ['(', stack];
          usedLiteral = true;
        } else {
          // Get or create the current stack name for use by the inline
          createdStack = true;
          var name = this.incrStack();

          prefix = ['((', this.push(name), ' = ', top, ')'];
          stack = this.topStack();
        }

        var item = callback.call(this, stack);

        if (!usedLiteral) {
          this.popStack();
        }
        if (createdStack) {
          this.stackSlot--;
        }
        this.push(prefix.concat(item, ')'));
      },

      incrStack: function() {
        this.stackSlot++;
        if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); }
        return this.topStackName();
      },
      topStackName: function() {
        return "stack" + this.stackSlot;
      },
      flushInline: function() {
        var inlineStack = this.inlineStack;
        this.inlineStack = [];
        for (var i = 0, len = inlineStack.length; i < len; i++) {
          var entry = inlineStack[i];
          /* istanbul ignore if */
          if (entry instanceof Literal) {
            this.compileStack.push(entry);
          } else {
            var stack = this.incrStack();
            this.pushSource([stack, ' = ', entry, ';']);
            this.compileStack.push(stack);
          }
        }
      },
      isInline: function() {
        return this.inlineStack.length;
      },

      popStack: function(wrapped) {
        var inline = this.isInline(),
            item = (inline ? this.inlineStack : this.compileStack).pop();

        if (!wrapped && (item instanceof Literal)) {
          return item.value;
        } else {
          if (!inline) {
            /* istanbul ignore next */
            if (!this.stackSlot) {
              throw new Exception('Invalid stack pop');
            }
            this.stackSlot--;
          }
          return item;
        }
      },

      topStack: function() {
        var stack = (this.isInline() ? this.inlineStack : this.compileStack),
            item = stack[stack.length - 1];

        /* istanbul ignore if */
        if (item instanceof Literal) {
          return item.value;
        } else {
          return item;
        }
      },

      contextName: function(context) {
        if (this.useDepths && context) {
          return 'depths[' + context + ']';
        } else {
          return 'depth' + context;
        }
      },

      quotedString: function(str) {
        return this.source.quotedString(str);
      },

      objectLiteral: function(obj) {
        return this.source.objectLiteral(obj);
      },

      aliasable: function(name) {
        var ret = this.aliases[name];
        if (ret) {
          ret.referenceCount++;
          return ret;
        }

        ret = this.aliases[name] = this.source.wrap(name);
        ret.aliasable = true;
        ret.referenceCount = 1;

        return ret;
      },

      setupHelper: function(paramSize, name, blockHelper) {
        var params = [],
            paramsInit = this.setupHelperArgs(name, paramSize, params, blockHelper);
        var foundHelper = this.nameLookup('helpers', name, 'helper');

        return {
          params: params,
          paramsInit: paramsInit,
          name: foundHelper,
          callParams: [this.contextName(0)].concat(params)
        };
      },

      setupParams: function(helper, paramSize, params) {
        var options = {}, contexts = [], types = [], ids = [], param;

        options.name = this.quotedString(helper);
        options.hash = this.popStack();

        if (this.trackIds) {
          options.hashIds = this.popStack();
        }
        if (this.stringParams) {
          options.hashTypes = this.popStack();
          options.hashContexts = this.popStack();
        }

        var inverse = this.popStack(),
            program = this.popStack();

        // Avoid setting fn and inverse if neither are set. This allows
        // helpers to do a check for `if (options.fn)`
        if (program || inverse) {
          options.fn = program || 'this.noop';
          options.inverse = inverse || 'this.noop';
        }

        // The parameters go on to the stack in order (making sure that they are evaluated in order)
        // so we need to pop them off the stack in reverse order
        var i = paramSize;
        while (i--) {
          param = this.popStack();
          params[i] = param;

          if (this.trackIds) {
            ids[i] = this.popStack();
          }
          if (this.stringParams) {
            types[i] = this.popStack();
            contexts[i] = this.popStack();
          }
        }

        if (this.trackIds) {
          options.ids = this.source.generateArray(ids);
        }
        if (this.stringParams) {
          options.types = this.source.generateArray(types);
          options.contexts = this.source.generateArray(contexts);
        }

        if (this.options.data) {
          options.data = 'data';
        }
        if (this.useBlockParams) {
          options.blockParams = 'blockParams';
        }
        return options;
      },

      setupHelperArgs: function(helper, paramSize, params, useRegister) {
        var options = this.setupParams(helper, paramSize, params, true);
        options = this.objectLiteral(options);
        if (useRegister) {
          this.useRegister('options');
          params.push('options');
          return ['options=', options];
        } else {
          params.push(options);
          return '';
        }
      }
    };


    var reservedWords = (
      "break else new var" +
      " case finally return void" +
      " catch for switch while" +
      " continue function this with" +
      " default if throw" +
      " delete in try" +
      " do instanceof typeof" +
      " abstract enum int short" +
      " boolean export interface static" +
      " byte extends long super" +
      " char final native synchronized" +
      " class float package throws" +
      " const goto private transient" +
      " debugger implements protected volatile" +
      " double import public let yield await" +
      " null true false"
    ).split(" ");

    var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};

    for(var i=0, l=reservedWords.length; i<l; i++) {
      compilerWords[reservedWords[i]] = true;
    }

    JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
      return !JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(name);
    };

    function strictLookup(requireTerminal, compiler, parts, type) {
      var stack = compiler.popStack();

      var i = 0,
          len = parts.length;
      if (requireTerminal) {
        len--;
      }

      for (; i < len; i++) {
        stack = compiler.nameLookup(stack, parts[i], type);
      }

      if (requireTerminal) {
        return [compiler.aliasable('this.strict'), '(', stack, ', ', compiler.quotedString(parts[i]), ')'];
      } else {
        return stack;
      }
    }

    __exports__["default"] = JavaScriptCompiler;
  });