dtbasat: implement dba_sat_minimize()
* src/tgbaalgos/dtbasat.cc, src/tgbaalgos/dtbasat.hh: New files. * src/tgbaalgos/Makefile.am: Add them. * src/tgbatest/ltl2tgba.cc: Add option -RS.
This commit is contained in:
parent
3fd49da159
commit
d9f3ca71c0
4 changed files with 917 additions and 13 deletions
|
|
@ -37,6 +37,7 @@ tgbaalgos_HEADERS = \
|
||||||
degen.hh \
|
degen.hh \
|
||||||
dottydec.hh \
|
dottydec.hh \
|
||||||
dotty.hh \
|
dotty.hh \
|
||||||
|
dtbasat.hh \
|
||||||
dupexp.hh \
|
dupexp.hh \
|
||||||
eltl2tgba_lacim.hh \
|
eltl2tgba_lacim.hh \
|
||||||
emptiness.hh \
|
emptiness.hh \
|
||||||
|
|
@ -84,6 +85,7 @@ libtgbaalgos_la_SOURCES = \
|
||||||
degen.cc \
|
degen.cc \
|
||||||
dotty.cc \
|
dotty.cc \
|
||||||
dottydec.cc \
|
dottydec.cc \
|
||||||
|
dtbasat.cc \
|
||||||
dupexp.cc \
|
dupexp.cc \
|
||||||
eltl2tgba_lacim.cc \
|
eltl2tgba_lacim.cc \
|
||||||
emptiness.cc \
|
emptiness.cc \
|
||||||
|
|
|
||||||
764
src/tgbaalgos/dtbasat.cc
Normal file
764
src/tgbaalgos/dtbasat.cc
Normal file
|
|
@ -0,0 +1,764 @@
|
||||||
|
// -*- coding: utf-8 -*-
|
||||||
|
// Copyright (C) 2013 Laboratoire de Recherche et Développement
|
||||||
|
// de l'Epita.
|
||||||
|
//
|
||||||
|
// 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 <iostream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include "dtbasat.hh"
|
||||||
|
#include "reachiter.hh"
|
||||||
|
#include <map>
|
||||||
|
#include <utility>
|
||||||
|
#include "scc.hh"
|
||||||
|
#include "tgba/bddprint.hh"
|
||||||
|
#include "ltlast/constant.hh"
|
||||||
|
#include "stats.hh"
|
||||||
|
#include "misc/tmpfile.hh"
|
||||||
|
|
||||||
|
// If the following DEBUG macro is set to 1, the temporary files used
|
||||||
|
// to communicate with the SAT-solver will be left in the current
|
||||||
|
// directory. (The files dtba-sat.cnf and dtba-sat.out contain the
|
||||||
|
// input and output for the last successful minimization attempted, or
|
||||||
|
// for the only failed attempt if the minimization failed.)
|
||||||
|
//
|
||||||
|
// Additionally, the CNF file will be output with a comment before
|
||||||
|
// each clause, and an additional output file (dtba-sat.dbg) will be
|
||||||
|
// created with a list of all positive variables in the result and
|
||||||
|
// their meaning.
|
||||||
|
//
|
||||||
|
// Note that the code use unique temporary filenames, so it is safe to
|
||||||
|
// run several such minimizations in parallel. It only when DEBUG=1
|
||||||
|
// that some of these files will be renamed to the above hard-coded
|
||||||
|
// names, possibly causing confusion if multiple minimizations are
|
||||||
|
// debugged in parallel and in the same directory.
|
||||||
|
|
||||||
|
#define DEBUG 0
|
||||||
|
#if DEBUG
|
||||||
|
#define dout out << "c "
|
||||||
|
#else
|
||||||
|
#define dout while (0) out
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
namespace spot
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
static bdd_dict* debug_dict = 0;
|
||||||
|
|
||||||
|
struct transition
|
||||||
|
{
|
||||||
|
int src;
|
||||||
|
bdd cond;
|
||||||
|
int dst;
|
||||||
|
|
||||||
|
transition(int src, bdd cond, int dst)
|
||||||
|
: src(src), cond(cond), dst(dst)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator<(const transition& other) const
|
||||||
|
{
|
||||||
|
if (this->src < other.src)
|
||||||
|
return true;
|
||||||
|
if (this->src > other.src)
|
||||||
|
return false;
|
||||||
|
if (this->dst < other.dst)
|
||||||
|
return true;
|
||||||
|
if (this->dst > other.dst)
|
||||||
|
return false;
|
||||||
|
return this->cond.id() < other.cond.id();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator==(const transition& other) const
|
||||||
|
{
|
||||||
|
return (this->src == other.src
|
||||||
|
&& this->dst == other.dst
|
||||||
|
&& this->cond.id() == other.cond.id());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct state_pair
|
||||||
|
{
|
||||||
|
int a;
|
||||||
|
int b;
|
||||||
|
|
||||||
|
state_pair(int a, int b)
|
||||||
|
: a(a), b(b)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator<(const state_pair& other) const
|
||||||
|
{
|
||||||
|
if (this->a < other.a)
|
||||||
|
return true;
|
||||||
|
if (this->a > other.a)
|
||||||
|
return false;
|
||||||
|
if (this->b < other.b)
|
||||||
|
return true;
|
||||||
|
if (this->b > other.b)
|
||||||
|
return false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct path
|
||||||
|
{
|
||||||
|
int src_cand;
|
||||||
|
int src_ref;
|
||||||
|
int dst_cand;
|
||||||
|
int dst_ref;
|
||||||
|
|
||||||
|
path(int src_cand, int src_ref,
|
||||||
|
int dst_cand, int dst_ref)
|
||||||
|
: src_cand(src_cand), src_ref(src_ref),
|
||||||
|
dst_cand(dst_cand), dst_ref(dst_ref)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator<(const path& other) const
|
||||||
|
{
|
||||||
|
if (this->src_cand < other.src_cand)
|
||||||
|
return true;
|
||||||
|
if (this->src_cand > other.src_cand)
|
||||||
|
return false;
|
||||||
|
if (this->src_ref < other.src_ref)
|
||||||
|
return true;
|
||||||
|
if (this->src_ref > other.src_ref)
|
||||||
|
return false;
|
||||||
|
if (this->dst_cand < other.dst_cand)
|
||||||
|
return true;
|
||||||
|
if (this->dst_cand > other.dst_cand)
|
||||||
|
return false;
|
||||||
|
if (this->dst_ref < other.dst_ref)
|
||||||
|
return true;
|
||||||
|
if (this->dst_ref > other.dst_ref)
|
||||||
|
return false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream& os, const state_pair& p)
|
||||||
|
{
|
||||||
|
os << "<" << p.a << "," << p.b << ">";
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream& os, const transition& t)
|
||||||
|
{
|
||||||
|
os << "<" << t.src << ","
|
||||||
|
<< bdd_format_formula(debug_dict, t.cond)
|
||||||
|
<< "," << t.dst << ">";
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream& os, const path& p)
|
||||||
|
{
|
||||||
|
os << "<"
|
||||||
|
<< p.src_cand << ","
|
||||||
|
<< p.src_ref << ","
|
||||||
|
<< p.dst_cand << ","
|
||||||
|
<< p.dst_ref << ">";
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct dict
|
||||||
|
{
|
||||||
|
typedef std::map<transition, int> trans_map;
|
||||||
|
trans_map transid;
|
||||||
|
trans_map transacc;
|
||||||
|
typedef std::map<int, transition> rev_map;
|
||||||
|
rev_map revtransid;
|
||||||
|
rev_map revtransacc;
|
||||||
|
|
||||||
|
std::map<state_pair, int> prodid;
|
||||||
|
std::map<path, int> pathid_ref;
|
||||||
|
std::map<path, int> pathid_cand;
|
||||||
|
int nvars;
|
||||||
|
typedef Sgi::hash_map<const state*, int,
|
||||||
|
state_ptr_hash, state_ptr_equal> state_map;
|
||||||
|
typedef Sgi::hash_map<int, const state*> int_map;
|
||||||
|
state_map state_to_int;
|
||||||
|
int_map int_to_state;
|
||||||
|
int cand_size;
|
||||||
|
|
||||||
|
~dict()
|
||||||
|
{
|
||||||
|
state_map::const_iterator s = state_to_int.begin();
|
||||||
|
while (s != state_to_int.end())
|
||||||
|
// Always advance the iterator before deleting the key.
|
||||||
|
s++->first->destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class filler_dfs: public tgba_reachable_iterator_depth_first
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
dict& d;
|
||||||
|
int size_;
|
||||||
|
bdd ap_;
|
||||||
|
public:
|
||||||
|
filler_dfs(const tgba* aut, dict& d, bdd ap)
|
||||||
|
:tgba_reachable_iterator_depth_first(aut), d(d), ap_(ap)
|
||||||
|
{
|
||||||
|
d.nvars = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int size()
|
||||||
|
{
|
||||||
|
return size_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void end()
|
||||||
|
{
|
||||||
|
size_ = seen.size();
|
||||||
|
|
||||||
|
if (d.cand_size == -1)
|
||||||
|
d.cand_size = size_ - 1;
|
||||||
|
|
||||||
|
int seen_size = seen.size();
|
||||||
|
for (int i = 1; i <= seen_size; ++i)
|
||||||
|
{
|
||||||
|
for (int j = 1; j <= d.cand_size; ++j)
|
||||||
|
{
|
||||||
|
d.prodid[state_pair(j, i)] = ++d.nvars;
|
||||||
|
|
||||||
|
for (int k = 1; k <= seen_size; ++k)
|
||||||
|
for (int l = 1; l <= d.cand_size; ++l)
|
||||||
|
{
|
||||||
|
path p(j, i, l, k);
|
||||||
|
d.pathid_ref[p] = ++d.nvars;
|
||||||
|
d.pathid_cand[p] = ++d.nvars;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (dict::state_map::const_iterator i = seen.begin();
|
||||||
|
i != seen.end(); ++i)
|
||||||
|
{
|
||||||
|
d.int_to_state[i->second] = i->first;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::swap(d.state_to_int, seen);
|
||||||
|
|
||||||
|
for (int i = 1; i <= d.cand_size; ++i)
|
||||||
|
for (int j = 1; j <= d.cand_size; ++j)
|
||||||
|
{
|
||||||
|
bdd all = bddtrue;
|
||||||
|
while (all != bddfalse)
|
||||||
|
{
|
||||||
|
bdd one = bdd_satoneset(all, ap_, bddfalse);
|
||||||
|
all -= one;
|
||||||
|
|
||||||
|
transition t(i, one, j);
|
||||||
|
d.transid[t] = ++d.nvars;
|
||||||
|
d.revtransid.insert(dict::rev_map::value_type(d.nvars, t));
|
||||||
|
d.transacc[t] = ++d.nvars;
|
||||||
|
d.revtransacc.insert(dict::rev_map::value_type(d.nvars, t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static
|
||||||
|
void dtba_to_sat(std::ostream& out, const tgba* ref, dict& d)
|
||||||
|
{
|
||||||
|
int nclauses = 0;
|
||||||
|
int ref_size = 0;
|
||||||
|
|
||||||
|
scc_map sm(ref);
|
||||||
|
sm.build_map();
|
||||||
|
bdd ap = sm.aprec_set_of(sm.initial());
|
||||||
|
|
||||||
|
// Number all the SAT variable we may need.
|
||||||
|
{
|
||||||
|
filler_dfs f(ref, d, ap);
|
||||||
|
f.run();
|
||||||
|
ref_size = f.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty automaton is impossible
|
||||||
|
if (d.cand_size == 0)
|
||||||
|
{
|
||||||
|
out << "p cnf 1 2\n-1 0\n1 0\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An empty line for the header
|
||||||
|
out << " \n";
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
debug_dict = ref->get_dict();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
dout << "(1) the candidate automaton is complete\n";
|
||||||
|
for (int q1 = 1; q1 <= d.cand_size; ++q1)
|
||||||
|
{
|
||||||
|
bdd all = bddtrue;
|
||||||
|
while (all != bddfalse)
|
||||||
|
{
|
||||||
|
bdd s = bdd_satoneset(all, ap, bddfalse);
|
||||||
|
all -= s;
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
dout;
|
||||||
|
for (int q2 = 1; q2 <= d.cand_size; q2++)
|
||||||
|
{
|
||||||
|
transition t(q1, s, q2);
|
||||||
|
out << t << "δ";
|
||||||
|
if (q2 != d.cand_size)
|
||||||
|
out << " ∨ ";
|
||||||
|
}
|
||||||
|
out << "\n";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
for (int q2 = 1; q2 <= d.cand_size; q2++)
|
||||||
|
{
|
||||||
|
transition t(q1, s, q2);
|
||||||
|
int ti = d.transid[t];
|
||||||
|
|
||||||
|
out << ti << " ";
|
||||||
|
}
|
||||||
|
out << "0\n";
|
||||||
|
|
||||||
|
++nclauses;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dout << "(2) the initial state is reachable\n";
|
||||||
|
dout << state_pair(1, 1) << "\n";
|
||||||
|
out << d.prodid[state_pair(1, 1)] << " 0\n";
|
||||||
|
++nclauses;
|
||||||
|
|
||||||
|
for (std::map<state_pair, int>::const_iterator pit = d.prodid.begin();
|
||||||
|
pit != d.prodid.end(); ++pit)
|
||||||
|
{
|
||||||
|
int q1 = pit->first.a;
|
||||||
|
int q1p = pit->first.b;
|
||||||
|
|
||||||
|
dout << "(2) states Cand[" << q1 << "] and Ref[" << q1p
|
||||||
|
<< "] are 0-length paths\n";
|
||||||
|
path p(q1, q1p, q1, q1p);
|
||||||
|
dout << pit->first << " → (" << p << "R ∧ " << p << "C)\n";
|
||||||
|
out << -pit->second << " " << d.pathid_ref[p] <<" 0\n";
|
||||||
|
out << -pit->second << " " << d.pathid_cand[p] <<" 0\n";
|
||||||
|
nclauses += 2;
|
||||||
|
|
||||||
|
dout << "(3) augmenting paths based on Cand[" << q1
|
||||||
|
<< "] and Ref[" << q1p << "]\n";
|
||||||
|
tgba_succ_iterator* it = ref->succ_iter(d.int_to_state[q1p]);
|
||||||
|
for (it->first(); !it->done(); it->next())
|
||||||
|
{
|
||||||
|
const state* dps = it->current_state();
|
||||||
|
int dp = d.state_to_int[dps];
|
||||||
|
dps->destroy();
|
||||||
|
|
||||||
|
bdd all = it->current_condition();
|
||||||
|
while (all != bddfalse)
|
||||||
|
{
|
||||||
|
bdd s = bdd_satoneset(all, ap, bddfalse);
|
||||||
|
all -= s;
|
||||||
|
|
||||||
|
for (int q2 = 1; q2 <= d.cand_size; q2++)
|
||||||
|
{
|
||||||
|
transition t(q1, s, q2);
|
||||||
|
int ti = d.transid[t];
|
||||||
|
|
||||||
|
state_pair p2(q2, dp);
|
||||||
|
int succ = d.prodid[p2];
|
||||||
|
|
||||||
|
dout << pit->first << " ∧ " << t << "δ → " << p2 << "\n";
|
||||||
|
out << -pit->second << " " << -ti << " "
|
||||||
|
<< succ << " 0\n";
|
||||||
|
++nclauses;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete it;
|
||||||
|
}
|
||||||
|
|
||||||
|
bdd all_acc = ref->all_acceptance_conditions();
|
||||||
|
|
||||||
|
// construction of contraints (4,5) : all loops in the product
|
||||||
|
// where no accepting run is detected in the ref. automaton,
|
||||||
|
// must also be marked as not accepting in the cand. automaton
|
||||||
|
for (int q1 = 1; q1 <= d.cand_size; ++q1)
|
||||||
|
for (int q1p = 1; q1p <= ref_size; ++q1p)
|
||||||
|
{
|
||||||
|
for (int q2 = 1; q2 <= d.cand_size; ++q2)
|
||||||
|
for (int q2p = 1; q2p <= ref_size; ++q2p)
|
||||||
|
{
|
||||||
|
path p1(q1, q1p, q2, q2p);
|
||||||
|
|
||||||
|
dout << "(4&5) matching paths from reference based on "
|
||||||
|
<< p1 << "\n";
|
||||||
|
|
||||||
|
int pid1 = d.pathid_ref[p1];
|
||||||
|
|
||||||
|
tgba_succ_iterator* it = ref->succ_iter(d.int_to_state[q2p]);
|
||||||
|
for (it->first(); !it->done(); it->next())
|
||||||
|
{
|
||||||
|
const state* dps = it->current_state();
|
||||||
|
int dp = d.state_to_int[dps];
|
||||||
|
dps->destroy();
|
||||||
|
if (it->current_acceptance_conditions() == all_acc)
|
||||||
|
continue;
|
||||||
|
for (int q3 = 1; q3 <= d.cand_size; ++q3)
|
||||||
|
{
|
||||||
|
if (dp == q1p && q3 == q1) // (4) looping
|
||||||
|
{
|
||||||
|
bdd all = it->current_condition();
|
||||||
|
while (all != bddfalse)
|
||||||
|
{
|
||||||
|
bdd s = bdd_satoneset(all, ap, bddfalse);
|
||||||
|
all -= s;
|
||||||
|
|
||||||
|
transition t(q2, s, q1);
|
||||||
|
int ti = d.transid[t];
|
||||||
|
int ta = d.transacc[t];
|
||||||
|
|
||||||
|
dout << p1 << "R ∧ " << t << "δ → ¬" << t
|
||||||
|
<< "F\n";
|
||||||
|
out << -pid1 << " " << -ti << " "
|
||||||
|
<< -ta << " 0\n";
|
||||||
|
++nclauses;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
else // (5) not looping
|
||||||
|
{
|
||||||
|
path p2 = path(q1, q1p, q3, dp);
|
||||||
|
int pid2 = d.pathid_ref[p2];
|
||||||
|
|
||||||
|
bdd all = it->current_condition();
|
||||||
|
while (all != bddfalse)
|
||||||
|
{
|
||||||
|
bdd s = bdd_satoneset(all, ap, bddfalse);
|
||||||
|
all -= s;
|
||||||
|
|
||||||
|
transition t(q2, s, q3);
|
||||||
|
int ti = d.transid[t];
|
||||||
|
|
||||||
|
dout << p1 << "R ∧ " << t << "δ → " << p2
|
||||||
|
<< "R\n";
|
||||||
|
out << -pid1 << " " << -ti << " "
|
||||||
|
<< pid2 << " 0\n";
|
||||||
|
++nclauses;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// construction of contraints (6,7): all loops in the product
|
||||||
|
// where accepting run is detected in the ref. automaton, must
|
||||||
|
// also be marked as accepting in the candidate.
|
||||||
|
for (int q1 = 1; q1 <= d.cand_size; ++q1)
|
||||||
|
for (int q1p = 1; q1p <= ref_size; ++q1p)
|
||||||
|
{
|
||||||
|
for (int q2 = 1; q2 <= d.cand_size; ++q2)
|
||||||
|
for (int q2p = 1; q2p <= ref_size; ++q2p)
|
||||||
|
{
|
||||||
|
path p1(q1, q1p, q2, q2p);
|
||||||
|
dout << "(6&7) matching paths from candidate based on "
|
||||||
|
<< p1 << "\n";
|
||||||
|
int pid1 = d.pathid_cand[p1];
|
||||||
|
|
||||||
|
tgba_succ_iterator* it = ref->succ_iter(d.int_to_state[q2p]);
|
||||||
|
for (it->first(); !it->done(); it->next())
|
||||||
|
{
|
||||||
|
const state* dps = it->current_state();
|
||||||
|
int dp = d.state_to_int[dps];
|
||||||
|
dps->destroy();
|
||||||
|
for (int q3 = 1; q3 <= d.cand_size; q3++)
|
||||||
|
{
|
||||||
|
if (dp == q1p && q3 == q1) // (6) looping
|
||||||
|
{
|
||||||
|
// We only care about the looping case if
|
||||||
|
// it is accepting in the reference.
|
||||||
|
if (it->current_acceptance_conditions()
|
||||||
|
!= all_acc)
|
||||||
|
continue;
|
||||||
|
bdd all = it->current_condition();
|
||||||
|
while (all != bddfalse)
|
||||||
|
{
|
||||||
|
bdd s = bdd_satoneset(all, ap, bddfalse);
|
||||||
|
all -= s;
|
||||||
|
|
||||||
|
transition t(q2, s, q1);
|
||||||
|
int ti = d.transid[t];
|
||||||
|
int ta = d.transacc[t];
|
||||||
|
|
||||||
|
dout << p1 << "C ∧ " << t << "δ → " << t
|
||||||
|
<< "F\n";
|
||||||
|
out << -pid1 << " " << -ti << " " << ta
|
||||||
|
<< " 0\n";
|
||||||
|
++nclauses;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // (7) no loop
|
||||||
|
{
|
||||||
|
path p2 = path(q1, q1p, q3, dp);
|
||||||
|
int pid2 = d.pathid_cand[p2];
|
||||||
|
|
||||||
|
bdd all = it->current_condition();
|
||||||
|
while (all != bddfalse)
|
||||||
|
{
|
||||||
|
bdd s = bdd_satoneset(all, ap, bddfalse);
|
||||||
|
all -= s;
|
||||||
|
|
||||||
|
transition t(q2, s, q3);
|
||||||
|
int ti = d.transid[t];
|
||||||
|
int ta = d.transacc[t];
|
||||||
|
|
||||||
|
dout << p1 << "C ∧ " << t << "δ ∧ ¬"
|
||||||
|
<< t << "F → " << p2 << "C\n";
|
||||||
|
|
||||||
|
out << -pid1 << " " << -ti << " "
|
||||||
|
<< ta << " " << pid2 << " 0\n";
|
||||||
|
++nclauses;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.seekp(0);
|
||||||
|
out << "p cnf " << d.nvars << " " << nclauses;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string
|
||||||
|
get_solution(const char* filename)
|
||||||
|
{
|
||||||
|
std::fstream in(filename, std::ios_base::in);
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(in, line))
|
||||||
|
{
|
||||||
|
if (line.empty() || line[0] == 'c')
|
||||||
|
continue;
|
||||||
|
if (line[0] == 'v')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (line[0] == 'v')
|
||||||
|
return line.c_str() + 1;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static tgba_explicit_number*
|
||||||
|
sat_build(const std::string& solution, dict& satdict, const tgba* aut)
|
||||||
|
{
|
||||||
|
bdd_dict* autdict = aut->get_dict();
|
||||||
|
tgba_explicit_number* a = new tgba_explicit_number(autdict);
|
||||||
|
autdict->register_all_variables_of(aut, a);
|
||||||
|
|
||||||
|
const ltl::formula* t = ltl::constant::true_instance();
|
||||||
|
bdd acc = bdd_ithvar(autdict->register_acceptance_variable(t, a));
|
||||||
|
a->set_acceptance_conditions(acc);
|
||||||
|
|
||||||
|
for (int s = 1; s < satdict.cand_size; ++s)
|
||||||
|
a->add_state(s);
|
||||||
|
|
||||||
|
std::stringstream sol(solution);
|
||||||
|
state_explicit_number::transition* last_aut_trans = 0;
|
||||||
|
const transition* last_sat_trans = 0;
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
std::fstream out("dtba-sat.dbg",
|
||||||
|
std::ios_base::trunc | std::ios_base::out);
|
||||||
|
std::set<int> positive;
|
||||||
|
#else
|
||||||
|
// "out" is not used, but it has to exist for the dout() macro to
|
||||||
|
// compile.
|
||||||
|
std::ostream& out(std::cout);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
dout << "--- transition variables ---\n";
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
int v;
|
||||||
|
sol >> v;
|
||||||
|
if (!sol)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (v < 0) // FIXME: maybe we can have (v < NNN)?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
positive.insert(v);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
dict::rev_map::const_iterator t = satdict.revtransid.find(v);
|
||||||
|
|
||||||
|
if (t != satdict.revtransid.end())
|
||||||
|
{
|
||||||
|
last_aut_trans = a->create_transition(t->second.src,
|
||||||
|
t->second.dst);
|
||||||
|
last_aut_trans->condition = t->second.cond;
|
||||||
|
last_sat_trans = &t->second;
|
||||||
|
|
||||||
|
dout << v << "\t" << t->second << "δ\n";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
t = satdict.revtransacc.find(v);
|
||||||
|
// This assumes that the sat solvers output variables in
|
||||||
|
// increasing order.
|
||||||
|
if (t != satdict.revtransacc.end())
|
||||||
|
{
|
||||||
|
dout << v << "\t" << t->second << "F\n";
|
||||||
|
|
||||||
|
if (last_sat_trans && t->second == *last_sat_trans)
|
||||||
|
last_aut_trans->acceptance_conditions = acc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
dout << "--- state_pair variables ---\n";
|
||||||
|
for (std::map<state_pair, int>::const_iterator pit =
|
||||||
|
satdict.prodid.begin(); pit != satdict.prodid.end(); ++pit)
|
||||||
|
if (positive.find(pit->second) != positive.end())
|
||||||
|
dout << pit->second << "\t" << pit->first << "\n";
|
||||||
|
|
||||||
|
dout << "--- pathid_cand variables ---\n";
|
||||||
|
for (std::map<path, int>::const_iterator pit =
|
||||||
|
satdict.pathid_cand.begin();
|
||||||
|
pit != satdict.pathid_cand.end(); ++pit)
|
||||||
|
if (positive.find(pit->second) != positive.end())
|
||||||
|
dout << pit->second << "\t" << pit->first << "C\n";
|
||||||
|
|
||||||
|
dout << "--- pathid_ref variables ---\n";
|
||||||
|
for (std::map<path, int>::const_iterator pit =
|
||||||
|
satdict.pathid_ref.begin();
|
||||||
|
pit != satdict.pathid_ref.end(); ++pit)
|
||||||
|
if (positive.find(pit->second) != positive.end())
|
||||||
|
dout << pit->second << "\t" << pit->first << "R\n";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
a->merge_transitions();
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
xrename(const char* from, const char* to)
|
||||||
|
{
|
||||||
|
if (!rename(from, to))
|
||||||
|
return false;
|
||||||
|
std::ostringstream msg;
|
||||||
|
msg << "cannot rename " << from << " to " << to;
|
||||||
|
perror(msg.str().c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tgba_explicit_number*
|
||||||
|
dba_sat_minimize(const tgba* a, int target_state_number)
|
||||||
|
{
|
||||||
|
int ref_states =
|
||||||
|
target_state_number == -1
|
||||||
|
? stats_reachable(a).states - 1
|
||||||
|
: target_state_number;
|
||||||
|
|
||||||
|
std::string current_solution;
|
||||||
|
std::string last_solution;
|
||||||
|
dict* last = 0;
|
||||||
|
dict* current = 0;
|
||||||
|
temporary_file* cnf = 0;
|
||||||
|
temporary_file* out = 0;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (DEBUG && current)
|
||||||
|
{
|
||||||
|
xrename(out->name(), "dtba-sat.out");
|
||||||
|
xrename(cnf->name(), "dtba-sat.cnf");
|
||||||
|
}
|
||||||
|
delete out;
|
||||||
|
delete cnf;
|
||||||
|
std::swap(current_solution, last_solution);
|
||||||
|
delete last;
|
||||||
|
last = current;
|
||||||
|
current = new dict;
|
||||||
|
current->cand_size = ref_states--;
|
||||||
|
|
||||||
|
cnf = create_tmpfile("dtba-sat-", ".cnf");
|
||||||
|
|
||||||
|
// FIXME: we should use proper temporary names
|
||||||
|
std::fstream cnfs(cnf->name(),
|
||||||
|
std::ios_base::trunc | std::ios_base::out);
|
||||||
|
dtba_to_sat(cnfs, a, *current);
|
||||||
|
cnfs.close();
|
||||||
|
|
||||||
|
out = create_tmpfile("dtba-sat-", ".out");
|
||||||
|
|
||||||
|
const char* satsolver = getenv("SATSOLVER");
|
||||||
|
if (!satsolver)
|
||||||
|
satsolver = "glucose";
|
||||||
|
|
||||||
|
std::string s(satsolver);
|
||||||
|
s += " ";
|
||||||
|
s += cnf->name();
|
||||||
|
s += " > ";
|
||||||
|
s += out->name();
|
||||||
|
system(s.c_str());
|
||||||
|
|
||||||
|
current_solution = get_solution(out->name());
|
||||||
|
}
|
||||||
|
while (target_state_number == -1 && !current_solution.empty());
|
||||||
|
|
||||||
|
if (target_state_number != -1)
|
||||||
|
{
|
||||||
|
std::swap(current_solution, last_solution);
|
||||||
|
last = current;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
delete current;
|
||||||
|
}
|
||||||
|
|
||||||
|
tgba_explicit_number* res;
|
||||||
|
if (last == 0)
|
||||||
|
{
|
||||||
|
res = 0;
|
||||||
|
if (DEBUG)
|
||||||
|
{
|
||||||
|
xrename(out->name(), "dtba-sat.out");
|
||||||
|
xrename(cnf->name(), "dtba-sat.cnf");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
res = sat_build(last_solution, *last, a);
|
||||||
|
delete last;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete out;
|
||||||
|
delete cnf;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
46
src/tgbaalgos/dtbasat.hh
Normal file
46
src/tgbaalgos/dtbasat.hh
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
// -*- coding: utf-8 -*-
|
||||||
|
// Copyright (C) 2013 Laboratoire de Recherche et Développement
|
||||||
|
// de l'Epita.
|
||||||
|
//
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
|
#ifndef SPOT_TGBAALGOS_DTBASAT_HH
|
||||||
|
# define SPOT_TGBAALGOS_DTBASAT_HH
|
||||||
|
|
||||||
|
#include <iosfwd>
|
||||||
|
#include "tgba/tgba.hh"
|
||||||
|
#include "tgba/tgbaexplicit.hh"
|
||||||
|
|
||||||
|
namespace spot
|
||||||
|
{
|
||||||
|
/// \brief Attempt to reduce a deterministic TBA with a SAT solver.
|
||||||
|
///
|
||||||
|
/// \param a the TGBA to reduce. It should have only one acceptance
|
||||||
|
/// set and be deterministic. I.e., it should be a deterministic TBA.
|
||||||
|
///
|
||||||
|
/// \param target_state_number the expected number of states wanted
|
||||||
|
/// in the resulting automaton. If \a target_state_number is left
|
||||||
|
/// to its default value of -1, this function will attempt to build
|
||||||
|
/// the smallest possible deterministic TBA is can produce.
|
||||||
|
///
|
||||||
|
/// If no automaton with \a target_state_number states is found, or
|
||||||
|
/// (in case <code>target_state_number == -1</code>) if no smaller
|
||||||
|
/// automaton is found, then a null pointer is returned.
|
||||||
|
SPOT_API tgba_explicit_number*
|
||||||
|
dba_sat_minimize(const tgba* a, int target_state_number = -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // SPOT_TGBAALGOS_DTBASAT_HH
|
||||||
|
|
@ -71,6 +71,8 @@
|
||||||
#include "tgbaalgos/compsusp.hh"
|
#include "tgbaalgos/compsusp.hh"
|
||||||
#include "tgbaalgos/powerset.hh"
|
#include "tgbaalgos/powerset.hh"
|
||||||
#include "tgbaalgos/dbacomp.hh"
|
#include "tgbaalgos/dbacomp.hh"
|
||||||
|
#include "tgbaalgos/complete.hh"
|
||||||
|
#include "tgbaalgos/dtbasat.hh"
|
||||||
|
|
||||||
#include "taalgos/tgba2ta.hh"
|
#include "taalgos/tgba2ta.hh"
|
||||||
#include "taalgos/dotty.hh"
|
#include "taalgos/dotty.hh"
|
||||||
|
|
@ -402,7 +404,7 @@ main(int argc, char** argv)
|
||||||
bool spin_comments = false;
|
bool spin_comments = false;
|
||||||
spot::ltl::environment& env(spot::ltl::default_environment::instance());
|
spot::ltl::environment& env(spot::ltl::default_environment::instance());
|
||||||
spot::ltl::atomic_prop_set* unobservables = 0;
|
spot::ltl::atomic_prop_set* unobservables = 0;
|
||||||
spot::tgba* system = 0;
|
spot::tgba* system_aut = 0;
|
||||||
const spot::tgba* product_to_free = 0;
|
const spot::tgba* product_to_free = 0;
|
||||||
spot::bdd_dict* dict = new spot::bdd_dict();
|
spot::bdd_dict* dict = new spot::bdd_dict();
|
||||||
spot::timer_map tm;
|
spot::timer_map tm;
|
||||||
|
|
@ -428,6 +430,8 @@ main(int argc, char** argv)
|
||||||
bool cs_nosimul = true;
|
bool cs_nosimul = true;
|
||||||
bool cs_early_start = false;
|
bool cs_early_start = false;
|
||||||
bool cs_oblig = false;
|
bool cs_oblig = false;
|
||||||
|
bool opt_complete = false;
|
||||||
|
int opt_dtbasat = -1;
|
||||||
|
|
||||||
for (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
|
|
@ -596,8 +600,8 @@ main(int argc, char** argv)
|
||||||
tm.start("reading -KP's argument");
|
tm.start("reading -KP's argument");
|
||||||
|
|
||||||
spot::kripke_parse_error_list pel;
|
spot::kripke_parse_error_list pel;
|
||||||
system = spot::kripke_parse(argv[formula_index] + 3,
|
system_aut = spot::kripke_parse(argv[formula_index] + 3,
|
||||||
pel, dict, env, debug_opt);
|
pel, dict, env, debug_opt);
|
||||||
if (spot::format_kripke_parse_errors(std::cerr,
|
if (spot::format_kripke_parse_errors(std::cerr,
|
||||||
argv[formula_index] + 2, pel))
|
argv[formula_index] + 2, pel))
|
||||||
return 2;
|
return 2;
|
||||||
|
|
@ -678,7 +682,7 @@ main(int argc, char** argv)
|
||||||
return 2;
|
return 2;
|
||||||
s->merge_transitions();
|
s->merge_transitions();
|
||||||
tm.stop("reading -P's argument");
|
tm.stop("reading -P's argument");
|
||||||
system = s;
|
system_aut = s;
|
||||||
}
|
}
|
||||||
else if (!strcmp(argv[formula_index], "-r"))
|
else if (!strcmp(argv[formula_index], "-r"))
|
||||||
{
|
{
|
||||||
|
|
@ -764,6 +768,10 @@ main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
simpcache_stats = true;
|
simpcache_stats = true;
|
||||||
}
|
}
|
||||||
|
else if (!strcmp(argv[formula_index], "-RC"))
|
||||||
|
{
|
||||||
|
opt_complete = true;
|
||||||
|
}
|
||||||
else if (!strcmp(argv[formula_index], "-RDS"))
|
else if (!strcmp(argv[formula_index], "-RDS"))
|
||||||
{
|
{
|
||||||
reduction_dir_sim = true;
|
reduction_dir_sim = true;
|
||||||
|
|
@ -805,6 +813,14 @@ main(int argc, char** argv)
|
||||||
if (argv[formula_index][3] != 0)
|
if (argv[formula_index][3] != 0)
|
||||||
opt_determinize_threshold = to_int(argv[formula_index] + 3);
|
opt_determinize_threshold = to_int(argv[formula_index] + 3);
|
||||||
}
|
}
|
||||||
|
else if (!strncmp(argv[formula_index], "-RS", 3))
|
||||||
|
{
|
||||||
|
if (argv[formula_index][3] != 0)
|
||||||
|
opt_dtbasat = to_int(argv[formula_index] + 3);
|
||||||
|
else
|
||||||
|
opt_dtbasat = -1;
|
||||||
|
//output = -1;
|
||||||
|
}
|
||||||
else if (!strcmp(argv[formula_index], "-RT"))
|
else if (!strcmp(argv[formula_index], "-RT"))
|
||||||
{
|
{
|
||||||
opt_bisim_ta = true;
|
opt_bisim_ta = true;
|
||||||
|
|
@ -1435,6 +1451,17 @@ main(int argc, char** argv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spot::tgba* determinized = 0;
|
||||||
|
if (opt_determinize && a->number_of_acceptance_conditions() <= 1
|
||||||
|
&& (!f || f->is_syntactic_recurrence()))
|
||||||
|
{
|
||||||
|
tm.start("determinization 2");
|
||||||
|
determinized = tba_determinize(a, 0, opt_determinize_threshold);
|
||||||
|
tm.stop("determinization 2");
|
||||||
|
if (determinized)
|
||||||
|
a = determinized;
|
||||||
|
}
|
||||||
|
|
||||||
const spot::tgba* monitor = 0;
|
const spot::tgba* monitor = 0;
|
||||||
if (opt_monitor)
|
if (opt_monitor)
|
||||||
{
|
{
|
||||||
|
|
@ -1446,15 +1473,55 @@ main(int argc, char** argv)
|
||||||
// pointless.
|
// pointless.
|
||||||
}
|
}
|
||||||
|
|
||||||
spot::tgba* determinized = 0;
|
if (degeneralized || determinized)
|
||||||
if (opt_determinize && a->number_of_acceptance_conditions() <= 1
|
{
|
||||||
&& (!f || f->is_syntactic_recurrence()))
|
if (reduction_dir_sim && !reduction_iterated_sim)
|
||||||
|
{
|
||||||
|
tm.start("direct simulation 2");
|
||||||
|
spot::tgba* tmp = spot::simulation(a);
|
||||||
|
delete temp_dir_sim;
|
||||||
|
a = temp_dir_sim = tmp;
|
||||||
|
tm.stop("direct simulation 2");
|
||||||
|
assume_sba = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reduction_rev_sim && !reduction_iterated_sim)
|
||||||
|
{
|
||||||
|
tm.start("reverse simulation 2");
|
||||||
|
spot::tgba* tmp = spot::cosimulation(a);
|
||||||
|
delete temp_rev_sim;
|
||||||
|
a = temp_rev_sim = tmp;
|
||||||
|
tm.stop("reverse simulation 2");
|
||||||
|
assume_sba = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reduction_iterated_sim)
|
||||||
|
{
|
||||||
|
tm.start("Reduction w/ iterated simulations");
|
||||||
|
spot::tgba* tmp = spot::iterated_simulations(a);
|
||||||
|
delete temp_iterated_sim;
|
||||||
|
a = temp_iterated_sim = tmp;
|
||||||
|
tm.stop("Reduction w/ iterated simulations");
|
||||||
|
assume_sba = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spot::tgba* completed = 0;
|
||||||
|
if (opt_complete)
|
||||||
{
|
{
|
||||||
tm.start("determinization");
|
tm.start("determinization");
|
||||||
determinized = tba_determinize(a, 0, opt_determinize_threshold);
|
a = completed = tgba_complete(a);
|
||||||
tm.stop("determinization");
|
tm.stop("determinization");
|
||||||
if (determinized)
|
}
|
||||||
a = determinized;
|
|
||||||
|
spot::tgba* satminimized = 0;
|
||||||
|
if (opt_dtbasat >= 0)
|
||||||
|
{
|
||||||
|
tm.start("dtbasat");
|
||||||
|
satminimized = dba_sat_minimize(a, opt_dtbasat);
|
||||||
|
tm.stop("dtbasat");
|
||||||
|
if (satminimized)
|
||||||
|
a = satminimized;
|
||||||
}
|
}
|
||||||
|
|
||||||
spot::tgba* complemented = 0;
|
spot::tgba* complemented = 0;
|
||||||
|
|
@ -1465,6 +1532,29 @@ main(int argc, char** argv)
|
||||||
tm.stop("DBA complement");
|
tm.stop("DBA complement");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (complemented || satminimized || determinized)
|
||||||
|
{
|
||||||
|
if (scc_filter && (reduction_dir_sim || reduction_rev_sim))
|
||||||
|
{
|
||||||
|
tm.start("SCC-filter post-sim");
|
||||||
|
delete aut_scc;
|
||||||
|
// Do not filter_all for SBA
|
||||||
|
aut_scc = a = spot::scc_filter(a, assume_sba ?
|
||||||
|
false : scc_filter_all);
|
||||||
|
tm.stop("SCC-filter post-sim");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opt_monitor)
|
||||||
|
{
|
||||||
|
tm.start("Monitor minimization");
|
||||||
|
minimized = a = minimize_monitor(a);
|
||||||
|
tm.stop("Monitor minimization");
|
||||||
|
assume_sba = false; // All states are accepting, so double
|
||||||
|
// circles in the dot output are
|
||||||
|
// pointless.
|
||||||
|
}
|
||||||
|
|
||||||
const spot::tgba* expl = 0;
|
const spot::tgba* expl = 0;
|
||||||
switch (dupexp)
|
switch (dupexp)
|
||||||
{
|
{
|
||||||
|
|
@ -1569,9 +1659,9 @@ main(int argc, char** argv)
|
||||||
|
|
||||||
spot::tgba* product_degeneralized = 0;
|
spot::tgba* product_degeneralized = 0;
|
||||||
|
|
||||||
if (system)
|
if (system_aut)
|
||||||
{
|
{
|
||||||
product_to_free = a = new spot::tgba_product(system, a);
|
product_to_free = a = new spot::tgba_product(system_aut, a);
|
||||||
|
|
||||||
assume_sba = false;
|
assume_sba = false;
|
||||||
|
|
||||||
|
|
@ -1921,12 +2011,14 @@ main(int argc, char** argv)
|
||||||
f->destroy();
|
f->destroy();
|
||||||
delete product_degeneralized;
|
delete product_degeneralized;
|
||||||
delete product_to_free;
|
delete product_to_free;
|
||||||
delete system;
|
delete system_aut;
|
||||||
delete expl;
|
delete expl;
|
||||||
delete monitor;
|
delete monitor;
|
||||||
delete minimized;
|
delete minimized;
|
||||||
|
delete satminimized;
|
||||||
delete degeneralized;
|
delete degeneralized;
|
||||||
delete determinized;
|
delete determinized;
|
||||||
|
delete completed;
|
||||||
delete complemented;
|
delete complemented;
|
||||||
delete aut_scc;
|
delete aut_scc;
|
||||||
delete to_free;
|
delete to_free;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue