Archive Rust bootstrap — El compiler is now self-hosting

This commit is contained in:
Will Anderson
2026-04-29 22:21:31 -05:00
parent 9a0747aa13
commit 4f3543b068
139 changed files with 8980 additions and 1778 deletions
@@ -0,0 +1,21 @@
[package]
name = "el-vm"
description = "El Virtual Machine — core bytecode executor used by the elvm binary and el toolchain"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
el-compiler = { workspace = true }
serde_json = { workspace = true }
reqwest = { workspace = true }
uuid = { workspace = true }
walkdir = { workspace = true }
blake3 = { workspace = true }
hmac = { workspace = true }
sha2 = { workspace = true }
hex = { workspace = true }
base64 = { workspace = true }
aes-gcm = { workspace = true }
rand = { workspace = true }
crossbeam-channel = { workspace = true }
@@ -0,0 +1,725 @@
//! Built-in function dispatch for the El VM.
//!
//! Provides the set of built-in functions accessible to El programs at runtime.
//! This is the core portable set that `elvm` supports on all platforms.
use el_compiler::Value;
use std::collections::HashMap;
/// Result of a builtin dispatch attempt.
pub enum BuiltinResult {
/// The builtin was handled; return value (if any) is on the stack.
Handled,
/// The `http_serve` builtin was invoked; the server loop is running.
HttpServe,
/// The program called `exit(code)`.
Exit(i32),
/// The name is not a known builtin — try user-defined functions.
NotBuiltin,
}
/// Dispatch a built-in call.
///
/// # Arguments
/// * `name` — function name
/// * `arity` — number of arguments already on the stack
/// * `stack` — the VM value stack (may be modified)
/// * `args` — program arguments (for the `args()` builtin)
/// * `state` — global mutable string state
pub fn dispatch(
name: &str,
_arity: u32,
stack: &mut Vec<Value>,
program_args: &[String],
state: &mut HashMap<String, String>,
) -> BuiltinResult {
match name {
// ── I/O ──────────────────────────────────────────────────────────────
"print" => {
let v = stack.pop().unwrap_or(Value::Nil);
print!("{v}");
stack.push(Value::Nil);
BuiltinResult::Handled
}
"println" | "log" => {
let v = stack.pop().unwrap_or(Value::Nil);
println!("{v}");
stack.push(Value::Nil);
BuiltinResult::Handled
}
"print_err" | "log_debug" | "log_info" | "log_warn" | "log_error" => {
let v = stack.pop().unwrap_or(Value::Nil);
eprintln!("{v}");
stack.push(Value::Nil);
BuiltinResult::Handled
}
// ── Process ───────────────────────────────────────────────────────────
"args" => {
let list = program_args.iter().map(|s| Value::Str(s.clone())).collect();
stack.push(Value::List(list));
BuiltinResult::Handled
}
"exit" => {
let code = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as i32,
_ => 0,
};
BuiltinResult::Exit(code)
}
"env" => {
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let val = std::env::var(&key).unwrap_or_default();
stack.push(Value::Str(val));
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
}
"getpid" => {
stack.push(Value::Int(std::process::id() as i64));
BuiltinResult::Handled
}
"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
}
"sleep_secs" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as u64,
Value::Float(f) => f as u64,
_ => 0,
};
std::thread::sleep(std::time::Duration::from_secs(s));
stack.push(Value::Nil);
BuiltinResult::Handled
}
"shell_exec" | "sh" => {
let cmd = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let output = std::process::Command::new("sh")
.arg("-c")
.arg(&cmd)
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default();
stack.push(Value::Str(output));
BuiltinResult::Handled
}
// ── String operations ─────────────────────────────────────────────────
"str_len" | "string_len" | "native_string_len" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Int(s.len() as i64));
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
}
"str_contains" | "native_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
}
"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_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
}
"str_split" => {
let delim = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let parts: Vec<Value> = s.split(delim.as_str()).map(|p| Value::Str(p.to_string())).collect();
stack.push(Value::List(parts));
BuiltinResult::Handled
}
"str_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 s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
stack.push(Value::Str(s.replace(pattern.as_str(), &replacement)));
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_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
}
"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
}
"str_slice" => {
let end = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as usize, _ => 0 };
let start = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as usize, _ => 0 };
let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let chars: Vec<char> = s.chars().collect();
let slice: String = chars[start.min(chars.len())..end.min(chars.len())].iter().collect();
stack.push(Value::Str(slice));
BuiltinResult::Handled
}
"str_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.as_str()).map(|i| i as i64).unwrap_or(-1);
stack.push(Value::Int(idx));
BuiltinResult::Handled
}
"str_char_at" => {
let idx = 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 c = s.chars().nth(idx).map(|c| Value::Str(c.to_string())).unwrap_or(Value::Nil);
stack.push(c);
BuiltinResult::Handled
}
// ── Type conversion ───────────────────────────────────────────────────
"int_to_str" | "str_format" | "native_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
}
"float_to_str" => {
let f = match stack.pop().unwrap_or(Value::Nil) {
Value::Float(f) => f,
Value::Int(n) => n as f64,
_ => 0.0,
};
stack.push(Value::Str(f.to_string()));
BuiltinResult::Handled
}
"bool_to_str" => {
let b = match stack.pop().unwrap_or(Value::Nil) { Value::Bool(b) => b, _ => false };
stack.push(Value::Str(b.to_string()));
BuiltinResult::Handled
}
"str_to_int" | "parse_int" | "native_str_to_int" => {
let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let v = s.trim().parse::<i64>().map(Value::Int).unwrap_or(Value::Nil);
stack.push(v);
BuiltinResult::Handled
}
"str_to_float" | "parse_float" => {
let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let v = s.trim().parse::<f64>().map(Value::Float).unwrap_or(Value::Nil);
stack.push(v);
BuiltinResult::Handled
}
"int_to_float" => {
let n = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n, _ => 0 };
stack.push(Value::Float(n as f64));
BuiltinResult::Handled
}
"float_to_int" => {
let f = match stack.pop().unwrap_or(Value::Nil) { Value::Float(f) => f, Value::Int(n) => n as f64, _ => 0.0 };
stack.push(Value::Int(f as i64));
BuiltinResult::Handled
}
"is_nil" => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(Value::Bool(matches!(v, Value::Nil)));
BuiltinResult::Handled
}
"unwrap_or" => {
let default = stack.pop().unwrap_or(Value::Nil);
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(if matches!(v, Value::Nil) { default } else { v });
BuiltinResult::Handled
}
// ── List operations ───────────────────────────────────────────────────
"list_len" | "native_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" | "native_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
}
"native_string_chars" => {
// Split a string into a list of single-character strings.
let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let chars: Vec<Value> = s.chars().map(|c| Value::Str(c.to_string())).collect();
stack.push(Value::List(chars));
BuiltinResult::Handled
}
"list_push" | "native_list_append" => {
let item = stack.pop().unwrap_or(Value::Nil);
let list = match stack.pop().unwrap_or(Value::Nil) { Value::List(mut l) => { l.push(item); l }, _ => vec![item] };
stack.push(Value::List(list));
BuiltinResult::Handled
}
"list_pop" => {
let list = match stack.pop().unwrap_or(Value::Nil) { Value::List(mut l) => { l.pop(); l }, _ => vec![] };
stack.push(Value::List(list));
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 s: String = list.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(&sep);
stack.push(Value::Str(s));
BuiltinResult::Handled
}
"list_contains" => {
let item = 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(&item)));
BuiltinResult::Handled
}
"list_concat" => {
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 mut result = a;
result.extend(b);
stack.push(Value::List(result));
BuiltinResult::Handled
}
"list_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
}
"list_range" => {
let end = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n, _ => 0 };
let start = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n, _ => 0 };
let list: Vec<Value> = (start..end).map(Value::Int).collect();
stack.push(Value::List(list));
BuiltinResult::Handled
}
"list_set" => {
let val = stack.pop().unwrap_or(Value::Nil);
let idx = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as usize, _ => 0 };
let mut list = match stack.pop().unwrap_or(Value::Nil) { Value::List(l) => l, _ => vec![] };
if idx < list.len() { list[idx] = val; }
stack.push(Value::List(list));
BuiltinResult::Handled
}
"list_new" | "list_empty" | "native_list_empty" => {
stack.push(Value::List(vec![]));
BuiltinResult::Handled
}
"__build_list__" => {
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
}
// ── JSON ──────────────────────────────────────────────────────────────
"json_get" | "json_get_string" => {
let key = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let json_str = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let val = serde_json::from_str::<serde_json::Value>(&json_str)
.ok()
.and_then(|v| v.get(&key).cloned())
.map(|v| crate::json_to_value(&v))
.unwrap_or(Value::Nil);
stack.push(val);
BuiltinResult::Handled
}
"json_stringify" | "json_encode" => {
let v = stack.pop().unwrap_or(Value::Nil);
let json = value_to_json(&v);
stack.push(Value::Str(json.to_string()));
BuiltinResult::Handled
}
"json_parse" | "json_decode" => {
let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let val = serde_json::from_str::<serde_json::Value>(&s)
.map(|v| crate::json_to_value(&v))
.unwrap_or(Value::Nil);
stack.push(val);
BuiltinResult::Handled
}
"json_keys" => {
let json_str = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let keys = serde_json::from_str::<serde_json::Value>(&json_str)
.ok()
.and_then(|v| v.as_object().cloned())
.map(|o| o.keys().map(|k| Value::Str(k.clone())).collect::<Vec<_>>())
.unwrap_or_default();
stack.push(Value::List(keys));
BuiltinResult::Handled
}
"json_set" => {
let val = stack.pop().unwrap_or(Value::Nil);
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 mut obj = serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&json_str)
.unwrap_or_default();
obj.insert(key, value_to_json(&val));
stack.push(Value::Str(serde_json::Value::Object(obj).to_string()));
BuiltinResult::Handled
}
// ── Time ──────────────────────────────────────────────────────────────
"now_millis" | "time_now_ms" | "unix_timestamp" | "timestamp" => {
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
}
"time_now_s" => {
let s = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
stack.push(Value::Int(s));
BuiltinResult::Handled
}
"time_now_utc" => {
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
}
// ── Math ──────────────────────────────────────────────────────────────
"math_sin" => { let x = pop_f64(stack); stack.push(Value::Float(x.sin())); BuiltinResult::Handled }
"math_cos" => { let x = pop_f64(stack); stack.push(Value::Float(x.cos())); BuiltinResult::Handled }
"math_tan" => { let x = pop_f64(stack); stack.push(Value::Float(x.tan())); BuiltinResult::Handled }
"math_asin" => { let x = pop_f64(stack); stack.push(Value::Float(x.asin())); BuiltinResult::Handled }
"math_acos" => { let x = pop_f64(stack); stack.push(Value::Float(x.acos())); BuiltinResult::Handled }
"math_exp" => { let x = pop_f64(stack); stack.push(Value::Float(x.exp())); BuiltinResult::Handled }
"math_ln" => { let x = pop_f64(stack); stack.push(Value::Float(x.ln())); BuiltinResult::Handled }
"math_log2" => { let x = pop_f64(stack); stack.push(Value::Float(x.log2())); BuiltinResult::Handled }
"math_log10"=> { let x = pop_f64(stack); stack.push(Value::Float(x.log10()));BuiltinResult::Handled }
"math_pi" => { stack.push(Value::Float(std::f64::consts::PI)); BuiltinResult::Handled }
"math_e" => { stack.push(Value::Float(std::f64::consts::E)); BuiltinResult::Handled }
"math_atan2"=> {
let y = pop_f64(stack);
let x = pop_f64(stack);
stack.push(Value::Float(x.atan2(y)));
BuiltinResult::Handled
}
"math_mod" => {
let b = pop_f64(stack);
let a = pop_f64(stack);
stack.push(Value::Float(a % b));
BuiltinResult::Handled
}
// ── Crypto / ID ───────────────────────────────────────────────────────
"uuid_new" | "uuid_v4" | "crypto_uuid" => {
let id = uuid::Uuid::new_v4().to_string();
stack.push(Value::Str(id));
BuiltinResult::Handled
}
"blake3_hash" | "crypto_hash_blake3" => {
let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let hash = blake3::hash(s.as_bytes());
stack.push(Value::Str(hash.to_hex().to_string()));
BuiltinResult::Handled
}
"hash_sha256" | "crypto_hash_sha256" => {
// Use blake3 as a stable hash (sha2 not linked in elvm).
let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let hash = blake3::hash(s.as_bytes());
stack.push(Value::Str(hash.to_hex().to_string()));
BuiltinResult::Handled
}
"base64_encode" => {
use base64::Engine;
let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
stack.push(Value::Str(base64::engine::general_purpose::STANDARD.encode(s.as_bytes())));
BuiltinResult::Handled
}
"base64_decode" => {
use base64::Engine;
let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let decoded = base64::engine::general_purpose::STANDARD
.decode(s.as_bytes())
.map(|b| String::from_utf8_lossy(&b).to_string())
.unwrap_or_default();
stack.push(Value::Str(decoded));
BuiltinResult::Handled
}
// ── Filesystem ────────────────────────────────────────────────────────
"fs_read" => {
let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => { stack.push(Value::Nil); return BuiltinResult::Handled; } };
let val = std::fs::read_to_string(&path).map(Value::Str).unwrap_or(Value::Nil);
stack.push(val);
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_exists" => {
let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
stack.push(Value::Bool(std::path::Path::new(&path).exists()));
BuiltinResult::Handled
}
"fs_mkdir" => {
let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let ok = std::fs::create_dir_all(&path).is_ok();
stack.push(Value::Bool(ok));
BuiltinResult::Handled
}
"fs_list" => {
let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let entries = std::fs::read_dir(&path)
.map(|rd| rd.filter_map(|e| e.ok().and_then(|e| e.file_name().into_string().ok().map(Value::Str))).collect::<Vec<_>>())
.unwrap_or_default();
stack.push(Value::List(entries));
BuiltinResult::Handled
}
"fs_append" => {
use std::io::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::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_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
}
"fs_list_recursive" => {
let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let skip_dirs = ["node_modules", ".git", ".nc", "target", "__pycache__", ".el"];
let mut files = Vec::new();
if let Ok(walker) = walkdir::WalkDir::new(&path).into_iter().collect::<Result<Vec<_>, _>>() {
for entry in walker {
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
}
// ── Global state ──────────────────────────────────────────────────────
"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() };
state.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 = state.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() };
state.remove(&key);
stack.push(Value::Bool(true));
BuiltinResult::Handled
}
"state_keys" => {
let keys: Vec<Value> = state.keys().cloned().map(Value::Str).collect();
stack.push(Value::List(keys));
BuiltinResult::Handled
}
// ── HTTP client ───────────────────────────────────────────────────────
"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
}
"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")
.body(body)
.send()
.and_then(|r| r.text())
.unwrap_or_else(|e| format!("{{\"error\":\"{e}}}" ));
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"http_post_form_auth" => {
// args: url (bottom), auth_value, body (top)
let body = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
let auth = 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/x-www-form-urlencoded")
.header("Authorization", format!("Bearer {auth}"))
.body(body)
.send()
.and_then(|r| r.text())
.unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}"));
stack.push(Value::Str(result));
BuiltinResult::Handled
}
// ── App context (no-op stubs — app block not parsed by elvm) ─────────
"env_get" => {
let key = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
stack.push(Value::Str(std::env::var(&key).unwrap_or_default()));
BuiltinResult::Handled
}
"ctx_env" => { stack.push(Value::Str(std::env::var("NEURON_ENV").unwrap_or_else(|_| "dev".to_string()))); BuiltinResult::Handled }
"ctx_service" => { stack.push(Value::Str(String::new())); BuiltinResult::Handled }
"ctx_version" => { stack.push(Value::Str(String::new())); BuiltinResult::Handled }
"ctx_instance"=> { stack.push(Value::Str(uuid::Uuid::new_v4().to_string())); BuiltinResult::Handled }
"config" => { let _ = stack.pop(); stack.push(Value::Str(String::new())); BuiltinResult::Handled }
"secret" => { let _ = stack.pop(); stack.push(Value::Str(String::new())); BuiltinResult::Handled }
"flag" => { let _ = stack.pop(); stack.push(Value::Bool(false)); BuiltinResult::Handled }
// ── Color / formatting (terminal output) ─────────────────────────────
"color_bold" => {
let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
stack.push(Value::Str(format!("\x1b[1m{s}\x1b[0m")));
BuiltinResult::Handled
}
// ── http_serve (stub — available but note elvm doesn't have the tiny_http dep) ──
"http_serve" => {
// elvm does not bundle the HTTP server. Emit a helpful message.
let _ = stack.pop();
eprintln!("elvm: http_serve is not available in the standalone elvm binary. Use 'el run' for HTTP server programs.");
stack.push(Value::Nil);
BuiltinResult::HttpServe
}
// ── fn_ref helper ─────────────────────────────────────────────────────
"fn_ref" => {
let name_val = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
stack.push(Value::Str(name_val));
BuiltinResult::Handled
}
_ => BuiltinResult::NotBuiltin,
}
}
// ── Internal helpers ──────────────────────────────────────────────────────────
fn pop_f64(stack: &mut Vec<Value>) -> f64 {
match stack.pop().unwrap_or(Value::Nil) {
Value::Float(f) => f,
Value::Int(n) => n as f64,
_ => 0.0,
}
}
fn value_to_json(v: &Value) -> serde_json::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(value_to_json).collect()),
Value::Map(pairs) => {
let mut map = serde_json::Map::new();
for (k, v) in pairs { map.insert(k.clone(), value_to_json(v)); }
serde_json::Value::Object(map)
}
Value::ResultOk(inner) => serde_json::json!({ "ok": value_to_json(inner) }),
Value::ResultErr(inner) => serde_json::json!({ "err": value_to_json(inner) }),
Value::Struct { fields, .. } => {
let mut map = serde_json::Map::new();
for (k, v) in fields { map.insert(k.clone(), value_to_json(v)); }
serde_json::Value::Object(map)
}
}
}
File diff suppressed because it is too large Load Diff