Exit belongs to its own evaluation
One parser is routinely shared by several threads - the plotting component
hands the same object to four workers at once. Evaluation always survived
that; Exit did not.
Fixed: Exit answered to the thread scheduler. The nesting depth lived
in a field of the parser, shared by every thread using it. Two threads
evaluating at the same time read a depth of 2, so the one whose formula hit
Exit took itself for a nested call and threw the value out as an exception
instead of returning it. The counter also drifted: Inc and Dec are
read-modify-write, a lost update parked the field below zero, and Exit
stayed broken for that parser until another race happened to repair it. The
depth now lives in a frame on the stack of the call, linked through a
thread-local pointer, with a reference to the nearest frame of the same
parser. No locks, no atomics, no allocation on the path of an ordinary
formula. Parallel roots are independent, two parsers nested in one thread are
independent, and only true recursion into the same parser passes the Exit
upward.
One declared change of behaviour. A bracketed group in a formula is a
script of its own, and the evaluation mode decides when it runs. Evaluating
groups up front used to happen outside the handler, so the group kept its own
Exit: 99 + (Exit(42)) answered 141 with ExecuteKindSet = [], and 42
in the default mode. One public call now means one scope for Exit, and both
modes answer 42. The pair of tests R4/G4 in the new suite records both
the new value and the fact that the default mode did not move.
Proof. tests/ThreadSafetyTest.dpr - 29 checks. On 1.0.1 it fails 5 of
them, exactly the defects above; on this release it is green, along with the
rest of the battery on Delphi 13 and FPC 3.3.1, Windows and Linux.
Documented. The loop guards from 1.0.1 (ParseBreak, ParseLoopLeft)
now have a README section of their own with a compiled-and-run example,
samples/docs/loopguard.dpr. The scope is stated exactly: guards belong to
the thread, every nested evaluation in it shares the budget and the flag, and
they are set at the boundary of the outermost call.