Hymn Script
Home Learn Play Download Source Light

hymn.js


/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

const HYMN_VERSION = '0.11.0'

const UINT8_MAX = 255
const UINT16_MAX = 65535
const HYMN_UINT8_COUNT = UINT8_MAX + 1
const HYMN_FRAMES_MAX = 64

const HYMN_VALUE_NONE = 1
const HYMN_VALUE_BOOL = 2
const HYMN_VALUE_INTEGER = 3
const HYMN_VALUE_FLOAT = 4
const HYMN_VALUE_STRING = 5
const HYMN_VALUE_ARRAY = 6
const HYMN_VALUE_TABLE = 7
const HYMN_VALUE_FUNC = 8
const HYMN_VALUE_FUNC_NATIVE = 9
const HYMN_VALUE_POINTER = 10

const node = typeof window === 'undefined'
const nodeFs = node ? require('fs') : null
const nodePath = node ? require('path') : null
const nodeProcess = node ? require('process') : null

const LOAD_FACTOR = 0.8
const INITIAL_BINS = 1 << 3
const MAXIMUM_BINS = 1 << 30

class HymnValue {
  constructor(is, value) {
    this.is = is
    this.value = value
  }
}

function copyValueToFrom(to, from) {
  to.is = from.is
  to.value = from.value
}

function copyValue(original) {
  const value = new HymnValue()
  copyValueToFrom(value, original)
  return value
}

class HymnTableItem {
  constructor(hash, key, value) {
    this.hash = hash
    this.key = key
    this.value = value
    this.next = null
  }
}

class HymnTable {
  constructor() {
    this.size = 0
    this.bins = INITIAL_BINS
    this.items = new Array(this.bins).fill(null)
  }
}

class HymnNativeFunction {
  constructor(name, func) {
    this.name = name
    this.func = func
  }
}

class HymnByteCode {
  constructor() {
    this.count = 0
    this.instructions = new Uint8Array(128)
    this.lines = new Uint32Array(128)
    this.constants = []
  }
}

class ExceptList {
  constructor() {
    this.start = 0
    this.end = 0
    this.stack = 0
    this.next = null
  }
}

class HymnFunction {
  constructor() {
    this.count = 0
    this.name = null
    this.script = null
    this.source = null
    this.arity = 0
    this.code = null
    this.except = null
  }
}

class HymnFrame {
  constructor() {
    this.func = null
    this.ip = 0
    this.stack = 0
  }
}

function printOut(text) {
  if (node) nodeProcess.stdout.write(text)
  else console.log(text)
}

function printError(text) {
  if (node) nodeProcess.stderr.write(text)
  else console.error(text)
}

function printLine(text) {
  console.log(text)
}

class Hymn {
  constructor() {
    this.stack = []
    this.stackTop = 0
    this.frames = []
    this.frameCount = 0
    this.globals = new HymnTable()
    this.paths = []
    this.imports = new HymnTable()
    this.error = null
    this.print = printOut
    this.printError = printError
    this.printLine = printLine
  }
}

const TOKEN_ADD = 0
const TOKEN_AND = 1
const TOKEN_ASSIGN = 2
const TOKEN_ASSIGN_ADD = 3
const TOKEN_ASSIGN_BIT_AND = 4
const TOKEN_ASSIGN_BIT_LEFT_SHIFT = 5
const TOKEN_ASSIGN_BIT_OR = 6
const TOKEN_ASSIGN_BIT_RIGHT_SHIFT = 7
const TOKEN_ASSIGN_BIT_XOR = 8
const TOKEN_ASSIGN_DIVIDE = 9
const TOKEN_ASSIGN_MODULO = 10
const TOKEN_ASSIGN_MULTIPLY = 11
const TOKEN_ASSIGN_SUBTRACT = 12
const TOKEN_BIT_AND = 13
const TOKEN_BIT_LEFT_SHIFT = 14
const TOKEN_BIT_NOT = 15
const TOKEN_BIT_OR = 16
const TOKEN_BIT_RIGHT_SHIFT = 17
const TOKEN_BIT_XOR = 18
const TOKEN_BREAK = 19
const TOKEN_CLEAR = 20
const TOKEN_COLON = 21
const TOKEN_COMMA = 22
const TOKEN_CONTINUE = 23
const TOKEN_COPY = 24
const TOKEN_DELETE = 25
const TOKEN_DIVIDE = 26
const TOKEN_DOT = 27
const TOKEN_ECHO = 28
const TOKEN_ELIF = 29
const TOKEN_ELSE = 30
const TOKEN_EOF = 31
const TOKEN_EQUAL = 32
const TOKEN_ERROR = 33
const TOKEN_EXCEPT = 34
const TOKEN_EXISTS = 35
const TOKEN_FALSE = 36
const TOKEN_FLOAT = 37
const TOKEN_FOR = 38
const TOKEN_FUNCTION = 39
const TOKEN_GREATER = 40
const TOKEN_GREATER_EQUAL = 41
const TOKEN_IDENT = 42
const TOKEN_IF = 43
const TOKEN_IN = 44
const TOKEN_INDEX = 45
const TOKEN_INSERT = 46
const TOKEN_SRC = 47
const TOKEN_INTEGER = 48
const TOKEN_KEYS = 49
const TOKEN_LEFT_CURLY = 50
const TOKEN_LEFT_PAREN = 51
const TOKEN_LEFT_SQUARE = 52
const TOKEN_LEN = 53
const TOKEN_LESS = 54
const TOKEN_LESS_EQUAL = 55
const TOKEN_SET = 56
const TOKEN_MODULO = 57
const TOKEN_MULTIPLY = 58
const TOKEN_NONE = 59
const TOKEN_NOT = 60
const TOKEN_NOT_EQUAL = 61
const TOKEN_OR = 62
const TOKEN_POINTER = 63
const TOKEN_POP = 64
const TOKEN_PRINT = 65
const TOKEN_PUSH = 66
const TOKEN_RETURN = 67
const TOKEN_RIGHT_CURLY = 68
const TOKEN_RIGHT_PAREN = 69
const TOKEN_RIGHT_SQUARE = 70
const TOKEN_STRING = 71
const TOKEN_SUBTRACT = 72
const TOKEN_THROW = 73
const TOKEN_TO_FLOAT = 74
const TOKEN_TO_INTEGER = 75
const TOKEN_TO_STRING = 76
const TOKEN_TRUE = 77
const TOKEN_TRY = 78
const TOKEN_TYPE_FUNC = 79
const TOKEN_UNDEFINED = 80
const TOKEN_USE = 81
const TOKEN_VALUE = 82
const TOKEN_WHILE = 83
const TOKEN_OPCODES = 84
const TOKEN_STACK = 85
const TOKEN_REFERENCE = 86
const TOKEN_FORMAT = 87

const PRECEDENCE_NONE = 0
const PRECEDENCE_ASSIGN = 1
const PRECEDENCE_BITS = 2
const PRECEDENCE_OR = 3
const PRECEDENCE_AND = 4
const PRECEDENCE_EQUALITY = 5
const PRECEDENCE_COMPARE = 6
const PRECEDENCE_TERM = 7
const PRECEDENCE_FACTOR = 8
const PRECEDENCE_UNARY = 9
const PRECEDENCE_CALL = 10

const STRING_STATUS_NONE = 0
const STRING_STATUS_BEGIN = 1
const STRING_STATUS_ADD = 2
const STRING_STATUS_CLOSE = 3
const STRING_STATUS_CONTINUE = 4

const OP_ADD = 0
const OP_INSERT = 1
const OP_ARRAY_POP = 2
const OP_ARRAY_PUSH = 3
const OP_BIT_AND = 4
const OP_BIT_LEFT_SHIFT = 5
const OP_BIT_NOT = 6
const OP_BIT_OR = 7
const OP_BIT_RIGHT_SHIFT = 8
const OP_BIT_XOR = 9
const OP_CALL = 10
const OP_CLEAR = 11
const OP_CONSTANT = 12
const OP_COPY = 13
const OP_DEFINE_GLOBAL = 14
const OP_DELETE = 15
const OP_DIVIDE = 16
const OP_DUPLICATE = 17
const OP_ECHO = 18
const OP_EQUAL = 19
const OP_EXISTS = 20
const OP_FALSE = 21
const OP_FLOAT = 22
const OP_FOR = 23
const OP_FOR_LOOP = 24
const OP_GET_DYNAMIC = 25
const OP_GET_GLOBAL = 26
const OP_GET_LOCAL = 27
const OP_GET_PROPERTY = 28
const OP_GREATER = 29
const OP_GREATER_EQUAL = 30
const OP_INCREMENT_LOCAL_AND_SET = 31
const OP_INDEX = 32
const OP_SOURCE = 33
const OP_INT = 34
const OP_JUMP = 35
const OP_JUMP_IF_FALSE = 36
const OP_JUMP_IF_TRUE = 37
const OP_KEYS = 38
const OP_LEN = 39
const OP_LESS = 40
const OP_LESS_EQUAL = 41
const OP_LOOP = 42
const OP_MODULO = 43
const OP_MULTIPLY = 44
const OP_NEGATE = 45
const OP_NEW_ARRAY = 46
const OP_NEW_TABLE = 47
const OP_NONE = 48
const OP_NOT = 49
const OP_NOT_EQUAL = 50
const OP_POP = 51
const OP_PRINT = 52
const OP_RETURN = 53
const OP_SELF = 54
const OP_SET_DYNAMIC = 55
const OP_SET_GLOBAL = 56
const OP_SET_LOCAL = 57
const OP_SET_PROPERTY = 58
const OP_SLICE = 59
const OP_STRING = 60
const OP_SUBTRACT = 61
const OP_THROW = 62
const OP_TRUE = 63
const OP_TYPE = 64
const OP_USE = 65
const OP_CODES = 66
const OP_STACK = 67
const OP_REFERENCE = 68
const OP_FORMAT = 69

const TYPE_FUNCTION = 0
const TYPE_SCRIPT = 1
const TYPE_DIRECT = 2

class Token {
  constructor() {
    this.type = TOKEN_UNDEFINED
    this.row = 0
    this.column = 0
    this.start = 0
    this.length = 0
    this.number = 0
  }
}

function copyToken(dest, src) {
  dest.type = src.type
  dest.row = src.row
  dest.column = src.column
  dest.start = src.start
  dest.length = src.length
  dest.number = src.number
}

class Local {
  constructor() {
    this.name = null
    this.depth = 0
  }
}

class Rule {
  constructor(prefix, infix, precedence) {
    this.prefix = prefix
    this.infix = infix
    this.precedence = precedence
  }
}

class Scope {
  constructor() {
    this.enclosing = null
    this.func = null
    this.type = TYPE_FUNCTION
    this.begin = 0
    this.locals = []
    this.localCount = 0
    this.depth = 0
  }
}

class Compiler {
  constructor(script, source, H) {
    this.pos = 0
    this.row = 1
    this.column = 1
    this.script = script
    this.source = source
    this.previous = new Token()
    this.current = new Token()
    this.stringFormat = 0
    this.stringStatus = STRING_STATUS_NONE
    this.H = H
    this.scope = null
    this.pop = -1
    this.barrier = -1
    this.loop = null
    this.jump = null
    this.jumpOr = null
    this.jumpAnd = null
    this.jumpFor = null
    this.error = null
  }
}

class JumpList {
  constructor() {
    this.jump = 0
    this.depth = 0
    this.next = null
  }
}

class LoopList {
  constructor(start, depth, next, isFor) {
    this.start = start
    this.depth = depth
    this.next = next
    this.isFor = isFor
  }
}

const rules = []
rules[TOKEN_ADD] = new Rule(null, compileBinary, PRECEDENCE_TERM)
rules[TOKEN_AND] = new Rule(null, compileAnd, PRECEDENCE_AND)
rules[TOKEN_ASSIGN] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_ASSIGN_ADD] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_ASSIGN_BIT_AND] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_ASSIGN_BIT_LEFT_SHIFT] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_ASSIGN_BIT_OR] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_ASSIGN_BIT_RIGHT_SHIFT] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_ASSIGN_BIT_XOR] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_ASSIGN_DIVIDE] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_ASSIGN_MODULO] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_ASSIGN_MULTIPLY] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_ASSIGN_SUBTRACT] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_BIT_AND] = new Rule(null, compileBinary, PRECEDENCE_BITS)
rules[TOKEN_BIT_LEFT_SHIFT] = new Rule(null, compileBinary, PRECEDENCE_BITS)
rules[TOKEN_BIT_NOT] = new Rule(compileUnary, null, PRECEDENCE_NONE)
rules[TOKEN_BIT_OR] = new Rule(null, compileBinary, PRECEDENCE_BITS)
rules[TOKEN_BIT_RIGHT_SHIFT] = new Rule(null, compileBinary, PRECEDENCE_BITS)
rules[TOKEN_BIT_XOR] = new Rule(null, compileBinary, PRECEDENCE_BITS)
rules[TOKEN_BREAK] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_CLEAR] = new Rule(clearExpression, null, PRECEDENCE_NONE)
rules[TOKEN_COLON] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_COMMA] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_CONTINUE] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_COPY] = new Rule(copyExpression, null, PRECEDENCE_NONE)
rules[TOKEN_OPCODES] = new Rule(opCodesExpression, null, PRECEDENCE_NONE)
rules[TOKEN_STACK] = new Rule(stackExpression, null, PRECEDENCE_NONE)
rules[TOKEN_REFERENCE] = new Rule(referenceExpression, null, PRECEDENCE_NONE)
rules[TOKEN_DELETE] = new Rule(deleteExpression, null, PRECEDENCE_NONE)
rules[TOKEN_DIVIDE] = new Rule(null, compileBinary, PRECEDENCE_FACTOR)
rules[TOKEN_DOT] = new Rule(null, compileDot, PRECEDENCE_CALL)
rules[TOKEN_ECHO] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_ELIF] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_ELSE] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_EOF] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_EQUAL] = new Rule(null, compileBinary, PRECEDENCE_EQUALITY)
rules[TOKEN_ERROR] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_EXCEPT] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_EXISTS] = new Rule(existsExpression, null, PRECEDENCE_NONE)
rules[TOKEN_FALSE] = new Rule(compileFalse, null, PRECEDENCE_NONE)
rules[TOKEN_FLOAT] = new Rule(compileFloat, null, PRECEDENCE_NONE)
rules[TOKEN_FOR] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_FORMAT] = new Rule(formatExpression, null, PRECEDENCE_NONE)
rules[TOKEN_FUNCTION] = new Rule(functionExpression, null, PRECEDENCE_NONE)
rules[TOKEN_GREATER] = new Rule(null, compileBinary, PRECEDENCE_COMPARE)
rules[TOKEN_GREATER_EQUAL] = new Rule(null, compileBinary, PRECEDENCE_COMPARE)
rules[TOKEN_IDENT] = new Rule(compileVariable, null, PRECEDENCE_NONE)
rules[TOKEN_IF] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_IN] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_INDEX] = new Rule(indexExpression, null, PRECEDENCE_NONE)
rules[TOKEN_INSERT] = new Rule(arrayInsertExpression, null, PRECEDENCE_NONE)
rules[TOKEN_SRC] = new Rule(sourceExpression, null, PRECEDENCE_NONE)
rules[TOKEN_INTEGER] = new Rule(compileInteger, null, PRECEDENCE_NONE)
rules[TOKEN_KEYS] = new Rule(keysExpression, null, PRECEDENCE_NONE)
rules[TOKEN_LEFT_CURLY] = new Rule(compileTable, null, PRECEDENCE_NONE)
rules[TOKEN_LEFT_PAREN] = new Rule(compileGroup, compileCall, PRECEDENCE_CALL)
rules[TOKEN_LEFT_SQUARE] = new Rule(compileArray, compileSquare, PRECEDENCE_CALL)
rules[TOKEN_LEN] = new Rule(lenExpression, null, PRECEDENCE_NONE)
rules[TOKEN_LESS] = new Rule(null, compileBinary, PRECEDENCE_COMPARE)
rules[TOKEN_LESS_EQUAL] = new Rule(null, compileBinary, PRECEDENCE_COMPARE)
rules[TOKEN_SET] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_MODULO] = new Rule(null, compileBinary, PRECEDENCE_FACTOR)
rules[TOKEN_MULTIPLY] = new Rule(null, compileBinary, PRECEDENCE_FACTOR)
rules[TOKEN_NONE] = new Rule(compileNone, null, PRECEDENCE_NONE)
rules[TOKEN_NOT] = new Rule(compileUnary, null, PRECEDENCE_NONE)
rules[TOKEN_NOT_EQUAL] = new Rule(null, compileBinary, PRECEDENCE_EQUALITY)
rules[TOKEN_OR] = new Rule(null, compileOr, PRECEDENCE_OR)
rules[TOKEN_POINTER] = new Rule(null, compilePointer, PRECEDENCE_CALL)
rules[TOKEN_POP] = new Rule(arrayPopExpression, null, PRECEDENCE_NONE)
rules[TOKEN_PRINT] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_PUSH] = new Rule(arrayPushExpression, null, PRECEDENCE_NONE)
rules[TOKEN_RETURN] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_RIGHT_CURLY] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_RIGHT_PAREN] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_RIGHT_SQUARE] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_STRING] = new Rule(compileString, null, PRECEDENCE_NONE)
rules[TOKEN_SUBTRACT] = new Rule(compileUnary, compileBinary, PRECEDENCE_TERM)
rules[TOKEN_THROW] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_TO_FLOAT] = new Rule(castFloatExpression, null, PRECEDENCE_NONE)
rules[TOKEN_TO_INTEGER] = new Rule(castIntegerExpression, null, PRECEDENCE_NONE)
rules[TOKEN_TO_STRING] = new Rule(castStringExpression, null, PRECEDENCE_NONE)
rules[TOKEN_TRUE] = new Rule(compileTrue, null, PRECEDENCE_NONE)
rules[TOKEN_TRY] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_TYPE_FUNC] = new Rule(typeExpression, null, PRECEDENCE_NONE)
rules[TOKEN_UNDEFINED] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_USE] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_VALUE] = new Rule(null, null, PRECEDENCE_NONE)
rules[TOKEN_WHILE] = new Rule(null, null, PRECEDENCE_NONE)

