The LIRA 1.x Handbook

Bernd Becker

Christian Dax

Jochen Eisinger

Felix Klaedtke

Note: This handbook is under construction. It refers to the most recent version of LIRA, which is version 1.1.

December 2006


Table of Contents

1. Preface
2. Using LIRA
3. Abstract Syntax Trees
3.1. Creating an Abstract Syntax Tree
3.2. Terms
3.3. Expressions
3.4. Modifying Abstract Syntax Trees
3.5. Deleting Abstract Syntax Trees
4. Execution Plan Language
4.1. Generating a Plan
4.2. The Plan Language
5. Interpreters
5.1. Using an Interpreter
5.2. Existing Interpreters
5.3. Writing Your Own Handlers
6. Low-Level API
6.1. Datastructures
6.2. Creating Automata
6.3. Modifying Automata
6.4. Statistics

1. Preface

There are many classes of system which can be described using first-order logic over mixed integer/real addition, i.e., over the structure (ℝ, ℤ, +, ≤). One class of such systems are linear hybrid automata, that is, systems consisting of digital and analog parts. An efficient decision procedure for this logic is required to analyze this class of systems.

We decide this logic using automata. We construct automata that represent all satisfying variable assignments. Since the binary encoding for reals is of infinite length, we use automata over so-called ω-words. It has been shown that weak deterministic Büchi automata suffice to represent subsets of ℝn that are definable in the first-order logic over (ℝ, ℤ, +, ≤).

LIRA consists of a toolkit for efficiently representing and manipulating such automata. Our aim is to establish new theoretical results that allow for analyzing larger systems, and to put those results into use.

This handbook describes the API of the LIRA toolkit.

The LIRA toolkit implements two APIs. A high-level API where you can define formulas as trees, apply transformations on them, and decide the formulas using a given logic and solver. Additionally, LIRA has a low-level API where you can directly manipulate automata.

2. Using LIRA

Compiling and installing LIRA should be straightforward:


$ ./configure
$ make
$ make check
$ make install

  

The make check command will only run tests if you have a recent version of CppUnit on your machine.

The last make command installs LIRA. The default directory is /usr/local. You can change this directory by running ./configure with the parameter --prefix=/LIRA/is/installed/in/this/directory. make install copies the LIRA binary in the subdirectory bin, the libraries in the subdirectory lib, the header files in the subdirectory include/LIRA, and the documentation in subdirectory share/doc/LIRA. Note that there is also a GENEPI plugin called liblira2fast.so for the model checker FASTer. This plugin is installed in the subdirectory lib.

You can use LIRA in your C/C++ programs by including the header LIRA.h.


#include <LIRA/LIRA.h>

  

When compiling your program, you need to link against libLIRA, for example


cc main.cc -lLIRA -o yourcode

  

3. Abstract Syntax Trees

This chapter will introduce abstract syntax trees which are used in LIRA to represent formulas. All preprocessing and rewriting is done on this data structure.

3.1. Creating an Abstract Syntax Tree

All abstract syntax tree related stuff is defined in the namespace ast. The abstract syntax tree itself consists of classes which inherit from ast::ast_node.

An abstract syntax tree is constructed by creating single nodes, and connecting them bottom up.


// create an AST for x = 42
ast::ast_node *var_x    = new ast::variable("x");
ast::ast_node *const_42 = new ast::constant(42);
ast::ast_node *eq       = new ast::relation("=", 2);

var_x->connect_to(*eq, 0);
const_42->connect_to(*eq, 1);

    

Every abstract syntax tree node defines iterator to access its children and its parents.


ast::ast_node::child_iterator ast::ast_node::children_begin() const;
ast::ast_node::child_iterator ast::ast_node::children_end() const;

ast::ast_node::parent_iterator ast::ast_node::parent_begin() const;
ast::ast_node::parent_iterator ast::ast_node::parent_end() const;

    

Connections between nodes are always made from a child to its parent. You can only connect to parents of an arity greater or equal to one, and you have to define the position to connect to. The same holds for disconnecting.


bool ast::ast_node::connect_to(ast::ast_node& parent, unsigned poisiton);
bool ast::ast_node::disconnect_from(ast::ast_node& parent, unsigned poisiton);

    

The return value of connect_to and disconnect_from indicates whether the connection was successfully established. If some other node already was connected to this position, it is disconnected before the new connection is established.

When a node is destructed, it disconnects all its children and disconnects from all its parents.

3.2. Terms

The following AST nodes represent terms.

3.2.1. Atomic Terms

LIRA defines two different atomic terms, namely constants and variables.

3.2.1.1. Constants

