Many safety critical systems in Ada ban the usage of dynamic allocation and Unchecked_Deallocation by adding "pragma Restrictions (No_Heap);" and "pragma Restrictions (No_Dependence => Ada.Unchecked_Deallocation);" at the top of the file where the main subprogram is located (application entry). The tradition when these pragma are in effect is to define the entities used in the application in arrays. The sizes of the arrays need not be defined at compile-time but can be determined at application startup (run-time). It means the sizes of the arrays can be specified in configuration files and vary depending on the hardware support the application is installed upon. Just because the entities/objects are located at indexes in an array it doesn't mean that they need to know about it and can point to other objects using access-to-object type variables (references). The problem with dynamic allocations is the risk of memory fragmentation and the performance of the application may "mysteriously" degrade over time. One also runs the risk of running out of heap memory unless the application checks for example there is at least 5% memory left on the device for the heap allocation to be successful.
Also note that one can run into memory leak problems using automatic garbage collected languages. I've personally needed to track down memory leaks in both C# and Javascript applications. Thankfully this rarely happens. It indicates that even when working in an automatic garbage collected language a developer needs to be aware of potential memory issues and think carefully about architecture.
Glad to hear you were successful in the project (with 100s of developers)!
Thanks for clarifying how the dangling pointers may arise. Not everyone agrees with me, but these are my thoughts/recommendations when using Ada. Which thread/task that has ownership of a variable is paramount. Whenever one defines a variable it must be crystal clear which thread/task that owns it, for example has the right the read or write a value to the variable. What I recommend is the Actor Model (https://en.wikipedia.org/wiki/Actor_model). Synchronization between two tasks can either be through shared variables or message passing. Last time I checked Academia is inconclusive as to what is the best (least error-prone) way for threads/tasks to communicate. What seems the simplest to me is message passing. 10 years ago, first time I heard of the Actor Model and message passing is Erlang and it's a language where these ideas are fundamental. So a task owns a variable. If another task wishes to change the value of that variable it must send a message to the owning task and request it to change the value. If another task wishes to know the value it must ask the owning task what the value is. Since the time I heard of Erlang, other languages like Rust and the Pony language has picked up on this too. Rust has taken this further by making it possible for one task to temporarily borrow ownership to another task and it is checked by the borrow-checker.
To implement the Actor Model in Ada one puts all variables in the body of the tasks that are in the application. It makes them not visible from other tasks. So what you need to keep in mind when developing is for a task to never send an access-to-object type variable to another task. If there is a need to do that you need to use Ada/SPARK or Rust to get the proper ownership checking done. Btw, Codepeer (static code analysis tool for Ada) finds race-conditions, has deadlock detection, and warns if there are variables that may be read or written to by more than one task.
If one sticks to vanilla Ada (not SPARK) one could develop an application based on libadalang that parses all the Ada source code and checks that all task entries have input arguments that do not contain any access-to-object types (to find instances where a developer has sent an access-to-object variable to another task by mistake). Such a tool does not exist but libadalang exists to allow the creation of custom rules checking on one's Ada code.
I'm confused not more complex large-scale software projects are written in Ada. The way I understand it, they are not complicated enough you need Ada.
For example, Ada makes it easy to structure one's code in a strict tree hierarchy (whenever you with a package put pragma Elaborate_All (..) on it, which works well with any Ada compiler I've tried it with and is in the Ada standard since 1995). It makes monolith creation by mistake impossible.
Strange you had memory management problems in an Ada application. With all the focus on safety and security within the Ada community, it makes me wonder how the software engineers were using the language in that project?
Hi Geofft! Ada developer here. When I read and study the Ada reference manual for the 1995 standard I get the impression that the Ada language designers were not thinking of third-party garbage collection or reference counting as the primary way of achieving memory safety but they were thinking of arena pools/storage pools. When one defines an access-to-object type in the 1995 standard one can specify in which storage pool the allocated object ends up in. The Ada standard does not talk about Stack and heap but talks about Stack and Storage pools. I get the impression that the idea is for an Ada application to get a number of storage pools (with statically determined sizes?) and one can allocate objects inside of these and when one is finished with the objects in a pool one deallocates them all at once by emptying the pool and then one can reuse it again. More efficient and less-error-prone than deallocating each object separately. You are right in point number 2 that the door for garbage collection is open in the Ada standard. No Ada compiler vendor has implemented a GC but you are right about it being considered. The idea for arena pool/storage pool can also been seen in the Ada language (also 1995 standard) by being able to define an access-to-object type locally inside a function/subprogram and at the point of the access-to-object types existence some memory is heap allocated (the size of the allocated memory is specified by the access-to-object type definition) and when the access-to-object type goes out of scope the memory is deallocated (without the use of unchecked_deallocation)... so there should be a forth point on arena/storage pools on your list. And it may be more suited for "a (say) safe implementation of the DOM that can render real-world web pages while efficiently using memory on a general-purpose computer".
To use storage pools in Ada I would recommend Deepend (https://sourceforge.net/projects/deepend/). Deepend is a storage pool with subpool capabilities for Ada 2012, Ada 2005, and Ada 95. Memory allocations can be associated with subpools and subpools can be deallocated as a whole which provides a safer alternative than managing deletions of individual objects. It also is likely to be more deterministic and efficient than garbage collection.
Also note that one can run into memory leak problems using automatic garbage collected languages. I've personally needed to track down memory leaks in both C# and Javascript applications. Thankfully this rarely happens. It indicates that even when working in an automatic garbage collected language a developer needs to be aware of potential memory issues and think carefully about architecture.
Glad to hear you were successful in the project (with 100s of developers)!