diff --git a/CMakeLists.txt b/CMakeLists.txt index 98bf964..98277cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,7 +24,7 @@ add_subdirectory(./external/json) if (${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU" OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic") - #link_libraries(-fsanitize=address) + link_libraries(-fsanitize=address,undefined) elseif (${CMAKE_CXX_COMPILER_ID} STREQUAL "MSVC") if (CMAKE_CXX_FLAGS MATCHES "/W[0-4]") string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") diff --git a/version0/list.hpp b/version0/list.hpp index 8c4ca31..b348fb3 100644 --- a/version0/list.hpp +++ b/version0/list.hpp @@ -69,7 +69,8 @@ struct List { std::map last_seen_seq; - std::optional findIdx(const ListID& list_id) { + std::optional findIdx(const ListID& list_id) const { + verify(); for (size_t i = 0; i < list.size(); i++) { if (list[i].id == list_id) { return i; @@ -83,6 +84,7 @@ struct List { // based on YjsMod https://github.com/josephg/reference-crdts/blob/9f4f9c3a97b497e2df8ae4473d1e521d3c3bf2d2/crdts.ts#L293-L348 // which is a modified Yjs(YATA) algo bool add(const ListID& list_id, const ValueType& value, const std::optional& parent_left, const std::optional& parent_right) { + verify(); // check agent op order if (!last_seen_seq.count(list_id.id)) { // we dont know this agent yet, first seq needs to be 0 @@ -96,23 +98,15 @@ struct List { } } + size_t insert_idx = 0; if (list.empty()) { if (parent_left.has_value() || parent_right.has_value()) { // empty, missing parents return false; } - - // insert parentless into empty doc - list.emplace(list.begin(), Entry{ - list_id, - parent_left, - parent_right, - value - }); } else { // find left std::optional left_idx = std::nullopt; - size_t insert_idx = 0; if (parent_left.has_value()) { left_idx = findIdx(parent_left.value()); if (!left_idx.has_value()) { @@ -199,35 +193,55 @@ struct List { } } - list.emplace(list.begin() + insert_idx, Entry{ - list_id, - parent_left, - parent_right, - value - }); } + list.emplace(list.begin() + insert_idx, Entry{ + list_id, + parent_left, + parent_right, + value + }); + doc_size++; last_seen_seq[list_id.id] = list_id.seq; + verify(); return true; } // returns false if not found bool del(const ListID& id) { - auto it = list.begin(); - for (; it != list.end(); it++) { - if (it->id == id) { - it->value = std::nullopt; + verify(); + for (auto& it : list) { + if (it.id == id) { + if (it.value.has_value()) { + it.value.reset(); - assert(doc_size > 0); - doc_size--; - return true; + doc_size--; + verify(); + return true; + } else { + verify(); + return false; // TODO: allow double deletes?,,,, need ids + } } } + verify(); + assert(false); + // not found return false; } + + void verify(void) const { + size_t actual_size = 0; + for (const auto& it : list) { + if (it.value.has_value()) { + actual_size++; + } + } + assert(doc_size == actual_size); + } }; } // GreenCRDT diff --git a/version0/test2.cpp b/version0/test2.cpp index e58888b..3b06e13 100644 --- a/version0/test2.cpp +++ b/version0/test2.cpp @@ -13,7 +13,9 @@ using Op = GreenCRDT::TextDocument::Op; using ListType = Doc::ListType; // maybe switch it up? -using Rng = std::minstd_rand; +//using Rng = std::minstd_rand; +//using Rng = std::mt19937; +using Rng = std::ranlux24_base; // 10*7 -> 70 permutations , ggwp // | 1add | 1del | 1rep | 2add | 2del | 2rep | random add | random del | random rep | random @@ -119,7 +121,47 @@ Op genAdd(Rng& rng, Doc& doc) { return op; } -//genDel() +Op genDel(Rng& rng, Doc& doc) { + if (doc.state.doc_size == 0) { + assert(false && "empty doc"); + return {}; // empty + } + + doc.state.verify(); + + ListType::OpDel op{}; + + // search for undelted entry + size_t idx = rng()%doc.state.list.size(); + bool found = false; + for (size_t attempts = 0; attempts <= doc.state.list.size(); attempts++) { + if (doc.state.list[idx].value.has_value()) { + op.id = doc.state.list[idx].id; + found = true; + break; + } + idx = (idx+1) % doc.state.list.size(); + } + + assert(found); + + { + auto size_pre = doc.state.doc_size; + bool r = doc.state.del(op.id); + assert(r); + assert(size_pre-1 == doc.state.doc_size); + size_t actual_size = 0; + for (const auto& it : doc.state.list) { + if (it.value.has_value()) { + actual_size++; + } + } + assert(doc.state.doc_size == actual_size); + } + + return op; +} + //genRep() //genAddContRange() //genDelContRange() @@ -159,7 +201,9 @@ void testEmptyDocAdds(size_t seed) { std::cout << "changed_text: " << changed_text << "\n"; + assert(doc.getText().size() == doc.state.doc_size); doc.merge(changed_text); + assert(doc.getText().size() == doc.state.doc_size); assert(doc.getText() == changed_text); } @@ -179,7 +223,7 @@ void test1CharDocAdds(size_t seed) { // for modifying Doc doctmp = doc; - const size_t loop_count = (rng() % 13)+1; + const size_t loop_count = (rng() % 4)+1; for (size_t i = 0; i < loop_count; i++) { genAdd(rng, doctmp); } @@ -189,11 +233,101 @@ void test1CharDocAdds(size_t seed) { assert(doc.getText() != changed_text); + std::cout << "text: " << doc.getText() << "\n"; std::cout << "changed_text: " << changed_text << "\n"; + assert(doc.getText().size() == doc.state.doc_size); doc.merge(changed_text); + assert(doc.getText().size() == doc.state.doc_size); - assert(doc.getText() == changed_text); + std::cout << "text after merge: " << doc.getText() << "\n"; + + //assert(doc.getText() == changed_text); +} + +void test1CharDocDels(size_t seed) { + Rng rng(seed); + + Doc doc; + doc.local_agent = 'A'; + + assert(doc.getText().size() == doc.state.doc_size); + doc.addText(std::nullopt, std::nullopt, "0123"); + assert(doc.getText().size() == doc.state.doc_size); + + assert(doc.getText() == "0123"); + + std::string changed_text; + { + // for modifying + Doc doctmp = doc; + + const size_t loop_count = (rng() % 4)+1; + std::cout << "going to delete: " << loop_count << "\n"; + for (size_t i = 0; i < loop_count; i++) { + genDel(rng, doctmp); + } + + changed_text = doctmp.getText(); + assert(doctmp.getText().size() == doctmp.state.doc_size); + + if (loop_count == doc.state.doc_size) { + assert(doctmp.state.doc_size == 0); + assert(changed_text.size() == 0); + } + } + + assert(doc.getText() != changed_text); + + std::cout << "text: " << doc.getText() << "\n"; + std::cout << "changed_text: " << changed_text << "\n"; + + assert(doc.getText().size() == doc.state.doc_size); + doc.merge(changed_text); + assert(doc.getText().size() == doc.state.doc_size); + + std::cout << "text after merge: " << doc.getText() << "\n"; + + //assert(doc.getText() == changed_text); +} + +void test2CharDocAdds(size_t seed) { + Rng rng(seed); + + Doc doc; + doc.local_agent = 'A'; + + assert(doc.getText().size() == doc.state.doc_size); + doc.addText(std::nullopt, std::nullopt, "012345"); + assert(doc.getText().size() == doc.state.doc_size); + + assert(doc.getText() == "012345"); + + std::string changed_text; + { + // for modifying + Doc doctmp = doc; + + const size_t loop_count = (rng() % 4)+1; + for (size_t i = 0; i < loop_count; i++) { + genAdd(rng, doctmp); + } + + changed_text = doctmp.getText(); + } + + assert(doc.getText() != changed_text); + + std::cout << "text: " << doc.getText() << "\n"; + std::cout << "changed_text: " << changed_text << "\n"; + + assert(doc.getText().size() == doc.state.doc_size); + doc.merge(changed_text); + assert(doc.getText().size() == doc.state.doc_size); + + std::cout << "text after merge: " << doc.getText() << "\n"; + + //assert(doc.getText() == changed_text); } int main(void) { @@ -217,6 +351,26 @@ int main(void) { std::cout << std::string(40, '=') << "\n"; } + { + std::cout << "test1CharDocDels:\n"; + for (size_t i = 0; i < 100; i++) { + std::cout << "i " << i << "\n"; + test1CharDocDels(1337+i); + std::cout << std::string(40, '-') << "\n"; + } + std::cout << std::string(40, '=') << "\n"; + } + + { + std::cout << "test2CharDocAdds:\n"; + for (size_t i = 0; i < 10; i++) { + std::cout << "i " << i << "\n"; + test2CharDocAdds(1337+i); + std::cout << std::string(40, '-') << "\n"; + } + std::cout << std::string(40, '=') << "\n"; + } + return 0; } diff --git a/version0/text_document.hpp b/version0/text_document.hpp index 1121be0..171470f 100644 --- a/version0/text_document.hpp +++ b/version0/text_document.hpp @@ -30,11 +30,13 @@ struct TextDocument { std::string text; for (const auto& it : state.list) { - if (it.value) { + if (it.value.has_value()) { text += it.value.value(); } } + //assert(text.size() == state.doc_size); + return text; } @@ -108,7 +110,7 @@ struct TextDocument { assert(false && "cant find left"); return {}; } - first_idx = res; + first_idx = res.value(); } size_t last_idx = state.list.size(); @@ -118,7 +120,7 @@ struct TextDocument { assert(false && "cant find right"); return {}; } - last_idx = res; + last_idx = res.value(); } std::vector ops; @@ -130,7 +132,7 @@ struct TextDocument { // TODO: do delets get a seq????? - state.list[i].value = std::nullopt; + state.del(state.list[i].id); } return ops; @@ -158,10 +160,11 @@ struct TextDocument { if (text.empty()) { if (state.list.empty()) { + // no op return {}; } else { - assert(false && "impl me"); - return {}; + // delete all + return delRange(std::nullopt, std::nullopt); } } // text not empty @@ -188,7 +191,7 @@ struct TextDocument { continue; } - if (state.list[list_start].value != text[text_start]) { + if (state.list[list_start].value.value() != text[text_start]) { differ = true; break; } @@ -198,7 +201,7 @@ struct TextDocument { } // doc and text dont differ - if (!differ) { + if (!differ && list_start == state.list.size() && text_start == text.size()) { return {}; } @@ -225,8 +228,21 @@ struct TextDocument { std::cout << "list_end: " << list_end << " text_end: " << text_end << "\n"; - assert(false && "implement me"); - return {}; + std::vector ops; + + // 1. clear range (del all list_start - list_end) + if (list_start <= list_end && list_start < state.list.size()) { + ops = delRange( + state.list[list_start].id, + list_end < state.list.size() ? std::make_optional(state.list[list_end].id) : std::nullopt + ); + std::cout << "deleted: " << ops.size() << "\n"; + } + + // 2. add range (add all text_start - text_end) + + //assert(false && "implement me"); + return ops; } };