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:
Ali Mohammad Pur 2025-12-22 13:05:47 +01:00 committed by Andreas Kling
parent 77d982d6fe
commit 637d47ba30
9 changed files with 467 additions and 27 deletions

View file

@ -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 };

View file

@ -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
{

View file

@ -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
{

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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&);
};

View file

@ -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;

View file

@ -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));
}