Logo AND Algorithmique Numérique Distribuée

Public GIT Repository
Merge branch 'odpor-implementation' into 'master'
authorMartin Quinson <martin.quinson@ens-rennes.fr>
Mon, 5 Jun 2023 10:22:51 +0000 (10:22 +0000)
committerMartin Quinson <martin.quinson@ens-rennes.fr>
Mon, 5 Jun 2023 10:22:51 +0000 (10:22 +0000)
Introduce SDPOR and ODPOR into Simgrid

See merge request simgrid/simgrid!154

41 files changed:
MANIFEST.in
examples/cpp/mc-bugged2/s4u-mc-bugged2.tesh
examples/cpp/synchro-mutex/s4u-mc-synchro-mutex-stateful.tesh
examples/cpp/synchro-mutex/s4u-mc-synchro-mutex.tesh
examples/sthread/pthread-mc-mutex-simple.tesh
examples/sthread/pthread-mc-mutex-simpledeadlock.tesh
examples/sthread/pthread-mc-producer-consumer.tesh
src/mc/api/ActorState.hpp
src/mc/api/ClockVector.cpp [new file with mode: 0644]
src/mc/api/ClockVector.hpp [new file with mode: 0644]
src/mc/api/State.cpp
src/mc/api/State.hpp
src/mc/explo/CommunicationDeterminismChecker.cpp
src/mc/explo/DFSExplorer.cpp
src/mc/explo/DFSExplorer.hpp
src/mc/explo/Exploration.hpp
src/mc/explo/odpor/ClockVector_test.cpp [new file with mode: 0644]
src/mc/explo/odpor/Execution.cpp [new file with mode: 0644]
src/mc/explo/odpor/Execution.hpp [new file with mode: 0644]
src/mc/explo/odpor/Execution_test.cpp [new file with mode: 0644]
src/mc/explo/odpor/ReversibleRaceCalculator.cpp [new file with mode: 0644]
src/mc/explo/odpor/ReversibleRaceCalculator.hpp [new file with mode: 0644]
src/mc/explo/odpor/WakeupTree.cpp [new file with mode: 0644]
src/mc/explo/odpor/WakeupTree.hpp [new file with mode: 0644]
src/mc/explo/odpor/WakeupTreeIterator.cpp [new file with mode: 0644]
src/mc/explo/odpor/WakeupTreeIterator.hpp [new file with mode: 0644]
src/mc/explo/odpor/WakeupTree_test.cpp [new file with mode: 0644]
src/mc/explo/odpor/odpor_forward.hpp [new file with mode: 0644]
src/mc/explo/odpor/odpor_tests_private.hpp [new file with mode: 0644]
src/mc/explo/simgrid_mc.cpp
src/mc/mc_config.cpp
src/mc/mc_config.hpp
src/mc/transition/TransitionActorJoin.cpp
src/mc/transition/TransitionAny.cpp
src/mc/transition/TransitionComm.cpp
src/mc/transition/TransitionObjectAccess.cpp
src/mc/transition/TransitionRandom.hpp
src/mc/transition/TransitionSynchro.cpp
teshsuite/smpi/coll-allreduce-with-leaks/mc-coll-allreduce-with-leaks.tesh
tools/cmake/DefinePackages.cmake
tools/cmake/Tests.cmake

index f4fe53a..1fa074d 100644 (file)
@@ -2185,6 +2185,8 @@ include src/mc/AddressSpace.hpp
 include src/mc/VisitedState.cpp
 include src/mc/VisitedState.hpp
 include src/mc/api/ActorState.hpp
+include src/mc/api/ClockVector.cpp
+include src/mc/api/ClockVector.hpp
 include src/mc/api/RemoteApp.cpp
 include src/mc/api/RemoteApp.hpp
 include src/mc/api/State.cpp
@@ -2203,6 +2205,19 @@ include src/mc/explo/LivenessChecker.cpp
 include src/mc/explo/LivenessChecker.hpp
 include src/mc/explo/UdporChecker.cpp
 include src/mc/explo/UdporChecker.hpp
+include src/mc/explo/odpor/ClockVector_test.cpp
+include src/mc/explo/odpor/Execution.cpp
+include src/mc/explo/odpor/Execution.hpp
+include src/mc/explo/odpor/Execution_test.cpp
+include src/mc/explo/odpor/ReversibleRaceCalculator.cpp
+include src/mc/explo/odpor/ReversibleRaceCalculator.hpp
+include src/mc/explo/odpor/WakeupTree.cpp
+include src/mc/explo/odpor/WakeupTree.hpp
+include src/mc/explo/odpor/WakeupTreeIterator.cpp
+include src/mc/explo/odpor/WakeupTreeIterator.hpp
+include src/mc/explo/odpor/WakeupTree_test.cpp
+include src/mc/explo/odpor/odpor_forward.hpp
+include src/mc/explo/odpor/odpor_tests_private.hpp
 include src/mc/explo/simgrid_mc.cpp
 include src/mc/explo/udpor/Comb.hpp
 include src/mc/explo/udpor/Configuration.cpp
index 653cbf9..56813fd 100644 (file)
@@ -16,7 +16,7 @@ $ $VALGRIND_NO_TRACE_CHILDREN ${bindir:=.}/../../../bin/simgrid-mc --cfg=model-c
 > [0.000000] [mc_explo/INFO]   3: iSend(mbox=0)
 > [0.000000] [mc_explo/INFO]   1: WaitComm(from 3 to 1, mbox=0, no timeout)
 > [0.000000] [mc_explo/INFO] You can debug the problem (and see the whole details) by rerunning out of simgrid-mc with --cfg=model-check/replay:'1;3;1;1;3;3;1'
-> [0.000000] [mc_dfs/INFO] DFS exploration ended. 551 unique states visited; 137 backtracks (2239 transition replays, 1551 states visited overall)
+> [0.000000] [mc_dfs/INFO] DFS exploration ended. 1070 unique states visited; 252 backtracks (4082 transition replays, 2760 states visited overall)
 
 ! expect return 1
 ! timeout 20
@@ -26,12 +26,13 @@ $ $VALGRIND_NO_TRACE_CHILDREN ${bindir:=.}/../../../bin/simgrid-mc --cfg=model-c
 > [0.000000] [mc_explo/INFO] *** PROPERTY NOT VALID ***
 > [0.000000] [mc_explo/INFO] **************************
 > [0.000000] [mc_explo/INFO] Counter-example execution trace:
-> [0.000000] [mc_explo/INFO]   3: iSend(mbox=0)
 > [0.000000] [mc_explo/INFO]   1: iRecv(mbox=0)
+> [0.000000] [mc_explo/INFO]   3: iSend(mbox=0)
 > [0.000000] [mc_explo/INFO]   1: WaitComm(from 3 to 1, mbox=0, no timeout)
 > [0.000000] [mc_explo/INFO]   1: iRecv(mbox=0)
 > [0.000000] [mc_explo/INFO]   3: WaitComm(from 3 to 1, mbox=0, no timeout)
 > [0.000000] [mc_explo/INFO]   3: iSend(mbox=0)
 > [0.000000] [mc_explo/INFO]   1: WaitComm(from 3 to 1, mbox=0, no timeout)
-> [0.000000] [mc_explo/INFO] You can debug the problem (and see the whole details) by rerunning out of simgrid-mc with --cfg=model-check/replay:'3;1;1;1;3;3;1'
-> [0.000000] [mc_dfs/INFO] DFS exploration ended. 1161 unique states visited; 282 backtracks (4556 transition replays, 3113 states visited overall)
+> [0.000000] [mc_explo/INFO] You can debug the problem (and see the whole details) by rerunning out of simgrid-mc with --cfg=model-check/replay:'1;3;1;1;3;3;1'
+> [0.000000] [mc_dfs/INFO] DFS exploration ended. 995 unique states visited; 253 backtracks (4006 transition replays, 2758 states visited overall)
+
index 3d1e75d..8ca73b7 100644 (file)
@@ -7,7 +7,7 @@ $ $VALGRIND_NO_TRACE_CHILDREN ${bindir:=.}/../../../bin/simgrid-mc --cfg=model-c
 > [0.000000] [xbt_cfg/INFO] Configuration change: Set 'model-check/sleep-set' to 'true'
 > [0.000000] [xbt_cfg/INFO] Configuration change: Set 'actors' to '2'
 > [0.000000] [mc_dfs/INFO] Start a DFS exploration. Reduction is: dpor.
-> [0.000000] [mc_dfs/INFO] DFS exploration ended. 130 unique states visited; 27 backtracks (209 transition replays, 52 states visited overall)
+> [0.000000] [mc_dfs/INFO] DFS exploration ended. 66 unique states visited; 11 backtracks (97 transition replays, 20 states visited overall)
 
 p The stats without checkpoints is:               130 unique states visited; 27 backtracks (308 transition replays, 151 states visited overall)
 p But it runs much faster (0.6 sec vs. 1.6 sec), damn slow checkpointing code.
index 3ef36cc..4de47b1 100644 (file)
@@ -81,11 +81,11 @@ $ $VALGRIND_NO_TRACE_CHILDREN ${bindir:=.}/../../../bin/simgrid-mc --cfg=model-c
 > [0.000000] [xbt_cfg/INFO] Configuration change: Set 'model-check/sleep-set' to 'true'
 > [0.000000] [xbt_cfg/INFO] Configuration change: Set 'actors' to '2'
 > [0.000000] [mc_dfs/INFO] Start a DFS exploration. Reduction is: dpor.
-> [0.000000] [mc_dfs/INFO] DFS exploration ended. 130 unique states visited; 27 backtracks (308 transition replays, 151 states visited overall)
+> [0.000000] [mc_dfs/INFO] DFS exploration ended. 66 unique states visited; 11 backtracks (126 transition replays, 49 states visited overall)
 
 $ $VALGRIND_NO_TRACE_CHILDREN ${bindir:=.}/../../../bin/simgrid-mc --cfg=model-check/sleep-set:true --cfg=model-check/strategy:nb_wait -- ${bindir:=.}/s4u-synchro-mutex  --cfg=actors:3 --log=s4u_test.thres:critical
 > [0.000000] [xbt_cfg/INFO] Configuration change: Set 'model-check/sleep-set' to 'true'
 > [0.000000] [xbt_cfg/INFO] Configuration change: Set 'model-check/strategy' to 'nb_wait'
 > [0.000000] [xbt_cfg/INFO] Configuration change: Set 'actors' to '3'
 > [0.000000] [mc_dfs/INFO] Start a DFS exploration. Reduction is: dpor.
-> [0.000000] [mc_dfs/INFO] DFS exploration ended. 3492 unique states visited; 743 backtracks (12498 transition replays, 8263 states visited overall)
\ No newline at end of file
+> [0.000000] [mc_dfs/INFO] DFS exploration ended. 296 unique states visited; 52 backtracks (765 transition replays, 417 states visited overall)
\ No newline at end of file
index d6d474a..3946c2d 100644 (file)
@@ -9,10 +9,10 @@ $ $VALGRIND_NO_TRACE_CHILDREN ${bindir:=.}/../../bin/simgrid-mc --cfg=model-chec
 > The thread 0 is terminating.
 > The thread 1 is terminating.
 > User's main is terminating.
-> The thread 1 is terminating.
 > The thread 0 is terminating.
+> The thread 1 is terminating.
 > User's main is terminating.
-> The thread 0 is terminating.
 > The thread 1 is terminating.
+> The thread 0 is terminating.
 > User's main is terminating.
 > [0.000000] [mc_dfs/INFO] DFS exploration ended. 23 unique states visited; 2 backtracks (27 transition replays, 2 states visited overall)
\ No newline at end of file
index 666325f..590fdc6 100644 (file)
@@ -12,6 +12,9 @@ $ $VALGRIND_NO_TRACE_CHILDREN ${bindir:=.}/../../bin/simgrid-mc --cfg=model-chec
 > The thread 0 is terminating.
 > The thread 1 is terminating.
 > User's main is terminating.
+> The thread 0 is terminating.
+> The thread 1 is terminating.
+> User's main is terminating.
 > [0.000000] [mc_global/INFO] **************************
 > [0.000000] [mc_global/INFO] *** DEADLOCK DETECTED ***
 > [0.000000] [mc_global/INFO] **************************
@@ -28,4 +31,4 @@ $ $VALGRIND_NO_TRACE_CHILDREN ${bindir:=.}/../../bin/simgrid-mc --cfg=model-chec
 > [0.000000] [mc_global/INFO]   3: MUTEX_WAIT(mutex: 1, owner: 3)
 > [0.000000] [mc_global/INFO]   3: MUTEX_ASYNC_LOCK(mutex: 0, owner: 2)
 > [0.000000] [mc_Session/INFO] You can debug the problem (and see the whole details) by rerunning out of simgrid-mc with --cfg=model-check/replay:'2;2;3;2;3;3'
-> [0.000000] [mc_dfs/INFO] DFS exploration ended. 19 unique states visited; 1 backtracks (22 transition replays, 2 states visited overall)
\ No newline at end of file
+> [0.000000] [mc_dfs/INFO] DFS exploration ended. 28 unique states visited; 2 backtracks (37 transition replays, 7 states visited overall)
\ No newline at end of file
index 404282a..89be825 100644 (file)
@@ -5,11 +5,11 @@ $ $VALGRIND_NO_TRACE_CHILDREN ${bindir:=.}/../../bin/simgrid-mc --cfg=model-chec
 > [0.000000] [xbt_cfg/INFO] Configuration change: Set 'model-check/sleep-set' to 'true'
 > [0.000000] [sthread/INFO] Starting the simulation.
 > [0.000000] [mc_dfs/INFO] Start a DFS exploration. Reduction is: dpor.
-> [0.000000] [mc_dfs/INFO] DFS exploration ended. 1101 unique states visited; 136 backtracks (2950 transition replays, 1713 states visited overall)
+> [0.000000] [mc_dfs/INFO] DFS exploration ended. 106 unique states visited; 17 backtracks (295 transition replays, 172 states visited overall)
 
 $ $VALGRIND_NO_TRACE_CHILDREN ${bindir:=.}/../../bin/simgrid-mc --cfg=model-check/sleep-set:true --cfg=model-check/strategy:nb_wait --cfg=model-check/setenv:LD_PRELOAD=${libdir:=.}/libsgmalloc.so:${libdir:=.}/libsthread.so ${bindir:=.}/pthread-producer-consumer -q -c 2 -C 1 -p 2 -P 1
 > [0.000000] [xbt_cfg/INFO] Configuration change: Set 'model-check/sleep-set' to 'true'
 > [0.000000] [xbt_cfg/INFO] Configuration change: Set 'model-check/strategy' to 'nb_wait'
 > [0.000000] [sthread/INFO] Starting the simulation.
 > [0.000000] [mc_dfs/INFO] Start a DFS exploration. Reduction is: dpor.
-> [0.000000] [mc_dfs/INFO] DFS exploration ended. 1101 unique states visited; 136 backtracks (2950 transition replays, 1713 states visited overall)
\ No newline at end of file
+> [0.000000] [mc_dfs/INFO] DFS exploration ended. 107 unique states visited; 18 backtracks (300 transition replays, 175 states visited overall)
\ No newline at end of file
index 315ed8a..2e8e136 100644 (file)
@@ -9,6 +9,7 @@
 #include "src/kernel/activity/CommImpl.hpp"
 #include "src/mc/remote/RemotePtr.hpp"
 
+#include <algorithm>
 #include <exception>
 #include <vector>
 
@@ -97,8 +98,10 @@ public:
       mark_done();
     return times_considered_++;
   }
+  unsigned int get_max_considered() const { return max_consider_; }
   unsigned int get_times_considered() const { return times_considered_; }
   unsigned int get_times_not_considered() const { return max_consider_ - times_considered_; }
+  bool has_more_to_consider() const { return get_times_not_considered() > 0; }
   aid_t get_aid() const { return aid_; }
 
   /* returns whether the actor is marked as enabled in the application side */
@@ -115,16 +118,42 @@ public:
   }
   void mark_done() { this->state_ = InterleavingType::done; }
 
-  inline Transition* get_transition(unsigned times_considered) const
+  /**
+   * @brief Retrieves the transition that we should consider for execution by
+   * this actor from the State instance with respect to which this ActorState object
+   * is considered
+   */
+  std::shared_ptr<Transition> get_transition() const
+  {
+    // The rationale for this selection is as follows:
+    //
+    // 1. For transitions with only one possibility of execution,
+    // we always wish to select action `0` even if we've
+    // marked the transition already as considered (which
+    // we'll do if we explore a trace following that transition).
+    //
+    // 2. For transitions that can be considered multiple
+    // times, we want to be sure to select the most up-to-date
+    // action. In general, this means selecting that which is
+    // now being considered at this state. If, however, we've
+    // executed the
+    //
+    // The formula satisfies both of the above conditions:
+    //
+    // > std::clamp(times_considered_, 0u, max_consider_ - 1)
+    return get_transition(std::clamp(times_considered_, 0u, max_consider_ - 1));
+  }
+
+  std::shared_ptr<Transition> get_transition(unsigned times_considered) const
   {
     xbt_assert(times_considered < this->pending_transitions_.size(),
                "Actor %ld does not have a state available transition with `times_considered = %u`,\n"
                "yet one was asked for",
                aid_, times_considered);
-    return this->pending_transitions_[times_considered].get();
+    return this->pending_transitions_[times_considered];
   }
 
-  inline void set_transition(std::shared_ptr<Transition> t, unsigned times_considered)
+  void set_transition(std::shared_ptr<Transition> t, unsigned times_considered)
   {
     xbt_assert(times_considered < this->pending_transitions_.size(),
                "Actor %ld does not have a state available transition with `times_considered = %u`, "
diff --git a/src/mc/api/ClockVector.cpp b/src/mc/api/ClockVector.cpp
new file mode 100644 (file)
index 0000000..b411bba
--- /dev/null
@@ -0,0 +1,23 @@
+/* Copyright (c) 2015-2023. The SimGrid Team. All rights reserved.          */
+
+/* This program is free software; you can redistribute it and/or modify it
+ * under the terms of the license (GNU LGPL) which comes with this package. */
+
+#include "src/mc/api/ClockVector.hpp"
+
+namespace simgrid::mc {
+
+ClockVector ClockVector::max(const ClockVector& cv1, const ClockVector& cv2)
+{
+  auto max_vector = ClockVector();
+
+  for (const auto& [aid, value] : cv1.contents)
+    max_vector[aid] = std::max(value, cv2.get(aid).value_or(0));
+
+  for (const auto& [aid, value] : cv2.contents)
+    max_vector[aid] = std::max(value, cv1.get(aid).value_or(0));
+
+  return max_vector;
+}
+
+} // namespace simgrid::mc
\ No newline at end of file
diff --git a/src/mc/api/ClockVector.hpp b/src/mc/api/ClockVector.hpp
new file mode 100644 (file)
index 0000000..5e6318b
--- /dev/null
@@ -0,0 +1,130 @@
+/* Copyright (c) 2016-2023. The SimGrid Team. All rights reserved.          */
+
+/* This program is free software; you can redistribute it and/or modify it
+ * under the terms of the license (GNU LGPL) which comes with this package. */
+
+#ifndef SIMGRID_MC_CLOCK_VECTOR_HPP
+#define SIMGRID_MC_CLOCK_VECTOR_HPP
+
+#include "simgrid/forward.h"
+
+#include <cstdint>
+#include <initializer_list>
+#include <optional>
+#include <unordered_map>
+
+namespace simgrid::mc {
+
+/**
+ * @brief A multi-dimensional vector used to keep track of
+ * happens-before relation between dependent events in an
+ * execution of DPOR, SDPOR, or ODPOR
+ *
+ * Clock vectors permit actors in a distributed system
+ * to determine whether two events occurred one after the other
+ * but they may not have); i.e. they permit actors to keep track of "time".
+ * A clever observation made in the original DPOR paper is that a
+ * transition-based "happens-before" relation can be computed for
+ * any particular trace `S` using clock vectors, effectively
+ * treating dependency like the passing of a message (the context
+ * in which vector clocks are typically used).
+ *
+ * Support, however, needs to be added to clock vectors since
+ * Simgrid permits the *creation* of new actors during execution.
+ * Since we don't know this size before-hand, we have to allow
+ * clock vectors to behave as if they were "infinitely" large. To
+ * do so, all newly mapped elements, if not assigned a value, are
+ * defaulted to `0`. This corresponds to the value this actor would
+ * have had regardless had its creation been known to have evnetually
+ * occurred: no actions taken by that actor had occurred prior, so
+ * there's no way the clock vector would have been updated. In other
+ * words, when comparing clock vectors of different sizes, it's equivalent
+ * to imagine both of the same size with elements absent in one or
+ * the other implicitly mapped to zero.
+ */
+struct ClockVector final {
+private:
+  std::unordered_map<aid_t, uint32_t> contents;
+
+public:
+  ClockVector()                              = default;
+  ClockVector(const ClockVector&)            = default;
+  ClockVector& operator=(ClockVector const&) = default;
+  ClockVector(ClockVector&&)                 = default;
+  ClockVector(std::initializer_list<std::pair<const aid_t, uint32_t>> init) : contents(std::move(init)) {}
+
+  /**
+   * @brief The number of components in this
+   * clock vector
+   *
+   * A `ClockVector` implicitly maps the id of an actor
+   * it does not contain to a default value of `0`.
+   * Thus, a `ClockVector` is "lazy" in the sense
+   * that new actors are "automatically" mapped
+   * without needing to be explicitly added the clock
+   * vector when the actor is created. This means that
+   * comparison between clock vectors is possible
+   * even as actors become enabled and disabled
+   *
+   * @return uint32_t the number of elements in
+   * the clock vector
+   */
+  size_t size() const { return this->contents.size(); }
+
+  uint32_t& operator[](aid_t aid)
+  {
+    // NOTE: The `operator[]` overload of
+    // unordered_map will create a new key-value
+    // pair if `tid` does not exist and will use
+    // a _default_ value for the value (0 in this case)
+    // which is precisely what we want here
+    return this->contents[aid];
+  }
+
+  /**
+   * @brief Retrieves the value mapped to the given
+   * actor if it is contained in this clock vector
+   */
+  std::optional<uint32_t> get(aid_t aid) const
+  {
+    if (const auto iter = this->contents.find(aid); iter != this->contents.end())
+      return std::optional<uint32_t>{iter->second};
+    return std::nullopt;
+  }
+
+  /**
+   * @brief Computes a clock vector whose components
+   * are larger than the components of both of
+   * the given clock vectors
+   *
+   * The maximum of two clock vectors is definied to
+   * be the clock vector whose components are the maxmimum
+   * of the corresponding components of the arguments.
+   * Since the `ClockVector` class is "lazy", the two
+   * clock vectors given as arguments may not be of the same size.
+   * The resultant clock vector has components as follows:
+   *
+   * 1. For each actor that each clock vector maps, the
+   * resulting clock vector maps that thread to the maxmimum
+   * of the values mapped for the actor in each clock vector
+   *
+   * 2. For each actor that only a single clock vector maps,
+   * the resulting clock vector maps that thread to the
+   * value mapped by the lone clock vector
+   *
+   * The scheme is equivalent to assuming that an unmapped
+   * thread by any one clock vector is implicitly mapped to zero
+   *
+   * @param cv1 the first clock vector
+   * @param cv2  the second clock vector
+   * @return a clock vector whose components are at
+   * least as large as the corresponding components of each clock
+   * vector and whose size is large enough to contain the union
+   * of components of each clock vector
+   */
+  static ClockVector max(const ClockVector& cv1, const ClockVector& cv2);
+};
+
+} // namespace simgrid::mc
+
+#endif
index a178f55..b05bf39 100644 (file)
@@ -9,6 +9,7 @@
 #include "src/mc/explo/Exploration.hpp"
 #include "src/mc/mc_config.hpp"
 
+#include <algorithm>
 #include <boost/range/algorithm.hpp>
 
 XBT_LOG_NEW_DEFAULT_SUBCATEGORY(mc_state, mc, "Logging specific to MC states");
@@ -62,16 +63,15 @@ State::State(RemoteApp& remote_app, std::shared_ptr<State> parent_state)
      * And if we kept it and the actor is enabled in this state, mark the actor as already done, so that
      * it is not explored*/
     for (auto& [aid, transition] : parent_state_->get_sleep_set()) {
-      if (not incoming_transition_->depends(&transition)) {
+      if (not incoming_transition_->depends(transition.get())) {
         sleep_set_.try_emplace(aid, transition);
         if (strategy_->actors_to_run_.count(aid) != 0) {
           XBT_DEBUG("Actor %ld will not be explored, for it is in the sleep set", aid);
-
           strategy_->actors_to_run_.at(aid).mark_done();
         }
       } else
         XBT_DEBUG("Transition >>%s<< removed from the sleep set because it was dependent with incoming >>%s<<",
-                  transition.to_string().c_str(), incoming_transition_->to_string().c_str());
+                  transition->to_string().c_str(), incoming_transition_->to_string().c_str());
     }
   }
 }
@@ -119,6 +119,11 @@ std::pair<aid_t, int> State::next_transition_guided() const
   return strategy_->next_transition();
 }
 
+aid_t State::next_odpor_transition() const
+{
+  return wakeup_tree_.get_min_single_process_actor().value_or(-1);
+}
+
 // This should be done in GuidedState, or at least interact with it
 std::shared_ptr<Transition> State::execute_next(aid_t next, RemoteApp& app)
 {
@@ -131,7 +136,7 @@ std::shared_ptr<Transition> State::execute_next(aid_t next, RemoteApp& app)
   // when simcall_handle will be called on it
   auto& actor_state                        = strategy_->actors_to_run_.at(next);
   const unsigned times_considered          = actor_state.do_consider();
-  const auto* expected_executed_transition = actor_state.get_transition(times_considered);
+  const auto* expected_executed_transition = actor_state.get_transition(times_considered).get();
   xbt_assert(expected_executed_transition != nullptr,
              "Expected a transition with %u times considered to be noted in actor %ld", times_considered, next);
 
@@ -160,4 +165,122 @@ std::shared_ptr<Transition> State::execute_next(aid_t next, RemoteApp& app)
 
   return outgoing_transition_;
 }
