// -*- coding: utf-8 -*-
// Copyright (C) 2008, 2010, 2012, 2013, 2014, 2015 Laboratoire de
// Recherche et Développement de l'Epita (LRDE)
// Copyright (C) 2003, 2004 Laboratoire d'Informatique de Paris 6 (LIP6),
// département Systèmes Répartis Coopératifs (SRC), Université Pierre
// et Marie Curie.
//
// 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
#include
#include
#include
#include
#include "unabbrev.hh"
#include "print.hh"
#include "misc/escape.hh"
namespace spot
{
namespace ltl
{
namespace
{
enum keyword {
KFalse = 0,
KTrue = 1,
KEmptyWord = 2,
KXor,
KImplies,
KEquiv,
KU,
KR,
KW,
KM,
KSeq,
KSeqNext,
KSeqMarked,
KSeqMarkedNext,
KTriggers,
KTriggersNext,
KNot,
KX,
KF,
KG,
KOr,
KOrRat,
KAnd,
KAndRat,
KAndNLM,
KConcat,
KFusion,
KOpenSERE,
KCloseSERE,
KCloseBunop,
KStarBunop,
KPlusBunop,
KFStarBunop,
KFPlusBunop,
KEqualBunop,
KGotoBunop,
};
const char* spot_kw[] = {
"0",
"1",
"[*0]",
" xor ",
" -> ",
" <-> ",
" U ",
" R ",
" W ",
" M ",
"<>-> ",
"<>=> ",
"<>+> ",
"<>=+> ",
"[]-> ",
"[]=> ",
"!",
"X",
"F",
"G",
" | ",
" | ",
" & ",
" && ",
" & ",
";",
":",
"{",
"}",
"]",
"[*",
"[+]",
"[:*",
"[:+]",
"[=",
"[->",
};
const char* spin_kw[] = {
"false", // 0 doesn't work from the command line
"true", // 1 doesn't work from the command line
"[*0]", // not supported
" xor ", // rewritten
" -> ",
" <-> ",
" U ",
" V ",
" W ", // rewritten
" M ", // rewritten
"<>-> ", // not supported
"<>=> ", // not supported
"<>+> ", // not supported
"<>=+> ", // not supported
"[]-> ", // not supported
"[]=> ", // not supported
"!",
"X",
"<>",
"[]",
" || ",
" || ",
" && ",
" && ", // not supported
" & ", // not supported
";", // not supported
":", // not supported
"{", // not supported
"}", // not supported
"]", // not supported
"[*", // not supported
"[+]", // not supported
"[:*", // not supported
"[:+]", // not supported
"[=", // not supported
"[->", // not supported
};
const char* wring_kw[] = {
"FALSE",
"TRUE",
"[*0]", // not supported
" ^ ",
" -> ",
" <-> ",
" U ",
" R ",
" W ", // rewritten
" M ", // rewritten
"<>-> ", // not supported
"<>=> ", // not supported
"<>+> ", // not supported
"<>=+> ", // not supported
"[]-> ", // not supported
"[]=> ", // not supported
"!",
"X",
"F",
"G",
" + ",
" | ", // not supported
" * ",
" && ", // not supported
" & ", // not supported
";", // not supported
":", // not supported
"{", // not supported
"}", // not supported
"]", // not supported
"[*", // not supported
"[+]", // not supported
"[:*", // not supported
"[:+]", // not supported
"[=", // not supported
"[->", // not supported
};
const char* utf8_kw[] = {
"0",
"1",
"[*0]",
"⊕",
" → ",
" ↔ ",
" U ",
" R ",
" W ",
" M ",
"◇→ ",
"◇⇒ ",
"◇→̃ ",
"◇⇒̃ ",
"□→ ",
"□⇒ ",
"¬",
"○",
"◇",
"□",
"∨",
" | ",
"∧",
" ∩ ",
" & ",
";",
":",
"{",
"}",
"]",
"[*",
"[+]",
"[:*",
"[:+]",
"[=",
"[->",
};
const char* latex_kw[] = {
"\\ffalse",
"\\ttrue",
"\\eword",
" \\lxor ",
" \\limplies ",
" \\liff ",
" \\U ",
" \\R ",
" \\W ",
" \\M ",
"\\seq ",
"\\seqX ",
"\\seqM ",
"\\seqXM ",
"\\triggers ",
"\\triggersX ",
"\\lnot ",
"\\X ",
"\\F ",
"\\G ",
" \\lor ",
" \\SereOr ",
" \\land ",
" \\SereAnd ",
" \\SereAndNLM ",
" \\SereConcat ",
" \\SereFusion ",
"\\{",
"\\}",
"}",
"\\SereStar{",
"\\SerePlus{}",
"\\SereFStar{",
"\\SereFPlus{}",
"\\SereEqual{",
"\\SereGoto{",
};
const char* sclatex_kw[] = {
"\\bot",
"\\top",
"\\varepsilon",
" \\oplus ",
" \\rightarrow ",
" \\leftrightarrow ",
" \\mathbin{\\mathsf{U}} ",
" \\mathbin{\\mathsf{R}} ",
" \\mathbin{\\mathsf{W}} ",
" \\mathbin{\\mathsf{M}} ",
("\\mathrel{\\Diamond\\kern-1.7pt\\raise.4pt"
"\\hbox{$\\mathord{\\rightarrow}$}} "),
("\\mathrel{\\Diamond\\kern-1.7pt\\raise.4pt"
"\\hbox{$\\mathord{\\Rightarrow}$}} "),
"\\seqM ",
"\\seqXM ",
("\\mathrel{\\Box\\kern-1.7pt\\raise.4pt"
"\\hbox{$\\mathord{\\rightarrow}$}} "),
("\\mathrel{\\Box\\kern-1.7pt\\raise.4pt"
"\\hbox{$\\mathord{\\Rightarrow}$}} "),
"\\lnot ",
"\\mathsf{X} ",
"\\mathsf{F} ",
"\\mathsf{G} ",
" \\lor ",
" \\cup ",
" \\land ",
" \\cap ",
" \\mathbin{\\mathsf{\\&}} ",
" \\mathbin{\\mathsf{;}} ",
" \\mathbin{\\mathsf{:}} ",
"\\{",
"\\}",
"}",
"^{\\star",
"^+",
"^{\\mathsf{:}\\star",
"^{\\mathsf{:}+}",
"^{=",
"^{\\to",
};
static bool
is_bare_word(const char* str)
{
// Bare words cannot be empty, start with the letter of a
// unary operator, or be the name of an existing constant or
// operator. Also they should start with an letter.
if (!*str
|| *str == 'F'
|| *str == 'G'
|| *str == 'X'
|| !(isalpha(*str) || *str == '_' || *str == '.')
|| ((*str == 'U' || *str == 'W' || *str == 'M' || *str == 'R')
&& str[1] == 0)
|| !strcasecmp(str, "true")
|| !strcasecmp(str, "false"))
return false;
// The remaining of the word must be alphanumeric.
while (*++str)
if (!(isalnum(*str) || *str == '_' || *str == '.'))
return false;
return true;
}
// If the formula has the form (!b)[*], return b.
static formula
strip_star_not(formula f)
{
return f.get_child_of({op::Star, op::Not});
}
// If the formula at position i in mo has the form
// (!b)[*];b with b being a Boolean formula, return b.
static formula
match_goto(formula mo, unsigned i)
{
assert(i + 1 < mo.size());
formula b = strip_star_not(mo[i]);
if (b == nullptr || !b.is_boolean())
return nullptr;
if (mo[i + 1] == b)
return b;
return nullptr;
}
class to_string_visitor final
{
public:
to_string_visitor(std::ostream& os,
bool full_parent = false,
bool ratexp = false,
const char** kw = spot_kw)
: os_(os), top_level_(true),
full_parent_(full_parent), in_ratexp_(ratexp),
kw_(kw)
{
}
void
openp() const
{
if (in_ratexp_)
emit(KOpenSERE);
else
os_ << '(';
}
void
closep() const
{
if (in_ratexp_)
emit(KCloseSERE);
else
os_ << ')';
}
std::ostream&
emit(int symbol) const
{
return os_ << kw_[symbol];
}
void
visit(formula f)
{
bool top_level = top_level_;
top_level_ = false;
auto s = f.size();
bool want_par = (full_parent_ || s > 1) && !top_level;
if (want_par)
openp();
auto emit_bunop_child = [this](formula b)
{
// b[*] is OK, no need to print {b}[*]. However we want
// braces for {!b}[*], the only unary operator that can
// be nested with Star/FStar.
// If full_parent_ is set, we do NOT emit those extra
// braces, because they are already output for the node
// below.
bool need_parent = (!full_parent_ && b.is(op::Not));
if (need_parent)
openp();
this->visit(b);
if (need_parent)
closep();
};
op o = f.kind();
switch (o)
{
case op::False:
emit(KFalse);
break;
case op::True:
emit(KTrue);
break;
case op::EmptyWord:
emit(KEmptyWord);
break;
case op::AP:
{
const std::string& str = f.ap_name();
if (!is_bare_word(str.c_str()))
{
// Spin 6 supports atomic propositions such as (a == 0)
// as long as they are enclosed in parentheses.
if (kw_ == sclatex_kw || kw_ == latex_kw)
escape_latex(os_ << "``\\mathit{", str)
<< "}\\textrm{''}";
else if (kw_ != spin_kw)
escape_str(os_ << '"', str) << '"';
else if (!full_parent_)
os_ << '(' << str << ')';
else
os_ << str;
}
else
{
if (kw_ == latex_kw || kw_ == sclatex_kw)
{
size_t s = str.size();
while (str[s - 1] >= '0' && str[s - 1] <= '9')
{
--s;
// bare words cannot start with digits
assert(s != 0);
}
if (s > 1)
os_ << "\\mathit{";
escape_latex(os_, str.substr(0, s));
if (s > 1)
os_ << '}';
if (s != str.size())
os_ << "_{"
<< str.substr(s)
<< '}';
}
else
{
os_ << str;
}
}
if (kw_ == wring_kw)
os_ << "=1";
}
break;
case op::Not:
{
formula c = f[0];
if (c.is(op::AP))
{
// If we negate a single letter in UTF-8, use a
// combining overline.
if (!full_parent_ && kw_ == utf8_kw)
{
auto& name = c.ap_name();
if (name.size() == 1 && is_bare_word(name.c_str()))
{
os_ << name << "̅";
break;
}
}
// If we negate an atomic proposition for Wring,
// output prop=0.
if (kw_ == wring_kw)
{
auto& name = c.ap_name();
if (is_bare_word(name.c_str()))
{
os_ << name << "=0";
break;
}
}
}
emit(KNot);
visit(c);
break;
}
case op::X:
emit(KX);
visit(f[0]);
break;
case op::F:
emit(KF);
visit(f[0]);
break;
case op::G:
emit(KG);
visit(f[0]);
break;
case op::NegClosure:
case op::NegClosureMarked:
emit(KNot);
if (o == op::NegClosureMarked)
os_ << (kw_ == utf8_kw ? "̃": "+");
// Fall through
case op::Closure:
os_ << '{';
in_ratexp_ = true;
top_level_ = true;
visit(f[0]);
os_ << '}';
in_ratexp_ = false;
top_level_ = false;
break;
case op::Xor:
visit(f[0]);
emit(KXor);
visit(f[1]);
break;
case op::Implies:
visit(f[0]);
emit(KImplies);
visit(f[1]);
break;
case op::Equiv:
visit(f[0]);
emit(KEquiv);
visit(f[1]);
break;
case op::U:
visit(f[0]);
emit(KU);
visit(f[1]);
break;
case op::R:
visit(f[0]);
emit(KR);
visit(f[1]);
break;
case op::W:
visit(f[0]);
emit(KW);
visit(f[1]);
break;
case op::M:
visit(f[0]);
emit(KM);
visit(f[1]);
break;
case op::EConcat:
case op::EConcatMarked:
case op::UConcat:
{
in_ratexp_ = true;
openp();
top_level_ = true;
formula left = f[0];
formula right = f[1];
unsigned last = left.size() - 1;
bool onelast = false;
if (left.is(op::Concat) && left[last].is_true())
{
visit(left.all_but(last));
onelast = true;
}
else
{
visit(left);
}
top_level_ = false;
closep();
in_ratexp_ = false;
if (o == op::UConcat)
{
emit(onelast ? KTriggersNext : KTriggers);
visit(right);
}
else if (o == op::EConcatMarked)
{
emit(onelast ? KSeqMarkedNext : KSeqMarked);
visit(right);
}
else if (o == op::EConcat)
{
if (f[1].is(op::True))
{
os_ << '!';
// No recursion on right.
}
else
{
emit(onelast ? KSeqNext : KSeq);
visit(right);
}
}
else
{
SPOT_UNREACHABLE();
}
}
break;
case op::Or:
case op::OrRat:
case op::And:
case op::AndRat:
case op::AndNLM:
case op::Fusion:
{
visit(f[0]);
keyword k = KFalse; // Initialize to something to please GCC.
switch (o)
{
case op::Or:
k = KOr;
break;
case op::OrRat:
k = KOrRat;
break;
case op::And:
k = in_ratexp_ ? KAndRat : KAnd;
break;
case op::AndRat:
k = KAndRat;
break;
case op::AndNLM:
k = KAndNLM;
break;
case op::Fusion:
k = KFusion;
break;
default:
SPOT_UNREACHABLE();
}
assert(k != KFalse);
unsigned max = f.size();
for (unsigned n = 1; n < max; ++n)
{
emit(k);
visit(f[n]);
}
break;
}
case op::Concat:
{
unsigned max = f.size();
for (unsigned i = 0; i < max; ++i)
{
if (i > 0)
emit(KConcat);
if (i + 1 < max)
{
// Try to match (!b)[*];b
formula b = match_goto(f, i);
if (b != nullptr)
{
emit_bunop_child(b);
// Wait... maybe we are looking at (!b)[*];b;(!b)[*]
// in which case it's b[=1].
if (i + 2 < max && f[i] == f[i + 2])
{
emit(KEqualBunop);
os_ << '1';
emit(KCloseBunop);
i += 2;
}
else
{
emit(KGotoBunop);
emit(KCloseBunop);
++i;
}
continue;
}
// Try to match ((!b)[*];b)[*i..j];(!b)[*]
formula fi = f[i];
if (fi.is(op::Star))
{
if (formula b2 = strip_star_not(f[i + 1]))
{
formula fic = fi[0];
if (fic.is(op::Concat))
if (formula b1 = match_goto(fic, 0))
if (b1 == b2)
{
emit_bunop_child(b1);
emit(KEqualBunop);
unsigned min = fi.min();
os_ << min;
unsigned max = fi.max();
if (max != min)
{
os_ << "..";
if (max != formula::unbounded())
os_ << max;
}
emit(KCloseBunop);
++i;
continue;
}
}
}
}
visit(f[i]);
}
break;
}
case op::Star:
case op::FStar:
{
formula c = f[0];
enum { Star, FStar, Goto } sugar = Star;
unsigned default_min = 0;
unsigned default_max = formula::unbounded();
// Abbreviate "1[*]" as "[*]".
if (!c.is(op::True) || o != op::Star)
{
if (o == op::Star)
{
// Is this a Goto?
if (c.is(op::Concat))
{
unsigned s = c.size();
if (s == 2)
if (formula b = match_goto(c, 0))
{
c = b;
sugar = Goto;
}
}
}
else if (o == op::FStar)
{
sugar = FStar;
}
else
{
SPOT_UNREACHABLE();
}
emit_bunop_child(c);
}
unsigned min = f.min();
unsigned max = f.max();
bool range = true;
switch (sugar)
{
case Star:
if (min == 1 && max == formula::unbounded())
{
range = false;
emit(KPlusBunop);
}
else
{
emit(KStarBunop);
}
break;
case FStar:
if (min == 1 && max == formula::unbounded())
{
range = false;
emit(KFPlusBunop);
}
else
{
emit(KFStarBunop);
}
break;
case Goto:
emit(KGotoBunop);
default_min = 1;
default_max = 1;
break;
}
// Beware that the default parameters of the Goto operator are
// not the same as Star or Equal:
//
// [->] = [->1..1]
// [->..] = [->1..unbounded]
// [*] = [*0..unbounded]
// [*..] = [*0..unbounded]
// [=] = [=0..unbounded]
// [=..] = [=0..unbounded]
//
// Strictly speaking [=] is not specified by PSL, and anyway we
// automatically rewrite Exp[=0..unbounded] as
// Exp[*0..unbounded], so we should never have to print [=]
// here.
//
// Also
// [*..] = [*0..unbounded]
if (range)
{
if (min != default_min || max != default_max)
{
// Always print the min_, even when it is equal to
// default_min, this way we avoid ambiguities (like
// when reading [*..3] near [->..2])
os_ << min;
if (min != max)
{
os_ << "..";
if (max != formula::unbounded())
os_ << max;
}
}
emit(KCloseBunop);
}
}
break;
}
if (want_par)
closep();
}
protected:
std::ostream& os_;
bool top_level_;
bool full_parent_;
bool in_ratexp_;
const char** kw_;
};
std::ostream&
printer_(std::ostream& os, formula f, bool full_parent,
bool ratexp, const char** kw)
{
to_string_visitor v(os, full_parent, ratexp, kw);
v.visit(f);
return os;
}
std::string
str_(formula f, bool full_parent, bool ratexp, const char** kw)
{
std::ostringstream os;
printer_(os, f, full_parent, ratexp, kw);
return os.str();
}
} // anonymous
std::ostream&
print_psl(std::ostream& os, formula f, bool full_parent)
{
return printer_(os, f, full_parent, false, spot_kw);
}
std::string
str_psl(formula f, bool full_parent)
{
return str_(f, full_parent, false, spot_kw);
}
std::ostream&
print_sere(std::ostream& os, formula f, bool full_parent)
{
return printer_(os, f, full_parent, true, spot_kw);
}
std::string
str_sere(formula f, bool full_parent)
{
return str_(f, full_parent, false, spot_kw);
}
std::ostream&
print_utf8_psl(std::ostream& os, formula f, bool full_parent)
{
return printer_(os, f, full_parent, false, utf8_kw);
}
std::string
str_utf8_psl(formula f, bool full_parent)
{
return str_(f, full_parent, false, utf8_kw);
}
std::ostream&
print_utf8_sere(std::ostream& os, formula f, bool full_parent)
{
return printer_(os, f, full_parent, true, utf8_kw);
}
std::string
str_utf8_sere(formula f, bool full_parent)
{
return str_(f, full_parent, false, utf8_kw);
}
std::ostream&
print_spin_ltl(std::ostream& os, formula f, bool full_parent)
{
to_string_visitor v(os, full_parent, false, spin_kw);
v.visit(unabbreviate(f, "^MW"));
return os;
}
std::string
str_spin_ltl(formula f, bool full_parent)
{
std::ostringstream os;
print_spin_ltl(os, f, full_parent);
return os.str();
}
std::ostream&
print_wring_ltl(std::ostream& os, formula f)
{
to_string_visitor v(os, true, false, wring_kw);
v.visit(unabbreviate(f, "MW"));
return os;
}
std::string
str_wring_ltl(formula f)
{
std::ostringstream os;
print_wring_ltl(os, f);
return os.str();
}
std::ostream&
print_latex_psl(std::ostream& os, formula f, bool full_parent)
{
return printer_(os, f, full_parent, false, latex_kw);
}
std::string
str_latex_psl(formula f, bool full_parent)
{
return str_(f, full_parent, false, latex_kw);
}
std::ostream&
print_latex_sere(std::ostream& os, formula f, bool full_parent)
{
return printer_(os, f, full_parent, true, latex_kw);
}
std::string
str_latex_sere(formula f, bool full_parent)
{
return str_(f, full_parent, true, latex_kw);
}
std::ostream&
print_sclatex_psl(std::ostream& os, formula f, bool full_parent)
{
return printer_(os, f, full_parent, false, sclatex_kw);
}
std::string
str_sclatex_psl(formula f, bool full_parent)
{
return str_(f, full_parent, false, sclatex_kw);
}
std::ostream&
print_sclatex_sere(std::ostream& os, formula f, bool full_parent)
{
return printer_(os, f, full_parent, true, sclatex_kw);
}
std::string
str_sclatex_sere(formula f, bool full_parent)
{
return str_(f, full_parent, true, sclatex_kw);
}
namespace
{
// Does str match p[0-9]+ ?
static bool
is_pnum(const char* str)
{
if (str[0] != 'p' || str[1] == 0)
return false;
while (*++str)
if (*str < '0' || *str > '9')
return false;
return true;
}
class lbt_visitor final
{
protected:
std::ostream& os_;
bool first_;
public:
lbt_visitor(std::ostream& os)
: os_(os), first_(true)
{
}
void
visit(formula f)
{
if (first_)
first_ = false;
else
os_ << ' ';
op o = f.kind();
switch (o)
{
case op::False:
os_ << 'f';
break;
case op::True:
os_ << 't';
break;
case op::AP:
{
const std::string& str = f.ap_name();
if (!is_pnum(str.c_str()))
escape_str(os_ << '"', str) << '"';
else
os_ << str;
break;
}
case op::Not:
os_ << '!';
break;
case op::X:
os_ << 'X';
break;
case op::F:
os_ << 'F';
break;
case op::G:
os_ << 'G';
break;
case op::Xor:
os_ << '^';
break;
case op::Implies:
os_ << 'i';
break;
case op::Equiv:
os_ << 'e';
break;
case op::U:
os_ << 'U';
break;
case op::R:
os_ << 'V';
break;
case op::W:
os_ << 'W';
break;
case op::M:
os_ << 'M';
break;
case op::Or:
for (unsigned i = f.size() - 1; i != 0; --i)
os_ << "| ";
first_ = true;
break;
case op::And:
for (unsigned i = f.size() - 1; i != 0; --i)
os_ << "& ";
first_ = true;
break;
case op::EmptyWord:
case op::Closure:
case op::NegClosure:
case op::NegClosureMarked:
case op::EConcat:
case op::EConcatMarked:
case op::UConcat:
case op::OrRat:
case op::AndRat:
case op::AndNLM:
case op::Concat:
case op::Fusion:
case op::Star:
case op::FStar:
SPOT_UNIMPLEMENTED();
}
for (auto c: f)
visit(c);
}
};
} // anonymous
std::ostream&
print_lbt_ltl(std::ostream& os, formula f)
{
assert(f.is_ltl_formula());
lbt_visitor v(os);
v.visit(f);
return os;
}
std::string
str_lbt_ltl(formula f)
{
std::ostringstream os;
print_lbt_ltl(os, f);
return os.str();
}
}
}