Repository navigation
Conversation
When a group called with a capture list, such as (?&r(2)), ended with (*ACCEPT), the interpreter went back to the caller's frame and moved past the OP_RECURSE, but left the following OP_CREF list in place. Matching then continued at OP_CREF, which hit the unreachable-opcode default: an assertion failure in debug builds and PCRE2_ERROR_INTERNAL otherwise. The callee's captures were also dropped. The ACCEPT path now does what the end of a recursed group already does: if the OP_RECURSE has a capture list, recurse_update_offsets() builds the caller's captures, keeping the listed groups from the callee, and moves past the list. The result is copied into the caller's frame before matching continues there. Fixes PCRE2Project#1045
Yes, that would be useful, thanks! I think one of them has the following reproducer: |
|
I think there's one more thing that's needed before Here's an example: The documentation for Unfortunately, the bytecode before ACCEPT which does this is skipped inside recursions; instead in should be changed to auto-close the groups that we are returning. |
OP_CLOSE was skipped inside recursions because captures used not to be passed out of them. With capture lists they can be, so close the groups that are open at the (*ACCEPT), stopping the frame search at the recursion's frame so that only groups opened within the recursion are closed.
|
Thanks, pushed a follow-up commit for this. New tests in testinput2 cover your example, two nested open groups returned together, an open group that is not listed (it stays unset), whole-pattern recursion One side effect to check: this also applies to Run: CMake Debug build with |
|
Thank you! Completely by chance, there's another open PR right now (#1011) which also deals with ACCEPT, and adds a new That could be very useful here. I'm going to be quite cautious about altering the behaviour of ACCEPT in general. The case of ACCEPT inside assertions and recursions (and assertions within recursions, and recursions within assertions) is something that we can compare against Perl (current and old versions). In each case, knowing whether an in-progress group is closed by the ACCEPT should have an observable effect that can be tested with a Perl script and pcre2test case. I'll take a few days to think about this. I would be willing to alter the behaviour for cases other than recurse-with-returned-groups, but only if it's a correctness improvement (not just accidental or convenient), and only if there's no compatibility issue with other engines. |
|
The best route here is probably to merge the PR #1011 as a pre-requisite, and then rebase this one. The behaviour of ACCEPT should ultimately depend on whether the heapframe-stack shows the nearest enclosing block to be an assertion, or a subroutine, or neither. That depends on how the ACCEPT was reached, dynamically. |
|
Sounds good. I'll wait for #1011 to land, then rebase this on top. I'll redo the closing of in-progress groups so that it depends on whether the nearest enclosing frame is an assertion, a subroutine or neither, and I'll check the cases against Perl as you describe. |
Fixes #1045.
Problem
When a group called with a capture list (for example
(?&r(2))) ends with(*ACCEPT), the interpreter's ACCEPT-in-recursion path goes back to the caller's frame and moves pastOP_RECURSE, but it does not handle theOP_CREFlist that follows. Matching then resumes atOP_CREF, which reaches the unreachable-opcodedefault:inmatch():Assertion failed: ... "Execution reached unexpected point" ... pcre2_match.c, line 7639PCRE2_DEBUG=OFFbuild:Failed: error -44: internal error - pattern overwritten?0: ab,1: <unset>,2: aThe callee's captures were also discarded on this path, so skipping past the list would not have been enough.
Fix
The ACCEPT path now does what the normal end-of-recursion path (
OP_KETfor the called group) already does. If theOP_RECURSEis followed by a capture list,recurse_update_offsets()builds the caller's capture vector in the current (callee) frame, keeping the listed groups from the callee, and moves past theOP_CREFlist. The result and itsoffset_topare copied into the caller's frame before matching continues there. When there is no capture list, behaviour is the same as before: the caller's frame is resumed just afterOP_RECURSE.Tests
New cases sit at the end of the "recurse with returned captures" block in
testinput2/testoutput2:(*ACCEPT)fires;(?R(1));(*ACCEPT)still does not backtrack into the called group.The expected output was generated by the patched interpreter and agrees with JIT.
RunTestchecks test 2 against both engines.Without the fix,
../RunTest 2fails in the interpreter at all three widths with the assertion above, and passesOK with JIT. With the fix, it passes in both modes at 8, 16 and 32 bits.What was run
ctest, 4/4 passed.devcontainer-asan-ubsanpreset's flags:ctest, 4/4 passed.PCRE2_DEBUG=OFF, JIT off:ctest, 3/3 passed.(*ACCEPT), subroutine calls and capture lists were compared between the interpreter and JIT (part of them under ASan/UBSan). No crashes, and no differences apart from the existing ones mentioned below.Not run: Valgrind, GCC, Autotools,
maint/ManyConfigTests, EBCDIC, Windows, andmaint/ApplySourceFormatters.py(clang-format-20 is not installed; the new code follows the surrounding formatting by hand).Two existing interpreter/JIT differences in this area turned up while testing and are not changed here: trailing
<unset>groups after a return with a capture list (same onmain), and JIT keeping a returned capture from an abandoned attempt in some ACCEPT-in-subroutine cases. Happy to file separate issues with reproducers if that is useful.This touches the same
OP_ACCEPTrecursion path that #1011 routesOP_ASSERT_ACCEPTthrough, so whichever lands second may need a small rebase.