function valueType(type) {
  switch (type) {
    case HYMN_VALUE_NONE:
      return 'none'
    case HYMN_VALUE_BOOL:
      return 'boolean'
    case HYMN_VALUE_INTEGER:
      return 'integer'
    case HYMN_VALUE_FLOAT:
      return 'float'
    case HYMN_VALUE_STRING:
      return 'string'
    case HYMN_VALUE_ARRAY:
      return 'array'
    case HYMN_VALUE_TABLE:
      return 'table'
    case HYMN_VALUE_FUNC:
      return 'function'
    case HYMN_VALUE_FUNC_NATIVE:
      return 'native'
    case HYMN_VALUE_POINTER:
      return 'pointer'
    default:
      return '?'
  }
}

function stringMixHashCode(key) {
  const length = key.length
  let hash = 0
  for (let i = 0; i < length; i++) {
    hash = 31 * hash + key.charCodeAt(i)
    hash |= 0
  }
  return hash ^ (hash >> 16)
}

function tableGetBin(table, hash) {
  return (table.bins - 1) & hash
}

function tableResize(table) {
  const binsOld = table.bins
  const bins = binsOld << 1

  if (bins > MAXIMUM_BINS) return

  const itemsOld = table.items
  const items = new Array(bins).fill(null)

  for (let i = 0; i < binsOld; i++) {
    let item = itemsOld[i]
    if (item === null) continue
    if (item.next === null) {
      items[(bins - 1) & item.hash] = item
    } else {
      let lowHead = null
      let lowTail = null
      let highHead = null
      let highTail = null
      do {
        if ((binsOld & item.hash) === 0) {
          if (lowTail === null) lowHead = item
          else lowTail.next = item
          lowTail = item
        } else {
          if (highTail === null) highHead = item
          else highTail.next = item
          highTail = item
        }
        item = item.next
      } while (item !== null)

      if (lowTail !== null) {
        lowTail.next = null
        items[i] = lowHead
      }

      if (highTail !== null) {
        highTail.next = null
        items[i + binsOld] = highHead
      }
    }
  }

  table.bins = bins
  table.items = items
}

function tablePut(table, key, value) {
  const hash = stringMixHashCode(key)
  const bin = tableGetBin(table, hash)
  let item = table.items[bin]
  let previous = null
  while (item !== null) {
    if (key === item.key) {
      const old = item.value
      item.value = value
      return old
    }
    previous = item
    item = item.next
  }
  item = new HymnTableItem(hash, key, value)
  if (previous === null) table.items[bin] = item
  else previous.next = item
  table.size++
  if (table.size > table.bins * LOAD_FACTOR) tableResize(table)
  return null
}

function tableGet(table, key) {
  const hash = stringMixHashCode(key)
  const bin = tableGetBin(table, hash)
  let item = table.items[bin]
  while (item !== null) {
    if (key === item.key) return item.value
    item = item.next
  }
  return null
}

function tableNext(table, key) {
  const bins = table.bins
  if (key === null) {
    for (let i = 0; i < bins; i++) {
      const item = table.items[i]
      if (item !== null) return item
    }
    return null
  }
  const hash = stringMixHashCode(key)
  const bin = tableGetBin(table, hash)
  {
    let item = table.items[bin]
    while (item !== null) {
      const next = item.next
      if (key === item.key) {
        if (next !== null) return next
      }
      item = next
    }
  }
  for (let i = bin + 1; i < bins; i++) {
    const item = table.items[i]
    if (item !== null) return item
  }
  return null
}

function tableRemove(table, key) {
  const hash = stringMixHashCode(key)
  const bin = tableGetBin(table, hash)
  let item = table.items[bin]
  let previous = null
  while (item !== null) {
    if (key === item.key) {
      if (previous === null) table.items[bin] = item.next
      else previous.next = item.next
      const value = item.value
      table.size--
      return value
    }
    previous = item
    item = item.next
  }
  return null
}

function tableClear(table) {
  table.size = 0
  const bins = table.bins
  for (let i = 0; i < bins; i++) {
    let item = table.items[i]
    while (item !== null) {
      const next = item.next
      item.next = null
      item = next
    }
    table.items[i] = null
  }
}

function newTableCopy(from) {
  const copy = new HymnTable()
  const bins = from.bins
  for (let i = 0; i < bins; i++) {
    let item = from.items[i]
    while (item !== null) {
      tablePut(copy, item.key, copyValue(item.value))
      item = item.next
    }
  }
  return copy
}

function stringCompare(a, b) {
  return a === b ? 0 : a > b ? 1 : -1
}

function tableKeys(table) {
  const size = table.size
  const keys = new Array(size)
  if (size === 0) return keys
  let total = 0
  const bins = table.bins
  for (let i = 0; i < bins; i++) {
    let item = table.items[i]
    while (item !== null) {
      const key = item.key
      let insert = 0
      while (insert !== total) {
        if (stringCompare(key, keys[insert].value) < 0) {
          for (let swap = total; swap > insert; swap--) {
            keys[swap] = keys[swap - 1]
          }
          break
        }
        insert++
      }
      keys[insert] = newString(item.key)
      total++
      item = item.next
    }
  }
  return keys
}

function tableKeyOf(table, input) {
  let bin = 0
  let item = null

  const bins = table.bins
  for (let i = 0; i < bins; i++) {
    const start = table.items[i]
    if (start) {
      bin = i
      item = start
      break
    }
  }

  if (item === null) return null
  if (matchValues(input, item.value)) return item.key

  while (true) {
    item = item.next
    if (item === null) {
      for (bin = bin + 1; bin < bins; bin++) {
        const start = table.items[bin]
        if (start) {
          item = start
          break
        }
      }
      if (item === null) return null
    }
    if (matchValues(input, item.value)) return item.key
  }
}

function sourceSubstring(C, len, start) {
  return C.source.substring(start, start + len)
}

function newNone() {
  return new HymnValue(HYMN_VALUE_NONE, null)
}

function newBool(boolean) {
  return new HymnValue(HYMN_VALUE_BOOL, boolean)
}

function newInt(number) {
  return new HymnValue(HYMN_VALUE_INTEGER, number)
}

function newFloat(number) {
  return new HymnValue(HYMN_VALUE_FLOAT, number)
}

function newString(string) {
  return new HymnValue(HYMN_VALUE_STRING, string)
}

function newArrayValue(array) {
  return new HymnValue(HYMN_VALUE_ARRAY, array)
}

function newTableValue(table) {
  return new HymnValue(HYMN_VALUE_TABLE, table)
}

function newFuncValue(func) {
  return new HymnValue(HYMN_VALUE_FUNC, func)
}

function newNativeFuncValue(name, func) {
  return new HymnValue(HYMN_VALUE_FUNC_NATIVE, new HymnNativeFunction(name, func))
}

function newPointerValue(pointer) {
  return new HymnValue(HYMN_VALUE_POINTER, pointer)
}

function newFunction(script) {
  const func = new HymnFunction()
  func.code = new HymnByteCode()
  if (script) func.script = script
  return func
}

function isNone(value) {
  return value.is === HYMN_VALUE_NONE
}

function isBool(value) {
  return value.is === HYMN_VALUE_BOOL
}

function isInt(value) {
  return value.is === HYMN_VALUE_INTEGER
}

function isFloat(value) {
  return value.is === HYMN_VALUE_FLOAT
}

function isString(value) {
  return value.is === HYMN_VALUE_STRING
}

function isArray(value) {
  return value.is === HYMN_VALUE_ARRAY
}

function isTable(value) {
  return value.is === HYMN_VALUE_TABLE
}

function isFunc(value) {
  return value.is === HYMN_VALUE_FUNC
}

function isFuncNative(value) {
  return value.is === HYMN_VALUE_FUNC_NATIVE
}

function isPointer(value) {
  return value.is === HYMN_VALUE_POINTER
}

function current(C) {
  return C.scope.func.code
}

function compileError(C, token, format) {
  if (C.error !== null) return

  let error = format

  if (token.type !== TOKEN_EOF && token.length > 0) {
    const source = C.source

    let begin = token.start
    while (true) {
      if (source[begin] === '\n') {
        begin++
        break
      }
      if (begin === 0) break
      begin--
    }

    while (true) {
      if (source[begin] !== ' ' || begin === source.length) break
      begin++
    }

    let end = token.start
    while (true) {
      if (source[end] === '\n' || end === source.length) break
      end++
    }

    if (begin < end) {
      error += '\n  ' + source.substring(begin, end) + '\n  '
      const spaces = token.start - begin
      if (spaces > 0) for (let i = 0; i < spaces; i++) error += ' '
      for (let i = 0; i < token.length; i++) error += '^'
    }
  }

  error += '\n  at ' + (C.script === null ? 'script' : C.script) + ':' + token.row

  C.error = error

  C.previous.type = TOKEN_EOF
  C.current.type = TOKEN_EOF
}

function nextChar(C) {
  const pos = C.pos
  if (pos === C.source.length) return '\0'
  const c = C.source[pos]
  C.pos = pos + 1
  if (c === '\n') {
    C.row++
    C.column = 0
  } else {
    C.column++
  }
  return c
}

function peekChar(C) {
  if (C.pos === C.source.length) return '\0'
  return C.source[C.pos]
}

function peekTwoChar(C) {
  if (C.pos + 1 >= C.source.length) return '\0'
  return C.source[C.pos + 1]
}

function token(C, type) {
  const token = C.current
  token.type = type
  token.row = C.row
  token.column = C.column
  token.start = C.pos - 1
  token.length = 1
}

function tokenSpecial(C, type, offset, len) {
  const token = C.current
  token.type = type
  token.row = C.row
  token.column = C.column
  token.start = C.pos - offset
  token.length = len
}

function valueToken(C, type, start, end) {
  const token = C.current
  token.type = type
  token.row = C.row
  token.column = C.column
  token.start = start
  token.length = end - start
}

function numberToken(C, type, start, end, value) {
  const token = C.current
  token.type = type
  token.row = C.row
  token.column = C.column
  token.start = start
  token.length = end - start
  token.number = value
}

function identTrie(ident, offset, rest, type) {
  for (let i = 0; i < rest.length; i++) {
    if (ident[offset + i] !== rest[i]) {
      return TOKEN_UNDEFINED
    }
  }
  return type
}

function identKey(ident, size) {
  switch (ident[0]) {
    case 'o':
      if (size === 2) return identTrie(ident, 1, 'r', TOKEN_OR)
      break
    case 'u':
      if (size === 3) return identTrie(ident, 1, 'se', TOKEN_USE)
      break
    case 'a':
      if (size === 3) return identTrie(ident, 1, 'nd', TOKEN_AND)
      break
    case 'n':
      if (size === 3) return identTrie(ident, 1, 'ot', TOKEN_NOT)
      if (size === 4) return identTrie(ident, 1, 'one', TOKEN_NONE)
      break
    case 'w':
      if (size === 5) return identTrie(ident, 1, 'hile', TOKEN_WHILE)
      break
    case 'b':
      if (size === 5) return identTrie(ident, 1, 'reak', TOKEN_BREAK)
      break
    case 'd':
      if (size === 6) return identTrie(ident, 1, 'elete', TOKEN_DELETE)
      break
    case 'r':
      if (size === 6) return identTrie(ident, 1, 'eturn', TOKEN_RETURN)
      break
    case 's':
      if (size === 3) {
        if (ident[1] === 'e' && ident[2] === 't') return TOKEN_SET
        if (ident[1] === 't' && ident[2] === 'r') return TOKEN_TO_STRING
      }
      break
    case 'k':
      if (size === 4) return identTrie(ident, 1, 'eys', TOKEN_KEYS)
      break
    case 'c':
      if (size === 4) return identTrie(ident, 1, 'opy', TOKEN_COPY)
      if (size === 5) return identTrie(ident, 1, 'lear', TOKEN_CLEAR)
      if (size === 8) return identTrie(ident, 1, 'ontinue', TOKEN_CONTINUE)
      break
    case 'l':
      if (size === 3) return identTrie(ident, 1, 'en', TOKEN_LEN)
      break
    case 't':
      if (size === 3) return identTrie(ident, 1, 'ry', TOKEN_TRY)
      if (size === 5) return identTrie(ident, 1, 'hrow', TOKEN_THROW)
      if (size === 4) {
        if (ident[1] === 'r') return identTrie(ident, 2, 'ue', TOKEN_TRUE)
        if (ident[1] === 'y') return identTrie(ident, 2, 'pe', TOKEN_TYPE_FUNC)
      }
      break
    case 'i':
      if (size === 3) return identTrie(ident, 1, 'nt', TOKEN_TO_INTEGER)
      if (size === 5) return identTrie(ident, 1, 'ndex', TOKEN_INDEX)
      if (size === 6) return identTrie(ident, 1, 'nsert', TOKEN_INSERT)
      if (size === 2) {
        if (ident[1] === 'f') return TOKEN_IF
        if (ident[1] === 'n') return TOKEN_IN
      }
      break
    case 'p':
      if (size === 3) return identTrie(ident, 1, 'op', TOKEN_POP)
      if (size === 5) return identTrie(ident, 1, 'rint', TOKEN_PRINT)
      if (size === 4) return identTrie(ident, 1, 'ush', TOKEN_PUSH)
      break
    case 'e':
      if (size === 6) {
        if (ident[1] === 'x') {
          if (ident[2] === 'c') return identTrie(ident, 3, 'ept', TOKEN_EXCEPT)
          if (ident[2] === 'i') return identTrie(ident, 3, 'sts', TOKEN_EXISTS)
        }
      } else if (size === 4) {
        if (ident[1] === 'l') {
          if (ident[2] === 's') {
            if (ident[3] === 'e') {
              return TOKEN_ELSE
            }
          } else if (ident[2] === 'i' && ident[3] === 'f') {
            return TOKEN_ELIF
          }
        } else if (ident[1] === 'c') {
          return identTrie(ident, 2, 'ho', TOKEN_ECHO)
        }
      }
      break
    case 'f':
      if (size === 3) return identTrie(ident, 1, 'or', TOKEN_FOR)
      if (size === 4) return identTrie(ident, 1, 'unc', TOKEN_FUNCTION)
      if (size === 5) {
        if (ident[1] === 'a') return identTrie(ident, 2, 'lse', TOKEN_FALSE)
        if (ident[1] === 'l') return identTrie(ident, 2, 'oat', TOKEN_TO_FLOAT)
      }
      break
    case 'S':
      if (size === 5) return identTrie(ident, 1, 'TACK', TOKEN_STACK)
      if (size === 6) return identTrie(ident, 1, 'OURCE', TOKEN_SRC)
      break
    case 'F':
      if (size === 6) return identTrie(ident, 1, 'ORMAT', TOKEN_FORMAT)
      break
    case 'O':
      if (size === 7) return identTrie(ident, 1, 'PCODES', TOKEN_OPCODES)
      break
    case 'R':
      if (size === 9) return identTrie(ident, 1, 'EFERENCE', TOKEN_REFERENCE)
      break
  }
  return TOKEN_UNDEFINED
}

function pushIdentToken(C, start, end) {
  const ident = C.source.substring(start, end)
  const keyword = identKey(ident, end - start)
  if (keyword !== TOKEN_UNDEFINED) {
    valueToken(C, keyword, start, end)
  } else {
    valueToken(C, TOKEN_IDENT, start, end)
  }
}

function isDigit(c) {
  return c >= '0' && c <= '9'
}

function isIdent(c) {
  return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '_'
}

function stringStatus(C) {
  let i = C.pos
  const source = C.source
  const size = source.length
  let expression = false
  let brackets = 1
  while (true) {
    if (i >= size) return false
    switch (source[i]) {
      case '}':
        if (brackets > 1) {
          expression = true
          i++
          brackets--
          continue
        }
        return expression ? STRING_STATUS_BEGIN : STRING_STATUS_CONTINUE
      case '"':
        return STRING_STATUS_NONE
      case ' ':
      case '\t':
      case '\r':
      case '\n':
        i++
        continue
      case '{':
        expression = true
        i++
        brackets++
        continue
      default:
        expression = true
        i++
        continue
    }
  }
}

function parseString(C, start) {
  while (true) {
    const c = nextChar(C)
    if (c === '\\') {
      nextChar(C)
    } else if (c === '$') {
      if (peekChar(C) === '{') {
        nextChar(C)
        const status = stringStatus(C)
        if (status === STRING_STATUS_BEGIN) {
          C.stringFormat = 1
          C.stringStatus = STRING_STATUS_BEGIN
          const end = C.pos - 2
          valueToken(C, TOKEN_STRING, start, end)
          return
        } else if (status === STRING_STATUS_CONTINUE) {
          C.stringStatus = STRING_STATUS_CONTINUE
          const end = C.pos - 2
          valueToken(C, TOKEN_STRING, start, end)
          while (true) {
            const c = nextChar(C)
            if (c === '}' || c === '\0') return
          }
        } else {
          continue
        }
      }
    } else if (c === '"' || c === '\0') {
      break
    }
  }
  const end = C.pos - 1
  valueToken(C, TOKEN_STRING, start, end)
  return
}

