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$F400with$F5, writes aJP INT_HANDLERtrampoline at$F5F5, and loadsIwith$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 withRST $38so 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) wheneverERR_NRis 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:
| Offset | Size | Contents |
|---|---|---|
| +0,+1 | 2 | Countdown counter (ticks remaining) |
| +2 | 1 | Flags: bit 0 = disabled, bit 1 = repeating (EVERY) |
| +3,+4 | 2 | Address of the BASIC line to run |
| +5,+6 | 2 | Reload 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)
| Offset | Contents |
|---|---|
| +0 | Statement number |
| +1,+2 | Line number (PPC / NEWPPC) |
| +3,+4 | COORDS (plot coordinates) |
| +5,+6 | DF_CC (display file cursor) |
| +7,+8 | S_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 $20for 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/$30F9for literal numbers, then falls back to a variable lookup via$2C70and$3773for 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_ADDRroutine. 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 63866after 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 theREM *prefix to be recognized as such. - The
$FF07stack pointer is compared againstRAMTOP+1to detect an empty context stack, but RAMTOP is a user-settable value. IfCLEARis executed while a task is running, the frame-stack base shifts and the empty-stack check becomes unreliable.
Content
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.
