//! Compiler plugin system. //! //! Plugins are Rust dynamic libraries (`.dylib` / `.so`) that implement the //! [`CompilerPlugin`] trait. They are loaded at compile time and receive hooks //! at each stage of the compilation pipeline. //! //! # Lifecycle hooks //! 1. `on_ast` — called after parsing, before type checking //! 2. `on_typed_ast` — called after type checking, before codegen //! 3. `on_bytecode` — called after codegen, before sealing //! //! # Writing a plugin //! ```rust,ignore //! use el_build::CompilerPlugin; //! use el_parser::Program; //! use el_types::TypeEnv; //! //! pub struct MyPlugin; //! //! impl CompilerPlugin for MyPlugin { //! fn name(&self) -> &str { "my-plugin" } //! fn version(&self) -> &str { "0.1.0" } //! fn on_ast(&self, _program: &mut Program) -> Result<(), el_build::PluginError> { Ok(()) } //! fn on_typed_ast(&self, _program: &Program, _types: &TypeEnv) -> Result<(), el_build::PluginError> { Ok(()) } //! fn on_bytecode(&self, _bytecode: &mut Vec) -> Result<(), el_build::PluginError> { Ok(()) } //! } //! ``` use std::path::Path; use thiserror::Error; use el_manifest::Manifest; // ── Error ───────────────────────────────────────────────────────────────────── #[derive(Debug, Error)] pub enum PluginError { #[error("plugin '{name}' ast hook failed: {reason}")] AstHookFailed { name: String, reason: String }, #[error("plugin '{name}' typed-ast hook failed: {reason}")] TypedAstHookFailed { name: String, reason: String }, #[error("plugin '{name}' bytecode hook failed: {reason}")] BytecodeHookFailed { name: String, reason: String }, #[error("plugin '{name}' not found in {dir}")] NotFound { name: String, dir: String }, #[error("plugin loading is not supported on this platform")] PlatformUnsupported, } // ── Plugin trait ────────────────────────────────────────────────────────────── /// The interface that all compiler plugins must implement. /// /// Plugins receive three optional hooks during compilation. Each hook may /// mutate the data it receives (AST, bytecode) or read it for analysis. pub trait CompilerPlugin: Send + Sync { /// The plugin's canonical name (matches its key in `el.toml [plugins]`). fn name(&self) -> &str; /// The plugin's version string. fn version(&self) -> &str; /// Called after parsing, before type checking. /// /// Implementations may add synthetic AST nodes, remove nodes, or /// record observations. Mutation is allowed. fn on_ast(&self, program: &mut el_parser::Program) -> Result<(), PluginError>; /// Called after type checking, before code generation. /// /// The AST is immutable at this stage. Implementations may inspect the /// resolved types for documentation generation, linting, etc. fn on_typed_ast( &self, program: &el_parser::Program, types: &el_types::TypeEnv, ) -> Result<(), PluginError>; /// Called after code generation, before sealing. /// /// Implementations may inspect or transform the raw bytecode bytes. fn on_bytecode(&self, bytecode: &mut Vec) -> Result<(), PluginError>; } // ── Registry ────────────────────────────────────────────────────────────────── /// A registry of loaded compiler plugins. pub struct PluginRegistry { plugins: Vec>, } impl PluginRegistry { /// Create an empty registry. pub fn new() -> Self { Self { plugins: Vec::new(), } } /// Register a plugin directly (used in tests and for built-in plugins). pub fn register(&mut self, plugin: Box) { self.plugins.push(plugin); } /// Load all plugins listed in the manifest's `[plugins]` section. /// /// Plugins are expected to be `.dylib` (macOS) / `.so` (Linux) files /// in `plugin_dir`. Dynamic loading is marked as a TODO — for now, this /// is a no-op stub that validates the plugin manifest entries. pub fn load_from_manifest( &mut self, manifest: &Manifest, plugin_dir: &Path, ) -> Result<(), PluginError> { for name in manifest.plugins.keys() { // TODO(LLVM backend): use `libloading` crate to dlopen the .dylib/.so, // look up the `engram_plugin_init` symbol, call it, and register the // returned Box. // // Extension point: // let lib = unsafe { libloading::Library::new(dylib_path) }?; // let init: Symbol Box> = // unsafe { lib.get(b"engram_plugin_init") }?; // self.plugins.push(init()); let dylib_name = if cfg!(target_os = "macos") { format!("lib{name}.dylib") } else if cfg!(target_os = "windows") { format!("{name}.dll") } else { format!("lib{name}.so") }; let dylib_path = plugin_dir.join(&dylib_name); if !dylib_path.exists() { // Not treating missing plugins as fatal during the stub phase. // In production, this would be an error. eprintln!( "warning: plugin '{name}' not found at {} (dynamic loading is a TODO)", dylib_path.display() ); } } Ok(()) } /// Run the `on_ast` hook for all registered plugins. pub fn run_ast_hooks(&self, program: &mut el_parser::Program) -> Result<(), PluginError> { for plugin in &self.plugins { plugin.on_ast(program)?; } Ok(()) } /// Run the `on_typed_ast` hook for all registered plugins. pub fn run_typed_hooks( &self, program: &el_parser::Program, types: &el_types::TypeEnv, ) -> Result<(), PluginError> { for plugin in &self.plugins { plugin.on_typed_ast(program, types)?; } Ok(()) } /// Run the `on_bytecode` hook for all registered plugins. pub fn run_bytecode_hooks(&self, bytecode: &mut Vec) -> Result<(), PluginError> { for plugin in &self.plugins { plugin.on_bytecode(bytecode)?; } Ok(()) } /// Number of plugins currently registered. pub fn len(&self) -> usize { self.plugins.len() } /// Whether no plugins are registered. pub fn is_empty(&self) -> bool { self.plugins.is_empty() } } impl Default for PluginRegistry { fn default() -> Self { Self::new() } } // ── Tests ───────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; /// A no-op test plugin. struct NopPlugin; impl CompilerPlugin for NopPlugin { fn name(&self) -> &str { "nop-plugin" } fn version(&self) -> &str { "0.1.0" } fn on_ast(&self, _program: &mut el_parser::Program) -> Result<(), PluginError> { Ok(()) } fn on_typed_ast(&self, _p: &el_parser::Program, _t: &el_types::TypeEnv) -> Result<(), PluginError> { Ok(()) } fn on_bytecode(&self, _b: &mut Vec) -> Result<(), PluginError> { Ok(()) } } /// A plugin that appends a byte to the bytecode (to verify mutation). struct MutatingPlugin; impl CompilerPlugin for MutatingPlugin { fn name(&self) -> &str { "mutating-plugin" } fn version(&self) -> &str { "1.0.0" } fn on_ast(&self, _program: &mut el_parser::Program) -> Result<(), PluginError> { Ok(()) } fn on_typed_ast(&self, _p: &el_parser::Program, _t: &el_types::TypeEnv) -> Result<(), PluginError> { Ok(()) } fn on_bytecode(&self, bytecode: &mut Vec) -> Result<(), PluginError> { bytecode.push(0xFF); // marker byte Ok(()) } } #[test] fn test_empty_registry() { let reg = PluginRegistry::new(); assert!(reg.is_empty()); assert_eq!(reg.len(), 0); } #[test] fn test_register_plugin() { let mut reg = PluginRegistry::new(); reg.register(Box::new(NopPlugin)); assert_eq!(reg.len(), 1); assert!(!reg.is_empty()); } #[test] fn test_bytecode_hook_mutates() { let mut reg = PluginRegistry::new(); reg.register(Box::new(MutatingPlugin)); let mut bytecode = vec![0x01, 0x02, 0x03]; reg.run_bytecode_hooks(&mut bytecode).unwrap(); assert_eq!(bytecode.last(), Some(&0xFF)); assert_eq!(bytecode.len(), 4); } #[test] fn test_multiple_plugins_run_in_order() { let mut reg = PluginRegistry::new(); reg.register(Box::new(MutatingPlugin)); reg.register(Box::new(MutatingPlugin)); let mut bytecode = vec![0x01]; reg.run_bytecode_hooks(&mut bytecode).unwrap(); // Two MutatingPlugins → two 0xFF bytes appended assert_eq!(bytecode, vec![0x01, 0xFF, 0xFF]); } #[test] fn test_nop_plugin_hooks_succeed() { let mut reg = PluginRegistry::new(); reg.register(Box::new(NopPlugin)); let mut bytecode = vec![0x00]; assert!(reg.run_bytecode_hooks(&mut bytecode).is_ok()); assert_eq!(bytecode.len(), 1); // NopPlugin does not mutate } }