Power vs. Adventure - PL/I and C

This paper was presented at the G.U.I.D.E.&SHARE Europe Joint Conference (10-13 October 1994, Vienna, Austria).

Eberhard Sturm
Universitätsrechenzentrum
Einsteinstr. 60
D-48149 Münster
Germany

Text marked up in cooperation with:

    Dave Jones
    Velocity Software

Please use at least WebExplorer Version 1.03 or NetScape Version 1.1 as WWW browser.


Abstract

The "old and honest" programming language PL/I seems to grow new blossoms; in OS/2 -- and soon AIX -- PL/I offers capabilities comparable to C. This presentation shows the differences in philosophy of both languages: PL/I allows problem-oriented programming without knowledge of low-level constructs, whereas C requires, for example, pointer programming even when reading a file. You should consider using PL/I instead of C if you prefer a programming style where the compiler does the tedious work for you.

The author is member of the scientific staff for application programming at the Computing Center of the University of Münster, Germany.


Introduction

This presentation is biased -- neither accidentally nor intentionally but necessarily. The reason is that programmers have their favorite programming language. Mine is PL/I but I studied C because there is a widespread operating system called Unix which I had to write programs for. Additionally I had many discussions with colleagues in my computing center comparing the two languages. Of course, I will always refer to OS/2 PL/I and ANSI-C because they are the modern versions!

First I want to present 5 theses: Too many things in C...

  1. are different from what they seem to be!
  2. are undefined!
  3. are poorly defined!
  4. cannot be checked by the compiler!
  5. are not checked by the compiler!
When I contrast the C paradigms to those of PL/I I will often return to these theses.

How does a program look?

Let's start with the outward appearance of a C and a PL/I program:
C PL/I
/* compilation unit: file */

#include 
static void subroutine (...);
float function (...);
float something;
static float anything;
extern float nothing;

int main (void)
  {...
  subroutine (...);
  ...
  }

void subroutine (...)
   {...
   printf ("%f", function(...));
   ...
   }

float function (...)
   {
   ...
   return ...;
   }
/* compilation unit: package */
Program: package exports (Main);

declare
  Something float external,
  Anything  float static;

Main: procedure options (main);
   ...
   call Subroutine (...);
   ...
   end;

Subroutine: procedure (...);
   ...
   put (Function(...));
   ...
   end;

Function: procedure (...)
          returns(float);
   ...
   return (...);
   end;
end;

As you can see, at least comments are the same in both languages. The first difference is the treatment of letters. C is case-sensitive, PL/I doesn't care. For example, don't write the word main in C in capital letters; you will get an error message by the linker! The main procedure in C always has to have the name main! PL/I handles the specification of a main procedure in the usual way: it introduces keywords: options (main). Here we come to the next difference: Keywords in C are reserved words, in PL/I it's clear from the syntax what is a keyword and what is not. In PL/I you can truthfully say "What you don't know can't hurt you!"

Both in C and in PL/I the unit of compilation is a file. But in PL/I there must be at least one complete procedure contained in the file; in the general case a file will contain a package as shown here. The advantage compared to C is that the compiler is able to recognize a corrupted file, perhaps caused by a file transfer.

The scope of functions and variables defined outside functions in C is global by default! The PL/I rule -- names are local to the package or procedure -- isn't as dangerous: You have to mention procedures in the export-option of the package statement and to declare variables external if you want them to be used by other routines. The method to reduce the scope in C is to define it as static. This treatment is a candidate for thesis 1! Static storage is storage that lives during the lifetime of a program. And that is true whether you specify static or not. static in this place means "local to the file"! Even the scope of a function can be reduced to the file by declaring it "static".

If you wonder why there is a #include-line in the C program: input and output is done by library functions, and you have to include the function declarations in order to be able to do I/O.

The two lines following in the C example have no counterpart in PL/I. They declare the functions coming afterwards in the file, for the purpose of correct parameter passing. This is neither necessary nor possible in PL/I because the compiler "knows" all procedures in the package, only procedures in other files have to be declared in an analogous manner. C differentiates between declaration and definition: the first is an association of attributes and the second an association of storage. In PL/I there are only declarations, a PL/I programmer doesn't even understand the problem! The extern-declaration doesn't reserve storage. Why it's called extern is mysterious: The definition may come in another file or in the same file later on!

