Skip to main content

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}