Guru: Beyond The Basics Of Code Conversion
March 19, 2018 Paul Tuohy
Today many RPG programmers are tasked with having to modernize existing programs. These programs often have long and varied history. They may be RPG IV programs or they may be RPG IV programs that were originally RPG II or RPG III programs, or any permutation or combination you can imagine. There are tools available to help us convert from fixed-form RPG to free-form RPG and, within RDi, we have options to help us reformat code and refactor variable names. But these tools can only go so far. We finally reach the point where we have to put fingers to the keyboard and start changing the code. Or do we?
Within most applications there are recognizable patterns and standards within the code. These patterns and standards may be too specific for a generic conversion tool but there is nothing to stop us from writing our own conversion programs.
This story contains code, which you can download here.
In this article, I will describe a conversion program which refactors a set of standard variable names and replaces any CTL-OPT operations with a standard /INCLUDE directive. Although RDi has a marvelous option for refactoring variables names, I would sooner not have to repeat the same process for twenty standard variables names in every program I edit. At the end of the article, I will discuss some other candidates for code conversion programs.
A Variable Conversion Table
The following SQL code shows the creation and population of a conversion table of from- and to-values. Our conversion program will scan a source member and replace every occurrence of STRIN with the corresponding value of STROUT. The case of the value of STRIN is not relevant but the case of STROUT is what will be placed into the new source.
create table cvvars (strin varchar(50) not null default, strout varchar(50) not null default); insert into cvvars values ('getRec','getRow') ,('dispSc','displayScreen') , ('valdSc','validateScreen') ,('procSc','processScreen') , ('dispCD','displayConfirm') ,('F4Prmt','F4Prompt') , ('#retrn','returnCode') ,('#ctl','g_nextFunction');
A word of caution: the conversion program is going to perform string replacement so be sure to avoid values that may be subsets of longer names, e.g. (from the example above) if a source contains a variable named LastGetRec, it will be changed to LastGetRow.
Conversion
The conversion process requires three objects.
Member | Description |
CONV001 | SQLRPGLE program to perform the conversion |
LCONV001 | Command Prompt for required parameters |
LCONV001C | Command Processing Program for LCONV001 |
Prompting the LCONV001 command provides options to identify from- and to-members for conversion.
The conversion program assumes that the code has already been converted to free-form (either with or without **FREE).
This is the source for the LCONV001 command.
CMD PROMPT('Sample Local Conversion') /* CRTCMD CMD(LCONV001) PGM(LCONV001C) SRCFILE(CONVPGMS) */ PARM KWD(FROMSRCPF) TYPE(QUAL01) PROMPT('From Source File') PARM KWD(FROMMBR) TYPE(*NAME) MIN(1) PROMPT('From Member Name') PARM KWD(TOSRCPF) TYPE(QUAL02) PROMPT('To Source File') PARM KWD(TOMBR) TYPE(*NAME) DFT(*FROMMBR) SPCVAL((*FROMMBR)) + PROMPT('To Member Name') QUAL01: QUAL TYPE(*NAME) MIN(1) QUAL TYPE(*NAME) MIN(1) PROMPT('From Library') QUAL02: QUAL TYPE(*NAME) DFT(*FROMSRCF) SPCVAL((*FROMSRCF)) QUAL TYPE(*NAME) DFT(*FROMLIB) SPCVAL((*FROMLIB)) + PROMPT('To Library')
The command processing CL program (LCONV001C) performs some basic housekeeping before call the conversion program. Refer to the callouts for details.
PGM (&FROMQUAL &MBR &TOQUAL &TOMBR) /* CRTCLPGM PGM(LCONV001C) SRCFILE(CONVPGMS) */ DCL &FROMQUAL *CHAR 20 DCL &FROMSRCF *CHAR 10 STG(*DEFINED) DEFVAR(&FROMQUAL 1) DCL &FROMLIB *CHAR 10 STG(*DEFINED) DEFVAR(&FROMQUAL 11) DCL &MBR *CHAR 10 DCL &TOQUAL *CHAR 20 DCL &TOSRCF *CHAR 10 STG(*DEFINED) DEFVAR(&TOQUAL 1) DCL &TOLIB *CHAR 10 STG(*DEFINED) DEFVAR(&TOQUAL 11) DCL &TOMBR *CHAR 10 DCL &SRCTYPE *CHAR 10 DCL &TEXT *CHAR 50 (A) IF (&TOLIB = '*FROMLIB') CHGVAR &TOLIB &FROMLIB IF (&TOSRCF = '*FROMSRCF') CHGVAR &TOSRCF &FROMSRCF IF (&TOMBR = '*FROMMBR') CHGVAR &TOMBR &MBR (B) CHKOBJ OBJ(&TOLIB/&TOSRCF) OBJTYPE(*FILE) MBR(&TOMBR) MonMsg (CPF9815) Exec(DO) RTVMBRD FILE(&FROMLIB/&FROMSRCF) MBR(&MBR) + SRCTYPE(&SRCTYPE) TEXT(&TEXT) ADDPFM FILE(&TOLIB/&TOSRCF) MBR(&TOMBR) SRCTYPE(&SRCTYPE) + TEXT(&TEXT)) ENDDO (C) CALL CONV001 (&FROMLIB &FROMSRCF &MBR &TOLIB &TOSRCF &TOMBR)
(A) Set the values of the TO parameters if default values were specified.
(B) If the TO member does not exist, retrieve the source type and text of the FROM member and create the TO member.
(C) Call the conversion program.
The SQLRPGLE Conversion Program
The SQLRPGLE Conversion Program (CONV001) converts source from the FROM library/source file/member to the TO library/source file/member.
**free // CRTSQLRPGI OBJ(CONV001) SRCFILE(CONVPGMS) RPGPPOPT(*LVL2) DBGVIEW(*SOURCE) ctl-opt dftActGrp(*no) actGrp(*new) datFmt(*ymd); dcl-pi *n; libraryIn char(10); sourceFileIn char(10); memberIn char(10); libraryOut char(10); sourceFileOut char(10); memberOut char(10); end-pi;
Templates
Templates define the format of a source line and the variables conversion table defined earlier.
dcl-ds t_sourceLine template qualified inz; srcSeq zoned(6: 2); srcDate zoned(6: 0); srcData char(100); end-ds; dcl-ds t_cvtNames qualified template; strIn varchar(50); strOut varChar(50); end-ds;
Data Structure Arrays
Data structures arrays (based on the templates) are defined for the source member input, the source member output and the list of variable names for conversion. The integers are used to indicate the number of rows in the corresponding arrays.
dcl-dS g_inRec likeDs(t_sourceLine) inz(*likeDs) dim(32766); dcl-dS g_outRec likeDs(t_sourceLine) inz(*likeDs) dim(32766); dcl-s g_numLines int(10); dcl-s g_currentLine int(10); dcl-ds g_cvtNames likeDs(t_cvtNames) dim(1000); dcl-s g_numNames int(10);
If the source member contains more than 32,766 lines of code, I recommend some prior surgery – or the logic to the conversion program needs to change to “page” the source.
Other Work Fields
Work fields are used to process the current line (in original format and in upper case) and a loop counter.
dcl-s g_wrkLine varChar(100); dcl-s g_original varChar(100); dcl-s i int(10);
Mainline
The mainline processes the input source member, converts the source, and outputs the new member. Refer to the callouts for details.
exec SQL set option commit=*none, naming=*SYS; (A) g_numLines = get_Source(g_inRec); (B) g_numNames = get_Names(g_cvtNames); (C) set_CTLOPT(); (D) for i = 1 to g_numLines; (E) g_original = %trimR(g_inrec(i).srcData); g_wrkLine = str_toUpper(g_original); g_outRec(g_currentLine).srcDate = g_inrec(i).srcDate; (F) if check_CTLOPTorFREE(); // Ignore current CTL-OPT or **FREE else; // or (G) addNames(); // Scan/replace names endIf; endFor; (H) put_Source(); *inLR = *on;
(A) Retrieve the source member into the G_INREC data structure array. G_NUMLINES contains the number of source lines retrieved.
(B) Retrieve the list of variables names for conversion into the G_CVTNAMES data structure array. G_NUMNAMES contains the number of variables names for conversion.
(C) Add the /INCLUDE directive for the standard CTL-OPT copy member.
(D) Process each source line.
(E) Copy the current source lines to work fields – in original form and in uppercase.
(F) Ignore any lines containing CTL-OPT or **FREE.
(G) Replace any required variables names.
(H) Output the new source member.
Convert To Uppercase
The str_toUpper() subprocedure uses the SQL UPPER scalar function to convert a request string to uppercase.
dcl-proc str_toUpper; dcl-pi *n varchar(200); stringIn varChar(200) const; end-pi; dcl-s stringOut varChar(200); exec SQL values upper(:stringIn) into :stringOut; return %trimR(stringOut); end-proc;
Adding A Standard CTL-OPT
The set_CTLOPT() subprocedures adds an /INCLUDE directive for a standard CTL-OPT copy member. The subprocedure caters for the first line containing a **FREE directive. The include directive will be added as the first or second line depending on whether or not there is a **FREE directive.
dcl-proc set_CTLOPT; dcl-pi *n end-pi; dcl-s j int(5) inz(1); dcl-s S_INC_HEADER varChar(50) inz(' /include QCopy,stdHeader') ; if (%trimR(str_toUpper(g_inrec(1).srcData)) = '**FREE'); eval-Corr g_outRec(1) = g_inrec(1); j += 1; S_INC_HEADER = %trim(S_INC_HEADER); endIf; g_outRec(j).srcDate = uDate; g_outRec(j).srcData = S_INC_HEADER; g_currentLine = j + 1; end-proc;
Get The Input Source
The get_Source() subprocedure retrieves the input source member into a data structure array. Refer to the callouts for details.
dcl-proc get_Source; dcl-pi *n int(10); list likeDs(g_inRec) dim(32600); end-pi; dcl-s gotRows int(10); dcl-s getRows int(10) inz(%elem(list)); dcl-s mySQL varChar(1000); (A) mySQL = 'create or replace alias qtemp.sourceIn for ' + %trim(libraryIn) + '.' + %trim(sourceFileIn) + '(' + %trim(memberIn) + ')'; exec SQL (A) prepare AliasSourceIn from :mySQL; if (SQLCode <> 0); return 0; endIf; exec SQL (A) execute AliasSourceIn; (B) exec SQL declare get_Source scroll cursor for select srcSeq, srcDat, srcDta from qtemp.sourceIn order by srcSeq for read only; exec SQL open get_Source; exec SQL (C) fetch first from get_Source for :getRows rows into :list; (D) gotRows = SQLERRD(3); exec SQL close get_Source; return gotRows; end-proc;
(A) Use dynamic SQL to create an Alias (SOURCEIN in QTEMP) which aliases the requested member to be processed. Think of this as an OVRDBF.
(B) Declare a cursor to input source rows from the alias (which is now aliasing the requested member).
(C) Use a multi row fetch to retrieve all source into the data structure array.
(D) Store the number of rows retrieved.
Get The List Of Variable Names
The get_Names() subprocedure retrieves the list of variables names (for conversion) into a data structure array. The subprocedure uses a standard multi-row fetch. Note that the select statement converts the value of the STRIN variable to uppercase.
dcl-proc get_Names; dcl-pi *n int(10); list likeDs(t_cvtNames) dim(1000); end-pi; dcl-s gotRows int(10); dcl-s getRows int(10) inz(%elem(list)); exec SQL declare get_Names scroll cursor for select upper(strIn), strout from cvvars for read only; exec SQL open get_Names; exec SQL fetch first from get_Names for :getRows rows into :list; gotRows = SQLERRD(3); exec SQL close get_Names; return gotRows; end-proc;
Ignore Lines
The check_CTLOPTorFREE() subprocedure returns a true condition if the current line contains a CTL-OPT or *FREE directive. This means that this line will be ignored and will not be copied to the output source.
dcl-proc check_CTLOPTorFREE; dcl-pi *n ind end-pi; return ((%scan('CTL-OPT':g_wrkLine) > 0) or (g_wrkLine = '**FREE') ); end-proc;
Replace Variable Names
The add_Names() subprocedure scans the source line for all occurrences of variables to be replaced and replaces them. Refer to the callouts for details.
dcl-proc addNames; dcl-pi *n end-pi; dcl-s i int(10); dcl-s j int(10); (A) for i = 1 to g_numNames; (B) doU j = 0; (C) j = %scan(g_cvtNames(i).strIn: g_wrkLine); if j > 0; (D) g_original = %trimR(%replace(g_cvtNames(i).strOut: g_original: j : %len(g_cvtNames(i).strIn))); (D) g_wrkLine = str_toUpper(g_original); endIf; endDo; endFor; (E) g_outRec(g_currentLine).srcData = g_original; (F) g_currentLine += 1; end-proc;
(A) Check for each variable in the conversion table.
(B) Keep scanning a line for a variable until it isn’t found. A variable may be defined more than once in a line.
(C) Scan the line for the input variable.
(D) Replace the variables name with the new name.
(E) When all variables have been processed, set the new source line.
(F) Set the counter for the next source line.
Output Source Member
The put_Source() subprocedure uses a multi-row insert to create the new source member. Refer to the callouts for details.
dcl-proc put_Source; dcl-pi *n end-pi; dcl-s i int(10); dcl-s mySQL varChar(1000); (A) mySQL = 'create or replace alias qtemp.sourceOut for ' + %trim(libraryOut) + '.' + %trim(sourceFileOut) + '(' + %trim(memberOut) + ')'; exec SQL (A) prepare AliasSourceOut from :mySQL; if (SQLCode <> 0); return; endIf; exec SQL (A) execute AliasSourceOut; (B) for i = 1 to g_currentLine; g_outRec(i).srcSeq = i; endFor; exec SQL (C) delete from qtemp.sourceOut ; g_currentLine -= 1; exec SQL (D) insert into qtemp.sourceOut :g_currentLine rows values (:g_outRec) with nc; end-proc;
(A) Use dynamic SQL to create an Alias (SOURCEOUT in QTEMP) which aliases the member to be created. As with the input source member, this is a form of OVRDBF.
(B) Loop through the elements of the array and reset the source sequence number.
(C) Delete any existing lines from the source member.
(D) Insert the new source lines.
Other Candidates For Conversion
Hopefully, this conversion program has given you the idea of how a code conversion program might work for you. What can be achieved by a conversion is really dependent on the patterns and standards in your code. And conversion is not just for single lines of code, as in this example.
You could have a conversion program that replaces blocks of code. For example, you currently have a process that copies data into standard work fields and executes a subroutine and you want to change this to a subprocedure call. Delimit the block of code to be converted with special comments (e.g. //$$BmakeProc and //$$EmakeProc) and your conversion program processes and converts all of the lines between the comments.
Or, if you make use of “standard” RPG indicators, how about renaming them to named indicators?
You know the patterns in your code. Can you write a program to change them?