sots-engine/src/mars/stream/archive.h

684 lines
25 KiB
C++

// mars::stream — archives that drive a typed shape's io() field list.
//
// A shape is a plain struct with
// static constexpr const char* kStreamName = "Sys"; // "" when the frame is unnamed ("." elements)
// template <class Ar> void io(Ar& ar) { ar.i32(A("Idx"), idx); ... }
// listing its fields in on-disk order. The same io() is driven by three
// archives:
// ReadArchive fills the struct from a generic Node tree (walker output),
// matching tags by name (A) or position (R), coercing kinds
// and reporting deviations as Issues;
// WriteArchive emits the struct through a Writer in the same order;
// SchemaBuilder records the field list as a Desc so the walker can type the
// frame's children before any shape is applied.
//
// Tag helpers: A("Turn") = on-disk tag confirmed, matched by name.
// R("treasury") = reference/positional field, written as "." (or
// the given disk tag); matched by position.
#pragma once
#include <cstdint>
#include <functional>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <type_traits>
#include <typeindex>
#include <vector>
#include "node.h"
#include "schema.h"
#include "writer.h"
namespace mars::stream {
struct Tag {
const char* name; // schema name
const char* disk; // tag emitted when writing
bool positional;
};
constexpr Tag A(const char* n) { return Tag{n, n, false}; }
constexpr Tag R(const char* n, const char* disk = ".") { return Tag{n, disk, true}; }
struct Vec3 {
float x = 0, y = 0, z = 0;
bool operator==(const Vec3& o) const { return x == o.x && y == o.y && z == o.z; }
};
template <class T, class = void>
struct has_io : std::false_type {};
template <class T>
struct has_io<T, std::void_t<decltype(T::kStreamName)>> : std::true_type {};
// ---------------------------------------------------------------------------
// ReadArchive
// ---------------------------------------------------------------------------
class ReadArchive {
public:
static constexpr bool reading = true, writing = false, building = false;
ReadArchive(const std::vector<Node>& nodes, std::vector<Issue>& issues, std::string path)
: nodes_(&nodes), issues_(&issues), path_(std::move(path)) {}
// --- scalars ------------------------------------------------------------
void i32(Tag t, int32_t& v) {
if (const Node* n = take_named(t)) v = coerce_int(*n, fpath(t));
}
void f32(Tag t, float& v) {
if (const Node* n = take_named(t)) v = coerce_float(*n, fpath(t));
}
void b(Tag t, bool& v) {
if (const Node* n = take_named(t)) v = coerce_bool(*n, fpath(t));
}
void i64(Tag t, int64_t& v) {
if (const Node* n = take_named(t)) v = coerce_int64(*n, fpath(t));
}
void str(Tag t, std::string& v) {
if (const Node* n = take_named(t)) v = coerce_string(*n, fpath(t));
}
void vec3(Tag t, Vec3& v) {
if (const Node* n = take_named(t)) v = coerce_vec3(*n, fpath(t));
}
// generic item kept as a node copy ("any")
void any(Tag t, Node& v) {
if (const Node* n = take_named(t)) v = *n;
}
void raw_frame(Tag t, Node& v) { any(t, v); }
// --- optional named items (consumed only when the next tag matches) ------
void opt_i32(Tag t, std::optional<int32_t>& v) {
if (next_is(t)) v = coerce_int(*take(), fpath(t));
}
void opt_f32(Tag t, std::optional<float>& v) {
if (next_is(t)) v = coerce_float(*take(), fpath(t));
}
void opt_b(Tag t, std::optional<bool>& v) {
if (next_is(t)) v = coerce_bool(*take(), fpath(t));
}
void opt_any(Tag t, std::optional<Node>& v) {
if (next_is(t)) v = *take();
}
template <class T>
void opt_obj(Tag t, std::optional<T>& v) {
if (next_is(t)) {
v.emplace();
obj(t, *v);
}
}
// --- framed struct ---------------------------------------------------------
template <class T>
void obj(Tag t, T& v) {
const Node* n = take_named(t);
if (!n) return;
read_frame(*n, v, fpath(t));
}
// struct that may also appear inline (unframed); `framed` records which
template <class T>
void obj_flex(Tag t, T& v, bool& framed) {
const Node* n = peek();
if (n && !n->is_complex()) {
framed = false;
v.io(*this);
return;
}
framed = true;
obj(t, v);
}
// --- framed array: "." count + n x "." element -----------------------------
template <class T>
void carr(Tag t, std::vector<T>& v) {
const Node* n = take_named(t);
if (!n) return;
std::string p = fpath(t);
if (!n->is_complex()) {
issue(Issue::Error, p, n, "expected framed array, found " + std::string(kind_name(n->kind)) + " '" +
n->name + "'");
return;
}
ReadArchive sub(n->children, *issues_, p);
v.clear();
if (sub.done()) return;
int32_t count = coerce_int(*sub.take(), p);
size_t remaining = sub.remaining();
if (count < 0 || size_t(count) > remaining) {
issue(Issue::Error, p, n,
"framed-array count " + std::to_string(count) + " exceeds " + std::to_string(remaining) + " item(s)");
count = int32_t(std::min<size_t>(size_t(std::max(count, 0)), remaining));
}
for (int32_t i = 0; i < count; ++i) {
if (sub.done()) {
issue(Issue::Error, p, n, "framed array truncated: " + std::to_string(i) + " of " + std::to_string(count));
break;
}
v.emplace_back();
sub.read_elem(v.back(), p + "[" + std::to_string(i) + "]");
}
if (!sub.done())
issue(Issue::Warn, p, sub.peek(),
std::to_string(sub.remaining()) + " item(s) after framed array elements");
}
template <class T>
void carr_flex(Tag t, std::vector<T>& v, bool& framed) {
const Node* n = peek();
if (n && !n->is_complex()) {
framed = false;
narr(t, v, [](ReadArchive& a, T& e) { a.read_elem(e, a.path_); });
return;
}
framed = true;
carr(t, v);
}
// --- inline array: named count + n x element -------------------------------
template <class T, class F>
void narr(Tag t, std::vector<T>& v, F elem) {
const Node* cn = take_named(t);
if (!cn) return;
std::string p = fpath(t);
int32_t count = coerce_int(*cn, p);
size_t rem = remaining();
v.clear();
if (count < 0 || size_t(count) > rem) {
issue(Issue::Error, p, cn,
"array count " + std::to_string(count) + " exceeds the " + std::to_string(rem) +
" item(s) left in the frame");
count = int32_t(std::min<size_t>(size_t(std::max(count, 0)), rem));
}
for (int32_t i = 0; i < count; ++i) {
if (done()) {
issue(Issue::Error, p, nullptr, "array truncated: " + std::to_string(i) + " of " + std::to_string(count) +
" elements present");
break;
}
size_t before = i_;
v.emplace_back();
std::string save = path_;
path_ = p + "[" + std::to_string(i) + "]";
elem(*this, v.back());
path_ = save;
if (i_ == before) {
issue(Issue::Error, p, peek(), "array element " + std::to_string(i) + " consumed nothing; stopping");
v.pop_back();
break;
}
}
}
// --- conditional group -----------------------------------------------------
template <class F>
void when(bool cond, F body) {
if (cond) body(*this);
}
// --- uncounted repetition while the next tag is `lead` ---------------------
template <class T, class F>
void repeat(const char* lead, std::vector<T>& v, F elem) {
v.clear();
while (!done() && peek()->tagged && peek()->name == lead) {
size_t before = i_;
v.emplace_back();
std::string save = path_;
path_ = path_ + "/" + lead + "[" + std::to_string(v.size() - 1) + "]";
elem(*this, v.back());
path_ = save;
if (i_ == before) {
v.pop_back();
break;
}
}
}
// --- everything left in the frame, kept generic ----------------------------
void rest(std::vector<Node>& v) {
v.assign(nodes_->begin() + long(i_), nodes_->end());
i_ = nodes_->size();
absorbed_ = true;
}
// --- cursor ----------------------------------------------------------------
bool done() const { return i_ >= nodes_->size(); }
size_t remaining() const { return nodes_->size() - i_; }
const Node* peek() const { return done() ? nullptr : &(*nodes_)[i_]; }
const Node* take() { return &(*nodes_)[i_++]; }
// After io(): leftover items are unexpected unless rest() absorbed them.
void finish(const Node* frame) {
if (!done() && !absorbed_) {
issue(Issue::Warn, path_, peek(), std::to_string(remaining()) + " unexpected item(s) at end of frame");
(void)frame;
i_ = nodes_->size();
}
}
template <class T>
void read_frame(const Node& n, T& v, const std::string& p) {
if (!n.is_complex()) {
issue(Issue::Error, p, &n, "expected frame, found " + std::string(kind_name(n.kind)) + " '" + n.name + "'");
return;
}
ReadArchive sub(n.children, *issues_, p);
v.io(sub);
sub.finish(&n);
}
// one array element: struct -> "." frame, int32 -> "." int, Node -> any
template <class T>
void read_elem(T& e, const std::string& p) {
const Node* n = take();
if constexpr (std::is_same_v<T, int32_t>) e = coerce_int(*n, p);
else if constexpr (std::is_same_v<T, Node>) e = *n;
else read_frame(*n, e, p);
}
private:
const std::vector<Node>* nodes_;
size_t i_ = 0;
std::vector<Issue>* issues_;
std::string path_;
bool absorbed_ = false;
std::string fpath(Tag t) const { return path_ + "/" + t.name; }
void issue(Issue::Level l, const std::string& p, const Node* n, std::string msg) {
issues_->push_back(Issue{l, p, n ? n->offset : 0u, std::move(msg)});
}
static std::string lower(std::string s) {
for (char& c : s)
if (c >= 'A' && c <= 'Z') c = char(c - 'A' + 'a');
return s;
}
bool next_is(Tag t) const {
const Node* n = peek();
return n && n->tagged && lower(n->name) == lower(t.name);
}
const Node* take_named(Tag t) {
std::string p = fpath(t);
const Node* n = peek();
if (!n) {
issue(t.positional ? Issue::Warn : Issue::Error, p, nullptr, "missing field '" + std::string(t.name) + "' (frame ended)");
return nullptr;
}
if (!t.positional) {
if (!n->tagged || n->name != t.name) {
size_t j = i_;
for (; j < nodes_->size(); ++j)
if ((*nodes_)[j].tagged && (*nodes_)[j].name == t.name) break;
if (j == nodes_->size()) {
issue(Issue::Error, p, n,
"expected '" + std::string(t.name) + "', found '" + (n->tagged ? n->name : "<tagless>") +
"'; field missing");
return nullptr;
}
std::string names;
for (size_t k = i_; k < j && k < i_ + 6; ++k) names += (k > i_ ? ", '" : "'") + (*nodes_)[k].name + "'";
issue(Issue::Warn, p, n,
std::to_string(j - i_) + " unexpected item(s) before '" + t.name + "': " + names);
i_ = j;
}
} else if (n->tagged && lower(n->name) != lower(t.name)) {
issue(Issue::Info, p, n, "tag '" + n->name + "' read positionally as '" + t.name + "'");
}
return take();
}
// --- coercions (mirror the reference reader's rules) ----------------------
int32_t coerce_int(const Node& n, const std::string& p) {
switch (n.kind) {
case Kind::Int: return n.as_int();
case Kind::Float: return n.raw.size() == 4 ? n.as_int() : fail_int(n, p, "int");
case Kind::Bool: return n.as_bool() ? 1 : 0;
case Kind::Int64:
issue(Issue::Warn, p, &n, "int expected, int64 read");
return int32_t(n.as_int64());
default: return fail_int(n, p, "int");
}
}
float coerce_float(const Node& n, const std::string& p) {
switch (n.kind) {
case Kind::Float: return n.as_float();
case Kind::Int: return n.raw.size() == 4 ? n.as_float() : float(fail_int(n, p, "float"));
case Kind::Bool: return n.as_bool() ? 1.f : 0.f;
default: return float(fail_int(n, p, "float"));
}
}
bool coerce_bool(const Node& n, const std::string& p) {
if (n.kind == Kind::Bool) return n.as_bool();
if ((n.kind == Kind::Int || n.kind == Kind::Float) && n.raw.size() == 4) {
if (n.raw[1] || n.raw[2] || n.raw[3] || n.raw[0] > 1)
issue(Issue::Warn, p, &n, "bool expected, word " + hex(n.raw.data(), 4) + " read");
return n.raw[0] != 0;
}
return fail_int(n, p, "bool") != 0;
}
int64_t coerce_int64(const Node& n, const std::string& p) {
if (n.kind == Kind::Int64) return n.as_int64();
if ((n.kind == Kind::Int || n.kind == Kind::Float) && n.raw.size() == 4) {
issue(Issue::Warn, p, &n, "int64 expected, 4-byte word read (width mismatch)");
return n.as_int();
}
return fail_int(n, p, "int64");
}
std::string coerce_string(const Node& n, const std::string& p) {
if (n.kind == Kind::String) return n.as_string();
// an empty string is "len 0" = 4 zero bytes, byte-identical to int 0
if ((n.kind == Kind::Int || n.kind == Kind::Float) && n.raw.size() == 4 && n.as_int() == 0) return "";
fail_int(n, p, "string");
return "";
}
Vec3 coerce_vec3(const Node& n, const std::string& p) {
Vec3 v;
if (n.is_complex()) {
const auto& ch = n.children;
if (ch.size() == 1 && ch[0].kind == Kind::Raw && ch[0].raw.size() == 12) {
v.x = rd_f32(ch[0].raw.data());
v.y = rd_f32(ch[0].raw.data() + 4);
v.z = rd_f32(ch[0].raw.data() + 8);
return v;
}
if (ch.size() == 3 && !ch[0].is_complex() && !ch[1].is_complex() && !ch[2].is_complex() &&
ch[0].raw.size() == 4 && ch[1].raw.size() == 4 && ch[2].raw.size() == 4) {
v.x = ch[0].as_float();
v.y = ch[1].as_float();
v.z = ch[2].as_float();
return v;
}
issue(Issue::Error, p, &n, "vec3 frame has unexpected body (" + std::to_string(ch.size()) + " items)");
return v;
}
if (n.kind == Kind::Raw && n.raw.size() == 12) {
v.x = rd_f32(n.raw.data());
v.y = rd_f32(n.raw.data() + 4);
v.z = rd_f32(n.raw.data() + 8);
return v;
}
issue(Issue::Error, p, &n, "expected vec3, found " + std::string(kind_name(n.kind)));
return v;
}
int32_t fail_int(const Node& n, const std::string& p, const char* want) {
issue(Issue::Error, p, &n,
std::string("expected ") + want + ", read " + (n.is_complex() ? "frame '" + n.name + "'" : kind_name(n.kind)));
return n.raw.size() >= 4 ? n.as_int() : 0;
}
};
// ---------------------------------------------------------------------------
// WriteArchive
// ---------------------------------------------------------------------------
class WriteArchive {
public:
static constexpr bool reading = false, writing = true, building = false;
explicit WriteArchive(Writer& w) : w_(w) {}
void i32(Tag t, int32_t& v) { w_.int32(t.disk, v); }
void f32(Tag t, float& v) { w_.float32(t.disk, v); }
void b(Tag t, bool& v) { w_.boolean(t.disk, v); }
void i64(Tag t, int64_t& v) { w_.int64(t.disk, v); }
void str(Tag t, std::string& v) { w_.string(t.disk, v); }
void vec3(Tag t, Vec3& v) {
w_.begin(t.disk);
w_.float32(".", v.x);
w_.float32(".", v.y);
w_.float32(".", v.z);
w_.end();
}
void any(Tag, Node& v) { w_.node(v); }
void raw_frame(Tag t, Node& v) { any(t, v); }
void opt_i32(Tag t, std::optional<int32_t>& v) {
if (v) w_.int32(t.disk, *v);
}
void opt_f32(Tag t, std::optional<float>& v) {
if (v) w_.float32(t.disk, *v);
}
void opt_b(Tag t, std::optional<bool>& v) {
if (v) w_.boolean(t.disk, *v);
}
void opt_any(Tag, std::optional<Node>& v) {
if (v) w_.node(*v);
}
template <class T>
void opt_obj(Tag t, std::optional<T>& v) {
if (v) obj(t, *v);
}
template <class T>
void obj(Tag t, T& v) {
w_.begin(t.disk);
v.io(*this);
w_.end();
}
template <class T>
void obj_flex(Tag t, T& v, bool& framed) {
if (framed) obj(t, v);
else v.io(*this);
}
template <class T>
void carr(Tag t, std::vector<T>& v) {
w_.begin(t.disk);
w_.int32(".", int32_t(v.size()));
for (T& e : v) write_elem(e);
w_.end();
}
template <class T>
void carr_flex(Tag t, std::vector<T>& v, bool& framed) {
if (framed) {
carr(t, v);
return;
}
w_.int32(t.disk, int32_t(v.size()));
for (T& e : v) write_elem(e);
}
template <class T, class F>
void narr(Tag t, std::vector<T>& v, F elem) {
w_.int32(t.disk, int32_t(v.size()));
for (T& e : v) elem(*this, e);
}
template <class F>
void when(bool cond, F body) {
if (cond) body(*this);
}
template <class T, class F>
void repeat(const char*, std::vector<T>& v, F elem) {
for (T& e : v) elem(*this, e);
}
void rest(std::vector<Node>& v) {
for (const Node& n : v) w_.node(n);
}
template <class T>
void write_elem(T& e) {
if constexpr (std::is_same_v<T, int32_t>) w_.int32(".", e);
else if constexpr (std::is_same_v<T, Node>) w_.node(e);
else {
w_.begin(".");
e.io(*this);
w_.end();
}
}
private:
Writer& w_;
};
// ---------------------------------------------------------------------------
// SchemaBuilder — runs io() on default-constructed shapes to record hints.
// ---------------------------------------------------------------------------
class SchemaBuilder {
public:
static constexpr bool reading = false, writing = false, building = true;
struct Context {
Registry& reg;
std::map<std::string, std::set<Prim>> strong; // A/R fields: name -> kinds seen
std::map<std::string, Prim> weak; // Opt fields: lowest priority
std::map<std::type_index, const Desc*> memo;
std::set<std::type_index> in_progress;
const Desc* raw_desc = nullptr;
explicit Context(Registry& r) : reg(r) {}
};
SchemaBuilder(Context& ctx, Desc* cur) : ctx_(ctx), cur_(cur) {}
void i32(Tag t, int32_t&) { prim(t, Prim::Int); }
void f32(Tag t, float&) { prim(t, Prim::Float); }
void b(Tag t, bool&) { prim(t, Prim::Bool); }
void i64(Tag t, int64_t&) { prim(t, Prim::Int64); }
void str(Tag t, std::string&) { prim(t, Prim::String); }
void vec3(Tag t, Vec3&) { nohint(t); }
void any(Tag t, Node&) { nohint(t); }
void raw_frame(Tag t, Node&) {
nohint(t);
if (!ctx_.raw_desc) {
Desc d;
d.type = Desc::Raw;
d.name = t.name;
ctx_.raw_desc = ctx_.reg.add(std::move(d));
}
ctx_.reg.shapes.emplace(t.name, ctx_.raw_desc);
}
void opt_i32(Tag t, std::optional<int32_t>&) { prim(t, Prim::Int, true); }
void opt_f32(Tag t, std::optional<float>&) { prim(t, Prim::Float, true); }
void opt_b(Tag t, std::optional<bool>&) { prim(t, Prim::Bool, true); }
void opt_any(Tag t, std::optional<Node>&) {
close_prefix();
nohint(t);
}
template <class T>
void opt_obj(Tag t, std::optional<T>&) {
close_prefix();
const Desc* d = describe<T>();
by_name(t, Hint{Prim::None, d});
register_shape(t, d);
}
template <class T>
void obj(Tag t, T&) {
const Desc* d = describe<T>();
push_prefix(Hint{Prim::None, d});
by_name(t, Hint{Prim::None, d});
register_shape(t, d);
}
template <class T>
void obj_flex(Tag t, T& v, bool&) {
close_prefix();
obj(t, v);
}
template <class T>
void carr(Tag t, std::vector<T>&) {
Desc d;
d.type = Desc::CArr;
if constexpr (std::is_same_v<T, int32_t>) d.elem.kind = Prim::Int;
else if constexpr (std::is_same_v<T, Node>) d.elem = Hint{};
else d.elem.sub = describe<T>();
const Desc* cd = ctx_.reg.add(std::move(d));
push_prefix(Hint{Prim::None, cd});
// the NULL-name tag "." is never a frame hint key: it carries ints,
// floats and frames alike (bare "." elements inside inline arrays)
if (std::string(t.name) != ".") {
by_name(t, Hint{Prim::None, cd});
if (*t.name) ctx_.reg.shapes.emplace(t.name, cd);
}
}
template <class T>
void carr_flex(Tag t, std::vector<T>& v, bool&) {
close_prefix();
carr(t, v);
}
template <class T, class F>
void narr(Tag t, std::vector<T>&, F elem) {
push_prefix(Hint{Prim::Int, nullptr});
close_prefix();
by_name(t, Hint{});
T tmp{};
elem(*this, tmp); // element fields register by name (prefix already closed)
}
template <class F>
void when(bool, F body) {
close_prefix();
body(*this);
}
template <class T, class F>
void repeat(const char*, std::vector<T>&, F elem) {
close_prefix();
T tmp{};
elem(*this, tmp);
}
void rest(std::vector<Node>&) { close_prefix(); }
// Describe shape T (memoized per type) and register its named frame.
template <class T>
const Desc* describe() {
std::type_index ti(typeid(T));
auto it = ctx_.memo.find(ti);
if (it != ctx_.memo.end()) return it->second;
if (ctx_.in_progress.count(ti)) return nullptr; // recursive shape: no positional hints
ctx_.in_progress.insert(ti);
Desc d;
d.type = Desc::Shape;
d.name = T::kStreamName;
Desc* nd = ctx_.reg.add(std::move(d));
SchemaBuilder sub(ctx_, nd);
T tmp{};
tmp.io(sub);
ctx_.in_progress.erase(ti);
ctx_.memo.emplace(ti, nd);
if (!nd->name.empty()) ctx_.reg.shapes.emplace(nd->name, nd);
return nd;
}
// Build the global catalog after every shape has been visited.
static void finalize(Context& ctx, const std::map<std::string, Prim>& manual_kinds) {
std::set<std::string> conflicts;
for (auto& [name, kinds] : ctx.strong) {
if (kinds.size() == 1) ctx.reg.kinds.emplace(name, *kinds.begin());
else conflicts.insert(name);
}
for (auto& [name, k] : ctx.weak)
if (!conflicts.count(name)) ctx.reg.kinds.emplace(name, k);
ctx.reg.kinds.erase("."); // the NULL-name tag carries ints, floats and frames alike
for (auto& [name, k] : manual_kinds) ctx.reg.kinds[name] = k;
}
private:
Context& ctx_;
Desc* cur_;
bool prefix_open_ = true;
static std::string lower(std::string s) {
for (char& c : s)
if (c >= 'A' && c <= 'Z') c = char(c - 'A' + 'a');
return s;
}
void push_prefix(Hint h) {
if (cur_ && prefix_open_) cur_->prefix.push_back(h);
}
void close_prefix() { prefix_open_ = false; }
void by_name(Tag t, Hint h) {
if (!cur_) return;
cur_->by_name.emplace(t.name, h);
cur_->by_name.emplace(lower(t.name), h);
}
void prim(Tag t, Prim k, bool opt = false) {
if (opt) close_prefix();
else push_prefix(Hint{k, nullptr});
by_name(t, Hint{k, nullptr});
if (opt) ctx_.weak.emplace(t.name, k);
else ctx_.strong[t.name].insert(k);
}
void nohint(Tag t) {
push_prefix(Hint{});
by_name(t, Hint{});
}
void register_shape(Tag t, const Desc* d) {
if (!d) return;
if (d->name.empty() && *t.name) ctx_.reg.shapes.emplace(t.name, d);
}
};
} // namespace mars::stream