diff --git a/.gitignore b/.gitignore
index 155a9b5e7..73745a48f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -82,3 +82,4 @@ GTAGS
*.dsc
*.gcov
spot.spec
+default.nix
diff --git a/Makefile.am b/Makefile.am
index e198a977c..a5d842b4c 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -65,7 +65,8 @@ EXTRA_DIST = HACKING ChangeLog.1 tools/gitlog-to-changelog \
tools/help2man tools/man2html.pl \
tools/test-driver-teamcity $(UTF8) $(DEBIAN) \
m4/gnulib-cache.m4 .dir-locals.el \
- spot.spec spot.spec.in README.ltsmin
+ spot.spec spot.spec.in README.ltsmin \
+ default.nix default.nix.in
dist-hook: gen-ChangeLog
@@ -111,3 +112,6 @@ deb: dist
spot.spec: configure.ac spot.spec.in
sed 's/[@]VERSION[@]/$(VERSION)/;s/[@]GITPATCH[@]/@@@$(GITPATCH)/;s/@@@\.//' spot.spec.in > $@.tmp && mv $@.tmp $@
+
+default.nix: configure.ac default.nix.in
+ sed 's/[@]VERSION[@]/$(VERSION)/' default.nix.in > $@.tmp && mv $@.tmp $@
diff --git a/bin/common_post.cc b/bin/common_post.cc
index fdb1dc903..7bead8135 100644
--- a/bin/common_post.cc
+++ b/bin/common_post.cc
@@ -231,7 +231,14 @@ parse_opt_post(int key, char* arg, struct argp_state*)
if (arg)
type = XARGMATCH(key == 'P' ? "--parity" : "--colored-parity",
arg, parity_args, parity_types);
- else
+ else if (!(type & spot::postprocessor::Parity))
+ // If no argument was given, we just require Parity.
+ // However, if a Parity condition was already set before,
+ // don't overwrite it. This way if someone mistakenly write
+ // `--parity='max even' --colored` without realizing that
+ // `--colored` is just the abbreviation for
+ // `--colored-parity=...` with the default argument, we
+ // won't reset the 'max even' setting.
type = spot::postprocessor::Parity;
if (key == 'p')
colored = spot::postprocessor::Colored;
diff --git a/bin/ltlsynt.cc b/bin/ltlsynt.cc
index 4b5858c01..1b741fa9a 100644
--- a/bin/ltlsynt.cc
+++ b/bin/ltlsynt.cc
@@ -91,17 +91,15 @@ static const argp_option options[] =
/**************************************************/
{ nullptr, 0, nullptr, 0, "Fine tuning:", 10 },
{ "algo", OPT_ALGO, "sd|ds|ps|lar|lar.old|acd", 0,
- "choose the algorithm for synthesis:"
- " \"sd\": translate to tgba, split, then determinize;"
- " \"ds\": translate to tgba, determinize, then split;"
- " \"ps\": translate to dpa, then split;"
- " \"lar\": translate to a deterministic automaton with arbitrary"
- " acceptance condition, then use LAR to turn to parity,"
- " then split (default);"
- " \"lar.old\": old version of LAR, for benchmarking;"
- " \"acd\": translate to a deterministic automaton with arbitrary"
- " acceptance condition, then use ACD to turn to parity,"
- " then split.\n", 0 },
+ "choose the algorithm for synthesis:\n"
+ " sd: translate to TGBA, split, determinize\n"
+ " ds: translate to TGBA, determinize, split\n"
+ " ps: translate to DPA, split\n"
+ " lar: translate to a deterministic TELA, convert to DPA"
+ " with LAR, split (default)\n"
+ " lar.old: old version of LAR, for benchmarking;\n"
+ " acd: translate to a deterministic TELA, convert to DPA"
+ " with ACD, split", 0 },
{ "bypass", OPT_BYPASS, "yes|no", 0,
"whether to try to avoid to construct a parity game "
"(enabled by default)", 0},
diff --git a/default.nix.in b/default.nix.in
new file mode 100644
index 000000000..8101e4f74
--- /dev/null
+++ b/default.nix.in
@@ -0,0 +1,35 @@
+# -*- mode: nix; coding: utf-8 -*-
+# Copyright (C) 2022 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 .
+
+{ pkgs ? import {} }:
+let
+ version = "@VERSION@";
+in
+pkgs.stdenv.mkDerivation {
+ inherit version;
+ pname = "spot";
+
+ buildInputs = [
+ pkgs.python3
+ ];
+
+ src = ./.;
+
+ enableParallelBuilding = true;
+}
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 000000000..dd215f1c6
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,61 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1741196730,
+ "narHash": "sha256-0Sj6ZKjCpQMfWnN0NURqRCQn2ob7YtXTAOTwCuz7fkA=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "48913d8f9127ea6530a2a2f1bd4daa1b8685d8a3",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-24.11",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 000000000..f0f3f95b5
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,214 @@
+{
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
+ flake-utils.url = "github:numtide/flake-utils";
+ };
+ outputs = { self, nixpkgs, flake-utils, ... }:
+ flake-utils.lib.eachSystem
+ [
+ "x86_64-linux"
+ ]
+
+ (system:
+ let
+ pkgs = import nixpkgs { inherit system; };
+ lib = pkgs.lib;
+
+ mkSpotApps = appNames:
+ pkgs.lib.genAttrs appNames
+ (name: flake-utils.lib.mkApp {
+ drv = self.packages.${system}.spot;
+ name = name;
+ });
+
+ spotPackage =
+ let
+ inherit (builtins)
+ filter
+ head
+ isString
+ match
+ readFile
+ split
+ ;
+
+ # NOTE: Maintaining the version separately would be a pain, and we
+ # can't have a flake.nix.in with a @VERSION@ because it would make
+ # the flake unusable without running autoconf first, defeating some
+ # of its purpose.
+ #
+ # So let's get it the hard way instead :)
+ extractVersionRegex = ''^AC_INIT\(\[spot], \[([^]]+)], \[spot@lrde\.epita\.fr]\)$'';
+ getLines = (fileContent:
+ filter isString (split "\n" fileContent)
+ );
+ findVersionLine = (lines:
+ lib.lists.findFirst
+ (l: lib.strings.hasPrefix "AC_INIT(" l)
+ null
+ lines
+ );
+ getVersion = (file:
+ let
+ lines = getLines (readFile file);
+ versionLine = findVersionLine lines;
+ version = head (match extractVersionRegex versionLine);
+ in
+ version
+ );
+ in
+ {
+ lib,
+ pkgs,
+ stdenv,
+ # FIXME: do we want this flag?
+ buildOrgDoc ? false,
+ # Whether to enable Spot's Python 3 bindings
+ enablePython ? false
+ }:
+ stdenv.mkDerivation {
+ pname = "spot";
+ version = getVersion ./configure.ac;
+
+ src = self;
+
+ enableParallelBuilding = true;
+
+ # NOTE: Nix enables a lot of hardening flags by default, some of
+ # these probably harm performance so I've disabled everything
+ # (haven't benchmarked with vs without these, though).
+ hardeningDisable = [ "all" ];
+
+ # NOTE: mktexpk fails without a HOME set
+ preBuild = ''
+ export HOME=$TMPDIR
+ patchShebangs tools
+ '' + (if buildOrgDoc then ''
+ ln -s ${pkgs.plantuml}/lib/plantuml.jar doc/org/plantuml.jar
+ '' else ''
+ touch doc/org-stamp
+ '');
+
+ configureFlags = [
+ "--disable-devel"
+ "--enable-optimizations"
+ ] ++ lib.optional (!enablePython) [
+ "--disable-python"
+ ];
+
+ nativeBuildInputs = with pkgs; [
+ autoreconfHook
+
+ autoconf
+ automake
+ bison
+ flex
+ libtool
+ perl
+ ] ++ lib.optional buildOrgDoc [
+ graphviz
+ groff
+ plantuml
+ pdf2svg
+ R
+ ] ++ lib.optional enablePython [
+ python3
+ swig4
+ ];
+
+ buildInputs = with pkgs; [
+ # should provide the minimum amount of packages necessary for
+ # building tl.pdf
+ (texlive.combine {
+ inherit (texlive)
+ scheme-basic
+ latexmk
+
+ booktabs
+ cm-super
+ doi
+ doublestroke
+ etoolbox
+ koma-script
+ mathabx-type1
+ mathpazo
+ metafont
+ microtype
+ nag
+ pgf
+ standalone
+ stmaryrd
+ tabulary
+ todonotes
+ wasy-type1
+ wasysym
+ ;
+ })
+ ];
+ };
+ in
+ {
+ defaultPackage = self.packages.${system}.spot;
+
+ packages = {
+ # binaries + library only
+ spot = pkgs.callPackage spotPackage {};
+
+ # NOTE: clang build is broken on Nix when linking to stdlib++, using
+ # libcxx instead. See:
+ # https://github.com/NixOS/nixpkgs/issues/91285
+ spotClang = pkgs.callPackage spotPackage {
+ stdenv = pkgs.llvmPackages.libcxxStdenv;
+ };
+
+ spotWithOrgDoc = pkgs.callPackage spotPackage {
+ buildOrgDoc = true;
+ };
+
+ spotWithPython = pkgs.python3Packages.toPythonModule (
+ pkgs.callPackage spotPackage {
+ enablePython = true;
+ }
+ );
+
+ spotFull = pkgs.python3Packages.toPythonModule (
+ pkgs.callPackage spotPackage {
+ buildOrgDoc = true; enablePython = true;
+ }
+ );
+ };
+
+ apps = mkSpotApps [
+ "autcross"
+ "autfilt"
+ "dstar2tgba"
+ "genaut"
+ "genltl"
+ "ltl2tgba"
+ "ltl2tgta"
+ "ltlcross"
+ "ltldo"
+ "ltlfilt"
+ "ltlgrind"
+ "ltlmix"
+ "ltlsynt"
+ "randaut"
+ "randltl"
+ ];
+
+ devShell = pkgs.mkShell {
+ name = "spot-dev";
+ inputsFrom = [ self.packages.${system}.spotFull ];
+ buildInputs = [
+ pkgs.gdb
+ pkgs.clang-tools # for clangd
+ pkgs.bear
+
+ (pkgs.python3.withPackages (p: [
+ p.jupyter
+ p.ipython # otherwise ipython module isn't found when running ipynb tests
+ ]))
+ ];
+ };
+ });
+}
diff --git a/python/spot/impl.i b/python/spot/impl.i
index 2291730ed..a435758f6 100644
--- a/python/spot/impl.i
+++ b/python/spot/impl.i
@@ -86,7 +86,10 @@
#include
#include
+#include
#include
+#include
+#include
#include
#include
#include
@@ -162,6 +165,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -544,6 +548,8 @@ namespace std {
%template(vectorofvectorofformulas) vector>;
%template(setunsigned) set;
%template(relabeling_map) map;
+ %template(pair_formula) pair;
+ %template(vector_pair_formula) vector>;
}
%include
@@ -631,6 +637,9 @@ namespace std {
%include
%include
+%include
+%include
+%include
%include
%include
%include
@@ -788,6 +797,7 @@ def state_is_accepting(self, src) -> "bool":
%include
%include
%include
+%include
%include
%include
%include
diff --git a/spot.spec.in b/spot.spec.in
index bd465fd62..a2630d2de 100755
--- a/spot.spec.in
+++ b/spot.spec.in
@@ -38,6 +38,7 @@ logic (LTL & PSL).
%{_bindir}/ltldo
%{_bindir}/ltlfilt
%{_bindir}/ltlgrind
+%{_bindir}/ltlmix
%{_bindir}/ltlsynt
%{_bindir}/randaut
%{_bindir}/randltl
@@ -52,11 +53,12 @@ logic (LTL & PSL).
%{_mandir}/man1/ltldo.1*
%{_mandir}/man1/ltlfilt.1*
%{_mandir}/man1/ltlgrind.1*
+%{_mandir}/man1/ltlmix.1*
%{_mandir}/man1/ltlsynt.1*
%{_mandir}/man1/randaut.1*
%{_mandir}/man1/randltl.1*
-%{_mandir}/man7/spot-x.7*
%{_mandir}/man7/spot.7*
+%{_mandir}/man7/spot-x.7*
%license COPYING
%doc AUTHORS COPYING NEWS README THANKS
diff --git a/spot/graph/graph.hh b/spot/graph/graph.hh
index 3b43f751b..16d117ffc 100644
--- a/spot/graph/graph.hh
+++ b/spot/graph/graph.hh
@@ -557,10 +557,11 @@ namespace spot
{
std::map, unsigned> uniq_;
G& g_;
+ unsigned acc_sink_;
public:
- univ_dest_mapper(G& graph)
- : g_(graph)
+ univ_dest_mapper(G& graph, unsigned sink = -1u)
+ : g_(graph), acc_sink_(sink)
{
}
@@ -570,6 +571,9 @@ namespace spot
std::vector tmp(begin, end);
std::sort(tmp.begin(), tmp.end());
tmp.erase(std::unique(tmp.begin(), tmp.end()), tmp.end());
+ if (acc_sink_ != -1u && tmp.size() > 1)
+ tmp.erase(std::remove(tmp.begin(), tmp.end(), acc_sink_),
+ tmp.end());
auto p = uniq_.emplace(tmp, 0);
if (p.second)
p.first->second = g_.new_univ_dests(tmp.begin(), tmp.end());
diff --git a/spot/tl/Makefile.am b/spot/tl/Makefile.am
index 6c7650875..f2ff0fcad 100644
--- a/spot/tl/Makefile.am
+++ b/spot/tl/Makefile.am
@@ -28,9 +28,11 @@ tl_HEADERS = \
declenv.hh \
defaultenv.hh \
delta2.hh \
+ derive.hh \
dot.hh \
environment.hh \
exclusive.hh \
+ expansions2.hh \
formula.hh \
hierarchy.hh \
length.hh \
@@ -54,8 +56,11 @@ libtl_la_SOURCES = \
declenv.cc \
defaultenv.cc \
delta2.cc \
+ derive.cc \
dot.cc \
exclusive.cc \
+ expansions.cc \
+ expansions2.cc \
formula.cc \
hierarchy.cc \
length.cc \
diff --git a/spot/tl/derive.cc b/spot/tl/derive.cc
new file mode 100644
index 000000000..5e8526eec
--- /dev/null
+++ b/spot/tl/derive.cc
@@ -0,0 +1,589 @@
+// -*- coding: utf-8 -*-
+// Copyright (C) 2021 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 "config.h"
+#include
+#include
+#include
+#include
+#include
+
+namespace spot
+{
+ namespace
+ {
+ static std::pair
+ first(formula f, const bdd_dict_ptr& d, void* owner)
+ {
+ if (f.is_boolean())
+ {
+ bdd res = formula_to_bdd(f, d, owner);
+ return { res, bdd_support(res) };
+ }
+
+ switch (f.kind())
+ {
+ // handled by is_boolean above
+ case op::ff:
+ case op::tt:
+ case op::ap:
+ case op::And:
+ case op::Or:
+ SPOT_UNREACHABLE();
+
+ case op::eword:
+ return { bddfalse, bddtrue };
+
+ case op::OrRat:
+ {
+ bdd res = bddfalse;
+ bdd support = bddtrue;
+ for (auto subformula : f)
+ {
+ auto [r, sup] = first(subformula, d, owner);
+ res |= r;
+ support &= sup;
+ }
+ return { res, support };
+ }
+
+ case op::AndRat:
+ {
+ bdd res = bddtrue;
+ bdd support = bddtrue;
+ for (auto subformula : f)
+ {
+ auto [r, sup] = first(subformula, d, owner);
+ res &= r;
+ support &= sup;
+ }
+ return { res, support };
+ }
+
+ case op::AndNLM:
+ return first(rewrite_and_nlm(f), d, owner);
+
+ case op::Concat:
+ {
+ auto [res, support] = first(f[0], d, owner);
+
+ if (f[0].accepts_eword())
+ {
+ auto [r, sup] = first(f.all_but(0), d, owner);
+ res |= r;
+ support &= sup;
+ }
+
+ return { res, support };
+ }
+
+ case op::Fusion:
+ {
+ auto [res, support] = first(f[0], d, owner);
+
+ // this should be computed only if f[0] recognizes words of size 1
+ // or accepts eword ?
+ auto p = first(f.all_but(0), d, owner);
+
+ return { res, support & p.second };
+ }
+
+ case op::Star:
+ case op::first_match:
+ return first(f[0], d, owner);
+
+ case op::FStar:
+ {
+ auto [res, support] = first(f[0], d, owner);
+
+ if (f.min() == 0)
+ res = bddtrue;
+
+ return { res, support };
+ }
+
+ default:
+ std::cerr << "unimplemented kind "
+ << static_cast(f.kind())
+ << std::endl;
+ SPOT_UNIMPLEMENTED();
+ }
+
+ return { bddfalse, bddtrue };
+ }
+
+ static std::vector
+ formula_aps(formula f)
+ {
+ auto res = std::unordered_set();
+
+ f.traverse([&res](formula f)
+ {
+ if (f.is(op::ap))
+ {
+ res.insert(f.ap_name());
+ return true;
+ }
+
+ return false;
+ });
+
+ return std::vector(res.begin(), res.end());
+ }
+ }
+
+ formula
+ rewrite_and_nlm(formula f)
+ {
+ unsigned s = f.size();
+ std::vector final;
+ std::vector non_final;
+
+ for (auto g: f)
+ if (g.accepts_eword())
+ final.emplace_back(g);
+ else
+ non_final.emplace_back(g);
+
+ if (non_final.empty())
+ // (a* & b*);c = (a*|b*);c
+ return formula::OrRat(std::move(final));
+ if (!final.empty())
+ {
+ // let F_i be final formulae
+ // N_i be non final formula
+ // (F_1 & ... & F_n & N_1 & ... & N_m)
+ // = (F_1 | ... | F_n);[*] && (N_1 & ... & N_m)
+ // | (F_1 | ... | F_n) && (N_1 & ... & N_m);[*]
+ formula f = formula::OrRat(std::move(final));
+ formula n = formula::AndNLM(std::move(non_final));
+ formula t = formula::one_star();
+ formula ft = formula::Concat({f, t});
+ formula nt = formula::Concat({n, t});
+ formula ftn = formula::AndRat({ft, n});
+ formula fnt = formula::AndRat({f, nt});
+ return formula::OrRat({ftn, fnt});
+ }
+ // No final formula.
+ // Translate N_1 & N_2 & ... & N_n into
+ // N_1 && (N_2;[*]) && ... && (N_n;[*])
+ // | (N_1;[*]) && N_2 && ... && (N_n;[*])
+ // | (N_1;[*]) && (N_2;[*]) && ... && N_n
+ formula star = formula::one_star();
+ std::vector disj;
+ for (unsigned n = 0; n < s; ++n)
+ {
+ std::vector conj;
+ for (unsigned m = 0; m < s; ++m)
+ {
+ formula g = f[m];
+ if (n != m)
+ g = formula::Concat({g, star});
+ conj.emplace_back(g);
+ }
+ disj.emplace_back(formula::AndRat(std::move(conj)));
+ }
+ return formula::OrRat(std::move(disj));
+ }
+
+ twa_graph_ptr
+ derive_finite_automaton_with_first(formula f, bdd_dict_ptr bdd_dict,
+ bool deterministic, derive_opts options)
+ {
+ auto aut = make_twa_graph(bdd_dict);
+
+ aut->prop_state_acc(true);
+ const auto acc_mark = aut->set_buchi();
+
+ auto formula2state = robin_hood::unordered_map();
+
+ unsigned init_state = aut->new_state();
+ aut->set_init_state(init_state);
+
+ formula2state.insert({ f, init_state });
+
+ auto f_aps = formula_aps(f);
+ for (auto& ap : f_aps)
+ aut->register_ap(ap);
+
+ auto todo = std::vector>();
+ todo.push_back({f, init_state});
+
+ auto state_names = new std::vector();
+ std::ostringstream ss;
+ ss << f;
+ state_names->push_back(ss.str());
+
+ auto find_dst = [&](formula derivative) -> unsigned
+ {
+ unsigned dst;
+ auto it = formula2state.find(derivative);
+ if (it != formula2state.end())
+ {
+ dst = it->second;
+ }
+ else
+ {
+ dst = aut->new_state();
+ todo.push_back({derivative, dst});
+ formula2state.insert({derivative, dst});
+ std::ostringstream ss;
+ ss << derivative;
+ state_names->push_back(ss.str());
+ }
+
+ return dst;
+ };
+
+ while (!todo.empty())
+ {
+ auto [curr_f, curr_state] = todo[todo.size() - 1];
+ todo.pop_back();
+
+ auto curr_acc_mark = curr_f.accepts_eword()
+ ? acc_mark
+ : acc_cond::mark_t();
+
+ auto [firsts, firsts_support] = first(curr_f, bdd_dict, aut.get());
+ for (const bdd one : minterms_of(firsts, firsts_support))
+ {
+ formula derivative =
+ partial_derivation(curr_f, one, bdd_dict, aut.get(), options);
+
+ // no transition possible from this letter
+ if (derivative.is(op::ff))
+ continue;
+
+ // either the formula isn't an OrRat, or if it is we consider it as
+ // as a whole to get a deterministic automaton
+ if (deterministic || !derivative.is(op::OrRat))
+ {
+ auto dst = find_dst(derivative);
+ aut->new_edge(curr_state, dst, one, curr_acc_mark);
+ continue;
+ }
+
+ // formula is an OrRat and we want a non deterministic automaton,
+ // so consider each child as a destination
+ for (const auto& subformula : derivative)
+ {
+ auto dst = find_dst(subformula);
+ aut->new_edge(curr_state, dst, one, curr_acc_mark);
+ }
+ }
+
+ // if state has no transitions and should be accepting, create
+ // artificial transition
+ if (aut->get_graph().state_storage(curr_state).succ == 0
+ && curr_f.accepts_eword())
+ aut->new_edge(curr_state, curr_state, bddfalse, acc_mark);
+ }
+
+ aut->set_named_prop("state-names", state_names);
+
+ aut->merge_edges();
+
+ return aut;
+ }
+
+ twa_graph_ptr
+ derive_finite_automaton(formula f, bool deterministic)
+ {
+ auto bdd_dict = make_bdd_dict();
+ auto aut = make_twa_graph(bdd_dict);
+
+ aut->prop_state_acc(true);
+ const auto acc_mark = aut->set_buchi();
+
+ auto formula2state = robin_hood::unordered_map();
+
+ unsigned init_state = aut->new_state();
+ aut->set_init_state(init_state);
+
+ formula2state.insert({ f, init_state });
+
+ auto f_aps = formula_aps(f);
+ for (auto& ap : f_aps)
+ aut->register_ap(ap);
+ bdd all_aps = aut->ap_vars();
+
+ auto todo = std::vector>();
+ todo.push_back({f, init_state});
+
+ auto state_names = new std::vector();
+ std::ostringstream ss;
+ ss << f;
+ state_names->push_back(ss.str());
+
+ auto find_dst = [&](formula derivative) -> unsigned
+ {
+ unsigned dst;
+ auto it = formula2state.find(derivative);
+ if (it != formula2state.end())
+ {
+ dst = it->second;
+ }
+ else
+ {
+ dst = aut->new_state();
+ todo.push_back({derivative, dst});
+ formula2state.insert({derivative, dst});
+ std::ostringstream ss;
+ ss << derivative;
+ state_names->push_back(ss.str());
+ }
+
+ return dst;
+ };
+
+ while (!todo.empty())
+ {
+ auto [curr_f, curr_state] = todo[todo.size() - 1];
+ todo.pop_back();
+
+ auto curr_acc_mark = curr_f.accepts_eword()
+ ? acc_mark
+ : acc_cond::mark_t();
+
+ for (const bdd one : minterms_of(bddtrue, all_aps))
+ {
+ formula derivative =
+ partial_derivation(curr_f, one, bdd_dict, aut.get());
+
+ // no transition possible from this letter
+ if (derivative.is(op::ff))
+ continue;
+
+ // either the formula isn't an OrRat, or if it is we consider it as
+ // as a whole to get a deterministic automaton
+ if (deterministic || !derivative.is(op::OrRat))
+ {
+ auto dst = find_dst(derivative);
+ aut->new_edge(curr_state, dst, one, curr_acc_mark);
+ continue;
+ }
+
+ // formula is an OrRat and we want a non deterministic automaton,
+ // so consider each child as a destination
+ for (const auto& subformula : derivative)
+ {
+ auto dst = find_dst(subformula);
+ aut->new_edge(curr_state, dst, one, curr_acc_mark);
+ }
+ }
+
+ // if state has no transitions and should be accepting, create
+ // artificial transition
+ if (aut->get_graph().state_storage(curr_state).succ == 0
+ && curr_f.accepts_eword())
+ aut->new_edge(curr_state, curr_state, bddfalse, acc_mark);
+ }
+
+ aut->set_named_prop("state-names", state_names);
+
+ aut->merge_edges();
+
+ return aut;
+ }
+
+ twa_graph_ptr
+ derive_automaton_with_first(formula f, bdd_dict_ptr bdd_dict,
+ bool deterministic)
+ {
+ auto finite = derive_finite_automaton_with_first(f, bdd_dict,
+ deterministic);
+
+ return from_finite(finite);
+ }
+
+ twa_graph_ptr
+ derive_automaton(formula f, bool deterministic)
+ {
+ auto finite = derive_finite_automaton(f, deterministic);
+
+ return from_finite(finite);
+ }
+
+ formula
+ partial_derivation(formula f, const bdd var, const bdd_dict_ptr& d,
+ void* owner, derive_opts options)
+ {
+ if (f.is_boolean())
+ {
+ auto f_bdd = formula_to_bdd(f, d, owner);
+
+ if (bdd_implies(var, f_bdd))
+ return formula::eword();
+
+ return formula::ff();
+ }
+
+ switch (f.kind())
+ {
+ // handled by is_boolean above
+ case op::ff:
+ case op::tt:
+ case op::ap:
+ SPOT_UNREACHABLE();
+
+ case op::eword:
+ return formula::ff();
+
+ // d(E.F) = { d(E).F } U { c(E).d(F) }
+ case op::Concat:
+ {
+ formula E = f[0];
+ formula F = f.all_but(0);
+
+ formula d_E = partial_derivation(E, var, d, owner, options);
+
+ formula res;
+
+ if (options.concat_star_distribute && d_E.is(op::OrRat))
+ {
+ std::vector distributed;
+ for (auto g : d_E)
+ {
+ distributed.push_back(formula::Concat({g, F}));
+ }
+
+ res = formula::OrRat(distributed);
+ }
+ else
+ {
+ res =
+ formula::Concat({ partial_derivation(E, var, d, owner, options), F });
+ }
+
+
+ if (E.accepts_eword())
+ res = formula::OrRat(
+ { res, partial_derivation(F, var, d, owner, options) });
+
+ return res;
+ }
+
+ // d(E*) = d(E).E*
+ // d(E[*i..j]) = d(E).E[*(i-1)..(j-1)]
+ case op::Star:
+ {
+ auto min = f.min() == 0 ? 0 : (f.min() - 1);
+ auto max = f.max() == formula::unbounded()
+ ? formula::unbounded()
+ : (f.max() - 1);
+
+ formula d_E = partial_derivation(f[0], var, d, owner, options);
+
+ if (options.concat_star_distribute && !f[0].is_finite() && d_E.is(op::OrRat))
+ {
+ std::vector distributed;
+ for (auto g : d_E)
+ {
+ distributed.push_back(formula::Concat({g, formula::Star(f[0], min, max)}));
+ }
+
+ return formula::OrRat(distributed);
+ }
+
+ return formula::Concat({ d_E, formula::Star(f[0], min, max) });
+ }
+
+ // d(E[:*i..j]) = E:E[:*(i-1)..(j-1)] + (eword if i == 0 or c(d(E)))
+ case op::FStar:
+ {
+ formula E = f[0];
+
+ if (f.min() == 0 && f.max() == 0)
+ return formula::tt();
+
+ auto d_E = partial_derivation(E, var, d, owner, options);
+
+ auto min = f.min() == 0 ? 0 : (f.min() - 1);
+ auto max = f.max() == formula::unbounded()
+ ? formula::unbounded()
+ : (f.max() - 1);
+
+ auto results = std::vector();
+
+ auto E_i_j_minus = formula::FStar(E, min, max);
+ results.push_back(formula::Fusion({ d_E, E_i_j_minus }));
+
+ if (d_E.accepts_eword())
+ results.push_back(d_E);
+
+ if (f.min() == 0)
+ results.push_back(formula::eword());
+
+ return formula::OrRat(std::move(results));
+ }
+
+ // d(E && F) = d(E) && d(F)
+ // d(E + F) = {d(E)} U {d(F)}
+ case op::AndRat:
+ case op::OrRat:
+ {
+ std::vector subderivations;
+ for (auto subformula : f)
+ {
+ auto subderivation =
+ partial_derivation(subformula, var, d, owner, options);
+ subderivations.push_back(subderivation);
+ }
+ return formula::multop(f.kind(), std::move(subderivations));
+ }
+
+ case op::AndNLM:
+ {
+ formula rewrite = rewrite_and_nlm(f);
+ return partial_derivation(rewrite, var, d, owner, options);
+ }
+
+ // d(E:F) = {d(E):F} U {c(d(E)).d(F)}
+ case op::Fusion:
+ {
+ formula E = f[0];
+ formula F = f.all_but(0);
+
+ auto d_E = partial_derivation(E, var, d, owner, options);
+ auto res = formula::Fusion({ d_E, F });
+
+ if (d_E.accepts_eword())
+ res =
+ formula::OrRat({ res, partial_derivation(F, var, d, owner, options) });
+
+ return res;
+ }
+
+ case op::first_match:
+ {
+ formula E = f[0];
+ auto d_E = partial_derivation(E, var, d, owner, options);
+ // if d_E.accepts_eword(), first_match(d_E) will return eword
+ return formula::first_match(d_E);
+ }
+
+ default:
+ std::cerr << "unimplemented kind "
+ << static_cast(f.kind())
+ << std::endl;
+ SPOT_UNIMPLEMENTED();
+ }
+ return formula::ff();
+ }
+}
diff --git a/spot/tl/derive.hh b/spot/tl/derive.hh
new file mode 100644
index 000000000..993db2ed2
--- /dev/null
+++ b/spot/tl/derive.hh
@@ -0,0 +1,60 @@
+// -*- coding: utf-8 -*-
+// Copyright (C) 2021 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 .
+
+#pragma once
+
+#include
+
+#include
+
+#include
+#include
+#include
+
+namespace spot
+{
+
+ struct derive_opts
+ {
+ bool concat_star_distribute = true;
+ };
+
+ /// \ingroup tl_misc
+ /// \brief Produce a SERE formula's partial derivative
+ SPOT_API formula
+ partial_derivation(formula f, const bdd var, const bdd_dict_ptr& d,
+ void* owner, derive_opts options = {});
+
+ SPOT_API twa_graph_ptr
+ derive_automaton(formula f, bool deterministic = true);
+
+ SPOT_API twa_graph_ptr
+ derive_automaton_with_first(formula f, bdd_dict_ptr bdd_dict,
+ bool deterministic = true);
+
+ SPOT_API twa_graph_ptr
+ derive_finite_automaton(formula f, bool deterministic = true);
+
+ SPOT_API twa_graph_ptr
+ derive_finite_automaton_with_first(formula f, bdd_dict_ptr bdd_dict,
+ bool deterministic = true, derive_opts options = {});
+
+ SPOT_API formula
+ rewrite_and_nlm(formula f);
+}
diff --git a/spot/tl/expansions.cc b/spot/tl/expansions.cc
new file mode 100644
index 000000000..402236ea6
--- /dev/null
+++ b/spot/tl/expansions.cc
@@ -0,0 +1,966 @@
+// -*- coding: utf-8 -*-
+// Copyright (C) 2021 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 "config.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace spot
+{
+ namespace
+ {
+ // FIXME: could probably just return a map directly
+ static std::vector
+ formula_aps(formula f)
+ {
+ auto res = std::unordered_set();
+
+ f.traverse([&res](formula f)
+ {
+ if (f.is(op::ap))
+ {
+ res.insert(f.ap_name());
+ return true;
+ }
+
+ return false;
+ });
+
+ return std::vector(res.begin(), res.end());
+ }
+
+ formula
+ rewrite_and_nlm(formula f)
+ {
+ unsigned s = f.size();
+ std::vector final;
+ std::vector non_final;
+
+ for (auto g: f)
+ if (g.accepts_eword())
+ final.emplace_back(g);
+ else
+ non_final.emplace_back(g);
+
+ if (non_final.empty())
+ // (a* & b*);c = (a*|b*);c
+ return formula::OrRat(std::move(final));
+ if (!final.empty())
+ {
+ // let F_i be final formulae
+ // N_i be non final formula
+ // (F_1 & ... & F_n & N_1 & ... & N_m)
+ // = (F_1 | ... | F_n);[*] && (N_1 & ... & N_m)
+ // | (F_1 | ... | F_n) && (N_1 & ... & N_m);[*]
+ formula f = formula::OrRat(std::move(final));
+ formula n = formula::AndNLM(std::move(non_final));
+ formula t = formula::one_star();
+ formula ft = formula::Concat({f, t});
+ formula nt = formula::Concat({n, t});
+ formula ftn = formula::AndRat({ft, n});
+ formula fnt = formula::AndRat({f, nt});
+ return formula::OrRat({ftn, fnt});
+ }
+ // No final formula.
+ // Translate N_1 & N_2 & ... & N_n into
+ // N_1 && (N_2;[*]) && ... && (N_n;[*])
+ // | (N_1;[*]) && N_2 && ... && (N_n;[*])
+ // | (N_1;[*]) && (N_2;[*]) && ... && N_n
+ formula star = formula::one_star();
+ std::vector disj;
+ for (unsigned n = 0; n < s; ++n)
+ {
+ std::vector conj;
+ for (unsigned m = 0; m < s; ++m)
+ {
+ formula g = f[m];
+ if (n != m)
+ g = formula::Concat({g, star});
+ conj.emplace_back(g);
+ }
+ disj.emplace_back(formula::AndRat(std::move(conj)));
+ }
+ return formula::OrRat(std::move(disj));
+ }
+
+ class bdd_finalizer
+ {
+ public:
+ int encode(formula f)
+ {
+ bool is_anon = false;
+ int var_num;
+ auto it = formula2bdd_.find(f);
+ if (it != formula2bdd_.end())
+ {
+ var_num = it->second;
+ }
+ else
+ {
+ if (opt_sigma_star_ && (f.is(op::Star)
+ && f[0].is(op::tt)
+ && f.min() == 0
+ && f.max() == formula::unbounded()))
+ {
+ var_num = bddtrue.id();
+ }
+ else if (opt_bdd_encode_ && (f.is(op::AndRat) || f.is(op::OrRat)))
+ {
+ bdd var = f.is(op::AndRat) ? bdd(bddtrue) : bdd(bddfalse);
+ for (const auto& sub_f : f)
+ {
+ int bddid = encode(sub_f);
+ bdd subvar = bdd_ithvar(bddid);
+ var = f.is(op::AndRat) ? var & subvar : var | subvar;
+ }
+ var_num = var.id();
+ }
+ else
+ {
+ var_num = d_->register_anonymous_variables(1, this);
+ is_anon = true;
+ }
+
+ formula2bdd_.insert({f, var_num});
+ bdd2formula_.insert({var_num, f});
+ }
+
+ bdd var = bdd_ithvar(var_num);
+
+ if (is_anon)
+ anon_set_ &= var;
+
+ return var_num;
+ }
+
+ bdd_finalizer(expansion_t& exp, bdd_dict_ptr d, bool opt_sigma_star, bool opt_bdd_encode)
+ : anon_set_(bddtrue)
+ , d_(d)
+ , opt_sigma_star_(opt_sigma_star)
+ , opt_bdd_encode_(opt_bdd_encode)
+ {
+ for (const auto& [prefix, suffix] : exp)
+ {
+ int var_num = encode(suffix);
+ bdd var = bdd_ithvar(var_num);
+ exp_ |= prefix & var;
+ }
+ }
+
+ ~bdd_finalizer()
+ {
+ d_->unregister_all_my_variables(this);
+ }
+
+ expansion_t
+ simplify(exp_opts::expand_opt opts);
+
+ private:
+ bdd exp_;
+ bdd anon_set_;
+ std::map formula2bdd_;
+ std::map bdd2formula_;
+ bdd_dict_ptr d_;
+ bool opt_sigma_star_;
+ bool opt_bdd_encode_;
+
+ formula var_to_formula(int var);
+ formula conj_bdd_to_sere(bdd b);
+ formula bdd_to_sere(bdd b);
+ };
+
+ formula
+ bdd_finalizer::var_to_formula(int var)
+ {
+ formula f = bdd2formula_[var];
+ assert(f);
+ return f;
+ }
+
+ formula
+ bdd_finalizer::bdd_to_sere(bdd f)
+ {
+ if (f == bddfalse)
+ return formula::ff();
+
+ std::vector v;
+ minato_isop isop(f);
+ bdd cube;
+ while ((cube = isop.next()) != bddfalse)
+ v.emplace_back(conj_bdd_to_sere(cube));
+ return formula::OrRat(std::move(v));
+ }
+
+ formula
+ bdd_finalizer::conj_bdd_to_sere(bdd b)
+ {
+ if (b == bddtrue)
+ {
+ if (opt_sigma_star_){
+ return formula::Star(formula::tt(), 0, formula::unbounded());
+ } else {
+ return formula::tt();
+ }
+ }
+ if (b == bddfalse)
+ return formula::ff();
+
+ // Unroll the first loop of the next do/while loop so that we
+ // do not have to create v when b is not a conjunction.
+ formula res = var_to_formula(bdd_var(b));
+ bdd high = bdd_high(b);
+ if (high == bddfalse)
+ {
+ res = formula::Not(res);
+ b = bdd_low(b);
+ }
+ else
+ {
+ assert(bdd_low(b) == bddfalse);
+ b = high;
+ }
+ if (b == bddtrue)
+ return res;
+ std::vector v{std::move(res)};
+ do
+ {
+ res = var_to_formula(bdd_var(b));
+ high = bdd_high(b);
+ if (high == bddfalse)
+ {
+ res = formula::Not(res);
+ b = bdd_low(b);
+ }
+ else
+ {
+ assert(bdd_low(b) == bddfalse);
+ b = high;
+ }
+ assert(b != bddfalse);
+ v.emplace_back(std::move(res));
+ }
+ while (b != bddtrue);
+ return formula::multop(op::AndRat, std::move(v));
+ }
+
+ expansion_t
+ bdd_finalizer::simplify(exp_opts::expand_opt opts)
+ {
+ expansion_t res;
+
+ if (opts & exp_opts::expand_opt::BddMinterm)
+ {
+ bdd prop_set = bdd_exist(bdd_support(exp_), anon_set_);
+ bdd or_labels = bdd_exist(exp_, anon_set_);
+ // TODO: check are_equivalent avec or_labels/exp_ en premier argument
+ for (bdd letter: minterms_of(or_labels, prop_set))
+ {
+ bdd dest_bdd = bdd_restrict(exp_, letter);
+ formula dest = bdd_to_sere(dest_bdd);
+
+ // #ifndef NDEBUG
+ // // make sure it didn't exist before
+ // auto it = std::find(res.begin(), res.end(), {letter, dest});
+ // SPOT_ASSERT(it == res.end());
+ // #endif
+
+ res.push_back({letter, dest});
+ }
+ }
+ else // BddIsop
+ {
+ minato_isop isop(exp_);
+ bdd cube;
+ while ((cube = isop.next()) != bddfalse)
+ {
+ bdd letter = bdd_exist(cube, anon_set_);
+ bdd suffix = bdd_existcomp(cube, anon_set_);
+ formula dest = conj_bdd_to_sere(suffix);
+
+ res.push_back({letter, dest});
+ }
+ }
+
+ return res;
+ }
+
+ expansion_t
+ unique_prefix(const expansion_t& exp)
+ {
+ std::map unique_map;
+ for (const auto& [prefix, suffix] : exp)
+ {
+ auto res = unique_map.insert({prefix, suffix});
+ if (!res.second)
+ {
+ auto it = res.first;
+ it->second = formula::OrRat({it->second, suffix});
+ }
+ }
+
+ expansion_t res(unique_map.begin(), unique_map.end());
+ return res;
+ }
+
+ expansion_t
+ unique_prefix_seen(const expansion_t& exp, std::unordered_set* seen)
+ {
+ std::map unique_map;
+ for (const auto& [prefix, suffix] : exp)
+ {
+ auto res = unique_map.insert({prefix, suffix});
+ if (!res.second)
+ {
+ auto it = res.first;
+ it->second = formula::OrRat({it->second, suffix});
+ }
+ }
+
+ expansion_t res;
+
+ for (const auto& [prefix, suffix] : unique_map)
+ {
+ if (!suffix.is(op::OrRat))
+ {
+ res.push_back({prefix, suffix});
+ continue;
+ }
+
+ std::vector merge;
+ std::vector single;
+
+ for (const auto& sub_f : suffix)
+ {
+ if (seen->find(sub_f) != seen->end())
+ {
+ single.push_back(sub_f);
+ }
+ else
+ {
+ merge.push_back(sub_f);
+ }
+ }
+
+ for (const auto& sub_f : single)
+ res.push_back({prefix, sub_f});
+
+ if (!merge.empty())
+ res.push_back({prefix, formula::OrRat(merge)});
+ }
+
+ return res;
+ }
+
+ size_t count_new(const expansion_t& exp, std::unordered_set* seen)
+ {
+ size_t count = 0;
+ for (const auto& [_, suffix] : exp)
+ {
+ if (seen->find(suffix) == seen->end())
+ count++;
+ }
+ return count;
+ }
+
+ const expansion_t&
+ find_smallest(const expansion_t& left,
+ const expansion_t& right,
+ std::unordered_set* seen)
+ {
+ size_t left_new = count_new(left, seen);
+ size_t right_new = count_new(right, seen);
+
+ if (left_new < right_new)
+ return left;
+
+ if (left_new == right_new && left.size() > right.size())
+ return right;
+
+ return right;
+ }
+
+ expansion_t
+ unique_prefix_count(const expansion_t& exp, std::unordered_set* seen)
+ {
+ expansion_t up = unique_prefix(exp);
+ expansion_t up_seen = unique_prefix_seen(exp, seen);
+
+ const expansion_t& maybe_smallest = find_smallest(exp, up, seen);
+ const expansion_t& smallest = find_smallest(maybe_smallest, up_seen, seen);
+
+ return smallest;
+ }
+
+ void
+ finalize(expansion_t& exp, exp_opts::expand_opt opts, bdd_dict_ptr d, std::unordered_set* seen)
+ {
+ if (opts & (exp_opts::expand_opt::BddIsop
+ | exp_opts::expand_opt::BddMinterm))
+ {
+ bdd_finalizer bddf(exp, d, opts & exp_opts::expand_opt::BddSigmaStar, opts & exp_opts::expand_opt::BddEncode);
+ exp = bddf.simplify(opts);
+ }
+
+ if (opts & exp_opts::expand_opt::UniqueSuffixPre)
+ {
+ std::map unique_map;
+ for (const auto& [prefix, suffix] : exp)
+ {
+ auto res = unique_map.insert({suffix, prefix});
+ if (!res.second)
+ {
+ auto it = res.first;
+ it->second |= prefix;
+ }
+ }
+
+ exp.clear();
+ for (const auto& [suffix, prefix] : unique_map)
+ {
+ exp.push_back({prefix, suffix});
+ }
+ }
+
+ if (opts & exp_opts::expand_opt::UniquePrefix
+ || opts & exp_opts::expand_opt::UniquePrefixSeenOpt
+ || opts & exp_opts::expand_opt::UniquePrefixSeenCountOpt)
+ {
+ if (opts & exp_opts::expand_opt::UniquePrefixSeenOpt)
+ exp = unique_prefix_seen(exp, seen);
+ else if (opts & exp_opts::expand_opt::UniquePrefixSeenCountOpt)
+ exp = unique_prefix_count(exp, seen);
+ else
+ exp = unique_prefix(exp);
+ }
+
+ if (opts & exp_opts::expand_opt::UniqueSuffixPost)
+ {
+ std::map unique_map;
+ for (const auto& [prefix, suffix] : exp)
+ {
+ auto res = unique_map.insert({suffix, prefix});
+ if (!res.second)
+ {
+ auto it = res.first;
+ it->second |= prefix;
+ }
+ }
+
+ exp.clear();
+ for (const auto& [suffix, prefix] : unique_map)
+ {
+ exp.push_back({prefix, suffix});
+ }
+ }
+
+ if (opts & exp_opts::expand_opt::Determinize)
+ {
+ expansion_t exp_new;
+
+ bdd props = bddtrue;
+ for (const auto& [prefix, _] : exp)
+ props &= bdd_support(prefix);
+
+ std::vector dests;
+ for (bdd letter : minterms_of(bddtrue, props))
+ {
+ for (const auto& [prefix, suffix] : exp)
+ {
+ if (bdd_implies(letter, prefix))
+ dests.push_back(suffix);
+ }
+ formula or_dests = formula::OrRat(dests);
+ exp_new.push_back({letter, or_dests});
+ dests.clear();
+ }
+ exp = exp_new;
+ }
+
+ // sort and remove duplicates from expansion to canonicalize it for
+ // eventual signature use
+ if (exp.size() >= 2)
+ {
+ std::sort(exp.begin(), exp.end(),
+ [](const auto& lhs, const auto& rhs) {
+ return std::make_pair(lhs.first.id(), lhs.second.id())
+ < std::make_pair(rhs.first.id(), rhs.second.id());
+ });
+ exp.erase(std::unique(exp.begin(), exp.end()), exp.end());
+ }
+ }
+ }
+
+ formula
+ expansion_to_formula(expansion_t e, bdd_dict_ptr& d)
+ {
+ std::vector res;
+
+ for (const auto& [key, val] : e)
+ {
+ formula prefix = bdd_to_formula(key, d);
+ res.push_back(formula::Concat({prefix, val}));
+ }
+
+ return formula::OrRat(res);
+ }
+
+ void print_expansion(const expansion_t& exp, const bdd_dict_ptr& dict)
+ {
+ for (const auto& [prefix, suffix] : exp)
+ {
+ std::cout << bdd_to_formula(prefix, dict) << ": " << suffix << std::endl;
+ }
+ }
+
+ std::vector>
+ expansion_simple(formula f)
+ {
+ int owner = 42;
+ auto d = make_bdd_dict();
+
+ auto exp = expansion(f, d, &owner, exp_opts::None);
+
+ std::vector> res;
+ for (const auto& [bdd, f] : exp)
+ res.push_back({bdd_to_formula(bdd, d), f});
+
+ d->unregister_all_my_variables(&owner);
+ return res;
+ }
+
+ expansion_t
+ expansion(formula f, const bdd_dict_ptr& d, void *owner, exp_opts::expand_opt opts, std::unordered_set* seen)
+ {
+ using exp_t = expansion_t;
+
+ if (f.is_boolean())
+ {
+ auto f_bdd = formula_to_bdd(f, d, owner);
+
+ if (f_bdd == bddfalse)
+ return {};
+
+ return {{f_bdd, formula::eword()}};
+ }
+
+ auto rec = [&d, owner, seen](formula f){
+ return expansion(f, d, owner, exp_opts::None, seen);
+ };
+
+
+ switch (f.kind())
+ {
+ case op::ff:
+ case op::tt:
+ case op::ap:
+ SPOT_UNREACHABLE();
+
+ case op::eword:
+ // return {{bddfalse, formula::ff()}};
+ return {};
+
+ case op::Concat:
+ {
+ auto exps = rec(f[0]);
+
+ exp_t res;
+ for (const auto& [bdd_l, form] : exps)
+ {
+ res.push_back({bdd_l, formula::Concat({form, f.all_but(0)})});
+ }
+
+ if (f[0].accepts_eword())
+ {
+ auto exps_rest = rec(f.all_but(0));
+ for (const auto& [bdd_l, form] : exps_rest)
+ {
+ res.push_back({bdd_l, form});
+ }
+ }
+
+ finalize(res, opts, d, seen);
+ return res;
+ }
+
+ case op::FStar:
+ {
+ formula E = f[0];
+
+ if (f.min() == 0 && f.max() == 0)
+ return {{bddtrue, formula::eword()}};
+
+ auto min = f.min() == 0 ? 0 : (f.min() - 1);
+ auto max = f.max() == formula::unbounded()
+ ? formula::unbounded()
+ : (f.max() - 1);
+
+ auto E_i_j_minus = formula::FStar(E, min, max);
+
+ auto exp = rec(E);
+ exp_t res;
+ for (const auto& [li, ei] : exp)
+ {
+ res.push_back({li, formula::Fusion({ei, E_i_j_minus})});
+
+ if (ei.accepts_eword() && f.min() != 0)
+ {
+ for (const auto& [ki, fi] : rec(E_i_j_minus))
+ {
+ // FIXME: build bdd once
+ if ((li & ki) != bddfalse)
+ res.push_back({li & ki, fi});
+ }
+ }
+ }
+ if (f.min() == 0)
+ res.push_back({bddtrue, formula::eword()});
+
+ finalize(res, opts, d, seen);
+ return res;
+ }
+
+ case op::Star:
+ {
+ auto min = f.min() == 0 ? 0 : (f.min() - 1);
+ auto max = f.max() == formula::unbounded()
+ ? formula::unbounded()
+ : (f.max() - 1);
+
+ auto exps = rec(f[0]);
+
+ exp_t res;
+ for (const auto& [bdd_l, form] : exps)
+ {
+ res.push_back({bdd_l, formula::Concat({form, formula::Star(f[0], min, max)})});
+ }
+
+ finalize(res, opts, d, seen);
+ return res;
+ }
+
+ case op::AndNLM:
+ {
+ formula rewrite = rewrite_and_nlm(f);
+ auto res = rec(rewrite);
+ finalize(res, opts, d, seen);
+ return res;
+ }
+
+ case op::first_match:
+ {
+ auto exps = rec(f[0]);
+
+ exp_t res;
+ for (const auto& [bdd_l, form] : exps)
+ {
+ res.push_back({bdd_l, form});
+ }
+
+ // determinize
+ bdd or_labels = bddfalse;
+ bdd support = bddtrue;
+ bool is_det = true;
+ for (const auto& [l, _] : res)
+ {
+ support &= bdd_support(l);
+ if (is_det)
+ is_det = !bdd_have_common_assignment(l, or_labels);
+ or_labels |= l;
+ }
+
+ if (is_det)
+ {
+ for (auto& [_, dest] : res)
+ dest = formula::first_match(dest);
+ finalize(res, opts, d, seen);
+ return res;
+ }
+
+ exp_t res_det;
+ std::vector dests;
+ for (bdd l: minterms_of(or_labels, support))
+ {
+ for (const auto& [ndet_label, ndet_dest] : res)
+ {
+ if (bdd_implies(l, ndet_label))
+ dests.push_back(ndet_dest);
+ }
+ formula or_dests = formula::OrRat(dests);
+ res_det.push_back({l, or_dests});
+ dests.clear();
+ }
+
+ for (auto& [_, dest] : res_det)
+ dest = formula::first_match(dest);
+ finalize(res_det, opts, d, seen);
+ return res_det;
+ }
+
+ case op::Fusion:
+ {
+ exp_t res;
+ formula E = f[0];
+ formula F = f.all_but(0);
+
+ exp_t Ei = rec(E);
+ // TODO: std::option
+ exp_t Fj = rec(F);
+
+ for (const auto& [li, ei] : Ei)
+ {
+ if (ei.accepts_eword())
+ {
+ for (const auto& [kj, fj] : Fj)
+ if ((li & kj) != bddfalse)
+ res.push_back({li & kj, fj});
+ }
+
+ formula ei_fusion_F = formula::Fusion({ei, F});
+ if (!ei_fusion_F.is(op::ff))
+ res.push_back({li, ei_fusion_F});
+ }
+
+ finalize(res, opts, d, seen);
+ return res;
+ }
+
+ case op::AndRat:
+ {
+ exp_t res;
+ for (const auto& sub_f : f)
+ {
+ auto exps = rec(sub_f);
+
+ if (exps.empty())
+ {
+ // op::AndRat: one of the expansions was empty (the only
+ // edge was `false`), so the AndRat is empty as
+ // well
+ res.clear();
+ break;
+ }
+
+ if (res.empty())
+ {
+ res = std::move(exps);
+ continue;
+ }
+
+ exp_t new_res;
+ bool inserted = false;
+ for (const auto& [l_key, l_val] : exps)
+ {
+ for (const auto& [r_key, r_val] : res)
+ {
+ if ((l_key & r_key) != bddfalse)
+ {
+ new_res.push_back({l_key & r_key, formula::multop(f.kind(), {l_val, r_val})});
+ inserted = true;
+ }
+ }
+ }
+
+ if (!inserted)
+ {
+ // all prefix conjuctions led to bddfalse, And is empty
+ res.clear();
+ break;
+ }
+
+ res = std::move(new_res);
+ }
+
+ finalize(res, opts, d, seen);
+ return res;
+ }
+
+ case op::OrRat:
+ {
+ exp_t res;
+ for (const auto& sub_f : f)
+ {
+ auto exps = rec(sub_f);
+ if (exps.empty())
+ continue;
+
+ if (res.empty())
+ {
+ res = std::move(exps);
+ continue;
+ }
+
+ for (const auto& [label, dest] : exps)
+ res.push_back({label, dest});
+ }
+
+ finalize(res, opts, d, seen);
+ return res;
+ }
+
+ default:
+ std::cerr << "unimplemented kind "
+ << static_cast(f.kind())
+ << std::endl;
+ SPOT_UNIMPLEMENTED();
+ }
+
+ return {};
+ }
+
+ twa_graph_ptr
+ expand_automaton(formula f, bdd_dict_ptr d, exp_opts::expand_opt opts)
+ {
+ auto finite = expand_finite_automaton(f, d, opts);
+ return from_finite(finite);
+ }
+
+ struct signature_hash
+ {
+ std::size_t
+ operator() (const std::pair& sig) const noexcept
+ {
+ size_t hash = std::hash()(sig.first);
+
+ for (const auto& keyvalue : sig.second)
+ {
+ hash ^= (bdd_hash()(keyvalue.first) ^ std::hash()(keyvalue.second))
+ + 0x9e3779b9 + (hash << 6) + (hash >> 2);
+ }
+
+ return hash;
+ }
+ };
+
+ twa_graph_ptr
+ expand_finite_automaton(formula f, bdd_dict_ptr d, exp_opts::expand_opt opts)
+ {
+ bool signature_merge = opts & exp_opts::expand_opt::SignatureMerge;
+
+ auto aut = make_twa_graph(d);
+
+ aut->prop_state_acc(true);
+ const auto acc_mark = aut->set_buchi();
+
+ auto formula2state = robin_hood::unordered_map();
+ auto signature2state = std::unordered_map, unsigned, signature_hash>();
+ auto seen = std::unordered_set();
+ seen.insert(f);
+
+ unsigned init_state = aut->new_state();
+ aut->set_init_state(init_state);
+ formula2state.insert({ f, init_state });
+
+
+ auto f_aps = formula_aps(f);
+ for (auto& ap : f_aps)
+ aut->register_ap(ap);
+
+ if (signature_merge)
+ signature2state.insert({ {f.accepts_eword(), expansion(f, d, aut.get(), opts, &seen)}, init_state});
+
+
+ auto todo = std::vector>();
+ todo.push_back({f, init_state});
+
+ auto state_names = new std::vector();
+ std::ostringstream ss;
+ ss << f;
+ state_names->push_back(ss.str());
+
+ auto find_dst = [&](formula suffix) -> unsigned
+ {
+ unsigned dst;
+ auto it = formula2state.find(suffix);
+ if (it != formula2state.end())
+ {
+ dst = it->second;
+ }
+ else
+ {
+ if (signature_merge)
+ {
+ auto exp = expansion(suffix, d, aut.get(), opts, &seen);
+ bool accepting = suffix.accepts_eword();
+ auto it2 = signature2state.find({accepting, exp});
+ if (it2 != signature2state.end())
+ {
+ formula2state.insert({suffix, it2->second});
+ return it2->second;
+ }
+ }
+
+ dst = aut->new_state();
+ todo.push_back({suffix, dst});
+ seen.insert(suffix);
+
+ formula2state.insert({suffix, dst});
+ if (signature_merge)
+ signature2state.insert({{suffix.accepts_eword(), expansion(suffix, d, aut.get(), opts, &seen)}, dst});
+
+ std::ostringstream ss;
+ ss << suffix;
+ state_names->push_back(ss.str());
+ }
+
+ return dst;
+ };
+
+ while (!todo.empty())
+ {
+ auto [curr_f, curr_state] = todo[todo.size() - 1];
+ todo.pop_back();
+
+ auto curr_acc_mark= curr_f.accepts_eword()
+ ? acc_mark
+ : acc_cond::mark_t();
+
+ auto exp = expansion(curr_f, d, aut.get(), opts, &seen);
+
+ for (const auto& [letter, suffix] : exp)
+ {
+ if (suffix.is(op::ff))
+ // TODO ASSERT NOT
+ continue;
+
+ auto dst = find_dst(suffix);
+ aut->new_edge(curr_state, dst, letter, curr_acc_mark);
+ }
+
+ // if state has no transitions and should be accepting, create
+ // artificial transition
+ if (aut->get_graph().state_storage(curr_state).succ == 0
+ && curr_f.accepts_eword())
+ aut->new_edge(curr_state, curr_state, bddfalse, acc_mark);
+ }
+
+ aut->set_named_prop("state-names", state_names);
+
+ if ((opts & exp_opts::MergeEdges)
+ && !(opts & exp_opts::UniqueSuffixPre || opts & exp_opts::UniqueSuffixPost))
+ aut->merge_edges();
+
+ return aut;
+ }
+}
diff --git a/spot/tl/expansions.hh b/spot/tl/expansions.hh
new file mode 100644
index 000000000..9db8d2c8d
--- /dev/null
+++ b/spot/tl/expansions.hh
@@ -0,0 +1,72 @@
+// -*- coding: utf-8 -*-
+// Copyright (C) 2021 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 .
+
+#pragma once
+
+#include