function advance(C) {
  copyToken(C.previous, C.current)
  if (C.previous.type === TOKEN_EOF) {
    return
  }
  switch (C.stringStatus) {
    case STRING_STATUS_BEGIN:
      C.stringStatus = STRING_STATUS_ADD
      token(C, TOKEN_ADD)
      return
    case STRING_STATUS_ADD:
      C.stringStatus = STRING_STATUS_NONE
      token(C, TOKEN_LEFT_PAREN)
      return
    case STRING_STATUS_CLOSE:
      C.stringStatus = STRING_STATUS_CONTINUE
      token(C, TOKEN_ADD)
      return
    case STRING_STATUS_CONTINUE: {
      C.stringStatus = STRING_STATUS_NONE
      const start = C.pos
      parseString(C, start)
      return
    }
    default:
      break
  }
  while (true) {
    let c = nextChar(C)
    switch (c) {
      case ' ':
      case '\t':
      case '\r':
      case '\n':
        c = peekChar(C)
        while (c !== '\0' && (c === ' ' || c === '\t' || c === '\r' || c === '\n')) {
          nextChar(C)
          c = peekChar(C)
        }
        continue
      case '#': {
        nextChar(C)
        c = peekChar(C)
        while (c !== '\n' && c !== '\0') {
          nextChar(C)
          c = peekChar(C)
        }
        continue
      }
      case '!':
        if (peekChar(C) === '=') {
          nextChar(C)
          tokenSpecial(C, TOKEN_NOT_EQUAL, 2, 2)
        } else {
          token(C, TOKEN_ERROR)
          compileError(C, C.current, "expected '!='")
        }
        return
      case '=':
        if (peekChar(C) === '=') {
          nextChar(C)
          tokenSpecial(C, TOKEN_EQUAL, 2, 2)
        } else {
          token(C, TOKEN_ASSIGN)
        }
        return
      case '-': {
        if (peekChar(C) === '=') {
          nextChar(C)
          tokenSpecial(C, TOKEN_ASSIGN_SUBTRACT, 2, 2)
          return
        } else if (peekChar(C) === '>') {
          nextChar(C)
          tokenSpecial(C, TOKEN_POINTER, 2, 2)
          return
        } else {
          token(C, TOKEN_SUBTRACT)
          return
        }
      }
      case '+':
        if (peekChar(C) === '=') {
          nextChar(C)
          tokenSpecial(C, TOKEN_ASSIGN_ADD, 2, 2)
        } else {
          token(C, TOKEN_ADD)
        }
        return
      case '*':
        if (peekChar(C) === '=') {
          nextChar(C)
          tokenSpecial(C, TOKEN_ASSIGN_MULTIPLY, 2, 2)
        } else {
          token(C, TOKEN_MULTIPLY)
        }
        return
      case '/':
        if (peekChar(C) === '=') {
          nextChar(C)
          tokenSpecial(C, TOKEN_ASSIGN_DIVIDE, 2, 2)
        } else {
          token(C, TOKEN_DIVIDE)
        }
        return
      case '%':
        if (peekChar(C) === '=') {
          nextChar(C)
          tokenSpecial(C, TOKEN_ASSIGN_MODULO, 2, 2)
        } else {
          token(C, TOKEN_MODULO)
        }
        return
      case '&':
        if (peekChar(C) === '=') {
          nextChar(C)
          tokenSpecial(C, TOKEN_ASSIGN_BIT_AND, 2, 2)
        } else {
          token(C, TOKEN_BIT_AND)
        }
        return
      case '|':
        if (peekChar(C) === '=') {
          nextChar(C)
          tokenSpecial(C, TOKEN_ASSIGN_BIT_OR, 2, 2)
        } else {
          token(C, TOKEN_BIT_OR)
        }
        return
      case '^':
        if (peekChar(C) === '=') {
          nextChar(C)
          tokenSpecial(C, TOKEN_ASSIGN_BIT_XOR, 2, 2)
        } else {
          token(C, TOKEN_BIT_XOR)
        }
        return
      case '>':
        if (peekChar(C) === '=') {
          nextChar(C)
          tokenSpecial(C, TOKEN_GREATER_EQUAL, 2, 2)
        } else if (peekChar(C) === '>') {
          nextChar(C)
          if (peekChar(C) === '=') {
            nextChar(C)
            tokenSpecial(C, TOKEN_ASSIGN_BIT_RIGHT_SHIFT, 2, 2)
          } else {
            tokenSpecial(C, TOKEN_BIT_RIGHT_SHIFT, 2, 2)
          }
        } else {
          token(C, TOKEN_GREATER)
        }
        return
      case '<':
        if (peekChar(C) === '=') {
          nextChar(C)
          tokenSpecial(C, TOKEN_LESS_EQUAL, 2, 2)
        } else if (peekChar(C) === '<') {
          nextChar(C)
          if (peekChar(C) === '=') {
            nextChar(C)
            tokenSpecial(C, TOKEN_ASSIGN_BIT_LEFT_SHIFT, 2, 2)
          } else {
            tokenSpecial(C, TOKEN_BIT_LEFT_SHIFT, 2, 2)
          }
        } else {
          token(C, TOKEN_LESS)
        }
        return
      case '~':
        token(C, TOKEN_BIT_NOT)
        return
      case ',':
        token(C, TOKEN_COMMA)
        return
      case '.':
        token(C, TOKEN_DOT)
        return
      case '(':
        token(C, TOKEN_LEFT_PAREN)
        return
      case ')':
        token(C, TOKEN_RIGHT_PAREN)
        return
      case '[':
        token(C, TOKEN_LEFT_SQUARE)
        return
      case ']':
        token(C, TOKEN_RIGHT_SQUARE)
        return
      case '{':
        if (C.stringFormat > 0) {
          C.stringFormat++
        }
        token(C, TOKEN_LEFT_CURLY)
        return
      case '}':
        if (C.stringFormat === 1) {
          C.stringFormat = 0
          C.stringStatus = STRING_STATUS_CLOSE
          token(C, TOKEN_RIGHT_PAREN)
          return
        } else if (C.stringFormat > 1) {
          C.stringFormat--
        }
        token(C, TOKEN_RIGHT_CURLY)
        return
      case ':':
        token(C, TOKEN_COLON)
        return
      case '\0':
        token(C, TOKEN_EOF)
        return
      case '"': {
        const start = C.pos
        parseString(C, start)
        return
      }
      case "'": {
        const start = C.pos
        while (true) {
          c = nextChar(C)
          if (c === '\\') {
            nextChar(C)
            continue
          } else if (c === "'" || c === '\0') {
            break
          }
        }
        const end = C.pos - 1
        valueToken(C, TOKEN_STRING, start, end)
        return
      }
      default: {
        if (isDigit(c)) {
          const start = C.pos - 1
          if (c === '0') {
            const p = peekChar(C)
            if (p === 'b') {
              nextChar(C)
              while (true) {
                c = peekChar(C)
                if (c !== '0' && c !== '1') {
                  break
                }
                nextChar(C)
              }
              const end = C.pos
              const value = sourceSubstring(C, end - start - 2, start + 2)
              numberToken(C, TOKEN_INTEGER, start, end, parseInt(value, 2))
              return
            } else if (p === 'x') {
              nextChar(C)
              while (true) {
                c = peekChar(C)
                if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f')) {
                  break
                }
                nextChar(C)
              }
              const end = C.pos
              const value = sourceSubstring(C, end - start - 2, start + 2)
              numberToken(C, TOKEN_INTEGER, start, end, parseInt(value, 16))
              return
            }
          }
          while (isDigit(peekChar(C))) {
            nextChar(C)
          }
          const p = peekChar(C)
          if (p === '.') {
            nextChar(C)
            while (isDigit(peekChar(C))) {
              nextChar(C)
            }
            const n = peekChar(C)
            if (n === 'e' || n === 'E') {
              nextChar(C)
              const e = peekChar(C)
              if (e === '-' || e === '+') nextChar(C)
              while (isDigit(peekChar(C))) {
                nextChar(C)
              }
            }
          } else if (p === 'e' || p === 'E') {
            nextChar(C)
            const n = peekChar(C)
            if (n === '-' || n === '+') nextChar(C)
            while (isDigit(peekChar(C))) {
              nextChar(C)
            }
          }
          const end = C.pos
          const value = sourceSubstring(C, end - start, start)
          const number = Number(value)
          if (Number.isSafeInteger(number)) numberToken(C, TOKEN_INTEGER, start, end, number)
          else numberToken(C, TOKEN_FLOAT, start, end, number)
          return
        } else if (isIdent(c)) {
          const start = C.pos - 1
          while (true) {
            c = peekChar(C)
            if (isIdent(c)) {
              nextChar(C)
              continue
            } else if (c === '-') {
              if (isIdent(peekTwoChar(C))) {
                nextChar(C)
                nextChar(C)
                continue
              }
            }
            break
          }
          const end = C.pos
          pushIdentToken(C, start, end)
          return
        } else {
          token(C, TOKEN_ERROR)
          compileError(C, C.current, 'unknown character: ' + c)
        }
      }
    }
  }
}

function hymnFalse(value) {
  switch (value.is) {
    case HYMN_VALUE_NONE:
      return true
    case HYMN_VALUE_BOOL:
      return !value.value
    case HYMN_VALUE_INTEGER:
      return value.value === 0
    case HYMN_VALUE_FLOAT:
      return value.value === 0.0
    case HYMN_VALUE_STRING:
    case HYMN_VALUE_ARRAY:
      return value.value.length === 0
    case HYMN_VALUE_TABLE:
      return value.value.size === 0
    default:
      return false
  }
}

function hymnEqual(a, b) {
  switch (a.is) {
    case HYMN_VALUE_NONE:
      return isNone(b)
    case HYMN_VALUE_BOOL:
      return isBool(b) && a.value === b.value
    case HYMN_VALUE_INTEGER:
      switch (b.is) {
        case HYMN_VALUE_INTEGER:
          return a.value === b.value
        case HYMN_VALUE_FLOAT:
          return a.value === b.value
        default:
          return false
      }
    case HYMN_VALUE_FLOAT:
      switch (b.is) {
        case HYMN_VALUE_INTEGER:
          return a.value === b.value
        case HYMN_VALUE_FLOAT:
          return a.value === b.value
        default:
          return false
      }
    case HYMN_VALUE_STRING:
    case HYMN_VALUE_ARRAY:
    case HYMN_VALUE_TABLE:
    case HYMN_VALUE_FUNC:
    case HYMN_VALUE_FUNC_NATIVE:
      if (b.is === a.is) return a.value === b.value
      return false
    default:
      return false
  }
}

function matchValues(a, b) {
  if (a.is !== b.is) return false
  if (a.is === HYMN_VALUE_NONE) return true
  return a.value === b.value
}

function scopeGetLocal(scope, index) {
  if (index < scope.locals.length) {
    return scope.locals[index]
  }
  const local = new Local()
  scope.locals.push(local)
  return local
}

function scopeInit(C, scope, type, begin) {
  scope.enclosing = C.scope
  C.scope = scope

  scope.localCount = 0
  scope.depth = 0
  scope.func = newFunction(C.script)
  scope.type = type
  scope.begin = begin

  if (type === TYPE_FUNCTION) {
    scope.func.name = sourceSubstring(C, C.previous.length, C.previous.start)
  }

  const local = scopeGetLocal(scope, scope.localCount++)
  local.depth = 0
  local.name = null
}

function byteCodeNewConstant(C, value) {
  const code = current(C)
  const constants = code.constants
  const count = constants.length
  for (let c = 0; c < count; c++) {
    if (matchValues(constants[c], value)) {
      return c
    }
  }
  constants.push(value)
  if (count > UINT8_MAX) {
    compileError(C, C.previous, 'too many constants')
    return 0
  }
  return count
}

function arrayIndexOf(array, input) {
  for (let i = 0; i < array.length; i++) {
    if (matchValues(input, array[i])) {
      return i
    }
  }
  return -1
}

function writeByte(code, byte, row) {
  const count = code.count
  const size = code.instructions.length
  if (count + 1 > size) {
    const instructions = new Uint8Array(size * 2)
    const lines = new Uint32Array(size * 2)
    for (let i = 0; i < size; i++) instructions[i] = code.instructions[i]
    for (let i = 0; i < size; i++) lines[i] = code.lines[i]
    code.instructions = instructions
    code.lines = lines
  }
  code.instructions[count] = byte
  code.lines[count] = row
  code.count = count + 1
}

function emit(C, i) {
  writeByte(current(C), i, C.previous.row)
}

function emitPop(C) {
  const code = current(C)
  writeByte(code, OP_POP, C.previous.row)
  C.pop = code.count
}

function emitShort(C, i, b) {
  const row = C.previous.row
  const code = current(C)
  writeByte(code, i, row)
  writeByte(code, b, row)
}

function emitWord(C, i, b, n) {
  const row = C.previous.row
  const code = current(C)
  writeByte(code, i, row)
  writeByte(code, b, row)
  writeByte(code, n, row)
}

function emitConstant(C, value) {
  const constant = byteCodeNewConstant(C, value)
  emitShort(C, OP_CONSTANT, constant)
  return constant
}

function checkAssign(C) {
  switch (C.current.type) {
    case TOKEN_ASSIGN:
    case TOKEN_ASSIGN_ADD:
    case TOKEN_ASSIGN_BIT_AND:
    case TOKEN_ASSIGN_BIT_LEFT_SHIFT:
    case TOKEN_ASSIGN_BIT_OR:
    case TOKEN_ASSIGN_BIT_RIGHT_SHIFT:
    case TOKEN_ASSIGN_BIT_XOR:
    case TOKEN_ASSIGN_DIVIDE:
    case TOKEN_ASSIGN_MODULO:
    case TOKEN_ASSIGN_MULTIPLY:
    case TOKEN_ASSIGN_SUBTRACT:
      return true
    default:
      return false
  }
}

function check(C, type) {
  return C.current.type === type
}

function match(C, type) {
  if (C.current.type !== type) {
    return false
  }
  advance(C)
  return true
}

function compileWithPrecedence(C, precedence) {
  advance(C)
  const rule = rules[C.previous.type]
  const prefix = rule.prefix
  if (prefix === null) {
    compileError(C, C.previous, `expression expected near '${sourceSubstring(C, C.previous.length, C.previous.start)}'`)
    return
  }
  const assign = precedence <= PRECEDENCE_ASSIGN
  prefix(C, assign)
  while (precedence <= rules[C.current.type].precedence) {
    advance(C)
    const infix = rules[C.previous.type].infix
    if (infix === null) {
      compileError(C, C.previous, 'expected infix')
    }
    infix(C, assign)
  }
  if (assign && checkAssign(C)) {
    advance(C)
    compileError(C, C.current, 'invalid assignment')
  }
}

function consume(C, type, error) {
  if (C.current.type === type) {
    advance(C)
    return
  }
  compileError(C, C.current, error)
}

function pushHiddenLocal(C) {
  const scope = C.scope
  if (scope.localCount === HYMN_UINT8_COUNT) {
    compileError(C, C.previous, 'too many local variables in scope')
    return 0
  }
  const index = scope.localCount++
  const local = scopeGetLocal(scope, index)
  local.name = null
  local.depth = scope.depth
  return index
}

function args(C) {
  let count = 0
  if (!check(C, TOKEN_RIGHT_PAREN)) {
    do {
      expression(C)
      if (count === UINT8_MAX) {
        compileError(C, C.previous, 'too many function arguments')
        break
      }
      count++
    } while (match(C, TOKEN_COMMA))
  }
  if (C.current.type === TOKEN_RIGHT_PAREN) {
    advance(C)
  } else {
    compileError(C, C.previous, "function has no closing ')'")
  }
  return count
}

function compileCall(C) {
  const count = args(C)
  emitShort(C, OP_CALL, count)
}

function compileGroup(C) {
  expression(C)
  if (C.current.type === TOKEN_RIGHT_PAREN) {
    advance(C)
  } else {
    compileError(C, C.previous, "parenthesis group has no closing ')'")
  }
}

function compileNone(C) {
  emit(C, OP_NONE)
}

function compileTrue(C) {
  emit(C, OP_TRUE)
}

function compileFalse(C) {
  emit(C, OP_FALSE)
}

function compileInteger(C) {
  emitConstant(C, newInt(C.previous.number))
}

function compileFloat(C) {
  emitConstant(C, newFloat(C.previous.number))
}

function escapeSequence(c) {
  switch (c) {
    case 'b':
      return '\b'
    case 'f':
      return '\f'
    case 'n':
      return '\n'
    case 'r':
      return '\r'
    case 't':
      return '\t'
    case 'v':
      return '\v'
    default:
      return c
  }
}

function parseStringLiteral(string, start, len) {
  const end = start + len
  let literal = ''
  for (let i = start; i < end; i++) {
    const c = string[i]
    if (c === '\\' && i + 1 < end) {
      literal += escapeSequence(string[i + 1])
      i++
    } else {
      literal += c
    }
  }
  return literal
}

function stringLiteral(C) {
  const previous = C.previous
  let string = parseStringLiteral(C.source, previous.start, previous.length)
  while (check(C, TOKEN_STRING)) {
    const current = C.current
    const and = parseStringLiteral(C.source, current.start, current.length)
    string += and
    advance(C)
  }
  return string
}

function compileString(C) {
  emitConstant(C, newString(stringLiteral(C)))
}

function identConstantString(C) {
  return byteCodeNewConstant(C, newString(stringLiteral(C)))
}

function identConstant(C, token) {
  const string = sourceSubstring(C, token.length, token.start)
  return byteCodeNewConstant(C, newString(string))
}

function beginScope(C) {
  C.scope.depth++
}

function endScope(C) {
  const scope = C.scope
  scope.depth--
  while (scope.localCount > 0 && scope.locals[scope.localCount - 1].depth > scope.depth) {
    emitPop(C)
    scope.localCount--
  }
  C.barrier = scope.func.code.count
}

function compileArray(C) {
  emit(C, OP_NEW_ARRAY)
  if (match(C, TOKEN_RIGHT_SQUARE)) {
    return
  }
  while (!check(C, TOKEN_RIGHT_SQUARE) && !check(C, TOKEN_EOF)) {
    emit(C, OP_DUPLICATE)
    expression(C)
    emit(C, OP_ARRAY_PUSH)
    emitPop(C)
    if (!check(C, TOKEN_RIGHT_SQUARE)) {
      consume(C, TOKEN_COMMA, "expected ',' between array elements")
    }
  }
  consume(C, TOKEN_RIGHT_SQUARE, "expected ']' at end of array declaration")
}

function compileTable(C) {
  emit(C, OP_NEW_TABLE)
  if (match(C, TOKEN_RIGHT_CURLY)) {
    return
  }
  while (!check(C, TOKEN_RIGHT_CURLY) && !check(C, TOKEN_EOF)) {
    emit(C, OP_DUPLICATE)
    let name = UINT8_MAX
    if (match(C, TOKEN_IDENT)) {
      name = identConstant(C, C.previous)
    } else if (match(C, TOKEN_STRING)) {
      name = identConstantString(C)
    } else {
      compileError(C, C.current, 'expected table key')
    }
    consume(C, TOKEN_COLON, "expected ':' between table key and value")
    expression(C)
    emitShort(C, OP_SET_PROPERTY, name)
    emitPop(C)
    if (!check(C, TOKEN_RIGHT_CURLY)) {
      consume(C, TOKEN_COMMA, "expected ',' between table elements")
    }
  }
  consume(C, TOKEN_RIGHT_CURLY, "expected '}' at end of table declaration")
}

function pushLocal(C, name) {
  const scope = C.scope
  if (scope.localCount === HYMN_UINT8_COUNT) {
    compileError(C, C.previous, 'too many local variables in scope')
    return
  }
  const local = scopeGetLocal(scope, scope.localCount++)
  local.name = name
  local.depth = -1
}

function variable(C, error) {
  consume(C, TOKEN_IDENT, error)
  const scope = C.scope
  if (scope.depth === 0) {
    return identConstant(C, C.previous)
  }
  const token = C.previous
  const name = sourceSubstring(C, token.length, token.start)
  for (let i = scope.localCount - 1; i >= 0; i--) {
    const local = scope.locals[i]
    if (local.depth !== -1 && local.depth < scope.depth) {
      break
    } else if (name === local.name) {
      compileError(C, C.previous, `variable '${name}' already exists in scope`)
    }
  }
  pushLocal(C, name)
  return 0
}

