MultiTask

Date: 198x
Type: Program
Platform(s): TS 2068

MultiTask is a machine code extension that adds ten cooperative multitasking and error-handling commands to BASIC, implemented as a 2560-byte code block loaded at address 62721. It installs an IM 2 interrupt handler by building a 257-byte vector page filled with $F5 so any data-bus value routes to a single JP instruction, then chains RST $38 to preserve the ROM’s existing interrupt routine before counting down up to eight task control blocks. The extension replaces the ROM’s BASIC statement loop with its own version that intercepts REM statements containing command text, matching keywords by case-insensitive string comparison against a table of lower-case names with bit-7-terminated entries. Commands such as AFTER and EVERY resolve GOSUB target lines to memory addresses at parse time, storing them in 7-byte task control blocks; fired tasks save a 9-byte context frame—including print position, display cursor, and plot coordinates—which CONTINUE restores. ON ERROR GOTO works by overwriting the return address in the ERRSP error stack so the ROM’s error routine lands in the extension’s own trap handler instead of producing a report.


Program Structure

The BASIC loader is minimal by design. Lines 10–90 display a brief help screen listing the ten new commands, then wait for a keypress before calling RANDOMIZE USR 62721 (INSTALL) and halting. Line 100 is the save/verify master record, and line 150 is the auto-start entry point: it issues CLEAR 60000, loads the companion CODE block, and jumps to line 20 to restart the demo. The real program is entirely in the 2560-byte machine code block saved as "MultiTaskC".

Machine Code Architecture

The code block spans $F501$FF00 and is divided into several cooperating subsystems:

  • INSTALL ($F501) – fills a 257-byte page at $F400 with $F5, writes a JP INT_HANDLER trampoline at $F5F5, and loads I with $F4. Any data-bus value during an interrupt therefore produces vector address $F5F5.
  • START ($FD21, USR 63866) – clears the task table, switches the Z80 to IM 2, initializes both workspace pointers, and falls into the replacement statement loop via REENTER. It never returns to the ROM loop.
  • INT_HANDLER ($FB70) – begins with RST $38 so the ROM’s own interrupt routine (keyboard, frame counter) runs first and returns normally. The handler then iterates all eight task control blocks, decrements live counters, and calls FIRE_TASK for any that reach zero. It also latches the ticker off (via MTFLAG bit 0) whenever ERR_NR is not $FF, meaning BASIC has reported something.
  • STMT_LOOP / STMT_RET ($FA87 / $FAB5) – a near-clone of the ROM statement loop. After every statement, STMT_RET calls CHECK_BRK and then DISPATCH_TASK before advancing to the next statement.

Task Control Blocks

Eight 7-byte blocks reside at $FF0F$FF46:

OffsetSizeContents
+0,+12Countdown counter (ticks remaining)
+21Flags: bit 0 = disabled, bit 1 = repeating (EVERY)
+3,+42Address of the BASIC line to run
+5,+62Reload interval (for EVERY)

The counter is written last under DI/EI to prevent the interrupt handler from seeing a partially updated block. AFTER tasks leave the counter at zero after firing (one-shot); EVERY tasks copy the reload value back into the counter.

Dual-Workspace Memory Layout

Two workspaces grow toward each other inside the reserved arena. The fired-task queue grows downward from QUEUE_BASE ($F979), two bytes per entry (the line address). Context frames grow upward from RAMTOP+1, nine bytes per frame. FIRE_TASK checks that at least 15 bytes of headroom remain between them before pushing; if not, the firing is silently discarded.

Context Frame (9 bytes, growing upward)

OffsetContents
+0Statement number
+1,+2Line number (PPC / NEWPPC)
+3,+4COORDS (plot coordinates)
+5,+6DF_CC (display file cursor)
+7,+8S_POSN (print position)

Keyword Dispatch via REM Hijacking

Because MultiTask cannot modify the ROM tokenizer, all ten commands are typed inside REM statements as plain ASCII text. DO_REM ($FAFC) intercepts every REM token: if the first character is * the remainder is treated as an ordinary comment; if the line is otherwise empty, execution continues; anything else is passed to DISPATCH. DISPATCH walks a table (KW_TABLE at $F9D9) of lower-case, bit-7-terminated names and calls MATCH_KW for each. MATCH_KW skips characters below space (control codes, colour attributes), ORs each typed character with $20 to force lower case, and advances CH_ADD on a successful match. This means any ordinary comment that does not begin with * will fall through to “C Nonsense in BASIC”.

ON ERROR GOTO Mechanism

CMD_ON_ERROR ($FC7F) reads the target line number and then overwrites the word at the address held in ERRSP with the address of ERR_TRAP ($FD8B). Because ERRSP points to the return address the ROM will use when it processes an error, every subsequent error lands in ERR_TRAP instead. ERR_TRAP saves the error code, statement, and line, clears ERR_NR to $FF (so BASIC thinks all is well), re-arms the ticker and the trap itself, then jumps to the handler line via NEWPPC/NSPPC. RESUME re-examines $FF03/$FF02 to retry the failing statement, or accepts an explicit target line.

BREAK Handling

Three BREAK modes are stored at $FF0B (requested) and $FF0D (in force): $FF00 = stop, $FE00 = ignore, any other value = a handler line number. When a BREAK GOSUB handler is running, $FF0D is forced to the stop mode regardless of $FF0B, ensuring BREAK still works inside handlers. The GOSUB frame pushed by BRK_GOSUB sets bit 15 of the stacked line number as a marker; DO_RETURN tests this bit to distinguish real GOSUB frames from handler frames and restores the BREAK setting accordingly.

CONTINUE Repurposing

The standard CONTINUE token ($E8) is intercepted and completely redefined. It no longer resumes after a STOP; instead, it pops the topmost 9-byte context frame from the upward-growing stack and restores the interrupted program’s line, statement, print position, display cursor, and plot coordinates. Every task line must end with CONTINUE (or DROP) — there is no implicit return. DROP ($FEE5) simply discards the top frame without restoring any state, allowing a task to deliberately abandon the program it preempted.

FN_INFO Query Interface

USR 63872 (FN_INFO at $FB21) is designed to be called from inside a DEF FN expression. It reads its numeric argument via DEFADD (the system variable pointing to DEF FN parameters) rather than from the expression evaluator. Arguments 0–7 return the live tick counter for that task (or zero if disabled, guarded by DI/EI around the read). Arguments 8, 9, and 10 return the line number, statement number, and error-code character of the most recently trapped error respectively; the error code byte is converted to a printable character by adding $31 and, if the result exceeds '9', a further $07 to skip into the letter range.

Notable Techniques

  • The IM 2 vector page trick (257 bytes of $F5 + a JP at $F5F5) is a well-known Z80 idiom that guarantees a single handler regardless of the data bus state during the interrupt acknowledge cycle.
  • The counter write in CMD_AFTER/CMD_EVERY is split: the reload value and line address are written first without disabling interrupts; only the live counter write is guarded by DI/EI, minimizing the interrupt-disabled window.
  • MATCH_KW’s use of OR $20 for case folding is efficient but means digits and symbols are also affected (their bit 5 is already set, so they are unchanged). In practice only alphabetic names appear in the keyword table.
  • GET_NUM calls the ROM’s decimal-reader routines at $3046/$304B/$30F9 for literal numbers, then falls back to a variable lookup via $2C70 and $3773 for names, making AFTER/EVERY accept either literals or numeric variables as arguments.
  • The INT_ADDR word at $FEFF$FF00 (the last two bytes of the saved block) stores the handler address so it travels with the CODE file; START copies it back over itself on each invocation, making the block self-relocating in principle — though in practice the addresses throughout the binary are absolute.

Bugs and Anomalies

  • AFTER and EVERY resolve the GOSUB target to a memory address at parse time using the ROM’s LINE_ADDR routine. Editing the BASIC program after setting up a task leaves stale pointers that will execute random memory as BASIC bytecode.
  • Bare RESUME without an argument re-runs the exact statement that caused the error, so an unconditional ON ERROR GOTO / RESUME pair on a persistent error produces an infinite loop with no escape except BREAK (if not ignored).
  • The ticker is latched off whenever BASIC reports anything at all — including the normal “0 OK” end-of-program report — and is only re-armed by START or ERR_TRAP. Multitasking must therefore be restarted with RANDOMIZE USR 63866 after any program run completes.
  • Any REM statement whose text does not begin with * and does not match a MultiTask keyword will produce “C Nonsense in BASIC”. Ordinary comments require the REM * prefix to be recognized as such.
  • The $FF07 stack pointer is compared against RAMTOP+1 to detect an empty context stack, but RAMTOP is a user-settable value. If CLEAR is executed while a task is running, the frame-stack base shifts and the empty-stack check becomes unreliable.

Content

Appears On

Related Products

Related Articles

Related Content

Image Gallery

Source Code

  10 REM \{20}\{1}MULTI-TASK\{20}\{0}
  20 CLS 
  30 PRINT "COMMANDS AVAILABLE:"
  40 PRINT '"AFTER","ON ERROR GOTO","EVERY","ON BREAK STOP","DISABLE","ON BREAK GOSUB","ENABLE","IGNORE BREAK","DROP","RESUME"
  50 PRINT ''"USE RANDOMIZE 63866 TO START"
  60 PRINT '"NOTE THIS NUMBER AND PRESS ANY  KEY TO CONTINUE"
  70 PAUSE 0
  80 RANDOMIZE USR 62721
  90 STOP 
 100 SAVE "MultiTask" LINE 150:SAVE "MultiTaskC"CODE 62721,2560:REM PRINT "REWIND FOR VERIFY":VERIFY "":VERIFY ""CODE 
 120 STOP 
 150 CLEAR 60000:LOAD "MultiTaskC"CODE :GO TO 20


; ============================================================================
; MultiTask  --  interrupt-driven task scheduler and error/BREAK handling
;                for the Timex/Sinclair 2068
;
; Source   : MultiTaskC, from SNUG_5_6.img
; Origin   : $F501 (62721), 2560 bytes, ending at $FF00 (65280)
; sha256   : 64e254c9acf13a0bc35a7a5e0b358ec6f0378c2cc38f9549a78d53f78ec8bafa
;
; Disassembly note: this listing was produced by rebuilding the object image
; from the byte column of the machine disassembly (the rebuilt image matches
; the recorded sha256 exactly) and re-walking it by recursive descent from the
; real entry points, so the code/data split below is derived from reachability
; rather than a linear scan.  Every byte in the 2560 is accounted for and every
; branch target lands on an instruction boundary.
;
; ---------------------------------------------------------------------------
; WHAT IT DOES
; ---------------------------------------------------------------------------
; MultiTask adds ten commands to TS2068 BASIC:
;
;     AFTER n[,t] GOSUB line     run a line once, n interrupts from now
;     EVERY n[,t] GOSUB line     run a line every n interrupts
;     DISABLE [t] / ENABLE [t]   suspend / restart a task (or all eight)
;     ON ERROR GOTO line         trap BASIC errors
;     RESUME [line]              return from an error handler
;     ON BREAK STOP              default: BREAK reports "L BREAK into program"
;     ON BREAK GOSUB line        BREAK calls a handler instead
;     IGNORE BREAK               BREAK does nothing
;     DROP                       discard a saved context without resuming it
;
; It gets them into BASIC without touching the tokeniser, by putting them
; inside REM statements and replacing the ROM's statement loop with its own.
; Three mechanisms do all the work:
;
;   1. IM 2 TIMER.  INSTALL builds a 257-byte vector page at $F400 filled
;      with $F5, so any data-bus value routes to $F5F5, where it plants a
;      JP to INT_HANDLER.  The handler begins with RST $38 -- the ROM's own
;      interrupt routine runs first and returns into it -- then counts down
;      eight task control blocks and queues any that reach zero.
;
;   2. REPLACEMENT STATEMENT LOOP.  START (RANDOMIZE USR 63866) never returns
;      to the ROM's loop; it falls into STMT_LOOP, a copy of the ROM's loop
;      with the token dispatch changed.  REM, IF, RETURN and CONTINUE are
;      handled here; everything else is passed to the ROM with MultiTask's
;      own STMT_RET already on the stack, so the ROM comes back.  STMT_RET
;      runs the BREAK check and the task dispatcher after EVERY statement --
;      that is the granularity of the "multitasking".
;
;   3. TWO WORKSPACES growing toward each other.  Fired tasks queue downward
;      from $F979 into the 1109 reserved bytes inside the saved block; saved
;      program contexts stack upward from RAMTOP+1.  A task switch saves the
;      interrupted program's line, statement, print position, display-file
;      cursor and plot coordinates as a 9-byte frame and enters the task line
;      with no return address.  The task returns by executing CONTINUE.
;
; ON ERROR GOTO works differently again: it overwrites the return address
; stacked at ERRSP so that the ROM's error routine lands in ERR_TRAP instead
; of the ROM's report.
;
; ---------------------------------------------------------------------------
; USING IT  (from the BASIC loader)
; ---------------------------------------------------------------------------
;     CLEAR 60000 : LOAD "MultiTaskC" CODE
;     RANDOMIZE USR 62721      install the interrupt vector page  (once)
;     RANDOMIZE USR 63866      start multitasking
;
; Points that are not obvious from the commands themselves:
;
;   * An ordinary remark must be written  REM * text .  DO_REM treats any
;     REM whose first character is not "*" as a MultiTask command, so a
;     plain  REM this is a comment  reports "C Nonsense in BASIC".
;
;   * Every task line must end with CONTINUE (or DROP).  CONTINUE no longer
;     means what it means in ordinary BASIC.
;
;   * AFTER/EVERY resolve the GOSUB line to an ADDRESS when the command runs.
;     Editing the program afterwards leaves a live task pointing into the
;     wrong place.
;
;   * The ticker latches itself off as soon as BASIC reports anything at all,
;     including "0 OK" at the end of a program (INT_HANDLER, $FB9E).  Only
;     START and ERR_TRAP clear that latch, so multitasking has to be
;     restarted with RANDOMIZE USR 63866 after the program stops.
;
;   * DEF FN f(n) = USR 63872 gives access to task counters (n = 0..7) and to
;     the line, statement and report letter of the last trapped error
;     (n = 8, 9, 10).
;
; ---------------------------------------------------------------------------
; MEMORY MAP  (with CLEAR 60000)
; ---------------------------------------------------------------------------
;   $EA61..        context frames, 9 bytes each, growing UP from RAMTOP+1
;   $F400..$F500   IM 2 vector page, 257 bytes of $F5   (built by INSTALL)
;   $F501..$F524   INSTALL
;   $F525..$F979   reserved arena; fired-task queue grows DOWN from $F979
;   $F5F5..$F5F7   JP INT_HANDLER, the IM 2 landing pad (inside the arena)
;   $F97A..$F982   three-entry jump table: 63866 / 63869 / 63872
;   $F983..$FEFE   code and the keyword table
;   $FEFF..$FF00   INT_ADDR, last two bytes of the saved block
;   $FF01..$FF0E   MultiTask variables (above the saved block, not saved)
;   $FF0F..$FF46   eight 7-byte task control blocks
;
; MULTITASK VARIABLES
;   $FF01     error code of the last trapped error (ERR_NR as saved)
;   $FF02     statement number it happened in
;   $FF03/04  line number it happened on
;   $FF05/06  ON ERROR GOTO target line
;   $FF07/08  context-frame stack pointer   (grows up from RAMTOP+1)
;   $FF09/0A  fired-task queue pointer      (grows down from $F979)
;   $FF0B/0C  BREAK action the program asked for
;   $FF0D/0E  BREAK action actually in force
;             ($FF00 = STOP, $FE00 = IGNORE, else a handler line number)
;
; MTFLAG -- NMIADD low byte, $5CB0, unused by the ROM, borrowed as a bit set
;   bit 0     ticker suspended (latched when BASIC reports)
;   bit 1     an ON ERROR handler is running (RESUME is legal)
;   bit 2     a task or BREAK GOSUB handler is running
;
; ---------------------------------------------------------------------------
; TS2068 HOME ROM ROUTINES USED
; ---------------------------------------------------------------------------
; Confirmed against the TS2068 ROM Manuscript / timexsinclair.com ROM notes:
ROM_SET_WORK    EQU $134E   ; SET-WORK, reclaim the workspace
ROM_RESERVE     EQU $133F   ; reserve/reset workspace after an error
ROM_FIND_INT1   EQU $1F1E   ; calculator stack -> single integer in A
ROM_FIND_INT2   EQU $1F23   ; calculator stack -> line number in BC
ROM_BREAK       EQU $2009   ; BREAK pressed?  carry set = NOT pressed
ROM_LOOKUP_VAR  EQU $2C70   ; variable lookup
;
; Identified from the calling code (names are descriptive, not from a
; published label list -- treat them as inferred):
ROM_STMT_DISP   EQU $1A71   ; token in A -> the ROM's statement routine
ROM_REPORT_OK   EQU $1AFE   ; report reached past the end of the program
ROM_REPORT_N    EQU $1B42   ; report reached when the target line is missing
ROM_EXPT_1NUM   EQU $1BE5   ; evaluate a numeric expression (IF condition)
ROM_REPORT_C    EQU $1BED   ; "C Nonsense in BASIC"
ROM_LINE_ADDR   EQU $16D6   ; line number in HL -> address of that line, Z=exact
ROM_SKIP_STMTS  EQU $16F3   ; skip forward D-1 statements in a line
ROM_ERROR_2     EQU $0ED8   ; the ROM error report ON ERROR GOTO displaces
ROM_NUM_START   EQU $3046   ; $30xx family: read a decimal number from RAM
ROM_NUM_READ    EQU $304B   ;   to the calculator stack
ROM_NUM_DONE    EQU $30F9   ;
ROM_STK_VAR     EQU $3773   ; stack the value of the variable found at HL
ROM_TEST_ZERO   EQU $3904   ; carry set if the value on the stack is zero
;
; System variables (Spectrum-compatible layout, IY = $5C3A)
ERR_NR   EQU $5C3A
ERRSP    EQU $5C3D
NEWPPC   EQU $5C42
NSPPC    EQU $5C44
PPC      EQU $5C45
SUBPPC   EQU $5C47
NXTLIN   EQU $5C55
CH_ADD   EQU $5C5D
X_PTR    EQU $5C5F
DEFADD   EQU $5C0B
FLAGX    EQU $5C71
COORDS   EQU $5C7D
DF_CC    EQU $5C84
S_POSN   EQU $5C88
MTFLAG   EQU $5CB0            ; NMIADD low byte, borrowed
RAMTOP   EQU $5CB2
; ============================================================================

        ORG $F501

;----------------------------------------------------------------------------
; INSTALL  --  entry from RANDOMIZE USR 62721
;
; Builds the IM 2 interrupt vector page and points it at INT_HANDLER.
; Called once, immediately after the CODE block is loaded.  Does NOT
; switch the Z80 into IM 2 -- START (63866) does that.
;----------------------------------------------------------------------------
INSTALL:
$F501  C5           PUSH BC
$F502  D5           PUSH DE
$F503  E5           PUSH HL
$F504  F5           PUSH AF
$F505  21 00 F4     LD HL,$F400               ; vector page: $F400..$F500 (257 bytes)
$F508  06 00        LD B,$00                  ; B=0 -> 256 passes
FILL:
$F50A  36 F5        LD (HL),$F5               ; fill every entry with $F5
$F50C  23           INC HL
$F50D  10 FB        DJNZ FILL                 ; ...256 bytes
$F50F  36 F5        LD (HL),$F5               ; 257th byte: a vector read at $F4FF needs $F500 too
$F511  3E C3        LD A,$C3                  ; opcode for JP nn
$F513  32 F5 F5     LD (INT_VECTOR),A         ; any data-bus value D gives vector $F400+D,
$F516  21 70 FB     LD HL,INT_HANDLER         ; whose word is $F5F5 -- so plant "JP INT_HANDLER"
$F519  22 F6 F5     LD ($F5F6),HL             ; there, at $F5F5..$F5F7
$F51C  3E F4        LD A,$F4                  ; I = $F4  (vector page = I*256)
$F51E  ED 47        LD I,A
$F520  F1           POP AF
$F521  E1           POP HL
$F522  D1           POP DE
$F523  C1           POP BC
$F524  C9           RET                       ; return to BASIC; interrupts still IM 1

;----------------------------------------------------------------------------
; QUEUE_ARENA  --  $F525..$F979,  1109 bytes, saved as zeros
;
; Reserved workspace.  Two things live in here:
;   * $F5F5..$F5F7  the IM 2 landing pad written by INSTALL (see below)
;   * the fired-task queue, which grows DOWNWARD from QUEUE_BASE ($F979)
;     two bytes at a time.  FIRE_TASK pushes, DISPATCH_TASK pops.
;----------------------------------------------------------------------------
QUEUE_ARENA:
$F525               DEFS 208,$00
; --- IM 2 landing pad, written by INSTALL ---
INT_VECTOR:
$F5F5               DEFB $C3,$70,$FB                    ; |.p.|
; --- queue space continues ---
$F5F8               DEFS 893,$00
; last two bytes below are a stale queue entry left in the saved image
$F975               DEFB $00,$00,$00,$E8                ; |....|
QUEUE_BASE:
$F979               DEFB $68                            ; |h|

;----------------------------------------------------------------------------
; ENTRY JUMP TABLE  --  the three user-callable addresses
;
;   63866 ($F97A)  RANDOMIZE USR 63866  -- start/restart multitasking
;   63869 ($F97D)  RANDOMIZE USR 63869  -- re-enter the loop, no reset
;   63872 ($F980)  used as DEF FN ... = USR 63872 -- query tasks/errors
;----------------------------------------------------------------------------
ENT_START:
$F97A  C3 21 FD     JP START                  ; START: full initialise, then fall into the loop
ENT_RESUME:
$F97D  C3 1A FD     JP REENTER                ; REENTER: resume the replacement statement loop
ENT_FN:
$F980  C3 21 FB     JP FN_INFO                ; FN_INFO: DEF FN query function

;----------------------------------------------------------------------------
; GET_NUM  --  read a number from the REM text, return it in BC
;
; MultiTask commands are typed inside REM statements, so their operands
; are plain ASCII characters -- there is no hidden 5-byte binary form for
; the interpreter to pick up.  This routine therefore parses the digits
; itself (ROM $30xx family, "read decimal from RAM to calculator stack"),
; or, failing that, looks the text up as a numeric variable.
; On exit: BC = value, A = the character that terminated it.
;----------------------------------------------------------------------------
GET_NUM:
$F983  DF           RST $18                   ; A = char at CH_ADD, HL = CH_ADD
$F984  CD 46 30     CALL $3046                ; ROM: is this the start of a decimal number?
$F987  30 20        JR NC,ERR_NONSENSE        ; no -> "C Nonsense in BASIC"
$F989  CD 4B 30     CALL $304B                ; ROM: read the digits
$F98C  38 08        JR C,SCAN_NAME            ; not a literal number -> try a variable name
$F98E  CD F9 30     CALL $30F9                ; ROM: finish stacking the value
NUM_TO_BC:
$F991  CD 23 1F     CALL $1F23                ; ROM: line number from calc stack -> BC
$F994  DF           RST $18                   ; A = terminating character
$F995  C9           RET
; operand was not a literal -- scan out the variable name
SCAN_NAME:
$F996  23           INC HL                    ; walk forward looking for the end of the name
$F997  7E           LD A,(HL)
$F998  FE 0D        CP $0D                    ; end of line ends the name
$F99A  28 0F        JR Z,LOOKUP_VAR
$F99C  FE 2C        CP $2C                    ; so does a comma
$F99E  28 0B        JR Z,LOOKUP_VAR
$F9A0  FE 21        CP $21                    ; skip anything below "!" (spaces, control codes)
$F9A2  38 F2        JR C,SCAN_NAME
$F9A4  CD 46 30     CALL $3046                ; ROM: still a legal name character?
$F9A7  38 ED        JR C,SCAN_NAME
ERR_NONSENSE:
$F9A9  CF           RST $08                   ; RST 8 / $0B  ->  "C Nonsense in BASIC"
$F9AA  0B           DEFB $0B                  
LOOKUP_VAR:
$F9AB  CD 70 2C     CALL $2C70                ; ROM: variable lookup
$F9AE  38 06        JR C,ERR_NOVAR            ; not found
$F9B0  23           INC HL
$F9B1  CD 73 37     CALL $3773                ; ROM: stack the variable's value
$F9B4  18 DB        JR NUM_TO_BC              ; and convert it to BC as before
ERR_NOVAR:
$F9B6  CF           RST $08                   ; RST 8 / $01  ->  "2 Variable not found"
$F9B7  01           DEFB $01                  

;----------------------------------------------------------------------------
; MATCH_KW  --  compare the BASIC line at CH_ADD against the name at DE
;
; Entry:  DE = name in KW_TABLE, last character has bit 7 set
; Exit :  carry set  = matched, CH_ADD advanced past the name
;         carry clear= no match, DE advanced past the name
;
; OR $20 forces the typed text to lower case, which is why the table is
; stored in lower case.  Characters below space are skipped, so embedded
; colour/AT control codes do not break a match -- but extra SPACES do,
; since space ($20) is not skipped.
;----------------------------------------------------------------------------
MATCH_KW:
$F9B8  DF           RST $18                   ; HL = CH_ADD
MK_NEXT:
$F9B9  1A           LD A,(DE)                 ; next character of the stored name
$F9BA  E6 7F        AND $7F                   ; drop the end-of-name marker
$F9BC  4F           LD C,A                    ; C = character we expect
MK_SKIP:
$F9BD  7E           LD A,(HL)                 ; take a character from the BASIC line
$F9BE  23           INC HL
$F9BF  FE 20        CP $20                    ; below space?
$F9C1  38 FA        JR C,MK_SKIP              ; yes - ignore it and take another
$F9C3  F6 20        OR $20                    ; force lower case
$F9C5  B9           CP C                      ; compare
$F9C6  20 0A        JR NZ,MK_FAIL             ; mismatch -> skip the rest of this name
$F9C8  1A           LD A,(DE)
$F9C9  13           INC DE
$F9CA  17           RLA                       ; bit 7 was set -> that was the last character
$F9CB  30 EC        JR NC,MK_NEXT
$F9CD  22 5D 5C     LD (CH_ADD),HL            ; matched: leave CH_ADD after the name
$F9D0  37           SCF                       ; carry set = success
$F9D1  C9           RET
MK_FAIL:
$F9D2  1A           LD A,(DE)                 ; run off the end of the rejected name
$F9D3  13           INC DE
$F9D4  17           RLA
$F9D5  30 FB        JR NC,MK_FAIL
$F9D7  B7           OR A                      ; carry clear = failure
$F9D8  C9           RET

;----------------------------------------------------------------------------
; KW_TABLE  --  the ten MultiTask commands
;
; Each entry: name in lower case with bit 7 set on the last character,
; then the 2-byte address of its handler.  A $00 byte ends the table.
;----------------------------------------------------------------------------
KW_TABLE:
$F9D9               DEFM "afte"
                     DEFB 'r'+$80
                     DEFW CMD_AFTER
$F9E0               DEFM "ever"
                     DEFB 'y'+$80
                     DEFW CMD_EVERY
$F9E7               DEFM "disabl"
                     DEFB 'e'+$80
                     DEFW CMD_DISABLE
$F9F0               DEFM "enabl"
                     DEFB 'e'+$80
                     DEFW CMD_ENABLE
$F9F8               DEFM "resum"
                     DEFB 'e'+$80
                     DEFW CMD_RESUME
$FA00               DEFM "on error got"
                     DEFB 'o'+$80
                     DEFW CMD_ON_ERROR
$FA0F               DEFM "on break sto"
                     DEFB 'p'+$80
                     DEFW CMD_BRK_STOP
$FA1E               DEFM "on break gosu"
                     DEFB 'b'+$80
                     DEFW CMD_BRK_GOSUB
$FA2E               DEFM "ignore brea"
                     DEFB 'k'+$80
                     DEFW CMD_BRK_IGNORE
$FA3C               DEFM "dro"
                     DEFB 'p'+$80
                     DEFW CMD_DROP
$FA42               DEFB $00                      ; end of table
; KW_GOSUB -- separate name, tested by AFTER/EVERY only
KW_GOSUB:
$FA43               DEFM "gosu"
                     DEFB 'b'+$80             ; tested on its own by AFTER/EVERY

;----------------------------------------------------------------------------
; DISPATCH  --  reached from DO_REM once a REM has been found to hold
; something other than "*".  Walks KW_TABLE and jumps to the handler.
;----------------------------------------------------------------------------
DISPATCH:
$FA48  11 D9 F9     LD DE,KW_TABLE            ; start of the keyword table
DP_LOOP:
$FA4B  CD B8 F9     CALL MATCH_KW             ; try this entry
$FA4E  38 08        JR C,DP_FOUND             ; matched
$FA50  13           INC DE                    ; step over the 2-byte handler address
$FA51  13           INC DE
$FA52  1A           LD A,(DE)                 ; $00 = end of table
$FA53  A7           AND A
$FA54  20 F5        JR NZ,DP_LOOP
$FA56  CF           RST $08                   ; RST 8 / $0B  ->  "C Nonsense in BASIC"
$FA57  0B           DEFB $0B                  
DP_FOUND:
$FA58  EB           EX DE,HL                  ; DE points at the handler address
$FA59  5E           LD E,(HL)
$FA5A  23           INC HL
$FA5B  56           LD D,(HL)
$FA5C  EB           EX DE,HL
$FA5D  E9           JP (HL)                   ; jump to it

;----------------------------------------------------------------------------
; ON BREAK STOP  /  IGNORE BREAK  /  ON BREAK GOSUB n
;
; $FF0B holds the setting the program asked for; $FF0D holds the one
; actually in force.  They differ only while a BREAK GOSUB handler is
; running, when $FF0D is forced to "stop" so BREAK still works inside
; the handler.  Valid line numbers are <= 9999 ($270F), so $FF00 and
; $FE00 are unambiguous sentinels.
;----------------------------------------------------------------------------
CMD_BRK_STOP:
$FA5E  21 00 FF     LD HL,$FF00               ; $FF00 = stop on BREAK (the default)
SET_BRK:
$FA61  22 0B FF     LD ($FF0B),HL             ; record what the program asked for
$FA64  FD CB 76 56  BIT 2,(IY+118)            ; inside a BREAK/task handler?  [= $5CB0 NMIADD]
$FA68  C0           RET NZ                    ; yes - do not disturb the live setting
$FA69  22 0D FF     LD ($FF0D),HL             ; no - make it live immediately
$FA6C  C9           RET
CMD_BRK_IGNORE:
$FA6D  21 00 FE     LD HL,$FE00               ; $FE00 = ignore BREAK entirely
$FA70  18 EF        JR SET_BRK
CMD_BRK_GOSUB:
$FA72  CD 83 F9     CALL GET_NUM              ; read the handler line number
$FA75  FE 0D        CP $0D                    ; must be the end of the REM
$FA77  C2 A9 F9     JP NZ,ERR_NONSENSE
$FA7A  69           LD L,C
$FA7B  60           LD H,B
$FA7C  11 10 27     LD DE,$2710               ; 10000
$FA7F  B7           OR A
$FA80  ED 52        SBC HL,DE
$FA82  19           ADD HL,DE
$FA83  38 DC        JR C,SET_BRK              ; line < 10000 -> accept it
$FA85  CF           RST $08                   ; RST 8 / $0A  ->  "B Integer out of range"
$FA86  0A           DEFB $0A                  

;----------------------------------------------------------------------------
; STMT_LOOP  --  MultiTask's replacement for the ROM statement loop
;
; This is a copy of the ROM's own loop with the token dispatch changed.
; Four tokens are handled here instead of by the ROM:
;     REM      ($EA)  -- carries MultiTask commands
;     IF       ($FA)  -- must end back inside THIS loop
;     RETURN   ($FE)  -- must recognise task/BREAK frames
;     CONTINUE ($E8)  -- repurposed as "resume the interrupted program"
; Everything else goes to the ROM dispatcher at $1A71 with STMT_RET
; already stacked, so the ROM routine returns into MultiTask.
;----------------------------------------------------------------------------
STMT_LOOP:
$FA87  E7           RST $20                   ; step to the next character
$FA88  CD 4E 13     CALL $134E                ; ROM: SET-WORK, reclaim the workspace
$FA8B  FD 34 0D     INC (IY+13)               ; count the statement  [= $5C47 SUBPPC]
$FA8E  DF           RST $18                   ; A = first character of the statement
$FA8F  06 00        LD B,$00
$FA91  FE 0D        CP $0D                    ; end of line?
$FA93  28 2E        JR Z,LINE_END
$FA95  FE 3A        CP $3A                    ; ":" -> another statement follows
$FA97  28 EE        JR Z,STMT_LOOP
$FA99  21 B5 FA     LD HL,STMT_RET            ; stack MultiTask's own STMT_RET, so whatever
$FA9C  E5           PUSH HL                   ; runs the statement comes back to us
$FA9D  4F           LD C,A                    ; C = the token
$FA9E  E7           RST $20                   ; step past it
$FA9F  79           LD A,C
$FAA0  FE EA        CP $EA                    ; REM
$FAA2  28 58        JR Z,DO_REM
$FAA4  FE FA        CP $FA                    ; IF
$FAA6  28 62        JR Z,DO_IF
$FAA8  FE FE        CP $FE                    ; RETURN
$FAAA  CA 56 FD     JP Z,DO_RETURN
$FAAD  FE E8        CP $E8                    ; CONTINUE
$FAAF  CA D0 FD     JP Z,DO_CONTINUE
$FAB2  C3 71 1A     JP $1A71                  ; ROM: run any other statement
; --- STMT_RET: reached after every statement ---
STMT_RET:
$FAB5  CD 0A FE     CALL CHECK_BRK            ; BREAK test, then run any task that has fired
STMT_NEXT:
$FAB8  DF           RST $18                   ; A = character after the statement
$FAB9  FE 0D        CP $0D
$FABB  28 06        JR Z,LINE_END             ; end of line
$FABD  FE 3A        CP $3A
$FABF  28 C6        JR Z,STMT_LOOP            ; ":" -> next statement
$FAC1  CF           RST $08                   ; RST 8 / $0B  ->  "C Nonsense in BASIC"
$FAC2  0B           DEFB $0B                  
; --- LINE_END: move on to the next BASIC line ---
LINE_END:
$FAC3  2A 55 5C     LD HL,(NXTLIN)            ; HL = address of the next line
LINE_USE:
$FAC6  3E C0        LD A,$C0                  ; a line number has bits 6-7 clear;
$FAC8  A6           AND (HL)                  ; $C0 set means end of program
$FAC9  28 02        JR Z,LU_1
$FACB  CF           RST $08                   ; RST 8 / $FF  ->  report "0 OK"
$FACC  FF           DEFB $FF                  
LU_1:
$FACD  AF           XOR A                     ; A = 0: start at statement 1
; --- LINE_RUN: HL = line header, A = statement number ---
LINE_RUN:
$FACE  FE 01        CP $01                    ; A=0 -> A=1, else leave it alone
$FAD0  CE 00        ADC A,$00
$FAD2  56           LD D,(HL)                 ; line number is stored high byte first
$FAD3  23           INC HL
$FAD4  5E           LD E,(HL)
$FAD5  ED 53 45 5C  LD (PPC),DE               ; PPC = the line we are on
$FAD9  23           INC HL
$FADA  5E           LD E,(HL)                 ; line length follows the number
$FADB  23           INC HL
$FADC  56           LD D,(HL)
$FADD  EB           EX DE,HL
$FADE  19           ADD HL,DE                 ; HL = address of the next line
$FADF  23           INC HL
$FAE0  22 55 5C     LD (NXTLIN),HL            ; NXTLIN
$FAE3  EB           EX DE,HL
$FAE4  22 5D 5C     LD (CH_ADD),HL            ; CH_ADD = first byte of this line's text
$FAE7  57           LD D,A                    ; D = statement number wanted
$FAE8  1E 00        LD E,$00
$FAEA  FD 36 0A FF  LD (IY+10),$FF            ; NSPPC = $FF: no jump pending  [= $5C44 NSPPC]
$FAEE  15           DEC D
$FAEF  FD 72 0D     LD (IY+13),D              ; SUBPPC = statement-1  [= $5C47 SUBPPC]
$FAF2  28 93        JR Z,STMT_LOOP            ; statement 1 -> just run the line
$FAF4  14           INC D
$FAF5  CD F3 16     CALL $16F3                ; ROM: skip forward D-1 statements
$FAF8  28 BE        JR Z,STMT_NEXT            ; found it
$FAFA  CF           RST $08                   ; RST 8 / $16  ->  "N Statement lost"
$FAFB  16           DEFB $16                  

;----------------------------------------------------------------------------
; DO_REM  --  the hook that makes the whole thing work
;
; "REM *"  is an ordinary comment: skip the rest of the line.
; "REM"    alone does nothing.
; Anything else is offered to DISPATCH as a MultiTask command.
;
; NOTE: this means that under MultiTask an ordinary remark MUST be
; written "REM * text".  A plain "REM text" will fail the keyword
; match and report "C Nonsense in BASIC".
;----------------------------------------------------------------------------
DO_REM:
$FAFC  DF           RST $18
$FAFD  FE 2A        CP $2A                    ; "*" = ordinary comment
$FAFF  20 03        JR NZ,REM_1
$FB01  E1           POP HL                    ; drop STMT_RET - nothing follows on this line
$FB02  18 BF        JR LINE_END
REM_1:
$FB04  FE 0D        CP $0D                    ; bare REM
$FB06  C8           RET Z
$FB07  C3 48 FA     JP DISPATCH               ; otherwise treat it as a command

