Repository navigation
Conversation
`_guaranteesForwardProgressImpl` walks the flattened node list with a cursor, and several of its cases answer without visiting every child: `contains` stops at the first child of a concatenation that progresses, `allSatisfy` at the first alternative that doesn't, and a lookahead or a zero-lower-bound quantification doesn't look at its child at all. When a case stopped early, the cursor was left in the middle of the subtree, and the caller then read a node from inside it as the next sibling. In `(?:.c|)*` the first alternative is the concatenation `.c`. Deciding it only takes `.`, so `c` was read as the second, empty alternative. That made every alternative look like it progresses, so no progress check was emitted and matching the empty alternative looped forever - or grew until an allocation failed. Find the end of the node's subtree up front and restore the cursor to it on the way out, so the invariant holds however much of the subtree a case needs to look at. Fixes swiftlang#897, fixes swiftlang#894.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves #897 and #894.
The bug
_guaranteesForwardProgressImplinByteCodeGen+DSLList.swiftwalks the flattened node list with aninout positioncursor, and several of its cases answer without visiting every child:.concatenationusescontains, which stops at the first child that progresses..orderedChoiceusesallSatisfy, which stops at the first alternative that doesn't.The cases that don't descend skipped their subtree explicitly, but the short-circuiting ones left the cursor in the middle of theirs, and the caller then read a node from inside the subtree as the next sibling.
In
(?:.c|)*the first alternative is the concatenation.c. Deciding it only takes., socwas read as the second, empty alternative. That made every alternative look like it progresses, so the forward progress check was never emitted, and matching the empty alternative looped forever:Some variants ended in
Fatal error: failed to allocate ... bytes of memoryinstead of hanging.The fix
Find the end of the node's subtree up front, with the same
skipNodethe explicit cases used, and restore the cursor to it on the way out. The invariant - on return,positionis just past the node's whole subtree - then holds however much of the subtree a case needs to look at, and the two explicit skips become redundant and are dropped.Testing
testQuantificationForwardProgressgets the three patterns from Infinite loop: forward-progress analysis loses its place (contains/allSatisfy short-circuit), e.g. (?:.c|)*x #897 and the one from Regex infinite loop for empty alternate #894; each one fails its 3 second completion timeout without the fix.testQuantificationForwardProgressCompilechecks the progress check instructions are now emitted for those bodies, and - for patterns whose alternatives all progress, or whose concatenation progresses only in a later child - that they are still not emitted, so the analysis didn't just become conservative.testEmptyAlternativeInQuantificationchecks the match results themselves, including the non-empty inputs that were already fine.Full
swift testpasses.As a check that nothing else moved, two generated corpora were run against the OS's own
_StringProcessing(which predates the flattenedDSLList) over a set of short inputs: 4000 assorted patterns, and 2500 of the shape this fixes - an empty or non-progressing alternative beside one that is a multi-node concatenation, under each kind of quantifier. On the assorted corpus the results are identical before and after this change. Against the OS engine, 6498 of the 6500 patterns agree.Both disagreements are pre-existing - byte-for-byte the same before this change - and both go away when
root.autoPossessify()is skipped inemitRoot, so they are Auto-possessification loses its place after an optional sibling: (?i)(?:a*\+*)A{2} misses "aA" #898, auto-possessification losing its place inAutoPossessification.swift. That is the same class of bookkeeping bug in a different walk, and is not addressed here. In case they are useful there, the two are(b{1,3}\s?)bagainst"bb"and(?:a+a?|a*|\+.(?#c)\+)[ab]*.against"a", both of which find no match where the OS engine matches - neither one needsignoresCase, unlike the repro in the issue.Independent of #872: that one changes when
guaranteesForwardProgressis consulted, this one what it computes. Every assertion added here uses an unbounded quantifier, so they hold either way, but the two do touch the same two test functions.