let source, pos, end,
  openTokenDepth,
  lastTokenPos,
  openTokenPosStack,
  openClassPosStack,
  curDynamicImport,
  templateStackDepth,
  facade,
  lastSlashWasDivision,
  nextBraceIsClass,
  templateDepth,
  templateStack,
  imports,
  exports,
  name;

function addImport (ss, s, e, d) {
  const impt = { ss, se: d === -2 ? e : d === -1 ? e + 1 : 0, s, e, d, a: -1, n: undefined };
  imports.push(impt);
  return impt;
}

function readName (impt) {
  let { d, s } = impt;
  if (d !== -1)
    s++;
  impt.n = readString(s, source.charCodeAt(s - 1));
}

// Note: parsing is based on the _assumption_ that the source is already valid
export function parse (_source, _name) {
  openTokenDepth = 0;
  curDynamicImport = null;
  templateDepth = -1;
  lastTokenPos = -1;
  lastSlashWasDivision = false;
  templateStack = Array(1024);
  templateStackDepth = 0;
  openTokenPosStack = Array(1024);
  openClassPosStack = Array(1024);
  nextBraceIsClass = false;
  facade = true;
  name = _name || '@';

  imports = [];
  exports = new Set();

  source = _source;
  pos = -1;
  end = source.length - 1;
  let ch = 0;

  // start with a pure "module-only" parser
  m: while (pos++ < end) {
    ch = source.charCodeAt(pos);

    if (ch === 32 || ch < 14 && ch > 8)
      continue;

    switch (ch) {
      case 101/*e*/:
        if (openTokenDepth === 0 && keywordStart(pos) && source.startsWith('xport', pos + 1)) {
          tryParseExportStatement();
          // export might have been a non-pure declaration
          if (!facade) {
            lastTokenPos = pos;
            break m;
          }
        }
        break;
      case 105/*i*/:
        if (keywordStart(pos) && source.startsWith('mport', pos + 1))
          tryParseImportStatement();
        break;
      case 59/*;*/:
        break;
      case 47/*/*/: {
        const next_ch = source.charCodeAt(pos + 1);
        if (next_ch === 47/*/*/) {
          lineComment();
          // dont update lastToken
          continue;
        }
        else if (next_ch === 42/***/) {
          blockComment(true);
          // dont update lastToken
          continue;
        }
        // fallthrough
      }
      default:
        // as soon as we hit a non-module token, we go to main parser
        facade = false;
        pos--;
        break m;
    }
    lastTokenPos = pos;
  }

  while (pos++ < end) {
    ch = source.charCodeAt(pos);

    if (ch === 32 || ch < 14 && ch > 8)
      continue;

    switch (ch) {
      case 101/*e*/:
        if (openTokenDepth === 0 && keywordStart(pos) && source.startsWith('xport', pos + 1))
          tryParseExportStatement();
        break;
      case 105/*i*/:
        if (keywordStart(pos) && source.startsWith('mport', pos + 1))
          tryParseImportStatement();
        break;
      case 99/*c*/:
        if (keywordStart(pos) && source.startsWith('lass', pos + 1) && isBrOrWs(source.charCodeAt(pos + 5)))
          nextBraceIsClass = true;
        break;
      case 40/*(*/:
        openTokenPosStack[openTokenDepth++] = lastTokenPos;
        break;
      case 41/*)*/:
        if (openTokenDepth === 0)
          syntaxError();
        openTokenDepth--;
        if (curDynamicImport && curDynamicImport.d === openTokenPosStack[openTokenDepth]) {
          if (curDynamicImport.e === 0)
            curDynamicImport.e = pos;
          curDynamicImport.se = pos;
          curDynamicImport = null;
        }
        break;
      case 123/*{*/:
        // dynamic import followed by { is not a dynamic import (so remove)
        // this is a sneaky way to get around { import () {} } v { import () }
        // block / object ambiguity without a parser (assuming source is valid)
        if (source.charCodeAt(lastTokenPos) === 41/*)*/ && imports.length && imports[imports.length - 1].e === lastTokenPos) {
          imports.pop();
        }
        openClassPosStack[openTokenDepth] = nextBraceIsClass;
        nextBraceIsClass = false;
        openTokenPosStack[openTokenDepth++] = lastTokenPos;
        break;
      case 125/*}*/:
        if (openTokenDepth === 0)
          syntaxError();
        if (openTokenDepth-- === templateDepth) {
          templateDepth = templateStack[--templateStackDepth];
          templateString();
        }
        else {
          if (templateDepth !== -1 && openTokenDepth < templateDepth)
            syntaxError();
        }
        break;
      case 39/*'*/:
      case 34/*"*/:
        stringLiteral(ch);
        break;
      case 47/*/*/: {
        const next_ch = source.charCodeAt(pos + 1);
        if (next_ch === 47/*/*/) {
          lineComment();
          // dont update lastToken
          continue;
        }
        else if (next_ch === 42/***/) {
          blockComment(true);
          // dont update lastToken
          continue;
        }
        else {
          // Division / regex ambiguity handling based on checking backtrack analysis of:
          // - what token came previously (lastToken)
          // - if a closing brace or paren, what token came before the corresponding
          //   opening brace or paren (lastOpenTokenIndex)
          const lastToken = source.charCodeAt(lastTokenPos);
          if (isExpressionPunctuator(lastToken) &&
              !(lastToken === 46/*.*/ && (source.charCodeAt(lastTokenPos - 1) >= 48/*0*/ && source.charCodeAt(lastTokenPos - 1) <= 57/*9*/)) &&
              !(lastToken === 43/*+*/ && source.charCodeAt(lastTokenPos - 1) === 43/*+*/) && !(lastToken === 45/*-*/ && source.charCodeAt(lastTokenPos - 1) === 45/*-*/) ||
              lastToken === 41/*)*/ && isParenKeyword(openTokenPosStack[openTokenDepth]) ||
              lastToken === 125/*}*/ && (isExpressionTerminator(openTokenPosStack[openTokenDepth]) || openClassPosStack[openTokenDepth]) ||
              lastToken === 47/*/*/ && lastSlashWasDivision ||
              isExpressionKeyword(lastTokenPos) ||
              !lastToken) {
            regularExpression();
            lastSlashWasDivision = false;
          }
          else {
            lastSlashWasDivision = true;
          }
        }
        break;
      }
      case 96/*`*/:
        templateString();
        break;
    }
    lastTokenPos = pos;
  }

  if (templateDepth !== -1 || openTokenDepth)
    syntaxError();

  return [imports, [...exports], facade];
}

