mu-sexp: improve corner-cases, performance

Some C++20 updates; use the new ascii/ctype functions. Avoid unlimited
recursion. Speed-up conversion to_string

Update unit-tests.
This commit is contained in:
Dirk-Jan C. Binnema
2026-07-21 16:54:52 +03:00
committed by Seth Ladygo
parent dfdd943817
commit da2995a768
2 changed files with 163 additions and 124 deletions

View File

@ -21,47 +21,45 @@
#include "mu-sexp.hh" #include "mu-sexp.hh"
#include "mu-utils.hh" #include "mu-utils.hh"
#include <atomic> #include <charconv>
#include <sstream>
#include <array>
using namespace Mu; using namespace Mu;
/* avoid unbounded recursion (i.e., stack overflow) for pathological input */
constexpr size_t MaxDepth{1024};
template<typename...T> static Mu::Error template<typename...T> static Mu::Error
parsing_error(size_t pos, fmt::format_string<T...> frm, T&&... args) parsing_error(size_t pos, fmt::format_string<T...> frm, T&&... args)
{ {
const auto&& msg{fmt::format(frm, std::forward<T>(args)...)}; const auto&& msg{fmt::format(frm, std::forward<T>(args)...)};
if (pos == 0) return Mu::Error(Error::Code::Parsing, "{}: {}", pos, msg);
return Mu::Error(Error::Code::Parsing, "{}", msg);
else
return Mu::Error(Error::Code::Parsing, "{}: {}", pos, msg);
} }
static size_t static size_t
skip_whitespace(const std::string& s, size_t pos) skip_whitespace(const std::string& s, size_t pos)
{ {
while (pos != s.size()) { while (pos != s.size() && is_ascii_space(s[pos]))
if (s[pos] == ' ' || s[pos] == '\t' || s[pos] == '\n') ++pos;
++pos;
else
break;
}
return pos; return pos;
} }
static Result<Sexp> parse(const std::string& expr, size_t& pos); static Result<Sexp> parse(const std::string& expr, size_t& pos, size_t depth);
static Result<Sexp> static Result<Sexp>
parse_list(const std::string& expr, size_t& pos) parse_list(const std::string& expr, size_t& pos, size_t depth)
{ {
if (expr[pos] != '(') // sanity check. if (expr[pos] != '(') // sanity check.
return Err(parsing_error(pos, "expected: '(' but got '{}", expr[pos])); return Err(parsing_error(pos, "expected: '(' but got '{}'", expr[pos]));
if (depth >= MaxDepth)
return Err(parsing_error(pos, "parentheses nested too deeply"));
Sexp lst{}; Sexp lst{};
++pos; ++pos;
while (pos < expr.size() && expr[pos] != ')') { while (pos < expr.size() && expr[pos] != ')') {
if (auto&& item = parse(expr, pos); item) if (auto&& item = parse(expr, pos, depth + 1); item)
lst.add(std::move(*item)); lst.add(std::move(*item));
else else
return Err(item.error()); return Err(item.error());
@ -80,27 +78,30 @@ static Result<Sexp>
parse_string(const std::string& expr, size_t& pos) parse_string(const std::string& expr, size_t& pos)
{ {
if (expr[pos] != '"') // sanity check. if (expr[pos] != '"') // sanity check.
return Err(parsing_error(pos, "expected: '\"'' but got '{}", expr[pos])); return Err(parsing_error(pos, "expected: '\"' but got '{}'", expr[pos]));
bool escape{}; bool escape{};
std::string str; std::string str;
for (++pos; pos != expr.size(); ++pos) { for (++pos; pos != expr.size(); ++pos) {
auto kar = expr[pos]; auto kar = expr[pos];
if (escape && (kar == '"' || kar == '\\')) { if (escape) {
str += kar; // follow the elisp reader: '\n', '\t' are special,
// any other escaped character stands for itself.
switch (kar) {
case 'n': str += '\n'; break;
case 't': str += '\t'; break;
default: str += kar; break;
}
escape = false; escape = false;
continue; } else if (kar == '\\')
}
if (kar == '"')
break;
else if (kar == '\\')
escape = true; escape = true;
else if (kar == '"')
break;
else else
str += kar; str += kar;
} }
if (escape || expr[pos] != '"') if (escape || pos == expr.size())
return Err(parsing_error(pos, "unterminated string '{}'", str)); return Err(parsing_error(pos, "unterminated string '{}'", str));
++pos; ++pos;
@ -111,54 +112,57 @@ parse_string(const std::string& expr, size_t& pos)
static Result<Sexp> static Result<Sexp>
parse_integer(const std::string& expr, size_t& pos) parse_integer(const std::string& expr, size_t& pos)
{ {
if (!isdigit(expr[pos]) && expr[pos] != '-') // sanity check. if (!is_ascii_digit(expr[pos]) && expr[pos] != '-') // sanity check.
return Err(parsing_error(pos, "expected: <digit> but got '{}", expr[pos])); return Err(parsing_error(pos, "expected: <digit> but got '{}'", expr[pos]));
std::string num; // negative number? auto end{pos + (expr[pos] == '-' ? 1U : 0U)};
if (expr[pos] == '-') { while (end != expr.size() && is_ascii_digit(expr[end]))
num = "-"; ++end;
++pos;
}
for (; isdigit(expr[pos]); ++pos) Sexp::Number num{};
num += expr[pos]; const auto res{std::from_chars(expr.data() + pos, expr.data() + end, num)};
if (res.ec != std::errc{} || res.ptr != expr.data() + end)
return Err(parsing_error(pos, "invalid number '{}'",
expr.substr(pos, end - pos)));
return Ok(Sexp{::atoi(num.c_str())}); pos = end;
return Ok(Sexp{num});
} }
static Result<Sexp> static Result<Sexp>
parse_symbol(const std::string& expr, size_t& pos) parse_symbol(const std::string& expr, size_t& pos)
{ {
if (!isalpha(expr[pos]) && expr[pos] != ':') // sanity check. if (!is_ascii_alpha(expr[pos]) && expr[pos] != ':') // sanity check.
return Err(parsing_error(pos, "expected: <alpha>|: but got '{}", expr[pos])); return Err(parsing_error(pos, "expected: <alpha>|: but got '{}'", expr[pos]));
std::string symb(1, expr[pos]); const auto start{pos};
for (++pos; isalnum(expr[pos]) || expr[pos] == '-'; ++pos) for (++pos; pos != expr.size() &&
symb += expr[pos]; (is_ascii_alnum(expr[pos]) || expr[pos] == '-'); ++pos)
;
return Ok(Sexp{Sexp::Symbol{symb}}); return Ok(Sexp{Sexp::Symbol{expr.substr(start, pos - start)}});
} }
static Result<Sexp> static Result<Sexp>
parse(const std::string& expr, size_t& pos) parse(const std::string& expr, size_t& pos, size_t depth)
{ {
pos = skip_whitespace(expr, pos); pos = skip_whitespace(expr, pos);
if (pos == expr.size()) if (pos == expr.size())
return Err(parsing_error(pos, "expected: character '{}", expr[pos])); return Err(parsing_error(pos, "unexpected end of input"));
const auto kar = expr[pos]; const auto kar = expr[pos];
const auto sexp = std::invoke([&]() -> Result<Sexp> { const auto sexp = std::invoke([&]() -> Result<Sexp> {
if (kar == '(') if (kar == '(')
return parse_list(expr, pos); return parse_list(expr, pos, depth);
else if (kar == '"') else if (kar == '"')
return parse_string(expr, pos); return parse_string(expr, pos);
else if (isdigit(kar) || kar == '-') else if (is_ascii_digit(kar) || kar == '-')
return parse_integer(expr, pos); return parse_integer(expr, pos);
else if (isalpha(kar) || kar == ':') else if (is_ascii_alpha(kar) || kar == ':')
return parse_symbol(expr, pos); return parse_symbol(expr, pos);
else else
return Err(parsing_error(pos, "unexpected character '{}", kar)); return Err(parsing_error(pos, "unexpected character '{}'", kar));
}); });
if (sexp) if (sexp)
@ -171,7 +175,7 @@ Result<Sexp>
Sexp::parse(const std::string& expr) Sexp::parse(const std::string& expr)
{ {
size_t pos{}; size_t pos{};
auto res = ::parse(expr, pos); auto res = ::parse(expr, pos, 0/*depth*/);
if (!res) if (!res)
return res; return res;
else if (pos != expr.size()) else if (pos != expr.size())
@ -183,31 +187,39 @@ Sexp::parse(const std::string& expr)
std::string std::string
Sexp::to_string(Format fopts) const Sexp::to_string(Format fopts) const
{ {
std::stringstream sstrm; std::string str;
const auto splitp{any_of(fopts & Format::SplitList)}; to_string_into(str, fopts);
const auto typeinfop{any_of(fopts & Format::TypeInfo)};
return str;
}
void
Sexp::to_string_into(std::string& out, Format fopts) const
{
if (listp()) { if (listp()) {
sstrm << '('; out += '(';
bool first{true}; bool first{true};
for(auto&& elm: list()) { for(auto&& elm: list()) {
sstrm << (first ? "" : " ") << elm.to_string(fopts); if (!first)
out += ' ';
elm.to_string_into(out, fopts);
first = false; first = false;
} }
sstrm << ')'; out += ')';
if (splitp) if (any_of(fopts & Format::SplitList))
sstrm << '\n'; out += '\n';
} else if (stringp()) } else if (stringp())
sstrm << quote(string()); out += quote(string());
else if (numberp()) else if (numberp())
sstrm << number(); out += std::to_string(number());
else if (symbolp()) else if (symbolp())
sstrm << symbol().name; out += symbol().name;
if (typeinfop) if (any_of(fopts & Format::TypeInfo)) {
sstrm << '<' << Sexp::type_name(type()) << '>'; out += '<';
out += Sexp::type_name(type());
return sstrm.str(); out += '>';
}
} }
// LCOV_EXCL_START // LCOV_EXCL_START
@ -226,8 +238,15 @@ unix_tstamp(const Sexp& emacs_tstamp)
std::string std::string
Sexp::to_json_string(Format fopts) const Sexp::to_json_string(Format fopts) const
{ {
std::stringstream sstrm; std::string str;
to_json_string_into(str, fopts);
return str;
}
void
Sexp::to_json_string_into(std::string& out, Format fopts) const
{
const auto sym_name=[&](const std::string& sym) { const auto sym_name=[&](const std::string& sym) {
if (any_of(fopts & Format::NoColon) && sym[0] == ':') if (any_of(fopts & Format::NoColon) && sym[0] == ':')
return sym.substr(1); // remove colon return sym.substr(1); // remove colon
@ -239,60 +258,64 @@ Sexp::to_json_string(Format fopts) const
case Type::List: { case Type::List: {
// property-lists become JSON objects // property-lists become JSON objects
if (plistp()) { if (plistp()) {
sstrm << "{"; out += '{';
auto it{list().begin()}; auto it{list().begin()};
bool first{true}; bool first{true};
while (it != list().end()) { while (it != list().end()) {
const auto key{it->symbol().name}; const auto key{it->symbol().name};
sstrm << (first ? "" : ",") if (!first)
<< quote(sym_name(key)) << ":"; out += ',';
out += quote(sym_name(key));
out += ':';
++it; ++it;
const auto emacs_tstamp{*it}; const auto& propval{*it};
sstrm << emacs_tstamp.to_json_string(fopts); propval.to_json_string_into(out, fopts);
++it; ++it;
first = false; first = false;
// special-case: tstamp-fields also get a "unix" value, // special-case: tstamp-fields also get a "unix" value,
// which are easier to work with than the "emacs" timestamps // which are easier to work with than the "emacs" timestamps
if (key == ":date" || key == ":changed") if (key == ":date" || key == ":changed") {
sstrm << "," << quote(sym_name(key) + "-unix") out += ',';
<< ":" out += quote(sym_name(key) + "-unix");
<< unix_tstamp(emacs_tstamp); out += ':';
out += std::to_string(unix_tstamp(propval));
}
} }
sstrm << "}"; out += '}';
if (any_of(fopts & Format::SplitList)) if (any_of(fopts & Format::SplitList))
sstrm << '\n'; out += '\n';
} else { // other lists become arrays. } else { // other lists become arrays.
sstrm << '['; out += '[';
bool first{true}; bool first{true};
for (auto&& child : list()) { for (auto&& child : list()) {
sstrm << (first ? "" : ", ") << child.to_json_string(fopts); if (!first)
out += ", ";
child.to_json_string_into(out, fopts);
first = false; first = false;
} }
sstrm << ']'; out += ']';
if (any_of(fopts & Format::SplitList)) if (any_of(fopts & Format::SplitList))
sstrm << '\n'; out += '\n';
} }
break; break;
} }
case Type::String: case Type::String:
sstrm << quote(string()); out += quote(string());
break; break;
case Type::Symbol: case Type::Symbol:
if (nilp()) if (nilp())
sstrm << "false"; out += "false";
else if (symbol() == "t") else if (symbol() == "t")
sstrm << "true"; out += "true";
else else
sstrm << quote(symbol().name); out += quote(symbol().name);
break; break;
case Type::Number: case Type::Number:
sstrm << number(); out += std::to_string(number());
break; break;
default: default:
break; break;
} }
return sstrm.str();
} }
@ -511,6 +534,21 @@ test_parser()
check_parse(R"("foo check_parse(R"("foo
bar")", bar")",
"\"foo\nbar\""); "\"foo\nbar\"");
// escapes: '\n'/'\t' are special; \\ and unknown escapes
// stand for the escaped character itself
check_parse(R"("hello\nworld")", "\"hello\nworld\"");
check_parse(R"("tab\there")", "\"tab\there\"");
check_parse(R"("back\\slash")", "\"back\\\\slash\"");
check_parse(R"("what\qever")", "\"whatqever\"");
// full int64 range
check_parse("4294967296", "4294967296");
check_parse("9223372036854775807", "9223372036854775807");
check_parse("-9223372036854775808", "-9223372036854775808");
// \r\n is whitespace, too
check_parse("(foo\r\nbar)", "(foo bar)");
} }
static void static void
@ -522,6 +560,19 @@ test_parser_fail()
g_assert_false(!!Sexp::parse(")")); g_assert_false(!!Sexp::parse(")"));
g_assert_false(!!Sexp::parse("(hello (boo))))")); g_assert_false(!!Sexp::parse("(hello (boo))))"));
// numbers that don't fit an int64, or aren't numbers at all
g_assert_false(!!Sexp::parse("99999999999999999999"));
g_assert_false(!!Sexp::parse("-"));
// trailing backslash
g_assert_false(!!Sexp::parse(R"("foo\)"));
// deeply-nested input gives an error, not a stack overflow
std::string deep(100000, '(');
deep += "a";
deep.append(100000, ')');
g_assert_false(!!Sexp::parse(deep));
g_assert_true(Sexp::type_name(static_cast<Sexp::Type>(-1)) == "<error>"); g_assert_true(Sexp::type_name(static_cast<Sexp::Type>(-1)) == "<error>");
} }

View File

@ -25,9 +25,9 @@
#include <vector> #include <vector>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <iostream>
#include <variant> #include <variant>
#include <ostream> #include <ostream>
#include <type_traits>
#include <utils/mu-result.hh> #include <utils/mu-result.hh>
#include <utils/mu-option.hh> #include <utils/mu-option.hh>
@ -56,13 +56,20 @@ struct Sexp {
operator const std::string&() const {return name; } operator const std::string&() const {return name; }
std::string name; std::string name;
bool operator==(const Symbol& rhs) const { bool operator==(const Symbol& rhs) const = default;
return this == &rhs ? true : rhs.name == name;
}
bool operator!=(const Symbol& rhs) const { return *this == rhs ? false : true; }
}; };
enum struct Type { List, String, Number, Symbol }; enum struct Type { List, String, Number, Symbol };
using ValueType = std::variant<List, String, Number, Symbol>; using ValueType = std::variant<List, String, Number, Symbol>;
// the Type enum and the ValueType variant must be kept in sync,
// since type() merely casts the variant-index.
static_assert(std::is_same_v<std::variant_alternative_t<
static_cast<size_t>(Type::List), ValueType>, List>);
static_assert(std::is_same_v<std::variant_alternative_t<
static_cast<size_t>(Type::String), ValueType>, String>);
static_assert(std::is_same_v<std::variant_alternative_t<
static_cast<size_t>(Type::Number), ValueType>, Number>);
static_assert(std::is_same_v<std::variant_alternative_t<
static_cast<size_t>(Type::Symbol), ValueType>, Symbol>);
/** /**
* Is some Sexp of the given type? * Is some Sexp of the given type?
@ -91,8 +98,8 @@ struct Sexp {
*/ */
Sexp():value{List{}} {} // default: an empty list. Sexp():value{List{}} {} // default: an empty list.
// Copy & move ctors // Copy & move ctors
Sexp(const Sexp& other):value{other.value}{} Sexp(const Sexp& other) = default;
Sexp(Sexp&& other):value{std::move(other.value)}{} Sexp(Sexp&& other) = default;
// From various types // From various types
Sexp(const List& lst): value{lst} {} Sexp(const List& lst): value{lst} {}
Sexp(List&& lst): value{std::move(lst)} {} Sexp(List&& lst): value{std::move(lst)} {}
@ -118,31 +125,9 @@ struct Sexp {
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
} }
/** // Copy & move assignment
* Copy-assignment Sexp& operator=(const Sexp& rhs) = default;
* Sexp& operator=(Sexp&& rhs) = default;
* @param rhs another sexp
*
* @return the sexp
*/
Sexp& operator=(const Sexp& rhs) {
if (this != &rhs)
value = rhs.value;
return *this;
}
/**
* Move-assignment
*
* @param rhs another sexp
*
* @return the sexp
*/
Sexp& operator=(Sexp&& rhs) {
if (this != &rhs)
value = std::move(rhs.value);
return *this;
}
/** /**
* Get the type of value * Get the type of value
@ -207,7 +192,7 @@ struct Sexp {
Sexp& add() { return *this; } Sexp& add() { return *this; }
template <typename V1, typename V2, typename... Args> template <typename V1, typename V2, typename... Args>
Sexp& add(V1&& v1, V2&& v2, Args... args) { Sexp& add(V1&& v1, V2&& v2, Args&&... args) {
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" #pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
return add(std::forward<V1>(v1)) return add(std::forward<V1>(v1))
@ -241,7 +226,7 @@ struct Sexp {
bool plistp() const { return listp() && plistp(cbegin(), cend()); } bool plistp() const { return listp() && plistp(cbegin(), cend()); }
Sexp& put_props() { return *this; } // Final case for template pack. Sexp& put_props() { return *this; } // Final case for template pack.
template <class PropType, class SexpType, typename... Args> template <class PropType, class SexpType, typename... Args>
Sexp& put_props(PropType&& prop, SexpType&& sexp, Args... args) { Sexp& put_props(PropType&& prop, SexpType&& sexp, Args&&... args) {
auto&& propname{std::string(prop)}; auto&& propname{std::string(prop)};
return del_prop(propname) return del_prop(propname)
.add(Symbol(std::move(propname)), .add(Symbol(std::move(propname)),
@ -256,7 +241,7 @@ struct Sexp {
* *
* @return the property if found, or nothing * @return the property if found, or nothing
*/ */
const Option<const Sexp&> get_prop(const std::string& p) const { Option<const Sexp&> get_prop(const std::string& p) const {
if (auto&& it = find_prop(p, cbegin(), cend()); it != cend()) if (auto&& it = find_prop(p, cbegin(), cend()); it != cend())
return *(std::next(it)); return *(std::next(it));
else else
@ -295,6 +280,9 @@ protected:
private: private:
iterator find_prop(const std::string& s,iterator b, iterator find_prop(const std::string& s,iterator b,
iterator e); iterator e);
void to_string_into(std::string& out, Format fopts) const;
void to_json_string_into(std::string& out, Format fopts) const;
ValueType value; ValueType value;
@ -308,7 +296,7 @@ MU_ENABLE_BITOPS(Sexp::Format);
inline Sexp::Symbol inline Sexp::Symbol
operator""_sym(const char* str, std::size_t n) operator""_sym(const char* str, std::size_t n)
{ {
return Sexp::Symbol{str}; return Sexp::Symbol{std::string{str, n}};
} }
inline std::ostream& inline std::ostream&