The keywords STOP and END are synonymous. We don't need to do anything other than lose the return address and fall into Main.
01F7 | C0 | Stop | RNZ | Syntax Error if args. |
01F8 | C1 | POP B | Lose return address. |
Here's where a BASIC programmer in 1975 spent most of their time : typing at an "OK" prompt, one line at a time. A line of input would either be exec'd immediately (eg "PRINT 2+2"), or it would be a line of a program to be RUN later. Program lines would be prefixed with a line number. The code below looks for that line number, and jumps ahead to Exec if it's not there.
Print "OK" | ||||
01F9 | 218D01 | Main | LXI H,szOK | |
01FC | CDA305 | CALL PrintString | ||
Set current line number to -1, indicating we're in immediate mode. | ||||
01FF | 21FFFF | GetNonBlankLine | LXI H,FFFF | |
0202 | 226101 | SHLD CURRENT_LINE | ||
Get a line of input | ||||
0205 | CD3C03 | CALL InputLine | ||
Get first char of input. Note that carry will be set if it's a digit. | ||||
0208 | D7 | RST NextChar | ||
If line is blank (ie A is the null byte terminating the input buffer) then loop back to get a new input line. Notice the interesting method of testing for 0 - inc followed by dec. This is done so as not to affect the carry flag, which CPI 0 and OR A would have both done. | ||||
0209 | 3C | INR A | ||
020A | 3D | DCR A | ||
020B | CAFF01 | JZ GetNonBlankLine | ||
Preserve first-char-is-digit flag (carry). | ||||
020E | F5 | PUSH PSW | ||
If the line of input begins with a line number, this call will read that number into DE. | ||||
020F | CD9D04 | CALL LineNumberFromStr | ||
Preserve line number on stack and tokenize the rest of the line. | ||||
0212 | D5 | PUSH D | ||
0213 | CDCC02 | CALL Tokenize | ||
Tokenize returns with the length of the tokenized line content in C and with A=0. Here we load B with 0, so as to have the line length in the 16-bit register BC. | ||||
0216 | 47 | MOV B,A | ||
Restore line number to DE. | ||||
0217 | D1 | POP D | ||
Restore first-char-is-digit flag (carry), and if the first char was /not/ a digit, then execute the line of input now. | ||||
0218 | F1 | POP PSW | ||
0219 | D23E04 | JNC Exec | ||
First char was a digit, therefore it's a line of program which needs to be stored, so we can fall into StoreProgramLine... |
Here's where a program line has been typed, which we now need to store in program memory.
021C | D5 | StoreProgramLine | PUSH D | Push line number |
021D | C5 | PUSH B | Push line length | |
021E | D7 | RST NextChar | Get first char of line | |
021F | B7 | ORA A | Zero set if line is empty (ie removing a line) | |
0220 | F5 | PUSH PSW | Preserve line-empty flag | |
0221 | CD7D02 | CALL FindProgramLine | Get nearest program line address in BC. | |
0224 | C5 | PUSH B | Push line address. | |
0225 | D23902 | JNC InsertProgramLine | If line doesn't exist, jump ahead to insert it. | |
Carry was set by the call to FindProgramLine, meaning that the line already exists. So we have to remove the old program line before inserting the new one in it's place. To remove the program line we simply move the remainder of the program (ie every line that comes after it) down in memory. | ||||
0228 | EB | RemoveProgramLine | XCHG | DE=Next line address. |
0229 | 2A6701 | LHLD VAR_BASE | ||
022C | 1A | LDAX D | Move byte of program remainder down | |
022D | 02 | STAX B | in memory. | |
022E | 03 | INX B | ||
022F | 13 | INX D | ||
0230 | E7 | RST CompareHLDE | Loop until DE==VAR_BASE, ie whole | |
0231 | C22C02 | JNZ RemoveLine+4 | program remainder done. | |
0234 | 60 | MOV H,B | ||
0235 | 69 | MOV L,C | Update VAR_BASE from BC. | |
0236 | 226701 | SHLD VAR_BASE | ||
To insert the program line, firstly the program remainder (every line that comes after the one to be inserted) must be moved up in memory to make room. | ||||
0239 | D1 | InsertProgramLine | POP D | DE=Line address (from 224) |
023A | F1 | POP PSW | Restore line-empty flag (see above) | |
023B | CA6002 | JZ UpdateLinkedList | If line is empty, then we don't need to insert it so can jump ahead. | |
023E | 2A6701 | LHLD VAR_BASE | ||
0241 | E3 | XTHL | HL = Line length (see 21D) | |
0242 | C1 | POP B | BC = VAR_BASE | |
0243 | 09 | DAD B | HL = VAR_BASE + line length. | |
0244 | E5 | PUSH H | ||
0245 | CDA701 | CALL CopyMemoryUp | Move remainder of program so there's enough space for the new line. | |
0248 | E1 | POP H | ||
0249 | 226701 | SHLD VAR_BASE | Update VAR_BASE | |
024C | EB | XCHG | HL=Line address, DE=VAR_BASE | |
024D | 74 | MOV M,H | ??? | |
024E | 23 | INX H | Skip over next line ptr (updated below) | |
024F | 23 | INX H | ||
0250 | D1 | POP D | DE = line number (see 21C) | |
0251 | 73 | MOV M,E | Write line number to program line memory. | |
0252 | 23 | INX H | ||
0253 | 72 | MOV M,D | ||
0254 | 23 | INX H | ||
0255 | 111301 | CopyFromBuffer | LXI D,LINE_BUFFER | Copy the line into the program. |
0258 | 1A | LDAX D | ||
0259 | 77 | MOV M,A | ||
025A | 23 | INX H | ||
025B | 13 | INX D | ||
025C | B7 | ORA A | ||
025D | C25802 | JNZ CopyFromBuffer+3 | ||
Now the program line has been inserted/removed, all the pointers from each line to the next need to be updated. | ||||
0260 | CDA202 | UpdateLinkedList | CALL ResetAll | |
0263 | 23 | INX H | ||
0264 | EB | XCHG | ||
0265 | 62 | MOV H,D | ||
0266 | 6B | MOV L,E | ||
0267 | 7E | MOV A,M | If the pointer to the next line is a null | |
0268 | 23 | INX H | word then we've reached the end of the | |
0269 | B6 | ORA M | program, job is done, and we can jump back | |
026A | CAFF01 | JZ GetNonBlankLine | to let the user type in the next line. | |
026D | 23 | INX H | Skip over line number. | |
026E | 23 | INX H | ||
026F | 23 | INX H | ||
0270 | AF | XRA A | ||
0271 | BE | CMP M | ||
0272 | 23 | INX H | ||
0273 | C27102 | JNZ 0271 | ||
0276 | EB | XCHG | ||
0277 | 73 | MOV M,E | ||
0278 | 23 | INX H | ||
0279 | 72 | MOV M,D | ||
027A | C36502 | JMP 0265 |
Given a line number in DE, this function returns the address of that progam line in BC. If the line doesn't exist, then BC points to the next line's address, ie where the line could be inserted. Carry flag is set if the line exists, otherwise carry reset.
027D | 2A6501 | FindProgramLine | LHLD PROGRAM_BASE | |
0280 | 44 | MOV B,H | BC=this line | |
0281 | 4D | MOV C,L | ||
0282 | 7E | MOV A,M | If we've found two consecutive | |
0283 | 23 | INX H | null bytes, then we've reached the end | |
0284 | B6 | ORA M | of the program and so return. | |
0285 | 2B | DCX H | ||
0286 | C8 | RZ | ||
0287 | C5 | PUSH B | Push this line address | |
0288 | F7 | RST PushNextWord | Push (next line address) | |
0289 | F7 | RST PushNextWord | Push (this line number) | |
028A | E1 | POP H | HL = this line number | |
028B | E7 | RST CompareHLDE | Compare line numbers | |
028C | E1 | POP H | HL = next line address | |
028D | C1 | POP B | BC = this line address | |
028E | 3F | CMC | ||
028F | C8 | RZ | Return carry set if line numbers match. | |
0290 | 3F | CMC | ||
0291 | D0 | RNC | Return if we've reached a line number greater than the one required. | |
0292 | C38002 | JMP FindProgramLine+3 |
Keyword NEW. Writes the null line number to the bottom of program storage (ie an empty program), updates pointer to variables storage, and falls into RUN which just happens to do the rest of the work NEW needs to do.
No arguments allowed for the NEW keyword. | ||||
0295 | C0 | New | RNZ | |
Write the two null bytes program terminator to the start of program storage. | ||||
0296 | 2A6501 | LHLD PROGRAM_BASE | ||
0299 | AF | XRA A | ||
029A | 77 | MOV M,A | ||
029B | 23 | INX H | ||
029C | 77 | MOV M,A | ||
029D | 23 | INX H | ||
And set the base of variable storage to immediately follow the null program. | ||||
029E | 226701 | SHLD VAR_BASE |
Runs the program. We don't actually need to do anything here, except check that no arguments have been supplied! We can just fall into ResetAll which sets everything up ready to run the program, and we then return to ExecNext.
No arguments allowed for the RUN keyword. | ||||
02A1 | C0 | Run | RNZ |
Resets everything.
Set PROG_PTR_TEMP to just before the start of the program. | ||||
02A2 | 2A6501 | ResetAll | LHLD PROGRAM_BASE | |
02A5 | 2B | DCX H | ||
02A6 | 225D01 | SHLD PROG_PTR_TEMP | ||
Reset the data pointer | ||||
02A9 | CD6904 | CALL Restore | ||
Reset variable pointers | ||||
02AC | 2A6701 | LHLD VAR_BASE | ||
02AF | 226901 | SHLD VAR_ARRAY_BASE | ||
02B2 | 226B01 | SHLD VAR_TOP | ||
Get return address in BC and reset the stack pointer to it's top. | ||||
02B5 | C1 | ResetStack | POP B | |
02B6 | 2A6301 | LHLD STACK_TOP | ||
02B9 | F9 | SPHL | ||
Push address of stack top module 256. Fixme - why??? | ||||
02BA | AF | XRA A | ||
02BB | 6F | MOV L,A | ||
02BC | E5 | PUSH H | ||
Put return address back on stack, set HL to ??? and return. | ||||
02BD | C5 | PUSH B | ||
02BE | 2A5D01 | LHLD PROG_PTR_TEMP | ||
02C1 | C9 | RET |
Gets a line of input at a '? ' prompt.
02C2 | 3E3F | InputLineWith'?' | MVI A,'?' | Print '?' |
02C4 | DF | RST OutChar | ||
02C5 | 3E20 | MVI A,' ' | Print ' ' | |
02C7 | DF | RST OutChar | ||
02C8 | CD3C03 | CALL InputLine | ||
02CB | 23 | INX H |
Tokenises LINE_BUFFER, replacing keywords with their IDs. On exit, C holds the length of the tokenised line plus a few bytes to make it a complete program line.
02CC | 0E05 | Tokenize | MVI C,05 | Initialise line length to 5. |
02CE | 111301 | LXI D,LINE_BUFFER | ie, output ptr is same as input ptr at start. | |
If char is a space, jump ahead to write it out. | ||||
02D1 | 7E | MOV A,M | ||
02D2 | FE20 | CPI ' ' | ||
02D4 | CA0203 | JZ WriteChar | ||
If char is a " (indicating a string literal) then freely copy up to the closing ". Obviously we don't want to tokenize string literals. | ||||
02D7 | 47 | MOV B,A | ||
02D8 | FE22 | CPI '\"' | ||
02DA | CA1503 | JZ FreeCopy | ||
If char is null then we've reached the end of input, and can exit this function. | ||||
02DD | B7 | ORA A | ||
02DE | CA2903 | JZ Exit | ||
Here's where we start to see if we've got a keyword. | ||||
02E1 | D5 | PUSH D | Preserve output ptr. | |
02E2 | 0600 | MVI B,00 | Initialise Keyword ID to 0. | |
02E4 | 115600 | LXI D,KEYWORDS-1 | ||
02E7 | E5 | PUSH H | Preserve input ptr. | |
02E8 | 3E.. | MVI A,.. | LXI over get-next-char | |
fixme. | ||||
02E9 | D7 | KwCompare | RST SyntaxCheck0 | Get next input char |
02EA | 13 | INX D | ||
02EB | 1A | LDAX D | Get keyword char to compare with. | |
02EC | E67F | ANI 7F | Ignore bit 7 of keyword char. | |
02EE | CAFF02 | JZ NotAKeyword | If keyword char==0, then end of keywords reached. | |
02F1 | BE | CMP M | Keyword char matches input char? | |
02F2 | C21C03 | JNZ NextKeyword | If not, jump to get next keyword. | |
OK, so input char == keyword char. Now we test bit 7 of the keyword char : if it's 0 then we haven't yet reached the end of the keyword and so have to loop back to continue comparing. | ||||
02F5 | 1A | LDAX D | ||
02F6 | B7 | ORA A | ||
02F7 | F2E902 | JP KwCompare | ||
Matched a keyword! First thing we do is remove input ptr from the stack, as since we're matched to a keyword we don't need to go back and try to match another keyword - HL is already the correct input ptr. Then we set A to the keyword ID which gets written out in the next block but one (notice we LXI over the next block). | ||||
02FA | F1 | POP PSW | Remove input ptr from stack. We don't need it. | |
02FB | 78 | MOV A,B | A=Keyword ID | |
02FC | F680 | ORI 80 | Set bit 7 (indicates a keyword) | |
02FE | F2.... | JP .... | LXI trick again. | |
Here we have found that the input does not lead with a keyword, so we restore the input ptr and write out the literal character. | ||||
02FF | E1 | NotAKeyword | POP H | Restore input ptr |
0300 | 7E | MOV A,M | and get input char | |
Write character, and advance buffer pointers. | ||||
0301 | D1 | POP D | Restore output ptr | |
0302 | 23 | WriteChar | INX H | Advance input ptr |
0303 | 12 | STAX D | Store output char | |
0304 | 13 | INX D | Advance output ptr | |
0305 | 0C | INR C | C++ (arf!). | |
If we've just written the ID of keyword REM then we need to freecopy the rest of the line. Here we test for REM (8E) and jump back to the outer loop if it isn't. Note that if it is REM, then we set B to 0 so the freecopy won't stop prematurely. | ||||
0306 | D68E | SUI 8E | If it's not the | |
0308 | C2D102 | JNZ Tokenize+5 | ||
030B | 47 | MOV B,A | B=0 | |
Free copy loop. This loop copies from input to output without tokenizing, as needs to be done for string literals and comment lines. The B register holds the terminating character - when this char is reached the free copy is complete and it jumps back | ||||
030C | 7E | FreeCopyLoop | MOV A,M | A=Input char |
030D | B7 | ORA A | If char is null then exit | |
030E | CA2903 | JZ Exit | ||
0311 | B8 | CMP B | If input char is term char then | |
0312 | CA0203 | JZ WriteChar | we're done free copying. | |
0315 | 23 | FreeCopy | INX H | |
0316 | 12 | STAX D | ||
0317 | 0C | INR C | ||
0318 | 13 | INX D | ||
0319 | C30C03 | JMP FreeCopyLoop | ||
NextKeyword. Advances keyword ptr in DE to point to the next keyword in the table, then jumps back to KwCompare to see if it matches. Note we also increment the keyword ID. | ||||
031C | E1 | NextKeyword | POP H | Restore input ptr |
031D | E5 | PUSH H | ||
031E | 04 | INR B | Keyword ID ++; | |
031F | EB | XCHG | HL=keyword table ptr | |
0320 | B6 | NextKwLoop | ORA M | Loop until |
0321 | 23 | INX H | bit 7 of previous | |
0322 | F22003 | JP NextKwLoop | keyword char is set. | |
0325 | EB | XCHG | DE=keyword ptr, HL=input ptr | |
0326 | C3EB02 | JMP KwCompare+2 | ||
Exit. Restore LINE_BUFFER to HL, null-terminated the tokenized line buffer (three times in fact - why?) and return. | ||||
0329 | 211201 | Exit | LXI H,LINE_BUFFER | |
032C | 12 | STAX D | ||
032D | 13 | INX D | ||
032E | 12 | STAX D | ||
032F | 13 | INX D | ||
0330 | 12 | STAX D | ||
0331 | C9 | RET |
Gets a line of input into LINE_BUFFER.
0332 | 05 | Backspace | DCR B | Char count--; |
0333 | 2B | DCX H | Input ptr--; | |
0334 | DF | RST OutChar | Print backspace char. | |
0335 | C24103 | JNZ InputNext | ||
0338 | DF | ResetInput | RST OutChar | |
0339 | CD8A05 | CALL NewLine | ||
033C | 211301 | InputLine | LXI H,LINE_BUFFER | |
033F | 0601 | MVI B,01 | ||
Get a character and jump out of here if user has pressed 'Enter'. | ||||
0341 | CD8203 | InputNext | CALL InputChar | |
0344 | FE0D | CPI '\r' | ||
0346 | CA8505 | JZ TerminateInput | ||
If user has not given a printable character, then loop back until they do. | ||||
0349 | FE20 | CPI ' ' | If < ' ' | |
034B | DA4103 | JC InputNext | or | |
034E | FE7D | CPI 7D | > '}' | |
0350 | D24103 | JNC InputNext | then loop back. | |
Deal with line-abort. The character for this key was '@'. | ||||
0353 | FE40 | CPI '@' | ||
0355 | CA3803 | JZ ResetInput | ||
Deal with backspace. The character for this key was '_'. | ||||
0358 | FE5F | CPI '_' | ||
035A | CA3203 | JZ Backspace | ||
A normal character has been pressed. Here we store it in LINE_BUFFER, only we don't if the terminal width has been exceeded. If the terminal width is exceeded then we ring the bell (ie print ASCII code 7) and ignore the char. Finally we loop back for the next input character. | ||||
035D | 4F | MOV C,A | ||
035E | 78 | MOV A,B | ||
035F | FE48 | CPI 48 | ||
0361 | 3E07 | MVI A,07 | ||
0363 | D26A03 | JNC 036A | ||
0366 | 79 | MOV A,C | Write char to LINE_BUFFER. | |
0367 | 71 | MOV M,C | ||
0368 | 23 | INX H | ||
0369 | 04 | INR B | ||
036A | DF | RST OutChar | ||
036B | C34103 | JMP InputNext |