function tryParseImportStatement () {
  const startPos = pos;

  pos += 6;

  let ch = commentWhitespace(true);
  
  switch (ch) {
    // dynamic import
    case 40/*(*/:
      openTokenPosStack[openTokenDepth++] = startPos;
      if (source.charCodeAt(lastTokenPos) === 46/*.*/)
        return;
      // dynamic import indicated by positive d
      const impt = addImport(startPos, pos + 1, 0, startPos);
      curDynamicImport = impt;
      // try parse a string, to record a safe dynamic import string
      pos++;
      ch = commentWhitespace(true);
      if (ch === 39/*'*/ || ch === 34/*"*/) {
        stringLiteral(ch);
      }
      else {
        pos--;
        return;
      }
      pos++;
      ch = commentWhitespace(true);
      if (ch === 44/*,*/) {
        impt.e = pos;
        pos++;
        ch = commentWhitespace(true);
        impt.a = pos;
        readName(impt);
        pos--;
      }
      else if (ch === 41/*)*/) {
        openTokenDepth--;
        impt.e = pos;
        impt.se = pos;
        readName(impt);
      }
      else {
        pos--;
      }
      return;
    // import.meta
    case 46/*.*/:
      pos++;
      ch = commentWhitespace(true);
      // import.meta indicated by d === -2
      if (ch === 109/*m*/ && source.startsWith('eta', pos + 1) && source.charCodeAt(lastTokenPos) !== 46/*.*/)
        addImport(startPos, startPos, pos + 4, -2);
      return;
    
    default:
      // no space after "import" -> not an import keyword
      if (pos === startPos + 6)
        break;
    case 34/*"*/:
    case 39/*'*/:
    case 123/*{*/:
    case 42/***/:
      // import statement only permitted at base-level
      if (openTokenDepth !== 0) {
        pos--;
        return;
      }
      while (pos < end) {
        ch = source.charCodeAt(pos);
        if (ch === 39/*'*/ || ch === 34/*"*/) {
          readImportString(startPos, ch);
          return;
        }
        pos++;
      }
      syntaxError();
  }
}

