/* ** Copyright (C) 2022-2023 Dirk-Jan C. Binnema ** ** This program 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, or (at your option) any ** later version. ** ** This program 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 this program; if not, write to the Free Software Foundation, ** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ** */ #include "mu-sexp.hh" #include "mu-utils.hh" #include #include #include using namespace Mu; __attribute__((format(printf, 2, 0))) static Mu::Error parsing_error(size_t pos, const char* frm, ...) { va_list args; va_start(args, frm); auto msg = vformat(frm, args); va_end(args); if (pos == 0) return Mu::Error(Error::Code::Parsing, "%s", msg.c_str()); else return Mu::Error(Error::Code::Parsing, "%zu: %s", pos, msg.c_str()); } static size_t skip_whitespace(const std::string& s, size_t pos) { while (pos != s.size()) { if (s[pos] == ' ' || s[pos] == '\t' || s[pos] == '\n') ++pos; else break; } return pos; } static Result parse(const std::string& expr, size_t& pos); static Result parse_list(const std::string& expr, size_t& pos) { if (expr[pos] != '(') // sanity check. return Err(parsing_error(pos, "expected: '(' but got '%c", expr[pos])); Sexp lst{}; ++pos; while (expr[pos] != ')' && pos != expr.size()) { if (auto&& item = parse(expr, pos); item) lst.add(std::move(*item)); else return Err(item.error()); } if (expr[pos] != ')') return Err(parsing_error(pos, "expected: ')' but got '%c'", expr[pos])); ++pos; return Ok(std::move(lst)); } static Result parse_string(const std::string& expr, size_t& pos) { if (expr[pos] != '"') // sanity check. return Err(parsing_error(pos, "expected: '\"'' but got '%c", expr[pos])); bool escape{}; std::string str; for (++pos; pos != expr.size(); ++pos) { auto kar = expr[pos]; if (escape && (kar == '"' || kar == '\\')) { str += kar; escape = false; continue; } if (kar == '"') break; else if (kar == '\\') escape = true; else str += kar; } if (escape || expr[pos] != '"') return Err(parsing_error(pos, "unterminated string '%s'", str.c_str())); ++pos; return Ok(Sexp{std::move(str)}); } static Result parse_integer(const std::string& expr, size_t& pos) { if (!isdigit(expr[pos]) && expr[pos] != '-') // sanity check. return Err(parsing_error(pos, "expected: but got '%c", expr[pos])); std::string num; // negative number? if (expr[pos] == '-') { num = "-"; ++pos; } for (; isdigit(expr[pos]); ++pos) num += expr[pos]; return Ok(Sexp{::atoi(num.c_str())}); } static Result parse_symbol(const std::string& expr, size_t& pos) { if (!isalpha(expr[pos]) && expr[pos] != ':') // sanity check. return Err(parsing_error(pos, "expected: |: but got '%c", expr[pos])); std::string symb(1, expr[pos]); for (++pos; isalnum(expr[pos]) || expr[pos] == '-'; ++pos) symb += expr[pos]; return Ok(Sexp{Sexp::Symbol{symb}}); } static Result parse(const std::string& expr, size_t& pos) { pos = skip_whitespace(expr, pos); if (pos == expr.size()) return Err(parsing_error(pos, "expected: character '%c", expr[pos])); const auto kar = expr[pos]; const auto sexp = std::invoke([&]() -> Result { if (kar == '(') return parse_list(expr, pos); else if (kar == '"') return parse_string(expr, pos); else if (isdigit(kar) || kar == '-') return parse_integer(expr, pos); else if (isalpha(kar) || kar == ':') return parse_symbol(expr, pos); else return Err(parsing_error(pos, "unexpected character '%c", kar)); }); if (sexp) pos = skip_whitespace(expr, pos); return sexp; } Result Sexp::parse(const std::string& expr) { size_t pos{}; auto res = ::parse(expr, pos); if (!res) return res; else if (pos != expr.size()) return Err(parsing_error(pos, "trailing data starting with '%c'", expr[pos])); else return res; } std::string Sexp::to_string(Format fopts) const { std::stringstream sstrm; const auto splitp{any_of(fopts & Format::SplitList)}; const auto typeinfop{any_of(fopts & Format::TypeInfo)}; if (listp()) { sstrm << '('; bool first{true}; for(auto&& elm: list()) { sstrm << (first ? "" : " ") << elm.to_string(fopts); first = false; } sstrm << ')'; if (splitp) sstrm << '\n'; } else if (stringp()) sstrm << quote(string()); else if (numberp()) sstrm << number(); else if (symbolp()) sstrm << symbol().name; if (typeinfop) sstrm << '<' << Sexp::type_name(type()) << '>'; return sstrm.str(); } // LCOV_EXCL_START std::string Sexp::to_json_string(Format fopts) const { std::stringstream sstrm; switch (type()) { case Type::List: { // property-lists become JSON objects if (plistp()) { sstrm << "{"; auto it{list().begin()}; bool first{true}; while (it != list().end()) { sstrm << (first ? "" : ",") << quote(it->symbol().name) << ":"; ++it; sstrm << it->to_json_string(); ++it; first = false; } sstrm << "}"; if (any_of(fopts & Format::SplitList)) sstrm << '\n'; } else { // other lists become arrays. sstrm << '['; bool first{true}; for (auto&& child : list()) { sstrm << (first ? "" : ", ") << child.to_json_string(); first = false; } sstrm << ']'; if (any_of(fopts & Format::SplitList)) sstrm << '\n'; } break; } case Type::String: sstrm << quote(string()); break; case Type::Symbol: if (nilp()) sstrm << "false"; else if (symbol() == "t") sstrm << "true"; else sstrm << quote(symbol().name); break; case Type::Number: sstrm << number(); break; default: break; } return sstrm.str(); } Sexp& Sexp::del_prop(const std::string& pname) { if (auto kill_it = find_prop(pname, begin(), end()); kill_it != cend()) list().erase(kill_it, kill_it + 2); return *this; } Sexp::const_iterator Sexp::find_prop(const std::string& s, Sexp::const_iterator b, Sexp::const_iterator e) const { for (auto&& it = b; it != e && it+1 != e; it += 2) if (it->symbolp() && it->symbol() == s) return it; return e; } Sexp::iterator Sexp::find_prop(const std::string& s, Sexp::iterator b, Sexp::iterator e) { for (auto&& it = b; it != e && it+1 != e; it += 2) if (it->symbolp() && it->symbol() == s) return it; return e; } bool Sexp::plistp(Sexp::const_iterator b, Sexp::const_iterator e) const { if (b == e) return true; else if (b + 1 == e) return false; else return b->symbolp() && plistp(b + 2, e); } // LCOV_EXCL_STOP #if BUILD_TESTS #include "mu-test-utils.hh" static void test_list() { { Sexp s; g_assert_true(s.listp()); g_assert_true(s.to_string() == "()"); g_assert_true(Sexp::type_name(s.type()) == "list"); g_assert_true(s.empty()); } { Sexp::List items = { Sexp("hello"), Sexp(123), Sexp::Symbol("world") }; const Sexp s{std::move(items)}; g_assert_false(s.empty()); g_assert_cmpuint(s.size(),==,3); g_assert_true(s.to_string() == "(\"hello\" 123 world)"); /* copy */ Sexp s2 = s; g_assert_true(s2.to_string() == "(\"hello\" 123 world)"); /* move */ Sexp s3 = std::move(s2); g_assert_true(s3.to_string() == "(\"hello\" 123 world)"); s3.clear(); g_assert_true(s3.empty()); } } static void test_string() { { Sexp s("hello"); g_assert_true(s.stringp()); g_assert_true(s.string()=="hello"); g_assert_true(s.to_string()=="\"hello\""); g_assert_true(Sexp::type_name(s.type()) == "string"); } { // Sexp s(std::string_view("hel\"lo")); // g_assert_true(s.is_string()); // g_assert_cmpstr(s.string().c_str(),==,"hel\"lo"); // g_assert_cmpstr(s.to_string().c_str(),==,"\"hel\\\"lo\""); } } static void test_number() { { Sexp s(123); g_assert_true(s.numberp()); g_assert_cmpint(s.number(),==,123); g_assert_true(s.to_string() == "123"); g_assert_true(Sexp::type_name(s.type()) == "number"); } { Sexp s(true); g_assert_true(s.numberp()); g_assert_cmpint(s.number(),==,1); g_assert_true(s.to_string()=="1"); } } static void test_symbol() { { Sexp s{Sexp::Symbol("hello")}; g_assert_true(s.symbolp()); g_assert_true(s.symbol()=="hello"); g_assert_true (s.to_string()=="hello"); g_assert_true(Sexp::type_name(s.type()) == "symbol"); } { Sexp s{"hello"_sym}; g_assert_true(s.symbolp()); g_assert_true(s.symbol()=="hello"); g_assert_true (s.to_string()=="hello"); } } static void test_multi() { Sexp s{"abc", 123, Sexp::Symbol{"def"}}; g_assert_true(s.to_string() == "(\"abc\" 123 def)"); } static void test_add() { { Sexp s{"abc", 123}; s.add("def"_sym); g_assert_true(s.to_string() == "(\"abc\" 123 def)"); } } static void test_add_multi() { { Sexp s{"abc", 123}; s.add("def"_sym, 456, Sexp{"boo", 2}); g_assert_true(s.to_string() == "(\"abc\" 123 def 456 (\"boo\" 2))"); } { Sexp s{"abc", 123}; Sexp t{"boo", 2}; s.add("def"_sym, 456, t); g_assert_true(s.to_string() == "(\"abc\" 123 def 456 (\"boo\" 2))"); } } static void test_plist() { Sexp s; s.put_props("hello", "world"_sym, "foo", 123, "bar"_sym, "cuux"); g_assert_true(s.to_string() == R"((hello world foo 123 bar "cuux"))"); s.put_props("hello", 12345); g_assert_true(s.to_string() == R"((foo 123 bar "cuux" hello 12345))"); } static void check_parse(const std::string& expr, const std::string& expected) { auto sexp = Sexp::parse(expr); assert_valid_result(sexp); assert_equal(to_string(*sexp), expected); } static void test_parser() { check_parse(":foo-123", ":foo-123"); check_parse("foo", "foo"); check_parse(R"(12345)", "12345"); check_parse(R"(-12345)", "-12345"); check_parse(R"((123 bar "cuux"))", "(123 bar \"cuux\")"); check_parse(R"("foo\"bar\"cuux")", "\"foo\\\"bar\\\"cuux\""); check_parse(R"("foo bar")", "\"foo\nbar\""); } static void test_parser_fail() { g_assert_false(!!Sexp::parse("\"")); g_assert_false(!!Sexp::parse("123abc")); g_assert_false(!!Sexp::parse("(")); g_assert_false(!!Sexp::parse(")")); g_assert_false(!!Sexp::parse("(hello (boo))))")); g_assert_true(Sexp::type_name(static_cast(-1)) == ""); } int main(int argc, char* argv[]) try { mu_test_init(&argc, &argv); g_test_add_func("/sexp/list", test_list); g_test_add_func("/sexp/string", test_string); g_test_add_func("/sexp/number", test_number); g_test_add_func("/sexp/symbol", test_symbol); g_test_add_func("/sexp/multi", test_multi); g_test_add_func("/sexp/add", test_add); g_test_add_func("/sexp/add-multi", test_add_multi); g_test_add_func("/sexp/plist", test_plist); g_test_add_func("/sexp/parser", test_parser); g_test_add_func("/sexp/parser-fail", test_parser_fail); return g_test_run(); } catch (const std::runtime_error& re) { std::cerr << re.what() << "\n"; return 1; } #endif /*BUILD_TESTS*/