ast::constant(int64_t numerator, int64_t denominator = 1);

	

ast::constant represents a single rational constant. If the denominator is zero, an exception of the type ast::ast_node::DivisionByZero is thrown. The numerator and the denominator may be accessed via


int64_t ast::constant::get_numerator() const;
int64_t ast::constant::get_denominator() const;

	
3.2.1.2. Variables

ast::variable(const std::string& name);

	

ast::variable represents a single variable. The domain of the variable is not defined. If no name is given an exception of the type ast::ast_node::EmptyName is thrown. The name of the variable is accessible via


const std::string& ast::variable::get_name() const;

	

3.2.2. Composite Terms

It is possible to compose terms from other terms using functions. Furthermore, a term polynom is defined which represents a linear polynom.

3.2.2.1. Functions

ast::function(const std::string& name, unsigned arity);

	

An ast::function represents a function identified by its name of a given arity. Technically, you could use functions of arity zero as variables, but you should prefer the object ast::variable for this purpose. If no name was supplied an exception of type ast::ast_node::EmptyName is thrown. The arity of a function object equals the distance between children_begin and children_end, the name can be queried via


const std::string& ast::function::get_name() const;

	

You can use arbitrary function names, however, LIRA interprets the following names per default.

NameAritySemantics
+2Addition
-2Substraction
*2Multiplication
/2Division
~1unary Minus
3.2.2.2. Polynoms

Polynoms are the most complex ast node you will encounter. The represent linear polynoms of the form a0 + a1x1 + ... + an xn. Polynoms are constructed either as empty polynom, or, unlike other ast nodes, as a copy of a given polynom.


ast::polynom();
ast::polynom(const ast::polynom& other);

	

You can add either constants, or multiples of a variable, or other polynoms.


void ast::polynom::add(int64_t numerator, int64_t denominator = 1);
void ast::polynom::add(const std::string& var, int64_t numerator, int64_t denominator = 1);
void ast::polynom::add(const ast::polynom& other);

	

It is also possible to negate all coefficients or multiply them with a given constant.


void ast::polynom::negate();
void ast::polynom::mult(int64_t numerator, int64_t denominator = 1);
void ast::polynom::div(int64_t numerator, int64_t denominator = 1);

	

If you want to ensure that all coefficients are integer, you can multiple the polynom with the least common multiple of all denominators.


int64_t ast::polynom::derationalize();

	

The return value is the factor used to multiply the polynom.

All these functions may throw exceptions of the type ast::ast_node::Overflow or ast::ast_node::DivisonByZero.

It is possible to query certain informations about the polynom, i.e. whether it contains no variables, whether it is equal to zero, or whether it contains rational coefficients.


bool ast::polynom::is_constant() const;
bool ast::polynom::is_null() const;
bool ast::polynom::is_rational() const;

	

You can also query the coefficients of the variables and the constant element:


int64_t ast::polynom::get_numerator(const std::string& var) const;
int64_t ast::polynom::get_denominator(const std::string& var) const;
int64_t ast::polynom::get_numerator() const;
int64_t ast::polynom::get_denominator() const;

	

Finally, it is possible to "translate" a polynom. This operation can be used for two purposes. First, it will return you all variable names present in the polynom. And second, it will allow you to access the variables in a given order. There are special functions available to read the coefficients in that order.


void ast::polynom::translate(std::vector<std::string>& variables);
int64_t ast::polynom::get_numerator(unsigned index) const;
int64_t ast::polynom::get_denominator(unsigned index) const;

	

For the first purpose, invoke the function with an empty vector of strings. After the function call, the vector will contain all variable names. For the second purpose, you should put the desired variable order in the vector and then invoke the function.


// create a polynom 2x + y
ast::polynom *p = new ast::polynom;
std::vector<std::string> variables;

p->add("y", 1);
p->add("x", 2);
variables.push_back("x");
variables.push_back("y");
variables.push_back("z");
p->translate(variables);

// will output 2*x, 1*y, 0*z
for (unsigned idx=0; idx<variables.size(); idx++)
  std::cout << p->get_numerator(idx) << "*" << variables[idx] << "\n";

	

3.3. Expressions

You can build expressions by relations over terms and combinations of terms. Furthermore, there are some extra expressions that allow you to permutate variables etc.

3.3.1. Standard Expressions

3.3.1.1. Relations

Atomic expressions are made from relations over terms.


ast::relation(const std::string& name, unsigned arity);

	

ast::relation defines a relation with a given arity. If no name was supplied, an exception of type ast::ast_node::EmptyName is thrown. You can query the name of the relation using


const std::string& ast::relation::get_name() const;

	

