Welcome to the Datatron 205 and 220 Blog

This blog is a companion to T J Sawyer's Web Page that outlines the history of the Burroughs Datatron 205 and Paul Kimpel's incredible 205 and 220 emulators. Please visit those sites and return here to post any comments.
Showing posts with label assembler. Show all posts
Showing posts with label assembler. Show all posts

Sunday, August 9, 2020

Recovering the 220 BALGOL Compiler

The retro-220 emulator for the Burroughs 220 computer system has been stable for almost two years. The recovered Burroughs Algebraic Compiler, better known as BALGOL, has been stable for nearly as long. Previous posts for this blog ([1], [2]) have discussed the architecture of the 220 and the retro-220 emulator. Other posts ([3], [4]) have discussed the syntax and features of BALGOL, as well as the preliminary version of the Algol programming language, Algol-58, upon which BALGOL was based.

The story that has not yet been told is how I recovered the BALGOL compiler and made it run again.

Compiler Assets and Components


The BALGOL compiler was first released by Burroughs in March 1960, then received a significant update in February 1962. The compiler was distributed to customers as a magnetic tape, termed the "Generator Tape," along with a few small card decks used for bootstrapping and utility functions.

The 220, first shipped in 1958, was one of the last of the major vacuum tube systems, and quickly became obsolete. Considering that the 220 had a highly unusual magnetic tape subsystem, the possibility that any software in machine-readable form may have survived to the present is remote.

There was another form of release for the compiler, however. For some reason, Burroughs generated complete assembly listings of the 1962 compiler, its standard library routines, and a configuration utility, then made sets of copies in 8.5x11-inch format. They must have made a lot of copies, apparently to be given away. Michael Mahon has told me that there was still a stack of these copies in a closet at the Pasadena plant when Burroughs decommissioned their internal 220 systems around 1968. He says he grabbed one of the copies and still has it.

Another person who had a copy and retained it was Donald Knuth. In 2005, Professor Knuth loaned a large number of documents from his files to the Computer History Museum in Mountain View, California. These documents were scanned by the museum, which then made the resulting PDFs publicly available on its web site [5]. Among those documents is Knuth's copy of the BALGOL compiler listings [6]. That PDF is about 17MB in size and comprises 267 pages.

It was the CHM scan of Professor Knuth's copy of the BALGOL listing that I used to recover the compiler source code and restore the compiler to operation.

The compiler source consists of four main parts:
  1. Compiler main module, 4760 lines, 4663 cards.
    This is the one-pass BALGOL translator, which parses the language text and generates machine instructions for the 220.
  2. Compiler overlay module, 2431 lines, 2388 cards.
    This is the second part of the compiler, which merged into the object program any required machine-language routines from the standard library, any optional user-supplied machine language routines on punched cards, and the code for any debugging aids specified in the BALGOL source (tracing, monitoring, symbolic dump). It produced a loadable program on magnetic tape.
  3. Standard library routines, 1466 lines, 1381 cards.
    The standard BALGOL library of 28 subroutines, including math routines, integer/floating conversion routines, formatted I/O (input/output) routines, and debugging support. The standard routines are described in Appendix G of the compiler reference manual cited below.
  4. Generator program, 2215 lines, 1954 cards.
    This was a "sysgen" program that, using the Generator Tape, created customized tapes from which the compiler could be run. It could prepare the compiler for a given computer configuration, update the standard library with new or modified routines, replace the default punched-card I/O interfaces for the compiler and library, and modify some aspects of the way the compiler operated.
The number of "lines" cited above reflects the number of printed lines on the scanned listings. Due to assembler-generated printing (extra spacing, multiple words generated by pseudo operators, etc.), the listings have more lines than would have been input as source code to the assembler. The number of "cards" reflects the number of lines of assembly source code that were extracted from the transcribed listings.

In addition to these listings, there are Burroughs 220 documents on the bitsavers.org web site [7]. The most important of these is the compiler reference manual from March 1963, 220-21017_B220_BALGOL_Mar63.pdf, prepared for the 1962 version of the compiler represented in Professor Knuth's listing. In addition to these documents, the bitsavers.org site has an older version of the reference manual and an incomplete listing of the compiler and library from 1961.

Thus, when starting the compiler recovery project, we had what appeared to be listings of the complete compiler, library, and generator program (this eventually proved to be true), and good documentation for the compiler, library, and generator program. But here's what we did not have:
  1. The source code in machine-readable form.
  2. Assembly software for the source code. The compiler main, overlay, and library were all written for one assembler. The generator program was written for an entirely different assembler.
  3. Documentation in any form for either assembler. Not even their names were known.
  4. Information on how the Generator Tape was formatted.
It therefore appeared that I would need to transcribe all of the text from the scanned listings (totaling almost 11,000 lines of assembly code), reverse-engineer the two assembler notations and write two different assemblers from scratch, assemble the 31 source modules (compiler, overlay, generator, and 28 library routines) into a form that could be used to construct a Generator Tape, somehow deduce what the format of that tape needed to be, combine the assembled modules to construct an initial tape, and from that use the generator to create a loadable compiler tape. Having done all that, I would then be ready to find some BALGOL source code and start debugging the whole mess. At the time, I found this list of tasks to be somewhat daunting.

Recovering the Source Code


I began transcribing the listings in early November 2016, before I started work on the retro-220 emulator at the end of that December. Initial transcription, proofing, and correction of the transcribed text continued through May 2017. I was usually able to transcribe four pages a day, about 180 lines, or proof about double that amount. I found that trying to do more than that at one sitting simply resulted in introducing more errors during transcription or failing to catch them during proofing, so I took my time and tried to work carefully.

The assembly listing for the generator program is in the traditional form, with memory addresses and words of assembled object code on the left, and the text of each line of assembly source on the right. My approach with listings of this type -- probably a common one -- is to transcribe the listing completely, as is, including the addresses, object code words, spacing, and any additional output by the assembler. The purpose of this is to ease the job of proofing the transcription. After extracting card images for the source code from the transcribed text and assembling it, you can capture the assembly listing as a text file and mechanically compare it to the text file from the transcription. Any differences between the two are easily identified. The assembler symbolic instructions and any comments will of course match, but if the assembled addresses and object code words also match, then there is a good probability that the symbolic instructions were transcribed correctly.

Before attempting to assemble the transcribed generator code, I completed a manual proofing pass to catch obvious errors and as many typos in the comments as I could. Next, I extracted a text file of card images from the transcription containing the symbolic instructions and comments in a form that could be input to the assembler discussed below. I wrote scripts to do this. Then I iterated between assembly runs and correction of the transcribed text until the assembler output matched the transcriptions. This is a fairly reliable proofing process, but it is not guaranteed to catch everything.

Proofing the transcription of the remaining modules, which comprise 80% of the lines, was more problematic. These listings appear to be from a first pass of the assembler. They show the memory addresses assigned to each symbolic instruction, but not the words of generated object code. That meant a mechanical match between the transcribed text and assembler output could only detect address assignment errors, not errors in the symbolic instructions. That in turn meant that for these modules I had to do a much more careful job of manual proofing.

In the end, I had to do two complete proofing passes on the compiler, overlay, and library transcriptions -- the second one after I had started to test the compiler with sample programs and was running into serious problems. I also made at least one extra proofing pass on the main tables in the compiler, which are dense and cryptic. The correctness of those tables are absolutely essential to the compiler's operation.

Creating Assemblers from Scratch


Without documentation on either of the assemblers required to reconstruct the compiler, I had to puzzle out for myself both of the symbolic assembly notations and from that design programs to parse each notation and generate the necessary words of object code and data. These programs were not intended to be recreations of the original assemblers, because those assemblers likely had features that were not used in the transcribed source code, and there was no way to know that those features had existed. Instead, these assembly programs were intended to support just whatever was necessary to successfully assemble the transcribed source code, plus whatever additional features I thought (a) might be useful for assembling 220 programs in general, and (b) would not be too difficult to implement.

The BAC-Assembler


The assembler that Burroughs used for the compiler, overlay, and library routines is fairly straightforward, at least as it was used with those modules. It has a classic columnar assembler format, with fields for an optional label, a mnemonic code for the hardware or pseudo instruction, an optional sign digit to be applied to the assembled word, and an operand field consisting of comma-delimited symbolic expressions for addresses and variant fields. The remainder of the source line, delimited by a space after the operands, is ignored and may be used for comments.

The mnemonic codes for the hardware instructions matched those documented in the 220 Operational Characteristics manual (in [7]), except for a few additional ones. These few, however, appeared to be convenience mnemonics for specialized forms of other instructions. For example, from its context, "BZA <address>" appeared to stand for a "branch if the A register is zero" instruction. There is no such instruction for the 220, but there is a "BFA <address>,<sL field>,<digits>" instruction (branch if the <sL field> of the A register matches <digits>). If <sL field> and <digits> are both zero, this is effectively a branch-on-zero instruction. These convenience mnemonics were easy to accommodate in the assembler.

