[fpc-devel] Deeper problem with Internal Error 200309201
J. Gareth Moreton
gareth at moreton-family.com
Sat Jul 13 14:12:21 CEST 2019
Hi everyone,
So my patch to fix #32913
<https://bugs.freepascal.org/view.php?id=32913> was unfortunately
rejected because it relied on a slightly hacky feature in the form of a
new state variable that, currently, is only used to patch this
particular issue, and as Florian has hinted in the issue notes, a deeper
fix needs to be investigated.
What I can put together so far is that if you have a construct like this...
try
except
try
[b]
finally
[c]
end;
end;
...and [b] is not empty ([c] doesn't matter), then the compiler will
trigger Internal Error 200309201. What happened is that during the
simplify stage of ttryexceptnode, if the try block is empty (represented
by the 'left' node), the compiler would replace the entire construct
with a 'nothing' node. Logically a sound choice, because it's
impossible for an exception to be raised in this instance. The problem
comes that the except section contains a try..finally block which sets
up a stack unwind block. It's a little hard to follow at this point,
but what it culminates in is that there's a non-null exception filter
(try..finally and try..except get bundled together for the stack unwind)
that the compiler is not expecting later on, which causes a sanity check
to fail and trigger the internal error.
One approach is to simply not transform the except section into a
nothing node, but this adds significant overhead to the compiled
binaries and incur a performance penalty by having an unnecessary stack
unwind (such try..except blocks may occur in the case of inlined
functions yielding no code, for example).
My first attempt at fixing this was to move the 'simplify' code into
'pass_1' for ttryexceptnode, and if it detected an empty try section, to
simply not run "pass_1" on anything in the except section, so no code is
ever put into the exception filter - I argued this logic to myself
because, as already explained, this exception handler would never be
executed. This worked, but caused a regression in tests/tbf/tb0089.pp
and tests/tbf/tb0090.pp - these tests had an empty try section in their
try..except blocks, because all they had to test was to attempt to use
'goto' from the except section to jump to a label outside of the block.
This particular syntactic check was only performed in "pass_1", so by
not running it for those nodes, the syntax check was skipped and the
tests erroneously compiled successfully.
Since this approach resulted in a regression, it wasn't acceptable and
so I had to find some other means to prevent code entering the exception
filter without completely overhauling the node parser. This led to what
is how the "current_nodes_dead" flag, set if the try section is empty
and designed with future expansion in mind (if you know nodes are
unreachable, you can skip doing any kind of code generation or even
delete them entirely once you do the syntactic checking). After
writing the necessary code in the ttryfinallynode class, I got the issue
fixed at long last.
It goes without saying that I was upset that the patch was rejected,
especially as this particular issue has been on my books for literally a
year now, and I had no other viable solution to offer since everything
else I had tried resulted in an error elsewhere. However, now that
Florian's actually responded and hinted at a willingness to look for a
bigger solution, I'm able to look at it more objectively. Having a flag
that's only used to fix one feature is a little dangerous, even with a
proposal for future use, because chances are it will be forgotten about
and people will just introduce other flags when and where they need.
I would like some discussion on this with the administrative team
because this does require some careful design if we're going to do this
properly. Building on Florian's suggestion, I would like to propose
splitting "pass_1" into "pass_syntax" and a pass that transforms the
nodes (I would name it "pass_transform", but this doesn't sound
'important' enough, given it is effectively compiling the nodes).
"pass_syntax" would be a public method declared in tnode as the
following "function pass_syntax: Boolean; virtual;" - if it returns
True, then everything was fine; if not, it returns False and this can be
used to set the error flag. By being defined this way, this is no
ability for the node to transform itself, but it can set private fields
based on the syntax it finds (by setting private fields, the "first
pass" won't have to repeat some of the work needed to determine how to
transform the nodes).
There are two ways this can then be implemented... one way is to have
the syntax pass be completely separate from the now badly-named "first
pass" and just do syntax checking on all the nodes. If any syntax
errors are found, then the error flag can be set and "firstpass" will
skip these nodes already. After this syntax pass is done, then the
compiler can move onto the "first pass". The drawback to this is that
it will likely increase compilation time quite noticably, since the
compiler will be running an additional traversal through the entire node
tree.
The second way is to have the syntax pass as the first step of
"firstpass", so an extra traversal is not required. However, this will
be harder to develop and maintain because care has to be taken that all
nodes are syntax-checked but not transformed if they would result in
dead or unexpected code (as with internal error 200309201). Given this
would require a flag for a 'dead node', we may end up in a similar
situation as with the "hacky" patch. However, one thought that occurred
to me is that we can make a new flag for tnodeflag named something like
'nf_dead', since this doesn't add any new fields, and if this flag is
set, then "pass_1" and "simplify" are not called, and it cascades the
flag to the child nodes. This would be the 32nd flag for tnodeflag, so
it reaches the upper limit for a small set - something to keep in mind
if it gets expanded later.
On the surface, the first implementation suggestion feels like the
cleaner option, but I don't know how much of a penalty it will add to
the compiler. Personally I would like to attempt a design for the
second implementation to keep the number of passes down to a minimum and
the compiler reasonably fast (also my motivation behind the x86_64
peephole optimizer overhaul). But that's just my opinion.
On an additional note, I do wonder if it's possible to merge "pass_1"
and "simplify", since they are treated pretty much identically in the
"firstpass" routine - if either of them return something other than nil,
it is considered to be a node transformation, running this block of code
(there are two copies, one for each call):
p.free;
p := hp;
firstpass(p);
(hp contains the return value of whatever method was just called) This
may be impractical because "simplify" is called elsewhere for some nodes.
At least that's what I've thought about so far. Where do we go from
here? I sense for this task, I should write up a PDF design spec like I
did with the optimizer overhaul, but this is something that needs a fair
bit of discussion. And implementation will be a fairly mammoth task
because it would require introducing the new method to every single node
type.
Thank you for your time, and I hope we can find the best solution to
this issue.
Gareth aka. Kit
---
This email has been checked for viruses by Avast antivirus software.
https://www.avast.com/antivirus
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.freepascal.org/pipermail/fpc-devel/attachments/20190713/2ce256c8/attachment-0001.html>
More information about the fpc-devel
mailing list