Menu

implementation notes

Trevor Blight

p5c Implementation Notes

Pascal and C have important differences

c and pascal are similar in many respects, but a few difference do exist.
Pascal has strong typing so is able to find more errors at compilation time rather than at run time, etc, etc

Both have a similar approach to data & code, so much Pascal data code translates directly to c:, eg

    integer       --> int,
    record        --> struct
    while         --> while
    function f(x) --> f(x)

There are, however, enough differences to prevent simple direct compilation:

arrays are different.

At first sight they appear the same since they have the same syntax: array1[i] is the ith component of the array called array1.
In c, however, array1 is not an array as a pascal user would understand it, but a pointer to an array.
This c code

   array1 = array2

does not copy the contents of the array, it copies the pointer to some array (or is illegal).
So in this example, array1 & array2 now both refer to the same array, and in c the statement

   array1[1] = 5

also sets array2[1] to 5.

p5c and p5x work around this by wrapping all arrays inside structs, so a pascal array declaration:

    type MyArray = array[ 1..9 ] of integer;

is compiled to a c declaration like this:

    typedef struct {
       int component[9];
    } MyArray;

Now, if arrays a and b are of the same type (or are strings), the pascal assignment

     a := b;

compiles directly into the corersponding c assignment

     a = b;

with the results the pascal programmer expects.

goto is different.

c allows local goto only and p5c compiles a local pascal goto into a c goto.

pascal also allows interprocedural gotos, and these are implemented using c's longjump function.

p5x and p5c use the symbol table produced from compiling the declarations to create the equivalent c declarations.
In the code generation phase, p5x accesses this table to create c statements from the pascal statements.


compilation of declarations and compilation of code will now be considered separately:

Compiling Declarations:

labels

labels are compiled into jump buffers.
If a label is used in a subprocedure a jumpbuffer is set up at the start of the procedure. Otherwise the jump buffer is left unused, and gcc is free to optimise it out.

constants

constant identifers are kept internal to the compiler, so no corresponding c code is emitted.

types

a c type declaration reverses the order of a pascal declaration, eg

 i : ^integer;     ===> int *i;

This is done by procedure genCType

r : record .... end;  ==> struct { ... } r;

but note arrays:

in c, an array is just a pointer, so if arrayA & arrayB are arrays,

in pascal the statement

 arrayA := arrayB

copies all the components of arrayB into arrayA, and assigning a new value to one of the elents in arrayA has no effect on arrayB.

In c, on the other hand, the statement

arrayA = arrayB

makes arrayA point to the same components as arrayB.
Now a change to one of the components to arrayA will be seen in arrayB as well.

To overcome this, p5x wraps pascal arrays are inside a c struct.

eg

    arrayA : array[0..9] of integer;

is translated into the c code

    struct {
        int component[10] } arrayA;

so that all components of arrayA can be copied when assigned.

sets

sets are declared in the same way as arrays are.

pointers

In pascal, pointers are resolved at the end of the type declaration block to enable recursive data structures.
In c, recursive data structures are implemented by forward referencing structs.

So in pascal, we might have for example:

    pr = ^rec1;
    rec1 = record
             i : integer;
             link2 : ^rec2;   { this is a forward reference to rec2  }
           end;

    rec2 = record
             i : integer;
             link1 : ^rec1;
           end;

In c, these records become

    struct rec2;        /* this is a forward reference to rec2  */
    typedef struct {
        int i;
        rec2 *link;
    } rec1;

    rec1 *pr;

    typedef struct {
        int i;
        rec1 *link;
    } rec2;

Note how the order of the declarations needs to change.
The delaration of pr needs to move down so it is declared after rec1.
Also, the declaration of rec2 is split into 2 parts so that rec1 can forward reference it.

pascal recursive declarations like self = ^self are not possible in c, so void * is used instead.

This means that type declarations need to be ordered differently for the 2 languages. In fact the pascal type declaration order is ignored and the c order of the types is determined from scratch.

This reordering is handled by the procedure declareType.
The declareType function also detects and handles pointer recursion

example

In pascal:

   type
       p1 = ^someType;

       r1 = record
              link2 : ^r2;
              info  : someOtherType;
             end;

       colour = (red, orange, green);

       r2 = record
              link1 : ^r1;
              see   : colour;
            end;

       sometype =  ...

In c, on the other hand sometype must be declared before the pointer to it, and record r2 must be forward declared so r1 can access it.
Also colour needs to be declared earlier than r2, so if r2 is moved the colour may need to be moved as well.

The equivalent c code might be like this:

     typedef declaration for sometype;
     typedef someType *p1;
     typedef struct r1 r1;

     typedef int colour;

     typedef struct r1 {
             r1 *link1;
             colour see;
             } r2;

     typedef struct r1 {
             r2 *link2;
             someOtherType info;
             } r2;

identifiers

identifier names need to be unique, so the declaration level is appended to the name. eg for the declaration if an integer declared at the program level,

     i: integer;

might become

     int i_1;

This prevents pascal identifiers clashing with c reserved words, and c can refer to the id at the correct level. (this could otherwise happen in some pathological cases.)

procedures and functions

These are compiled directly.
gcc allows nested functions, so nested pascal functions are simply compiled into nested c functions.


Code Compilation

This is mostly straight forward.

pascal p5 and hence p5c and p5x are implemented by a recursive descent compiler.

See Pemberton for a complete explanation.

Consequently, the compiler has almost no memory of the code it has just processed and code needs to be generated in the same sequence as it is encountered.

expressions become trees

p5 (but not p5c or p5x) produces code for an abstract stack machine.
So for example, the p5 compiler takes the following steps when compiling the expression

       'string1' < 'string2'
  • encounter 'string1', push it onto the stack. At this point, the compiler doesn't know what the remainder of the expression will be, and has lost information of the previous code. The compiler will be in the same state as if 'string1' is a procedure parameter;

  • encounter the '<' relational operator. This is simple enough to remember.

  • encounter 'string2', and push it onto the stack

  • recall the '<' operator, and compare the strings on the top two stack positions. Replace them with the result of this comparison, ie with true or false.

p5c and p5x, in contrast, compile this into c code, so need to produce something like this:

   (strcmp( "string1", "string2" ) < 0)

Note that p5c needs to emit the strcmp before "string1". This is not the order in which it finds them in the code, so it needs to remember the "string1" until it knows what to do with it. What if 'string1' was deep inside an array of records of records of arrays of packed arrays of char (or worse)?

For this and other reasons, p5c builds expressions into trees so it can change the order in which it emits things.

So for above example, when p5c find the 'string1' < 'string2' expression, it does this:

  • encounter 'string1', make a node for 'string1'
  • encounter <, make a node for < string operator, put node for 'string1' on left child
  • encounter 'string2', make a node for 'string2', put onto right child of the < node

Now when the expression has been completely parsed, p5c can use the tree to generate code. It starts at the root of the tree, ie the string < operator node, and knows that it needs to emit something like this:

  • emit "(strcmp("
  • emit left child, ie "string1"
  • emit ","
  • emit right child, ie "string2"
  • emit ") < 0)"

As previously mentioned, The arguments of the strcmp() function could be arbitrarily complex, and this is easily handled by traversing the left and right children of the tree.

Also note that this could be part of a larger expression, such as

       ('string1' < 'string2') and ( .... )

in which case there would be a much larger tree, of which the above is just a subtree, in this case forming the left child of an and-operator node.

For a further example, consider

    writeln( n:w )

where w is an arbitrary expression for the total width of the output. n itself could also be an arbitrarily complex expression.

the equivalent c code is

    printf( "%*i", w, n );

So the expression for w needs to be emitted before the expression for n. This is the reverse of the order they appear in the pascal code, so the expressions need to be remembered in order to generate valid c code.
This is easily accomplished if expression trees are used.