;----------------------------------------------------------------------------
; DO_IF  --  IF is re-implemented only so that it ends up back in
; MultiTask's loop instead of the ROM's.  The logic is the ROM's.
;----------------------------------------------------------------------------
DO_IF:
$FB0A  CD E5 1B     CALL $1BE5                ; ROM: evaluate the condition
$FB0D  DF           RST $18
$FB0E  FE CB        CP $CB                    ; THEN
$FB10  C2 ED 1B     JP NZ,$1BED               ; ROM: report "C Nonsense in BASIC"
$FB13  C1           POP BC                    ; discard STMT_RET; we jump, not return
$FB14  EF           RST $28                   ; calculator: delete, end-calc
$FB15  02           DEFB $02                  
$FB16  38           DEFB $38                  
$FB17  EB           EX DE,HL
$FB18  CD 04 39     CALL $3904                ; ROM: test the value for zero
$FB1B  DA C3 FA     JP C,LINE_END             ; false -> abandon the rest of the line
$FB1E  C3 87 FA     JP STMT_LOOP              ; true  -> run what follows THEN

;----------------------------------------------------------------------------
; FN_INFO  --  USR 63872, called from inside a DEF FN
;
; Reads its argument through DEFADD, so it is meant to be set up as
;     DEF FN t(n) = USR 63872
; Argument 0-7  : ticks left on that task (0 if the task is disabled)
; Argument 8    : line number of the last trapped error
; Argument 9    : statement number of the last trapped error
; Argument 10   : character code of the last error report letter
;----------------------------------------------------------------------------
FN_INFO:
$FB21  2A 0B 5C     LD HL,(DEFADD)            ; address of the DEF FN argument
$FB24  23           INC HL
$FB25  23           INC HL
$FB26  CD 73 37     CALL $3773                ; ROM: stack the argument value
$FB29  CD 1E 1F     CALL $1F1E                ; ROM: single integer -> A
$FB2C  FE 0B        CP $0B                    ; 0..10 only
$FB2E  38 02        JR C,FI_TASK
$FB30  CF           RST $08                   ; RST 8 / $0A  ->  "B Integer out of range"
$FB31  0A           DEFB $0A                  
FI_TASK:
$FB32  FE 08        CP $08                    ; 0..7 = a task number
$FB34  30 19        JR NC,FI_8
$FB36  4F           LD C,A                    ; A = 7 * task
$FB37  87           ADD A,A
$FB38  87           ADD A,A
$FB39  87           ADD A,A
$FB3A  91           SUB C
$FB3B  6F           LD L,A
$FB3C  26 00        LD H,$00
$FB3E  11 0F FF     LD DE,$FF0F               ; base of the task table
$FB41  19           ADD HL,DE
$FB42  F3           DI                        ; the tick routine may be halfway through this
$FB43  4E           LD C,(HL)                 ; BC = counter
$FB44  23           INC HL
$FB45  46           LD B,(HL)
$FB46  FB           EI
$FB47  23           INC HL                    ; +2 = flags
$FB48  CB 46        BIT 0,(HL)                ; disabled?
$FB4A  C8           RET Z                     ; no - return the live count
$FB4B  01 00 00     LD BC,$0000               ; disabled tasks read as zero
$FB4E  C9           RET
FI_8:
$FB4F  FE 09        CP $09                    ; 8 = error line number
$FB51  30 05        JR NC,FI_9
$FB53  ED 4B 03 FF  LD BC,($FF03)
$FB57  C9           RET
FI_9:
$FB58  FE 0A        CP $0A                    ; 9 = error statement number
$FB5A  30 07        JR NC,FI_10
$FB5C  3A 02 FF     LD A,($FF02)
FI_RET8:
$FB5F  4F           LD C,A
$FB60  06 00        LD B,$00
$FB62  C9           RET
FI_10:
$FB63  3A 01 FF     LD A,($FF01)              ; 10 = error code
$FB66  C6 31        ADD A,$31                 ; convert to the report character: 0-9 ...
$FB68  FE 3A        CP $3A                    ; ... or A-R
$FB6A  38 F3        JR C,FI_RET8
$FB6C  C6 07        ADD A,$07
$FB6E  18 EF        JR FI_RET8

;----------------------------------------------------------------------------
; INT_HANDLER  --  the 50/60 Hz interrupt, reached via $F5F5
;
; The first byte is RST $38, which runs the ROM's own interrupt routine
; (frame counter, keyboard scan) and returns here -- so nothing the ROM
; normally does is lost.  Then every enabled task is counted down by one,
; and any that reaches zero is queued for the interpreter to pick up.
;
; TASK CONTROL BLOCK, 8 of them at $FF0F, 7 bytes each:
;     +0,+1   counter -- ticks remaining
;     +2      flags: bit 0 = disabled, bit 1 = repeating (EVERY)
;     +3,+4   ADDRESS of the BASIC line to run
;     +5,+6   reload value (the original interval)
;----------------------------------------------------------------------------
INT_HANDLER:
$FB70  FF           RST $38                   ; chain to the ROM interrupt routine first
$FB71  F5           PUSH AF
$FB72  C5           PUSH BC
$FB73  D5           PUSH DE
$FB74  E5           PUSH HL
$FB75  FD CB 76 46  BIT 0,(IY+118)            ; MTFLAG bit 0: ticking suspended?  [= $5CB0 NMIADD]
$FB79  20 2D        JR NZ,TICK_EXIT           ; yes - do nothing this tick
$FB7B  06 08        LD B,$08                  ; 8 tasks
$FB7D  21 0F FF     LD HL,$FF0F               ; first task control block
TICK_LOOP:
$FB80  E5           PUSH HL                   ; remember where this block starts
$FB81  5E           LD E,(HL)                 ; DE = counter
$FB82  23           INC HL
$FB83  56           LD D,(HL)
$FB84  23           INC HL                    ; HL -> flags
$FB85  CB 46        BIT 0,(HL)                ; disabled?
$FB87  20 0E        JR NZ,TICK_NEXT
$FB89  7A           LD A,D                    ; already expired?
$FB8A  B3           OR E
$FB8B  28 0A        JR Z,TICK_NEXT
$FB8D  1B           DEC DE                    ; count down one tick
$FB8E  2B           DEC HL
$FB8F  72           LD (HL),D
$FB90  2B           DEC HL
$FB91  73           LD (HL),E
$FB92  7A           LD A,D                    ; reached zero?
$FB93  B3           OR E
$FB94  CC AD FB     CALL Z,FIRE_TASK          ; yes - fire it
TICK_NEXT:
$FB97  11 07 00     LD DE,$0007               ; 7 bytes per block
$FB9A  E1           POP HL
$FB9B  19           ADD HL,DE
$FB9C  10 E2        DJNZ TICK_LOOP            ; next task
$FB9E  FD CB 00 7E  BIT 7,(IY+0)              ; ERR_NR = $FF means BASIC is running normally  [= $5C3A ERR_NR]
$FBA2  20 04        JR NZ,TICK_EXIT           ; still running - leave the ticker armed
$FBA4  FD CB 76 C6  SET 0,(IY+118)            ; BASIC has reported: latch the ticker off  [= $5CB0 NMIADD]
TICK_EXIT:
$FBA8  E1           POP HL                    ; (only START/ERR_TRAP clear that latch)
$FBA9  D1           POP DE
$FBAA  C1           POP BC
$FBAB  F1           POP AF
$FBAC  C9           RET

;----------------------------------------------------------------------------
; FIRE_TASK  --  push this task's line address onto the queue
; Entry: HL -> task control block +0.  Called with interrupts disabled.
;----------------------------------------------------------------------------
FIRE_TASK:
$FBAD  23           INC HL                    ; HL -> flags
$FBAE  23           INC HL
$FBAF  E5           PUSH HL                   ; keep it
$FBB0  23           INC HL                    ; DE = address of the line to run
$FBB1  5E           LD E,(HL)
$FBB2  23           INC HL
$FBB3  56           LD D,(HL)
$FBB4  D5           PUSH DE
$FBB5  2A 09 FF     LD HL,($FF09)             ; room left between the two workspaces?
$FBB8  ED 5B 07 FF  LD DE,($FF07)
$FBBC  B7           OR A
$FBBD  ED 52        SBC HL,DE
$FBBF  11 0F 00     LD DE,$000F               ; need 15 bytes of headroom
$FBC2  ED 52        SBC HL,DE
$FBC4  D1           POP DE
$FBC5  38 0A        JR C,FT_RELOAD            ; no room - drop this firing silently
$FBC7  2A 09 FF     LD HL,($FF09)             ; push the line address, queue grows downward
$FBCA  72           LD (HL),D
$FBCB  2B           DEC HL
$FBCC  73           LD (HL),E
$FBCD  2B           DEC HL
$FBCE  22 09 FF     LD ($FF09),HL
FT_RELOAD:
$FBD1  E1           POP HL                    ; HL -> flags again
$FBD2  CB 4E        BIT 1,(HL)                ; repeating (EVERY) rather than one-shot?
$FBD4  C8           RET Z                     ; AFTER: leave the counter at zero, task done
$FBD5  E5           PUSH HL
$FBD6  23           INC HL                    ; +5,+6 = the reload interval
$FBD7  23           INC HL
$FBD8  23           INC HL
$FBD9  5E           LD E,(HL)
$FBDA  23           INC HL
$FBDB  56           LD D,(HL)
$FBDC  E1           POP HL
$FBDD  2B           DEC HL                    ; write it back into the counter
$FBDE  72           LD (HL),D
$FBDF  2B           DEC HL
$FBE0  73           LD (HL),E
$FBE1  C9           RET

;----------------------------------------------------------------------------
; DISABLE [n]  /  ENABLE [n]   --  set or clear flags bit 0.
; With no argument, all eight tasks are affected.
;----------------------------------------------------------------------------
CMD_DISABLE:
$FBE2  DF           RST $18                   ; argument present?
$FBE3  FE 0D        CP $0D
$FBE5  28 1D        JR Z,DIS_ALL              ; no - do all eight
$FBE7  CD 12 FC     CALL GET_TASKNO           ; read a task number
$FBEA  FE 08        CP $08                    ; 0..7 only
$FBEC  30 2C        JR NC,ERR_RANGE
$FBEE  4F           LD C,A
$FBEF  7B           LD A,E
$FBF0  FE 0D        CP $0D                    ; nothing may follow it
$FBF2  C2 A9 F9     JP NZ,ERR_NONSENSE
$FBF5  79           LD A,C                    ; A = 7 * task
$FBF6  87           ADD A,A
$FBF7  87           ADD A,A
$FBF8  87           ADD A,A
$FBF9  91           SUB C
$FBFA  6F           LD L,A
$FBFB  26 00        LD H,$00
$FBFD  11 11 FF     LD DE,$FF11               ; flags byte of task 0
$FC00  19           ADD HL,DE
$FC01  CB C6        SET 0,(HL)                ; bit 0 = disabled
$FC03  C9           RET
DIS_ALL:
$FC04  06 08        LD B,$08                  ; all eight
$FC06  21 11 FF     LD HL,$FF11
$FC09  11 07 00     LD DE,$0007
$FC0C  CB C6        SET 0,(HL)
$FC0E  19           ADD HL,DE
$FC0F  10 FB        DJNZ $FC0C
$FC11  C9           RET
; --- GET_TASKNO: a number that must fit in one byte ---
GET_TASKNO:
$FC12  CD 83 F9     CALL GET_NUM              ; BC = value, A = terminator
$FC15  5F           LD E,A                    ; E = terminator
$FC16  78           LD A,B                    ; B must be zero
$FC17  A7           AND A
$FC18  79           LD A,C                    ; A = the value
$FC19  C8           RET Z
ERR_RANGE:
$FC1A  CF           RST $08                   ; RST 8 / $0A  ->  "B Integer out of range"
$FC1B  0A           DEFB $0A                  
CMD_ENABLE:
$FC1C  DF           RST $18
$FC1D  FE 0D        CP $0D
$FC1F  28 1D        JR Z,ENA_ALL
$FC21  CD 12 FC     CALL GET_TASKNO
$FC24  FE 08        CP $08
$FC26  30 F2        JR NC,ERR_RANGE
$FC28  4F           LD C,A
$FC29  7B           LD A,E
$FC2A  FE 0D        CP $0D
$FC2C  C2 A9 F9     JP NZ,ERR_NONSENSE
$FC2F  79           LD A,C
$FC30  87           ADD A,A
$FC31  87           ADD A,A
$FC32  87           ADD A,A
$FC33  91           SUB C
$FC34  6F           LD L,A
$FC35  26 00        LD H,$00
$FC37  11 11 FF     LD DE,$FF11
$FC3A  19           ADD HL,DE
$FC3B  CB 86        RES 0,(HL)
$FC3D  C9           RET
ENA_ALL:
$FC3E  06 08        LD B,$08
$FC40  21 11 FF     LD HL,$FF11
$FC43  11 07 00     LD DE,$0007
$FC46  CB 86        RES 0,(HL)
$FC48  19           ADD HL,DE
$FC49  10 FB        DJNZ $FC46
$FC4B  C9           RET

;----------------------------------------------------------------------------
; RESUME [n]  --  end an ON ERROR handler.
; Bare RESUME goes back to the statement that failed -- which will run
; it again, so an unconditional RESUME on a permanent fault loops.
; RESUME n continues at line n.
;----------------------------------------------------------------------------
CMD_RESUME:
$FC4C  FD CB 76 4E  BIT 1,(IY+118)            ; MTFLAG bit 1: are we in a handler?  [= $5CB0 NMIADD]
$FC50  20 02        JR NZ,RES_1
$FC52  CF           RST $08                   ; RST 8 / $06  ->  "7 RETURN without GOSUB"
$FC53  06           DEFB $06                  
RES_1:
$FC54  DF           RST $18
$FC55  FE 0D        CP $0D
$FC57  28 13        JR Z,RES_RETRY            ; bare RESUME
$FC59  CD 83 F9     CALL GET_NUM              ; RESUME n
$FC5C  FE 0D        CP $0D
$FC5E  C2 A9 F9     JP NZ,ERR_NONSENSE
$FC61  21 0F 27     LD HL,$270F               ; 9999
$FC64  B7           OR A
$FC65  ED 42        SBC HL,BC
$FC67  38 B1        JR C,ERR_RANGE
$FC69  AF           XOR A                     ; statement 0 -> the first one
$FC6A  18 07        JR RES_GO
RES_RETRY:
$FC6C  ED 4B 03 FF  LD BC,($FF03)             ; the line the error happened on
$FC70  3A 02 FF     LD A,($FF02)              ; and the statement within it
RES_GO:
$FC73  FD CB 76 8E  RES 1,(IY+118)            ; no longer inside a handler  [= $5CB0 NMIADD]
$FC77  ED 43 42 5C  LD (NEWPPC),BC            ; make the interpreter jump there
$FC7B  32 44 5C     LD (NSPPC),A
$FC7E  C9           RET

;----------------------------------------------------------------------------
; ON ERROR GOTO n
;
; ERRSP points at the stacked address that the ROM error routine will
; return to.  Overwriting that word with ERR_TRAP is what steals every
; subsequent error.
;----------------------------------------------------------------------------
CMD_ON_ERROR:
$FC7F  CD 83 F9     CALL GET_NUM
$FC82  FE 0D        CP $0D
$FC84  C2 A9 F9     JP NZ,ERR_NONSENSE
$FC87  21 0F 27     LD HL,$270F               ; 9999
$FC8A  B7           OR A
$FC8B  ED 42        SBC HL,BC
$FC8D  38 8B        JR C,ERR_RANGE
$FC8F  ED 43 05 FF  LD ($FF05),BC             ; remember the handler line
$FC93  11 8B FD     LD DE,ERR_TRAP            ; our trap
$FC96  2A 3D 5C     LD HL,(ERRSP)             ; top of the error stack
$FC99  73           LD (HL),E                 ; displace the ROM's error return address
$FC9A  23           INC HL
$FC9B  72           LD (HL),D
$FC9C  C9           RET

;----------------------------------------------------------------------------
; AFTER n[,t] GOSUB line     one-shot
; EVERY n[,t] GOSUB line     repeating
;
; n = interrupts to wait (1/50 s each on a 50 Hz machine, 1/60 on 60 Hz)
; t = task number 0-7, default 0
;
; NOTE the line is resolved to an ADDRESS here and stored that way, so a
; task set up before the program is edited will point at the wrong place.
;----------------------------------------------------------------------------
CMD_AFTER:
$FC9D  DD 2E 00     LD IXL,$00                ; IXL = 0: one-shot
$FCA0  18 03        JR AE_PARSE
CMD_EVERY:
$FCA2  DD 2E 02     LD IXL,$02                ; IXL = 2: sets flags bit 1, repeating
AE_PARSE:
$FCA5  CD 83 F9     CALL GET_NUM              ; BC = interval
$FCA8  FE 2C        CP $2C                    ; comma must follow
$FCAA  28 02        JR Z,AE_1
AE_ERR:
$FCAC  CF           RST $08                   ; RST 8 / $0B  ->  "C Nonsense in BASIC"
$FCAD  0B           DEFB $0B                  
AE_1:
$FCAE  E7           RST $20
$FCAF  C5           PUSH BC                   ; save the interval
$FCB0  11 43 FA     LD DE,KW_GOSUB            ; "gosub"?
$FCB3  CD B8 F9     CALL MATCH_KW
$FCB6  3E 00        LD A,$00                  ; yes - task 0 by default
$FCB8  38 19        JR C,AE_LINE
$FCBA  CD 12 FC     CALL GET_TASKNO           ; otherwise read the task number
$FCBD  FE 08        CP $08                    ; 0..7
$FCBF  D2 1A FC     JP NC,ERR_RANGE
$FCC2  F5           PUSH AF
$FCC3  7B           LD A,E                    ; a comma must have ended it
$FCC4  FE 2C        CP $2C
$FCC6  C2 A9 F9     JP NZ,ERR_NONSENSE
$FCC9  E7           RST $20
$FCCA  11 43 FA     LD DE,KW_GOSUB            ; "gosub" must follow
$FCCD  CD B8 F9     CALL MATCH_KW
$FCD0  30 DA        JR NC,AE_ERR
$FCD2  F1           POP AF
AE_LINE:
$FCD3  F5           PUSH AF
$FCD4  CD 83 F9     CALL GET_NUM              ; BC = the line number
$FCD7  FE 0D        CP $0D
$FCD9  C2 A9 F9     JP NZ,ERR_NONSENSE
$FCDC  21 0F 27     LD HL,$270F               ; 9999
$FCDF  B7           OR A
$FCE0  ED 42        SBC HL,BC
$FCE2  DA 1A FC     JP C,ERR_RANGE
$FCE5  F1           POP AF                    ; A = task number
$FCE6  5F           LD E,A                    ; HL = $FF0F + 7*task
$FCE7  87           ADD A,A
$FCE8  87           ADD A,A
$FCE9  87           ADD A,A
$FCEA  93           SUB E
$FCEB  6F           LD L,A
$FCEC  26 00        LD H,$00
$FCEE  11 0F FF     LD DE,$FF0F
$FCF1  19           ADD HL,DE
$FCF2  E5           PUSH HL
$FCF3  69           LD L,C
$FCF4  60           LD H,B
$FCF5  CD D6 16     CALL $16D6                ; ROM: line number -> line address
$FCF8  EB           EX DE,HL
$FCF9  E1           POP HL
$FCFA  36 00        LD (HL),$00               ; +0,+1 counter (set last, under DI)
$FCFC  23           INC HL
$FCFD  36 00        LD (HL),$00
$FCFF  23           INC HL
$FD00  DD 7D        LD A,IXL                  ; 0 for AFTER, 2 for EVERY
$FD02  CB 8E        RES 1,(HL)                ; replace the repeat bit only...
$FD04  B6           OR (HL)                   ; ...keeping any existing "disabled" bit
$FD05  77           LD (HL),A
$FD06  23           INC HL
$FD07  73           LD (HL),E                 ; +3,+4 = address of the line
$FD08  23           INC HL
$FD09  72           LD (HL),D
$FD0A  23           INC HL
$FD0B  D1           POP DE                    ; the interval again
$FD0C  73           LD (HL),E                 ; +5,+6 = reload value
$FD0D  23           INC HL
$FD0E  72           LD (HL),D
$FD0F  2B           DEC HL
$FD10  2B           DEC HL
$FD11  2B           DEC HL
$FD12  2B           DEC HL
$FD13  2B           DEC HL
$FD14  F3           DI                        ; the tick routine must not see a half-written count
$FD15  72           LD (HL),D                 ; +0,+1 = counter, started at the interval
$FD16  2B           DEC HL
$FD17  73           LD (HL),E
$FD18  FB           EI
$FD19  C9           RET

