
(function() {

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('@');
});

})();
