Add server-side builtins, import system, and http_serve for Neuron Code rewrite

- Import resolution: resolve_imports() pre-processes import statements by
  reading and concatenating referenced .el files before compilation
- http_serve builtin: tiny_http-based server on configurable port; POST
  /axon/message stores request in __request__ state, invokes handle_request
  entry point via sub-interpreter, reads __response__ state for reply
- New builtins: blake3_hash, uuid_new, fs_list_recursive, fs_mkdir, fs_exists,
  path_join, path_parent, str_trim, str_contains, str_replace, str_starts_with,
  str_ends_with, str_last_index_of, json_get, json_array_push, json_array_len,
  now_millis, http_get, http_post, int_to_str
- Catch-all arms in el-types and el-compiler for new AST variants (Import,
  ProtocolDef, ImplDef, Closure, Try, MapLiteral, TypeExpr::Result, TypeExpr::Map)
- Parser: decorators field on FnDef, import/protocol/impl parsing
This commit is contained in:
Will Anderson
2026-04-27 20:08:55 -05:00
parent 46d5650e45
commit 316c0a85ce
12 changed files with 1796 additions and 181 deletions
+4
View File
@@ -24,3 +24,7 @@ thiserror = { workspace = true }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
reqwest = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true }
walkdir = { workspace = true }
tiny_http = { workspace = true }
blake3 = { workspace = true }
+838 -4
View File
@@ -36,6 +36,26 @@ use el_test;
use el_manifest::{BuildTarget, Manifest};
use el_seal::{seal as seal_fn, unseal as unseal_fn, SealedArtifact, DeploymentBinding, SealAlgorithm, SealConfig};
// ── Global state (thread-local for simplicity) ────────────────────────────────
thread_local! {
static GLOBAL_STATE: std::cell::RefCell<std::collections::HashMap<String, String>> =
std::cell::RefCell::new(std::collections::HashMap::new());
/// Callback set by the interpreter before http_serve blocks.
/// When a request arrives, http_serve calls this to invoke handle_request.
static HTTP_SERVE_CALL: std::cell::RefCell<Option<Box<dyn Fn()>>> =
std::cell::RefCell::new(None);
/// Shared bytecode instructions for sub-interpreter calls from http_serve.
static SERVE_INSTRUCTIONS: std::cell::RefCell<Option<std::sync::Arc<Vec<el_compiler::Bytecode>>>> =
std::cell::RefCell::new(None);
/// Shared fn_table for sub-interpreter calls.
static SERVE_FN_TABLE: std::cell::RefCell<Option<std::sync::Arc<std::collections::HashMap<String, usize>>>> =
std::cell::RefCell::new(None);
}
// ── CLI definition ────────────────────────────────────────────────────────────
#[derive(Parser, Debug)]
@@ -441,8 +461,8 @@ async fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
// ── Low-level / single-file ───────────────────────────────────────────
Command::RunFile { file, args } => {
let source = std::fs::read_to_string(&file)
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
let source = resolve_imports(&file)
.map_err(|e| format!("cannot resolve imports for {}: {e}", file.display()))?;
let opts = CompilerOptions {
target: Target::Debug,
@@ -689,6 +709,56 @@ fn cmd_plugin_add(plugin: &str) -> Result<(), Box<dyn std::error::Error>> {
// ── Helpers ───────────────────────────────────────────────────────────────────
/// Resolve `import "path.el"` directives by reading and concatenating source files.
/// Imports are resolved relative to the directory of the file being imported from.
/// Circular imports are detected via a visited set.
fn resolve_imports(file: &std::path::Path) -> Result<String, Box<dyn std::error::Error>> {
let mut visited = std::collections::HashSet::new();
resolve_imports_inner(file, &mut visited)
}
fn resolve_imports_inner(
file: &std::path::Path,
visited: &mut std::collections::HashSet<PathBuf>,
) -> Result<String, Box<dyn std::error::Error>> {
let canonical = file.canonicalize()
.unwrap_or_else(|_| file.to_path_buf());
if visited.contains(&canonical) {
return Ok(String::new()); // circular — skip
}
visited.insert(canonical.clone());
let dir = file.parent().unwrap_or(std::path::Path::new("."));
let source = std::fs::read_to_string(file)
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
let mut out = String::new();
for line in source.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("import ") {
// import "filename.el"
let rest = rest.trim();
if rest.starts_with('"') && rest.ends_with('"') {
let import_path_str = &rest[1..rest.len() - 1];
let import_path = dir.join(import_path_str);
let imported = resolve_imports_inner(&import_path, visited)?;
out.push_str(&imported);
out.push('\n');
} else {
out.push_str(line);
out.push('\n');
}
} else {
out.push_str(line);
out.push('\n');
}
}
Ok(out)
}
/// Find the nearest `el.toml` starting from the current directory.
fn resolve_manifest(path: Option<&std::path::Path>) -> Result<PathBuf, Box<dyn std::error::Error>> {
if let Some(p) = path {
@@ -776,6 +846,201 @@ fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
run_interpreter_with_args(instructions, &[]);
}
/// Run a sub-interpreter starting at the given function entry point.
/// Returns the value left on the stack (the return value).
fn run_sub_interpreter(
instructions: &[el_compiler::Bytecode],
fn_table: &std::collections::HashMap<String, usize>,
entry: usize,
) -> el_compiler::Value {
use el_compiler::{Bytecode, Value};
let mut stack: Vec<Value> = Vec::new();
let mut locals: std::collections::HashMap<String, Value> = std::collections::HashMap::new();
let mut call_stack: Vec<(usize, std::collections::HashMap<String, Value>)> = Vec::new();
let mut ip = entry;
let program_args: Vec<String> = vec![];
// Bind zero params (handle_request takes none)
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::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::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::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::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 => {
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) => {
stack.pop();
stack.push(Value::Str(format!("<field:{field}>")));
}
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);
}
_ => stack.push(Value::Nil),
}
}
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 { .. } => {
stack.push(Value::List(vec![]));
}
_ => {}
}
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};
@@ -811,6 +1076,45 @@ fn run_interpreter_with_args(instructions: &[el_compiler::Bytecode], program_arg
}
}
// 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
HTTP_SERVE_CALL.with(|f| {
*f.borrow_mut() = Some(Box::new(move || {
// Run a sub-interpreter starting at handle_request
if let Some(entry) = arc_fn_table_clone.get("handle_request") {
let result = run_sub_interpreter(
&arc_instructions_clone,
&arc_fn_table_clone,
*entry,
);
// Store the result as __response__ in global state
let response = match result {
el_compiler::Value::Str(s) => s,
other => other.to_string(),
};
GLOBAL_STATE.with(|gs| {
gs.borrow_mut().insert("__response__".to_string(), response);
});
} else {
GLOBAL_STATE.with(|gs| {
gs.borrow_mut().insert(
"__response__".to_string(),
r#"{"error":"handle_request function not found"}"#.to_string(),
);
});
}
}));
});
// Call stack for user-defined function calls: (return_ip, saved_locals)
let mut call_stack: Vec<(usize, std::collections::HashMap<String, Value>)> = Vec::new();
@@ -921,7 +1225,7 @@ fn run_interpreter_with_args(instructions: &[el_compiler::Bytecode], program_arg
Bytecode::Call { name, arity } => {
let result = dispatch_builtin(name, *arity, &mut stack, program_args);
match result {
BuiltinResult::Handled => {}
BuiltinResult::Handled | BuiltinResult::HttpServe => {}
BuiltinResult::Exit(code) => std::process::exit(code),
BuiltinResult::NotBuiltin => {
// Try user-defined function
@@ -1011,6 +1315,9 @@ 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(
@@ -1237,6 +1544,533 @@ fn dispatch_builtin(
stack.push(Value::List(items));
BuiltinResult::Handled
}
// ── Filesystem builtins ───────────────────────────────────────────────
"fs_read" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => { stack.push(Value::Nil); return BuiltinResult::Handled; }
};
let result = std::fs::read_to_string(&path)
.map(Value::Str)
.unwrap_or(Value::Nil);
stack.push(result);
BuiltinResult::Handled
}
"fs_write" => {
let content = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let ok = std::fs::write(&path, &content).is_ok();
stack.push(Value::Bool(ok));
BuiltinResult::Handled
}
"fs_append" => {
let content = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
use std::io::Write;
let ok = std::fs::OpenOptions::new()
.create(true).append(true).open(&path)
.and_then(|mut f| f.write_all(content.as_bytes()))
.is_ok();
stack.push(Value::Bool(ok));
BuiltinResult::Handled
}
"fs_exists" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Bool(std::path::Path::new(&path).exists()));
BuiltinResult::Handled
}
"fs_mkdir" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let ok = std::fs::create_dir_all(&path).is_ok();
stack.push(Value::Bool(ok));
BuiltinResult::Handled
}
"fs_list" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let entries = std::fs::read_dir(&path)
.map(|rd| {
rd.filter_map(|e| {
e.ok().and_then(|e| {
e.file_name().into_string().ok().map(Value::Str)
})
}).collect::<Vec<_>>()
})
.unwrap_or_default();
stack.push(Value::List(entries));
BuiltinResult::Handled
}
"fs_remove" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let p = std::path::Path::new(&path);
let ok = if p.is_dir() {
std::fs::remove_dir(p).is_ok()
} else {
std::fs::remove_file(p).is_ok()
};
stack.push(Value::Bool(ok));
BuiltinResult::Handled
}
"fs_is_dir" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Bool(std::path::Path::new(&path).is_dir()));
BuiltinResult::Handled
}
"path_join" => {
let name = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let base = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let joined = std::path::Path::new(&base).join(&name);
stack.push(Value::Str(joined.to_string_lossy().to_string()));
BuiltinResult::Handled
}
"path_parent" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let parent = std::path::Path::new(&path)
.parent()
.map(|p| Value::Str(p.to_string_lossy().to_string()))
.unwrap_or(Value::Nil);
stack.push(parent);
BuiltinResult::Handled
}
// ── Filesystem recursive list ─────────────────────────────────────────
"fs_list_recursive" => {
let path = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let skip_dirs = ["node_modules", ".git", ".nc", "target", "__pycache__", ".el"];
let mut files = Vec::new();
if let Ok(walker) = walkdir::WalkDir::new(&path).into_iter().collect::<Result<Vec<_>, _>>() {
for entry in walker {
// Skip hidden/build dirs
let should_skip = entry.path().components().any(|c| {
let s = c.as_os_str().to_string_lossy();
skip_dirs.iter().any(|d| s == *d)
});
if should_skip && entry.path() != std::path::Path::new(&path) {
continue;
}
if entry.file_type().is_file() {
files.push(Value::Str(entry.path().to_string_lossy().to_string()));
}
}
}
stack.push(Value::List(files));
BuiltinResult::Handled
}
// ── Crypto / ID builtins ──────────────────────────────────────────────
"blake3_hash" => {
let content = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let hash = blake3::hash(content.as_bytes());
stack.push(Value::Str(hash.to_hex().to_string()));
BuiltinResult::Handled
}
"uuid_new" => {
let id = uuid::Uuid::new_v4().to_string();
stack.push(Value::Str(id));
BuiltinResult::Handled
}
"now_millis" => {
let ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
stack.push(Value::Int(ms));
BuiltinResult::Handled
}
// ── State builtins ────────────────────────────────────────────────────
"state_set" => {
let value = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
GLOBAL_STATE.with(|gs| gs.borrow_mut().insert(key, value));
stack.push(Value::Bool(true));
BuiltinResult::Handled
}
"state_get" => {
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let val = GLOBAL_STATE.with(|gs| {
gs.borrow().get(&key).cloned().map(Value::Str).unwrap_or(Value::Nil)
});
stack.push(val);
BuiltinResult::Handled
}
"state_del" => {
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
GLOBAL_STATE.with(|gs| gs.borrow_mut().remove(&key));
stack.push(Value::Bool(true));
BuiltinResult::Handled
}
"state_keys" => {
let keys = GLOBAL_STATE.with(|gs| {
gs.borrow().keys().cloned().map(Value::Str).collect::<Vec<_>>()
});
stack.push(Value::List(keys));
BuiltinResult::Handled
}
// ── HTTP server builtin ───────────────────────────────────────────────
// http_serve starts a blocking HTTP server. It calls handle_request
// for each POST /axon/message by invoking the stored callback.
"http_serve" => {
let port = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as u16,
_ => 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!("Neuron Code · Engram edition · http://localhost:{port}");
for mut request in server.incoming_requests() {
let method = request.method().to_string();
let url = request.url().to_string();
// GET /health
if method == "GET" && url == "/health" {
let _ = request.respond(
tiny_http::Response::from_string(r#"{"status":"ok","version":"0.1.0"}"#)
.with_header("Content-Type: application/json".parse::<tiny_http::Header>().unwrap())
);
continue;
}
// POST /axon/message
if method == "POST" && (url == "/axon/message" || url.starts_with("/axon/message?")) {
// Read body
let mut body = String::new();
{
use std::io::Read;
let _ = request.as_reader().read_to_string(&mut body);
}
// Store request in state
GLOBAL_STATE.with(|gs| gs.borrow_mut().insert("__request__".to_string(), body));
GLOBAL_STATE.with(|gs| gs.borrow_mut().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":"no response"}"#.to_string())
});
let _ = request.respond(
tiny_http::Response::from_string(response_body)
.with_header("Content-Type: application/json".parse::<tiny_http::Header>().unwrap())
);
continue;
}
// Default 404
let _ = request.respond(
tiny_http::Response::from_string(r#"{"error":"not found"}"#)
.with_status_code(404)
);
}
stack.push(Value::Nil);
BuiltinResult::Handled
}
// ── String utility builtins ───────────────────────────────────────────
"str_replace" => {
let to = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let from = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Str(s.replace(&from, &to)));
BuiltinResult::Handled
}
"str_to_lowercase" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Str(s.to_lowercase()));
BuiltinResult::Handled
}
"str_trim" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Str(s.trim().to_string()));
BuiltinResult::Handled
}
"str_index_of" => {
let sub = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let idx = s.find(&sub).map(|i| i as i64).unwrap_or(-1);
stack.push(Value::Int(idx));
BuiltinResult::Handled
}
"str_last_index_of" => {
let sub = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let idx = s.rfind(&sub).map(|i| i as i64).unwrap_or(-1);
stack.push(Value::Int(idx));
BuiltinResult::Handled
}
"str_slice" => {
let end = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as usize,
_ => 0,
};
let start = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as usize,
_ => 0,
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let chars: Vec<char> = s.chars().collect();
let start = start.min(chars.len());
let end = end.min(chars.len());
let slice: String = chars[start..end].iter().collect();
stack.push(Value::Str(slice));
BuiltinResult::Handled
}
"str_ends_with" => {
let suffix = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
stack.push(Value::Bool(s.ends_with(suffix.as_str())));
BuiltinResult::Handled
}
// ── JSON utility builtins ─────────────────────────────────────────────
"json_set" => {
let value = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let json_str = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => "{}".to_string(),
};
let result = serde_json::from_str::<serde_json::Value>(&json_str)
.ok()
.and_then(|mut v| {
if let serde_json::Value::Object(ref mut map) = v {
// Try to parse value as JSON, otherwise store as string
let jv = serde_json::from_str(&value)
.unwrap_or(serde_json::Value::String(value.clone()));
map.insert(key.clone(), jv);
serde_json::to_string(&v).ok()
} else {
None
}
})
.unwrap_or_else(|| format!("{{\"{key}\":\"{value}\"}}"));
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"json_keys" => {
let json_str = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => "{}".to_string(),
};
let keys = serde_json::from_str::<serde_json::Value>(&json_str)
.ok()
.and_then(|v| {
if let serde_json::Value::Object(map) = v {
Some(map.keys().cloned().map(Value::Str).collect::<Vec<_>>())
} else {
None
}
})
.unwrap_or_default();
stack.push(Value::List(keys));
BuiltinResult::Handled
}
"json_array_push" => {
let item = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => "null".to_string(),
};
let json_str = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => "[]".to_string(),
};
let result = serde_json::from_str::<serde_json::Value>(&json_str)
.ok()
.and_then(|mut v| {
if let serde_json::Value::Array(ref mut arr) = v {
let item_val = serde_json::from_str(&item)
.unwrap_or(serde_json::Value::String(item.clone()));
arr.push(item_val);
serde_json::to_string(&v).ok()
} else {
None
}
})
.unwrap_or_else(|| format!("[{}]", item));
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"json_array_len" => {
let json_str = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => "[]".to_string(),
};
let len = serde_json::from_str::<serde_json::Value>(&json_str)
.ok()
.and_then(|v| if let serde_json::Value::Array(arr) = v { Some(arr.len() as i64) } else { None })
.unwrap_or(0);
stack.push(Value::Int(len));
BuiltinResult::Handled
}
"json_array_get" => {
let idx = match stack.pop().unwrap_or(Value::Nil) {
Value::Int(n) => n as usize,
_ => { stack.push(Value::Nil); return BuiltinResult::Handled; }
};
let json_str = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => "[]".to_string(),
};
let item = serde_json::from_str::<serde_json::Value>(&json_str)
.ok()
.and_then(|v| {
if let serde_json::Value::Array(arr) = v {
arr.get(idx).map(|item| Value::Str(item.to_string()))
} else {
None
}
})
.unwrap_or(Value::Nil);
stack.push(item);
BuiltinResult::Handled
}
"int_parse" => {
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = s.trim().parse::<i64>()
.map(Value::Int)
.unwrap_or(Value::Nil);
stack.push(result);
BuiltinResult::Handled
}
"bool_to_str" => {
let b = match stack.pop().unwrap_or(Value::Nil) {
Value::Bool(b) => b,
_ => false,
};
stack.push(Value::Str(if b { "true".to_string() } else { "false".to_string() }));
BuiltinResult::Handled
}
"list_join" => {
let sep = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let list = match stack.pop().unwrap_or(Value::Nil) {
Value::List(l) => l,
_ => vec![],
};
let strs: Vec<String> = list.iter().map(|v| v.to_string()).collect();
stack.push(Value::Str(strs.join(&sep)));
BuiltinResult::Handled
}
_ => BuiltinResult::NotBuiltin,
}
}
@@ -1292,7 +2126,7 @@ fn run_interpreter_debug(instructions: &[el_compiler::Bytecode], debugger: &mut
Bytecode::Call { name, arity } => {
let result = dispatch_builtin(name, *arity, &mut stack, &program_args);
match result {
BuiltinResult::Handled | BuiltinResult::NotBuiltin => {}
BuiltinResult::Handled | BuiltinResult::NotBuiltin | BuiltinResult::HttpServe => {}
BuiltinResult::Exit(code) => std::process::exit(code),
}
}