+
+std::unordered_set<aid_t> State::get_backtrack_set() const
+{
+  std::unordered_set<aid_t> actors;
+  for (const auto& [aid, state] : get_actors_list()) {
+    if (state.is_todo() or state.is_done()) {
+      actors.insert(aid);
+    }
+  }
+  return actors;
+}
+
+std::unordered_set<aid_t> State::get_sleeping_actors() const
+{
+  std::unordered_set<aid_t> actors;
+  for (const auto& [aid, _] : get_sleep_set()) {
+    actors.insert(aid);
+  }
+  return actors;
+}
+
+std::unordered_set<aid_t> State::get_enabled_actors() const
+{
+  std::unordered_set<aid_t> actors;
+  for (const auto& [aid, state] : get_actors_list()) {
+    if (state.is_enabled()) {
+      actors.insert(aid);
+    }
+  }
+  return actors;
+}
+
+void State::seed_wakeup_tree_if_needed(const odpor::Execution& prior)
+{
+  // TODO: It would be better not to have such a flag.
+  if (has_initialized_wakeup_tree) {
+    return;
+  }
+  // TODO: Note that the next action taken by the actor may be updated
+  // after it executes. But we will have already inserted it into the
+  // tree and decided upon "happens-before" at that point for different
+  // executions :(
+  if (wakeup_tree_.empty()) {
+    // Find an enabled transition to pick
+    for (const auto& [_, actor] : get_actors_list()) {
+      if (actor.is_enabled()) {
+        // For each variant of the transition, we want
+        // to insert the action into the tree. This ensures
+        // that all variants are searched
+        for (unsigned times = 0; times < actor.get_max_considered(); ++times) {
+          wakeup_tree_.insert(prior, odpor::PartialExecution{actor.get_transition(times)});
+        }
+        break; // Only one actor gets inserted (see pseudocode)
+      }
+    }
+  }
+  has_initialized_wakeup_tree = true;
+}
+
+void State::sprout_tree_from_parent_state()
+{
+  xbt_assert(parent_state_ != nullptr, "Attempting to construct a wakeup tree for the root state "
+                                       "(or what appears to be, rather for state without a parent defined)");
+  const auto min_process_node = parent_state_->wakeup_tree_.get_min_single_process_node();
+  xbt_assert(min_process_node.has_value(), "Attempting to construct a subtree for a substate from a "
+                                           "parent with an empty wakeup tree. This indicates either that ODPOR "
+                                           "actor selection in State.cpp is incorrect, or that the code "
+                                           "deciding when to make subtrees in ODPOR is incorrect");
+  xbt_assert((get_transition_in()->aid_ == min_process_node.value()->get_actor()) &&
+                 (get_transition_in()->type_ == min_process_node.value()->get_action()->type_),
+             "We tried to make a subtree from a parent state who claimed to have executed `%s` "
+             "but whose wakeup tree indicates it should have executed `%s`. This indicates "
+             "that exploration is not following ODPOR. Are you sure you're choosing actors "
+             "to schedule from the wakeup tree?",
+             get_transition_in()->to_string(false).c_str(),
+             min_process_node.value()->get_action()->to_string(false).c_str());
+  this->wakeup_tree_ = odpor::WakeupTree::make_subtree_rooted_at(min_process_node.value());
+}
+
+void State::remove_subtree_using_current_out_transition()
+{
+  if (auto out_transition = get_transition_out(); out_transition != nullptr) {
+    if (const auto min_process_node = wakeup_tree_.get_min_single_process_node(); min_process_node.has_value()) {
+      xbt_assert((out_transition->aid_ == min_process_node.value()->get_actor()) &&
+                     (out_transition->type_ == min_process_node.value()->get_action()->type_),
+                 "We tried to make a subtree from a parent state who claimed to have executed `%s` "
+                 "but whose wakeup tree indicates it should have executed `%s`. This indicates "
+                 "that exploration is not following ODPOR. Are you sure you're choosing actors "
+                 "to schedule from the wakeup tree?",
+                 out_transition->to_string(false).c_str(),
+                 min_process_node.value()->get_action()->to_string(false).c_str());
+    }
+  }
+  wakeup_tree_.remove_min_single_process_subtree();
+}
+
+odpor::WakeupTree::InsertionResult State::insert_into_wakeup_tree(const odpor::PartialExecution& pe,
+                                                                  const odpor::Execution& E)
+{
+  return this->wakeup_tree_.insert(E, pe);
+}
+
+void State::do_odpor_unwind()
+{
+  if (auto out_transition = get_transition_out(); out_transition != nullptr) {
+    remove_subtree_using_current_out_transition();
+
+    // Only when we've exhausted all variants of the transition which
+    // can be chosen from this state do we finally add the actor to the
+    // sleep set. This ensures that the current logic handling sleep sets
+    // works with ODPOR in the way we intend it to work. There is not a
+    // good way to perform transition equality in SimGrid; instead, we
+    // effectively simply check for the presence of an actor in the sleep set.
+    if (!get_actors_list().at(out_transition->aid_).has_more_to_consider())
+      add_sleep_set(std::move(out_transition));
+  }
+}
+
 } // namespace simgrid::mc
index e60e547..cfa440d 100644 (file)
@@ -7,8 +7,10 @@
 #define SIMGRID_MC_STATE_HPP
 
 #include "src/mc/api/ActorState.hpp"
+#include "src/mc/api/ClockVector.hpp"
 #include "src/mc/api/RemoteApp.hpp"
 #include "src/mc/api/strategy/Strategy.hpp"
+#include "src/mc/explo/odpor/WakeupTree.hpp"
 #include "src/mc/transition/Transition.hpp"
 
 #if SIMGRID_HAVE_STATEFUL_MC
@@ -42,7 +44,15 @@ class XBT_PRIVATE State : public xbt::Extendable<State> {
   /* Sleep sets are composed of the actor and the corresponding transition that made it being added to the sleep
    * set. With this information, it is check whether it should be removed from it or not when exploring a new
    * transition */
-  std::map<aid_t, Transition> sleep_set_;
+  std::map<aid_t, std::shared_ptr<Transition>> sleep_set_;
+
+  /**
+   * The wakeup tree with respect to the execution represented
+   * by the totality of all states before and including this one
+   * and with respect to this state's sleep set
+   */
+  odpor::WakeupTree wakeup_tree_;
+  bool has_initialized_wakeup_tree = false;
 
 public:
   explicit State(RemoteApp& remote_app);
@@ -50,10 +60,16 @@ public:
   /* Returns a positive number if there is another transition to pick, or -1 if not */
   aid_t next_transition() const; // this function should disapear as it is redundant with the next one
 
-  /* Same as next_transition, but choice is now guided, and an integer corresponding to the
+  /* Same as next_transition(), but choice is now guided, and an integer corresponding to the
    internal cost of the transition is returned */
   std::pair<aid_t, int> next_transition_guided() const;
 
+  /**
+   * Same as next_transition(), but the choice is not based off the ODPOR
+   * wakeup tree associated with this state
+   */
+  aid_t next_odpor_transition() const;
+
   /**
    * @brief Explore a new path on the remote app; the parameter 'next' must be the result of a previous call to
    * next_transition()
@@ -66,8 +82,8 @@ public:
 
   /* Marking as TODO some actor in this state:
    *  + consider_one mark aid actor (and assert it is possible)
-   *  + consider_best ensure one actor is marked by eventually marking the best regarding its guiding methode
-   *  + conside_all mark all enabled actor that are not done yet */
+   *  + consider_best ensure one actor is marked by eventually marking the best regarding its guiding method
+   *  + consider_all mark all enabled actor that are not done yet */
   void consider_one(aid_t aid) const { strategy_->consider_one(aid); }
   void consider_best() const { strategy_->consider_best(); }
   unsigned long consider_all() const { return strategy_->consider_all(); }
@@ -85,12 +101,76 @@ public:
   Snapshot* get_system_state() const { return system_state_.get(); }
   void set_system_state(std::shared_ptr<Snapshot> state) { system_state_ = std::move(state); }
 
-  std::map<aid_t, Transition> const& get_sleep_set() const { return sleep_set_; }
-  void add_sleep_set(std::shared_ptr<Transition> t)
+  /**
+   * @brief Computes the backtrack set for this state
+   * according to its definition in Simgrid.
+   *
+   * The backtrack set as it appears in DPOR, SDPOR, and ODPOR
+   * in SimGrid consists of those actors marked as `todo`
+   * (i.e. those that have yet to be explored) as well as those
+   * marked `done` (i.e. those that have already been explored)
+   * since the pseudocode in none of the above algorithms explicitly
+   * removes elements from the backtrack set. DPOR makes use
+   * explicitly of the `done` set, but we again note that the
+   * backtrack set still contains processes added to the done set.
+   */
+  std::unordered_set<aid_t> get_backtrack_set() const;
+  std::unordered_set<aid_t> get_sleeping_actors() const;
+  std::unordered_set<aid_t> get_enabled_actors() const;
+  std::map<aid_t, std::shared_ptr<Transition>> const& get_sleep_set() const { return sleep_set_; }
+  void add_sleep_set(std::shared_ptr<Transition> t) { sleep_set_.insert_or_assign(t->aid_, std::move(t)); }
+  bool is_actor_sleeping(aid_t actor) const
   {
-    sleep_set_.insert_or_assign(t->aid_, Transition(t->type_, t->aid_, t->times_considered_));
+    return std::find_if(sleep_set_.begin(), sleep_set_.end(), [=](const auto& pair) { return pair.first == actor; }) !=
+           sleep_set_.end();
   }
 
+  /**
+   * @brief Inserts an arbitrary enabled actor into the wakeup tree
+   * associated with this state, if such an actor exists and if
+   * the wakeup tree is already not empty
+   *
+   * @param prior The sequence of steps leading up to this state
+   * with respec to which the tree associated with this state should be
+   * a wakeup tree (wakeup trees are defined relative to an execution)
+   *
+   * @invariant: You should not manipulate a wakeup tree with respect
+   * to more than one execution; doing so will almost certainly lead to
+   * unexpected results as wakeup trees are defined relative to a single
+   * execution
+   */
+  void seed_wakeup_tree_if_needed(const odpor::Execution& prior);
+
+  /**
+   * @brief Initializes the wakeup_tree_ instance by taking the subtree rooted at the
+   * single-process node `N` running actor `p := "actor taken by parent to form this state"`
+   * of the *parent's* wakeup tree
+   */
+  void sprout_tree_from_parent_state();
+
+  /**
+   * @brief Removes the subtree rooted at the single-process node
+   * `N` running actor `p` of this state's wakeup tree
+   */
+  void remove_subtree_using_current_out_transition();
+  bool has_empty_tree() const { return this->wakeup_tree_.empty(); }
+
+  /**
+   * @brief
+   */
+  odpor::WakeupTree::InsertionResult insert_into_wakeup_tree(const odpor::PartialExecution&, const odpor::Execution&);
+
+  /** @brief Prepares the state for re-exploration following
+   * another after having followed ODPOR from this state with
+   * the current out transition
+   *
+   * After ODPOR has completed searching a maximal trace, it
+   * finds the first point in the execution with a nonempty wakeup
+   * tree. This method corresponds to lines 20 and 21 in the ODPOR
+   * pseudocode
+   */
+  void do_odpor_unwind();
+
   /* Returns the total amount of states created so far (for statistics) */
   static long get_expanded_states() { return expended_states_; }
 };
index c36dfff..a340c2c 100644 (file)
@@ -320,14 +320,14 @@ void CommDetExtension::handle_comm_pattern(const Transition* transition)
       }
  */
 
-Exploration* create_communication_determinism_checker(const std::vector<char*>& args, bool with_dpor)
+Exploration* create_communication_determinism_checker(const std::vector<char*>& args, ReductionMode mode)
 {
   CommDetExtension::EXTENSION_ID = simgrid::mc::Exploration::extension_create<CommDetExtension>();
   StateCommDet::EXTENSION_ID     = simgrid::mc::State::extension_create<StateCommDet>();
 
   XBT_DEBUG("********* Start communication determinism verification *********");
 
-  auto base      = new DFSExplorer(args, with_dpor, true);
+  auto base      = new DFSExplorer(args, mode, true);
   auto extension = new CommDetExtension(*base);
 
   DFSExplorer::on_exploration_start([extension](RemoteApp const&) {
index a8faec6..2c72764 100644 (file)
 #include <cassert>
 #include <cstdio>
 
+#include <algorithm>
 #include <memory>
 #include <string>
+#include <unordered_set>
 #include <vector>
 
 XBT_LOG_NEW_DEFAULT_SUBCATEGORY(mc_dfs, mc, "DFS exploration algorithm of the model-checker");
@@ -82,6 +84,7 @@ RecordTrace DFSExplorer::get_record_trace() // override
 void DFSExplorer::restore_stack(std::shared_ptr<State> state)
 {
   stack_.clear();
+  execution_seq_     = odpor::Execution();
   auto current_state = state;
   stack_.emplace_front(current_state);
   // condition corresponds to reaching initial state
@@ -90,6 +93,18 @@ void DFSExplorer::restore_stack(std::shared_ptr<State> state)
     stack_.emplace_front(current_state);
   }
   XBT_DEBUG("Replaced stack by %s", get_record_trace().to_string().c_str());
+  if (reduction_mode_ == ReductionMode::sdpor || reduction_mode_ == ReductionMode::odpor) {
+    // NOTE: The outgoing transition for the top-most
+    // state of the  stack refers to that which was taken
+    // as part of the last trace explored by the algorithm.
+    // Thus, only the sequence of transitions leading up to,
+    // but not including, the last state must be included
+    // when reconstructing the Exploration for SDPOR.
+    for (auto iter = std::next(stack_.begin()); iter != stack_.end(); ++iter) {
+      execution_seq_.push_transition((*iter)->get_transition_in());
+    }
+    XBT_DEBUG("Replaced SDPOR/ODPOR execution to reflect the new stack");
+  }
 }
 
 void DFSExplorer::log_state() // override
@@ -125,8 +140,14 @@ void DFSExplorer::run()
         XBT_ERROR("/!\\ Max depth of %d reached! THIS WILL PROBABLY BREAK the dpor reduction /!\\",
                   _sg_mc_max_depth.get());
         XBT_ERROR("/!\\ If bad things happen, disable dpor with --cfg=model-check/reduction:none /!\\");
-      } else
+      } else if (reduction_mode_ == ReductionMode::sdpor || reduction_mode_ == ReductionMode::odpor) {
+        XBT_ERROR("/!\\ Max depth of %d reached! THIS **WILL** BREAK the reduction, which is not sound "
+                  "when stopping at a fixed depth /!\\",
+                  _sg_mc_max_depth.get());
+        XBT_ERROR("/!\\ If bad things happen, disable the reduction with --cfg=model-check/reduction:none /!\\");
+      } else {
         XBT_WARN("/!\\ Max depth reached ! /!\\ ");
+      }
       this->backtrack();
       continue;
     }
@@ -143,9 +164,25 @@ void DFSExplorer::run()
     }
 #endif
 
+    if (reduction_mode_ == ReductionMode::odpor) {
+      // In the case of ODPOR, the wakeup tree for this
+      // state may be empty if we're exploring new territory
+      // (rather than following the partial execution of a
+      // wakeup tree). This corresponds to lines 9 to 13 of
+      // the ODPOR pseudocode
+      //
+      // INVARIANT: The execution sequence should be consistent
+      // with the state when seeding the tree. If the sequence
+      // gets out of sync with the state, selection will not
+      // work as we intend
+      state->seed_wakeup_tree_if_needed(execution_seq_);
+    }
+
     // Search for the next transition
-    // next_transition returns a pair<aid_t, int> in case we want to consider multiple state (eg. during backtrack)
-    auto [next, _] = state->next_transition_guided();
+    // next_transition returns a pair<aid_t, int>
+    // in case we want to consider multiple states (eg. during backtrack)
+    const aid_t next = reduction_mode_ == ReductionMode::odpor ? state->next_odpor_transition()
+                                                               : std::get<0>(state->next_transition_guided());
 
     if (next < 0) { // If there is no more transition in the current state, backtrack.
       XBT_VERB("%lu actors remain, but none of them need to be interleaved (depth %zu).", state->get_actor_count(),
@@ -164,11 +201,11 @@ void DFSExplorer::run()
     if (_sg_mc_sleep_set && XBT_LOG_ISENABLED(mc_dfs, xbt_log_priority_verbose)) {
       XBT_VERB("Sleep set actually containing:");
       for (auto& [aid, transition] : state->get_sleep_set())
-        XBT_VERB("  <%ld,%s>", aid, transition.to_string().c_str());
+        XBT_VERB("  <%ld,%s>", aid, transition->to_string().c_str());
     }
 
     /* Actually answer the request: let's execute the selected request (MCed does one step) */
-    state->execute_next(next, get_remote_app());
+    const auto executed_transition = state->execute_next(next, get_remote_app());
     on_transition_execute_signal(state->get_transition_out().get(), get_remote_app());
 
     // If there are processes to interleave and the maximum depth has not been
@@ -180,13 +217,25 @@ void DFSExplorer::run()
     auto next_state = std::make_shared<State>(get_remote_app(), state);
     on_state_creation_signal(next_state.get(), get_remote_app());
 
-    /* Sleep set procedure:
-     * adding the taken transition to the sleep set of the original state.
-     * <!> Since the parent sleep set is used to compute the child sleep set, this need to be
-     * done after next_state creation */
-    XBT_DEBUG("Marking Transition >>%s<< of process %ld done and adding it to the sleep set",
-              state->get_transition_out()->to_string().c_str(), state->get_transition_out()->aid_);
-    state->add_sleep_set(state->get_transition_out()); // Actors are marked done when they are considerd in ActorState
+    if (reduction_mode_ == ReductionMode::odpor) {
+      // With ODPOR, after taking a step forward, we must
+      // assign a copy of that subtree to the next state.
+      //
+      // NOTE: We only add actions to the sleep set AFTER
+      // we've regenerated states. We must perform the search
+      // fully down a single path before we consider adding
+      // any elements to the sleep set according to the pseudocode
+      next_state->sprout_tree_from_parent_state();
+    } else {
+      /* Sleep set procedure:
+       * adding the taken transition to the sleep set of the original state.
+       * <!> Since the parent sleep set is used to compute the child sleep set, this need to be
+       * done after next_state creation */
+      XBT_DEBUG("Marking Transition >>%s<< of process %ld done and adding it to the sleep set",
+                state->get_transition_out()->to_string().c_str(), state->get_transition_out()->aid_);
+      state->add_sleep_set(
+          state->get_transition_out()); // Actors are marked done when they are considered in ActorState
+    }
 
     /* DPOR persistent set procedure:
      * for each new transition considered, check if it depends on any other previous transition executed before it
@@ -229,6 +278,45 @@ void DFSExplorer::run()
         }
         tmp_stack.pop_back();
       }
+    } else if (reduction_mode_ == ReductionMode::sdpor) {
+      /**
+       * SDPOR Source Set Procedure:
+       *
+       * Find "reversible races" in the current execution `E` with respect
+       * to the latest action `p`. For each such race, determine one thread
+       * not contained in the backtrack set at the "race point" `r` which
+       * "represents" the trace formed by first executing everything after
+       * `r` that doesn't depend on it (`v := notdep(r, E)`) and then `p` to
+       * flip the race.
+       *
+       * The intuition is that some subsequence of `v` may enable `p`, so
+       * we want to be sure that search "in that direction"
+       */
+      execution_seq_.push_transition(std::move(executed_transition));
+      xbt_assert(execution_seq_.get_latest_event_handle().has_value(), "No events are contained in the SDPOR execution "
+                                                                       "even though one was just added");
+
+      const auto next_E_p = execution_seq_.get_latest_event_handle().value();
+      for (const auto e_race : execution_seq_.get_reversible_races_of(next_E_p)) {
+        State* prev_state  = stack_[e_race].get();
+        const auto choices = execution_seq_.get_missing_source_set_actors_from(e_race, prev_state->get_backtrack_set());
+        if (!choices.empty()) {
+          // NOTE: To incorporate the idea of attempting to select the "best"
+          // backtrack point into SDPOR, instead of selecting the `first` initial,
+          // we should instead compute all choices and decide which is best
+          //
+          // Here, we choose the actor with the lowest ID to ensure
+          // we get deterministic results
+          const auto q =
+              std::min_element(choices.begin(), choices.end(), [](const aid_t a1, const aid_t a2) { return a1 < a2; });
+          prev_state->consider_one(*q);
+          opened_states_.emplace_back(std::move(prev_state));
+        }
+      }
+    } else if (reduction_mode_ == ReductionMode::odpor) {
+      // In the case of ODPOR, we simply observe the transition that was executed
+      // until we've reached a maximal trace
+      execution_seq_.push_transition(std::move(executed_transition));
     }
 
     // Before leaving that state, if the transition we just took can be taken multiple times, we
@@ -240,7 +328,8 @@ void DFSExplorer::run()
       this->check_non_termination(next_state.get());
 
 #if SIMGRID_HAVE_STATEFUL_MC
-    /* Check whether we already explored next_state in the past (but only if interested in state-equality reduction) */
+    /* Check whether we already explored next_state in the past (but only if interested in state-equality reduction)
+     */
     if (_sg_mc_max_visited_states > 0)
       visited_state_ = visited_states_.addVisitedState(next_state->get_num(), next_state.get(), get_remote_app());
 #endif
@@ -304,8 +393,75 @@ std::shared_ptr<State> DFSExplorer::best_opened_state()
   return best_state;
 }
 