Declarations and definitions are order-dependent in C and block-dependent in PL/I. This means that in C you cannot use something you have not at least declared previously in the file. In PL/I declare-statements may be in any order, relevant is only the block they are contained in. This makes life easier for PL/I programmers!

Obviously there are two kinds of subprograms in PL/I and only one in C. The subprogram subroutine is called by a call statement in PL/I and only by referencing the name in C. C programmers praise this fact as a clean solution. But referencing thesis 4 I would reply that a subprogram should only serve one of two purposes: either computing a value or providing an effect! This can be checked by a compiler and is, of course, checked by the OS/2-PL/I compiler: there must be either a returns-attribute and a return-statement with value or a return-statement without a value (or return by an end-statement). The methods of parameter passing will be explained later.


Signed Characters?

At first glance declarations (or definitions) look similar in C and PL/I:
C PL/I
const volatile float f = 1;

dcl F float init(1) nonasgn abnormal;
dcl F float nonasgn abnormal init(1);

But there could be no greater error! First you have to obey a certain order of keywords in C whereas PL/I only demands that the first keyword specifies the statement type. The attributes following the name of the variable may be in any order. Of course the initial value 1 has to follow immediately the keyword init! The keyword const in C means in fact nonassignable as it it called in PL/I! Recall Thesis 1! The following statements declare fixed-point datatypes:
C PL/I
char I;
signed char J;
unsigned char K;
short int L;
int M;
long int N;
/* no counterpart */
/* no counterpart */
dcl I char; /* ? */
dcl J fixed binary (7);
dcl K fixed bin (8) unsigned;
dcl L fixed bin (15);
dcl M fixed bin;
dcl N fixed bin (31);
dcl O fixed bin (31,16);
dcl S fixed dec (7,2);

If you specify only char, it depends on which C compiler you're using whether it's signed or unsigned. What adventurous consequences this has will be discussed in the operations part of this presentation. If you have never heard of signed characters you needn't worry! It's up to thesis 1: char means in fact small integer, as you see when you regard their PL/I counterparts! You feel the power of PL/I when you look at the various types on the right: Binary fractionals (you can find a long-winded C-style definition of them in OS/2 Presentation Manager manuals) and decimal numbers even with fractional parts. In PL/I you can declare in a problem-oriented fashion what precision you want, as it is for floating-point numbers:
CPL/I
float A;


double B;

long double C;

/* no counterpart */
dcl A float;
dcl A float decimal (6);
dcl A float binary (21);
dcl B float dec (16);
dcl B float bin (53);
dcl C float dec (33);
dcl C float bin(109);
dcl D float dec (12);


Non-Computational Datatypes?

Additionally there are non-computational datatypes in PL/I: area, entry, file, format, handle, label, offset, pointer, and task! Because of the little space in this presentation I will only comment on two PL/I counterparts to the C datatype pointer. First a typical usage in both languages:
CPL/I
char x, *p = &x;
*p = 'Z';
dcl X char, P ptr init(addr(X)),
    C char based (P);
C = 'Z';

The C definition means that x and *p each have one byte of storage. * in this context is the indirection operator: *p is the storage where p points to. In other words: p is a pointer to char. The syntax is slightly chaotic; on the one hand *p is of type char, on the other hand p is initialized to the address of x (by using the address operator &). It's unclear whether to group char *p or p = &x! The assignment means in fact that x gets the value 'Z'.

In the PL/I example the pointer p is "untyped". This is a point where using PL/I can get adventurous, but the advantage is that pointers in PL/I often only appear in declare statements as it is in this case. You have to define a character object C which is located where the pointer P points to. In the assignment no pointer is used, only the name of a based object.

The other example shows the usage of variable subprograms in the two languages:
CPL/I
float (*f) (float);

f = func;
printf ("%f", f(7.3));
dcl F entry (float) variable
      returns (float);
F = Func;
put (F(7.3));

