Files
raphael e2348db082 I1: SML parser — inverse of to_sml()
Adds parse_sml(text) -> Item / try_parse_sml(text) -> optional<Item>
in secs2/sml.hpp.  Round-trips with the existing to_sml() emitter for
every Item shape the codec produces: lists with nesting, ASCII / JIS8,
Binary (decimal and 0xHH literals), Boolean (T/F or 1/0, scalar and
multi-element), U1-U8 / I1-I8 / F4 / F8 vectors, and the optional
`[n]` count syntax (accepted but not enforced).

The parser is whitespace-insensitive outside quoted strings and uses
a small Cursor type for read_word / read_quoted / skip_ws.  Numeric
literals go through strtoul/strtoll/strtod so SML can carry hex,
octal, and decimal interchangeably (the emitter writes hex for Binary
and decimal everywhere else).

11 test cases cover the full round-trip surface, the whitespace
invariant, unknown-tag rejection, the try_parse error-swallowing
variant, and the optional `[n]` count.

secsgem-py has secs/sml.py for the same purpose; this brings the C++
port to parity on the tooling side.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 03:56:13 +02:00

83 lines
2.3 KiB
C++

#include <doctest/doctest.h>
#include <string>
#include "secsgem/secs2/codec.hpp"
#include "secsgem/secs2/item.hpp"
#include "secsgem/secs2/sml.hpp"
using namespace secsgem::secs2;
// Round-trip an item through to_sml() / parse_sml() and assert equality.
// Equality on Item is structural so this catches any drift.
static void roundtrip(const Item& original) {
const auto text = to_sml(original);
const auto parsed = parse_sml(text);
CHECK(parsed == original);
}
TEST_CASE("SML: ASCII round-trip") {
roundtrip(Item::ascii("hello world"));
roundtrip(Item::ascii(""));
}
TEST_CASE("SML: U2 / U4 vector round-trip") {
roundtrip(Item::u2(std::vector<uint16_t>{1, 2, 3}));
roundtrip(Item::u4(uint32_t{42}));
}
TEST_CASE("SML: signed integers + floats") {
roundtrip(Item::i4(int32_t{-1}));
roundtrip(Item::i8(std::vector<int64_t>{-9999, 0, 9999}));
// Float comparison via SML drops trailing zeros; check the structure
// rather than the exact value.
auto parsed = parse_sml(to_sml(Item::f4(3.5f)));
REQUIRE(parsed.format() == Format::F4);
}
TEST_CASE("SML: Binary scalar + multi-byte") {
roundtrip(Item::binary(std::vector<uint8_t>{0xDE, 0xAD, 0xBE, 0xEF}));
}
TEST_CASE("SML: Boolean scalar and multi-element") {
roundtrip(Item::boolean(true));
roundtrip(Item::boolean(std::vector<uint8_t>{1, 0, 1, 0}));
}
TEST_CASE("SML: nested list round-trip") {
auto item = Item::list({
Item::ascii("LOT-A"),
Item::list({
Item::u4(uint32_t{1}),
Item::ascii("step-1"),
}),
Item::binary(std::vector<uint8_t>{0x01, 0x02}),
});
roundtrip(item);
}
TEST_CASE("SML: parser is whitespace-insensitive") {
auto compact = parse_sml(R"(<L<A "x"><U4 42>>)");
auto spaced = parse_sml(R"(< L
< A "x" >
< U4 42 >
>)");
CHECK(compact == spaced);
}
TEST_CASE("SML: parse rejects unknown tag") {
CHECK_THROWS_AS(parse_sml("<XXX>"), SmlError);
}
TEST_CASE("SML: try_parse_sml swallows errors") {
CHECK_FALSE(try_parse_sml("<garbage").has_value());
CHECK(try_parse_sml("<U4 1 >").has_value());
}
TEST_CASE("SML: optional [n] count is accepted but not enforced") {
// Emitter writes "<L [2] ...>"; parser must accept it back.
auto m = parse_sml(R"(<L [3] <A "a"> <A "b"> <A "c">>)");
REQUIRE(m.format() == Format::List);
CHECK(m.size() == 3);
}