Rust took out cloudflare

Either one or another. Ada does not encourage stupid ways of programming as the example given… :grinning:

Let’s say I am writing code for a function F that uses a function G. If G raises an exception, I’d like to handle it immediately in F. If G does not raise an exception, I don’t want to waste time writing an exception handler, especially since Ada’s exception handling occurs at the end of F rather than at the place where F invokes G; i.e., someone reading F might not notice or understand that G is the cause of the potential exception.

How does a catch-all help with that? How would I even know to write a catch-all in F as opposed to higher up the chain?

(An aside: my impression is that exception handlers add some overhead which impacts performance. If that’s true, then adding a catch-all in F would impact performance, right? Correct me if I’m wrong.)

I’m not arguing the topic either way, but something I thought you might be interested in to look forward to in the future (I know, doesn’t help today) is Ada added the ability to leverage this type of feature in the current spec (Ada 2022). I haven’t tested GNAT 15 and higher, but I don’t think GNAT has implemented it yet.

They call it procedural iterators. I believe it would look like this in the new syntax:

-- Note I call Enumerate here, but  in Ada speak it would just be 
-- a call to Iterate, though the specification appears to allow you 
-- to call the operation whatever you want for this form
for (Index, Value) of Some_Container.Enumerate(<>) loop
   Put_Line (Value'Image & " @" & Index'Image);
end loop;

I don’t know if any of the containers will provide that specific iterator by default (couldn’t find the operations in the spec), but for personal use it’s not too hard to add. You would just extend the type:

   package Enumeration is

      package Vectors is new Ada.Containers.Vectors(Positive, Character);
      type Vector is new Vectors.Vector with null record;

      procedure Enumerate
         (Container : in out Vector;
          Process   : not null access procedure (Index : Positive; Value : in out Character))
          with Allows_Exit;
          
   end Enumeration;

   package body Enumeration is

      procedure Enumerate
         (Container : in out Vector;
          Process   : not null access procedure (Index : Positive; Value : in out Character))
      is begin
         for Index in Container.First_Index .. Container.Last_Index loop
            Process(Index, Container(Index));
         end loop;
      end Enumerate;

   end Enumeration;

Again, not trying to argue the topic in general, but I thought you might at least be interested in this feature. I wish GNAT would implement it soon if it hasn’t already.

3 Likes

You can use declare blocks to do exception handling at a narrower scope. For example:

declare
begin
   Something_That_Raises_An_Exception;
exception
   when others =>
       Handle_Exception;
end;

Ada won’t help with ensuring that F catches all exceptions from G, but SPARK will. In SPARK you must specify all exceptions that can be propagated from a subprogram, and GNATprove will emit a diagnostic if the exception is not handled.

So if G can propagate an exception, then it must state the exception(s) in its spec with an Exception_Cases aspect, then F must either handle the exception, or also state that the exception can be propagated in its Exceptional_Cases. This prevents any unhandled exceptions.

Whether or not exception handlers add overhead depend on the runtime environment. Typically, “zero cost exceptions” (ZCX) are used where there is no additional run-time overhead when exceptions are not raised. If an exception is raised, then there is a bit of overhead to unwind the stack and find the correct handler.

The other approach uses setjmp/longjmp (SJLJ) and has a slight overhead when entering a block with an exception handler, but the run-time cost when an exception is raised is a bit lower than ZCX.

3 Likes

This would be an appealing solution. I was aware of 5.5.1 and I use it, but I wasn’t aware of 5.5.3. Thank you!

I thought about using declare blocks later, but not at the time, so thank you for pointing that out, and for the rest of your answer, which was interesting.

I don’t think so, though I might have missed it.
Though we did talk about it as a sub-point/tangent for some other features, but the reason that we don’t is because (a) it would very quickly become unwieldy, (b) there are “unexpected”/surprising exceptions, particularly in odd corner-cases, and (c) there some exceptions that would force themselves out in viral/exponential growth as they propagate to anything that touches them. Example: Consider how having to handle Storage_Error at every level because you COULD exhaust your stack-space.

I threw together this Venn diagram:

I guess we know which part of the Rust circle Cloudflare is in.

2 Likes

I posted the link above. You missed it twice :face_with_tongue:

(d) leaving room for optimization. A code block can have instructions reordered, pieces removed etc.
(f) program semantics. E.g. x - y does not raise if the result stays in the range of the base type. The check is made upon assignment,
(g) non-deterministic execution, e.g. exceptions raised in parallel loops.

In general the fundamental concept of exception is delaying handling until the point were handling (recovery) were possible. So the very question illustrates lack of understanding what exceptions are actually for.

Not really a fan of iterators especially for arrays. Or the functional style in general for that matter but I can see some use for iterators at times.

The original 83 Booch components had some form of iterators and they were considered before designing the Ada containers so I assume if they don’t have them then it is for a reason.

Actually I use status codes as the light runtime does not have exception propagation. I like that I can show which functions have which codes but exceptions whilst heavy in the unhappy path are faster in the happy path.

package Locus is

  subtype Copy is Status_Code with
      Static_Predicate => Copy in Exception_Overflow | OK;

  subtype Bind is Status_Code with
      Static_Predicate =>
       Bind in Multiple_Binding | Exception_Overflow | OK;

  subtype Unbind is Status_Code with
      Static_Predicate => Unbind in Domain_Is_Not_Bound | OK;

  subtype Range_Of is Status_Code with
      Static_Predicate => Range_Of in Domain_Is_Not_Bound | OK;

end Locus;

   procedure Unbind
     (The_Domain   :        Domain;
      In_The_Map   : in out Map;
      Booch_Status :    out Locus.Unbind);

First. GCC (and GNAT) has zero-cost exceptions.

Second. Status code is an anti-pattern that should never be used.

1 Like

I also found this?

Aren’t exceptions supposed to be for exceptional cases and not for code flow?

Perhaps I should rename it Status instead of Status_Code as they are enums with case statements that just include the Status cases for each procedure via the predicates.

No. Exceptions are for code flow in exceptional cases.

Exceptionality is defined by the programmer and algorithm of choice. Exceptionality /= bug.

For example, end of file is an exceptional case handled by code flow terminating looping over the lines of the file.

That is exactly why this is an anti-pattern as it incurs distributed overhead and burden on the programmer. If you use a predicate then it must be static with obligatory correctness check.

“Raise statements and raise expressions are in SPARK. An exception is said to be expected if it is covered by a choice of an exception handler in an enclosing handled sequence of statements“

That’s pretty cool, SPARK having your back like that if it’s not too difficult to work with. Kinda wish I always had a runtime that supported exception propagation now. Either you prove it can’t happen or handle it or the tooling warns you, I assume. (except storage_error).

Right.

Note also that people frequently bring false argument about exceptions like that Storage_Error can be raised anywhere. This is true, but also true is that any correctness proof is conditional to an infinite number for preconditions, e.g. that the power cable will not plugged, that the gravitational constant is constant etc. Absence of Storage_Error raised asynchronously is such a precondition. Just consider it true and go on with relevant issues.

1 Like

In your large server application you will have A calling B calling C calling D calling E calling F calling G.
You will put some catch-all handlers at a few strategic places plus in the big loop/task/event handler/whatever in A, and you will see all issues. The operation of your program may be suboptimal on some, but it will continue to run in most cases (probably if you have a leak and the heap is exhausted, not for long).
With the panic behaviour your program is guaranteed to hang immediately.
So it is a question of choosing the lesser evil…

2 Likes

I get what you’re saying, but I wanted to catch exceptions of G in F, not in A. As in, I may be able to recover from them, or even if not (exceptional cases being truly exceptional) I’d at least like to know that G may throw an exception, and what sort, since that may affect how I write F. Your approach doesn’t help with that.

@dmitry-kazakov mentioned contracts, and that makes sense to me.

Something that has still not be mentioned in this discussion of exceptions, error handling, recovery, etc… Is Erlang/Elixir/Gleam/the general BEAM ecosystem. I HIGHLY recommend watching all of Joe Armstrong’s talks and explanations.

Basically, they acknowledged that large and complex systems will eventually fail somewhere. So instead of trying to create something unbreakable, you build recovery into the system at every level. This is a very interesting approach and it is how most systems in real live work. Additionally, some mechanical systems generally have a weak point by design to know where the system is most likely to fail, think of SRV (Safety Relief Valves).

I think it is something that we should consider in Ada (beyond the Last_Chance_Handler).

Best regards,
Fer

1 Like