TLA+ sequence not being updated with Append or Tail calls - tla+

The Problem
I'm playing around with TLA+, and thought I'd write the following clearly false specification in PlusCal:
---- MODULE transfer ----
EXTENDS Naturals, TLC, Sequences
(* --algorithm transfer
\* Simple algorithm:
\* 1. Start with a shared-memory list with one element.
\* 2. A process adds arbitrary numbers of elements to the list.
\* 3. Another process removes arbitrary numbers of elements from the list,
\* but only if the list has more than one item in it. This check is
\* applied just before trying to removing an element.
\* Is it true that the list will always have a length of 1?
\* You would expect this to be false, since the adder process can add more elements
\* than the remover process can consume.
variables stack = <<0>>
process Adder = 0
begin
AddElement:
stack := Append(stack, Len(stack));
either goto AddElement
or skip
end either;
end process;
process Remover = 1
begin
RemoveElement:
\* Pop from the front of the stack
if Len(stack) > 1 then
stack := Tail(stack);
end if;
either goto RemoveElement
or skip
end either;
end process;
end algorithm *)
IsStackAlwaysUnitLength == Len(stack) = 1
====
After checking IsStackAlwaysUnitLength as one of the temporal properties to report on, I expected TLA+ to mark this property as failed.
However, all states passed! Why is it not failing?
Debugging Attempts
On debugging with print statements, I noticed the following odd behaviour:
process Adder = 0
begin
AddElement:
print stack;
print "Adder applied!";
stack := Append(stack, Len(stack));
print stack;
print "Adder task complete!";
\* Force
either goto AddElement
or skip
end either;
end process;
process Remover = 1
begin
RemoveElement:
\* Pop from the front of the stack
print stack;
print "Remover applied!";
if Len(stack) > 1 then
stack := Tail(stack);
print stack;
print "Remover task complete!";
else
print "Remover task complete!";
end if;
either goto RemoveElement
or skip
end either;
end process;
yields in the debugging panel
<<0>>
"Adder applied!"
<<0>>
"Adder task complete!"
<<0>>
<<0>>
"Remover applied!"
"Remover applied!"
"Remover task complete!"
"Remover task complete!"
<<0>>
"Adder applied!"
<<0>>
"Adder task complete!"
I'm unsure why stack := Append(stack, Len(stack)); and stack := Tail(stack); are not updating the global stack variable.
Full TLA Specification Generated
---- MODULE transfer ----
EXTENDS Naturals, TLC, Sequences
(* --algorithm transfer
variables stack = <<0>>
process Adder = 0
begin
AddElement:
stack := Append(stack, Len(stack));
either goto AddElement
or skip
end either;
end process;
process Remover = 1
begin
RemoveElement:
\* Pop from the front of the stack
if Len(stack) > 1 then
stack := Tail(stack);
end if;
either goto RemoveElement
or skip
end either;
end process;
end algorithm *)
\* BEGIN TRANSLATION
VARIABLES stack, pc
vars == << stack, pc >>
ProcSet == {0} \cup {1}
Init == (* Global variables *)
/\ stack = <<0>>
/\ pc = [self \in ProcSet |-> CASE self = 0 -> "AddElement"
[] self = 1 -> "RemoveElement"]
AddElement == /\ pc[0] = "AddElement"
/\ stack' = [stack EXCEPT ![0] = Append(stack, Len(stack))]
/\ \/ /\ pc' = [pc EXCEPT ![0] = "AddElement"]
\/ /\ TRUE
/\ pc' = [pc EXCEPT ![0] = "Done"]
Adder == AddElement
RemoveElement == /\ pc[1] = "RemoveElement"
/\ IF Len(stack) > 1
THEN /\ stack' = [stack EXCEPT ![1] = Tail(stack)]
ELSE /\ TRUE
/\ stack' = stack
/\ \/ /\ pc' = [pc EXCEPT ![1] = "RemoveElement"]
\/ /\ TRUE
/\ pc' = [pc EXCEPT ![1] = "Done"]
Remover == RemoveElement
Next == Adder \/ Remover
\/ (* Disjunct to prevent deadlock on termination *)
((\A self \in ProcSet: pc[self] = "Done") /\ UNCHANGED vars)
Spec == Init /\ [][Next]_vars
Termination == <>(\A self \in ProcSet: pc[self] = "Done")
\* END TRANSLATION
IsStackAlwaysUnitLength == Len(stack) = 1
====

