Merge pull request #542 from joakim-hove/actionx-parse

Actionx parse
This commit is contained in:
Joakim Hove 2018-11-09 20:20:26 +01:00 committed by GitHub
commit 01abada947
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 758 additions and 1 deletions

View File

@ -74,6 +74,7 @@ if(ENABLE_ECL_INPUT)
src/opm/parser/eclipse/EclipseState/IOConfig/IOConfig.cpp
src/opm/parser/eclipse/EclipseState/IOConfig/RestartConfig.cpp
src/opm/parser/eclipse/EclipseState/Runspec.cpp
src/opm/parser/eclipse/EclipseState/Schedule/ActionAST.cpp
src/opm/parser/eclipse/EclipseState/Schedule/ActionContext.cpp
src/opm/parser/eclipse/EclipseState/Schedule/Actions.cpp
src/opm/parser/eclipse/EclipseState/Schedule/ActionX.cpp
@ -440,6 +441,7 @@ if(ENABLE_ECL_INPUT)
opm/parser/eclipse/EclipseState/Aquancon.hpp
opm/parser/eclipse/EclipseState/AquiferCT.hpp
opm/parser/eclipse/EclipseState/Aquifetp.hpp
opm/parser/eclipse/EclipseState/Schedule/ActionAST.hpp
opm/parser/eclipse/EclipseState/Schedule/ActionContext.hpp
opm/parser/eclipse/EclipseState/Schedule/Actions.hpp
opm/parser/eclipse/EclipseState/Schedule/ActionX.hpp

View File

@ -0,0 +1,141 @@
/*
Copyright 2018 Equinor ASA.
This file is part of the Open Porous Media project (OPM).
OPM 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.
OPM 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 OPM. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef ActionAST_HPP
#define ActionAST_HPP
#include <string>
#include <vector>
namespace Opm {
class ActionContext;
enum TokenType {
number, // 0
ecl_expr, // 1
open_paren, // 2
close_paren, // 3
op_gt, // 4
op_ge, // 5
op_lt, // 6
op_le, // 7
op_eq, // 8
op_ne, // 9
op_and, // 10
op_or, // 11
end, // 12
error // 13
};
struct ParseNode {
ParseNode(TokenType type, const std::string& value) :
type(type),
value(value)
{}
ParseNode(TokenType type) : ParseNode(type, "")
{}
TokenType type;
std::string value;
};
class ASTNode {
public:
ASTNode() :
type(TokenType::error)
{}
ASTNode(TokenType type):
type(type)
{}
ASTNode(double value) :
type(TokenType::number),
number(value)
{}
ASTNode(TokenType type, const std::string& func, const std::vector<std::string>& arg_list):
type(type),
func(func),
arg_list(arg_list)
{}
bool eval(const ActionContext& context) const;
double value(const ActionContext& context) const;
TokenType type;
void add_child(const ASTNode& child);
size_t size() const;
private:
std::string func;
std::vector<std::string> arg_list;
double number;
/*
To have a memmber std::vector<ASTNode> inside the ASTNode class is
supposedly borderline undefined behaviour; it compiles without warnings
and works. Good for enough for me.
*/
std::vector<ASTNode> children;
};
class ActionParser {
public:
ActionParser(const std::vector<std::string>& tokens);
TokenType get_type(const std::string& arg) const;
ParseNode current() const;
ParseNode next();
size_t pos() const;
void print() const;
private:
const std::vector<std::string>& tokens;
ssize_t current_pos = -1;
};
class ActionAST{
public:
ActionAST() = default;
explicit ActionAST(const std::vector<std::string>& tokens);
ASTNode parse_right(ActionParser& parser);
ASTNode parse_left(ActionParser& parser);
ASTNode parse_op(ActionParser& parser);
ASTNode parse_cmp(ActionParser& parser);
ASTNode parse_or(ActionParser& parser);
ASTNode parse_and(ActionParser& parser);
bool eval(const ActionContext& context) const;
private:
ASTNode tree;
};
}
#endif

View File

