X-Git-Url: http://plrg.eecs.uci.edu/git/?p=c11tester.git;a=blobdiff_plain;f=model.cc;h=97f235c463ce2915fb136865e3bd7a3fc0aaebbb;hp=9e611fae96367283e33d7b927224eb259b52a27e;hb=8b2d7b35e955c675bbfd1f415d25b01494cc330f;hpb=3b3533a76db06884f44b3e70ccdbad647275fcc4 diff --git a/model.cc b/model.cc index 9e611fae..97f235c4 100644 --- a/model.cc +++ b/model.cc @@ -39,7 +39,6 @@ struct bug_message { */ struct model_snapshot_members { model_snapshot_members() : - current_action(NULL), /* First thread created will have id INITIAL_THREAD_ID */ next_thread_id(INITIAL_THREAD_ID), used_sequence_numbers(0), @@ -48,6 +47,7 @@ struct model_snapshot_members { stats(), failed_promise(false), too_many_reads(false), + no_valid_reads(false), bad_synchronization(false), asserted(false) { } @@ -58,7 +58,6 @@ struct model_snapshot_members { bugs.clear(); } - ModelAction *current_action; unsigned int next_thread_id; modelclock_t used_sequence_numbers; ModelAction *next_backtrack; @@ -66,6 +65,7 @@ struct model_snapshot_members { struct execution_stats stats; bool failed_promise; bool too_many_reads; + bool no_valid_reads; /** @brief Incorrectly-ordered synchronization was made */ bool bad_synchronization; bool asserted; @@ -216,7 +216,7 @@ Thread * ModelChecker::get_next_thread(ModelAction *curr) if (curr != NULL) { /* Do not split atomic actions. */ if (curr->is_rmwr()) - return thread_current(); + return get_thread(curr); else if (curr->get_type() == THREAD_CREATE) return curr->get_thread_operand(); } @@ -293,12 +293,8 @@ void ModelChecker::execute_sleep_set() for (unsigned int i = 0; i < get_num_threads(); i++) { thread_id_t tid = int_to_id(i); Thread *thr = get_thread(tid); - if (scheduler->is_sleep_set(thr) && thr->get_pending() == NULL) { - thr->set_state(THREAD_RUNNING); - scheduler->next_thread(thr); - Thread::swap(&system_context, thr); - priv->current_action->set_sleep_flag(); - thr->set_pending(priv->current_action); + if (scheduler->is_sleep_set(thr) && thr->get_pending()) { + thr->get_pending()->set_sleep_flag(); } } } @@ -729,16 +725,19 @@ bool ModelChecker::process_read(ModelAction *curr, bool second_part_of_rmw) read_from(curr, reads_from); mo_graph->commitChanges(); - mo_check_promises(curr->get_tid(), reads_from, NULL); + mo_check_promises(curr, true); updated |= r_status; } else if (!second_part_of_rmw) { /* Read from future value */ struct future_value fv = curr->get_node()->get_future_value(); + Promise *promise = new Promise(curr, fv); value = fv.value; - curr->set_value(fv.value); - curr->set_read_from(NULL); - promises->push_back(new Promise(curr, fv)); + curr->set_read_from_promise(promise); + promises->push_back(promise); + mo_graph->startChanges(); + updated = r_modification_order(curr, promise); + mo_graph->commitChanges(); } get_thread(curr)->set_return_value(value); return updated; @@ -892,7 +891,7 @@ bool ModelChecker::process_write(ModelAction *curr) } mo_graph->commitChanges(); - mo_check_promises(curr->get_tid(), curr, NULL); + mo_check_promises(curr, false); get_thread(curr)->set_return_value(VALUE_NONE); return updated_mod_order || updated_promises; @@ -965,7 +964,10 @@ bool ModelChecker::process_thread_action(ModelAction *curr) switch (curr->get_type()) { case THREAD_CREATE: { - Thread *th = curr->get_thread_operand(); + thrd_t *thrd = (thrd_t *)curr->get_location(); + struct thread_params *params = (struct thread_params *)curr->get_value(); + Thread *th = new Thread(thrd, params->func, params->arg); + add_thread(th); th->set_creation(curr); /* Promises can be satisfied by children */ for (unsigned int i = 0; i < promises->size(); i++) { @@ -1208,16 +1210,6 @@ bool ModelChecker::check_action_enabled(ModelAction *curr) { return true; } -/** - * Stores the ModelAction for the current thread action. Call this - * immediately before switching from user- to system-context to pass - * data between them. - * @param act The ModelAction created by the user-thread action - */ -void ModelChecker::set_current_action(ModelAction *act) { - priv->current_action = act; -} - /** * This is the heart of the model checker routine. It performs model-checking * actions corresponding to a given "current action." Among other processes, it @@ -1304,8 +1296,14 @@ ModelAction * ModelChecker::check_current_action(ModelAction *curr) if (act->is_read()) { const ModelAction *rf = act->get_reads_from(); - if (rf != NULL && r_modification_order(act, rf)) - updated = true; + const Promise *promise = act->get_reads_from_promise(); + if (rf) { + if (r_modification_order(act, rf)) + updated = true; + } else if (promise) { + if (r_modification_order(act, promise)) + updated = true; + } } if (act->is_write()) { if (w_modification_order(act)) @@ -1372,14 +1370,14 @@ void ModelChecker::print_infeasibility(const char *prefix) const { char buf[100]; char *ptr = buf; - if (mo_graph->checkForRMWViolation()) - ptr += sprintf(ptr, "[RMW atomicity]"); if (mo_graph->checkForCycles()) ptr += sprintf(ptr, "[mo cycle]"); if (priv->failed_promise) ptr += sprintf(ptr, "[failed promise]"); if (priv->too_many_reads) ptr += sprintf(ptr, "[too many reads]"); + if (priv->no_valid_reads) + ptr += sprintf(ptr, "[no valid reads-from]"); if (priv->bad_synchronization) ptr += sprintf(ptr, "[bad sw ordering]"); if (promises_expired()) @@ -1407,8 +1405,8 @@ bool ModelChecker::is_feasible_prefix_ignore_relseq() const */ bool ModelChecker::is_infeasible() const { - return mo_graph->checkForRMWViolation() || - mo_graph->checkForCycles() || + return mo_graph->checkForCycles() || + priv->no_valid_reads || priv->failed_promise || priv->too_many_reads || priv->bad_synchronization || @@ -1419,8 +1417,11 @@ bool ModelChecker::is_infeasible() const ModelAction * ModelChecker::process_rmw(ModelAction *act) { ModelAction *lastread = get_last_action(act->get_tid()); lastread->process_rmw(act); - if (act->is_rmw() && lastread->get_reads_from() != NULL) { - mo_graph->addRMWEdge(lastread->get_reads_from(), lastread); + if (act->is_rmw()) { + if (lastread->get_reads_from()) + mo_graph->addRMWEdge(lastread->get_reads_from(), lastread); + else + mo_graph->addRMWEdge(lastread->get_reads_from_promise(), lastread); mo_graph->commitChanges(); } return lastread; @@ -1485,13 +1486,9 @@ void ModelChecker::check_recency(ModelAction *curr, const ModelAction *rf) continue; /* Test to see whether this is a feasible write to read from */ - mo_graph->startChanges(); - r_modification_order(curr, write); - bool feasiblereadfrom = !is_infeasible(); - mo_graph->rollbackChanges(); + /** NOTE: all members of read-from set should be + * feasible, so we no longer check it here **/ - if (!feasiblereadfrom) - continue; rit = ritcopy; bool feasiblewrite = true; @@ -1524,7 +1521,7 @@ void ModelChecker::check_recency(ModelAction *curr, const ModelAction *rf) * read. * * Basic idea is the following: Go through each other thread and find - * the lastest action that happened before our read. Two cases: + * the last action that happened before our read. Two cases: * * (1) The action is a write => that write must either occur before * the write we read from or be the write we read from. @@ -1533,10 +1530,11 @@ void ModelChecker::check_recency(ModelAction *curr, const ModelAction *rf) * must occur before the write we read from or be the same write. * * @param curr The current action. Must be a read. - * @param rf The action that curr reads from. Must be a write. + * @param rf The ModelAction or Promise that curr reads from. Must be a write. * @return True if modification order edges were added; false otherwise */ -bool ModelChecker::r_modification_order(ModelAction *curr, const ModelAction *rf) +template +bool ModelChecker::r_modification_order(ModelAction *curr, const rf_type *rf) { std::vector *thrd_lists = get_safe_ptr_vect_action(obj_thrd_map, curr->get_location()); unsigned int i; @@ -1564,26 +1562,23 @@ bool ModelChecker::r_modification_order(ModelAction *curr, const ModelAction *rf for (rit = list->rbegin(); rit != list->rend(); rit++) { ModelAction *act = *rit; - if (act->is_write() && act != rf && act != curr) { + if (act->is_write() && !act->equals(rf) && act != curr) { /* C++, Section 29.3 statement 5 */ if (curr->is_seqcst() && last_sc_fence_thread_local && *act < *last_sc_fence_thread_local) { - mo_graph->addEdge(act, rf); - added = true; + added = mo_graph->addEdge(act, rf) || added; break; } /* C++, Section 29.3 statement 4 */ else if (act->is_seqcst() && last_sc_fence_local && *act < *last_sc_fence_local) { - mo_graph->addEdge(act, rf); - added = true; + added = mo_graph->addEdge(act, rf) || added; break; } /* C++, Section 29.3 statement 6 */ else if (last_sc_fence_thread_before && *act < *last_sc_fence_thread_before) { - mo_graph->addEdge(act, rf); - added = true; + added = mo_graph->addEdge(act, rf) || added; break; } } @@ -1594,9 +1589,8 @@ bool ModelChecker::r_modification_order(ModelAction *curr, const ModelAction *rf */ if (act->happens_before(curr) && act != curr) { if (act->is_write()) { - if (rf != act) { - mo_graph->addEdge(act, rf); - added = true; + if (!act->equals(rf)) { + added = mo_graph->addEdge(act, rf) || added; } } else { const ModelAction *prevreadfrom = act->get_reads_from(); @@ -1604,9 +1598,8 @@ bool ModelChecker::r_modification_order(ModelAction *curr, const ModelAction *rf if (prevreadfrom == NULL) continue; - if (rf != prevreadfrom) { - mo_graph->addEdge(prevreadfrom, rf); - added = true; + if (!prevreadfrom->equals(rf)) { + added = mo_graph->addEdge(prevreadfrom, rf) || added; } } break; @@ -1614,71 +1607,15 @@ bool ModelChecker::r_modification_order(ModelAction *curr, const ModelAction *rf } } - return added; -} - -/** This method fixes up the modification order when we resolve a - * promises. The basic problem is that actions that occur after the - * read curr could not property add items to the modification order - * for our read. - * - * So for each thread, we find the earliest item that happens after - * the read curr. This is the item we have to fix up with additional - * constraints. If that action is write, we add a MO edge between - * the Action rf and that action. If the action is a read, we add a - * MO edge between the Action rf, and whatever the read accessed. - * - * @param curr is the read ModelAction that we are fixing up MO edges for. - * @param rf is the write ModelAction that curr reads from. - * - */ -void ModelChecker::post_r_modification_order(ModelAction *curr, const ModelAction *rf) -{ - std::vector *thrd_lists = get_safe_ptr_vect_action(obj_thrd_map, curr->get_location()); - unsigned int i; - ASSERT(curr->is_read()); - - /* Iterate over all threads */ - for (i = 0; i < thrd_lists->size(); i++) { - /* Iterate over actions in thread, starting from most recent */ - action_list_t *list = &(*thrd_lists)[i]; - action_list_t::reverse_iterator rit; - ModelAction *lastact = NULL; - - /* Find last action that happens after curr that is either not curr or a rmw */ - for (rit = list->rbegin(); rit != list->rend(); rit++) { - ModelAction *act = *rit; - if (curr->happens_before(act) && (curr != act || curr->is_rmw())) { - lastact = act; - } else - break; - } + /* + * All compatible, thread-exclusive promises must be ordered after any + * concrete loads from the same thread + */ + for (unsigned int i = 0; i < promises->size(); i++) + if ((*promises)[i]->is_compatible_exclusive(curr)) + added = mo_graph->addEdge(rf, (*promises)[i]) || added; - /* Include at most one act per-thread that "happens before" curr */ - if (lastact != NULL) { - if (lastact == curr) { - //Case 1: The resolved read is a RMW, and we need to make sure - //that the write portion of the RMW mod order after rf - - mo_graph->addEdge(rf, lastact); - } else if (lastact->is_read()) { - //Case 2: The resolved read is a normal read and the next - //operation is a read, and we need to make sure the value read - //is mod ordered after rf - - const ModelAction *postreadfrom = lastact->get_reads_from(); - if (postreadfrom != NULL && rf != postreadfrom) - mo_graph->addEdge(rf, postreadfrom); - } else { - //Case 3: The resolved read is a normal read and the next - //operation is a write, and we need to make sure that the - //write is mod ordered after rf - if (lastact != rf) - mo_graph->addEdge(rf, lastact); - } - break; - } - } + return added; } /** @@ -1715,8 +1652,7 @@ bool ModelChecker::w_modification_order(ModelAction *curr) so we are initialized. */ ModelAction *last_seq_cst = get_last_seq_cst_write(curr); if (last_seq_cst != NULL) { - mo_graph->addEdge(last_seq_cst, curr); - added = true; + added = mo_graph->addEdge(last_seq_cst, curr) || added; } } @@ -1759,8 +1695,7 @@ bool ModelChecker::w_modification_order(ModelAction *curr) /* C++, Section 29.3 statement 7 */ if (last_sc_fence_thread_before && act->is_write() && *act < *last_sc_fence_thread_before) { - mo_graph->addEdge(act, curr); - added = true; + added = mo_graph->addEdge(act, curr) || added; break; } @@ -1776,14 +1711,13 @@ bool ModelChecker::w_modification_order(ModelAction *curr) * readfrom(act) --mo--> act */ if (act->is_write()) - mo_graph->addEdge(act, curr); + added = mo_graph->addEdge(act, curr) || added; else if (act->is_read()) { //if previous read accessed a null, just keep going if (act->get_reads_from() == NULL) continue; - mo_graph->addEdge(act->get_reads_from(), curr); + added = mo_graph->addEdge(act->get_reads_from(), curr) || added; } - added = true; break; } else if (act->is_read() && !act->could_synchronize_with(curr) && !act->same_thread(curr)) { @@ -1809,6 +1743,15 @@ bool ModelChecker::w_modification_order(ModelAction *curr) } } + /* + * All compatible, thread-exclusive promises must be ordered after any + * concrete stores to the same thread, or else they can be merged with + * this store later + */ + for (unsigned int i = 0; i < promises->size(); i++) + if ((*promises)[i]->is_compatible_exclusive(curr)) + added = mo_graph->addEdge(curr, (*promises)[i]) || added; + return added; } @@ -2323,44 +2266,44 @@ ClockVector * ModelChecker::get_cv(thread_id_t tid) const */ bool ModelChecker::resolve_promises(ModelAction *write) { - bool resolved = false; + bool haveResolved = false; std::vector< ModelAction *, ModelAlloc > actions_to_check; + promise_list_t mustResolve, resolved; for (unsigned int i = 0, promise_index = 0; promise_index < promises->size(); i++) { Promise *promise = (*promises)[promise_index]; if (write->get_node()->get_promise(i)) { ModelAction *read = promise->get_action(); - if (read->is_rmw()) { - mo_graph->addRMWEdge(write, read); - } read_from(read, write); - //First fix up the modification order for actions that happened - //before the read - r_modification_order(read, write); - //Next fix up the modification order for actions that happened - //after the read. - post_r_modification_order(read, write); //Make sure the promise's value matches the write's value - ASSERT(promise->get_value() == write->get_value()); - delete(promise); + ASSERT(promise->is_compatible(write)); + mo_graph->resolvePromise(read, write, &mustResolve); + resolved.push_back(promise); promises->erase(promises->begin() + promise_index); actions_to_check.push_back(read); - resolved = true; + haveResolved = true; } else promise_index++; } + for (unsigned int i = 0; i < mustResolve.size(); i++) { + if (std::find(resolved.begin(), resolved.end(), mustResolve[i]) + == resolved.end()) + priv->failed_promise = true; + } + for (unsigned int i = 0; i < resolved.size(); i++) + delete resolved[i]; //Check whether reading these writes has made threads unable to //resolve promises for (unsigned int i = 0; i < actions_to_check.size(); i++) { - ModelAction *read=actions_to_check[i]; - mo_check_promises(read->get_tid(), write, read); + ModelAction *read = actions_to_check[i]; + mo_check_promises(read, true); } - return resolved; + return haveResolved; } /** @@ -2417,71 +2360,39 @@ void ModelChecker::check_promises_thread_disabled() * @brief Checks promises in response to addition to modification order for * threads. * - * Definitions: - * - * pthread is the thread that performed the read that created the promise - * - * pread is the read that created the promise + * We test whether threads are still available for satisfying promises after an + * addition to our modification order constraints. Those that are unavailable + * are "eliminated". Once all threads are eliminated from satisfying a promise, + * that promise has failed. * - * pwrite is either the first write to same location as pread by - * pthread that is sequenced after pread or the write read by the - * first read to the same location as pread by pthread that is - * sequenced after pread. - * - * 1. If tid=pthread, then we check what other threads are reachable - * through the mod order starting with pwrite. Those threads cannot - * perform a write that will resolve the promise due to modification - * order constraints. - * - * 2. If the tid is not pthread, we check whether pwrite can reach the - * action write through the modification order. If so, that thread - * cannot perform a future write that will resolve the promise due to - * modificatin order constraints. - * - * @param tid The thread that either read from the model action write, or - * actually did the model action write. - * - * @param write The ModelAction representing the relevant write. - * @param read The ModelAction that reads a promised write, or NULL otherwise. + * @param act The ModelAction which updated the modification order + * @param is_read_check Should be true if act is a read and we must check for + * updates to the store from which it read (there is a distinction here for + * RMW's, which are both a load and a store) */ -void ModelChecker::mo_check_promises(thread_id_t tid, const ModelAction *write, const ModelAction *read) +void ModelChecker::mo_check_promises(const ModelAction *act, bool is_read_check) { - void *location = write->get_location(); + const ModelAction *write = is_read_check ? act->get_reads_from() : act; + for (unsigned int i = 0; i < promises->size(); i++) { Promise *promise = (*promises)[i]; - const ModelAction *act = promise->get_action(); + const ModelAction *pread = promise->get_action(); // Is this promise on the same location? - if (act->get_location() != location) + if (!pread->same_var(write)) continue; - // same thread as the promise - if (act->get_tid() == tid) { - // make sure that the reader of this write happens after the promise - if ((read == NULL) || (promise->get_action()->happens_before(read))) { - // do we have a pwrite for the promise, if not, set it - if (promise->get_write() == NULL) { - promise->set_write(write); - // The pwrite cannot happen before the promise - if (write->happens_before(act) && (write != act)) { - priv->failed_promise = true; - return; - } - } - - if (mo_graph->checkPromise(write, promise)) { - priv->failed_promise = true; - return; - } - } + if (pread->happens_before(act) && mo_graph->checkPromise(write, promise)) { + priv->failed_promise = true; + return; } // Don't do any lookups twice for the same thread - if (!promise->thread_is_available(tid)) + if (!promise->thread_is_available(act->get_tid())) continue; - if (promise->get_write() && mo_graph->checkReachable(promise->get_write(), write)) { - if (promise->eliminate_thread(tid)) { + if (mo_graph->checkReachable(promise, write)) { + if (mo_graph->checkPromise(write, promise)) { priv->failed_promise = true; return; } @@ -2550,14 +2461,25 @@ void ModelChecker::build_reads_from_past(ModelAction *curr) else if (curr->get_sleep_flag() && !curr->is_seqcst() && !sleep_can_read_from(curr, act)) allow_read = false; - if (allow_read) - curr->get_node()->add_read_from(act); + if (allow_read) { + /* Only add feasible reads */ + mo_graph->startChanges(); + r_modification_order(curr, act); + if (!is_infeasible()) + curr->get_node()->add_read_from(act); + mo_graph->rollbackChanges(); + } /* Include at most one act per-thread that "happens before" curr */ if (act->happens_before(curr)) break; } } + /* We may find no valid may-read-from only if the execution is doomed */ + if (!curr->get_node()->get_read_from_size()) { + priv->no_valid_reads = true; + set_assert(); + } if (DBG_ENABLED()) { model_print("Reached read action:\n"); @@ -2630,7 +2552,7 @@ void ModelChecker::dumpGraph(char *filename) const for (action_list_t::iterator it = action_trace->begin(); it != action_trace->end(); it++) { ModelAction *action = *it; if (action->is_read()) { - fprintf(file, "N%u [label=\"%u, T%u\"];\n", action->get_seq_number(), action->get_seq_number(), action->get_tid()); + fprintf(file, "N%u [label=\"N%u, T%u\"];\n", action->get_seq_number(), action->get_seq_number(), action->get_tid()); if (action->get_reads_from() != NULL) fprintf(file, "N%u -> N%u[label=\"rf\", color=red];\n", action->get_seq_number(), action->get_reads_from()->get_seq_number()); } @@ -2650,7 +2572,6 @@ void ModelChecker::dumpGraph(char *filename) const void ModelChecker::print_summary() const { #if SUPPORT_MOD_ORDER_DUMP - scheduler->print(); char buffername[100]; sprintf(buffername, "exec%04u", stats.num_total); mo_graph->dumpGraphToFile(buffername); @@ -2742,8 +2663,7 @@ uint64_t ModelChecker::switch_to_master(ModelAction *act) { DBG(); Thread *old = thread_current(); - set_current_action(act); - old->set_state(THREAD_READY); + old->set_pending(act); if (Thread::swap(old, &system_context) < 0) { perror("swap threads"); exit(EXIT_FAILURE); @@ -2754,13 +2674,11 @@ uint64_t ModelChecker::switch_to_master(ModelAction *act) /** * Takes the next step in the execution, if possible. * @param curr The current step to take - * @return Returns true (success) if a step was taken and false otherwise. + * @return Returns the next Thread to run, if any; NULL if this execution + * should terminate */ -bool ModelChecker::take_step(ModelAction *curr) +Thread * ModelChecker::take_step(ModelAction *curr) { - if (has_asserted()) - return false; - Thread *curr_thrd = get_thread(curr); ASSERT(curr_thrd->get_state() == THREAD_READY); @@ -2768,15 +2686,14 @@ bool ModelChecker::take_step(ModelAction *curr) /* Infeasible -> don't take any more steps */ if (is_infeasible()) - return false; + return NULL; else if (isfeasibleprefix() && have_bug_reports()) { set_assert(); - return false; + return NULL; } - if (params.bound != 0) - if (priv->used_sequence_numbers > params.bound) - return false; + if (params.bound != 0 && priv->used_sequence_numbers > params.bound) + return NULL; if (curr_thrd->is_blocked() || curr_thrd->is_complete()) scheduler->remove_thread(curr_thrd); @@ -2803,26 +2720,15 @@ bool ModelChecker::take_step(ModelAction *curr) ModelAction *fixup = new ModelAction(MODEL_FIXUP_RELSEQ, std::memory_order_seq_cst, NULL, VALUE_NONE, model_thread); - set_current_action(fixup); - return true; + model_thread->set_pending(fixup); + return model_thread; } /* next_thrd == NULL -> don't take any more steps */ if (!next_thrd) - return false; - - next_thrd->set_state(THREAD_RUNNING); - - if (next_thrd->get_pending() != NULL) { - /* restart a pending action */ - set_current_action(next_thrd->get_pending()); - next_thrd->set_pending(NULL); - next_thrd->set_state(THREAD_READY); - return true; - } + return NULL; - /* Return false only if swap fails with an error */ - return (Thread::swap(&system_context, next_thrd) == 0); + return next_thrd; } /** Wrapper to run the user's main function, with appropriate arguments */ @@ -2837,15 +2743,29 @@ void ModelChecker::run() do { thrd_t user_thread; Thread *t = new Thread(&user_thread, &user_main_wrapper, NULL); - add_thread(t); - /* Run user thread up to its first action */ - scheduler->next_thread(t); - Thread::swap(&system_context, t); + do { + for (unsigned int i = 0; i < get_num_threads(); i++) { + thread_id_t tid = int_to_id(i); + Thread *thr = get_thread(tid); + if (!thr->is_model_thread() && !thr->is_complete() && !thr->get_pending()) { + scheduler->next_thread(thr); + Thread::swap(&system_context, thr); + } + } + + /* Catch assertions from prior take_step or from + * between-ModelAction bugs (e.g., data races) */ + if (has_asserted()) + break; - /* Wait for all threads to complete */ - while (take_step(priv->current_action)); + /* Consume the next action for a Thread */ + ModelAction *curr = t->get_pending(); + t->set_pending(NULL); + t = take_step(curr); + } while (t && !t->is_model_thread()); + /** @TODO Re-write release sequence fixups here */ } while (next_execution()); print_stats();