keep replace replace keep keep keep keep keep

var CS = ConstraintSolver;
var PV = PackageVersion;

CS.Solver = function (input, options) {
  var self = this;
  check(input, CS.Input);

  self.input = input;
  self.errors = []; // [String]

  self.debugLog = null;
  if (options && options.debugLog) {
    self.debugLog = [];
  }

  self._getVersionInfo = _.memoize(PV.parse);
  self._getConstraintFormula = _.memoize(_getConstraintFormula,
                                         function (p, vConstraint) {
                                           return p + "@" + vConstraint.raw;
                                         });
};

CS.Solver.prototype.getSolution = function () {
  var self = this;

  self.logic = new Logic.Solver;

  self._requireTopLevelDependencies(); // may throw

  // "bar" -> ["foo 1.0.0", ...] if "foo 1.0.0" requires "bar"
  self._requirers = {};
  // package names we come across that aren't in the cache
  self._unknownPackages = {}; // package name -> true
  // populates _requirers and _unknownPackages:
  self._enforceStrongDependencies();

  // if this is greater than 0, we will throw an error later
  // and say what they are, after we run the constraints
  // and the cost function.
  self._numUnknownPackagesNeeded = self._minimizeUnknownPackages();

  self._constraintSatisfactionOptions = {
    anticipatedPrereleases: self.input.anticipatedPrereleases
  };
  self._allConstraints = []; // added to by self._addConstraint(...)
  self._numConflicts = self._enforceConstraints();

  self._costFunction = self._generateCostFunction();
  self._minimizeCostFunction();

  self._solution = self.logic.solve();
  if (! self._solution) {
    // can't get here; should be in a satisfiable state (or have thrown)
    throw new Error("Unexpected unsatisfiability");
  }

  self._throwUnknownPackages();
  self._throwConflicts();

  var versionMap = {};
  _.each(self._solution.getTrueVars(), function (x) {
    if (x.indexOf(' ') >= 0) {
      var pv = CS.PackageAndVersion.fromString(x);
      versionMap[pv.package] = pv.version;
    }
  });

  return {
    neededToUseUnanticipatedPrereleases: false, // XXX
    answer: versionMap
  };
};

CS.Solver.prototype.getCostReport = function () {
  var self = this;
  var solution = self._solution;
  return _.map(self._costFunction.components, function (comp) {
    var total = 0;
    var terms = {};
    _.each(comp.terms, function (t, i) {
      var w = comp.weights[i];
      if (w && solution.evaluate(t)) {
        total += w;
        terms[t] = w;
      }
    });
    return [comp.name, total, terms];
  });
};

var pvVar = function (p, v) {
  return p + ' ' + v;
};

CS.Solver.prototype._requireTopLevelDependencies = function () {
  var self = this;
  var input = self.input;

  _.each(input.dependencies, function (p) {
    if (! input.isKnownPackage(p)) {
      // Unknown package at top level
      self.errors.push('unknown package: ' + p);
    } else {
      self.logic.require(p);
      if (self.debugLog) {
        self.debugLog.push('REQUIRE ' + p);
      }
    }
  });

  self.throwAnyErrors();
};

CS.Solver.prototype._enforceStrongDependencies = function () {
  var self = this;
  var input = self.input;
  var cache = input.catalogCache;

  var unknownPackages = self._unknownPackages;
  var requirers = self._requirers;

  cache.eachPackage(function (p, versions) {
    // ["foo 1.0.0", "foo 1.0.1", ...] for a given "foo"
    var packageAndVersions = _.map(versions, function (v) {
      return pvVar(p, v);
    });
    // At most one of ["foo 1.0.0", "foo 1.0.1", ...] is true.
    self.logic.require(Logic.atMostOne(packageAndVersions));
    if (self.debugLog) {
      self.debugLog.push("AT MOST ONE: " +
                         (packageAndVersions.join(', ') || '[]'));
    }
    // The variable "foo" is true if and only if at least one of the
    // variables ["foo 1.0.0", "foo 1.0.1", ...] is true.
    // Note that this doesn't apply to unknown packages (packages
    // that aren't in the cache), which aren't visited here.
    // We will forbid them later, and generate a good error message
    // if that leads to unsatisfiability.
    self.logic.require(Logic.equiv(p, Logic.or(packageAndVersions)));
    if (self.debugLog) {
      self.debugLog.push(p + ' IFF ONE OF: ' +
                         (packageAndVersions.join(', ') || '[]'));
    }

    _.each(versions, function (v) {
      var pv = pvVar(p, v);
      _.each(cache.getDependencyMap(p, v), function (dep) {
        // `dep` is a CS.Dependency
        var p2 = dep.packageConstraint.package;
        if (! input.isKnownPackage(p2)) {
          unknownPackages[p2] = true;
        }
        if (! dep.isWeak) {
          requirers[p2] = (requirers[p2] || []);
          requirers[p2].push(pv);
        }
      });
    });
  });

  _.each(requirers, function (pvs, p) {
    // pvs are all the package-versions that require p.
    // We want to select p if-and-only-if we select one of the pvs
    // (except when p is a root dependency, in which case
    // we've already required it).
    if (! input.isRootDependency(p)) {
      self.logic.require(Logic.equiv(p, Logic.or(pvs)));
      if (self.debugLog) {
        self.debugLog.push(p + ' IFF ONE OF: ' +
                           (pvs.join(', ') || '[]'));
      }
    }
  });
};

CS.Solver.prototype.throwAnyErrors = function () {
  if (this.errors.length) {
    CS.throwConstraintSolverError(this.errors.join('\n'));
  }
};

CS.Solver.prototype._minimizeUnknownPackages = function () {
  var self = this;
  var unknownPackages = _.keys(self._unknownPackages);
  var useAnyUnknown = Logic.or(unknownPackages);

  var sol = self.logic.solve();
  if (! sol) {
    // so far we have no version constraints, and it's a valid solution
    // to just select some version of every package, and also select
    // all the non-existent packages that are mentioned, since we haven't
    // forbid them yet.
    throw new Error("Unexpected unsatisfiability");
  }

  if (self.logic.solveAssuming(Logic.not(useAnyUnknown))) {
    if (self.debugLog) {
      self.debugLog.push('FORBID: ' +
                         (unknownPackages.join(', ') || '[]'));
    }
    self.logic.forbid(unknownPackages);
    return [];
  } else {
    // apparently we can't ignore some of the unknown packages;
    // we have to use at least one.  this will become an error,
    // but we don't want to throw it yet so that we can run
    // the cost function so that we are showing realistic versions
    // in the error.
    sol = self.logic.minimize(sol, unknownPackages, 1);
    var result = sol.getWeightedSum(unknownPackages, 1);
    if (self.debugLog) {
      self.debugLog.push('AT MOST ' + result + ' OF: ' +
                         (unknownPackages.join(', ') || '[]'));
    }
    return result;
  }
};

CS.Solver.prototype._generateCostFunction = function () {
  var self = this;
  // classify packages into categories, which determine what we
  // are supposed to be optimizing about the version and with what
  // priority.
  var costFunc = new CS.Solver.CostFunction();
  // 1 if we change the major version of a root dep with previous version
  costFunc.addComponent('previous_root_major');
  // 1 if we move a root dep backwards in the same major version
  costFunc.addComponent('previous_root_incompat');
  // 1 if we change a root dep from previous version
  costFunc.addComponent('previous_root_change');
  // number of versions forward or backward we move a root dep
  costFunc.addComponent('previous_root_distance');

  costFunc.addComponent('previous_indirect_major');
  costFunc.addComponent('previous_indirect_incompat');
  costFunc.addComponent('previous_indirect_change');
  costFunc.addComponent('previous_indirect_distance');

  // XXX probably need some more nuance here.
  // In general, we want packages we're upgrading and new root dependencies
  // (just added or in case of no previous solution) to be as new as possible,
  // so we penalize oldness.  New indirect dependencies should be as old as
  // possible, so we penalize newness.
  costFunc.addComponent('upgrade_oldness');
  costFunc.addComponent('new_root_oldness');
  costFunc.addComponent('new_indirect_newness');

  var input = self.input;
  input.catalogCache.eachPackage(function (p, versions) {
    if (input.isUpgrading(p)) {
      _.each(versions, function (v, i) {
        var pv = pvVar(p, v);
        costFunc.addToComponent('upgrade_oldness', pv,
                                versions.length - 1 - i);
      });
    } else if (input.isInPreviousSolution(p)) {
      var previous = input.previousSolution[p];
      var previousVInfo = self._getVersionInfo(previous);
      if (input.isRootDependency(p)) {
        // previous_root

        var firstGteIndex = versions.length;
        // previous version should be in versions array, but we don't
        // want to assume that
        var previousFound = false;
        _.each(versions, function (v, i) {
          var vInfo = self._getVersionInfo(v);
          var pv = pvVar(p, v);
          if (vInfo.major !== previousVInfo.major) {
            costFunc.addToComponent('previous_root_major', pv, 1);
          } else if (PV.lessThan(vInfo, previousVInfo)) {
            costFunc.addToComponent('previous_root_incompat', pv, 1);
          }
          if (v !== previous) {
            costFunc.addToComponent('previous_root_change', pv, 1);
          }
          if (firstGteIndex === versions.length &&
              ! PV.lessThan(vInfo, previousVInfo)) {
            firstGteIndex = i;
            if (v === previous) {
              previousFound = true;
            }
          }
        });
        _.each(versions, function (v, i) {
          var pv = pvVar(p, v);
          if (i < firstGteIndex) {
            costFunc.addToComponent('previous_root_distance', pv,
                                    firstGteIndex - i);
          } else {
            costFunc.addToComponent('previous_root_distance', pv,
                                    i - firstGteIndex +
                                    (previousFound ? 0 : 1));
          }
        });

      } else {
        // previous_indirect

        var firstGteIndex = versions.length;
        // previous version should be in versions array, but we don't
        // want to assume that
        var previousFound = false;
        _.each(versions, function (v, i) {
          var vInfo = self._getVersionInfo(v);
          var pv = pvVar(p, v);
          if (vInfo.major !== previousVInfo.major) {
            costFunc.addToComponent('previous_indirect_major', pv, 1);
          } else if (PV.lessThan(vInfo, previousVInfo)) {
            costFunc.addToComponent('previous_indirect_incompat', pv, 1);
          }
          if (v !== previous) {
            costFunc.addToComponent('previous_indirect_change', pv, 1);
          }
          if (firstGteIndex === versions.length &&
              ! PV.lessThan(vInfo, previousVInfo)) {
            firstGteIndex = i;
            if (v === previous) {
              previousFound = true;
            }
          }
        });
        _.each(versions, function (v, i) {
          var pv = pvVar(p, v);
          if (i < firstGteIndex) {
            costFunc.addToComponent('previous_indirect_distance', pv,
                                    firstGteIndex - i);
          } else {
            costFunc.addToComponent('previous_indirect_distance', pv,
                                    i - firstGteIndex +
                                    (previousFound ? 0 : 1));
          }
        });
      }
    } else {
      if (input.isRootDependency(p)) {
        // new_root
        _.each(versions, function (v, i) {
          var pv = pvVar(p, v);
          costFunc.addToComponent('new_root_oldness', pv,
                                  versions.length - 1 - i);
        });
      } else {
        // new_indirect
        _.each(versions, function (v, i) {
          var pv = pvVar(p, v);
          costFunc.addToComponent('new_indirect_newness', pv, i);
        });
      }
    }
  });

  return costFunc;
};

