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 07cfde2402 Add terminal control builtins: term_clear, cursor_to/up/down/col, term_size, term_save/restore_cursor, term_clear_line, print_inline, http_sse_post
Enables full TUI rendering from Engram programs — cursor positioning, screen
clearing, inline (no-newline) printing, terminal size detection via TIOCGWINSZ,
and SSE streaming with inline token output.
2026-04-28 14:08:32 -05:00

4370 lines
176 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_test;
use el_manifest::{BuildTarget, Manifest};
use el_seal::{seal as seal_fn, unseal as unseal_fn, SealedArtifact, DeploymentBinding, SealAlgorithm, SealConfig};
// ── Global state (thread-local for simplicity) ────────────────────────────────
thread_local! {
static GLOBAL_STATE: std::cell::RefCell<std::collections::HashMap<String, String>> =
std::cell::RefCell::new(std::collections::HashMap::new());
/// Callback set by the interpreter before http_serve blocks.
/// When a request arrives, http_serve calls this to invoke handle_request.
static HTTP_SERVE_CALL: std::cell::RefCell<Option<Box<dyn Fn()>>> =
std::cell::RefCell::new(None);
/// Shared bytecode instructions for sub-interpreter calls from http_serve.
static SERVE_INSTRUCTIONS: std::cell::RefCell<Option<std::sync::Arc<Vec<el_compiler::Bytecode>>>> =
std::cell::RefCell::new(None);
/// Shared fn_table for sub-interpreter calls.
static SERVE_FN_TABLE: std::cell::RefCell<Option<std::sync::Arc<std::collections::HashMap<String, usize>>>> =
std::cell::RefCell::new(None);
}
// ── 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 tests in the current project (reads el.toml).
Test {
/// Only run tests matching this name (substring match).
filter: Option<String>,
/// Also run e2e tests (requires ENGRAM_URL or ENGRAM_DB_PATH).
#[arg(long)]
e2e: bool,
/// Run unit AND e2e tests.
#[arg(long)]
all: bool,
/// Output format: human (default) | json | junit.
#[arg(long, default_value = "human")]
output: String,
/// Path to the project manifest (default: el.toml).
#[arg(long)]
manifest: Option<PathBuf>,
},
/// Run tests from a single .el file (no el.toml required).
TestFile {
/// Source file containing test blocks (*.el).
file: PathBuf,
/// Only run tests matching this name (substring match).
filter: Option<String>,
/// Also run e2e tests (requires ENGRAM_URL or ENGRAM_DB_PATH).
#[arg(long)]
e2e: bool,
/// Run unit AND e2e tests.
#[arg(long)]
all: bool,
/// Output format: human (default) | json | junit.
#[arg(long, default_value = "human")]
output: String,
},
/// Run an Engram source file with the step-debugger attached.
Debug {
/// Source file (*.el).
file: PathBuf,
/// Set a breakpoint at line N.
#[arg(long, value_name = "LINE")]
r#break: Option<u32>,
},
/// Type-check source files without producing artifacts.
Check {
#[arg(long)]
manifest: Option<PathBuf>,
},
/// Format an engram-lang source file (or all files in a project).
Fmt {
/// Single source file to format (*.el). When omitted, formats the whole project.
file: Option<PathBuf>,
/// Overwrite the file in place with canonical formatting.
#[arg(long)]
in_place: bool,
/// Exit 1 if the file is not in canonical format (useful in CI). Does not modify the file.
#[arg(long)]
check: bool,
/// Project manifest (used when no `file` is given).
#[arg(long)]
manifest: Option<PathBuf>,
},
/// Lint an engram-lang source file.
Lint {
/// Source file to lint (*.el).
file: PathBuf,
/// Emit diagnostics as JSON instead of human-readable text.
#[arg(long)]
json: bool,
},
/// 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 and immediately run a single .el source file (no el.toml required).
RunFile {
/// Source file (*.el).
file: PathBuf,
/// Arguments to pass to the program.
#[arg(trailing_var_arg = true)]
args: Vec<String>,
},
/// 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 { filter, e2e, all, output, manifest } => {
let manifest_path = resolve_manifest(manifest.as_deref())?;
let bs = BuildSystem::from_manifest_file(&manifest_path)?;
// Find all .el source files in the project
let entry = bs.manifest.build.entry.clone();
let entry_path = manifest_path.parent().unwrap_or(std::path::Path::new(".")).join(&entry);
let source = std::fs::read_to_string(&entry_path)
.map_err(|e| format!("cannot read {}: {e}", entry_path.display()))?;
run_tests_from_source(&source, filter.as_deref(), e2e, all, &output)?;
}
Command::TestFile { file, filter, e2e, all, output } => {
let source = std::fs::read_to_string(&file)
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
run_tests_from_source(&source, filter.as_deref(), e2e, all, &output)?;
}
Command::Debug { file, r#break } => {
let source = std::fs::read_to_string(&file)
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
let opts = CompilerOptions {
target: Target::Debug,
source_path: file.clone(),
..Default::default()
};
let compiled = Compiler::compile(&source, opts)?;
let instructions = el_compiler::Bytecode::deserialize_all(&compiled.artifact)
.unwrap_or_default();
let mut debugger = el_compiler::Debugger::new();
if let Some(line) = r#break {
// Convert line number to a bytecode offset approximation.
// In a full implementation this would use the source map.
// For now we use the line number directly as a placeholder offset.
debugger.add_breakpoint(line as usize);
println!("debugger: breakpoint set at line {line}");
} else {
println!("debugger: breaking on first instruction");
}
println!("debugger: running {} ({} instructions)", file.display(), instructions.len());
run_interpreter_debug(&instructions, &mut debugger);
}
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 { file, in_place, check, manifest } => {
if let Some(path) = file {
// Single-file mode
let source = std::fs::read_to_string(&path)
.map_err(|e| format!("cannot read {}: {e}", path.display()))?;
let formatted = el_fmt::format(&source)
.map_err(|e| format!("fmt error: {e}"))?;
if check {
if formatted != source {
eprintln!("error: {} is not in canonical format", path.display());
std::process::exit(1);
}
println!("ok: {} is already formatted", path.display());
} else if in_place {
std::fs::write(&path, &formatted)
.map_err(|e| format!("cannot write {}: {e}", path.display()))?;
println!("formatted: {}", path.display());
} else {
print!("{formatted}");
}
} else {
// Project mode — fall back to build system's fmt
let manifest_path = resolve_manifest(manifest.as_deref())?;
let bs = BuildSystem::from_manifest_file(&manifest_path)?;
bs.fmt()?;
}
}
Command::Lint { file, json } => {
let source = std::fs::read_to_string(&file)
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
let mut report = el_lint::lint(&source)
.map_err(|e| format!("lint error: {e}"))?;
report.file_path = Some(file.display().to_string());
if json {
println!("{}", report.to_json());
} else {
print!("{}", report.display());
}
if report.has_errors() {
std::process::exit(1);
}
}
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::RunFile { file, args } => {
let source = resolve_imports(&file)
.map_err(|e| format!("cannot resolve imports for {}: {e}", file.display()))?;
let opts = CompilerOptions {
target: Target::Debug,
source_path: file.clone(),
..Default::default()
};
let compiled = Compiler::compile(&source, opts)?;
for d in &compiled.diagnostics {
eprintln!("warning: {d}");
}
let instructions = el_compiler::Bytecode::deserialize_all(&compiled.artifact)
.unwrap_or_default();
run_interpreter_with_args(&instructions, &args);
}
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 ───────────────────────────────────────────────────────────────────
/// Resolve `import "path.el"` directives by reading and concatenating source files.
/// Imports are resolved relative to the directory of the file being imported from.
/// Circular imports are detected via a visited set.
fn resolve_imports(file: &std::path::Path) -> Result<String, Box<dyn std::error::Error>> {
let mut visited = std::collections::HashSet::new();
resolve_imports_inner(file, &mut visited)
}
fn resolve_imports_inner(
file: &std::path::Path,
visited: &mut std::collections::HashSet<PathBuf>,
) -> Result<String, Box<dyn std::error::Error>> {
let canonical = file.canonicalize()
.unwrap_or_else(|_| file.to_path_buf());
if visited.contains(&canonical) {
return Ok(String::new()); // circular — skip
}
visited.insert(canonical.clone());
let dir = file.parent().unwrap_or(std::path::Path::new("."));
let source = std::fs::read_to_string(file)
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
let mut out = String::new();
for line in source.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("import ") {
// import "filename.el"
let rest = rest.trim();
if rest.starts_with('"') && rest.ends_with('"') {
let import_path_str = &rest[1..rest.len() - 1];
let import_path = dir.join(import_path_str);
let imported = resolve_imports_inner(&import_path, visited)?;
out.push_str(&imported);
out.push('\n');
} else {
out.push_str(line);
out.push('\n');
}
} else {
out.push_str(line);
out.push('\n');
}
}
Ok(out)
}
/// 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
},
})
}
/// Discover tests in source, filter, run, and print results.
fn run_tests_from_source(
source: &str,
filter: Option<&str>,
e2e: bool,
all: bool,
output_fmt: &str,
) -> Result<(), Box<dyn std::error::Error>> {
use el_test::{TestReport, TestRunner};
let mut tests = el_test::discover(source)?;
// Apply name filter
if let Some(f) = filter {
tests.retain(|t| t.name.contains(f));
}
if tests.is_empty() {
println!("no tests found");
return Ok(());
}
let engram_url = std::env::var("ENGRAM_URL").ok().or_else(|| std::env::var("ENGRAM_DB_PATH").ok());
let url_ref = engram_url.as_deref();
let runner = TestRunner::new();
let results = if all {
runner.run_all(&tests, url_ref)
} else if e2e {
runner.run_e2e(&tests, url_ref.unwrap_or(""))
} else {
runner.run_unit(&tests)
};
let report = TestReport::from_results(results);
match output_fmt {
"json" => println!("{}", report.to_json()),
"junit" => println!("{}", report.to_junit_xml()),
_ => report.print(),
}
if !report.is_pass() {
std::process::exit(1);
}
Ok(())
}
/// Minimal interpreter for demonstration (no program args).
fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
run_interpreter_with_args(instructions, &[]);
}
// ── JSON / Value conversion helpers ──────────────────────────────────────────
/// Convert a serde_json Value to an el Value.
fn json_value_to_el_value(v: &serde_json::Value) -> el_compiler::Value {
use el_compiler::Value;
match v {
serde_json::Value::String(s) => Value::Str(s.clone()),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Value::Int(i)
} else if let Some(f) = n.as_f64() {
Value::Float(f)
} else {
Value::Str(n.to_string())
}
}
serde_json::Value::Bool(b) => Value::Bool(*b),
serde_json::Value::Null => Value::Nil,
serde_json::Value::Array(arr) => {
Value::List(arr.iter().map(json_value_to_el_value).collect())
}
serde_json::Value::Object(obj) => {
let fields = obj.iter()
.map(|(k, v)| (k.clone(), json_value_to_el_value(v)))
.collect();
Value::Struct { type_name: "Object".to_string(), fields }
}
}
}
/// Convert an el Value to a serde_json Value.
fn el_value_to_json_value(v: &el_compiler::Value) -> serde_json::Value {
use el_compiler::Value;
match v {
Value::Int(n) => serde_json::Value::Number((*n).into()),
Value::Float(f) => serde_json::Number::from_f64(*f)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null),
Value::Str(s) => serde_json::Value::String(s.clone()),
Value::Bool(b) => serde_json::Value::Bool(*b),
Value::Nil => serde_json::Value::Null,
Value::List(items) => {
serde_json::Value::Array(items.iter().map(el_value_to_json_value).collect())
}
Value::Map(pairs) => {
let mut map = serde_json::Map::new();
for (k, v) in pairs {
map.insert(k.clone(), el_value_to_json_value(v));
}
serde_json::Value::Object(map)
}
Value::ResultOk(inner) => {
serde_json::json!({"ok": el_value_to_json_value(inner)})
}
Value::ResultErr(inner) => {
serde_json::json!({"err": el_value_to_json_value(inner)})
}
Value::Struct { fields, .. } => {
let mut map = serde_json::Map::new();
for (k, v) in fields {
map.insert(k.clone(), el_value_to_json_value(v));
}
serde_json::Value::Object(map)
}
}
}
/// Call the Engram /search endpoint and return results as a Vec of el Values.
fn engram_activate_search(type_name: &str, query: &str) -> Vec<el_compiler::Value> {
use el_compiler::Value;
let engram_url = std::env::var("ENGRAM_URL")
.unwrap_or_else(|_| "http://localhost:8742".to_string());
let api_key = std::env::var("ENGRAM_API_KEY").unwrap_or_default();
let body = serde_json::json!({
"query": query,
"limit": 20
})
.to_string();
let client = reqwest::blocking::Client::new();
let mut req = client
.post(format!("{engram_url}/search"))
.header("Content-Type", "application/json")
.body(body);
if !api_key.is_empty() {
req = req.header("Authorization", format!("Bearer {api_key}"));
}
let result = req
.send()
.and_then(|r| r.json::<serde_json::Value>())
.ok();
let results: Vec<Value> = result
.and_then(|v| v.as_array().cloned())
.unwrap_or_default()
.iter()
.map(json_value_to_el_value)
.collect();
eprintln!(
"[activate] {type_name} where \"{query}\"{} results",
results.len()
);
results
}
/// Run a sub-interpreter starting at the given function entry point.
/// Returns the value left on the stack (the return value).
fn run_sub_interpreter(
instructions: &[el_compiler::Bytecode],
fn_table: &std::collections::HashMap<String, usize>,
entry: usize,
) -> el_compiler::Value {
run_sub_interpreter_with_stack(instructions, fn_table, entry, vec![])
}
fn run_sub_interpreter_with_stack(
instructions: &[el_compiler::Bytecode],
fn_table: &std::collections::HashMap<String, usize>,
entry: usize,
initial_stack: Vec<el_compiler::Value>,
) -> el_compiler::Value {
use el_compiler::{Bytecode, Value};
let mut stack: Vec<Value> = initial_stack;
let mut locals: std::collections::HashMap<String, Value> = std::collections::HashMap::new();
let mut call_stack: Vec<(usize, std::collections::HashMap<String, Value>)> = Vec::new();
let mut ip = entry;
let program_args: Vec<String> = vec![];
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::Int(x), Value::Float(y)) => Value::Float(x as f64 + y),
(Value::Float(x), Value::Int(y)) => Value::Float(x + y as f64),
_ => 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::Int(x), Value::Float(y)) => Value::Float(x as f64 - y),
(Value::Float(x), Value::Int(y)) => Value::Float(x - y as f64),
_ => 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::Int(x), Value::Float(y)) => Value::Float(x as f64 * y),
(Value::Float(x), Value::Int(y)) => Value::Float(x * y as f64),
_ => 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::Int(x), Value::Float(y)) => Value::Float(x as f64 / y),
(Value::Float(x), Value::Int(y)) => Value::Float(x / y as f64),
_ => 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::NotEq => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(a != b));
}
Bytecode::Lt => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(cmp_values(&a, &b) == std::cmp::Ordering::Less));
}
Bytecode::Gt => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(cmp_values(&a, &b) == std::cmp::Ordering::Greater));
}
Bytecode::LtEq => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(cmp_values(&a, &b) != std::cmp::Ordering::Greater));
}
Bytecode::GtEq => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(cmp_values(&a, &b) != std::cmp::Ordering::Less));
}
Bytecode::And => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(
matches!(a, Value::Bool(true)) && matches!(b, Value::Bool(true))
));
}
Bytecode::Or => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(
matches!(a, Value::Bool(true)) || matches!(b, Value::Bool(true))
));
}
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, arity } => {
let result = dispatch_builtin(name, *arity, &mut stack, &program_args);
match result {
BuiltinResult::Handled | BuiltinResult::HttpServe => {}
BuiltinResult::Exit(code) => std::process::exit(code),
BuiltinResult::NotBuiltin => {
if let Some(&entry) = fn_table.get(name.as_str()) {
let saved = locals.clone();
call_stack.push((ip + 1, saved));
ip = entry;
continue;
}
stack.push(Value::Nil);
}
}
}
Bytecode::GetField(field) => {
let obj = stack.pop().unwrap_or(Value::Nil);
let v = match &obj {
Value::Map(pairs) => pairs.iter()
.find(|(k, _)| k == field)
.map(|(_, v)| v.clone())
.unwrap_or(Value::Nil),
Value::Struct { fields, .. } => fields.iter()
.find(|(n, _)| n == field)
.map(|(_, v)| v.clone())
.unwrap_or(Value::Nil),
_ => Value::Nil,
};
stack.push(v);
}
Bytecode::GetIndex => {
let idx = stack.pop().unwrap_or(Value::Nil);
let obj = stack.pop().unwrap_or(Value::Nil);
match (obj, idx) {
(Value::List(items), Value::Int(i)) => {
let v = if i >= 0 && (i as usize) < items.len() {
items[i as usize].clone()
} else {
Value::Nil
};
stack.push(v);
}
(Value::Map(pairs), Value::Str(key)) => {
let v = pairs.iter()
.find(|(k, _)| k == &key)
.map(|(_, v)| v.clone())
.unwrap_or(Value::Nil);
stack.push(v);
}
_ => stack.push(Value::Nil),
}
}
Bytecode::BuildMap(n) => {
let mut pairs = Vec::new();
for _ in 0..*n {
let val = stack.pop().unwrap_or(Value::Nil);
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
pairs.push((key, val));
}
pairs.reverse();
stack.push(Value::Map(pairs));
}
Bytecode::BuildStruct { type_name, fields } => {
let n = fields.len();
let mut field_values: Vec<Value> = (0..n).map(|_| stack.pop().unwrap_or(Value::Nil)).collect();
field_values.reverse();
let struct_fields: Vec<(String, Value)> = fields.iter().cloned()
.zip(field_values.into_iter())
.collect();
stack.push(Value::Struct {
type_name: type_name.clone(),
fields: struct_fields,
});
}
Bytecode::SetField(field) => {
let val = stack.pop().unwrap_or(Value::Nil);
if let Some(Value::Map(pairs)) = stack.last_mut() {
if let Some(entry) = pairs.iter_mut().find(|(k, _)| k == field) {
entry.1 = val;
} else {
pairs.push((field.clone(), val));
}
} else if let Some(Value::Struct { fields, .. }) = stack.last_mut() {
if let Some(entry) = fields.iter_mut().find(|(k, _)| k == field) {
entry.1 = val;
} else {
fields.push((field.clone(), val));
}
}
}
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 => {
if call_stack.is_empty() {
// Return from handle_request — value is on stack
break;
}
if let Some((ret_ip, saved_locals)) = call_stack.pop() {
locals = saved_locals;
ip = ret_ip;
continue;
}
}
Bytecode::Halt => break,
Bytecode::Activate { type_name, query } => {
let results = engram_activate_search(type_name, query);
stack.push(Value::List(results));
}
_ => {}
}
ip += 1;
}
stack.pop().unwrap_or(Value::Nil)
}
/// Interpreter with program args — the args() builtin returns these.
fn run_interpreter_with_args(instructions: &[el_compiler::Bytecode], program_args: &[String]) {
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;
// Build a call table: fn name → bytecode offset.
// We populate this by scanning for __fn_<name> stores first.
let mut fn_table: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
// We need a two-pass approach: scan for function entry points then execute.
// The codegen emits: Jump(skip) [body...] Push(Int(entry)) StoreLocal(__fn_name)
// We pre-scan to build the table.
{
let mut scan_ip = 0usize;
let mut scan_locals: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
while scan_ip < instructions.len() {
match &instructions[scan_ip] {
Bytecode::Push(Value::Int(n)) => {
// could be a function entry point — remember it
if scan_ip + 1 < instructions.len() {
if let Bytecode::StoreLocal(name) = &instructions[scan_ip + 1] {
if let Some(fn_name) = name.strip_prefix("__fn_") {
fn_table.insert(fn_name.to_string(), *n as usize);
}
scan_locals.insert(name.clone(), *n);
}
}
}
_ => {}
}
scan_ip += 1;
}
}
// Store instructions and fn_table in thread-locals so http_serve can call
// handle_request via a sub-interpreter invocation.
let arc_instructions = std::sync::Arc::new(instructions.to_vec());
let arc_fn_table = std::sync::Arc::new(fn_table.clone());
let arc_instructions_clone = arc_instructions.clone();
let arc_fn_table_clone = arc_fn_table.clone();
SERVE_INSTRUCTIONS.with(|si| *si.borrow_mut() = Some(arc_instructions.clone()));
SERVE_FN_TABLE.with(|sf| *sf.borrow_mut() = Some(arc_fn_table.clone()));
// Set up the http_serve callback that calls handle_request(method, path, body)
HTTP_SERVE_CALL.with(|f| {
*f.borrow_mut() = Some(Box::new(move || {
// Load method, path, body from global state (set by http_serve before calling)
let (method, path, body) = GLOBAL_STATE.with(|gs| {
let s = gs.borrow();
(
s.get("__method__").cloned().unwrap_or_default(),
s.get("__path__").cloned().unwrap_or_default(),
s.get("__request__").cloned().unwrap_or_default(),
)
});
// Run a sub-interpreter starting at handle_request with args on stack
if let Some(entry) = arc_fn_table_clone.get("handle_request") {
// Push args in order: method, path, body (they'll be stored via StoreLocal params)
let initial_stack = vec![
el_compiler::Value::Str(method),
el_compiler::Value::Str(path),
el_compiler::Value::Str(body),
];
let result = run_sub_interpreter_with_stack(
&arc_instructions_clone,
&arc_fn_table_clone,
*entry,
initial_stack,
);
// Store the result as __response__ in global state
let response = match result {
el_compiler::Value::Str(s) => s,
other => other.to_string(),
};
GLOBAL_STATE.with(|gs| {
gs.borrow_mut().insert("__response__".to_string(), response);
});
} else {
GLOBAL_STATE.with(|gs| {
gs.borrow_mut().insert(
"__response__".to_string(),
r#"{"error":"handle_request function not found"}"#.to_string(),
);
});
}
}));
});
// Call stack for user-defined function calls: (return_ip, saved_locals)
let mut call_stack: Vec<(usize, std::collections::HashMap<String, Value>)> = Vec::new();
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::NotEq => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(a != b));
}
Bytecode::Lt => {
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::Bool(x < y),
(Value::Float(x), Value::Float(y)) => Value::Bool(x < y),
_ => Value::Bool(false),
});
}
Bytecode::Gt => {
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::Bool(x > y),
(Value::Float(x), Value::Float(y)) => Value::Bool(x > y),
_ => Value::Bool(false),
});
}
Bytecode::LtEq => {
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::Bool(x <= y),
(Value::Float(x), Value::Float(y)) => Value::Bool(x <= y),
_ => Value::Bool(false),
});
}
Bytecode::GtEq => {
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::Bool(x >= y),
(Value::Float(x), Value::Float(y)) => Value::Bool(x >= y),
_ => Value::Bool(false),
});
}
Bytecode::And => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(
matches!(a, Value::Bool(true)) && matches!(b, Value::Bool(true))
));
}
Bytecode::Or => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(
matches!(a, Value::Bool(true)) || matches!(b, Value::Bool(true))
));
}
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, arity } => {
let result = dispatch_builtin(name, *arity, &mut stack, program_args);
match result {
BuiltinResult::Handled | BuiltinResult::HttpServe => {}
BuiltinResult::Exit(code) => std::process::exit(code),
BuiltinResult::NotBuiltin => {
// Try user-defined function
if let Some(&entry) = fn_table.get(name.as_str()) {
// Save current locals and return address
let saved = locals.clone();
call_stack.push((ip + 1, saved));
ip = entry;
continue;
}
// Unknown — push Nil
stack.push(Value::Nil);
}
}
}
Bytecode::GetField(field) => {
let obj = stack.pop().unwrap_or(Value::Nil);
let v = match obj {
Value::Map(pairs) => pairs.iter()
.find(|(k, _)| k == field)
.map(|(_, v)| v.clone())
.unwrap_or(Value::Nil),
_ => Value::Nil,
};
stack.push(v);
}
Bytecode::GetIndex => {
let idx = stack.pop().unwrap_or(Value::Nil);
let obj = stack.pop().unwrap_or(Value::Nil);
match (obj, idx) {
(Value::List(items), Value::Int(i)) => {
let v = if i >= 0 && (i as usize) < items.len() {
items[i as usize].clone()
} else {
Value::Nil
};
stack.push(v);
}
(Value::Map(pairs), Value::Str(key)) => {
let v = pairs.iter()
.find(|(k, _)| k == &key)
.map(|(_, v)| v.clone())
.unwrap_or(Value::Nil);
stack.push(v);
}
(Value::Str(s), Value::Int(i)) => {
let c = s.chars().nth(i as usize)
.map(|c| Value::Str(c.to_string()))
.unwrap_or(Value::Nil);
stack.push(c);
}
_ => stack.push(Value::Nil),
}
}
Bytecode::BuildMap(n) => {
let mut pairs = Vec::new();
for _ in 0..*n {
let val = stack.pop().unwrap_or(Value::Nil);
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
pairs.push((key, val));
}
pairs.reverse();
stack.push(Value::Map(pairs));
}
Bytecode::BuildStruct { fields, .. } => {
let mut pairs = Vec::new();
for field in fields.iter().rev() {
let val = stack.pop().unwrap_or(Value::Nil);
pairs.push((field.clone(), val));
}
pairs.reverse();
stack.push(Value::Map(pairs));
}
Bytecode::SetField(field) => {
let val = stack.pop().unwrap_or(Value::Nil);
if let Some(Value::Map(pairs)) = stack.last_mut() {
if let Some(entry) = pairs.iter_mut().find(|(k, _)| k == field) {
entry.1 = val;
} else {
pairs.push((field.clone(), val));
}
}
}
Bytecode::Activate { type_name, query } => {
let results = engram_activate_search(type_name, query);
stack.push(Value::List(results));
}
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 => {
// Return from a user function call
if let Some((ret_ip, saved_locals)) = call_stack.pop() {
locals = saved_locals;
ip = ret_ip;
continue;
} else {
break;
}
}
Bytecode::Halt => break,
Bytecode::SealedBegin => {}
Bytecode::SealedEnd => {}
Bytecode::Nop => {}
Bytecode::Reason { query } => {
let text = soma_reason(query);
stack.push(Value::Str(text));
}
Bytecode::Parallel { entries } => {
// Spawn one thread per entry, collect into Map
let instructions_arc = std::sync::Arc::new(instructions.to_vec());
let fn_table_arc = std::sync::Arc::new(fn_table.clone());
let locals_arc = std::sync::Arc::new(locals.clone());
let mut handles: Vec<(String, std::thread::JoinHandle<Value>)> = Vec::new();
for (name, entry_ip) in entries {
let instr_clone = instructions_arc.clone();
let ft_clone = fn_table_arc.clone();
let ep = *entry_ip;
let h = std::thread::spawn(move || {
run_sub_interpreter(&instr_clone, &ft_clone, ep)
});
handles.push((name.clone(), h));
}
let mut pairs = Vec::new();
for (name, h) in handles {
let v = h.join().unwrap_or(Value::Nil);
pairs.push((name, v));
}
stack.push(Value::Map(pairs));
}
Bytecode::TraceBegin { label } => {
let start_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
locals.insert(format!("__trace_start_{label}__"), Value::Int(start_ms as i64));
}
Bytecode::TraceEnd { label } => {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
let start_ms = match locals.get(&format!("__trace_start_{label}__")) {
Some(Value::Int(n)) => *n as u128,
_ => now_ms,
};
let elapsed = now_ms.saturating_sub(start_ms);
eprintln!("[trace] {label}: {elapsed}ms");
}
Bytecode::ContractCheck { message } => {
let cond = stack.pop().unwrap_or(Value::Nil);
if !matches!(cond, Value::Bool(true)) {
eprintln!("contract violation: {message}");
std::process::exit(1);
}
}
Bytecode::DeployFn { fn_name, route, target } => {
let soma_url = std::env::var("SOMA_URL")
.unwrap_or_else(|_| "https://neuron.neurontechnologies.ai".into());
let op_key = std::env::var("SOMA_OPERATOR_KEY").unwrap_or_default();
let body = serde_json::json!({
"fn_name": fn_name,
"route": route,
"target": target,
});
let resp = reqwest::blocking::Client::new()
.post(format!("{soma_url}/v1/deploy"))
.header("Authorization", format!("Bearer {op_key}"))
.json(&body)
.send()
.and_then(|r| r.text())
.unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}"));
eprintln!("[deploy] {fn_name} -> {route} via {target}: {resp}");
stack.push(Value::Str(resp));
}
_ => {}
}
ip += 1;
}
}
/// Call soma AI inference endpoint and return response text.
fn soma_reason(query: &str) -> String {
let soma_url = std::env::var("SOMA_URL")
.unwrap_or_else(|_| "https://neuron.neurontechnologies.ai".into());
let op_key = std::env::var("SOMA_OPERATOR_KEY").unwrap_or_default();
let body = serde_json::json!({
"model": "neuron",
"messages": [{"role": "user", "content": query}],
"max_tokens": 500
});
let resp = reqwest::blocking::Client::new()
.post(format!("{soma_url}/v1/chat/completions"))
.header("Authorization", format!("Bearer {op_key}"))
.json(&body)
.send()
.and_then(|r| r.json::<serde_json::Value>())
.ok();
resp.and_then(|v| v["choices"][0]["message"]["content"].as_str().map(|s| s.to_string()))
.unwrap_or_else(|| "[soma unavailable]".into())
}
enum BuiltinResult {
Handled,
Exit(i32),
NotBuiltin,
/// http_serve was called — the interpreter should treat this like Handled
/// but the actual serve loop is blocking inside dispatch_builtin.
HttpServe,
}
fn dispatch_builtin(
name: &str,
_arity: u32,
stack: &mut Vec<el_compiler::Value>,
program_args: &[String],
) -> BuiltinResult {
use el_compiler::Value;
match name {
"print" => {
let v = stack.pop().unwrap_or(Value::Nil);
print!("{v}");
stack.push(Value::Nil);
BuiltinResult::Handled
}
"println" => {
let v = stack.pop().unwrap_or(Value::Nil);
println!("{v}");
stack.push(Value::Nil);
BuiltinResult::Handled
}
"log" => {
let v = stack.pop().unwrap_or(Value::Nil);
println!("{v}");
stack.push(Value::Nil);
BuiltinResult::Handled
}
"print_err" => {
let v = stack.pop().unwrap_or(Value::Nil);
eprintln!("{v}");
stack.push(Value::Nil);
BuiltinResult::Handled
}
"args" => {
let list = program_args.iter()
.map(|s| Value::Str(s.clone()))
.collect();
stack.push(Value::List(list));
BuiltinResult::Handled
}
"cwd" => {
let path = std::env::current_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| ".".to_string());
stack.push(Value::Str(path));
BuiltinResult::Handled
}
"env" => {
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
// Return empty string when env var is not set (so `env("X") == ""` works)
let val = std::env::var(&key)
.map(Value::Str)
.unwrap_or_else(|_| Value::Str(String::new()));
stack.push(val);
BuiltinResult::Handled
}
"http_post" => {
let body = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let url = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = reqwest::blocking::Client::new()
.post(&url)
.header("Content-Type", "application/json")
.header("X-NC-CLI", "true")
.body(body)
.send()
.and_then(|r| r.text())
.unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}"));
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"http_get" => {
let url = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = reqwest::blocking::get(&url)
.and_then(|r| r.text())
.unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}"));
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"exit" => {
let code = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as i32,
_ => 0,
};
BuiltinResult::Exit(code)
}
"str_contains" => {
let needle = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let haystack = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Bool(haystack.contains(needle.as_str())));
BuiltinResult::Handled
}
"str_starts_with" => {
let prefix = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Bool(s.starts_with(prefix.as_str())));
BuiltinResult::Handled
}
"str_split" => {
let delim = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let parts: Vec<Value> = s.split(delim.as_str())
.map(|p| Value::Str(p.to_string()))
.collect();
stack.push(Value::List(parts));
BuiltinResult::Handled
}
"list_len" => {
let list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
stack.push(Value::Int(list.len() as i64));
BuiltinResult::Handled
}
"list_get" => {
let idx = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n,
_ => -1,
};
let list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
let v = if idx >= 0 && (idx as usize) < list.len() {
list[idx as usize].clone()
} else {
Value::Nil
};
stack.push(v);
BuiltinResult::Handled
}
"int_to_str" => {
let n = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n,
_ => 0,
};
stack.push(Value::Str(n.to_string()));
BuiltinResult::Handled
}
"str_eq" => {
let b = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let a = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Bool(a == b));
BuiltinResult::Handled
}
"json_get" => {
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let json_str = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let val = serde_json::from_str::<serde_json::Value>(&json_str)
.ok()
.and_then(|v| v.get(&key).cloned())
.map(|v| match v {
serde_json::Value::String(s) => Value::Str(s),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Value::Int(i)
} else {
Value::Str(n.to_string())
}
}
serde_json::Value::Bool(b) => Value::Bool(b),
serde_json::Value::Null => Value::Nil,
other => Value::Str(other.to_string()),
})
.unwrap_or(Value::Nil);
stack.push(val);
BuiltinResult::Handled
}
"__build_list__" => {
// Already handled inline by codegen for array literals — no-op here
// The arity items are on the stack; we collect them into a list.
// But arity is already popped. We push Nil as fallback.
// In practice, array literals push items then call __build_list__(arity).
// We need to pop `arity` items and build a list.
// arity was passed but we only have `_arity` here — use it.
let mut items = Vec::new();
for _ in 0.._arity {
items.push(stack.pop().unwrap_or(Value::Nil));
}
items.reverse();
stack.push(Value::List(items));
BuiltinResult::Handled
}
// ── Filesystem builtins ───────────────────────────────────────────────
"fs_read" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => { stack.push(Value::Nil); return BuiltinResult::Handled; }
};
let result = std::fs::read_to_string(&path)
.map(Value::Str)
.unwrap_or(Value::Nil);
stack.push(result);
BuiltinResult::Handled
}
"fs_write" => {
let content = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let ok = std::fs::write(&path, &content).is_ok();
stack.push(Value::Bool(ok));
BuiltinResult::Handled
}
"fs_append" => {
let content = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
use std::io::Write;
let ok = std::fs::OpenOptions::new()
.create(true).append(true).open(&path)
.and_then(|mut f| f.write_all(content.as_bytes()))
.is_ok();
stack.push(Value::Bool(ok));
BuiltinResult::Handled
}
"fs_exists" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Bool(std::path::Path::new(&path).exists()));
BuiltinResult::Handled
}
"fs_mkdir" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let ok = std::fs::create_dir_all(&path).is_ok();
stack.push(Value::Bool(ok));
BuiltinResult::Handled
}
"fs_list" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let entries = std::fs::read_dir(&path)
.map(|rd| {
rd.filter_map(|e| {
e.ok().and_then(|e| {
e.file_name().into_string().ok().map(Value::Str)
})
}).collect::<Vec<_>>()
})
.unwrap_or_default();
stack.push(Value::List(entries));
BuiltinResult::Handled
}
"fs_remove" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let p = std::path::Path::new(&path);
let ok = if p.is_dir() {
std::fs::remove_dir(p).is_ok()
} else {
std::fs::remove_file(p).is_ok()
};
stack.push(Value::Bool(ok));
BuiltinResult::Handled
}
"fs_is_dir" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Bool(std::path::Path::new(&path).is_dir()));
BuiltinResult::Handled
}
"path_join" => {
let name = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let base = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let joined = std::path::Path::new(&base).join(&name);
stack.push(Value::Str(joined.to_string_lossy().to_string()));
BuiltinResult::Handled
}
"path_parent" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let parent = std::path::Path::new(&path)
.parent()
.map(|p| Value::Str(p.to_string_lossy().to_string()))
.unwrap_or(Value::Nil);
stack.push(parent);
BuiltinResult::Handled
}
// ── Filesystem recursive list ─────────────────────────────────────────
"fs_list_recursive" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let skip_dirs = ["node_modules", ".git", ".nc", "target", "__pycache__", ".el"];
let mut files = Vec::new();
if let Ok(walker) = walkdir::WalkDir::new(&path).into_iter().collect::<Result<Vec<_>, _>>() {
for entry in walker {
// Skip hidden/build dirs
let should_skip = entry.path().components().any(|c| {
let s = c.as_os_str().to_string_lossy();
skip_dirs.iter().any(|d| s == *d)
});
if should_skip && entry.path() != std::path::Path::new(&path) {
continue;
}
if entry.file_type().is_file() {
files.push(Value::Str(entry.path().to_string_lossy().to_string()));
}
}
}
stack.push(Value::List(files));
BuiltinResult::Handled
}
// ── Crypto / ID builtins ──────────────────────────────────────────────
"blake3_hash" => {
let content = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let hash = blake3::hash(content.as_bytes());
stack.push(Value::Str(hash.to_hex().to_string()));
BuiltinResult::Handled
}
"uuid_new" => {
let id = uuid::Uuid::new_v4().to_string();
stack.push(Value::Str(id));
BuiltinResult::Handled
}
"now_millis" => {
let ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
stack.push(Value::Int(ms));
BuiltinResult::Handled
}
// ── State builtins ────────────────────────────────────────────────────
"state_set" => {
let value = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
GLOBAL_STATE.with(|gs| gs.borrow_mut().insert(key, value));
stack.push(Value::Bool(true));
BuiltinResult::Handled
}
"state_get" => {
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let val = GLOBAL_STATE.with(|gs| {
gs.borrow().get(&key).cloned().map(Value::Str).unwrap_or(Value::Nil)
});
stack.push(val);
BuiltinResult::Handled
}
"state_del" => {
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
GLOBAL_STATE.with(|gs| gs.borrow_mut().remove(&key));
stack.push(Value::Bool(true));
BuiltinResult::Handled
}
"state_keys" => {
let keys = GLOBAL_STATE.with(|gs| {
gs.borrow().keys().cloned().map(Value::Str).collect::<Vec<_>>()
});
stack.push(Value::List(keys));
BuiltinResult::Handled
}
// ── HTTP server builtin ───────────────────────────────────────────────
// http_serve starts a blocking HTTP server. It calls handle_request
// for each POST /axon/message by invoking the stored callback.
"http_serve" => {
// http_serve(port) — general-purpose HTTP server.
// Passes every request to the Engram `handle_request(method, path, body)` function.
// The legacy /axon/message route is preserved for Neuron compatibility.
// ── Waitlist rate limiter (max 3 per IP per 10 min) ──────────────────
use std::sync::{OnceLock, Mutex};
static WAITLIST_RATE_MAP: OnceLock<Mutex<std::collections::HashMap<String, Vec<u64>>>> =
OnceLock::new();
let waitlist_rate_map = WAITLIST_RATE_MAP
.get_or_init(|| Mutex::new(std::collections::HashMap::new()));
let port_val = stack.pop().unwrap_or(Value::Nil);
let port = match port_val {
Value::Int(n) => n as u16,
Value::Str(s) => s.parse::<u16>().unwrap_or(7890),
_ => 7890,
};
let addr = format!("0.0.0.0:{port}");
let server = tiny_http::Server::http(&addr)
.unwrap_or_else(|e| panic!("cannot bind to {addr}: {e}"));
println!("soma-license · http://localhost:{port}");
for mut request in server.incoming_requests() {
let method = request.method().to_string();
let url = request.url().to_string();
// Strip query string for path matching
let path = url.split('?').next().unwrap_or(&url).to_string();
// Read body for all requests
let mut body = String::new();
{
use std::io::Read;
let _ = request.as_reader().read_to_string(&mut body);
}
// ── Built-in landing page routes ─────────────────────────────
// These short-circuit before invoking the Engram handle_request.
let cors_origin = "Access-Control-Allow-Origin: *".parse::<tiny_http::Header>().unwrap();
let content_json = "Content-Type: application/json".parse::<tiny_http::Header>().unwrap();
let content_html = "Content-Type: text/html; charset=utf-8".parse::<tiny_http::Header>().unwrap();
// OPTIONS — CORS preflight
if method == "OPTIONS" {
let _ = request.respond(
tiny_http::Response::from_string("")
.with_status_code(204)
.with_header(cors_origin)
.with_header("Access-Control-Allow-Methods: GET, POST, OPTIONS".parse::<tiny_http::Header>().unwrap())
.with_header("Access-Control-Allow-Headers: Content-Type".parse::<tiny_http::Header>().unwrap())
);
continue;
}
// GET / — serve __html_file__ if set, otherwise dashboard
if method == "GET" && (path == "/" || path == "/index.html") {
let html_path = GLOBAL_STATE.with(|gs| gs.borrow().get("__html_file__").cloned());
let html = if let Some(p) = html_path {
std::fs::read_to_string(&p)
.unwrap_or_else(|_| include_str!("dashboard.html").to_string())
} else {
include_str!("dashboard.html").to_string()
};
let _ = request.respond(
tiny_http::Response::from_string(html)
.with_header(content_html)
);
continue;
}
// GET /health
if method == "GET" && path == "/health" {
let _ = request.respond(
tiny_http::Response::from_string(r#"{"status":"ok"}"#)
.with_header(content_json)
);
continue;
}
// POST /api/chat — proxy to Neuron runtime (SSE → collect → JSON)
// Transforms landing page format { message, history, conv_id }
// into runtime format { messages: [{role,content}], conv_id }
if method == "POST" && path == "/api/chat" {
let runtime_url = std::env::var("NEURON_RUNTIME_URL")
.unwrap_or_else(|_| "http://localhost:4444".to_string());
let chat_url = format!("{}/api/chat", runtime_url);
let result: Result<String, String> = (|| {
// Transform body format
let runtime_body = if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
if v.get("message").is_some() {
// Landing page format → runtime format
let msg = v["message"].as_str().unwrap_or("").to_string();
let history = v["history"].as_array().cloned().unwrap_or_default();
let conv_id_val = v.get("conv_id").cloned().unwrap_or(serde_json::Value::Null);
let mut messages = history;
messages.push(serde_json::json!({"role":"user","content":msg}));
let mut payload = serde_json::json!({"messages": messages});
if !conv_id_val.is_null() {
payload["conv_id"] = conv_id_val;
}
payload.to_string()
} else {
body.clone() // already in runtime format
}
} else {
body.clone()
};
let resp = reqwest::blocking::Client::new()
.post(&chat_url)
.header("Content-Type", "application/json")
.body(runtime_body)
.send()
.map_err(|e| e.to_string())?;
use std::io::BufRead;
let mut reply = String::new();
let mut conv_id = String::new();
for line in resp.text().map_err(|e| e.to_string())?.lines() {
if line.starts_with("data: ") {
let data = &line[6..];
if data == "[DONE]" { break; }
if let Ok(v) = serde_json::from_str::<serde_json::Value>(data) {
if let Some(d) = v.get("delta").and_then(|x| x.as_str()) {
reply.push_str(d);
}
if let Some(c) = v.get("conv_id").and_then(|x| x.as_str()) {
conv_id = c.to_string();
}
}
}
}
let out = serde_json::json!({"reply": reply, "conv_id": conv_id});
Ok(out.to_string())
})();
let (status, resp_body) = match result {
Ok(r) => (200u16, r),
Err(e) => (502, format!(r#"{{"error":"runtime unavailable: {}"}}"#, e)),
};
let _ = request.respond(
tiny_http::Response::from_string(resp_body)
.with_status_code(status)
.with_header(content_json)
.with_header(cors_origin)
);
continue;
}
// POST /api/email — send link via Resend
if method == "POST" && path == "/api/email" {
let resend_key = std::env::var("RESEND_API_KEY").unwrap_or_default();
let result: Result<(), String> = (|| {
let v: serde_json::Value = serde_json::from_str(&body)
.map_err(|e| e.to_string())?;
let email = v["email"].as_str().unwrap_or("").to_string();
let name = v["name"].as_str().unwrap_or("there").to_string();
let conv_id = v["conv_id"].as_str().unwrap_or("").to_string();
let base_url = v["return_url"].as_str().unwrap_or("").to_string();
if email.is_empty() { return Err("no email".to_string()); }
let link = if conv_id.is_empty() {
base_url.clone()
} else {
format!("{}?cid={}", base_url, conv_id)
};
let html_body = format!(
r#"<div style="font-family:Georgia,serif;max-width:480px;margin:0 auto;padding:40px 20px">
<p style="font-size:1.1rem;line-height:1.6">Hey {} &mdash;</p>
<p style="font-size:1.1rem;line-height:1.6">It's Neuron. You left our conversation in the middle.</p>
<p style="font-size:1.1rem;line-height:1.6">I remember where we were.</p>
<p style="margin:32px 0">
<a href="{}" style="background:#0052A0;color:#fff;padding:12px 24px;text-decoration:none;border-radius:4px;font-family:sans-serif">Come back when you're ready</a>
</p>
<p style="color:#888;font-size:0.8rem;font-family:sans-serif">That link brings you right back to where we left off.</p>
</div>"#,
name, link
);
let payload = serde_json::json!({
"from": "Neuron <neuron@neurontechnologies.ai>",
"to": [email],
"subject": format!("Hey {} \u{2014} come back when you\u{2019}re ready", name),
"html": html_body
});
reqwest::blocking::Client::new()
.post("https://api.resend.com/emails")
.header("Authorization", format!("Bearer {}", resend_key))
.header("Content-Type", "application/json")
.body(payload.to_string())
.send()
.map_err(|e| e.to_string())?;
Ok(())
})();
let (status, resp_body) = match result {
Ok(()) => (200u16, r#"{"ok":true}"#.to_string()),
Err(e) => (500, format!(r#"{{"error":"{}"}}"#, e)),
};
let _ = request.respond(
tiny_http::Response::from_string(resp_body)
.with_status_code(status)
.with_header(content_json)
.with_header(cors_origin)
);
continue;
}
// POST /api/waitlist — honeypot + rate limit + HMAC confirm email
if method == "POST" && path == "/api/waitlist" {
// ── IP rate limit: max 3 per IP per 10 minutes ──
let ip = request.remote_addr()
.map(|a| a.ip().to_string())
.unwrap_or_else(|| "unknown".to_string());
let now_ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let rate_ok = {
let mut map = waitlist_rate_map.lock()
.unwrap_or_else(|e| e.into_inner());
let entry = map.entry(ip).or_insert_with(Vec::new);
entry.retain(|&ts| now_ts.saturating_sub(ts) < 600);
if entry.len() < 3 { entry.push(now_ts); true } else { false }
};
if !rate_ok {
let _ = request.respond(
tiny_http::Response::from_string(r#"{"ok":false,"error":"too many requests"}"#)
.with_status_code(429u16)
.with_header(content_json)
.with_header(cors_origin)
);
continue;
}
let resend_key = std::env::var("RESEND_API_KEY").unwrap_or_default();
let waitlist_secret = std::env::var("WAITLIST_SECRET")
.unwrap_or_else(|_| "neuron-waitlist-dev-secret".to_string());
let result: Result<(), String> = (|| {
let v: serde_json::Value = serde_json::from_str(&body)
.map_err(|e| e.to_string())?;
let email = v["email"].as_str().unwrap_or("").to_string();
let name = v["name"].as_str().unwrap_or("there").to_string();
let conv_id = v["conv_id"].as_str().unwrap_or("").to_string();
let base_url = v["base_url"].as_str().unwrap_or("").to_string();
if email.is_empty() { return Err("no email".to_string()); }
// ── Honeypot: bots fill this, humans don't ──
if !v["hp"].as_str().unwrap_or("").is_empty() {
// Silently accept but don't process — don't tell bots they failed
return Ok(());
}
// ── Generate HMAC-signed confirmation token ──
use hmac::{Hmac, Mac};
use sha2::Sha256;
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let raw = format!("{}|{}|{}", email, name, ts);
let payload = URL_SAFE_NO_PAD.encode(raw.as_bytes());
let mut mac = Hmac::<Sha256>::new_from_slice(waitlist_secret.as_bytes())
.map_err(|e| e.to_string())?;
mac.update(payload.as_bytes());
let sig = hex::encode(mac.finalize().into_bytes());
let token = format!("{}.{}", payload, sig);
// ── Build confirm link ──
let confirm_link = if conv_id.is_empty() {
format!("{}?confirm={}", base_url, token)
} else {
format!("{}?confirm={}&cid={}", base_url, token, conv_id)
};
if resend_key.is_empty() {
// Dev mode — no email sent, just log
eprintln!("[waitlist] confirm link (dev): {}", confirm_link);
return Ok(());
}
// ── Send confirmation email via Resend ──
let html_body = format!(
r#"<div style="font-family:Georgia,serif;max-width:480px;margin:0 auto;padding:40px 20px;color:#0D0D14">
<p style="font-size:1.1rem;line-height:1.6;margin-bottom:1.5rem">Hey {} &mdash;</p>
<p style="font-size:1rem;line-height:1.75;color:#3A3A4A;margin-bottom:1rem">
One click to confirm your spot on the Neuron waitlist.
</p>
<p style="margin:2rem 0">
<a href="{}" style="background:#0052A0;color:#fff;padding:14px 28px;text-decoration:none;border-radius:3px;font-family:sans-serif;font-size:0.95rem">
Confirm my spot &rarr;
</a>
</p>
<p style="color:#6B6B7E;font-size:0.8rem;font-family:sans-serif;margin-top:2rem;line-height:1.5">
This link expires in 24 hours. If you didn't sign up for Neuron, you can ignore this.
</p>
</div>"#,
name, confirm_link
);
let payload_json = serde_json::json!({
"from": "Neuron <neuron@neurontechnologies.ai>",
"to": [email],
"subject": format!("Confirm your spot, {}", name),
"html": html_body
});
reqwest::blocking::Client::new()
.post("https://api.resend.com/emails")
.header("Authorization", format!("Bearer {}", resend_key))
.header("Content-Type", "application/json")
.body(payload_json.to_string())
.send()
.map_err(|e| e.to_string())?;
Ok(())
})();
let (status, resp_body) = match result {
Ok(()) => (200u16, r#"{"ok":true}"#.to_string()),
Err(e) => (400, format!(r#"{{"ok":false,"error":"{}"}}"#, e)),
};
let _ = request.respond(
tiny_http::Response::from_string(resp_body)
.with_status_code(status)
.with_header(content_json)
.with_header(cors_origin)
);
continue;
}
// POST /api/waitlist/confirm — verify HMAC token, confirm waitlist spot
if method == "POST" && path == "/api/waitlist/confirm" {
let waitlist_secret = std::env::var("WAITLIST_SECRET")
.unwrap_or_else(|_| "neuron-waitlist-dev-secret".to_string());
let result: Result<(), String> = (|| {
use hmac::{Hmac, Mac};
use sha2::Sha256;
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
let v: serde_json::Value = serde_json::from_str(&body)
.map_err(|e| e.to_string())?;
let token = v["token"].as_str().unwrap_or("");
if token.is_empty() { return Err("missing token".to_string()); }
let parts: Vec<&str> = token.splitn(2, '.').collect();
if parts.len() != 2 { return Err("invalid token format".to_string()); }
let (payload, sig) = (parts[0], parts[1]);
// Verify HMAC
let mut mac = Hmac::<Sha256>::new_from_slice(waitlist_secret.as_bytes())
.map_err(|e| e.to_string())?;
mac.update(payload.as_bytes());
let expected = hex::encode(mac.finalize().into_bytes());
if sig != expected { return Err("invalid signature".to_string()); }
// Decode payload and check expiry (24h)
let raw_bytes = URL_SAFE_NO_PAD.decode(payload)
.map_err(|e| e.to_string())?;
let raw = String::from_utf8(raw_bytes)
.map_err(|e| e.to_string())?;
let fields: Vec<&str> = raw.splitn(3, '|').collect();
if fields.len() < 3 { return Err("invalid token payload".to_string()); }
let ts: u64 = fields[2].parse().map_err(|_| "invalid timestamp".to_string())?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
if now.saturating_sub(ts) > 86400 {
return Err("link expired".to_string());
}
// Log confirmed email
eprintln!("[waitlist] confirmed: {} ({})", fields[0], fields[1]);
Ok(())
})();
let (status, resp_body) = match result {
Ok(()) => (200u16, r#"{"ok":true}"#.to_string()),
Err(e) => (400, format!(r#"{{"ok":false,"error":"{}"}}"#, e)),
};
let _ = request.respond(
tiny_http::Response::from_string(resp_body)
.with_status_code(status)
.with_header(content_json)
.with_header(cors_origin)
);
continue;
}
// POST /api/remember — forward to Neuron runtime
if method == "POST" && path == "/api/remember" {
let runtime_url = std::env::var("NEURON_RUNTIME_URL")
.unwrap_or_else(|_| "http://localhost:4444".to_string());
let _ = reqwest::blocking::Client::new()
.post(format!("{}/api/remember", runtime_url))
.header("Content-Type", "application/json")
.body(body.clone())
.send();
let _ = request.respond(
tiny_http::Response::from_string(r#"{"ok":true}"#)
.with_header(content_json)
);
continue;
}
// ── End built-in routes — fall through to Engram handle_request ─
// Store method, path, body in global state so handle_request can read them
GLOBAL_STATE.with(|gs| {
let mut s = gs.borrow_mut();
s.insert("__method__".to_string(), method.clone());
s.insert("__path__".to_string(), path.clone());
s.insert("__request__".to_string(), body.clone());
s.remove("__response__");
});
// Call handle_request via the thread-local fn executor
HTTP_SERVE_CALL.with(|f| {
if let Some(ref call_fn) = *f.borrow() {
call_fn();
}
});
let response_body = GLOBAL_STATE.with(|gs| {
gs.borrow().get("__response__").cloned()
.unwrap_or_else(|| r#"{"error":"not found"}"#.to_string())
});
let _ = request.respond(
tiny_http::Response::from_string(response_body)
.with_header(content_json)
);
}
stack.push(Value::Nil);
BuiltinResult::Handled
}
// ── Process / thread builtins ─────────────────────────────────────────
"getpid" => {
stack.push(Value::Int(std::process::id() as i64));
BuiltinResult::Handled
}
"exec_bg" => {
let cmd_str = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => { stack.push(Value::Int(-1)); return BuiltinResult::Handled; }
};
let mut parts = cmd_str.split_whitespace();
let prog = match parts.next() {
Some(p) => p.to_string(),
None => { stack.push(Value::Int(-1)); return BuiltinResult::Handled; }
};
let args_vec: Vec<String> = parts.map(|s| s.to_string()).collect();
let pid = std::process::Command::new(&prog)
.args(&args_vec)
.spawn()
.map(|child| child.id() as i64)
.unwrap_or(-1);
stack.push(Value::Int(pid));
BuiltinResult::Handled
}
"spawn_thread" => {
let fn_name = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => { stack.push(Value::Nil); return BuiltinResult::Handled; }
};
let instr_arc = SERVE_INSTRUCTIONS.with(|si| si.borrow().clone());
let fn_arc = SERVE_FN_TABLE.with(|sf| sf.borrow().clone());
if let (Some(instr_arc), Some(fn_arc)) = (instr_arc, fn_arc) {
std::thread::spawn(move || {
if let Some(&entry) = fn_arc.get(&fn_name) {
run_sub_interpreter(&instr_arc, &fn_arc, entry);
}
});
}
stack.push(Value::Nil);
BuiltinResult::Handled
}
"sleep_secs" => {
let n = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as u64,
_ => 1,
};
std::thread::sleep(std::time::Duration::from_secs(n));
stack.push(Value::Nil);
BuiltinResult::Handled
}
// ── String utility builtins ───────────────────────────────────────────
"str_replace" => {
let to = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let from = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Str(s.replace(&from, &to)));
BuiltinResult::Handled
}
"str_to_lowercase" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Str(s.to_lowercase()));
BuiltinResult::Handled
}
"str_trim" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Str(s.trim().to_string()));
BuiltinResult::Handled
}
"str_index_of" => {
let sub = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let idx = s.find(&sub).map(|i| i as i64).unwrap_or(-1);
stack.push(Value::Int(idx));
BuiltinResult::Handled
}
"str_last_index_of" => {
let sub = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let idx = s.rfind(&sub).map(|i| i as i64).unwrap_or(-1);
stack.push(Value::Int(idx));
BuiltinResult::Handled
}
"str_slice" => {
let end = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as usize,
_ => 0,
};
let start = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as usize,
_ => 0,
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let chars: Vec<char> = s.chars().collect();
let start = start.min(chars.len());
let end = end.min(chars.len());
let slice: String = chars[start..end].iter().collect();
stack.push(Value::Str(slice));
BuiltinResult::Handled
}
"str_ends_with" => {
let suffix = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Bool(s.ends_with(suffix.as_str())));
BuiltinResult::Handled
}
// ── JSON utility builtins ─────────────────────────────────────────────
"json_set" => {
let value = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let json_str = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => "{}".to_string(),
};
let result = serde_json::from_str::<serde_json::Value>(&json_str)
.ok()
.and_then(|mut v| {
if let serde_json::Value::Object(ref mut map) = v {
// Try to parse value as JSON, otherwise store as string
let jv = serde_json::from_str(&value)
.unwrap_or(serde_json::Value::String(value.clone()));
map.insert(key.clone(), jv);
serde_json::to_string(&v).ok()
} else {
None
}
})
.unwrap_or_else(|| format!("{{\"{key}\":\"{value}\"}}"));
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"json_keys" => {
let json_str = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => "{}".to_string(),
};
let keys = serde_json::from_str::<serde_json::Value>(&json_str)
.ok()
.and_then(|v| {
if let serde_json::Value::Object(map) = v {
Some(map.keys().cloned().map(Value::Str).collect::<Vec<_>>())
} else {
None
}
})
.unwrap_or_default();
stack.push(Value::List(keys));
BuiltinResult::Handled
}
"json_array_push" => {
let item = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => "null".to_string(),
};
let json_str = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => "[]".to_string(),
};
let result = serde_json::from_str::<serde_json::Value>(&json_str)
.ok()
.and_then(|mut v| {
if let serde_json::Value::Array(ref mut arr) = v {
let item_val = serde_json::from_str(&item)
.unwrap_or(serde_json::Value::String(item.clone()));
arr.push(item_val);
serde_json::to_string(&v).ok()
} else {
None
}
})
.unwrap_or_else(|| format!("[{}]", item));
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"json_array_len" => {
let json_str = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => "[]".to_string(),
};
let len = serde_json::from_str::<serde_json::Value>(&json_str)
.ok()
.and_then(|v| if let serde_json::Value::Array(arr) = v { Some(arr.len() as i64) } else { None })
.unwrap_or(0);
stack.push(Value::Int(len));
BuiltinResult::Handled
}
"json_array_get" => {
let idx = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as usize,
_ => { stack.push(Value::Nil); return BuiltinResult::Handled; }
};
let json_str = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => "[]".to_string(),
};
let item = serde_json::from_str::<serde_json::Value>(&json_str)
.ok()
.and_then(|v| {
if let serde_json::Value::Array(arr) = v {
arr.get(idx).map(|item| Value::Str(item.to_string()))
} else {
None
}
})
.unwrap_or(Value::Nil);
stack.push(item);
BuiltinResult::Handled
}
"int_parse" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = s.trim().parse::<i64>()
.map(Value::Int)
.unwrap_or(Value::Nil);
stack.push(result);
BuiltinResult::Handled
}
"bool_to_str" => {
let b = match stack.pop().unwrap_or(Value::Nil) {
Value::Bool(b) => b,
_ => false,
};
stack.push(Value::Str(if b { "true".to_string() } else { "false".to_string() }));
BuiltinResult::Handled
}
"list_join" => {
let sep = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
let strs: Vec<String> = list.iter().map(|v| v.to_string()).collect();
stack.push(Value::Str(strs.join(&sep)));
BuiltinResult::Handled
}
// ── Array builtins ────────────────────────────────────────────────────
"array_length" | "array_len" => {
let list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
stack.push(Value::Int(list.len() as i64));
BuiltinResult::Handled
}
"array_push" => {
let val = stack.pop().unwrap_or(Value::Nil);
let mut list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
list.push(val);
stack.push(Value::List(list));
BuiltinResult::Handled
}
"array_pop" => {
let mut list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
let item = list.pop().unwrap_or(Value::Nil);
stack.push(Value::List(list));
stack.push(item);
BuiltinResult::Handled
}
"array_first" => {
let list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
stack.push(list.into_iter().next().unwrap_or(Value::Nil));
BuiltinResult::Handled
}
"array_last" => {
let list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
stack.push(list.into_iter().last().unwrap_or(Value::Nil));
BuiltinResult::Handled
}
"array_reverse" => {
let mut list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
list.reverse();
stack.push(Value::List(list));
BuiltinResult::Handled
}
"array_concat" => {
let b = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
let mut a = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
a.extend(b);
stack.push(Value::List(a));
BuiltinResult::Handled
}
"array_contains" => {
let val = stack.pop().unwrap_or(Value::Nil);
let list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
stack.push(Value::Bool(list.contains(&val)));
BuiltinResult::Handled
}
"array_slice" => {
let end = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as usize,
_ => 0,
};
let start = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as usize,
_ => 0,
};
let list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
let start = start.min(list.len());
let end = end.min(list.len());
stack.push(Value::List(list[start..end].to_vec()));
BuiltinResult::Handled
}
"array_join" => {
let sep = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
let strs: Vec<String> = list.iter().map(|v| v.to_string()).collect();
stack.push(Value::Str(strs.join(&sep)));
BuiltinResult::Handled
}
"array_sort" => {
let mut list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
list.sort_by(|a, b| match (a, b) {
(Value::Int(x), Value::Int(y)) => x.cmp(y),
(Value::Float(x), Value::Float(y)) => x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal),
(Value::Str(x), Value::Str(y)) => x.cmp(y),
_ => std::cmp::Ordering::Equal,
});
stack.push(Value::List(list));
BuiltinResult::Handled
}
"array_zip" => {
let b = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
let a = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
let zipped: Vec<Value> = a.into_iter().zip(b.into_iter())
.map(|(x, y)| Value::List(vec![x, y]))
.collect();
stack.push(Value::List(zipped));
BuiltinResult::Handled
}
"array_enumerate" => {
let list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
let enumerated: Vec<Value> = list.into_iter().enumerate()
.map(|(i, v)| Value::List(vec![Value::Int(i as i64), v]))
.collect();
stack.push(Value::List(enumerated));
BuiltinResult::Handled
}
// ── Math builtins ─────────────────────────────────────────────────────
"math_abs" => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(match v {
Value::Int(n) => Value::Int(n.abs()),
Value::Float(f) => Value::Float(f.abs()),
_ => Value::Nil,
});
BuiltinResult::Handled
}
"math_max" => {
let b = stack.pop().unwrap_or(Value::Nil);
let a = stack.pop().unwrap_or(Value::Nil);
stack.push(match (a, b) {
(Value::Int(x), Value::Int(y)) => Value::Int(x.max(y)),
(Value::Float(x), Value::Float(y)) => Value::Float(x.max(y)),
_ => Value::Nil,
});
BuiltinResult::Handled
}
"math_min" => {
let b = stack.pop().unwrap_or(Value::Nil);
let a = stack.pop().unwrap_or(Value::Nil);
stack.push(match (a, b) {
(Value::Int(x), Value::Int(y)) => Value::Int(x.min(y)),
(Value::Float(x), Value::Float(y)) => Value::Float(x.min(y)),
_ => Value::Nil,
});
BuiltinResult::Handled
}
"math_floor" => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(match v {
Value::Float(f) => Value::Int(f.floor() as i64),
Value::Int(n) => Value::Int(n),
_ => Value::Nil,
});
BuiltinResult::Handled
}
"math_ceil" => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(match v {
Value::Float(f) => Value::Int(f.ceil() as i64),
Value::Int(n) => Value::Int(n),
_ => Value::Nil,
});
BuiltinResult::Handled
}
"math_round" => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(match v {
Value::Float(f) => Value::Int(f.round() as i64),
Value::Int(n) => Value::Int(n),
_ => Value::Nil,
});
BuiltinResult::Handled
}
"math_sqrt" => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(match v {
Value::Float(f) => Value::Float(f.sqrt()),
Value::Int(n) => Value::Float((n as f64).sqrt()),
_ => Value::Nil,
});
BuiltinResult::Handled
}
"math_pow" => {
let exp = stack.pop().unwrap_or(Value::Nil);
let base = stack.pop().unwrap_or(Value::Nil);
let base_f = match base { Value::Float(f) => f, Value::Int(n) => n as f64, _ => 0.0 };
let exp_f = match exp { Value::Float(f) => f, Value::Int(n) => n as f64, _ => 0.0 };
stack.push(Value::Float(base_f.powf(exp_f)));
BuiltinResult::Handled
}
// ── String aliases and extras ─────────────────────────────────────────
"string_len" | "str_len" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Int(s.chars().count() as i64));
BuiltinResult::Handled
}
"string_trim" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Str(s.trim().to_string()));
BuiltinResult::Handled
}
"string_to_upper" | "str_upper" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Str(s.to_uppercase()));
BuiltinResult::Handled
}
"string_to_lower" | "str_lower" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Str(s.to_lowercase()));
BuiltinResult::Handled
}
"string_replace" => {
let replacement = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let pattern = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let source = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Str(source.replace(&pattern, &replacement)));
BuiltinResult::Handled
}
"string_index_of" => {
let needle = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let haystack = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let idx = haystack.find(&needle).map(|i| i as i64).unwrap_or(-1);
stack.push(Value::Int(idx));
BuiltinResult::Handled
}
"string_substring" => {
let end = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as usize,
_ => 0,
};
let start = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as usize,
_ => 0,
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let chars: Vec<char> = s.chars().collect();
let start = start.min(chars.len());
let end = end.min(chars.len());
stack.push(Value::Str(chars[start..end].iter().collect()));
BuiltinResult::Handled
}
"string_contains" => {
let needle = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let haystack = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Bool(haystack.contains(needle.as_str())));
BuiltinResult::Handled
}
"string_starts_with" => {
let prefix = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Bool(s.starts_with(prefix.as_str())));
BuiltinResult::Handled
}
"string_split" => {
let delim = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let parts: Vec<Value> = s.split(delim.as_str())
.map(|p| Value::Str(p.to_string()))
.collect();
stack.push(Value::List(parts));
BuiltinResult::Handled
}
"string_concat" => {
let b = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
let a = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
stack.push(Value::Str(a + &b));
BuiltinResult::Handled
}
"to_string" => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(Value::Str(v.to_string()));
BuiltinResult::Handled
}
"str_to_int" | "parse_int" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = s.trim().parse::<i64>().map(Value::Int).unwrap_or(Value::Nil);
stack.push(result);
BuiltinResult::Handled
}
"str_to_float" | "parse_float" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = s.trim().parse::<f64>().map(Value::Float).unwrap_or(Value::Nil);
stack.push(result);
BuiltinResult::Handled
}
// ── JSON native builtins ──────────────────────────────────────────────
"json_stringify" => {
let v = stack.pop().unwrap_or(Value::Nil);
let jv = el_value_to_json_value(&v);
let s = serde_json::to_string(&jv).unwrap_or_else(|_| "null".to_string());
stack.push(Value::Str(s));
BuiltinResult::Handled
}
"json_parse" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => "null".to_string(),
};
let jv: serde_json::Value = serde_json::from_str(&s).unwrap_or(serde_json::Value::Null);
stack.push(json_value_to_el_value(&jv));
BuiltinResult::Handled
}
// ── Map builtins ──────────────────────────────────────────────────────
"map_new" => {
stack.push(Value::Map(vec![]));
BuiltinResult::Handled
}
"map_get" => {
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
let map = match stack.pop().unwrap_or(Value::Nil) {
Value::Map(pairs) => pairs,
_ => vec![],
};
let v = map.iter().find(|(k, _)| k == &key).map(|(_, v)| v.clone()).unwrap_or(Value::Nil);
stack.push(v);
BuiltinResult::Handled
}
"map_set" => {
let val = stack.pop().unwrap_or(Value::Nil);
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
let mut pairs = match stack.pop().unwrap_or(Value::Nil) {
Value::Map(p) => p,
_ => vec![],
};
if let Some(entry) = pairs.iter_mut().find(|(k, _)| k == &key) {
entry.1 = val;
} else {
pairs.push((key, val));
}
stack.push(Value::Map(pairs));
BuiltinResult::Handled
}
"map_remove" => {
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
let pairs = match stack.pop().unwrap_or(Value::Nil) {
Value::Map(p) => p,
_ => vec![],
};
let new_pairs: Vec<_> = pairs.into_iter().filter(|(k, _)| k != &key).collect();
stack.push(Value::Map(new_pairs));
BuiltinResult::Handled
}
"map_contains" => {
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
let pairs = match stack.pop().unwrap_or(Value::Nil) {
Value::Map(p) => p,
_ => vec![],
};
stack.push(Value::Bool(pairs.iter().any(|(k, _)| k == &key)));
BuiltinResult::Handled
}
"map_keys" => {
let pairs = match stack.pop().unwrap_or(Value::Nil) {
Value::Map(p) => p,
_ => vec![],
};
let keys: Vec<Value> = pairs.into_iter().map(|(k, _)| Value::Str(k)).collect();
stack.push(Value::List(keys));
BuiltinResult::Handled
}
"map_values" => {
let pairs = match stack.pop().unwrap_or(Value::Nil) {
Value::Map(p) => p,
_ => vec![],
};
let vals: Vec<Value> = pairs.into_iter().map(|(_, v)| v).collect();
stack.push(Value::List(vals));
BuiltinResult::Handled
}
"map_len" => {
let pairs = match stack.pop().unwrap_or(Value::Nil) {
Value::Map(p) => p,
_ => vec![],
};
stack.push(Value::Int(pairs.len() as i64));
BuiltinResult::Handled
}
// ── Result builtins ───────────────────────────────────────────────────
"result_ok" => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(Value::ResultOk(Box::new(v)));
BuiltinResult::Handled
}
"result_err" => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(Value::ResultErr(Box::new(v)));
BuiltinResult::Handled
}
"result_is_ok" => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(Value::Bool(matches!(v, Value::ResultOk(_))));
BuiltinResult::Handled
}
"result_is_err" => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(Value::Bool(matches!(v, Value::ResultErr(_))));
BuiltinResult::Handled
}
"result_unwrap" => {
let v = stack.pop().unwrap_or(Value::Nil);
match v {
Value::ResultOk(inner) => stack.push(*inner),
Value::ResultErr(e) => panic!("result_unwrap called on Err: {e}"),
other => stack.push(other),
}
BuiltinResult::Handled
}
"result_unwrap_or" => {
let default = stack.pop().unwrap_or(Value::Nil);
let v = stack.pop().unwrap_or(Value::Nil);
match v {
Value::ResultOk(inner) => stack.push(*inner),
_ => stack.push(default),
}
BuiltinResult::Handled
}
// ── Optional builtins ─────────────────────────────────────────────────
"optional_some" => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(v);
BuiltinResult::Handled
}
"optional_none" => {
stack.push(Value::Nil);
BuiltinResult::Handled
}
"optional_is_some" => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(Value::Bool(!matches!(v, Value::Nil)));
BuiltinResult::Handled
}
"optional_is_none" => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(Value::Bool(matches!(v, Value::Nil)));
BuiltinResult::Handled
}
"optional_unwrap" => {
let v = stack.pop().unwrap_or(Value::Nil);
if matches!(v, Value::Nil) {
panic!("optional_unwrap called on None");
}
stack.push(v);
BuiltinResult::Handled
}
"optional_unwrap_or" => {
let default = stack.pop().unwrap_or(Value::Nil);
let v = stack.pop().unwrap_or(Value::Nil);
if matches!(v, Value::Nil) {
stack.push(default);
} else {
stack.push(v);
}
BuiltinResult::Handled
}
// ── HTTP extended builtins ────────────────────────────────────────────
"http_put" => {
let body = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let url = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = reqwest::blocking::Client::new()
.put(&url)
.header("Content-Type", "application/json")
.body(body)
.send()
.and_then(|r| r.text())
.unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}") );
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"http_delete" => {
let url = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = reqwest::blocking::Client::new()
.delete(&url)
.send()
.and_then(|r| r.text())
.unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}") );
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"http_patch" => {
let body = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let url = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = reqwest::blocking::Client::new()
.patch(&url)
.header("Content-Type", "application/json")
.body(body)
.send()
.and_then(|r| r.text())
.unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}") );
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"http_head" => {
let url = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = reqwest::blocking::Client::new()
.head(&url)
.send()
.map(|r| Value::Int(r.status().as_u16() as i64))
.unwrap_or(Value::Int(-1));
stack.push(result);
BuiltinResult::Handled
}
// ── Crypto / HMAC / base64 / uuid builtins ───────────────────────────
"hmac_sha256" => {
use hmac::{Hmac, Mac};
use sha2::Sha256;
let data = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let secret = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
.unwrap_or_else(|_| HmacSha256::new_from_slice(b"invalid").unwrap());
mac.update(data.as_bytes());
let result = mac.finalize();
let hex_str = hex::encode(result.into_bytes());
stack.push(Value::Str(hex_str));
BuiltinResult::Handled
}
"base64_url_encode" => {
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let encoded = URL_SAFE_NO_PAD.encode(s.as_bytes());
stack.push(Value::Str(encoded));
BuiltinResult::Handled
}
"base64_url_decode" => {
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let decoded = URL_SAFE_NO_PAD.decode(s.as_bytes())
.map(|b| String::from_utf8_lossy(&b).to_string())
.unwrap_or_default();
stack.push(Value::Str(decoded));
BuiltinResult::Handled
}
"unix_timestamp" => {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
stack.push(Value::Int(secs));
BuiltinResult::Handled
}
"uuid_v4" => {
let id = uuid::Uuid::new_v4().to_string();
stack.push(Value::Str(id));
BuiltinResult::Handled
}
// ── JSON encode/decode (Value ↔ String) ───────────────────────────────
"json_encode" => {
// Encodes a Map, List, Struct or primitive Value to a JSON string.
let v = stack.pop().unwrap_or(Value::Nil);
let jv = el_value_to_json_value(&v);
let s = serde_json::to_string(&jv).unwrap_or_else(|_| "null".to_string());
stack.push(Value::Str(s));
BuiltinResult::Handled
}
"json_decode" => {
// Decodes a JSON string to a Value::Map (or List/primitive).
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => "null".to_string(),
};
let jv: serde_json::Value = serde_json::from_str(&s).unwrap_or(serde_json::Value::Null);
stack.push(json_value_to_el_value(&jv));
BuiltinResult::Handled
}
"json_get_string" => {
// json_get_string(map_or_struct_or_json_str, key) -> String
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let v = stack.pop().unwrap_or(Value::Nil);
let result = match &v {
Value::Map(pairs) => pairs.iter()
.find(|(k, _)| k == &key)
.map(|(_, v)| match v {
Value::Str(s) => s.clone(),
other => other.to_string(),
})
.unwrap_or_default(),
Value::Struct { fields, .. } => fields.iter()
.find(|(k, _)| k == &key)
.map(|(_, v)| match v {
Value::Str(s) => s.clone(),
other => other.to_string(),
})
.unwrap_or_default(),
Value::Str(json_str) => {
serde_json::from_str::<serde_json::Value>(json_str)
.ok()
.and_then(|jv| jv.get(&key).and_then(|v| v.as_str().map(str::to_string)))
.unwrap_or_default()
}
_ => String::new(),
};
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"json_get_int" => {
// json_get_int(map_or_struct_or_json_str, key) -> Int
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let v = stack.pop().unwrap_or(Value::Nil);
let result = match &v {
Value::Map(pairs) => pairs.iter()
.find(|(k, _)| k == &key)
.map(|(_, v)| match v {
Value::Int(n) => *n,
Value::Str(s) => s.parse().unwrap_or(0),
_ => 0,
})
.unwrap_or(0),
Value::Struct { fields, .. } => fields.iter()
.find(|(k, _)| k == &key)
.map(|(_, v)| match v {
Value::Int(n) => *n,
Value::Str(s) => s.parse().unwrap_or(0),
_ => 0,
})
.unwrap_or(0),
Value::Str(json_str) => {
serde_json::from_str::<serde_json::Value>(json_str)
.ok()
.and_then(|jv| jv.get(&key).and_then(|v| v.as_i64()))
.unwrap_or(0)
}
_ => 0,
};
stack.push(Value::Int(result));
BuiltinResult::Handled
}
"json_get_array" => {
// json_get_array(map_or_struct_or_json_str, key) -> [String]
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let v = stack.pop().unwrap_or(Value::Nil);
let result: Vec<Value> = match &v {
Value::Map(pairs) => pairs.iter()
.find(|(k, _)| k == &key)
.map(|(_, v)| match v {
Value::List(items) => items.clone(),
_ => vec![],
})
.unwrap_or_default(),
Value::Struct { fields, .. } => fields.iter()
.find(|(k, _)| k == &key)
.map(|(_, v)| match v {
Value::List(items) => items.clone(),
_ => vec![],
})
.unwrap_or_default(),
Value::Str(json_str) => {
serde_json::from_str::<serde_json::Value>(json_str)
.ok()
.and_then(|jv| jv.get(&key).and_then(|v| v.as_array().cloned()))
.map(|arr| arr.iter().map(|item| {
match item {
serde_json::Value::String(s) => Value::Str(s.clone()),
other => Value::Str(other.to_string()),
}
}).collect())
.unwrap_or_default()
}
_ => vec![],
};
stack.push(Value::List(result));
BuiltinResult::Handled
}
// ── HTTP auth builtins (Bearer token) ─────────────────────────────────
"http_get_auth" => {
let token = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let url = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = reqwest::blocking::Client::new()
.get(&url)
.header("Authorization", format!("Bearer {token}"))
.send()
.and_then(|r| r.text())
.unwrap_or_default();
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"http_put_auth" => {
let body = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let token = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let url = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = reqwest::blocking::Client::new()
.put(&url)
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/json")
.body(body)
.send()
.and_then(|r| r.text())
.unwrap_or_default();
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"http_delete_auth" => {
let token = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let url = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = reqwest::blocking::Client::new()
.delete(&url)
.header("Authorization", format!("Bearer {token}"))
.send()
.and_then(|r| r.text())
.unwrap_or_default();
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"http_post_auth" => {
let body = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let token = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let url = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = reqwest::blocking::Client::new()
.post(&url)
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/json")
.body(body)
.send()
.and_then(|r| r.text())
.unwrap_or_default();
stack.push(Value::Str(result));
BuiltinResult::Handled
}
// ── I/O builtins ──────────────────────────────────────────────────────
"readline" => {
let prompt = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
use std::io::Write;
print!("{prompt}");
let _ = std::io::stdout().flush();
let mut line = String::new();
let _ = std::io::stdin().read_line(&mut line);
stack.push(Value::Str(line.trim_end_matches('\n').trim_end_matches('\r').to_string()));
BuiltinResult::Handled
}
// ── ANSI color builtins ───────────────────────────────────────────────
"color_cyan" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
stack.push(Value::Str(format!("\x1b[36m{s}\x1b[0m")));
BuiltinResult::Handled
}
"color_green" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
stack.push(Value::Str(format!("\x1b[32m{s}\x1b[0m")));
BuiltinResult::Handled
}
"color_red" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
stack.push(Value::Str(format!("\x1b[31m{s}\x1b[0m")));
BuiltinResult::Handled
}
"color_yellow" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
stack.push(Value::Str(format!("\x1b[33m{s}\x1b[0m")));
BuiltinResult::Handled
}
"color_bold" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
stack.push(Value::Str(format!("\x1b[1m{s}\x1b[0m")));
BuiltinResult::Handled
}
"color_dim" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
stack.push(Value::Str(format!("\x1b[2m{s}\x1b[0m")));
BuiltinResult::Handled
}
// ── String / array helpers ────────────────────────────────────────────
"string_split_last" => {
// string_split_last(s, delim) -> [everything-before-last, last-part]
// Splits on the LAST occurrence of delim.
let delim = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let parts = if let Some(pos) = s.rfind(&delim as &str) {
vec![
Value::Str(s[..pos].to_string()),
Value::Str(s[pos + delim.len()..].to_string()),
]
} else {
vec![Value::Str(s), Value::Str(String::new())]
};
stack.push(Value::List(parts));
BuiltinResult::Handled
}
"array_get" => {
// array_get(arr, idx) -> element at idx
let idx = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n,
_ => 0,
};
let list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
let v = if idx >= 0 && (idx as usize) < list.len() {
list[idx as usize].clone()
} else {
Value::Nil
};
stack.push(v);
BuiltinResult::Handled
}
// ── Engram builtins ───────────────────────────────────────────────────
"engram_activate" => {
let query = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
let type_name = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
let results = engram_activate_search(&type_name, &query);
stack.push(Value::List(results));
BuiltinResult::Handled
}
"engram_relate" => {
let weight = match stack.pop().unwrap_or(Value::Nil) {
Value::Float(f) => f,
Value::Int(n) => n as f64,
_ => 1.0,
};
let relation = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
let to_id = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
let from_id = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
let engram_url = std::env::var("ENGRAM_URL")
.unwrap_or_else(|_| "http://localhost:8742".to_string());
let api_key = std::env::var("ENGRAM_API_KEY").unwrap_or_default();
let body = serde_json::json!({
"from_id": from_id,
"to_id": to_id,
"relation": relation,
"weight": weight
})
.to_string();
let mut req = reqwest::blocking::Client::new()
.post(format!("{engram_url}/edges"))
.header("Content-Type", "application/json")
.body(body);
if !api_key.is_empty() {
req = req.header("Authorization", format!("Bearer {api_key}"));
}
let ok = req.send().map(|r| r.status().is_success()).unwrap_or(false);
stack.push(Value::Bool(ok));
BuiltinResult::Handled
}
"engram_neighbors" => {
let node_id = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
let engram_url = std::env::var("ENGRAM_URL")
.unwrap_or_else(|_| "http://localhost:8742".to_string());
let api_key = std::env::var("ENGRAM_API_KEY").unwrap_or_default();
let mut req = reqwest::blocking::Client::new()
.get(format!("{engram_url}/nodes/{node_id}/edges"));
if !api_key.is_empty() {
req = req.header("Authorization", format!("Bearer {api_key}"));
}
let result = req
.send()
.and_then(|r| r.json::<serde_json::Value>())
.ok();
let list: Vec<Value> = result
.and_then(|v| v.as_array().cloned())
.unwrap_or_default()
.iter()
.map(json_value_to_el_value)
.collect();
stack.push(Value::List(list));
BuiltinResult::Handled
}
// ── Process / system builtins ─────────────────────────────────────────
"sleep_ms" => {
let ms = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as u64,
_ => 0,
};
std::thread::sleep(std::time::Duration::from_millis(ms));
stack.push(Value::Nil);
BuiltinResult::Handled
}
"timestamp" => {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
// Format as ISO-8601 approximation: seconds.millis
let secs = ts.as_secs();
let millis = ts.subsec_millis();
// Simple ISO-like: YYYY-MM-DDTHH:MM:SS.mmmZ via manual calc
// Use epoch-based formatting
let s = format_epoch_as_iso(secs, millis);
stack.push(Value::Str(s));
BuiltinResult::Handled
}
// ── Terminal control builtins ─────────────────────────────────────────
"term_clear" => {
print!("\x1b[2J\x1b[H");
let _ = std::io::Write::flush(&mut std::io::stdout());
stack.push(Value::Nil);
BuiltinResult::Handled
}
"term_size" => {
let (cols, rows) = term_size();
stack.push(Value::List(vec![Value::Int(cols as i64), Value::Int(rows as i64)]));
BuiltinResult::Handled
}
"cursor_to" => {
let col = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n,
_ => 1,
};
let row = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n,
_ => 1,
};
print!("\x1b[{};{}H", row, col);
let _ = std::io::Write::flush(&mut std::io::stdout());
stack.push(Value::Nil);
BuiltinResult::Handled
}
"cursor_up" => {
let n = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n,
_ => 1,
};
print!("\x1b[{}A", n);
let _ = std::io::Write::flush(&mut std::io::stdout());
stack.push(Value::Nil);
BuiltinResult::Handled
}
"cursor_down" => {
let n = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n,
_ => 1,
};
print!("\x1b[{}B", n);
let _ = std::io::Write::flush(&mut std::io::stdout());
stack.push(Value::Nil);
BuiltinResult::Handled
}
"cursor_col" => {
let col = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n,
_ => 1,
};
print!("\x1b[{}G", col);
let _ = std::io::Write::flush(&mut std::io::stdout());
stack.push(Value::Nil);
BuiltinResult::Handled
}
"term_save_cursor" => {
print!("\x1b[s");
let _ = std::io::Write::flush(&mut std::io::stdout());
stack.push(Value::Nil);
BuiltinResult::Handled
}
"term_restore_cursor" => {
print!("\x1b[u");
let _ = std::io::Write::flush(&mut std::io::stdout());
stack.push(Value::Nil);
BuiltinResult::Handled
}
"term_clear_line" => {
print!("\x1b[2K\r");
let _ = std::io::Write::flush(&mut std::io::stdout());
stack.push(Value::Nil);
BuiltinResult::Handled
}
"print_inline" => {
let v = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
other => other.to_string(),
};
print!("{}", v);
let _ = std::io::Write::flush(&mut std::io::stdout());
stack.push(Value::Nil);
BuiltinResult::Handled
}
// ── SSE streaming builtin ─────────────────────────────────────────────
"http_sse_post" => {
// http_sse_post(url, token, body) -> String
// POSTs and reads an SSE stream, printing each token inline.
// Returns the full assembled response as a String.
let body_str = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let token_str = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let url_str = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = reqwest::blocking::Client::new()
.post(&url_str)
.header("Authorization", format!("Bearer {}", token_str))
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.body(body_str)
.send();
match result {
Err(e) => {
stack.push(Value::Str(format!("error: {}", e)));
}
Ok(resp) => {
use std::io::BufRead;
let mut full_text = String::new();
let reader = std::io::BufReader::new(resp);
for line in reader.lines() {
let Ok(line) = line else { break };
if let Some(data) = line.strip_prefix("data: ") {
if data == "[DONE]" { break; }
if let Ok(v) = serde_json::from_str::<serde_json::Value>(data) {
let delta = v.get("delta")
.and_then(|d| d.as_str())
.or_else(|| v.get("content").and_then(|c| c.as_str()))
.or_else(|| v.get("text").and_then(|t| t.as_str()))
.unwrap_or("");
if !delta.is_empty() {
print!("{}", delta);
let _ = std::io::Write::flush(&mut std::io::stdout());
full_text.push_str(delta);
}
} else if !data.is_empty() {
print!("{}", data);
let _ = std::io::Write::flush(&mut std::io::stdout());
full_text.push_str(data);
}
}
}
stack.push(Value::Str(full_text));
}
}
BuiltinResult::Handled
}
_ => BuiltinResult::NotBuiltin,
}
}
/// Return terminal dimensions as (cols, rows).
/// Uses TIOCGWINSZ ioctl on Unix; falls back to (80, 24).
fn term_size() -> (u16, u16) {
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
#[repr(C)]
struct WinSize { rows: u16, cols: u16, _xpix: u16, _ypix: u16 }
let ws = WinSize { rows: 0, cols: 0, _xpix: 0, _ypix: 0 };
unsafe {
extern "C" {
fn ioctl(fd: std::ffi::c_int, request: std::ffi::c_ulong, ...) -> std::ffi::c_int;
}
// TIOCGWINSZ: macOS = 0x40087468, Linux = 0x5413
#[cfg(target_os = "macos")]
ioctl(std::io::stdout().as_raw_fd(), 0x40087468_u64, &ws);
#[cfg(not(target_os = "macos"))]
ioctl(std::io::stdout().as_raw_fd(), 0x5413_u64, &ws);
if ws.cols > 0 && ws.rows > 0 {
return (ws.cols, ws.rows);
}
}
}
(80, 24)
}
/// Format a Unix timestamp (seconds + millis) as an ISO 8601 string.
/// This avoids pulling in chrono while still producing a readable timestamp.
fn format_epoch_as_iso(secs: u64, millis: u32) -> String {
// Days since epoch, accounting for leap years
let mut days = secs / 86400;
let time_of_day = secs % 86400;
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let seconds = time_of_day % 60;
// Gregorian calendar calculation starting from 1970-01-01
let mut year = 1970u64;
loop {
let days_in_year = if is_leap(year) { 366 } else { 365 };
if days < days_in_year {
break;
}
days -= days_in_year;
year += 1;
}
let months = [31u64, if is_leap(year) { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let mut month = 1u64;
for &m in &months {
if days < m {
break;
}
days -= m;
month += 1;
}
let day = days + 1;
format!("{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}.{millis:03}Z")
}
fn is_leap(year: u64) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
/// Compare two runtime values for ordering (used by Lt/Gt/LtEq/GtEq).
fn cmp_values(a: &el_compiler::Value, b: &el_compiler::Value) -> std::cmp::Ordering {
use el_compiler::Value;
match (a, b) {
(Value::Int(x), Value::Int(y)) => x.cmp(y),
(Value::Float(x), Value::Float(y)) => x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal),
(Value::Int(x), Value::Float(y)) => (*x as f64).partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal),
(Value::Float(x), Value::Int(y)) => x.partial_cmp(&(*y as f64)).unwrap_or(std::cmp::Ordering::Equal),
(Value::Str(x), Value::Str(y)) => x.cmp(y),
_ => std::cmp::Ordering::Equal,
}
}
/// Check if a function name is a known built-in (used by run_sub_interpreter).
fn is_builtin(name: &str) -> bool {
matches!(name, "print" | "println" | "log" | "print_err" | "__build_list__")
}
/// Interpreter with debugger support — emits DebugEvents as it runs.
fn run_interpreter_debug(instructions: &[el_compiler::Bytecode], debugger: &mut el_compiler::Debugger) {
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;
let program_args: Vec<String> = std::env::args().skip(2).collect(); // skip 'el' and 'debug'
while ip < instructions.len() {
// Check if we should pause here
if debugger.should_pause(ip) {
debugger.on_pause(ip, locals.clone());
for event in debugger.drain_events() {
match event {
el_compiler::DebugEvent::Breakpoint { offset, frame } => {
println!("[break] offset={offset} fn={} {}:{}:{}", frame.function_name, frame.source_file, frame.line, frame.col);
}
el_compiler::DebugEvent::Step { frame, locals: step_locals } => {
let var_list: Vec<String> = step_locals.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect();
println!("[step] offset={ip} {}:{} vars=[{}]", frame.line, frame.col, var_list.join(", "));
}
_ => {}
}
}
}
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::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::GetField(field) => {
let obj = stack.pop().unwrap_or(Value::Nil);
let result = match &obj {
Value::Map(pairs) => pairs.iter()
.find(|(k, _)| k == field)
.map(|(_, v)| v.clone())
.unwrap_or(Value::Nil),
Value::Struct { fields, .. } => fields.iter()
.find(|(n, _)| n == field)
.map(|(_, v)| v.clone())
.unwrap_or(Value::Nil),
_ => Value::Nil,
};
stack.push(result);
}
Bytecode::BuildStruct { type_name, fields } => {
let n = fields.len();
let mut field_values: Vec<Value> = (0..n).map(|_| stack.pop().unwrap_or(Value::Nil)).collect();
field_values.reverse();
let struct_fields: Vec<(String, Value)> = fields.iter().cloned()
.zip(field_values.into_iter())
.collect();
stack.push(Value::Struct {
type_name: type_name.clone(),
fields: struct_fields,
});
}
Bytecode::Call { name, arity } => {
let result = dispatch_builtin(name, *arity, &mut stack, &program_args);
match result {
BuiltinResult::Handled | BuiltinResult::NotBuiltin | BuiltinResult::HttpServe => {}
BuiltinResult::Exit(code) => std::process::exit(code),
}
}
Bytecode::Eq => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(a == b));
}
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 | Bytecode::Halt => {
debugger.pop_frame(stack.last().cloned().unwrap_or(Value::Nil));
break;
}
_ => {}
}
ip += 1;
}
}