Preprocessor - A 2 Part Tutorial

Part II

© 2006, Rich Ries

author contact:

Rich.Ries@Honeywell.com

Home

Tip Corner: ByRef

API Corner: File Download

Working with Strings 3

Stylebits Corner: Dialogs

Eddie's Lessons, v.10

Liberty Basic Wiki

Preprocessor 1

Preprocessor 2

Find Folder

Multiple Listboxes

Newsletter help

Index


As promised in the previous part, B-Prep will start off simple, and get more complex as time goes on. The time for complexity has arrived.

File: BPREP-4.BAS

As first designed, the #if-#else-#endif preprocessor command set does not allow nesting. That is,

#if A
        Some Code
  #if B
        More Code
  #else
    Different Code
  #endif
#endif

is an illegal construct. If A is undefined, then the preprocessor will gobble lines until it hits "#else", at which point it will stop gobbling. If A is defined, then the preprocessor will work as expected. One possible way to solve this problem is to keep track of which #endif/#else goes with which #if:

#if A
        Some Code
  #if B
        More Code
  #else B
    Different Code
  #endif B
#endif A

This would enforce a better style. A lot of my time is spent looking at others' code, and trying to figure out which #endif goes with which #if. A better way is to run Analyze on the code blocks. As it stands, Analyze works on one line of code, and returns to its caller. The caller loops, reads in the next line of code, and calls Analyze. Rather than adding code for this to each command that needs it, we'll convert the caller into a subroutine, and call it whenever we get a block of code.

Go to the [Top] label, and remove it and the line "goto [Top]". Move the line "call GetNextLine ' Loads RawData$" to the start of Analyze. Now we need to add a loop to Analyze. You could use the "[Top]"/"goto [Top]", but I got a little fancier, and used "DO ... LOOP WHILE TRUE"

'---------------------------------------------------
sub Analyze

        do
                call GetNextLine ' Loads RawData$
                if instr(RawData$,"#") <> 0 then
                        ' Trim any leading spaces
                        PPCmdStr$ = trim$(RawData$)
                        ' make sure the # is first in the line
                        if left$(PPCmdStr$,1) = "#" then
                                ' Get the command
                                PPCmd$ = word$(PPCmdStr$,1)
                                ' Trim it
                                PPCmdStr$ = trim$(mid$(PPCmdStr$,len(PPCmd$)+1))
                                select case PPCmd$
                                        case "#define"                                  
                                                call DoDefine PPCmdStr$
                                        case "#undefine"                                        
                                                call DoUndefine PPCmdStr$
                                        case "#ifdef"
                                                call DoIfDef PPCmdStr$
                                        case "#else"
                                                call DoElse
                        exit do ' Get out of the #IFDEF Analyze
                                        case "#endif"
                                                call DoEndIf
                        exit do ' Get out of the #IFDEF Analyze
' For Testing
                                        case "#end"
                                                exit do
                                                
                                end select
                        end if
                else
                        call write RawData$
                end if
        loop while TRUE
end sub

Next, we'll need to adjust the "#if" command, and modify Analyze. "#endif" does nothing, and we want to keep it that way. "#else" gulps lines, and we want to keep that, too.

DO loop at its heart is a miniature Analyzer. This means that we can replace the mini-Analyzer with a call to the real one, and add a "gulper" in the case that the #if is undefined (in which case we want to ignore all lines until #else or #endif are found). If #else is found, we want to start analyzing the block, so we add a call to Analyze in the "#else" portion:
'---------------------------------------------------
sub DoIfDef TheName$
        Du = IsDefined(TheName$)
        if Du <> 0 then
                ' It's defined
                call Analyze
        else ' Gulp until we find "#else" or "#endif"
                do
                        call GetNextLine
                        Du$ = word$(RawData$,1)
                        if Du$ = "#else" then
                                call Analyze
                                exit do
                        end if
                loop until Du$ = "#endif"
        
        end if
end sub

Add the code, and test it out. You should now be able to #define a variable within a #if block, something you could not do previously.

File: BPREP-5.BAS

The next command will be "#include". To start this, we'll need to modify the initialization subroutine as well as the input and output subroutines and the error handler. We'll also need a test program or two. We'll start with the testers:

' File Test5-1.bas

FOR n = 1 TO 10
        PRINT "Who are you?"
NEXT n

There's no #include command yet -- we're just seeing how well we can read & write a single file.

' File: Test5-2.bas