var getDebugLogForWeightedSum = function (solution, terms, weights) {
  if (typeof weights === 'number') {
    weights = _.map(terms, function () { return weights; });
  }
  return 'REQUIRE ' + (_.map(terms, function (t, i) {
    return weights[i] + '*(' + t + ')';
  }).join(' + ') || '0')+ ' = ' + solution.getWeightedSum(terms, weights);
};

CS.Solver.prototype._minimizeCostFunction = function () {
  var self = this;
  var sol = self.logic.solve();
  if (! sol) {
    // we've already been checking as we go along that the
    // problem is still solvable
    throw new Error("Unexpected unsatisfiability");
  }

  var costFunc = self._costFunction;
  _.each(costFunc.components, function (comp) {
    sol = self.logic.minimize(sol, comp.terms, comp.weights);
    if (self.debugLog) {
      self.debugLog.push(getDebugLogForWeightedSum(
        sol, comp.terms, comp.weights));
    }
  });
};

CS.Solver.prototype._getOkVersions = function (toPackage, vConstraint,
                                               targetVersions) {
  var self = this;
  return _.compact(_.map(targetVersions, function (v) {
    if (CS.isConstraintSatisfied(
      toPackage, vConstraint, v, self._constraintSatisfactionOptions)) {
      return pvVar(toPackage, v);
    } else {
      return null;
    }
  }));
};

// The CS.Solver constructor turns this into a memoized method.
// Memoizing the Formula object reduces clause generation a lot.
var _getConstraintFormula = function (toPackage, vConstraint) {
  var self = this;

  var targetVersions = self.input.catalogCache.getPackageVersions(toPackage);
  var okVersions = self._getOkVersions(toPackage, vConstraint, targetVersions);

  if (okVersions.length === targetVersions.length) {
    return Logic.TRUE;
  } else {
    return Logic.or(Logic.not(toPackage), okVersions);
  }
};

CS.Solver.prototype._addConstraint = function (fromVar, toPackage, vConstraint) {
  // fromVar is a return value of pvVar(p, v), or null for a top-level constraint
  check(fromVar, Match.OneOf(String, null));
  check(toPackage, String); // package name
  check(vConstraint, CS.Input.VersionConstraintType);

  var self = this;
  var allConstraints = self._allConstraints;

  var newConstraint = new CS.Solver.Constraint(
    "constraint#" + allConstraints.length, fromVar, toPackage, vConstraint);
  allConstraints.push(newConstraint);

  // We logically require that IF:
  //
  // - the constraint var is true, meaning the constraint is active and not
  // being skipped for conflict-detection purposes; and
  // - fromVar is true, meaning we have selected the package version having
  // the constraint, or is non-existent, meaning this is a top-level
  // constraint; and
  // - toPackage is true, meaning we have selected the package that the
  // constraint is about
  //
  // ... then one of the versions of toPackage that satisfies the constraint
  // must be selected.
  self.logic.require(
    Logic.implies(newConstraint.varName,
                  Logic.or(fromVar ? Logic.not(fromVar) : [],
                           self._getConstraintFormula(toPackage, vConstraint))));

  if (self.debugLog) {
    var conditions = [newConstraint.varName];
    if (fromVar) {
      conditions.push('(' + fromVar + ')');
    }
    conditions.push(toPackage);
    self.debugLog.push(
      'IF ' + conditions.join(' AND ') + ' THEN ONE OF: ' +
        (self._getOkVersions(
          toPackage, vConstraint,
          self.input.catalogCache.getPackageVersions(toPackage)
        ).join(', ') || '[]'));
  }
};

// Register the constraints with the logic solver, but don't actually
// enforce them yet (so we can do conflict detection).
CS.Solver.prototype._enforceConstraints = function () {
  var self = this;
  var cache = self.input.catalogCache;

  // top-level constraints
  _.each(self.input.constraints, function (c) {
    self._addConstraint(null, c.package, c.versionConstraint);
  });

  // constraints specified by package versions
  cache.eachPackage(function (p, versions) {
    _.each(versions, function (v) {
      var pv = pvVar(p, v);
      _.each(cache.getDependencyMap(p, v), function (dep) {
        // `dep` is a CS.Dependency
        var p2 = dep.packageConstraint.package;
        if (self.input.isKnownPackage(p2)) {
          self._addConstraint(pv, p2,
                              dep.packageConstraint.versionConstraint);
        }
      });
    });
  });

  // minimize conflicts
  var allConstraints = self._allConstraints;
  var allConstraintVars = _.pluck(allConstraints, 'varName');
  var allConstraintsActive = Logic.and(allConstraintVars);

  var sol = self.logic.solveAssuming(allConstraintsActive);
  if (sol) {
    self.logic.require(allConstraintVars);
    if (self.debugLog) {
      self.debugLog.push('REQUIRE: ' +
                         (allConstraintVars.join(', ') || '[]'));
    }
    return 0; // no conflicts
  }

  // Couldn't solve with all constraints.  Figure out how many constraints
  // we need to skip to achieve satisfiability, and later we will report
  // them as conflicts to the user.

  // First solve with no constraints necessarily active (as a sanity check
  // and as a starting point for optimization).
  sol = self.logic.solve();
  if (! sol) {
    // We should either still be satisfiable or have thrown an error.
    throw new Error("Unexpected unsatisfiability");
  }

  sol = self.logic.maximize(sol, allConstraintVars, 1);
  if (self.debugLog) {
    self.debugLog.push(getDebugLogForWeightedSum(
      sol, allConstraintVars, 1));
  }

  return allConstraintVars.length - sol.getWeightedSum(allConstraintVars, 1);
};

CS.Solver.prototype._throwUnknownPackages = function () {
  var self = this;

  if (! self._numUnknownPackagesNeeded) {
    return;
  }

  var solution = self._solution;
  var unknownPackages = _.keys(self._unknownPackages);
  var unknownPackagesNeeded = _.filter(unknownPackages, function (p) {
    return solution.evaluate(p);
  });
  _.each(unknownPackagesNeeded, function (p) {
    self.errors.push('unknown package: ' + p);
  });
  self.throwAnyErrors();
};

CS.Solver.prototype._throwConflicts = function () {
  var self = this;

  if (! self._numConflicts) {
    return;
  }

  var allConstraints = self._allConstraints;

  var solution = self._solution;

  _.each(allConstraints, function (c) {
    // c is a CS.Solver.Constraint
    if (! solution.evaluate(c.varName)) {
      // skipped this constraint
      var possibleVersions =
            self.input.catalogCache.getPackageVersions(c.toPackage);
      var chosenVersion = _.find(possibleVersions, function (v) {
        return solution.evaluate(pvVar(c.toPackage, v));
      });
      if (! chosenVersion) {
        // this can't happen, because for a constraint to be a problem,
        // we must have chosen some version of the package it applies to!
        throw new Error("Internal error: Version not found");
      }
      var error = (
        'conflict: constraint ' + (new PV.PackageConstraint(
          c.toPackage, c.vConstraint)) +
          ' is not satisfied by ' + c.toPackage + ' ' + chosenVersion + '.');

      error += '\nConstraints:';

      _.each(allConstraints, function (c2) {
        if (c2.toPackage === c.toPackage) {
          var paths;
          if (c2.fromVar) {
            paths = self._getAllPathsToPackageVersion(
              CS.PackageAndVersion.fromString(c2.fromVar));
          } else {
            paths = [['top level']];
          }
          _.each(paths, function (path) {
            error += '\n  ' + (new PV.PackageConstraint(
              c.toPackage, c2.vConstraint)) + ' <- ' + path.join(' <- ');
          });
        }
      });

      self.errors.push(error);
    }
  });

  // always throws, never returns
  self.throwAnyErrors();

  throw new Error("Internal error: conflicts could not be explained");
};

// Takes a PackageVersion and returns an array of arrays of PackageVersions.
// If the `packageVersion` is not selected in `self._solution`, returns
// an empty array.  Otherwise, returns an array of all paths from
// root dependencies to the package, in reverse order.  In other words,
// the first element of each path is `packageVersion`,
// and the last element is the selected version of a root dependency.
CS.Solver.prototype._getAllPathsToPackageVersion = function (packageAndVersion) {
  check(packageAndVersion, CS.PackageAndVersion);
  var self = this;
  var solution = self._solution;
  if (! solution.evaluate(packageAndVersion.toString())) {
    return [];
  } else if (self.input.isRootDependency(packageAndVersion.package)) {
    return [[packageAndVersion]];
  } else {
    var requirers = self._requirers[packageAndVersion.package];
    var paths = [];
    _.each(requirers, function (r) {
      if (solution.evaluate(r)) {
        var pv = CS.PackageAndVersion.fromString(r);
        _.each(self._getAllPathsToPackageVersion(pv), function (path) {
          paths.push([packageAndVersion].concat(path));
        });
      }
    });
    return paths;
  }
};

CS.Solver.CostFunction = function () {
  this.components = [];
  this.componentsByName = {};
};

CS.Solver.CostFunction.prototype.addComponent = function (name, terms, weights) {
  check(name, String);
  terms = terms || [];
  check(terms, [String]);
  weights = weights || [];
  check(weights, [Logic.WholeNumber]);
  var comp = {name: name,
              terms: terms,
              weights: weights};
  this.components.push(comp);
  this.componentsByName[name] = comp;
};

