Add CLI builtins: args, env, http_post, http_get, str ops, json_get, cwd; fix block tail expr and wildcard match codegen; add run-file command
This commit is contained in:
@@ -22,3 +22,5 @@ el-test = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
reqwest = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
+402
-12
@@ -201,6 +201,15 @@ enum Command {
|
||||
output: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// Compile and run a single .el source file (no el.toml required).
|
||||
RunFile {
|
||||
/// Source file (*.el).
|
||||
file: PathBuf,
|
||||
/// Arguments passed to the program (available via the args() builtin).
|
||||
#[arg(trailing_var_arg = true)]
|
||||
args: Vec<String>,
|
||||
},
|
||||
|
||||
/// Seal an existing release artifact.
|
||||
Seal {
|
||||
artifact: PathBuf,
|
||||
@@ -431,6 +440,21 @@ 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 opts = CompilerOptions {
|
||||
target: Target::Debug,
|
||||
source_path: file.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let compiled = Compiler::compile(&source, opts)?;
|
||||
let instructions = el_compiler::Bytecode::deserialize_all(&compiled.artifact)
|
||||
.unwrap_or_default();
|
||||
run_interpreter_with_args(&instructions, &args);
|
||||
}
|
||||
|
||||
Command::BuildFile { file, target, output } => {
|
||||
let source = std::fs::read_to_string(&file)
|
||||
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
|
||||
@@ -747,13 +771,49 @@ fn run_tests_from_source(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Minimal interpreter for demonstration.
|
||||
/// Minimal interpreter for demonstration (no program args).
|
||||
fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
|
||||
run_interpreter_with_args(instructions, &[]);
|
||||
}
|
||||
|
||||
/// 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};
|
||||
let mut stack: Vec<Value> = Vec::new();
|
||||
let mut locals: std::collections::HashMap<String, Value> = std::collections::HashMap::new();
|
||||
let mut ip = 0usize;
|
||||
|
||||
// Build a call table: fn name → bytecode offset.
|
||||
// We populate this by scanning for __fn_<name> stores first.
|
||||
let mut fn_table: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
|
||||
// We need a two-pass approach: scan for function entry points then execute.
|
||||
// The codegen emits: Jump(skip) [body...] Push(Int(entry)) StoreLocal(__fn_name)
|
||||
// We pre-scan to build the table.
|
||||
{
|
||||
let mut scan_ip = 0usize;
|
||||
let mut scan_locals: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
|
||||
while scan_ip < instructions.len() {
|
||||
match &instructions[scan_ip] {
|
||||
Bytecode::Push(Value::Int(n)) => {
|
||||
// could be a function entry point — remember it
|
||||
if scan_ip + 1 < instructions.len() {
|
||||
if let Bytecode::StoreLocal(name) = &instructions[scan_ip + 1] {
|
||||
if let Some(fn_name) = name.strip_prefix("__fn_") {
|
||||
fn_table.insert(fn_name.to_string(), *n as usize);
|
||||
}
|
||||
scan_locals.insert(name.clone(), *n);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
scan_ip += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Call stack for user-defined function calls: (return_ip, saved_locals)
|
||||
let mut call_stack: Vec<(usize, std::collections::HashMap<String, Value>)> = Vec::new();
|
||||
|
||||
while ip < instructions.len() {
|
||||
match &instructions[ip] {
|
||||
Bytecode::Push(v) => stack.push(v.clone()),
|
||||
@@ -798,6 +858,54 @@ fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
|
||||
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::Float(x), Value::Float(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::Float(x), Value::Float(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::Float(x), Value::Float(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::Float(x), Value::Float(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))));
|
||||
@@ -810,11 +918,49 @@ fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
|
||||
let v = locals.get(name).cloned().unwrap_or(Value::Nil);
|
||||
stack.push(v);
|
||||
}
|
||||
Bytecode::Call { name, .. } => {
|
||||
if name == "print" || name == "println" {
|
||||
let v = stack.pop().unwrap_or(Value::Nil);
|
||||
println!("{v}");
|
||||
stack.push(Value::Nil);
|
||||
Bytecode::Call { name, arity } => {
|
||||
let result = dispatch_builtin(name, *arity, &mut stack, program_args);
|
||||
match result {
|
||||
BuiltinResult::Handled => {}
|
||||
BuiltinResult::Exit(code) => std::process::exit(code),
|
||||
BuiltinResult::NotBuiltin => {
|
||||
// Try user-defined function
|
||||
if let Some(&entry) = fn_table.get(name.as_str()) {
|
||||
// Save current locals and return address
|
||||
let saved = locals.clone();
|
||||
call_stack.push((ip + 1, saved));
|
||||
ip = entry;
|
||||
continue;
|
||||
}
|
||||
// Unknown — push Nil
|
||||
stack.push(Value::Nil);
|
||||
}
|
||||
}
|
||||
}
|
||||
Bytecode::GetField(field) => {
|
||||
// Pop the object; for now just push Nil (structs not fully implemented)
|
||||
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);
|
||||
}
|
||||
(Value::Str(s), Value::Int(i)) => {
|
||||
let c = s.chars().nth(i as usize)
|
||||
.map(|c| Value::Str(c.to_string()))
|
||||
.unwrap_or(Value::Nil);
|
||||
stack.push(c);
|
||||
}
|
||||
_ => stack.push(Value::Nil),
|
||||
}
|
||||
}
|
||||
Bytecode::Activate { type_name, query } => {
|
||||
@@ -842,7 +988,16 @@ fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Bytecode::Return => break,
|
||||
Bytecode::Return => {
|
||||
// Return from a user function call
|
||||
if let Some((ret_ip, saved_locals)) = call_stack.pop() {
|
||||
locals = saved_locals;
|
||||
ip = ret_ip;
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Bytecode::Halt => break,
|
||||
Bytecode::SealedBegin => eprintln!("[sealed section begin]"),
|
||||
Bytecode::SealedEnd => eprintln!("[sealed section end]"),
|
||||
@@ -852,12 +1007,247 @@ fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
|
||||
}
|
||||
}
|
||||
|
||||
enum BuiltinResult {
|
||||
Handled,
|
||||
Exit(i32),
|
||||
NotBuiltin,
|
||||
}
|
||||
|
||||
fn dispatch_builtin(
|
||||
name: &str,
|
||||
_arity: u32,
|
||||
stack: &mut Vec<el_compiler::Value>,
|
||||
program_args: &[String],
|
||||
) -> BuiltinResult {
|
||||
use el_compiler::Value;
|
||||
match name {
|
||||
"print" => {
|
||||
let v = stack.pop().unwrap_or(Value::Nil);
|
||||
print!("{v}");
|
||||
stack.push(Value::Nil);
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"println" => {
|
||||
let v = stack.pop().unwrap_or(Value::Nil);
|
||||
println!("{v}");
|
||||
stack.push(Value::Nil);
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"print_err" => {
|
||||
let v = stack.pop().unwrap_or(Value::Nil);
|
||||
eprintln!("{v}");
|
||||
stack.push(Value::Nil);
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"args" => {
|
||||
let list = program_args.iter()
|
||||
.map(|s| Value::Str(s.clone()))
|
||||
.collect();
|
||||
stack.push(Value::List(list));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"cwd" => {
|
||||
let path = std::env::current_dir()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| ".".to_string());
|
||||
stack.push(Value::Str(path));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"env" => {
|
||||
let key = match stack.pop().unwrap_or(Value::Nil) {
|
||||
Value::Str(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
let val = std::env::var(&key)
|
||||
.map(Value::Str)
|
||||
.unwrap_or(Value::Nil);
|
||||
stack.push(val);
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"http_post" => {
|
||||
let body = 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()
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body)
|
||||
.send()
|
||||
.and_then(|r| r.text())
|
||||
.unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}"));
|
||||
stack.push(Value::Str(result));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"http_get" => {
|
||||
let url = match stack.pop().unwrap_or(Value::Nil) {
|
||||
Value::Str(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
let result = reqwest::blocking::get(&url)
|
||||
.and_then(|r| r.text())
|
||||
.unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}"));
|
||||
stack.push(Value::Str(result));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"exit" => {
|
||||
let code = match stack.pop().unwrap_or(Value::Nil) {
|
||||
Value::Int(n) => n as i32,
|
||||
_ => 0,
|
||||
};
|
||||
BuiltinResult::Exit(code)
|
||||
}
|
||||
"str_len" => {
|
||||
let s = match stack.pop().unwrap_or(Value::Nil) {
|
||||
Value::Str(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
stack.push(Value::Int(s.len() as i64));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"str_contains" => {
|
||||
let needle = match stack.pop().unwrap_or(Value::Nil) {
|
||||
Value::Str(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
let haystack = match stack.pop().unwrap_or(Value::Nil) {
|
||||
Value::Str(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
stack.push(Value::Bool(haystack.contains(needle.as_str())));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"str_starts_with" => {
|
||||
let prefix = 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.starts_with(prefix.as_str())));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"str_split" => {
|
||||
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: Vec<Value> = s.split(delim.as_str())
|
||||
.map(|p| Value::Str(p.to_string()))
|
||||
.collect();
|
||||
stack.push(Value::List(parts));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"list_len" => {
|
||||
let list = match stack.pop().unwrap_or(Value::Nil) {
|
||||
Value::List(l) => l,
|
||||
_ => vec![],
|
||||
};
|
||||
stack.push(Value::Int(list.len() as i64));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"list_get" => {
|
||||
let idx = match stack.pop().unwrap_or(Value::Nil) {
|
||||
Value::Int(n) => n,
|
||||
_ => -1,
|
||||
};
|
||||
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
|
||||
}
|
||||
"int_to_str" => {
|
||||
let n = match stack.pop().unwrap_or(Value::Nil) {
|
||||
Value::Int(n) => n,
|
||||
_ => 0,
|
||||
};
|
||||
stack.push(Value::Str(n.to_string()));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"str_eq" => {
|
||||
let b = match stack.pop().unwrap_or(Value::Nil) {
|
||||
Value::Str(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
let a = match stack.pop().unwrap_or(Value::Nil) {
|
||||
Value::Str(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
stack.push(Value::Bool(a == b));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"json_get" => {
|
||||
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,
|
||||
_ => String::new(),
|
||||
};
|
||||
let val = serde_json::from_str::<serde_json::Value>(&json_str)
|
||||
.ok()
|
||||
.and_then(|v| v.get(&key).cloned())
|
||||
.map(|v| match v {
|
||||
serde_json::Value::String(s) => Value::Str(s),
|
||||
serde_json::Value::Number(n) => {
|
||||
if let Some(i) = n.as_i64() {
|
||||
Value::Int(i)
|
||||
} else {
|
||||
Value::Str(n.to_string())
|
||||
}
|
||||
}
|
||||
serde_json::Value::Bool(b) => Value::Bool(b),
|
||||
serde_json::Value::Null => Value::Nil,
|
||||
other => Value::Str(other.to_string()),
|
||||
})
|
||||
.unwrap_or(Value::Nil);
|
||||
stack.push(val);
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
"__build_list__" => {
|
||||
// Already handled inline by codegen for array literals — no-op here
|
||||
// The arity items are on the stack; we collect them into a list.
|
||||
// But arity is already popped. We push Nil as fallback.
|
||||
// In practice, array literals push items then call __build_list__(arity).
|
||||
// We need to pop `arity` items and build a list.
|
||||
// arity was passed but we only have `_arity` here — use it.
|
||||
let mut items = Vec::new();
|
||||
for _ in 0.._arity {
|
||||
items.push(stack.pop().unwrap_or(Value::Nil));
|
||||
}
|
||||
items.reverse();
|
||||
stack.push(Value::List(items));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
_ => BuiltinResult::NotBuiltin,
|
||||
}
|
||||
}
|
||||
|
||||
/// Interpreter with debugger support — emits DebugEvents as it runs.
|
||||
fn run_interpreter_debug(instructions: &[el_compiler::Bytecode], debugger: &mut el_compiler::Debugger) {
|
||||
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 ip = 0usize;
|
||||
let program_args: Vec<String> = std::env::args().skip(2).collect(); // skip 'el' and 'debug'
|
||||
|
||||
while ip < instructions.len() {
|
||||
// Check if we should pause here
|
||||
@@ -899,11 +1289,11 @@ fn run_interpreter_debug(instructions: &[el_compiler::Bytecode], debugger: &mut
|
||||
let v = locals.get(name).cloned().unwrap_or(Value::Nil);
|
||||
stack.push(v);
|
||||
}
|
||||
Bytecode::Call { name, .. } => {
|
||||
if name == "print" || name == "println" {
|
||||
let v = stack.pop().unwrap_or(Value::Nil);
|
||||
println!("{v}");
|
||||
stack.push(Value::Nil);
|
||||
Bytecode::Call { name, arity } => {
|
||||
let result = dispatch_builtin(name, *arity, &mut stack, &program_args);
|
||||
match result {
|
||||
BuiltinResult::Handled | BuiltinResult::NotBuiltin => {}
|
||||
BuiltinResult::Exit(code) => std::process::exit(code),
|
||||
}
|
||||
}
|
||||
Bytecode::Jump(offset) => {
|
||||
|
||||
Reference in New Issue
Block a user