+std::shared_ptr<State> DFSExplorer::next_odpor_state()
+{
+  for (auto iter = stack_.rbegin(); iter != stack_.rend(); ++iter) {
+    const auto& state = *iter;
+    state->do_odpor_unwind();
+    XBT_DEBUG("\tPerformed ODPOR 'clean-up'. Sleep set has:");
+    for (auto& [aid, transition] : state->get_sleep_set())
+      XBT_DEBUG("\t  <%ld,%s>", aid, transition->to_string().c_str());
+    if (!state->has_empty_tree()) {
+      return state;
+    }
+  }
+  return nullptr;
+}
+
 void DFSExplorer::backtrack()
 {
+  if (const auto last_event = execution_seq_.get_latest_event_handle();
+      reduction_mode_ == ReductionMode::odpor and last_event.has_value()) {
+    /**
+     * ODPOR Race Detection Procedure:
+     *
+     * For each reversible race in the current execution, we
+     * note if there are any continuations `C` equivalent to that which
+     * would reverse the race that have already either a) been searched by ODPOR or
+     * b) been *noted* to be searched by the wakeup tree at the
+     * appropriate reversal point, either as `C` directly or
+     * an as equivalent to `C` ("eventually looks like C", viz. the `~_E`
+     * relation)
+     */
+    for (auto e_prime = static_cast<odpor::Execution::EventHandle>(0); e_prime <= last_event.value(); ++e_prime) {
+      for (const auto e : execution_seq_.get_reversible_races_of(e_prime)) {
+        XBT_DEBUG("ODPOR: Reversible race detected between events `%u` and `%u`", e, e_prime);
+        State& prev_state = *stack_[e];
+        if (const auto v = execution_seq_.get_odpor_extension_from(e, e_prime, prev_state); v.has_value()) {
+          const auto result = prev_state.insert_into_wakeup_tree(v.value(), execution_seq_.get_prefix_before(e));
+          switch (result) {
+            case odpor::WakeupTree::InsertionResult::root: {
+              XBT_DEBUG("ODPOR: Reversible race with `%u` unaccounted for in the wakeup tree for "
+                        "the execution prior to event `%u`:",
+                        e_prime, e);
+              break;
+            }
+            case odpor::WakeupTree::InsertionResult::interior_node: {
+              XBT_DEBUG("ODPOR: Reversible race with `%u` partially accounted for in the wakeup tree for "
+                        "the execution prior to event `%u`:",
+                        e_prime, e);
+              break;
+            }
+            case odpor::WakeupTree::InsertionResult::leaf: {
+              XBT_DEBUG("ODPOR: Reversible race with `%u` accounted for in the wakeup tree for "
+                        "the execution prior to event `%u`:",
+                        e_prime, e);
+              break;
+            }
+          }
+          for (const auto& seq : simgrid::mc::odpor::get_textual_trace(v.value())) {
+            XBT_DEBUG(" %s", seq.c_str());
+          }
+        } else {
+          XBT_DEBUG("ODPOR: Ignoring race: `sleep(E')` intersects `WI_[E'](v := notdep(%u, E))`", e);
+          XBT_DEBUG("Sleep set contains:");
+          for (auto& [aid, transition] : prev_state.get_sleep_set())
+            XBT_DEBUG("  <%ld,%s>", aid, transition->to_string().c_str());
+        }
+      }
+    }
+  }
+
   XBT_VERB("Backtracking from %s", get_record_trace().to_string().c_str());
   XBT_DEBUG("%lu alternatives are yet to be explored:", opened_states_.size());
 
@@ -313,7 +469,7 @@ void DFSExplorer::backtrack()
   get_remote_app().check_deadlock();
 
   // Take the point with smallest distance
-  auto backtracking_point = best_opened_state();
+  auto backtracking_point = reduction_mode_ == ReductionMode::odpor ? next_odpor_state() : best_opened_state();
 
   // if no backtracking point, then set the stack_ to empty so we can end the exploration
   if (not backtracking_point) {
@@ -372,20 +528,16 @@ void DFSExplorer::backtrack()
   this->restore_stack(backtracking_point);
 }
 
-DFSExplorer::DFSExplorer(const std::vector<char*>& args, bool with_dpor, bool need_memory_info)
+DFSExplorer::DFSExplorer(const std::vector<char*>& args, ReductionMode mode, bool need_memory_info)
     : Exploration(args, need_memory_info || _sg_mc_termination
 #if SIMGRID_HAVE_STATEFUL_MC
                             || _sg_mc_checkpoint > 0
 #endif
-      )
+                  )
+    , reduction_mode_(mode)
 {
-  if (with_dpor)
-    reduction_mode_ = ReductionMode::dpor;
-  else
-    reduction_mode_ = ReductionMode::none;
-
   if (_sg_mc_termination) {
-    if (with_dpor) {
+    if (mode != ReductionMode::none) {
       XBT_INFO("Check non progressive cycles (turning DPOR off)");
       reduction_mode_ = ReductionMode::none;
     } else {
@@ -409,11 +561,19 @@ DFSExplorer::DFSExplorer(const std::vector<char*>& args, bool with_dpor, bool ne
   }
   if (stack_.back()->count_todo_multiples() > 1)
     opened_states_.emplace_back(stack_.back());
+
+  if (mode == ReductionMode::odpor && !_sg_mc_sleep_set) {
+    // ODPOR requires the use of sleep sets; SDPOR
+    // "likes" using sleep sets but it is not strictly
+    // required
+    XBT_INFO("Forcing the use of sleep sets for use with ODPOR");
+    _sg_mc_sleep_set = true;
+  }
 }
 
-Exploration* create_dfs_exploration(const std::vector<char*>& args, bool with_dpor)
+Exploration* create_dfs_exploration(const std::vector<char*>& args, ReductionMode mode)
 {
-  return new DFSExplorer(args, with_dpor);
+  return new DFSExplorer(args, mode);
 }
 
 } // namespace simgrid::mc
index 27194a8..a0dc478 100644 (file)
@@ -6,26 +6,30 @@
 #ifndef SIMGRID_MC_SAFETY_CHECKER_HPP
 #define SIMGRID_MC_SAFETY_CHECKER_HPP
 
+#include "src/mc/api/ClockVector.hpp"
 #include "src/mc/api/State.hpp"
 #include "src/mc/explo/Exploration.hpp"
+#include "src/mc/explo/odpor/Execution.hpp"
+#include "src/mc/mc_config.hpp"
 
 #if SIMGRID_HAVE_STATEFUL_MC
 #include "src/mc/VisitedState.hpp"
 #endif
 
+#include <deque>
 #include <list>
 #include <memory>
 #include <set>
 #include <string>
+#include <unordered_map>
 #include <vector>
 
 namespace simgrid::mc {
 
-using stack_t = std::list<std::shared_ptr<State>>;
+using stack_t = std::deque<std::shared_ptr<State>>;
 
 class XBT_PRIVATE DFSExplorer : public Exploration {
-  XBT_DECLARE_ENUM_CLASS(ReductionMode, none, dpor);
-
+private:
   ReductionMode reduction_mode_;
   unsigned long backtrack_count_      = 0; // for statistics
   unsigned long visited_states_count_ = 0; // for statistics
@@ -43,7 +47,7 @@ class XBT_PRIVATE DFSExplorer : public Exploration {
   static xbt::signal<void(RemoteApp&)> on_log_state_signal;
 
 public:
-  explicit DFSExplorer(const std::vector<char*>& args, bool with_dpor, bool need_memory_info = false);
+  explicit DFSExplorer(const std::vector<char*>& args, ReductionMode mode, bool need_memory_info = false);
   void run() override;
   RecordTrace get_record_trace() override;
   void log_state() override;
@@ -93,6 +97,16 @@ private:
 
   /** Stack representing the position in the exploration graph */
   stack_t stack_;
+
+  /**
+   * Provides additional metadata about the position in the exploration graph
+   * which is used by SDPOR and ODPOR
+   */
+  odpor::Execution execution_seq_;
+
+  /** Per-actor clock vectors used to compute the "happens-before" relation */
+  std::unordered_map<aid_t, ClockVector> per_actor_clocks_;
+
 #if SIMGRID_HAVE_STATEFUL_MC
   VisitedStates visited_states_;
   std::unique_ptr<VisitedState> visited_state_;
@@ -105,6 +119,11 @@ private:
   std::vector<std::shared_ptr<State>> opened_states_;
   std::shared_ptr<State> best_opened_state();
 
+  /** If we're running ODPOR, picks the corresponding state in the stack
+   * (opened_states_ are ignored)
+   */
+  std::shared_ptr<State> next_odpor_state();
+
   /** Change current stack_ value to correspond to the one we would have
    *  had if we executed transition to get to state. This is required when
    *  backtracking, and achieved thanks to the fact states save their parent.*/
index 824adcd..64114d4 100644 (file)
@@ -8,6 +8,7 @@
 
 #include "simgrid/forward.h"
 #include "src/mc/api/RemoteApp.hpp"
+#include "src/mc/mc_config.hpp"
 #include "src/mc/mc_exit.hpp"
 #include "src/mc/mc_record.hpp"
 #include <xbt/Extendable.hpp>
@@ -40,7 +41,7 @@ public:
 
   static Exploration* get_instance() { return instance_; }
   // No copy:
-  Exploration(Exploration const&) = delete;
+  Exploration(Exploration const&)            = delete;
   Exploration& operator=(Exploration const&) = delete;
 
   /** Main function of this algorithm */
@@ -72,8 +73,8 @@ public:
 
 // External constructors so that the types (and the types of their content) remain hidden
 XBT_PUBLIC Exploration* create_liveness_checker(const std::vector<char*>& args);
-XBT_PUBLIC Exploration* create_dfs_exploration(const std::vector<char*>& args, bool with_dpor);
-XBT_PUBLIC Exploration* create_communication_determinism_checker(const std::vector<char*>& args, bool with_dpor);
+XBT_PUBLIC Exploration* create_dfs_exploration(const std::vector<char*>& args, ReductionMode mode);
+XBT_PUBLIC Exploration* create_communication_determinism_checker(const std::vector<char*>& args, ReductionMode mode);
 XBT_PUBLIC Exploration* create_udpor_checker(const std::vector<char*>& args);
 
 } // namespace simgrid::mc
diff --git a/src/mc/explo/odpor/ClockVector_test.cpp b/src/mc/explo/odpor/ClockVector_test.cpp
new file mode 100644 (file)
index 0000000..9d49c60
--- /dev/null
@@ -0,0 +1,366 @@
+/* Copyright (c) 2017-2023. The SimGrid Team. All rights reserved.               */
+
+/* This program is free software; you can redistribute it and/or modify it
+ * under the terms of the license (GNU LGPL) which comes with this package. */
+
+#include "src/3rd-party/catch.hpp"
+#include "src/mc/api/ClockVector.hpp"
+
+using namespace simgrid::mc;
+
+TEST_CASE("simgrid::mc::ClockVector: Constructing Vectors")
+{
+  SECTION("Without values")
+  {
+    ClockVector cv;
+    REQUIRE(cv.size() == 0);
+
+    // Verify `cv` doesn't map any values
+    REQUIRE_FALSE(cv.get(0).has_value());
+    REQUIRE_FALSE(cv.get(1).has_value());
+    REQUIRE_FALSE(cv.get(2).has_value());
+    REQUIRE_FALSE(cv.get(3).has_value());
+  }
+
+  SECTION("With initial values")
+  {
+    ClockVector cv{
+        {1, 5}, {3, 1}, {7, 10}, {6, 5}, {8, 1}, {10, 10},
+    };
+    REQUIRE(cv.size() == 6);
+
+    // Verify `cv` maps each value
+    REQUIRE(cv.get(1).has_value());
+    REQUIRE(cv.get(1).value() == 5);
+    REQUIRE(cv[1] == 5);
+    REQUIRE(cv.get(3).has_value());
+    REQUIRE(cv.get(3).value() == 1);
+    REQUIRE(cv[3] == 1);
+    REQUIRE(cv.get(7).has_value());
+    REQUIRE(cv.get(7).value() == 10);
+    REQUIRE(cv[7] == 10);
+    REQUIRE(cv.get(6).has_value());
+    REQUIRE(cv.get(6).value() == 5);
+    REQUIRE(cv[6] == 5);
+    REQUIRE(cv.get(8).has_value());
+    REQUIRE(cv.get(8).value() == 1);
+    REQUIRE(cv[8] == 1);
+    REQUIRE(cv.get(10).has_value());
+    REQUIRE(cv.get(10).value() == 10);
+    REQUIRE(cv[10] == 10);
+  }
+}
+
+TEST_CASE("simgrid::mc::ClockVector: Testing operator[]")
+{
+  ClockVector cv;
+  cv[0] = 1;
+  REQUIRE(cv.size() == 1);
+
+  REQUIRE(cv.get(0).has_value());
+  REQUIRE(cv.get(0).value() == 1);
+
+  // Verify `cv` doesn't map other values
+  REQUIRE_FALSE(cv.get(2).has_value());
+  REQUIRE_FALSE(cv.get(3).has_value());
+
+  cv[10] = 31;
+  REQUIRE(cv.size() == 2);
+
+  // Old values are still mapped
+  REQUIRE(cv.get(0).has_value());
+  REQUIRE(cv.get(0).value() == 1);
+  REQUIRE(cv[0] == 1);
+  REQUIRE(cv.get(10).has_value());
+  REQUIRE(cv.get(10).value() == 31);
+  REQUIRE(cv[10] == 31);
+
+  // Verify `cv` doesn't map other values
+  REQUIRE_FALSE(cv.get(2).has_value());
+  REQUIRE_FALSE(cv.get(3).has_value());
+}
+
+TEST_CASE("simgrid::mc::ClockVector: Testing Maximal Clock Vectors")
+{
+  SECTION("Max with zero clock vector yields self")
+  {
+    ClockVector cv1{
+        {1, 2},
+        {2, 10},
+        {3, 5},
+    };
+    ClockVector cv2;
+    ClockVector maxCV = ClockVector::max(cv1, cv2);
+
+    REQUIRE(maxCV.size() == 3);
+    REQUIRE(maxCV.get(1).has_value());
+    REQUIRE(maxCV.get(1).value() == 2);
+    REQUIRE(maxCV[1] == 2);
+
+    REQUIRE(maxCV.get(2).has_value());
+    REQUIRE(maxCV.get(2).value() == 10);
+    REQUIRE(maxCV[2] == 10);
+
+    REQUIRE(maxCV.get(3).has_value());
+    REQUIRE(maxCV.get(3).value() == 5);
+    REQUIRE(maxCV[3] == 5);
+  }
+
+  SECTION("Max with self clock vector yields self")
+  {
+    ClockVector cv1{
+        {1, 2},
+        {2, 10},
+        {3, 5},
+    };
+    ClockVector maxCV = ClockVector::max(cv1, cv1);
+
+    REQUIRE(maxCV.size() == 3);
+    REQUIRE(maxCV.get(1).has_value());
+    REQUIRE(maxCV.get(1).value() == 2);
+    REQUIRE(maxCV[1] == 2);
+
+    REQUIRE(maxCV.get(2).has_value());
+    REQUIRE(maxCV.get(2).value() == 10);
+    REQUIRE(maxCV[2] == 10);
+
+    REQUIRE(maxCV.get(3).has_value());
+    REQUIRE(maxCV.get(3).value() == 5);
+    REQUIRE(maxCV[3] == 5);
+  }
+
+  SECTION("Testing with partial overlaps")
+  {
+    SECTION("Example 1")
+    {
+      ClockVector cv1{
+          {1, 2},
+          {2, 10},
+          {3, 5},
+      };
+      ClockVector cv2{
+          {1, 5},
+          {3, 1},
+          {7, 10},
+      };
+      ClockVector maxCV = ClockVector::max(cv1, cv2);
+
+      REQUIRE(maxCV.size() == 4);
+      REQUIRE(maxCV.get(1).has_value());
+      REQUIRE(maxCV.get(1).value() == 5);
+      REQUIRE(maxCV[1] == 5);
+
+      REQUIRE(maxCV.get(2).has_value());
+      REQUIRE(maxCV.get(2).value() == 10);
+      REQUIRE(maxCV[2] == 10);
+
+      REQUIRE(maxCV.get(3).has_value());
+      REQUIRE(maxCV.get(3).value() == 5);
+      REQUIRE(maxCV[3] == 5);
+
+      REQUIRE(maxCV.get(7).has_value());
+      REQUIRE(maxCV.get(7).value() == 10);
+      REQUIRE(maxCV[7] == 10);
+    }
+
+    SECTION("Example 2")
+    {
+      ClockVector cv1{
+          {1, 2}, {2, 10}, {3, 5}, {4, 40}, {11, 3}, {12, 8},
+      };
+      ClockVector cv2{
+          {1, 18}, {2, 4}, {4, 41}, {10, 3}, {12, 8},
+      };
+      ClockVector maxCV = ClockVector::max(cv1, cv2);
+
+      REQUIRE(maxCV.size() == 7);
+      REQUIRE(maxCV.get(1).has_value());
+      REQUIRE(maxCV.get(1).value() == 18);
+      REQUIRE(maxCV[1] == 18);
+
+      REQUIRE(maxCV.get(2).has_value());
+      REQUIRE(maxCV.get(2).value() == 10);
+      REQUIRE(maxCV[2] == 10);
+
+      REQUIRE(maxCV.get(3).has_value());
+      REQUIRE(maxCV.get(3).value() == 5);
+      REQUIRE(maxCV[3] == 5);
+
+      REQUIRE(maxCV.get(4).has_value());
+      REQUIRE(maxCV.get(4).value() == 41);
+      REQUIRE(maxCV[4] == 41);
+
+      REQUIRE(maxCV.get(10).has_value());
+      REQUIRE(maxCV.get(10).value() == 3);
+      REQUIRE(maxCV[10] == 3);
+
+      REQUIRE(maxCV.get(11).has_value());
+      REQUIRE(maxCV.get(11).value() == 3);
+      REQUIRE(maxCV[11] == 3);
+
+      REQUIRE(maxCV.get(12).has_value());
+      REQUIRE(maxCV.get(12).value() == 8);
+      REQUIRE(maxCV[12] == 8);
+    }
+
+    SECTION("Example 3")
+    {
+      ClockVector cv1{{1, 2}, {4, 41}, {12, 0}, {100, 5}};
+      ClockVector cv2{{2, 4}, {4, 10}, {10, 3}, {12, 8}, {19, 0}, {21, 6}, {22, 0}};
+      ClockVector cv3{{21, 60}, {22, 6}, {100, 3}};
+      ClockVector maxCV = ClockVector::max(cv1, cv2);
+      maxCV             = ClockVector::max(maxCV, cv3);
+
+      REQUIRE(maxCV.size() == 9);
+      REQUIRE(maxCV.get(1).has_value());
+      REQUIRE(maxCV.get(1).value() == 2);
+      REQUIRE(maxCV[1] == 2);
+
+      REQUIRE(maxCV.get(2).has_value());
+      REQUIRE(maxCV.get(2).value() == 4);
+      REQUIRE(maxCV[2] == 4);
+
+      REQUIRE(maxCV.get(4).has_value());
+      REQUIRE(maxCV.get(4).value() == 41);
+      REQUIRE(maxCV[4] == 41);
+
+      REQUIRE(maxCV.get(10).has_value());
+      REQUIRE(maxCV.get(10).value() == 3);
+      REQUIRE(maxCV[10] == 3);
+
+      REQUIRE(maxCV.get(12).has_value());
+      REQUIRE(maxCV.get(12).value() == 8);
+      REQUIRE(maxCV[12] == 8);
+
+      REQUIRE(maxCV.get(19).has_value());
+      REQUIRE(maxCV.get(19).value() == 0);
+      REQUIRE(maxCV[19] == 0);
+
+      REQUIRE(maxCV.get(21).has_value());
+      REQUIRE(maxCV.get(21).value() == 60);
+      REQUIRE(maxCV[21] == 60);
+
+      REQUIRE(maxCV.get(22).has_value());
+      REQUIRE(maxCV.get(22).value() == 6);
+      REQUIRE(maxCV[22] == 6);
+
+      REQUIRE(maxCV.get(100).has_value());
+      REQUIRE(maxCV.get(100).value() == 5);
+      REQUIRE(maxCV[100] == 5);
+    }
+  }
+
+  SECTION("Testing without overlaps")
+  {
+    SECTION("Example 1")
+    {
+      ClockVector cv1{{1, 2}};
+      ClockVector cv2{
+          {2, 4},
+          {4, 41},
+          {10, 3},
+          {12, 8},
+      };
+      ClockVector maxCV = ClockVector::max(cv1, cv2);
+
+      REQUIRE(maxCV.size() == 5);
+      REQUIRE(maxCV.get(1).has_value());
+      REQUIRE(maxCV.get(1).value() == 2);
+      REQUIRE(maxCV[1] == 2);
+
+      REQUIRE(maxCV.get(2).has_value());
+      REQUIRE(maxCV.get(2).value() == 4);
+      REQUIRE(maxCV[2] == 4);
+
+      REQUIRE(maxCV.get(4).has_value());
+      REQUIRE(maxCV.get(4).value() == 41);
+      REQUIRE(maxCV[4] == 41);
+
+      REQUIRE(maxCV.get(10).has_value());
+      REQUIRE(maxCV.get(10).value() == 3);
+      REQUIRE(maxCV[10] == 3);
+
+      REQUIRE(maxCV.get(12).has_value());
+      REQUIRE(maxCV.get(12).value() == 8);
+      REQUIRE(maxCV[12] == 8);
+    }
+
+    SECTION("Example 2")
+    {
+      ClockVector cv1{{1, 2}, {4, 41}};
+      ClockVector cv2{
+          {2, 4},
+          {10, 3},
+          {12, 8},
+      };
+      ClockVector maxCV = ClockVector::max(cv1, cv2);
+
+      REQUIRE(maxCV.size() == 5);
+      REQUIRE(maxCV.get(1).has_value());
+      REQUIRE(maxCV.get(1).value() == 2);
+      REQUIRE(maxCV[1] == 2);
+
+      REQUIRE(maxCV.get(2).has_value());
+      REQUIRE(maxCV.get(2).value() == 4);
+      REQUIRE(maxCV[2] == 4);
+
+      REQUIRE(maxCV.get(4).has_value());
+      REQUIRE(maxCV.get(4).value() == 41);
+      REQUIRE(maxCV[4] == 41);
+
+      REQUIRE(maxCV.get(10).has_value());
+      REQUIRE(maxCV.get(10).value() == 3);
+      REQUIRE(maxCV[10] == 3);
+
+      REQUIRE(maxCV.get(12).has_value());
+      REQUIRE(maxCV.get(12).value() == 8);
+      REQUIRE(maxCV[12] == 8);
+    }
+
+    SECTION("Example 3")
+    {
+      ClockVector cv1{{1, 2}, {4, 41}};
+      ClockVector cv2{{2, 4}, {10, 3}, {12, 8}, {19, 0}, {21, 6}};
+      ClockVector cv3{{22, 6}, {100, 3}};
+      ClockVector maxCV = ClockVector::max(cv1, cv2);
+      maxCV             = ClockVector::max(maxCV, cv3);
+
+      REQUIRE(maxCV.size() == 9);
+      REQUIRE(maxCV.get(1).has_value());
+      REQUIRE(maxCV.get(1).value() == 2);
+      REQUIRE(maxCV[1] == 2);
+
+      REQUIRE(maxCV.get(2).has_value());
+      REQUIRE(maxCV.get(2).value() == 4);
+      REQUIRE(maxCV[2] == 4);
+
+      REQUIRE(maxCV.get(4).has_value());
+      REQUIRE(maxCV.get(4).value() == 41);
+      REQUIRE(maxCV[4] == 41);
+
+      REQUIRE(maxCV.get(10).has_value());
+      REQUIRE(maxCV.get(10).value() == 3);
+      REQUIRE(maxCV[10] == 3);
+
+      REQUIRE(maxCV.get(12).has_value());
+      REQUIRE(maxCV.get(12).value() == 8);
+      REQUIRE(maxCV[12] == 8);
+
+      REQUIRE(maxCV.get(19).has_value());
+      REQUIRE(maxCV.get(19).value() == 0);
+      REQUIRE(maxCV[19] == 0);
+
+      REQUIRE(maxCV.get(21).has_value());
+      REQUIRE(maxCV.get(21).value() == 6);
+      REQUIRE(maxCV[21] == 6);
+
+      REQUIRE(maxCV.get(22).has_value());
+      REQUIRE(maxCV.get(22).value() == 6);
+      REQUIRE(maxCV[22] == 6);
+
+      REQUIRE(maxCV.get(100).has_value());
+      REQUIRE(maxCV.get(100).value() == 3);
+      REQUIRE(maxCV[100] == 3);
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/mc/explo/odpor/Execution.cpp b/src/mc/explo/odpor/Execution.cpp
new file mode 100644 (file)
index 0000000..ef10e4f
--- /dev/null
@@ -0,0 +1,490 @@
+/* Copyright (c) 2008-2023. The SimGrid Team. All rights reserved.          */
+
+/* This program is free software; you can redistribute it and/or modify it
+ * under the terms of the license (GNU LGPL) which comes with this package. */
+
+#include "src/mc/explo/odpor/Execution.hpp"
+#include "src/mc/api/State.hpp"
+#include "src/mc/explo/odpor/ReversibleRaceCalculator.hpp"
+#include "xbt/asserts.h"
+#include "xbt/string.hpp"
+#include <algorithm>
+#include <limits>
+#include <vector>
+
+namespace simgrid::mc::odpor {
+
+std::vector<std::string> get_textual_trace(const PartialExecution& w)
+{
+  std::vector<std::string> trace;
+  for (const auto& t : w) {
+    const auto a = xbt::string_printf("Actor %ld: %s", t->aid_, t->to_string(true).c_str());
+    trace.push_back(std::move(a));
+  }
+  return trace;
+}
+
+Execution::Execution(const PartialExecution& w)
+{
+  push_partial_execution(w);
+}
+
+void Execution::push_transition(std::shared_ptr<Transition> t)
+{
+  if (t == nullptr) {
+    throw std::invalid_argument("Unexpectedly received `nullptr`");
+  }
+  ClockVector max_clock_vector;
+  for (const Event& e : this->contents_) {
+    if (e.get_transition()->depends(t.get())) {
+      max_clock_vector = ClockVector::max(max_clock_vector, e.get_clock_vector());
+    }
+  }
+  max_clock_vector[t->aid_] = this->size();
+  contents_.push_back(Event({std::move(t), max_clock_vector}));
+}
+
+void Execution::push_partial_execution(const PartialExecution& w)
+{
+  for (const auto& t : w) {
+    push_transition(t);
+  }
+}
+
+std::vector<std::string> Execution::get_textual_trace() const
+{
+  std::vector<std::string> trace;
+  for (const auto& t : this->contents_) {
+    const auto a =
+        xbt::string_printf("Actor %ld: %s", t.get_transition()->aid_, t.get_transition()->to_string(true).c_str());
+    trace.push_back(std::move(a));
+  }
+  return trace;
+}
+
+std::unordered_set<Execution::EventHandle> Execution::get_racing_events_of(Execution::EventHandle target) const
+{
+  std::unordered_set<Execution::EventHandle> racing_events;
+  std::unordered_set<Execution::EventHandle> disqualified_events;
+
+  // For each event of the execution
+  for (auto e_i = target; e_i != std::numeric_limits<Execution::EventHandle>::max(); e_i--) {
+    // We need `e_i -->_E target` as a necessary condition
+    if (not happens_before(e_i, target)) {
+      continue;
+    }
+
+    // Further, `proc(e_i) != proc(target)`
+    if (get_actor_with_handle(e_i) == get_actor_with_handle(target)) {
+      disqualified_events.insert(e_i);
+      continue;
+    }
+
+    // There could an event that "happens-between" the two events which would discount `e_i` as a race
+    for (auto e_j = e_i; e_j < target; e_j++) {
+      // If both:
+      // 1. e_i --->_E e_j; and
+      // 2. disqualified_events.count(e_j) > 0
+      // then e_i --->_E target indirectly (either through
+      // e_j directly, or transitively through e_j)
+      if (disqualified_events.count(e_j) > 0 and happens_before(e_i, e_j)) {
+        disqualified_events.insert(e_i);
+        break;
+      }
+    }
+
+    // If `e_i` wasn't disqualified in the last round,
+    // it's in a race with `target`. After marking it
+    // as such, we ensure no other event `e` can happen-before
+    // it (since this would transitively make it the event
+    // which "happens-between" `target` and `e`)
+    if (disqualified_events.count(e_i) == 0) {
+      racing_events.insert(e_i);
+      disqualified_events.insert(e_i);
+    }
+  }
+
+  return racing_events;
+}
+
+std::unordered_set<Execution::EventHandle> Execution::get_reversible_races_of(EventHandle handle) const
+{
+  std::unordered_set<EventHandle> reversible_races;
+  for (EventHandle race : get_racing_events_of(handle)) {
+    if (ReversibleRaceCalculator::is_race_reversible(*this, race, handle)) {
+      reversible_races.insert(race);
+    }
+  }
+  return reversible_races;
+}
+
+Execution Execution::get_prefix_before(Execution::EventHandle handle) const
+{
+  return Execution(std::vector<Event>{contents_.begin(), contents_.begin() + handle});
+}
+
+std::unordered_set<aid_t>
+Execution::get_missing_source_set_actors_from(EventHandle e, const std::unordered_set<aid_t>& backtrack_set) const
+{
+  // If this execution is empty, there are no initials
+  // relative to the last transition added to the execution
+  // since such a transition does not exist
+  if (empty()) {
+    return std::unordered_set<aid_t>{};
+  }
+
+  // To actually compute `I_[E'](v) ∩ backtrack(E')`, we must
+  // first compute `E'` and "move" in the direction of `v`.
+  // We perform a scan over `E` (this execution) and make
+  // note of any events which occur after `e` but don't
+  // "happen-after" `e` by pushing them onto `E'`. Note that
+  // correctness is still preserved in computing `v` "on-the-fly"
+  // to determine if an event `e` by actor `q` is an initial for `E'`
+  // after `v`: only those events that "occur-before" `e` in `v` could
+  // "happen-before" `ve for any valid "happens-before" relation
+  // (see property 1 in the ODPOR paper, viz. "is included in <_E")
+
+  // First, grab `E' := pre(e, E)` and determine what actor `p` is
+  const auto next_E_p = get_latest_event_handle().value();
+  xbt_assert(e != next_E_p,
+             "This method assumes that the event `e` (%u) and `next_[E](p)` (%u)"
+             "are in a reversible race, yet we claim to have such a race between the"
+             "same event. This indicates the the SDPOR pseudocode implementation is broken "
+             "as it supplies these values.",
+             e, next_E_p);
+  Execution E_prime_v = get_prefix_before(e);
+  std::vector<sdpor::Execution::EventHandle> v;
+  std::unordered_set<aid_t> I_E_prime_v;
+  std::unordered_set<aid_t> disqualified_actors;
+
+  // Note `e + 1` here: `notdep(e, E)` is defined as the
+  // set of events that *occur-after* but don't *happen-after* `e`
+  for (auto e_prime = e + 1; e_prime <= next_E_p; ++e_prime) {
+    // Any event `e*` which occurs after `e` but which does not
+    // happen after `e` is a member of `v`. In addition to marking
+    // the event in `v`, we also "simulate" running the action `v`
+    // from E'
+    if (not happens_before(e, e_prime) or e_prime == next_E_p) {
+      // First, push the transition onto the hypothetical execution
+      E_prime_v.push_transition(get_event_with_handle(e_prime).get_transition());
+      const EventHandle e_prime_in_E_prime_v = E_prime_v.get_latest_event_handle().value();
+
+      // When checking whether any event in `dom_[E'](v)` happens before
+      // `next_[E'](q)` below for thread `q`, we must consider that the
+      // events relative to `E` (this execution) are different than those
+      // relative to `E'.v`. Thus e.g. event `7` in `E` may be event `4`
+      // in `E'.v`. Since we are asking about "happens-before"
+      // `-->_[E'.v]` about `E'.v`, we must build `v` relative to `E'`.
+      //
+      // Note that we add `q` to v regardless of whether `q` itself has been
+      // disqualified since  we've determined that `e_prime` "occurs-after" but
+      // does not "happen-after" `e`
+      v.push_back(e_prime_in_E_prime_v);
+
+      const aid_t q = E_prime_v.get_actor_with_handle(e_prime_in_E_prime_v);
+      if (disqualified_actors.count(q) > 0) { // Did we already note that `q` is not an initial?
+        continue;
+      }
+      const bool is_initial = std::none_of(v.begin(), v.end(), [&](const auto& e_star) {
+        return E_prime_v.happens_before(e_star, e_prime_in_E_prime_v);
+      });
+      if (is_initial) {
+        // If the backtrack set already contains `q`, we're done:
+        // they've made note to search for (or have already searched for)
+        // this initial
+        if (backtrack_set.count(q) > 0) {
+          return std::unordered_set<aid_t>{};
+        } else {
+          I_E_prime_v.insert(q);
+        }
+      } else {
+        // If `q` is disqualified as a candidate, clearly
+        // no event occurring after `e_prime` in `E` executed
+        // by actor `q` will qualify since any (valid) happens-before
+        // relation orders actions taken by each actor
+        disqualified_actors.insert(q);
+      }
+    }
+  }
+  xbt_assert(!I_E_prime_v.empty(),
+             "For any non-empty execution, we know that "
+             "at minimum one actor is an initial since "
+             "some execution is possible with respect to a "
+             "prefix before event `%u`, yet we didn't find anyone. "
+             "This implies the implementation of this function is broken.",
+             e);
+  return I_E_prime_v;
+}
+
+std::optional<PartialExecution> Execution::get_odpor_extension_from(EventHandle e, EventHandle e_prime,
+                                                                    const State& state_at_e) const
+{
+  // `e` is assumed to be in a reversible race with `e_prime`.
+  // If `e > e_prime`, then `e` occurs-after `e_prime` which means
+  // `e` could not race with if
+  if (e > e_prime) {
+    throw std::invalid_argument("ODPOR extensions can only be computed for "
+                                "events in a reversible race, which is claimed, "
+                                "yet the racing event 'occurs-after' the target");
+  }
+
+  if (empty()) {
+    return std::nullopt;
+  }
+
+  PartialExecution v;
+  std::vector<Execution::EventHandle> v_handles;
+  std::unordered_set<aid_t> WI_E_prime_v;
+  std::unordered_set<aid_t> disqualified_actors;
+  Execution E_prime_v                           = get_prefix_before(e);
+  const std::unordered_set<aid_t> sleep_E_prime = state_at_e.get_sleeping_actors();
+
+  // Note `e + 1` here: `notdep(e, E)` is defined as the
+  // set of events that *occur-after* but don't *happen-after* `e`
+  //
+  // SUBTLE NOTE: ODPOR requires us to compute `notdep(e, E)` EVEN THOUGH
+  // the race is between `e` and `e'`; that is, events occurring in `E`
+  // that "occur-after" `e'` may end up in the partial execution `v`.
+  //
+  // Observe that `notdep(e, E).proc(e')` will contain all transitions
+  // that don't happen-after `e` in the order they appear FOLLOWED BY
+  // THE **TRANSITION** ASSOCIATED WITH **`e'`**!!
+  //
+  // SUBTLE NOTE: Observe that any event that "happens-after" `e'`
+  // must necessarily "happen-after" `e` as well, since `e` and
+  // `e'` are presumed to be in a reversible race. Hence, we know that
+  // all events `e_star` such that `e` "happens-before" `e_star` cannot affect
+  // the enabledness of `e'`; furthermore, `e'` cannot affect the enabledness
+  // of any event independent with `e` that "occurs-after" `e'`
+  for (auto e_star = e + 1; e_star <= get_latest_event_handle().value(); ++e_star) {
+    // Any event `e*` which occurs after `e` but which does not
+    // happen after `e` is a member of `v`. In addition to marking
+    // the event in `v`, we also "simulate" running the action `v` from E'
+    // to be able to compute `--->[E'.v]`
+    if (not happens_before(e, e_star)) {
+      xbt_assert(e_star != e_prime,
+                 "Invariant Violation: We claimed events %u and %u were in a reversible race, yet we also "
+                 "claim that they do not happen-before one another. This is impossible: "
+                 "are you sure that the two events are in a reversible race?",
+                 e, e_prime);
+      E_prime_v.push_transition(get_event_with_handle(e_star).get_transition());
+      v.push_back(get_event_with_handle(e_star).get_transition());
+
+      const EventHandle e_star_in_E_prime_v = E_prime_v.get_latest_event_handle().value();
+
+      // When checking whether any event in `dom_[E'](v)` happens before
+      // `next_[E'](q)` below for thread `q`, we must consider that the
+      // events relative to `E` (this execution) are different than those
+      // relative to `E'.v`. Thus e.g. event `7` in `E` may be event `4`
+      // in `E'.v`. Since we are asking about "happens-before"
+      // `-->_[E'.v]` about `E'.v`, we must build `v` relative to `E'`
+      v_handles.push_back(e_star_in_E_prime_v);
+
+      // Note that we add `q` to v regardless of whether `q` itself has been
+      // disqualified since `q` may itself disqualify other actors
+      // (i.e. even if `q` is disqualified from being an initial, it
+      // is still contained in the sequence `v`)
+      const aid_t q = E_prime_v.get_actor_with_handle(e_star_in_E_prime_v);
+      if (disqualified_actors.count(q) > 0) { // Did we already note that `q` is not an initial?
+        continue;
+      }
+      const bool is_initial = std::none_of(v_handles.begin(), v_handles.end(), [&](const auto& e_star) {
+        return E_prime_v.happens_before(e_star, e_star_in_E_prime_v);
+      });
+      if (is_initial) {
+        // If the sleep set already contains `q`, we're done:
+        // we've found an initial contained in the sleep set and
+        // so the intersection is non-empty
+        if (sleep_E_prime.count(q) > 0) {
+          return std::nullopt;
+        } else {
+          WI_E_prime_v.insert(q);
+        }
+      } else {
+        // If `q` is disqualified as a candidate, clearly
+        // no event occurring after `e_prime` in `E` executed
+        // by actor `q` will qualify since any (valid) happens-before
+        // relation orders actions taken by each actor
+        disqualified_actors.insert(q);
+      }
+    }
+  }
+
+  // Now we add `e_prime := <q, i>` to `E'.v` and repeat the same work
+  // It's possible `proc(e_prime)` is an initial
+  //
+  // Note the form of `v` in the pseudocode:
+  //  `v := notdep(e, E).e'^
+  {
+    E_prime_v.push_transition(get_event_with_handle(e_prime).get_transition());
+    v.push_back(get_event_with_handle(e_prime).get_transition());
+
+    const EventHandle e_prime_in_E_prime_v = E_prime_v.get_latest_event_handle().value();
+    v_handles.push_back(e_prime_in_E_prime_v);
+
+    const bool is_initial = std::none_of(v_handles.begin(), v_handles.end(), [&](const auto& e_star) {
+      return E_prime_v.happens_before(e_star, e_prime_in_E_prime_v);
+    });
+    if (is_initial) {
+      if (const aid_t q = E_prime_v.get_actor_with_handle(e_prime_in_E_prime_v); sleep_E_prime.count(q) > 0) {
+        return std::nullopt;
+      } else {
+        WI_E_prime_v.insert(q);
+      }
+    }
+  }
+  {
+    const Execution pre_E_e    = get_prefix_before(e);
+    const auto sleeping_actors = state_at_e.get_sleeping_actors();
+
+    // Check if any enabled actor that is independent with
+    // this execution after `v` is contained in the sleep set
+    for (const auto& [aid, astate] : state_at_e.get_actors_list()) {
+      const bool is_in_WI_E =
+          astate.is_enabled() and pre_E_e.is_independent_with_execution_of(v, astate.get_transition());
+      const bool is_in_sleep_set = sleeping_actors.count(aid) > 0;
+
+      // `action(aid)` is in `WI_[E](v)` but also is contained in the sleep set.
+      // This implies that the intersection between the two is non-empty
+      if (is_in_WI_E && is_in_sleep_set) {
+        return std::nullopt;
+      }
+    }
+  }
+  return v;
+}
+
+bool Execution::is_initial_after_execution_of(const PartialExecution& w, aid_t p) const
+{
+  auto E_w = *this;
+  std::vector<EventHandle> w_handles;
+  for (const auto& w_i : w) {
+    // Take one step in the direction of `w`
+    E_w.push_transition(w_i);
+
+    // If that step happened to be executed by `p`,
+    // great: we know that `p` is contained in `w`.
+    // We now need to verify that it doens't "happen-after"
+    // any events which occur before it
+    if (w_i->aid_ == p) {
+      const auto p_handle = E_w.get_latest_event_handle().value();
+      return std::none_of(w_handles.begin(), w_handles.end(),
+                          [&](const auto handle) { return E_w.happens_before(handle, p_handle); });
+    } else {
+      w_handles.push_back(E_w.get_latest_event_handle().value());
+    }
+  }
+  return false;
+}
+
+bool Execution::is_independent_with_execution_of(const PartialExecution& w, std::shared_ptr<Transition> next_E_p) const
+{
+  // INVARIANT: Here, we assume that for any process `p_i` of `w`,
+  // the events of this execution followed by the execution of all
+  // actors occurring before `p_i` in `v` (`p_j`, `0 <= j < i`)
+  // are sufficient to enable `p_i`. This is fortunately the case
+  // with what ODPOR requires of us, viz. to ask the question about
+  // `v := notdep(e, E)` for some execution `E` and event `e` of
+  // that execution.
+  auto E_p_w = *this;
+  E_p_w.push_transition(std::move(next_E_p));
+  const auto p_handle = E_p_w.get_latest_event_handle().value();
+
+  // As we add events to `w`, verify that none
+  // of them "happen-after" the event associated with
+  // the step `next_E_p` (viz. p_handle)
+  for (const auto& w_i : w) {
+    E_p_w.push_transition(w_i);
+    const auto w_i_handle = E_p_w.get_latest_event_handle().value();
+    if (E_p_w.happens_before(p_handle, w_i_handle)) {
+      return false;
+    }
+  }
+  return true;
+}
+
+std::optional<PartialExecution> Execution::get_shortest_odpor_sq_subset_insertion(const PartialExecution& v,
+                                                                                  const PartialExecution& w) const
+{
+  // See section 4 of Abdulla. et al.'s 2017 ODPOR paper for details (specifically
+  // where the [iterative] computation of `v ~_[E] w` is described)
+  auto E_v   = *this;
+  auto w_now = w;
+
+  for (const auto& next_E_p : v) {
+    const aid_t p = next_E_p->aid_;
+
+    // Is `p in `I_[E](w)`?
+    if (E_v.is_initial_after_execution_of(w_now, p)) {
+      // Remove `p` from w and continue
+
+      // INVARIANT: If `p` occurs in `w`, it had better refer to the same
+      // transition referenced by `v`. Unfortunately, we have two
+      // sources of truth here which can be manipulated at the same
+      // time as arguments to the function. If ODPOR works correctly,
+      // they should always refer to the same value; but as a sanity check,
+      // we have an assert that tests that at least the types are the same.
+      const auto action_by_p_in_w =
+          std::find_if(w_now.begin(), w_now.end(), [=](const auto& action) { return action->aid_ == p; });
+      xbt_assert(action_by_p_in_w != w_now.end(), "Invariant violated: actor `p` "
+                                                  "is claimed to be an initial after `w` but is "
+                                                  "not actually contained in `w`. This indicates that there "
+                                                  "is a bug computing initials");
+      const auto& w_action = *action_by_p_in_w;
+      xbt_assert(w_action->type_ == next_E_p->type_,
+                 "Invariant violated: `v` claims that actor `%ld` executes '%s' while "
+                 "`w` claims that it executes '%s'. These two partial executions both "
+                 "refer to `next_[E](p)`, which should be the same",
+                 p, next_E_p->to_string(false).c_str(), w_action->to_string(false).c_str());
+      w_now.erase(action_by_p_in_w);
+    }
+    // Is `E ⊢ p ◇ w`?
+    else if (E_v.is_independent_with_execution_of(w_now, next_E_p)) {
+      // INVARIANT: Note that it is impossible for `p` to be
+      // excluded from the set `I_[E](w)` BUT ALSO be contained in
+      // `w` itself if `E ⊢ p ◇ w` (intuitively, the fact that `E ⊢ p ◇ w`
+      // means that are able to move `p` anywhere in `w` IF it occurred, so
+      // if it really does occur we know it must then be an initial).
+      // We assert this is the case here
+      const auto action_by_p_in_w =
+          std::find_if(w_now.begin(), w_now.end(), [=](const auto& action) { return action->aid_ == p; });
+      xbt_assert(action_by_p_in_w == w_now.end(),
+                 "Invariant violated: We claimed that actor `%ld` is not an initial "
+                 "after `w`, yet it's independent with all actions of `w` AND occurs in `w`."
+                 "This indicates that there is a bug computing initials",
+                 p);
+    } else {
+      // Neither of the two above conditions hold, so the relation fails
+      return std::nullopt;
+    }
+
+    // Move one step forward in the direction of `v` and repeat
+    E_v.push_transition(next_E_p);
+  }
+  return std::optional<PartialExecution>{std::move(w_now)};
+}
+
+bool Execution::happens_before(Execution::EventHandle e1_handle, Execution::EventHandle e2_handle) const
+{
+  // 1. "happens-before" (-->_E) is a subset of "occurs before" (<_E)
+  // and is an irreflexive relation
+  if (e1_handle >= e2_handle) {
+    return false;
+  }
+
+  // Each execution maintains a stack of clock vectors which are updated
+  // according to the procedure outlined in section 4 of the original DPOR paper
+  const Event& e2     = get_event_with_handle(e2_handle);
+  const aid_t proc_e1 = get_actor_with_handle(e1_handle);
+
+  if (const auto e1_in_e2_clock = e2.get_clock_vector().get(proc_e1); e1_in_e2_clock.has_value()) {
+    return e1_handle <= e1_in_e2_clock.value();
+  }
+  // If `e1` does not appear in e2's clock vector, this implies
+  // not only that the transitions associated with `e1` and `e2
+  // are independent, but further that there are no transitive
+  // dependencies between e1 and e2
+  return false;
+}
+
+} // namespace simgrid::mc::odpor
\ No newline at end of file
diff --git a/src/mc/explo/odpor/Execution.hpp b/src/mc/explo/odpor/Execution.hpp
new file mode 100644 (file)
index 0000000..f3bd7ed
--- /dev/null
@@ -0,0 +1,346 @@
+/* Copyright (c) 2007-2023. The SimGrid Team. All rights reserved.          */
+
+/* This program is free software; you can redistribute it and/or modify it
+ * under the terms of the license (GNU LGPL) which comes with this package. */
+
+#ifndef SIMGRID_MC_ODPOR_EXECUTION_HPP
+#define SIMGRID_MC_ODPOR_EXECUTION_HPP
+
+#include "src/mc/api/ClockVector.hpp"
+#include "src/mc/explo/odpor/odpor_forward.hpp"
+#include "src/mc/mc_forward.hpp"
+#include "src/mc/mc_record.hpp"
+#include "src/mc/transition/Transition.hpp"
+
+#include <list>
+#include <optional>
+#include <unordered_set>
+#include <vector>
+
+namespace simgrid::mc::odpor {
+
+std::vector<std::string> get_textual_trace(const PartialExecution& w);
+
+/**
+ * @brief The occurrence of a transition in an execution
+ *
+ * An execution is set of *events*, where each element represents
+ * the occurrence or execution of the `i`th step of a particular
+ * actor `j`
+ */
+class Event {
+  std::pair<std::shared_ptr<Transition>, ClockVector> contents_;
+
+public:
+  Event()                        = default;
+  Event(Event&&)                 = default;
+  Event(const Event&)            = default;
+  Event& operator=(const Event&) = default;
+  explicit Event(std::pair<std::shared_ptr<Transition>, ClockVector> pair) : contents_(std::move(pair)) {}
+
+  std::shared_ptr<Transition> get_transition() const { return std::get<0>(contents_); }
+  const ClockVector& get_clock_vector() const { return std::get<1>(contents_); }
+};
+
+/**
+ * @brief An ordered sequence of transitions which describe
+ * the evolution of a process undergoing model checking
+ *
+ * An execution conceptually is just a string of actors
+ * ids (e.g. "1.2.3.1.2.2.1.1"), where the `i`th occurrence
+ * of actor id `j` corresponds to the `i`th action executed
+ * by the actor with id `j` (viz. the `i`th step of actor `j`).
+ * Executions can stand alone on their own or can extend
+ * the execution of other sequences
+ *
+ * Executions are conceived based on the following papers:
+ * 1. "Source Sets: A Foundation for Optimal Dynamic Partial Order Reduction"
+ * by Abdulla et al.
+ *
+ * In addition to representing an actual steps taken,
+ * an execution keeps track of the "happens-before"
+ * relation among the transitions in the execution
+ * by following the procedure outlined in section 4 of the
+ * original DPOR paper with clock vectors.
+ * As new transitions are added to the execution, clock vectors are
+ * computed as appropriate and associated with the corresponding position
+ * in the execution. This allows us to determine “happens-before” in
+ * constant-time between points in the execution (called events
+ * [which is unfortunately the same name used in UDPOR for a slightly
+ * different concept]), albeit for an up-front cost of traversing the
+ * execution stack. The happens-before relation is important in many
+ * places in SDPOR and ODPOR.
+ *
+ * @note: For more nuanced happens-before relations, clock
+ * vectors may not always suffice. Clock vectors work
+ * well with transition-based dependencies like that used in
+ * SimGrid; but to have a more refined independence relation,
+ * an event-based dependency approach is needed. See the section 2
+ * in the ODPOR paper [1] concerning event-based dependencies and
+ * how the happens-before relation can be refined in a
+ * computation model much like that of SimGrid. In fact, the same issue
+ * arrises with UDPOR with context-sensitive dependencies:
+ * the two concepts are analogous if not identical
+ */
+class Execution {
+private:
+  std::vector<Event> contents_;
+  Execution(std::vector<Event>&& contents) : contents_(std::move(contents)) {}
+
+public:
+  using EventHandle = uint32_t;
+
+  Execution()                            = default;
+  Execution(const Execution&)            = default;
+  Execution& operator=(Execution const&) = default;
+  Execution(Execution&&)                 = default;
+  Execution(const PartialExecution&);
+
+  std::vector<std::string> get_textual_trace() const;
+
+  size_t size() const { return this->contents_.size(); }
+  bool empty() const { return this->contents_.empty(); }
+  auto begin() const { return this->contents_.begin(); }
+  auto end() const { return this->contents_.end(); }
+
+  /**
+   * @brief Computes the "core" portion the SDPOR algorithm,
+   * viz. the intersection of the backtracking set and the
+   * set of initials with respect to the *last* event added
+   * to the execution
+   *
+   * The "core" portion of the SDPOR algorithm is found on
+   * lines 6-9 of the pseudocode:
+   *
+   * 6 | let E' := pre(E, e)
+   * 7 | let v :=  notdep(e, E).p
+   * 8 | if I_[E'](v) ∩ backtrack(E') = empty then
+   * 9 |    --> add some q in I_[E'](v) to backtrack(E')
+   *
+   * This method computes all of the lines simultaneously,
+   * returning some actor `q` if it passes line 8 and exists.
+   * The event `e` and the set `backtrack(E')` are the provided
+   * arguments to the method.
+   *
+   * @param e the event with respect to which to determine
+   * whether a backtrack point needs to be added for the
+   * prefix corresponding to the execution prior to `e`
+   *
+   * @param backtrack_set The set of actors which should
+   * not be considered for selection as an SDPOR initial.
+   * While this set need not necessarily correspond to the
+   * backtrack set `backtrack(E')`, doing so provides what
+   * is expected for SDPOR
+   *
+   * See the SDPOR algorithm pseudocode in [1] for more
+   * details for the context of the function.
+   *
+   * @invariant: This method assumes that events `e` and
+   * `e' := get_latest_event_handle()` are in a *reversible* race
+   * as is explicitly the case in SDPOR
+   *
+   * @returns an actor not contained in `disqualified` which
+   * can serve as an initial to reverse the race between `e`
+   * and `e'`
+   */
+  std::unordered_set<aid_t> get_missing_source_set_actors_from(EventHandle e,
+                                                               const std::unordered_set<aid_t>& backtrack_set) const;
+
+  /**
+   * @brief Computes the analogous lines from the SDPOR algorithm
+   * in the ODPOR algorithm, viz. the intersection of the slee set
+   * and the set of weak initials with respect to the given pair
+   * of racing events
+   *
+   * This method computes lines 4-6 of the ODPOR pseudocode, viz.:
+   *
+   * 4 | let E' := pre(E, e)
+   * 5 | let v := notdep(e, E).e'^
+   * 6 | if sleep(E') ∩ WI_[E'](v) = empty then ...
+   *
+   * The sequence `v` is computed and returned as needed, based on whether
+   * the check on line 6 passes.
+   *
+   * @invariant: This method assumes that events `e` and
+   * `e_prime` are in a *reversible* race as is the case
+   * in ODPOR
+   */
+  std::optional<PartialExecution> get_odpor_extension_from(EventHandle e, EventHandle e_prime,
+                                                           const State& state_at_e) const;
+
+  /**
+   * @brief For a given sequence of actors `v` and a sequence of transitions `w`,
+   * computes the sequence, if any, that should be inserted as a child in wakeup tree for
+   * this execution
+   *
+   * Recall that the procedure for implementing the insertion
+   * is outlined in section 6.2 of Abdulla et al. 2017 as follows:
+   *
+   * | Let `v` be the smallest (w.r.t to "<") sequence in [the tree] B
+   * | such that `v ~_[E] w`. If `v` is a leaf node, the tree can be left
+   * | unmodified.
+   * |
+   * | Otherwise let `w'` be the shortest sequence such that `w [=_[E] v.w'`
+   * | and add `v.w'` as a new leaf, ordered after all already existing nodes
+   * | of the form `v.w''`
+   *
+   * This method computes the result `v.w'` as needed (viz. only if `v ~_[E] w`
+   * with respect to this execution `E`)
+   *
+   * The procedure for determining `v ~_[E] w` is given as Lemma 4.6 of
+   * Abdulla et al. 2017:
+   *
+   * | The relation `v ~_[E] w` holds if either
+   * | (1) v = <>, or
+   * | (2) v := p.v' and either
+   * |     (a) p in I_[E](w) and `v' ~_[E.p] (w \ p)`
+   * |     (b) E ⊢ p ◊ w and `v' ~_[E.p] w`
+   *
+   * @invariant: This method assumes that `E.v` is a valid execution, viz.
+   * that the events of `E` are sufficient to enabled `v_0` and that
+   * `v_0, ..., v_{i - 1}` are sufficient to enable `v_i`. This is the
+   * case when e.g. `v := notdep(e, E).p` for example in ODPOR
+   *
+   * @returns a partial execution `w'` that should be inserted
+   * as a child of a wakeup tree node with the associated sequence `v`.
+   */
+  std::optional<PartialExecution> get_shortest_odpor_sq_subset_insertion(const PartialExecution& v,
+                                                                         const PartialExecution& w) const;
+
+  /**
+   * @brief For a given sequence `w`, determines whether p in I_[E](w)
+   *
+   * @note: You may notice that some of the other methods compute this
+   * value as well. What we notice, though, in those cases is that
+   * we are repeatedly asking about initials with respect to an execution.
+   * It is better, then, to bunch the work together in those cases to
+   * get asymptotically better results (e.g. instead of calling with all
+   * `N` actors, we can process them "in-parallel" as is done with the
+   * computation of SDPOR initials)
+   */
+  bool is_initial_after_execution_of(const PartialExecution& w, aid_t p) const;
+
+  /**
+   * @brief Determines whether `E ⊢ p ◊ w` given the next action taken by `p`
+   */
+  bool is_independent_with_execution_of(const PartialExecution& w, std::shared_ptr<Transition> next_E_p) const;
+
+  /**
+   * @brief Determines the event associated with
+   * the given handle `handle`
+   */
+  const Event& get_event_with_handle(EventHandle handle) const { return contents_[handle]; }
+
+  /**
+   * @brief Determines the actor associated with
+   * the given event handle `handle`
+   */
+  aid_t get_actor_with_handle(EventHandle handle) const { return get_event_with_handle(handle).get_transition()->aid_; }
+
+  /**
+   * @brief Determines the transition associated with the given handle `handle`
+   */
+  const Transition* get_transition_for_handle(EventHandle handle) const
+  {
+    return get_event_with_handle(handle).get_transition().get();
+  }
+
+  /**
+   * @brief Returns a handle to the newest event of the execution,
+   * if such an event exists
+   */
+  std::optional<EventHandle> get_latest_event_handle() const
+  {
+    return contents_.empty() ? std::nullopt : std::optional<EventHandle>{static_cast<EventHandle>(size() - 1)};
+  }
+
+  /**
+   * @brief Returns a set of events which are in
+   * "immediate conflict" (according to the definition given
+   * in the ODPOR paper) with the given event
+   *
+   * Two events `e` and `e'` in an execution `E` are said to
+   * race iff
+   *
+   * 1. `proc(e) != proc(e')`; that is, the events correspond to
+   * the execution of different actors
+   * 2. `e -->_E e'` and there is no `e''` in `E` such that
+   *  `e -->_E e''` and `e'' -->_E e'`; that is, the two events
+   * "happen-before" one another in `E` and no other event in
+   * `E` "happens-between" `e` and `e'`
+   *
+   * @param handle the event with respect to which races are
+   * computed
+   * @returns a set of event handles from which race with `handle`
+   */
+  std::unordered_set<EventHandle> get_racing_events_of(EventHandle handle) const;
+
+  /**
+   * @brief Returns a set of events which are in a reversible
+   * race with the given event handle `handle`
+   *
+   * Two events `e` and `e'` in an execution `E` are said to
+   * be in a reversible race iff
+   *
+   * 1. `e` and `e'` race
+   * 2. In any equivalent execution sequence `E'` to `E`
+   * where `e` occurs immediately before `e'`, the actor
+   * running `e'` was enabled in the state prior to `e`
+   *
+   * @param handle the event with respect to which
+   * reversible races are computed
+   * @returns a set of event handles from which are in a reversible
+   * race with `handle`
+   */
+  std::unordered_set<EventHandle> get_reversible_races_of(EventHandle handle) const;
+
+  /**
+   * @brief Computes `pre(e, E)` as described in ODPOR [1]
+   *
+   * The execution `pre(e, E)` for an event `e` in an
+   * execution `E` is the contiguous prefix of events
+   * `E' <= E` up to by excluding the event `e` itself.
+   * The prefix intuitively represents the "history" of
+   * causes that permitted event `e` to exist (roughly
+   * speaking)
+   */
+  Execution get_prefix_before(EventHandle) const;
+
+  /**
+   * @brief Whether the event represented by `e1`
+   * "happens-before" the event represented by
+   * `e2` in the context of this execution
+   *
+   * In the terminology of the ODPOR paper,
+   * this function computes
+   *
+   * `e1 --->_E e2`
+   *
+   * where `E` is this execution
+   *
+   * @note: The happens-before relation computed by this
+   * execution is "coarse" in the sense that context-sensitive
+   * independence is not exploited. To include such context-sensitive
+   * dependencies requires a new method of keeping track of
+   * the happens-before procedure, which is nontrivial...
+   */
+  bool happens_before(EventHandle e1, EventHandle e2) const;
+
+  /**
+   * @brief Extends the execution by one more step
+   *
+   * Intutively, pushing a transition `t` onto execution `E`
+   * is equivalent to making the execution become (using the
+   * notation of [1]) `E.proc(t)` where `proc(t)` is the
+   * actor which executed transition `t`.
+   */
+  void push_transition(std::shared_ptr<Transition>);
+
+  /**
+   * @brief Extends the execution by a sequence of steps
+   */
+  void push_partial_execution(const PartialExecution&);
+};
+
+} // namespace simgrid::mc::odpor
+#endif
diff --git a/src/mc/explo/odpor/Execution_test.cpp b/src/mc/explo/odpor/Execution_test.cpp
new file mode 100644 (file)
index 0000000..364d9a0
--- /dev/null
@@ -0,0 +1,732 @@
+/* Copyright (c) 2017-2023. The SimGrid Team. All rights reserved.               */
+
+/* This program is free software; you can redistribute it and/or modify it
+ * under the terms of the license (GNU LGPL) which comes with this package. */
+
+#include "src/3rd-party/catch.hpp"
+#include "src/mc/explo/odpor/Execution.hpp"
+#include "src/mc/explo/odpor/odpor_tests_private.hpp"
+#include "src/mc/transition/TransitionComm.hpp"
+
+using namespace simgrid::mc;
+using namespace simgrid::mc::odpor;
+using namespace simgrid::mc::udpor;
+
+TEST_CASE("simgrid::mc::odpor::Execution: Constructing Executions")
+{
+  Execution execution;
+  REQUIRE(execution.empty());
+  REQUIRE(execution.size() == 0);
+  REQUIRE_FALSE(execution.get_latest_event_handle().has_value());
+}
+
+TEST_CASE("simgrid::mc::odpor::Execution: Testing Happens-Before")
+{
+  SECTION("Example 1")
+  {
+    // We check each permutation for happens before
+    // among the given actions added to the execution
+    const auto a1 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto a2 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto a3 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto a4 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 4);
+
+    Execution execution;
+    execution.push_transition(a1);
+    execution.push_transition(a2);
+    execution.push_transition(a3);
+    execution.push_transition(a4);
+
+    SECTION("Happens-before is irreflexive")
+    {
+      REQUIRE_FALSE(execution.happens_before(0, 0));
+      REQUIRE_FALSE(execution.happens_before(1, 1));
+      REQUIRE_FALSE(execution.happens_before(2, 2));
+      REQUIRE_FALSE(execution.happens_before(3, 3));
+    }
+
+    SECTION("Happens-before values for each pair of events")
+    {
+      REQUIRE_FALSE(execution.happens_before(0, 1));
+      REQUIRE_FALSE(execution.happens_before(0, 2));
+      REQUIRE(execution.happens_before(0, 3));
+      REQUIRE_FALSE(execution.happens_before(1, 2));
+      REQUIRE_FALSE(execution.happens_before(1, 3));
+      REQUIRE_FALSE(execution.happens_before(2, 3));
+    }
+
+    SECTION("Happens-before is a subset of 'occurs-before' ")
+    {
+      REQUIRE_FALSE(execution.happens_before(1, 0));
+      REQUIRE_FALSE(execution.happens_before(2, 0));
+      REQUIRE_FALSE(execution.happens_before(3, 0));
+      REQUIRE_FALSE(execution.happens_before(2, 1));
+      REQUIRE_FALSE(execution.happens_before(3, 1));
+      REQUIRE_FALSE(execution.happens_before(3, 2));
+    }
+  }
+
+  SECTION("Example 2")
+  {
+    const auto a1 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto a2 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto a3 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto a4 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 3);
+
+    // Notice that `a5` and `a6` are executed by the same actor; thus, although
+    // the actor is executing independent actions, each still "happen-before"
+    // the another
+
+    Execution execution;
+    execution.push_transition(a1);
+    execution.push_transition(a2);
+    execution.push_transition(a3);
+    execution.push_transition(a4);
+
+    SECTION("Happens-before is irreflexive")
+    {
+      REQUIRE_FALSE(execution.happens_before(0, 0));
+      REQUIRE_FALSE(execution.happens_before(1, 1));
+      REQUIRE_FALSE(execution.happens_before(2, 2));
+      REQUIRE_FALSE(execution.happens_before(3, 3));
+    }
+
+    SECTION("Happens-before values for each pair of events")
+    {
+      REQUIRE_FALSE(execution.happens_before(0, 1));
+      REQUIRE_FALSE(execution.happens_before(0, 2));
+      REQUIRE_FALSE(execution.happens_before(0, 3));
+      REQUIRE(execution.happens_before(1, 2));
+      REQUIRE(execution.happens_before(1, 3));
+      REQUIRE(execution.happens_before(2, 3));
+    }
+
+    SECTION("Happens-before is a subset of 'occurs-before'")
+    {
+      REQUIRE_FALSE(execution.happens_before(1, 0));
+      REQUIRE_FALSE(execution.happens_before(2, 0));
+      REQUIRE_FALSE(execution.happens_before(3, 0));
+      REQUIRE_FALSE(execution.happens_before(2, 1));
+      REQUIRE_FALSE(execution.happens_before(3, 1));
+      REQUIRE_FALSE(execution.happens_before(3, 2));
+    }
+  }
+
+  SECTION("Happens-before is transitively-closed")
+  {
+    SECTION("Example 1")
+    {
+      // Given a reversible race between events `e1` and `e3` in a simulation,
+      // we assert that `e5` would be eliminated from being contained in
+      // the sequence `notdep(e1, E)`
+      const auto e0 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 2);
+      const auto e1 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 1);
+      const auto e2 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 3);
+      const auto e3 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 1);
+      const auto e4 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 3);
+
+      Execution execution;
+      execution.push_partial_execution(PartialExecution{e0, e1, e2, e3, e4});
+      REQUIRE(execution.happens_before(0, 2));
+      REQUIRE(execution.happens_before(2, 4));
+      REQUIRE(execution.happens_before(0, 4));
+    }
+
+    SECTION("Stress testing transitivity of the happens-before relation")
+    {
+      // This test verifies that for each triple of events
+      // in the execution, for a modestly intersting one,
+      // that transitivity holds
+      const auto e0  = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 2);
+      const auto e1  = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 1);
+      const auto e2  = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+      const auto e3  = std::make_shared<DependentIfSameValueAction>(Transition::Type::UNKNOWN, 2, -10);
+      const auto e4  = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 3);
+      const auto e5  = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 0);
+      const auto e6  = std::make_shared<DependentIfSameValueAction>(Transition::Type::UNKNOWN, 2, -5);
+      const auto e7  = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 1);
+      const auto e8  = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 0);
+      const auto e9  = std::make_shared<DependentIfSameValueAction>(Transition::Type::UNKNOWN, 2, -10);
+      const auto e10 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 3);
+
+      Execution execution;
+      execution.push_partial_execution(PartialExecution{e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10});
+
+      const auto max_handle = execution.get_latest_event_handle();
+      for (Execution::EventHandle e_i = 0; e_i < max_handle; ++e_i) {
+        for (Execution::EventHandle e_j = 0; e_j < max_handle; ++e_j) {
+          for (Execution::EventHandle e_k = 0; e_k < max_handle; ++e_k) {
+            const bool e_i_before_e_j = execution.happens_before(e_i, e_j);
+            const bool e_j_before_e_k = execution.happens_before(e_j, e_k);
+            const bool e_i_before_e_k = execution.happens_before(e_i, e_k);
+            // Logical equivalent of `e_i_before_e_j ^ e_j_before_e_k --> e_i_before_e_k`
+            REQUIRE((!(e_i_before_e_j and e_j_before_e_k) or e_i_before_e_k));
+          }
+        }
+      }
+    }
+  }
+}
+
+TEST_CASE("simgrid::mc::odpor::Execution: Testing Racing Events and Initials")
+{
+  SECTION("Example 1")
+  {
+    const auto a1 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto a2 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto a3 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto a4 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto a5 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 2);
+
+    Execution execution;
+    execution.push_transition(a1);
+    execution.push_transition(a2);
+    execution.push_transition(a3);
+    execution.push_transition(a4);
+    execution.push_transition(a5);
+
+    // Nothing comes before event 0
+    REQUIRE(execution.get_racing_events_of(0) == std::unordered_set<Execution::EventHandle>{});
+
+    // Events 0 and 1 are independent
+    REQUIRE(execution.get_racing_events_of(1) == std::unordered_set<Execution::EventHandle>{});
+
+    // 2 and 1 are executed by different actors and happen right after each other
+    REQUIRE(execution.get_racing_events_of(2) == std::unordered_set<Execution::EventHandle>{1});
+
+    // Although a3 and a4 are dependent, they are executed by the same actor
+    REQUIRE(execution.get_racing_events_of(3) == std::unordered_set<Execution::EventHandle>{});
+
+    // Event 4 is in a race with neither event 0 nor event 2 since those events
+    // "happen-before" event 3 with which event 4 races
+    //
+    // Furthermore, event 1 is run by the same actor and thus also is not in a race.
+    // Hence, only event 3 races with event 4
+    REQUIRE(execution.get_racing_events_of(4) == std::unordered_set<Execution::EventHandle>{3});
+  }
+
+  SECTION("Example 2: Events with multiple races")
+  {
+    const auto a1 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto a2 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto a3 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto a4 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 3);
+
+    Execution execution;
+    execution.push_transition(a1);
+    execution.push_transition(a2);
+    execution.push_transition(a3);
+    execution.push_transition(a4);
+
+    // Nothing comes before event 0
+    REQUIRE(execution.get_racing_events_of(0) == std::unordered_set<Execution::EventHandle>{});
+
+    // Events 0 and 1 are independent
+    REQUIRE(execution.get_racing_events_of(1) == std::unordered_set<Execution::EventHandle>{});
+
+    // Event 2 is independent with event 1 and run by the same actor as event 0
+    REQUIRE(execution.get_racing_events_of(2) == std::unordered_set<Execution::EventHandle>{});
+
+    // All events are dependent with event 3, but event 0 "happens-before" event 2
+    REQUIRE(execution.get_racing_events_of(3) == std::unordered_set<Execution::EventHandle>{1, 2});
+
+    SECTION("Check initials with respect to event 1")
+    {
+      // First, note that v := notdep(1, execution).p == {e2}.{e3} == {e2, e3}
+      // Since e2 -->_E e3, actor 3 is not an initial for E' := pre(1, execution)
+      // with respect to `v`. e2, however, has nothing happening before it in dom_E(v),
+      // so it is an initial of E' wrt. `v`
+      const auto initial_wrt_event1 = execution.get_missing_source_set_actors_from(1, std::unordered_set<aid_t>{});
+      REQUIRE(initial_wrt_event1 == std::unordered_set<aid_t>{1});
+    }
+
+    SECTION("Check initials with respect to event 2")
+    {
+      // First, note that v := notdep(1, execution).p == {}.{e3} == {e3}
+      // e3 has nothing happening before it in dom_E(v), so it is an initial
+      // of E' wrt. `v`
+      const auto initial_wrt_event2 = execution.get_missing_source_set_actors_from(2, std::unordered_set<aid_t>{});
+      REQUIRE(initial_wrt_event2 == std::unordered_set<aid_t>{3});
+    }
+  }
+
+  SECTION("Example 3: Testing 'Lock' Example")
+  {
+    // In this example, `e0` and `e1` are lock actions that
+    // are in a race. We assert that
+    const auto e0 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto e1 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto e2 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto e3 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto e4 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 3);
+
+    Execution execution;
+    execution.push_transition(e0);
+    execution.push_transition(e1);
+    execution.push_transition(e2);
+    execution.push_transition(e3);
+    execution.push_transition(e4);
+    REQUIRE(execution.get_racing_events_of(4) == std::unordered_set<Execution::EventHandle>{0});
+  }
+
+  SECTION("Example 4: Indirect Races")
+  {
+    const auto e0 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto e1 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto e2 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto e3 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto e4 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 6);
+    const auto e5 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto e6 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto e7 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto e8 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 4);
+    const auto e9 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 2);
+
+    Execution execution(PartialExecution{e0, e1, e2, e3, e4, e5, e6, e7, e8, e9});
+
+    // Nothing comes before event 0
+    REQUIRE(execution.get_racing_events_of(0) == std::unordered_set<Execution::EventHandle>{});
+
+    // Events 0 and 1 are independent
+    REQUIRE(execution.get_racing_events_of(1) == std::unordered_set<Execution::EventHandle>{});
+
+    // Events 1 and 2 are independent
+    REQUIRE(execution.get_racing_events_of(2) == std::unordered_set<Execution::EventHandle>{});
+
+    // Events 1 and 3 are independent; the rest are executed by the same actor
+    REQUIRE(execution.get_racing_events_of(3) == std::unordered_set<Execution::EventHandle>{});
+
+    // 1. Events 3 and 4 race
+    // 2. Events 2 and 4 do NOT race since 2 --> 3 --> 4
+    // 3. Events 1 and 4 do NOT race since 1 is independent of 4
+    // 4. Events 0 and 4 do NOT race since 0 --> 2 --> 4
+    REQUIRE(execution.get_racing_events_of(4) == std::unordered_set<Execution::EventHandle>{3});
+
+    // Events 4 and 5 race; and because everyone before 4 (including 3) either
+    // a) happens-before, b) races, or c) does not race with 4, 4 is the race
+    REQUIRE(execution.get_racing_events_of(5) == std::unordered_set<Execution::EventHandle>{4});
+
+    // The same logic that applied to event 5 applies to event 6
+    REQUIRE(execution.get_racing_events_of(6) == std::unordered_set<Execution::EventHandle>{5});
+
+    // The same logic applies, except that this time since events 6 and 7 are run
+    // by the same actor, they don'tt actually race with one another
+    REQUIRE(execution.get_racing_events_of(7) == std::unordered_set<Execution::EventHandle>{});
+
+    // Event 8 is independent with everything
+    REQUIRE(execution.get_racing_events_of(8) == std::unordered_set<Execution::EventHandle>{});
+
+    // Event 9 is independent with events 7 and 8; event 6, however, is in race with 9.
+    // The same logic above eliminates events before 6
+    REQUIRE(execution.get_racing_events_of(9) == std::unordered_set<Execution::EventHandle>{6});
+  }
+
+  SECTION("Example 5: Stress testing race detection")
+  {
+    const auto e0  = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto e1  = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto e2  = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto e3  = std::make_shared<DependentIfSameValueAction>(Transition::Type::UNKNOWN, 2, -10);
+    const auto e4  = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto e5  = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 0);
+    const auto e6  = std::make_shared<DependentIfSameValueAction>(Transition::Type::UNKNOWN, 2, -5);
+    const auto e7  = std::make_shared<DependentIfSameValueAction>(Transition::Type::UNKNOWN, 1, -5);
+    const auto e8  = std::make_shared<DependentIfSameValueAction>(Transition::Type::UNKNOWN, 0, 4);
+    const auto e9  = std::make_shared<DependentIfSameValueAction>(Transition::Type::UNKNOWN, 2, -10);
+    const auto e10 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 3);
+
+    Execution execution(PartialExecution{e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10});
+
+    // Nothing comes before event 0
+    CHECK(execution.get_racing_events_of(0) == std::unordered_set<Execution::EventHandle>{});
+
+    // Events 0 and 1 are independent
+    CHECK(execution.get_racing_events_of(1) == std::unordered_set<Execution::EventHandle>{});
+
+    // Events 1 and 2 are executed by the same actor, even though they are dependent
+    CHECK(execution.get_racing_events_of(2) == std::unordered_set<Execution::EventHandle>{});
+
+    // Events 2 and 3 are independent while events 1 and 2 are dependent
+    CHECK(execution.get_racing_events_of(3) == std::unordered_set<Execution::EventHandle>{1});
+
+    // Event 4 is independent with everything so it can never be in a race
+    CHECK(execution.get_racing_events_of(4) == std::unordered_set<Execution::EventHandle>{});
+
+    // Event 5 is independent with event 4. Since events 2 and 3 are dependent with event 5,
+    // but are independent of each other, both events are in a race with event 5
+    CHECK(execution.get_racing_events_of(5) == std::unordered_set<Execution::EventHandle>{2, 3});
+
+    // Events 5 and 6 are dependent. Everyone before 5 who's dependent with 5
+    // cannot be in a race with 6; everyone before 5 who's independent with 5
+    // could not race with 6
+    CHECK(execution.get_racing_events_of(6) == std::unordered_set<Execution::EventHandle>{5});
+
+    // Same goes for event 7 as for 6
+    CHECK(execution.get_racing_events_of(7) == std::unordered_set<Execution::EventHandle>{6});
+
+    // Events 5 and 8 are both run by the same actor; events in-between are independent
+    CHECK(execution.get_racing_events_of(8) == std::unordered_set<Execution::EventHandle>{});
+
+    // Event 6 blocks event 9 from racing with event 6
+    CHECK(execution.get_racing_events_of(9) == std::unordered_set<Execution::EventHandle>{});
+
+    // Event 10 is independent with everything so it can never be in a race
+    CHECK(execution.get_racing_events_of(10) == std::unordered_set<Execution::EventHandle>{});
+  }
+
+  SECTION("Example with many races for one event")
+  {
+    const auto e0 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto e1 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto e2 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto e3 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 4);
+    const auto e4 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 5);
+    const auto e5 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 6);
+    const auto e6 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 7);
+
+    Execution execution(PartialExecution{e0, e1, e2, e3, e4, e5, e6});
+    REQUIRE(execution.get_racing_events_of(6) == std::unordered_set<Execution::EventHandle>{0, 1, 2, 3, 4, 5});
+  }
+}
+
+TEST_CASE("simgrid::mc::odpor::Execution: Independence Tests")
+{
+  SECTION("Complete independence")
+  {
+    // Every transition is independent with every other and run by different actors. Hopefully
+    // we say that the events are independent with each other...
+    const auto a0 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto a1 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto a2 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto a3 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 4);
+    const auto a4 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 5);
+    const auto a5 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 6);
+    const auto a6 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 7);
+    const auto a7 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 7);
+    Execution execution(PartialExecution{a0, a1, a2, a3});
+
+    REQUIRE(execution.is_independent_with_execution_of(PartialExecution{a4, a5}, a6));
+    REQUIRE(execution.is_independent_with_execution_of(PartialExecution{a6, a5}, a1));
+    REQUIRE(execution.is_independent_with_execution_of(PartialExecution{a6, a1}, a0));
+    REQUIRE(Execution().is_independent_with_execution_of(PartialExecution{a7, a7, a1}, a3));
+    REQUIRE(Execution().is_independent_with_execution_of(PartialExecution{a4, a0, a1}, a3));
+    REQUIRE(Execution().is_independent_with_execution_of(PartialExecution{a0, a7, a1}, a2));
+
+    // In this case, we notice that `a6` and `a7` are executed by the same actor.
+    // Hence, a6 cannot be independent with extending the execution with a4.a5.a7.
+    // Notice that we are treating *a6* as the next step of actor 7 (that is what we
+    // supplied as an argument)
+    REQUIRE_FALSE(execution.is_independent_with_execution_of(PartialExecution{a4, a5, a7}, a6));
+  }
+
+  SECTION("Independence is trivial with an empty extension")
+  {
+    REQUIRE(Execution().is_independent_with_execution_of(
+        PartialExecution{}, std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 1)));
+  }
+
+  SECTION("Dependencies stopping independence from being possible")
+  {
+    const auto a0    = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto a1    = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto a2    = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto a3    = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 4);
+    const auto a4    = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 4);
+    const auto a5    = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto a6    = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto a7    = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto a8    = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 4);
+    const auto indep = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 2);
+    Execution execution(PartialExecution{a0, a1, a2, a3});
+
+    // We see that although `a4` comes after `a1` with which it is dependent, it
+    // would come before "a6" in the partial execution `w`, so it would not be independent
+    REQUIRE_FALSE(execution.is_independent_with_execution_of(PartialExecution{a5, a6, a7}, a4));
+
+    // Removing `a6` from the picture, though, gives us the independence we're looking for.
+    REQUIRE(execution.is_independent_with_execution_of(PartialExecution{a5, a7}, a4));
+
+    // BUT, we we ask about a transition which is run by the same actor, even if they would be
+    // independent otherwise, we again lose independence
+    REQUIRE_FALSE(execution.is_independent_with_execution_of(PartialExecution{a5, a7, a8}, a4));
+
+    // This is a more interesting case:
+    // `indep` clearly is independent with the rest of the series, even though
+    // there are dependencies among the other events in the partial execution
+    REQUIRE(Execution().is_independent_with_execution_of(PartialExecution{a1, a6, a7}, indep));
+
+    // This ensures that independence is trivial with an empty partial execution
+    REQUIRE(execution.is_independent_with_execution_of(PartialExecution{}, a1));
+  }
+
+  SECTION("More interesting dependencies stopping independence")
+  {
+    const auto e0 = std::make_shared<DependentIfSameValueAction>(Transition::Type::UNKNOWN, 1, 5);
+    const auto e1 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto e2 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto e3 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto e4 = std::make_shared<DependentIfSameValueAction>(Transition::Type::UNKNOWN, 3, 5);
+    const auto e5 = std::make_shared<DependentIfSameValueAction>(Transition::Type::UNKNOWN, 4, 4);
+    Execution execution(PartialExecution{e0, e1, e2, e3, e4, e5});
+
+    SECTION("Action run by same actor disqualifies independence")
+    {
+      const auto w_1 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+      const auto w_2 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+      const auto w_3 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+      const auto w_4 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 4);
+      const auto w   = PartialExecution{w_1, w_2, w_3};
+
+      const auto actor4_action  = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 4);
+      const auto actor4_action2 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 4);
+
+      // Action `actor4_action` is independent with everything EXCEPT the last transition
+      // which is executed by the same actor
+      execution.is_independent_with_execution_of(w, actor4_action);
+
+      // Action `actor4_action2` is independent with everything
+      // EXCEPT the last transition which is executed by the same actor
+      execution.is_independent_with_execution_of(w, actor4_action);
+    }
+  }
+}
+
+TEST_CASE("simgrid::mc::odpor::Execution: Initials Test")
+{
+  const auto a0    = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+  const auto a1    = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 1);
+  const auto a2    = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 3);
+  const auto a3    = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 4);
+  const auto a4    = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 4);
+  const auto a5    = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 2);
+  const auto a6    = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 3);
+  const auto a7    = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+  const auto a8    = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 4);
+  const auto indep = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 2);
+  Execution execution(PartialExecution{a0, a1, a2, a3});
+
+  SECTION("Initials trivial with empty executions")
+  {
+    // There are no initials for an empty extension since
+    // a requirement is that the actor be contained in the
+    // extension itself
+    REQUIRE_FALSE(execution.is_initial_after_execution_of(PartialExecution{}, 0));
+    REQUIRE_FALSE(execution.is_initial_after_execution_of(PartialExecution{}, 1));
+    REQUIRE_FALSE(execution.is_initial_after_execution_of(PartialExecution{}, 2));
+    REQUIRE_FALSE(Execution().is_initial_after_execution_of(PartialExecution{}, 0));
+    REQUIRE_FALSE(Execution().is_initial_after_execution_of(PartialExecution{}, 1));
+    REQUIRE_FALSE(Execution().is_initial_after_execution_of(PartialExecution{}, 2));
+  }
+
+  SECTION("The first actor is always an initial")
+  {
+    // Even in the case that the action is dependent with every
+    // other, if it is the first action to occur as part of the
+    // extension of the execution sequence, by definition it is
+    // an initial (recall that initials intuitively tell you which
+    // actions can be run starting from an execution `E`; if we claim
+    // to witness `E.w`, then clearly at least the first step of `w` is an initial)
+    REQUIRE(execution.is_initial_after_execution_of(PartialExecution{a3}, a3->aid_));
+    REQUIRE(execution.is_initial_after_execution_of(PartialExecution{a2, a3, a6}, a2->aid_));
+    REQUIRE(execution.is_initial_after_execution_of(PartialExecution{a6, a1, a0}, a6->aid_));
+    REQUIRE(Execution().is_initial_after_execution_of(PartialExecution{a0, a1, a2, a3}, a0->aid_));
+    REQUIRE(Execution().is_initial_after_execution_of(PartialExecution{a5, a2, a8, a7, a6, a0}, a5->aid_));
+    REQUIRE(Execution().is_initial_after_execution_of(PartialExecution{a8, a7, a1}, a8->aid_));
+  }
+
+  SECTION("Example: Disqualified and re-qualified after switching ordering")
+  {
+    // Even though actor `2` executes an independent
+    // transition later on, it is blocked since its first transition
+    // is dependent with actor 1's transition
+    REQUIRE_FALSE(Execution().is_initial_after_execution_of(PartialExecution{a1, a5, indep}, 2));
+
+    // However, if actor 2 executes its independent action first, it DOES become an initial
+    REQUIRE(Execution().is_initial_after_execution_of(PartialExecution{a1, indep, a5}, 2));
+  }
+
+  SECTION("Example: Large partial executions")
+  {
+    // The record trace is `1 3 4 4 3 1 4 2`
+
+    // Actor 1 starts the execution
+    REQUIRE(Execution().is_initial_after_execution_of(PartialExecution{a1, a2, a3, a4, a6, a7, a8, indep}, 1));
+
+    // Actor 2 all the way at the end is independent with everybody: despite
+    // the tangle that comes before it, we can more it to the front with no issue
+    REQUIRE(Execution().is_initial_after_execution_of(PartialExecution{a1, a2, a3, a4, a6, a7, a8, indep}, 2));
+
+    // Actor 3 is eliminated since `a1` is dependent with `a2`
+    REQUIRE_FALSE(Execution().is_initial_after_execution_of(PartialExecution{a1, a2, a3, a4, a6, a7, a8, indep}, 3));
+
+    // Likewise with actor 4
+    REQUIRE_FALSE(Execution().is_initial_after_execution_of(PartialExecution{a1, a2, a3, a4, a6, a7, a8, indep}, 4));
+  }
+
+  SECTION("Example: Stress tests for initials computation")
+  {
+    const auto v_1 = std::make_shared<DependentIfSameValueAction>(Transition::Type::UNKNOWN, 1, 3);
+    const auto v_2 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto v_3 = std::make_shared<DependentIfSameValueAction>(Transition::Type::UNKNOWN, 2, 3);
+    const auto v_4 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto v_5 = std::make_shared<DependentIfSameValueAction>(Transition::Type::UNKNOWN, 3, 8);
+    const auto v_6 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto v_7 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto v_8 = std::make_shared<DependentIfSameValueAction>(Transition::Type::UNKNOWN, 4, 3);
+    const auto v   = PartialExecution{v_1, v_2, v_3, v_4};
+
+    // Actor 1 being the first actor in the expansion, it is clearly an initial
+    REQUIRE(Execution().is_initial_after_execution_of(v, 1));
+
+    // Actor 2 can't be switched before actor 1 without creating an trace
+    // that leads to a different state than that of `E.v := ().v := v`
+    REQUIRE_FALSE(Execution().is_initial_after_execution_of(v, 2));
+
+    // The first action of Actor 3 is independent with what comes before it, so it can
+    // be moved forward. Note that this is the case even though it later executes and action
+    // that's dependent with most everyone else
+    REQUIRE(Execution().is_initial_after_execution_of(v, 3));
+
+    // Actor 4 is blocked actor 3's second action `v_7`
+    REQUIRE_FALSE(Execution().is_initial_after_execution_of(v, 4));
+  }
+}
+
+TEST_CASE("simgrid::mc::odpor::Execution: SDPOR Backtracking Simulation")
+{
+  // This test case assumes that each detected race is detected to also
+  // be reversible. For each reversible
+  const auto e0 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+  const auto e1 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 1);
+  const auto e2 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 3);
+  const auto e3 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 4);
+  const auto e4 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 4);
+  const auto e5 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 2);
+  const auto e6 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 3);
+  const auto e7 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+
+  Execution execution;
+
+  execution.push_transition(e0);
+  REQUIRE(execution.get_racing_events_of(0) == std::unordered_set<Execution::EventHandle>{});
+
+  execution.push_transition(e1);
+  REQUIRE(execution.get_racing_events_of(1) == std::unordered_set<Execution::EventHandle>{});
+
+  // Actor 3, since it starts the extension from event 1, clearly is an initial from there
+  execution.push_transition(e2);
+  REQUIRE(execution.get_racing_events_of(2) == std::unordered_set<Execution::EventHandle>{1});
+  CHECK(execution.get_missing_source_set_actors_from(1, std::unordered_set<aid_t>{}) == std::unordered_set<aid_t>{3});
+  CHECK(execution.get_missing_source_set_actors_from(1, std::unordered_set<aid_t>{4, 5}) ==
+        std::unordered_set<aid_t>{3});
+  CHECK(execution.get_missing_source_set_actors_from(1, std::unordered_set<aid_t>{3}) == std::unordered_set<aid_t>{});
+
+  // e1 and e3 are in an reversible race. Actor 4 is not hindered from being moved to
+  // the front since e2 is independent with e3; hence, it is an initial
+  execution.push_transition(e3);
+  REQUIRE(execution.get_racing_events_of(3) == std::unordered_set<Execution::EventHandle>{1});
+  CHECK(execution.get_missing_source_set_actors_from(1, std::unordered_set<aid_t>{}) == std::unordered_set<aid_t>{4});
+  CHECK(execution.get_missing_source_set_actors_from(1, std::unordered_set<aid_t>{3, 5}) ==
+        std::unordered_set<aid_t>{4});
+  CHECK(execution.get_missing_source_set_actors_from(1, std::unordered_set<aid_t>{4}) == std::unordered_set<aid_t>{});
+
+  // e4 is not in a race since e3 is run by the same actor and occurs before e4
+  execution.push_transition(e4);
+  REQUIRE(execution.get_racing_events_of(4) == std::unordered_set<Execution::EventHandle>{});
+
+  // e5 is independent with everything between e1 and e5, and `proc(e5) == 2`
+  execution.push_transition(e5);
+  REQUIRE(execution.get_racing_events_of(5) == std::unordered_set<Execution::EventHandle>{1});
+  CHECK(execution.get_missing_source_set_actors_from(1, std::unordered_set<aid_t>{}) == std::unordered_set<aid_t>{2});
+  CHECK(execution.get_missing_source_set_actors_from(1, std::unordered_set<aid_t>{4, 5}) ==
+        std::unordered_set<aid_t>{2});
+  CHECK(execution.get_missing_source_set_actors_from(1, std::unordered_set<aid_t>{2}) == std::unordered_set<aid_t>{});
+
+  // Event 6 has two races: one with event 4 and one with event 5. In the latter race, actor 3 follows
+  // immediately after and so is evidently a source set actor; in the former race, only actor 2 can
+  // be brought to the front of the queue
+  execution.push_transition(e6);
+  REQUIRE(execution.get_racing_events_of(6) == std::unordered_set<Execution::EventHandle>{4, 5});
+  CHECK(execution.get_missing_source_set_actors_from(4, std::unordered_set<aid_t>{}) == std::unordered_set<aid_t>{2});
+  CHECK(execution.get_missing_source_set_actors_from(4, std::unordered_set<aid_t>{6, 7}) ==
+        std::unordered_set<aid_t>{2});
+  CHECK(execution.get_missing_source_set_actors_from(4, std::unordered_set<aid_t>{2}) == std::unordered_set<aid_t>{});
+  CHECK(execution.get_missing_source_set_actors_from(5, std::unordered_set<aid_t>{}) == std::unordered_set<aid_t>{3});
+  CHECK(execution.get_missing_source_set_actors_from(5, std::unordered_set<aid_t>{6, 7}) ==
+        std::unordered_set<aid_t>{3});
+  CHECK(execution.get_missing_source_set_actors_from(5, std::unordered_set<aid_t>{3}) == std::unordered_set<aid_t>{});
+
+  // Finally, event e7 races with e6 and `proc(e7) = 1` is brought forward
+  execution.push_transition(e7);
+  REQUIRE(execution.get_racing_events_of(7) == std::unordered_set<Execution::EventHandle>{6});
+  CHECK(execution.get_missing_source_set_actors_from(1, std::unordered_set<aid_t>{}) == std::unordered_set<aid_t>{1});
+  CHECK(execution.get_missing_source_set_actors_from(1, std::unordered_set<aid_t>{4, 5}) ==
+        std::unordered_set<aid_t>{1});
+  CHECK(execution.get_missing_source_set_actors_from(1, std::unordered_set<aid_t>{1}) == std::unordered_set<aid_t>{});
+}
+
+TEST_CASE("simgrid::mc::odpor::Execution: ODPOR Smallest Sequence Tests")
+{
+  const auto a0 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 2);
+  const auto a1 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 4);
+  const auto a2 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 3);
+  const auto a3 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 1);
+  const auto a4 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 2);
+  const auto a5 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 4);
+
+  Execution execution;
+  execution.push_transition(a0);
+  execution.push_transition(a1);
+  execution.push_transition(a2);
+  execution.push_transition(a3);
+  execution.push_transition(a4);
+  execution.push_transition(a5);
+
+  SECTION("Equivalent insertions")
+  {
+    SECTION("Example: Eliminated through independence")
+    {
+      // TODO: Is this even a sensible question to ask if the two sequences
+      // don't agree upon what each actor executed after `E`?
+      const auto insertion =
+          Execution().get_shortest_odpor_sq_subset_insertion(PartialExecution{a1, a2}, PartialExecution{a2, a5});
+      REQUIRE(insertion.has_value());
+      REQUIRE(insertion.value() == PartialExecution{});
+    }
+
+    SECTION("Exact match yields empty set")
+    {
+      const auto insertion =
+          Execution().get_shortest_odpor_sq_subset_insertion(PartialExecution{a1, a2}, PartialExecution{a1, a2});
+      REQUIRE(insertion.has_value());
+      REQUIRE(insertion.value() == PartialExecution{});
+    }
+  }
+
+  SECTION("Match against empty executions")
+  {
+    SECTION("Empty `v` trivially yields `w`")
+    {
+      auto insertion =
+          Execution().get_shortest_odpor_sq_subset_insertion(PartialExecution{}, PartialExecution{a1, a5, a2});
+      REQUIRE(insertion.has_value());
+      REQUIRE(insertion.value() == PartialExecution{a1, a5, a2});
+
+      insertion = execution.get_shortest_odpor_sq_subset_insertion(PartialExecution{}, PartialExecution{a1, a5, a2});
+      REQUIRE(insertion.has_value());
+      REQUIRE(insertion.value() == PartialExecution{a1, a5, a2});
+    }
+
+    SECTION("Empty `w` yields empty execution")
+    {
+      auto insertion =
+          Execution().get_shortest_odpor_sq_subset_insertion(PartialExecution{a1, a4, a5}, PartialExecution{});
+      REQUIRE(insertion.has_value());
+      REQUIRE(insertion.value() == PartialExecution{});
+
+      insertion = execution.get_shortest_odpor_sq_subset_insertion(PartialExecution{a1, a5, a2}, PartialExecution{});
+      REQUIRE(insertion.has_value());
+      REQUIRE(insertion.value() == PartialExecution{});
+    }
+  }
+}
diff --git a/src/mc/explo/odpor/ReversibleRaceCalculator.cpp b/src/mc/explo/odpor/ReversibleRaceCalculator.cpp
new file mode 100644 (file)
index 0000000..efab9d3
--- /dev/null
@@ -0,0 +1,199 @@
+/* Copyright (c) 2008-2023. The SimGrid Team. All rights reserved.          */
+
+/* This program is free software; you can redistribute it and/or modify it
+ * under the terms of the license (GNU LGPL) which comes with this package. */
+
+#include "src/mc/explo/odpor/ReversibleRaceCalculator.hpp"
+#include "src/mc/explo/odpor/Execution.hpp"
+
+#include <functional>
+#include <unordered_map>
+#include <xbt/asserts.h>
+#include <xbt/ex.h>
+
+namespace simgrid::mc::odpor {
+
+bool ReversibleRaceCalculator::is_race_reversible(const Execution& E, Execution::EventHandle e1,
+                                                  Execution::EventHandle e2)
+{
+  using Action     = Transition::Type;
+  using Handler    = std::function<bool(const Execution&, Execution::EventHandle, const Transition*)>;
+  using HandlerMap = std::unordered_map<Action, Handler>;
+
+  const static HandlerMap handlers =
+      HandlerMap{{Action::ACTOR_JOIN, &ReversibleRaceCalculator::is_race_reversible_ActorJoin},
+                 {Action::BARRIER_ASYNC_LOCK, &ReversibleRaceCalculator::is_race_reversible_BarrierAsyncLock},
+                 {Action::BARRIER_WAIT, &ReversibleRaceCalculator::is_race_reversible_BarrierWait},
+                 {Action::COMM_ASYNC_SEND, &ReversibleRaceCalculator::is_race_reversible_CommSend},
+                 {Action::COMM_ASYNC_RECV, &ReversibleRaceCalculator::is_race_reversible_CommRecv},
+                 {Action::COMM_TEST, &ReversibleRaceCalculator::is_race_reversible_CommTest},
+                 {Action::COMM_WAIT, &ReversibleRaceCalculator::is_race_reversible_CommWait},
+                 {Action::MUTEX_ASYNC_LOCK, &ReversibleRaceCalculator::is_race_reversible_MutexAsyncLock},
+                 {Action::MUTEX_TEST, &ReversibleRaceCalculator::is_race_reversible_MutexTest},
+                 {Action::MUTEX_TRYLOCK, &ReversibleRaceCalculator::is_race_reversible_MutexTrylock},
+                 {Action::MUTEX_UNLOCK, &ReversibleRaceCalculator::is_race_reversible_MutexUnlock},
+                 {Action::MUTEX_WAIT, &ReversibleRaceCalculator::is_race_reversible_MutexWait},
+                 {Action::OBJECT_ACCESS, &ReversibleRaceCalculator::is_race_reversible_ObjectAccess},
+                 {Action::RANDOM, &ReversibleRaceCalculator::is_race_reversible_Random},
+                 {Action::SEM_ASYNC_LOCK, &ReversibleRaceCalculator::is_race_reversible_SemAsyncLock},
+                 {Action::SEM_UNLOCK, &ReversibleRaceCalculator::is_race_reversible_SemUnlock},
+                 {Action::SEM_WAIT, &ReversibleRaceCalculator::is_race_reversible_SemWait},
+                 {Action::TESTANY, &ReversibleRaceCalculator::is_race_reversible_TestAny},
+                 {Action::WAITANY, &ReversibleRaceCalculator::is_race_reversible_WaitAny}};
+
+  const auto e2_action = E.get_transition_for_handle(e2);
+  if (const auto handler = handlers.find(e2_action->type_); handler != handlers.end()) {
+    return handler->second(E, e1, e2_action);
+  } else {
+    xbt_die("There is currently no specialized computation for the transition "
+            "'%s' for computing reversible races in ODPOR, so the model checker cannot "
+            "determine how to proceed. Please submit a bug report requesting "
+            "that the transition be supported in SimGrid using ODPPR and consider "
+            "using the other model-checking algorithms supported by SimGrid instead "
+            "in the meantime",
+            e2_action->to_string().c_str());
+  }
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_ActorJoin(const Execution&, Execution::EventHandle e1,
+                                                            const Transition* e2)
+{
+  // ActorJoin races with another event iff its target `T` is the same as
+  // the actor executing the other transition. Clearly, then, we could not join
+  // on that actor `T` and then run a transition by `T`, so no race is reversible
+  return false;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_BarrierAsyncLock(const Execution&, Execution::EventHandle e1,
+                                                                   const Transition* e2)
+{
+  // BarrierAsyncLock is always enabled
+  return true;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_BarrierWait(const Execution& E, Execution::EventHandle e1,
+                                                              const Transition* e2)
+{
+  // If the other event is a barrier lock event, then we
+  // are not reversible; otherwise we are reversible.
+  const auto e1_action = E.get_transition_for_handle(e1)->type_;
+  return e1_action != Transition::Type::BARRIER_ASYNC_LOCK;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_CommRecv(const Execution&, Execution::EventHandle e1,
+                                                           const Transition* e2)
+{
+  // CommRecv is always enabled
+  return true;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_CommSend(const Execution&, Execution::EventHandle e1,
+                                                           const Transition* e2)
+{
+  // CommSend is always enabled
+  return true;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_CommWait(const Execution& E, Execution::EventHandle e1,
+                                                           const Transition* e2)
+{
+  // If the other event is a communication event, then we
+  // are not reversible; otherwise we are reversible.
+  const auto e1_action = E.get_transition_for_handle(e1)->type_;
+  return e1_action != Transition::Type::COMM_ASYNC_SEND and e1_action != Transition::Type::COMM_ASYNC_RECV;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_CommTest(const Execution&, Execution::EventHandle e1,
+                                                           const Transition* e2)
+{
+  // CommTest is always enabled
+  return true;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_MutexAsyncLock(const Execution&, Execution::EventHandle e1,
+                                                                 const Transition* e2)
+{
+  // MutexAsyncLock is always enabled
+  return true;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_MutexTest(const Execution&, Execution::EventHandle e1,
+                                                            const Transition* e2)
+{
+  // MutexTest is always enabled
+  return true;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_MutexTrylock(const Execution&, Execution::EventHandle e1,
+                                                               const Transition* e2)
+{
+  // MutexTrylock is always enabled
+  return true;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_MutexUnlock(const Execution&, Execution::EventHandle e1,
+                                                              const Transition* e2)
+{
+  // MutexUnlock is always enabled
+  return true;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_MutexWait(const Execution& E, Execution::EventHandle e1,
+                                                            const Transition* e2)
+{
+  // TODO: Get the semantics correct here
+  const auto e1_action = E.get_transition_for_handle(e1)->type_;
+  return e1_action != Transition::Type::MUTEX_ASYNC_LOCK and e1_action != Transition::Type::MUTEX_UNLOCK;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_SemAsyncLock(const Execution&, Execution::EventHandle e1,
+                                                               const Transition* e2)
+{
+  // SemAsyncLock is always enabled
+  return true;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_SemUnlock(const Execution&, Execution::EventHandle e1,
+                                                            const Transition* e2)
+{
+  // SemUnlock is always enabled
+  return true;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_SemWait(const Execution&, Execution::EventHandle e1,
+                                                          const Transition* e2)
+{
+  // TODO: Get the semantics correct here
+  return false;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_ObjectAccess(const Execution&, Execution::EventHandle e1,
+                                                               const Transition* e2)
+{
+  // Object access is always enabled
+  return true;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_Random(const Execution&, Execution::EventHandle e1,
+                                                         const Transition* e2)
+{
+  // Random is always enabled
+  return true;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_TestAny(const Execution&, Execution::EventHandle e1,
+                                                          const Transition* e2)
+{
+  // TestAny is always enabled
+  return true;
+}
+
+bool ReversibleRaceCalculator::is_race_reversible_WaitAny(const Execution&, Execution::EventHandle e1,
+                                                          const Transition* e2)
+{
+  // TODO: We need to check if any of the transitions
+  // waited on occurred before `e1`
+  return false;
+}
+
+} // namespace simgrid::mc::odpor
\ No newline at end of file
diff --git a/src/mc/explo/odpor/ReversibleRaceCalculator.hpp b/src/mc/explo/odpor/ReversibleRaceCalculator.hpp
new file mode 100644 (file)
index 0000000..5135bd2
--- /dev/null
@@ -0,0 +1,58 @@
+/* Copyright (c) 2007-2023. The SimGrid Team. All rights reserved.          */
+
+/* This program is free software; you can redistribute it and/or modify it
+ * under the terms of the license (GNU LGPL) which comes with this package. */
+
+#ifndef SIMGRID_MC_ODPOR_REVERSIBLE_RACE_CALCULATOR_HPP
+#define SIMGRID_MC_ODPOR_REVERSIBLE_RACE_CALCULATOR_HPP
+
+#include "src/mc/explo/odpor/Execution.hpp"
+#include "src/mc/explo/odpor/odpor_forward.hpp"
+#include "src/mc/transition/Transition.hpp"
+#include "src/mc/transition/TransitionActorJoin.hpp"
+#include "src/mc/transition/TransitionAny.hpp"
+#include "src/mc/transition/TransitionComm.hpp"
+#include "src/mc/transition/TransitionObjectAccess.hpp"
+#include "src/mc/transition/TransitionRandom.hpp"
+#include "src/mc/transition/TransitionSynchro.hpp"
+
+#include <memory>
+
+namespace simgrid::mc::odpor {
+
+/**
+ * @brief Computes whether a race between two events
+ * in a given execution is a reversible race.
+ *
+ * @note: All of the methods assume that there is
+ * indeed a race between the two events in the
+ * execution; indeed, the question the method answers
+ * is only sensible in the context of a race
+ */
+struct ReversibleRaceCalculator final {
+  static bool is_race_reversible_ActorJoin(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_BarrierAsyncLock(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_BarrierWait(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_CommRecv(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_CommSend(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_CommWait(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_CommTest(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_MutexAsyncLock(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_MutexTest(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_MutexTrylock(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_MutexUnlock(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_MutexWait(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_SemAsyncLock(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_SemUnlock(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_SemWait(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_ObjectAccess(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_Random(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_TestAny(const Execution&, Execution::EventHandle e1, const Transition* e2);
+  static bool is_race_reversible_WaitAny(const Execution&, Execution::EventHandle e1, const Transition* e2);
+
+public:
+  static bool is_race_reversible(const Execution&, Execution::EventHandle e1, Execution::EventHandle e2);
+};
+
+} // namespace simgrid::mc::odpor
+#endif
diff --git a/src/mc/explo/odpor/WakeupTree.cpp b/src/mc/explo/odpor/WakeupTree.cpp
new file mode 100644 (file)
index 0000000..7b73e6c
--- /dev/null
@@ -0,0 +1,219 @@
+/* Copyright (c) 2008-2023. The SimGrid Team. All rights reserved.          */
+
+/* This program is free software; you can redistribute it and/or modify it
+ * under the terms of the license (GNU LGPL) which comes with this package. */
+
+#include "src/mc/explo/odpor/WakeupTree.hpp"
+#include "src/mc/explo/odpor/Execution.hpp"
+#include "xbt/asserts.h"
+#include "xbt/string.hpp"
+
+#include <algorithm>
+#include <exception>
+#include <queue>
+
+namespace simgrid::mc::odpor {
+
+void WakeupTreeNode::add_child(WakeupTreeNode* node)
+{
+  this->children_.push_back(node);
+  node->parent_ = this;
+}
+
+PartialExecution WakeupTreeNode::get_sequence() const
+{
+  // TODO: Prevent having to compute this at the node level
+  // and instead track this with the iterator
+  PartialExecution seq_;
+  const WakeupTreeNode* cur_node = this;
+  while (cur_node != nullptr and !cur_node->is_root()) {
+    seq_.push_front(cur_node->action_);
+    cur_node = cur_node->parent_;
+  }
+  return seq_;
+}
+
+void WakeupTreeNode::detatch_from_parent()
+{
+  if (parent_ != nullptr) {
+    // TODO: There may be a better method
+    // of keeping track of a node's reference to
+    // its parent, perhaps keeping track
+    // of a std::list<>::iterator instead.
+    // This would allow us to detach a node
+    // in O(1) instead of O(|children|) time
+    parent_->children_.remove(this);
+  }
+}
+
+WakeupTree::WakeupTree() : WakeupTree(std::unique_ptr<WakeupTreeNode>(new WakeupTreeNode({}))) {}
+WakeupTree::WakeupTree(std::unique_ptr<WakeupTreeNode> root) : root_(root.get())
+{
+  this->insert_node(std::move(root));
+}
+
+std::vector<std::string> WakeupTree::get_single_process_texts() const
+{
+  std::vector<std::string> trace;
+  for (const auto* child : root_->children_) {
+    const auto t       = child->get_action();
+    const auto message = xbt::string_printf("Actor %ld: %s", t->aid_, t->to_string(true).c_str());
+    trace.push_back(std::move(message));
+  }
+  return trace;
+}
+
+std::optional<aid_t> WakeupTree::get_min_single_process_actor() const
+{
+  if (const auto node = get_min_single_process_node(); node.has_value()) {
+    return node.value()->get_actor();
+  }
+  return std::nullopt;
+}
+
+std::optional<WakeupTreeNode*> WakeupTree::get_min_single_process_node() const
+{
+  if (empty()) {
+    return std::nullopt;
+  }
+  // INVARIANT: The induced post-order relation always places children
+  // in order before the parent. The list of children maintained by
+  // each node represents that ordering, and the first child of
+  // the root is by definition the smallest of the single-process nodes
+  xbt_assert(not this->root_->children_.empty(), "What the");
+  return this->root_->children_.front();
+}
+
+WakeupTree WakeupTree::make_subtree_rooted_at(WakeupTreeNode* root)
+{
+  // Perform a BFS search to perform a deep copy of the portion
+  // of the tree underneath and including `root`. Note that `root`
+  // is contained within the context of a *different* wakeup tree;
+  // hence, we have to be careful to update each node's children
+  // appropriately
+  auto subtree = WakeupTree();
+
+  std::list<std::pair<WakeupTreeNode*, WakeupTreeNode*>> frontier{std::make_pair(root, subtree.root_)};
+  while (not frontier.empty()) {
+    auto [node_in_other_tree, subtree_equivalent] = frontier.front();
+    frontier.pop_front();
+
+    // For each child of the node corresponding to that in `subtree`,
+    // make clones of each of its children and add them to `frontier`
+    // to that their children are added, and so on.
+    for (WakeupTreeNode* child_in_other_tree : node_in_other_tree->get_ordered_children()) {
+      WakeupTreeNode* child_equivalent = subtree.make_node(child_in_other_tree->get_action());
+      subtree_equivalent->add_child(child_equivalent);
+      frontier.push_back(std::make_pair(child_in_other_tree, child_equivalent));
+    }
+  }
+  return subtree;
+}
+
+void WakeupTree::remove_subtree_rooted_at(WakeupTreeNode* root)
+{
+  if (not contains(root)) {
+    throw std::invalid_argument("Attempting to remove a subtree pivoted from a node "
+                                "that is not contained in this wakeup tree");
+  }
+
+  std::list<WakeupTreeNode*> subtree_contents{root};
+  std::list<WakeupTreeNode*> frontier{root};
+  while (not frontier.empty()) {
+    auto node = frontier.front();
+    frontier.pop_front();
+    for (const auto& child : node->get_ordered_children()) {
+      frontier.push_back(child);
+      subtree_contents.push_back(child);
+    }
+  }
+
+  // After having found each node with BFS, now we can
+  // remove them. This prevents the "joys" of iteration during mutation.
+  // We also remove the `root` from being referenced by its own parent (since
+  // it will soon be destroyed)
+  root->detatch_from_parent();
+  for (WakeupTreeNode* node_to_remove : subtree_contents) {
+    this->remove_node(node_to_remove);
+  }
+}
+
+void WakeupTree::remove_min_single_process_subtree()
+{
+  if (const auto node = get_min_single_process_node(); node.has_value()) {
+    remove_subtree_rooted_at(node.value());
+  }
+}
+
+bool WakeupTree::contains(WakeupTreeNode* node) const
+{
+  return std::find_if(this->nodes_.begin(), this->nodes_.end(), [=](const auto& pair) { return pair.first == node; }) !=
+         this->nodes_.end();
+}
+
+WakeupTreeNode* WakeupTree::make_node(std::shared_ptr<Transition> u)
+{
+  auto node                 = std::unique_ptr<WakeupTreeNode>(new WakeupTreeNode(std::move(u)));
+  auto* node_handle         = node.get();
+  this->nodes_[node_handle] = std::move(node);
+  return node_handle;
+}
+
+void WakeupTree::insert_node(std::unique_ptr<WakeupTreeNode> node)
+{
+  auto* node_handle         = node.get();
+  this->nodes_[node_handle] = std::move(node);
+}
+
+void WakeupTree::remove_node(WakeupTreeNode* node)
+{
+  this->nodes_.erase(node);
+}
+
+WakeupTree::InsertionResult WakeupTree::insert(const Execution& E, const PartialExecution& w)
+{
+  // See section 6.2 of Abdulla. et al.'s 2017 ODPOR paper for details
+
+  // Find the first node `v` in the tree such that
+  // `v ~_[E] w` and `v`  is not a leaf node
+  for (WakeupTreeNode* node : *this) {
+    if (const auto shortest_sequence = E.get_shortest_odpor_sq_subset_insertion(node->get_sequence(), w);
+        shortest_sequence.has_value()) {
+      // Insert the sequence as a child of `node`, but only
+      // if the node is not already a leaf
+      if (not node->is_leaf() or node == this->root_) {
+        // NOTE: It's entirely possible that the shortest
+        // sequence we are inserting is empty. Consider the
+        // following two cases:
+        //
+        // 1. `w` is itself empty. Evidently, insertion succeeds but nothing needs
+        // to happen
+        //
+        // 2. a leaf node in the tree already contains `w` exactly.
+        // In this case, the empty `w'` returned (viz. `shortest_seq`)
+        // such that `w [=_[E] v.w'` would be empty
+        this->insert_sequence_after(node, shortest_sequence.value());
+        return node == this->root_ ? InsertionResult::root : InsertionResult::interior_node;
+      }
+      // Since we're following the post-order traversal of the tree,
+      // the first such node we see is the smallest w.r.t "<"
+      return InsertionResult::leaf;
+    }
+  }
+  xbt_die("Insertion should always succeed with the root node (which contains no "
+          "prior execution). If we've reached this point, this implies either that "
+          "the wakeup tree traversal is broken or that computation of the shortest "
+          "sequence to insert into the tree is broken");
+}
+
+void WakeupTree::insert_sequence_after(WakeupTreeNode* node, const PartialExecution& w)
+{
+  WakeupTreeNode* cur_node = node;
+  for (const auto& w_i : w) {
+    WakeupTreeNode* new_node = this->make_node(w_i);
+    cur_node->add_child(new_node);
+    cur_node = new_node;
+  }
+}
+
+} // namespace simgrid::mc::odpor
\ No newline at end of file
diff --git a/src/mc/explo/odpor/WakeupTree.hpp b/src/mc/explo/odpor/WakeupTree.hpp
new file mode 100644 (file)
index 0000000..ea8cf29
--- /dev/null
@@ -0,0 +1,228 @@
+/* Copyright (c) 2007-2023. The SimGrid Team. All rights reserved.          */
+
+/* This program is free software; you can redistribute it and/or modify it
+ * under the terms of the license (GNU LGPL) which comes with this package. */
+
+#ifndef SIMGRID_MC_ODPOR_WAKEUP_TREE_HPP
+#define SIMGRID_MC_ODPOR_WAKEUP_TREE_HPP
+
+#include "src/mc/explo/odpor/WakeupTreeIterator.hpp"
+#include "src/mc/explo/odpor/odpor_forward.hpp"
+#include "src/mc/transition/Transition.hpp"
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+namespace simgrid::mc::odpor {
+
+/**
+ * @brief A single node in a wakeup tree
+ *
+ * Each node in a wakeup tree contains
+ */
+class WakeupTreeNode {
+private:
+  explicit WakeupTreeNode(std::shared_ptr<Transition> u) : action_(u) {}
+
+  WakeupTreeNode* parent_ = nullptr;
+
+  /** An ordered list of children of for this node in the tree */
+  std::list<WakeupTreeNode*> children_;
+
+  /** @brief The contents of the node */
+  std::shared_ptr<Transition> action_;
+
+  /** @brief Removes the node as a child from the parent */
+  void detatch_from_parent();
+
+  /** Allows the owning tree to insert directly into the child */
+  friend WakeupTree;
+  friend WakeupTreeIterator;
+
+public:
+  ~WakeupTreeNode()                                = default;
+  WakeupTreeNode(const WakeupTreeNode&)            = delete;
+  WakeupTreeNode(WakeupTreeNode&&)                 = default;
+  WakeupTreeNode& operator=(const WakeupTreeNode&) = delete;
+  WakeupTreeNode& operator=(WakeupTreeNode&&)      = default;
+
+  auto begin() const { return this->children_.begin(); }
+  auto end() const { return this->children_.end(); }
+  auto rbegin() const { return this->children_.rbegin(); }
+  auto rend() const { return this->children_.rend(); }
+
+  bool is_leaf() const { return children_.empty(); }
+  bool is_root() const { return parent_ == nullptr; }
+  aid_t get_actor() const { return action_->aid_; }
+  PartialExecution get_sequence() const;
+  std::shared_ptr<Transition> get_action() const { return action_; }
+  const std::list<WakeupTreeNode*>& get_ordered_children() const { return children_; }
+
+  /** Insert a node `node` as a new child of this node */
+  void add_child(WakeupTreeNode* node);
+};
+
+/**
+ * @brief The structure used by ODPOR to maintains paths of execution
+ * that should be followed in the future
+ *
+ * The wakeup tree data structure is formally defined in the Abdulla et al.
+ * 2017 ODPOR paper. Conceptually, the tree consists of nodes which are
+ * mapped to actions. Each node represents a partial extension of an execution,
+ * the complete extension being the transitions taken in sequence from
+ * the root of the tree to the node itself. Leaf nodes in the tree conceptually,
+ * then, represent paths that are guaranteed to explore different parts
+ * of the search space.
+ *
+ * Iteration over a wakeup tree occurs as a post-order traversal of its nodes
+ *
+ * @note A wakeup tree is defined relative to some execution `E`. The
+ * structure itself does not hold onto a reference of the execution with
+ * respect to which it is a wakeup tree.
+ *
+ * @todo: If the idea of execution "views"  is ever added -- viz. being able
+ * to share the contents of a single execution -- then a wakeup tree could
+ * contain a reference to such a view which would then be maintained by the
+ * manipulator of the tree
+ */
+class WakeupTree {
+private:
+  WakeupTreeNode* root_;
+
+  /**
+   * @brief All of the nodes that are currently are a part of the tree
+   *
+   * @invariant Each node event maps itself to the owner of that node,
+   * i.e. the unique pointer that manages the data at the address. The tree owns all
+   * of the addresses that are referenced by the nodes WakeupTreeNode.
+   * ODPOR guarantees that nodes are persisted as long as needed.
+   */
+  std::unordered_map<WakeupTreeNode*, std::unique_ptr<WakeupTreeNode>> nodes_;
+
+  void insert_node(std::unique_ptr<WakeupTreeNode> node);
+  void insert_sequence_after(WakeupTreeNode* node, const PartialExecution& w);
+  void remove_node(WakeupTreeNode* node);
+  bool contains(WakeupTreeNode* node) const;
+
+  /**
+   * @brief Removes the node `root` and all of its descendants from
+   * this wakeup tree
+   *
+   * @throws: If the node `root` is not contained in this tree, an
+   * exception is raised
+   */
+  void remove_subtree_rooted_at(WakeupTreeNode* root);
+
+  /**
+   * @brief Adds a new node to the tree, disconnected from
+   * any other, which represents the partial execution
+   * "fragment" `u`
+   */
+  WakeupTreeNode* make_node(std::shared_ptr<Transition> u);
+
+  /* Allow the iterator to access the contents of the tree */
+  friend WakeupTreeIterator;
+
+public:
+  WakeupTree();
+  explicit WakeupTree(std::unique_ptr<WakeupTreeNode> root);
+
+  /**
+   * @brief Creates a copy of the subtree whose root is the node
+   * `root` in this tree
+   */
+  static WakeupTree make_subtree_rooted_at(WakeupTreeNode* root);
+
+  auto begin() const { return WakeupTreeIterator(*this); }
+  auto end() const { return WakeupTreeIterator(); }
+
+  std::vector<std::string> get_single_process_texts() const;
+
+  /**
+   * @brief Remove the subtree of the smallest (with respect
+   * to the tree's "<" relation) single-process node.
+   *
+   * A "single-process" node is one whose execution represents
+   * taking a single action (i.e. those of the root node). The
+   * smallest under "<" is that which is continuously selected and
+   * removed by ODPOR.
+   *
+   * If the tree is empty, this method has no effect.
+   */
+  void remove_min_single_process_subtree();
+
+  /**
+   * @brief Whether or not this tree is considered empty
+   *
+   * @note Unlike other collection types, a wakeup tree is
+   * considered "empty" if it only contains the root node;
+   * that is, if it is "uninteresting". In such a case,
+   */
+  bool empty() const { return nodes_.size() == static_cast<size_t>(1); }
+
+  /**
+   * @brief Returns the number of *non-empty* entries in the tree, viz. the
+   * number of nodes in the tree that have an action mapped to them
+   */
+  size_t get_num_entries() const { return !empty() ? (nodes_.size() - 1) : static_cast<size_t>(0); }
+
+  /**
+   * @brief Returns the number of nodes in the tree, including the root node
+   */
+  size_t get_num_nodes() const { return nodes_.size(); }
+
+  /**
+   * @brief Gets the actor of the node that is the "smallest" (with respect
+   * to the tree's "<" relation) single-process node.
+   *
+   * If the tree is empty, returns std::nullopt
+   */
+  std::optional<aid_t> get_min_single_process_actor() const;
+
+  /**
+   * @brief Gets the node itself that is the "smallest" (with respect
+   * to the tree's "<" relation) single-process node.
+   *
+   * If the tree is empty, returns std::nullopt
+   */
+  std::optional<WakeupTreeNode*> get_min_single_process_node() const;
+
+  /** @brief Describes how a tree insertion was carried out */
+  enum class InsertionResult { leaf, interior_node, root };
+
+  /**
+   * @brief Inserts an sequence `seq` of processes into the tree
+   * such that that this tree is a wakeup tree relative to the
+   * given execution
+   *
+   * A key component of managing wakeup trees in ODPOR is
+   * determining what should be inserted into a wakeup tree.
+   * The procedure for implementing the insertion is outlined in section 6.2
+   * of Abdulla et al. 2017 as follows:
+   *
+   * | Let `v` be the smallest (w.r.t to "<") sequence in [the tree] B
+   * | such that `v ~_[E] w`. If `v` is a leaf node, the tree can be left
+   * | unmodified.
+   * |
+   * | Otherwise let `w'` be the shortest sequence such that `w [=_[E] v.w'`
+   * | and add `v.w'` as a new leaf, ordered after all already existing nodes
+   * | of the form `v.w''`
+   *
+   * This method performs the post-order search of part one and the insertion of
+   * `v.w'` of part two of the above procedure. Note that the execution will
+   * provide `v.w'` (see `Execution::get_shortest_odpor_sq_subset_insertion()`).
+   *
+   * @invariant: It is assumed that this tree is a wakeup tree
+   * with respect to the given execution `E`
+   *
+   * @return Whether a sequence equivalent to `seq` is already contained
+   * as a leaf node in the tree
+   */
+  InsertionResult insert(const Execution& E, const PartialExecution& seq);
+};
+
+} // namespace simgrid::mc::odpor
+#endif
diff --git a/src/mc/explo/odpor/WakeupTreeIterator.cpp b/src/mc/explo/odpor/WakeupTreeIterator.cpp
new file mode 100644 (file)
index 0000000..6c81203
--- /dev/null
@@ -0,0 +1,73 @@
+/* Copyright (c) 2008-2023. The SimGrid Team. All rights reserved.          */
+
+/* This program is free software; you can redistribute it and/or modify it
+ * under the terms of the license (GNU LGPL) which comes with this package. */
+
+#include "src/mc/explo/odpor/WakeupTreeIterator.hpp"
+#include "src/mc/explo/odpor/WakeupTree.hpp"
+
+namespace simgrid::mc::odpor {
+
+WakeupTreeIterator::WakeupTreeIterator(const WakeupTree& tree) : root_list{tree.root_}
+{
+  post_order_iteration.push(root_list.begin());
+  push_until_left_most_found();
+}
+
+void WakeupTreeIterator::push_until_left_most_found()
+{
+  // INVARIANT: Since we are traversing over a tree,
+  // there are no cycles. This means that at least
+  // one node in the tree won't have any children,
+  // so the loop will eventually terminate
+  auto* cur_top_node = *post_order_iteration.top();
+  while (not cur_top_node->is_leaf()) {
+    // INVARIANT: Since we push children in
+    // reverse order (right-most to left-most),
+    // we ensure that we'll always process left-most
+    // children first
+    auto& children = cur_top_node->children_;
+
+    for (auto iter = children.rbegin(); iter != children.rend(); ++iter) {
+      // iter.base() points one element past where we seek; hence,
+      // we move it over one position
+      post_order_iteration.push(std::prev(iter.base()));
+    }
+    cur_top_node = *post_order_iteration.top();
+  }
+}
+
+void WakeupTreeIterator::increment()
+{
+  // If there are no nodes in the stack, we've
+  // completed the traversal: there's nothing left
+  // to do
+  if (post_order_iteration.empty()) {
+    return;
+  }
+
+  auto prev_top_handle = post_order_iteration.top();
+  post_order_iteration.pop();
+
+  // If there are now no longer any nodes left,
+  // we know that `prev_top` must be the original
+  // root; that is, we were *just* pointing at the
+  // original root, so we're done
+  if (post_order_iteration.empty()) {
+    return;
+  }
+
+  // Otherwise, look at the next top node. If
+  // `prev_top` is that node's right-most child,
+  // then we don't attempt to re-add `next_top`'s
+  // children again for we would have already seen them.
+  // To actually determine "right-most", we check if
+  // moving over to the right one spot brings us to the
+  // end of the candidate parent's list
+  const auto* next_top_node = *post_order_iteration.top();
+  if ((++prev_top_handle) != next_top_node->get_ordered_children().end()) {
+    push_until_left_most_found();
+  }
+}
+
+} // namespace simgrid::mc::odpor
\ No newline at end of file
diff --git a/src/mc/explo/odpor/WakeupTreeIterator.hpp b/src/mc/explo/odpor/WakeupTreeIterator.hpp
new file mode 100644 (file)
index 0000000..32b968c
--- /dev/null
@@ -0,0 +1,71 @@
+/* Copyright (c) 2007-2023. The SimGrid Team. All rights reserved.          */
+
+/* This program is free software; you can redistribute it and/or modify it
+ * under the terms of the license (GNU LGPL) which comes with this package. */
+
+#ifndef SIMGRID_MC_ODPOR_WAKEUP_TREE_ITERATOR_HPP
+#define SIMGRID_MC_ODPOR_WAKEUP_TREE_ITERATOR_HPP
+
+#include "src/mc/explo/odpor/odpor_forward.hpp"
+
+#include <boost/iterator/iterator_facade.hpp>
+#include <list>
+#include <stack>
+
+namespace simgrid::mc::odpor {
+
+/**
+ * @brief A forward-iterator that performs a postorder traversal
+ * of the nodes of a WakeupTree
+ *
+ * Inserting a sequence `w` into a wakeup tree `B` with respect to
+ * some execution `E` requires determining the "<-minimal" node `N`
+ * with sequence `v` in the tree such that `v ~_[E] w`. The "<" relation
+ * over a wakeup tree orders its nodes by first recursively ordering all
+ * children of a node `N` followed by the node `N` itself, viz. a postorder.
+ * This iterator provides such a postorder traversal over the nodes in the
+ * wakeup tree.
+ */
+struct WakeupTreeIterator
+    : public boost::iterator_facade<WakeupTreeIterator, WakeupTreeNode*, boost::forward_traversal_tag> {
+public:
+  WakeupTreeIterator() = default;
+  explicit WakeupTreeIterator(const WakeupTree& tree);
+
+private:
+  using node_handle = std::list<WakeupTreeNode*>::iterator;
+
+  /**
+   *  @brief A list which is used to "store" the root node of the traversed
+   * wakeup tree
+   *
+   * The root node is, by definition, not the child of any other node. This
+   * means that the root node also is contained in any list into which the
+   * iterator can generate a pointer (iterator). This list takes the role
+   * of allowing the iterator to treat the root node like any other.
+   */
+  std::list<WakeupTreeNode*> root_list;
+
+  /**
+   * @brief The current "view" of the iteration in post-order traversal
+   */
+  std::stack<node_handle> post_order_iteration;
+
+  /**
+   * @brief Search the wakeup tree until a leaf node appears at the front
+   * of the iteration, pushing all children towards the top of the stack
+   * as the search progresses
+   */
+  void push_until_left_most_found();
+
+  // boost::iterator_facade<...> interface to implement
+  void increment();
+  bool equal(const WakeupTreeIterator& other) const { return post_order_iteration == other.post_order_iteration; }
+  WakeupTreeNode*& dereference() const { return *post_order_iteration.top(); }
+
+  // Allows boost::iterator_facade<...> to function properly
+  friend class boost::iterator_core_access;
+};
+
+} // namespace simgrid::mc::odpor
+#endif
diff --git a/src/mc/explo/odpor/WakeupTree_test.cpp b/src/mc/explo/odpor/WakeupTree_test.cpp
new file mode 100644 (file)
index 0000000..e0ef032
--- /dev/null
@@ -0,0 +1,469 @@
+/* Copyright (c) 2017-2023. The SimGrid Team. All rights reserved.               */
+
+/* This program is free software; you can redistribute it and/or modify it
+ * under the terms of the license (GNU LGPL) which comes with this package. */
+
+#include "src/3rd-party/catch.hpp"
+#include "src/mc/explo/odpor/Execution.hpp"
+#include "src/mc/explo/odpor/WakeupTree.hpp"
+#include "src/mc/explo/udpor/udpor_tests_private.hpp"
+#include "src/xbt/utils/iter/LazyPowerset.hpp"
+
+using namespace simgrid::mc;
+using namespace simgrid::mc::odpor;
+using namespace simgrid::mc::udpor;
+
+static void test_tree_iterator(const WakeupTree& tree, const std::vector<PartialExecution>& expected)
+{
+  uint64_t num_nodes_traversed = 0;
+  auto tree_iter               = tree.begin();
+  for (auto expected_iter = expected.begin(); expected_iter != expected.end();
+       ++expected_iter, ++tree_iter, ++num_nodes_traversed) {
+    REQUIRE(tree_iter != tree.end());
+    REQUIRE((*tree_iter)->get_sequence() == *expected_iter);
+  }
+  REQUIRE(num_nodes_traversed == tree.get_num_nodes());
+}
+
+static void test_tree_empty(const WakeupTree& tree)
+{
+  REQUIRE(tree.empty());
+  REQUIRE(tree.get_num_entries() == 0);
+  REQUIRE(tree.get_num_nodes() == 1);
+  REQUIRE_FALSE(tree.get_min_single_process_node().has_value());
+  REQUIRE_FALSE(tree.get_min_single_process_actor().has_value());
+  test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{}});
+}
+
+TEST_CASE("simgrid::mc::odpor::WakeupTree: Constructing Trees")
+{
+  SECTION("Constructing empty trees")
+  {
+    test_tree_empty(WakeupTree());
+  }
+
+  SECTION("Testing subtree creation and manipulation")
+  {
+    // Here, we make everything dependent. This will ensure that each unique sequence
+    // inserted into the tree never "eventually looks like"
+    const auto a0 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto a1 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto a2 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto a3 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 4);
+    const auto a4 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 5);
+    const auto a5 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 6);
+
+    Execution execution;
+    execution.push_transition(a0);
+    execution.push_transition(a1);
+    execution.push_transition(a2);
+    execution.push_transition(a3);
+    execution.push_transition(a4);
+    execution.push_transition(a5);
+
+    // The tree is as follows:
+    //                  {}
+    //               /     /
+    //             a1       a4
+    //           /    /       /
+    //          a2    a3       a1
+    //         /     /   /      /
+    //        a3    a2   a5     a2
+    //       /     /             /
+    //      a4    a4             a3
+    //
+    // Recall that new nodes (in this case the one with
+    // action `a2`) are added such that they are "greater than" (under
+    // the tree's `<` relation) all those that exist under the given parent
+    WakeupTree tree;
+    tree.insert(Execution(), {a1, a2, a3, a4});
+    tree.insert(Execution(), {a1, a3, a2, a4});
+    tree.insert(Execution(), {a1, a3, a2, a4, a5});
+    tree.insert(Execution(), {a1, a3, a5});
+    tree.insert(Execution(), {a4, a2, a1, a3});
+    REQUIRE(tree.get_num_nodes() == 13);
+    test_tree_iterator(tree, std::vector<PartialExecution>{
+                                 PartialExecution{a1, a2, a3, a4}, PartialExecution{a1, a2, a3},
+                                 PartialExecution{a1, a2}, PartialExecution{a1, a3, a2, a4},
+                                 PartialExecution{a1, a3, a2}, PartialExecution{a1, a3, a5}, PartialExecution{a1, a3},
+                                 PartialExecution{a1}, PartialExecution{a4, a2, a1, a3}, PartialExecution{a4, a2, a1},
+                                 PartialExecution{a4, a2}, PartialExecution{a4}, PartialExecution{}});
+
+    SECTION("Cloning a tree from the root produces the same tree")
+    {
+      // The root node is the last node
+      auto tree_root = tree.begin();
+      std::advance(tree_root, tree.get_num_nodes() - 1);
+
+      WakeupTree clone = WakeupTree::make_subtree_rooted_at(*tree_root);
+      REQUIRE(clone.empty() == tree.empty());
+      REQUIRE(clone.get_num_entries() == tree.get_num_entries());
+      REQUIRE(clone.get_num_nodes() == tree.get_num_nodes());
+
+      auto tree_iter = tree.begin();
+      for (auto clone_iter = clone.begin(); clone_iter != clone.end(); ++clone_iter, ++tree_iter) {
+        REQUIRE(tree_iter != tree.end());
+        REQUIRE((*tree_iter)->get_sequence() == (*clone_iter)->get_sequence());
+      }
+    }
+
+    SECTION("Cloning a subtree from a leaf gives an empty tree")
+    {
+      // Let's pick the first leaf
+      WakeupTree clone = WakeupTree::make_subtree_rooted_at(*tree.begin());
+      REQUIRE(clone.empty());
+      REQUIRE(clone.get_num_entries() == 0);
+      REQUIRE(clone.get_num_nodes() == 1);
+    }
+
+    SECTION("Cloning a subtree from an interior node gives the subtree underneath")
+    {
+      // Here, we pick the second-to-last node in the
+      // series, which is the right-most child of the root
+      auto right_most = tree.begin();
+      std::advance(right_most, tree.get_num_nodes() - 2);
+
+      WakeupTree clone = WakeupTree::make_subtree_rooted_at(*right_most);
+      REQUIRE_FALSE(clone.empty());
+      REQUIRE(clone.get_num_entries() == 3);
+      REQUIRE(clone.get_num_nodes() == 4);
+      // Importantly, note that action `a4` is not included in
+      // any of the executions; for in the subtree `clone` `a4`
+      // is now the root.
+      test_tree_iterator(clone, std::vector<PartialExecution>{PartialExecution{a2, a1, a3}, PartialExecution{a2, a1},
+                                                              PartialExecution{a2}, PartialExecution{}});
+    }
+
+    SECTION("Removing the first single-process subtree")
+    {
+      // Prior to removal, the first `a1` was the first single-process node
+      REQUIRE(tree.get_min_single_process_node().has_value());
+      REQUIRE(tree.get_min_single_process_actor().has_value());
+      REQUIRE(tree.get_min_single_process_actor().value() == a1->aid_);
+
+      tree.remove_min_single_process_subtree();
+
+      // Now the first `a4` is
+      REQUIRE(tree.get_min_single_process_node().has_value());
+      REQUIRE(tree.get_min_single_process_actor().has_value());
+      REQUIRE(tree.get_min_single_process_actor().value() == a4->aid_);
+
+      REQUIRE(tree.get_num_nodes() == 5);
+      test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{a4, a2, a1, a3},
+                                                             PartialExecution{a4, a2, a1}, PartialExecution{a4, a2},
+                                                             PartialExecution{a4}, PartialExecution{}});
+      tree.remove_min_single_process_subtree();
+
+      // At this point, we've removed each single-process subtree, so
+      // the tree should be empty
+      REQUIRE(tree.empty());
+    }
+
+    SECTION("Removing the first single-process subtree from an empty tree has no effect")
+    {
+      WakeupTree empty_tree;
+      test_tree_empty(empty_tree);
+
+      empty_tree.remove_min_single_process_subtree();
+
+      // There should be no effect: the tree should still be empty
+      // and the function should have no effect
+      test_tree_empty(empty_tree);
+    }
+  }
+}
+
+TEST_CASE("simgrid::mc::odpor::WakeupTree: Testing Insertion for Empty Executions")
+{
+  SECTION("Following an execution")
+  {
+    // We imagine the following completed execution `E`
+    // consisting of executing actions a0-a3. Execution
+    // `E` is the first such maximal execution explored
+    // by ODPOR, which implies that a) all sleep sets are
+    // empty and b) all wakeup trees (i.e. for each prefix) consist of the root
+    // node with a single leaf containing the action
+    // taken, save for the wakeup tree of the execution itself
+    // which is empty.
+
+    // We first notice that there's a reversible race between
+    // events 0 and 3.
+
+    const auto a0 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto a1 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 4);
+    const auto a2 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto a3 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 4);
+    const auto a4 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 2);
+
+    Execution execution;
+    execution.push_transition(a0);
+    execution.push_transition(a1);
+    execution.push_transition(a2);
+    execution.push_transition(a3);
+    execution.push_transition(a4);
+
+    REQUIRE(execution.get_racing_events_of(2) == std::unordered_set<Execution::EventHandle>{0});
+    REQUIRE(execution.get_racing_events_of(3) == std::unordered_set<Execution::EventHandle>{0});
+
+    WakeupTree tree;
+
+    SECTION("Attempting to insert the empty sequence into an empty tree should have no effect")
+    {
+      tree.insert(Execution(), {});
+      test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{}});
+    }
+
+    // First, we initialize the tree to how it looked prior
+    // to the insertion of the race.
+    tree.insert(Execution(), {a0});
+
+    // Then, after insertion, we ensure that the node was
+    // indeed added to the tree.
+    tree.insert(Execution(), {a1, a3});
+    test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{a0}, PartialExecution{a1, a3},
+                                                           PartialExecution{a1}, PartialExecution{}});
+
+    SECTION("Attempting to re-insert the same EXACT sequence should have no effect")
+    {
+      tree.insert(Execution(), {a1, a3});
+      test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{a0}, PartialExecution{a1, a3},
+                                                             PartialExecution{a1}, PartialExecution{}});
+    }
+
+    SECTION("Attempting to re-insert an equivalent sequence should have no effect")
+    {
+      // a3 and a1 are interchangeable since `a1` is independent with everything.
+      // Since we found an equivalent sequence that is a leaf, nothing should result..
+      tree.insert(Execution(), {a3, a1});
+      test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{a0}, PartialExecution{a1, a3},
+                                                             PartialExecution{a1}, PartialExecution{}});
+    }
+
+    SECTION("Attempting to insert the empty sequence should have no effect")
+    {
+      tree.insert(Execution(), {});
+      test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{a0}, PartialExecution{a1, a3},
+                                                             PartialExecution{a1}, PartialExecution{}});
+    }
+
+    SECTION("Inserting an extension should create a branch point")
+    {
+      // `a1.a2` shares the same `a1` prefix as `a1.a3`. Thus, the tree
+      // should now look as follows:
+      //
+      //                 {}
+      //               /    /
+      //             a0     a1
+      //                   /   /
+      //                  a3   a4
+      //
+      // Recall that new nodes (in this case the one with
+      // action `a2`) are added such that they are "greater than" (under
+      // the tree's `<` relation) all those that exist under the given parent.
+      tree.insert(Execution(), {a1, a4});
+      test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{a0}, PartialExecution{a1, a3},
+                                                             PartialExecution{a1, a4}, PartialExecution{a1},
+                                                             PartialExecution{}});
+    }
+
+    SECTION("Inserting an equivalent sequence to a leaf should preserve the tree as-is")
+    {
+      // `a1.a2` is equivalent to `a1.a3` since `a2` and `a3` are independent
+      // (`E ⊢ p ◊ w` where `p := proc(a2)` and `w := a3`). Thus, the tree
+      // should now STILL look as follows:
+      //
+      //                 {}
+      //               /    /
+      //             a0     a1
+      //                   /
+      //                  a3
+      //
+      // Recall that new nodes (in this case the one with
+      // action `a2`) are added such that they are "greater than" (under
+      // the tree's `<` relation) all those that exist under the given parent.
+      tree.insert(Execution(), {a1, a3});
+      test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{a0}, PartialExecution{a1, a3},
+                                                             PartialExecution{a1}, PartialExecution{}});
+    }
+  }
+
+  SECTION("Performing Arbitrary Insertions")
+  {
+    const auto a0 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto a1 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 4);
+    const auto a2 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto a3 = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto a4 = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto a5 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 4);
+    WakeupTree tree;
+
+    SECTION("Attempting to insert the empty sequence into an empty tree should have no effect")
+    {
+      tree.insert(Execution(), {});
+      test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{}});
+    }
+
+    SECTION("Attempting to re-insert the same sequence multiple times should have no extra effect")
+    {
+      tree.insert(Execution(), {a4});
+      tree.insert(Execution(), {a4});
+      tree.insert(Execution(), {a4});
+      REQUIRE(tree.get_num_nodes() == 2);
+      test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{a4}, PartialExecution{}});
+    }
+
+    SECTION("Attempting to insert an independent sequence same should have no extra effect")
+    {
+      // a4 and a1 are independent actions. Intuitively, then, we need only
+      // search one ordering of the two actions. The wakeup tree handles
+      // this by computing the `~` relation. The relation itself determines
+      // whether the `a1` is an initial of `a3`, which it is not. It then
+      // checks whether `a1` is independent with everything in the sequence
+      // (in this case, consisting only of `a1`) which IS true. Since `a4`
+      // is already a leaf node of the tree, it suffices to only have `a4`
+      // in this tree to guide ODPOR.
+      tree.insert(Execution(), {a4});
+      tree.insert(Execution(), {a1});
+      REQUIRE(tree.get_num_nodes() == 2);
+      test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{a4}, PartialExecution{}});
+    }
+
+    SECTION(
+        "Attempting to insert a progression of executions should have no extra effect when the first process is a leaf")
+    {
+      // All progressions starting with `a0` are effectively already accounted
+      // for by inserting `a0` since we `a0` "can always be made to look like"
+      // (viz. the `~` relation) `a0.*` where `*` is some sequence of actions
+      tree.insert(Execution(), {a0});
+      tree.insert(Execution(), {a0, a3});
+      tree.insert(Execution(), {a0, a3, a2});
+      tree.insert(Execution(), {a0, a3, a2, a4});
+      tree.insert(Execution(), {a0, a3, a2, a4});
+      REQUIRE(tree.get_num_nodes() == 2);
+      test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{a0}, PartialExecution{}});
+    }
+
+    SECTION("Stress test with multiple branch points: `~_E` with different looking sequences")
+    {
+      // After the insertions below, the tree looks like the following:
+      //                {}
+      //              /    /
+      //            a0     a2
+      //                 /  |   /
+      //               a0  a3   a5
+      tree.insert(Execution(), {a0});
+      tree.insert(Execution(), {a2, a0});
+      tree.insert(Execution(), {a2, a3});
+      tree.insert(Execution(), {a2, a5});
+      REQUIRE(tree.get_num_nodes() == 6);
+      test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{a0}, PartialExecution{a2, a0},
+                                                             PartialExecution{a2, a3}, PartialExecution{a2, a5},
+                                                             PartialExecution{a2}, PartialExecution{}});
+      SECTION("Adding more stress")
+      {
+        // In this case, `a2` and `a1` can be interchanged with each other.
+        // Thus `a2.a1 == a1.a2`. Since there is already an interior node
+        // containing `a2`, we attempt to add the what remains (viz. `a1`) to the
+        // series. HOWEVER: we notice that `a2.a5` is "eventually equivalent to"
+        // (that is `~` with) `a1.a2` since `a2` is an initial of the latter and
+        // `a1` and `a5` are independent of each other.
+        tree.insert(Execution(), {a1, a2});
+        REQUIRE(tree.get_num_nodes() == 6);
+        test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{a0}, PartialExecution{a2, a0},
+                                                               PartialExecution{a2, a3}, PartialExecution{a2, a5},
+                                                               PartialExecution{a2}, PartialExecution{}});
+
+        // With a3.a0, we notice that starting a sequence with `a3` is
+        // always different than starting one with either `a0` or
+        //
+        // After the insertion, the tree looks like the following:
+        //                     {}
+        //              /     /        /
+        //            a0     a2        a3
+        //                 /  |  /     |
+        //               a0  a3  a5    a0
+        tree.insert(Execution(), {a3, a0});
+        REQUIRE(tree.get_num_nodes() == 8);
+        test_tree_iterator(tree, std::vector<PartialExecution>{PartialExecution{a0}, PartialExecution{a2, a0},
+                                                               PartialExecution{a2, a3}, PartialExecution{a2, a5},
+                                                               PartialExecution{a2}, PartialExecution{a3, a0},
+                                                               PartialExecution{a3}, PartialExecution{}});
+      }
+    }
+  }
+
+  SECTION("Insertion with more subtle equivalents")
+  {
+    const auto cd_1 = std::make_shared<ConditionallyDependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto i_2  = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 2);
+    const auto i_3  = std::make_shared<IndependentAction>(Transition::Type::UNKNOWN, 3);
+    const auto d_1  = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 1);
+    const auto d_2  = std::make_shared<DependentAction>(Transition::Type::UNKNOWN, 2);
+    WakeupTree complex_tree;
+    // After the insertions below, the tree looks like the following:
+    //                              {}
+    //                           /     /
+    //                       cd_1      i_2
+    //                       /            /
+    //                    i_2              d_2
+    //                   /                  /
+    //                 d_1                 cd_1
+    //                 /                 /      /
+    //               i_3               d_1      i_3
+    //              /                  /          /
+    //            d_2                i_3          d_2
+    //            /                  /              /
+    //          d_2                d_2              d_1
+    //
+    // d_1.i_3.d_2 is equivalent to i_3.d_2.d_1
+    complex_tree.insert(Execution(), {cd_1, i_2, d_1, i_3, d_2, d_2});
+    complex_tree.insert(Execution(), {i_2, d_2, cd_1, d_1, i_3, d_2});
+    complex_tree.insert(Execution(), {i_2, d_2, cd_1, i_3, d_2, d_1});
+    REQUIRE(complex_tree.get_num_nodes() == 16);
+    test_tree_iterator(complex_tree, std::vector<PartialExecution>{{cd_1, i_2, d_1, i_3, d_2, d_2},
+                                                                   {cd_1, i_2, d_1, i_3, d_2},
+                                                                   {cd_1, i_2, d_1, i_3},
+                                                                   {cd_1, i_2, d_1},
+                                                                   {cd_1, i_2},
+                                                                   {cd_1},
+                                                                   {i_2, d_2, cd_1, d_1, i_3, d_2},
+                                                                   {i_2, d_2, cd_1, d_1, i_3},
+                                                                   {i_2, d_2, cd_1, d_1},
+                                                                   {i_2, d_2, cd_1, i_3, d_2, d_1},
+                                                                   {i_2, d_2, cd_1, i_3, d_2},
+                                                                   {i_2, d_2, cd_1, i_3},
+                                                                   {i_2, d_2, cd_1},
+                                                                   {i_2, d_2},
+                                                                   {i_2},
+                                                                   {}});
+    // Here we note that the sequence that we are attempting to insert, viz.
+    //
+    //    i_3.i_2.d_2.cd_1.d_2.d_1
+    //
+    // is already equivalent to
+    //
+    //    i_2.d_2.cd_1.i_3.d_2.d_1
+    complex_tree.insert(Execution(), {i_3, i_2, d_2, cd_1, d_2, d_1});
+    REQUIRE(complex_tree.get_num_nodes() == 16);
+
+    // Here we note that the sequence that we are attempting to insert, viz.
+    //
+    //    i_2.d_2.cd_1.d_1.i_3
+    //
+    // is already equivalent to
+    //
+    //    i_2.d_2.cd_1.i_3.d_2.d_1
+    complex_tree.insert(Execution(), {i_2, d_2, cd_1, d_1, i_3});
+    REQUIRE(complex_tree.get_num_nodes() == 16);
+
+    // Here we note that the sequence that we are attempting to insert, viz.
+    //
+    //    i_2.d_2.cd_1
+    //
+    // is accounted for by an interior node of the tree. Since there is no
+    // "extra" portions that are different from what is already
+    // contained in the tree, nothing is added and the tree stays the same
+    complex_tree.insert(Execution(), {i_2, d_2, cd_1});
+    REQUIRE(complex_tree.get_num_nodes() == 16);
+  }
+}
\ No newline at end of file
diff --git a/src/mc/explo/odpor/odpor_forward.hpp b/src/mc/explo/odpor/odpor_forward.hpp
new file mode 100644 (file)
index 0000000..52388bc
--- /dev/null
@@ -0,0 +1,43 @@
+/* Copyright (c) 2007-2023. The SimGrid Team. All rights reserved.          */
+
+/* This program is free software; you can redistribute it and/or modify it
+ * under the terms of the license (GNU LGPL) which comes with this package. */
+
+/** @file odpor_forward.hpp
+ *
+ *  Forward definitions for MC types specific to ODPOR
+ */
+
+#ifndef SIMGRID_MC_ODPOR_FORWARD_HPP
+#define SIMGRID_MC_ODPOR_FORWARD_HPP
+
+#include "src/mc/mc_forward.hpp"
+#include <list>
+#include <memory>
+#include <simgrid/forward.h>
+
+namespace simgrid::mc::odpor {
+
+using PartialExecution = std::list<std::shared_ptr<Transition>>;
+
+class Event;
+class Execution;
+class ReversibleRaceCalculator;
+class WakeupTree;
+class WakeupTreeNode;
+class WakeupTreeIterator;
+
+} // namespace simgrid::mc::odpor
+
+namespace simgrid::mc {
+
+// Permit ODPOR or SDPOR to be used as namespaces
+// Many of the structures overlap, so it doesn't
+// make sense to some in one and not the other.
+// Having one for each algorithm makes the corresponding
+// code easier to read
+namespace sdpor = simgrid::mc::odpor;
+
+} // namespace simgrid::mc
+
+#endif
diff --git a/src/mc/explo/odpor/odpor_tests_private.hpp b/src/mc/explo/odpor/odpor_tests_private.hpp
new file mode 100644 (file)
index 0000000..0a29f7d
--- /dev/null
@@ -0,0 +1,52 @@
+/* Copyright (c) 2007-2023. The SimGrid Team. All rights reserved.          */
+
+/* This program is free software; you can redistribute it and/or modify it
+ * under the terms of the license (GNU LGPL) which comes with this package. */
+
+/** @file odpor_tests_private.hpp
+ *
+ * A private header file for all ODPOR tests
+ */
+
+#ifndef SIMGRID_MC_ODPOR_TEST_PRIVATE_HPP
+#define SIMGRID_MC_ODPOR_TEST_PRIVATE_HPP
+
+#include "src/mc/explo/udpor/udpor_tests_private.hpp"
+#include "src/mc/transition/Transition.hpp"
+
+namespace simgrid::mc::odpor {
+
+struct DependentIfSameValueAction : public Transition {
+private:
+  const int value;
+
+public:
+  DependentIfSameValueAction() = default;
+  DependentIfSameValueAction(Type type, aid_t issuer, int value, int times_considered = 0)
+      : Transition(type, issuer, times_considered), value(value)
+  {
+  }
+  DependentIfSameValueAction(aid_t issuer, int value, int times_considered = 0)
+      : Transition(simgrid::mc::Transition::Type::UNKNOWN, issuer, times_considered), value(value)
+  {
+  }
+
+  // Dependent only with DependentAction (i.e. not itself)
+  bool depends(const Transition* other) const override
+  {
+    if (aid_ == other->aid_) {
+      return true;
+    }
+
+    if (const auto* same_value = dynamic_cast<const DependentIfSameValueAction*>(other); same_value != nullptr) {
+      return value == same_value->value;
+    }
+
+    // `DependentAction` is dependent with everyone who's not the `IndependentAction`
+    return dynamic_cast<const simgrid::mc::udpor::DependentAction*>(other) != nullptr;
+  }
+};
+
+} // namespace simgrid::mc::odpor
+
+#endif
index 4e51cdd..513c997 100644 (file)
@@ -37,14 +37,15 @@ int main(int argc, char** argv)
 
 #if SIMGRID_HAVE_STATEFUL_MC
   if (_sg_mc_comms_determinism || _sg_mc_send_determinism)
-    explo = std::unique_ptr<Exploration>(create_communication_determinism_checker(argv_copy, cfg_use_DPOR()));
+    explo = std::unique_ptr<Exploration>(
+        create_communication_determinism_checker(argv_copy, get_model_checking_reduction()));
   else if (_sg_mc_unfolding_checker)
     explo = std::unique_ptr<Exploration>(create_udpor_checker(argv_copy));
   else if (not _sg_mc_property_file.get().empty())
     explo = std::unique_ptr<Exploration>(create_liveness_checker(argv_copy));
   else
 #endif
-    explo = std::unique_ptr<Exploration>(create_dfs_exploration(argv_copy, cfg_use_DPOR()));
+    explo = std::unique_ptr<Exploration>(create_dfs_exploration(argv_copy, get_model_checking_reduction()));
 
   ExitStatus status;
   try {
index 069d1ce..1d9375f 100644 (file)
@@ -57,8 +57,9 @@ int _sg_mc_max_visited_states = 0;
 static simgrid::config::Flag<std::string> cfg_mc_reduction{
     "model-check/reduction", "Specify the kind of exploration reduction (either none or DPOR)", "dpor",
     [](std::string_view value) {
-      if (value != "none" && value != "dpor")
-        xbt_die("configuration option 'model-check/reduction' can only take 'none' or 'dpor' as a value");
+      if (value != "none" && value != "dpor" && value != "sdpor" && value != "odpor")
+        xbt_die("configuration option 'model-check/reduction' must be one of the following: "
+                " 'none', 'dpor', 'sdpor', or 'odpor'");
     }};
 
 simgrid::config::Flag<bool> _sg_mc_sleep_set{
@@ -135,11 +136,29 @@ simgrid::config::Flag<bool> _sg_mc_termination{
     "model-check/termination", "Whether to enable non progressive cycle detection", false,
     [](bool) { _mc_cfg_cb_check("value to enable/disable the detection of non progressive cycles"); }};
 
-bool simgrid::mc::cfg_use_DPOR()
+simgrid::mc::ReductionMode simgrid::mc::get_model_checking_reduction()
 {
-  if (cfg_mc_reduction.get() == "dpor" && _sg_mc_max_visited_states__ > 0) {
+  if ((cfg_mc_reduction.get() == "dpor" || cfg_mc_reduction.get() == "sdpor" || cfg_mc_reduction.get() == "odpor") &&
+      _sg_mc_max_visited_states__ > 0) {
     XBT_INFO("Disabling DPOR since state-equality reduction is activated with 'model-check/visited'");
-    return false;
+    return simgrid::mc::ReductionMode::none;
+  }
+
+  if (cfg_mc_reduction.get() == "none") {
+    return ReductionMode::none;
+  } else if (cfg_mc_reduction.get() == "dpor") {
+    return ReductionMode::dpor;
+  } else if (cfg_mc_reduction.get() == "sdpor") {
+    return ReductionMode::sdpor;
+  } else if (cfg_mc_reduction.get() == "odpor") {
+    return ReductionMode::odpor;
+  } else if (cfg_mc_reduction.get() == "udpor") {
+    XBT_INFO("No reduction will be used: "
+             "UDPOR has a dedicated invocation 'model-check/unfolding-checker' "
+             "but is not yet fully supported in SimGrid");
+    return ReductionMode::none;
+  } else {
+    XBT_INFO("Unknown reduction mode: defaulting to no reduction");
+    return ReductionMode::none;
   }
-  return cfg_mc_reduction.get() == "dpor";
 }
index 64acccc..b544580 100644 (file)
 
 /********************************** Configuration of MC **************************************/
 namespace simgrid::mc {
-bool cfg_use_DPOR(); // "model-check/reduction" == "DPOR"
+XBT_DECLARE_ENUM_CLASS(ReductionMode, none, dpor, sdpor, odpor);
 XBT_DECLARE_ENUM_CLASS(ModelCheckingMode, NONE, APP_SIDE, CHECKER_SIDE, REPLAY);
+ReductionMode get_model_checking_reduction(); // "model-check/reduction" == "DPOR"
 XBT_PUBLIC ModelCheckingMode get_model_checking_mode();
 XBT_PUBLIC void set_model_checking_mode(ModelCheckingMode mode);
-};
+}; // namespace simgrid::mc
 
 extern XBT_PUBLIC simgrid::config::Flag<std::string> _sg_mc_buffering;
 extern XBT_PRIVATE simgrid::config::Flag<int> _sg_mc_checkpoint;
index c6e1533..463bfcb 100644 (file)
@@ -27,7 +27,17 @@ std::string ActorJoinTransition::to_string(bool verbose) const
 }
 bool ActorJoinTransition::depends(const Transition* other) const
 {
-  // Joining is indep with any other transitions:
+  // Joining is dependent with any transition whose
+  // actor is that of the `other` action. , Join i
+  if (other->aid_ == target_) {
+    return true;
+  }
+
+  // Actions executed by the same actor are always dependent
+  if (other->aid_ == aid_)
+    return true;
+
+  // Otherwise, joining is indep with any other transitions:
   // - It is only enabled once the target ends, and after this point it's enabled no matter what
   // - Other joins don't affect it, and it does not impact on the enabledness of any other transition
   return false;
index 4813a00..f532369 100644 (file)
@@ -35,6 +35,10 @@ std::string TestAnyTransition::to_string(bool verbose) const
 }
 bool TestAnyTransition::depends(const Transition* other) const
 {
+  // Actions executed by the same actor are always dependent
+  if (other->aid_ == aid_)
+    return true;
+
   return transitions_[times_considered_]->depends(other);
 }
 WaitAnyTransition::WaitAnyTransition(aid_t issuer, int times_considered, std::stringstream& stream)
@@ -57,6 +61,9 @@ std::string WaitAnyTransition::to_string(bool verbose) const
 }
 bool WaitAnyTransition::depends(const Transition* other) const
 {
+  // Actions executed by the same actor are always dependent
+  if (other->aid_ == aid_)
+    return true;
   return transitions_[times_considered_]->depends(other);
 }
 