CS.Solver.CostFunction.prototype.addToComponent = function (
  compName, term, weight) {

  check(compName, String);
  check(term, String);
  check(weight, Logic.WholeNumber);
  if (! _.has(this.componentsByName, compName)) {
    throw new Error("No such cost function component: " + compName);
  }
  var comp = this.componentsByName[compName];
  comp.terms.push(term);
  comp.weights.push(weight);
};

CS.Solver.Constraint = function (varName, fromVar, toPackage, vConstraint) {
  this.varName = varName;
  this.fromVar = fromVar;
  this.toPackage = toPackage;
  this.vConstraint = vConstraint;

  check(this.varName, String);
  // this.fromVar is a return value of pvVar(p, v), or null for a
  // top-level constraint
  check(this.fromVar, Match.OneOf(String, null));
  check(this.toPackage, String); // package name
  check(this.vConstraint, CS.Input.VersionConstraintType);
};


Package.describe({
  summary: "Check whether a value matches a pattern",
  version: '1.0.4'
});

Package.onUse(function (api) {
  api.use(['underscore', 'ejson'], ['client', 'server']);

  api.export(['check', 'Match']);

  api.addFiles('match.js', ['client', 'server']);
});

Package.onTest(function (api) {
  api.use(['check', 'tinytest', 'underscore', 'ejson'], ['client', 'server']);

  api.addFiles('match_test.js', ['client', 'server']);
});


Package.describe({
  summary: "Meteor Reactive Templating library",
  version: '2.0.4'
});

Package.onUse(function (api) {
  api.export(['Blaze', 'UI', 'Handlebars']);
  api.use('jquery'); // should be a weak dep, by having multiple "DOM backends"
  api.use('tracker');
  api.use('underscore'); // only the subset in microscore.js
  api.use('htmljs');
  api.imply('htmljs');
  api.use('observe-sequence');
  api.use('reactive-var');

  api.addFiles([
    'preamble.js'
  ]);

  // client-only files
  api.addFiles([
    'dombackend.js',
    'domrange.js',
    'events.js',
    'attrs.js',
    'materializer.js'
  ], 'client');

  // client and server
  api.addFiles([
    'exceptions.js',
    'view.js',
    'builtins.js',
    'lookup.js',
    'template.js',
    'backcompat.js'
  ]);
});

Package.onTest(function (api) {
  api.use('tinytest');
  api.use('jquery'); // strong dependency, for testing jQuery backend
  api.use('blaze');
  api.use('test-helpers');
  api.use('underscore');
  api.use('blaze-tools'); // for BlazeTools.toJS
  api.use('html-tools');
  api.use('reactive-var');

  api.addFiles('view_tests.js');
  api.addFiles('render_tests.js', 'client');
});


// Welcome message
.wrapper-welcome {
  .position(absolute, 1.25em, auto, auto, 0, 25%);
  padding: 0 1.25rem;
  z-index: -1;

  // Show & hide message on web/desktop
  display: none;
  @media screen and (min-width: 60em) { display: block; }

  .title-welcome {
    .font-s3;
    .type-black;
    .title-caps;
    color: @color-primary;
    margin-bottom: .5em;
    text-indent: 0;
  }

  .description-welcome {
    .font-s2;
    color: @color-well;

    & + .description-welcome { margin-top: .5rem; }

    a {
      .type-bold;
      color: @color-medium-well;

      &:hover { color: darken(@color-well, 10%); }
      &:active { color: @color-well; }
    }
  }
}


