You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
345 lines
24 KiB
345 lines
24 KiB
#include "PgclParser.h"
|
|
// If the parser fails due to ill-formed data, this exception is thrown.
|
|
#include "storm/exceptions/WrongFormatException.h"
|
|
|
|
namespace storm {
|
|
namespace parser {
|
|
storm::pgcl::PgclProgram PgclParser::parse(std::string const& filename) {
|
|
// Create empty program.
|
|
storm::pgcl::PgclProgram result;
|
|
|
|
// Open file and initialize result.
|
|
std::ifstream inputFileStream(filename, std::ios::in);
|
|
STORM_LOG_THROW(inputFileStream.good(), storm::exceptions::WrongFormatException, "Unable to read from file '" << filename << "'.");
|
|
|
|
// Now try to parse the contents of the file.
|
|
try {
|
|
std::string fileContent((std::istreambuf_iterator<char>(inputFileStream)), (std::istreambuf_iterator<char>()));
|
|
result = parseFromString(fileContent, filename);
|
|
} catch(std::exception& e) {
|
|
// In case of an exception properly close the file before passing exception.
|
|
inputFileStream.close();
|
|
throw e;
|
|
}
|
|
|
|
// Close the stream in case everything went smoothly and return result.
|
|
inputFileStream.close();
|
|
return result;
|
|
}
|
|
|
|
storm::pgcl::PgclProgram PgclParser::parseFromString(std::string const& input, std::string const& filename) {
|
|
PositionIteratorType first(input.begin());
|
|
PositionIteratorType iter = first;
|
|
PositionIteratorType last(input.end());
|
|
|
|
// Create empty program.
|
|
storm::pgcl::PgclProgram result;
|
|
|
|
// Create grammar.
|
|
storm::parser::PgclParser grammar(filename, first);
|
|
try {
|
|
// Start the parsing run.
|
|
bool succeeded = qi::phrase_parse(iter, last, grammar, boost::spirit::ascii::space | qi::lit("//") >> *(qi::char_ - (qi::eol | qi::eoi)) >> (qi::eol | qi::eoi), result);
|
|
STORM_LOG_THROW(succeeded, storm::exceptions::WrongFormatException, "Parsing of PGCL program failed.");
|
|
STORM_LOG_DEBUG("Parse of PGCL program finished.");
|
|
} catch(qi::expectation_failure<PositionIteratorType> const& e) {
|
|
// If the parser expected content different than the one provided, display information about the location of the error.
|
|
std::size_t lineNumber = boost::spirit::get_line(e.first);
|
|
// Now propagate exception.
|
|
STORM_LOG_THROW(false, storm::exceptions::WrongFormatException, "Parsing error in line " << lineNumber << " of file " << filename << ".");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
PgclParser::PgclParser(std::string const& filename, Iterator first) :
|
|
PgclParser::base_type(program, "PGCL grammar"),
|
|
annotate(first),
|
|
expressionManager(std::shared_ptr<storm::expressions::ExpressionManager>(new storm::expressions::ExpressionManager())),
|
|
expressionParser(*expressionManager, invalidIdentifiers)
|
|
{
|
|
this->enableExpressions();
|
|
/*
|
|
* PGCL grammar is defined here
|
|
*/
|
|
// Rough program structure
|
|
program = (qi::lit("function ") > programName > qi::lit("(") > -(doubleDeclaration % qi::lit(",")) > qi::lit(")") > qi::lit("{") >
|
|
variableDeclarations >
|
|
sequenceOfStatements >
|
|
qi::lit("}"))[qi::_val = phoenix::bind(&PgclParser::createProgram, phoenix::ref(*this), qi::_1, qi::_2, qi::_3, qi::_4)];
|
|
sequenceOfStatements %= +(statement);
|
|
sequenceOfStatements.name("sequence of statements");
|
|
|
|
variableDeclarations %= qi::lit("var") > qi::lit("{") > +(integerDeclaration | booleanDeclaration) > qi::lit("}");
|
|
variableDeclarations.name("variable declarations");
|
|
|
|
// Statements
|
|
statement %= simpleStatement | compoundStatement;
|
|
simpleStatement %= assignmentStatement | observeStatement;
|
|
compoundStatement %= ifElseStatement | loopStatement | branchStatement;
|
|
|
|
// Simple statements
|
|
doubleDeclaration = (qi::lit("double ") >> variableName)[qi::_val = phoenix::bind(&PgclParser::declareDoubleVariable, phoenix::ref(*this), qi::_1)];
|
|
integerDeclaration = (qi::lit("int ") > variableName > qi::lit(":=") > expression > qi::lit(";"))[qi::_val = phoenix::bind(&PgclParser::createIntegerDeclarationStatement, phoenix::ref(*this), qi::_1, qi::_2)];
|
|
integerDeclaration.name("integer declaration");
|
|
booleanDeclaration = (qi::lit("bool ") > variableName > qi::lit(":=") > expression > qi::lit(";"))[qi::_val = phoenix::bind(&PgclParser::createBooleanDeclarationStatement, phoenix::ref(*this), qi::_1, qi::_2)];
|
|
booleanDeclaration.name("boolean declaration");
|
|
|
|
assignmentStatement = (variableName > qi::lit(":=") > (expression | uniformExpression) > qi::lit(";"))[qi::_val = phoenix::bind(&PgclParser::createAssignmentStatement, phoenix::ref(*this), qi::_1, qi::_2)];
|
|
observeStatement = (qi::lit("observe") > qi::lit("(") >> booleanCondition >> qi::lit(")") > qi::lit(";"))[qi::_val = phoenix::bind(&PgclParser::createObserveStatement, phoenix::ref(*this), qi::_1)];
|
|
|
|
// Compound statements
|
|
ifElseStatement = (qi::lit("if") > qi::lit("(") >> booleanCondition >> qi::lit(")") >> qi::lit("{") >>
|
|
sequenceOfStatements >>
|
|
qi::lit("}") >> -(qi::lit("else") >> qi::lit("{") >>
|
|
sequenceOfStatements >>
|
|
qi::lit("}")))
|
|
[qi::_val = phoenix::bind(&PgclParser::createIfElseStatement, phoenix::ref(*this), qi::_1, qi::_2, qi::_3)];
|
|
ifElseStatement.name("if/else statement");
|
|
branchStatement = nondeterministicBranch | probabilisticBranch;
|
|
loopStatement = (qi::lit("while") > qi::lit("(") > booleanCondition > qi::lit(")") > qi::lit("{") >>
|
|
sequenceOfStatements >>
|
|
qi::lit("}"))
|
|
[qi::_val = phoenix::bind(&PgclParser::createLoopStatement, phoenix::ref(*this), qi::_1, qi::_2)];
|
|
loopStatement.name("loop statement");
|
|
nondeterministicBranch = (qi::lit("{") >> sequenceOfStatements >> qi::lit("}") >> qi::lit("[]") >> qi::lit("{") >> sequenceOfStatements>> qi::lit("}"))[qi::_val = phoenix::bind(&PgclParser::createNondeterministicBranch, phoenix::ref(*this), qi::_1, qi::_2)];
|
|
probabilisticBranch = (qi::lit("{") >> sequenceOfStatements >> qi::lit("}") >> qi::lit("[") >> expression >> qi::lit("]") >> qi::lit("{") >> sequenceOfStatements >> qi::lit("}"))[qi::_val = phoenix::bind(&PgclParser::createProbabilisticBranch, phoenix::ref(*this), qi::_2, qi::_1, qi::_3)];
|
|
|
|
// Expression and condition building, and basic identifiers
|
|
expression %= expressionParser;
|
|
expression.name("expression");
|
|
booleanCondition = expressionParser[qi::_val = phoenix::bind(&PgclParser::createBooleanExpression, phoenix::ref(*this), qi::_1)];
|
|
uniformExpression = (qi::lit("unif") >> qi::lit("(") >> qi::int_ >> qi::lit(",") >> qi::int_ >> qi::lit(")"))[qi::_val = phoenix::bind(&PgclParser::createUniformExpression, phoenix::ref(*this), qi::_1, qi::_2)];
|
|
variableName %= (+(qi::alnum | qi::lit("_"))) - invalidIdentifiers;
|
|
variableName.name("variable name");
|
|
programName %= +(qi::alnum | qi::lit("_"));
|
|
programName.name("program name");
|
|
|
|
// Enables location tracking for important entities
|
|
auto setLocationInfoFunction = this->annotate(*qi::_val, qi::_1, qi::_3);
|
|
qi::on_success(assignmentStatement, setLocationInfoFunction);
|
|
qi::on_success(observeStatement, setLocationInfoFunction);
|
|
qi::on_success(nondeterministicBranch, setLocationInfoFunction);
|
|
qi::on_success(probabilisticBranch, setLocationInfoFunction);
|
|
qi::on_success(loopStatement, setLocationInfoFunction);
|
|
qi::on_success(ifElseStatement, setLocationInfoFunction);
|
|
|
|
// Enable error reporting.
|
|
qi::on_error<qi::fail>(program, handler(qi::_1, qi::_2, qi::_3, qi::_4));
|
|
qi::on_error<qi::fail>(variableDeclarations, handler(qi::_1, qi::_2, qi::_3, qi::_4));
|
|
qi::on_error<qi::fail>(integerDeclaration, handler(qi::_1, qi::_2, qi::_3, qi::_4));
|
|
qi::on_error<qi::fail>(booleanDeclaration, handler(qi::_1, qi::_2, qi::_3, qi::_4));
|
|
|
|
qi::on_error<qi::fail>(assignmentStatement, handler(qi::_1, qi::_2, qi::_3, qi::_4));
|
|
qi::on_error<qi::fail>(observeStatement, handler(qi::_1, qi::_2, qi::_3, qi::_4));
|
|
qi::on_error<qi::fail>(ifElseStatement, handler(qi::_1, qi::_2, qi::_3, qi::_4));
|
|
qi::on_error<qi::fail>(loopStatement, handler(qi::_1, qi::_2, qi::_3, qi::_4));
|
|
qi::on_error<qi::fail>(branchStatement, handler(qi::_1, qi::_2, qi::_3, qi::_4));
|
|
|
|
|
|
qi::on_error<qi::fail>(expression, handler(qi::_1, qi::_2, qi::_3, qi::_4));
|
|
qi::on_error<qi::fail>(booleanCondition, handler(qi::_1, qi::_2, qi::_3, qi::_4));
|
|
qi::on_error<qi::fail>(uniformExpression, handler(qi::_1, qi::_2, qi::_3, qi::_4));
|
|
|
|
// Adds dummy to the 0-th location.
|
|
std::shared_ptr<storm::pgcl::AssignmentStatement> dummy(new storm::pgcl::AssignmentStatement());
|
|
this->locationToStatement.insert(this->locationToStatement.begin(), dummy);
|
|
} // PgclParser()
|
|
|
|
void PgclParser::enableExpressions() {
|
|
(this->expressionParser).setIdentifierMapping(&this->identifiers_);
|
|
}
|
|
|
|
/*
|
|
* Creators for various program parts. They all follow the basic scheme
|
|
* to retrieve the subparts of the program part and mold them together
|
|
* into one new statement. They wrap the statement constructors and
|
|
* throw excpetions in case something unexpected was parsed, e.g.
|
|
* (x+5) as a boolean expression, or types of assignments don't match.
|
|
*/
|
|
storm::pgcl::PgclProgram PgclParser::createProgram(std::string const& programName, boost::optional<std::vector<storm::expressions::Variable>> parameters, std::vector<std::shared_ptr<storm::pgcl::VariableDeclaration>> const& variableDeclarations, std::vector<std::shared_ptr<storm::pgcl::Statement>> const& statements) {
|
|
// Creates an empty paramter list in case no parameters were given.
|
|
std::vector<storm::expressions::Variable> params;
|
|
if(parameters != boost::none) {
|
|
params = *parameters;
|
|
}
|
|
std::vector<storm::pgcl::VariableDeclaration> declarations;
|
|
for (auto const& decl : variableDeclarations) {
|
|
declarations.push_back(*decl);
|
|
}
|
|
|
|
// Creates the actual program.
|
|
std::shared_ptr<storm::pgcl::PgclProgram> result(
|
|
new storm::pgcl::PgclProgram(declarations, statements, this->locationToStatement, params, this->expressionManager, this->loopCreated, this->nondetCreated, this->observeCreated)
|
|
);
|
|
result->back()->setLast(true);
|
|
// Sets the current program as a parent to all its direct children statements.
|
|
for(storm::pgcl::iterator it = result->begin(); it != result->end(); ++it) {
|
|
(*it)->setParentBlock(result.get());
|
|
}
|
|
return *result;
|
|
}
|
|
|
|
storm::expressions::Variable PgclParser::declareDoubleVariable(std::string const& variableName) {
|
|
storm::expressions::Variable variable = expressionManager->declareRationalVariable(variableName);
|
|
this->identifiers_.add(variableName, variable.getExpression());
|
|
return variable;
|
|
}
|
|
|
|
std::shared_ptr<storm::pgcl::AssignmentStatement> PgclParser::createAssignmentStatement(std::string const& variableName, boost::variant<storm::expressions::Expression, storm::pgcl::UniformExpression> const& assignedExpression) {
|
|
storm::expressions::Variable variable;
|
|
if(!(*expressionManager).hasVariable(variableName)) {
|
|
variable = (*expressionManager).declareIntegerVariable(variableName);
|
|
} else {
|
|
variable = (*expressionManager).getVariable(variableName);
|
|
// Checks if assignment types match.
|
|
if(assignedExpression.which() == 0 && !(variable.getType() == boost::get<storm::expressions::Expression>(assignedExpression).getType())) {
|
|
STORM_LOG_THROW(false, storm::exceptions::WrongFormatException, "Wrong type when assigning to " << variableName << ".");
|
|
}
|
|
}
|
|
std::shared_ptr<storm::pgcl::AssignmentStatement> newAssignment(new storm::pgcl::AssignmentStatement(variable, assignedExpression));
|
|
newAssignment->setLocationNumber(this->currentLocationNumber);
|
|
this->locationToStatement.insert(this->locationToStatement.begin() + this->currentLocationNumber, newAssignment);
|
|
currentLocationNumber++;
|
|
return newAssignment;
|
|
}
|
|
|
|
std::shared_ptr<storm::pgcl::VariableDeclaration> PgclParser::createIntegerDeclarationStatement(std::string const& variableName, storm::expressions::Expression const& assignedExpression) {
|
|
storm::expressions::Variable variable;
|
|
if(!(*expressionManager).hasVariable(variableName)) {
|
|
variable = (*expressionManager).declareIntegerVariable(variableName);
|
|
this->identifiers_.add(variableName, variable.getExpression());
|
|
} else {
|
|
// In case that a declaration already happened.
|
|
variable = (*expressionManager).getVariable(variableName);
|
|
STORM_LOG_THROW(false, storm::exceptions::WrongFormatException, "Declaration of integer variable " << variableName << " which was already declared previously.");
|
|
}
|
|
// Todo add some checks
|
|
return std::make_shared<storm::pgcl::VariableDeclaration>(variable, assignedExpression);
|
|
}
|
|
|
|
std::shared_ptr<storm::pgcl::VariableDeclaration> PgclParser::createBooleanDeclarationStatement(std::string const& variableName, storm::expressions::Expression const& assignedExpression) {
|
|
storm::expressions::Variable variable;
|
|
if(!(*expressionManager).hasVariable(variableName)) {
|
|
variable = (*expressionManager).declareBooleanVariable(variableName);
|
|
this->identifiers_.add(variableName, variable.getExpression());
|
|
} else {
|
|
// In case that a declaration already happened.
|
|
variable = (*expressionManager).getVariable(variableName);
|
|
STORM_LOG_THROW(false, storm::exceptions::WrongFormatException, "Declaration of boolean variable " << variableName << " which was already declared previously.");
|
|
}
|
|
// TODO add some checks.
|
|
return std::make_shared<storm::pgcl::VariableDeclaration>(variable, assignedExpression);
|
|
}
|
|
|
|
std::shared_ptr<storm::pgcl::ObserveStatement> PgclParser::createObserveStatement(storm::pgcl::BooleanExpression const& condition) {
|
|
std::shared_ptr<storm::pgcl::ObserveStatement> observe(new storm::pgcl::ObserveStatement(condition));
|
|
this->observeCreated = true;
|
|
observe->setLocationNumber(this->currentLocationNumber);
|
|
this->locationToStatement.insert(this->locationToStatement.begin() + this->currentLocationNumber, observe);
|
|
currentLocationNumber++;
|
|
return observe;
|
|
}
|
|
|
|
std::shared_ptr<storm::pgcl::IfStatement> PgclParser::createIfElseStatement(storm::pgcl::BooleanExpression const& condition, std::vector<std::shared_ptr<storm::pgcl::Statement> > const& ifBody, boost::optional<std::vector<std::shared_ptr<storm::pgcl::Statement> > > const& elseBody) {
|
|
std::shared_ptr<storm::pgcl::PgclBlock> ifBodyProgram(new storm::pgcl::PgclBlock(ifBody, this->expressionManager, this->loopCreated, this->nondetCreated, this->observeCreated));
|
|
std::shared_ptr<storm::pgcl::IfStatement> ifElse;
|
|
if(elseBody) {
|
|
std::shared_ptr<storm::pgcl::PgclBlock> elseBodyProgram(new storm::pgcl::PgclBlock(*elseBody, this->expressionManager, this->loopCreated, this->nondetCreated, this->observeCreated));
|
|
ifElse = std::shared_ptr<storm::pgcl::IfStatement>(new storm::pgcl::IfStatement(condition, ifBodyProgram, elseBodyProgram));
|
|
(*(ifElse->getIfBody()->back())).setLast(true);
|
|
(*(ifElse->getElseBody()->back())).setLast(true);
|
|
// Sets the current program as a parent to all its children statements.
|
|
for(storm::pgcl::iterator it = ifElse->getElseBody()->begin(); it != ifElse->getElseBody()->end(); ++it) {
|
|
(*it)->setParentBlock(ifElse->getElseBody().get());
|
|
}
|
|
for(storm::pgcl::iterator it = ifElse->getIfBody()->begin(); it != ifElse->getIfBody()->end(); ++it) {
|
|
(*it)->setParentBlock(ifElse->getIfBody().get());
|
|
}
|
|
} else {
|
|
ifElse = std::shared_ptr<storm::pgcl::IfStatement>(new storm::pgcl::IfStatement(condition, ifBodyProgram));
|
|
(*(ifElse->getIfBody()->back())).setLast(true);
|
|
// Sets the current program as a parent to all its children statements.
|
|
for(storm::pgcl::iterator it = ifElse->getIfBody()->begin(); it != ifElse->getIfBody()->end(); ++it) {
|
|
(*it)->setParentBlock(ifElse->getIfBody().get());
|
|
}
|
|
}
|
|
ifElse->setLocationNumber(this->currentLocationNumber);
|
|
this->locationToStatement.insert(this->locationToStatement.begin() + this->currentLocationNumber, ifElse);
|
|
currentLocationNumber++;
|
|
return ifElse;
|
|
}
|
|
|
|
std::shared_ptr<storm::pgcl::LoopStatement> PgclParser::createLoopStatement(storm::pgcl::BooleanExpression const& condition, std::vector<std::shared_ptr<storm::pgcl::Statement> > const& body) {
|
|
std::shared_ptr<storm::pgcl::PgclBlock> bodyProgram(new storm::pgcl::PgclBlock(body, this->expressionManager, this->loopCreated, this->nondetCreated, this->observeCreated));
|
|
std::shared_ptr<storm::pgcl::LoopStatement> loop(new storm::pgcl::LoopStatement(condition, bodyProgram));
|
|
this->loopCreated = true;
|
|
// Sets the current program as a parent to all its children statements.
|
|
for(storm::pgcl::iterator it = loop->getBody()->begin(); it != loop->getBody()->end(); ++it) {
|
|
(*it)->setParentBlock(loop->getBody().get());
|
|
}
|
|
(*(loop->getBody()->back())).setLast(true);
|
|
loop->setLocationNumber(this->currentLocationNumber);
|
|
this->locationToStatement.insert(this->locationToStatement.begin() + this->currentLocationNumber, loop);
|
|
currentLocationNumber++;
|
|
return loop;
|
|
}
|
|
|
|
std::shared_ptr<storm::pgcl::NondeterministicBranch> PgclParser::createNondeterministicBranch(std::vector<std::shared_ptr<storm::pgcl::Statement> > const& left, std::vector<std::shared_ptr<storm::pgcl::Statement> > const& right) {
|
|
std::shared_ptr<storm::pgcl::PgclBlock> leftProgram(new storm::pgcl::PgclBlock(left, this->expressionManager, this->loopCreated, this->nondetCreated, this->observeCreated));
|
|
std::shared_ptr<storm::pgcl::PgclBlock> rightProgram(new storm::pgcl::PgclBlock(right, this->expressionManager, this->loopCreated, this->nondetCreated, this->observeCreated));
|
|
std::shared_ptr<storm::pgcl::NondeterministicBranch> branch(new storm::pgcl::NondeterministicBranch(leftProgram, rightProgram));
|
|
this->nondetCreated = true;
|
|
// Sets the left program as a parent to all its children statements.
|
|
for(storm::pgcl::iterator it = branch->getLeftBranch()->begin(); it != branch->getLeftBranch()->end(); ++it) {
|
|
(*it)->setParentBlock(branch->getLeftBranch().get());
|
|
}
|
|
// Sets the right program as a parent to all its children statements.
|
|
for(storm::pgcl::iterator it = branch->getRightBranch()->begin(); it != branch->getRightBranch()->end(); ++it) {
|
|
(*it)->setParentBlock(branch->getRightBranch().get());
|
|
}
|
|
(*(branch->getLeftBranch()->back())).setLast(true);
|
|
(*(branch->getRightBranch()->back())).setLast(true);
|
|
branch->setLocationNumber(this->currentLocationNumber);
|
|
this->locationToStatement.insert(this->locationToStatement.begin() + this->currentLocationNumber, branch);
|
|
currentLocationNumber++;
|
|
return branch;
|
|
}
|
|
|
|
std::shared_ptr<storm::pgcl::ProbabilisticBranch> PgclParser::createProbabilisticBranch(storm::expressions::Expression const& probability, std::vector<std::shared_ptr<storm::pgcl::Statement> > const& left, std::vector<std::shared_ptr<storm::pgcl::Statement> > const& right) {
|
|
std::shared_ptr<storm::pgcl::PgclBlock> leftProgram(new storm::pgcl::PgclBlock(left, this->expressionManager, this->loopCreated, this->nondetCreated, this->observeCreated));
|
|
std::shared_ptr<storm::pgcl::PgclBlock> rightProgram(new storm::pgcl::PgclBlock(right, this->expressionManager, this->loopCreated, this->nondetCreated, this->observeCreated));
|
|
std::shared_ptr<storm::pgcl::ProbabilisticBranch> branch(new storm::pgcl::ProbabilisticBranch(probability, leftProgram, rightProgram));
|
|
// Sets the left program as a parent to all its children statements.
|
|
for(storm::pgcl::iterator it = branch->getLeftBranch()->begin(); it != branch->getLeftBranch()->end(); ++it) {
|
|
(*it)->setParentBlock(branch->getLeftBranch().get());
|
|
}
|
|
// Sets the right program as a parent to all its children statements.
|
|
for(storm::pgcl::iterator it = branch->getRightBranch()->begin(); it != branch->getRightBranch()->end(); ++it) {
|
|
(*it)->setParentBlock(branch->getRightBranch().get());
|
|
}
|
|
(*(branch->getLeftBranch()->back())).setLast(true);
|
|
(*(branch->getRightBranch()->back())).setLast(true);
|
|
branch->setLocationNumber(this->currentLocationNumber);
|
|
this->locationToStatement.insert(this->locationToStatement.begin() + this->currentLocationNumber, branch);
|
|
currentLocationNumber++;
|
|
return branch;
|
|
}
|
|
|
|
storm::pgcl::BooleanExpression PgclParser::createBooleanExpression(storm::expressions::Expression const& expression) {
|
|
if(expression.hasBooleanType()) {
|
|
storm::pgcl::BooleanExpression booleanExpression = storm::pgcl::BooleanExpression(expression);
|
|
return booleanExpression;
|
|
} else {
|
|
// In case that a non-boolean expression was used (e.g. (x+5)).
|
|
STORM_LOG_THROW(false, storm::exceptions::WrongFormatException, "Non boolean expression was used in a condition.");
|
|
}
|
|
}
|
|
|
|
storm::pgcl::UniformExpression PgclParser::createUniformExpression(int const& begin, int const& end) {
|
|
return storm::pgcl::UniformExpression(begin, end);
|
|
}
|
|
|
|
} // namespace parser
|
|
} // namespace storm
|
|
|