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.