To understand the operand formats, I wrote scripts to extract the operation codes and number of operand fields from the transcribed text and tabulate the frequencies of operation codes and their numbers of operands. This helped identify the operands that were required for each instruction, as well as cases where operand fields are optional.

The tabulation of mnemonic codes also helped identify the pseudo instructions used by the compiler modules. There were several of these, with most used for typical assembler functions such as defining the value of a symbol or label, setting the next memory address to be assigned, constructing data words from constant values, and signaling the end of the program.

The only challenging pseudo was FBGR, which generates the 29 words of digit codes for a format band used by the 220's Cardatron punched-card interface. See Chapter 6 in the Operational Characteristics manual (in [7]). The coding of a format band is quite complex. FBGR considerably eases that task by producing the necessary digit patterns from a list of parameters that are somewhat like COBOL PICTURE clauses. Fortunately, this assembler's formatting notation was very similar to that used in the Shell Assembler for the Burroughs 205 [11] and its Cardatron interface, so I found it relatively easy to implement this pseudo.

I christened the assembler for this symbolic notation the "BAC-Assembler," from the product code that Burroughs assigned to the BALGOL compiler, BAC-220. It is actually a cross-assembler, as it does not run in the 220 environment. The assembler is written in Javascript and runs from a web page in a standard browser. It can be run directly from the retro-220 project's hosting site, or downloaded from the project repository and run locally. Documentation and instructions for its use are in the retro-220 project wiki [12].

Although the BAC-Assembler was developed specifically to assemble the compiler and library modules, it has enough features that it can be used for general 220 programming. It generates absolute machine code that can be loaded into the retro-220 emulator and executed directly. It supports the following output formats:
  • A standard, self-loading Cardatron format band 6 card deck. Format 6 is a hardwired band used to load numeric words of data.
  • A self-loading paper tape image.
  • A "machine language" (ML) card deck. This format is used by the compiler for machine-language routines to be merged with BALGOL programs at compile time, and by the generator program for updating the standard library. It is described in Appendix F of the compiler reference manual.
  • A "media" card deck. This format is used with the generator program's INPUTMEDIA and OUTPUTMEDIA specifications to replace the default Cardatron I/O routines in the compiler and library. This format is documented in Appendix F of the compiler reference manual. See the BALGOL-INPUTMEDIA-OUTPUTMEDIA folder [14] in the retro-220 project repository for an example.
  • A magnetic tape image.
The last three formats were implemented to support reconstruction of the compiler and generator program. The first two were implemented as a convenience for general 220 programming.

The GEN-Assembler


The assembler that Burroughs used for the generator program appeared to be quite a bit more sophisticated than the one used for the compiler and library. The columnar format it used was similar to but not compatible with that of the BAC-Assembler. This assembler supported more complex symbolic expressions as well as a more sophisticated notation for literal operands.

From an analysis of operation codes and operand formats like the one done for the BAC-Assembler, this assembler used the same hardware operation codes, although there were a few additional convenience mnemonics. It is likely that this assembler had a macro processing facility, as the generator source has many instances of a DO operator that generated two instruction words to execute a subroutine call. The cross-assembler discussed below simply implemented DO as a special pseudo operator.

This assembler also had a richer set of pseudo operators compared to the BAC-Assembler. It had equivalents for all of the ones mentioned above for BAC-Assembler, but in addition, ones to assign assembly addresses starting at one location but store the assembled code at another, specify the location of the literal pool, erase the internal symbol table and start a fresh assembly, construct words from multiple sub-fields, fill a range of words in memory with a constant value, and generate sequences of data words from lists of constant values and expressions. It had a FORMAT pseudo for generating Cardatron format band data similar to FBGR for the BAC-Assembler, but fortunately the notation was the same and I was able to transfer the FBGR implementation from BAC-Assembler with almost no change.

Since it was designed to reconstruct the generator, I christened this cross-assembler the "GEN-Assembler." It is also written in Javascript and runs in a standard web browser. Like the BAC-Assembler, it can be run from the retro-220 project's hosting site or downloaded and run locally. The user interface is identical to BAC-Assembler, and it can output object code in the same formats. It is in many ways a more capable assembler than BAC-Assembler, and is at least as well-suited for general 220 programming. Documentation and instructions for its use are also in the retro-220 project wiki [13].

Pre-Loading Cross-Assembler Literal Pools


Something that proved to be necessary with both cross-assemblers was a capability to pre-load the literal pool. A literal pool is a block of data the assembler constructs to hold values that are defined as literals in the operand fields of instructions. Rather than have the programmer manually declare a constant value, give the address of that constant a symbolic name, and then reference that name in operand expressions, literal pools allow the programmer to simply write the literal directly in an operand expression. The assembler automatically allocates storage for the value and essentially assigns the literal itself as the value's symbolic name. Multiple references to the same literal value throughout a program result in addressing the same value in the pool.

For example, the following instruction occurs at address 3182 in the compiler main module:
CAD   +6034037172
The assembler recognizes +6034037172 as a literal, assigns it address 4113 in the literal pool, assembles the value of the literal in that location, and assembles that CAD (Clear and Add) instruction with the pool address, resulting in an object word of 0 0000 10 4113.

A literal pool is not difficult to implement in an assembler, but what proved to be impractical was allocating the pool words at the same physical addresses as the original assembler did. The ordering of literal address assignments was probably some function of the original assembler's symbol-table structure and symbol lookup mechanism. Although assigning different pool addresses would not affect the correctness of the program, it produced undesirable differences in the mechanical matching of the generated assembler listing back to the transcription of the original documents.

I tried a few fairly obvious address assignment schemes without success before deciding that this was a problem not worth trying to solve the same way the original assembler did. Instead, I simply extracted the pool addresses and values from the transcriptions and designed a JSON format to hold them. You then load a file containing this JSON into the cross-assembler at the start of the assembly process to pre-populate the assembler's literal pool table with values at the specified addresses. This is a feature specifically intended for recovering the compiler and generator, and is not something you would expect to use with either cross-assembler for regular 220 programming.

Pre-loading the literal pool proved to be useful for resolving another problem. In the generator program there was one literal expression involving a symbolic value that was a forward reference to a symbol that in turn was defined after the point where the assembler was instructed to place the pool. See the LDB instruction at card sequence 1903, address 4310, with the literal operand "=BUF+1=".
 
The location of symbol BUF after the location of the pool, and the fact that the value of the symbol depended upon the size of the pool, meant that the literal value was also dependent on size of the pool. I could never get the assembler to assign locations correctly after the point where the pool was emitted to the object code. Since I was more in the business of recovering a compiler than writing an assembler, I decided not to try to fix what amounted to a GEN-Assembler design deficiency and let pre-loading of the pool resolve that problem for me.

Constructing the Generator Tape


After transcribing and proofing the original listings, writing two cross-assemblers, and getting everything to assemble and match back to the transcriptions, the next task was to take the assembled object code and construct a Generator Tape. The Generator Tape cannot be used to run the compiler; rather the generator program constructs a loadable compiler tape from it.

This task was not the most time-consuming part of the project, but it proved to be one of the most complex and challenging ones. Not having access to any documentation or an actual Generator Tape, the only way to approach the problem was to try to deduce from the generator program the layout of the tape and try to reproduce it.

The 220 Magnetic Tape Subsystem


Before discussing the Generator Tape, it will be useful to discuss the 220 magnetic tape subsystem, because it was quite different from the IBM standard that later became dominant, and the compiler took advantage of that subsystem's unique characteristics.

The tapes themselves were 3/4-inch Mylar on 3500-foot reels. The drive operated at 120 inches/second, recording at 208.3 bits/inch. Each 11-digit 220 word was recorded on the tape as 12 four-bit BCD (binary coded decimal) digits, with a "spacer" digit between words. Each six-bit recording frame on the tape held a BCD digit, an even-parity bit, and a control bit used to detect block boundaries. No longitudinal block-parity checking was used.

The tapes were recorded in two parallel, six-bit "lanes," a feature brought forward from the Burroughs 205 tape subsystem. There were two tape heads, but a drive could read or write only one lane at a time. Several tape instructions provided for switching between lanes, a process that took about 70ms.

Blocks could contain 10 to 100 words. Importantly, the design of the block format allowed drives to overwrite existing blocks reliably, something you could not do with the IBM format. Once a block was written, though, that structure was locked down. To overwrite the tape with a different block structure, the entire tape first had to be erased, something the drive could do off line. There were two forms of tape write instruction: MIW was an "initial" write, and only worked on erased tape. MOW was an "overwrite," only worked on existing blocks, and had to write data of exactly the same length as the existing block.

Single tape instructions could read or write up to 10 blocks at a time. The tape control unit also had two forms of search that operated independently of the processor. A "search" considered the first word of a block to be a numeric keyword and could search the tape bidirectionally, stopping at the first block with a keyword greater than or equal to a value provided by the search instruction. This obviously worked best if the keywords on tape were arranged in monotonically ascending order. A "scan" worked only in the forward direction, but would stop when it found a matching value in one of the first 10 words in a block. Due to the overwrite and search capabilities, 220 applications tended to use magnetic tape more like a semi-random device than a purely sequential one.

Because of the difference between the initial and overwrite modes of writing to tape, it was common practice for applications to be designed to work with pre-formatted tapes having blocks of a constant size. Because larger block sizes used tape more efficiently, it was also common to format tapes with the maximum block size.

This was the case with the BALGOL generator and compiler. Both the Generator Tape and the compiler tapes created from it required magnetic tapes that were pre-formatted with 100-word blocks. The BALGOL generator and compiler used both the block overwrite and the search capabilities.

Constructing the Compiler and Overlay


Appendix A in the compiler reference manual has a good description for creating a loadable compiler tape from the Generator Tape. It mentions using a bootstrap card deck, the "Generator Callout program," but this deck was apparently distributed only on cards and we did not have it. The operating instructions for the compiler in Appendix B mentions a similar deck for the "Compiler Callout program," which we did not have either. Fortunately, though, that appendix shows the code for an equivalent paper-tape compiler bootstrap routine:
6 1000 04 0002       PRB   2      READ BOOTSTRAP INTO ADDR 2
0 0000 39 0000 SOR DO NOT HALT ON OVERFLOW
0 2008 50 0000 MRW 002 REWIND TAPE 2, SWITCH TO LANE 0
0 2001 52 0000 MNC 0,2,0 READ 10 BLOCKS FROM TAPE 2 TO ADDR 0
6 0000 30 0002 BUN LOAD BRANCH TO LOAD ROUTINE AT ADDR 2
From that five-instruction paper-tape bootstrap it was easy to understand both how the compiler loaded and how to construct a similar bootstrap routine for punched cards:
0 0000 39 0000       SOR          DO NOT HALT ON OVERFLOW
0 2008 50 0000 MRW 002 REWIND TAPE 2, SWITCH TO LANE 0
0 2001 52 0000 MNC 0,2,0 READ 10 BLOCKS FROM TAPE 2 TO ADDR 0
0 0000 30 0002 BUN LOAD BRANCH TO LOAD ROUTINE AT ADDR 2
From understanding the way the compiler loaded, I could see that the generator program loaded the same way. Both bootstraps read the first ten blocks (1000 words) from tape to memory starting at address 0000 and then branched to address 0002, which is the start of a routine that loads the rest of the program, validates and strips out the checksum words embedded in the code, and then branches to the beginning of the program proper. Thus, these small bootstrap programs were the first clues as to the layout of both the Generator and loadable compiler tapes.

The routine at address 0002 in both the generator and compiler assembly listings is labeled "LOAD." Immediately after that routine is another one labeled "STORE." Inspection of that routine in both programs revealed that it writes the program from memory to tape, calculating and inserting checksum words in the manner expected by the LOAD routine. So here was the second clue -- if we could arrange to get the assembled object code for these programs into memory, all we need to do is execute a branch to the STORE routine in order to get that code written properly to magnetic tape.

The overlay module, as its name implies, is loaded by the compiler to overlay a portion of the compiler's memory. Thus it does not have a load routine of its own, but does have a corresponding (and unlabeled) store routine located at its end, starting at address 4000.

These embedded store routines were sufficient to get the generator, compiler, and overlay written correctly to the Generator Tape. The compiler and overlay are written to lane 0 on the Generator Tape, while the generator is written to lane 1. I eventually wrote a few small 220 programs to partially automate this process by positioning the tapes and calling the internal LOAD/STORE routines within the modules.

The progress thus far was a good start on constructing a Generator Tape, but there were a number of other details that still needed to be resolved, the most significant of which was getting the standard library onto the tape.

Constructing the Standard Library


The Generator Tape as distributed by Burroughs would store the object code for the standard library in the form required by the generator program. Loadable compiler tapes produced by the generator would store the library in the form required by the overlay module to select individual routines and merge them into a program's executable image.

During the process of creating a loadable compiler tape, the generator program has the ability to modify the standard library, allowing sites to build a compiler tape having a customized library. Sites could alter the existing routines, add new library routines of their own, or even delete routines they did not want.

"Modify" is perhaps too generous a word here, because the process is one of total replacement. The complete set of library routines to be on the compiler tape must be fed to the generator program on cards in the "machine language" (ML) format mentioned earlier. Thus, you need the ML card decks for all of the routines that will make up the library, including the ones you are neither modifying nor adding.

The set of object code cards for each library routine must be prefixed by a card with the name of the routine, the type of its return value if any, and a list of the types of its parameters if any. If a library routine references other library routines, the name card must be followed by an "equivalence" card for each referenced routine. Using the equivalence information, an address-coding convention in the ML format allows a library routine to reference locations at relative offsets within other library routines.

To make the process of updating the library easier, the generator supports an option to punch a card deck in ML format containing the object code for the entire library, complete with name and equivalence cards, in the format required for feeding back into the generator on a subsequent run. A site wanting to modify the library could thus start by punching this deck and then modifying only parts of that deck as needed.

I started with the idea of somehow constructing the library structures for the Generator Tape myself from the assembled output of the transcribed library routines. I tried to determine the structure of the library on tape from the generator program, but the handling of the parameter signatures and equivalence information involved a lot of quite complex code, and I had trouble understanding how those structures should be constructed.

But then I had a thought -- instead of trying to construct the library structures for the Generator Tape myself, would it be possible to get the library update feature of the generator program to do it for me? It appeared from looking at the generator and overlay code that the structure of the library on tape was the same for both the Generator Tape and the loadable compiler tapes. The library comes at the end of the Generator Tape, after the overlay module, so inserting the library should be simply a matter of appending the library blocks to the end of the tape.

I decided to try the following scheme:
  • Construct a preliminary Generator Tape with an empty library.
  • Manually construct the complete card deck for updating the library as required by the generator from the assembled output for each routine, inserting the necessary name and equivalence cards.
  • Use the generator program with the preliminary tape to create a compiler tape.
  • During the creation of the compiler tape, use the card deck to replace the (empty) library coming from the preliminary Generator Tape and thus output the library routines from the card deck to the compiler tape.
  • Finally, extract the tape blocks containing the library from that compiler tape and append them to the preliminary Generator Tape, thus creating a final Generator Tape.
To make a long story short, this approach worked. Of course, I had to assemble all 28 of the transcribed library routines individually to get their object code in ML card-deck format, as well as manually prepare their name and equivalence cards.

Fortunately, the information for the names and equivalences is embedded as constant strings in the source code for each of the routines, bracketed by words with special sign digits. For the ARCSIN function, it looks like this:
CNST  40000990000
CNST $ARCSIN,REAL(REAL) ARCTAN=1 ROMXX=2 ERROR=3 $
CNST 90000000000
The 40000990000 word signals the end of the object code in an ML deck (see Appendix F in the compiler reference manual), while the 90000000000 word signals the end of the name/equivalence information. The complete library deck for the ARCSIN function that would be fed to the generator program thus looks like the following. The digits 2 and 6 in the first column allow the generator to distinguish between name/equivalence cards and ML object cards. Note how the 40000990000 word is now the last one on the last card of the deck and the name/equivalence information has been dropped, as the generator will not tolerate that additional data.
2 ARCSIN, REAL(REAL)
2   ARCTAN=1
2   ROMXX=2
2   ERROR=3
60600000100000000000100006000040035280000410011600004402006000030020480000420000
60600000200006600003602396000040034960000100352600002503496000030010280000000012
602000003000122416249550040000990000
The presence of the bracketing words for the name/equivalence information suggests that Burroughs probably had a Generator Tape-builder program that would process the ML decks for the library routines, extract the name and equivalence information, drop the words following the 40000990000 word, and write the remaining code with the necessary library structures directly to the Generator Tape. Not having such a builder program (and not knowing enough to write one), I had to use a much clumsier process, but it worked, and was only a one-time effort.

When speaking of "cards" and "decks" throughout this post, I am of course not referring to actual punched cards. The retro-220 emulator uses plain text files to simulate card decks, with the individual lines in the files simulating individual card images. A sample "deck" for generating a compiler tape from a preliminary Generator Tape with card images for the full standard library is available as a text file in the retro-220 project repository.

Documenting the Process


