Guru: Fundamentals Of Prototyping, Part 1
July 24, 2017 Jon Paris
In my series on parameter passing fundamentals, I mentioned the importance of using prototypes to help ensure consistency of parameter definitions; i.e., to help ensure that the parameter passed to the called routine matches what is expected. After that article was published, it came to my attention that many of my readers were unaware of some of the additional capabilities of prototypes, in particular that prototypes can not only identity errors in parameter usage, but that in some cases they can actually fix them for you!
Before we can begin, though, it would be a good idea to ensure that we are all on the same page of the hymnbook as far as the basics of prototyping are concerned. Take a look at the field definitions, prototype, and call in the code below.
dcl-s shortChar Char(4); dcl-s longChar Char(100); dcl-s packed72 Packed(7: 2); dcl-s zoned72 Zoned(7: 2); dcl-s zoned92 Zoned(9: 2); dcl-pr TestCall ExtPgm; parm1 Char(4); parm2 Char(100); parm3 Zoned(7: 2); End-Pr; TestCall( longChar: shortChar: zoned72 );
In this case, the programmer apparently mixed up the first and second parameters and put them in the wrong order. Luckily the compiler will catch this and issue the error message: “The type and attributes of parameter 2 do not match those of the prototype.”
So far so good, but wait . . . shouldn’t the compiler have flagged the first parameter as an error as well? The prototype says it should be four characters long, but the compiler appears to have accepted one of 100 characters!
Welcome to the one “feature” of prototyping that I really don’t like. Way back in the mists of time, the compiler writers decided that it is not an error to pass a parameter that is longer than requested because it cannot result in any memory corruption. Confusion for the programmer, yes, but corruption, no. A shorter parameter on the other hand is a definite problem for the reasons that I outlined in the first tip and so needs to trigger an error.
What about this call? It corrects the mistake in the first example, and the third parameter is of the correct data type but it is longer than specified in the prototype. Will it compile?
TestCall( shortChar: longChar: zoned92 );
Based on what I just said about long parameters you might expect the answer to be “Yes,” but the correct answer is “No.” While the compiler allows you to pass a longer character variable than required, the same is not true for numerics.
There’s a good reason for this. With an oversized character field you are effectively just truncating the string. But with numerics, you would be changing the scale of the number. To demonstrate, imagine a five-character field containing “ABCDE” that is passed as parm1. It would be seen by the called program as having a value of “ABCD” – simple truncation. But if field zoned92 contained the value 12345.67 and we allowed it to be passed as parm3, the value that would be received would be 123.45. That’s a very different situation than simply dropping the last character. And if we were dealing with a negative value things would be even worse, as a negative value would become positive.
So, what can prototyping do to help you in these situations?
Proto Power To The Rescue!
The magical keyword that can help fix this for you is CONST, which actually stands for “Read-only Reference,” but that is a little misleading as I will explain shortly. By using this keyword, we give the compiler permission to copy mismatched data to a temporary field with the correct format and to pass that temporary field as the parameter. The only limitation is that the field you present as the parameter must have the same base data type as the parameter in question. For instance, character, numeric, date, etc. But as you’ll see shortly, even this is not really much of a limitation.
So if I modify the prototype as shown to include the CONST keyword:
dcl-pr TestCallC ExtPgm; parm1 Char(4) Const; parm2 Char(100) Const; parm3 Zoned(7: 2) Const; End-Pr;
Now this call:
TestCall( longChar: shortChar: packed92 );
Even though this call is perhaps nonsensical, it is perfectly legal.
But what values will be passed? The easy way to remember this is ask yourself how you would handle the situation if you had to do all the work by hand. You would have to manually define a temporary field of the correct size and type for each of the parameters and then copy the field that you want to pass into that temporary field before passing the temporary to the called routine. The result might look a bit like this:
dcl-s temp1 Char(4); dcl-s temp2 Char(100); dcl-s temp3 Zoned(7: 2); temp1 = longChar; temp2 = shortChar; temp3 = zoned92; TestCall( temp1: temp2: temp3 );
Using this approach, you can see that the first four characters in longChar would be placed in temp1 and the balance of the content discarded. Apply the same thinking to the temp2 field – the four characters of shortChar will be copied to temp2 and padded with trailing blanks to make up the full length of 100 characters. So far so good.
But what about the third (numeric) parameter? In the example I gave earlier, a value of 12345.67 in zoned92 would be correctly copied to temp3 and all would be well. However, if the field zoned92 contained a value such as 123456.78 (i.e., too large a value to be contained within temp3) then a run time error “. . . too small to contain result . . .” would occur on the assignment. This set of operations is exactly what will happen on the call, except that you don’t have to code all the temporary fields yourself – the compiler will do it for you under the covers. If there is indeed a possibility of numeric overflow, then the error can be trapped by a MONITOR block and appropriate corrective/reporting action taken.
For me this is a really useful feature, particularly when it comes to numeric values. It was always a pain having to remember to copy values to an appropriately sized and typed field prior to calling some utility function or other. With CONST I don’t have to worry about that.
If this was all that CONST did for me it would be enough, but in the immortal words of the infomercial, “Wait . . . there’s more!”
Remember that I noted earlier that the only limitation for CONST parameters was that the basic data type match? That means that any of the following can be passed as parameters:
- A literal such as 123 (for numerics), ‘ABC’ (for character) or even D’05/19/17′ (for date)
- A fixed-length field when a varying is expected and vice versa
- A named constant
- Built-in functions such as %Dec (numerics) or %Char (character)
- Subprocedure or bound API calls that return the appropriate data type
- Expressions – yes really! For example, something like ( ‘A’ + prodCode ) is just fine
- Any combination of the above!
Now that makes life much more interesting.
The CONST “Limitation”
There are of course limits to this functionality. I quoted “limitation” in the heading because in many cases, this limit might well be considered a “feature.” Why do I say “feature?” Because it would seem that the called routine cannot modify parameters identified as CONST, since such parameters may be copies of the original data. Indeed, IBM’s description of CONST as meaning “read-only reference” would imply that such parameters cannot be changed. Certainly that is the intention, but it is still somewhat misleading. Taking IBM’s description at face value would lead you to believe that the called program can never change the value of any CONST parameter. After all, a copy of the data is being passed right? Well, the problem is that this is only true sometimes.
CONST will only cause a copy of the data to be made if there is a parameter size or type mismatch. If the field we pass exactly matches the requirements, then no copy will be made and the original field will be passed. The result is that if the called routine either does not, or cannot (as in the case of CL for example), use the same prototype, the compiler cannot prevent the field’s value from being changed by the called routine.
Personally, I would like to see IBM provide an extended version of CONST that would provide true read-only characteristics. I guess it is time to break out my RFE writer’s hat again. I’m not sure what to call this option though. COPY would be my preference. Any suggestions?
Wrapping Up For Now
CONST is just the beginning. There are many other useful keywords that can be added to your prototypes all of which help to make your code simpler and more bullet proof. In a later tip I will examine several of them, including some (such as VALUE) that can only be used on bound calls.
If you are still using the old-fashioned CALL/PARM approach to program calls, then I urge you to start taking advantage of prototypes. Any of the types of problems mentioned above could arise in your programs, particularly when older programs are being modified. The results can be nasty and difficult to track down, particularly in the case of the mis-sized numeric parameters. You could make a simple mistake and the code might well continue to run, except of course the results would be incorrect. And yes, I have seen code with just this kind of error put into production with disastrous results.
For those of you who think my describing CALL/PARM as “old-fashioned” was a bit harsh, all I can say is that prototyped program calls have been part of RPG since V3R2 – and that first became available in June of 1996, nearly 21 years ago! So yes, it is old-fashioned, and it is long past time to adopt this “newfangled” language feature!
Regarding CONST not creating read only copy of data if the size matches, I agree that the COPY keyword instead of CONST makes sense. But it may not be completely intuitive. How about READONLY ?
I like CONST-NOCHG