You can use arbitrary names for your relations, however, LIRA interprets the following names.

NameAritySemantics
=2equal
!=2not equal
<2strictly less than
<=2less or equal
>2strictly greater than
>=2greater or equal
true0true
false0false
int1term is integer

Relations of arity 0 are interpreted as boolean variables.

3.3.1.2. Boolean Connectives

You can compose expressions of other expressions using boolean connectives.


ast::bool_and();
ast::bool_or();
ast::bool_xor();
ast::bool_iff();
ast::bool_implies();
ast::bool_minus();
ast::bool_not();

	

All connectives but ast::bool_not are binary. The semantics are as expected. You can think of ast::bool_minus as "set minus (\)", i.e., x1 \ x2 ⇎ x1 ∧ ¬ x2 ⇎ ¬ (x1 ⇒ x2) .

3.3.1.3. Quantifiers

LIRA defines both universal and existential quantifiers.


ast::exists(const std::string& variable);
ast::forall(const std::string& variable);

	

If the variable name is empty an exception of the type ast::ast_node::EmptyName is thrown. The name of the variable can be obtained by


const std::string& ast::exists::get_variable() const;
const std::string& ast::forall::get_variable() const;

	

3.3.2. Other Expressions

Currently, there are two other types of expressions available, namely permutations and external automata.

3.3.2.1. Permutations

ast::permutation();

	

A permutation stores information about which variable is to be mapped to which other variable.


void ast::permutation::map(const std::string& from, const std::string& to);
const std::string& get(const std::string& var) const;

	

When creating a permutation, you have to make sure that the permutation is complete, i.e. if you map a variable "x" to a variable "y", you have to map "y" somewhere else. If you don't specify where to map a variable, it is mapped on itself.

3.3.2.2. External Automata

The solver backend can load automata from streams. To build formulas that reference such automata, you can use the node ast::external_automata as a placeholder.


void ast::external_automaton(const std::string& filename);

	

The filename should point to a file with an automaton in the graphviz format as produced by the backend function rva::dump_dot.

Since the file format doesn't contain information about the variable ordering, you should always connect an ast::external_automaton node to a ast::permutation node. When the automaton is loaded, the first variable in the file is assigned to the first variable in the current variable order, etc. If the first variable in the file should be another variable, you have to fix this using the permutation.

3.4. Modifying Abstract Syntax Trees

LIRA implements visitors to modify abstract syntax trees. There is a number of pre-defined visitors, but you can also write your own visitors.

The visitors roughly fall into three classes, checks, functions, and planners. The latter will be discussed in the next chapter.

3.4.1. Checks

If you plan to construct your own abstract syntax trees, it is probably best to run some checks on them.

LIRA defines three basic checks namely isValidStructure, isValidSyntax, and isTree.


bool test_my_ast(ast::ast_node* root) {
  ast::visitor::isValidStructure;
  ast::visitor::isValidSyntax;
  ast::visitor::isTree;

  try {
    root->Accept(isValidStructure);
    root->Accept(isTree);
    root->Accept(isValidSyntax);
    return true;
  } catch (ast::visitor::ast_visitor::FalseResult& res) {
    std::cout << "Invalid AST: " << res.get_reason() << "\n";
    return false;
  }
}

      

The visitor ast::visitor::isValidStructure checks whether the AST is fully connected and free of cycles. The visitor ast::visitor::isTree checks whether the AST is actually a tree (it could be a DAG as well). And the visitor ast::visitor::isValidSyntax checks whether the syntax of the formula is correct, i.e. terms are made of constants, variables, and functions, and not relations for example, etc.

There is also a visitor that will generate a graphviz representation of the AST which might be handy for debugging.


ast::ast_node* ex       = new ast::exists("x");
ast::ast_node* node_and = new ast::bool_and;
ast::ast_node* rel1     = new ast::relation("!=", 2);
ast::ast_node* rel2     = new ast::relation("=", 2);
ast::ast_node* var1     = new ast::variable("x");
ast::ast_node* var2     = new ast::variable("x");
ast::ast_node* var3     = new ast::variable("y");
ast::ast_node* const1   = new ast::constant(0);
ast::ast_node* const2   = new ast::constant(0);
ast::ast_node* func     = new ast::function("+", 2);

var1->connect_to(*rel1, 0);
const1->connect_to(*rel1, 1);

var2->connect_to(*func, 0);
var3->connect_to(*func, 1);

func->connect_to(*rel2, 0);
const2->connect_to(*rel2, 1);

rel1->connect_to(*node_and, 0);
rel2->connect_to(*node_and, 1);

node_and->connect_to(*ex, 0);

