Thursday | 10 OCT 2024
[ previous ]
[ next ]

PRECOMPILE in BASIC

Title:
Date: 2024-09-18
Tags:  basic

Pick files have two parts to them, you have the DATA section which as the name suggests holds the data. You then have the DICT section which is the dictionary for that data.

One core idea of the system is that you have a single A record, an attribute record, in the dictionary for each piece of data you want to hold. You can have as many S type definitions but there should just be one A type. However this idea is not enforced anywhere in the system and relies on the programmers being diligent.

For example if you have a STUDENT-FILE, a record would represent a student. A field or also known as an attribute will then be one piece information about that student. For example we may keep track of the gender of the student on attribute 4.

We should then create a dictionary item called GENDER that is an A record. We can also create an S record for 4 so that listing the record and the number 4 prints the attribute 4. We can also create a dictionary called SEX that is an S type.

This keeps the dictionary clean and a byproduct of this design is that we can then use dictionary for variable names when we want to read from the STUDENT-FILE.

When we write a program and open the STUDENT-FILE, instead of using the attribute numbers directly, it would be much better to use the attribute name.

   PRINT STUDENT.ITEM(4)

versus

   PRINT STUDENT.ITEM(STUDENT.GENDER.ATTRIBUTE)

By using variable names, we don't have to keep looking up what the numbers mean and can parse the program much more easily.

We have the dictionary file and we have a single A record for each piece of data. With this information we could then use the dictionary names directly in our program and this would make our programs much easier to read.

Unfortunately there is no native way to do this and so we will need to do something ourselves. This is where precompiling comes in. We can write a program that when it sees a file being opened, it will automatically set up the variables for each of the attribute names.

Something like:

   $STUDENT-FILE
   PRINT STUDENT.ITEM(STUDENT.GENDER.ATTRIBUTE)

When we precompile this we should get:

*
* $STUDENT-FILE
*
   OPEN '','STUDENT-FILE' TO STUDENT.FILE ELSE
      PRINT 'Failed to open file: STUDENT-FILE'
      STOP
   END
*
   EQU STUDENT.FIRST.NAME.ATTRIBUTE TO 1
   EQU STUDENT.LAST.NAME.ATTRIBUTE TO 2
   EQU STUDENT.NUMBER.ATTRIBUTE TO 3
   EQU STUDENT.GENDER.ATTRIBUTE TO 4
*
   PRINT STUDENT.ITEM(STUDENT.GENDER.ATTRIBUTE)

We open the file and we set up the equates so that we have real names we can then use in our program.

We could make our precompile work with all dictionary items but this would result in many different names for the same attribute to be set up and this would make it harder to read and understand the program.

This is why we only read in the A dictionary items.

Now with the reasoning out of the way, let's take a look at the precompiler program.

The most up to date version can be found at github link


*
   GIT.FILENAME = 'PRECOMPILE'
   GIT.REPO = 'https://github.com/krowemoh/TCL-Utilities.git'
*
   EQU TRUE TO 1
   EQU FALSE TO 0
*
* COMPILER DIRECTIVES
*
   $DEFINE DATABASE.QM
   $DEFINE PLATFORM.LINUX
*
   $IFDEF DATABASE.QM
      $CATALOGUE LOCAL
   $ENDIF
*
   @USER1 = 'PRECOMPILE'
   @USER2 = 'PRECOMPILE'
*
   CALL GET.ARGUMENTS(ARGUMENTS)
*
   ARGS.LEN = DCOUNT(ARGUMENTS,@AM)
*
   IF ARGS.LEN = 1 THEN
      PRINT 'PRECOMPILE - Preprocess a BASIC program'
      PRINT
      PRINT '    PRECOMPILE BP TEST.PREC'
      PRINT
      STOP
   END
*
   IF ARGS.LEN > 3 THEN
      PRINT 'Invalid number of arguments.'
      STOP
   END
*
   FILENAME = ARGUMENTS<2>
   ITEM.ID = ARGUMENTS<3>
*
   OPEN '',FILENAME TO FILE ELSE
      PRINT 'Unable to open file: ' : FILENAME
      STOP
   END
*
   READ ORIGINAL.RECORD FROM FILE,ITEM.ID ELSE
      PRINT 'Failed to read: ' : FILENAME : ' ' : ITEM.ID
      STOP
   END
*
   GOSUB SETUP.HEADER
   GOSUB SETUP.FOOTER
*
   RECORD = ''
*
   NUMBER.OF.LINES = DCOUNT(ORIGINAL.RECORD,@AM)
*
   END.FOUND = FALSE
*
   FOR I = 1 TO NUMBER.OF.LINES
      RAW.LINE = ORIGINAL.RECORD<I>
      LINE = TRIM(RAW.LINE)
*
      BEGIN CASE
         CASE LINE = '$END' AND NOT(END.FOUND)
            END.FOUND = TRUE
            FOOTER<-1> = '* $END'
*
         CASE LINE[1,1] = '$' AND NOT(END.FOUND)
            INCLUDE.NAME = LINE[2,LEN(LINE)]
*
            OPEN '',INCLUDE.NAME TO INCLUDE.FILE ELSE
               HEADER<-1> = '* ' : LINE
               HEADER<-1> = '* FAILED TO OPEN FILE: ' : LINE
               HEADER<-1> = '*'
               CONTINUE
            END
