Compiler Faults

This is a place to highlight faults in the compiler, (No warning of uninitialised variable when used in if-then) not the place for things that you just think are ugly and you don’t like. (Strings indexed from 1 not 0. My preference;-) )

It is the situations when the compiler behaves in a way that presents us with surprises. It does something that could result in a catastrophic failure. It is for the kind of thing that in order to avoid the condition you would have to explicitly be told to avoid it.

Remeber this is not C/C++ where we can blame the programmer for not knowing the subtleties of the language and just let it fail in the field. In Ada we should take our inspiration from the aviation industries “no blame culture” where we say, "If a human can cause this to happen on this aircraft and pilots are human, we need to make the aircraft able to cope with humans.

So this is about making the compiler better able to deal with human programmers by removing peculiarities.

Ada compilers MUST not allow multiple references to the same memory block to be passed into a procedure if one of those references will be used to change the memory blocks contents within the procedure!

The reason for this is explained below but if Ada followed the rule above, it could not happen. Of course you can write code to get around the problem but that is not the point. There will be some code out there that suffers from this and it could be part of something critical like a rocket. Complex numbers are often used in dynamics.

BASICS: When you pass parameters to a procedure (or function) the Ada compiler decides to pass by reference or by value. Passing by reference just means no new memory is allocated and any changes will be to the original variable's memory. Passing by value means some new memory is allocated and a copy of the variable's value is copied into it on entry and if necessary copied back out on exit.

BASICS: Ada allows three possible forms of parameter passing "in", "out" and "in out" and although there are restrictions on when values are copied in and and copied out, ada can use passing by value or by reference in all three cases. It will prefer passing by reference on larger parameters in order to save wasting time making copies.

BASICS: It is the compilers decision, and here lies the root of the fault I am highlighting!

In the code below if myArray is (0..2) then opComplexProduct2(X,X) fails and if (0..1) it succeeds because Ada decides to pass by reference or value respectively. I am using the gcc compiler so other compilers may optimise differently. opComplexProduct2(X,Y); will still succeed as X and Y are different variables.

Obviously this is because X and Y are passed by reference, if myArray is (0..2) or (0..3) or more and by value if myArray is (0..1) or (0..0).

This is a very serious "side effect"!

Question: Why on earth should you pass the same parameter twice? Because opComplexProduct2(X,X); gives the square of the complex number which has many applications.

I repeat: **Ada compilers MUST not allow multiple references to the same memory block to be passed into a procedure if one of those references will be used to change the memory blocks contents within the procedure!**