JsHintLinter.prototype.processFilesForTarget = function (files, globals) {

keep keep keep keep add keep keep keep keep keep

var _ = require('underscore');

var buildmessage = require('./buildmessage.js');
var compiler = require('./compiler.js');
var files = require('./files.js');
var isopackModule = require('./isopack.js');
var utils = require('./utils.js');
var watch = require('./watch.js');
var colonConverter = require("./colon-converter.js");

exports.IsopackCache = function (options) {
  var self = this;
  options = options || {};

  // cacheDir may be null; in this case, we just don't ever save things to disk.
  self.cacheDir = options.cacheDir;

  // Root directory for caches used by build plugins.  Can be null, in which
  // case we never give the build plugins a cache.  The directory structure is:
  // <pluginCacheDirRoot>/<escapedPackageName>/<version>, where <version> is
  // either the package's version if it's a versioned package, or "local" if
  // it's a local package.  In the latter case, we make sure to empty it any
  // time we rebuild the package.
  self._pluginCacheDirRoot = options.pluginCacheDirRoot;

  // This is a bit of a hack, but basically: we really don't want to spend time
  // building web.cordova unibuilds in a project that doesn't have any Cordova
  // platforms. (Note that we need to be careful with 'meteor publish' to still
  // publish a web.cordova unibuild!)
  self._includeCordovaUnibuild = !! options.includeCordovaUnibuild;

  // Defines the versions of packages that we build. Must be set.
  self._packageMap = options.packageMap;

  // tropohouse may be null; in this case, we can't load versioned packages.
  // eg, for building isopackets.
  self._tropohouse = options.tropohouse;

  // If provided, this is another IsopackCache for the same cache dir; when
  // loading Isopacks, if they are definitely unchanged we can load the
  // in-memory objects from this cache instead of recompiling.
  self._previousIsopackCache = options.previousIsopackCache;
  if (self._previousIsopackCache &&
      self._previousIsopackCache.cacheDir !== self.cacheDir) {
    throw Error("previousIsopackCache has different cacheDir!");
  }

  // Map from package name to Isopack.
  self._isopacks = {};

  self._noLineNumbers = !! options.noLineNumbers;

  self.allLoadedLocalPackagesWatchSet = new watch.WatchSet;

  // a number that is incremented from the previous IsopackCache passed, used to
  // figure out what packages are freshly (re)built and which were pre-built
  // previously.
  self._buildIteration = 0;
  if (self._previousIsopackCache) {
    self._buildIteration = self._previousIsopackCache._buildIteration + 1;
  }
};

_.extend(exports.IsopackCache.prototype, {
  buildLocalPackages: function (rootPackageNames) {
    var self = this;
    buildmessage.assertInCapture();

    if (self.cacheDir)
      files.mkdir_p(self.cacheDir);

    var onStack = {};
    if (rootPackageNames) {
      _.each(rootPackageNames, function (name) {
        self._ensurePackageLoaded(name, onStack);
      });
    } else {
      self._packageMap.eachPackage(function (name, packageInfo) {
        self._ensurePackageLoaded(name, onStack);
      });
    }
  },

  wipeCachedPackages: function (packages) {
    var self = this;
    if (packages) {
      // Wipe specific packages.
      _.each(packages, function (packageName) {
        if (self.cacheDir) {
          files.rm_recursive(self._isopackDir(packageName));
        }
        if (self._pluginCacheDirRoot) {
          files.rm_recursive(self._pluginCacheDirForPackage(packageName));
        }
      });
    } else {
      // Wipe all packages.
      if (self.cacheDir) {
        files.rm_recursive(self.cacheDir);
      }
      if (self._pluginCacheDirRoot) {
        files.rm_recursive(self._pluginCacheDirRoot);
      }
    }
  },

  // Returns the isopack (already loaded in memory) for a given name. It is an
  // error to call this if it's not already loaded! So it should only be called
  // after buildLocalPackages has returned, or in the process of building a
  // package whose dependencies have all already been built.
  getIsopack: function (name) {
    var self = this;
    if (! _.has(self._isopacks, name))
      throw Error("isopack " + name + " not yet loaded?");
    return self._isopacks[name];
  },

  eachBuiltIsopack: function (iterator) {
    var self = this;
    _.each(self._isopacks, function (isopack, packageName) {
      iterator(packageName, isopack);
    });
  },

  _ensurePackageLoaded: function (name, onStack) {
    var self = this;
    buildmessage.assertInCapture();
    if (_.has(self._isopacks, name))
      return;

    var ensureLoaded = function (depName) {
      if (_.has(onStack, depName)) {
        buildmessage.error("circular dependency between packages " +
                           name + " and " + depName);
        // recover by not enforcing one of the dependencies
        return;
      }
      onStack[depName] = true;
      self._ensurePackageLoaded(depName, onStack);
      delete onStack[depName];
    };

    var packageInfo = self._packageMap.getInfo(name);
    if (! packageInfo)
      throw Error("Depend on unknown package " + name + "?");
    var previousIsopack = null;
    if (self._previousIsopackCache &&
        _.has(self._previousIsopackCache._isopacks, name)) {
      var previousInfo = self._previousIsopackCache._packageMap.getInfo(name);
      if ((packageInfo.kind === 'versioned' &&
           previousInfo.kind === 'versioned' &&
           packageInfo.version === previousInfo.version) ||
          (packageInfo.kind === 'local' &&
           previousInfo.kind === 'local' &&
           (packageInfo.packageSource.sourceRoot ===
            previousInfo.packageSource.sourceRoot))) {
        previousIsopack = self._previousIsopackCache._isopacks[name];
      }
    }

    if (packageInfo.kind === 'local') {
      var packageNames =
            packageInfo.packageSource.getPackagesToLoadFirst(self._packageMap);
      buildmessage.enterJob("preparing to build package " + name, function () {
        _.each(packageNames, function (depName) {
          ensureLoaded(depName);
        });
        // If we failed to load something that this package depends on, don't
        // load it.
        if (buildmessage.jobHasMessages())
          return;
        self._loadLocalPackage(name, packageInfo, previousIsopack);
      });
    } else if (packageInfo.kind === 'versioned') {
      // We don't have to build this package, and we don't have to build its
      // dependencies either! Just load it from disk.

      if (!self._tropohouse) {
        throw Error("Can't load versioned packages without a tropohouse!");
      }

      var isopack = null, packagesToLoad = [];
      if (previousIsopack) {
        // We can always reuse a previous Isopack for a versioned package, since
        // we assume that it never changes.  (Admittedly, this means we won't
        // notice if we download an additional build for the package.)
        isopack = previousIsopack;
        packagesToLoad = isopack.getStrongOrderedUsedAndImpliedPackages();
      }
      if (! isopack) {
        // Load the isopack from disk.
        buildmessage.enterJob(
          "loading package " + name + "@" + packageInfo.version,
          function () {
            var pluginCacheDir;
            if (self._pluginCacheDirRoot) {
              pluginCacheDir = self._pluginCacheDirForVersion(
                name, packageInfo.version);
              files.mkdir_p(pluginCacheDir);
            }
            var isopackPath = self._tropohouse.packagePath(
              name, packageInfo.version);

            var Isopack = isopackModule.Isopack;
            isopack = new Isopack();
            isopack.initFromPath(name, isopackPath, {
              pluginCacheDir: pluginCacheDir
            });
            // If loading the isopack fails, then we don't need to look for more
            // packages to load, but we should still recover by putting it in
            // self._isopacks.
            if (buildmessage.jobHasMessages())
              return;
            packagesToLoad = isopack.getStrongOrderedUsedAndImpliedPackages();
          });
      }

      self._isopacks[name] = isopack;
      // Also load its dependencies. This is so that if this package is being
      // built as part of a plugin, all the transitive dependencies of the
      // plugin are loaded.
      _.each(packagesToLoad, function (packageToLoad) {
        ensureLoaded(packageToLoad);
      });
    } else {
      throw Error("unknown packageInfo kind?");
    }
  },

  _loadLocalPackage: function (name, packageInfo, previousIsopack) {
    var self = this;
    buildmessage.assertInCapture();
    buildmessage.enterJob("building package " + name, function () {
      var isopack;
      if (previousIsopack && self._checkUpToDatePreloaded(previousIsopack)) {
        isopack = previousIsopack;
      } else {
        var pluginCacheDir;
        if (self._pluginCacheDirRoot) {
          pluginCacheDir = self._pluginCacheDirForLocal(name);
        }

        // Do we have an up-to-date package on disk?
        var isopackBuildInfoJson = self.cacheDir && files.readJSONOrNull(
          self._isopackBuildInfoPath(name));
        var upToDate = self._checkUpToDate(isopackBuildInfoJson);

        if (upToDate) {
          // Reuse existing plugin cache dir
          pluginCacheDir && files.mkdir_p(pluginCacheDir);

          var Isopack = isopackModule.Isopack;
          isopack = new Isopack();
          isopack.initFromPath(name, self._isopackDir(name), {
            isopackBuildInfoJson: isopackBuildInfoJson,
            pluginCacheDir: pluginCacheDir
          });
          // _checkUpToDate already verified that
          // isopackBuildInfoJson.pluginProviderPackageMap is a subset of
          // self._packageMap, so this operation is correct. (It can't be done
          // by isopack.initFromPath, because Isopack doesn't have access to the
          // PackageMap, and specifically to the local catalog it knows about.)
          isopack.setPluginProviderPackageMap(
            self._packageMap.makeSubsetMap(
              _.keys(isopackBuildInfoJson.pluginProviderPackageMap)));
        } else {
          // Nope! Compile it again. Give it a fresh plugin cache.
          if (pluginCacheDir) {
            files.rm_recursive(pluginCacheDir);
            files.mkdir_p(pluginCacheDir);
          }
          isopack = compiler.compile(packageInfo.packageSource, {
            packageMap: self._packageMap,
            isopackCache: self,
            noLineNumbers: self._noLineNumbers,
            includeCordovaUnibuild: self._includeCordovaUnibuild,
            includePluginProviderPackageMap: true,
            pluginCacheDir: pluginCacheDir
          });
          isopack._buildIteration = self._buildIteration;
          // Accept the compiler's result, even if there were errors (since it
          // at least will have a useful WatchSet and will allow us to keep
          // going and compile other packages that depend on this one). However,
          // only save it to disk if there were no errors.
          if (self.cacheDir && ! buildmessage.jobHasMessages()) {
            // Save to disk, for next time!
            isopack.saveToPath(self._isopackDir(name), {
              includeIsopackBuildInfo: true
            });
          }
        }
      }

      self.allLoadedLocalPackagesWatchSet.merge(isopack.getMergedWatchSet());
      self._isopacks[name] = isopack;

      // Isopack is built and loaded. Run linters and save the result in the
      // Isopack object.
      var wipeLinterCache = false;
      if (! previousIsopack) {
        // We don't have previous Isopack-cache which probably means we are
        // running the build for the first time in this process. Let's lint the
        // isopack from scratch and collect all errors.
        wipeLinterCache = true;
      }
      var lintingMessages = buildmessage.capture({
        title: "linting isopack " + name
      }, function () {
        compiler.lint(packageInfo.packageSource, {
          isopackCache: self,
          isopack: isopack,
          wipeLinterCache: wipeLinterCache,
          includeCordovaUnibuild: self._includeCordovaUnibuild
        });
      });
      if (! lintingMessages.hasMessages()) {
        lintingMessages = null;
      }

      isopack.lintingMessages = lintingMessages;
    });
  },

  _checkUpToDate: function (isopackBuildInfoJson) {
    var self = this;
    // If there isn't an isopack-buildinfo.json file, then we definitely aren't
    // up to date!
    if (! isopackBuildInfoJson)
      return false;

    // If we include Cordova but this Isopack doesn't, or via versa, then we're
    // not up to date.
    if (self._includeCordovaUnibuild !==
        isopackBuildInfoJson.includeCordovaUnibuild) {
      return false;
    }

    // Was the package built by a different compiler version?
    if (isopackBuildInfoJson.builtBy !== compiler.BUILT_BY) {
      return false;
    }

    // If any of the direct dependencies changed their version or location, we
    // aren't up to date.
    if (!self._packageMap.isSupersetOfJSON(
      isopackBuildInfoJson.pluginProviderPackageMap)) {
      return false;
    }
    // Merge in the watchsets for all unibuilds and plugins in the package, then
    // check it once.
    var watchSet = watch.WatchSet.fromJSON(
      isopackBuildInfoJson.pluginDependencies);

    _.each(isopackBuildInfoJson.unibuildDependencies, function (deps) {
      watchSet.merge(watch.WatchSet.fromJSON(deps));
    });
    return watch.isUpToDate(watchSet);
  },

  _checkUpToDatePreloaded: function (previousIsopack) {
    var self = this;

    // If we include Cordova but this Isopack doesn't, or via versa, then we're
    // not up to date.
    if (self._includeCordovaUnibuild !== previousIsopack.hasCordovaUnibuild()) {
      return false;
    }

    // We don't have to check builtBy because we don't change BUILT_BY without
    // restarting the process.

    // If any of the direct dependencies changed their version or location, we
    // aren't up to date.
    if (!self._packageMap.isSupersetOfJSON(
      previousIsopack.pluginProviderPackageMap.toJSON())) {
      return false;
    }
    // Merge in the watchsets for all unibuilds and plugins in the package, then
    // check it once.
    var watchSet = previousIsopack.getMergedWatchSet();
    return watch.isUpToDate(watchSet);
  },

  _isopackDir: function (packageName) {
    var self = this;
    return files.pathJoin(self.cacheDir, colonConverter.convert(packageName));
  },

  _pluginCacheDirForPackage: function (packageName) {
    var self = this;
    return files.pathJoin(self._pluginCacheDirRoot,
                          colonConverter.convert(packageName));
  },

  _pluginCacheDirForVersion: function (packageName, version) {
    var self = this;
    return files.pathJoin(
      self._pluginCacheDirForPackage(packageName), version);
  },

  _pluginCacheDirForLocal: function (packageName) {
    var self = this;
    // assumes that `local` is not a valid package version.
    return files.pathJoin(
      self._pluginCacheDirForPackage(packageName), 'local');
  },

  _isopackBuildInfoPath: function (packageName) {
    var self = this;
    return files.pathJoin(
      self._isopackDir(packageName), 'isopack-buildinfo.json');
  },

  forgetPreviousIsopackCache: function () {
    var self = this;
    self._previousIsopackCache = null;
  },

  _getLintingMessagesForLocalPackages: function () {
    var self = this;

    var messages = [];
    self._packageMap.eachPackage(function (name, packageInfo) {
      var packageInfo = self._packageMap.getInfo(name);
      var isopack = self._isopacks[name];
      if (packageInfo.kind === 'local') {
        // skip the messages we previously reported
        if (self._buildIteration &&
            isopack._buildIteration !== self._buildIteration) {
          return;
        }
        var isopackMessages = isopack.lintingMessages;
        if (isopackMessages) {
          messages.push(isopackMessages.formatMessages());
        }
      }
    });

    return messages;
  }
});


var parserTestOptions = { includeComments: true };

var allNodeNames = [
  ";",
  "array",
  "assignment",
  "binary",
  "blockStmnt",
  "boolean",
  "bracket",
  "breakStmnt",
  "call",
  "case",
  "catch",
  "comma",
  "comment",
  "continueStmnt",
  "debuggerStmnt",
  "default",
  "doStmnt",
  "dot",
  "emptyStmnt",
  "expressionStmnt",
  "finally",
  "forInSpec",
  "forSpec",
  "forStmnt",
  "forVarInSpec",
  "forVarSpec",
  "functionDecl",
  "functionExpr",
  "idPropName",
  "identifier",
  "ifStmnt",
  "labelStmnt",
  "new",
  "newcall",
  "nil",
  "null",
  "numPropName",
  "number",
  "object",
  "parens",
  "postfix",
  "program",
  "prop",
  "regex",
  "returnStmnt",
  "strPropName",
  "string",
  "switchStmnt",
  "ternary",
  "this",
  "throwStmnt",
  "tryStmnt",
  "unary",
  "varDecl",
  "varStmnt",
  "whileStmnt",
  "withStmnt"
];

var allNodeNamesSet = {};
_.each(allNodeNames, function (n) { allNodeNamesSet[n] = true; });

var makeTester = function (test) {
  return {
    // Parse code and make sure it matches expectedTreeString.
    goodParse: function (code, expectedTreeString, regexTokenHints) {
      var expectedTree = ParseNode.unstringify(expectedTreeString);

      // first use lexer to collect all tokens
      var lexer = new JSLexer(code);
      var allTokensInOrder = [];
      while (! lexer.next().isEOF()) {
        var lex = lexer.lastLexeme;
        if (lex.isError())
          test.fail("Lexer error at " + lex.startPos());
        if (lex.isToken())
          allTokensInOrder.push(lex);
        if (regexTokenHints && regexTokenHints[allTokensInOrder.length])
          lexer.divisionPermitted = false;
      }

      var parser = new JSParser(code, parserTestOptions);
      var actualTree = parser.getSyntaxTree();

      var nextTokenIndex = 0;
      var check = function (tree) {
        if (tree instanceof ParseNode) {
          // This is a NODE (non-terminal).
          var nodeName = tree.name;
          if (! (nodeName && typeof nodeName === "string" &&
                 allNodeNamesSet[nodeName] === true))
            test.fail("Not a node name: " + nodeName);
          _.each(tree.children, check);
        } else if (typeof tree === 'object' &&
                   typeof tree.text === 'function') {
          // This is a TOKEN (terminal).
          // Make sure we are visiting every token once, in order.
          // Make an exception for any comment lexemes present,
          // because we couldn't know whether to include them in
          // allTokensInOrder.
          if (tree.type() !== "COMMENT") {
            if (nextTokenIndex >= allTokensInOrder.length)
              test.fail("Too many tokens: " + (nextTokenIndex + 1));
            var referenceToken = allTokensInOrder[nextTokenIndex++];
            if (tree.text() !== referenceToken.text())
              test.fail(tree.text() + " !== " + referenceToken.text());
            if (tree.startPos() !== referenceToken.startPos())
              test.fail(tree.startPos() + " !== " + referenceToken.startPos());
            if (code.substring(tree.startPos(), tree.endPos()) !== tree.text())
              test.fail("Didn't see " + tree.text() + " at " + tree.startPos() +
                        " in " + code);
          }
        } else {
          test.fail("Unknown tree part: " + tree);
        }
      };

      check(actualTree);
      if (nextTokenIndex !== allTokensInOrder.length)
        test.fail("Too few tokens: " + nextTokenIndex);

      test.equal(parser.pos, code.length);

      test.equal(ParseNode.stringify(actualTree),
                 ParseNode.stringify(expectedTree), code);
    },
    // Takes code with part of it surrounding with backticks.
    // Removes the two backtick characters, tries to parse the code,
    // and then asserts that there was a tokenization-level error,
    // with the part that was between the backticks called out as
    // the bad token.
    //
    // For example, the test "123`@`" will try to parse "123@" and
    // assert that a tokenization error occurred at '@'.
    badToken: function (code) {
      var constructMessage = function (pos, text) {
        var nicePos = JSLexer.prettyOffset(code, pos);
        return "Bad token at " + nicePos + ", text `" + text + "`";
      };
      var pos = code.indexOf('`');
      var text = code.match(/`(.*?)`/)[1];
      code = code.replace(/`/g, '');

      var parsed = false;
      var error = null;
      try {
        var tree = new JSParser(code, parserTestOptions).getSyntaxTree();
        parsed = true;
      } catch (e) {
        error = e;
      }
      test.isFalse(parsed);
      test.isTrue(error);
      test.equal(error.message, constructMessage(pos, text));
    },
    // Takes code with a backtick-quoted string embedded in it.
    // Removes the backticks and their contents, tries to parse the code,
    // and then asserts that there was a parse error at the location
    // where the backtick-quoted string was embedded.  The embedded
    // string must match whatever the error message says was "expected".
    //
    // For example, the test "{`statement`" will try to parse the code
    // "{" and then assert that an error occured at the end of the string
    // saying "Expected statement".  The test "1 `semicolon`2" will try
    // to parse "1 2" and assert that the error "Expected semicolon"
    // appeared after the space and before the 2.
    //
    // A second backtick-quoted string is used as the "found" token
    // in the error message.
    badParse: function (code) {
      var constructMessage = function (whatExpected, pos, found, after) {
        return "Expected " + whatExpected + (after ? " after " + after : "") +
          " at " + JSLexer.prettyOffset(code, pos) + ", found " + found;
      };
      var pos = code.indexOf('`');

      var backticked = code.match(/`.*?`/g);
      var whatExpected = backticked[0] && backticked[0].slice(1,-1);
      var found = backticked[1] && backticked[1].slice(1, -1);
      code = code.replace(/`.*?`/g, '');

      var parsed = false;
      var error = null;
      var parser = new JSParser(code, parserTestOptions);
      try {
        var tree = parser.getSyntaxTree();
        parsed = true;
      } catch (e) {
        error = e;
      }
      test.isFalse(parsed);
      test.isTrue(error);
      if (! parsed && error) {
        var after = parser.oldToken;
        found = (found || parser.newToken);
        test.equal(error.message,
                   constructMessage(whatExpected, pos, found, after),
                   code);
      }
    }
  };
};

Tinytest.add("jsparse - basics", function (test) {
  var tester = makeTester(test);
  tester.goodParse('1', "program(expressionStmnt(number(1) ;()))");
  tester.goodParse('1 + 1', "program(expressionStmnt(binary(number(1) + number(1)) ;()))");
  tester.goodParse('1*2+3*4', "program(expressionStmnt(binary(binary(number(1) * number(2)) + " +
                "binary(number(3) * number(4))) ;()))");
  tester.goodParse('1 + 1;', "program(expressionStmnt(binary(number(1) + number(1)) ;))");
  tester.goodParse('1 + 1;;', "program(expressionStmnt(binary(number(1) + number(1)) ;) emptyStmnt(;))");
  tester.goodParse('', "program()");
  tester.goodParse('\n', "program()");
  tester.goodParse(';;;\n\n;\n', "program(emptyStmnt(;) emptyStmnt(;) emptyStmnt(;) emptyStmnt(;))");
  tester.goodParse('foo', "program(expressionStmnt(identifier(foo) ;()))");
  tester.goodParse('foo();', "program(expressionStmnt(call(identifier(foo) `(` `)`) ;))");
  tester.goodParse('var x = 3', "program(varStmnt(var varDecl(x = number(3)) ;()))");
  tester.goodParse('++x;', "program(expressionStmnt(unary(++ identifier(x)) ;))");
  tester.goodParse('x++;', "program(expressionStmnt(postfix(identifier(x) ++) ;))");
  tester.goodParse(
    'throw new Error',
    "program(throwStmnt(throw new(new identifier(Error)) ;()))");
  tester.goodParse(
    'var x = function () { return 123; };',
    'program(varStmnt(var varDecl(x = functionExpr(function nil() `(` `)` ' +
      '{ returnStmnt(return number(123) ;) })) ;))');

  tester.badParse("var x = `expression`");
  tester.badParse("1 `semicolon`1");
  tester.badParse("1+1`semicolon`:");
});

Tinytest.add("jsparse - tokenization errors", function (test) {
  var tester = makeTester(test);
  tester.badToken("123`@`");
  tester.badToken("thisIsATestOf = `'unterminated `\n strings'");
  // make sure newlines aren't quietly included in regex literals
  tester.badToken("var x = `/`a\nb/;");
  tester.badToken("var x = `/`a\\\nb/;");
  tester.badToken("var x = `/`a[\n]b/;");
});

Tinytest.add("jsparse - syntax forms", function (test) {
  var tester = makeTester(test);
  var trials = [
    // STATEMENTS
    ['1',
     'program(expressionStmnt(number(1) ;()))'],
    ['1;;;;2',
     'program(expressionStmnt(number(1) ;) emptyStmnt(;) emptyStmnt(;) emptyStmnt(;) ' +
     'expressionStmnt(number(2) ;()))'],
    ['{}',
     'program(blockStmnt({ }))'],
    ['{null}',
     'program(blockStmnt({ expressionStmnt(null(null) ;()) }))'],
    ['{\nfoo()\nbar();\n}',
     'program(blockStmnt({ expressionStmnt(call(identifier(foo) `(` `)`) ;()) ' +
     'expressionStmnt(call(identifier(bar) `(` `)`) ;) }))'],
    ['{{{}}}',
     'program(blockStmnt({ blockStmnt({ blockStmnt({ }) }) }))'],
    ['var x = y, z,\n  a = b = c;',
     'program(varStmnt(var varDecl(x = identifier(y)) , varDecl(z) , varDecl(a = ' +
     'assignment(identifier(b) = identifier(c))) ;))'],
    ['if (x === y);',
     'program(ifStmnt(if `(` binary(identifier(x) === identifier(y)) `)` emptyStmnt(;)))'],
    ['if (z) return',
     'program(ifStmnt(if `(` identifier(z) `)` returnStmnt(return nil() ;())))'],
    ['if (a) b; else c',
     'program(ifStmnt(if `(` identifier(a) `)` expressionStmnt(identifier(b) ;) else ' +
     'expressionStmnt(identifier(c) ;())))'],
    ['if (n === 1) { foo(); } else if (n === 2) { bar(); } else { baz(); }',
     'program(ifStmnt(if `(` binary(identifier(n) === number(1)) `)` blockStmnt(' +
     '{ expressionStmnt(call(identifier(foo) `(` `)`) ;) }) else ifStmnt(' +
     'if `(` binary(identifier(n) === number(2)) `)` blockStmnt(' +
     '{ expressionStmnt(call(identifier(bar) `(` `)`) ;) }) else blockStmnt(' +
     '{ expressionStmnt(call(identifier(baz) `(` `)`) ;) }))))'],
    ['while (false);',
     'program(whileStmnt(while `(` boolean(false) `)` emptyStmnt(;)))'],
    ['while (/foo/.test(bar.baz)) {\n  bar = bar.baz;\n}',
     'program(whileStmnt(while `(` call(dot(regex(/foo/) . test) `(` ' +
     'dot(identifier(bar) . baz) `)`) `)` blockStmnt({ expressionStmnt(' +
     'assignment(identifier(bar) = dot(identifier(bar) . baz)) ;) })))'],
    ['while (false) while (false);',
     'program(whileStmnt(while `(` boolean(false) `)` ' +
     'whileStmnt(while `(` boolean (false) `)` emptyStmnt(;))))'],
    ['do a; while (b);',
     'program(doStmnt(do expressionStmnt(identifier(a) ;) while `(` identifier(b) `)` ;))'],
    ['do { x-- } while (x);',
     'program(doStmnt(do blockStmnt({ expressionStmnt(postfix(identifier(x) --) ;()) }) ' +
     'while `(` identifier(x) `)` ;))'],
    ['do a\n while (b)\n x++',
     'program(doStmnt(do expressionStmnt(identifier(a) ;()) while `(` identifier(b) `)` ;()) ' +
     'expressionStmnt(postfix(identifier(x) ++) ;()))'],
    ["for(;;);",
     "program(forStmnt(for `(` forSpec(nil() ; nil() ; nil()) `)` emptyStmnt(;)))"],
    ["for(x in y);",
     "program(forStmnt(for `(` forInSpec(identifier(x) in identifier(y)) `)` emptyStmnt(;)))"],
    ["for(var x in y);",
     "program(forStmnt(for `(` forVarInSpec(var varDecl(x) in identifier(y)) `)` emptyStmnt(;)))"],
    ["for(var x;;);",
     "program(forStmnt(for `(` forVarSpec(var varDecl(x) ; nil() ; nil()) `)` emptyStmnt(;)))"],
    ["for(var i=0;i<N;i++) {}",
     "program(forStmnt(for `(` forVarSpec(var varDecl(i = number(0)) ; " +
     "binary(identifier(i) < identifier(N)) ; postfix(identifier(i) ++)) `)` blockStmnt({ })))"],
    ["for (var x=3 in y);",
     "program(forStmnt(for `(` forVarInSpec(var varDecl(x = number(3)) in identifier(y)) `)` " +
     "emptyStmnt(;)))"],
    ["for (x.foo in y);",
     "program(forStmnt(for `(` forInSpec(dot(identifier(x) . foo) in identifier(y)) `)` emptyStmnt(;)))"],
    ["return",
     "program(returnStmnt(return nil() ;()))"],
    ["return;",
     "program(returnStmnt(return nil() ;))"],
    ["return null",
     "program(returnStmnt(return null(null) ;()))"],
    ["return null;",
     "program(returnStmnt(return null(null) ;))"],
    ["return\n1+1",
     "program(returnStmnt(return nil() ;()) expressionStmnt(binary(number(1) + number(1)) ;()))"],
    ["return 1\n  +1",
     "program(returnStmnt(return binary(number(1) + number(1)) ;()))"],
    ["continue",
     "program(continueStmnt(continue nil() ;()))"],
    ["continue foo",
     "program(continueStmnt(continue foo ;()))"],
    ["continue foo;",
     "program(continueStmnt(continue foo ;))"],
    ["continue\n  foo;",
     "program(continueStmnt(continue nil() ;()) expressionStmnt(identifier(foo) ;))"],
    ["break",
     "program(breakStmnt(break nil() ;()))"],
    ["break foo",
     "program(breakStmnt(break foo ;()))"],
    ["break foo;",
     "program(breakStmnt(break foo ;))"],
    ["break\n  foo;",
     "program(breakStmnt(break nil() ;()) expressionStmnt(identifier(foo) ;))"],
    ["throw e;",
     "program(throwStmnt(throw identifier(e) ;))"],
    ["throw e",
     "program(throwStmnt(throw identifier(e) ;()))"],
    ["throw new Error;",
     "program(throwStmnt(throw new(new identifier(Error)) ;))"],
    ["with(x);",
     "program(withStmnt(with `(` identifier(x) `)` emptyStmnt(;)))"],
    ["with(a=b) {}",
     "program(withStmnt(with `(` assignment(identifier(a) = identifier(b)) `)` blockStmnt({ })))"],
    ["switch(x) {}",
     "program(switchStmnt(switch `(` identifier(x) `)` { }))"],
    ["switch(x) {case 1:case 2:case 3:default:case 4:}",
     "program(switchStmnt(switch `(` identifier(x) `)` { " +
     "case(case number(1) :) case(case number(2) :) case(case number(3) :) " +
     "default(default :) case(case number(4) :) }))"],
    ["switch(x) {\ncase 1:\n  return\ncase 2:\ncase 3:\n  throw e}",
     "program(switchStmnt(switch `(` identifier(x) `)` { " +
     "case(case number(1) : returnStmnt(return nil() ;())) " +
     "case(case number(2) :) case(case number(3) : " +
     "throwStmnt(throw identifier(e) ;())) }))"],
    ["switch(x) {default:;}",
     "program(switchStmnt(switch `(` identifier(x) `)` { default(default : emptyStmnt(;)) }))"],
    ["try {} catch (e) {} finally {}",
     "program(tryStmnt(try blockStmnt({ }) catch(catch `(` e `)` blockStmnt({ })) " +
     "finally(finally blockStmnt({ }))))"],
    ["try {} finally {}",
     "program(tryStmnt(try blockStmnt({ }) nil() finally(finally blockStmnt({ }))))"],
    ["try {} catch (e) {}",
     "program(tryStmnt(try blockStmnt({ }) catch(catch `(` e `)` blockStmnt({ })) nil()))"],
    ["a:;",
     "program(labelStmnt(a : emptyStmnt(;)))"],
    ["{x:1}",
     "program(blockStmnt({ labelStmnt(x : expressionStmnt(number(1) ;())) }))"],
    ["{x:y:z:1}",
     "program(blockStmnt({ labelStmnt(x : labelStmnt(y : " +
     "labelStmnt(z : expressionStmnt(number(1) ;())))) }))"],
    [";;foo:\nfor(;;);",
     "program(emptyStmnt(;) emptyStmnt(;) labelStmnt(foo : " +
     "forStmnt(for `(` forSpec(nil() ; nil() ; nil()) `)` emptyStmnt(;))))"],
    ["debugger",
     "program(debuggerStmnt(debugger ;()))"],
    ["debugger;",
     "program(debuggerStmnt(debugger ;))"],
    ["function foo() {}",
     "program(functionDecl(function foo `(` `)` { }))"],
    ["function foo() {function bar() {}}",
     "program(functionDecl(function foo `(` `)` { functionDecl(function bar `(` `)` { }) }))"],
    [";;function f() {};;",
     "program(emptyStmnt(;) emptyStmnt(;) functionDecl(function f `(` `)` { }) " +
     "emptyStmnt(;) emptyStmnt(;))"],
    ["function foo(a,b,c) {}",
     "program(functionDecl(function foo `(` a , b , c `)` { }))"],

    // EXPRESSIONS
    ["null + this - 3 + true",
     "program(expressionStmnt(binary(binary(binary(null(null) + this(this)) - " +
     "number(3)) + boolean(true)) ;()))"],
    ["+.5",
     "program(expressionStmnt(unary(+ number(.5)) ;()))"],
    ["a1a1a",
     "program(expressionStmnt(identifier(a1a1a) ;()))"],
    ["/abc/mig",
     "program(expressionStmnt(regex(/abc/mig) ;()))"],
    ["/[]/",
     "program(expressionStmnt(regex(/[]/) ;()))"],
    ["/[/]/",
     "program(expressionStmnt(regex(/[/]/) ;()))"],
    ["/[[/]/",
     "program(expressionStmnt(regex(/[[/]/) ;()))"],
    ["/.\\/[a//b]\\[\\][[\\d/]/",
     "program(expressionStmnt(regex(/.\\/[a//b]\\[\\][[\\d/]/) ;()))"],
    ["a / /b/mgi / c",
     "program(expressionStmnt(binary(binary(identifier(a) / " +
     "regex(/b/mgi)) / identifier(c)) ;()))"],
    ["'a' + \"\" + \"b\" + '\\''",
     "program(expressionStmnt(binary(binary(binary(string('a') + string(\"\")) + " +
     "string(\"b\")) + string('\\'')) ;()))"],
    ["_ + x0123 + $",
     "program(expressionStmnt(binary(binary(identifier(_) + " +
     "identifier(x0123)) + identifier($)) ;()))"],
    ["if ((x = 1)) return ((1+2))*((1<<2));",
     "program(ifStmnt(if `(` parens(`(` assignment(identifier(x) = number(1)) `)`) " +
     "`)` returnStmnt(return binary(parens(`(` parens(`(` binary(number(1) + " +
     "number(2)) `)`) `)`) * parens(`(` parens(`(` binary(number(1) << number(2)) " +
     "`)`) `)`)) ;)))"],
    ["[];",
     "program(expressionStmnt(array([ ]) ;))"],
    ["[,,,];",
     "program(expressionStmnt(array([ , , , ]) ;))"],
    ["[(1,2),,3];",
     "program(expressionStmnt(array([ parens(`(` comma(number(1) , " +
     "number(2)) `)`) , , number(3) ]) ;))"],
    ["({});",
     "program(expressionStmnt(parens(`(` object({ }) `)`) ;))"],
    ["({1:1});",
     "program(expressionStmnt(parens(`(` object({ prop(numPropName(1) : number(1)) }) `)`) ;))"],
    ["({x:true});",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(x) : boolean(true)) }) `)`) ;))"],
    ["({'a':b, c:'d', 1:null});",
     "program(expressionStmnt(parens(`(` object({ prop(strPropName('a') : " +
     "identifier(b)) , prop(idPropName(c) : string('d')) , prop(numPropName(1) " +
     ": null(null)) }) `)`) ;))"],
    ["(function () {});",
     "program(expressionStmnt(parens(`(` functionExpr(function nil() `(` `)` { }) `)`) ;))"],
    ["(function foo() {});",
     "program(expressionStmnt(parens(`(` functionExpr(function foo `(` `)` { }) `)`) ;))"],
    ["x = function () {}.y;",
     "program(expressionStmnt(assignment(identifier(x) = dot(functionExpr(" +
     "function nil() `(` `)` { }) . y)) ;))"],
    ["(function (a) {})",
     "program(expressionStmnt(parens(`(` functionExpr(function nil() " +
     "`(` a `)` { }) `)`) ;()))"],
    ["(function (a,b,c) {})",
     "program(expressionStmnt(parens(`(` functionExpr(function nil() `(` " +
     "a , b , c `)` { }) `)`) ;()))"],
    ["foo.bar.baz;",
     "program(expressionStmnt(dot(dot(identifier(foo) . bar) . baz) ;))"],
    ["foo[bar,bar][baz].qux[1+1];",
     "program(expressionStmnt(bracket(dot(bracket(bracket(identifier(foo) " +
     "[ comma(identifier(bar) , identifier(bar)) ]) [ identifier(baz) ]) . qux) " +
     "[ binary(number(1) + number(1)) ]) ;))"],
    ["new new a.b.c[d]",
     "program(expressionStmnt(new(new new(new bracket(dot(dot(identifier(a) " +
     ". b) . c) [ identifier(d) ]))) ;()))"],
    ["new new a.b.c[d]()",
     "program(expressionStmnt(new(new newcall(new " +
     "bracket(dot(dot(identifier(a) . b) . c) [ identifier(d) ]) `(` `)`)) ;()))"],
    ["new new a.b.c[d]()()",
     "program(expressionStmnt(newcall(new newcall(new " +
     "bracket(dot(dot(identifier(a) . b) . c) [ identifier(d) ]) `(` `)`) `(` `)`) ;()))"],
    ["new foo(x).bar(y)",
     "program(expressionStmnt(call(dot(newcall(new identifier(foo) `(` " +
     "identifier(x) `)`) . bar) `(` identifier(y) `)`) ;()))"],
    ["new new foo().bar",
     "program(expressionStmnt(new(new dot(newcall(new identifier(foo) `(` `)`) . bar)) ;()))"],
    ["delete void typeof - + ~ ! -- ++ x;",
     "program(expressionStmnt(unary(delete unary(void unary(typeof unary(- unary(+ " +
     "unary(~ unary(! unary(-- unary(++ identifier(x)))))))))) ;))"],
    ["x++ + ++y",
     "program(expressionStmnt(binary(postfix(identifier(x) ++) + " +
     "unary(++ identifier(y))) ;()))"],
    ["1*2+3*4",
     "program(expressionStmnt(binary(binary(number(1) * number(2)) " +
     "+ binary(number(3) * number(4))) ;()))"],
    ["a*b/c%d+e-f<<g>>h>>>i<j>k<=l>=m instanceof n in o==p!=q===r!==s&t^u|v&&w||x",
     "program(expressionStmnt(binary(binary(binary(binary(binary(binary(binary(" +
     "binary(binary(binary(binary(binary(binary(binary(binary(binary(binary(binary(" +
     "binary(binary(binary(binary(binary(identifier(a) * identifier(b)) / " +
     "identifier(c)) % identifier(d)) + identifier(e)) - identifier(f)) << identifier(g)) " +
     ">> identifier(h)) >>> identifier(i)) < identifier(j)) > identifier(k)) <= " +
     "identifier(l)) >= identifier(m)) instanceof identifier(n)) in identifier(o)) == " +
     "identifier(p)) != identifier(q)) === identifier(r)) !== identifier(s)) & " +
     "identifier(t)) ^ identifier(u)) | identifier(v)) && identifier(w)) || " +
     "identifier(x)) ;()))"],
    ["a||b&&c|d^e&f!==g===h!=i==j in k instanceof l>=m<=n<o<p>>>q>>r<<s-t+u%v/w*x",
     "program(expressionStmnt(binary(identifier(a) || binary(identifier(b) && " +
     "binary(identifier(c) | binary(identifier(d) ^ binary(identifier(e) & " +
     "binary(binary(binary(binary(identifier(f) !== identifier(g)) === identifier(h)) " +
     "!= identifier(i)) == binary(binary(binary(binary(binary(binary(identifier(j) in " +
     "identifier(k)) instanceof identifier(l)) >= identifier(m)) <= identifier(n)) < " +
     "identifier(o)) < binary(binary(binary(identifier(p) >>> identifier(q)) >> " +
     "identifier(r)) << binary(binary(identifier(s) - identifier(t)) + " +
     "binary(binary(binary(identifier(u) % identifier(v)) / identifier(w)) * " +
     "identifier(x))))))))))) ;()))"],
    ["a?b:c",
     "program(expressionStmnt(ternary(identifier(a) ? identifier(b) : " +
     "identifier(c)) ;()))"],
    ["1==2?3=4:5=6",
     "program(expressionStmnt(ternary(binary(number(1) == number(2)) ? " +
     "assignment(number(3) = number(4)) : assignment(number(5) = number(6))) ;()))"],
    ["a=b,c=d",
     "program(expressionStmnt(comma(assignment(identifier(a) = identifier(b)) , " +
     "assignment(identifier(c) = identifier(d))) ;()))"],
    ["a=b=c=d",
     "program(expressionStmnt(assignment(identifier(a) = assignment(identifier(b) " +
     "= assignment(identifier(c) = identifier(d)))) ;()))"],
    ["x[0]=x[1]=true",
     "program(expressionStmnt(assignment(bracket(identifier(x) [ number(0) ]) = " +
     "assignment(bracket(identifier(x) [ number(1) ]) = boolean(true))) ;()))"],
    ["a*=b/=c%=d+=e-=f<<=g>>=h>>>=i&=j^=k|=l",
     "program(expressionStmnt(assignment(identifier(a) *= assignment(identifier(b) " +
     "/= assignment(identifier(c) %= assignment(identifier(d) += " +
     "assignment(identifier(e) -= assignment(identifier(f) <<= " +
     "assignment(identifier(g) >>= assignment(identifier(h) >>>= " +
     "assignment(identifier(i) &= assignment(identifier(j) ^= " +
     "assignment(identifier(k) |= identifier(l)))))))))))) ;()))"],
    ["1;\n\n\n\n/* foo */\n// bar\n", // trailing whitespace and comments
     "program(expressionStmnt(number(1) ;) comment(`/* foo */`) comment(`// bar`))"],
    // includeComments option; comments in AST
    ["//foo",
     "program(comment(//foo))"],
    ["//foo\n",
     "program(comment(//foo))"],
    ["/*foo*/",
     "program(comment(/*foo*/))"],
    ["/*foo*/\n",
     "program(comment(/*foo*/))"],
    [";\n//foo",
     "program(emptyStmnt(;) comment(//foo))"],
    [";\n/*foo*/",
     "program(emptyStmnt(;) comment(/*foo*/))"],
    [";\n//foo\n;",
     "program(emptyStmnt(;) comment(//foo) emptyStmnt(;))"],
    [";\n/*foo*/\n;",
     "program(emptyStmnt(;) comment(/*foo*/) emptyStmnt(;))"],
    [";\n//foo\n//bar\n;",
     "program(emptyStmnt(;) comment(//foo) comment(//bar) emptyStmnt(;))"],
    [";\n/*foo*/ /*bar*/\n;",
     "program(emptyStmnt(;) comment(/*foo*/) comment(/*bar*/) emptyStmnt(;))"],
    [";//foo\n//bar\n;",
     "program(emptyStmnt(;) comment(//bar) emptyStmnt(;))"],
    [";/*foo*/\n/*bar*/\n;",
     "program(emptyStmnt(;) comment(/*bar*/) emptyStmnt(;))"],
    [";/*foo*//*bar*///baz\n;",
     "program(emptyStmnt(;) emptyStmnt(;))"],
    [";/*foo*//*bar*///baz",
     "program(emptyStmnt(;))"],
    ["/*foo*//*bar*///baz",
     "program(comment(/*foo*/) comment(/*bar*/) comment(//baz))"],
    ["//foo\n//bar\nfunction aaa() {}\nfunction bbb() {}",
     "program(comment(//foo) comment(//bar) functionDecl(function aaa `(` `)` { }) " +
     "functionDecl(function bbb `(` `)` { }))"],
    // comments don't interfere with parse
    ["if (true)\n//comment\nfoo();",
     "program(ifStmnt(if `(` boolean(true) `)` " +
     "expressionStmnt(call(identifier(foo) `(` `)`) ;)))"],
    // bare keywords allowed in property access and object literal
    ["foo.return();",
     "program(expressionStmnt(call(dot(identifier(foo) . return) `(` `)`) ;))"],
    ["foo.true();",
     "program(expressionStmnt(call(dot(identifier(foo) . true) `(` `)`) ;))"],
    ["foo.null();",
     "program(expressionStmnt(call(dot(identifier(foo) . null) `(` `)`) ;))"],
    ["({true:3})",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(true) : number(3)) }) `)`) ;()))"],
    ["({null:3})",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(null) : number(3)) }) `)`) ;()))"],
    ["({if:3})",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(if) : number(3)) }) `)`) ;()))"],
    // ES5 line continuations in string literals
    ["var x = 'a\\\nb\\\nc';",
     "program(varStmnt(var varDecl(x = string(`'a\\\nb\\\nc'`)) ;))"],
    // ES5 trailing comma in object literal
    ["({});",
     "program(expressionStmnt(parens(`(` object({ }) `)`) ;))"],
    ["({x:1});",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(x) : number(1)) }) `)`) ;))"],
    ["({x:1,});",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(x) : number(1)) , }) `)`) ;))"],
    ["({x:1,y:2});",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(x) : number(1)) , " +
     "prop(idPropName(y) : number(2)) }) `)`) ;))"],
    ["({x:1,y:2,});",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(x) : number(1)) , " +
     "prop(idPropName(y) : number(2)) , }) `)`) ;))"]
  ];
  _.each(trials, function (tr) {
    tester.goodParse(tr[0], tr[1]);
  });
});

Tinytest.add("jsparse - bad parses", function (test) {
  var tester = makeTester(test);
  // string between backticks is pulled out and becomes what's "expected"
  // at that location, according to the correct error message
  var trials = [
    '{`statement`',
    'if (`expression`)',
    'if `(`else',
    'var`varDecl`;',
    'while (`expression`);',
    'while`(`;',
    'do a `semicolon`while b;',
    'do a\n while `(`b;',
    '1 `semicolon`2',
    'for (`forSpec`);',
    'for (1\n`semicolon`2\n3);',
    'continue `semicolon`1+1;',
    'break `semicolon`1+1;',
    'throw`expression`',
    'throw`expression`;',
    'throw\n`expression`',
    'throw\n`expression``end of line`e',
    'throw `expression`=;',
    'with(`expression`);',
    'switch(`expression`)',
    'switch(x)`{`;',
    'try`block`',
    'try {}`catch`',
    'try {} catch`(`;',
    'try {} catch(e)`block`;',
    '1+1`semicolon`:',
    '{a:`statement`}',
    'function `IDENTIFIER`() {}',
    'foo: `statement`function foo() {}',
    '[`expression`=',
    '[,,`expression`=',
    '({`propertyName`|:3})',
    '({1:2,3`:`})',
    '({1:2,`propertyName`',
    'x.`IDENTIFIER`,',
    'foo;`semicolon`:;',
    '1;`statement`=',
    'a+b`semicolon`=c;',
    'for(1+1 `semicolon`in {});',
    '`statement`=',
    'for(;`expression`var;) {}',
    '({`propertyName`',
    '({`propertyName`,})',
    '({`propertyName`:})',
    '({x`:`})',
    '({x:1,`propertyName`',
    '({x:1,`propertyName`,})',
    '({x:1`,`',
    '({x:1,`propertyName`,y:2})',
    '({x:1,`propertyName`,})',
    '({x:1,y:2`,`:',
    '({x:1,y:2,`propertyName`',
    '({x:1,y:2,`propertyName`:',
    '({x:1,y:2,`propertyName`,})'
  ];
  _.each(trials, function (tr) {
    tester.badParse(tr);
  });
});

