//! el — The Engram language CLI. //! //! # Commands //! //! ## Project management //! el new scaffold new project with el.toml + src/main.el //! el add [@ver] add a dependency to el.toml //! el remove 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 search registry //! el plugin add add compiler plugin //! //! ## Low-level //! el build-file compile a single .el file (no el.toml needed) //! el seal seal an existing release artifact //! el unseal 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::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>> = std::cell::RefCell::new(None); /// Shared bytecode instructions for sub-interpreter calls from http_serve. static SERVE_INSTRUCTIONS: std::cell::RefCell>>> = std::cell::RefCell::new(None); /// Shared fn_table for sub-interpreter calls. static SERVE_FN_TABLE: std::cell::RefCell>>> = 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, // Font for text rendering (fontdue) font: Option, // 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, // Clip rectangle stack: (x, y, w, h) clips: Vec<(i32, i32, u32, u32)>, } thread_local! { static CANVAS: std::cell::RefCell = 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: /). #[arg(long, short = 'd')] dir: Option, }, /// 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, }, /// 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, /// 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, }, /// Build in debug mode and run the output. Run { /// Path to the project manifest (default: el.toml). #[arg(long)] manifest: Option, }, /// Run tests in the current project (reads el.toml). Test { /// Only run tests matching this name (substring match). filter: Option, /// 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, }, /// 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, /// 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, }, /// Type-check source files without producing artifacts. Check { #[arg(long)] manifest: Option, }, /// 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, /// 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, }, /// 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, }, // ── Registry ────────────────────────────────────────────────────────────── /// Publish this package to the Engram registry. Publish { /// Registry API key. #[arg(long, env = "ENGRAM_REGISTRY_KEY")] api_key: Option, #[arg(long)] manifest: Option, }, /// 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, }, /// 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, }, /// Seal an existing release artifact. Seal { artifact: PathBuf, #[arg(long, short = 'o')] output: Option, }, /// Unseal a sealed artifact (requires ENGRAM_SEAL_KEY). Unseal { artifact: PathBuf, #[arg(long, short = 'o')] output: Option, }, } #[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> { 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> { 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> { 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> { 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::>() .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> { 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> { 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, ) -> Result> { 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> { 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> { s.parse::().map_err(|e| e.into()) } fn parse_target(s: &str) -> Result { 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 { 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> { 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 { 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::()) .ok(); let results: Vec = 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, 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, entry: usize, initial_stack: Vec, ) -> el_compiler::Value { use el_compiler::{Bytecode, Value}; let mut stack: Vec = initial_stack; let mut locals: std::collections::HashMap = std::collections::HashMap::new(); let mut call_stack: Vec<(usize, std::collections::HashMap)> = Vec::new(); let mut ip = entry; let program_args: Vec = 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 = (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 = Vec::new(); let mut locals: std::collections::HashMap = std::collections::HashMap::new(); let mut ip = 0usize; // Build a call table: fn name → bytecode offset. // We populate this by scanning for __fn_ stores first. let mut fn_table: std::collections::HashMap = 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 = 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)> = 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)> = 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::()) .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)>, 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, 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 = 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::(&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::>() }) .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::, _>>() { 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::>() }); 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>>> = 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::().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::().unwrap(); let content_json = "Content-Type: application/json".parse::().unwrap(); let content_html = "Content-Type: text/html; charset=utf-8".parse::().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::().unwrap()) .with_header("Access-Control-Allow-Headers: Content-Type".parse::().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/ first, then src/ 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 = (|| { // Transform body format let runtime_body = if let Ok(v) = serde_json::from_str::(&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::(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::(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!( "

It\u{2019}s Neuron. You left our conversation in the middle.

\

I remember where we were.

" ) } 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!("

{}

", l.trim())) .collect::>() .join("\n") }; let html_body = format!( r#"

Hey {} —

{}

Come back when you’re ready

That link brings you right back to where we left off.

"#, name, body_paragraphs, link ); let payload = serde_json::json!({ "from": "Neuron ", "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::::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#"

Hey {} —

One click to confirm your spot on the Neuron waitlist.

Confirm my spot →

This link expires in 24 hours. If you didn't sign up for Neuron, you can ignore this.

"#, name, confirm_link ); let payload_json = serde_json::json!({ "from": "Neuron ", "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::::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 = 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 = 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::(&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::(&json_str) .ok() .and_then(|v| { if let serde_json::Value::Object(map) = v { Some(map.keys().cloned().map(Value::Str).collect::>()) } 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::(&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::(&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::(&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::() .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 = 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 = 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 = 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 = 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 = 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 = 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::().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::().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 = 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 = 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; 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::(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::(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 = 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::(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::()) .ok(); let list: Vec = 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::(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 = 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 = Vec::new(); let mut locals: std::collections::HashMap = std::collections::HashMap::new(); let mut ip = 0usize; let program_args: Vec = 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 = 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 = (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; } }