Add pipe operator, with-update, retry/fallback, reason, parallel, trace, contract, deploy

Implements 8 new language features:
- |> pipe operator: a |> f desugars to f(a), left-associative chains
- with record update: let b = a with { field: val } — non-destructive struct update
- retry/fallback: retry N times { ... } fallback { ... } with counter-based loop codegen
- reason: AI inference primitive calling soma /v1/chat/completions at runtime
- parallel: concurrent execution block returning a Map of named results via threads
- trace: zero-cost observability block emitting TraceBegin/TraceEnd with ms timing
- requires: precondition annotation on fn, emits ContractCheck bytecode at entry
- deploy: deployment-as-syntax posting to soma /v1/deploy at runtime

All features thread through lexer → parser/AST → codegen → runtime interpreter.
This commit is contained in:
Will Anderson
2026-04-28 12:04:45 -05:00
parent f2202e0e5e
commit afd99f5e0d
10 changed files with 868 additions and 59 deletions
+427 -55
View File
@@ -1020,10 +1020,19 @@ fn run_sub_interpreter(
instructions: &[el_compiler::Bytecode],
fn_table: &std::collections::HashMap<String, usize>,
entry: usize,
) -> el_compiler::Value {
run_sub_interpreter_with_stack(instructions, fn_table, entry, vec![])
}
fn run_sub_interpreter_with_stack(
instructions: &[el_compiler::Bytecode],
fn_table: &std::collections::HashMap<String, usize>,
entry: usize,
initial_stack: Vec<el_compiler::Value>,
) -> el_compiler::Value {
use el_compiler::{Bytecode, Value};
let mut stack: Vec<Value> = Vec::new();
let mut stack: Vec<Value> = initial_stack;
let mut locals: std::collections::HashMap<String, Value> = std::collections::HashMap::new();
let mut call_stack: Vec<(usize, std::collections::HashMap<String, Value>)> = Vec::new();
let mut ip = entry;
@@ -1309,15 +1318,32 @@ fn run_interpreter_with_args(instructions: &[el_compiler::Bytecode], program_arg
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
// 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 || {
// Run a sub-interpreter starting at handle_request
// 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") {
let result = run_sub_interpreter(
// 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 {
@@ -1574,11 +1600,104 @@ fn run_interpreter_with_args(instructions: &[el_compiler::Bytecode], program_arg
Bytecode::SealedBegin => {}
Bytecode::SealedEnd => {}
Bytecode::Nop => {}
Bytecode::Reason { query } => {
let text = soma_reason(query);
stack.push(Value::Str(text));
}
Bytecode::Parallel { entries } => {
// Spawn one thread per entry, collect into Map
let instructions_arc = std::sync::Arc::new(instructions.to_vec());
let fn_table_arc = std::sync::Arc::new(fn_table.clone());
let locals_arc = std::sync::Arc::new(locals.clone());
let mut handles: Vec<(String, std::thread::JoinHandle<Value>)> = Vec::new();
for (name, entry_ip) in entries {
let instr_clone = instructions_arc.clone();
let ft_clone = fn_table_arc.clone();
let ep = *entry_ip;
let h = std::thread::spawn(move || {
run_sub_interpreter(&instr_clone, &ft_clone, ep)
});
handles.push((name.clone(), h));
}
let mut pairs = Vec::new();
for (name, h) in handles {
let v = h.join().unwrap_or(Value::Nil);
pairs.push((name, v));
}
stack.push(Value::Map(pairs));
}
Bytecode::TraceBegin { label } => {
let start_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
locals.insert(format!("__trace_start_{label}__"), Value::Int(start_ms as i64));
}
Bytecode::TraceEnd { label } => {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
let start_ms = match locals.get(&format!("__trace_start_{label}__")) {
Some(Value::Int(n)) => *n as u128,
_ => now_ms,
};
let elapsed = now_ms.saturating_sub(start_ms);
eprintln!("[trace] {label}: {elapsed}ms");
}
Bytecode::ContractCheck { message } => {
let cond = stack.pop().unwrap_or(Value::Nil);
if !matches!(cond, Value::Bool(true)) {
eprintln!("contract violation: {message}");
std::process::exit(1);
}
}
Bytecode::DeployFn { fn_name, route, target } => {
let soma_url = std::env::var("SOMA_URL")
.unwrap_or_else(|_| "https://neuron.neurontechnologies.ai".into());
let op_key = std::env::var("SOMA_OPERATOR_KEY").unwrap_or_default();
let body = serde_json::json!({
"fn_name": fn_name,
"route": route,
"target": target,
});
let resp = reqwest::blocking::Client::new()
.post(format!("{soma_url}/v1/deploy"))
.header("Authorization", format!("Bearer {op_key}"))
.json(&body)
.send()
.and_then(|r| r.text())
.unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}"));
eprintln!("[deploy] {fn_name} -> {route} via {target}: {resp}");
stack.push(Value::Str(resp));
}
_ => {}
}
ip += 1;
}
}
/// Call soma AI inference endpoint and return response text.
fn soma_reason(query: &str) -> String {
let soma_url = std::env::var("SOMA_URL")
.unwrap_or_else(|_| "https://neuron.neurontechnologies.ai".into());
let op_key = std::env::var("SOMA_OPERATOR_KEY").unwrap_or_default();
let body = serde_json::json!({
"model": "neuron",
"messages": [{"role": "user", "content": query}],
"max_tokens": 500
});
let resp = reqwest::blocking::Client::new()
.post(format!("{soma_url}/v1/chat/completions"))
.header("Authorization", format!("Bearer {op_key}"))
.json(&body)
.send()
.and_then(|r| r.json::<serde_json::Value>())
.ok();
resp.and_then(|v| v["choices"][0]["message"]["content"].as_str().map(|s| s.to_string()))
.unwrap_or_else(|| "[soma unavailable]".into())
}
enum BuiltinResult {
Handled,
Exit(i32),
@@ -2038,75 +2157,58 @@ fn dispatch_builtin(
// for each POST /axon/message by invoking the stored callback.
"http_serve" => {
let port = match stack.pop().unwrap_or(Value::Nil) {
// 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.
let port_val = stack.pop().unwrap_or(Value::Nil);
let port = match port_val {
Value::Int(n) => n as u16,
Value::Str(s) => s.parse::<u16>().unwrap_or(7890),
_ => 7890,
};
let addr = format!("0.0.0.0:{port}");
let server = tiny_http::Server::http(&addr)
.unwrap_or_else(|e| panic!("cannot bind to {addr}: {e}"));
println!("Neuron Code · Engram edition · http://localhost:{port}");
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();
// 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;
// Read body for all requests
let mut body = String::new();
{
use std::io::Read;
let _ = request.as_reader().read_to_string(&mut body);
}
// 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 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();
}
});
// 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__"));
let response_body = GLOBAL_STATE.with(|gs| {
gs.borrow().get("__response__").cloned()
.unwrap_or_else(|| r#"{"error":"no response"}"#.to_string())
});
// 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;
}
// GET / — web dashboard
if method == "GET" && (url == "/" || url == "/index.html") {
let html = include_str!("dashboard.html");
let _ = request.respond(
tiny_http::Response::from_string(html)
.with_header("Content-Type: text/html; charset=utf-8".parse::<tiny_http::Header>().unwrap())
);
continue;
}
// Default 404
let _ = request.respond(
tiny_http::Response::from_string(r#"{"error":"not found"}"#)
.with_status_code(404)
tiny_http::Response::from_string(response_body)
.with_header("Content-Type: application/json".parse::<tiny_http::Header>().unwrap())
);
}
stack.push(Value::Nil);
@@ -2986,6 +3088,276 @@ fn dispatch_builtin(
BuiltinResult::Handled
}
// ── Crypto / HMAC / base64 / uuid builtins ───────────────────────────
"hmac_sha256" => {
use hmac::{Hmac, Mac};
use sha2::Sha256;
let data = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let secret = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
.unwrap_or_else(|_| HmacSha256::new_from_slice(b"invalid").unwrap());
mac.update(data.as_bytes());
let result = mac.finalize();
let hex_str = hex::encode(result.into_bytes());
stack.push(Value::Str(hex_str));
BuiltinResult::Handled
}
"base64_url_encode" => {
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let encoded = URL_SAFE_NO_PAD.encode(s.as_bytes());
stack.push(Value::Str(encoded));
BuiltinResult::Handled
}
"base64_url_decode" => {
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let decoded = URL_SAFE_NO_PAD.decode(s.as_bytes())
.map(|b| String::from_utf8_lossy(&b).to_string())
.unwrap_or_default();
stack.push(Value::Str(decoded));
BuiltinResult::Handled
}
"unix_timestamp" => {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
stack.push(Value::Int(secs));
BuiltinResult::Handled
}
"uuid_v4" => {
let id = uuid::Uuid::new_v4().to_string();
stack.push(Value::Str(id));
BuiltinResult::Handled
}
// ── JSON encode/decode (Value ↔ String) ───────────────────────────────
"json_encode" => {
// Encodes a Map, List, Struct or primitive Value to a JSON string.
let v = stack.pop().unwrap_or(Value::Nil);
let jv = el_value_to_json_value(&v);
let s = serde_json::to_string(&jv).unwrap_or_else(|_| "null".to_string());
stack.push(Value::Str(s));
BuiltinResult::Handled
}
"json_decode" => {
// Decodes a JSON string to a Value::Map (or List/primitive).
let s = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => "null".to_string(),
};
let jv: serde_json::Value = serde_json::from_str(&s).unwrap_or(serde_json::Value::Null);
stack.push(json_value_to_el_value(&jv));
BuiltinResult::Handled
}
"json_get_string" => {
// json_get_string(map_or_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::Str(json_str) => {
serde_json::from_str::<serde_json::Value>(json_str)
.ok()
.and_then(|jv| jv.get(&key).and_then(|v| v.as_str().map(str::to_string)))
.unwrap_or_default()
}
_ => String::new(),
};
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"json_get_int" => {
// json_get_int(map_or_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::Str(json_str) => {
serde_json::from_str::<serde_json::Value>(json_str)
.ok()
.and_then(|jv| jv.get(&key).and_then(|v| v.as_i64()))
.unwrap_or(0)
}
_ => 0,
};
stack.push(Value::Int(result));
BuiltinResult::Handled
}
"json_get_array" => {
// json_get_array(map_or_json_str, key) -> [String]
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let v = stack.pop().unwrap_or(Value::Nil);
let result: Vec<Value> = match &v {
Value::Map(pairs) => pairs.iter()
.find(|(k, _)| k == &key)
.map(|(_, v)| match v {
Value::List(items) => items.clone(),
_ => vec![],
})
.unwrap_or_default(),
Value::Str(json_str) => {
serde_json::from_str::<serde_json::Value>(json_str)
.ok()
.and_then(|jv| jv.get(&key).and_then(|v| v.as_array().cloned()))
.map(|arr| arr.iter().map(|item| {
match item {
serde_json::Value::String(s) => Value::Str(s.clone()),
other => Value::Str(other.to_string()),
}
}).collect())
.unwrap_or_default()
}
_ => vec![],
};
stack.push(Value::List(result));
BuiltinResult::Handled
}
// ── HTTP auth builtins (Bearer token) ─────────────────────────────────
"http_get_auth" => {
let token = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let url = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = reqwest::blocking::Client::new()
.get(&url)
.header("Authorization", format!("Bearer {token}"))
.send()
.and_then(|r| r.text())
.unwrap_or_default();
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"http_put_auth" => {
let body = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let token = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let url = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = reqwest::blocking::Client::new()
.put(&url)
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/json")
.body(body)
.send()
.and_then(|r| r.text())
.unwrap_or_default();
stack.push(Value::Str(result));
BuiltinResult::Handled
}
"http_delete_auth" => {
let token = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let url = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
};
let result = reqwest::blocking::Client::new()
.delete(&url)
.header("Authorization", format!("Bearer {token}"))
.send()
.and_then(|r| r.text())
.unwrap_or_default();
stack.push(Value::Str(result));
BuiltinResult::Handled
}
// ── 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" => {