Gem #150: Out and Uninitialized

by Robert Dewar —AdaCore

Perhaps surprisingly, the Ada standard indicates cases where objects passed to out and in out parameters might not be updated when a procedure terminates due to an exception. Let's take an example:

with Ada.Text_IO;  use Ada.Text_IO;
procedure Gem is

   procedure Local (A : in out Integer; Error : Boolean) is
   begin
      A := 1;

      if Error then
         raise Program_Error;
      end if;
   end Local;

   B : Integer := 0;

begin
   Local (B, Error => True);
exception
   when Program_Error =>
      Put_Line ("Value for B is" & Integer'Image (B));  --  "0"
end Gem;

This program outputs a value of 0 for B, whereas the code indicates that A is assigned before raising the exception, and so the reader might expect B to also be updated.

The catch, though, is that a compiler must by default pass objects of elementary types (scalars and access types) by copy and might choose to do so for other types (records, for example), including when passing out and in out parameters. So what happens is that while the formal parameter A is properly initialized, the exception is raised before the new value of A has been copied back into B (the copy will only happen on a normal return).

In general, any code that reads the actual object passed to an out or in out parameter after an exception is suspect and should be avoided. GNAT has useful warnings here, so that if we simplify the above code to:

with Ada.Text_IO;  use Ada.Text_IO;
procedure Gem2 is

    procedure Local (A : in out Integer) is
    begin
       A := 1;
       raise Program_Error;
    end Local;

   B : Integer := 0;

begin
   Local (B);
exception
   when others =>
      Put_Line ("Value for B is" & Integer'Image (B));
end Gem2;

We now get a compilation warning:

gem.adb:6:10: warning: assignment to pass-by-copy formal may have no effect
gem.adb:6:10: warning: "raise" statement may result in abnormal return (RM 6.4.1(17))

Of course, GNAT is not able to point out all such errors (see first example above), which in general would require full flow analysis.

The behavior is different when using parameter types that the standard mandates passing by reference, such as tagged types for instance. So the following code will work as expected, updating the actual parameter despite the exception:

procedure Gem3 is

   type Rec is tagged record
      Field : Integer;
   end record;

   procedure Local (A : in out Rec) is
   begin
      A.Field := 1;
      raise Program_Error;
   end Local;
   
   V : Rec;

begin
   V.Field := 0;
   Local (V);
exception
   when others => Put_Line ("Value of Field is" & V.Field'Img); -- "1"
end Gem3;

It's worth mentioning that GNAT provides a pragma called Export_Procedure that forces reference semantics on out parameters. Use of this pragma would ensure updates of the actual parameter prior to abnormal completion of the procedure. However, this pragma only applies to library-level procedures, so the examples above have to be rewritten to avoid the use of a nested procedure, and really this pragma is intended mainly for use in interfacing with foreign code. The code below shows an example that ensures that B is set to 1 after the call to Local:

package Gem4_Support is

  procedure Local (A : in out Integer; Error : Boolean);
  pragma Export_Procedure (Local, Mechanism => (A => Reference));

end Gem4_Support;
package body Gem4_Support is

   procedure Local (A : in out Integer; Error : Boolean) is
   begin A := 1;
      if Error then
         raise Program_Error;
      end if;
   end Local;

end Gem4_Support;
with Ada.Text_IO;  use Ada.Text_IO;
with Gem4_Support; use Gem4_Support;
procedure Gem4 is
   B : Integer := 0;
begin
   Local (B, Error => True);
exception
   when Program_Error =>
      Put_Line ("Value for B is" & Integer'Image (B)); -- "1"
end Gem4;

In the case of direct assignments to global variables, the behavior in the presence of exceptions is somewhat different. For predefined exceptions, most notably Constraint_Error, the optimization permissions allow some flexibility in whether a global variable is or is not updated when an exception occurs (see Ada RM 11.6). For instance, the following code makes an incorrect assumption:

X := 0;     -- about to try addition
Y := Y + 1; -- see if addition raises exception
X := 1      -- addition succeeded

A program is not justified in assuming that X = 0 if the addition raises an exception (assuming X is a global here). So any such assumptions in a program are incorrect code which should be fixed.


About the Author

Dr. Robert Dewar is co-founder, President and CEO of AdaCore and Emeritus Professor of Computer Science at New York University. With a focus on programming language design and implementation, Dr. Dewar has been a major contributor to Ada throughout its evolution and is a principal architect of AdaCore’s GNAT Ada technology. He has co-authored compilers for SPITBOL (SNOBOL), Realia COBOL for the PC (now marketed by Computer Associates), and Alsys Ada, and has also written several real-time operating systems, for Honeywell Inc. Dr. Dewar has delivered papers and presentations on programming language issues and safety certification and, as an expert on computers and the law, he is frequently invited to conferences to speak on Open Source software, licensing issues, and related topics.