Computing: DOS, OS/2 & Windows Programming

Developing PL/I programs on DOS.

PL/I (Programming Language One), sometimes written PL/1, is a procedural, imperative computer programming language initially developed by IBM. It is designed for scientific, engineering, business and system programming. It has been in continuous use by academic, commercial and industrial organizations since it was introduced in the 1960s. PL/I's main domains are data processing, numerical computation, scientific computing, and system programming. It supports recursion, structured programming, linked data structure handling, fixed-point, floating-point, complex, character string handling, and bit string handling. The language syntax is English-like and suited for describing complex data formats with a wide set of functions available to verify and manipulate them (for further details, please have a look at the PL/I article in Wikipedia).

PL/1 includes elements of both COBOL and FORTRAN, thus is suitable for both scientific and business (plus system programming); it's also more structured than these two languages. However, I think that it was lots less used, and if, then primarily on IBM mainframes. But, there were (and are) PL/I compilers for PC. This tutorial is about the Digital Research PL/I 1.0 compiler for DOS, that I installed and tried out on my FreeDOS 1.3 RC5 VMware virtual machine. It should be similar on other DOS systems.

I downloaded the software from the PL/I compiler for CP/M and later also for DOS... page at the z80.eu website (you can also get it from WinWorld). There are 4 archives available for download: The first one contains all the files that you need to develop PL/I programs on DOS (including example source code), the second one contains an alternate library that should allow to read DOS files not created by Digital Research PL/I (I actually didn't try this out), and the other two contain the documentation (that is also available at the WinWorld site).

I unpacked the first archive on my Windows 10 and copied the extracted files onto a floppy diskette. On my FreeDOS, I created the directory C:\PLI and copied the files from the diskette to there. Supposing that the current directory is C:\
    MKDIR PLI
    CD PLI
    A:
    COPY *.* C:

The screenshot shows the content of my C:\PLI directory. PLI.EXE is the compiler, LIN86.EXE the linker, files with a .PLI extension are PL/I sources (the program samples).

PL/I on FreeDOS: Files of the Digital Research PL/I 1.0 compiler for DOS

There is no IDE or programming editor included with the install files. You can write your sources in the FreeDOS editor or in an editor on your Windows system (note that Notepad++ has no syntax highlighting for PL/I). The build of the program has to be done from the command line. Building a PL/I program requires 2 steps:

  1. Compiling the source file (file extension .PLI) to produce a relocatable object file (file extension .OBJ). PL/I is a 3-pass compiler (plus a pre-processor). Pass 1 collects declarations, and builds a Symbol Table used by subsequent passes (file with extension .SYM). Pass 2 processes executable statements, augments the Symbol Table, and generates intermediate language in tree-structure form. Both passes analyze the source text using recursive descent. If there aren't any errors during Pass 1 and Pass2, Pass 3 performs the actual code generation, and includes a comprehensive code optimizer that processes the intermediate tree structures. Alternate forms of an equivalent expression are reduced to the same form, and expressions are rearranged to reduce the number of temporary variables.
  2. Linking the object file (eventually together with other object files) to produce the executable (file extension .EXE).

The code of the compiler executable is contained in several parts, the main executable PLI.EXE and three overlay parts (PLI0.OVR, PLI1.OVR, PLI2.OVR), that actually contain the code of the three compiler passes respectively. This is good for us, because it minimizes memory usage, what on DOS can be a real issue. The disadvantage is that it's not possible to just add C:\PLI to the path, but it's mandatory to set the current directory to C:\PLI when running the compiler.

Here are the commands to be used to build a PL/I executable:
    PLI <source-filename>.PLI
    LINK86 <object-filename>.OBJ

This is fine if there are no, or only a few compilation errors. In the case, where there are lots of mistakes in your source code, you'll see only the few last error message, in particular if your display is set to 25 lines. A simple work-around is to redirect the compiler output to a text file, what may be done using the DOS "output to file" symbol ">"
    PLI <source-filename>.PLI ><compiler-output-filename>.TXT

One of the sample programs included with the install files is DEMO.PLI. It's a simple "Hello user" program, that asks for the user's name and then displays a greeting message. Here is the code.

    demo:
        procedure options(main);
        declare
            name character(20) varying;
        put skip(2) list('PLEASE ENTER YOUR FIRST NAME: ');
        get list(name);
        put skip(2) list('HELLO ' || name || ', WELCOME TO PL/I');
    end demo;

Here are some details concerning the PL/1 programming language (with a comparison of the syntax to Pascal). A PL/1 program consists of a series of code blocks, following one another, or included one within another. The outer bloc, including all the others corresponds to the Pascal statements
    PROGRAM program-name
    BEGIN

        ...
    END.
and has the following form in PL/1:
    program-name:
        PROCEDURE OPTIONS(MAIN);

        ...
    END program-name;
where "program-name" is a label, giving a name to the bloc. Note the differences with Pascal: blocs have no begin and the end statement includes the bloc label; in PL/1, the final end statement is terminated (as all other statements) with a semicolon (not with a dot as in Pascal).

The outer bloc contains the code of the main program - procedure options(main) - and may contain further blocs, primarily subroutines and functions (corresponding to procedures and functions in Pascal). Note that loop and conditional instructions define a bloc, too (mostly without a label, but terminated with end;).

Procedure and function blocs normally start with the declaration of the constants and variables, what is done as follows:
    DECLARE
        variable-name variable-type;
as for the variable "name" in our sample program.

You may declare several variables of being of the same type by including them between brackets; you may use a single declare directive, by separating the different declarations with a comma. Example:
    DECLARE
        (wrd1, wrd2) CHARACTER(254) VARYING,
        i FIXED BINARY;

The PL/1 data types are somewhat special. Concerning arithmetic data types, we have fixed binary (corresponding to the Pascal type Integer), float binary (corresponding to the Pascal type Double), and fixed decimal that, internally coded in BCD, have a defined precision (total number of digits; 1 to 15) and a defined scale (number of digits to the right of the decimal point; 0 to 15). This data type may be used for both integer and decimal numbers; it is particularly useful in business applications, that require exact representations of euros and cents and cannot accept the truncation errors of binary arithmetic.

The PL/1 data type character is quite the same as string in Pascal. Limited to 254 bytes, it may have a fixed length (given between brackets in the declaration), or a variable length (by adding the word varying in the declaration); in this latter case, the number given between brackets indicates the maximum length of the string. Note the usage of round brackets (and not square brackets as in Pascal). Example of a string declaration: in our sample program, the variable "name" can be assigned a variable length string up to 20 characters.

The definition of a string data type (and considering a character as a one byte string) is lots more convenient for the programmer than defining a character data type and considering a string as an array of characters (as does C, for example). In fact, we can directly assign a string constant (literal), or another variable to a string variable, using the assignment operator = (:= in Pascal). We can also directly add (concatenate) two strings using the operator || (corresponding to . in Perl; in Pascal, + is used for both adding numbers and concatenating strings). Concerning functions operating on strings, lets mention two of them, we often need: length() returns the actual length (number of characters) of a string (just as in Pascal); substr() returns a substring (just as the Pascal function with the same name; and as in Pascal, the first character position is 1).

PL/I allows one- or multi-dimensional arrays of all other data types. They are declared by adding the array size(s) or the range of their index(es) between round brackets (not square brackets as in Pascal) behind the variable name. Important to note that arrays declared using there size are one-based, i.e. the index of the first element is 1 (and not 0, as in most programming languages). Examples:
    DECLARE
        a(100) FIXED BINARY,
        b(2, 10) FLOAT BINARY,
        c(0:999) CHARACTER(1);
where, compared to Pascal, the first declaration corresponds to A: array[1..100] of Integer, the second one to B: array[1..2, 1..10] of Double (two-dimensional array), and the third one to C: array[0..999] of Char (usage of an array of one-character strings instead of strings, if the string length exceeds 254). Note that array elements are referenced by placing the index between round brackets (not square brackets as in Pascal).

PL/1 includes several other data types: pointer (that exist in most programming languages), bit (we'll see an example at the end of this tutorial), structures (hierarchical constructs similar to those that you have in COBOL), files (of course...), and labels and entries (PL/I language specific).

Arithmetic operators include the usual +, -, *, and /, plus ** (exponentiation = power). Comparison operators are =, <, >, >=, and <= (note that the equal sign is used for both the comparison operator "is equal to" and the assignment operator!), and their "not forms" (for =, <, and >), obtained by placing the symbol ^ or ~ in front of them (~=, ~< ~>). Logical operators (used with the bit data type) are: ^ or ~ (not), & (and), | or ! or \ (or). And, as already mentioned, the strings concatenation operator || (or !! or \\).

Reading data from the keyboard (or from a file) is done using the instruction get, writing data to the screen (or to a file) using the instruction put. Our sample program uses unformatted input-output, that is done using the instructions under the form
    GET LIST(variable1, variable2, ...)
;     PUT LIST(variable1, variable2, ...)
;
The instruction skip(n) may be used to advance n lines (and position the cursor to the begin of the line). A skip(1) executed before or after a put thus corresponds to a "new line" command (writing the ASCII codes for carriage return + line-feed on DOS or Windows). The two instructions can also be used to do formatted input-output. We'll see an example of formatted output further down in the text.

Somewhat long these explanations, but I think that if you never saw a PL/1 program before, they give you a general base to understand the code of our sample program and the other samples included with the install files or shown in this tutorial.

The screenshots below show the compilation (screenshot on the left) and linkage (screenshot on the right) of DEMO.PLI.

PL/I on FreeDOS: Compiling the sample program DEMO.PLI
PL/I on FreeDOS: Linking the object file resulting from the compilation of the sample program DEMO.PLI

And the following screenshot shows the files that were created during the build: DEMO.OBJ, the relocatable object file (serving as input to the linker); DEMO.SYM, the symbol table listing (as information for the programmer), and DEMO.EXE, the DOS executable, as well as the execution of the program with the input of a name and the output of the related greeting message.

PL/I on FreeDOS: Running the executable build from the sample program DEMO.PLI

I could terminate the tutorial here, but I think that it might be helpful, if I share "my first steps in PL/1 programming", 3 small programs, that I wrote after having installed Digital Research PL/1 for DOS. You can view them here, or download the source code.

The program FIBO.PLI calculates the first 20 items of the Fibonacci sequence. It's a sequence F(n) of integer numbers defined by F(n) = F(n-1) + F(n-2), with F(0) = 0 and F(1) = 1. Here is the code.

    fibo:
        procedure options(main);
        declare
            i fixed,
            fib(20) fixed;
        fib(1) = 0;
        fib(2) = 1;
        do i = 3 to 20;
            fib(i) = fib(i - 1) + fib(i - 2);
        end;
        put skip(1) list('Fibonacci numbers from 1 to 20:');
        do i = 1 to 20;
            put skip(1) list(fib(i));
        end;
    end fibo;

There shouldn't be a problem to understand the code. The numbers of the sequence are stored in a size-10 array, where each element (except for the first two, that are set to constant values) is equal to the sum of the two preceding ones. The calculations of these sums is done by a loop. The block
    DO counter-variable = start-value to end-value
        ...
    END;
corresponding to the Pascal bloc
    FOR counter-variable = start-value to end-value DO BEGIN
        ...
    END;

The screenshot shows the execution of the program. Don't ask me why there are all these spaces in front of the numbers; I have no idea. Anyway, in order to display the numbers as you want them to be displayed, you can use formatted output, as shown in the next program sample.

PL/I on FreeDOS: The Fibonacci sequence in PL/1 (program execution)

The program PASCAL.PLI calculates the first 15 rows of Pascal's triangle. This is a triangular array of the binomial coefficients arising in probability theory, combinatorics, and algebra. In much of the Western world, it is named after the French mathematician Blaise Pascal, although other mathematicians studied it centuries before him in Persia, India, China, Germany, and Italy. For details, cf. the corresponding article in Wikipedia.

The triangle consists of n rows of integer numbers, the first row containing a single element (the integer 1), the second row containing two elements (twice the integer 1), for all subsequent rows, the number of elements is increased by one and the value of a given element is either 1 (first or last element), or the sum of the two elements right above them (in the preceding row). The figure below shows the 8 first rows of Pascal's triangle. As a calculation example, lets take row 5. The first and last elements are 1, the others the sum of the elements above, so for the second and fourth element 1 + 3 (resp. 3 + 1) = 4, and for the third element 3 + 3 = 6. So, the fifth row is: 1 4 6 4 1.

The first 8 rows of Pascal's triangle

My program PASCAL.PLI is a simplification of the task, in the sense that it simply displays one row beneath the other, without recreating the triangular structure. Here is the code.

    pascal:
        procedure options(main);
        declare
            (nrows, row, i) fixed,
            values(15, 15) fixed;
        nrows = 15;
        values(1, 1) = 1;
        values(2, 1) = 1;
        values(2, 2) = 1;
        do row = 3 to nrows;
            values(row, 1) = 1;
            values(row, row) = 1;
            do i = 2 to row - 1;
                values(row, i) = values(row - 1, i - 1) + values(row - 1, i);
            end;
        end;
        do row = 1 to nrows;
            do i = 1 to row;
                put edit (values(row, i), ' ') (f(4), a);
            end;
            put skip(1);
        end;
    end pascal;

The program demonstrates the usage of a two-dimensional array. It also shows an example of formatted output. The general format of the formatted output instruction is as follows:
    PUT EDIT (format1, format2, ...) (variable1, variable2, ...);
what means that variable1 is displayed using format1, variable2 is displayed using format2, etc.

The instruction (very similar to the C printf function) is used in our sample program to display the triangle values (elements of the two-dimensional array) properly right-aligned one beneath the other and leaving a space between two values of the same row. So, for a given row, for each value in this row, we display the array element using format f(4), followed by a space displayed using format a. After all values in the row have been displayed, we use put skip(1) to move to the next display line.

Concerning input-output formats, lets just mention that a(w) reads or writes the next alphanumeric field whose width is specified by w, with truncation or blank padding on the right. If you omit w, the A format uses the size of the converted character data as a field width, and that f(w[,d]) reads or writes fixed-point arithmetic values with a field width of w digits, and d optional digits to the right of the decimal point. Numbers written using the F format will be right-aligned (blank padding on the left). For details, please, have a look in the PL/I Language Programmer's Guide.

The screenshot shows the execution of the program.

PL/I on FreeDOS: Pascal's triangle in PL/1 (program execution)

The program PALDR.PLI checks if a word, entered by the user, is or is not a palindrome, i.e. a word, that read from left to right, is identical to this word, read from right to left. Here is the code.

   paldr:

      procedure options(main);
      declare
         word character(254) varying;
      put skip(1) list('Please enter a word? ');
      get list(word);
      if is_palindrome(word) then
         put skip(0) list('This is a palindrome');
      else
         put skip(0) list('This is not a palindrome');

      is_palindrome:
         procedure (wrd) returns (bit(1));
         declare
            wrd character(254) varying,
            upper character(26) static initial
               ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),
            lower character(26) static initial
               ('abcdefghijklmnopqrstuvwxyz');
         wrd = translate(wrd, lower, upper);
         return (wrd = reverse(wrd));
      end is_palindrome;

      reverse:
         procedure (wrd1) returns (character(254) varying);
         declare
            (wrd1, wrd2) character(254) varying,
            i fixed;
         wrd2 = '';
         do i = 1 to length(wrd1);
            wrd2 = substr(wrd1, i, 1) || wrd2;
         end;
         return (wrd2);
      end reverse;

   end paldr;

The major new PL/1 feature in this sample program is the usage of procedures, more exactly the usage of functions. As a difference with Pascal, PL/1 uses the reserved word procedure for both independent program blocs that return a value (these are called functions) and blocs that don't return a value (called subroutines in PL/1, procedures in Pascal). A PL/1 function is declared as follows:
    function-name:
        PROCEDURE(arg1, arg2, ...) RETURNS return-item-data-type;
        DECLARE arg1 data-type1, arg2 data-type2, ...;

        ...
        RETURN(return-item);
    END function-name;
what corresponds to the Pascal declaration:
    FUNCTION function-name(arg1: data-type1; arg2: data-type2, ...): return-item-data-type;
    BEGIN

        ...
        RESULT := return-item;
    END;

The program PALDR.PLI asks the user for a word, and then checks if it is or is not a palindrome; note that this check is case-independent. The check is actually done by the function "is_palindrome", that has a return-item of data type BIT(1), that is similar to the Pascal data type BOOLEAN. Thus, the test if is_palindrome(word) then ... means that we check if the return value of the function is equal to '1'B (a bit set to 1, corresponding to True in Pascal).

The program considers that a word is a palindrome if it is not changed when reversing it. Determining the reversed word is done by the function "reverse" that, with the user word as argument, returns the reversed word. All that the "is_palindrome" function has to do is to compare the user word with the reversed word, and to return '1'B if they are equal, '0'B otherwise. You could do this with an IF ... ELSE statement, of course, but it's easier done with return (wrd = reverse(wrd)), what is nothing else than returning the result of the comparison for equality of the word and its reverse.

Two further remarks concerning the code of this program. First, note the usage of the reserved words static initial to declare a constant. Second, the internal function translate, is used to transform one string to another (here lowercase letters to uppercase); its way to work is similar to tr in Perl.

And do be complete, here is a screenshot that shows the execution of my palindrome program.

PL/I on FreeDOS: Palindrome check in PL/1 (program execution)

If you find this text helpful, please, support me and this website by signing my guestbook.