IN PRAISE OF PL/I

By Lou Marco

What programming language has been available for more than 20 years, is used by hundreds of thousands of mainframe programmers, has clear, crisp syntax and supports the following features?

The answer is Programming Language/I (PL/I). This article discusses some powerful features of the PL/I programming language -- those marked with ** in the preceding list. The article also covers the new PL/I features found in IBM's PL/I compiler for OS/2. It concludes with some comments on the possible future of PL/I in today's I/S shop.

[The article also applies to Enterprise PL/I, VisualAge PL/I, and AIX, as the OS/2 PL/I compiler has been ported to those systems - Eds]

Refer to Table 1 for a list of programming languages and language features. Note that PL/I stacks up nicely against other, even newer, programming languages.

Start Table 1:

____________________________________________________________________

Language Feature		PL/I	COBOL	Pascal	Ada	C	REXX

Data Typing			Rep	Rep	Strong	Strong	Strong	Weak

Dynamic Memory Allocation	Yes	No	Yes	Yes	Yes	Yes

Pointer Usage/			Yes	Yes	Yes	Yes	Yes	No
Pointer Arithmetic		Yes	No	Yes	Yes	Yes	No

User-Written Functions		Yes	No	Yes	Yes	Yes	Yes

Approx # Built-in Functions	100	None	20	50	100	150

Multi-tasking			Yes	No	Yes	Yes	Yes	No

Generic Procedures		Yes	No	No	Yes	No	No

Parameter Passing		Ref/	Ref/	Ref/	Ref/	Ref/	Ref/
Mechanism			Val(1)	Val	Val	Val	Val(2)	Val

Parameters of Adjustable Extents Yes	No	No	Yes	No	Yes

Condition Trapping		Yes	Yes	No	Yes(3)	No	Some

DBMS Connectivity		IMS/DB2	IMS/DB2	DB2	DB2	DB2	DB2(4)

Local Variables			Yes	No	Yes	Yes	Yes	Yes

Array Support(5)		Much	Some	Much	Much	Some	Some

PROCEDURE data type		Yes	No	Yes	Yes	No	No

Object Extensions		No	Yes	Yes	Yes	Yes	No
_______________________________________________________________________________
(1) Pass by value done by forcing PL/I to evaluate arguments as expressions
    [PL/I programmers on the mainframe, AIX & OS/2 can specify
    the attributes BYADDR and BYVALUE for parameters, in addition to
    using an expression to indicate call-by-value. -- Eds]
(2) Pass by reference done by passing a pointer to variables
(3) User-written exception handlers
(4) Available via third-party software vendor
(5) Some: Define arrays and pass arrays as arguments
    Much: Some, plus performing arithmetic on arrays and referencing
    array cross sections.
_______________________________________________________________________________
End Table 1: Language Comparison Chart (MVS Implementations)

Dynamic Memory Allocation

PL/I gives programmers great flexibility in storage use. PL/I programmers can have their programs create and destroy data structures as the program executes. This feature comes in handy when programmers need a data structure of undetermined size, such as a stack or linked list.

Refer to Example 1 for a possible (but not unique) PL/I stack implementation. The stack itself has the CONTROLLED storage class, which means the program will create and destroy stack elements during execution. The ALLOCATE statement creates a new generation of the data structure -- a character string of varying length in this example. The FREE statement releases the memory held by the string created by the last ALLOCATE command.

Start Example 1:

_______________________________________________________________________________
STACK:PROC;
        DCL STACK CHAR(32767) VARYING CONTROLLED;

POP: ENTRY RETURNS( CHAR(32767) VARYING );
        DCL STACK_ELEMENT CHAR(32767) VARYING;

        STACK ELEMENT = "";
        IF ^STACK_IS_EMPTY() THEN
           DO;
                STACK_ELEMENT = STACK;
		FREE STACK;
           END;
        RETURN( STACK_ELEMENT );

PUSH : ENTRY( STACK_ELEMENT );
        DCL STACK_ELEMENT CHAR(32767) VARYING;

        ALLOCATE STACK;
        STACK = STACK_ELEMENT;