The C philosophy is that f is a pointer to a function with parameter float which returns a float value. In PL/I there is no pointer needed, only the program control data type entry. The keyword variable is necessary because names of procedures are entry constants. You shouldn't confuse the C definition with the following: float *f (float); which means that f is a function which returns a pointer to float! By the way, what do you think is the result of quotient in the following example:
C
float *pdenominator; ...
quotient = nominator/*pdenominator /* ? */;

You think that nominator is divided by the value pdenominator points to? No, that's wrong (surprise, surprise)! The characters "/*" after nominator start a comment! And thinking of thesis 5: does your compiler complain?


What Is an Array?

In order to introduce character strings we have first to present arrays. Although there is a rudimentary concept of arrays in C, no "good" C programmer uses the rudimentary array notation directly (with the exception of character arrays). Instead they use pointers which are closely related to arrays in C. For example you want to copy array A to array B:
CPL/I
int i;
float a [10], b [10];
for (i = 1; i < 10; i += 1)
   a[i] = b[i];
dcl I fixed bin;
dcl (A, B init ((5)0,(5)1))
    dim (0:9) float;
do I = lbound(A) to hbound(A);
   A(I) = B(I);
   end;

The C philosophy is that array bounds start with 0 and extend to N-1, if N is the declared number of elements. In PL/I you can specify that you mean the pumpkin harvest of the years 1989 to 1993!
PL/I
dcl Pumpkin_harvest (1989:1993) fixed bin (31);

Of course this supports thesis 3 because no human starts counting from 0! And what do "real" programmers in C and PL/I do to improve the examples above?
CPL/I
float *pa, *pb;
float a [10], b [10];
for (pa=a, pb=b; p < a+10;
     *pa++, *pb++);
A = B;

Instead of arrays only pointers are used, with pointer arithmetic used exclusively! What advantage are C programmers hoping for? The PL/I compiler can do a better job of optimization than any C programmer! Here thesis 1 and 4 apply! An array is treated as a pointer and no subscript value can be checked by the compiler. Using pointers is not a natural way of programming arrays. They obscure inherently clear things! The most extreme example is the C definition that a[i] is the same as *(a+i). And this is the same as *(i+a). As you can easily conclude, they are all the same as i[a]!

Let A and B declared as above, then the following is possible in PL/I:
PL/I
dcl C float dim (0:9);
C = A + 2*B - 4;

Arrays are full fledged objects and not pointers in PL/I!


What Is a String?

Now to the translations of char to PL/I counterparts:
CPL/I
char c = ' ';
char s [10] = {'a','b','c'};
char t [4] = "abc";
t = s;     /* illegal */
t = "abc"; /* illegal */
strcpy (t, s);
dcl C char init ('Ä');
dcl S char (3) init ('abc');
dcl T char (3) var;
T = S;
T = 'abc';
T = T || 'xy';

The C paradigms know about character arrays like s in the example above and strings which extend up to a null byte like t. Here t effectively uses 4 bytes of storage, the length is maximal 3, and the subscript of the last character is 2! As arrays are in fact constant pointers you cannot assign s to t because you cannot assign a value to a constant!. The string constant "abc" may be assigned to t in the definition but not by a assignment statement! Indeed, the library function strcpy brings some limited help, but it will blindly move many bytes of s to t until it finds the null byte. There is no check possible whether there is enough space in t! In PL/I there is even a concatenation operator (||), a task which has to be done in C again by using a library function!

In PL/I, if you want to declare varying length strings ending with a null byte like in C, you can use the varyingz attribute. If you want to use varying strings of arbitrary contents (even ones containing null bytes in the midst of the string) PL/I gives you the opportunity to do so by specifying the varying attribute. There is no distinction between characters and strings. Both are scalars in PL/I. Of course, you can declare arrays of strings!


What Are Bits?

In C there are three concepts regarding bits. The first one treats int as a sequence of bits. In PL/I you have to use the pseudovariable and builtin function unspec if you want to treat anything as bit if it is not:
CPL/I
short int k;
k = k & 0x00FF;
dcl K fixed bin (15);
unspec(K) = unspec(K) & '00FF'x;

This is, of course, not really equivalent because unspec means bit representation whereas a hexadecimal constant in C represents an int! Of course, use of unspec may inhibit portability! In PL/I there is an extra datatype bit which can be used in an analogous manner to character strings, complete with builtin functions like substr, which can be modelled in C only in an arcane manner:
CPL/I
short int x;
k = (x>>(m+1-n))& ˜(˜0<<n);
dcl X bit (16);
K = substr(X,M,N);

