Integer_Text_IO and Exception, Loops on Non-Integer Input

Hello again, it’s been a while.

There must be something I am missing here:

Some information: I am using bounded strings in Main_Menu, and the index is 1 .. 2, which would answer the results below the code. This is simply a test program.

-- Get user input
while not End_Program loop
   begin
      Ada.Integer_Text_IO.Get (Selection);
         
      if Command_String.To_String (Main_Menu (Selection)) = "quit" then
         End_Program := True;
      end if;
   exception
      when Constraint_Error | Ada.IO_Exceptions.Data_Error =>
         Ada.Text_IO.Put_Line ("Invalid command");
   end;
end loop;

This works, when I enter an integer. 0 => Invalid command and 3 => Invalid command, and it will wait until I re-enter a new value.

However, if I type in b or anything that is not an integer, it will loop Invalid command forever without asking me for another input, until I pull the plug on it.

Any and all insight is much appreciated.

I think what it is doing is failing to read in the input if there is an exception, so it just rereads it over and over again. Try adding a call to Ada.Text_IO.Skip_Line in your exception handler to tell it to skip over that line and see if that does anything

   exception
      when Constraint_Error | Ada.IO_Exceptions.Data_Error =>
         Ada.Text_IO.Put_Line ("Invalid command");
         Ada.Text_IO.Skip_Line;
   end;
1 Like

Thank you so much. This worked!

This infinite loop on invalid numeric input has existed since Ada 83. The description of Get is in ARM 10.8(7-10/2): “If the value of the parameter Width is zero, skips any leading blanks, line terminators, or page terminators, then reads a plus sign if present or (for a signed type only) a minus sign if present, then reads the longest possible sequence of characters matching the syntax of a numeric literal without a point.”

In your case that “longest possible sequence of characters” is presumably zero characters.

What’s important is what it doesn’t say: It doesn’t say that it reads any characters after that “longest possible sequence of characters”. Any characters at that point in the input are unread and remain for the next input operation.

What you are saying by adding a Skip_Line is that the program has to process complete lines of input at a time. This is better expressed by actually inputting complete lines and processing them. I use the convention that user input is always read a line at a time (using function Ada.Text_IO.Get_Line), followed by parsing that line.

Then there is the awkward construction

while not End_Program loop

Psychology tells us that negative logic (not End_Program) is more difficult to understand than positive logic (End_Program), and the the exit condition of a loop is more intuitive than the continuation condition. Like many while loops, this one uses both negative logic and the continuation condition, maximizing the difficulty of understanding it (admittedly not very bad in this case, but in real cases, often significant). Better would be

loop
   exit when End_Program;

My convention is to use a plain loop with exit when that results in positive logic, and a while loop (while Program_Running loop) when that uses positive logic.

Finally, doing an Elements of Programming Style-like analysis of the whole loop, it loops until the user input causes a change to End_Program, at which point it exits. This is a classic “loop and a half”, which are more clear using an exit statement than a Boolean variable, since the use of a variable separates the logic that results in the exit from the exit itself. Simply put, with an exit statement, the reader knows immediately that result of this input is exiting the loop; with the variable, that requires remembering that the variable controls the loop (again, not a big problem in this example, but can be for real cases).

Combining these observations results in

loop
   declare
      Line : constant String := Ada.Text_IO.Get_Line;
   begin
      exit when Command_String.To_String (Main_Menu (Integer'Value (Line) ) ) = "quit";
   exception
   when Constraint_Error => -- Data_Error is no longer possible
      Ada.Text_IO.Put_Line (Item => "Invalid command");
   end;
end loop;
4 Likes

Thank you @JC001 for your very informative, and educational, response. You went above and beyond - as seems to be the standard in this community.

I tested the code you wrote, and it does in fact remove the possibility of a Data_Error. It was interesting to see that Integer'Value gives a constraint error when using non-integer values.

Regarding your suggestion to use positive logic was a nice point, thank you for that. I will keep that in mind henceforth.

This should not be surprising. Data_Error is an I/O exception (since it’s declared in Ada.IO_Exceptions), and there’s no I/O in the executable part of the block statement.