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

5035 lines
211 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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);
}
// ── Canvas / native window state ─────────────────────────────────────────────
#[derive(Default)]
struct CanvasState {
// Window configuration
title: String,
width: u32,
height: u32,
// Drawing surface (tiny-skia pixmap)
pixmap: Option<tiny_skia::Pixmap>,
// Font for text rendering (fontdue)
font: Option<fontdue::Font>,
// Input state
mouse_x: i32,
mouse_y: i32,
mouse_buttons: u8, // bit 0 = left, bit 1 = right
// Events accumulated since last frame (JSON strings)
events: Vec<String>,
// Clip rectangle stack: (x, y, w, h)
clips: Vec<(i32, i32, u32, u32)>,
}
thread_local! {
static CANVAS: std::cell::RefCell<CanvasState> = std::cell::RefCell::new(CanvasState {
title: String::new(),
width: 1200,
height: 800,
pixmap: None,
font: None,
mouse_x: 0,
mouse_y: 0,
mouse_buttons: 0,
events: Vec::new(),
clips: Vec::new(),
});
}
// ── 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();
// block_in_place lets the interpreter use reqwest::blocking from within tokio::main
tokio::task::block_in_place(|| 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();
// block_in_place lets the interpreter use reqwest::blocking from within tokio::main
tokio::task::block_in_place(|| 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())
}
// ── Canvas helper functions ───────────────────────────────────────────────────
/// Parse "#rrggbb" or "#rrggbbaa" into tiny-skia Color.
fn parse_color(s: &str) -> tiny_skia::Color {
let s = s.trim_start_matches('#');
let r = u8::from_str_radix(s.get(0..2).unwrap_or("ff"), 16).unwrap_or(255);
let g = u8::from_str_radix(s.get(2..4).unwrap_or("ff"), 16).unwrap_or(255);
let b = u8::from_str_radix(s.get(4..6).unwrap_or("ff"), 16).unwrap_or(255);
let a = u8::from_str_radix(s.get(6..8).unwrap_or("ff"), 16).unwrap_or(255);
tiny_skia::Color::from_rgba8(r, g, b, a)
}
/// Rasterize `text` at `size` pixels using the stored fontdue font.
/// Returns (glyphs: Vec<(x_offset, metrics, bitmap)>, total_width).
fn rasterize_text(text: &str, size: f32) -> (Vec<(i32, fontdue::Metrics, Vec<u8>)>, i32) {
CANVAS.with(|cv| {
let cv = cv.borrow();
let font = match &cv.font { Some(f) => f, None => return (vec![], 0) };
let mut glyphs = Vec::new();
let mut cursor_x = 0i32;
for ch in text.chars() {
let (metrics, bitmap) = font.rasterize(ch, size);
glyphs.push((cursor_x, metrics, bitmap));
cursor_x += metrics.advance_width as i32;
}
(glyphs, cursor_x)
})
}
// ─────────────────────────────────────────────────────────────────────────────
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;
}
// GET /assets/* — serve static files adjacent to the HTML file
if method == "GET" && path.starts_with("/assets/") {
let file_name = &path["/assets/".len()..];
let html_dir = GLOBAL_STATE.with(|gs| {
gs.borrow().get("__html_file__").and_then(|p| {
std::path::Path::new(p).parent().map(|d| d.to_path_buf())
})
});
let content_type = if file_name.ends_with(".png") { "image/png" }
else if file_name.ends_with(".svg") { "image/svg+xml" }
else if file_name.ends_with(".jpg") || file_name.ends_with(".jpeg") { "image/jpeg" }
else { "application/octet-stream" };
let ct_header = tiny_http::Header::from_bytes(
"Content-Type", content_type
).unwrap();
if let Some(dir) = html_dir {
// Try src/assets/<file> first, then src/<file> as fallback
let asset_path = dir.join("assets").join(file_name);
let asset_path = if asset_path.exists() { asset_path } else { dir.join(file_name) };
if let Ok(bytes) = std::fs::read(&asset_path) {
let _ = request.respond(
tiny_http::Response::from_data(bytes)
.with_header(ct_header)
);
continue;
}
}
let _ = request.respond(
tiny_http::Response::from_string("not found")
.with_status_code(404u16)
);
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();
let history = v["history"].as_array().cloned().unwrap_or_default();
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)
};
// ── Ask Neuron to generate a personalised email body ──
let email_body_text: String = (|| -> String {
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 prompt = format!(
"Write a brief, personal email to {} inviting them back to continue our conversation. \
Reference specifically what we discussed. Write as Neuron, first person, warm but not sentimental. \
Include only the email body — no subject line, no greeting header, no sign-off. \
3-4 sentences maximum.",
name
);
let mut messages = history.clone();
messages.push(serde_json::json!({"role":"user","content":prompt}));
let mut payload = serde_json::json!({"messages": messages});
if !conv_id.is_empty() {
payload["conv_id"] = serde_json::Value::String(conv_id.clone());
}
let resp = match reqwest::blocking::Client::new()
.post(&chat_url)
.header("Content-Type", "application/json")
.timeout(std::time::Duration::from_secs(20))
.body(payload.to_string())
.send()
{
Ok(r) => r,
Err(_) => return String::new(),
};
let mut generated = String::new();
for line in resp.text().unwrap_or_default().lines() {
if line.starts_with("data: ") {
let data = &line[6..];
if data == "[DONE]" { break; }
if let Ok(jv) = serde_json::from_str::<serde_json::Value>(data) {
if let Some(d) = jv.get("delta").and_then(|x| x.as_str()) {
generated.push_str(d);
}
}
}
}
generated.trim().to_string()
})();
// Wrap generated text (or fallback) in HTML template
let body_paragraphs = if email_body_text.is_empty() {
format!(
"<p style=\"font-size:1.1rem;line-height:1.6\">It\u{2019}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>"
)
} else {
// Split on newlines and wrap each non-empty line as a paragraph
email_body_text
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| format!("<p style=\"font-size:1.1rem;line-height:1.6\">{}</p>", l.trim()))
.collect::<Vec<_>>()
.join("\n")
};
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="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&rsquo;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, body_paragraphs, 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
}
// ── Canvas builtins ───────────────────────────────────────────────────
"canvas_open" => {
// canvas_open(title: String, width: Int, height: Int) -> Void
let height = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as u32, _ => 800 };
let width = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as u32, _ => 1200 };
let title = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "Neuron".into() };
// Load font (try SF NS, Arial, DejaVu in order)
let font_data =
std::fs::read("/System/Library/Fonts/SFNS.ttf")
.or_else(|_| std::fs::read("/System/Library/Fonts/Supplemental/Arial.ttf"))
.or_else(|_| std::fs::read("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"))
.unwrap_or_default();
let font = if font_data.is_empty() { None } else {
fontdue::Font::from_bytes(font_data.as_slice(), fontdue::FontSettings::default()).ok()
};
CANVAS.with(|cv| {
let mut cv = cv.borrow_mut();
cv.title = title;
cv.width = width;
cv.height = height;
cv.font = font;
cv.pixmap = tiny_skia::Pixmap::new(width, height);
});
stack.push(Value::Nil);
BuiltinResult::Handled
}
"canvas_clear" => {
// canvas_clear(color: String) -> Void
let color_str = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "#000000".into() };
let color = parse_color(&color_str);
CANVAS.with(|cv| {
if let Some(px) = cv.borrow_mut().pixmap.as_mut() {
px.fill(color);
}
});
stack.push(Value::Nil);
BuiltinResult::Handled
}
"canvas_fill_rect" => {
// canvas_fill_rect(x, y, w, h: Int, color: String, radius: Int) -> Void
let radius = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, Value::Float(f) => f as f32, _ => 0.0 };
let color_str = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "#ffffff".into() };
let rh = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
let rw = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
let ry = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
let rx = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
let color = parse_color(&color_str);
CANVAS.with(|cv| {
let mut cv = cv.borrow_mut();
// Collect clip info before mutably borrowing pixmap
let clip_rect = cv.clips.last().copied();
if let Some(px) = cv.pixmap.as_mut() {
let mut paint = tiny_skia::Paint::default();
paint.set_color(color);
paint.anti_alias = true;
let path = {
let mut pb = tiny_skia::PathBuilder::new();
if radius > 0.0 {
let r = radius.min(rw / 2.0).min(rh / 2.0);
pb.move_to(rx + r, ry);
pb.line_to(rx + rw - r, ry);
pb.quad_to(rx + rw, ry, rx + rw, ry + r);
pb.line_to(rx + rw, ry + rh - r);
pb.quad_to(rx + rw, ry + rh, rx + rw - r, ry + rh);
pb.line_to(rx + r, ry + rh);
pb.quad_to(rx, ry + rh, rx, ry + rh - r);
pb.line_to(rx, ry + r);
pb.quad_to(rx, ry, rx + r, ry);
pb.close();
} else if let Some(rect) = tiny_skia::Rect::from_xywh(rx, ry, rw.max(0.1), rh.max(0.1)) {
pb.push_rect(rect);
}
pb.finish()
};
if let Some(path) = path {
// Apply clip if any
let clip_mask = clip_rect.and_then(|(cx, cy, cw, ch)| {
tiny_skia::Rect::from_xywh(cx as f32, cy as f32, cw as f32, ch as f32).and_then(|rect| {
let clip_path = tiny_skia::PathBuilder::from_rect(rect);
let mut mask = tiny_skia::Mask::new(px.width(), px.height())?;
mask.fill_path(&clip_path, tiny_skia::FillRule::Winding, false, tiny_skia::Transform::identity());
Some(mask)
})
});
px.fill_path(&path, &paint, tiny_skia::FillRule::Winding, tiny_skia::Transform::identity(), clip_mask.as_ref());
}
}
});
stack.push(Value::Nil);
BuiltinResult::Handled
}
"canvas_stroke_rect" => {
// canvas_stroke_rect(x, y, w, h: Int, color: String, stroke_w: Int, radius: Int) -> Void
let radius = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, Value::Float(f) => f as f32, _ => 0.0 };
let stroke_w = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 1.0 };
let color_str = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "#ffffff".into() };
let rh = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
let rw = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
let ry = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
let rx = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
let color = parse_color(&color_str);
CANVAS.with(|cv| {
let mut cv = cv.borrow_mut();
if let Some(px) = cv.pixmap.as_mut() {
let mut paint = tiny_skia::Paint::default();
paint.set_color(color);
paint.anti_alias = true;
let mut stroke = tiny_skia::Stroke::default();
stroke.width = stroke_w;
let path = {
let mut pb = tiny_skia::PathBuilder::new();
if radius > 0.0 {
let r = radius.min(rw / 2.0).min(rh / 2.0);
pb.move_to(rx + r, ry);
pb.line_to(rx + rw - r, ry);
pb.quad_to(rx + rw, ry, rx + rw, ry + r);
pb.line_to(rx + rw, ry + rh - r);
pb.quad_to(rx + rw, ry + rh, rx + rw - r, ry + rh);
pb.line_to(rx + r, ry + rh);
pb.quad_to(rx, ry + rh, rx, ry + rh - r);
pb.line_to(rx, ry + r);
pb.quad_to(rx, ry, rx + r, ry);
pb.close();
} else if let Some(rect) = tiny_skia::Rect::from_xywh(rx, ry, rw.max(0.1), rh.max(0.1)) {
pb.push_rect(rect);
}
pb.finish()
};
if let Some(path) = path {
px.stroke_path(&path, &paint, &stroke, tiny_skia::Transform::identity(), None);
}
}
});
stack.push(Value::Nil);
BuiltinResult::Handled
}
"canvas_line" => {
// canvas_line(x1, y1, x2, y2: Int, color: String, line_w: Int) -> Void
let line_w = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 1.0 };
let color_s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "#ffffff".into() };
let y2 = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
let x2 = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
let y1 = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
let x1 = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
let color = parse_color(&color_s);
CANVAS.with(|cv| {
let mut cv = cv.borrow_mut();
if let Some(px) = cv.pixmap.as_mut() {
let mut paint = tiny_skia::Paint::default();
paint.set_color(color);
paint.anti_alias = true;
let mut stroke = tiny_skia::Stroke::default();
stroke.width = line_w;
let mut pb = tiny_skia::PathBuilder::new();
pb.move_to(x1, y1);
pb.line_to(x2, y2);
if let Some(path) = pb.finish() {
px.stroke_path(&path, &paint, &stroke, tiny_skia::Transform::identity(), None);
}
}
});
stack.push(Value::Nil);
BuiltinResult::Handled
}
"canvas_text" => {
// canvas_text(x, y: Int, text: String, size: Int, color: String) -> Void
let color_s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "#ffffff".into() };
let size = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 14.0 };
let text = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let y = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
let x = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
let color = parse_color(&color_s);
let (glyphs, _) = rasterize_text(&text, size);
CANVAS.with(|cv| {
let mut cv = cv.borrow_mut();
if let Some(px) = cv.pixmap.as_mut() {
let pw = px.width() as i32;
let ph = px.height() as i32;
let cr = (color.red() * 255.0) as u32;
let cg = (color.green() * 255.0) as u32;
let cb = (color.blue() * 255.0) as u32;
for (gx_off, metrics, bitmap) in &glyphs {
let gx = x + gx_off + metrics.xmin;
let gy = y - metrics.height as i32 - metrics.ymin;
for row in 0..metrics.height {
for col in 0..metrics.width {
let alpha = bitmap[row * metrics.width + col];
if alpha == 0 { continue; }
let px_x = gx + col as i32;
let px_y = gy + row as i32;
if px_x < 0 || px_y < 0 || px_x >= pw || px_y >= ph { continue; }
let idx = (px_y as usize * pw as usize + px_x as usize) * 4;
let data = px.data_mut();
let a = alpha as u32;
let ia = 255 - a;
data[idx] = ((ia * data[idx] as u32 + a * cr) / 255) as u8;
data[idx + 1] = ((ia * data[idx+1] as u32 + a * cg) / 255) as u8;
data[idx + 2] = ((ia * data[idx+2] as u32 + a * cb) / 255) as u8;
data[idx + 3] = 255;
}
}
}
}
});
stack.push(Value::Nil);
BuiltinResult::Handled
}
"canvas_text_width" => {
// canvas_text_width(text: String, size: Int) -> Int
let size = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 14.0 };
let text = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let (_, w) = rasterize_text(&text, size);
stack.push(Value::Int(w as i64));
BuiltinResult::Handled
}
"canvas_text_height" => {
// canvas_text_height(size: Int) -> Int
let size = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 14.0 };
stack.push(Value::Int((size * 1.2) as i64));
BuiltinResult::Handled
}
"canvas_clip" => {
// canvas_clip(x, y, w, h: Int) -> Void
let h = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as u32, _ => 0 };
let w = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as u32, _ => 0 };
let y = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
let x = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
CANVAS.with(|cv| cv.borrow_mut().clips.push((x, y, w, h)));
stack.push(Value::Nil);
BuiltinResult::Handled
}
"canvas_unclip" => {
CANVAS.with(|cv| { cv.borrow_mut().clips.pop(); });
stack.push(Value::Nil);
BuiltinResult::Handled
}
"canvas_size" => {
// canvas_size() -> [Int] — [width, height]
let (w, h) = CANVAS.with(|cv| {
let cv = cv.borrow();
(cv.width as i64, cv.height as i64)
});
stack.push(Value::List(vec![Value::Int(w), Value::Int(h)]));
BuiltinResult::Handled
}
"canvas_mouse_pos" => {
// canvas_mouse_pos() -> [Int] — [x, y]
let (x, y) = CANVAS.with(|cv| {
let cv = cv.borrow();
(cv.mouse_x as i64, cv.mouse_y as i64)
});
stack.push(Value::List(vec![Value::Int(x), Value::Int(y)]));
BuiltinResult::Handled
}
"canvas_events" => {
// canvas_events() -> String — JSON array of event objects
let evts = CANVAS.with(|cv| cv.borrow().events.clone());
let result = if evts.is_empty() {
"[]".to_string()
} else {
format!("[{}]", evts.join(","))
};
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"canvas_swap" => {
// canvas_swap() -> Void — no-op; canvas_run_loop handles presentation
stack.push(Value::Nil);
BuiltinResult::Handled
}
// ── Frame-persistent key-value state ────────────────────────────────
// state_set / state_get use GLOBAL_STATE so values survive between frames
// when canvas_run_loop calls the draw function repeatedly.
"state_set" => {
// state_set(key: String, val: String) -> Void
let val = 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, val));
stack.push(Value::Nil);
BuiltinResult::Handled
}
"state_get" => {
// state_get(key: String) -> String
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().unwrap_or_default());
stack.push(Value::Str(val));
BuiltinResult::Handled
}
"canvas_run_loop" => {
// canvas_run_loop(draw_fn: String) -> Void (never returns)
let draw_fn = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "draw".into() };
let arc_instructions = SERVE_INSTRUCTIONS.with(|si| si.borrow().clone())
.expect("canvas_run_loop: no instructions stored (call canvas_open first and ensure interpreter stored them)");
let arc_fn_table = SERVE_FN_TABLE.with(|sf| sf.borrow().clone())
.expect("canvas_run_loop: no fn_table stored");
let (title, width, height) = CANVAS.with(|cv| {
let cv = cv.borrow();
(cv.title.clone(), cv.width, cv.height)
});
use winit::event::{Event, WindowEvent, MouseButton, ElementState};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::keyboard::{Key, NamedKey};
use winit::window::WindowBuilder;
let event_loop = EventLoop::new().expect("failed to create event loop");
let window = WindowBuilder::new()
.with_title(&title)
.with_inner_size(winit::dpi::LogicalSize::new(width, height))
.build(&event_loop)
.expect("failed to create window");
let context = unsafe { softbuffer::Context::new(&window) }.expect("softbuffer context");
let mut surface = unsafe { softbuffer::Surface::new(&context, &window) }.expect("softbuffer surface");
let mut frame_events: Vec<String> = Vec::new();
let _ = event_loop.run(move |event, elwt| {
elwt.set_control_flow(ControlFlow::Poll);
match event {
Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => {
std::process::exit(0);
}
Event::WindowEvent { event: WindowEvent::Resized(physical), .. } => {
let nw = physical.width.max(1);
let nh = physical.height.max(1);
CANVAS.with(|cv| {
let mut cv = cv.borrow_mut();
cv.width = nw;
cv.height = nh;
cv.pixmap = tiny_skia::Pixmap::new(nw, nh);
});
frame_events.push(format!(r#"{{"type":"resize","w":{},"h":{}}}"#, nw, nh));
}
Event::WindowEvent { event: WindowEvent::CursorMoved { position, .. }, .. } => {
let mx = position.x as i32;
let my = position.y as i32;
CANVAS.with(|cv| {
let mut cv = cv.borrow_mut();
cv.mouse_x = mx;
cv.mouse_y = my;
});
frame_events.push(format!(r#"{{"type":"mouse_move","x":{},"y":{}}}"#, mx, my));
}
Event::WindowEvent { event: WindowEvent::MouseInput { state, button, .. }, .. } => {
let btn_str = match button { MouseButton::Left => "left", MouseButton::Right => "right", _ => "other" };
let ev_type = match state { ElementState::Pressed => "mouse_down", ElementState::Released => "mouse_up" };
let (mx, my) = CANVAS.with(|cv| { let cv = cv.borrow(); (cv.mouse_x, cv.mouse_y) });
frame_events.push(format!(r#"{{"type":"{}","x":{},"y":{},"button":"{}"}}"#, ev_type, mx, my, btn_str));
if state == ElementState::Released && button == MouseButton::Left {
frame_events.push(format!(r#"{{"type":"mouse_click","x":{},"y":{},"button":"left"}}"#, mx, my));
}
}
Event::WindowEvent { event: WindowEvent::MouseWheel { delta, .. }, .. } => {
let (dx, dy) = match delta {
winit::event::MouseScrollDelta::LineDelta(x, y) => (x as i32, y as i32),
winit::event::MouseScrollDelta::PixelDelta(p) => (p.x as i32, p.y as i32),
};
frame_events.push(format!(r#"{{"type":"scroll","dx":{},"dy":{}}}"#, dx, dy));
}
Event::WindowEvent { event: WindowEvent::KeyboardInput { event: ref kev, .. }, .. } => {
// Emit char events for printable keys
if kev.state == ElementState::Pressed {
if let Key::Character(ref s) = kev.logical_key {
let ch_str = s.as_str();
if !ch_str.chars().all(|c| c.is_control()) {
let escaped = ch_str.replace('"', "\\\"");
frame_events.push(format!(r#"{{"type":"char","char":"{}"}}"#, escaped));
}
}
}
// Emit key_down events for named keys
if kev.state == ElementState::Pressed {
let key_str = match &kev.logical_key {
Key::Named(NamedKey::Enter) => "Return",
Key::Named(NamedKey::Escape) => "Escape",
Key::Named(NamedKey::Backspace) => "Backspace",
Key::Named(NamedKey::Delete) => "Delete",
Key::Named(NamedKey::ArrowLeft) => "ArrowLeft",
Key::Named(NamedKey::ArrowRight) => "ArrowRight",
Key::Named(NamedKey::ArrowUp) => "ArrowUp",
Key::Named(NamedKey::ArrowDown) => "ArrowDown",
Key::Named(NamedKey::Tab) => "Tab",
Key::Named(NamedKey::Home) => "Home",
Key::Named(NamedKey::End) => "End",
_ => "",
};
if !key_str.is_empty() {
frame_events.push(format!(r#"{{"type":"key_down","key":"{}"}}"#, key_str));
}
}
}
Event::AboutToWait => {
// Set this frame's events, then call the Engram draw function
CANVAS.with(|cv| {
cv.borrow_mut().events = std::mem::take(&mut frame_events);
});
if let Some(&entry) = arc_fn_table.get(draw_fn.as_str()) {
run_sub_interpreter(&arc_instructions, &arc_fn_table, entry);
}
// Present the pixmap to the screen
let (w, h) = CANVAS.with(|cv| { let cv = cv.borrow(); (cv.width, cv.height) });
let nz_w = std::num::NonZeroU32::new(w.max(1)).unwrap();
let nz_h = std::num::NonZeroU32::new(h.max(1)).unwrap();
if surface.resize(nz_w, nz_h).is_ok() {
if let Ok(mut buf) = surface.buffer_mut() {
CANVAS.with(|cv| {
let cv = cv.borrow();
if let Some(px) = &cv.pixmap {
let pixels = px.data(); // RGBA bytes
for (i, chunk) in pixels.chunks_exact(4).enumerate() {
// softbuffer expects 0x00RRGGBB in native u32
buf[i] = ((chunk[0] as u32) << 16)
| ((chunk[1] as u32) << 8)
| (chunk[2] as u32);
}
}
});
let _ = buf.present();
}
}
window.request_redraw();
}
_ => {}
}
});
// event_loop.run never returns normally (exits via std::process::exit)
unreachable!()
}
"canvas_image" => {
// canvas_image(path: String, x: Int, y: Int, w: Int, h: Int) -> Void
// Draws a PNG image scaled to w×h at (x, y) with alpha blending.
let draw_h = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
let draw_w = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
let dy = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
let dx = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => { stack.push(Value::Nil); return BuiltinResult::Handled; } };
if draw_w <= 0 || draw_h <= 0 { stack.push(Value::Nil); return BuiltinResult::Handled; }
use image::GenericImageView;
if let Ok(img) = image::open(&path) {
let img = img.resize_exact(draw_w as u32, draw_h as u32, image::imageops::FilterType::Lanczos3);
let rgba = img.to_rgba8();
CANVAS.with(|cv| {
let mut cv = cv.borrow_mut();
if let Some(px) = cv.pixmap.as_mut() {
let pw = px.width() as i32;
let ph = px.height() as i32;
let data = px.data_mut();
for row in 0..draw_h {
for col in 0..draw_w {
let ix = dx + col;
let iy = dy + row;
if ix < 0 || iy < 0 || ix >= pw || iy >= ph { continue; }
let p = rgba.get_pixel(col as u32, row as u32);
let sa = p[3] as u32;
if sa == 0 { continue; }
let ia = 255 - sa;
let idx = (iy as usize * pw as usize + ix as usize) * 4;
data[idx] = ((ia * data[idx] as u32 + sa * p[0] as u32) / 255) as u8;
data[idx + 1] = ((ia * data[idx+1] as u32 + sa * p[1] as u32) / 255) as u8;
data[idx + 2] = ((ia * data[idx+2] as u32 + sa * p[2] as u32) / 255) as u8;
data[idx + 3] = ((ia * data[idx+3] as u32 + sa * sa) / 255) as u8;
}
}
}
});
}
stack.push(Value::Nil);
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;
}
}