STACK_IS_EMPTY:ENTRY RETURNS( BIT(01) );
        RETURN( NUMBER_OF_ELEMENTS() = ZERO );;

NUMBER_OF_ELEMENTS: ENTRY RETURNS( FIXED BIN(15) );
        DCL ALLOCATION BUILTIN;
        DCL ZERO FIXED BIN(31) INIT(0B);

        RETURN( ALLOCATION( STACK ) );
END STACK;
_______________________________________________________________________________
End Example 1: PL/I Stack Implementation

The calling program invokes the stack routines with these statements:

STACK_ELEMENT = POP() ;
ELEMENT_SUCCESSFULLY_ADDED = PUSH( STACK ELEMENT );
Example 1 shows an interesting feature of PL/I: the ability to declare multiple entry points within one load module. The main routine STACK contains routines POP, PUSH, IS_STACK_EMPTY and NUMBER_OF_STACK_ELEMENTS in one load module. This feature helps package related routines; everything related to stack processing is in one place.

Array Support

PL/I contains extensive support for arrays. PL/I programmers can create arrays up to 49(!) dimensions. The only limitation is that any dimension cannot exceed 32K elements. [this limit was true for the older PL/I-F compiler, but is increased to 2Gbytes in both the mainframe, AIX, Windows PL/I, and OS/2 PL/I products. -- Eds].

PL/I supports array arithmetic. For example, if the arrays A, B and C have identical dimensions and contain numbers, the following expressions perform element-by-element operations:

C = A + B: C = A - B; C = A * B; C = A / B;
A handy PL/I array feature is starting the array index with any integer. For example, assume the following array holds average temperatures by year and day for the 1990s:
TEMPERATURE( 1990:1999, 365 )
The average temperature for January 1, 1993 is:
TEMPERATURE( 1993, 1 )
Negative integers and zero are also valid PL/I array subscripts.

PL/I programmers can pass entire arrays as arguments to procedures. They can use or pass array Cross Sections as arguments. A Cross Section is a piece of an array composed from one or more adjacent dimensions.

Assume the following array holds claim counts by state, by county within each state and by zip code within each county (assume no state has more than 255 counties and no county has more than 120 zips):

CLAIM_COUNTS( 52, 255, 120 )
Assume the routine COMPUTE_STD_DEV takes a one-dimensional array as an argument. These expressions pass a Cross Section of CLAIM_COUNTS to COMPUTE_STD_DEV:
STD_DEV = COMPUTE_STD_DEV(CLAIM_COUNTS(24,10 *));
STD_DEV = COMPUTE_STD_DEV(CLAIM_COUNTS(*, 1, 1));
This expression adds up the counts for state 2, county 5:
SUM( CLAIM_COUNTS( 2, 5, * ) )
This expression adds up the counts for state 25:
SUM( CLAIM_COUNT5( 25, *, * ), )

Parameter Passing In PL/I

The default parameter passing mechanism in PL/I is reference. Passing parmameters by reference means the runtime environment passes the starting address and a description of the variable, not the variable itself. In effect, the declared variables in the calling and called routines share the same storage.

PL/I programmers can easily pass arguments by value. Passing arguments by value means the runtime environment generates a copy of the argument and makes the copy known to the called program. The declared variables in the calling and called procedures do not share the same storage.

PL/I programmers enclose the arguments in parentheses to pass by value. For example:

CALL X( A, B, C );		A, B, C passed by reference
CALL X( (A), (B), (C) );	A, B, C passed by value
CALL X( A, (B), C );		A and C passed by reference, B passed by value.
[PL/I programmers on the mainframe, AIX & OS/2 can specify the attributes BYADDR and BYVALUE for parameters, in addition to using an expression to indicate call-by-value. -- Eds]

PL/I routines accept parameters of varying extents. A parameter of varying extent has no true length or size until the calling program passes the argument. For example, a PL/I programmer can write a routine to reverse the order of the characters in a character string of any allowable length. This, of course, helps programmers write reusable routines.

