Archived
276 lines
9.7 KiB
Rust
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
|
|
}
|
|
}
|