*
            HEADER<-1> = '* ' : LINE
            HEADER<-1> = '*'
*
            HEADER<-1> = \   OPEN '','\ : INCLUDE.NAME : \' TO \:CHANGE(INCLUDE.NAME,'-','.'):\ ELSE\
            HEADER<-1> = \      PRINT 'Failed to open file: \ : INCLUDE.NAME : \'\
            HEADER<-1> = \      STOP\
            HEADER<-1> = \   END\
*
            OPEN 'DICT',INCLUDE.NAME TO DICT.FILE ELSE
               CONTINUE
            END
*
            ATTR.LIST = ''
*
            NAMESPACE = INCLUDE.NAME
            IF NAMESPACE[5] = '-FILE' THEN
               NAMESPACE = NAMESPACE[1,LEN(NAMESPACE)-5]
            END
*
            CLEARSELECT
            SELECT DICT.FILE
*
            EOF = FALSE
*
            LOOP
               READNEXT DICT.ID ELSE EOF = TRUE
*
            UNTIL EOF DO
               READ DICT.ITEM FROM DICT.FILE,DICT.ID ELSE
                  CONTINUE
               END
*
               IF DICT.ITEM<1> # 'A' THEN
                  CONTINUE
               END
*
               ATTR = DICT.ITEM<2>
               LOCATE(ATTR,ATTR.LIST<1>,1;ANYPOS;'AR') ELSE
                  ATTR.LIST = INSERT(ATTR.LIST,1,ANYPOS;ATTR)
                  ATTR.LIST = INSERT(ATTR.LIST,2,ANYPOS;DICT.ID)
               END
            REPEAT
*
            HEADER<-1> = \*\
*
            FOR ATTR.CTR = 1 TO DCOUNT(ATTR.LIST<1>,@VM)
               ATTR = ATTR.LIST<1,ATTR.CTR>
               DICT.ID = ATTR.LIST<2,ATTR.CTR>
               HEADER<-1> = \   EQU \ : NAMESPACE : \.\ : DICT.ID : \.ATTRIBUTE TO \ : ATTR : \\
            NEXT ATTR.CTR
*
            HEADER<-1> = \*\
*
         CASE TRUE
            RECORD<-1> = RAW.LINE
            
      END CASE
   NEXT I
*
   IF END.FOUND THEN
      NEW.RECORD = HEADER
      NEW.RECORD<-1> = FOOTER
      NEW.RECORD<-1> = RECORD
*
      WRITE NEW.RECORD ON FILE,ITEM.ID
   END
*
   STOP
*
*********************  S U B R O U T I N E  *********************
*
SETUP.HEADER:NULL
*
   HEADER = \*\
   HEADER<-1> = \   GIT.FILENAME = '\ : ITEM.ID : \'\
   HEADER<-1> = \   GIT.REPO = 'https://github.com/krowemoh/TCL-Utilities.git'\
   HEADER<-1> = \*\
   HEADER<-1> = \   EQU TRUE TO 1\
   HEADER<-1> = \   EQU FALSE TO 0\
   HEADER<-1> = \*\
   HEADER<-1> = \* COMPILER DIRECTIVES\
   HEADER<-1> = \*\
   HEADER<-1> = \   $DEFINE DATABASE.QM\
   HEADER<-1> = \   $DEFINE PLATFORM.LINUX\
   HEADER<-1> = \*\
   HEADER<-1> = \   $IFDEF DATABASE.QM\
   HEADER<-1> = \      $CATALOGUE LOCAL\
   HEADER<-1> = \   $ENDIF\
   HEADER<-1> = \*\
*
   RETURN
*
*********************  S U B R O U T I N E  *********************
*
SETUP.FOOTER:NULL
*
   FOOTER = \   @USER1 = '\ : ITEM.ID : \'\
   FOOTER<-1> = \   @USER2 = '\ : ITEM.ID : \'\
   FOOTER<-1> = \*\
*
   RETURN
*
* END OF PROGRAM
*
   END
*

The core logic is that we read in the FILENAME and ITEM.ID from the command and then we open the record up.

Once we have the record, we go through every line and add it to a new record.

If the line starts with a $, dollar sign, we will execute some logic. We want to first check if the $ include is a file. If it is, then we add in the open logic to the new record we are creating.

We then get the dictionaries and loop through all of them. We will filter out everything except the A type records. We will then use a LOCATE statement to sort the attributes numerically so that the order makes sense.

Once we have all the A type records, we then add it as well to the new record we are building.

Once we've process all the dollar includes, we then check to see if the $END was found. We only want to precompile files with $END. This is because we can then use $END in our DEPRECOMPILE program to make things easier. Note that we definitely want a deprecompile program so that we can always undo a precompile.

If the $END was found, then we can save the new record we created, overwritting the old one.

The above PRECOMPILE does a bit more as it also brings in some constants like TRUE and FALSE.

There is a certain amount of fun in the fact that one needs to build all the tooling to make BASIC easier to work with but it does feel good to be this intimate with your tools. This kind of logic exists in other languages but it is done for you rather than having to do it yourself.