function localInitialize(C) {
  const scope = C.scope
  if (scope.depth === 0) {
    return
  }
  scope.locals[scope.localCount - 1].depth = scope.depth
}

function finalizeVariable(C, global) {
  if (C.scope.depth > 0) {
    localInitialize(C)
    return
  }
  emitShort(C, OP_DEFINE_GLOBAL, global)
}

function typeDeclaration(C) {
  if (match(C, TOKEN_COLON)) {
    const type = C.current.type
    switch (type) {
      case TOKEN_NONE:
      case TOKEN_TO_FLOAT:
      case TOKEN_TO_STRING:
      case TOKEN_TO_INTEGER:
        advance(C)
        return
      default:
        compileError(C, C.current, 'unavailable type declaration')
        return
    }
  }
}

function defineNewVariable(C) {
  const v = variable(C, 'expected a variable name')
  typeDeclaration(C)
  consume(C, TOKEN_ASSIGN, "expected '=' after variable")
  expression(C)
  finalizeVariable(C, v)
}

function resolveLocal(C, token) {
  const name = sourceSubstring(C, token.length, token.start)
  const scope = C.scope
  for (let i = scope.localCount - 1; i >= 0; i--) {
    const local = scope.locals[i]
    if (name === local.name) {
      if (local.depth === -1) {
        compileError(C, token, `local variable '${name}' referenced before assignment`)
      }
      return i
    }
  }
  return -1
}

function namedVariable(C, token, assign) {
  let get
  let set
  let v = resolveLocal(C, token)
  if (v !== -1) {
    get = OP_GET_LOCAL
    set = OP_SET_LOCAL
  } else {
    get = OP_GET_GLOBAL
    set = OP_SET_GLOBAL
    v = identConstant(C, token)
  }
  if (assign && checkAssign(C)) {
    const type = C.current.type
    advance(C)
    if (type !== TOKEN_ASSIGN) {
      emitShort(C, get, v)
    }
    expression(C)
    switch (type) {
      case TOKEN_ASSIGN_ADD:
        emit(C, OP_ADD)
        break
      case TOKEN_ASSIGN_BIT_AND:
        emit(C, OP_BIT_AND)
        break
      case TOKEN_ASSIGN_BIT_LEFT_SHIFT:
        emit(C, OP_BIT_LEFT_SHIFT)
        break
      case TOKEN_ASSIGN_BIT_OR:
        emit(C, OP_BIT_OR)
        break
      case TOKEN_ASSIGN_BIT_RIGHT_SHIFT:
        emit(C, OP_BIT_RIGHT_SHIFT)
        break
      case TOKEN_ASSIGN_BIT_XOR:
        emit(C, OP_BIT_XOR)
        break
      case TOKEN_ASSIGN_DIVIDE:
        emit(C, OP_DIVIDE)
        break
      case TOKEN_ASSIGN_MODULO:
        emit(C, OP_MODULO)
        break
      case TOKEN_ASSIGN_MULTIPLY:
        emit(C, OP_MULTIPLY)
        break
      case TOKEN_ASSIGN_SUBTRACT:
        emit(C, OP_SUBTRACT)
        break
      default:
        break
    }
    emitShort(C, set, v)
  } else {
    emitShort(C, get, v)
  }
}

function compileVariable(C, assign) {
  namedVariable(C, C.previous, assign)
}

function compileUnary(C) {
  const type = C.previous.type
  compileWithPrecedence(C, PRECEDENCE_UNARY)
  switch (type) {
    case TOKEN_NOT:
      emit(C, OP_NOT)
      break
    case TOKEN_SUBTRACT:
      emit(C, OP_NEGATE)
      break
    case TOKEN_BIT_NOT:
      emit(C, OP_BIT_NOT)
      break
    default:
      return
  }
}

function compileBinary(C) {
  const type = C.previous.type
  const rule = rules[type]
  compileWithPrecedence(C, rule.precedence + 1)
  switch (type) {
    case TOKEN_ADD:
      emit(C, OP_ADD)
      break
    case TOKEN_SUBTRACT:
      emit(C, OP_SUBTRACT)
      break
    case TOKEN_MODULO:
      emit(C, OP_MODULO)
      break
    case TOKEN_MULTIPLY:
      emit(C, OP_MULTIPLY)
      break
    case TOKEN_DIVIDE:
      emit(C, OP_DIVIDE)
      break
    case TOKEN_EQUAL:
      emit(C, OP_EQUAL)
      break
    case TOKEN_NOT_EQUAL:
      emit(C, OP_NOT_EQUAL)
      break
    case TOKEN_LESS:
      emit(C, OP_LESS)
      break
    case TOKEN_LESS_EQUAL:
      emit(C, OP_LESS_EQUAL)
      break
    case TOKEN_GREATER:
      emit(C, OP_GREATER)
      break
    case TOKEN_GREATER_EQUAL:
      emit(C, OP_GREATER_EQUAL)
      break
    case TOKEN_BIT_OR:
      emit(C, OP_BIT_OR)
      break
    case TOKEN_BIT_AND:
      emit(C, OP_BIT_AND)
      break
    case TOKEN_BIT_XOR:
      emit(C, OP_BIT_XOR)
      break
    case TOKEN_BIT_LEFT_SHIFT:
      emit(C, OP_BIT_LEFT_SHIFT)
      break
    case TOKEN_BIT_RIGHT_SHIFT:
      emit(C, OP_BIT_RIGHT_SHIFT)
      break
    default:
      return
  }
}

function compileDot(C, assign) {
  consume(C, TOKEN_IDENT, "expected table key after '.'")
  const name = identConstant(C, C.previous)
  if (assign && match(C, TOKEN_ASSIGN)) {
    expression(C)
    emitShort(C, OP_SET_PROPERTY, name)
  } else {
    emitShort(C, OP_GET_PROPERTY, name)
  }
}

function compilePointer(C) {
  consume(C, TOKEN_IDENT, "expected table key after '->'")
  const name = identConstant(C, C.previous)
  consume(C, TOKEN_LEFT_PAREN, "expected '(' after function name")
  emitShort(C, OP_SELF, name)
  const count = args(C)
  if (count === UINT8_MAX) {
    compileError(C, C.previous, 'too many function arguments')
    return
  }
  emitShort(C, OP_CALL, count + 1)
}

function compileSquare(C, assign) {
  if (match(C, TOKEN_COLON)) {
    emitConstant(C, newInt(0))
    if (match(C, TOKEN_RIGHT_SQUARE)) {
      emitConstant(C, newNone())
    } else {
      expression(C)
      consume(C, TOKEN_RIGHT_SQUARE, "expected ']' after square bracket expression")
    }
    emit(C, OP_SLICE)
  } else {
    expression(C)
    if (match(C, TOKEN_COLON)) {
      if (match(C, TOKEN_RIGHT_SQUARE)) {
        emitConstant(C, newNone())
      } else {
        expression(C)
        consume(C, TOKEN_RIGHT_SQUARE, "expected ']' after square bracket expression")
      }
      emit(C, OP_SLICE)
    } else {
      consume(C, TOKEN_RIGHT_SQUARE, "expected ']' after square bracket expression")
      if (assign && match(C, TOKEN_ASSIGN)) {
        expression(C)
        emit(C, OP_SET_DYNAMIC)
      } else {
        emit(C, OP_GET_DYNAMIC)
      }
    }
  }
}

function emitJump(C, instruction) {
  emit(C, instruction)
  emitShort(C, UINT8_MAX, UINT8_MAX)
  return current(C).count - 2
}

function patchJump(C, jump) {
  if (jump === -1) {
    return
  }
  const code = current(C)
  const offset = code.count - jump - 2
  if (offset > UINT16_MAX) {
    compileError(C, C.previous, 'jump offset too large')
    return
  }
  code.instructions[jump] = (offset >> 8) & UINT8_MAX
  code.instructions[jump + 1] = offset & UINT8_MAX
}

function addJump(C, list, instruction) {
  const jump = new JumpList()
  jump.jump = emitJump(C, instruction)
  jump.depth = C.scope.depth
  jump.code = current(C)
  jump.next = list
  return jump
}

function freeJumpAndList(C) {
  let jump = C.jumpAnd
  const code = current(C)
  const depth = C.scope.depth
  while (jump !== null) {
    if (jump.code !== code || jump.depth < depth) {
      break
    }
    patchJump(C, jump.jump)
    jump = jump.next
  }
  C.jumpAnd = jump
}

function freeJumpOrList(C) {
  let jump = C.jumpOr
  const code = current(C)
  const depth = C.scope.depth
  while (jump !== null) {
    if (jump.code !== code || jump.depth < depth) {
      break
    }
    patchJump(C, jump.jump)
    jump = jump.next
  }
  C.jumpOr = jump
}

function freeJumps(C, jump) {
  while (jump !== null) {
    patchJump(C, jump.jump)
    jump = jump.next
  }
}

function compileAnd(C) {
  C.jumpAnd = addJump(C, C.jumpAnd, OP_JUMP_IF_FALSE)
  compileWithPrecedence(C, PRECEDENCE_AND)
}

function compileOr(C) {
  C.jumpOr = addJump(C, C.jumpOr, OP_JUMP_IF_TRUE)
  freeJumpAndList(C)
  compileWithPrecedence(C, PRECEDENCE_OR)
}

function echoIfNone(C) {
  const code = C.scope.func.code
  const count = code.count
  if (C.barrier === count) return
  if (C.pop === count) code.instructions[count - 1] = OP_ECHO
}

function endFunction(C) {
  const scope = C.scope
  const func = scope.func
  if (scope.type === TYPE_DIRECT) echoIfNone(C)
  emitShort(C, OP_NONE, OP_RETURN)
  if (scope.type === TYPE_FUNCTION) func.source = C.source.substring(scope.begin, C.previous.start + C.previous.length)
  C.scope = scope.enclosing
  return func
}

function compileFunction(C, type, begin) {
  const scope = new Scope()
  scopeInit(C, scope, type, begin)

  beginScope(C)

  consume(C, TOKEN_LEFT_PAREN, "expected '(' after function name")

  if (!check(C, TOKEN_RIGHT_PAREN)) {
    do {
      C.scope.func.arity++
      if (C.scope.func.arity > UINT8_MAX) {
        compileError(C, C.previous, 'too many function parameters')
      }
      const parameter = variable(C, 'expected parameter name')
      finalizeVariable(C, parameter)
      typeDeclaration(C)
    } while (match(C, TOKEN_COMMA))
  }

  consume(C, TOKEN_RIGHT_PAREN, "expected ')' after function parameters")
  typeDeclaration(C)
  consume(C, TOKEN_LEFT_CURLY, "expected '{' after function parameters")

  while (!check(C, TOKEN_RIGHT_CURLY) && !check(C, TOKEN_EOF)) {
    declaration(C)
  }

  endScope(C)
  consume(C, TOKEN_RIGHT_CURLY, "expected '}' at end of function body")

  const func = endFunction(C)
  emitConstant(C, newFuncValue(func))
}

function functionExpression(C) {
  compileFunction(C, TYPE_FUNCTION, C.previous.start)
}

function declareFunction(C) {
  const begin = C.previous.start
  const global = variable(C, 'expected function name')
  localInitialize(C)
  compileFunction(C, TYPE_FUNCTION, begin)
  finalizeVariable(C, global)
}

function declaration(C) {
  if (match(C, TOKEN_SET)) {
    defineNewVariable(C)
  } else if (match(C, TOKEN_FUNCTION)) {
    declareFunction(C)
  } else {
    statement(C)
  }
}

function block(C) {
  beginScope(C)
  while (!check(C, TOKEN_RIGHT_CURLY) && !check(C, TOKEN_EOF)) {
    declaration(C)
  }
  endScope(C)
}

function ifStatement(C) {
  expression(C)
  let jump = emitJump(C, OP_JUMP_IF_FALSE)

  freeJumpOrList(C)

  consume(C, TOKEN_LEFT_CURLY, "expected '{' after if statement")
  beginScope(C)
  while (!check(C, TOKEN_RIGHT_CURLY) && !check(C, TOKEN_EOF)) {
    declaration(C)
  }
  endScope(C)

  consume(C, TOKEN_RIGHT_CURLY, "expected '}' at end of if statement body")

  if (check(C, TOKEN_ELIF) || check(C, TOKEN_ELSE)) {
    const jumpEnd = new JumpList()
    jumpEnd.jump = emitJump(C, OP_JUMP)
    let tail = jumpEnd

    while (match(C, TOKEN_ELIF)) {
      patchJump(C, jump)
      freeJumpAndList(C)

      expression(C)
      jump = emitJump(C, OP_JUMP_IF_FALSE)

      freeJumpOrList(C)

      consume(C, TOKEN_LEFT_CURLY, "missing '{' in elif statement")
      beginScope(C)
      while (!check(C, TOKEN_RIGHT_CURLY) && !check(C, TOKEN_EOF)) {
        declaration(C)
      }
      endScope(C)
      consume(C, TOKEN_RIGHT_CURLY, "expected '}' at end of elif statement body")

      const next = new JumpList()
      next.jump = emitJump(C, OP_JUMP)

      tail.next = next
      tail = next
    }

    patchJump(C, jump)
    freeJumpAndList(C)

    if (match(C, TOKEN_ELSE)) {
      consume(C, TOKEN_LEFT_CURLY, "expected '{' after else statement")
      block(C)
      consume(C, TOKEN_RIGHT_CURLY, "expected '}' at end of else statement body")
    }

    patchJump(C, jumpEnd.jump)
    freeJumps(C, jumpEnd.next)
  } else {
    patchJump(C, jump)
    freeJumpAndList(C)
  }
}

function emitLoop(C, start) {
  emit(C, OP_LOOP)
  const offset = current(C).count - start + 2
  if (offset > UINT16_MAX) {
    compileError(C, C.previous, 'loop is too large')
  }
  emitShort(C, (offset >> 8) & UINT8_MAX, offset & UINT8_MAX)
}

function patchJumpList(C) {
  while (C.jump !== null) {
    let depth = 1
    if (C.loop !== null) {
      depth = C.loop.depth + 1
    }
    if (C.jump.depth < depth) {
      break
    }
    patchJump(C, C.jump.jump)
    C.jump = C.jump.next
  }
}

function patchJumpForList(C) {
  while (C.jumpFor !== null) {
    let depth = 1
    if (C.loop !== null) {
      depth = C.loop.depth
    }
    if (C.jumpFor.depth < depth) {
      break
    }
    patchJump(C, C.jumpFor.jump)
    C.jumpFor = C.jumpFor.next
  }
}

function iteratorStatement(C, pair) {
  localInitialize(C)

  const index = C.scope.localCount
  const value = index + 1
  const object = index - 1

  pushHiddenLocal(C)

  if (pair) {
    variable(C, 'expected variable name in for loop')
    localInitialize(C)
    consume(C, TOKEN_IN, "expected 'in' after variable name in for loop")
    C.scope.locals[index].name = C.scope.locals[object].name
  } else {
    pushHiddenLocal(C)
    C.scope.locals[value].name = C.scope.locals[object].name
  }

  C.scope.locals[object].name = null

  // IN

  expression(C)

  emitShort(C, OP_FOR, object)
  emitShort(C, UINT8_MAX, UINT8_MAX)

  const start = current(C).count
  const jump = start - 2

  const loop = new LoopList(start, C.scope.depth + 1, C.loop, true)
  C.loop = loop

  // BODY

  consume(C, TOKEN_LEFT_CURLY, "expected '{' after for loop declaration")
  block(C)

  // LOOP

  patchJumpForList(C)
  emitShort(C, OP_FOR_LOOP, object)
  const offset = current(C).count - start + 2
  if (offset > UINT16_MAX) {
    compileError(C, C.previous, 'loop is too large')
  }
  emitShort(C, (offset >> 8) & UINT8_MAX, offset & UINT8_MAX)

  // END

  C.loop = loop.next

  patchJump(C, jump)
  patchJumpList(C)

  endScope(C)

  consume(C, TOKEN_RIGHT_CURLY, "expected '}' at end of for loop")
}

function forStatement(C) {
  beginScope(C)

  // ASSIGN

  const index = C.scope.localCount

  variable(C, 'expected variable name in for loop')

  if (match(C, TOKEN_ASSIGN)) {
    expression(C)
    localInitialize(C)
    consume(C, TOKEN_COMMA, "expected ',' in for loop after variable assignment")
  } else if (match(C, TOKEN_COMMA)) {
    iteratorStatement(C, true)
    return
  } else if (match(C, TOKEN_IN)) {
    iteratorStatement(C, false)
    return
  } else {
    compileError(C, C.previous, 'incomplete for loop declaration')
    return
  }

  // COMPARE

  const compare = current(C).count

  expression(C)

  const jump = emitJump(C, OP_JUMP_IF_FALSE)

  // INCREMENT

  const increment = current(C).count

  const loop = new LoopList(increment, C.scope.depth + 1, C.loop, true)
  C.loop = loop

  if (match(C, TOKEN_COMMA)) {
    expressionStatement(C)
  } else {
    emitWord(C, OP_INCREMENT_LOCAL_AND_SET, index, 1)
  }

  const code = current(C)

  const count = code.count - increment
  const instructions = new Array(count)
  const lines = new Array(count)
  for (let x = 0; x < count; x++) {
    instructions[x] = code.instructions[increment + x]
    lines[x] = code.lines[increment + x]
  }
  code.count = increment

  // BODY

  consume(C, TOKEN_LEFT_CURLY, "expected '{' after for loop declaration")
  block(C)

  // INCREMENT

  patchJumpForList(C)

  const position = code.count
  for (let x = 0; x < count; x++) {
    code.instructions[position + x] = instructions[x]
    code.lines[position + x] = lines[x]
  }
  code.count += count

  emitLoop(C, compare)

  // END

  C.loop = loop.next

  patchJump(C, jump)
  patchJumpList(C)

  endScope(C)

  consume(C, TOKEN_RIGHT_CURLY, "expected '}' at end of for loop")
}

function whileStatement(C) {
  const start = current(C).count

  const loop = new LoopList(start, C.scope.depth + 1, C.loop, false)
  C.loop = loop

  expression(C)

  const jump = emitJump(C, OP_JUMP_IF_FALSE)

  consume(C, TOKEN_LEFT_CURLY, "expected '{' after while loop declaration")
  block(C)
  emitLoop(C, start)

  C.loop = loop.next

  patchJump(C, jump)
  patchJumpList(C)

  consume(C, TOKEN_RIGHT_CURLY, "expected '}' at end of while loop")
}

