bin: introduce autcross

Fixes #252.

* NEWS: Mention it.
* bin/autcross.cc, bin/man/autcross.x, doc/org/autcross.org: New
files.
* bin/Makefile.am, bin/man/Makefile.am, doc/org/tools.org,
doc/Makefile.am: Add them.
* bin/autfilt.cc: Use is_universal() instead of is_deterministic().
* bin/common_hoaread.hh, bin/common_trans.cc, bin/common_trans.hh,
bin/ltlcross.cc, bin/ltldo.cc: Factor some bits common between
ltlcross, ltldo and autcross.
* tests/core/autcross.test, tests/core/autcross2.test: New files.
* tests/Makefile.am: Add them.
* tests/core/dra2dba.test, tests/core/sbacc.test,
tests/core/streett.test: Use autcross.
This commit is contained in:
Alexandre Duret-Lutz 2017-07-27 18:42:05 +02:00
parent b9fff6a4b1
commit 0cf250d839
21 changed files with 1726 additions and 53 deletions

1
bin/.gitignore vendored
View file

@ -1,3 +1,4 @@
autcross
autfilt
dstar2tgba
genaut

View file

@ -59,6 +59,7 @@ libcommon_a_SOURCES = \
common_trans.hh
bin_PROGRAMS = \
autcross \
autfilt \
dstar2tgba \
genaut \
@ -77,6 +78,7 @@ bin_PROGRAMS = \
# version number that is automatically updated).
noinst_PROGRAMS = spot-x spot
autcross_SOURCES = autcross.cc
autfilt_SOURCES = autfilt.cc
ltlfilt_SOURCES = ltlfilt.cc
genaut_SOURCES = genaut.cc

898
bin/autcross.cc Normal file
View file

