David is a member of the X3J3 Fortran committee and one of the creators of F, a new language for educational and professional programming. He can be reached at [email protected].
In C, conditional compilation is a preprocessor feature that provides text substitution, macro expansion, and file inclusion. While often useful, these features can lead to a number of problems, which contributed to their being replaced or augmented in C++ (examples are const, inline functions, and type-safe linkage).
Fortunately, modern Fortran provides safe replacements for these C++ features. Fortran's PARAMETER provides named constants, PURE or internal procedures offer the speed benefits of C's macro functions without the risks, and Fortran's MODULE provides a safe way for one file to use facilities defined in another. All that's missing is the ability to choose which parts of the program will be compiled.
The Fortran 90 Conditional Compilation Facility (CCF) provides this missing functionality. CCF is a line-based language that can easily be adapted to other programming languages. CCF also is easy to implement; Listing One gives a simple Fortran CCF processor written in under 200 standard Fortran statements.
Although Listing One implements CCF as a preprocessor, this isn't mandatory. In fact, the first implementation of CCF handled conditional compilation as an integral part of the compiler. The benefits of this approach are improved speed and consistent error messages. This kind of full CCF processor would use a scanner, parser, symbol table, and expression analyzer, and most likely would not be free or readily available on new hardware.
CCF for Fortran
CCF is based on a subset of regular Fortran, which makes it easy to learn, implement, and adapt to other languages. To define a CCF for your favorite programming language, just choose a subset of that language that you'd like to use for conditional compilation, and a way to distinguish CCF statements from the regular program statements. With this in hand, you can write a simple CCF processor using the approach in Listing One.
To keep the Fortran CCF simple, I included only the minimum subset of Fortran that supports conditional compilation. This required at least two features: an IF statement, to support the "conditional" aspect, and variables, to control the conditionals. Combining these minimal requirements with a few usability statements yields the nine statements in Table 1.
The difference between initializing a CCF variable with a value in the INTEGER or LOGICAL statements and assigning a value to a CCF variable with the assignment statement is that the assignment statement value cannot be overridden with an optional invocation value.
The aforementioned nine statements are distinguished in the source code by the characters !CCF$ in columns one to five of a line. Since ! starts a comment in Fortran, all CCF statements look like comments to a Fortran compiler.
CCF Processor Output
The CCF processor reads the source file, interprets the CCF commands, and produces one of three different output formats: short, big replace, and big shift.
The CCF short output contains only the lines that will actually be compiled. All CCF statements and all lines that are in the FALSE branches of the CCF IF constructs are deleted. The advantage of this file format is that it's easy to produce; it's the only format supported by the preprocessor in Listing One.
The CCF big replace output contains the same number of lines as the input file. The only difference between the big-replace file and the input file are the lines in the FALSE branch(es) of the CCF IF constructs. These lines are commented out by placing the characters !CCF* in columns one to five. Because it has the same number of lines, it's easy to match compiler warnings against the original file. Also, no lines become longer. However, some of the original file might be lost if the lines being commented out already had something in the first five columns.
The CCF big shift is similar, but the lines in the FALSE branches are shifted to the right five characters, and the string !CCF> is placed in columns one to five of this new line. Like the big-replace file, the big-shift file makes it easy to match compiler warnings to the original source. Unlike the big-replace output, however, big-shift files preserve all of the original program text.
The big-shift and big-replace files make it possible to simply discard the original file after processing. The processed file can be used as subsequent input to the CCF processor, which simply recognizes lines beginning with !CCF* or !CCF> as lines commented by a previous run through CCF. For instance, Example 1(a) has already been run through a CCF processor to produce code for DOS. Without any additional editing, it can be reprocessed to produce UNIX code, as in Example 1(b).
Having only one file helps avoid problems with accidentally changing the wrong file. With the short output, it may be tempting to make temporary changes to the processed file, which can cause confusion if these changes don't get carried into the original source.
CCF Processor-Invocation Options
The CCF processor must know two pieces of information when it is invoked: which CCF variables to override, and what type of output file to produce. The simple CCF processor in Listing One accepts these options from the keyboard or a batch file.
A Simple CCF Processor
Because CCF statements are a subset of the statements in the target language, it's relatively easy to use the compiler for your target language to help process CCF input. Listing One implements a CCF processor by creating a new program that, when compiled and run, outputs the final program. This involves simply uncommenting the CCF lines and replacing all other lines with corresponding PRINT statements. Example 2(a) is sample input to this algorithm, and the corresponding output is Example 2(b). When Example 2(b) is compiled and run, it will output Example 2(c)--the final, processed output. This simple processor doesn't support either the big-shift or big-replace formats. (For the more complete CCF_95 processor, contact [email protected].)
Other Uses of Conditional Compilation
Although portability is the most popular use of conditional compilation, it can simplify many other tasks as well. Conditional compilation can be used to selectively enable debugging code. This allows the program to be compiled with different levels of debugging enabled, ranging from simple Entering FUNCTION Foo comments to more extensive checks on complex processes. Example 3 shows how this might be used. To avoid using the processor on every compile, you can place !CCF> and !CCF* comments manually.
Because the CCF processor completely ignores all non-CCF lines, you can place comments or other text in the FALSE branch of a CCF condition. You can even maintain several different kinds of source code in a single file, such as the Fortran source and a batch file; suitable CCF options will extract either one.
I've used CCF to test Fortran 90 expression parsers by including CCF statements to select parts of a large test file. This made it easy to narrow down a failure to a specific part of the input without manually editing the file to identify the problem line.
The Future of CCF
CCF has been submitted to X3J3 as a possible addition to the Fortran standard. The most recent summary (available from ftp://ftp.ncsa.uiuc.edu/x3j3) lists CCF as a "high priority'' for the next update. Additional information is available from http://www.fortran.com/fortran/market.html.
Acknowledgments
Many have contributed to the development of CCF, including Karen Barney, John Ehrman, Dick Weaver, Bruce Pumplin, Mike Dedina, Mark Epstein, Kelly Flanagan, and Harris Hall. The use of conditional compilation for debugging was introduced to me by Don Rose. Support for the standardization of CCF has come from many X3J3 members, but particularly from Jeanne Martin.
References
Epstein, David. "Imagine A Conditional Compilation Facility (CCF)." Fortran Journal (May/June 1994).
------. "CCF Here and Now." Fortran Journal (September/October 1994).
------. "CCF Is Unlike The Others." Fortran Journal (January/February 1995).
Table 1: CCF statements.
Statement Description INTEGER Declare and initialize an integer CCF variable. LOGICAL Declare and initialize a logical CCF variable. IF Classical conditional statement. ELSEIF Part of the IF construct. ELSE Part of the IF construct. ENDIF Part of the IF construct. assignment Assign a value to a CCF variable. PRINT Output to the screen during CCF processing. STOP Halt during CCF processing.
Example 1: (a) File processed for DOS; (b) file processed for UNIX.
(a) !CCF$ IF (system == DOS) THEN filename = '\back\slash\eightdot.3' !CCF$ ELSEIF (system == UNIX) THEN !CCF* filename = '/forward/slash/manydot.many' !CCF$ ELSE !CCF$ STOP 'Set CCF variable "system" to DOS or UNIX' !CCF$ ENDIF (b) !CCF$ IF (system == DOS) THEN !CCF* filename = '\back\slash\eightdot.3' !CCF$ ELSEIF (system == UNIX) THEN filename = '/forward/slash/manydot.many' !CCF$ ELSE !CCF$ STOP 'Set CCF var "system" to DOS or UNIX' !CCF$ ENDIF
Example 2: (a) Original file; (b) intermediate file; (c) output file.
(a) !CCF$ INTEGER :: version_num = 2 !CCF$ IF (version_num == 2) THEN max = 1024 !CCF$ ELSE max = 512 !CCF$ ENDIF (b) INTEGER :: version_num = 2 IF (version_num == 2) THEN PRINT *,' max = 1024' ELSE PRINT *,' max = 512' ENDIF (c) max = 1024
Example 3: Using CCF for debugging.
!CCF$ IF (debug_level > 0) THEN !CCF> PRINT *, 'Entering FUNCTION SnickersBar' !CCF$ IF (debug_level > 1) THEN !CCF> PRINT *, 'with argument "foo" = ',foo !CCF> PRINT *, ' and argument "moo" = ',moo !CCF$ ENDIF ! (debug_level > 1) THEN !CCF$ ENDIF ! (debug_level > 0) THEN
Listing One
!****************************************************************************** ! Simple CCF Processor ! This code is free. You are also free (do whatever you want with this code). ! David Epstein and imagine1 appreciates any donations (comments, ! coding suggestions or checks made payable to imagine1). ! e-mail : [email protected] ! telephone : +1-503-383-4846 ! address : imagine1 ! PO Box 250 ! Sweet Home, OR 97386 ! BASIC IDEA ! Since the Conditional Compilation Facility (CCF) is a subset of Fortran, ! a simple CCF processor is achieved by using a Fortran processor to ! execute the CCF statements. ! STEPS ! 1. Compile this CCF processor with your Fortran processor. ! You now have a CCF processor. ! 2. Invoke this CCF processor and follow the prompts for required input. ! 3. Compile the ccf-temp file (ccftemp.f90 for dos and unix systems). ! 4. Execute the ccf-temp file to create the resulting output file. ! ALGORITHM ! Turn !ccf$ lines into Fortran lines by replacing !ccf$ with blanks. ! Turn all other lines into output statements (include handling of !ccf* ! and !ccf> lines as lines that CCF previously turned into comments). ! Special handling of CCF variables is required due to Fortran requirement ! that the specification-part precedes the execution-part. All CCF variable ! declaration lines are buffered until written to a MODULE. This MODULE ! is also used to handle initialization of CCF variables when the user ! wants to override initial values in the source. ! DESCRIPTION OF OUTPUT ! This simple CCF processor creates a file that consists only of the lines ! that the Fortran processor would see. In other words, all CCF lines ! (those with !ccf$ in columns 1 to 5) will not appear in the created file ! and all lines that are in a FALSE branch of a CCF if-construct will not ! appear in the created file. ! DIFFERENCES BETWEEN THIS CCF AND FULL CCF ! This simple CCF processor differs from the CCF in your Fortran processor ! in a few ways: ! 1) Two files must be maintained because this CCF processor does not ! give the option of creating a file with the same number of lines ! as the input file. A full CCF processor can comment lines in the ! the FALSE branch of a CCF if-construct with a !ccf* or !ccf>. This ! CCF processor will not include CCF lines or the lines in a FALSE ! branch of a CCF if-construct. ! 2) This CCF processor creates two temp files that must be compiled and ! run in order to create the CCF output file. One of these temp files ! is INCLUDEd in the other temp file. A full CCF processor does not ! require creating extra files. ! 3) The CCF PRINT and STOP statements are executed at compile time in ! the CCF in your Fortran processor. In this CCF processor the CCF ! PRINT and STOP statements are executed when the ccf temp file is ! executed. This slightly hinders the usability of these CCF stmts. ! 4) There is a limit on the number of CCF variable declaration lines ! (you may change this limit by changing MAX_CCF_VAR_DECL_LINES). ! 5) Fortran keywords INTEGER and LOGICAL are reserved words for this ! CCF processor (do not name a CCF variable "integer" or "logical"). ! 6) Diagnostic checks are not made on the CCF statements. Please stick ! to the CCF language definition (CCF variables do not have "kinds", ! CCF statements do not have labels, use free source form for the ! CCF lines (even if your source is fixed source form), etc.). !****************************************************************************** program SimpleCcf implicit NONE ! for buffering CCF variable declarations so they can be written to a MODULE integer, parameter :: MAX_CCF_VAR_DECL_LINES = 100 character (len = 132) :: ccf_var_lines(MAX_CCF_VAR_DECL_LINES) integer :: num_ccf_var_decl_lines = 0 ! for storing lines of input character (len = 132) :: line ! file names (limit of 32 is a random selection and can be changed) character (len = 132) :: in_file, & ! Input file to be CCFed out_file ! Output file excludes CCF lines and ! lines inside FALSE branches character (len = 132) :: temp_file1, & ! Middle-step file MAIN temp_file2, & ! Middle-step file MODULE batch_file ! Option to run CCF batch mode ! batch mode for input (input and output filenames and initial values) logical :: batch ! TRUE if batch_file exists !CCF$ integer :: dos = 1 !CCF$ integer :: unix = 2 !CCF$ integer :: your_system = 3 ! Edit here for system other than dos or unix !CCF$ integer :: system = 1 ! set filenames !CCF$ if (system == dos .OR. system == unix) then temp_file1 = "ccftemp.f90" temp_file2 = "ccfmod.f90" batch_file = "ccfbatch.dat" !CCF$ else ! system other than dos or unix !CCF$ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !CCF$ stop 'Please edit here and above for system other than dos or unix' !CCF$ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !CCF> temp_file1 = "ccftemp filename for your system" !CCF> temp_file2 = "ccfmod filename for your system" !CCF> batch_file = "ccfbatch filename for your system" !CCF$ endif inquire (FILE=batch_file, EXIST=batch) if (batch) then open (UNIT=7, ACTION="READ", & STATUS="OLD", POSITION="REWIND", ERR=101, FILE=batch_file) read (UNIT=7, FMT="(A)", END=102) in_file read (UNIT=7, FMT="(A)", END=102) out_file else print *, "Enter the name of the file to be CCFed:" read *, in_file print *, "Enter the name you want for the output file:" read *, out_file endif open (UNIT=8, ACTION="READ", & STATUS="OLD", POSITION="REWIND", ERR=103, FILE=in_file) open (UNIT=9, ACTION="WRITE", & STATUS="REPLACE", POSITION="REWIND", ERR=104, FILE=temp_file1) call WriteLine(9, "INCLUDE '" // TRIM(temp_file2) // "'" ) call WriteLine(9, "PROGRAM CcfIt" ) call WriteLine(9, "USE CcfVars ! Module with CCF vars and init values" ) call WriteLine(9, "implicit NONE" ) call WriteLine(9, " " ) call WriteLine(9, "CALL mCcfInits ! Init CCF values" ) call WriteLine(9, 'open (UNIT=9, ACTION="WRITE", STATUS="REPLACE", ERR=7,&') call WriteLine(9, ' POSITION="REWIND", FILE="' //TRIM(out_file)// '")') !**************************************************************************! !** Read each line of the input file looking for **! !** 1) CCF lines **! !** 2) lines previously commented out by CCF in a Fortran processor **! !** 3) other lines **! !** 1) CCF lines - **! !** a) CCF variable declarations are buffered until written to **! !** the CCF module **! !** b) all other CCF lines are turned into Fortran lines by **! !** replacing the !ccf$ in columns 1-5 with blanks **! !** 2) lines previously commented out by CCF in a Fortran processor - **! !** a) !ccf* in columns 1-5 - Replaced columns 1-5 with blanks **! !** b) !ccf> in columns 1-5 - Shift the line left 5 columns **! !** Double all double quotes (") as in 3) **! !** 3) other lines - **! !** Other lines are simply echoed to the output file. Doing **! !** this requires doubling all the double quotes ("). **! !**************************************************************************! do ! until end of file read (UNIT=8, FMT="(A)", END=50) line ! !ccf$ - a ccf line if ((line(1:5) == '!ccf$') .OR. (line(1:5) == '!CCF$')) then ! if a CCF var declaration, save this line for the CcfVar MODULE ! else replace the !ccf$ with blanks (turn into a Fortran statement) if (CcfVarDeclaration()) then num_ccf_var_decl_lines = num_ccf_var_decl_lines + 1 if (num_ccf_var_decl_lines > MAX_CCF_VAR_DECL_LINES) then print *, 'CCF halts. Limit of CCF variable declaration lines' print *, MAX_CCF_VAR_DECL_LINES,'exceeded on line:' print *, line stop endif line(1:5) = ' ' ! replace !ccf$ with blanks ccf_var_lines(num_ccf_var_decl_lines) = line else line(1:5) = ' ' ! replace !ccf$ with blanks call WriteLine(9, line) endif else ! not a CCF line ! !ccf* - a non-ccf line, commented by ccf by replacing columns 1 to 5 ! !ccf> - a non-ccf line, commented by ccf by shifting the line ! right 5 columns and placing !ccf> in columns 1-5 if ((line(1:5) == '!ccf*') .OR. (line(1:5) == '!CCF*')) then line(1:5) = ' ' ! blank out "!ccf$" elseif ((line(1:5) == '!ccf>') .OR. (line(1:5) == '!CCF>')) then line = line(6:) ! shift left 5 char else ! No !ccf* or !ccf> to blank out or shift. endif ! Double each double quote (") and output the new line call HandleDoubleQuotesThenWriteLine() endif enddo 50 continue ! end of input file call WriteLine (9, "goto 8 ! We are done." ) call WriteLine (9, "7 print *, 'Trying to open CCF output file with the&") call WriteLine (9, "& name you supplied >" // TRIM(out_file) // "'" ) call WriteLine (9, "stop 'CCF Error: opening this output file.'" ) call WriteLine (9, "8 continue" ) call WriteLine (9, "END") call WriteLine (9, " ") call WriteLine (9, "subroutine WriteLine(unit_num, line) ") call WriteLine (9, " integer :: unit_num ") call WriteLine (9, " character (len=*) :: line ") call WriteLine (9, " ") call WriteLine (9, ' write (UNIT=unit_num, FMT="(A)") TRIM(line) ') call WriteLine (9, "end subroutine WriteLine ") ! Write the MODULE file which contains all the CCF variable declarations ! and any initial values. call WriteCcfVarsModule() ! Reminder of middle-step if (.NOT. batch) then print * print *, "CCF is creating ", TRIM(temp_file1), & " as the temporary file to compile and run" endif ! Close files and we are done close (UNIT=7, STATUS="KEEP", ERR=105) close (UNIT=8, STATUS="KEEP", ERR=106) close (UNIT=9, STATUS="KEEP", ERR=107) goto 110 !**** ERROR messages ******************************************************! 101 print *, 'Trying to open CCF batch file "', TRIM(batch_file), '"' stop 'CCF Error: opening this input file.' 102 print *, 'Trying to read a line from CCF Batch file "', & TRIM(batch_file), '"' stop 'CCF Error: Batch file expecting input and output filenames' 103 print *, 'Trying to open CCF input file "', TRIM(in_file), '"' stop 'CCF Error: opening this input file.' 104 print *, 'Trying to open CCF temp file "', TRIM(temp_file1), '"' stop 'CCF Error: opening this input/output file.' 105 print *, 'Trying to close CCF batch file "', TRIM(batch_file), '"' stop 'CCF Error: closing this input file.' 106 print *, 'Trying to close CCF input file "', TRIM(in_file), '"' stop 'CCF Error: closing this input file.' 107 print *, 'Trying to close CCF temp file "', TRIM(temp_file1), '"' stop 'CCF Error: closing this input/output file.' ! end ERROR messages ******************************************************! 110 continue ! no I/O errors. We are done. contains !****************************************************************************** subroutine HandleDoubleQuotesThenWriteLine() ! Echo a non-CCF line to the middle-step temp file. This is done by turning ! the line into a character constant. Turning a line into a character constant ! requires placing it in between two double quotes (") and doubling each ! occurrence of a double quote. The new line is then passed to a WriteLine ! subroutine. Note that the original line could be 132 double quotes. character (len = 264) :: did_quotes_line ! line after doubling the " integer :: new_len ! line len after doubling the " character (len = 285) :: new_line ! 285 handles a line of 132 "s as: ! 1-19 : call WriteLine(9, " ! 20-283: 264 "s (132 "s doubled) ! 284-285: ") integer :: pos, & ! pos in original line new_pos ! position in did_quotes_line did_quotes_line = ' ' new_pos = 1 ! Double each occurrence of a double quote ("). do pos = 1, LEN_TRIM(line) if (line(pos:pos) == '"') then did_quotes_line(new_pos:new_pos+1) = '""' new_pos = new_pos + 2 else did_quotes_line(new_pos:new_pos) = line(pos:pos) new_pos = new_pos + 1 endif enddo new_len = new_pos - 1 new_line(1:19) = 'call WriteLine(9, "' new_line(20:20+new_len -1) = did_quotes_line(1:new_len) new_line(20+new_len:20+new_len+1) = '")' ! Set new_len to the length of the Fortran statement that may need splitting new_len = 20+new_len+1 if (new_len <=132) then call WriteLine(9, new_line(1:new_len)) elseif (new_len <= 262) then ! need to split line once call WriteLine(9, new_line(1:131)//"&") call WriteLine(9, "&"//new_line(132:new_len)) else ! need to split line twice call WriteLine(9, new_line(1:131)//"&") call WriteLine(9, "&"//new_line(132:261)//"&") call WriteLine(9, "&"//new_line(262:new_len)) endif end subroutine HandleDoubleQuotesThenWriteLine !****************************************************************************** function CcfVarDeclaration() ! Determine if the current CCF line ("line") is a CCF variable declaration ! line. This simple CCF processor expects free source form, keywords are ! reserved words, and there are no "kinds" on these variables. logical :: CcfVarDeclaration ! TRUE if CCF INTEGER or ! CCF LOGICAL statement integer :: pos ! pos in line ! skip over the !ccf$ and the blanks pos = 6 do while (line(pos:pos) == ' ') pos = pos + 1 end do ! This Simple CCF processor expects a blank or a ':' after ! INTEGER or LOGICAL. Note that INTEGER and LOGICAL are reserved words. if (((line(pos:pos+6) == 'INTEGER') .OR. & (line(pos:pos+6) == 'LOGICAL') .OR. & (line(pos:pos+6) == 'integer') .OR. & (line(pos:pos+6) == 'logical')) & .AND. & ((line(pos+7:pos+7) == ' ') .OR. & (line(pos+7:pos+7) == ':'))) THEN CcfVarDeclaration = .TRUE. else CcfVarDeclaration = .FALSE. endif end function CcfVarDeclaration !****************************************************************************** subroutine WriteCcfVarsModule() ! Write the CcfVars MODULE. CcfVars contains all the CCF variable ! declarations and a subroutine called mCcfInits. mCcfInits contains ! assignments of any initial values for CCF variables supplied either ! in the CCF batch file or from standard input. integer :: i ! counter for loop character (len = 32) :: init_var, & ! CCF variable to be initialized init_val ! intial value for CCF variable open (UNIT=10, ACTION="WRITE", STATUS="REPLACE", & POSITION="REWIND", ERR=201, FILE=temp_file2) call WriteLine(10, "MODULE CcfVars") call WriteLine(10, "implicit NONE") do i = 1, num_ccf_var_decl_lines call WriteLine(10, ccf_var_lines(i)) enddo call WriteLine(10, "CONTAINS") call WriteLine(10, "SUBROUTINE mCcfInits") init_var = "foo" ! any foo other than '0' since '0' terminates loop do while (init_var /= '0') if (batch) then read (UNIT=7, FMT="(A)", END=202) init_var if (init_var(1:1) /= '0') then read (UNIT=7, FMT="(A)", END=202) init_val line = init_var // "=" // init_val call WriteLine(10, line) endif else print *, "Enter the name of a CCF variable to be initialized" print *, "(or '0' when you are done initializing):" read *, init_var if (init_var /= '0') then print *, "Enter the value you want for this CCF variable: " read *, init_val line = init_var // "=" // init_val call WriteLine(10, line) endif endif enddo call WriteLine(10, "END SUBROUTINE mCcfInits") call WriteLine(10, "END MODULE CcfVars") close (UNIT=10, STATUS="KEEP", ERR=203) goto 210 !**** ERROR messages ******************************************************! 201 print *, 'Trying to open CcfVars Module file "', TRIM(temp_file2), '"' stop 'CCF Error: opening this output file.' 202 print *, 'Trying to read a line from CCF Batch file "', & TRIM(batch_file), '"' stop 'CCF Error: Batch file expecting initial var-val pairs or 0' 203 print *, 'Trying to close CcfVars Module file "', TRIM(temp_file2), '"' stop 'CCF Error: closing this output file.' ! end ERROR messages ******************************************************! 210 continue ! no I/O errors end subroutine WriteCcfVarsModule end program !****************************************************************************** subroutine WriteLine(unit_num, line) implicit NONE integer :: unit_num character (len=*) :: line write (UNIT=unit_num, FMT="(A)") TRIM(line) end subroutine WriteLine