for loops

These need careful consideration

  • the end condition must be executed only once
  • the loop variable is assigned only if the loop is entered.
  • be careful not to allow overflow of the loop variable

sets

sets are not part of the gcc language, so need to be specially implemented. Some simple optimisations are done, eg p5c may keep lists of const elements and var elements of sets.

Sets are implemented with a c struct containing an array of elements:

     struct { uint8_t element[n] }  mySet;

where n is the number of bytes (ie uint8_t) needed to hold the set elements.

A set union is a bitwise or operation on the bytes and set intersection is a bitwise and operation. Set difference is slightly more complicated - it is a bitwise and with a bitwise compliment,
eg

        setA - setB

becomes

        setA & ~setB

p5c and p5x construct expression trees for sets before generating c code.
This enables analysis of set expressions for possible efficiencies and to determine how to implement the set operations.

Many pascal compilers make all sets the same size, say 256 bits. This is simple to implement, but it is wasteful for small sets and not big enough for large sets. In this case a small set like

    colour = set of (red, orange, yellow, green, blue, indigo, violet)

uses 256 bits, even though only 7 are needed.
Also, with fixed set sizes something like this is not possible:

    primes = set of 0..50000;

To avoid both these problems, p5c and p5x do not compromise and make sets the correct size.
This, however, creates different problems to be solved.

Firstly, any alignment problems can be solved if the elements are aligned in bytes where bit nr = (value mod 8), so mixed sets can be manipulated without having to shift bits within the sets.

For example this set:

      myset : set of 30..40;

will occupy 3 bytes, with the first byte holding the element values 24..31, even though only values 30 & 31 are used. The second byte will hold values 32..39, and the third byte will hold the values 40..47 with only the value 40 being used,

A bigger question is what size does a set need to be to evaluate a given expression?

For a statement like

            s1 := setExpr;

the answer is straightforward: the size of setExpr needs to be the same size as s1. As the tree for setExpr is traversed to generate code, the required set size is passed to each child node.
For example, in the statement

             s1 := s2 + s3;

where s1, s2 and s3 are sets of different sizes, we now know that the size of the expression (s2 + s3) must be the same as the size of s1, whatever the sizes of s2 and s3 are.
The expression (s2 + s3) forms a simple tree with the '+' at the root, and s2 and s3 forming the left and right branches respectively.
So code generation starts at the root with the size of s1, and recursively passes this known size to each of its child branches. When control returns to the root node, it has 2 sets of exactly the correct size that it can logically or-op together to produce the result.
This recursion is handled in the procedure genCCode().

The procedure constructSet() generates a set with given bounds.

The set "in" operator doesn't need to construct a set to evaluate its expression.
Consider, for example, a statement like this:

   if e in [ x, y, z ] then ...

It is more efficient to check if e matches one of x,y,z directly than it is to put x, y, & z into a set then check if e is in the set.
This also sidesteps the issue of how big a set is needed to hold x,y & z.

Similarly, where s1 and s2 are sets, a statement like

      if e in s1 + s2 then ...

is translated into

    if (e in s1) or (e in s2) then ...

Also

    e in s1 * s2

is translated as

    (e in s1) and (e in s2)

and

    e in s1 - s2

is translated as

    (e in s1) and not(e in s2)

Since a set expression is represented by a tree, abitrarily complex set expressions on the right hand side of the "in" operator can be compiled recursively without needing to construct intermediate sets (whose size we might not know).
See the function isMember() for implementation details.

set comparisons

We can also use expression trees to analyse set comparisons.

As the tree is being constructed, each node combines the sizes of its children to form a new size for its result.

The lower bound of a '+' node (set union) is the minimum of the lower bounds of each of its children. Similarly, the resulting upper bound is the maximum of the upper bounds of its children.
For example the resulting bounds of a subexression like,

    [-12..3] + [8..80]

are -12 and 80 for the lower and upper bounds respectively as this diagram shows:

 left side    -->         [-12................3]
 right side   -->                  [8................................80]
 left + right -->         [-12.......................................80]

The results are the opposite for a '*' node (set intersection), ie it's the bigger of the two lower bounds and the smaller of the two upper bounds.
eg the resulting bounds of

    [ 2..20] * [10..30]

are [10..20], as shown here:

 left side    -->         [-12................3]
 right side   -->                  [8................................80]
 left * right -->                  [8.. ......3]

Finally set subtraction dooesn't change bounds, ie the bounds of s1 - s2 are eaxctly the same as the bounds of s1.
So the resulting bounds for a '-' node are the exactly the bounds of the left child as shown here:

 left side    -->         [-12................3]
 right side   -->                  [8................................80]
 left - right -->         [-12................3]

Note

  • if we say the empty set has a lower bound of maxint and an upper bound of -maxint, these results work as expected for empty sets.

See the procedure findResBounds() for the implementation details.

Suppose we have constructed a tree for a set comparison, where we have left and right set expressions and a compare operator. We also know the bounds each of the set expressions needs to be to determine its value. These two bounds are not necessarily the same, and not necessarily the set bounds needed to make the comparison.

What happens next depends on the compare operator.

set equality

For equality, we have

      setSubExpression1 = setSubExpression2

To compare the two sub-expressions, the left and right hand sides must have the same bounds.
The lower bound of both the left and right sides must be whichever is the mininum of the two. Similarly, the upper bounds of each side must be whichever is the maximum of the two.
A simple example should make this clear:

    s1 : set of [0..20];
    s2 : set of [10..50];

    if s1 + [24] = s2 + [15] then
       writeln( 'equal' );

While the tree for the set expression s1+[24] tree is being created, the bounds of the left hand side of the = operator are determined to be[0..24], as described above.
Similarly, the bounds of the set expression s2+15 are [10..50].
Each side of the = operator must now be constructed in a set with bounds [0..50] so that the sets can be compared.

For set inequality, ie <>, the bound calculations are similar.

set inclusion

For set inclusion, we might have

      setSubExpression1 <= setSubExpression2

Now the bounds of the whole expression are determined solely by the bounds of setSubExpression1. Any elements of setSubExpression2 that lie outside these bounds do not contribute to the result.

So the size of most set expressions can be determined at compile time, but there is still a problem with expressions like

    if [i,j,k] = [m..n] then ...

where all the varaibles have values that are unknown at compile time.

We know the size of expressions like [x..y], ie -maxint..maxint, but this set cannot be implemented in practice. At this point, the compiler issues a warning and uses a default set size of [-255..255].

If this is not suitable, there are a number of options:

  • Recalling the rules for the size of set intersection expressions, we find that a workaround is to use set intersection to give the compiler the information it needs to determine the set sizes, eg

    if [i,j,k]*[0...3000] = [m..n]*[0..3000] then ...
    

where in this case 0 and 3000 are known to be the bounds of the set expressions. This is the recommended option.

  • To change the default set size in p5c and p5x from 255, change the value of the constant setMax in pcom.pas and recompile.

  • Alternatively, assign the expressions to a set of suitable size, eg

     s1 := [i,j,k];
     s2 := [m..n];
     if s1 = s2 then ...   { now the compiler can use the correct size sets }
    

If the set size implied by these options is impossibly large, you may need to consider the {$Z+} compiler option described below.

set bounds errors

We have seen already the p5c implementation needs to solve the difficulties of alignment of the elements and determining the size of sets inside set expressions. There is another problem to be solved - how do we determine if a set's bounds are exceeded?

This example is simple enough:

   mySet : set of 1..10;
   ...
   mySet := [0,5];  {!!! ILLEGAL: 0 is out of bounds}

Is it sufficient to check every element in a set expression and issue an error if any are out of bounds? Unfortunately not. This expression is similar, but it is now not obvious if it is illegal or not:

   mySet := [0,5] - [i];

We don't need to know where each element of the set expression is, just whether any element is outside the bounds of mySet.

For now, assume we have a function, isNonEmpty(expression, range), which checks if there are elements of the set expression in the range.

Recall that in p5c, we can analyse setexpressions because they are implemented by trees, and let's say the expression in the above example is represented by the tree T.

We can now see that the above set expression has an error iff

   isNonEmpty(T, -maxint..0) or isNonEmpty(T, 11..maxint)

Also,

          s0 := s1 + s2;

is in error if either s1 or s2 has an element that is out of bounds.
In other words, set expressions that are unions can be split up and examined individually. So our isNonEmpty function doesn't need to consider the set + operator when it is at the top level.

When there is a set + operators at lower a level in the expression tree, we can use the associative rule:

     (s1+s2)*s3 = s1*s3 + s2*s3

We can repeat this rule as often as necessary to move all + operators to the top level of the set expression.

It is beginning to look like our isNonEmpty function needs to examine terms like s1*s2 to see if they produce any elements inside the specified range.

But what about the set - operator? Recall that s1 - s2 is implemented as

 s1 * ~s2

where ~s2 is the bitwise complement of s2.

Note that this complement extends over the whole range that the isNonEmpty function needs to consider.

So if the bounds of s2 are m..n, and isNonEmpty is considering the range r0..r1, then s1 - s2 becomes

 s1*( sa + sb + sc)

where

 sa = [r0 .. m]
 sb = ~s2
 sc = [n .. r1]

We can now see that the plus operators in this expression can be moved to the top of the expression tree, and that any set expression can be rewritten as a union of terms, ie

  sa*sb*sc...*sz  + ssa*ssb*...

Our isNonEmpty function needs to determine whether any of these terms produce an elemnt in the specified range.

There are two factors that save us some work:

  • these are set intersections, so the low limit of each term is

      max(low bounds of each set in the term)
    

    and similarly the high limit is

    min(high bounds of each set in the term)
    

In many cases, the low limit will be greater than the high bound, so there is nothing to do.

  • many of the terms are of the form [a..b], so apart from contributing to the low & high limits of the term, they need no further consideration.
    ( [a..b] * s = s inside these limits)

Some of this analysis can be done at compile time, some needs to be done at run time, but after all this we can now catch set assignment errors.

eg Let's see this in action with a worked example:

assume we have a set s, declared like this

      s : set of 1..10;

and we have a statement like this

      s1 := [i..j] - [k];             ---- (1)

We need to know if the rhs expression generates any elements that are outside the bounds of s, ie are any of the elements of the rhs less than 1 or greater than 10?

The relevant expression tree, T is

    [i..j] - [k]

and the ranges of interest are [-maxint..0] and [11..maxint], so there is an error iff

 isNonEmpty(T, -maxint..0) or isNonEmpty(T, 11..maxint)

Considering the rhs of this (with the range 11..maxint), we need to manipulate tree T as follows:

 [i..j] - [k]  =  [i..j] * /[k]

where /[k] is the compliment of [k]. This is [-maxint..k-1, k+1..maxint], but here we are considering only the range 11..maxint, so

           /[k] = [11..k-1, k+1..maxint]

We now have

 [i..j] - [k]  =  [i..j] * ([11..k-1] + [k+1..maxint])
               =  [i..j] * [11..k-1] + [i..j] * [k+1..maxint]   --(2)

So if the range i..j intersects the range 11..k-1, or ifi..j intersects k+1..maxint, then the expression is non-empty. That is, the region above the upper bound of s1 is occupied and there is an error in statement (1) above.
eg, if i is 6 and j and k are both 11, statement (1) is

                  s1 := [6..11] - [11];

which is OK.

In equation (2), we have

                  [6..11]*[11..10] + [6..11]*[12..maxint]

both terms of the expression are empty so statement (1) is OK.