function tryParseExportStatement () {
  const sStartPos = pos;

  pos += 6;

  const curPos = pos;

  let ch = commentWhitespace(true);

  if (pos === curPos && !isPunctuator(ch))
    return;

  switch (ch) {
    // export default ...
    case 100/*d*/:
      exports.add(source.slice(pos, pos + 7));
      return;

    // export async? function*? name () {
    case 97/*a*/:
      pos += 5;
      commentWhitespace(true);
    // fallthrough
    case 102/*f*/:
      pos += 8;
      ch = commentWhitespace(true);
      if (ch === 42/***/) {
        pos++;
        ch = commentWhitespace(true);
      }
      const startPos = pos;
      ch = readToWsOrPunctuator(ch);
      exports.add(source.slice(startPos, pos));
      pos--;
      return;

    case 99/*c*/:
      if (source.startsWith('lass', pos + 1) && isBrOrWsOrPunctuatorNotDot(source.charCodeAt(pos + 5))) {
        pos += 5;
        ch = commentWhitespace(true);
        const startPos = pos;
        ch = readToWsOrPunctuator(ch);
        exports.add(source.slice(startPos, pos));
        pos--;
        return;
      }
      pos += 2;
    // fallthrough

    // export var/let/const name = ...(, name = ...)+
    case 118/*v*/:
    case 109/*l*/:
      // destructured initializations not currently supported (skipped for { or [)
      // also, lexing names after variable equals is skipped (export var p = function () { ... }, q = 5 skips "q")
      pos += 2;
      facade = false;
      do {
        pos++;
        ch = commentWhitespace(true);
        const startPos = pos;
        ch = readToWsOrPunctuator(ch);
        // dont yet handle [ { destructurings
        if (ch === 123/*{*/ || ch === 91/*[*/) {
          pos--;
          return;
        }
        if (pos === startPos)
          return;
        exports.add(source.slice(startPos, pos));
        ch = commentWhitespace(true);
        if (ch === 61/*=*/) {
          pos--;
          return;
        }
      } while (ch === 44/*,*/);
      pos--;
      return;


    // export {...}
    case 123/*{*/:
      pos++;
      ch = commentWhitespace(true);
      while (true) {
        const startPos = pos;
        readToWsOrPunctuator(ch);
        const endPos = pos;
        commentWhitespace(true);
        ch = readExportAs(startPos, endPos);
        // ,
        if (ch === 44/*,*/) {
          pos++;
          ch = commentWhitespace(true);
        }
        if (ch === 125/*}*/)
          break;
        if (pos === startPos)
          return syntaxError(); 
        if (pos > end)
          return syntaxError();
      }
      pos++;
      ch = commentWhitespace(true);
    break;
    
    // export *
    // export * as X
    case 42/***/:
      pos++;
      commentWhitespace(true);
      ch = readExportAs(pos, pos);
      ch = commentWhitespace(true);
    break;
  }

  // from ...
  if (ch === 102/*f*/ && source.startsWith('rom', pos + 1)) {
    pos += 4;
    readImportString(sStartPos, commentWhitespace(true));
  }
  else {
    pos--;
  }
}

