Skip to main content

gen_platform/
emit.rs

1//! Emit substrate-side `quirk-apply.nix` skeletons from typed
2//! `TypedDispatcher` reflection. Closes the loop typed Rust enum
3//! → typed Nix dispatch arm without the operator hand-writing the
4//! mapping table.
5//!
6//! Pattern: given a `DispatcherEntry` (catalog) or any
7//! `TypedDispatcher` impl, produce the Nix `helpers = { ... }`
8//! table skeleton that the substrate's
9//! `mk-typed-dispatcher.nix` combinator consumes. The skeleton
10//! has typed stub bodies (`{}`) per variant; the operator fills in
11//! the apply functions per ecosystem.
12//!
13//! See `theory/TYPED-ABSORPTION.md` §IV.2 — this is the
14//! "mechanical absorption" step: adding a new ecosystem becomes
15//! "declare the Rust enum, emit the Nix skeleton, fill in
16//! helpers." No dispatch fold reimplementation. No Nix parser
17//! gymnastics.
18
19use crate::catalog::DispatcherEntry;
20
21/// Render a `quirk-apply.nix` skeleton for a dispatcher catalog
22/// entry. Output is a complete, syntactically-valid Nix file
23/// (verified by `nix-instantiate --eval --strict`) consuming
24/// `../shared/mk-typed-dispatcher.nix` with one helpers arm per
25/// variant kind.
26///
27/// Field bindings:
28/// - Unit variants → `_: {}` body.
29/// - Single-field variants → `<field>Apply quirk.<field>` body.
30/// - Multi-field variants → `<verb>Apply { inherit (quirk) <fields>...; }` body.
31///
32/// The class-helper function names use camelCase derived from the
33/// kebab-case kind (e.g. `pin-toolchain` → `pinToolchainApply`).
34#[must_use]
35pub fn to_helpers_skeleton(entry: &DispatcherEntry) -> String {
36    let variant_fields = (entry.variant_fields)();
37    let mut out = String::with_capacity(512);
38    out.push_str(&format!(
39        "# quirk-apply.nix — mechanically emitted skeleton for the\n"
40    ));
41    out.push_str(&format!(
42        "# `{}` dispatcher (gen_platform::DispatcherCatalog).\n",
43        entry.label
44    ));
45    out.push_str("# Each helpers arm calls a class-helper function — fill the\n");
46    out.push_str("# function bodies in the `let` block below to drive the\n");
47    out.push_str("# matching builder.\n");
48    out.push_str("#\n");
49    out.push_str(
50        "# Refresh via:\n#   gen dispatchers helpers-skeleton \\\n#     --label ",
51    );
52    out.push_str(entry.label);
53    out.push_str(" --format nix\n");
54    out.push_str("{ lib }:\nlet\n");
55
56    // One class-helper stub per variant. Body is `_: {}` (no-op);
57    // the operator replaces with the real apply logic.
58    for (kind, fields) in &variant_fields {
59        let helper_name = format!("{}Apply", kebab_to_camel(kind));
60        if fields.is_empty() {
61            out.push_str(&format!("  # Unit variant — no payload.\n"));
62            out.push_str(&format!("  {helper_name} = _: _: {{}};\n\n"));
63        } else if fields.len() == 1 {
64            let field = &fields[0];
65            out.push_str(&format!(
66                "  # Single-field variant — receives the field's value.\n"
67            ));
68            out.push_str(&format!("  {helper_name} = {field}: _: {{}};\n\n"));
69        } else {
70            let inherit_str = fields.join(" ");
71            out.push_str(&format!(
72                "  # Multi-field variant — destructure the typed payload.\n"
73            ));
74            out.push_str(&format!(
75                "  {helper_name} = {{ {} }}: _: {{}};\n\n",
76                inherit_str.replace(' ', ", ")
77            ));
78        }
79    }
80
81    out.push_str("in\nimport ../shared/mk-typed-dispatcher.nix {\n");
82    out.push_str("  inherit lib;\n");
83    out.push_str("  helpers = {\n");
84
85    for (kind, fields) in &variant_fields {
86        let helper_name = format!("{}Apply", kebab_to_camel(kind));
87        if fields.is_empty() {
88            out.push_str(&format!("    \"{kind}\" = quirk: {helper_name} quirk;\n"));
89        } else if fields.len() == 1 {
90            let field = &fields[0];
91            out.push_str(&format!(
92                "    \"{kind}\" = quirk: {helper_name} quirk.{field};\n"
93            ));
94        } else {
95            let inherit_str = fields.join(" ");
96            out.push_str(&format!(
97                "    \"{kind}\" = quirk: {helper_name} {{\n"
98            ));
99            out.push_str(&format!(
100                "      inherit (quirk) {inherit_str};\n"
101            ));
102            out.push_str("    };\n");
103        }
104    }
105
106    out.push_str("  };\n}\n");
107    out
108}
109
110/// kebab-case → camelCase. `pin-toolchain` → `pinToolchain`.
111fn kebab_to_camel(s: &str) -> String {
112    let mut out = String::with_capacity(s.len());
113    let mut upper_next = false;
114    for ch in s.chars() {
115        if ch == '-' {
116            upper_next = true;
117        } else if upper_next {
118            for u in ch.to_uppercase() {
119                out.push(u);
120            }
121            upper_next = false;
122        } else {
123            out.push(ch);
124        }
125    }
126    out
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn kebab_to_camel_handles_simple_cases() {
135        assert_eq!(kebab_to_camel("force-cfg"), "forceCfg");
136        assert_eq!(kebab_to_camel("substitute-source"), "substituteSource");
137        assert_eq!(kebab_to_camel("ldflag"), "ldflag");
138        assert_eq!(kebab_to_camel("pin-toolchain"), "pinToolchain");
139        assert_eq!(
140            kebab_to_camel("fold-normal-into-build"),
141            "foldNormalIntoBuild"
142        );
143    }
144}