The second concept in C uses structures; this, of course, is possible in PL/I, too:
CPL/I
struct {int a: 4;
 signed int b: 1;
        int c: 3;} byte;
byte.c = 2;
dcl 1 Byte unaligned,
      2 A bit (4),
      2 B bit,
      2 C bit (3);
C = '010'b;

Really strange is the treatment of bits as integers: The variable b can have only one of two possible values: 0 and -1!

The third point is that C uses int values for storing logical values: 0 means false and all other values mean true. In PL/I a bit string of length one is used to hold logical values. Thus there is only one set of logical operators in PL/I whereas C has two:
CPL/I
if (x > 0 && j == 2) ...
b = k < 0 & j == 1;
if K > 0 & J = 2 then ...
B = K > 0 & J = 2;

As you can see, C needs three concepts where PL/I has only one!


Static, Automatic, and more ...

As static and automatic are very similar in C and PL/I, there is one storage class you can find only in C: register. It expresses your wish to hold a variable in a register. There are two storage classes which can be found only in PL/I: controlled and based. Let's first consider an example of controlled:
PL/I
declare
   (K, L) fixed bin,
   A      dim (K) char (L) ctl;

K = 10; L = 10; allocate A;
  ...
K = 15; L = 29; allocate A;
  ...
put (A); free A; put (A); free A;

The controlled (abbrev. ctl) means that the variable initially has no storage allocated to it. Only after executing an allocate statement can you make a legal reference to it. After executing a corresponding free statement, the controlled variable is no longer available. A powerful use of this type of storage class is to make multiple allocations of a variable before freeing it. PL/I treats this as building a stack. Older generations of the variable are hidden in the meantime, and only the most recent allocation of the variable is available to the program. Even more remarkable is that array bounds and string sizes may be different in different generations.


Say It in the Declaration!

The PL/I attribute based can best be explained when defining chained (or "linked") structures. Look at the following example:
CPL/I
struct atype
      {struct btype *a1;
       float a2;};
struct btype
      {long int b1;
       float b2 [10];};
struct atype *a;

a = (struct atype *) malloc
    (sizeof(struct atype));
a->a1 = (struct btype *) malloc
        (sizeof(struct btype));

printf ("%f", a->a1>b2[3]);
dcl 1 A    based (P);
      2 A1 ptr,
      2 A2 float;
dcl 1 B    based (A1),
      2 B1 fixed bin (31),
      2 B2 float dim (10);


allocate A;

allocate B;


put (B2(3));

In PL/I you find complexity only in the declaration: A is where P points to, B is where A.A1 points to. After this you can forget the pointers and use only the object names A and B. In C, however, you have to use the pointers everywhere. Pointers are indeed typed in C, but even for the purpose of allocation you have to cast them to byte pointers!


How Many Must a Programming Language Have?

If you compare the following tables, what do you think? Is it really necessary to have 45 operators instead of 20? Is it really necessary to have 15 priority groups instead of 7?
C
priority direction operator
15 < () [] . ->
14 > ++ -- ˜ - + ! & * (typename) sizeof()
13 > * / %
12 > + -
11 > << >>
10 > < > <= >=
9 > == !=
8 > &
7 > ^
6 > |
5 > &.&.
4 > ||
3 < ?:
2 < = += -= *= /= <<= %= &= ^= |= >>=
1 > ,
PL/I
priority direction Operator
7 < + - ^ **
6 > * /
5 > + -
4 > = ^= < > <= >= ^< ^>
3 > ||
2 > &
1 > | ^

Even == and >= are in different precedence groups! ** and || of PL/I cannot to be found in C, and C's shift operators (the << and >> operators) are handled by builtin functions in PL/I.


Something to Do with Mathematics?

Many operations in C are done after the so-called usual conversions. So, what is done in the following C example:
C
signed char c = 0xFF;
if (c == 0xFF) ...
If you are lucky you will get a warning message for the initial assignment. The comparison will yield false in every case! This is because c is converted to int giving -1, 0xFF is converted to int giving 255! In the first case sign bits are added, in the second case zeroes.

