V5R4 CL Enhancements, Revealed and Detailed
February 8, 2006 Ted Holt
Version 5 Release 4 of i5/OS continues the tradition that V5R3 began–that is, making a liar of me. Before V5R3, I would have bet my last Federal Reserve Note that IBM would never enhance the i5/iSeries Control Language (cleverly known by its acronym, CL), and I would have lost the bet. Like V5R3, V5R4 introduces several new CL features. I would like to thank Guy Vig of IBM for telling me about them. Now I’d like to share them with you.
Enhancements come in two general areas–subroutines and data definition. Let’s learn more about them. Keep in mind that I have not tested any of the code in this article, since I don’t have access to a machine that is running the new release.
Most of the CL code I write does not need subroutines, yet I am very glad to see that CL now supports a subroutine structure. I expect to use subroutines primarily to divide a CL procedure (a program or a module) into tasks of logically related code. The second way I plan to use subroutines is to avoid the duplication of code within a CL procedure.
Place subroutines at the end of a CL source member, just before the ENDPGM command. No need to add a GOTO or RETURN command before the first subroutine, the CL compiler inserts an implicit RETURN for you. Use the SUBR and ENDSUBR commands to indicate the beginning and end of a subroutine. Here is the general outline CL procedures follow.
PGM <declarations: DCL, DCLF, DCLPRCOPT, COPYRIGHT> <global MONMSG commands> <procedural code> SUBR <procedural code> ENDSUBR SUBR <procedural code> ENDSUBR ENDPGM
I would have thought that IBM would use the label to indicate the name of a subroutine, but I would have been wrong. The name of a subroutine goes in the SUBR command’s only parameter, also named SUBR. If you place a label on the SUBR command, or precede the SUBR command with a labeled null command, you may use GOTO within the subroutine to return to the first executable command within the subroutine. In the following example, branching to the Again label continues execution with the first CHGVAR command.
Again: SUBR SUBR(DoSomethin) chgvar &Var1 &Var2 chgvar &Var3 &Var4 call SomePgm (&SomeVar &RtnCode) if (&RtnCode *eq ' ') then(goto Again) <... more commands ...> ENDSUBR
Speaking of labels, I found it interesting that the compiler permits two or more labels to have the same name as long as they are not in the same routine. In the following example, three END labels exist–one in the main routine and two in subroutines Validate and NextFile.
pgm <... more commands ...> Read: rcvf monmsg cpf0864 exec(goto end) <... more commands ...> goto Read End: return subr Validate <... more commands ...> if &ErrorFound then(goto End) <... more commands ...> End: chgvar (&ErrorCount) (&ErrorCount + 1) endsubr subr NextFile <... more commands ...> if &EOF then(goto End) <... more commands ...> End: endsubr
And while we’re on the subject of labels, don’t try to access a label in one routine from another routine. The compiler won’t allow it.
The ENDSUBR command has one optional parameter, RTNVAL, which allows you to return a four-byte signed integer value to the caller. One way you might use this feature is to provide a return status, which would indicate whether the subroutine completed normally or abnormally. You might use zero, the default value, to indicate that the subroutine completed normally, and non-zero values to indicate errors. The caller can test the return value and proceed accordingly.
The subroutine returns to the caller when the last executable instruction has run, but you may exit the subroutine earlier with the RTNSUBR command. RTNSUBR has the same optional RTNVAL parameter that ENDSUBR has.
To invoke a subroutine, use the CALLSUBR command. CALLSUBR has two parameters. SUBR indicates the name of the subroutine to be called, while RTNVAL may contain the name of a four-byte integer variable to receive the subroutine’s return code.
The following example may give you a clearer picture of how subroutines fit into the CL procedure model.
This procedure calls program PGM1, which may or may not create a report. If PGM1 creates the report, then programs PGM2, PGM3, and PGM4 should also run. But if PGM1 does not build the report, the job is complete. Subroutine RtvSplfNbr uses the QSPRILSP API to retrieve the number of the last spool file that was created in the job. I used the RTNVAL parameter of the ENDSUBR command to load the retrieved spool file number into the &SPLFNBR1 and &SPLFNBR2 variables. Calling the subroutine immediately before and after calling PGM1 provides two spool file numbers. If they are the same, PGM1 did not create a spool file.
pgm dcl &SplfNbr1 *int 4 dcl &SplfNbr2 *int 4 dcl &RcvVar *char 70 dcl &BytesAvail *int 4 stg(*defined) defvar(&RcvVar 1) dcl &BytesRtn *int 4 stg(*defined) defvar(&RcvVar 5) dcl &SplfName *char 10 stg(*defined) defvar(&RcvVar 9) dcl &JobName *char 10 stg(*defined) defvar(&RcvVar 19) dcl &UserName *char 10 stg(*defined) defvar(&RcvVar 29) dcl &JobNbr *char 6 stg(*defined) defvar(&RcvVar 39) dcl &SplfNbr *int 4 stg(*defined) defvar(&RcvVar 45) dcl &SysName *char 8 stg(*defined) defvar(&RcvVar 49) dcl &SplfCrtDat *char 7 stg(*defined) defvar(&RcvVar 57) dcl &SplfCrtTim *char 6 stg(*defined) defvar(&RcvVar 65) dcl &RcvVarLen *int 4 value(70) dcl &FmtName *char 10 dcl &ErrorCode *char 8 dcl &Stat *lgl dcl &SplfExists *lgl callsubr subr(RtvSplfNbr) rtnval(&SplfNbr1) call pgm1 callsubr subr(RtvSplfNbr) rtnval(&SplfNbr2) /* If pgm1 created a report, continue with other tasks. */ if (&SplfNbr2 *ne &SplfNbr1) do call pgm2 call pgm3 call pgm4 enddo return subr subr(RtvSplfNbr) chgvar &BytesAvail 70 chgvar &FmtName 'SPRL0100' chgvar &ErrorCode x'0000000000000000' chgvar &SplfExists '1' call QSPRILSP (&RcvVar &RcvVarLen &FmtName &ErrorCode) monmsg cpf333a exec(chgvar &SplfExists '0') if (&SplfExists) then else do chgvar &SplfNbr 0 enddo endsubr rtnval(&SplfNbr) endpgm
The Subroutine Stack
Subroutines cannot be nested. However, subroutines can call other subroutines. In fact, a subroutine can call itself. Each invocation of a subroutine pushes an entry onto a stack that by default can go 99 levels deep. An entry is popped off the subroutine stack when the subroutine returns so, as far as I’m concerned, 99 may as well be infinity. However, you can use the new Declare Processing Options (DCLPRCOPT) command to change the depth of the subroutine stack to any value from 20 to 9,999 levels. The following command allows 180 levels of subroutine calls. If the program containing this DCLPRCOPT command were to go into a loop of subroutines calling subroutines, the CL runtime will send escape message CPF0822 (‘Subroutine stack overflow’) when the subroutine at level 180 tries to call another subroutine.
A CL procedure may have only one DCLPRCOPT command, which must appear in the declarations section with the other declaration commands–DCL, DCLF, and COPYRIGHT.
If you were getting tired of the Declare (DCL) command, with its four mundane, ordinary, commonplace, and monotonous parameters, you’re in for a treat. In V5R4, DCL gets four new parameters and a new data type to boot! CL will never be the same.
The new DCL provides two new features–the ability to overlay one variable with another and the pointer data type. Let’s look at overlaying data first.
Suppose you have a command with a parameter that defines a qualified object name.
CMD PROMPT('Do Something') PARM KWD(FILE) TYPE(Q1) MIN(1) + PROMPT('Physical file') Q1: QUAL TYPE(*NAME) LEN(10) MIN(1) QUAL TYPE(*NAME) LEN(10) DFT(*CURLIB) + SPCVAL((*CURLIB)) PROMPT('Library')
The command processor passes the qualified object name to the command-processing program as a twenty-byte character string. The first ten characters are the object name. The last ten characters are the library name. Prior to V5R4, you must use the substring function to extract the object and library names to variables of their own.
pgm (&QualFile) dcl &QualFile type(*char) len(20) dcl &File type(*char) len(10) dcl &Lib type(*char) len(10) chgvar &File %sst(&QualFile 1 10) chgvar &Lib %sst(&QualFile 11 10) chkobj &Lib/&File *file
The new DCL supports the STG (storage) keyword, which lets you designate how a variable’s storage is to be allocated. STG(*DEFINED) tells the compiler that a variable overlays all or part of another variable. You’ll also need to use the DEFVAR (defined on variable) keyword to tell which variable this new variable is part of. DEFVAR takes two values–the name of the variable that is overlaid and the starting position for the overlay. The default starting position is one.
Let’s return to the qualified object example. Under V5R4, you may define the object and library variables to be substrings of the parameter. This means that you no longer need the substring operations. Compare the following code to the pre-V5R4 code of the previous example.
pgm (&QualFile) dcl &QualFile type(*char) len(20) dcl &File type(*char) len(10) + stg(*defined) defvar(&QualFile 1) dcl &Lib type(*char) len(10) + stg(*defined) defvar(&QualFile 11) chkobj &Lib/&File *file
Note that the two sections of code are not equivalent. In the pre-V5R4 version, changing &File or &Lib does not change &QualFile, and changing &QualFile does not change &File and &Lib. In the V5R4 version, changing &File or &Lib does change &QualFile, and changing &QualFile does change &File and &Lib.
The ability to overlay character fields with character fields is OK, but it doesn’t let us do anything we couldn’t already do using substring and concatenation operations. The real power of this feature is when data of one type is contained within a variable of another type. For example, let’s say you have written a CL command that defines a list of packed decimal parameters.
CMD PROMPT('Print Some Customer Report') PARM KWD(ACCOUNT) TYPE(*DEC) LEN(7 0) MIN(1) + MAX(12) PROMPT('Customer account number(s)')
The list of account numbers gets passed to the command-processing program in a fifty-byte string. The first two bytes contain the number of entries in the list in binary format. Each group of four bytes thereafter holds a packed decimal number. Now, how does your CL program extract the decimal values from that big ol’ character string? Not easily. In fact, your CL program can’t do it. It has to call a program in some other language (e.g., RPG or COBOL) to get the job done.
That little problem goes away with V5R4. Here’s some code from the command-processing program.
pgm parm(&List) dcl &List *char 50 dcl &NbrOfListE *int 2 stg(*defined) defvar(&List) dcl &ListNdx *int 2 dcl &Offset *int 2 value(3) dcl &AccountCh *char 4 dcl &AccountNbr *dec 7 stg(*defined) defvar(&AccountCh) DoFor &ListNdx from(1) to(&NbrOfListE) /* Extract the next account number from the list */ ChgVar &AccountCh %sst(&List &Offset 4) /* do something with &AccountNbr here */ ChgVar &Offset (&Offset + 4) EndDo
Notice that &AccountCh (a character variable) and &AccountNbr (a decimal variable) are defined to occupy the same spot in memory. This gives me a way to access each customer account number as a decimal value.
Did you notice the other instance of overlaid data in this example? The first two bytes of the &LIST parameter contain the number of list entries as a binary number. I have defined &NBROFLISTE to contain that number. In V5R3 and earlier, I have to use the %binary (or %bin) built-in function to extract the number of list entries.
dcl &List *char 50 dcl &NbrOfListE *int 2 ChgVar &NbrOfListE %bin(&List 1 2)
I am not exuberantly excited about the addition of pointers to CL, mainly because I do not like pointers and I avoid them when possible. I first learned how to use pointers eons ago when I learned Pascal. I found them very useful for linked lists and other data structures that I had to master in order to get my computer science degree. While I enjoy programming with pointers for fun, I haven’t found pointers to be of much help in shipping product out the factory doors. (For one use of pointers that I do like, see Define Compile-Time Array Data in D-Specs.)
However, many C functions and some APIs use pointers. On one hand, if CL is to fully participate as an ILE language, it probably makes sense to add pointer support to CL. On the other hand, CL is fundamentally a job control language. Does a job control language really need to do the sorts of things that RPG, C, COBOL, and Java do so much more easily? For example, V5R4 allows you to bind CL to the C record I/O routines (the ones whose names begin _R), but the code will be messy. Why not use RPG for database processing instead?
To declare a pointer variable, specify TYPE(*PTR) but do not code the LEN parameter.
DCL &ListAddr TYPE(*PTR)
Pointer &ListAddr does not point to anything yet. To assign a memory address to it, use the new %ADDRESS built-in function. The following example shows how pointer &ListAddr is set to point to character variable &List1, then later set to point to &List2.
DCL &List1 TYPE(*CHAR) LEN(57) DCL &List2 TYPE(*CHAR) LEN(57) DCL &ListAddr TYPE(*PTR) CHGVAR VAR(&ListAddr) VALUE(%ADDRESS(&LIST1)) <...more commands...> CHGVAR VAR(&ListAddr) VALUE(%ADDRESS(&LIST2))
If you like, you may abbreviate %ADDRESS to %ADDR.
CHGVAR VAR(&ListAddr) VALUE(%ADDR(&LIST1))
You can initialize a pointer variable by naming the variable to which it points in the ADDRESS parameter of the DCL command.
DCL &List TYPE(*CHAR) LEN(82) DCL &ListAddr TYPE(*PTR) ADDRESS(&List)
A based variable does not occupy storage of its own. The pointer on which a based variable is based must be set to the address of another variable before the based variable can be used. Look at the following variable declarations.
dcl &ptrText type(*ptr) dcl &Text type(*char) len(256) stg(*based) basptr(&ptrText)
Even though &TEXT is defined as a 256-byte character variable, the compiler does not allocate 256 bytes of memory for it. The STG(*BASED) parameter means that the compiler is not to allocate storage. The BASPTR parameter says that the pointer variable &PtrText has the address of the memory that &Text should reference.
Contrast this to the overlaid variables. In the following example, &Name is assigned to the first 20 bytes of memory occupied by the &Data variable.
dcl &Data *char 80 dcl &Name *char 20 stg(*defined) defvar(&Data)
However, in this code snippet, &Name cannot be used until its basing pointer, &pName, has been assigned a memory address. In this example, &pName is set to the same memory address allocated for the &Data variable, so &Name will be mapped over the first 20 bytes of memory allocated for &Data.
dcl &pName *ptr dcl &Data *char 80 dcl &Name *char 20 stg(*based) basptr(&pName) chgvar var(&pName) value(%addr(&Data))
But what if &Data contains a list of four qualified names, each 20 bytes long? The next code snippet shows how you can change the pointer offset to process each of these names, using based and defined variables. The new %OFFSET (or %OFS) built-in function can be used to store the offset portion of a pointer or, when used on the VAR parameter of a CHGVAR command, to change the offset in a pointer variable.
dcl &pName *ptr dcl &Data *char 80 dcl &Name *char 20 stg(*based) basptr(&pName) dcl &objname *char 10 stg(*defined) defvar(&Name 1) dcl &libname *char 10 stg(*defined) defvar(&Name 11) dcl &loopctr *int 2 chgvar var(&pName) value(%addr(&Data)) DoFor &loopctr from(1) to(4) If (&libname *eq '*CURLIB') then(callsubr getcurlib) /* Move the basing pointer for &Name forward 20 bytes */ Chgvar var(%offset(&pName)) value(%offset(&pName)+20)) Enddo
Maybe another pointer example will help. Suppose there are four identically defined variables that must be processed in the same way. You could duplicate the processing code, once for each variable, but wouldn’t it be nice to write one subroutine that can handle one variable and use the subroutine four times? The following example does just that.
pgm parm(&Text1 &Text2 &Text3 &Text4) dcl &ptrText type(*ptr) dcl &Text type(*char) len(256) stg(*based) basptr(&ptrText) dcl &Text1 type(*char) len(256) dcl &Text2 type(*char) len(256) dcl &Text3 type(*char) len(256) dcl &Text4 type(*char) len(256) chgvar var(&ptrText) value(%addr(&Text1)) callsubr Process chgvar var(&ptrText) value(%addr(&Text2)) callsubr Process chgvar var(&ptrText) value(%addr(&Text3)) callsubr Process chgvar var(&ptrText) value(%addr(&Text4)) callsubr Process subr Process /* commands that do something with &TEXT */ if (%sst(&Text 31 1) *eq 'A') do <... more commands ...> enddo chgvar %sst(&Text 97 6) value('BR-549') <... more commands ...> endsubr endpgm
The common subroutine has the name Process. It references the &Text variable. To make Process carry out its mission for any of the procedure’s parameters is a simple matter of setting &ptrText to the address of that parameter. That is, while &ptrText is set to the address of &Text3, all changes to &Text are really changes to &Text3.
More to Come?
IBM didn’t add my most-desired feature to V5R4 CL. I was hoping for source code inclusion directives, like /COPY in RPG, COPY in COBOL, and #include in C. Nevertheless, I’m glad to see the compiler enhanced again after so many years. I’ll hope for compiler directives in a future release.