Unverified Commit 03dfce58 authored by Joseph Noir's avatar Joseph Noir Committed by GitHub

Merge pull request #999

Enable compact list notation in config files
parents f983dd0c ef580c37
......@@ -38,7 +38,11 @@ constexpr bool in_whitelist(char whitelist, char ch) {
}
inline bool in_whitelist(const char* whitelist, char ch) {
return strchr(whitelist, ch) != nullptr;
// Note: using strchr breaks if `ch == '\0'`.
for (char c = *whitelist++; c != '\0'; c = *whitelist++)
if (c == ch)
return true;
return false;
}
inline bool in_whitelist(bool (*filter)(char), char ch) {
......
......@@ -273,18 +273,30 @@
#endif // CAF_MSVC
/// Makes a transition into another state if the `statement` is true.
/// Enables a transition into another state if the `statement` is true.
#define transition_if(statement, ...) \
if (statement) { \
transition(__VA_ARGS__) \
}
/// Makes an epsiolon transition into another state if the `statement` is true.
/// Enables a transition if the constexpr `statement` is true.
#define transition_static_if(statement, ...) \
if constexpr (statement) { \
transition(__VA_ARGS__) \
}
/// Enables an epsiolon transition if the `statement` is true.
#define epsilon_if(statement, ...) \
if (statement) { \
epsilon(__VA_ARGS__) \
}
/// Enables an epsiolon transition if the constexpr `statement` is true.
#define epsilon_static_if(statement, ...) \
if constexpr (statement) { \
epsilon(__VA_ARGS__) \
}
/// Makes an transition transition into another FSM if `statement` is true,
/// resuming at state `target`.
#define fsm_transition_if(statement, ...) \
......@@ -292,9 +304,23 @@
fsm_transition(__VA_ARGS__) \
}
/// Makes an transition transition into another FSM if `statement` is true,
/// resuming at state `target`.
#define fsm_transition_static_if(statement, ...) \
if constexpr (statement) { \
fsm_transition(__VA_ARGS__) \
}
/// Makes an epsilon transition into another FSM if `statement` is true,
/// resuming at state `target`.
#define fsm_epsilon_if(statement, ...) \
if (statement) { \
fsm_epsilon(__VA_ARGS__) \
}
/// Makes an epsilon transition into another FSM if `statement` is true,
/// resuming at state `target`.
#define fsm_epsilon_static_if(statement, ...) \
if constexpr (statement) { \
fsm_epsilon(__VA_ARGS__) \
}
......@@ -112,7 +112,8 @@ void read_floating_point(State& ps, Consumer&& consumer,
start();
unstable_state(init) {
epsilon_if(start_value == none, regular_init)
epsilon(after_dec)
epsilon(after_dec, "eE.")
epsilon(after_dot, any_char)
}
state(regular_init) {
transition(regular_init, " \t")
......
......@@ -71,8 +71,9 @@ void read_ini_comment(State& ps, Consumer&&) {
// clang-format on
}
template <class State, class Consumer>
void read_ini_value(State& ps, Consumer&& consumer);
template <class State, class Consumer, class InsideList = std::false_type>
void read_ini_value(State& ps, Consumer&& consumer,
InsideList inside_list = {});
template <class State, class Consumer>
void read_ini_list(State& ps, Consumer&& consumer) {
......@@ -85,7 +86,7 @@ void read_ini_list(State& ps, Consumer&& consumer) {
transition(before_value, " \t\n")
transition(done, ']', consumer.end_list())
fsm_epsilon(read_ini_comment(ps, consumer), before_value, ';')
fsm_epsilon(read_ini_value(ps, consumer), after_value)
fsm_epsilon(read_ini_value(ps, consumer, std::true_type{}), after_value)
}
state(after_value) {
transition(after_value, " \t\n")
......@@ -183,8 +184,8 @@ void read_ini_uri(State& ps, Consumer&& consumer) {
// clang-format on
}
template <class State, class Consumer>
void read_ini_value(State& ps, Consumer&& consumer) {
template <class State, class Consumer, class InsideList>
void read_ini_value(State& ps, Consumer&& consumer, InsideList inside_list) {
// clang-format off
start();
state(init) {
......@@ -192,7 +193,8 @@ void read_ini_value(State& ps, Consumer&& consumer) {
fsm_epsilon(read_atom(ps, consumer), done, '\'')
fsm_epsilon(read_number(ps, consumer), done, '.')
fsm_epsilon(read_bool(ps, consumer), done, "ft")
fsm_epsilon(read_number_or_timespan(ps, consumer), done, "0123456789+-")
fsm_epsilon(read_number_or_timespan(ps, consumer, inside_list),
done, "0123456789+-")
fsm_epsilon(read_ini_uri(ps, consumer), done, '<')
fsm_transition(read_ini_list(ps, consumer.begin_list()), done, '[')
fsm_transition(read_ini_map(ps, consumer.begin_map()), done, '{')
......
......@@ -19,8 +19,10 @@
#pragma once
#include <cstdint>
#include <type_traits>
#include "caf/config.hpp"
#include "caf/detail/consumer.hpp"
#include "caf/detail/parser/add_ascii.hpp"
#include "caf/detail/parser/chars.hpp"
#include "caf/detail/parser/is_char.hpp"
......@@ -36,10 +38,25 @@ CAF_PUSH_UNUSED_LABEL_WARNING
namespace caf::detail::parser {
/// Reads the second half of 'n..m' range statement.
///
/// Expect the current position to point at the number *after* the dots:
///
/// ~~~
/// foo = [1..2]
/// ~~~^
/// ~~~
template <class State, class Consumer>
void read_number_range(State& ps, Consumer& consumer, int64_t begin);
/// Reads a number, i.e., on success produces either an `int64_t` or a
/// `double`.
template <class State, class Consumer>
void read_number(State& ps, Consumer& consumer) {
template <class State, class Consumer, class EnableFloat = std::true_type,
class EnableRange = std::false_type>
void read_number(State& ps, Consumer& consumer, EnableFloat = {},
EnableRange = {}) {
static constexpr bool enable_float = EnableFloat::value;
static constexpr bool enable_range = EnableRange::value;
// Our result when reading an integer number.
int64_t result = 0;
// Computes the result on success.
......@@ -47,6 +64,7 @@ void read_number(State& ps, Consumer& consumer) {
if (ps.code <= pec::trailing_character)
consumer.value(result);
});
using odbl = optional<double>;
// clang-format off
// Definition of our parser FSM.
start();
......@@ -54,18 +72,23 @@ void read_number(State& ps, Consumer& consumer) {
transition(init, " \t")
transition(has_plus, '+')
transition(has_minus, '-')
fsm_epsilon_static_if(enable_float,
read_floating_point(ps, consumer, odbl{0.}),
done, '.', g.disable())
epsilon(has_plus)
}
// "+" or "-" alone aren't numbers.
state(has_plus) {
fsm_epsilon(read_floating_point(ps, consumer, optional<double>{0.}),
done, '.', g.disable())
fsm_epsilon_static_if(enable_float,
read_floating_point(ps, consumer, odbl{0.}),
done, '.', g.disable())
transition(pos_zero, '0')
epsilon(pos_dec)
}
state(has_minus) {
fsm_epsilon(read_floating_point(ps, consumer, optional<double>{0.}, true),
done, '.', g.disable())
fsm_epsilon_static_if(enable_float,
read_floating_point(ps, consumer, odbl{0.}, true),
done, '.', g.disable())
transition(neg_zero, '0')
epsilon(neg_dec)
}
......@@ -73,15 +96,13 @@ void read_number(State& ps, Consumer& consumer) {
term_state(pos_zero) {
transition(start_pos_bin, "bB")
transition(start_pos_hex, "xX")
fsm_epsilon(read_floating_point(ps, consumer, optional<double>{0.}),
done, '.', g.disable())
transition_static_if(enable_float || enable_range, pos_dot, '.')
epsilon(pos_oct)
}
term_state(neg_zero) {
transition(start_neg_bin, "bB")
transition(start_neg_hex, "xX")
fsm_epsilon(read_floating_point(ps, consumer, optional<double>{0.}, true),
done, '.', g.disable())
transition_static_if(enable_float || enable_range, neg_dot, '.')
epsilon(neg_oct)
}
// Binary integers.
......@@ -131,19 +152,101 @@ void read_number(State& ps, Consumer& consumer) {
term_state(pos_dec) {
transition(pos_dec, decimal_chars, add_ascii<10>(result, ch),
pec::integer_overflow)
fsm_epsilon(read_floating_point(ps, consumer, optional<double>{result}),
done, "eE", g.disable())
fsm_epsilon(read_floating_point(ps, consumer, optional<double>{result}),
done, '.', g.disable())
fsm_epsilon_static_if(enable_float,
read_floating_point(ps, consumer, odbl{result}),
done, "eE", g.disable())
transition_static_if(enable_float || enable_range, pos_dot, '.')
}
// Reads the integer part of the mantissa or a negative decimal integer.
term_state(neg_dec) {
transition(neg_dec, decimal_chars, sub_ascii<10>(result, ch),
pec::integer_underflow)
fsm_epsilon(read_floating_point(ps, consumer, optional<double>{result}, true),
done, "eE", g.disable())
fsm_epsilon(read_floating_point(ps, consumer, optional<double>{result}, true),
done, '.', g.disable())
fsm_epsilon_static_if(enable_float,
read_floating_point(ps, consumer, odbl{result}, true),
done, "eE", g.disable())
transition_static_if(enable_float || enable_range, neg_dot, '.')
}
unstable_state(pos_dot) {
fsm_transition_static_if(enable_range,
read_number_range(ps, consumer, result),
done, '.', g.disable())
fsm_epsilon_static_if(enable_float,
read_floating_point(ps, consumer, odbl{result}),
done, any_char, g.disable())
epsilon(done)
}
unstable_state(neg_dot) {
fsm_transition_static_if(enable_range,
read_number_range(ps, consumer, result),
done, '.', g.disable())
fsm_epsilon_static_if(enable_float,
read_floating_point(ps, consumer, odbl{result}, true),
done, any_char, g.disable())
epsilon(done)
}
term_state(done) {
// nop
}
fin();
// clang-format on
}
template <class State, class Consumer>
void read_number_range(State& ps, Consumer& consumer, int64_t begin) {
optional<int64_t> end;
optional<int64_t> step;
auto end_consumer = make_consumer(end);
auto step_consumer = make_consumer(step);
auto generate_2 = [&](int64_t n, int64_t m) {
if (n <= m)
while (n <= m)
consumer.value(n++);
else
while (n >= m)
consumer.value(n--);
};
auto generate_3 = [&](int64_t n, int64_t m, int64_t s) {
if (n == m) {
consumer.value(n);
return;
}
if (s == 0 || (n > m && s > 0) || (n < m && s < 0)) {
ps.code = pec::invalid_range_expression;
return;
}
if (n <= m)
for (auto i = n; i <= m; i += s)
consumer.value(i);
else
for (auto i = n; i >= m; i += s)
consumer.value(i);
};
auto g = caf::detail::make_scope_guard([&] {
if (ps.code <= pec::trailing_character) {
if (!end) {
ps.code = pec::invalid_range_expression;
} else if (!step) {
generate_2(begin, *end);
} else {
generate_3(begin, *end, *step);
}
}
});
static constexpr std::false_type no_float = std::false_type{};
// clang-format off
// Definition of our parser FSM.
start();
state(init) {
fsm_epsilon(read_number(ps, end_consumer, no_float), after_end_num)
}
term_state(after_end_num) {
transition(first_dot, '.')
}
state(first_dot) {
transition(second_dot, '.')
}
state(second_dot) {
fsm_epsilon(read_number(ps, step_consumer, no_float), done)
}
term_state(done) {
// nop
......
......@@ -42,19 +42,34 @@ namespace caf::detail::parser {
/// Reads a number or a duration, i.e., on success produces an `int64_t`, a
/// `double`, or a `timespan`.
template <class State, class Consumer>
void read_number_or_timespan(State& ps, Consumer& consumer) {
template <class State, class Consumer, class EnableRange = std::false_type>
void read_number_or_timespan(State& ps, Consumer& consumer,
EnableRange enable_range = {}) {
using namespace std::chrono;
struct interim_consumer {
size_t invocations = 0;
Consumer* outer = nullptr;
variant<none_t, int64_t, double> interim;
void value(int64_t x) {
interim = x;
switch (++invocations) {
case 1:
interim = x;
break;
case 2:
CAF_ASSERT(holds_alternative<int64_t>(interim));
outer->value(get<int64_t>(interim));
interim = none;
[[fallthrough]];
default:
outer->value(x);
}
}
void value(double x) {
interim = x;
}
};
interim_consumer ic;
ic.outer = &consumer;
auto has_int = [&] { return holds_alternative<int64_t>(ic.interim); };
auto has_dbl = [&] { return holds_alternative<double>(ic.interim); };
auto get_int = [&] { return get<int64_t>(ic.interim); };
......@@ -66,10 +81,11 @@ void read_number_or_timespan(State& ps, Consumer& consumer) {
consumer.value(get_int());
}
});
static constexpr std::true_type enable_float = std::true_type{};
// clang-format off
start();
state(init) {
fsm_epsilon(read_number(ps, ic), has_number)
fsm_epsilon(read_number(ps, ic, enable_float, enable_range), has_number)
}
term_state(has_number) {
epsilon_if(has_int(), has_integer)
......
......@@ -72,7 +72,9 @@ enum class pec : uint8_t {
repeated_field_name,
/// Stopped while reading a user-defined type with one or more missing
/// mandatory fields.
missing_field,
missing_field = 20,
/// Parsing a range statement ('n..m' or 'n..m..step') failed.
invalid_range_expression,
};
/// @relates pec
......
......@@ -126,8 +126,9 @@ some-other-bool=false
some-list=[
; here we have some list entries
123,
1..3,
23 ; twenty-three!
,
,2..4..2,
"abc",
'def', ; some comment and a trailing comma
]
......@@ -179,7 +180,12 @@ const auto ini0_log = make_log(
"key: some-list",
"[",
"value (integer): 123",
"value (integer): 1",
"value (integer): 2",
"value (integer): 3",
"value (integer): 23",
"value (integer): 2",
"value (integer): 4",
"value (string): \"abc\"",
"value (atom): 'def'",
"]",
......
......@@ -36,16 +36,26 @@ using namespace caf;
namespace {
struct numbers_parser_consumer {
struct number_consumer {
variant<int64_t, double> x;
inline void value(double y) {
void value(double y) {
x = y;
}
inline void value(int64_t y) {
void value(int64_t y) {
x = y;
}
};
struct range_consumer {
std::vector<int64_t> xs;
void value(double) {
CAF_FAIL("range consumer called with a double");
}
void value(int64_t y) {
xs.emplace_back(y);
}
};
struct res_t {
variant<pec, double, int64_t> val;
template <class T>
......@@ -54,6 +64,10 @@ struct res_t {
}
};
std::string to_string(const res_t& x) {
return caf::visit([](auto& y) { return deep_to_string(y); }, x.val);
}
bool operator==(const res_t& x, const res_t& y) {
if (x.val.index() != y.val.index())
return false;
......@@ -70,7 +84,7 @@ bool operator==(const res_t& x, const res_t& y) {
struct numbers_parser {
res_t operator()(string_view str) {
numbers_parser_consumer f;
number_consumer f;
string_parser_state res{str.begin(), str.end()};
detail::parser::read_number(res, f);
if (res.code == pec::success)
......@@ -79,6 +93,17 @@ struct numbers_parser {
}
};
struct range_parser {
expected<std::vector<int64_t>> operator()(string_view str) {
range_consumer f;
string_parser_state res{str.begin(), str.end()};
detail::parser::read_number(res, f, std::true_type{}, std::true_type{});
if (res.code == pec::success)
return std::move(f.xs);
return make_error(res);
}
};
template <class T>
typename std::enable_if<std::is_integral<T>::value, res_t>::type res(T x) {
return {static_cast<int64_t>(x)};
......@@ -92,6 +117,7 @@ res(T x) {
struct fixture {
numbers_parser p;
range_parser r;
};
} // namespace
......@@ -205,12 +231,18 @@ CAF_TEST(floating point numbers) {
CHECK_NUMBER(0.0);
CHECK_NUMBER(.0);
CHECK_NUMBER(0.);
CHECK_NUMBER(1.1);
CHECK_NUMBER(.1);
CHECK_NUMBER(1.);
CHECK_NUMBER(0.123);
CHECK_NUMBER(.123);
CHECK_NUMBER(123.456);
CHECK_NUMBER(-0.0);
CHECK_NUMBER(-.0);
CHECK_NUMBER(-0.);
CHECK_NUMBER(-1.1);
CHECK_NUMBER(-.1);
CHECK_NUMBER(-1.);
CHECK_NUMBER(-0.123);
CHECK_NUMBER(-.123);
CHECK_NUMBER(-123.456);
......@@ -269,4 +301,58 @@ CAF_TEST(fractional mantissa with negative exponent) {
CHECK_NUMBER(-42.0001e-5);
}
#define CHECK_RANGE(expr, ...) \
CAF_CHECK_EQUAL(r(expr), std::vector<int64_t>({__VA_ARGS__}))
CAF_TEST(a range from n to n is just n) {
CHECK_RANGE("0..0", 0);
CHECK_RANGE("1..1", 1);
CHECK_RANGE("2..2", 2);
CHECK_RANGE("101..101", 101);
CHECK_RANGE("101..101..1", 101);
CHECK_RANGE("101..101..2", 101);
CHECK_RANGE("101..101..-1", 101);
CHECK_RANGE("101..101..-2", 101);
}
CAF_TEST(ranges are either ascending or descending) {
CHECK_RANGE("0..1", 0, 1);
CHECK_RANGE("0..2", 0, 1, 2);
CHECK_RANGE("0..3", 0, 1, 2, 3);
CHECK_RANGE("3..0", 3, 2, 1, 0);
CHECK_RANGE("3..1", 3, 2, 1);
CHECK_RANGE("3..2", 3, 2);
}
CAF_TEST(ranges can use positive step values) {
CHECK_RANGE("2..6..2", 2, 4, 6);
CHECK_RANGE("3..8..3", 3, 6);
}
CAF_TEST(ranges can use negative step values) {
CHECK_RANGE("6..2..-2", 6, 4, 2);
CHECK_RANGE("8..3..-3", 8, 5);
}
CAF_TEST(ranges can use signed integers) {
CHECK_RANGE("+2..+6..+2", 2, 4, 6);
CHECK_RANGE("+6..+2..-2", 6, 4, 2);
CHECK_RANGE("+2..-2..-2", 2, 0, -2);
CHECK_RANGE("-2..+2..+2", -2, 0, 2);
}
#define CHECK_ERR(expr, enum_value) \
if (auto res = r(expr)) { \
CAF_FAIL("expected expression to produce to an error"); \
} else { \
auto& err = res.error(); \
CAF_CHECK_EQUAL(err.category(), atom("parser")); \
CAF_CHECK_EQUAL(err.code(), static_cast<uint8_t>(enum_value)); \
}
CAF_TEST(the parser rejects invalid step values) {
CHECK_ERR("-2..+2..-2", pec::invalid_range_expression);
CHECK_ERR("+2..-2..+2", pec::invalid_range_expression);
}
CAF_TEST_FIXTURE_SCOPE_END()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment