Index: js/src/irregexp/imported/regexp-bytecode-generator.h =================================================================== --- js/src/irregexp/imported/regexp-bytecode-generator.h +++ js/src/irregexp/imported/regexp-bytecode-generator.h @@ -141,11 +141,11 @@ void CheckPosition(int cp_offset, Label* on_outside_input) override; void CheckSpecialClassRanges(StandardCharacterSet type, Label* on_no_match) override; void LoadCurrentCharacterImpl(int cp_offset, Label* on_end_of_input, bool check_bounds, int characters, - int eats_at_least) override; + int bounds_check_offset) override; void CheckCharacter(unsigned c, Label* on_equal) override; void CheckCharacterAfterAnd(unsigned c, unsigned mask, Label* on_equal) override; void CheckCharacterGT(base::uc16 limit, Label* on_greater) override; void CheckCharacterLT(base::uc16 limit, Label* on_less) override; @@ -178,25 +178,25 @@ return false; } void CheckBitInTable(Handle table, Label* on_bit_set) override; void SkipUntilBitInTable(int cp_offset, Handle table, Handle nibble_table, int advance_by, - Label* on_match, Label* on_no_match) override; + int bounds_check_offset, Label* on_match, + Label* on_no_match) override; void SkipUntilCharAnd(int cp_offset, int advance_by, unsigned character, - unsigned mask, int eats_at_least, Label* on_match, + unsigned mask, int bounds_check_offset, Label* on_match, Label* on_no_match) override; void SkipUntilChar(int cp_offset, int advance_by, unsigned character, - Label* on_match, Label* on_no_match) override; - void SkipUntilCharPosChecked(int cp_offset, int advance_by, - unsigned character, int eats_at_least, - Label* on_match, Label* on_no_match) override; + int bounds_check_offset, Label* on_match, + Label* on_no_match) override; void SkipUntilCharOrChar(int cp_offset, int advance_by, unsigned char1, - unsigned char2, Label* on_match, - Label* on_no_match) override; + unsigned char2, int bounds_check_offset, + Label* on_match, Label* on_no_match) override; void SkipUntilGtOrNotBitInTable(int cp_offset, int advance_by, unsigned character, Handle table, - Label* on_match, Label* on_no_match) override; + int bounds_check_offset, Label* on_match, + Label* on_no_match) override; void SkipUntilOneOfMasked(int cp_offset, int advance_by, unsigned both_chars, unsigned both_mask, int max_offset, unsigned chars1, unsigned mask1, unsigned chars2, unsigned mask2, Label* on_match1, Label* on_match2, Label* on_failure) override; Index: js/src/irregexp/imported/regexp-bytecode-generator.cc =================================================================== --- js/src/irregexp/imported/regexp-bytecode-generator.cc +++ js/src/irregexp/imported/regexp-bytecode-generator.cc @@ -223,11 +223,11 @@ } template void BytecodeGenerator::Emit(Args... args) { // Converts nullptr labels into our internal backtrack_ label. - [[maybe_unused]] auto fix_label = [this](auto arg) { + auto fix_label = [this](auto arg) { if constexpr (std::is_convertible_v) { Label* l = static_cast(arg); return l ? l : &backtrack_; } else { return arg; @@ -354,40 +354,24 @@ void BytecodeGenerator::LoadCurrentCharacterImpl(int cp_offset, Label* on_failure, bool check_bounds, int characters, - int eats_at_least) { - DCHECK_GE(eats_at_least, characters); - if (eats_at_least > characters && check_bounds) { - // eats_at_least + cp_offset can overflow BytecodeOperandType::kOffset. - // Only emit this optimization if the computed value fits into the expected - // range for the CheckPosition BC. - using CheckPositionOps = BytecodeOperands; - using OffsetTraits = - OperandTypeTraits; - using OffsetType = OffsetTraits::kCType; - base::internal::CheckedNumeric checked_offset = cp_offset; - checked_offset += eats_at_least - 1; - OffsetType total_offset; - if (checked_offset.AssignIfValid(&total_offset) && - base::IsInRange(total_offset, OffsetTraits::kMinValue, - OffsetTraits::kMaxValue)) { - Emit(total_offset, on_failure); - check_bounds = false; // Load below doesn't need to check. - } - } - + int bounds_check_offset) { CHECK(base::IsInRange(cp_offset, kMinCPOffset, kMaxCPOffset)); if (check_bounds) { + DCHECK_LE(cp_offset, bounds_check_offset); if (characters == 4) { - Emit(cp_offset, on_failure); + Emit(cp_offset, bounds_check_offset, + on_failure); } else if (characters == 2) { - Emit(cp_offset, on_failure); + Emit(cp_offset, bounds_check_offset, + on_failure); } else { DCHECK_EQ(1, characters); - Emit(cp_offset, on_failure); + Emit(cp_offset, bounds_check_offset, + on_failure); } } else { if (characters == 4) { Emit(cp_offset); } else if (characters == 2) { @@ -475,64 +459,58 @@ void BytecodeGenerator::CheckBitInTable(Handle table, Label* on_bit_set) { Emit(on_bit_set, table); } -void BytecodeGenerator::SkipUntilBitInTable(int cp_offset, - Handle table, - Handle nibble_table, - int advance_by, Label* on_match, - Label* on_no_match) { - Emit(cp_offset, advance_by, table, on_match, - on_no_match); +void BytecodeGenerator::SkipUntilBitInTable( + int cp_offset, Handle table, Handle nibble_table, + int advance_by, int bounds_check_offset, Label* on_match, + Label* on_no_match) { + Emit( + cp_offset, advance_by, table, bounds_check_offset, on_match, on_no_match); } void BytecodeGenerator::SkipUntilCharAnd(int cp_offset, int advance_by, unsigned character, unsigned mask, - int eats_at_least, Label* on_match, - Label* on_no_match) { + int bounds_check_offset, + Label* on_match, Label* on_no_match) { Emit(cp_offset, advance_by, character, mask, - eats_at_least, on_match, on_no_match); + bounds_check_offset, on_match, on_no_match); } void BytecodeGenerator::SkipUntilChar(int cp_offset, int advance_by, - unsigned character, Label* on_match, + unsigned character, + int bounds_check_offset, Label* on_match, Label* on_no_match) { - // Only generated by peephole optimization. - UNREACHABLE(); -} - -void BytecodeGenerator::SkipUntilCharPosChecked(int cp_offset, int advance_by, - unsigned character, - int eats_at_least, - Label* on_match, - Label* on_no_match) { - // Only generated by peephole optimization. - UNREACHABLE(); + Emit(cp_offset, advance_by, character, + bounds_check_offset, on_match, on_no_match); } void BytecodeGenerator::SkipUntilCharOrChar(int cp_offset, int advance_by, unsigned char1, unsigned char2, + int bounds_check_offset, Label* on_match, Label* on_no_match) { - // Only generated by peephole optimization. - UNREACHABLE(); + Emit(cp_offset, advance_by, char1, char2, + bounds_check_offset, on_match, + on_no_match); } void BytecodeGenerator::SkipUntilGtOrNotBitInTable( int cp_offset, int advance_by, unsigned character, Handle table, - Label* on_match, Label* on_no_match) { + int bounds_check_offset, Label* on_match, Label* on_no_match) { // Only generated by peephole optimization. UNREACHABLE(); } void BytecodeGenerator::SkipUntilOneOfMasked( int cp_offset, int advance_by, unsigned both_chars, unsigned both_mask, int max_offset, unsigned chars1, unsigned mask1, unsigned chars2, unsigned mask2, Label* on_match1, Label* on_match2, Label* on_failure) { - // Only generated by peephole optimization. - UNREACHABLE(); + Emit( + cp_offset, advance_by, both_chars, both_mask, max_offset, chars1, mask1, + chars2, mask2, on_match1, on_match2, on_failure); } void BytecodeGenerator::SkipUntilOneOfMasked3( const SkipUntilOneOfMasked3Args& args) { // Only generated by peephole optimization. Index: js/src/irregexp/imported/regexp-bytecode-peephole.cc =================================================================== --- js/src/irregexp/imported/regexp-bytecode-peephole.cc +++ js/src/irregexp/imported/regexp-bytecode-peephole.cc @@ -583,50 +583,33 @@ .IfArgumentEqualsOffset(I(B::kAdvanceCpAndGoto, on_goto), 0) .ReplaceWith(Target) .MapArgument(T(cp_offset), 0, I(B::kLoadCurrentCharacter, cp_offset)) .MapArgument(T(advance_by), 2, I(B::kAdvanceCpAndGoto, by)) .MapArgument(T(table), 1, I(B::kCheckBitInTable, table)) + .MapArgument(T(bounds_check_offset), 0, + I(B::kLoadCurrentCharacter, bounds_check_offset)) .MapArgument(T(on_match), 1, I(B::kCheckBitInTable, on_bit_set)) .MapArgument(T(on_no_match), 0, I(B::kLoadCurrentCharacter, on_failure)) .IgnoreArgument(2, I(B::kAdvanceCpAndGoto, on_goto)); } { - static constexpr auto Target = B::kSkipUntilCharPosChecked; - CreateSequence(B::kCheckPosition) - .FollowedBy(B::kLoadCurrentCharacterUnchecked) - .FollowedBy(B::kCheckCharacter) - .FollowedBy(B::kAdvanceCpAndGoto) - .IfArgumentEqualsOffset(I(B::kAdvanceCpAndGoto, on_goto), 0) - .ReplaceWith(Target) - .MapArgument(T(cp_offset), 1, - I(B::kLoadCurrentCharacterUnchecked, cp_offset)) - .MapArgument(T(advance_by), 3, I(B::kAdvanceCpAndGoto, by)) - .MapArgument(T(character), 2, I(B::kCheckCharacter, character)) - .MapArgument(T(eats_at_least), 0, I(B::kCheckPosition, cp_offset)) - .MapArgument(T(on_match), 2, I(B::kCheckCharacter, on_equal)) - .MapArgument(T(on_no_match), 0, I(B::kCheckPosition, on_failure)) - .IgnoreArgument(3, I(B::kAdvanceCpAndGoto, on_goto)); - } - - { static constexpr auto Target = B::kSkipUntilCharAnd; - CreateSequence(B::kCheckPosition) - .FollowedBy(B::kLoadCurrentCharacterUnchecked) + CreateSequence(B::kLoadCurrentCharacter) .FollowedBy(B::kCheckCharacterAfterAnd) .FollowedBy(B::kAdvanceCpAndGoto) .IfArgumentEqualsOffset(I(B::kAdvanceCpAndGoto, on_goto), 0) .ReplaceWith(Target) - .MapArgument(T(cp_offset), 1, - I(B::kLoadCurrentCharacterUnchecked, cp_offset)) - .MapArgument(T(advance_by), 3, I(B::kAdvanceCpAndGoto, by)) - .MapArgument(T(character), 2, I(B::kCheckCharacterAfterAnd, character)) - .MapArgument(T(mask), 2, I(B::kCheckCharacterAfterAnd, mask)) - .MapArgument(T(eats_at_least), 0, I(B::kCheckPosition, cp_offset)) - .MapArgument(T(on_match), 2, I(B::kCheckCharacterAfterAnd, on_equal)) - .MapArgument(T(on_no_match), 0, I(B::kCheckPosition, on_failure)) - .IgnoreArgument(3, I(B::kAdvanceCpAndGoto, on_goto)); + .MapArgument(T(cp_offset), 0, I(B::kLoadCurrentCharacter, cp_offset)) + .MapArgument(T(advance_by), 2, I(B::kAdvanceCpAndGoto, by)) + .MapArgument(T(character), 1, I(B::kCheckCharacterAfterAnd, character)) + .MapArgument(T(mask), 1, I(B::kCheckCharacterAfterAnd, mask)) + .MapArgument(T(bounds_check_offset), 0, + I(B::kLoadCurrentCharacter, bounds_check_offset)) + .MapArgument(T(on_match), 1, I(B::kCheckCharacterAfterAnd, on_equal)) + .MapArgument(T(on_no_match), 0, I(B::kLoadCurrentCharacter, on_failure)) + .IgnoreArgument(2, I(B::kAdvanceCpAndGoto, on_goto)); } // TODO(pthier): It might make sense for short sequences like this one to only // optimize them if the resulting optimization is not longer than the current // one. This could be the case if there are jumps inside the sequence and we @@ -640,10 +623,12 @@ .IfArgumentEqualsOffset(I(B::kAdvanceCpAndGoto, on_goto), 0) .ReplaceWith(Target) .MapArgument(T(cp_offset), 0, I(B::kLoadCurrentCharacter, cp_offset)) .MapArgument(T(advance_by), 2, I(B::kAdvanceCpAndGoto, by)) .MapArgument(T(character), 1, I(B::kCheckCharacter, character)) + .MapArgument(T(bounds_check_offset), 0, + I(B::kLoadCurrentCharacter, bounds_check_offset)) .MapArgument(T(on_match), 1, I(B::kCheckCharacter, on_equal)) .MapArgument(T(on_no_match), 0, I(B::kLoadCurrentCharacter, on_failure)) .IgnoreArgument(2, I(B::kAdvanceCpAndGoto, on_goto)); } @@ -659,10 +644,12 @@ .ReplaceWith(Target) .MapArgument(T(cp_offset), 0, I(B::kLoadCurrentCharacter, cp_offset)) .MapArgument(T(advance_by), 3, I(B::kAdvanceCpAndGoto, by)) .MapArgument(T(char1), 1, I(B::kCheckCharacter, character)) .MapArgument(T(char2), 2, I(B::kCheckCharacter, character)) + .MapArgument(T(bounds_check_offset), 0, + I(B::kLoadCurrentCharacter, bounds_check_offset)) .MapArgument(T(on_match), 1, I(B::kCheckCharacter, on_equal)) .MapArgument(T(on_no_match), 0, I(B::kLoadCurrentCharacter, on_failure)) .IgnoreArgument(2, I(B::kCheckCharacter, on_equal)) .IgnoreArgument(3, I(B::kAdvanceCpAndGoto, on_goto)); } @@ -671,15 +658,15 @@ static constexpr auto Target = B::kSkipUntilGtOrNotBitInTable; CreateSequence(B::kLoadCurrentCharacter) .FollowedBy(B::kCheckCharacterGT) // Sequence is only valid if the jump target of kCheckCharacterGT is the // first bytecode AFTER the whole sequence. - .IfArgumentEqualsOffset(I(B::kCheckCharacterGT, on_greater), 56) + .IfArgumentEqualsOffset(I(B::kCheckCharacterGT, on_greater), 60) .FollowedBy(B::kCheckBitInTable) // Sequence is only valid if the jump target of kCheckBitInTable is // the kAdvanceCpAndGoto bytecode at the end of the sequence. - .IfArgumentEqualsOffset(I(B::kCheckBitInTable, on_bit_set), 48) + .IfArgumentEqualsOffset(I(B::kCheckBitInTable, on_bit_set), 52) .FollowedBy(B::kGoTo) // Sequence is only valid if the jump target of kGoTo is the same as the // jump target of kCheckCharacterGT (i.e. both jump to the first // bytecode AFTER the whole sequence. .IfArgumentEqualsValueAtOffset(I(B::kGoTo, label), 1, @@ -689,20 +676,21 @@ .ReplaceWith(Target) .MapArgument(T(cp_offset), 0, I(B::kLoadCurrentCharacter, cp_offset)) .MapArgument(T(advance_by), 4, I(B::kAdvanceCpAndGoto, by)) .MapArgument(T(character), 1, I(B::kCheckCharacterGT, limit)) .MapArgument(T(table), 2, I(B::kCheckBitInTable, table)) + .MapArgument(T(bounds_check_offset), 0, + I(B::kLoadCurrentCharacter, bounds_check_offset)) .MapArgument(T(on_match), 1, I(B::kCheckCharacterGT, on_greater)) .MapArgument(T(on_no_match), 0, I(B::kLoadCurrentCharacter, on_failure)) .IgnoreArgument(2, I(B::kCheckBitInTable, on_bit_set)) .IgnoreArgument(3, I(B::kGoTo, label)) .IgnoreArgument(4, I(B::kAdvanceCpAndGoto, on_goto)); } { static constexpr auto Target = B::kSkipUntilOneOfMasked; - CreateSequence(B::kCheckPosition) - .FollowedBy(B::kLoad4CurrentCharsUnchecked) + CreateSequence(B::kLoad4CurrentChars) .FollowedBy(B::kAndCheck4Chars) // Jump target is the offset of the next AndCheck4Chars (right after // AdvanceCpAndGoto). .IfArgumentEqualsOffset(I(B::kAndCheck4Chars, on_equal), 0x24) .FollowedBy(B::kAdvanceCpAndGoto) @@ -710,106 +698,104 @@ .FollowedBy(B::kAndCheck4Chars) .FollowedBy(B::kAndCheckNot4Chars) // Jump target is AdvanceCpAndGoto. .IfArgumentEqualsOffset(I(B::kAndCheckNot4Chars, on_not_equal), 0x1c) .ReplaceWith(Target) - .MapArgument(T(cp_offset), 1, - I(B::kLoad4CurrentCharsUnchecked, cp_offset)) - .MapArgument(T(advance_by), 3, I(B::kAdvanceCpAndGoto, by)) - .MapArgument(T(both_chars), 2, I(B::kAndCheck4Chars, characters)) - .MapArgument(T(both_mask), 2, I(B::kAndCheck4Chars, mask)) - .MapArgument(T(max_offset), 0, I(B::kCheckPosition, cp_offset)) - .MapArgument(T(chars1), 4, I(B::kAndCheck4Chars, characters)) - .MapArgument(T(mask1), 4, I(B::kAndCheck4Chars, mask)) - .MapArgument(T(chars2), 5, I(B::kAndCheckNot4Chars, characters)) - .MapArgument(T(mask2), 5, I(B::kAndCheckNot4Chars, mask)) - .MapArgument(T(on_match1), 4, I(B::kAndCheck4Chars, on_equal)) + .MapArgument(T(cp_offset), 0, I(B::kLoad4CurrentChars, cp_offset)) + .MapArgument(T(advance_by), 2, I(B::kAdvanceCpAndGoto, by)) + .MapArgument(T(both_chars), 1, I(B::kAndCheck4Chars, characters)) + .MapArgument(T(both_mask), 1, I(B::kAndCheck4Chars, mask)) + .MapArgument(T(max_offset), 0, + I(B::kLoad4CurrentChars, bounds_check_offset)) + .MapArgument(T(chars1), 3, I(B::kAndCheck4Chars, characters)) + .MapArgument(T(mask1), 3, I(B::kAndCheck4Chars, mask)) + .MapArgument(T(chars2), 4, I(B::kAndCheckNot4Chars, characters)) + .MapArgument(T(mask2), 4, I(B::kAndCheckNot4Chars, mask)) + .MapArgument(T(on_match1), 3, I(B::kAndCheck4Chars, on_equal)) .EmitOffsetAfterSequence(T(on_match2)) - .MapArgument(T(on_failure), 0, I(B::kCheckPosition, on_failure)) - .IgnoreArgument(3, I(B::kAdvanceCpAndGoto, on_goto)) - .IgnoreArgument(2, I(B::kAndCheck4Chars, on_equal)); + .MapArgument(T(on_failure), 0, I(B::kLoad4CurrentChars, on_failure)) + .IgnoreArgument(2, I(B::kAdvanceCpAndGoto, on_goto)) + .IgnoreArgument(1, I(B::kAndCheck4Chars, on_equal)); } // The original bytecode sequence for kSkipUntilOneOfMasked3 is: // // sequence offset name // bc0 0 SkipUntilBitInTable - // bc1 20 CheckPosition - // bc2 28 Load4CurrentCharsUnchecked - // bc3 2c AndCheck4Chars - // bc4 3c AdvanceCpAndGoto - // bc5 48 Load4CurrentChars - // bc6 4c AndCheck4Chars - // bc7 5c AndCheck4Chars - // bc8 6c AndCheckNot4Chars + // bc1 24 Load4CurrentChars + // bc2 30 AndCheck4Chars + // bc3 40 AdvanceCpAndGoto + // bc4 48 Load4CurrentChars + // bc5 54 AndCheck4Chars + // bc6 64 AndCheck4Chars + // bc7 74 AndCheckNot4Chars { static constexpr int kOffsetOfBc0SkipUntilBitInTable = 0x0; - static constexpr int kOffsetOfBc1CheckCurrentPosition = 0x20; - static constexpr int kOffsetOfBc4AdvanceBcAndGoto = 0x3c; + static constexpr int kOffsetOfBc1Load4CurrentChars = 0x24; + static constexpr int kOffsetOfBc3AdvanceCpAndGoto = 0x40; static constexpr auto Target = B::kSkipUntilOneOfMasked3; BytecodeSequenceNode& s0 = CreateSequence(B::kSkipUntilBitInTable) + .IfArgumentEqualsOffset(I(B::kSkipUntilBitInTable, on_match), + kOffsetOfBc1Load4CurrentChars) .IfArgumentEqualsOffset(I(B::kSkipUntilBitInTable, on_no_match), - kOffsetOfBc1CheckCurrentPosition) - .IfArgumentEqualsOffset(I(B::kSkipUntilBitInTable, on_no_match), - kOffsetOfBc1CheckCurrentPosition); + kOffsetOfBc1Load4CurrentChars); - DCHECK_EQ(s0.SequenceLength(), 0x20); - DCHECK_EQ(s0.SequenceLength(), kOffsetOfBc1CheckCurrentPosition); - static constexpr int kOffsetOfBc5Load4CurrentChars = 0x44; + DCHECK_EQ(s0.SequenceLength(), kOffsetOfBc1Load4CurrentChars); + static constexpr int kOffsetOfBc4Load4CurrentChars = 0x48; BytecodeSequenceNode& s1 = - s0.FollowedBy(B::kCheckPosition) - .FollowedBy(B::kLoad4CurrentCharsUnchecked) + s0.FollowedBy(B::kLoad4CurrentChars) .FollowedBy(B::kAndCheck4Chars) .IfArgumentEqualsOffset(I(B::kAndCheck4Chars, on_equal), - kOffsetOfBc5Load4CurrentChars); + kOffsetOfBc4Load4CurrentChars); - DCHECK_EQ(s1.SequenceLength(), 0x3c); - DCHECK_EQ(s1.SequenceLength(), kOffsetOfBc4AdvanceBcAndGoto); + DCHECK_EQ(s1.SequenceLength(), kOffsetOfBc3AdvanceCpAndGoto); BytecodeSequenceNode& s2 = s1.FollowedBy(B::kAdvanceCpAndGoto) .IfArgumentEqualsOffset(I(B::kAdvanceCpAndGoto, on_goto), kOffsetOfBc0SkipUntilBitInTable); - DCHECK_EQ(s2.SequenceLength(), 0x44); - DCHECK_EQ(s2.SequenceLength(), kOffsetOfBc5Load4CurrentChars); + DCHECK_EQ(s2.SequenceLength(), kOffsetOfBc4Load4CurrentChars); BytecodeSequenceNode& s3 = s2.FollowedBy(B::kLoad4CurrentChars) .IfArgumentEqualsOffset(I(B::kLoad4CurrentChars, on_failure), - kOffsetOfBc4AdvanceBcAndGoto) + kOffsetOfBc3AdvanceCpAndGoto) .FollowedBy(B::kAndCheck4Chars) .FollowedBy(B::kAndCheck4Chars) .FollowedBy(B::kAndCheckNot4Chars) .IfArgumentEqualsOffset(I(B::kAndCheckNot4Chars, on_not_equal), - kOffsetOfBc4AdvanceBcAndGoto); + kOffsetOfBc3AdvanceCpAndGoto); s3.ReplaceWith(Target) .MapArgument(T(bc0_cp_offset), 0, I(B::kSkipUntilBitInTable, cp_offset)) .MapArgument(T(bc0_advance_by), 0, I(B::kSkipUntilBitInTable, advance_by)) .MapArgument(T(bc0_table), 0, I(B::kSkipUntilBitInTable, table)) .IgnoreArgument(0, I(B::kSkipUntilBitInTable, on_match)) .IgnoreArgument(0, I(B::kSkipUntilBitInTable, on_no_match)) - .MapArgument(T(bc1_cp_offset), 1, I(B::kCheckPosition, cp_offset)) - .MapArgument(T(bc1_on_failure), 1, I(B::kCheckPosition, on_failure)) - .MapArgument(T(bc2_cp_offset), 2, - I(B::kLoad4CurrentCharsUnchecked, cp_offset)) - .MapArgument(T(bc3_characters), 3, I(B::kAndCheck4Chars, characters)) - .MapArgument(T(bc3_mask), 3, I(B::kAndCheck4Chars, mask)) - .IgnoreArgument(3, I(B::kAndCheck4Chars, on_equal)) - .MapArgument(T(bc4_by), 4, I(B::kAdvanceCpAndGoto, by)) - .IgnoreArgument(4, I(B::kAdvanceCpAndGoto, on_goto)) - .MapArgument(T(bc5_cp_offset), 5, I(B::kLoad4CurrentChars, cp_offset)) - .IgnoreArgument(5, I(B::kLoad4CurrentChars, on_failure)) + .IgnoreArgument(0, I(B::kSkipUntilBitInTable, bounds_check_offset)) + .MapArgument(T(bc1_bounds_check_offset), 1, + I(B::kLoad4CurrentChars, bounds_check_offset)) + .MapArgument(T(bc1_on_failure), 1, I(B::kLoad4CurrentChars, on_failure)) + .MapArgument(T(bc1_cp_offset), 1, I(B::kLoad4CurrentChars, cp_offset)) + .MapArgument(T(bc2_characters), 2, I(B::kAndCheck4Chars, characters)) + .MapArgument(T(bc2_mask), 2, I(B::kAndCheck4Chars, mask)) + .IgnoreArgument(2, I(B::kAndCheck4Chars, on_equal)) + .MapArgument(T(bc3_by), 3, I(B::kAdvanceCpAndGoto, by)) + .IgnoreArgument(3, I(B::kAdvanceCpAndGoto, on_goto)) + .MapArgument(T(bc4_bounds_check_offset), 4, + I(B::kLoad4CurrentChars, bounds_check_offset)) + .MapArgument(T(bc4_cp_offset), 4, I(B::kLoad4CurrentChars, cp_offset)) + .IgnoreArgument(4, I(B::kLoad4CurrentChars, on_failure)) + .MapArgument(T(bc5_characters), 5, I(B::kAndCheck4Chars, characters)) + .MapArgument(T(bc5_mask), 5, I(B::kAndCheck4Chars, mask)) + .MapArgument(T(bc5_on_equal), 5, I(B::kAndCheck4Chars, on_equal)) .MapArgument(T(bc6_characters), 6, I(B::kAndCheck4Chars, characters)) .MapArgument(T(bc6_mask), 6, I(B::kAndCheck4Chars, mask)) .MapArgument(T(bc6_on_equal), 6, I(B::kAndCheck4Chars, on_equal)) - .MapArgument(T(bc7_characters), 7, I(B::kAndCheck4Chars, characters)) - .MapArgument(T(bc7_mask), 7, I(B::kAndCheck4Chars, mask)) - .MapArgument(T(bc7_on_equal), 7, I(B::kAndCheck4Chars, on_equal)) - .MapArgument(T(bc8_characters), 8, I(B::kAndCheckNot4Chars, characters)) - .MapArgument(T(bc8_mask), 8, I(B::kAndCheckNot4Chars, mask)) - .IgnoreArgument(8, I(B::kAndCheckNot4Chars, on_not_equal)) + .MapArgument(T(bc7_characters), 7, I(B::kAndCheckNot4Chars, characters)) + .MapArgument(T(bc7_mask), 7, I(B::kAndCheckNot4Chars, mask)) + .IgnoreArgument(7, I(B::kAndCheckNot4Chars, on_not_equal)) .EmitOffsetAfterSequence(T(fallthrough_jump_target)); } #undef I #undef T @@ -1088,11 +1074,10 @@ DirectHandle array = isolate->factory()->NewTrustedByteArray(optimized_length); MemCopy(array->begin(), optimized_bytecode, optimized_length); -#ifndef COMPILING_IRREGEXP_FOR_EXTERNAL_EMBEDDER if (did_optimize && v8_flags.trace_regexp_peephole_optimization) { std::unique_ptr pattern_cstring = re_data->escaped_source()->ToCString(); PrintF("Original Bytecode:\n"); RegExpBytecodeDisassemble(original_bytecode->data(), @@ -1100,11 +1085,10 @@ pattern_cstring.get()); PrintF("Optimized Bytecode:\n"); RegExpBytecodeDisassemble(array->begin(), optimized_length, pattern_cstring.get()); } -#endif // !COMPILING_IRREGEXP_FOR_EXTERNAL_EMBEDDER return array; } } // namespace regexp Index: js/src/irregexp/imported/regexp-bytecodes-inl.h =================================================================== --- js/src/irregexp/imported/regexp-bytecodes-inl.h +++ js/src/irregexp/imported/regexp-bytecodes-inl.h @@ -73,11 +73,11 @@ constexpr int CountOf() { return sizeof...(Args); } template -constexpr std::array SplitNames(const char* raw_names) { +consteval std::array SplitNames(const char* raw_names) { std::array result; std::string_view names(raw_names); // Remove '(' and ')'. DCHECK_EQ(names.front(), '('); @@ -102,11 +102,11 @@ } // Calculates packed offsets for each Bytecode operand. // All operands are aligned to their own size. template -constexpr auto CalculateAlignedOffsets() { +consteval auto CalculateAlignedOffsets() { constexpr int N = sizeof...(operand_types); constexpr std::array kOperandSizes = { OperandTypeTraits::kSize...}; constexpr std::array kOperandAlignments = { OperandTypeTraits::kAlignment...}; @@ -154,45 +154,46 @@ struct BytecodeOperandNames; #define DECLARE_OPERAND_NAMES(CamelName, OpNames, OpTypes, ...) \ template <> \ struct BytecodeOperandNames { \ - enum Operand { UNPAREN(OpNames) }; \ + enum class Operand { UNPAREN(OpNames) }; \ + using enum Operand; \ static constexpr size_t kCount = detail::CountOf(); \ static constexpr auto kNames = detail::SplitNames(#OpNames); \ static constexpr std::string_view Name(Operand op) { \ return kNames[static_cast(op)]; \ } \ }; REGEXP_BYTECODE_LIST(DECLARE_OPERAND_NAMES) #undef DECLARE_OPERAND_NAMES template -class BytecodeOperandsBase : public BytecodeOperandNames { +class BytecodeOperandsBase { public: static constexpr Bytecode kBytecode = bc; using Operand = BytecodeOperandNames::Operand; using Traits = BytecodeOperandsTraits; static constexpr int kCount = Traits::kOperandCount; static constexpr int kTotalSize = Traits::kSize; - static constexpr int Index(Operand op) { return static_cast(op); } - static constexpr int Size(Operand op) { + static consteval int Index(Operand op) { return static_cast(op); } + static consteval int Size(Operand op) { return Traits::kOperandSizes[Index(op)]; } - static constexpr int Offset(Operand op) { + static consteval int Offset(Operand op) { return Traits::kOperandOffsets[Index(op)]; } - static constexpr BytecodeOperandType Type(Operand op) { + static consteval BytecodeOperandType Type(Operand op) { return Traits::kOperandTypes[Index(op)]; } static constexpr std::string_view Name(Operand op) { return BytecodeOperandNames::Name(op); } // Returns a tuple of all operands. - static constexpr auto GetOperandsTuple() { + static consteval auto GetOperandsTuple() { return [](std::index_sequence) { return std::tuple_cat([]() { constexpr auto id = static_cast(I); return std::tuple(std::integral_constant{}); }.template operator()()...); @@ -291,10 +292,12 @@ template <> \ class BytecodeOperands final \ : public detail::BytecodeOperandsBase, \ public AllStatic { \ + public: \ + using enum Operand; \ }; REGEXP_BYTECODE_LIST(DECLARE_OPERANDS) #undef DECLARE_OPERANDS Index: js/src/irregexp/imported/regexp-bytecodes.h =================================================================== --- js/src/irregexp/imported/regexp-bytecodes.h +++ js/src/irregexp/imported/regexp-bytecodes.h @@ -19,18 +19,21 @@ V(Int32, int32_t) \ V(Uint32, uint32_t) \ V(Char, base::uc16) \ V(JumpTarget, uint32_t) -#define BASIC_BYTECODE_OPERAND_TYPE_LIMITS_LIST(V) \ - V(Offset, int16_t, RegExpMacroAssembler::kMinCPOffset, \ - RegExpMacroAssembler::kMaxCPOffset) \ - V(Register, uint16_t, 0, RegExpMacroAssembler::kMaxRegister) \ - V(StackCheckFlag, RegExpMacroAssembler::StackCheckFlag, \ - RegExpMacroAssembler::StackCheckFlag::kNoStackLimitCheck, \ - RegExpMacroAssembler::StackCheckFlag::kCheckStackLimit) \ - V(StandardCharacterSet, StandardCharacterSet, \ +#define BASIC_BYTECODE_OPERAND_TYPE_LIMITS_LIST(V) \ + V(Offset, int16_t, RegExpMacroAssembler::kMinCPOffset, \ + RegExpMacroAssembler::kMaxCPOffset) \ + V(BoundsCheckOffset, int32_t, RegExpMacroAssembler::kMinCPOffset, \ + RegExpMacroAssembler::kMaxCPOffset + \ + RegExpMacroAssembler::kMaxEatsAtLeastValue) \ + V(Register, uint16_t, 0, RegExpMacroAssembler::kMaxRegister) \ + V(StackCheckFlag, RegExpMacroAssembler::StackCheckFlag, \ + RegExpMacroAssembler::StackCheckFlag::kNoStackLimitCheck, \ + RegExpMacroAssembler::StackCheckFlag::kCheckStackLimit) \ + V(StandardCharacterSet, StandardCharacterSet, \ StandardCharacterSet::kEverything, StandardCharacterSet::kWord) // Special operand types that don't have a direct mapping to a C-type. // Getters/Setters for these types need to be specialized manually. // Format: V(Name, Size in bytes, Alignment in bytes) @@ -113,12 +116,14 @@ V(Succeed, (), (), (ReBcFlag::kNoFallthrough)) \ V(AdvanceCurrentPosition, (by), (ReBcOpType::kOffset), ()) \ /* Jump to another bytecode given its offset. */ \ V(GoTo, (label), (ReBcOpType::kJumpTarget), (ReBcFlag::kNoFallthrough)) \ /* Check if offset is in range and load character at given offset. */ \ - V(LoadCurrentCharacter, (cp_offset, on_failure), \ - (ReBcOpType::kOffset, ReBcOpType::kJumpTarget), (ReBcFlag::kLoadsCC)) \ + V(LoadCurrentCharacter, (cp_offset, bounds_check_offset, on_failure), \ + (ReBcOpType::kOffset, ReBcOpType::kBoundsCheckOffset, \ + ReBcOpType::kJumpTarget), \ + (ReBcFlag::kLoadsCC)) \ /* Checks if current position + given offset is in range. */ \ /* I.e. jumps to |on_failure| if current pos + |cp_offset| >= subject len */ \ V(CheckPosition, (cp_offset, on_failure), \ (ReBcOpType::kOffset, ReBcOpType::kJumpTarget), ()) \ V(CheckSpecialClassRanges, (character_set, on_no_match), \ @@ -186,16 +191,20 @@ /* Checks if the current character matches any of the characters encoded */ \ /* in a bit table. Similar to/inspired by boyer moore string search */ \ /* Todo(pthier): Change order to (table, label) and move to Basic */ \ V(CheckBitInTable, (on_bit_set, table), \ (ReBcOpType::kJumpTarget, ReBcOpType::kBitTable), (ReBcFlag::kUsesCC)) \ - V(Load2CurrentChars, (cp_offset, on_failure), \ - (ReBcOpType::kOffset, ReBcOpType::kJumpTarget), (ReBcFlag::kLoadsCC)) \ + V(Load2CurrentChars, (cp_offset, bounds_check_offset, on_failure), \ + (ReBcOpType::kOffset, ReBcOpType::kBoundsCheckOffset, \ + ReBcOpType::kJumpTarget), \ + (ReBcFlag::kLoadsCC)) \ V(Load2CurrentCharsUnchecked, (cp_offset), (ReBcOpType::kOffset), \ (ReBcFlag::kLoadsCC)) \ - V(Load4CurrentChars, (cp_offset, on_failure), \ - (ReBcOpType::kOffset, ReBcOpType::kJumpTarget), (ReBcFlag::kLoadsCC)) \ + V(Load4CurrentChars, (cp_offset, bounds_check_offset, on_failure), \ + (ReBcOpType::kOffset, ReBcOpType::kBoundsCheckOffset, \ + ReBcOpType::kJumpTarget), \ + (ReBcFlag::kLoadsCC)) \ V(Load4CurrentCharsUnchecked, (cp_offset), (ReBcOpType::kOffset), \ (ReBcFlag::kLoadsCC)) \ V(Check4Chars, (characters, on_equal), \ (ReBcOpType::kUint32, ReBcOpType::kJumpTarget), (ReBcFlag::kUsesCC)) \ V(CheckNot4Chars, (characters, on_not_equal), \ @@ -232,67 +241,65 @@ // Format: V(CamelName, (OperandNames...), (OperandTypes...), (Flags...)) #define PEEPHOLE_BYTECODE_LIST(V) \ /* Combination of: */ \ /* LoadCurrentCharacter, CheckBitInTable and AdvanceCpAndGoto */ \ V(SkipUntilBitInTable, \ - (cp_offset, advance_by, table, on_match, on_no_match), \ + (cp_offset, advance_by, table, bounds_check_offset, on_match, \ + on_no_match), \ (ReBcOpType::kOffset, ReBcOpType::kOffset, ReBcOpType::kBitTable, \ + ReBcOpType::kBoundsCheckOffset, ReBcOpType::kJumpTarget, \ + ReBcOpType::kJumpTarget), \ + (ReBcFlag::kLoadsCC)) \ + /* Combination of: */ \ + /* LoadCurrentCharacter, CheckCharacterAfterAnd and AdvanceCpAndGoto */ \ + /* TODO(pthier): mask should be kChar */ \ + V(SkipUntilCharAnd, \ + (cp_offset, advance_by, character, mask, bounds_check_offset, on_match, \ + on_no_match), \ + (ReBcOpType::kOffset, ReBcOpType::kOffset, ReBcOpType::kChar, \ + ReBcOpType::kUint32, ReBcOpType::kBoundsCheckOffset, \ ReBcOpType::kJumpTarget, ReBcOpType::kJumpTarget), \ (ReBcFlag::kLoadsCC)) \ /* Combination of: */ \ - /* CheckPosition, LoadCurrentCharacterUnchecked, CheckCharacterAfterAnd */ \ - /* and AdvanceCpAndGoto */ \ - /* TODO(pthier): mask should be kChar */ \ - /* TODO(pthier): eats_at_least should be Offset */ \ - V(SkipUntilCharAnd, \ - (cp_offset, advance_by, character, mask, eats_at_least, on_match, \ + /* LoadCurrentCharacter, CheckCharacter and AdvanceCpAndGoto */ \ + V(SkipUntilChar, \ + (cp_offset, advance_by, character, bounds_check_offset, on_match, \ on_no_match), \ (ReBcOpType::kOffset, ReBcOpType::kOffset, ReBcOpType::kChar, \ - ReBcOpType::kUint32, ReBcOpType::kOffset, ReBcOpType::kJumpTarget, \ + ReBcOpType::kBoundsCheckOffset, ReBcOpType::kJumpTarget, \ ReBcOpType::kJumpTarget), \ (ReBcFlag::kLoadsCC)) \ /* Combination of: */ \ - /* LoadCurrentCharacter, CheckCharacter and AdvanceCpAndGoto */ \ - V(SkipUntilChar, (cp_offset, advance_by, character, on_match, on_no_match), \ - (ReBcOpType::kOffset, ReBcOpType::kOffset, ReBcOpType::kChar, \ - ReBcOpType::kJumpTarget, ReBcOpType::kJumpTarget), \ - (ReBcFlag::kLoadsCC)) \ - /* Combination of: */ \ - /* CheckPosition, LoadCurrentCharacterUnchecked, CheckCharacter */ \ - /* and AdvanceCpAndGoto */ \ - V(SkipUntilCharPosChecked, \ - (cp_offset, advance_by, character, eats_at_least, on_match, on_no_match), \ - (ReBcOpType::kOffset, ReBcOpType::kOffset, ReBcOpType::kChar, \ - ReBcOpType::kOffset, ReBcOpType::kJumpTarget, ReBcOpType::kJumpTarget), \ - (ReBcFlag::kLoadsCC)) \ - /* TODO(pthier): eats_at_least should be Offset instead of Uint32 */ \ - /* Combination of: */ \ /* LoadCurrentCharacter, CheckCharacter, CheckCharacter and */ \ /* AdvanceCpAndGoto */ \ V(SkipUntilCharOrChar, \ - (cp_offset, advance_by, char1, char2, on_match, on_no_match), \ + (cp_offset, advance_by, char1, char2, bounds_check_offset, on_match, \ + on_no_match), \ (ReBcOpType::kOffset, ReBcOpType::kOffset, ReBcOpType::kChar, \ - ReBcOpType::kChar, ReBcOpType::kJumpTarget, ReBcOpType::kJumpTarget), \ + ReBcOpType::kChar, ReBcOpType::kBoundsCheckOffset, \ + ReBcOpType::kJumpTarget, ReBcOpType::kJumpTarget), \ (ReBcFlag::kLoadsCC)) \ /* Combination of: */ \ /* LoadCurrentCharacter, CheckCharacterGT, CheckBitInTable, GoTo and */ \ /* AdvanceCpAndGoto */ \ V(SkipUntilGtOrNotBitInTable, \ - (cp_offset, advance_by, character, table, on_match, on_no_match), \ + (cp_offset, advance_by, character, table, bounds_check_offset, on_match, \ + on_no_match), \ (ReBcOpType::kOffset, ReBcOpType::kOffset, ReBcOpType::kChar, \ - ReBcOpType::kBitTable, ReBcOpType::kJumpTarget, ReBcOpType::kJumpTarget), \ + ReBcOpType::kBitTable, ReBcOpType::kBoundsCheckOffset, \ + ReBcOpType::kJumpTarget, ReBcOpType::kJumpTarget), \ (ReBcFlag::kLoadsCC)) \ /* Combination of: */ \ /* CheckPosition, Load4CurrentCharsUnchecked, AndCheck4Chars, */ \ /* AdvanceCpAndGoto, AndCheck4Chars, AndCheckNot4Chars */ \ /* This pattern is common for finding a match from an alternative with */ \ /* few different characters. E.g. /[ab]bbbc|[de]eeef/. */ \ V(SkipUntilOneOfMasked, \ (cp_offset, advance_by, both_chars, both_mask, max_offset, chars1, mask1, \ chars2, mask2, on_match1, on_match2, on_failure), \ (ReBcOpType::kOffset, ReBcOpType::kOffset, ReBcOpType::kUint32, \ - ReBcOpType::kUint32, ReBcOpType::kOffset, ReBcOpType::kUint32, \ + ReBcOpType::kUint32, ReBcOpType::kBoundsCheckOffset, ReBcOpType::kUint32, \ ReBcOpType::kUint32, ReBcOpType::kUint32, ReBcOpType::kUint32, \ ReBcOpType::kJumpTarget, ReBcOpType::kJumpTarget, \ ReBcOpType::kJumpTarget), \ (ReBcFlag::kLoadsCC)) \ /* Combination of: */ \ @@ -301,21 +308,22 @@ /* CheckCharacterAfterAnd, CheckCharacterAfterAnd, */ \ /* CheckNotCharacterAfterAnd. */ \ /* This pattern is common for finding a match from an alternative, e.g.: */ \ /* / -#include #include #include #include #include "irregexp/imported/regexp-bytecode-analysis.h" @@ -35,11 +34,10 @@ CodeGenerator::Result CodeGenerator::Assemble(DirectHandle re_data, Flags flags) { USE(isolate_); USE(masm_); -#ifndef COMPILING_IRREGEXP_FOR_EXTERNAL_EMBEDDER // Bytecode analysis is currently unused. In future work it could form the // basis for compiler optimizations. if (V8_UNLIKELY(v8_flags.regexp_bytecode_analysis)) { BytecodeAnalysis analysis(isolate_, &zone_, bytecode_); analysis.Analyze(); @@ -49,11 +47,10 @@ RegExpBytecodeDisassemble(bytecode_->begin(), bytecode_->ulength().value(), pattern_cstring.get(), &analysis); } } -#endif // !COMPILING_IRREGEXP_FOR_EXTERNAL_EMBEDDER PreVisitBytecodes(); iter_.reset(); VisitBytecodes(); if (has_unsupported_bytecode_) return Result::UnsupportedBytecode(); @@ -185,12 +182,12 @@ VISIT(AdvanceCurrentPosition, by) { __ AdvanceCurrentPosition(by); } VISIT(GoTo, label) { __ GoTo(label); } -VISIT(LoadCurrentCharacter, cp_offset, on_failure) { - __ LoadCurrentCharacter(cp_offset, on_failure); +VISIT(LoadCurrentCharacter, cp_offset, bounds_check_offset, on_failure) { + __ LoadCurrentCharacter(cp_offset, on_failure, true, 1, bounds_check_offset); } VISIT(CheckPosition, cp_offset, on_failure) { __ CheckPosition(cp_offset, on_failure); } @@ -322,23 +319,25 @@ VISIT(LoadCurrentCharacterUnchecked, cp_offset) { static constexpr int kChars = 1; __ LoadCurrentCharacterImpl(cp_offset, nullptr, false, kChars, kChars); } -VISIT(Load2CurrentChars, cp_offset, on_failure) { +VISIT(Load2CurrentChars, cp_offset, bounds_check_offset, on_failure) { static constexpr int kChars = 2; - __ LoadCurrentCharacterImpl(cp_offset, on_failure, true, kChars, kChars); + __ LoadCurrentCharacterImpl(cp_offset, on_failure, true, kChars, + bounds_check_offset); } VISIT(Load2CurrentCharsUnchecked, cp_offset) { static constexpr int kChars = 2; __ LoadCurrentCharacterImpl(cp_offset, nullptr, false, kChars, kChars); } -VISIT(Load4CurrentChars, cp_offset, on_failure) { +VISIT(Load4CurrentChars, cp_offset, bounds_check_offset, on_failure) { static constexpr int kChars = 4; - __ LoadCurrentCharacterImpl(cp_offset, on_failure, true, kChars, kChars); + __ LoadCurrentCharacterImpl(cp_offset, on_failure, true, kChars, + bounds_check_offset); } VISIT(Load4CurrentCharsUnchecked, cp_offset) { static constexpr int kChars = 4; __ LoadCurrentCharacterImpl(cp_offset, nullptr, false, kChars, kChars); @@ -389,52 +388,48 @@ __ CheckNotBackReferenceIgnoreCase(start_reg, true, true, on_not_equal); } // Bytecodes generated by peephole optimization. -VISIT(SkipUntilBitInTable, cp_offset, advance_by, table_data, on_match, - on_no_match) { +VISIT(SkipUntilBitInTable, cp_offset, advance_by, table_data, + bounds_check_offset, on_match, on_no_match) { // Nibble table is optionally constructed if we use SIMD. Handle nibble_table; if (masm_->SkipUntilBitInTableUseSimd(advance_by)) { static_assert(RegExpMacroAssembler::kTableSize == 128); nibble_table = isolate_->factory()->NewByteArray( RegExpMacroAssembler::kTableSize / kBitsPerByte, AllocationType::kOld); } Handle table = CreateBitTableByteArray(isolate_, table_data, nibble_table); - __ SkipUntilBitInTable(cp_offset, table, nibble_table, advance_by, on_match, - on_no_match); + __ SkipUntilBitInTable(cp_offset, table, nibble_table, advance_by, + bounds_check_offset, on_match, on_no_match); } -VISIT(SkipUntilCharAnd, cp_offset, advance_by, character, mask, eats_at_least, - on_match, on_no_match) { - __ SkipUntilCharAnd(cp_offset, advance_by, character, mask, eats_at_least, - on_match, on_no_match); +VISIT(SkipUntilCharAnd, cp_offset, advance_by, character, mask, + bounds_check_offset, on_match, on_no_match) { + __ SkipUntilCharAnd(cp_offset, advance_by, character, mask, + bounds_check_offset, on_match, on_no_match); } -VISIT(SkipUntilChar, cp_offset, advance_by, character, on_match, on_no_match) { - __ SkipUntilChar(cp_offset, advance_by, character, on_match, on_no_match); +VISIT(SkipUntilChar, cp_offset, advance_by, character, bounds_check_offset, + on_match, on_no_match) { + __ SkipUntilChar(cp_offset, advance_by, character, bounds_check_offset, + on_match, on_no_match); } -VISIT(SkipUntilCharPosChecked, cp_offset, advance_by, character, eats_at_least, - on_match, on_no_match) { - __ SkipUntilCharPosChecked(cp_offset, advance_by, character, eats_at_least, - on_match, on_no_match); -} - -VISIT(SkipUntilCharOrChar, cp_offset, advance_by, char1, char2, on_match, - on_no_match) { - __ SkipUntilCharOrChar(cp_offset, advance_by, char1, char2, on_match, - on_no_match); +VISIT(SkipUntilCharOrChar, cp_offset, advance_by, char1, char2, + bounds_check_offset, on_match, on_no_match) { + __ SkipUntilCharOrChar(cp_offset, advance_by, char1, char2, + bounds_check_offset, on_match, on_no_match); } VISIT(SkipUntilGtOrNotBitInTable, cp_offset, advance_by, character, table_data, - on_match, on_no_match) { + bounds_check_offset, on_match, on_no_match) { Handle table = CreateBitTableByteArray(isolate_, table_data); __ SkipUntilGtOrNotBitInTable(cp_offset, advance_by, character, table, - on_match, on_no_match); + bounds_check_offset, on_match, on_no_match); } VISIT(SkipUntilOneOfMasked, cp_offset, advance_by, both_chars, both_mask, max_offset, chars1, mask1, chars2, mask2, on_match1, on_match2, on_failure) { @@ -442,34 +437,35 @@ max_offset, chars1, mask1, chars2, mask2, on_match1, on_match2, on_failure); } VISIT(SkipUntilOneOfMasked3, bc0_cp_offset, bc0_advance_by, bc0_table, - bc1_cp_offset, bc1_on_failure, bc2_cp_offset, bc3_characters, bc3_mask, - bc4_by, bc5_cp_offset, bc6_characters, bc6_mask, bc6_on_equal, - bc7_characters, bc7_mask, bc7_on_equal, bc8_characters, bc8_mask, - fallthrough_jump_target) { + bc1_bounds_check_offset, bc1_on_failure, bc1_cp_offset, bc2_characters, + bc2_mask, bc3_by, bc4_bounds_check_offset, bc4_cp_offset, bc5_characters, + bc5_mask, bc5_on_equal, bc6_characters, bc6_mask, bc6_on_equal, + bc7_characters, bc7_mask, fallthrough_jump_target) { RegExpMacroAssembler::SkipUntilOneOfMasked3Args args = { .bc0_cp_offset = bc0_cp_offset, .bc0_advance_by = bc0_advance_by, .bc0_table = {}, .bc0_nibble_table = {}, - .bc1_cp_offset = bc1_cp_offset, + .bc1_bounds_check_offset = bc1_bounds_check_offset, .bc1_on_failure = bc1_on_failure, - .bc2_cp_offset = bc2_cp_offset, - .bc3_characters = bc3_characters, - .bc3_mask = bc3_mask, - .bc4_by = bc4_by, - .bc5_cp_offset = bc5_cp_offset, + .bc1_cp_offset = bc1_cp_offset, + .bc2_characters = bc2_characters, + .bc2_mask = bc2_mask, + .bc3_by = bc3_by, + .bc4_bounds_check_offset = bc4_bounds_check_offset, + .bc4_cp_offset = bc4_cp_offset, + .bc5_characters = bc5_characters, + .bc5_mask = bc5_mask, + .bc5_on_equal = bc5_on_equal, .bc6_characters = bc6_characters, .bc6_mask = bc6_mask, .bc6_on_equal = bc6_on_equal, .bc7_characters = bc7_characters, .bc7_mask = bc7_mask, - .bc7_on_equal = bc7_on_equal, - .bc8_characters = bc8_characters, - .bc8_mask = bc8_mask, .fallthrough_jump_target = fallthrough_jump_target, }; // The nibble table is optionally constructed if we use SIMD. // The fallback version without SIMD for this bytecode emits Index: js/src/irregexp/imported/regexp-compiler-tonode.cc =================================================================== --- js/src/irregexp/imported/regexp-compiler-tonode.cc +++ js/src/irregexp/imported/regexp-compiler-tonode.cc @@ -1472,94 +1472,127 @@ class AssertionSequenceRewriter final { public: // TODO(jgruber): Consider moving this to a separate AST tree rewriter pass // instead of sprinkling rewrites into the AST->Node conversion process. - static void MaybeRewrite(ZoneList* terms, Zone* zone) { - AssertionSequenceRewriter rewriter(terms, zone); + static ZoneList* MaybeRewrite(ZoneList* terms, + Compiler* compiler) { + Zone* zone = compiler->zone(); + Flags flags = compiler->flags(); - static constexpr int kNoIndex = -1; - int from = kNoIndex; + bool has_assertion = false; + bool needs_flattening = false; for (int i = 0; i < terms->length(); i++) { Tree* t = terms->at(i); - if (from == kNoIndex && t->IsAssertion()) { - from = i; // Start a sequence. - } else if (from != kNoIndex && !t->IsAssertion()) { - // Terminate and process the sequence. - if (i - from > 1) rewriter.Rewrite(from, i); - from = kNoIndex; + if (t->IsAssertion()) { + has_assertion = true; + } else if (t->IsAlternative() || + (t->IsGroup() && t->AsGroup()->flags() == flags)) { + needs_flattening = true; } } - if (from != kNoIndex && terms->length() - from > 1) { - rewriter.Rewrite(from, terms->length()); - } - } - - // All assertions are zero width. A consecutive sequence of assertions is - // order-independent. There's two ways we can optimize here: - // 1. fold all identical assertions. - // 2. if any assertion combinations are known to fail (e.g. \b\B), the entire - // sequence fails. - void Rewrite(int from, int to) { - DCHECK_GT(to, from + 1); + if (!has_assertion && !needs_flattening) return terms; - // Bitfield of all seen assertions. + ZoneList* flattened = + zone->New>(terms->length(), zone); uint32_t seen_assertions = 0; - static_assert(static_cast(Assertion::Type::LAST_ASSERTION_TYPE) < - kUInt32Size * kBitsPerByte); + int sequence_start_idx = -1; - for (int i = from; i < to; i++) { - Assertion* t = terms_->at(i)->AsAssertion(); - const uint32_t bit = 1 << static_cast(t->assertion_type()); - - if (seen_assertions & bit) { - // Fold duplicates. - terms_->Set(i, zone_->New()); - } - - seen_assertions |= bit; + for (int i = 0; i < terms->length(); i++) { + AppendFlattenedAndFold(flattened, terms->at(i), flags, zone, + &seen_assertions, &sequence_start_idx); } - // Collapse failures. - const uint32_t always_fails_mask = - 1 << static_cast(Assertion::Type::BOUNDARY) | - 1 << static_cast(Assertion::Type::NON_BOUNDARY); - if ((seen_assertions & always_fails_mask) == always_fails_mask) { - ReplaceSequenceWithFailure(from, to); - } - } - - void ReplaceSequenceWithFailure(int from, int to) { - // Replace the entire sequence with a single node that always fails. - // TODO(jgruber): Consider adding an explicit Fail kind. Until then, the - // negated '*' (everything) range serves the purpose. - ZoneList* ranges = - zone_->New>(0, zone_); - ClassRanges* cc = zone_->New(zone_, ranges); - terms_->Set(from, cc); - - // Zero out the rest. - Empty* empty = zone_->New(); - for (int i = from + 1; i < to; i++) terms_->Set(i, empty); + return flattened; } private: - AssertionSequenceRewriter(ZoneList* terms, Zone* zone) - : zone_(zone), terms_(terms) {} + // Appends a term to the flattened list, unwrapping same-flag Groups and + // Alternatives while folding consecutive assertions in a single pass. + // + // All assertions are zero width, so a consecutive sequence of assertions is + // order-independent. We optimize consecutive assertions by: + // 1. Folding all identical assertions (e.g. \b\b -> \b). Duplicate assertions + // are skipped during list construction without creating Empty AST nodes. + // 2. Collapsing conflicting assertion combinations (e.g. \b\B) into a single + // failure node (ClassRanges). + // + // Note some crucial safety invariants: + // 1. Concatenation is associative, so nested Alternatives are completely + // redundant and can be flattened: A(BC)D -> ABCD. + // 2. Only same-flags groups are flattened (ensuring flag scoping is + // correct). + // 3. Quantified groups, capturing groups, and lookarounds are represented + // by Quantifier, Capture, and Lookaround nodes respectively. Since they + // are not Group nodes, they are naturally shielded from flattening here, + // preserving correct syntax and quantifier bindings. + static void AppendFlattenedAndFold(ZoneList* list, Tree* term, + Flags flags, Zone* zone, + uint32_t* seen_assertions, + int* sequence_start_idx) { + if (term->IsAssertion()) { + Assertion* assertion = term->AsAssertion(); + uint32_t bit = 1 << static_cast(assertion->assertion_type()); + + if (*seen_assertions & bit) { + // Duplicate assertion! Skip adding it to the list. + return; + } + + *seen_assertions |= bit; + + const uint32_t always_fails_mask = + 1 << static_cast(Assertion::Type::BOUNDARY) | + 1 << static_cast(Assertion::Type::NON_BOUNDARY); - Zone* zone_; - ZoneList* terms_; + if ((*seen_assertions & always_fails_mask) == always_fails_mask) { + // Replace the current assertion sequence with a single node that always + // fails. + DCHECK_NE(*sequence_start_idx, -1); + list->Rewind(*sequence_start_idx); + ZoneList* ranges = + zone->New>(0, zone); + list->Add(zone->New(zone, ranges), zone); + return; + } + + if (*sequence_start_idx == -1) { + *sequence_start_idx = list->length(); + } + list->Add(term, zone); + } else if (term->IsAlternative()) { + Alternative* alternative = term->AsAlternative(); + ZoneList* nodes = alternative->nodes(); + for (int i = 0; i < nodes->length(); i++) { + AppendFlattenedAndFold(list, nodes->at(i), flags, zone, seen_assertions, + sequence_start_idx); + } + } else if (term->IsGroup()) { + Group* group = term->AsGroup(); + if (group->flags() == flags) { + AppendFlattenedAndFold(list, group->body(), flags, zone, + seen_assertions, sequence_start_idx); + } else { + *seen_assertions = 0; + *sequence_start_idx = -1; + list->Add(term, zone); + } + } else { + *seen_assertions = 0; + *sequence_start_idx = -1; + list->Add(term, zone); + } + } }; } // namespace Node* Alternative::ToNodeImpl(Compiler* compiler, Node* on_success) { - ZoneList* children = nodes(); - - AssertionSequenceRewriter::MaybeRewrite(children, compiler->zone()); + ZoneList* children = + AssertionSequenceRewriter::MaybeRewrite(nodes(), compiler); TRACE_WITH_NODE("* After assertion sequence rewrite: ", this); Node* current = on_success; if (compiler->read_backward()) { for (int i = 0; i < children->length(); i++) { Index: js/src/irregexp/imported/regexp-compiler.h =================================================================== --- js/src/irregexp/imported/regexp-compiler.h +++ js/src/irregexp/imported/regexp-compiler.h @@ -222,11 +222,17 @@ void SetAll(int map_number) { bitmaps_->at(map_number)->SetAll(); } void SetRest(int from_map) { for (int i = from_map; i < length_; i++) SetAll(i); } - void EmitSkipInstructions(RegExpMacroAssembler* masm); + // Emits a Boyer-Moore skip-scan prelude for the unanchored search, if + // profitable. Returns true iff code that owns the search was emitted: either + // a skip-scan, or an unconditional Fail() when some lookahead position can + // never match. In both cases the caller must not emit a competing scan over + // the same loop. Returns false iff nothing was emitted (PC unchanged) and the + // caller should fall back to another strategy. + bool EmitSkipInstructions(RegExpMacroAssembler* masm); private: // This is the value obtained by EatsAtLeast. If we do not have at least this // many characters left in the sample string then the match is bound to fail. // Therefore it is OK to read a character this far ahead of the current match Index: js/src/irregexp/imported/regexp-compiler.cc =================================================================== --- js/src/irregexp/imported/regexp-compiler.cc +++ js/src/irregexp/imported/regexp-compiler.cc @@ -1703,13 +1703,16 @@ // than go to the next choice. The number of characters preloaded may be // less than the number used for the bounds check. int eats_at_least = predecessor->EatsAtLeast( bounds_check_trace->at_start() == Trace::FALSE_VALUE); DCHECK_GE(eats_at_least, details->characters()); - assembler->LoadCurrentCharacter( - trace->cp_offset(), bounds_check_trace->backtrack(), - !preload_has_checked_bounds, details->characters(), eats_at_least); + int cp_offset = trace->cp_offset(); + int bounds_check_offset = + assembler->CalculateBoundsCheckOffset(cp_offset, eats_at_least); + assembler->LoadCurrentCharacter(cp_offset, bounds_check_trace->backtrack(), + !preload_has_checked_bounds, + details->characters(), bounds_check_offset); } bool need_mask = true; if (details->characters() == 1) { @@ -2971,35 +2974,34 @@ const int skip = max_lookahead + 1 - min_lookahead; return skip; } // See comment above on the implementation of GetSkipTable. -void BoyerMooreLookahead::EmitSkipInstructions(RegExpMacroAssembler* masm) { +bool BoyerMooreLookahead::EmitSkipInstructions(RegExpMacroAssembler* masm) { const int kSize = RegExpMacroAssembler::kTableSize; int min_lookahead = 0; int max_lookahead = 0; if (!FindWorthwhileInterval(&min_lookahead, &max_lookahead)) { TRACE_COMPILER(compiler_, " No worthwhile interval found"); - return; + return false; } // Check if we only have a single non-empty position info, and that info // contains only one or two characters. bool found_single_position = false; constexpr uint32_t kNoChar = 0xffffffff; uint32_t char_one = kNoChar; uint32_t char_two = kNoChar; - bool use_simd = masm->SkipUntilBitInTableUseSimd(1); for (int i = min_lookahead; i <= max_lookahead; i++) { BoyerMoorePositionInfo* map = bitmaps_->at(i); if (map->map_count() == 0) { // If we have a position where no characters can match then we just can't // match. masm->Fail(); - return; + return true; } if (found_single_position || map->map_count() > 2) { // We found a second position or there were more than two characters that // matched at this position. @@ -3033,26 +3035,28 @@ DCHECK_IMPLIES(found_single_position, max_lookahead == min_lookahead); if (found_single_position && max_lookahead < 3) { // The mask-compare can probably handle this better. - return; + return false; } - if (found_single_position && !use_simd) { - // TODO(pthier): Add vectorized version. At that point we will want - // to remove the ' && !use_simd' above. + // TODO(pthier): Remove condition once all architectures that support SIMD for + // SkipUntilBitInTable also support SkipUntilCharAnd (RISCV missing). + const bool use_char_and_simd = + masm->SkipUntilCharAndUseSimd(1) || !masm->SkipUntilBitInTableUseSimd(1); + if (found_single_position && use_char_and_simd) { DCHECK(max_char_ > kSize); // This means we will have to do the 'and'. Label cont; base::uc16 mask = RegExpMacroAssembler::kTableMask; mask &= ~(char_one ^ char_two); // Mask out the bit where they differ. - masm->SkipUntilCharAnd(max_lookahead, 1, char_one & mask, mask, length(), - &cont, &cont); + masm->SkipUntilCharAnd(max_lookahead, 1, char_one & mask, mask, + length() - 1, &cont, &cont); masm->Bind(&cont); - return; + return true; } Factory* factory = masm->isolate()->factory(); Handle boolean_skip_table = factory->NewByteArray(kSize, AllocationType::kOld); @@ -3067,12 +3071,13 @@ GetSkipTable(min_lookahead, max_lookahead, boolean_skip_table, nibble_table); DCHECK_NE(0, skip_distance); Label cont; masm->SkipUntilBitInTable(max_lookahead, boolean_skip_table, nibble_table, - skip_distance, &cont, &cont); + skip_distance, length() - 1, &cont, &cont); masm->Bind(&cont); + return true; } /* Code generation for choice nodes. * * We generate quick checks that do a mask and compare to eliminate a @@ -3234,12 +3239,34 @@ if (choice_count > 1 && text_length != kNodeIsTooComplexForFixedLengthLoops) { trace = EmitFixedLengthLoop(compiler, trace, &alt_gens, &preload, &special_loop_state, text_length, flags); if (trace == nullptr) return EmitResult::Error(); } else { - preload.eats_at_least_ = - EmitOptimizedUnanchoredSearch(compiler, trace, &special_loop_state); + bool bm_scan_emitted = false; + preload.eats_at_least_ = EmitOptimizedUnanchoredSearch( + compiler, trace, &special_loop_state, &bm_scan_emitted); + + // Try the SkipUntilOneOfMasked dispatch and the SkipUntil* search prelude, + // both restricted to the implicit `.*?` LoopChoice and only when BM + // lookahead didn't already emit a competing scan. OneOfMasked is tried + // first: it emits the op dispatching directly to the per-alt bodies and + // owns the whole node (mirroring the peephole), so on a match it returns + // and EmitChoices is skipped. The prelude is a straight-line + // position-finder that falls through to EmitChoices. + if (v8_flags.regexp_simd_in_rc && !bm_scan_emitted && + AsLoopChoiceNode() != nullptr && trace->is_trivial()) { + ActionNode* wrapper = nullptr; + Node* body = MatchLazyStarLoopBody(compiler, &wrapper); + if (body != nullptr) { + if (std::optional result = EmitSkipUntilOneOfMaskedSearch( + compiler, trace, body, wrapper)) { + RETURN_IF_ERROR(*result); + return EmitResult::Success(); + } + EmitSkipUntilSearchPrelude(compiler, trace, body); + } + } RETURN_IF_ERROR( EmitChoices(compiler, &alt_gens, 0, trace, &preload, flags)); } @@ -3278,10 +3305,27 @@ // handled by pushing the current position on the stack and then incrementing // the current position each time around the switch. On backtrack we // decrement the current position and check it against the pushed value. // This avoids pushing backtrack information for each iteration of the loop, // which could take up a lot of space. + // + // Shape (the "loop envelope"); the per-iteration body and the fused consume + // scan (MaybeEmitFixedLengthConsumeScan) plug into the same frame: + // + // PushCurrentPosition() // step-back start marker + // loop_top: + // // one iteration; backtracks to + // // after_body_match_attempt on failure + // after_body_match_attempt: + // + // step_label: + // CheckFixedLengthLoop(backtrack) // at the marker? give up + // AdvanceCurrentPosition(-len) // else step back one iteration ... + // GoTo(after_body_match_attempt) // ... and retry the continuation + // + // Replacing with a single forward scan leaves this frame untouched, so + // backtracking is identical. DCHECK(trace->special_loop_state() == nullptr); macro_assembler->PushCurrentPosition(); // This is the label for trying to match what comes after the greedy // quantifier, either because the body of the quantifier failed, or because // we have stepped back to try again with one iteration fewer. @@ -3289,12 +3333,18 @@ Trace fixed_length_match_trace; if (not_at_start()) fixed_length_match_trace.set_at_start(Trace::FALSE_VALUE); fixed_length_match_trace.set_backtrack(&after_body_match_attempt); fixed_length_loop_state->BindLoopTopLabel(macro_assembler); fixed_length_match_trace.set_special_loop_state(fixed_length_loop_state); - EmitResult result = - alternatives_->at(0).node()->Emit(compiler, &fixed_length_match_trace); + // Fuse a greedy character-class body into a single forward scan; otherwise + // emit the per-iteration body. + EmitResult result = EmitResult::Success(); + if (!MaybeEmitFixedLengthConsumeScan(compiler, &after_body_match_attempt, + text_length)) { + result = + alternatives_->at(0).node()->Emit(compiler, &fixed_length_match_trace); + } macro_assembler->Bind(&after_body_match_attempt); if (result.IsError()) return nullptr; Trace* new_trace = fixed_length_loop_state->backtrack_trace(); @@ -3311,11 +3361,13 @@ macro_assembler->GoTo(&after_body_match_attempt); return new_trace; } int ChoiceNode::EmitOptimizedUnanchoredSearch( - Compiler* compiler, Trace* trace, SpecialLoopState* search_loop_state) { + Compiler* compiler, Trace* trace, SpecialLoopState* search_loop_state, + bool* bm_scan_emitted) { + *bm_scan_emitted = false; int eats_at_least = PreloadState::kEatsAtLeastNotYetInitialized; if (alternatives_->length() != 2) return eats_at_least; GuardedAlternative alt1 = alternatives_->at(1); if (alt1.guards() != nullptr && alt1.guards()->length() != 0) { @@ -3365,15 +3417,580 @@ std::ostream& os = compiler->diagnostics()->os(); os << " "; printer->PrintBoyerMooreLookahead(bm); } #endif - bm->EmitSkipInstructions(macro_assembler); + if (bm->EmitSkipInstructions(macro_assembler)) { + // BM owns the search; do not attempt the inline SkipUntil* scan paths. + *bm_scan_emitted = true; + } } return eats_at_least; } +namespace { + +// EATS_AT_LEAST tags are no-ops that propagate to on_success; skip past any +// run of them. +Node* SkipEatsAtLeastTags(Node* node) { + while (ActionNode* a = node->AsActionNode()) { + if (a->action_type() != ActionNode::EATS_AT_LEAST) break; + node = a->on_success(); + } + return node; +} + +// Walks past EATS_AT_LEAST tags and at most one deferrable ActionNode wrapper +// to expose the first "real" body node. Returns nullptr if the chain has more +// than one non-deferrable action. The wrapper (e.g. STORE_POSITION for the +// implicit capture group 0) is returned in *wrapper_out when non-null, so a +// caller that emits the body directly can defer it into the body's trace; a +// caller that goes through EmitChoices passes nullptr and lets the real node +// graph emit it. +Node* FindBodyNodeUnderOneWrapper(Node* body, ActionNode** wrapper_out) { + if (wrapper_out != nullptr) *wrapper_out = nullptr; + Node* node = SkipEatsAtLeastTags(body); + if (ActionNode* a = node->AsActionNode()) { + if (!a->IsSimpleAction()) return nullptr; + if (wrapper_out != nullptr) *wrapper_out = a; + node = SkipEatsAtLeastTags(a->on_success()); + } + return node; +} + +// Emits `body` against a copy of `base_trace`, deferring `wrapper` (if any) +// into the trace so its action (e.g. the capture-0 STORE_POSITION) runs as part +// of the body's flush. For callers that emit the body directly rather than +// through EmitChoices. +EmitResult EmitBodyMaybeWrapped(Compiler* compiler, Node* body, + const Trace& base_trace, ActionNode* wrapper) { + Trace trace(base_trace); + if (wrapper != nullptr) trace.add_action(wrapper); + return body->Emit(compiler, &trace); +} + +} // namespace + +// Shared structural gate for the two inline SkipUntil* scan strategies. See the +// header for the full contract. +Node* ChoiceNode::MatchLazyStarLoopBody(Compiler* compiler, + ActionNode** wrapper_out) { + // The caller has established that `this` is some LoopChoiceNode with a + // trivial entry trace, but it might just be a `(?:foo)*` quantifier loop. + // We only fire on the implicit `.*?` shape: alt 1 is an omnivorous Text + // node re-entering this loop. Same predicate that + // EmitOptimizedUnanchoredSearch uses. + if (alternatives_->length() != 2) return nullptr; + GuardedAlternative alt1 = alternatives_->at(1); + if (alt1.guards() != nullptr && alt1.guards()->length() != 0) return nullptr; + if (alt1.node()->GetSuccessorOfOmnivorousTextNode(compiler) != this) { + return nullptr; + } + return FindBodyNodeUnderOneWrapper(alternatives_->at(0).node(), wrapper_out); +} + +std::optional ChoiceNode::EmitSkipUntilOneOfMaskedSearch( + Compiler* compiler, Trace* trace, Node* body, ActionNode* wrapper) { + // Matches the implicit unanchored-search sub-graph whose body is a plain + // 2-alternative Choice of TextNodes, and emits a SkipUntilOneOfMasked scan + // that dispatches straight to the two alternative bodies (see the block + // comment on the emission below for the resulting control flow): + // + // clang-format off + // LoopChoiceNode (implicit `.*?` search) + // |- alt 0: ChoiceNode <- `inner`: the A|B we accelerate + // | |- alt 0: TextNode (e.g. [cgt]gggtaaa) + // | `- alt 1: TextNode (e.g. tttaccc[acg]) + // `- alt 1: TextNode (omnivorous advance; re-enters the loop) + // clang-format on + if (!v8_flags.regexp_quick_check) return std::nullopt; + // The op lowerings only handle 1-byte (LATIN1) input. + // TODO(jgruber): Support 2-byte. + if (!compiler->one_byte()) return std::nullopt; + + DCHECK(trace->is_trivial()); + ChoiceNode* inner = body->AsChoiceNode(); + if (inner == nullptr) return std::nullopt; + static constexpr int kNumAlternatives = 2; + if (inner->alternatives()->length() != kNumAlternatives) return std::nullopt; + // Must be a plain ChoiceNode-style alternation. LoopChoiceNode's two alts + // are (body, continue), not parallel patterns; NegativeLookaroundChoice's + // alts encode lookaround semantics. Neither matches the SkipUntilOneOfMasked + // dispatch model. + if (inner->AsLoopChoiceNode() != nullptr) return std::nullopt; + if (inner->AsNegativeLookaroundChoiceNode() != nullptr) return std::nullopt; + + // Both alternatives must be direct TextNodes; this keeps QuickCheck local + // and matches the peephole's target shape. + for (int i = 0; i < kNumAlternatives; i++) { + auto* guards = inner->alternatives()->at(i).guards(); + if (guards != nullptr && guards->length() != 0) return std::nullopt; + TextNode* alt_text = inner->alternatives()->at(i).node()->AsTextNode(); + if (alt_text == nullptr) return std::nullopt; + // The bodies are emitted forward from the candidate with at_start=FALSE + // (see the emit_alt lambda below); the inner alternatives of the implicit + // `.*?` search are always forward-reading. A backward TextNode would break + // both the forward scan geometry and the at_start assumption. + DCHECK(!alt_text->read_backward()); + } + + // The masked op loads a 4-char window at each candidate. + static constexpr int kSkipChars = 4; + constexpr bool kPossiblyAtStart = false; + + // eats_at_least is cached by the analysis pass, so this cheap bail (each + // alternative must supply the kSkipChars-wide load) can run before the QC + // work below. + const int eats_at_least = inner->EatsAtLeast(kPossiblyAtStart); + if (eats_at_least < kSkipChars) return std::nullopt; + const int max_offset = eats_at_least - 1; + + // Per-alt and union QuickCheck details across the first kSkipChars + // characters. Budget=1 is enough since both alts are TextNodes; QC fills + // positions locally without recursing into Choices. + static constexpr int kQCBudget = 1; + // Both alternatives' QC details describe the window starting at the scan + // candidate, so each fills from -- and the union merges at -- window + // position 0. + static constexpr int kQCWindowStart = 0; + QuickCheckDetails alt0_qc(kSkipChars), alt1_qc(kSkipChars); + inner->alternatives()->at(0).node()->GetQuickCheckDetails( + &alt0_qc, compiler, kQCWindowStart, kPossiblyAtStart, kQCBudget); + if (alt0_qc.cannot_match()) return std::nullopt; + inner->alternatives()->at(1).node()->GetQuickCheckDetails( + &alt1_qc, compiler, kQCWindowStart, kPossiblyAtStart, kQCBudget); + if (alt1_qc.cannot_match()) return std::nullopt; + + // The union is the per-alt details merged. Merge mutates both its receiver + // and its argument, so merge into copies to keep alt0_qc/alt1_qc intact for + // the per-alt filter. + QuickCheckDetails union_qc = alt0_qc; + { + QuickCheckDetails tmp = alt1_qc; + union_qc.Merge(&tmp, kQCWindowStart); + } + + if (!alt0_qc.Rationalize(compiler->one_byte())) return std::nullopt; + if (!alt1_qc.Rationalize(compiler->one_byte())) return std::nullopt; + if (!union_qc.Rationalize(compiler->one_byte())) return std::nullopt; + if (alt0_qc.mask() == 0 || alt1_qc.mask() == 0 || union_qc.mask() == 0) { + return std::nullopt; + } + + // Emit the op dispatching directly to the two alternative bodies, mirroring + // the peephole-folded form (which routes on_match1->alt0, on_match2->alt1 + // plus a preserved-tail back-edge, and is ~2% faster on regex-dna than + // routing all exits to one candidate and re-deriving the dispatch in + // EmitChoices). The resulting control flow: + // + // loop: SkipUntilOneOfMasked(...) -> alt0_body / alt1_body / fail + // advance: cp += 1; goto loop + // retry_alt1: reload, re-check alt 1's QC; miss -> advance, else fall + // through to alt1_body (alt 0's backtrack target) + // alt1_body: backtrack -> advance + // alt0_body: backtrack -> retry_alt1 (try alt 1 here) + // fail: Backtrack + // + // The op's scalar dispatcher (which the SIMD path funnels through) loads + // kSkipChars chars, bounds-checks to max_offset, and verifies the per-alt + // mask before reaching on_match*, so each body carries characters_preloaded / + // bound_checked_up_to / quick_check_performed for its alternative. + TRACE("* Emit SkipUntilOneOfMasked dispatch"); + RegExpMacroAssembler* masm = compiler->macro_assembler(); + // The scan reads the candidate at offset 0 and steps forward one char. + constexpr int kScanCpOffset = 0; + constexpr int kScanAdvanceBy = 1; + Label loop, advance, retry_alt1, alt0_body, alt1_body, fail; + + masm->Bind(&loop); + masm->SkipUntilOneOfMasked(kScanCpOffset, kScanAdvanceBy, union_qc.value(), + union_qc.mask(), max_offset, alt0_qc.value(), + alt0_qc.mask(), alt1_qc.value(), alt1_qc.mask(), + &alt0_body, &alt1_body, &fail); + + masm->Bind(&advance); + masm->AdvanceCurrentPosition(kScanAdvanceBy); + masm->GoTo(&loop); + + // alt 0's body failed: try alt 1 at the same position. The op bounds-checked + // the kSkipChars chars, so the reload is unchecked; re-check alt 1's quick- + // check and on a miss advance, otherwise fall into alt 1's body. + masm->Bind(&retry_alt1); + masm->LoadCurrentCharacter(kScanCpOffset, nullptr, /*check_bounds=*/false, + kSkipChars); + masm->CheckNotCharacterAfterAnd(alt1_qc.value(), alt1_qc.mask(), &advance); + + auto emit_alt = [&](Label* entry, int alt_index, QuickCheckDetails* qc, + Label* backtrack) -> EmitResult { + masm->Bind(entry); + Trace base_trace; + base_trace.set_characters_preloaded(kSkipChars); + base_trace.set_bound_checked_up_to(max_offset); + base_trace.set_quick_check_performed(qc); + base_trace.set_at_start(Trace::FALSE_VALUE); + base_trace.set_backtrack(backtrack); + return EmitBodyMaybeWrapped(compiler, + inner->alternatives()->at(alt_index).node(), + base_trace, wrapper); + }; + + // alt 1 is emitted first so it falls through from retry_alt1; alt 0 + // backtracks to retry_alt1 (the priority chain). + RETURN_IF_ERROR(emit_alt(&alt1_body, 1, &alt1_qc, &advance)); + RETURN_IF_ERROR(emit_alt(&alt0_body, 0, &alt0_qc, &retry_alt1)); + + // SkipUntilOneOfMasked exhausted the input. Trace is trivial, so backtrack + // pops the stack and lands on the bottom-of-stack PushBacktrack(fail). + masm->Bind(&fail); + masm->Backtrack(); + + return EmitResult::Success(); +} + +namespace { + +// Builds the SkipUntilBitInTable boolean table (and the SIMD nibble table when +// want_nibble_table) from the leading ClassRanges into *table_out / +// *nibble_table_out. Returns false without allocating if the class is +// unsuitable. 1 = "might match, stop scanning"; 0 = "skip". Chars >= 128 fold +// into (c & 0x7f) with OR; the extra false positives are filtered by the body. +bool BuildBitTableForClassPrefix(ClassRanges* cr, bool one_byte, + bool want_nibble_table, Factory* factory, + Zone* zone, Handle* table_out, + Handle* nibble_table_out) { + // The table indexes by (c & 0x7f) and caps iteration at the one-byte max, so + // it is only correct for 1-byte subjects today. The `one_byte` parameter is + // kept for the planned 2-byte widening (see the TODO at the call site). + DCHECK(one_byte); + // TODO(jgruber): Negated classes (e.g. `[^"]*"`) are common leading classes + // and could be supported by marking the complement set. Mind the mod-128 + // folding order: compute membership over the full [0, 0xff] range first + // (i.e. the chars NOT in the class), then fold into slots with OR. Inverting + // the table after folding is wrong, since a slot may collide a matching and a + // non-matching char. + if (cr->is_negated()) return false; + + ZoneList* ranges = cr->ranges(zone); + CharacterRange::Canonicalize(ranges); + if (one_byte) CharacterRange::ClampToOneByte(ranges); + if (ranges->is_empty()) return false; + + // In 1-byte mode the subject character is at most 0xff, so we only consider + // that far. Ranges are canonicalized (ascending), so if the smallest `from` + // is already past kMaxChar nothing would be set: bail without allocating. + static constexpr base::uc32 kMaxChar = String::kMaxOneByteCharCode; + if (ranges->at(0).from() > kMaxChar) return false; + + // The class is suitable; allocate the table(s) now. + // TODO(jgruber): Dedupe with GetSkipTable. + static constexpr int kMask = RegExpMacroAssembler::kTableMask; + static constexpr int kSize = RegExpMacroAssembler::kTableSize; + static constexpr int kBitsPerByte = 8; + Handle table = factory->NewByteArray(kSize, AllocationType::kOld); + Handle nibble_table; + if (want_nibble_table) { + static_assert(kSize == 128); + nibble_table = + factory->NewByteArray(kSize / kBitsPerByte, AllocationType::kOld); + } + std::memset(table->begin(), 0, table->ulength().value()); + const bool fill_nibble = !nibble_table.is_null(); + if (fill_nibble) { + std::memset(nibble_table->begin(), 0, nibble_table->ulength().value()); + } + + for (auto& r : *ranges) { + const base::uc32 lo = r.from(); + const base::uc32 hi = std::min(r.to(), kMaxChar); + // lo > hi means lo > kMaxChar; ranges are canonicalized ascending, so every + // later range starts even higher. Nothing more can be set. + if (lo > hi) break; + for (base::uc32 c = lo; c <= hi; c++) { + const int idx = c & kMask; + table->set(idx, 1); + if (fill_nibble) { + int lo_nibble = idx & 0x0f; + int hi_nibble = (idx >> 4) & 0x07; + int row = nibble_table->get(lo_nibble); + row |= 1 << hi_nibble; + nibble_table->set(lo_nibble, row); + } + } + } + *table_out = table; + *nibble_table_out = nibble_table; + return true; +} + +// Collects up to two code units <= max_char from the canonical (ascending) +// range list `ranges` into `chars`, in order. Returns the number collected (0, +// 1, or 2), or kCharSetTooLarge if a third such unit exists, in which case no +// single SkipUntil* op covers the set. +constexpr int kCharSetTooLarge = -1; +int CollectSmallCharSet(const ZoneList* ranges, + base::uc32 max_char, base::uc32 chars[2]) { + int count = 0; + for (const CharacterRange& r : *ranges) { + if (r.from() > max_char) break; // canonical ascending: nothing lower left + for (base::uc32 c = r.from(); c <= std::min(r.to(), max_char); c++) { + if (count == 2) return kCharSetTooLarge; + chars[count++] = c; + } + } + return count; +} + +// Emits the cheapest SkipUntil* op that stops the scan at one of `chars` +// (`count` is 1 or 2 code units). Mirrors EmitQuickCheck's masked/unmasked +// selection: a one-bit-apart pair (e.g. the case-insensitive {a, A}) folds into +// a single masked compare, as in ShortCutEmitCharacterPair. Both exits target +// the same labels; search preludes pass on_match == on_no_match and re-check +// the body there, consume loops pass the loop exit for both. +void EmitSkipUntilSmallCharSet(RegExpMacroAssembler* masm, bool one_byte, + int cp_offset, int advance_by, + const base::uc32* chars, int count, + int bounds_check_offset, Label* on_match, + Label* on_no_match) { + DCHECK(count == 1 || count == 2); + if (count == 1) { + masm->SkipUntilChar(cp_offset, advance_by, chars[0], bounds_check_offset, + on_match, on_no_match); + return; + } + const base::uc32 exor = chars[0] ^ chars[1]; + if ((exor & (exor - 1)) == 0) { + const uint32_t mask = CharMask(one_byte) ^ exor; + masm->SkipUntilCharAnd(cp_offset, advance_by, chars[0], mask, + bounds_check_offset, on_match, on_no_match); + } else { + masm->SkipUntilCharOrChar(cp_offset, advance_by, chars[0], chars[1], + bounds_check_offset, on_match, on_no_match); + } +} + +} // namespace + +void ChoiceNode::EmitSkipUntilSearchPrelude(Compiler* compiler, Trace* trace, + Node* body) { + // Matches the implicit unanchored-search sub-graph and accelerates the scan + // to the first position where the body can match. The op is only a + // position-finder: EmitChoices emits the body at the candidate and re-checks + // it, so an over-approximate stop set is fine. + // + // clang-format off + // LoopChoiceNode (implicit `.*?` search) + // |- alt 0: body <- what we accelerate + // `- alt 1: TextNode (omnivorous advance; re-enters the loop) + // clang-format on + // + // Three strategies, cheapest first: + // (1) an exact small positive leading class [c] / [cd] -> SkipUntilChar / + // SkipUntilCharOrChar / SkipUntilCharAnd, + // (2) otherwise the body's 1-char quick check -> SkipUntilChar / CharAnd + // (covers leading atoms and sub-loops, e.g. /a+/), + // (3) otherwise a leading-class bit table -> SkipUntilBitInTable (1-byte). + // (1) and (2) are valid for 2-byte input; (3) is 1-byte only. Gated on the + // same flag as the sibling scan strategies it competes with so that + // --no-regexp-quick-check disables all of them uniformly when debugging. + if (!v8_flags.regexp_quick_check) return; + DCHECK(trace->is_trivial()); + + RegExpMacroAssembler* masm = compiler->macro_assembler(); + const bool one_byte = compiler->one_byte(); + // The scan reads the candidate at offset 0 and steps forward one char; only + // the candidate position must be in bounds. + constexpr int kScanCpOffset = 0; + constexpr int kScanAdvanceBy = 1; + constexpr int kBoundsCheckOffset = 0; + const base::uc32 max_char = MaxCodeUnit(one_byte); + Label cont; + + // (1) Exact small positive leading class. SkipUntilChar / CharOrChar / + // CharAnd are valid for 2-byte input: the scalar lowerings go through the + // width-aware LoadCurrentCharacter, and the SIMD lowerings compare + // char_size()-wide lanes. + TextNode* body_text = body->AsTextNode(); + const bool class_led = + body_text != nullptr && !body_text->read_backward() && + !body_text->elements()->is_empty() && + body_text->elements()->at(0).text_type() == TextElement::CLASS_RANGES; + if (class_led) { + ClassRanges* cr = body_text->elements()->at(0).class_ranges(); + if (!cr->is_negated()) { + // Work on a copy; Canonicalize mutates, and the ranges are shared with + // the AST. + ZoneList* ranges = + zone()->New>(2, zone()); + ranges->AddAll(*cr->ranges(zone()), zone()); + CharacterRange::Canonicalize(ranges); + base::uc32 chars[2]; + int count = CollectSmallCharSet(ranges, max_char, chars); + if (count >= 1) { + TRACE("* Emit SkipUntil* search prelude (exact leading class)"); + EmitSkipUntilSmallCharSet(masm, one_byte, kScanCpOffset, kScanAdvanceBy, + chars, count, kBoundsCheckOffset, &cont, + &cont); + masm->Bind(&cont); + return; + } + } + } + + // (2) General: the body's 1-char quick check, mirroring EmitQuickCheck's + // masked/unmasked selection. Covers leading atoms and sub-loops (e.g. /a+/). + // The quick check is only a *necessary* condition for a match when the body + // must consume at least one character. If the body can match the empty string + // (e.g. /|z/), some match positions impose no first-char constraint, so a + // skip would wrongly jump over them; require EatsAtLeast >= 1. Within that, + // the check never skips a real match; false positives are filtered by the + // body re-check at `cont`. + constexpr bool kPossiblyAtStart = false; + if (body->EatsAtLeast(kPossiblyAtStart) >= 1) { + QuickCheckDetails qc(1); + body->GetQuickCheckDetails(&qc, compiler, 0, kPossiblyAtStart, + kRecursionBudget); + if (!qc.cannot_match() && qc.Rationalize(one_byte)) { + const uint32_t char_mask = CharMask(one_byte); + const uint32_t mask = qc.mask() & char_mask; + const uint32_t value = qc.value() & char_mask; + if (mask != 0) { + TRACE("* Emit SkipUntil* search prelude (body quick check)"); + if (mask == char_mask) { + masm->SkipUntilChar(kScanCpOffset, kScanAdvanceBy, value, + kBoundsCheckOffset, &cont, &cont); + } else { + masm->SkipUntilCharAnd(kScanCpOffset, kScanAdvanceBy, value, mask, + kBoundsCheckOffset, &cont, &cont); + } + masm->Bind(&cont); + return; + } + } + } else { + TRACE("* SkipUntil* search: body may match empty; no quick-check prelude"); + } + + // (3) Fall back to a leading-class bit table. The 128-entry table is 1-byte + // only. + // TODO(jgruber): Support 2-byte (the (c & 0x7f) table stays valid; needs + // 2-byte loads in the lowerings). + if (!one_byte || !class_led) { + TRACE("* No SkipUntil* search prelude emitted"); + return; + } + ClassRanges* cr = body_text->elements()->at(0).class_ranges(); + Handle table, nibble_table; + if (!BuildBitTableForClassPrefix( + cr, one_byte, masm->SkipUntilBitInTableUseSimd(kScanAdvanceBy), + masm->isolate()->factory(), zone(), &table, &nibble_table)) { + return; + } + + // Emit the scan as a straight-line prelude, mirroring + // EmitOptimizedUnanchoredSearch: it advances to the next position whose first + // character is in the class and falls through to `cont`. The caller emits the + // body there via EmitChoices, and the loop's continue-alternative provides + // advance + rescan. At end-of-input the scan also falls through to `cont`, + // where the body fails its bounds check and the loop terminates. + TRACE("* Emit SkipUntilBitInTable scan prelude"); + masm->SkipUntilBitInTable(kScanCpOffset, table, nibble_table, kScanAdvanceBy, + kBoundsCheckOffset, &cont, &cont); + masm->Bind(&cont); +} + +bool ChoiceNode::MaybeEmitFixedLengthConsumeScan(Compiler* compiler, + Label* exit, int text_length) { + // Called from EmitFixedLengthLoop. If the greedy loop body is a character + // class [B], replace the per-iteration body + back-edge with a single forward + // scan over its exit set (the complement of B: the chars that end the loop), + // landing on |exit|; return true. The op follows the exit-set shape via + // EmitSkipUntilSmallCharSet (SkipUntilChar / SkipUntilCharAnd / + // SkipUntilCharOrChar). A larger exit set falls through to the generic loop. + // + // The caller keeps the fixed-length-loop frame (pushed start + + // CheckFixedLengthLoop step-back), so this replaces only the forward scan: no + // atomicity precondition on the continuation is needed. + if (!v8_flags.regexp_simd_in_rc) return false; + // The scan compares single code units and advances by one. Only a two-byte + // subject under /u or /v is unsafe: there a class can match a supplementary + // code point (a surrogate pair), so the loop's one-code-unit step-back could + // land mid-pair. One-byte subjects (latin1, all BMP) and non-unicode two-byte + // subjects (each code unit independent) are both fine. + if (!compiler->one_byte() && IsEitherUnicode(compiler->flags())) { + TRACE("* No consume scan (two-byte unicode: surrogate step-back unsafe)"); + return false; + } + + LoopChoiceNode* loop = AsLoopChoiceNode(); + if (loop == nullptr || loop->read_backward()) return false; + + // The body is alternative 0 (alternative 1 is the continuation). Counted + // quantifiers carry guards that need per-iteration bookkeeping. + GuardedAlternative body_alt = alternatives_->at(0); + if (body_alt.guards() != nullptr && body_alt.guards()->length() != 0) { + return false; + } + // Body must be a single character class, so "matches the body" is "in the + // class" and the loop ends on its complement. + TextNode* body = body_alt.node()->AsTextNode(); + if (body == nullptr || body->read_backward()) return false; + if (body->elements()->length() != 1) return false; + TextElement el = body->elements()->at(0); + if (el.text_type() != TextElement::CLASS_RANGES) return false; + ClassRanges* cr = el.class_ranges(); + + // The loop ends on the first char the body does not match, so the exit set is + // the complement of [B]'s match set. A class stores positive ranges plus a + // negate flag, so the complement is: + // [c...] (positive): body matches the ranges -> exit = Negate(ranges) + // [^c...] (negated): body matches the rest -> exit = ranges + // Work on a copy; Canonicalize/Negate mutate, and the ranges are shared with + // the AST. + ZoneList* exit_set = + zone()->New>(2, zone()); + exit_set->AddAll(*cr->ranges(zone()), zone()); + CharacterRange::Canonicalize(exit_set); + if (!cr->is_negated()) { + ZoneList* negated = + zone()->New>(2, zone()); + CharacterRange::Negate(exit_set, negated, zone()); + exit_set = negated; + } + + // Collect up to two exit chars within the subject's code-unit range; a larger + // set has no single-op scan. Clamping to the code-unit max here subsumes a + // ClampToOneByte step and covers 2-byte subjects. + const base::uc32 max_char = MaxCodeUnit(compiler->one_byte()); + base::uc32 chars[2]; + int count = CollectSmallCharSet(exit_set, max_char, chars); + if (count == kCharSetTooLarge) { + TRACE("* No consume scan (exit set larger than two chars)"); + return false; + } + if (count == 0) return false; // Body matches everything: never stops. + + // The body is a single character class, so the loop consumes exactly one code + // unit per iteration; the scan and the caller's step-back both stride by one. + DCHECK_EQ(text_length, 1); + USE(text_length); + + // The scan reads the current position and steps forward one code unit; only + // that position must be in bounds. + constexpr int kScanCpOffset = 0; + constexpr int kScanAdvanceBy = 1; + constexpr int kBoundsCheckOffset = 0; + // Both exits target |exit|: on a stop char the position is left there for the + // continuation, at end-of-input at the greedy maximum; the caller's step-back + // walks back from either. See the SkipUntil* position contract in + // regexp-macro-assembler.h. + TRACE("* Emit SkipUntil* consume scan"); + EmitSkipUntilSmallCharSet(compiler->macro_assembler(), compiler->one_byte(), + kScanCpOffset, kScanAdvanceBy, chars, count, + kBoundsCheckOffset, exit, exit); + return true; +} + EmitResult ChoiceNode::EmitChoices(Compiler* compiler, AlternativeGenerationList* alt_gens, int first_choice, Trace* trace, PreloadState* preload, Flags flags) { TRACE("* Emit Choices"); Index: js/src/irregexp/imported/regexp-graph-printer.cc =================================================================== --- js/src/irregexp/imported/regexp-graph-printer.cc +++ js/src/irregexp/imported/regexp-graph-printer.cc @@ -5,12 +5,10 @@ #ifdef V8_ENABLE_REGEXP_DIAGNOSTICS #include "irregexp/imported/regexp-graph-printer.h" #include -#include -#include #include "irregexp/imported/regexp-ast-printer.h" #include "irregexp/imported/regexp-compiler.h" #include "irregexp/imported/regexp-node-printer.h" Index: js/src/irregexp/imported/regexp-interpreter.cc =================================================================== --- js/src/irregexp/imported/regexp-interpreter.cc +++ js/src/irregexp/imported/regexp-interpreter.cc @@ -203,11 +203,11 @@ IrregexpInterpreter::Result ThrowStackOverflow(Isolate* isolate, RegExp::CallOrigin call_origin) { CHECK(call_origin == RegExp::CallOrigin::kFromRuntime); // We abort interpreter execution after the stack overflow is thrown, and thus // allow allocation here despite the outer DisallowGarbageCollectionScope. - [[maybe_unused]] AllowGarbageCollection yes_gc; + AllowGarbageCollection yes_gc; isolate->StackOverflow(); return IrregexpInterpreter::EXCEPTION; } // Only throws if called from the runtime, otherwise just returns the EXCEPTION @@ -280,11 +280,11 @@ } else if (check.InterruptRequested()) { const bool was_one_byte = String::IsOneByteRepresentationUnderneath(*subject_string_out); Tagged result; { - [[maybe_unused]] AllowGarbageCollection yes_gc; + AllowGarbageCollection yes_gc; result = isolate->stack_guard()->HandleInterrupts(); } if (IsExceptionHole(result)) { return IrregexpInterpreter::EXCEPTION; } @@ -679,50 +679,47 @@ } else { ADVANCE(); } DISPATCH(); } - BYTECODE(LoadCurrentCharacter, cp_offset, on_failure) { - int pos = current + cp_offset; - if (pos >= subject.length() || pos < 0) { + BYTECODE(LoadCurrentCharacter, cp_offset, bounds_check_offset, on_failure) { + if (!IndexIsInBounds(current + bounds_check_offset, subject.length())) { SET_PC_FROM_OFFSET(on_failure); } else { ADVANCE(); - current_char = subject[pos]; + current_char = subject[current + cp_offset]; } DISPATCH(); } BYTECODE(LoadCurrentCharacterUnchecked, cp_offset) { ADVANCE(); int pos = current + cp_offset; current_char = subject[pos]; DISPATCH(); } - BYTECODE(Load2CurrentChars, cp_offset, on_failure) { - int pos = current + cp_offset; - if (pos + 2 > subject.length() || pos < 0) { + BYTECODE(Load2CurrentChars, cp_offset, bounds_check_offset, on_failure) { + if (!IndexIsInBounds(current + bounds_check_offset, subject.length())) { SET_PC_FROM_OFFSET(on_failure); } else { ADVANCE(); - current_char = Load2Characters(subject, pos); + current_char = Load2Characters(subject, current + cp_offset); } DISPATCH(); } BYTECODE(Load2CurrentCharsUnchecked, cp_offset) { ADVANCE(); int pos = current + cp_offset; current_char = Load2Characters(subject, pos); DISPATCH(); } - BYTECODE(Load4CurrentChars, cp_offset, on_failure) { + BYTECODE(Load4CurrentChars, cp_offset, bounds_check_offset, on_failure) { DCHECK_EQ(1, sizeof(Char)); - int pos = current + cp_offset; - if (pos + 4 > subject.length() || pos < 0) { + if (!IndexIsInBounds(current + bounds_check_offset, subject.length())) { SET_PC_FROM_OFFSET(on_failure); } else { ADVANCE(); - current_char = Load4Characters(subject, pos); + current_char = Load4Characters(subject, current + cp_offset); } DISPATCH(); } BYTECODE(Load4CurrentCharsUnchecked, cp_offset) { ADVANCE(); @@ -996,13 +993,13 @@ } else { SET_PC_FROM_OFFSET(on_no_match); } DISPATCH(); } - BYTECODE(SkipUntilChar, cp_offset, advance_by, character, on_match, - on_no_match) { - while (IndexIsInBounds(current + cp_offset, subject.length())) { + BYTECODE(SkipUntilChar, cp_offset, advance_by, character, + bounds_check_offset, on_match, on_no_match) { + while (IndexIsInBounds(current + bounds_check_offset, subject.length())) { current_char = subject[current + cp_offset]; if (character == current_char) { SET_PC_FROM_OFFSET(on_match); DISPATCH(); } @@ -1010,38 +1007,25 @@ } SET_PC_FROM_OFFSET(on_no_match); DISPATCH(); } BYTECODE(SkipUntilCharAnd, cp_offset, advance_by, character, mask, - eats_at_least, on_match, on_no_match) { - while (IndexIsInBounds(current + eats_at_least, subject.length())) { + bounds_check_offset, on_match, on_no_match) { + while (IndexIsInBounds(current + bounds_check_offset, subject.length())) { current_char = subject[current + cp_offset]; if (character == (current_char & mask)) { SET_PC_FROM_OFFSET(on_match); DISPATCH(); } ADVANCE_CURRENT_POSITION(advance_by); } SET_PC_FROM_OFFSET(on_no_match); DISPATCH(); } - BYTECODE(SkipUntilCharPosChecked, cp_offset, advance_by, character, - eats_at_least, on_match, on_no_match) { - while (IndexIsInBounds(current + eats_at_least, subject.length())) { - current_char = subject[current + cp_offset]; - if (character == current_char) { - SET_PC_FROM_OFFSET(on_match); - DISPATCH(); - } - ADVANCE_CURRENT_POSITION(advance_by); - } - SET_PC_FROM_OFFSET(on_no_match); - DISPATCH(); - } - BYTECODE(SkipUntilBitInTable, cp_offset, advance_by, table, on_match, - on_no_match) { - while (IndexIsInBounds(current + cp_offset, subject.length())) { + BYTECODE(SkipUntilBitInTable, cp_offset, advance_by, table, + bounds_check_offset, on_match, on_no_match) { + while (IndexIsInBounds(current + bounds_check_offset, subject.length())) { current_char = subject[current + cp_offset]; if (CheckBitInTable(current_char, table)) { SET_PC_FROM_OFFSET(on_match); DISPATCH(); } @@ -1049,12 +1033,12 @@ } SET_PC_FROM_OFFSET(on_no_match); DISPATCH(); } BYTECODE(SkipUntilGtOrNotBitInTable, cp_offset, advance_by, character, - table, on_match, on_no_match) { - while (IndexIsInBounds(current + cp_offset, subject.length())) { + table, bounds_check_offset, on_match, on_no_match) { + while (IndexIsInBounds(current + bounds_check_offset, subject.length())) { current_char = subject[current + cp_offset]; if (current_char > character) { SET_PC_FROM_OFFSET(on_match); DISPATCH(); } @@ -1065,13 +1049,13 @@ ADVANCE_CURRENT_POSITION(advance_by); } SET_PC_FROM_OFFSET(on_no_match); DISPATCH(); } - BYTECODE(SkipUntilCharOrChar, cp_offset, advance_by, char1, char2, on_match, - on_no_match) { - while (IndexIsInBounds(current + cp_offset, subject.length())) { + BYTECODE(SkipUntilCharOrChar, cp_offset, advance_by, char1, char2, + bounds_check_offset, on_match, on_no_match) { + while (IndexIsInBounds(current + bounds_check_offset, subject.length())) { current_char = subject[current + cp_offset]; // The two if-statements below are split up intentionally, as combining // them seems to result in register allocation behaving quite // differently and slowing down the resulting code. if (char1 == current_char) { @@ -1089,11 +1073,11 @@ } BYTECODE(SkipUntilOneOfMasked, cp_offset, advance_by, both_chars, both_mask, max_offset, chars1, mask1, chars2, mask2, on_match1, on_match2, on_failure) { DCHECK_GE(cp_offset, 0); - DCHECK_GE(max_offset, cp_offset); + DCHECK_GE(max_offset, cp_offset + 3); // We should only get here in 1-byte mode. DCHECK_EQ(1, sizeof(Char)); while (IndexIsInBounds(current + max_offset, subject.length())) { int pos = current + cp_offset; current_char = Load4Characters(subject, pos); @@ -1111,75 +1095,77 @@ } SET_PC_FROM_OFFSET(on_failure); DISPATCH(); } BYTECODE(SkipUntilOneOfMasked3, bc0_cp_offset, bc0_advance_by, bc0_table, - bc1_cp_offset, bc1_on_failure, bc2_cp_offset, bc3_characters, - bc3_mask, bc4_by, bc5_cp_offset, bc6_characters, bc6_mask, - bc6_on_equal, bc7_characters, bc7_mask, bc7_on_equal, - bc8_characters, bc8_mask, fallthrough_jump_target) { + bc1_bounds_check_offset, bc1_on_failure, bc1_cp_offset, + bc2_characters, bc2_mask, bc3_by, bc4_bounds_check_offset, + bc4_cp_offset, bc5_characters, bc5_mask, bc5_on_equal, + bc6_characters, bc6_mask, bc6_on_equal, bc7_characters, bc7_mask, + fallthrough_jump_target) { // We should only get here in 1-byte mode. DCHECK_EQ(1, sizeof(Char)); while (true) { - // bcO: kSkipUntilBitInTable + // bc0: kSkipUntilBitInTable // on_match and on_no_match are constrained to jump to bc1. while (IndexIsInBounds(current + bc0_cp_offset, subject.length())) { current_char = subject[current + bc0_cp_offset]; if (CheckBitInTable(current_char, bc0_table)) { break; } ADVANCE_CURRENT_POSITION(bc0_advance_by); } - // bc1: kCheckPosition - if (!IndexIsInBounds(current + bc1_cp_offset, subject.length())) { + // bc1: kLoad4CurrentChars + if (!IndexIsInBounds(current + bc1_bounds_check_offset, + subject.length())) { SET_PC_FROM_OFFSET(bc1_on_failure); DISPATCH(); } - // bc2: Load4CurrentCharsUnchecked - int pos = current + bc2_cp_offset; + // Load 4 characters + int pos = current + bc1_cp_offset; current_char = Load4Characters(subject, pos); - // bc3: AndCheck4Chars - // on_equal is constrained to jump to bc5. - if (bc3_characters == (current_char & bc3_mask)) { - // bc5: Load4CurrentChars - // on_failure is constrained to jump to bc4. - DCHECK_GE(bc5_cp_offset, 0); - if (current + bc5_cp_offset + 4 > subject.length()) { - // bc4: AdvanceCpAndGoto + // bc2: AndCheck4Chars + // on_equal is constrained to jump to bc4. + if (bc2_characters == (current_char & bc2_mask)) { + // bc4: Load4CurrentChars + // on_failure is constrained to jump to bc3 (AdvanceCpAndGoto). + if (!IndexIsInBounds(current + bc4_bounds_check_offset, + subject.length())) { + // bc3: AdvanceCpAndGoto // on_goto is constrained to jump back to bc0. - ADVANCE_CURRENT_POSITION(bc4_by); + ADVANCE_CURRENT_POSITION(bc3_by); continue; } // TODO(jgruber): Usually we can reuse some of the bytes loaded above. - pos = current + bc5_cp_offset; + pos = current + bc4_cp_offset; current_char = Load4Characters(subject, pos); + // bc5: AndCheck4Chars + if (bc5_characters == (current_char & bc5_mask)) { + SET_PC_FROM_OFFSET(bc5_on_equal); + DISPATCH(); + } // bc6: AndCheck4Chars if (bc6_characters == (current_char & bc6_mask)) { SET_PC_FROM_OFFSET(bc6_on_equal); DISPATCH(); } - // bc7: AndCheck4Chars + // bc7: AndCheckNot4Chars + // on_not_equal is constrained to jump to bc3. if (bc7_characters == (current_char & bc7_mask)) { - SET_PC_FROM_OFFSET(bc7_on_equal); - DISPATCH(); - } - // bc8: AndCheckNot4Chars - // on_not_equal is constrained to jump to bc4. - if (bc8_characters == (current_char & bc8_mask)) { SET_PC_FROM_OFFSET(fallthrough_jump_target); DISPATCH(); } } - // bc4: AdvanceCpAndGoto + // bc3: AdvanceCpAndGoto // on_goto is constrained to jump back to bc0. - ADVANCE_CURRENT_POSITION(bc4_by); + ADVANCE_CURRENT_POSITION(bc3_by); } UNREACHABLE(); } BYTECODES_END() Index: js/src/irregexp/imported/regexp-macro-assembler-tracer.h =================================================================== --- js/src/irregexp/imported/regexp-macro-assembler-tracer.h +++ js/src/irregexp/imported/regexp-macro-assembler-tracer.h @@ -14,11 +14,11 @@ // Decorator on a RegExpMacroAssembler that write all calls. class RegExpMacroAssemblerTracer : public RegExpMacroAssembler { public: explicit RegExpMacroAssemblerTracer( - js::UniquePtr&& assembler); + std::unique_ptr&& assembler); ~RegExpMacroAssemblerTracer() override; void AbortedCodeGeneration() override; void AdvanceCurrentPosition(int by) override; // Signed cp change. void AdvanceRegister(int reg, int by) override; // r[reg] += by. void Backtrack() override; @@ -53,27 +53,30 @@ Label* on_not_in_range) override; void CheckBitInTable(Handle table, Label* on_bit_set) override; bool SkipUntilBitInTableUseSimd(int advance_by) override { return assembler_->SkipUntilBitInTableUseSimd(advance_by); } + bool SkipUntilCharAndUseSimd(int advance_by) override { + return assembler_->SkipUntilCharAndUseSimd(advance_by); + } void SkipUntilBitInTable(int cp_offset, Handle table, Handle nibble_table, int advance_by, - Label* on_match, Label* on_no_match) override; + int bounds_check_offset, Label* on_match, + Label* on_no_match) override; void SkipUntilCharAnd(int cp_offset, int advance_by, unsigned character, - unsigned mask, int eats_at_least, Label* on_match, + unsigned mask, int bounds_check_offset, Label* on_match, Label* on_no_match) override; void SkipUntilChar(int cp_offset, int advance_by, unsigned character, - Label* on_match, Label* on_no_match) override; - void SkipUntilCharPosChecked(int cp_offset, int advance_by, - unsigned character, int eats_at_least, - Label* on_match, Label* on_no_match) override; + int bounds_check_offset, Label* on_match, + Label* on_no_match) override; void SkipUntilCharOrChar(int cp_offset, int advance_by, unsigned char1, - unsigned char2, Label* on_match, - Label* on_no_match) override; + unsigned char2, int bounds_check_offset, + Label* on_match, Label* on_no_match) override; void SkipUntilGtOrNotBitInTable(int cp_offset, int advance_by, unsigned character, Handle table, - Label* on_match, Label* on_no_match) override; + int bounds_check_offset, Label* on_match, + Label* on_no_match) override; void SkipUntilOneOfMasked(int cp_offset, int advance_by, unsigned both_chars, unsigned both_mask, int max_offset, unsigned chars1, unsigned mask1, unsigned chars2, unsigned mask2, Label* on_match1, Label* on_match2, Label* on_failure) override; @@ -93,11 +96,11 @@ void IfRegisterLT(int reg, int comparand, Label* if_lt) override; void IfRegisterEqPos(int reg, Label* if_eq) override; IrregexpImplementation Implementation() override; void LoadCurrentCharacterImpl(int cp_offset, Label* on_end_of_input, bool check_bounds, int characters, - int eats_at_least) override; + int bounds_check_offset) override; void PopCurrentPosition() override; void PopRegister(int register_index) override; void PushBacktrack(Label* label) override; void PushCurrentPosition() override; void PushRegister(int register_index, @@ -119,11 +122,11 @@ void set_global_mode(GlobalMode mode) override; void set_backtrack_limit(uint32_t backtrack_limit) override; void set_can_fallback(bool val) override; private: - js::UniquePtr assembler_; + std::unique_ptr assembler_; }; } // namespace regexp } // namespace internal } // namespace v8 Index: js/src/irregexp/imported/regexp-macro-assembler-tracer.cc =================================================================== --- js/src/irregexp/imported/regexp-macro-assembler-tracer.cc +++ js/src/irregexp/imported/regexp-macro-assembler-tracer.cc @@ -10,11 +10,11 @@ namespace v8 { namespace internal { namespace regexp { RegExpMacroAssemblerTracer::RegExpMacroAssemblerTracer( - js::UniquePtr&& assembler) + std::unique_ptr&& assembler) : RegExpMacroAssembler(*assembler), assembler_(std::move(assembler)) { PrintF("RegExpMacroAssembler%s();\n", ImplementationToString(assembler_->Implementation())); } @@ -144,19 +144,19 @@ assembler_->ReadStackPointerFromRegister(reg); } void RegExpMacroAssemblerTracer::LoadCurrentCharacterImpl( int cp_offset, Label* on_end_of_input, bool check_bounds, int characters, - int eats_at_least) { + int bounds_check_offset) { const char* check_msg = check_bounds ? "" : " (unchecked)"; PrintF( - " LoadCurrentCharacter(cp_offset=%d, label[%08x]%s (%d chars) (eats at " - "least %d));\n", + " LoadCurrentCharacter(cp_offset=%d, label[%08x]%s (%d chars) (bounds " + "check offset %d));\n", cp_offset, LabelToInt(on_end_of_input), check_msg, characters, - eats_at_least); + bounds_check_offset); assembler_->LoadCurrentCharacter(cp_offset, on_end_of_input, check_bounds, - characters, eats_at_least); + characters, bounds_check_offset); } namespace { class PrintablePrinter { @@ -382,98 +382,89 @@ } // namespace void RegExpMacroAssemblerTracer::SkipUntilBitInTable( int cp_offset, Handle table, Handle nibble_table, - int advance_by, Label* on_match, Label* on_no_match) { + int advance_by, int bounds_check_offset, Label* on_match, + Label* on_no_match) { PrintF( - "SkipUntilBitInTable(cp_offset=%d, advance_by=%d, on_match=label[%08x], " + "SkipUntilBitInTable(cp_offset=%d, advance_by=%d, " + "bounds_check_offset=%d, on_match=label[%08x], " "on_no_match=label[%08x]\n ", - cp_offset, advance_by, LabelToInt(on_match), LabelToInt(on_no_match)); + cp_offset, advance_by, bounds_check_offset, LabelToInt(on_match), + LabelToInt(on_no_match)); PrintTables(table, nibble_table); PrintF(");\n"); assembler_->SkipUntilBitInTable(cp_offset, table, nibble_table, advance_by, - on_match, on_no_match); + bounds_check_offset, on_match, on_no_match); } void RegExpMacroAssemblerTracer::SkipUntilCharAnd( int cp_offset, int advance_by, unsigned character, unsigned mask, - int eats_at_least, Label* on_match, Label* on_no_match) { + int bounds_check_offset, Label* on_match, Label* on_no_match) { PrintablePrinter printable(character); PrintF( "SkipUntilCharAnd(cp_offset=%d, advance_by=%d, character=0x%04x%s, " - "mask=0x%04x, eats_at_least=%d, on_match=label[%08x], " + "mask=0x%04x, bounds_check_offset=%d, on_match=label[%08x], " "on_no_match=label[%08x]\n", - cp_offset, advance_by, character, *printable, mask, eats_at_least, + cp_offset, advance_by, character, *printable, mask, bounds_check_offset, LabelToInt(on_match), LabelToInt(on_no_match)); assembler_->SkipUntilCharAnd(cp_offset, advance_by, character, mask, - eats_at_least, on_match, on_no_match); + bounds_check_offset, on_match, on_no_match); } void RegExpMacroAssemblerTracer::SkipUntilChar(int cp_offset, int advance_by, unsigned character, + int bounds_check_offset, Label* on_match, Label* on_no_match) { PrintablePrinter printable(character); PrintF( "SkipUntilChar(cp_offset=%d, advance_by=%d, character=0x%04x%s, " - "on_match=label[%08x], " + "bounds_check_offset=%d, on_match=label[%08x], " "on_no_match=label[%08x]\n", - cp_offset, advance_by, character, *printable, LabelToInt(on_match), - LabelToInt(on_no_match)); - assembler_->SkipUntilChar(cp_offset, advance_by, character, on_match, - on_no_match); -} - -void RegExpMacroAssemblerTracer::SkipUntilCharPosChecked( - int cp_offset, int advance_by, unsigned character, int eats_at_least, - Label* on_match, Label* on_no_match) { - PrintablePrinter printable(character); - PrintF( - "SkipUntilCharPosChecked(cp_offset=%d, advance_by=%d, " - "character=0x%04x%s, eats_at_least=%d, on_match=label[%08x], " - "on_no_match=label[%08x]\n", - cp_offset, advance_by, character, *printable, eats_at_least, + cp_offset, advance_by, character, *printable, bounds_check_offset, LabelToInt(on_match), LabelToInt(on_no_match)); - assembler_->SkipUntilCharPosChecked(cp_offset, advance_by, character, - eats_at_least, on_match, on_no_match); + assembler_->SkipUntilChar(cp_offset, advance_by, character, + bounds_check_offset, on_match, on_no_match); } void RegExpMacroAssemblerTracer::SkipUntilCharOrChar( int cp_offset, int advance_by, unsigned char1, unsigned char2, - Label* on_match, Label* on_no_match) { + int bounds_check_offset, Label* on_match, Label* on_no_match) { PrintablePrinter printable1(char1); PrintablePrinter printable2(char2); PrintF( "SkipUntilCharOrChar(cp_offset=%d, advance_by=%d, char1=0x%04x%s, " - "char2=0x%04x%s, on_match=label[%08x], " + "char2=0x%04x%s, bounds_check_offset=%d, on_match=label[%08x], " "on_no_match=label[%08x]\n", cp_offset, advance_by, char1, *printable1, char2, *printable2, - LabelToInt(on_match), LabelToInt(on_no_match)); - assembler_->SkipUntilCharOrChar(cp_offset, advance_by, char1, char2, on_match, - on_no_match); + bounds_check_offset, LabelToInt(on_match), LabelToInt(on_no_match)); + assembler_->SkipUntilCharOrChar(cp_offset, advance_by, char1, char2, + bounds_check_offset, on_match, on_no_match); } void RegExpMacroAssemblerTracer::SkipUntilGtOrNotBitInTable( int cp_offset, int advance_by, unsigned character, Handle table, - Label* on_match, Label* on_no_match) { + int bounds_check_offset, Label* on_match, Label* on_no_match) { PrintablePrinter printable(character); PrintF( "SkipUntilGtOrNotBitInTable(cp_offset=%d, advance_by=%d, " - "character=0x%04x%s, on_match=label[%08x], " + "character=0x%04x%s, bounds_check_offset=%d, on_match=label[%08x], " "on_no_match=label[%08x]\n ", - cp_offset, advance_by, character, *printable, LabelToInt(on_match), - LabelToInt(on_no_match)); + cp_offset, advance_by, character, *printable, bounds_check_offset, + LabelToInt(on_match), LabelToInt(on_no_match)); for (int i = 0; i < kTableSize; i++) { PrintF("%c", table->get(i) != 0 ? 'X' : '.'); if (i % 32 == 31 && i != kTableMask) { PrintF("\n "); } } PrintF(");\n"); assembler_->SkipUntilGtOrNotBitInTable(cp_offset, advance_by, character, - table, on_match, on_no_match); + table, bounds_check_offset, on_match, + on_no_match); } void RegExpMacroAssemblerTracer::SkipUntilOneOfMasked( int cp_offset, int advance_by, unsigned both_chars, unsigned both_mask, int max_offset, unsigned chars1, unsigned mask1, unsigned chars2, @@ -496,28 +487,29 @@ void RegExpMacroAssemblerTracer::SkipUntilOneOfMasked3( const SkipUntilOneOfMasked3Args& args) { PrintF( "SkipUntilOneOfMasked3(bc0_cp_offset=%d, bc0_advance_by=%d, " - "bc1_cp_offset=%d, bc1_on_failure=label[%08x], " - "bc2_cp_offset=%d, " - "bc3_characters=0x%04x%s, bc3_mask=0x%04x, " - "bc4_by=%d, " - "bc5_cp_offset=%d, " + "bc1_bounds_check_offset=%d, bc1_on_failure=label[%08x], " + "bc1_cp_offset=%d, " + "bc2_characters=0x%04x%s, bc2_mask=0x%04x, " + "bc3_by=%d, " + "bc4_bounds_check_offset=%d, " + "bc4_cp_offset=%d, " + "bc5_characters=0x%04x%s, bc5_mask=0x%04x, bc5_on_equal=label[%08x], " "bc6_characters=0x%04x%s, bc6_mask=0x%04x, bc6_on_equal=label[%08x], " - "bc7_characters=0x%04x%s, bc7_mask=0x%04x, bc7_on_equal=label[%08x], " - "bc8_characters=0x%04x%s, bc8_mask=0x%04x, " + "bc7_characters=0x%04x%s, bc7_mask=0x%04x, " "fallthrough_jump_target=label[%08x]\n ", - args.bc0_cp_offset, args.bc0_advance_by, args.bc1_cp_offset, - LabelToInt(args.bc1_on_failure), args.bc2_cp_offset, args.bc3_characters, - *PrintablePrinter(args.bc3_characters), args.bc3_mask, args.bc4_by, - args.bc5_cp_offset, args.bc6_characters, + args.bc0_cp_offset, args.bc0_advance_by, args.bc1_bounds_check_offset, + LabelToInt(args.bc1_on_failure), args.bc1_cp_offset, args.bc2_characters, + *PrintablePrinter(args.bc2_characters), args.bc2_mask, args.bc3_by, + args.bc4_bounds_check_offset, args.bc4_cp_offset, args.bc5_characters, + *PrintablePrinter(args.bc5_characters), args.bc5_mask, + LabelToInt(args.bc5_on_equal), args.bc6_characters, *PrintablePrinter(args.bc6_characters), args.bc6_mask, LabelToInt(args.bc6_on_equal), args.bc7_characters, *PrintablePrinter(args.bc7_characters), args.bc7_mask, - LabelToInt(args.bc7_on_equal), args.bc8_characters, - *PrintablePrinter(args.bc8_characters), args.bc8_mask, LabelToInt(args.fallthrough_jump_target)); PrintTables(args.bc0_table, args.bc0_nibble_table); PrintF(");\n"); assembler_->SkipUntilOneOfMasked3(args); } Index: js/src/irregexp/imported/regexp-macro-assembler.h =================================================================== --- js/src/irregexp/imported/regexp-macro-assembler.h +++ js/src/irregexp/imported/regexp-macro-assembler.h @@ -3,10 +3,11 @@ // found in the LICENSE file. #ifndef V8_REGEXP_REGEXP_MACRO_ASSEMBLER_H_ #define V8_REGEXP_REGEXP_MACRO_ASSEMBLER_H_ +#include #include #include "irregexp/imported/regexp-ast.h" #include "irregexp/imported/regexp.h" #include "irregexp/RegExpShim.h" @@ -37,16 +38,17 @@ // Note the minimum value is chosen s.t. a negated valid offset is also a // valid offset. static constexpr int kMaxCPOffset = (1 << 15) - 1; static constexpr int kMinCPOffset = -kMaxCPOffset; + static constexpr int kMaxEatsAtLeastValue = + std::numeric_limits::max(); + static constexpr int kTableSizeBits = 7; static constexpr int kTableSize = 1 << kTableSizeBits; static constexpr int kTableMask = kTableSize - 1; - static constexpr int kUseCharactersValue = -1; - // Type of input string to generate code for. enum Mode { LATIN1 = 1, UC16 = 2 }; RegExpMacroAssembler(Isolate* isolate, Zone* zone, Mode mode); RegExpMacroAssembler(const RegExpMacroAssembler& other) V8_NOEXCEPT = default; @@ -118,31 +120,61 @@ // The current character (modulus the kTableSize) is looked up in the byte // array, and if the found byte is non-zero, we jump to the on_bit_set label. virtual void CheckBitInTable(Handle table, Label* on_bit_set) = 0; + // Scan helpers. Each loads the character at |cp_offset| and advances the + // position by |advance_by| until its stop condition holds (a single char, one + // of two chars, a bit-table test, ...). Position on exit, which SIMD + // overrides must preserve: + // - on_match: stop condition held; position unadvanced, so the matching + // char is at |cp_offset| (not consumed). + // - on_no_match: bounds check failed first; position left where the load at + // |cp_offset| went out of bounds, for the caller to step + // back. virtual void SkipUntilBitInTable(int cp_offset, Handle table, Handle nibble_table, - int advance_by, Label* on_match, - Label* on_no_match) = 0; + int advance_by, int bounds_check_offset, + Label* on_match, Label* on_no_match) = 0; virtual bool SkipUntilBitInTableUseSimd(int advance_by) { return false; } virtual void SkipUntilCharAnd(int cp_offset, int advance_by, unsigned character, unsigned mask, - int eats_at_least, Label* on_match, + int bounds_check_offset, Label* on_match, Label* on_no_match); + virtual bool SkipUntilCharAndUseSimd(int advance_by) { return false; } + virtual void SkipUntilCharAndSimd(int cp_offset, int advance_by, + unsigned character, unsigned mask, + int bounds_check_offset, Label* on_match, + Label* on_no_match) { + UNREACHABLE(); + } virtual void SkipUntilChar(int cp_offset, int advance_by, unsigned character, - Label* on_match, Label* on_no_match); - virtual void SkipUntilCharPosChecked(int cp_offset, int advance_by, - unsigned character, int eats_at_least, - Label* on_match, Label* on_no_match); + int bounds_check_offset, Label* on_match, + Label* on_no_match); + virtual bool SkipUntilCharUseSimd(int advance_by) { return false; } + virtual void SkipUntilCharSimd(int cp_offset, int advance_by, + unsigned character, int bounds_check_offset, + Label* on_match, Label* on_no_match) { + UNREACHABLE(); + } + virtual void SkipUntilCharOrChar(int cp_offset, int advance_by, unsigned char1, unsigned char2, - Label* on_match, Label* on_no_match); + int bounds_check_offset, Label* on_match, + Label* on_no_match); + virtual bool SkipUntilCharOrCharUseSimd(int advance_by) { return false; } + virtual void SkipUntilCharOrCharSimd(int cp_offset, int advance_by, + unsigned char1, unsigned char2, + int bounds_check_offset, Label* on_match, + Label* on_no_match) { + UNREACHABLE(); + } virtual void SkipUntilGtOrNotBitInTable(int cp_offset, int advance_by, unsigned character, Handle table, + int bounds_check_offset, Label* on_match, Label* on_no_match); virtual void SkipUntilOneOfMasked(int cp_offset, int advance_by, unsigned both_chars, unsigned both_mask, int max_offset, unsigned chars1, unsigned mask1, unsigned chars2, @@ -151,25 +183,26 @@ struct SkipUntilOneOfMasked3Args { int bc0_cp_offset; int bc0_advance_by; Handle bc0_table; Handle bc0_nibble_table; - int bc1_cp_offset; + int bc1_bounds_check_offset; Label* bc1_on_failure; - int bc2_cp_offset; - unsigned bc3_characters; - unsigned bc3_mask; - int bc4_by; - int bc5_cp_offset; + int bc1_cp_offset; + unsigned bc2_characters; + unsigned bc2_mask; + int bc3_by; + int bc4_bounds_check_offset; + int bc4_cp_offset; + unsigned bc5_characters; + unsigned bc5_mask; + Label* bc5_on_equal; unsigned bc6_characters; unsigned bc6_mask; Label* bc6_on_equal; unsigned bc7_characters; unsigned bc7_mask; - Label* bc7_on_equal; - unsigned bc8_characters; - unsigned bc8_mask; Label* fallthrough_jump_target; }; virtual bool SkipUntilOneOfMasked3UseSimd( const SkipUntilOneOfMasked3Args& args) { return false; @@ -201,16 +234,23 @@ // Backtracks instead if the label is nullptr. virtual void IfRegisterLT(int reg, int comparand, Label* if_lt) = 0; // Check whether a register is == to the current position and go to a // label if it is. virtual void IfRegisterEqPos(int reg, Label* if_eq) = 0; + + int CalculateBoundsCheckOffset(int cp_offset, int characters); + // Default value for bounds_check_limit. + // If set to this default sentinel, bounds_check_offset is computed based on + // `cp_offset` and `characters` using `CalculateBoundsCheckOffset()`. + static constexpr int kDefaultBoundsCheckOffset = + std::numeric_limits::min(); V8_EXPORT_PRIVATE void LoadCurrentCharacter( int cp_offset, Label* on_end_of_input, bool check_bounds = true, - int characters = 1, int eats_at_least = kUseCharactersValue); + int characters = 1, int bounds_check_offset = kDefaultBoundsCheckOffset); virtual void LoadCurrentCharacterImpl(int cp_offset, Label* on_end_of_input, bool check_bounds, int characters, - int eats_at_least) = 0; + int bounds_check_offset) = 0; virtual void PopCurrentPosition() = 0; virtual void PopRegister(int register_index) = 0; // Pushes the label on the backtrack stack, so that a following Backtrack // will go to this label. Always checks the backtrack stack limit. virtual void PushBacktrack(Label* label) = 0; @@ -339,10 +379,14 @@ static_assert(static_cast(Mode::LATIN1) == sizeof(uint8_t)); static_assert(static_cast(Mode::UC16) == sizeof(uint16_t)); return static_cast(mode()); } + inline uint32_t char_mask() const { + return mode() == LATIN1 ? 0xffU : 0xffffU; + } + bool has_backtrack_limit() const; uint32_t backtrack_limit() const { return backtrack_limit_; } bool can_fallback() const { return can_fallback_; } @@ -401,11 +445,11 @@ const uint8_t* input_end, int* output, int output_size, Isolate* isolate, Tagged regexp); void LoadCurrentCharacterImpl(int cp_offset, Label* on_end_of_input, bool check_bounds, int characters, - int eats_at_least) override; + int bounds_check_offset) override; // Load a number of characters at the given offset from the // current position, into the current-character register. virtual void LoadCurrentCharacterUnchecked(int cp_offset, int character_count) = 0; Index: js/src/irregexp/imported/regexp-macro-assembler.cc =================================================================== --- js/src/irregexp/imported/regexp-macro-assembler.cc +++ js/src/irregexp/imported/regexp-macro-assembler.cc @@ -291,97 +291,125 @@ CheckPosition(0, on_failure); AdvanceCurrentPosition(1); } } +int RegExpMacroAssembler::CalculateBoundsCheckOffset(int cp_offset, + int characters) { + return cp_offset >= 0 ? cp_offset + characters - 1 : cp_offset; +} + void RegExpMacroAssembler::LoadCurrentCharacter(int cp_offset, Label* on_end_of_input, bool check_bounds, int characters, - int eats_at_least) { - // By default, eats_at_least = characters. - if (eats_at_least == kUseCharactersValue) { - eats_at_least = characters; + int bounds_check_offset) { + DCHECK_IMPLIES(bounds_check_offset < 0, + bounds_check_offset == kDefaultBoundsCheckOffset || + bounds_check_offset == cp_offset); + if (bounds_check_offset == kDefaultBoundsCheckOffset) { + bounds_check_offset = CalculateBoundsCheckOffset(cp_offset, characters); } LoadCurrentCharacterImpl(cp_offset, on_end_of_input, check_bounds, characters, - eats_at_least); + bounds_check_offset); } void NativeRegExpMacroAssembler::LoadCurrentCharacterImpl( int cp_offset, Label* on_end_of_input, bool check_bounds, int characters, - int eats_at_least) { - // It's possible to preload a small number of characters when each success - // path requires a large number of characters, but not the reverse. - DCHECK_GE(eats_at_least, characters); - + int bounds_check_offset) { CHECK(base::IsInRange(cp_offset, kMinCPOffset, kMaxCPOffset)); if (check_bounds) { - if (cp_offset >= 0) { - CheckPosition(cp_offset + eats_at_least - 1, on_end_of_input); - } else { - CheckPosition(cp_offset, on_end_of_input); - } + DCHECK_LE(cp_offset, bounds_check_offset); + CheckPosition(bounds_check_offset, on_end_of_input); } LoadCurrentCharacterUnchecked(cp_offset, characters); } void RegExpMacroAssembler::SkipUntilCharAnd(int cp_offset, int advance_by, unsigned character, unsigned mask, - int eats_at_least, Label* on_match, + int bounds_check_offset, + Label* on_match, Label* on_no_match) { + auto emit_scalar_check = [&]() { + LoadCurrentCharacter(cp_offset, on_no_match, true, 1, bounds_check_offset); + CheckCharacterAfterAnd(character, mask, on_match); + AdvanceCurrentPosition(advance_by); + }; + if (SkipUntilCharAndUseSimd(advance_by)) { + // Scalar check for the first position to avoid SIMD setup overhead if we + // find a potential match immediately. + emit_scalar_check(); + SkipUntilCharAndSimd(cp_offset, advance_by, character, mask, + bounds_check_offset, on_match, on_no_match); + } Label loop; Bind(&loop); - LoadCurrentCharacter(cp_offset, on_no_match, true, 1, eats_at_least); - CheckCharacterAfterAnd(character, mask, on_match); - AdvanceCurrentPosition(advance_by); + emit_scalar_check(); GoTo(&loop); } void RegExpMacroAssembler::SkipUntilChar(int cp_offset, int advance_by, - unsigned character, Label* on_match, - Label* on_no_match) { + unsigned character, + int bounds_check_offset, + Label* on_match, Label* on_no_match) { + auto emit_scalar_check = [&]() { + LoadCurrentCharacter(cp_offset, on_no_match, true, 1, bounds_check_offset); + CheckCharacter(character, on_match); + AdvanceCurrentPosition(advance_by); + }; + + if (SkipUntilCharUseSimd(advance_by)) { + // Scalar check for the first position to avoid SIMD setup overhead if we + // find a potential match immediately. + emit_scalar_check(); + + SkipUntilCharSimd(cp_offset, advance_by, character, bounds_check_offset, + on_match, on_no_match); + } + + // Fallback scalar loop Label loop; Bind(&loop); - LoadCurrentCharacter(cp_offset, on_no_match, true); - CheckCharacter(character, on_match); - AdvanceCurrentPosition(advance_by); - GoTo(&loop); -} - -void RegExpMacroAssembler::SkipUntilCharPosChecked( - int cp_offset, int advance_by, unsigned character, int eats_at_least, - Label* on_match, Label* on_no_match) { - Label loop; - Bind(&loop); - LoadCurrentCharacter(cp_offset, on_no_match, true, 1, eats_at_least); - CheckCharacter(character, on_match); - AdvanceCurrentPosition(advance_by); + emit_scalar_check(); GoTo(&loop); } void RegExpMacroAssembler::SkipUntilCharOrChar(int cp_offset, int advance_by, unsigned char1, unsigned char2, + int bounds_check_offset, Label* on_match, Label* on_no_match) { + auto emit_scalar_check = [&]() { + LoadCurrentCharacter(cp_offset, on_no_match, true, 1, bounds_check_offset); + CheckCharacter(char1, on_match); + CheckCharacter(char2, on_match); + AdvanceCurrentPosition(advance_by); + }; + + if (SkipUntilCharOrCharUseSimd(advance_by)) { + // Scalar check for the first position to avoid SIMD setup overhead if we + // find a potential match immediately. + emit_scalar_check(); + + SkipUntilCharOrCharSimd(cp_offset, advance_by, char1, char2, + bounds_check_offset, on_match, on_no_match); + } Label loop; Bind(&loop); - LoadCurrentCharacter(cp_offset, on_no_match, true); - CheckCharacter(char1, on_match); - CheckCharacter(char2, on_match); - AdvanceCurrentPosition(advance_by); + emit_scalar_check(); GoTo(&loop); } void RegExpMacroAssembler::SkipUntilGtOrNotBitInTable( int cp_offset, int advance_by, unsigned character, Handle table, - Label* on_match, Label* on_no_match) { + int bounds_check_offset, Label* on_match, Label* on_no_match) { DCHECK(base::IsInRange(character, std::numeric_limits::min(), std::numeric_limits::max())); Label loop, advance_and_continue; Bind(&loop); - LoadCurrentCharacter(cp_offset, on_no_match, true); + LoadCurrentCharacter(cp_offset, on_no_match, true, 1, bounds_check_offset); CheckCharacterGT(character, on_match); CheckBitInTable(table, &advance_and_continue); GoTo(on_match); Bind(&advance_and_continue); AdvanceCurrentPosition(advance_by); @@ -428,39 +456,39 @@ // See the definition of the kSkipUntilOneOfMasked3 peephole bytecode // for more context. The initial bytecode sequence is: // // sequence offset name // bc0 0 SkipUntilBitInTable - // bc1 20 CheckPosition - // bc2 28 Load4CurrentCharsUnchecked - // bc3 2c AndCheck4Chars - // bc4 3c AdvanceCpAndGoto - // bc5 48 Load4CurrentChars - // bc6 4c AndCheck4Chars - // bc7 5c AndCheck4Chars - // bc8 6c AndCheckNot4Chars + // bc1 24 Load4CurrentChars + // bc2 30 AndCheck4Chars + // bc3 40 AdvanceCpAndGoto + // bc4 48 Load4CurrentChars + // bc5 54 AndCheck4Chars + // bc6 64 AndCheck4Chars + // bc7 74 AndCheckNot4Chars - Label bc0_skip_until_bit_in_table, bc1_check_current_position, - bc4_advance_cp_and_goto, bc5_load_4_current_chars; + Label bc0_skip_until_bit_in_table, bc1_load_4_current_chars, + bc3_advance_cp_and_goto, bc4_load_4_current_chars; Bind(&bc0_skip_until_bit_in_table); SkipUntilBitInTable(args.bc0_cp_offset, args.bc0_table, args.bc0_nibble_table, - args.bc0_advance_by, &bc1_check_current_position, - &bc1_check_current_position); - Bind(&bc1_check_current_position); - CheckPosition(args.bc1_cp_offset, args.bc1_on_failure); - LoadCurrentCharacter(args.bc2_cp_offset, nullptr, false, 4); - CheckCharacterAfterAnd(args.bc3_characters, args.bc3_mask, - &bc5_load_4_current_chars); - Bind(&bc4_advance_cp_and_goto); - AdvanceCurrentPosition(args.bc4_by); + args.bc0_advance_by, args.bc0_cp_offset, + &bc1_load_4_current_chars, &bc1_load_4_current_chars); + Bind(&bc1_load_4_current_chars); + LoadCurrentCharacter(args.bc1_cp_offset, args.bc1_on_failure, true, 4, + args.bc1_bounds_check_offset); + CheckCharacterAfterAnd(args.bc2_characters, args.bc2_mask, + &bc4_load_4_current_chars); + Bind(&bc3_advance_cp_and_goto); + AdvanceCurrentPosition(args.bc3_by); GoTo(&bc0_skip_until_bit_in_table); - Bind(&bc5_load_4_current_chars); - LoadCurrentCharacter(args.bc5_cp_offset, &bc4_advance_cp_and_goto, true, 4); + Bind(&bc4_load_4_current_chars); + LoadCurrentCharacter(args.bc4_cp_offset, &bc3_advance_cp_and_goto, true, 4, + args.bc4_bounds_check_offset); + CheckCharacterAfterAnd(args.bc5_characters, args.bc5_mask, args.bc5_on_equal); CheckCharacterAfterAnd(args.bc6_characters, args.bc6_mask, args.bc6_on_equal); - CheckCharacterAfterAnd(args.bc7_characters, args.bc7_mask, args.bc7_on_equal); - CheckNotCharacterAfterAnd(args.bc8_characters, args.bc8_mask, - &bc4_advance_cp_and_goto); + CheckNotCharacterAfterAnd(args.bc7_characters, args.bc7_mask, + &bc3_advance_cp_and_goto); GoTo(args.fallthrough_jump_target); } #ifndef COMPILING_IRREGEXP_FOR_EXTERNAL_EMBEDDER @@ -508,15 +536,15 @@ int return_value = 0; { DisableGCMole no_gc_mole; if (js_has_overflowed) { - [[maybe_unused]] AllowGarbageCollection yes_gc; + AllowGarbageCollection yes_gc; isolate->StackOverflow(); return_value = EXCEPTION; } else if (check.InterruptRequested()) { - [[maybe_unused]] AllowGarbageCollection yes_gc; + AllowGarbageCollection yes_gc; Tagged result = isolate->stack_guard()->HandleInterrupts(); if (IsExceptionHole(result)) return_value = EXCEPTION; } // We are not using operator == here because it does a slow DCHECK Index: js/src/irregexp/imported/regexp-nodes.h =================================================================== --- js/src/irregexp/imported/regexp-nodes.h +++ js/src/irregexp/imported/regexp-nodes.h @@ -3,10 +3,12 @@ // found in the LICENSE file. #ifndef V8_REGEXP_REGEXP_NODES_H_ #define V8_REGEXP_REGEXP_NODES_H_ +#include + #include "irregexp/imported/regexp-macro-assembler.h" namespace v8 { namespace internal { namespace regexp { @@ -715,12 +717,51 @@ AlternativeGeneration* alt_gen, int preload_characters, bool next_expects_preload); void SetUpPreLoad(Compiler* compiler, Trace* current_trace, PreloadState* preloads); void AssertGuardsMentionRegisters(Trace* trace); + // Sets *bm_scan_emitted iff a Boyer-Moore skip-scan was emitted as a + // straight-line prelude. Callers can use this to gate alternative scan-loop + // strategies that would otherwise conflict. int EmitOptimizedUnanchoredSearch(Compiler* compiler, Trace* trace, - SpecialLoopState* search_loop_state); + SpecialLoopState* search_loop_state, + bool* bm_scan_emitted); + // Shared structural gate for the inline SkipUntil* scan strategies below. + // Returns the body node of the implicit `.*?` lazy-star loop (alt 1 is an + // omnivorous Text re-entering this loop) for inspection, skipping past any + // EATS_AT_LEAST tags and at most one deferrable ActionNode wrapper (e.g. + // STORE_POSITION for capture 0). When *wrapper_out is provided it receives + // that wrapper (for callers that emit the body directly and must defer it). + // Returns nullptr if `this` is not that shape. The caller computes this once + // and passes the result into both EmitSkipUntil* strategies below. + Node* MatchLazyStarLoopBody(Compiler* compiler, + ActionNode** wrapper_out = nullptr); + // Sibling of EmitOptimizedUnanchoredSearch for the SkipUntilOneOfMasked + // peephole pattern. `body`/`wrapper` come from MatchLazyStarLoopBody. If + // `body` is a 2-alt Choice of Texts whose first 4 chars yield useful + // QuickCheck details, emits the scan op dispatching directly to the two + // alternative bodies (with a back-edge re-checking alt 1 at the same + // position, mirroring the peephole) and returns the body-emission result; + // the caller propagates it and skips EmitChoices. Returns nullopt if `body` + // is not that shape, so the caller falls through to the BitInTable strategy. + V8_WARN_UNUSED_RESULT std::optional + EmitSkipUntilOneOfMaskedSearch(Compiler* compiler, Trace* trace, Node* body, + ActionNode* wrapper); + // Sibling of EmitOptimizedUnanchoredSearch: accelerates the implicit + // unanchored search to the first position where `body` (from + // MatchLazyStarLoopBody) can match, via the cheapest applicable SkipUntil* + // scan (Char / CharAnd / CharOrChar / BitInTable). Emits a straight-line + // prelude; the caller unconditionally falls through to EmitChoices afterwards + // (which emits the body at the candidate position), so there is no need to + // report back whether the prelude fired. + void EmitSkipUntilSearchPrelude(Compiler* compiler, Trace* trace, Node* body); + // For a greedy one-byte character-class body, emit a single SkipUntilChar / + // SkipUntilCharOrChar / SkipUntilCharAnd scan over its exit set in place of + // the per-iteration body + back-edge (landing on |exit|), and return true. + // The caller keeps the surrounding loop frame. False if not fusible. + bool MaybeEmitFixedLengthConsumeScan(Compiler* compiler, Label* exit, + int text_length); // Returns nullptr on failure. // TODO(jgruber): Consider wrapping the return value in EmitResult. V8_WARN_UNUSED_RESULT Trace* EmitFixedLengthLoop( Compiler* compiler, Trace* trace, AlternativeGenerationList* alt_gens, PreloadState* preloads, SpecialLoopState* fixed_length_loop_state, Index: js/src/irregexp/imported/regexp-parser.cc =================================================================== --- js/src/irregexp/imported/regexp-parser.cc +++ js/src/irregexp/imported/regexp-parser.cc @@ -8,11 +8,10 @@ #include "irregexp/imported/regexp-ast.h" #include "irregexp/imported/regexp-macro-assembler.h" #include "irregexp/imported/regexp.h" #ifdef V8_INTL_SUPPORT -#include "js/properties_glue.h" #include "unicode/uniset.h" #include "unicode/unistr.h" #include "unicode/usetiter.h" #include "unicode/utf16.h" // For U16_NEXT #endif // V8_INTL_SUPPORT @@ -969,10 +968,11 @@ } // Restore previous state. state = state->previous_state(); builder = state->builder(); + flags_ = builder->flags(); builder->AddAtom(body); // For compatibility with JSC and ES3, we allow quantifiers after // lookaheads, and break in all cases. break; @@ -2106,13 +2106,11 @@ zone); } else if (NameEquals(name, "Assigned")) { return LookupPropertyValueName(UCHAR_GENERAL_CATEGORY, "Unassigned", !negate, result, nullptr, flags, zone); } else { - return mozilla_properties_glue_add_property_ranges( - static_cast(result), static_cast(zone), name, negate, - IsUnicodeSets(flags) && IsIgnoreCase(flags)); + return false; } return true; } // Explicitly allowlist supported binary properties. The spec forbids supporting Index: js/src/irregexp/imported/regexp-stack.h =================================================================== --- js/src/irregexp/imported/regexp-stack.h +++ js/src/irregexp/imported/regexp-stack.h @@ -91,14 +91,16 @@ void FreeThreadResources() { thread_local_.ResetToStaticStack(this); } // Maximal size of allocated stack area. static constexpr size_t kMaximumStackSize = 64 * MB; + private: + // Currently private as we need to use New/Delete instead. + // TODO(426514762): revert this change once its no longer needed. Stack(); ~Stack(); - private: // Artificial limit used when the thread-local state has been destroyed. static const Address kMemoryTop = static_cast
(static_cast(-1)); // In addition to dynamically-allocated, variable-sized stacks, we also have Index: js/src/irregexp/imported/regexp-stack.cc =================================================================== --- js/src/irregexp/imported/regexp-stack.cc +++ js/src/irregexp/imported/regexp-stack.cc @@ -22,12 +22,10 @@ Stack::Stack() : thread_local_(this) {} Stack::~Stack() { thread_local_.FreeAndInvalidate(); } -#ifndef COMPILING_IRREGEXP_FOR_EXTERNAL_EMBEDDER - // static Stack* Stack::New() { #ifdef V8_ENABLE_SANDBOX_HARDWARE_SUPPORT // TODO(426514762): Stack objects must currently be accessible to // sandboxed code (which is unsafe). As such we need to register them as @@ -57,12 +55,10 @@ #else delete instance; #endif } -#endif // !COMPILING_IRREGEXP_FOR_EXTERNAL_EMBEDDER - char* Stack::ArchiveStack(char* to) { if (!thread_local_.owns_memory_) { // Force dynamic stacks prior to archiving. Any growth will do. A dynamic // stack is needed because stack archival & restoration rely on `memory_` // pointing at a fixed-location backing store, whereas the static stack is Index: js/src/irregexp/imported/regexp.h =================================================================== --- js/src/irregexp/imported/regexp.h +++ js/src/irregexp/imported/regexp.h @@ -116,22 +116,23 @@ #endif // V8_ENABLE_REGEXP_DIAGNOSTICS // See ECMA-262 section 15.10.6.2. // This function calls the garbage collector if necessary. V8_EXPORT_PRIVATE V8_WARN_UNUSED_RESULT static std::optional Exec( - Isolate* isolate, DirectHandle regexp, + Isolate* isolate, DirectHandle regexp_data, DirectHandle subject, int index, int32_t* result_offsets_vector, uint32_t result_offsets_vector_length); // As above, but passes the result through the old-style RegExpMatchInfo|Null // interface. At most one match is returned. V8_EXPORT_PRIVATE V8_WARN_UNUSED_RESULT static MaybeDirectHandle Exec_Single(Isolate* isolate, DirectHandle regexp, DirectHandle subject, int index, DirectHandle last_match_info); V8_EXPORT_PRIVATE V8_WARN_UNUSED_RESULT static std::optional - ExperimentalOneshotExec(Isolate* isolate, DirectHandle regexp, + ExperimentalOneshotExec(Isolate* isolate, + DirectHandle regexp_data, DirectHandle subject, int index, int32_t* result_offsets_vector, uint32_t result_offsets_vector_length); // Called directly from generated code through ExternalReference. Index: js/src/irregexp/moz.yaml =================================================================== --- js/src/irregexp/moz.yaml +++ js/src/irregexp/moz.yaml @@ -7,12 +7,12 @@ origin: name: irregexp description: A fast regular expression engine from V8 url: https://v8.dev - release: 199031eb59cdee6a25799d4d4ea40092c3522a71 (Fri Jun 12 17:06:11 2026). - revision: 199031eb59cdee6a25799d4d4ea40092c3522a71 + release: 90fd800ce3ce65c77bfcb96669027dc3cf50ac00 (Thu Jul 02 18:33:07 2026). + revision: 90fd800ce3ce65c77bfcb96669027dc3cf50ac00 license: BSD-3-Clause license-file: LICENSE.v8 vendoring: