/**
 * A JavaScript tokenizer / parser / generator, originally written in Lisp.
 * Copyright (c) Marijn Haverbeke <marijnh@gmail.com>
 * http://marijn.haverbeke.nl/parse-js/
 *
 * Ported by to JavaScript by Mihai Bazon
 * Copyright (c) 2010, Mihai Bazon <mihai.bazon@gmail.com>
 * http://mihai.bazon.net/blog/
 *
 * Modifications and adaptions to browser (c) 2011, Juerg Lehni
 * http://lehni.org/
 *
 * Distributed under the BSD license.
 */

var parse_js = new function() {

/* -----[ Tokenizer (constants) ]----- */

var KEYWORDS = array_to_hash([
	"break",
	"case",
	"catch",
	"const",
	"continue",
	"default",
	"delete",
	"do",
	"else",
	"finally",
	"for",
	"function",
	"if",
	"in",
	"instanceof",
	"new",
	"return",
	"switch",
	"throw",
	"try",
	"typeof",
	"var",
	"void",
	"while",
	"with"
]);

var RESERVED_WORDS = array_to_hash([
	"abstract",
	"boolean",
	"byte",
	"char",
	"class",
	"debugger",
	"double",
	"enum",
	"export",
	"extends",
	"final",
	"float",
	"goto",
	"implements",
	"import",
	"int",
	"interface",
	"long",
	"native",
	"package",
	"private",
	"protected",
	"public",
	"short",
	"static",
	"super",
	"synchronized",
	"throws",
	"transient",
	"volatile"
]);

var KEYWORDS_BEFORE_EXPRESSION = array_to_hash([
	"return",
	"new",
	"delete",
	"throw",
	"else",
	"case"
]);

var KEYWORDS_ATOM = array_to_hash([
	"false",
	"null",
	"true",
	"undefined"
]);

var OPERATOR_CHARS = array_to_hash(characters("+-*&%=<>!?|~^"));

var RE_HEX_NUMBER = /^0x[0-9a-f]+$/i;
var RE_OCT_NUMBER = /^0[0-7]+$/;
var RE_DEC_NUMBER = /^\d*\.?\d*(?:e[+-]?\d*(?:\d\.?|\.?\d)\d*)?$/i;

var OPERATORS = array_to_hash([
	"in",
	"instanceof",
	"typeof",
	"new",
	"void",
	"delete",
	"++",
	"--",
	"+",
	"-",
	"!",
	"~",
	"&",
	"|",
	"^",
	"*",
	"/",
	"%",
	">>",
	"<<",
	">>>",
	"<",
	">",
	"<=",
	">=",
	"==",
	"===",
	"!=",
	"!==",
	"?",
	"=",
	"+=",
	"-=",
	"/=",
	"*=",
	"%=",
	">>=",
	"<<=",
	">>>=",
	"|=",
	"^=",
	"&=",
	"&&",
	"||"
]);

var WHITESPACE_CHARS = array_to_hash(characters(" \u00a0\n\r\t\f\v\u200b"));

var PUNC_BEFORE_EXPRESSION = array_to_hash(characters("[{}(,.;:"));

var PUNC_CHARS = array_to_hash(characters("[]{}(),;:"));

var REGEXP_MODIFIERS = array_to_hash(characters("gmsiy"));

/* -----[ Tokenizer ]----- */

// regexps adapted from http://xregexp.com/plugins/#unicode
var UNICODE = {
	letter: new RegExp("[\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0523\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0621-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971\\u0972\\u097B-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D28\\u0D2A-\\u0D39\\u0D3D\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC\\u0EDD\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8B\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10D0-\\u10FA\\u10FC\\u1100-\\u1159\\u115F-\\u11A2\\u11A8-\\u11F9\\u1200-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u1676\\u1681-\\u169A\\u16A0-\\u16EA\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19A9\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u2094\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183\\u2184\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2C6F\\u2C71-\\u2C7D\\u2C80-\\u2CE4\\u2D00-\\u2D25\\u2D30-\\u2D65\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005\\u3006\\u3031-\\u3035\\u303B\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31B7\\u31F0-\\u31FF\\u3400\\u4DB5\\u4E00\\u9FC3\\uA000-\\uA48C\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA65F\\uA662-\\uA66E\\uA67F-\\uA697\\uA717-\\uA71F\\uA722-\\uA788\\uA78B\\uA78C\\uA7FB-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA90A-\\uA925\\uA930-\\uA946\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAC00\\uD7A3\\uF900-\\uFA2D\\uFA30-\\uFA6A\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC]"),
	non_spacing_mark: new RegExp("[\\u0300-\\u036F\\u0483-\\u0487\\u0591-\\u05BD\\u05BF\\u05C1\\u05C2\\u05C4\\u05C5\\u05C7\\u0610-\\u061A\\u064B-\\u065E\\u0670\\u06D6-\\u06DC\\u06DF-\\u06E4\\u06E7\\u06E8\\u06EA-\\u06ED\\u0711\\u0730-\\u074A\\u07A6-\\u07B0\\u07EB-\\u07F3\\u0816-\\u0819\\u081B-\\u0823\\u0825-\\u0827\\u0829-\\u082D\\u0900-\\u0902\\u093C\\u0941-\\u0948\\u094D\\u0951-\\u0955\\u0962\\u0963\\u0981\\u09BC\\u09C1-\\u09C4\\u09CD\\u09E2\\u09E3\\u0A01\\u0A02\\u0A3C\\u0A41\\u0A42\\u0A47\\u0A48\\u0A4B-\\u0A4D\\u0A51\\u0A70\\u0A71\\u0A75\\u0A81\\u0A82\\u0ABC\\u0AC1-\\u0AC5\\u0AC7\\u0AC8\\u0ACD\\u0AE2\\u0AE3\\u0B01\\u0B3C\\u0B3F\\u0B41-\\u0B44\\u0B4D\\u0B56\\u0B62\\u0B63\\u0B82\\u0BC0\\u0BCD\\u0C3E-\\u0C40\\u0C46-\\u0C48\\u0C4A-\\u0C4D\\u0C55\\u0C56\\u0C62\\u0C63\\u0CBC\\u0CBF\\u0CC6\\u0CCC\\u0CCD\\u0CE2\\u0CE3\\u0D41-\\u0D44\\u0D4D\\u0D62\\u0D63\\u0DCA\\u0DD2-\\u0DD4\\u0DD6\\u0E31\\u0E34-\\u0E3A\\u0E47-\\u0E4E\\u0EB1\\u0EB4-\\u0EB9\\u0EBB\\u0EBC\\u0EC8-\\u0ECD\\u0F18\\u0F19\\u0F35\\u0F37\\u0F39\\u0F71-\\u0F7E\\u0F80-\\u0F84\\u0F86\\u0F87\\u0F90-\\u0F97\\u0F99-\\u0FBC\\u0FC6\\u102D-\\u1030\\u1032-\\u1037\\u1039\\u103A\\u103D\\u103E\\u1058\\u1059\\u105E-\\u1060\\u1071-\\u1074\\u1082\\u1085\\u1086\\u108D\\u109D\\u135F\\u1712-\\u1714\\u1732-\\u1734\\u1752\\u1753\\u1772\\u1773\\u17B7-\\u17BD\\u17C6\\u17C9-\\u17D3\\u17DD\\u180B-\\u180D\\u18A9\\u1920-\\u1922\\u1927\\u1928\\u1932\\u1939-\\u193B\\u1A17\\u1A18\\u1A56\\u1A58-\\u1A5E\\u1A60\\u1A62\\u1A65-\\u1A6C\\u1A73-\\u1A7C\\u1A7F\\u1B00-\\u1B03\\u1B34\\u1B36-\\u1B3A\\u1B3C\\u1B42\\u1B6B-\\u1B73\\u1B80\\u1B81\\u1BA2-\\u1BA5\\u1BA8\\u1BA9\\u1C2C-\\u1C33\\u1C36\\u1C37\\u1CD0-\\u1CD2\\u1CD4-\\u1CE0\\u1CE2-\\u1CE8\\u1CED\\u1DC0-\\u1DE6\\u1DFD-\\u1DFF\\u20D0-\\u20DC\\u20E1\\u20E5-\\u20F0\\u2CEF-\\u2CF1\\u2DE0-\\u2DFF\\u302A-\\u302F\\u3099\\u309A\\uA66F\\uA67C\\uA67D\\uA6F0\\uA6F1\\uA802\\uA806\\uA80B\\uA825\\uA826\\uA8C4\\uA8E0-\\uA8F1\\uA926-\\uA92D\\uA947-\\uA951\\uA980-\\uA982\\uA9B3\\uA9B6-\\uA9B9\\uA9BC\\uAA29-\\uAA2E\\uAA31\\uAA32\\uAA35\\uAA36\\uAA43\\uAA4C\\uAAB0\\uAAB2-\\uAAB4\\uAAB7\\uAAB8\\uAABE\\uAABF\\uAAC1\\uABE5\\uABE8\\uABED\\uFB1E\\uFE00-\\uFE0F\\uFE20-\\uFE26]"),
	space_combining_mark: new RegExp("[\\u0903\\u093E-\\u0940\\u0949-\\u094C\\u094E\\u0982\\u0983\\u09BE-\\u09C0\\u09C7\\u09C8\\u09CB\\u09CC\\u09D7\\u0A03\\u0A3E-\\u0A40\\u0A83\\u0ABE-\\u0AC0\\u0AC9\\u0ACB\\u0ACC\\u0B02\\u0B03\\u0B3E\\u0B40\\u0B47\\u0B48\\u0B4B\\u0B4C\\u0B57\\u0BBE\\u0BBF\\u0BC1\\u0BC2\\u0BC6-\\u0BC8\\u0BCA-\\u0BCC\\u0BD7\\u0C01-\\u0C03\\u0C41-\\u0C44\\u0C82\\u0C83\\u0CBE\\u0CC0-\\u0CC4\\u0CC7\\u0CC8\\u0CCA\\u0CCB\\u0CD5\\u0CD6\\u0D02\\u0D03\\u0D3E-\\u0D40\\u0D46-\\u0D48\\u0D4A-\\u0D4C\\u0D57\\u0D82\\u0D83\\u0DCF-\\u0DD1\\u0DD8-\\u0DDF\\u0DF2\\u0DF3\\u0F3E\\u0F3F\\u0F7F\\u102B\\u102C\\u1031\\u1038\\u103B\\u103C\\u1056\\u1057\\u1062-\\u1064\\u1067-\\u106D\\u1083\\u1084\\u1087-\\u108C\\u108F\\u109A-\\u109C\\u17B6\\u17BE-\\u17C5\\u17C7\\u17C8\\u1923-\\u1926\\u1929-\\u192B\\u1930\\u1931\\u1933-\\u1938\\u19B0-\\u19C0\\u19C8\\u19C9\\u1A19-\\u1A1B\\u1A55\\u1A57\\u1A61\\u1A63\\u1A64\\u1A6D-\\u1A72\\u1B04\\u1B35\\u1B3B\\u1B3D-\\u1B41\\u1B43\\u1B44\\u1B82\\u1BA1\\u1BA6\\u1BA7\\u1BAA\\u1C24-\\u1C2B\\u1C34\\u1C35\\u1CE1\\u1CF2\\uA823\\uA824\\uA827\\uA880\\uA881\\uA8B4-\\uA8C3\\uA952\\uA953\\uA983\\uA9B4\\uA9B5\\uA9BA\\uA9BB\\uA9BD-\\uA9C0\\uAA2F\\uAA30\\uAA33\\uAA34\\uAA4D\\uAA7B\\uABE3\\uABE4\\uABE6\\uABE7\\uABE9\\uABEA\\uABEC]"),
	connector_punctuation: new RegExp("[\\u005F\\u203F\\u2040\\u2054\\uFE33\\uFE34\\uFE4D-\\uFE4F\\uFF3F]")
};

function is_letter(ch) {
	return UNICODE.letter.test(ch);
};

function is_digit(ch) {
	ch = ch.charCodeAt(0);
	return ch >= 48 && ch <= 57; //XXX: find out if "UnicodeDigit" means something else than 0..9
};

function is_alphanumeric_char(ch) {
	return is_digit(ch) || is_letter(ch);
};

function is_unicode_combining_mark(ch) {
	return UNICODE.non_spacing_mark.test(ch) || UNICODE.space_combining_mark.test(ch);
};

function is_unicode_connector_punctuation(ch) {
	return UNICODE.connector_punctuation.test(ch);
};

function is_identifier_start(ch) {
	return ch == "$" || ch == "_" || is_letter(ch);
};

function is_identifier_char(ch) {
	return is_identifier_start(ch)
		|| is_unicode_combining_mark(ch)
		|| is_digit(ch)
		|| is_unicode_connector_punctuation(ch)
		|| ch == "\u200c" // zero-width non-joiner <ZWNJ>
		|| ch == "\u200d" // zero-width joiner <ZWJ> (in my ECMA-262 PDF, this is also 200c)
	;
};

function parse_js_number(num) {
	if (RE_HEX_NUMBER.test(num)) {
		return parseInt(num.substr(2), 16);
	} else if (RE_OCT_NUMBER.test(num)) {
		return parseInt(num.substr(1), 8);
	} else if (RE_DEC_NUMBER.test(num)) {
		return parseFloat(num);
	}
};

function JS_Parse_Error(message, line, col, pos) {
	this.message = message;
	this.line = line;
	this.col = col;
	this.pos = pos;
};

JS_Parse_Error.prototype.toString = function() {
	return this.message + " (line: " + this.line + ", col: " + this.col + ", pos: " + this.pos + ")";
};

function js_error(message, line, col, pos) {
	throw new JS_Parse_Error(message, line, col, pos);
};

function is_token(token, type, val) {
	return token.type == type && (val == null || token.value == val);
};

var EX_EOF = {};

function tokenizer($TEXT) {

	var S = {
		text            : $TEXT.replace(/\r\n?|[\n\u2028\u2029]/g, "\n").replace(/^\uFEFF/, ''),
		pos             : 0,
		tokpos          : 0,
		line            : 0,
		tokline         : 0,
		col             : 0,
		tokcol          : 0,
		newline_before  : false,
		regex_allowed   : false,
		comments_before : []
	};

	function peek() { return S.text.charAt(S.pos); };

	function next(signal_eof) {
		var ch = S.text.charAt(S.pos++);
		if (signal_eof && !ch)
			throw EX_EOF;
		if (ch == "\n") {
			S.newline_before = true;
			++S.line;
			S.col = 0;
		} else {
			++S.col;
		}
		return ch;
	};

	function eof() {
		return !S.peek();
	};

	function find(what, signal_eof) {
		var pos = S.text.indexOf(what, S.pos);
		if (signal_eof && pos == -1) throw EX_EOF;
		return pos;
	};

	function start_token() {
		S.tokline = S.line;
		S.tokcol = S.col;
		S.tokpos = S.pos;
	};

	function token(type, value, is_comment) {
		S.regex_allowed = ((type == "operator" && !HOP(UNARY_POSTFIX, value)) ||
				   (type == "keyword" && HOP(KEYWORDS_BEFORE_EXPRESSION, value)) ||
				   (type == "punc" && HOP(PUNC_BEFORE_EXPRESSION, value)));
		var ret = {
			type  : type,
			value : value,
			line  : S.tokline,
			col   : S.tokcol,
			pos   : S.tokpos,
			nlb   : S.newline_before
		};
		if (!is_comment) {
			ret.comments_before = S.comments_before;
			S.comments_before = [];
		}
		S.newline_before = false;
		return ret;
	};

	function skip_whitespace() {
		while (HOP(WHITESPACE_CHARS, peek()))
			next();
	};

	function read_while(pred) {
		var ret = "", ch = peek(), i = 0;
		while (ch && pred(ch, i++)) {
			ret += next();
			ch = peek();
		}
		return ret;
	};

	function parse_error(err) {
		js_error(err, S.tokline, S.tokcol, S.tokpos);
	};

	function read_num(prefix) {
		var has_e = false, after_e = false, has_x = false, has_dot = prefix == ".";
		var num = read_while(function(ch, i){
			if (ch == "x" || ch == "X") {
				if (has_x) return false;
				return has_x = true;
			}
			if (!has_x && (ch == "E" || ch == "e")) {
				if (has_e) return false;
				return has_e = after_e = true;
			}
			if (ch == "-") {
				if (after_e || (i == 0 && !prefix)) return true;
				return false;
			}
			if (ch == "+") return after_e;
			after_e = false;
			if (ch == ".") {
				if (!has_dot && !has_x)
					return has_dot = true;
				return false;
			}
			return is_alphanumeric_char(ch);
		});
		if (prefix)
			num = prefix + num;
		var valid = parse_js_number(num);
		if (!isNaN(valid)) {
			return token("num", valid);
		} else {
			parse_error("Invalid syntax: " + num);
		}
	};

	function read_escaped_char() {
		var ch = next(true);
		switch (ch) {
			case "n" : return "\n";
			case "r" : return "\r";
			case "t" : return "\t";
			case "b" : return "\b";
			case "v" : return "\v";
			case "f" : return "\f";
			case "0" : return "\0";
			case "x" : return String.fromCharCode(hex_bytes(2));
			case "u" : return String.fromCharCode(hex_bytes(4));
			default  : return ch;
		}
	};

	function hex_bytes(n) {
		var num = 0;
		for (; n > 0; --n) {
			var digit = parseInt(next(true), 16);
			if (isNaN(digit))
				parse_error("Invalid hex-character pattern in string");
			num = (num << 4) | digit;
		}
		return num;
	};

	function read_string() {
		return with_eof_error("Unterminated string constant", function(){
			var quote = next(), ret = "";
			for (;;) {
				var ch = next(true);
				if (ch == "\\") ch = read_escaped_char();
				else if (ch == quote) break;
				ret += ch;
			}
			return token("string", ret);
		});
	};

	function read_line_comment() {
		next();
		var i = find("\n"), ret;
		if (i == -1) {
			ret = S.text.substr(S.pos);
			S.pos = S.text.length;
		} else {
			ret = S.text.substring(S.pos, i);
			S.pos = i;
		}
		return token("comment1", ret, true);
	};

	function read_multiline_comment() {
		next();
		return with_eof_error("Unterminated multiline comment", function(){
			var i = find("*/", true),
				text = S.text.substring(S.pos, i),
				tok = token("comment2", text, true);
			S.pos = i + 2;
			S.line += text.split("\n").length - 1;
			S.newline_before = text.indexOf("\n") >= 0;
			return tok;
		});
	};

	function read_name() {
		var backslash = false, name = "", ch;
		while ((ch = peek()) != null) {
			if (!backslash) {
				if (ch == "\\") backslash = true, next();
				else if (is_identifier_char(ch)) name += next();
				else break;
			}
			else {
				if (ch != "u") parse_error("Expecting UnicodeEscapeSequence -- uXXXX");
				ch = read_escaped_char();
				if (!is_identifier_char(ch)) parse_error("Unicode char: " + ch.charCodeAt(0) + " is not valid in identifier");
				name += ch;
				backslash = false;
			}
		}
		return name;
	};

	function read_regexp() {
		return with_eof_error("Unterminated regular expression", function(){
			var prev_backslash = false, regexp = "", ch, in_class = false;
			while ((ch = next(true))) if (prev_backslash) {
				regexp += "\\" + ch;
				prev_backslash = false;
			} else if (ch == "[") {
				in_class = true;
				regexp += ch;
			} else if (ch == "]" && in_class) {
				in_class = false;
				regexp += ch;
			} else if (ch == "/" && !in_class) {
				break;
			} else if (ch == "\\") {
				prev_backslash = true;
			} else {
				regexp += ch;
			}
			var mods = read_name();
			return token("regexp", [ regexp, mods ]);
		});
	};

	function read_operator(prefix) {
		function grow(op) {
			if (!peek()) return op;
			var bigger = op + peek();
			if (HOP(OPERATORS, bigger)) {
				next();
				return grow(bigger);
			} else {
				return op;
			}
		};
		return token("operator", grow(prefix || next()));
	};

	function handle_slash() {
		next();
		var regex_allowed = S.regex_allowed;
		switch (peek()) {
			case "/":
			S.comments_before.push(read_line_comment());
			S.regex_allowed = regex_allowed;
			return next_token();
			case "*":
			S.comments_before.push(read_multiline_comment());
			S.regex_allowed = regex_allowed;
			return next_token();
		}
		return S.regex_allowed ? read_regexp() : read_operator("/");
	};

	function handle_dot() {
		next();
		return is_digit(peek())
			? read_num(".")
			: token("punc", ".");
	};

	function read_word() {
		var word = read_name();
		return !HOP(KEYWORDS, word)
			? token("name", word)
			: HOP(OPERATORS, word)
			? token("operator", word)
			: HOP(KEYWORDS_ATOM, word)
			? token("atom", word)
			: token("keyword", word);
	};

	function with_eof_error(eof_error, cont) {
		try {
			return cont();
		} catch(ex) {
			if (ex === EX_EOF) parse_error(eof_error);
			else throw ex;
		}
	};

	function next_token(force_regexp) {
		if (force_regexp)
			return read_regexp();
		skip_whitespace();
		start_token();
		var ch = peek();
		if (!ch) return token("eof");
		if (is_digit(ch)) return read_num();
		if (ch == '"' || ch == "'") return read_string();
		if (HOP(PUNC_CHARS, ch)) return token("punc", next());
		if (ch == ".") return handle_dot();
		if (ch == "/") return handle_slash();
		if (HOP(OPERATOR_CHARS, ch)) return read_operator();
		if (ch == "\\" || is_identifier_start(ch)) return read_word();
		parse_error("Unexpected character '" + ch + "'");
	};

	next_token.context = function(nc) {
		if (nc) S = nc;
		return S;
	};

	return next_token;

};

/* -----[ Parser (constants) ]----- */

var UNARY_PREFIX = array_to_hash([
	"typeof",
	"void",
	"delete",
	"--",
	"++",
	"!",
	"~",
	"-",
	"+"
]);

var UNARY_POSTFIX = array_to_hash([ "--", "++" ]);

var ASSIGNMENT = (function(a, ret, i){
	while (i < a.length) {
		ret[a[i]] = a[i].substr(0, a[i].length - 1);
		i++;
	}
	return ret;
})(
	["+=", "-=", "/=", "*=", "%=", ">>=", "<<=", ">>>=", "|=", "^=", "&="],
	{ "=": true },
	0
);

var PRECEDENCE = (function(a, ret){
	for (var i = 0, n = 1; i < a.length; ++i, ++n) {
		var b = a[i];
		for (var j = 0; j < b.length; ++j) {
			ret[b[j]] = n;
		}
	}
	return ret;
})(
	[
		["||"],
		["&&"],
		["|"],
		["^"],
		["&"],
		["==", "===", "!=", "!=="],
		["<", ">", "<=", ">=", "in", "instanceof"],
		[">>", "<<", ">>>"],
		["+", "-"],
		["*", "/", "%"]
	],
	{}
);

var STATEMENTS_WITH_LABELS = array_to_hash([ "for", "do", "while", "switch" ]);

var ATOMIC_START_TOKEN = array_to_hash([ "atom", "num", "string", "regexp", "name" ]);

/* -----[ Parser ]----- */

function NodeWithToken(str, start, end) {
	this.name = str;
	this.start = start;
	this.end = end;
};

NodeWithToken.prototype.toString = function() { return this.name; };

function parse($TEXT, exigent_mode, embed_tokens) {

	var S = {
		input       : typeof $TEXT == "string" ? tokenizer($TEXT, true) : $TEXT,
		token       : null,
		prev        : null,
		peeked      : null,
		in_function : 0,
		in_loop     : 0,
		labels      : []
	};

	S.token = next();

	function is(type, value) {
		return is_token(S.token, type, value);
	};

	function peek() { return S.peeked || (S.peeked = S.input()); };

	function next() {
		S.prev = S.token;
		if (S.peeked) {
			S.token = S.peeked;
			S.peeked = null;
		} else {
			S.token = S.input();
		}
		return S.token;
	};

	function prev() {
		return S.prev;
	};

	function croak(msg, line, col, pos) {
		var ctx = S.input.context();
		js_error(msg,
			 line != null ? line : ctx.tokline,
			 col != null ? col : ctx.tokcol,
			 pos != null ? pos : ctx.tokpos);
	};

	function token_error(token, msg) {
		croak(msg, token.line, token.col);
	};

	function unexpected(token) {
		if (token == null)
			token = S.token;
		token_error(token, "Unexpected token: " + token.type + " (" + token.value + ")");
	};

	function expect_token(type, val) {
		if (is(type, val)) {
			return next();
		}
		token_error(S.token, "Unexpected token " + S.token.type + ", expected " + type);
	};

	function expect(punc) { return expect_token("punc", punc); };

	function can_insert_semicolon() {
		return !exigent_mode && (
			S.token.nlb || is("eof") || is("punc", "}")
		);
	};

	function semicolon() {
		if (is("punc", ";")) next();
		else if (!can_insert_semicolon()) unexpected();
	};

	function as() {
		return slice(arguments);
	};

	function parenthesised() {
		expect("(");
		var ex = expression();
		expect(")");
		return ex;
	};

	function add_tokens(str, start, end) {
		return str instanceof NodeWithToken ? str : new NodeWithToken(str, start, end);
	};

	function maybe_embed_tokens(parser) {
		if (embed_tokens) return function() {
			var start = S.token;
			var ast = parser.apply(this, arguments);
			ast[0] = add_tokens(ast[0], start, prev());
			return ast;
		};
		else return parser;
	};

	var statement = maybe_embed_tokens(function() {
		if (is("operator", "/")) {
			S.peeked = null;
			S.token = S.input(true); // force regexp
		}
		switch (S.token.type) {
			case "num":
			case "string":
			case "regexp":
			case "operator":
			case "atom":
			return simple_statement();

			case "name":
			return is_token(peek(), "punc", ":")
				? labeled_statement(prog1(S.token.value, next, next))
				: simple_statement();

			case "punc":
			switch (S.token.value) {
				case "{":
				return as("block", block_());
				case "[":
				case "(":
				return simple_statement();
				case ";":
				next();
				return as("block");
				default:
				unexpected();
			}

			case "keyword":
			switch (prog1(S.token.value, next)) {
				case "break":
				return break_cont("break");

				case "continue":
				return break_cont("continue");

				case "debugger":
				semicolon();
				return as("debugger");

				case "do":
				return (function(body){
					expect_token("keyword", "while");
					return as("do", prog1(parenthesised, semicolon), body);
				})(in_loop(statement));

				case "for":
				return for_();

				case "function":
				return function_(true);

				case "if":
				return if_();

				case "return":
				if (S.in_function == 0)
					croak("'return' outside of function");
				return as("return",
					  is("punc", ";")
					  ? (next(), null)
					  : can_insert_semicolon()
					  ? null
					  : prog1(expression, semicolon));

				case "switch":
				return as("switch", parenthesised(), switch_block_());

				case "throw":
				return as("throw", prog1(expression, semicolon));

				case "try":
				return try_();

				case "var":
				return prog1(var_, semicolon);

				case "const":
				return prog1(const_, semicolon);

				case "while":
				return as("while", parenthesised(), in_loop(statement));

				case "with":
				return as("with", parenthesised(), statement());

				default:
				unexpected();
			}
		}
	});

	function labeled_statement(label) {
		S.labels.push(label);
		var start = S.token, stat = statement();
		if (exigent_mode && !HOP(STATEMENTS_WITH_LABELS, stat[0]))
			unexpected(start);
		S.labels.pop();
		return as("label", label, stat);
	};

	function simple_statement() {
		return as("stat", prog1(expression, semicolon));
	};

	function break_cont(type) {
		var name;
		if (!can_insert_semicolon()) {
			name = is("name") ? S.token.value : null;
		}
		if (name != null) {
			next();
			if (!member(name, S.labels))
				croak("Label " + name + " without matching loop or statement");
		}
		else if (S.in_loop == 0)
			croak(type + " not inside a loop or switch");
		semicolon();
		return as(type, name);
	};

	function for_() {
		expect("(");
		var init = null;
		if (!is("punc", ";")) {
			init = is("keyword", "var")
				? (next(), var_(true))
				: expression(true, true);
			if (is("operator", "in"))
				return for_in(init);
		}
		return regular_for(init);
	};

	function regular_for(init) {
		expect(";");
		var test = is("punc", ";") ? null : expression();
		expect(";");
		var step = is("punc", ")") ? null : expression();
		expect(")");
		return as("for", init, test, step, in_loop(statement));
	};

	function for_in(init) {
		var lhs = init[0] == "var" ? as("name", init[1][0]) : init;
		next();
		var obj = expression();
		expect(")");
		return as("for-in", init, lhs, obj, in_loop(statement));
	};

	var function_ = maybe_embed_tokens(function(in_statement) {
		var name = is("name") ? prog1(S.token.value, next) : null;
		if (in_statement && !name)
			unexpected();
		expect("(");
		return as(in_statement ? "defun" : "function",
			  name,
			  // arguments
			  (function(first, a){
				  while (!is("punc", ")")) {
					  if (first) first = false; else expect(",");
					  if (!is("name")) unexpected();
					  a.push(S.token.value);
					  next();
				  }
				  next();
				  return a;
			  })(true, []),
			  // body
			  (function(){
				  ++S.in_function;
				  var loop = S.in_loop;
				  S.in_loop = 0;
				  var a = block_();
				  --S.in_function;
				  S.in_loop = loop;
				  return a;
			  })());
	});

	function if_() {
		var cond = parenthesised(), body = statement(), belse;
		if (is("keyword", "else")) {
			next();
			belse = statement();
		}
		return as("if", cond, body, belse);
	};

	function block_() {
		expect("{");
		var a = [];
		while (!is("punc", "}")) {
			if (is("eof")) unexpected();
			a.push(statement());
		}
		next();
		return a;
	};

	var switch_block_ = curry(in_loop, function(){
		expect("{");
		var a = [], cur = null;
		while (!is("punc", "}")) {
			if (is("eof")) unexpected();
			if (is("keyword", "case")) {
				next();
				cur = [];
				a.push([ expression(), cur ]);
				expect(":");
			}
			else if (is("keyword", "default")) {
				next();
				expect(":");
				cur = [];
				a.push([ null, cur ]);
			}
			else {
				if (!cur) unexpected();
				cur.push(statement());
			}
		}
		next();
		return a;
	});

	function try_() {
		var body = block_(), bcatch, bfinally;
		if (is("keyword", "catch")) {
			next();
			expect("(");
			if (!is("name"))
				croak("Name expected");
			var name = S.token.value;
			next();
			expect(")");
			bcatch = [ name, block_() ];
		}
		if (is("keyword", "finally")) {
			next();
			bfinally = block_();
		}
		if (!bcatch && !bfinally)
			croak("Missing catch/finally blocks");
		return as("try", body, bcatch, bfinally);
	};

	function vardefs(no_in) {
		var a = [];
		for (;;) {
			if (!is("name"))
				unexpected();
			var name = S.token.value;
			next();
			if (is("operator", "=")) {
				next();
				a.push([ name, expression(false, no_in) ]);
			} else {
				a.push([ name ]);
			}
			if (!is("punc", ","))
				break;
			next();
		}
		return a;
	};

	function var_(no_in) {
		return as("var", vardefs(no_in));
	};

	function const_() {
		return as("const", vardefs());
	};

	function new_() {
		var newexp = expr_atom(false), args;
		if (is("punc", "(")) {
			next();
			args = expr_list(")");
		} else {
			args = [];
		}
		return subscripts(as("new", newexp, args), true);
	};

	var expr_atom = maybe_embed_tokens(function(allow_calls) {
		if (is("operator", "new")) {
			next();
			return new_();
		}
		if (is("operator") && HOP(UNARY_PREFIX, S.token.value)) {
			return make_unary("unary-prefix",
					  prog1(S.token.value, next),
					  expr_atom(allow_calls));
		}
		if (is("punc")) {
			switch (S.token.value) {
				case "(":
				next();
				return subscripts(prog1(expression, curry(expect, ")")), allow_calls);
				case "[":
				next();
				return subscripts(array_(), allow_calls);
				case "{":
				next();
				return subscripts(object_(), allow_calls);
			}
			unexpected();
		}
		if (is("keyword", "function")) {
			next();
			return subscripts(function_(false), allow_calls);
		}
		if (HOP(ATOMIC_START_TOKEN, S.token.type)) {
			var atom = S.token.type == "regexp"
				? as("regexp", S.token.value[0], S.token.value[1])
				: as(S.token.type, S.token.value);
			return subscripts(prog1(atom, next), allow_calls);
		}
		unexpected();
	});

	function expr_list(closing, allow_trailing_comma, allow_empty) {
		var first = true, a = [];
		while (!is("punc", closing)) {
			if (first) first = false; else expect(",");
			if (allow_trailing_comma && is("punc", closing)) break;
			if (is("punc", ",") && allow_empty) {
				a.push([ "atom", "undefined" ]);
			} else {
				a.push(expression(false));
			}
		}
		next();
		return a;
	};

	function array_() {
		return as("array", expr_list("]", !exigent_mode, true));
	};

	function object_() {
		var first = true, a = [];
		while (!is("punc", "}")) {
			if (first) first = false; else expect(",");
			if (!exigent_mode && is("punc", "}"))
				// allow trailing comma
				break;
			var type = S.token.type;
			var name = as_property_name();
			if (type == "name" && (name == "get" || name == "set") && !is("punc", ":")) {
				a.push([ as_name(), function_(false), name ]);
			} else {
				expect(":");
				a.push([ name, expression(false) ]);
			}
		}
		next();
		return as("object", a);
	};

	function as_property_name() {
		switch (S.token.type) {
			case "num":
			case "string":
			return prog1(S.token.value, next);
		}
		return as_name();
	};

	function as_name() {
		switch (S.token.type) {
			case "name":
			case "operator":
			case "keyword":
			case "atom":
			return prog1(S.token.value, next);
			default:
			unexpected();
		}
	};

	function subscripts(expr, allow_calls) {
		if (is("punc", ".")) {
			next();
			return subscripts(as("dot", expr, as_name()), allow_calls);
		}
		if (is("punc", "[")) {
			next();
			return subscripts(as("sub", expr, prog1(expression, curry(expect, "]"))), allow_calls);
		}
		if (allow_calls && is("punc", "(")) {
			next();
			return subscripts(as("call", expr, expr_list(")")), true);
		}
		if (allow_calls && is("operator") && HOP(UNARY_POSTFIX, S.token.value)) {
			return prog1(curry(make_unary, "unary-postfix", S.token.value, expr),
					next);
		}
		return expr;
	};

	function make_unary(tag, op, expr) {
		if ((op == "++" || op == "--") && !is_assignable(expr))
			croak("Invalid use of " + op + " operator");
		return as(tag, op, expr);
	};

	function expr_op(left, min_prec, no_in) {
		var op = is("operator") ? S.token.value : null;
		if (op && op == "in" && no_in) op = null;
		var prec = op != null ? PRECEDENCE[op] : null;
		if (prec != null && prec > min_prec) {
			next();
			var right = expr_op(expr_atom(true), prec, no_in);
			return expr_op(as("binary", op, left, right), min_prec, no_in);
		}
		return left;
	};

	function expr_ops(no_in) {
		return expr_op(expr_atom(true), 0, no_in);
	};

	function maybe_conditional(no_in) {
		var expr = expr_ops(no_in);
		if (is("operator", "?")) {
			next();
			var yes = expression(false);
			expect(":");
			return as("conditional", expr, yes, expression(false, no_in));
		}
		return expr;
	};

	function is_assignable(expr) {
		if (!exigent_mode) return true;
		switch (expr[0]) {
			case "dot":
			case "sub":
			case "new":
			case "call":
			return true;
			case "name":
			return expr[1] != "this";
		}
	};

	function maybe_assign(no_in) {
		var left = maybe_conditional(no_in), val = S.token.value;
		if (is("operator") && HOP(ASSIGNMENT, val)) {
			if (is_assignable(left)) {
				next();
				return as("assign", ASSIGNMENT[val], left, maybe_assign(no_in));
			}
			croak("Invalid assignment");
		}
		return left;
	};

	var expression = maybe_embed_tokens(function(commas, no_in) {
		if (arguments.length == 0)
			commas = true;
		var expr = maybe_assign(no_in);
		if (commas && is("punc", ",")) {
			next();
			return as("seq", expr, expression(true, no_in));
		}
		return expr;
	});

	function in_loop(cont) {
		try {
			++S.in_loop;
			return cont();
		} finally {
			--S.in_loop;
		}
	};

	return as("toplevel", (function(a){
		while (!is("eof"))
			a.push(statement());
		return a;
	})([]));

};

/* -----[ helper for AST traversal ]----- */

function ast_walker() {
	function _vardefs(defs) {
		return [ this[0], MAP(defs, function(def){
			var a = [ def[0] ];
			if (def.length > 1)
				a[1] = walk(def[1]);
			return a;
		}) ];
	};
	function _block(statements) {
		var out = [ this[0] ];
		if (statements != null)
			out.push(MAP(statements, walk));
		return out;
	};
	var walkers = {
		"string": function(str) {
			return [ this[0], str ];
		},
		"num": function(num) {
			return [ this[0], num ];
		},
		"name": function(name) {
			return [ this[0], name ];
		},
		"toplevel": function(statements) {
			return [ this[0], MAP(statements, walk) ];
		},
		"block": _block,
		"splice": _block,
		"var": _vardefs,
		"const": _vardefs,
		"try": function(t, c, f) {
			return [
				this[0],
				MAP(t, walk),
				c != null ? [ c[0], MAP(c[1], walk) ] : null,
				f != null ? MAP(f, walk) : null
			];
		},
		"throw": function(expr) {
			return [ this[0], walk(expr) ];
		},
		"new": function(ctor, args) {
			return [ this[0], walk(ctor), MAP(args, walk) ];
		},
		"switch": function(expr, body) {
			return [ this[0], walk(expr), MAP(body, function(branch){
				return [ branch[0] ? walk(branch[0]) : null,
					 MAP(branch[1], walk) ];
			}) ];
		},
		"break": function(label) {
			return [ this[0], label ];
		},
		"continue": function(label) {
			return [ this[0], label ];
		},
		"conditional": function(cond, t, e) {
			return [ this[0], walk(cond), walk(t), walk(e) ];
		},
		"assign": function(op, lvalue, rvalue) {
			return [ this[0], op, walk(lvalue), walk(rvalue) ];
		},
		"dot": function(expr) {
			return [ this[0], walk(expr) ].concat(slice(arguments, 1));
		},
		"call": function(expr, args) {
			return [ this[0], walk(expr), MAP(args, walk) ];
		},
		"function": function(name, args, body) {
			return [ this[0], name, args.slice(), MAP(body, walk) ];
		},
		"defun": function(name, args, body) {
			return [ this[0], name, args.slice(), MAP(body, walk) ];
		},
		"if": function(conditional, t, e) {
			return [ this[0], walk(conditional), walk(t), walk(e) ];
		},
		"for": function(init, cond, step, block) {
			return [ this[0], walk(init), walk(cond), walk(step), walk(block) ];
		},
		"for-in": function(vvar, key, hash, block) {
			return [ this[0], walk(vvar), walk(key), walk(hash), walk(block) ];
		},
		"while": function(cond, block) {
			return [ this[0], walk(cond), walk(block) ];
		},
		"do": function(cond, block) {
			return [ this[0], walk(cond), walk(block) ];
		},
		"return": function(expr) {
			return [ this[0], walk(expr) ];
		},
		"binary": function(op, left, right) {
			return [ this[0], op, walk(left), walk(right) ];
		},
		"unary-prefix": function(op, expr) {
			return [ this[0], op, walk(expr) ];
		},
		"unary-postfix": function(op, expr) {
			return [ this[0], op, walk(expr) ];
		},
		"sub": function(expr, subscript) {
			return [ this[0], walk(expr), walk(subscript) ];
		},
		"object": function(props) {
			return [ this[0], MAP(props, function(p){
				return p.length == 2
					? [ p[0], walk(p[1]) ]
					: [ p[0], walk(p[1]), p[2] ]; // get/set-ter
			}) ];
		},
		"regexp": function(rx, mods) {
			return [ this[0], rx, mods ];
		},
		"array": function(elements) {
			return [ this[0], MAP(elements, walk) ];
		},
		"stat": function(stat) {
			return [ this[0], walk(stat) ];
		},
		"seq": function() {
			return [ this[0] ].concat(MAP(slice(arguments), walk));
		},
		"label": function(name, block) {
			return [ this[0], name, walk(block) ];
		},
		"with": function(expr, block) {
			return [ this[0], walk(expr), walk(block) ];
		},
		"atom": function(name) {
			return [ this[0], name ];
		}
	};

	var user = {};
	var stack = [];
	function walk(ast) {
		if (ast == null)
			return null;
		try {
			stack.push(ast);
			var type = ast[0];
			var gen = user[type];
			if (gen) {
				var ret = gen.apply(ast, ast.slice(1));
				if (ret != null)
					return ret;
			}
			gen = walkers[type];
			return gen.apply(ast, ast.slice(1));
		} finally {
			stack.pop();
		}
	};

	function with_walkers(walkers, cont){
		var save = {}, i;
		for (i in walkers) if (HOP(walkers, i)) {
			save[i] = user[i];
			user[i] = walkers[i];
		}
		var ret = cont();
		for (i in save) if (HOP(save, i)) {
			if (!save[i]) delete user[i];
			else user[i] = save[i];
		}
		return ret;
	};

	return {
		walk: walk,
		with_walkers: with_walkers,
		parent: function() {
			return stack[stack.length - 2]; // last one is current node
		},
		stack: function() {
			return stack;
		}
	};
};

function empty(b) {
	return !b || (b[0] == "block" && (!b[1] || b[1].length == 0));
};

/* -----[ re-generate code from the AST ]----- */

var DOT_CALL_NO_PARENS = array_to_hash([
	"name",
	"array",
	"object",
	"string",
	"dot",
	"sub",
	"call",
	"regexp"
]);

function make_string(str, ascii_only) {
	var dq = 0, sq = 0;
	str = str.replace(/[\\\b\f\n\r\t\x22\x27\u2028\u2029]/g, function(s){
		switch (s) {
			case "\\": return "\\\\";
			case "\b": return "\\b";
			case "\f": return "\\f";
			case "\n": return "\\n";
			case "\r": return "\\r";
			case "\t": return "\\t";
			case "\u2028": return "\\u2028";
			case "\u2029": return "\\u2029";
			case '"': ++dq; return '"';
			case "'": ++sq; return "'";
		}
		return s;
	});
	if (ascii_only) str = to_ascii(str);
	if (dq > sq) return "'" + str.replace(/\x27/g, "\\'") + "'";
	else return '"' + str.replace(/\x22/g, '\\"') + '"';
};

function to_ascii(str) {
	return str.replace(/[\u0080-\uffff]/g, function(ch) {
		var code = ch.charCodeAt(0).toString(16);
		while (code.length < 4) code = "0" + code;
		return "\\u" + code;
	});
};

var SPLICE_NEEDS_BRACKETS = array_to_hash([ "if", "while", "do", "for", "for-in", "with" ]);

function gen_code(ast, options) {
	options = defaults(options, {
		indent_start : 0,
		indent_level : 4,
		quote_keys   : false,
		space_colon  : false,
		beautify	 : false,
		ascii_only   : false
	});
	var beautify = !!options.beautify;
	var indentation = 0,
		newline = beautify ? "\n" : "",
		space = beautify ? " " : "";

	function encode_string(str) {
		return make_string(str, options.ascii_only);
	};

	function make_name(name) {
		name = name.toString();
		if (options.ascii_only)
			name = to_ascii(name);
		return name;
	};

	function indent(line) {
		if (line == null)
			line = "";
		if (beautify)
			line = repeat_string(" ", options.indent_start + indentation * options.indent_level) + line;
		return line;
	};

	function with_indent(cont, incr) {
		if (incr == null) incr = 1;
		indentation += incr;
		try { return cont.apply(null, slice(arguments, 1)); }
		finally { indentation -= incr; }
	};

	function add_spaces(a) {
		if (beautify)
			return a.join(" ");
		var b = [];
		for (var i = 0; i < a.length; ++i) {
			var next = a[i + 1];
			b.push(a[i]);
			if (next &&
				((/[a-z0-9_\x24]$/i.test(a[i].toString()) && /^[a-z0-9_\x24]/i.test(next.toString())) ||
				 (/[\+\-]$/.test(a[i].toString()) && /^[\+\-]/.test(next.toString())))) {
				b.push(" ");
			}
		}
		return b.join("");
	};

	function add_commas(a) {
		return a.join("," + space);
	};

	function parenthesize(expr) {
		var gen = make(expr);
		for (var i = 1; i < arguments.length; ++i) {
			var el = arguments[i];
			if ((el instanceof Function && el(expr)) || expr[0] == el)
				return "(" + gen + ")";
		}
		return gen;
	};

	function best_of(a) {
		if (a.length == 1) {
			return a[0];
		}
		if (a.length == 2) {
			var b = a[1];
			a = a[0];
			return a.length <= b.length ? a : b;
		}
		return best_of([ a[0], best_of(a.slice(1)) ]);
	};

	function needs_parens(expr) {
		if (expr[0] == "function" || expr[0] == "object") {
			// dot/call on a literal function requires the
			// function literal itself to be parenthesized
			// only if it's the first "thing" in a
			// statement.  This means that the parent is
			// "stat", but it could also be a "seq" and
			// we're the first in this "seq" and the
			// parent is "stat", and so on.  Messy stuff,
			// but it worths the trouble.
			var a = slice($stack), self = a.pop(), p = a.pop();
			while (p) {
				if (p[0] == "stat") return true;
				if (((p[0] == "seq" || p[0] == "call" || p[0] == "dot" || p[0] == "sub" || p[0] == "conditional") && p[1] === self) ||
					((p[0] == "binary" || p[0] == "assign" || p[0] == "unary-postfix") && p[2] === self)) {
					self = p;
					p = a.pop();
				} else {
					return false;
				}
			}
		}
		return !HOP(DOT_CALL_NO_PARENS, expr[0]);
	};

	function make_num(num) {
		var str = num.toString(10), a = [ str.replace(/^0\./, ".") ], m;
		if (Math.floor(num) === num) {
			a.push("0x" + num.toString(16).toLowerCase(), // probably pointless
				   "0" + num.toString(8)); // same.
			if ((m = /^(.*?)(0+)$/.exec(num))) {
				a.push(m[1] + "e" + m[2].length);
			}
		} else if ((m = /^0?\.(0+)(.*)$/.exec(num))) {
			a.push(m[2] + "e-" + (m[1].length + m[2].length),
				   str.substr(str.indexOf(".")));
		}
		return best_of(a);
	};

	var generators = {
		"string": encode_string,
		"num": make_num,
		"name": make_name,
		"toplevel": function(statements) {
			return make_block_statements(statements)
				.join(newline + newline);
		},
		"splice": function(statements) {
			var parent = $stack[$stack.length - 2][0];
			if (HOP(SPLICE_NEEDS_BRACKETS, parent)) {
				// we need block brackets in this case
				return make_block.apply(this, arguments);
			} else {
				return MAP(make_block_statements(statements, true),
					   function(line, i) {
						   // the first line is already indented
						   return i > 0 ? indent(line) : line;
					   }).join(newline);
			}
		},
		"block": make_block,
		"var": function(defs) {
			return "var " + add_commas(MAP(defs, make_1vardef)) + ";";
		},
		"const": function(defs) {
			return "const " + add_commas(MAP(defs, make_1vardef)) + ";";
		},
		"try": function(tr, ca, fi) {
			var out = [ "try", make_block(tr) ];
			if (ca) out.push("catch", "(" + ca[0] + ")", make_block(ca[1]));
			if (fi) out.push("finally", make_block(fi));
			return add_spaces(out);
		},
		"throw": function(expr) {
			return add_spaces([ "throw", make(expr) ]) + ";";
		},
		"new": function(ctor, args) {
			args = args.length > 0 ? "(" + add_commas(MAP(args, make)) + ")" : "";
			return add_spaces([ "new", parenthesize(ctor, "seq", "binary", "conditional", "assign", function(expr){
				var w = ast_walker(), has_call = {};
				try {
					w.with_walkers({
						"call": function() { throw has_call },
						"function": function() { return this }
					}, function(){
						w.walk(expr);
					});
				} catch(ex) {
					if (ex === has_call)
						return true;
					throw ex;
				}
			}) + args ]);
		},
		"switch": function(expr, body) {
			return add_spaces([ "switch", "(" + make(expr) + ")", make_switch_block(body) ]);
		},
		"break": function(label) {
			var out = "break";
			if (label != null)
				out += " " + make_name(label);
			return out + ";";
		},
		"continue": function(label) {
			var out = "continue";
			if (label != null)
				out += " " + make_name(label);
			return out + ";";
		},
		"conditional": function(co, th, el) {
			return add_spaces([ parenthesize(co, "assign", "seq", "conditional"), "?",
						parenthesize(th, "seq"), ":",
						parenthesize(el, "seq") ]);
		},
		"assign": function(op, lvalue, rvalue) {
			if (op && op !== true) op += "=";
			else op = "=";
			return add_spaces([ make(lvalue), op, parenthesize(rvalue, "seq") ]);
		},
		"dot": function(expr) {
			var out = make(expr), i = 1;
			if (expr[0] == "num") {
				if (!/\./.test(expr[1]))
					out += ".";
			} else if (needs_parens(expr))
				out = "(" + out + ")";
			while (i < arguments.length)
				out += "." + make_name(arguments[i++]);
			return out;
		},
		"call": function(func, args) {
			var f = make(func);
			if (needs_parens(func))
				f = "(" + f + ")";
			return f + "(" + add_commas(MAP(args, function(expr){
				return parenthesize(expr, "seq");
			})) + ")";
		},
		"function": make_function,
		"defun": make_function,
		"if": function(co, th, el) {
			var out = [ "if", "(" + make(co) + ")", el ? make_then(th) : make(th) ];
			if (el) {
				out.push("else", make(el));
			}
			return add_spaces(out);
		},
		"for": function(init, cond, step, block) {
			var out = [ "for" ];
			init = (init != null ? make(init) : "").replace(/;*\s*$/, ";" + space);
			cond = (cond != null ? make(cond) : "").replace(/;*\s*$/, ";" + space);
			step = (step != null ? make(step) : "").replace(/;*\s*$/, "");
			var args = init + cond + step;
			if (args == "; ; ") args = ";;";
			out.push("(" + args + ")", make(block));
			return add_spaces(out);
		},
		"for-in": function(vvar, key, hash, block) {
			return add_spaces([ "for", "(" +
						(vvar ? make(vvar).replace(/;+$/, "") : make(key)),
						"in",
						make(hash) + ")", make(block) ]);
		},
		"while": function(condition, block) {
			return add_spaces([ "while", "(" + make(condition) + ")", make(block) ]);
		},
		"do": function(condition, block) {
			return add_spaces([ "do", make(block), "while", "(" + make(condition) + ")" ]) + ";";
		},
		"return": function(expr) {
			var out = [ "return" ];
			if (expr != null) out.push(make(expr));
			return add_spaces(out) + ";";
		},
		"binary": function(operator, lvalue, rvalue) {
			var left = make(lvalue), right = make(rvalue);
			// XXX: I'm pretty sure other cases will bite here.
			//	  we need to be smarter.
			//	  adding parens all the time is the safest bet.
			if (member(lvalue[0], [ "assign", "conditional", "seq" ]) ||
				lvalue[0] == "binary" && PRECEDENCE[operator] > PRECEDENCE[lvalue[1]]) {
				left = "(" + left + ")";
			}
			if (member(rvalue[0], [ "assign", "conditional", "seq" ]) ||
				rvalue[0] == "binary" && PRECEDENCE[operator] >= PRECEDENCE[rvalue[1]] &&
				!(rvalue[1] == operator && member(operator, [ "&&", "||", "*" ]))) {
				right = "(" + right + ")";
			}
			return add_spaces([ left, operator, right ]);
		},
		"unary-prefix": function(operator, expr) {
			var val = make(expr);
			if (!(expr[0] == "num" || (expr[0] == "unary-prefix" && !HOP(OPERATORS, operator + expr[1])) || !needs_parens(expr)))
				val = "(" + val + ")";
			return operator + (is_alphanumeric_char(operator.charAt(0)) ? " " : "") + val;
		},
		"unary-postfix": function(operator, expr) {
			var val = make(expr);
			if (!(expr[0] == "num" || (expr[0] == "unary-postfix" && !HOP(OPERATORS, operator + expr[1])) || !needs_parens(expr)))
				val = "(" + val + ")";
			return val + operator;
		},
		"sub": function(expr, subscript) {
			var hash = make(expr);
			if (needs_parens(expr))
				hash = "(" + hash + ")";
			return hash + "[" + make(subscript) + "]";
		},
		"object": function(props) {
			if (props.length == 0)
				return "{}";
			return "{" + newline + with_indent(function(){
				return MAP(props, function(p){
					if (p.length == 3) {
						// getter/setter.  The name is in p[0], the arg.list in p[1][2], the
						// body in p[1][3] and type ("get" / "set") in p[2].
						return indent(make_function(p[0], p[1][2], p[1][3], p[2]));
					}
					var key = p[0], val = make(p[1]);
					if (options.quote_keys) {
						key = encode_string(key);
					} else if ((typeof key == "number" || !beautify && +key + "" == key)
						   && parseFloat(key) >= 0) {
						key = make_num(+key);
					} else if (!is_identifier(key)) {
						key = encode_string(key);
					}
					return indent(add_spaces(beautify && options.space_colon
								 ? [ key, ":", val ]
								 : [ key + ":", val ]));
				}).join("," + newline);
			}) + newline + indent("}");
		},
		"regexp": function(rx, mods) {
			return "/" + rx + "/" + mods;
		},
		"array": function(elements) {
			if (elements.length == 0) return "[]";
			return add_spaces([ "[", add_commas(MAP(elements, function(el){
				if (!beautify && el[0] == "atom" && el[1] == "undefined") return "";
				return parenthesize(el, "seq");
			})), "]" ]);
		},
		"stat": function(stmt) {
			return make(stmt).replace(/;*\s*$/, ";");
		},
		"seq": function() {
			return add_commas(MAP(slice(arguments), make));
		},
		"label": function(name, block) {
			return add_spaces([ make_name(name), ":", make(block) ]);
		},
		"with": function(expr, block) {
			return add_spaces([ "with", "(" + make(expr) + ")", make(block) ]);
		},
		"atom": function(name) {
			return make_name(name);
		}
	};

	// The squeezer replaces "block"-s that contain only a single
	// statement with the statement itself; technically, the AST
	// is correct, but this can create problems when we output an
	// IF having an ELSE clause where the THEN clause ends in an
	// IF *without* an ELSE block (then the outer ELSE would refer
	// to the inner IF).  This function checks for this case and
	// adds the block brackets if needed.
	function make_then(th) {
		if (th[0] == "do") {
			// https://github.com/mishoo/UglifyJS/issues/#issue/57
			// IE croaks with "syntax error" on code like this:
			//	 if (foo) do ... while(cond); else ...
			// we need block brackets around do/while
			return make([ "block", [ th ]]);
		}
		var b = th;
		while (true) {
			var type = b[0];
			if (type == "if") {
				if (!b[3])
					// no else, we must add the block
					return make([ "block", [ th ]]);
				b = b[3];
			}
			else if (type == "while" || type == "do") b = b[2];
			else if (type == "for" || type == "for-in") b = b[4];
			else break;
		}
		return make(th);
	};

	function make_function(name, args, body, keyword) {
		var out = keyword || "function";
		if (name) {
			out += " " + make_name(name);
		}
		out += "(" + add_commas(MAP(args, make_name)) + ")";
		return add_spaces([ out, make_block(body) ]);
	};

	function make_block_statements(statements, noindent) {
		for (var a = [], last = statements.length - 1, i = 0; i <= last; ++i) {
			var stat = statements[i];
			var code = make(stat);
			if (code != ";") {
				if (!beautify && i == last) {
					if ((stat[0] == "while" && empty(stat[2])) ||
						(member(stat[0], [ "for", "for-in"] ) && empty(stat[4])) ||
						(stat[0] == "if" && empty(stat[2]) && !stat[3]) ||
						(stat[0] == "if" && stat[3] && empty(stat[3]))) {
						code = code.replace(/;*\s*$/, ";");
					} else {
						code = code.replace(/;+\s*$/, "");
					}
				}
				a.push(code);
			}
		}
		return noindent ? a : MAP(a, indent);
	};

	function make_switch_block(body) {
		var n = body.length;
		if (n == 0) return "{}";
		return "{" + newline + MAP(body, function(branch, i){
			var has_body = branch[1].length > 0, code = with_indent(function(){
				return indent(branch[0]
						  ? add_spaces([ "case", make(branch[0]) + ":" ])
						  : "default:");
			}, 0.5) + (has_body ? newline + with_indent(function(){
				return make_block_statements(branch[1]).join(newline);
			}) : "");
			if (!beautify && has_body && i < n - 1)
				code += ";";
			return code;
		}).join(newline) + newline + indent("}");
	};

	function make_block(statements) {
		if (!statements) return ";";
		if (statements.length == 0) return "{}";
		return "{" + newline + with_indent(function(){
			return make_block_statements(statements).join(newline);
		}) + newline + indent("}");
	};

	function make_1vardef(def) {
		var name = def[0], val = def[1];
		if (val != null)
			name = add_spaces([ make_name(name), "=", parenthesize(val, "seq") ]);
		return name;
	};

	var $stack = [];

	function make(node) {
		var type = node[0];
		var gen = generators[type];
		if (!gen)
			throw new Error("Can't find generator for \"" + type + "\"");
		$stack.push(node);
		var ret = gen.apply(type, node.slice(1));
		$stack.pop();
		return ret;
	};

	return make(ast);
};

/* -----[ Utilities ]----- */

function curry(f) {
	var args = slice(arguments, 1);
	return function() { return f.apply(this, args.concat(slice(arguments))); };
};

function prog1(ret) {
	if (ret instanceof Function)
		ret = ret();
	for (var i = 1, n = arguments.length; --n > 0; ++i)
		arguments[i]();
	return ret;
};

function array_to_hash(a) {
	var ret = {};
	for (var i = 0; i < a.length; ++i)
		ret[a[i]] = true;
	return ret;
};

function slice(a, start) {
	return Array.prototype.slice.call(a, start || 0);
};

function characters(str) {
	return str.split("");
};

function member(name, array) {
	for (var i = array.length; --i >= 0;)
		if (array[i] === name)
			return true;
	return false;
};

function repeat_string(str, i) {
	return i < 1 ? "" : new Array(i + 1).join(str);
};

function defaults(args, defs) {
	var ret = {};
	if (args === true)
		args = {};
	for (var i in defs) if (HOP(defs, i)) {
		ret[i] = (args && HOP(args, i)) ? args[i] : defs[i];
	}
	return ret;
};

function is_identifier(name) {
	return /^[a-z_$][a-z0-9_$]*$/i.test(name)
		&& name != "this"
		&& !HOP(KEYWORDS_ATOM, name)
		&& !HOP(RESERVED_WORDS, name)
		&& !HOP(KEYWORDS, name);
};

function HOP(obj, prop) {
	return Object.prototype.hasOwnProperty.call(obj, prop);
};

function MAP(a, f, o) {
	var ret = [];
	for (var i = 0; i < a.length; ++i) {
		ret.push(f.call(o, a[i], i));
	}
	return ret;
};

/* -----[ Exports ]----- */

return {
	parse: parse,
	gen_code: gen_code,
	tokenizer: tokenizer,
	ast_walker: ast_walker
};

};