;----------------------------------------------------------------------------
; REENTER (63869)  --  drop back into MultiTask's statement loop
;----------------------------------------------------------------------------
REENTER:
$FD1A  ED 7B 3D 5C  LD SP,(ERRSP)             ; discard whatever the caller stacked
$FD1E  C3 B8 FA     JP STMT_NEXT              ; carry on with the next statement

;----------------------------------------------------------------------------
; START (63866)  --  initialise everything, then take over the loop
;
; Because it never returns to the ROM loop -- it falls into REENTER --
; the BASIC program simply carries on from the statement after the
; RANDOMIZE USR, but from now on every statement passes through
; STMT_LOOP.  That is how MultiTask installs itself.
;----------------------------------------------------------------------------
START:
$FD21  FD 36 76 00  LD (IY+118),$00           ; clear all MultiTask flags  [= $5CB0 NMIADD]
$FD25  21 70 FB     LD HL,INT_HANDLER         ; re-plant the handler address in the saved image
$FD28  22 FF FE     LD (INT_ADDR),HL
$FD2B  21 0F FF     LD HL,$FF0F               ; clear the 56-byte task table:
$FD2E  11 10 FF     LD DE,$FF10
$FD31  01 37 00     LD BC,$0037
$FD34  36 00        LD (HL),$00               ; $FF0F..$FF46, all eight blocks
$FD36  ED B0        LDIR
$FD38  00           NOP
$FD39  00           NOP
$FD3A  00           NOP
$FD3B  00           NOP
$FD3C  ED 5E        IM 2                      ; NOW switch to interrupt mode 2
$FD3E  2A B2 5C     LD HL,(RAMTOP)            ; context frames grow up from RAMTOP+1
$FD41  23           INC HL
$FD42  22 07 FF     LD ($FF07),HL
$FD45  21 79 F9     LD HL,QUEUE_BASE          ; fired-task queue grows down from here
$FD48  22 09 FF     LD ($FF09),HL
$FD4B  21 00 FF     LD HL,$FF00               ; $FF00 = ON BREAK STOP
$FD4E  22 0B FF     LD ($FF0B),HL
$FD51  22 0D FF     LD ($FF0D),HL
$FD54  18 C4        JR REENTER                ; enter the loop and never come back

;----------------------------------------------------------------------------
; RETURN  --  re-implemented because the GOSUB stack now carries two
; kinds of frame.  A frame whose line number has bit 15 set was pushed
; by BRK_GOSUB, not by a real GOSUB, and returning from it also has to
; put the BREAK setting back.
;----------------------------------------------------------------------------
DO_RETURN:
$FD56  ED 7B 3D 5C  LD SP,(ERRSP)             ; the GOSUB stack lives below ERRSP
$FD5A  DD E1        POP IX                    ; the ROM error-handler frame
$FD5C  D1           POP DE                    ; DE = stacked line number
$FD5D  7A           LD A,D
$FD5E  FE 3E        CP $3E                    ; $3E marks the bottom of the GOSUB stack
$FD60  20 05        JR NZ,RET_1
$FD62  D5           PUSH DE                   ; put it back
$FD63  DD E5        PUSH IX
$FD65  CF           RST $08                   ; RST 8 / $06  ->  "7 RETURN without GOSUB"
$FD66  06           DEFB $06                  
RET_1:
$FD67  CB 7A        BIT 7,D                   ; a MultiTask frame?
$FD69  20 12        JR NZ,RET_TASK
RET_GO:
$FD6B  3B           DEC SP                    ; A = stacked statement number
$FD6C  F1           POP AF
$FD6D  DD E5        PUSH IX
$FD6F  ED 73 3D 5C  LD (ERRSP),SP             ; shorten the error stack
$FD73  ED 53 42 5C  LD (NEWPPC),DE            ; jump back to the caller
$FD77  32 44 5C     LD (NSPPC),A
$FD7A  C3 B5 FA     JP STMT_RET
RET_TASK:
$FD7D  CB BA        RES 7,D                   ; strip the marker bit
$FD7F  FD CB 76 96  RES 2,(IY+118)            ; no longer inside a handler  [= $5CB0 NMIADD]
$FD83  2A 0B FF     LD HL,($FF0B)             ; restore the BREAK setting the program asked for
$FD86  22 0D FF     LD ($FF0D),HL
$FD89  18 E0        JR RET_GO

;----------------------------------------------------------------------------
; ERR_TRAP  --  entered instead of the ROM error report, because
; ON ERROR GOTO overwrote the address at ERRSP.
;----------------------------------------------------------------------------
ERR_TRAP:
$FD8B  FD CB 76 4E  BIT 1,(IY+118)            ; already handling an error?  [= $5CB0 NMIADD]
$FD8F  C2 D8 0E     JP NZ,$0ED8               ; yes - let the ROM report this one normally
$FD92  3A 3A 5C     LD A,(ERR_NR)             ; save the error code,
$FD95  32 01 FF     LD ($FF01),A
$FD98  FD 36 00 FF  LD (IY+0),$FF             ; then clear it so BASIC keeps running  [= $5C3A ERR_NR]
$FD9C  FD CB 76 86  RES 0,(IY+118)            ; re-arm the ticker (INT_HANDLER latched it off)  [= $5CB0 NMIADD]
$FDA0  FD CB 76 CE  SET 1,(IY+118)            ; mark: inside a handler, RESUME is now legal  [= $5CB0 NMIADD]
$FDA4  21 00 00     LD HL,$0000
$FDA7  FD 75 37     LD (IY+55),L              ; the ROM's standard post-error tidy-up  [= $5C71 FLAGX]
$FDAA  22 5F 5C     LD (X_PTR),HL
$FDAD  22 0B 5C     LD (DEFADD),HL
$FDB0  CD 3F 13     CALL $133F                ; ROM: reset the workspace
$FDB3  21 8B FD     LD HL,ERR_TRAP            ; re-arm the trap for the next error
$FDB6  E5           PUSH HL
$FDB7  2A 45 5C     LD HL,(PPC)               ; record where the error happened,
$FDBA  22 03 FF     LD ($FF03),HL
$FDBD  3A 47 5C     LD A,(SUBPPC)             ; line and statement, for RESUME and FN_INFO
$FDC0  32 02 FF     LD ($FF02),A
$FDC3  2A 05 FF     LD HL,($FF05)             ; the ON ERROR GOTO line
$FDC6  22 42 5C     LD (NEWPPC),HL
$FDC9  FD 36 0A 00  LD (IY+10),$00            ; first statement of it  [= $5C44 NSPPC]
$FDCD  C3 B5 FA     JP STMT_RET

;----------------------------------------------------------------------------
; CONTINUE  --  repurposed.  It no longer resumes after a STOP; it pops
; the context frame that DISPATCH_TASK saved and resumes the program the
; task interrupted, restoring the print position and plot coordinates
; as well as the line and statement.  Every task line must end in one.
;
; 9-byte frame, written upward from $FF07:
;     +0  statement number
;     +1,+2  line number     +3,+4  COORDS
;     +5,+6  DF_CC           +7,+8  S_POSN
;----------------------------------------------------------------------------
DO_CONTINUE:
$FDD0  ED 5B 07 FF  LD DE,($FF07)             ; top of the context stack
$FDD4  2A B2 5C     LD HL,(RAMTOP)            ; empty means nothing was interrupted
$FDD7  23           INC HL
$FDD8  B7           OR A
$FDD9  ED 52        SBC HL,DE
$FDDB  20 02        JR NZ,CONT_1
$FDDD  CF           RST $08                   ; RST 8 / $06  ->  "7 RETURN without GOSUB"
$FDDE  06           DEFB $06                  
CONT_1:
$FDDF  EB           EX DE,HL
$FDE0  CD 05 FE     CALL POP_WORD             ; restore the print position,
$FDE3  ED 53 88 5C  LD (S_POSN),DE
$FDE7  CD 05 FE     CALL POP_WORD             ; the display file cursor,
$FDEA  ED 53 84 5C  LD (DF_CC),DE
$FDEE  CD 05 FE     CALL POP_WORD             ; the graphics coordinates,
$FDF1  ED 53 7D 5C  LD (COORDS),DE
$FDF5  CD 05 FE     CALL POP_WORD             ; and the line to go back to
$FDF8  ED 53 42 5C  LD (NEWPPC),DE
$FDFC  2B           DEC HL                    ; and the statement within it
$FDFD  7E           LD A,(HL)
$FDFE  32 44 5C     LD (NSPPC),A
$FE01  22 07 FF     LD ($FF07),HL             ; drop the frame
$FE04  C9           RET
; --- POP_WORD: fetch the word just below HL ---
POP_WORD:
$FE05  2B           DEC HL
$FE06  56           LD D,(HL)
$FE07  2B           DEC HL
$FE08  5E           LD E,(HL)
$FE09  C9           RET

