Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/cvc/core/state_exec/intrinsics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,13 @@ value_t intrinsic_state_data_get(intrinsics_context *ctx, std::span<const value_
// other C++ type parked on the node) still comes back as a data_object.
if (auto *v = boost::any_cast<value_t>(&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<std::string>(&d))
return make_bytes(*s);
auto obj = std::make_shared<data_object>();
obj->payload = d;
obj->type_name = d.type().name();
Expand All @@ -258,8 +265,15 @@ value_t intrinsic_state_data_set(intrinsics_context *ctx, std::span<const value_
std::holds_alternative<generator_ptr>(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<bytes_value>(&args[1].v))
(*ctx->root)(path).data(boost::any(b->data));
else
(*ctx->root)(path).data(boost::any(args[1]));
return nil_value;
}

Expand Down
46 changes: 46 additions & 0 deletions src/cvc/tests/state_exec_intrinsics_test.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// @file state_exec_intrinsics_test.cpp
/// @brief Tests for Phase 5: Intrinsics, Resource Policy, Stdlib.

#include <boost/any.hpp>
#include <cmath>
#include <cvc/core/app.h>
#include <cvc/core/state.h>
Expand Down Expand Up @@ -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<bytes_value>(got.v));
EXPECT_EQ(std::get<bytes_value>(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<bytes_value>(got.v));
EXPECT_EQ(std::get<bytes_value>(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<std::string>(&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<std::string>(t.v)); // string track (unchanged)
EXPECT_EQ(std::get<std::string>(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<bytes_value>(y.v)); // bytes track
EXPECT_EQ(std::get<bytes_value>(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);
Expand Down
Loading