What exceptions are thrown?

How do I find out what exceptions a procedure or function throws?

Is there a language construct like “throws” in C++ or is it up to the developer to document this in comments?

I’m wrapping a C library that returns a handle in an argument unless the error code returned is non-zero. I would rather not have users of my library check for null handles so I thought I would convert errors to Ada exceptions. Is this the way to go?

No, there is no direct equivalence to “throws” in Ada, at least not yet. There are proposals to add “exception contracts” to future versions of Ada. At present, you can write a subprogram post-condition that contains expressions of the form “if [no failure] then True else raise [some exception]”. But then [no failure] must be a valid Ada expression.

To document which exceptions can be propagated from a subprogram (the Ada term for “procedure or function”) I usually write comments at the declaration of the subprogram.

But note that Ada code executes many built-in checks, such as array indexing checks, and those can raise predefined exceptions if they fail. Whether those should be documented per subprogram depends on context. In most cases I document only exceptions that are explicitly raised.

I have a vague feeling that there have been proposals for a raises keyword, or something similar (=> “this subprogram may raise these exceptions”), but they were rejected by the ARG.

You might try something like

function Foo (<args>) return not null Handle;

but it could be hard to actually implement Foo (what to do if the C library returns an error?), so raising an explicit exception would probably be the way to go.

No exceptions are thrown by Ada subprograms. While unruly, combative languages may throw things, Ada is a refined Victorian lady who politely raises exceptions. (In other words, in an Ada forum, it’s a good idea to use Ada terminology.)

The problem with providing such a list is that it has to be complete, and in Ada, a subprogram may raise an exception that is not visible at the point of the subprogram’s declaration, and so cannot be included in the list. Also, anything may raise Storage_Error; would all such lists have to name it?

3 Likes

I work with a zero footprint runtime where exception propagation does not exist. I use static predicate subtype on a packages status enum above each function. This not only documents the status and exceptions but a case statement on that static predicate causes the compiler to catch any missing checks upon exception or status conditions. In addition, library changes to the status will be caught by the users case statements. Whether an exception is program or functionality terminating should come down to the user at the end of the day. Something Ada thankfully lets you control in a nice fashion. Go and Rust panic recovery is quite ugly, I believe.

Hi @kevlar700, can you provide short example code illustrating the method you just described? Thx.

package Wireless_Package is

   package Status is
      type Code is
        (OK,
         SPI_Err_CRC,
         SPI_Err_Overrun,
         SPI_Err_Timeout,
         SPI_Busy,
         SPI_Fail,
         Wake_Timeout,
         APB_Poll_Timeout,
         Firmware_Processing_Timeout,
         Incorrect_Access_Mode,
         SRAM_Write_Failure,
         Data_Length,
         IRQ_Pin_Timeout,
         IRQ_Queue_Size_Mismatch,
         Pool_Bytes_Exhausted,
         Pool_Bytes_Insufficient,
         Pool_Slot_Non_Existant,
         Pool_Invalid_Size,
         Pool_Slots_Exhausted,
         Invalid_Data,
         Exception_Convert_8_To_32_Array,
         Exception_STM32_SPI_Run_Configure) with
        Default_Value => OK;
   end Status;
   
   subtype Status_SPI is Status.Code with
       Static_Predicate =>
        Status_SPI in
          Status.SPI_Busy | Status.SPI_Err_CRC | Status.SPI_Err_Overrun
          | Status.SPI_Err_Timeout | Status.SPI_Fail;
                    
   subtype Status_Initialise is Status.Code with
       Static_Predicate =>
        Status_Initialise in
          Status_SPI | Status.Data_Length | Status.Firmware_Processing_Timeout
          | Status.Wake_Timeout | Status.Incorrect_Access_Mode
          | Status.Exception_Convert_8_To_32_Array
          | Status.Exception_STM32_SPI_Run_Configure | Status.OK;
   procedure Initialise
     (SPI_Dev    : in out STM32_SVD.SPI.SPI_Peripheral;
      Timeout_Ms : in     STM32.Millisecond_Timer.Max_Ms;
      Wireless_Status :    out Status_Initialise);
      
   procedure Reset;
   
   subtype Status_Enable_Sleep is Status.Code with
       Static_Predicate => Status_Enable_Sleep in Status_SPI | Status.OK;
   procedure Enable_Sleep
     (SPI_Dev    : in out STM32_SVD.SPI.SPI_Peripheral;
      Timeout_Ms : in     STM32.Millisecond_Timer.Max_Ms;
      Wireless_Status :    out Status_Enable_Sleep);

