Skip to main content

lattice_config/
option_decl.rs

1// `linkme`'s distributed slices use `link_section` to
2// aggregate items at link time. Workspace lints `deny`
3// `unsafe_code` (CLAUDE.md), so we opt this module in --
4// the safety argument is that linkme is the standard Rust
5// solution for cross-crate aggregation, the link-section
6// usage is contained to its own machinery, and no raw
7// pointer / unchecked-cast code lives here.
8#![allow(unsafe_code)]
9
10//! Types-as-keys option declarations (M.2.0).
11//!
12//! Each option is a unique Rust type implementing [`OptionDecl`].
13//! Cross-crate uniqueness of the *type* is enforced by Rust's type
14//! system (path-qualified types are unambiguously different);
15//! cross-crate uniqueness of the *display name* is enforced at
16//! startup via [`linkme`] aggregation -- the registry walks the
17//! distributed slice [`OPTION_DECLS`] before user-facing work and
18//! panics on a duplicate.
19//!
20//! Strings appear only at boundaries (cmdline `:set`, TOML, plugin
21//! manifests, `:describe-option`). Internal access on the hot path
22//! is type-driven (`config.get_typed::<Tabstop>()`).
23//!
24//! See `docs/dev/architecture/mode-architecture.md` §6.4 for the design rationale
25//! (types-as-keys eliminating the cross-crate string-collision
26//! risk) and §6.8 for the constraint enforcement table (which
27//! rules land at compile time vs link time vs plugin-load time).
28//!
29//! ## Coexistence with the legacy `Option<T>` API
30//!
31//! M.2.0a lands the trait + macros + linkme aggregation alongside
32//! the legacy [`crate::Option`] / [`crate::ConfigRegistry::register`]
33//! imperative API. Both work. M.2.0b migrates every built-in
34//! option from the imperative API to the macro path; M.2.0c
35//! removes the public imperative API. Until then, this module is
36//! the *new* declaration surface; the legacy module is the
37//! existing one.
38
39use std::any::TypeId;
40
41use crate::option_type::OptionType;
42
43/// Compile-time declaration of one option. Each option type is a
44/// unique unit struct (typically generated by the [`crate::options!`]
45/// macro) implementing this trait.
46///
47/// Constants on the trait carry the metadata `:set` / TOML /
48/// `:describe-option` need; the type itself carries the identity
49/// (`TypeId` lookups for hot-path reads). The associated `Value`
50/// type's `OptionType` impl carries the parse / format / enumerate
51/// machinery already in this crate.
52///
53/// `'static` bound: option types are zero-sized markers and have
54/// no runtime data; the `'static` lets us key registry storage
55/// on `TypeId`.
56pub trait OptionDecl: 'static {
57    /// The runtime value type. `bool`, `i64`, `String`, plus any
58    /// domain enum that impls [`OptionType`].
59    type Value: OptionType;
60
61    /// Public display name. Boundary-facing only -- `:set <NAME>=…`,
62    /// TOML keys, completion candidates, `:describe-option`.
63    /// Internal access is type-driven. The macro derives this
64    /// from the option's identifier (with namespace prefix when
65    /// declared via `mode_options!`); manually-typed `NAME`s are
66    /// not supported for built-in options.
67    const NAME: &'static str;
68
69    /// Doc string. Shown in `:describe-option`, the `:customize`
70    /// form's per-row help, and `:apropos` results.
71    const DOC: &'static str;
72
73    /// Customize-visibility flag. `true` (the default) ⇒ option
74    /// appears in `:set` autocomplete + the `:customize` group
75    /// view. `false` ⇒ plugin / engine-internal state, hidden
76    /// from user surfaces. Equivalent of emacs's `defvar` vs
77    /// `defcustom`. (See `mode-architecture.md` §6.5.3.)
78    const CUSTOMIZABLE: bool = true;
79
80    /// Default value. Returned by a function (rather than a
81    /// `const`) so non-const-constructible types like `String`
82    /// can be defaults.
83    fn default_value() -> Self::Value;
84
85    /// `TypeId` of the declaration type itself. Hot-path key
86    /// for the registry's type-driven storage. Default impl
87    /// works for every concrete impl.
88    fn type_id() -> TypeId
89    where
90        Self: Sized,
91    {
92        TypeId::of::<Self>()
93    }
94}
95
96/// Group binding declared by [`OptionDecl`] declarations. Each
97/// option belongs to exactly one [`crate::OptionGroup`] (selected per
98/// declaration block in the macro, with optional per-option
99/// override). Stored on the metadata record so `:customize <name>`
100/// can prefix-match.
101///
102/// Trait alias rather than a `const` on `OptionDecl` because the
103/// group is set by the macro at the call site, not by the option
104/// type itself.
105pub trait HasGroup {
106    /// Group display name (`"editor"`, `"lsp"`, ...). The macro
107    /// pulls this from the `group = SomeGroup;` directive at the
108    /// declaration block's head.
109    const GROUP_NAME: &'static str;
110}
111
112// --- linkme distributed slice + metadata record ---
113
114/// Metadata captured at macro expansion for one option. The
115/// `register` function pointer is monomorphised at the call
116/// site so the registry boot path can register every option
117/// without pulling each `<T>` into a generic over the slice.
118///
119/// `register_fn` returns `()` and inserts the option into the
120/// [`crate::ConfigRegistry`] it is handed (via
121/// [`crate::ConfigRegistry::register_with_typeid`]);
122/// [`crate::ConfigRegistry::init_from_linkme`] calls it once per
123/// element of [`OPTION_DECLS`].
124///
125/// Also the data the browse surfaces read without a registry:
126/// `:customize` groups by [`Self::group_name`] and hides entries
127/// whose [`Self::customizable`] is `false`.
128pub struct OptionDeclMetadata {
129    /// The option's [`OptionDecl::NAME`] — its `:set` / TOML key.
130    pub name: &'static str,
131    /// The option's [`OptionDecl::DOC`].
132    pub doc: &'static str,
133    /// The option's [`OptionDecl::CUSTOMIZABLE`]; `false` hides it
134    /// from `:customize`.
135    pub customizable: bool,
136    /// The owning group's display name ([`HasGroup::GROUP_NAME`]),
137    /// which `:customize <group>` matches against.
138    pub group_name: &'static str,
139    /// `&'static str` produced by the value type's
140    /// [`OptionType::type_label`] -- captured as a function
141    /// pointer so the slice element can be a `static` (the
142    /// trait method isn't `const`, so calling it directly in
143    /// a static initializer fails). Resolved at the registry
144    /// boot loop; cheap (one function call per option, once).
145    pub type_label: fn() -> &'static str,
146    /// Format the type's default value for diagnostic surfaces
147    /// (`:describe-option` "default: X"). Captured as a function
148    /// pointer so the slice element stays object-safe-shaped.
149    pub default_formatted: fn() -> String,
150    /// `TypeId::of::<Self>()` for the declaration type.
151    /// Same as `<T as OptionDecl>::type_id()` at the macro
152    /// call site; pre-computed so the boot loop doesn't pay
153    /// the indirection.
154    pub type_id: fn() -> TypeId,
155    /// Self-registration thunk. Called by the registry's
156    /// `init_from_linkme` boot loop with the registry as
157    /// argument; the thunk constructs the runtime
158    /// [`crate::option::Option<T>`] spec via `Self::build_spec()` and
159    /// calls `registry.register_with_typeid`. The `register_fn`
160    /// is generated by the [`crate::options!`] macro and
161    /// captures the option's type identity at compile time.
162    /// Boot panics (with the option's name in the message) if
163    /// any registration fails -- duplicate names at startup
164    /// are a build-config bug.
165    pub register_fn: fn(&crate::ConfigRegistry),
166}
167
168impl OptionDeclMetadata {
169    /// Construct a metadata record for the given declaration
170    /// type. Helper used by the [`crate::options!`] macro's
171    /// generated code; not intended for hand-call.
172    pub const fn for_decl<D: OptionDecl + HasGroup>(
173        type_label: fn() -> &'static str,
174        default_formatted: fn() -> String,
175        register_fn: fn(&crate::ConfigRegistry),
176    ) -> Self {
177        Self {
178            name: D::NAME,
179            doc: D::DOC,
180            customizable: D::CUSTOMIZABLE,
181            group_name: D::GROUP_NAME,
182            type_label,
183            default_formatted,
184            type_id: TypeId::of::<D>,
185            register_fn,
186        }
187    }
188}
189
190/// Distributed slice of every option declared anywhere in the
191/// workspace. Each `options!` macro invocation submits a
192/// `&'static OptionDeclMetadata` element here; the registry's
193/// startup loader walks the slice to register each option and
194/// validate cross-crate display-name uniqueness.
195#[linkme::distributed_slice]
196pub static OPTION_DECLS: [&'static OptionDeclMetadata];
197
198#[cfg(test)]
199mod tests {
200    #![allow(clippy::unwrap_used, clippy::panic, clippy::assertions_on_constants)]
201    use super::*;
202
203    // Test fixture: declare an option manually (without the
204    // macro) so we can exercise the trait surface in isolation.
205    // M.2.0a's macro generates equivalent code; M.2.0b migrates
206    // production sites.
207    struct TestTabstop;
208    impl OptionDecl for TestTabstop {
209        type Value = i64;
210        const NAME: &'static str = "test-tabstop";
211        const DOC: &'static str = "Test option for OptionDecl trait.";
212        fn default_value() -> i64 {
213            8
214        }
215    }
216    impl HasGroup for TestTabstop {
217        const GROUP_NAME: &'static str = "editor";
218    }
219
220    #[test]
221    fn trait_constants_round_trip() {
222        assert_eq!(TestTabstop::NAME, "test-tabstop");
223        assert_eq!(TestTabstop::DOC, "Test option for OptionDecl trait.");
224        assert!(TestTabstop::CUSTOMIZABLE);
225        assert_eq!(TestTabstop::default_value(), 8);
226    }
227
228    #[test]
229    fn type_id_is_unique_per_type() {
230        struct A;
231        impl OptionDecl for A {
232            type Value = bool;
233            const NAME: &'static str = "a";
234            const DOC: &'static str = "";
235            fn default_value() -> bool {
236                false
237            }
238        }
239        impl HasGroup for A {
240            const GROUP_NAME: &'static str = "editor";
241        }
242        struct B;
243        impl OptionDecl for B {
244            type Value = bool;
245            const NAME: &'static str = "b";
246            const DOC: &'static str = "";
247            fn default_value() -> bool {
248                false
249            }
250        }
251        impl HasGroup for B {
252            const GROUP_NAME: &'static str = "editor";
253        }
254        assert_ne!(A::type_id(), B::type_id());
255        assert_eq!(A::type_id(), TypeId::of::<A>());
256    }
257
258    #[test]
259    fn metadata_for_decl_carries_constants() {
260        fn tl() -> &'static str {
261            "integer"
262        }
263        fn df() -> String {
264            "8".to_string()
265        }
266        fn rf(_r: &crate::ConfigRegistry) {}
267        let m = OptionDeclMetadata::for_decl::<TestTabstop>(tl, df, rf);
268        assert_eq!(m.name, "test-tabstop");
269        assert_eq!(m.group_name, "editor");
270        assert_eq!((m.type_label)(), "integer");
271        assert_eq!((m.default_formatted)(), "8");
272        assert_eq!((m.type_id)(), TypeId::of::<TestTabstop>());
273    }
274}