Arrays can also be of varying extents. PL/I programmers can write a routine to sort an array of 10, 100 or 30,000 elements. The only requirement is that the variables in the calling and called routines have the same number of dimensions.

PL/I programmers use built-in functions to determine the parameter's true length or size. The LENGTH built-in function returns a string's length, and the HBOUND function returns the number of elements in a dimension of an array. [HBOUND gives the upper bound of an array. HBOUND() - LBOUUND () + 1 gives the extent -- Eds]

Multitasking

One of PL/I's most interesting (and probably underused) features is its ability to spawn concurrently executing tasks. These tasks can execute independently (asynchronously) or execute in a coordinated fashion (synchronously).

PL/I programmers can use asynchronous tasking to perform complex calculations on large arrays. One task can process the even-numbered array elements and another, separately executing task, can process the odd-numbered array elements. PL/I programmers can use synchronous tasking to simultaneously read and write files. One task can read the file; another task can write a different file.

PL/I programmers use event or task variables to spawn and coordinate tasks, with two built-in functions -- STATUS and COMPLETION. PL/I programmers can prioritize tasks with the PRIORITY option and check the priority of any executing task with the PRIORITY built-in function. PL/I performs task rendezvous with the WAIT statement.

See Example 2 for a PL/I code block that spawns two asynchronous tasks. The overall idea is to set the task status to normal and completion value to incomplete. The program generates the tasks, then waits for their completion. The program ends by resetting the status and completion values using the built-in functions.

Start Example 2:

_______________________________________________________________________________

TASKER:PROC OPTIONS(MAIN REENTRANT);
        DCL TASKS( 2 )          EVENT;
        DCL TASK_READY(2)       EVENT;
        DCL TASK_COMPLETE(2)    EVENT;
        DCL THE_TASKS(2)        ENTRY           INIT(TASK1,TASK2};
        DCL NUMBER_OF_TASKS     FIXED BIN(15)   INIT(2);
        DCL TASK_INDEX          FIXED BIN(15);
        DCL (STATUS,COMPLETION) BUILTIN;

        DO TASK INDEX = 1 TO NUMBER_OF_TASKS;
                STATUS( TASK_READY(TASK_INDEX ) ) = 0;
                CALL THE_TASKS( TASK_INDEX ) ) EVENT( TASKS( TASK_INDEX ) );
                COMPLETION( TASK_READY (TASK_INDEX ) ) = '1'B;
        END;

        WAIT( TASK_COMPLETE(1), TASK_COMPLETE(2) );

        DO TASK_INDEX = 1 TO NUMBER_OF_TASKS;
                COMPLETION( TASK_COMPLETE (TASK_INDEX ) ) = '0'B;
                STATUS( TASK_READY( TASK_INDEX ) ) = 4;
                COMPLETION( TASK_READY( TASK_INDEX ) ) = '1'B;
                WAIT( TASKS( TASK_INDEX ) );
        END;
END;
_______________________________________________________________________________
End Example 2: PL/I Spawns Two Asynchronous Tasks

The situation for synchronous tasking is more complicated than the asynchronous one. In synchronous tasking, the programmer must assume responsibility for correct task coordination. The two big problems that arise with synchronously tasking programs are the use of shared memory and task locking.

Shared memory refers to two or more tasks referencing and changing the same variables. The programmer must ensure that each task receives the data from shared memory that the task expects. Remember that just because you call Task A before Task B, you have no guarantee that Task A begins execution first.

Task locking refers to a task entering a WAIT state because of activity in another task. The simplest case is that Task A must wait for some event to complete in Task B, and Task B must wait for some event to complete in Task A.

PLITEST, the IBM source-level debugger for PL/I, supports multitasking. As you step through your program, PLITEST jumps from task to task. You can step through your program and examine the contents of shared memory. If PLITEST hangs on a WAIT statement, your tasks are locked.

[Mainframe PL/I also provides the interactive debugger CODE/370. --Eds]

You must allocate the PL/I multitasking libraries to DDName SYSLIB when linking multitasking PL/I programs.

Built-In And User-Written Functions

