harn_builtin_registry/lib.rs
1//! Process-global registry of builtin signatures.
2//!
3//! `harn-vm` owns the implementations and emits one `&'static BuiltinDef<H>`
4//! per `#[harn_builtin]`-annotated function via the `harn-builtin-macros`
5//! crate. At startup the driver (CLI, LSP, lint, serve, dap, tests) installs
6//! the full slice of signatures here; the parser/typechecker then reads them
7//! through [`installed_signatures`].
8//!
9//! This decouples `harn-parser` (which needs to see signatures to typecheck)
10//! from `harn-vm` (which owns the impls) without a dependency cycle —
11//! `harn-parser` depends only on this crate plus `harn-builtin-meta`, never
12//! on the vm.
13
14use std::sync::OnceLock;
15
16use harn_builtin_meta::BuiltinSignature;
17
18/// A complete description of one builtin: its signature, its aliases, the
19/// runtime handler (typed by the consumer via `H`), and optional metadata.
20///
21/// `H` is parametric so this crate stays free of any handler-type
22/// dependency. `harn-vm` instantiates it as
23/// `BuiltinDef<VmBuiltinHandler>`; parser-only consumers ignore the handler
24/// and read just the [`Self::sig`] field.
25#[derive(Debug, Clone, Copy)]
26pub struct BuiltinDef<H: 'static> {
27 /// Static signature consumed by the parser/typechecker.
28 pub sig: BuiltinSignature,
29 /// Additional names that share this impl + signature. Each alias gets
30 /// its own [`BuiltinSignature`] entry at install time (with the same
31 /// param/return types) so the typechecker accepts both.
32 pub aliases: &'static [&'static str],
33 /// Runtime handler (sync fn, async fn, or `None` for parser-only
34 /// builtins). Type is opaque to this crate.
35 pub handler: H,
36 /// Free-form category label used for metadata/observability.
37 pub category: Option<&'static str>,
38 /// Human-readable doc, typically the leading `///` block from the impl
39 /// function. Surfaced to LSP hover and `harn explain`.
40 pub doc: Option<&'static str>,
41 /// Free-form Harn-style signature text (e.g. `"foo(a: dict) -> dict"`).
42 /// Populated by `#[harn_builtin]` from the `sig = "..."` literal so the
43 /// runtime metadata layer can surface the original source spelling
44 /// without re-rendering [`Self::sig`]. The DSL builder shape used to
45 /// store this via `.signature(...)`; the macro shape replaces it.
46 pub signature_text: Option<&'static str>,
47 /// Set to `true` for builtins that exist in the parser registry but
48 /// have no runtime entry (`len`, `split`, … — see
49 /// `PARSER_ONLY_EXCEPTIONS` in the alignment test). The registry
50 /// skips runtime registration for these.
51 pub parser_only: bool,
52 /// Set to `true` for compiler-synthesized runtime helpers (sigil
53 /// prefix `__`, opcode keywords, enum constructors) that exist as VM
54 /// builtins but should NOT show up in the parser signature table.
55 /// The registry skips signature publishing for these.
56 pub runtime_only: bool,
57}
58
59impl<H: 'static> BuiltinDef<H> {
60 /// Compact constructor for the common case: one signature, no aliases,
61 /// no metadata flags.
62 pub const fn new(sig: BuiltinSignature, handler: H) -> Self {
63 Self {
64 sig,
65 aliases: &[],
66 handler,
67 category: None,
68 doc: None,
69 signature_text: None,
70 parser_only: false,
71 runtime_only: false,
72 }
73 }
74}
75
76/// Process-global slice of installed signatures, populated once by the
77/// driver. Reads via [`installed_signatures`] are O(1); writes via
78/// [`install_builtin_signatures`] are one-shot.
79static INSTALLED: OnceLock<&'static [&'static BuiltinSignature]> = OnceLock::new();
80
81/// Install the process-global signature registry. Called once by the driver
82/// (CLI, LSP, lint, serve, dap) at startup. Test harnesses that build a Vm
83/// via `harn_vm::stdlib::stdlib_probe_vm()` inherit the install through that
84/// helper.
85///
86/// # Panics
87/// Panics if called more than once with different slices. Repeat calls with
88/// the same pointer are tolerated (CLI + test harness can both call it).
89pub fn install_builtin_signatures(sigs: &'static [&'static BuiltinSignature]) {
90 if INSTALLED.set(sigs).is_err() {
91 assert!(
92 INSTALLED.get().copied().map(<[_]>::as_ptr) == Some(sigs.as_ptr()),
93 "install_builtin_signatures called twice with different slices — \
94 drivers should install once at startup"
95 );
96 }
97}
98
99/// Reset the installed slice — only callable from tests (`#[cfg(test)]`
100/// guarded). Avoids leaking state across in-process unit tests that need
101/// to swap the installed slice. **Not** for production use.
102#[doc(hidden)]
103pub fn _test_only_reinstall(sigs: &'static [&'static BuiltinSignature]) {
104 // OnceLock has no public reset, but we can't avoid OnceLock semantics
105 // here. Tests that need to reinstall should call this exactly once
106 // before any other test calls `install_builtin_signatures`.
107 let _ = INSTALLED.set(sigs);
108}
109
110/// Read the installed signature slice. Returns an empty slice before the
111/// first call to [`install_builtin_signatures`] (e.g. in pure-parser unit
112/// tests that don't need a registry).
113pub fn installed_signatures() -> &'static [&'static BuiltinSignature] {
114 INSTALLED.get().copied().unwrap_or(&[])
115}
116
117/// True when the registry has been populated. Useful for guards in parser
118/// code that wants to assert it's running in a configured driver context.
119pub fn is_installed() -> bool {
120 INSTALLED.get().is_some()
121}