From 71c1c9e50b8bae7ea604a3a359d7e7bd8688a61a Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Fri, 5 Dec 2025 16:04:23 -0800 Subject: [PATCH] Remove GapBuffer and associated legacy implementation. - Deleted `GapBuffer` class and its API implementations. - Removed `AppendBuffer` selector and conditional `KTE_USE_PIECE_TABLE` macros. - Eliminated legacy support in buffer APIs, file I/O, benchmarks, and correctness tests. - Updated guidelines and comments to reflect PieceTable as the default and only buffer backend. --- .junie/guidelines.md | 31 ++-- AppendBuffer.h | 12 -- Buffer.cc | 297 ++++++---------------------------- Buffer.h | 30 +--- CMakeLists.txt | 23 +-- GapBuffer.cc | 204 ------------------------ GapBuffer.h | 76 --------- bench/BufferBench.cc | 206 ------------------------ bench/PerformanceSuite.cc | 318 ------------------------------------- test_buffer_correctness.cc | 102 ------------ 10 files changed, 71 insertions(+), 1228 deletions(-) delete mode 100644 AppendBuffer.h delete mode 100644 GapBuffer.cc delete mode 100644 GapBuffer.h delete mode 100644 bench/BufferBench.cc delete mode 100644 bench/PerformanceSuite.cc delete mode 100644 test_buffer_correctness.cc diff --git a/.junie/guidelines.md b/.junie/guidelines.md index 0d3ad62..4dfd5a7 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -1,28 +1,35 @@ # Project Guidelines -kte is Kyle's Text Editor — a simple, fast text editor written in C++17. It -replaces the earlier C implementation, ke (see the ke manual in `docs/ke.md`). The -design draws inspiration from Antirez' kilo, with keybindings rooted in the +kte is Kyle's Text Editor — a simple, fast text editor written in C++17. +It +replaces the earlier C implementation, ke (see the ke manual in +`docs/ke.md`). The +design draws inspiration from Antirez' kilo, with keybindings rooted in +the WordStar/VDE family and emacs. The spiritual parent is `mg(1)`. -These guidelines summarize the goals, interfaces, key operations, and current +These guidelines summarize the goals, interfaces, key operations, and +current development practices for kte. ## Goals - Keep the core small, fast, and understandable. -- Provide an ncurses-based terminal-first editing experience, with an additional ImGui GUI. +- Provide an ncurses-based terminal-first editing experience, with an + additional ImGui GUI. - Preserve familiar keybindings from ke while modernizing the internals. -- Favor simple data structures (e.g., piece table) and incremental evolution. +- Favor simple data structures (e.g., piece table) and incremental + evolution. Project entry point: `main.cpp` ## Core Components (current codebase) - Buffer: editing model and file I/O (`Buffer.h/.cpp`). -- GapBuffer: editable in-memory text representation (`GapBuffer.h/.cpp`). -- PieceTable: experimental/alternative representation (`PieceTable.h/.cpp`). -- InputHandler: interface for handling text input (`InputHandler.h/`), along +- PieceTable: editable in-memory text representation ( + `PieceTable.h/.cpp`). +- InputHandler: interface for handling text input (`InputHandler.h/`), + along with `TerminalInputHandler` (ncurses-based) and `GUIInputHandler`. - Renderer: interface for rendering text (`Renderer.h`), along with `TerminalRenderer` (ncurses-based) and `GUIRenderer`. @@ -38,11 +45,13 @@ The file `docs/ke.md` contains the canonical reference for keybindings. - C++ standard: C++17. - Keep dependencies minimal. -- Prefer small, focused changes that preserve ke’s UX unless explicitly changing +- Prefer small, focused changes that preserve ke’s UX unless explicitly + changing behavior. ## References -- Previous editor manual: `ke.md` (canonical keybinding/spec reference for now). +- Previous editor manual: `ke.md` (canonical keybinding/spec reference + for now). - Inspiration: kilo, WordStar/VDE, emacs, `mg(1)`. diff --git a/AppendBuffer.h b/AppendBuffer.h deleted file mode 100644 index 231a79e..0000000 --- a/AppendBuffer.h +++ /dev/null @@ -1,12 +0,0 @@ -/* - * AppendBuffer.h - selector header to choose GapBuffer or PieceTable - */ -#pragma once - -#ifdef KTE_USE_PIECE_TABLE -#include "PieceTable.h" -using AppendBuffer = PieceTable; -#else -#include "GapBuffer.h" -using AppendBuffer = GapBuffer; -#endif \ No newline at end of file diff --git a/Buffer.cc b/Buffer.cc index db3f98f..f9369b0 100644 --- a/Buffer.cc +++ b/Buffer.cc @@ -30,24 +30,22 @@ Buffer::Buffer(const std::string &path) // Copy constructor/assignment: perform a deep copy of core fields; reinitialize undo for the new buffer. Buffer::Buffer(const Buffer &other) { - curx_ = other.curx_; - cury_ = other.cury_; - rx_ = other.rx_; - nrows_ = other.nrows_; - rowoffs_ = other.rowoffs_; - coloffs_ = other.coloffs_; - rows_ = other.rows_; -#ifdef KTE_USE_BUFFER_PIECE_TABLE + curx_ = other.curx_; + cury_ = other.cury_; + rx_ = other.rx_; + nrows_ = other.nrows_; + rowoffs_ = other.rowoffs_; + coloffs_ = other.coloffs_; + rows_ = other.rows_; content_ = other.content_; rows_cache_dirty_ = other.rows_cache_dirty_; -#endif - filename_ = other.filename_; - is_file_backed_ = other.is_file_backed_; - dirty_ = other.dirty_; - read_only_ = other.read_only_; - mark_set_ = other.mark_set_; - mark_curx_ = other.mark_curx_; - mark_cury_ = other.mark_cury_; + filename_ = other.filename_; + is_file_backed_ = other.is_file_backed_; + dirty_ = other.dirty_; + read_only_ = other.read_only_; + mark_set_ = other.mark_set_; + mark_curx_ = other.mark_curx_; + mark_cury_ = other.mark_cury_; // Copy syntax/highlighting flags version_ = other.version_; syntax_enabled_ = other.syntax_enabled_; @@ -82,27 +80,25 @@ Buffer::operator=(const Buffer &other) { if (this == &other) return *this; - curx_ = other.curx_; - cury_ = other.cury_; - rx_ = other.rx_; - nrows_ = other.nrows_; - rowoffs_ = other.rowoffs_; - coloffs_ = other.coloffs_; - rows_ = other.rows_; -#ifdef KTE_USE_BUFFER_PIECE_TABLE + curx_ = other.curx_; + cury_ = other.cury_; + rx_ = other.rx_; + nrows_ = other.nrows_; + rowoffs_ = other.rowoffs_; + coloffs_ = other.coloffs_; + rows_ = other.rows_; content_ = other.content_; rows_cache_dirty_ = other.rows_cache_dirty_; -#endif - filename_ = other.filename_; - is_file_backed_ = other.is_file_backed_; - dirty_ = other.dirty_; - read_only_ = other.read_only_; - mark_set_ = other.mark_set_; - mark_curx_ = other.mark_curx_; - mark_cury_ = other.mark_cury_; - version_ = other.version_; - syntax_enabled_ = other.syntax_enabled_; - filetype_ = other.filetype_; + filename_ = other.filename_; + is_file_backed_ = other.is_file_backed_; + dirty_ = other.dirty_; + read_only_ = other.read_only_; + mark_set_ = other.mark_set_; + mark_curx_ = other.mark_curx_; + mark_cury_ = other.mark_cury_; + version_ = other.version_; + syntax_enabled_ = other.syntax_enabled_; + filetype_ = other.filetype_; // Recreate undo system for this instance undo_tree_ = std::make_unique(); undo_sys_ = std::make_unique(*this, *undo_tree_); @@ -146,14 +142,12 @@ Buffer::Buffer(Buffer &&other) noexcept undo_sys_(std::move(other.undo_sys_)) { // Move syntax/highlighting state - version_ = other.version_; - syntax_enabled_ = other.syntax_enabled_; - filetype_ = std::move(other.filetype_); - highlighter_ = std::move(other.highlighter_); -#ifdef KTE_USE_BUFFER_PIECE_TABLE + version_ = other.version_; + syntax_enabled_ = other.syntax_enabled_; + filetype_ = std::move(other.filetype_); + highlighter_ = std::move(other.highlighter_); content_ = std::move(other.content_); rows_cache_dirty_ = other.rows_cache_dirty_; -#endif // Update UndoSystem's buffer reference to point to this object if (undo_sys_) { undo_sys_->UpdateBufferReference(*this); @@ -186,15 +180,12 @@ Buffer::operator=(Buffer &&other) noexcept undo_sys_ = std::move(other.undo_sys_); // Move syntax/highlighting state - version_ = other.version_; - syntax_enabled_ = other.syntax_enabled_; - filetype_ = std::move(other.filetype_); - highlighter_ = std::move(other.highlighter_); - -#ifdef KTE_USE_BUFFER_PIECE_TABLE + version_ = other.version_; + syntax_enabled_ = other.syntax_enabled_; + filetype_ = std::move(other.filetype_); + highlighter_ = std::move(other.highlighter_); content_ = std::move(other.content_); rows_cache_dirty_ = other.rows_cache_dirty_; -#endif // Update UndoSystem's buffer reference to point to this object if (undo_sys_) { undo_sys_->UpdateBufferReference(*this); @@ -246,11 +237,9 @@ Buffer::OpenFromFile(const std::string &path, std::string &err) mark_set_ = false; mark_curx_ = mark_cury_ = 0; -#ifdef KTE_USE_BUFFER_PIECE_TABLE // Empty PieceTable content_.Clear(); rows_cache_dirty_ = true; -#endif return true; } @@ -261,7 +250,6 @@ Buffer::OpenFromFile(const std::string &path, std::string &err) return false; } -#ifdef KTE_USE_BUFFER_PIECE_TABLE // Read entire file into PieceTable as-is std::string data; in.seekg(0, std::ios::end); @@ -275,53 +263,10 @@ Buffer::OpenFromFile(const std::string &path, std::string &err) if (!data.empty()) content_.Append(data.data(), data.size()); rows_cache_dirty_ = true; - nrows_ = 0; // not used under adapter -#else - // Detect if file ends with a newline so we can preserve a final empty line - // in our in-memory representation (mg-style semantics). - bool ends_with_nl = false; - { - in.seekg(0, std::ios::end); - std::streamoff sz = in.tellg(); - if (sz > 0) { - in.seekg(-1, std::ios::end); - char last = 0; - in.read(&last, 1); - ends_with_nl = (last == '\n'); - } else { - in.clear(); - } - // Rewind to start for line-by-line read - in.clear(); - in.seekg(0, std::ios::beg); - } - - rows_.clear(); - std::string line; - while (std::getline(in, line)) { - // std::getline strips the '\n', keep raw line content only - // Handle potential Windows CRLF: strip trailing '\r' - if (!line.empty() && line.back() == '\r') { - line.pop_back(); - } - rows_.emplace_back(line); - } - - // If the file ended with a newline and we didn't already get an - // empty final row from getline (e.g., when the last textual line - // had content followed by '\n'), append an empty row to represent - // the cursor position past the last newline. - if (ends_with_nl) { - if (rows_.empty() || !rows_.back().empty()) { - rows_.emplace_back(std::string()); - } - } - - nrows_ = rows_.size(); -#endif - filename_ = norm; - is_file_backed_ = true; - dirty_ = false; + nrows_ = 0; // not used under PieceTable + filename_ = norm; + is_file_backed_ = true; + dirty_ = false; // Reset/initialize undo system for this loaded file if (!undo_tree_) @@ -353,22 +298,10 @@ Buffer::Save(std::string &err) const err = "Failed to open for write: " + filename_; return false; } -#ifdef KTE_USE_BUFFER_PIECE_TABLE const char *d = content_.Data(); std::size_t n = content_.Size(); if (d && n) out.write(d, static_cast(n)); -#else - for (std::size_t i = 0; i < rows_.size(); ++i) { - const char *d = rows_[i].Data(); - std::size_t n = rows_[i].Size(); - if (d && n) - out.write(d, static_cast(n)); - if (i + 1 < rows_.size()) { - out.put('\n'); - } - } -#endif if (!out.good()) { err = "Write error"; return false; @@ -407,24 +340,12 @@ Buffer::SaveAs(const std::string &path, std::string &err) err = "Failed to open for write: " + out_path; return false; } -#ifdef KTE_USE_BUFFER_PIECE_TABLE { const char *d = content_.Data(); std::size_t n = content_.Size(); if (d && n) out.write(d, static_cast(n)); } -#else - for (std::size_t i = 0; i < rows_.size(); ++i) { - const char *d = rows_[i].Data(); - std::size_t n = rows_[i].Size(); - if (d && n) - out.write(d, static_cast(n)); - if (i + 1 < rows_.size()) { - out.put('\n'); - } - } -#endif if (!out.good()) { err = "Write error"; return false; @@ -445,11 +366,7 @@ Buffer::AsString() const if (this->Dirty()) { ss << "*"; } -#ifdef KTE_USE_BUFFER_PIECE_TABLE ss << ">: " << content_.LineCount() << " lines"; -#else - ss << ">: " << rows_.size() << " lines"; -#endif return ss.str(); } @@ -458,7 +375,6 @@ Buffer::AsString() const void Buffer::insert_text(int row, int col, std::string_view text) { -#ifdef KTE_USE_BUFFER_PIECE_TABLE if (row < 0) row = 0; if (col < 0) @@ -469,47 +385,9 @@ Buffer::insert_text(int row, int col, std::string_view text) content_.Insert(off, text.data(), text.size()); rows_cache_dirty_ = true; } - return; -#else - if (row < 0) - row = 0; - if (static_cast(row) > rows_.size()) - row = static_cast(rows_.size()); - if (rows_.empty()) - rows_.emplace_back(""); - if (static_cast(row) >= rows_.size()) - rows_.emplace_back(""); - - auto y = static_cast(row); - auto x = static_cast(col); - if (x > rows_[y].size()) { - x = rows_[y].size(); - } - - std::string remain(text); - while (true) { - auto pos = remain.find('\n'); - if (pos == std::string::npos) { - rows_[y].insert(x, remain); - break; - } - // Insert up to newline - std::string seg = remain.substr(0, pos); - rows_[y].insert(x, seg); - x += seg.size(); - // Split line at x - std::string tail = rows_[y].substr(x); - rows_[y].erase(x); - rows_.insert(rows_.begin() + static_cast(y + 1), Line(tail)); - y += 1; - x = 0; - remain.erase(0, pos + 1); - } - // Do not set dirty here; UndoSystem will manage state/dirty externally -#endif } -#ifdef KTE_USE_BUFFER_PIECE_TABLE + // ===== Adapter helpers for PieceTable-backed Buffer ===== void Buffer::ensure_rows_cache() const @@ -527,18 +405,17 @@ Buffer::ensure_rows_cache() const rows_cache_dirty_ = false; } + std::size_t Buffer::content_LineCount_() const { return content_.LineCount(); } -#endif void Buffer::delete_text(int row, int col, std::size_t len) { -#ifdef KTE_USE_BUFFER_PIECE_TABLE if (len == 0) return; if (row < 0) @@ -608,48 +485,12 @@ Buffer::delete_text(int row, int col, std::size_t len) content_.Delete(start, end - start); rows_cache_dirty_ = true; } - return; -#else - if (rows_.empty() || len == 0) - return; - if (row < 0) - row = 0; - if (static_cast(row) >= rows_.size()) - return; - const auto y = static_cast(row); - const auto x = std::min(static_cast(col), rows_[y].size()); - - std::size_t remaining = len; - while (remaining > 0 && y < rows_.size()) { - auto &line = rows_[y]; - const std::size_t in_line = std::min(remaining, line.size() - std::min(x, line.size())); - if (x < line.size() && in_line > 0) { - line.erase(x, in_line); - remaining -= in_line; - } - if (remaining == 0) - break; - // If at or beyond end of line and there is a next line, join it (deleting the implied '\n') - if (y + 1 < rows_.size()) { - line += rows_[y + 1]; - rows_.erase(rows_.begin() + static_cast(y + 1)); - // deleting the newline consumes one virtual character - if (remaining > 0) { - // Treat the newline as one deletion unit if len spans it - // We already joined, so nothing else to do here. - } - } else { - break; - } - } -#endif } void Buffer::split_line(int row, const int col) { -#ifdef KTE_USE_BUFFER_PIECE_TABLE if (row < 0) row = 0; if (col < 0) @@ -659,28 +500,12 @@ Buffer::split_line(int row, const int col) const char nl = '\n'; content_.Insert(off, &nl, 1); rows_cache_dirty_ = true; - return; -#else - if (row < 0) { - row = 0; - } - - if (static_cast(row) >= rows_.size()) { - rows_.resize(static_cast(row) + 1); - } - const auto y = static_cast(row); - const auto x = std::min(static_cast(col), rows_[y].size()); - const auto tail = rows_[y].substr(x); - rows_[y].erase(x); - rows_.insert(rows_.begin() + static_cast(y + 1), Line(tail)); -#endif } void Buffer::join_lines(int row) { -#ifdef KTE_USE_BUFFER_PIECE_TABLE if (row < 0) row = 0; std::size_t r = static_cast(row); @@ -691,27 +516,12 @@ Buffer::join_lines(int row) // end_of_line now equals line end (clamped before newline). The newline should be exactly at this position. content_.Delete(end_of_line, 1); rows_cache_dirty_ = true; - return; -#else - if (row < 0) { - row = 0; - } - - const auto y = static_cast(row); - if (y + 1 >= rows_.size()) { - return; - } - - rows_[y] += rows_[y + 1]; - rows_.erase(rows_.begin() + static_cast(y + 1)); -#endif } void Buffer::insert_row(int row, const std::string_view text) { -#ifdef KTE_USE_BUFFER_PIECE_TABLE if (row < 0) row = 0; std::size_t off = content_.LineColToByteOffset(static_cast(row), 0); @@ -720,21 +530,12 @@ Buffer::insert_row(int row, const std::string_view text) const char nl = '\n'; content_.Insert(off + text.size(), &nl, 1); rows_cache_dirty_ = true; - return; -#else - if (row < 0) - row = 0; - if (static_cast(row) > rows_.size()) - row = static_cast(rows_.size()); - rows_.insert(rows_.begin() + row, Line(std::string(text))); -#endif } void Buffer::delete_row(int row) { -#ifdef KTE_USE_BUFFER_PIECE_TABLE if (row < 0) row = 0; std::size_t r = static_cast(row); @@ -747,14 +548,6 @@ Buffer::delete_row(int row) std::size_t end = range.second; content_.Delete(start, end - start); rows_cache_dirty_ = true; - return; -#else - if (row < 0) - row = 0; - if (static_cast(row) >= rows_.size()) - return; - rows_.erase(rows_.begin() + row); -#endif } diff --git a/Buffer.h b/Buffer.h index af4c242..0a7370d 100644 --- a/Buffer.h +++ b/Buffer.h @@ -9,10 +9,7 @@ #include #include -#include "AppendBuffer.h" -#ifdef KTE_USE_BUFFER_PIECE_TABLE #include "PieceTable.h" -#endif #include "UndoSystem.h" #include #include @@ -66,11 +63,7 @@ public: [[nodiscard]] std::size_t Nrows() const { -#ifdef KTE_USE_BUFFER_PIECE_TABLE return content_LineCount_(); -#else - return nrows_; -#endif } @@ -86,7 +79,7 @@ public: } - // Line wrapper backed by AppendBuffer (GapBuffer/PieceTable) + // Line wrapper backed by PieceTable class Line { public: Line() = default; @@ -183,7 +176,7 @@ public: // minimal find() to support search within a line [[nodiscard]] std::size_t find(const std::string &needle, const std::size_t pos = 0) const { - // Materialize to std::string for now; Line is backed by AppendBuffer + // Materialize to std::string for now; Line is backed by PieceTable const auto s = static_cast(*this); return s.find(needle, pos); } @@ -256,29 +249,21 @@ public: } - AppendBuffer buf_; + PieceTable buf_; }; [[nodiscard]] const std::vector &Rows() const { -#ifdef KTE_USE_BUFFER_PIECE_TABLE ensure_rows_cache(); return rows_; -#else - return rows_; -#endif } [[nodiscard]] std::vector &Rows() { -#ifdef KTE_USE_BUFFER_PIECE_TABLE ensure_rows_cache(); return rows_; -#else - return rows_; -#endif } @@ -477,13 +462,8 @@ private: std::size_t rx_ = 0; // render x (tabs expanded) std::size_t nrows_ = 0; // number of rows std::size_t rowoffs_ = 0, coloffs_ = 0; // viewport offsets -#ifdef KTE_USE_BUFFER_PIECE_TABLE mutable std::vector rows_; // materialized cache of rows (without trailing newlines) -#else - std::vector rows_; // buffer rows (without trailing newlines) -#endif -#ifdef KTE_USE_BUFFER_PIECE_TABLE - // When using the adapter, PieceTable is the source of truth. + // PieceTable is the source of truth. PieceTable content_{}; mutable bool rows_cache_dirty_ = true; // invalidate on edits / I/O @@ -492,7 +472,7 @@ private: // Helper to query content_.LineCount() while keeping header minimal std::size_t content_LineCount_() const; -#endif + std::string filename_; bool is_file_backed_ = false; bool dirty_ = false; diff --git a/CMakeLists.txt b/CMakeLists.txt index a9b130a..03638d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,15 +4,13 @@ project(kte) include(GNUInstallDirs) set(CMAKE_CXX_STANDARD 20) -set(KTE_VERSION "1.4.1") +set(KTE_VERSION "1.4.2") # Default to terminal-only build to avoid SDL/OpenGL dependency by default. # Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available. set(BUILD_GUI ON CACHE BOOL "Enable building the graphical version.") set(KTE_USE_QT OFF CACHE BOOL "Build the QT frontend instead of ImGui.") set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.") -option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON) -option(KTE_USE_BUFFER_PIECE_TABLE "Use PieceTable inside Buffer adapter (Phase 2)" OFF) set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI") option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF) option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF) @@ -129,7 +127,6 @@ if (BUILD_GUI) endif () set(COMMON_SOURCES - GapBuffer.cc PieceTable.cc Buffer.cc Editor.cc @@ -214,11 +211,9 @@ set(FONT_HEADERS ) set(COMMON_HEADERS - GapBuffer.h PieceTable.h Buffer.h Editor.h - AppendBuffer.h Command.h HelpText.h KKeymap.h @@ -271,12 +266,6 @@ add_executable(kte ${COMMON_HEADERS} ) -if (KTE_USE_PIECE_TABLE) - target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1) -endif () -if (KTE_USE_BUFFER_PIECE_TABLE) - target_compile_definitions(kte PRIVATE KTE_USE_BUFFER_PIECE_TABLE=1) -endif () if (KTE_UNDO_DEBUG) target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1) endif () @@ -310,13 +299,6 @@ if (BUILD_TESTS) ${COMMON_HEADERS} ) - if (KTE_USE_PIECE_TABLE) - target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1) - endif () - if (KTE_USE_BUFFER_PIECE_TABLE) - target_compile_definitions(test_undo PRIVATE KTE_USE_BUFFER_PIECE_TABLE=1) - endif () - if (KTE_UNDO_DEBUG) target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1) endif () @@ -364,9 +346,6 @@ if (${BUILD_GUI}) if (KTE_UNDO_DEBUG) target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1) endif () - if (KTE_USE_BUFFER_PIECE_TABLE) - target_compile_definitions(kge PRIVATE KTE_USE_BUFFER_PIECE_TABLE=1) - endif () if (KTE_USE_QT) target_link_libraries(kge ${CURSES_LIBRARIES} Qt6::Widgets) else () diff --git a/GapBuffer.cc b/GapBuffer.cc deleted file mode 100644 index 7acaa46..0000000 --- a/GapBuffer.cc +++ /dev/null @@ -1,204 +0,0 @@ -#include -#include -#include - -#include "GapBuffer.h" - - -GapBuffer::GapBuffer() = default; - - -GapBuffer::GapBuffer(std::size_t initialCapacity) - : buffer_(nullptr), size_(0), capacity_(0) -{ - if (initialCapacity > 0) { - Reserve(initialCapacity); - } -} - - -GapBuffer::GapBuffer(const GapBuffer &other) - : buffer_(nullptr), size_(0), capacity_(0) -{ - if (other.capacity_ > 0) { - Reserve(other.capacity_); - if (other.size_ > 0) { - std::memcpy(buffer_, other.buffer_, other.size_); - size_ = other.size_; - } - setTerminator(); - } -} - - -GapBuffer & -GapBuffer::operator=(const GapBuffer &other) -{ - if (this == &other) - return *this; - if (other.capacity_ > capacity_) { - Reserve(other.capacity_); - } - if (other.size_ > 0) { - std::memcpy(buffer_, other.buffer_, other.size_); - } - size_ = other.size_; - setTerminator(); - return *this; -} - - -GapBuffer::GapBuffer(GapBuffer &&other) noexcept - : buffer_(other.buffer_), size_(other.size_), capacity_(other.capacity_) -{ - other.buffer_ = nullptr; - other.size_ = 0; - other.capacity_ = 0; -} - - -GapBuffer & -GapBuffer::operator=(GapBuffer &&other) noexcept -{ - if (this == &other) - return *this; - delete[] buffer_; - buffer_ = other.buffer_; - size_ = other.size_; - capacity_ = other.capacity_; - other.buffer_ = nullptr; - other.size_ = 0; - other.capacity_ = 0; - return *this; -} - - -GapBuffer::~GapBuffer() -{ - delete[] buffer_; -} - - -void -GapBuffer::Reserve(const std::size_t newCapacity) -{ - if (newCapacity <= capacity_) [[likely]] - return; - // Allocate space for terminator as well - char *nb = new char[newCapacity + 1]; - if (size_ > 0 && buffer_) { - std::memcpy(nb, buffer_, size_); - } - delete[] buffer_; - buffer_ = nb; - capacity_ = newCapacity; - setTerminator(); -} - - -void -GapBuffer::AppendChar(const char c) -{ - ensureCapacityFor(1); - buffer_[size_++] = c; - setTerminator(); -} - - -void -GapBuffer::Append(const char *s, const std::size_t len) -{ - if (!s || len == 0) [[unlikely]] - return; - ensureCapacityFor(len); - std::memcpy(buffer_ + size_, s, len); - size_ += len; - setTerminator(); -} - - -void -GapBuffer::Append(const GapBuffer &other) -{ - if (other.size_ == 0) - return; - Append(other.buffer_, other.size_); -} - - -void -GapBuffer::PrependChar(char c) -{ - ensureCapacityFor(1); - // shift right by 1 - if (size_ > 0) [[likely]] { - std::memmove(buffer_ + 1, buffer_, size_); - } - buffer_[0] = c; - ++size_; - setTerminator(); -} - - -void -GapBuffer::Prepend(const char *s, std::size_t len) -{ - if (!s || len == 0) [[unlikely]] - return; - ensureCapacityFor(len); - if (size_ > 0) [[likely]] { - std::memmove(buffer_ + len, buffer_, size_); - } - std::memcpy(buffer_, s, len); - size_ += len; - setTerminator(); -} - - -void -GapBuffer::Prepend(const GapBuffer &other) -{ - if (other.size_ == 0) - return; - Prepend(other.buffer_, other.size_); -} - - -void -GapBuffer::Clear() -{ - size_ = 0; - setTerminator(); -} - - -void -GapBuffer::ensureCapacityFor(std::size_t delta) -{ - if (capacity_ - size_ >= delta) [[likely]] - return; - auto required = size_ + delta; - Reserve(growCapacity(capacity_, required)); -} - - -std::size_t -GapBuffer::growCapacity(std::size_t current, std::size_t required) -{ - // geometric growth, at least required - std::size_t newCap = current ? current : 8; - while (newCap < required) - newCap = newCap + (newCap >> 1); // 1.5x growth - return newCap; -} - - -void -GapBuffer::setTerminator() const -{ - if (!buffer_) { - return; - } - - buffer_[size_] = '\0'; -} \ No newline at end of file diff --git a/GapBuffer.h b/GapBuffer.h deleted file mode 100644 index c8f7630..0000000 --- a/GapBuffer.h +++ /dev/null @@ -1,76 +0,0 @@ -/* - * GapBuffer.h - C++ replacement for abuf append/prepend buffer utilities - */ -#pragma once -#include - - -class GapBuffer { -public: - GapBuffer(); - - explicit GapBuffer(std::size_t initialCapacity); - - GapBuffer(const GapBuffer &other); - - GapBuffer &operator=(const GapBuffer &other); - - GapBuffer(GapBuffer &&other) noexcept; - - GapBuffer &operator=(GapBuffer &&other) noexcept; - - ~GapBuffer(); - - void Reserve(std::size_t newCapacity); - - - void AppendChar(char c); - - void Append(const char *s, std::size_t len); - - void Append(const GapBuffer &other); - - void PrependChar(char c); - - void Prepend(const char *s, std::size_t len); - - void Prepend(const GapBuffer &other); - - // Content management - void Clear(); - - // Accessors - char *Data() - { - return buffer_; - } - - - [[nodiscard]] const char *Data() const - { - return buffer_; - } - - - [[nodiscard]] std::size_t Size() const - { - return size_; - } - - - [[nodiscard]] std::size_t Capacity() const - { - return capacity_; - } - -private: - void ensureCapacityFor(std::size_t delta); - - static std::size_t growCapacity(std::size_t current, std::size_t required); - - void setTerminator() const; - - char *buffer_ = nullptr; - std::size_t size_ = 0; // number of valid bytes (excluding terminator) - std::size_t capacity_ = 0; // capacity of buffer_ excluding space for terminator -}; \ No newline at end of file diff --git a/bench/BufferBench.cc b/bench/BufferBench.cc deleted file mode 100644 index 10e3737..0000000 --- a/bench/BufferBench.cc +++ /dev/null @@ -1,206 +0,0 @@ -/* - * BufferBench.cc - microbenchmarks for GapBuffer and PieceTable - * - * This benchmark exercises the public APIs shared by both structures as used - * in Buffer::Line: Reserve, AppendChar, Append, PrependChar, Prepend, Clear. - * - * Run examples: - * ./kte_bench_buffer # defaults - * ./kte_bench_buffer 200000 8 4096 # N=200k, rounds=8, chunk=4096 - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "GapBuffer.h" -#include "PieceTable.h" - -using clock_t = std::chrono::steady_clock; -using us = std::chrono::microseconds; - -struct Result { - std::string name; - std::string scenario; - double micros = 0.0; - std::size_t bytes = 0; -}; - - -static void -print_header() -{ - std::cout << std::left << std::setw(14) << "Structure" - << std::left << std::setw(18) << "Scenario" - << std::right << std::setw(12) << "time(us)" - << std::right << std::setw(14) << "bytes" - << std::right << std::setw(14) << "MB/s" - << "\n"; - std::cout << std::string(72, '-') << "\n"; -} - - -static void -print_row(const Result &r) -{ - double mb = r.bytes / (1024.0 * 1024.0); - double mbps = (r.micros > 0.0) ? (mb / (r.micros / 1'000'000.0)) : 0.0; - std::cout << std::left << std::setw(14) << r.name - << std::left << std::setw(18) << r.scenario - << std::right << std::setw(12) << std::fixed << std::setprecision(2) << r.micros - << std::right << std::setw(14) << r.bytes - << std::right << std::setw(14) << std::fixed << std::setprecision(2) << mbps - << "\n"; -} - - -template -Result -bench_sequential_append(std::size_t N, int rounds) -{ - Result r; - r.name = typeid(Buf).name(); - r.scenario = "seq_append"; - const char c = 'x'; - auto start = clock_t::now(); - std::size_t bytes = 0; - for (int t = 0; t < rounds; ++t) { - Buf b; - b.Reserve(N); - for (std::size_t i = 0; i < N; ++i) { - b.AppendChar(c); - } - bytes += N; - } - auto end = clock_t::now(); - r.micros = std::chrono::duration_cast(end - start).count(); - r.bytes = bytes; - return r; -} - - -template -Result -bench_sequential_prepend(std::size_t N, int rounds) -{ - Result r; - r.name = typeid(Buf).name(); - r.scenario = "seq_prepend"; - const char c = 'x'; - auto start = clock_t::now(); - std::size_t bytes = 0; - for (int t = 0; t < rounds; ++t) { - Buf b; - b.Reserve(N); - for (std::size_t i = 0; i < N; ++i) { - b.PrependChar(c); - } - bytes += N; - } - auto end = clock_t::now(); - r.micros = std::chrono::duration_cast(end - start).count(); - r.bytes = bytes; - return r; -} - - -template -Result -bench_chunk_append(std::size_t N, std::size_t chunk, int rounds) -{ - Result r; - r.name = typeid(Buf).name(); - r.scenario = "chunk_append"; - std::string payload(chunk, 'y'); - auto start = clock_t::now(); - std::size_t bytes = 0; - for (int t = 0; t < rounds; ++t) { - Buf b; - b.Reserve(N); - std::size_t written = 0; - while (written < N) { - std::size_t now = std::min(chunk, N - written); - b.Append(payload.data(), now); - written += now; - } - bytes += N; - } - auto end = clock_t::now(); - r.micros = std::chrono::duration_cast(end - start).count(); - r.bytes = bytes; - return r; -} - - -template -Result -bench_mixed(std::size_t N, std::size_t chunk, int rounds) -{ - Result r; - r.name = typeid(Buf).name(); - r.scenario = "mixed"; - std::string payload(chunk, 'z'); - auto start = clock_t::now(); - std::size_t bytes = 0; - for (int t = 0; t < rounds; ++t) { - Buf b; - b.Reserve(N); - std::size_t written = 0; - while (written < N) { - // alternate append/prepend with small chunks - std::size_t now = std::min(chunk, N - written); - if ((written / chunk) % 2 == 0) { - b.Append(payload.data(), now); - } else { - b.Prepend(payload.data(), now); - } - written += now; - } - bytes += N; - } - auto end = clock_t::now(); - r.micros = std::chrono::duration_cast(end - start).count(); - r.bytes = bytes; - return r; -} - - -int -main(int argc, char **argv) -{ - // Parameters - std::size_t N = 100'000; // bytes per round - int rounds = 5; // iterations - std::size_t chunk = 1024; // chunk size for chunked scenarios - if (argc >= 2) - N = static_cast(std::stoull(argv[1])); - if (argc >= 3) - rounds = std::stoi(argv[2]); - if (argc >= 4) - chunk = static_cast(std::stoull(argv[3])); - - std::cout << "KTE Buffer Microbenchmarks" << "\n"; - std::cout << "N=" << N << ", rounds=" << rounds << ", chunk=" << chunk << "\n\n"; - - print_header(); - - // Run for GapBuffer - print_row(bench_sequential_append(N, rounds)); - print_row(bench_sequential_prepend(N, rounds)); - print_row(bench_chunk_append(N, chunk, rounds)); - print_row(bench_mixed(N, chunk, rounds)); - - // Run for PieceTable - print_row(bench_sequential_append(N, rounds)); - print_row(bench_sequential_prepend(N, rounds)); - print_row(bench_chunk_append(N, chunk, rounds)); - print_row(bench_mixed(N, chunk, rounds)); - - return 0; -} \ No newline at end of file diff --git a/bench/PerformanceSuite.cc b/bench/PerformanceSuite.cc deleted file mode 100644 index 2517584..0000000 --- a/bench/PerformanceSuite.cc +++ /dev/null @@ -1,318 +0,0 @@ -/* - * PerformanceSuite.cc - broader performance and verification benchmarks - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "GapBuffer.h" -#include "PieceTable.h" -#include "OptimizedSearch.h" - -using clock_t = std::chrono::steady_clock; -using us = std::chrono::microseconds; - -namespace { -struct Stat { - double micros{0.0}; - std::size_t bytes{0}; - std::size_t ops{0}; -}; - - -static void -print_header(const std::string &title) -{ - std::cout << "\n" << title << "\n"; - std::cout << std::left << std::setw(18) << "Case" - << std::left << std::setw(18) << "Type" - << std::right << std::setw(12) << "time(us)" - << std::right << std::setw(14) << "bytes" - << std::right << std::setw(14) << "ops/s" - << std::right << std::setw(14) << "MB/s" - << "\n"; - std::cout << std::string(90, '-') << "\n"; -} - - -static void -print_row(const std::string &caseName, const std::string &typeName, const Stat &s) -{ - double mb = s.bytes / (1024.0 * 1024.0); - double sec = s.micros / 1'000'000.0; - double mbps = sec > 0 ? (mb / sec) : 0.0; - double opss = sec > 0 ? (static_cast(s.ops) / sec) : 0.0; - std::cout << std::left << std::setw(18) << caseName - << std::left << std::setw(18) << typeName - << std::right << std::setw(12) << std::fixed << std::setprecision(2) << s.micros - << std::right << std::setw(14) << s.bytes - << std::right << std::setw(14) << std::fixed << std::setprecision(2) << opss - << std::right << std::setw(14) << std::fixed << std::setprecision(2) << mbps - << "\n"; -} -} // namespace - -class PerformanceSuite { -public: - void benchmarkBufferOperations(std::size_t N, int rounds, std::size_t chunk) - { - print_header("Buffer Operations"); - run_buffer_case("append_char", N, rounds, chunk, [&](auto &b, std::size_t count) { - for (std::size_t i = 0; i < count; ++i) - b.AppendChar('a'); - }); - run_buffer_case("prepend_char", N, rounds, chunk, [&](auto &b, std::size_t count) { - for (std::size_t i = 0; i < count; ++i) - b.PrependChar('a'); - }); - run_buffer_case("chunk_mix", N, rounds, chunk, [&](auto &b, std::size_t) { - std::string payload(chunk, 'x'); - std::size_t written = 0; - while (written < N) { - std::size_t now = std::min(chunk, N - written); - if (((written / chunk) & 1) == 0) - b.Append(payload.data(), now); - else - b.Prepend(payload.data(), now); - written += now; - } - }); - run_buffer_case("append_char", N, rounds, chunk, [&](auto &b, std::size_t count) { - for (std::size_t i = 0; i < count; ++i) - b.AppendChar('a'); - }); - run_buffer_case("prepend_char", N, rounds, chunk, [&](auto &b, std::size_t count) { - for (std::size_t i = 0; i < count; ++i) - b.PrependChar('a'); - }); - run_buffer_case("chunk_mix", N, rounds, chunk, [&](auto &b, std::size_t) { - std::string payload(chunk, 'x'); - std::size_t written = 0; - while (written < N) { - std::size_t now = std::min(chunk, N - written); - if (((written / chunk) & 1) == 0) - b.Append(payload.data(), now); - else - b.Prepend(payload.data(), now); - written += now; - } - }); - } - - - void benchmarkSearchOperations(std::size_t textLen, std::size_t patLen, int rounds) - { - print_header("Search Operations"); - std::mt19937_64 rng(0xC0FFEE); - std::uniform_int_distribution dist('a', 'z'); - std::string text(textLen, '\0'); - for (auto &ch: text) - ch = static_cast(dist(rng)); - std::string pattern(patLen, '\0'); - for (auto &ch: pattern) - ch = static_cast(dist(rng)); - - // Ensure at least one hit - if (textLen >= patLen && patLen > 0) { - std::size_t pos = textLen / 2; - std::memcpy(&text[pos], pattern.data(), patLen); - } - - // OptimizedSearch find_all vs std::string reference - OptimizedSearch os; - Stat s{}; - auto start = clock_t::now(); - std::size_t matches = 0; - std::size_t bytesScanned = 0; - for (int r = 0; r < rounds; ++r) { - auto hits = os.find_all(text, pattern, 0); - matches += hits.size(); - bytesScanned += text.size(); - // Verify with reference - std::vector ref; - std::size_t from = 0; - while (true) { - auto p = text.find(pattern, from); - if (p == std::string::npos) - break; - ref.push_back(p); - from = p + (patLen ? patLen : 1); - } - assert(ref == hits); - } - auto end = clock_t::now(); - s.micros = std::chrono::duration_cast(end - start).count(); - s.bytes = bytesScanned; - s.ops = matches; - print_row("find_all", "OptimizedSearch", s); - } - - - void benchmarkMemoryAllocation(std::size_t N, int rounds) - { - print_header("Memory Allocation (allocations during editing)"); - // Measure number of allocations by simulating editing patterns. - auto run_session = [&](auto &&buffer) { - // alternate small appends and prepends - const std::size_t chunk = 32; - std::string payload(chunk, 'q'); - for (int r = 0; r < rounds; ++r) { - buffer.Clear(); - for (std::size_t i = 0; i < N; i += chunk) - buffer.Append(payload.data(), std::min(chunk, N - i)); - for (std::size_t i = 0; i < N / 2; i += chunk) - buffer.Prepend(payload.data(), std::min(chunk, N / 2 - i)); - } - }; - - // Local allocation counters for this TU via overriding operators - reset_alloc_counters(); - GapBuffer gb; - run_session(gb); - auto gap_allocs = current_allocs(); - print_row("edit_session", "GapBuffer", Stat{ - 0.0, static_cast(gap_allocs.bytes), - static_cast(gap_allocs.count) - }); - - reset_alloc_counters(); - PieceTable pt; - run_session(pt); - auto pt_allocs = current_allocs(); - print_row("edit_session", "PieceTable", Stat{ - 0.0, static_cast(pt_allocs.bytes), - static_cast(pt_allocs.count) - }); - } - -private: - template - void run_buffer_case(const std::string &caseName, std::size_t N, int rounds, std::size_t chunk, Fn fn) - { - Stat s{}; - auto start = clock_t::now(); - std::size_t bytes = 0; - std::size_t ops = 0; - for (int t = 0; t < rounds; ++t) { - Buf b; - b.Reserve(N); - fn(b, N); - // compare to reference string where possible (only for append_char/prepend_char) - bytes += N; - ops += N / (chunk ? chunk : 1); - } - auto end = clock_t::now(); - s.micros = std::chrono::duration_cast(end - start).count(); - s.bytes = bytes; - s.ops = ops; - print_row(caseName, typeid(Buf).name(), s); - } - - - // Simple global allocation tracking for this TU - struct AllocStats { - std::uint64_t count{0}; - std::uint64_t bytes{0}; - }; - - - static AllocStats &alloc_stats() - { - static AllocStats s; - return s; - } - - - static void reset_alloc_counters() - { - alloc_stats() = {}; - } - - - static AllocStats current_allocs() - { - return alloc_stats(); - } - - - // Friend global new/delete defined below - friend void *operator new(std::size_t sz) noexcept(false); - - friend void operator delete(void *p) noexcept; - - friend void *operator new[](std::size_t sz) noexcept(false); - - friend void operator delete[](void *p) noexcept; -}; - -// Override new/delete only in this translation unit to track allocations made here -void * -operator new(std::size_t sz) noexcept(false) -{ - auto &s = PerformanceSuite::alloc_stats(); - s.count++; - s.bytes += sz; - if (void *p = std::malloc(sz)) - return p; - throw std::bad_alloc(); -} - - -void -operator delete(void *p) noexcept -{ - std::free(p); -} - - -void * -operator new[](std::size_t sz) noexcept(false) -{ - auto &s = PerformanceSuite::alloc_stats(); - s.count++; - s.bytes += sz; - if (void *p = std::malloc(sz)) - return p; - throw std::bad_alloc(); -} - - -void -operator delete[](void *p) noexcept -{ - std::free(p); -} - - -int -main(int argc, char **argv) -{ - std::size_t N = 200'000; // bytes per round for buffer cases - int rounds = 3; - std::size_t chunk = 1024; - if (argc >= 2) - N = static_cast(std::stoull(argv[1])); - if (argc >= 3) - rounds = std::stoi(argv[2]); - if (argc >= 4) - chunk = static_cast(std::stoull(argv[3])); - - std::cout << "KTE Performance Suite" << "\n"; - std::cout << "N=" << N << ", rounds=" << rounds << ", chunk=" << chunk << "\n"; - - PerformanceSuite suite; - suite.benchmarkBufferOperations(N, rounds, chunk); - suite.benchmarkSearchOperations(1'000'000, 16, rounds); - suite.benchmarkMemoryAllocation(N, rounds); - return 0; -} \ No newline at end of file diff --git a/test_buffer_correctness.cc b/test_buffer_correctness.cc deleted file mode 100644 index c2937c0..0000000 --- a/test_buffer_correctness.cc +++ /dev/null @@ -1,102 +0,0 @@ -// Simple buffer correctness tests comparing GapBuffer and PieceTable to std::string -#include -#include -#include -#include -#include -#include - -#include "GapBuffer.h" -#include "PieceTable.h" - - -template -static void -check_equals(const Buf &b, const std::string &ref) -{ - assert(b.Size() == ref.size()); - if (b.Size() == 0) - return; - const char *p = b.Data(); - assert(p != nullptr); - assert(std::memcmp(p, ref.data(), ref.size()) == 0); -} - - -template -static void -run_basic_cases() -{ - // empty - { - Buf b; - std::string ref; - check_equals(b, ref); - } - - // append chars - { - Buf b; - std::string ref; - for (int i = 0; i < 1000; ++i) { - b.AppendChar('a'); - ref.push_back('a'); - } - check_equals(b, ref); - } - - // prepend chars - { - Buf b; - std::string ref; - for (int i = 0; i < 1000; ++i) { - b.PrependChar('b'); - ref.insert(ref.begin(), 'b'); - } - check_equals(b, ref); - } - - // append/prepend strings - { - Buf b; - std::string ref; - const char *hello = "hello"; - b.Append(hello, 5); - ref.append("hello"); - b.Prepend(hello, 5); - ref.insert(0, "hello"); - check_equals(b, ref); - } - - // larger random blocks - { - std::mt19937 rng(42); - std::uniform_int_distribution len_dist(0, 128); - std::uniform_int_distribution coin(0, 1); - Buf b; - std::string ref; - for (int step = 0; step < 2000; ++step) { - int L = len_dist(rng); - std::string payload(L, '\0'); - for (int i = 0; i < L; ++i) - payload[i] = static_cast('a' + (i % 26)); - if (coin(rng)) { - b.Append(payload.data(), payload.size()); - ref.append(payload); - } else { - b.Prepend(payload.data(), payload.size()); - ref.insert(0, payload); - } - } - check_equals(b, ref); - } -} - - -int -main() -{ - run_basic_cases(); - run_basic_cases(); - return 0; -} \ No newline at end of file