diff --git a/.npmignore b/.npmignore
index 5c49ba1b2..2f42c9f44 100644
--- a/.npmignore
+++ b/.npmignore
@@ -5,7 +5,6 @@ Gemfile
 Gemfile.lock
 Rakefile
 bench/*
-dist/*
 spec/*
 src/*
 vendor/*
diff --git a/LICENSE b/LICENSE
index 237cd0346..f466a93fd 100644
--- a/LICENSE
+++ b/LICENSE
@@ -17,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
-
diff --git a/Rakefile b/Rakefile
index e1563772b..9ecf913d4 100644
--- a/Rakefile
+++ b/Rakefile
@@ -61,7 +61,7 @@ def build_for_task(task)
   FileUtils.rm_rf("dist/*") if File.directory?("dist")
   FileUtils.mkdir_p("dist")
 
-  contents = []
+  contents = ["/*\n\n" + File.read('LICENSE') + "\n*/\n"]
   task.prerequisites.each do |filename|
     next if filename == "dist"
 
diff --git a/dist/handlebars.js b/dist/handlebars.js
index 247a3fdff..9c653ee72 100644
--- a/dist/handlebars.js
+++ b/dist/handlebars.js
@@ -1,3 +1,27 @@
+/*
+
+Copyright (C) 2011 by Yehuda Katz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+
 // lib/handlebars/base.js
 
 /*jshint eqnull:true*/
@@ -5,7 +29,13 @@ this.Handlebars = {};
 
 (function(Handlebars) {
 
-Handlebars.VERSION = "1.0.rc.2";
+Handlebars.VERSION = "1.0.0-rc.3";
+Handlebars.COMPILER_REVISION = 2;
+
+Handlebars.REVISION_CHANGES = {
+  1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
+  2: '>= 1.0.0-rc.3'
+};
 
 Handlebars.helpers  = {};
 Handlebars.partials = {};
@@ -618,9 +648,13 @@ return new Parser;
 // lib/handlebars/compiler/base.js
 Handlebars.Parser = handlebars;
 
-Handlebars.parse = function(string) {
+Handlebars.parse = function(input) {
+
+  // Just return if an already-compile AST was passed in.
+  if(input.constructor === Handlebars.AST.ProgramNode) { return input; }
+
   Handlebars.Parser.yy = Handlebars.AST;
-  return Handlebars.Parser.parse(string);
+  return Handlebars.Parser.parse(input);
 };
 
 Handlebars.print = function(ast) {
@@ -702,8 +736,11 @@ Handlebars.print = function(ast) {
     for(var i=0,l=parts.length; i<l; i++) {
       var part = parts[i];
 
-      if(part === "..") { depth++; }
-      else if(part === "." || part === "this") { this.isScoped = true; }
+      if (part === ".." || part === "." || part === "this") {
+        if (dig.length > 0) { throw new Handlebars.Exception("Invalid path: " + this.original); }
+        else if (part === "..") { depth++; }
+        else { this.isScoped = true; }
+      }
       else { dig.push(part); }
     }
 
@@ -853,6 +890,26 @@ Handlebars.JavaScriptCompiler = function() {};
 
       return out.join("\n");
     },
+    equals: function(other) {
+      var len = this.opcodes.length;
+      if (other.opcodes.length !== len) {
+        return false;
+      }
+
+      for (var i = 0; i < len; i++) {
+        var opcode = this.opcodes[i],
+            otherOpcode = other.opcodes[i];
+        if (opcode.opcode !== otherOpcode.opcode || opcode.args.length !== otherOpcode.args.length) {
+          return false;
+        }
+        for (var j = 0; j < opcode.args.length; j++) {
+          if (opcode.args[j] !== otherOpcode.args[j]) {
+            return false;
+          }
+        }
+      }
+      return true;
+    },
 
     guid: 0,
 
@@ -944,7 +1001,7 @@ Handlebars.JavaScriptCompiler = function() {};
         // evaluate it by executing `blockHelperMissing`
         this.opcode('pushProgram', program);
         this.opcode('pushProgram', inverse);
-        this.opcode('pushHash');
+        this.opcode('emptyHash');
         this.opcode('blockValue');
       } else {
         this.ambiguousMustache(mustache, program, inverse);
@@ -953,7 +1010,7 @@ Handlebars.JavaScriptCompiler = function() {};
         // evaluate it by executing `blockHelperMissing`
         this.opcode('pushProgram', program);
         this.opcode('pushProgram', inverse);
-        this.opcode('pushHash');
+        this.opcode('emptyHash');
         this.opcode('ambiguousBlockValue');
       }
 
@@ -977,6 +1034,7 @@ Handlebars.JavaScriptCompiler = function() {};
 
         this.opcode('assignToHash', pair[0]);
       }
+      this.opcode('popHash');
     },
 
     partial: function(partial) {
@@ -1017,17 +1075,19 @@ Handlebars.JavaScriptCompiler = function() {};
     },
 
     ambiguousMustache: function(mustache, program, inverse) {
-      var id = mustache.id, name = id.parts[0];
+      var id = mustache.id,
+          name = id.parts[0],
+          isBlock = program != null || inverse != null;
 
       this.opcode('getContext', id.depth);
 
       this.opcode('pushProgram', program);
       this.opcode('pushProgram', inverse);
 
-      this.opcode('invokeAmbiguous', name);
+      this.opcode('invokeAmbiguous', name, isBlock);
     },
 
-    simpleMustache: function(mustache, program, inverse) {
+    simpleMustache: function(mustache) {
       var id = mustache.id;
 
       if (id.type === 'DATA') {
@@ -1158,7 +1218,7 @@ Handlebars.JavaScriptCompiler = function() {};
       if(mustache.hash) {
         this.hash(mustache.hash);
       } else {
-        this.opcode('pushHash');
+        this.opcode('emptyHash');
       }
 
       return params;
@@ -1175,7 +1235,7 @@ Handlebars.JavaScriptCompiler = function() {};
       if(mustache.hash) {
         this.hash(mustache.hash);
       } else {
-        this.opcode('pushHash');
+        this.opcode('emptyHash');
       }
 
       return params;
@@ -1189,7 +1249,7 @@ Handlebars.JavaScriptCompiler = function() {};
   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) {
+    nameLookup: function(parent, name /* , type*/) {
       if (/^[0-9]+$/.test(name)) {
         return parent + "[" + name + "]";
       } else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
@@ -1204,7 +1264,11 @@ Handlebars.JavaScriptCompiler = function() {};
       if (this.environment.isSimple) {
         return "return " + string + ";";
       } else {
-        return "buffer += " + string + ";";
+        return {
+          appendToBuffer: true,
+          content: string,
+          toString: function() { return "buffer += " + string + ";"; }
+        };
       }
     },
 
@@ -1225,6 +1289,7 @@ Handlebars.JavaScriptCompiler = function() {};
       this.isChild = !!context;
       this.context = context || {
         programs: [],
+        environments: [],
         aliases: { }
       };
 
@@ -1234,6 +1299,7 @@ Handlebars.JavaScriptCompiler = function() {};
       this.stackVars = [];
       this.registers = { list: [] };
       this.compileStack = [];
+      this.inlineStack = [];
 
       this.compileChildren(environment, options);
 
@@ -1255,11 +1321,11 @@ Handlebars.JavaScriptCompiler = function() {};
     },
 
     nextOpcode: function() {
-      var opcodes = this.environment.opcodes, opcode = opcodes[this.i + 1];
+      var opcodes = this.environment.opcodes;
       return opcodes[this.i + 1];
     },
 
-    eat: function(opcode) {
+    eat: function() {
       this.i = this.i + 1;
     },
 
@@ -1297,7 +1363,6 @@ Handlebars.JavaScriptCompiler = function() {};
 
       // Generate minimizer alias mappings
       if (!this.isChild) {
-        var aliases = [];
         for (var alias in this.context.aliases) {
           this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias];
         }
@@ -1322,16 +1387,48 @@ Handlebars.JavaScriptCompiler = function() {};
         params.push("depth" + this.environment.depths.list[i]);
       }
 
+      // Perform a second pass over the output to merge content when possible
+      var source = this.mergeSource();
+
+      if (!this.isChild) {
+        var revision = Handlebars.COMPILER_REVISION,
+            versions = Handlebars.REVISION_CHANGES[revision];
+        source = "this.compilerInfo = ["+revision+",'"+versions+"'];\n"+source;
+      }
+
       if (asObject) {
-        params.push(this.source.join("\n  "));
+        params.push(source);
 
         return Function.apply(this, params);
       } else {
-        var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n  ' + this.source.join("\n  ") + '}';
+        var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n  ' + source + '}';
         Handlebars.log(Handlebars.logger.DEBUG, functionSource + "\n\n");
         return functionSource;
       }
     },
+    mergeSource: function() {
+      // WARN: We are not handling the case where buffer is still populated as the source should
+      // not have buffer append operations as their final action.
+      var source = '',
+          buffer;
+      for (var i = 0, len = this.source.length; i < len; i++) {
+        var line = this.source[i];
+        if (line.appendToBuffer) {
+          if (buffer) {
+            buffer = buffer + '\n    + ' + line.content;
+          } else {
+            buffer = line.content;
+          }
+        } else {
+          if (buffer) {
+            source += 'buffer += ' + buffer + ';\n  ';
+            buffer = undefined;
+          }
+          source += line + '\n  ';
+        }
+      }
+      return source;
+    },
 
     // [blockValue]
     //
@@ -1369,6 +1466,9 @@ Handlebars.JavaScriptCompiler = function() {};
       var current = this.topStack();
       params.splice(1, 0, current);
 
+      // Use the options value generated from the invocation
+      params[params.length-1] = 'options';
+
       this.source.push("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }");
     },
 
@@ -1392,6 +1492,9 @@ Handlebars.JavaScriptCompiler = function() {};
     // If `value` is truthy, or 0, it is coerced into a string and appended
     // Otherwise, the empty string is appended
     append: function() {
+      // Force anything that is inlined onto the stack so we don't have duplication
+      // when we examine local
+      this.flushInline();
       var local = this.popStack();
       this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
       if (this.environment.isSimple) {
@@ -1406,15 +1509,9 @@ Handlebars.JavaScriptCompiler = function() {};
     //
     // Escape `value` and append it to the buffer
     appendEscaped: function() {
-      var opcode = this.nextOpcode(), extra = "";
       this.context.aliases.escapeExpression = 'this.escapeExpression';
 
-      if(opcode && opcode.opcode === 'appendContent') {
-        extra = " + " + this.quotedString(opcode.args[0]);
-        this.eat(opcode);
-      }
-
-      this.source.push(this.appendToBuffer("escapeExpression(" + this.popStack() + ")" + extra));
+      this.source.push(this.appendToBuffer("escapeExpression(" + this.popStack() + ")"));
     },
 
     // [getContext]
@@ -1438,7 +1535,7 @@ Handlebars.JavaScriptCompiler = function() {};
     // Looks up the value of `name` on the current context and pushes
     // it onto the stack.
     lookupOnContext: function(name) {
-      this.pushStack(this.nameLookup('depth' + this.lastContext, name, 'context'));
+      this.push(this.nameLookup('depth' + this.lastContext, name, 'context'));
     },
 
     // [pushContext]
@@ -1486,7 +1583,7 @@ Handlebars.JavaScriptCompiler = function() {};
     //
     // Push the result of looking up `id` on the current data
     lookupData: function(id) {
-      this.pushStack(this.nameLookup('data', id, 'data'));
+      this.push(this.nameLookup('data', id, 'data'));
     },
 
     // [pushStringParam]
@@ -1509,13 +1606,25 @@ Handlebars.JavaScriptCompiler = function() {};
       }
     },
 
-    pushHash: function() {
-      this.push('{}');
+    emptyHash: function() {
+      this.pushStackLiteral('{}');
 
       if (this.options.stringParams) {
         this.register('hashTypes', '{}');
       }
     },
+    pushHash: function() {
+      this.hash = {values: [], types: []};
+    },
+    popHash: function() {
+      var hash = this.hash;
+      this.hash = undefined;
+
+      if (this.options.stringParams) {
+        this.register('hashTypes', '{' + hash.types.join(',') + '}');
+      }
+      this.push('{\n    ' + hash.values.join(',\n    ') + '\n  }');
+    },
 
     // [pushString]
     //
@@ -1534,7 +1643,8 @@ Handlebars.JavaScriptCompiler = function() {};
     //
     // Push an expression onto the stack
     push: function(expr) {
-      this.pushStack(expr);
+      this.inlineStack.push(expr);
+      return expr;
     },
 
     // [pushLiteral]
@@ -1577,12 +1687,14 @@ Handlebars.JavaScriptCompiler = function() {};
     invokeHelper: function(paramSize, name) {
       this.context.aliases.helperMissing = 'helpers.helperMissing';
 
-      var helper = this.lastHelper = this.setupHelper(paramSize, name);
-      this.register('foundHelper', helper.name);
+      var helper = this.lastHelper = this.setupHelper(paramSize, name, true);
 
-      this.pushStack("foundHelper ? foundHelper.call(" +
-        helper.callParams + ") " + ": helperMissing.call(" +
-        helper.helperMissingParams + ")");
+      this.push(helper.name);
+      this.replaceStack(function(name) {
+        return name + ' ? ' + name + '.call(' +
+            helper.callParams + ") " + ": helperMissing.call(" +
+            helper.helperMissingParams + ")";
+      });
     },
 
     // [invokeKnownHelper]
@@ -1594,7 +1706,7 @@ Handlebars.JavaScriptCompiler = function() {};
     // so a `helperMissing` fallback is not required.
     invokeKnownHelper: function(paramSize, name) {
       var helper = this.setupHelper(paramSize, name);
-      this.pushStack(helper.name + ".call(" + helper.callParams + ")");
+      this.push(helper.name + ".call(" + helper.callParams + ")");
     },
 
     // [invokeAmbiguous]
@@ -1609,19 +1721,18 @@ Handlebars.JavaScriptCompiler = function() {};
     // 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) {
+    invokeAmbiguous: function(name, helperCall) {
       this.context.aliases.functionType = '"function"';
 
-      this.pushStackLiteral('{}');
-      var helper = this.setupHelper(0, name);
+      this.pushStackLiteral('{}');    // Hash value
+      var helper = this.setupHelper(0, name, helperCall);
 
       var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
-      this.register('foundHelper', helperName);
 
       var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
       var nextStack = this.nextStack();
 
-      this.source.push('if (foundHelper) { ' + nextStack + ' = foundHelper.call(' + helper.callParams + '); }');
+      this.source.push('if (' + nextStack + ' = ' + helperName + ') { ' + nextStack + ' = ' + nextStack + '.call(' + helper.callParams + '); }');
       this.source.push('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '.apply(depth0) : ' + nextStack + '; }');
     },
 
@@ -1640,7 +1751,7 @@ Handlebars.JavaScriptCompiler = function() {};
       }
 
       this.context.aliases.self = "this";
-      this.pushStack("self.invokePartial(" + params.join(", ") + ")");
+      this.push("self.invokePartial(" + params.join(", ") + ")");
     },
 
     // [assignToHash]
@@ -1651,17 +1762,19 @@ Handlebars.JavaScriptCompiler = function() {};
     // Pops a value and hash off the stack, assigns `hash[key] = value`
     // and pushes the hash back onto the stack.
     assignToHash: function(key) {
-      var value = this.popStack();
+      var value = this.popStack(),
+          type;
 
       if (this.options.stringParams) {
-        var type = this.popStack();
+        type = this.popStack();
         this.popStack();
-        this.source.push("hashTypes['" + key + "'] = " + type + ";");
       }
 
-      var hash = this.topStack();
-
-      this.source.push(hash + "['" + key + "'] = " + value + ";");
+      var hash = this.hash;
+      if (type) {
+        hash.types.push("'" + key + "': " + type);
+      }
+      hash.values.push("'" + key + "': (" + value + ")");
     },
 
     // HELPERS
@@ -1675,11 +1788,27 @@ Handlebars.JavaScriptCompiler = function() {};
         child = children[i];
         compiler = new this.compiler();
 
-        this.context.programs.push('');     // Placeholder to prevent name conflicts for nested children
-        var index = this.context.programs.length;
-        child.index = index;
-        child.name = 'program' + index;
-        this.context.programs[index] = compiler.compile(child, options, this.context);
+        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.context.environments[index] = child;
+        } else {
+          child.index = index;
+          child.name = 'program' + index;
+        }
+      }
+    },
+    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;
+        }
       }
     },
 
@@ -1723,57 +1852,111 @@ Handlebars.JavaScriptCompiler = function() {};
     },
 
     pushStackLiteral: function(item) {
-      this.compileStack.push(new Literal(item));
-      return item;
+      return this.push(new Literal(item));
     },
 
     pushStack: function(item) {
+      this.flushInline();
+
       var stack = this.incrStack();
-      this.source.push(stack + " = " + item + ";");
+      if (item) {
+        this.source.push(stack + " = " + item + ";");
+      }
       this.compileStack.push(stack);
       return stack;
     },
 
     replaceStack: function(callback) {
-      var stack = this.topStack(),
-          item = callback.call(this, stack);
+      var prefix = '',
+          inline = this.isInline(),
+          stack;
+
+      // If we are currently inline then we want to merge the inline statement into the
+      // replacement statement via ','
+      if (inline) {
+        var top = this.popStack(true);
+
+        if (top instanceof Literal) {
+          // Literals do not need to be inlined
+          stack = top.value;
+        } else {
+          // Get or create the current stack name for use by the inline
+          var name = this.stackSlot ? this.topStackName() : this.incrStack();
 
-      // Prevent modification of the context depth variable. Through replaceStack
-      if (/^depth/.test(stack)) {
-        stack = this.nextStack();
+          prefix = '(' + this.push(name) + ' = ' + top + '),';
+          stack = this.topStack();
+        }
+      } else {
+        stack = this.topStack();
       }
 
-      this.source.push(stack + " = " + item + ";");
+      var item = callback.call(this, stack);
+
+      if (inline) {
+        if (this.inlineStack.length || this.compileStack.length) {
+          this.popStack();
+        }
+        this.push('(' + prefix + item + ')');
+      } else {
+        // Prevent modification of the context depth variable. Through replaceStack
+        if (!/^stack/.test(stack)) {
+          stack = this.nextStack();
+        }
+
+        this.source.push(stack + " = (" + prefix + item + ");");
+      }
       return stack;
     },
 
-    nextStack: function(skipCompileStack) {
-      var name = this.incrStack();
-      this.compileStack.push(name);
-      return name;
+    nextStack: function() {
+      return this.pushStack();
     },
 
     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;
+      if (inlineStack.length) {
+        this.inlineStack = [];
+        for (var i = 0, len = inlineStack.length; i < len; i++) {
+          var entry = inlineStack[i];
+          if (entry instanceof Literal) {
+            this.compileStack.push(entry);
+          } else {
+            this.pushStack(entry);
+          }
+        }
+      }
+    },
+    isInline: function() {
+      return this.inlineStack.length;
+    },
 
-    popStack: function() {
-      var item = this.compileStack.pop();
+    popStack: function(wrapped) {
+      var inline = this.isInline(),
+          item = (inline ? this.inlineStack : this.compileStack).pop();
 
-      if (item instanceof Literal) {
+      if (!wrapped && (item instanceof Literal)) {
         return item.value;
       } else {
-        this.stackSlot--;
+        if (!inline) {
+          this.stackSlot--;
+        }
         return item;
       }
     },
 
-    topStack: function() {
-      var item = this.compileStack[this.compileStack.length - 1];
+    topStack: function(wrapped) {
+      var stack = (this.isInline() ? this.inlineStack : this.compileStack),
+          item = stack[stack.length - 1];
 
-      if (item instanceof Literal) {
+      if (!wrapped && (item instanceof Literal)) {
         return item.value;
       } else {
         return item;
@@ -1788,22 +1971,22 @@ Handlebars.JavaScriptCompiler = function() {};
         .replace(/\r/g, '\\r') + '"';
     },
 
-    setupHelper: function(paramSize, name) {
+    setupHelper: function(paramSize, name, missingParams) {
       var params = [];
-      this.setupParams(paramSize, params);
+      this.setupParams(paramSize, params, missingParams);
       var foundHelper = this.nameLookup('helpers', name, 'helper');
 
       return {
         params: params,
         name: foundHelper,
         callParams: ["depth0"].concat(params).join(", "),
-        helperMissingParams: ["depth0", this.quotedString(name)].concat(params).join(", ")
+        helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ")
       };
     },
 
     // the params and contexts arguments are passed in arrays
     // to fill in
-    setupParams: function(paramSize, params) {
+    setupParams: function(paramSize, params, useRegister) {
       var options = [], contexts = [], types = [], param, inverse, program;
 
       options.push("hash:" + this.popStack());
@@ -1848,7 +2031,13 @@ Handlebars.JavaScriptCompiler = function() {};
         options.push("data:data");
       }
 
-      params.push("{" + options.join(",") + "}");
+      options = "{" + options.join(",") + "}";
+      if (useRegister) {
+        this.register('options', options);
+        params.push('options');
+      } else {
+        params.push(options);
+      }
       return params.join(", ");
     }
   };
@@ -1886,23 +2075,23 @@ Handlebars.JavaScriptCompiler = function() {};
 
 })(Handlebars.Compiler, Handlebars.JavaScriptCompiler);
 
-Handlebars.precompile = function(string, options) {
-  if (typeof string !== 'string') {
-    throw new Handlebars.Exception("You must pass a string to Handlebars.compile. You passed " + string);
+Handlebars.precompile = function(input, options) {
+  if (!input || (typeof input !== 'string' && input.constructor !== Handlebars.AST.ProgramNode)) {
+    throw new Handlebars.Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
   }
 
   options = options || {};
   if (!('data' in options)) {
     options.data = true;
   }
-  var ast = Handlebars.parse(string);
+  var ast = Handlebars.parse(input);
   var environment = new Handlebars.Compiler().compile(ast, options);
   return new Handlebars.JavaScriptCompiler().compile(environment, options);
 };
 
-Handlebars.compile = function(string, options) {
-  if (typeof string !== 'string') {
-    throw new Handlebars.Exception("You must pass a string to Handlebars.compile. You passed " + string);
+Handlebars.compile = function(input, options) {
+  if (!input || (typeof input !== 'string' && input.constructor !== Handlebars.AST.ProgramNode)) {
+    throw new Handlebars.Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
   }
 
   options = options || {};
@@ -1911,7 +2100,7 @@ Handlebars.compile = function(string, options) {
   }
   var compiled;
   function compile() {
-    var ast = Handlebars.parse(string);
+    var ast = Handlebars.parse(input);
     var environment = new Handlebars.Compiler().compile(ast, options);
     var templateSpec = new Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true);
     return Handlebars.template(templateSpec);
@@ -1946,12 +2135,32 @@ Handlebars.VM = {
         }
       },
       programWithDepth: Handlebars.VM.programWithDepth,
-      noop: Handlebars.VM.noop
+      noop: Handlebars.VM.noop,
+      compilerInfo: null
     };
 
     return function(context, options) {
       options = options || {};
-      return templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
+      var result = templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
+
+      var compilerInfo = container.compilerInfo || [],
+          compilerRevision = compilerInfo[0] || 1,
+          currentRevision = Handlebars.COMPILER_REVISION;
+
+      if (compilerRevision !== currentRevision) {
+        if (compilerRevision < currentRevision) {
+          var runtimeVersions = Handlebars.REVISION_CHANGES[currentRevision],
+              compilerVersions = Handlebars.REVISION_CHANGES[compilerRevision];
+          throw "Template was precompiled with an older version of Handlebars than the current runtime. "+
+                "Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").";
+        } else {
+          // Use the embedded version info since the runtime doesn't know about this revision yet
+          throw "Template was precompiled with a newer version of Handlebars than the current runtime. "+
+                "Please update your runtime to a newer version ("+compilerInfo[1]+").";
+        }
+      }
+
+      return result;
     };
   },
 
diff --git a/dist/handlebars.runtime.js b/dist/handlebars.runtime.js
index e97f5ac07..f3811348d 100644
--- a/dist/handlebars.runtime.js
+++ b/dist/handlebars.runtime.js
@@ -1,3 +1,27 @@
+/*
+
+Copyright (C) 2011 by Yehuda Katz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+
 // lib/handlebars/base.js
 
 /*jshint eqnull:true*/
@@ -5,7 +29,13 @@ this.Handlebars = {};
 
 (function(Handlebars) {
 
-Handlebars.VERSION = "1.0.rc.2";
+Handlebars.VERSION = "1.0.0-rc.3";
+Handlebars.COMPILER_REVISION = 2;
+
+Handlebars.REVISION_CHANGES = {
+  1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
+  2: '>= 1.0.0-rc.3'
+};
 
 Handlebars.helpers  = {};
 Handlebars.partials = {};
@@ -225,12 +255,32 @@ Handlebars.VM = {
         }
       },
       programWithDepth: Handlebars.VM.programWithDepth,
-      noop: Handlebars.VM.noop
+      noop: Handlebars.VM.noop,
+      compilerInfo: null
     };
 
     return function(context, options) {
       options = options || {};
-      return templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
+      var result = templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
+
+      var compilerInfo = container.compilerInfo || [],
+          compilerRevision = compilerInfo[0] || 1,
+          currentRevision = Handlebars.COMPILER_REVISION;
+
+      if (compilerRevision !== currentRevision) {
+        if (compilerRevision < currentRevision) {
+          var runtimeVersions = Handlebars.REVISION_CHANGES[currentRevision],
+              compilerVersions = Handlebars.REVISION_CHANGES[compilerRevision];
+          throw "Template was precompiled with an older version of Handlebars than the current runtime. "+
+                "Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").";
+        } else {
+          // Use the embedded version info since the runtime doesn't know about this revision yet
+          throw "Template was precompiled with a newer version of Handlebars than the current runtime. "+
+                "Please update your runtime to a newer version ("+compilerInfo[1]+").";
+        }
+      }
+
+      return result;
     };
   },
 
diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js
index 2ad183221..04e9a529a 100644
--- a/lib/handlebars/base.js
+++ b/lib/handlebars/base.js
@@ -5,7 +5,13 @@ this.Handlebars = {};
 
 (function(Handlebars) {
 
-Handlebars.VERSION = "1.0.rc.2";
+Handlebars.VERSION = "1.0.0-rc.3";
+Handlebars.COMPILER_REVISION = 2;
+
+Handlebars.REVISION_CHANGES = {
+  1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
+  2: '>= 1.0.0-rc.3'
+};
 
 Handlebars.helpers  = {};
 Handlebars.partials = {};
diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js
index fd6cdc5a5..f4485237c 100644
--- a/lib/handlebars/compiler/ast.js
+++ b/lib/handlebars/compiler/ast.js
@@ -76,8 +76,11 @@ var Handlebars = require('./base');
     for(var i=0,l=parts.length; i<l; i++) {
       var part = parts[i];
 
-      if(part === "..") { depth++; }
-      else if(part === "." || part === "this") { this.isScoped = true; }
+      if (part === ".." || part === "." || part === "this") {
+        if (dig.length > 0) { throw new Handlebars.Exception("Invalid path: " + this.original); }
+        else if (part === "..") { depth++; }
+        else { this.isScoped = true; }
+      }
       else { dig.push(part); }
     }
 
diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js
index 6919d3897..df8ced0b5 100644
--- a/lib/handlebars/compiler/base.js
+++ b/lib/handlebars/compiler/base.js
@@ -4,9 +4,13 @@ var Handlebars = require("../base");
 // BEGIN(BROWSER)
 Handlebars.Parser = handlebars;
 
-Handlebars.parse = function(string) {
+Handlebars.parse = function(input) {
+
+  // Just return if an already-compile AST was passed in.
+  if(input.constructor === Handlebars.AST.ProgramNode) { return input; }
+
   Handlebars.Parser.yy = Handlebars.AST;
-  return Handlebars.Parser.parse(string);
+  return Handlebars.Parser.parse(input);
 };
 
 Handlebars.print = function(ast) {
diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js
index 9cdf250a3..4a9a23fd7 100644
--- a/lib/handlebars/compiler/compiler.js
+++ b/lib/handlebars/compiler/compiler.js
@@ -38,6 +38,26 @@ Handlebars.JavaScriptCompiler = function() {};
 
       return out.join("\n");
     },
+    equals: function(other) {
+      var len = this.opcodes.length;
+      if (other.opcodes.length !== len) {
+        return false;
+      }
+
+      for (var i = 0; i < len; i++) {
+        var opcode = this.opcodes[i],
+            otherOpcode = other.opcodes[i];
+        if (opcode.opcode !== otherOpcode.opcode || opcode.args.length !== otherOpcode.args.length) {
+          return false;
+        }
+        for (var j = 0; j < opcode.args.length; j++) {
+          if (opcode.args[j] !== otherOpcode.args[j]) {
+            return false;
+          }
+        }
+      }
+      return true;
+    },
 
     guid: 0,
 
@@ -129,7 +149,7 @@ Handlebars.JavaScriptCompiler = function() {};
         // evaluate it by executing `blockHelperMissing`
         this.opcode('pushProgram', program);
         this.opcode('pushProgram', inverse);
-        this.opcode('pushHash');
+        this.opcode('emptyHash');
         this.opcode('blockValue');
       } else {
         this.ambiguousMustache(mustache, program, inverse);
@@ -138,7 +158,7 @@ Handlebars.JavaScriptCompiler = function() {};
         // evaluate it by executing `blockHelperMissing`
         this.opcode('pushProgram', program);
         this.opcode('pushProgram', inverse);
-        this.opcode('pushHash');
+        this.opcode('emptyHash');
         this.opcode('ambiguousBlockValue');
       }
 
@@ -162,6 +182,7 @@ Handlebars.JavaScriptCompiler = function() {};
 
         this.opcode('assignToHash', pair[0]);
       }
+      this.opcode('popHash');
     },
 
     partial: function(partial) {
@@ -202,17 +223,19 @@ Handlebars.JavaScriptCompiler = function() {};
     },
 
     ambiguousMustache: function(mustache, program, inverse) {
-      var id = mustache.id, name = id.parts[0];
+      var id = mustache.id,
+          name = id.parts[0],
+          isBlock = program != null || inverse != null;
 
       this.opcode('getContext', id.depth);
 
       this.opcode('pushProgram', program);
       this.opcode('pushProgram', inverse);
 
-      this.opcode('invokeAmbiguous', name);
+      this.opcode('invokeAmbiguous', name, isBlock);
     },
 
-    simpleMustache: function(mustache, program, inverse) {
+    simpleMustache: function(mustache) {
       var id = mustache.id;
 
       if (id.type === 'DATA') {
@@ -343,7 +366,7 @@ Handlebars.JavaScriptCompiler = function() {};
       if(mustache.hash) {
         this.hash(mustache.hash);
       } else {
-        this.opcode('pushHash');
+        this.opcode('emptyHash');
       }
 
       return params;
@@ -360,7 +383,7 @@ Handlebars.JavaScriptCompiler = function() {};
       if(mustache.hash) {
         this.hash(mustache.hash);
       } else {
-        this.opcode('pushHash');
+        this.opcode('emptyHash');
       }
 
       return params;
@@ -374,7 +397,7 @@ Handlebars.JavaScriptCompiler = function() {};
   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) {
+    nameLookup: function(parent, name /* , type*/) {
       if (/^[0-9]+$/.test(name)) {
         return parent + "[" + name + "]";
       } else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
@@ -389,7 +412,11 @@ Handlebars.JavaScriptCompiler = function() {};
       if (this.environment.isSimple) {
         return "return " + string + ";";
       } else {
-        return "buffer += " + string + ";";
+        return {
+          appendToBuffer: true,
+          content: string,
+          toString: function() { return "buffer += " + string + ";"; }
+        };
       }
     },
 
@@ -410,6 +437,7 @@ Handlebars.JavaScriptCompiler = function() {};
       this.isChild = !!context;
       this.context = context || {
         programs: [],
+        environments: [],
         aliases: { }
       };
 
@@ -419,6 +447,7 @@ Handlebars.JavaScriptCompiler = function() {};
       this.stackVars = [];
       this.registers = { list: [] };
       this.compileStack = [];
+      this.inlineStack = [];
 
       this.compileChildren(environment, options);
 
@@ -440,11 +469,11 @@ Handlebars.JavaScriptCompiler = function() {};
     },
 
     nextOpcode: function() {
-      var opcodes = this.environment.opcodes, opcode = opcodes[this.i + 1];
+      var opcodes = this.environment.opcodes;
       return opcodes[this.i + 1];
     },
 
-    eat: function(opcode) {
+    eat: function() {
       this.i = this.i + 1;
     },
 
@@ -482,7 +511,6 @@ Handlebars.JavaScriptCompiler = function() {};
 
       // Generate minimizer alias mappings
       if (!this.isChild) {
-        var aliases = [];
         for (var alias in this.context.aliases) {
           this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias];
         }
@@ -507,16 +535,48 @@ Handlebars.JavaScriptCompiler = function() {};
         params.push("depth" + this.environment.depths.list[i]);
       }
 
+      // Perform a second pass over the output to merge content when possible
+      var source = this.mergeSource();
+
+      if (!this.isChild) {
+        var revision = Handlebars.COMPILER_REVISION,
+            versions = Handlebars.REVISION_CHANGES[revision];
+        source = "this.compilerInfo = ["+revision+",'"+versions+"'];\n"+source;
+      }
+
       if (asObject) {
-        params.push(this.source.join("\n  "));
+        params.push(source);
 
         return Function.apply(this, params);
       } else {
-        var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n  ' + this.source.join("\n  ") + '}';
+        var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n  ' + source + '}';
         Handlebars.log(Handlebars.logger.DEBUG, functionSource + "\n\n");
         return functionSource;
       }
     },
+    mergeSource: function() {
+      // WARN: We are not handling the case where buffer is still populated as the source should
+      // not have buffer append operations as their final action.
+      var source = '',
+          buffer;
+      for (var i = 0, len = this.source.length; i < len; i++) {
+        var line = this.source[i];
+        if (line.appendToBuffer) {
+          if (buffer) {
+            buffer = buffer + '\n    + ' + line.content;
+          } else {
+            buffer = line.content;
+          }
+        } else {
+          if (buffer) {
+            source += 'buffer += ' + buffer + ';\n  ';
+            buffer = undefined;
+          }
+          source += line + '\n  ';
+        }
+      }
+      return source;
+    },
 
     // [blockValue]
     //
@@ -554,6 +614,9 @@ Handlebars.JavaScriptCompiler = function() {};
       var current = this.topStack();
       params.splice(1, 0, current);
 
+      // Use the options value generated from the invocation
+      params[params.length-1] = 'options';
+
       this.source.push("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }");
     },
 
@@ -577,6 +640,9 @@ Handlebars.JavaScriptCompiler = function() {};
     // If `value` is truthy, or 0, it is coerced into a string and appended
     // Otherwise, the empty string is appended
     append: function() {
+      // Force anything that is inlined onto the stack so we don't have duplication
+      // when we examine local
+      this.flushInline();
       var local = this.popStack();
       this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
       if (this.environment.isSimple) {
@@ -591,15 +657,9 @@ Handlebars.JavaScriptCompiler = function() {};
     //
     // Escape `value` and append it to the buffer
     appendEscaped: function() {
-      var opcode = this.nextOpcode(), extra = "";
       this.context.aliases.escapeExpression = 'this.escapeExpression';
 
-      if(opcode && opcode.opcode === 'appendContent') {
-        extra = " + " + this.quotedString(opcode.args[0]);
-        this.eat(opcode);
-      }
-
-      this.source.push(this.appendToBuffer("escapeExpression(" + this.popStack() + ")" + extra));
+      this.source.push(this.appendToBuffer("escapeExpression(" + this.popStack() + ")"));
     },
 
     // [getContext]
@@ -623,7 +683,7 @@ Handlebars.JavaScriptCompiler = function() {};
     // Looks up the value of `name` on the current context and pushes
     // it onto the stack.
     lookupOnContext: function(name) {
-      this.pushStack(this.nameLookup('depth' + this.lastContext, name, 'context'));
+      this.push(this.nameLookup('depth' + this.lastContext, name, 'context'));
     },
 
     // [pushContext]
@@ -671,7 +731,7 @@ Handlebars.JavaScriptCompiler = function() {};
     //
     // Push the result of looking up `id` on the current data
     lookupData: function(id) {
-      this.pushStack(this.nameLookup('data', id, 'data'));
+      this.push(this.nameLookup('data', id, 'data'));
     },
 
     // [pushStringParam]
@@ -694,13 +754,25 @@ Handlebars.JavaScriptCompiler = function() {};
       }
     },
 
-    pushHash: function() {
-      this.push('{}');
+    emptyHash: function() {
+      this.pushStackLiteral('{}');
 
       if (this.options.stringParams) {
         this.register('hashTypes', '{}');
       }
     },
+    pushHash: function() {
+      this.hash = {values: [], types: []};
+    },
+    popHash: function() {
+      var hash = this.hash;
+      this.hash = undefined;
+
+      if (this.options.stringParams) {
+        this.register('hashTypes', '{' + hash.types.join(',') + '}');
+      }
+      this.push('{\n    ' + hash.values.join(',\n    ') + '\n  }');
+    },
 
     // [pushString]
     //
@@ -719,7 +791,8 @@ Handlebars.JavaScriptCompiler = function() {};
     //
     // Push an expression onto the stack
     push: function(expr) {
-      this.pushStack(expr);
+      this.inlineStack.push(expr);
+      return expr;
     },
 
     // [pushLiteral]
@@ -762,12 +835,14 @@ Handlebars.JavaScriptCompiler = function() {};
     invokeHelper: function(paramSize, name) {
       this.context.aliases.helperMissing = 'helpers.helperMissing';
 
-      var helper = this.lastHelper = this.setupHelper(paramSize, name);
-      this.register('foundHelper', helper.name);
+      var helper = this.lastHelper = this.setupHelper(paramSize, name, true);
 
-      this.pushStack("foundHelper ? foundHelper.call(" +
-        helper.callParams + ") " + ": helperMissing.call(" +
-        helper.helperMissingParams + ")");
+      this.push(helper.name);
+      this.replaceStack(function(name) {
+        return name + ' ? ' + name + '.call(' +
+            helper.callParams + ") " + ": helperMissing.call(" +
+            helper.helperMissingParams + ")";
+      });
     },
 
     // [invokeKnownHelper]
@@ -779,7 +854,7 @@ Handlebars.JavaScriptCompiler = function() {};
     // so a `helperMissing` fallback is not required.
     invokeKnownHelper: function(paramSize, name) {
       var helper = this.setupHelper(paramSize, name);
-      this.pushStack(helper.name + ".call(" + helper.callParams + ")");
+      this.push(helper.name + ".call(" + helper.callParams + ")");
     },
 
     // [invokeAmbiguous]
@@ -794,19 +869,18 @@ Handlebars.JavaScriptCompiler = function() {};
     // 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) {
+    invokeAmbiguous: function(name, helperCall) {
       this.context.aliases.functionType = '"function"';
 
-      this.pushStackLiteral('{}');
-      var helper = this.setupHelper(0, name);
+      this.pushStackLiteral('{}');    // Hash value
+      var helper = this.setupHelper(0, name, helperCall);
 
       var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
-      this.register('foundHelper', helperName);
 
       var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
       var nextStack = this.nextStack();
 
-      this.source.push('if (foundHelper) { ' + nextStack + ' = foundHelper.call(' + helper.callParams + '); }');
+      this.source.push('if (' + nextStack + ' = ' + helperName + ') { ' + nextStack + ' = ' + nextStack + '.call(' + helper.callParams + '); }');
       this.source.push('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '.apply(depth0) : ' + nextStack + '; }');
     },
 
@@ -825,7 +899,7 @@ Handlebars.JavaScriptCompiler = function() {};
       }
 
       this.context.aliases.self = "this";
-      this.pushStack("self.invokePartial(" + params.join(", ") + ")");
+      this.push("self.invokePartial(" + params.join(", ") + ")");
     },
 
     // [assignToHash]
@@ -836,17 +910,19 @@ Handlebars.JavaScriptCompiler = function() {};
     // Pops a value and hash off the stack, assigns `hash[key] = value`
     // and pushes the hash back onto the stack.
     assignToHash: function(key) {
-      var value = this.popStack();
+      var value = this.popStack(),
+          type;
 
       if (this.options.stringParams) {
-        var type = this.popStack();
+        type = this.popStack();
         this.popStack();
-        this.source.push("hashTypes['" + key + "'] = " + type + ";");
       }
 
-      var hash = this.topStack();
-
-      this.source.push(hash + "['" + key + "'] = " + value + ";");
+      var hash = this.hash;
+      if (type) {
+        hash.types.push("'" + key + "': " + type);
+      }
+      hash.values.push("'" + key + "': (" + value + ")");
     },
 
     // HELPERS
@@ -860,11 +936,27 @@ Handlebars.JavaScriptCompiler = function() {};
         child = children[i];
         compiler = new this.compiler();
 
-        this.context.programs.push('');     // Placeholder to prevent name conflicts for nested children
-        var index = this.context.programs.length;
-        child.index = index;
-        child.name = 'program' + index;
-        this.context.programs[index] = compiler.compile(child, options, this.context);
+        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.context.environments[index] = child;
+        } else {
+          child.index = index;
+          child.name = 'program' + index;
+        }
+      }
+    },
+    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;
+        }
       }
     },
 