function returnStatement(C) {
  if (C.scope.type !== TYPE_FUNCTION) {
    compileError(C, C.previous, 'return statement outside of function')
  }
  if (check(C, TOKEN_RIGHT_CURLY)) {
    emit(C, OP_NONE)
  } else {
    expression(C)
  }
  emit(C, OP_RETURN)
}

function popStackLoop(C) {
  const depth = C.loop.depth
  const scope = C.scope
  for (let i = scope.localCount; i > 0; i--) {
    if (scope.locals[i - 1].depth < depth) {
      return
    }
    emitPop(C)
  }
}

function breakStatement(C) {
  if (C.loop === null) {
    compileError(C, C.previous, 'break statement outside of loop')
    return
  }
  popStackLoop(C)
  const jumpNext = C.jump
  const jump = new JumpList()
  jump.jump = emitJump(C, OP_JUMP)
  jump.depth = C.loop.depth
  jump.next = jumpNext
  C.jump = jump
}

function continueStatement(C) {
  if (C.loop === null) {
    compileError(C, C.previous, 'continue statement outside of loop')
    return
  }
  popStackLoop(C)
  if (C.loop.isFor) {
    const jumpNext = C.jumpFor
    const jump = new JumpList()
    jump.jump = emitJump(C, OP_JUMP)
    jump.depth = C.loop.depth
    jump.next = jumpNext
    C.jumpFor = jump
  } else {
    emitLoop(C, C.loop.start)
  }
}

function tryStatement(C) {
  const except = new ExceptList()
  except.stack = C.scope.localCount
  except.start = current(C).count

  const func = C.scope.func
  except.next = func.except
  func.except = except

  consume(C, TOKEN_LEFT_CURLY, "expected '{' after try declaration")
  beginScope(C)
  while (!check(C, TOKEN_RIGHT_CURLY) && !check(C, TOKEN_EOF)) {
    declaration(C)
  }
  endScope(C)

  const jump = emitJump(C, OP_JUMP)

  consume(C, TOKEN_RIGHT_CURLY, "expected '}' at end of try statement")
  consume(C, TOKEN_EXCEPT, "expected 'except' at end of try statement")

  except.end = current(C).count

  beginScope(C)
  const message = variable(C, 'expected variable name in exception declaration')
  finalizeVariable(C, message)
  consume(C, TOKEN_LEFT_CURLY, "expected '{' after exception declaration")
  while (!check(C, TOKEN_RIGHT_CURLY) && !check(C, TOKEN_EOF)) {
    declaration(C)
  }
  endScope(C)

  consume(C, TOKEN_RIGHT_CURLY, "expected '}' at end of exception statement")

  patchJump(C, jump)
}

function echoStatement(C) {
  expression(C)
  emit(C, OP_ECHO)
}

function printStatement(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'print'`)
  expression(C)
  consume(C, TOKEN_COMMA, `not enough arguments in call to 'print' (expected 2)`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'print'`)
  emit(C, OP_PRINT)
}

function useStatement(C) {
  expression(C)
  emit(C, OP_USE)
}

function throwStatement(C) {
  expression(C)
  emit(C, OP_THROW)
}

function statement(C) {
  if (match(C, TOKEN_ECHO)) {
    echoStatement(C)
  } else if (match(C, TOKEN_PRINT)) {
    printStatement(C)
  } else if (match(C, TOKEN_USE)) {
    useStatement(C)
  } else if (match(C, TOKEN_IF)) {
    ifStatement(C)
  } else if (match(C, TOKEN_FOR)) {
    forStatement(C)
  } else if (match(C, TOKEN_WHILE)) {
    whileStatement(C)
  } else if (match(C, TOKEN_RETURN)) {
    returnStatement(C)
  } else if (match(C, TOKEN_BREAK)) {
    breakStatement(C)
  } else if (match(C, TOKEN_CONTINUE)) {
    continueStatement(C)
  } else if (match(C, TOKEN_TRY)) {
    tryStatement(C)
  } else if (match(C, TOKEN_THROW)) {
    throwStatement(C)
  } else if (match(C, TOKEN_LEFT_CURLY)) {
    block(C)
    consume(C, TOKEN_RIGHT_CURLY, "expected '}' at end of block statement")
  } else {
    expressionStatement(C)
  }
}

function arrayPushExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'push'`)
  expression(C)
  consume(C, TOKEN_COMMA, `not enough arguments in call to 'push' (expected 2)`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'push'`)
  emit(C, OP_ARRAY_PUSH)
}

function arrayInsertExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'insert'`)
  expression(C)
  consume(C, TOKEN_COMMA, `not enough arguments in call to 'insert' (expected 3)`)
  expression(C)
  consume(C, TOKEN_COMMA, `not enough arguments in call to 'insert' (expected 3)`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'insert'`)
  emit(C, OP_INSERT)
}

function arrayPopExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'pop'`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'pop'`)
  emit(C, OP_ARRAY_POP)
}

function deleteExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'delete'`)
  expression(C)
  consume(C, TOKEN_COMMA, `not enough arguments in call to 'delete' (expected 2)`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'delete'`)
  emit(C, OP_DELETE)
}

function lenExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'len'`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'len'`)
  emit(C, OP_LEN)
}

function castIntegerExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'int'`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'int'`)
  emit(C, OP_INT)
}

function castFloatExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'float'`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'float'`)
  emit(C, OP_FLOAT)
}

function castStringExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'str'`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'str'`)
  emit(C, OP_STRING)
}

function typeExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'type'`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'type'`)
  emit(C, OP_TYPE)
}

function clearExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'clear'`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'clear'`)
  emit(C, OP_CLEAR)
}

function copyExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'copy'`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'copy'`)
  emit(C, OP_COPY)
}

function keysExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'keys'`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'keys'`)
  emit(C, OP_KEYS)
}

function indexExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'index'`)
  expression(C)
  consume(C, TOKEN_COMMA, `not enough arguments in call to 'index' (expected 2)`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'index'`)
  emit(C, OP_INDEX)
}

function existsExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'exists'`)
  expression(C)
  consume(C, TOKEN_COMMA, `not enough arguments in call to 'exists' (expected 2)`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'exists'`)
  emit(C, OP_EXISTS)
}

function sourceExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'SOURCE'`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'SOURCE'`)
  emit(C, OP_SOURCE)
}

function opCodesExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'OPCODES'`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'OPCODES'`)
  emit(C, OP_CODES)
}

function stackExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'STACK'`)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'STACK'`)
  emit(C, OP_STACK)
}

function referenceExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'REFERENCE'`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'REFERENCE'`)
  emit(C, OP_REFERENCE)
}

function formatExpression(C) {
  consume(C, TOKEN_LEFT_PAREN, `expected opening '(' in call to 'FORMAT'`)
  expression(C)
  consume(C, TOKEN_RIGHT_PAREN, `expected closing ')' in call to 'FORMAT'`)
  emit(C, OP_FORMAT)
}

function expressionStatement(C) {
  expression(C)
  emitPop(C)
}

function expression(C) {
  compileWithPrecedence(C, PRECEDENCE_ASSIGN)
}

function parentFrame(H, offset) {
  const frameCount = H.frameCount
  if (offset > frameCount) {
    return null
  }
  return H.frames[frameCount - offset]
}

function currentFrame(H) {
  return H.frames[H.frameCount - 1]
}

function compile(H, script, source, type) {
  const scope = new Scope()
  const C = new Compiler(script, source, H)
  scopeInit(C, scope, type, 0)

  advance(C)
  while (!match(C, TOKEN_EOF)) {
    declaration(C)
  }

  const func = endFunction(C)
  return { func: func, error: C.error }
}

function quoteString(string) {
  let quoted = '"'
  const size = string.length
  for (let i = 0; i < size; i++) {
    const c = string[i]
    if (c === '\\') {
      quoted += '\\\\'
    } else if (c === '"') {
      quoted += '\\"'
    } else {
      quoted += c
    }
  }
  return quoted + '"'
}

function valueToStringRecursive(value, set, quote) {
  switch (value.is) {
    case HYMN_VALUE_NONE:
      return 'none'
    case HYMN_VALUE_BOOL:
      return value.value ? 'true' : 'false'
    case HYMN_VALUE_INTEGER:
    case HYMN_VALUE_FLOAT:
    case HYMN_VALUE_POINTER:
      return String(value.value)
    case HYMN_VALUE_STRING:
      if (quote) {
        return quoteString(value.value)
      }
      return value.value
    case HYMN_VALUE_ARRAY: {
      const array = value.value
      if (!array || array.length === 0) {
        return '[]'
      }
      if (set === null) {
        set = new Set()
      } else if (set.has(array)) {
        return '[..]'
      }
      set.add(array)
      let print = '['
      for (let i = 0; i < array.length; i++) {
        const item = array[i]
        if (i !== 0) {
          print += ', '
        }
        print += valueToStringRecursive(item, set, true)
      }
      print += ']'
      return print
    }
    case HYMN_VALUE_TABLE: {
      const table = value.value
      if (!table || table.size === 0) {
        return '{}'
      }
      if (set === null) {
        set = new Set()
      } else if (set.has(table)) {
        return '{ .. }'
      }
      set.add(table)
      const size = table.size
      const keys = new Array(size)
      let total = 0
      const bins = table.bins
      for (let i = 0; i < bins; i++) {
        let item = table.items[i]
        while (item !== null) {
          const key = item.key
          let insert = 0
          while (insert !== total) {
            if (stringCompare(key, keys[insert]) < 0) {
              for (let swap = total; swap > insert; swap--) {
                keys[swap] = keys[swap - 1]
              }
              break
            }
            insert++
          }
          keys[insert] = item.key
          total++
          item = item.next
        }
      }
      let print = '{ '
      for (let i = 0; i < size; i++) {
        if (i !== 0) {
          print += ', '
        }
        const key = keys[i]
        const item = tableGet(table, key)
        print += quoteString(key) + ': ' + valueToStringRecursive(item, set, true)
      }
      print += ' }'
      return print
    }
    case HYMN_VALUE_FUNC: {
      const func = value.value
      if (func.name) return func.name
      if (func.script) return func.script
      return 'script'
    }
    case HYMN_VALUE_FUNC_NATIVE:
      return value.value.name
  }
}

function valueToString(value) {
  return valueToStringRecursive(value, null, false)
}

function valueToInspect(value) {
  if (value.is === HYMN_VALUE_FUNC) {
    const func = value.value
    if (func.source) return func.source
  }
  return valueToStringRecursive(value, null, false)
}

function valueToDebug(value) {
  if (value.is === HYMN_VALUE_FUNC) {
    const func = value.value
    return disassembleByteCode(func.code, func.name)
  }
  return valueToStringRecursive(value, null, false)
}

function valueToFormat(value) {
  if (value.is === HYMN_VALUE_FUNC) {
    return format(value.value.source)
  } else if (value.is === HYMN_VALUE_STRING) {
    return format(value.value)
  }
  return valueToStringRecursive(value, null, false)
}

function hymnConcat(a, b) {
  return newString(valueToString(a) + valueToString(b))
}

function debugValueToString(value) {
  return valueType(value.is) + ': ' + valueToString(value)
}

function hymnResetStack(H) {
  H.stackTop = 0
  H.frameCount = 0
}

function hymnStackGet(H, index) {
  if (index < H.stack.length) {
    return H.stack[index]
  }
  const value = new HymnValue(HYMN_VALUE_NONE, null)
  H.stack.push(value)
  return value
}

function hymnPush(H, value) {
  copyValueToFrom(hymnStackGet(H, H.stackTop++), value)
}

function hymnPeek(H, dist) {
  if (dist > H.stackTop) {
    console.error('nothing on stack to peek')
    return newNone()
  }
  return copyValue(H.stack[H.stackTop - dist])
}

function hymnPop(H) {
  if (H.stackTop === 0) {
    console.error('nothing on stack to pop')
    return newNone()
  }
  return copyValue(H.stack[--H.stackTop])
}

function hymnException(H) {
  let frame = currentFrame(H)
  while (true) {
    let except = null
    let range = frame.func.except
    while (range !== null) {
      if (frame.ip >= range.start && frame.ip <= range.end) {
        except = range
        break
      }
      range = range.next
    }
    const result = hymnPop(H)
    if (except !== null) {
      H.stackTop = frame.stack + except.stack
      frame.ip = except.end
      hymnPush(H, result)
      return frame
    }
    H.stackTop = frame.stack
    H.frameCount--
    if (H.frameCount === 0 || frame.func.name === null) {
      H.error = valueToString(result)
      return null
    }
    hymnPush(H, result)
    frame = currentFrame(H)
  }
}

function hymnStacktrace(H) {
  let trace = ''
  for (let i = H.frameCount - 1; i >= 0; i--) {
    const frame = H.frames[i]
    const func = frame.func
    const ip = frame.ip - 1
    const row = func.code.lines[ip]
    trace += '  at'
    if (func.name !== null) trace += ' ' + func.name
    trace += (func.script === null ? ' script:' : ' ' + func.script + ':') + row
    if (i > 0) trace += '\n'
  }
  return trace
}

function hymnPushError(H, error) {
  hymnPush(H, newString(error))
  return hymnException(H)
}

function hymnThrowExistingError(H, error) {
  return hymnPushError(H, error)
}

function hymnThrowError(H, error) {
  error += '\n' + hymnStacktrace(H)
  return hymnPushError(H, error)
}

function hymnFrameGet(H, index) {
  if (index < H.frames.length) {
    return H.frames[index]
  }
  const frame = new HymnFrame()
  H.frames.push(frame)
  return frame
}

function hymnCall(H, func, count) {
  if (count !== func.arity) {
    if (count < func.arity) return hymnThrowError(H, `not enough arguments in call to '${func.name}' (expected ${func.arity})`)
    return hymnThrowError(H, `too many arguments in call to '${func.name}' (expected ${func.arity})`)
  } else if (H.frameCount === HYMN_FRAMES_MAX) {
    return hymnThrowError(H, 'stack overflow')
  }

  const frame = hymnFrameGet(H, H.frameCount++)
  frame.func = func
  frame.ip = 0
  frame.stack = H.stackTop - count - 1

  return frame
}

function hymnCallValue(H, value, count) {
  switch (value.is) {
    case HYMN_VALUE_FUNC:
      return hymnCall(H, value.value, count)
    case HYMN_VALUE_FUNC_NATIVE: {
      const func = value.value.func
      const result = func(H, count, H.stack.slice(H.stackTop - count))
      const top = H.stackTop - count - 1
      H.stackTop = top
      hymnPush(H, result)
      return currentFrame(H)
    }
    default: {
      return hymnThrowError(H, `can't call ${valueType(value.is)} (expected function)`)
    }
  }
}

function readByte(frame) {
  return frame.func.code.instructions[frame.ip++]
}

function readShort(frame) {
  frame.ip += 2
  return (frame.func.code.instructions[frame.ip - 2] << 8) | frame.func.code.instructions[frame.ip - 1]
}

function readConstant(frame) {
  return frame.func.code.constants[readByte(frame)]
}

function httpPathParent(path) {
  if (path.length < 2) {
    return path
  }
  let i = path.length - 2
  while (true) {
    if (i === 0) break
    if (path[i] === '/') break
    i--
  }
  return path.substring(0, i)
}

async function hymnImport(H, file) {
  const imports = H.imports

  let script = null
  let p = 1
  while (true) {
    const frame = parentFrame(H, p)
    if (frame === null) {
      break
    }
    script = frame.func.script
    if (script !== null) {
      break
    }
    p++
  }

  const paths = H.paths
  const size = paths.length

  let module = null
  let source = null

  if (node) {
    const parent = script ? nodePath.dirname(script) : null

    for (let i = 0; i < size; i++) {
      const value = paths[i]
      if (!isString(value)) {
        continue
      }
      const question = value.value

      const replace = question.replace(/<path>/g, file)
      const path = parent ? replace.replace(/<parent>/g, parent) : replace
      const use = nodePath.resolve(path)

      if (tableGet(imports, use) !== null) {
        return currentFrame(H)
      }

      if (nodeFs.existsSync(use)) {
        module = use
        break
      }
    }

    if (module === null) {
      let missing = 'import not found: ' + file

      for (let i = 0; i < size; i++) {
        const value = paths[i]
        if (!isString(value)) {
          continue
        }
        const question = value.value

        const replace = question.replace(/<path>/g, file)
        const path = parent ? replace.replace(/<parent>/g, parent) : replace
        const use = nodePath.resolve(path)

        missing += '\n  no file: ' + use
      }

      return hymnThrowError(H, missing)
    }

    tablePut(imports, module, newBool(true))

    source = nodeFs.readFileSync(module, { encoding: 'utf-8' })
  } else {
    const parent = script ? httpPathParent(script) : null

    for (let i = 0; i < size; i++) {
      const value = paths[i]
      if (!isString(value)) {
        continue
      }
      const question = value.value

      const replace = question.replace(/<path>/g, file)
      const use = parent ? replace.replace(/<parent>/g, parent) : replace

      if (tableGet(imports, use) !== null) {
        return currentFrame(H)
      }

      const response = await fetch(use).catch((exception) => {
        return { ok: false, status: 404, exception: exception }
      })

      if (response.ok) {
        source = await response.text().catch((exception) => {
          return exception
        })
        module = use
        break
      }
    }

    if (module === null) {
      let missing = 'import not found: ' + file

      for (let i = 0; i < size; i++) {
        const value = paths[i]
        if (!isString(value)) {
          continue
        }
        const question = value.value

        const replace = question.replace(/<path>/g, file)
        const use = parent ? replace.replace(/<parent>/g, parent) : replace

        missing += '\n  no file: ' + use
      }

      return hymnThrowError(H, missing)
    }

    tablePut(imports, module, newBool(true))
  }

  const result = compile(H, module, source, TYPE_SCRIPT)

  const func = result.func
  let error = result.error

  if (error) {
    return hymnThrowExistingError(H, error)
  }

  const funcValue = newFuncValue(func)

  hymnPush(H, funcValue)
  hymnCall(H, func, 0)

  error = await hymnRun(H)
  if (error) {
    return hymnThrowExistingError(H, error)
  }

  return currentFrame(H)
}

function debugConstantInstruction(debug, name, code, index) {
  const constant = code.instructions[index + 1]
  debug[0] += `${name}: [${constant}] [${debugValueToString(code.constants[constant])}]`
  return index + 2
}

function debugByteInstruction(debug, name, code, index) {
  const byte = code.instructions[index + 1]
  debug[0] += `${name}: [${byte}]`
  return index + 2
}

