SSG Tips & Techniques

Coding Guidelines


To indent or not to indent?

Most SSG programmers, including me, don't indent their code. They start all directive and non-directive images in column 1 and use the 'E' processor call option when they want a listing of the skeleton with automatic indentation applied.

But it's quite permissible to do your own indenting, as this sample code, courtesy of Erich Staubringer, shows:

        *add sgs [*file]./toc
        *clear found
        *set stmt$ = 0
        *increment i to 1
        *  if column search from toc,stmt$+1,6 for [*element]
        *    if [toc,stmt$,4,1] = SYM
        *      set found
        *    else
        *      clear i
        *    endif
        *  endif
        *loop
Notice that the directives still begin in column 1 but there are spaces between the asterisk and the remainder of the directive. This is quite legal. And it's legal in ECL to leave spaces between the masterspace (@) and the command or processor name.

I think the decision of whether to indent or not is a personal one.


Use symbolic references within square brackets

Which of the following three equivalent statements do you prefer?

     *SET EQUIPMENT = '[*PACK,16,2]'
     *SET EQUIPMENT = '[*PACK,LSTR$,2]'
     *SET EQUIPMENT = '[*PACK,LEFT_STRING$,2]'

Please, please, please, don't answer "The first one." Skeletons that use numeric references are much harder to understand than those that use symbolic ones. I once came across a statement like this a couple of years after I wrote it:

     *IF COLUMN SEARCH FROM PACK,1,2,1 FOR [TIPFILE,T,4,1]

I hadn't the faintest idea what this test meant. I had to look at the PACK and TIPFILE SGSs to try to decipher the intent. My task would have been easier had the statement looked like this:

     *IF COLUMN SEARCH FROM PACK,1,PACK_PREP_FACTOR,1 FOR [TIPFILE,T,TIPFILE_REC_SIZE,1]

I find that the use of symbolic references within square brackets is the single most important factor in writing clear SSG skeletons. It's the equivalent of using EQUs and EQUFs in MASM.


Avoid the A option

I never use the A option on the SSG processor call ('@SSG,A'). The A options tells SSG to continue in the event of a no-find condition. Some programmers (lazy programmers) are tempted to use this where an SGS subfield is optional and where generating a null string is the correct action when the optional subfield is missing. We might call these good no-finds.

The problem with setting the A option is that SSG won't tell you about bad no-finds. These are no-finds resulting from typing errors or other programming mistakes: The A option will force the skeleton will continue and the error will be obscured. I want all the help I can get in isolating my errors as early as possible.


Use in-line comments

In-line comments are very valuable because they can help the reader understand the intent of a line of code. Unfortunately, programmers who come to SSG from languages that don't allow in-line comments (e.g., COBOL) often don't use them.

Unless its meaning is self-evident, include an in-line comment describing the condition intended by the *IF or *ELSEIF directives. Example:

     *IF [SUPS$,1,6,1] = 000000000000    . If no Core Block SUPS (i.e., if XPA system)

Likewise with the *INCREMENT (*DO) directive:
     *INCREMENT R TO [REEL,1,1]          . For each input reel number

On *SET or *CLEAR directives that create a global variable, include an in-line comment describing its purpose. Example:
     *CLEAR  LINE_CNT                    . Counts # of lines printed on current page
     *CLEAR  TOT_LINE_CNT                . Counts # of lines printed on all pages


Be very careful with *IF conditions

Beginning SSG programmers will sometimes write:

     *IF FIRST_NAME  =  'STEVE'

This compares two constants and so can never be true. It's wrong on two counts. First, if FIRST_NAME is a string variable, the value-of construct ([*var]) must be used in *IF:

     *IF [*FIRST_NAME]  =  'STEVE'

Second, a string literal in a *IF must not be placed in single quotes. (But in *SET directives string literals are placed in single quotes. This is one of those inconsistencies that vex beginners.) Thus, our corrected statement is:

     *IF [*FIRST_NAME]  =  STEVE

You can also put a string literal in two single quotes. Sometimes you'll need to do so because the string will contain a special character that can fool SSG:

     *IF [*FIRST_NAME]  =  ''Mary-Jo''

In an integer context, use a plus sign (+) to indicate that it's an integer variable rather than a string literal: :

     *IF +DELETE_CNT  >  100

And be careful when including an integer expression to include a plus sign (+) in the first token of the expression. Compare

     *IF +DELETE_CNT  >  MAX+3                  . syntax OK
with
     *IF +DELETE_CNT  >  MAX + 3                . syntax error!

It shouldn't work this way, but it does!