ast::visitor::dumpDot visitor;

ex->Accept(visitor);

visitor.dump(std::cout);

      

This will generate the following graph

3.4.2. Functions

It is also possible to modify the AST using a visitor. The functions will do such modifications. Currently, there are three functions implemented.

3.4.2.1. Negation Normal Form

The visitor ast::visitor::NNF will put your AST in negation normal form, i.e. push negations inwards, and replace universal quantifiers with existential quantifiers.


ast::ast_node* nnf(ast::ast_node *root) {
  ast::visitor::NNF visitor(true);
  try {
    return root->Accept(visitor);
  } catch (ast::visitor::ast_visitor::FalseResult& res) {
    std::cout << "Could not turn into NNF: " << res.get_reason() << "\n";
    return NULL;
  }
}

        

Note that the return value of Accept is the new root of the AST, the old root may no longer be valid.

Also note that the visitor was constructed with the parameter true. This means that the visitor will free nodes it replaces.

3.4.2.2. Propagation of Boolean Constants

The visitor ast::visitor::booleanPropagate will propagate boolean constants where possible, i.e. x1 ∨ true is converted to just true.


ast::ast_node* propagate(ast::ast_node *root) {
  ast::visitor::booleanPropagate visitor(true);
  try {
    return root->Accept(visitor);
  } catch (ast::visitor::ast_visitor::FalseResult& res) {
    std::cout << "Could not propagate boolean constants: " << res.get_reason() << "\n";
    return NULL;
  }
}

        

Note that the return value of Accept is the new root of the AST, the old root may no longer be valid.

Also note that the visitor was constructed with the parameter true. This means that the visitor will free nodes it replaces.

3.4.2.3. Converting Terms to Polynoms

The third function converts terms to polynoms if possible.


ast::ast_node* insert_polynoms(ast::ast_node *root) {
  ast::visitor::makePolynom visitor(true);
  try {
    return root->Accept(visitor);
  } catch (ast::visitor::ast_visitor::FalseResult& res) {
    std::cout << "Could not insert polynom nodes: " << res.get_reason() << "\n";
    return NULL;
  } catch (ast::visitor::ast_visitor::Overflow& res) {
    std::cout << "Numeric overflow during creation of polynoms\n";
    return NULL;
  }
}

        

Note that the return value of Accept is the new root of the AST, the old root may no longer be valid.

Also note that the visitor was constructed with the parameter true. This means that the visitor will free nodes it replaces.

3.4.3. Writing your own Visitor

If you want to implement additional checks or functions, you can easily implement your own visitor.

For each type of AST node you want to visit, your visitor has to implement a Visit function. If you do not want to differentiate between certain types, like expressions for example, you can also implement a single function for all nodes of that type. The existing classes are:

NameInlcudes
ast::ast_nodeall
ast::expressionast::quantifier, ast::boolean, ast::atomic, ast::permutation, ast::external_automaton
ast::quantifierast::exists, ast::forall
ast::booleanast::bool_and, ...
ast::atomicast::relation
ast::termast::function, ast::variable, ast::constant

All Visit functions have to pass the visitor to all childrens of the node (if appropriate), and return a pointer to the resulting node (or the input node, if nothing was changed).

The following visitor collects the unbound variables in an AST.


class unboundVariables :
	public ast::visitor::ast_visitor,
	public Loki::BaseVisitor,
	public Loki::Visitor<Loki::Seq<
		ast::ast_node,
		ast::variable,
		ast::polynom,
		ast::relation,
		ast::quantifier>::Type, ast::ast_node*>
{
	private:
		std::set<std::string> vars_;

	public:
		virtual ~unboundVariables() {}

		virtual void reset() { vars_.clear(); }

		const std::set<std::string>& get_result() const {
			return vars_;
		}

		ast::ast_node* Visit(ast::ast_node& node) {
			ast::ast_node::child_iterator iter;

			for (iter = node.children_begin(); iter != node.children_end(); iter++)
				(*iter)->Accept(*this);

			return &node;
		}

		ast::ast_node* Visit(ast::variable& node) {
			vars_.insert(node.get_name());
			return &node;
		}

		ast::ast_node* Visit(ast::relation& node) {
			this->Visit(*(ast::ast_node*)&node);

			if ((std::distance(node.children_begin(), 
				node.children_end()) == 0) &&
				(node.get_name() != "true") && 
				(node.get_name() != "false"))
					vars_.insert(node.get_name());

			return &node;
		}

		ast::ast_node* Visit(ast::polynom& node) {
			ast::polynom p(node);
			std::vector<std::string> variables;
			p.translate(variables);
			std::vector<std::string>::iterator iter;
			for (iter = variables.begin();
				iter != variables.end();
				iter++)
					vars_.insert(*iter);
			return &node;
		}

		ast::ast_node* Visit(ast::quantifier& node) {
			this->Visit(*(ast::ast_node*)&node);

			vars_.erase(node.get_variable());
			return &node;
		}
};

      

