nichlink/registry_core/release/release.rs
1//! Compact registration topology retained by release applications.
2//! 正式应用保留的紧凑注册拓扑。
3
4use crate::{NodeId, RegistrationInfo, RegistrationRule};
5
6/// One built-in face after registration checks have passed.
7/// 通过注册检查后的一个内置注册面。
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub struct StaticFace {
10 id: NodeId,
11 parent: NodeId,
12 owns_registry: bool,
13}
14
15impl StaticFace {
16 /// Create a face record from its already-checked identity, parent, and
17 /// registry ownership; nothing is validated here.
18 /// 用已校验的身份、父级与注册表归属创建注册面记录;此处不做校验。
19 pub const fn new(id: NodeId, parent: NodeId, owns_registry: bool) -> Self {
20 Self {
21 id,
22 parent,
23 owns_registry,
24 }
25 }
26
27 /// The face's compile-time identity.
28 /// 该注册面的编译期身份。
29 pub const fn id(self) -> NodeId {
30 self.id
31 }
32
33 /// The face this one registers under.
34 /// 本注册面所挂载的父面。
35 pub const fn parent(self) -> NodeId {
36 self.parent
37 }
38
39 /// Whether this face provides a registry its children may register into.
40 /// 该注册面是否提供可供子级注册的注册表。
41 pub const fn owns_registry(self) -> bool {
42 self.owns_registry
43 }
44}
45
46/// A zero-allocation view of the built-in registration tree.
47/// 内置注册树的零分配视图。
48#[derive(Clone, Copy, Debug)]
49pub struct StaticPlan {
50 faces: &'static [StaticFace],
51 grafts: &'static [StaticGraftCut],
52}
53
54/// How a release-time graft selector addresses a face.
55/// 发布态 graft 选择器如何寻址一个注册面。
56///
57/// `Path` is the human-written single-node selector (`root/control/button`)
58/// parsed from the host entry; a range keeps its far endpoint in
59/// [`StaticGraftCut`]'s separate `cut_end` field, so a path never encodes a
60/// range. `Id` is a compile-time node identity, so a host can write the target
61/// as a Rust path (`crate::control::object::button::NODE_ID`) and let the
62/// compiler and editor resolve it instead of spelling a string.
63/// `Path` 是从宿主入口解析出的手写单节点选择器(`root/control/button`);区间的
64/// 远端端点保存在 [`StaticGraftCut`] 独立的 `cut_end` 字段里,因此路径永远不编码
65/// 区间。`Id` 是编译期节点身份,宿主因此可以把目标写成 Rust 路径
66/// (`crate::control::object::button::NODE_ID`),由编译器和编辑器解析,而不必
67/// 手写字符串。
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum CutTarget {
70 /// A human-written single-node path selector from the host entry.
71 /// 来自宿主入口的手写单节点路径选择器。
72 Path(&'static str),
73 /// A compile-time node identity, so tools can resolve the target as a path.
74 /// 编译期节点身份,工具因此可把目标解析为 Rust 路径。
75 Id(NodeId),
76}
77
78impl CutTarget {
79 /// The compile-time identity this selector addresses, when it is an `Id`.
80 /// 该选择器寻址的编译期身份——当它是 `Id` 时。
81 ///
82 /// Kept by B3a: `examples/control-button/tests/registry.rs` asserts the
83 /// generated static plan carries typed cuts through this accessor, so it is
84 /// not zero-caller even though no in-workspace library code uses it.
85 /// B3a 保留:`examples/control-button/tests/registry.rs` 通过该访问器断言生成的
86 /// 静态计划携带类型化切口;因此尽管工作区内没有库代码使用它,它也不是零调用者。
87 pub const fn id(self) -> Option<NodeId> {
88 match self {
89 Self::Id(id) => Some(id),
90 Self::Path(_) => None,
91 }
92 }
93
94 /// Render the selector for diagnostics; an identity prints as its hex form.
95 /// 渲染选择器用于诊断;身份打印为十六进制形式。
96 pub fn describe(self) -> String {
97 match self {
98 Self::Path(path) => path.to_owned(),
99 Self::Id(id) => id.to_string(),
100 }
101 }
102}
103
104/// One host-declared external graft cut retained in release metadata.
105/// 正式构建保留的一条宿主外部 graft 切口元数据。
106///
107/// The table stores selectors only; it never pulls implementation code into
108/// the binary. The host resolves these selectors against an external Registry
109/// when it chooses to enable an overlay.
110/// 表中只保存选择器,不会把实现代码拉进二进制。宿主启用覆盖层时,再将选择器
111/// 解析到外部 Registry。
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub struct StaticGraftCut {
114 cut: CutTarget,
115 cut_end: Option<CutTarget>,
116 graft: CutTarget,
117 full: bool,
118}
119
120impl StaticGraftCut {
121 /// A single logical path replaced by a named external implementation.
122 /// 用命名外部实现替换的单个逻辑路径。
123 pub const fn new(cut: &'static str, graft: &'static str, full: bool) -> Self {
124 Self {
125 cut: CutTarget::Path(cut),
126 cut_end: None,
127 graft: CutTarget::Path(graft),
128 full,
129 }
130 }
131
132 /// A contiguous sibling range replaced by one external implementation.
133 /// 用同一个外部实现替换的一段连续兄弟。
134 pub const fn new_range(
135 start: &'static str,
136 end: &'static str,
137 graft: &'static str,
138 full: bool,
139 ) -> Self {
140 Self {
141 cut: CutTarget::Path(start),
142 cut_end: Some(CutTarget::Path(end)),
143 graft: CutTarget::Path(graft),
144 full,
145 }
146 }
147
148 /// A single slot addressed by compile-time identity on both sides.
149 /// 两侧都用编译期身份寻址的单个槽位。
150 pub const fn from_ids(cut: NodeId, graft: NodeId, full: bool) -> Self {
151 Self {
152 cut: CutTarget::Id(cut),
153 cut_end: None,
154 graft: CutTarget::Id(graft),
155 full,
156 }
157 }
158
159 /// A contiguous sibling range addressed by compile-time identity.
160 /// 用编译期身份寻址的一段连续兄弟。
161 pub const fn from_id_range(start: NodeId, end: NodeId, graft: NodeId, full: bool) -> Self {
162 Self {
163 cut: CutTarget::Id(start),
164 cut_end: Some(CutTarget::Id(end)),
165 graft: CutTarget::Id(graft),
166 full,
167 }
168 }
169
170 /// The selector for the slot this graft replaces; for a range, the first
171 /// sibling.
172 /// 该 graft 所替换槽位的选择器;区间时为起始兄弟。
173 pub const fn cut(self) -> CutTarget {
174 self.cut
175 }
176
177 /// The last sibling of a range, or `None` when the cut is a single slot.
178 /// 区间的最后一个兄弟;切口为单个槽位时为 `None`。
179 pub const fn cut_end(self) -> Option<CutTarget> {
180 self.cut_end
181 }
182
183 /// The selector for the external implementation that replaces the cut.
184 /// 替换该切口的外部实现选择器。
185 pub const fn graft(self) -> CutTarget {
186 self.graft
187 }
188
189 /// Whether the whole subtree rooted at the cut is replaced.
190 /// 是否替换切口根节点下的整棵子树。
191 pub const fn full(self) -> bool {
192 self.full
193 }
194}
195
196impl StaticPlan {
197 /// Build a release plan whose graft selectors are stored in read-only data.
198 /// 构造把 graft selector 直接保存在只读数据中的发布计划。
199 pub const fn with_grafts(
200 faces: &'static [StaticFace],
201 grafts: &'static [StaticGraftCut],
202 ) -> Self {
203 Self { faces, grafts }
204 }
205
206 /// The built-in faces in registry-tree order, so a parent precedes its
207 /// children.
208 /// 按注册树顺序排列的内置注册面,父级先于子级。
209 pub const fn faces(self) -> &'static [StaticFace] {
210 self.faces
211 }
212
213 /// Host-declared grafts captured by the build step without allocation.
214 /// 构建阶段捕获、无需分配即可读取的宿主 graft 声明。
215 pub const fn grafts(self) -> &'static [StaticGraftCut] {
216 self.grafts
217 }
218
219 /// How many faces the plan carries.
220 /// 该计划携带的注册面数量。
221 pub const fn len(self) -> usize {
222 self.faces.len()
223 }
224
225 /// Whether the plan carries no faces.
226 /// 该计划是否不携带任何注册面。
227 pub const fn is_empty(self) -> bool {
228 self.faces.is_empty()
229 }
230
231 /// Find a face by identity.
232 /// 按身份查找注册面。
233 ///
234 /// The generated table is emitted in registry-tree traversal order, because
235 /// `registrations()` and everything that registers from it rely on parents
236 /// coming first. It is therefore **not** sorted by identity, and a binary
237 /// search over it silently missed faces — two of the three faces in the
238 /// example plan. The scan is linear, and the table stays in the order its
239 /// other consumers need.
240 /// 生成的表按注册树遍历顺序发射,因为 `registrations()` 以及所有据此注册的代码都
241 /// 依赖父级先出现。它因此**不是**按身份排序的,对它做二分会让注册面被静默漏掉——
242 /// 示例计划里三个面漏了两个。这里改为线性扫描,表则保持其他消费方需要的顺序。
243 pub fn find(self, id: NodeId) -> Option<&'static StaticFace> {
244 self.faces.iter().find(|face| face.id == id)
245 }
246
247 /// The faces whose parent is `parent`, in table order.
248 /// 父级为 `parent` 的注册面,按表顺序。
249 ///
250 /// The table is emitted in traversal order, not identity order, so this is
251 /// a linear filter rather than a range lookup.
252 /// 该表按遍历顺序而非身份顺序发射,因此这里用线性过滤而不是区间查找。
253 pub fn children_of(self, parent: NodeId) -> impl Iterator<Item = &'static StaticFace> {
254 self.faces.iter().filter(move |face| face.parent == parent)
255 }
256}
257
258/// Evaluate mounting and construction rules during crate generation.
259/// 在生成 crate 时求值挂载规则与构造规则。
260///
261/// # Panics
262///
263/// This is the **const twin** of [`RegistrationRule::validate`]: same five
264/// checks, same order, different cost. The runtime side collects one `String` per
265/// failure and names it; this side cannot allocate or format inside const
266/// evaluation, so it stops at the first failure with a fixed message. They must be
267/// changed together — the runtime side is pinned by
268/// `parent_rule_aggregates_every_missing_structural_requirement`, and this side by
269/// every registry-owning face in the workspace compiling (or not) under the rule
270/// its own declaration carries. Folding the two into one function is not possible
271/// while this one is `const`: the shared checker returns a `Vec<String>`.
272/// 这是 [`RegistrationRule::validate`] 的 **const 孪生**:同样五项检查、同样顺序、不同代价。
273/// 运行期一侧为每个失败收集一个 `String` 并点名它;本侧在常量求值里无法分配或格式化,因此停
274/// 在第一个失败上并给出固定消息。两者必须一起改——运行期一侧由
275/// `parent_rule_aggregates_every_missing_structural_requirement` 钉住,本侧则由工作区里每个
276/// 拥有注册机的注册面按它自己声明所带的规则能否编译来钉住。在本函数仍是 `const` 期间把两者
277/// 合成一个是不可能的:共享的那个校验器返回 `Vec<String>`。
278///
279/// # Panics
280///
281/// Panics when `info` does not satisfy `rule`: a wrong preset, a missing
282/// structural part, export, handle interface, or parts interface, or a preset
283/// whose parts the face does not provide. The generated crate calls this in a
284/// `const` item, so the panic is a compile error naming the declaration source
285/// rather than a runtime failure — which is the whole point of evaluating it
286/// there.
287/// 当 `info` 不满足 `rule` 时 panic:preset 不符,缺少结构部件、导出、handle 接口或 parts
288/// 接口,或 preset 的 parts 没有被该面提供。生成的 crate 在 `const` 项里调用它,因此这次
289/// panic 是点名声明来源的编译错误,而不是运行期失败——这正是放在那里求值的目的。
290#[doc(hidden)]
291pub const fn assert_static_registration(rule: RegistrationRule, info: RegistrationInfo) {
292 if let Some(required) = rule.required_preset
293 && !str_eq(required, info.preset)
294 {
295 panic!("static registration failed: wrong preset; see declaration source");
296 }
297 if !contains_all(info.contract.provided_parts, rule.required_parts) {
298 panic!("static registration failed: missing structural part; see declaration source");
299 }
300 if !contains_all(info.exports, rule.required_exports) {
301 panic!("static registration failed: missing export; see declaration source");
302 }
303 if !contains_all(info.handle_traits, rule.required_handle_traits) {
304 panic!("static registration failed: missing handle interface; see declaration source");
305 }
306 if !contains_all(info.part_traits, rule.required_part_traits) {
307 panic!("static registration failed: missing parts interface; see declaration source");
308 }
309 // The output contract used to be compared here, as two strings the author
310 // wrote beside each other. It is checked by types now: `assert_contract`
311 // inside every face, and `assert_contract::<cut::__Preset, graft::__Parts>`
312 // for every typed graft cut. A string comparison could not catch a wrong
313 // claim; the type system cannot fail to.
314 // 输出合同过去在这里比较——比较的是作者并排写下的两个字符串。现在由类型检查:
315 // 每个面内的 `assert_contract`,以及每个类型化 graft 切口的
316 // `assert_contract::<cut::__Preset, graft::__Parts>`。字符串比较抓不到错误声明,
317 // 类型系统则不可能漏掉。
318 if !contains_all(info.contract.provided_parts, info.contract.required_parts) {
319 panic!(
320 "static registration failed: preset parts are not satisfied; see declaration source"
321 );
322 }
323}
324
325const fn contains_all(actual: &[&str], required: &[&str]) -> bool {
326 let mut index = 0;
327 while index < required.len() {
328 if !has_str(actual, required[index]) {
329 return false;
330 }
331 index += 1;
332 }
333 true
334}
335
336const fn has_str(values: &[&str], needle: &str) -> bool {
337 let mut index = 0;
338 while index < values.len() {
339 if str_eq(values[index], needle) {
340 return true;
341 }
342 index += 1;
343 }
344 false
345}
346
347const fn str_eq(left: &str, right: &str) -> bool {
348 let left = left.as_bytes();
349 let right = right.as_bytes();
350 if left.len() != right.len() {
351 return false;
352 }
353 let mut index = 0;
354 while index < left.len() {
355 if left[index] != right[index] {
356 return false;
357 }
358 index += 1;
359 }
360 true
361}
362
363#[cfg(test)]
364mod static_plan_find_tests {
365 use super::{NodeId, StaticFace, StaticPlan};
366
367 /// The emitted table follows the registry tree, so `find` must not assume
368 /// identity order.
369 /// 发射的表跟随注册树,因此 `find` 不能假定身份顺序。
370 #[test]
371 fn find_sees_every_face_of_an_unsorted_table() {
372 static FACES: &[StaticFace] = &[
373 StaticFace::new(NodeId::from_raw([9; 16]), NodeId::from_raw([0; 16]), true),
374 StaticFace::new(NodeId::from_raw([1; 16]), NodeId::from_raw([9; 16]), false),
375 StaticFace::new(NodeId::from_raw([5; 16]), NodeId::from_raw([9; 16]), false),
376 ];
377 let plan = StaticPlan::with_grafts(FACES, &[]);
378 for face in FACES {
379 assert_eq!(plan.find(face.id).map(|found| found.id), Some(face.id));
380 }
381 assert!(plan.find(NodeId::from_raw([7; 16])).is_none());
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 const FRAMEWORK: crate::FrameworkId = crate::FrameworkId::new("static-plan-test");
390 const ROOT: NodeId = crate::root_node_id("static-plan-test");
391 const CHILD: NodeId = NodeId::from_namespaced_path("static-plan-test", "child.rs", "Child");
392 static FACES: &[StaticFace] = &[StaticFace::new(CHILD, ROOT, false)];
393 static GRAFTS: &[StaticGraftCut] = &[StaticGraftCut::new("root/child", "child_fast", false)];
394
395 // The `static_graft_plan!` macro moved to nichlink-runtime; anchor the same
396 // compile-time assertions here without a kernel -> runtime dependency.
397 // `static_graft_plan!` 宏已移至 nichlink-runtime;为避免 kernel 反向依赖,
398 // 这里直接写出等价的编译期断言。
399 const _: crate::FrameworkId = FRAMEWORK;
400 const _: &str = stringify!(cut "root/child" graft "child_fast");
401
402 #[test]
403 fn graft_selectors_are_part_of_the_zero_allocation_static_plan() {
404 let plan = StaticPlan::with_grafts(FACES, GRAFTS);
405
406 assert_eq!(plan.len(), 1);
407 assert_eq!(plan.grafts(), GRAFTS);
408 }
409}