From 46d5650e45fbbb9da79add083691db5869a079a7 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Mon, 27 Apr 2026 19:41:33 -0500 Subject: [PATCH] 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 --- Cargo.lock | 304 ++++++++++++++++++++++ Cargo.toml | 1 + bin/el/Cargo.toml | 2 + bin/el/src/main.rs | 414 +++++++++++++++++++++++++++++- crates/el-compiler/src/codegen.rs | 129 ++++++---- 5 files changed, 793 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0755c2e..bade204 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -233,6 +233,32 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -296,6 +322,8 @@ dependencies = [ "el-seal", "el-test", "el-types", + "reqwest", + "serde_json", "thiserror 2.0.18", "tokio", ] @@ -408,6 +436,15 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "engram-crypto" version = "0.1.0" @@ -450,12 +487,33 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -472,6 +530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -480,6 +539,18 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + [[package]] name = "futures-task" version = "0.3.32" @@ -493,7 +564,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-io", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -558,6 +632,25 @@ dependencies = [ "polyval", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -628,6 +721,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -654,6 +748,22 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -672,9 +782,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -889,6 +1001,12 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "mio" version = "1.2.0" @@ -900,6 +1018,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -918,6 +1053,50 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -930,6 +1109,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "polyval" version = "0.6.2" @@ -1122,15 +1307,22 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", + "encoding_rs", + "futures-channel", "futures-core", + "futures-util", + "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", + "mime", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -1141,6 +1333,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls", "tower", "tower-http", @@ -1232,6 +1425,38 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -1393,6 +1618,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -1498,6 +1744,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -1508,6 +1764,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" @@ -1689,6 +1958,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -1852,6 +2127,35 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index a17cc3a..22c56ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,3 +42,4 @@ blake3 = "1" aes-gcm = "0.10" rand = "0.8" clap = { version = "4", features = ["derive", "env"] } +reqwest = { version = "0.12", features = ["json", "blocking"] } diff --git a/bin/el/Cargo.toml b/bin/el/Cargo.toml index 1928d87..b884002 100644 --- a/bin/el/Cargo.toml +++ b/bin/el/Cargo.toml @@ -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 } diff --git a/bin/el/src/main.rs b/bin/el/src/main.rs index 1d77217..313bf16 100644 --- a/bin/el/src/main.rs +++ b/bin/el/src/main.rs @@ -201,6 +201,15 @@ enum Command { output: Option, }, + /// 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, + }, + /// Seal an existing release artifact. Seal { artifact: PathBuf, @@ -431,6 +440,21 @@ async fn run(cli: Cli) -> Result<(), Box> { // ── 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 = Vec::new(); let mut locals: std::collections::HashMap = std::collections::HashMap::new(); let mut ip = 0usize; + // Build a call table: fn name → bytecode offset. + // We populate this by scanning for __fn_ stores first. + let mut fn_table: std::collections::HashMap = 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 = 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)> = 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!(""))); + } + 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, + 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 = 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::(&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 = Vec::new(); let mut locals: std::collections::HashMap = std::collections::HashMap::new(); let mut ip = 0usize; + let program_args: Vec = 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) => { diff --git a/crates/el-compiler/src/codegen.rs b/crates/el-compiler/src/codegen.rs index 41f58b4..f356088 100644 --- a/crates/el-compiler/src/codegen.rs +++ b/crates/el-compiler/src/codegen.rs @@ -65,6 +65,27 @@ impl Codegen { // ── Statement code generation ───────────────────────────────────────────── + /// Generate a statement in tail position (the last stmt of a block). + /// Expression statements leave their value on the stack instead of popping it. + fn gen_stmt_tail(&mut self, stmt: &Stmt) -> CompileResult<()> { + match stmt { + Stmt::Expr(expr, _) => { + // In tail position, leave the value on the stack. + self.gen_expr(expr)?; + } + Stmt::Return(expr, _) => { + self.gen_expr(expr)?; + self.emit(Bytecode::Return); + } + // All other statement kinds behave the same as non-tail; push Nil as block value. + other => { + self.gen_stmt(other)?; + self.emit(Bytecode::Push(Value::Nil)); + } + } + Ok(()) + } + fn gen_stmt(&mut self, stmt: &Stmt) -> CompileResult<()> { // Record the source span for this statement in the source map if self.emit_source_map { @@ -187,20 +208,18 @@ impl Codegen { self.emit(Bytecode::Call { name: fn_name, arity: args.len() as u32 }); } Expr::Block(stmts) => { - for (i, s) in stmts.iter().enumerate() { - self.gen_stmt(s)?; - // The last expression statement is the block's value - if i == stmts.len() - 1 { - if let Stmt::Expr(_, _) = s { - // Already on stack from gen_stmt (before the Pop) - // We need to not pop it — handled by gen_stmt not - // popping Block results; but we already did Pop. - // Push nil as fallback for empty/void blocks. - } - } - } if stmts.is_empty() { self.emit(Bytecode::Push(Value::Nil)); + } else { + for (i, s) in stmts.iter().enumerate() { + let is_last = i == stmts.len() - 1; + if is_last { + // Last statement: emit as a tail expression (leave value on stack). + self.gen_stmt_tail(s)?; + } else { + self.gen_stmt(s)?; + } + } } } Expr::If { cond, then, else_ } => { @@ -225,48 +244,68 @@ impl Codegen { // compare, branch. A full implementation would use a jump table. let mut end_jumps = Vec::new(); for arm in arms { - self.emit(Bytecode::Dup); - // Push pattern value match &arm.pattern { - el_parser::Pattern::Literal(lit) => { - let v = match lit { - Literal::Int(n) => Value::Int(*n), - Literal::Str(s) => Value::Str(s.clone()), - Literal::Bool(b) => Value::Bool(*b), - Literal::Float(f) => Value::Float(*f), - }; - self.emit(Bytecode::Push(v)); - } - el_parser::Pattern::EnumVariant { variant, payload, .. } => { - // Push the variant name as a string for comparison - self.emit(Bytecode::Push(Value::Str(variant.clone()))); - if let Some(bind) = payload { - // Store the payload in a local (simplified: store subject as payload) - self.emit(Bytecode::StoreLocal(bind.clone())); - } + el_parser::Pattern::Wildcard => { + // Wildcard always matches — pop subject and run body directly. + self.emit(Bytecode::Pop); + self.gen_expr(&arm.body)?; + end_jumps.push(self.emit(Bytecode::Jump(0))); + // No jump_no_match needed — wildcard always matches. + // But we still need to patch the end jumps at the end. + // Nothing else to do; break out of the loop since wildcard + // is a catch-all and subsequent arms are unreachable. + break; } el_parser::Pattern::Binding(name) => { - // Bind and always match — push duplicate and store + // Bind and always match. self.emit(Bytecode::Dup); self.emit(Bytecode::StoreLocal(name.clone())); - // Fall through — will compare to itself (always true) + // Dup'd subject is still on stack; compare to itself. + self.emit(Bytecode::Dup); + self.emit(Bytecode::Eq); + let jump_no_match = self.emit(Bytecode::JumpIfNot(0)); + self.emit(Bytecode::Pop); + self.gen_expr(&arm.body)?; + end_jumps.push(self.emit(Bytecode::Jump(0))); + let next_arm = self.current_idx(); + self.patch_jump(jump_no_match, next_arm); } - el_parser::Pattern::Wildcard => { - // Wildcard — push nil (always "matches") - self.emit(Bytecode::Push(Value::Nil)); + _ => { + self.emit(Bytecode::Dup); + // Push pattern value + match &arm.pattern { + el_parser::Pattern::Literal(lit) => { + let v = match lit { + Literal::Int(n) => Value::Int(*n), + Literal::Str(s) => Value::Str(s.clone()), + Literal::Bool(b) => Value::Bool(*b), + Literal::Float(f) => Value::Float(*f), + }; + self.emit(Bytecode::Push(v)); + } + el_parser::Pattern::EnumVariant { variant, payload, .. } => { + // Push the variant name as a string for comparison + self.emit(Bytecode::Push(Value::Str(variant.clone()))); + if let Some(bind) = payload { + // Store the subject (simplified: payload = subject) + self.emit(Bytecode::StoreLocal(bind.clone())); + } + } + _ => unreachable!("wildcard and binding handled above"), + } + self.emit(Bytecode::Eq); + let jump_no_match = self.emit(Bytecode::JumpIfNot(0)); + // Pop subject from stack + self.emit(Bytecode::Pop); + // Generate arm body + self.gen_expr(&arm.body)?; + end_jumps.push(self.emit(Bytecode::Jump(0))); + let next_arm = self.current_idx(); + self.patch_jump(jump_no_match, next_arm); } } - self.emit(Bytecode::Eq); - let jump_no_match = self.emit(Bytecode::JumpIfNot(0)); - // Pop subject from stack - self.emit(Bytecode::Pop); - // Generate arm body - self.gen_expr(&arm.body)?; - end_jumps.push(self.emit(Bytecode::Jump(0))); - let next_arm = self.current_idx(); - self.patch_jump(jump_no_match, next_arm); } - // Default: pop subject, push nil + // Default fallthrough: pop subject, push nil self.emit(Bytecode::Pop); self.emit(Bytecode::Push(Value::Nil)); let end = self.current_idx();