function debugJumpInstruction(debug, name, sign, code, index) {
  const jump = (code.instructions[index + 1] << 8) | code.instructions[index + 2]
  debug[0] += `${name}: [${index}] -> [${index + 3 + sign * jump}]`
  return index + 3
}

function debugThreeByteInstruction(debug, name, code, index) {
  const byte = code.instructions[index + 1]
  const next = code.instructions[index + 2]
  debug[0] += `${name}: [${byte}] [${next}]`
  return index + 3
}

function debugForLoopInstruction(debug, name, sign, code, index) {
  const slot = code.instructions[index + 1]
  const jump = (code.instructions[index + 2] << 8) | code.instructions[index + 3]
  debug[0] += `${name}: [${slot}] [${index}] -> [${index + 4 + sign * jump}]`
  return index + 4
}

function debugInstruction(debug, name, index) {
  debug[0] += name
  return index + 1
}

function disassembleInstruction(debug, code, index) {
  debug[0] += String(index).padStart(4, '0') + ' '
  if (index > 0 && code.lines[index] === code.lines[index - 1]) {
    debug[0] += '   | '
  } else {
    debug[0] += String(code.lines[index]).padStart(4, ' ') + ' '
  }
  const instruction = code.instructions[index]
  switch (instruction) {
    case OP_ADD:
      return debugInstruction(debug, 'OP_ADD', index)
    case OP_INSERT:
      return debugInstruction(debug, 'OP_INSERT', index)
    case OP_ARRAY_POP:
      return debugInstruction(debug, 'OP_ARRAY_POP', index)
    case OP_ARRAY_PUSH:
      return debugInstruction(debug, 'OP_ARRAY_PUSH', index)
    case OP_BIT_AND:
      return debugInstruction(debug, 'OP_BIT_AND', index)
    case OP_BIT_LEFT_SHIFT:
      return debugInstruction(debug, 'OP_BIT_LEFT_SHIFT', index)
    case OP_BIT_NOT:
      return debugInstruction(debug, 'OP_BIT_NOT', index)
    case OP_BIT_OR:
      return debugInstruction(debug, 'OP_BIT_OR', index)
    case OP_BIT_RIGHT_SHIFT:
      return debugInstruction(debug, 'OP_BIT_RIGHT_SHIFT', index)
    case OP_BIT_XOR:
      return debugInstruction(debug, 'OP_BIT_XOR', index)
    case OP_CALL:
      return debugByteInstruction(debug, 'OP_CALL', code, index)
    case OP_SELF:
      return debugConstantInstruction(debug, 'OP_SELF', code, index)
    case OP_CLEAR:
      return debugInstruction(debug, 'OP_CLEAR', index)
    case OP_CONSTANT:
      return debugConstantInstruction(debug, 'OP_CONSTANT', code, index)
    case OP_NEW_ARRAY:
      return debugInstruction(debug, 'OP_NEW_ARRAY', index)
    case OP_NEW_TABLE:
      return debugInstruction(debug, 'OP_NEW_TABLE', index)
    case OP_COPY:
      return debugInstruction(debug, 'OP_COPY', index)
    case OP_DEFINE_GLOBAL:
      return debugConstantInstruction(debug, 'OP_DEFINE_GLOBAL', code, index)
    case OP_DELETE:
      return debugInstruction(debug, 'OP_DELETE', index)
    case OP_DIVIDE:
      return debugInstruction(debug, 'OP_DIVIDE', index)
    case OP_DUPLICATE:
      return debugInstruction(debug, 'OP_DUPLICATE', index)
    case OP_EQUAL:
      return debugInstruction(debug, 'OP_EQUAL', index)
    case OP_ECHO:
      return debugInstruction(debug, 'OP_ECHO', index)
    case OP_EXISTS:
      return debugInstruction(debug, 'OP_EXISTS', index)
    case OP_FALSE:
      return debugInstruction(debug, 'OP_FALSE', index)
    case OP_FOR:
      return debugForLoopInstruction(debug, 'OP_FOR', 1, code, index)
    case OP_FOR_LOOP:
      return debugForLoopInstruction(debug, 'OP_FOR_LOOP', -1, code, index)
    case OP_GET_DYNAMIC:
      return debugInstruction(debug, 'OP_GET_DYNAMIC', index)
    case OP_GET_GLOBAL:
      return debugConstantInstruction(debug, 'OP_GET_GLOBAL', code, index)
    case OP_GET_LOCAL:
      return debugByteInstruction(debug, 'OP_GET_LOCAL', code, index)
    case OP_GET_PROPERTY:
      return debugConstantInstruction(debug, 'OP_GET_PROPERTY', code, index)
    case OP_GREATER:
      return debugInstruction(debug, 'OP_GREATER', index)
    case OP_GREATER_EQUAL:
      return debugInstruction(debug, 'OP_GREATER_EQUAL', index)
    case OP_INCREMENT_LOCAL_AND_SET:
      return debugThreeByteInstruction(debug, 'OP_INCREMENT_LOCAL_AND_SET', code, index)
    case OP_INDEX:
      return debugInstruction(debug, 'OP_INDEX', index)
    case OP_JUMP:
      return debugJumpInstruction(debug, 'OP_JUMP', 1, code, index)
    case OP_JUMP_IF_FALSE:
      return debugJumpInstruction(debug, 'OP_JUMP_IF_FALSE', 1, code, index)
    case OP_JUMP_IF_TRUE:
      return debugJumpInstruction(debug, 'OP_JUMP_IF_TRUE', 1, code, index)
    case OP_KEYS:
      return debugInstruction(debug, 'OP_KEYS', index)
    case OP_LEN:
      return debugInstruction(debug, 'OP_LEN', index)
    case OP_LESS:
      return debugInstruction(debug, 'OP_LESS', index)
    case OP_LESS_EQUAL:
      return debugInstruction(debug, 'OP_LESS_EQUAL', index)
    case OP_LOOP:
      return debugJumpInstruction(debug, 'OP_LOOP', -1, code, index)
    case OP_MODULO:
      return debugInstruction(debug, 'OP_MODULO', index)
    case OP_MULTIPLY:
      return debugInstruction(debug, 'OP_MULTIPLY', index)
    case OP_NEGATE:
      return debugInstruction(debug, 'OP_NEGATE', index)
    case OP_NONE:
      return debugInstruction(debug, 'OP_NONE', index)
    case OP_NOT:
      return debugInstruction(debug, 'OP_NOT', index)
    case OP_NOT_EQUAL:
      return debugInstruction(debug, 'OP_NOT_EQUAL', index)
    case OP_POP:
      return debugInstruction(debug, 'OP_POP', index)
    case OP_PRINT:
      return debugInstruction(debug, 'OP_PRINT', index)
    case OP_RETURN:
      return debugInstruction(debug, 'OP_RETURN', index)
    case OP_SET_DYNAMIC:
      return debugInstruction(debug, 'OP_SET_DYNAMIC', index)
    case OP_SET_GLOBAL:
      return debugConstantInstruction(debug, 'OP_SET_GLOBAL', code, index)
    case OP_SET_LOCAL:
      return debugByteInstruction(debug, 'OP_SET_LOCAL', code, index)
    case OP_SET_PROPERTY:
      return debugConstantInstruction(debug, 'OP_SET_PROPERTY', code, index)
    case OP_SLICE:
      return debugInstruction(debug, 'OP_SLICE', index)
    case OP_SUBTRACT:
      return debugInstruction(debug, 'OP_SUBTRACT', index)
    case OP_THROW:
      return debugInstruction(debug, 'OP_THROW', index)
    case OP_FLOAT:
      return debugInstruction(debug, 'OP_FLOAT', index)
    case OP_INT:
      return debugInstruction(debug, 'OP_INT', index)
    case OP_STRING:
      return debugInstruction(debug, 'OP_STRING', index)
    case OP_TRUE:
      return debugInstruction(debug, 'OP_TRUE', index)
    case OP_TYPE:
      return debugInstruction(debug, 'OP_TYPE', index)
    case OP_USE:
      return debugInstruction(debug, 'OP_USE', index)
    case OP_SOURCE:
      return debugInstruction(debug, 'OP_SOURCE', index)
    case OP_CODES:
      return debugInstruction(debug, 'OP_CODES', index)
    case OP_STACK:
      return debugInstruction(debug, 'OP_STACK', index)
    case OP_REFERENCE:
      return debugInstruction(debug, 'OP_REFERENCE', index)
    case OP_FORMAT:
      return debugInstruction(debug, 'OP_FORMAT', index)
    default:
      return (debug[0] += 'UNKNOWN_OPCODE ' + instruction)
  }
}

function disassembleByteCode(code) {
  const debug = ['']
  if (code.count > 0) {
    let index = disassembleInstruction(debug, code, 0)
    while (index < code.count) {
      debug[0] += '\n'
      index = disassembleInstruction(debug, code, index)
    }
  }
  return debug[0]
}

