From b93dafc121099090ffa982e8a1172372b298533f Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Wed, 3 Jun 2026 09:03:18 +0200 Subject: [PATCH 01/10] Hash the list of arguments in the mocking table This allows variable numbers of arguments to be stored and matched. TODO: - Use buckets for collisions! - Update the overrides state in snapshots --- src/Debug/debugger.cpp | 72 +++++++++++++++++++++++++++++++++++------- src/Debug/debugger.h | 8 ++--- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/Debug/debugger.cpp b/src/Debug/debugger.cpp index 94edf40b..56603e72 100644 --- a/src/Debug/debugger.cpp +++ b/src/Debug/debugger.cpp @@ -1679,27 +1679,50 @@ std::string read_string(uint8_t **pos) { return str; } +// TODO: I should probably use uint32_t simply because most microcontrollers are 32bit so it will be faster +uint64_t FNV1a_uint32_list(const std::vector& values) { + constexpr uint64_t FNV_offset_basis = 14695981039346656037ULL; + uint64_t result_hash = FNV_offset_basis; + + for (const uint32_t v : values) { + for (int i = 0; i < 4; ++i) { + constexpr std::uint64_t FNV_prime = 1099511628211ULL; + const uint8_t byte = (v >> (i * 8)) & 0xff; + result_hash ^= byte; + result_hash *= FNV_prime; + } + } + + return result_hash; +} + void Debugger::addOverride(Module *m, uint8_t *interruptData) { std::string primitive_name = read_string(&interruptData); - uint32_t arg = read_B32(&interruptData); - uint32_t result = read_B32(&interruptData); std::optional fidx = resolve_imported_function(m, primitive_name); if (!fidx) { channel->write( "Cannot override the result for unknown function \"%s\".\n", primitive_name.c_str()); + channel->write("ack%x;0\n", interruptUnsetOverridePinValue); return; } - channel->write("Override %s(%d) = %d.\n", primitive_name.c_str(), arg, - result); - overrides[fidx.value()][arg] = result; + uint32_t param_count = m->functions[fidx.value()].type->param_count; + std::vector args(param_count); + for (int i = 0; i < param_count; i++) { + args[i] = read_B32(&interruptData); + channel->write("Arg %d\n", args[args.size() - 1]); + } + uint64_t args_hash = FNV1a_uint32_list(args); + const uint32_t result = read_B32(&interruptData); + channel->write("Register mock %s(%d) = %d\n", primitive_name.c_str(), args_hash, result); + channel->write("ack%x;1\n", interruptSetOverridePinValue); + overrides[fidx.value()][args_hash] = result; } void Debugger::removeOverride(Module *m, uint8_t *interruptData) { std::string primitive_name = read_string(&interruptData); - uint32_t arg = read_B32(&interruptData); std::optional fidx = resolve_imported_function(m, primitive_name); if (!fidx) { @@ -1708,15 +1731,40 @@ void Debugger::removeOverride(Module *m, uint8_t *interruptData) { return; } - if (overrides[fidx.value()].count(arg) == 0) { - channel->write("Override for %s(%d) not found.\n", - primitive_name.c_str(), arg); + uint32_t param_count = m->functions[fidx.value()].type->param_count; + std::vector args(param_count); + for (int i = 0; i < param_count; i++) { + args[i] = read_B32(&interruptData); + } + uint64_t args_hash = FNV1a_uint32_list(args); + + if (overrides[fidx.value()].count(args_hash) == 0) { + channel->write("Mock for %s(%d) not found.\n", + primitive_name.c_str(), args_hash); + channel->write("ack%x;0\n", interruptUnsetOverridePinValue); return; } - channel->write("Removing override %s(%d) = %d.\n", primitive_name.c_str(), - arg, overrides[fidx.value()][arg]); - overrides[fidx.value()].erase(arg); + channel->write("Removing mock %s(%d) = %d.\n", primitive_name.c_str(), + args_hash, overrides[fidx.value()][args_hash]); + channel->write("ack%x;1\n", interruptUnsetOverridePinValue); + overrides[fidx.value()].erase(args_hash); +} + +bool Debugger::isMocked(uint32_t fidx, uint32_t argument) { + std::vector args(1); + args[0] = argument; + uint64_t args_hash = FNV1a_uint32_list(args); + channel->write("Arg %d\n", argument); + channel->write("Arg hash %d\n", args_hash); + return overrides.count(fidx) > 0 && overrides[fidx].count(args_hash) > 0; +} + +uint32_t Debugger::getMockedValue(uint32_t fidx, uint32_t argument) { + std::vector args(1); + args[0] = argument; + uint64_t args_hash = FNV1a_uint32_list(args); + return overrides[fidx][args_hash]; } bool Debugger::handleContinueFor(Module *m) { diff --git a/src/Debug/debugger.h b/src/Debug/debugger.h index c612ddf8..812e32f9 100644 --- a/src/Debug/debugger.h +++ b/src/Debug/debugger.h @@ -308,12 +308,8 @@ class Debugger { bool handlePushedEvent(char *bytes) const; // Concolic Multiverse Debugging - inline bool isMocked(uint32_t fidx, uint32_t argument) { - return overrides.count(fidx) > 0 && overrides[fidx].count(argument) > 0; - } - inline uint32_t getMockedValue(uint32_t fidx, uint32_t argument) { - return overrides[fidx][argument]; - } + bool isMocked(uint32_t fidx, uint32_t argument); + uint32_t getMockedValue(uint32_t fidx, uint32_t argument); void addOverride(Module *m, uint8_t *interruptData); void removeOverride(Module *m, uint8_t *interruptData); From 5076a380b5c1ac5a13cb28ca44f0a16cc0c3808d Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Wed, 3 Jun 2026 15:06:16 +0200 Subject: [PATCH 02/10] Use buckets for mock elements + update override snapshot state --- src/Debug/debugger.cpp | 149 ++++++++++++++++++++----------- src/Debug/debugger.h | 21 +++-- src/Interpreter/instructions.cpp | 10 ++- 3 files changed, 119 insertions(+), 61 deletions(-) diff --git a/src/Debug/debugger.cpp b/src/Debug/debugger.cpp index 56603e72..37a75af2 100644 --- a/src/Debug/debugger.cpp +++ b/src/Debug/debugger.cpp @@ -974,12 +974,23 @@ void Debugger::inspect(Module *m, const uint16_t sizeStateArray, this->channel->write("%s", addComma ? "," : ""); this->channel->write(R"("overrides": [)"); bool comma = false; - for (auto key : overrides) { - for (auto argResult : key.second) { + for (const auto &[_, bucket] : overrides) { + for (const MockItem *mock : bucket) { this->channel->write("%s", comma ? ", " : ""); - this->channel->write( - R"({"fidx": %d, "arg": %d, "return_value": %d})", - key.first, argResult.first, argResult.second); + this->channel->write(R"({"fidx": %d, "args": [)", + mock->key[mock->key.size() - 1]); + + if (!mock->key.empty()) { + this->channel->write("%d", mock->key[0]); + } + + for (uint32_t i = 1; i < mock->key.size() - 1; i++) { + this->channel->write(", %d", mock->key[i]); + } + + this->channel->write(R"(], "return_value": %d})", + mock->result); + comma = true; } } @@ -1126,6 +1137,24 @@ void Debugger::checkpoint(Module *m, const bool force) { instructions_executed = 0; } +// TODO: I should probably use uint32_t simply because most microcontrollers are +// 32bit so it will be faster +uint64_t FNV1a_uint32_list(const std::vector &values) { + constexpr uint64_t FNV_offset_basis = 14695981039346656037ULL; + uint64_t result_hash = FNV_offset_basis; + + for (const uint32_t v : values) { + for (int i = 0; i < 4; ++i) { + constexpr std::uint64_t FNV_prime = 1099511628211ULL; + const uint8_t byte = (v >> (i * 8)) & 0xff; + result_hash ^= byte; + result_hash *= FNV_prime; + } + } + + return result_hash; +} + void Debugger::freeState(Module *m, uint8_t *interruptData) { debug("freeing the program state\n"); uint8_t *first_msg = nullptr; @@ -1482,9 +1511,19 @@ bool Debugger::saveState(Module *m, uint8_t *interruptData) { uint8_t overrides_count = *program_state++; for (uint32_t i = 0; i < overrides_count; i++) { uint32_t fidx = read_B32(&program_state); - uint32_t arg = read_B32(&program_state); + uint32_t param_count = m->functions[fidx].type->param_count; + std::vector key(param_count + 1); + for (uint32_t j = 0; j < param_count; j++) { + key[j] = read_B32(&program_state); + } + key[param_count] = fidx; uint32_t return_value = read_B32(&program_state); - overrides[fidx][arg] = return_value; + uint64_t key_hash = FNV1a_uint32_list(key); + if (overrides[key_hash].empty()) { + overrides[key_hash] = {}; + } + overrides[key_hash].push_back( + new MockItem{.key = key, .result = return_value}); debug("Override %d %d %d\n", fidx, arg, return_value); } break; @@ -1679,21 +1718,19 @@ std::string read_string(uint8_t **pos) { return str; } -// TODO: I should probably use uint32_t simply because most microcontrollers are 32bit so it will be faster -uint64_t FNV1a_uint32_list(const std::vector& values) { - constexpr uint64_t FNV_offset_basis = 14695981039346656037ULL; - uint64_t result_hash = FNV_offset_basis; +MockItem *Debugger::getMock(uint32_t hash, const std::vector &key) { + if (overrides.count(hash) == 0) { + // Not found + return nullptr; + } - for (const uint32_t v : values) { - for (int i = 0; i < 4; ++i) { - constexpr std::uint64_t FNV_prime = 1099511628211ULL; - const uint8_t byte = (v >> (i * 8)) & 0xff; - result_hash ^= byte; - result_hash *= FNV_prime; + for (MockItem *mock : overrides[hash]) { + if (mock->key == key) { + // Found + return mock; } } - - return result_hash; + return nullptr; } void Debugger::addOverride(Module *m, uint8_t *interruptData) { @@ -1709,16 +1746,29 @@ void Debugger::addOverride(Module *m, uint8_t *interruptData) { } uint32_t param_count = m->functions[fidx.value()].type->param_count; - std::vector args(param_count); - for (int i = 0; i < param_count; i++) { - args[i] = read_B32(&interruptData); - channel->write("Arg %d\n", args[args.size() - 1]); + std::vector key(param_count + 1); + for (uint32_t i = 0; i < param_count; i++) { + key[i] = read_B32(&interruptData); + channel->write("Arg %d\n", key[key.size() - 1]); } - uint64_t args_hash = FNV1a_uint32_list(args); + key[param_count] = fidx.value(); + + uint64_t key_hash = FNV1a_uint32_list(key); const uint32_t result = read_B32(&interruptData); - channel->write("Register mock %s(%d) = %d\n", primitive_name.c_str(), args_hash, result); + channel->write("Register mock %s(%d) = %d\n", primitive_name.c_str(), + key_hash, result); channel->write("ack%x;1\n", interruptSetOverridePinValue); - overrides[fidx.value()][args_hash] = result; + + MockItem *item = getMock(key_hash, key); + if (item) { + item->result = result; + return; + } + + if (overrides.count(key_hash) == 0) { + overrides[key_hash] = {}; + } + overrides[key_hash].push_back(new MockItem{.key = key, .result = result}); } void Debugger::removeOverride(Module *m, uint8_t *interruptData) { @@ -1732,39 +1782,38 @@ void Debugger::removeOverride(Module *m, uint8_t *interruptData) { } uint32_t param_count = m->functions[fidx.value()].type->param_count; - std::vector args(param_count); - for (int i = 0; i < param_count; i++) { - args[i] = read_B32(&interruptData); + std::vector key(param_count + 1); + for (uint32_t i = 0; i < param_count; i++) { + key[i] = read_B32(&interruptData); } - uint64_t args_hash = FNV1a_uint32_list(args); + key[param_count] = fidx.value(); + uint64_t key_hash = FNV1a_uint32_list(key); - if (overrides[fidx.value()].count(args_hash) == 0) { - channel->write("Mock for %s(%d) not found.\n", - primitive_name.c_str(), args_hash); + MockItem *item = getMock(key_hash, key); + if (!item) { + channel->write("Mock for %s(%d) not found.\n", primitive_name.c_str(), + key_hash); channel->write("ack%x;0\n", interruptUnsetOverridePinValue); return; } - channel->write("Removing mock %s(%d) = %d.\n", primitive_name.c_str(), - args_hash, overrides[fidx.value()][args_hash]); + // TODO: This looks up the element again, maybe this can be done more + // efficiently + overrides[key_hash].remove(item); + free(item); channel->write("ack%x;1\n", interruptUnsetOverridePinValue); - overrides[fidx.value()].erase(args_hash); } -bool Debugger::isMocked(uint32_t fidx, uint32_t argument) { - std::vector args(1); - args[0] = argument; - uint64_t args_hash = FNV1a_uint32_list(args); - channel->write("Arg %d\n", argument); - channel->write("Arg hash %d\n", args_hash); - return overrides.count(fidx) > 0 && overrides[fidx].count(args_hash) > 0; -} - -uint32_t Debugger::getMockedValue(uint32_t fidx, uint32_t argument) { - std::vector args(1); - args[0] = argument; - uint64_t args_hash = FNV1a_uint32_list(args); - return overrides[fidx][args_hash]; +MockItem *Debugger::getMockForArgs(Module *m, uint32_t fidx) { + uint32_t param_count = m->functions[fidx].type->param_count; + std::vector key(param_count + 1); + const ExecutionContext *ectx = m->warduino->execution_context; + for (uint32_t i = 0; i < param_count; i++) { + key[i] = ectx->stack[ectx->sp - i].value.uint32; + } + key[param_count] = fidx; + const uint64_t hash = FNV1a_uint32_list(key); + return getMock(hash, key); } bool Debugger::handleContinueFor(Module *m) { diff --git a/src/Debug/debugger.h b/src/Debug/debugger.h index 812e32f9..343857ba 100644 --- a/src/Debug/debugger.h +++ b/src/Debug/debugger.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include // std::queue @@ -110,6 +111,11 @@ enum class SnapshotPolicy : int { // points where primitives are used. }; +struct MockItem { + std::vector key; // key = args + fidx + uint32_t result; +}; + class Debugger { private: std::deque debugMessages = {}; @@ -130,8 +136,7 @@ class Debugger { warduino::mutex *supervisor_mutex; // Mocking - std::unordered_map> - overrides; + std::unordered_map> overrides; // Checkpointing SnapshotPolicy snapshotPolicy; @@ -206,6 +211,12 @@ class Debugger { bool reset(Module *m); + //// Handle mocking + + MockItem *getMock(uint32_t hash, const std::vector &key); + void addOverride(Module *m, uint8_t *interruptData); + void removeOverride(Module *m, uint8_t *interruptData); + //// Handle out-of-place debugging void freeState(Module *m, uint8_t *interruptData); @@ -308,11 +319,7 @@ class Debugger { bool handlePushedEvent(char *bytes) const; // Concolic Multiverse Debugging - bool isMocked(uint32_t fidx, uint32_t argument); - uint32_t getMockedValue(uint32_t fidx, uint32_t argument); - - void addOverride(Module *m, uint8_t *interruptData); - void removeOverride(Module *m, uint8_t *interruptData); + MockItem *getMockForArgs(Module *m, uint32_t fidx); // Checkpointing void checkpoint(Module *m, bool force = false); diff --git a/src/Interpreter/instructions.cpp b/src/Interpreter/instructions.cpp index eb05081a..66c0e26d 100644 --- a/src/Interpreter/instructions.cpp +++ b/src/Interpreter/instructions.cpp @@ -305,10 +305,12 @@ bool i_instr_call(Module *m) { // Mocking only works on primitives, no need to check for it otherwise. if (ectx->sp >= 0) { - uint32_t arg = ectx->stack[ectx->sp].value.uint32; - if (m->warduino->debugger->isMocked(fidx, arg)) { - ectx->stack[ectx->sp].value.uint32 = - m->warduino->debugger->getMockedValue(fidx, arg); + if (const MockItem *mock = + m->warduino->debugger->getMockForArgs(m, fidx)) { + const uint32_t param_count = + m->functions[fidx].type->param_count; + ectx->sp -= static_cast(param_count) - 1; + ectx->stack[ectx->sp].value.uint32 = mock->result; return true; } } From f7146d7928673a3a2f5361e31d25a0c81b8798a1 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Thu, 4 Jun 2026 10:27:14 +0200 Subject: [PATCH 03/10] Use delete instead of free for MockItem --- src/Debug/debugger.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Debug/debugger.cpp b/src/Debug/debugger.cpp index 37a75af2..3d048df6 100644 --- a/src/Debug/debugger.cpp +++ b/src/Debug/debugger.cpp @@ -1800,7 +1800,7 @@ void Debugger::removeOverride(Module *m, uint8_t *interruptData) { // TODO: This looks up the element again, maybe this can be done more // efficiently overrides[key_hash].remove(item); - free(item); + delete item; channel->write("ack%x;1\n", interruptUnsetOverridePinValue); } From b8d2fbfc67ae6f904a8e6892e13dc20e14b16b75 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Thu, 4 Jun 2026 10:37:10 +0200 Subject: [PATCH 04/10] Make some more things const --- src/Debug/debugger.cpp | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Debug/debugger.cpp b/src/Debug/debugger.cpp index 3d048df6..56342bac 100644 --- a/src/Debug/debugger.cpp +++ b/src/Debug/debugger.cpp @@ -1709,7 +1709,7 @@ std::optional resolve_imported_function(Module *m, } std::string read_string(uint8_t **pos) { - std::string str = ""; + std::string str; char c = *(*pos)++; while (c != '\0') { str += c; @@ -1734,9 +1734,8 @@ MockItem *Debugger::getMock(uint32_t hash, const std::vector &key) { } void Debugger::addOverride(Module *m, uint8_t *interruptData) { - std::string primitive_name = read_string(&interruptData); - - std::optional fidx = resolve_imported_function(m, primitive_name); + const std::string primitive_name = read_string(&interruptData); + const std::optional fidx = resolve_imported_function(m, primitive_name); if (!fidx) { channel->write( "Cannot override the result for unknown function \"%s\".\n", @@ -1745,7 +1744,7 @@ void Debugger::addOverride(Module *m, uint8_t *interruptData) { return; } - uint32_t param_count = m->functions[fidx.value()].type->param_count; + const uint32_t param_count = m->functions[fidx.value()].type->param_count; std::vector key(param_count + 1); for (uint32_t i = 0; i < param_count; i++) { key[i] = read_B32(&interruptData); @@ -1753,7 +1752,7 @@ void Debugger::addOverride(Module *m, uint8_t *interruptData) { } key[param_count] = fidx.value(); - uint64_t key_hash = FNV1a_uint32_list(key); + const uint64_t key_hash = FNV1a_uint32_list(key); const uint32_t result = read_B32(&interruptData); channel->write("Register mock %s(%d) = %d\n", primitive_name.c_str(), key_hash, result); @@ -1772,22 +1771,21 @@ void Debugger::addOverride(Module *m, uint8_t *interruptData) { } void Debugger::removeOverride(Module *m, uint8_t *interruptData) { - std::string primitive_name = read_string(&interruptData); - - std::optional fidx = resolve_imported_function(m, primitive_name); + const std::string primitive_name = read_string(&interruptData); + const std::optional fidx = resolve_imported_function(m, primitive_name); if (!fidx) { channel->write("Cannot remove override for unknown function \"%s\".\n", primitive_name.c_str()); return; } - uint32_t param_count = m->functions[fidx.value()].type->param_count; + const uint32_t param_count = m->functions[fidx.value()].type->param_count; std::vector key(param_count + 1); for (uint32_t i = 0; i < param_count; i++) { key[i] = read_B32(&interruptData); } key[param_count] = fidx.value(); - uint64_t key_hash = FNV1a_uint32_list(key); + const uint64_t key_hash = FNV1a_uint32_list(key); MockItem *item = getMock(key_hash, key); if (!item) { From 160d2e0ebaf6fd73575431898fdbe67b88188fd7 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Thu, 4 Jun 2026 10:55:02 +0200 Subject: [PATCH 05/10] Don't look up the mock item twice when removing it Previously remove would look up the item again when we already look it up and should know where it is. --- src/Debug/debugger.cpp | 43 +++++++++++++++++++++++++----------------- src/Debug/debugger.h | 2 ++ 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/Debug/debugger.cpp b/src/Debug/debugger.cpp index 56342bac..e8129db0 100644 --- a/src/Debug/debugger.cpp +++ b/src/Debug/debugger.cpp @@ -1718,24 +1718,34 @@ std::string read_string(uint8_t **pos) { return str; } -MockItem *Debugger::getMock(uint32_t hash, const std::vector &key) { +bool Debugger::getMockIterator(const uint32_t hash, + const std::vector &key, + std::list::iterator &iter) { if (overrides.count(hash) == 0) { - // Not found - return nullptr; + return false; } - for (MockItem *mock : overrides[hash]) { - if (mock->key == key) { - // Found - return mock; - } + std::list &bucket = overrides[hash]; + iter = bucket.begin(); + while (iter != bucket.end() && (*iter)->key != key) { + ++iter; + } + return iter != bucket.end(); +} + +MockItem *Debugger::getMock(const uint32_t hash, + const std::vector &key) { + std::list::iterator it; + if (!getMockIterator(hash, key, it)) { + return nullptr; } - return nullptr; + return *it; } void Debugger::addOverride(Module *m, uint8_t *interruptData) { const std::string primitive_name = read_string(&interruptData); - const std::optional fidx = resolve_imported_function(m, primitive_name); + const std::optional fidx = + resolve_imported_function(m, primitive_name); if (!fidx) { channel->write( "Cannot override the result for unknown function \"%s\".\n", @@ -1772,7 +1782,8 @@ void Debugger::addOverride(Module *m, uint8_t *interruptData) { void Debugger::removeOverride(Module *m, uint8_t *interruptData) { const std::string primitive_name = read_string(&interruptData); - const std::optional fidx = resolve_imported_function(m, primitive_name); + const std::optional fidx = + resolve_imported_function(m, primitive_name); if (!fidx) { channel->write("Cannot remove override for unknown function \"%s\".\n", primitive_name.c_str()); @@ -1787,18 +1798,16 @@ void Debugger::removeOverride(Module *m, uint8_t *interruptData) { key[param_count] = fidx.value(); const uint64_t key_hash = FNV1a_uint32_list(key); - MockItem *item = getMock(key_hash, key); - if (!item) { + std::list::iterator it; + if (!getMockIterator(key_hash, key, it)) { channel->write("Mock for %s(%d) not found.\n", primitive_name.c_str(), key_hash); channel->write("ack%x;0\n", interruptUnsetOverridePinValue); return; } - // TODO: This looks up the element again, maybe this can be done more - // efficiently - overrides[key_hash].remove(item); - delete item; + overrides[key_hash].erase(it); + delete *it; channel->write("ack%x;1\n", interruptUnsetOverridePinValue); } diff --git a/src/Debug/debugger.h b/src/Debug/debugger.h index 343857ba..10a003e5 100644 --- a/src/Debug/debugger.h +++ b/src/Debug/debugger.h @@ -213,6 +213,8 @@ class Debugger { //// Handle mocking + bool getMockIterator(uint32_t hash, const std::vector &key, + std::list::iterator &iter); MockItem *getMock(uint32_t hash, const std::vector &key); void addOverride(Module *m, uint8_t *interruptData); void removeOverride(Module *m, uint8_t *interruptData); From 0cbcaf7e0ea4a0ccdeb2f69655404f75ffbf2cc7 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Thu, 4 Jun 2026 11:08:27 +0200 Subject: [PATCH 06/10] Remove some debug channel->write calls --- src/Debug/debugger.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Debug/debugger.cpp b/src/Debug/debugger.cpp index e8129db0..5edd422e 100644 --- a/src/Debug/debugger.cpp +++ b/src/Debug/debugger.cpp @@ -1758,14 +1758,11 @@ void Debugger::addOverride(Module *m, uint8_t *interruptData) { std::vector key(param_count + 1); for (uint32_t i = 0; i < param_count; i++) { key[i] = read_B32(&interruptData); - channel->write("Arg %d\n", key[key.size() - 1]); } key[param_count] = fidx.value(); const uint64_t key_hash = FNV1a_uint32_list(key); const uint32_t result = read_B32(&interruptData); - channel->write("Register mock %s(%d) = %d\n", primitive_name.c_str(), - key_hash, result); channel->write("ack%x;1\n", interruptSetOverridePinValue); MockItem *item = getMock(key_hash, key); @@ -1787,6 +1784,7 @@ void Debugger::removeOverride(Module *m, uint8_t *interruptData) { if (!fidx) { channel->write("Cannot remove override for unknown function \"%s\".\n", primitive_name.c_str()); + channel->write("ack%x;0\n", interruptUnsetOverridePinValue); return; } @@ -1800,8 +1798,6 @@ void Debugger::removeOverride(Module *m, uint8_t *interruptData) { std::list::iterator it; if (!getMockIterator(key_hash, key, it)) { - channel->write("Mock for %s(%d) not found.\n", primitive_name.c_str(), - key_hash); channel->write("ack%x;0\n", interruptUnsetOverridePinValue); return; } From 664e4560de237a2e9d96a12122d3228d78a669d6 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Thu, 4 Jun 2026 11:50:31 +0200 Subject: [PATCH 07/10] Switch from FNV64 to FNV32 --- src/Debug/debugger.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Debug/debugger.cpp b/src/Debug/debugger.cpp index 5edd422e..5b8df01b 100644 --- a/src/Debug/debugger.cpp +++ b/src/Debug/debugger.cpp @@ -1137,15 +1137,16 @@ void Debugger::checkpoint(Module *m, const bool force) { instructions_executed = 0; } -// TODO: I should probably use uint32_t simply because most microcontrollers are -// 32bit so it will be faster -uint64_t FNV1a_uint32_list(const std::vector &values) { - constexpr uint64_t FNV_offset_basis = 14695981039346656037ULL; - uint64_t result_hash = FNV_offset_basis; +/* + * FNV-1a 32bit, https://datatracker.ietf.org/doc/html/draft-eastlake-fnv-17.html + */ +uint32_t FNV1a_uint32_list(const std::vector &values) { + constexpr uint32_t FNV_offset_basis = 0x811C9DC5; + uint32_t result_hash = FNV_offset_basis; for (const uint32_t v : values) { for (int i = 0; i < 4; ++i) { - constexpr std::uint64_t FNV_prime = 1099511628211ULL; + constexpr uint32_t FNV_prime = 0x01000193; const uint8_t byte = (v >> (i * 8)) & 0xff; result_hash ^= byte; result_hash *= FNV_prime; From a4b840dcf6accc825f8e02ab91aa5968a0c3c025 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Thu, 4 Jun 2026 11:52:03 +0200 Subject: [PATCH 08/10] getMockForArgs should assemble the key in argument order, not stack pop order --- src/Debug/debugger.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Debug/debugger.cpp b/src/Debug/debugger.cpp index 5b8df01b..888fa11a 100644 --- a/src/Debug/debugger.cpp +++ b/src/Debug/debugger.cpp @@ -1138,7 +1138,8 @@ void Debugger::checkpoint(Module *m, const bool force) { } /* - * FNV-1a 32bit, https://datatracker.ietf.org/doc/html/draft-eastlake-fnv-17.html + * FNV-1a 32bit: + * https://datatracker.ietf.org/doc/html/draft-eastlake-fnv-17.html */ uint32_t FNV1a_uint32_list(const std::vector &values) { constexpr uint32_t FNV_offset_basis = 0x811C9DC5; @@ -1813,7 +1814,7 @@ MockItem *Debugger::getMockForArgs(Module *m, uint32_t fidx) { std::vector key(param_count + 1); const ExecutionContext *ectx = m->warduino->execution_context; for (uint32_t i = 0; i < param_count; i++) { - key[i] = ectx->stack[ectx->sp - i].value.uint32; + key[i] = ectx->stack[ectx->sp - (param_count - i - 1)].value.uint32; } key[param_count] = fidx; const uint64_t hash = FNV1a_uint32_list(key); From a69d728dbbd5dd21635059d28821a92ca9fabc64 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Thu, 4 Jun 2026 14:35:42 +0200 Subject: [PATCH 09/10] Fix usage of iterator after invalidation --- src/Debug/debugger.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Debug/debugger.cpp b/src/Debug/debugger.cpp index 888fa11a..8dbabac6 100644 --- a/src/Debug/debugger.cpp +++ b/src/Debug/debugger.cpp @@ -1804,8 +1804,9 @@ void Debugger::removeOverride(Module *m, uint8_t *interruptData) { return; } - overrides[key_hash].erase(it); - delete *it; + const MockItem *item = *it; + overrides[key_hash].erase(it); // Invalidates it + delete item; channel->write("ack%x;1\n", interruptUnsetOverridePinValue); } From cfd8b31af3e7305b3978726d61f9d1f879d8d53b Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Thu, 4 Jun 2026 14:58:33 +0200 Subject: [PATCH 10/10] Refactor overrides to use a custom hash function on unordered_map instead of implementing it partially myself which made it more complex Unordered map already has buckets, by then storing my own bucket list in those buckets things just got more complex and slower. --- src/Debug/debugger.cpp | 112 ++++++------------------------- src/Debug/debugger.h | 30 ++++++--- src/Interpreter/instructions.cpp | 6 +- 3 files changed, 44 insertions(+), 104 deletions(-) diff --git a/src/Debug/debugger.cpp b/src/Debug/debugger.cpp index 8dbabac6..50d3c8d1 100644 --- a/src/Debug/debugger.cpp +++ b/src/Debug/debugger.cpp @@ -974,25 +974,16 @@ void Debugger::inspect(Module *m, const uint16_t sizeStateArray, this->channel->write("%s", addComma ? "," : ""); this->channel->write(R"("overrides": [)"); bool comma = false; - for (const auto &[_, bucket] : overrides) { - for (const MockItem *mock : bucket) { - this->channel->write("%s", comma ? ", " : ""); - this->channel->write(R"({"fidx": %d, "args": [)", - mock->key[mock->key.size() - 1]); - - if (!mock->key.empty()) { - this->channel->write("%d", mock->key[0]); - } - - for (uint32_t i = 1; i < mock->key.size() - 1; i++) { - this->channel->write(", %d", mock->key[i]); - } - - this->channel->write(R"(], "return_value": %d})", - mock->result); - - comma = true; + for (const auto &[key, return_value] : overrides) { + this->channel->write("%s", comma ? ", " : ""); + const uint32_t fidx = key[key.size() - 1]; + this->channel->write(R"({"fidx": %d, "args": [)", fidx); + for (uint32_t i = 0; i < key.size() - 1; i++) { + this->channel->write("%s%d", i > 0 ? ", " : "", key[i]); } + this->channel->write(R"(], "return_value": %d})", + return_value); + comma = true; } this->channel->write("]"); addComma = true; @@ -1137,26 +1128,6 @@ void Debugger::checkpoint(Module *m, const bool force) { instructions_executed = 0; } -/* - * FNV-1a 32bit: - * https://datatracker.ietf.org/doc/html/draft-eastlake-fnv-17.html - */ -uint32_t FNV1a_uint32_list(const std::vector &values) { - constexpr uint32_t FNV_offset_basis = 0x811C9DC5; - uint32_t result_hash = FNV_offset_basis; - - for (const uint32_t v : values) { - for (int i = 0; i < 4; ++i) { - constexpr uint32_t FNV_prime = 0x01000193; - const uint8_t byte = (v >> (i * 8)) & 0xff; - result_hash ^= byte; - result_hash *= FNV_prime; - } - } - - return result_hash; -} - void Debugger::freeState(Module *m, uint8_t *interruptData) { debug("freeing the program state\n"); uint8_t *first_msg = nullptr; @@ -1520,13 +1491,7 @@ bool Debugger::saveState(Module *m, uint8_t *interruptData) { } key[param_count] = fidx; uint32_t return_value = read_B32(&program_state); - uint64_t key_hash = FNV1a_uint32_list(key); - if (overrides[key_hash].empty()) { - overrides[key_hash] = {}; - } - overrides[key_hash].push_back( - new MockItem{.key = key, .result = return_value}); - debug("Override %d %d %d\n", fidx, arg, return_value); + overrides[key] = return_value; } break; } @@ -1720,30 +1685,6 @@ std::string read_string(uint8_t **pos) { return str; } -bool Debugger::getMockIterator(const uint32_t hash, - const std::vector &key, - std::list::iterator &iter) { - if (overrides.count(hash) == 0) { - return false; - } - - std::list &bucket = overrides[hash]; - iter = bucket.begin(); - while (iter != bucket.end() && (*iter)->key != key) { - ++iter; - } - return iter != bucket.end(); -} - -MockItem *Debugger::getMock(const uint32_t hash, - const std::vector &key) { - std::list::iterator it; - if (!getMockIterator(hash, key, it)) { - return nullptr; - } - return *it; -} - void Debugger::addOverride(Module *m, uint8_t *interruptData) { const std::string primitive_name = read_string(&interruptData); const std::optional fidx = @@ -1763,20 +1704,9 @@ void Debugger::addOverride(Module *m, uint8_t *interruptData) { } key[param_count] = fidx.value(); - const uint64_t key_hash = FNV1a_uint32_list(key); const uint32_t result = read_B32(&interruptData); channel->write("ack%x;1\n", interruptSetOverridePinValue); - - MockItem *item = getMock(key_hash, key); - if (item) { - item->result = result; - return; - } - - if (overrides.count(key_hash) == 0) { - overrides[key_hash] = {}; - } - overrides[key_hash].push_back(new MockItem{.key = key, .result = result}); + overrides[key] = result; } void Debugger::removeOverride(Module *m, uint8_t *interruptData) { @@ -1796,30 +1726,28 @@ void Debugger::removeOverride(Module *m, uint8_t *interruptData) { key[i] = read_B32(&interruptData); } key[param_count] = fidx.value(); - const uint64_t key_hash = FNV1a_uint32_list(key); - std::list::iterator it; - if (!getMockIterator(key_hash, key, it)) { + if (overrides.erase(key) == 0) { channel->write("ack%x;0\n", interruptUnsetOverridePinValue); return; } - - const MockItem *item = *it; - overrides[key_hash].erase(it); // Invalidates it - delete item; channel->write("ack%x;1\n", interruptUnsetOverridePinValue); } -MockItem *Debugger::getMockForArgs(Module *m, uint32_t fidx) { - uint32_t param_count = m->functions[fidx].type->param_count; +bool Debugger::getMockForArgs(Module *m, uint32_t fidx, uint32_t &result) { + const uint32_t param_count = m->functions[fidx].type->param_count; std::vector key(param_count + 1); const ExecutionContext *ectx = m->warduino->execution_context; for (uint32_t i = 0; i < param_count; i++) { key[i] = ectx->stack[ectx->sp - (param_count - i - 1)].value.uint32; } key[param_count] = fidx; - const uint64_t hash = FNV1a_uint32_list(key); - return getMock(hash, key); + const auto it = overrides.find(key); + if (it == overrides.end()) { + return false; + } + result = it->second; + return true; } bool Debugger::handleContinueFor(Module *m) { diff --git a/src/Debug/debugger.h b/src/Debug/debugger.h index 10a003e5..6a241ab8 100644 --- a/src/Debug/debugger.h +++ b/src/Debug/debugger.h @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include // std::queue @@ -111,9 +110,24 @@ enum class SnapshotPolicy : int { // points where primitives are used. }; -struct MockItem { - std::vector key; // key = args + fidx - uint32_t result; +/* + * FNV-1a 32bit: + * https://datatracker.ietf.org/doc/html/draft-eastlake-fnv-17.html + */ +struct FNV1aVectorHash { + size_t operator()(const std::vector &values) const { + constexpr uint32_t FNV_offset_basis = 0x811c9dc5; + uint32_t result_hash = FNV_offset_basis; + for (const uint32_t v : values) { + for (int i = 0; i < 4; ++i) { + constexpr uint32_t FNV_prime = 0x01000193; + const uint8_t byte = (v >> (i * 8)) & 0xff; + result_hash ^= byte; + result_hash *= FNV_prime; + } + } + return result_hash; + } }; class Debugger { @@ -136,7 +150,8 @@ class Debugger { warduino::mutex *supervisor_mutex; // Mocking - std::unordered_map> overrides; + std::unordered_map, uint32_t, FNV1aVectorHash> + overrides; // Checkpointing SnapshotPolicy snapshotPolicy; @@ -213,9 +228,6 @@ class Debugger { //// Handle mocking - bool getMockIterator(uint32_t hash, const std::vector &key, - std::list::iterator &iter); - MockItem *getMock(uint32_t hash, const std::vector &key); void addOverride(Module *m, uint8_t *interruptData); void removeOverride(Module *m, uint8_t *interruptData); @@ -321,7 +333,7 @@ class Debugger { bool handlePushedEvent(char *bytes) const; // Concolic Multiverse Debugging - MockItem *getMockForArgs(Module *m, uint32_t fidx); + bool getMockForArgs(Module *m, uint32_t fidx, uint32_t &result); // Checkpointing void checkpoint(Module *m, bool force = false); diff --git a/src/Interpreter/instructions.cpp b/src/Interpreter/instructions.cpp index 66c0e26d..d2df36d2 100644 --- a/src/Interpreter/instructions.cpp +++ b/src/Interpreter/instructions.cpp @@ -305,12 +305,12 @@ bool i_instr_call(Module *m) { // Mocking only works on primitives, no need to check for it otherwise. if (ectx->sp >= 0) { - if (const MockItem *mock = - m->warduino->debugger->getMockForArgs(m, fidx)) { + uint32_t mock_result; + if (m->warduino->debugger->getMockForArgs(m, fidx, mock_result)) { const uint32_t param_count = m->functions[fidx].type->param_count; ectx->sp -= static_cast(param_count) - 1; - ectx->stack[ectx->sp].value.uint32 = mock->result; + ectx->stack[ectx->sp].value.uint32 = mock_result; return true; } }