Tinytest.add("jsparse - regex division ambiguity", function (test) {
  var tester = makeTester(test);
  tester.goodParse("if (e) /f/g;",
                   "program(ifStmnt(if `(` identifier(e) `)` expressionStmnt(regex(/f/g) ;)))",
                   {4: true});
  tester.goodParse("++/x/.y;",
                   "program(expressionStmnt(unary(++ dot(regex(/x/) . y)) ;))",
                   {1: true});
  tester.goodParse("x++/2/g;",
                   "program(expressionStmnt(binary(binary(postfix(identifier(x) ++) / " +
                   "number(2)) / identifier(g)) ;))");
  tester.goodParse("(1+1)/2/g;",
                   "program(expressionStmnt(binary(binary(parens(`(` binary(number(1) + " +
                   "number(1)) `)`) / " +
                   "number(2)) / identifier(g)) ;))");
  tester.goodParse("/x/",
                   "program(expressionStmnt(regex(/x/) ;()))");
});

Tinytest.add("jsparse - semicolon insertion", function (test) {
  var tester = makeTester(test);
  // Spec section 7.9.2
  tester.badParse("{ 1 `semicolon`2 } 3");
  tester.goodParse("{ 1\n2 } 3", "program(blockStmnt({ expressionStmnt(number(1) " +
                   ";()) expressionStmnt(number(2) ;()) }) expressionStmnt(number(3) ;()))");
  tester.badParse("for (a; b\n`semicolon`)");
  tester.goodParse("return\na + b",
                   "program(returnStmnt(return nil() ;()) " +
                   "expressionStmnt(binary(identifier(a) + identifier(b)) ;()))");
  tester.goodParse("a = b\n++c",
                   "program(expressionStmnt(assignment(identifier(a) = identifier(b)) ;())" +
                   "expressionStmnt(unary(++ identifier(c)) ;()))");
  tester.badParse("if (a > b)\n`statement`else c = d");
  tester.goodParse("a = b + c\n(d + e).print()",
                   "program(expressionStmnt(assignment(identifier(a) = " +
                   "binary(identifier(b) + call(dot(call(identifier(c) `(` " +
                   "binary(identifier(d) + identifier(e)) `)`) . print) `(` `)`))) ;()))");
});

