Adding Error Traps,
RAM-Based Assembly, and
Special Notes on the Construction of Atari and Apple LADS
Imagine how nice it would be if you could add any additional commands to BASIC that you desired. You wouldn't just temporarily wedge the new commands into a frozen ROM BASIC. Instead, you would simply define the new commands, and they would then become a permanent part of your programming language.
This freedom to change a language is called extensibility. It's one of the best features of Forth and a few other languages. Extensibility opens up a language. It gives the programmer easy access to all aspects of his programming tool. LADS, too, is extensible since the internals of the assembler are thoroughly mapped, documented, and explained in this book. You can customize it at will, building in any features that you would find useful.
After exploring the details of the LADS assembler and using LADS to write your own machine language, you may have thought of some features or pseudo-ops that you would like to add. In this chapter, we'll show how to make several different kinds of modifications. These examples, even if they're not features of use to you, will demonstrate how to extend and customize the language. We'll add some new error traps, create a disassembler, and make a fundamental change to the Commodore and Apple LADS-the capability of assembling directly from RAM. (The Atari version has this feature built-in already.)
At the end of this chapter we'll cover the details of the Atari and Apple LADS source code where they differ from the general LADS source listings (printed at the end of each chapter). The three versions-Commodore, Atari, and Apple-are functionally identical, so the descriptions throughout the book apply to each version. However, a few adjustments had to be made: input/output variations, a special source code editor for the Atari, etc. All these will be discussed below. But first, let's see some examples of how to customize LADS.
A Naked Mnemonic Error Trap
The original version of LADS notifies you of most serious errors: branch out of range, duplicated or undefined labels, naked labels (labels without arguments), invalid pseudo-ops, no starting address, file not found on disk, and various syntax errors. Other kinds of errors are forgiven by LADS since it can interpret what you meant to type in your source code. For example, LADS can interpret what you meant when you type errors like these:
100 INY #77; (adding an argument to a one-byte opcode)
100 INY : LDA #15:INY:INX,(extra spaces before or after colons)
The source code in these examples will be correctly assembled. Also, if you forget to leave a space between a mnemonic and its argument (like: LDA# 15), that sort of error will be trapped and announced.
But the original LADS didn't have a built-in trap for naked mnemonics. If you wrote:
100 INC:INY:LDA #15 ; (that "INC" requires an argument)
the assembler would have crashed. No error message, no warning, just a crash.
Programmers who tested the early versions of LADS asked that this error be trapped. That is, if this mistake was made during the typing of an ML program's source code, it shouldn't cause the assembler to go insane. The following two error-trap modifications have been made a permanent part of LADS (and are already in the object code version you typed in from this book or received on disk).
To expose naked mnemonic errors, a special trap was inserted into the Eval subprogram (see Listing 11.1)
After Eval has determined (line 930 of Program 3-1) that the mnemonic under evaluation does require an argument (it's not like INY, which uses Implied addressing and never has an argument), Eval then goes down to check to see if the argument is a label or a number (1460).
Here's where we can check to see if the programmer forgot to give an argument. If the mnemonic is followed by a colon or a 0 (end of logical line), that's a sure signal that the argument has been left out. We can load in the character just after the mnemonic (see line 1474, Listing 11.1). If there is a space character (#32), all is well and we can continue (1480) with our assembly. If not, we jump to L700, the error-reporting routine which will print the error and ring the bell.
A Trap for Impossible Instructions
Another programmer who tested LADS was just starting to learn machine language. Unfamiliar with some of the mnemonics and addressing modes, he once tried to assemble a line like this:
100 LDA 15,Y
not knowing that Zero Page,Y addressing is a rare addressing mode, exclusively reserved for only two mnemonics: LDX and STX. But LADS didn't crash on this. Instead, it assembled an LDA 15,X (the correct addressing mode, but fatal to his particular program since he was trying to use the Y Register as an index).
The trap was inserted into LADS (Listing 11.2) to make a harmless substitution, to assemble an Absolute,Y (at a zero page address). Thus, the programmer's intent is preserved, but the illegal addressing mode is replaced.
By the time Eval reaches this point, it has already filtered out many other possible addressing modes. Eval knows that the addressing mode is some form of X or Y and that it's Zero Page. Eval first checks to see if we are dealing with an attempted ,Y addressing mode (CMP #89, the Y character). If not, we continue with the assembly (5271) by a BNE to line 5274.
But if it is a Y, we check the opcode to see if it is LDX, the only correct opcode for this addressing mode. If so, we continue.
However, if it is some other mnemonic like LDA or STY, this Y addressing mode is illegal and we make the adjustment to Absolute,Y by a JMP to the area of Eval where that addressing mode is accomplished.
Most illegal addressing will be reported by LADS. Nevertheless, if there's a peculiar error that you often make when programming and LADS doesn't alert you, just add an errorreporting trap or have the assembler automatically correct the problem.
A final minor modification to the PDISK routine in the Pseudo subprogram will permit embedded keywords in filenames when using the D pseudo-op to save object code to disk. (The Atari version will not need this modification.) As printed in this book, LADS will correctly extend and print a filename following the D pseudo-op which contains a keyword. For example, D OLDSTOP will look correct onscreen. However, LADS will send the tokenized keyword to the disk as the filename. This will result in unpredictable filenames when you use BASIC commands as part of a filename. To correct this, remove line 1190 of Program 8-1 and adjust the following lines in the Pseudo subprogram. Then reassemble a new version of LADS:
1230 PD1 LDY #0
1231 PDLO LDA LABEL,Y:BEQ PDEN:STA FILEN,Y:INY:JMP PDLO; MOVE NAME
1239 PDEN LDA #44; PUT P,W (PROGRAM, WRITE) SIGNAL S ONTO FILENAME
1472 ;------------------- SEE CHAPTER 11 FOR DESCRIPTION OF THIS ERROR TRAP
1473 ; (TRAP FOR NAKED MNEMONICS ERROR)
1474 LDA LABEL+3:CMP #32:BEQ GVEG:JMP L700; (TEST FOR "INC:" TYPE ERROR)
5270 ; ---------- SEE CHAPTER 11 FOR EXPLANATION OF THIS ERROR TRAP ------
5271 L760 LDA BUFFER+2,Y:CMP #89:BNE ML760; --- ERROR TRAP FOR LDA (15,Y)
5272 LDA OP:CMP #182:BEQ ML760; IS THE MNEMONIC LDX (IF SO, MODE IS CORRECT)
5273 JMP L680; IF NOT, JUMP TO MAKE IT (LDA $0015,Y) ABSOLUTE Y
5274 ML760 JMP TWOS
A Remarkably Simple, Yet Radical, Change
Since LADS uses symbols instead of numbers, it's fairly easy to change, to make it what you want it to be. What's more, all the programs you write with LADS will also be symbolic and easily changed. Let's make a radical change to LADS and see how easy it is to profoundly alter the nature of the assembler.
As designed, LADS reads source code off a disk program file. Let's make it read its source code from within the computer's RAM memory, instead of from disk. This makes two things possible: 1. You can change source code, then test it by a simple SYS to LADS. 2. Tape drive users can use LADS. This version of LADS isn't functionally different from the normal version since long, linked assembly will still be coming from disk files. However, it can be a more convenient way to write and debug smaller ML programs or subroutines. Everything works the same when you assemble, except that the first (or only) source code program resides in RAM instead of on disk. Commodore and Atari RAM-LADS versions can use linked files, but the Apple RAM-based version cannot link files as it can in the normal Apple LADS.
You make a radical change whenever you change *= 864 to * = 5000. You are making a small change at the beginning, the root, of your source code. After making this change, the entire program is assembled at address 5000 instead of address 864. The effect-in the usual sense of the term-is quite radical. The effort on your part, however, is rather minor. Likewise, we can drastically alter the way that LADS works by making a few minor changes to the symbols in LADS.
Our goal is to make LADS read source code from memory instead of from disk files. First, we need to add two new pointers to the LADS zero page equates (in the Defs file). We create PMEM. It will serve as a dynamic pointer. It will always keep track of our current position in memory as we assemble source code.
The intelligence in the disk drive keeps track of where we are in a file; whenever we call CHARIN, it increments a pointer so that the next CHARIN call will pull a new byte into A, the Accumulator. But we're going to be reading from memory so we'll need to update our own dynamic pointer. To create this pointer, just type in a new line in Defs: PMEM = $xx (whatever zero page, two-byte space is safe in your computer).
The other new pointer we need to define in zero page will tell LADS where your BASIC RAM memory starts, where a program in BASIC starts. To create this register, just look at a map of the zero page of your particular computer and define: RAMSTART = $xx (whatever it is).
Note: These definitions have already been added to the Commodore versions of the Defs subprogram in this book. If you are creating a RAM-based version of LADS for the Apple, add the following two lines to the Apple Defs file:
135 RAMSTART = $67, POINTER TO START OF RAM MEMORY
157 PMEM = $E2
The Apple version of the RAM-based LADS requires the same changes to the Eval subprogram as Commodore machines require. However, no changes are needed in the Pseudo or Openl subprograms. The one difference between Commodore and Apple versions in the Getsa subprogram is that Apple requires #$2A in line 300 instead of the #172.
A New CHARIN
Next, we need to change the CHARIN subroutine itself. As LADS normally runs, it goes to BASIC's get-a-byte subroutine whenever CHARIN is invoked. This won't work for memorybased source code. BASIC RAM cannot, alas, be OPENed as if it were a file. So, since LADS is peppered with references to CHARIN, we can just undefine CHARIN in the Defs subprogram by putting a semicolon in front of it (Listing 11.3).
Similarly, CHKIN is scattered throughout LADS to reopen file #1, the read-code-from-disk file. We're not using file #1 in this version of LADS, so we add a semicolon to its definition too (Listing 11.4).
But throughout LADS there are references to these two subroutines. We need to write a new CHARIN and CHKIN to replace the ones we just obliterated. LADS will then have somewhere to go, something to do, as it comes upon CHARRNs or CHKKNs throughout the code. We do this by adding to the Getsa subprogram (Listing 11.5).
260 ;CHARIN = $FFE4; PULLS IN ONE BYTE
240 ;CHKIN = $FFC6; OPENS A CHANNEL FOR READ (FILE# IN X)
340 ; --------------------------
350 ; "NEW CHARIN" ASSEMBLE SOURCECODE FROM MEMORY RATHER THAN DISK.
360 ; (IMITATES CHARIN FOR DISK)
370 ; RETURNS WITH NEXT BYTE FROM MEMORY, IN A
390 CHARIN INC PMEM:BNE INCP1:INC PMEM+1; REPLACES CONVENTIONAL CHARIN/DISK
400 INCP1 STY Y:LDY #O:LDA (PMEM),Y:PHP:LDY Y:PLP:RTS; SAVE STATUS REGISTER
410 CHKIN RTS; REPLACES DISK ROUTINE IN DEFS
Line 410 is just an RTS. It's a placebo. We never want to reopen file #1 (CHKIN's normal job), so whenever LADS tries to do that, we JSR/RTS and nothing happens. Something does have to happen with CHARIN, however. CHARIN's job is to fetch the next byte in the source code and give it to the Accumulator. So this new version of CHARIN (390-400) increments PMEM, our new RAM memory pointer, saves Y, loads the byte, saves the Status Register, restores Y, restores the Status Register, and returns. This effectively imitates the actions of the normal disk CHARIN, except it draws upon RAM for source code.
Here you can see one of those rare uses for PHP and PLP. There are times when it's not enough to save the A, Y, and X Registers. This is one of those times. INDISK returns to Eval only when it finds a colon (end of source instruction), a semicolon (end of instruction, start of comment), or a zero (end of BASIC program line, hence end of source instruction). When we get a zero when we LDA, the zero flag will be set. But the LDY instruction will reset the zero flag. So, to preserve the effect of LDA on the zero flag, we PHP to store the flags on the stack. Then, after the LDY, we restore the status of the flags, using PLP before we return to the Indisk file. This way, whatever effect the LDA had on the flags will be intact. Indisk can thus expect to find the zero flag properly set if a particular LDA is pulling in the final 0 which signifies the end of a line in the BASIC RAM source code.
After making these substitutions to LADS, we need to remove the two references to Openl (the routine which opens a disk file for source code reading) in the Eval subprogram. These references are at lines 350 and 4350. We can simply remove them from assembly by putting a semicolon in front of them (Listing 11.6).
Early in Eval, we have a JSR GETSA. This is the GETStart-Address-from-disk routine. We want to change this to: JSR MEMSA. GETSA isn't needed. MEMSA will perform the same job, but for memory-based source code instead of diskbased source code. MEMSA is found in the Getsa subprogram (Listing 11.7).
The first thing that MEMSA does is to put the start-ofBASIC-RAM pointer into PMEM (our dynamic pointer). This positions us to the first byte in the source code. Then it pulls off enough bytes to point to the * in the start address definition in the source code. This is just what Getsa does for a disk file. The rest of MEMSA is identical to Getsa.
Second Generation LADS
That's it. These few substitutions and LADS will read a source file from RAM memory. You can still use D NAME to create a disk object code file. You can still send the object code disassembly to a printer with P. All the other pseudo-ops still work fine. A radical change in ten minutes.
The Getsa subprogram contains a complete, step-by-step description of this disk-to-RAM modification of LADS. After you've made the changes to the source code (and saved them to disk), just load in the normal disk version of LADS, enter Defs as the starting file for assembly, and SYS to LADS. It will grind out a brand new, RAM-based assembler for you.
As always, when making a new version of your LADS assembler, be sure to direct object code to the disk (use the D pseudo-op) so that you won't overwrite the working LADS in the computer. Also be sure you've given the new version a filename that doesn't already exist on the disk.
350 ;JSR OPEN1; OPEN READ FILE (SOURCE CODE FILE ON DISK)
4350 ;JSR OPEN1; OPEN INPUT FILE (POINT IT TO THE 1ST BYTE IN THE FILE)
220 ; "MEMSA" GET STARTING ADDRESS FROM MEMORY. LEAVES DISK POINTING AT-
230 ; *= THIS SPACE (START ADDRESS)
240 ; 11 INITIALIZES PMEM TO START OF MEMORY
250 ; REPLACES "GETSA" SOURCE CODE FILE TO CREATE RAM-BASED ASSEMBLER.
260 ; -------------------------
270 MEMSA LDA RAMSTART:STA PMEM:LDA RAMSTART+1:STA PMEM+1
280 LDX #3:MEM1 JSR CHARIN:DEX:BNE MEM1; ADD 4 TO PMEM TO POINT TO *_
300 JSR CHARIN:CMP #172:BEQ MMSA
310 LDA #<MNOSTART:STA TEMP:LDA #>MNOSTART:STA TEMP+1:JSR PRNTMESS
320 JMP FIN; GO BACK TO EASIC VIA ROUTINE WITHIN EVAL
330 MMSA RTS
In a perfectly symmetrical universe, with a right hand for every left, and a north pole for every south, you could transform an assembler into a disassembler by just making it run backwards.
Unfortunately, ours is not such a universe. Since LADS turns source code into object code, it would seem possible to tinker with it and adjust it a bit and make it turn object code back into source code, to disassemble. Not so. We have to link two new files onto LADS to add a disassembler function: Dis and Dtables.
Personal Programming Style
Dis is an example of how a fairly complex ML program can be constructed using LADS. The relatively few comments reflect my personal style of programming. I find many of the variable names are meaningful enough to make the code understandable, especially since the purpose of the lookup tables in Dtables is fairly easy to see.
The relatively few comments in the compressed code in Dis also allow you to look at more instructions at the same time on the screen. This can help during debugging since you might be able to more quickly locate a fault in the overall logic of a program. Nevertheless, many programmers find such dense code hard to read, hard to debug, and generally inefficient.
Obviously, you should write the kind of source code that works for you. The degree of compression is a matter of programming style and personal preference. Some programming teachers insist on heavy commenting and airy, decompressed coding. Perhaps this emphasis is appropriate for students who are just starting out with computing for the same reasons that penmanship is stressed when students are just starting to learn how to write. But you needn't feel that there is only one programming style. There are many paths, many styles.
How to Use the Disassembler
For convenience, Dis is set to start at 17000. That's an easy number to remember when you want to SYS, CALL, or USR to see a disassembly. The version at the end of this chapter is fully functional, but you might want to make modifications. As printed, it will ask for the start address location in RAM of the object code you want to see listed. Notice that the object code must be residing in RAM to be disassembled. (It would be simple, though, to make a disassembler which operated on disk or tape code.) Then it will disassemble until you hit the STOP or BREAK key. You might want to adjust it- you could have it assemble 20 instructions and then halt until a key was pressed. Or you might want to make it print disassemblies to the printer. Or it could ask for both starting and ending addresses before it begins. To have the disassembler you prefer, just modify the code.
The disassembler is included in this book because it demonstrates compressed LADS source code and it also shows how LADS itself can be expanded while borrowing from existing LADS subroutines like STOPKEY and PRNTNUM.
The source code in other parts of the book is somewhat artificial: Each line contains only one mnemonic followed by a description, a comment about the purpose of that line. Normally, such extensive commentary will not be necessary, and many lines can contain multiple statements separated by colons. Dis is an example of LADS source code as many programmers will probably write it.
To add the disassembler to LADS, change the END REFS at the end of the Tables subprogram in LADS to FILE DIS. This will cause the file for Dis to be assembled along with LADS. Dis will link to Dtables, which ends with.END REFS to permit the second pass through the combined LADS/Dis code.
Let's briefly outline the structure and functions of the disassembler. It starts off by printing its prompt message called DISMESS (30). The actual message is located in line 710. PRNTMESS is a subroutine within LADS which prints any message pointed to by the variable TEMP.
Then $3F, the ? symbol, is printed and STARTDIS (50) sets the hexflag up so that number printouts will be in hexadecimal. If you prefer decimal, LDA #0 and store it in HXFLAG.
Now there's an input loop to let the user input a decimal start address, character by character. If a carriage return is detected (90), we leave the loop to process the number. The number's characters are stored in the LABEL buffer and are also printed to the screen as they are entered (100).
When we finish getting the input, the LADS Valdec routine changes the ASCII numbers into a two-byte integer in the variable RESULT. We pick up the two-byte number and store it in the variable SA which will be printed to the screen as the address of each disassembled mnemonic.
Line 150 is a bit obscure. It wasn't originally written this way, but testing revealed that the JSR GB in line 190 would increment the start address right off the bat (before anything was disassembled or printed). At the same time, putting that increment lower in the main loop was inconvenient. So the easiest thing was to simply accept a start address from the user, then decrement it. The disassembler will start off with a start address that is one lower than the user intends, but that early increment will fix things up. Thus, the variable PMEM will hold a number which is one lower than the variable SA. Both these variables are keeping track of where in memory we are currently disassembling. But we've got to distinguish in this way between SA which prints to the screen and PMEM which tells the computer the current location.
This is a good place to observe that programming is never a smooth trip from the original concept to the final product. No programmer is so well-prepared or knowledgeable that he or she simply sits down and calmly creates a workable program. If you find yourself scratching your head, circling around a bug and not trapping it, spending hours or days trying to see what could possibly be wrong-you're in good company. I've worked with some very experienced, very talented people and have yet to see someone fashion a program without snags. And the more significant and sophisticated the program, the more snags it has.
All that can be done, when you hit a snag, is to singlestep through the offending area of your program, or set BRK traps, or puzzle over the source code, or try making some tentative reassemblies (not knowing for sure if your changes will have any salutary effect), or sometimes even toss out an entire subroutine and start over. For example, I wrote the rough draft, the first draft of this disassembler, in about two hours. I didn't have the final version working until I'd spent two full days battling bugs. Some were easy to fix, some were monsters. It took about ten minutes to cure that problem with the start address being one too high. But it took hours to locate an error in the disassembler tables, Dtables.
After the user has input the start address, TEMP is made to point to the LABEL buffer and VALDEC is invoked. VALDEC leaves the result of an ASCII-to-integer conversion in the RESULT variable. That number is stored in PMEM and SA (140-150). One final adjustment restores SA to the original number input by the user. SA will only print addresses onscreen; PMEM is the real pointer to the current address during disassembly. The decrementing of PMEM, made necessary by that JSR GB early in the main loop, is not necessary for SA. (SA is not incremented by the GB subroutine.)
GETBYTE: The Main Loop
Now we arrive at the main loop. GETBYTE (190) first tests to see if the user wants to stop disassembly via the STOPKEY subroutine (in the Eval subprogram within LADS). Then the GB subroutine (690) raises the memory pointer PMEM and fetches a byte from memory. This byte is saved in the FILEN buffer and will act as an index, a pointer to the various tables in the Dtables subprogram. For purposes of illustration, let's assume that the byte we picked up held the number 1. One is the opcode for ORA (Indirect,X). We can trace through the main loop of Dis and see what happens when Dis picks up a 1.
The 1 is transferred to the Y Register (200), and we then load whatever value is in MTABLE + 1 since we LDA MTABLE,Y and Y holds a 1. This turns out to be the number 2, signifying that we've come upon the second opcode (if the opcodes are arranged in ascending order). Notice that BNE will make us skip over the next couple of lines. Anytime we pull a 0 out of MTABLE it means that there is no valid opcode for that number, and we just print the address, the number, and a question mark ($3F). Then we raise the printout address pointer with INCSA and return to fetch the next byte (210-220).
However, in our example, we did find something other than a 0 in MTABLE. We've got a valid opcode. Now we have to find out its addressing mode and print a one- or two-byte argument, depending on that addressing mode. Is it Immediate addressing like LDA #15 (one-byte argument) or Absolute addressing like LDA 1500 (two-byte argument)?
Having found a valid opcode, we now extract the mnemonic from WORDTABLE and print it out (240-330). First we multiply our number from MTABLE by 3 since each mnemonic has three letters. The number we found in MTABLE was a 2, so we have a 6 after the multiplication. That means that our mnemonic will start in the sixth position within WORDTABLE. We add 6 to the address of WORDTABLE (280-290) and leave the variable PARRAY pointing at the first letter O in WORDTABLE.
Now the SA (current disassembly address) is printed onscreen with PRNTSA and a space is printed (300). We then print ORA onscreen, one letter at a time (310-330), and print another space. Now we're ready to figure out the addressing mode.
We had previously saved our original byte (the number 1 in our example) in FILEN (190). We now retrieve it, pull out the position value from MTABLE (getting the number 2), and load in the addressing mode type from TYPETABLE (see lines 360-410 in the Dtables subroutine listing at the end of this chapter). It turns out that the number 2 we're using in our example will pull out a number 4 from TYPETABLE. The number 4 identifies this as an Indirect X addressing mode.
Between lines 380 and 410 we have a simple decision structure, much like BASIC's ON-GOTO structure. In our example, the CMP #4 in line 390 will now send us to a routine called DINDX which handles Indirect X addressing.
DINDX (460) takes advantage of several routines which print symbols to the screen for us: LEPAR prints a left parenthesis; DOONE fetches and prints the next number in RAM memory (the argument for the current mnemonic); COMX prints a comma and an X; and RIPAR finishes things off with a right parenthesis. Now we have something like this onscreen:
0360 ORA (12,X)
so our disassembly of this particular instruction is complete. We JMP to ALLDONE (600) and print a carriage return and start the main loop over again to disassemble the next mnemonic.
Other mnemonics and other addressing modes follow a similar path through Dis as they are looked up in Dtables and then printed out.
By the way, if you look at lines 650-680 on page 296, you'll see a peculiar #" pseudo-op. It allows you to specify a character instead of a number for immediate addressing. In line 650 we need to print a comma to the screen. You could LDA #44 (the ASCII code for a comma) and JSR PRINT.
But if you don't want to look up the ASCII code, LADS will do it for you. Just use a quote after the # symbol: LDA #", (followed by the character you're after; in this case, the comma). The correct value for the character will be inserted into your object code. You can see that we used this pseudoop to load the value for X, Y, ), and ( symbols as well, in lines 650-680.
Program 11-1. Dis-The Disassembler
10 ; DIS -- DISASSEMBLER
20 *= 17000
30 LDA #<DISMESS:STA TEMP:LDA #>DISMESS:STA TEMP+1:JSR PRNTMESS
40 JSR PRNTCR:LDA #$3F:JSR PRINT
50 STARTDIS LDA #1:STA HXFLAG:LDY #0:STY Y
60 DTMO JSR CHARIN; -- GET START ADDRESS (DECIMAL) --
70 BEQ DTMO
80 CMP #$0D; CARRIAGE RETURN
90 BEQ DMO
100 LDY Y:STA LABEL,Y:JSR PRINT
110 INY:STY Y:JMP DTMO
120 DMO LDA #0:STA LABEL,Y:JSR PRNTCR
130 LDA #<LABEL:STA TEMP:LDA #>LABEL:STA TEMP+1:JSR VALDEC
140 LDA RESULT:STA SA:LDA RESULT+1:STA SA+l
150 LDA RESULT:BNE EF:DEC RESULT+1:BF DEC RESULT; LOWER BY ONE
160 LDA RESULT:STA PMEM:LDA RESULT+1:STA PMEM+1
180 ;-------------- PULL IN A BYTE AND SEE IF IT IS A VALID OPCODE
190 GETBYTE JSR STOPKEY:JSR GB:STA FILEN;(SAVE AS INDEX)
200 TAY:LDA MTABLE,Y:BNE DMORE:JSR PRNTSA:JSR-PRNTSPACE
210 LDX FILEN:LDA #0:JSR PRNTNUM:JSR PRNTSPACE
220 LDA #$3F:JSR PRINT:JSR INCSA:JMP ALLDONE; NOT A VALID OPCODE
230 ; CONTINUE ON, FOUND A VALID OPCODE-----
240 DMORE STA WORK:LDY #0:STY PARRAY+1:ASL:STA PARRAY:ROL PARRAY+1
250 ; MULTIPLY Y BY THREE
260 LDA WORK:CLC:ADC PARRAY:STA PARRAY:LDA #O:ADC PARRAY+1:STA PARRAY+1
270 ; ADD THIS TO WORDTABLE
280 CLC:LDA #<IIORDTABLE:ADC PARRAY:STA PARRAY
290 LDA #>WORDTABLE:ADC PARRAY+1:STA PARRAY+1
300 JSR PRNTSA:JSR PRNTSPACE
310 LDY #0:LDA (PARRAY),Y:JSR PRINT:INY
320 LDA (PARRAY),Y:JSR PRINT:INY
330 LDA (PARRAY),Y:JSR PRINT:JSR PRNTSPACE
340 LDY FILEN:LDA MTABLE,Y; 0 MEANS NO ARGUMENT(INDIRECT OR ACCUMULATOR MODES)
350 TAY:DEY:LDA TYPETABLE,Y:BNE BRANCHES
360 JSR INCSA:JMP ALLDONE
370 BRANCHES LDA TYPETABLE,Y
380 CMP #1:BEQ DIMMED
390 CMP #2:BEQ DABSOL:CMP #3:BEQ DZERO:CMP #4:BEQ DINDX:CMP #5:BEQ DINDY
400 CMP #6:BEQ DZEROX:CMP #7:BEQ DABSOLX:CMP #8:BEQ DABSOLY:CMP #9:BEQ DREL
410 CMP #10:BEQ JDJUMPIND
420 JSR DOONE:JSR COMX:JMP ALLDONE; FALL-THROUGH TO TYPE 11 (ZERO,X)
430 DIMMED LDA #"#:JSR PRINT:JSR DOONE:JMP ALLDONE; IMMEDIATE (TYPE 1)
440 DABSOL JSR DOTWO:JMP ALLDONE:JDJUMPIND JMP DJUMPIND;ABSOLUTE (TYPE 2)
450 DZERO JSR DOONE:JMP ALLDONE; ZERO PG (TYPE 3)
460 DINDX JSR LEPAR:JSR DOONE:JSR COMX:JSR RIPAR:JMP ALLDONE; INDEX (TYPE 4)
470 DINDY JSR LEPAR:JSR DOONE:JSR RIPAR:JSR COMY:JMP ALLDONE; IND. Y (TYPE 5)
480 DZEROX JSR DOONE:JSR COMX:JMP ALLDONE; ZERO X (TYPE 6)
490 DABSOLX JSR DOTWO:JSR COMX:JMP ALLDONE; ABSOLUTE X (TYPE 7)
500 DABSOLY JSR DOTWO:JSR COMY:JMP ALLDONE; ABSOLUTE Y (TYPE 8)
510 DREL JSR GB:BPL RELPL; RELATIVE (TYPE 8)
520 STA WORK:LDA #$FE:SEC:SBC WORK:STA WORK+1
530 SEC:LDA SA:SBC WORK+1:STA WORK
540 LDA SA+1:SBC #$00:TAX:JSR PRNTNUM
550 LDX WORK:JSR PRNTNUM:JSR INCSA:JSR INCSA:JMP ALLDONE
560 RELPL CLC:ADC SA:ADC #2:STA WORK:LDA #0:ADC SA+l
570 TAX:JSR PRNTNUM
580 LDX WORK:JSR PRNTNUM:JSR INCSA:JSR INCSA:JMP ALLDONE
590 DJUMPIND JSR LEPAR:JSR DOTWO:JSR RIPAR:JMP ALLDONE; IND. JUMP (TYPE 10)
600 ALLDONE JSR PRNTCR:LDX BABFLAG:CPX #1:BCC ALLD1:PLA:PLA:JMP FIN
610 ALLD1 JMP GETBYTE
620 DOONE JSR GB:TAX:LDA #O:JSR PRNTNUM:JSR INCSA:JSR INCSA:RTS
630 DOTWO JSR GB:PHA:JSR GB:TAX:LDA #0
640 JSR PRNTNUM:PLA:TAX:JSR PRNTNUM:JSR INCSA:JSR INCSA:JSR INCSA:RTS
650 COMX LDA #",:JSR PRINT:LDA #"X:JSR PRINT:RTS
660 COMY LDA #",:JSR PRINT:LDA #"Y:JSR PRINT:RTS
670 LEPAR LDA #"(:JSR PRINT:RTS
680 RIPAR LDA #"):JSR PRINT:RTS
690 GB INC PMEM:BNE DINCP1:INC PMEM+1;REPLACES CONVENTIONAL CHARIN/DISK
700 DINCP1 STY Y:LDY #0:LDA (PMEM),Y:PHP:LDY Y:PLP:RTS; SAVE STATUS REGISTER
710 DISMESS BYTE "DISASSEMBLY START ADDRESS (DECIMAL)":.BYTE 0
720 .FILE DTABLES
Program 11-2. Dtables
10 ; "DTABLES" TABLES FOR DISASSEMBLER
30 ; TABLE OF 256 POSSIBLE VALUES (SOME ARE VALID ADDRESSING MODES)
50 MTABLE .BYTE 1 2 0 0 0 3 4 0 5 6 7 0 0 8 9 0
60 .BYTE 10 11 0 0 0 12 13 0 14 15 0 0 0 16 17 0
70 .BYTE 18 19 0 0 20 21 22 0 23 24 25 0 26 27 28 0
80 .BYTE 29 30 0 0 0 31 32 0 33 34 0 0 0 35 36 0
90 .BYTE 37 38 0 0 0 39 40 0 41 42 43 0 44 45 46 0
100 .BYTE 47 48 0 0 0 49 50 0 51 52 0 0 0 53 54 0
110 .BYTE 55 56 0 0 0 57 58 0 59 60 61 0 62 63 64 0
120 .BYTE 65 66 0 0 0 67 68 0 69 70 0 0 0 71 72 0
130 .BYTE 0 73 0 0 74 75 76 0 77 0 78 0 79 80 81 0
140 .BYTE 82 83 0 0 84 85 86 0 87 88 89 0 0 90 0 0
150 .BYTE 91 92 93 0 94 95 96 0 97 98 99 0 100 101 102 0
160 .BYTE 103 104 0 0 105 106 107 0 108 109 110 0 111 112 113 0
170 .BYTE 114 115 0 0 116 117 118 0 119 120 121 0 122 123 124 0
180 .BYTE 125 126 0 0 0 127 128 0 129 130 0 0 0 131 132 0
190 .BYTE 133 134 0 0 135 136 137 0 138 139 140 0 141 142 143 0
200 .BYTE 144 145 0 0 0 146 147 0 148 149 0 0 0 150 151 0
220 ; TABLE OF MNEMONICS (TIED TO THE NUMBERS IN TABLE ABOVE)
240 WORDTABLE .BYTE "XXXBRKORAORAASLPHPORAASLORAASLBPLORAORAASL
250 .BYTE "CLCORAORAASLJSRANDBITANDROLPLPANDROLBIT
260 .BYTE "ANDROLBMIANDANDROLSECANDANDROLRTIEOR
270 .BYTE "EORLSRPHAEORLSRJMPEORLSRBVCEOREORLSRCLIEOR
280 .BYTE "EORLSRRTSADCADCRORPLAADCRORJMPADCRORBVSADC
290 .BYTE "ADCRORSEIADCADCRORSTASTYSTASTXDEYTXASTYSTA
300 .BYTE "STXBCCSTASTYSTASTXTYASTATXSSTALDYLDALDX
310 .BYTE "LDYLDALDXTAYLDATAXLDYLDALDXBCSLDALDYLDALDX
320 .BYTE "CLVLDATSXLDYLDALDXCPYCMPCPYCMPDECINYCMPDEXCPYCMPDEC
330 .BYTE "BNECMPCMPDECCLDCMPCMPDECCPXSBCCPXSBCINC
340 .BYTE "INXSBCNOPCPXSBCINCBEQSBCSBCINCSEDSBCSBCINC
360 ; TABLE OF MODE TYPES (TIED TO THE NUMBERS IN MTABLE ABOVE)
380 ; (TYPE 0 = IMPLIED) (1 = IMMEDIATE) (2 = ABSOLUTE) (3 = ZERO PG.)
390 ; (TYPE 4 = INDIRECT X ) (5 = INDIRECT Y) (6 = ZERO X) (7 = ABSOLUTE X)
400 ; (TYPE 8 = ABSOLUTE Y ) (9 = RELATIVE)
410 ; (TYPE 10 = JMP INDIRECT) (11 = ZERO Y)
430 TYPETABLE .BYTE 0 4 3 3 0 1 0 2 2 9
440 .BYTE 5 6 6 0 8 7 7 2 4 3
450 .BYTE 3 3 0 1 0 2 2 2 9 5
460 .BYTE 6 6 0 8 7 7 0 4 3 3
470 .BYTE 0 1 0 2 2 2 9 5 6 6
480 .BYTE 0 8 7 7 0 4 3 3 0 1 0 10
490 .BYTE 2 2 9 5 6 6 0 8 7 7 4 3 3
500 .BYTE 3 0 0 2 2 2 9 5 6 6
510 .BYTE 11 0 8 0 7 1 4 1 3 3
520 .BYTE 3 0 1 0 2 2 2 9 5 6
530 .BYTE 6 11 0 8 0 7 7 8 1 4
540 .BYTE 3 3 3 0 1 0 2 2 2 9
550 .BYTE 5 6 6 0 8 7 7 1 4 3
560 .BYTE 3 3 0 1 0 2 2 2 9 5
570 .BYTE 6 6 0 8 7 7
580 .END DEFS
Notes on the Structure of Atari LADS
The Atari and Commodore machines have one thing in common-a 6502 microprocessor. The Atari 6502 runs at 1.79 megahertz, making it somewhat faster than the Commodore machines. However, the non-6502 hardware-input/output, graphics, and sound-is entirely different. Although many Atari enthusiasts argue that it is the most powerful available on any 6502-based microcomputer, the operating system of the Atari does not perform basic tasks like input/output in the same manner as Commodore machines. An understanding of these differences is essential to fully understand the Atari LADS source code.
The common tasks machine language programs need to perform with input/output are: open a file, read a character or block of characters from the file, write a character or block of characters to a file, and close the file. With the Commodore operating system (often called the Kernal), there are separate routines for each task. You approach each task by adjusting the Accumulator, X, and Y Registers as necessary, as well as storing any required information into special memory locations (usually in zero page). See the discussion of OPEN1 in Chapter 5 for details. For example, the Commodore OPEN must know where to find the filename, the length of the filename, parameters like read or write, and the device number.
On the Atari, there is just one entry point-$E456, called CIO, for all these tasks. Instead of separate entry points, CIO checks a memory location for the command, a number representing the action to take, such as OPEN, CLOSE, PUT, or GET. Other memory locations hold the starting address of a filename or buffer, and the length of the filename or buffer. Extra locations hold specialized information. Each block of I/O information is called an IOCB, for Input/Output Control Block. There are eight of these IOCBs, numbered 0 to 7. IOCB 0 is reserved for the screen editor, and 7 is usually reserved for language I/O, such as LPRINT in BASIC, or SAVE in the LADS editor.
Although much of LADS is concerned with internal data base-type manipulations, such as looking up a label or converting a mnemonic, there is also a good amount of Commodorestyle input/output. Routines like OPEN, CLRCHN, CHKIN, and PRINT are actual ROM entry points on Commodore computers. To avoid complex changes in the source code, Atari LADS has a special file called Kernal (see program listings below), which transparently supports all these routines, making the conversion between the Atari's I/O system and the Commodore's transparent. Explanations of Commodore I/O given in Chapter 5, then, are valid as well for the Atari LADS system. In other words, when the original Commodore version of LADS was translated to the Atari, the Kernal subprogram was added to mimic the operations of the Commodore operating system I/O. This emulation allows the descriptions of LADS to remain essentially identical for nonCommodore machines.
Atari Memory Layout
Memory maps for Commodore computers are relatively simple. Zero page is used by the system, page 1 for the stack, page 2 for operating system storage, and page 3 for the cassette buffer(s). On the Commodore PET, page 4 (starting at address 1024) on up to location 32768 is free RAM. 32768 is the start of screen memory on the PET, and never moves. On the 64, the screen occupies page 4 up to 2047 ($07FF). Free RAM starts at 2048 ($0800) all the way up to 40959 ($9FFF). BASIC in ROM and the operating system start at 40960 ($A000). Although there is hidden memory beneath the ROMs on both the Atari XL series and the Commodore 64, LADS does not use it.
The Atari memory layout is less fixed. Zero page from locations 0 to 127 completely used by the operating system. An applications program like BASIC can use almost all the memory from 128 to 255. Since Atari LADS operates outside the BASIC environment, it is free to use this zero page memory upwards from location $80.
Unlike the PET and 64, Atari machines have no set amount of memory. Atari 400/800 owners have the option of expanding to 48K, without using bank selection or other tricks. Without DOS, free memory starts at $0700 (page 6 is reserved). With DOS, free RAM starts at about $2000. The screen memory, a little over 1K in length, is stored at the top of memory, and is not fixed, due to memory expansion. Many Atari machine language programs store themselves at the bottom of memory, then use memory above themselves to store text or other information. But because LADS stores its labels below itself, the Atari version must be located at the top of memory. Since the top of memory with a cartridge (or with 40K of RAM) is $9FFF, and since Atari LADS is about 7K long, $8000 seems to be a good place. If you have a 48K Atari, you may want to reassemble LADS at $A000. The choice of $8000 does exclude Atari owners with less than 40K, but if you have access to a 40K machine, you could reassemble LADS at 8K below the top of memory.
Let's look at the major differences between the Atari LADS and Commodore LADS source code. We won't get into specifics; for that you can refer to the source code itself. The translation of Atari LADS involved two goals: the creation of a powerful assembly development system without making major changes to most of the Commodore LADS source code. Some subprograms needed no changes, others did. Three new subprograms are required by the Atari version: Kernal, System, and Edit.
Here's how all the subprograms in the Atari LADS are linked:
Defs → Eval → Equate → Array → Openl → Findmn → Getsa → Valdec → Indisk → Math → Printops → Pseudo → Kernal → System → Edit → Tables
Defs. Here we set the origin to $8000. Since we are simulating Commodore I/O, we have to create some label variables such as FNAMELEN (filename length). These are used by the Kernal routines. Other LADS variables like MEMTOP and PMEM are also given zero page definitions for the sake of speed and for indirect addressing. The BABUF, used for holding comments and holding a line in the editor, is defined as $0500. On Commodore machines it is $0200, the address of the BASIC input buffer.
Eval. The first difference between the Commodore and Atari versions of Eval is that instead of reading the filename off the screen, Atari LADS gets the filename from the command line, passed by the editor. The editor has previously set RAMFLAG to 1 if there is no filename. This is a default to RAM-based assembly (your source code is already in memory and need not be read from disk). If RAMFLAG is 0, LADS must assemble from disk. If the RAMFLAG is nonzero, we skip over putting the filename into FILEN, and jump past the JSR OPEN1 in Eval (since there is nothing to open). At the top of Eval, the left margin is set to zero.
Since LADS has complete control of the Atari, no memory needs to be protected from anything, so the top-of-memory pointer need not be lowered.
In FINI, the RAMFLAG is also checked so that JSR OPEN1 is skipped. In FIN, which FINI falls to after the end of the second pass, we send an extra byte out to the object file, if .D was used.
Equate, Array, and Findmn. There was no need to change any of these modules, since they contain no system-specific coding.
Openl. Many changes have also been made to Openl, although a lot of the source code is similar. FDEV and FSECOND hold the device number and secondary address in Commodore LADS. Here they are used to hold the access type (4 for read, 8 for write) and the auxiliary byte (which is zero here). Openl checks the RAMFLAG to see whether it should load the file after it's been opened, in case memory assembly has been elected. The actual load is done by using part of the editor's load routine. Because of RAMFLAG, we don't need a separate LOAD1 routine.
If the file can't be opened, we call the editor's error message routine, and then return to the editor. The same error handling is performed for all the OPENs.
OPEN2 writes out the binary file header, made up of two 255's, followed by the starting and ending addresses in low byte/high byte format. The origin (the starting address for the object code) is saved in the variable TA. The object code's ending address is known, and stored in LLSA. LLSA is actually one higher than the ending address, which is why we write an extra zero to the end of the file in Eval. This prevents an ERROR 136 when loading the file from DOS.
OPEN4 just opens a file for write to the printer. The printer's filename is P:, which is given in the BYTE statement as 80 58.
Getsa. Getsa is very similar to the Commodore version. There is no MEMSA-Getsa initializes PMEM to point to the start of the editor's text buffer (TEXTBAS), even if PMEM is not used. Since CHARIN is smart, checking RAMFLAG to decide whether to assemble from memory or from disk, no more changes need to be made.
Valdec. Valdec would have been unchanged from the Commodore version, since there is no machine-specific code. However, the editor makes use of Valdec to convert ASCII line numbers into integers. The ASCII line number does not end with a zero, though. The first part of Valdec finds the length of the number by checking for a zero. It has been changed in the Atari version to exit on any nonnumeric digit (one with an ASCII value less than 48 or greater than/equal to 58). The change does not affect any other use of Valdec.
Indisk. It is in Indisk where we see many modifications to the Commodore version. Since the editor does not tokenize anything, KEYWORD and KEYWAD are not needed, and references to them in this source code, as well as the KEYWORD and KEYWAD routines themselves, have been deleted. Again, since nothing is tokenized, checks for +, *, <, >, etc., look for the ASCII values instead of the tokenized ones. Since line numbers are stored as a string of digits instead of a two-byte integer, we must call LINENUMBER in the SYSTEM module in order to set LINEN. ENDPRO, instead of looking for three zeros to signify the end of a program, must check the disk status variable for end of file. End of file returns 136 after the last character has been read, and $03 if you try to read past the end of file, so we check for both to be safe. We check the status for file #1 (the input file) directly ($0353), instead of ST, since ST may have been changed by another I/O operation. Nonetheless, large parts of Indisk are unchanged from the Commodore version.
Printops. Because of the Kernal simulator, even though Printops has plenty of Commodore I/O calls, few changes were needed to make Printops work on the Atari.
Pseudo. There are some minor changes here. KEYWORD does not need to be used by END or FILE. FILE finds the end of the pseudo-op by looking for a space delimiter. The filename is then copied into FILEN, and the file opened. If the current operation is a RAM-based assembly, Openl takes care of loading in the next file. PEND, which supports END, first calls FILE to open the file, then copies SA, which holds the current address, into LLSA for use with OPEN2.
Speaking of OPEN2, some code was deleted from PDISK and instead implemented in OPEN2. There were no more changes after PDISK to the Pseudo module. In Commodore LADS, Pseudo links to Tables, the last module. Here we link to Kernal, inserting Kernal, System, and Edit into the chain.
Kernal. This is the most important module in the Atari translation. It implements all the Commodore I/O functions by simulating CHKIN and CHKOUT, and referencing the appropriate IOCB according to FNUM. The CIO equates are first defined: ICCOM, the command byte; ICBADR, which holds the address of the filename or buffer; ICBLEN, which holds the length of the filename or buffer; ICAUX1 and ICAUX2, which need to be set to zero; and CIO itself, that single entry point for all input/output.
A simple routine is X16, which multiplies the Accumulator times 16 and stores it in the X Register. X will be an offset from the first IOCB. Since each IOCB is 16 bytes long, we can use Indexed addressing to change the appropriate IOCB with a statement like STA ICCOM,X.
OPEN is the basic open-file routine. It uses X16 to get the IOCB offset, then stores the filename pointer and filename length into ICBADR and ICBLEN. The command byte for open ($03) is stored in ICCOM, then CIO is called. CIO's error status, which is returned in the Y Register, is saved in ST.
CHKIN changes the default input IOCB, which is used in CHARIN. CHKOUT changes the default output IOCB, which is checked for in PRINT. CLOSE just stores the close command (12) into ICCOM and jumps to CALLCIO, part of OPEN. CLRCHN sets the default INFILE and OUTFILE, as well as FNUM and ST to zero, which makes CHARIN and PRINT use IOCB #0, opened to the screen editor.
PRINT is expected to print the character currently in the Accumulator. It first changes any 13's it sees, which are Commodore carriage returns, into 155's (Atari carriage returns). Another entry point, OBJPRINT, does not transform 13's. This is called when object bytes need to be sent to disk, where you don't want 13's changing into 155's. Depending on OUTFILE, PRINT will automatically use the appropriate IOCB (0 for screen, 2 for object output, 4 for printer output). We then set the buffer length to zero, which tells CIO to expect to find the character to print in the Accumulator. The print text command is used, then we call CIO and restore the X and Y Registers, which were saved when PRINT was entered. This prevents any interference with LADS.
CHRIN is also a busy routine. It first checks RAMFLAG to see whether it should get a byte from an 1/O device or from the editor's text memory. If it gets a byte from memory, it must check to see if it has gone past the last byte. If so, we jump straight to FINI in Eval. Otherwise, CHRIN gets a byte from disk or the keyboard. It uses INFILE to decide which IOCB to use, then sets the buffer length to zero. This way it requests a single byte from CIO. If a 155 is returned, it is changed into a zero, which is what LADS looks for as end of line.
There is no "check for BREAK key" routine in Atari ROM, so STOPKEY checks the BREAK key flag, which is set to zero if the BREAK key is pressed. If BREAK was pressed, we execute TOBASIC, which jumps back to the editor.
CLALL is not used by LADS, but is used by the editor to close all files in case of an error. It works like the Commodore CLALL routine, and restores the default 1/0 (input from keyboard, output to screen) by jumping to CLRCHN.
System. A few more routines are provided here which are not directly supported by the operating system. OUTNUM prints the ASCII number given to it in the X Register, which holds the low byte of the number to print, and the Accumulator holding the high byte. We then call $D9AA, which converts the integer number in locations $D4 and $D5 into floating point, and then call $D8E6, which converts the floating point into a printable ASCII sequence of digits starting at $0580. The routine at $D8E6 sets bit 7 in the last digit of the ASCII numeral string. We print the string, checking and masking off bit 7. LINENUMBER reads the ASCII line number from source code and converts it to an integer, using VALDEC. The result is saved in LINEN.
Tables. The major changes here are that the error messages must be typed in inverse video. One extra variable is defined: LLSA to hold the ending address.
Program 11-3. Kernal
100 ICCOM = $0342
110 ICBADR = $0344
120 ICBLEN = $0348
130 ICAUX1 = $034A
140 ICAUX2 = $034B
150 CCLOSE = 12
160 CIO = $E456
170 X16 ASL
230 ;Opens a file OPEN #FNUM,FDEV,FSECOND,(F
240 OPEN LDA FNUM
250 JSR X16
260 LDA FNAMEPTR
270 STA ICBADR,X
280 LDA FNAMEPTR+1
290 STA ICBADR+I.X
300 LDA FNAMELEN.
310 STA ICBLEN,X
320 LDA #0
330 STA ICBLEN+1,X
340 LDA FDEV
350 STA ICAUX1,X
360 LDA FSECOND
370 STA ICAUX2,X
380 LDA #$03
390 STA ICCOM,X
400 CALLCIO JSR CIO
410 STY ST
430 CHKIN STX INFILE
450 CHKOUT STX OUTFILE
470 CLRCHN LDX #0
480 STX INFILE
490 STX OUTFILE
500 STX FNUM
501 STX ST
520 CLOSE JSR X16
530 LDA #12
540 STA ICCOM,X
550 JMP CALLCIO
560 PRINT CMP #13
570 BNE OBJPRINT
580 LDA #155
590 OBJPRINT STA KASAVE
600 STY KYSAVE
610 STX kXSAVE
620 LDA OUTFILE
630 JSR X16
640 LDA #0
650 STA ICBLEN,X
660 STA ICBLEN+I,X
670 LDA #11
680 STA ICCOM.X
690 LDA KASAVE
700 JSR CALLCIO
710 LDY KYSAVE
720 LDX KXSAVE
730 LDA KXSAVE
760 CHARIN STY KYSAVE
770 STX KXSAVE
780 LDA RAMFLAG
790 BEQ CHRIN;If RAMFLAG=0 (False) then get
byte from device
800 ;Else get byte from memory
810 LDY #0:LDA (PMEM),Y:PHA
820 INC PMEM:BNE NINCPI:INC PMEM+1
830 NINCPI CLC:LDA PMEM:SBC TEXEND:STA KTEMP
840 LDA PMEM+1
850 SBC TEXEND+1
860 ORA KTEMP:BCC NOTEOF:BEQ NOTEOF
880 JMP FINI
890 NOTEOF LDA #0:STA ST:STA $0353
900 PLA:JMP CHRXIT
910 CHRIN LDA INFILE
920 JSR X16
930 LDA #0
940 STA ICBLEN,X
950 STA ICBLEN+1,X
960 LDA #7
970 STA ICCOM,X
980 JSR CALLCIO
990 CHRXIT LDY KYSAVE
1000 LDX KXSAVE
1010 CMP #155
1020 BNE ZICR
1030 LDA #0
1040 ZICR RTS
1050 STOPKEY FHA
1060 LDA $11
1070 BEQ TOBASIC
1100 TOBASIC JMP EDIT
1150 CLALL LDX #7
1160 CLLOOP STX KTEMP:TXA:JSR CLOSE
1170 LDX KTEMP:DEX:BNE CLLOOP i
1180 JMP CLRCHN
1190 KXSAVE BYTE 0
1200 KYSAVE BYTE 0
1210 KXSAVE .BYTE 0
1220 KTEMP BYTE 0
1230 .FILE D:SYSTEM.SRC
Program 11-4. System
170 OUTNUM STX $D4
180 STA $D5
190 JSR $D9AA
200 JSR $DGE6
230 LDY #0
240 ONUMLOOP STY OYSAVE
250 LDA ($F3),Y
270 AND #$7F
280 JSR PRINT
300 BMI ONUMEXIT
310 LDY OYSAVE
330 BNE ONUMLOOP
340 ONUMEXIT RTS
360 OYSAVE BYTE 0
390 LINENUMBER LDY #0
400 LINELOOP JSR CHARIN
410 CMP #32
420 BEG OUTLINE
430 STA BABUF,Y
450 JMP LINELOOP
460 OUTLINE LDA #0
470 STA BABUF,Y
480 LDA #<BABUF
490 STA TEMP
500 LDA #>BABUF
510 STA TEMP+1
520 JSR VALDEC
530 LDA RESULT
540 STA LINEN
550 LDA RESULT+1
560 STA LINEN+1
570 LDY #0
590 .FILE D:EDIT.SRC
The Atari LADS Editor
The Atari editor is a whole minilanguage system itself. The source code for this subprogram is well commented and should be understandable as it stands. Since it is not a part of LADS proper, we'll limit ourselves here to an overview of the major routines.
UMOVE and DMOVE are high-speed memory move routines used to adjust the source code when lines are deleted, added, and so forth. UMOVE can move one range of memory to another, provided that the block to be moved is higher in memory. The range of bytes can overlap so UMOVE can be used as a delete routine. DMOVE moves memory downward, and is used for inserting. If the memory ranges do not overlap, either one can be used. FROML and FROMH hold the start of the block to be moved. DESTL and DESTH are where the block is moved to. LLEN and HLEN are set to hold the length of the block to be moved. These routines use self-modifying code for speed.
EDIT is the entry point for LADS when it is first run, as well as the return point from the LADS assembler. It cleans up the stack, resets the left margin to 2, then stores the addresses of all the editor commands into COMVECT, which is a lookup table used by COMMAND. The BRK interrupt is initialized to point to a special breakpoint entry to the editor. We then check to see if this is the first time EDIT has been entered. If so, we need to NEW out any garbage in memory. The NEW routine sets the end-of-text pointer to point to the beginning of text. No memory is actually cleared.
PROMPT is the entry point for a new line. It prints "LADS Ready", then falls through to ENTER, which is the entry point for a new line without printing a prompt. CHARIN from Kernal gets a byte, which is then processed to remove lowercase, etc. The line is stored in the BABUF, starting at $0500. When a carriage return is detected, an Atari carriage return is added to the end of the line in BABUF, and the length of the line is saved in INLEN. If the length is zero, we go back for another line. The first character of the line is checked. If it is a numeric digit, there must be a line number. If there is no line number, then the line must be a command.
If it is a line number, we call GETLNUM to get the integer value of the line number. GETLNUM also calls FINDLINE to see if that line already exists. If it does, the line is deleted. Then we check to see if there is anything else besides just a line number. If not, we don't insert the line into the source code. Since the line was already deleted, this has the desired effect. We then go back for another line.
COMMAND searches through a table of commands, matching the line the user typed in against the table. If the command is not found, a syntax error message is displayed, and we return to PROMPT. If the command is found, we save the position of whatever's after the command (the argument) in ARGPOS. The command number (COMNUM) is used as an index into COMVECT, which holds the addresses of all commands. We get the address, subtract one from it, then put it on the stack. A RTS then ends up pulling this address off and jumping to it. It's like ON-GOTO in BASIC.
MLIST lists the entire text buffer, from TEXTBAS to TEXEND. A second entry point in MUST, INLIST, is called by the LIST routine to list a part of a program. We also check here for the BREAK key. MUST is used by SAVE to list the program to disk, cassette, or the printer.
DOS is spectacularly simple. It just jumps through the DOS vector, location $0A.
FINDLINE is crucial to the editor. It searches through the source code, trying to match the line number given to it (LNUM) against all the ASCII line numbers in the program. It uses Valdec to convert the ASCII line number into an integer. Because of all the ASCII to integer conversions, FINDLINE can be slow on long programs. It returns with BEGPTR pointing to the beginning of the line found, and ENDPTR pointing to the end of the line. If there is no program in memory, it returns with BEGPTR and ENDPTR pointing to the start of text. If the line is not found, BEGPTR and ENDPTR point to the next line greater than the line number searched for. If there is no such line, they point to the end of text. The size of the line found is also calculated for the benefit of the delete routine.
DELETE calls FINDLINE, then calls UMOVE to move memory from the end of the line on top of the beginning of the line. TEXEND is then changed to reflect a shorter program. Many checks have to be made to prevent a crash under conditions such as no program in memory. INSERT is similar to DELETE. It calls DMOVE to insert a gap at the position the line was found.
ERRPRINT is used to display an error message. To be safe, it also closes all files. GETNUM gets and converts an ASCII line number to an integer, using the system ASCII-to-floating-point and floating-point-to-integer routines. The routines return a pointer to the end of the number. This pointer is always kept track of so we can check for new command arguments. GETLNUM uses this routine, then calls FINDLINE.
LIST calls GLIST, which is also used by SAVE. GLIST finds out the line number range you want to list. If there is no line number range given, it goes to MUST to list the entire program. Otherwise, it has to check for just one line given, or a range of lines. It's complicated, but it works.
OPENFILE is used by SAVE, LOAD, and MERGE. It looks at the argument of the command to get the filename, then calls OPEN within Kernal. If there is an error, we jump to PROMPT. SAVE calls OPENFILE with an 8 for output. It then sets the output file and calls GLIST, which sends the listing out to the current output file. After GLIST returns, the file is closed.
MERGE just sets the input file to the device and jumps to PROMPT. PROMPT keeps requesting input and storing lines until it gets an error. It then closes the file and restores default I/O.
Adding Your Own Editor Commands
The LADS command checks to see if there is a filename, then sets the RAMFLAG accordingly and jumps into EVAL. The SYS command calls GETNUM to get the decimal argument, then stores the address right after a JSR, to which it then falls through, creating a self-modifying indirect JSR. If the routine being called ends in a RTS, control will be returned to PROMPT. You can use SYS to add new editor commands. Just check location $DO, which will point to a position with BABUF ($0500) after the SYS number. You can use $DO to check for extra arguments within BABUF.
LOAD calls OPENFILE to open the load file for read. It has a second entry point (AFTEROPEN) if the file has already opened. For maximum speed, the program is loaded by calling the CIO get-record routine, which loads in the entire file directly at TEXTBAS, the start of text. Beware, though, that no conversions are done on any of the text, and no checks are made for a legal source file. You could even load and list word processing files. AFTEROPEN is called by Open1 if RAM needs to be reloaded during a memory assembly.
The last routine in the editor handles a BRK instruction entry encountered. It prints a message, uses OUTNUM to display the address where the BRK was found, clears the interrupt flag, cleans the stack, then jumps to the Edit entry point. Edit then links to Tables.
Program 11-5. Editor
100 ;Line Editor for LADS
110 ;Charles Brannon 1984
0130 PTR = $CB
0140 TEXTBAS = $2000
0150 ;Move routines
0170 JMP EDIT
0180 FROML .BYTE 0
0190 FROMH .BYTE 0
0200 DESTL .BYTE 0
0210 DESTH .BYTE 0
0220 LLEN .BYTE 0
0210 HLEN .BYTE 0
0240 ENDPOS .BYTE 0
0250 INLEN .BYTE 0
0260 LNUM .BYTE 0 0
0270 TEXTPTR .BYTE 0
0280 COMNUM .BYTE 0
0290 TEXEND .BYTE 0 0
0300 LEN .BYTE 0
0310 YSAVE .BYTE 0
0320 BEGPTR .BYTE 0 0
0330 ENDPTR .BYTE 0 0
0140 FOUNDFLAG .BYTE 0
0350 LINESIZE .BYTE 0 0
0360 SAVEND .BYTE 0 0
0370 SAVBEG .BYTE 0 0
0380 ARGPOS .BYTE 0
0390 ZFLAG .BYTE 0
0400 LCFLAG .BYTE 0
0410 FIRSTRUN .BYTE 0
0420 INDEX = $D0
0430 TMP .BYTE 0
0450 UMOVE LDA FROML
0460 STA M0VL00P+1
0470 LDA FROMH
0480 STA MOVLOOP+2
0490 LDA DESTL
0500 STA MOVLOOP+4
0510 LDA DESTH
0520 STA MOVLOOP+5
0530 LDX HLEN
0540 BEQ SkIPMOV
0550 MOV1 LDA #0
0560 MOV2 STA ENDPOS
0570 LDY #0
0580 MOVLOOP LDA $FFFF,Y
0590 STA $FFFF,Y
0610 CPY ENDPOS
0620 BNE MOVLOOP
0630 INC MOVLOOP+2
0640 INC MOVLOOP+5
0650 CPX #0
0660 BEQ OUT
0680 BNE MOV1
0690 SKIPMOV LDA LLEN
0700 BNE MOV2
0710 OUT RTS
0730 DMOVE LDA HLEN
0750 ORA LLEN
0760 BNE NOTNULL
0780 NOTNULL CLC
0800 ADC FROMH
0810 STA DMOVLOOP+2
0820 LDA FROML
0830 STA DMOVLOOP+1
0660 ADC DESTH
0870 STA DMOVLOOP+5
0880 LDA DESTL
0890 STA DMOVLOOP+4
0910 LDY LLEN
0920 BNE DMOVLOOP
0930 BEQ SKIPDMOV
0940 DMOV1 LDY #255
0950 DMOVLOOP LDA $FFFF,Y
0960 STA $FFFF,Y
0980 CPY #255
0990 BNE DMOVLOOP
1000 SKIPDMOV DEC DMOVLOOP+2
1010 DEC DMOVLOOP+5
1030 BNE DMOV1
1060 EDIT LDX #255;Reset stack
1071 JSR CLALL
1080 LDA #0;Clear RAMFLAG
1090 STA RAMFLAG
1100 LDA #2;Left margin
1110 STA 82
1120 JSR PRNTCR
1130 ;Store addresses of commands
1140 LDA #<LIST
1150 STA COMVECT
1160 LDA #>LIST
1170 STA COMVECT+1
1180 LDA #<DOS
1190 STA COMVECT+2
1200 LDA #>DOS
1210 STA COMVECT+3
1220 LDA #< INIT
1230 STA COMVECT+4
1240 LDA #>INIT
1250 STA COMVECT+5
1260 LDA #<SAVE
1270 STA COMVECT+6
1280 LDA #>SAVE
1290 STA COMVECT+7
1300 LDA #<LOAD
1310 STA COMVECT+B
1320 LDA #>LOAD
1330 STA COMVECT+9
1340 LDA #<MERGE
1350 STA COMVECT+10
1360 LDA #>MERGE
1370 STA COMVECT+11
1380 LDA #<LADS
1390 STA COMVECT+12
1400 LDA #.`I-LADS
1410 STA COMVECT+13
1420 LDA #<SYS
1430 STA COMVECT+14
1440 LDA #>SYS
1450 STA COMVECT+15
1460 ;Set BRK instr. interrupt to breakpoint
1470 LDA #<BREAK:STA 518:LDA #>BREAK:STA 519
1480 LDA FIRSTRUN
1490 BEQ DONEW
1500 JMP PROMPT
1510 DONEW LDA #$CB
1520 STA FIRSTRUN
1530 JMP INIT
1540 NEW LDA #<TEXTBAS;Store beginning locat
ion at ending pointer
1550 STA TEXEND
1560 LDA #>TEXTBAS
1570 STA TEXEND+1
1580 JSR CLRCHN;Keyboard/Screen
1600 INIT JSR NEW
1620 PROMPT LDA #<PMSG;Print prompt
1630 LDY #>PMSG
1640 JSR PRMSG
1650 ENTER LDY #0;Get a line
1660 STY ZFLAG
1670 STY LCFLAG
1680 GETIT JSR CHARIN;a character
1690 LDX ST;Error?
1700 BPL NOERR
1710 CPX #136;End of file?
1720 BEQ EOF;don't print error
1730 CPX #128;same for break key abort
1740 BEQ EOF
1750 JSR ERRPRINT;print other error
1760 EOF JSR CLOSEIT;close down active file
1770 JMP PROMPT;get new line
1780 NOERR CMP #34;A quote toggles the lower
1790 BNE NOTQUOTE
1800 PHA;save quote
1810 LDA LCFLAG;flip lowercase
1820 EOR #1
1830 STA LCFLAG
1840 PLA;restore quote
1850 NOTQUOTE CMP #48;an ASCII "0"?
1860 BNE NOTZ
1870 LDX ZFLAG;if so, check to see if it's a
1680 BEQ GETIT;if it is, ignore it
1890 NOTZ INC ZFLAG;if we get here, reset le
ading zero flag
1900 CMP #59;now check for comment
1910 BNE NOTREM
1920 INC LCFLAG;disable lowercase conversion
for rest of line
1930 NOTREM LDX LCFLAG
1940 BNE NOTLOWER;if remflag has been set, d
on't convert lowercase
1950 AND #127;kill inverse
1960 CMP #97;lowercase "a"
1970 BCC NOTLOWER;if less than, not lowercas
1980 CMP #123:lowercase "z"+1
1990 BCS NOTLOWER;if >=, not lowercase
2000 AND #95;kill bit 5(127-32=95)
2010 NOTLOWER STA BABUF,Y;store it
2030 CMP #0
2040 BNE GETIT
2060 LDA #155
2070 STA BABUF,Y
2080 STY INLEN;save length of line
2090 CPY #0
2100 BEQ ENTER;if length=0, blank line, so g
2110 LDA BABUF;first character: is it a numb
2120 CMP #58
2130 BCS COMMAND;greater than "9", so must b
e a command
2140 CMP #48;"0"
2150 BCS LINE;greater than "9" but greater
2160 JMP COMMAND;no, so command
2170 ;Must be a line, so get line number
2180 LINE LDA #255;no offset
2190 JSR GETLNUM
2200 LDA INDEX;INDEX points to first non-num
2210 STA TEXTPTR;so save it
2220 LDA FOUNDFLAG;if it exists
2230 BNE NODELETE;it not, don't delete it
2240 JSR DELETE
2250 NODELETE LDY TEXTPTR;is there any text
on the line?
2260 CPY INLEN;compare to line length
2270 BEQ OVERINS;no text, just delete
2280 JSR INSERT;otherwise insert line
2290 OVERINS JMP ENTER;and get another line
2310 COMMAND LDA #eCOMTAHLE;point to start o
f command table
2320 STA PTR
2330 LDA #>COMTAHLE
2340 STA PTR+1
2350 LDY #0;for loop
2360 STY COMNUM_;clear command number
2370 LDX #0;for loop
2380 SRCH LDA (PTR),Y;get a character of com
2390 BEQ COMFOUND;if we get zero here, comma
nd is found
2400 CMP #255;or syntax error
2410 BEQ SYNERR
2420 CMP HABUF,X;match with parallel charact
er in line buffer?
2430 BNE NOTFND;if comparison fails, try nex;
2440 INX;next character
2450 BACK; IN INY
2460 BNE SRCH;bump high byte?
2470 INC PTR+1;yes
2480 JMP SRCH;continue
2490 NOTFND LDA (PTR),Y;if not found, skip p
ast ending zero
2500 BEQ NXTONE
2520 BNE NOTFND
2530 INC PTR+1
2540 JMP NOTFND
2550 NXTONE INC COMNUM;bump up command numbe
2560 LDX #0;continue search
2570 JMP BACKIN
2590 SYNERR LDA #<SYNMSG;print syntax: error
2590 LDY #>SYNMSG
2600 JSR PRMSG
2610 JMP PROMPT
2620 COMFOUND STX ARGPOS
2630 LDA COMNUM;indirect jump to address of
2660 LDA COMVECT,X
2680 SBC #1
2690 STA TMP
2700 LDA COMVECT+1,X
2710 SBC #0
2730 LDA TMP
2760 ;Command table. Format:
2770 ;.BYTE "command" 0,"command" 0,255 (255
to end table)
2780 COMTABLE .BYTE "LIST"
2790 .BYTE 0
2800 .BYTE "DOS"
2810 .BYTE 0
2820 .BYTE "NEW"
2830 .BYTE 0
2840 .BYTE "SAVE "
2850 .BYTE 0
2860 .BYTE "LOAD "
2880 .BYTE "MERGE "
2890 .BYTE 0
2900 .BYTE "LADS"
2910 .BYTE 0
2920 .BYTE "SYS"
2930 .BYTE 0
2940 .BYTE 255
2950 ;table will hold address of each commas
d routine in low,high format
2960 COMVECT .BYTE 0 0 0 0 0 0 0 0 0 0
2980 MLIST LDA #:TEXTBAS;Point to beginning
2990 STA PTR
3000 SEC;get length of program to list
3010 LDA TEXEND
3020 SBC PTR
3030 STA LLEN into LLEN
3040 LDA #>TEXTBAS
3050 STA PTR+1
3060 LDA TEXEND+1
3070 SBC PTR+1
3080 STA HLEN;and HLEN
3090 INLIST LDA HLEN
3110 ORA LLEN;both zero7
3120 BNE DOLIST
3130 RTS;if so, exit LIST
3131 DOLIST LDA #1:STA 766
3140 CPX #0;hich byte zero?
3150 BEQ LOLST;if so. skip primary pass
3160 LDA #0;for primary pass, list fully
3170 STA LEN
3180 RELIST LDY #0
3190 FRLIST LDA (PTR),Y
3200 JSR PRINT;print a character
3210 LDA ST
3220 BMI OUTLIST;exit on error
3240 CPY LEN
3250 BNE FRLIST
3260 INC PTR+1
3270 DEX;primary pass completed?
3280 BMI OUTLIST;if so, do secondary pass
3290 BNE FRLIST;if not, continue
3300 LOLST LDA LLEN;now list remainder (seco
ndary pass) -
3310 STA LEN
3320 JMP RELIST;continue
3330 OUTLIST LDA #0:STA 766:RTS;go back. to R
3350 DOS JMP (10);DOS Vector
3370 FINDLINE LDA #<TEXTHAS;start at top of
3380 STA PTR;initialize pointer
3390 LDA #>TEXTBAS;same for high bytes
3400 STA PTR+1
3410 LDA #0
3420 STA FOUNDFLA6;set foundflag to affirmat
3450 BEQ STY YSAVE;preserve Y
3460 TYA;point to first byte in line
3480 ADC PTR
3490 STA TEMP;so we can convert line #
3500 STA HEGPTR;save start of line
3510 STA ENDPTR
3520 LDA PTR+1;same for high byte
3530 ADC #0
3540 STA TEMP+1
3550 STA HEGPTR+1
3560 STA ENDPTR+1
3570 ;check to see if at end
3590 LDA BEGPTR
3600 SBC TEXEND
3610 STA TMP
3620 LDA BEGPTR+1
3630 SBC TEXEND+1
3640 ORA TMP
3650 BCC NOTEND
3660 JMP NOTFOUND2
3670 NOTEND JSR VALDEC
3680 SEC;see if line number matches
3690 LDA RESULT
3700 SBC LNUM
3710 STA TMP
3720 LDA RESULT+1
3730 SBC LNUM+1
3740 ORA TMP
3750 BEQ FOUNDLINE;if match, line found
3760 BCS NOTFOUND
3770 ;no match at all, so continue search
3780 NEXTLINE JSR EOL;skip to end of line
3790 INY;skip over eol
3800 BNE NOADJ2
3810 INC PTR+1
3820 NOADJ2 JMP LEQ;continue search
3830 FOUNDLINE DEC FOUNDFLAG;set to found to
fter INC in NOTFOUND2)
3840 NOTFOUND JSR EOL;skip past end of line
3850 CLC;store at ending address
3870 ADC PTR
3880 STA ENDPTR
3890 LDA #0
3900 ADC PTR+1
3910 STA ENDPTR+1
3920 NOTFOUND2 INC FOUNDFLAG;if 255, then 0
(found), else 1 (not found)
3930 SEC;get size of line
3940 LDA ENDPTR
3950 SBC BEGPTR
3960 STA LINESIZE;put it in LINESIZE
3970 LDA ENDPTR+1
3980 SBC BEGPTR+1
3990 STA LINESIZE+1
4000 INC LINESIZE
4010 BNE NOINC3
4020 INC LINESIZE+1
4030 NOINC3 RTS
4040 ;skip past end of line
4050 EOL LDY YSAVE;restore Y
4060 SRCHEND LDA (PTR),Y;get character
4070 CMP #155
4080 BEQ ENDLINE;if zero (EOL)
4090 INY;bump up pointer
4100 BNE SRCHEND;zero?
4110 INC PTR+1;next block
4120 NOADJ JMP SRCHEND;end of line?
4130 ENDLINE RTS
4150 ;Print message
4160 PRMSG STA PTR;prepare pointer
4170 STY PTR+1
4180 LDY #0
4190 PRLOOP LDA (PTR).Y;get msg char
4200 BEQ OUTMSG;zero (end of message)
4210 JSR PRINT;else print char
4220 INY;continue loop
4230 BNE PRLOOP
4240 OUTMSG RTS
4260 ;FINDLINE has initialized BEGPTR, ENDPT
R, and LINESIZE
4270 DELETE LDA ENDPTR;move FROM [end of lin
4290 ADC #1
4300 STA FROML
4310 LDA ENDPTR+1
4320 ADC #0
4330 STA FROMH
4340 LDA BEGPTR;to beginning of line
4350 STA DESTL
4360 LDA BEGPTR+1
4370 STA DESTH
4380 SEC:length of move is TEXEND-ENDPTR
4390 LDA TEXEND
4400 SBC ENDPTR
4410 STA LLEN
4420 LDA TEXEND+1
4430 SBC ENDPTR+1
4440 BCS ZLAST
4450 LDA TEXEND
4460 BEQ NODEC2
4470 DEC TEXEND+1
4480 NODEC2 DEC TEXEND
4490 JMP NOMOV
4500 ZLAST STA HLEN
4510 ORA LLEN
4520 BEQ Sk;IPDEL;nothing to move'.
4530 JSR UMOVE
4540 NOMOV SEC
4550 LDA TEXEND;subtract size of deleted lin
e from program end
4560 SBC LINESIZE
4570 STA TEXEND
4580 LDA TEXEND+1
4590 SBC LINESIZE+1
4600 STA TEXEND+1
4610 SKIPDEL RTS;delete done!
4630 INSERT LDA BEGPTR;insert gap at found l
4640 STA PTR;also set pointer
4650 STA FROML;move From BEGPTR
4670 ADC INLEN;to BEGPTR+INLEN+l
4680 STA DESTL
4690 LDA BEGPTR+1
4700 STA PTR+l:same for high
4710 STA FROMH
4720 ADC #0
4730 STA DESTH
4740 SEC;# of bytes to move is
4750 LDA TEXEND;(TEXEND-BEGPTR)+1
4760 SBC BEGPTR
4770 STA LLEN
4780 LDA TEXEND+1
4790 SBC BEGPTR+1
4800 STA HLEN
4810 BCS NOTLAST
4820 LDA TEXEND
4830 BNE NODEC
4840 DEC TEXEND+1
4650 NODEC DEC TEXEND
4860 JMP INSEXIT
4670 NOTLAST ORA LLEN
4880 BEQ INSEXIT;nothing to insert!
4890 NOINC2 JSR DMOVE;do insert
4900 INSEXIT SEC;add length of line added
4910 LDA TEXEND;to end of text pointer
4920 ADC INLEN
4921 STA TEXEND
4940 LDA TEXEND+1
4950 ADC #0
4960 STA TEXEND+1
4970 LDY #0;gap ready, put in line
4980 INSLOOP LDA BABUF,Y
4990 STA (PTR),Y
5010 CPY INLEN
5020 BCC INSLOOP
5930 BEQ INSLOOP
5040 RTS;insert done!
5050 CLOSEIT LDA FNUM
5060 BEQ NOCLOSE
5070 JSR CLOSE
5080 NOCLOSE JSR CLRCHN
5100 ERRPRINT LDA ST
5110 STA TMP
5120 JSR CLALL
5130 LDA #<ERRMSG
5140 LDY #>ERRMSG
5150 JSR PRMSG
5160 LDX TMP
5170 LDA #0
5180 JSR OUTNUM
5190 JSR PRNTCR
5210 PMSG .BYTE 155
5220 .BYTE "LADS Ready."
5230 .BYTE 155 0
5240 SYNMSG .BYTE 25-3
5250 .BYTE "Syntax Error"
5260 .BYTE 155 0
5270 ERRMSG .BYTE 253
5280 .BYTE "Error - "
5290 .BYTE 0
5300 BRKMSG .BYTE "BRK from "
5310 .BYTE 0
5330 GETNUM STA $F2
5340 INC $F2
5350 LDA #<BABUF;point to line buffer
5360 STA $F3
5370 LDA #>BABUF
5380 STA $F4;offset should be in $f2
5390 JSR $D800;convert ASCII to floating poi
5400 BCS NUMERR
5410 JSR $D9D2;floating point to integer
5420 LDA $F2;store pointer to first non-nume
5430 STA INDEX
5450 NUMERR LDA #0;clear result
5460 STA $D4
5470 STA $D5
5490 GETLNUM JSR GETNUM;Get number from BABU
5500 LDA $D4;put it in LNUM
5510 STA LNUM
5520 LDA $D5
5530 STA LNUM+1
5540 JSR FINDLINE;find the line
5560 LIST JSR GLIST
5570 JMP PROMPT
5580 GLIST LDA ARGPOS;Any arguments?
5590 CMP INLEN;not if.argpos is at end of li
5600 BNE YESARG
5610 JMP MLIST;so list all
5620 YESARG JSR GETLNUM;get first numeric ar
5630 LDA BEGPTR;list from beginning of first
5640 STA SAVBEG;save beginning pointer
5650 LDA BEGPTR+1
5660 STA SAVBEG+1
5670 LDA ENDPTR;save end of first line
5680 STA SAVEND
5690 LDA ENDPTR+1
5700 STA SAVEND+1
5710 LDA INDEX;point to second argument
5720 CMP INLEN;if equal, no second argument
5730 BNE YESARG2
5740 LDA FOUNDFLAG;no second arg, so check f
or legal line
5750 BNE NOLIST;line wasn't found, so don't
5760 LDA SAVEND;restore end of line
5770 STA ENDPTR
5780 LDA SAVEND+1
5790 STA ENDPTR+1
5600 JMP OVER2;and skip
5810 YESARG2 JSR GETLNUM;get second line num
5820 OVER LDA SAVBEG
5830 STA PTR
5840 LDA SAVBEG+1
5850 STA PTR+1
5860 SEC;calculate length
5870 LDA ENDPTR
5880 SBC PTR
5890 STA LLEN
5900 LDA ENDPTR+1
5910 SBC PTR+1
5920 STA HLEN
5930 BCS GOLIST;if second # < first#_, don't
5940 NOLIST RTS
5941 GOLIST LDA FOUNDFLAG:BNE NOINCH
5950 INC LLEN
5960 BNE NOINCH
5970 INC HLEN
5980 NOINCH JMP INLIST
6000 OPENFILE CLC
6010 LDA ARGPOS
6020 ADC #<BABUF
6030 STA FNAMEPTR;point to filename
6040 LDA #0
6050 ADC # >BABUF
6060 STA FNAMEPTR+1
6070 LDY ARGPOS;find end of filename
6080 GETFNAME LDA BABUF, Y
6090 CMP #155;end of line?
6100 BEQ ENDFNAME;if so, exit loop
6110 CMP #44;end of filename?
6120 BEQ ENDFNAME;also legal
6140 BNE GETFNAME;if no delimiter found...
6150 JMP SYNERR;it's a syntax error
6160 ENDFNAME TYA;convert Y pointer to lengt
6180 SBC ARGPOS;Y-argpos
6190 STY ARGPOS;reset argpos for list
6200 STA FNAMELEN;filename length
6210 LDA #7;CLOSE #7
6220 STA FNUM
6230 JSR CLOSE
6240 LDA #0;OPEN #7,n,O_.filename
6250 STA FSECOND
6260 JSR OPEN;do open
6270 LDX ST:chectt for error
6280 BMI ERRABORT;yes, error
6300 ERRABORT PLA;disk error, so abort
6320 JSR ERRPRINT
6330 JMP PROMPT
6340 SAVE LDA #8;8 means output
6350 STA FDEV
6360 JSR OPENFILE;open the file
6370 LDX FNUM;all PRINTS go
6380 JSR CHKOUT; to file
6390 JSR GLIST;send out listing
6400 JSR CLOSEIT;close file
6410 JMP PROMPT
6420 MERGE LDA #4;4 for input
6430 STA FDEV
6440 JSR OPENFILE;open it
6450 LDX FNUM;all input comes from this file
6460 JSR CHK I N
6470 JMF ENTER:file will be closed automatic
6480 LADS LDA ARGPOS;Any argument?
6490 CMP INLEN
6500 BNE NOTMEM;if argpose<>inlen, then there
is,so don't change RAMFLAG
6510 INC RAMFLAG
6520 NOTMEM JMP START
6530 SYS LDA ARGPOS;locate number
6540 JSR GETNUM;get it
6550 LDA $D4;put address directly
6560 STA JUMPVEC+1;into code
6570 LDA $D5;self-modifying!
6580 STA JUMPVEC+2
6590 JUMPVEC JSR $FFFF;this address will be
changed by above
6600 JMP PROMPT
6610 LOAD JSR PLOAD;do load
6620 JMP PROMPT;done
6630 PLOAD LDA #4;4 for read
6640 STA FDEV
6650 JSR OPENFILE;open file
6660 AFTEROPEN LDA FNUM;all input comes from
6670 JSR X16
6680 LDA #<TEXTBAS
6690 STA ICBADR,X
6700 LDA #>TEXTBAS
6710 STA ICBADR+1,X
6720 LDA #0
6730 STA ICBLEN,X
6740 LDA #$50
6750 STA ICBLEN+1,X
6760 LDA #7
6770 STA ICCOM,X
6780 JSR CALLCIO
6790 LDA FNUM
6800 JSR X16
6810 CLC:add buffer length to get ending add
6820 LDA ICBLEN,X
6830 ADC #<TEXTBAS
6840 STA TEXEND;update end
6850 LDA ICBLEN+1,X
6860 ADC #>TEXTBAS
6870 STA TEXEND+1
6880 LDA ST
6890 CMP #136;end of file?
6900 BEQ NOPRERR;if so, don't print an error
6910 JSR ERRPRINT
6920 JMP PROMPT
6930 NOPRERR JSR CLOSEIT;close down file
6940 RTS;end of load
6950 BREAK CLI:LDA #<BRKMSG:LDY #>BRKMSG:JSR
6960 PLA:PLA:PLA:SEC:SBC #2:TAX:PLA:SBC #0:J
6965 LDX #255:TXS:JSR PRNTCR:JMP EDIT
6970 .FILE D:TABLES.SRC
Atari Machine Language Programming
There is a lot to be learned from the Atari LADS source code. Both the assembler and the editor are complex, powerful programs. You might find uses in your own programming for such general-purpose routines as Valdec, UMOVE, and DMOVE. You can add functions to the editor such as search and replace. Or you could simply bypass the editor altogether, creating LADS-compatible source files using an ordinary word processor (and thus have access to the search and replace and other features of the word processor program).
Since maps are invaluable in sophisticated ML programming, you might want to purchase Mapping the Atari (COMPUTE! Books, 1983).
Special Apple Notes
The Apple version of LADS works the same as the Commodore 64 version with only slight modifications. The Apple doesn't have the convenience of Kernal routines to access DOS, so routines had to be written which could directly access the DOS file manager routines. This required extensive changes to the Open1 subprogram, which are discussed below.
Also, because the Applesoft tokenize routine takes the spaces out of the text, it was necessary to put a wedge into Apple's CHRGET routine to intercept the BASIC tokenize routine. And the wedge includes a routine that puts the filename of the program you want to assemble to the top of the screen where LADS expects to find it.
Apple Disk Access
The Apple DOS file manager is the part of DOS that handles all file input and output to the disk. It calls RWTS (Read/Write to Track/Sector) and is called from the command interpreter. The command interpreter sends control bytes to the file manager through the file manager parameter list. You can access the file manager directly by sending it the parameters it requires.
To get the address of the parameter field you JSR to $03DC. This loads the Accumulator with the high byte and the Y Register with the low byte of the parameter field. You can then store these to a zero page location for easy transfer of the parameters.
Table 11-1. Apple File Manager Parameter List
|Read 1 Byte
READ 1 Byte
|WRITE 1 Byte
WRITE 1 Byte
Note: The numbers in the leftmost column represent the opcode; the numbers across the top of this chart represent byte positions relative to the start of the parameter list. Asterisks signify that a byte is required for the operation listed. A blank space means that this parameter can be ignored. Nevertheless, the byte positions must be maintained. For example, to DELETE, you do not need to worry about the second, third, or fourth bytes-anything can be in thembut they must exist. The first byte must contain a five, and the fifth through the eighteenth bytes must be set up as described below.The parameters are expained in sections. The first section tells you about all the opcodes except for the read, write, and positions opcodes, because they are slightly different from the rest. The second section tells you about the read, write, and position opcodes; the third, about the last set of parameters that is common to all opcodes.
The first byte of the parameter field is the opcode type. This parameter can be in the range of 1 to 12.
The second parameter is used only with the INIT opcodes. If you are using a 48K Apple, the correct value for this parameter is 157.
The third and fourth parameters are used with the OPEN and RENAME opcodes. Together they hold the record length of a random access file. If you are not using a random access file, you should have a zero in both of these locations. With the RENAME opcode, these bytes hold the address of the new name.
The fifth byte holds the volume number. The sixth byte holds the drive number. The seventh byte holds the slot number. The eighth byte holds the file type.
The ninth and tenth bytes hold the address of the filename. The filename must be stored in the address pointed to by these bytes. It must be padded with spaces.
This section explains the read, write, and position opcodes.
The first byte holds the opcode. The second byte holds the subcode.
The next four bytes are used only when you require a position command. The third and fourth bytes hold the record number. The fifth and sixth bytes hold the byte offset. To reposition the pointer in an open file, you can use these bytes to calculate a new position. The new position is equal to the length of the file specified in the open opcode times the record number plus the byte offset.
The seventh and eighth bytes hold the length of the range of bytes. This is used only when reading or writing a range.
When reading or writing a range of bytes, the ninth and tenth bytes hold the start address of the range. If you are reading or writing only one byte, then the ninth byte holds the byte you read or the byte you are going to write.
The following are parameters for all the opcodes.
The eleventh byte is the error byte. It should be checked each time after you access the file manager. The errors are as follows:
0: NO ERROR
2: INVALID OPCODE
3: INVALID SUBCODE
4: WRITE PROTECTED
5: END OF DATA
6: FILE NOT FOUND
7: VOLUME MISMATCH
8: I/O ERROR
9: DISK FULL
10: FILE LOCKED
The twelfth byte is unused. The thirteenth and fourteenth bytes are used for the address of the work area buffer. This is a 45-byte buffer in one of the DOS buffers.
The fifteenth and sixteenth bytes hold the address of the track/sector list sector buffer. This is a 256-byte buffer in one of the DOS buffers.
The seventeenth and eighteenth bytes hold the address of the data sector buffer. This is another 256-byte buffer in one of the DOS buffers.
Once you have sent the correct parameters, you can call the file manager by a JSR to $03D6. You must specify if you want to create new file on disk if the one you are accessing doesn't exist. This is done by loading the X Register with a 0. If you don't want to create a new file, you can load the X Register with a 1. If you don't want to create a new file and you try to access a file that doesn't exist, you will receive an error number 6 in byte 11 of the parameter field.
Apple LADS uses the routines in the file manager that read or write one byte from or to the disk at a time. The general routine to transfer the parameters from Tables to the file manager can be found between lines 810 and 920 in the Open1 listing. This is called from the individual subroutines for opening, closing, reading, and writing. The OPEN routines require a filename. Lines 580-800 handle the transfer of the filename from the filename buffer to the specific buffer.
There is also a check to see whether a file about to be opened has been opened previously. This was needed because you cannot close a file unless it was previously opened. This is handled in the close routine (370-570).
The PRINT routine handles all output, and the CHARIN routine handles all input. There is one input and one output channel, and all input and output must be handled through a channel. The bytes which govern this event are set in the CHKIN and CHKOUT routine. The CHKIN routine (930-940) sets all input to come from that file. The CHKOUT routine (950-1030) sets all output to go to that file. The PRINT routine (1170-1430) and the CHARIN routine (1040-1160) check to see what channel is currently open, then go to that routine.
The BASIC wedge (1700-2530) handles the tokenizing of the BASIC text. It checks to see if the text pointer is at $200 (the input buffer). If not, it goes to the normal GETCHR routine. Otherwise, it checks to see if the first character is a number. If so, it goes to the insert line routine, and if not, it checks for the characters ASM. If that is found, the wedge concludes its work by putting the filename at the top of the screen and jumping to the start of LADS.
The insert line routine gets the line number, then jumps to the Apple tokenize routine, which loads the Y Register with the length of the line plus six and then jumps to the normal line insert and tokenize routine.
The last subroutine in Open1 is the first thing that is called when you BRUN LADS. It initializes the wedge and sets HIMEM to the start of LADS.
Return to Table of Contents | Previous Chapter | Next Chapter