From d698c5bbd5806745e13076ba64c18a97529a70ae 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 fb1ab4df53e1d83ecba876b64602324ddf86b097 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 91f5e3609da84ff94f469e40e288bb41dfd2b15f 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 e9aeacf01254abf4165c864d6329fbe34e401d2a 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 ab9a3e882c8f58e7f9b94b709840da26866f3944 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 ad2e598376aacb9e8afbaef04c97124a70c610a2 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 ae8e4bfddf1892f7e4e3d551525a40e58cd09f3f 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 7c9cfd9ec14b5ba6efb530bea496671f3d1b9218 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 7825aa0a5552ba8b119917dadbe089d86f636b07 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 6517072525fc1b8f9be12aa1727cfa6ee1d02791 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; } }