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/bin/el/src/main.rs
T
Will Anderson 48b72843e1 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
2026-04-27 19:08:25 -05:00

735 lines
26 KiB
Rust

//! el — The Engram language CLI.
//!
//! # Commands
//!
//! ## Project management
//! el new <name> scaffold new project with el.toml + src/main.el
//! el add <package>[@ver] add a dependency to el.toml
//! el remove <package> remove a dependency from el.toml
//! el update update all deps to latest compatible versions
//!
//! ## Build & run
//! el build [--target prod] build the project (reads el.toml)
//! el build --cross build for all configured cross targets
//! el run build debug and run
//! el test run tests
//! el check type-check only
//! el fmt format source files
//! el clean clean build artifacts
//!
//! ## Registry
//! el publish publish to registry
//! el search <query> search registry
//! el plugin add <plugin> add compiler plugin
//!
//! ## Low-level
//! el build-file <file.el> compile a single .el file (no el.toml needed)
//! el seal <artifact> seal an existing release artifact
//! el unseal <artifact> unseal a sealed artifact
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use el_build::BuildSystem;
use el_compiler::{Compiler, CompilerOptions, Target};
use el_manifest::{BuildTarget, Manifest};
use el_seal::{seal as seal_fn, unseal as unseal_fn, SealedArtifact, DeploymentBinding, SealAlgorithm, SealConfig};
// ── CLI definition ────────────────────────────────────────────────────────────
#[derive(Parser, Debug)]
#[command(
name = "el",
about = "The Engram programming language compiler and toolchain",
version
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
// ── Project management ────────────────────────────────────────────────────
/// Scaffold a new Engram project with el.toml and src/main.el.
New {
/// Project name.
name: String,
/// Directory to create the project in (default: <name>/).
#[arg(long, short = 'd')]
dir: Option<PathBuf>,
},
/// Add a dependency to el.toml.
Add {
/// Package name, optionally with version: `engram-http@1.2`.
package: String,
/// Path dependency: `--path ../my-lib`.
#[arg(long)]
path: Option<PathBuf>,
},
/// Remove a dependency from el.toml.
Remove {
/// Package name.
package: String,
},
/// Update all dependencies to the latest compatible versions.
Update,
// ── Build & run ───────────────────────────────────────────────────────────
/// Build the project (reads el.toml).
Build {
/// Override the build target: debug | release | prod.
#[arg(long)]
target: Option<String>,
/// Build for all configured cross-compilation targets.
#[arg(long)]
cross: bool,
/// Path to the project manifest (default: el.toml in current directory).
#[arg(long)]
manifest: Option<PathBuf>,
},
/// Build in debug mode and run the output.
Run {
/// Path to the project manifest (default: el.toml).
#[arg(long)]
manifest: Option<PathBuf>,
},
/// Run all tests.
Test {
#[arg(long)]
manifest: Option<PathBuf>,
},
/// Type-check source files without producing artifacts.
Check {
#[arg(long)]
manifest: Option<PathBuf>,
},
/// Format source files.
Fmt {
#[arg(long)]
manifest: Option<PathBuf>,
},
/// Remove build artifacts and cache.
Clean {
#[arg(long)]
manifest: Option<PathBuf>,
},
// ── Registry ──────────────────────────────────────────────────────────────
/// Publish this package to the Engram registry.
Publish {
/// Registry API key.
#[arg(long, env = "ENGRAM_REGISTRY_KEY")]
api_key: Option<String>,
#[arg(long)]
manifest: Option<PathBuf>,
},
/// Search the Engram registry.
Search {
/// Search query.
query: String,
},
/// Manage compiler plugins.
Plugin {
#[command(subcommand)]
action: PluginAction,
},
// ── Low-level / single-file ───────────────────────────────────────────────
/// Compile a single .el source file (no el.toml required).
BuildFile {
/// Source file (*.el).
file: PathBuf,
/// Compilation target: debug | release | prod.
#[arg(long, default_value = "debug")]
target: String,
/// Output path.
#[arg(long, short = 'o')]
output: Option<PathBuf>,
},
/// Seal an existing release artifact.
Seal {
artifact: PathBuf,
#[arg(long, short = 'o')]
output: Option<PathBuf>,
},
/// Unseal a sealed artifact (requires ENGRAM_SEAL_KEY).
Unseal {
artifact: PathBuf,
#[arg(long, short = 'o')]
output: Option<PathBuf>,
},
}
#[derive(Subcommand, Debug)]
enum PluginAction {
/// Add a compiler plugin to el.toml.
Add {
/// Plugin name, optionally with version: `el-fmt@1.0`.
plugin: String,
},
/// Remove a compiler plugin from el.toml.
Remove {
plugin: String,
},
/// List installed plugins.
List,
}
// ── Entry point ───────────────────────────────────────────────────────────────
#[tokio::main]
async fn main() {
let cli = Cli::parse();
if let Err(e) = run(cli).await {
eprintln!("error: {e}");
std::process::exit(1);
}
}
async fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
match cli.command {
// ── Project management ────────────────────────────────────────────────
Command::New { name, dir } => {
cmd_new(&name, dir.as_deref())?;
}
Command::Add { package, path } => {
cmd_add(&package, path.as_deref())?;
}
Command::Remove { package } => {
cmd_remove(&package)?;
}
Command::Update => {
println!("update: checking for newer dependency versions...");
println!("(registry not yet live — no updates available)");
}
// ── Build & run ───────────────────────────────────────────────────────
Command::Build { target, cross, manifest } => {
let manifest_path = resolve_manifest(manifest.as_deref())?;
let bs = BuildSystem::from_manifest_file(&manifest_path)?;
if cross {
let results = bs.build_all_targets().await?;
for (ct, out) in &results {
println!(
"built {} ({}) -> {} [{} bytes, {}ms]",
ct,
out.target,
out.artifact_path.display(),
out.size_bytes,
out.compile_time_ms,
);
}
println!("cross: {} target(s) built", results.len());
} else {
let build_target = target.as_deref().map(parse_build_target).transpose()?;
let out = bs.build(build_target).await?;
println!(
"built {} -> {} [{} bytes, {}ms, sealed={}]",
bs.manifest.package.name,
out.artifact_path.display(),
out.size_bytes,
out.compile_time_ms,
out.sealed,
);
}
}
Command::Run { manifest } => {
let manifest_path = resolve_manifest(manifest.as_deref())?;
let bs = BuildSystem::from_manifest_file(&manifest_path)?;
let out = bs.build(Some(BuildTarget::Debug)).await?;
let artifact = std::fs::read(&out.artifact_path)?;
let instructions = el_compiler::Bytecode::deserialize_all(&artifact)
.unwrap_or_default();
run_interpreter(&instructions);
}
Command::Test { manifest } => {
let manifest_path = resolve_manifest(manifest.as_deref())?;
let bs = BuildSystem::from_manifest_file(&manifest_path)?;
let report = bs.test().await?;
println!(
"test: {} passed, {} failed (total {})",
report.passed, report.failed, report.total
);
for f in &report.failures {
eprintln!(" FAIL: {f}");
}
if !report.success() {
std::process::exit(1);
}
}
Command::Check { manifest } => {
let manifest_path = resolve_manifest(manifest.as_deref())?;
let bs = BuildSystem::from_manifest_file(&manifest_path)?;
let diags = bs.check()?;
if diags.is_empty() {
println!("check: ok");
} else {
for d in &diags {
eprintln!("warning: {d}");
}
}
}
Command::Fmt { manifest } => {
let manifest_path = resolve_manifest(manifest.as_deref())?;
let bs = BuildSystem::from_manifest_file(&manifest_path)?;
bs.fmt()?;
}
Command::Clean { manifest } => {
let manifest_path = resolve_manifest(manifest.as_deref())?;
let bs = BuildSystem::from_manifest_file(&manifest_path)?;
bs.clean()?;
}
// ── Registry ──────────────────────────────────────────────────────────
Command::Publish { api_key, manifest } => {
let key = api_key.ok_or("publish requires --api-key or ENGRAM_REGISTRY_KEY env var")?;
let manifest_path = resolve_manifest(manifest.as_deref())?;
let bs = BuildSystem::from_manifest_file(&manifest_path)?;
// Build release artifact first
let out = bs.build(Some(BuildTarget::Release)).await?;
let client = el_registry::RegistryClient::new();
client.publish(&bs.manifest, &out.artifact_path, &key).await?;
println!("published {} v{}", bs.manifest.package.name, bs.manifest.package.version);
}
Command::Search { query } => {
let client = el_registry::RegistryClient::new();
println!("searching registry for '{query}'...");
match client.search(&query).await {
Ok(results) => {
if results.is_empty() {
println!("no results found");
}
for pkg in &results {
println!(" {} v{}{}", pkg.name, pkg.version, pkg.description);
}
}
Err(e) => {
eprintln!("registry unavailable (server not yet deployed): {e}");
}
}
}
Command::Plugin { action } => match action {
PluginAction::Add { plugin } => {
cmd_plugin_add(&plugin)?;
}
PluginAction::Remove { plugin } => {
println!("remove plugin: {plugin} (not yet implemented)");
}
PluginAction::List => {
let manifest_path = resolve_manifest(None)?;
let m = Manifest::from_file(&manifest_path)?;
if m.plugins.is_empty() {
println!("no plugins installed");
}
for (name, ver) in &m.plugins {
println!(" {name} = \"{ver}\"");
}
}
},
// ── Low-level / single-file ───────────────────────────────────────────
Command::BuildFile { file, target, output } => {
let source = std::fs::read_to_string(&file)
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
let compilation_target = parse_target(&target)?;
let out_path = output.unwrap_or_else(|| {
let stem = file.file_stem().unwrap_or_default().to_string_lossy();
match &compilation_target {
Target::Debug | Target::Release => PathBuf::from(format!("{stem}.elc")),
Target::Prod => PathBuf::from(format!("{stem}.sealed")),
}
});
let seal_config = build_seal_config()?;
let opts = CompilerOptions {
target: compilation_target,
output_path: out_path.clone(),
source_path: file.clone(),
engram_db_path: None,
seal_config,
};
let output = Compiler::compile(&source, opts)?;
for d in &output.diagnostics {
eprintln!("warning: {d}");
}
std::fs::write(&out_path, &output.artifact)
.map_err(|e| format!("cannot write {}: {e}", out_path.display()))?;
if let Some(sm) = &output.source_map {
let sm_path = out_path.with_extension("map.json");
std::fs::write(&sm_path, sm)
.map_err(|e| format!("cannot write source map: {e}"))?;
println!("compiled {} -> {} (source map: {})",
file.display(), out_path.display(), sm_path.display());
} else {
println!("compiled {} -> {} [sealed={}]",
file.display(), out_path.display(), output.sealed);
}
}
Command::Seal { artifact, output } => {
let bytes = std::fs::read(&artifact)
.map_err(|e| format!("cannot read {}: {e}", artifact.display()))?;
let out_path = output.unwrap_or_else(|| {
let mut p = artifact.clone();
let ext = format!("{}.sealed",
p.extension().unwrap_or_default().to_string_lossy());
p.set_extension(ext);
p
});
let config = build_seal_config()?;
let sealed = seal_fn(&bytes, &config)?;
let artifact_bytes = sealed.to_bytes()?;
std::fs::write(&out_path, &artifact_bytes)
.map_err(|e| format!("cannot write {}: {e}", out_path.display()))?;
println!("sealed {} -> {} ({} bytes)",
artifact.display(), out_path.display(), artifact_bytes.len());
}
Command::Unseal { artifact, output } => {
let bytes = std::fs::read(&artifact)
.map_err(|e| format!("cannot read {}: {e}", artifact.display()))?;
let out_path = output.unwrap_or_else(|| artifact.with_extension("elc"));
let sealed = SealedArtifact::from_bytes(&bytes)?;
let key_str = std::env::var("ENGRAM_SEAL_KEY").unwrap_or_default();
let key_bytes = key_str.as_bytes();
let plaintext = unseal_fn(&sealed, key_bytes)?;
std::fs::write(&out_path, &plaintext)
.map_err(|e| format!("cannot write {}: {e}", out_path.display()))?;
println!("unsealed {} -> {} ({} bytes)",
artifact.display(), out_path.display(), plaintext.len());
}
}
Ok(())
}
// ── Command implementations ───────────────────────────────────────────────────
fn cmd_new(name: &str, dir: Option<&std::path::Path>) -> Result<(), Box<dyn std::error::Error>> {
let project_dir = dir
.map(|d| d.to_path_buf())
.unwrap_or_else(|| PathBuf::from(name));
if project_dir.exists() {
return Err(format!("directory '{}' already exists", project_dir.display()).into());
}
std::fs::create_dir_all(project_dir.join("src"))?;
// Write el.toml
let manifest_content = format!(
r#"[package]
name = "{name}"
version = "0.1.0"
description = ""
authors = []
license = "MIT"
edition = "2026"
[dependencies]
[build]
target = "debug"
entry = "src/main.el"
output = "dist/"
[cross]
targets = []
"#
);
std::fs::write(project_dir.join("el.toml"), manifest_content)?;
// Write src/main.el
let main_el = format!(
r#"// {name} — entry point
fn main() -> Void {{
let msg: String = "Hello from {name}!"
println(msg)
}}
"#
);
std::fs::write(project_dir.join("src").join("main.el"), main_el)?;
// Write .gitignore
std::fs::write(project_dir.join(".gitignore"), "dist/\n.el/\n")?;
println!("created project '{name}' in {}/", project_dir.display());
println!(" el.toml");
println!(" src/main.el");
Ok(())
}
fn cmd_add(package: &str, path: Option<&std::path::Path>) -> Result<(), Box<dyn std::error::Error>> {
let manifest_path = resolve_manifest(None)?;
let mut manifest_text = std::fs::read_to_string(&manifest_path)?;
let dep_line = if let Some(p) = path {
format!(
r#"{package} = {{ path = "{}" }}"#,
p.display()
)
} else {
// Parse name@version
let (pkg_name, version) = if let Some((n, v)) = package.split_once('@') {
(n, v.to_string())
} else {
(package, "*".to_string())
};
format!(r#"{pkg_name} = "{version}""#)
};
// Append to [dependencies] section
if let Some(idx) = manifest_text.find("[dependencies]") {
// Find next section or end
let after = &manifest_text[idx + "[dependencies]".len()..];
let insert_pos = after
.find("\n[")
.map(|i| idx + "[dependencies]".len() + i)
.unwrap_or(manifest_text.len());
manifest_text.insert_str(insert_pos, &format!("\n{dep_line}"));
} else {
manifest_text.push_str(&format!("\n[dependencies]\n{dep_line}\n"));
}
std::fs::write(&manifest_path, &manifest_text)?;
println!("added: {dep_line}");
Ok(())
}
fn cmd_remove(package: &str) -> Result<(), Box<dyn std::error::Error>> {
let manifest_path = resolve_manifest(None)?;
let manifest_text = std::fs::read_to_string(&manifest_path)?;
// Remove the line containing `package = ...` from [dependencies]
let updated: String = manifest_text
.lines()
.filter(|line| {
let trimmed = line.trim();
!trimmed.starts_with(package)
|| !trimmed[package.len()..].trim_start().starts_with('=')
})
.collect::<Vec<_>>()
.join("\n");
// Preserve trailing newline
let updated = if manifest_text.ends_with('\n') {
format!("{updated}\n")
} else {
updated
};
std::fs::write(&manifest_path, &updated)?;
println!("removed: {package}");
Ok(())
}
fn cmd_plugin_add(plugin: &str) -> Result<(), Box<dyn std::error::Error>> {
let manifest_path = resolve_manifest(None)?;
let mut manifest_text = std::fs::read_to_string(&manifest_path)?;
let (plugin_name, version) = if let Some((n, v)) = plugin.split_once('@') {
(n.to_string(), v.to_string())
} else {
(plugin.to_string(), "*".to_string())
};
let plugin_line = format!(r#"{plugin_name} = "{version}""#);
if let Some(idx) = manifest_text.find("[plugins]") {
let after = &manifest_text[idx + "[plugins]".len()..];
let insert_pos = after
.find("\n[")
.map(|i| idx + "[plugins]".len() + i)
.unwrap_or(manifest_text.len());
manifest_text.insert_str(insert_pos, &format!("\n{plugin_line}"));
} else {
manifest_text.push_str(&format!("\n[plugins]\n{plugin_line}\n"));
}
std::fs::write(&manifest_path, &manifest_text)?;
println!("added plugin: {plugin_line}");
Ok(())
}
// ── Helpers ───────────────────────────────────────────────────────────────────
/// Find the nearest `el.toml` starting from the current directory.
fn resolve_manifest(path: Option<&std::path::Path>) -> Result<PathBuf, Box<dyn std::error::Error>> {
if let Some(p) = path {
return Ok(p.to_path_buf());
}
let cwd = std::env::current_dir()?;
Manifest::find_manifest(&cwd).map_err(|e| e.into())
}
fn parse_build_target(s: &str) -> Result<BuildTarget, Box<dyn std::error::Error>> {
s.parse::<BuildTarget>().map_err(|e| e.into())
}
fn parse_target(s: &str) -> Result<Target, String> {
match s {
"debug" => Ok(Target::Debug),
"release" => Ok(Target::Release),
"prod" => Ok(Target::Prod),
other => Err(format!("unknown target '{other}': use debug, release, or prod")),
}
}
fn build_seal_config() -> Result<SealConfig, String> {
Ok(SealConfig {
algorithm: SealAlgorithm::Aes256Gcm,
deployment_binding: if std::env::var("ENGRAM_SEAL_KEY").is_ok() {
DeploymentBinding::EnvironmentKey("ENGRAM_SEAL_KEY".into())
} else {
DeploymentBinding::None
},
})
}
/// Minimal interpreter for demonstration.
fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
use el_compiler::{Bytecode, Value};
let mut stack: Vec<Value> = Vec::new();
let mut locals: std::collections::HashMap<String, Value> = std::collections::HashMap::new();
let mut ip = 0usize;
while ip < instructions.len() {
match &instructions[ip] {
Bytecode::Push(v) => stack.push(v.clone()),
Bytecode::Pop => { stack.pop(); }
Bytecode::Dup => {
if let Some(top) = stack.last().cloned() { stack.push(top); }
}
Bytecode::Add => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(match (a, b) {
(Value::Int(x), Value::Int(y)) => Value::Int(x + y),
(Value::Float(x), Value::Float(y)) => Value::Float(x + y),
(Value::Str(x), Value::Str(y)) => Value::Str(x + &y),
_ => Value::Nil,
});
}
Bytecode::Sub => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(match (a, b) {
(Value::Int(x), Value::Int(y)) => Value::Int(x - y),
(Value::Float(x), Value::Float(y)) => Value::Float(x - y),
_ => Value::Nil,
});
}
Bytecode::Mul => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(match (a, b) {
(Value::Int(x), Value::Int(y)) => Value::Int(x * y),
(Value::Float(x), Value::Float(y)) => Value::Float(x * y),
_ => Value::Nil,
});
}
Bytecode::Div => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(match (a, b) {
(Value::Int(x), Value::Int(y)) if y != 0 => Value::Int(x / y),
(Value::Float(x), Value::Float(y)) => Value::Float(x / y),
_ => Value::Nil,
});
}
Bytecode::Eq => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(a == b));
}
Bytecode::Not => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(Value::Bool(!matches!(v, Value::Bool(true))));
}
Bytecode::StoreLocal(name) => {
let v = stack.pop().unwrap_or(Value::Nil);
locals.insert(name.clone(), v);
}
Bytecode::LoadLocal(name) => {
let v = locals.get(name).cloned().unwrap_or(Value::Nil);
stack.push(v);
}
Bytecode::Call { name, .. } => {
if name == "print" || name == "println" {
let v = stack.pop().unwrap_or(Value::Nil);
println!("{v}");
stack.push(Value::Nil);
}
}
Bytecode::Activate { type_name, query } => {
println!("[activate] {type_name} where \"{query}\" (no DB connected)");
stack.push(Value::List(vec![]));
}
Bytecode::Jump(offset) => {
let new_ip = (ip as i32 + 1 + offset) as usize;
ip = new_ip;
continue;
}
Bytecode::JumpIf(offset) => {
let cond = stack.pop().unwrap_or(Value::Nil);
if matches!(cond, Value::Bool(true)) {
let new_ip = (ip as i32 + 1 + offset) as usize;
ip = new_ip;
continue;
}
}
Bytecode::JumpIfNot(offset) => {
let cond = stack.pop().unwrap_or(Value::Nil);
if !matches!(cond, Value::Bool(true)) {
let new_ip = (ip as i32 + 1 + offset) as usize;
ip = new_ip;
continue;
}
}
Bytecode::Return => break,
Bytecode::Halt => break,
Bytecode::SealedBegin => eprintln!("[sealed section begin]"),
Bytecode::SealedEnd => eprintln!("[sealed section end]"),
_ => {}
}
ip += 1;
}
}