// -*- coding: utf-8 -*-
// Copyright (C) 2012, 2013, 2014, 2015 Laboratoire de Recherche et
// Développement de l'Epita (LRDE).
//
// This file is part of Spot, a model checking library.
//
// Spot is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// Spot is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
// License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
#include "common_sys.hh"
#include
#include
#include
#include
#include
#include
#include
#include
#include "error.h"
#include "argmatch.h"
#include "common_setup.hh"
#include "common_cout.hh"
#include "common_conv.hh"
#include "common_trans.hh"
#include "common_file.hh"
#include "common_finput.hh"
#include "dstarparse/public.hh"
#include "hoaparse/public.hh"
#include "ltlast/unop.hh"
#include "ltlvisit/tostring.hh"
#include "ltlvisit/apcollect.hh"
#include "ltlvisit/mutation.hh"
#include "ltlvisit/relabel.hh"
#include "ltlvisit/lbt.hh"
#include "tgbaalgos/lbtt.hh"
#include "tgbaalgos/product.hh"
#include "tgbaalgos/remfin.hh"
#include "tgbaalgos/gtec/gtec.hh"
#include "tgbaalgos/randomgraph.hh"
#include "tgbaalgos/sccinfo.hh"
#include "tgbaalgos/isweakscc.hh"
#include "tgbaalgos/reducerun.hh"
#include "tgbaalgos/word.hh"
#include "tgbaalgos/dtgbacomp.hh"
#include "tgbaalgos/cleanacc.hh"
#include "misc/formater.hh"
#include "tgbaalgos/stats.hh"
#include "tgbaalgos/isdet.hh"
#include "misc/escape.hh"
#include "misc/hash.hh"
#include "misc/random.hh"
#include "misc/tmpfile.hh"
#include "misc/timer.hh"
const char argp_program_doc[] ="\
Call several LTL/PSL translators and cross-compare their output to detect \
bugs, or to gather statistics. The list of formulas to use should be \
supplied on standard input, or using the -f or -F options.\v\
Exit status:\n\
0 everything went fine (timeouts are OK too)\n\
1 some translator failed to output something we understand, or failed\n\
sanity checks (statistics were output nonetheless)\n\
2 ltlcross aborted on error\n\
";
enum {
OPT_BOGUS = 256,
OPT_COLOR,
OPT_CSV,
OPT_DENSITY,
OPT_DUPS,
OPT_GRIND,
OPT_IGNORE_EXEC_FAIL,
OPT_JSON,
OPT_NOCHECKS,
OPT_NOCOMP,
OPT_OMIT,
OPT_PRODUCTS,
OPT_SEED,
OPT_STATES,
OPT_STOP_ERR,
OPT_VERBOSE,
};
static const argp_option options[] =
{
/**************************************************/
{ 0, 0, 0, 0, "ltlcross behavior:", 5 },
{ "allow-dups", OPT_DUPS, 0, 0,
"translate duplicate formulas in input", 0 },
{ "no-checks", OPT_NOCHECKS, 0, 0,
"do not perform any sanity checks (negated formulas "
"will not be translated)", 0 },
{ "no-complement", OPT_NOCOMP, 0, 0,
"do not complement deterministic automata to perform extra checks", 0 },
{ "stop-on-error", OPT_STOP_ERR, 0, 0,
"stop on first execution error or failure to pass"
" sanity checks (timeouts are OK)", 0 },
{ "ignore-execution-failures", OPT_IGNORE_EXEC_FAIL, 0, 0,
"ignore automata from translators that return with a non-zero exit code,"
" but do not flag this as an error", 0 },
/**************************************************/
{ 0, 0, 0, 0, "State-space generation:", 6 },
{ "states", OPT_STATES, "INT", 0,
"number of the states in the state-spaces (200 by default)", 0 },
{ "density", OPT_DENSITY, "FLOAT", 0,
"probability, between 0.0 and 1.0, to add a transition between "
"two states (0.1 by default)", 0 },
{ "seed", OPT_SEED, "INT", 0,
"seed for the random number generator (0 by default)", 0 },
{ "products", OPT_PRODUCTS, "[+]INT", 0,
"number of products to perform (1 by default), statistics will be "
"averaged unless the number is prefixed with '+'", 0 },
/**************************************************/
{ 0, 0, 0, 0, "Statistics output:", 7 },
{ "json", OPT_JSON, "[>>]FILENAME", OPTION_ARG_OPTIONAL,
"output statistics as JSON in FILENAME or on standard output", 0 },
{ "csv", OPT_CSV, "[>>]FILENAME", OPTION_ARG_OPTIONAL,
"output statistics as CSV in FILENAME or on standard output "
"(if '>>' is used to request append mode, the header line is "
"not output)", 0 },
{ "omit-missing", OPT_OMIT, 0, 0,
"do not output statistics for timeouts or failed translations", 0 },
/**************************************************/
{ 0, 0, 0, 0, "Miscellaneous options:", -2 },
{ "color", OPT_COLOR, "WHEN", OPTION_ARG_OPTIONAL,
"colorize output; WHEN can be 'never', 'always' (the default if "
"--color is used without argument), or "
"'auto' (the default if --color is not used)", 0 },
{ "grind", OPT_GRIND, "[>>]FILENAME", 0,
"for each formula for which a problem was detected, write a simpler " \
"formula that fails on the same test in FILENAME", 0 },
{ "save-bogus", OPT_BOGUS, "[>>]FILENAME", 0,
"save formulas for which problems were detected in FILENAME", 0 },
{ "verbose", OPT_VERBOSE, 0, 0,
"print what is being done, for debugging", 0 },
{ 0, 0, 0, 0, "If an output FILENAME is prefixed with '>>', is it open "
"in append mode instead of being truncated.", -1 },
{ 0, 0, 0, 0, 0, 0 }
};
const struct argp_child children[] =
{
{ &finput_argp, 0, 0, 1 },
{ &trans_argp, 0, 0, 0 },
{ &misc_argp, 0, 0, -2 },
{ 0, 0, 0, 0 }
};
enum color_type { color_never, color_always, color_if_tty };
static char const *const color_args[] =
{
"always", "yes", "force",
"never", "no", "none",
"auto", "tty", "if-tty", 0
};
static color_type const color_types[] =
{
color_always, color_always, color_always,
color_never, color_never, color_never,
color_if_tty, color_if_tty, color_if_tty
};
ARGMATCH_VERIFY(color_args, color_types);
static color_type color_opt = color_if_tty;
static const char* bright_red = "\033[01;31m";
static const char* bright_blue = "\033[01;34m";
static const char* bright_yellow = "\033[01;33m";
static const char* reset_color = "\033[m";
static unsigned states = 200;
static float density = 0.1;
static const char* json_output = 0;
static const char* csv_output = 0;
static bool want_stats = false;
static bool allow_dups = false;
static bool no_checks = false;
static bool no_complement = false;
static bool stop_on_error = false;
static int seed = 0;
static unsigned products = 1;
static bool products_avg = true;
static bool opt_omit = false;
static bool has_sr = false; // Has Streett or Rabin automata to process.
static const char* bogus_output_filename = 0;
static output_file* bogus_output = 0;
static output_file* grind_output = 0;
static bool verbose = false;
static bool ignore_exec_fail = false;
static unsigned ignored_exec_fail = 0;
static bool global_error_flag = false;
static std::ostream&
global_error()
{
global_error_flag = true;
if (color_opt)
std::cerr << bright_red;
return std::cerr;
}
static std::ostream&
example()
{
if (color_opt)
std::cerr << bright_yellow;
return std::cerr;
}
static void
end_error()
{
if (color_opt)
std::cerr << reset_color;
}
struct statistics
{
statistics()
: ok(false),
has_in(false),
status_str(0),
status_code(0),
time(0),
in_type(0),
in_states(0),
in_edges(0),
in_transitions(0),
in_acc(0),
in_scc(0),
states(0),
edges(0),
transitions(0),
acc(0),
scc(0),
nonacc_scc(0),
terminal_scc(0),
weak_scc(0),
strong_scc(0),
nondetstates(0),
nondeterministic(false),
terminal_aut(false),
weak_aut(false),
strong_aut(false)
{
}
// If OK is false, only the status_str, status_code, and time fields
// should be valid.
bool ok;
// has in_* data to display.
bool has_in;
const char* status_str;
int status_code;
double time;
const char* in_type;
unsigned in_states;
unsigned in_edges;
unsigned in_transitions;
unsigned in_acc;
unsigned in_scc;
unsigned states;
unsigned edges;
unsigned transitions;
unsigned acc;
unsigned scc;
unsigned nonacc_scc;
unsigned terminal_scc;
unsigned weak_scc;
unsigned strong_scc;
unsigned nondetstates;
bool nondeterministic;
bool terminal_aut;
bool weak_aut;
bool strong_aut;
std::vector product_states;
std::vector product_transitions;
std::vector product_scc;
static void
fields(std::ostream& os, bool show_exit, bool show_sr)
{
if (show_exit)
os << "\"exit_status\",\"exit_code\",";
os << "\"time\",";
if (show_sr)
os << ("\"in_type\",\"in_states\",\"in_edges\",\"in_transitions\","
"\"in_acc\",\"in_scc\",");
os << ("\"states\","
"\"edges\","
"\"transitions\","
"\"acc\","
"\"scc\","
"\"nonacc_scc\","
"\"terminal_scc\","
"\"weak_scc\","
"\"strong_scc\","
"\"nondet_states\","
"\"nondet_aut\","
"\"terminal_aut\","
"\"weak_aut\","
"\"strong_aut\"");
size_t m = products_avg ? 1U : products;
for (size_t i = 0; i < m; ++i)
os << ",\"product_states\",\"product_transitions\",\"product_scc\"";
}
void
to_csv(std::ostream& os, bool show_exit, bool show_sr, const char* na = "")
{
if (show_exit)
os << '"' << status_str << "\"," << status_code << ',';
os << time << ',';
if (ok)
{
if (has_in)
os << '"' << in_type << "\","
<< in_states << ','
<< in_edges << ','
<< in_transitions << ','
<< in_acc << ','
<< in_scc << ',';
else if (show_sr)
os << na << ',' << na << ',' << na << ','
<< na << ',' << na << ',' << na << ',';
os << states << ','
<< edges << ','
<< transitions << ','
<< acc << ','
<< scc << ','
<< nonacc_scc << ','
<< terminal_scc << ','
<< weak_scc << ','
<< strong_scc << ','
<< nondetstates << ','
<< nondeterministic << ','
<< terminal_aut << ','
<< weak_aut << ','
<< strong_aut;
if (!products_avg)
{
for (size_t i = 0; i < products; ++i)
os << ',' << product_states[i]
<< ',' << product_transitions[i]
<< ',' << product_scc[i];
}
else
{
double st = 0.0;
double tr = 0.0;
double sc = 0.0;
for (size_t i = 0; i < products; ++i)
{
st += product_states[i];
tr += product_transitions[i];
sc += product_scc[i];
}
os << ',' << (st / products)
<< ',' << (tr / products)
<< ',' << (sc / products);
}
}
else
{
size_t m = products_avg ? 1U : products;
m *= 3;
m += 13 + show_sr * 6;
os << na;
for (size_t i = 0; i < m; ++i)
os << ',' << na;
}
}
};
typedef std::vector statistics_formula;
typedef std::vector statistics_vector;
statistics_vector vstats;
std::vector formulas;
static int
parse_opt(int key, char* arg, struct argp_state*)
{
// This switch is alphabetically-ordered.
switch (key)
{
case ARGP_KEY_ARG:
translators.push_back(arg);
break;
case OPT_BOGUS:
{
bogus_output = new output_file(arg);
bogus_output_filename = arg;
break;
}
case OPT_COLOR:
{
if (arg)
color_opt = XARGMATCH("--color", arg, color_args, color_types);
else
color_opt = color_always;
break;
}
case OPT_CSV:
want_stats = true;
csv_output = arg ? arg : "-";
break;
case OPT_DENSITY:
density = to_probability(arg);
break;
case OPT_DUPS:
allow_dups = true;
break;
case OPT_GRIND:
grind_output = new output_file(arg);
break;
case OPT_IGNORE_EXEC_FAIL:
ignore_exec_fail = true;
break;
case OPT_JSON:
want_stats = true;
json_output = arg ? arg : "-";
break;
case OPT_PRODUCTS:
if (*arg == '+')
{
products_avg = false;
++arg;
}
products = to_pos_int(arg);
break;
case OPT_NOCHECKS:
no_checks = true;
no_complement = true;
break;
case OPT_NOCOMP:
no_complement = true;
break;
case OPT_OMIT:
opt_omit = true;
break;
case OPT_SEED:
seed = to_pos_int(arg);
break;
case OPT_STATES:
states = to_pos_int(arg);
break;
case OPT_STOP_ERR:
stop_on_error = true;
break;
case OPT_VERBOSE:
verbose = true;
break;
default:
return ARGP_ERR_UNKNOWN;
}
return 0;
}
namespace
{
class xtranslator_runner: public translator_runner
{
public:
xtranslator_runner(spot::bdd_dict_ptr dict)
: translator_runner(dict)
{
has_sr = has('D');
}
spot::tgba_digraph_ptr
translate(unsigned int translator_num, char l, statistics_formula* fstats,
bool& problem)
{
output.reset(translator_num);
std::ostringstream command;
format(command, translators[translator_num].cmd);
assert(output.format != printable_result_filename::None);
std::string cmd = command.str();
std::cerr << "Running [" << l << translator_num << "]: "
<< cmd << std::endl;
spot::stopwatch sw;
sw.start();
int es = exec_with_timeout(cmd.c_str());
double duration = sw.stop();
const char* status_str = 0;
spot::tgba_digraph_ptr res = 0;
if (timed_out)
{
// This is not considered to be a global error.
std::cerr << "warning: timeout during execution of command\n";
++timeout_count;
status_str = "timeout";
problem = false; // A timeout is not a sign of a bug
es = -1;
}
else if (WIFSIGNALED(es))
{
status_str = "signal";
problem = true;
es = WTERMSIG(es);
global_error() << "error: execution terminated by signal "
<< es << ".\n";
end_error();
}
else if (WIFEXITED(es) && WEXITSTATUS(es) != 0)
{
es = WEXITSTATUS(es);
status_str = "exit code";
if (!ignore_exec_fail)
{
problem = true;
global_error() << "error: execution returned exit code "
<< es << ".\n";
end_error();
}
else
{
problem = false;
std::cerr << "warning: execution returned exit code "
<< es << ".\n";
++ignored_exec_fail;
}
}
else
{
status_str = "ok";
problem = false;
es = 0;
switch (output.format)
{
case printable_result_filename::Dstar:
{
spot::dstar_parse_error_list pel;
std::string filename = output.val()->name();
auto aut = spot::dstar_parse(filename, pel, dict);
if (!pel.empty())
{
status_str = "parse error";
problem = true;
es = -1;
std::ostream& err = global_error();
err << "error: failed to parse the produced DSTAR"
" output.\n";
spot::format_dstar_parse_errors(err, filename, pel);
end_error();
res = nullptr;
}
else
{
const char* type = 0;
switch (aut->type)
{
case spot::Rabin:
type = "DRA";
break;
case spot::Streett:
type = "DSA";
break;
}
assert(type);
// Gather statistics about the input automaton
if (want_stats)
{
statistics* st = &(*fstats)[translator_num];
st->has_in = true;
st->in_type = type;
spot::tgba_sub_statistics s =
sub_stats_reachable(aut->aut);
st->in_states= s.states;
st->in_edges = s.transitions;
st->in_transitions = s.sub_transitions;
st->in_acc = aut->accpair_count;
st->in_scc = spot::scc_info(aut->aut).scc_count();
}
// convert it into TGBA for further processing
if (verbose)
std::cerr << "info: converting " << type << " to TGBA\n";
res = dstar_to_tgba(aut);
}
break;
}
case printable_result_filename::Hoa: // Will also read neverclaims
{
spot::hoa_parse_error_list pel;
std::string filename = output.val()->name();
auto aut = spot::hoa_parse(filename, pel, dict);
if (!pel.empty())
{
status_str = "parse error";
problem = true;
es = -1;
std::ostream& err = global_error();
err << "error: failed to parse the produced automaton.\n";
spot::format_hoa_parse_errors(err, filename, pel);
end_error();
res = nullptr;
}
else if (!aut)
{
status_str = "empty output";
problem = true;
es = -1;
global_error() << "error: empty output.\n";
end_error();
res = nullptr;
}
else if (aut->aborted)
{
status_str = "aborted";
problem = true;
es = -1;
global_error() << "error: aborted HOA file.\n";
end_error();
res = nullptr;
}
else
{
res = aut->aut;
}
break;
}
case printable_result_filename::None:
SPOT_UNREACHABLE();
}
}
if (want_stats)
{
statistics* st = &(*fstats)[translator_num];
st->status_str = status_str;
st->status_code = es;
st->time = duration;
// Compute statistics.
if (res)
{
if (verbose)
std::cerr << "info: getting statistics\n";
st->ok = true;
spot::tgba_sub_statistics s = sub_stats_reachable(res);
st->states = s.states;
st->edges = s.transitions;
st->transitions = s.sub_transitions;
st->acc = res->acc().num_sets();
spot::scc_info m(res);
unsigned c = m.scc_count();
st->scc = c;
st->nondetstates = spot::count_nondet_states(res);
st->nondeterministic = st->nondetstates != 0;
for (unsigned n = 0; n < c; ++n)
{
if (m.is_rejecting_scc(n))
++st->nonacc_scc;
else if (is_terminal_scc(m, n))
++st->terminal_scc;
else if (is_weak_scc(m, n))
++st->weak_scc;
else
++st->strong_scc;
}
if (st->strong_scc)
st->strong_aut = true;
else if (st->weak_scc)
st->weak_aut = true;
else
st->terminal_aut = true;
}
}
output.cleanup();
return res;
}
};
static bool
check_empty_prod(const spot::const_tgba_digraph_ptr& aut_i,
const spot::const_tgba_digraph_ptr& aut_j,
size_t i, size_t j, bool icomp, bool jcomp)
{
auto prod = spot::product(aut_i, aut_j);
if (verbose)
{
std::cerr << "info: check_empty ";
if (icomp)
std::cerr << "Comp(N" << i << ')';
else
std::cerr << 'P' << i;
if (jcomp)
std::cerr << "*Comp(P" << j << ')';
else
std::cerr << "*N" << j;
std::cerr << '\n';
}
auto res = spot::couvreur99(prod)->check();
if (res)
{
std::ostream& err = global_error();
err << "error: ";
if (icomp)
err << "Comp(N" << i << ')';
else
err << 'P' << i;
if (jcomp)
err << "*Comp(P" << j << ')';
else
err << "*N" << j;
err << " is nonempty";
auto run = res->accepting_run();
if (run)
{
run = reduce_run(prod, run);
std::cerr << "; both automata accept the infinite word\n"
<< " ";
spot::tgba_word w(run);
w.simplify();
w.print(example(), prod->get_dict()) << '\n';
}
else
{
std::cerr << '\n';
}
end_error();
}
return !!res;
}
static bool
cross_check(const std::vector& maps, char l, unsigned p)
{
size_t m = maps.size();
if (verbose)
{
std::cerr << "info: cross_check {";
bool first = true;
for (size_t i = 0; i < m; ++i)
{
if (first)
first = false;
else
std::cerr << ',';
std::cerr << l << i;
}
std::cerr << "}, state-space #" << p << '/' << products << '\n';
}
std::vector res(m);
unsigned verified = 0;
unsigned violated = 0;
for (size_t i = 0; i < m; ++i)
if (spot::scc_info* m = maps[i])
{
// r == true iff the automaton i is accepting.
bool r = false;
for (auto& scc: *m)
if (scc.is_accepting())
{
r = true;
break;
}
res[i] = r;
if (r)
++verified;
else
++violated;
}
if (verified != 0 && violated != 0)
{
std::ostream& err = global_error();
err << "error: {";
bool first = true;
for (size_t i = 0; i < m; ++i)
if (maps[i] && res[i])
{
if (first)
first = false;
else
err << ',';
err << l << i;
}
err << "} disagree with {";
first = true;
for (size_t i = 0; i < m; ++i)
if (maps[i] && !res[i])
{
if (first)
first = false;
else
err << ',';
err << l << i;
}
err << "} when evaluating ";
if (products > 1)
err << "state-space #" << p << '/' << products << '\n';
else
err << "the state-space\n";
end_error();
return true;
}
return false;
}
typedef std::set state_set;
// Collect all the states of SSPACE that appear in the accepting SCCs
// of PROD. (Trivial SCCs are considered accepting.)
static void
states_in_acc(const spot::scc_info* m, state_set& s)
{
auto aut = m->get_aut();
auto ps = aut->get_named_prop("product-states");
for (auto& scc: *m)
if (scc.is_accepting() || scc.is_trivial())
for (auto i: scc.states())
// Get the projection on sspace.
s.insert((*ps)[i].second);
}
static bool
consistency_check(const spot::scc_info* pos, const spot::scc_info* neg)
{
// the states of SSPACE should appear in the accepting SCC of at
// least one of POS or NEG. Maybe both.
state_set s;
states_in_acc(pos, s);
states_in_acc(neg, s);
return s.size() == states;
}
typedef
std::unordered_set > fset_t;
class processor: public job_processor
{
spot::bdd_dict_ptr dict = spot::make_bdd_dict();
xtranslator_runner runner;
fset_t unique_set;
public:
processor():
runner(dict)
{
}
~processor()
{
fset_t::iterator i = unique_set.begin();
while (i != unique_set.end())
(*i++)->destroy();
}
int
process_string(const std::string& input,
const char* filename,
int linenum)
{
spot::ltl::parse_error_list pel;
const spot::ltl::formula* f = parse_formula(input, pel);
if (!f || !pel.empty())
{
if (filename)
error_at_line(0, 0, filename, linenum, "parse error:");
spot::ltl::format_parse_errors(std::cerr, input, pel);
if (f)
f->destroy();
return 1;
}
f->clone();
int res = process_formula(f, filename, linenum);
if (res && bogus_output)
bogus_output->ostream() << input << std::endl;
if (res && grind_output)
{
std::string bogus = input;
std::vector mutations;
unsigned mutation_count;
unsigned mutation_max;
while (res)
{
std::cerr << "Trying to find a bogus mutation of ";
if (color_opt)
std::cerr << bright_blue;
std::cerr << bogus;
if (color_opt)
std::cerr << reset_color;
std::cerr << "...\n";
mutations = mutate(f);
mutation_count = 1;
mutation_max = mutations.size();
res = 0;
for (auto g: mutations)
{
std::cerr << "Mutation " << mutation_count << '/'
<< mutation_max << ": ";
f->destroy();
f = g->clone();
res = process_formula(g->clone());
if (res)
break;
++mutation_count;
}
for (auto g: mutations)
g->destroy();
if (res)
{
if (lbt_input)
bogus = spot::ltl::to_lbt_string(f);
else
bogus = spot::ltl::to_string(f);
if (bogus_output)
bogus_output->ostream() << bogus << std::endl;
}
}
std::cerr << "Smallest bogus mutation found for ";
if (color_opt)
std::cerr << bright_blue;
std::cerr << input;
if (color_opt)
std::cerr << reset_color;
std::cerr << " is ";
if (color_opt)
std::cerr << bright_blue;
std::cerr << bogus;
if (color_opt)
std::cerr << reset_color;
std::cerr << ".\n\n";
grind_output->ostream() << bogus << std::endl;
}
f->destroy();
return 0;
}
int
process_formula(const spot::ltl::formula* f,
const char* filename = 0, int linenum = 0)
{
static unsigned round = 0;
// If we need LBT atomic proposition in any of the input or
// output, relabel the formula.
if (!f->has_lbt_atomic_props() &&
(runner.has('l') || runner.has('L') || runner.has('T')))
{
const spot::ltl::formula* g = spot::ltl::relabel(f, spot::ltl::Pnn);
f->destroy();
f = g;
}
// ---------- Positive Formula ----------
runner.round_formula(f, round);
// Call formula() before printing anything else, in case it
// complains.
std::string fstr = runner.formula();
if (filename)
std::cerr << filename << ':';
if (linenum)
std::cerr << linenum << ':';
if (filename || linenum)
std::cerr << ' ';
if (color_opt)
std::cerr << bright_blue;
std::cerr << fstr << '\n';
if (color_opt)
std::cerr << reset_color;
// Make sure we do not translate the same formula twice.
if (!allow_dups)
{
if (unique_set.insert(f).second)
{
f->clone();
}
else
{
std::cerr
<< ("warning: This formula or its negation has already"
" been checked.\n Use --allow-dups if it "
"should not be ignored.\n")
<< std::endl;
f->destroy();
return 0;
}
}
int problems = 0;
// These store the result of the translation of the positive and
// negative formulas.
size_t m = translators.size();
std::vector pos(m);
std::vector neg(m);
// These store the complement of the above results, when we can
// compute it easily.
std::vector comp_pos(m);
std::vector comp_neg(m);
unsigned n = vstats.size();
vstats.resize(n + (no_checks ? 1 : 2));
statistics_formula* pstats = &vstats[n];
statistics_formula* nstats = 0;
pstats->resize(m);
formulas.push_back(fstr);
for (size_t n = 0; n < m; ++n)
{
bool prob;
pos[n] = runner.translate(n, 'P', pstats, prob);
problems += prob;
// If the automaton is deterministic, compute its complement
// as well. Note that if we have computed statistics
// already, there is no need to call is_deterministic()
// again.
if (!no_complement && pos[n]
&& ((want_stats && !(*pstats)[n].nondeterministic)
|| (!want_stats && is_deterministic(pos[n]))))
comp_pos[n] = dtgba_complement(pos[n]);
}
// ---------- Negative Formula ----------
// The negative formula is only needed when checks are
// activated.
if (!no_checks)
{
nstats = &vstats[n + 1];
nstats->resize(m);
const spot::ltl::formula* nf =
spot::ltl::unop::instance(spot::ltl::unop::Not, f->clone());
if (!allow_dups)
{
bool res = unique_set.insert(nf->clone()).second;
// It is not possible to discover that nf has already been
// translated, otherwise that would mean that f had been
// translated too and we would have caught it before.
assert(res);
(void) res;
}
runner.round_formula(nf, round);
formulas.push_back(runner.formula());
for (size_t n = 0; n < m; ++n)
{
bool prob;
neg[n] = runner.translate(n, 'N', nstats, prob);
problems += prob;
// If the automaton is deterministic, compute its
// complement as well. Note that if we have computed
// statistics already, there is no need to call
// is_deterministic() again.
if (!no_complement && neg[n]
&& ((want_stats && !(*nstats)[n].nondeterministic)
|| (!want_stats && is_deterministic(neg[n]))))
comp_neg[n] = dtgba_complement(neg[n]);
}
nf->destroy();
}
spot::cleanup_tmpfiles();
++round;
if (!no_checks)
{
std::cerr << "Performing sanity checks and gathering statistics..."
<< std::endl;
if (verbose)
std::cerr << "info: getting rid of any Inf acceptance...\n";
for (unsigned i = 0; i < m; ++i)
{
#define DO(x, prefix, suffix) if (x[i]) \
{ \
cleanup_acceptance_here(x[i]); \
if (x[i]->acc().uses_fin_acceptance()) \
{ \
auto st = x[i]->num_states(); \
auto tr = x[i]->num_transitions(); \
auto ac = x[i]->acc().num_sets(); \
x[i] = remove_fin(x[i]); \
if (verbose) \
std::cerr << "info:\t" prefix << i \
<< suffix << "\t(" \
<< st << " st., " \
<< tr << " ed., " \
<< ac << " sets) -> (" \
<< x[i]->num_states() << " st., " \
<< x[i]->num_transitions() << " ed., " \
<< x[i]->acc().num_sets() << " sets)\n"; \
} \
}
DO(pos, " P", " ");
DO(neg, " N", " ");
DO(comp_pos, "Comp(P", ")");
DO(comp_neg, "Comp(N", ")");
#undef DO
}
// intersection test
for (size_t i = 0; i < m; ++i)
if (pos[i])
for (size_t j = 0; j < m; ++j)
if (neg[j])
{
problems +=
check_empty_prod(pos[i], neg[j], i, j, false, false);
// Deal with the extra complemented automata if we
// have some.
// If comp_pos[j] and comp_neg[j] exist for the
// same j, it means pos[j] and neg[j] were both
// deterministic. In that case, we will want to
// make sure that comp_pos[j]*comp_neg[j] is empty
// to assert the complementary of pos[j] and
// neg[j]. However using comp_pos[j] and
// comp_neg[j] against other translator will not
// give us any more insight than pos[j] and
// neg[j]. So we only do intersection checks with
// a complement automata when one of the two
// translation was not deterministic.
if (i != j && comp_pos[j] && !comp_neg[j])
problems +=
check_empty_prod(pos[i], comp_pos[j],
i, j, false, true);
if (i != j && comp_neg[i] && !comp_pos[i])
problems +=
check_empty_prod(comp_neg[i], neg[j],
i, j, true, false);
if (comp_pos[i] && comp_neg[j] &&
(i == j || (!comp_neg[i] && !comp_pos[j])))
problems +=
check_empty_prod(comp_pos[i], comp_neg[j],
i, j, true, true);
}
}
else
{
std::cerr << "Gathering statistics..." << std::endl;
}
spot::ltl::atomic_prop_set* ap = spot::ltl::atomic_prop_collect(f);
f->destroy();
if (want_stats)
for (size_t i = 0; i < m; ++i)
{
(*pstats)[i].product_states.reserve(products);
(*pstats)[i].product_transitions.reserve(products);
(*pstats)[i].product_scc.reserve(products);
if (neg[i])
{
(*nstats)[i].product_states.reserve(products);
(*nstats)[i].product_transitions.reserve(products);
(*nstats)[i].product_scc.reserve(products);
}
}
for (unsigned p = 0; p < products; ++p)
{
// build a random state-space.
spot::srand(seed);
if (verbose)
std::cerr << "info: building state-space #" << p << '/' << products
<< " of " << states << " states with seed " << seed
<< '\n';
auto statespace = spot::random_graph(states, density, ap, dict);
if (verbose)
std::cerr << "info: state-space has "
<< statespace->num_transitions()
<< " edges\n";
// Products of the state space with the positive automata.
std::vector pos_prod(m);
// Products of the state space with the negative automata.
std::vector neg_prod(m);
// Associated SCC maps.
std::vector pos_map(m);
std::vector neg_map(m);
for (size_t i = 0; i < m; ++i)
if (pos[i])
{
if (verbose)
std::cerr << ("info: building product between state-space and"
" P") << i
<< " (" << pos[i]->num_states() << " st., "
<< pos[i]->num_transitions() << " ed.)\n";
auto p = spot::product(pos[i], statespace);
pos_prod[i] = p;
auto sm = new spot::scc_info(p);
pos_map[i] = sm;
// Statistics
if (want_stats)
{
(*pstats)[i].product_scc.push_back(sm->scc_count());
spot::tgba_statistics s = spot::stats_reachable(p);
(*pstats)[i].product_states.push_back(s.states);
(*pstats)[i].product_transitions.push_back(s.transitions);
}
}
if (!no_checks)
for (size_t i = 0; i < m; ++i)
if (neg[i])
{
if (verbose)
std::cerr << ("info: building product between state-space and"
" N") << i
<< " (" << neg[i]->num_states() << " st., "
<< neg[i]->num_transitions() << " ed.)\n";
auto p = spot::product(neg[i], statespace);
neg_prod[i] = p;
auto sm = new spot::scc_info(p);
neg_map[i] = sm;
// Statistics
if (want_stats)
{
(*nstats)[i].product_scc.push_back(sm->scc_count());
spot::tgba_statistics s = spot::stats_reachable(p);
(*nstats)[i].product_states.push_back(s.states);
(*nstats)[i].product_transitions.push_back(s.transitions);
}
}
if (!no_checks)
{
// cross-comparison test
problems += cross_check(pos_map, 'P', p);
problems += cross_check(neg_map, 'N', p);
// consistency check
for (size_t i = 0; i < m; ++i)
if (pos_map[i] && neg_map[i])
{
if (verbose)
std::cerr << "info: consistency_check (P" << i
<< ",N" << i << "), state-space #"
<< p << '/' << products << '\n';
if (!(consistency_check(pos_map[i], neg_map[i])))
{
++problems;
std::ostream& err = global_error();
err << "error: inconsistency between P" << i
<< " and N" << i;
if (products > 1)
err << " for state-space #" << p
<< '/' << products << '\n';
else
err << '\n';
end_error();
}
}
}
// Cleanup.
if (!no_checks)
for (size_t i = 0; i < m; ++i)
delete neg_map[i];
for (size_t i = 0; i < m; ++i)
delete pos_map[i];
++seed;
}
std::cerr << std::endl;
delete ap;
// Shall we stop processing formulas now?
abort_run = global_error_flag && stop_on_error;
return problems;
}
};
}
// Output an RFC4180-compatible CSV file.
static void
print_stats_csv(const char* filename)
{
if (verbose)
std::cerr << "info: writing CSV to " << filename << '\n';
output_file outf(filename);
std::ostream& out = outf.ostream();
unsigned ntrans = translators.size();
unsigned rounds = vstats.size();
assert(rounds == formulas.size());
if (!outf.append())
{
// Do not output the header line if we append to a file.
// (Even if that file was empty initially.)
out << "\"formula\",\"tool\",";
statistics::fields(out, !opt_omit, has_sr);
out << '\n';
}
for (unsigned r = 0; r < rounds; ++r)
for (unsigned t = 0; t < ntrans; ++t)
if (!opt_omit || vstats[r][t].ok)
{
out << '"';
spot::escape_rfc4180(out, formulas[r]);
out << "\",\"";
spot::escape_rfc4180(out, translators[t].name);
out << "\",";
vstats[r][t].to_csv(out, !opt_omit, has_sr);
out << '\n';
}
}
static void
print_stats_json(const char* filename)
{
if (verbose)
std::cerr << "info: writing JSON to " << filename << '\n';
output_file outf(filename);
std::ostream& out = outf.ostream();
unsigned ntrans = translators.size();
unsigned rounds = vstats.size();
assert(rounds == formulas.size());
out << "{\n \"tool\": [\n \"";
spot::escape_str(out, translators[0].name);
for (unsigned t = 1; t < ntrans; ++t)
{
out << "\",\n \"";
spot::escape_str(out, translators[t].name);
}
out << "\"\n ],\n \"formula\": [\n \"";
spot::escape_str(out, formulas[0]);
for (unsigned r = 1; r < rounds; ++r)
{
out << "\",\n \"";
spot::escape_str(out, formulas[r]);
}
out << ("\"\n ],\n \"fields\": [\n \"formula\",\"tool\",");
statistics::fields(out, !opt_omit, has_sr);
out << "\n ],\n \"inputs\": [ 0, 1 ],";
out << "\n \"results\": [";
bool notfirst = false;
for (unsigned r = 0; r < rounds; ++r)
for (unsigned t = 0; t < ntrans; ++t)
if (!opt_omit || vstats[r][t].ok)
{
if (notfirst)
out << ',';
notfirst = true;
out << "\n [ " << r << ',' << t << ',';
vstats[r][t].to_csv(out, !opt_omit, has_sr, "null");
out << " ]";
}
out << "\n ]\n}\n";
}
int
main(int argc, char** argv)
{
setup(argv);
const argp ap = { options, parse_opt, "[COMMANDFMT...]",
argp_program_doc, children, 0, 0 };
if (int err = argp_parse(&ap, argc, argv, ARGP_NO_HELP, 0, 0))
exit(err);
if (jobs.empty())
jobs.emplace_back("-", 1);
if (translators.empty())
error(2, 0, "No translator to run? Run '%s --help' for usage.",
program_name);
if (color_opt == color_if_tty)
color_opt = isatty(STDERR_FILENO) ? color_always : color_never;
setup_sig_handler();
processor p;
if (p.run())
return 2;
if (formulas.empty())
{
error(2, 0, "no formula to translate");
}
else
{
if (global_error_flag)
{
std::ostream& err = global_error();
if (bogus_output)
err << ("error: some error was detected during the above runs.\n"
" Check file ")
<< bogus_output_filename
<< " for problematic formulas.";
else
err << ("error: some error was detected during the above runs,\n"
" please search for 'error:' messages in the above"
" trace.");
err << std::endl;
if (timeout_count > 0 || ignored_exec_fail > 0)
{
err << "Additionally, ";
if (timeout_count == 1)
err << "1 timeout occurred";
else if (timeout_count > 1)
err << timeout_count << " timeouts occurred";
if (timeout_count > 0 && ignored_exec_fail > 0)
err << " and ";
if (ignored_exec_fail == 1)
err << "1 non-zero exit status was ignored";
if (ignored_exec_fail > 1)
err << ignored_exec_fail
<< " non-zero exit statuses were ignored";
err << '.' << std::endl;
}
end_error();
}
else if (timeout_count == 0 && ignored_exec_fail == 0)
{
std::cerr << "No problem detected." << std::endl;
}
else
{
if (timeout_count == 1)
std::cerr << "1 timeout";
if (timeout_count > 1)
std::cerr << timeout_count << " timeouts";
if (timeout_count > 0 && ignored_exec_fail > 0)
std::cerr << " and ";
if (ignored_exec_fail == 1)
std::cerr << "1 non-zero exit status";
if (ignored_exec_fail > 1)
std::cerr << ignored_exec_fail
<< " non-zero exit statuses";
std::cerr << ", but no other problem detected." << std::endl;
}
}
delete bogus_output;
delete grind_output;
if (json_output)
print_stats_json(json_output);
if (csv_output)
print_stats_csv(csv_output);
return global_error_flag;
}