PRINT "In Test1.mod"

Now, we'll change InitializeSystem by adding an OPEN command. File handles are naturally Global, so that saves a little work. We won't do any writes to a file yet -- we want to see where we're going.

'---------------------------------------------------
sub InitializeSystem
        FILEDIALOG "File to Process", "*.*", FilePath$
        InHandle$ = "1"
        open FilePath$ for input as #InHandle$
        ' Get the File Path
        for n = len(FilePath$) to 1 step -1
                Du$ = mid$(FilePath$,n,1)
                if Du$ = "\" then
                        Path$ = left$(FilePath$,n)
                        exit for
                end if
        next n
    call write "' BASIC Preprocessor"
    call write "'"
end sub

'---------------------------------------------------
sub DoError ErrMsg$
        call write ErrMsg$
        close #InHandle$
        input A$
        end
end sub

'+++

We'll have to modify GetNextLine to read from files. So far, we're just opening and closing one file, so we don't need to get too fancy; however, note that I'm changing GetNextLine from a SUB to a FUNCTION, to flag when we've hit the end of the file.

'---------------------------------------------------
function GetNextLine()
' Eats up blank lines
    do
[CheckEOF]      
                if eof(#InHandle$) <> 0 then
                        if FHAIndex > 0 then
                                close #InHandle$
                                InHandle$ = FileHandleArray$(FHAIndex)
                                FHAIndex = FHAIndex - 1
                                goto [CheckEOF]
                        else ' FHAIndex
                                close #InHandle$
                                RawData$ = ""
                                GetNextLine = FALSE
                                exit do
                        end if ' FHAIndex
                end if ' eof

                line input #InHandle$, RawData$
                        
                Du = instr(RawData$, COMMENT$)
                if Du <> 0 Then
                        RawData$ = left$(RawData$,Du-1)
                end if
                RawData$ = RTrim$(RawData$)
                GetNextLine = TRUE
        loop while RawData$ = ""
end function

Run the code. You should see the File Dialog come up. Select "TEST.BAS", and you'll see the contents of TEST1.BAS getting printed on the console. Now, we'll need to add the "include" command to Analyze, and a subroutine for it, along with a function to test if the file exists.

'---------------------------------------------------
sub DoInclude TheFileName$
        Du = instr(TheFileName$,chr$(34))
        
        ' Test for quotes
        if Du = 0 then 
                call DoError "Include filenames must have "+chr$(34)+"s"
        end if
        File$ = mid$(TheFileName$,Du+1)
        Du = instr(File$,chr$(34))
        
        ' Test for quotes
        if Du = 0 then 
                call DoError "Include filenames must have "+chr$(34)+"s"
        end if
        File$ = left$(File$,Du-1)

        if File$ = "" then
                call DoError "Include must have a filename."
        else
                ' Load InHandle$ into next entry of array
                FHAIndex = FHAIndex + 1
                if FHAIndex > MAXFILES then
                        call DoError "Maximum number of open files exceeded."
                end if
                FileHandleArray$(FHAIndex) = InHandle$
                ' Iniz file handle
                InHandle$ = "1"
                ' Add current path if none is shown
                if instr(File$,"/") = 0 then
                        FilePath$ = Path$+File$
                end if
                ' Open the file
                if FileExists(File$) then
                        open FilePath$ for input as #InHandle$
                else
                        call DoError File$+" does not exist in "+Path$
                end if
        end if
end sub

'---------------------------------------------------
function FileExists(File$)
        files Path$, File$, FileInfo$()
        FileExists = val(FileInfo$(0, 0))  'non zero is true
end function

Note that there is a lot of testing going on in the function. I'd rather have the software tell me why it failed rather than the OS error message, which is usually less than helpful! Also, I've limited the maximum number of files that can be open simultaneously to 10, which should be more than enough. If not, just change MAXFILES.

File: BPREP-6.BAS

We'll wind this up with modifying the way the outputs are written. Up to this point, all output has gone to the main window. This is OK for testing, but for serious work we need to put the preprocessor output to a file, and use some popup windows for the error messages.

First, we'll turn off the main window, and add a global name for the output handle. Call it "OutHandle"!

NOMAINWIN
'====== GLOBALS =======
global TRUE, FALSE, TRUE$, FALSE$
global COMMENT$, TAB$

FALSE = 0
TRUE = -1
FALSE$ = "0"
TRUE$ = "-1"
COMMENT$ = "'"
TAB$ = CHR$(9)

global RawData$

' Definitions
global MAXDEFS, Definition$, NextDef
MAXDEFS = 1000
DIM Definition$(MAXDEFS)
NextDef = 1

' Include
' Most files open at one time
global MAXFILES
' Working file handle
global InHandle$
' Fully-qualified file name
global FilePath$
' Path (from FilePath$)
global Path$

MAXFILES = 10
' Holds open handles
DIM FileHandleArray$(MAXFILES)
' Points to current FileHandleArray$ entry
global FHAIndex
FHAIndex = 0
' Used to see if file exists
dim FileInfo$(10, 10)

Then we'll need to add a FILEDIALOG to InitializeSystem to get the output file name, and close the handle in GetNextLine().

'---------------------------------------------------
sub InitializeSystem
        FILEDIALOG "File to Process", "*.*", FilePath$
        InHandle$ = "1"
        open FilePath$ for input as #InHandle$
        ' Get the File Path
        for n = len(FilePath$) to 1 step -1
                Du$ = mid$(FilePath$,n,1)
                if Du$ = "\" then
                        Path$ = left$(FilePath$,n)
                        exit for
                end if
        next n

        FILEDIALOG "File to Write to", "*.*", OFilePath$
        open OFilePath$ for output as #OutHandle

    call write "' BASIC Preprocessor"
    call write "'"
end sub

'---------------------------------------------------
function GetNextLine()
' Eats up blank lines
    do
[CheckEOF]      
                if eof(#InHandle$) <> 0 then
                        if FHAIndex > 0 then
                                close #InHandle$
                                InHandle$ = FileHandleArray$(FHAIndex)
                                FHAIndex = FHAIndex - 1
                                goto [CheckEOF]
                        else ' FHAIndex
                                close #InHandle$
                                close #OutHandle
                                RawData$ = ""
                                GetNextLine = FALSE
                                exit do
                        end if ' FHAIndex
                end if ' eof

                line input #InHandle$, RawData$
                        
                Du = instr(RawData$, COMMENT$)
                if Du <> 0 Then
                        RawData$ = left$(RawData$,Du-1)
                end if
                RawData$ = RTrim$(RawData$)
                GetNextLine = TRUE
        loop while RawData$ = ""
end function

"write" will need to be changed to send output to the output file handle:

'---------------------------------------------------
sub write aString$
    print #OutHandle, aString$
end sub
and we can remove the "#end" case from Analyze.
'---------------------------------------------------
sub Analyze
        if GetNextLine() = TRUE then ' Loads RawData$
                do              
                        if instr(RawData$,"#") <> 0 then
                                ' Trim any leading spaces
                                PPCmdStr$ = trim$(RawData$)
                                ' make sure the # is first in the line
                                if left$(PPCmdStr$,1) = "#" then
                                        ' Get the command
                                        PPCmd$ = word$(PPCmdStr$,1)
                                        ' Trim it
                                        PPCmdStr$ = trim$(mid$(PPCmdStr$,len(PPCmd$)+1))
                                        select case PPCmd$
                                                case "#define"                                  
                                                        call DoDefine PPCmdStr$
                                                case "#undefine"                                        
                                                        call DoUndefine PPCmdStr$
                                                case "#ifdef"
                                                        call DoIfDef PPCmdStr$
                                                case "#else"
                                                        call DoElse
                            exit do ' Get out of the #IFDEF Analyze
                                                case "#endif"
                                                        call DoEndIf
                            exit do ' Get out of the #IFDEF Analyze
                                                case "#include"
                                                        call DoInclude PPCmdStr$
                                        end select
                                end if
                        else
                                call write RawData$
                        end if
                loop while GetNextLine() = TRUE
        end if
end sub

Finally, we'll send the error messages to a notice window, and add a completion notice to the end.

'---------------------------------------------------
sub DoError ErrMsg$
        notice "Fatal Error!" + chr$(13) + ErrMsg$
        close #InHandle$
        close #OutHandle
        end
end sub

'+++

        notice "Preprocessing is Complete."
end

Once this is done, we can try running tests. The test files are Test6-1.bas through Test6-4.bas. Double-click on Test6-1.bas to select it for input, then enter a filename for the results (output) file. I use "Du.bas" -- a carry-over from math class when Du was a dummy variable.

Once the preprocessing is finished, check Du.bas to make sure that what you've expected to happen has indeed happened.

Enjoy!