CHAPTER 21:

BACK END FUNCTIONS

This chapter describes the back end of the Small C compiler. The functions which comprise this part all reside in the fourth source file--CC4.C. They fall into three general categories: (1) code generation functions, (2) code optimizing functions, and (3) output functions. Since optimizing is a major topic, and since the optimizing functions are pretty much self-contained, that part of the back end is covered in a chapter of its own (Chapter 27).

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 ENDS

is generated, and if oldseg equals DATASEG, then

		DATA ENDS

is generated. If oldseg is zero, nothing is written.

Next, if newseg is non-zero, the new segment is initiated with either

		DATA SEGMENT PUBLIC

or

		CODE SEGMENT PUBLIC
		ASSUME CS:CODE, SS:DATA, DS:DATA

depending 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 : size

where 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)
            ____________________________________________

Table 21-1: Small C P-code Legend

Table 21-1 is a legend of the lowercase letters, digits, and special characters that are used in p-code names. In the explanations in Tables 21-2 and 21-3, the abbreviation pr refers to the primary register, and sr refers to the secondary register. Each p-code name is based on a verb that is written in uppercase letters. This verb indicates the basic operation performed at run time or assembly time by the p-code. Attached to this are numbers, lowercase letters, and/or special characters which further define the specific activity of the p-code. For example,

		DIV12u

designates 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
        ____________________________________________

Table 21-2: Compiler Generated P-codes

    ____________________________________________________    
     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
    ____________________________________________________

Table 21-3: Optimizer Generated P-codes

Take some time now to study Tables 21-1, 21-2, and 21-3 to become familiar with the p-code naming system. This will be time well spent. Learning the system will make reading the compiler's source files much easier.

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,

		rINC1

produces 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
_______________________________________________

Table 21-4: P-code Translation Strings

Probably, the most obvious thing about these strings is that each one begins with a byte that is coded with an octal escape sequence. That leading byte contains three codes that tell the optimizer about the effects of the instruction(s) in the string. It is not a part of the string proper, and so is skipped over when outcode() writes a string to the output file. More will be said about the meaning of these codes in Chapter 27.

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.

  1. Part indicates which part of a ?...?...? directive is being processed. It contains a number indicating which question mark has been encountered.
  2. Skip contains true or false, indicating whether or not the current part of the string is to be written to the output file. It is true during the part of a ?...?...? sequence that is to be skipped.
  3. Count counts down the number of repetitions of a #...# sequence.

Also, two character pointers are used.

  1. Cp is used to scan the string as it is being written.
  2. Back saves the value of cp when it reaches the first text character in a #...# sequence. It is used later, when the terminal number sign is reached, to reset cp for the next repetition. When zero, it means that the next number sign will be the first one of a sequence rather than the last.

On entry, outcode() initializes part and back to zero, and skip to false. It then sets cp to the second character of the p-code's string; this is where the translation from p-code to string is accomplished. Next, outcode() falls into a loop which lasts as long as non-zero characters remain in the string. In the loop, it first looks for a <, indicating one of the lettered directives <l>, <m>, or <n>. On finding one, and if the current code is not being skipped, it tests the letter with a switch statement. Three cases cover the possibilities. Then cp is advanced over the directive and the loop continues.

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.

Go to Chapter 22 Return to Table of Contents