diff --git a/LST2.bas b/LST2.bas new file mode 100644 index 0000000..da74af5 --- /dev/null +++ b/LST2.bas @@ -0,0 +1,561 @@ +10REM This program provides a formatted listing of a BASIC program, +11REM in particular, indenting loops and splitting multiple statements +12REM out onto separate lines. +13REM Since it calls into the BASIC ROM, it requires BASIC 2. +14 +20REM It lives at &08C0-0AFF, which is normally used for the following: +21REM 800-8FF: sound workspace & buffers, printer buffer, envelope storage +22REM 900-9FF: envelope storage, RS423/speech/cassette output buffers +23REM A00-AFF: cassette/RS423 input buffers +24 +30REM Run this program to assemble it, and save the generated machine code. +31 +40REM Enter LIST commands as normal, but with a trailing period e.g. +41REM LIST 100,200. +42REM LIST ,200. +43REM LIST 100,. +44REM LIST. +45 +50REM NOTE: Page references in the source code are for "The BBC Micro +51REM Compendium", by Jeremy Ruston. +52 +100REM === VARIABLES ======================================================= +101 +102REM We borrow some zero-page locations normally used by BASIC (p170). +103 +110REM When when we're parsing the input command, this points into +111REM the keyboard buffer (&0700-07FF), then we switch to the program code +112REM when we start printing the listing. +113pinput = &4B : REM and &4C +114 +120REM This points into the program code being listed (only while +121REM we're parsing the input command). +122pcode = &3B : REM and &3C +123 +130REM This holds the current line# being listed. +131curr_lineno = &2A : REM and &2B +132 +140REM This holds the last line# to be listed. +141last_lineno = &2E : REM and &2F +142 +150REM This holds the current indent. +151REM The first 5 columns are used for the line#, followed by a space, +152REM then the code itself starts at col.6 +153curr_indent = &30 +154 +160REM This flags if we are in a REM statement. +161in_rem = &31 +162 +170REM This flags if we are in a double-quoted string. +171in_quotes = &32 +172 +180REM This flags if we are in BASIC code (0 means we are in assembly code). +181in_basic = &33 +182 +190REM This flags if we are in an assembly label. +191in_asm_label = &34 +192 +200REM This flags if the current line has not been indented yet. +201indent_pending = &35 +202 +900osbyte = &FFF4 : oswrch = &FFEE : osnewl = &FFE7 +910 +1000FOR opt = 0 TO 3 STEP 3 +1010BASE = &08C0 : P% = BASE +1020[ OPT opt +1030 +1100\ === MAIN ENTRY POINT ==================================================== +1101 +1102\ Adding a trailing "." to a LIST command will generate a syntax error, +1103\ so we replace the default BRK error handler with ours, so that we can +1104\ check for this. +1105 +1110\ install our custom BRK handler +1120LDA #(brkHandler MOD 256) +1130STA &0202 +1140LDA #(brkHandler DIV 256) +1150STA &0203 +1160 +1170\ beep, then exit +1180LDA #7 +1190JMP oswrch +1200 +1500\ === CUSTOM ERROR HANDLER ================================================ +1501 +1502\ We check for our special LIST command here. +1503 +1510.brkHandler +1520\ check for a "Bad program" +1530JSR &BE6F +1540 +1550\ if the error was not "Syntax error" (error code 16), then jump to +1551\ the normal BRK handler +1560LDY #0 +1570LDA (&FD),Y +1580CMP #16 +1590BEQ P%+5 +1600.defaultBRK +1610JMP &B402 +1620 +2000\ initialize our variables +2001\ PINPUT <- &0700 +2002\ PCODE <- PAGE +2003\ CURR_INDENT <- 6 +2004\ IN_BASIC <- &FF +2005\ LAST_LINENO <- &7FFF (in case a line# isn't explicitly set) +2010STY pinput +2020STY pcode +2030LDA &18 +2040STA pcode+1 +2050LDX #7 +2060STX pinput+1 +2070DEX +2080STX curr_indent +2090DEY +2100STY in_basic +2110STY last_lineno +2120LDY #&7F +2130STY last_lineno+1 +2140 +2150\ if the next non-space byte in the keyboard buffer is not the LIST keyword, +2151\ then jump to the normal BRK handler +2160JSR skipSpaces +2170CMP #&C9 +2180BNE defaultBRK +2190 +2200\ if the next non-space byte is a ".", then skip parsing line numbers (the user +2201\ wants to "LIST." the entire program) +2210JSR skipSpaces +2220CMP #&2C +2230BEQ parseEndLineNo +2240 +2250\ if the byte is &8D (pseudo-keyword for a line#), then decode the line# +2251\ into CURR_LINENO +2260CMP #&8D +2270BNE endParseInput +2280JSR decodeLineNo +2290 +2300\ find the start of the specified line in the program code +2301\ NOTE - Program lines are stored as follows +2302\ &0D +2303\ line# (MSB, LSB) +2304\ line length (1 byte, the count includes the 3 leading bytes) +2305\ The end of the program is stored as +2306\ &0D &FF +2310.checkNextLine +2320\ check if we've found the first line to be listed (i.e. the line# of +2321\ the line pointed to by PCODE is >= CURR_LINENO) +2330LDY #2 +2340LDA (pcode),Y \ this is the LSB of the current line# +2350SEC +2360SBC curr_lineno +2370DEY +2380LDA (pcode),Y \ this is the MSB of the current line# +2390BPL P%+5 \ negative line# MSB = end-of-program marker +2400JMP done +2410SBC curr_lineno+1 +2420BPL foundFirstLine +2430\ nope - move to the next line +2440LDY #3 +2450LDA (pcode),Y \ this is the line length +2460CLC +2470ADC pcode +2480STA pcode +2490BCC P%+4 +2500INC pcode+1 +2510JMP checkNextLine +2520 +2530.foundFirstLine +2540\ if the next non-space byte in the keyboard buffer is ",", then parse +2541\ the end line# +2550JSR skipSpaces +2560CMP #&2C +2570BEQ parseEndLineNo +2580 +2590\ if the byte is not ".", then jump to the normal BRK handler +2600CMP #&2E +2610BNE defaultBRK +2620 +2630\ the input was of the form "LIST 12345.", so set the last line# +2631\ to be the same as the first line# +2640LDA curr_lineno +2650STA last_lineno +2660LDA curr_lineno+1 +2670STA last_lineno+1 +2680JMP parseEOL +2690 +2700.parseEndLineNo +2710\ if the next non-space byte in the keyboard buffer is &8D (pseudo-keyword +2711\ for a line#), then decode the line# into LAST_LINENO +2712\ NOTE - While decodeLineNo stores the result in CURR_LINENO (thus +2713\ corrupting it), the main loop extracts the line# each time it starts +2714\ a new line of program code, and so will restore it when it starts +2715\ processing the first line. +2720JSR skipSpaces +2730CMP #&8D +2740BNE endParseInput +2750JSR decodeLineNo +2760STA last_lineno+1 +2770LDA curr_lineno +2780STA last_lineno +2790JSR skipSpaces +2800 +2810.endParseInput +2820\ check for the trailing "." +2830CMP #&2E +2840BEQ P%+5 +2850JMP defaultBRK +2860 +2870.parseEOL +2880\ check for the terminating CR +2890JSR skipSpaces +2900CMP #&0D +2910BEQ P%+5 +2920JMP defaultBRK +2930 +2940\ start taking input from the program code i.e. PINPUT <- PCODE +2950LDA pcode +2960STA pinput +2970LDA pcode+1 +2980STA pinput+1 +2990 +5000\ === MAIN LOOP =========================================================== +5001 +5002\ We now start printing the listing, by stepping through each byte of +5003\ the program code, deciding how to print it out, until we go past LAST_LINENO. +5004 +5010.processNextByte +5020\ check if we're at the start of a line in the program code +5030JSR getNextByte +5040CMP #&0D +5050BNE printCode +5060 +5070\ yup - print a newline, save the line# in CURR_LINENO +5080JSR osnewl +5090JSR getNextByte +5100STA curr_lineno+1 +5110JSR getNextByte +5120STA curr_lineno +5130 +5140\ skip over the line length byte +5150JSR getNextByte +5160 +5170\ check if we're done (CURR_LINENO > LAST_LINENO) +5180LDA last_lineno +5190SEC +5200SBC curr_lineno +5210LDA last_lineno+1 +5220SBC curr_lineno+1 +5230BMI done +5240 +5250\ figure out how many digits the current line# has (by subtracting +5251\ each power of 10), then print enough spaces so that the line# +5252\ will be right-adjusted when we print it out +5260LDX #3 +5270.checkPow10 +5280LDA curr_lineno +5290SEC +5300SBC tensLSB,X +5310LDA curr_lineno+1 +5320SBC tensMSB,X +5330BMI P%+5 +5340DEX +5350BPL checkPow10 +5360INX +5370JSR indent +5380 +5390\ print the current line# +5400JSR &991F \ this prints the IAC (&2A/2B) as a 16-bit number +5410 +5420\ print a space +5430LDA #&20 +5440JSR oswrch +5450 +5460.startNextLine +5470\ initialize for the next line of program code, then loop back for the next byte +5471\ IN_REM <- 0 +5472\ IN_QUOTES <- 0 +5473\ IN_ASM_LABEL <- 0 +5474\ INDENT_PENDING <- &FF +5480LDX #0 +5490STX in_rem +5500STX in_quotes +5510STX in_asm_label +5520DEX +5530STX indent_pending +5540BMI processNextByte +5550 +5560.done +5570\ we're all done - warm-start BASIC +5580JSR osnewl +5590JMP &8AF3 +5600 +5610\ LSB/MSB's for 10000, 1000, 100, 10 +5620.tensLSB +5630EQUB &10 : EQUB &E8 : EQUB &64 : EQUB &0A +5640.tensMSB +5650EQUB &27 : EQUB &03 : EQUB &00 : EQUB &00 +5660 +5670\ we now print out the next byte of program code (in A) +5671 +5680.printCode +5690 +5700\ if we have a line# (pseudo-keyword &8D), then decode and print it out +5710CMP #&8D +5720BNE P%+11 +5730JSR decodeLineNo +5740JSR &991F \ this prints the IAC as a 16-bit number +5750JMP processNextByte +5760 +5770\ if we have a double-quote, then toggle the IN_QUOTES flag +5780CMP #&22 +5790BNE P%+10 +5800LDA in_quotes +5810EOR #&FF +5820STA in_quotes +5830LDA #&22 +5840 +5850\ if we are currently in a quoted string or a REM statement, +5851\ then output the current byte verbatim +5860BIT in_rem +5870BMI P%+6 +5880BIT in_quotes +5890BPL P%+5 +5900JMP outputByte +5910 +5920\ if we have a colon, then print a newline, indent, print a colon +5930CMP #&3A +5940BNE P%+18 +5950JSR osnewl +5960LDX #5 +5970JSR indent +5980LDA #&3A +5990JSR oswrch +6000JMP startNextLine +6010 +6020\ if we are currently in assembly code, then jump to handle that +6030BIT in_basic +6040BPL checkAssembly +6050 +6060\ if we have a REM token, then update the flag +6070CMP #&F4 +6080BNE P%+6 +6090LDX #&FF +6100STX in_rem +6110 +6120\ check if we have a NEXT or UNTIL token +6130CMP #&ED +6140BEQ P%+6 +6150CMP #&FD +6160BNE postNextUntil +6170\ yup - decrease the current level of indentation +6180JSR dedent +6190\ if we have a NEXT token, and the next program byte is a ",", then dedent again +6191\ NOTE - We peek ahead at the program bytes, and don't consume them. +6200CMP #&FD +6210BEQ postNextUntil +6220LDY #0 +6230.checkForComma +6240LDA (pinput),Y +6250INY +6260CMP #&2C +6270BNE P%+5 +6280JSR dedent +6290\ if we don't have a colon or the start of the next program line, +6291\ then loop back to check for another "," +6300CMP #&3A +6310BEQ P%+6 +6320CMP #&0D +6330BNE checkForComma +6340LDA #&ED \ restore the NEXT token +6350 +6360.postNextUntil +6370\ if the byte is "[" (start assembly code), then indent to col.24, flag that +6371\ we are not in BASIC code +6380CMP #&5B +6390BNE P%+11 +6400LDX #24 +6410JSR indent +6420LDX #0 +6430STX in_basic +6440 +6450\ if we need to indent, then make it so +6460BIT indent_pending +6470BPL P%+7 +6480LDX curr_indent +6490JSR indent +6500 +6510\ if we have a FOR or REPEAT token, then increase the indent +6520CMP #&E3 +6530BEQ P%+6 +6540CMP #&F5 +6550BNE outputByte +6560INC curr_indent +6570INC curr_indent +6580BNE outputByte +6590 +6600.checkAssembly +6610 +6620\ if we haven't indented yet, and we have a ".", then flag that we are +6621\ in an assembly label +6630BIT indent_pending +6640BPL P%+10 +6650CMP #&2E +6660BNE P%+6 +6670LDX #&FF +6680STX in_asm_label +6690 +6700\ if we haven't indented yet, and are in a label, and we've found +6701\ the end of the label, then update the "in label" flag +6710BIT indent_pending +6720BPL postAsmPrefix +6730BIT in_asm_label +6740BPL P%+10 +6750CMP #&20 +6760BNE postAsmPrefix +6770LDX #0 +6780STX in_asm_label +6790 +6800\ if we have something other than a space, then indent to col.24 +6810CMP #&20 +6820BEQ postAsmPrefix +6830LDX #24 +6840JSR indent +6850 +6860.postAsmPrefix +6870\ if we have a "\" (start of comment), then indent to col.41 +6880CMP #&5C +6890BNE P%+7 +6900LDX #41 +6910JSR indent +6920 +6930\ if we have a "]" (end of assembly code), then flag that we are in BASIC code +6940CMP #&5D +6950BNE outputByte +6960LDX #&FF +6970STX in_basic +6980 +6990.outputByte +7000JSR &B50E \ this prints the character or token in A +7010JMP processNextByte +7020 +8000\ === SUPPORT ROUTINES ==================================================== +8010 +8020\ get the next byte from PINPUT (keyboard buffer or program code) +8030 +8040.getNextByte +8050LDY #0 +8060LDA (pinput),Y +8070INC pinput +8080BNE P%+4 +8090INC pinput+1 +8100 +8110\ check for Escape +8120BIT &FF +8130BPL P%+14 +8140BRK +8150EQUB &17 +8160EQUB &0A : EQUB &0D : EQUS "Escape." : EQUB 0 +8170 +8180RTS +8190 +8200\ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +8201 +8202\ print spaces until the cursor is at the column specified in X +8210 +8220.indent +8230PHA +8240\ NOTE - PCODE is only used when parsing the keyboard input, +8241\ and we re-use that byte here. +8250STX pcode +8260 +8270\ get the current text cursor position +8280LDA #&86 +8290JSR osbyte +8300 +8310\ subtract the requested column position to get the number of spaces +8311\ we need to print (this won't work properly if the indent is so large, +8312\ it wraps). +8320TXA +8330SEC +8340SBC pcode +8350BPL indentDone +8360 +8370\ print the spaces +8380TAX +8390LDA #&20 +8400JSR oswrch +8410INX +8420BNE P%-4 +8430 +8440.indentDone +8450\ flag that the current line has been indented +8460LDA #0 +8470STA indent_pending +8480 +8490PLA +8500RTS +8510 +8520\ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +8521 +8522\ get the next non-space byte from PINPUT (keyboard buffer or program code) +8530 +8540.skipSpaces +8550JSR getNextByte +8560CMP #&20 +8570BEQ skipSpaces +8580 +8590RTS +8600 +8610\ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +8611 +8612\ decode a line number in the 3 bytes at PINPUT, and store it in CURR_LINENO. +8613\ Adapted from the BBC BASIC ROM &97EB (p334). See p174 for details +8614\ on how these are encoded. +8620 +8630.decodeLineNo +8640JSR getNextByte +8650ASL A +8660ASL A +8670TAX +8680JSR getNextByte +8690STA curr_lineno +8700JSR getNextByte +8710STA curr_lineno+1 +8720TXA +8730AND #&C0 +8740EOR curr_lineno +8750STA curr_lineno +8760TXA +8770ASL A +8780ASL A +8790EOR curr_lineno+1 +8800STA curr_lineno+1 +8810 +8820RTS +8830 +8840\ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +8841 +8842\ decrease the level of indentation (to a minimum of 6) +8850 +8860.dedent +8870DEC curr_indent +8880DEC curr_indent +8890 +8900LDX #6 +8910CPX curr_indent +8920BCC P%+4 +8930STX curr_indent +8940 +8950RTS ` +8960 +10000] : NEXT opt +10010PRINT +10020PRINT "To save the generated machine code:" +10030PRINT " *SAVE lst2 " + STR$~(BASE) + " " + STR$~(P%) +10040PRINT +10050PRINT "To activate it now:" +10060PRINT " CALL &" + STR$~(BASE) +10070PRINT "Or to reload it at a later time:" +10080PRINT " *lst2" + diff --git a/LST2.ssd b/LST2.ssd new file mode 100644 index 0000000..7a5dd14 Binary files /dev/null and b/LST2.ssd differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..056b68a --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +This program provides a formatted listing of a BBC BASIC program, by splitting multiple statements out onto separate lines, indenting loops, etc. Assembly language also gets nicely formatted. + +I was doing a lot of work on the BBC Micro in the late 80's, and recently managed to retrieve a lot of it from floppy discs. The discs were in relatively good condition, given that they were nearly 40 years old (!), but it was still a challenge reading them (they were shedding so much oxide, we had to clean the disk heads after each use :-)). + +This program was incredibly useful, but we were only able to get a binary executable off the floppies, not the source code, so I disassembled it, annotated it, and have made it available here. + +Working on the BBC Micro was not easy, since you only had, at best, 24¾KB of memory (!) (5¾KB if you were using hi-res graphics), so saving every byte of memory possible was really important. Since BASIC is an interpreted language, and each line had a 3-byte overhead, you could save quite a few bytes by cramming as many statements onto each line as possible, separated by a colon, thus saving 2 bytes for each statement. + +Of course, this made the code very difficult to read, so I wrote this listing formatter, which converts something like this: +``` + 807DEFPROCsp LOCALT%,I%,P%,S$:PROCblip(100):REPEATUNTILNOTINKEY-99:REPEAT:PRINTTAB(0,24)SPC10f$B$bk$Y$"Press SPACE "bb$SPC10;:PROCfx21:T%=INKEY500:IFT%=esc T%=FNesc(T%):GOTO810:ELSEIFT%=32ORT%<>-1ORRND(500)>1GOTO810 + 809P%=msgs+1:FORI%=1TORND(?msgs):S$=$P%:P%=P%+LENS$+1:NEXT:prvb=prvb+1:PRINTTAB(0,24)CHR$(128+RND(7))SPC38;:I%=1:REPEAT:PRINTTAB(1,24)MID$(STRING$(39," ")+"Ancient proverb #"+STR$prvb+": "+S$+STRING$(50," "),I%,38);:I%=I%+1:UNTILI%=70+LENS$ORNOTINKEY20 + 810UNTILT%=32:PRINTTAB(0,24)SPC39;:ENDPROC +``` +into this: +``` + 807 DEFPROCsp LOCALT%,I%,P%,S$ + :PROCblip(100) + :REPEATUNTILNOTINKEY-99 + :REPEAT + : PRINTTAB(0,24)SPC10f$B$bk$Y$"Press SPACE "bb$SPC10; + : PROCfx21 + : T%=INKEY500 + : IFT%=esc T%=FNesc(T%) + : GOTO810 + : ELSEIFT%=32ORT%<>-1ORRND(500)>1GOTO810 + 809 P%=msgs+1 + : FORI%=1TORND(?msgs) + : S$=$P% + : P%=P%+LENS$+1 + : NEXT + : prvb=prvb+1 + : PRINTTAB(0,24)CHR$(128+RND(7))SPC38; + : I%=1 + : REPEAT + : PRINTTAB(1,24)MID$(STRING$(39," ")+"Ancient proverb #"+STR$prvb+": "+S$+STRING$(50," "),I%,38); + : I%=I%+1 + : UNTILI%=70+LENS$ORNOTINKEY20 + 810 UNTILT%=32 + :PRINTTAB(0,24)SPC39; + :ENDPROC +``` + +To get the fully authentic Beeb experience, you can type the program in from the listing :-), but as a convenience, a `.ssd` file is provided with the source code, together with a binary executable. + +Load the program into memory like this: +``` + *LST2 +``` + +And then enter LIST commands as normal, but with a trailing period e.g. +``` + LIST 100,200. + LIST ,200. + LIST 100,. + LIST. +```