None of these language features is possible without using PL/I built-in functions. Every example shown uses these functions. They allow the PL/I program to determine facts about data and change it.

PL/I built-in functions deal with arithmetic, mathematical computations, I/O, tasking and character strings. If you want to do something to, or learn something about, a variable, look first to PL/I's extensive built-in function library. You could save yourself plenty of code.

PL/I also supports user-written functions. A user-written function is a PL/I routine (internal or external) that returns a single value. Syntactically, you use a user-written (or built-in function) in expressions as you would a variable or constant. Semantically, the runtime replaces the function's returned value with the function reference.

Condition Trapping

PL/I programmers can code routines to trap and handle conditions, regardless of where these conditions occur in the program. The only requirement is that the condition trapping code must appear before the condition it traps can occur. When the trapped condition occurs (the PL/I term for this is "raised"), the program transfers control to the trapping code, executes and returns.

For example, if a program requirement is to replace invalid data items with a zero, the condition trapping code can be:

ON CONVERSION BEGIN ;
	DCL ONSOURCE BUILTIN ;
	ONSOURCE() = '0';
END ;
[text and example changed to correct an error -- Eds.]

ONSOURCE is a built-in function that returns the invalid data. Here, the program uses ONSOURCE as a pseudovariable, which replaces the invalid data with a zero and blanks. Note that the program in Example 2 uses the built-in functions STATUS and COMPLETION as pseudovariables.

PL/I allows programmers to detect any errors that may arise in the program. For example, the RECORD condition means the program found a mismatch between the file's logical record length and the data structure to hold the file's record. The RECORD condition is actually two separate conditions: the LRECL could be shorter or longer than the data structure. If programmers need to know which condition arose, they can use the ONCODE built-in function to return a four-digit code. The trapped condition is the ERROR condition. For example:

ON ERROR BEGIN :
	DCL ONCODE BUILTIN ;
	SELECT ( ONCODE() )
	    WHEN ( 20 )
		PUT SKIP LIST( 'LRECL Longer Than Data Structure');
	    WHEN ( 21 )
		PUT SKIP LIST( 'LRECL Shorter Than Data Structure);
	    OTHERWISE
		PUT SKIP LlST('Unknown Error - ', ONCODE( ) );
	END;
END ;

PL/I Package/2

PL/I Package/2 is IBM's PL/I implementation under OS/2. [called PL/I for OS/2 from 1994. -- Eds] IBM added features to the OS/2 PL/I compiler not found in the MVS product. These features include the following.

IBM will no doubt provide these features in its MVS PL/I compiler soon -- maybe with the release of MVS 5.2.2.

[These features have now been available on the mainframe, Windows, and AIX for some years. -- Eds]

The Future of PL/I

The future holds some surprises for PL/I programmers. Many believe PL/I is a dying language. Those with long memories recall that industry pundits have also predicted the death of COBOL for 20 years! If you browse SHARE or GUIDE literature, you will see strong interest in PL/I. With PL/I now available under OS/2, you may see more PL/I programming in the future.

[IBM PL/I for OS/2 was released in 1994. IBM PL/I for AIX was released in September 1995, 3 months prior to the publication of this article. Liant's Open PL/I is available under Windows and many Unix platforms. Kednos's PL/I compiler is available on Digital Unix. --Eds]

PL/I programmers may find a home in COBOL/370. COBOL/370 (which some say suffers from PL/I envy) has language features found in PL/I. If your COBOL/370 application needs a user-written function library or some dynamic data structures, look to PL/I programmers; they may have experience coding these features.


ABOUT THE AUTHOR

Lou Marco has 13 years' experience in data processing training and I/S and is the author of "ISPF/REXX For Experienced Programmers" (ISBN I-87895650-7, CBM Books, Ft. Washington, PA). He is a computer scientist and mathematician at Shapiro Consulting Group, Inc., 1310 Highway 620 South, Austin, TX 78734, (512)263-7540.


The above article appeared in Enterprise Systems Journal, December 1995, pp. 32-37.
This article was edited by D. Jones & R. Vowels, to correct some errors.