All the symptoms (including the bonus question) are symptoms of imperfect attribute propagation machinery.
Automatic attribute propagation is very nice, but there will continue to be cases where you have to help the system.
Looking at your desired rule and outcomes:
const auto foo
= *x3::alpha >> -(':' >> x3::double_) >> ';' >> even_int
| *x3::alpha >> '|' >> odd_int
;
I conclude that you want the same rule, just without the optional double for even ordinals and using a different delimiter for even vs. odd.
I would try to stay closer to the declarative nature of parser expressions and try to make the verdict more highlevel. E.g.
Live On Coliru
#include <boost/fusion/include/adapt_struct.hpp>
#include <boost/spirit/home/x3.hpp>
#include <iomanip>
#include <optional>
namespace ast {
enum class discriminator { even, odd };
struct foo {
std::string s;
std::optional<double> od;
discriminator ind;
int id;
bool is_valid() const {
bool is_even = 0 == (id % 2);
switch (ind) {
case discriminator::even: return is_even;
case discriminator::odd: return not(is_even or od.has_value());
default: return false;
}
}
};
std::ostream& operator<<(std::ostream& os, const foo& foo)
{
os << std::quoted(foo.s); //
if (foo.od.has_value())
os << "(" << *foo.od << ")";
return os << " " << foo.id //
<< " (" << (foo.is_valid() ? "valid" : "INVALID") << ")";
}
} // namespace ast
BOOST_FUSION_ADAPT_STRUCT(ast::foo, s, od, ind, id)
namespace parser {
namespace x3 = boost::spirit::x3;
static const auto indicator_ = [] {
x3::symbols<ast::discriminator> sym;
sym.add //
(";", ast::discriminator::even) //
("|", ast::discriminator::odd);
return sym;
}();
static const auto foo //
= +x3::alpha >> -(':' >> x3::double_) >> indicator_ >> x3::int_;
}
int main()
{
for (std::string const input : {
"foobar:3.14;4",
"foobar;4",
"foobar|5",
// Invalid cases
"foobar:3.14;5",
"foobar;5",
"foobar|4",
"foobar:3.14|4",
}) //
{
ast::foo result;
if (parse(input.begin(), input.end(), parser::foo, result))
std::cout << std::quoted(input) << " -> " << result << std::endl;
else
std::cout << std::quoted(input) << " Syntax error" << std::endl;
}
}
Prints
"foobar:3.14;4" -> "foobar"(3.14) 4 (valid)
"foobar;4" -> "foobar" 4 (valid)
"foobar|5" -> "foobar" 5 (valid)
"foobar:3.14;5" -> "foobar"(3.14) 5 (INVALID)
"foobar;5" -> "foobar" 5 (INVALID)
"foobar|4" -> "foobar" 4 (INVALID)
"foobar:3.14|4" -> "foobar"(3.14) 4 (INVALID)
Note that you could view this approach as a separation of syntax and semantics.
Alternatives/Improving From Here
Of course you can now write the parse as
return parse(input.begin(), input.end(), parser::foo, result)
&& result.is_valid();
Or if you insist you can encapsulate that check in a semantic action like before:
auto is_valid_ = [](auto& ctx) {
_pass(ctx) = _val(ctx).is_valid();
};
static const auto foo //
= x3::rule<struct foo_, ast::foo, true>{"foo"} //
= (+x3::alpha >> -(':' >> x3::double_) >> indicator_ >>
x3::int_)[is_valid_];
Now the output morphs into:
Live On Coliru
"foobar:3.14;4" -> "foobar"(3.14) 4 (valid)
"foobar;4" -> "foobar" 4 (valid)
"foobar|5" -> "foobar" 5 (valid)
"foobar:3.14;5" Syntax error
"foobar;5" Syntax error
"foobar|4" Syntax error
"foobar:3.14|4" Syntax error
Without Fusion
Now, the above explicitly still used fusion sequence adaptation with automatic attribute propagation. However, since you're deep into semantic actions anyways¹, you can of course do the rest of the work there:
Live On Coliru
#include <boost/spirit/home/x3.hpp>
#include <iomanip>
#include <optional>
namespace ast {
struct foo {
std::string s;
std::optional<double> od;
int id;
};
std::ostream& operator<<(std::ostream& os, const foo& foo)
{
os << std::quoted(foo.s); //
if (foo.od.has_value())
os << "(" << *foo.od << ")";
return os << " " << foo.id;
}
} // namespace ast
namespace parser {
namespace x3 = boost::spirit::x3;
enum class discriminator { even, odd };
static const auto indicator_ = [] {
x3::symbols<discriminator> sym;
sym.add //
(";", discriminator::even) //
("|", discriminator::odd);
return sym;
}();
auto make_foo = [](auto& ctx) {
using boost::fusion::at_c;
auto& attr = _attr(ctx);
auto& s = at_c<0>(attr); // where are
auto& od = at_c<1>(attr); // structured bindings
auto& ind = at_c<2>(attr); // when you
auto& id = at_c<3>(attr); // need them? :|
bool is_even = 0 == (id % 2);
if (ind == discriminator::even)
_pass(ctx) = is_even;
else
_pass(ctx) = not(is_even or od.has_value());
_val(ctx) = ast::foo{
std::move(s),
od.has_value() ? std::make_optional(*od) : std::nullopt, id};
};
static const auto foo = x3::rule<struct foo_, ast::foo> {}
= (+x3::alpha >> -(':' >> x3::double_) >> indicator_ >>
x3::int_)[make_foo];
} // namespace parser
int main()
{
for (std::string const input : {
"foobar:3.14;4",
"foobar;4",
"foobar|5",
// Invalid cases
"foobar:3.14;5",
"foobar;5",
"foobar|4",
"foobar:3.14|4",
}) //
{
ast::foo result;
if (parse(input.begin(), input.end(), parser::foo, result))
std::cout << std::quoted(input) << " -> " << result << std::endl;
else
std::cout << std::quoted(input) << " Syntax error" << std::endl;
}
}
This has pros and cons. The pros would be
- reduced compile time
discriminator
is now private to the parser
Cons:
- you're doing manual propagation (like
boost::optional
->std::optional
which is clumsy)
- semantic actions¹
Hybrid
As you can probably tell, I'm not fond of the hand-writing-attribute-propagation genuflection. If you must hide the ind
field from the ast, perhaps make it so:
Live On Coliru
#include <boost/fusion/adapted/struct.hpp>
#include <boost/spirit/home/x3.hpp>
#include <iomanip>
#include <optional>
namespace ast {
struct foo {
std::string s;
std::optional<double> od;
int id;
};
std::ostream& operator<<(std::ostream& os, const foo& foo)
{
os << std::quoted(foo.s); //
if (foo.od.has_value())
os << "(" << *foo.od << ")";
return os << " " << foo.id;
}
} // namespace ast
namespace parser {
namespace x3 = boost::spirit::x3;
enum class discriminator { even, odd };
struct p_foo : ast::foo {
discriminator ind;
struct semantic_error : std::runtime_error {
using std::runtime_error::runtime_error;
};
void check_semantics() const {
bool is_even = 0 == (id % 2);
switch (ind) {
case discriminator::even:
if (!is_even)
throw semantic_error("id should be even");
break;
case discriminator::odd:
if (is_even)
throw semantic_error("id should be odd");
if (od.has_value())
throw semantic_error("illegal double at odd foo");
break;
}
}
};
}
BOOST_FUSION_ADAPT_STRUCT(parser::p_foo, s, od, ind, id)
namespace parser {
static const auto indicator_ = [] {
x3::symbols<discriminator> sym;
sym.add //
(";", discriminator::even) //
("|", discriminator::odd);
return sym;
}();
static const auto raw_foo //
= x3::rule<p_foo, p_foo>{} //
= +x3::alpha >> -(':' >> x3::double_) >> indicator_ >> x3::int_;
auto checked_ = [](auto& ctx) {
auto& _pf = _attr(ctx);
_pf.check_semantics();
_val(ctx) = std::move(_pf);
};
static const auto foo //
= x3::rule<struct foo_, ast::foo>{} //
= raw_foo[checked_];
} // namespace parser
int main()
{
for (std::string const input : {
"foobar:3.14;4",
"foobar;4",
"foobar|5",
// Invalid cases
"foobar:3.14;5",
"foobar;5",
"foobar|4",
"foobar:3.14|4",
"foobar:3.14|5",
}) //
{
ast::foo result;
try {
if (parse(input.begin(), input.end(), parser::foo, result))
std::cout << std::quoted(input) << " -> " << result << std::endl;
else
std::cout << std::quoted(input) << " Syntax error" << std::endl;
} catch(std::exception const& e) {
std::cout << std::quoted(input) << " Semantic error: " << e.what() << std::endl;
}
}
}
Printing
"foobar:3.14;4" -> "foobar"(3.14) 4
"foobar;4" -> "foobar" 4
"foobar|5" -> "foobar" 5
"foobar:3.14;5" Semantic error: id should be even
"foobar;5" Semantic error: id should be even
"foobar|4" Semantic error: id should be odd
"foobar:3.14|4" Semantic error: id should be odd
"foobar:3.14|5" Semantic error: illegal double at odd foo
Note the richer diagnostic information.
Post Scriptum: Minimal Change
Later, re-reading your question I suddenly realized there was asmaller change that would help your grammar. I introduced my answer with the words:
Automatic attribute propagation is very nice, but there will continue to be cases where you have to help the system
Here you can help it by making both branches have the same structure. So instead of
const auto foo
= *x3::alpha >> -(':' >> x3::double_) >> ';' >> even_int
| *x3::alpha >> '|' >> odd_int
;
You could manually insert an empty optional double in the middle of the odd branch:
const auto foo //
= +x3::alpha >> -(':' >> x3::double_) >> ';' >> even_int //
| +x3::alpha >> x3::attr(ast::optdbl{}) >> '|' >> odd_int;
(where optdbl
is an alias for std::optional<double>
for style).
Now, if you refactor those odd_int
/even_int
rules a bit, I'd say this appraoch has some appear over the other options above:
Live On Coliru
#include <boost/fusion/include/adapt_struct.hpp>
#include <boost/spirit/home/x3.hpp>
#include <iomanip>
#include <optional>
namespace ast{
using optdbl = std::optional<double>;
struct foo {
std::string s;
optdbl od;
int id;
};
std::ostream& operator<<(std::ostream& os, const foo& foo)
{
os << std::quoted(foo.s); //
if (foo.od.has_value())
os << "(" << *foo.od << ")";
return os << " " << foo.id;
}
}
BOOST_FUSION_ADAPT_STRUCT(ast::foo, s, od,id)
namespace parser {
namespace x3 = boost::spirit::x3;
static auto mod2check(int remainder) {
return [=](auto& ctx) { //
_pass(ctx) = _val(ctx) % 2 == remainder;
};
}
static auto mod2int(int remainder) {
return x3::rule<struct _, int, true>{} = x3::int_[mod2check(remainder)];
}
const auto foo //
= +x3::alpha >> //
(-(':' >> x3::double_) | x3::attr(ast::optdbl{})) >> //
(';' >> mod2int(0) | '|' >> mod2int(1)) //
;
} // namespace parser
int main()
{
for (std::string const input : {
"foobar:3.14;4",
"foobar;4",
"foobar|5",
// Invalid cases
"foobar:3.14;5",
"foobar;5",
"foobar|4",
"foobar:3.14|4",
}) //
{
ast::foo result;
if (parse(input.begin(), input.end(), parser::foo, result))
std::cout << std::quoted(input) << " -> " << result << std::endl;
else
std::cout << std::quoted(input) << " Syntax error" << std::endl;
}
}
¹ Boost Spirit: "Semantic actions are evil"?