This repository has been archived on 2026-08-20. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-retired/crates/el-build/src/plugin.rs
T

276 lines
9.7 KiB
Rust

//! 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<u8>) -> 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<u8>) -> Result<(), PluginError>;
}
// ── Registry ──────────────────────────────────────────────────────────────────
/// A registry of loaded compiler plugins.
pub struct PluginRegistry {
plugins: Vec<Box<dyn CompilerPlugin>>,
}
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<dyn CompilerPlugin>) {
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<dyn CompilerPlugin>.
//
// Extension point:
// let lib = unsafe { libloading::Library::new(dylib_path) }?;
// let init: Symbol<fn() -> Box<dyn CompilerPlugin>> =
// 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<u8>) -> 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<u8>) -> 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<u8>) -> 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
}
}