index b374f19..029548d 100644 (file)
@@ -42,6 +42,10 @@ bool CommWaitTransition::depends(const Transition* other) const
   if (other->type_ < type_)
     return other->depends(this);
 
+  // Actions executed by the same actor are always dependent
+  if (other->aid_ == aid_)
+    return true;
+
   if (const auto* wait = dynamic_cast<const CommWaitTransition*>(other)) {
     if (timeout_ || wait->timeout_)
       return true; // Timeouts are not considered by the independence theorem, thus assumed dependent
@@ -80,6 +84,10 @@ bool CommTestTransition::depends(const Transition* other) const
   if (other->type_ < type_)
     return other->depends(this);
 
+  // Actions executed by the same actor are always dependent
+  if (other->aid_ == aid_)
+    return true;
+
   if (dynamic_cast<const CommTestTransition*>(other) != nullptr)
     return false; // Test & Test are independent
 
@@ -112,6 +120,10 @@ bool CommRecvTransition::depends(const Transition* other) const
   if (other->type_ < type_)
     return other->depends(this);
 
+  // Actions executed by the same actor are always dependent
+  if (other->aid_ == aid_)
+    return true;
+
   if (const auto* recv = dynamic_cast<const CommRecvTransition*>(other))
     return mbox_ == recv->mbox_;
 
@@ -164,6 +176,10 @@ bool CommSendTransition::depends(const Transition* other) const
   if (other->type_ < type_)
     return other->depends(this);
 
+  // Actions executed by the same actor are always dependent
+  if (other->aid_ == aid_)
+    return true;
+
   if (const auto* other_isend = dynamic_cast<const CommSendTransition*>(other))
     return mbox_ == other_isend->mbox_;
 
index 63a7d92..0936359 100644 (file)
@@ -34,6 +34,13 @@ std::string ObjectAccessTransition::to_string(bool verbose) const
 }
 bool ObjectAccessTransition::depends(const Transition* o) const
 {
+  if (o->type_ < type_)
+    return o->depends(this);
+
+  // Actions executed by the same actor are always dependent
+  if (o->aid_ == aid_)
+    return true;
+
   if (const auto* other = dynamic_cast<const ObjectAccessTransition*>(o))
     return objaddr_ == other->objaddr_; // dependent only if it's an access to the same object
   return false;
index ba00821..374d69d 100644 (file)
@@ -17,7 +17,13 @@ class RandomTransition : public Transition {
 public:
   std::string to_string(bool verbose) const override;
   RandomTransition(aid_t issuer, int times_considered, std::stringstream& stream);
-  bool depends(const Transition* other) const override { return aid_ == other->aid_; } // Independent with any other transition
+  bool depends(const Transition* other) const override
+  {
+    if (other->type_ < type_)
+      return other->depends(this);
+
+    return aid_ == other->aid_;
+  } // Independent with any other transition
 };
 
 } // namespace simgrid::mc
index a93e27b..dbd39a5 100644 (file)
@@ -63,6 +63,10 @@ bool MutexTransition::depends(const Transition* o) const
   if (o->type_ < type_)
     return o->depends(this);
 
+  // Actions executed by the same actor are always dependent
+  if (o->aid_ == aid_)
+    return true;
+
   // type_ <= other->type_ in  MUTEX_LOCK, MUTEX_TEST, MUTEX_TRYLOCK, MUTEX_UNLOCK, MUTEX_WAIT,
 
   if (auto* other = dynamic_cast<const MutexTransition*>(o)) {
index 96f475d..5377503 100644 (file)
@@ -403,4 +403,246 @@ $ $VALGRIND_NO_LEAK_CHECK ${bindir:=.}/../../../smpi_script/bin/smpirun -wrapper
 > If this is too much, consider sharing allocations for computation buffers.
 > This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
 > 
-> [0.000000] [mc_dfs/INFO] DFS exploration ended. 635 unique states visited; 173 backtracks (3896 transition replays, 3088 states visited overall)
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed MPI handles:
+> [0.000000] [smpi_utils/WARNING] To get more information (location of allocations), compile your code with -trace-call-location flag of smpicc/f90
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Comm
+> [0.000000] [smpi_utils/INFO] 4 leaked handles of type MPI_Group
+> [0.000000] [smpi_utils/INFO] Probable memory leaks in your code: SMPI detected 8 unfreed buffers:
+> [0.000000] [smpi_utils/INFO] leaked allocations of total size 152, called 8 times, with minimum size 16 and maximum size 28
+> [0.000000] [smpi_utils/INFO] Memory Usage: Simulated application allocated 152 bytes during its lifetime through malloc/calloc calls.
+> Largest allocation at once from a single process was 28 bytes, at coll-allreduce-with-leaks.c:28. It was called 1 times during the whole simulation.
+> If this is too much, consider sharing allocations for computation buffers.
+> This can be done automatically by setting --cfg=smpi/auto-shared-malloc-thresh to the minimum size wanted size (this can alter execution if data content is necessary)
+> 
+> [0.000000] [mc_dfs/INFO] DFS exploration ended. 1005 unique states visited; 276 backtracks (6560 transition replays, 5279 states visited overall)
index d6d2a5b..fc80edc 100644 (file)
@@ -524,6 +524,8 @@ set(MC_SRC_BASE
   
 set(MC_SRC_STATELESS
   src/mc/api/ActorState.hpp
+  src/mc/api/ClockVector.cpp
+  src/mc/api/ClockVector.hpp
   src/mc/api/State.cpp
   src/mc/api/State.hpp
   src/mc/api/RemoteApp.cpp
@@ -534,6 +536,17 @@ set(MC_SRC_STATELESS
   src/mc/explo/Exploration.cpp
   src/mc/explo/Exploration.hpp
 
+  src/mc/explo/odpor/Execution.cpp
+  src/mc/explo/odpor/Execution.hpp
+  src/mc/explo/odpor/ReversibleRaceCalculator.cpp
+  src/mc/explo/odpor/ReversibleRaceCalculator.hpp
+  src/mc/explo/odpor/WakeupTree.cpp
+  src/mc/explo/odpor/WakeupTree.hpp
+  src/mc/explo/odpor/WakeupTreeIterator.cpp
+  src/mc/explo/odpor/WakeupTreeIterator.hpp
+  src/mc/explo/odpor/odpor_forward.hpp
+  src/mc/explo/odpor/odpor_tests_private.hpp
+
   src/mc/remote/AppSide.cpp
   src/mc/remote/AppSide.hpp
   src/mc/remote/Channel.cpp
index aab3aab..09491e4 100644 (file)
@@ -132,6 +132,9 @@ set(UNIT_TESTS  src/xbt/unit-tests_main.cpp
 
 set(MC_UNIT_TESTS src/mc/sosp/Snapshot_test.cpp 
                   src/mc/sosp/PageStore_test.cpp
+                  src/mc/explo/odpor/ClockVector_test.cpp
+                  src/mc/explo/odpor/Execution_test.cpp
+                  src/mc/explo/odpor/WakeupTree_test.cpp
                   src/mc/explo/udpor/EventSet_test.cpp
                   src/mc/explo/udpor/Unfolding_test.cpp
                   src/mc/explo/udpor/UnfoldingEvent_test.cpp