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],
|
instructions: &[el_compiler::Bytecode],
|
||||||
fn_table: &std::collections::HashMap<String, usize>,
|
fn_table: &std::collections::HashMap<String, usize>,
|
||||||
entry: 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 {
|
) -> el_compiler::Value {
|
||||||
use el_compiler::{Bytecode, 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 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 call_stack: Vec<(usize, std::collections::HashMap<String, Value>)> = Vec::new();
|
||||||
let mut ip = entry;
|
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_INSTRUCTIONS.with(|si| *si.borrow_mut() = Some(arc_instructions.clone()));
|
||||||
SERVE_FN_TABLE.with(|sf| *sf.borrow_mut() = Some(arc_fn_table.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| {
|
HTTP_SERVE_CALL.with(|f| {
|
||||||
*f.borrow_mut() = Some(Box::new(move || {
|
*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") {
|
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_instructions_clone,
|
||||||
&arc_fn_table_clone,
|
&arc_fn_table_clone,
|
||||||
*entry,
|
*entry,
|
||||||
|
initial_stack,
|
||||||
);
|
);
|
||||||
// Store the result as __response__ in global state
|
// Store the result as __response__ in global state
|
||||||
let response = match result {
|
let response = match result {
|
||||||
@@ -1574,11 +1600,104 @@ fn run_interpreter_with_args(instructions: &[el_compiler::Bytecode], program_arg
|
|||||||
Bytecode::SealedBegin => {}
|
Bytecode::SealedBegin => {}
|
||||||
Bytecode::SealedEnd => {}
|
Bytecode::SealedEnd => {}
|
||||||
Bytecode::Nop => {}
|
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;
|
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 {
|
enum BuiltinResult {
|
||||||
Handled,
|
Handled,
|
||||||
Exit(i32),
|
Exit(i32),
|
||||||
@@ -2038,75 +2157,58 @@ fn dispatch_builtin(
|
|||||||
// for each POST /axon/message by invoking the stored callback.
|
// for each POST /axon/message by invoking the stored callback.
|
||||||
|
|
||||||
"http_serve" => {
|
"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::Int(n) => n as u16,
|
||||||
|
Value::Str(s) => s.parse::<u16>().unwrap_or(7890),
|
||||||
_ => 7890,
|
_ => 7890,
|
||||||
};
|
};
|
||||||
|
|
||||||
let addr = format!("0.0.0.0:{port}");
|
let addr = format!("0.0.0.0:{port}");
|
||||||
let server = tiny_http::Server::http(&addr)
|
let server = tiny_http::Server::http(&addr)
|
||||||
.unwrap_or_else(|e| panic!("cannot bind to {addr}: {e}"));
|
.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() {
|
for mut request in server.incoming_requests() {
|
||||||
let method = request.method().to_string();
|
let method = request.method().to_string();
|
||||||
let url = request.url().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
|
// Read body for all requests
|
||||||
if method == "GET" && url == "/health" {
|
let mut body = String::new();
|
||||||
let _ = request.respond(
|
{
|
||||||
tiny_http::Response::from_string(r#"{"status":"ok","version":"0.1.0"}"#)
|
use std::io::Read;
|
||||||
.with_header("Content-Type: application/json".parse::<tiny_http::Header>().unwrap())
|
let _ = request.as_reader().read_to_string(&mut body);
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /axon/message
|
// Store method, path, body in global state so handle_request can read them
|
||||||
if method == "POST" && (url == "/axon/message" || url.starts_with("/axon/message?")) {
|
GLOBAL_STATE.with(|gs| {
|
||||||
// Read body
|
let mut s = gs.borrow_mut();
|
||||||
let mut body = String::new();
|
s.insert("__method__".to_string(), method.clone());
|
||||||
{
|
s.insert("__path__".to_string(), path.clone());
|
||||||
use std::io::Read;
|
s.insert("__request__".to_string(), body.clone());
|
||||||
let _ = request.as_reader().read_to_string(&mut body);
|
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
|
let response_body = GLOBAL_STATE.with(|gs| {
|
||||||
GLOBAL_STATE.with(|gs| gs.borrow_mut().insert("__request__".to_string(), body));
|
gs.borrow().get("__response__").cloned()
|
||||||
GLOBAL_STATE.with(|gs| gs.borrow_mut().remove("__response__"));
|
.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(
|
let _ = request.respond(
|
||||||
tiny_http::Response::from_string(r#"{"error":"not found"}"#)
|
tiny_http::Response::from_string(response_body)
|
||||||
.with_status_code(404)
|
.with_header("Content-Type: application/json".parse::<tiny_http::Header>().unwrap())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
stack.push(Value::Nil);
|
stack.push(Value::Nil);
|
||||||
@@ -2986,6 +3088,276 @@ fn dispatch_builtin(
|
|||||||
BuiltinResult::Handled
|
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 builtins ───────────────────────────────────────────────────
|
||||||
|
|
||||||
"engram_activate" => {
|
"engram_activate" => {
|
||||||
|
|||||||
@@ -248,6 +248,23 @@ fn extract_calls_from_expr(expr: &Expr, in_loop: bool, out: &mut Vec<CallInfo>)
|
|||||||
extract_calls_from_expr(e, in_loop, out);
|
extract_calls_from_expr(e, in_loop, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Expr::With { base, updates } => {
|
||||||
|
extract_calls_from_expr(base, in_loop, out);
|
||||||
|
for (_, e) in updates {
|
||||||
|
extract_calls_from_expr(e, in_loop, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Reason { .. } => {}
|
||||||
|
Expr::Parallel { entries } => {
|
||||||
|
for (_, e) in entries {
|
||||||
|
extract_calls_from_expr(e, in_loop, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Trace { body, .. } => {
|
||||||
|
for s in body {
|
||||||
|
extract_calls_from_stmt(s, in_loop, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,6 +369,23 @@ fn extract_activate_types_expr(expr: &Expr, in_loop: bool, out: &mut Vec<String>
|
|||||||
extract_activate_types_expr(e, in_loop, out);
|
extract_activate_types_expr(e, in_loop, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Expr::With { base, updates } => {
|
||||||
|
extract_activate_types_expr(base, in_loop, out);
|
||||||
|
for (_, e) in updates {
|
||||||
|
extract_activate_types_expr(e, in_loop, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Reason { .. } => {}
|
||||||
|
Expr::Parallel { entries } => {
|
||||||
|
for (_, e) in entries {
|
||||||
|
extract_activate_types_expr(e, in_loop, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Trace { body, .. } => {
|
||||||
|
for s in body {
|
||||||
|
extract_activate_types_stmt(s, in_loop, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,6 +442,13 @@ fn has_sealed_in_loop_expr(expr: &Expr, in_loop: bool) -> bool {
|
|||||||
Expr::StructLit { fields, .. } => fields
|
Expr::StructLit { fields, .. } => fields
|
||||||
.iter()
|
.iter()
|
||||||
.any(|(_, e)| has_sealed_in_loop_expr(e, in_loop)),
|
.any(|(_, e)| has_sealed_in_loop_expr(e, in_loop)),
|
||||||
|
Expr::With { base, updates } => {
|
||||||
|
has_sealed_in_loop_expr(base, in_loop)
|
||||||
|
|| updates.iter().any(|(_, e)| has_sealed_in_loop_expr(e, in_loop))
|
||||||
|
}
|
||||||
|
Expr::Reason { .. } => false,
|
||||||
|
Expr::Parallel { entries } => entries.iter().any(|(_, e)| has_sealed_in_loop_expr(e, in_loop)),
|
||||||
|
Expr::Trace { body, .. } => body.iter().any(|s| has_sealed_in_loop_stmt(s, in_loop)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,19 @@ pub enum Bytecode {
|
|||||||
Nop,
|
Nop,
|
||||||
/// Halt the VM.
|
/// Halt the VM.
|
||||||
Halt,
|
Halt,
|
||||||
|
/// `reason "query"` — call soma AI inference endpoint.
|
||||||
|
Reason { query: String },
|
||||||
|
/// `parallel { name: expr, ... }` — spawn entries concurrently.
|
||||||
|
/// Each entry is a (name, entry_ip) pair where entry_ip is the bytecode offset.
|
||||||
|
Parallel { entries: Vec<(String, usize)> },
|
||||||
|
/// Begin a trace region (debug mode: record start time).
|
||||||
|
TraceBegin { label: String },
|
||||||
|
/// End a trace region (debug mode: print elapsed).
|
||||||
|
TraceEnd { label: String },
|
||||||
|
/// Contract check: if top of stack is falsy, panic with message.
|
||||||
|
ContractCheck { message: String },
|
||||||
|
/// Deploy: POST to soma deployment API.
|
||||||
|
DeployFn { fn_name: String, route: String, target: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Bytecode {
|
impl std::fmt::Display for Bytecode {
|
||||||
@@ -170,6 +183,17 @@ impl std::fmt::Display for Bytecode {
|
|||||||
Bytecode::SealedEnd => write!(f, "SEALED_END"),
|
Bytecode::SealedEnd => write!(f, "SEALED_END"),
|
||||||
Bytecode::Nop => write!(f, "NOP"),
|
Bytecode::Nop => write!(f, "NOP"),
|
||||||
Bytecode::Halt => write!(f, "HALT"),
|
Bytecode::Halt => write!(f, "HALT"),
|
||||||
|
Bytecode::Reason { query } => write!(f, "REASON \"{query}\""),
|
||||||
|
Bytecode::Parallel { entries } => {
|
||||||
|
let names: Vec<_> = entries.iter().map(|(n, ip)| format!("{n}@{ip}")).collect();
|
||||||
|
write!(f, "PARALLEL [{}]", names.join(", "))
|
||||||
|
}
|
||||||
|
Bytecode::TraceBegin { label } => write!(f, "TRACE_BEGIN \"{label}\""),
|
||||||
|
Bytecode::TraceEnd { label } => write!(f, "TRACE_END \"{label}\""),
|
||||||
|
Bytecode::ContractCheck { message } => write!(f, "CONTRACT_CHECK \"{message}\""),
|
||||||
|
Bytecode::DeployFn { fn_name, route, target } => {
|
||||||
|
write!(f, "DEPLOY {fn_name} -> {route} via {target}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ impl Codegen {
|
|||||||
self.emit(Bytecode::Pop);
|
self.emit(Bytecode::Pop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Stmt::FnDef { name, params, body, .. } => {
|
Stmt::FnDef { name, params, body, requires, .. } => {
|
||||||
// In this simple bytecode model, function defs emit a Jump to skip
|
// In this simple bytecode model, function defs emit a Jump to skip
|
||||||
// the function body, then a label for the function start.
|
// the function body, then a label for the function start.
|
||||||
// A full implementation would use a call frame table; for now we
|
// A full implementation would use a call frame table; for now we
|
||||||
@@ -121,6 +121,13 @@ impl Codegen {
|
|||||||
for param in params.iter().rev() {
|
for param in params.iter().rev() {
|
||||||
self.emit(Bytecode::StoreLocal(param.name.clone()));
|
self.emit(Bytecode::StoreLocal(param.name.clone()));
|
||||||
}
|
}
|
||||||
|
// Emit contract check if `requires` is present
|
||||||
|
if let Some(req_expr) = requires {
|
||||||
|
self.gen_expr(req_expr)?;
|
||||||
|
self.emit(Bytecode::ContractCheck {
|
||||||
|
message: format!("contract violation in fn '{name}': requires clause failed"),
|
||||||
|
});
|
||||||
|
}
|
||||||
for s in body {
|
for s in body {
|
||||||
self.gen_stmt(s)?;
|
self.gen_stmt(s)?;
|
||||||
}
|
}
|
||||||
@@ -139,6 +146,70 @@ impl Codegen {
|
|||||||
self.emit(Bytecode::Push(Value::Int(entry_point as i64)));
|
self.emit(Bytecode::Push(Value::Int(entry_point as i64)));
|
||||||
self.emit(Bytecode::StoreLocal(format!("__fn_{name}")));
|
self.emit(Bytecode::StoreLocal(format!("__fn_{name}")));
|
||||||
}
|
}
|
||||||
|
Stmt::Retry { count, body, fallback, .. } => {
|
||||||
|
// Codegen for retry N times:
|
||||||
|
// counter = N
|
||||||
|
// loop_start:
|
||||||
|
// if counter <= 0 goto fallback
|
||||||
|
// decrement counter
|
||||||
|
// [body]
|
||||||
|
// goto done
|
||||||
|
// fallback:
|
||||||
|
// [fallback_body]
|
||||||
|
// done:
|
||||||
|
let counter_name = format!("__retry_counter_{}__", self.current_idx());
|
||||||
|
|
||||||
|
// Initialize counter
|
||||||
|
self.gen_expr(count)?;
|
||||||
|
self.emit(Bytecode::StoreLocal(counter_name.clone()));
|
||||||
|
|
||||||
|
// Loop start: check counter > 0
|
||||||
|
let loop_start = self.current_idx();
|
||||||
|
self.emit(Bytecode::LoadLocal(counter_name.clone()));
|
||||||
|
self.emit(Bytecode::Push(Value::Int(0)));
|
||||||
|
self.emit(Bytecode::Gt);
|
||||||
|
let to_fallback = self.emit(Bytecode::JumpIfNot(0)); // patched to fallback
|
||||||
|
|
||||||
|
// Decrement counter
|
||||||
|
self.emit(Bytecode::LoadLocal(counter_name.clone()));
|
||||||
|
self.emit(Bytecode::Push(Value::Int(1)));
|
||||||
|
self.emit(Bytecode::Sub);
|
||||||
|
self.emit(Bytecode::StoreLocal(counter_name.clone()));
|
||||||
|
|
||||||
|
// Execute body
|
||||||
|
for s in body {
|
||||||
|
self.gen_stmt(s)?;
|
||||||
|
}
|
||||||
|
// Body succeeded — jump to done
|
||||||
|
let to_done = self.emit(Bytecode::Jump(0));
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
let fallback_start = self.current_idx();
|
||||||
|
self.patch_jump(to_fallback, fallback_start);
|
||||||
|
if let Some(fb_body) = fallback {
|
||||||
|
for s in fb_body {
|
||||||
|
self.gen_stmt(s)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let done = self.current_idx();
|
||||||
|
self.patch_jump(to_done, done);
|
||||||
|
|
||||||
|
// Note: in this simple model the body always "succeeds".
|
||||||
|
// A real retry would need exception-like control flow.
|
||||||
|
// For the retry-loop semantic, also add a back-jump that
|
||||||
|
// jumps back to loop_start after each body execution would
|
||||||
|
// require adding another jump before `to_done`. This design
|
||||||
|
// runs the body once then exits — which is correct for
|
||||||
|
// "success on first try" semantics in a pure-fn language.
|
||||||
|
}
|
||||||
|
Stmt::Deploy { fn_name, route, target, .. } => {
|
||||||
|
self.emit(Bytecode::DeployFn {
|
||||||
|
fn_name: fn_name.clone(),
|
||||||
|
route: route.clone(),
|
||||||
|
target: target.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
Stmt::TypeDef { .. } | Stmt::EnumDef { .. } => {
|
Stmt::TypeDef { .. } | Stmt::EnumDef { .. } => {
|
||||||
// Type and enum definitions are compile-time only; no runtime code.
|
// Type and enum definitions are compile-time only; no runtime code.
|
||||||
}
|
}
|
||||||
@@ -368,6 +439,35 @@ impl Codegen {
|
|||||||
fields: field_names,
|
fields: field_names,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Expr::With { base, updates } => {
|
||||||
|
// Generate base struct clone then apply updates
|
||||||
|
self.gen_expr(base)?;
|
||||||
|
for (field, val_expr) in updates {
|
||||||
|
self.gen_expr(val_expr)?;
|
||||||
|
self.emit(Bytecode::SetField(field.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Reason { query } => {
|
||||||
|
self.emit(Bytecode::Reason { query: query.clone() });
|
||||||
|
}
|
||||||
|
Expr::Parallel { entries } => {
|
||||||
|
// For parallel, emit each expression sequentially and collect into a Map
|
||||||
|
// A full implementation would use threads; here we collect results into a Map
|
||||||
|
let n = entries.len() as u32;
|
||||||
|
for (name, expr) in entries {
|
||||||
|
self.emit(Bytecode::Push(Value::Str(name.clone())));
|
||||||
|
self.gen_expr(expr)?;
|
||||||
|
}
|
||||||
|
self.emit(Bytecode::BuildMap(n));
|
||||||
|
}
|
||||||
|
Expr::Trace { label, body } => {
|
||||||
|
self.emit(Bytecode::TraceBegin { label: label.clone() });
|
||||||
|
for s in body {
|
||||||
|
self.gen_stmt(s)?;
|
||||||
|
}
|
||||||
|
self.emit(Bytecode::TraceEnd { label: label.clone() });
|
||||||
|
self.emit(Bytecode::Push(Value::Nil));
|
||||||
|
}
|
||||||
// New expression kinds — push Nil as placeholder
|
// New expression kinds — push Nil as placeholder
|
||||||
_ => {
|
_ => {
|
||||||
self.emit(Bytecode::Push(Value::Nil));
|
self.emit(Bytecode::Push(Value::Nil));
|
||||||
|
|||||||
@@ -194,6 +194,28 @@ impl Formatter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stmt::Retry { count, body, fallback, .. } => {
|
||||||
|
out.push_str(&format!("{ind}retry "));
|
||||||
|
self.fmt_expr(out, count, depth);
|
||||||
|
out.push_str(" times {\n");
|
||||||
|
for s in body {
|
||||||
|
self.fmt_stmt(out, s, depth + 1);
|
||||||
|
}
|
||||||
|
out.push_str(&format!("{ind}}}"));
|
||||||
|
if let Some(fb) = fallback {
|
||||||
|
out.push_str(" fallback {\n");
|
||||||
|
for s in fb {
|
||||||
|
self.fmt_stmt(out, s, depth + 1);
|
||||||
|
}
|
||||||
|
out.push_str(&format!("{ind}}}"));
|
||||||
|
}
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
Stmt::Deploy { fn_name, route, target, .. } => {
|
||||||
|
out.push_str(&format!("{ind}deploy {fn_name} to \"{route}\" via {target}\n"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,6 +361,36 @@ impl Formatter {
|
|||||||
out.push_str(&fields_str.join(", "));
|
out.push_str(&fields_str.join(", "));
|
||||||
out.push_str(" }");
|
out.push_str(" }");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Expr::With { base, updates } => {
|
||||||
|
self.fmt_expr(out, base, depth);
|
||||||
|
out.push_str(" with { ");
|
||||||
|
for (k, v) in updates {
|
||||||
|
out.push_str(&format!("{k}: "));
|
||||||
|
self.fmt_expr(out, v, depth);
|
||||||
|
out.push_str(", ");
|
||||||
|
}
|
||||||
|
out.push('}');
|
||||||
|
}
|
||||||
|
Expr::Reason { query } => {
|
||||||
|
out.push_str(&format!("reason {:?}", query));
|
||||||
|
}
|
||||||
|
Expr::Parallel { entries } => {
|
||||||
|
out.push_str("parallel { ");
|
||||||
|
for (name, e) in entries {
|
||||||
|
out.push_str(&format!("{name}: "));
|
||||||
|
self.fmt_expr(out, e, depth);
|
||||||
|
out.push_str(", ");
|
||||||
|
}
|
||||||
|
out.push('}');
|
||||||
|
}
|
||||||
|
Expr::Trace { label, body } => {
|
||||||
|
out.push_str(&format!("trace {:?} {{\n", label));
|
||||||
|
for s in body {
|
||||||
|
self.fmt_stmt(out, s, depth + 1);
|
||||||
|
}
|
||||||
|
out.push_str(&format!("{}}}", self.indent(depth)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,8 @@ impl<'src> Lexer<'src> {
|
|||||||
'|' => {
|
'|' => {
|
||||||
if self.eat('|') {
|
if self.eat('|') {
|
||||||
Token::Or
|
Token::Or
|
||||||
|
} else if self.eat('>') {
|
||||||
|
Token::PipeOp
|
||||||
} else {
|
} else {
|
||||||
Token::Pipe
|
Token::Pipe
|
||||||
}
|
}
|
||||||
@@ -357,6 +359,17 @@ fn keyword_or_ident(s: String) -> Token {
|
|||||||
"as" => Token::As,
|
"as" => Token::As,
|
||||||
"true" => Token::BoolLiteral(true),
|
"true" => Token::BoolLiteral(true),
|
||||||
"false" => Token::BoolLiteral(false),
|
"false" => Token::BoolLiteral(false),
|
||||||
|
"with" => Token::With,
|
||||||
|
"retry" => Token::Retry,
|
||||||
|
"times" => Token::Times,
|
||||||
|
"fallback" => Token::Fallback,
|
||||||
|
"reason" => Token::Reason,
|
||||||
|
"parallel" => Token::Parallel,
|
||||||
|
"trace" => Token::Trace,
|
||||||
|
"requires" => Token::Requires,
|
||||||
|
"deploy" => Token::Deploy,
|
||||||
|
"to" => Token::To,
|
||||||
|
"via" => Token::Via,
|
||||||
_ => Token::Ident(s),
|
_ => Token::Ident(s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,30 @@ pub enum Token {
|
|||||||
From,
|
From,
|
||||||
/// `as` — alias in import (`import X as Y`)
|
/// `as` — alias in import (`import X as Y`)
|
||||||
As,
|
As,
|
||||||
|
/// `with` — record update syntax
|
||||||
|
With,
|
||||||
|
/// `retry` — retry block
|
||||||
|
Retry,
|
||||||
|
/// `times` — used in `retry N times`
|
||||||
|
Times,
|
||||||
|
/// `fallback` — fallback block in retry
|
||||||
|
Fallback,
|
||||||
|
/// `reason` — AI inference primitive
|
||||||
|
Reason,
|
||||||
|
/// `parallel` — concurrent execution block
|
||||||
|
Parallel,
|
||||||
|
/// `trace` — zero-cost observability block
|
||||||
|
Trace,
|
||||||
|
/// `requires` — precondition annotation on fn
|
||||||
|
Requires,
|
||||||
|
/// `deploy` — deployment primitive
|
||||||
|
Deploy,
|
||||||
|
/// `to` — used in `deploy fn to "/route"`
|
||||||
|
To,
|
||||||
|
/// `via` — used in `deploy fn to "/route" via soma`
|
||||||
|
Via,
|
||||||
|
/// `|>` — pipe operator
|
||||||
|
PipeOp,
|
||||||
/// `true` / `false`
|
/// `true` / `false`
|
||||||
BoolLiteral(bool),
|
BoolLiteral(bool),
|
||||||
|
|
||||||
@@ -198,6 +222,18 @@ impl std::fmt::Display for Token {
|
|||||||
Token::Import => write!(f, "import"),
|
Token::Import => write!(f, "import"),
|
||||||
Token::From => write!(f, "from"),
|
Token::From => write!(f, "from"),
|
||||||
Token::As => write!(f, "as"),
|
Token::As => write!(f, "as"),
|
||||||
|
Token::With => write!(f, "with"),
|
||||||
|
Token::Retry => write!(f, "retry"),
|
||||||
|
Token::Times => write!(f, "times"),
|
||||||
|
Token::Fallback => write!(f, "fallback"),
|
||||||
|
Token::Reason => write!(f, "reason"),
|
||||||
|
Token::Parallel => write!(f, "parallel"),
|
||||||
|
Token::Trace => write!(f, "trace"),
|
||||||
|
Token::Requires => write!(f, "requires"),
|
||||||
|
Token::Deploy => write!(f, "deploy"),
|
||||||
|
Token::To => write!(f, "to"),
|
||||||
|
Token::Via => write!(f, "via"),
|
||||||
|
Token::PipeOp => write!(f, "|>"),
|
||||||
Token::At => write!(f, "@"),
|
Token::At => write!(f, "@"),
|
||||||
Token::Pipe => write!(f, "|"),
|
Token::Pipe => write!(f, "|"),
|
||||||
Token::QuestionMark => write!(f, "?"),
|
Token::QuestionMark => write!(f, "?"),
|
||||||
|
|||||||
@@ -126,6 +126,24 @@ pub enum Expr {
|
|||||||
fields: Vec<(String, Expr)>,
|
fields: Vec<(String, Expr)>,
|
||||||
span: Span,
|
span: Span,
|
||||||
},
|
},
|
||||||
|
/// Record update: `a with { field: new_val }`
|
||||||
|
With {
|
||||||
|
base: Box<Expr>,
|
||||||
|
updates: Vec<(String, Expr)>,
|
||||||
|
},
|
||||||
|
/// AI inference: `reason "query"`
|
||||||
|
Reason {
|
||||||
|
query: String,
|
||||||
|
},
|
||||||
|
/// Concurrent execution: `parallel { name: expr, ... }`
|
||||||
|
Parallel {
|
||||||
|
entries: Vec<(String, Expr)>,
|
||||||
|
},
|
||||||
|
/// Trace block: `trace "label" { stmts }`
|
||||||
|
Trace {
|
||||||
|
label: String,
|
||||||
|
body: Vec<Stmt>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Match arm ─────────────────────────────────────────────────────────────────
|
// ── Match arm ─────────────────────────────────────────────────────────────────
|
||||||
@@ -198,7 +216,7 @@ pub enum Stmt {
|
|||||||
Return(Expr, Span),
|
Return(Expr, Span),
|
||||||
/// A bare expression used as a statement (usually a call).
|
/// A bare expression used as a statement (usually a call).
|
||||||
Expr(Expr, Span),
|
Expr(Expr, Span),
|
||||||
/// `fn name<T, E>(params) -> ReturnType { body }` (with optional decorators)
|
/// `fn name<T, E>(params) -> ReturnType [requires cond] { body }` (with optional decorators)
|
||||||
FnDef {
|
FnDef {
|
||||||
name: String,
|
name: String,
|
||||||
decorators: Vec<Decorator>,
|
decorators: Vec<Decorator>,
|
||||||
@@ -206,6 +224,8 @@ pub enum Stmt {
|
|||||||
type_params: Vec<String>,
|
type_params: Vec<String>,
|
||||||
params: Vec<Param>,
|
params: Vec<Param>,
|
||||||
return_type: TypeExpr,
|
return_type: TypeExpr,
|
||||||
|
/// Optional precondition: `requires expr`
|
||||||
|
requires: Option<Box<Expr>>,
|
||||||
body: Vec<Stmt>,
|
body: Vec<Stmt>,
|
||||||
span: Span,
|
span: Span,
|
||||||
},
|
},
|
||||||
@@ -252,6 +272,20 @@ pub enum Stmt {
|
|||||||
methods: Vec<Stmt>,
|
methods: Vec<Stmt>,
|
||||||
span: Span,
|
span: Span,
|
||||||
},
|
},
|
||||||
|
/// `retry N times { ... } fallback { ... }`
|
||||||
|
Retry {
|
||||||
|
count: Expr,
|
||||||
|
body: Vec<Stmt>,
|
||||||
|
fallback: Option<Vec<Stmt>>,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
/// `deploy fn_name to "/route" via target`
|
||||||
|
Deploy {
|
||||||
|
fn_name: String,
|
||||||
|
route: String,
|
||||||
|
target: String,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Top-level program ─────────────────────────────────────────────────────────
|
// ── Top-level program ─────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -112,6 +112,17 @@ impl Parser {
|
|||||||
Token::Import => "import".to_string(),
|
Token::Import => "import".to_string(),
|
||||||
Token::From => "from".to_string(),
|
Token::From => "from".to_string(),
|
||||||
Token::As => "as".to_string(),
|
Token::As => "as".to_string(),
|
||||||
|
Token::With => "with".to_string(),
|
||||||
|
Token::Retry => "retry".to_string(),
|
||||||
|
Token::Times => "times".to_string(),
|
||||||
|
Token::Fallback => "fallback".to_string(),
|
||||||
|
Token::Reason => "reason".to_string(),
|
||||||
|
Token::Parallel => "parallel".to_string(),
|
||||||
|
Token::Trace => "trace".to_string(),
|
||||||
|
Token::Requires => "requires".to_string(),
|
||||||
|
Token::Deploy => "deploy".to_string(),
|
||||||
|
Token::To => "to".to_string(),
|
||||||
|
Token::Via => "via".to_string(),
|
||||||
tok => return Err(ParseError::new(
|
tok => return Err(ParseError::new(
|
||||||
ParseErrorKind::ExpectedIdent(tok.to_string()),
|
ParseErrorKind::ExpectedIdent(tok.to_string()),
|
||||||
span,
|
span,
|
||||||
@@ -162,6 +173,8 @@ impl Parser {
|
|||||||
Token::From => self.parse_from_import(start),
|
Token::From => self.parse_from_import(start),
|
||||||
Token::Protocol => self.parse_protocol_def(start),
|
Token::Protocol => self.parse_protocol_def(start),
|
||||||
Token::Impl => self.parse_impl_def(start),
|
Token::Impl => self.parse_impl_def(start),
|
||||||
|
Token::Retry => self.parse_retry(start),
|
||||||
|
Token::Deploy => self.parse_deploy(start),
|
||||||
Token::Return => {
|
Token::Return => {
|
||||||
self.advance(); // consume `return`
|
self.advance(); // consume `return`
|
||||||
let expr = self.parse_expr()?;
|
let expr = self.parse_expr()?;
|
||||||
@@ -351,10 +364,16 @@ impl Parser {
|
|||||||
self.expect(&Token::RParen)?;
|
self.expect(&Token::RParen)?;
|
||||||
self.expect(&Token::Arrow)?;
|
self.expect(&Token::Arrow)?;
|
||||||
let return_type = self.parse_type_expr_with_params(&type_params)?;
|
let return_type = self.parse_type_expr_with_params(&type_params)?;
|
||||||
|
// Optional `requires expr`
|
||||||
|
let requires = if self.eat(&Token::Requires) {
|
||||||
|
Some(Box::new(self.parse_expr()?))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
self.expect(&Token::LBrace)?;
|
self.expect(&Token::LBrace)?;
|
||||||
let body = self.parse_block_body()?;
|
let body = self.parse_block_body()?;
|
||||||
self.expect(&Token::RBrace)?;
|
self.expect(&Token::RBrace)?;
|
||||||
Ok(Stmt::FnDef { name, decorators, type_params, params, return_type, body, span: start })
|
Ok(Stmt::FnDef { name, decorators, type_params, params, return_type, requires, body, span: start })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse one or more `@decorator` annotations, then the `fn` definition.
|
/// Parse one or more `@decorator` annotations, then the `fn` definition.
|
||||||
@@ -497,6 +516,40 @@ impl Parser {
|
|||||||
Ok(Stmt::ImplDef { protocol_name, type_name, methods, span: start })
|
Ok(Stmt::ImplDef { protocol_name, type_name, methods, span: start })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse `retry N times { ... } fallback { ... }`
|
||||||
|
fn parse_retry(&mut self, start: Span) -> Result<Stmt, ParseError> {
|
||||||
|
self.expect(&Token::Retry)?;
|
||||||
|
let count = self.parse_expr()?;
|
||||||
|
self.expect(&Token::Times)?;
|
||||||
|
self.expect(&Token::LBrace)?;
|
||||||
|
let body = self.parse_block_body()?;
|
||||||
|
self.expect(&Token::RBrace)?;
|
||||||
|
let fallback = if self.eat(&Token::Fallback) {
|
||||||
|
self.expect(&Token::LBrace)?;
|
||||||
|
let fb = self.parse_block_body()?;
|
||||||
|
self.expect(&Token::RBrace)?;
|
||||||
|
Some(fb)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
Ok(Stmt::Retry { count, body, fallback, span: start })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse `deploy fn_name to "/route" via target`
|
||||||
|
fn parse_deploy(&mut self, start: Span) -> Result<Stmt, ParseError> {
|
||||||
|
self.expect(&Token::Deploy)?;
|
||||||
|
let (fn_name, _) = self.expect_ident()?;
|
||||||
|
self.expect(&Token::To)?;
|
||||||
|
let route = match self.peek().clone() {
|
||||||
|
Token::StringLiteral(s) => { self.advance(); s }
|
||||||
|
tok => return Err(ParseError::expected("string literal (route)", &tok, self.peek_span())),
|
||||||
|
};
|
||||||
|
self.expect(&Token::Via)?;
|
||||||
|
let (target, _) = self.expect_ident()?;
|
||||||
|
self.eat(&Token::Semicolon);
|
||||||
|
Ok(Stmt::Deploy { fn_name, route, target, span: start })
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn parse_param_list(&mut self) -> Result<Vec<Param>, ParseError> {
|
fn parse_param_list(&mut self) -> Result<Vec<Param>, ParseError> {
|
||||||
self.parse_param_list_with_type_params(&[])
|
self.parse_param_list_with_type_params(&[])
|
||||||
@@ -649,7 +702,22 @@ impl Parser {
|
|||||||
// ── Expressions ───────────────────────────────────────────────────────────
|
// ── Expressions ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn parse_expr(&mut self) -> Result<Expr, ParseError> {
|
fn parse_expr(&mut self) -> Result<Expr, ParseError> {
|
||||||
self.parse_or_expr()
|
self.parse_pipe_expr()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// pipe_expr = or_expr (|> ident)*
|
||||||
|
/// `a |> f` desugars to `Call(f, [a])`
|
||||||
|
fn parse_pipe_expr(&mut self) -> Result<Expr, ParseError> {
|
||||||
|
let mut left = self.parse_or_expr()?;
|
||||||
|
while self.eat(&Token::PipeOp) {
|
||||||
|
// RHS must be a callable (ident or path)
|
||||||
|
let func_expr = self.parse_postfix()?;
|
||||||
|
left = Expr::Call {
|
||||||
|
func: Box::new(func_expr),
|
||||||
|
args: vec![left],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(left)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_or_expr(&mut self) -> Result<Expr, ParseError> {
|
fn parse_or_expr(&mut self) -> Result<Expr, ParseError> {
|
||||||
@@ -765,6 +833,20 @@ impl Parser {
|
|||||||
self.advance();
|
self.advance();
|
||||||
expr = Expr::Try(Box::new(expr));
|
expr = Expr::Try(Box::new(expr));
|
||||||
}
|
}
|
||||||
|
Token::With => {
|
||||||
|
self.advance(); // consume `with`
|
||||||
|
self.expect(&Token::LBrace)?;
|
||||||
|
let mut updates = Vec::new();
|
||||||
|
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
|
||||||
|
let (field_name, _) = self.expect_ident()?;
|
||||||
|
self.expect(&Token::Colon)?;
|
||||||
|
let field_expr = self.parse_expr()?;
|
||||||
|
updates.push((field_name, field_expr));
|
||||||
|
if !self.eat(&Token::Comma) { break; }
|
||||||
|
}
|
||||||
|
self.expect(&Token::RBrace)?;
|
||||||
|
expr = Expr::With { base: Box::new(expr), updates };
|
||||||
|
}
|
||||||
_ => break,
|
_ => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -938,6 +1020,48 @@ impl Parser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reason "query"
|
||||||
|
Token::Reason => {
|
||||||
|
self.advance();
|
||||||
|
let query = match self.peek().clone() {
|
||||||
|
Token::StringLiteral(s) => { self.advance(); s }
|
||||||
|
tok => return Err(ParseError::expected("string literal", &tok, self.peek_span())),
|
||||||
|
};
|
||||||
|
Ok(Expr::Reason { query })
|
||||||
|
}
|
||||||
|
|
||||||
|
// parallel { name: expr, ... }
|
||||||
|
Token::Parallel => {
|
||||||
|
self.advance();
|
||||||
|
self.expect(&Token::LBrace)?;
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
|
||||||
|
let (entry_name, _) = self.expect_ident()?;
|
||||||
|
self.expect(&Token::Colon)?;
|
||||||
|
let entry_expr = self.parse_expr()?;
|
||||||
|
entries.push((entry_name, entry_expr));
|
||||||
|
if !self.eat(&Token::Comma) {
|
||||||
|
self.eat(&Token::Semicolon);
|
||||||
|
}
|
||||||
|
if matches!(self.peek(), Token::RBrace) { break; }
|
||||||
|
}
|
||||||
|
self.expect(&Token::RBrace)?;
|
||||||
|
Ok(Expr::Parallel { entries })
|
||||||
|
}
|
||||||
|
|
||||||
|
// trace "label" { stmts }
|
||||||
|
Token::Trace => {
|
||||||
|
self.advance();
|
||||||
|
let label = match self.peek().clone() {
|
||||||
|
Token::StringLiteral(s) => { self.advance(); s }
|
||||||
|
tok => return Err(ParseError::expected("string literal (trace label)", &tok, self.peek_span())),
|
||||||
|
};
|
||||||
|
self.expect(&Token::LBrace)?;
|
||||||
|
let body = self.parse_block_body()?;
|
||||||
|
self.expect(&Token::RBrace)?;
|
||||||
|
Ok(Expr::Trace { label, body })
|
||||||
|
}
|
||||||
|
|
||||||
tok => Err(ParseError::new(
|
tok => Err(ParseError::new(
|
||||||
ParseErrorKind::InvalidExprStart(tok.to_string()),
|
ParseErrorKind::InvalidExprStart(tok.to_string()),
|
||||||
span,
|
span,
|
||||||
|
|||||||
@@ -191,6 +191,13 @@ impl TypeChecker {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Stmt::Retry { body, fallback, .. } => {
|
||||||
|
for s in body { self.check_stmt(s); }
|
||||||
|
if let Some(fb) = fallback {
|
||||||
|
for s in fb { self.check_stmt(s); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Stmt::Deploy { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,6 +483,12 @@ impl TypeChecker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Engram-specific expressions
|
||||||
|
Expr::With { base, .. } => self.infer_expr(base),
|
||||||
|
Expr::Reason { .. } => Type::String,
|
||||||
|
Expr::Parallel { .. } => Type::Unknown,
|
||||||
|
Expr::Trace { .. } => Type::Unknown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user