Congratulations, you've hit a PlusCal bug! And also an edge case that's not a bug but still unintuitive. Let's start with the bug.
Sometimes when using PlusCal we want to have multiple processes share labels. We do this with a procedure. In order to make it all work the PlusCal translator adds an extra bookkeeping variable called stack. Normally, if the user defines a variable foo that conflicts with a generated variable foo, the translation renames one to foo_. In this case, since there was no conflict, there wasn't any renaming.* The bug is that the translator got confused and translated the variable as if it was supposed to be the bookkeeping stack. You can see this as it turned the append into
stack' = [stack EXCEPT ![0] = Append(stack, Len(stack))]
When it should just be
stack' = Append(stack, Len(stack))
You can fix this by renaming stack to mystack. That should get the spec behaving properly. But it will still pass: that's because you put IsStackAlwaysUnitLength as a property and not an invariant. As a temporal property, IsStackAlwaysUnitLength is true if it's true in the initial state. As an invariant, IsStackAlwaysUnitLength is true if it's true in all states.** YOu can get the spec to fail properly by changing IsStackAlwaysUnitLength from a temporal property to an invariant in the "what is the model" page.
*Actually in this case the translator won't rename stack if you add a procedure, it just throws an error. But that's still fail-safe.
**This is because TLC (the model checker) treats the invariant P as the temporal property []P. It's syntactic sugar, basically.

Related

how to model a queue in promela?