/*
 * Ported from Acorn
 *   
 * MIT License

 * Copyright (C) 2012-2020 by various contributors (see AUTHORS)

 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
let acornPos;
function readString (start, quote) {
  acornPos = start;
  let out = '', chunkStart = acornPos;
  for (;;) {
    if (acornPos >= source.length) syntaxError();
    const ch = source.charCodeAt(acornPos);
    if (ch === quote) break;
    if (ch === 92) { // '\'
      out += source.slice(chunkStart, acornPos);
      out += readEscapedChar();
      chunkStart = acornPos;
    }
    else if (ch === 0x2028 || ch === 0x2029) {
      ++acornPos;
    }
    else {
      if (isBr(ch)) syntaxError();
      ++acornPos;
    }
  }
  out += source.slice(chunkStart, acornPos++);
  return out;
}

// Used to read escaped characters

function readEscapedChar () {
  let ch = source.charCodeAt(++acornPos);
  ++acornPos;
  switch (ch) {
    case 110: return '\n'; // 'n' -> '\n'
    case 114: return '\r'; // 'r' -> '\r'
    case 120: return String.fromCharCode(readHexChar(2)); // 'x'
    case 117: return readCodePointToString(); // 'u'
    case 116: return '\t'; // 't' -> '\t'
    case 98: return '\b'; // 'b' -> '\b'
    case 118: return '\u000b'; // 'v' -> '\u000b'
    case 102: return '\f'; // 'f' -> '\f'
    case 13: if (source.charCodeAt(acornPos) === 10) ++acornPos; // '\r\n'
    case 10: // ' \n'
      return '';
    case 56:
    case 57:
      syntaxError();
    default:
      if (ch >= 48 && ch <= 55) {
        let octalStr = source.substr(acornPos - 1, 3).match(/^[0-7]+/)[0];
        let octal = parseInt(octalStr, 8);
        if (octal > 255) {
          octalStr = octalStr.slice(0, -1);
          octal = parseInt(octalStr, 8);
        }
        acornPos += octalStr.length - 1;
        ch = source.charCodeAt(acornPos);
        if (octalStr !== '0' || ch === 56 || ch === 57)
          syntaxError();
        return String.fromCharCode(octal);
      }
      if (isBr(ch)) {
        // Unicode new line characters after \ get removed from output in both
        // template literals and strings
        return '';
      }
      return String.fromCharCode(ch);
  }
}

// Used to read character escape sequences ('\x', '\u', '\U').

function readHexChar (len) {
  const start = acornPos;
  let total = 0, lastCode = 0;
  for (let i = 0; i < len; ++i, ++acornPos) {
    let code = source.charCodeAt(acornPos), val;

    if (code === 95) {
      if (lastCode === 95 || i === 0) syntaxError();
      lastCode = code;
      continue;
    }

    if (code >= 97) val = code - 97 + 10; // a
    else if (code >= 65) val = code - 65 + 10; // A
    else if (code >= 48 && code <= 57) val = code - 48; // 0-9
    else break;
    if (val >= 16) break;
    lastCode = code;
    total = total * 16 + val;
  }

  if (lastCode === 95 || acornPos - start !== len) syntaxError();

  return total;
}

// Read a string value, interpreting backslash-escapes.

function readCodePointToString () {
  const ch = source.charCodeAt(acornPos);
  let code;
  if (ch === 123) { // '{'
    ++acornPos;
    code = readHexChar(source.indexOf('}', acornPos) - acornPos);
    ++acornPos;
    if (code > 0x10FFFF) syntaxError();
  } else {
    code = readHexChar(4);
  }
  // UTF-16 Decoding
  if (code <= 0xFFFF) return String.fromCharCode(code);
  code -= 0x10000;
  return String.fromCharCode((code >> 10) + 0xD800, (code & 1023) + 0xDC00);
}

/*
 * </ Acorn Port>
 */

function readExportAs (startPos, endPos) {
  let ch = source.charCodeAt(pos);
  if (ch === 97 /*a*/) {
    pos += 2;
    ch = commentWhitespace(true);
    startPos = pos;
    readToWsOrPunctuator(ch);
    endPos = pos;
    ch = commentWhitespace(true);
  }
  if (pos !== startPos)
    exports.add(source.slice(startPos, endPos));
  return ch;
}

function readImportString (ss, ch) {
  const startPos = pos + 1;
  if (ch === 39/*'*/ || ch === 34/*"*/) {
    stringLiteral(ch);
  }
  else {
    syntaxError();
    return;
  }
  const impt = addImport(ss, startPos, pos, -1);
  readName(impt);
  pos++;
  ch = commentWhitespace(false);
  if (ch !== 97/*a*/ || !source.startsWith('ssert', pos + 1)) {
    pos--;
    return;
  }
  const assertIndex = pos;

  pos += 6;
  ch = commentWhitespace(true);
  if (ch !== 123/*{*/) {
    pos = assertIndex;
    return;
  }
  const assertStart = pos;
  do {
    pos++;
    ch = commentWhitespace(true);
    if (ch === 39/*'*/ || ch === 34/*"*/) {
      stringLiteral(ch);
      pos++;
      ch = commentWhitespace(true);
    }
    else {
      ch = readToWsOrPunctuator(ch);
    }
    if (ch !== 58/*:*/) {
      pos = assertIndex;
      return;
    }
    pos++;
    ch = commentWhitespace(true);
    if (ch === 39/*'*/ || ch === 34/*"*/) {
      stringLiteral(ch);
    }
    else {
      pos = assertIndex;
      return;
    }
    pos++;
    ch = commentWhitespace(true);
    if (ch === 44/*,*/) {
      pos++;
      ch = commentWhitespace(true);
      if (ch === 125/*}*/)
        break;
      continue;
    }
    if (ch === 125/*}*/)
      break;
    pos = assertIndex;
    return;
  } while (true);
  impt.a = assertStart;
  impt.se = pos + 1;
}

