#
# Jupiter ACE Forth Interpreter and Compiler
#
# Copyright 2025 Pedro Gimeno Fortea
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

import sys, getopt

# Python 2-3 interoperability
unichr = getattr(__builtins__, 'unichr', chr)

def version():
  sys.stdout.write("JAForth.py 1.0\n")

def openFileWrite(name, binary, append = False):
  if name == '-':
    return getattr(sys.stdout, 'buffer', sys.stdout) if binary else sys.stdout
  if name == '2-':
    return getattr(sys.stderr, 'buffer', sys.stderr) if binary else sys.stderr
  mode = ('a' if append else 'w') + ('b' if binary else '')

  return open(name, mode)

def openFileRead(name, binary):
  if name == '-':
    return getattr(sys.stdin, 'buffer', sys.stdin) if binary else sys.stdin
  return open(name, 'rb' if binary else 'r')

def closeFile(f, name):
  if name not in {'-', '2-'}:
    f.close()

# FORTH error exception
class ForthError(Exception):
  pass

class InternalAbort(Exception):
  pass

class sv:
  # Addresses of system variables we use
  HLD     = 0x3C1A
  CURRENT = 0x3C31
  CONTEXT = 0x3C33
  VOCLNK  = 0x3C35
  STKBOT  = 0x3C37
  DICT    = 0x3C39
  SPARE   = 0x3C3B
  ERR_NO  = 0x3C3D
  FLAGS   = 0x3C3E
  BASE    = 0x3C3F
  # Not a system variable, but a memory area
  PAD     = 0x2701

class marker:
  # The different marker bytes used by the ROM to check for proper nesting
  BEGIN    = 1
  IF       = 2
  ELSE     = 2
  DO       = 3
  WHILE    = 4
  colon    = 10
  COMPILER = 11
  DEFINER  = 12

# Addresses of ROM words (what FIND reports)
WordsROM = {
  'FORTH'      : 0x3C4A,
  'UFLOAT'     : 0x1D59,
  'INT'        : 0x1D22,
  'FNEGATE'    : 0x1D0F,
  'F/'         : 0x1C7B,
  'F*'         : 0x1C4B,
  'F+'         : 0x1BB1,
  'F-'         : 0x1BA4,
  'LOAD'       : 0x198A,
  'BVERIFY'    : 0x1979,
  'VERIFY'     : 0x1967,
  'BLOAD'      : 0x1954,
  'BSAVE'      : 0x1944,
  'SAVE'       : 0x1934,
  'LIST'       : 0x1670,
  'EDIT'       : 0x165E,
  'FORGET'     : 0x1638,
  'REDEFINE'   : 0x13FD,
  'EXIT'       : 0x13F0,
  '."'         : 0x1388,
  '('          : 0x1361,
  '['          : 0x13D5,
  '+LOOP'      : 0x12D0,
  'LOOP'       : 0x12BD,
  'DO'         : 0x12AB,
  'UNTIL'      : 0x1263,
  'REPEAT'     : 0x124C,
  'BEGIN'      : 0x121A,
  'THEN'       : 0x1207,
  'ELSE'       : 0x11EC,
  'WHILE'      : 0x11D5,
  'IF'         : 0x11C0,
  ']'          : 0x13E1,
  'LEAVE'      : 0x1316,
  'J'          : 0x1302,
  "I'"         : 0x12F7,
  'I'          : 0x12E9,
  'DEFINITIONS': 0x11AB,
  'VOCABULARY' : 0x117D,
  'IMMEDIATE'  : 0x1160,
  'RUNS>'      : 0x1125,
  'DOES>'      : 0x10B4,
  'COMPILER'   : 0x10F5,
  'CALL'       : 0x10A7,
  'DEFINER'    : 0x1074,
  'ASCII'      : 0x1028,
  'LITERAL'    : 0x1006,
  'CONSTANT'   : 0x0FE2,
  'VARIABLE'   : 0x0FCF,
  'ALLOT'      : 0x0F76,
  'C,'         : 0x0F5F,
  ','          : 0x0F4E,
  'CREATE'     : 0x0ED0,
  ':'          : 0x0EAF,
  'DECIMAL'    : 0x0EA3,
  'MIN'        : 0x0E87,
  'MAX'        : 0x0E75,
  'XOR'        : 0x0E60,
  'AND'        : 0x0E4B,
  'OR'         : 0x0E36,
  '2-'         : 0x0E29,
  '1-'         : 0x0E1F,
  '2+'         : 0x0E13,
  '1+'         : 0x0E09,
  'D+'         : 0x0DEE,
  '-'          : 0x0DE1,
  '+'          : 0x0DD2,
  'DNEGATE'    : 0x0DBA,
  'NEGATE'     : 0x0DA9,
  'U/MOD'      : 0x0D8C,
  '*/'         : 0x0D7A,
  '*'          : 0x0D6D,
  'MOD'        : 0x0D61,
  '/'          : 0x0D51,
  '*/MOD'      : 0x0D31,
  '/MOD'       : 0x0D00,
  'U*'         : 0x0CA8,
  'D<'         : 0x0C83,
  'U<'         : 0x0C72,
  '<'          : 0x0C65,
  '>'          : 0x0C56,
  '='          : 0x0C4A,
  '0>'         : 0x0C3A,
  '0<'         : 0x0C2E,
  '0='         : 0x0C1A,
  'ABS'        : 0x0C0D,
  'OUT'        : 0x0BFD,
  'IN'         : 0x0BEB,
  'INKEY'      : 0x0BDB,
  'BEEP'       : 0x0B98,
  'PLOT'       : 0x0B4A,
  'AT'         : 0x0B19,
  'F.'         : 0x0AAF,
  'EMIT'       : 0x0AA3,
  'CR'         : 0x0A95,
  'SPACES'     : 0x0A83,
  'SPACE'      : 0x0A73,
  'HOLD'       : 0x0A5C,
  'CLS'        : 0x0A1D,
  '#'          : 0x09F7,
  '#S'         : 0x09E1,
  'U.'         : 0x09D0,
  '.'          : 0x09B3,
  'SIGN'       : 0x0A4A,
  '#>'         : 0x099C,
  '<#'         : 0x098D,
  'TYPE'       : 0x096E,
  'ROLL'       : 0x0933,
  'PICK'       : 0x0925,
  'OVER'       : 0x0912,
  'ROT'        : 0x08FF,
  '?DUP'       : 0x08EE,
  'R>'         : 0x08DF,
  '>R'         : 0x08D2,
  '!'          : 0x08C1,
  '@'          : 0x08B3,
  'C!'         : 0x08A5,
  'C@'         : 0x0896,
  'SWAP'       : 0x0885,
  'DROP'       : 0x0879,
  'DUP'        : 0x086B,
  'SLOW'       : 0x0846,
  'FAST'       : 0x0837,
  'INVIS'      : 0x0828,
  'VIS'        : 0x0818,
  'CONVERT'    : 0x078A,
  'NUMBER'     : 0x06A9,
  'EXECUTE'    : 0x069A,
  'FIND'       : 0x063D,
  'VLIST'      : 0x062D,
  'WORD'       : 0x05AB,
  'RETYPE'     : 0x0578,
  'QUERY'      : 0x058C,
  'LINE'       : 0x0506,
  ';'          : 0x04A1,
  'PAD'        : 0x0499,
  'BASE'       : 0x048A,
  'CURRENT'    : 0x0480,
  'CONTEXT'    : 0x0473,
  'HERE'       : 0x0460,
  'ABORT'      : 0x00AB,
  'QUIT'       : 0x0099,

  # Internal words. Can't be parsed by accident because they contain
  # lowercase letters. Used by the code to reference certain internal
  # addresses.
  # Some of these words perform the same action, but they have different
  # compilation addresses to trace them back to the original word while
  # decompiling.
  'compile-int': 0x1006,  # Compiles a stk-int and the int on the stack.
  'stk-int'    : 0x1011,  # Pushes the next int.
  'compile-fp' : 0x1055,  # Compiles a stk-fp and the 2 ints on the stack.
  'stk-fp'     : 0x1064,  # Pushes the next 2 ints. Decompiles as float.
  'stk-byte'   : 0x104B,  # Run-time action for ASCII (push next byte)
  'pr-pas-str' : 0x1396,  # Run-time action for ." (prints pascal string)
  'ig-pas-str' : 0x1379,  # Run-time action for ( (ignores pascal string)
  'brloopn'    : 0x133C,  # Run-time action for +LOOP (+, branchif<, rpop)
  'brloop'     : 0x1332,  # Run-time action for LOOP (+1, branchif<, rpop)
  'dosetup'    : 0x1323,  # Run-time-action for DO (sets up rstk for loop)
  'brfalse-U'  : 0x128D,  # Run-time action for UNTIL (branch-if-zero)
  'branch-R'   : 0x1276,  # Run-time action for REPEAT (branch)
  'nop-B'      : 0x129F,  # Run-time action for BEGIN (nop)
  'nop-T'      : 0x12A4,  # Run-time action for THEN (nop)
  'branch-E'   : 0x1271,  # Run-time action for ELSE (branch)
  'brfalse-W'  : 0x1288,  # Run-time action for WHILE (branch-if-zero)
  'brfalse-I'  : 0x1283,  # Run-time action for IF (branch-if-zero)
  'exit-R'     : 0x1140,  # Run-time action for RUNS> part 1 (exit)
  'exit-D'     : 0x10E8,  # Run-time action for DOES> (exit)
  'exit-;'     : 0x04B6,  # Run-time action for ; (exit)

}

NamedRoutines = {
  # These point to actual routines, not pointers. Used by name in the code.
  'docolon'    : 0x0EC3,  # Execute a colon definition finished with EXIT
  'stkparam-V' : 0x0FF0,  # Stack the param field of the word (VARIABLE)
  'stkparam-C' : 0x0FEC,  # Stack the param field of the word (CREATE)
  'stkvalue'   : 0x0FF5,  # Stack the value at the param field (CONSTANT)
  'runs2'      : 0x1142,  # Run-time action for RUNS> part 2
  'set-context': 0x11B5,  # Action for words created with VOCABULARY
  'compile'    : 0x1108,  # Checks compiling mode & encloses the next word
  'define'     : 0x1085,  # Creates a word
}

