///|
fn text_primitive(word : String) -> Bool {
  ["save-input", "restore-input", "refill"].contains(word) ||
  string_word(word) ||
  [
    "char", "char+", "chars", "bl", "count", "type", "emit", "cr", "space", "spaces",
    "base", "hex", "decimal", "u.", ".r", "u.r", "source", "source-id", ">in", "parse",
    "parse-name", "word", "pad", "evaluate", "catch", "throw", "abort", "abort-message",
    "-trailing", "/string", "compare", "search", "blank", "?dup", "2over", "pick",
    "roll", "true", "false", "2@", "2!", "unused",
  ].contains(word)
}

///|
fn Machine::text_word(
  self : Machine,
  word : String,
  depth : Int,
) -> Unit raise ForthError {
  match word {
    "char" => self.stack.push(@utf8.encode(self.source_name())[0].to_int())
    "chars" => {
      let n = self.pop()
      self.stack.push(n)
    }
    "char+" => {
      let n = self.pop()
      self.stack.push(n + 1)
    }
    "bl" => self.stack.push(32)
    "count" => {
      let address = self.pop()
      let n = self.load(address, false)
      self.stack.push(address + 1)
      self.stack.push(n)
    }
    "type" => {
      let count = self.pop()
      let address = self.pop()
      let data = self.read_bytes(address, count)
      self.write_bytes(data)
    }
    "emit" => self.write_bytes([self.pop().to_byte()])
    "cr" => self.write_text("\n")
    "space" => self.write_text(" ")
    "spaces" => {
      let count = self.pop()
      if count > 2000000 {
        raise Invalid("output byte limit")
      }
      if count > 0 {
        self.write_bytes(Array::make(count, b' '))
      }
    }
    "base" => self.stack.push(-8)
    "hex" => self.base = 16
    "decimal" => self.base = 10
    "u." | ".r" | "u.r" => {
      let width = if word == "u." { 0 } else { self.pop() }
      let text = self.format_integer(self.pop(), word != ".r")
      if width > 2000000 {
        raise Invalid("output byte limit")
      }
      if width > text.length() {
        self.write_bytes(Array::make(width - text.length(), b' '))
      }
      self.write_text(text + (if word == "u." { " " } else { "" }))
    }
    "source" => {
      let frame = self.source_frame()
      self.stack.push(frame.address)
      self.stack.push(frame.data.length())
    }
    "source-id" => self.stack.push(-1)
    ">in" => self.stack.push(-12)
    "parse" => {
      let delimiter = self.pop()
      let (address, count) = self.parse_source(delimiter, false)
      self.stack.push(address)
      self.stack.push(count)
    }
    "parse-name" => {
      let frame = self.source_frame()
      while frame.cursor < frame.data.length() &&
            frame.data[frame.cursor].to_int() <= 32 {
        frame.cursor += 1
      }
      let start = frame.cursor
      ignore(frame.name())
      let mut end = frame.cursor
      if end > start && frame.data[end - 1].to_int() <= 32 {
        end -= 1
      }
      self.stack.push(frame.address + start)
      self.stack.push(end - start)
    }
    "word" => {
      let delimiter = self.pop()
      let (address, count) = self.parse_source(delimiter, true)
      if count > 255 {
        raise Invalid("WORD exceeds 255 bytes")
      }
      let data = self.read_bytes(address, count)
      self.scratch[0] = count.to_byte()
      for i, byte in data {
        self.scratch[i + 1] = byte
      }
      self.stack.push(65536)
    }
    "pad" => self.stack.push(65792)
    "evaluate" => {
      let count = self.pop()
      let address = self.pop()
      let text = @utf8.decode(
        Bytes::from_array(self.read_bytes(address, count)[:])[:],
      ) catch {
        _ => raise Invalid("EVALUATE requires UTF-8")
      }
      self.interpret_source(text, depth + 1)
    }
    "catch" => self.catch_word(depth)
    "throw" => {
      let code = self.pop()
      if code != 0 {
        raise Thrown(code)
      }
    }
    "abort" => raise Thrown(-1)
    "abort-message" => {
      let count = self.pop()
      let address = self.pop()
      let flag = self.pop()
      if flag != 0 {
        if self.catch_depth == 0 {
          self.write_bytes(self.read_bytes(address, count))
        }
        raise Thrown(-2)
      }
    }
    "save-input" => {
      let frame = self.source_frame()
      self.stack.push(frame.cursor)
      self.stack.push(frame.address)
      self.stack.push(2)
    }
    "restore-input" => {
      let n = self.pop()
      if n < 0 || n > self.stack.length() {
        raise Invalid("invalid saved input dimensions")
      }
      if n != 2 {
        for _ in 0.. frame.data.length() {
          self.stack.push(-1)
        } else {
          frame.cursor = cursor
          self.stack.push(0)
        }
      }
    }
    "refill" => self.stack.push(0)
    "-trailing" => {
      let count = self.pop()
      let address = self.pop()
      let data = self.read_bytes(address, count)
      let mut end = count
      while end > 0 && data[end - 1] == b' ' {
        end -= 1
      }
      self.stack.push(address)
      self.stack.push(end)
    }
    "/string" => {
      let n = self.pop()
      let count = self.pop()
      let address = self.pop()
      if n < 0 || n > count {
        raise Invalid("invalid /STRING range")
      }
      self.stack.push(address + n)
      self.stack.push(count - n)
    }
    "compare" | "search" => self.compare_string(word)
    "blank" => {
      let count = self.pop()
      let address = self.pop()
      self.memory_range(address, count)
      for i in 0.. {
      let value = self.pop()
      self.stack.push(value)
      if value != 0 {
        self.stack.push(value)
      }
    }
    "2over" => {
      let n = self.stack.length()
      if n < 4 {
        raise Invalid("stack underflow")
      }
      let a = self.stack[n - 4]
      let b = self.stack[n - 3]
      self.stack.push(a)
      self.stack.push(b)
    }
    "pick" | "roll" => {
      let offset = self.pop()
      if offset < 0 || offset >= self.stack.length() {
        raise Invalid("invalid stack index")
      }
      let i = self.stack.length() - 1 - offset
      let value = self.stack[i]
      if word == "roll" {
        for j in i..<(self.stack.length() - 1) {
          self.stack[j] = self.stack[j + 1]
        }
        ignore(self.stack.pop())
      }
      self.stack.push(value)
    }
    "true" => self.stack.push(-1)
    "false" => self.stack.push(0)
    "unused" => self.stack.push(65536 - self.memory.length())
    "2@" => {
      let address = self.pop()
      let low = self.load(address + 4, true)
      let high = self.load(address, true)
      self.stack.push(low)
      self.stack.push(high)
    }
    "2!" => {
      let address = self.pop()
      let high = self.pop()
      let low = self.pop()
      self.memory_range(address, 8)
      self.save(address, high, true)
      self.save(address + 4, low, true)
    }
    _ =>
      if string_word(word) {
        self.execute(self.string_code(word), depth)
      } else {
        raise Invalid("unknown word: " + word)
      }
  }
}

///|
fn Machine::compare_string(
  self : Machine,
  word : String,
) -> Unit raise ForthError {
  let length2 = self.pop()
  let address2 = self.pop()
  let length1 = self.pop()
  let address1 = self.pop()
  let a = self.read_bytes(address1, length1)
  let b = self.read_bytes(address2, length2)
  if word == "compare" {
    for i in 0.. length2 {
        1
      } else {
        0
      },
    )
  } else {
    if length2 == 0 {
      self.stack.push(address1)
      self.stack.push(length1)
      self.stack.push(-1)
      return
    }
    let prefix = Array::make(length2, 0)
    let mut matched = 0
    for i in 1.. 0 && b[i] != b[matched] {
        matched = prefix[matched - 1]
      }
      if b[i] == b[matched] {
        matched += 1
      }
      prefix[i] = matched
      self.fuel -= 1
      if self.fuel < 0 {
        raise Invalid("execution budget exhausted")
      }
    }
    matched = 0
    for i in 0.. 0 && a[i] != b[matched] {
        matched = prefix[matched - 1]
      }
      if a[i] == b[matched] {
        matched += 1
      }
      self.fuel -= 1
      if self.fuel < 0 {
        raise Invalid("execution budget exhausted")
      }
      if matched == length2 {
        let offset = i + 1 - length2
        self.stack.push(address1 + offset)
        self.stack.push(length1 - offset)
        self.stack.push(-1)
        return
      }
    }
    self.stack.push(address1)
    self.stack.push(length1)
    self.stack.push(0)
  }
}