diff --git a/README.md b/README.md index 417a60f..fa3cf1a 100644 --- a/README.md +++ b/README.md @@ -180,14 +180,14 @@ This relies on the Structive managed contract. If another thread deliberately wr A computed property is usually intrinsically read-only, but it may read writable dependencies. ```cpp -computed_property([](const auto& view) { +computed_property(depends_on<&Device::min_speed, &Device::max_speed>, [](const auto& view) { return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>(); }, key<"speed_span">) ``` -The computed value has no writable storage of its own. Its synchronized view protects the consistency domain of writable dependencies. Read-only stored dependencies can be read directly because they cannot change through the managed path. +The computed value has no writable storage of its own. Dependencies are explicit Schema facts, and the computed view can read only declared direct dependencies. Read-only stored dependencies can be read directly because they cannot change through the managed path. -Writable dependencies that must form one snapshot should be placed in the same synchronization group as the computed property. +Writable dependencies that must form one snapshot should be placed in the same synchronization group with each other. The computed property's read slot is derived from its dependency graph and must not be configured directly in synchronization rules. ## Managed access and raw access diff --git a/README.zh-CN.md b/README.zh-CN.md index 0a2be77..dbf85f1 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -186,16 +186,17 @@ struct structive::Type_Descriptor { Computed Property 通常自身不可写,但它可能依赖可写字段: ```cpp -computed_property([](const auto& view) { +computed_property(depends_on<&Device::min_speed, &Device::max_speed>, [](const auto& view) { return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>(); }, key<"speed_span">) ``` Computed value 本身没有可写存储。它的 synchronized view 保护的是**可写依赖的一致性域**。 +- dependency 是 Schema 的显式结构事实,computed view 只能读取已声明的直接依赖; - read-only 存储依赖可以直接读取,不需要锁; -- writable 依赖如果需要同一快照,应与 computed property 放在同一个 synchronization group; -- computed property 的同步语义不是“给只读值创建一个成员锁”,而是描述其依赖一致性边界。 +- writable 依赖如果需要同一快照,只需要彼此处于同一个 synchronization group,computed property 的读取 slot 会从 dependency 自动推导; +- computed property 不再直接加入 synchronization rule,它的同步语义由 dependency graph 决定。 ## Managed Access 与 Raw Access diff --git a/core/include/structive/property/accessor.hpp b/core/include/structive/property/accessor.hpp index c26791f..04e5b1c 100644 --- a/core/include/structive/property/accessor.hpp +++ b/core/include/structive/property/accessor.hpp @@ -1,9 +1,47 @@ #pragma once +#include "fixed_string.hpp" #include #include #include #include namespace structive { +template +struct Member_Property_Dependency { + static constexpr auto member = Member; +}; +template +struct Key_Property_Dependency { + static constexpr auto key = Key; +}; +template +struct Property_Dependencies { + using property_dependencies_tag = void; + static constexpr std::size_t count = sizeof...(Dependencies); +}; +template +concept Property_Dependencies_Type = requires { + typename Type::property_dependencies_tag; + { Type::count } -> std::convertible_to; +}; +template +inline constexpr Property_Dependencies...> depends_on{}; +template +inline constexpr Property_Dependencies...> depends_on_keys{}; +using No_Property_Dependencies = Property_Dependencies<>; +template +struct Accessor_Dependency_Model { + using type = No_Property_Dependencies; + static constexpr bool declared = false; +}; +template +struct Accessor_Dependency_Model> { + using type = typename Accessor::dependency_spec; + static constexpr bool declared = true; +}; +template +using accessor_dependency_spec_t = typename Accessor_Dependency_Model::type; +template +inline constexpr bool accessor_declares_dependencies_v = Accessor_Dependency_Model::declared; template struct Member_Pointer_Traits; template @@ -55,6 +93,7 @@ struct Member_Accessor { static constexpr bool writable = !std::is_const_v; static constexpr bool synchronized_view_read = false; static constexpr bool trusted_object_access = false; + using dependency_spec = No_Property_Dependencies; static constexpr auto member = Member; constexpr const value_type& read(const object_type& object) const { return object.*Member; @@ -67,7 +106,7 @@ struct Member_Accessor { object.*Member = std::forward(value); } }; -template +template struct Synchronized_Computed_Accessor { using object_type = Object; using value_type = Value; @@ -76,6 +115,7 @@ struct Synchronized_Computed_Accessor { static constexpr bool writable = false; static constexpr bool synchronized_view_read = true; static constexpr bool trusted_object_access = false; + using dependency_spec = Dependencies; [[no_unique_address]] Function function; template constexpr value_type read(const View& view) const requires std::invocable && std::constructible_from> { @@ -91,6 +131,7 @@ struct Trusted_Computed_Accessor { static constexpr bool writable = false; static constexpr bool synchronized_view_read = false; static constexpr bool trusted_object_access = true; + using dependency_spec = No_Property_Dependencies; constexpr decltype(auto) read(const object_type& object) const noexcept(std::is_nothrow_invocable_v) { return std::invoke(Getter, object); } @@ -104,6 +145,7 @@ struct Trusted_Getter_Setter_Accessor { static constexpr bool writable = true; static constexpr bool synchronized_view_read = false; static constexpr bool trusted_object_access = true; + using dependency_spec = No_Property_Dependencies; constexpr decltype(auto) read(const object_type& object) const noexcept(std::is_nothrow_invocable_v) { return std::invoke(Getter, object); } diff --git a/core/include/structive/property/descriptor.hpp b/core/include/structive/property/descriptor.hpp index d872148..85d3c11 100644 --- a/core/include/structive/property/descriptor.hpp +++ b/core/include/structive/property/descriptor.hpp @@ -24,6 +24,7 @@ struct Property_Descriptor { using object_type = typename Accessor::object_type; using value_type = typename Accessor::value_type; using storage_identity = typename Accessor::storage_identity; + using dependency_spec = accessor_dependency_spec_t; using attribute_types = Type_List; using capability_type = find_attribute_in_list_t; static constexpr Property_Capability intrinsic_capability = [] { @@ -43,6 +44,8 @@ struct Property_Descriptor { static constexpr bool writable = Accessor::writable && (intrinsic_capability == Property_Capability::write || intrinsic_capability == Property_Capability::read_write); static constexpr bool synchronized_view_read = Accessor::synchronized_view_read; static constexpr bool trusted_object_access = Accessor::trusted_object_access; + static constexpr bool declares_dependencies = accessor_declares_dependencies_v; + static constexpr std::size_t dependency_count = dependency_spec::count; static constexpr bool runtime_copy_writable = writable && requires(const Accessor& accessor, object_type& object, const value_type& candidate) { accessor.write(object, candidate); }; static_assert(unique_single_value_categories()); static_assert(!((intrinsic_capability == Property_Capability::read || intrinsic_capability == Property_Capability::read_write) && !Accessor::readable), "property capability requests read from a non-readable accessor"); @@ -113,9 +116,10 @@ template constexpr auto field(Attributes&&... attributes) { return property(std::forward(attributes)...); } -template -constexpr auto computed_property(Function&& function, Attributes&&... attributes) { - using accessor = Synchronized_Computed_Accessor>; +template +constexpr auto computed_property(Dependencies, Function&& function, Attributes&&... attributes) { + static_assert(Dependencies::count > 0, "computed property must declare at least one dependency"); + using accessor = Synchronized_Computed_Accessor, Dependencies>; return Property_Descriptor...>{accessor{std::forward(function)}, {std::forward(attributes)...}}; } template requires Trusted_Getter_Function diff --git a/core/include/structive/property/property_object.hpp b/core/include/structive/property/property_object.hpp index 07cb385..723e8ac 100644 --- a/core/include/structive/property/property_object.hpp +++ b/core/include/structive/property/property_object.hpp @@ -331,37 +331,42 @@ private: const auto& descriptor = type_descriptor().template property(); descriptor.accessor.write(static_cast(*this), std::forward(value)); } + template class Single_Read_View { const Property_Object* owner_{}; - std::size_t property_index_{}; std::size_t lock_slot_{}; + template + decltype(auto) get_index() const { + using Schema = type_descriptor_schema_t; + using Owner_Property = typename Schema::template property_type; + using Property = typename Schema::template property_type; + if constexpr (Owner_Property::declares_dependencies) { + static_assert(schema_property_depends_on_index_v, "computed property reads an undeclared dependency"); + } else if constexpr (Property::writable || Property::accessor_type::synchronized_view_read) { + if (owner_->slot(Index) != lock_slot_ || (lock_slot_ == Resolved_Synchronization_View::unsynchronized_slot && Index != Property_Index)) { + throw std::logic_error("Computed property reads outside its synchronization slot"); + } + } + if constexpr (Property::accessor_type::synchronized_view_read) { + Single_Read_View nested{*owner_, lock_slot_}; + return owner_->template read_unlocked(nested); + } else { + return owner_->template read_unlocked(*this); + } + } public: - Single_Read_View(const Property_Object& owner, std::size_t property_index, std::size_t lock_slot) : owner_(&owner), property_index_(property_index), lock_slot_(lock_slot) {} + Single_Read_View(const Property_Object& owner, std::size_t lock_slot) : owner_(&owner), lock_slot_(lock_slot) {} template decltype(auto) get() const requires Schema_Readable_Property_Member, Member> { using Schema = type_descriptor_schema_t; constexpr auto index = schema_member_property_index_v; - using Property = typename Schema::template property_type; - if constexpr (!Property::writable && !Property::accessor_type::synchronized_view_read) { - return owner_->template read_unlocked(*this); - } - if (owner_->slot(index) != lock_slot_ || (lock_slot_ == Resolved_Synchronization_View::unsynchronized_slot && index != property_index_)) { - throw std::logic_error("Computed property reads outside its synchronization slot"); - } - return owner_->template read_unlocked(*this); + return get_index(); } template decltype(auto) get() const requires Schema_Readable_Property_Key, Key> { using Schema = type_descriptor_schema_t; constexpr auto index = schema_property_index_v; - using Property = typename Schema::template property_type; - if constexpr (!Property::writable && !Property::accessor_type::synchronized_view_read) { - return owner_->template read_unlocked(*this); - } - if (owner_->slot(index) != lock_slot_ || (lock_slot_ == Resolved_Synchronization_View::unsynchronized_slot && index != property_index_)) { - throw std::logic_error("Computed property reads outside its synchronization slot"); - } - return owner_->template read_unlocked(*this); + return get_index(); } }; template @@ -374,7 +379,7 @@ private: return Value(read_unlocked(*this)); } auto lock_slot = slot(Index); - Single_Read_View view{*this, Index, lock_slot}; + Single_Read_View view{*this, lock_slot}; if constexpr (!uses_real_mutexes) { return Value(read_unlocked(view)); } @@ -671,7 +676,7 @@ private: } } else { auto lock_slot = slot(property_index); - Single_Read_View view{*this, property_index, lock_slot}; + Single_Read_View view{*this, lock_slot}; auto emit = [&] { if constexpr (std::is_reference_v(view))>) { auto&& value = read_unlocked(view); diff --git a/core/include/structive/property/schema.hpp b/core/include/structive/property/schema.hpp index 276e71c..06f6e0f 100644 --- a/core/include/structive/property/schema.hpp +++ b/core/include/structive/property/schema.hpp @@ -203,6 +203,74 @@ template concept Schema_Property_Member = Property_Schema && std::is_member_object_pointer_v && schema_member_property_index_v < Schema::property_count; template concept Schema_Property_Index = Property_Schema && Index < Schema::property_count; +template +struct Schema_Dependency_Index; +template +struct Schema_Dependency_Index> { + static constexpr std::size_t value = schema_member_property_index_v; + static_assert(value < Schema::property_count, "computed dependency member is not registered in this property schema"); + static_assert(Schema::template property_type::readable, "computed dependency must be readable"); +}; +template +struct Schema_Dependency_Index> { + static constexpr std::size_t value = schema_property_index_v; + static_assert(value < Schema::property_count, "computed dependency key is not registered in this property schema"); + static_assert(Schema::template property_type::readable, "computed dependency must be readable"); +}; +template +consteval bool unique_dependency_indices(const std::array& indices) { + for (std::size_t left = 0; left < Size; ++left) { + for (std::size_t right = left + 1; right < Size; ++right) { + if (indices[left] == indices[right]) { + return false; + } + } + } + return true; +} +template +struct Schema_Dependency_Indices; +template +struct Schema_Dependency_Indices> { + static constexpr std::array value{Schema_Dependency_Index::value...}; + static_assert(unique_dependency_indices(value), "computed property declares the same dependency more than once"); +}; +template +consteval auto schema_property_dependency_indices() requires Schema_Property_Index { + using dependency_spec = typename Schema::template property_type::dependency_spec; + constexpr auto indices = Schema_Dependency_Indices::value; + for (auto dependency : indices) { + if (dependency == Index) { + throw "computed property cannot depend on itself"; + } + } + return indices; +} +template +inline constexpr std::size_t schema_property_dependency_count_v = schema_property_dependency_indices().size(); +template +inline constexpr bool schema_property_depends_on_index_v = [] { + constexpr auto dependencies = schema_property_dependency_indices(); + for (auto index : dependencies) { + if (index == Dependency_Index) { + return true; + } + } + return false; +}(); +template +consteval auto schema_property_dependency_matrix() { + std::array, Schema::property_count> matrix{}; + [&](std::index_sequence) { + ([&] { + constexpr auto dependencies = schema_property_dependency_indices(); + for (auto dependency : dependencies) { + matrix[Index][dependency] = true; + } + }(), ...); + }(std::make_index_sequence{}); + return matrix; +} template struct Effective_Attribute { private: @@ -237,6 +305,10 @@ concept Valid_Property_Schema = Property_Schema; template inline constexpr bool property_requires_synchronization_v = Schema::template property_type::writable || Schema::template property_type::synchronized_view_read; template +inline constexpr bool property_has_derived_synchronization_v = Schema::template property_type::synchronized_view_read && !Schema::template property_type::writable && Schema::template property_type::declares_dependencies; +template +inline constexpr bool property_owns_synchronization_domain_v = Schema::template property_type::writable || (Schema::template property_type::synchronized_view_read && !property_has_derived_synchronization_v); +template concept Schema_Readable_Property_Index = Schema_Property_Index && Schema::template property_type::readable; template concept Schema_Writable_Property_Index = Schema_Property_Index && Schema::template property_type::writable; @@ -252,6 +324,14 @@ template Resolved_Synchronization_Plan resolve_synchronization_plan(const Schema&, const Synchronization_Plan& plan) { using resolved_type = Resolved_Synchronization_Plan; constexpr auto unsynchronized_slot = resolved_type::unsynchronized_slot; + constexpr auto owns_synchronization_domain = [](std::index_sequence) { + return std::array{property_owns_synchronization_domain_v...}; + }(std::make_index_sequence{}); + constexpr auto derived_synchronization = [](std::index_sequence) { + return std::array{property_has_derived_synchronization_v...}; + }(std::make_index_sequence{}); + constexpr auto dependencies = schema_property_dependency_matrix(); + constexpr auto keys = schema_property_keys(); std::array logical{}; std::array explicitly_configured{}; std::size_t next_token = Schema::property_count + 1; @@ -272,6 +352,9 @@ Resolved_Synchronization_Plan resolve_synchronization_pl return *index; }; auto mark = [&](std::size_t index, std::string_view key_value) { + if (derived_synchronization[index]) { + throw std::invalid_argument("Synchronization of computed property is derived from dependencies: " + std::string(key_value)); + } if (explicitly_configured[index]) { throw std::invalid_argument("Synchronization plan configures property more than once: " + std::string(key_value)); } @@ -299,11 +382,8 @@ Resolved_Synchronization_Plan resolve_synchronization_pl logical[index] = token; } } - constexpr auto requires_synchronization = [](std::index_sequence) { - return std::array{property_requires_synchronization_v...}; - }(std::make_index_sequence{}); for (std::size_t index = 0; index < Schema::property_count; ++index) { - if (!requires_synchronization[index]) { + if (!owns_synchronization_domain[index]) { logical[index] = unsynchronized_slot; } } @@ -326,6 +406,43 @@ Resolved_Synchronization_Plan resolve_synchronization_pl resolved.lock_slots[index] = it->second; } } + if constexpr (Schema::property_count > 0) { + std::array dependency_state{}; + auto resolve_derived_slot = [&](auto&& self, std::size_t index) -> std::size_t { + if (!derived_synchronization[index]) { + return resolved.lock_slots[index]; + } + if (dependency_state[index] == 2) { + return resolved.lock_slots[index]; + } + if (dependency_state[index] == 1) { + throw std::invalid_argument("Computed property dependency cycle: " + std::string(keys[index])); + } + dependency_state[index] = 1; + bool has_dependency_domain = false; + std::size_t dependency_slot = unsynchronized_slot; + for (std::size_t dependency = 0; dependency < Schema::property_count; ++dependency) { + if (!dependencies[index][dependency] || (!owns_synchronization_domain[dependency] && !derived_synchronization[dependency])) { + continue; + } + auto current_slot = derived_synchronization[dependency] ? self(self, dependency) : resolved.lock_slots[dependency]; + if (!has_dependency_domain) { + dependency_slot = current_slot; + has_dependency_domain = true; + } else if (dependency_slot != current_slot) { + throw std::invalid_argument("Computed property dependencies do not share one synchronization domain: " + std::string(keys[index])); + } + } + resolved.lock_slots[index] = has_dependency_domain ? dependency_slot : unsynchronized_slot; + dependency_state[index] = 2; + return resolved.lock_slots[index]; + }; + for (std::size_t index = 0; index < Schema::property_count; ++index) { + if (derived_synchronization[index]) { + resolve_derived_slot(resolve_derived_slot, index); + } + } + } return resolved; } template diff --git a/core/main.cmake b/core/main.cmake index 4d8798a..6503289 100644 --- a/core/main.cmake +++ b/core/main.cmake @@ -78,4 +78,6 @@ if(STRUCTIVE_BUILD_TESTS) structive_expect_compile_failure(incompatible_constraint "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/incompatible_constraint.cpp") structive_expect_compile_failure(foreign_sync_member "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/foreign_sync_member.cpp") structive_expect_compile_failure(duplicate_attribute "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/duplicate_attribute.cpp") + structive_expect_compile_failure(missing_computed_dependency "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/missing_computed_dependency.cpp") + structive_expect_compile_failure(undeclared_computed_read "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/undeclared_computed_read.cpp") endif() diff --git a/core/tests/compile_fail/missing_computed_dependency.cpp b/core/tests/compile_fail/missing_computed_dependency.cpp new file mode 100644 index 0000000..dd2b391 --- /dev/null +++ b/core/tests/compile_fail/missing_computed_dependency.cpp @@ -0,0 +1,21 @@ +#include +using namespace structive; +struct Device : Property_Object { + int value{}; + int hidden{}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + field<&Device::value>(key<"value">), + computed_property(depends_on<&Device::hidden>, [](const auto& view) { + return view.template get<&Device::hidden>(); + }, key<"computed">) + ); + } +}; +int main() { + static_cast(Type_Descriptor::get()); + return 0; +} diff --git a/core/tests/compile_fail/undeclared_computed_read.cpp b/core/tests/compile_fail/undeclared_computed_read.cpp new file mode 100644 index 0000000..5baaf1a --- /dev/null +++ b/core/tests/compile_fail/undeclared_computed_read.cpp @@ -0,0 +1,23 @@ +#include +using namespace structive; +struct Device : Property_Object { + int left{}; + int right{}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + synchronization(sync_all_shared), + field<&Device::left>(key<"left">), + field<&Device::right>(key<"right">), + computed_property(depends_on<&Device::left>, [](const auto& view) { + return view.template get<&Device::right>(); + }, key<"computed">) + ); + } +}; +int main() { + Device device; + return device.read<"computed">(); +} diff --git a/core/tests/property_core_test.cpp b/core/tests/property_core_test.cpp index 84c3a91..832ccbf 100644 --- a/core/tests/property_core_test.cpp +++ b/core/tests/property_core_test.cpp @@ -47,6 +47,8 @@ struct structive::Type_Descriptor { } }; struct Computed_Device : Property_Object { + Computed_Device() = default; + explicit Computed_Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {} int min_speed{10}; int max_speed{100}; int fixed_offset{5}; @@ -55,13 +57,19 @@ template <> struct structive::Type_Descriptor { static auto get() { return object( - synchronization(sync_all_independent, sync_group("speed", "min_speed", "max_speed", "speed_span")), + synchronization(sync_all_independent, sync_group("speed", "min_speed", "max_speed")), field<&Computed_Device::min_speed>(key<"min_speed">), field<&Computed_Device::max_speed>(key<"max_speed">), field<&Computed_Device::fixed_offset>(key<"fixed_offset">, read_only), - computed_property([](const auto& view) { + computed_property(depends_on<&Computed_Device::min_speed, &Computed_Device::max_speed, &Computed_Device::fixed_offset>, [](const auto& view) { return view.template get<"max_speed">() - view.template get<"min_speed">() + view.template get<"fixed_offset">(); - }, key<"speed_span">) + }, key<"speed_span">), + computed_property(depends_on_keys<"speed_span">, [](const auto& view) { + return view.template get<"speed_span">() * 2; + }, key<"double_span">), + computed_property(depends_on<&Computed_Device::fixed_offset>, [](const auto& view) { + return view.template get<&Computed_Device::fixed_offset>() * 2; + }, key<"fixed_twice">) ); } }; @@ -84,10 +92,10 @@ template <> struct structive::Type_Descriptor { static auto get() { return object( - synchronization(sync_all_independent, sync_group("sum", "left", "right", "sum")), + synchronization(sync_all_independent, sync_group("sum", "left", "right")), field<&Lockless_Device::left>(key<"left">), field<&Lockless_Device::right>(key<"right">), - computed_property([](const auto& view) { + computed_property(depends_on_keys<"left", "right">, [](const auto& view) { return view.template get<&Lockless_Device::left>() + view.template get<&Lockless_Device::right>(); }, key<"sum">) ); @@ -265,11 +273,34 @@ int main() { ++value_visits; }); REQUIRE(value_visits == 5); + using Computed_Schema = type_descriptor_schema_t; + constexpr auto speed_span_index = schema_property_index_v; + constexpr auto speed_span_dependencies = schema_property_dependency_indices(); + static_assert(speed_span_dependencies.size() == 3); + static_assert(speed_span_dependencies[0] == schema_member_property_index_v); + static_assert(speed_span_dependencies[1] == schema_member_property_index_v); + static_assert(speed_span_dependencies[2] == schema_member_property_index_v); Computed_Device computed; REQUIRE(computed.lock_slot<&Computed_Device::fixed_offset>() == Resolved_Synchronization_View::unsynchronized_slot); + constexpr auto double_span_index = schema_property_index_v; + constexpr auto fixed_twice_index = schema_property_index_v; + REQUIRE(computed.resolved_synchronization().slot(speed_span_index) == computed.lock_slot<&Computed_Device::min_speed>()); + REQUIRE(computed.resolved_synchronization().slot(speed_span_index) == computed.lock_slot<&Computed_Device::max_speed>()); + REQUIRE(computed.resolved_synchronization().slot(double_span_index) == computed.resolved_synchronization().slot(speed_span_index)); + REQUIRE(computed.resolved_synchronization().slot(fixed_twice_index) == Resolved_Synchronization_View::unsynchronized_slot); REQUIRE(computed.read<"speed_span">() == 95); + REQUIRE(computed.read<"double_span">() == 190); + REQUIRE(computed.read<"fixed_twice">() == 10); computed.write<&Computed_Device::min_speed>(20); REQUIRE(computed.read<"speed_span">() == 85); + REQUIRE(computed.read<"double_span">() == 170); + bool rejected_independent_dependencies = false; + try { + Computed_Device invalid{property_synchronization(synchronization(sync_all_independent))}; + } catch (const std::invalid_argument&) { + rejected_independent_dependencies = true; + } + REQUIRE(rejected_independent_dependencies); Lockless_Device lockless; REQUIRE(lockless.read<&Lockless_Device::left>() == 1); lockless.write<&Lockless_Device::right>(4); diff --git a/docs/CORE_GUIDE.md b/docs/CORE_GUIDE.md index 0a4d684..59c8dcb 100644 --- a/docs/CORE_GUIDE.md +++ b/docs/CORE_GUIDE.md @@ -508,21 +508,25 @@ This acquires all writable managed lock domains; it does not perform validation A synchronized computed property receives a read view: ```cpp -computed_property([](const auto& view) { +computed_property(depends_on<&Device::min_speed, &Device::max_speed>, [](const auto& view) { return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>(); }, key<"speed_span">) ``` -Writable dependencies that must form one snapshot should share the computed synchronization domain: +`depends_on<...>` makes dependencies explicit Schema facts. `depends_on_keys<"a", "b">` is available when key-based declaration is more appropriate. A computed view can read only its declared direct dependencies. + +Writable dependencies that must form one snapshot should share a synchronization domain with each other: ```cpp synchronization( sync_all_independent, - sync_group("speed", "min_speed", "max_speed", "speed_span") + sync_group("speed", "min_speed", "max_speed") ) ``` -A read-only stored dependency may be read through the computed view without joining a lock slot, because it has no managed writer. +Do not put the computed property itself in a synchronization rule. Its read slot is derived from the dependency graph; a schema or per-instance topology is rejected when synchronized dependencies do not resolve to one domain. Read-only stored dependencies need no lock slot. + +`schema_property_dependency_indices()` exposes the direct dependency indices for schema consumers. ## 21. Trusted accessor properties diff --git a/docs/CORE_GUIDE.zh-CN.md b/docs/CORE_GUIDE.zh-CN.md index f1acc37..dd3c369 100644 --- a/docs/CORE_GUIDE.zh-CN.md +++ b/docs/CORE_GUIDE.zh-CN.md @@ -500,21 +500,25 @@ device.with_all_writable_locked([&](auto& guard) { Synchronized computed property 接收 read view: ```cpp -computed_property([](const auto& view) { +computed_property(depends_on<&Device::min_speed, &Device::max_speed>, [](const auto& view) { return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>(); }, key<"speed_span">) ``` -如果 writable dependency 需要同一快照,应共享 computed synchronization domain: +`depends_on<...>` 把 dependency 变成 Schema 的显式结构事实。也可以通过 `depends_on_keys<"a", "b">` 按 key 声明。Computed view 只能读取已声明的直接 dependency。 + +如果 writable dependency 需要同一快照,只需要让这些 writable dependency 共享 synchronization domain: ```cpp synchronization( sync_all_independent, - sync_group("speed", "min_speed", "max_speed", "speed_span") + sync_group("speed", "min_speed", "max_speed") ) ``` -Read-only stored dependency 可以直接通过 computed view 读取,不需要加入 lock slot,因为它没有 managed writer。 +Computed Property 自身不要加入 synchronization rule。它的读取 slot 根据 dependency graph 自动推导;如果需要同步的 dependency 不在同一个 domain,Schema/实例 synchronization topology 会被拒绝。Read-only stored dependency 不需要 lock slot。 + +Schema 可通过 `schema_property_dependency_indices()` 读取某个 Property 的直接 dependency index。 ## 21. Trusted Accessor Property diff --git a/docs/DESIGN.md b/docs/DESIGN.md index c883e5a..14925a5 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -213,9 +213,9 @@ A computed property may itself be read-only while depending on writable fields. The computed property does not contain writable storage. However, its read may require a stable snapshot of mutable dependencies. -`Synchronized_Computed_Accessor` therefore uses a synchronized read view. Writable dependencies must share the computed consistency domain when the computed expression needs an atomic snapshot. +Dependencies must be explicit Schema facts. The `Synchronized_Computed_Accessor` read view can access only declared direct dependencies, and the computed property's read slot is derived from the dependency graph instead of requiring its key to be repeated in a synchronization group. -Read-only stored dependencies are safe to read directly through the synchronized view because they have no managed writer. +Writable dependencies that require one atomic snapshot must share one synchronization domain with each other. Read-only stored dependencies are safe to read directly through the synchronized view because they have no managed writer. ### Rule diff --git a/docs/DESIGN.zh-CN.md b/docs/DESIGN.zh-CN.md index d704bc5..8d9546a 100644 --- a/docs/DESIGN.zh-CN.md +++ b/docs/DESIGN.zh-CN.md @@ -211,9 +211,9 @@ Computed Property 自身可以只读,但它可能依赖可写字段。 Computed value 本身没有可写存储,但读取它时可能需要 mutable dependency 的一致快照。 -`Synchronized_Computed_Accessor` 因此使用 synchronized read view。需要原子一致性的 writable dependency 应和 computed property 处于同一个同步域。 +Dependency 必须显式进入 Schema。`Synchronized_Computed_Accessor` 的 read view 只能读取声明过的直接 dependency,computed property 的读取 slot 从 dependency graph 推导,而不是由调用方再次把 computed key 写进 synchronization group。 -Read-only stored dependency 没有 managed writer,所以可以直接读取而无需锁。 +需要原子一致性的 writable dependency 应彼此处于同一个同步域。Read-only stored dependency 没有 managed writer,所以可以直接读取而无需锁。 ### 原则