2017-09-17 10:50:53 +02:00
|
|
|
#ifndef regex_impl_hh_INCLUDED
|
|
|
|
#define regex_impl_hh_INCLUDED
|
|
|
|
|
2017-10-07 06:46:27 +02:00
|
|
|
#include "exception.hh"
|
2017-10-07 04:43:21 +02:00
|
|
|
#include "flags.hh"
|
|
|
|
#include "ref_ptr.hh"
|
2017-10-02 08:59:04 +02:00
|
|
|
#include "unicode.hh"
|
|
|
|
#include "utf8.hh"
|
|
|
|
#include "vector.hh"
|
2018-10-10 13:35:38 +02:00
|
|
|
#include "utils.hh"
|
2017-10-02 08:59:04 +02:00
|
|
|
|
2017-09-26 09:44:30 +02:00
|
|
|
namespace Kakoune
|
|
|
|
{
|
|
|
|
|
2017-10-09 08:04:14 +02:00
|
|
|
struct regex_error : runtime_error
|
|
|
|
{
|
|
|
|
using runtime_error::runtime_error;
|
|
|
|
};
|
|
|
|
|
2017-10-07 06:46:27 +02:00
|
|
|
enum class MatchDirection
|
|
|
|
{
|
|
|
|
Forward,
|
|
|
|
Backward
|
|
|
|
};
|
|
|
|
|
2017-11-25 11:14:15 +01:00
|
|
|
enum class CharacterType : unsigned char
|
|
|
|
{
|
|
|
|
None = 0,
|
2017-11-27 17:13:42 +01:00
|
|
|
Whitespace = 1 << 0,
|
|
|
|
HorizontalWhitespace = 1 << 1,
|
|
|
|
Word = 1 << 2,
|
2017-11-25 11:14:15 +01:00
|
|
|
Digit = 1 << 3,
|
2017-11-27 17:13:42 +01:00
|
|
|
NotWhitespace = 1 << 4,
|
|
|
|
NotHorizontalWhitespace = 1 << 5,
|
|
|
|
NotWord = 1 << 6,
|
2017-11-25 11:14:15 +01:00
|
|
|
NotDigit = 1 << 7
|
|
|
|
};
|
|
|
|
constexpr bool with_bit_ops(Meta::Type<CharacterType>) { return true; }
|
|
|
|
|
|
|
|
struct CharacterClass
|
|
|
|
{
|
|
|
|
struct Range { Codepoint min, max; };
|
|
|
|
|
|
|
|
Vector<Range, MemoryDomain::Regex> ranges;
|
|
|
|
CharacterType ctypes = CharacterType::None;
|
|
|
|
bool negative = false;
|
|
|
|
bool ignore_case = false;
|
|
|
|
};
|
|
|
|
|
|
|
|
bool is_character_class(const CharacterClass& character_class, Codepoint cp);
|
|
|
|
bool is_ctype(CharacterType ctype, Codepoint cp);
|
|
|
|
|
2017-10-15 03:23:57 +02:00
|
|
|
struct CompiledRegex : RefCountable, UseMemoryDomain<MemoryDomain::Regex>
|
2017-10-02 08:59:04 +02:00
|
|
|
{
|
|
|
|
enum Op : char
|
|
|
|
{
|
|
|
|
Match,
|
2017-10-20 09:17:02 +02:00
|
|
|
FindNextStart,
|
2017-10-02 08:59:04 +02:00
|
|
|
Literal,
|
2017-10-10 05:21:21 +02:00
|
|
|
Literal_IgnoreCase,
|
2017-10-02 08:59:04 +02:00
|
|
|
AnyChar,
|
2018-06-24 12:13:35 +02:00
|
|
|
AnyCharExceptNewLine,
|
2017-11-25 11:14:15 +01:00
|
|
|
Class,
|
|
|
|
CharacterType,
|
2017-10-02 08:59:04 +02:00
|
|
|
Jump,
|
|
|
|
Split_PrioritizeParent,
|
|
|
|
Split_PrioritizeChild,
|
|
|
|
Save,
|
|
|
|
LineStart,
|
|
|
|
LineEnd,
|
|
|
|
WordBoundary,
|
|
|
|
NotWordBoundary,
|
|
|
|
SubjectBegin,
|
|
|
|
SubjectEnd,
|
|
|
|
LookAhead,
|
|
|
|
NegativeLookAhead,
|
2017-10-04 17:00:19 +02:00
|
|
|
LookBehind,
|
2017-10-02 08:59:04 +02:00
|
|
|
NegativeLookBehind,
|
2017-10-10 05:21:21 +02:00
|
|
|
LookAhead_IgnoreCase,
|
|
|
|
NegativeLookAhead_IgnoreCase,
|
|
|
|
LookBehind_IgnoreCase,
|
|
|
|
NegativeLookBehind_IgnoreCase,
|
2017-10-02 08:59:04 +02:00
|
|
|
};
|
|
|
|
|
2018-10-10 13:35:38 +02:00
|
|
|
enum class Lookaround : Codepoint
|
|
|
|
{
|
|
|
|
OpBegin = 0xF0000,
|
|
|
|
AnyChar = 0xF0000,
|
|
|
|
AnyCharExceptNewLine = 0xF0001,
|
|
|
|
CharacterClass = 0xF0002,
|
|
|
|
CharacterType = 0xF8000,
|
|
|
|
OpEnd = 0xFFFFF,
|
|
|
|
EndOfLookaround = static_cast<Codepoint>(-1)
|
|
|
|
};
|
|
|
|
|
2017-10-07 12:51:32 +02:00
|
|
|
struct Instruction
|
|
|
|
{
|
|
|
|
Op op;
|
2017-10-15 03:34:49 +02:00
|
|
|
// Those mutables are used during execution
|
2017-10-07 13:58:10 +02:00
|
|
|
mutable bool scheduled;
|
2017-10-14 06:58:42 +02:00
|
|
|
mutable uint16_t last_step;
|
2017-10-07 12:51:32 +02:00
|
|
|
uint32_t param;
|
|
|
|
};
|
2017-10-07 13:08:14 +02:00
|
|
|
static_assert(sizeof(Instruction) == 8, "");
|
2017-10-07 12:51:32 +02:00
|
|
|
|
2017-10-20 09:17:02 +02:00
|
|
|
static constexpr uint16_t search_prefix_size = 3;
|
|
|
|
|
2017-10-07 12:51:32 +02:00
|
|
|
explicit operator bool() const { return not instructions.empty(); }
|
2017-10-02 08:59:04 +02:00
|
|
|
|
2017-10-15 03:23:57 +02:00
|
|
|
Vector<Instruction, MemoryDomain::Regex> instructions;
|
2017-11-25 11:14:15 +01:00
|
|
|
Vector<CharacterClass, MemoryDomain::Regex> character_classes;
|
2018-10-10 13:35:38 +02:00
|
|
|
Vector<Lookaround, MemoryDomain::Regex> lookarounds;
|
2017-12-01 12:57:02 +01:00
|
|
|
uint32_t first_backward_inst; // -1 if no backward support, 0 if only backward, >0 if both forward and backward
|
|
|
|
uint32_t save_count;
|
2017-10-06 07:40:27 +02:00
|
|
|
|
2018-02-24 06:29:24 +01:00
|
|
|
struct StartDesc : UseMemoryDomain<MemoryDomain::Regex>
|
2017-10-09 12:19:36 +02:00
|
|
|
{
|
2018-04-29 08:42:46 +02:00
|
|
|
static constexpr Codepoint count = 128;
|
|
|
|
static constexpr Codepoint other = 0;
|
|
|
|
bool map[count];
|
2017-10-09 12:19:36 +02:00
|
|
|
};
|
2017-10-21 04:04:08 +02:00
|
|
|
|
2017-12-01 12:57:02 +01:00
|
|
|
std::unique_ptr<StartDesc> forward_start_desc;
|
|
|
|
std::unique_ptr<StartDesc> backward_start_desc;
|
2017-10-02 08:59:04 +02:00
|
|
|
};
|
|
|
|
|
2018-04-27 00:34:49 +02:00
|
|
|
String dump_regex(const CompiledRegex& program);
|
|
|
|
|
2017-10-23 11:29:03 +02:00
|
|
|
enum class RegexCompileFlags
|
2017-10-09 08:04:14 +02:00
|
|
|
{
|
|
|
|
None = 0,
|
|
|
|
NoSubs = 1 << 0,
|
2017-12-01 12:57:02 +01:00
|
|
|
Optimize = 1 << 1,
|
2018-07-19 10:34:40 +02:00
|
|
|
Backward = 1 << 2,
|
|
|
|
NoForward = 1 << 3,
|
2017-10-09 08:04:14 +02:00
|
|
|
};
|
|
|
|
constexpr bool with_bit_ops(Meta::Type<RegexCompileFlags>) { return true; }
|
|
|
|
|
2017-12-01 12:57:02 +01:00
|
|
|
CompiledRegex compile_regex(StringView re, RegexCompileFlags flags);
|
2017-10-02 08:59:04 +02:00
|
|
|
|
2017-10-02 16:34:57 +02:00
|
|
|
enum class RegexExecFlags
|
|
|
|
{
|
|
|
|
None = 0,
|
|
|
|
Search = 1 << 0,
|
|
|
|
NotBeginOfLine = 1 << 1,
|
|
|
|
NotEndOfLine = 1 << 2,
|
|
|
|
NotBeginOfWord = 1 << 3,
|
|
|
|
NotEndOfWord = 1 << 4,
|
2018-03-04 19:48:10 +01:00
|
|
|
NotInitialNull = 1 << 5,
|
|
|
|
AnyMatch = 1 << 6,
|
|
|
|
NoSaves = 1 << 7,
|
2017-10-02 16:34:57 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
constexpr bool with_bit_ops(Meta::Type<RegexExecFlags>) { return true; }
|
|
|
|
|
2018-11-01 11:51:10 +01:00
|
|
|
template<typename It, typename=void>
|
|
|
|
struct SentinelType { using Type = It; };
|
|
|
|
|
|
|
|
template<typename It>
|
|
|
|
struct SentinelType<It, void_t<typename It::Sentinel>> { using Type = typename It::Sentinel; };
|
|
|
|
|
2017-10-07 06:46:27 +02:00
|
|
|
template<typename Iterator, MatchDirection direction>
|
2017-10-06 13:30:46 +02:00
|
|
|
class ThreadedRegexVM
|
2017-10-02 08:59:04 +02:00
|
|
|
{
|
2017-10-06 13:30:46 +02:00
|
|
|
public:
|
2017-10-02 08:59:04 +02:00
|
|
|
ThreadedRegexVM(const CompiledRegex& program)
|
2017-10-07 06:46:27 +02:00
|
|
|
: m_program{program}
|
2017-10-09 15:56:48 +02:00
|
|
|
{
|
2018-11-04 22:17:50 +01:00
|
|
|
kak_assert((forward and program.first_backward_inst != 0) or
|
2017-12-01 12:57:02 +01:00
|
|
|
(direction == MatchDirection::Backward and program.first_backward_inst != -1));
|
2017-10-09 15:56:48 +02:00
|
|
|
}
|
2017-10-02 08:59:04 +02:00
|
|
|
|
2017-10-04 14:11:15 +02:00
|
|
|
ThreadedRegexVM(const ThreadedRegexVM&) = delete;
|
2017-10-06 13:30:46 +02:00
|
|
|
ThreadedRegexVM& operator=(const ThreadedRegexVM&) = delete;
|
2017-10-04 14:11:15 +02:00
|
|
|
|
2017-10-04 05:14:24 +02:00
|
|
|
~ThreadedRegexVM()
|
|
|
|
{
|
|
|
|
for (auto* saves : m_saves)
|
|
|
|
{
|
|
|
|
for (size_t i = m_program.save_count-1; i > 0; --i)
|
|
|
|
saves->pos[i].~Iterator();
|
|
|
|
saves->~Saves();
|
2017-10-15 03:23:57 +02:00
|
|
|
operator delete(saves);
|
2017-10-04 05:14:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-04 19:48:10 +01:00
|
|
|
bool exec(Iterator begin, Iterator end,
|
|
|
|
Iterator subject_begin, Iterator subject_end,
|
|
|
|
RegexExecFlags flags)
|
2017-10-06 13:30:46 +02:00
|
|
|
{
|
2017-10-11 13:24:01 +02:00
|
|
|
if (flags & RegexExecFlags::NotInitialNull and begin == end)
|
|
|
|
return false;
|
|
|
|
|
2017-10-20 09:17:02 +02:00
|
|
|
const bool search = (flags & RegexExecFlags::Search);
|
2018-04-21 04:44:54 +02:00
|
|
|
|
|
|
|
ConstArrayView<CompiledRegex::Instruction> instructions{m_program.instructions};
|
2018-11-04 22:17:50 +01:00
|
|
|
if (forward)
|
2018-04-21 04:44:54 +02:00
|
|
|
instructions = instructions.subrange(0, m_program.first_backward_inst);
|
|
|
|
else
|
|
|
|
instructions = instructions.subrange(m_program.first_backward_inst);
|
|
|
|
if (not search)
|
|
|
|
instructions = instructions.subrange(CompiledRegex::search_prefix_size);
|
|
|
|
|
|
|
|
const ExecConfig config{
|
2018-11-01 11:51:10 +01:00
|
|
|
Sentinel{forward ? begin : end},
|
|
|
|
Sentinel{forward ? end : begin},
|
2018-11-03 03:52:40 +01:00
|
|
|
Sentinel{subject_begin},
|
|
|
|
Sentinel{subject_end},
|
2018-04-21 04:44:54 +02:00
|
|
|
flags,
|
|
|
|
instructions
|
|
|
|
};
|
|
|
|
|
2018-11-04 22:17:50 +01:00
|
|
|
Iterator start = forward ? begin : end;
|
2018-11-03 03:52:40 +01:00
|
|
|
if (const auto& start_desc = forward ? m_program.forward_start_desc : m_program.backward_start_desc)
|
2017-12-01 08:03:03 +01:00
|
|
|
{
|
|
|
|
if (search)
|
|
|
|
{
|
2018-11-04 22:17:50 +01:00
|
|
|
to_next_start(start, config, *start_desc);
|
2018-11-01 22:23:39 +01:00
|
|
|
if (start == config.end) // If start_desc is not null, it means we consume at least one char
|
2017-12-01 08:03:03 +01:00
|
|
|
return false;
|
|
|
|
}
|
2018-11-04 22:17:50 +01:00
|
|
|
else if (start != config.end)
|
|
|
|
{
|
|
|
|
const Codepoint cp = codepoint(start, config);
|
|
|
|
if (not start_desc->map[cp < StartDesc::count ? cp : StartDesc::other])
|
|
|
|
return false;
|
|
|
|
}
|
2017-12-01 08:03:03 +01:00
|
|
|
}
|
2017-10-06 13:30:46 +02:00
|
|
|
|
2018-04-21 04:44:54 +02:00
|
|
|
return exec_program(std::move(start), config);
|
2017-10-06 13:30:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
ArrayView<const Iterator> captures() const
|
|
|
|
{
|
2018-04-27 00:18:04 +02:00
|
|
|
if (m_captures >= 0)
|
|
|
|
return { m_saves[m_captures]->pos, m_program.save_count };
|
2017-10-06 13:30:46 +02:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2017-10-03 04:54:43 +02:00
|
|
|
struct Saves
|
|
|
|
{
|
2018-11-05 11:54:29 +01:00
|
|
|
int16_t refcount;
|
|
|
|
int16_t next_free;
|
2017-10-04 05:14:24 +02:00
|
|
|
Iterator pos[1];
|
2017-10-03 04:54:43 +02:00
|
|
|
};
|
|
|
|
|
2017-10-04 13:49:16 +02:00
|
|
|
template<bool copy>
|
2018-04-27 00:18:04 +02:00
|
|
|
int16_t new_saves(Iterator* pos)
|
2017-10-03 04:54:43 +02:00
|
|
|
{
|
2017-10-04 13:49:16 +02:00
|
|
|
kak_assert(not copy or pos != nullptr);
|
|
|
|
const auto count = m_program.save_count;
|
2018-04-27 00:18:04 +02:00
|
|
|
if (m_first_free >= 0)
|
2017-10-03 04:54:43 +02:00
|
|
|
{
|
2018-04-27 00:18:04 +02:00
|
|
|
const int16_t res = m_first_free;
|
2018-11-05 11:54:29 +01:00
|
|
|
Saves& saves = *m_saves[res];
|
|
|
|
m_first_free = saves.next_free;
|
|
|
|
kak_assert(saves.refcount == 1);
|
2017-10-04 13:49:16 +02:00
|
|
|
if (copy)
|
2018-11-05 11:54:29 +01:00
|
|
|
std::copy_n(pos, count, saves.pos);
|
2017-10-04 13:49:16 +02:00
|
|
|
else
|
2018-11-05 11:54:29 +01:00
|
|
|
std::fill_n(saves.pos, count, Iterator{});
|
2017-10-04 13:49:16 +02:00
|
|
|
|
2017-10-03 12:23:31 +02:00
|
|
|
return res;
|
2017-10-03 04:54:43 +02:00
|
|
|
}
|
|
|
|
|
2017-10-15 03:23:57 +02:00
|
|
|
void* ptr = operator new (sizeof(Saves) + (count-1) * sizeof(Iterator));
|
2018-11-05 11:54:29 +01:00
|
|
|
Saves* saves = new (ptr) Saves{1, 0, {copy ? pos[0] : Iterator{}}};
|
2017-10-04 13:49:16 +02:00
|
|
|
for (size_t i = 1; i < count; ++i)
|
|
|
|
new (&saves->pos[i]) Iterator{copy ? pos[i] : Iterator{}};
|
|
|
|
m_saves.push_back(saves);
|
2018-04-27 00:18:04 +02:00
|
|
|
return static_cast<int16_t>(m_saves.size() - 1);
|
2017-10-03 04:54:43 +02:00
|
|
|
}
|
|
|
|
|
2018-11-05 11:54:29 +01:00
|
|
|
void release_saves(int16_t index)
|
2017-10-04 04:49:40 +02:00
|
|
|
{
|
2018-11-05 11:54:29 +01:00
|
|
|
if (index < 0)
|
|
|
|
return;
|
|
|
|
auto& saves = *m_saves[index];
|
|
|
|
if (saves.refcount == 1)
|
2017-10-13 15:58:38 +02:00
|
|
|
{
|
2018-11-05 11:54:29 +01:00
|
|
|
saves.next_free = m_first_free;
|
|
|
|
m_first_free = index;
|
2017-10-13 15:58:38 +02:00
|
|
|
}
|
2018-11-05 11:54:29 +01:00
|
|
|
else
|
|
|
|
--saves.refcount;
|
2017-10-04 04:49:40 +02:00
|
|
|
};
|
|
|
|
|
2017-10-02 08:59:04 +02:00
|
|
|
struct Thread
|
|
|
|
{
|
2018-04-27 00:18:04 +02:00
|
|
|
int16_t inst;
|
|
|
|
int16_t saves;
|
2017-10-02 08:59:04 +02:00
|
|
|
};
|
|
|
|
|
2018-04-29 09:26:49 +02:00
|
|
|
using StartDesc = CompiledRegex::StartDesc;
|
2018-11-01 11:51:10 +01:00
|
|
|
using Sentinel = typename SentinelType<Iterator>::Type;
|
2018-04-21 04:44:54 +02:00
|
|
|
struct ExecConfig
|
2017-10-11 04:24:05 +02:00
|
|
|
{
|
2018-11-01 11:51:10 +01:00
|
|
|
const Sentinel begin;
|
|
|
|
const Sentinel end;
|
|
|
|
const Sentinel subject_begin;
|
|
|
|
const Sentinel subject_end;
|
2018-04-21 04:44:54 +02:00
|
|
|
const RegexExecFlags flags;
|
|
|
|
ConstArrayView<CompiledRegex::Instruction> instructions;
|
2017-10-11 04:24:05 +02:00
|
|
|
};
|
|
|
|
|
2017-10-07 10:36:53 +02:00
|
|
|
// Steps a thread until it consumes the current character, matches or fail
|
2018-11-05 09:46:53 +01:00
|
|
|
void step_thread(const Iterator& pos, uint16_t current_step, Thread thread, const ExecConfig& config)
|
2017-10-02 08:59:04 +02:00
|
|
|
{
|
2018-11-05 09:46:53 +01:00
|
|
|
auto failed = [this](const Thread& thread) {
|
|
|
|
release_saves(thread.saves);
|
|
|
|
};
|
|
|
|
auto consumed = [this](const Thread& thread) {
|
|
|
|
if (m_program.instructions[thread.inst].scheduled)
|
|
|
|
return release_saves(thread.saves);
|
|
|
|
m_program.instructions[thread.inst].scheduled = true;
|
|
|
|
m_threads.push_next(thread);
|
|
|
|
};
|
|
|
|
|
2017-11-11 08:15:13 +01:00
|
|
|
auto* instructions = m_program.instructions.data();
|
2017-10-02 08:59:04 +02:00
|
|
|
while (true)
|
|
|
|
{
|
2018-04-27 00:18:04 +02:00
|
|
|
auto& inst = instructions[thread.inst++];
|
2018-02-24 07:21:15 +01:00
|
|
|
// if this instruction was already executed for this step in another thread,
|
|
|
|
// then this thread is redundant and can be dropped
|
2018-04-21 04:44:54 +02:00
|
|
|
if (inst.last_step == current_step)
|
2018-11-05 09:46:53 +01:00
|
|
|
return failed(thread);
|
2018-04-21 04:44:54 +02:00
|
|
|
inst.last_step = current_step;
|
2017-10-07 08:25:14 +02:00
|
|
|
|
2017-10-07 12:51:32 +02:00
|
|
|
switch (inst.op)
|
2017-10-02 08:59:04 +02:00
|
|
|
{
|
|
|
|
case CompiledRegex::Literal:
|
2018-11-04 22:17:50 +01:00
|
|
|
if (pos != config.end and inst.param == codepoint(pos, config))
|
2018-11-05 09:46:53 +01:00
|
|
|
return consumed(thread);
|
|
|
|
return failed(thread);
|
2017-10-10 05:21:21 +02:00
|
|
|
case CompiledRegex::Literal_IgnoreCase:
|
2018-11-04 22:17:50 +01:00
|
|
|
if (pos != config.end and inst.param == to_lower(codepoint(pos, config)))
|
2018-11-05 09:46:53 +01:00
|
|
|
return consumed(thread);
|
|
|
|
return failed(thread);
|
2017-10-02 08:59:04 +02:00
|
|
|
case CompiledRegex::AnyChar:
|
2018-11-05 09:46:53 +01:00
|
|
|
return consumed(thread);
|
2018-06-24 12:13:35 +02:00
|
|
|
case CompiledRegex::AnyCharExceptNewLine:
|
2018-11-04 22:17:50 +01:00
|
|
|
if (pos != config.end and codepoint(pos, config) != '\n')
|
2018-11-05 09:46:53 +01:00
|
|
|
return consumed(thread);
|
|
|
|
return failed(thread);
|
2017-10-02 08:59:04 +02:00
|
|
|
case CompiledRegex::Jump:
|
2018-04-27 00:18:04 +02:00
|
|
|
thread.inst = static_cast<int16_t>(inst.param);
|
2017-10-02 08:59:04 +02:00
|
|
|
break;
|
|
|
|
case CompiledRegex::Split_PrioritizeParent:
|
|
|
|
{
|
2018-04-27 00:18:04 +02:00
|
|
|
if (thread.saves >= 0)
|
|
|
|
++m_saves[thread.saves]->refcount;
|
2018-04-29 08:04:26 +02:00
|
|
|
m_threads.push_current({static_cast<int16_t>(inst.param), thread.saves});
|
2017-10-02 08:59:04 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case CompiledRegex::Split_PrioritizeChild:
|
|
|
|
{
|
2018-04-27 00:18:04 +02:00
|
|
|
if (thread.saves >= 0)
|
|
|
|
++m_saves[thread.saves]->refcount;
|
2018-04-29 08:04:26 +02:00
|
|
|
m_threads.push_current({thread.inst, thread.saves});
|
2018-04-27 00:18:04 +02:00
|
|
|
thread.inst = static_cast<uint16_t>(inst.param);
|
2017-10-02 08:59:04 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case CompiledRegex::Save:
|
|
|
|
{
|
2018-11-05 09:46:53 +01:00
|
|
|
if (config.flags & RegexExecFlags::NoSaves)
|
2017-10-03 13:07:44 +02:00
|
|
|
break;
|
2018-04-27 00:18:04 +02:00
|
|
|
if (thread.saves < 0)
|
2017-10-20 10:55:38 +02:00
|
|
|
thread.saves = new_saves<false>(nullptr);
|
2018-04-27 00:18:04 +02:00
|
|
|
else if (m_saves[thread.saves]->refcount > 1)
|
2017-10-03 04:54:43 +02:00
|
|
|
{
|
2018-04-27 00:18:04 +02:00
|
|
|
--m_saves[thread.saves]->refcount;
|
|
|
|
thread.saves = new_saves<true>(m_saves[thread.saves]->pos);
|
2017-10-03 04:54:43 +02:00
|
|
|
}
|
2018-11-04 22:17:50 +01:00
|
|
|
m_saves[thread.saves]->pos[inst.param] = pos;
|
2017-10-02 08:59:04 +02:00
|
|
|
break;
|
|
|
|
}
|
2017-11-25 11:14:15 +01:00
|
|
|
case CompiledRegex::Class:
|
2018-11-01 22:23:39 +01:00
|
|
|
if (pos == config.end)
|
2018-11-05 09:46:53 +01:00
|
|
|
return failed(thread);
|
2018-11-04 22:17:50 +01:00
|
|
|
return is_character_class(m_program.character_classes[inst.param], codepoint(pos, config)) ?
|
2018-11-05 09:46:53 +01:00
|
|
|
consumed(thread) : failed(thread);
|
2017-11-25 11:14:15 +01:00
|
|
|
case CompiledRegex::CharacterType:
|
2018-11-01 22:23:39 +01:00
|
|
|
if (pos == config.end)
|
2018-11-05 09:46:53 +01:00
|
|
|
return failed(thread);
|
2018-11-04 22:17:50 +01:00
|
|
|
return is_ctype((CharacterType)inst.param, codepoint(pos, config)) ?
|
2018-11-05 09:46:53 +01:00
|
|
|
consumed(thread) : failed(thread);
|
2017-10-02 08:59:04 +02:00
|
|
|
case CompiledRegex::LineStart:
|
2018-04-21 04:44:54 +02:00
|
|
|
if (not is_line_start(pos, config))
|
2018-11-05 09:46:53 +01:00
|
|
|
return failed(thread);
|
2017-10-02 08:59:04 +02:00
|
|
|
break;
|
|
|
|
case CompiledRegex::LineEnd:
|
2018-04-21 04:44:54 +02:00
|
|
|
if (not is_line_end(pos, config))
|
2018-11-05 09:46:53 +01:00
|
|
|
return failed(thread);
|
2017-10-02 08:59:04 +02:00
|
|
|
break;
|
|
|
|
case CompiledRegex::WordBoundary:
|
2018-04-21 04:44:54 +02:00
|
|
|
if (not is_word_boundary(pos, config))
|
2018-11-05 09:46:53 +01:00
|
|
|
return failed(thread);
|
2017-10-02 08:59:04 +02:00
|
|
|
break;
|
|
|
|
case CompiledRegex::NotWordBoundary:
|
2018-04-21 04:44:54 +02:00
|
|
|
if (is_word_boundary(pos, config))
|
2018-11-05 09:46:53 +01:00
|
|
|
return failed(thread);
|
2017-10-02 08:59:04 +02:00
|
|
|
break;
|
|
|
|
case CompiledRegex::SubjectBegin:
|
2018-11-01 22:23:39 +01:00
|
|
|
if (pos != config.subject_begin)
|
2018-11-05 09:46:53 +01:00
|
|
|
return failed(thread);
|
2017-10-02 08:59:04 +02:00
|
|
|
break;
|
|
|
|
case CompiledRegex::SubjectEnd:
|
2018-11-01 22:23:39 +01:00
|
|
|
if (pos != config.subject_end)
|
2018-11-05 09:46:53 +01:00
|
|
|
return failed(thread);
|
2017-10-02 08:59:04 +02:00
|
|
|
break;
|
|
|
|
case CompiledRegex::LookAhead:
|
|
|
|
case CompiledRegex::NegativeLookAhead:
|
2018-04-21 04:44:54 +02:00
|
|
|
if (lookaround<MatchDirection::Forward, false>(inst.param, pos, config) !=
|
2017-10-10 05:21:21 +02:00
|
|
|
(inst.op == CompiledRegex::LookAhead))
|
2018-11-05 09:46:53 +01:00
|
|
|
return failed(thread);
|
2017-10-10 05:21:21 +02:00
|
|
|
break;
|
|
|
|
case CompiledRegex::LookAhead_IgnoreCase:
|
|
|
|
case CompiledRegex::NegativeLookAhead_IgnoreCase:
|
2018-04-21 04:44:54 +02:00
|
|
|
if (lookaround<MatchDirection::Forward, true>(inst.param, pos, config) !=
|
2017-10-10 05:21:21 +02:00
|
|
|
(inst.op == CompiledRegex::LookAhead_IgnoreCase))
|
2018-11-05 09:46:53 +01:00
|
|
|
return failed(thread);
|
2017-10-02 08:59:04 +02:00
|
|
|
break;
|
|
|
|
case CompiledRegex::LookBehind:
|
|
|
|
case CompiledRegex::NegativeLookBehind:
|
2018-04-21 04:44:54 +02:00
|
|
|
if (lookaround<MatchDirection::Backward, false>(inst.param, pos, config) !=
|
2017-10-10 05:21:21 +02:00
|
|
|
(inst.op == CompiledRegex::LookBehind))
|
2018-11-05 09:46:53 +01:00
|
|
|
return failed(thread);
|
2017-10-10 05:21:21 +02:00
|
|
|
break;
|
|
|
|
case CompiledRegex::LookBehind_IgnoreCase:
|
|
|
|
case CompiledRegex::NegativeLookBehind_IgnoreCase:
|
2018-04-21 04:44:54 +02:00
|
|
|
if (lookaround<MatchDirection::Backward, true>(inst.param, pos, config) !=
|
2017-10-10 05:21:21 +02:00
|
|
|
(inst.op == CompiledRegex::LookBehind_IgnoreCase))
|
2018-11-05 09:46:53 +01:00
|
|
|
return failed(thread);
|
2017-10-02 08:59:04 +02:00
|
|
|
break;
|
2017-10-20 09:17:02 +02:00
|
|
|
case CompiledRegex::FindNextStart:
|
2018-11-05 09:46:53 +01:00
|
|
|
// search thread should by construction be the lowest priority thread
|
|
|
|
kak_assert(m_threads.current_is_empty());
|
|
|
|
if (not m_threads.next_is_empty())
|
|
|
|
return consumed(thread);
|
|
|
|
m_threads.push_next(thread);
|
|
|
|
m_find_next_start = true;
|
|
|
|
return;
|
2017-10-02 08:59:04 +02:00
|
|
|
case CompiledRegex::Match:
|
2018-11-05 09:46:53 +01:00
|
|
|
if ((pos != config.end and not (config.flags & RegexExecFlags::Search)) or
|
|
|
|
(config.flags & RegexExecFlags::NotInitialNull and pos == config.begin))
|
|
|
|
return failed(thread);
|
|
|
|
|
|
|
|
release_saves(m_captures);
|
|
|
|
m_captures = thread.saves;
|
|
|
|
m_found_match = true;
|
|
|
|
|
|
|
|
// remove lower priority threads
|
|
|
|
while (not m_threads.current_is_empty())
|
|
|
|
release_saves(m_threads.pop_current().saves);
|
|
|
|
return;
|
2017-10-02 08:59:04 +02:00
|
|
|
}
|
|
|
|
}
|
2018-11-05 09:46:53 +01:00
|
|
|
return failed(thread);
|
2017-10-02 08:59:04 +02:00
|
|
|
}
|
|
|
|
|
2018-11-04 22:17:50 +01:00
|
|
|
bool exec_program(Iterator pos, const ExecConfig& config)
|
2017-10-02 08:59:04 +02:00
|
|
|
{
|
2018-04-29 08:04:26 +02:00
|
|
|
kak_assert(m_threads.current_is_empty() and m_threads.next_is_empty());
|
2018-04-21 04:44:54 +02:00
|
|
|
release_saves(m_captures);
|
2018-04-27 00:18:04 +02:00
|
|
|
m_captures = -1;
|
2018-11-05 21:32:47 +01:00
|
|
|
m_threads.grow_ifn();
|
2018-04-29 08:04:26 +02:00
|
|
|
m_threads.push_current({static_cast<int16_t>(&config.instructions[0] - &m_program.instructions[0]), -1});
|
2017-12-01 12:57:02 +01:00
|
|
|
|
2018-11-04 22:17:50 +01:00
|
|
|
const auto& start_desc = forward ? m_program.forward_start_desc : m_program.backward_start_desc;
|
2017-10-03 13:07:44 +02:00
|
|
|
|
2018-11-05 11:54:29 +01:00
|
|
|
const bool any_match = config.flags & RegexExecFlags::AnyMatch;
|
2018-04-21 04:44:54 +02:00
|
|
|
uint16_t current_step = -1;
|
2018-11-05 09:46:53 +01:00
|
|
|
m_found_match = false;
|
2017-10-07 13:58:10 +02:00
|
|
|
while (true) // Iterate on all codepoints and once at the end
|
2017-10-07 13:27:06 +02:00
|
|
|
{
|
2018-04-21 04:44:54 +02:00
|
|
|
if (++current_step == 0)
|
2017-10-14 06:58:42 +02:00
|
|
|
{
|
|
|
|
// We wrapped, avoid potential collision on inst.last_step by resetting them
|
2018-04-21 04:44:54 +02:00
|
|
|
for (auto& inst : config.instructions)
|
2017-10-14 06:58:42 +02:00
|
|
|
inst.last_step = 0;
|
2018-04-21 04:44:54 +02:00
|
|
|
current_step = 1; // step 0 is never valid
|
2017-10-14 06:58:42 +02:00
|
|
|
}
|
|
|
|
|
2018-11-05 09:46:53 +01:00
|
|
|
m_find_next_start = false;
|
2018-04-29 08:04:26 +02:00
|
|
|
while (not m_threads.current_is_empty())
|
2018-11-05 09:46:53 +01:00
|
|
|
step_thread(pos, current_step, m_threads.pop_current(), config);
|
2017-10-20 10:55:38 +02:00
|
|
|
|
2018-04-29 08:04:26 +02:00
|
|
|
for (auto& thread : m_threads.next_threads())
|
2018-04-27 00:18:04 +02:00
|
|
|
m_program.instructions[thread.inst].scheduled = false;
|
2017-10-11 04:24:05 +02:00
|
|
|
|
2018-11-05 11:54:29 +01:00
|
|
|
if (pos == config.end or m_threads.next_is_empty() or (m_found_match and any_match))
|
2017-10-20 10:55:38 +02:00
|
|
|
{
|
2018-04-29 08:04:26 +02:00
|
|
|
for (auto& t : m_threads.next_threads())
|
2017-10-20 10:55:38 +02:00
|
|
|
release_saves(t.saves);
|
2018-04-29 08:04:26 +02:00
|
|
|
m_threads.clear_next();
|
2018-11-05 09:46:53 +01:00
|
|
|
return m_found_match;
|
2017-10-20 10:55:38 +02:00
|
|
|
}
|
2017-10-03 12:00:52 +02:00
|
|
|
|
2018-04-29 08:04:26 +02:00
|
|
|
m_threads.swap_next();
|
2018-11-04 22:17:50 +01:00
|
|
|
forward ? utf8::to_next(pos, config.subject_end)
|
|
|
|
: utf8::to_previous(pos, config.subject_begin);
|
2017-10-20 09:17:02 +02:00
|
|
|
|
2018-11-05 09:46:53 +01:00
|
|
|
if (m_find_next_start and start_desc)
|
2018-11-04 22:17:50 +01:00
|
|
|
to_next_start(pos, config, *start_desc);
|
2017-10-02 08:59:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-04 22:17:50 +01:00
|
|
|
void to_next_start(Iterator& start, const ExecConfig& config, const StartDesc& start_desc)
|
2017-10-06 07:40:27 +02:00
|
|
|
{
|
2018-11-04 22:17:50 +01:00
|
|
|
while (start != config.end)
|
2018-10-31 11:13:14 +01:00
|
|
|
{
|
2018-11-04 22:17:50 +01:00
|
|
|
const Codepoint cp = read_codepoint(start, config);
|
2018-10-31 11:13:14 +01:00
|
|
|
if (start_desc.map[(cp >= 0 and cp < StartDesc::count) ? cp : StartDesc::other])
|
|
|
|
{
|
2018-11-04 22:17:50 +01:00
|
|
|
forward ? utf8::to_previous(start, config.subject_begin)
|
|
|
|
: utf8::to_next(start, config.subject_end);
|
2018-10-31 11:13:14 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2017-10-06 07:40:27 +02:00
|
|
|
}
|
|
|
|
|
2017-10-10 05:21:21 +02:00
|
|
|
template<MatchDirection look_direction, bool ignore_case>
|
2018-11-04 22:17:50 +01:00
|
|
|
bool lookaround(uint32_t index, Iterator pos, const ExecConfig& config) const
|
2017-10-09 05:12:42 +02:00
|
|
|
{
|
2018-10-10 13:35:38 +02:00
|
|
|
using Lookaround = CompiledRegex::Lookaround;
|
|
|
|
|
2018-11-03 03:52:40 +01:00
|
|
|
if (look_direction == MatchDirection::Backward)
|
|
|
|
{
|
|
|
|
if (pos == config.subject_begin)
|
|
|
|
return m_program.lookarounds[index] == Lookaround::EndOfLookaround;
|
2018-11-04 22:17:50 +01:00
|
|
|
utf8::to_previous(pos, config.subject_begin);
|
2018-11-03 03:52:40 +01:00
|
|
|
}
|
|
|
|
|
2018-10-10 13:35:38 +02:00
|
|
|
for (auto it = m_program.lookarounds.begin() + index; *it != Lookaround::EndOfLookaround; ++it)
|
2017-10-09 05:12:42 +02:00
|
|
|
{
|
2018-11-03 03:52:40 +01:00
|
|
|
if (look_direction == MatchDirection::Forward and pos == config.subject_end)
|
2017-10-09 05:12:42 +02:00
|
|
|
return false;
|
2018-11-03 03:52:40 +01:00
|
|
|
|
2018-11-04 22:17:50 +01:00
|
|
|
Codepoint cp = utf8::codepoint(pos, config.subject_end);
|
2017-10-10 05:21:21 +02:00
|
|
|
if (ignore_case)
|
|
|
|
cp = to_lower(cp);
|
|
|
|
|
2018-10-10 13:35:38 +02:00
|
|
|
const Lookaround op = *it;
|
|
|
|
if (op == Lookaround::AnyChar)
|
2017-10-09 05:12:42 +02:00
|
|
|
{} // any character matches
|
2018-10-10 13:35:38 +02:00
|
|
|
else if (op == Lookaround::AnyCharExceptNewLine)
|
2018-06-24 12:13:35 +02:00
|
|
|
{
|
|
|
|
if (cp == '\n')
|
|
|
|
return false;
|
|
|
|
}
|
2018-10-10 13:35:38 +02:00
|
|
|
else if (op >= Lookaround::CharacterClass and op < Lookaround::CharacterType)
|
2017-11-25 11:14:15 +01:00
|
|
|
{
|
2018-10-10 13:35:38 +02:00
|
|
|
auto index = to_underlying(op) - to_underlying(Lookaround::CharacterClass);
|
|
|
|
if (not is_character_class(m_program.character_classes[index], cp))
|
2017-11-25 11:14:15 +01:00
|
|
|
return false;
|
|
|
|
}
|
2018-10-10 13:35:38 +02:00
|
|
|
else if (op >= Lookaround::CharacterType and op < Lookaround::OpEnd)
|
2017-10-09 05:20:05 +02:00
|
|
|
{
|
2018-10-10 13:35:38 +02:00
|
|
|
auto ctype = static_cast<CharacterType>(to_underlying(op) & 0xFF);
|
|
|
|
if (not is_ctype(ctype, cp))
|
2017-10-09 05:20:05 +02:00
|
|
|
return false;
|
|
|
|
}
|
2018-10-10 13:35:38 +02:00
|
|
|
else if (static_cast<Codepoint>(op) != cp)
|
2017-10-09 05:12:42 +02:00
|
|
|
return false;
|
|
|
|
|
2018-11-03 03:52:40 +01:00
|
|
|
if (look_direction == MatchDirection::Backward and pos == config.subject_begin)
|
|
|
|
return *++it == Lookaround::EndOfLookaround;
|
|
|
|
|
2018-11-04 22:17:50 +01:00
|
|
|
(look_direction == MatchDirection::Forward) ? utf8::to_next(pos, config.subject_end)
|
|
|
|
: utf8::to_previous(pos, config.subject_begin);
|
2017-10-09 05:12:42 +02:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-11-04 22:17:50 +01:00
|
|
|
static bool is_line_start(const Iterator& pos, const ExecConfig& config)
|
2017-10-02 08:59:04 +02:00
|
|
|
{
|
2018-11-01 22:23:39 +01:00
|
|
|
if (pos == config.subject_begin)
|
2018-04-21 04:44:54 +02:00
|
|
|
return not (config.flags & RegexExecFlags::NotBeginOfLine);
|
2018-11-04 22:17:50 +01:00
|
|
|
return utf8::codepoint(utf8::previous(pos, config.subject_begin), config.subject_end) == '\n';
|
2017-10-02 08:59:04 +02:00
|
|
|
}
|
|
|
|
|
2018-11-04 22:17:50 +01:00
|
|
|
static bool is_line_end(const Iterator& pos, const ExecConfig& config)
|
2017-10-02 08:59:04 +02:00
|
|
|
{
|
2018-11-01 22:23:39 +01:00
|
|
|
if (pos == config.subject_end)
|
2018-04-21 04:44:54 +02:00
|
|
|
return not (config.flags & RegexExecFlags::NotEndOfLine);
|
2018-11-04 22:17:50 +01:00
|
|
|
return utf8::codepoint(pos, config.subject_end) == '\n';
|
2017-10-02 08:59:04 +02:00
|
|
|
}
|
|
|
|
|
2018-11-04 22:17:50 +01:00
|
|
|
static bool is_word_boundary(const Iterator& pos, const ExecConfig& config)
|
2017-10-02 08:59:04 +02:00
|
|
|
{
|
2018-11-01 22:23:39 +01:00
|
|
|
if (pos == config.subject_begin)
|
2018-04-21 04:44:54 +02:00
|
|
|
return not (config.flags & RegexExecFlags::NotBeginOfWord);
|
2018-11-01 22:23:39 +01:00
|
|
|
if (pos == config.subject_end)
|
2018-04-21 04:44:54 +02:00
|
|
|
return not (config.flags & RegexExecFlags::NotEndOfWord);
|
2018-11-04 22:17:50 +01:00
|
|
|
return is_word(utf8::codepoint(utf8::previous(pos, config.subject_begin), config.subject_end)) !=
|
|
|
|
is_word(utf8::codepoint(pos, config.subject_end));
|
2017-10-02 08:59:04 +02:00
|
|
|
}
|
|
|
|
|
2018-11-04 22:17:50 +01:00
|
|
|
static Codepoint read_codepoint(Iterator& it, const ExecConfig& config)
|
2018-11-01 22:23:39 +01:00
|
|
|
{
|
2018-11-04 22:17:50 +01:00
|
|
|
if (forward)
|
|
|
|
return utf8::read_codepoint(it, config.subject_end);
|
2018-11-01 22:23:39 +01:00
|
|
|
else
|
2018-11-04 22:17:50 +01:00
|
|
|
{
|
|
|
|
utf8::to_previous(it, config.subject_begin);
|
|
|
|
return utf8::codepoint(it, config.subject_end);
|
|
|
|
}
|
2018-11-01 22:23:39 +01:00
|
|
|
}
|
2018-10-31 11:13:14 +01:00
|
|
|
|
2018-11-04 22:17:50 +01:00
|
|
|
static Codepoint codepoint(const Iterator& it, const ExecConfig& config)
|
|
|
|
{
|
|
|
|
return utf8::codepoint(forward ? it : utf8::previous(it, config.subject_begin),
|
|
|
|
config.subject_end);
|
|
|
|
}
|
2017-10-07 06:46:27 +02:00
|
|
|
|
2017-10-02 08:59:04 +02:00
|
|
|
const CompiledRegex& m_program;
|
|
|
|
|
2018-04-29 08:04:26 +02:00
|
|
|
struct DualThreadStack
|
|
|
|
{
|
2018-11-05 11:54:29 +01:00
|
|
|
DualThreadStack() = default;
|
|
|
|
DualThreadStack(const DualThreadStack&) = delete;
|
|
|
|
~DualThreadStack() { delete[] m_data; }
|
|
|
|
|
2018-04-29 08:04:26 +02:00
|
|
|
bool current_is_empty() const { return m_current == 0; }
|
|
|
|
bool next_is_empty() const { return m_next == m_capacity; }
|
|
|
|
|
2018-11-05 21:32:47 +01:00
|
|
|
void push_current(Thread thread) { kak_assert(m_current < m_next); m_data[m_current++] = thread; grow_ifn(); }
|
2018-04-29 08:04:26 +02:00
|
|
|
Thread pop_current() { kak_assert(m_current > 0); return m_data[--m_current]; }
|
|
|
|
|
2018-11-05 21:32:47 +01:00
|
|
|
void push_next(Thread thread) { kak_assert(m_current < m_next); m_data[--m_next] = thread; }
|
2018-04-29 08:04:26 +02:00
|
|
|
void clear_next() { m_next = m_capacity; }
|
2018-11-05 11:54:29 +01:00
|
|
|
ConstArrayView<Thread> next_threads() const { return { m_data + m_next, m_data + m_capacity }; }
|
2018-04-29 08:04:26 +02:00
|
|
|
|
|
|
|
void swap_next()
|
|
|
|
{
|
2018-11-05 11:54:29 +01:00
|
|
|
kak_assert(m_next < m_capacity);
|
|
|
|
do
|
|
|
|
{
|
|
|
|
m_data[m_current++] = m_data[m_next++];
|
|
|
|
}
|
|
|
|
while (m_next < m_capacity);
|
2018-04-29 08:04:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void grow_ifn()
|
|
|
|
{
|
|
|
|
if (m_current != m_next)
|
|
|
|
return;
|
|
|
|
const auto new_capacity = m_capacity ? m_capacity * 2 : 4;
|
|
|
|
Thread* new_data = new Thread[new_capacity];
|
2018-11-05 11:54:29 +01:00
|
|
|
std::copy(m_data, m_data + m_current, new_data);
|
2018-04-29 08:04:26 +02:00
|
|
|
const auto new_next = new_capacity - (m_capacity - m_next);
|
2018-11-05 11:54:29 +01:00
|
|
|
std::copy(m_data + m_next, m_data + m_capacity, new_data + new_next);
|
|
|
|
m_data = new_data;
|
2018-04-29 08:04:26 +02:00
|
|
|
m_capacity = new_capacity;
|
|
|
|
m_next = new_next;
|
|
|
|
}
|
|
|
|
|
2018-11-05 21:32:47 +01:00
|
|
|
private:
|
2018-11-05 11:54:29 +01:00
|
|
|
Thread* m_data = nullptr;
|
2018-10-08 03:43:03 +02:00
|
|
|
int32_t m_capacity = 0; // Maximum capacity should be 2*instruction count, so 65536
|
|
|
|
int32_t m_current = 0;
|
|
|
|
int32_t m_next = 0;
|
2018-04-29 08:04:26 +02:00
|
|
|
};
|
2017-10-02 08:59:04 +02:00
|
|
|
|
2018-11-04 22:17:50 +01:00
|
|
|
static constexpr bool forward = direction == MatchDirection::Forward;
|
|
|
|
|
2018-04-29 08:04:26 +02:00
|
|
|
DualThreadStack m_threads;
|
2017-10-15 03:23:57 +02:00
|
|
|
Vector<Saves*, MemoryDomain::Regex> m_saves;
|
2018-04-27 00:18:04 +02:00
|
|
|
int16_t m_first_free = -1;
|
|
|
|
int16_t m_captures = -1;
|
2018-11-05 09:46:53 +01:00
|
|
|
bool m_found_match = false;
|
|
|
|
bool m_find_next_start = false;
|
2017-10-02 08:59:04 +02:00
|
|
|
};
|
|
|
|
|
2017-09-26 09:44:30 +02:00
|
|
|
}
|
|
|
|
|
2017-09-17 10:50:53 +02:00
|
|
|
#endif // regex_impl_hh_INCLUDED
|