As I worked through the process of understanding the layout of the Generator Tape and marshaling all of the parts that needed to go into it, manual generation of that tape started to become quite complex. At first I was keeping all of this in my head, but as I got further along, the complexity reached a point where I had to start writing things down.

Those notes evolved into a 12-page document that describes a 24-step procedure for constructing a Generator Tape, starting from only the transcribed source code and a few utility decks in the retro-220 project's GitHub repository [9]. That document is available in the project wiki [10].

It is no longer necessary for anyone to work through that 24-step procedure, unless the Generator Tape needs to be corrected. To make using the compiler, creating customized compiler tapes, or reconstructing the Generator Tape easier, the following files are available in the project repository [9]:
  • Two loadable compiler tape images -- one configured for a system with 5KW of memory (the minimum the compiler will support) and another for 10KW (the maximum the 220 supported).
  • A pre-built Generator Tape image for the generator, compiler, and library in their current state.
  • Files for the various bootstrap and utility card decks.
  • Pre-assembled files of object code for the compiler, overlay, library routines, and generator.
Summarizing from the 12-page document, the layout of the Generator Tape turned out to be the following (all blocks are 100 words in length):
  • Lane 0:
    1. 50 blocks (#1-50) of the compiler main module. These are written in groups of 10 blocks containing 999 program words followed by a checksum word.
    2. Two blocks (#51-52) reserved for compiler patches, also checksummed. These blocks are not used with the standard compiler.
    3. Three blocks (#53-55) containing a copy of words 3996-4295 from the compiler main module, not checksummed. This is part of the compiler's initial symbol table, before entries for the library routines have been inserted. These blocks are used by the generator to rebuild the symbol table when replacing the library, after which the new symbol table containing information for the library routines is written back over blocks 41-43 of the generated compiler tape. This use of blocks #53-55 was the most obtuse part of the tape format to figure out.
    4. Three blocks (#56-58) containing addresses 0000-0399 of the overlay module, not checksummed. This is the object code for the Symbolic Dump diagnostic routine.
    5. 29 blocks (#59-87) containing addresses 0700-3599 of the overlay module, not checksummed. This is the code loaded by the compiler after the end of the user's BALGOL source program has been compiled. The 32 blocks of the overlay module in blocks #56-87 will be checksummed by the generator as one 3200-word unit before being written to a generated compiler tape.
    6. Three blocks (#88-90) for the Library Table, a directory to the library routines.
    7. One block (#91) with a zero as the first word. This appears to be a low-key stopper block for the 220's MTS (Magnetic Tape Search) instruction, which was used to position the tape efficiently to individual library routines.
    8. n blocks (#92 - n+91) for library routines. The first word of each block is a search keyword for the MTS instruction. Each routine starts on a block boundary and may span multiple consecutive blocks.
    9. One block (#n+92) with all nines in the first word. This serves as an end-of-file sentinel to the generator program and as a high-key stopper block for the MTS instruction.
  • Lane 1:
    • 50 blocks of the generator program, checksummed as for the compiler main module above.
A loadable compiler tape has a similar format to the Generator Tape, but with these differences:
  • Lane 0:
    • The initial symbol table for the generator's use in blocks #53-55 is omitted. Therefore, the overlay module (with its checksum word) starts at block #53 and the Library Table starts at #85.
    • Following the all-nines sentinel block at the end of the library, the generator writes four blocks containing the code for the Object Dump and Program Card Loader routines described in Appendix B of the compiler reference manual. These routines are assembled as part of the generator program.
  • Lane 1:
    • Used as workspace by the compiler's Symbolic Dump diagnostic facility.
The tape images in the repository for the retro-220 emulator are plain ASCII text files with a ".tape" file name extension. You can look at these files with a text editor and view the structure of the Generator and compiler tapes. See the wiki page on the magnetic tape subsystem for details on the tape image format.

Debugging the Recovered Compiler


By the time I had finished the emulator, assembled all of the source code, puzzled out the format of the Generator Tape, gotten the generator program working, and created the first loadable compiler tape, it was December 2017. 
 
Obtaining sample BALGOL source code with which to test the compiler and debug the recovery effort was by far the simplest of the tasks in this project. Section 11 of the 1963 reference manual has four sample programs, apparently written by faculty at Stanford University, including such luminaries as J. G. Herriot and George Forsythe. The 1961 edition of the manual also includes these, plus a fifth sample program. None of them is very long, so I simply transcribed them from the manuals. There were a few typographical errors in the manuals, but these were easy to discover and correct once the compiler was starting to work. I also wrote a couple of small test programs from scratch.

The generator program proved to be very easy to debug and get working, largely because its assembly listing included the object code, which allowed the mechanical matching process discussed earlier to find the significant transcription errors. The biggest problems with the generator were not with the program, but in getting its specification cards properly prepared and getting the various modules of object code arranged on the preliminary Generator Tape so that the program would work properly.

Debugging the compiler and library was another matter. I started compiling some of the test programs, and by setting console switches to cause the compiler to print the object code it was compiling, could see that it was working -- somewhat.

The compiler would be parsing the BALGOL source text and generating what appeared to be correct code, but then would start reporting errors that were not related to the source code, including some strange ones like "INVALID PSEUDO-OP" that suggested internal compiler errors. In other cases, the compiler would go off into never-never land, eventually either halting without explanation or looping continuously.

This behavior was highly distressing, partly because something was clearly wrong with the compiler, or the emulator, or both, but mostly because I had no good idea how to start diagnosing these problems, let alone fixing them. The compiler is an extremely complex piece of software, written very cleverly and efficiently by people who really knew the 220, and I never got to the point of a comprehensive understanding of how the compiler worked internally. Plus, the problems were likely occurring long before any evidence of them was surfacing, resulting in a very murky trail to try to follow.

At this point, I decided to launch the complete second proofing of the transcriptions for the compiler, overlay, and library routines. That effort took several weeks to complete, and found a handful of errors, especially in the all-critical compiler tables. That solved a few problems, but still left a number of mysterious errors.

As always with such situations, the only thing one can do is to start with the evidence at hand and try to work backward from there to the cause. Since most of the evidence I had consisted of error messages produced by the compiler, it seemed logical to start at the point in the code where the error message was being generated and try to see which execution paths led to that point. In most cases this was straightforward to do, either by inspecting the transcribed code in the neighborhood of the error message, or by following subroutine return linkages and inspecting the code in the neighborhood of the originating calls.

Tracing the error messages revealed the cause of the error about half the time. For the rest, I had to try to reason about what the compiler was trying to do at that point and hypothesize what could have gone wrong. In other words, the other half of the problems were solved by making what amounted to lucky guesses and following up on them. I was surprised at how successful I was at this. Eventually I started to get error-free compilations and be able to run the resulting object programs.

One thing that helped in the debugging is that the 220 has a feature, the S Register, which allows you to set a trap address on the console. Before the processor executes an instruction at that address, it halts. From there you can see the contents of the registers and use the console keypad to inspect words in memory. I had also implemented a feature in the emulator that would generate a complete dump of the 220 memory. That proved to be much more convenient for inspecting variables and tables in the compiler than finger-boning the console one word at a time.

In the end, most of the trouble in the compiler was caused by a few subtle typos in the transcription. The following table shows the typos in the compiler main module uncovered by debugging:
 Address Assembly Text Error
 0621NOPV1 CNST  00000100001
 10 should be 01
 0691      CNST  20176000000 17 should be 71
 1480
*M    LDB   MODE *M should be *H
 1971
      STA   SYMBL
 SYMBL should be SMBL
 2469
      BUN   SMBL,BUNZ SMBL should be ASMBL
 2660PRFSW F4241 1210,01,1
 1 should be 0
The overlay module was much cleaner. The only significant problem was at address 0010: "MRD 4 100,OT,BMOD" should have been "MRD 4 100,OT,3,BMOD". MRD is a magnetic tape read instruction; the missing "3" is the number of blocks to read.

These types of problems just go to show that even with careful transcription and multiple careful proofing passes, it is almost impossible to recover software from listings without introducing errors, some of which will have nasty effects and be difficult to find.

Debugging the compiler revealed one serious error in the emulator. The 220 supports two similar instructions, CFA (Compare Field A) and CFR (Compare Field R). These compare either a whole word or a partial-word field between a word in memory and either the A or R register, setting the result in the processor's LOW/EQUAL/HIGH comparison toggles. That result can be acted upon by the 220's BCH/BCL/BCE/BCU conditional branch instructions.

Compare instructions like these are usually easy to implement -- the words in the register and in memory are virtually subtracted, and the comparison toggles are set based on the virtual difference being positive, negative, or zero. This is complicated in the 220, however, by the fact that the sign is a whole digit. The low-order bit in that four-bit digit indicates the arithmetic sign (1=negative), but the other bits have meaning in certain situations.

If the sign digit is part of the field to be compared, then CFA and CFR take the extra sign-digit bits into account and order the sign values in a strange way:

3 < 2 < 1 < 0 < 7 < 6 < 5 < 4 < 8 < 9

In addition, if the sign digit is 1, 2, or 3, the remaining digits in the word or field are compared using their nines-complement value.

To put it simply, I didn't get the implementation of this right the first time, nor the second, nor the third. It got to the point towards the end that, whenever I encountered strange behavior by the compiler, I started looking for CFA or CFR instructions in the code path leading up to the error and examining how those instructions were behaving in that specific case.

You would not think that weird comparisons of signs other than 0 or 1 would have much impact on something like a compiler, but BALGOL does extensive field packing in its internal tables to minimize their size, thus to allow compiling larger programs. The sign digits in these tables are frequently included as parts of fields or as fields on their own, and comparisons involving those fields are surprisingly frequent. The people writing the compiler knew exactly how CFA and CFR were supposed to work, and they took every advantage of the somewhat unorthodox behavior of those instructions. Once I had the comparison logic implemented properly in the emulator, the last of the strange compiler behavior simply disappeared.

Recovery of the BALGOL compiler was completed in mid-July 2018. The compiler and library seem to be working quite well now, but I will not be the least bit surprised if someone eventually finds additional errors in the transcriptions.

Recovering the BALGOL compiler for the 220 has been one of the most difficult and challenging software projects I ever tried to do. The retro-220 emulator, while hardly trivial, was a straightforward development task by comparison. Regardless of the difficulties involved, though, recovering the BALGOL compiler and getting to see it run again has been immensely gratifying.

Postscript

Compiling from Paper Tape


After finishing the compiler recovery project in the summer of 2018, I started working with Michael Mahon to enable paper tape input and SPO (console typewriter) output for the compiler. Michael has written a 220 emulator for the Apple II [15], based upon his experience at CalTech with a 220 that did not support the Cardatron punched card interface. As with that 220, his emulator does not support punched cards, thus he was unable to run the standard compiler.
 
The generator program has the ability to replace the I/O routines for the compiler and library using its aforementioned INPUTMEDIA and OUTPUTMEDIA specifications, which are described in Appendix A of the 1963 compiler reference manual. Thus we set out to try to customize the compiler so that it could be used with his emulator.

Appendix A mentions a document issued by Burroughs, "Supplemental Material for Use with the BAC-220 Generator," that purported to show examples of alternate input and output routines, but of course we did not have that. Appendix A describes how to generate a compiler tape with custom I/O routines, and Appendix F describes the calling conventions and "media deck" format for such routines, but actually figuring out how to write the routines was going to be up to us. We eventually were successful, and the results are available in the BALGOL-INPUTMEDIA-OUTPUTMEDIA folder of the retro-220 project's repository [14].

The interesting thing about this episode with respect to the compiler recovery effort is that it uncovered a few bugs in the generator program. These were not, however, due to errors in the transcription -- they appear to be original bugs present in Professor Knuth's copy of the generator listing:
  1. The generator needs to know the location of several items in the compiler and overlay modules so it can adjust their code during the process of generating a compiler tape. It does this by means of symbolic address definitions, but these definitions need to be kept in sync with the actual locations in the compiler and overlay. One of the symbolic definitions, NUMB, was incorrect. It was defined in the generator as "4800+1335," but based on the location of that symbol in the overlay module, should have been "4800+1334." This error did not affect generation of a compiler tape with the default Cardatron I/O routines, but it did affect the code adjustments the generator had to do when replacing the default routines, resulting in words at the wrong memory addresses getting adjusted. This suggests that in the 1962 listings Burroughs distributed, the version of the generator may not quite have matched the versions of the compiler and overlay modules.
  2. The generator is driven largely by a series of small tables that are selected for use in a given run based upon the specifications present in its input card deck. Each table consists of a series of words that encode operations the generator must perform to adjust a specific feature of the compiler. Each of these tables begins with a word indicating the number of words that follow it in the table. One of the tables, F2, had an incorrect length word, 6 instead of 8. This was another case where the error did not affect generation of a compiler with standard I/O routines. When replacing those routines, however, the truncated table resulted the generator failing to delete support for the compiler's standard output completely, which in turn resulted in processor alarms when the overlay module tried to execute a Cardatron instruction on a system without a Cardatron.
  3. Another table, with the point-label "1" following the label PTVER, was missing two entries that adjusted the way the compiler output its heading line at the start of a compilation. The consequence of this was relatively benign, in that the compiler simply printed the wrong heading.

Each of these errors has been corrected in the source for the generator available on the retro-220 project repository, but the original version of the generator, matching the original assembly listing, remains available in the repository's BALGOL-Generator--ORIGINAL sub-folder.


Finally, an Assembler Manual

 
During a visit to the Charles Babbage Institute in Minneapolis during the Fall of 2019, I discovered a manual for what appears to be the assembler used for the compiler, overlay, and library modules [8]. That document indicates, not surprisingly, that this was a much more fully-featured assembler than is apparent from the 1962 listings. It has more powerful address expressions, additional pseudo instructions (termed "control ops"), provision for overlays and source code libraries, and a relatively sophisticated macro facility (termed "generators").

From inspecting that document, I had done a pretty good job of reverse-engineering its assembly notation, or at least those parts of it that were revealed in the compiler listings. My source card format is a little different, the pseudo op for comments is REMK instead of REM, and there are additional convenience operator mnemonics, but none of that was apparent from the transcribed listings. It would be interesting at some point to go through the document and bring the BAC-Assembler more in line with this specification, but implementing some of the more advanced features, such as the library support and generators, would probably require a complete rewrite.

References



[3] "Algol-58 -- The International Algebraic Language," https://datatron.blogspot.com/2018/07/algol-58-international-algebraic.html.




[7] Bitsavers archive of Burroughs 220 documents, http://bitsavers.org/pdf/burroughs/electrodata/220/.

[8] "Burroughs 220 Assembler-Compiler," Bulletin 5024, Burroughs Corporation, Data-Processing Systems Group, April 1960. Burroughs Corporation Records, Product Literature (CBI 90), Charles Babbage Institute, University of Minnesota, Minneapolis. https://archives.lib.umn.edu/repositories/3/resources/186. Series 74, box 5, folder 17.

[9] BALGOL section of retro-220 GitHub repository, https://github.com/pkimpel/retro-220/tree/master/software/BALGOL.

[10] "Building the BALGOL Compiler," retro-220 project wiki, https://github.com/pkimpel/retro-220/wiki/BuildingBALGOL.


[12] "Using the BAC-Assembler," retro-220 project wiki, https://github.com/pkimpel/retro-220/wiki/UsingBACAssembler.

[13] "Using the GEN-Assembler," retro-220 project wiki, https://github.com/pkimpel/retro-220/wiki/UsingBACAssembler.

[14] BALGOL-INPUTMEDIA-OUTPUTMEDIA section of the retro-220 GitHub repository, https://github.com/pkimpel/retro-220/tree/master/software/BALGOL/BALGOL-INPUTMEDIA-OUTPUTMEDIA.

[15] "Burroughs 220 Simulator, v1.1," Michael J. Mahon, http://www.michaeljmahon.com/.

Saturday, June 11, 2016

The Shell Assembler, Part 2

In Part 1 of this post, I introduced the Shell Assembler for the Burroughs 205 [1]. That part discussed the origin of the assembler, an overview of its features, its reconstruction from listings in its Part II manual [2], and how to operate the assembler in the retro-205 emulator.

This is Paul Kimpel, with another post on Tom Sawyer's 205 blog. In this second part, I will discuss programming with the Shell Assembler, along with its relatively advanced features for making the programmer's job easier and more productive. Unlike Knuth's EASY and MEASY assemblers [3], which were built to be lightweight and fast, Shell was built for power and convenience. It became the assembler of choice for shops that had to write and maintain a lot of code.


Operator and Pseudo-Operator Mnemonics


In contrast to EASY and MEASY, which use a set of operator mnemonic codes designed to be compatible between the Burroughs 205 and 220 computer systems, the Shell Assembler uses the standard set of mnemonics initially established by ElectroData and continued by Burroughs in their reference manuals:
     00 PTR     20 CU      40 MTR     60 M       80 FAD
     01 CIRA    21 CUR     41 --      61 DIV     81 FSU
     02 STC     22 DB      42 MTS     62 --      82 FM
     03 PTW     23 RO      43 --      63 EX      83 FDIV
     04 CNZ     24 BF4     44 CDR     64 CAD     84 --
     05 --      25 BF5     45 CDRI    65 CSU     85 --
     06 UA      26 BF6     46 --      66 CADA    86 --
     07 PTWF    27 BF7     47 --      67 CSUA    87 --
     08 STOP    28 CC      48 CDRF    68 --      88 --
     09 --      29 CCR     49 --      69 --      89 --
     10 DAD     30 CUB     50 MTW     70 MRO     90 (FAA)
     11 BA      31 CUBR    51 --      71 EXC     91 (FSA)
     12 ST      32 IB      52 MTRW    72 SB      92 (FMA)
     13 SR      33 CR      53 --      73 OSGD    93 (FDA)
     14 SL      34 BT4     54 CDW     74 AD      94 --
     15 NOR     35 BT5     55 CDWI    75 SU      95 --
     16 ADSC    36 BT6     56 --      76 ADA     96 --
     17 SUSC    37 BT7     57 --      77 SUA     97 --
     18 --      38 CCB     58 CDWF    78 --      98 --
     19 --      39 CCBR    59 --      79 --      99 --

It also implements a number of synonyms for some of the instructions, e.g., A for AD (Add) and S for SU (subtract). For some reason, the version of the assembler we have does not implement mnemonics for the floating-absolute instructions (90=FAA, 91=FSA, 92=FMA, 93=FDA). It would not be difficult, however, to add these to the assembler's symbol table.

In addition to supporting the mnemonic operator codes for the 205 machine instructions, the Shell Assembler offers these pseudo operators:
  • REM: a remark or comment. The remaining contents the card are ignored by the assembler, except that they are printed on the listing.
  • EQU: assign a value to a symbol. This is typically used to assign a symbol to an absolute address, or to assign the value of one symbol to another.
  • ORG: specify the next assembly address. Normally, the assembler increments its address counter by one after assembling an instruction. This pseudo operator is typically used to establish a starting address for a program or to position a section of a program at a particular address.
  • REG: reserve a block of memory locations. This simply adds the value of the operand field to the address counter. Any label for the line is assigned the value of the starting address of the block
  • MOD: advances the address counter forward as necessary so that it is a modulo 20 address. This is particularly useful for positioning the address of magnetic tape records in memory. Data transfers to and from tape can begin at any main memory address, but because magnetic tape I/Os are buffered through the 4000 and 5000 high-speed loops on the memory drum, and because of the way that blocking transfers between main memory and the high-speed loops work, the word at the first mod-20 congruent address is the first one read from or written to the tape. Thus a program must read data from a tape using the same mod-20 congruent address as was used to write data to the tape in order for the words end up in the same sequence in memory. Starting the memory buffers on mod-20 addresses helps eliminate any confusion as to the word ordering between tape and memory.
  • NUM: define a one-word numeric literal value. The value must be right-justified in the 10-character operand field, with the sign coded as either 0/1 or +/- in the tag field. Spaces in the operand and tag fields are interpreted as zeroes. See the "Input Card Layout" section below for the field locations.
  • ALF: define a one word (five character) alphanumeric literal encoded using Cardatron card codes.
  • ALFS: define an alphanumeric literal up to six words (30 characters) in length, encoded using Cardatron codes. The text of the literal is written in the remarks field of the card. The length of the literal in words (1-6) must be written right-justified in the address portion of the operand field. The resulting words are stored in reverse order in memory to support both the way the Cardatron accessed memory and the decrement-and-branch indexing native to the 205's B register.
  • FLX and FLXS: similar to ALF and ALFS, but generate words using the older Flexowriter code used on the Datatron 204 and earlier system models before the Cardatron was introduced.
  • TAG: specify the high-speed loop to which addresses in assembled instructions should be assigned. Efficient programming for the 205 requires that instructions be transferred to one of the high-speed loops (typically the 7000 loop) and executed from there. The TAG operator specifies the digit (4-7) that the assembler will automatically set in the high-order digit of the operand address for instructions that reference memory, saving the programmer the trouble of continually having to override that digit on individual instructions. This behavior can be overridden on individual instructions as described for the Loop Tag field below, or disabled altogether by coding a TAG operator with an "X" as the tag digit.
  • MACRO and NEW: define assembler macro instructions. The difference between them is that MACRO is used to define a temporary macro that will exist only for the current assembly run, while NEW defines a macro that can be used in the current assembly run, but will also be inserted into the macro library on magnetic tape. See the "Macro Definition and Usage" section below for more information on how these two pseudo-operators are used.
  • SUBR: causes a subroutine to be copied from the macro library and appended to the end of the program being assembled. No call linkage to the subroutine is provided. A given subroutine will be included only once, no matter how many references are made to it using SUBR. Thus a body of code can be written as a closed subroutine, and a macro written to provide parameters and linkage to the subroutine, using SUBR within the macro to call out the closed body of code from the library. Each macro invocation will generate unique linkage code at that point in memory, but the body of the closed subroutine will be included only once. Subroutines can be added to the library as if they are a parameter-less macro, using NEW.
  • RFB, WFB, and PFB: generate Cardatron format bands. RFB generates a format band for reading cards; WFB generates a format band for printing to a line printer; PFB generates a format band for punching cards. These operators use a simple notation to define the card or print line format, dramatically reducing the effort and potential for error compared to coding a band manually. See the "Cardatron Format Band Assembly" section below for more information.
  • END: signals the end of the main program or a macro. The card deck must always end with this pseudo-instruction.
As mentioned in Part 1 of this post, the Shell Assembler does rather extensive checking of the input text and assembled code, generating error messages in English and identifying the line on which they were detected. It also does "invalid pair" checking -- flagging instances of one instruction following another that are unreasonable or which could lead to incorrect results.


Programming with the Shell Assembler


Source program input to the Shell Assembler was punched on cards. This section describes the layout of the cards, how the fixed columns of that layout were used, and some special features of the assembler.

As described in an earlier post [4], the Cardatron input unit requires a punch in a specified column to select a format band from its small drum memory. The codes in this format band describe how the Cardatron will reformat and translate columns of data on the IBM card machine to the 205 memory. The Shell Assembler normally takes its band selection from column 1 and uses two format bands:
  1. Format for instruction cards.
  2. Format for a header card.

The Header Card


The first card in the deck must have a "2" in column 1, indicating it is the header card. The contents of this card are read using the format band defined in FMB7 of Movement 1 and output to the line printer using the band defined in FMB6. The contents of this card are not otherwise used by the assembler.
  • Columns 1-3 are read as a number.
  • Columns 4-53 are read alphanumerically (these are probably intended to be a title).
  • Columns 54-55 are also read alphanumerically.
  • Columns 56-61 are read numerically (these appear to be intended as a date in MMDDYY format).
  • The remaining 19 columns on the card are ignored.
 

Input Card Format


The remaining cards of the source program must have a "1" in column 1. These cards carry the instructions that comprise the program.

Cards for format band 1 have the following layout:

     1       10        20        30        40        50        60
     .........|.........|.........|.........|.........|.........|.....
     1        LLLLLSSSSSCCCCCTAAAAAIIIIIRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR

Columns 2-9 and 66-80 are ignored by the assembler, and can be used for sequencing or identification of the deck.

Column 1: Cardatron format band code. The character in this column is used by the Cardatron, but otherwise ignored by the assembler.

Columns 10-14: Symbolic location label: the traditional label field for an assembly line. The symbol in this field is assigned the current value of the address counter. Labels are limited to five characters and must be unique within the program. They must be left-justified and start with an alphabetic character, but any characters, including punctuation, can be used in the other four positions.

Alternatively, this field could have a "program-point" label, written left-justified in the field as a period followed by a letter, e.g., ".A". Program points are temporary labels with a relative, local scope. They can be defined multiple times within a program. The program can refer to an address at the immediately-prior or immediately-next program point. Once a program-point label is redefined, its prior location falls out of scope and can no longer be referred to by subsequent instructions. See columns 26-30, below, for the way that these labels were referenced.

Program points allow a programmer to refer to locally-relevant locations, e.g. for short branches and local constants, without cluttering up the symbol table with labels that may be used only a few times within a small region of the program.

As discussed in a footnote on page 2 of the Part I manual for this assembler, the idea of program points apparently originated with Melvin Conway of the Case Institute of Technology (now Case Western Reserve University) and were first incorporated into the SOAP III assembler for the IBM 650 by Donald Knuth. Knuth later used program points in his EASY and MEASY assemblers. They also found their way into a number of later Burroughs assemblers. Program points were not available in STAR 0, the official Burroughs assembler for the 205, however.

Columns 15-19: "Spares": the sign and first four numeric digits of an instruction word. The sign must be coded in column 15, and can be a numeric digit, a space (which was translated to 0) or "-" (which was translated to 1). Odd-valued sign digits in instruction words cause the B-register to be added to the operand address to form the effective address used when the instruction executes. The other four digits are used for the breakpoint control digit (in column 19) and for control digits in I/O instructions. Programmers sometimes used this field for data. If all five columns are left blank, the sign and control digits are assembled as zeroes. If any of the four control digits are non-zero, all four must be coded.

Columns 20-24: Operation code: the symbolic operation code for the instruction or pseudo-instruction. This must be left-justified in the field. Alternatively, a numeric operation code can be entered in the last two positions of this field. The Shell Assembler uses the standard ElectroData/Burroughs mnemonics as discussed in the prior section.

Column 25: Loop tag: for instructions that reference memory addresses, a value of 4-7 in this column will cause the high-order digit of the assembled address to be set to this value, making it an address for one of the high-speed loops on the drum. If this field is left blank and a loop digit has been specified previously by the TAG pseudo-instruction, that loop digit will be placed in the high-order digit of the assembled instruction. If column 25 contains an "X", any effect of TAG is suppressed in the assembled instruction, and the high-order digit of the address is assembled normally.

Use of the TAG pseudo-instruction considerably eases the task of writing code to run in the 205's high-speed loops. Instructions must be assembled into main memory locations and then blocked to one of the loops. Instructions referencing locations within the loop, e.g., for short branches or local constants, must use loop addresses instead of the ones that would normally be assigned for their originally-assembled location, so the use of TAG allows this address modification to be applied automatically. Most of the code in the Shell Assembler itself is written under the influence of a TAG 7 directive, as most of the code is blocked into the 7000 loop by CUB instructions for execution.

Columns 26-30: Operand address: the coding in this field specifies the address field to be assembled into the instruction (possibly modified by the increment field below). This field can hold one of the following:
  • Spaces: this causes the current value of the location counter to be assembled into the instruction. This is equivalent to the symbol "*" used by many other assemblers to designate the current value of the location counter.
  • Absolute address: a literal, decimal address, right-justified in the five columns of the field. Leading zeroes could be replaced by spaces. This was normally used only with instructions that require a numeric operand, such as shifts and console I/O.
  • Symbolic address: one of the alphanumeric symbols defined in columns 10-14 on some other line of the program.
  • Program-point address: a reference to a program-point label. This is coded in columns 26-27 as either "+L" or "-L", where "L" is a letter. "+L" refers to next line in the source labeled ".L". "-L" refers to the prior line in the source with that label. Only the immediately-next or immediately-prior lines with that label can be referenced with this notation. Note that, depending on the type of print wheels installed in the 407, "+" (a 12-row punch on a card) may print as "&".
Columns 31-35: Operand address increment: If this field is blank, the operand address specified in columns 26-30 is assembled as is. Otherwise, the value of this field modifies that operand address. Column 26 must be coded as a "+" ("&") or "-". The remaining four columns specify the increment, right-justified in the field. Leading zeroes may be replaced by spaces. This increment is then algebraically added to the operand address, modulo 10,000. Negative results are represented in 10s-complement form, e.g., 0000 - 0001 is 9999.

Columns 36-65: Remarks: These 30 columns can be used for comments, and are reproduced as-is on the output listing. This field is also used as the operand for the ALFS, FLXS, RFB, PFB, and WFB pseudo-instructions. Format-band coding for the three format-band instructions can overflow on up to eight continuation cards by leaving the operation code field on the continuation cards blank.


Cardatron Format Band Assembly


One of the most useful features of the Shell Assembler is its ability to construct Cardatron format bands from a fairly simple and intuitive notation.

Designing and coding format bands for the Cardatron is extremely tedious. As described in [4], a 205 format band consists of 315 digits, which occupy 29 words in memory before being loaded to the Cardatron drum. Bands are coded using values 0-3 in each digit position of a memory word, including the sign digits.

The Cardatron works by splitting each column on a card or line of print into two decimal digits, one for the numeric portion (the bottom nine rows on a card) and one for the zone portion (the top three rows on a card). The format band digits operate on the stream of numeric and zone digits coming from or going to the IBM device, by copying digits, deleting digits, or inserting zero digits into the stream. The result is a mapping between the 80 or 120 columns on the IBM device and up to 28 words in 205 memory. The format band digits can drop columns and insert sign, leading, and scaling digits on input. They can also insert blanks on output, control whether digits from memory words are output numerically or alphanumerically, reduce 10-digit memory word values to fewer columns on a card or print line, and position signs as either separate columns or overpunches.

This mapping is complicated by the order in which the Cardatron processes data. It starts with the last column on a card or print line and works towards the front, with the first word in memory mapped to the last columns on the card or print line. Thus the low-order digits in the first word of a format band in memory references the end of a card or print line. The band designer must code digits to account for exactly 80 columns on a card or 120 columns on a print line, and provide "3" (delete digit) codes in all remaining positions of the format band. On input, the designer must code a single additional "0" (insert zero digit) code at the end of the format band to push the last word being assembled out of the D register and into memory.

Electrodata developed a tabloid-size (11x17-inch) form to aid in designing and coding format bands. See page 6-11 in [5] for an example of the form used with the 220 Cardatron, which is almost identical to that for the 205. This form made it easier for the designer to keep track of columns on the IBM device, the mapping between those columns and words in 205 memory, and any padding required to fill the band data out to 29 memory words. Even with this form, though, the process of designing and coding the band digits was tedious and error-prone.

The format band pseudo-operators in the Shell Assembler dramatically simplify the task of designing and coding format bands. They use a free-field notation with letter codes and repeat factors, not unlike a FORTRAN FORMAT statement or a COBOL PICTURE clause. The band definition consists of phrases, one per 205 memory word, that define the mapping between digits in the memory word and columns on the IBM device. The word-phrases are delimited by "+" (or "&") characters. For example, consider the definition of format band 1 described in the prior section that is used to read the instruction card for the Shell Assembler:

FMB1     RFB            9B & 7¤P10Z¤ & 3¤P5A¤ & P8ZA
                           & 8¤P5A¤ & P10Z & 15B
The codes used in format band phrases have the following meanings:
  • A - translate a column alphanumerically. On input, two digits from the card (usually the numeric and zone digits from the same column) are translated to the Cardatron code and stored in the next two digits in memory, proceeding from right to left in the memory word. On output, two digits in Cardatron code are translated from the memory word to numeric and zone codes for the next column (again, proceeding from right to left) on the card or print line.
  • B - ignore a column. On input, the next two digits from the card are dropped and not stored in memory. On output, two zero digits are generated, producing a blank column on the IBM device.
  • N - translate a column numerically. On input this copies the numeric digit from a card column to memory and discards the zone digit. On output, it copies a digit from memory as the numeric digit for a column and supplies a zero for the zone digit in that column. As a special case on output, if the digit in memory is a zero, a zone digit is output that causes a zero to be printed or punched in the column. This was necessary because the IBM devices consider  a zero punch (third row on a card) to be a zone, not a numeric punch. Without this special case, zeroes in memory would be output as blank columns instead of zeroes.
  • P - ignore the sign digit in memory. This code is identical to the Z code below, except that the assembler will allow a P only for the leftmost digit in a word. The combination of a special code for the sign digit and delimiters between word-phases allows the assembler to verify that exactly 11 digits have been generated or consumed by the band for each memory word.
  • X - transfer zone digit. This is normally used to place the sign in a column. The combination XN will treat the sign as an over-punch on the numeric digit formatted by N. The combination XB will treat the sign as a separate column.
  • Z - ignore digit position in memory. On input, this will not consume a digit coming from the card and will store a zero digit in memory. On output, it will skip the next digit in memory and output nothing to the IBM device.
  • ¤ - the lozenge (which prints as a right-parenthesis on some tabulators) serves as a bracket character to group one or more phrases for repetition.
Any of these codes can be prefixed by an integer to designate that it will be repeated that number of times. A repeat count in front of a ¤-group indicates the entire group is to be repeated that number of times.

For the format band example above, the word-phrases can be understood to have the following meaning:
  • 9B - skip the first 9 positions on the card.
  • & 7¤P10Z¤ - store seven words of zeroes into memory. The P stores a zero in the sign-digit position and 10Z stores the other ten digits of the word.
  • & 3¤P5A¤ - transfer the next 15 columns on the card alphanumerically to the next three words in memory. This corresponds to the Symbolic Location, Spares, and Operation Code fields in columns 10-24 on the card. The P stores a zero in the sign digit of each five-character word.
  • & P8ZA - Store the alphanumeric code for next column (TAG in column 25) in the low-order two digits of the next word in memory. P8Z stores zeroes in the sign and high-order eight digits of that word.
  • & 8¤P5A¤ - Transfer the next 40 columns (26-65) from the card alphanumerically into the next eight words in memory. This comprises the Operand Address, Address Increment, and Remarks fields on the card.
  • & P10Z - Store another word of zeroes into memory.
  • & 15B - Skip the remaining 15 columns (66-80) on the card.
Note that the additional zero digit to push the last input word out of the D register is supplied automatically by the assembler when translating a RFB defnition.

The way that format bands are coded internally for input and output is slightly different. One very nice thing about the format-band phrase notation is that it is symmetric between input and output. Reading a card from an input device with a set of format phrases in an RFB specification and then writing that data using the same set of phrases in a WFB or PFB specification produces an identical set of columns on the output device. The digits in the format band will be somewhat different, but the assembler knows how to code the band differently based on the pseudo-operation code used.

The only difference between WFB and PFB is that bands for punching cards must format 80 columns, while bands for printing must format 120 columns. The band designer must still account for the appropriate number of columns on the IBM device, and the assembler will verify this, issuing an error message if the number of columns is incorrect. The designer does not need to worry about padding the band out to 315 digits, however -- the assembler does that automatically.


Macro Definition and Usage


Our word macro comes from the Greek word for long. In the context of computers and software, macro is typically used as an abbreviation for macroinstruction. There are many forms of macroinstructions, ranging from simple text replacement (e.g., named constants) to parameterized replacement of text (e.g., the C #define pre-processor) to the conditional and iterative generation of highly customized text.

Macro assemblers are assembler programs that implement the ability to create and invoke macroinstructions. The first assembler to employ macros appears to be Autocoder for the IBM 702 and 705 in 1956. The presence of macros in the Shell Assembler of 1958 is therefore a fairly early application of this concept. There were people at Shell at the time who had come from IBM, including Bob Barton, so it's possible that the idea of including macros in the Shell Assembler came from those former IBMers.

The main idea of macros in assembly languages is to take commonly-used sequences of instructions and encapsulate them, give that sequence a name, and call forth the sequence later, possibly specifying parameters that will customize the sequence at that point in the program. The invocation of a macro often looks like the coding of an instruction, but with the operation code naming the macroinstruction instead of the mnemonic for a machine instruction.

Macros and subroutines are related, in that both can be used to define a sequence of code once and then activate that sequence from many places later. Subroutines are generally stored in memory only once, the program transfers control to the subroutine when it is invoked, and the subroutine passes control back to the program when it is finished. Macros, on the other hand, generate code in the program at the point they are invoked. Each time the macro is invoked, additional code is generated at that point in the program, and the code may be customized based on the parameters passed as part of the macro invocation.

Another important difference is that while subroutines are executed at run time, macros are executed at assembly time -- invocation of a macro generates additional text to be assembled. The instructions resulting from assembling that additional text will be executed at run time, but not the macro itself.

Invoking Macros in the Shell Assembler


A macro in invoked by writing its name in the operation code field (columns 20-24) on a card. A label, if desired, can be written in the label field. Actual parameters to the macro, if any, are written in the operand address and increment fields (columns 26-35) just as they are for a normal instruction. The first parameter is written in the operand field to the right of the macro name. Any additional parameters are written in the operand fields of subsequent cards, one per card. The operation code of these continuation cards must be blank. There is no limit to the number of parameters a macro may have.

All forms of operands may be passed as macro parameters. The only exception to operand coding for macros as compared to normal instructions is that the loop tag is never applied to the parameter operand as part of the invocation -- a loop tag may be applied to the parameter when it is used inside the macro, of course, but operand parameters are passed to the macro as is, without loop address modification.

Thus, a macro instruction with three parameters might be coded as follows:
.G    CLEAR         0020
              TBL1 +0012

                   +0001
As discussed above, invocation of a macro generates code in the program at the point of invocation. The Shell Assembler does this a little differently than most macro assemblers, however. At the point of invocation, the assembler emits a CUB (30, Change Unconditionally and Block) instruction. The code generated by the macro is placed at the effective address of that instruction, which will be after the end of the non-macro instructions in the program.

Any return to the point of invocation is the responsibility of the macro, so it was typical to pass a label or relative address as one of the parameters to the macro. An example of this is shown in the third parameter above -- the +0001 is relative to the CUB instruction emitted by the assembler at the point the macro is invoked. Thus the return would be to the instruction immediately following the invocation.

The reason for using a CUB at the point of macro invocation is that, once again, efficient programming for the 205 requires that code be executed out of one of the high-speed memory loops as much as possible. Well-written 205 programs were coded mostly as short sequences of instructions linked by CUBs to load and execute the sequences. Thus, macro invocation automatically puts the generated code in the 7000 loop. It is the macro coder's responsibility to deal with macro expansion that exceeds the 20-word capacity of the 7000 loop.

Defining Macros in the Shell Assembler


Macros are defined using the pseudo-operators MACRO and NEW. MACRO defines a temporary macro that will available only during the current assembly run. NEW defines a macro that will be permanently cataloged on the library tape. Other than that distinction, the two pseudo-operators are identical and function the same. MACRO and NEW can also be used to define bodies of code to be called out using the SUBR pseudo-operator, although such definitions should not use parameters.

Temporary macros are stored on the library tape along with permanent macros; they are simply not recorded in the tape's catalog. For that reason, Section VII of the Part I manual recommends that MACRO definitions follow any NEW definitions to avoid leaving unusable gaps on the library tape. It also recommends that permanent updates to the library be done in a separate assembly run dedicated to that purpose.

To define a macro, code MACRO or NEW in the operation-code field and the name of the macro (up to five characters) in the operand address field of a card. In the middle two digits of the "spares" field (columns 17-18), code the number of cards that make up the macro definition, not including the MACRO/NEW card, but including the required END card that terminates the macro. The names of newly-defined macros must not match the name of any existing macro -- attempting to do so results in an error.

Within the body of the macro, instructions are written the same way they are in open code. Macros may invoke other macros. Parameters are referred to by their ordinal position in the invocation -- #0001, #0002, #0003, etc., and may be referenced as the operand address in instructions and other macro invocations. The effective address of these parameters may be modified by the operand address increment field and by means of loop tags.

Here is one possible implementation for the CLEAR macro invoked above. Note the count of cards in the first line of the macro definition (0080) and the negative sign on the second STC instruction, causing it to apply B-register modification to the operand address.
      0080 MACRO  CLEAR
           TAG  7
           SB     +C
           STC    +C
     -     STC  X #0002
           DB          -0001
           CUB  X #0003
.C            00X #0001-0001
           END
When a macro invocation is encountered during Movement 1, the assembler emits a CUB with an unresolved forward address at that point in the program, then searches the macro library for the macro, and when found, makes an entry in a memory table for the macro and its parameters. After the END card is sensed in Movement 1, the assembler steps through this table, and for each macro invocation, re-positions the library tape to the macro in the library and appends its body on tape unit 1 after the records that were written there for the main portion of the program, partially assembling the instructions in the body in the same way that instructions in the main portion are during that first movement. In Movement 2, assembly of the instructions from the macro bodies is completed as for other instructions, and the addresses in the CUB instructions at the point of macro invocation are resolved to the location where the macro bodies are assembled.

The project repository has a folder [6] with small sample card decks and assembly listings, showing both definition and invocation of macros. The listings show how the CUB instructions emitted at the point of invocation branch to the code appended at the end of the program for each invocation.

Recovery of the Shell Assembler has been a very interesting part of this project. I wish we had more examples of its use in significant applications, especially ones demonstrating the macro facility, and perhaps some will surface in due time. In any case, it's an excellent example of a sophisticated piece of programming from the mid-late 1950s.


References

[1] Shell Symbolic Assembly Program for the Burroughs 205, Part I, Burroughs Corporation, Bulletin 3038, March 1960:
https://www.dropbox.com/s/gnsdrwors4j3mnf/Shell-Symbolic-Assembly-Program-for-the-Burroughs-205-Part-I.pdf?dl=0

[2] Shell Symbolic Assembly Program for the Burroughs 205, Part II, Burroughs Corporation, Bulletin 3038, March 1960:
https://www.dropbox.com/s/lc3317ptvuec199/Shell-Symbolic-Assembly-Program-for-the-Burroughs-205-Part-II.pdf?dl=0

[3] "Knuth's EASY Assembler":
http://datatron.blogspot.com/2015/08/knuths-easy-assembler.html

[4] "The Mighty CARDATRON":
http://datatron.blogspot.com/2015/03/the-mighty-cardatron.html

[5] Burroughs 220 Operational Characteristics, Bulletin 5020A, Burroughs Corporation, August 1960:
http://bitsavers.org/pdf/burroughs/electrodata/220/5020A_B220_OperCharac_Aug60.pdf

[6] Folder of sample macro definitions and invocations:
https://github.com/pkimpel/retro-205/tree/master/software/Shell-Assembler/Shell-Macro-Tests