Files
secs-gem/src/secs2/sml.cpp
T
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

221 lines
6.5 KiB
C++

#include "secsgem/secs2/sml.hpp"
#include <cctype>
#include <cstdlib>
#include <string>
#include <vector>
namespace secsgem::secs2 {
namespace {
struct Cursor {
const std::string& s;
std::size_t i = 0;
bool done() const { return i >= s.size(); }
char peek() const { return s[i]; }
char get() { return s[i++]; }
void skip_ws() {
while (i < s.size() && std::isspace(static_cast<unsigned char>(s[i]))) ++i;
}
void expect(char c) {
skip_ws();
if (i >= s.size() || s[i] != c)
throw SmlError(std::string("expected `") + c + "` at " + std::to_string(i));
++i;
}
bool consume(char c) {
skip_ws();
if (i < s.size() && s[i] == c) { ++i; return true; }
return false;
}
std::string read_word() {
skip_ws();
const std::size_t start = i;
while (i < s.size() &&
!std::isspace(static_cast<unsigned char>(s[i])) &&
s[i] != '<' && s[i] != '>' && s[i] != '[' && s[i] != ']' &&
s[i] != '"') {
++i;
}
if (i == start) throw SmlError("expected word at " + std::to_string(i));
return s.substr(start, i - start);
}
std::string read_quoted() {
skip_ws();
if (i >= s.size() || s[i] != '"') throw SmlError("expected `\"`");
++i;
const std::size_t start = i;
while (i < s.size() && s[i] != '"') {
// No escape sequences in the SML emitter; mirror that here.
++i;
}
if (i >= s.size()) throw SmlError("unterminated string");
std::string out = s.substr(start, i - start);
++i; // consume closing quote
return out;
}
};
Format parse_tag(const std::string& tag) {
if (tag == "L") return Format::List;
if (tag == "A") return Format::ASCII;
if (tag == "J") return Format::JIS8;
if (tag == "C") return Format::C2;
if (tag == "B") return Format::Binary;
if (tag == "BOOLEAN") return Format::Boolean;
if (tag == "U1") return Format::U1;
if (tag == "U2") return Format::U2;
if (tag == "U4") return Format::U4;
if (tag == "U8") return Format::U8;
if (tag == "I1") return Format::I1;
if (tag == "I2") return Format::I2;
if (tag == "I4") return Format::I4;
if (tag == "I8") return Format::I8;
if (tag == "F4") return Format::F4;
if (tag == "F8") return Format::F8;
throw SmlError("unknown SML tag `" + tag + "`");
}
// Parse one byte literal — accepts 0xHH and decimal forms.
uint8_t parse_byte(const std::string& w) {
return static_cast<uint8_t>(std::strtoul(w.c_str(), nullptr, 0));
}
Item parse_item(Cursor& c);
Item parse_scalar_body(Cursor& c, Format fmt) {
switch (fmt) {
case Format::ASCII:
case Format::JIS8: {
auto s = c.read_quoted();
return fmt == Format::ASCII ? Item::ascii(s) : Item::jis8(s);
}
case Format::C2: {
// C2 is a vector of 16-bit code points; SML round-trip for C2 is
// not yet specified by the emitter — accept whitespace-separated
// integers for now.
std::vector<uint16_t> v;
while (true) {
c.skip_ws();
if (c.done() || c.peek() == '>') break;
v.push_back(static_cast<uint16_t>(std::strtoul(c.read_word().c_str(), nullptr, 0)));
}
return Item::c2(v);
}
case Format::Binary: {
std::vector<uint8_t> bytes;
while (true) {
c.skip_ws();
if (c.done() || c.peek() == '>') break;
bytes.push_back(parse_byte(c.read_word()));
}
return Item::binary(bytes);
}
case Format::Boolean: {
std::vector<uint8_t> bytes;
while (true) {
c.skip_ws();
if (c.done() || c.peek() == '>') break;
auto w = c.read_word();
bool b = (w == "T" || w == "TRUE" || w == "true" || w == "1");
bytes.push_back(b ? 1 : 0);
}
return Item::boolean(bytes);
}
#define SML_NUM(fmtT, ctype, factory, parser) \
case Format::fmtT: { \
std::vector<ctype> v; \
while (true) { \
c.skip_ws(); \
if (c.done() || c.peek() == '>') break; \
auto w = c.read_word(); \
v.push_back(static_cast<ctype>(parser(w.c_str(), nullptr, 0))); \
} \
return factory(v); \
}
SML_NUM(U1, uint8_t, Item::u1, std::strtoul)
SML_NUM(U2, uint16_t, Item::u2, std::strtoul)
SML_NUM(U4, uint32_t, Item::u4, std::strtoul)
SML_NUM(U8, uint64_t, Item::u8, std::strtoull)
SML_NUM(I1, int8_t, Item::i1, std::strtol)
SML_NUM(I2, int16_t, Item::i2, std::strtol)
SML_NUM(I4, int32_t, Item::i4, std::strtol)
SML_NUM(I8, int64_t, Item::i8, std::strtoll)
#undef SML_NUM
case Format::F4: {
std::vector<float> v;
while (true) {
c.skip_ws();
if (c.done() || c.peek() == '>') break;
v.push_back(std::strtof(c.read_word().c_str(), nullptr));
}
return Item::f4(v);
}
case Format::F8: {
std::vector<double> v;
while (true) {
c.skip_ws();
if (c.done() || c.peek() == '>') break;
v.push_back(std::strtod(c.read_word().c_str(), nullptr));
}
return Item::f8(v);
}
case Format::List:
// Unreachable; handled in parse_item.
throw SmlError("internal: List shape in scalar dispatch");
}
throw SmlError("unhandled format in scalar dispatch");
}
Item parse_item(Cursor& c) {
c.expect('<');
auto tag = c.read_word();
Format fmt = parse_tag(tag);
// Optional `[n]` count — accepted, ignored.
if (c.consume('[')) {
(void)c.read_word();
c.expect(']');
}
if (fmt == Format::List) {
Item::List children;
while (true) {
c.skip_ws();
if (c.done()) throw SmlError("unterminated list");
if (c.peek() == '>') { c.get(); break; }
children.push_back(parse_item(c));
}
return Item::list(std::move(children));
}
Item result = parse_scalar_body(c, fmt);
c.expect('>');
return result;
}
} // namespace
Item parse_sml(const std::string& text) {
Cursor c{text};
c.skip_ws();
return parse_item(c);
}
std::optional<Item> try_parse_sml(const std::string& text) {
try {
return parse_sml(text);
} catch (const SmlError&) {
return std::nullopt;
}
}
} // namespace secsgem::secs2