3.5. Deleting Abstract Syntax Trees

You can also use an visitor to free an abstract syntax tree. The visitor freeChildren will free the children of an AST.


void delete_ast(ast::ast_node* root) {
  ast::visitor::freeChildren visitor;
  root->Accept(visitor);
  delete root;
}

    

4. Execution Plan Language

After you have constructed an abstract syntax tree that represents the formula you want to solve, you have to generate a plan how to check its satisfiability.

This plan is a simple program in the execution plan language that will be interpreted by the LIRA solver.

The most simple way to obtain such a program is using one of the two visitors, simplePlannerReal and simplePlannerInt, which generate plans for the reals and the integer solver respectively.

4.1. Generating a Plan

Before you can invoke a planner, you have to replace all terms with polynoms. Also, if you have boolean variables, you need to define a variable order for the real or integer variables and for the boolean variables.


std::vector<std::string> real_variables;
std::vector<std::string> boolean_variables;

ast::ast_node *root = magic_ast_producer("exists (x : x != 0 and x + y = 0)");

real_variables.push_back("x");
real_variables.push_back("y");

std::vector<plan::plan> plan;
ast::visitor::simplePlannerReal visitor(real_variables, boolean_variables);

root->Accept(visitor);
plan = visitor.get_plan();

    

magic_ast_producer does not exists by the way...

Please note that simplePlannerReal and simplePlannerInt produce different plans, because strict inequalities are treated differently.

If you don't use ast::external_automata, ast::permutation, and no boolean variables, you don't have to define a variable order. The planner will then generate a variable order for you.

4.2. The Plan Language

The plan language is build of single instructions that create and modify sets. The single commands are concatenated into a vector which is passed to the interpreter.

4.2.1. Declaration part

Before you can use and modify sets, you have to declare variables for sets and variables for reals, integers, and booleans.


plan::plan item;

// declare variables 0 to 7 to be reals (ints, booleans, sets)
item.command = plan::DECLARE;
item.param.declare.start = 0;
item.param.declare.count = 7;
item.param.declare.domain = plan::REAL; // plan::INT, plan::BOOL, plan::SET

      

You can declare variables anywhere you want, because the program is scanned for declarations prior to execution.

You can't declare the same variable for different domains. You can however use the same numbers for variables and sets. You don't have to use consecutive numbers for declaring variables.

In the rest of the program, variables are refered to by their respective index starting from zero.

4.2.2. Creating Sets

The following commands can be used to create sets.


plan::plan item;

// store in 'result' the set satisfying polynom = 0
item.command = plan::EQUATION;
item.param.atomic.polynom = polynom;
item.param.atomic.result = result;

// store in 'result' the set satisfying polynom <= 0
item.command = plan::INEQUATION;
item.param.atomic.polynom = polynom;
item.param.atomic.result = result;

// store in 'result' the empty set(false) or the opposite(true)
item.command = plan::EVERYTHING;
item.param.everything.value = empty_set_or_everything;
item.param.everything.result = result;

// store in 'result' the set where x is integer
item.command = plan::INTEGER;
item.param.integer.var = x;
item.param.integer.result = result;

// store in 'result' the set where the boolean variable x is true/false
item.command = plan::PROPOSITION;
item.param.prop.var = x;
item.param.prop.result = result;

// create a set from a automaton stored in a file
item.command = plan::LOAD;
item.param.load.file = filename;
item.param.load.result = result;

      

You have to make sure the destination set is not used when creating a set. Once you have used a set variable, you have to delete it first before you can store another set into it.

You also have to make sure that the variables referenced in some commands are of the correct domain.

4.2.3. Modifying Sets

Once you have created sets you can start modifying them using the following commands.


plan::plan item;

// boolean operation on set1 and set2
item.command = plan::AND; // plan::OR, plan::IMPLIES, plan::IFF, plan::MINUS, plan::XOR
item.param.boolean.op1 = set1;
item.param.boolean.op2 = set2;
item.param.boolean.result = result;

// invert the set
item.command = plan::NOT;
item.param.boolean.op1 = set;
item.param.boolean.result = result;

// existential quantify variable
item.command = plan::EXISTS;
item.param.quantifier.var = var;
item.param.quantifier.set = set;
item.param.quantifier.result = result;