Avoid unnecessary value-of constructs

Misadventure with *IF conditions often causes programmers to use the value-of ([*var]) construct where it's not necessary. For example: :

     *INCREMENT U TO [USERID]
     *SET FIRST_NAME = '[USERID,[*U],4,2]'
     ...
     *LOOP

Within an SGS reference the occurrence, field and subfield must be integers. The variable U above is an integer but the programmer unnecessarily uses value-of. This causes SSG to convert U to a string and then back to an integer, thereby harming efficiency and, more important, readability. A more straightforward *SET directive is:

     *SET FIRST_NAME = '[USERID,U,4,2]'

Similarly, when doing arithmetic

     *SET FILE_CNT = FILE_CNT + 1

is preferable to

     *SET FILE_CNT = [*FILE_CNT] + 1


Restrict your variable names to 16 characters

Consider this skeleton fragment:

    *SET USERID_DELETE_CNT       = 7
    *SET USERID_DELETE_CNT_YTD   = 20
    *DISPLAY '[*USERID_DELETE_CNT]'
    *DISPLAY '[*USERID_DELETE_CNT_YTD]'

What values will be displayed? Both *DISPLAYs will display 20. This is because while SSG allows variable names to be up to 30 characters long, it uses only the first 16 characters internally. Thus, SSG sees only one variable here while the programmer sees two. This is the kind of help you don't need from a language processor! I suggest restricting your variable names to 16 characters.


Use intuitive loop index variable names

It's often necessary to process every SGS for a given label. This is done with the *INCREMENT (*DO) directive. The variable used in the *INCREMENT directive is the loop index. By convention I usually call this variable by the first letter of the label through which I'm looping. For example:

     *INCREMENT U TO [USERID]             . For each user-id
     *INCREMENT V TO [VIOLATION]          . For each security violation
     ...
     *DISPLAY 'User [USERID,U,1,1] committed a [VIOLATION,V,1,1]'
     *LOOP . V
     *LOOP . U

This is merely a convention that I find to be intuitive. Another appraoch that would be quite intuitive, although somewhat verbose, is:

     *INCREMENT USERID_NDX TO [USERID]         . For each user-id
     *INCREMENT VIOLATION_NDX TO [VIOLATION]   . For each security violation
     ...
     *DISPLAY 'User [USERID,USERID_NDX,1,1] committed a [VIOLATION,VIOLATION_NDX,1,1]'
     *LOOP . VIOLATION_NDX
     *LOOP . USERID_NDX

Here the loop index when scanning a set of SGSs is the SGS label with '_NDX' appended.


Use a variable naming discipline

Someone I-know-not-who once said that almost all program variables are one of eight types: It's a good idea to develop a variable naming discipline around these types. I append a suffix to variable names as follows:

Type of variable Suffix Example
Counter
_CNT
LINE_CNT
Switch
_FLG
FIRST_TIME_FLG
Code
_CD
EQUIP_CD
Date
_DATE
FILE_CAT_DATE
Time
_TIME
RUN_START_TIME
Identifier
_ID
USER_ID
Description
_DESC
RUN_DESC


Be careful with local and global variables

Consider this skeleton fragment:

    *SET X = 7
    *.
    *INCREMENT X FROM 0 TO 9 BY 1
    *CREATE SGS:   DIGIT  [*X]
    *LOOP
    *.
    *DISPLAY '[*X]'

What value of X will be displayed? The answer is 7. The reason for this is because the X referred to in the *SET and *DISPLAY directives is a different X than the one referred to in the *INCREMENT and *CREATE directives. The former is a global variable whereas the latter is a local variable, A *INCREMENT or *DO automatically creates a local variables that survives for the life of the loop.

This is SSG's sole attempt to provide scope for variables. Because it can lead to confusion, I believe it does more harm than good. I suggest never taking advantage of this feature--it's apt to confuse anyone who reads your skeletons.


Be careful with variables internal to a *COPY proc