If sizeof(int) is less than sizeof(long) the following comparisons are both true: -1L < 1U and -1L > 1UL! This is because conversion of signed long to unsigned long preserves the bit pattern, -1L is converted to the largest possible integer!

Let's consider the division of signed integers in C: -7/4 can yield -1 or -2, depending not on the language definition, but on the hardware the C program is running on. Unsigned int operations are carried out (silently!) in modulo arithmetic: 1 - 2 yields the largest possible integer!

You will not be astonished to hear that during the >> operation the sign bit or zeroes may be filled in, dependent not on the standard but on the hardware!


Is an Operator Allowed to Alter Its Operands?

There is no operator in PL/I which can alter its operands, but in C they abound! Because assignment is an operator you can write:
C
if (a = b) ...
In most cases you meant a == b. The resulting value of the assignment in C is the left operand after having done the side effect of assignment. The increment and decrement operators only establish a side effect. Thus, the following examples yield undefined results:
C
b = a++ + a;
a[i] = i++;

In both C and PL/I the order of evaluating an expression is undefined. But whereas this is important in PL/I only if you write functions that have side effects, this is possible in C by specifying operators. You may enter such strange situations as
C
if (a < b || c < d++) ...
where the value of d after evaluation depends on the values of a and b because in C the evaluation of a logical expression stops if the final result is already known. In this case d is not incremented!


Even German TV knows ...

The if statements in C and PL/I are very similar. Let's concentrate therefore on the comparison of the switch and the select statements:
CPL/I
switch (k) {
   int i = 1; /* ? */
   case 1: case 2:
      ...
      break;
   default:
      ...
      break;
   case 0:
      ...
      break;
   }
select (K);
   when (1, 2) do;
      ...
      end;
   when (0)
      ...
   otherwise do;
      ...
      end;
 end;

The switch statement is only a goto statement in disguise: dependent on the value of the control variable a jump is done to that case which matches the value. If you forget the break statement the next case is also executed. This is even known by German TV script writers. I watched a thriller last year where hackers broke into the computer at a bank. In the final take one of the hackers pointed to the screen and said to the detective: "Here, a break is missing in the switch statement, thus we fall into the next case and get unlimited authorization!" By the way, the initialization isn't done in a switch block!


Unsigned? Not Again!

When regarding loop statements we first encounter unsigned problems again:
CPL/I
unsigned int k = 10;
while (k-- >= 0) {
   ...
   }
dcl K unsigned fixed bin init (10);
do while (K >= 0);
   (size): K = K - 1;
   ...
   end;

The loop is intended to execute while k >= 0. Unfortunately k is decremented at 0 nevertheless. In C this doesn't yield -1 but the largest possible integer, in effect making this an infinite loop. In PL/I you have the same effect unless you allow the compiler to check whether the result of an assignment yields a value beyond the declared size. You do this by specifying the appropriate condition prefix. PL/I will issue an error message at execution time. This is typical for C and PL/I: C keeps quiet when something is going wrong, PL/I raises a condition.

If you want to enter a loop first and check it at the end, both languages offer a similar construct. But more interesting is a loop with a control variable:
CPL/I
for (i = 1; i <= 10; i++) {
    ...
    }
   do I = 1 to 10;
   ...
   end;

In C you have to write down the name of the control variable three times, thus allowing more chances for mistyping. In PL/I you say what you want. Confusing in C is that you can do almost anything between the parentheses of the for statement. Loop control is intended but not forced to be the only purpose. If you exploit the powerful possibilities of the PL/I do construct, it can be confusing, too:
PL/I
do K = 1 to 10, 9 to 1 by -1, 2 repeat 2*I while (I <= 1024);
   ...
   end;
The loop variable K takes the values 1 to 10, 9 down through 1, and then all powers of 2 up to and including 1024!


By Value or by Address?

In the beginning of this presentation we already saw that C has only functions. If you specify void as the return type it's in fact a subroutine in the PL/I sense. If you specify void as the parameter there is no parameter in fact. But let's now concentrate on the question of parameter passing. At first glance there are simple concepts in both languages:

The C paradigm brings problems when more than one value is to be returned. The solution is to pass pointers to values. The pointers cannot be altered but the values can! In the following example you can discover that arrays in C are only pointers in disguise:
CPL/I
int a [2] = {1,2}, k, *m;
void sub (int k, int *m, int a[]);
a++; /* illegal */
  ...
sub (k, &m, a);
printf ("%d %d %d %d",
        k, *m, a[0], a[1]);
  ...
void sub (int k, int *m, int a[]) {
   k++;
   (*m)++;
   a++; /* legal */
   a[0] += 1; /* ??? */
}
dcl (A dim (0:1) init (1, 2),
    K, M) fixed bin;

  ...
call Sub ((K), M, A);
put (K,M,A);

  ...
Sub: procedure (K, M, A);
dcl (K, M, A dim(*)) fixed bin;
K += 1;
M += 1;
A(0) += 1;
end;

The value of argument k is passed to sub, incrementing by 1 doesn't alter the contents of the argument. The integer value m points to can be altered because only the pointer is passed. But what about array a? It's declared as an array also in sub, but it's allowed to be incremented because it's in fact a pointer, too. Really adventurous is then the incrementing of a[0] because it's in fact incrementing a[1]!

In PL/I there is only a small problem: if the argument is an expression or has different attributes than the corresponding parameter, a copy is passed because a parameter cannot be identical to an expression. By enclosing K in parentheses you can therefore force passing by value. Arrays are also passed by address. They keep their object nature, and you can even inquire their bounds by using builtin functions.


Builtin or Not Builtin?

There again is an area where PL/I has only one concept to be learnt, but C, however, has four! The PL/I builtin function size is in C the operator sizeof! The PL/I builtin function abs is in C realized as two library functions: abs and labs (you yourself have to differentiate according to the type of the argument)! The PL/I builtin function huge returns the greatest possible value of a floating point variable; this is accomplished in C by three preprocessor values FLT_MAX, DBL_MAX, and LDBL_MAX, according to the type of the argument. The fourth case is casting: where C uses a type in parentheses in front of a variable, PL/I again has builtin functions. A PL/I programmer would find two types of casting in C:
CPL/I
x = (unsigned) h;
y = (float) k;
unspec(X) = unspec(H);
y = float(K);

The first case preserves the bit pattern which in PL/I can only be done by using the unspec pseudovariable and builtin function. The second case is identical in both languages: the value is preserved and converted appropriately.


Implementation Overkill

The C library isn't indeed mandatory in the ANSI standard but if it exists it must conform to the standard. I want to show only a few examples but they are symptomatic of the whole library. First, there are character functions which are insufficient even for citizens of Great Britain. Translating the word manœuver to upper case will fail because you will get
CPL/I
toupper('œ') == EOF
translate(S, 'ä', 'Ä')

You've seen it correctly, translating a character which doesn't belong to the normal ASCII set will yield "End of File" in C! In PL/I you can specify what you want to be translated, also German Umlauts! There are also other functions in C which have imprecise return values. For instance, if malloc fails to allocate the requested amount of storage, it returns a null pointer. This is handled in PL/I in the usual manner: the storage condition is raised and you have the chance to take an appropriate action. There are many library functions in C regarding string manipulation. This is copying the string s to string t in C and PL/I:
CPL/I
strcpy(t, s);
(stringsize): T = S;

In C characters are copied until a null byte is found, and no check is made whether there is enough space in the target variable t to hold all of the characters. In PL/I there is no chance of destroying your data. If the source string is longer than the target, it is truncated. If you want to consider this an error, allow the compiler to check the stringsize condition. Another example is strncpy: I don't want to explain here exactly why the C library function has a bad design: sometimes it adds a null character, sometimes not!

But here is the absolute implementation overkill. Depending on the arguments the function realloc either behaves as allocate, or as free. Additionally it may increase or decrease the storage allocated previously (and not tell you about it!).
C
realloc (NULL, size)
realloc (&a, greater_size)
realloc (&a, less_size)
realloc (&a, 0)

There is no argument validation possible! Reading the program yourself doesn't show what the function does. You have to watch the execution!


Library or Language?

All input and output is done by library functions in C. What is to be done if you want to read a character?
CPL/I
if ((c = getchar()) != EOF) ...
get edit (C) (a(1));
if endfile() then
   ...

