Emacs: Please use -*- Text -*- mode. Thank you. $Id: porting.guide,v 1.26 1993/11/10 22:47:25 gjr Exp $ Copyright (c) 1991-1993 Massachusetts Institute of Technology LIAR PORTING GUIDE *DRAFT* Notes: This porting guide applies to Liar version 4.91, but most of the relevant information has not changed for a while, nor is it likely to change in major ways any time soon. This is an early version of this document, and the order of presentation leaves a lot to be desired. In particular, the document does not follow a monotonic progression, but is instead organized in a dictionary-like manner. We recommend that you read through the whole document twice since some important details, apparently omitted, may have their explanation later in the document. When reading the document for the second time, you will have an idea of where this other information is to be found, if it is present at all. We have attempted to insert sufficient forward pointers to make the first reading bearable, but we may have missed some. This document implicitly assumes that you are trying to build the compiler under Unix. The only compiler sources that depend on Unix are the sources that contain the pathnames of other files. The syntax could easily be changed for other file systems. This document uses Unix pathname syntax and assumes a hierarchical file system, but it should easy to map these directories to a different file system. The DOS runtime library accepts forward slashes (#\/) as a substitute for backward slashes (#\\), so the scripts are shared between Unix and DOS. This document also assumes that you are familiar with MIT Scheme, C, and the C preprocessor. It does not describe Liar in detail, and it does not cover many machine-independent portions at all. It is intended to guide a programmer familiar with (MIT) Scheme and C in the task of porting the compiler to a new architecture, not in modifying the compiler in any other way. For questions on Liar not covered by this document, or questions about this document, contact ``liar-implementors@zurich.ai.mit.edu''. Text tagged by ==> is intended primarily for the compiler developers. Good luck! Acknowledgments Liar is the work of many people. The current version is mostly the effort of Chris Hanson and Bill Rozas, with significant contributions from Mark Friedman and Jim Miller. Arthur Gleckler, Brian LaMacchia, and Henry Wu have also contributed to the current version of Liar. Many other people have offered suggestions and criticisms. The current Liar might never have existed had it not been for the efforts and help of the now-extinct BBN Butterfly Lisp group. That group included Don Allen, Seth Steinberg, Larry Stabile, and Anthony Courtemanche. Don Allen, in particular, babysat computers to painstakingly bootstrap the first version of the then new Liar. Many of the ideas and algorithms used in Liar, particularly at the RTL level, are taken from the GNU C compiler, written by Richard Stallman and many others. This document was written by Bill Rozas, with modifications and hints from the people listed above. The section on the MIT Scheme package system was written by Arthur Gleckler. 0. Introduction and a brief walk through Liar. Liar translates Scode as produced by the procedure SYNTAX, or by the file syntaxer (SF, for syntax file) into compiled code objects. The Scode is translated into a sequences of languages, the last of which is the binary representation of the compiled code. The sequence of external languages manipulated is Characters --READ--> S-Expressions --SYNTAX--> Scode --COMPILE-SCODE--> compiled code objects. Liar is a multi-pass compiler, where each major pass has multiple subpasses. Many of the subpasses do not manipulate the whole code graph, but instead follow threads that link the relevant parts of the graph. COMPILE-SCODE is the main entry point to Liar, although CBF (for compile bin file) is the usual entry point. CBF uses COMPILE-SCODE, and assumes that the code has been syntaxed by SF producing a .bin file, and dumps the resulting compiled code into a .com file. CF (for compile file) invokes SF and then CBF on a file name argument. The internal sub-languages used by Liar are: Scode --FGGEN--> Flow-graph --RTLGEN--> RTL (Register Transfer Language) --LAPGEN--> LAP (Lisp assembly program) --ASSEMBLER--> bits --LINK--> compiled code object. where FGGEN, etc., are some of the major passes of the compiler. The remaining major passes are FGOPT (the flow-graph optimizer), and RTLOPT (the RTL-level optimizer). RTL-level register allocation is performed by RTLOPT, and hardware-level register allocation is performed by LAPGEN. ASSEMBLER branch-tensions the output code. Branch-tensioning is described in a later section. LINK constructs a Scheme compiled code object from the bits representing the code and the fixed data that the compiled code uses at runtime. compiler/toplev.scm contains the top-level calls of the compiler and its pass structure. The ``.com'' files contain compiled code objects, which are linked further at load time. 0.1. Liar's package structure This section assumes that you are familiar with the MIT Scheme package system. If you are not, there is a small description in an appendix to this document. The package structure of the compiler reflects the pass structure and is specified in compiler/machines/port/comp.pkg, where port is the name of a machine (bobcat, vax, spectrum, mips, i386, alpha, etc.). The major packages are: (COMPILER): Utilities and data structures shared by most of the compiler. (COMPILER MACROS): Syntax extensions used by the compiler to define language translation rules. (COMPILER TOP-LEVEL): Top level pass structure of the compiler. (COMPILER FG-GENERATOR): This package contains the flow-graph generator, FGGEN. (COMPILER FG-OPTIMIZER): This package contains the flow-graph analyzer and optimizer, FGOPT. It has many sub-packages to contain the individual sub-passes. (COMPILER RTL-GENERATOR): This package contains the flow-graph to RTL translator, RTLGEN. It contains a few sub-packages for the major kinds of flow-graph operations. (COMPILER RTL-OPTIMIZER): This package contains most of the RTL-level optimizer, RTLOPT. It has various sub-packages corresponding to some of its sub-passes. (COMPILER RTL-CSE): This package contains the RTL-level common (redundant) subexpression eliminator pass of the RTL-level optimizer. (COMPILER LAP-SYNTAXER): This package contains most of the machine-dependent parts of the compiler and the back end utilities. In particular, it contains the RTL -> LAP translation rules, and the LAP -> bits translation rules, i.e. the LAPGEN and ASSEMBLER passes respectively. It has some sub-packages for various major utilities (linearizer, map-merger, etc.). (COMPILER ASSEMBLER): This package contains most of the machine-independent portion of the assembler. In particular, it contains the bit-assembler, i.e. the portion of the assembler that accumulates the bit strings produced by ASSEMBLER and performs branch-tensioning on the result. (COMPILER DISASSEMBLER): This package contains the disassembler. It is not needed for ordinary compiler operation, but is useful for low-level debugging, and debugging of the compiler and assembler. 0.2. Liar's sources' directory structure The directory structure loosely reflects the pass structure of the compiler. compiler/machines/port/comp.pkg declares the packages and the files that constitute them. compiler/back: This directory contains the machine-independent portion of the back end. It contains bit-string utilities, symbol table utilities, label management procedures, the hardware register allocator, and the top-level assembler calls. compiler/base: This directory contains common utilities used by the whole compiler, and the top level procedures provided by the compiler. compiler/etc: This directory contains utilities used for cross-compiling, and checking re-compilations. compiler/fggen: This directory contains the front end of the compiler. The code in this directory translates Scode into a flow-graph used by the analyzer and optimizer. compiler/fgopt: This directory contains the flow-graph analyzer and optimizer. compiler/rtlbase: This directory contains utilities used by the RTL generator and optimizer. compiler/rtlgen: This directory contains the code that translates the flow-graph into register transfer language (RTL). compiler/rtlopt: This directory contains the RTL-level optimizer. It contains code to perform lifetime analysis, redundant subexpression elimination, elimination of dead code, etc. compiler/machines: This directory contains a subdirectory for each port of the compiler. Each of these subdirectories contains the port (machine) dependent files of the compiler. compiler/machines/port: This directory contains the definition of machine parameters, the assembler rules, the disassembler, and RTL to assembly-language rules for the port. All machine-dependent files are in compiler/machines/port and this is the only directory that needs to be written to port the compiler to a new architecture. 1. Liar's runtime model. Liar does not open code (inline) all operations that the code would need to execute. In particular, it leaves error handling and recovery, interrupt processing, initialization, and invocation of unknown procedures, to a runtime library written in assembly language. Although this runtime library need not run in the context of the CScheme interpreter, currently the only implementation of this library runs from the interpreter and uses it for many of its operations. In other words, code generated by Liar does not depend on the interpreter directly, but indirectly through the runtime library. It does depend on the ability to invoke CScheme primitives at runtime, some of which (eval, etc.) require the interpreter to be present. It should be possible, however, to provide an alternate runtime library and primitive set that would allow code produced by Liar to run without the interpreter being present. (Foot) We often toy with this idea. On the other hand, since the only instance of the runtime library is that supplied by the interpreter, Liar currently assumes that the Scheme object representation is the same as that used by the interpreter, but this is relatively well abstracted and should not be hard to change. (Foot) Famous last words. The runtime library is currently implemented by microcode/cmpaux-port.m4 and microcode/cmpint.c . The files cmpaux.txt and cmpint.txt document these files. The documentation files may be found in the microcode or the documentation directories. microcode/cmpaux-port.m4 is an assembly language machine-dependent file that allows compiled Scheme to call the C-written library routines and vice versa. It is described in cmpaux.txt. microcode/cmpint.c defines the library in a machine-independent way, but requires some information about the port and this is provided in microcode/cmpint2.h, a copy (or link) of the appropriate microcode/cmpint-port.h file. The microcode/cmpint-port.h files are described in cmpint.txt . cmpint.txt also describes many of the data structures that the compiled code and runtime library manipulate, and defines some of the concepts needed to understand the compiler. The rest of this document assumes that you are using the runtime library provided by the CScheme interpreter. If you wish to use Liar as a compiler for stand-alone programs, a lot of work needs to be done, and this work is not described here. Perhaps we will do it in the future. If you have not yet read cmpaux.txt and cmpint.txt, please do so before reading the rest of this document. You should probably also read [1] and [2] for a discussion of some of the implementation issues. 2. Preliminary Observations 2.1. Constraints on architectures to which Liar can be ported: - Liar assumes that the target machine is a general-register machine. That is, operations are based on processor registers, and there is a set of general-purpose registers that can be used interchangeably. It would be hard to port Liar to a pure stack machine, a graph-reduction engine, a Turing machine, or a 4-counter machine. However, the register set required is not huge. Liar has been ported to the 386/486 architecture which only has eight registers, four of which are reserved for implementation quantities (e.g. stack pointer and free pointer) and four of which are left to the register allocator. - Liar currently assumes that floating-point registers and integer registers are separate or the same size. In other words, currently Liar cannot handle quantities that need multiple registers to hold them. For example, on the DEC VAX and the Motorola 88100, there is a single set of registers, and double floating point values (the only kind used by Scheme) take two consecutive integer registers. The register allocator in Liar does not currently handle this situation, and thus, floating-point operations are not currently open-coded on the VAX. - Liar assumes that the target machine has an address space that is flat enough that all Scheme objects can be addressed uniformly. In other words, segmented address spaces with segments necessarily smaller than the Scheme runtime heap (i.e. Intel 286) will make Liar difficult to port. - Liar assumes that instructions and data can coexist in the same address space, and that new code objects that contain machine instructions can be dynamically allocated from and written to the heap (memory pool) used to allocate all other Scheme objects. This assumption in Liar conflicts with some current hardware that has programmer-visible separate (split) data and instruction caches -- that is, there are two different caches, one used by the processor for instruction references and the other for data references, and storing data into memory only updates the data cache, but not the instruction cache, and perhaps not even memory. Most of the problems this causes can be resolved if the user is given enough control over the hardware caches, i.e. some way to flush or synchronize them. Furthermore, a true Harvard architecture, with separate code and data memories, would be hard to accommodate without relatively major changes. At some point in the future we may write a C back end for Liar that handles this case, since C code space and data space are typically kept separate by the operating system. Whatever technique the C back end may use can probably be emulated by architectures with such a strong division, although it is likely to be expensive. 2.2. Some implementation decisions that may make your job harder or impair the quality of the output code: - Liar generates code that passes arguments to procedures on a stack. This decision especially affects the performance on load-store architectures, common these days. Liar may be changed in the future to generate code that passes arguments in registers because most modern machines have large register sets and memory-based operations are slower than register-based operations even when the memory locations have been cached. - Liar assumes that pushing and popping elements from a stack is cheap. Currently Liar does not attempt to bump the stack pointer once per block of operations, but instead bumps it once per item. This is expensive on many modern machines where pre-and-post incrementing are not supported by the hardware. This may also change in the not-too-far future. - Liar assumes that it is cheap to compute overflow conditions on integer arithmetic operations. Generic arithmetic primitives have the frequent fixnum (small integer) case open coded, and the overflow and non-fixnum cases coded out of line, but this depends on the ability of the code to detect and branch on overflow conditions cheaply. This is not true of some modern machines, notably MIPS processors. If your processor does not make branching on such conditions reasonably cheap, you may have to use code similar to that used in the MIPS port. The MIPS processor has trapping and non-trapping arithmetic instructions. The trapping arithmetic instructions trap on overflow, but the trap recovery code is typically so expensive that the generated code computes the overflow conditions explicitly. - Liar assumes that extracting, inserting, and comparing bit-fields is relatively cheap. The current object representation for Liar (compatible with the interpreter) consists of using a number of bits (usually 6) in the most significant bit positions of a machine word as a type tag, and the rest as the datum, usually an encoded address. Not only must extracting, comparing, and inserting these tags be cheap, but decoding the address must be cheap as well. These operations are relatively cheap on architectures with bit-field instructions, but more expensive if they must be emulated with bitwise boolean operations and shifts, as on the MIPS R3000. Decoding a datum into an address may involve inserting segment bits in some of the positions where the tag is placed, further increasing the dependency on cheap bit-field manipulation. - The CScheme interpreter uses a particularly poor representation for fixnums, forcing Liar's hand. Fixnums are suitably small integers. They are immediate objects with a particular tag. This tag was not wisely chosen, making fixnum operations more expensive than they need to be. This tag may be changed in the future. - The CScheme interpreter manipulates a stack that grows in a fixed direction (from higher to lower addresses). On many modern machines, there are no special instructions to deal with the stack, so the decision is arbitrary. On some machines, however, there are special instructions to pop and push elements on the stack. Liar may not be able to use these instructions if the machine's preferred direction of stack growth does not match the interpreter's. 2.3. Emulating an existing port. The simplest way to port Liar is to find an architecture to which Liar has already been ported that is sufficiently similar to the target architecture that most of the code can be written by copying or trivially translating existing code. In particular, if the architectures are really close, there may be no need for architecture-specific additional tuning. The compiler is primarily developed on Motorola MC68020 processors, so this is the best-tuned version, and the other ports are not very well tuned or not tuned at all. If you improve an existing port, please share the improvements by notifying liar-implementors. - If you have a Vax-like CISC machine, you can try starting from the Vax, the Motorola MC68020, or the i386 ports. The Vax and i386 ports were written by starting from the MC68020 port. This is probably the best solution for some architectures like the NS32000, and perhaps even the IBM 370. - If you have an ``enlarged'' RISC processor, with some complex addressing modes, and bit-field instructions, you may want to start by looking at the Spectrum (HP Precision Architecture) port. This is probably a good starting point for the Motorola 88000 and for the IBM RS/6000. - If you have a bare-bones RISC processor, similar to a MIPS R3000 processor, you may want to start from this port. Since the MIPS R3000 is a minimalist architecture, it almost subsumes all other RISCs, and may well be a good starting point for all of them. This is probably a good starting point for the Sparc. The MIPS port used the Spectrum port as its model, and the Alpha port used the MIPS port as its model. - If you have a machine significantly different from those listed above, you are out of luck and will have to write a port from scratch. For example, the port to the Intel 386+387/486 uses some of the concepts and code from ports to other CISCs, but due to the floating-point stack architecture (instead of register-based), the floating-point stack management is different (but not very good). Of course, no architecture is identical to any other, so you may want to mix and match ideas from many of the ports already done, and it is probably a good idea for you to compare how the various ports solve the various problems. 3. Compiler operation, LAPGEN rules and ASSEMBLER rules. The front end of the compiler translates Scode into a flow-graph that is then translated into RTL. The back end does machine-independent optimization on the RTL, generates assembly language (in LAP format) from the RTL, and assembles the resulting bits. Although RTL is a machine-independent language, the particular RTL generated for a given program will vary from machine to machine. The RTL can vary in the following ways: - RTL is a language for manipulating the contents of conceptual registers. RTL registers are divided into ``pseudo registers'' and ``machine registers''. Machine registers represent physical hardware registers, some of which have been reserved and given fixed meanings by the port (stack pointer, value register, etc.) while pseudo-registers represent conceptual locations that contain quantities that will need physical registers or memory locations to hold them in the final translation. An RTL pseudo register can be mapped to any number of physical registers in the final translation, and may ``move'' between physical registers. In order to make the RTL more homogeneous, the RTL registers are not distinguished syntactically in the RTL, but are instead distinguished by their value range. Machine registers are represented as the N lowest numbered RTL registers (where N is the number of hardware registers), and all others are pseudo registers. Since some RTL instructions explicitly mention machine registers and these (and their numbers) vary from architecture to architecture, the register numbers in an RTL program will vary depending on the back end in use. Machine registers may be divided into separate classes (e.g. address, data, and floating-point registers) that can contain different types of values. Pseudo registers are not distinguished a-priori, but the values stored in them must be consistent. For example, if a floating point value is stored into a particular pseudo register, the register can only be mapped to floating-point machine registers, and non-floating-point values cannot be stored in it. - RTL assumes a load-store architecture, but can accommodate architectures that allow memory operands and rich addressing modes. RTL is constructed by generating statements that include relatively complex expressions. These expressions may represent multiple memory indirections or other operations. An RTL simplifier runs over this initial RTL, assigning these intermediate quantities to new pseudo registers and rewriting the original statements to manipulate the original and new pseudo-registers. Typically this simplification results in a sequence of assignments to pseudo-registers with single operations per assignment and where the only memory operations are load and store. However, this simplification pass is controlled by the port. The port supplies a set of rewriting rules to the simplifier that causes the simplifier to leave more complex expressions untouched, or to be simplified in different ways, depending on the availability of memory operands or richer addressing modes. Since these rules vary from port to port, the final RTL differs for the different ports. The simplification process is also controlled by the availability of various rules in the port, and ports for richer instruction sets may require less simplification because hardware instructions and addressing modes that encode more complicated RTL patterns are directly available. - The open coding (inlining) of Scheme primitives is machine-dependent. On some machines, for example, there is no instruction to multiply integers, and it may not be advantageous to open code the multiplication primitive. The RTL for a particular program may reflect the set of primitive operations that the back end for the port can open code. The resulting RTL program is represented as a control flow-graph where each of the nodes has an associated list of RTL statements. The edges in the graph correspond to conditional and unconditional branches in the code, and include a low-level predicate used to choose between the alternatives. The graph is linearized after the instructions have been translated to LAP. There is a debugging RTL linearizer used by the RTL output routine. Besides assignments and tests, the RTL has some higher level statements that correspond to procedure headers, continuation (return address) headers, etc. Thus an RTL program is made mostly of register to register operation statements, a few conditional tests, and a few higher-level ``glue'' statements. Once a program has been translated to RTL, the RTL code is optimized in a machine-independent way by minimizing the number of RTL pseudo-registers used, removing redundant subexpressions, eliminating dead code, and by using various other techniques. The RTL program is then translated into a Lisp-format assembly-language program (LAP). Hardware register allocation occurs during this translation. The register allocator is machine-independent and can accommodate different register classes, but does not currently accommodate register pairs (this is why floating point operations are not currently open coded on the Vax). The register allocator works by considering unused machine registers (those not reserved by the port) to be a cache for the pseudo-registers. Thus a particular pseudo-register may map into multiple machine registers of different types, and these aliases are invalidated as the pseudo-registers are written or the corresponding machine registers reused. Thus the most basic facility that the register allocator provides is a utility to allocate an alias of a particular type for a given pseudo-register. The port defines the types and numbers of machine registers and the subset that is available for allocation, and the register allocator manages the associations between the pseudo-registers and their aliases and the set of free machine registers. The register allocator also automatically spills the contents of machine registers to memory when pressed for machine registers, and reloads the values when necessary. Thus the resulting LAP program is the collection of the code issued by the rules that translate RTL into LAP, the instructions issued behind the scenes by the register allocator, and the instructions used to linearize the control flow graph. The back end provides a set of rules for the translation of RTL to LAP, and a set of procedures that the register allocator and the linearizer use to generate such instructions. The rules are written using a special syntax that creates entries in a data base used by a pattern matcher to translate the RTL into LAP. The linear LAP is then translated into binary form by using the same pattern matcher with a different set of rules. These rules define the translation between assembly language and machine language for the architecture. Most of these rules output bit strings to be collected together, but some output a set of directives to the bit-level assembler to define labels, or choose between alternative encoding of the fields depending on the final value of a displacement. These alternative encodings are typically used for PC-relative quantities. The machine-independent bit-assembler collects all the bits together and keeps track of a virtual program counter used to determine the distance between instruction fields. A relaxation process is used to reduce the size of the resulting encoding (to tension branches, i.e. to choose the smallest encoding that will do the job when there are alternatives). Since most of the LAPGEN rules generate almost fixed assembly language, where the only difference is the register numbers, most of the LAP to bits translation can be done when the compiler is compiled. A compiler switch, ``COMPILER:ENABLE-EXPANSION-DECLARATIONS?'' allows this process to take place. This mechanism has not been used for a while, however, because the resulting compiler was, although somewhat faster, considerably bigger, so this switch may not currently work. Several other compiler parameters and switches control various aspects of the operation of the back end. Most parameters and switches are machine independent, and are defined in compiler/base/switch.scm . The remaining parameters and switches are defined in compiler/machines/port/machin.scm. All compiler parameters and switches are exported to the Scheme global package for easy manipulation. The following switches are of special importance to the back end writer: * COMPILER:COMPILE-BY-PROCEDURES? This switch controls whether the compiler should compile each top-level lambda expression independently or compile the whole input program (or file) as a block. It is usually set to true, but must be set to false for cross-compilation. The cross-compiler does this automatically. (Foot) The reason for this is that the gc offset words in compiled entry points may differ for the source and target machines, and thus the source machine's garbage collector may be confused by the target machine's compiled entry points. This is circumvented by having the cross compiler generate a single compiled code block object, manipulated and dumped as a vector object (instead of as an entry point). The final entry points are generated by cross-compile-bin-file-end running interpreted on the target machine. * COMPILER:OPEN-CODE-PRIMITIVES? This switch controls whether Liar will open code (inline) MIT Scheme primitives. It is usually set to true and should probably be left that way. On the other hand, it is possible to do a lot less work in porting the compiler by not providing the open coding of primitives and turning this switch off. Some of the primitives are open coded by the machine-independent portion of the compiler, since they depend only on structural information, and not on the details of the particular architecture. In other words, CAR, CONS, and many others can be open-coded in a machine-independent way since their open codings are performed directly in the RTL. Turning this switch to false would prevent the compiler from open coding these primitives as well. * COMPILER:GENERATE-RTL-FILES? and COMPILER:GENERATE-LAP-FILES? These are mostly compiler debugging switches. They control whether the compiler will issue .rtl and .lap files for every file compiled. The .rtl file will contain the RTL for the program, and the .lap file will contain the input to the assembler. Their usual value is false. * COMPILER:INTERSPERSE-RTL-IN-LAP? This is another debugging switch. If turned on, and COMPILER:GENERATE-LAP-FILES? is also on, the lap output file includes the RTL statements as comments preceding their LAP translations. Its usual value is true. ==> RTL predicates are not included, making the control-flow hard to follow. This should be fixed. * COMPILER:OPEN-CODE-FLOATING-POINT-ARITHMETIC? This switch is defined in compiler/machines/port/machin.scm and determines whether floating point primitives can and should be open coded by the compiler or not. If the port provides open codings for them, it should be set to true, otherwise to false. * COMPILER:PRIMITIVES-WITH-NO-OPEN-CODING This parameter is defined in compiler/machines/port/machin.scm. It contains a list of primitive names that the port cannot open code. Currently there is no simple list of all the primitives that Liar can open-code. The list is implicit in the code contained in rtlgen/opncod.scm. ==> The last two parameters should probably be combined and inverted. COMPILER:PRIMITIVES-WITH-OPEN-CODINGS should replace both of the above. This has the advantage that if the RTL level is taught how to deal with additional primitives, but not all ports have open codings for them, there is no need to change all the machin.scm files, only those for which the open coding has been provided. 4. Description of the machine-specific files The following is the list of files that usually appears in the port directory. The files can be organized differently for each port, but it is probably easiest if the same pattern is kept. In particular, the best way to write most is by editing the corresponding files from an existing port. Keeping the structure identical will make writing decls.scm, comp.pkg, and comp.sf straightforward, and will make future updates easier to track. A useful thing to do when writing new port files is to keep track of the original version from which you started, and additionally, that on which your original is based. For example, if you use machines/mips/assmd.scm as a model for your version, in it you would find something like $ Header: assmd.scm,v 1.1 90/05/07 04:10:19 GMT jinx Exp $ $MC68020-Header: assmd.scm,v 1.36 89/08/28 18:33:33 GMT cph Exp $ In order to allow an easier merge in the future, it would be good if you transformed this header into $ Header $ $mips-Header: assmd.scm,v 1.1 90/05/07 04:10:19 GMT jinx Exp $ $MC68020-Header: assmd.scm,v 1.36 89/08/28 18:33:33 GMT cph Exp $ The new $ Header $ line would be used by RCS to keep track of the versions of your port and the others could be used to find updates to the originals that would make updating your port easier. 4.1. Compiler building files: * comp.pkg: This file describes the Scheme package structure of the compiler, the files loaded into each package, and what names are exported and imported from each package. To write this file, copy the similar file from an existing port, change the name of the port (i.e. mips -> sparc), and add or remove files as appropriate. You should only need to add or remove assembler and LAPGEN files. * comp.cbf: This file is a script that can be used to compile the compiler from scratch. You can copy this file from another port, and change the port name. There is more information in a later section about how to build the compiler. * comp.sf: This file is a script that is used to pre-process the compiler sources before they are loaded to be interpreted or compiled. You should be able to copy the file from an existing port and replace the name of the port. You should also edit the names of the instruction files in the assembler instruction database section, although this section is no longer used by default. The previous three files should be copied or linked to the top-level compiler directory. I.E., compiler/comp.pkg should be a link (symbolic preferably) or copy of compiler/machines/port/comp.pkg . * comp.con, comp.ldr, comp.bcon, and comp.bldr: These files are generated by the CREF subsystem from the information in the cref.pkg file. The .bcon and .bldr files are binary versions of the others, which are scheme sources. The .con file contains the ``connectivity code'', that is, the code to create and link the package objects specified in the .pkg file. The .ldr file contains the ``loading code'', that is, the code to load the source files into the appropriate packages and, in theory, to initialize the packages. The CREF subsystem also generates a comp.cref file that includes cross-reference information. It is useful to examine this file to find unbound references (often typos). * make.scm: This file is used to load the compiler on top of a runtime system that has the file syntaxer (SF) loaded, and defines the version of the compiler. The list of files does not appear here because the comp.pkg already declares them, CREF will build the comp.con and comp.ldr files that contain this information and will be loaded by make.scm. * decls.scm: This file defines the pre-processing dependencies between the various source files. There are three kinds of pre-processing dependencies: - Syntactic: Different files need to be processed in different syntax tables that define the macros used by the files. - Integrations: Different files import integrable (inline) definitions from other files, and must be processed in the right sequence in order to obtain the maximum effect from the integrations (mostly because of transitive steps). - Expansions: Certain procedures can be expanded at compiler pre-processing time into accumulations of simpler calls. This is how the assembly language in the LAPGEN rules can be translated into bits at compiler pre-processing time. The files that define the pre-processing-time expansion functions must be loaded in order to process those files that use the procedures that can be expanded. decls.scm builds a database of the dependencies. This database is topologically sorted by some of the code in decls.scm itself in order to determine the processing order. Since there are circularities in the integration dependencies, some of the files are processed multiple times, but the mechanism in decls takes care of doing this correctly. You should be able to edit the version from another port in the appropriate way. Mostly you will need to rename the port (i.e. mips -> sparc), and add/delete instruction and rule files as needed. ==> decls.scm should probably be split into two sections: The machine-independent dependency management code, and the actual declaration of the dependencies for each port. This would allow us to share more of the code, and make the task of rewriting it less daunting. 4.2. Miscellaneous files: * rgspcm.scm: This file declares a set of primitives that can be coded by invoking runtime library procedures. This file is no longer machine dependent, since the portable library has made all the sets identical. It lives in machines/port for historical reasons, and should probably move elsewhere. Obviously, you can just copy it from another port. ==> Let's move it or get rid of it! * rulrew.scm: This file defines the simplifier rules that allow more efficient use of the hardware's addressing modes and other capabilities. The rules use the same syntax as the LAPGEN rules, but belong in the (rule) rewriting database. Although these rules are machine-dependent, it should be straightforward to emulate what other ports have done in order to arrive at a working set. Moreover, it is possible to start out with an empty set and only add them as inefficiencies are discovered in the output assembly language. These rules manipulate RTL expressions by using the procedures defined in compiler/rtlbase/rtlty1.scm and compiler/rtlbase/rtlty2.scm. * lapopt.scm: This file defines a LAP-level peephole optimizer. Currently only used in the MIPS port to reduce the number of NOPs in the ``delay slots'' of load instructions. The instructions in each LAP-level basic block are passed to optimize-linear-lap, which outputs the new sequence of instructions corresponding to the basic block. Currently all ports (except the MIPS port) implement this procedure as the identity procedure. * machin.scm: This file defines architecture and port parameters needed by various parts of the compiler. The following is the current list of the primary parameters. The definitions of derived parameters not mentioned here should be copied verbatim from existing ports. Some of these parameters are not currently in use, but should all be provided for completeness. - USE-PRE/POST-INCREMENT?: Should be true or false depending on whether the architecture has addressing modes that update the base address. It is true on the MC68020, Vax, i386, and HP-PA, and false on the MIPS and Alpha. - ENDIANNESS: Should be the symbol LITTLE if an address, when used as a byte address, refers to the least significant byte of the long-word addressed by it. It should be BIG if it refers to the most significant byte of the long-word. The compiler has not been ported to any machines where the quantum of addressability is not an 8-bit byte, so the notion may not apply to those. - ADDRESSING-GRANULARITY: How many bits are addressed by the addressing quantum. I.e., increasing an address by 1 will bump the address to point past this number of bits. Again, the compiler has not been ported to any machine where this value is not 8. - SCHEME-OBJECT-WIDTH: How many bits are taken up by a Scheme object. This should be the number of bits in a C ``unsigned long'', since Scheme objects are declared as such by the portable runtime library. - SCHEME-TYPE-WIDTH: How many bits at the most-significant end of a Scheme object are taken up by the type tag. The value of TYPE_CODE_LENGTH in the microcode must match this value. The value is currently 6 for systems with a compiler and 8 for systems without one. - ADDRESS-UNITS-PER-PACKED-CHAR: This parameter defines how much to increment an address by in order to make it point to the next character in a string. The compiler has not been ported to any configuration where this is not 1, but may be if 16-bit characters are used in the future. - FLONUM-SIZE: This is the ceiling of the ratio of the size of a C ``double'' to the size of a C ``unsigned long''. It reflects how many Scheme units of memory (measured in Scheme objects) the data in a Scheme floating point object will take. - FLOAT-ALIGNMENT: This value defines the bit-alignment constraints for a C ``double''. It must be a multiple of scheme-object-width. If floating point values can only be stored at even long-word addresses, for example, this value should be twice scheme-object-width. - SIGNED-FIXNUM/UPPER-LIMIT: This parameter should be derived from others, but is specified as a constant due to a shortcoming of the compiler pre-processing system (EXPT is not constant-folded). Use the commented-out expression to derive the value for your port. All values that should be derived but are instead specified as constants are tagged by a comment containing ``***''. - STACK->MEMORY-OFFSET: This procedure is provided to accommodate stacks that grow in either direction, but we have not tested any port in which the stack grows towards larger addresses, because the CScheme interpreter imposes its own direction of growth. It should probably be copied verbatim. - EXECUTE-CACHE-SIZE: This should match EXECUTE_CACHE_ENTRY_SIZE in microcode/cmpint-port.h, and is explained in cmpint.txt. ==> We should probably rename one or the other to be alike. The following parameters describe to the front-end the format of closures containing multiple entry points. Closures are described in some detail in cmpint.txt and in section 5.3.3. Very briefly, a closure is a procedure object that contains a code pointer and a set of free variable locations or values. - CLOSURE-OBJECT-FIRST-OFFSET: This procedure takes a single argument, the number of entry points in a closure object, and computes the distance in long-words between the first long-word in the closure object, and the first long-word containing a free variable. This is the number of long-words taken up by the closure object's header, and the code to represent N closure entry points. - CLOSURE-FIRST-OFFSET: This procedure takes two arguments, the number of entry points in a closure object, and the index of one of them, the first being zero. It computes the distance between that entry's environment pointer and the first free variable in the closure object. The entry's environment pointer will be the address of the entry point itself if closure entry points are always aligned on long-word boundaries, or the address of the first entry point if they are not. - CLOSURE-ENTRY-DISTANCE: This procedure is given the number of entry points in a closure object, and the indices for two of its entry points, and computes the number of bytes that separate the two entry points in the closure object. This distance should be a multiple of the parameter COMPILED_CLOSURE_ENTRY_SIZE described in cmpint.txt and defined in microcode/cmpint-port.h. - CLOSURE-ENVIRONMENT-ADJUSTMENT: This procedure takes two parameters, the number of entry points in a closure object, and the index of one of them. It computes the number of bytes that must be added to the entry point's address to result in the entry point's environment pointer. If entry points are always aligned on long-word boundaries, this number should always be zero, otherwise it should be the distance to the first (lowest addressed) entry point. The remaining code in machin.scm describes the register set of the architecture and defines the register conventions imposed by the port. These conventions must match the expectations of microcode/cmpaux-port.m4 described in cmpaux.txt. Machine registers are assigned a contiguous range of non-negative integers starting from zero. Typically symbolic names are given to each of these integers for use in some of the rules, especially those dealing with the assembly language interface. - NUMBER-OF-MACHINE-REGISTERS should be the number of machine registers, i.e. one greater than the number assigned to the last machine register. - NUMBER-OF-TEMPORARY-REGISTERS is the number of reserved memory locations used for storing the contents of spilled pseudo-registers. Liar requires certain fixed locations to hold various implementation quantities such as the stack pointer, the heap (free memory) pointer, the pointer to the runtime library and interpreter's ``register'' array, and the dynamic link ``register''. Typically each of these locations is a fixed machine register. In addition, a processor register is typically reserved for returned values and another for holding a bit-mask used to clear type tags from objects (the pointer or datum mask). All of these registers should be given additional symbolic names. ==> What is MACHINE-REGISTER-KNOWN-VALUE used for? It would seem that the datum mask is a known value, but... Currently all the ports seem to have the same definition. The contents of pseudo-registers are divided into various classes to allow some consistency checking. Some machine registers always contain values in a fixed class (e.g. floating point registers and the register holding the datum mask). - MACHINE-REGISTER-VALUE-CLASS is a procedure that maps a register to its inherent value class. The main value classes are value-class=object, value-class=address, and value-class=float. The registers allocated for the special implementation quantities have fixed value classes. The remaining registers, managed by the compiler's register allocator, may be generic (value-class=word) or allow only certain values to be stored in them (value-class=float, value-class=address, etc.). Most of the remainder of compiler/machines/port/machin.scm is a set of procedures that return and compare the port's chosen locations for various operations. Some of these operations are no longer used by the compiler, and reflect a previous reliance on the interpreter to accomplish certain environment operations. These operations are now handled by invoking the appropriate primitives rather than using special entry points in the runtime library for them. Under some compiler switch settings the older methods for handling these operations can be re-activated, but this never worked completely, and may no longer work at all. - RTL:MACHINE-REGISTER? should return a machine register for those special RTL registers that have been allocated to fixed registers, and false otherwise. - RTL:INTERPRETER-REGISTER? should return the long-word offset in the runtime library's memory ``register'' array for those special RTL registers not allocated to fixed registers, and false otherwise. - RTL:INTERPRETER-REGISTER->OFFSET errors when the special RTL register has not been allocated to a fixed register, and otherwise returns the long-word offset into the register array. - RTL:CONSTANT-COST is a procedure that computes some metric of how expensive is to generate a particular constant. If the constant is cheaply reconstructed, the register allocator may decide to flush it (rather than spill it to memory) and re-generate it the next time it is needed. The best estimate is the number of cycles that constructing the constant would take, but the number of bytes of instructions can be used instead. - COMPILER:OPEN-CODE-FLOATING-POINT-ARITHMETIC? and COMPILER:PRIMITIVES-WITH-NO-OPEN-CODING have been described in the section on compiler switches and parameters. 4.3. LAPGEN files: The following files control the RTL -> LAP translation. They define the rules used by the pattern matcher to perform the translation, and procedures used by the register allocator and linearizer to connect the code that results from each rule. The rules, and how to write them, are described further in a later section. The rule set is partitioned into multiple subsets. This is not necessary, but makes re-compiling the compiler faster and reduces the memory requirements of the compiler. The partition can be done in a different way, but is probably best left as uniform as possible between the different ports to facilitate comparison and updating. The LAPGEN (RTL->LAP) rules are separated into two different data bases. The larger is the statement data base, used to translate whole RTL instructions. The smaller is the predicate data base, used to translate decisions to branch between the RTL basic blocks. * lapgen.scm: This file does not define any rules, but provides a set of utilities for the back end. It provides utilities for the rules, typically procedures for generating code that manipulates the object representation, additional entry points to the register allocator that are better suited to the port, and the interface procedures for the register allocator and the linearizer. The following definitions constitute the register allocator interface and must be provided by lapgen.scm: - AVAILABLE-MACHINE-REGISTERS is a list of the RTL register numbers corresponding to those registers that the register allocator should manage. This should include all machine registers except those reserved by the port. - SORT-MACHINE-REGISTERS is a procedure that reorders a list of registers into the preferred allocation order. ==> Is this right? - REGISTER-TYPE is a procedure that maps RTL register numbers to their inherent register types (typically GENERAL and FLOAT). - REGISTER-TYPES-COMPATIBLE? is a boolean procedure that decides whether two registers can hold the same range of values. - REGISTER-REFERENCE maps RTL register numbers into register references, i.e. pieces of assembly language used to refer to those registers. - REGISTER->REGISTER-TRANSFER issues code to copy the contents of one RTL register into another. - REFERENCE->REGISTER-TRANSFER issues code to copy the contents of a machine register described by its reference into a given RTL register. - PSEUDO-REGISTER-HOME maps RTL registers to a fragment of assembly language used to refer to the memory location into which they will be spilled if necessary. This is typically a location (or set of locations) in the Scheme ``register'' array. - HOME->REGISTER-TRANSFER generates code that copies the contents of an RTL register's home (its spill location) into a machine register. - REGISTER->HOME-TRANSFER generates code that copies the contents of an RTL register, currently held in a machine register, into its memory home. The following definitions constitute the linearizer interface, and must be provided by lapgen.scm: - LAP:MAKE-LABEL-STATEMENT generates an assembly language directive that defines the specified label. - LAP:MAKE-UNCONDITIONAL-BRANCH generates a fragment of assembly language used to unconditionally transfer control to the specified label. - LAP:MAKE-ENTRY-POINT generates a fragment of assembly language used to precede the root of the control flow graph. Its output should use the assembler directive ENTRY-POINT and generate format and GC words for the entry point. The rest of the code in lapgen.scm is a machine-specific set of utilities for the LAPGEN rules. Some of the more common procedures are described in the section that covers the rules. Of special interest are the utility procedures for manipulating pc-relative addresses and loads on RISC machines. RISC machines typically only have pc-relative branch instructions, but no pc-relative loads or pc-relative load-effective-address instructions. On the other hand, they usually have a branch-and-link instruction that performs a pc-relative branch and stores the return address in a processor register. This instruction can be used (by branching to the next instruction) to obtain its own address, and pc-relative addresses and loads can use them. The MIPS back end currently implements a simple pc-relative address caching scheme that attempts to reduce the number of such branches by re-using the values produced by previous branches if they are still available. This code can be suitably modified to work on most RISC architectures. * rules1.scm: This file contains RTL statement rules for simple register assignments and operations. In particular, it contains the rules for constructing and destructuring Scheme objects, allocating storage, and memory <-> register transfers. * rules2.scm: This file contains RTL predicate rules for simple equality predicates (EQ-TEST, TYPE-TEST). * rules3.scm: This file contains RTL statement rules for control-flow statements like continuation (return address) invocation, several mechanisms for invoking procedures, stack reformatting prior to invocation, procedure headers, closure object allocation, expression headers and declaring the data segment of compiled code blocks for assembly. See [1] for some background information on stack reformatting, and [2] for a discussion of how calls to (the values of) free variables are handled by Liar. * rules4.scm: This file contains RTL statement rules for runtime library routines that handle manipulation of variables in first class environments. Many of these rules are no longer used by the compiler unless some switch settings are changed. See [2] for a discussion of how Liar handles references to free variables. * rulfix.scm: This file contains statement and predicate rules for manipulating fixnums (small integers represented in immediate form). The rules handle tagging and de-tagging fixnum objects, arithmetic on them, comparison predicates, and overflow tests. * rulflo.scm: This file contains statement and predicate rules for manipulating flonums (floating point data in boxed form). The rules handle boxing and un-boxing of flonums, arithmetic on them, and comparison predicates. 4.4. Assembler files: * assmd.scm: This file defines the following machine-dependent parameters and utilities for the bit-level assembler: - MAXIMUM-PADDING-LENGTH: If instructions are not always long-word aligned, the maximum distance in bits between the end of an instruction and the next (higher) long-word boundary. - PADDING-STRING: A bit-string used for padding the instruction block to a long-word boundary. If possible, it should encode a HALT or ILLEGAL instruction. The length of this bit-string should evenly divide maximum-padding-length. - BLOCK-OFFSET-WIDTH: This should be the size in bits of format_word described in cmpint.txt. It should be 16 for all byte-addressed machines where registers hold 32 bits. - MAXIMUM-BLOCK-OFFSET: The maximum byte offset that can be encoded in block-offset-width bits. This depends on the encoding described in cmpint.txt. The least significant bit is always used to indicate whether this block offset points to the start of the object or to another block offset, so the range may be smaller than the obvious value. Furthermore, if instruction alignment constraints are tighter than byte boundaries, this range may be larger. For example, if instructions always start on even long-word boundaries, the bottom two bits (always zero) are encoded implicitly, and the range is accordingly larger. - BLOCK-OFFSET->BIT-STRING: This procedure is given a byte offset and a boolean flag indicating whether this is the offset to the start of a compiled code block or to another block-offset, and returns the encoded value of this offset. - MAKE-NMV-HEADER: This procedure is given the size in long-words of a block of instructions, and constructs the non-marked-vector header that must precede the instructions in memory in order to prevent the garbage collector from examining the data as Scheme objects. This header is just an ``object'' whose type tag is manifest-nm-vector (TC_MANIFEST_NM_VECTOR in the microcode) and whose datum is the size in long-words (excluding the header itself). The following three parameters define how instruction fields are to be assembled in memory depending on the ``endianness'' (byte ordering) of the architecture. You should be able to use the MC68020 (big endian) or the Vax (little endian) version, or the MIPS version which is conditionalized for both possibilities since MIPS processors can be configured either way. - INSTRUCTION-INSERT! is a procedure, that given a bit-string encoding instruction fields, a larger bit-string into which the smaller should be inserted, a position within the larger one, and a continuation, inserts the smaller bit-string into the larger at the specified position, and returns the new bit position at which the immediately following instruction field should be inserted. - INSTRUCTION-INITIAL-POSITION is a procedure, that given a bit-string representing a segment of compiled code, returns the bit-string position at which instruction-insert! should insert the first instruction. - INSTRUCTION-APPEND is a procedure, that given the bit-string encoding successive (fields of) instructions, produces the bit-string that corresponds to their concatenation in the correct order. * coerce.scm: This file defines a set of coercion procedures. These procedures are used to fill fields in instructions. Each coercion procedure checks the range of its argument and produces a bit string of the appropriate length encoding the argument. Most coercions will coerce their signed or unsigned argument into a bit string of the required fixed length. On some machines (e.g. HP PA), some coercions may permute the bits appropriately. * insmac.scm: This file defines machine-specific syntax used in the assembler, and the procedure PARSE-INSTRUCTION, invoked by the syntax expander for DEFINE-INSTRUCTION to parse the body of each of the instruction rules. This code is typically complex and you are encouraged to emulate one of the existing ports in order to reuse its code. The following ports use the following syntax for describing instructions in machine language: - Spectrum and MIPS: (LONG ( ) ( ) ... ( )) where all the widths must add up to an even multiple of 32. - Vax: Instruction descriptions are made of arbitrary sequences of the following field descriptors: (BYTE ( ) ( ) ... ( )) (OPERAND ) (DISPLACEMENT ( )) The total width of each of these field descriptors must add up to a multiple of 8. BYTE is used primarily for instruction opcodes. OPERAND is used for general addressing modes. DISPLACEMENT is used for PC-relative branch displacements. - MC68020: (WORD ( ) ( ) ... ( )) where all the widths must add up to an even multiple of 16. Size refers to immediate operands to be encoded in the instruction, and are omitted when irrelevant. A missing coercion type means that the ordinary unsigned coercion (for the corresponding number of bits) should be used. Additionally, each of these ports provides a syntax for specifying instructions whose final format must be determined by the branch-tensioning algorithm in the bit assembler. The syntax of these instructions is usually (VARIABLE-WIDTH ( ) (( ) ) (( ) ) ... ((() ()) )) Each instruction specifier is an ordinary (i.e. not VARIABLE-WIDTH) instruction specifier. NAME is a variable to be bound to the bit-assembly-time value of EXPRESSION. Each of the ranges - -, etc. must be properly nested in the next, and () specifies no bound. The final format chosen is that corresponding to the lowest numbered range containing the value of . Successive instruction specifiers must yield instructions of non-decreasing lengths for the branch tensioner to work correctly. The MC68020 port uses GROWING-WORD instead of VARIABLE-WIDTH as the keyword for this syntax. ==> This should probably be changed. * inerly.scm: This file provides alternative expanders for the machine-specific syntax. These alternative expanders are used when the assembly language that appears in the LAPGEN rules is assembled (early) at compiler pre-processing time. That is, the procedures defined in this file are only used if COMPILER:ENABLE-EXPANSION-DECLARATIONS? is set to true. If you reuse the code in insmac.scm from another port, you should be able to reuse the inerly.scm file from the same port. Alternatively, you can write a dummy version of this code and require COMPILER:ENABLE-EXPANSION-DECLARATIONS? to be always false. This switch defaults to false, currently. The Spectrum and MIPS versions currently have dummy versions of this code. * insutl.scm: This file defines machine-specific rule qualifiers and transformers. It is often used to define addressing-mode filters and handling procedures for architectures with general addressing modes. This file does not exist in the Spectrum port because all the relevant code has been placed in instr1.scm, and the MIPS port has no machine-specific qualifiers and transformers. Qualifiers and transformers are described further in the chapter on the syntax of translation rules. * instr.scm: These files define the instruction set of the architecture by using the syntax defined in insmac.scm and inerly.scm. There can be as many of these files or as few as desired by whoever writes the assembler. They are usually split according to the size of the files or along the divisions in the architecture manual. Not all instructions in the architecture need to be listed here -- only those actually used by the back end in the LAPGEN rules and utility procedures. Privileged/supervisory instructions, BCD (binary coded decimal) instructions, COBOL-style EDIT instructions, etc., can probably be safely ignored. 4.5. Disassembler files: The disassembler is almost completely machine dependent. For many machines, a reasonable disassembler could be derived from the description of the instruction set used to assemble programs. The Vax disassembler, is essentially constructed this way. Unfortunately this has not been generalized, and currently each port has its own disassembler, often duplicating information contained in the assembler. The disassembler is not necessary for the operation of the compiler proper. It is, however, a good debugging tool. You can bring the compiler up without a disassembler by providing stubs for the procedures referenced in dassm2. * dassm1.scm: This file contains the top-level of the disassembler. It is not machine-dependent, and should probably be moved to another directory. ==> Is compiler/back the right place for this? * dassm2.scm: This file contains various utilities for the disassembler. In particular, it contains the definitions of - COMPILED-CODE-BLOCK/BYTES-PER-OBJECT - COMPILED-CODE-BLOCK/OBJECTS-PER-PROCEDURE-CACHE - COMPILED-CODE-BLOCK/OBJECTS-PER-VARIABLE-CACHE These parameters specify various relative sizes. ==> Shouldn't these be in machin.scm? The first two have counterparts there, and the last is always 1. - DISASSEMBLER/READ-VARIABLE-CACHE - DISASSEMBLER/READ-PROCEDURE-CACHE These procedures are used to extract free variable information from a linked compiled code block. Variable caches are maintained as native addresses (i.e. no tag bits), and procedure (execute) caches contain absolute jump instructions that must be decoded to extract the address of the called procedure. Appropriate type bits must be added to both values before they are returned. This file also contains a state machine that allows the disassembler to display data appearing in the instruction stream in an appropriate format (gc and format words, mainly), and heuristics for displaying addressing modes and PC-relative offsets in a more legible form. The output of the disassembler need not be identical to the input of the assembler. The disassembler is used almost exclusively for debugging, and additional syntactic hints make it easier to read. * dassm3.scm: This file contains the code to disassemble one instruction at a time. It is completely machine dependent at the time, and any old way of doing it is fine. * dinstr.scm: In the VAX port, these are copies (or links) to the instr.scm files. They are processed with a different syntax table to construct the disassembler tables instead of the assembler tables. * dsyn.scm: In the VAX port, this file provides the alternative expansion of DEFINE-INSTRUCTION used to construct the disassembler tables instead of the assembler rule data base. 5. All about rules There are three subsystems in Liar that use rule-based languages. They are the RTL simplifier, LAPGEN (RTL->LAP translation), and the assembler. The assembler need not be rule-based, but given the availability of the rule language, using the rule mechanism may be the easiest way to write it. 5.1. Rule syntax The assembler rules use a somewhat different syntax from the rest and will be described later. The rest of the rules are defined in the following way: (DEFINE-RULE ; optional ) * is an expression evaluating to a rule database. It should be one of STATEMENT, PREDICATE, or REWRITING. * is a list that represents the pattern to match. Variables in the pattern are written by using the ``?'' syntax. For example, - (hello) matches the constant list (hello) - (? thing) matches anything, and THING is bound in and to whatever was matched. - (hello (? person)) matches a list of two elements whose first element is the symbol HELLO, and whose second element can be anything. The variable PERSON will be bound in and and will have as its value the second element of the list matched. Thus it would match (hello bill) and PERSON would be the symbol BILL, (hello (bill rozas)) would match and PERSON would be the list (BILL ROZAS). - (hello . (? person)) matches a list of one or more elements whose first element is the symbol HELLO. PERSON is bound to the rest of the list. Thus (hello my dog likes frankfurters) would match and PERSON would be (MY DOG LIKES FRANKFURTERS). (hello (my dog)) would match, and PERSON would be ((MY DOG)). Variable syntax is further described below. * is (QUALIFIER ) where evaluates to a boolean and further filters matches. If the qualifier expression evaluates to false, the rule is not fired. Otherwise it is. For example, (DEFINE-RULE (multiple (? number) (? divisor)) (QUALIFIER (and (number? number) (number? divisor) (zero? (remainder number divisor)))) ) will match (MULTIPLE 14 7) and (MULTIPLE 36 4), but will not match (MULTIPLE FOO 3), (MULTIPLE 37 4), (MULTIPLE 2), (MULTIPLE 14 2 3), nor (HELLO 14 7). Rule qualifiers are optional. * is an arbitrary Lisp expression whose value is the translation determined by the rule. It will typically use the variables bound by ``?'' to perform the translation. The statement and predicate rules use the LAP macro to generate sequences of assembly language instructions. The assembler rules use the following syntax: (DEFINE-INSTRUCTION ( ) ( ) ... ) Where is the name of the instruction, and the patterns will be matched against the cdr of lists whose car is . The , , and are as in the RTL rules, except that there are typically no qualifiers, and the bodies are typically written in a special syntax defined in compiler/machines/port/insmac.scm and described in section 4.4. For example, (DEFINE-INSTRUCTION ADD (((R (? target)) (R (? reg1)) (R (? reg2))) (WORD (6 #x24) (5 target) (5 reg1) (5 reg2) (11 0))) (((R (? target)) (R (? reg)) (& (? constant))) (WORD (6 #x23) (5 target) (5 reg) (16 constant SIGNED)))) would match (ADD (R 1) (R 2) (R 3)) and (ADD (R 7) (R 22) (& 257)), firing the corresponding body. The bodies are defined in terms of the WORD syntax defined in insmac.scm, and the ``commas'' used with the pattern variables in the rule bodies are a consequence of the WORD syntax. The meaning of the commas is identical to the meaning of the commas in a ``backquote'' Scheme expression, and is briefly described in section 5.3.1. 5.2. Rule variable syntax. Although qualifiers and the simple variable syntax shown are sufficient, some additional variable syntax is available for common patterns. Moreover, the early matcher (used when COMPILER:ENABLE-EXPANSION-DECLARATIONS? is true) cannot currently handle qualifiers but can handle the additional variable syntax that can supplant most qualifiers. The early matcher is used only on the assembler rules, so if you want to use it, you only need to use the restricted language when writing those rules. The complete variable syntax is as follows: * (? ) This syntax matches anything in that position of the potential instance, and binds to the sub-structure matched. * (? ) This syntax matches anything in that position of the potential instance as long as returns non-false on the sub-structure matched. is bound to the result returned by . For example, (? q (lambda (obj) (and (number? obj) (* 2 obj)))) will match 2, and Q will be bound to 4, but will not match FOO. * (? ) and have the same meaning as in the previous syntax, and this syntax matches exactly the same objects, but provides the additional convenience of binding to the sub-structure matched, before the transformation. For example, (? q (lambda (obj) (and (pair? obj) (number? (car obj)) (- (car obj) 23))) z) will match (2 . HELLO), Q will be bound to -21, and Z will be bound to (2 . HELLO), and will not match 34 or (HELLO . 2). ==> The pattern parser seems to understand (?@ ) as well, but this syntax is used nowhere. The early parser does not understand it. Should it be flushed? 5.3. Writing statement rules. Statement rules provide the translation between RTL instructions and fragments of assembly language. Most RTL instructions are assignments, where an RTL register is written with the contents of a virtual location or the result of some operation. 5.3.1. Output of the statement rules The output of the statement rules is a fragment of assembly language written in the syntax expected by the LAP assembler. The fragments, containing any number of machine instructions, are constructed by using the LAP macro, built on top of Scheme's QUASIQUOTE (back-quote). Within a LAP form, you can use UNQUOTE (comma) and UNQUOTE-SPLICING (comma at-sign) to tag subexpressions that should be evaluated and appended. For example, (LAP (MOV L ,r1 ,r2) (ADD L ,r3 ,r2)) constructs a fragment with two instructions in it where the values of r1, r2, and r3 are substituted in the instructions. The code (LAP (MOV L ,r1 ,r2) ,@(generate-test r2)) constructs a fragment whose first instruction is a MOV instruction, and the rest is the fragment returned by generate-test. The INST macro is similar to LAP but constructs a single instruction. It should not be used unless necessary (i.e. in LAP:MAKE-LABEL-STATEMENT), since you may find yourself later wanting to change a single instruction into a fragment in a utility procedure, and having to find every use of the procedure. ==> We should change the linearizer to expect LAP:MAKE-LABEL-STATEMENT to return a fragment, and do away with INST. An additional macro, INST-EA, is provided to construct a piece of assembly language representing an addressing mode. For example, INST-EA is used by the following procedure in the Vax back-end: (define (non-pointer->ea type datum) (if (and (zero? type) (<= 0 datum 63)) (INST-EA (S ,datum)) (INST-EA (&U ,(make-non-pointer-literal type datum))))) where non-pointer->ea may be used in (LAP (MOV L ,(non-pointer->ea ) ,(any-register-reference target))) INST-EA is superfluous on machines without general addressing modes (i.e. load-store architectures). Each port provides a procedure, named REGISTER-REFERENCE, that maps between RTL machine registers and the assembly language syntax used to refer to the registers. It uses INST-EA to build such references. The macros LAP, INST, and INST-EA, besides providing the functionality of QUASIQUOTE, also provide a hook for the compiler pre-processing time assembly of the code generated by the rules. 5.3.2. Hardware register allocation Hardware register allocation occurs during the RTL->LAP translation. The rules, besides generating assembly language, invoke utilities provided by the register allocator to reserve and free hardware registers on which the operations can be performed. Hardware registers are often divided into different non-overlapping types that are used in different operations. For example, modern hardware typically has a set of integer registers and a set of floating point registers. Address operations typically require operands in integer registers, while floating point operations typically require floating point registers. On some machines, notably the Motorola 68K family, the integer register set is further subdivided into types with specific operations (address and data). The register allocator manipulates RTL registers. RTL registers are just small integers. The low end of the valid range of RTL registers is used to represent the physical registers of the processor (called machine registers), and the rest of the numbers represent virtual (pseudo) registers. The core allocator operations are given an RTL register number and a register type, and return a suitable machine register to be used for the operation. A machine register that holds the value of a pseudo register is called an ``alias'' for the pseudo register. A pseudo register may have many valid aliases simultaneously, usually of different types. Any assignment to the pseudo register will invalidate all aliases but one, namely the machine register actually written, rather than copy the new value into all the previous aliases. Thus source references and destination references have different effects, and are handled by different procedures in the register allocator. Pseudo registers have associated homes, memory locations that hold their values when the machine registers are needed for other purposes. Most pseudo registers are never written to their homes, since a pseudo register's value is usually kept in machine register aliases until the pseudo register is dead, i.e. until its value is no longer needed. A pseudo register's aliases can be reused for other purposes if there are other remaining aliases or this is the last reference to the pseudo register. An alias that can be reused is a ``reusable'' alias. Occasionally, the value of a pseudo register may be transferred to the register's home and the last alias invalidated, if the register allocator is running out of registers. This is called ``spilling'' a register. The register allocator maintains a table of associations, called the ``register map'', that associates each pseudo register with its valid aliases, and each machine register with the pseudo register whose value it holds (if any). The register allocator routines modify the register map after aliases are requested and invalidated, and they generate assembly language instructions to perform the necessary data motion for spilling and re-loading at run time. These instructions are usually inserted before the code output of the RTL rule in execution. If you have chosen your RTL register numbers for machine registers so that they match the hardware numbers, and your assembly language does not distinguish between references to a register and other fields, you can ignore register references and use the RTL register numbers directly. This is commonly the case when using integer registers in load-store architectures. As a convenience, the register allocator also provides operations that manipulate register references. A register reference is a fragment of assembly language, typically a register addressing mode for general register machines, that when inserted into a LAP instruction, denotes the appropriate register. For example, on the Motorola MC68020, physical register A3 is represented as RTL register number 11, and a register reference for it would be ``(A 3)''. RTL pseudo register 44 may at some point have RTL machine register 11 as its only address-register alias. At that time, (REGISTER-ALIAS 44 'ADDRESS) would return 11. The interface to the register allocator is defined in compiler/back/lapgn2.scm. Not all ports use all of the procedures defined there. Often a smaller subset is sufficient depending on whether there are general addressing modes, etc. A list of the most frequently used follows: * REGISTER-ALIAS expects an RTL register and a register type, and returns a machine register of the specified type that is a valid alias for that RTL register if there is one, or false if there is none. This procedure should only be used for source operand RTL registers. If the register type is false, then REGISTER-ALIAS will return any valid alias. * LOAD-ALIAS-REGISTER! is like REGISTER-ALIAS but always returns a machine register, allocating one of the specified type if necessary. This procedure should only be used for source operand RTL registers. * REFERENCE-ALIAS-REGISTER! performs the same action as LOAD-ALIAS-REGISTER! but returns a register reference instead of an RTL register number. * ALLOCATE-ALIAS-REGISTER! expects an RTL register and a register type, and returns a machine register of the specified type that is the only alias for the RTL register and should be written with the new contents of the RTL register. ALLOCATE-ALIAS-REGISTER! is used to generate aliases for target RTL registers. * REFERENCE-TARGET-ALIAS! performs the same action as ALLOCATE-ALIAS-REGISTER! but returns a register reference instead of an RTL register number. See CLEAR-REGISTERS! below. * STANDARD-REGISTER-REFERENCE expects an RTL register, a register type, and a boolean. It will return a reference for an alias of the specified register containing the current value of the RTL register. This reference will be of the specified type if the boolean is false, or sometimes of other types if the boolean is true. In other words, the boolean argument determines whether other types are acceptable, although not desirable. The register type may be false, specifying that there really is no preference for the type, and any reference is valid. STANDARD-REGISTER-REFERENCE should be used only for source operands (i.e. those that already contain data), and may return a memory reference for those machines with general addressing modes if there is no preferred type and alternates are acceptable. * MOVE-TO-ALIAS-REGISTER! expects a source RTL register, a register type, and a target RTL register. It returns a new alias for the target of the specified type containing a copy of the current contents of the source. Often this is accomplished by choosing an alias of the source that already contains the correct data and making it the only alias for target. MOVE-TO-ALIAS-REGISTER! attempts to reuse an alias for the source register. * MOVE-TO-TEMPORARY-REGISTER! expects a source RTL register and a register type and returns an appropriate register containing a copy of the source. The register is intended for temporary use, that is, use only within the code generated by the expansion of the current RTL instruction, and as such it should not be permanently recorded in the register map. The register becomes automatically freed for subsequent RTL instructions. MOVE-TO-TEMPORARY-REGISTER! attempts to reuse an alias for the source register. * REUSE-PSEUDO-REGISTER-ALIAS! expects an RTL register, a register type, and two procedures. It attempts to find a reusable alias for the RTL register of the specified type, and invokes the first procedure giving it the alias if it succeeds, or the second procedure with no arguments if it fails. MOVE-TO-ALIAS-REGISTER! and MOVE-TO-TEMPORARY-REGISTER! use REUSE-PSEUDO-REGISTER-ALIAS! but occasionally neither meets the requirements. * NEED-REGISTER! expects an RTL machine register and informs the register allocator that the rule in use requires that register so it should not be available for subsequent requests while translating the current RTL instruction. The register is available for later RTL instructions unless the relevant rules invoke NEED-REGISTER! again. The procedures described above that allocate and assign aliases and temporary registers call NEED-REGISTER! behind the scenes, but you will need to invoke it explicitly when calling out-of-line routines. * LOAD-MACHINE-REGISTER! expects an RTL register and an RTL machine register and generates code that copies the current value of the RTL register to the machine register. It is used to pass arguments in fixed registers to out-of-line code, typically in the compiled code runtime library. * ADD-PSEUDO-REGISTER-ALIAS! expects an RTL pseudo-register and an available machine register (no longer an alias), and makes the specified machine register an alias for the pseudo-register. * CLEAR-REGISTERS! expects any number of RTL registers and clears them from the register map, preserving their current contents in memory if needed. It returns the code that will perform the required motion at runtime. It should be used before invoking LOAD-MACHINE-REGISTER! to ensure that the potentially valid previous contents of the machine register have been saved. * CLEAR-MAP! deletes all aliases from the register map, pushing the data only held in aliases into the memory homes if needed. This procedure returns an assembly language code fragment, and is typically used before invoking out-of-line code. * DELETE-DEAD-REGISTERS! informs the register allocator that RTL pseudo registers whose contents will not be needed after the current RTL instruction can be eliminated from the register map and their aliases subsequently used for other purposes. Most of the rules are actually written in terms of machine-specific procedures that invoke the procedures listed above in fixed ways. Rule bodies typically match the following code pattern: (let* ((rs1 (standard-source source1)) (rs2 (standard-source source2)) (rt (standard-target target))) (LAP ...)) where STANDARD-SOURCE and STANDARD-TARGET are machine-specific procedures. The reason for the use of LET* (instead of LET) is given below. On a machine with general addressing modes and memory operands, we might provide their definitions as follows: (define (standard-source rtl-reg) (standard-register-reference rtl-reg 'GENERAL true)) (define (standard-target rtl-reg) (delete-dead-registers!) (reference-target-alias! rtl-reg 'GENERAL)) while on a load-store architecture we might define them as follows: (define (standard-source rtl-reg) (load-alias-register! rtl-reg 'GENERAL)) (define (standard-target rtl-reg) (delete-dead-registers!) (allocate-alias-register! rtl-reg 'GENERAL)) - VERY IMPORTANT: - This example brings up the cardinal rule of RTL assignments: Any rule that writes into an RTL pseudo-register MUST invoke DELETE-DEAD-REGISTERS! after allocating aliases for the necessary sources but before allocating an alias for the target. If this is not done, the register allocator may decide to spill no-longer valid data into memory, which will probably make the compiler get confused in other ways or cause garbage collection problems later. If it is done too early, the last valid alias for a source operand may have been reused in the interim, and the compiler will assume that the source quantity is contained in memory and will often generate code that fetches and operates on garbage. The example above uses LET* instead of LET. LET would not work in the above example because Scheme does not specify the order of argument evaluation, and Liar chooses arbitrary orders, so the DELETE-DEAD-REGISTERS! implicit in STANDARD-TARGET might be called too early possibly causing STANDARD-SOURCE to fail. MOVE-TO-ALIAS-REGISTER! invokes DELETE-DEAD-REGISTERS! because it simultaneously allocates an alias for a source and for a target. Thus, if there are other source operands, their aliases must be allocated before MOVE-TO-ALIAS-REGISTER! is invoked. 5.3.3. Invocation rules, etc. The meaning and intent of most statement rules in an existing port is readily apparent. The more arcane rules have to do with procedures and the representation of numbers. What follows is a description of some of the more obscure rules related to procedures and some of the implementation concepts required to understand them. In the invocation rules, FRAME-SIZE is the number of arguments passed in the call (often plus one), and there is often more than one rule with the same keyword, typically to handle the common cases (small FRAME-SIZE) more efficiently. Various of the rules specify the number of arguments that the resulting procedure will accept. The range is described in terms of two parameters, MIN and MAX: - MIN is always positive and it is one greater than the smallest number of arguments allowed. - MAX may be positive or negative. If positive, it is one greater than the largest number of arguments allowed. If negative, it indicates that the procedure will accept an unbounded number of arguments, and the absolute value of MAX, minus (MIN + 1), is the number of positional optional parameters. Either way, the absolute value of MAX is the size of the procedure's call frame counting the procedure itself. These two values are encoded in the format word of the resulting procedures so that dynamic APPLY can check the number of arguments passed and reformat the stack frame appropriately. Non-positive MINs are used to indicate that the compiled entry point is not a procedure, but a return address, a compiled expression, or a pointer to an internal label. The CONS-CLOSURE rules will dynamically create some instructions in the runtime heap, and these instructions must be visible to the processor's instruction fetch unit. If the instruction and data caches are not automatically kept consistent by the hardware, especially for newly addressed memory, the caches must be explicitly synchronized by the Scheme system. On machines where the programmer is given no control over the caches, this will be very hard to do. On machines where the control is minimal or flushing is expensive, the following solution can be used to amortize the cost: The CONS-CLOSURE rules can generate code to allocate a closure from a pre-allocated pool and invoke an out-of-line routine to refill the pool when it is empty. The routine allocates more space from the heap, initializes the instructions, and synchronizes the caches. Since the real entry points are not known until the closure objects are created, instead of using absolute jumps to the real entry points, the pre-allocated closures can contain jumps to a fixed routine that will extract the real entry point from the word pointed at by the return address and invoke it. In other words, the code inserted in closure objects will be jsr fixed-routine and fixed-routine, written in assembly language, will do something like load 0(return-address),rtemp jmp 0(rtemp) The 68040 version of the Motorola 68000 family port uses this trick because the 68040 cache is typically configured in copyback mode, and synchronizing the caches involves an expensive supervisor (OS) call. The Alpha back-end also uses this trick because the caches can be synchronized only by using the CALL_PAL IMB instruction, which flushes the complete instruction cache, therefore implying a large re-start cost. The Alpha version of this code is currently better than the 68040 version, so you should probably emulate that version. * (INVOCATION:UUO-LINK (? frame-size) (? continuation) (? name)) This rule is used to invoke a procedure named by a free variable. It is the rule used to generate a branch to an execute cache as described in cmpint.txt. The rule should allocate a new execute cache in the compiled code block by using FREE-UUO-LINK-LABEL, and should then branch to the instruction portion of the execute cache. FRAME-SIZE is the number of arguments passed in the call, plus one. * (INVOCATION:GLOBAL-LINK (? frame-size) (? continuation) (? name)) This rule is identical to the previous one, except that the free variable must be looked up in the global environment. It is used to improve the expansion of some macros that insert explicit references to the global environment (e.g. The expansion for FLUID-LET inserts uses (ACCESS DYNAMIC-WIND #f) as the operator of a call). * (INVOCATION-PREFIX:MOVE-FRAME-UP (? frame-size) (? address)) This rule is used to shift call frames on the stack to maintain proper tail recursion. ADDRESS specifies where to start pushing the frame. It should be a pointer into the used portion of the stack, i.e. point to a higher address. For example, assume that what follows depicts the stack before (INVOCATION-PREFIX:MOVE-FRAME-UP 3 addr) | ... | | | +-------------------------------+ | | addr -> +-------------------------------+ | | direction of | | stack growth | | | ... | | | | | | | V | | +-------------------------------+ | | +-------------------------------+ | | +-------------------------------+ | | spbf -> +-------------------------------+ Where spbf is the contents of the stack pointer register. After the invocation prefix, it will look as follows: | ... | | | +-------------------------------+ | | direction of addr -> +-------------------------------+ stack growth | | +-------------------------------+ | | | | +-------------------------------+ V | | spaf -> +-------------------------------+ The stack pointer register will now contain the value of spaf. * (INVOCATION-PREFIX:DYNAMIC-LINK (? frame-size) (? address-1) (? address-2)) This rule is similar to the INVOCATION-PREFIX:MOVE-FRAME-UP rule, but is used when the destination of the frame is not known at compile time. The destination depends on the continuation in effect at the time of the call, and the section of the stack that contains enclosing environment frames for the called procedure. Two addresses are specified and the one that is closest to the current stack pointer should be used, that is, the target address is the numerically smaller of the two addresses since the Liar stack grows towards smaller addresses. ==> This rule need not need not exist in the RTL. It could be expanded into a comparison and a use of INVOCATION-PREFIX:MOVE-FRAME-UP with a computed address. * (ASSIGN (REGISTER (? target)) (CONS-CLOSURE (ENTRY:PROCEDURE (? procedure-label)) (? min) (? max) (? size))) This rule issues the code to create a closure object whose real entry point is PROCEDURE-LABEL, that will accept a number of arguments specified by MIN and MAX, and that will have storage for SIZE free variables. The free variable storage need not be initialized since it will be written immediately by subsequent RTL instructions. The entry point of the resulting closure object should be written to RTL register TARGET. The format of closure objects is described in cmpint.txt. * (ASSIGN (REGISTER (? target)) (CONS-MULTICLOSURE (? nentries) (? size) (? entries))) This rule is similar to the previous rule, but issues code to allocate a closure object with NENTRIES entry points. ENTRIES is a vector of entry-point descriptors, each being a list containing a label, a min, and a max as in the previous rule. TARGET receives the compiled code object corresponding to the first entry. * (OPEN-PROCEDURE-HEADER (? label-name)) This rule and its siblings are used to generate the entry code to procedures and return addresses. On entry to procedures and continuations, a gc/interrupt check is performed, and the appropriate routine in the runtime library is invoked if necessary. This check is performed by comparing the memory Free pointer to the compiled code's version of the MemTop pointer. The low-level interrupt handlers change the MemTop pointer to guarantee that such comparisons will fail in the future. A standard header generates the following code: (LABEL gc-label) (LABEL label-name) = MemTop> Each kind of header invokes a different runtime library utility. In addition, procedures that expect dynamic links must guarantee that the dynamic link is preserved around the execution of the interrupt handler. This is accomplished by passing the contents of the dynamic link register to the appropriate runtime library utility. * (CLOSURE-HEADER (? label-name) (? nentries) (? num-entry)) NENTRIES is the number of entry points in the closure object, and NUM-ENTRY is the zero-based index for this entry point. Closure headers are similar to other procedure headers but also have to complete the Hand-Shake initiated by the instructions stored in the closure objects so that the closure object appears on top of the stack. On architectures where it is necessary, they also have to map closure objects to their canonical representatives, and back when backing out because of interrupts or garbage collection. The file compiler/machines/port/rules3.scm contains most of these procedure-related rules. It also contains three procedures that generate assembly language and are required by the compiler. These procedures are used to generate initialization code for compiled code blocks. Compiled code blocks have two sections, a code section that contains the instructions, and a ``constants'' section that contains scheme objects referenced by the code (e.g. quoted lists and symbols), the free variable caches for the code, the debugging information descriptor (more on this later), and the environment where the free variables in the code must be referenced. This environment is not known at compile time, so the compiler allocates a slot in the constants section for it, but the code itself must store it on first entry. In addition, the linker is invoked on first entry to look up the free variables and fill the variable caches with their correct contents. The compiler allocates enough space for each free variable cache and initializes the space with the information required by the linker to patch the reference. This information consists of the name of the free variable in addition to the number of actual arguments passed (plus one) for execute references. If COMPILER:COMPILE-BY-PROCEDURES? is true, the compiler will generate multiple compiled code blocks, one corresponding to each top-level lambda expression. Each of these must be initialized and linked, but instead of initializing them on first entry, the root compiled code block links all of them when it is entered. The linker (a runtime library utility) expects three arguments: The address of the first word of the compiled code block, the word containing the GC vector header for the compiled code block. The address of the first linker section in the constants area of the compiled code block. The linker sections contain the free variable caches and are all contiguous. The number of linker sections in the compiled code block. * (GENERATE/QUOTATION-HEADER env-label free-label n-sections) This procedure generates the code that initializes the environment slot at location labeled ENV-LABEL. The environment is fetched from the interpreter's environment register. It also generates code to invoke the linker on the executing compiled code block. The first word of the compiled code block is labeled by the value of *BLOCK-LABEL*, the first linker section is labeled by FREE-LABEL, and the number of linker sections is N-SECTIONS. * (GENERATE/REMOTE-LINK label env-offset free-offset n-sections) This procedure is similar to GENERATE/QUOTATION-HEADER but is used to generate code that initializes and links a different compiled code block. It is used to generate the code to insert into the root compiled code block to link each of the other compiled code blocks generated when COMPILER:COMPILE-BY-PROCEDURES? is true. LABEL is a label in current block's constant section where the pointer to the code block to be linked is stored, ENV-OFFSET is the vector offset in the other code block where the environment of evaluation should be stored, FREE-OFFSET is the vector offset of the first linker section in the other compiled code block, and N-SECTIONS is the number of linker sections in the other block. * (GENERATE/CONSTANTS-BLOCK consts reads writes execs global-execs statics) This procedure generates the assembler pseudo-ops used to generate the constants and linker section for a compiled code block. This section consists of: - The constant objects (e.g. quoted lists) referenced by the code. - The read variable caches used by the code. - The write variable caches used by the code. - The execute variable caches used by the code. - The global execute variable caches used by the code. - The locations for static variables. - A slot for the debugging information descriptor generated by the compiler. - A slot for the environment where the code is linked. Each word of storage in the constants block is allocated by using a SCHEME-OBJECT assembler pseudo-op, and the order in which they appear is the same as the order in which they appear in the final object. The linker sections (free variable cache sections) must be contiguous, and each has a one-word header describing the kind of section and its length. The environment slot must be the last word in the compiled code block, immediately preceded by the debugging information descriptor. Each SCHEME-OBJECT directive takes a label and the initial contents of the slot. This procedure is almost machine-independent, and you should be able to trivially modify an existing version. The only machine dependence is the layout and size of the storage allocated for each execute cache (uuo link). This machine-dependence consists entirely of the definition of the TRANSMOGRIFLY procedure. TRANSMOGRIFLY takes a list of the following form: ((free-variable-1 (frame-size-1-1 . label-1-1) (frame-size-1-2 . label-1-2) ...) (free-variable-2 (frame-size-2-1 . label-2-1) (frame-size-2-2 . label-2-2) ...) ...) This list is interpreted as follows: an execute cache for calls to FREE-VARIABLE-1 with frame size FRAME-SIZE-1-1 (number of arguments plus one) must be created, and labeled LABEL-1-1, similarly for , , etc. Assuming that the initial layout of an execute cache is free variable name ; labeled word false ; optional storage (e.g. for branch delay slot) frame size of call ; arity + 1 TRANSMOGRIFLY will return a list of the following form: ((frame-variable-1 label-1-1) (#f dummy-label-1-1) ; optional word(s) (frame-size-1-1 dummy-label-1-1) (frame-variable-1 label-1-2) (#f dummy-label-1-2) ; optional word(s) (frame-size-1-2 dummy-label-1-2) ... (frame-variable-2 label-2-1) (#f dummy-label-2-1) ; optional word(s) (frame-size-2-1 dummy-label-2-1) ...) There may be any number of optional words, but the layout must match that expected by the macros defined in microcode/cmpint-port.h. In particular, the length in longwords must match the definition of EXECUTE_CACHE_ENTRY_SIZE in microcode/cmpint-port.h, and the definition of EXECUTE-CACHE-SIZE in compiler/machines/port/machin.scm. Furthermore, the instructions that the linker will insert should appear at the word labeled by LABEL-N-M, and should not overwrite the relevant part of FRAME-SIZE-N-M, since the frame size will be needed when re-linking after an incremental definition or assignment. The output format of TRANSMOGRIFLY is the input format for the read and write execute cache sections. The procedure DECLARE-CONSTANTS, local to GENERATE/CONSTANTS-BLOCK, reformats such lists into the final SCHEME-OBJECT directives and tacks on the appropriate linkage section headers. 5.3.4. Fixnum rules. Scheme's generic arithmetic primitives cannot be open-coded fully for space reasons. Most Scheme code that manipulates numbers manipulates small integers used as counters, vector indices, etc., and using out-of-line arithmetic procedures to operate on them would make the code too slow. The compromise, therefore, is to open-code the common small integer case, and to handle the rest out of line. This, of course, does not perform particularly well for the other common case of floating point data. Scheme integers are represented in two formats. The most common, fixnum representation, uses the datum field of the objects to directly encode the values. The other format, bignum representation, stores the values in multiple words in memory, and the datum is a pointer to this storage. Scheme generic arithmetic procedures will generate fixnums whenever possible, resorting to bignums when the value exceeds the range that can be represented in fixnum format. Since the open-codings provided for the compiler only handle fixnums, these open-codings must also detect when the result will not fit in a fixnum in order to invoke the out-of-line utility that will handle them correctly. Most hardware provides facilities for detecting and branching if an integer operation overflows. Fixnums cannot use these facilities directly, because of the tag bits at the high-end of the word. To be able to use these facilities (and get the sign bit in the right place), Scheme fixnums are converted to an internal format before they are operated on, and converted back to Scheme object format before storing them in memory or returning them as values. In this internal format, the value has been shifted left so that the fixnum sign-bit coincides with the integer sign bit, and a number of bits in the least-significant end of the word hold zeros. The shift amount is the length of the type-tag field. The rules (ASSIGN (REGISTER (? target)) (OBJECT->FIXNUM (REGISTER (? source)))) (ASSIGN (REGISTER (? target)) (FIXNUM->OBJECT (REGISTER (? source)))) perform this translation. The open-coding of fixnum arithmetic assumes that the sources and the result are in this format. This format is good for value comparisons, addition, subtraction, and bitwise logical operations, but must be transformed for multiplication, division, and shifting operations. In addition to open-coding fixnum operations within generic arithmetic, fixnum primitives can be invoked directly, and the code can be open coded as well. Under these circumstances, the result will not be checked for overflow, and the code generated can be quite different. The RTL instructions that perform fixnum arithmetic have a boolean flag that specifies whether overflow conditions should be generated or not. The compiler does not generally require fixnum arithmetic to be open coded. If the names of all the fixnum primitives are listed in COMPILER:PRIMITIVES-WITH-NO-OPEN-CODING, all of them will be handled by issuing code to invoke them out of line. There is one exception to this, however. The following rules MUST be provided: (ASSIGN (REGISTER (? target)) (FIXNUM-2-ARGS MULTIPLY-FIXNUM (OBJECT->FIXNUM (CONSTANT 4)) (OBJECT->FIXNUM (REGISTER (? source))) #F)) (ASSIGN (REGISTER (? target)) (FIXNUM-2-ARGS MULTIPLY-FIXNUM (OBJECT->FIXNUM (REGISTER (? source))) (OBJECT->FIXNUM (CONSTANT 4)) #F)) The reason is that VECTOR-REF and VECTOR-SET! translate into a sequence that uses these patterns when the index is not a compile-time constant. Of course, you can include VECTOR-REF and VECTOR-SET! in compiler:PRIMITIVES-WITH-NO-OPEN-CODING to avoid the problem altogether, but this is probably not advisable. 5.3.5. Rules used to invoke the runtime library Some of the rules issue code that invokes the runtime library. The runtime library is invoked through a primary entry point, SCHEME-TO-INTERFACE, typically directly accessible through a dedicated processor register. SCHEME-TO-INTERFACE expects at least one and up to five arguments. The first argument is the index of the runtime library service to invoke, and the rest are the parameters to the service routine. These arguments are passed in fixed locations, typically registers. Runtime library utilities return their values (if any) in the compiler's value register. The following is a typical example of such an invocation where INVOKE-INTERFACE expects the index of a utility, and generates the code that writes the index into the appropriate location and jumps to SCHEME-TO-INTERFACE. (define-rule statement (INVOCATION:APPLY (? frame-size) (? continuation)) (LAP ,@(clear-map!) ,@(load-rn frame-size 2) (MOV L (@R+ 14) (R 1)) ,@(invoke-interface code:compiler-apply))) The code names are typically defined in compiler/machines/port/lapgen.scm. Many of the utilities expect return addresses as their first argument, and it is convenient to define a procedure, INVOKE-INTERFACE-JSB (sometimes called LINK-TO-INTERFACE) which receives an index but leaves the appropriate return address in the first argument's location. INVOKE-INTERFACE-JSB can be written by using INVOKE-INTERFACE (and SCHEME-TO-INTERFACE), but given the frequency of this type of call, it is often written in terms of an alternate entry point to the runtime library (e.g. SCHEME-TO-INTERFACE-JSB). An example of a more complicated call to the runtime library is (define-rule statement (INTERPRETER-CALL:CACHE-ASSIGNMENT (? extension) (? value)) (QUALIFIER (and (interpreter-call-argument? extension) (interpreter-call-argument? value))) (let* ((set-extension (interpreter-call-argument->machine-register! extension r2)) (set-value (interpreter-call-argument->machine-register! value r3)) (clear-map (clear-map!))) (LAP ,@set-extension ,@set-value ,@clear-map ,@(invoke-interface-jsb code:compiler-assignment-trap)))) where INTERPRETER-CALL-ARGUMENT->MACHINE-REGISTER! invokes CLEAR-REGISTERS! and NEED-REGISTER! besides performing the assignment. For very frequent calls, the assembly language part of the runtime library can provide additional entry points. The calling convention for these would be machine-dependent, but frequently they take arguments in the same way that SCHEME-TO-INTERFACE and SCHEME-TO-INTERFACE-JSB take them, but avoid passing the utility index, and may do part or all of the work of the utility in assembly language instead of invoking the portable C version. Many of the ports have out-of-line handlers for generic arithmetic, with the commond fixnum/flonum cases handled there. The following is a possible specialized version of apply where the special entry point expects the procedure argument on the stack rather than in a fixed register: (define-rule statement (INVOCATION:APPLY (? frame-size) (? continuation)) (LAP ,@(clear-map!) ,@(load-rn frame-size 2) (JMP ,entry:compiler-apply))) The procedure object will have been pushed on the stack by earlier code. 5.4. Writing predicate rules. Predicate rules are used to generate code to discriminate between alternatives at runtime. The code generated depends on the conditional branch facilities of the hardware at hand. There are two main ways in which architectures provide conditional branching facilities: * condition codes. Arithmetic instructions compute condition codes that are stored in hardware registers. These hardware registers may be targeted explicitly by the programmer or implicitly by the hardware. Conditional branch instructions determine whether to branch or not depending on the contents of the condition registers at the time the branch instruction is executed. These condition registers may be named explicitly by the instructions, or assumed implicitly. * compare-and-branch instructions. The instruction set includes instructions that compare two values (or a value against 0) and branch depending on the comparison. The results of the comparison are not stored in special or explicit registers, since they are used immediately, by the instruction itself, to branch to the desired target. Liar accommodates both models for branching instructions. Predicate rules generate code that precede the actual branches, and then invoke the procedure SET-CURRENT-BRANCHES! informing it of the code to generate to branch to the target. Depending on the model, the prefix code may be empty, and all the code may appear in the arguments to SET-CURRENT-BRANCHES! SET-CURRENT-BRANCHES! expects two procedures as arguments. Each of them receives a label as an argument, and is supposed to generate code that branches to the label if the predicate condition is true (first argument) or false (second argument). Both options are provided because linearization of the control-flow graph occurs after LAP generation, and it is therefore not known when the predicate rule is fired which of the two possible linearizations will be chosen. Thus on an architecture with condition codes, the rule will return the code that performs the comparison, targeting the appropriate condition-code registers (if they are not implicit), and the arguments to SET-CURRENT-BRANCHES! will just generate the conditional-branch instructions that use the generated condition codes. On an architecture with compare-and-branch instructions, the code returned by the rule body will perform any work needed before the compare-and-branch instructions, and the arguments to SET-CURRENT-BRANCHES! will generate the compare-and-branch instructions. For example, on the DEC Vax, a machine with implicit condition codes, where compare (and most) instructions set the hidden condition-code register, a predicate rule could be as follows: (define-rule predicate (EQ-TEST (REGISTER (? register-1)) (REGISTER (? register-2))) (set-current-branches! (lambda (label) (LAP (B EQL (@PCR ,label)))) (lambda (label) (LAP (B NEQ (@PCR ,label))))) (LAP (CMP L ,(any-register-reference register-1) ,(any-register-reference register-2)))) The prefix code performs the comparison. The arguments to SET-CURRENT-BRANCHES! branch depending on the result. On the HP Precision Architecture (Spectrum), a machine with compare-and-branch instructions, the same rule would be written as follows: (define-rule predicate ;; test for two registers EQ? (EQ-TEST (REGISTER (? source1)) (REGISTER (? source2))) (let* ((r1 (standard-source! source1)) (r2 (standard-source! source2))) (set-current-branches! (lambda (label) (LAP (COMB (EQ) ,r1 ,r2 (@PCR ,label)) (NOP ()))) ; handle delay slot (lambda (label) (LAP (COMB (LTGT) ,r1 ,r2 (@PCR ,label)) (NOP ())))) ; handle delay slot (LAP))) There is no prefix code, and the arguments to SET-CURRENT-BRANCHES! perform the comparison and branch. The (OVERFLOW-TEST) predicate condition does not fit this model neatly. The current compiler issues overflow tests when open-coding generic arithmetic. Fixnum overflow implies that bignums should be used for the result, and this predicate is used to conditionally invoke out-of-line utilities. The problem is that the decomposition of the code assumes that the result of the overflow test is stored implicitly by the code that generates the arithmetic instructions, and that this condition can be later used for branching by the code generated for (OVERFLOW-TEST). The code for the test will be adjacent to the code for the corresponding arithmetic operation, but the compiler assumes that the condition can be passed implicitly between these adjacent instructions. This decomposition only matches hardware with condition codes. Hardware with compare-and-branch instructions can be accommodated by explicitly computing conditions into a hardware register reserved for this purpose, and the code generated by the predicate rule can then branch according to the contents of this register. On these machines, the arithmetic operator will not only generate the desired result, but will set or clear a fixed register according to whether the computation overflowed or not. The predicate code will then branch when the fixed register contains a non-zero value for the first linearization choice, or zero for the other possibility. This problem is particularly acute on MIPS processors. The MIPS architecture does not detect overflow conditions, so the overflow condition must be computed by examining the inputs and outputs of the arithmetic instructions. There are conditional branches used just to store the correct overflow condition in a register, and the code generated for the overflow test will then branch again depending on the value stored. This makes the code generated by the open-coding of generic arithmetic contain multiple branches and quite large. The Spectrum port solves this problem a little differently. On the Spectrum, arithmetic instructions can conditionally cause the following instruction to be skipped. Since the code generated by (OVERFLOW-TEST) is guaranteed to follow the code generated by the arithmetic operation, the last instruction generated by the arithmetic operations conditionally skips if there is no overflow. The (OVERFLOW-TEST) code generates an unconditional branch for the first linearization choice, and an unconditional skip and an unconditional branch for the alternative linearization. A more efficient solution, currently employed in the MIPS port (version 4.87 or later) depends on the fact that the RTL instruction immediately preceding an RTL OVERFLOW-TEST encodes the arithmetic operation whose overflow condition is being tested. Given this assumption (that the arithmetic operation producing the overflow conditions and the test of such condition are adjacent), the rule for OVERFLOW-TEST need not generate any code, and the rule for the arithmetic operation can generate both the prefix code and invoke SET-CURRENT-BRANCHES! as appropriate. This is possible because the RTL encoding of arithmetic operations includes a boolean flag that specifies whether the overflow condition is desired or not. 6. Suggested ordering of tasks. The task of porting the compiler requires a lot of work. In the past, it has taken approximately three full weeks for a single person knowledgeable with MIT Scheme and the compiler, but without documentation. This guide was written after the first three ports. One unfortunate aspect is that a lot of mechanism must be in place before most of the compiler can be tried out. In other words, there is a lot of code that needs to be written before small pieces can be tested, and the compiler is not properly organized so that parts of it can be run independently. Note also that cmpint-port.h, machin.scm, rules3.scm, and cmpaux-port.m4 are very intertwined, and you may often have to iterate while writing them until you converge on a final design. Keeping all this in mind, here is a suggested ordering of the tasks: 6.1. Learn the target instruction set well. In particular, pay close attention to the branch and jump instructions and to the facilities available for controlling the processor caches (if necessary). You may need to find out the facilities that the operating system provides if the instructions to control the cache are privileged instructions. 6.2. Write microcode/cmpint-port.h: cmpint.txt documents most of the definitions that this file must provide. 6.2.1. Design the trampoline code format. Trampolines are used to invoke C utilities indirectly. In other words, Scheme code treats trampolines like compiled Scheme entry points, but they immediately invoke a utility to accomplish their task. Since return-to-interpreter is implemented as a trampoline, you will need to get this working before you can run any compiled code at all. 6.2.1. Design the closure format and the execute cache format. This is needed to get the Scheme part of the compiler up AND to get the compiled code interface in the microcode working. Try to keep the number of instructions low since closures and execute caches are very common. 6.2.2. Design the interrupt check instructions that are executed on entry to every procedure, continuation, and closure. Again, try to keep the number of instructions low, and attempt to make the non-interrupting case fast at the expense of the case when interrupts must be processed. Note that when writing the Scheme code to generate the interrupt sequences, you can use the ADD-END-OF-BLOCK-CODE! procedure to make sure that the interrupt sequence does not confuse your hardware's branch prediction strategy. 6.2.3. Given all this, write cmpint-port.h. Be especially careful with the code used to extract and insert absolute addresses into closures and execute caches. A bug in this code would typically manifest itself much later, after a couple of garbage collections. During this process you will be making decisions about what registers will be fixed by the port, namely the stack pointer, the free pointer, the register block pointer, and at least one register holding the address of a label used to get back to C, typically scheme_to_interface. 6.3. Write machin.scm: Most of the definitions in this file have direct counterparts or are direct consequences of the code in and microcode/cmpint-port.h, so it will be mostly a matter of re-coding the definitions in Scheme rather than C. In particular, you will have to decide how registers are going to be used and split between your C compiler and Liar. If your architecture has a large register set, you can let C keep those registers to which it assigns a fixed meaning (stack pointer, frame pointer, global pointer), and use the rest for Liar. If your machine has few registers or you feel more ambitious, you can give all the registers to Liar, but the code for transferring control between both languages in cmpaux-port.m4 will become more complex. Either way, you will need to choose appropriate registers for the Liar fixed registers (stack pointer, free pointer, register block pointer, dynamic link register and optionally, datum mask, return value register, memtop register, and scheme_to_interface address pointer). 6.4. Write the assembler: You can write the assembler any old way you want, but it is easier to use the branch tensioner and the rest of the facilities if you use the same conventions that the existing assemblers use. In particular, with any luck, you will be able to copy inerly.scm, insmac.scm, and parts of assmd.scm verbatim from an existing port, and for most machines, coerce.scm is straightforward to write. assmd.scm defines procedures that depend only on the endianness of the architecture. You may want to start with the MIPS version since this version accommodates both endianness possibilities as MIPS processors can be configured either way. If your processor has fixed endianness, you can prune the inappropriate code. The block-offset definitions must agree with those in microcode/cmpint-port.h, and the padding definitions are simple constants. Assuming that you decide to use the same structure as existing assemblers, you may need to write parsers for addressing modes if your machine has them. You can use the versions in the MC68020 (bobcat), Vax, and i386 (Intel 386) ports for guidance. Addressing modes are described by a set of conditions under which they are valid, and some output code to issue. The higher-level code that parses instructions in insmac.scm must decide where the bits for the addressing modes must appear. The MC68020 version divides the code into two parts, the part that is inserted into the opcode word of the instruction (further subdivided into two parts), and the part that follows the opcode word as an extension. The Vax version produces all the bits at once since addressing modes are not split on that architecture. You should write the addressing mode definitions in port/insutl.scm, plus any additional transformers that the instruction set may require. Once you have the code for the necessary addressing modes and transformers (if any), and the parsing code for their declarations in port/insmac.scm, writing the instr.scm files should not be hard. Remember to include pseudo-opcodes for inserting constants in the assembly language, and for declaring external labels so that the gc-offset to the beginning of the compiled code block will be inserted correctly. See for example, the definition of the EXTERNAL-LABEL pseudo-opcode in machines/mips/instr1.scm, and its use in machines/mips/rules3.scm. 6.5. Write the LAPGEN rules: You will need to write lapgen.scm, rules1.scm, rules2.scm, rules3.scm, and parts of rules4.scm. Most of rules4.scm is not used by the compiler with the ordinary switch settings and the code may no longer work in any of the ports, and rulfix.scm and rulflo.scm are only necessary to open code fixnum and flonum arithmetic. A good way to reduce the amount of code needed at first is to turn primitive open coding off, and ignore rulfix.scm and rulflo.scm. Lapgen.scm need not include the shared code used to deal with fixnums and flonums, but will require the rest, especially the code used to invoke utilities in the compiled code interface. rules1.scm and rules2.scm are relatively straightforward since the RTL instructions whose translations are provided there typically map easily into instructions. rules4.scm need only have the INTERPRETER-CALL:CACHE-??? rules, and these are simple invocations of runtime library routines which you can emulate from exisiting ports. rules3.scm is an entirely different matter. It is probably the hardest file to write when porting the compiler. The most complicated parts to understand, and write, are the closure code, the invocation prefix code, and the block assembly code. The block assembly code can be taken from another port. You will only have to change how the transmogrifly procedure works to take into account the size and layout of un-linked execute caches. The invocation prefix code is used to adjust the stack pointer, and move a frame in the stack prior to a call to guarantee proper tail recursion. The frame moved is the one pointed at by the stack pointer, and it may be moved a distance known at compile time (invocation-prefix:move-frame-up rules) or a distance that cannot be computed statically (invocation-prefix:dynamic-link rules). The move-frame-up rules are simple, but you should remember that the starting and ending locations for the frame may overlap, so you must ensure that data is not overwritten prematurely. The dynamic link rules are similar to the move-frame-up rules (and typically share the actual moving code) but must first decide the location where the frame should be placed. This is done by comparing two possible values for the location, and choosing the value closest to the current stack pointer (i.e. numerically lower since the stack grows towards smaller addresses). Again, the source and target locations for the frame may overlap, so the generated code must be careful to move the data in such a way that no data will be lost. The closure code is the most painful to write. When writing cmpint-port.h you decided what the actual code in closure entries would be, and the code for closure headers is a direct consequence of this. The combination of the instructions in a closure object, the helper instructions in assembly language (if any), and the instructions in the closure header must ultimately push the closure object (or its canonical representative) on the stack as if it were the last argument to the procedure, and pending interrupts (and gc) must be checked on entry to the closure. The interrupt back-out code is different from the ordinary procedure interrupt back-out code because the procedure object (the closure or its representative) is on top of the stack. The cons-closure rules are used to allocate closure objects from the runtime heap. Some of this allocation/initialization may be done out of line, especially if ``assembling'' the appropriate instructions on the fly would require a lot of code. In addition, you may have to call out-of-line routines to synchronize the processor caches or block-allocate multiple closure entries. 6.6. Write stubs for remaining port files: rgspcm.scm and dassm1.scm can be copied verbatim from any other port. lapopt.scm only needs to define an identity procedure. rulfix.scm, rulflo.scm, and rulrew.scm need not define any rules, since you can initially turn off open coding of primitive operators. dassm2.scm and dassm3.scm need not be written at first, but they are useful to debug the assembler (since disassembling some code should produce code equivalent to the input to the assembler) and compiler output when you forgot to make it output the LAP. 6.7. Write the compiler-building files: make.scm, and comp.cbf should be minorly modified copies of the corresponding files in another port. comp.sf and decls.scm can be essentially copied from another port, but you will need to change the pathnames to refer to your port directory instead of the one you copied them from, and in addition, you may have to add or remove instr and other files as appropriate. 6.8. Write microcode/cmpaux-port.m4: cmpaux.txt documents the entry points that this file must provide. You need not use m4, but it is convenient to conditionalize the code for debugging and different type code size. If you decide not to use it, you should call your file cmpaux-port.s 6.8.1. Determine your C compiler's calling convention. Find out what registers have fixed meanings, which are supposed to be saved by callees if written, and which are supposed to be saved by callers if they contain useful data. 6.8.2. Find out how C code returns scalars and small C structures. If the documentation for the compiler does not describe this, you can write a C program consisting of two procedures, one of which returns a two-word (two int) struct to the other, and you can examine the assembly language produced by the compiler. 6.8.3. Design how scheme compiled code will invoke the C utilities. Decide where the parameters (maximum of four) to the utilities will be passed (preferably wherever C procedures expect arguments), and where the utility index will be passed (preferably in a C caller-saves register). 6.8.4. Given all this, write a minimalist cmpaux-port.m4. In other words, write those entry points that are absolutely required (C_to_interface, interface_to_C, interface_to_scheme, and scheme_to_interface). Be especially careful with the code that switches between calling conventions and register sets. C_to_interface and interface_to_scheme must switch between C and Liar conventions, while scheme_to_interface must switch the other way. interface_to_C must return from the original call to C_to_interface. Make sure that C code always sees a valid C register set and that code compiled by Liar always sees a valid Scheme register set. 6.9. After the preliminary code works: Once the compiler is up enough to successfully compile moderately complex test programs, and the compiled code and the interface have been tested by running the code, you probably will want to go back and write the files that were skipped over. In particular, you definitely should write rulfix.scm and rulrew.scm, and rulflo.scm and the disassembler if at all possible. 7. Building and testing the compiler. Once the port files have been written, you are ready to build and test the compiler. The first step is to build an interpreted compiler and run simple programs. Most simple bugs will be caught by this. 7.1. Re-building scheme. You need to build a version of the microcode with the compiled code interface (portable runtime library) in it. Besides writing cmpint-port.h and cmpaux-port.m4, you will need to do the following: - Copy (or link) cmpint-port.h to cmpint2.h. - Modify m.h to use 6-bit-long type tags (rather than the default 8) if you did not do this when you installed the microcode. If you do this, you will not be able to load .bin files created with 8 bit type tags. You can overcome this problem by using the original .psb files again to regenerate the .bin files. Alternatively, you can use a version of Bintopsb compiled with 8-bit tags to generate new .psb files, and a version of Psbtobin compiled with 6-bit tags to generate the new .bin files. Anotheroption is to bring the compiler up using 8 bit tags, but you may run out of address space. The simplest way to specify 6-bit type tags is to add a definition of C_SWITCH_MACHINE that includes -DTYPE_CODE_LENGTH=6 . Be sure to add any m4 switches that you may need so that the assembly language will agree on the number of tag bits if it needs it at all. If your version of m4 does not support command-line definitions, you can use the s/ultrix.m4 script to overcome this problem. Look at the m/vax.h and s/ultrix.h files for m4-related definitions. ==> We should just switch the default to 6 bits and be done with it. - Modify ymakefile to include the processor dependent section that lists the cmpint-port.h and cmpaux-port.m4 files. You can emulate the version for any other compiler port. It is especially important that the microcode sources be compiled with HAS_COMPILER_SUPPORT defined. - Remove (or save elsewhere) all the .o files, scheme.touch, and scheme, the linked scheme microcode. - Do ``make xmakefile;make -f xmakefile scheme'' to generate a new linked microcode. Once you have a new linked microcode, you need to regenerate the runtime system image files even if you have not changed the length of the type tags. This is done as follows: - Re-generate a runtime.com (actually runtime.bin) image file by invoking scheme with the options ``-large -fasl make.bin'' while connected to the runtime directory, and then typing (disk-save "/runtime.com") at the Scheme prompt. - You should probably also generate a runtime+sf.com file by typing (begin (cd "") (load "make") (disk-save "/runtime+sf.com")) at the Scheme prompt. You also need to have a working version of cref. This can be done by invoking scheme with the options ``-band runtime+sf.com'', and then typing (begin (cd "") (load "cref.sf")) at the Scheme prompt. If this errors because of the lack of a ``runtim.glob'' file, try it again after executing (begin (cd "") (load "runtim.sf")) 7.2. Building an interpreted compiler. Once you have a new microcode, compatible runtime system, and ready cref, you can pre-process the compiler as follows: - Copy (or link) comp.pkg, comp.sf, and comp.cbf from the compiler/machines/port directory to the compiler directory. - For convenience, make a link from compiler/machines/port to compiler/port. - Invoke scheme with the ``-band runtime+sf.com'' option, and then execute (begin (cd "") (load "comp.sf")) This will take quite a while, and pre-process some of the files twice. At the end of this process, you should have a .bin file for each of the .scm files, a .ext file for some of them, and a bunch of additional files in the compiler directory (comp.con, comp.ldr, comp.bcon, comp.bldr, comp.glob, comp.free, comp.cref). It is a good idea to look at the comp.cref file. This is a cross-reference of the compiler and may lead you to find typos or other small mistakes. The first section of the cref file (labeled ``Free References:'') lists all variables that are not defined in the compiler or the runtime system. The only variables that should be in this list are SF, and SF/PATHNAME-DEFAULTING. The ``Undefined Bindings:'' section lists those variables defined in the runtime system and referenced freely by the compiler sources. The remainder of the cref file lists the compiler packages and the cross reference of the procedures defined by it. - Load up the compiler. Invoke scheme with the options ``-compiler -band runtime+sf.com'', and then type (begin (cd "") (load "port/make") (disk-save "/compiler.com")) You should then be able to invoke the compiler by giving scheme the ``-compiler'' option, and use it by invoking CF. 7.3. Testing the compiler. There is no comprehensive test suite for the compiler. There is, however, a small test suite that is likely to catch gross errors. The files for the test suite are in compiler/etc/tests. Each file contains a short description of how it can be used. Make sure, in particular, that you test the closure code thoroughly, especially if closure allocation hand-shakes with out-of-line code to accomodate the CPU's caches. A good order to try the test suite in is three.scm expr.scm pred.scm close.scm blast.scm reverse.scm arith.scm bitwse.scm fib.scm vector.scm reptd.scm lexpr.scm klexpr.scm close2.scm prim.scm free.scm uvuuo.scm link.scm uuo.scm unv.scm tail.scm y.scm sort/*.scm (see sort/README for a description) The programs in the first list test various aspects of code generation. The programs in the second list test the handling of various dynamic conditions (e.g. error recovery). The programs in the third list are somewhat larger, and register allocation bugs, etc., are more likely to show up in them. A good idea at the beginning is to turn COMPILER:GENERATE-RTL-FILES? and COMPILER:GENERATE-LAP-FILES? on and compare them for plausibility. If you have ported the disassembler as well, you should try disassembling some files and comparing them to the input LAP. They won't be identical, but they should be similar. The disassembler can be invoked as follows: (compiler:write-lap-file "") ; writes a .lap file. (compiler:disassemble ) ; writes on the screen. The .lap filename extension is used by COMPILER:WRITE-LAP-FILE and by the compiler when COMPILER:GENERATE-LAP-FILES? is true, so you may want to rename the .lap file generated by the compiler to avoid overwriting it when using COMPILER:WRITE-LAP-FILE. Various runtime system files also make good tests. In particular, you may want to try list.scm, vector.scm, and arith.scm. You can try them by loading them, and invoking procedures defined in them, but you must execute (initialize-microcode-dependencies!) after loading arith.com and before invoking procedures defined there. 7.4. Compiling the compiler. The real test of the compiler comes when it is used to compile itself and the runtime system. Re-compiling the system is a slow process, that can take a few hours even with a compiled compiler on a fast machine. Compiling the compiler with an interpreted compiler would probably take days. There are two ways to speed up the process: * Cross-compiling: If you can access some machines on which the compiler already runs, you can cross-compile the sources using a compiled compiler. This method is somewhat involved because you will need binaries for both machines, since neither can load or dump the other's .bin files. Imagine that you have a Vax, and you are porting to a Sparc. You will need to pre-process and compile the Sparc's compiler on the Vax to use it as a cross-compiler. This can be done by following the same pattern that you used to generate the interpreted compiler on the Sparc, but running everything on the Vax, and then compiling the cross-compiler on the Vax by running scheme with the ``-compiler'' option, and typing (begin (cd "") (load "comp.cbf") (disk-restore "runtime+sf.com")) ;; After the disk-restore (begin (load "make") (in-package (->environment '(compiler)) (set! compiler:cross-compiling? true)) (disk-save "sparccom.com")) to produce a cross-compiler band called "sparccom.com". Once you have the cross-compiler, you can use CROSS-COMPILE-BIN-FILE to generate .moc files. The .moc files can be translated to .psb files on the Vax. These .psb files can in turn be translated to .moc files on the Sparc, and you can generate the final .com files by using CROSS-COMPILE-BIN-FILE-END defined in compiler/base/crsend. compiler/base/crsend can be loaded on a plain runtime system (i.e. without SF or a compiler). You will probably find the following idioms useful: (for-each cross-compile-bin-file (directory-read "/*.bin")) (for-each cross-compile-bin-file-end (directory-read "/*.moc")). To translate the original .moc files to .psb files, you should use microcode/Bintopsb on the Vax as follows: Bintopsb ci_processor=?? foo.psb where the value of ci_processor should be the value of COMPILER_PROCESSOR_TYPE defined in microcode/cmpint-port.h. You can then generate the target .moc files by using microcode/Psbtobin on the Sparc as follows: Psbtobin allow_cc foo.moc * Distributing the task over several machines: You can use more than one machine to compile the sources. If the machines do not share a file system, you will have to pre-partition the job and generate a script for each machine. If the machines share a (network) file system, you can try to use compiler/etc/xcbfdir. This file defines two procedures, COMPILE-DIRECTORY, and CROSS-COMPILE-DIRECTORY, that use a simple-minded protocol based on creating .tch files to reserve files to compile, and can therefore be run on many machines simultaneously without uselessly repeating work or getting in each other's way. These two methods are not exclusive. We typically bring up the compiler on a new machine by distributing the cross-compilation job. The compiler and the cross-compiler use a lot of memory while running, and virtual memory is really no substitute for physical memory. You may want to increase your physical memory limit on those systems where this can be controlled (e.g. under BSD use the ``limit'' command). If your machines don't have much physical memory, or it is too painful to increase your limit, i.e. you have to re-compile or re-link the kernel, you may want to use microcode/bchscheme instead of microcode/scheme. Bchscheme uses a disk file for the spare heap, rather than a region of memory, putting the available memory to use at all times. 7.5. Compiler convergence testing. Once you have a compiled compiler, you should run the same test suite that you ran with the interpreted compiler. Once you have some degree of confidence that the compiled compiler works, you should make sure that it can correctly compile itself and the runtime system. This re-compilation can manifest second-order compiler bugs, that is, bugs in the compiler that cause it to compile parts of itself incorrectly without crashing, so that programs compiled by this incorrectly-compiled compiler fail even though these programs did not fail when compiled by the original compiler. Of course, you can never really tell if the compiler has compiled itself successfully. You can only tell that it is not obviously wrong (i.e. it did not crash). Furthermore, there could be higher-order bugs that would take many re-compilations to find. However, if the binaries produced by two successive re-compilations are identical, further re-compilations would keep producing identical binaries and no additional bugs will be found this way. Moreover, if the compiler and system survive a couple of re-compilations, the compiler is likely to be able to compile correctly most programs. To run this compiler convergence test, you need to re-compile the compiler. In order to do this, you need to move the .com files from the source directories so that COMPILE-DIRECTORY and RECOMPILE-DIRECTORY will not skip all the files (they avoid compiling those already compiled). The simplest way to move all these files is to type ``make stage1'' at your shell in the source directories (runtime, sf, cref, and compiler). This command will create a STAGE1 subdirectory for each of the source directories, and move all the .com and .binf files there. You can then use compiler/comp.cbf, or compiler/etc/xcbfdir and RECOMPILE-DIRECTORY to regenerate the compiler. If you generated the stage1 compiled compiler by running the compiler interpreted, the new .com files should match the stage1 .com files. If you generated the stage1 compiler by cross-compilation, they will not. The cross-compiler turns COMPILER:COMPILE-BY-PROCEDURES? off, while the default setting is on. In the latter case, you want to generate one more stage to check for convergence, i.e. execute ``make stage2'' in each source directory, and re-compile once more, at each stage using the compiler produced by the previous stage. Once you have two stages that you think should have identical binaries, you can use COMPARE-COM-FILES, defined in compiler/etc/comcmp, to compare the binaries. The simplest way to use it is to also load compiler/etc/comfiles and then use the CHECK-STAGE procedure. (check-stage "STAGE2" '("runtime" "sf" "compiler/base")) will compare the corresponding .com files from runtime and runtime/STAGE2, sf and sf/STAGE2, and compiler/base and compiler/base/STAGE2. If nothing is printed, the binaries are identical. Otherwise some description of the differences is printed. COMPARE-COM-FILES does not check for isomorphism of Scode objects, so any sources that reference Scode constants (e.g. runtime/advice.scm) will show some differences that can safely be ignored. Generally, differences in constants can be ignored, but length and code differences should be understood. The code in question can be disassembled to determine whether the differences are real or not. While testing the compiler, in addition to checking for the correct operation of the compiled code, you should also watch out for crashes and other forms of unexpected failure. In particular, hardware traps (e.g. segmentation violations, bus errors, illegal instructions) occurring during the re-compilation process are a good clue that there is a problem somewhere. 8. Debugging. The process of porting a compiler, due to its complexity, is unlikely to proceed perfectly. Things are likely to break more than once while running the compiler and testing the compiled code. Debugging a compiler is not trivial, because often the failures (especially after a while) will not manifest themselves until days, weeks, or months after the compiler was released, at which point the context of debugging the compiler has been swapped out by the programmer. Second-order compiler bugs do not make things any easier. Liar does not have many facilities to aid in debugging. This section mentions some of the few, and some techniques to use with assembly-language debuggers (gdb, dbx, or adb). The main assumption in this section is that the front end and other machine-independent parts of the compiler work correctly. Of course, this cannot be guaranteed, but in all likelihood virtually all of the bugs that you will meet when porting the compiler will be in the new machine-specific code. If you need to examine some of the front-end data structures, you may want to use the utilities in base/debug.scm which is loaded in the compiler by default. In particular, you will want to use PO (for print-object) to examine compiler data structures, and DEBUG/FIND-PROCEDURE to map procedure names to the data structures that represent the procedures, or more correctly, the lambda expressions. 8.1. Preliminary debugging of the compiled code interface. The first item of business, after the microcode interface (cmpaux-port.m4 and cmpint-port.h) has been written, is to guarantee that properly constructed compiled code addresses do not confuse the garbage collector. This can be done before writing any of the remaining files, but you must have rebuilt the microcode and the runtime.com band. A simple test to run is the following: (define foo ((make-primitive-procedure 'COERCE-TO-COMPILED-PROCEDURE) (lambda (x y) (+ x y)) 2)) (gc-flip) (gc-flip) (gc-flip) If the system does not crash or complain, in all likelihood the garbage collector can now properly relocate compiled code objects. This object can also be used to test parts of the compiled code interface. FOO is bound to a trampoline that will immediately revert back to the interpreter when invoked. The next test is to determine that FOO works properly. You can follow the execution of FOO by using a debugger and placing breakpoints at cmpint.c:apply_compiled_procedure, cmpaux-port.s:C_to_interface, cmpaux-port.s:scheme_to_interface (or trampoline_to_interface if it is written), cmpint.c:comutil_operator_apply_trap, cmpint.c:comutil_apply, and cmpaux-port.s:interface_to_C and then evaluating (FOO 3 4). When setting the breakpoints, remember that C_to_interface, scheme_to_interface, and interface_to_scheme are not proper C procedures, so you should use the instruction-level breakpoint instructions or formats, not the C procedure breakpoint instructions or formats. If you are using adb, this is moot, since adb is purely an assembly-language debugger. If you are using gdb, you should use ``break *&C_to_interface'' instead of ``break C_to_interface''. If you are using dbx, you will want to use the ``stopi'' command, instead of the ``stop'' command to set breakpoints in the assembly language routines. Make sure that the arguments to comutil_operator_apply_trap look plausible and that the registers have the appropriate contents when going into scheme code and back into C. In particular, you probably should examine the contents of the registers right before jumping into the trampoline code, and single step the trampoline code until you get back to scheme_to_interface. In order to parse the Scheme objects, you may want to keep a copy of microcode/type.names handy. This file contains the names of all the scheme type tags and their values as they appear in the most significant byte of the word when type tags are 8 bits long and 6 bits long. Remember that you may have to insert segment bits into addresses in order to examine memory locations. You should also make sure that an error is signalled when FOO is invoked with the wrong number of arguments, and that the system correctly recovers from the error (i.e., it gives a meaningful error message and an error prompt, and resets itself when you type ^G). This test exercises most of the required assembly language code. The only entry point not exercised is interface_to_scheme. 8.2. Debugging the assembler. Assuming that the compiler generates correctly formatted compiled code objects, fasdump should be able to dump them out without a problem. If you have problems when dumping the first objects, and assuming that you ran the tests in section 8.1., then in all likelihood the block offsets are not computed correctly. You should probably re-examine the rule for the EXTERNAL-LABEL pseudo operation, and the block-offset definitions in machines/port/assmd.scm. Once you can dump compiled code objects, you should test the assembler. A simple, but somewhat inconvenient way of doing this is to use adb as a disassembler as follows: Scheme binary (.bin and .com) files have a 50 longword header that contain relocation information. The longword that follows immediately is the root of the dumped object. If COMPILER:COMPILE-BY-PROCEDURES? is false, the compiler dumps a compiled entry point directly, so the format word for the first entry is at longword location 53 (* 4 = 0xd4), and the instructions follow immediately. If COMPILER:COMPILE-BY-PROCEDURES? is true, the compiler dumps an Scode object that contains the first entry point as the ``comment expression''. The format word for the first entry point is then at longword location 55 (* 4 = 0xdc), and the instructions for the top-level block follow immediately. Thus, assuming that there are four bytes per Scheme object (unsigned long in C), and that foo.com was dumped by the compiler with COMPILER:COMPILE-BY-PROCEDURES? set to false, the following would disassemble the first 10 instructions of the generated code. adb foo.com 0xd8?10i If COMPILER:COMPILE-BY-PROCEDURES? was set to true, the following would accomplish the same task: adb foo.com 0xe0?10i You can use adb in this way to compare the input assembly language to its binary output. Remember that you can obtain the input assembly language by using the COMPILER:GENERATE-LAP-FILES? switch. 8.3. Setting breakpoints in Scheme compiled code. Compiled code is not likely to work correctly at first even after the compiler stops signalling errors. In general, when you find that compiled code executes incorrectly, you should try to narrow it down as much as possible by trying the individual procedures, etc., in the code, but ultimately you may need the ability to set instruction-level breakpoints and single-step instructions in compiled code. A problem peculiar to systems in which code is relocated on the fly is that you cannot, in general, obtain a permanent address for a procedure or entry point. The code may move at every garbage collection, and if you set a machine-level breakpoint with a Unix debugger, and then the code moves, you will probably get spurious traps when re-running the code. Unix debuggers typically replace some instructions at the breakpoint location with instructions that will cause a specific trap, and then look up the trapping location in some table when the debugged process signals the trap. One way around this problem is to ``purify'' all compiled scheme code that you will be setting breakpoints in. If you purify the code, it will move into ``constant space'' and remain at a constant location across garbage collections. The PURIFY procedure expects an object to purify as the first argument, a boolean flag specifying whether the object should be moved into constant space (if false) or pure space (if true) as a second argument, and a boolean flag specifying whether purification should occur immediately (if false) or be delayed until the next convenient time (if true) as a third argument. You should (purify false false) when moving compiled code objects to constant space for debugging purposes. Alternatively, you can specify that you want the code to be purified when you load it by passing appropriate arguments to LOAD. Since load delays the actual purification, you will need to invoke GC-FLIP twice to flush the purification queue. At any rate, setting the actual breakpoints is not completely trivial, since you must find the virtual address of the instructions, and then use them with your assembly-language debugger. The simplest way to do this is to get Scheme to print the datum of the entry points for you, and then type one of Scheme's interrupt characters to gain the debugger's attention and set the breakpoint. Continuing from the debugger will allow you to type further expressions to Scheme. Imagine, for example, that we have compiled the runtime file list.scm and some of the procedures in it, namely MEMBER and MEMQ, do not work properly. After purifying the code, you can type ``memq'' at the read-eval-print loop and it will respond with something like ;Value 37: #[compiled-procedure 37 ("list" #x5A) #x10 #x10FE880] This specifies that MEMQ is bound to an ordinary compiled procedure (not a closure), that it was originally compiled as part of file ``list'' and it was part of compiled code block number 90 (= #x5a) in that file. The current datum of the object is #x10FE880 (this is the address without the segment bits if any), and the offset to the beginning of the containing compiled code block is #x10. Thus you could then gain the attention of the debugger and set a breakpoint at address 0x10fe880 (remember to add the segment bits if necessary) and after continuing back into Scheme, use MEMQ normally to trigger the breakpoint. The case with MEMBER is similar. Typing ``member'' at the read-eval-print loop will cause something like ;Value 36: #[compiled-closure 36 ("list" #x56) #x5C #x10FE484 #x1180DF8] to be printed. This specifies that MEMBER is bound to a compiled closure, originally in compiled code block number 86 (= #x56) in file ``list'', that the entry point to the closure is at datum #x1180DF8, that the entry point shared by all closures of the same lambda expression (the ``real'' entry point) is at datum #x10FE484, and that this entry point is at offset #x5C of its containing compiled code block. Thus if you want to single step the closure code (a good idea when you try them at first), you would want to set a breakpoint at address #x1180DF8 (plus appropriate segment bits), and if you want to single step or examine the real code, then you should use address #x10FE484. If you purified the code when you loaded it, the real code would be pure, but the closure itself would not be, since it was not a part of the file being loaded (closures are created dynamically). Thus, before setting any breakpoints in a closure, you should probably purify it as specified above, and obtain its address again, since it would have moved in the meantime. For example, if you are using adb on an HP-PA (where the top two bits of a data segment address are always 01, and thus the top nibble of a Scheme's object address is always 4), assuming that the interpreter printed the above addresses, 0x41180df8:b would set a breakpoint in the MEMBER closure, 0x410fe484:b would set a breakpoint at the start of the code shared by MEMBER and all closures of the same lambda expression, 0x410fe880:b would set a breakpoint at the start of MEMQ. If you are using gdb on a Motorola MC68020 machine, with no segment bits for the data segment, the equivalent commands would be break *0x1180df8 for a breakpoint in the MEMBER closure, break *0x10fe484 for a breakpoint in MEMBER's shared code break *0x10fe880 for a breakpoint in MEMQ. If you are using dbx, you will need to use a command like ``stopi at 0x10fe484'' to achieve the same effect. 8.4. Examining arguments to Scheme procedures. Commonly, after setting a breakpoint at some interesting procedure, you will want to examine the arguments. Currently, Liar passes all arguments on the stack. The Scheme stack always grows towards decreasing addresses, and arguments are pushed from left to right. On entry to a procedure, the stack frame must have been reformatted so that optional arguments have been defaulted, and tail (lexpr) arguments have been collected into a list (possibly empty). Thus on entry to an ordinary procedure's code the stack pointer points to the rightmost parameter in the lambda list, and the rest of the parameters follow at increasing longword addresses. This is also the case on entry to a closure object's instructions, but by the time the shared code starts executing the body of the lambda expression, the closure object itself (or its canonical representative) will be on top of the stack with the arguments following in the standard format. On entry to a closure's shared code, the stack will contain the arguments in the standard format, but the closure object will typically be in the process of being constructed and pushed, depending on the distribution of the task between the instructions in the closure object, the optional helper instructions in assembly language, and the instructions in the closure header. If you are using adb, you can use the following commands: $r displays the names and contents of the processor registers, : 0x8280 0x003e The adb (and dbx) command ``0x4fc564/2x'' should yield similar output. This confirms that the object in question is a return address because the most significant bit of the format word is a 1, and it would be a 0 for a procedure. The encoded gc offset is 0x3e. GC offsets and format words are described in detail in cmpint.txt. Since the least significant bit of the GC offset is 0, it points directly to the vector header of a compiled code block. The real offset is ((0x3e >> 1) << PC_ZERO_BITS) = 0x3e Thus the compiled code block starts at location 0x8fe2ee-0x3e = 0x008fe2b0 Examining the top two words at this address (using the gdb command ``x/2wx 0x008fe2b0'' or the adb and dbx command ``0x8fe2b0/2X'') we 0x8fe2b0 : 0x00000028 0x9c00001d The first word is an ordinary vector header, and the second a non-marked vector header used to inform the GC that 0x1d longwords of binary data, the actual instructions, follow. The last location in the vector, containing the environment, is at address 0x8fe2b0+4*0x28 = 0x8fe350 Examining the preceding adjacent location and this one (using gdb's ``x/2wx 0x8fe34c'' or a similar command for a different debugger) will yield 0x8fe34c : 0x0498ef9c 0x4898e864 The second object is the loading environment, and the first object is the debugging information, in this case a pair. This pair can be examined (using gdb's ``x/2wx 0x98ef9c'' or an analogous command for a different debugger) to yield 0x98ef9c : 0x789ac5ec 0x6800001c The first object is a string, and the second a fixnum, indicating that the return address at hand belongs to the compiled code block numbered 0x1c in the file whose name is that string. Scheme strings have two longwords of header, followed by an ordinary C string that includes a null terminating character, thus the C string starts at address 0x9ac5ec+4*2=0x9ac5f4, and the gdb command ``x/s 0x9ac5f4'' or the adb and dbx command ``0x9ac5f4/s'' will display something like: 0x9ac5f4 : (char *) 0x9ac5f4 "/usr/local/lib/mit-scheme/SRC/runtime/parse.binf" Thus the return address we are examining is at offset 0x3e in compiled code block number 0x1c of the runtime system file ``parse.com''. If the disassembler is available, you can then use (compiler:write-lap-file "parse") to find out what this return address is, or if you compiled (or re-compile) parse.scm generating lap files, you can probably guess what return address is at offset 0x3e (the input lap files do not contain computed offsets, since these are computed on final assembly). This interaction would remain very similar for other machines and compiled entry points, given the same or similar debuggers. The variations would be the following: - Segment bits might have to be added to the object datum components to produce addresses. For example, on the HP-PA with segment bits 01 at the most significant end of a word, the C string for Scheme string object 0x789ac5ec would start at address 0x409ac5ec+8=0x409ac5f4, instead of at address 0x9ac5ec+8=0x9ac5f4. - The gc offset might be computed differently, depending on the value of PC_ZERO_BITS. For example, on a Vax, where PC_ZERO_BITS has the value 0, an encoded offset of value 0x3e would imply a real offset of value (0x3e >> 1)=0x1f. On a MIPS R3000, where PC_ZERO_BITS is 2, the same encoded offset would encode the real offset value ((0x3e >> 1) << 2)=0x7c. In addition, if the low order bit of the encoded gc offset field were 1, a new gc offset would have to be extracted, and the process repeated until the beginning of the block was reached. - The constant offsets added to various addresses (e.g. that added to a string object's address to obtain the C string's address) would vary if the number of bytes per Scheme object (sizeof (unsigned long)) in C were not 4. - Not all compiled entry points have debugging information descriptors accessible the same way. Trampolines don't have them at all, and closures have them in the shared code, not in the closure objects. To check whether something is a trampoline, you can check the format field (most trampolines have 0xfffd) or verify that the instructions immediately call the compiled code interface. Closure objects have type code MANIFEST-COMPILED-CLOSURE instead of MANIFEST-VECTOR in the length word of the compiled code block. Once you obtain the real entry point for a closure, you can use the same method to find out the information about it. 8.6. Things to watch out for. The worst bugs to track are interrupt and garbage-collection related. They will often make the compiled code crash at seemingly random points, and are very hard to reproduce. A common source of this kind of bug is a problem in the rules for procedure headers. Make sure that the rules for the various kinds of procedure headers generate the desired code, and that the desired code operates correctly. You can test this explicitly by using an assembly-language debugger to set breakpoints at the entry points of various kinds of procedures. When the breakpoints are reached, you can bump the Free pointer to a value larger than MemTop, so that the interrupt branch will be taken. If the code continues to execute correctly, you are probably safe. You should especially check procedures that expect dynamic links for these must be saved and restored correctly. Closures should also be tested carefully, since they need to be reentered correctly, and the closure object on the stack may have to be de-canonicalized. Currently C_to_interface and interface_to_scheme must copy the interpreter's value register into the compiler's value register, and must extract the address of this value and store it in the dynamic link register. Register allocation bugs also manifest themselves in unexpected ways. If you forget to use NEED-REGISTER! on a register used by a LAPGEN rule, or if you allocate registers for the sources and target of a rule in the wrong order (remember the cardinal rule!), you may not notice for a long time, but some poor program will. If this happens, you will be lucky if you can find and disassemble a relatively small procedure that does not operate properly, but typically the only notice you will get is when Scheme crashes in an unrelated place. Fortunately, this type of bug is reproducible. In order to find the incorrectly compiled code, you can use binary search on the sources by mixing interpreted and compiled binaries. When loading the compiler, .bin files will be used for those files for which the corresponding .com file does not exist. Thus you can move .com files in and out of the appropriate directories, reload, and test again. Once you determine the procedure in which the bug occurs, re-compiling the module and examining the resulting RTL and LAP programs should lead to identification of the bug. 9. Bibliography 1. "Efficient Stack Allocation for Tail-Recursive Languages" by Chris Hanson, in Proceedings of the 1990 ACM Conference on Lisp and Functional Programming. 2. "Free Variables and First-Class Environments" by James S. Miller and Guillermo J. Rozas, in Lisp and Symbolic Computation, 4, 107-141, 1991, Kluwer Academic Publishers. 3. "MIT Scheme User's Manual for Scheme Release 7.1" by Chris Hanson, distributed with MIT CScheme version 7.1. 4. "MIT Scheme Reference Manual for Scheme Release 7.1" by Chris Hanson, distributed with MIT CScheme version 7.1. 5. "Taming the Y Operator" by Guillermo J. Rozas, in Proceedings of the 1992 ACM Conference on Lisp and Functional Programming. A.1. MIT Scheme package system The MIT Scheme package system is used to divide large programs into separate name spaces which are then ``wired together''. A large program, like the runtime system, Edwin, or the compiler, has many files and variable names all of which must exist at the same time without conflict. The package system is a prototype system to accomplish this separation, but will probably be replaced once a better module system is developed. Currently, each package corresponds, at runtime, to a Scheme environment. Environments have their usual, tree-shaped structure, and packages are also structured in a tree, but the trees need not be isomorphic, although they often are. Each package is given a name, e.g.: (compiler reference-contexts) whose corresponding environment can be found using the procedure ->ENVIRONMENT: (->environment '(compiler reference-contexts)) ;; Call this CR By convention, this package corresponds to an environment below the (->environment '(compiler)) ;; Call this C environment, and therefore CR contains all variables defined in C, as well as those specifically defined in CR. The package name ``()'' corresponds to the system global environment. The package structure for the compiler is defined in the file /machines//comp.pkg In that file, each package has a description of the form: (define-package (files ) (parent ) (export ) (import )) where are the names of the files that should be loaded in to package . (parent ) declares the package whose name is to be the parent package of . Lexical scoping will make all variables visible in also visible in . The EXPORT and IMPORT declarations are used to describe cross-package links. A package may export any of its variables to any other package using EXPORT; these variables will appear in both packages (environments), and any side effect to one of these variables in either package will be immediately visible in the other package. Similarly, a package may import any of another package's variables using IMPORT. Any number (including zero) of IMPORT and EXPORT declarations may appear in any package declaration. Here is an example package declaration, drawn from the compiler: (define-package (compiler top-level) (files "base/toplev" "base/crstop") (parent (compiler)) (export () cf compile-bin-file compile-procedure compile-scode compiler:reset! cross-compile-bin-file cross-compile-bin-file-end) (export (compiler fg-generator) compile-recursively) (export (compiler rtl-generator) *ic-procedure-headers* *rtl-continuations* *rtl-expression* *rtl-graphs* *rtl-procedures*) (export (compiler lap-syntaxer) *block-label* *external-labels* label->object) (export (compiler debug) *root-expression* *rtl-procedures* *rtl-graphs*) (import (runtime compiler-info) make-dbg-info-vector) (import (runtime unparser) *unparse-uninterned-symbols-by-name?*)) The read-eval-print loop of Scheme evaluates all expressions in the same environment. It is possible to change this environment using the procedure GE, e.g.: (ge (->environment '(compiler top-level))) To find the package name of the current read-eval-print loop environment, if there is one, evaluate: (pe) The package system is currently completely static; it is difficult to create packages and wire them together on the fly. If you find that you need to temporarily wire a variable to two different environments (as you would do with an IMPORT or EXPORT declaration), use the procedure ENVIRONMENT-LINK-NAME: (environment-link-name ) For example, to make WRITE-RESTARTS, originally defined in the (runtime debugger) package, also visible in the (edwin debugger) package, evaluate: (environment-link-name (->environment '(edwin debugger)) (->environment '(runtime debugger)) 'write-restarts)