private
   function Translate_SPI_Status
     (SPI_Status : in STM32.SPI.Status.Code)
      return Status.Code;

   function Translate_Pool_Status
     (Pool_Status : in 
     Elansys.Container_Array.Status.Code)
        return Status.Code;
      
end Wireless_Package;
4 Likes

This cool, thanks!

Pondering if I should adopt it as I want to go ZFP on the RPi 3 and 4.

I suppose there’s no other way in absence of exceptions.

1 Like

Note that if you assign a status to a subtype that is out of it’s range by mistake then you will get a runtime exception. I decided there must be some kind of logic error in that case and so program termination works for me in that case or rather non case, haha.

Thanks for posting the example. The made it crystal clear for me.

1 Like

I can’t speak for Go, but on Rust they view panics very similar to how Ada views unhandled exceptions. There are some hooks you can use to do some cleanup when they happen (similar to an exception handler only the panic is still executed and the thread terminates). Rust tends to favor a similar methodology to what you are using: returning an error type (the rust type also allows for packaging data with the error type). They provide the Result<> type for error handling. It somewhat analogous to this:

    type Result_Status is (Ok, Error);
    
    generic
        type Error_Type is private;
        type Ok_Type is private;
    package Generic_Result is
        type Result(Status : Result_Status := Error) is record
            case Status is
                when Ok    => Value      : Ok_Type;
                when Error => Error_Data : Error_Type;
            end case;
        end record;
    end Generic_Result;

Rust also provides language hooks to allow more streamlined use of the Result<> type, like special operators, specialized case statement support, and some built in type related functionality.

REF: std::result - Rust

1 Like

Rust panics aren’t intended to be recoverable. They aren’t like Adas nice exception handling at all to be honest. You are supposed to use Result but that doesn’t help if the runtime or a library panics. Having said that. I am not a fan of Adas heavier runtimes that support exceptions.

I also wouldn’t want data to be packaged. That isn’t really compatible with embedded practices. I use a configurable but fixed size round robin logging system.

That’s what I was saying. Panics are like unhandled exceptions (not recoverable). I generally run baremetal Ada runtimes in GNAT, so it isn’t really any different from a panic in that sense. You mentioned “rust panic recovery is ugly” in your previous post, I was just mentioning that in Rust you don’t generally recover (my reference to the thread still terminating). So the recovery is non existent rather than ugly is all. You have the option to do some cleanup before it actually happens (similar to using the last chance handler in GNAT), but it still happens.

Right, fair enough. However an unhandled exception in full Ada is easily recoverable and easy to handle. Even on zfp. If you put an exception handler in every function then you can handle all exceptions including system generated ones. I don’t believe that is possible in Rust. It certainly isn’t a nice syntax. Caveat: I believe Storage_Error cannot be handled though.

p.s. I am not recommending handling all exceptions. Just that you can and the need to do so is application specific.

1 Like

You shouldn’t try to recover from an unhandled exception, except by restart, because you don’t know what state the program is in; you only know that the design/implementation wasn’t expecting it.

All exceptions are unhandled until they are handled. There is lots of information that you might use and plenty of ways of resetting functionality without rebooting everything.

For example an array out of bounds in Rust causes a panic that you are not provided any nice way to recover from. In Ada it is perfectly reasonable and nicely supported to handle it locally and return a failed status in this scenario.

Of course it is even better to avoid it with Spark mode, when you have the time and understanding.

You are right though. You are unlikely to have the understanding to handle an unhandled exception from a third party library. On zfp. You can’t even, unless you edit the library itself.

I wasn’t thinking about pragma Restrictions (No_Exception_Propagation);, in the presence of which you can only check before executing the code which might fail, or use SPARK to minimise the chance of failure, or–as you’ve shown above–use status returns instead of exceptions.

In full Ada, though, what’s normally meant by ‘unhandled’ is an exception which reaches the top of a task, including the environment task.

I suspect that catch exception unhandled in GDB breaks on the last chance handler.

1 Like

Yes sorry, I have much less experience of full Ada.

in the presence of which you can only check before executing the code which might fail

Not exactly. An exception handler at the bottom of the procedure acts as a nice goto. You can recover immediately from constraint or range or integer overflows and whilst you may need to consider in out variable changes. Generally the stack clears itself up when you return. You can even create something safe to return. It doesn’t work with contracts etc. though. You should certainly perform checks before. It is really an alternative to spark; generally logging a bug without re-setting everything.

1 Like