On the other hand, if both j and k are now 12, statement (1) is

                   s1 := [6..12] - [12];

which is in error because element 11 cannot legally be assigned to s1.

In equation (2), we have

                  [6..12]*[11..11] + [6..12]*[13..maxint]

which reduces to [11] + [] which is non-empty and statement (1) is in error.

Returning to equation (2) above, we consider the term on each side of the plus operator separately. Since each term is just the intersection of 2 ranges, the result is another range whose lower limit is

  max(lower limit of each range)

and higher limit is

  min(higher limit of each range)

The result is

                  [max(i,11) .. min(j,k-1)] + [max(i,k+1)..j]

So, statement (1) above is in error if any of these terms are non-empty, ie iff

      max(i,11) <= min(j,k-1) or max(i,k+1) <= min(j,maxint)

In the p5c world, we'll refer to this technique as 'analytic comparison', or 'algebraic comparison'.

Notes:

  • compilicated set expressions can use up a lot of code space. If this matters, you may need to put {$d-} and {$d+}option comments around set expressions to disable debugging.

  • since many subexpressions are evaluated more than once, it is important to consider side efefcts. We need to store all sub expressions in temporary variables to avoid unnecessary side efefcts, and to save reevaluating them again.

The benefits of evaluating set expressions analytically go beyond error detection.
We can use analytic comparison to enhance some features of set evaluation mentioned earlier where the set size is unknown at compile time or is known but is impossibly large.

We saw earlier that in a statement such as

      if [i..j] - [k] <= s then writeln('less than or equal to');

it is impossible to know how to build a set of the correct size to evaluate the condition in the if statement. Then, the way forward was to imply a set size by, for example, changing the condition to

      if [c1..c2] * [i..j] - [k]) <= s then ...

where c1 & c2 are compile time constants.

But what if [c1..c2] is still impossibly large?

Note that if a and b are set expressions, then a <= b is equivalent to

        a - b = []

We can use now use our isNonEmpty() function to check the set expression a-b and determine the result of the expression. This can be done without creating the large set(s), but possibly at the cost of creating a large amont of code.

Note also that the set expression

        a=b

is equivalent to

       (a-b) + (b-a) = []

so again we can use our isNonEmpty() function evalute this expression without knowing the set size.


Finally, the set expression may have a known size, but might be too extreme to use.
For example this expression:

   if [ i..13, maxint ] <> [3..j, k] then ...

can use analytic comparison to check the set expressions without needing to know the size of the set expressions.

If you do need to compare set expressions with extreme ranges, there is the {$Z+} compiler option available.

This option instructs p5c to evaluate the expression analytically without constructiong the component sets if and only if one of these conditions apply:

  • the set expression size is unknown, or

  • the set expression size is greater than the default limits set by p5c (normally -255..+255)

This is an advanced feature, and may involve a complex tradeoff between memory needed to evaluate a set comparison on the one hand and the volume of code needed to make the same comparison.

The volume of code is caused by every term in the set expression needing to be combined with every other term. The way to reduce it is by keeping the set expressions as simple as possible.

For example the expression

                 if s1 * s2 - s3 + s4 = [i3, 8..15] then ...

could be simplified by creating and using a temporary set:

                 tmpSet := s1 * s2 - s3 + s4
                 if tmpSet = [i3, 8..15] then ...

conformant arrays

In pascal, a conformant array is declared something like this...

    procedure p( a: array[lo..hi:integer] of myType );

... and is compiled into code like this

    void p_2( int lo, int hi, void *a_2c );
    struct { myType_1 component[hi+1-lo] } a_2;
    a_2 = *(typeof(a2)*)a_2c;

Note that this uses gcc's ability to declare arrays with run time expressions.

Apart from slight differences to checking bounds overflow at runtime, accessing a conformant array is the same as accessing a normal array.

files

pascal files are translated into the following c struct:

    struct {
           FILE *f;
           int flags
           char *name
           buffer
    }

