From b9c7431f4f4d3f47e4c434228581e4a91d1f809e Mon Sep 17 00:00:00 2001 From: Joe Rivera Date: Tue, 29 Sep 2026 01:33:27 -0500 Subject: [PATCH] state_exec: store/read the data-channel `bytes` value as a raw std::string (coherent with ?data) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-node data() channel (a boost::any) had two non-interoperating conventions: state-data-get/-set stored a value_t, while the ?data URI channel (uri_state.cpp) and the planned §13.9 HTTP cache store a raw std::string. A byte blob written by one was invisible-as-bytes to the other -- a std::string parked by ?data / the cache came back from state-data-get as an opaque data_object, and a `bytes` value set from the DSL (stored as a value_t) was not byte-serializable via state://...?data. Make the raw std::string THE on-node representation of the DSL `bytes` type (types.h: `bytes` is opaque octets backed by std::string, the sanctioned home for HTTP octet-stream bodies), so one byte blob is coherent across the resolver and state_exec: - state-data-get: a raw std::string payload returns make_bytes(...) (the `bytes` track) instead of an opaque data_object. A value_t still round-trips via the value_t path; a foreign C++ type still comes back as a data_object. - state-data-set: a `bytes` value is stored as boost::any(std::string) -- the same representation ?data reads -- instead of boost::any(value_t). Every other value_t (text strings, lists, dicts, scalars) is stored as-is and round-trips unchanged. The text `string` track and the `bytes` track stay distinct. Nothing else any_casts value_t off data() (only state-data-get), and data() replication (set_data) is a documented no-op, so the on-node representation change is safe. Precursor to the §13.9 HTTP cache: the cache parks response bodies as byte blobs on data(), and this makes those bodies show up as `bytes` in state_exec and serve byte-exact via state://...?data. Tests (state_exec_intrinsics_test): a raw std::string blob reads as bytes; a DSL bytes value lands as a raw std::string (readable via ?data) and round-trips byte-exact through an embedded NUL; the string and bytes tracks stay distinct. Full intrinsics (154) + integration (85) suites green. --- src/cvc/core/state_exec/intrinsics.cpp | 18 +++++++- src/cvc/tests/state_exec_intrinsics_test.cpp | 46 ++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/cvc/core/state_exec/intrinsics.cpp b/src/cvc/core/state_exec/intrinsics.cpp index fd09ca15..4f860926 100644 --- a/src/cvc/core/state_exec/intrinsics.cpp +++ b/src/cvc/core/state_exec/intrinsics.cpp @@ -238,6 +238,13 @@ value_t intrinsic_state_data_get(intrinsics_context *ctx, std::span(&d)) return deep_copy(*v); + // A RAW std::string blob on the data channel reads back as the DSL `bytes` type (opaque octets), + // NOT an opaque data_object. That raw representation is what state-data-set stores for a `bytes` + // value, what the `?data` URI channel writes (uri_state.cpp), and what the §13.9 HTTP cache parks + // on a node — so a byte blob stays coherent across the resolver and state_exec (types.h: `bytes` + // is opaque octets backed by std::string, the sanctioned home for HTTP octet-stream bodies). + if (const std::string *s = boost::any_cast(&d)) + return make_bytes(*s); auto obj = std::make_shared(); obj->payload = d; obj->type_name = d.type().name(); @@ -258,8 +265,15 @@ value_t intrinsic_state_data_set(intrinsics_context *ctx, std::span(args[1].v)) throw std::runtime_error( "state-data-set: cannot store a callable (function/closure/generator) as data"); - // Store the value_t as boost::any - (*ctx->root)(path).data(boost::any(args[1])); + // A `bytes` value (opaque octets) is stored as a RAW std::string on the data channel — the SAME + // representation the `?data` URI channel (uri_state.cpp) and the §13.9 HTTP cache use — so a byte + // blob written from the DSL is readable via state://…?data and a blob parked by the cache reads + // back as `bytes` here (state-data-get, above). Every other value_t is stored as-is, so + // structured data (lists/dicts/scalars/text strings) round-trips via the value_t path. + if (const bytes_value *b = std::get_if(&args[1].v)) + (*ctx->root)(path).data(boost::any(b->data)); + else + (*ctx->root)(path).data(boost::any(args[1])); return nil_value; } diff --git a/src/cvc/tests/state_exec_intrinsics_test.cpp b/src/cvc/tests/state_exec_intrinsics_test.cpp index 2ed87f09..af878e03 100644 --- a/src/cvc/tests/state_exec_intrinsics_test.cpp +++ b/src/cvc/tests/state_exec_intrinsics_test.cpp @@ -1,6 +1,7 @@ /// @file state_exec_intrinsics_test.cpp /// @brief Tests for Phase 5: Intrinsics, Resource Policy, Stdlib. +#include #include #include #include @@ -253,6 +254,51 @@ TEST_F(StateTreeIntrinsicsTest, StateDataGetNonexistent) { EXPECT_TRUE(result.is_nil()); } +TEST_F(StateTreeIntrinsicsTest, StateDataGetReadsRawStringBlobAsBytes) { + // A RAW std::string parked on the data channel — what the ?data URI channel (uri_state.cpp) and + // the §13.9 HTTP cache write — reads back as the DSL `bytes` type, NOT an opaque data_object. + // This is the coherence the cache depends on: a byte blob is the same on both sides. + cvc::state &root = cvc::state::instance(app_ctx); + root("blob").data(boost::any(std::string("raw-octets"))); + auto got = call("state-data-get", {std::string("blob")}); + ASSERT_TRUE(std::holds_alternative(got.v)); + EXPECT_EQ(std::get(got.v).data, "raw-octets"); +} + +TEST_F(StateTreeIntrinsicsTest, StateDataSetBytesStoredAsRawStringForDataChannel) { + // A `bytes` value set from the DSL is stored as a RAW std::string on the data channel (the ?data + // / cache representation), so it round-trips as `bytes` AND is byte-serializable via + // state://…?data. + const std::string octets("A\0B\0C", 5); // embedded NULs prove byte transparency + call("state-data-set", {std::string("b"), make_bytes(octets)}); + + auto got = call("state-data-get", {std::string("b")}); // round-trips as bytes, byte-exact + ASSERT_TRUE(std::holds_alternative(got.v)); + EXPECT_EQ(std::get(got.v).data, octets); + + // the on-node payload is a raw std::string — exactly what uri_state.cpp's ?data any_cast reads + cvc::state *n = cvc::state::instance(app_ctx).findDescendant("b"); + ASSERT_NE(n, nullptr); + const boost::any d = n->data(); + const std::string *raw = boost::any_cast(&d); + ASSERT_NE(raw, nullptr) << "a bytes value must land as a raw std::string for ?data compatibility"; + EXPECT_EQ(*raw, octets); +} + +TEST_F(StateTreeIntrinsicsTest, StateDataStringAndBytesTracksAreDistinct) { + // The text `string` track and the `bytes` track stay distinct through state-data-set/get: a text + // string comes back a string (stored as a value_t), a bytes value comes back bytes. + call("state-data-set", {std::string("t"), value_t(std::string("text"))}); + auto t = call("state-data-get", {std::string("t")}); + ASSERT_TRUE(std::holds_alternative(t.v)); // string track (unchanged) + EXPECT_EQ(std::get(t.v), "text"); + + call("state-data-set", {std::string("y"), make_bytes(std::string("bin"))}); + auto y = call("state-data-get", {std::string("y")}); + ASSERT_TRUE(std::holds_alternative(y.v)); // bytes track + EXPECT_EQ(std::get(y.v).data, "bin"); +} + TEST_F(StateTreeIntrinsicsTest, StateGetWrongArgCount) { EXPECT_THROW(call("state-get", {}), std::runtime_error); EXPECT_THROW(call("state-get", {std::string("a"), std::string("b")}), std::runtime_error);