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}