This is what I do in nontrivial embedded projects. It makes things easier and less error-prone to replace malloc()/free()/etc implementations directly with ones that behave as needed. And then you don't have the standard implementations sitting around waiting for a new dev to accidentally use them.
The article somewhat alludes to this, although they don't use the notion of progress that you use, which is extremely important.
> This rule does not, of course, apply to iterations that are meant to be nonterminating—for example, in a process scheduler. In those special cases, the reverse rule is applied: It should be possible for a checking tool to prove statically that the iteration cannot terminate.
As a further searchable term, people can look up "total functions." Total functions must either terminate (loop ends) or be productive (always make forward progress). That as many functions as possible should be total is a pretty good rule of thumb in any codebase.
This part made me pause for a moment, but then it clicked, and I'm remembering the rare and unusual experience of facing a process that escaped a loop that was supposed to go on indefinitely. A software equivalent of transmission belt slipping off. Most frequently seen in video games, where it's mostly harmless - but imagining a scheduler in an embedded system quitting on me like this fills me with dread to irrational extent.
> As a further searchable term, people can look up "total functions." Total functions must either terminate (loop ends) or be productive (always make forward progress).
That's a new one to me. I'm not deep into more advanced flavors of functional programming, but I thought total functions are defined as terminating in finite time for all inputs. I suppose it depends on whether you consider infinite loop a successful termination in infinity or not, but personally I wouldn't, at least not when it's clear the function is plain running in circles (as opposed to asymptotically approaching a value).
The main paper that lays this out is "Total Functional Programming" (https://www.jucs.org/jucs_10_7/total_functional_programming/...).
But to briefly summarize:
I was playing a bit fast and loose with wording since most notions of total functions are built on top of FP languages as you point out and therefore ultimately on the lambda calculus where we don't have a native notion of a loop.
Instead of loops in FP languages we have lists (and folds on the list or an unfold to generate the list) and other data structures that reify the notion of iteration.
Hence what I was really saying is that a total function can also generate an infinite data structure, which is only roughly like saying an infinite loop.
Totality is usually formally defined in terms of data and codata (or equivalently recursion and corecursion). The co- prefix may sound a bit scary, but an informal way of thinking about it is that data is always finite and codata can be infinite. The canonical example is that a list is an example of data and an infinite, lazy stream is an example of codata.
This seems like a lot of ceremony for no real good reason if you consider an "infinite" data structure as simply a finite data structure that can store functions that happens to have additional computation in it hidden behind some closure. And indeed you are still correct that an informal and usually sufficiently useful definition of totality for a function is that it terminates in finite time for all inputs (after all even if you generate an "infinite" data structure you still terminate in finite time and just hide the additional computation within a finite data structure).
It's still nice to have the data vs codata distinction though.
Practically speaking bringing in the notion of codata gives us a nice little heuristic for checking termination when creating an infinite data structure. Only types marked as codata are allowed to have infinite descent and when returning them from a function we have to always follow a certain syntactic pattern known as guarded recursion. Otherwise if we didn't have the notion of codata, a naive termination checker would have a hard time with checking termination of a function that recurses on itself, but only behind a lambda.
E.g.
// Pseudo-code
data class Stream(head: Int, rest: () => Stream)
// generateIncrementingStream(0) generates the infinite stream 0, 1, 2, ...
generateIncrementingStream(start: Int): Int = {
Stream(start, () => generateIncrementingStream(start + 1))
// This appears to a naive termination checker to never terminate
// You could try to simply check if the recursive call is inside a function
// but then you run into other issues if e.g. the entire function is called
// again and it becomes very thorny very quickly
// e.g. Stream(start, {(() => generateIncrementingStream(start + 1))(); () => generateIncrementingStream(start + 1)}
// never terminates even though all calls to generateIncrementingStream are
// contained behind lambdas. You might have to start doing some annoying
// symbolic computation.
}
vs // Note that the recursive call to Stream in rest is no longer behind a lambda
// Under the hood this might be implemented with lambdas
codata class Stream(head: Int, rest: Stream)
def generateIncrementingStream(start: Int): Stream = {
// Much easier heuristic: all recursive calls must be inside a codata
// constructor, i.e. "guarded" by a constructor call hence "guarded recursion"
Stream(start, generateIncrementingStream(start + 1))
}
The general idea here is that a function that generates codata is productive and hence total if it generates a finite amount of the potentially infinite codata structure before its next recursive call. A nice heuristic that captures most useful cases is guarded recursion (of course because of the halting problem this is only a heuristic).The usual heuristic is that for functions that generate data, you must have structural recursion, i.e. every recursive call must be on a piece of data "smaller" than the original input. On the other hand if you generate codata, you must have guarded recursion, i.e. every recursive call must be guarded behind a codata constructor.
This resolves some things quite nicely. E.g. if have a recursive function that accepts a codata `Stream` and wants to return some data, e.g. a finite `List`, then we must have some other argument that gets smaller every function call. E.g.
// Taking in infinite codata and producing finite data
def take(stream: Stream, n: NaturalNum): List[Int] = {
n match {
// This passes totality checking because m is "smaller" than n, which here
// means we had to strip away one level of NaturalNum constructors
// hence
Successor(m) => Cons(stream.head, take(stream.rest, m))
Zero => EmptyList
}
}
def streamIdentity(stream: Stream): Stream = {
// Since we're producing codata, we check for guarded recursion
// Passes totality checker because streamIdentity is guarded behind Stream
Stream(stream.head, streamIdentity(stream.rest))
}
def listIdentity<A>(list: List[A]): List[A] = {
list match {
case EmptyList => EmptyList
// Since we're producing data, we check for structural recursion
// Passes totality checker because the recursive call is on a smaller piece of data
case Cons(x, rest) => Cons(x, listIdentity(rest))
}
}
// Can't blindly produce finite data from an infinite piece of codata!
def nontotalStreamToList(stream: Stream): List[Int] = {
// Looking for structural recursion
// Does not pass totality checker because stream.rest is not considered to be smaller than stream since Stream is codata
Cons(stream.head, nontotalStreamToList(stream.rest))
}
// Can definitely produce infinite codata from finite data though
def padOutListWithInfiniteZeroes(initialSegment: List[Int]): Stream = {
initialSegment match {
case Cons(x, rest) => Stream x (padOutListWithInfiniteZeroes(rest))
case EmptyList => Stream 0 (padOutListWithInfiniteZeroes(initialSegment))
}
}
Bringing in the notion of codata also gives us some theoretical nicety. How do you express totality for a program that must run forever, e.g. a server? You say that the IO type a la Haskell is codata and that's that! Then you know that `main` having the `IO` type means that it can potentially run forever, but it will always generate a finite amount of stuff on every codata "iteration" and therefore is productive and is not just spinning its wheels in a pointless infinite loop that does nothing.Similarly, allocation is fine with today’s large memories. You need to bound the request queue depth and the amount of memory allocated per request though.
I also question the idea that safety critical code is a special class that is distinct from normal code.
Buggy consumer devices lead to all sorts of deaths — after all, a one in a billion scenario is going to hit a few people per day on average — for example, look at the correlated 999 and 911 spam that hit iOS and (separately) android this week.
Many of the other rules broadly hold up though. You still want to avoid things like pointers because static analyzers are terrible at proving through them. This typically means LUTs are wrapped in the equivalent of an unsafe block though. Not ideal, and hopefully better tooling will eventually exist for these sorts of things.