ccalc_engine/plugin.rs
1//! Plugin system for extending ccalc with new built-in functions.
2//!
3//! Third-party crates implement the [`plugin::Plugin`] trait and register via
4//! [`plugin::register_plugin`]. The engine checks the registry before its own
5//! built-in table, so plugins can shadow any existing built-in if needed.
6//!
7//! # Minimal plugin example
8//!
9//! ```rust,ignore
10//! use ccalc_engine::env::{Env, Value};
11//! use ccalc_engine::plugin::Plugin;
12//!
13//! pub struct MyPlugin;
14//!
15//! impl Plugin for MyPlugin {
16//! fn name(&self) -> &str { "myfunc" }
17//!
18//! fn call(&self, _name: &str, args: &[Value], _env: &Env) -> Result<Value, String> {
19//! if args.is_empty() {
20//! return Err("myfunc: at least one argument required".into());
21//! }
22//! Ok(args[0].clone())
23//! }
24//! }
25//!
26//! // In main.rs / startup:
27//! ccalc_engine::plugin::register_plugin(Box::new(MyPlugin));
28//! ```
29//!
30//! Plugins that expose several names (e.g. a plot plugin with `plot`, `scatter`,
31//! `bar`, …) should also override [`plugin::Plugin::exported_names`] and return a
32//! `const`-backed slice. The `name` argument to `call` identifies which exported
33//! name was invoked, enabling a single plugin to dispatch multiple functions:
34//!
35//! ```rust,ignore
36//! const NAMES: &[&str] = &["plot", "scatter", "bar"];
37//!
38//! fn exported_names(&self) -> &[&str] { NAMES }
39//!
40//! fn call(&self, name: &str, args: &[Value], _env: &Env) -> Result<Value, String> {
41//! match name {
42//! "plot" => { /* ... */ Ok(Value::Void) }
43//! "scatter" => { /* ... */ Ok(Value::Void) }
44//! _ => Err(format!("{name}: not implemented"))
45//! }
46//! }
47//! ```
48
49use std::cell::RefCell;
50
51use crate::env::{Env, Value};
52
53/// Trait that all ccalc plugins must implement.
54///
55/// Implement this in a separate crate and register an instance via
56/// [`register_plugin`] before any evaluation takes place.
57pub trait Plugin: Send + Sync {
58 /// The primary name of this plugin.
59 ///
60 /// Used as the canonical identifier when looking up the plugin if
61 /// [`Plugin::exported_names`] is empty.
62 fn name(&self) -> &str;
63
64 /// All names exported by this plugin (used for dispatch and tab completion).
65 ///
66 /// Defaults to `&[]`. If you return a non-empty slice the engine dispatches
67 /// by the slice; otherwise it falls back to [`Plugin::name`].
68 ///
69 /// For multi-function plugins, override this with a `const`-backed slice:
70 ///
71 /// ```rust,ignore
72 /// const NAMES: &[&str] = &["plot", "scatter", "bar"];
73 /// fn exported_names(&self) -> &[&str] { NAMES }
74 /// ```
75 fn exported_names(&self) -> &[&str] {
76 &[]
77 }
78
79 /// Evaluate a call to one of this plugin's exported names.
80 ///
81 /// # Arguments
82 ///
83 /// * `name` — the exact function name that was called (one of [`Plugin::exported_names`])
84 /// * `args` — evaluated argument values (already evaluated by the engine)
85 /// * `env` — current variable environment (read-only)
86 ///
87 /// # Errors
88 ///
89 /// Return `Err(msg)` to propagate an error to the user.
90 fn call(&self, name: &str, args: &[Value], env: &Env) -> Result<Value, String>;
91}
92
93/// Registry that maps exported names to their plugin implementations.
94pub struct PluginRegistry {
95 plugins: Vec<Box<dyn Plugin>>,
96}
97
98impl Default for PluginRegistry {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104impl PluginRegistry {
105 /// Creates an empty registry.
106 pub fn new() -> Self {
107 Self {
108 plugins: Vec::new(),
109 }
110 }
111
112 /// Registers a plugin, making all its exported names available for dispatch.
113 pub fn register(&mut self, p: Box<dyn Plugin>) {
114 self.plugins.push(p);
115 }
116
117 /// Returns the plugin that handles `name`, or `None` if no plugin claims it.
118 ///
119 /// Checks `exported_names()` first; falls back to `name()` for plugins that
120 /// did not override `exported_names`.
121 pub fn get(&self, name: &str) -> Option<&dyn Plugin> {
122 self.plugins
123 .iter()
124 .find(|p| {
125 let exported = p.exported_names();
126 if exported.is_empty() {
127 p.name() == name
128 } else {
129 exported.contains(&name)
130 }
131 })
132 .map(|p| p.as_ref())
133 }
134
135 /// Returns every name exported by all registered plugins.
136 pub fn all_names(&self) -> Vec<String> {
137 self.plugins
138 .iter()
139 .flat_map(|p| {
140 let exported = p.exported_names();
141 if exported.is_empty() {
142 vec![p.name().to_string()]
143 } else {
144 exported.iter().map(|s| s.to_string()).collect()
145 }
146 })
147 .collect()
148 }
149}
150
151thread_local! {
152 static REGISTRY: RefCell<PluginRegistry> = RefCell::new(PluginRegistry::new());
153}
154
155/// Registers a plugin in the thread-local plugin registry.
156///
157/// Call this once at program startup (before any evaluation) for each plugin.
158///
159/// # Examples
160///
161/// ```rust,ignore
162/// ccalc_engine::plugin::register_plugin(Box::new(MyPlugin));
163/// ```
164pub fn register_plugin(p: Box<dyn Plugin>) {
165 REGISTRY.with(|r| r.borrow_mut().register(p));
166}
167
168/// Calls the plugin that handles `name`, if one is registered.
169///
170/// Returns `Some(result)` when a plugin claims the name, `None` otherwise.
171/// Used by `call_builtin` to dispatch before the built-in table.
172pub(crate) fn call_plugin(name: &str, args: &[Value], env: &Env) -> Option<Result<Value, String>> {
173 REGISTRY.with(|r| {
174 let reg = r.borrow();
175 reg.get(name).map(|p| p.call(name, args, env))
176 })
177}
178
179/// Returns all names exported by currently registered plugins.
180///
181/// Used for tab completion in the REPL.
182pub fn plugin_names() -> Vec<String> {
183 REGISTRY.with(|r| r.borrow().all_names())
184}