LibRegex: Add an optimisation for replacing /.*x/ with a seek op
This will avoid some catastrophic backtracking by just skipping to 'x'.
This commit is contained in:
parent
77d982d6fe
commit
637d47ba30
9 changed files with 467 additions and 27 deletions
|
|
@ -266,6 +266,38 @@ Optional<size_t> Utf16View::find_code_unit_offset(char16_t needle, size_t start_
|
|||
return result - start + start_offset;
|
||||
}
|
||||
|
||||
Optional<size_t> Utf16View::find_last_code_unit_offset(char16_t needle, size_t end_offset) const
|
||||
{
|
||||
if (end_offset == 0)
|
||||
return {};
|
||||
|
||||
if (has_ascii_storage()) {
|
||||
if (!AK::is_ascii(needle))
|
||||
return {};
|
||||
auto ascii_end_offset = min(end_offset, length_in_code_units());
|
||||
auto ascii_view = StringView { m_string.ascii, ascii_end_offset };
|
||||
auto index = ascii_view.find_last(static_cast<char>(needle));
|
||||
if (!index.has_value())
|
||||
return {};
|
||||
return *index;
|
||||
}
|
||||
|
||||
auto const* start = m_string.utf16;
|
||||
auto const* end = m_string.utf16 + end_offset;
|
||||
|
||||
auto const* last_result = simdutf::find(start, end, needle);
|
||||
while (true) {
|
||||
auto const* result = simdutf::find(last_result + 1, end, needle);
|
||||
if (result == end)
|
||||
break;
|
||||
last_result = result;
|
||||
}
|
||||
if (last_result == end)
|
||||
return {};
|
||||
|
||||
return last_result - start;
|
||||
}
|
||||
|
||||
Vector<Utf16View> Utf16View::split_view(char16_t separator, SplitBehavior split_behavior) const
|
||||
{
|
||||
Utf16View separator_view { &separator, 1 };
|
||||
|
|
|
|||
|
|
@ -485,6 +485,7 @@ public:
|
|||
}
|
||||
|
||||
Optional<size_t> find_code_unit_offset(char16_t needle, size_t start_offset = 0) const;
|
||||
Optional<size_t> find_last_code_unit_offset(char16_t needle, size_t end_offset = NumericLimits<size_t>::max()) const;
|
||||
|
||||
constexpr Optional<size_t> find_code_unit_offset(Utf16View const& needle, size_t start_offset = 0) const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -554,6 +554,20 @@ ALWAYS_INLINE ExecutionResult OpCode_SaveRightNamedCaptureGroup<ByteCode>::execu
|
|||
return ExecutionResult::Continue;
|
||||
}
|
||||
|
||||
template<typename ByteCode>
|
||||
ALWAYS_INLINE ExecutionResult OpCode_RSeekTo<ByteCode>::execute(MatchInput const& input, MatchState& state) const
|
||||
{
|
||||
auto ch = argument(0);
|
||||
auto last_position = exchange(state.string_position_before_rseek, state.string_position);
|
||||
auto last_position_in_code_units = exchange(state.string_position_in_code_units_before_rseek, state.string_position_in_code_units);
|
||||
auto next = input.view.find_index_of_previous(ch, last_position, last_position_in_code_units);
|
||||
if (!next.has_value())
|
||||
return ExecutionResult::Failed_ExecuteLowPrioForksButNoFurtherPossibleMatches;
|
||||
state.string_position = next->code_point_index;
|
||||
state.string_position_in_code_units = next->code_unit_index;
|
||||
return ExecutionResult::Continue;
|
||||
}
|
||||
|
||||
template<typename ByteCode>
|
||||
ALWAYS_INLINE ExecutionResult OpCode_Compare<ByteCode>::execute(MatchInput const& input, MatchState& state) const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ using ByteCodeValueType = u64;
|
|||
__ENUMERATE_OPCODE(SaveLeftCaptureGroup) \
|
||||
__ENUMERATE_OPCODE(SaveRightCaptureGroup) \
|
||||
__ENUMERATE_OPCODE(SaveRightNamedCaptureGroup) \
|
||||
__ENUMERATE_OPCODE(RSeekTo) \
|
||||
__ENUMERATE_OPCODE(CheckBegin) \
|
||||
__ENUMERATE_OPCODE(CheckEnd) \
|
||||
__ENUMERATE_OPCODE(CheckBoundary) \
|
||||
|
|
@ -808,12 +809,13 @@ private:
|
|||
Vector<ByteCodeValueType> m_data;
|
||||
};
|
||||
|
||||
#define ENUMERATE_EXECUTION_RESULTS \
|
||||
__ENUMERATE_EXECUTION_RESULT(Continue) \
|
||||
__ENUMERATE_EXECUTION_RESULT(Fork_PrioHigh) \
|
||||
__ENUMERATE_EXECUTION_RESULT(Fork_PrioLow) \
|
||||
__ENUMERATE_EXECUTION_RESULT(Failed) \
|
||||
__ENUMERATE_EXECUTION_RESULT(Failed_ExecuteLowPrioForks) \
|
||||
#define ENUMERATE_EXECUTION_RESULTS \
|
||||
__ENUMERATE_EXECUTION_RESULT(Continue) \
|
||||
__ENUMERATE_EXECUTION_RESULT(Fork_PrioHigh) \
|
||||
__ENUMERATE_EXECUTION_RESULT(Fork_PrioLow) \
|
||||
__ENUMERATE_EXECUTION_RESULT(Failed) \
|
||||
__ENUMERATE_EXECUTION_RESULT(Failed_ExecuteLowPrioForks) \
|
||||
__ENUMERATE_EXECUTION_RESULT(Failed_ExecuteLowPrioForksButNoFurtherPossibleMatches) \
|
||||
__ENUMERATE_EXECUTION_RESULT(Succeeded)
|
||||
|
||||
enum class ExecutionResult : u8 {
|
||||
|
|
@ -1144,6 +1146,26 @@ public:
|
|||
}
|
||||
};
|
||||
|
||||
template<typename ByteCode>
|
||||
class REGEX_API OpCode_RSeekTo final : public OpCode<ByteCode> {
|
||||
public:
|
||||
using OpCode<ByteCode>::argument;
|
||||
using OpCode<ByteCode>::name;
|
||||
using OpCode<ByteCode>::state;
|
||||
using OpCode<ByteCode>::bytecode;
|
||||
|
||||
ExecutionResult execute(MatchInput const& input, MatchState& state) const override;
|
||||
ALWAYS_INLINE OpCodeId opcode_id() const override { return OpCodeId::RSeekTo; }
|
||||
ALWAYS_INLINE size_t size() const override { return 2; }
|
||||
ByteString arguments_string() const override
|
||||
{
|
||||
auto ch = argument(0);
|
||||
if (ch <= 0x7f)
|
||||
return ByteString::formatted("before '{}'", ch);
|
||||
return ByteString::formatted("before u+{:04x}", argument(0));
|
||||
}
|
||||
};
|
||||
|
||||
template<typename ByteCode>
|
||||
class REGEX_API OpCode_Compare final : public OpCode<ByteCode> {
|
||||
public:
|
||||
|
|
|
|||
|
|
@ -271,7 +271,12 @@ public:
|
|||
[&](StringView view) {
|
||||
return other.m_view.visit(
|
||||
[&](StringView other_view) { return view.equals_ignoring_ascii_case(other_view); },
|
||||
[](auto&) -> bool { TODO(); });
|
||||
[&](Utf16View other_view) -> bool {
|
||||
auto result = other_view.to_utf8();
|
||||
if (result.is_error())
|
||||
return false;
|
||||
return view.equals_ignoring_ascii_case(result.value().bytes_as_string_view());
|
||||
});
|
||||
},
|
||||
[&](Utf16View view) {
|
||||
return other.m_view.visit(
|
||||
|
|
@ -290,6 +295,45 @@ public:
|
|||
[&](StringView view) { return view.starts_with(str); });
|
||||
}
|
||||
|
||||
struct FoundIndex {
|
||||
size_t code_unit_index;
|
||||
size_t code_point_index;
|
||||
};
|
||||
Optional<FoundIndex> find_index_of_previous(u32 code_point, size_t end_code_point_index, size_t end_code_unit_index) const
|
||||
{
|
||||
return m_view.visit(
|
||||
[&](Utf16View const& view) -> Optional<FoundIndex> {
|
||||
auto result = view.find_last_code_unit_offset(code_point, end_code_unit_index);
|
||||
if (!result.has_value())
|
||||
return {};
|
||||
return FoundIndex { result.value(), view.code_point_offset_of(result.value()) };
|
||||
},
|
||||
[&](StringView const& view) -> Optional<FoundIndex> {
|
||||
if (unicode()) {
|
||||
Utf8View utf8_view { view };
|
||||
auto it = utf8_view.begin();
|
||||
size_t current_code_point_index = 0;
|
||||
Optional<FoundIndex> found_index;
|
||||
|
||||
for (; it != utf8_view.end(); ++it, ++current_code_point_index) {
|
||||
if (current_code_point_index > end_code_point_index)
|
||||
break;
|
||||
if (*it == code_point) {
|
||||
auto byte_index = utf8_view.byte_offset_of(it);
|
||||
found_index = { byte_index, current_code_point_index };
|
||||
}
|
||||
}
|
||||
|
||||
return found_index;
|
||||
}
|
||||
|
||||
auto byte_index = view.substring_view(0, min(end_code_unit_index, view.length())).find_last(code_point);
|
||||
if (!byte_index.has_value())
|
||||
return {};
|
||||
return FoundIndex { byte_index.value(), byte_index.value() };
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
NO_UNIQUE_ADDRESS Variant<StringView, Utf16View> m_view { StringView {} };
|
||||
NO_UNIQUE_ADDRESS bool m_unicode { false };
|
||||
|
|
@ -372,6 +416,8 @@ struct MatchState {
|
|||
size_t instruction_position { 0 };
|
||||
size_t fork_at_position { 0 };
|
||||
size_t forks_since_last_save { 0 };
|
||||
size_t string_position_before_rseek { NumericLimits<size_t>::max() };
|
||||
size_t string_position_in_code_units_before_rseek { NumericLimits<size_t>::max() };
|
||||
Optional<size_t> initiating_fork;
|
||||
COWVector<Match> matches;
|
||||
COWVector<Match> flat_capture_group_matches; // Vector<Vector<Match>> indexed by match index, then by capture group id; flattened for performance
|
||||
|
|
|
|||
|
|
@ -277,9 +277,9 @@ RegexResult Matcher<Parser>::match(Vector<RegexStringView> const& views, Optiona
|
|||
state.instruction_position = 0;
|
||||
state.repetition_marks.clear();
|
||||
|
||||
auto success = execute(input, state, temp_operations);
|
||||
auto result = execute(input, state, temp_operations);
|
||||
// This success is acceptable only if it doesn't read anything from the input (input length is 0).
|
||||
if (success && (state.string_position <= view_index)) {
|
||||
if (result == ExecuteResult::Matched && (state.string_position <= view_index)) {
|
||||
operations = temp_operations;
|
||||
if (!match_count) {
|
||||
// Nothing was *actually* matched, so append an empty match.
|
||||
|
|
@ -337,7 +337,7 @@ RegexResult Matcher<Parser>::match(Vector<RegexStringView> const& views, Optiona
|
|||
state.instruction_position = 0;
|
||||
state.repetition_marks.clear();
|
||||
|
||||
if (execute(input, state, operations)) {
|
||||
if (auto const result = execute(input, state, operations); result == ExecuteResult::Matched) {
|
||||
succeeded = true;
|
||||
|
||||
if (input.regex_options.has_flag_set(AllFlags::MatchNotEndOfLine) && state.string_position == input.view.length()) {
|
||||
|
|
@ -375,6 +375,8 @@ RegexResult Matcher<Parser>::match(Vector<RegexStringView> const& views, Optiona
|
|||
|
||||
append_match(input, state, view_index);
|
||||
break;
|
||||
} else if (result == ExecuteResult::DidNotMatchAndNoFurtherPossibleMatchesInView) {
|
||||
break;
|
||||
}
|
||||
|
||||
done_matching:
|
||||
|
|
@ -513,7 +515,7 @@ struct SufficientlyUniformValueTraits : DefaultTraits<u64> {
|
|||
};
|
||||
|
||||
template<class Parser>
|
||||
bool Matcher<Parser>::execute(MatchInput const& input, MatchState& state, size_t& operations) const
|
||||
Matcher<Parser>::ExecuteResult Matcher<Parser>::execute(MatchInput const& input, MatchState& state, size_t& operations) const
|
||||
{
|
||||
if (m_pattern->parser_result.optimization_data.pure_substring_search.has_value() && input.view.is_u16_view()) {
|
||||
// Yay, we can do a simple substring search!
|
||||
|
|
@ -527,10 +529,10 @@ bool Matcher<Parser>::execute(MatchInput const& input, MatchState& state, size_t
|
|||
|
||||
if (is_unicode) {
|
||||
if (needle_view.length_in_code_points() + state.string_position > input_view.length_in_code_points())
|
||||
return false;
|
||||
return ExecuteResult::DidNotMatch;
|
||||
} else {
|
||||
if (needle_view.length_in_code_units() + state.string_position_in_code_units > input_view.length_in_code_units())
|
||||
return false;
|
||||
return ExecuteResult::DidNotMatch;
|
||||
}
|
||||
|
||||
Utf16View haystack;
|
||||
|
|
@ -541,10 +543,10 @@ bool Matcher<Parser>::execute(MatchInput const& input, MatchState& state, size_t
|
|||
|
||||
if (is_insensitive) {
|
||||
if (!haystack.equals_ignoring_ascii_case(needle_view))
|
||||
return false;
|
||||
return ExecuteResult::DidNotMatch;
|
||||
} else {
|
||||
if (haystack != needle_view)
|
||||
return false;
|
||||
return ExecuteResult::DidNotMatch;
|
||||
}
|
||||
|
||||
if (input.view.unicode())
|
||||
|
|
@ -552,7 +554,7 @@ bool Matcher<Parser>::execute(MatchInput const& input, MatchState& state, size_t
|
|||
else
|
||||
state.string_position += haystack.length_in_code_units();
|
||||
state.string_position_in_code_units += haystack.length_in_code_units();
|
||||
return true;
|
||||
return ExecuteResult::Matched;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -607,6 +609,8 @@ bool Matcher<Parser>::execute(MatchInput const& input, MatchState& state, size_t
|
|||
states_to_try_next.last().initiating_fork = state.instruction_position - opcode_size;
|
||||
states_to_try_next.last().instruction_position = state.fork_at_position;
|
||||
}
|
||||
state.string_position_before_rseek = NumericLimits<size_t>::max();
|
||||
state.string_position_in_code_units_before_rseek = NumericLimits<size_t>::max();
|
||||
continue;
|
||||
}
|
||||
case ExecutionResult::Fork_PrioHigh: {
|
||||
|
|
@ -625,6 +629,8 @@ bool Matcher<Parser>::execute(MatchInput const& input, MatchState& state, size_t
|
|||
if (!found) {
|
||||
states_to_try_next.append(state);
|
||||
states_to_try_next.last().initiating_fork = state.instruction_position - opcode_size;
|
||||
states_to_try_next.last().string_position_before_rseek = NumericLimits<size_t>::max();
|
||||
states_to_try_next.last().string_position_in_code_units_before_rseek = NumericLimits<size_t>::max();
|
||||
}
|
||||
state.instruction_position = state.fork_at_position;
|
||||
#if REGEX_DEBUG
|
||||
|
|
@ -635,7 +641,7 @@ bool Matcher<Parser>::execute(MatchInput const& input, MatchState& state, size_t
|
|||
case ExecutionResult::Continue:
|
||||
continue;
|
||||
case ExecutionResult::Succeeded:
|
||||
return true;
|
||||
return ExecuteResult::Matched;
|
||||
case ExecutionResult::Failed: {
|
||||
bool found = false;
|
||||
while (!states_to_try_next.is_empty()) {
|
||||
|
|
@ -649,7 +655,7 @@ bool Matcher<Parser>::execute(MatchInput const& input, MatchState& state, size_t
|
|||
}
|
||||
if (found)
|
||||
continue;
|
||||
return false;
|
||||
return ExecuteResult::DidNotMatch;
|
||||
}
|
||||
case ExecutionResult::Failed_ExecuteLowPrioForks: {
|
||||
bool found = false;
|
||||
|
|
@ -663,7 +669,25 @@ bool Matcher<Parser>::execute(MatchInput const& input, MatchState& state, size_t
|
|||
break;
|
||||
}
|
||||
if (!found)
|
||||
return false;
|
||||
return ExecuteResult::DidNotMatch;
|
||||
#if REGEX_DEBUG
|
||||
++recursion_level;
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
case ExecutionResult::Failed_ExecuteLowPrioForksButNoFurtherPossibleMatches: {
|
||||
bool found = false;
|
||||
while (!states_to_try_next.is_empty()) {
|
||||
state = states_to_try_next.take_last();
|
||||
if (auto hash = state.u64_hash(); seen_state_hashes.set(hash) != HashSetResult::InsertedNewEntry) {
|
||||
dbgln_if(REGEX_DEBUG, "Already seen state, skipping: {}", hash);
|
||||
continue;
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (!found)
|
||||
return ExecuteResult::DidNotMatchAndNoFurtherPossibleMatchesInView;
|
||||
#if REGEX_DEBUG
|
||||
++recursion_level;
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -71,7 +71,12 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
bool execute(MatchInput const& input, MatchState& state, size_t& operations) const;
|
||||
enum class ExecuteResult {
|
||||
DidNotMatch,
|
||||
Matched,
|
||||
DidNotMatchAndNoFurtherPossibleMatchesInView,
|
||||
};
|
||||
ExecuteResult execute(MatchInput const& input, MatchState& state, size_t& operations) const;
|
||||
|
||||
Regex<Parser> const* m_pattern;
|
||||
typename ParserTraits<Parser>::OptionsType const m_regex_options;
|
||||
|
|
@ -233,6 +238,7 @@ private:
|
|||
void attempt_rewrite_loops_as_atomic_groups(BasicBlockList const&);
|
||||
bool attempt_rewrite_entire_match_as_substring_search(BasicBlockList const&);
|
||||
void attempt_rewrite_adjacent_compares_as_string_compare(BasicBlockList const&);
|
||||
void attempt_rewrite_dot_star_sequences_as_seek(BasicBlockList const&);
|
||||
void fill_optimization_data(BasicBlockList const&);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -256,8 +256,9 @@ void Regex<Parser>::run_optimization_passes()
|
|||
rewrite_with_useless_jumps_removed();
|
||||
|
||||
auto blocks = split_basic_blocks(parser_result.bytecode.get<ByteCode>());
|
||||
if (attempt_rewrite_entire_match_as_substring_search(blocks))
|
||||
if (attempt_rewrite_entire_match_as_substring_search(blocks)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Rewrite fork loops as atomic groups
|
||||
// e.g. a*b -> (ATOMIC a*)b
|
||||
|
|
@ -268,6 +269,10 @@ void Regex<Parser>::run_optimization_passes()
|
|||
blocks = split_basic_blocks(parser_result.bytecode.get<ByteCode>());
|
||||
attempt_rewrite_adjacent_compares_as_string_compare(blocks);
|
||||
|
||||
// Rewrite /.*x/ as a seek to x
|
||||
blocks = split_basic_blocks(parser_result.bytecode.get<ByteCode>());
|
||||
attempt_rewrite_dot_star_sequences_as_seek(blocks);
|
||||
|
||||
fill_optimization_data(split_basic_blocks(parser_result.bytecode.template get<ByteCode>()));
|
||||
}
|
||||
|
||||
|
|
@ -288,7 +293,7 @@ struct StaticallyInterpretedCompares {
|
|||
HashTable<Unicode::Script> negated_unicode_script_extensions;
|
||||
};
|
||||
|
||||
static bool interpret_compares(Vector<CompareTypeAndValuePair> const& lhs, StaticallyInterpretedCompares& compares)
|
||||
static bool interpret_compares(Vector<CompareTypeAndValuePair> const& lhs, StaticallyInterpretedCompares& compares, ByteCodeBase const* bytecode = nullptr, bool as_follow = false)
|
||||
{
|
||||
bool inverse { false };
|
||||
bool temporary_inverse { false };
|
||||
|
|
@ -337,10 +342,18 @@ static bool interpret_compares(Vector<CompareTypeAndValuePair> const& lhs, Stati
|
|||
else
|
||||
lhs_negated_ranges.insert(pair.value, pair.value);
|
||||
break;
|
||||
case CharacterCompareType::String:
|
||||
// FIXME: We just need to look at the last character of this string, but we only have the first character here.
|
||||
// Just bail out to avoid false positives.
|
||||
return false;
|
||||
case CharacterCompareType::String: {
|
||||
if (!as_follow)
|
||||
return false;
|
||||
auto string = bytecode->get_u16_string(pair.value);
|
||||
u32 ch = string.code_point_at(0);
|
||||
|
||||
if (!current_lhs_inversion_state())
|
||||
lhs_ranges.insert(ch, ch);
|
||||
else
|
||||
lhs_negated_ranges.insert(ch, ch);
|
||||
break;
|
||||
}
|
||||
case CharacterCompareType::StringSet:
|
||||
return false;
|
||||
case CharacterCompareType::CharClass:
|
||||
|
|
@ -1529,6 +1542,288 @@ void Regex<Parser>::attempt_rewrite_adjacent_compares_as_string_compare(BasicBlo
|
|||
parser_result.bytecode = rewriter.rebuild(bytecode, move(insert_replacement));
|
||||
}
|
||||
|
||||
template<typename Parser>
|
||||
void Regex<Parser>::attempt_rewrite_dot_star_sequences_as_seek(BasicBlockList const& basic_blocks)
|
||||
{
|
||||
auto& bytecode = parser_result.bytecode.get<ByteCode>();
|
||||
|
||||
if (basic_blocks.is_empty()) {
|
||||
dbgln_if(REGEX_DEBUG, "No basic blocks, skipping /.*/ rewrite");
|
||||
return;
|
||||
}
|
||||
|
||||
// If a /.*/ sequence is followed by a compare C (with some non-matching ops {O} in between), we can rewrite:
|
||||
// bbN: {O0} (optional non-matching ops before the pattern)
|
||||
// ForkStay bbM
|
||||
// Checkpoint p
|
||||
// Compare AnyChar
|
||||
// JumpNonEmpty (back to ForkStay) p
|
||||
// bbM: {O1} (optional non-matching ops)
|
||||
// Compare C
|
||||
// as
|
||||
// bbN: {O0}
|
||||
// bbR: RSeekTo C
|
||||
// ForkStay bbR
|
||||
// bbM: {O1}
|
||||
// Compare C
|
||||
//
|
||||
// Note: bbM is determined by the ForkStay's target, not necessarily the next sequential block
|
||||
// Note: The pattern may span across multiple basic blocks
|
||||
|
||||
struct DotStarCandidate {
|
||||
size_t fork_ip;
|
||||
size_t checkpoint_ip;
|
||||
size_t compare_ip;
|
||||
size_t jump_ip;
|
||||
size_t following_block_start;
|
||||
u64 checkpoint_id;
|
||||
u32 seek_code_point;
|
||||
};
|
||||
Vector<DotStarCandidate> candidates;
|
||||
|
||||
auto state = MatchState::only_for_enumeration();
|
||||
|
||||
for (size_t i = 0; i < basic_blocks.size(); ++i) {
|
||||
auto const& block = basic_blocks[i];
|
||||
|
||||
state.instruction_position = block.start;
|
||||
|
||||
if (state.instruction_position > block.end)
|
||||
continue;
|
||||
|
||||
// Skip non-matching ops at the start of the block
|
||||
while (state.instruction_position <= block.end) {
|
||||
auto& op = bytecode.get_opcode(state);
|
||||
|
||||
switch (op.opcode_id()) {
|
||||
case OpCodeId::Checkpoint:
|
||||
case OpCodeId::Save:
|
||||
case OpCodeId::SaveLeftCaptureGroup:
|
||||
case OpCodeId::SaveRightCaptureGroup:
|
||||
case OpCodeId::SaveRightNamedCaptureGroup:
|
||||
case OpCodeId::ClearCaptureGroup:
|
||||
state.instruction_position += op.size();
|
||||
continue;
|
||||
|
||||
default:
|
||||
goto found_potential_fork;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if (state.instruction_position >= bytecode.size())
|
||||
continue;
|
||||
|
||||
auto& op_at_boundary = bytecode.get_opcode(state);
|
||||
if (op_at_boundary.opcode_id() != OpCodeId::ForkStay)
|
||||
continue;
|
||||
}
|
||||
|
||||
found_potential_fork:
|
||||
// (1) ForkStay bbM
|
||||
dbgln_if(REGEX_DEBUG, "Examining block {} from {} to {}", i, block.start, block.end);
|
||||
auto& first_op = bytecode.get_opcode(state);
|
||||
if (first_op.opcode_id() != OpCodeId::ForkStay) {
|
||||
dbgln_if(REGEX_DEBUG, " did not find ForkStay at {}", state.instruction_position);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto fork_ip = state.instruction_position;
|
||||
auto& fork_op = to<OpCode_ForkStay>(first_op);
|
||||
|
||||
// Find the actual following block by the fork target
|
||||
auto fork_target = fork_ip + fork_op.size() + fork_op.offset();
|
||||
|
||||
size_t following_block_idx = 0;
|
||||
bool found_following_block = false;
|
||||
for (size_t j = 0; j < basic_blocks.size(); ++j) {
|
||||
if (basic_blocks[j].start == fork_target) {
|
||||
if (basic_blocks[j].start < basic_blocks[j].end) {
|
||||
following_block_idx = j;
|
||||
found_following_block = true;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_following_block) {
|
||||
dbgln_if(REGEX_DEBUG, " did not find non-empty following block for fork target {}", fork_target);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto const& following_block = basic_blocks[following_block_idx];
|
||||
dbgln_if(REGEX_DEBUG, " Fork target {} is in block {} (from {} to {})", fork_target, following_block_idx, following_block.start, following_block.end);
|
||||
|
||||
state.instruction_position += first_op.size();
|
||||
|
||||
// (2) Checkpoint p
|
||||
auto& second_op = bytecode.get_opcode(state);
|
||||
if (second_op.opcode_id() != OpCodeId::Checkpoint) {
|
||||
dbgln_if(REGEX_DEBUG, " did not find Checkpoint at {} (found opcode {})", state.instruction_position, (int)second_op.opcode_id());
|
||||
continue;
|
||||
}
|
||||
|
||||
auto checkpoint_ip = state.instruction_position;
|
||||
auto checkpoint_id = to<OpCode_Checkpoint>(second_op).id();
|
||||
|
||||
state.instruction_position += second_op.size();
|
||||
|
||||
// (3) Compare AnyChar
|
||||
auto& third_op = bytecode.get_opcode(state);
|
||||
if (third_op.opcode_id() != OpCodeId::Compare) {
|
||||
dbgln_if(REGEX_DEBUG, " did not find Compare at {} (found opcode {})", state.instruction_position, (int)third_op.opcode_id());
|
||||
continue;
|
||||
}
|
||||
|
||||
auto compare_ip = state.instruction_position;
|
||||
auto& compare_op = to<OpCode_Compare>(third_op);
|
||||
auto flat_compares = compare_op.flat_compares();
|
||||
|
||||
if (flat_compares.size() != 1 || flat_compares[0].type != CharacterCompareType::AnyChar) {
|
||||
dbgln_if(REGEX_DEBUG, " Compare at {} is not AnyChar", state.instruction_position);
|
||||
continue;
|
||||
}
|
||||
|
||||
state.instruction_position += third_op.size();
|
||||
|
||||
// (4) JumpNonEmpty back to ForkStay
|
||||
auto& fourth_op = bytecode.get_opcode(state);
|
||||
if (fourth_op.opcode_id() != OpCodeId::JumpNonEmpty) {
|
||||
dbgln_if(REGEX_DEBUG, " did not find JumpNonEmpty at {} (found opcode {})", state.instruction_position, (int)fourth_op.opcode_id());
|
||||
continue;
|
||||
}
|
||||
|
||||
auto jump_ip = state.instruction_position;
|
||||
auto& jump_op = to<OpCode_JumpNonEmpty>(fourth_op);
|
||||
|
||||
if (jump_ip + jump_op.size() + jump_op.offset() != fork_ip) {
|
||||
dbgln_if(REGEX_DEBUG, " JumpNonEmpty at {} does not jump back to ForkStay at {} (instead jumps to {})",
|
||||
state.instruction_position, fork_ip, jump_ip + jump_op.size() + jump_op.offset());
|
||||
continue;
|
||||
}
|
||||
if ((size_t)jump_op.checkpoint() != checkpoint_id) {
|
||||
dbgln_if(REGEX_DEBUG, " JumpNonEmpty at {} does not reference Checkpoint id {} (instead references {})",
|
||||
state.instruction_position, checkpoint_id, jump_op.checkpoint());
|
||||
continue;
|
||||
}
|
||||
|
||||
dbgln_if(REGEX_DEBUG, " Found .* pattern from IP {} to {}", fork_ip, jump_ip + jump_op.size());
|
||||
|
||||
// The following block must contain a Compare C, with only non-matching ops in between
|
||||
state.instruction_position = following_block.start;
|
||||
while (state.instruction_position < following_block.end) {
|
||||
auto& op = bytecode.get_opcode(state);
|
||||
|
||||
switch (op.opcode_id()) {
|
||||
case OpCodeId::Checkpoint:
|
||||
case OpCodeId::Save:
|
||||
case OpCodeId::SaveLeftCaptureGroup:
|
||||
case OpCodeId::SaveRightCaptureGroup:
|
||||
case OpCodeId::SaveRightNamedCaptureGroup:
|
||||
case OpCodeId::ClearCaptureGroup:
|
||||
state.instruction_position += op.size();
|
||||
continue;
|
||||
|
||||
case OpCodeId::Compare: {
|
||||
auto& following_compare_op = to<OpCode_Compare>(op);
|
||||
auto following_compares = following_compare_op.flat_compares();
|
||||
|
||||
StaticallyInterpretedCompares compares;
|
||||
if (!interpret_compares(following_compares, compares, &bytecode, true)) {
|
||||
dbgln_if(REGEX_DEBUG, " could not statically interpret compares at {} in following block", state.instruction_position);
|
||||
goto next_block;
|
||||
}
|
||||
|
||||
// Must be able to pull a single code point from this compare (i.e. a single 1-long range, no negations, no char classes, and no unicode properties)
|
||||
if (compares.ranges.size() != 1 || !compares.negated_ranges.is_empty()
|
||||
|| !compares.char_classes.is_empty() || !compares.negated_char_classes.is_empty()
|
||||
|| compares.has_any_unicode_property
|
||||
|| !compares.unicode_general_categories.is_empty()
|
||||
|| !compares.unicode_properties.is_empty()
|
||||
|| !compares.unicode_scripts.is_empty()
|
||||
|| !compares.unicode_script_extensions.is_empty()
|
||||
|| !compares.negated_unicode_general_categories.is_empty()
|
||||
|| !compares.negated_unicode_properties.is_empty()
|
||||
|| !compares.negated_unicode_scripts.is_empty()
|
||||
|| !compares.negated_unicode_script_extensions.is_empty()) {
|
||||
dbgln_if(REGEX_DEBUG, " compares at {} in following block are too complex to rewrite as SeekTo", state.instruction_position);
|
||||
goto next_block;
|
||||
}
|
||||
|
||||
auto it = compares.ranges.begin();
|
||||
if (it.key() != *it) {
|
||||
// Not a single code point
|
||||
dbgln_if(REGEX_DEBUG, " compares at {} in following block are a range, not a single code point ({}..{})", state.instruction_position, it.key(), *it);
|
||||
goto next_block;
|
||||
}
|
||||
|
||||
auto seeked_code_point = it.key();
|
||||
|
||||
candidates.append({ fork_ip,
|
||||
checkpoint_ip,
|
||||
compare_ip,
|
||||
jump_ip,
|
||||
following_block.start,
|
||||
checkpoint_id,
|
||||
seeked_code_point });
|
||||
|
||||
dbgln_if(REGEX_DEBUG, " Found sequence from {} to {} followed by Compare '{}' at {}, can rewrite as SeekTo",
|
||||
fork_ip, jump_ip + 4, (char)seeked_code_point, state.instruction_position);
|
||||
goto next_block;
|
||||
}
|
||||
|
||||
default:
|
||||
dbgln_if(REGEX_DEBUG, " Hit non-matching, non-skippable opcode {} at {} in following block", (int)op.opcode_id(), state.instruction_position);
|
||||
goto next_block;
|
||||
}
|
||||
}
|
||||
|
||||
next_block:
|
||||
continue;
|
||||
}
|
||||
|
||||
dbgln_if(REGEX_DEBUG, "Found {} dot-star sequences to rewrite as SeekTo", candidates.size());
|
||||
|
||||
if (candidates.is_empty())
|
||||
return;
|
||||
|
||||
BytecodeRewriter rewriter(bytecode);
|
||||
|
||||
for (auto& candidate : candidates) {
|
||||
rewriter.mark_range_for_skip(candidate.fork_ip, candidate.jump_ip + 4); // JumpNonEmpty = 4
|
||||
}
|
||||
|
||||
size_t candidate_index = 0;
|
||||
auto insert_replacement = [&](auto const& instr, ByteCode& result) {
|
||||
while (candidate_index < candidates.size()) {
|
||||
auto& candidate = candidates[candidate_index];
|
||||
|
||||
if (instr.old_ip == candidate.fork_ip) {
|
||||
result.empend(static_cast<ByteCodeValueType>(OpCodeId::RSeekTo));
|
||||
result.empend(candidate.seek_code_point);
|
||||
result.empend(static_cast<ByteCodeValueType>(OpCodeId::ForkStay));
|
||||
result.empend(static_cast<ByteCodeValueType>(-4)); // Offset back to RSeekTo
|
||||
candidate_index++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (instr.old_ip < candidate.jump_ip + 4)
|
||||
return;
|
||||
|
||||
candidate_index++;
|
||||
}
|
||||
};
|
||||
|
||||
parser_result.bytecode = rewriter.rebuild(bytecode, move(insert_replacement));
|
||||
|
||||
if constexpr (REGEX_DEBUG) {
|
||||
dbgln("After dot-star rewrite as SeekTo:");
|
||||
RegexDebug<ByteCode> dbg;
|
||||
dbg.print_bytecode(*this);
|
||||
}
|
||||
}
|
||||
|
||||
void Optimizer::append_alternation(ByteCode& target, ByteCode&& left, ByteCode&& right)
|
||||
{
|
||||
Array<ByteCode, 2> alternatives;
|
||||
|
|
|
|||
|
|
@ -740,7 +740,7 @@ ALWAYS_INLINE bool PosixExtendedParser::parse_sub_expression(ByteCode& stack, si
|
|||
if (length > 1) {
|
||||
// last character is inserted into 'bytecode' for duplication symbol handling
|
||||
auto new_length = length - (match_repetition_symbol() ? 1 : 0);
|
||||
auto substring = start_token.value().substring_view(0, new_length);
|
||||
auto substring = m_parser_state.lexer.source().substring_view_starting_from_substring(start_token.value()).substring_view(0, new_length);
|
||||
stack.insert_bytecode_compare_string(Utf16FlyString::from_utf8(substring));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue