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:
@@ -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
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user