Ok, so I'm trying to model a CLH-RW lock in Promela.
The way the lock works is simple, really:
The queue consists of a tail, to which both readers and writers enqueue a node containing a single bool succ_must_wait they do so by creating a new node and CAS-ing it with the tail.
The tail thereby becomes the node's predecessor, pred.
Then they spin-wait on pred.succ_must_wait until it is false.
Readers first increment a reader counter ncritR and then set their own flag to false, allowing multiple readers at in the critical section at the same time. Releasing a readlock simply means decrementing ncritR again.
Writers wait until ncritR reaches zero, then enter the critical section. They do not set their flag to false until the lock is released.
I'm kind of struggling to model this in promela, though.
My current attempt (see below) tries to make use of arrays, where each node basically consists of a number of array entries.
This fails because let's say A enqueues itself, then B enqueues itself. Then the queue will look like this:
S <- A <- B
Where S is a sentinel node.
The problem now is, that when A runs to completeness and re-enqueues, the queue will look like
S <- A <- B <- A'
In actual execution, this is absolutely fine because A and A' are distinct node objects. And since A.succ_must_wait will have been set to false when A first released the lock, B will eventually make progress, and therefore A' will eventually make progress.
What happens in the array-based promela model below, though, is that A and A' occupy the same array positions, causing B to miss the fact that A has released the lock, thereby creating a deadlock where B is (wrongly) waiting for A' instead of A and A' is waiting (correctly) for B.
A possible "solution" to this could be to have A wait until B acknowledges the release. But that would not be true to how the lock works.
Another "solution" would be to wait for a CHANGE in pred.succ_must_wait, where a release would increment succ_must_wait, rather than reset it to 0.
But I'm intending to model a version of the lock, where pred may change (i.e. where a node may be allowed to disregard some of its predecessors), and I'm not entirely convinced something like the increasing version wouldn't cause an issue with this change.
So what's the "smartest" way to model an implicit queue like this in promela?
/* CLH-RW Lock */
/*pid: 0 = init, 1-2 = reader, 3-4 = writer*/
ltl liveness{
([]<> reader[1]#progress_reader)
&& ([]<> reader[2]#progress_reader)
&& ([]<> writer[3]#progress_writer)
&& ([]<> writer[4]#progress_writer)
}
bool initialised = 0;
byte ncritR;
byte ncritW;
byte tail;
bool succ_must_wait[5]
byte pred[5]
init{
assert(_pid == 0);
ncritR = 0;
ncritW = 0;
/*sentinel node*/
tail =0;
pred[0] = 0;
succ_must_wait[0] = 0;
initialised = 1;
}
active [2] proctype reader()
{
assert(_pid >= 1);
(initialised == 1)
do
:: else ->
succ_must_wait[_pid] = 1;
atomic {
pred[_pid] = tail;
tail = _pid;
}
(succ_must_wait[pred[_pid]] == 0)
ncritR++;
succ_must_wait[_pid] = 0;
atomic {
/*freeing previous node for garbage collection*/
pred[_pid] = 0;
}
/*CRITICAL SECTION*/
progress_reader:
assert(ncritR >= 1);
assert(ncritW == 0);
ncritR--;
atomic {
/*necessary to model the fact that the next access creates a new queue node*/
if
:: tail == _pid -> tail = 0;
:: else ->
fi
}
od
}
active [2] proctype writer()
{
assert(_pid >= 1);
(initialised == 1)
do
:: else ->
succ_must_wait[_pid] = 1;
atomic {
pred[_pid] = tail;
tail = _pid;
}
(succ_must_wait[pred[_pid]] == 0)
(ncritR == 0)
atomic {
/*freeing previous node for garbage collection*/
pred[_pid] = 0;
}
ncritW++;
/* CRITICAL SECTION */
progress_writer:
assert(ncritR == 0);
assert(ncritW == 1);
ncritW--;
succ_must_wait[_pid] = 0;
atomic {
/*necessary to model the fact that the next access creates a new queue node*/
if
:: tail == _pid -> tail = 0;
:: else ->
fi
}
od
}
First of all, a few notes:
You don't need to initialize your variables to 0, since:
The default initial value of all variables is zero.
see the docs.
You don't need to enclose a single instruction inside an atomic {} statement, since any elementary statement is executed atomically. For better efficiency of the verification process, whenever possible, you should use d_step {} instead. Here you can find a related stackoverflow Q/A on the topic.
init {} is guaranteed to have _pid == 0 when one of the two following conditions holds:
no active proctype is declared
init {} is declared before any other active proctype appearing in the source code
Active Processes, includig init {}, are spawned in order of appearance inside the source code. All other processes are spawned in order of appearance of the corresponding run ... statement.
I identified the following issues on your model:
the instruction pred[_pid] = 0 is useless because that memory location is only read after the assignment pred[_pid] = tail
When you release the successor of a node, you set succ_must_wait[_pid] to 0 only and you don't invalidate the node instance onto which your successor is waiting for. This is the problem that you identified in your question, but was unable to solve. The solution I propose is to add the following code:
pid j;
for (j: 1..4) {
if
:: pred[j] == _pid -> pred[j] = 0;
:: else -> skip;
fi
}
This should be enclosed in an atomic {} block.
You correctly set tail back to 0 when you find that the node that has just left the critical section is also the last node in the queue. You also correctly enclose this operation in an atomic {} block. However, it may happen that --when you are about to enter this atomic {} block-- some other process --who was still waiting in some idle state-- decides to execute the initial atomic block and copies the current value of tail --which corresponds to the node that has just expired-- into his own pred[_pid] memory location. If now the node that has just exited the critical section attempts to join it once again, setting his own value of succ_must_wait[_pid] to 1, you will get another instance of circular wait among processes. The correct approach is to merge this part with the code releasing the successor.
The following inline function can be used to release the successor of a given node:
inline release_succ(i)
{
d_step {
pid j;
for (j: 1..4) {
if
:: pred[j] == i ->
pred[j] = 0;
:: else ->
skip;
fi
}
succ_must_wait[i] = 0;
if
:: tail == _pid -> tail = 0;
:: else -> skip;
fi
}
}
The complete model, follows:
byte ncritR;
byte ncritW;
byte tail;
bool succ_must_wait[5];
byte pred[5];
init
{
skip
}
inline release_succ(i)
{
d_step {
pid j;
for (j: 1..4) {
if
:: pred[j] == i ->
pred[j] = 0;
:: else ->
skip;
fi
}
succ_must_wait[i] = 0;
if
:: tail == _pid -> tail = 0;
:: else -> skip;
fi
}
}
active [2] proctype reader()
{
loop:
succ_must_wait[_pid] = 1;
d_step {
pred[_pid] = tail;
tail = _pid;
}
trying:
(succ_must_wait[pred[_pid]] == 0)
ncritR++;
release_succ(_pid);
// critical section
progress_reader:
assert(ncritR > 0);
assert(ncritW == 0);
ncritR--;
goto loop;
}
active [2] proctype writer()
{
loop:
succ_must_wait[_pid] = 1;
d_step {
pred[_pid] = tail;
tail = _pid;
}
trying:
(succ_must_wait[pred[_pid]] == 0) && (ncritR == 0)
ncritW++;
// critical section
progress_writer:
assert(ncritR == 0);
assert(ncritW == 1);
ncritW--;
release_succ(_pid);
goto loop;
}
I added the following properties to the model:
p0: the writer with _pid equal to 4 goes through its progress state infinitely often, provided that it is given the chance to execute some instruction infinitely often:
ltl p0 {
([]<> (_last == 4)) ->
([]<> writer[4]#progress_writer)
};
This property should be true.
p1: there is never more than one reader in the critical section:
ltl p1 {
([] (ncritR <= 1))
};
Obviously, we expect this property to be false in a model that matches your specification.
p2: there is never more than one writer in the critical section:
ltl p2 {
([] (ncritW <= 1))
};
This property should be true.
p3: there isn't any node that is the predecessor of two other nodes at the same time, unless such node is node 0:
ltl p3 {
[] (
(((pred[1] != 0) && (pred[2] != 0)) -> (pred[1] != pred[2])) &&
(((pred[1] != 0) && (pred[3] != 0)) -> (pred[1] != pred[3])) &&
(((pred[1] != 0) && (pred[4] != 0)) -> (pred[1] != pred[4])) &&
(((pred[2] != 0) && (pred[3] != 0)) -> (pred[2] != pred[3])) &&
(((pred[2] != 0) && (pred[4] != 0)) -> (pred[2] != pred[4])) &&
(((pred[3] != 0) && (pred[4] != 0)) -> (pred[3] != pred[4]))
)
};
This property should be true.
p4: it is always true that whenever writer with _pid equal to 4 tries to access the critical section then it will eventually get there:
ltl p4 {
[] (writer[4]#trying -> <> writer[4]#progress_writer)
};
This property should be true.
The outcome of the verification matches our expectations:
~$ spin -search -ltl p0 -a clhrw_lock.pml
...
Full statespace search for:
never claim + (p0)
assertion violations + (if within scope of claim)
acceptance cycles + (fairness disabled)
invalid end states - (disabled by never claim)
State-vector 68 byte, depth reached 3305, errors: 0
...
~$ spin -search -ltl p1 -a clhrw_lock.pml
...
Full statespace search for:
never claim + (p1)
assertion violations + (if within scope of claim)
acceptance cycles + (fairness disabled)
invalid end states - (disabled by never claim)
State-vector 68 byte, depth reached 1692, errors: 1
...
~$ spin -search -ltl p2 -a clhrw_lock.pml
...
Full statespace search for:
never claim + (p2)
assertion violations + (if within scope of claim)
acceptance cycles + (fairness disabled)
invalid end states - (disabled by never claim)
State-vector 68 byte, depth reached 3115, errors: 0
...
~$ spin -search -ltl p3 -a clhrw_lock.pml
...
Full statespace search for:
never claim + (p3)
assertion violations + (if within scope of claim)
acceptance cycles + (fairness disabled)
invalid end states - (disabled by never claim)
State-vector 68 byte, depth reached 3115, errors: 0
...
~$ spin -search -ltl p4 -a clhrw_lock.pml
...
Full statespace search for:
never claim + (p4)
assertion violations + (if within scope of claim)
acceptance cycles + (fairness disabled)
invalid end states - (disabled by never claim)
State-vector 68 byte, depth reached 3115, errors: 0
...

How to check for unknown registers in verilog

When getting three inputs from the console, I am wondering how to check and 'warn' the user that a register may not have been initialized.
to do this I am trying:
flag = $value$plusargs("a=%b", a);
if (flag != 0 && flag != 1) begin
$display("a might not be initialized");
end
flag = $value$plusargs("b=%b", b);
flag = $value$plusargs("c=%b", c);
#1 $display("a=%b b=%b c=%b z=%b", a, b, c, z);
However with my limited knowledge I am having a difficult time figuring out what to do. When I run my compiled code with no paramaters I get:
a = x, b = x, c = x, z = x;
but no warning, even though the flag(a) is clearly not 1 and not 0
flag returns true(1) if $value$plusargs finds +a=value on the command line and sets the value of a. So you want
if (flag == 0) begin
$display("a might not be initialized");
You can do this in one step
if ( !$value$plusargs("a=%b", a) ) begin
$display("a might not be initialized");
And if you use SystemVerilog, you can use $warning() instead of $display().
To compare with unknown value, you should use !== or === instead of != and ==

Crystal recursive function resulting in a signal 11 (invalid memory access)?

I wanted to test recursive functions in Crystal, so I wrote something like...
def repeat(n)
return if n < 0
# Useless line just to do something
n + 1
repeat(n - 1)
end
repeat(100_000_000)
I didn't expect this to work with either crystal recurse.cr or crystal build recurse.cr, because neither one of those can optimize for tail call recursion - as expected, both resulted in stack overflows.
When I used the --release flag, it was totally fine - and incredibly fast.
If I did the following...
NUM = 100_000
def p(n)
puts n if n % (NUM / 4) == 0
end
def recursive_repeat(n)
return if n < 0
p(n)
recursive_repeat(n-1)
end
recursive_repeat(NUM)
... everything is fine - I don't even need to build it.
If I change to a much larger number and a non-recursive function, again it's fine...
NUM = 100_000_000
def p(n)
puts n if n % (NUM / 4) == 0
end
def repeat(num)
(0..num).each { |n| p(n) }
end
repeat(NUM)
If I instead use recursion, however...
NUM = 100_000
def p(n)
puts n if n % (NUM / 4) == 0
end
def recursive_repeat(n)
return if n < 0
p(n)
recursive_repeat(n-1)
end
recursive_repeat(NUM)
...I get the following output:
100000000
Invalid memory access (signal 11) at address 0x7fff500edff8
[4549929098] *CallStack::print_backtrace:Int32 +42
[4549928595] __crystal_sigfault_handler +35
[140735641769258] _sigtramp +26
[4549929024] *recursive_repeat<Int32>:Nil +416
[4549929028] *recursive_repeat<Int32>:Nil +420 (65519 times)
[4549926888] main +2808
How does using puts like that trigger a stack overflow - especially given that it would only write 5 lines total?
Shouldn't this still be TCO?
Edit
Okay, to add to the weirdness, this works...
NUM = 100_000_000
def p(n)
puts n if n % (NUM / 4) == 0
end
def repeat(num)
(0..num).each { |n| p(n) }
end
repeat(NUM)
def recursive_repeat(n)
return if n < 0
p(n)
recursive_repeat(n-1)
end
recursive_repeat(NUM)
... but removing the call to repeat(NUM), literally keeping every other line the same, will again result in the error.

Why am I not getting an expected output using logical operators and indexing?

I am having trouble achieving an expected output. I am trying to create a byte adder using logical operators such as AND, XOR and OR. I have taken the minimal code required to reproduce the problem out of code, so assume that finalfirstvalue = "1010" and finalsecondvalue = "0101".
secondvalueindex = (len(finalsecondvalue) - 1)
carry, finalans = False, []
for i in range(-1, -len(finalfirstvalue) - 1, -1):
andone = (bool(finalfirstvalue[i])) & (bool(finalsecondvalue[secondvalueindex]))
xorone = (bool(finalfirstvalue[i])) ^ (bool(finalsecondvalue[secondvalueindex]))
andtwo = (bool(carry)) & (bool(xorone))
xortwo = (bool(carry)) ^ (bool(xorone))
orone = (bool(andone)) | (bool(andtwo))
carry = (bool(orone))
finalans.append(xortwo)
secondvalueindex -= 1
answer = ''.join(str(e) for e in finalans)
print (answer)
Actual Output: FalseTrueTrueTrue
Expected Output: TrueTrueTrueTrue
The code then follows to change back into zeroes and ones.
Because its missing a single boolean I feel like the issue is with my indexing. Although I've played around with it a bit and not had any luck.
I need to carry out these operations on the two variables mentioned at the start, but for the right most elements, and then move to the left by one for the next loop and so on.
First mistake is You are representing your binary numbers as string values.
finalfirstvalue = "1010"
finalsecondvalue = "0101"
secondvalueindex = (len(finalsecondvalue) - 1) == 3
So in second for loop you will get the result as
(finalsecondvalue[secondvalueindex]) == '0'
If you check in your Idle
>>> bool('0')
True
>>>
Because '0' is not actual 0 it is an non-empty string so it return True.
You need to cast your result to int before checking them with bool
Like this
(bool(int(finalsecondvalue[secondvalueindex])))
EDIT 2 Adding with variable lenghts
Full adder with verification using bin() function
a="011101"
b="011110"
if a>b:
b=b.zfill(len(a))
if a<b:
a=a.zfill(len(b))
finalfirstvalue = a
finalsecondvalue = b
carry, finalans = 0, []
secondvalueindex = (len(finalsecondvalue))
for i in reversed(range(0, len(finalfirstvalue))):
xorone = (bool(int(finalfirstvalue[i]))) ^ (bool(int(finalsecondvalue[i])))
andone = (bool(int(finalfirstvalue[i]))) & (bool(int(finalsecondvalue[i])))
xortwo = (carry) ^ (xorone)
andtwo = (carry) & (xorone)
orone = (andone) | (andtwo)
carry = (orone)
finalans.append(xortwo)
finalans.reverse()
answer=(''.join(str(e) for e in finalans))
print(str(carry)+answer)
print(bin(int(a,2) + int(b,2))) #verification
So I found the issue was to do with carry. I changed my code to look like the following. Prior to this code below, is code to convert binary values to boolean. For instance, all ones will equal True and all zeroes will equal False.
carry, finalans = False, []
indexvalue = (len(finalfirstvalue)-1)
while indexvalue >= 0:
andone = (firstvaluelist[indexvalue]) & (secondvaluelist[indexvalue])
xorone = (firstvaluelist[indexvalue]) ^ (secondvaluelist[indexvalue])
andtwo = (carry) & (xorone)
xortwo = (carry) ^ (xorone)
orone = (andone) | (andtwo)
carry = (orone)
if (carry == True) & (indexvalue == 0):
finalans.append(xortwo)
finalans.append(True)
else:
finalans.append(xortwo)
indexvalue -= 1
for n, i in enumerate(finalans):
if i == False:
finalans[n] = "0"
if i == True:
finalans[n] = "1"
finalans.reverse()
answer = ''.join(str(e) for e in finalans)
print (answer)
So if there was a single value missing, it was still stored in carry from the final loop but did not get the opportunity to be appended to the final result. To fix this, I added in an if statement to check if carry is containing anything (True) and if the loop is on its final loop by checking if indexvalue is at 0. This way, if the inputs are 32 and 32, rather than getting [False, False, False, False, False, False] as the output, the newly entered if statement will add the missing value in.

F# MailboxProcessor memory leak in try/catch block

Updated after obvious error pointed out by John Palmer in the comments.
The following code results in OutOfMemoryException:
let agent = MailboxProcessor<string>.Start(fun agent ->
let maxLength = 1000
let rec loop (state: string list) i = async {
let! msg = agent.Receive()
try
printfn "received message: %s, iteration: %i, length: %i" msg i state.Length
let newState = state |> Seq.truncate maxLength |> Seq.toList
return! loop (msg::newState) (i+1)
with
| ex ->
printfn "%A" ex
return! loop state (i+1)
}
loop [] 0
)
let greeting = "hello"
while true do
agent.Post greeting
System.Threading.Thread.Sleep(1) // avoid piling up greetings before they are output
The error is gone if I don't use try/catch block.
Increasing the sleep time only postpones the error.
Update 2: I guess the issue here is that the function stops being tail recursive as the recursive call is no longer the last one to execute. Would be nice for somebody with more F# experience to desugar it as I'm sure this is a common memory-leak situation in F# agents as the code is very simple and generic.
Solution:
It turned out to be a part of a bigger problem: the function can't be tail-recursive if the recursive call is made within try/catch block as it has to be able to unroll the stack if the exception is thrown and thus has to save call stack information.
More details here:
Tail recursion and exceptions in F#
Properly rewritten code (separate try/catch and return):
let agent = MailboxProcessor<string>.Start(fun agent ->
let maxLength = 1000
let rec loop (state: string list) i = async {
let! msg = agent.Receive()
let newState =
try
printfn "received message: %s, iteration: %i, length: %i" msg i state.Length
let truncatedState = state |> Seq.truncate maxLength |> Seq.toList
msg::truncatedState
with
| ex ->
printfn "%A" ex
state
return! loop newState (i+1)
}
loop [] 0
)
I suspect the issue is actually here:
while true do
agent.Post "hello"
All the "hello"s that you post have to be stored in memory somewhere and will be pushed much faster than the output can happen with printf
See my old post here http://vaskir.blogspot.ru/2013/02/recursion-and-trywithfinally-blocks.html
random chars in order to satisfy this site rules *
Basically anything that is done after the return (like a try/with/finally/dispose) will prevent tail calls.
See https://blogs.msdn.microsoft.com/fsharpteam/2011/07/08/tail-calls-in-f/
There is also work underway to have the compiler warn about lack of tail recursion: https://github.com/fsharp/fslang-design/issues/82

Resources