From 32cf623d238670c7e96c70bced6a4c8bd6792ebb Mon Sep 17 00:00:00 2001 From: Guannan Wei Date: Mon, 17 Aug 2026 22:18:49 -0400 Subject: [PATCH 1/3] refactor thread pool to improve safety and performance --- headers/gensym/thread_pool.hpp | 163 +++++++++++++++-------------- src/test/scala/gensym/TestGS.scala | 2 +- 2 files changed, 85 insertions(+), 80 deletions(-) diff --git a/headers/gensym/thread_pool.hpp b/headers/gensym/thread_pool.hpp index 03245d01e..173596216 100644 --- a/headers/gensym/thread_pool.hpp +++ b/headers/gensym/thread_pool.hpp @@ -1,10 +1,9 @@ #ifndef GS_THREAD_POOL_HEADER #define GS_THREAD_POOL_HEADER -// Code adapted and changed from https://github.com/bshoshany/thread-pool/blob/master/thread_pool.hpp - #include #include +#include #include #include #include @@ -16,14 +15,12 @@ #include #include -#ifdef USE_LKFREE_Q -/* Pros: Performance is good for long-running + large number of threads. +/* #include "concurrentqueue/blockingconcurrentqueue.h" + * Pros: Performance is good for long-running + large number of threads. * Cons: * - number-in-queue is an apprimation, leading to a few seconds latency after execution. * - just FIFO queue, no priority. */ -#include "concurrentqueue/blockingconcurrentqueue.h" -#endif using TaskFun = std::function; @@ -51,21 +48,24 @@ struct std::less { class thread_pool { private: - std::atomic running = true; - std::atomic paused = false; + bool running = true; + bool paused = false; + + // Protects task publication and the scheduler state below. A worker first + // reserves a published task under this lock, then removes a task from one of + // the independently locked queues. + mutable std::mutex scheduler_lock; + std::condition_variable task_available; + std::condition_variable tasks_finished; + size_t queued_tasks = 0; + size_t tasks_num_total = 0; -#ifdef USE_LKFREE_Q - moodycamel::ConcurrentQueue Q; -#else std::vector qlocks; std::vector> ptasks; -#endif std::unique_ptr threads; std::unique_ptr thread_ids; - size_t sleep_duration = 500; - std::atomic tasks_num_total = 0; bool inited = false; public: @@ -74,7 +74,11 @@ class thread_pool { thread_pool() : thread_num(0) {} ~thread_pool() { - running = false; + { + const std::scoped_lock lock(scheduler_lock); + running = false; + } + task_available.notify_all(); for (size_t i = 0; i < thread_num; i++) { threads[i].join(); } @@ -85,11 +89,8 @@ class thread_pool { thread_num = n_thread; queue_num = n_queue; -#ifdef USE_LKFREE_Q -#else qlocks = std::vector(n_queue); ptasks = std::vector>(n_queue); -#endif threads.reset(new std::thread[thread_num]); thread_ids.reset(new std::thread::id[thread_num]); @@ -108,105 +109,109 @@ class thread_pool { void queue_add_task(const TaskFun& f, int w) { INFO("Adding task into queue with weight " << w); -#ifdef USE_LKFREE_Q - Q.enqueue({f, w}); -#else unsigned id = rand_int(queue_num)-1; { const std::scoped_lock lock(qlocks.at(id)); ptasks[id].push({f, w}); } -#endif } + void add_task(uint64_t ssid, const TaskFun& f) { - tasks_num_total++; - if (SearcherKind::randomPath == searcher_kind) { - ptree_add_task(ssid, f); - } else { - ASSERT(SearcherKind::randomWeight == searcher_kind, "unknown searcher"); - queue_add_task(f, rand_int(1024)); + { + const std::scoped_lock lock(scheduler_lock); + if (SearcherKind::randomPath == searcher_kind) { + ptree_add_task(ssid, f); + } else { + ASSERT(SearcherKind::randomWeight == searcher_kind, "unknown searcher"); + queue_add_task(f, rand_int(1024)); + } + ++queued_tasks; + ++tasks_num_total; } + task_available.notify_one(); } void worker(unsigned id) { - while (running) { - //std::cout << "Running tasks " << running_tasks_num() - // << "; queued tasks " << tasks_num_queued() << "\n"; + while (true) { struct Task task; + { + std::unique_lock lock(scheduler_lock); + task_available.wait(lock, [this] { + return !running || (!paused && queued_tasks != 0); + }); + if (!running) return; + + --queued_tasks; + } + + // Concurrent reservations and insertions can move the task that backs a + // reservation to a queue already inspected in this pass. Retry until a + // reserved task is found; unlike the old idle loop, this path is entered + // only when published work is known to exist. bool get = false; - if (SearcherKind::randomPath == searcher_kind) { - get = ptree_pop_task(task.f); - } else { - ASSERT(SearcherKind::randomWeight == searcher_kind, "unknown searcher"); - for (size_t i = id; i < id+queue_num; i++) { - if (queue_pop_task(i % queue_num, task)) { get = true; break; } + while (!get) { + if (SearcherKind::randomPath == searcher_kind) { + get = ptree_pop_task(task.f); + } else { + ASSERT(SearcherKind::randomWeight == searcher_kind, "unknown searcher"); + for (size_t i = id; i < id+queue_num; i++) { + if (queue_pop_task(i % queue_num, task)) { get = true; break; } + } } + if (!get) std::this_thread::yield(); + } + + //std::cout << "thread " << std::this_thread::get_id() << " is running; " << running_tasks_num() << "\n"; + try { + task.f(); + } catch (NullDerefException e) { + std::cout << "Warning: read/write at a null location; generating a test case\n"; + check_pc_to_file(e.ss.get()); } - if (!paused && get) { - //std::cout << "thread " << std::this_thread::get_id() << " is running; " << running_tasks_num() << "\n"; - try { - task.f(); - } catch (NullDerefException e) { - std::cout << "Warning: read/write at a null location; generating a test case\n"; - check_pc_to_file(e.ss.get()); + //std::cout << "thread " << std::this_thread::get_id() << " finished\n"; + { + const std::scoped_lock lock(scheduler_lock); + --tasks_num_total; + if (tasks_num_total == 0 || (paused && tasks_num_total == queued_tasks)) { + tasks_finished.notify_all(); } - //std::cout << "thread " << std::this_thread::get_id() << " finished\n"; - tasks_num_total--; - } else { - sleep_or_yield(); } } } bool queue_pop_task(unsigned id, struct Task& task) { -#ifdef USE_LKFREE_Q - bool found = Q.try_dequeue(task); - return found; -#else const std::scoped_lock lock(qlocks.at(id)); if (ptasks[id].empty()) return false; task = std::move(ptasks[id].top()); ptasks[id].pop(); return true; -#endif } void stop_all_tasks() { - running = false; - paused = true; + { + const std::scoped_lock lock(scheduler_lock); + running = false; + paused = true; + } + task_available.notify_all(); + tasks_finished.notify_all(); } void wait_for_tasks() { - while (true) { - if (!paused) { - if (tasks_num_total == 0) break; - } else { - if (running_tasks_num() == 0) break; - } - sleep_or_yield(); - } + std::unique_lock lock(scheduler_lock); + tasks_finished.wait(lock, [this] { + return paused ? tasks_num_total == queued_tasks : tasks_num_total == 0; + }); } size_t running_tasks_num() { - return tasks_num_total - tasks_num_queued(); + const std::scoped_lock lock(scheduler_lock); + return tasks_num_total - queued_tasks; } size_t tasks_num_queued() { -#ifdef USE_LKFREE_Q - return Q.size_approx(); -#else - // FIXME: check balance? - size_t sum = 0; - for (int i = 0; i < ptasks.size(); i++) { - sum += ptasks[i].size(); - } - return sum; -#endif - } - - void sleep_or_yield() { - if (sleep_duration) std::this_thread::sleep_for(std::chrono::milliseconds(sleep_duration)); - else std::this_thread::yield(); + const std::scoped_lock lock(scheduler_lock); + return queued_tasks; } }; diff --git a/src/test/scala/gensym/TestGS.scala b/src/test/scala/gensym/TestGS.scala index 366f7c963..c4008f620 100644 --- a/src/test/scala/gensym/TestGS.scala +++ b/src/test/scala/gensym/TestGS.scala @@ -200,7 +200,7 @@ class Playground extends TestGS { val cases = List(TestPrg( CoreutilsPOSIX.echo, "echo_linked_posix", "@main", noMainFileOpt, - "--output-tests-cov-new --thread=1 --search=random-path --solver=z3 --output-ktest --argv=./echo.bc --sym-stdout --sym-arg 2 --sym-arg 7", + "--output-tests-cov-new --thread=1 --search-strategy=random-path --solver=z3 --output-ktest --argv=./echo.bc --sym-stdout --sym-arg 2 --sym-arg 7", nPath(216136)++status(0))) testGS(gs, cases) } From 66077bf86be190beeb62b91442bf99ced0c10e32 Mon Sep 17 00:00:00 2001 From: Guannan Wei Date: Mon, 17 Aug 2026 23:53:42 -0400 Subject: [PATCH 2/3] add coverage guided search strategy --- .github/workflows/scala.yml | 1 + .../demo-benchmarks/coverage_guided_assert.c | 116 ++++++++++++++++++ headers/gensym/branch.hpp | 18 ++- headers/gensym/cli.hpp | 4 + headers/gensym/defs.hpp | 3 +- headers/gensym/external_imp.hpp | 16 ++- headers/gensym/libcpolyfill.hpp | 4 +- headers/gensym/metadata.hpp | 15 ++- headers/gensym/monitor.hpp | 75 ++++++++++- headers/gensym/runtime.hpp | 12 +- headers/gensym/state_tsnt.hpp | 1 + headers/gensym/thread_pool.hpp | 18 ++- headers/test/Makefile | 5 +- headers/test/coverage_test.cpp | 46 +++++++ runtime/runtime.cpp | 17 ++- src/main/scala/gensym/Codegen.scala | 13 +- src/main/scala/gensym/Driver.scala | 10 +- src/main/scala/gensym/EngineBase.scala | 66 +++++++++- src/main/scala/gensym/GenericDefs.scala | 1 + src/main/scala/gensym/IRUtils.scala | 58 ++++++++- src/main/scala/gensym/ImpCPSEngine.scala | 9 +- src/main/scala/llvm/Benchmarks.scala | 1 + src/test/scala/gensym/CoverageGraphTest.scala | 47 +++++++ src/test/scala/gensym/TestCases.scala | 4 + src/test/scala/gensym/TestGS.scala | 10 ++ 25 files changed, 518 insertions(+), 52 deletions(-) create mode 100644 benchmarks/demo-benchmarks/coverage_guided_assert.c create mode 100644 headers/test/coverage_test.cpp create mode 100644 src/test/scala/gensym/CoverageGraphTest.scala diff --git a/.github/workflows/scala.yml b/.github/workflows/scala.yml index 3250a4b79..b562f03fe 100644 --- a/.github/workflows/scala.yml +++ b/.github/workflows/scala.yml @@ -78,6 +78,7 @@ jobs: sbt 'testOnly gensym.TestImpCPSGS' sbt 'testOnly gensym.TestImpCPSGS_Z3' sbt 'testOnly gensym.TestLibrary' + sbt 'testOnly gensym.CoverageGraphTest' sbt 'testOnly gensym.wasm.TestEval' sbt 'testOnly gensym.wasm.TestScriptRun' sbt 'testOnly gensym.wasm.TestConcolic' diff --git a/benchmarks/demo-benchmarks/coverage_guided_assert.c b/benchmarks/demo-benchmarks/coverage_guided_assert.c new file mode 100644 index 000000000..a65acd3a6 --- /dev/null +++ b/benchmarks/demo-benchmarks/coverage_guided_assert.c @@ -0,0 +1,116 @@ +#include +#include +#include + +void make_symbolic(void *addr, size_t byte_size, ...); +void gs_assert_eager(bool condition, ...); + +#if defined(__clang__) || defined(__GNUC__) +#define NOINLINE __attribute__((noinline)) +#else +#define NOINLINE +#endif + +/* + * This benchmark deliberately separates new coverage from path count. + * warm_up_coverage() visits every block in decoy() and guarded_dispatch() + * before symbolic execution begins. Consequently, a state sent to decoy() + * can create 2^24 paths but cannot discover a new block. The all-ones route + * through guarded_dispatch(), on the other hand, is the shortest route to the + * one block which warm-up does not visit: the failing assertion. + */ +static NOINLINE uint32_t decoy(uint32_t bits) { + volatile uint32_t value = 0x9e3779b9u; + +#define DECOY_STEP(N) \ + do { \ + if (bits & (1u << (N))) \ + value = (value << 5) + value + (uint32_t)(N) + 1u; \ + else \ + value = (value ^ (0x45d9f3bu + (uint32_t)(N))) * 33u; \ + } while (0) + + DECOY_STEP(0); + DECOY_STEP(1); + DECOY_STEP(2); + DECOY_STEP(3); + DECOY_STEP(4); + DECOY_STEP(5); + DECOY_STEP(6); + DECOY_STEP(7); + DECOY_STEP(8); + DECOY_STEP(9); + DECOY_STEP(10); + DECOY_STEP(11); + DECOY_STEP(12); + DECOY_STEP(13); + DECOY_STEP(14); + DECOY_STEP(15); + DECOY_STEP(16); + DECOY_STEP(17); + DECOY_STEP(18); + DECOY_STEP(19); + DECOY_STEP(20); + DECOY_STEP(21); + DECOY_STEP(22); + DECOY_STEP(23); + +#undef DECOY_STEP + return value; +} + +static NOINLINE void hidden_bug(uint32_t key) { + if (key == 0xc0def00du) + gs_assert_eager(false, "coverage-guided benchmark reached the hidden bug"); +} + +static NOINLINE uint32_t guarded_dispatch(uint32_t selector, + uint32_t payload, + uint32_t key) { + if (!(selector & 0x01u)) return decoy(payload); + if (!(selector & 0x02u)) return decoy(payload); + if (!(selector & 0x04u)) return decoy(payload); + if (!(selector & 0x08u)) return decoy(payload); + if (!(selector & 0x10u)) return decoy(payload); + if (!(selector & 0x20u)) return decoy(payload); + if (!(selector & 0x40u)) return decoy(payload); + if (!(selector & 0x80u)) return decoy(payload); + if (!(selector & 0x100u)) return decoy(payload); + if (!(selector & 0x200u)) return decoy(payload); + + hidden_bug(key); + return 0; +} + +static NOINLINE void warm_up_coverage(void) { + /* Visit both sides of every decoy branch without creating symbolic paths. */ + (void)decoy(0u); + (void)decoy(UINT32_MAX); + + /* Visit every early-exit block in guarded_dispatch(). */ + (void)guarded_dispatch(0u, 0u, 0u); + (void)guarded_dispatch(1u, 0u, 0u); + (void)guarded_dispatch(3u, 0u, 0u); + (void)guarded_dispatch(7u, 0u, 0u); + (void)guarded_dispatch(15u, 0u, 0u); + (void)guarded_dispatch(31u, 0u, 0u); + (void)guarded_dispatch(63u, 0u, 0u); + (void)guarded_dispatch(127u, 0u, 0u); + (void)guarded_dispatch(255u, 0u, 0u); + (void)guarded_dispatch(511u, 0u, 0u); + + /* Cover hidden_bug()'s safe path, leaving only its assertion block new. */ + (void)guarded_dispatch(1023u, 0u, 0u); +} + +int main(void) { + uint32_t selector; + uint32_t payload; + + warm_up_coverage(); + make_symbolic(&selector, sizeof(selector), "selector"); + make_symbolic(&payload, sizeof(payload), "payload"); + + (void)guarded_dispatch(selector, payload, 0xc0def00du); + return 0; +} diff --git a/headers/gensym/branch.hpp b/headers/gensym/branch.hpp index 8053fb573..d43bccf8d 100644 --- a/headers/gensym/branch.hpp +++ b/headers/gensym/branch.hpp @@ -5,9 +5,10 @@ inline std::monostate async_exec_block( std::monostate (*f)(SS&, std::function), - SS ss, std::function k) { + BlockLabel target_block, SS ss, std::function k) { if (can_par_tp()) { - tp.add_task(ss.get_ssid(), [f, ss=std::move(ss), k]{ return f((SS&)ss, k); }); + auto task_id = ss.get_ssid(); + tp.add_task(task_id, target_block, [f, ss=std::move(ss), k]{ return f((SS&)ss, k); }); return std::monostate{}; } return f(ss, k); @@ -55,6 +56,7 @@ sym_exec_br(SS& ss, unsigned int block_id, PtrVal t_cond, PtrVal f_cond, inline std::monostate sym_exec_br_k(SS& ss, unsigned int block_id, PtrVal t_cond, PtrVal f_cond, + BlockLabel t_block, BlockLabel f_block, std::function)> tf, std::function)> ff, std::function k) { @@ -69,11 +71,13 @@ sym_exec_br_k(SS& ss, unsigned int block_id, PtrVal t_cond, PtrVal f_cond, tbr_ss.add_PC(t_cond); fbr_ss.add_PC(f_cond); if (can_par_tp()) { - tp.add_task(tbr_ss.get_ssid(), [tf, block_id, tbr_ss=std::move(tbr_ss), k]{ + auto t_task_id = tbr_ss.get_ssid(); + auto f_task_id = fbr_ss.get_ssid(); + tp.add_task(t_task_id, t_block, [tf, block_id, tbr_ss=std::move(tbr_ss), k]{ cov().inc_branch(block_id, 0); return tf((SS&)tbr_ss, k); }); - tp.add_task(fbr_ss.get_ssid(), [ff, block_id, fbr_ss=std::move(fbr_ss), k]{ + tp.add_task(f_task_id, f_block, [ff, block_id, fbr_ss=std::move(fbr_ss), k]{ cov().inc_branch(block_id, 1); return ff((SS&)fbr_ss, k); }); @@ -110,7 +114,7 @@ br_k(SS& ss, PtrVal t_cond, PtrVal f_cond, else return ff(ss, k); } // FIXME: pass correct current block id - return sym_exec_br_k(ss, 0, t_cond, f_cond, tf, ff, k); + return sym_exec_br_k(ss, 0, t_cond, f_cond, unknown_block_id, unknown_block_id, tf, ff, k); } inline immer::flex_vector> @@ -178,7 +182,9 @@ array_lookup_k(SS& ss, PtrVal base, PtrVal offset, size_t esize, auto new_loc = baseloc + (offset_val*esize); auto new_ss = (1 == cnt) ? ss.add_PC(t_cond) : ss.fork().add_PC(t_cond); if (can_par_tp()) { - tp.add_task(new_ss.get_ssid(), [new_loc=std::move(new_loc), new_ss=std::move(new_ss), k]{ return k((SS&)new_ss, new_loc); }); + auto task_block = new_ss.current_block(); + auto task_id = new_ss.get_ssid(); + tp.add_task(task_id, task_block, [new_loc=std::move(new_loc), new_ss=std::move(new_ss), k]{ return k((SS&)new_ss, new_loc); }); } else { k(new_ss, new_loc); } diff --git a/headers/gensym/cli.hpp b/headers/gensym/cli.hpp index 3c2576662..fdda6cba9 100644 --- a/headers/gensym/cli.hpp +++ b/headers/gensym/cli.hpp @@ -55,6 +55,8 @@ inline void set_searcher(std::string& searcher) { searcher_kind = SearcherKind::randomPath; } else if ("random-weight" == searcher) { searcher_kind = SearcherKind::randomWeight; + } else if ("coverage-guided" == searcher) { + searcher_kind = SearcherKind::coverageGuided; } else { ABORT("unknown searcher"); } @@ -97,6 +99,8 @@ inline void print_help(char* main_name) { printf("={stp,z3,disable}"); } else if (key == "symloc-strategy") { printf("={one,feasible,all}"); + } else if (key == "search-strategy") { + printf("={random-path,random-weight,coverage-guided}"); } else { // TODO: doc for other options printf("="); diff --git a/headers/gensym/defs.hpp b/headers/gensym/defs.hpp index dfa1ee94f..705de0f0a 100644 --- a/headers/gensym/defs.hpp +++ b/headers/gensym/defs.hpp @@ -7,6 +7,7 @@ inline std::mutex dt_lock; inline duration debug_time = microseconds::zero(); using BlockLabel = int; +inline constexpr BlockLabel unknown_block_id = -1; using Id = int; using Addr = unsigned int; using IntData = int64_t; @@ -128,7 +129,7 @@ inline std::ofstream gs_log; // Disable output log in stdout inline bool stdout_log = true; -enum class SearcherKind { randomPath, randomWeight }; +enum class SearcherKind { randomPath, randomWeight, coverageGuided }; // The path searcher to be used inline SearcherKind searcher_kind = SearcherKind::randomWeight; diff --git a/headers/gensym/external_imp.hpp b/headers/gensym/external_imp.hpp index 2f93a5658..f02083571 100644 --- a/headers/gensym/external_imp.hpp +++ b/headers/gensym/external_imp.hpp @@ -347,7 +347,9 @@ inline std::monostate __llvm_memcpy(SS& state, List& args, __Cont& args, __Cont{dest, src, conc_size}; SS conc_state = curr_state.copy().add_PC(result[i].first); if (can_par_tp()) { - tp.add_task(conc_state.get_ssid(), [conc_state=std::move(conc_state), conc_args=std::move(conc_args), k]{ return __llvm_memcpy((SS&)conc_state, (List&)conc_args, k); }); + auto task_block = conc_state.current_block(); + auto task_id = conc_state.get_ssid(); + tp.add_task(task_id, task_block, [conc_state=std::move(conc_state), conc_args=std::move(conc_args), k]{ return __llvm_memcpy((SS&)conc_state, (List&)conc_args, k); }); } else { __llvm_memcpy(conc_state, conc_args, k); } @@ -412,7 +416,9 @@ inline std::monostate __llvm_memcpy(SS& state, List& args, __Cont&)conc_args, k); }); + auto task_block = conc_state.current_block(); + auto task_id = conc_state.get_ssid(); + tp.add_task(task_id, task_block, [conc_state=std::move(conc_state), conc_args=std::move(conc_args), k]{ return __llvm_memcpy((SS&)conc_state, (List&)conc_args, k); }); } else { __llvm_memcpy(conc_state, conc_args, k); } @@ -485,7 +491,9 @@ inline std::monostate __llvm_memcpy(SS& state, List& args, __Cont&)conc_args, k); }); + auto task_block = conc_state.current_block(); + auto task_id = conc_state.get_ssid(); + tp.add_task(task_id, task_block, [conc_state=std::move(conc_state), conc_args=std::move(conc_args), k]{ return __llvm_memcpy((SS&)conc_state, (List&)conc_args, k); }); } else { __llvm_memcpy(conc_state, conc_args, k); } diff --git a/headers/gensym/libcpolyfill.hpp b/headers/gensym/libcpolyfill.hpp index 5380c8b7f..a6e24e770 100644 --- a/headers/gensym/libcpolyfill.hpp +++ b/headers/gensym/libcpolyfill.hpp @@ -7,9 +7,9 @@ inline std::monostate gs_dummy(SS&, Args, Cont) { std::cout << "Warning: invoking gs_dummy, some path is not continued!\n"; return std::monostate{}; } -inline std::monostate start_gs_main(SS& state, Args args, Cont cont) { +inline std::monostate start_gs_main(SS& state, Args args, Cont cont, BlockLabel entry_block) { if (can_par_tp()) { - add_task(1, [=] () mutable { return gs_main(state, args, cont); }); + add_task(1, entry_block, [=] () mutable { return gs_main(state, args, cont); }); return std::monostate{}; } return gs_main(state, args, cont); diff --git a/headers/gensym/metadata.hpp b/headers/gensym/metadata.hpp index 21f9accdc..f9fb73f21 100644 --- a/headers/gensym/metadata.hpp +++ b/headers/gensym/metadata.hpp @@ -5,13 +5,19 @@ class MetaData: public Printable { public: uint64_t ssid; BlockLabel bb; + BlockLabel current_bb; bool has_cover_new; List sym_objs; List preferred_cex; MetaData(uint64_t ssid, BlockLabel bb, bool covernew, List sym_objs, List preferred_cex) : - ssid(ssid), bb(bb), has_cover_new(covernew), sym_objs(sym_objs), preferred_cex(preferred_cex) {} - MetaData fork() { return MetaData(ss_fork(ssid), bb, false, sym_objs, preferred_cex); } + ssid(ssid), bb(bb), current_bb(unknown_block_id), has_cover_new(covernew), + sym_objs(sym_objs), preferred_cex(preferred_cex) {} + MetaData fork() { + MetaData result(ss_fork(ssid), bb, false, sym_objs, preferred_cex); + result.current_bb = current_bb; + return result; + } // XXX(GW): what count_name does? just check existence? int count_name(const std::string& name) { for (auto symobj : sym_objs) { @@ -24,6 +30,7 @@ class MetaData: public Printable { ss << "MetaData(" << "ssid : " << ssid << ", " << "bb : " << bb << ", " << + "current_bb : " << current_bb << ", " << "has_cover_new : " << has_cover_new << ", " << "sym_objs : " << vec_to_string(sym_objs) << "preferred_cex : " << vec_to_string(preferred_cex) << ")"; @@ -32,8 +39,8 @@ class MetaData: public Printable { void add_incoming_block(BlockLabel blabel) { bb = blabel; } void cover_block(BlockLabel new_bb) { - bool is_cover_new = cov().is_uncovered(new_bb); - cov().inc_block(new_bb); + current_bb = new_bb; + bool is_cover_new = cov().inc_block(new_bb); has_cover_new = has_cover_new | is_cover_new; } void add_symbolic(const std::string& name, int size, bool is_whole) { diff --git a/headers/gensym/monitor.hpp b/headers/gensym/monitor.hpp index 7783a0163..7af8f796c 100644 --- a/headers/gensym/monitor.hpp +++ b/headers/gensym/monitor.hpp @@ -1,6 +1,9 @@ #ifndef GS_MON_HEADER #define GS_MON_HEADER +#include +#include + /* Coverage information */ struct Monitor { @@ -11,6 +14,14 @@ struct Monitor { uint64_t num_blocks; // The number of execution for each block std::vector block_cov; + // Static interprocedural block graph and its reverse, emitted by the compiler. + std::vector> block_succ; + std::vector> block_pred; + // Distances are rebuilt lazily after a block is covered for the first time. + std::atomic_uint64_t coverage_epoch{0}; + uint64_t distance_epoch = std::numeric_limits::max(); + std::vector distance_to_uncovered; + std::mutex distance_lock; // The number of execution for each branch std::map> branch_cov; // Number of discovered paths @@ -26,15 +37,31 @@ struct Monitor { public: Monitor() : num_blocks(0), num_paths(0), num_states(1), start(steady_clock::now()) {} - Monitor(uint64_t num_blocks, const std::vector> &branch_num) : + Monitor(uint64_t num_blocks, const std::vector> &branch_num, + const std::vector> &successors = {}) : num_blocks(num_blocks), num_paths(0), num_states(1), block_cov(num_blocks), start(steady_clock::now()) { - extend_blocks(num_blocks, branch_num); + extend_blocks(num_blocks, branch_num, successors); } - void extend_blocks(uint64_t nblks, const std::vector> &branch_num) { + void extend_blocks(uint64_t nblks, const std::vector> &branch_num, + const std::vector> &successors = {}) { if (num_blocks != nblks) block_cov = std::move(decltype(block_cov)(num_blocks = nblks)); + if (!successors.empty() || block_succ.size() != num_blocks) { + block_succ.assign(num_blocks, {}); + for (size_t from = 0; from < std::min(num_blocks, successors.size()); ++from) { + for (auto to : successors[from]) { + if (to < num_blocks) block_succ[from].push_back(to); + } + } + block_pred.assign(num_blocks, {}); + for (BlockId from = 0; from < block_succ.size(); ++from) { + for (auto to : block_succ[from]) block_pred[to].push_back(from); + } + distance_to_uncovered.assign(num_blocks, std::numeric_limits::max()); + distance_epoch = std::numeric_limits::max(); + } // `branch_num` contains the ids of blocks whose terminator is br/switch, // for each of such block, `br_arity` is the number of branches. for (const auto& [blk_id, br_arity] : branch_num) { @@ -46,12 +73,46 @@ struct Monitor { } } - void inc_block(BlockId b) { - block_cov[b]++; + bool inc_block(BlockId b) { + ASSERT(b < block_cov.size(), "Invalid block id"); + bool first_visit = block_cov[b].fetch_add(1) == 0; + if (first_visit) coverage_epoch.fetch_add(1); + return first_visit; } bool is_uncovered(BlockId b) { return 0 == block_cov[b]; } + uint64_t coverage_priority(BlockLabel block, uint64_t random_tie) { + if (block < 0 || static_cast(block) >= num_blocks) return random_tie; + const auto epoch = coverage_epoch.load(); + std::scoped_lock lock(distance_lock); + if (distance_epoch != epoch) { + const auto infinity = std::numeric_limits::max(); + distance_to_uncovered.assign(num_blocks, infinity); + std::queue work; + for (BlockId id = 0; id < num_blocks; ++id) { + if (block_cov[id].load() == 0) { + distance_to_uncovered[id] = 0; + work.push(id); + } + } + while (!work.empty()) { + auto current = work.front(); + work.pop(); + for (auto pred : block_pred[current]) { + if (distance_to_uncovered[pred] == infinity) { + distance_to_uncovered[pred] = distance_to_uncovered[current] + 1; + work.push(pred); + } + } + } + distance_epoch = epoch; + } + auto distance = distance_to_uncovered[block]; + if (distance == std::numeric_limits::max()) return random_tie; + uint64_t rank = num_blocks - std::min(distance, num_blocks) + 1; + return rank * 1024 + random_tie; + } void inc_branch(BlockId b, BranchId x) { branch_cov[b][x]++; } @@ -203,4 +264,8 @@ struct Monitor { inline Monitor& cov(); +inline uint64_t coverage_guided_priority(BlockLabel block) { + return cov().coverage_priority(block, rand_uint32() % 1024); +} + #endif diff --git a/headers/gensym/runtime.hpp b/headers/gensym/runtime.hpp index 30b886521..ebbe69550 100644 --- a/headers/gensym/runtime.hpp +++ b/headers/gensym/runtime.hpp @@ -14,7 +14,7 @@ namespace gensym::runtime::v1 { -inline constexpr std::uint32_t api_version = 1; +inline constexpr std::uint32_t api_version = 2; enum class iOP { op_add, op_sub, op_mul, op_sdiv, op_udiv, @@ -44,6 +44,7 @@ using PC = PathCondition; using String = std::string; using Addr = std::uint32_t; using BlockLabel = int; +inline constexpr BlockLabel unknown_block_id = -1; using IntData = std::int64_t; using UIntData = std::uint64_t; using Args = std::vector; @@ -109,6 +110,7 @@ class State { State fork(); State copy() const; std::uint64_t get_ssid() const; + BlockLabel current_block() const; int incoming_block() const; Value env_lookup(int); std::size_t heap_size() const; @@ -141,6 +143,7 @@ class State { struct ProgramConfig { std::size_t block_count = 0; std::vector> branch_arity; + std::vector> block_successors; bool symbolic_uninitialized = false; bool debug = false; }; @@ -148,7 +151,8 @@ struct ProgramConfig { class Coverage { public: void set_num_blocks(std::size_t); - void extend_blocks(std::size_t, const std::vector>&); + void extend_blocks(std::size_t, const std::vector>&, + const std::vector>& = {}); void inc_block(std::size_t); void inc_branch(std::size_t, std::size_t); void inc_path(std::size_t); @@ -166,7 +170,7 @@ void prelude(int argc, char** argv, const ProgramConfig&); void epilogue(); int runtime_exit_code(); bool can_par_tp(); -void add_task(std::uint64_t, std::function); +void add_task(std::uint64_t, BlockLabel, std::function); State make_initial_state(const Args& heap = {}); extern Value g_argc; @@ -199,7 +203,7 @@ Value structV_at(Value, std::size_t); Value make_CPSFunV(CPSFunc); std::monostate cps_apply(Value, State, Args, Cont); std::monostate cont_apply(Cont, State&, Value); -std::monostate sym_exec_br_k(State&, unsigned, Value, Value, Block, Block, Cont); +std::monostate sym_exec_br_k(State&, unsigned, Value, Value, BlockLabel, BlockLabel, Block, Block, Cont); std::vector> array_lookup(State&, Value, Value, std::size_t); std::monostate array_lookup_k(State&, Value, Value, std::size_t, Cont); bool check_pc(PathCondition); diff --git a/headers/gensym/state_tsnt.hpp b/headers/gensym/state_tsnt.hpp index 3827dd9c0..98d1bf63d 100644 --- a/headers/gensym/state_tsnt.hpp +++ b/headers/gensym/state_tsnt.hpp @@ -431,6 +431,7 @@ class SS { } PtrVal heap_lookup(size_t addr) { return heap.at(addr); } uint64_t get_ssid() { return meta.ssid; } + BlockLabel current_block() { return meta.current_bb; } BlockLabel incoming_block() { return meta.bb; } bool has_cover_new() {return meta.has_cover_new; } List get_sym_objs() { return meta.sym_objs + fs.sym_objs; } diff --git a/headers/gensym/thread_pool.hpp b/headers/gensym/thread_pool.hpp index 173596216..85847173b 100644 --- a/headers/gensym/thread_pool.hpp +++ b/headers/gensym/thread_pool.hpp @@ -29,12 +29,13 @@ using TaskFun = std::function; inline void ptree_add_task(uint64_t ssid, const TaskFun& f); inline bool ptree_pop_task(TaskFun& task); inline void check_pc_to_file(const SS& state); +inline uint64_t coverage_guided_priority(BlockLabel block); // The Task structure stored in task pool struct Task { TaskFun f; - int weight; + uint64_t weight; }; template<> @@ -107,7 +108,7 @@ class thread_pool { for (size_t i = 0; i < thread_num; i++) { f(thread_ids[i]); } } - void queue_add_task(const TaskFun& f, int w) { + void queue_add_task(const TaskFun& f, uint64_t w) { INFO("Adding task into queue with weight " << w); unsigned id = rand_int(queue_num)-1; { @@ -116,14 +117,18 @@ class thread_pool { } } - void add_task(uint64_t ssid, const TaskFun& f) { + void add_task(uint64_t ssid, BlockLabel block, const TaskFun& f) { { const std::scoped_lock lock(scheduler_lock); if (SearcherKind::randomPath == searcher_kind) { ptree_add_task(ssid, f); } else { - ASSERT(SearcherKind::randomWeight == searcher_kind, "unknown searcher"); - queue_add_task(f, rand_int(1024)); + ASSERT(SearcherKind::randomWeight == searcher_kind || + SearcherKind::coverageGuided == searcher_kind, "unknown searcher"); + auto weight = SearcherKind::coverageGuided == searcher_kind + ? coverage_guided_priority(block) + : static_cast(rand_int(1024)); + queue_add_task(f, weight); } ++queued_tasks; ++tasks_num_total; @@ -153,7 +158,8 @@ class thread_pool { if (SearcherKind::randomPath == searcher_kind) { get = ptree_pop_task(task.f); } else { - ASSERT(SearcherKind::randomWeight == searcher_kind, "unknown searcher"); + ASSERT(SearcherKind::randomWeight == searcher_kind || + SearcherKind::coverageGuided == searcher_kind, "unknown searcher"); for (size_t i = id; i < id+queue_num; i++) { if (queue_pop_task(i % queue_num, task)) { get = true; break; } } diff --git a/headers/test/Makefile b/headers/test/Makefile index 966bf791b..b6d962035 100644 --- a/headers/test/Makefile +++ b/headers/test/Makefile @@ -1,9 +1,12 @@ FLAGS := -I ../ -I ../../third-party/immer -I ../../third-party/parallel-hashmap -I ../../third-party/stp/build/include/ -L ../../third-party/stp/build/lib/ -lstp -fPIC -targets = fact_lms fact_plain sym_test conc_test stp_test fs_test external_test +targets = fact_lms fact_plain sym_test conc_test stp_test fs_test external_test coverage_test all: $(targets) +coverage_test: coverage_test.cpp ../gensym.hpp + g++ -std=c++17 coverage_test.cpp -o coverage_test $(FLAGS) -pthread + fs_test: fs_test.cpp ../gensym.hpp g++ -std=c++17 fs_test.cpp -o fs_test $(FLAGS) diff --git a/headers/test/coverage_test.cpp b/headers/test/coverage_test.cpp new file mode 100644 index 000000000..478616aab --- /dev/null +++ b/headers/test/coverage_test.cpp @@ -0,0 +1,46 @@ +#include "../gensym.hpp" + +#include +#include +#include + +inline Monitor& cov() { + static Monitor monitor; + return monitor; +} + +int main() { + Monitor monitor(4, {}, {{1}, {2}, {}, {}}); + + assert(monitor.inc_block(0)); + assert(!monitor.inc_block(0)); + assert(monitor.inc_block(1)); + + auto distance_two = monitor.coverage_priority(0, 0); + auto distance_one = monitor.coverage_priority(1, 0); + auto uncovered = monitor.coverage_priority(2, 0); + assert(uncovered > distance_one); + assert(distance_one > distance_two); + assert(monitor.coverage_priority(unknown_block_id, 17) == 17); + assert(monitor.coverage_priority(3, 23) > 23); // block 3 itself is uncovered + + Task frozen_task{[] { return std::monostate{}; }, distance_one}; + assert(monitor.inc_block(2)); + assert(monitor.coverage_priority(1, 31) == 31); // no reachable uncovered block + assert(frozen_task.weight == distance_one); // an enqueued Task keeps this value + + std::priority_queue queue; + queue.push(Task{[] { return std::monostate{}; }, distance_two}); + queue.push(Task{[] { return std::monostate{}; }, uncovered}); + queue.push(Task{[] { return std::monostate{}; }, distance_one}); + assert(queue.top().weight == uncovered); + + Monitor concurrent(1, {}, {{}}); + std::atomic first_visits{0}; + std::vector workers; + for (int i = 0; i < 8; ++i) { + workers.emplace_back([&] { if (concurrent.inc_block(0)) ++first_visits; }); + } + for (auto& worker : workers) worker.join(); + assert(first_visits == 1); +} diff --git a/runtime/runtime.cpp b/runtime/runtime.cpp index 7f910e497..f7de8edcc 100644 --- a/runtime/runtime.cpp +++ b/runtime/runtime.cpp @@ -91,6 +91,7 @@ State::~State() { if (owned_) delete static_cast<::SS*>(impl_); } State State::fork() { return Bridge::own(Bridge::unwrap(*this).fork()); } State State::copy() const { return State(*this); } std::uint64_t State::get_ssid() const { return const_cast<::SS&>(Bridge::unwrap(*this)).get_ssid(); } +BlockLabel State::current_block() const { return const_cast<::SS&>(Bridge::unwrap(*this)).current_block(); } int State::incoming_block() const { return const_cast<::SS&>(Bridge::unwrap(*this)).incoming_block(); } Value State::env_lookup(int id) { return Bridge::wrap(Bridge::unwrap(*this).env_lookup(id)); } std::size_t State::heap_size() const { return const_cast<::SS&>(Bridge::unwrap(*this)).heap_size(); } @@ -126,7 +127,10 @@ static Coverage public_coverage; Coverage& cov() { return public_coverage; } bool debug_enabled() { return ::runtime_debug; } void Coverage::set_num_blocks(std::size_t n) { ::cov().extend_blocks(n, {}); } -void Coverage::extend_blocks(std::size_t n, const std::vector>& branches) { ::cov().extend_blocks(n, branches); } +void Coverage::extend_blocks(std::size_t n, const std::vector>& branches, + const std::vector>& successors) { + ::cov().extend_blocks(n, branches, successors); +} void Coverage::inc_block(std::size_t id) { ::cov().inc_block(id); } void Coverage::inc_branch(std::size_t id, std::size_t branch) { ::cov().inc_branch(id, branch); } void Coverage::inc_path(std::size_t n) { ::cov().inc_path(n); } @@ -139,7 +143,7 @@ void Coverage::print_path_cov() { ::cov().print_path_cov(std::cout); } void configure(const ProgramConfig& config) { ::symbolic_uninit = config.symbolic_uninitialized; ::runtime_debug = config.debug; - ::cov().extend_blocks(config.block_count, config.branch_arity); + ::cov().extend_blocks(config.block_count, config.branch_arity, config.block_successors); } Value g_argc; Value g_argv; @@ -152,7 +156,9 @@ void prelude(int argc, char** argv, const ProgramConfig& config) { void epilogue() { ::epilogue(); } int runtime_exit_code() { return ::exit_code.load().value_or(0); } bool can_par_tp() { return ::can_par_tp(); } -void add_task(std::uint64_t id, std::function task) { ::tp.add_task(id, std::move(task)); } +void add_task(std::uint64_t id, BlockLabel block, std::function task) { + ::tp.add_task(id, block, std::move(task)); +} State make_initial_state(const Args& heap) { if (heap.empty()) return Bridge::own(::mt_ss); @@ -215,8 +221,9 @@ static std::function unwrap_block(Block block) { return block(view, wrap_cont(std::move(cont))); }; } -std::monostate sym_exec_br_k(State& state, unsigned id, Value t, Value f, Block tb, Block fb, Cont cont) { - return ::sym_exec_br_k(Bridge::unwrap(state), id, Bridge::unwrap(t), Bridge::unwrap(f), +std::monostate sym_exec_br_k(State& state, unsigned id, Value t, Value f, + BlockLabel t_id, BlockLabel f_id, Block tb, Block fb, Cont cont) { + return ::sym_exec_br_k(Bridge::unwrap(state), id, Bridge::unwrap(t), Bridge::unwrap(f), t_id, f_id, unwrap_block(std::move(tb)), unwrap_block(std::move(fb)), unwrap_cont(std::move(cont))); } std::vector> array_lookup(State& state, Value base, Value offset, std::size_t size) { diff --git a/src/main/scala/gensym/Codegen.scala b/src/main/scala/gensym/Codegen.scala index e1ef992d4..0a038dfa1 100644 --- a/src/main/scala/gensym/Codegen.scala +++ b/src/main/scala/gensym/Codegen.scala @@ -19,6 +19,7 @@ trait GenericGSCodeGen extends CppSAICodeGenBase { registerHeader("third-party/parallel-hashmap", "") val codegenFolder: String + def coverageGraphInfo: CoverageGraphInfo = CoverageGraphInfo.empty(Counter.block.count) var funMap = new HashMap[Sym, String]() var blockMap = new HashMap[Sym, String]() @@ -189,7 +190,7 @@ trait GenericGSCodeGen extends CppSAICodeGenBase { case Node(s, "print-branch-map", _, _) => es"cov().extend_blocks(${Counter.block.count}, ${Counter.printBranchStat})" case Node(s, "add_tp_task", List(ssid, b: Block), _) => - es"tp.add_task($ssid" + es"tp.add_task($ssid, unknown_block_id" quoteTypedBlock(b, false, true, capture = "=") es")" case Node(s, "async_exec_block", List(ssid, b: Block), _) => @@ -254,7 +255,7 @@ trait GenericGSCodeGen extends CppSAICodeGenBase { } emitln(s""" |inline Monitor& cov() { - | static Monitor m(${Counter.block.count}, ${branchStatStr}); + | static Monitor m(${Counter.block.count}, ${branchStatStr}, ${coverageGraphInfo.cppSuccessors}); | return m; |}""".stripMargin) emitln("/* End of header file */") @@ -303,7 +304,7 @@ trait GenericGSCodeGen extends CppSAICodeGenBase { |int main(int argc, char *argv[]) { | prelude(argc, argv); | if (can_par_tp()) { - | tp.add_task(1, []() { return $name(0); }); + | tp.add_task(1, ${coverageGraphInfo.initialBlock}, []() { return $name(0); }); | } else { | $name(0); | } @@ -382,7 +383,7 @@ trait ImpCPSRuntimeCodeGen extends ImpureGSCodeGen { case Node(s, "list-size", List(xs), _) => es"$xs.size()" case Node(s, "list-isEmpty", List(xs), _) => es"$xs.empty()" case Node(s, "add_tp_task", List(ssid, b: Block), _) => - es"add_task($ssid" + es"add_task($ssid, unknown_block_id" quoteTypedBlock(b, false, true, capture = "=") es")" case _ => super.shallow(n) @@ -421,9 +422,9 @@ trait ImpCPSRuntimeCodeGen extends ImpureGSCodeGen { emit(src) emitln(s""" |int main(int argc, char *argv[]) { - | prelude(argc, argv, ProgramConfig{${Counter.block.count}, ${Counter.printBranchStat}, ${Global.config.symbolicUninit}, ${Global.config.genDebug}}); + | prelude(argc, argv, ProgramConfig{${Counter.block.count}, ${Counter.printBranchStat}, ${coverageGraphInfo.cppSuccessors}, ${Global.config.symbolicUninit}, ${Global.config.genDebug}}); | if (can_par_tp()) { - | add_task(1, []() { return $name(0); }); + | add_task(1, ${coverageGraphInfo.initialBlock}, []() { return $name(0); }); | } else { | $name(0); | } diff --git a/src/main/scala/gensym/Driver.scala b/src/main/scala/gensym/Driver.scala index 8d3faf67a..45c13c4f5 100644 --- a/src/main/scala/gensym/Driver.scala +++ b/src/main/scala/gensym/Driver.scala @@ -226,6 +226,7 @@ abstract class ImpureEngineDriver[A: Manifest, B: Manifest] extends GenericGSDri override lazy val codegen: GenericGSCodeGen = new ImpureGSCodeGen { val IR: q.type = q val codegenFolder = s"$folder/$appName/" + override def coverageGraphInfo = q.coverageGraphInfo setFunMap(q.funNameMap) setBlockMap(q.nodeBlockMap) } @@ -335,6 +336,7 @@ abstract class ImpCPSGSDriver[A: Manifest, B: Manifest]( override lazy val codegen: GenericGSCodeGen = new ImpCPSRuntimeCodeGen { val IR: q.type = q val codegenFolder = s"$folder/$appName/" + override def coverageGraphInfo = q.coverageGraphInfo setFunMap(q.funNameMap) setBlockMap(q.nodeBlockMap) } @@ -460,6 +462,7 @@ class ImpCPSGS_lib extends GenSym with ImpureState { override lazy val codegen: GenericGSCodeGen = new ImpCPSRuntimeCodeGen { val IR: q.type = q val codegenFolder = s"$folder/$appName/" + override def coverageGraphInfo = q.coverageGraphInfo setFunMap(q.funNameMap) setBlockMap(q.nodeBlockMap) override def emitHeaderFile: Unit = { @@ -602,6 +605,7 @@ class ImpCPSGS_lib extends GenSym with ImpureState { folder, appName, CntInfo(Counter.variable.count, Counter.block.count), + coverageGraphInfo, RuntimeABI.Version) val oos = new ObjectOutputStream(new FileOutputStream(s"$folder/$appName/Manifest")) oos.writeObject(module) @@ -620,6 +624,7 @@ class ImpCPSGS_app extends GenSym with ImpureState { override lazy val codegen: GenericGSCodeGen = new ImpCPSRuntimeCodeGen { val IR: q.type = q val codegenFolder = s"$folder/$appName/" + override def coverageGraphInfo = libcdef.coverageGraph.merge(q.coverageGraphInfo) setFunMap(q.funNameMap) setBlockMap(q.nodeBlockMap) override def emitHeaderFile: Unit = { @@ -652,7 +657,7 @@ class ImpCPSGS_app extends GenSym with ImpureState { emit(src) emitln(s""" |int main(int argc, char *argv[]) { - | prelude(argc, argv, ProgramConfig{${Counter.block.count}, ${Counter.printBranchStat}, ${Global.config.symbolicUninit}, ${Global.config.genDebug}}); + | prelude(argc, argv, ProgramConfig{${Counter.block.count}, ${Counter.printBranchStat}, ${coverageGraphInfo.cppSuccessors}, ${Global.config.symbolicUninit}, ${Global.config.genDebug}}); | $name(0); | epilogue(); | return runtime_exit_code(); @@ -679,7 +684,8 @@ class ImpCPSGS_app extends GenSym with ImpureState { ss.updateArg ss.initErrorLoc val k: Rep[Cont] = fun { case sv => checkPCToFile(sv._1) } - "start_gs_main".reflectReadWith[Unit](ss, config.args, k)(fv) + val gsMainEntry = libcdef.coverageGraph.entries.getOrElse("gs_main", -1) + "start_gs_main".reflectReadWith[Unit](ss, config.args, k, gsMainEntry)(fv) } val ss0 = initState "initlib".reflectWith[Unit](ss0, List[Value](), initmain) diff --git a/src/main/scala/gensym/EngineBase.scala b/src/main/scala/gensym/EngineBase.scala index c56b6c9aa..d761e9853 100644 --- a/src/main/scala/gensym/EngineBase.scala +++ b/src/main/scala/gensym/EngineBase.scala @@ -21,7 +21,7 @@ case class Ctx(funName: String, blockLab: String) { } trait EngineBase extends SAIOps { self: BasicDefs with ValueDefs => - import scala.collection.immutable.{List => StaticList, Map => StaticMap} + import scala.collection.immutable.{List => StaticList, Map => StaticMap, Set => StaticSet} import collection.mutable.{HashMap, HashSet} import Constants._ @@ -48,6 +48,70 @@ trait EngineBase extends SAIOps { self: BasicDefs with ValueDefs => val symDefMap: StaticMap[String, IndirectSymbolDef] = m.symDefMap lazy val cfg: CFG = CFG(funMap) + private def functionAliases(f: FunctionDef): StaticSet[String] = + StaticSet(f.id, f.id.stripPrefix("@"), getRealFunName(f.id), "@" + getRealFunName(f.id)) + + private def directCallee(value: LLVMValue): Option[String] = value match { + case GlobalId(id) if symDefMap.contains(id) => directCallee(symDefMap(id).const) + case GlobalId(id) => Some(id) + case BitCastExpr(_, value, _) => directCallee(value) + case _ => None + } + + def coverageGraphInfo: CoverageGraphInfo = { + val graph = Array.fill(Counter.block.count)(StaticSet.empty[Int]) + var entries = StaticMap.empty[String, Int] + var returns = StaticMap.empty[String, List[Int]] + var calls = StaticList.empty[CoverageCallSite] + + funMap.values.foreach { f => + val aliases = functionAliases(f) + f.blocks.headOption.flatMap { block => + Counter.block.getOption(Ctx(f.id, block.label.get).toString) + }.foreach(id => entries ++= aliases.map(_ -> id)) + + val returnIds = f.blocks.collect { + case block if block.term.isInstanceOf[RetTerm] => + Counter.block.getOption(Ctx(f.id, block.label.get).toString) + }.flatten + aliases.foreach(alias => returns += alias -> returnIds) + + f.blocks.foreach { block => + Counter.block.getOption(Ctx(f.id, block.label.get).toString).foreach { from => + val continuationLabels = block.term match { + case BrTerm(label) => StaticList(label) + case CondBrTerm(_, _, thn, els) => StaticList(thn, els) + case SwitchTerm(_, _, default, table) => default :: table.map(_.label) + case _ => StaticList.empty[String] + } + val continuation = continuationLabels.flatMap { label => + Counter.block.getOption(Ctx(f.id, label).toString) + }.distinct + graph(from) ++= continuation + + block.ins.foreach { + case CallInst(_, callee, _) => + directCallee(callee).foreach { name => + calls ::= CoverageCallSite(from, name, if (continuation.nonEmpty) continuation else StaticList(from)) + } + case AssignInst(_, CallInst(_, callee, _)) => + directCallee(callee).foreach { name => + calls ::= CoverageCallSite(from, name, if (continuation.nonEmpty) continuation else StaticList(from)) + } + case _ => + } + } + } + } + + CoverageGraphInfo( + graph.map(_.toVector.sorted).toVector, + entries, + returns, + calls.reverse + ).resolved + } + var heapEnv: StaticMap[String, () => Rep[Value]] = StaticMap() val blockNameMap: HashMap[Int, String] = new HashMap() val nodeBlockMap: HashMap[Backend.Sym, String] = new HashMap() diff --git a/src/main/scala/gensym/GenericDefs.scala b/src/main/scala/gensym/GenericDefs.scala index ec1779ce1..909a380cf 100644 --- a/src/main/scala/gensym/GenericDefs.scala +++ b/src/main/scala/gensym/GenericDefs.scala @@ -27,6 +27,7 @@ case class Counter() { require(s.contains("_")) if (map.contains(s)) map(s) else try { fresh } finally { map(s) = count-1 } } + def getOption(s: String): Option[Int] = map.get(s) } object Counter { diff --git a/src/main/scala/gensym/IRUtils.scala b/src/main/scala/gensym/IRUtils.scala index f358683e5..9fb6d464f 100644 --- a/src/main/scala/gensym/IRUtils.scala +++ b/src/main/scala/gensym/IRUtils.scala @@ -237,10 +237,64 @@ case class CFG(funMap: Map[String, FunctionDef]) { } } +case class CoverageCallSite(block: Int, callee: String, continuation: List[Int]) extends Serializable + +case class CoverageGraphInfo( + successors: Vector[Vector[Int]], + entries: Map[String, Int], + returns: Map[String, List[Int]], + calls: List[CoverageCallSite] +) extends Serializable { + def blockCount: Int = successors.size + def initialBlock: Int = if (entries.isEmpty) -1 else entries.values.min + + def resolved: CoverageGraphInfo = { + val graph = successors.map(_.toSet).toArray + calls.foreach { call => + entries.get(call.callee).foreach { entry => + if (call.block >= 0 && call.block < graph.length) graph(call.block) += entry + returns.getOrElse(call.callee, Nil).foreach { ret => + if (ret >= 0 && ret < graph.length) graph(ret) ++= call.continuation + } + } + } + copy( + successors = graph.map(_.toVector.sorted).toVector, + calls = calls.filterNot(call => entries.contains(call.callee)) + ) + } + + def merge(other: CoverageGraphInfo): CoverageGraphInfo = { + val size = blockCount max other.blockCount + val graph = Array.fill(size)(Set.empty[Int]) + successors.indices.foreach(i => graph(i) ++= successors(i)) + other.successors.indices.foreach(i => graph(i) ++= other.successors(i)) + val keys = returns.keySet ++ other.returns.keySet + val mergedReturns = keys.map { key => + key -> (returns.getOrElse(key, Nil) ++ other.returns.getOrElse(key, Nil)).distinct.sorted + }.toMap + CoverageGraphInfo( + graph.map(_.toVector.sorted).toVector, + entries ++ other.entries, + mergedReturns, + (calls ++ other.calls).distinct + ).resolved + } + + def cppSuccessors: String = + "{" + successors.map(xs => "{" + xs.mkString(",") + "}").mkString(",") + "}" +} + +object CoverageGraphInfo { + def empty(blockCount: Int): CoverageGraphInfo = + CoverageGraphInfo(Vector.fill(blockCount)(Vector.empty), Map.empty, Map.empty, Nil) +} + // Definitions used for generating manifest in separation compilation case class FuncDef(ref: String, name: String) case class VarDef(name: String, off: Int, size: Int) case class CntInfo(vars: Int, blks: Int) -object RuntimeABI { final val Version = 1 } +object RuntimeABI { final val Version = 2 } case class ModDef(funlist: List[FuncDef], varlist: List[VarDef], folder: String, - libName: String, counters: CntInfo, runtimeApiVersion: Int) + libName: String, counters: CntInfo, coverageGraph: CoverageGraphInfo, + runtimeApiVersion: Int) diff --git a/src/main/scala/gensym/ImpCPSEngine.scala b/src/main/scala/gensym/ImpCPSEngine.scala index 9b4968669..7b031f8e3 100644 --- a/src/main/scala/gensym/ImpCPSEngine.scala +++ b/src/main/scala/gensym/ImpCPSEngine.scala @@ -29,8 +29,10 @@ trait ImpCPSGSEngine extends ImpSymExeDefs with EngineBase { val tBrFunName = getRealBlockFunName(Ctx(ctx.funName, tBlockLab)) val fBrFunName = getRealBlockFunName(Ctx(ctx.funName, fBlockLab)) val curBlockId = Counter.block.get(ctx.toString) + val tBlockId = Counter.block.get(Ctx(ctx.funName, tBlockLab).toString) + val fBlockId = Counter.block.get(Ctx(ctx.funName, fBlockLab).toString) "sym_exec_br_k".reflectWriteWith[Unit](ss, curBlockId, tCond, fCond, - unchecked[String](tBrFunName), unchecked[String](fBrFunName), k)(Adapter.CTRL) + tBlockId, fBlockId, unchecked[String](tBrFunName), unchecked[String](fBrFunName), k)(Adapter.CTRL) } def branch(ss: Rep[SS], tCond: Rep[Value], fCond: Rep[Value], @@ -40,10 +42,11 @@ trait ImpCPSGSEngine extends ImpSymExeDefs with EngineBase { "br_k".reflectWriteWith[Unit](ss, tCond, fCond, unchecked[String](tBrFunName), unchecked[String](fBrFunName), k)(Adapter.CTRL) } + @deprecated def asyncExecBlock(funName: String, lab: String, ss: Rep[SS], k: Rep[Cont]): Rep[Unit] = { - // execBlock(funName, lab, ss, k) //TODO: phantom application val realBlockFunName = getRealBlockFunName(Ctx(funName, lab)) - "async_exec_block".reflectWriteWith[Unit](unchecked[String](realBlockFunName), ss, k)(Adapter.CTRL) + val blockId = Counter.block.get(Ctx(funName, lab).toString) + "async_exec_block".reflectWriteWith[Unit](unchecked[String](realBlockFunName), blockId, ss, k)(Adapter.CTRL) } def contApply(cont: Rep[Cont], ss: Rep[SS], v: Rep[Value]): Rep[Unit] = { diff --git a/src/main/scala/llvm/Benchmarks.scala b/src/main/scala/llvm/Benchmarks.scala index 0332f73ba..754282fef 100644 --- a/src/main/scala/llvm/Benchmarks.scala +++ b/src/main/scala/llvm/Benchmarks.scala @@ -83,6 +83,7 @@ object Benchmarks { lazy val binSearch = parseFile("benchmarks/demo-benchmarks/bin_search.ll") lazy val knapsack = parseFile("benchmarks/demo-benchmarks/knapsack.ll") lazy val nqueen = parseFile("benchmarks/demo-benchmarks/nqueen.ll") + lazy val coverageGuidedAssert = parseFile("benchmarks/demo-benchmarks/coverage_guided_assert.ll") lazy val simple0 = parseFile("benchmarks/ccbse/simple_0.ll") lazy val simple1 = parseFile("benchmarks/ccbse/simple_1.ll") diff --git a/src/test/scala/gensym/CoverageGraphTest.scala b/src/test/scala/gensym/CoverageGraphTest.scala new file mode 100644 index 000000000..7d45ebbcd --- /dev/null +++ b/src/test/scala/gensym/CoverageGraphTest.scala @@ -0,0 +1,47 @@ +package gensym + +import org.scalatest.FunSuite + +class CoverageGraphTest extends FunSuite { + test("direct calls add entry and conservative return edges") { + val graph = CoverageGraphInfo( + Vector(Vector(1), Vector.empty, Vector(3), Vector.empty), + Map("@callee" -> 2), + Map("@callee" -> List(3)), + List(CoverageCallSite(0, "@callee", List(1))) + ).resolved + + assert(graph.successors(0) == Vector(1, 2)) + assert(graph.successors(3) == Vector(1)) + } + + test("module merge resolves a previously external call") { + val library = CoverageGraphInfo( + Vector(Vector.empty, Vector.empty), + Map("library_entry" -> 0), + Map("library_entry" -> List(1)), + List(CoverageCallSite(0, "app_main", Nil)) + ) + val application = CoverageGraphInfo( + Vector(Vector.empty, Vector.empty, Vector.empty), + Map("app_main" -> 2), + Map("app_main" -> List(2)), + Nil + ) + + val merged = library.merge(application) + assert(merged.successors.size == 3) + assert(merged.successors(0).contains(2)) + } + + test("unresolved indirect or external calls do not add graph edges") { + val graph = CoverageGraphInfo( + Vector(Vector(1), Vector.empty), + Map.empty, + Map.empty, + List(CoverageCallSite(0, "@external", List(1))) + ).resolved + + assert(graph.successors(0) == Vector(1)) + } +} diff --git a/src/test/scala/gensym/TestCases.scala b/src/test/scala/gensym/TestCases.scala index a27d64b48..05532672c 100644 --- a/src/test/scala/gensym/TestCases.scala +++ b/src/test/scala/gensym/TestCases.scala @@ -131,6 +131,10 @@ object TestCases { TestPrg(binSearch, "binSearch", "@main", noArg, noOpt, nPath(92)), TestPrg(knapsack, "knapsackTest", "@main", noArg, noOpt, nPath(1666)), TestPrg(nqueen, "nQueens", "@main", noArg, noOpt, nPath(1363)), + TestPrg(coverageGuidedAssert, "coverageGuidedAssert", "@main", noArg, + "--thread=1 --search-strategy=coverage-guided --timeout=1", + // If not using coverage-guided search, it cannot find the assertion violation in 1 sec + nPath(11) ++ nTest(1) ++ expectBlocks(148, 148) ++ branches(0, 35, 35)), // The oopsla20 version of maze TestPrg(maze, "mazeTest", "@main", noArg, noOpt, nPath(309)), TestPrg(mp1024, "mp1024Test", "@f", symArg(10), noOpt, nPath(1024)), diff --git a/src/test/scala/gensym/TestGS.scala b/src/test/scala/gensym/TestGS.scala index c4008f620..8315d44a9 100644 --- a/src/test/scala/gensym/TestGS.scala +++ b/src/test/scala/gensym/TestGS.scala @@ -197,10 +197,20 @@ class Playground extends TestGS { //val cases = CoreutilsPOSIX.coreutils + /* val cases = List(TestPrg( CoreutilsPOSIX.echo, "echo_linked_posix", "@main", noMainFileOpt, "--output-tests-cov-new --thread=1 --search-strategy=random-path --solver=z3 --output-ktest --argv=./echo.bc --sym-stdout --sym-arg 2 --sym-arg 7", nPath(216136)++status(0))) + */ + val cases = List( + TestPrg( + coverageGuidedAssert, "coverageGuidedAssert", "@main", noArg, + "--thread=1 --search-strategy=coverage-guided", + nPath(11) ++ nTest(1) ++ expectBlocks(148, 148) ++ branches(0, 35, 35)) + ) testGS(gs, cases) + + } From 38e3d28e98d8baffeefdb922300495fb369c1ee9 Mon Sep 17 00:00:00 2001 From: Guannan Wei Date: Tue, 18 Aug 2026 16:44:26 -0400 Subject: [PATCH 3/3] reset branch stat counter --- src/main/scala/gensym/Driver.scala | 1 + src/test/scala/gensym/TestGS.scala | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/scala/gensym/Driver.scala b/src/main/scala/gensym/Driver.scala index 45c13c4f5..aff2d6c7d 100644 --- a/src/main/scala/gensym/Driver.scala +++ b/src/main/scala/gensym/Driver.scala @@ -365,6 +365,7 @@ trait GenSym { Some(loaded) case None => None } + Counter.branchStat.clear() libdef match { case Some(modref) => // library linking mode - set counters to specified values Counter.block.reset(modref.counters.blks) diff --git a/src/test/scala/gensym/TestGS.scala b/src/test/scala/gensym/TestGS.scala index 8315d44a9..20ce7cb63 100644 --- a/src/test/scala/gensym/TestGS.scala +++ b/src/test/scala/gensym/TestGS.scala @@ -205,10 +205,10 @@ class Playground extends TestGS { nPath(216136)++status(0))) */ val cases = List( - TestPrg( - coverageGuidedAssert, "coverageGuidedAssert", "@main", noArg, - "--thread=1 --search-strategy=coverage-guided", - nPath(11) ++ nTest(1) ++ expectBlocks(148, 148) ++ branches(0, 35, 35)) + TestPrg(coverageGuidedAssert, "coverageGuidedAssert", "@main", noArg, + "--thread=1 --search-strategy=coverage-guided --timeout=1", + // If not using coverage-guided search, it cannot find the assertion violation in 1 sec + nPath(11) ++ nTest(1) ++ expectBlocks(148, 148) ++ branches(0, 35, 35)), ) testGS(gs, cases)