@ -0,0 +1,898 @@
// -*- coding: utf-8 -*-
// Copyright (C) 2017 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 <http://www.gnu.org/licenses/>.
#include "common_sys.hh"
#include <string>
#include <iostream>
#include <sstream>
#include <cstdlib>
#include <cstdio>
#include <argp.h>
#include <unistd.h>
#include <cmath>
#include <sys/wait.h>
#include <iomanip>
#include "error.h"
#include "argmatch.h"
#include "common_setup.hh"
#include "common_hoaread.hh"
#include "common_finput.hh"
#include "common_color.hh"
#include "common_trans.hh"
#include "common_cout.hh"
#include "common_aoutput.hh"
#include "common_post.hh"
#include <spot/twaalgos/hoa.hh>
#include <spot/twaalgos/postproc.hh>
#include <spot/twaalgos/isdet.hh>
#include <spot/twaalgos/determinize.hh>
#include <spot/twaalgos/dualize.hh>
#include <spot/twaalgos/alternation.hh>
#include <spot/twaalgos/cleanacc.hh>
#include <spot/twaalgos/remfin.hh>
#include <spot/twaalgos/product.hh>
#include <spot/misc/escape.hh>
const char argp_program_doc[] ="\
Call several tools that process automata and cross-compare their output \
to detect bugs, or to gather statistics. The list of automata 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 tools failed to output something we understand, or failed\n\
sanity checks (statistics were output nonetheless)\n\
2 autcross aborted on error\n\
";
enum {
OPT_BOGUS = 256,
OPT_CSV,
OPT_HIGH,
OPT_IGNORE_EXEC_FAIL,
OPT_LANG,
OPT_LOW,
OPT_MEDIUM,
OPT_NOCHECKS,
OPT_OMIT,
OPT_STOP_ERR,
OPT_VERBOSE,
};
static const argp_option options[] =
{
{ nullptr, 0, nullptr, 0, "Input:", 1 },
{ "file", 'F', "FILENAME", 0,
"process automata from FILENAME", 0 },
/**************************************************/
{ nullptr, 0, nullptr, 0, "autcross behavior:", 5 },
{ "stop-on-error", OPT_STOP_ERR, nullptr, 0,
"stop on first execution error or failure to pass"
" sanity checks (timeouts are OK)", 0 },
{ "ignore-execution-failures", OPT_IGNORE_EXEC_FAIL, nullptr, 0,
"ignore automata from translators that return with a non-zero exit code,"
" but do not flag this as an error", 0 },
{ "language-preserved", OPT_LANG, nullptr, 0,
"expect that each tool preserves the input language", 0 },
{ "no-checks", OPT_NOCHECKS, nullptr, 0,
"do not perform any sanity checks", 0 },
/**************************************************/
{ nullptr, 0, nullptr, 0, "Statistics output:", 7 },
{ "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, nullptr, 0,
"do not output statistics for timeouts or failed translations", 0 },
/**************************************************/
{ nullptr, 0, nullptr, 0, "Simplification level (for complementation):",
21 },
{ "low", OPT_LOW, nullptr, 0, "minimal optimizations (fast)", 0 },
{ "medium", OPT_MEDIUM, nullptr, 0, "moderate optimizations", 0 },
{ "high", OPT_HIGH, nullptr, 0,
"all available optimizations (slow, default)", 0 },
/**************************************************/
{ nullptr, 0, nullptr, 0, "Miscellaneous options:", -2 },
{ "save-bogus", OPT_BOGUS, "[>>]FILENAME", 0,
"save formulas for which problems were detected in FILENAME", 0 },
{ "verbose", OPT_VERBOSE, nullptr, 0,
"print what is being done, for debugging", 0 },
{ nullptr, 0, nullptr, 0, nullptr, 0 }
};
const struct argp_child children[] =
{
{ &autproc_argp, 0, nullptr, 0 },
{ &hoaread_argp, 0, "Parsing of automata:", 4 },
{ &misc_argp, 0, nullptr, -2 },
{ &color_argp, 0, nullptr, 0 },
{ nullptr, 0, nullptr, 0 }
};
static bool verbose = false;
static bool ignore_exec_fail = false;
static unsigned ignored_exec_fail = 0;
static bool stop_on_error = false;
static bool no_checks = false;
static bool opt_language_preserved = false;
static bool opt_omit = false;
static const char* csv_output = nullptr;
static unsigned round_num = 0;
static const char* bogus_output_filename = nullptr;
static output_file* bogus_output = nullptr;
static int
parse_opt(int key, char* arg, struct argp_state*)
{
switch (key)
{
case 'F':
jobs.emplace_back(arg, true);
break;
case OPT_BOGUS:
{
bogus_output = new output_file(arg);
bogus_output_filename = arg;
break;
}
case OPT_CSV:
csv_output = arg ? arg : "-";
break;
case OPT_HIGH:
level = spot::postprocessor::High;
level_set = true;
break;
case OPT_IGNORE_EXEC_FAIL:
ignore_exec_fail = true;
break;
case OPT_LANG:
opt_language_preserved = true;
break;
case OPT_LOW:
level = spot::postprocessor::Low;
level_set = true;
break;
case OPT_MEDIUM:
level = spot::postprocessor::Medium;
level_set = true;
break;
case OPT_NOCHECKS:
no_checks = true;
break;
case OPT_OMIT:
opt_omit = true;
break;
case OPT_STOP_ERR:
stop_on_error = true;
break;
case OPT_VERBOSE:
verbose = true;
break;
case ARGP_KEY_ARG:
if (arg[0] == '-' && !arg[1])
jobs.emplace_back(arg, true);
else
tools_push_autproc(arg);
break;
default:
return ARGP_ERR_UNKNOWN;
}
return 0;
}
static bool global_error_flag = false;
// static unsigned oom_count = 0U;
static std::ostream&
global_error()
{
global_error_flag = true;
std::cerr << bright_red;
return std::cerr;
}
static void
end_error()
{
std::cerr << reset_color;
}
static std::ostream&
example()
{
std::cerr << bold_std;
return std::cerr;
}
struct aut_statistics
{
unsigned ap;
unsigned states;
unsigned edges;
unsigned transitions;
unsigned acc_sets;
unsigned scc;
unsigned nondetstates;
bool nondeterministic;
bool alternating;
aut_statistics()
{
}
void set(const spot::const_twa_graph_ptr& aut)
{
if (!csv_output)
// Do not waste time.
return;
ap = aut->ap().size();
spot::twa_sub_statistics s = sub_stats_reachable(aut);
states = s.states;
edges = s.edges;
transitions = s.transitions;
spot::scc_info m(aut);
acc_sets = aut->num_sets();
unsigned c = m.scc_count();
scc = c;
nondetstates = spot::count_nondet_states(aut);
nondeterministic = nondetstates != 0;
alternating = !aut->is_existential();
}
void to_csv(std::ostream& os) const
{
os << ap << ','
<< states << ','
<< edges << ','
<< transitions << ','
<< acc_sets << ','
<< scc << ','
<< nondetstates << ','
<< nondeterministic << ','
<< alternating;
}
void empty(std::ostream& os) const
{
os << ",,,,,,,,";
}
static void fields(std::ostream& os, const char* prefix)
{
os << '"'
<< prefix << "ap\",\""
<< prefix << "states\",\""
<< prefix << "edges\",\""
<< prefix << "transitions\",\""
<< prefix << "acc_sets\",\""
<< prefix << "scc\",\""
<< prefix << "nondetstates\",\""
<< prefix << "nondeterministic\",\""
<< prefix << "alternating\"";
}
};
struct in_statistics
{
std::string input_source;
std::string input_name;
aut_statistics input;
static void fields(std::ostream& os)
{
os << "\"input.source\",\"input.name\",";
aut_statistics::fields(os, "input.");
}
void to_csv(std::ostream& os) const
{
spot::escape_rfc4180(os << '"', input_source) << "\",";
if (!input_name.empty())
spot::escape_rfc4180(os << '"', input_name) << "\",";
else
os << ',';
input.to_csv(os);
}
};
struct out_statistics
{
// If OK is false, output statistics are not available.
bool ok;
const char* status_str;
int status_code;
double time;
aut_statistics output;
out_statistics()
: ok(false),
status_str(nullptr),
status_code(0),
time(0)
{
}
static void fields(std::ostream& os)
{
os << "\"exit_status\",\"exit_code\",\"time\",";
aut_statistics::fields(os, "output.");
}
void to_csv(std::ostream& os) const
{
os << '"' << status_str << "\"," << status_code << ','
<< time << ',';
if (ok)
output.to_csv(os);
else
output.empty(os);
}
};
std::vector<in_statistics> input_statistics;
typedef std::vector<out_statistics> vector_tool_statistics;
std::vector<vector_tool_statistics> output_statistics;
namespace
{
class autcross_runner final: public autproc_runner
{
spot::bdd_dict_ptr dict;
public:
autcross_runner(spot::bdd_dict_ptr dict)
: dict(dict)
{
}
spot::twa_graph_ptr
run_tool(unsigned int tool_num, char l, bool& problem,
out_statistics& stats)
{
output.reset(tool_num);
std::ostringstream command;
format(command, tools[tool_num].cmd);
std::string cmd = command.str();
std::cerr << "Running [" << l << tool_num << "]: "
<< cmd << std::endl;
process_timer timer;
timer.start();
int es = exec_with_timeout(cmd.c_str());
timer.stop();
const char* status_str = nullptr;
spot::twa_graph_ptr res = nullptr;
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;
auto aut = spot::parse_aut(output.val()->name(), dict,
spot::default_environment::instance(),
opt_parse);
if (!aut->errors.empty())
{
status_str = "parse error";
problem = true;
es = -1;
std::ostream& err = global_error();
err << "error: failed to parse the produced automaton.\n";
aut->format_errors(err);
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;
}
}
output.cleanup();
stats.status_str = status_str;
stats.status_code = es;
stats.time = timer.get_lap_sw();
if (res)
{
stats.ok = true;
stats.output.set(res);
}
return res;
}
};
static std::string
autname(unsigned i, bool complemented = false)
{
std::ostringstream str;
if (complemented)
str << "Comp(";
if (i < tools.size())
str << 'A' << i;
else
str << "input";
if (complemented)
str << ')';
return str.str();
}
static bool
check_empty_prod(const spot::const_twa_graph_ptr& aut_i,
const spot::const_twa_graph_ptr& aut_j,
size_t i, size_t j)
{
if (aut_i->num_sets() + aut_j->num_sets()
> 8 * sizeof(spot::acc_cond::mark_t::value_t))
{
std::cerr << "info: building " << autname(i)
<< '*' << autname(j, true)
<< " requires more acceptance sets than supported\n";
return false;
}
auto prod = spot::product(aut_i, aut_j);
if (verbose)
std::cerr << "info: check_empty "
<< autname(i) << '*' << autname(j, true) << '\n';
auto w = prod->accepting_word();
if (w)
{
std::ostream& err = global_error();
err << "error: " << autname(i) << '*' << autname(j, true)
<< (" is nonempty; both automata accept the infinite word:\n"
" ");
example() << *w << '\n';
end_error();
}
return !!w;
}
class autcross_processor final: public hoa_processor
{
autcross_runner runner;
public:
autcross_processor()
: hoa_processor(spot::make_bdd_dict()), runner(dict_)
{
}
int
process_automaton(const spot::const_parsed_aut_ptr& haut) override
{
auto printsize = [](const spot::const_twa_graph_ptr& aut,
bool props)
{
std::cerr << '(' << aut->num_states() << " st.,"
<< aut->num_edges() << " ed.,"
<< aut->num_sets() << " sets)";
if (props)
{
if (!aut->is_existential())
std::cerr << " univ-edges";
if (is_deterministic(aut))
std::cerr << " deterministic";
if (is_complete(aut))
std::cerr << " complete";
std::cerr << '\n';
}
};
spot::twa_graph_ptr input = haut->aut;
runner.round_automaton(input, round_num);
std::string source = [&]()
{
std::ostringstream src;
src << haut->filename << ':' << haut->loc;
return src.str();
}();
input_statistics.push_back(in_statistics());
std::cerr << bold << source << reset_color;
input_statistics[round_num].input_source = std::move(source);
if (auto name = input->get_named_prop<std::string>("automaton-name"))
{
std::cerr << '\t' << *name;
input_statistics[round_num].input_name = *name;
}
std::cerr << '\n';
input_statistics[round_num].input.set(input);
int problems = 0;
size_t m = tools.size();
size_t mi = m + opt_language_preserved;
std::vector<spot::twa_graph_ptr> pos(mi);
std::vector<spot::twa_graph_ptr> neg(mi);
vector_tool_statistics stats(m);
if (opt_language_preserved)
pos[mi - 1] = input;
if (verbose)
{
std::cerr << "info: input\t";
printsize(input, true);
}
for (size_t n = 0; n < m; ++n)
{
bool prob;
pos[n] = runner.run_tool(n, 'A', prob, stats[n]);
problems += prob;
}
spot::cleanup_tmpfiles();
++round_num;
output_statistics.push_back(std::move(stats));
if (verbose)
{
std::cerr << "info: collected automata:\n";
for (unsigned i = 0; i < m; ++i)
if (pos[i])
{
std::cerr << "info: A" << i << '\t';
printsize(pos[i], true);
}
}
if (!no_checks)
{
std::cerr << "Performing sanity checks and gathering statistics..."
<< std::endl;
{
bool print_first = true;
for (unsigned i = 0; i < mi; ++i)
{
if (!pos[i])
continue;
if (!pos[i]->is_existential())
{
if (verbose)
{
if (print_first)
{
std::cerr <<
"info: getting rid of universal edges...\n";
print_first = false;
}
std::cerr << "info: "
<< std::setw(8) << autname(i) << '\t';
printsize(pos[i], false);
std::cerr << " -> ";
}
pos[i] = remove_alternation(pos[i]);
if (verbose)
{
printsize(pos[i], false);
std::cerr << '\n';
}
}
if (is_universal(pos[i]))
neg[i] = dualize(pos[i]);
}
}
{
bool print_first = verbose;
for (unsigned i = 0; i < mi; ++i)
{
if (pos[i] && !neg[i])
{
if (print_first)
{
std::cerr << "info: complementing non-deterministic "
"automata via determinization...\n";
print_first = false;
}
spot::postprocessor p;
p.set_type(spot::postprocessor::Generic);
p.set_pref(spot::postprocessor::Deterministic);
p.set_level(level);
neg[i] = dualize(p.run(pos[i]));
if (verbose)
{
std::cerr << "info: "
<< std::setw(8) << autname(i) << '\t';
printsize(pos[i], false);
std::cerr << " -> ";
printsize(neg[i], false);
std::cerr << '\t' << autname(i, true) << '\n';
}
}
};
}
{
bool print_first = true;
auto tmp = [&](std::vector<spot::twa_graph_ptr>& x, unsigned i,
bool neg)
{
if (!x[i])
return;
if (x[i]->acc().uses_fin_acceptance())
{
if (verbose)
{
if (print_first)
{
std::cerr <<
"info: getting rid of any Fin acceptance...\n";
print_first = false;
}
std::cerr << "info: "
<< std::setw(8) << autname(i, neg) << '\t';
printsize(x[i], false);
std::cerr << " ->";
}
cleanup_acceptance_here(x[i]);
x[i] = remove_fin(x[i]);
if (verbose)
{
std::cerr << ' ';
printsize(x[i], false);
std::cerr << '\n';
}
}
else
{
// Remove useless sets nonetheless.
cleanup_acceptance_here(x[i]);
}
};
for (unsigned i = 0; i < mi; ++i)
{
tmp(pos, i, false);
tmp(neg, i, true);
}
}
// Just make a circular implication check
// A0 <= A1, A1 <= A2, ..., AN <= A0
unsigned ok = 0;
for (size_t i = 0; i < mi; ++i)
if (pos[i])
{
size_t j = ((i + 1) % mi);
if (i != j && neg[j])
{
int res = check_empty_prod(pos[i], neg[j], i, j);
problems += res;
ok += !res;
}
}
// If the circular check failed, do the rest of all
// mi(mi-1)/2 checks, as this will help diagnose the issue.
if (ok != mi)
for (size_t i = 0; i < mi; ++i)
if (pos[i])
{
size_t k = ((i + 1) % mi);
for (size_t j = 0; j < mi; ++j)
if (i != j && j != k && neg[j])
problems += check_empty_prod(pos[i], neg[j], i, j);
}
}
else
{
std::cerr << "Gathering statistics..." << std::endl;
}
if (problems && bogus_output)
print_hoa(bogus_output->ostream(), input) << std::endl;
std::cerr << std::endl;
// Shall we stop processing 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 ntools = tools.size();
assert(round_num == output_statistics.size());
assert(round_num == input_statistics.size());
if (!outf.append())
{
// Do not output the header line if we append to a file.
// (Even if that file was empty initially.)
in_statistics::fields(out);
out << ",\"tool\",";
out_statistics::fields(out);
out << '\n';
}
for (unsigned r = 0; r < round_num; ++r)
for (unsigned t = 0; t < ntools; ++t)
if (!opt_omit || output_statistics[r][t].ok)
{
input_statistics[r].to_csv(out);
out << ",\"";
spot::escape_rfc4180(out, tools[t].name);
out << "\",";
output_statistics[r][t].to_csv(out);
out << '\n';
}
outf.close(filename);
}
int
main(int argc, char** argv)
{
setup(argv);
const argp ap = { options, parse_opt, "[COMMANDFMT...]",
argp_program_doc, children, nullptr, nullptr };
if (int err = argp_parse(&ap, argc, argv, ARGP_NO_HELP, nullptr, nullptr))
exit(err);
check_no_automaton();
auto s = tools.size();
if (s == 0)
error(2, 0, "No tool to run? Run '%s --help' for usage.",
program_name);
if (s == 1 && !opt_language_preserved && !no_checks)
error(2, 0, "Since --language-preserved is not used, you need "
"at least two tools to compare.");
setup_color();
setup_sig_handler();
autcross_processor p;
if (p.run())
return 2;
if (round_num == 0)
{
error(2, 0, "no automaton 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 automata.";
else
err << ("error: some error was detected during the above runs,\n"
" please search for 'error:' messages in the above"
" trace.");
err << std::endl;
end_error();
}
else if (timeout_count == 0 && ignored_exec_fail == 0)
{
std::cerr << "No problem detected." << std::endl;
}
else
{
std::cerr << "No major problem detected." << std::endl;
}
unsigned additional_errors = 0U;
additional_errors += timeout_count > 0;
additional_errors += ignored_exec_fail > 0;
if (additional_errors)
{
std::cerr << (global_error_flag ? "Additionally, " : "However, ");
if (timeout_count)
{
if (additional_errors > 1)
std::cerr << "\n - ";
if (timeout_count == 1)
std::cerr << "1 timeout occurred";
else
std::cerr << timeout_count << " timeouts occurred";
}
if (ignored_exec_fail)
{
if (additional_errors > 1)
std::cerr << "\n - ";
if (ignored_exec_fail == 1)
std::cerr << "1 non-zero exit status was ignored";
else
std::cerr << ignored_exec_fail
<< " non-zero exit statuses were ignored";
}
if (additional_errors == 1)
std::cerr << '.';
std::cerr << std::endl;
}
}
if (csv_output)
print_stats_csv(csv_output);
return global_error_flag;
}

View file

@ -515,7 +515,7 @@ static bool opt_highlight_languages = false;
static spot::twa_graph_ptr
ensure_deterministic(const spot::twa_graph_ptr& aut, bool nonalt = false)
{
if ((!nonalt || aut->is_existential()) && spot::is_deterministic(aut))
if ((!nonalt || aut->is_existential()) && spot::is_universal(aut))
return aut;
spot::postprocessor p;
p.set_type(spot::postprocessor::Generic);

View file

@ -43,6 +43,7 @@ read_automaton(const char* filename, spot::bdd_dict_ptr& dict);
class hoa_processor: public job_processor
{
protected:
spot::bdd_dict_ptr dict_;
public:

View file

@ -33,14 +33,17 @@
#include <spot/tl/unabbrev.hh>
#include "common_conv.hh"
#include <spot/misc/escape.hh>
#include <spot/twaalgos/hoa.hh>
#include <spot/twaalgos/lbtt.hh>
#include <spot/twaalgos/neverclaim.hh>
// A set of tools for which we know the correct output
static struct shorthands_t
struct shorthands_t
{
const char* prefix;
const char* suffix;
}
shorthands[] = {
};
static shorthands_t shorthands_ltl[] = {
{ "lbt", " <%L>%O" },
{ "ltl2ba", " -f %s>%O" },
{ "ltl2da", " %f>%O" },
@ -55,25 +58,30 @@ static struct shorthands_t
{ "spin", " -f %s>%O" },
};
static void show_shorthands()
static shorthands_t shorthands_autproc[] = {
{ "autfilt", " %H>%O" },
{ "dstar2tgba", " %H>%O" },
{ "ltl2dstar", " -B %H %O" },
{ "nba2ldpa", " <%H>%O" },
{ "seminator", " %H>%O" },
};
static void show_shorthands(shorthands_t* begin, shorthands_t* end)
{
std::cout
<< ("If a COMMANDFMT does not use any %-sequence, and starts with one of\n"
"the following words, then the string on the right is appended.\n\n");
for (auto& s: shorthands)
std::cout << " "
<< std::left << std::setw(12) << s.prefix
<< s.suffix << '\n';
std::cout
<< ("\nAny {name} and directory component is skipped for the purpose of\n"
"matching those prefixes. So for instance\n"
" '{DRA} ~/mytools/ltl2dstar-0.5.2'\n"
"will changed into\n"
" '{DRA} ~/mytools/ltl2dstar-0.5.2 --output-format=hoa %[MR]L %O'\n");
while (begin != end)
{
std::cout << " "
<< std::left << std::setw(12) << begin->prefix
<< begin->suffix << '\n';
++begin;
}
}
tool_spec::tool_spec(const char* spec)
tool_spec::tool_spec(const char* spec, shorthands_t* begin, shorthands_t* end)
: spec(spec), cmd(spec), name(spec)
{
if (*cmd == '{')
@ -114,8 +122,9 @@ tool_spec::tool_spec(const char* spec)
++pos;
}
// Match a shorthand.
for (auto& p: shorthands)
while (begin != end)
{
auto& p = *begin++;
int n = strlen(p.prefix);
if (strncmp(basename, p.prefix, n) == 0)
{
@ -165,6 +174,20 @@ tool_spec::~tool_spec()
std::vector<tool_spec> tools;
void tools_push_trans(const char* trans)
{
tools.emplace_back(trans,
std::begin(shorthands_ltl),
std::end(shorthands_ltl));
}
void tools_push_autproc(const char* proc)
{
tools.emplace_back(proc,
std::begin(shorthands_autproc),
std::end(shorthands_autproc));
}
void
quoted_string::print(std::ostream& os, const char*) const
{
@ -254,16 +277,8 @@ printable_result_filename::print(std::ostream& os, const char*) const
spot::quote_shell_string(os, val()->name());
}
void
filed_formula::print(std::ostream& os, const char* pos) const
{
std::ostringstream ss;
f_.print(ss, pos);
os << '\'' << string_to_tmp(ss.str(), serial_) << '\'';
}
std::string
filed_formula::string_to_tmp(const std::string str, unsigned n) const
static std::string
string_to_tmp(const std::string str, unsigned n)
{
char prefix[30];
snprintf(prefix, sizeof prefix, "lcr-i%u-", n);
@ -278,6 +293,43 @@ filed_formula::string_to_tmp(const std::string str, unsigned n) const
return tmpname;
}
void
filed_formula::print(std::ostream& os, const char* pos) const
{
std::ostringstream ss;
f_.print(ss, pos);
os << '\'' << string_to_tmp(ss.str(), serial_) << '\'';
}
void
filed_automaton::print(std::ostream& os, const char* pos) const
{
std::ostringstream ss;
char* opt = nullptr;
if (*pos == '[')
{
++pos;
auto end = strchr(pos, ']');
opt = strndup(pos, end - pos);
pos = end + 1;
}
switch (*pos)
{
case 'H':
spot::print_hoa(ss, aut_, opt);
break;
case 'S':
spot::print_never_claim(ss, aut_, opt);
break;
case 'L':
spot::print_lbtt(ss, aut_, opt);
break;
}
if (opt)
free(opt);
os << '\'' << string_to_tmp(ss.str(), serial_) << '\'';
}
translator_runner::translator_runner(spot::bdd_dict_ptr dict,
bool no_output_allowed)
: dict(dict)
@ -345,6 +397,42 @@ translator_runner::round_formula(spot::formula f, unsigned serial)
filename_formula.new_round(serial);
}
autproc_runner::autproc_runner(bool no_output_allowed)
{
declare('H', &filename_automaton);
declare('S', &filename_automaton);
declare('L', &filename_automaton);
declare('O', &output);
size_t s = tools.size();
assert(s);
for (size_t n = 0; n < s; ++n)
{
// Check that each translator uses at least one input and
// one output.
std::vector<bool> has(256);
const tool_spec& t = tools[n];
scan(t.cmd, has);
if (!(has['H'] || has['S'] || has['L']))
error(2, 0, "no input %%-sequence in '%s'.\n Use "
"one of %%H,%%S,%%L to indicate the input automaton filename.",
t.spec);
if (!no_output_allowed && !has['O'])
error(2, 0, "no output %%-sequence in '%s'.\n Use "
"%%O to indicate where the automaton is output.",
t.spec);
// Remember the %-sequences used by all tools.
prime(t.cmd);
}
}
void
autproc_runner::round_automaton(spot::const_twa_graph_ptr aut, unsigned serial)
{
filename_automaton.new_round(aut, serial);
}
volatile bool timed_out = false;
unsigned timeout_count = 0;
@ -553,7 +641,7 @@ exec_command(const char* cmd)
int fd0 = open(stdin, O_RDONLY, 0644);
if (fd0 < 0)
error(2, errno, "failed to open '%s'", stdin);
if (dup2(fd0, 0) < 0)
if (dup2(fd0, STDIN_FILENO) < 0)
error(2, errno, "dup2() failed");
if (close(fd0) < 0)
error(2, errno, "close() failed");
@ -563,7 +651,7 @@ exec_command(const char* cmd)
int fd1 = creat(stdout, 0644);
if (fd1 < 0)
error(2, errno, "failed to open '%s'", stdout);
if (dup2(fd1, 1) < 0)
if (dup2(fd1, STDOUT_FILENO) < 0)
error(2, errno, "dup2() failed");
if (close(fd1) < 0)
error(2, errno, "close() failed");
@ -602,6 +690,11 @@ exec_with_timeout(const char* cmd)
if (child_pid == 0)
{
setpgid(0, 0);
// Close stdin so that children may not read our input. We had
// this nice surprise with Seminator, who greedily consumes its
// stdin (which was also ours) even if it does not use it
// because it was given a file.
close(STDIN_FILENO);
exec_command(cmd);
// never reached
return -1;
@ -675,7 +768,7 @@ static int parse_opt_trans(int key, char* arg, struct argp_state*)
switch (key)
{
case 't':
tools.push_back(arg);
tools_push_trans(arg);
break;
case 'T':
timeout = to_pos_int(arg);
@ -685,7 +778,14 @@ static int parse_opt_trans(int key, char* arg, struct argp_state*)
#endif
break;
case OPT_LIST:
show_shorthands();
show_shorthands(std::begin(shorthands_ltl), std::end(shorthands_ltl));
std::cout
<< ("\nAny {name} and directory component is skipped for the purpose "
"of\nmatching those prefixes. So for instance\n "
"'{DRA} ~/mytools/ltl2dstar-0.5.2'\n"
"will be changed into\n "
"'{DRA} ~/mytools/ltl2dstar-0.5.2 --output-format=hoa %[MW]L %O'"
"\n");
exit(0);
case OPT_RELABEL:
opt_relabel = true;
@ -698,3 +798,61 @@ static int parse_opt_trans(int key, char* arg, struct argp_state*)
const struct argp trans_argp = { options, parse_opt_trans, nullptr, nullptr,
nullptr, nullptr, nullptr };
static const argp_option options_aut[] =
{
/**************************************************/
{ nullptr, 0, nullptr, 0, "Specifying tools to call:", 2 },
{ "tool", 't', "COMMANDFMT", 0,
"register one tool to call", 0 },
{ "timeout", 'T', "NUMBER", 0, "kill tools after NUMBER seconds", 0 },
{ "list-shorthands", OPT_LIST, nullptr, 0,
"list availabled shorthands to use in COMMANDFMT", 0},
/**************************************************/
{ nullptr, 0, nullptr, 0,
"COMMANDFMT should specify input and output arguments using the "
"following character sequences:", 3 },
{ "%H,%S,%L", 0, nullptr, OPTION_DOC | OPTION_NO_USAGE,
"filename for the input automaton in (H) HOA, (S) Spin's neverclaim, "
"or (L) LBTT's format", 0 },
{ "%O", 0, nullptr, OPTION_DOC | OPTION_NO_USAGE,
"filename for the automaton output in HOA, never claim, LBTT, or "
"ltl2dstar's format", 0 },
{ "%%", 0, nullptr, OPTION_DOC | OPTION_NO_USAGE, "a single %", 0 },
{ nullptr, 0, nullptr, 0, nullptr, 0 }
};
static int parse_opt_autproc(int key, char* arg, struct argp_state*)
{
switch (key)
{
case 't':
tools_push_autproc(arg);
break;
case 'T':
timeout = to_pos_int(arg);
#if !ENABLE_TIMEOUT
std::cerr << "warning: setting a timeout is not supported "
<< "on your platform" << std::endl;
#endif
break;
case OPT_LIST:
show_shorthands(std::begin(shorthands_autproc),
std::end(shorthands_autproc));
std::cout
<< ("\nAny {name} and directory component is skipped for the purpose "
"of\nmatching those prefixes. So for instance\n "
"'{AF} ~/mytools/autfilt-2.4 --remove-fin'\n"
"will be changed into\n "
"'{AF} ~/mytools/autfilt-2.4 --remove-fin %H>%O'\n");
exit(0);
default:
return ARGP_ERR_UNKNOWN;
}
return 0;
}
const struct argp autproc_argp = { options_aut, parse_opt_autproc, nullptr,
nullptr, nullptr, nullptr, nullptr };

View file

@ -28,9 +28,13 @@
#include <spot/twa/twagraph.hh>
extern const struct argp trans_argp;
extern const struct argp trans_argp; // ltlcross, ltldo
extern const struct argp autproc_argp; // autcross
extern bool opt_relabel;
struct shorthands_t;
struct tool_spec
{
// The translator command, as specified on the command-line.
@ -44,7 +48,7 @@ struct tool_spec
// name of the translator (or spec)
const char* name;
tool_spec(const char* spec);
tool_spec(const char* spec, shorthands_t* begin, shorthands_t* end);
tool_spec(const tool_spec& other);
tool_spec& operator=(const tool_spec& other);
~tool_spec();
@ -52,6 +56,9 @@ struct tool_spec
extern std::vector<tool_spec> tools;
void tools_push_trans(const char* trans);
void tools_push_autproc(const char* proc);
struct quoted_string final: public spot::printable_value<std::string>
{
using spot::printable_value<std::string>::operator=;
@ -80,7 +87,25 @@ struct filed_formula final: public spot::printable
private:
const quoted_formula& f_;
unsigned serial_;
std::string string_to_tmp(const std::string str, unsigned n) const;
};
struct filed_automaton final: public spot::printable
{
filed_automaton()
{
}
void print(std::ostream& os, const char* pos) const override;
void new_round(spot::const_twa_graph_ptr aut, unsigned serial)
{
aut_ = aut;
serial_ = serial;
}
private:
spot::const_twa_graph_ptr aut_;
unsigned serial_;
};
struct printable_result_filename final:
@ -118,6 +143,23 @@ public:
};
class autproc_runner: protected spot::formater
{
protected:
// Round-specific variables
filed_automaton filename_automaton;
// Run-specific variables
printable_result_filename output;
public:
using spot::formater::has;
autproc_runner(// whether we accept the absence of output
// specifier
bool no_output_allowed = false);
void round_automaton(spot::const_twa_graph_ptr aut, unsigned serial);
};
// Disable handling of timeout on systems that miss kill() or alarm().
// For instance MinGW.
#if HAVE_KILL && HAVE_ALARM

View file

@ -219,7 +219,6 @@ example()
return std::cerr;
}
static void
end_error()
{
@ -423,7 +422,7 @@ parse_opt(int key, char* arg, struct argp_state*)
if (arg[0] == '-' && !arg[1])
jobs.emplace_back(arg, true);
else
tools.push_back(arg);
tools_push_trans(arg);
break;
case OPT_AUTOMATA:
opt_automata = true;
@ -1111,7 +1110,7 @@ namespace
std::vector<spot::twa_graph_ptr>& comp,
unsigned i)
{
if (!no_complement && x[i] && is_deterministic(x[i]))
if (!no_complement && x[i] && is_universal(x[i]))
comp[i] = dualize(x[i]);
};

View file

@ -171,7 +171,7 @@ parse_opt(int key, char* arg, struct argp_state*)
if (arg[0] == '-' && !arg[1])
jobs.emplace_back(arg, true);
else
tools.push_back(arg);
tools_push_trans(arg);
break;
default:
return ARGP_ERR_UNKNOWN;

View file

@ -25,6 +25,7 @@ convman7 = ARGP_HELP_FMT=header-col=0 $(SHELL) "$(x_to_1)" \
"$(PERL)" "$(top_srcdir)/tools/help2man -s7 -N"
dist_man1_MANS = \
autcross.1 \
autfilt.1 \
dstar2tgba.1 \
genaut.1 \
@ -44,6 +45,9 @@ dist_man7_MANS = \
MAINTAINERCLEANFILES = $(dist_man1_MANS) $(dist_man7_MANS)
EXTRA_DIST = $(dist_man1_MANS:.1=.x) $(dist_man7_MANS:.7=.x)
autcross.1: $(common_dep) $(srcdir)/autcross.x $(srcdir)/../autcross.cc
$(convman) ../autcross$(EXEEXT) $(srcdir)/autcross.x $@
autfilt.1: $(common_dep) $(srcdir)/autfilt.x $(srcdir)/../autfilt.cc
$(convman) ../autfilt$(EXEEXT) $(srcdir)/autfilt.x $@

97
bin/man/autcross.x Normal file
View file

@ -0,0 +1,97 @@
.\" -*- coding: utf-8 -*-
[NAME]
autcross \- cross-compare tools that process automata
[ENVIRONMENT VARIABLES]
.TP
\fBSPOT_TMPDIR\fR, \fBTMPDIR\fR
These variables control in which directory temporary files (e.g.,
those who contain the input and output when interfacing with
translators) are created. \fBTMPDIR\fR is only read if
\fBSPOT_TMPDIR\fR does not exist. If none of these environment
variables exist, or if their value is empty, files are created in the
current directory.
.TP
\fBSPOT_TMPKEEP\fR
When this variable is defined, temporary files are not removed.
This is mostly useful for debugging.
[OUTPUT DATA]
The following columns are output in the CSV files.
.TP
\fBinput.source\fR
Location of the input automaton fed to the tool.
.TP
\fBinput.name\fR
Name of the input automaton, if any. This is supported
by the HOA format.
.TP
\fBinput.ap\fR,\fBoutput.ap\fR,
Number of atomic proposition in the input and output automata.
.TP
\fBinput.states\fR,\fBoutput.states\fR
Number of states in the input and output automata.
.TP
\fBinput.edges\fR,\fBoutput.edges\fR
Number of edges in the input and output automata.
.TP
\fBinput.transitions\fR,\fBoutput.transitions\fR
Number of transitions in the input and output automata.
.TP
\fBinput.acc_sets\fR,\fBoutput.acc_sets\fR
Number of acceptance sets in the input and output automata.
.TP
\fBinput.scc\fR,\fBoutput.scc\fR
Number of strongly connected components in the input and output automata.
.TP
\fBinput.nondetstates\fR,\fBoutput.nondetstates\fR
Number of nondeterministic states in the input and output automata.
.TP
\fBinput.nondeterministic\fR,\fBoutput.nondetstates\fR
1 if the automaton is nondeterministic, 0 if it is deterministic.
.TP
\fBinput.alternating\fR,\fBoutput.alternating\fR
1 if the automaton has some universal branching, 0 otherwise.
\fBexit_status\fR, \fBexit_code\fR
Information about how the execution of the tool went.
\fBexit_status\fR is a string that can take the following
values:
.RS
.TP
\f(CW"ok"\fR
The tool ran succesfully (this does not imply that the produced
automaton is correct) and autcross could parse the resulting
automaton. In this case \fBexit_code\fR is always 0.
.TP
\f(CW"timeout"\fR
The tool ran for more than the number of seconds
specified with the \fB\-\-timeout\fR option. In this
case \fBexit_code\fR is always -1.
.TP
\f(CW"exit code"\fR
The tool terminated with a non-zero exit code.
\fBexit_code\fR contains that value.
.TP
\f(CW"signal"\fR
The tool terminated with a signal.
\fBexit_code\fR contains that signal's number.
.TP
\f(CW"parse error"\fR
The tool terminated normally, but autcross could not
parse its output. In this case \fBexit_code\fR is always -1.
.TP
\f(CW"no output"\fR
The tool terminated normally, but without creating the specified
output file. In this case \fBexit_code\fR is always -1.
.RE
.TP
\fBtime\fR
A floating point number giving the run time of the tool in seconds.
This is reported for all executions, even failling ones.
[SEE ALSO]
.BR randaut (1),
.BR genaut (1),
.BR autfilt (1),
.BR ltlcross (1)