// permutation
item.command = plan::PERMUTE;
item.param.permutate.map = map;
item.param.permutate.set = set;
item.param.permutate.result = result;

// move set
item.command = plan::MOVE;
item.param.move.set = set;
item.param.move.result = result;

// delete set
item.command = plan::DELETE;
item.param.del.set = set;

      

The commands plan::MOVE and plan::DELETE free the set pointed to by the parameter set.

The map given to the command plan::PERMUTE has to be a complete permutation for all variables, i.e. including sets. However, only variables from the same domain can be mapped to each other. Also, you must not permute set variables, they have to point to themselves.

4.2.4. Control Commands

Besides creating and modifying sets, you can also test them for emptyness and control the flow of the program.


plan::plan item;

// do nothing
item.command = plan::NOP;

// stop the execution
item.command = plan::RETURN;
item.param.ret.value = return_value;

// stop the execution if a set is (not) empty
item.command = plan::RETURN_IF;
item.param.ret.value = return_value;
item.param.ret.check_empty = check_if_empty_or_not;
item.param.ret.set = set;

// goto another line (the first line is 0)
item.command = plan::GOTO;
item.param.jump.label = line;

// goto another line if a set is (not) empty
item.command = plan::GOTO_IF;
item.param.jump.label = line;
item.param.jump.check_empty = check_if_empty_or_not;
item.param.jump.set = set;

// break execution 
item.command = plan::BREAKPOINT;

      

Note that plan::DECLARE commands encountered during execution are treated as plan::NOP.

5. Interpreters

Once you have created an execution plan, you can feed it to an interpreter. Currently, there are three interpreters, one for the logic over reals and integers, one for integers only, and one for presburger arithmetic.

You can also override the actions taken for specific commands, or even write your own interpreter by supplying actions for all commands.

5.1. Using an Interpreter

In order to use an interpreter, you have to create a automaton manager for the low-level interface, and a log object. Then you have to select the interpreter you want to use, and you can start.


std::vector<plan::plan> plan = magic_get_plan("exists ( x : x != 0 and x + y = 0)");

rva::Manager = new rva::Manager;
logging::Log *log = new logging::StdLog(std::cerr); // use logging::NoLog for no log
log->minLog(logging::INFO);

exec::execute solver(manager, plan, log);

solver.register_handler(exec::handler::real_handlers);

bool sat;

try {
  sat = solver.run();
} catch (exec::execute::EmptyPlan& ex) {
  log->log(logging::ERROR, "no plan generated");
} catch (exec::execute::DeclarationError& ex) {
  log->log(logging::ERROR, "invalid declaration");
} catch (exec::execute::NoManager& ex) {
  log->log(logging::ERROR, "no manager supplied");
} catch (exec::execute::Breakpoint& ex) {
  log->log(logging::ERROR, "breakpoint in line %d", ex.get_line());
} catch (exec::execute::RuntimeError& ex) {
  log->log(logging::ERROR, "runtime error in line %d: %s", ex.get_line(), ex.get_msg());
}


    

As you might have already guessed, there is no function magic_get_plan.

The exec::execute object allows you to gather a lot of information about the current state and modify most of it. The interface to do this is described in the following.

5.1.1. Executing Plans

Plans can be either executed using the function run, or in single step mode.


bool exec::execute::run();
bool exec::execute::single_step(bool& result);

      

The return value of function single_step indicates whether the execution has terminated. In that case, the parameter result contains the return value of the program.

5.1.2. Accessing the State of the Interpreter

The following functions can be used to modify the current line of the program.


unsigned exec::execute::get_current_line();
void exec::execute::set_current_line(unsigned line);

      

It is also possible to set a return code. Note that if you set the return code, the execution will terminate with this return code. This function is used to implement for example the command plan::RETURN.


bool exec::execute::get_return_code(bool& value) const;
void exec::execute::set_return_line(bool value);

      

The return value of get_return_code indicates whether the return code was actually set.

5.1.3. Functions for Implementing Command Handlers

For implementing command handlers, you also need to be able to access the low-level manager and the log object.


rva::Manager* exec::execute::get_manager() const;
logging::Log& exec::execute::get_log();

      

The rest of the exec::execute interface is dedicated to accessing sets and variables. The following functions return the number of declared sets and variables, as well as the domains of variables.


unsigned exec::execute::get_variable_count() const;
unsigned exec::execute::get_set_count() const;
plan::domain_type exec::execute::get_domain(unsigned var) const;

      

The number of variables returned might be larger than the number you have declared. This can be the case if you declared non-continous blocks of variables. However, there are only as many variables available as you have declared. If you query the domain of an undeclared variable an exception of type exec::execute::RuntimeError is thrown.