function commentWhitespace (br) {
  let ch;
  do {
    ch = source.charCodeAt(pos);
    if (ch === 47/*/*/) {
      const next_ch = source.charCodeAt(pos + 1);
      if (next_ch === 47/*/*/)
        lineComment();
      else if (next_ch === 42/***/)
        blockComment(br);
      else
        return ch;
    }
    else if (br ? !isBrOrWs(ch): !isWsNotBr(ch)) {
      return ch;
    }
  } while (pos++ < end);
  return ch;
}

function templateString () {
  while (pos++ < end) {
    const ch = source.charCodeAt(pos);
    if (ch === 36/*$*/ && source.charCodeAt(pos + 1) === 123/*{*/) {
      pos++;
      templateStack[templateStackDepth++] = templateDepth;
      templateDepth = ++openTokenDepth;
      return;
    }
    if (ch === 96/*`*/)
      return;
    if (ch === 92/*\*/)
      pos++;
  }
  syntaxError();
}

function blockComment (br) {
  pos++;
  while (pos++ < end) {
    const ch = source.charCodeAt(pos);
    if (!br && isBr(ch))
      return;
    if (ch === 42/***/ && source.charCodeAt(pos + 1) === 47/*/*/) {
      pos++;
      return;
    }
  }
}

function lineComment () {
  while (pos++ < end) {
    const ch = source.charCodeAt(pos);
    if (ch === 10/*\n*/ || ch === 13/*\r*/)
      return;
  }
}

function stringLiteral (quote) {
  while (pos++ < end) {
    let ch = source.charCodeAt(pos);
    if (ch === quote)
      return;
    if (ch === 92/*\*/) {
      ch = source.charCodeAt(++pos);
      if (ch === 13/*\r*/ && source.charCodeAt(pos + 1) === 10/*\n*/)
        pos++;
    }
    else if (isBr(ch))
      break;
  }
  syntaxError();
}

function regexCharacterClass () {
  while (pos++ < end) {
    let ch = source.charCodeAt(pos);
    if (ch === 93/*]*/)
      return ch;
    if (ch === 92/*\*/)
      pos++;
    else if (ch === 10/*\n*/ || ch === 13/*\r*/)
      break;
  }
  syntaxError();
}

function regularExpression () {
  while (pos++ < end) {
    let ch = source.charCodeAt(pos);
    if (ch === 47/*/*/)
      return;
    if (ch === 91/*[*/)
      ch = regexCharacterClass();
    else if (ch === 92/*\*/)
      pos++;
    else if (ch === 10/*\n*/ || ch === 13/*\r*/)
      break;
  }
  syntaxError();
}

function readToWsOrPunctuator (ch) {
  do {
    if (isBrOrWs(ch) || isPunctuator(ch))
      return ch;
  } while (ch = source.charCodeAt(++pos));
  return ch;
}

// Note: non-asii BR and whitespace checks omitted for perf / footprint
// if there is a significant user need this can be reconsidered
function isBr (c) {
  return c === 13/*\r*/ || c === 10/*\n*/;
}

function isWsNotBr (c) {
  return c === 9 || c === 11 || c === 12 || c === 32 || c === 160;
}

function isBrOrWs (c) {
  return c > 8 && c < 14 || c === 32 || c === 160;
}

function isBrOrWsOrPunctuatorNotDot (c) {
  return c > 8 && c < 14 || c === 32 || c === 160 || isPunctuator(c) && c !== 46/*.*/;
}

function keywordStart (pos) {
  return pos === 0 || isBrOrWsOrPunctuatorNotDot(source.charCodeAt(pos - 1));
}

function readPrecedingKeyword (pos, match) {
  if (pos < match.length - 1)
    return false;
  return source.startsWith(match, pos - match.length + 1) && (pos === 0 || isBrOrWsOrPunctuatorNotDot(source.charCodeAt(pos - match.length)));
}

function readPrecedingKeyword1 (pos, ch) {
  return source.charCodeAt(pos) === ch && (pos === 0 || isBrOrWsOrPunctuatorNotDot(source.charCodeAt(pos - 1)));
}