@ -33,9 +33,14 @@ namespace Opm {
class ActionContext {
public:
ActionContext();
double get(const std::string& func, const std::string& arg) const;
void add(const std::string& func, const std::string& arg, double value);
double get(const std::string& func) const;
void add(const std::string& func, double value);
private:
std::map<std::string, double> values;
};

View File

@ -26,6 +26,7 @@
#include <ctime>
#include <opm/parser/eclipse/Deck/DeckKeyword.hpp>
#include <opm/parser/eclipse/EclipseState/Schedule/ActionAST.hpp>
namespace Opm {
/*
@ -78,6 +79,7 @@ private:
std::time_t m_start_time;
std::vector<DeckKeyword> keywords;
ActionAST ast;
size_t run_count = 0;
std::time_t last_run = 0;
};

View File

@ -0,0 +1,337 @@
/*
Copyright 2018 Equinor ASA.
This file is part of the Open Porous Media project (OPM).
OPM 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.
OPM 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 OPM. If not, see <http://www.gnu.org/licenses/>.
*/
#include <vector>
#include <string>
#include <algorithm>
#include <cstring>
#include <opm/parser/eclipse/EclipseState/Schedule/ActionAST.hpp>
#include <opm/parser/eclipse/EclipseState/Schedule/ActionContext.hpp>
namespace Opm {
ActionParser::ActionParser(const std::vector<std::string>& tokens) :
tokens(tokens)
{}
TokenType ActionParser::get_type(const std::string& arg) const {
std::string lower_arg = arg;
std::for_each(lower_arg.begin(),
lower_arg.end(),
[](char& c) {
c = std::tolower(static_cast<unsigned char>(c));
});
if (lower_arg == "and")
return TokenType::op_and;
if (lower_arg == "or")
return TokenType::op_or;
if (lower_arg == "(")
return TokenType::open_paren;
if (lower_arg == ")")
return TokenType::close_paren;
if (lower_arg == ">" || lower_arg == ".gt.")
return TokenType::op_gt;
if (lower_arg == ">=" || lower_arg == ".ge.")
return TokenType::op_ge;
if (lower_arg == "<=" || lower_arg == ".le.")
return TokenType::op_le;
if (lower_arg == "<" || lower_arg == ".lt.")
return TokenType::op_lt;
if (lower_arg == "<=" || lower_arg == ".le.")
return TokenType::op_le;
if (lower_arg == "=" || lower_arg == ".eq.")
return TokenType::op_eq;
if (lower_arg == "!=" || lower_arg == ".ne.")
return TokenType::op_ne;
{
char * end_ptr;
strtod(lower_arg.c_str(), &end_ptr);
if (std::strlen(end_ptr) == 0)
return TokenType::number;
}
return TokenType::ecl_expr;
}
ParseNode ActionParser::next() {
this->current_pos++;
if (static_cast<size_t>(this->current_pos) == this->tokens.size())
return TokenType::end;
std::string arg = this->tokens[this->current_pos];
return ParseNode(get_type(arg), arg);
}
ParseNode ActionParser::current() const {
if (static_cast<size_t>(this->current_pos) == this->tokens.size())
return TokenType::end;
std::string arg = this->tokens[this->current_pos];
return ParseNode(get_type(arg), arg);
}
size_t ActionParser::pos() const {
return this->current_pos;
}
/*****************************************************************/
void ASTNode::add_child(const ASTNode& child) {
this->children.push_back(child);
}
double ASTNode::value(const ActionContext& context) const {
if (this->children.size() != 0)
throw std::invalid_argument("value() method should only reach leafnodes");
if (this->type == TokenType::number)
return this->number;
if (this->arg_list.size() == 0)
return context.get(this->func);
else {
std::string arg_key = this->arg_list[0];
for (size_t index = 1; index < this->arg_list.size(); index++)
arg_key += ":" + this->arg_list[index];
return context.get(this->func, arg_key);
}
}
bool ASTNode::eval(const ActionContext& context) const {
if (this->children.size() == 0)
throw std::invalid_argument("bool eval should not reach leafnodes");
if (this->type == TokenType::op_or || this->type == TokenType::op_and) {
bool value = (this->type == TokenType::op_and);
for (const auto& child : this->children) {
if (this->type == TokenType::op_or)
value = value || child.eval(context);
else
value = value && child.eval(context);
}
return value;
}
double v1 = this->children[0].value(context);
double v2 = this->children[1].value(context);
switch (this->type) {
case TokenType::op_eq:
return v1 == v2;
case TokenType::op_ge:
return v1 >= v2;
case TokenType::op_le:
return v1 <= v2;
case TokenType::op_ne:
return v1 != v2;
case TokenType::op_gt:
return v1 > v2;
case TokenType::op_lt:
return v1 < v2;
default:
throw std::invalid_argument("Incorrect operator type - expected comparison");
}
}
size_t ASTNode::size() const {
return this->children.size();
}
/*****************************************************************/
ASTNode ActionAST::parse_left(ActionParser& parser) {
auto current = parser.current();
if (current.type != TokenType::ecl_expr)
return TokenType::error;
std::string func = current.value;
std::vector<std::string> arg_list;
current = parser.next();
while (current.type == TokenType::ecl_expr || current.type == TokenType::number) {
arg_list.push_back(current.value);
current = parser.next();
}
return ASTNode(TokenType::ecl_expr, func, arg_list);
}
ASTNode ActionAST::parse_op(ActionParser& parser) {
auto current = parser.current();
if (current.type == TokenType::op_gt ||
current.type == TokenType::op_ge ||
current.type == TokenType::op_lt ||
current.type == TokenType::op_le ||
current.type == TokenType::op_eq ||
current.type == TokenType::op_ne) {
parser.next();
return current.type;
}
return TokenType::error;
}
ASTNode ActionAST::parse_right(ActionParser& parser) {
auto current = parser.current();
if (current.type == TokenType::number) {
parser.next();
return ASTNode( strtod(current.value.c_str(), nullptr) );
}
current = parser.current();
if (current.type != TokenType::ecl_expr)
return TokenType::error;
std::string func = current.value;
std::vector<std::string> arg_list;
current = parser.next();
while (current.type == TokenType::ecl_expr || current.type == TokenType::number) {
arg_list.push_back(current.value);
current = parser.next();
}
return ASTNode(TokenType::ecl_expr, func, arg_list);
}
ASTNode ActionAST::parse_cmp(ActionParser& parser) {
auto current = parser.current();
if (current.type == TokenType::open_paren) {
parser.next();
auto inner_expr = this->parse_or(parser);
current = parser.current();
if (current.type != TokenType::close_paren)
return TokenType::error;
parser.next();
return inner_expr;
} else {
auto left_node = parse_left(parser);
if (left_node.type == TokenType::error)
return TokenType::error;
auto op_node = parse_op(parser);
if (op_node.type == TokenType::error)
return TokenType::error;
auto right_node = parse_right(parser);
if (right_node.type == TokenType::error)
return TokenType::error;
op_node.add_child(left_node);
op_node.add_child(right_node);
return op_node;
}
}
ASTNode ActionAST::parse_and(ActionParser& parser) {
auto left = this->parse_cmp(parser);
if (left.type == TokenType::error)
return TokenType::error;
auto current = parser.current();
if (current.type == TokenType::op_and) {
ASTNode and_node(TokenType::op_and);
and_node.add_child(left);
while (parser.current().type == TokenType::op_and) {
parser.next();
auto next_cmp = this->parse_cmp(parser);
if (next_cmp.type == TokenType::error)
return TokenType::error;
and_node.add_child(next_cmp);
}
return and_node;
}
return left;
}
ASTNode ActionAST::parse_or(ActionParser& parser) {
auto left = this->parse_and(parser);
if (left.type == TokenType::error)
return TokenType::error;
auto current = parser.current();
if (current.type == TokenType::op_or) {
ASTNode or_node(TokenType::op_or);
or_node.add_child(left);
while (parser.current().type == TokenType::op_or) {
parser.next();
auto next_cmp = this->parse_or(parser);
if (next_cmp.type == TokenType::error)
return TokenType::error;
or_node.add_child(next_cmp);
}
return or_node;
}
return left;
}
ActionAST::ActionAST(const std::vector<std::string>& tokens) {
ActionParser parser(tokens);
auto current = parser.next();
this->tree = this->parse_or(parser);
current = parser.current();
if (current.type != TokenType::end) {
size_t index = parser.pos();
throw std::invalid_argument("Extra unhandled data starting with token[" + std::to_string(index) + "] = " + current.value);
}
if (this->tree.type == TokenType::error)
throw std::invalid_argument("Failed to parse");
}
bool ActionAST::eval(const ActionContext& context) const {
return this->tree.eval(context);
}
}

View File

@ -18,15 +18,28 @@
*/
#include <opm/parser/eclipse/EclipseState/Schedule/ActionContext.hpp>
#include <opm/parser/eclipse/EclipseState/Schedule/TimeMap.hpp>
namespace Opm {
ActionContext::ActionContext() {
for (const auto& pair : TimeMap::eclipseMonthIndices())
this->add(pair.first, pair.second);
}
void ActionContext::add(const std::string& func, const std::string& arg, double value) {
this->values[func + ":" + arg] = value;
}
void ActionContext::add(const std::string& func, double value) {
this->values[func] = value;
}
double ActionContext::get(const std::string& func, const std::string& arg) const {
return this->values.at( func + ":" + arg );
}
double ActionContext::get(const std::string& func) const {
return this->values.at( func );
}
}

View File

@ -45,7 +45,15 @@ ActionX::ActionX(const DeckRecord& record, std::time_t start_time) :
ActionX::ActionX(const DeckKeyword& kw, std::time_t start_time) :
ActionX(kw.getRecord(0), start_time)
{}
{
std::vector<std::string> tokens;
for (size_t record_index = 1; record_index < kw.size(); record_index++) {
const auto& record = kw.getRecord(record_index);
for (const auto& token : record.getItem("CONDITION").getData<std::string>())
tokens.push_back(token);
}
this->ast = ActionAST(tokens);
}
void ActionX::addKeyword(const DeckKeyword& kw) {

View File

@ -30,6 +30,7 @@
#include <opm/parser/eclipse/EclipseState/Grid/EclipseGrid.hpp>
#include <opm/parser/eclipse/EclipseState/Schedule/Schedule.hpp>
#include <opm/parser/eclipse/EclipseState/Schedule/ActionAST.hpp>
#include <opm/parser/eclipse/EclipseState/Schedule/ActionContext.hpp>
#include <opm/parser/eclipse/EclipseState/Schedule/Actions.hpp>
#include <opm/parser/eclipse/EclipseState/Schedule/ActionX.hpp>
@ -198,3 +199,251 @@ BOOST_AUTO_TEST_CASE(TestContext) {
context.add("FUNC", "ARG", 100);
BOOST_CHECK_EQUAL(context.get("FUNC", "ARG"), 100);
}
Opm::Schedule make_action(const std::string& action_string) {
std::string start = std::string{ R"(
SCHEDULE
)"};
std::string end = std::string{ R"(
ENDACTIO
TSTEP
10 /
)"};
std::string deck_string = start + action_string + end;
Opm::Parser parser;
auto deck = parser.parseString(deck_string, Opm::ParseContext());
EclipseGrid grid1(10,10,10);
TableManager table ( deck );
Eclipse3DProperties eclipseProperties ( deck , table, grid1);
Runspec runspec(deck);
return Schedule(deck, grid1, eclipseProperties, runspec, ParseContext());
}
BOOST_AUTO_TEST_CASE(TestActionAST_BASIC) {
// Missing comparator
BOOST_REQUIRE_THROW( ActionAST( {"WWCT", "OPX", "0.75"} ), std::invalid_argument);
// Left hand side must be function expression
BOOST_REQUIRE_THROW( ActionAST({"0.75", "<", "1.0"}), std::invalid_argument);
//Extra data
BOOST_REQUIRE_THROW(ActionAST({"0.75", "<", "1.0", "EXTRA"}), std::invalid_argument);
ActionAST ast1({"WWCT", "OPX", ">", "0.75"});
ActionAST ast2({"WWCT", "OPX", "=", "WWCT", "OPX"});
ActionAST ast3({"WWCT", "OPY", ">", "0.75"});
ActionContext context;
context.add("WWCT", "OPX", 100);
BOOST_CHECK(ast1.eval(context));
context.add("WWCT", "OPX", -100);
BOOST_CHECK(!ast1.eval(context));
BOOST_CHECK(ast2.eval(context));
BOOST_REQUIRE_THROW(ast3.eval(context), std::out_of_range);
}
BOOST_AUTO_TEST_CASE(TestActionAST_OR_AND) {
ActionAST ast_or({"WWCT", "OPX", ">", "0.75", "OR", "WWCT", "OPY", ">", "0.75"});
ActionAST ast_and({"WWCT", "OPX", ">", "0.75", "AND", "WWCT", "OPY", ">", "0.75"});
ActionAST par({"WWCT", "OPX", ">", "0.75", "AND", "(", "WWCT", "OPY", ">", "0.75", "OR", "WWCT", "OPZ", ">", "0.75", ")"});
ActionContext context;
context.add("WWCT", "OPX", 100);
context.add("WWCT", "OPY", -100);
context.add("WWCT", "OPZ", 100);
BOOST_CHECK( ast_or.eval(context) );
BOOST_CHECK( !ast_and.eval(context) );
BOOST_CHECK( par.eval(context));
context.add("WWCT", "OPX", -100);
context.add("WWCT", "OPY", 100);
context.add("WWCT", "OPZ", 100);
BOOST_CHECK( ast_or.eval(context) );
BOOST_CHECK( !ast_and.eval(context) );
BOOST_CHECK( !par.eval(context));
context.add("WWCT", "OPX", 100);
context.add("WWCT", "OPY", 100);
context.add("WWCT", "OPZ", -100);
BOOST_CHECK( ast_or.eval(context) );
BOOST_CHECK( ast_and.eval(context) );
BOOST_CHECK( par.eval(context));
context.add("WWCT", "OPX", -100);
context.add("WWCT", "OPY", -100);
context.add("WWCT", "OPZ", -100);
BOOST_CHECK( !ast_or.eval(context) );
BOOST_CHECK( !ast_and.eval(context) );
BOOST_CHECK( !par.eval(context));
}
BOOST_AUTO_TEST_CASE(DATE) {
ActionAST ast({"MNTH", ">=", "JUN"});
ActionContext context;
context.add("MNTH", 6);
BOOST_CHECK( ast.eval(context) );
context.add("MNTH", 8);
BOOST_CHECK( ast.eval(context) );
context.add("MNTH", 5);
BOOST_CHECK( !ast.eval(context) );
}
BOOST_AUTO_TEST_CASE(MANUAL1) {
ActionAST ast({"GGPR", "FIELD", ">", "50000", "AND", "WGOR", "PR", ">" ,"GGOR", "FIELD"});
ActionContext context;
context.add("GGPR", "FIELD", 60000 );
context.add("WGOR", "PR" , 300 );
context.add("GGOR", "FIELD", 200);
BOOST_CHECK( ast.eval(context) );
context.add("GGPR", "FIELD", 0 );
context.add("WGOR", "PR" , 300 );
context.add("GGOR", "FIELD", 200);
BOOST_CHECK( !ast.eval(context) );
context.add("GGPR", "FIELD", 60000 );
context.add("WGOR", "PR" , 100 );
context.add("GGOR", "FIELD", 200);
BOOST_CHECK( !ast.eval(context) );
}
BOOST_AUTO_TEST_CASE(MANUAL2) {
ActionAST ast({"GWCT", "LIST1", ">", "0.70", "AND", "(", "GWPR", "LIST1", ">", "GWPR", "LIST2", "OR", "GWPR", "LIST1", ">", "GWPR", "LIST3", ")"});
ActionContext context;
context.add("GWCT", "LIST1", 1.0);
context.add("GWPR", "LIST1", 1 );
context.add("GWPR", "LIST2", 2 );
context.add("GWPR", "LIST3", 3 );
BOOST_CHECK( !ast.eval(context));
context.add("GWCT", "LIST1", 1.0);
context.add("GWPR", "LIST1", 1 );
context.add("GWPR", "LIST2", 2 );
context.add("GWPR", "LIST3", 0 );
BOOST_CHECK( ast.eval(context));
context.add("GWCT", "LIST1", 1.0);
context.add("GWPR", "LIST1", 1 );
context.add("GWPR", "LIST2", 0 );
context.add("GWPR", "LIST3", 3 );
BOOST_CHECK( ast.eval(context));
context.add("GWCT", "LIST1", 1.0);
context.add("GWPR", "LIST1", 1 );
context.add("GWPR", "LIST2", 0 );
context.add("GWPR", "LIST3", 0 );
BOOST_CHECK( ast.eval(context));
context.add("GWCT", "LIST1", 0.0);
context.add("GWPR", "LIST1", 1 );
context.add("GWPR", "LIST2", 0 );
context.add("GWPR", "LIST3", 3 );
BOOST_CHECK( !ast.eval(context));
}
BOOST_AUTO_TEST_CASE(MANUAL3) {
ActionAST ast({"MNTH", ".GE.", "MAR", "AND", "MNTH", ".LE.", "OCT", "AND", "GMWL", "HIGH", ".GE.", "4"});
ActionContext context;
context.add("MNTH", 4);
context.add("GMWL", "HIGH", 4);
BOOST_CHECK( ast.eval(context));
context.add("MNTH", 3);
context.add("GMWL", "HIGH", 4);
BOOST_CHECK( ast.eval(context));
context.add("MNTH", 11);
context.add("GMWL", "HIGH", 4);
BOOST_CHECK( !ast.eval(context));
context.add("MNTH", 3);
context.add("GMWL", "HIGH", 3);
BOOST_CHECK( !ast.eval(context));
}
BOOST_AUTO_TEST_CASE(MANUAL4) {
ActionAST ast({"GWCT", "FIELD", ">", "0.8", "AND", "DAY", ">", "1", "AND", "MNTH", ">", "JUN", "AND", "YEAR", ">=", "2021"});
ActionContext context;
context.add("MNTH", 7);
context.add("DAY", 2);
context.add("YEAR", 2030);
context.add("GWCT", "FIELD", 1.0);
BOOST_CHECK( ast.eval(context) );
context.add("MNTH", 7);
context.add("DAY", 2);
context.add("YEAR", 2019);
context.add("GWCT", "FIELD", 1.0);
BOOST_CHECK( !ast.eval(context) );
}
BOOST_AUTO_TEST_CASE(MANUAL5) {
ActionAST ast({"WCG2", "PROD1", ">", "WCG5", "PROD2", "AND", "GCG3", "G1", ">", "GCG7", "G2", "OR", "FCG1", ">", "FCG7"});
ActionContext context;
context.add("WCG2", "PROD1", 100);
context.add("WCG5", "PROD2", 50);
context.add("GCG3", "G1", 200);
context.add("GCG7", "G2", 100);
context.add("FCG1", 100);
context.add("FCG7", 50);
BOOST_CHECK(ast.eval(context));
context.add("WCG2", "PROD1", 100);
context.add("WCG5", "PROD2", 50);
context.add("GCG3", "G1", 200);
context.add("GCG7", "G2", 100);
context.add("FCG1", 100);
context.add("FCG7", 150);
BOOST_CHECK(ast.eval(context));
context.add("WCG2", "PROD1", 100);
context.add("WCG5", "PROD2", 50);
context.add("GCG3", "G1", 20);
context.add("GCG7", "G2", 100);
context.add("FCG1", 100);
context.add("FCG7", 150);
BOOST_CHECK(!ast.eval(context));
context.add("WCG2", "PROD1", 100);
context.add("WCG5", "PROD2", 50);
context.add("GCG3", "G1", 20);
context.add("GCG7", "G2", 100);
context.add("FCG1", 200);
context.add("FCG7", 150);
BOOST_CHECK(ast.eval(context));
}
BOOST_AUTO_TEST_CASE(LGR) {
ActionAST ast({"LWCC" , "OPX", "LOCAL", "1", "2", "3", ">", "100"});
ActionContext context;
context.add("LWCC", "OPX:LOCAL:1:2:3", 200);
BOOST_CHECK(ast.eval(context));
context.add("LWCC", "OPX:LOCAL:1:2:3", 20);
BOOST_CHECK(!ast.eval(context));
}