If you write a *COPY procedure that uses variables that are purely internal to it, there's a danger that you'll choose variable names that will conflict with those used in skeletons that *COPY your proc. This danger arises from SSG's lack of proper variable scoping. To minimize (not eliminate) this possibility I use this convention: It's also a good practice to *REMOVE internal variables immediately before exiting a proc. This will hide the internals of your procs from callers (you don't want them to come to rely on proc internals that might change in the future). Example:

    *REMOVE VARIABLE WRDS__PER__TRK             . not to be used outside this proc


*LOOP does not take a variable name

Frequently people will code:

    *INCREMENT U TO [USERID]
    ...
    *LOOP U

Strictly speaking the *LOOP directive above is syntactically incorect. The SSG Programmer Reference Manual does not specify that *LOOP can take a variable name. But SSG does not flag this as a syntax violation because as soon as it sees '*LOOP ' it terminates scanning that line. There is some danger that a future release of SSG will implement a stricter syntax check and flag statements like this in error (although this change would break a million skeletons and so Unisys should think twice).

A syntactically correct way to indicate to the reader which *INCREMENT the *LOOP belongs to is to make use an in-line comment on the *LOOP:

    *INCREMENT U TO [USERID]
    ...
    *LOOP . U


Don't make the user get the case right

If your skeleton accepts alphabetic input from a demand user and that input must be all upper case (or all lower case), allow the user to enter it in either case and then translate to the desired case. For example:

    *ACCEPT USERID_IN 'Please enter user-id'
    *SET USERID = '[*USERID_IN,UCSTR$]'                . translate to upper case
    *REMOVE VARIABLE USERID_IN                         . no longer needed

This saves the demand user much frustration at the cost of very little code.


Use *IF COLUMN SEARCH wherever possible

Assume that the user supplies a pack-id and you must ensure that the pack exists by scanning a set of PACK SGSs. Further assume that field 1 of the PACK SGSs contains the pack-id. To determine whether the user's choice is valid you could code:

    *ACCEPT PACKID 'Please enter pack-id'
    *CLEAR PACK_OK_FLG                      . assume pack-id is invalid
    *.
    *INCREMENT P TO [PACK]                  . for each pack
    *IF [PACK,P,1,1]  =  [*PACKID,UCSTR$]   . if pack-id is what user specified
    *SET PACK_OK_FLAG                       .  indicate that we found it
    *EXIT P                                 .  no need to look further
    *ENDIF
    *LOOP
    *.
    *IF PACK_OK_FLAG IS SET                 . if user made good choice
    *RETURN                                 .  continue
    *ELSE
    *DISPLAY 'Error:  invalid pack-id'
    *ENDIF

Or you could code:

    *ACCEPT PACKID 'Please enter pack-id'
    *IF COLUMN SEARCH FROM PACK,1,1,1 FOR [*PACKID,UCSTR$]   . if pack-id is valid
    *RETURN                                                  .  continue
    *ELSE
    *DISPLAY 'Error:  invalid pack-id'
    *ENDIF

With *IF COLUMN SEARCH your code is simpler and faster. In this case, we avoided a loop and a flag variable.


Avoid *SORT on large SGS sets

One situation where you may find that SSG performs too poorly is *SORT on a large set of SGSs. This has become a more severe problem since SSG 23R1 greatly expanded the amount of memory SSG could use (and hence the size of the SGS sets it could manipulate). Unisys did not enhance the performance of *SORT to scale appropriately. *SORT does not call the Sort/Merge package.

I find that if I've got 1000 or more SGSs to sort performance may be unacceptably slow. And it seems to scale exponentially, although I haven't run any careful tests. If you've got 50,000 SGSs to sort, forget it.

The alternative is to write the SGSs to a file, use @SORT (or the @ZIP SORT command which calls Sort/Merge), and then re-invoke SSG to process the sorted SGSs. I've cut large sorts from minutes to seconds by doing this.


Check ERRCNT$ immediately before skeleton termination

By design, SSG does not perform ER ERR$ or ABORT$ when it encounters certain syntax errors, even in batch mode. The SSG manual does not document which syntax errors cause error termination and and which do not. Unisys says they have no list. This leads me to believe that this action isn't really by design--it's just a bug they don't care to fix (and which fixing would anger some people who've come to rely on SSG ignoring certain errors!). Here is an example of a syntax error that does not cause ERR$ or ABORT$:
    *DISPLAY 'This message lacks a closing quote

In cases like this, a job with a syntactically incorrect skeleton (for which SSG will not @ADD any generated ECL) can FIN normally. Sometimes the syntax error won't be apparent because it'll be buried in a rarely-used path within the code. SSG does increment ERRCNT$, however, on any syntax error. Thus, you can catch errors by executing this as the last code before skeleton termination:

    *IF ERRCNT$ IS SET  AND  [INFO$,1,5,1] <> DEMAND
    *DISPLAY 'Fatal:  ERRCNT$ has been set during skeleton processing'
    *ABORT                 . or *MESSAGE,X if you want ER ERR$
    *ENDIF

I use this so frequently that I've created a *COPY proc for it. I suggest putting it in all important batch skeletons.



Revised 1998-05-18