# Words contain a pointer to a routine. This table maps the address of the
# pointer to the address of the routine. This allows user programs to create
# words that point to one of these routines.
Pointers = {
#  0x3C4A: Interpreter.FORTH,
  0x1D59: 0x1D5B,  # UFLOAT
  0x1D22: 0x1D24,  # INT
  0x1D0F: 0x1D11,  # FNEGATE
  0x1C7B: 0x1C7D,  # F/
  0x1C4B: 0x1C4D,  # F*
  0x1BB1: 0x1BB3,  # F+
  0x1BA4: 0x0EC3,  # F-
  0x198A: 0x0EC3,  # LOAD
  0x1979: 0x0EC3,  # BVERIFY
  0x1967: 0x0EC3,  # VERIFY
  0x1954: 0x0EC3,  # BLOAD
  0x1944: 0x0EC3,  # BSAVE
  0x1934: 0x0EC3,  # SAVE
  0x1670: 0x1672,  # LIST
  0x165E: 0x1660,  # EDIT
  0x1638: 0x163A,  # FORGET
  0x13FD: 0x13FF,  # REDEFINE
  0x13F0: 0x04B8,  # EXIT
  0x1388: 0x1108,  # ."
  0x1396: 0x1398,  # pr-pas-str
  0x1379: 0x137B,  # ig-pas-str
  0x1361: 0x1108,  # (
  0x13D5: 0x13D7,  # [
  0x133C: 0x133E,  # brloopn
  0x12D0: 0x1108,  # +LOOP
  0x1332: 0x1334,  # brloop
  0x12BD: 0x1108,  # LOOP
  0x1323: 0x1325,  # dosetup
  0x12AB: 0x1108,  # DO
  0x128D: 0x128F,  # brfalse-U
  0x1263: 0x1108,  # UNTIL
  0x1276: 0x1278,  # branch-R
  0x124C: 0x1108,  # REPEAT
  0x129F: 0x04B9,  # nop-B
  0x121A: 0x1108,  # BEGIN
  0x12A4: 0x04B9,  # nop-T
  0x1207: 0x1108,  # THEN
  0x1271: 0x1278,  # branch-E
  0x11EC: 0x1108,  # ELSE
  0x1288: 0x128F,  # brfalse-W
  0x11D5: 0x1108,  # WHILE
  0x1283: 0x128F,  # brfalse-I
  0x11C0: 0x1108,  # IF
  0x13E1: 0x13E3,  # ]
  0x1316: 0x1318,  # LEAVE
  0x1302: 0x1304,  # J
  0x12F7: 0x12F9,  # I'
  0x12E9: 0x12EB,  # I
  0x11AB: 0x11AD,  # DEFINITIONS
  0x117D: 0x1085,  # VOCABULARY
  0x1160: 0x0EC3,  # IMMEDIATE
  0x1140: 0x04B8,  # exit-R
  0x1125: 0x1108,  # RUNS>
  0x10E8: 0x04B8,  # exit-D
  0x10B4: 0x1108,  # DOES>
  0x10F5: 0x1085,  # COMPILER
  0x10A7: 0x10A9,  # CALL
  0x1074: 0x1085,  # DEFINER
  0x104B: 0x104D,  # stkbyte
  0x1028: 0x0EC3,  # ASCII
  0x1006: 0x1108,  # LITERAL
  0x0FE2: 0x1085,  # CONSTANT
  0x0FCF: 0x1085,  # VARIABLE
  0x0F76: 0x0F78,  # ALLOT
  0x0F5F: 0x0EC3,  # C,
  0x0F4E: 0x0EC3,  # ,
  0x0ED0: 0x0EC3,  # CREATE
  0x0EAF: 0x1085,  # :
  0x0EA3: 0x0EA5,  # DECIMAL
  0x0E87: 0x0EC3,  # MIN
  0x0E75: 0x0EC3,  # MAX
  0x0E60: 0x0E62,  # XOR
  0x0E4B: 0x0E4D,  # AND
  0x0E36: 0x0E38,  # OR
  0x0E29: 0x0E2B,  # 2-
  0x0E1F: 0x0E21,  # 1-
  0x0E13: 0x0E15,  # 2+
  0x0E09: 0x0E0B,  # 1+
  0x0DEE: 0x0DF0,  # D+
  0x0DE1: 0x0EC3,  # -
  0x0DD2: 0x0DD4,  # +
  0x0DBA: 0x0DBC,  # DNEGATE
  0x0DA9: 0x0DAB,  # NEGATE
  0x0D8C: 0x0EC3,  # U/MOD
  0x0D7A: 0x0EC3,  # */
  0x0D6D: 0x0EC3,  # *
  0x0D61: 0x0EC3,  # MOD
  0x0D51: 0x0EC3,  # /
  0x0D31: 0x0EC3,  # */MOD
  0x0D00: 0x0EC3,  # /MOD
  0x0CA8: 0x0CAA,  # U*
  0x0C83: 0x0C85,  # D<
  0x0C72: 0x0C74,  # U<
  0x0C65: 0x0EC3,  # <
  0x0C56: 0x0C58,  # >
  0x0C4A: 0x0EC3,  # =
  0x0C3A: 0x0C3C,  # 0>
  0x0C2E: 0x0C30,  # 0<
  0x0C1A: 0x0C1C,  # 0=
  0x0C0D: 0x0EC3,  # ABS
  0x0BFD: 0x0BFF,  # OUT
  0x0BEB: 0x0BED,  # IN
  0x0BDB: 0x0BDD,  # INKEY
  0x0B98: 0x0EC3,  # BEEP
  0x0B4A: 0x0B4C,  # PLOT
  0x0B19: 0x0B1B,  # AT
  0x0AAF: 0x0AB1,  # F.
  0x0AA3: 0x0AA5,  # EMIT
  0x0A95: 0x0A97,  # CR
  0x0A83: 0x0A85,  # SPACES
  0x0A73: 0x0A75,  # SPACE
  0x0A5C: 0x0A5E,  # HOLD
  0x0A1D: 0x0A1F,  # CLS
  0x09F7: 0x0EC3,  # #
  0x09E1: 0x0EC3,  # #S
  0x09D0: 0x0EC3,  # U.
  0x09B3: 0x0EC3,  # .
  0x0A4A: 0x0A4C,  # SIGN
  0x099C: 0x099E,  # #>
  0x098D: 0x098F,  # <#
  0x096E: 0x0970,  # TYPE
  0x0933: 0x0935,  # ROLL
  0x0925: 0x0927,  # PICK
  0x0912: 0x0EC3,  # OVER
  0x08FF: 0x0EC3,  # ROT
  0x08EE: 0x08F0,  # ?DUP
  0x08DF: 0x08E1,  # R>
  0x08D2: 0x08D4,  # >R
  0x08C1: 0x08C3,  # !
  0x08B3: 0x08B5,  # @
  0x08A5: 0x08A7,  # C!
  0x0896: 0x0898,  # C@
  0x0885: 0x0887,  # SWAP
  0x0879: 0x087B,  # DROP
  0x086B: 0x086D,  # DUP
  0x0846: 0x0848,  # SLOW
  0x0837: 0x0839,  # FAST
  0x0828: 0x082A,  # INVIS
  0x0818: 0x081A,  # VIS
  0x078A: 0x0EC3,  # CONVERT
  0x06A9: 0x06AB,  # NUMBER
  0x069A: 0x069C,  # EXECUTE
  0x063D: 0x063F,  # FIND
  0x062D: 0x062F,  # VLIST
  0x05AB: 0x05AD,  # WORD
  0x0578: 0x057A,  # RETYPE
  0x058C: 0x058E,  # QUERY
  0x0506: 0x0EC3,  # LINE
  0x04B6: 0x04B8,  # exit-;
  0x04A1: 0x1108,  # ;
  0x0499: 0x0FF5,  # PAD
  0x048A: 0x044D,  # BASE
  0x0480: 0x044D,  # CURRENT
  0x0473: 0x044D,  # CONTEXT
  0x0460: 0x0462,  # HERE
  0x00AB: 0x00AD,  # ABORT
  0x0099: 0x009B,  # QUIT

  # Internal words

  0x04C6: 0x04C8,  # memcheck
  0x0536: 0x0538,  # prOK
  0x054F: 0x0551,  # exec-or-compile
  0x0564: 0x0566,  # drop-or-execute
  0x061B: 0x061D,  # next-word-len
  0x1064: 0x1066,  # stk-fp
  0x1055: 0x1108,  # compile-fp
  0x1011: 0x1013,  # stk-int

}

ForthRoutines = frozenset({
  0x1085,  # create
  0x1108,  # compile
  0x0EC3,  # docolon
  0x0FF5,  # push int
  0x044D,  # push sysvar
})

# Forward declaration
Routines = {}

# Words that are immediate in the ROM dict
ImmediateROM = frozenset(WordsROM[i] for i in ('."', '(', '[', '+LOOP',
  'LOOP', 'DO', 'UNTIL', 'REPEAT', 'BEGIN', 'THEN', 'ELSE', 'WHILE', 'IF',
  'RUNS>', 'DOES>', 'ASCII', 'LITERAL', ';'))


# Compiled main execution loop, at addresses 0x04F5-0x0535
MainLoop = bytearray(
  b'\x8C\x05'  # QUERY
  b'\x06\x05'  # LINE
  b'\x36\x05'  # prOK
  b'\x76\x12'  # branch-R
  b'\xF7\xFF'  # to QUERY

  # It's immediately followed by the LINE word, which we also need.
  b'LIN\xC5'   # name `LINE`
  b'\xA0\x04'  # linked list pointer, points to `;`
  b'\x04'      # length of the word `LINE`, immediate flag clear
  b'\xC3\x0E'  # docolon
  # Start of thread
  b'\xC6\x04'  # memcheck
  b'\x3D\x06'  # FIND
  b'\xEE\x08'  # ?DUP
  b'\x83\x12'  # brfalse-I
  b'\x07\x00'  # skip the next three lines (to get-word-length)
  b'\x4F\x05'  # exec-or-compile
  b'\x76\x12'  # branch-R
  b'\xF1\xFF'  # back to the beginning of the word (where memcheck is)
  b'\xA9\x06'  # NUMBER
  b'\xEE\x08'  # ?DUP
  b'\x83\x12'  # brfalse-I
  b'\x07\x00'  # skip the next three lines (to
  b'\x64\x05'  # drop-or-execute
  b'\x76\x12'  # branch-R
  b'\xE3\xFF'  # back to the beginning of the word (where memcheck is)
  b'\x1B\x06'  # get-word-length
  b'\x1A\x0C'  # 0=
  b'\x83\x12'  # brfalse-I
  b'\x03\x00'  # skip the next line
  b'\xB6\x04'  # EXIT
  b'\x78\x05'  # RETYPE
  b'\x76\x12'  # branch-R
  b'\xD3\xFF'  # back to the beginning of the word (where memcheck is)
)

def div(a, b):
  """Trunc division."""

  return abs(a) // abs(b) if a * b >= 0 else -(abs(a) // abs(b))

class Interpreter:
  """Implements a Jupiter ACE Forth interpreter and compiler."""

  # ROM primitives

  # This is actually executable normally now as a user word.
  #def FORTH(self):
  #  """The FORTH Forth base word. Can't be deleted; it's a vocabulary."""
  #  self.wpoke(sv.CONTEXT, self.param)

  def UFLOAT(self):
    """The UFLOAT Forth primitive."""
    a = self.pop()
    if a == 0:
      self.dpush(0)
      return
    num = str(a)
    bcd = len(num) + 64
    for i in num:
      bcd = (bcd << 4) | (ord(i) - ord('0'))
    while bcd < 0x40000000:
      bcd <<= 4
    self.dpush(bcd)

  def INT(self):
    """The INT Forth primitive."""
    bcd = self.dpop()
    sign = -1 if bcd & 0x80000000 else 1
    exp = ((bcd >> 24) & 0x7F) - 70
    mant = 0
    for i in range(6):
      mant = mant * 10 + (bcd >> (20 - 4 * i) & 0xF)
    if exp < 0:
      self.push((mant // 10**-exp) * sign & 0xFFFF)
    else:
      self.push((mant * 10**exp) * sign & 0xFFFF)

  def FNEGATE(self):
    """The FNEGATE Forth primitive."""
    a = self.pop()
    self.push(a ^ 0x8000 if a & 0xFF00 else a)

  def F_slash(self):
    """The F/ Forth primitive."""
    a = self.fp2fix()
    b = self.fp2fix()
    if b == 0:
      # 0./0. = 0. according to the Juppy
      self.dpush(0)
      return
    if a == 0:
      self.errno(8)
    self.fix2fp(div(b * 10**69, a))

  def F_star(self):
    """The F* Forth primitive."""
    self.fix2fp(div(self.fp2fix() * self.fp2fix(), 10**69))

  def F_plus(self):
    """The F+ Forth primitive."""
    self.fix2fp(self.fp2fix() + self.fp2fix())

  def F_minus(self):
    """The F- Forth primitive."""
    a = self.fp2fix()
    self.fix2fp(self.fp2fix() - a)

  def LOAD(self):
    """The LOAD Forth primitive."""
    # not planned
    self.notimpl()

  def BVERIFY(self):
    """The BVERIFY Forth primitive."""
    # not planned
    self.notimpl()

  def VERIFY(self):
    """The VERIFY Forth primitive."""
    # not planned
    self.notimpl()

  def BLOAD(self):
    """The BLOAD Forth primitive."""
    # not planned
    self.notimpl()

  def BSAVE(self):
    """The BSAVE Forth primitive."""
    # not planned
    self.notimpl()

  def SAVE(self):
    """The SAVE Forth primitive."""
    if self.incomplete():
      self.errno(12)
    # Patch last word's length lazily
    lastWord = self.wpeek(sv.DICT)
    if lastWord:
      self.wpoke(lastWord, self.wpeek(sv.STKBOT) - lastWord)

    if not self.tapeFile:
      self.error("Output file not defined")

    self.push(32)
    self.WORD()

    if self.peek(self.pop()) == 0:  # length of word parsed
      self.errno(10)

    self.poke(sv.PAD, 0)  # flag this header as a Dict header
    self.wpoke(sv.PAD + 11, self.wpeek(sv.STKBOT) - 0x3C51)  # length of block
    self.wpoke(sv.PAD + 13, 0x3C51)  # start address: where user dict begins
    self.wpoke(sv.PAD + 15, self.wpeek(0x3C4C))  # Last word link in "FORTH"
    self.wpoke(sv.PAD + 17, self.wpeek(sv.CURRENT))
    self.wpoke(sv.PAD + 19, self.wpeek(sv.CONTEXT))
    self.wpoke(sv.PAD + 21, self.wpeek(sv.VOCLNK))
    self.wpoke(sv.PAD + 23, self.wpeek(sv.STKBOT))

    # Header ready, save it
    f = openFileWrite(self.tapeFile, binary = True, append = not
      self.firstSave)
    try:
      self.writeTapeBlock(f, self.pad[1 : 26], 0x00)
      self.writeTapeBlock(f, self.bin[0x51 : self.wpeek(sv.STKBOT) - 0x3C00],
        0xFF)
    finally:
      self.firstSave = False
      f.close()

  def LIST(self):
    """The LIST Forth primitive."""
    # not planned ?
    self.notimpl()

  def EDIT(self):
    """The EDIT Forth primitive."""
    # not planned
    self.notimpl()

  def FORGET(self):
    """The FORGET Forth primitive."""
    # not planned
    self.notimpl()

  def REDEFINE(self):
    """The REDEFINE Forth primitive."""
    # not planned
    self.notimpl()

  def EXIT(self):
    """The EXIT Forth primitive."""
    self.insptr = self.rpop()

  def dot_quote(self):
    """The ." Forth primitive (immediate)."""
    self.compile(WordsROM['pr-pas-str'])
    self.encloseToDelim('"')

  def lparen(self):
    """The ( Forth primitive (immediate)."""
    self.compile(WordsROM['ig-pas-str'])
    self.encloseToDelim(')')

  def lbracket(self):
    """The [ Forth primitive (immediate)."""
    self.setCompiling(False)

  def plus_LOOP(self):
    """The +LOOP Forth primitive (immediate)."""
    self.compile(WordsROM['brloopn'])
    if self.pop() != marker.DO:
      self.errno(5)
    self.enclose(self.pop() - self.wpeek(sv.STKBOT) - 1)

  def LOOP(self):
    """The LOOP Forth primitive (immediate)."""
    self.compile(WordsROM['brloop'])
    if self.pop() != marker.DO:
      self.errno(5)
    self.enclose(self.pop() - self.wpeek(sv.STKBOT) - 1)

  def DO(self):
    """The DO Forth primitive (immediate)."""
    self.compile(WordsROM['dosetup'])
    self.HERE()           # address to branch to after loop
    self.push(marker.DO)  # nesting check code

  def UNTIL(self):
    """The UNTIL Forth primitive (immediate)."""
    self.compile(WordsROM['brfalse-U'])
    if self.pop() != marker.BEGIN:
      self.errno(5)
    # Use the target on the stack to calculate the branch offset
    self.enclose(self.pop() - self.wpeek(sv.STKBOT) - 1)

  def REPEAT(self):
    """The REPEAT Forth primitive (immediate)."""
    self.compile(WordsROM['branch-R'])
    if self.pop() != marker.WHILE:
      self.errno(5)
    # Calculate the offset using the target address in the stack
    addr = self.pop()  # avoids a SWAP if we do it here
    self.enclose(self.pop() - self.wpeek(sv.STKBOT) - 1)
    # Patch the WHILE target to be right past us
    self.wpoke(addr, self.wpeek(sv.STKBOT) - addr - 1)

  def BEGIN(self):
    """The BEGIN Forth primitive (immediate)."""
    self.compile(WordsROM['nop-B'])
    self.HERE()              # address to branch to after loop
    self.push(marker.BEGIN)  # nesting check code

  def THEN(self):
    """The THEN Forth primitive (immediate)."""
    self.compile(WordsROM['nop-T'])
    a = self.pop()
    if a != marker.IF and a != marker.ELSE:
      self.errno(5)
    # Patch previous offset, be it either IF (brfalse) or ELSE (branch)
    addr = self.pop()
    self.wpoke(addr, self.wpeek(sv.STKBOT) - addr - 1)

  def ELSE(self):
    """The ELSE Forth primitive (immediate)."""
    self.compile(WordsROM['branch-E'])
    if self.pop() != marker.IF:
      self.errno(5)
    # Leave room for offset
    self.makeRoom(2)
    # Patch previous branch offset
    addr = self.pop()
    self.wpoke(addr, self.wpeek(sv.STKBOT) - addr - 1)
    # address to patch by next ELSE or THEN
    self.push(self.wpeek(sv.STKBOT) - 2)
    self.push(marker.ELSE)  # nesting check code

  def WHILE(self):
    """The WHILE Forth primitive (immediate)."""
    self.compile(WordsROM['brfalse-W'])
    if self.pop() != marker.BEGIN:
      self.errno(5)
    # The address past the BEGIN remains in the stack
    self.HERE()              # address to patch with loop exit
    self.push(marker.WHILE)  # nesting check code
    self.makeRoom(2)         # room for branch offset

  def IF(self):
    """The IF Forth primitive (immediate)."""
    self.compile(WordsROM['brfalse-I'])
    self.HERE()           # address to patch by ELSE or THEN
    self.push(marker.IF)  # nesting check code
    self.makeRoom(2)      # room for branch offset

  def rbracket(self):
    """The ] Forth primitive."""
    self.setCompiling(True)

  def LEAVE(self):
    """The LEAVE Forth primitive."""
    if len(self.rstk) < 2:
      self.error("Can not LEAVE - return stack too short")
    self.rstk[-1] = self.rstk[-2]

  def J(self):
    """The J Forth primitive."""
    if len(self.rstk) < 3:
      self.error("Return stack does not have J")
    self.push(self.rstk[-3])

  def I_apos(self):
    """The I' Forth primitive."""
    if len(self.rstk) < 2:
      self.error("Return stack does not have I'")
    self.push(self.rstk[-2])

  def I(self):
    """The I Forth primitive."""
    if not self.rstk:
      self.error("Return stack does not have I")
    self.push(self.rstk[-1])

  def DEFINITIONS(self):
    """The DEFINITIONS Forth primitive."""
    self.wpoke(sv.CURRENT, self.wpeek(sv.CONTEXT))

  def VOCABULARY(self):
    """The VOCABULARY Forth primitive."""
    self.newWord()
    self.enclose(NamedRoutines['set-context'])
    self.enclose(self.wpeek(sv.CURRENT) + 2)
    self.push(0)
    self.C_comma()
    self.enclose(self.wpeek(sv.VOCLNK))
    self.wpoke(sv.VOCLNK, self.wpeek(sv.STKBOT) - 2)

  def IMMEDIATE(self):
    """The IMMEDIATE Forth primitive."""
    current = self.wpeek(self.wpeek(sv.CURRENT))
    self.poke(current, self.peek(current) | 0x40)

  def RUNS_gt(self):
    """The RUNS> Forth primitive (immediate)."""
    self.compile(WordsROM['exit-R'])
    if self.pop() != marker.COMPILER:
      self.errno(5)
    addr = self.pop()
    self.C_comma()  # Enclose the parameter to COMPILER
    # Find name of word given the parameter address
    nameaddr = addr - (5 if addr < 0x3C03 else 7) - (self.peek(addr - 3)
      & 0x3F)
    self.enclose(nameaddr - self.wpeek(sv.STKBOT) - 1)
    self.wpoke(addr, self.wpeek(sv.STKBOT))
    self.enclose(NamedRoutines['runs2'])
    self.push(marker.colon)

  def DOES_gt(self):
    """The DOES> Forth primitive (immediate)."""
    self.compile(WordsROM['exit-D'])
    if self.pop() != marker.DEFINER:
      self.errno(5)
    addr = self.pop()
    # Find name of word given the parameter address
    nameaddr = addr - (5 if addr < 0x3C03 else 7) - (self.peek(addr - 3)
      & 0x3F)
    self.enclose(nameaddr - self.wpeek(sv.STKBOT) - 1)
    self.wpoke(addr, self.wpeek(sv.STKBOT))
    self.push(0xCD)  # CALL instruction
    self.C_comma()
    self.enclose(NamedRoutines['stkparam-V'])
    self.push(marker.colon)

  def COMPILER(self):
    """The COMPILER Forth primitive."""
    self.newWord()
    self.enclose(NamedRoutines['compile'])
    self.IMMEDIATE()
    self.HERE()
    self.push(marker.COMPILER)
    self.makeRoom(2)
    self.setCompiling(True)
    self.setIncomplete(True)

  def CALL(self):
    """The CALL Forth primitive. Not very useful...

    Makes things like this work on both the Jupiter and the interpreter:
    FIND FAST @ CALL
    """
    addr = self.pop()
    if addr not in Routines:
      self.error("Unknown CALL address: %04x" % addr)
    self.param = self.wpeek(sv.SPARE)  # not too useful but it's what it is
    Routines[addr](self)

  def DEFINER(self):
    """The DEFINER Forth primitive."""
    self.newWord()
    self.enclose(NamedRoutines['define'])
    self.HERE()
    self.push(marker.DEFINER)
    self.makeRoom(2)
    self.setCompiling(True)
    self.setIncomplete(True)

  def ASCII(self):
    """The ASCII Forth primitive (immediate)."""
    if self.nextWord() == '':
      self.push(0)
      self.visWrite('\0')  # don't ask me
    else:
      # Can't use the current word because it's uppercased and bit 7 reset
      self.push(ord(self.input[self.lineno][self.prev]))
      self.visWrite(self.lastInputWord() + ' ')
    if self.compiling():
      self.enclose(WordsROM['stk-byte'])
      self.C_comma()

  def LITERAL(self):
    """The LITERAL Forth primitive (immediate)."""
    self.compile(WordsROM['stk-int'])
    self.comma()

  def CONSTANT(self):
    """The CONSTANT Forth primitive."""
    self.newWord()
    self.enclose(NamedRoutines['stkvalue'])
    self.comma()

  def VARIABLE(self):
    """The VARIABLE Forth primitive."""
    self.newWord()
    self.enclose(NamedRoutines['stkparam-V'])
    self.comma()

  def ALLOT(self):
    """The ALLOT Forth primitive."""
    self.makeRoom(self.pop())

  def C_comma(self):
    """The C, Forth primitive."""
    self.makeRoom(1)
    self.poke(self.wpeek(sv.STKBOT) - 1, self.pop() & 0xFF)

  def comma(self):
    """The , Forth primitive."""
    self.makeRoom(2)
    self.wpoke(self.wpeek(sv.STKBOT) - 2, self.pop())

  def CREATE(self):
    """The CREATE Forth primitive."""
    self.newWord()
    self.enclose(NamedRoutines['stkparam-C'])

  def colon(self):
    """The : Forth primitive."""
    self.newWord()
    self.enclose(NamedRoutines['docolon'])
    self.setCompiling(True)
    self.push(marker.colon)  # block type ID to detect nesting errors

  def DECIMAL(self):
    """The DECIMAL Forth primitive."""
    self.poke(sv.BASE, 10)

  def MIN(self):
    """The MIN Forth primitive."""
    self.push(min(self.ipop(), self.ipop()))

  def MAX(self):
    """The MAX Forth primitive."""
    self.push(max(self.ipop(), self.ipop()))

  def XOR(self):
    """The XOR Forth primitive."""
    self.push(self.pop() ^ self.pop())

  def AND(self):
    """The AND Forth primitive."""
    self.push(self.pop() & self.pop())

  def OR(self):
    """The OR Forth primitive."""
    self.push(self.pop() | self.pop())

  def n2_minus(self):
    """The 2- Forth primitive."""
    self.push(self.pop() - 2)

  def n1_minus(self):
    """The 1- Forth primitive."""
    self.push(self.pop() - 1)

  def n2_plus(self):
    """The 2+ Forth primitive."""
    self.push(self.pop() + 2)

  def n1_plus(self):
    """The 1+ Forth primitive."""
    self.push(self.pop() + 1)

  def D_plus(self):
    """The D+ Forth primitive."""
    self.dpush(self.dpop() + self.dpop())

  def minus(self):
    """The - Forth primitive."""
    a = self.pop()
    self.push(self.pop() - a)

  def plus(self):
    """The + Forth primitive."""
    self.push(self.pop() + self.pop())

  def DNEGATE(self):
    """The DNEGATE Forth primitive."""
    self.dpush(-self.idpop())

  def NEGATE(self):
    """The NEGATE Forth primitive."""
    self.push(-self.ipop())

  def U_slash_MOD(self):
    """The U/MOD Forth primitive."""
    a = self.pop()
    b = self.dpop()
    self.push(b if a == 0 else b % -abs(a) if b < 0 else b % abs(a))
    self.push(div(b, a) if a != 0 else 1 if b < 0 else -1)

  def star_slash(self):
    """The */ Forth primitive."""
    a = self.ipop()
    b = self.ipop()
    c = self.ipop()
    sign = (a ^ b ^ c) & 0x8000
    b *= c
    # n / 0 = -sgn(n) according to the Juppy
    self.push(div(b, a) if a != 0 else 1 if sign else -1)

  def star(self):
    """The * Forth primitive."""
    self.push(self.pop() * self.pop())

  def MOD(self):
    """The MOD Forth primitive."""
    a = abs(self.ipop())
    b = self.ipop()
    self.push(b if a == 0 else b % -abs(a) if b < 0 else b % abs(a))

  def slash(self):
    """The / Forth primitive."""
    a = self.ipop()
    b = self.ipop()
    # n / 0 = -sgn(n) according to the Juppy
    self.push(div(b, a) if a != 0 else 1 if b < 0 else -1)

  def star_slash_MOD(self):
    """The */MOD Forth primitive."""
    a = self.ipop()
    b = self.ipop()
    c = self.ipop()
    sign = (a ^ b ^ c) & 0x8000
    b *= c
    self.push(b if a == 0 else b % -abs(a) if b < 0 else b % abs(a))
    # n / 0 = -sgn(n) according to the Juppy
    self.push(div(b, a) if a != 0 else 1 if sign else -1)

  def slash_MOD(self):
    """The /MOD Forth primitive."""
    a = self.ipop()
    b = self.ipop()
    self.push(b if a == 0 else b % -abs(a) if b < 0 else b % abs(a))
    # n / 0 = -sgn(n) according to the Juppy
    self.push(div(b, a) if a != 0 else 1 if b < 0 else -1)

  def U_star(self):
    """The U* Forth primitive."""
    self.dpush(self.pop() * self.pop())

  def D_lt(self):
    """The D< Forth primitive."""
    a = self.idpop()
    self.push(1 if self.idpop() < a else 0)

  def U_lt(self):
    """The U< Forth primitive."""
    a = self.pop()
    self.push(1 if self.pop() < a else 0)

  def lt(self):
    """The < Forth primitive."""
    a = self.ipop()
    self.push(1 if self.ipop() < a else 0)

  def gt(self):
    """The > Forth primitive."""
    a = self.ipop()
    self.push(1 if self.ipop() > a else 0)

  def equal(self):
    """The = Forth primitive."""
    self.push(1 if self.pop() == self.pop() else 0)

  def n0_gt(self):
    """The 0> Forth primitive."""
    self.push(1 if self.ipop() > 0 else 0)

  def n0_lt(self):
    """The 0< Forth primitive."""
    self.push(1 if self.ipop() < 0 else 0)

  def n0_equal(self):
    """The 0= Forth primitive."""
    self.push(1 if self.pop() == 0 else 0)

  def ABS(self):
    """The ABS Forth primitive."""
    self.push(abs(self.ipop()))

  def OUT(self):
    """The OUT Forth primitive."""
    # Pretend we have implemented it
    #self.notimpl()
    self.DROP()
    self.DROP()

  def IN(self):
    """The IN Forth primitive."""
    # Pretend we have implemented it
    #self.notimpl()
    self.DROP()
    self.push(255)

  def INKEY(self):
    """The INKEY Forth primitive."""
    # not planned
    self.notimpl()

  def BEEP(self):
    """The BEEP Forth primitive."""
    # not planned
    self.notimpl()

  def PLOT(self):
    """The PLOT Forth primitive."""
    # not planned (or use VT-100 control chars?)
    self.notimpl()

  def AT(self):
    """The AT Forth primitive."""
    # not planned (or use VT-100 control chars?)
    self.notimpl()

  def F_dot(self):
    """The F. Forth primitive."""
    bcd = self.dpop()
    if bcd & 0x80000000:
      self.console.write('-')
    exp = (bcd & 0x7F000000) >> 24
    if exp == 0:
      exp = 65
    if 61 <= exp <= 73:
      # Use standard notation
      mant = ''
      bcd &= 0x00FFFFFF
      for i in range(6):
        if bcd == 0:
          break
        mant += chr(((bcd >> 20) & 0xF) + ord('0'))
        bcd = (bcd << 4) & 0x00FFFFFF
      mant = '0' * (64 - exp) + mant
      if mant == '':
        mant = '0' * (exp - 64)
      mant = mant[:max(0, exp - 64)] + '.' + mant[max(0, exp - 64):]
      self.console.write(mant + ' ')
      return

    # Scientific notation
    for i in range(6):
      self.console.write(chr(((bcd >> 20) & 0xF) + ord('0')))
      bcd = (bcd << 4) & 0xFFFFFF
      if i == 0:
        self.console.write('.')
      if bcd == 0:
        break
    self.console.write('E')
    self.printInt(exp - 65)

  def EMIT(self):
    """The EMIT Forth primitive."""
    self.console.write(chr(self.pop() & 0xFF))

  def CR(self):
    """The CR Forth primitive."""
    self.console.write('\n')

  def SPACES(self):
    """The SPACES Forth primitive."""
    a = self.ipop()
    if a > 0:
      self.console.write(' ' * a)

  def SPACE(self):
    """The SPACE Forth primitive."""
    self.console.write(' ')

  def HOLD(self):
    """The HOLD Forth primitive."""
    self.hold(self.pop())

  def CLS(self):
    """The CLS Forth primitive."""
    # not planned (or use VT-100 control chars?)
    self.notimpl()

  def hash(self):
    """The # Forth primitive."""
    a = self.dpop()
    base = self.peek(sv.BASE)
    digit = a % base
    self.dpush(a // base)
    if digit < 10:
      digit += ord('0')
    else:
      digit += ord('A') - 10
    self.hold(digit)

  def hash_S(self):
    """The #S Forth primitive."""
    while True:
      self.hash()
      a = self.dpop()
      self.dpush(a)
      if a == 0:
        break

  def U_dot(self):
    """The U. Forth primitive."""
    self.printInt(self.pop())

  def dot(self):
    """The . Forth primitive."""
    self.printInt(self.ipop())

  def SIGN(self):
    """The SIGN Forth primitive."""
    if self.ipop() < 0:
      self.hold(ord('-'))

  def hash_gt(self):
    """The #> Forth primitive."""
    self.dpop()
    self.push(self.wpeek(sv.HLD))
    self.push(10239 - self.wpeek(sv.HLD))

  def lt_hash(self):
    """The <# Forth primitive."""
    self.wpoke(sv.HLD, 10239)

  def TYPE(self):
    """The TYPE Forth primitive."""
    Len = self.pop()
    addr = self.pop()
    for i in range(Len):
      self.console.write(chr(self.peek(addr)))
      addr += 1

  def ROLL(self):
    """The ROLL Forth primitive."""
    n = self.pop()
    if n > 32768 or n == 0:
      self.errno(7)
    spare = self.wpeek(sv.SPARE) - 0x3C00 - len(self.bin)
    pos = spare - n * 2
    if pos < 0:
      self.errno(2)
    self.dstk[pos:spare] = self.dstk[pos+2:spare] + self.dstk[pos:pos+2]

  def PICK(self):
    """The PICK Forth primitive."""
    n = self.pop()
    if n > 32768 or n == 0:
      self.errno(7)
    spare = self.wpeek(sv.SPARE)
    a = self.wpeek(spare - n * 2)
    self.push(a)

  def OVER(self):
    """The OVER Forth primitive."""
    a = self.pop()
    b = self.pop()
    self.push(b)
    self.push(a)
    self.push(b)

  def ROT(self):
    """The ROT Forth primitive."""
    a = self.pop()
    b = self.pop()
    c = self.pop()
    self.push(b)
    self.push(a)
    self.push(c)

  def question_DUP(self):
    """The ?DUP Forth primitive."""
    n = self.pop()
    self.push(n)
    if n:
      self.push(n)

  def R_gt(self):
    """The R> Forth primitive."""
    self.push(self.rpop() & 0xFFFF)

  def gt_R(self):
    """The >R Forth primitive."""
    self.rpush(self.pop())

  def bang(self):
    """The ! Forth primitive."""
    addr = self.pop()
    self.wpoke(addr, self.pop())

  def at(self):
    """The @ Forth primitive."""
    self.push(self.wpeek(self.pop()))

  def C_bang(self):
    """The C! Forth primitive."""
    addr = self.pop()
    self.poke(addr, self.pop() & 0xFF)

  def C_at(self):
    """The C@ Forth primitive."""
    self.push(self.peek(self.pop()))

  def SWAP(self):
    """The SWAP Forth primitive."""
    a = self.pop()
    b = self.pop()
    self.push(a)
    self.push(b)

  def DROP(self):
    """The DROP Forth primitive."""
    self.pop()

  def DUP(self):
    """The DUP Forth primitive."""
    n = self.pop()
    self.push(n)
    self.push(n)

  def SLOW(self):
    """The SLOW Forth primitive."""
    # we don't do anything special with it

  def FAST(self):
    """The FAST Forth primitive."""
    # we don't do anything special with it

  def INVIS(self):
    """The INVIS Forth primitive."""
    self.poke(sv.FLAGS, self.peek(sv.FLAGS) | 0x10)

  def VIS(self):
    """The VIS Forth primitive."""
    self.poke(sv.FLAGS, self.peek(sv.FLAGS) & ~0x10)

  def CONVERT(self):
    """The CONVERT Forth primitive."""

    while True:
      addr = self.pop() + 1
      num = self.dpop()
      digit = self.peek(addr)
      base = self.peek(sv.BASE)
      if ord('0') <= digit <= ord('9'):
        digit = digit - ord('0')
      elif ord('a') <= digit <= ord('z'):
        digit = digit - ord('a') + 10
      elif ord('A') <= digit:
        digit = digit - ord('A') + 10
      else:
        break
      if digit >= base:
        break
      num = (num * base + digit) & 0xFFFFFFFF
      self.dpush(num)
      self.push(addr)
    self.dpush(num)
    self.push(addr)

  def NUMBER(self):
    """The NUMBER Forth primitive."""

    savep = self.p
    saveprev = self.prev

    num = self.nextWord()

    newp = self.p
    newprev = self.prev
    self.p = savep
    self.prev = saveprev

    if not num:
      self.push(0)
      return
    sign = 1 if self.input[self.lineno].startswith('-', newprev) else 0
    if '.' in num:  # JA requires a point in all float numbers
      # Floats are all inverse-sensitive, so we pick the raw word
      num = self.input[self.lineno][newprev : newp]
      # Deal with it as floating-point
      p = sign
      Len = len(num)
      # Search for nondigit
      while '0' <= num[p] <= '9':
        p += 1
      # Must be a point
      if num[p] != '.':
        self.push(0)
        return
      ppoint = p
      p += 1
      while p != Len and '0' <= num[p] <= '9':
        p += 1
      if p != Len and num[p] not in 'Ee':
        self.push(0)
        return
      pmant = p     # Record position where the mantissa ends

      # Parse exponent.
      exp = 0
      if p != Len:  # There is an exponent
        p += 1      # Skip over the 'E'
        esign = 1
        if p != Len and num[p] == '-':  # JA does not allow + in exponent
          esign = -1
          p += 1
        if p == Len:
          self.push(0)
          return
        base = self.peek(sv.BASE)
        for i in range(p, Len):
          c = num[i]
          if '0' <= c <= '9':
            digit = ord(c) - ord('0')
          elif 'a' <= c <= 'z':
            digit = ord(c) - ord('a') + 10
          elif c >= 'A':
            digit = ord(c) - ord('A') + 10
          else:
            self.push(0)
            return
          if digit >= base:
            self.push(0)
            return
          exp = exp * base + digit
        exp *= esign

      # Remove decimal point
      mant = num[sign : ppoint] + num[ppoint + 1 : pmant]

      # Remove leading zeros
      p = 0
      Len = len(mant)
      while p != Len and mant[p] == '0':
        p += 1

      if p == Len:  # number is all zeros
        # A literal can't produce minus zero, so we're done.
        bcd = 0
      else:
        exp += 64 + ppoint - p - sign
        if not (1 <= exp <= 127):
          self.push(0)
          return

        bcd = exp | (sign << 7)
        for i in (mant + '00000')[p:p+6]:
          bcd = (bcd << 4) | (ord(i) - ord('0'))
      self.dpush(bcd)
      self.push(WordsROM['compile-fp'])
      self.p = newp
      self.prev = newprev
      self.visWrite(self.lastInputWord() + ' ')
      return

    # Deal with integer number
    p = sign
    # The sign is inverse-sensitive; the rest of the number isn't.
    # That's why we use num here.
    Len = len(num)
    if p == Len:
      self.push(0)
      return
    base = self.peek(sv.BASE)
    value = 0
    for i in range(p, Len):
      c = num[i]
      if '0' <= c <= '9':
        digit = ord(c) - ord('0')
      elif c >= 'A':
        digit = ord(c) - ord('A') + 10
      else:
        self.push(0)
        return
      if digit >= base:
        self.push(0)
        return
      value = (value * base + digit) & 0xFFFF
    self.push(-value if sign else value)
    self.push(WordsROM['compile-int'])
    self.p = newp
    self.prev = newprev
    self.visWrite(self.lastInputWord() + ' ')

  def EXECUTE(self):
    """The EXECUTE Forth primitive."""
    self.Execute(self.pop())

  def FIND(self):
    """The FIND Forth primitive.

    Jupiter ACE quirk:
    If the word in the buffer is not found, it is left in the buffer.
    """

    prev = self.prev
    p = self.p
    word = self.nextWord()
    if word == '':
      self.push(0)
      return

    wordt = bytearray(word.encode('latin1'))
    wordt[-1] |= 128

    # Find the last word in the context vocabulary
    findptr = self.wpeek(self.wpeek(sv.CONTEXT))

    while True:
      if findptr <= 0x1FFF:
        # Not found in RAM. Look in the ROM dictionary.
        if word in WordsROM:
          self.push(WordsROM[word])
          self.visWrite(self.lastInputWord() + ' ')
        else:
          # Push 0 to indicate not found
          self.push(0)
          # Unput the word
          self.p = p
          self.prev = prev
        break
      Len = self.peek(findptr) & 0x3F
      ptr = findptr - Len - 4 - 0x3C00
      if self.bin[ptr : ptr + Len] == wordt:
        self.push(findptr + 1)
        self.visWrite(self.lastInputWord() + ' ')
        break
      # Follow the link to the previous word
      findptr = self.wpeek(findptr - 2)

  def VLIST(self):
    """The VLIST Forth primitive."""
    # TODO ?
    self.notimpl()

  def WORD(self):
    """The WORD Forth primitive."""
    delim = self.pop()
    self.pad[2:0xFF] = b' ' * 0xFD  # fill PAD with spaces
    inp = self.input[self.lineno]
    Len = len(inp)
    p = self.p
    # Skip the first space
    if p != Len and self.sp(inp[p]):
      p += 1
    # Skip the leading delimiters
    while p != Len and ord(inp[p]) == delim:
      p += 1
    # Gather chars until the delimiter is found or the string ends
    prev = p
    while p != Len and ord(inp[p]) != delim:
      p += 1
    wlen = min(p - prev, 253)
    self.pad[2 : 2 + wlen] = inp[prev : prev + wlen].encode('latin1')
    self.pad[1] = min(p - prev, 255)
    # Skip final delimiter and conditionally add a space to the vis output
    if p != Len and ord(inp[p]) == delim:
      p += 1
      self.visWrite(inp[prev : p])
    else:
      self.visWrite(inp[prev : p] + ' ')
    self.p = p
    self.prev = prev
    self.PAD()

  def RETYPE(self):
    """The RETYPE Forth primitive.

    This primitive shows the [?] prompt and inputs a line. It is called by
    the LINE primitive when a word is not found, so we use it to check for
    internal words if we're in the LINE address. If not an internal word,
    since interactive input is not supported, we report an error that the
    word was not found.
    """
    if self.insptr == 0x0532:  # if we're called from LINE
      lastWord = self.lastInputWord().upper()
      if lastWord == '\\':
        # Ignore the rest of the line, read the next one
        self.QUERY()
      elif lastWord in {'#INCBIN', '#IMPORTSYM'}:
        filename = self.input[self.lineno][self.p + 1 :]
        if not filename:
          self.error("%s: No filename provided" % lastWord)
        f = openFileRead(filename, binary = True)
        try:
          bin = f.read()
        finally:
          closeFile(f, filename)

        if lastWord == '#INCBIN':
          self.makeRoom(len(bin))
          here = self.wpeek(sv.STKBOT)
          self.bin[here - len(bin) - 0x3C00 : here - 0x3C00] = bin
        else:
          import re
          for i in re.finditer(br'^\s*(\S+)[\s:]\s*(?:EQU\s|=)\s*(\S+)\s*$',
              bin, re.M | re.I):
            label = bytearray(i.group(1))
            if label.startswith(b'x_'):
              label = label[2:]
              value = bytearray(i.group(2).upper())
              # Follow Pasmo's conventions with some additions
              try:
                value = value.decode('latin1')
                if value.startswith('&H') or value.startswith('0X'
                    ) or value.startswith('&X'):
                  value = int(value[2:], 16)
                elif value.startswith('&O') or value.startswith('0O'):
                  value = int(value[2:], 8)
                elif value.startswith('0B'):
                  value = int(value[2:], 2)
                elif value.startswith('$') or value.startswith('#'
                    ) or value.startswith('&'):
                  value = int(value[1:], 16)
                elif value.startswith('%'):
                  value = int(value[1:], 2)
                elif '0' <= value[0] <= '9':
                  if value.endswith('H'):
                    value = int(value[:-1], 16)
                  elif value.endswith('D') or value.endswith('T'):
                    value = int(value[:-1], 10)
                  elif value.endswith('O'):
                    value = int(value[:-1], 8)
                  elif value.endswith('B') or value.endswith('Y'):
                    value = int(value[:-1], 2)
                  else:
                    value = int(value, 10)
                else:
                  self.error("Non-numeric value in symbol file: %s" % value)
              except:
                self.error("Invalid numeric value in symbol file: %s" % value)

              self.newWord(b'&' + label)
              self.enclose(NamedRoutines['stkvalue'])
              self.enclose(value)
        # Skip filename, read the next line
        self.QUERY()
      elif lastWord in {'/"', '\\"'}:
        s = bytearray(self.input[self.lineno][self.p + 1 :].encode('latin1'))
        here = self.wpeek(sv.STKBOT)
        self.makeRoom(len(s))
        for i in s:
          self.poke(here, i if lastWord == '/"' else i ^ 0x80)
          here += 1

        # Skip filename, read the next line
        self.QUERY()

      else:
        self.error("Unknown word")

    else:
      self.error("RETYPE is not supported")

  def QUERY(self):
    """The QUERY Forth primitive."""
    self.lineno += 1
    self.prev = self.p = 0

  def LINE(self):
    """The LINE Forth primitive.

    It is implemented in Forth, and the compiled thread is defined in
    MainLoop.
    """
    self.docolon()

  def semicolon(self):
    """The ; Forth primitive (immediate)."""
    self.compile(WordsROM['exit-;'])
    if self.pop() != marker.colon:
      self.errno(5)
    self.setCompiling(False)
    self.setIncomplete(False)

  def PAD(self):
    """The PAD Forth primitive."""
    self.push(0x2701)

  def BASE(self):
    """The BASE Forth primitive."""
    self.push(sv.BASE)

  def CURRENT(self):
    """The CURRENT Forth primitive."""
    self.push(sv.CURRENT)

  def CONTEXT(self):
    """The CONTEXT Forth primitive."""
    self.push(sv.CONTEXT)

  def HERE(self):
    """The HERE Forth primitive."""
    self.push(self.wpeek(sv.STKBOT))

  def ABORT(self):
    """The ABORT Forth primitive."""
    if self.peek(sv.ERR_NO) != 255:
      self.errno(self.peek(sv.ERR_NO))
    self.wpoke(sv.SPARE, self.wpeek(sv.STKBOT) + 12)
    self.QUIT()

  def QUIT(self):
    """The QUIT Forth primitive."""
    self.rstk[:] = []
    self.insptr = 0x04F5

  # Internal "words" whose address can appear in threads

  def prOK(self):
    """Internal Forth word that should print " OK  " CR."""

    if not self.compiling():
      self.visWrite(" OK  \n")

  def memcheck(self):
    """Internal Forth word that should check for RAM and break key.

    It's no use for us, so we implement it as a nop.
    """

  def compileOrExecute(self):
    """Compile or execute the last word.

    This is used by LINE when a word is found, to perform a sensible action
    according to the current mode, either compilation or immediate mode. If
    we're in compilation mode and it's not an immediate word, it encloses the
    address, otherwise it executes it.
    """

    addr = self.pop()
    if self.compiling() and not self.isImmediate(addr):
      self.enclose(addr)
    else:
      self.Execute(addr)

  def dropOrExecute(self):
    """Run an EXECUTE if in compile mode, otherwise a DROP.

    Used by LINE when NUMBER parsed a number successfully. The last integer
    pushed by NUMBER is the address of a routine to compile the successfully
    parsed number, be it either a float or an integer. So, when compiling,
    use that address to compile the number, and when executing, just drop
    that integer and leave the parsed result in the stack.
    """

    if self.compiling():
      self.Execute(self.pop())
    else:
      self.pop()

  def bufWordLen(self):
    """Return the length of the buffer.

    Used by LINE to check whether this is the last word in the current line.
    """

    self.push(len(self.nextWord()))

  def CompileFP(self):
    """Enclose a floating-point number."""

    self.enclose(WordsROM['stk-fp'])
    self.SWAP()
    self.comma()
    self.comma()

  def StackInteger(self):
    """Internal Forth word that stacks the next integer from the thread."""

    self.push(self.wpeek(self.insptr))
    self.insptr += 2

  def StackDouble(self):
    """Internal Forth word that stacks the following two integers."""

    self.StackInteger()
    self.StackInteger()

  def stkvalue(self):
    """Internal Forth word. Stacks the integer in the param field.

    It's what CONSTANT compiles to.
    """

    self.push(self.wpeek(self.param))

  def stkparam(self):
    """Pushes the parameter field to the stack.

    It's the action of VARIABLE and CREATE.
    """

    self.push(self.param)

  def docolon(self):
    """Internal word to execute a colon definition.

    Pushes the instruction pointer to the return stack and sets the new
    instruction pointer to point to the parameter field of the word.
    """

    self.rpush(self.insptr)
    self.insptr = self.param

  def DefinerWord(self):
    """Word defined using DEFINER."""

    self.newWord()
    self.enclose(self.wpeek(self.param))
    self.param += 2
    self.docolon()

  def CompilerWord(self):
    """Word defined using COMPILER."""

    self.compile(self.wpeek(self.param))
    self.param += 2
    self.docolon()

  def VocabularyWord(self):
    """Select the CONTEXT when a vocabulary word is invoked."""

    self.wpoke(sv.CONTEXT, self.param)

  # Compiled words used in a thread.

  def pr_pas_str(self):
    """The internal `pr-pas-str` Forth primitive.

    Prints a Pascal string, except the length is an int instead of a byte.
    It's what ." compiles to.
    """

    Len = self.wpeek(self.insptr)
    self.push(self.insptr + 2)
    self.push(Len)
    self.TYPE()
    self.insptr += 2 + Len

  def ig_pas_str(self):
    """The internal `ig-pas-str` Forth primitive.

    Ignores a Pascal string. This is what ( compiles to.
    """

    self.insptr += 2 + self.wpeek(self.insptr)

  def brloopn(self):
    """The internal `brloopn` Forth primitive (run-time).

    This is what +LOOP compiles to. It pops a word from the stack, then
    applies that increment to the top of the return stack. If the result
    is not past the limit, it reads an offset and branches to it;
    otherwise it clears the increment and limit from the return stack and
    skips over the branch offset.
    """

    if len(self.rstk) < 2:
      self.error("Return stack underflow in +LOOP")
    inc = self.ipop()
    self.rstk[-1] += inc
    sign = 1 if inc < 0 else 0
    if self.rstk[-1 ^ sign] < self.rstk[-2 ^ sign]:
      self.insptr = (self.insptr + self.wpeek(self.insptr) + 1) & 0xFFFF
    else:
      self.insptr += 2
      self.rpop()
      self.rpop()

  def brloop(self):
    """The internal `brloop` Forth primitive.

    This is what LOOP compiles to. It increments the top of the return
    stack. If the result is not past the limit, it reads an offset and
    branches to it; otherwise it clears the increment and limit from the
    return stack and skips over the branch offset.
    """

    if len(self.rstk) < 2:
      self.error("Return stack underflow in LOOP")
    self.rstk[-1] += 1
    if self.rstk[-1] < self.rstk[-2]:
      self.insptr = (self.insptr + self.wpeek(self.insptr) + 1) & 0xFFFF
    else:
      self.insptr += 2
      self.rpop()
      self.rpop()

  def dosetup(self):
    """The internal `dosetup` Forth primitive.

    Run-time action for DO.
    """

    # Transfer the counter to the return stack
    a = self.ipop()
    self.rpush(self.ipop())
    # Transfer the limit to the return stack
    self.rpush(a)

  def branch(self):
    """The internal `branch` Forth primitive.

    Used by ELSE and REPEAT.
    """

    self.insptr = (self.insptr + self.wpeek(self.insptr) + 1) & 0xFFFF

  def brfalse(self):
    """The internal `brfalse` Forth primitive.

    Used by IF, WHILE and UNTIL.
    """

    if self.pop() == 0:
      self.insptr = (self.insptr + self.wpeek(self.insptr) + 1) & 0xFFFF
    else:
      self.insptr += 2

  def nop(self):
    """The internal `nop` Forth primitive.

    Used by THEN and BEGIN.
    """

  def runs2(self):
    """The internal `runs2` Forth primitive.

    Used in the compilation of RUNS>.
    """

    # This is the code field for words defined with COMPILER.
    # Right after it is the content of RUNS>. Right before it
    # is a negative offset to the name (for listing purposes)
    # and right before that offset, the operand length byte
    # that was pased to COMPILER.
    #
    # What this routine does is prepare the return address to
    # point just past the operands, and then execute the RUNS>
    # part as a colon definition (after pushing the operands
    # address to the stack).
    #
    # insptr points just past the word in the current thread.
    # param points to the thread immediately after RUNS>.

    self.push(self.insptr)  # pass address of operands to the RUNS part
    # Point insptr past the operands
    if self.peek(self.param - 3) & 0x80:  # skip code field, check name ofs
      # If name offset is negative, there's an operand length byte; take it
      a = self.peek(self.param - 5)
      if a == 255:
        # 255 means the actual length is coded in the operand
        a = self.wpeek(self.insptr) + 2
      self.insptr += a
    self.docolon()

  def stkbyte(self):
    """The internal `stk-byte` Forth primitive.

    Used by ASCII at run time.
    """

    # push-byte
    self.push(self.peek(self.insptr))
    self.insptr += 1

  def stkzero(self):
    """The internal `stk-zero` Forth primitive."""

    self.push(0)


  # Interpreter functions

  def notimpl(self):
    """An exception for a word that is not implemented."""

    self.error("Not implemented")

  def writeTapeBlock(self, f, block, blockType):
    """Save a block to file prepending length and appending xorsum."""
    Len = len(block) + (2 if 'spectrumtape' in self.opts else 1)
    data = bytearray((Len & 0xFF, (Len >> 8) & 0xFF))
    if 'spectrumtape' in self.opts:
      data.append(blockType)
    data += block
    xor = 0
    for i in block:
      xor ^= i
    data.append(xor)
    f.write(data)

  def printInt(self, a):
    """Prints a number to stdout using current BASE."""

    digits = []
    neg = a < 0
    a = abs(a)
    base = self.peek(sv.BASE)
    if base < 2:
      # JA hangs in this case; we behave more gently
      self.error("Base 0 or 1 not supported")
    while a:
      digits.append(a % base)
      a //= base
    if not digits:
      digits.append(0)
    if neg:
      self.console.write('-')
    for i in digits[::-1]:
      if i < 10:
        c = chr(ord('0') + i)
      else:
        c = chr((ord('A') + i - 10) & 0xFF)
      self.console.write(c)
    self.console.write(' ')

  def fp2fix(self):
    """Converts a float number on the stack to a fixed-point number.

    The fixed-point number is multiplied by 10**69, in order to convert
    losslessly.
    """

    bcd = self.dpop()
    if not (bcd & 0xFF000000):
      return 0
    exp = (bcd >> 24) & 0x7F
    sign = -1 if bcd & 0x80000000 else 1
    num = 0
    for i in range(6):
      num = num * 10 + (bcd >> (20 - 4 * i) & 0xF)

    return sign * num * 10 ** (exp - 1)

  def getExp(self, num):
    """Get the exponent of a fixed-point number."""

    exp = 64
    while num >= 10**exp:
      exp += 8
    if exp != 64:
      exp -= 7
      while num >= 10**exp:
        exp += 1
    else:
      while num < 10**exp:
        exp -= 8
      exp += 7
      while num < 10**exp:
        exp -= 1
      exp += 1
    return exp

  def fix2fp(self, num):
    """Converts a fixed-point number to float and pushes it.

    The fixed-point number is multiplied by 10**69, in order to convert
    losslessly.
    """

    if num == 0:
      self.dpush(0)
      return

    sign = 0x80000000 if num < 0 else 0
    num = abs(num)
    if num >= 10**132:
      self.errno(8)

    exp = self.getExp(num)

    if exp < 6:
      # Underflow
      self.dpush(0)
      return

    # Round
    num = (num * 10 + 5 * 10**(exp - 6)) // 10

    # Get new exponent after rounding
    exp = self.getExp(num)

    num //= 10**(exp - 6)
    bcd = int(str(num), 16) | ((exp - 5) << 24) | sign
    self.dpush(bcd)

  def encloseToDelim(self, delim):
    """Enclose all characters in the buffer up to the given delimiter.

    Used by ( and ."
    """

    lenptr = self.wpeek(sv.STKBOT)
    self.makeRoom(2)  # Allocate space for the length
    inp = self.input[self.lineno]
    Len = len(inp)
    p = self.p
    if p != Len:
      p += 1
    if p != Len and inp[p] == delim:
      self.error("Empty strings not allowed")
    while p != Len and inp[p] != delim:
      self.push(ord(inp[p]))
      self.C_comma()
      self.visWrite(inp[p])
      p += 1
    if p == Len:
      self.error("No closing delimiter found before end of line")
    self.visWrite(inp[p])
    self.p = p + 1
    self.wpoke(lenptr, self.wpeek(sv.STKBOT) - (lenptr + 2))

  def visWrite(self, text):
    """Writes or not, depending on the INVIS flag."""

    if (self.peek(sv.FLAGS) & 0x10) == 0:
      self.console.write(text)

  def sp(self, c):
    """Defines the default word separators."""

    return c in {' ', '\t'}

  def nextWord(self):
    """Returns the next FORTH word in the input buffer."""

    inp = self.input[self.lineno]
    Len = len(inp)
    # Cache the program position
    p = self.p
    # Find next non-space
    while p != Len and self.sp(inp[p]):
      p += 1
    self.prev = p
    # Find next space
    while p != Len and not self.sp(inp[p]):
      p += 1
    # Flush the program position cache
    self.p = p
    # Return word with bit 7 unset
    return ''.join(chr(ord(i) & 0x7F) for i in inp[self.prev : p]).upper()

  def lastInputWord(self):
    """Returns the unadulterated word seen in the buffer.

    Used when it's necessary to respect case and bit 7.
    """

    return self.input[self.lineno][self.prev : self.p]

  def peek(self, addr):
    """Returns a memory byte.

    Supports addresses in range 3C00...end of stack, or certain ROM
    positions.
    """

    if addr < 0x2000:
      if 0x04F5 <= addr <= 0x0535:
        return MainLoop[addr - 0x04F5]
      self.error("ROM address out of range in peek(): %d ($%04X)"
        % (addr, addr))
    if addr < 0x3000:
      if addr < 0x2400:
        addr |= 0x0400
      if addr >= 0x2800:
        self.error("Charset RAM not supported in peek(): %d ($%04X)"
          % (addr, addr))
      if addr < 0x2700:
        self.error("Screen RAM not supported in peek(): %d ($%04X)"
          % (addr, addr))
      return self.pad[addr - 0x2700]
    if addr < 0x3C00:
      addr |= 0x0C00  # deal with mirrors
    if addr >= 0x3C00 + len(self.bin):
      if addr >= 0x3C00 + len(self.bin) + len(self.dstk):
        self.error("Address overflow in peek(): %d ($%04X)" % (addr, addr))
      return self.dstk[addr - 0x3C00 - len(self.bin)]
    return self.bin[addr - 0x3C00]

  def wpeek(self, addr):
    """Returns a double byte using self.peek()."""

    return self.peek(addr) + 256 * self.peek(addr + 1)

  def poke(self, addr, value):
    """Sets an address to a value. Only supports RAM."""

    assert -256 <= value <= 255, "poke: value out of range: %d" % value
    if addr < 0x2000:
      self.error("Address underflow in poke(): %d ($%04X)" % (addr, addr))
    if addr < 0x3000:
      if addr < 0x2400:
        addr |= 0x0400
      if addr >= 0x2800:
        self.error("Charset RAM not supported in poke(): %d ($%04X)"
          % (addr, addr))
      if addr < 0x2700:
        self.error("Screen RAM not supported in poke(): %d ($%04X)"
          % (addr, addr))
      self.pad[addr - 0x2700] = value & 0xFF
      return
    if addr < 0x3C00:
      addr |= 0x0C00  # deal with mirrors
    Len = len(self.bin)
    if addr >= 0x3C00 + Len + 12:
      if addr >= 0x3C00 + Len + len(self.dstk):
        self.error("Address overflow: %d ($%04X)" % (addr, addr))
      self.dstk[addr - 0x3C00 - len(self.bin)] = value & 0xFF
    elif addr - 0x3C00 >= Len:
      if addr - 0x3C00 == Len:
        self.bin.append(value & 0xFF)
      else:
        #self.bin.extend(b'\0' * (addr - 0x3C00 - Len))
        # Throw an error instead
        self.error("Attempting to extend by more than 1 byte")
    else:
      self.bin[addr - 0x3C00] = value & 0xFF

  def wpoke(self, addr, value):
    """Pokes a computer word into an address."""

    self.poke(addr, value & 0xFF)
    self.poke(addr + 1, (value & 0xFF00) >> 8)

  def signed(self, a):
    """Signed integer out of unsigned integer."""

    return a if a < 32768 else a - 65536

  def hold(self, value):
    """Stores a value in the pad starting with the end, in reverse order."""

    h = self.wpeek(sv.HLD) - 1
    if h >= sv.PAD:
      self.poke(h, value)
      self.wpoke(sv.HLD, h)

  def compiling(self):
    """Read the Compiling flag."""

    return self.peek(sv.FLAGS) & 0x40 != 0

  def incomplete(self):
    """Return whether we're in the middle of a definition."""

    return self.peek(sv.FLAGS) & 0x04 != 0

  def isImmediate(self, addr):
    """Returns whether the word at this address is immediate."""

    if addr < 0x3C00:
      return addr in ImmediateROM
    return (self.peek(addr - 1) & 0x40) != 0

  def setCompiling(self, value):
    """Set the Compiling flag."""

    flags = self.peek(sv.FLAGS)
    self.poke(sv.FLAGS, (flags | 0x40) if value else (flags & ~0x40))

  def setIncomplete(self, value):
    """Set the Incomplete Definition flag."""

    flags = self.peek(sv.FLAGS)
    self.poke(sv.FLAGS, (flags | 0x04) if value else (flags & ~0x04))

  def push(self, value):
    """Push a value into the data stack."""

    spare = self.wpeek(sv.SPARE)
    if spare - 0x3C00 - len(self.bin) >= len(self.dstk):
      self.dstk += b'\0\0'
      if len(self.dstk) >= 50176:
        self.errno(1)
    self.wpoke(spare, value)
    self.wpoke(sv.SPARE, spare + 2)

  def pop(self):
    """Pop an unsigned value from the data stack."""

    spare = self.wpeek(sv.SPARE) - 2
    if spare < 0x3C00 + len(self.bin) + 12:
      self.errno(2)
    self.wpoke(sv.SPARE, spare)
    return self.wpeek(spare)

  def ipop(self):
    """Pop a signed value from the data stack."""

    return self.signed(self.pop())

  def rpush(self, value):
    """Push a value onto the return stack."""

    if len(self.rstk) < 32768:
      return self.rstk.append(value)
    self.error("Return stack overflow")

  def rpop(self):
    """Pop an unsigned value from the return stack."""

    if self.rstk:
      return self.rstk.pop()
    self.error("Return stack underflow")

  def dpush(self, value):
    """Push a double int value to the stack."""

    self.push(value & 0xFFFF)
    self.push((value >> 16) & 0xFFFF)

  def dpop(self):
    """Pop an unsigned double int value from the stack."""

    value = self.pop()
    return value << 16 | self.pop()

  def idpop(self):
    """Pop a signed double int value from the stack."""

    a = self.dpop()
    return a if a < 0x80000000 else a - 0x100000000

  def makeRoom(self, size):
    """Allocates room in the latest word definition. Similar to ALLOT."""

    stkbot = self.wpeek(sv.STKBOT)
    if stkbot + size >= 65536:
      self.errno(1)

    if stkbot + size > 0x3C00 + len(self.bin):
      self.bin += b'\0' * (size -
        (0x3C00 + len(self.bin) - stkbot))
    self.wpoke(sv.STKBOT, stkbot + size)
    self.wpoke(sv.SPARE, self.wpeek(sv.SPARE) + size)

  def enclose(self, value):
    """Enclose a value; short for push + comma."""

    self.makeRoom(2)
    self.wpoke(self.wpeek(sv.STKBOT) - 2, value)

  def compile(self, value):
    """Enclose a value but only if in compiling mode.

    Equivalent to the ROM routine at 0x1108.
    """

    if not self.compiling():
      self.errno(4)
    self.enclose(value)

  def newWord(self, word = ''):
    """Writes a FORTH word definition to the output stream.

    Similar to the ROM routine at 0x0ED0 except no default code field.
    """

    if self.incomplete():
      self.errno(12)

    # Patch length lazily
    lastWord = self.wpeek(sv.DICT)
    if lastWord:
      self.wpoke(lastWord, self.wpeek(sv.STKBOT) - lastWord)

    if word:
      useInputBuf = False
    else:
      useInputBuf = True
      word = self.nextWord()
      if not word or len(word) > 0x3F:
        self.errno(6)
      word = bytearray(word.encode('ascii'))

    # Add the word to the stream
    here = self.wpeek(sv.STKBOT)
    self.makeRoom(len(word))
    for i in word:
      self.poke(here, i)
      here += 1
    self.poke(here - 1, word[-1] | 128)

    # Update last word pointer
    self.wpoke(sv.DICT, here)
    # Length of definition (will be known when writing the next word)
    self.enclose(0)
    # Link to previous
    current = self.wpeek(sv.CURRENT)
    self.enclose(self.wpeek(current))
    # Set new previous
    self.wpoke(current, self.wpeek(sv.STKBOT))

    # Length of name
    self.push(len(word))
    self.C_comma()

    if useInputBuf:
      self.visWrite(self.lastInputWord() + ' ')

  def error(self, msg):
    """Raises a Forth error exception with debug info."""

    # Dump memory
    if 'dumponerror' in self.opts:
      sys.stderr.write('\n')
      for i in range(0, len(self.bin), 16):
        sys.stderr.write("%04X " % (i + 0x3C00))
        for j in range(16):
          if i + j == len(self.bin):
            break
          if j == 8:
            sys.stderr.write("-%02X" % (self.bin[i + j]))
          else:
            sys.stderr.write(" %02X" % (self.bin[i + j]))
        if i + 16 > len(self.bin):
          sys.stderr.write(' '*(3 * (i + 16 - len(self.bin))))
        sys.stderr.write('  ')
        for j in range(16):
          if i + j == len(self.bin):
            break
          c = self.bin[i + j]
          sys.stderr.write(unichr(c if 32 <= c < 127 or 160 <= c else 183))
        sys.stderr.write('\n')

    raise ForthError("%d:%d: %s\n%s\n%s%s" % (
      # Line/column and message
      self.lineno + 1, self.prev + 1, msg,
      # Faulty line up to faulty position
      self.input[self.lineno],
      # Spaces up to faulty word, then underline the faulty word
      ' '*self.prev, '^' * (self.p - self.prev)
    ))

  def errno(self, no):
    """Give an error by number (Jupiter ACE numbering)."""

    s = ""
    if no == 1:
      s = "Error 1: Not enough memory"
    if no == 2:
      s = "Error 2: Stack underflow"
    if no == 3:
      s = "Error 3: BREAK pressed"
    if no == 4:
      s = "Error 4: Compiling word in interpreter mode"
    if no == 5:
      s = "Error 5: Malformed control flow structure"
    if no == 6:
      s = "Error 6: Name too short or too long"
    if no == 7:
      s = "Error 7: Invalid PICK or ROLL index"
    if no == 8:
      s = "Error 8: Floating point overflow"
    if no == 9:
      s = "Error 9: Trying to print in input buffer"
    if no == 10:
      s = "Error 10: Tape error"
    if no == 11:
      s = "Error 11: REDEFINE or FORGET error"
    if no == 12:
      s = "Error 12: Incomplete definition in dictionary"
    if no == 13:
      s = "Error 13: Word is not a user word"
    if no == 14:
      s = "Error 14: Word unlistable"
    if s:
      self.error(s)
    self.error("Internal error: errno() called with unknown error number %d"
      % no)

  def unknownAddress(self):
    self.error("Calling an address of an unknown routine")

  def Execute(self, addr):
    """Execute a Forth word, whether it's in RAM or ROM."""
    if addr >= 0x3C00:
      routine = self.wpeek(addr)
      if routine >= 0x3C00:
        # It better be a DEFINER body that starts with CALL $0FF0
        if self.peek(routine) != 0xCD or self.wpeek(routine + 1) != 0x0FF0:
          self.error("Unknown target address: $%04X, can't execute" % routine)
        self.push(addr + 2)  # parameter field of word to execute
        self.rpush(self.insptr)
        self.insptr = routine + 3  # resume execution after the CALL
        self.param = addr + 2
        return
    else:
      if addr not in Pointers:
        self.error("Unknown non-RAM routine: $%04X, can't execute" % addr)
      routine = Pointers[addr]
      if routine in ForthRoutines:
        routine = addr + 2

    if routine not in Routines:
      self.error("Unknown non-RAM routine: $%04X, can't execute" % routine)
    self.param = addr + 2  # in the JA it is held in the DE register
    Routines.get(routine, Interpreter.unknownAddress)(self)

  def __init__(self, inp, outfile = None, opts = None, console = sys.stdout,
      tape = None):
    """Init the object. Parameters:

    inp:     String containing the full input stream.
    opts:    Set indicating the compiling options
    console: Open file pointing to the console where to print.
    outfile: Open file where SAVE and BSAVE will write.
    """

    self.opts = opts
    self.firstSave = True
    self.tapeFile = tape
    self.outfile = outfile
    self.console = console
    inp = inp.replace('\r\n', '\r').replace('\n', '\r')
    self.input = inp.split('\r')
    # if the input file ends with a newline, don't store an empty line
    if inp.endswith('\r'):
      del self.input[-1]
    self.lineno = -1
    self.dstk = bytearray(b'\0' * 12)
    self.rstk = []
    # System variables
    self.bin = bytearray(b'\0' * 36)
    self.bin += b'\xE0\x26'
    self.bin += b'\0' * 11
    self.bin += (b'\x4C\x3C\x4C\x3C\x4F\x3C\x51\x3C\x45\x3C\x5D\x3C\xFF\0\x0A'
                 # First Forth word - the word "FORTH" (a vocabulary)
                 b'FORT\xC8\0\0\xFF\x1F\x05\xB5\x11\x49\x3C\0\0\0')
    # The PAD at 0x27FF
    self.pad = bytearray(b'\0' * 256)

    if 'vis' not in opts:
      self.poke(sv.FLAGS, 0x10)  # Enable INVIS mode at start

  def Interpret(self):
    """Start interpreter loop."""

    try:
      self.insptr = 0x04F5  # compiled main loop
      while True:
        if self.lineno >= len(self.input):
          break
        self.insptr += 2
        self.Execute(self.wpeek(self.insptr - 2))

    except ForthError as e:
      sys.stderr.write(str(e) + '\n')
      #raise

# Map the addresses now that we have the functions.
Routines.update({
  0x1D5B: Interpreter.UFLOAT,
  0x1D24: Interpreter.INT,
  0x1D11: Interpreter.FNEGATE,
  0x1C7D: Interpreter.F_slash,
  0x1C4D: Interpreter.F_star,
  0x1BB3: Interpreter.F_plus,
  0x1BA6: Interpreter.F_minus,
  0x198C: Interpreter.LOAD,
  0x197B: Interpreter.BVERIFY,
  0x1969: Interpreter.VERIFY,
  0x1956: Interpreter.BLOAD,
  0x1946: Interpreter.BSAVE,
  0x1936: Interpreter.SAVE,
  0x1672: Interpreter.LIST,
  0x1660: Interpreter.EDIT,
  0x163A: Interpreter.FORGET,
  0x13FF: Interpreter.REDEFINE,
  0x04B8: Interpreter.EXIT,
  0x138A: Interpreter.dot_quote,
  0x1398: Interpreter.pr_pas_str,
  0x137B: Interpreter.ig_pas_str,
  0x1363: Interpreter.lparen,
  0x13D7: Interpreter.lbracket,
  0x133E: Interpreter.brloopn,
  0x12D2: Interpreter.plus_LOOP,
  0x1334: Interpreter.brloop,
  0x12BF: Interpreter.LOOP,
  0x1325: Interpreter.dosetup,
  0x12AD: Interpreter.DO,
  0x128F: Interpreter.brfalse,
  0x1265: Interpreter.UNTIL,
  0x1278: Interpreter.branch,
  0x124E: Interpreter.REPEAT,
  0x04B9: Interpreter.nop,
  0x121C: Interpreter.BEGIN,
  0x1209: Interpreter.THEN,
  0x11EE: Interpreter.ELSE,
  0x11D7: Interpreter.WHILE,
  0x11C2: Interpreter.IF,
  0x13E3: Interpreter.rbracket,
  0x1318: Interpreter.LEAVE,
  0x1304: Interpreter.J,
  0x12F9: Interpreter.I_apos,
  0x12EB: Interpreter.I,
  0x11AD: Interpreter.DEFINITIONS,
  0x117F: Interpreter.VOCABULARY,
  0x1162: Interpreter.IMMEDIATE,
  0x1127: Interpreter.RUNS_gt,
  0x1142: Interpreter.runs2,
  0x10B6: Interpreter.DOES_gt,
  0x10F7: Interpreter.COMPILER,
  0x10A9: Interpreter.CALL,
  0x1076: Interpreter.DEFINER,
  0x104D: Interpreter.stkbyte,
  0x102A: Interpreter.ASCII,
  0x1008: Interpreter.LITERAL,
  0x0FE4: Interpreter.CONSTANT,
  0x0FD1: Interpreter.VARIABLE,
  0x0F78: Interpreter.ALLOT,
  0x0F61: Interpreter.C_comma,
  0x0F50: Interpreter.comma,
  0x0ED2: Interpreter.CREATE,
  0x0EB1: Interpreter.colon,
  0x0EA5: Interpreter.DECIMAL,
  0x0E89: Interpreter.MIN,
  0x0E77: Interpreter.MAX,
  0x0E62: Interpreter.XOR,
  0x0E4D: Interpreter.AND,
  0x0E38: Interpreter.OR,
  0x0E2B: Interpreter.n2_minus,
  0x0E21: Interpreter.n1_minus,
  0x0E15: Interpreter.n2_plus,
  0x0E0B: Interpreter.n1_plus,
  0x0DF0: Interpreter.D_plus,
  0x0DE3: Interpreter.minus,
  0x0DD4: Interpreter.plus,
  0x0DBC: Interpreter.DNEGATE,
  0x0DAB: Interpreter.NEGATE,
  0x0D8E: Interpreter.U_slash_MOD,
  0x0D7C: Interpreter.star_slash,
  0x0D6F: Interpreter.star,
  0x0D63: Interpreter.MOD,
  0x0D53: Interpreter.slash,
  0x0D33: Interpreter.star_slash_MOD,
  0x0D02: Interpreter.slash_MOD,
  0x0CAA: Interpreter.U_star,
  0x0C85: Interpreter.D_lt,
  0x0C74: Interpreter.U_lt,
  0x0C67: Interpreter.lt,
  0x0C58: Interpreter.gt,
  0x0C4C: Interpreter.equal,
  0x0C3C: Interpreter.n0_gt,
  0x0C30: Interpreter.n0_lt,
  0x0C1C: Interpreter.n0_equal,
  0x0C0F: Interpreter.ABS,
  0x0BFF: Interpreter.OUT,
  0x0BED: Interpreter.IN,
  0x0BDD: Interpreter.INKEY,
  0x0B9A: Interpreter.BEEP,
  0x0B4C: Interpreter.PLOT,
  0x0B1B: Interpreter.AT,
  0x0AB1: Interpreter.F_dot,
  0x0AA5: Interpreter.EMIT,
  0x0A97: Interpreter.CR,
  0x0A85: Interpreter.SPACES,
  0x0A75: Interpreter.SPACE,
  0x0A5E: Interpreter.HOLD,
  0x0A1F: Interpreter.CLS,
  0x09F9: Interpreter.hash,
  0x09E3: Interpreter.hash_S,
  0x09D2: Interpreter.U_dot,
  0x09B5: Interpreter.dot,
  0x0A4C: Interpreter.SIGN,
  0x099E: Interpreter.hash_gt,
  0x098F: Interpreter.lt_hash,
  0x0970: Interpreter.TYPE,
  0x0935: Interpreter.ROLL,
  0x0927: Interpreter.PICK,
  0x0914: Interpreter.OVER,
  0x0901: Interpreter.ROT,
  0x08F0: Interpreter.question_DUP,
  0x08E1: Interpreter.R_gt,
  0x08D4: Interpreter.gt_R,
  0x08C3: Interpreter.bang,
  0x08B5: Interpreter.at,
  0x08A7: Interpreter.C_bang,
  0x0898: Interpreter.C_at,
  0x0887: Interpreter.SWAP,
  0x087B: Interpreter.DROP,
  0x086D: Interpreter.DUP,
  0x0848: Interpreter.SLOW,
  0x0839: Interpreter.FAST,
  0x082A: Interpreter.INVIS,
  0x081A: Interpreter.VIS,
  0x078C: Interpreter.CONVERT,
  0x06AB: Interpreter.NUMBER,
  0x069C: Interpreter.EXECUTE,
  0x063F: Interpreter.FIND,
  0x062F: Interpreter.VLIST,
  0x05AD: Interpreter.WORD,
  0x057A: Interpreter.RETYPE,
  0x058E: Interpreter.QUERY,
  0x0508: Interpreter.LINE,
  0x04A3: Interpreter.semicolon,
  0x049B: Interpreter.PAD,
  0x048C: Interpreter.BASE,
  0x0482: Interpreter.CURRENT,
  0x0475: Interpreter.CONTEXT,
  0x0462: Interpreter.HERE,
  0x00AD: Interpreter.ABORT,
  0x009B: Interpreter.QUIT,

  # Internal words
  0x04C8: Interpreter.memcheck,
  0x0538: Interpreter.prOK,
  0x0551: Interpreter.compileOrExecute,
  0x0566: Interpreter.dropOrExecute,
  0x061D: Interpreter.bufWordLen,
  0x1066: Interpreter.StackDouble,
  0x1057: Interpreter.CompileFP,
  0x1013: Interpreter.StackInteger,

  # These are routines, not words.
  0x068A: Interpreter.stkzero,
  0x0FF5: Interpreter.stkvalue,
  0x0FF0: Interpreter.stkparam,
  0x0FEC: Interpreter.stkparam,
  0x0EC3: Interpreter.docolon,
  0x1085: Interpreter.DefinerWord,
  0x1108: Interpreter.CompilerWord,
  0x11B5: Interpreter.VocabularyWord,
})

class nullConsole:
  def write(self, x):
    pass

def usage():
  sys.stdout.write("Usage: python JAForth.py [options] infile [outfile]"
"\n"
"\nOptions:"
"\n"
"\n-h, --help              Display this help and exit"
"\n    --version           Display version and exit"
"\n-c, --console <file>    Specify file to write the output in VIS mode to"
"\n                        (default: stdout)"
"\n-n, --no-console        Don't use console output at all (overrides -c)"
"\n-v, --vis               Start in VIS mode (default is INVIS)"
"\n-z, --spectrum          Use ZX Spectrum .TAP format instead of Jupiter"
"\n-d, --dump-on-error     Show a hex dump of memory in case of error"
"\n"
"\ninfile, outfile and the console file can be '-' to input from or write to"
"\nstandard input/output, or '2-' to write to standard error. To write to a"
"\nfile with a name of '-' or '2-', prefix them with './'"
"\n"
  )

def main(argv):
  try:
    opts, args = getopt.gnu_getopt(argv[1:], 'hc:nvzd', ('help', 'version',
      'console=', 'no-console', 'vis', 'spectrum', 'dump-on-error'))
  except getopt.GetoptError:
    usage()
    return

  if not args:
    usage()
    return
  f = openFileRead(args[0], binary = True)
  try:
    prog = f.read()
  finally:
    closeFile(f, args[0])

  options = set()
  consoleFileName = '-'
  for opt, arg in opts:
    if opt in ('-h', '--help'):
      usage()
      return
    if opt == '--version':
      version()
      return
    if opt in ('-n', '--no-console'):
      options.add('noconsole')
      continue
    if opt in ('-c', '--console'):
      consoleFileName = arg
      continue
    if opt in ('-v', '--vis'):
      options.add('vis')
      continue
    if opt in ('-z', '--spectrum'):
      options.add('spectrumtape')
      continue
    if opt in ('-d', '--dump-on-error'):
      options.add('dumponerror')
      continue

  if 'noconsole' in options:
    console = nullConsole()
  else:
    console = openFileWrite(consoleFileName, binary = False)

  interpreter = Interpreter(prog.decode('latin1'), console = console,
    opts = options, tape = args[1] if len(args) > 1 else None)
  interpreter.Interpret()

  if 'noconsole' not in options:
    closeFile(console, consoleFileName)

if __name__ == '__main__':
  sys.exit(main(sys.argv))