Tinytest.add("jsparse - comments", function (test) {
  var tester = makeTester(test);
  // newline in multi-line comment makes it into a line break for semicolon
  // insertion purposes
  tester.badParse("1/**/`semicolon`2");
  tester.goodParse("1/*\n*/2",
                   "program(expressionStmnt(number(1) ;()) expressionStmnt(number(2) ;()))");
});

Tinytest.add("jsparse - initial lex error", function (test) {
  var doTest = function (code) {
    // this shouldn't throw
    var parser = new JSParser(code, parserTestOptions);
    // this should throw
    try {
      parser.getSyntaxTree();
      test.fail();
    } catch (e) {
      test.isTrue(/^Bad token/.test(e.message), e.message);
    }
  };

  doTest('/');
  doTest('@');
});


// Turns HTMLjs into DOM nodes and DOMRanges.
//
// - `htmljs`: the value to materialize, which may be any of the htmljs
//   types (Tag, CharRef, Comment, Raw, array, string, boolean, number,
//   null, or undefined) or a View or Template (which will be used to
//   construct a View).
// - `intoArray`: the array of DOM nodes and DOMRanges to push the output
//   into (required)
// - `parentView`: the View we are materializing content for (optional)
// - `_existingWorkStack`: optional argument, only used for recursive
//   calls when there is some other _materializeDOM on the call stack.
//   If _materializeDOM called your function and passed in a workStack,
//   pass it back when you call _materializeDOM (such as from a workStack
//   task).
//
// Returns `intoArray`, which is especially useful if you pass in `[]`.
Blaze._materializeDOM = function (htmljs, intoArray, parentView,
                                  _existingWorkStack) {
  // In order to use fewer stack frames, materializeDOMInner can push
  // tasks onto `workStack`, and they will be popped off
  // and run, last first, after materializeDOMInner returns.  The
  // reason we use a stack instead of a queue is so that we recurse
  // depth-first, doing newer tasks first.
  var workStack = (_existingWorkStack || []);
  materializeDOMInner(htmljs, intoArray, parentView, workStack);

  if (! _existingWorkStack) {
    // We created the work stack, so we are responsible for finishing
    // the work.  Call each "task" function, starting with the top
    // of the stack.
    while (workStack.length) {
      // Note that running task() may push new items onto workStack.
      var task = workStack.pop();
      task();
    }
  }

  return intoArray;
};