;----------------------------------------------------------------------------
; CHECK_BRK  --  called from STMT_RET after every single statement.
; Deals with BREAK, then falls into DISPATCH_TASK.
;----------------------------------------------------------------------------
CHECK_BRK:
$FE0A  FD CB 76 56  BIT 2,(IY+118)            ; already inside a handler? then skip BREAK  [= $5CB0 NMIADD]
$FE0E  20 4D        JR NZ,DISPATCH_TASK
$FE10  2A 0D FF     LD HL,($FF0D)             ; the BREAK setting in force
$FE13  7C           LD A,H
$FE14  FE FE        CP $FE                    ; $FE00 = IGNORE BREAK
$FE16  28 45        JR Z,DISPATCH_TASK
$FE18  FE FF        CP $FF                    ; $FF00 = ON BREAK STOP
$FE1A  20 07        JR NZ,BRK_GOSUB           ; anything else is a GOSUB line number
$FE1C  CD 09 20     CALL $2009                ; ROM: BREAK pressed? (carry set = no)
$FE1F  38 3C        JR C,DISPATCH_TASK
$FE21  CF           RST $08                   ; RST 8 / $14  ->  "L BREAK into program"
$FE22  14           DEFB $14                  
; --- ON BREAK GOSUB: manufacture a GOSUB frame by hand ---
BRK_GOSUB:
$FE23  CD 09 20     CALL $2009                ; ROM: BREAK pressed?
$FE26  38 35        JR C,DISPATCH_TASK
$FE28  ED 7B 3D 5C  LD SP,(ERRSP)             ; build the frame on the GOSUB stack
$FE2C  DD E1        POP IX
$FE2E  ED 5B 45 5C  LD DE,(PPC)               ; where we are now...
$FE32  3A 47 5C     LD A,(SUBPPC)
$FE35  3C           INC A
$FE36  FD CB 0A 7E  BIT 7,(IY+10)             ; ...unless a jump is already pending,  [= $5C44 NSPPC]
$FE3A  20 07        JR NZ,BG_PUSH
$FE3C  ED 5B 42 5C  LD DE,(NEWPPC)            ; in which case use that instead
$FE40  3A 44 5C     LD A,(NSPPC)
BG_PUSH:
$FE43  F5           PUSH AF                   ; push the statement number as one byte
$FE44  33           INC SP
$FE45  CB FA        SET 7,D                   ; bit 15 marks this as not a real GOSUB
$FE47  D5           PUSH DE
$FE48  DD E5        PUSH IX
$FE4A  ED 73 3D 5C  LD (ERRSP),SP             ; ERRSP now covers the new frame
$FE4E  22 42 5C     LD (NEWPPC),HL            ; jump to the handler line
$FE51  FD 36 0A 00  LD (IY+10),$00            ; = $5C44 NSPPC
$FE55  FD CB 76 D6  SET 2,(IY+118)            ; mark: inside a handler  [= $5CB0 NMIADD]
$FE59  21 B8 FA     LD HL,STMT_NEXT           ; come back into the loop afterwards
$FE5C  E5           PUSH HL

;----------------------------------------------------------------------------
; DISPATCH_TASK  --  if a task has fired, switch to it.
;
; Unlike ON BREAK GOSUB this does NOT stack a return address.  The
; interrupted program's position and screen state are saved as a 9-byte
; frame instead, and the task line is entered as though it were the next
; line of the program.  The task gets back by executing CONTINUE (or
; abandons the interrupted program with DROP).
;----------------------------------------------------------------------------
DISPATCH_TASK:
$FE5D  21 79 F9     LD HL,QUEUE_BASE          ; queue base
$FE60  B7           OR A
$FE61  F3           DI                        ; the interrupt must not push while we pop
$FE62  ED 5B 09 FF  LD DE,($FF09)
$FE66  ED 52        SBC HL,DE
$FE68  28 5A        JR Z,NO_TASK              ; queue empty - nothing fired
$FE6A  EB           EX DE,HL
$FE6B  23           INC HL                    ; pop the line address
$FE6C  5E           LD E,(HL)
$FE6D  23           INC HL
$FE6E  56           LD D,(HL)
$FE6F  22 09 FF     LD ($FF09),HL             ; queue shrinks upward
$FE72  FB           EI
$FE73  ED 4B 07 FF  LD BC,($FF07)             ; room for another context frame?
$FE77  B7           OR A
$FE78  ED 42        SBC HL,BC
$FE7A  C5           PUSH BC
$FE7B  01 0F 00     LD BC,$000F               ; need 15 bytes clear
$FE7E  ED 42        SBC HL,BC
$FE80  C1           POP BC
$FE81  38 41        JR C,NO_TASK              ; no room - abandon the switch
SAVE_CTX:
$FE83  ED 7B 3D 5C  LD SP,(ERRSP)
$FE87  69           LD L,C                    ; HL = top of the context stack
$FE88  60           LD H,B
$FE89  ED 4B 45 5C  LD BC,(PPC)               ; where the program is now...
$FE8D  3A 47 5C     LD A,(SUBPPC)
$FE90  3C           INC A
$FE91  FD CB 0A 7E  BIT 7,(IY+10)             ; ...or where it was about to jump  [= $5C44 NSPPC]
$FE95  20 07        JR NZ,SC_WRITE
$FE97  ED 4B 42 5C  LD BC,(NEWPPC)
$FE9B  3A 44 5C     LD A,(NSPPC)
SC_WRITE:
$FE9E  77           LD (HL),A                 ; +0 statement number
$FE9F  CD BF FE     CALL PUSH_WORD            ; +1,+2 line number
$FEA2  ED 4B 7D 5C  LD BC,(COORDS)            ; +3,+4 plot coordinates
$FEA6  CD BF FE     CALL PUSH_WORD
$FEA9  ED 4B 84 5C  LD BC,(DF_CC)             ; +5,+6 display file cursor
$FEAD  CD BF FE     CALL PUSH_WORD
$FEB0  ED 4B 88 5C  LD BC,(S_POSN)            ; +7,+8 print position
$FEB4  CD BF FE     CALL PUSH_WORD
$FEB7  23           INC HL
$FEB8  22 07 FF     LD ($FF07),HL             ; frame committed
$FEBB  EB           EX DE,HL                  ; HL = the task's line address
$FEBC  C3 C6 FA     JP LINE_USE               ; and run it
; --- PUSH_WORD ---
PUSH_WORD:
$FEBF  23           INC HL
$FEC0  71           LD (HL),C
$FEC1  23           INC HL
$FEC2  70           LD (HL),B
$FEC3  C9           RET
; --- NO_TASK: nothing fired; obey any pending jump ---
NO_TASK:
$FEC4  FB           EI
$FEC5  FD CB 0A 7E  BIT 7,(IY+10)             ; NSPPC bit 7 set = no jump pending  [= $5C44 NSPPC]
$FEC9  C0           RET NZ                    ; so just carry on
$FECA  2A 42 5C     LD HL,(NEWPPC)            ; the line we were told to go to
$FECD  CD D6 16     CALL $16D6                ; ROM: line number -> line address
$FED0  3A 44 5C     LD A,(NSPPC)
$FED3  28 0C        JR Z,NT_RUN               ; exact match - run it
$FED5  A7           AND A
$FED6  C2 42 1B     JP NZ,$1B42               ; ROM: report, line does not exist
$FED9  47           LD B,A
$FEDA  7E           LD A,(HL)                 ; past the end of the program?
$FEDB  E6 C0        AND $C0
$FEDD  78           LD A,B
$FEDE  C2 FE 1A     JP NZ,$1AFE               ; ROM: report
NT_RUN:
$FEE1  C1           POP BC                    ; discard the caller
$FEE2  C3 CE FA     JP LINE_RUN

;----------------------------------------------------------------------------
; DROP  --  throw away the top context frame without resuming it, so a
; task can decide not to return to the program it interrupted.
;----------------------------------------------------------------------------
CMD_DROP:
$FEE5  2A B2 5C     LD HL,(RAMTOP)
$FEE8  ED 5B 07 FF  LD DE,($FF07)             ; top of the context stack
$FEEC  23           INC HL                    ; empty?
$FEED  B7           OR A
$FEEE  ED 52        SBC HL,DE
$FEF0  20 02        JR NZ,DROP_1
$FEF2  CF           RST $08                   ; RST 8 / $0D  ->  "E Out of DATA"
$FEF3  0D           DEFB $0D                  
DROP_1:
$FEF4  EB           EX DE,HL
$FEF5  11 09 00     LD DE,$0009               ; frames are 9 bytes
$FEF8  B7           OR A
$FEF9  ED 52        SBC HL,DE
$FEFB  22 07 FF     LD ($FF07),HL
$FEFE  C9           RET

;----------------------------------------------------------------------------
; INT_ADDR  --  the last two bytes of the saved block.  START copies
; this value back over itself; it exists so the handler address travels
; with the CODE file.
;----------------------------------------------------------------------------
INT_ADDR:
$FEFF               DEFB $70,$FB                        ; |p.|

Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.

People

No people associated with this content.

Scroll to Top