The following commands are for accessing sets.


rva::automaton* exec::execute::get_set(unsigned set) const;
void exec::execute::set_set(unsigned no, rva::automaton* set);
void exec::execute::clear_set(unsigned set, bool del = true);

      

All functions will throw exceptions of the type exec::execute::RuntimeError if you try to access non-existing sets or overwrite existing sets.

The function clear_set deletes the low-level set per default. If you don't want this, you have to use the parameter del.

The function get_set returns the last set accessed if invoked with the magic number INT_MAX. This is handy for dumping the final automaton for example.

5.2. Existing Interpreters

There are currently three different interpreters, or actually four, because for reals you can use either the standard Büchi automaton based interpreter, or the new don't-care set based interpreter.

The interpreters are selecting by registering the respective handlers:

NamePlannerDescription
real_handlerssimplePlannerRealFOL(ℝ, ℤ, +, ≤) using weak deterministic Büchi automata
real_handlers_dcsimplePlannerRealFOL(ℝ, ℤ, +, ≤) using weak deterministic Büchi automata with don't-care words
int_handlerssimplePlannerImtFOL(ℤ, +, ≤) using DFA
presburger_handlerssimplePlannerImtFOL(ℕ, +) using DFA

5.3. Writing Your Own Handlers

You can easily extend the interpreter by overriding the actions for single commands, or writing your own set of handlers implementing all commands.

It is not possible to change the behaviour for the commands plan::DECLARE and plan::BREAKPOINT.

All other commands can be overwritten by providing an object derived from exec::cmd_handler.


class new_goto : public exec::cmd_handler {
	public:
		virtual ~new_goto() {}
		virtual bool operator()(execute& exec, const plan::plan& item) {
			exec.get_log().log(logging::INFO, "Dijkstra says GOTO is bad!");
			return false;
		}
};

exec::execute solver(manager, plan, log);

solver.register_handler(exec::handler::real_handlers);
solver.register_handler(plan::GOTO, new new_goto);

    

6. Low-Level API

The low-level API is only interesting if you plan to modify the automata directly. In contrast to the high-level API, it's a minefield and full of hidden traps and assumptions.

6.1. Datastructures

6.1.1. The Automaton Datatype

The solver backend uses the datatype rva::automaton for all its operations. It basically consists of two fields, an vector of BDD nodes representing the transitions, and a vector of integer values indicating the acceptance type of each state.


class automaton {
	public:
		std::vector<bdd::DdNode*> transitions_;
		std::vector<unsigned> stateStatus_;
		Manager *manager_;
		int flags_;
};

      

The acceptance type is either 0, 1, or two for rejecting, accepting, and don't-care. Don't-care states are special states that do not get flipped during negation.

The type of the automaton is indicated by the field flags_.

The other fields not mentioned here are used for statistics and for a faster minimization algorithm.

You have to make sure that there is never a transition to state zero. State zero is always the start state and must not be reachable from any other state including itself.

All states must be reachable from the start state. That is, there must not be any unused states.

6.1.2. The Manager

The manager contains a link to the BDD manager and has a function which returns a poison value. This value can be used for the modifed BDD apply functions defined. If the function you apply to a BDD changes you should use a new poison value.

There should always be only one manager in use.

6.1.3. The BDD transitions

When construction transitions, the BDD variable zero has a special meaning. It represents the decimal seperator. If it is true all other variables are ignored and the letter read is interpreted as the decimal seperator.

In the case of RVAs the decimal seperator is ignored in the start state and all states corresponding to the fractional part of the automaton.

In the case of NDDs the BDD variable zero is always ignored.

If the low-level API referes to a variable zero, it means the BDD variable one. In general the variable i refers to the BDD variable i+1.

6.2. Creating Automata

Similar to the command language, automata are created for equations, inequations, the complete or empty set, propositional variables, and the relation int.


// create (in)equations
rva::automaton* rva::equation(rva::Manager* mgr, const std::vector<int>& coefficitions, int rhs);
rva::automaton* rva::inequation(rva::Manager* mgr, const std::vector<int>& coefficitions, int rhs);
rva::automaton* rva::int_equation(rva::Manager* mgr, const std::vector<int>& coefficitions, int rhs);
rva::automaton* rva::int_inequation(rva::Manager* mgr, const std::vector<int>& coefficitions, int rhs);
rva::automaton* rva::presburger(rva::Manager* mgr, const std::vector<int>& coefficitions, int rhs);

// complete or empty set, type is rva::RVA, rva::NDD, or rva::PRESBURGER
rva::automaton* rva::accept_all(rva::Manager* mgr, bool everything, int type);
rva::automaton* rva::presburger_accept_all(rva::Manager* mgr, bool everything, unsigned dimension);