// Detects one of case, debugger, delete, do, else, in, instanceof, new,
//   return, throw, typeof, void, yield, await
function isExpressionKeyword (pos) {
  switch (source.charCodeAt(pos)) {
    case 100/*d*/:
      switch (source.charCodeAt(pos - 1)) {
        case 105/*i*/:
          // void
          return readPrecedingKeyword(pos - 2, 'vo');
        case 108/*l*/:
          // yield
          return readPrecedingKeyword(pos - 2, 'yie');
        default:
          return false;
      }
    case 101/*e*/:
      switch (source.charCodeAt(pos - 1)) {
        case 115/*s*/:
          switch (source.charCodeAt(pos - 2)) {
            case 108/*l*/:
              // else
              return readPrecedingKeyword1(pos - 3, 101/*e*/);
            case 97/*a*/:
              // case
              return readPrecedingKeyword1(pos - 3, 99/*c*/);
            default:
              return false;
          }
        case 116/*t*/:
          // delete
          return readPrecedingKeyword(pos - 2, 'dele');
        default:
          return false;
      }
    case 102/*f*/:
      if (source.charCodeAt(pos - 1) !== 111/*o*/ || source.charCodeAt(pos - 2) !== 101/*e*/)
        return false;
      switch (source.charCodeAt(pos - 3)) {
        case 99/*c*/:
          // instanceof
          return readPrecedingKeyword(pos - 4, 'instan');
        case 112/*p*/:
          // typeof
          return readPrecedingKeyword(pos - 4, 'ty');
        default:
          return false;
      }
    case 110/*n*/:
      // in, return
      return readPrecedingKeyword1(pos - 1, 105/*i*/) || readPrecedingKeyword(pos - 1, 'retur');
    case 111/*o*/:
      // do
      return readPrecedingKeyword1(pos - 1, 100/*d*/);
    case 114/*r*/:
      // debugger
      return readPrecedingKeyword(pos - 1, 'debugge');
    case 116/*t*/:
      // await
      return readPrecedingKeyword(pos - 1, 'awai');
    case 119/*w*/:
      switch (source.charCodeAt(pos - 1)) {
        case 101/*e*/:
          // new
          return readPrecedingKeyword1(pos - 2, 110/*n*/);
        case 111/*o*/:
          // throw
          return readPrecedingKeyword(pos - 2, 'thr');
        default:
          return false;
      }
  }
  return false;
}

function isParenKeyword (curPos) {
  return source.charCodeAt(curPos) === 101/*e*/ && source.startsWith('whil', curPos - 4) ||
      source.charCodeAt(curPos) === 114/*r*/ && source.startsWith('fo', curPos - 2) ||
      source.charCodeAt(curPos - 1) === 105/*i*/ && source.charCodeAt(curPos) === 102/*f*/;
}

function isPunctuator (ch) {
  // 23 possible punctuator endings: !%&()*+,-./:;<=>?[]^{}|~
  return ch === 33/*!*/ || ch === 37/*%*/ || ch === 38/*&*/ ||
    ch > 39 && ch < 48 || ch > 57 && ch < 64 ||
    ch === 91/*[*/ || ch === 93/*]*/ || ch === 94/*^*/ ||
    ch > 122 && ch < 127;
}

function isExpressionPunctuator (ch) {
  // 20 possible expression endings: !%&(*+,-.:;<=>?[^{|~
  return ch === 33/*!*/ || ch === 37/*%*/ || ch === 38/*&*/ ||
    ch > 39 && ch < 47 && ch !== 41 || ch > 57 && ch < 64 ||
    ch === 91/*[*/ || ch === 94/*^*/ || ch > 122 && ch < 127 && ch !== 125/*}*/;
}

function isExpressionTerminator (curPos) {
  // detects:
  // => ; ) finally catch else
  // as all of these followed by a { will indicate a statement brace
  switch (source.charCodeAt(curPos)) {
    case 62/*>*/:
      return source.charCodeAt(curPos - 1) === 61/*=*/;
    case 59/*;*/:
    case 41/*)*/:
      return true;
    case 104/*h*/:
      return source.startsWith('catc', curPos - 4);
    case 121/*y*/:
      return source.startsWith('finall', curPos - 6);
    case 101/*e*/:
      return source.startsWith('els', curPos - 3);
  }
  return false;
}

function syntaxError () {
  throw Object.assign(new Error(`Parse error ${name}:${source.slice(0, pos).split('\n').length}:${pos - source.lastIndexOf('\n', pos - 1)}`), { idx: pos });
}