Dummy line to overcome your editor BUG!

	type type_Complex is record
		x:Integer:=0;
		iy:Integer:=0;
	end record;
	
	--COMMENT OUT ONE OF THESE LINES BELOW
	type myArray is array (0..2) of type_Complex;    -- opComplexProduct3(A,A); FAILS
	--type myArray is array (0..1) of type_Complex;  -- opComplexProduct3(A,A);SUCCEEDS
	
	
	procedure opComplexProduct3(A:in out myArray; B:in myArray) is
		n:Integer:=0;
		t:Integer:=A(n).x;
	begin
		A(n).x  := t  * B(n).x  -  A(n).iy * B(n).iy;
		A(n).iy := A(n).iy * B(n).x  +  t  * B(n).iy;
		return;
	end opComplexProduct3;

	procedure opComplexProduct3(A:in myArray; B:in myArray; C:out myArray) is
		n:Integer:=0;
	begin
		C(n).x  := A(n).x  * B(n).x  -  A(n).iy * B(n).iy;
		C(n).iy := A(n).iy * B(n).x  +  A(n).x  * B(n).iy;
		return;
	end opComplexProduct3;
	  



	procedure opTest is
		X,Y,Z:myArray;
	begin

		X(0).x:=2;X(0).iy:=3;
		Y(0).x:=2;Y(0).iy:=3;
		opComplexProduct2(X,X); --FAILS
		Put_Line("The complex product is: ("
			& Integer'Image(X(0).x) & ","
			& Integer'Image(X(0).iy) & ")");

		X(0).x:=2;X(0).iy:=3;
		Y(0).x:=2;Y(0).iy:=3;
		opComplexProduct2(X,Y);
		Put_Line("The complex product is: ("
			& Integer'Image(X(0).x) & ","
			& Integer'Image(X(0).iy) & ")");

		X(0).x:=2;X(0).iy:=3;
		Y(0).x:=2;Y(0).iy:=3;
		opComplexProduct3(X,Y,Z);
		Put_Line("The complex product is for sure: ("
			& Integer'Image(Z(0).x) & ","
			& Integer'Image(Z(0).iy) & ")");
		
		X(0).x:=2;X(0).iy:=3;
		Y(0).x:=2;Y(0).iy:=3;
		opComplexProduct3(X,X,X);
		Put_Line("The complex product is: ("
			& Integer'Image(Z(0).x) & ","
			& Integer'Image(Z(0).iy) & ")");
		
		return;
	end opTest;

You should read the Ada LRM Chapter 6.2. It is defined when a parameter is passed by copy or by reference.
Pay special attention to the last paragraph (12/3) where it is written :
If one name denotes a part of a formal parameter, and a second name denotes a part of a distinct formal parameter or an object that is not part of a formal parameter, then the two names are considered distinct access paths. If an object is of a type for which the parameter passing mechanism is not specified and is not an explicitly aliased parameter, then it is a bounded error to assign to the object via one access path, and then read the value of the object via a distinct access path, unless the first access path denotes a part of a formal parameter that no longer exists at the point of the second access (due to leaving the corresponding callable construct). The possible consequences are that Program_Error is raised, or the newly assigned value is read, or some old value of the object is read.

If I understand this correctly, the compiler is correct and you made an error by accessing the same actual parameter from 2 distinct formal parameters.

Note that (at least with GCC 14), compiling with -gnatwa will warn about this (IMO, slightly misleadingly in that to my BrE eyes the parameters involved are named the wrong way round, but still useful).

2 Likes

Thank you for taking the trouble to find that and to respond so quickly.
I have now read 6.2 but the fact that you say “If I understand this correctly…” and given that you even found it, suggests you are a person well qualified to program in Ada, but if you must question your understanding it supports my allegation that this could be a source of catastrophic problems. What is correct must be obvious, not buried in a manual’s warning. That is why I left C and came to Ada. Ada in particular, should close the door on the possibility of such events. That is why we have things like “case others” and array bounds checking etc. Let’s face it every C manual tells you that you cannot write off the end of an array but it still happens. Putting a “don’t do this” in the manual is a last resort when it is impossible to do anything in the compiler to stop it happening. This is why Rust is the best thing that has happened to Ada. Anything simple that can make Ada stronger should be done.

If the principle I proposed " Ada compilers MUST not allow multiple references to the same memory block to be passed into a procedure if one of those references will be used to change the memory blocks contents within the procedure" was adopted then the problem could be avoided.

Interestingly I did find that there was a warning when passing simpler veriables but a bit like the uninitialised variable warning that disappears when the variable is used inside if-then end if; similarly this warning was sporadic. I also have noted warnings that depended on other unrelated factors. Ada has evolved into a great language and I really appreciate it. I just wish that it would tidy up some details.

In aviation when there is or could be a serious accident if a pilot presses the green button on the left instead of the green button on the right, nobody points out that the manual says what the buttons do. Rather one button is changed to make it distinct from the other and reduce the possibility of the mistake being made. We need to impove the machine to deal with possible human errors, not complain about the humans making mistakes because if they can they will.

I’m not a Ada expert. I’m still learning Ada at home when I have time.
I think I have already read discutions about the problem you described. I don’t remember when and where so I can’t give any pointer on it.
Passing the same actual parameter to two different formal parameters is tricky whatever the language used. But I admit it is misleading when one uses in mode but its content is not constant inside the subprogram.

You might investigate the tools that come with the SPARK subset of Ada, which is available via “Alire”. SPARK enforces this sort of “anti-aliasing” requirement. Also note that AdaCore has a static analysis component called “GNATSAS Inspector” which also performs anti-aliasing checks.

At some point it would be nice to add anti-aliasing checks to the compiler itself, but that presumes the compiler does enough control and data flow analysis to detect such problems, or the language imposes limitations to make the detection more straightforward (which is what Rust and SPARK do).

I attempted to look at Alire but within a few pages the website stopped explaining clearly and since my compiler works and I have code to write, I point this out not because I needed a solution but rather because I thought there was room for improvement in the compiler. Saying that I am interested in what your suggesting. I had not heard of GNATSAS. It took an hour to realise what was happening and it’s only ever happened once. SPARK intrests me but it might be overkill and I would rather Ada was fixed first. You are right though I really must take a better look at spark anyway.
Ada is so nice and simple and so safe most of the time that I don’t get the problems C/C++ used to give me. I started back in the 1980s writing 6502, 68000 and ARM assembler. Mostly the first and only moved to C when the Archimedes (early ARM) computer had a really optimal C compiler. Also now I have written my own database and my own webserver in Ada and the tasking is wonderful, the exceptions are wonderful. For my applications I don’t need formal proof and that sort of thing. I just think it would be great to clear up the last little anomalies of Ada.
Thanks for your info.
Tom

Relatively recently Ada was revised to permit [IN] OUT parameters on functions. At that point, a number of us insisted on the compiler being required to detect cases where order of evaluation would change the result (see Ada RM 6.4.1, paras 6.5 to 6.25). If we were designing Ada today, we would probably require the same thing for more general aliasing issues, but the general problem you identify is not just an interface issue, because it depends on the ordering of the reads vs. the updates inside the procedure to determine whether or not there is actually a problem. SPARK has rules which prevent these kinds of things without looking inside the procedure, but at this point, that would be incompatible for existing Ada code, and so is a bit of a non-starter.

One could imagine some aspects similar to “Global” which would provide finer grained control on the aliasing issue, which could then be checked both at the call site and inside the procedure. Perhaps worth investigating, but not trivial if we are trying to preserve upward compatibility.

Thank you. Very interesting and it tells me I should give more time to SPARK. Above as you saw I mentioned Rust and a bright young friend of mine has pointed out how it handles this kind of problem after I sent him this link entitled " I’ve been working with Rust for a couple years now, and I’m finding that I’m really not having a good time with the borrow checker.“:
https://www.reddit.com/r/rust/comments/1eq357a/ive_been_working_with_rust_for_a_couple_years_now/?chainedPosts=t3_7wzrqi
He pointed out it was about the same kind of problem.
And just for your reference this link entitled " Why Rust was the best thing that could have happened to Ada” https://www.reddit.com/r/ada/comments/7wzrqi/why_rust_was_the_best_thing_that_could_have/

So I need to explore more deeply what the “borrow checker” is all about, why it causes problems etc. because the examples that he has given me are really good and I hope to share them on this thread but they are mixid with images of code and so it’s not so easy.

I moved from C/C++ to Ada because when employing programmers I looked for “the ones that got away”, talented programmers and potentially talented programmers in the wrong jobs. I offered them the opportunity to do what they loved and in return received loyalty and also I got to pay a bit less than I would to a carreer programmer who is gone in 3 years leaving a tangle of C++ and a technical debt behind them. Even for C 3 years experience really is needed.
With Ada there was the great history on life critical systems thet kept me thinking there must be something good about it. When doing hardware I whent for VHDL which is based on Ada and then I decided to also go Ada with the application I was writing and I ported it from C++. I have been very glad of that decision.

The decision was motivated by the idea that Ada can be a first language to learn for my next lot of employees and it has also proved it’s self by catching many bugs that might have lived for years in C++ code.

Ada is certainly more rigorous than C++ but Rust is more rigorous than Ada but utterly not a first language to learn. Although when changing from C/C++ to Ada I didn’t like writing integer instead of int I have found that Ada’s verbosity is not any problem, in fact quite the reverse. The biggest asset was to get rid of the { and } and the appalling K&R style of use that turns a triple nested if into an unreadable tangle. “fn” for “function” in Rust just vanishes into the line that follows.
I can see that this issue is one requiring more thought than I have time to give at the moment as I must work on my 100% Ada project management system code.
Even though I criticise Ada, as I always say “Praise is good for my ego but criticism is good for my business.” There is no doubt that as I code I have had much reason to heap praise on the language.
However there is an entry barrier which meant that when my daughter decided to learn some programming she went for python not Ada as it is used more in the biological sciences (ouch). Then she decided to do a course in R this summer (ouch squared)
I do see AdaCore doing some admirable work at reaching beginners but there are some gaps to fill there. I just wish I had the time, because my experience in education is there, to take this one on and get Arduino users, using Ada. I have been asked the question many times about embedded Ada.
Sorry for wandering off topic.
Tom

Actually, Ada and Rust have different strengths and weaknesses – neither is uniformly more rigorous than the other. For example, Ada is stronger than Rust in numerics, but Rust’s borrow checker is stricter (and also more restrictive) than Ada’s “accessibility” checking.

SPARK, on the other hand, is uniformly more rigorous than pretty much any language out there, in that it is fully, formally verifiable (including for the kinds of problems you mentioned originally). Rust doesn’t really have a complete specification yet (though we and others are working on one), so doing full formal verification with Rust is not really feasible yet.

4 Likes

Is the thread misnamed? I thought this was a thread for compiler faults, but the only complaint so far is about a perceived language fault.

When I see “compiler faults” I think of some of the gnat issues that have bedeviled me, such as the period where it crashed when compiling certain Ada 2022 features. (I have personally filed 2 or 3 bug reports to that effect.)

Thanks for you replies. I don’t have any experience with Rust at all but I am impressed by what I have seen of a friends exploration. I am totally focussed on my Ada programming and using the gcc compiler and geany which doesn’t do some of the nice things GNAT Studio does but is closer to the metal and I like that. Although I have a few gripes about Ada like strings where the offset to the first element is 1, I have to say that overall my Ada experience is an extremely happy one and nearly everything fits with the way “I feel it should be done” given the needs expressed above. So a big thank you to AdaCore and to you for your part in the design process.

That’s an interesting philosophical idea but there is no language fault.The idea being expressed by passing the same parameter twice is clear. The fact that the fault only happens when the array bounds change from 0…1 to 0…2 or greater indicates that either the compiler is correct with 0…1 or it is correct with 0…2 and greater. Thus it is the compiler that contradicts it’s self. Yes I know there is a reason for it, but that isn’t the point. The point is to have a super safe language and that means the size of an array should not effect the what is returned from this function. Q.E.D
Yes I already know that Ada is a great language and really well put together and I love it. But you know what, when you try to pretend something is perfect, it’s a sure way to take it in the other direction.

I agree that the code shouldn’t compile, but I don’t think your example demonstrates an “unavoidable” situation, or something one would accidentally run into.

In mission-critical code, this isn’t usually going to happen, because the programmer is not leaving memory-allocation to chance.

In ADA.NUMERICS.GENERIC_COMPLEX_TYPES you can see how to implement your own complex functions for custom complex types.

with Ada.Text_IO;
with Ada.Integer_Text_IO;
with Ada.Numerics.Complex_Elementary_Functions;

procedure opTest_XL is

   type type_Complex is record
      x  : Integer := 0;
      iy : Integer := 0;
   end record;

   -- Complex multiplication
   function "*" (Left, Right : type_Complex) return type_Complex is
   begin
      return
        (x  => (Left.x * Right.x) - (Left.iy * Right.iy),
         iy => (Left.x * Right.iy) + (Left.iy * Right.x));
   end "*";

   -- COMMENT OUT ONE OF THESE LINES BELOW
   type myArray is
     array (0 .. 2) of type_Complex;    -- opComplexProduct3(A,A); FAILS
   --  type myArray is array (0 .. 1) of type_Complex;  -- opComplexProduct3(A,A); SUCCEEDS

   type mpArray_Ptr is access myArray;

   X, Y, Z : mpArray_Ptr;

begin

   -- Allocate *memory* for X, Y, Z
   X := new myArray;
   Y := new myArray;
   Z := new myArray;

   X.all (0).x  := 2;
   X.all (0).iy := 3;
   Y.all (0).x  := 2;
   Y.all (0).iy := 3;

   X.all (0) := X.all (0) * X.all (0);
   
   Ada.Text_IO.Put_Line
     ("The complex product is: (" & Integer'Image (X.all (0).x) & "," &
      Integer'Image (X.all (0).iy) & ")");

   X.all (0).x  := 2;
   X.all (0).iy := 3;
   Y.all (0).x  := 2;
   Y.all (0).iy := 3;
   X.all (0)    := X.all (0) * Y.all (0);
   
   Ada.Text_IO.Put_Line
     ("The complex product is: (" & Integer'Image (X.all (0).x) & "," &
      Integer'Image (X.all (0).iy) & ")");

   X.all (0).x  := 2;
   X.all (0).iy := 3;
   Y.all (0).x  := 2;
   Y.all (0).iy := 3;

   Z.all (0) := X.all (0) * Y.all (0);
   Ada.Text_IO.Put_Line
     ("The complex product is for sure: (" & Integer'Image (Z (0).x) & "," &
      Integer'Image (Z (0).iy) & ")");

   X.all (0).x  := 2;
   X.all (0).iy := 3;
   Y.all (0).x  := 2;
   Y.all (0).iy := 3;
   Z.all (0)    := X.all (0) * X.all (0);
   
   Ada.Text_IO.Put_Line
     ("The complex product is: (" & Integer'Image (Z (0).x) & "," &
      Integer'Image (Z (0).iy) & ")");

   return;
end opTest_XL;

Output:

The complex product is: (-5, 12)
The complex product is: (-5, 12)
The complex product is for sure: (-5, 12)
The complex product is: (-5, 12)

XLCOLDJ
There is very little that is unavoidable in almost any piece of code, and the complex number example was one I made up to explore the problem which I came across in another situation. If this can happen on my desktop it could also happen in a more critical situation. I would think the main danger would be in code that isn’t commonly executed. One has to look at all the problems that can result from this, not just the one I have presented here.
Tom

I would also suggest that for silver level (which goes beyond Rust) and below that Spark is actually easier to use than Rust. Strings starting at 1 actually helps avoid bugs too. You can make strings starting at 0 easily. Try it and compare.

Small tip. Turn gnatprove warnings off to start with unless starting a project from scratch without existing Ada code.

I may not agree with everything Kevin wrote in this post, but I absolutely agree with this, and translating Ada solutions for the 2023 Advent of Code to Rust drove that home. (Which reminds me: I need to finish that.)