Next: , Previous: Mixing Elaboration Models, Up: Elaboration Order Handling in GNAT


C.10 What to Do If the Default Elaboration Behavior Fails

If the binder cannot find an acceptable order, it outputs detailed diagnostics. For example:

     error: elaboration circularity detected
     info:   "proc (body)" must be elaborated before "pack (body)"
     info:     reason: Elaborate_All probably needed in unit "pack (body)"
     info:     recompile "pack (body)" with -gnatwl
     info:                             for full details
     info:       "proc (body)"
     info:         is needed by its spec:
     info:       "proc (spec)"
     info:         which is withed by:
     info:       "pack (body)"
     info:  "pack (body)" must be elaborated before "proc (body)"
     info:     reason: pragma Elaborate in unit "proc (body)"
     

In this case we have a cycle that the binder cannot break. On the one hand, there is an explicit pragma Elaborate in proc for pack. This means that the body of pack must be elaborated before the body of proc. On the other hand, there is elaboration code in pack that calls a subprogram in proc. This means that for maximum safety, there should really be a pragma Elaborate_All in pack for proc which would require that the body of proc be elaborated before the body of pack. Clearly both requirements cannot be satisfied. Faced with a circularity of this kind, you have three different options.

Fix the program
The most desirable option from the point of view of long-term maintenance is to rearrange the program so that the elaboration problems are avoided. One useful technique is to place the elaboration code into separate child packages. Another is to move some of the initialization code to explicitly called subprograms, where the program controls the order of initialization explicitly. Although this is the most desirable option, it may be impractical and involve too much modification, especially in the case of complex legacy code.
Perform dynamic checks
If the compilations are done using the -gnatE (dynamic elaboration check) switch, then GNAT behaves in a quite different manner. Dynamic checks are generated for all calls that could possibly result in raising an exception. With this switch, the compiler does not generate implicit Elaborate_All pragmas. The behavior then is exactly as specified in the Ada 95 Reference Manual. The binder will generate an executable program that may or may not raise Program_Error, and then it is the programmer's job to ensure that it does not raise an exception. Note that it is important to compile all units with the switch, it cannot be used selectively.
Suppress checks
The drawback of dynamic checks is that they generate a significant overhead at run time, both in space and time. If you are absolutely sure that your program cannot raise any elaboration exceptions, and you still want to use the dynamic elaboration model, then you can use the configuration pragma Suppress (Elaboration_Check) to suppress all such checks. For example this pragma could be placed in the gnat.adc file.
Suppress checks selectively
When you know that certain calls in elaboration code cannot possibly lead to an elaboration error, and the binder nevertheless generates warnings on those calls and inserts Elaborate_All pragmas that lead to elaboration circularities, it is possible to remove those warnings locally and obtain a program that will bind. Clearly this can be unsafe, and it is the responsibility of the programmer to make sure that the resulting program has no elaboration anomalies. The pragma Suppress (Elaboration_Check) can be used with different granularity to suppress warnings and break elaboration circularities:

These five cases are listed in order of decreasing safety, and therefore require increasing programmer care in their application. Consider the following program:

          package Pack1 is
            function F1 return Integer;
            X1 : Integer;
          end Pack1;
          
          package Pack2 is
            function F2 return Integer;
            function Pure (x : integer) return integer;
            --  pragma Suppress (Elaboration_Check, On => Pure);  -- (3)
            --  pragma Suppress (Elaboration_Check);              -- (4)
          end Pack2;
          
          with Pack2;
          package body Pack1 is
            function F1 return Integer is
            begin
              return 100;
            end F1;
            Val : integer := Pack2.Pure (11);    --  Elab. call (1)
          begin
            declare
              --  pragma Suppress(Elaboration_Check, Pack2.F2);   -- (1)
              --  pragma Suppress(Elaboration_Check);             -- (2)
            begin
              X1 := Pack2.F2 + 1;                --  Elab. call (2)
            end;
          end Pack1;
          
          with Pack1;
          package body Pack2 is
            function F2 return Integer is
            begin
               return Pack1.F1;
            end F2;
            function Pure (x : integer) return integer is
            begin
               return x ** 3 - 3 * x;
            end;
          end Pack2;
          
          with Pack1, Ada.Text_IO;
          procedure Proc3 is
          begin
            Ada.Text_IO.Put_Line(Pack1.X1'Img); -- 101
          end Proc3;
     

In the absence of any pragmas, an attempt to bind this program produces the following diagnostics:

          error: elaboration circularity detected
          info:    "pack1 (body)" must be elaborated before "pack1 (body)"
          info:       reason: Elaborate_All probably needed in unit "pack1 (body)"
          info:       recompile "pack1 (body)" with -gnatwl for full details
          info:          "pack1 (body)"
          info:             must be elaborated along with its spec:
          info:          "pack1 (spec)"
          info:             which is withed by:
          info:          "pack2 (body)"
          info:             which must be elaborated along with its spec:
          info:          "pack2 (spec)"
          info:             which is withed by:
          info:          "pack1 (body)"
     

The sources of the circularity are the two calls to Pack2.Pure and Pack2.F2 in the body of Pack1. We can see that the call to F2 is safe, even though F2 calls F1, because the call appears after the elaboration of the body of F1. Therefore the pragma (1) is safe, and will remove the warning on the call. It is also possible to use pragma (2) because there are no other potentially unsafe calls in the block.

The call to Pure is safe because this function does not depend on the state of Pack2. Therefore any call to this function is safe, and it is correct to place pragma (3) in the corresponding package spec.

Finally, we could place pragma (4) in the spec of Pack2 to disable warnings on all calls to functions declared therein. Note that this is not necessarily safe, and requires more detailed examination of the subprogram bodies involved. In particular, a call to F2 requires that F1 be already elaborated.

It is hard to generalize on which of these four approaches should be taken. Obviously if it is possible to fix the program so that the default treatment works, this is preferable, but this may not always be practical. It is certainly simple enough to use -gnatE but the danger in this case is that, even if the GNAT binder finds a correct elaboration order, it may not always do so, and certainly a binder from another Ada compiler might not. A combination of testing and analysis (for which the warnings generated with the -gnatwl switch can be useful) must be used to ensure that the program is free of errors. One switch that is useful in this testing is the -p (pessimistic elaboration order) switch for gnatbind. Normally the binder tries to find an order that has the best chance of of avoiding elaboration problems. With this switch, the binder plays a devil's advocate role, and tries to choose the order that has the best chance of failing. If your program works even with this switch, then it has a better chance of being error free, but this is still not a guarantee.

For an example of this approach in action, consider the C-tests (executable tests) from the ACVC suite. If these are compiled and run with the default treatment, then all but one of them succeed without generating any error diagnostics from the binder. However, there is one test that fails, and this is not surprising, because the whole point of this test is to ensure that the compiler can handle cases where it is impossible to determine a correct order statically, and it checks that an exception is indeed raised at run time.

This one test must be compiled and run using the -gnatE switch, and then it passes. Alternatively, the entire suite can be run using this switch. It is never wrong to run with the dynamic elaboration switch if your code is correct, and we assume that the C-tests are indeed correct (it is less efficient, but efficiency is not a factor in running the ACVC tests.)