Skip to main content

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}