The pascal file is represented by a c FILE variable.
The name is used only for files declared in the program heading, and points to the coresponding argv[] in the c argument list.
buffer is the file buffer variable, say f^.
The flags are used to implement lazy input.
Lazy input is used in pascal so input from files is not read until it is used.
Note that for any pascal file, say text file f, that is in read mode, the buffer variable must appear to contain the next item to be read.
In particular, the program must wait at startup (conceptually) for the first character to be entered so it can initialise input^. This is true for any file that might be connected to an interactive device.

To avoid this wait to enter a character at startup, pascal just pretends that it has already read the first character and put it into input^. The input is not read from the file until it is used by the program.

So for lazy input, the next item is not read from a file until it is needed, ie as late as possible.

The flags member of the file structure has the following values:

value meaning
-2 the file is a text file in write mode, and the current line has not been terminated with an eoln char
-1 the file is in write mode
0 the file has not yet read the next item into buffer, see note below.
1 the flag has read the next item into the buffer
2 the file is a text file at end of line and buffer contains space char

Additionally, if the file is a text file and flags is zero, the buffer has the value of flags for the previous file position. This enables correct handling of unterminated text files.
So if eof is detected, and the previous char was not eoln, then an eoln can be returned. On the other hand, it is an error if
the previous char was eoln.

A pascal file variable contains information about its attached file and the state of that file.

This has the following consequences:

  • you cannot make a new copy of a file variable, ie if f1 and f2 are both file variables, you cannot legally write

      f1 := f2
    

    You can, however, pass a file variable to a procedure (or function) as a var parameter since this is passing the variable itself, not making a new copy.

  • file variables must be able to represent "no file attached"

  • when a record variant contains files, the file variables must not share memory with other variables since the description of the file must not be overwriten and lost.

  • All files can be closed when file variable goes out of scope.

  • new copy: all variable types know if they contain a file and forbid all operations that make a new copy

  • all varaibles that contain a file are initialised to zero. This indicates that initially no file is attached to the variable.

  • when a record variant contains files, this variant is not declared as part of a union. This avoids sharing memory with other variables so file variables are not destroyed and the description of the file is kept intact.

closing files when they go out of scope

Cleanup is more complicated and several scenarios need to be considered:

  • normal return from a function
  • goto out of a function
  • fatal error
  • dispose dynamic memory containing a file variable

For every block (ie procedure/function) that contaions at least one file,
p5c creates a function called _Pcleanup that closes every file declared in that block. If that file has a name, it is external and is correctly terminated with a newline if necessary.
When the procedure or function returns, _Pcleanup is called to close all the files that are declared in this procedure or function.

There is a global list of all cleanup functions which is updated on every block entry and exit. The head of this list is _Phead.

Now, when a goto statement leaves a block, all the cleanup functions in this list are called upto the destination level, and _Phead is updated.

Similarly, a function called _Pexit is defined to call all the active cleanup functions. It registers with the c library's atexit() function to run whenever the program terminates.
When a fatal error occurs, a message is printed and the program terminates.
This activates _Pexit which adds a new line to all external files if necessary and closes all open files.

Dynamic memory may also include files, and these are closed when the dynamic memory is disposed. This is the only case where files in dynamic memory are closed, so if you don't want to leak file handles (and memory) dispose of all dynamic memory before it goes out of scope.

The close file statements are generated by the procedure genCloseFiles(), which takes 2 parameters: a type definition and a procedure parameter that prints the variable name. genclosefiles()recursively scans the type definition and outputs a close file statement for each file declaration that it finds.

Note that:

  • it needs to account for arrays of files by generating a loop to close each member of the array.
  • it calls itself recursively when it finds a record that contains files. In this case the procedure parameter that prints the variable name might print more than a simple identifier: it could print an arbitrarily complicated declaration (say a[i0].r.m).

Related

Wiki: -quick summary
Wiki: The Pascal Language
Wiki: using the p5c compiler
Wiki: using the p5x compiler

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.