Archived
feat: package manager, build system, native cross-compilation, plugin system
Add three new crates and extend the compiler and CLI toolchain: - el-manifest: el.toml manifest parser using serde + toml crate; supports package info, registry/path/version deps, build config with seal key sources, cross targets, and plugins; Manifest::find_manifest() walks up the directory tree - el-registry: HTTP registry client (reqwest + tokio) for packages.neurontechnologies.ai; PackageMetadata, fetch/download/publish/ search, BLAKE3 checksum verification, local cache at ~/.engram/packages/ - el-build: build orchestrator with incremental builds (BLAKE3 file hashes in .el/build-cache.json), cross-compilation target tagging, dep resolution, plugin registry with on_ast/on_typed_ast/on_bytecode hooks, test runner, fmt/check/clean commands - CrossTarget and NativeTarget enums with triple() and artifact_extension() methods; NativeTarget::Host detects compile-time platform via cfg! macros - Plugin system: CompilerPlugin trait + PluginRegistry; dynamic loading is a marked TODO with clear extension point for libloading - CLI extended with: new, add, remove, update, build --cross, run, test, check, fmt, clean, publish, search, plugin add/remove/list; old single-file commands moved to build-file/seal/unseal subcommands - Fix pre-existing debugger.rs borrow error (unwrap_or temporary lifetime) - Fix checker.rs and codegen.rs to handle TestDef/Seed/Assert Stmt variants - Add spec/language.md sections 12-14: package system, build system, plugin system, cross-compilation targets table 130 tests passing, zero warnings
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
//! 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, _version) in &manifest.plugins {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user