var materializeDOMInner = function (htmljs, intoArray, parentView, workStack) {
  if (htmljs == null) {
    // null or undefined
    return;
  }

  switch (typeof htmljs) {
  case 'string': case 'boolean': case 'number':
    intoArray.push(document.createTextNode(String(htmljs)));
    return;
  case 'object':
    if (htmljs.htmljsType) {
      switch (htmljs.htmljsType) {
      case HTML.Tag.htmljsType:
        intoArray.push(materializeTag(htmljs, parentView, workStack));
        return;
      case HTML.CharRef.htmljsType:
        intoArray.push(document.createTextNode(htmljs.str));
        return;
      case HTML.Comment.htmljsType:
        intoArray.push(document.createComment(htmljs.sanitizedValue));
        return;
      case HTML.Raw.htmljsType:
        // Get an array of DOM nodes by using the browser's HTML parser
        // (like innerHTML).
        var nodes = Blaze._DOMBackend.parseHTML(htmljs.value);
        for (var i = 0; i < nodes.length; i++)
          intoArray.push(nodes[i]);
        return;
      }
    } else if (HTML.isArray(htmljs)) {
      for (var i = htmljs.length-1; i >= 0; i--) {
        workStack.push(_.bind(Blaze._materializeDOM, null,
                              htmljs[i], intoArray, parentView, workStack));
      }
      return;
    } else {
      if (htmljs instanceof Blaze.Template) {
        htmljs = htmljs.constructView();
        // fall through to Blaze.View case below
      }
      if (htmljs instanceof Blaze.View) {
        Blaze._materializeView(htmljs, parentView, workStack, intoArray);
        return;
      }
    }
  }

  throw new Error("Unexpected object in htmljs: " + htmljs);
};

var materializeTag = function (tag, parentView, workStack) {
  var tagName = tag.tagName;
  var elem;
  if ((HTML.isKnownSVGElement(tagName) || isSVGAnchor(tag))
      && document.createElementNS) {
    // inline SVG
    elem = document.createElementNS('http://www.w3.org/2000/svg', tagName);
  } else {
    // normal elements
    elem = document.createElement(tagName);
  }

  var rawAttrs = tag.attrs;
  var children = tag.children;
  if (tagName === 'textarea' && tag.children.length &&
      ! (rawAttrs && ('value' in rawAttrs))) {
    // Provide very limited support for TEXTAREA tags with children
    // rather than a "value" attribute.
    // Reactivity in the form of Views nested in the tag's children
    // won't work.  Compilers should compile textarea contents into
    // the "value" attribute of the tag, wrapped in a function if there
    // is reactivity.
    if (typeof rawAttrs === 'function' ||
        HTML.isArray(rawAttrs)) {
      throw new Error("Can't have reactive children of TEXTAREA node; " +
                      "use the 'value' attribute instead.");
    }
    rawAttrs = _.extend({}, rawAttrs || null);
    rawAttrs.value = Blaze._expand(children, parentView);
    children = [];
  }

  if (rawAttrs) {
    var attrUpdater = new ElementAttributesUpdater(elem);
    var updateAttributes = function () {
      var expandedAttrs = Blaze._expandAttributes(rawAttrs, parentView);
      var flattenedAttrs = HTML.flattenAttributes(expandedAttrs);
      var stringAttrs = {};
      for (var attrName in flattenedAttrs) {
        stringAttrs[attrName] = Blaze._toText(flattenedAttrs[attrName],
                                              parentView,
                                              HTML.TEXTMODE.STRING);
      }
      attrUpdater.update(stringAttrs);
    };
    var updaterComputation;
    if (parentView) {
      updaterComputation =
        parentView.autorun(updateAttributes, undefined, 'updater');
    } else {
      updaterComputation = Tracker.nonreactive(function () {
        return Tracker.autorun(function () {
          Tracker._withCurrentView(parentView, updateAttributes);
        });
      });
    }
    Blaze._DOMBackend.Teardown.onElementTeardown(elem, function attrTeardown() {
      updaterComputation.stop();
    });
  }

  if (children.length) {
    var childNodesAndRanges = [];
    // push this function first so that it's done last
    workStack.push(function () {
      for (var i = 0; i < childNodesAndRanges.length; i++) {
        var x = childNodesAndRanges[i];
        if (x instanceof Blaze._DOMRange)
          x.attach(elem);
        else
          elem.appendChild(x);
      }
    });
    // now push the task that calculates childNodesAndRanges
    workStack.push(_.bind(Blaze._materializeDOM, null,
                          children, childNodesAndRanges, parentView,
                          workStack));
  }

  return elem;
};

var isSVGAnchor = function (node) {
  // We generally aren't able to detect SVG <a> elements because
  // if "A" were in our list of known svg element names, then all
  // <a> nodes would be created using
  // `document.createElementNS`. But in the special case of <a
  // xlink:href="...">, we can at least detect that attribute and
  // create an SVG <a> tag in that case.
  //
  // However, we still have a general problem of knowing when to
  // use document.createElementNS and when to use
  // document.createElement; for example, font tags will always
  // be created as SVG elements which can cause other
  // problems. #1977
  return (node.tagName === "a" &&
          node.attrs &&
          node.attrs["xlink:href"] !== undefined);
};


// Hold launch screen on app load. This reflects the fact that Meteor
// mobile apps that use this package always start with a launch screen
// visible. (see XXX comment at the top of package.js for more
// details)
var handle = LaunchScreen.hold();

var Template = Package.templating && Package.templating.Template;

Meteor.startup(function () {
  if (! Template) {
    handle.release();
  } else if (Package['iron:router']) {
    // XXX Instead of doing this here, this code should be in
    // iron:router directly. Note that since we're in a
    // `Meteor.startup` block it's ok that we don't have a
    // weak dependency on iron:router in package.js.
    Package['iron:router'].Router.onAfterAction(function () {
      handle.release();
    });
  } else {
    Template.body.onRendered(function () {
      handle.release();
    });

    // In case `Template.body` never gets rendered (due to some bug),
    // hide the launch screen after 6 seconds. This matches the
    // observed timeout that Cordova apps on Android (but not iOS)
    // have on hiding the launch screen (even if you don't call
    // `navigator.splashscreen.hide()`)
    setTimeout(function () {
      handle.release();
    }, 6000);
  }
});