diff --git a/README.md b/README.md index fa3cf1a..a1aa493 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ computed_property(depends_on<&Device::min_speed, &Device::max_speed 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 with each other. The computed property's read slot is derived from its dependency graph and must not be configured directly in synchronization rules. +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. Dependency cycles are rejected when the Schema is formed. `depends_on<>` is a valid explicit zero-dependency declaration and produces an unsynchronized computed read. ## Managed access and raw access @@ -212,9 +212,9 @@ The default synchronization topology is resolved once per type. Instances do not `No_Lock_Policy` removes real mutex storage entirely. -## Unified Attribute model +## Unified property metadata model -Core and extensions use one Attribute protocol. +A property descriptor has one metadata store containing Attributes and Constraints. Core and extensions use one Attribute protocol for descriptive metadata, while Constraints keep their validation protocol. ```cpp struct Label_Category {}; @@ -236,7 +236,7 @@ field<&Device::temperature>( ) ``` -Core stores extension metadata but does not interpret extension-owned categories. +Core stores extension metadata but does not interpret extension-owned categories. `for_each_metadata()` traverses all property metadata, `for_each_attribute()` traverses only Attributes, and `for_each_constraint()` traverses only Constraints. ## Validation is explicit diff --git a/README.zh-CN.md b/README.zh-CN.md index dbf85f1..42f133d 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -196,7 +196,9 @@ Computed value 本身没有可写存储。它的 synchronized view 保护的是* - dependency 是 Schema 的显式结构事实,computed view 只能读取已声明的直接依赖; - read-only 存储依赖可以直接读取,不需要锁; - writable 依赖如果需要同一快照,只需要彼此处于同一个 synchronization group,computed property 的读取 slot 会从 dependency 自动推导; -- computed property 不再直接加入 synchronization rule,它的同步语义由 dependency graph 决定。 +- computed property 不再直接加入 synchronization rule,它的同步语义由 dependency graph 决定; +- dependency graph 必须是 DAG,Schema 形成时会在编译期拒绝 cycle; +- `depends_on<>` 是合法的显式零依赖声明,此类 computed read 固定为 unsynchronized。 ## Managed Access 与 Raw Access @@ -219,9 +221,9 @@ Managed path 使用 Schema 描述的 intrinsic capability 和 synchronization。 `No_Lock_Policy` 完全不保存真实 mutex。 -## 统一 Attribute 模型 +## 统一 Property Metadata 模型 -Core 和 Extension 使用同一套 Attribute 协议: +Property Descriptor 只有一份 metadata storage,其中可以同时保存 Attribute 和 Constraint。Core 与 Extension 的描述性 metadata 共用 Attribute 协议,Constraint 保持自己的 validation 协议: ```cpp struct Label_Category {}; @@ -234,7 +236,7 @@ struct Label_Attribute { }; ``` -Extension metadata 可以直接挂在 Property 上: +Extension metadata 可以直接挂在 Property 上。`for_each_metadata()` 遍历全部 metadata,`for_each_attribute()` 只遍历 Attribute,`for_each_constraint()` 只遍历 Constraint: ```cpp field<&Device::temperature>( diff --git a/core/include/structive/property/accessor.hpp b/core/include/structive/property/accessor.hpp index 04e5b1c..e333307 100644 --- a/core/include/structive/property/accessor.hpp +++ b/core/include/structive/property/accessor.hpp @@ -28,20 +28,6 @@ inline constexpr Property_Dependencies...> d 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 @@ -159,6 +145,8 @@ concept Property_Accessor = requires { typename Accessor::object_type; typename Accessor::value_type; typename Accessor::storage_identity; + typename Accessor::dependency_spec; + requires Property_Dependencies_Type; { Accessor::readable } -> std::convertible_to; { Accessor::writable } -> std::convertible_to; { Accessor::synchronized_view_read } -> std::convertible_to; diff --git a/core/include/structive/property/attributes.hpp b/core/include/structive/property/attributes.hpp index bcbe853..752fe04 100644 --- a/core/include/structive/property/attributes.hpp +++ b/core/include/structive/property/attributes.hpp @@ -106,10 +106,18 @@ constexpr auto constraint(Function&& function) { return Custom_Constraint>{std::forward(function)}; } template +concept Property_Attribute = requires { + typename Attribute::attribute_category; + { Attribute::single_valued } -> std::convertible_to; + { Attribute::inheritable } -> std::convertible_to; +}; +template concept Property_Constraint = requires { typename Attribute::property_constraint_tag; { Attribute::code.view() } -> std::convertible_to; }; +template +concept Property_Metadata = Property_Attribute || Property_Constraint; template concept Property_Constraint_For = Property_Constraint && requires(const Attribute& attribute, const Value& value) { { attribute.validate(value) } -> std::convertible_to; diff --git a/core/include/structive/property/descriptor.hpp b/core/include/structive/property/descriptor.hpp index 85d3c11..bf59242 100644 --- a/core/include/structive/property/descriptor.hpp +++ b/core/include/structive/property/descriptor.hpp @@ -8,25 +8,25 @@ #include #include namespace structive { -template +template consteval bool constraints_compatible() { return (([] { - if constexpr (Property_Constraint) { - return Property_Constraint_For; + if constexpr (Property_Constraint) { + return Property_Constraint_For; } return true; }()) && ...); } -template +template struct Property_Descriptor { using property_descriptor_tag = void; using accessor_type = Accessor; 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; + using dependency_spec = typename Accessor::dependency_spec; + using metadata_types = Type_List; + using capability_type = find_attribute_in_list_t; static constexpr Property_Capability intrinsic_capability = [] { if constexpr (!std::same_as) { return capability_type::value; @@ -44,48 +44,59 @@ 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((Property_Metadata && ...), "property metadata must be an Attribute or Constraint"); + 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"); static_assert(!((intrinsic_capability == Property_Capability::write || intrinsic_capability == Property_Capability::read_write) && !Accessor::writable), "property capability requests write from a non-writable accessor"); - using key_type = find_attribute_in_list_t; + using key_type = find_attribute_in_list_t; static_assert(!std::same_as); static_assert(key_type::value.view().size() > 0); - static_assert(constraints_compatible()); + static_assert(constraints_compatible()); [[no_unique_address]] Accessor accessor{}; - std::tuple attributes; + std::tuple metadata; template - using attribute_type = find_attribute_in_list_t; + using attribute_type = find_attribute_in_list_t; template - static constexpr std::size_t attribute_count = count_attribute_category(); + static constexpr std::size_t attribute_count = count_attribute_category(); template static constexpr bool has_attribute = attribute_count != 0; template constexpr decltype(auto) attribute() const requires (attribute_count == 1) { using target = attribute_type; - return std::get(attributes); + return std::get(metadata); } constexpr std::string_view key() const noexcept { return key_type::value.view(); } template - constexpr void for_each_attribute(Function&& function) const { + constexpr void for_each_metadata(Function&& function) const { std::apply([&](const auto&... values) { (function(values), ...); - }, attributes); + }, metadata); + } + template + constexpr void for_each_attribute(Function&& function) const { + std::apply([&](const auto&... values) { + ([&] { + using type = std::remove_cvref_t; + if constexpr (Property_Attribute) { + function(values); + } + }(), ...); + }, metadata); } template constexpr void for_each_attribute(Function&& function) const { std::apply([&](const auto&... values) { ([&] { using type = std::remove_cvref_t; - if constexpr (std::same_as, Category>) { + if constexpr (Property_Attribute && std::same_as, Category>) { function(values); } }(), ...); - }, attributes); + }, metadata); } template constexpr void for_each_constraint(Function&& function) const { @@ -96,7 +107,7 @@ struct Property_Descriptor { function(values); } }(), ...); - }, attributes); + }, metadata); } }; template @@ -107,29 +118,28 @@ concept Property_Descriptor_Type = requires { typename Type::accessor_type; typename Type::storage_identity; }; -template -constexpr auto property(Attributes&&... attributes) { +template +constexpr auto property(Metadata&&... metadata) { using accessor = Member_Accessor; - return Property_Descriptor...>{{}, {std::forward(attributes)...}}; + return Property_Descriptor...>{{}, {std::forward(metadata)...}}; } -template -constexpr auto field(Attributes&&... attributes) { - return property(std::forward(attributes)...); +template +constexpr auto field(Metadata&&... metadata) { + return property(std::forward(metadata)...); } -template -constexpr auto computed_property(Dependencies, Function&& function, Attributes&&... attributes) { - static_assert(Dependencies::count > 0, "computed property must declare at least one dependency"); +template +constexpr auto computed_property(Dependencies, Function&& function, Metadata&&... metadata) { using accessor = Synchronized_Computed_Accessor, Dependencies>; - return Property_Descriptor...>{accessor{std::forward(function)}, {std::forward(attributes)...}}; + return Property_Descriptor...>{accessor{std::forward(function)}, {std::forward(metadata)...}}; } -template requires Trusted_Getter_Function -constexpr auto trusted_computed_property(Attributes&&... attributes) { +template requires Trusted_Getter_Function +constexpr auto trusted_computed_property(Metadata&&... metadata) { using accessor = Trusted_Computed_Accessor; - return Property_Descriptor...>{{}, {std::forward(attributes)...}}; + return Property_Descriptor...>{{}, {std::forward(metadata)...}}; } -template requires Trusted_Getter_Function && Trusted_Setter_Function_For, trusted_getter_value_t> -constexpr auto trusted_accessor_property(Attributes&&... attributes) { +template requires Trusted_Getter_Function && Trusted_Setter_Function_For, trusted_getter_value_t> +constexpr auto trusted_accessor_property(Metadata&&... metadata) { using accessor = Trusted_Getter_Setter_Accessor; - return Property_Descriptor...>{{}, {std::forward(attributes)...}}; + return Property_Descriptor...>{{}, {std::forward(metadata)...}}; } } diff --git a/core/include/structive/property/meta.hpp b/core/include/structive/property/meta.hpp index b2688a8..d9de569 100644 --- a/core/include/structive/property/meta.hpp +++ b/core/include/structive/property/meta.hpp @@ -55,7 +55,7 @@ struct Find_Attribute_By_Category { }; template struct Find_Attribute_By_Category { - using type = std::conditional_t>, Head, typename Find_Attribute_By_Category::type>; + using type = std::conditional_t, void> && std::same_as>, Head, typename Find_Attribute_By_Category::type>; }; template struct Find_Attribute_In_List; @@ -67,7 +67,7 @@ template using find_attribute_in_list_t = typename Find_Attribute_In_List::type; template consteval std::size_t count_attribute_category() { - return (std::size_t{0} + ... + (std::same_as, Category> ? 1u : 0u)); + return (std::size_t{0} + ... + (!std::same_as, void> && std::same_as, Category> ? 1u : 0u)); } template consteval bool unique_single_value_category_for() { diff --git a/core/include/structive/property/property_object.hpp b/core/include/structive/property/property_object.hpp index 723e8ac..2f33e0a 100644 --- a/core/include/structive/property/property_object.hpp +++ b/core/include/structive/property/property_object.hpp @@ -338,15 +338,8 @@ private: 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"); - } - } + static_assert(schema_property_depends_on_index_v, "computed property reads an undeclared dependency"); if constexpr (Property::accessor_type::synchronized_view_read) { Single_Read_View nested{*owner_, lock_slot_}; return owner_->template read_unlocked(nested); diff --git a/core/include/structive/property/schema.hpp b/core/include/structive/property/schema.hpp index 06f6e0f..da2ab4b 100644 --- a/core/include/structive/property/schema.hpp +++ b/core/include/structive/property/schema.hpp @@ -238,13 +238,7 @@ struct Schema_Dependency_Indices> 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; + return Schema_Dependency_Indices::value; } template inline constexpr std::size_t schema_property_dependency_count_v = schema_property_dependency_indices().size(); @@ -271,6 +265,37 @@ consteval auto schema_property_dependency_matrix() { }(std::make_index_sequence{}); return matrix; } +template +consteval bool schema_dependency_graph_acyclic() { + constexpr auto dependencies = schema_property_dependency_matrix(); + std::array dependency_counts{}; + std::array ready{}; + std::size_t ready_begin = 0; + std::size_t ready_end = 0; + for (std::size_t property = 0; property < Schema::property_count; ++property) { + for (std::size_t dependency = 0; dependency < Schema::property_count; ++dependency) { + dependency_counts[property] += dependencies[property][dependency] ? 1u : 0u; + } + if (dependency_counts[property] == 0) { + ready[ready_end++] = property; + } + } + std::size_t resolved_count = 0; + while (ready_begin != ready_end) { + auto resolved = ready[ready_begin++]; + ++resolved_count; + for (std::size_t property = 0; property < Schema::property_count; ++property) { + if (!dependencies[property][resolved]) { + continue; + } + --dependency_counts[property]; + if (dependency_counts[property] == 0) { + ready[ready_end++] = property; + } + } + } + return resolved_count == Schema::property_count; +} template struct Effective_Attribute { private: @@ -301,11 +326,11 @@ constexpr decltype(auto) declared_effective_attribute(const Schema& schema) requ } } template -concept Valid_Property_Schema = Property_Schema; +concept Valid_Property_Schema = Property_Schema && schema_dependency_graph_acyclic(); 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; +inline constexpr bool property_has_derived_synchronization_v = Schema::template property_type::synchronized_view_read && !Schema::template property_type::writable; 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 @@ -331,7 +356,6 @@ Resolved_Synchronization_Plan resolve_synchronization_pl 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; @@ -407,18 +431,12 @@ Resolved_Synchronization_Plan resolve_synchronization_pl } } if constexpr (Schema::property_count > 0) { - std::array dependency_state{}; + constexpr auto keys = schema_property_keys(); + std::array dependency_resolved{}; auto resolve_derived_slot = [&](auto&& self, std::size_t index) -> std::size_t { - if (!derived_synchronization[index]) { + if (!derived_synchronization[index] || dependency_resolved[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) { @@ -434,7 +452,7 @@ Resolved_Synchronization_Plan resolve_synchronization_pl } } resolved.lock_slots[index] = has_dependency_domain ? dependency_slot : unsynchronized_slot; - dependency_state[index] = 2; + dependency_resolved[index] = true; return resolved.lock_slots[index]; }; for (std::size_t index = 0; index < Schema::property_count; ++index) { @@ -512,10 +530,13 @@ bool visit_schema_property(const Schema& schema, std::string_view key_value, Fun template auto make_object_schema(Defaults&& object_defaults, Source&& synchronization_source, Properties&&... properties) requires Synchronization_Source_Type> { using schema_type = Object_Schema, std::decay_t...>; - static_assert(Valid_Property_Schema); + constexpr bool valid_schema = Valid_Property_Schema; + static_assert(valid_schema, "property dependency graph must be valid and acyclic"); auto plan = materialize_synchronization_plan(std::forward(synchronization_source)); schema_type schema{std::forward(object_defaults), std::move(plan), std::tuple...>{std::forward(properties)...}}; - static_cast(resolve_synchronization_plan(schema, schema.synchronization_plan())); + if constexpr (valid_schema) { + static_cast(resolve_synchronization_plan(schema, schema.synchronization_plan())); + } return schema; } template diff --git a/core/main.cmake b/core/main.cmake index 6503289..ba38ad6 100644 --- a/core/main.cmake +++ b/core/main.cmake @@ -80,4 +80,7 @@ if(STRUCTIVE_BUILD_TESTS) 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") + structive_expect_compile_failure(dependency_cycle "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/dependency_cycle.cpp") + structive_expect_compile_failure(missing_accessor_dependency_spec "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/missing_accessor_dependency_spec.cpp") + structive_expect_compile_failure(invalid_property_metadata "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/invalid_property_metadata.cpp") endif() diff --git a/core/tests/compile_fail/dependency_cycle.cpp b/core/tests/compile_fail/dependency_cycle.cpp new file mode 100644 index 0000000..7b0260e --- /dev/null +++ b/core/tests/compile_fail/dependency_cycle.cpp @@ -0,0 +1,14 @@ +#include +using namespace structive; +struct Device {}; +int main() { + auto schema = object( + computed_property(depends_on_keys<"right">, [](const auto& view) { + return view.template get<"right">(); + }, key<"left">), + computed_property(depends_on_keys<"left">, [](const auto& view) { + return view.template get<"left">(); + }, key<"right">) + ); + static_cast(schema); +} diff --git a/core/tests/compile_fail/invalid_property_metadata.cpp b/core/tests/compile_fail/invalid_property_metadata.cpp new file mode 100644 index 0000000..26e93dd --- /dev/null +++ b/core/tests/compile_fail/invalid_property_metadata.cpp @@ -0,0 +1,9 @@ +#include +struct Device { + int value{}; +}; +struct Invalid_Metadata {}; +int main() { + auto schema = structive::object(structive::field<&Device::value>(structive::key<"value">, Invalid_Metadata{})); + static_cast(schema); +} diff --git a/core/tests/compile_fail/missing_accessor_dependency_spec.cpp b/core/tests/compile_fail/missing_accessor_dependency_spec.cpp new file mode 100644 index 0000000..053c1fa --- /dev/null +++ b/core/tests/compile_fail/missing_accessor_dependency_spec.cpp @@ -0,0 +1,20 @@ +#include +struct Device { + int value{}; +}; +struct Accessor { + using object_type = Device; + using value_type = int; + using storage_identity = void; + static constexpr bool readable = true; + static constexpr bool writable = false; + static constexpr bool synchronized_view_read = false; + static constexpr bool trusted_object_access = false; + int read(const Device& device) const { + return device.value; + } +}; +int main() { + structive::Property_Descriptor> descriptor; + static_cast(descriptor); +} diff --git a/core/tests/property_core_test.cpp b/core/tests/property_core_test.cpp index 832ccbf..3938bfd 100644 --- a/core/tests/property_core_test.cpp +++ b/core/tests/property_core_test.cpp @@ -69,7 +69,10 @@ struct structive::Type_Descriptor { }, 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">) + }, key<"fixed_twice">), + computed_property(depends_on<>, [](const auto&) { + return 42; + }, key<"constant">) ); } }; @@ -211,6 +214,21 @@ int main() { multi_sum += std::remove_cvref_t::value; }); REQUIRE(multi_sum == 8); + std::size_t metadata_count = 0; + std::size_t attribute_count = 0; + std::size_t constraint_count = 0; + schema.template property<&Device::temperature>().for_each_metadata([&](const auto&) { + ++metadata_count; + }); + schema.template property<&Device::temperature>().for_each_attribute([&](const auto&) { + ++attribute_count; + }); + schema.template property<&Device::temperature>().for_each_constraint([&](const auto&) { + ++constraint_count; + }); + REQUIRE(metadata_count == 7); + REQUIRE(attribute_count == 5); + REQUIRE(constraint_count == 2); const auto& empty_schema = type_descriptor(); static_assert(type_descriptor_schema_t::property_count == 0); REQUIRE(empty_schema.synchronization_plan().property_rules().empty()); @@ -284,13 +302,18 @@ int main() { 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; + constexpr auto constant_index = schema_property_index_v; + static_assert(schema_dependency_graph_acyclic()); + static_assert(schema_property_dependency_count_v == 0); 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.resolved_synchronization().slot(constant_index) == Resolved_Synchronization_View::unsynchronized_slot); REQUIRE(computed.read<"speed_span">() == 95); REQUIRE(computed.read<"double_span">() == 190); REQUIRE(computed.read<"fixed_twice">() == 10); + REQUIRE(computed.read<"constant">() == 42); computed.write<&Computed_Device::min_speed>(20); REQUIRE(computed.read<"speed_span">() == 85); REQUIRE(computed.read<"double_span">() == 170); diff --git a/docs/CORE_GUIDE.md b/docs/CORE_GUIDE.md index 59c8dcb..6b29d24 100644 --- a/docs/CORE_GUIDE.md +++ b/docs/CORE_GUIDE.md @@ -100,8 +100,11 @@ static_assert(Property::writable); static_assert(Property::runtime_copy_writable); using Value = Property::value_type; using Accessor = Property::accessor_type; +using Dependencies = Property::dependency_spec; ``` +Every custom `Property_Accessor` must declare `using dependency_spec = ...`. Use `No_Property_Dependencies` when it has no dependencies; there is no implicit fallback. + A `read_only` field reports: ```cpp @@ -513,7 +516,7 @@ computed_property(depends_on<&Device::min_speed, &Device::max_speed }, key<"speed_span">) ``` -`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. +`depends_on<...>` makes dependencies explicit Schema facts. `depends_on_keys<"a", "b">` is available when key-based declaration is more appropriate. `depends_on<>` is a valid explicit zero-dependency declaration. 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: @@ -526,7 +529,9 @@ synchronization( 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. +`schema_property_dependency_indices()` exposes the direct dependency indices for schema consumers. `schema_dependency_graph_acyclic()` exposes the DAG invariant; `object_schema(...)` rejects cyclic dependency graphs at compile time. + +A descriptor stores Attributes and Constraints in one metadata tuple. `for_each_metadata()` traverses both kinds, `for_each_attribute()` traverses only Attributes, and `for_each_constraint()` traverses only Constraints. ## 21. Trusted accessor properties diff --git a/docs/CORE_GUIDE.zh-CN.md b/docs/CORE_GUIDE.zh-CN.md index dd3c369..213fe9d 100644 --- a/docs/CORE_GUIDE.zh-CN.md +++ b/docs/CORE_GUIDE.zh-CN.md @@ -98,8 +98,11 @@ static_assert(Property::writable); static_assert(Property::runtime_copy_writable); using Value = Property::value_type; using Accessor = Property::accessor_type; +using Dependencies = Property::dependency_spec; ``` +所有自定义 `Property_Accessor` 都必须显式声明 `using dependency_spec = ...`。无依赖时使用 `No_Property_Dependencies`,不再存在隐式 fallback。 + `read_only` 字段: ```cpp @@ -505,7 +508,7 @@ computed_property(depends_on<&Device::min_speed, &Device::max_speed }, key<"speed_span">) ``` -`depends_on<...>` 把 dependency 变成 Schema 的显式结构事实。也可以通过 `depends_on_keys<"a", "b">` 按 key 声明。Computed view 只能读取已声明的直接 dependency。 +`depends_on<...>` 把 dependency 变成 Schema 的显式结构事实。也可以通过 `depends_on_keys<"a", "b">` 按 key 声明。`depends_on<>` 是合法的显式零依赖声明。Computed view 只能读取已声明的直接 dependency。 如果 writable dependency 需要同一快照,只需要让这些 writable dependency 共享 synchronization domain: @@ -518,7 +521,9 @@ synchronization( 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。 +Schema 可通过 `schema_property_dependency_indices()` 读取某个 Property 的直接 dependency index;`schema_dependency_graph_acyclic()` 暴露 DAG invariant,`object_schema(...)` 会在编译期拒绝 dependency cycle。 + +Descriptor 使用一份 metadata tuple 同时保存 Attribute 与 Constraint。`for_each_metadata()` 遍历两者,`for_each_attribute()` 只遍历 Attribute,`for_each_constraint()` 只遍历 Constraint。 ## 21. Trusted Accessor Property diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 14925a5..d63a68b 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -46,6 +46,7 @@ Structive has two architectural layers. - intrinsic readable/writable capability; - Attributes; - Constraints; +- dependency graph; - default synchronization description. This information belongs to the type. @@ -213,7 +214,7 @@ 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. -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. +Dependencies must be explicit Schema facts. Every Property Accessor declares a `dependency_spec`; no-dependency accessors use `No_Property_Dependencies` explicitly. 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. The dependency graph is a DAG invariant and cycles are rejected at compile time when the Schema is formed. 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. @@ -221,17 +222,17 @@ Writable dependencies that require one atomic snapshot must share one synchroniz Do not give a read-only stored field a mutex. A computed read uses synchronization only for mutable dependencies that require consistency. -## 12. One Attribute protocol +## 12. One property metadata store -Core and extensions share one Attribute mechanism. +A Property Descriptor has one metadata store. Its entries are either Attributes or Constraints. -An Attribute owns a category and may define whether it is single-valued and inheritable. +Core and extensions share one Attribute mechanism for descriptive metadata. An Attribute owns a category and declares whether it is single-valued and inheritable. Constraints use the validation protocol but live in the same descriptor metadata store. -Core must not create parallel metadata systems for UI, serialization, diagnostics or domain-specific features. +Core must not create parallel metadata storage systems for UI, serialization, diagnostics or domain-specific features. ### Rule -New metadata domains should extend the Attribute protocol instead of adding a second metadata framework. +New descriptive metadata domains should extend the Attribute protocol; new validation rules should extend the Constraint protocol. Both remain entries in the same Property metadata store. ## 13. Category ownership must be clear diff --git a/docs/DESIGN.zh-CN.md b/docs/DESIGN.zh-CN.md index 8d9546a..819af97 100644 --- a/docs/DESIGN.zh-CN.md +++ b/docs/DESIGN.zh-CN.md @@ -46,6 +46,7 @@ Structive 有两层架构。 - intrinsic readable/writable; - Attribute; - Constraint; +- dependency graph; - 默认 synchronization 描述。 这些属于类型。 @@ -211,7 +212,7 @@ Computed Property 自身可以只读,但它可能依赖可写字段。 Computed value 本身没有可写存储,但读取它时可能需要 mutable dependency 的一致快照。 -Dependency 必须显式进入 Schema。`Synchronized_Computed_Accessor` 的 read view 只能读取声明过的直接 dependency,computed property 的读取 slot 从 dependency graph 推导,而不是由调用方再次把 computed key 写进 synchronization group。 +Dependency 必须显式进入 Schema。每个 Property Accessor 都必须声明 `dependency_spec`,无依赖 Accessor 显式使用 `No_Property_Dependencies`。`Synchronized_Computed_Accessor` 的 read view 只能读取声明过的直接 dependency,computed property 的读取 slot 从 dependency graph 推导,而不是由调用方再次把 computed key 写进 synchronization group。Dependency graph 是 DAG invariant,Schema 形成时在编译期拒绝 cycle。 需要原子一致性的 writable dependency 应彼此处于同一个同步域。Read-only stored dependency 没有 managed writer,所以可以直接读取而无需锁。 @@ -219,17 +220,17 @@ Dependency 必须显式进入 Schema。`Synchronized_Computed_Accessor` 的 read 不要给 read-only stored field 创建 mutex。Computed read 的同步只服务于需要一致性的 mutable dependency。 -## 12. 只有一套 Attribute 协议 +## 12. 只有一份 Property Metadata Storage -Core 与 Extension 共用同一套 Attribute mechanism。 +Property Descriptor 只有一份 metadata storage,其中的条目只能是 Attribute 或 Constraint。 -Attribute 拥有 category,并可以声明 single-valued 与 inheritable 语义。 +Core 与 Extension 的描述性 metadata 共用 Attribute mechanism。Attribute 拥有 category,并声明 single-valued 与 inheritable 语义;Constraint 使用 validation protocol,但与 Attribute 保存在同一份 descriptor metadata storage 中。 -UI、serialization、诊断等新领域不应该另起第二套 metadata system。 +UI、serialization、诊断等新领域不应该另起第二套 metadata storage。 ### 原则 -新的 metadata domain 应扩展 Attribute protocol,而不是创造另一套框架。 +新的描述性 metadata 扩展 Attribute protocol,新的校验规则扩展 Constraint protocol;两者都作为同一份 Property metadata 的条目存在。 ## 13. Category 必须有明确所有者