PieceTable: newline test from the line index, memchr rebuild; remove dead code

GetLine and LineColToByteOffset fetched each line's last byte (a piece
lookup, an allocation and a cache write) to see whether it was a newline;
the line index already says so: every line but the last ends at one.
LineColToByteOffset runs on every insert. RebuildLineIndex scans with
memchr instead of byte by byte.

Removed, having no callers: OptimizedSearch (built only into its own
test), UndoNodePool.h, SwapManager::RecordSplit/RecordJoin (replay still
reads SPLIT/JOIN), PieceTable::Reserve/Capacity and its capacity
constructor, Buffer's write-only render column, Editor's mode, no-kill,
dirtyex and repeatable flags and the UCount getter, and
RetryPolicy::Conservative.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
This commit is contained in:
Claude
2026-09-24 12:19:59 -07:00
committed by Kyle Isom
parent 6bc3a6e0af
commit 306a337516
15 changed files with 16 additions and 398 deletions
+2 -6
View File
@@ -347,7 +347,6 @@ Buffer::Buffer(const Buffer &other)
{ {
curx_ = other.curx_; curx_ = other.curx_;
cury_ = other.cury_; cury_ = other.cury_;
rx_ = other.rx_;
nrows_ = other.nrows_; nrows_ = other.nrows_;
rowoffs_ = other.rowoffs_; rowoffs_ = other.rowoffs_;
coloffs_ = other.coloffs_; coloffs_ = other.coloffs_;
@@ -401,7 +400,6 @@ Buffer::operator=(const Buffer &other)
return *this; return *this;
curx_ = other.curx_; curx_ = other.curx_;
cury_ = other.cury_; cury_ = other.cury_;
rx_ = other.rx_;
nrows_ = other.nrows_; nrows_ = other.nrows_;
rowoffs_ = other.rowoffs_; rowoffs_ = other.rowoffs_;
coloffs_ = other.coloffs_; coloffs_ = other.coloffs_;
@@ -450,7 +448,6 @@ Buffer::operator=(const Buffer &other)
Buffer::Buffer(Buffer &&other) noexcept Buffer::Buffer(Buffer &&other) noexcept
: curx_(other.curx_), : curx_(other.curx_),
cury_(other.cury_), cury_(other.cury_),
rx_(other.rx_),
nrows_(other.nrows_), nrows_(other.nrows_),
rowoffs_(other.rowoffs_), rowoffs_(other.rowoffs_),
coloffs_(other.coloffs_), coloffs_(other.coloffs_),
@@ -502,7 +499,6 @@ Buffer::operator=(Buffer &&other) noexcept
curx_ = other.curx_; curx_ = other.curx_;
cury_ = other.cury_; cury_ = other.cury_;
rx_ = other.rx_;
nrows_ = other.nrows_; nrows_ = other.nrows_;
rowoffs_ = other.rowoffs_; rowoffs_ = other.rowoffs_;
coloffs_ = other.coloffs_; coloffs_ = other.coloffs_;
@@ -593,7 +589,7 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
dirty_ = false; dirty_ = false;
// Reset cursor/viewport state // Reset cursor/viewport state
curx_ = cury_ = rx_ = 0; curx_ = cury_ = 0;
rowoffs_ = coloffs_ = 0; rowoffs_ = coloffs_ = 0;
mark_set_ = false; mark_set_ = false;
mark_curx_ = mark_cury_ = 0; mark_curx_ = mark_cury_ = 0;
@@ -687,7 +683,7 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
MarkContentChanged(); MarkContentChanged();
// Reset cursor/viewport state // Reset cursor/viewport state
curx_ = cury_ = rx_ = 0; curx_ = cury_ = 0;
rowoffs_ = coloffs_ = 0; rowoffs_ = coloffs_ = 0;
mark_set_ = false; mark_set_ = false;
mark_curx_ = mark_cury_ = 0; mark_curx_ = mark_cury_ = 0;
+1 -15
View File
@@ -5,7 +5,7 @@
* or scratch document and manages: * or scratch document and manages:
* *
* - Content storage: Uses PieceTable for efficient text operations * - Content storage: Uses PieceTable for efficient text operations
* - Cursor state: Current position (curx_, cury_), rendered column (rx_) * - Cursor state: Current position (curx_, cury_)
* - Viewport: Scroll offsets (rowoffs_, coloffs_) for display * - Viewport: Scroll offsets (rowoffs_, coloffs_) for display
* - File backing: Optional association with a file on disk * - File backing: Optional association with a file on disk
* - Undo/Redo: Integrated UndoSystem for operation history * - Undo/Redo: Integrated UndoSystem for operation history
@@ -17,7 +17,6 @@
* *
* 1. Cursor coordinates: * 1. Cursor coordinates:
* - (curx_, cury_): Logical character position in the document * - (curx_, cury_): Logical character position in the document
* - rx_: Rendered column accounting for tab expansion
* *
* 2. File backing: * 2. File backing:
* - Buffers can be file-backed (associated with a path) or scratch (unnamed) * - Buffers can be file-backed (associated with a path) or scratch (unnamed)
@@ -119,12 +118,6 @@ public:
} }
[[nodiscard]] std::size_t Rx() const
{
return rx_;
}
[[nodiscard]] std::size_t Nrows() const [[nodiscard]] std::size_t Nrows() const
{ {
return content_LineCount_(); return content_LineCount_();
@@ -394,12 +387,6 @@ public:
} }
void SetRenderX(const std::size_t rx)
{
rx_ = rx;
}
void SetOffsets(const std::size_t row, const std::size_t col) void SetOffsets(const std::size_t row, const std::size_t col)
{ {
rowoffs_ = row; rowoffs_ = row;
@@ -740,7 +727,6 @@ private:
// State mirroring original C struct (without undo_tree) // State mirroring original C struct (without undo_tree)
std::size_t curx_ = 0, cury_ = 0; // cursor position in characters std::size_t curx_ = 0, cury_ = 0; // cursor position in characters
std::size_t rx_ = 0; // render x (tabs expanded)
std::size_t nrows_ = 0; // number of rows std::size_t nrows_ = 0; // number of rows
std::size_t rowoffs_ = 0, coloffs_ = 0; // viewport offsets std::size_t rowoffs_ = 0, coloffs_ = 0; // viewport offsets
mutable std::vector<Line> rows_; // materialized cache of rows (without trailing newlines) mutable std::vector<Line> rows_; // materialized cache of rows (without trailing newlines)
-2
View File
@@ -389,7 +389,6 @@ if (BUILD_TESTS)
tests/test_swap_cleanup2.cc tests/test_swap_cleanup2.cc
tests/test_swap_git_editor.cc tests/test_swap_git_editor.cc
tests/test_piece_table.cc tests/test_piece_table.cc
tests/test_search.cc
tests/test_search_replace_flow.cc tests/test_search_replace_flow.cc
tests/test_reflow_paragraph.cc tests/test_reflow_paragraph.cc
tests/test_reflow_indented_bullets.cc tests/test_reflow_indented_bullets.cc
@@ -418,7 +417,6 @@ if (BUILD_TESTS)
SearchHighlight.cc SearchHighlight.cc
KKeymap.cc KKeymap.cc
SwapRecorder.h SwapRecorder.h
OptimizedSearch.cc
UndoNode.cc UndoNode.cc
UndoTree.cc UndoTree.cc
UndoSystem.cc UndoSystem.cc
-2
View File
@@ -265,7 +265,6 @@ ensure_cursor_visible(const Editor &ed, Buffer &buf)
} }
buf.SetOffsets(rowoffs, coloffs); buf.SetOffsets(rowoffs, coloffs);
buf.SetRenderX(rx);
} }
@@ -3051,7 +3050,6 @@ cmd_show_help(CommandContext &ctx)
b.SetDirty(false); b.SetDirty(false);
b.SetCursor(0, 0); b.SetCursor(0, 0);
b.SetOffsets(0, 0); b.SetOffsets(0, 0);
b.SetRenderX(0);
}; };
if (help_index != static_cast<std::size_t>(-1)) { if (help_index != static_cast<std::size_t>(-1)) {
-4
View File
@@ -728,15 +728,11 @@ void
Editor::Reset() Editor::Reset()
{ {
rows_ = cols_ = 0; rows_ = cols_ = 0;
mode_ = 0;
kill_ = 0; kill_ = 0;
no_kill_ = 0;
dirtyex_ = 0;
msg_.clear(); msg_.clear();
msgtm_ = 0; msgtm_ = 0;
uarg_ = 0; uarg_ = 0;
ucount_ = 0; ucount_ = 0;
repeatable_ = false;
quit_requested_ = false; quit_requested_ = false;
quit_confirm_pending_ = false; quit_confirm_pending_ = false;
// Reset close-confirm/save state // Reset close-confirm/save state
-60
View File
@@ -79,19 +79,6 @@ public:
} }
// Mode and flags (mirroring legacy fields)
void SetMode(int m)
{
mode_ = m;
}
[[nodiscard]] int Mode() const
{
return mode_;
}
void SetKillChain(bool on) void SetKillChain(bool on)
{ {
kill_ = on ? 1 : 0; kill_ = on ? 1 : 0;
@@ -104,18 +91,6 @@ public:
} }
void SetNoKill(bool on)
{
no_kill_ = on ? 1 : 0;
}
[[nodiscard]] bool NoKill() const
{
return no_kill_ != 0;
}
// Kill ring API // Kill ring API
void KillRingClear() void KillRingClear()
{ {
@@ -164,18 +139,6 @@ public:
} }
void SetDirtyEx(int d)
{
dirtyex_ = d;
}
[[nodiscard]] int DirtyEx() const
{
return dirtyex_;
}
void SetUniversalArg(int uarg, int ucount) void SetUniversalArg(int uarg, int ucount)
{ {
uarg_ = uarg; uarg_ = uarg;
@@ -189,12 +152,6 @@ public:
} }
[[nodiscard]] int UCount() const
{
return ucount_;
}
// --- Universal argument control (C-u) --- // --- Universal argument control (C-u) ---
// Begin or extend a universal argument (like ke's uarg_start) // Begin or extend a universal argument (like ke's uarg_start)
void UArgStart(); void UArgStart();
@@ -209,19 +166,6 @@ public:
// If no universal argument active, returns 1. // If no universal argument active, returns 1.
int UArgGet(); int UArgGet();
// Repeatable command flag: input layer can mark the next command as repeatable
void SetRepeatable(bool on)
{
repeatable_ = on;
}
[[nodiscard]] bool Repeatable() const
{
return repeatable_;
}
// Status message storage. Rendering is renderer-dependent; the editor // Status message storage. Rendering is renderer-dependent; the editor
// merely stores the current message and its timestamp. // merely stores the current message and its timestamp.
void SetStatus(const std::string &message); void SetStatus(const std::string &message);
@@ -666,14 +610,10 @@ private:
}; };
std::size_t rows_ = 0, cols_ = 0; std::size_t rows_ = 0, cols_ = 0;
int mode_ = 0;
int kill_ = 0; // KILL CHAIN int kill_ = 0; // KILL CHAIN
int no_kill_ = 0; // don't kill in delete_row
int dirtyex_ = 0;
std::string msg_; std::string msg_;
std::time_t msgtm_ = 0; std::time_t msgtm_ = 0;
int uarg_ = 0, ucount_ = 0; // C-u support int uarg_ = 0, ucount_ = 0; // C-u support
bool repeatable_ = false; // whether the next command is repeatable
std::vector<Buffer> buffers_; std::vector<Buffer> buffers_;
std::vector<Buffer> *shared_buffers_ = nullptr; // if set, use this instead of buffers_ std::vector<Buffer> *shared_buffers_ = nullptr; // if set, use this instead of buffers_
-5
View File
@@ -52,11 +52,6 @@ struct RetryPolicy {
} }
// Conservative policy for non-critical operations: fewer attempts, slower retries
static RetryPolicy Conservative()
{
return RetryPolicy{2, std::chrono::milliseconds(200), 2.5, std::chrono::milliseconds(10000)};
}
}; };
// Retry a function with exponential backoff for transient errors // Retry a function with exponential backoff for transient errors
-79
View File
@@ -1,79 +0,0 @@
#include "OptimizedSearch.h"
#include <algorithm>
void
OptimizedSearch::build_bad_char(const std::string &pattern)
{
if (pattern == last_pat_)
return;
last_pat_ = pattern;
std::fill(bad_char_.begin(), bad_char_.end(), -1);
for (std::size_t i = 0; i < pattern.size(); ++i) {
bad_char_[static_cast<unsigned char>(pattern[i])] = static_cast<int>(i);
}
}
std::size_t
OptimizedSearch::find_first(const std::string &text, const std::string &pattern, std::size_t start)
{
const std::size_t n = text.size();
const std::size_t m = pattern.size();
if (m == 0)
return start <= n ? start : std::string::npos;
if (m > n || start >= n)
return std::string::npos;
build_bad_char(pattern);
std::size_t s = start;
while (s <= n - m) {
std::size_t j = m;
while (j > 0 && pattern[j - 1] == text[s + j - 1]) {
--j;
}
if (j == 0) {
return s; // match found
}
unsigned char badc = static_cast<unsigned char>(text[s + j - 1]);
int bcidx = bad_char_[badc];
std::size_t shift = (j - 1 > static_cast<std::size_t>(bcidx))
? (j - 1 - static_cast<std::size_t>(bcidx))
: 1;
s += shift;
}
return std::string::npos;
}
std::vector<std::size_t>
OptimizedSearch::find_all(const std::string &text, const std::string &pattern, std::size_t start)
{
std::vector<std::size_t> res;
const std::size_t n = text.size();
const std::size_t m = pattern.size();
if (m == 0)
return res;
if (m > n || start >= n)
return res;
build_bad_char(pattern);
std::size_t s = start;
while (s <= n - m) {
std::size_t j = m;
while (j > 0 && pattern[j - 1] == text[s + j - 1]) {
--j;
}
if (j == 0) {
res.push_back(s);
s += m; // non-overlapping
continue;
}
unsigned char badc = static_cast<unsigned char>(text[s + j - 1]);
int bcidx = bad_char_[badc];
std::size_t shift = (j - 1 > static_cast<std::size_t>(bcidx))
? (j - 1 - static_cast<std::size_t>(bcidx))
: 1;
s += shift;
}
return res;
}
-23
View File
@@ -1,23 +0,0 @@
// OptimizedSearch.h - Boyer–Moore (bad character) based substring search
#pragma once
#include <array>
#include <cstddef>
#include <string>
#include <vector>
class OptimizedSearch {
public:
OptimizedSearch() = default;
// Find first occurrence at or after start. Returns npos if not found.
std::size_t find_first(const std::string &text, const std::string &pattern, std::size_t start = 0);
// Find all non-overlapping matches at or after start. Returns starting indices.
std::vector<std::size_t> find_all(const std::string &text, const std::string &pattern, std::size_t start = 0);
private:
std::array<int, 256> bad_char_{};
std::string last_pat_;
void build_bad_char(const std::string &pattern);
};
+10 -34
View File
@@ -10,13 +10,6 @@
PieceTable::PieceTable() = default; PieceTable::PieceTable() = default;
PieceTable::PieceTable(const std::size_t initialCapacity)
{
add_.reserve(initialCapacity);
materialized_.reserve(initialCapacity);
}
PieceTable::PieceTable(const std::size_t initialCapacity, PieceTable::PieceTable(const std::size_t initialCapacity,
const std::size_t piece_limit, const std::size_t piece_limit,
const std::size_t small_piece_threshold, const std::size_t small_piece_threshold,
@@ -109,14 +102,6 @@ PieceTable::operator=(PieceTable &&other) noexcept
PieceTable::~PieceTable() = default; PieceTable::~PieceTable() = default;
void
PieceTable::Reserve(const std::size_t newCapacity)
{
add_.reserve(newCapacity);
materialized_.reserve(newCapacity);
}
// Setter to allow tuning consolidation heuristics // Setter to allow tuning consolidation heuristics
void void
PieceTable::SetConsolidationParams(const std::size_t piece_limit, PieceTable::SetConsolidationParams(const std::size_t piece_limit,
@@ -417,12 +402,10 @@ PieceTable::RebuildLineIndex() const
const std::string &src = pc.src == Source::Original ? original_ : add_; const std::string &src = pc.src == Source::Original ? original_ : add_;
const char *base = src.data() + static_cast<std::ptrdiff_t>(pc.start); const char *base = src.data() + static_cast<std::ptrdiff_t>(pc.start);
for (std::size_t j = 0; j < pc.len; ++j) { const char *end = base + pc.len;
if (base[j] == '\n') { // The next line starts after each newline.
// next line starts after the newline for (const char *p = base; (p = static_cast<const char *>(std::memchr(p, '\n', end - p))) != nullptr; ++p)
line_index_.push_back(pos + j + 1); line_index_.push_back(pos + static_cast<std::size_t>(p - base) + 1);
}
}
pos += pc.len; pos += pc.len;
} }
@@ -800,14 +783,10 @@ PieceTable::GetLine(std::size_t line_num) const
auto [start, end] = GetLineRange(line_num); auto [start, end] = GetLineRange(line_num);
if (end < start) if (end < start)
return std::string(); return std::string();
// Trim trailing '\n' // Every line but the last ends at a newline (each newline starts an
if (end > start) { // index entry); leave it out.
// To check last char, we can get it via GetRange of len 1 at end-1 without materializing whole if (end > start && line_num + 1 < line_index_.size())
std::string last = GetRange(end - 1, 1);
if (!last.empty() && last[0] == '\n') {
end -= 1; end -= 1;
}
}
return GetRange(start, end - start); return GetRange(start, end - start);
} }
@@ -837,13 +816,10 @@ PieceTable::LineColToByteOffset(std::size_t row, std::size_t col) const
return total_size_; return total_size_;
std::size_t start = line_index_[row]; std::size_t start = line_index_[row];
std::size_t end = (row + 1 < line_index_.size()) ? line_index_[row + 1] : total_size_; std::size_t end = (row + 1 < line_index_.size()) ? line_index_[row + 1] : total_size_;
// Clamp col to line length excluding trailing newline // Clamp col to the line's length without its newline (every line but
if (end > start) { // the last has one).
std::string last = GetRange(end - 1, 1); if (end > start && row + 1 < line_index_.size())
if (!last.empty() && last[0] == '\n') {
end -= 1; end -= 1;
}
}
std::size_t target = start + std::min(col, end - start); std::size_t target = start + std::min(col, end - start);
return target; return target;
} }
-12
View File
@@ -52,8 +52,6 @@ class PieceTable {
public: public:
PieceTable(); PieceTable();
explicit PieceTable(std::size_t initialCapacity);
// Advanced constructor allowing configuration of consolidation heuristics // Advanced constructor allowing configuration of consolidation heuristics
PieceTable(std::size_t initialCapacity, PieceTable(std::size_t initialCapacity,
std::size_t piece_limit, std::size_t piece_limit,
@@ -70,9 +68,6 @@ public:
~PieceTable(); ~PieceTable();
// Public API mirrors GapBuffer
void Reserve(std::size_t newCapacity);
void AppendChar(char c); void AppendChar(char c);
void Append(const char *s, std::size_t len); void Append(const char *s, std::size_t len);
@@ -109,13 +104,6 @@ public:
} }
[[nodiscard]] std::size_t Capacity() const
{
// Capacity for piece table isn't directly meaningful; report materialized capacity
return materialized_.capacity();
}
// ===== New buffer-wide API (Phase 1) ===== // ===== New buffer-wide API (Phase 1) =====
// Byte-based editing operations // Byte-based editing operations
void Insert(std::size_t byte_offset, const char *text, std::size_t len); void Insert(std::size_t byte_offset, const char *text, std::size_t len);
-41
View File
@@ -1202,47 +1202,6 @@ SwapManager::RecordDelete(Buffer &buf, int row, int col, std::size_t len)
} }
void
SwapManager::RecordSplit(Buffer &buf, int row, int col)
{
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(&buf);
if (it == journals_.end() || it->second.suspended)
return;
}
Pending p;
p.buf = &buf;
p.type = SwapRecType::SPLIT;
// payload v1: [encver u8=1][row u32][col u32]
p.payload.push_back(1);
put_le32(p.payload, static_cast<std::uint32_t>(std::max(0, row)));
put_le32(p.payload, static_cast<std::uint32_t>(std::max(0, col)));
enqueue(std::move(p));
maybe_request_checkpoint(buf, 1);
}
void
SwapManager::RecordJoin(Buffer &buf, int row)
{
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(&buf);
if (it == journals_.end() || it->second.suspended)
return;
}
Pending p;
p.buf = &buf;
p.type = SwapRecType::JOIN;
// payload v1: [encver u8=1][row u32]
p.payload.push_back(1);
put_le32(p.payload, static_cast<std::uint32_t>(std::max(0, row)));
enqueue(std::move(p));
maybe_request_checkpoint(buf, 1);
}
void void
SwapManager::maybe_request_checkpoint(Buffer &buf, const std::size_t approx_edit_bytes) SwapManager::maybe_request_checkpoint(Buffer &buf, const std::size_t approx_edit_bytes)
{ {
+1 -4
View File
@@ -24,6 +24,7 @@ namespace kte {
enum class SwapRecType : std::uint8_t { enum class SwapRecType : std::uint8_t {
INS = 1, INS = 1,
DEL = 2, DEL = 2,
// SPLIT and JOIN are no longer written; replay still reads them.
SPLIT = 3, SPLIT = 3,
JOIN = 4, JOIN = 4,
META = 0xF0, META = 0xF0,
@@ -213,10 +214,6 @@ private:
void RecordDelete(Buffer &buf, int row, int col, std::size_t len); void RecordDelete(Buffer &buf, int row, int col, std::size_t len);
void RecordSplit(Buffer &buf, int row, int col);
void RecordJoin(Buffer &buf, int row);
void RecordCheckpoint(Buffer &buf, bool urgent_flush); void RecordCheckpoint(Buffer &buf, bool urgent_flush);
void maybe_request_checkpoint(Buffer &buf, std::size_t approx_edit_bytes); void maybe_request_checkpoint(Buffer &buf, std::size_t approx_edit_bytes);
-63
View File
@@ -1,63 +0,0 @@
#pragma once
#include <stack>
#include <vector>
#include <memory>
#include "UndoNode.h"
// Pool allocator for UndoNode to eliminate frequent malloc/free.
// Uses fixed-size blocks to keep node addresses stable.
class UndoNodePool {
public:
explicit UndoNodePool(std::size_t block_size = 64)
: block_size_(block_size) {}
UndoNode *acquire()
{
if (available_.empty())
allocate_block();
auto *node = available_.top();
available_.pop();
// Node comes zeroed; ensure links are reset
node->text.clear();
node->parent = nullptr;
node->child = nullptr;
node->next = nullptr;
node->row = node->col = 0;
node->type = UndoType{};
return node;
}
void release(UndoNode *node)
{
if (!node)
return;
// Clear heavy fields to free memory held by strings
node->text.clear();
node->parent = nullptr;
node->child = nullptr;
node->next = nullptr;
node->row = node->col = 0;
node->type = UndoType{};
available_.push(node);
}
private:
void allocate_block()
{
// allocate a new block; keep ownership so memory stays valid
std::unique_ptr<UndoNode[]> block(new UndoNode[block_size_]);
UndoNode *base = block.get();
blocks_.push_back(std::move(block));
for (std::size_t i = 0; i < block_size_; ++i) {
// ensure the node is reset; rely on default constructor/zero init
available_.push(&base[i]);
}
}
std::size_t block_size_;
std::vector<std::unique_ptr<UndoNode[]> > blocks_;
std::stack<UndoNode *> available_;
};
-46
View File
@@ -1,46 +0,0 @@
#include "Test.h"
#include "OptimizedSearch.h"
#include <string>
#include <vector>
static std::vector<std::size_t>
ref_find_all(const std::string &text, const std::string &pat)
{
std::vector<std::size_t> res;
if (pat.empty())
return res;
std::size_t from = 0;
while (true) {
auto p = text.find(pat, from);
if (p == std::string::npos)
break;
res.push_back(p);
from = p + pat.size();
}
return res;
}
TEST(OptimizedSearch_basic_cases)
{
OptimizedSearch os;
struct Case {
std::string text;
std::string pat;
} cases[] = {
{"", ""},
{"", "a"},
{"a", ""},
{"a", "a"},
{"aaaaa", "aa"},
{"hello world", "world"},
{"abcabcabc", "abc"},
{"the quick brown fox", "fox"},
};
for (auto &c: cases) {
auto got = os.find_all(c.text, c.pat, 0);
auto ref = ref_find_all(c.text, c.pat);
ASSERT_EQ(got, ref);
}
}