Description
If we solve #157 then we'll know at compile-time the maximum stack usage of the entire call graph.
Except for recursion. Recursion - whether direct or indirect - represents a loop in the graph.
Here you can see indirect recursion B->C->E->D->B...
Recursion is problematic because it can happen a number of times that is only known at runtime, whereas the stack size is determined at compile-time. If too much recursion happens, we get stack overflow. Apart from being an annoying bug, stack overflow is a real concern for embedded software, which typically must resort to heuristics to find out just how small of a stack size they can get away with using.
But recursion is very useful. It's an intuitive way to reason about control flow. @Hejsil wrote zig's self-hosted parser without recursion, and it is not as straightforward as recursive programming.
So what do we do?
We have our cake and eat it, too. That's what we do.
Looking at the graph above again, we can have every function call in the graph be normal, except for D calling B. That's the only entrance into the cycle. So we can make there be a compile error for a normal function call from D to B. To make a function call which starts a cycle, you have to use a builtin:
const new_stack = try allocator.alloc(u8, @stackSize(function));
defer allocator.free(new_stack);
_ = @recursiveCall(new_stack, function, args);
It is a compile error to use @recursiveCall
when you could have used a normal function call, and it is a compile error to use a normal function call when you must use @recursiveCall
. So the compiler always tells you what to do.
I've prototyped how this would be emitted to LLVM using inline assembly, and I am talking to the LLVM mailing list to find out what's the best way to accomplish this. http://lists.llvm.org/pipermail/llvm-dev/2018-May/123217.html
There's another, simpler way we can have safe recursion, by limiting the amount of recursion at compile-time. This would be another builtin, that would look like this:
var cycles_left: usize = undefined;
_ = @limitedRecursiveCall(999, &cycles_left, function, args);
if (cycles_left == 0) @panic("recursion limit exceeded");
This would work the same way except instead of allocating memory on the heap, we specify the maximum number of cycles, and the function call fails if the cycle limit is exceeded. Then when calculating the stack upper bound, we multiply the cycle stack size by the recursion limit.
So you then would decide at compile time how much stack space you're going to ask for from the OS.
The benefit of this idea is that it actually does not depend on any changes to LLVM and we could implement it even before #157 is finished.
Until #105 is done we would have to specify the Recursive or LimitedRecursive calling convention on the first node getting called in the cycle (B in the picture).
Metadata
Metadata
Assignees
Labels
Type
Projects
Status