@@ -908,57 +1000,111 @@ Handlebars.JavaScriptCompiler = function() {};
     },
 
     pushStackLiteral: function(item) {
-      this.compileStack.push(new Literal(item));
-      return item;
+      return this.push(new Literal(item));
     },
 
     pushStack: function(item) {
+      this.flushInline();
+
       var stack = this.incrStack();
-      this.source.push(stack + " = " + item + ";");
+      if (item) {
+        this.source.push(stack + " = " + item + ";");
+      }
       this.compileStack.push(stack);
       return stack;
     },
 
     replaceStack: function(callback) {
-      var stack = this.topStack(),
-          item = callback.call(this, stack);
+      var prefix = '',
+          inline = this.isInline(),
+          stack;
+
+      // If we are currently inline then we want to merge the inline statement into the
+      // replacement statement via ','
+      if (inline) {
+        var top = this.popStack(true);
+
+        if (top instanceof Literal) {
+          // Literals do not need to be inlined
+          stack = top.value;
+        } else {
+          // Get or create the current stack name for use by the inline
+          var name = this.stackSlot ? this.topStackName() : this.incrStack();
 
-      // Prevent modification of the context depth variable. Through replaceStack
-      if (/^depth/.test(stack)) {
-        stack = this.nextStack();
+          prefix = '(' + this.push(name) + ' = ' + top + '),';
+          stack = this.topStack();
+        }
+      } else {
+        stack = this.topStack();
       }
 
-      this.source.push(stack + " = " + item + ";");
+      var item = callback.call(this, stack);
+
+      if (inline) {
+        if (this.inlineStack.length || this.compileStack.length) {
+          this.popStack();
+        }
+        this.push('(' + prefix + item + ')');
+      } else {
+        // Prevent modification of the context depth variable. Through replaceStack
+        if (!/^stack/.test(stack)) {
+          stack = this.nextStack();
+        }
+
+        this.source.push(stack + " = (" + prefix + item + ");");
+      }
       return stack;
     },
 
-    nextStack: function(skipCompileStack) {
-      var name = this.incrStack();
-      this.compileStack.push(name);
-      return name;
+    nextStack: function() {
+      return this.pushStack();
     },
 
     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;
+      if (inlineStack.length) {
+        this.inlineStack = [];
+        for (var i = 0, len = inlineStack.length; i < len; i++) {
+          var entry = inlineStack[i];
+          if (entry instanceof Literal) {
+            this.compileStack.push(entry);
+          } else {
+            this.pushStack(entry);
+          }
+        }
+      }
+    },
+    isInline: function() {
+      return this.inlineStack.length;
+    },
 
-    popStack: function() {
-      var item = this.compileStack.pop();
+    popStack: function(wrapped) {
+      var inline = this.isInline(),
+          item = (inline ? this.inlineStack : this.compileStack).pop();
 
-      if (item instanceof Literal) {
+      if (!wrapped && (item instanceof Literal)) {
         return item.value;
       } else {
-        this.stackSlot--;
+        if (!inline) {
+          this.stackSlot--;
+        }
         return item;
       }
     },
 
-    topStack: function() {
-      var item = this.compileStack[this.compileStack.length - 1];
+    topStack: function(wrapped) {
+      var stack = (this.isInline() ? this.inlineStack : this.compileStack),
+          item = stack[stack.length - 1];
 
-      if (item instanceof Literal) {
+      if (!wrapped && (item instanceof Literal)) {
         return item.value;
       } else {
         return item;
@@ -973,22 +1119,22 @@ Handlebars.JavaScriptCompiler = function() {};
         .replace(/\r/g, '\\r') + '"';
     },
 
-    setupHelper: function(paramSize, name) {
+    setupHelper: function(paramSize, name, missingParams) {
       var params = [];
-      this.setupParams(paramSize, params);
+      this.setupParams(paramSize, params, missingParams);
       var foundHelper = this.nameLookup('helpers', name, 'helper');
 
       return {
         params: params,
         name: foundHelper,
         callParams: ["depth0"].concat(params).join(", "),
-        helperMissingParams: ["depth0", this.quotedString(name)].concat(params).join(", ")
+        helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ")
       };
     },
 
     // the params and contexts arguments are passed in arrays
     // to fill in
-    setupParams: function(paramSize, params) {
+    setupParams: function(paramSize, params, useRegister) {
       var options = [], contexts = [], types = [], param, inverse, program;
 
       options.push("hash:" + this.popStack());
@@ -1033,7 +1179,13 @@ Handlebars.JavaScriptCompiler = function() {};
         options.push("data:data");
       }
 
-      params.push("{" + options.join(",") + "}");
+      options = "{" + options.join(",") + "}";
+      if (useRegister) {
+        this.register('options', options);
+        params.push('options');
+      } else {
+        params.push(options);
+      }
       return params.join(", ");
     }
   };
@@ -1071,23 +1223,23 @@ Handlebars.JavaScriptCompiler = function() {};
 
 })(Handlebars.Compiler, Handlebars.JavaScriptCompiler);
 
-Handlebars.precompile = function(string, options) {
-  if (typeof string !== 'string') {
-    throw new Handlebars.Exception("You must pass a string to Handlebars.compile. You passed " + string);
+Handlebars.precompile = function(input, options) {
+  if (!input || (typeof input !== 'string' && input.constructor !== Handlebars.AST.ProgramNode)) {
+    throw new Handlebars.Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
   }
 
   options = options || {};
   if (!('data' in options)) {
     options.data = true;
   }
-  var ast = Handlebars.parse(string);
+  var ast = Handlebars.parse(input);
   var environment = new Handlebars.Compiler().compile(ast, options);
   return new Handlebars.JavaScriptCompiler().compile(environment, options);
 };
 
-Handlebars.compile = function(string, options) {
-  if (typeof string !== 'string') {
-    throw new Handlebars.Exception("You must pass a string to Handlebars.compile. You passed " + string);
+Handlebars.compile = function(input, options) {
+  if (!input || (typeof input !== 'string' && input.constructor !== Handlebars.AST.ProgramNode)) {
+    throw new Handlebars.Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
   }
 
   options = options || {};
@@ -1096,7 +1248,7 @@ Handlebars.compile = function(string, options) {
   }
   var compiled;
   function compile() {
-    var ast = Handlebars.parse(string);
+    var ast = Handlebars.parse(input);
     var environment = new Handlebars.Compiler().compile(ast, options);
     var templateSpec = new Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true);
     return Handlebars.template(templateSpec);
diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js
index f29efe451..d9bcc6df3 100644
--- a/lib/handlebars/runtime.js
+++ b/lib/handlebars/runtime.js
@@ -20,12 +20,32 @@ Handlebars.VM = {
         }
       },
       programWithDepth: Handlebars.VM.programWithDepth,
-      noop: Handlebars.VM.noop
+      noop: Handlebars.VM.noop,
+      compilerInfo: null
     };
 
     return function(context, options) {
       options = options || {};
-      return templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
+      var result = templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
+
+      var compilerInfo = container.compilerInfo || [],
+          compilerRevision = compilerInfo[0] || 1,
+          currentRevision = Handlebars.COMPILER_REVISION;
+
+      if (compilerRevision !== currentRevision) {
+        if (compilerRevision < currentRevision) {
+          var runtimeVersions = Handlebars.REVISION_CHANGES[currentRevision],
+              compilerVersions = Handlebars.REVISION_CHANGES[compilerRevision];
+          throw "Template was precompiled with an older version of Handlebars than the current runtime. "+
+                "Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").";
+        } else {
+          // Use the embedded version info since the runtime doesn't know about this revision yet
+          throw "Template was precompiled with a newer version of Handlebars than the current runtime. "+
+                "Please update your runtime to a newer version ("+compilerInfo[1]+").";
+        }
+      }
+
+      return result;
     };
   },
 
diff --git a/spec/parser_spec.rb b/spec/parser_spec.rb
index d9187ce4d..999702752 100644
--- a/spec/parser_spec.rb
+++ b/spec/parser_spec.rb
@@ -411,4 +411,23 @@ def path(*parts)
   it "knows how to report the correct line number in errors when the first character is a newline" do
     lambda { ast_for("\n\nhello\n\nmy\n\n{{foo}") }.should raise_error(V8::JSError, /Parse error on line 7/m)
   end
+
+  context "externally compiled AST" do
+
+    it "can pass through an already-compiled AST" do
+      ast_for(@context.eval('new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]);')).should == root do
+        content "Hello"
+      end
+    end
+
+    it "can pass through an already-compiled AST via compile/precompile" do
+      @context = Handlebars::Spec::FULL_CONTEXT
+
+      code = 'Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]))();'
+      @context.eval(code).should == "Hello"
+
+      code = @context.eval 'Handlebars.precompile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]))'
+      @context.eval("(#{code})(this)").should == "Hello"
+    end
+  end
 end
diff --git a/spec/qunit_spec.js b/spec/qunit_spec.js
index fb78c37c0..fa15c3c05 100644
--- a/spec/qunit_spec.js
+++ b/spec/qunit_spec.js
@@ -20,6 +20,12 @@ if (!Handlebars) {
       return Handlebars.compile(template, options);
     }
   };
+} else {
+  var _equal = equal;
+  equals = equal = function(a, b, msg) {
+    // Allow exec with missing message params
+    _equal(a, b, msg || '');
+  };
 }
 
 suite("basic context");
@@ -170,8 +176,15 @@ test("nested paths with empty string value", function() {
 });
 
 test("literal paths", function() {
-	shouldCompileTo("Goodbye {{[@alan]/expression}} world!", {"@alan": {expression: "beautiful"}},
-			"Goodbye beautiful world!", "Literal paths can be used");
+  shouldCompileTo("Goodbye {{[@alan]/expression}} world!", {"@alan": {expression: "beautiful"}},
+      "Goodbye beautiful world!", "Literal paths can be used");
+  shouldCompileTo("Goodbye {{[foo bar]/expression}} world!", {"foo bar": {expression: "beautiful"}},
+      "Goodbye beautiful world!", "Literal paths can be used");
+});
+
+test('literal references', function() {
+  shouldCompileTo("Goodbye {{[foo bar]}} world!", {"foo bar": "beautiful"},
+      "Goodbye beautiful world!", "Literal paths can be used");
 });
 
 test("that current context path ({{.}}) doesn't hit helpers", function() {
@@ -194,6 +207,13 @@ test("this keyword in paths", function() {
   shouldCompileTo(string, hash, "helloHelloHELLO", "This keyword evaluates in more complex paths");
 });
 
+test("this keyword nested inside path", function() {
+  var string = "{{#hellos}}{{text/this/foo}}{{/hellos}}";
+  shouldThrow(function() {
+      CompilerContext.compile(string);
+    }, Error, "Should throw exception");
+});
+
 test("this keyword in helpers", function() {
   var helpers = {foo: function(value) {
       return 'bar ' + value;
@@ -208,6 +228,13 @@ test("this keyword in helpers", function() {
   shouldCompileTo(string, [hash, helpers], "bar hellobar Hellobar HELLO", "This keyword evaluates in more complex paths");
 });
 
+test("this keyword nested inside helpers param", function() {
+  var string = "{{#hellos}}{{foo text/this/foo}}{{/hellos}}";
+  shouldThrow(function() {
+      CompilerContext.compile(string);
+    }, Error, "Should throw exception");
+});
+
 suite("inverted sections");
 
 test("inverted sections with unset value", function() {
@@ -273,6 +300,14 @@ test("block with complex lookup", function() {
                   "Templates can access variables in contexts up the stack with relative path syntax");
 });
 
+test("block with complex lookup using nested context", function() {
+  var string = "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}";
+
+  shouldThrow(function() {
+      CompilerContext.compile(string);
+    }, Error, "Should throw exception");
+});
+
 test("helper with complex lookup$", function() {
   var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}";
   var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]};
@@ -1319,7 +1354,7 @@ test("bug reported by @fat where lambdas weren't being properly resolved", funct
 test("Passing falsy values to Handlebars.compile throws an error", function() {
   shouldThrow(function() {
     CompilerContext.compile(null);
-  }, "You must pass a string to Handlebars.compile. You passed null");
+  }, "You must pass a string or Handlebars AST to Handlebars.compile. You passed null");
 });
 
 test('GH-408: Multiple loops fail', function() {
