UNPKG

imba

Version:

Intuitive and powerful language for building webapps that fly

2,861 lines 79.3 kB
import * as T from './token.mjs';
import { INVERSES as INVERSES } from './constants.mjs';
import { Compilation } from './compilation.mjs';
import * as ERR from './errors.mjs';
import * as helpers from './helpers.mjs';
function len$(a){
	return a && (a.len instanceof Function ? a.len() : a.length) || 0;
};
function iter$(a){ return a ? (a.toArray ? a.toArray() : a) : []; };
function map$(list){
	var map = Object.create(null);
	for (let i = 0, items = iter$(list), len = items.length; i < len; i++) {
		map[items[i]] = 1;
	};
	return map;
};
// helper for subclassing
function subclass$(obj,sup) {
	for (var k in sup) {
		if (sup.hasOwnProperty(k)) obj[k] = sup[k];
	};
	// obj.__super__ = sup;
	obj.prototype = Object.create(sup.prototype);
	obj.__super__ = obj.prototype.__super__ = sup.prototype;
	obj.prototype.initialize = obj.prototype.constructor = obj;
};

// imba$inlineHelpers=1
// imba$v2=0

var Token = T.Token;

var K = 0;

// Constants
// ---------

// Keywords that Imba shares in common with JavaScript.
var JS_KEYWORDS = [
	'true','false','null','this',
	'delete','typeof','in','instanceof',
	'throw','break','continue','debugger',
	'if','else','switch','for','while','do','try','catch','finally',
	'class','extends','super','return','yield'
];

var TSC_CARET_BEFORE = {
	',': 1,
	'\n': 1,
	')': 1,
	']': 1,
	'}': 1,
	'>': 1,
	' ': 1
};

var TYPE_GENERICS_AFTER = /\w|\]|\)$/;

// new can be used as a keyword in imba, since object initing is done through
// MyObject.new. new is a very useful varname.

// We want to treat return like any regular call for now
// Must be careful to throw the exceptions in AST, since the parser
// wont

// Imba-only keywords. var should move to JS_Keywords
// some words (like tokid) should be context-specific
var IMBA_KEYWORDS = [
	'undefined','then','unless','until','loop','of','by',
	'when','def','tag','do','elif','begin','var','let','const','await','import','module','export','static','extend','yield'
];

var IMBA_CONTEXTUAL_KEYWORDS = ['extend','local','global','prop'];

var CONTEXTUAL_KEYWORDS = {
	LET: {'global': 1,'declare': 1,static: 1},
	CONST: {'global': 1,'declare': 1,static: 1},
	VAR: {'global': 1,'declare': 1,static: 1},
	CLASS: {'global': 1,'declare': 1,static: 1,extend: 1,strict: 1,abstract: 1,mixin: 1},
	MIXIN: {'global': 1,'declare': 1,extend: 1,strict: 1,abstract: 1},
	INTERFACE: {'global': 1,'declare': 1,extend: 1,strict: 1,mixin: 1},
	TAG: {'global': 1,'declare': 1,'local': 1,extend: 1,strict: 1,abstract: 1},
	DEF: {'global': 1,'declare': 1,protected: 1},
	PROP: {'static': 1},
	ATTR: {'static': 1},
	CSS: {'global': 1,export: 1}
};

var MEMBER_KEYWORDS = {
	DEF: 1,
	PROP: 1,
	ATTR: 1
};

// FixedArray for performance
// var ALL_KEYWORDS = JS_KEYWORDS.concat(IMBA_KEYWORDS)
var ALL_KEYWORDS = [
	'true','false','null','this','self',
	'delete','typeof','in','instanceof',
	'throw','break','continue','debugger',
	'if','else','switch','for','while','do','try','catch','finally',
	'class','extends','super','return',
	'undefined','then','unless','until','loop','of','by',
	'when','def','tag','do','elif','begin','var','let','const','await','import',
	'and','or','is','isnt','not','yes','no','isa','case','nil','module','export','static','extend',
	'yield'
];
var ALL_KEYWORDS_MAP = map$(ALL_KEYWORDS);
var KEYWORD_CANDIDATE_MAP = map$(ALL_KEYWORDS.concat(['mixin','guard','alter','watch','css','interface','attr','prop','get','set','constructor','declare']));
var SPECIAL_IDENTIFIER_MAP = map$(['default','type','from','as','new','arguments']);

// The list of keywords that are reserved by JavaScript, but not used, or are
// used by Imba internally. We throw an error when these are encountered,
// to avoid having a JavaScript error at runtime.  # 'var', 'let', - not inside here
var RESERVED = ['case','default','function','void','with','const','enum','native'];
var STRICT_RESERVED = ['case','function','void','const'];

// The superset of both JavaScript keywords and reserved words, none of which may
// be used as identifiers or properties.
var JS_FORBIDDEN = JS_KEYWORDS.concat(RESERVED);

var METHOD_IDENTIFIER = /^((([\x23]?[\$A-Za-z_\x7f-\uffff][$\-\w\x7f-\uffff]*)([\=]?))|(<=>|\|(?![\|=])))/;
// removed ~=|~| |&(?![&=])

// Token matching regexes.
// added hyphens to identifiers now - to test
var IDENTIFIER = /^((\$|##|#|@@|@|\%)[\$\wA-Za-z_\-\x7f-\uffff][$\w\x7f-\uffff]*(\-[$\w\x7f-\uffff]+)*[\?]?|[$A-Za-z_][$\w\x7f-\uffff]*(\-[$\w\x7f-\uffff]+)*[\?]?)([^\n\S]*:)?/;

var IMPORTS = /^import\s+(\{?[^\"\'\}]+\}?)(?=\s+from\s+)/;

var OBJECT_KEY = /^((\$?)[$A-Za-z_\x7f-\uffff\-][$\w\x7f-\uffff\-]*)([^\n\S\s]*:(?![\*\=:$A-Za-z\_\x7f-\uffff]))/;

var TAG = /^(\<)(?=[A-Za-z\#\.\%\$\[\{\@\>\(])/;

var TAG_TYPE = /^(\w[\w\d]*:)?(\w[\w\d]*)(-[\w\d]+)*/;
var TAG_ID = /^#((\w[\w\d]*)(-[\w\d]+)*)/;
var TAG_PART = /^[\:\.\#]?([A-Za-z\_][\w\-]*)(\:[A-Za-z\_][\w\-]*)?/;
var TAG_ATTR = /^([\.\:]?[\w\_]+([\-\:\.][\w]+)*)(\s)*\=(?!\>)/;

var SELECTOR = /^([%\$]{1,2})([\(])/;
var SELECTOR_PART = /^(\#|\.|:|::)?([\w]+(\-[\w]+)*)/;
var SELECTOR_COMBINATOR = /^ (\+|\>|\~)*\s*(?=[\w\.\#\:\{\*\[])/;

var SELECTOR_PSEUDO_CLASS = /^(::?)([\w]+(\-[\w]+)*)/;
var SELECTOR_ATTR_OP = /^(\$=|\~=|\^=|\*=|\|=|=|\!=)/;
var SELECTOR_ATTR = /^\[([\w\_\-]+)(\$=|\~=|\^=|\*=|\|=|=|\!=)/;

var SYMBOL = /^\:((([\*\@$\w\x7f-\uffff]+)+([\-\\\:][\w\x7f-\uffff]+)*)|==|\<=\>)/;

var STYLE_HEX = /^\#[\w\-]+/;

var STYLE_NUMERIC = /^(\-?\d*\.?\d+)([A-Za-z]+|\%)?(?![\d\w])/;

var STYLE_IDENTIFIER = /^[\w\-\$]*\w[\w\-\$]*/;

var STYLE_URL = /^url\(([^\)]*)\)/;

var STYLE_PROPERTY = /^(\^?[\w\-\$\@\.\!\#\^]+)(?=\:([^\:]|$)|\s*\=)/;

var STYLE_MODIFIERS = /^(\@?[\w\-\$]*\w[\w\-\$]*)([\.\@][\w\-\$]*\w[\w\-\$]*)*(\@[\w\-\$]*\w[\w\-\$]*)*(?=\:)/;

var isStyleWordCode = function(code) {
	return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code == 95;
};

var isStylePropertyCode = function(code) {
	return isStyleWordCode(code) || code == 45 || code == 36 || code == 64 || code == 46 || code == 33 || code == 35 || code == 94;
};

var isStyleSpaceCode = function(code) {
	return (code >= 9 && code <= 13) || code == 32 || code == 160 || code == 5760 || code == 65279 || code == 8232 || code == 8233 || code == 8239 || code == 8287 || code == 12288 || (code >= 8192 && code <= 8202);
};

var stylePropertyLengthAt = function(str,start) {
	if(start === undefined) start = 0;
	var i = start;
	var len = str.length;
	while (i < len && isStylePropertyCode(str.charCodeAt(i))) {
		i++;
	};
	if (i == start) { return 0 };
	if (str.charCodeAt(i) == 58 && str.charCodeAt(i + 1) != 58) {
		return i - start;
	};
	var j = i;
	while (j < len && isStyleSpaceCode(str.charCodeAt(j))) {
		j++;
	};
	return str.charCodeAt(j) == 61 ? i - start : 0;
};

var canStartStyleSelector = function(str,chr) {
	var code = chr.charCodeAt(0);
	if (isStyleWordCode(code) || code == 94 || code == 37 || code == 42 || code == 38 || code == 36 || code == 62 || code == 47 || code == 46 || code == 91 || code == 64 || code == 33) {
		return true;
	};
	if (code == 35) {
		let next = str.charCodeAt(1);
		return isStyleWordCode(next) || next == 45;
	};
	return code == 58 && str.charCodeAt(1) == 58;
};

var NUMBER = /^0x[\da-f_]+|^0b[01_]+|^0o[\d_]+|^[\-]?(?:\d[_\d]*)\.?\d[_\d]*(?:e[+-]?\d+)?|^[\-]?\d*\.?\d+(?:e[+-]?\d+)?/i;

var HEREDOC = /^("""|''')([\s\S]*?)(?:\n[^\n\S]*)?\1/;

var OPERATOR = /^(?:[-=]=>|!&(?=[\s\n])|[&|~^]?=\?|[&|~^]=|\?\?=|===|---|->|=>|\/>|!==|\*\*=?|[-+*\/%<>&|^!?=]=|=<|>>>=?|([-+:])\1|([&|<>])\2=?|\?\.|\?\?|\.{2,3}|\*(?=[a-zA-Z\_]))/;

// FIXME splat should only be allowed when the previous thing is spaced or inside call?

var WHITESPACE = /^[^\n\S]+/;

var COMMENT = /^###([^#][\s\S]*?)(?:###[^\n\S]*|(?:###)?$)/;
var JS_COMMENT = /^\/\*([\s\S]*?)\*\//;
// var INLINE_COMMENT = /^(\s*)((#[ \t\!]||\/\/)(.*)|#[ \t]?(?=\n|$))+/
var INLINE_COMMENT = /^(\s*)((#[ \t\!]|\/\/(?!\/))(.*)|#[ \t]?(?=\n|$))+/;

var CODE = /^[-=]=>/;

var MULTI_DENT = /^(?:\n[^\n\S]*)+/;

var SIMPLESTR = /^'[^\\']*(?:\\.[^\\']*)*'/;

var JSTOKEN = /^`[^\\`]*(?:\\.[^\\`]*)*`/;

// Regex-matching-regexes.
var REGEX = /^(\/(?![\s=])[^[\/\n\\]*(?:(?:\\[\s\S]|\[[^\]\n\\]*(?:\\[\s\S][^\]\n\\]*)*])[^[\/\n\\]*)*\/)([a-z]{0,8})(?!\w)/;

var HEREGEX = /^\/{3}([\s\S]+?)\/{3}([a-z]{0,8})(?!\w)/;

var HEREGEX_OMIT = /\s+(?:#.*)?/g;

// Token cleaning regexes.
var MULTILINER = /\n/g;

var HEREDOC_INDENT = /\n+([^\n\S]*)/g;

var HEREDOC_ILLEGAL = /\*\//;

// expensive?
var LINE_CONTINUER = /^\s*(?:,|\??\.(?![.\d])|(?:&&|\|\||and|or)[\n\s])/;

var TRAILING_SPACES = /\s+$/;

var ENV_FLAG = /^\$\w+\$/;

var ARGVAR = /^\$\d$/;

// Compound assignment tokens.
var COMPOUND_ASSIGN = [
	'-=','+=','/=','*=','%=','||=','&&=','?=','??=',
	'<<=','>>=','>>>=','&=','^=','|=','~=','=<','**=',
	'=?','~=?','|=?','&=?','^=?'
];
var COMPOUND_ASSIGN_MAP = map$(COMPOUND_ASSIGN);

// Unary tokens.
var UNARY = ['!','~','NEW','TYPEOF','DELETE'];
var UNARY_MAP = map$(UNARY);

// Logical tokens.
var LOGIC = ['&&','||','??','and','or'];
var LOGIC_MAP = map$(LOGIC);

// Bit-shifting tokens.
var SHIFT = ['<<','>>','>>>'];
var SHIFT_MAP = map$(SHIFT);

// Comparison tokens.
var COMPARE = ['===','!==','==','!=','<','>','<=','>=','===','!==','&','|','^','!&'];
var COMPARE_MAP = map$(COMPARE);

// Mathematical tokens.
var MATH = ['*','/','%','∪','∩','√'];
var MATH_MAP = map$(MATH);

// Relational tokens that are negatable with `not` prefix.
var RELATION = ['IN','OF','INSTANCEOF','ISA'];
var RELATION_MAP = map$(RELATION);

// Boolean tokens.
var BOOL = ['TRUE','FALSE','NULL','UNDEFINED'];

// Our list is shorter, due to sans-parentheses method calls.
var NOT_REGEX = ['NUMBER','REGEX','BOOL','TRUE','FALSE','++','--',']'];
var NOT_REGEX_MAP = map$(NOT_REGEX);

// If the previous token is not spaced, there are more preceding tokens that
// force a division parse:
var NOT_SPACED_REGEX = ['NUMBER','REGEX','BOOL','TRUE','FALSE','++','--',']',')','}','THIS','SELF','IDENTIFIER','STRING'];
var NOT_SPACED_REGEX_MAP = map$(NOT_SPACED_REGEX);

// Tokens which could legitimately be invoked or indexed. An opening
// parentheses or bracket following these tokens will be recorded as the start
// of a function invocation or indexing operation.
// really?!

var UNFINISHED = ['\\','.','UNARY','MATH','EXP','+','-','SHIFT','RELATION','COMPARE','THROW','EXTENDS'];
var UNFINISHED_MAP = map$(UNFINISHED);

// } should not be callable anymore!!! '}', '::',
var CALLABLE = ['IDENTIFIER','SYMBOLID','STRING','REGEX',')',']','INDEX_END','THIS','SUPER','TAG_END','IVAR','SELF','NEW','ARGVAR','SYMBOL','RETURN','INDEX_END','CALL_END','DECORATOR','@','GENERICS'];
var CALLABLE_MAP = map$(CALLABLE);

// optimize for FixedArray
var INDEXABLE = [
	'IDENTIFIER','SYMBOLID','STRING','REGEX',')',']','THIS','SUPER','TAG_END','IVAR','SELF','NEW','ARGVAR','SYMBOL','RETURN','BANG',
	'NUMBER','BOOL','TAG_SELECTOR','ARGUMENTS','}','TAG_TYPE','TAG_REF','INDEX_END','CALL_END','DO_VALUE'
];
var INDEXABLE_MAP = map$(INDEXABLE);

var NOT_KEY_AFTER = ['.','?','?.','UNARY','??','+','-','*'];

var GLOBAL_IDENTIFIERS = ['global','exports'];

// Tokens that, when immediately preceding a `WHEN`, indicate that the `WHEN`
// occurs at the start of a line. We disambiguate these from trailing whens to
// avoid an ambiguity in the grammar.
var LINE_BREAK = ['INDENT','OUTDENT','TERMINATOR'];
var LINE_BREAK_MAP = map$(LINE_BREAK);

var DECLARE_START_MAP = map$(['INDENT','TERMINATOR','DECORATOR']);
var TAG_DECLARATION_START_MAP = map$(['INDENT','OUTDENT','TERMINATOR','EXPORT','DEFAULT','DECLARE','GLOBAL','LOCAL','EXTEND','ABSTRACT','STRICT','DECORATOR']);
var TAG_DECLARATION_PREFIX_MAP = map$(['global','declare','local','extend','abstract','strict']);
var PROPERTY_ACCESS_PREV_MAP = map$(['IDENTIFIER',')','}',']','NUMBER']);
var SYMBOL_STRING_PREV_MAP = map$(['(','[','=']);
var SPLAT_PREV_VALUE_MAP = map$([',','(','[','{','|','\n','\t']);
var AMPER_REF_TIGHT_TOKENS = map$(['COMPARE','.','(','[']);

function LexerError(message,file,line){
	this.message = message;
	this.file = file;
	this.line = line;
	return this;
};
subclass$(LexerError,SyntaxError);



var last = function(array,back) {
	if(back === undefined) back = 0;
	return array[array.length - back - 1];
};

var countLineBreaks = function(str) {
	var count = 0;
	for (let i = 0, len = str.length; i < len; i++) {
		if (str.charCodeAt(i) == 10) count++;
	};
	return count;
};

var repeatString = function(str,times) {
	var res = '';
	while (times > 0){
		if (times % 2 == 1) {
			res += str;
		};
		str += str;
		times >>= 1;
	};
	return res;
};

var tT = T.typ;
var tV = T.val;
var tTs = T.setTyp;
var tVs = T.setVal;

// The Lexer class reads a stream of Imba and divvies it up into tokidged
// tokens. Some potential ambiguity in the grammar has been avoided by
// pushing some extra smarts into the Lexer.

// Based on the original lexer.coffee from CoffeeScript
function Lexer(){
	this.reset();
	this;
};


Lexer.prototype.reset = function (){
	this._code = null;
	this._chunk = null; // The remainder of the source code.
	this._opts = null;
	this._state = {};
	
	this._indent = 0; // The current indentation level.
	this._indebt = 0; // The over-indentation at the current level.
	this._outdebt = 0; // The under-outdentation at the current level.
	
	this._indents = []; // The stack of all current indentation levels.
	this._ends = []; // The stack for pairing up tokens.
	this._contexts = []; // suplements @ends
	this._scopes = [];
	this._nextScope = null; // the scope to add on the next indent
	this._context = null;
	// should rather make it like a statemachine that moves from CLASS_DEF to CLASS_BODY etc
	// Things should compile differently when you are in a CLASS_BODY than when in a DEF_BODY++
	
	this._indentStyle = '\t';
	this._inTag = false;
	this._inStyle = 0;
	
	this._tokens = []; // Stream of parsed tokens in the form `['TYPE', value, line]`.
	this._seenFor = false;
	this._seenLoop = false;
	this._loopEnds = 0;
	this._loc = 0;
	this._locOffset = 0;
	
	this._end = null;
	this._char = null;
	this._bridge = null;
	
	this._last = null;
	this._lastTyp = '';
	this._lastVal = null;
	this._script = null;
	return this;
};

Lexer.prototype.jisonBridge = function (jison){
	return this._bridge = {
		lex: T.lex,
		setInput: function(tokens) {
			this.tokens = tokens;
			return this.pos = 0;
		},
		
		upcomingInput: function() { return ""; }
	};
};

Lexer.prototype.tokenize = function (code,o,script){
	
	var m;
	if(script === undefined) script = null;
	if (code.length == 0) {
		return [];
	};
	
	if (!o.inline) {
		if (WHITESPACE.test(code)) {
			code = ("\n" + code);
			if (code.match(/^\s*$/g)) { return [] };
		};
		
		if (code.indexOf('\r') >= 0) {
			code = code.replace(/\r/g,'');
		};
		let lastCode = code.charCodeAt(code.length - 1);
		if (lastCode == 9 || lastCode == 32) {
			code = code.replace(/[\t ]+$/g,'');
		};
	};
	
	this._last = null;
	this._lastTyp = null;
	this._lastVal = null;
	
	this._script = script;
	this._code = code;
	this._opts = o;
	this._locOffset = o.loc || 0;
	this._platform = o.platform || o.target;
	this._indentStyle = '\t';
	
	// if the very first line is indented, take this as a gutter
	if (m = code.match(/^([\ \t]*)[^\n\s\t]/)) {
		this._state.gutter = m[1];
	};
	
	if (o.gutter !== undefined) {
		this._state.gutter = o.gutter;
	};
	
	if (this._script && !o.inline) {
		this._script.tokens = this._tokens;
	};
	
	this.parse(code);
	if (!o.inline) this.closeIndentation();
	
	
	
	if (this._ends.length) {
		this.unclosedError("end of file");
	};
	
	if (this._platform == 'tsc') {
		for (let i = 0, items = iter$(this._tokens), len = items.length, token; i < len; i++) {
			token = items[i];
			if (token._type == 'SYMBOLID') {
				token._type = 'IDENTIFIER';
				// token.@value = token.@value.replace(/#/g,'_$SYM$_')
			};
		};
	};
	
	return this._tokens;
};

Lexer.prototype.parse = function (code){
	var i = 0;
	var pi = 0;
	
	this._loc = this._locOffset + i;
	
	while (this._chunk = code.slice(i)){
		let ctx = this._context;
		if (ctx && ctx.pop) {
			if (ctx.pop.test(this._chunk)) {
				this.popEnd();
			};
		};
		
		// we should let the current context decide which methods to call
		pi = (ctx && ctx.lexer && ctx.lexer.call(this)) || (this._end == 'TAG' && this.tagDefContextToken()) || (this._inTag && this.tagContextToken()) || (this._inStyle2 && this.lexStyleBody()) || this.basicContext();
		i += pi;
		this._loc = this._locOffset + i;
	};
	
	return;
};

Lexer.prototype.basicContext = function (){
	var chr = this._chunk.charAt(0);

	if (this._end == '%') {
		return this.selectorToken() || this.symbolToken() || this.identifierToken() || this.whitespaceToken() || this.lineToken() || this.commentToken() || this.heredocToken() || this.tagToken() || this.stringToken() || this.numberToken() || this.regexToken() || this.literalToken() || 0;
	};

	switch (chr) {
		case ' ':
		case '\t': {
			return this.whitespaceToken() || this.commentToken() || this.literalToken() || 0;
		}
		case '\n': {
			return this.whitespaceToken() || this.lineToken() || this.commentToken() || this.literalToken() || 0;
		}
		case '$':
		case '%': {
			return this.selectorToken() || this.identifierToken() || this.literalToken() || 0;
		}
		case '#': {
			return this.identifierToken() || this.commentToken() || this.literalToken() || 0;
		}
		case '@': {
			return this.identifierToken() || this.literalToken() || 0;
		}
		case ':': {
			return this.symbolToken() || this.literalToken() || 0;
		}
		case '"':
		case "'": {
			return this.heredocToken() || this.stringToken() || this.literalToken() || 0;
		}
		case '`': {
			return this.stringToken() || this.literalToken() || 0;
		}
		case '/': {
			return this.commentToken() || this.regexToken() || this.literalToken() || 0;
		}
		case '<': {
			return this.tagToken() || this.literalToken() || 0;
		}
		case '.':
		case '-': {
			return this.numberToken() || this.literalToken() || 0;
		}
	};

	var code = chr.charCodeAt(0);
	if ((code > 127 || code == 11 || code == 12 || code == 13) && WHITESPACE.test(this._chunk)) {
		return this.whitespaceToken() || this.commentToken() || this.literalToken() || 0;
	};

	if ((code >= 65 && code <= 90) || (code >= 97 && code <= 122) || chr == '_') {
		return this.identifierToken() || this.literalToken() || 0;
	};

	if (code >= 48 && code <= 57) {
		return this.numberToken() || this.literalToken() || 0;
	};

	return this.literalToken() || 0;
};

Lexer.prototype.prevChars = function (n){
	if(n === undefined) n = 1;
	return (n == 1) ? this._code[this._loc - 1] : this._code.slice(this._loc - n,this._loc);
};

Lexer.prototype.moveCaret = function (i){
	return this._loc += i;
};

Lexer.prototype.context = function (){
	return this._ends[this._ends.length - 1];
};

Lexer.prototype.inContext = function (key){
	var o = this._contexts[this._contexts.length - 1];
	return o && o[key];
};

Lexer.prototype.pushEnd = function (val,ctx){
	let prev = this._context;
	this._ends.push(val);
	this._contexts.push(this._context = (ctx || {}));
	this._end = val;
	this.refreshScope();
	
	if (ctx && (ctx.closeType == 'STYLE_END' || ctx.style)) {
		ctx.lexer = this.lexStyleBody;
		ctx.style = true;
		this._inStyle++;
	};
	
	if (prev && prev.style && val != '}') {
		ctx.lexer = this.lexStyleBody;
		ctx.style = true;
	};
	
	if (ctx && ctx.id) {
		ctx.start = new Token(ctx.id + '_START',val,this._last.region()[1],0);
		this._tokens.push(ctx.start);
	};
	return this;
};

Lexer.prototype.popEnd = function (val){
	var popped = this._ends.pop();
	this._end = this._ends[this._ends.length - 1];
	
	// automatically adding a closer if this is defined
	var ctx = this._context;
	if (ctx && ctx.start) {
		ctx.end = new Token(ctx.closeType || ctx.id + '_END',popped,this._last.region()[1],0);
		ctx.end._start = ctx.start;
		ctx.start._end = ctx.end;
		this._tokens.push(ctx.end);
	};
	
	if (ctx && (ctx.closeType == 'STYLE_END' || ctx.style)) {
		this._inStyle--;
	};
	
	this._contexts.pop();
	this._context = this._contexts[this._contexts.length - 1];
	
	this.refreshScope();
	return [popped,ctx];
};

Lexer.prototype.refreshScope = function (){
	var ctx0 = this._ends[this._ends.length - 1];
	var ctx1 = this._ends[this._ends.length - 2];
	return this._inTag = ctx0 == 'TAG_END' || (ctx1 == 'TAG_END' && ctx0 == 'OUTDENT');
};

Lexer.prototype.queueScope = function (val){
	this._scopes[this._indents.length] = val;
	return this;
};

Lexer.prototype.popScope = function (val){
	this._scopes.pop();
	return this;
};

Lexer.prototype.getScope = function (){
	return this._scopes[this._indents.length - 1];
};

Lexer.prototype.scope = function (sym,opts){
	var len = this._ends.push(this._end = sym);
	this._contexts.push(opts || null);
	return sym;
};

Lexer.prototype.closeSelector = function (){
	if (this._end == '%') {
		this.token('SELECTOR_END','%',0);
		return this.pair('%');
	};
};

Lexer.prototype.openDef = function (){
	return this.pushEnd('DEF');
};

Lexer.prototype.closeDef = function (){
	if (this.context() == 'DEF') {
		var prev = last(this._tokens);
		
		if (tT(prev) == 'TERMINATOR') {
			let n = this._tokens.pop();
			this.token('DEF_BODY','DEF_BODY',0);
			this._tokens.push(n);
		} else {
			this.token('DEF_BODY','DEF_BODY',0);
		};
		
		this.pair('DEF');
	};
	return;
};

Lexer.prototype.tagContextToken = function (){
	let chr = this._chunk[0];
	let chr2 = this._chunk[1];
	
	// let m = TAG_PART.exec(@chunk)
	let m = /^([A-Za-z\_\-\$\%\#][\w\-\$]*(\:[A-Za-z\_\-\$]+)*)/.exec(this._chunk); // (\:[A-Za-z\_][\w\-]*)
	
	if (m) { // and false
		let tok = m[1];
		let typ = 'TAG_LITERAL';
		let len = m[0].length;
		
		if (tok == 'self' && this._lastVal == '<') {
			typ = 'SELF';
		};
		
		if (chr == '$' && (this._lastTyp == 'TAG_TYPE' || this._lastTyp == 'TAG_START')) {
			typ = 'TAG_REF';
		};
		
		if (chr == '%') {
			typ = 'CSS_MIXIN';
		};
		
		if (chr == '#') {
			typ = 'TAG_SYMBOL_ID';
			if (tok.length == 1) {
				return 0;
			};
		};
		
		this.token(typ,tok,len);
		return len;
	};
	
	if (chr == '/' && chr2 == '>') {
		this.token("TAG_END",'/>',2);
		this.pair('TAG_END');
		return 2;
	};
	
	if (chr == '%' || chr == ':' || chr == '.' || chr == '@') {
		this.token(("T" + chr),chr,1);
		
		if (chr == '.' && (!chr2 || TSC_CARET_BEFORE[chr2]) && this._platform == 'tsc') {
			this.token('TAG_LITERAL','$CARET$',0,1);
		};
		return 1;
	} else if (chr == ' ' || chr == '\n' || chr == '\t') {
		// add whitespace inside tag
		let m = /^[\n\s\t]+/.exec(this._chunk);
		this.token('TAG_WS',m[0],m[0].length);
		return m[0].length;
	} else if (chr == '=' && this._chunk[1] != '>') {
		this.token('=','=',1);
		this.pushEnd('TAG_ATTR',{id: 'VALUE',pop: /^([\s\n\>]|\/\>)/});
		return 1;
	};
	return 0;
};

Lexer.prototype.tagDefContextToken = function (){
	// console.log "tagContextToken"
	var match;
	if (match = TAG_TYPE.exec(this._chunk)) {
		this.token('TAG_TYPE',match[0],match[0].length);
		return match[0].length;
	};
	
	if (match = TAG_ID.exec(this._chunk)) {
		var input = match[0];
		this.token('TAG_ID',input,input.length);
		return input.length;
	};
	
	if (this._chunk[0] == '\n') {
		this.pair('TAG');
	};
	
	return 0;
};

Lexer.prototype.findTypeAnnotation = function (str,autoend){
	if(autoend === undefined) autoend = false;
	var stack = [];
	var i = 0;
	var replaces = [];
	var ending = /[\=\n\ \t\.\,\:\+]/;
	// could it not happen here?
	while (i < str.length){
		var chr = str.charAt(i);
		let end = stack[0];
		let instr = end == '"' || end == "'";
		
		if (chr && chr == end) {
			stack.shift();
			if (autoend && stack.length == 0) {
				i++;break;
			};
		} else if (!end && (chr == ')' || chr == ']' || chr == '}' || chr == '>')) {
			break;
		} else if (chr == '(') {
			stack.unshift(')');
		} else if (chr == '[') {
			stack.unshift(']');
		} else if (chr == '{') {
			stack.unshift('}');
		} else if (chr == '<') {
			stack.unshift('>');
		} else if (chr == '"') {
			stack.unshift('"');
		} else if (chr == "'") {
			stack.unshift("'");
		} else if (!end && ending.test(chr)) {
			break;
		};
		i++;
	};
	
	if (i == 0) { return null };
	return str.slice(0,i);
};

Lexer.prototype.findBalancedSelector = function (str){
	var stack = [];
	var i = 0;
	var replaces = [];
	// could it not happen here?
	while (i < (str.length - 1)){
		var letter = str.charAt(i);
		
		let end = stack[0];
		let instr = end == '"' || end == "'";
		
		if (letter && letter == end) {
			stack.shift();
		} else if (!instr && (letter == ')' || letter == ']' || letter == '}')) {
			console.log('out of balance!!');
			break;
		} else if (letter == '/') {
			replaces.unshift([i,1,':']);
		} else if (letter == '(' && !instr) {
			stack.unshift(')');
		} else if (letter == '[' && !instr) {
			stack.unshift(']');
		} else if (letter == '"') {
			stack.unshift('"');
		} else if (letter == "'") {
			stack.unshift("'");
		};
		
		if (!end && (letter == '=' || letter == '\n' || letter == '{')) {
			break;
		};
		
		if (!end && letter == ' ') {
			if (stylePropertyLengthAt(str,i + 1)) {
				break;
			};
			
			let after = str.slice(i + 1);
			if (INLINE_COMMENT.exec(after)) {
				break;
			};
		};
		
		// console.log 'try',letter,i,end,str.substr(i,5)
		i++;
	};
	
	if (i == 0) { return null };
	
	let sel = str.slice(0,i);
	if (replaces.length) {
		sel = sel.split('');
		for (let j = 0, len = replaces.length; j < len; j++) {
			sel.splice.apply(sel,replaces[j]);
		};
		sel = sel.join('');
	};
	
	return sel;
};

Lexer.prototype.lexStyleRule = function (offset,force){
	// when we meet = enter into style context?
	if(offset === undefined) offset = 0;
	if(force === undefined) force = false;
	let chunk = offset ? this._chunk.slice(offset) : this._chunk;
	let sel = this.findBalancedSelector(chunk);
	
	if (sel || force) {
		let len = sel ? sel.length : 0;
		this.token('CSS_SEL',sel || '',len,offset);
		let seltoken = this._last;
		let next = chunk[len];
		
		if (next == '=') {
			len++;
		};
		
		// if @context
		//	@context:lexer = null
		
		this._indents.push(1);
		this._outdebt = this._indebt = 0;
		this.token('INDENT',"1",0,1);
		this.pushEnd('OUTDENT',{lexer: this.lexStyleBody,opener: seltoken,style: true});
		this._indent++;
		return len;
	};
	
	return 0;
};

Lexer.prototype.lexStyleBody = function (){
	if (this._end == '%') { return 0 };
	// return 0
	let chr = this._chunk[0];
	var m;
	
	let styleprop = stylePropertyLengthAt(this._chunk);
	let ltyp = this._lastTyp;
	
	if (!styleprop && (ltyp == 'TERMINATOR' || ltyp == 'INDENT') && canStartStyleSelector(this._chunk,chr)) {
		let sel = this.findBalancedSelector(this._chunk);
		if (sel) { return this.lexStyleRule(0) };
	};
	
	if (styleprop) {
		// what is the last one?
		this.token('CSSPROP',this._chunk.slice(0,styleprop),styleprop);
		return styleprop;
	};
	
	if (chr[0] == '#' && (m = STYLE_HEX.exec(this._chunk))) {
		let next = this._chunk[m[0].length];
		let typ = (next == '(') ? 'COLORMIX' : 'COLOR';
		this.token(typ,m[0],m[0].length);
		return m[0].length;
	};
	
	if (chr == '/' && !this._last.spaced) {
		// console.log '!!!'
		this.token('/',chr,1);
		return 1;
	};
	
	if (m = STYLE_NUMERIC.exec(this._chunk)) {
		let len = m[0].length;
		let typ = 'NUMBER';
		
		if (m[2] == '%') {
			typ = 'PERCENTAGE';
			// if @chunk[len]
		} else if (m[2]) {
			typ = 'DIMENSION';
		};
		
		if (this._lastTyp == 'COMPARE' && !this._last.spaced) {
			true;
		};
		this.token(typ,m[0],len);
		return len;
	} else if (m = STYLE_URL.exec(this._chunk)) {
		// console.log 'matching style url',m
		let len = m[0].length;
		this.token('CSSURL',m[0],len);
		return m[0].length;
	} else if (m = STYLE_IDENTIFIER.exec(this._chunk)) {
		let id = 'CSSIDENTIFIER';
		let val = m[0];
		let len = val.length;
		if (val[0] == '-' && val[1] == '-') {
			id = 'CSSVAR';
		} else if (this._last && !this._last.spaced && (ltyp == '}' || ltyp == ')')) {
			id = 'CSSUNIT';
		};
		
		if (this._chunk[len] == '(') {
			id = 'CSSFUNCTION';
		};
		
		this.token(id,val,len);
		
		return len;
	} else if (this._last && !this._last.spaced && (ltyp == '}' || ltyp == ')') && chr == '%') {
		this.token('CSSUNIT',chr,1);
		return 1;
	};
	
	return 0;
};

Lexer.prototype.importsToken = function (){
	var match;
	if (match = IMPORTS.exec(this._chunk)) {
		this.token('IMPORTS',match[1],match[1].length,7);
		return match[0].length;
	};
	return 0;
};

Lexer.prototype.tagToken = function (){
	var match, ary;
	if (!(match = TAG.exec(this._chunk))) { return 0 };
	var ary = iter$(match);var input = ary[0],type = ary[1],identifier = ary[2];
	
	
	if (type == '<') {
		if (TYPE_GENERICS_AFTER.test(this.prevChars(1) || '')) {
			return 0;
		};
		// if @last and !@last:spaced and @lastTyp != 'TERMINATOR' and @lastTyp != 'INDENT'
		//	return 0
		
		this.token('TAG_START','<',1);
		this.pushEnd(INVERSES.TAG_START,{i: this._tokens.length - 1});
		
		if (match = TAG_TYPE.exec(this._chunk.substr(1,40))) {
			let next = this._chunk[match[0].length + 1];
			
			if (match[0] != 'self' && (next != '{' && next != '-')) {
				this.token('TAG_TYPE',match[0],match[0].length,1);
				return input.length + match[0].length;
			};
		} else if (this._chunk[1] == '>') {
			this.token('TAG_TYPE','fragment',0,0);
		};
		
		if (identifier) {
			if (identifier.substr(0,1) == '{') {
				return type.length;
			} else {
				this.token('TAG_NAME',input.substr(1),0);
			};
		};
	};
	
	return input.length;
};

Lexer.prototype.selectorToken = function (){
	var ary;
	var match;
	
	// special handling if we are in this context
	if (this._end == '%') {
		var chr = this._chunk[0];
		var ctx = this._context;
		
		var i = 0;
		var part = '';
		var ending = false;
		
		while (chr = this._chunk[i++]){
			if (chr == ')' && ctx.parens == 0) {
				ending = true;
				break;
			} else if (chr == '(') {
				ctx.parens++;
				part += '(';
			} else if (chr == ')') {
				ctx.parens--;
				part += ')';
			} else if (chr == '{') {
				break;
			} else {
				part += chr;
			};
		};
		
		if (part) {
			this.token('SELECTOR_PART',part,i - 1);
		};
		if (ending) {
			this.token('SELECTOR_END',')',1,i - 1);
			this.pair('%');
			return i;
		};
		return i - 1;
	};
	
	if (!(match = SELECTOR.exec(this._chunk))) { return 0 };
	
	var ary = iter$(match);var input = ary[0],id = ary[1],kind = ary[2];
	
	// this is a closed selector
	if (kind == '(') {
		// token '(','('
		this.token('SELECTOR_START',id,id.length + 1);
		this.pushEnd('%',{parens: 0});
		return id.length + 1;
	} else if (id == '%') {
		// we are already scoped in on a selector
		if (this.context() == '%') { return 1 };
		this.token('SELECTOR_START',id,id.length);
		// this is a separate - scope. Full selector should rather be $, and keep the single selector as %
		
		this.pushEnd('%',{open: true});
		// @ends.push '%'
		// make sure a terminator breaks out
		return id.length;
	} else {
		return 0;
	};
};

Lexer.prototype.inTag = function (){
	var len = this._ends.length;
	if (len > 0) {
		var ctx0 = this._ends[len - 1];
		var ctx1 = (len > 1) ? this._ends[len - 2] : ctx0;
		return ctx0 == 'TAG_END' || (ctx1 == 'TAG_END' && ctx0 == 'OUTDENT');
	};
	return false;
};

Lexer.prototype.isKeyword = function (id,next){
	var m;
	if(next === undefined) next = '';
	if (id == 'tag') {
		return this.isTagDeclarationKeyword();
	};

	if (id == 'mixin' && (!next || next == ' ')) {
		if (MEMBER_KEYWORDS[this._lastTyp]) { return false };
		return true;
	};
	
	if (this._lastTyp == 'ATTR' || this._lastTyp == 'PROP' || this._lastTyp == 'DEF') {
		return false;
	};
	
	// `when`/`by` are only keywords as guard/step after for/while/until on
	// the same line, or - for `when` - as a case-marker at the start of a
	// line directly inside a switch block. Elsewhere they are plain identifiers
	if (id == 'when' || id == 'by') {
		if (!this._lastTyp || LINE_BREAK_MAP[this._lastTyp] == 1) {
			return id == 'when' && this.getScope() == 'SWITCH';
		};
		return this._seenLoop;
	};
	
	if (id == 'get' || id == 'set') {
		if (m = this._chunk.match(/^[gs]et ([\$\w\-]+|\[)/)) { // ( (do)|\n(\t+))
			let ctx = this._contexts[this._contexts.length - 1] || {};
			let before = ctx.opener && this._tokens[this._tokens.indexOf(ctx.opener) - 1];
			
			if (this._lastTyp == 'TERMINATOR' || this._lastTyp == 'INDENT') {
				if (before && (before._type == '=' || before._type == '{')) {
					return true;
				};
			};
		};
	};
	
	if ((id == 'guard' || id == 'alter' || id == 'watch') && (this.getScope() == 'PROP')) {
		// TODO Remove
		return true;
	};
	
	if (id == 'css') {
		// experimental css inside tag trees - making css keyword everywhere
		return true;
		
		if ((this._lastTyp == 'TERMINATOR' || !this._lastTyp)) {
			return true;
		};
		
		if ((this._lastVal == 'global' || this._lastVal == 'local' || this._lastVal == 'export' || this._lastVal == 'default')) {
			return true;
		};
		
		if ((this._lastTyp == '=')) {
			return true;
		};
	};
	
	if (id == 'interface') {
		if ((this._lastVal == 'global' || this._lastVal == 'export' || this._lastVal == 'default' || this._lastVal == 'declare')) {
			return true;
		};
	};
	
	if ((id == 'attr' || id == 'prop' || id == 'get' || id == 'set' || id == 'css' || id == 'constructor' || id == 'declare')) {
		
		var scop = this.getScope();
		var incls = scop == 'CLASS' || scop == 'TAG' || scop == 'EXTEND';
		
		// if id == 'css' and !@context and (@lastTyp in ['TERMINATOR'] or !@lastTyp)
		// 	return true
		
		// if id == 'css' and (@lastVal in ['global','local','export'])
		// 	return true
		
		if (id == 'declare') {
			return incls && DECLARE_START_MAP[this._lastTyp] == 1;
		};
		
		if (id == 'constructor') {
			return incls && DECLARE_START_MAP[this._lastTyp] == 1;
		};
		
		if (incls) { return true };
	};
	
	return ALL_KEYWORDS_MAP[id] == 1;
};

Lexer.prototype.isTagDeclarationKeyword = function (){
	var ltyp = this._lastTyp;
	var lval = this._lastVal;
	var atDeclarationStart = !ltyp || TAG_DECLARATION_START_MAP[ltyp] == 1 || (ltyp == 'IDENTIFIER' && TAG_DECLARATION_PREFIX_MAP[lval] == 1);
	return atDeclarationStart && /^tag[^\n\S]+(\w[\w\d]*:)?(\w[\w\d]*)(-[\w\d]+)*/.test(this._chunk);
};

// Matches identifying literals: variables, keywords, method names, etc.
// Check to ensure that JavaScript reserved words aren't being used as
// identifiers. Because Imba reserves a handful of keywords that are
// allowed in JavaScript, we're careful not to tokid them as keywords when
// referenced as property names here, so you can still do `jQuery.is()` even
// though `is` means `===` otherwise.
Lexer.prototype.identifierToken = function (){
	var ary;
	var match;
	var typ;
	
	if (!(match = IDENTIFIER.exec(this._chunk))) {
		return 0;
	};
	
	var ary = iter$(match);var input = ary[0],id = ary[1],typ = ary[2],m3 = ary[3],m4 = ary[4],colon = ary[5];
	var idlen = id.length;
	
	// What is the logic here?
	if (id === 'own' && this.lastTokenType() == 'FOR') {
		this.token('OWN',id,id.length);
		return id.length;
	};
	
	var prev = last(this._tokens);
	var lastTyp = this._lastTyp;
	
	if (lastTyp == '#') {
		this.token('IDENTIFIER',id,idlen);
		return idlen;
	};
	
	var forcedIdentifier = colon || lastTyp == '.' || lastTyp == '?.';
	
	if (colon && lastTyp == '?') { forcedIdentifier = false }; // for ternary
	
	if (!typ && !colon && (lastTyp == '.' || lastTyp == '?.')) {
		this.token('IDENTIFIER',id,idlen);
		return idlen;
	};

	if (id == 'css' && (/css\s\:\:/).exec(this._chunk)) {
		input = id + ' ';
		colon = null;
		forcedIdentifier = false;
	};
	
	var isKeyword = false;
	
	// little reason to check for this right here? but I guess it is only a simple check
	if (typ == '$' && ARGVAR.test(id)) {
		// console.log "TYP $"
		// if id == '$0'
		// 	typ = 'ARGUMENTS'
		// else
		typ = 'ARGVAR';
		id = id.substr(1);
	} else if (typ == '$' && ENV_FLAG.test(id)) {
		typ = 'ENV_FLAG';
		id = id.toUpperCase(); // .slice(1, -1)
	} else if (typ == '@') {
		if (lastTyp == '.') {
			typ = 'IDENTIFIER';
		} else {
			typ = 'DECORATOR';
		};
	} else if (typ == '#') {
		typ = 'SYMBOLID';
	} else if (typ == '##') {
		typ = 'SYMBOLID';
	} else if (typ == '%') {
		// use for something else
		let ltyp = this._lastTyp;
		if (ltyp == 'TERMINATOR' || ltyp == 'INDENT' || ltyp == 'EXPORT') {
			this.token('CSS',id,0);
			this.queueScope('CSS');
			return this.lexStyleRule(0,true);
		};
		typ = 'CSS_MIXIN';
	} else if (typ == '$' && !colon) {
		typ = 'IDENTIFIER';
	} else if (id == 'elif' && !forcedIdentifier) {
		this.token('ELSE','elif',id.length);
		this.token('IF','if');
		return id.length;
	} else {
		typ = 'IDENTIFIER';
	};
	
	if (typ == 'IDENTIFIER' && !colon && !KEYWORD_CANDIDATE_MAP[id] && !SPECIAL_IDENTIFIER_MAP[id] && lastTyp != 'CATCH' && !(lastTyp == 'IDENTIFIER' && this._lastVal == 'protected') && !((lastTyp == 'NUMBER' || lastTyp == ')') && prev && !prev.spaced) && this._end != 'IMPORT' && this._end != 'EXPORT' && lastTyp != 'IMPORT') {
		this.token('IDENTIFIER',id,idlen);
		return idlen;
	};

	var ctx0 = this._end;

	// this catches all
	if (!forcedIdentifier && KEYWORD_CANDIDATE_MAP[id] && (isKeyword = this.isKeyword(id,this._chunk[id.length]))) {
		// (id in JS_KEYWORDS or id in IMBA_KEYWORDS)
		
		if (typeof isKeyword == 'string') {
			typ = isKeyword;
		} else {
			typ = id.toUpperCase();
		};
		
		if (typ == 'MODULE') {
			if (!(/^module [a-zA-Z]/).test(this._chunk) || ctx0 == 'TAG_ATTR') {
				typ = 'IDENTIFIER';
			};
		};
		
		// clumsy - but testing performance
		if (typ == 'YES') {
			typ = 'TRUE';
		} else if (typ == 'NO') {
			typ = 'FALSE';
		} else if (typ == 'NIL') {
			typ = 'NULL';
		} else if ((typ == 'MIXIN' || typ == 'INTERFACE')) {
			typ = 'CLASS';
		} else if (typ == 'VAR' || typ == 'CONST' || typ == 'LET') {
			let ltyp = this._lastTyp;
			
			// extremely flakey - comma separated declaration blocks are inherently
			// ambiguous in the current syntax
			// if ltyp != 'TERMINATOR' and ltyp != 'INDENT' and ltyp != 'EXPORT' and ltyp
			//	typ = "INLINE_{typ}"
			
			// if @lastVal == 'export'
			//	tTs(prev,'EXPORT_VAR')
		} else if (typ == 'IF' || typ == 'ELSE' || typ == 'TRUE' || typ == 'FALSE' || typ == 'NULL') {
			true;
		} else if (typ == 'TAG') {
			this.pushEnd('TAG');
		} else if ((typ == 'DEF' || typ == 'GET' || typ == 'SET')) {
			typ = 'DEF';
			this.openDef();
		} else if ((typ == 'CONSTRUCTOR')) {
			this.token('DEF','',0);
			typ = 'IDENTIFIER';
			this.openDef();
		} else if (typ == 'DO') {
			if (this.context() == 'DEF') this.closeDef();
		} else if (typ === 'WHEN' && LINE_BREAK_MAP[this.lastTokenType()] == 1) {
			typ = 'LEADING_WHEN';
		} else if (typ === 'FOR') {
			this._seenFor = true;
			this._seenLoop = true;
			this._loopEnds = this._ends.length;
		} else if (typ === 'WHILE' || typ === 'UNTIL') {
			this._seenLoop = true;
			this._loopEnds = this._ends.length;
		} else if (typ === 'UNLESS') {
			typ = 'IF'; // WARN
		} else if (UNARY_MAP[typ] == 1) {
			typ = 'UNARY';
		} else if (RELATION_MAP[typ] == 1) {
			if (typ != 'INSTANCEOF' && typ != 'ISA' && this._seenFor) {
				typ = 'FOR' + typ; // ?
				this._seenFor = false;
			} else {
				typ = 'RELATION';
				
				if (prev._type == 'UNARY') {
					prev._type = 'NOT';
				};
			};
		};
	};
	
	// do we really want to check this here
	if (!forcedIdentifier) {
		// should already have dealt with this
		
		if (this._lastVal == 'export' && id == 'default') {
			// console.log 'id is default!!!'
			tTs(prev,'EXPORT');
			typ = 'DEFAULT';
		};
		
		// these really should not go here?!?
		switch (id) {
			case '!': 
			case 'not': {
				typ = 'UNARY';break;
			}
			case '==': 
			case '!=': 
			case '===': 
			case '!==': 
			case 'is': 
			case 'isnt': {
				typ = 'COMPARE';break;
			}
			case '&&': 
			case '||': 
			case 'and': 
			case 'or': 
			case '??': {
				typ = 'LOGIC';break;
			}
			case 'super': 
			case 'break': 
			case 'continue': 
			case 'debugger': 
			case 'arguments': {
				typ = id.toUpperCase();break;
			}
		};
	};
	
	// prev = last @tokens
	var len = input.length;
	
	// if typ == 'CONST' or typ == 'LET'
	//	if @lastVal == 'global' or @lastVal == 'declare'
	//		tTs(prev,@lastVal.toUpperCase)
	
	
	// should be strict about the order, check this manually instead
	if (typ == 'CLASS' || typ == 'DEF' || typ == 'TAG' || typ == "PROP" || typ == 'CSS' || typ == 'SWITCH') {
		this.queueScope(typ);
	};
	
	if (isKeyword && CONTEXTUAL_KEYWORDS[typ]) {
		var i = this._tokens.length;
		var alts = CONTEXTUAL_KEYWORDS[typ];
		
		while (i){
			prev = this._tokens[--i];
			var ctrl = "" + tV(prev);
			if (alts[ctrl]) {
				tTs(prev,ctrl.toUpperCase());
			} else {
				break;
			};
		};
	} else if (typ == 'IF') {
		this.queueScope(typ);
	} else if (typ == 'EXTEND' && !this._chunk.match(/^extend (class|tag|interface|mixin)(\s|\n|$)/)) {
		this.queueScope(typ);
	} else if (typ == 'IMPORT') {
		// console.log 'import last type',lastTyp,@chunk[idlen]
		let next = this._chunk[idlen];
		if (lastTyp == 'AWAIT' || next == '(' || next == '.') {
			typ = 'IDENTIFIER';
		} else {
			this.pushEnd('IMPORT');
			this.token(typ,id,idlen);
			// return importsToken or len
			return len;
		};
	} else if (id == 'type' && lastTyp == 'IMPORT') {
		this.token('TYPEIMPORT',id,idlen);
		// return importsToken or len
		return len;
	} else if (typ == 'EXPORT') {
		this.pushEnd('EXPORT');
		this.token(typ,id,idlen);
		return len;
	} else if (id == 'from' && ctx0 == 'IMPORT') {
		typ = 'FROM';
		this.pair('IMPORT');
	} else if (id == 'from' && ctx0 == 'EXPORT') {
		typ = 'FROM';
		this.pair('EXPORT');
	} else if (id == 'as' && (ctx0 == 'IMPORT' || this._lastTyp == 'IDENTIFIER' || ctx0 == 'EXPORT')) {
		typ = 'AS';
	};
	
	// if id == 'new'
	//	console.log 'new keyword', @chunk.slice(0,5),@chunk.match(/^new\s+[\w\$\(]/)
	
	if (id == 'new' && (this._lastTyp != '.' && this._chunk.match(/^new\s+[\w\$\(\<\#]/)) && this._lastTyp != 'DEF') {
		typ = 'NEW';
	};
	
	if (typ == 'IDENTIFIER') {
		// see if previous was catch -- belongs in rewriter?
		if (lastTyp == 'CATCH') {
			typ = 'CATCH_VAR';
		};
		
		if (this._lastVal == 'protected' && lastTyp == 'IDENTIFIER') {
			tTs(prev,'PROTECTED');
		};
	};
	
	if ((lastTyp == 'NUMBER' || lastTyp == ')') && !prev.spaced && (typ == 'IDENTIFIER' || id == '%')) {
		typ = 'UNIT';
	};
	
	if (colon) {
		this.token(typ,id,idlen);
		var colonOffset = colon.indexOf(':');
		
		this.moveCaret(idlen + colonOffset);
		
		this.token(':',':',1);
		this.moveCaret(-(idlen + colonOffset));
	} else {
		this.token(typ,id,idlen);
	};
	
	if (typ == 'CSS') {
		return len + this.lexStyleRule(len,true);
	};
	
	
	
	return len;
};

// Matches numbers, including decimals, hex, and exponential notation.
// Be careful not to interfere with ranges-in-progress.
Lexer.prototype.numberToken = function (){
	var binaryLiteral;
	var match,number,lexedLength;
	
	if (!(match = NUMBER.exec(this._chunk))) { return 0 };
	
	number = match[0];
	lexedLength = number.length;
	
	if (binaryLiteral = /0b([01_]+)/.exec(number)) {
		
		number = "" + parseInt(binaryLiteral[1].replace(/_/g,''),2);
	};
	
	var prev = last(this._tokens);
	
	if (match[0][0] == '.' && prev && !prev.spaced && PROPERTY_ACCESS_PREV_MAP[tT(prev)] == 1) {
		// console.log "got here"
		this.token(".",".");
		number = number.substr(1);
	};
	
	this.token('NUMBER',number,lexedLength);
	return lexedLength;
};

Lexer.prototype.symbolToken = function (){
	var match,symbol,prev;
	if (!(match = SYMBOL.exec(this._chunk))) { return 0 };
	symbol = match[0];
	prev = last(this._tokens);
	
	if (!prev || prev.spaced || SYMBOL_STRING_PREV_MAP[this._prevVal] == 1) {
		let sym = helpers.dashToCamelCase(symbol.slice(1));
		this.token('STRING','"' + sym + '"',match[0].length);
		return match[0].length;
	};
	return 0;
};

Lexer.prototype.escapeStr = function (str,heredoc,q){
	str = str.replace(MULTILINER,(heredoc ? '\\n' : ''));
	if (q) {
		var r = RegExp(("\\\\[" + q + "]"),"g");
		str = str.replace(r,q);
		str = str.replace(RegExp(("" + q),"g"),'\\$&');
	};
	return str;
	
	// str = str.replace(MULTILINER, '\\n')
	// str = str.replace(/\t/g, '\\t')
};
// Matches strings, including multi-line strings. Ensures that quotation marks
// are balanced within the string's contents, and within nested interpolations.
Lexer.prototype.stringToken = function (){
	var match,string;
	
	switch (this._chunk.charAt(0)) {
		case "'": {
			if (!(match = SIMPLESTR.exec(this._chunk))) { return 0 };
			string = match[0];
			this.token('STRING',this.escapeStr(string),string.length);
			// token 'STRING', (string = match[0]).replace(MULTILINER, '\\\n'), string:length
			break;
		}
		case '"': {
			if (!(string = this.balancedString(this._chunk,'"'))) { return 0 };
			// what about tripe quoted strings?
			
			if (string.indexOf('{') >= 0) {
				var len = string.length;
				// if this has no interpolation?
				// we are now messing with locations - beware
				this.token('STRING_START',string.charAt(0),1);
				this.interpolateString(string.slice(1,-1));
				this.token('STRING_END',string.charAt(len - 1),1,string.length - 1);
			} else {
				len = string.length;
				// string = string.replace(MULTILINER, '\\\n')
				this.token('STRING',this.escapeStr(string),len);
			};
			break;
		}
		case '`': {
			if (!(string = this.balancedString(this._chunk,'`'))) { return 0 };
			
			if (string.indexOf('{') >= 0) {
				len = string.length;
				// if this has no interpolation?
				// we are now messing with locations - beware
				this.token('STRING_START',string.charAt(0),1);
				this.interpolateString(string.slice(1,-1),{heredoc: true});
				this.token('STRING_END',string.charAt(len - 1),1,string.length - 1);
			} else {
				len = string.length;
				// string = string.replace(MULTILINER, '\\\n')
				this.token('STRING',this.escapeStr(string,true),len);
			};
			break;
		}
		default:
		
			return 0;
	
	};
	
	this.moveHead(string);
	return string.length;
};

// Matches heredocs, adjusting indentation to the correct level, as heredocs
// preserve whitespace, but ignore indentation to the left.
Lexer.prototype.heredocToken = function (){
	var match,heredoc,quote,doc;
	
	if (!(match = HEREDOC.exec(this._chunk))) { return 0 };
	
	heredoc = match[0];
	quote = heredoc.charAt(0);
	var opts = {quote: quote,indent: null,offset: 0};
	doc = this.sanitizeHeredoc(match[2],opts);
	// doc = match[2]
	// console.log "found heredoc {match[0]:length} {doc:length}"
	
	if (quote == '"' && doc.indexOf('{') >= 0) {
		var open = match[1];
		// console.log doc.substr(0,3),match[1]
		// console.log 'heredoc here',open:length,open
		
		this.token('STRING_START',open,open.length);
		this.interpolateString(doc,{heredoc: true,offset: (open.length + opts.offset),quote: quote,indent: opts.realIndent});
		this.token('STRING_END',open,open.length,heredoc.length - open.length);
	} else {
		this.token('STRING',this.makeString(doc,quote,true),0);
	};
	
	this.moveHead(heredoc);
	return heredoc.length;
};

Lexer.prototype.parseMagicalOptions = function (str){
	var self = this;
	if (str.indexOf('imba$') >= 0) {
		str.replace(/imba\$(\w+)\=(\S*)\b/g,function(m,name,val) {
			if ((/^\d+$/).test(val)) {
				val = parseInt(val);
			};
			return self._opts[name] = val;
		});
	};
	return self;
};

// Matches and consumes comments.
Lexer.prototype.commentToken = function (){
	var match,length,comment,indent,prev;
	
	var typ = 'HERECOMMENT';
	
	if (match = JS_COMMENT.exec(this._chunk)) {
		this.token('HERECOMMENT',match[1],match[1].length);
		this.token('TERMINATOR','\n');
		return match[0].length;
	};
	
	if (match = INLINE_COMMENT.exec(this._chunk)) { // .match(INLINE_COMMENT)
		// console.log "match inline comment"
		length = match[0].length;
		indent = match[1];
		comment = match[2];
		let commentBody = (match[4] || '');
		if (comment[0] == '#') {
			commentBody = ' ' + commentBody;
		};
		
		prev = last(this._tokens);
		var pt = prev && tT(prev);
		var note = '//' + commentBody; // comment.substr(1)
		
		this.parseMagicalOptions(note);
		
		if (this._last && this._last.spaced) {
			note = ' ' + note;
			// console.log "the previous node was SPACED"
		};
		// console.log "comment {note} - indent({indent}) - {length} {comment:length}"
		if (note.match(/^\/\/ \@(type|param)/)) {
			note = '/**' + commentBody + '*/';
		} else if (note.match(/^\/\/ \<(reference)/)) {
			note = '///' + commentBody;
		};
		
		if ((pt && pt != 'INDENT' && pt != 'TERMINATOR') || !pt) {
			// console.log "skip comment"
			// token 'INLINECOMMENT', comment.substr(2)
			// console.log "adding as terminator"
			this.token('TERMINATOR',note,length); // + '\n'
		} else {
			if (pt == 'TERMINATOR') {
				tVs(prev,tV(prev) + note);
				// prev[1] += note
			} else if (pt == 'INDENT') {
				this.addLinebreaks(1,note);
			} else {
				// console.log "comment here"
				// should we ever get here?
				this.token(typ,comment.substr(2),length); // are we sure?
			};
		};
		
		return length; // disable now while compiling
	};
	
	// should use exec?
	if (!(match = COMMENT.exec(this._chunk))) { return 0 };
	
	comment = match[0];
	var here = match[1];
	
	if (here) {
		this.token('HERECOMMENT',this.sanitizeHeredoc(here,{herecomment: true,indent: Array(this._indent + 1).join(' ')}),comment.length);
		this.token('TERMINATOR','\n');
	} else {
		this.token('HERECOMMENT',comment,comment.length);
		this.token('TERMINATOR','\n'); // auto? really?
	};
	
	this.moveHead(comment);
	return comment.length;
};

// Matches regular expression literals. Lexing regular expressions is difficult
// to distinguish from division, so we borrow some basic heuristics from
// JavaScript and Ruby.
Lexer.prototype.regexToken = function (){
	var ary;
	var match,length,prev;
	
	if (this._chunk.charAt(0) != '/') { return 0 };
	
	if (match = HEREGEX.exec(this._chunk)) {
		length = this.heregexToken(match);
		this.moveHead(match[0]);
		return length;
	};
	
	prev = last(this._tokens);
	// FIX
	if (prev && ((prev.spaced ? 
		NOT_REGEX_MAP
	 : 
		NOT_SPACED_REGEX_MAP
	)[tT(prev)] == 1)) { return 0 };
	if (!(match = REGEX.exec(this._chunk))) { return 0 };
	var ary = iter$(match);var m = ary[0],regex = ary[1],flags = ary[2];
	
	this.token('REGEX',("" + regex + flags),m.length);
	return m.length;
};

// Matches multiline extended regular expressions.
// The escaping should rather happen in AST - possibly as an additional flag?
Lexer.prototype.heregexToken = function (match){
	var ary;
	var ary = iter$(match);var heregex = ary[0],body = ary[1],flags = ary[2];
	this.token('REGEX',heregex,heregex.length);
	return heregex.length;
};

// Matches newlines, indents, and outdents, and determines which is which.
// If we can detect that the current line is continued onto the the next line,
// then the newline is suppressed:
//
//     elements
//       .each( ... )
//       .map( ... )
//
// Keeps track of the level of indentation, because a single outdent token
// can close multiple indents, so we need to know how far in we happen to be.
Lexer.prototype.lineToken = function (){
	var gutter;
	var match;
	
	if (!(match = MULTI_DENT.exec(this._chunk))) { return 0 };
	
	var indent = match[0];
	var brCount = this.moveHead(indent);

	this._seenFor = false;
	// reset column as well?
	var prev = last(this._tokens,1);
	let whitespace = indent.substr(indent.lastIndexOf('\n') + 1);
	var noNewlines = this.unfinished();

	// a loop header can span physical lines - via unfinished expressions or
	// inside pairs (parens/brackets) opened after the loop keyword
	if (!noNewlines && this._ends.length <= this._loopEnds) { this._seenLoop = false };
	
	if ((/^\n#\s/).test(this._chunk)) {
		this.addLinebreaks(1);
		return 0;
	};
	
	// decide the general line-prefix by the very first line with characters
	
	// if gutter is undefined - we create it on the very first chance we have
	if (this._state.gutter == undefined) {
		this._state.gutter = whitespace;
	};
	
	// if we have a gutter -- remove it
	if (gutter = this._state.gutter || this._opts.gutter) {
		if (whitespace.indexOf(gutter) == 0) {
			whitespace = whitespace.slice(gutter.length);
		} else if (this._chunk[indent.length] === undefined) {
			true;
		} else {
			this.error('incorrect indentation');
		};
		
		// should throw error otherwise?
	};
	
	var size = whitespace.length;
	
	if (this._opts.dropIndentation) {
		return size;
	};
	
	if (size > 0) {
		// seen indent?
		
		if (!this._indentStyle) {
			this._opts.indent = this._indentStyle = whitespace;
			this._indentRegex = new RegExp(whitespace,'g');
		};
		
		let indentSize = 0;
		let offset = 0;
		let offsetLoc = this._loc;
		
		while (true){
			let idx = whitespace.indexOf(this._indentStyle,offset);
			if (idx == offset) {
				indentSize++;
				offset += this._indentStyle.length;
			} else if (offset == whitespace.length) {
				break;
			} else {
				// workaround to report correct location
				this._loc += indent.length - whitespace.length;
				let start = this._loc;
				this.token('INDENT',whitespace,whitespace.length);
				
				this.error("Use tabs for indentation",{
					offset: start + offset,
					length: whitespace.length - offset
				});
			};
		};
		
		size = indentSize;
	};
	
	if ((size - this._indebt) == this._indent) {
		this._scopes.length = this._indents.length;
		
		if (noNewlines) {
			this.suppressNewlines();
		} else {
			this.newlineToken(brCount,indent);
		};
		return indent.length;
	};
	
	if (size > this._indent) {
		if (noNewlines) {
			this._indebt = size - this._indent;
			this.suppressNewlines();
			return indent.length;
		};
		
		if (this.inTag()) {
			return indent.length;
		};
		
		var diff = size - this._indent + this._outdebt;
		this.closeDef();
		
		var expectScope = this._scopes[this._indents.length];
		var immediate = last(this._tokens);
		
		if (immediate && tT(immediate) == 'TERMINATOR') {
			tTs(immediate,'INDENT');
			// should add terminator inside indent?
			immediate._meta || (immediate._meta = {pre: tV(immediate),post: ''});
			immediate.scope = expectScope;
		} else {
			// console.log "set indent {expectScope}"
			this.token('INDENT',"" + diff,0);
			this._last.scope = expectScope;
		};
		
		this._indents.push(diff);
		this.pushEnd('OUTDENT',{opener: this._last});
		this._outdebt = this._indebt = 0;
		this.addLinebreaks(brCount);
	} else if (true) {
		this._indebt = 0;
		
		let moveOut = this._indent - size;
		let currIndent = this._indent;
		let useTabs = this._indentStyle == '\t';
		let lines = indent.replace().split('\n');
		
		let levels = [];
		let k = lines.length;
		let lvl = 0;
		while (k > 0){
			let ln = lines[--k];
			let lnlvl = useTabs ? ln.length : ln.replace(this._indentRegex,'\t').length;
			if (lnlvl > lvl) {
				lvl = lnlvl;
			};
			
			levels[k] = lvl;
		};
		
		levels[0] = currIndent;
		
		// TODO track position as well
		
		let i = 0;
		let toks = [];
		let pre = "";
		for (let idx = 0, items = iter$(lines), len = items.length; idx < len; idx++) {
			let lvl = levels[idx];
			
			while (currIndent > lvl){
				if (pre) {
					this.terminatorToken(pre);
					pre = "";
				} else {
					this.terminatorToken('');
				};
				
				moveOut--;
				this.outdentToken(1,true);
				currIndent--;
			};
			
			pre += '\n' + items[idx];
		};
		
		if (pre) {
			this.terminatorToken(pre);
		};
		
		while (moveOut > 0){
			this.outdentToken(1,true);
			moveOut--;
		};
	};
	
	this._indent = size;
	return indent.length;
};

// Record an outdent token or multiple tokens, if we happen to be moving back
// inwards past several recorded indents.
Lexer.prototype.outdentToken = function (moveOut,noNewlines,newlineCount){
	// here we should also take care to pop / reset the scope-body
	// or context-type for indentation
	// console.log 'outdent!',@chunk,@loc,context == 'DEF'
	if (this.context() == 'DEF') {
		this.closeDef();
	};
	
	var dent = 0;
	while (moveOut > 0){
		var len = this._indents.length - 1;
		if (this._indents[len] == undefined) {
			moveOut = 0;
		} else if (this._indents[len] == this._outdebt) {
			moveOut -= this._outdebt;
			this._outdebt = 0;
		} else if (this._indents[len] < this._outdebt) {
			this._outdebt -= this._indents[len];
			moveOut -= this._indents[len];
		} else {
			dent = this._indents.pop() - this._outdebt;
			moveOut -= dent;
			this._outdebt = 0;
			
			if (!noNewlines) { this.addLinebreaks(1) };
			
			let paired = this.pair('OUTDENT');
			this.token('OUTDENT',"" + dent,0);
			if (paired[1] && paired[1].opener) {
				let opener = paired[1].opener;
				
				this._last._opener = opener;
				opener._closer = this._last;
				opener._closerIndex = this._tokens.length - 1;
				if (opener._type == 'CSS_SEL') {
					this.token('CSS_END',"",0);
				};
				// console.log 'paired now!!',opener,@tokens.indexOf(opener),@tokens:length - 1
			};
		};
	};
	
	if (dent) { this._outdebt -= moveOut };
	
	while (this.lastTokenValue() == ';'){
		this._tokens.pop();
	};
	
	if (!(this.lastTokenType() == 'TERMINATOR' || noNewlines)) { this.token('TERMINATOR','\n',0) };
	// capping scopes so they dont hang around
	this._scopes.length = this._indents.length;
	
	this.closeDef();
	var ctx = this.context();
	if (ctx == '%' || ctx == 'TAG' || ctx == 'IMPORT' || ctx == 'EXPORT') { this.pair(ctx) }; // or ctx == 'CSS' # really?
	return this;
};

// Matches and consumes non-meaningful whitespace. tokid the previous token
// as being "spaced", because there are some cases where it makes a difference.
Lexer.prototype.whitespaceToken = function (type){
	var match,nline,prev;
	if (!((match = WHITESPACE.exec(this._chunk)) || (nline = this._chunk.charAt(0) === '\n'))) { return 0 };
	prev = last(this._tokens);
	
	// FIX - why oh why?
	if (prev) {
		if (match) {
			prev.spaced = true;
			// prev.@s = match[0]
			// console.log 'whitespace',JSON.stringify(match[0]),prev
			return match[0].length;
		} else {
			prev.newLine = true;
			return 0;
		};
	};
};

Lexer.prototype.moveHead = function (str){
	var br = countLineBreaks(str);
	return br;
};

Lexer.prototype.terminatorToken = function (content,loc){
	if (this._lastTyp == 'TERMINATOR') {
		return this._last._value += content;
		// add location info as well?
	} else {
		return this.token('TERMINATOR',content,loc);
	};
};

Lexer.prototype.addLinebreaks = function (count,raw){
	var br;
	
	if (!raw && count == 0) { return this }; // no terminators?
	
	var prev = this._last;
	
	if (!raw) {
		if (count == 1) {
			br = '\n';
		} else if (count == 2) {
			br = '\n\n';
		} else if (count == 3) {
			br = '\n\n\n';
		} else {
			br = repeatString('\n',count);
		};
	};
	// FIX
	if (prev) {
		var t = prev._type; // @lastTyp
		var v = tV(prev);
		
		// we really want to add this
		if (t == 'INDENT') {
			// TODO we want to add to the indent
			// console.log "add the comment to the indent -- pre? {raw} {br}"
			
			var meta = prev._meta || (prev._meta = {pre: '',post: ''});
			meta.post += (raw || br);
			// tVs(v + (raw or br))
			return this;
		} else if (t == 'TERMINATOR') {
			// console.log "already exists terminator {br} {raw}"
			tVs(prev,v + (raw || br));
			return this;
		};
	};
	
	this.token('TERMINATOR',(raw || br),0);
	return;
};

// Generate a newline token. Consecutive newlines get merged together.
Lexer.prototype.newlineToken = function (lines,raw){
	
	// while lastTokenValue == ';'
	//	@tokens.pop
	
	this.addLinebreaks(lines,raw);
	// WARN now import cannot go over multiple lines
	this.closeDef(); // close def -- really?
	var ctx = this.context();
	if (ctx == 'TAG' || ctx == 'IMPORT' || ctx == 'EXPORT') { this.pair(ctx) };
	// closeDef()  # close def -- really?
	return this;
};

// Use a `\` at a line-ending to suppress the newline.
// The slash is removed here once its job is done.
Lexer.prototype.suppressNewlines = function (){
	if (this.value() === '\\') { this._tokens.pop() };
	return this;
};

// We treat all other single characters as a token. E.g.: `( ) , . !`
// Multi-character operators are also literal tokens, so that Jison can assign
// the proper order of operations. There are some symbols that we tokid specially
// here. `;` and newlines are both treated as a `TERMINATOR`, we distinguish
// parentheses that indicate a method call from regular parentheses, and so on.
Lexer.prototype.literalToken = function (){
	var match,value;
	if (match = OPERATOR.exec(this._chunk)) {
		value = match[0];
		if (CODE.test(value)) this.tagParameters();
	} else {
		value = this._chunk.charAt(0);
	};
	
	var end1 = this._ends[this._ends.length - 1];
	var end2 = this._ends[this._ends.length - 2];
	
	var inTag = end1 == 'TAG_END' || end1 == 'OUTDENT' && end2 == 'TAG_END';
	
	var tokid = value;
	var prev = last(this._tokens);
	var pt = prev && tT(prev);
	var pv = prev && tV(prev);
	var length = value.length;
	
	// is this needed?
	if (value == '=' && prev) {
		
		if (pv == '||' || pv == '&&') { // in ['||', '&&']
			tTs(prev,'COMPOUND_ASSIGN');
			tVs(prev,pv + '='); // need to change the length as well
			prev._len = this._loc - prev._loc + value.length;
			return value.length;
		};
	};
	
	if (value == 'ƒ') {
		tokid = 'DO';
	};
	
	if (value == '|') {
		// hacky way to implement this
		// with new lexer we'll use { ... } instead, and assume object-context,
		// then go back and correct when we see the context is invalid
		if (pv == '(') {
			this.token('DO','DO',0);
			this.pushEnd('|');
			this.token('BLOCK_PARAM_START',value,1);
			return length;
		} else if (pt == 'DO') {
			this.pushEnd('|');
			this.token('BLOCK_PARAM_START',value,1);
			return length;
		} else if (end1 == '|') {
			this.token('BLOCK_PARAM_END',value,1);
			this.pair('|');
			return length;
		};
	};
	
	if (value === ';') {
		this._seenFor = false;
		this._seenLoop = false;
		tokid = 'TERMINATOR';
	};
	
	if (value == '(' && pt == 'T.') {
		tokid = 'STYLE_START';
	} else if (value == '[' && inTag) {
		tokid = 'STYLE_START';
	} else if (value === '(' && inTag && pt != '=' && prev.spaced) { // FIXed
		// console.log 'spaced before ( in tokid'
		// FIXME - should rather add a special token like TAG_PARAMS_START
		this.token(',',',');
	} else if (value === '->' && inTag) {
		tokid = 'TAG_END';
		this.pair('TAG_END');
	} else if (value === '=>' && inTag) {
		tokid = 'TAG_END';
		this.pair('TAG_END');
	} else if (value === '/>' && inTag) {
		tokid = 'TAG_END';
		this.pair('TAG_END');
	} else if (value === '>' && inTag) {
		tokid = 'TAG_END';
		this.pair('TAG_END');
	} else if (value === 'TERMINATOR' && end1 === 'DEF') {
		this.closeDef();
	} else if (value === '&' && this.context() == 'DEF') {
		// console.log("okay!")
		tokid = 'BLOCK_ARG';
		// change the next identifier instead?
	} else if (value == '---') {
		tokid = 'SEPARATOR';
	} else if (value == '-' && pt == 'TERMINATOR' && this._chunk.match(/^\-\s*\n/)) {
		tokid = 'SEPARATOR';
	} else if (value == '*' && this._chunk.charAt(1).match(/[A-Za-z\_\@\[]/) && (prev.spaced || SPLAT_PREV_VALUE_MAP[pv] == 1)) {
		tokid = "SPLAT";
	} else if (value == '*' && (this.context() == 'IMPORT' || this.context() == 'EXPORT')) {
		tokid = ("" + this.context() + "_ALL");
	} else if (value == ',' && this.context() == 'IMPORT') {
		tokid = "IMPORT_COMMA";
	} else if (value == '!' && prev && !prev.spaced && ((pv == ']' || pv == ')') || (pt == 'IDENTIFIER' || pt == 'SYMBOLID' || pt == 'SUPER'))) {
		tokid = 'BANG';
	} else if (value == '&' && this._chunk.match(/^\&\s*[,\)\}\]]/)) {
		tokid = 'DO_PLACEHOLDER';
	} else if (value == '&' && this._chunk.match(/^\&(\s*[\.\>\<\=\%\^]|\s+(is|isnt|in|not|isa)\b|[\[\(])/)) {
		tokid = 'AMPER_REF';
	} else if (value == '**') {
		tokid = 'EXP';
	} else if (value == '%' && (pt == 'NUMBER' || pt == ')') && !prev.spaced) {
		tokid = 'UNIT';
	} else if (MATH_MAP[value] == 1) {
		tokid = 'MATH';
	} else if (COMPARE_MAP[value] == 1) {
		tokid = 'COMPARE';
	} else if (COMPOUND_ASSIGN_MAP[value] == 1) {
		tokid = 'COMPOUND_ASSIGN';
	} else if (UNARY_MAP[value] == 1) {
		tokid = 'UNARY';
	} else if (SHIFT_MAP[value] == 1) {
		tokid = 'SHIFT';
	} else if (LOGIC_MAP[value] == 1) {
		tokid = 'LOGIC'; // or value is '?' and prev?:spaced
	} else if (prev && !prev.spaced) {
		if (value == '{' && pt == 'IDENTIFIER') {
			tokid = '{{';
		} else if (value == '{' && pt == '#') {
			tokid = '{{';
		};
		
		if (value === '(' && CALLABLE_MAP[pt] == 1) {
			tokid = 'CALL_START';
		} else if (value === '(' && pt == 'DO') {
			tokid = 'BLOCK_PARAM_START';
		} else if (value === '[' && INDEXABLE_MAP[pt] == 1) {
			tokid = 'INDEX_START';
			if (pt == '?') { tTs(prev,'INDEX_SOAK') };
		};
	};
	
	if (pv == '&' && pt != 'AMPER_REF') {
		if (!prev.spaced && AMPER_REF_TIGHT_TOKENS[tokid] == 1) {
			tTs(prev,pt = 'AMPER_REF');
		} else if (prev.spaced && tokid == 'COMPARE') {
			tTs(prev,pt = 'AMPER_REF');
		};
	};
	
	
	let opener = null;
	
	switch (value) {
		case '(': 
		case '{': 
		case '[': {
			this.pushEnd(INVERSES[value],{closeType: INVERSES[tokid],i: this._tokens.length});
			break;
		}
		case ')': 
		case '}': 
		case ']': {
			let paired = this.pair(value);
			if (paired && paired[1].closeType) {
				tokid = paired[1].closeType;
				let other = this._tokens[paired[1].i];
				opener = this._tokens[paired[1].i];
			};
			break;
		}
	};
	
	if (value == '\\') {
		tokid = 'TYPE';
		let annotation = this.findTypeAnnotation(this._chunk.slice(1));
		if (annotation) {
			value = value + annotation;
		};
	};
	
	// Add unspaced <> gemneric annotation?
	if (value == '<' && TYPE_GENERICS_AFTER.test(this.prevChars(1))) {
		
		let typ = this.findTypeAnnotation(this._chunk,true);
		if (typ) {
			tokid = 'GENERICS';
			value = typ;
		};
	};
	
	if (value == '..' && !prev.spaced) {
		tokid = '?.';
		value = '?.';
	};
	
	if (value == ':' && end1 == 'TAG_RULE') {
		tokid = 'T:';
	};
	
	if ((tokid == '-' || tokid == '+')) {
		if (/\w|\(|\$/.test(this._chunk[1]) && (!prev || prev.spaced)) {
			tokid = tokid + tokid + tokid;
		};
	};
	
	this.token(tokid,value,value.length);
	
	if (opener) {
		opener._closer = this._last;
		opener._closerIndex = this._tokens.length - 1;
	};
	
	if (this._platform == 'tsc') {
		let next = this._chunk[1] || '';
		if (value == '.' && (!next || TSC_CARET_BEFORE[next])) {
			this.token('IDENTIFIER','$CARET$',0,1);
		} else if (value == '@' && (!next || (/[^\$\@\-\.\w]/).test(next)) && false) {
			this.token('IDENTIFIER','$CARET$',0,1);
		};
	};
	
	return value.length;
};

// Token Manipulators
// ------------------

// Sanitize a heredoc or herecomment by
// erasing all external indentation on the left-hand side.
Lexer.prototype.sanitizeHeredoc = function (doc,options){
	var match;
	var indent = options.indent;
	var herecomment = options.herecomment;
	
	if (herecomment) {
		if (HEREDOC_ILLEGAL.test(doc)) {
			this.error("block comment cannot contain '*/'",{length: 2});
		};
		
		if (doc.indexOf('\n') <= 0) { return doc };
	} else {
		var length_;while (match = HEREDOC_INDENT.exec(doc)){
			var attempt = match[1];
			if (indent === null || 0 < (length_ = attempt.length) && length_ < indent.length) {
				indent = attempt;
			};
		};
	};
	
	if (indent) { doc = doc.replace(RegExp(("\\n" + indent),"g"),'\n') };
	if (!herecomment) {
		if (doc[0] == '\n') {
			options.offset = indent.length + 1;
		};
		doc = doc.replace(/^\n/,'');
	};
	options.realIndent = indent;
	return doc;
};

// A source of ambiguity in our grammar used to be parameter lists in function
// definitions versus argument lists in function calls. Walk backwards, tokidging
// parameters specially in order to make things easier for the parser.
Lexer.prototype.tagParameters = function (){
	var tok;
	if (this.lastTokenType() != ')') { return this };
	var stack = [];
	var tokens = this._tokens;
	var i = tokens.length;
	
	tTs(tokens[--i],'PARAM_END');
	
	while (tok = tokens[--i]){
		var t = tT(tok);
		switch (t) {
			case ')': {
				stack.push(tok);
				break;
			}
			case '(': 
			case 'CALL_START': {
				if (stack.length) {
					stack.pop();
				} else if (t === '(') {
					tTs(tok,'PARAM_START');
					return this;
				} else {
					return this;
				};
				break;
			}
		};
	};
	
	return this;
};

// Close up all remaining open blocks at the end of the file.
Lexer.prototype.closeIndentation = function (){
	while (true){
		var ctx = this.context();
		if (ctx == 'TAG' || ctx == 'IMPORT' || ctx == 'EXPORT') {
			this.pair(ctx);
		} else {
			break;
		};
	};
	// pair(context) if context == 'IMPORT' or context == 'EXPORT'
	this.closeDef();
	this.closeSelector();
	return this.outdentToken(this._indent,false,0);
};

// Matches a balanced group such as a single or double-quoted string. Pass in
// a series of delimiters, all of which must be nested correctly within the
// contents of the string. This method allows us to have strings within
// interpolations within strings, ad infinitum.
Lexer.prototype.balancedString = function (str,end){
	var match,letter,prev;
	
	var stack = [end];
	var i = 0;
	
	// could it not happen here?
	while (i < (str.length - 1)){
		i++;
		letter = str.charAt(i);
		switch (letter) {
			case '\\': {
				i++;
				continue;
				break;
			}
			case end: {
				stack.pop();
				if (!stack.length) {
					var v = str.slice(0,i + 1);
					return v;
				};
				end = stack[stack.length - 1];
				continue;
				break;
			}
		};
		
		if (end === '}' && (letter == '"' || letter == "'" || letter == "`")) {
			stack.push(end = letter);
		} else if (end === '}' && letter === '/' && (match = (HEREGEX.exec(str.slice(i)) || REGEX.exec(str.slice(i))))) {
			i += match[0].length - 1;
		} else if (end === '}' && letter === '{') {
			stack.push(end = '}');
		} else if (end === '"' && letter === '{') {
			stack.push(end = '}');
		} else if (end === '`' && letter === '{') {
			stack.push(end = '}');
		};
		prev = letter;
	};

	var missing = stack.pop();
	var line = this.lineAt(this._loc);
	if (missing == '}') {
		return this.error(("unclosed interpolation in string starting on line " + line + " - expected '}'"),{length: 1});
	};
	return this.error(("unterminated string starting on line " + line + " - expected closing " + missing),{length: 1});
};

// Expand variables and expressions inside double-quoted strings using
// braces for substitution of arbitrary expressions.
//
//     "Hello {name.capitalize}."
//
// If it encounters an interpolation, this method will recursively create a
// new Lexer, tokenize the interpolated contents, and merge them into the
// token stream.
Lexer.prototype.interpolateString = function (str,options){
	
	if(options === undefined) options = {};
	var heredoc = options.heredoc;
	var quote = options.quote;
	var regex = options.regex;
	var prefix = options.prefix;
	var indent = options.indent;
	
	var startLoc = this._loc;
	var tokens = [];
	var pi = 0;
	var i = -1;
	var locOffset = options.offset || 1;
	var strlen = str.length;
	var letter;
	var expr;
	
	var isInterpolated = false;
	// out of bounds
	
	while (letter = str[i += 1]){
		if (letter === '\\') {
			i += 1;
			continue;
		};
		
		if (letter === '\n' && indent) {
			locOffset += indent.length;
		};
		
		if (!(str[i] == '{' && (expr = this.balancedString(str.slice(i),'}')))) {
			continue;
		};
		
		isInterpolated = true;
		
		// these have no real sense of location or anything?
		if (pi < i) {
			// this is the prefix-string - before any item
			var tok = new Token('NEOSTRING',this.escapeStr(str.slice(pi,i),heredoc,quote),this._loc + pi + locOffset,i - pi);
			// tok.@loc = @loc + pi
			// tok.@len = i - pi + 2
			tokens.push(tok);
		};
		
		tokens.push(new Token('{{','{',this._loc + i + locOffset,1));
		
		var inner = expr.slice(1,-1);
		
		// remove leading spaces
		// need to keep track of how much whitespace we dropped from the start
		inner = inner.replace(/^[^\n\S]+/,'');
		
		if (inner.length) {
			// we need to remember the loc we start at
			// console.log('interpolate from loc',@loc,i)
			// really? why not just add to the stack??
			// what about the added
			// should share with the selector no?
			// console.log "tokenize inner parts of string",inner
			var spaces = 0;
			var offset = this._loc + i + (expr.length - inner.length) - 1;
			// why create a whole new lexer? Should rather reuse one
			// much better to simply move into interpolation mode where
			// we continue parsing until we meet unpaired }
			var nested = new Lexer().tokenize(inner,{inline: true,rewrite: false,loc: offset + locOffset},this._script);
			// console.log nested.pop
			
			if (nested[0] && tT(nested[0]) == 'TERMINATOR') {
				nested.shift();
			};
			
			if (nested.length) {
				tokens.push.apply(tokens,nested); // T.token('TOKENS',nested,0)
			};
		};
		
		// should rather add the amount by which our lexer has moved?
		i += expr.length - 1;
		tokens.push(new Token('}}','}',this._loc + i + locOffset,1));
		pi = i + 1;
	};
	
	// adding the last part of the string here
	if (i >= pi && pi < str.length) {
		// set the length as well - or?
		// the string after?
		// console.log 'push neostring'
		tokens.push(new Token('NEOSTRING',this.escapeStr(str.slice(pi),heredoc,quote),this._loc + pi + locOffset,str.length - pi));
	};
	
	// console.log tokens:length
	if (regex) { return tokens };
	
	if (!tokens.length) { return this.token('NEOSTRING','""') };
	
	for (let j = 0, len = tokens.length; j < len; j++) {
		this._tokens.push(tokens[j]);
	};
	
	return tokens;
};

// Matches a balanced group such as a single or double-quoted string. Pass in
// a series of delimiters, all of which must be nested correctly within the
// contents of the string. This method allows us to have strings within
// interpolations within strings, ad infinitum.
Lexer.prototype.balancedSelector = function (str,end){
	var prev;
	var letter;
	var stack = [end];
	// FIXME
	for (let len = str.length, i = 1, rd = len - i; (rd > 0) ? (i < len) : (i > len); (rd > 0) ? (i++) : (i--)) {
		switch (letter = str.charAt(i)) {
			case '\\': {
				i++;
				continue;
				break;
			}
			case end: {
				stack.pop();
				if (!stack.length) {
					return str.slice(0,i + 1);
				};
				
				end = stack[stack.length - 1];
				continue;
				break;
			}
		};
		if (end === '}' && letter === ')') {
			stack.push(end = letter);
		} else if (end === '}' && letter === '{') {
			stack.push(end = '}');
		} else if (end === ')' && letter === '{') {
			stack.push(end = '}');
		};
		prev = letter; // what, why?
	};

	return this.error(("unclosed selector starting on line " + (this.lineAt(this._loc)) + " - expected '" + (stack.pop()) + "'"),{length: 1});
};

// Friendly descriptions of the pair-tokens tracked on the @ends stack,
// keyed by the closing token we are waiting for.
var END_OPENER_NAMES = {
	')': "'('",
	']': "'['",
	'}': "'{'",
	'TAG_END': "tag",
	'TAG': "tag declaration",
	'TAG_ATTR': "tag attribute value",
	'DEF': "method",
	'IMPORT': "import",
	'EXPORT': "export",
	'%': "selector",
	'|': "block parameters",
	'OUTDENT': "indented block"
};

var END_CLOSER_NAMES = {
	')': "')'",
	']': "']'",
	'}': "'}'",
	'TAG_END': "'>'",
	'|': "'|'"
};

// Describe the innermost unclosed pair on the @ends stack, locating the
// token that opened it (when known) so errors can point back to it.
Lexer.prototype.unclosedInfo = function (){
	var idx = this._ends.length - 1;
	var end = this._ends[idx];
	var ctx = this._contexts[idx];
	var opener = null;
	if (ctx) {
		opener = ctx.opener || (ctx.i != null && this._tokens[ctx.i]) || null;
	};
	if (opener && !(opener._loc >= 0)) { opener = null };
	return {
		end: end,
		opener: opener,
		what: END_OPENER_NAMES[end] || ("'" + end + "'"),
		closer: END_CLOSER_NAMES[end]
	};
};

// Report the innermost unclosed pair, anchored at the token that opened it.
Lexer.prototype.unclosedError = function (until){
	var info = this.unclosedInfo();
	var msg = "unclosed " + info.what;
	if (info.closer) { msg += " - expected " + info.closer };
	if (until) { msg += " before " + until };
	var params = {length: 1};
	if (info.opener) {
		params.range = this._script.rangeAt(info.opener._loc,info.opener._loc + (info.opener._len || 1));
	};
	return this.error(msg,params);
};

Lexer.prototype.lineAt = function (loc){
	return this._script.positionAt(loc).line + 1;
};

// Pairs up a closing token, ensuring that all listed pairs of tokens are
// correctly balanced throughout the course of the token stream.
Lexer.prototype.pair = function (tok){
	var wanted = last(this._ends);
	if (tok != wanted) {
		if (!('OUTDENT' === wanted)) {
			if (tok == 'OUTDENT') {
				// the code dedented (or the file ended) while an explicit
				// pair was still open - point back to where it was opened
				this.unclosedError(("the block ends (line " + (this.lineAt(this._loc)) + ")"));
			} else if (!wanted) {
				this.error(("unmatched " + (END_CLOSER_NAMES[tok] || ("'" + tok + "'"))),{length: 1});
			} else {
				let info = this.unclosedInfo();
				let msg = "unmatched " + (END_CLOSER_NAMES[tok] || ("'" + tok + "'"));
				if (info.closer) {
					msg += " - expected " + info.closer + " to close " + info.what;
					if (info.opener) { msg += " (line " + (this.lineAt(info.opener._loc)) + ")" };
				};
				this.error(msg,{length: 1});
			};
		};
		var size = last(this._indents);
		this._indent -= size;
		this.outdentToken(size,true,0);
		return this.pair(tok);
	};
	return this.popEnd();
};

// Helpers
// -------

// Add a token to the results, taking note of the line number.
Lexer.prototype.token = function (id,value,len,offset){
	this._lastTyp = id;
	this._lastVal = value;
	var tok = this._last = new Token(id,value,this._loc + (offset || 0),len || 0);
	this._tokens.push(tok);
	return;
};

Lexer.prototype.lastTokenType = function (){
	var token = this._tokens[this._tokens.length - 1];
	return token ? tT(token) : 'NONE';
};

Lexer.prototype.lastTokenValue = function (){
	var token = this._tokens[this._tokens.length - 1];
	return token ? token._value : '';
};

// Peek at a tokid in the current token stream.
Lexer.prototype.tokid = function (index,val){
	var tok;
	if (tok = last(this._tokens,index)) {
		if (val) { tTs(tok,val) };
		return tT(tok);
	} else {
		return null;
	};
};

// Peek at a value in the current token stream.
Lexer.prototype.value = function (index,val){
	var tok;
	if (tok = last(this._tokens,index)) {
		if (val) { tVs(tok,val) };
		return tV(tok);
	} else {
		return null;
	};
};

// Are we in the midst of an unfinished expression?
Lexer.prototype.unfinished = function (){
	if (LINE_CONTINUER.test(this._chunk) && (!this._context || !this._context.style)) { return true };
	return (UNFINISHED_MAP[this._lastTyp] == 1 && this._platform != 'tsc');
};

// Converts newlines for string literals.
Lexer.prototype.escapeLines = function (str,heredoc){
	return str.replace(MULTILINER,(heredoc ? '\\n' : ''));
};

// Constructs a string token by escaping quotes and newlines.
Lexer.prototype.makeString = function (body,quote,heredoc){
	if (!body) { return quote + quote };
	body = body.replace(/\\([\s\S])/g,function(match,contents) {
		return (contents == '\n' || contents == quote) ? contents : match;
	});
	// Does not work now
	body = body.replace(RegExp(("" + quote),"g"),'\\$&');
	return quote + this.escapeLines(body,heredoc) + quote;
};

// Throws a syntax error on the current `@line`.
Lexer.prototype.error = function (message,params){ // len = 0
	if(params === undefined) params = {};
	let loc = params.offset || this._loc;
	let err = this._script.addDiagnostic('error',{
		message: message,
		source: params.source || 'imba-lexer',
		range: params.range || this._script.rangeAt(loc,loc + (params.length || len$(this)))
	});
	throw err.toError();
};

export { ALL_KEYWORDS, Lexer, LexerError };