BACK END FUNCTIONS
As you reach each function's description in the following text, turn to that function in the listings (Appendix C) and familiarize yourself with it. Then continue studying it as you read the description. Refer to Appendix H to find the function in the listings.
Code Generation Functions
Header()
Header() is called once from main() before parsing begins. It initiates the code and data segments each with a dummy word to ensure that all addresses are non-zero. The choice of a word, instead of a byte, preserves word alignment in the data segment. Header() also declares external, each of the routines in the CALL module. This ensures that the CALL module will be linked with the program and that its routines can be accessed. Each segment is introduced by calling toseg().
Toseg()
Toseg() This function is called whenever a transition between the code and data segments is needed. The global integer oldseg remembers which segment is current. It initially contains zero, meaning that neither segment is current. On entry, toseg() accepts an integer argument indicating which is to be the new segment. The value one (defined as DATASEG) indicates the data segment, two (defined as CODESEG) indicates the code segment, and zero indicates that no segment is to be initiated but the current segment is to be terminated.
First, if the new segment matches the current segment, no action is taken and control returns to the caller. That failing, a switch is to be made, so the current segment, if there is one, is terminated first. If oldseg equals CODESEG, then
CODE ENDSis generated, and if oldseg equals DATASEG, then
DATA ENDSis generated. If oldseg is zero, nothing is written.
Next, if newseg is non-zero, the new segment is initiated with either
DATA SEGMENT PUBLICor
CODE SEGMENT PUBLIC ASSUME CS:CODE, SS:DATA, DS:DATAdepending on the value of newseg. Finally, newseg is moved to oldseg to remember which segment is current.
Trailer()
Trailer() is called by main() after parsing is finished. It performs five tasks.
First, it generates external references for all undeclared external functions. It does this by searching the global symbol table for function entries with a storage class of AUTOEXT--established by primary(), in the expression analyzer. When it finds an undeclared name, primary() assumes it to be a function and establishes it as such in the global symbol table with this storage class. If the function should be defined later in the program, then the storage class would be changed to a different value so that it will not also be declared to the assembler as external.
Next, if the function main() exists in the global symbol table, then _main() is declared to the assembler to be external. This refers to a function in the library module CSYSLIB which sets up conditions for the program to initially receive control, and in turn calls main() within the program. This external declaration guarantees that CSYSLIB will be linked with the program. Subprograms that do not contain main() do not generate this external reference. This pr events a "duplicate definition" error at assembly time when CSYSLIB.C is compiled.
Next, which ever segment (data or code) is current at the end of the program, is terminated by calling toseg() with zero for the new segment. This terminates the current segment without opening another one.
Then, the assembler directive END is written to terminate the source file.
The last task performed by trailer() is to display optimizer statistics. This logic is not compiled into the production compiler. However, if the #define of DISOPT (in CC4.C) is "de-commented," the fourth part of the compiler is recompiled, and the compiler is relinked, then a count of the number of times each optimization was applied will be displayed at the end of each compile run. The display goes to the standard output file.
Public()
Public() is called whenever a global object is being defined. It receives the identity code of the object so that it can distinguish functions from data. First, it calls toseg() to switch segments if necessary. Functions go into the code segment and global data goes into the data segment.
Next, it writes
PUBLIC name(where name is the string in the global array ssname[] which contains the name being defined) to the output file. This tells the assembler that the name is an entry point, allowing it to be referenced from other, separately compiled, source files.
Next, the name is written at the beginning of a new line. If this call is for a data item, then the new line is left unfinished; later, a DW or DB directive will complete the line. However, if it is a function, then the name is terminated with a colon and a new line is started. This establishes the reference point for the start of the function.
External()
External() is called whenever a name is to be declared external. It receives a pointer to the name, an integer giving the size of the object being declared, and an integer specifying the identity of the object. As with public(), it first establishes the correct segment. Then it writes
EXTRN name : sizewhere name is the designated name in uppercase (with a leading underscore) and size is one of BYTE, WORD, or NEAR. The first two of these apply to data declarations and the last to function declarations.
Outsize()
Outsize() is called by external() to generate the size portion of its output line. It bases its decision on the object's size and identity, as indicated above.
Point()
Point() simply writes DW $+2 to the output file. When this follows a label, it initializes a named pointer with the address of the following byte. This function is used to define pointers that are initialized with character strings. Since the string is stored immediately after the pointer, giving the pointer the value of the following byte effectively points it to the string.
Setstage()
This function performs three tasks. First, it saves, in the pointer whose address is supplied by the caller, the current position in the staging buffer (snext). This is the original position, before setstage() changes it, if it does. Next, if snext is zero, meaning that the buffer is not being used, setstage() points it to the first word in the buffer, thus initializing the buffer for use and flagging the fact that the buffer is to be used thereafter until clearstage() resets snext to zero again. However, if snext is already non-zero, then the buffer is already being used, so snext is left unchanged. Finally, setstage() saves the new (possibly changed) staging buffer position in another pointer whose address is also supplied by the caller.
To recap, this function initiates use of the staging buffer if it was not already being used. It also saves the previous and current buffer positions in pointers that the caller owns. These pointers will be the same unless the buffer was not previously in use (i.e., at the beginning of the analysis of an expression). In that case, the first will be zero and the latter will point to the beginning of the staging buffer.
Recall from Chapter 20, that the staging buffer is used only for holding code generated during expression evaluation. At other times it is bypassed; generated code is sent directly to the output file.
Gen()
This is the primary code generation function. From the other function descriptions, we can see that some functions write directly to the output file; these are the exception rather than the rule. Gen() accepts two integer arguments--a p-code and a value. The value could be a signed integer, or the address of a symbol table entry. In many cases, depending on the p-code, the value serves no purpose at all. Whether or not the value is used, every p-code has one, and this function treats them all the same.
Gen() tests the p-code for one of several cases that receive special treatment. If the p-code is GETb1pu, GETb1p, or GETw1p, the compiler wants to generate code that fetches an operand pointed to by the primary register. But these codes expect to find the address in the secondary register. So gen() first calls itself recursively to generate MOVE21 which moves the address to the secondary register.
Likewise, p-codes SUB12, MOD12, MOD12u, DIV12, and DIV12u first generate SWAP12 to swap the primary and secondary registers. This is because these operations are not commutative and the operands are in the wrong registers for the 8086 subtract and divide instructions.
Since pushes and pops must adjust the compiler relative stack pointer csp, PUSH1 and POP2 are sensed and appropriate adjustments to csp are made.
Two additional p-codes (ADDSP and RETURN), receive special treatment. These codes must adjust the machine stack pointer SP as well as the compiler relative stack pointer. The values associated with these codes directly specify a new value for csp. First, that value is saved. Then it is converted to an adjustment by subtracting csp from it; this adjustment will be added to SP to adjust the machine stack. And, finally, csp is set to its new (saved) value.
At this point, gen() does its primary task. If snext is zero, the staging buffer is not being used, so gen() calls outcode() to translate the p-code and its value to an ASCII string in the output file. Control, then returns to the caller.
However, if the staging buffer is in use, then an overflow test is performed. If successful, the p-code and its value are placed in the next two words of the buffer, snext is advanced by two, and control returns to the caller.
Clearstage()
This function clears code from the staging buffer. It either discards part of the code that was placed into the staging buffer most recently, or it dumps the buffer to the output file and resets it to its inactive state (snext == 0). In the latter case, it may simply reset the buffer without writing to the output file.
Two pointer arguments are received--before points to the buffer position that preceded the start of the code being cleared, and start points to the start of the code being cleared. These are normally the values that were earlier saved by setstage(). However, start is sometimes forced to zero by the caller.
If before is not zero, the whole expression is not being cleared (only the most recently evaluated subexpression), so snext is set to that value, effectively ignoring code that was generated beyond that point. Control then returns to the caller. As we shall see in Chapter 26, the code generated by the analyzer while evaluating constant subexpressions is discarded and replaced by a single p-code that loads the result.
However, if before is zero, the entire expression is being cleared from the buffer (this call corresponds to the first setstage() call for the expression), so the staging buffer is dumped via dumpstage() to the output file and then deactivated. But there are times when all of the code for an expression should be discarded. So, dumpstage() is called only if start is not zero. In either case, snext is reset to zero before returning to the caller. This deactivates buffering by causing gen() to write directly to the output file.
Dumpstage()
As we saw above, this function is called from clearstage() to dump the staging buffer to the output file. It does this, one p-code at a time, by calling outcode(). However, if optimizing has not been disabled for the run, it first calls peep() for each optimization that might possibly be applied. If an optimization is applied, then the optimizing loop is restarted at the beginning so that the optimized p-code sequence can be further optimized. Only the current and subsequent p-codes come under the scrutiny of peep().
When peep() indicates that all attempts have failed, dumpstage() calls outcode() to translate and write the current p-code to the output file as an ASCII string. Having done that, it advances to the next p-code and repeats the process until the last p-code in the buffer has been processed.
On entry, dumpstage() sets the global pointer stail to the value of snext which indicates the next unused position in the staging buffer. This marks the end of the data in the buffer. Then, snext is set back to the beginning of the buffer, from which it advances p-code by p-code as dumpstage() does its work.
The main loop, which steps from one p-code to the next, continues as long as snext is less than stail. The inner loop, in which peep() attempts to find an applicable optimization, continues until either an optimization is applied or all attempts fail. If DISOPT was defined when part 4 of the compiler was compiled, each successful optimization is displayed on the screen (stderr) as it occurs.
Dumplits()
Dumplits() is called at the end of each function definition to dump the accumulated string constants from the literal pool to the output file. It also serves to dump initial values for global objects.
It receives an integer argument that specifies whether the pool contains byte or word sized values. To save space in the output file, as many as 10 values are listed with each DB or DW directive. Therefore, this function comprises two loops. The outer loop calls gen() to generate a DB or DW directive, depending on the value of the argument. Then the inner loop repeats for a maximum of 10 times dumping successive values (bytes or words) from the literal pool as signed decimal strings. Commas separate the values. When 10 values have been dumped, a newline is written and the outer loop continues. If more values remain to be dumped, another DB or DW is generated and the inner loop is reinitiated. When the literal pool has been exhausted, a newline terminates the current line and control returns to the caller.
Dumpzero()
This is a simple function that dumps a specified number of zeroes (bytes or words) into the output file. It receives two arguments that specify the size of the items (byte or word) and the number to dump. It simply calls gen() to produce
DB n DUP(0)or
DW n DUP(0)where n is the number of items being dumped.
Output Functions
In this section, we look at each of the output functions except the one which translates p-codes to assembly language.
Colon()
Colon() calls the library function fputc() to write a colon to the output file designated by the global integer output.
Newline()
Newline() calls fputc() to write a newline character to the output file designated by the global integer output. Recall from Chapter 12 that the put library functions convert this character to two characters--a carriage return followed by a line feed. This is the standard end-of-line sequence in ASCII files. When sent to the screen or to a printer, it has the effect of locating the next character at the beginning of the following line.
Outdec()
Outdec() accepts a signed integer which it writes to the output file as a signed decimal character string. It calls fputc() repeatedly as it writes to the output file designated by output.
Outname()
Outname() accepts a pointer to a character string which it writes to the output file, as a Small C name. It prefixes the string with an underscore character and translates the characters to uppercase as it repeatedly calls fputc() until the end of the string is reached. The output file is designated by the global file descriptor output. This function does not write a newline character.
Outstr()
Outstr() accepts a pointer to a character string which it writes to the output file, by repeated calls to fputc() until the end of the string is reached. This function does not write a newline character.
A call to poll() at the beginning permits the compiler to be interrupted while writing to the output file. It honors control-S pauses as well as a control-C termination of the compile run.
Outline()
Outline() also accepts a pointer to a string which it writes to the output file. It differs from outstr() only in that it does append a newline to the end of the string. Thus it writes an entire line, or perhaps the last part of a line. This function first calls outstr() to write the string, then newline() to terminate the line.
Small C P-codes
For reasons of efficiency, before being output, code is first generated in the form of pseudo-codes or p-codes. These are small integer values, each of which corresponds to some particular assembly language instruction, instruction sequence, or partial instruction. Each p-code consists of two parts, the p-code itself and an integer value which influences the form of the ASCII string that it will become. The function outcode() is a specialized output function that translates a p-code and its value into ASCII assembly language.
Before looking into outcode(), however, we should first familiarize ourselves with the Small C p-codes. To improve readability, the p-codes are defined in CC.H as manifest constants. They have systematic names, so that even an unfamiliar p-code can usually be figured out simply by knowing the system.
Tables 21-2 and 21-3 list all of the Small C p-codes together with brief explanations of their effects. The p-codes in Table 21-2 are generated directly by the compiler, and those in Table 21-3 are generated by the optimizer as it operates on the compiler's p-codes.
____________________________________________ SYMBOL MEANING ____________________________________________ 0 the value zero 1 primary register (pr in comments) 2 secondary register (sr in comments) b byte f jump on false condition l current literal pool label number m memory reference by label n numeric constant p indirect reference thru pointer in sr r repeated r times s stack frame reference u unsigned w word _ incomplete instruction (sequence) ____________________________________________
DIV12udesignates a divide operation. The registers involved are the primary register (1) and the secondary register (2). Whenever registers (1 or 2) are specified, the left most (or only) register named receives the result of the operation. Thus, DIV12u yields the quotient in the primary register. Finally, the trailing letter u indicates that an unsigned operation is performed. Of course, a naming system like this can not tell everything. In this case, nothing designates which register contains the divisor and which the dividend.
_____________________________________________ P-CODE EFFECT ____________________________________________________ ADD12 add sr to pr ADDSP add to stack pointer AND12 AND sr to pr ANEG1 arithmetically negate pr ARGCNTn pass argument count to a function ASL12 arithmetically shift left sr by pr into pr ASR12 arithmetically shift right sr by pr into pr CALL1 call function thru pr CALLm call function directly BYTE_ define bytes (part 1) BYTEn define byte of value n BYTEr0 define r bytes of value 0 COM1 one's complement pr DBL1 double pr DBL2 double sr DIV12 divide pr by sr DIV12u divide pr by sr (unsigned) ENTER set stack frame upon function entry EQ10f jump if (pr == 0) is false EQ12 set pr TRUE if (sr == pr) GE10f jump if (pr >= 0) is false GE12 set pr TRUE if (sr >= pr) GE12u set pr TRUE if (sr >= pr) (unsigned) POINT1l point pr to function's literal pool POINT1m point pr to memory item thru label GETb1m get byte into pr from memory thru label GETb1mu get unsigned byte into pr from memory thru label GETb1p get byte into pr from memory thru sr ptr GETb1pu get unsigned byte into pr from memory thru sr ptr GETw1m get word into pr from memory thru label GETw1n get word of value n into pr GETw1p get word into pr from memory thru sr ptr GETw2n get word of value n into sr GT10f jump if (pr > 0) is false GT12 set pr TRUE if (sr > pr) GT12u set pr TRUE if (sr > pr) (unsigned) WORD_ define word (part 1) WORDn define word of value n WORDr0 define r words of value 0 JMPm jump LABm define label m LE10f jump if (pr <= 0) is false LE12 set pr TRUE if (sr <= pr) LE12u set pr TRUE if (sr <= pr) (unsigned) LNEG1 logical negation of pr LT10f jump if (pr < 0) is false LT12 set pr TRUE if (sr < pr) LT12u set pr TRUE if (sr < pr) (unsigned) MOD12 modulo pr by sr MOD12u modulo pr by sr (unsigned) MOVE21 move pr to sr MUL12 multiply pr by sr MUL12u multiply pr by sr (unsigned) NE10f jump if (pr != 0) is false NE12 set pr TRUE if (sr != pr) NEARm define near pointer thru label OR12 OR sr onto pr POINT1s point pr to stack item POP2 pop stack into sr PUSH1 push pr onto stack PUTbm1 put pr byte in memory thru label PUTbp1 put pr byte in memory thru sr ptr PUTwm1 put pr word in memory thru label PUTwp1 put pr word in memory thru sr ptr rDEC1 decrement pr (may repeat) REFm finish instruction with label RETURN restore stack and return rINC1 increment pr (may repeat) SUB12 sub sr from pr SWAP12 swap pr and sr SWAP1s swap pr and top of stack SWITCH call _SWITCH to find a switch's case XOR12 XOR pr with sr ____________________________________________
____________________________________________________ P-CODE EFFECT ____________________________________________________ ADD1n add n to pr ADD21 add pr to sr ADD2n add immediate value n to sr ADDbpn add n to memory byte thru sr pointer ADDwpn add n to memory word thru sr pointer ADDm_ add n to memory byte/word thru label (part 1) COMMAn finish instruction with ",n" DECbp decrement memory byte thru sr pointer DECwp decrement memory word thru sr pointer POINT2m point sr to memory thru label POINT2m_ point sr to memory thru label (part 1) GETb1s get byte into pr from stack GETb1su get unsigned byte into pr from stack GETw1m_ get word into pr from memory thru label (part 1) GETw1s get word into pr from stack GETw2m get word into sr from memory (label) GETw2p get word into sr thru sr pointer GETw2s get word into sr from stack INCbp increment byte in memory thru sr pointer INCwp increment word in memory thru sr pointer PLUSn finish instruction with "+n" POINT2s point sr to stack PUSH2 push sr PUSHm push word from memory thru label PUSHp push word from memory thru sr pointer PUSHs push word from stack PUT_m_ put byte/word into memory thru label (part 1) rDEC2 decrement sr (may repeat) rINC2 increment sr (may repeat) SUB_m_ subtract from memory byte/word thru label (part 1) SUB1n subtract n from pr SUBbpn subtract n from memory byte thru sr pointer SUBwpn subtract n from memory word thru sr pointer ____________________________________________________
A bit more explanation may help. The underscore symbol suffixes names that produce only the first part of an assembly language instruction or instruction sequence. The underscore should be read as though it were an ellipsis (...) meaning etc. It also prefixes names that complete an instruction (sequence).
The letter r indicates a repetition of whatever follows. The number of occurrences is indicated by the p-code's value. Thus,
rINC1produces as many occurrences of the instruction that increments the primary register as the p-code's corresponding value indicates.
Some p-codes produce an assembler instruction that contains a numeric (compiler generated) label. The value associated with these codes designate the label number.
The P-Code Output Function
Associated with each p-code is a character string that specifies the ASCII data that the p-code should produce. These strings are related to the p-codes by means of an array of string addresses called code[]. Each p-code is a subscript into this array. The designated array element contains the address of the string that translates the p-code. This arrangement makes for lightning fast testing of p-codes when they are translated. Outcode() simply uses the p-code as a subscript into codes[], then proceeds to process the indicated string.
Since Small C does not support the initializing of an array of pointers, the function setcodes() is called once, before parsing begins, to load code[] with its addresses. That function in CC4.C is the place to look for exactly what each p-code produces. It is very handy to have this association of p-code names and ASCII strings clustered at one place in the compiler. It saves a lot of searching around when we need to know the exact effect of a p-code.
As we saw in the description of gen(), several p-codes automatically trigger the generation of other codes. In these cases, we must also look at gen() to see the total effect of the original p-code. These p-codes are indicated with comments in setcodes().
Translating p-codes to ASCII strings involves more than simply writing a string for a given p-code. The value associated with the p-code may influence the final form of the output in several ways. Thus, the p-code strings include a kind of language that directs outcode() in its application of the p-code's value. Table 21-4 lists the p-code translation strings.
_____________________________________________ P-CODE TRANSLATION _____________________________________________ ADD12 \211ADD AX,BX\n ADD1n \010?ADD AX,<n>\n?? ADD21 \211ADD BX,AX\n ADD2n \010?ADD BX,<n>\n?? ADDbpn \001ADD BYTE PTR [BX],<n>\n ADDwpn \001ADD WORD PTR [BX],<n>\n ADDm_ \000ADD <m> ADDSP \100?ADD SP,<n>\n?? AND12 \211AND AX,BX\n ANEG1 \010NEG AX\n ARGCNTn \000?MOV CL,<n>?XOR CL,CL?\n ASL12 \011MOV CX,AX\nMOV AX,BX\nSAL AX,CL\n ASR12 \011MOV CX,AX\nMOV AX,BX\nSAR AX,CL\n CALL1 \010CALL AX\n CALLm \020CALL <m>\n BYTE_ \000 DB BYTEn \000 DB <n>\n BYTEr0 \000 DB <n> DUP(0)\n COM1 \010NOT AX\n COMMAn \000,<n>\n DBL1 \010SHL AX,1\n DBL2 \001SHL BX,1\n DECbp \001DEC BYTE PTR [BX]\n DECwp \001DEC WORD PTR [BX]\n DIV12 \011CWD\nIDIV BX\n DIV12u \011XOR DX,DX\nDIV BX\n ENTER \100PUSH BP\nMOV BP,SP\n EQ10f \010OR AX,AX\nJE $+5\nJMP _<n>\n EQ12 \211CALL __EQ\n GE10f \010OR AX,AX\nJGE $+5\nJMP _<n>\n GE12 \011CALL __GE\n GE12u \011CALL __UGE\n GETb1m \020MOV AL,<m>\nCBW\n GETb1mu \020MOV AL,<m>\nXOR AH,AH\n GETb1p \021MOV AL,?<n>??[BX]\nCBW\n GETb1pu \021MOV AL,?<n>??[BX]\nXOR AH,AH\n GETb1s \020MOV AL,<n>[BP]\nCBW\n GETb1su \020MOV AL,<n>[BP]\nXOR AH,AH\n GETw1m \020MOV AX,<m>\n GETw1m_ \020MOV AX,<m> GETw1n \020?MOV AX,<n>?XOR AX,AX?\n GETw1p \021MOV AX,?<n>??[BX]\n GETw1s \020MOV AX,<n>[BP]\n GETw2m \002MOV BX,<m>\n GETw2n \002?MOV BX,<n>?XOR BX,BX?\n GETw2p \021MOV BX,?<n>??[BX]\n GETw2s \002MOV BX,<n>[BP]\n GT10f \010OR AX,AX\nJG $+5\nJMP _<n>\n GT12 \010CALL __GT\n GT12u \011CALL __UGT\n INCbp \001INC BYTE PTR [BX]\n INCwp \001INC WORD PTR [BX]\n WORD_ \000 DW WORDn \000 DW <n>\n WORDr0 \000 DW <n> DUP(0)\n JMPm \000JMP _<n>\n LABm \000_<n>:\n LE10f \010OR AX,AX\nJLE $+5\nJMP _<n>\n LE12 \011CALL __LE\n LE12u \011CALL __ULE\n LNEG1 \010CALL __LNEG\n LT10f \010OR AX,AX\nJL $+5\nJMP _<n>\n LT12 \011CALL __LT\n LT12u \011CALL __ULT\n MOD12 \011CWD\nIDIV BX\nMOV AX,DX\n MOD12u \011XOR DX,DX\nDIV BX\nMOV AX,DX\n MOVE21 \012MOV BX,AX\n MUL12 \211IMUL BX\n MUL12u \211MUL BX\n NE10f \010OR AX,AX\nJNE $+5\nJMP _<n>\n NE12 \211CALL __NE\n NEARm \000 DW _<n>\n OR12 \211OR AX,BX\n PLUSn \000?+<n>??\n POINT1l \020MOV AX,OFFSET _<l>+<n>\n POINT1m \020MOV AX,OFFSET <m>\n POINT1s \020LEA AX,<n>[BP]\n POINT2m \002MOV BX,OFFSET <m>\n POINT2m_ \002MOV BX,OFFSET <m> POINT2s \002LEA BX,<n>[BP]\n POP2 \102POP BX\n PUSH1 \110PUSH AX\n PUSH2 \101PUSH BX\n PUSHm \100PUSH <m>\n PUSHp \100PUSH ?<n>??[BX]\n PUSHs \100PUSH ?<n>??[BP]\n PUT_m_ \000MOV <m> PUTbm1 \010MOV <m>,AL\n PUTbp1 \011MOV [BX],AL\n PUTwm1 \010MOV <m>,AX\n PUTwp1 \011MOV [BX],AX\n rDEC1 \010#DEC AX\n# rDEC2 \010#DEC BX\n# REFm \000_<n> RETURN \100?MOV SP,BP\n??POP BP\nRET\n rINC1 \010#INC AX\n# rINC2 \010#INC BX\n# SUB_m_ \000SUB <m> SUB12 \011SUB AX,BX\n SUB1n \010?SUB AX,<n>\n?? SUBbpn \001SUB BYTE PTR [BX],<n>\n SUBwpn \001SUB WORD PTR [BX],<n>\n SWAP12 \011XCHG AX,BX\n SWAP1s \012POP BX\nXCHG AX,BX\nPUSH BX\n SWITCH \012CALL __SWITCH\n XOR12 \211XOR AX,BX\n _______________________________________________
Now, notice that these strings include occurrences of the \n escape sequence which specifies the newline character. Outcode() gives these no special treatment, so each one has its normal effect in the output file--it begins a new line. Since some strings have embedded occurrences of newlines, it follows that some strings produce multi-line instruction sequences; that is, multiple instructions. Notice too that some strings do not end with a newline character. These are the strings for the p-codes whose names end with an underscore, meaning that more text must follow to complete the instruction(s).
The language mentioned above, which tells outcode() how to apply the value associated with the p-code, consists of five devices (or directives). They are:
?...?...?
Question marks always appear in groups of three. Arbitrary text may occur between them. When outcode() sees the first question mark, it tests the value associated with the p-code for true or false. If true, the text between the first two question marks is written to the output file; otherwise, the text between the second two is written. As we can see from scanning Table 21-4, this directive is used in two ways. First, it selects between alternate forms of code so as to produce the most efficient instructions; for example, it is more efficient to zero a register by performing an exclusive OR of it with itself than by loading the constant zero. The second use is in deciding whether or not to generate optional instructions (or parts of instructions). This usually appears as ?...??.
#...#
Number signs always occur in pairs. When outcode() sees the first number sign, it takes the p-code's value as a repetition count and writes the text between the number signs as many times as the count indicates. Thus, for instance, #INC AX\n# produces as many increment instructions as the p-code's value specifies.
<l>
A lowercase letter l (in angle brackets) tells outcode() to write the number of the current function's literal pool label as a decimal string. This is for references to string constants which reside in the literal pool that occurs at the end of each function. Each literal pool is preceded by a unique numeric label.
<m>
A lowercase letter m (in angle brackets) tells outcode() that the p-code's value is a pointer to a symbol table entry containing the name of a label which is to be used in a direct memory reference. On finding this, outcode() offsets the pointer by an amount that locates the symbol string in the table entry, and calls outname() to write the symbol to the output file as a name (in uppercase, preceded by an underscore).
<n>
A lowercase letter n (in angle brackets) tells outcode() that the p-code's value is a number that is to be written to the output file as a signed decimal string. Outcode() does not write the actual special characters and code letters to the output file. They are replaced by the output which they designate.
At this point, the task of outcode() has been fully explained! It accepts a p-code and its value and it outputs the p-code's string while carrying out the operations indicated by these directives. It uses three integer locals to help it carry out its special directives.
That failing, outcode() looks for a question mark. If one is found and it is the first question mark in the sequence, it tests the p-code's value. If zero, it sets skip true so that the first text segment will be bypassed. On finding the second question mark, skip is logically negated. If skipping was going on, the second text segment is written to the output file, and vice versa. Finally, on finding the third question mark, part is reset, so the next question mark will be taken as the first one of a sequence, and skip is set to false so that the following text will be written normally.
If these tests fail, outcode() tests the current character for a #. Upon finding one, and seeing that it is the first one, it saves the p-code's value in count and cp in back. The loop then continues normally. Text is written until the second # is found. At that point, count is decremented and tested for more repetitions. If any remain, cp is reset to the address saved in back and the loop continues. This repeats until no more repetitions remain, at which time back is reset to zero and the loop continues.
If none of these special cases exists, skip indicates whether or not the current character is to be written. In either case, cp is advanced over the current character and the loop continues.
When the end of the string is reached, the loop terminates and outcode() returns, having translated the p-code and its value to assembly language in the output file.