async function hymnRun(H) {
  let frame = currentFrame(H)
  while (true) {
    switch (readByte(frame)) {
      case OP_RETURN: {
        const result = hymnPop(H)
        H.frameCount--
        if (H.frameCount === 0 || frame.func.name === null) {
          hymnPop(H)
          return
        }
        H.stackTop = frame.stack
        hymnPush(H, result)
        frame = currentFrame(H)
        break
      }
      case OP_POP:
        hymnPop(H)
        break
      case OP_TRUE:
        hymnPush(H, newBool(true))
        break
      case OP_FALSE:
        hymnPush(H, newBool(false))
        break
      case OP_NONE:
        hymnPush(H, newNone())
        break
      case OP_CALL: {
        const count = readByte(frame)
        const call = hymnPeek(H, count + 1)
        frame = hymnCallValue(H, call, count)
        if (frame === null) return
        break
      }
      case OP_SELF: {
        const table = hymnPeek(H, 1)
        if (!isTable(table)) {
          frame = hymnThrowError(H, `can't get property of ${valueType(table.is)} (expected table)`)
          if (frame === null) return
          else break
        }
        const name = readConstant(frame).value
        const fun = tableGet(table.value, name)
        copyValueToFrom(hymnStackGet(H, H.stackTop - 1), fun)
        hymnPush(H, table)
        break
      }
      case OP_JUMP: {
        const jump = readShort(frame)
        frame.ip += jump
        break
      }
      case OP_JUMP_IF_FALSE: {
        const value = hymnPop(H)
        const jump = readShort(frame)
        if (hymnFalse(value)) {
          frame.ip += jump
        }
        break
      }
      case OP_JUMP_IF_TRUE: {
        const value = hymnPop(H)
        const jump = readShort(frame)
        if (!hymnFalse(value)) {
          frame.ip += jump
        }
        break
      }
      case OP_LOOP: {
        const jump = readShort(frame)
        frame.ip -= jump
        break
      }
      case OP_FOR: {
        const slot = readByte(frame)
        const object = H.stack[frame.stack + slot]
        H.stackTop += 2
        if (isTable(object)) {
          const table = object.value
          const next = tableNext(table, null)
          if (next === null) {
            H.stack[frame.stack + slot + 1] = newNone()
            H.stack[frame.stack + slot + 2] = newNone()
            const jump = readShort(frame)
            frame.ip += jump
          } else {
            H.stack[frame.stack + slot + 1] = newString(next.key)
            H.stack[frame.stack + slot + 2] = copyValue(next.value)
            frame.ip += 2
          }
        } else if (isArray(object)) {
          const array = object.value
          if (array.length === 0) {
            H.stack[frame.stack + slot + 1] = newNone()
            H.stack[frame.stack + slot + 2] = newNone()
            const jump = readShort(frame)
            frame.ip += jump
          } else {
            const item = array[0]
            H.stack[frame.stack + slot + 1] = newInt(0)
            H.stack[frame.stack + slot + 2] = copyValue(item)
            frame.ip += 2
          }
        } else {
          H.stack[frame.stack + slot + 1] = newNone()
          H.stack[frame.stack + slot + 2] = newNone()
          frame = hymnThrowError(H, `can't iterate over ${valueType(object.is)} (expected array or table)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_FOR_LOOP: {
        const slot = readByte(frame)
        const object = H.stack[frame.stack + slot]
        const index = slot + 1
        const value = slot + 2
        if (isTable(object)) {
          const table = object.value
          const key = H.stack[frame.stack + index].value
          const next = tableNext(table, key)
          if (next === null) {
            frame.ip += 2
          } else {
            H.stack[frame.stack + index] = newString(next.key)
            H.stack[frame.stack + value] = copyValue(next.value)
            const jump = readShort(frame)
            frame.ip -= jump
          }
        } else {
          const array = object.value
          const key = H.stack[frame.stack + index].value + 1
          if (key >= array.length) {
            frame.ip += 2
          } else {
            const item = array[key]
            H.stack[frame.stack + index].value++
            H.stack[frame.stack + value] = copyValue(item)
            const jump = readShort(frame)
            frame.ip -= jump
          }
        }
        break
      }
      case OP_EQUAL: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        hymnPush(H, newBool(hymnEqual(a, b)))
        break
      }
      case OP_NOT_EQUAL: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        hymnPush(H, newBool(!hymnEqual(a, b)))
        break
      }
      case OP_LESS: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        if ((isInt(a) || isFloat(a)) && (isInt(b) || isFloat(b))) {
          hymnPush(H, newBool(a.value < b.value))
        } else {
          frame = hymnThrowError(H, `comparison '<' can't use ${valueType(a.is)} and ${valueType(b.is)} (expected numbers)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_LESS_EQUAL: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        if ((isInt(a) || isFloat(a)) && (isInt(b) || isFloat(b))) {
          hymnPush(H, newBool(a.value <= b.value))
        } else {
          frame = hymnThrowError(H, `comparison '<=' can't use ${valueType(a.is)} and ${valueType(b.is)} (expected numbers)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_GREATER: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        if ((isInt(a) || isFloat(a)) && (isInt(b) || isFloat(b))) {
          hymnPush(H, newBool(a.value > b.value))
        } else {
          frame = hymnThrowError(H, `comparison '>' can't use ${valueType(a.is)} and ${valueType(b.is)} (expected numbers)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_GREATER_EQUAL: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        if ((isInt(a) || isFloat(a)) && (isInt(b) || isFloat(b))) {
          hymnPush(H, newBool(a.value >= b.value))
        } else {
          frame = hymnThrowError(H, `comparison '>=' can't use ${valueType(a.is)} and ${valueType(b.is)} (expected numbers)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_ADD: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        if (isNone(a)) {
          if (isString(b)) {
            hymnPush(H, hymnConcat(a, b))
          } else {
            frame = hymnThrowError(H, `can't add ${valueType(a.is)} and ${valueType(b.is)}`)
            if (frame === null) return
            else break
          }
        } else if (isBool(a)) {
          if (isString(b)) {
            hymnPush(H, hymnConcat(a, b))
          } else {
            frame = hymnThrowError(H, `can't add ${valueType(a.is)} and ${valueType(b.is)}`)
            if (frame === null) return
            else break
          }
        } else if (isInt(a)) {
          if (isInt(b)) {
            a.value += b.value
            hymnPush(H, a)
          } else if (isFloat(b)) {
            b.value += a.value
            hymnPush(H, a)
          } else if (isString(b)) {
            hymnPush(H, hymnConcat(a, b))
          } else {
            frame = hymnThrowError(H, `can't add ${valueType(a.is)} and ${valueType(b.is)}`)
            if (frame === null) return
            else break
          }
        } else if (isFloat(a)) {
          if (isInt(b)) {
            a.value += b.value
            hymnPush(H, a)
          } else if (isFloat(b)) {
            a.value += b.value
            hymnPush(H, a)
          } else if (isString(b)) {
            hymnPush(H, hymnConcat(a, b))
          } else {
            frame = hymnThrowError(H, `can't add ${valueType(a.is)} and ${valueType(b.is)}`)
            if (frame === null) return
            else break
          }
        } else if (isString(a)) {
          hymnPush(H, hymnConcat(a, b))
        } else {
          frame = hymnThrowError(H, `can't add ${valueType(a.is)} and ${valueType(b.is)}`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_SUBTRACT: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        if (isInt(a)) {
          if (isInt(b)) {
            a.value -= b.value
            hymnPush(H, a)
          } else if (isFloat(b)) {
            a.value -= b.value
            hymnPush(H, a)
          } else {
            frame = hymnThrowError(H, `can't subtract ${valueType(a.is)} and ${valueType(b.is)} (expected numbers)`)
            if (frame === null) return
            else break
          }
        } else if (isFloat(a)) {
          if (isInt(b)) {
            a.value -= b.value
            hymnPush(H, a)
          } else if (isFloat(b)) {
            a.value -= b.value
            hymnPush(H, a)
          } else {
            frame = hymnThrowError(H, `can't subtract ${valueType(a.is)} and ${valueType(b.is)} (expected numbers)`)
            if (frame === null) return
            else break
          }
        } else {
          frame = hymnThrowError(H, `can't subtract ${valueType(a.is)} and ${valueType(b.is)} (expected numbers)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_MULTIPLY: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        if (isInt(a)) {
          if (isInt(b)) {
            a.value *= b.value
            hymnPush(H, a)
          } else if (isFloat(b)) {
            a.value *= b.value
            hymnPush(H, a)
          } else {
            frame = hymnThrowError(H, `can't multiply ${valueType(a.is)} and ${valueType(b.is)} (expected numbers)`)
            if (frame === null) return
            else break
          }
        } else if (isFloat(a)) {
          if (isInt(b)) {
            a.value *= b.value
            hymnPush(H, a)
          } else if (isFloat(b)) {
            a.value *= b.value
            hymnPush(H, a)
          } else {
            frame = hymnThrowError(H, `can't multiply ${valueType(a.is)} and ${valueType(b.is)} (expected numbers)`)
            if (frame === null) return
            else break
          }
        } else {
          frame = hymnThrowError(H, `can't multiply ${valueType(a.is)} and ${valueType(b.is)} (expected numbers)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_DIVIDE: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        if (isInt(a)) {
          if (isInt(b)) {
            a.value /= b.value
            hymnPush(H, a)
          } else if (isFloat(b)) {
            a.value /= b.value
            hymnPush(H, a)
          } else {
            frame = hymnThrowError(H, `can't divide ${valueType(a.is)} and ${valueType(b.is)} (expected numbers)`)
            if (frame === null) return
            else break
          }
        } else if (isFloat(a)) {
          if (isInt(b)) {
            a.value /= b.value
            hymnPush(H, a)
          } else if (isFloat(b)) {
            a.value /= b.value
            hymnPush(H, a)
          } else {
            frame = hymnThrowError(H, `can't divide ${valueType(a.is)} and ${valueType(b.is)} (expected numbers)`)
            if (frame === null) return
            else break
          }
        } else {
          frame = hymnThrowError(H, `can't divide ${valueType(a.is)} and ${valueType(b.is)} (expected numbers)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_MODULO: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        if (isInt(a)) {
          if (isInt(b)) {
            a.value %= b.value
            hymnPush(H, a)
          } else {
            frame = hymnThrowError(H, `can't modulo ${valueType(a.is)} and ${valueType(b.is)} (expected integers)`)
            if (frame === null) return
            else break
          }
        } else {
          frame = hymnThrowError(H, `can't modulo ${valueType(a.is)} and ${valueType(b.is)} (expected integers)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_BIT_NOT: {
        const value = hymnPop(H)
        if (isInt(value)) {
          value.value = ~value.value
          hymnPush(H, value)
        } else {
          frame = hymnThrowError(H, `bitwise '~' can't use ${valueType(value.is)} (expected integer)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_BIT_OR: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        if (isInt(a)) {
          if (isInt(b)) {
            a.value |= b.value
            hymnPush(H, a)
          } else {
            frame = hymnThrowError(H, `bitwise '|' can't use ${valueType(a.is)} and ${valueType(b.is)} (expected integers)`)
            if (frame === null) return
            else break
          }
        } else {
          frame = hymnThrowError(H, `bitwise '|' can't use ${valueType(a.is)} and ${valueType(b.is)} (expected integers)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_BIT_AND: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        if (isInt(a)) {
          if (isInt(b)) {
            a.value &= b.value
            hymnPush(H, a)
          } else {
            frame = hymnThrowError(H, `bitwise '&' can't use ${valueType(a.is)} and ${valueType(b.is)} (expected integers)`)
            if (frame === null) return
            else break
          }
        } else {
          frame = hymnThrowError(H, `bitwise '&' can't use ${valueType(a.is)} and ${valueType(b.is)} (expected integers)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_BIT_XOR: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        if (isInt(a)) {
          if (isInt(b)) {
            a.value ^= b.value
            hymnPush(H, a)
          } else {
            frame = hymnThrowError(H, `bitwise '^' can't use ${valueType(a.is)} and ${valueType(b.is)} (expected integers)`)
            if (frame === null) return
            else break
          }
        } else {
          frame = hymnThrowError(H, `bitwise '^' can't use ${valueType(a.is)} and ${valueType(b.is)} (expected integers)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_BIT_LEFT_SHIFT: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        if (isInt(a)) {
          if (isInt(b)) {
            a.value <<= b.value
            hymnPush(H, a)
          } else {
            frame = hymnThrowError(H, `bitwise '<<' can't use ${valueType(a.is)} and ${valueType(b.is)} (expected integers)`)
            if (frame === null) return
            else break
          }
        } else {
          frame = hymnThrowError(H, `bitwise '<<' can't use ${valueType(a.is)} and ${valueType(b.is)} (expected integers)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_BIT_RIGHT_SHIFT: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        if (isInt(a)) {
          if (isInt(b)) {
            a.value >>= b.value
            hymnPush(H, a)
          } else {
            frame = hymnThrowError(H, `bitwise '>>' can't use ${valueType(a.is)} and ${valueType(b.is)} (expected integers)`)
            if (frame === null) return
            else break
          }
        } else {
          frame = hymnThrowError(H, `bitwise '>>' can't use ${valueType(a.is)} and ${valueType(b.is)} (expected integers)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_NEGATE: {
        const value = hymnPop(H)
        if (isInt(value)) {
          value.value = -value.value
        } else if (isFloat(value)) {
          value.value = -value.value
        } else {
          frame = hymnThrowError(H, `negation '-' can't use ${valueType(value.is)} (expected number)`)
          if (frame === null) return
          else break
        }
        hymnPush(H, value)
        break
      }
      case OP_NOT: {
        const value = hymnPop(H)
        if (isBool(value)) {
          value.value = !value.value
        } else {
          frame = hymnThrowError(H, `'not' can't use ${valueType(value.is)} (expected boolean)`)
          if (frame === null) return
          else break
        }
        hymnPush(H, value)
        break
      }
      case OP_CONSTANT: {
        const value = readConstant(frame)
        hymnPush(H, value)
        break
      }
      case OP_NEW_ARRAY: {
        const value = newArrayValue([])
        hymnPush(H, value)
        break
      }
      case OP_NEW_TABLE: {
        const value = newTableValue(new HymnTable())
        hymnPush(H, value)
        break
      }
      case OP_DEFINE_GLOBAL: {
        const name = readConstant(frame).value
        const value = hymnPop(H)
        const previous = tablePut(H.globals, name, value)
        if (previous !== null) {
          tablePut(H.globals, name, previous)
          frame = hymnThrowError(H, `multiple global definitions of '${name}'`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_SET_GLOBAL: {
        const name = readConstant(frame).value
        const value = hymnPeek(H, 1)
        tablePut(H.globals, name, value)
        break
      }
      case OP_GET_GLOBAL: {
        const name = readConstant(frame).value
        const get = tableGet(H.globals, name)
        if (get === null) {
          frame = hymnThrowError(H, `undefined global '${name}'`)
          if (frame === null) return
          else break
        }
        hymnPush(H, get)
        break
      }
      case OP_SET_LOCAL: {
        const slot = readByte(frame)
        const value = hymnPeek(H, 1)
        H.stack[frame.stack + slot] = value
        break
      }
      case OP_GET_LOCAL: {
        const slot = readByte(frame)
        const value = H.stack[frame.stack + slot]
        hymnPush(H, value)
        break
      }
      case OP_INCREMENT_LOCAL_AND_SET: {
        const slot = readByte(frame)
        const increment = readByte(frame)
        const value = copyValue(H.stack[frame.stack + slot])
        if (isInt(value) || isFloat(value)) {
          value.value += increment
        } else {
          frame = hymnThrowError(H, `can't increment ${valueType(value.is)} (expected number)`)
          if (frame === null) return
          else break
        }
        H.stack[frame.stack + slot] = value
        break
      }
      case OP_SET_PROPERTY: {
        const value = hymnPop(H)
        const tableValue = hymnPop(H)
        if (!isTable(tableValue)) {
          frame = hymnThrowError(H, `can't set property of ${valueType(value.is)} (expected table)`)
          if (frame === null) return
          else break
        }
        const table = tableValue.value
        const name = readConstant(frame).value
        tablePut(table, name, value)
        hymnPush(H, value)
        break
      }
      case OP_GET_PROPERTY: {
        const value = hymnPop(H)
        if (!isTable(value)) {
          frame = hymnThrowError(H, `can't get property of ${valueType(value.is)} (expected table)`)
          if (frame === null) return
          else break
        }
        const table = value.value
        const name = readConstant(frame).value
        const g = tableGet(table, name)
        if (g === null) hymnPush(H, newNone())
        else hymnPush(H, g)
        break
      }
      case OP_EXISTS: {
        const v = hymnPop(H)
        const o = hymnPop(H)
        if (!isTable(o)) {
          frame = hymnThrowError(H, `call to 'exists' can't use ${valueType(o.is)} for 1st argument (expected table)`)
          if (frame === null) return
          else break
        }
        if (!isString(v)) {
          frame = hymnThrowError(H, `call to 'exists' can't use ${valueType(v.is)} for 2nd argument (expected string)`)
          if (frame === null) return
          else break
        }
        const table = o.value
        const name = v.value
        const g = tableGet(table, name)
        if (g === null) {
          hymnPush(H, newBool(false))
        } else {
          hymnPush(H, newBool(true))
        }
        break
      }
      case OP_SET_DYNAMIC: {
        const s = hymnPop(H)
        const i = hymnPop(H)
        const v = hymnPop(H)
        if (isArray(v)) {
          if (!isInt(i)) {
            frame = hymnThrowError(H, `array assignment index can't be ${valueType(i.is)} (expected integer)`)
            if (frame === null) return
            else break
          }
          const array = v.value
          const size = array.length
          let index = i.value
          if (index > size) {
            frame = hymnThrowError(H, `array assignment index out of bounds: ${index} > ${size}`)
            if (frame === null) return
            else break
          }
          if (index < 0) {
            index = size + index
            if (index < 0) {
              frame = hymnThrowError(H, `negative array assignment index: ${index}`)
              if (frame === null) return
              else break
            }
          }
          if (index === size) {
            array.push(s)
          } else {
            array[index] = s
          }
        } else if (isTable(v)) {
          if (!isString(i)) {
            frame = hymnThrowError(H, `table assignment key can't be ${valueType(i.is)} (expected string)`)
            if (frame === null) return
            else break
          }
          const table = v.value
          const name = i.value
          tablePut(table, name, s)
        } else {
          frame = hymnThrowError(H, `can't assign value to ${valueType(v.is)} (expected array or table)`)
          if (frame === null) return
          else break
        }
        hymnPush(H, s)
        break
      }
      case OP_GET_DYNAMIC: {
        const i = hymnPop(H)
        const v = hymnPop(H)
        switch (v.is) {
          case HYMN_VALUE_STRING: {
            if (!isInt(i)) {
              frame = hymnThrowError(H, `string index can't be ${valueType(i.is)} (expected integer)`)
              if (frame === null) return
              else break
            }
            const string = v.value
            const size = string.length
            let index = i.value
            if (index >= size) {
              frame = hymnThrowError(H, `string index out of bounds: ${index} >= ${size}`)
              if (frame === null) return
              else break
            }
            if (index < 0) {
              index = size + index
              if (index < 0) {
                frame = hymnThrowError(H, `negative string index: ${index}`)
                if (frame === null) return
                else break
              }
            }
            const c = string[index]
            hymnPush(H, newString(String(c)))
            break
          }
          case HYMN_VALUE_ARRAY: {
            if (!isInt(i)) {
              frame = hymnThrowError(H, `array index can't be ${valueType(i.is)} (expected integer)`)
              if (frame === null) return
              else break
            }
            const array = v.value
            const size = array.length
            let index = i.value
            if (index >= size) {
              frame = hymnThrowError(H, `array index out of bounds: ${index} >= ${size}`)
              if (frame === null) return
              else break
            }
            if (index < 0) {
              index = size + index
              if (index < 0) {
                frame = hymnThrowError(H, `negative array index: ${index}`)
                if (frame === null) return
                else break
              }
            }
            const g = array[index]
            hymnPush(H, g)
            break
          }
          case HYMN_VALUE_TABLE: {
            if (!isString(i)) {
              frame = hymnThrowError(H, `table key can't be ${valueType(i.is)} (expected string)`)
              if (frame === null) return
              else break
            }
            const table = v.value
            const name = i.value
            const g = tableGet(table, name)
            if (g === null) hymnPush(H, newNone())
            else hymnPush(H, g)
            break
          }
          default: {
            frame = hymnThrowError(H, `can't get value from ${valueType(v.is)} (expected array, table, or string)`)
            if (frame === null) return
            else break
          }
        }
        break
      }
      case OP_LEN: {
        const value = hymnPop(H)
        switch (value.is) {
          case HYMN_VALUE_STRING: {
            const len = value.value.length
            hymnPush(H, newInt(len))
            break
          }
          case HYMN_VALUE_ARRAY: {
            const len = value.value.length
            hymnPush(H, newInt(len))
            break
          }
          case HYMN_VALUE_TABLE: {
            const len = value.value.size
            hymnPush(H, newInt(len))
            break
          }
          default:
            frame = hymnThrowError(H, `call to 'len' can't use ${valueType(value.is)} (expected array, string, or table)`)
            if (frame === null) return
            else break
        }

        break
      }
      case OP_ARRAY_POP: {
        const a = hymnPop(H)
        if (!isArray(a)) {
          frame = hymnThrowError(H, `call to 'pop' can't use ${valueType(a.is)} (expected array)`)
          if (frame === null) return
          else break
        } else {
          const value = a.value.pop()
          hymnPush(H, value)
        }
        break
      }
      case OP_ARRAY_PUSH: {
        const v = hymnPop(H)
        const a = hymnPop(H)
        if (!isArray(a)) {
          frame = hymnThrowError(H, `call to 'push' can't use ${valueType(v.is)} for 1st argument (expected array)`)
          if (frame === null) return
          else break
        } else {
          a.value.push(v)
          hymnPush(H, v)
        }
        break
      }
      case OP_INSERT: {
        const p = hymnPop(H)
        const i = hymnPop(H)
        const v = hymnPop(H)
        if (isArray(v)) {
          if (!isInt(i)) {
            frame = hymnThrowError(H, `call to 'insert' can't use ${valueType(v.is)} for 2nd argument (expected integer)`)
            if (frame === null) return
            else break
          }
          const array = v.value
          const size = array.length
          let index = i.value
          if (index > size) {
            frame = hymnThrowError(H, `index out of bounds in call to 'insert': ${index} > ${size}`)
            if (frame === null) return
            else break
          }
          if (index < 0) {
            index = size + index
            if (index < 0) {
              frame = hymnThrowError(H, `negative index in 'insert' call: ${index}`)
              if (frame === null) return
              else break
            }
          }
          if (index === size) {
            array.push(p)
          } else {
            array.splice(index, 0, p)
          }
          hymnPush(H, p)
        } else {
          frame = hymnThrowError(H, `call to 'insert' can't use ${valueType(v.is)} for 1st argument (expected array)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_DELETE: {
        const at = hymnPop(H)
        const value = hymnPop(H)
        if (isArray(value)) {
          if (!isInt(at)) {
            frame = hymnThrowError(H, `call to 'delete' can't use ${valueType(at.is)} for 2nd argument (expected integer)`)
            if (frame === null) return
            else break
          }
          const array = value.value
          const size = array.length
          let index = at.value
          if (index >= size) {
            frame = hymnThrowError(H, `index out of bounds in call to 'delete': ${index} >= ${size}`)
            if (frame === null) return
            else break
          }
          if (index < 0) {
            index = size + index
            if (index < 0) {
              frame = hymnThrowError(H, `negative index in 'delete' call: ${index}`)
              if (frame === null) return
              else break
            }
          }
          const item = array.splice(index, 1)[0]
          hymnPush(H, item)
        } else if (isTable(value)) {
          if (!isString(at)) {
            frame = hymnThrowError(H, `call to 'delete' can't use ${valueType(at.is)} for 2nd argument (expected string)`)
            if (frame === null) return
            else break
          }
          const table = value.value
          const name = at.value
          const item = tableGet(table, name)
          if (item !== null) {
            tableRemove(table, name)
            hymnPush(H, item)
          } else {
            hymnPush(H, newNone())
          }
        } else {
          frame = hymnThrowError(H, `call to 'delete' can't use ${valueType(value.is)} for 1st argument (expected array or table)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_COPY: {
        const value = hymnPop(H)
        switch (value.is) {
          case HYMN_VALUE_NONE:
          case HYMN_VALUE_BOOL:
          case HYMN_VALUE_INTEGER:
          case HYMN_VALUE_FLOAT:
          case HYMN_VALUE_STRING:
          case HYMN_VALUE_FUNC:
          case HYMN_VALUE_FUNC_NATIVE:
            hymnPush(H, value)
            break
          case HYMN_VALUE_ARRAY: {
            const copy = value.value.slice()
            hymnPush(H, newArrayValue(copy))
            break
          }
          case HYMN_VALUE_TABLE: {
            const copy = newTableCopy(value.value)
            hymnPush(H, newTableValue(copy))
            break
          }
          default:
            hymnPush(H, newNone())
        }
        break
      }
      case OP_SLICE: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        const v = hymnPop(H)
        if (!isInt(a)) {
          frame = hymnThrowError(H, `slice can't use ${valueType(a.is)} (expected integer)`)
          if (frame === null) return
          else break
        }
        const start = a.value
        if (start < 0) {
          frame = hymnThrowError(H, `negative slice start: ${start}`)
          if (frame === null) return
          else break
        }
        if (isString(v)) {
          const original = v.value
          const size = original.length
          let end
          if (isInt(b)) {
            end = b.value
          } else if (isNone(b)) {
            end = size
          } else {
            frame = hymnThrowError(H, `slice can't use ${valueType(b.is)} (expected integer)`)
            if (frame === null) return
            else break
          }
          if (end > size) {
            frame = hymnThrowError(H, `slice out of bounds: ${end} > ${size}`)
            if (frame === null) return
            else break
          }
          if (end < 0) {
            end = size + end
            if (end < 0) {
              frame = hymnThrowError(H, `negative slice end: ${end}`)
              if (frame === null) return
              else break
            }
          }
          if (start >= end) {
            frame = hymnThrowError(H, `slice out of range: ${start} >= ${end}`)
            if (frame === null) return
            else break
          }
          const sub = original.substring(start, end)
          hymnPush(H, newString(sub))
        } else if (isArray(v)) {
          const array = v.value
          const size = array.length
          let end
          if (isInt(b)) {
            end = b.value
          } else if (isNone(b)) {
            end = size
          } else {
            frame = hymnThrowError(H, `slice can't use ${valueType(b.is)} (expected integer)`)
            if (frame === null) return
            else break
          }
          if (end > size) {
            frame = hymnThrowError(H, `slice out of bounds: ${end} > ${size}`)
            if (frame === null) return
            else break
          }
          if (end < 0) {
            end = size + end
            if (end < 0) {
              frame = hymnThrowError(H, `negative slice end: ${end}`)
              if (frame === null) return
              else break
            }
          }
          if (start >= end) {
            frame = hymnThrowError(H, `slice out of range: ${start} >= ${end}`)
            if (frame === null) return
            else break
          }
          const copy = array.slice(start, end)
          hymnPush(H, newArrayValue(copy))
        } else {
          frame = hymnThrowError(H, `can't slice ${valueType(v.is)} (expected string or array)`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_CLEAR: {
        const value = hymnPop(H)
        switch (value.is) {
          case HYMN_VALUE_BOOL:
            hymnPush(H, newBool(false))
            break
          case HYMN_VALUE_INTEGER:
            hymnPush(H, newInt(0))
            break
          case HYMN_VALUE_FLOAT:
            hymnPush(H, newFloat(0.0))
            break
          case HYMN_VALUE_STRING:
            hymnPush(H, newString(''))
            break
          case HYMN_VALUE_ARRAY: {
            const array = value.value
            array.length = 0
            hymnPush(H, value)
            break
          }
          case HYMN_VALUE_TABLE: {
            const table = value.value
            tableClear(table)
            hymnPush(H, value)
            break
          }
          case HYMN_VALUE_NONE:
          case HYMN_VALUE_FUNC:
          case HYMN_VALUE_FUNC_NATIVE:
          case HYMN_VALUE_POINTER:
            hymnPush(H, newNone())
            break
        }
        break
      }
      case OP_KEYS: {
        const value = hymnPop(H)
        if (!isTable(value)) {
          frame = hymnThrowError(H, `call to 'keys' can't use ${valueType(value.is)} (expected table)`)
          if (frame === null) return
          else break
        } else {
          const table = value.value
          const keys = tableKeys(table)
          hymnPush(H, newArrayValue(keys))
        }
        break
      }
      case OP_INDEX: {
        const b = hymnPop(H)
        const a = hymnPop(H)
        switch (a.is) {
          case HYMN_VALUE_STRING: {
            if (!isString(b)) {
              frame = hymnThrowError(H, `call to 'index' can't use ${valueType(b.is)} for 2nd argument (expected string)`)
              if (frame === null) return
              else break
            }
            const index = a.value.indexOf(b.value)
            hymnPush(H, newInt(index))
            break
          }
          case HYMN_VALUE_ARRAY:
            hymnPush(H, newInt(arrayIndexOf(a.value, b)))
            break
          case HYMN_VALUE_TABLE: {
            const key = tableKeyOf(a.value, b)
            if (key === null) hymnPush(H, newNone())
            else hymnPush(H, newString(key))
            break
          }
          default:
            frame = hymnThrowError(H, `call to 'index' can't use ${valueType(a.is)} for 1st argument (expected string, array, or table)`)
            if (frame === null) return
            else break
        }
        break
      }
      case OP_TYPE: {
        const value = hymnPop(H)
        hymnPush(H, newString(valueType(value.is)))
        break
      }
      case OP_INT: {
        const value = hymnPop(H)
        if (isInt(value)) {
          hymnPush(H, value)
        } else if (isFloat(value)) {
          hymnPush(H, newInt(parseInt(value.value)))
        } else if (isString(value)) {
          const number = Number(value.value)
          if (isNaN(number)) {
            hymnPush(H, newNone())
          } else {
            hymnPush(H, newInt(parseInt(number)))
          }
        } else {
          frame = hymnThrowError(H, `can't cast ${valueType(value.is)} to integer`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_FLOAT: {
        const value = hymnPop(H)
        if (isInt(value)) {
          hymnPush(H, newFloat(parseFloat(value.value)))
        } else if (isFloat(value)) {
          hymnPush(H, value)
        } else if (isString(value)) {
          const number = Number(value.value)
          if (isNaN(number)) {
            hymnPush(H, newNone())
          } else {
            hymnPush(H, newFloat(number))
          }
        } else {
          frame = hymnThrowError(H, `can't cast ${valueType(value.is)} to float`)
          if (frame === null) return
          else break
        }
        break
      }
      case OP_STRING: {
        const value = hymnPop(H)
        hymnPush(H, newString(valueToString(value)))
        break
      }
      case OP_ECHO: {
        const value = hymnPop(H)
        H.printLine(valueToString(value))
        break
      }
      case OP_PRINT: {
        const value = hymnPop(H)
        const route = hymnPop(H)
        if (hymnFalse(route)) {
          H.print(valueToString(value))
        } else {
          H.printError(valueToString(value))
        }
        break
      }
      case OP_SOURCE: {
        const value = hymnPop(H)
        hymnPush(H, newString(valueToInspect(value)))
        break
      }
      case OP_CODES: {
        const value = hymnPop(H)
        hymnPush(H, newString(valueToDebug(value)))
        break
      }
      case OP_STACK: {
        if (H.stackTop !== 0) {
          let debug = ''
          for (let i = 0; i < H.stackTop; i++) {
            debug += '[' + debugValueToString(H.stack[i]) + ']\n'
          }
          hymnPush(H, newString(debug))
        } else {
          hymnPush(H, newString(''))
        }
        break
      }
      case OP_REFERENCE: {
        hymnPop(H)
        hymnPush(H, newInt(0))
        break
      }
      case OP_FORMAT: {
        const value = hymnPop(H)
        hymnPush(H, newString(valueToFormat(value)))
        break
      }
      case OP_THROW: {
        frame = hymnException(H)
        if (frame === null) return
        break
      }
      case OP_DUPLICATE: {
        const top = hymnPeek(H, 1)
        hymnPush(H, top)
        break
      }
      case OP_USE: {
        const file = hymnPop(H)
        if (isString(file)) {
          frame = await hymnImport(H, file.value)
          if (frame === null) return
        } else {
          frame = hymnThrowError(H, `import can't use ${valueType(file.is)} (expected string)`)
          if (frame === null) return
          else break
        }
        break
      }
      default:
        console.error('unknown instruction')
        return
    }
  }
}

function addTable(H, name, table) {
  tablePut(H.globals, name, newTableValue(table))
}

function addFunction(H, name, func) {
  tablePut(H.globals, name, newNativeFuncValue(name, func))
}

function addPointer(H, name, pointer) {
  tablePut(H.globals, name, newPointerValue(pointer))
}

function addFunctionToTable(table, name, func) {
  tablePut(table, name, newNativeFuncValue(name, func))
}

async function debugScript(H, script, source) {
  const result = compile(H, script, source, TYPE_SCRIPT)

  const func = result.func
  if (result.error !== null) {
    return result.error
  }

  console.debug(`\n-- ${script !== null ? script : 'script'} --\n${disassembleByteCode(func.code)}`)

  const constants = func.code.constants

  for (let i = 0; i < constants.length; i++) {
    const constant = constants[i]
    if (isFunc(constant)) {
      const value = constant.value
      console.debug(`\n-- ${value.name !== null ? value.name : 'script'} --\n${disassembleByteCode(value.code)}`)
    }
  }

  hymnResetStack(H)

  return null
}

async function interpretScript(H, script, source, type) {
  const result = compile(H, script, source, type)

  const func = result.func
  if (result.error !== null) {
    return result.error
  }

  const funcVal = newFuncValue(func)
  hymnPush(H, funcVal)
  hymnCall(H, func, 0)

  await hymnRun(H)
  if (H.error !== null) return H.error

  hymnResetStack(H)
  return null
}

async function interpret(H, source) {
  return interpretScript(H, null, source, TYPE_SCRIPT)
}

async function direct(H, source) {
  return interpretScript(H, null, source, TYPE_DIRECT)
}

function newVM() {
  const H = new Hymn()

  if (node) {
    H.paths.push(newString('<parent>/<path>.hm'))
    H.paths.push(newString('./<path>.hm'))
    H.paths.push(newString('./libs/<path>.hm'))
  } else {
    const address = window.location.href
    const url = address.substring(0, address.lastIndexOf('/') + 1)
    H.paths.push(newString(url + '<path>.hm'))
    H.paths.push(newString(url + 'libs/<path>.hm'))
    H.paths.push(newString('/<path>.hm'))
    H.paths.push(newString('/libs/<path>.hm'))
  }

  tablePut(H.globals, 'PATHS', newArrayValue(H.paths))
  tablePut(H.globals, 'IMPORTS', newTableValue(H.imports))
  tablePut(H.globals, 'GLOBALS', newTableValue(H.globals))

  return H
}

class Format {
  constructor(source) {
    this.source = source
    this.size = source.length
    this.dest = new Array()
    this.s = 0
    this.deep = 0
    this.compact = new Array()
    this.nest = new Array()
  }
}

function isImportant(f, len) {
  let start = len - 2
  while (start >= 0) {
    if (!isIdent(f.dest[start])) break
    start--
  }
  const word = f.dest.slice(start + 1, len).join('')
  switch (word) {
    case 'in':
    case 'if':
    case 'for':
    case 'not':
    case 'else':
    case 'elif':
    case 'echo':
    case 'while':
    case 'return':
      return true
    default:
      return false
  }
}

function space(f, t) {
  switch (t) {
    case ' ':
    case '\n':
    case '\t':
    case '\r':
      return
  }
  const len = f.dest.length
  if (len === 0) return
  const p = f.dest[len - 1]
  if (p === '\n') {
    indent(f)
    return
  } else if (p === ':') {
    if (f.nest.length === 0) {
      f.dest.push(' ')
    } else {
      const nest = f.nest[f.nest.length - 1]
      if (nest !== '[') {
        f.dest.push(' ')
      }
    }
    return
  } else if (p === '~') {
    return
  }
  if (isDigit(t) || isIdent(t)) {
    switch (p) {
      case ' ':
      case '.':
      case '(':
      case '[':
        return
      case '>':
        if (len >= 2 && f.dest[len - 2] === '-') return
        break
      case '-':
        if (len >= 3) {
          const b = f.dest[len - 3]
          if (b === ',' || b === '{') return
          if (isIdent(b) && isImportant(f, len - 2)) return
        }
        if (len >= 2) {
          const b = f.dest[len - 2]
          if (b !== ')' && b !== ' ') return
        }
        break
    }
    f.dest.push(' ')
    return
  }
  switch (t) {
    case '{':
      if (p === '(') return
      break
    case '(':
      if (isDigit(p)) return
      if (isIdent(p)) {
        if (isImportant(f, len)) break
        return
      }
      switch (p) {
        case ' ':
        case '(':
        case ')':
        case '[':
        case '}':
        case ']':
          return
      }
      break
    case '[':
      if (isIdent(p)) {
        if (isImportant(f, len)) break
        return
      }
      switch (p) {
        case '+':
        case '=':
        case ',':
        case '{':
          break
        default:
          return
      }
      break
    case '<':
      if (p === '<') return
      break
    case '>':
      if (p === '-' || p === '>') return
      break
    case '=':
      switch (p) {
        case '-':
        case '+':
        case '*':
        case '/':
        case '<':
        case '>':
        case '~':
        case '^':
        case '%':
        case '&':
        case '|':
        case '!':
        case '=':
          return
      }
      break
    case '-':
      if (f.s < f.size && f.source[f.s] === '>') return
      if (p === ',' || p === '{' || p === ')') break
      if (isIdent(p) || isDigit(p)) break
      return
    case '"':
    case "'":
      switch (p) {
        case '(':
        case '[':
          return
      }
      break
    case ':':
    case ',':
    case '.':
    case ')':
    case ']':
      return
  }
  f.dest.push(' ')
}

function skip(f) {
  if (f.s >= f.size) return
  let c = f.source[f.s]
  while (c === ' ' || c === '\t' || c === '\r') {
    f.s++
    if (f.s >= f.size) return
    c = f.source[f.s]
  }
}

function indent(f) {
  for (let a = 0; a < f.deep; a++) {
    f.dest.push(' ')
    f.dest.push(' ')
  }
}

function newline(f) {
  f.dest.push('\n')
  skip(f)
  if (f.s >= f.size) return
  const c = f.source[f.s]
  if (c === ')' || c === ']' || c === '}') {
    f.s++
    if (f.deep > 0) f.deep--
    indent(f)
    f.dest.push(c)
  }
}

function possibleNewline(f) {
  for (let p = f.dest.length - 1; p >= 0; p--) {
    const o = f.dest[p]
    if (o === '\n') {
      return
    } else if (o === ' ' || o === '\t' || o === '\r') {
      continue
    } else {
      newline(f)
      return
    }
  }
}

function stringly(f, c) {
  const source = f.source
  const dest = f.dest
  dest.push(c)
  while (f.s < f.size) {
    const n = source[f.s]
    dest.push(n)
    f.s++
    if (n === '\\') {
      dest.push(source[f.s++])
    } else if (n === c) {
      break
    }
  }
  let a = f.s
  let multiple = false
  loop: while (a < f.size) {
    const n = source[a]
    if (n === '\n') {
      for (let d = a + 1; d < f.size; d++) {
        const m = source[d]
        if (m === '"' || m === "'") {
          if (!multiple) {
            multiple = true
            f.deep++
          }
          dest.push('\n')
          indent(f)
          dest.push(m)
          f.s = d + 1
          while (f.s < f.size) {
            const i = source[f.s]
            dest.push(i)
            f.s++
            if (i === '\\') {
              dest.push(source[f.s++])
            } else if (i === m) {
              a = f.s
              continue loop
            }
          }
          break loop
        }
        if (m !== ' ' && m !== '\t' && m !== '\r' && m !== '\n') break loop
      }
      break loop
    }
    if (n === '"' || n === "'") {
      space(f, n)
      dest.push(n)
      f.s = a + 1
      while (f.s < f.size) {
        const i = source[f.s]
        dest.push(i)
        f.s++
        if (i === '\\') {
          dest.push(source[f.s++])
        } else if (i === n) {
          a = f.s
          continue loop
        }
      }
    }
    if (n !== ' ' && n !== '\t' && n !== '\r') break
    a++
  }
  if (multiple) {
    if (f.deep > 0) f.deep--
    possibleNewline(f)
  }
}

function format(source) {
  const f = new Format(source)
  const dest = f.dest
  const compact = f.compact
  const nest = f.nest
  skip(f)
  while (f.s < f.size) {
    const c = source[f.s++]
    space(f, c)
    if (isDigit(c)) {
      dest.push(c)
      if (f.s < f.size) {
        if (c === '0') {
          const n = source[f.s]
          if (n === 'x') {
            dest.push(n)
            f.s++
            while (f.s < f.size) {
              const m = source[f.s]
              if (!(m >= '0' && m <= '9') && !(m >= 'a' && m <= 'f')) break
              dest.push(m)
              f.s++
            }
            continue
          } else if (n === 'b') {
            dest.push(n)
            f.s++
            while (f.s < f.size) {
              const m = source[f.s]
              if (m !== '0' && m !== '1') break
              dest.push(m)
              f.s++
            }
            continue
          }
        }
        while (f.s < f.size) {
          const n = source[f.s]
          if (!isDigit(n)) {
            if (n === '.') {
              dest.push(n)
              f.s++
              while (f.s < f.size) {
                const d = source[f.s]
                if (!isDigit(d)) {
                  if (d === 'e' || d === 'E') {
                    dest.push(d)
                    f.s++
                    if (f.s < f.size) {
                      const m = source[f.s]
                      if (m === '+' || m === '-') {
                        dest.push(m)
                        f.s++
                      }
                      while (f.s < f.size) {
                        const e = source[f.s]
                        if (!isDigit(e)) break
                        dest.push(e)
                        f.s++
                      }
                    }
                    break
                  }
                  break
                }
                dest.push(d)
                f.s++
              }
              break
            } else if (n === 'e' || n === 'E') {
              dest.push(n)
              f.s++
              if (f.s < f.size) {
                const m = source[f.s]
                if (m === '+' || m === '-') {
                  dest.push(m)
                  f.s++
                }
                while (f.s < f.size) {
                  const e = source[f.s]
                  if (!isDigit(e)) break
                  dest.push(e)
                  f.s++
                }
              }
              break
            } else {
              break
            }
          }
          dest.push(n)
          f.s++
        }
      }
      continue
    } else if (isIdent(c)) {
      dest.push(c)
      while (f.s < f.size) {
        const n = source[f.s]
        if (isIdent(n)) {
          dest.push(n)
          f.s++
        } else if (n === '-' && f.s + 1 < f.size) {
          const m = source[f.s + 1]
          if (isIdent(m)) {
            dest.push('-')
            dest.push(m)
            f.s += 2
          } else {
            break
          }
        } else {
          break
        }
      }
      continue
    }
    switch (c) {
      case '(': {
        dest.push(c)
        nest.push(c)
        for (let a = f.s; a < f.size; a++) {
          const n = source[a]
          if (n === '\n') {
            f.deep++
            newline(f)
            compact.push(false)
            break
          } else if (n === ')') {
            compact.push(true)
            break
          }
        }
        break
      }
      case '[': {
        dest.push(c)
        nest.push(c)
        for (let a = f.s; a < f.size; a++) {
          const n = source[a]
          if (n === '\n') {
            f.deep++
            newline(f)
            compact.push(false)
            break
          } else if (n === ']') {
            compact.push(true)
            break
          }
        }
        break
      }
      case '{': {
        dest.push(c)
        nest.push(c)
        let any = false
        for (let a = f.s; a < f.size; a++) {
          const n = source[a]
          if (n === '\n') {
            let ok = true
            if (!any) {
              for (let d = a + 1; d < f.size; d++) {
                const m = source[d]
                if (m === '}') {
                  ok = false
                  dest.push('\n')
                  indent(f)
                  dest.push('}')
                  f.s = d + 1
                  break
                } else if (m !== ' ' && m !== '\t' && m !== '\r' && m !== '\n') {
                  break
                }
              }
            }
            if (ok) {
              f.deep++
              newline(f)
              compact.push(false)
            }
            break
          } else if (n === '}') {
            if (any) {
              compact.push(true)
            } else {
              dest.push('}')
              f.s = a + 1
            }
            break
          } else if (n !== ' ' && n !== '\t' && n !== '\r') {
            any = true
          }
        }
        break
      }
      case ')':
      case ']':
      case '}': {
        nest.pop()
        if (!compact.pop()) {
          if (f.deep > 0) f.deep--
          possibleNewline(f)
        }
        dest.push(c)
        break
      }
      case "'":
      case '"':
        stringly(f, c)
        break
      case '#': {
        dest.push(c)
        while (f.s < f.size) {
          const n = source[f.s]
          dest.push(n)
          if (n === '\n') break
          f.s++
        }
        break
      }
      case '\n': {
        let twice = false
        while (f.s < f.size) {
          const n = source[f.s]
          if (n === '\n') {
            f.s++
            twice = true
          } else if (n === ' ' || n === '\t' || n === '\r') {
            f.s++
          } else {
            break
          }
        }
        possibleNewline(f)
        if (twice) {
          newline(f)
        }
        break
      }
      case ' ':
      case '\t':
      case '\r':
        break
      default:
        dest.push(c)
        break
    }
  }
  return dest.join('')
}

if (node) {
  module.exports = {
    version: HYMN_VERSION,
    HymnTable: HymnTable,
    isFloat: isFloat,
    isString: isString,
    isFuncNative: isFuncNative,
    isPointer: isPointer,
    newNone: newNone,
    newBool: newBool,
    newFloat: newFloat,
    newString: newString,
    newArrayValue: newArrayValue,
    newNativeFuncValue: newNativeFuncValue,
    newPointerValue: newPointerValue,
    newFunction: newFunction,
    addTable: addTable,
    addFunction: addFunction,
    addPointer: addPointer,
    addFunctionToTable: addFunctionToTable,
    interpretScript: interpretScript,
    interpret: interpret,
    debug: debugScript,
    direct: direct,
    format: format,
    newVM: newVM,
  }
}