Archived
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:
+427
-55
@@ -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" => {
|
||||
|
||||
Reference in New Issue
Block a user