The C function getchar returns int, of course! If c in the above example is char you will never get an endfile indication because EOF is a negative int constant! In PL/I there are language elements to do I/O, the get statement and the endfile builtin function in this case.

A big difference lies in the processing of formats:
CPL/I
printf ("%*ld %13.6Lf",
        j, li, ld);
scanf ("10lf", &x);
put edit (K, D)
    (f(J), f(13,6));
get edit (X) (f(10));

Formats in C are interpreted at runtime. The compiler cannot do any error checking. Additionally you have to specify in the format not only how to output a value but also what type the variable has. Take notice that scanf needs pointers to be able to return values! All this is completely harmless in PL/I. Formats are interpreted and checked at compile time. Formats only specify the outward appearance of a variable, not its type. If you look at the printf example there are two formats and three variables. The first variable, j, specifies the field length the second variable is to have. This is done in a clearer way in PL/I: you only have to specify in the f-format a variable instead of a constant!

The naming of the C functions is standardized in a chaotic manner! Don't expect analogies: fputc, putc, and fputs all write to the file specified, whereas puts and putchar both write to stdout! Another example is the order of arguments of putc and fprintf: whereas putc has the file specification at the end of the argument list, fprintf demands it as the first argument!

A powerful example of I/O in PL/I is the so-called record-I/O. The statement
PL/I
read file (F) into (R) key ('Smith');
reads in the next record where 'Smith' is at a specified position within the record. Such a dataset can be considered a small database!


What the Eye Doesn't See...

We have sometimes mentioned error handling methods in C and PL/I. C has poor or no error indication, compared to PL/I: there are no floating point errors defined. There are no errors in unsigned arithmetic by definition! The library uses different error codes: putchar returns EOF, setvbuf a value not equal 0, and malloc a null pointer in case of an error. In PL/I every error has a number, and errors are divided into groups: anycondition, area, attention, condition, conversion, endfile, endpage, error, finish, fixedoverflow, invalidop, key, name, overflow, record, size, storage, stringrange, subscriptrange, transmit, undefinedfile, underflow, and zerodivide. Here is an example how to intelligently survive a division by zero:
PL/I
on zerodivide, underflow begin;
   put (oncode());
   X = 0;
   goto Continue;
   end;
  ...
Z = 0;
X = Y / Z;
Continue:;

You provide an on-unit which is entered in case of one of the conditions having been raised. You can, for example, output the error code, set X to a value you wish and continue with the statement following the statement that produced the error. This is the only case in PL/I where you need a goto statement!


Why Not a New Statement?

Now we come to the preprocessor. The main difference is clear to see: the C preprocessor uses a completely different language -- line-oriented, with special rules and operators whereas preprocessor PL/I is a subset of full PL/I with fewer statements and datatypes available. The capabilities of the C and the PL/I preprocessor are similar regarding simple text replacement activities. Macro facilities are very different. Here is the C definition of a macro computing the square root in C (pay attention to the fact that there is no space between the function name and the left parenthesis!):
C
#define square( x) (x) * (x)
  ... square(z+1) ...

This would be possible in PL/I, too. But, moreover, you can define a completely new statement, for example the REXX parse statement. If you want to be able to say:

parse value (Filename) with (Name, '.', Ext);
you can specify a preprocessor procedure with the statement option:
PL/I
%Parse: procedure (Upper, Value, With) statement returns (char);
 dcl (Upper, Value, With, R) char;
 if ^parmset(Value) | ^parmset(With) then
    note ('VALUE and WITH are required!', 12);
 ...
 R = 'do;';
 R = R || ... /* generate normal PL/I statements */
 return (R || 'end;');
%end;

A preprocessor procedure takes the parameters (the keywords of the statement) and computes a return value, consisting of normal PL/I statements using builtin functions, for instance. This return value is then used as replacement text.


Summary

We have seen that many things in C are defined in a more dangerous way than in PL/I. First I want to present a list of hints to programmers who occasionally need to program in C:

Nevertheless we must state that in the definition of C many of the experiences gained by other languages have been ignored. In my opinion, a good language is one that ...

If you would have to rate C and PL/I, to what language would you give more points?