23. C-Generating Back End#
c_gen_be.c contains code to translate the intermediate language to C;
c_gen_be.h contains the associated declarations. This is a fake “back end”
that is useful as a way of testing the front end in the absence of a real back
end, as a portability aid, and as an example of how to traverse the
intermediate code (which may be helpful to those writing back ends).
23.1. Function#
23.1.1. Generated C Code#
The C code generated can be either ANSI C or old-style K&R C, depending on the
setting of the configuration flag C_GEN_BE_GENERATES_ANSI_C.
The output contains #line directives that relate the generated code back to
the original source lines that generated it. The #line directives do not
ascend monotonically through the line numbers of the source file; they jump
around as the code generation jumps around. However, since the C-generating
back end tends to process all declarations of entities of one kind in a sweep
through those entities in source order, the output tends to have all the types
in source order, then all the variables in source order, etc.
Long output lines are broken (between tokens) to avoid unduly stressing C compilers that must process the generated code.
Since the generated code is intended to be fed into a C compiler, rather than
read by a human, it is not very readable. The readability can be improved
(slightly) by turning on debugging (e.g., -d0 on the command line); that
will cause the C-generating back end to generate annotation comments and
indentation. Unreferenced entities, which ordinarily are not put out at all,
are generated inside #if 0/#endif delimiters in this mode.
23.1.2. Transformations#
For the most part, what the C-generating back end does is just writing out the C code that corresponds directly to the IL. Since the IL has the same concepts and constructs as the C language, the processing is pretty straightforward. However, there are a few complicating transformations.
The following transformations on names are done:
- Names that match C reserved words, or words that are likely to mean something to the underlying C compiler (e.g.,
unix), are given a prefix of “__x”. - Names with no linkage (e.g., local variables) are given a prefix that is the source sequence (line) number and column number of the name’s declaration, to make sure they do not conflict with similar names from elsewhere in the program (e.g., “
i” is output as “__28_7_i”). - Unnamed entities are given generated names of the form “
__Tnnnnnn”, with nnnnnn generated from the IL entry address. - The “
this” parameter, passed through from C++, is given the name “this”. - Function-local and prototype-scope types are given names formed by concatenating the original name, “
__”, the (mangled) function name, “__”, and “Lnn”, where nn is a number identifying the function, prototype, or block scope scope of which the type is a member. (Renaming is needed because such types are promoted out of functions; more on this below.) - When generating K&R C, file-scope static variables are given a suffix formed from the name of the module and the date and time of compilation. This suffix is needed because initialized file-scope static variables must be made external to avoid some pcc limitations on declaring static variables more than once. The suffix helps to ensure that the now-external name does not conflict with like-named static and external variables from other compilation units.
Another set of transformations is necessitated by the fact that IL lowering generates “C” IL that actually contains some minor extensions. These “extensions” are things that a real back end would probably take in its stride (i.e., it wouldn’t see these cases as being different than normal cases), but which must be rewritten if standard C code must be generated:
- Dynamic initializations for declarations can appear in mid-block, after executable statements have appeared. (These are generated as executable code. Because of this, “
const” type qualifiers are suppressed on variables initialized dynamically and on all struct members.) - Structs and unions can have no members at all, and aggregate constants used in initializers can contain empty lists of constants (”
{}”). (The struct/union definition will include a dummy field, and the initialization puts a zero into that field.) - Enumerations having no enumerator constants are put out as the corresponding integral type.
- The
eok_bassignoperator is used to do assignment of arrays (e.g., in generated copy constructors). (Amemcpyis generated.) - Prototyped parameter variables may be unnamed. (A name is generated.)
- Variables can be indicated to be initialized to zero (the
initk_zeroinitialization kind). This is used to differentiate real definitions of external variables from tentative definitions (a distinction that is hazy in C), and for local aggregate variables that are partially initialized. (For static variables, the initialization to zero is preserved in the generated C; for automatic variables, amemsetis generated.) - String literals in C++ are
const. IL lowering leaves themconst, even though string literals in C are notconst. (This does not require a transformation in the generated C code: the string literals are put out as such, and the C compiler sees them as non-const, which works out okay.)
When generating K&R C, some transformations are needed to erase ANSI C features that do not exist in K&R C:
- Prototyped functions are put out as old-style unprototyped functions.
- Type qualifiers (”
const” and “volatile”) are not put out. - Enumerations are put out as integral types, and enumeration constants are put out as integral constants.
- “
signed” is never put out. “signed char” is put out as “char”, and “signed int” is not used for the base type of a bit field. - “
long double” is put out as “double”. - The suffixes “
U” on integral constants and “F” and “L” on floating constants are not put out; a cast is used instead. - Union initializations are rendered as executable code.
- Initializations of automatic aggregates are rendered as executable code.
- Bit fields in unions are put out as normal fields.
- Signed bit fields are simulated by adding code to sign-extend and truncate the bit fields on each access.
- Second and third operands of “
?” of typevoidare rewritten. extern voidvariables are put out with typechar.- Labels are given a prefix of “
__L_” to accommodate the fact that pcc does not maintain a separate name space for labels.
The following transformations also deal with features missing in K&R C, but these transformations are done even when generating ANSI C to avoid some features that might not be implemented (well) in C compilers:
- Rvalue field selections (e.g., “
f(x).i”) are rewritten as lvalue field selections by using a temporary.struct A { int i; }; extern struct A f(); void m() { j = f().i; /* Rewritten as "j = (Tnnnn = f(), Tnnnn.i)" */ }
- Wide string constants are stored in generated static variables and the variables are used in place of the constants at each reference.
- Character arrays initialized with string constants not containing a trailing null character are rendered in “exploded” form, e.g., something like
int a[4] = "abcd";is rendered asint a[4] = {'a', 'b', 'c', 'd'};
The following transformations are necessary to deal with ordering problems (i.e., they’re necessary because of the order in which the C-generating back end outputs entities):
- Local types of functions are generated at file scope. This avoids problems with extern declarations written inside functions (the extern entity is on the file-scope list, but it may refer to local types of the function).
void f() { struct A { int i; }; extern void g(struct A); }
is rewritten asstruct A { int i; }; extern void g(struct A); void f() {}
- Types in the prototype scope of a function are promoted out of the prototype scope. This is because each function and variable is put out twice, once as a declaration and once as a definition. Without this promotion, the declaration and definition would be incompatible because the two copies of the prototype scope types would be considered to be distinct types.
void f(struct A { int i; } p) {}is rewritten asstruct A { int i; }; void f(struct A); void f(struct A p) {}
Prototype scopes that are not part of a function or variable definition are not processed in this way. When generating K&R C, such unprocessed function types are put out unprototyped, which eliminates the types defined in the prototype scope. When generating ANSI C, such unprocessed function types are put out prototyped, and if any structs, unions, or enums are defined in the prototype scope they are put out inline.When a pointer type containing a prototype scope needs to be put out (i.e., as the type of a constant or in a cast), the type (and cast) is suppressed, because the conversion must have been implicit in the source program – it’s not possible to name the types defined in a prototype scope outside of that scope.
When virtual functions with covariant return types appear in the source
program, IL lowering rewrites them in a way that uses wrapper functions. That
is, when an overriding virtual function returns a pointer to a derived class,
and the overridden virtual function returns a pointer to a base, a wrapper
function is generated that calls the overriding function but returns a pointer
appropriate for the overridden function. Such functions have a body that is
simply a return of an enk_result_of_overriding_function node cast to the
proper pointer-to-base-class type. The C-generating back end expands these
wrapper functions by generating the body of the overriding function again, with
the added cast inserted over the expression at each return statement. Note
that the “obvious” implementation technique of replacing the
enk_result_of_overriding_function node by a call of the overriding function
(i.e., implementing these as actual wrapper functions) will not work if the
functions have variable-length argument lists. The same technique is used for
alternate entry points for constructors in the IA64-ABI, but only if the
routine has a variable-length argument list.
23.1.3. Configuration#
Although the generated C code is generally portable, there are some configuration issues that must be attended to.
The most obvious is to make sure that the sizes and alignments of basic types
defined in targ_def.h match those assumed by the target C compiler.
Block operations (e.g., memcpy) are generated in either the BSD form or the
System V/ANSI form under control of the __BSD__ configuration option, so
make sure its setting matches the library of the target C compiler.
If the target C compiler has some way of indicating that a function is inline,
the code in dump_routine_decl should probably be changed to output that
construct.
Long output lines are wrapped to avoid overly long lines. You can alter the
maximum size by adjusting MAX_OUTPUT_LINE_SIZE. The default is fairly
modest (a few hundred characters).
If the target C compiler has particular bugs or shortcomings, you may want to add code to work around them. There is existing code for the Sun, Microsoft, and gcc C compilers.
The C-generating back end can emit #pragma directives for IL pragma
entries, but only in a somewhat limited way. For pragmas bound to an entity,
the pragma is emitted right before the declaration of the entity. However,
pragmas not bound to an entity are emitted at the beginning of the scope
containing them, and therefore they may not be where expected. That makes that
kind of pragma suitable only in cases where the exact position of the pragma is
not very significant, e.g., for global-effect pragmas and those that associate
attributes with entities by naming the entities.
23.2. Operation#
The code can be compiled either as a subroutine back_end that is called in
the same program as the front end, or as a main program. In either case,
c_gen_be is called to do the C generation. It begins at il_header and
walks through the intermediate language tree, generating C and writing it to a
file with a .int.c suffix. First, it dumps the source correspondence
information (dump_source_file_correspondence_info) and some other header
code (dump_header_code). Then, for the file scope level, it
- calls
dump_scope_constantsto dump code for constants on the file-scope list. This has marginal utility, but dumps class member constants (an extension) promoted to the file scope by IL lowering. They are put out asenumdeclarations. - calls
dump_scope_typesto dump declarations for all named types – i.e.,typedefs and tags. The tags are output twice: as incomplete declarations on a first pass, and as complete declarations on the second, to avoid forward-reference problems. Local and prototype-scope types are also output here (at the file scope) to avoid some ordering problems. - calls
dump_scope_routinesto dump routine headers (no bodies). - calls
dump_scope_variablestwice. The first time dumps variables without initializers, and tentative declarations for those with initializers. The second time dumps only the initialized variables, this time with initializers. This is to avoid forward-reference problems. - calls
dump_scope_routinesto dump routines with bodies.
When ONE_INSTANTIATION_PER_OBJECT is enabled, each instantiation is put
out as a separate object file. There is still only one IL tree, but it is
marked so that the C-generating back end can sweep through it several times
and put out a different C file (a “slice”) each time, each C file
containing one instantiated entity (function or static data member) plus
exactly the set of other entities needed by that entity. The information
on which entities to include on each sweep is provided by the
per-instantiation “needed” flags, which are a bit vector attached to the
source correspondence entry (see Needed Flags). Each slice is
assigned one bit in that bit vector, and if the bit is 1 the associated
entity is needed in the slice. On beginning the generation of the C file
for a slice, the C-generating back end sets the global variable
needed_flag_bit_number to the bit number for the slice, and then goes
through the normal processing to generate C for everything in the IL tree.
The normal test of the “needed” flag, used to exclude things that aren’t
needed, in this mode tests the proper bit in the bit vector, and therefore
excludes all entities that are not in the slice. In addition, when a
variable or function with a definition is encountered, if the variable or
function is not assigned to the current slice (as indicated by the
instantiation_needed_bit_number field in the variable or routine
entry), the variable or function is put out as a declaration instead of a
definition.
The lower-level routines are as follows:
dump_type_decl generates a type declaration. It calls
dump_enum_definition,dump_struct_union_definition, ordump_typedef_decl.
Types are written out by dump_type and
dump_general_declaration_using_type. They use routines from
il_to_str.c to produce the output for the type. dump_enum_definition
puts out enum definitions, and dump_struct_union_definition puts out
struct/union definitions.
dump_variable_decl generates a variable declaration. For a variable with
an initializer, it calls dump_initializer. There’s some fairly complicated
processing under that routine to turn constant aggregate initialization into
executable code. Among other things, it involves writing initialization
assignments to an auxiliary file while processing declarations, then copying
the initialization code to the right place after all the declarations have been
processed. Because initialized static variables are put out twice (on two
calls of dump_variable_decl), they are made external when generating K&R C,
so their names are modified (by dump_variable_name) to avoid name conflicts
with like-named static and external variables in other compilation units.
Code generated for file-scope initializations (of unions when generating K&R C)
is placed in a separate routine (with a name beginning with “__cgi__”) that
needs to be called at program start-up time. If the compilation contains a
main program, or an initialization routine generated by IL lowering (with a
prefix of “__sti__”), a call to the C-generating back end initialization
routine is inserted there. Otherwise, if the compilation is of C++ source, the
C-generating back end generates an initialization routine of the same form as
that generated by IL lowering (”__sti__” prefix), along with the variables
needed to alert the link phase to the presence of the initialization routine.
If the compilation is of C source, a message is issued to ask the user to
include an appropriate “-i” option elsewhere to request invocation of the
initialization code.
dump_routine_decl generates a routine declaration. For a routine with a
definition, it calls dump_routine_definition. That in turn calls
dump_func_definition_type to dump the parameters (prototyped or old-style).
dump_statement is called to generate code for statements.
The statements and expressions of functions are pre-scanned before code for the routine is generated, to generate declarations for any temporaries that will be needed in the generated code. Specifically, temporaries are generated for rvalue field selections and wide string constants.
Expressions are dumped by calling dump_expr. It outputs the expression by
walking the tree. It puts in explicit parentheses where necessary to guarantee
the grouping of operators. Taking the address of a function or array must also
be handled carefully, and is done by dump_ampersand.
Since pcc doesn’t allow signed bit fields, operations on such fields are
surrounded by references to macros to sign-extend them or truncate them for
storing.
dump_constant dumps out a literal constant or address constant using the
IL-to-string routines in il_to_str.c.
Actual output of generated code is handled by the following routines:
m_write_ch,write_ch,m_write_str,write_str,m_write_tok_ch,write_tok_ch,m_write_tok_str,write_tok_str,write_unsigned_num, andwrite_num.
The “m_” versions are macros; the others are not. One can choose between
them based on the need for speed and/or a single evaluation of the argument.
The “tok_” routines always write a full token (or several); that allows the
output line to be broken before or after the output if the line is too long.
set_output_position is called to establish the source position of an entity
about to be generated. If necessary, it generates a #line directive or the
right number of empty lines to adjust the output position (see
write_line_directive).