// propositional variable
rva::automaton* rva::boolean_automaton(rva::Manager *mgr, unsigned var, bool state);

// real variable is integer
rva::automaton* rva::integer(rva::Manager *mgr, unsigned var);

    

The function presburger_accept_all returns the complete or empty set for presburger where only the first dimension variables are referenced. The function accept_all would reference all variables used so far.

6.3. Modifying Automata

Basically, you can negate automata, join two automata using a product construction, and project out a single variable.

However, you also have to normalize automata (in the case of Büchi automata) and minimize them. The solver backend won't do that automatically for you.

The functions for modifying automata are


// merge two automata
rva::automaton* rva::product(const rva::automaton *a1, const rva::automaton *a2,
                             rva::rvaProductFunction function);

// negate an automaton
rva::automaton* rva::negate(const rva::automaton *input);

// project out a variable
rva::automaton* rva::exists(const std::vector<unsigned>& variables, const rva::automaton* input);

    

The parameter function to the function product specifies the kind of product construction. It can be one of Implication, BiImplication, And, Or, ExOr, or Minus.

The function exists takes a whole vector of variables to quantify. This doesn't work for don't-care automata.

RVA automata have to be normalized before you can minimize them. You should always minimize automata. The atomic constructions should also be minimized.


// minimize an automaton
rva::automaton* rva::minimize(rva::automaton * input);

// normalize automaton, callback for RVAs (don't care automata) should be
// rva::AutoNormalize (rva::AutoNormalizeDc)
rva::automaton* rva::walk_scc(rva::automaton *input, rva::SccVisitor& callback);

    

If you want to quantify over a variable using don't care automata, you have to uncompress the track first. This is done like this.


rva::automaton *set = magic_make_automaton();

// uncompress the track corresponding to the variable we want to quantify
rva::AutoUncompress uc_callback(var);
rva::automaton *uncompressed = rva::uncompress(set, var);
rva::automaton *normalized = rva::walk_scc(uncompressed, uc_callback);
delete uncompressed;
rva::automaton *minimized = rva::minimize(normalized);
delete normalized;

// quantify the variable
std::vector<unsigned> vars;
vars.push_back(var);
rva::automaton *quantified = rva::exists(vars, minimized);
delete minimized;
rva::AutoNormalizeDc dc_callback;
normalized = rva::walk_scc(quantified, dc_callback);
delete quantified;
rva::automaton *result = rva::minimize(normalized);
delete normalized;

// result is in 'result' now

    

You can also swap the variables referenced in a set. The meaning of the variable map is the same as for the command plan::PERMUTE.


rva::automaton* rva::swap_variables(const rva::automaton* input, const std::map<unsigned, unsigned>& varmap);

    

Finally, you can check whether an automaton is empty, dump and load it to a file, produce a satisfying variable assignment, and increase the dimension of presburger automata.


// is the minimized automaton empty? 
bool rva::is_empty(const rva::automaton* input);

// dump an automaton
void rva::dump_dot(const rva::automaton* input, std::ostream& stream);

// load an automaton (only works for RVAs, also loads RVAs produced by LASH)
void rva::load_dot(rva::Manager* mgr, std::istream& stream);

// return an example contained in the automaton input
void rva::dump_example(const rva::automaton* input, std::vector<std::string>& example);

// increase dimenson of a presburger automaton
rva::automaton* rva::presbuger_fixup(const rva::automaton* input, unsigned dimension);

    

The dump_dot, load_dot, and dump_example functions are more or less for debugging only. They turned out to be useful sometimes nevertheless.

Presburger automata have a special structure. If you use the normal interpreter for the command language, the interpreter makes sure that all automata have the same dimensionality by referencing all variables before the actual execution starts. If you don't do that and change the number of variables as the GENEPI plugin does for example, you have to fixup the automaton to the correct dimension.

6.4. Statistics

There are two functions to gather statistics about the runtime of the constructions.


struct stats {
  struct timespec runtime;
  unsigned int_nodes;
  unsigned frac_nodes;
  unsigned scc_int_count;
  unsigned scc_frac_count;
  int int_iter;
  int frac_iter;
};

struct rva::stats& get_stats(const rva::automaton* input, bool expensive);
struct rva::stats& get_time(const rva::automaton* input);

    

The function get_stats will fill in all fields of the structure stats if the parameter expensive is true, and all but the scc fields if not. The fields are only meaningful for RVAs. The fields int_iter and frac_iter are not used at the moment.

The function get_time will only fill in the field runtime.