Gem #17: Pragma No_Return, Part 2 (functions)
by Bob Duff —AdaCore
Let's get started…
A function should always return a value or raise an exception. That is, reaching the "end" of a function is an error. In Ada, this error is detected in two ways:
1. Every function must contain at least one return statement. This rule is too liberal, because some control-flow path might bypass the return statement. This rule is too conservative, because it requires a return even when all you want to do is raise an exception:
function F (...) return ... is begin raise Not_Yet_Implemented; -- Illegal! end F;
Function F isn't WRONG -- it's just not finished. In my opinion, this rule should have been abolished, and replaced with a control-flow analysis similar to what GNAT does (see below).
2. If a function reaches its "end", Program_Error
is raised. This catches all such errors, but it's unsatisfying; a compile-time check would be better.
Indeed, GNAT provides an effective compile-time check: it gives a warning if there is any control-flow path that might reach the "end":
function F (...) return ... is begin if Some_Condition then return Something; elsif Some_Other_Condition then return Something_Else; end if; -- CAN get here! end F;
There is a bug, if Some_Condition
and Some_Other_Condition
don't cover all the cases, and GNAT will warn. We can fix it in several ways, such as:
function F (...) return ... is begin if Some_Condition then return Something; elsif Some_Other_Condition then return Something_Else; else raise Some_Exception; end if; end F;
or:
function F (...) return ... is begin if Some_Condition then return Something; else pragma Assert (Some_Other_Condition); return Something_Else; end if; end F;
or (and here's the No_Return
):
procedure Log_Error (...); pragma No_Return (Log_Error); function F (...) return ... is begin if Some_Condition then return Something; elsif Some_Other_Condition then return Something_Else; else Log_Error (...); end if; end F;
GNAT will not cry wolf on the last three versions of F, because it does a simple control-flow analysis to prove that every path reaches either a return statement, or a raise statement, or a non-returning procedure (which presumably contains a raise statement, directly or indirectly).
One way to look at it is that pragma No_Return
allows you to wrap a raise statement in a slightly higher level abstraction, without losing the above-mentioned control-flow analysis. No_Return
is part of the contract; F relies on Log_Error
not returning, and the compiler ensures that the body of Log_Error
obeys that contract.