Loop invariant and what the prover understands

Hi,
I found my first steps in spark endearing at first, now it’s straight up grating :sweat_smile:

procedure Count_And_Erase_Character (Buffer: in out Buffer_Type; Ch: Character; Count: out Buffer_count_type)
  with Post => (for all I in Buffer'Range when Buffer'Old (I) = Ch => Buffer (I) = ' ');```

It doesn’t understand that if at each iteration, a character is blanked if previously it was equal to Ch, then obvious nothing in the string before an iteration can be Ch.

procedure Count_And_Erase_Character (Buffer: in out Buffer_Type; Ch: Character; Count: out Buffer_count_type)
  with Post => (for all I in Buffer'Range => Buffer (I) /= Ch);

procedure Count_And_Erase_Character (Buffer: in out Buffer_Type; Ch: Character; Count: out Buffer_count_type) is
begin
 Count := 0;
 for I in Buffer'Range when Buffer (I) = Ch loop
	pragma Loop_Invariant (Count < I - Buffer'First + 1 and (if I > Buffer'First then (if Buffer'Loop_Entry (I-1) = Ch then Buffer (I-1) = ' ')));
    Count := @ + 1;
    Buffer (I) := ' ';
  end loop;
end Count_And_Erase_Character;

I tried several formulas but it doesn’t work. At least this last one, in the body, succeeded at proving the loop_invariant, but even though I copied equation (with 'Old instead of 'Loop_entry) it doesn’t check out.
also, i’m certain there are simpler ways to write down these assertions. I’m all ears, this is new to me.

The type defines as such:

Maximum_Buffer_Size : constant := 1024;
subtype Buffer_Count_Type is Natural  range 0 .. Maximum_Buffer_Size;
subtype Buffer_Index_Type is Positive range 1 .. Maximum_Buffer_Size;
type    Buffer_Type is array (Buffer_Index_Type) of Character;

When your postcondition quantifies over an array, usually the loop invariant has to as well. Right now, your loop invariant only speaks about the current iteration, but you need to summarize what has happened until then in the loop to the entire array. Something like that:

      pragma Loop_Invariant (Count <= I - Buffer'First and (for all K in Buffer'First .. I - 1 => (if Buffer'Loop_Entry (K) = Ch then Buffer (K) = ' ')));

Oh, thank you for the tips.
Hey, considering the definition

subtype Buffer_Count_Type is Natural;
subtype Buffer_Index_Type is Buffer_Count_Type range 1..Buffer_Count_Type'Last;
type    Buffer_Type       is array (Buffer_Index_Type range <>) of Character;
procedure Copy_Into (Buffer : out Buffer_Type;
                     Source : in  String) is
   Characters_To_Copy : Buffer_Count_Type;
begin
   pragma Assert (Buffer'Last in Buffer_index_type);
   Characters_To_Copy := Buffer'Last;
   Buffer := (others => ' ');  -- initialize to all blanks
   if Source'Length < Characters_To_Copy then
      Characters_To_Copy := Source'Length;
   end if;
   for Index in Buffer'First .. Characters_To_Copy loop
      pragma Loop_Invariant (Index <= Source'Length
       and Characters_To_Copy  = Characters_To_Copy'Loop_Entry);
      Buffer (Index) := Source (Source'First + (Index - 1));
   end loop;
end Copy_Into;

Does the complaint buffers.adb:30:22: medium: assertion might fail, cannot prove lower bound for Buffer'Last in Buffer_index_type makes any sense ? It can’t prove that an array’s bound fits into its OWN index subtype ?

It has to support empty arrays for the type as well. Consider the following:

with Ada.Text_IO; use Ada.Text_IO;

procedure jdoodle is
    subtype Buffer_Count_Type is Natural;
    subtype Buffer_Index_Type is Buffer_Count_Type range 1..Buffer_Count_Type'Last;
    type    Buffer_Type       is array (Buffer_Index_Type range <>) of Character;
    
    v : Buffer_Type(1..0); -- Empty array with no elements
begin
    Put_Line("Last =" & v'Last'Image);
end jdoodle;

Output:

Last = 0
1 Like

It makes sense, but then what type is 'Last in that case ? Your exemple throws no exception, yet the rm says:

A’Last For a prefix A that is of an array type (after any implicit dereference), or denotes a constrained array subtype: A’Last denotes the upper bound of the first index range; its type is the corresponding index type.

Not a base type, not a universal numeric type, but the index type.

It may have something to do with section 14 of 3.6:

I’m not a language lawyer by any means but there is also verbiage talking about resolving the type of the index to root integer, and the RM specifically calls the index part a “subtype” so it may be that the language is allowed to further go down the type liniage to handle out of bounds indexes. That’s speculation on my part though, I don’ t know.

Please do not confuse a type and its first subtype. Types are anonymous in Ada, what you declare in a type declaration is the first subtype. So the index type is the type of the first subtype Buffer_Index_Type.
The type of, e.g., Integer is the full range of mathematical integers. Static expressions make use of this:
Static: constant := Integer'Last + Integer'Last;
“+” is the operator of Integer (not universal_integer).
Static: constant := Integer'Last + Long_Integer'Last;
This is illegal, there is no such operator.

Ok, so we’re dealing with an unconstrained array, right?
There’s a concept in Ada called the null array, that is an array of length zero, and it is denoted with any array which has 'First strictly greater-than 'Last.

This is a little bit of a rough-spot in Ada, insofar as compiler creators are concerned, because you are required to preserve the bounds —this means that you can pass information in a null array, such as P: Example_Array:= (-12..-32 => <>), and P'First must equal -12 and P'Last must equal -32— now, obviously this is a Bad Idea™, but is technically doable.

The reason that this is such a pain is because, (a) say given Type Example_Array is (Unsigned_8 range <>) of Character;, this means that the number of ways to represent the null array are the combinatorial explosion of 1*2*3*…*254 rather than just a single conceptual null array, and (b) the leaking of information [preservation of values] illustrated above means that some care must be made WRT optimization.

But, as to why Val : Array(Positive range <>) of Positive:= (1..0 => 1); doesn’t throw an exception is because of the practical reason of recursion: you don’t want to raise an exception when your conceptual “grab half the array and process X, recursively” bottoms out on an array of length-0 (the other base-case typically being length-1). — The language-lawyer reason is because the subtype (here Positive) is an subset of Integer the array type being a conceptual Universal_Array_of_Positive_Indexed_by_Integer with the first subtype being Universal_Array_of_Positive_Indexed_by_Integer with Index in Positive'Range: thus Val is specified as Universal_Array_of_Positive_Indexed_by_Integer, but having a length-0, there is no Index which violates the constraint and thus the object is valid. (IIUC)

Can you see why this won’t prove:

pragma Overflow_Mode (minimized);
pragma Ada_2022;
with Ada.Text_IO; use Ada.Text_IO;

procedure main with Spark_Mode is
   Maximum_Buffer_Size : constant := 1024;
   subtype Buffer_Count_Type is Natural  range 0 .. Maximum_Buffer_Size;
   subtype Buffer_Index_Type is Positive range 1 .. Maximum_Buffer_Size;
   type    Buffer_Type       is array (Buffer_Index_Type) of Character;
   
   procedure Copy_From (Buffer      : in Buffer_Type;
                    Destination : out String;
                    Point       : in  Buffer_Index_Type;
                    Length      : in  Buffer_Count_Type)
     with Depends => (Destination =>+ (Buffer, Point, Length)),
       Post =>  (if Point not in Buffer'Range or  Point + Length - 1 > Buffer'Last then (for all A of Destination => A = ' ')
             else Destination (Destination'First .. Destination'First + Positive'Min (Length, Destination'Length) - 1) = String (Buffer (Point .. Point - 1 + Positive'Min (Length, Destination'Length))));
             
   procedure Copy_From (Buffer      : in  Buffer_Type;
                        Destination : out String;
                        Point       : in  Buffer_Index_Type;
                        Length      : in  Buffer_Count_Type) is
   begin
      Destination := (others=> ' ');
      if Point not in Buffer'Range  or Point + Length - 1 > Buffer'Last then
         return;
      else
         Destination (Destination'First .. Destination'First + Positive'Min (Length, Destination'Length) - 1) := String (Buffer (Point .. Point - 1 + Positive'Min (Length, Destination'Length)));
      end if;      
   end Copy_From;   
begin
    null;
end main;

The expression in the postcondition and body are nearly identical. I would have used an expression function but there’s no way to easily “insert” an array in another with an expression. Eitherway, this should work, since it doesn’t complain about the body.

Ok, can you see what’s wrong here ?
It can not prove the postcondition, but does not complain about the loop invariant, despite it saying exactly the same thing (I copied and pasted it !!).
And here i can’t prove that the loop that erases a certain character, does remove that character:
both the invariant and postcondition (Post => (for all A of Buffer => A /= Erase_Character)) that says the same thing, fails to prove. That said, I am yet to read the guide in details. I will.

pragma Overflow_Mode (minimized);
pragma Ada_2022;
procedure main with Spark_Mode is
  subtype Buffer_Count_Type is Natural;
  subtype Buffer_Index_Type is Buffer_Count_Type range 1..Buffer_Count_Type'Last;
  type Buffer_Type is array (Buffer_Index_Type range <>) of Character;
  function Search (Haystack: Buffer_Type; Needle: String) return Buffer_Count_Type
  with Post =>
     ((if Search'result = 0 then Needle'Length not in 1..Haystack'Length or
      (for all I in Haystack'First..HayStack'Last - Needle'Length + 1 => Haystack (I..I+Needle'Length-1) /= Buffer_type(Needle))
      else Haystack(Search'Result..Search'Result+Needle'Length-1) = Buffer_type(Needle)
      and (for all I in Haystack'First..Search'Result-1 => Haystack(I..I+Needle'Length-1) /= Buffer_type(Needle))));
  function Search (Haystack: Buffer_Type; Needle: String) return buffer_count_type is
  begin
        if Needle'Length not in 1..Haystack'Length then return 0; end if;
  		for I in Haystack'First..HayStack'Last - Needle'Length + 1
  			when String (haystack (I..I+Needle'Length-1)) = Needle loop
			pragma Loop_invariant ((if I > Haystack'First then (for all J in Haystack'First .. I - 1 => String (Haystack(J..J+Needle'Length-1)) /= Needle)));
			return I;
		end loop;
		return 0;
  	end Search;  	        
begin
  null;
end main;
procedure Compact (Buffer: in out Buffer_type; Erase_Character, Fill_Character: Character; Valid: out Buffer_Count_Type) is
	Index: Buffer_Count_type := 0;
begin
	Valid := Buffer'Length;
	if Valid = 0 then return;
	else
		while Index < Buffer'Length loop
			pragma Loop_Invariant (Index in 0..Buffer'Length-1 and Valid in 1..Buffer'Length);
			pragma Loop_Invariant ((for all I in Buffer'First..Buffer'First+Index-1 => Buffer(I) /= Erase_character));
			if Buffer (Buffer'First + Index) = Erase_Character then
				Buffer (Buffer'First + Index .. Buffer'Last - 1 - Buffer'Length + Valid) := Buffer (Buffer'First + Index+1..Buffer'Last- Buffer'Length + Valid);
				Valid := @ - 1;
			else
				Index := @ + 1;
			end if;
			exit when Valid = 0;
		end loop;
		if Valid < Buffer'Length then
			pragma Assert (Valid in 0.. Buffer'Length);
			Buffer (Buffer'First + Valid  .. Buffer'Last) := [others => Fill_Character];
		end if;
	end if;
end Compact;