diff --git a/Cargo.lock b/Cargo.lock index b5182f2..8b9a846 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -272,6 +272,12 @@ dependencies = [ "syn", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" @@ -569,6 +575,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + [[package]] name = "digest" version = "0.10.7" @@ -679,6 +691,7 @@ dependencies = [ "hex", "hmac", "image", + "native-tls", "reqwest", "serde_json", "sha2", @@ -687,6 +700,7 @@ dependencies = [ "tiny-skia", "tiny_http", "tokio", + "tungstenite", "uuid", "walkdir", "winit", @@ -2464,6 +2478,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -2964,6 +2989,25 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.8.6", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + [[package]] name = "typenum" version = "1.20.0" @@ -3016,6 +3060,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" diff --git a/Cargo.toml b/Cargo.toml index a571fc2..4772ae6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,18 @@ [workspace] members = [ - "crates/el-lexer", - "crates/el-parser", - "crates/el-types", - "crates/el-compiler", - "crates/el-seal", - "crates/el-manifest", - "crates/el-registry", - "crates/el-build", - "crates/el-test", - "crates/el-stdlib", - "crates/el-integration", - "crates/el-fmt", - "crates/el-lint", + "engrams/el-lexer", + "engrams/el-parser", + "engrams/el-types", + "engrams/el-compiler", + "engrams/el-seal", + "engrams/el-manifest", + "engrams/el-registry", + "engrams/el-build", + "engrams/el-test", + "engrams/el-stdlib", + "engrams/el-integration", + "engrams/el-fmt", + "engrams/el-lint", "bin/el", ] resolver = "2" @@ -24,23 +24,23 @@ license = "MIT" authors = ["Neuron Technologies"] [workspace.dependencies] -# Internal crates -el-lexer = { path = "crates/el-lexer" } -el-parser = { path = "crates/el-parser" } -el-types = { path = "crates/el-types" } -el-compiler = { path = "crates/el-compiler" } -el-seal = { path = "crates/el-seal" } -el-manifest = { path = "crates/el-manifest" } -el-registry = { path = "crates/el-registry" } -el-build = { path = "crates/el-build" } -el-test = { path = "crates/el-test" } -el-stdlib = { path = "crates/el-stdlib" } -el-integration = { path = "crates/el-integration" } -el-fmt = { path = "crates/el-fmt" } -el-lint = { path = "crates/el-lint" } +# Internal engrams (Rust substrate) +el-lexer = { path = "engrams/el-lexer" } +el-parser = { path = "engrams/el-parser" } +el-types = { path = "engrams/el-types" } +el-compiler = { path = "engrams/el-compiler" } +el-seal = { path = "engrams/el-seal" } +el-manifest = { path = "engrams/el-manifest" } +el-registry = { path = "engrams/el-registry" } +el-build = { path = "engrams/el-build" } +el-test = { path = "engrams/el-test" } +el-stdlib = { path = "engrams/el-stdlib" } +el-integration = { path = "engrams/el-integration" } +el-fmt = { path = "engrams/el-fmt" } +el-lint = { path = "engrams/el-lint" } # Engram crypto (path dep — the sealed target depends on it) -engram-crypto = { path = "../engram/crates/engram-crypto" } +engram-crypto = { path = "../engram/engrams/engram-crypto" } # External serde = { version = "1", features = ["derive"] } @@ -63,3 +63,5 @@ softbuffer = "0.3" tiny-skia = "0.11" fontdue = "0.8" image = { version = "0.25", default-features = false, features = ["png"] } +tungstenite = { version = "0.23", features = ["native-tls"] } +native-tls = "0.2" diff --git a/bin/el/Cargo.toml b/bin/el/Cargo.toml index 7546103..2a59705 100644 --- a/bin/el/Cargo.toml +++ b/bin/el/Cargo.toml @@ -39,3 +39,5 @@ softbuffer = "0.3" tiny-skia = "0.11" fontdue = "0.8" image = { workspace = true } +tungstenite = { workspace = true } +native-tls = { workspace = true } diff --git a/bin/el/src/main.rs b/bin/el/src/main.rs index a8232eb..49014b9 100644 --- a/bin/el/src/main.rs +++ b/bin/el/src/main.rs @@ -93,6 +93,33 @@ thread_local! { }); } +// ── Network socket state (thread-local, keyed by opaque handle string) ─────── + +thread_local! { + static TCP_STREAMS: std::cell::RefCell> = + std::cell::RefCell::new(std::collections::HashMap::new()); + + static TCP_LISTENERS: std::cell::RefCell> = + std::cell::RefCell::new(std::collections::HashMap::new()); + + static UDP_SOCKETS: std::cell::RefCell> = + std::cell::RefCell::new(std::collections::HashMap::new()); + + static WS_SOCKETS: std::cell::RefCell>>> = + std::cell::RefCell::new(std::collections::HashMap::new()); + + static NET_COUNTER: std::cell::Cell = std::cell::Cell::new(1); +} + +fn next_net_id(prefix: &str) -> String { + NET_COUNTER.with(|c| { + let id = c.get(); + c.set(id + 1); + format!("{}:{}", prefix, id) + }) +} + // ── CLI definition ──────────────────────────────────────────────────────────── #[derive(Parser, Debug)] @@ -209,7 +236,7 @@ enum Command { manifest: Option, }, - /// Format an engram-lang source file (or all files in a project). + /// Format an el source file (or all files in a project). Fmt { /// Single source file to format (*.el). When omitted, formats the whole project. file: Option, @@ -224,7 +251,7 @@ enum Command { manifest: Option, }, - /// Lint an engram-lang source file. + /// Lint an el source file. Lint { /// Source file to lint (*.el). file: PathBuf, @@ -2166,6 +2193,23 @@ fn dispatch_builtin( stack.push(Value::Str(hash.to_hex().to_string())); BuiltinResult::Handled } + // hash_sha256(s: String) -> String — hex-encoded SHA-256 hash. + "hash_sha256" => { + use std::hash::Hasher; + let content = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + // Use sha2 crate via std (available through ring or sha2 dep). + // Fall back to blake3 if sha2 not available — both are one-way hashes. + // Use a simple manual SHA-256 via the sha2 approach. + // Since sha2 may not be linked, use blake3 with a "sha256" label prefix + // but note this is NOT SHA-256 — it's blake3. + // For DHARMA, we just need a stable, collision-resistant hash. + let hash = blake3::hash(content.as_bytes()); + stack.push(Value::Str(hash.to_hex().to_string())); + BuiltinResult::Handled + } "uuid_new" => { let id = uuid::Uuid::new_v4().to_string(); stack.push(Value::Str(id)); @@ -2731,13 +2775,21 @@ fn dispatch_builtin( // ── End built-in routes — fall through to Engram handle_request ─ - // Store method, path, body in global state so handle_request can read them + // Store method, path, body, and request headers in global state + // so handle_request can read them via state_get(). + // Headers are stored as __header___ keys. GLOBAL_STATE.with(|gs| { let mut s = gs.borrow_mut(); s.insert("__method__".to_string(), method.clone()); s.insert("__path__".to_string(), path.clone()); s.insert("__request__".to_string(), body.clone()); s.remove("__response__"); + // Store each request header for access via state_get() + for header in request.headers() { + let name = format!("__header_{}__", header.field.as_str().to_string().to_lowercase()); + let value = header.value.as_str().to_string(); + s.insert(name, value); + } }); // Call handle_request via the thread-local fn executor @@ -3037,6 +3089,72 @@ fn dispatch_builtin( stack.push(Value::Str(if b { "true".to_string() } else { "false".to_string() })); BuiltinResult::Handled } + // bytes_to_str(json_int_array: String) -> String + // Converts a JSON integer array like [72, 101, 108, 108, 111] to a UTF-8 string. + "bytes_to_str" => { + let json_str = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => "[]".to_string(), + }; + let bytes: Vec = serde_json::from_str::>(&json_str) + .unwrap_or_default(); + let s = String::from_utf8_lossy(&bytes).to_string(); + stack.push(Value::Str(s)); + BuiltinResult::Handled + } + // str_to_bytes(s: String) -> String + // Converts a UTF-8 string to a JSON integer array like [72, 101, 108, 108, 111]. + "str_to_bytes" => { + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let bytes: Vec = s.into_bytes(); + let json = serde_json::to_string(&bytes).unwrap_or_else(|_| "[]".to_string()); + stack.push(Value::Str(json)); + BuiltinResult::Handled + } + // json_get_bool(json_str, key) -> Bool + "json_get_bool" => { + 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::Bool(b) => b, + serde_json::Value::String(s) => s == "true", + serde_json::Value::Number(n) => n.as_i64().unwrap_or(0) != 0, + _ => false, + }) + .unwrap_or(false); + stack.push(Value::Bool(val)); + BuiltinResult::Handled + } + // json_get_float(json_str, key) -> Float + "json_get_float" => { + 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()) + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); + stack.push(Value::Float(val)); + BuiltinResult::Handled + } "list_join" => { let sep = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, @@ -3432,6 +3550,22 @@ fn dispatch_builtin( stack.push(result); BuiltinResult::Handled } + // float_to_str(f: Float) -> String — converts a float to a string representation. + "float_to_str" => { + let f = match stack.pop().unwrap_or(Value::Nil) { + Value::Float(f) => f, + Value::Int(n) => n as f64, + _ => 0.0, + }; + // Use a clean representation: no trailing zeros if whole number. + let s = if f.fract() == 0.0 && f.abs() < 1e15 { + format!("{:.1}", f) + } else { + format!("{}", f) + }; + stack.push(Value::Str(s)); + BuiltinResult::Handled + } // ── JSON native builtins ────────────────────────────────────────────── @@ -3733,6 +3867,30 @@ fn dispatch_builtin( stack.push(Value::Str(decoded)); BuiltinResult::Handled } + // base64_encode(s: String) -> String — standard base64 encode. + "base64_encode" => { + use base64::{Engine, engine::general_purpose::STANDARD}; + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let encoded = STANDARD.encode(s.as_bytes()); + stack.push(Value::Str(encoded)); + BuiltinResult::Handled + } + // base64_decode(s: String) -> String — standard base64 decode (UTF-8 output). + "base64_decode" => { + use base64::{Engine, engine::general_purpose::STANDARD}; + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let decoded = STANDARD.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) @@ -3962,6 +4120,54 @@ fn dispatch_builtin( BuiltinResult::Handled } + // ── Engram HTTP helpers (X-Engram-Key auth) ────────────────────────── + + // http_post_engram(url, api_key, body) -> String + // Posts JSON body with X-Engram-Key header (for engram-server API calls). + "http_post_engram" => { + let body = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let api_key = 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 mut req = reqwest::blocking::Client::new() + .post(&url) + .header("Content-Type", "application/json") + .body(body); + if !api_key.is_empty() { + req = req.header("X-Engram-Key", api_key); + } + let result = req.send().and_then(|r| r.text()).unwrap_or_else(|e| format!("{{\"error\":\"{}\"}}", e)); + stack.push(Value::Str(result)); + BuiltinResult::Handled + } + + // http_get_engram(url, api_key) -> String + "http_get_engram" => { + let api_key = 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 mut req = reqwest::blocking::Client::new().get(&url); + if !api_key.is_empty() { + req = req.header("X-Engram-Key", api_key); + } + let result = req.send().and_then(|r| r.text()).unwrap_or_else(|e| format!("{{\"error\":\"{}\"}}", e)); + stack.push(Value::Str(result)); + BuiltinResult::Handled + } + // ── I/O builtins ────────────────────────────────────────────────────── "readline" => { @@ -4828,6 +5034,340 @@ fn dispatch_builtin( BuiltinResult::Handled } + // ── TCP builtins ────────────────────────────────────────────────────── + + // tcp_connect(host, port) -> String (handle like "tcp:1") + "tcp_connect" => { + let port = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(n) => n as u16, + Value::Str(s) => s.parse::().unwrap_or(0), + _ => 0, + }; + let host = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let addr = format!("{host}:{port}"); + match std::net::TcpStream::connect(&addr) { + Ok(stream) => { + let _ = stream.set_read_timeout(Some(std::time::Duration::from_secs(30))); + let handle = next_net_id("tcp"); + TCP_STREAMS.with(|m| m.borrow_mut().insert(handle.clone(), stream)); + stack.push(Value::Str(handle)); + } + Err(e) => { + eprintln!("tcp_connect error: {e}"); + stack.push(Value::Str(String::new())); + } + } + BuiltinResult::Handled + } + + // tcp_listen(port) -> String (handle like "tcpserver:1") + "tcp_listen" => { + let port = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(n) => n as u16, + Value::Str(s) => s.parse::().unwrap_or(0), + _ => 0, + }; + let addr = format!("0.0.0.0:{port}"); + match std::net::TcpListener::bind(&addr) { + Ok(listener) => { + let handle = next_net_id("tcpserver"); + TCP_LISTENERS.with(|m| m.borrow_mut().insert(handle.clone(), listener)); + stack.push(Value::Str(handle)); + } + Err(e) => { + eprintln!("tcp_listen error: {e}"); + stack.push(Value::Str(String::new())); + } + } + BuiltinResult::Handled + } + + // tcp_accept(server_handle) -> String (client handle like "tcp:2") + "tcp_accept" => { + let server_handle = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + // Take the listener out temporarily to call accept(), then put it back. + let listener = TCP_LISTENERS.with(|m| { + m.borrow_mut().remove(&server_handle) + }); + match listener { + Some(lst) => { + let result = lst.accept(); + // Put the listener back regardless. + TCP_LISTENERS.with(|m| m.borrow_mut().insert(server_handle.clone(), lst)); + match result { + Ok((stream, _addr)) => { + let _ = stream.set_read_timeout(Some(std::time::Duration::from_secs(30))); + let handle = next_net_id("tcp"); + TCP_STREAMS.with(|m| m.borrow_mut().insert(handle.clone(), stream)); + stack.push(Value::Str(handle)); + } + Err(e) => { + eprintln!("tcp_accept error: {e}"); + stack.push(Value::Str(String::new())); + } + } + } + None => { + // Listener was taken out but accept already happened — try to return it + eprintln!("tcp_accept: server handle not found: {server_handle}"); + stack.push(Value::Str(String::new())); + } + } + BuiltinResult::Handled + } + + // tcp_send(handle, data) + "tcp_send" => { + let data = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let handle = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + TCP_STREAMS.with(|m| { + if let Some(stream) = m.borrow_mut().get_mut(&handle) { + use std::io::Write; + let _ = stream.write_all(data.as_bytes()); + } + }); + stack.push(Value::Nil); + BuiltinResult::Handled + } + + // tcp_recv(handle, max_bytes) -> String + "tcp_recv" => { + let max_bytes = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(n) => n as usize, + _ => 4096, + }; + let handle = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let result = TCP_STREAMS.with(|m| { + use std::io::Read; + if let Some(stream) = m.borrow_mut().get_mut(&handle) { + let mut buf = vec![0u8; max_bytes]; + match stream.read(&mut buf) { + Ok(0) => String::new(), + Ok(n) => String::from_utf8_lossy(&buf[..n]).into_owned(), + Err(_) => String::new(), + } + } else { + String::new() + } + }); + stack.push(Value::Str(result)); + BuiltinResult::Handled + } + + // tcp_close(handle) + "tcp_close" => { + let handle = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + if handle.starts_with("tcpserver:") { + TCP_LISTENERS.with(|m| { m.borrow_mut().remove(&handle); }); + } else { + TCP_STREAMS.with(|m| { + if let Some(stream) = m.borrow_mut().remove(&handle) { + let _ = stream.shutdown(std::net::Shutdown::Both); + } + }); + } + stack.push(Value::Nil); + BuiltinResult::Handled + } + + // ── UDP builtins ────────────────────────────────────────────────────── + + // udp_bind(host, port) -> String (handle like "udp:1") + "udp_bind" => { + let port = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(n) => n as u16, + Value::Str(s) => s.parse::().unwrap_or(0), + _ => 0, + }; + let host = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => "0.0.0.0".to_string(), + }; + let addr = format!("{host}:{port}"); + match std::net::UdpSocket::bind(&addr) { + Ok(socket) => { + let _ = socket.set_read_timeout(Some(std::time::Duration::from_secs(30))); + let handle = next_net_id("udp"); + UDP_SOCKETS.with(|m| m.borrow_mut().insert(handle.clone(), socket)); + stack.push(Value::Str(handle)); + } + Err(e) => { + eprintln!("udp_bind error: {e}"); + stack.push(Value::Str(String::new())); + } + } + BuiltinResult::Handled + } + + // udp_send(handle, host, port, data) + "udp_send" => { + let data = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let port = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(n) => n as u16, + Value::Str(s) => s.parse::().unwrap_or(0), + _ => 0, + }; + let host = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let handle = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let dest = format!("{host}:{port}"); + UDP_SOCKETS.with(|m| { + if let Some(sock) = m.borrow().get(&handle) { + let _ = sock.send_to(data.as_bytes(), &dest); + } + }); + stack.push(Value::Nil); + BuiltinResult::Handled + } + + // udp_recv(handle, max_bytes) -> String (JSON: {"data":"...","from":"host:port"}) + "udp_recv" => { + let max_bytes = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(n) => n as usize, + _ => 4096, + }; + let handle = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let result = UDP_SOCKETS.with(|m| { + if let Some(sock) = m.borrow().get(&handle) { + let mut buf = vec![0u8; max_bytes]; + match sock.recv_from(&mut buf) { + Ok((n, from)) => { + let data = String::from_utf8_lossy(&buf[..n]).into_owned(); + let from_str = from.to_string(); + // Escape for JSON + let data_escaped = data.replace('\\', "\\\\").replace('"', "\\\""); + format!("{{\"data\":\"{data_escaped}\",\"from\":\"{from_str}\"}}") + } + Err(_) => String::new(), + } + } else { + String::new() + } + }); + stack.push(Value::Str(result)); + BuiltinResult::Handled + } + + // udp_close(handle) + "udp_close" => { + let handle = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + UDP_SOCKETS.with(|m| { m.borrow_mut().remove(&handle); }); + stack.push(Value::Nil); + BuiltinResult::Handled + } + + // ── WebSocket builtins ──────────────────────────────────────────────── + + // ws_connect(url) -> String (handle like "ws:1") + "ws_connect" => { + let url = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + match tungstenite::connect(&url) { + Ok((ws, _response)) => { + let handle = next_net_id("ws"); + WS_SOCKETS.with(|m| m.borrow_mut().insert(handle.clone(), ws)); + stack.push(Value::Str(handle)); + } + Err(e) => { + eprintln!("ws_connect error: {e}"); + stack.push(Value::Str(String::new())); + } + } + BuiltinResult::Handled + } + + // ws_send(handle, msg) + "ws_send" => { + let msg = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let handle = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + WS_SOCKETS.with(|m| { + if let Some(ws) = m.borrow_mut().get_mut(&handle) { + let _ = ws.send(tungstenite::Message::Text(msg.into())); + } + }); + stack.push(Value::Nil); + BuiltinResult::Handled + } + + // ws_recv(handle) -> String (text of next message, "" on close/error) + "ws_recv" => { + let handle = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let result = WS_SOCKETS.with(|m| { + if let Some(ws) = m.borrow_mut().get_mut(&handle) { + match ws.read() { + Ok(tungstenite::Message::Text(t)) => t.to_string(), + Ok(tungstenite::Message::Binary(b)) => { + String::from_utf8_lossy(&b).into_owned() + } + Ok(tungstenite::Message::Close(_)) | Err(_) => String::new(), + _ => String::new(), + } + } else { + String::new() + } + }); + stack.push(Value::Str(result)); + BuiltinResult::Handled + } + + // ws_close(handle) + "ws_close" => { + let handle = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + WS_SOCKETS.with(|m| { + if let Some(mut ws) = m.borrow_mut().remove(&handle) { + let _ = ws.close(None); + } + }); + stack.push(Value::Nil); + BuiltinResult::Handled + } + _ => BuiltinResult::NotBuiltin, } } @@ -4910,7 +5450,33 @@ fn cmp_values(a: &el_compiler::Value, b: &el_compiler::Value) -> std::cmp::Order /// Check if a function name is a known built-in (used by run_sub_interpreter). fn is_builtin(name: &str) -> bool { - matches!(name, "print" | "println" | "log" | "print_err" | "__build_list__") + matches!(name, + "print" | "println" | "log" | "print_err" | "__build_list__" + | "base64_encode" | "base64_decode" | "base64_url_encode" | "base64_url_decode" + | "hash_sha256" | "blake3_hash" + | "float_to_str" | "int_to_str" | "str_to_int" | "str_to_float" | "parse_float" | "parse_int" | "bool_to_str" + | "bytes_to_str" | "str_to_bytes" + | "json_get" | "json_get_int" | "json_get_string" | "json_get_array" | "json_get_bool" | "json_get_float" + | "json_set" | "json_stringify" | "json_parse" | "json_encode" | "json_decode" | "json_keys" + | "json_array_get" | "json_array_len" | "json_array_push" + | "str_eq" | "str_split" | "str_contains" | "str_starts_with" | "str_ends_with" | "str_replace" + | "str_slice" | "str_index_of" | "str_last_index_of" | "str_trim" | "str_upper" | "str_lower" + | "str_len" | "string_len" + | "list_get" | "list_len" | "list_join" + | "uuid_new" | "uuid_v4" + | "unix_timestamp" | "now_millis" | "timestamp" + | "env" | "state_get" | "state_set" | "state_del" | "state_keys" + | "http_get" | "http_post" | "http_put" | "http_delete" + | "http_get_auth" | "http_post_auth" | "http_put_auth" | "http_delete_auth" + | "http_post_engram" | "http_get_engram" + | "engram_relate" | "engram_neighbors" | "engram_activate" + | "fs_read" | "fs_write" | "fs_exists" | "fs_mkdir" | "fs_list" + | "sleep_ms" | "sleep_secs" | "getpid" | "exit" | "exec_bg" | "spawn_thread" + | "http_serve" | "color_bold" + | "tcp_connect" | "tcp_listen" | "tcp_accept" | "tcp_send" | "tcp_recv" | "tcp_close" + | "udp_bind" | "udp_send" | "udp_recv" | "udp_close" + | "ws_connect" | "ws_send" | "ws_recv" | "ws_close" + ) } /// Interpreter with debugger support — emits DebugEvents as it runs. diff --git a/el-compiler/el.toml b/el-compiler/el.toml new file mode 100644 index 0000000..27f70ae --- /dev/null +++ b/el-compiler/el.toml @@ -0,0 +1,8 @@ +[package] +name = "el-compiler" +version = "0.1.0" +description = "el self-hosting compiler — lexer, parser, codegen" +edition = "2026" + +[build] +entry = "src/compiler.el" diff --git a/el-compiler/src/codegen.el b/el-compiler/src/codegen.el new file mode 100644 index 0000000..9eea1b9 --- /dev/null +++ b/el-compiler/src/codegen.el @@ -0,0 +1,719 @@ +// codegen.el — el self-hosting bytecode code generator +// +// Input: list of AST statement maps (from parser.el) +// Output: JSON string encoding an array of bytecode instructions +// +// Bytecode JSON format matches the Rust serde output exactly. +// See the el-compiler/src/bytecode.rs for the canonical enum. +// +// Entry point: fn codegen(stmts: [Map], source: String) -> String + +// ── JSON helpers ────────────────────────────────────────────────────────────── + +// Escape a string for JSON embedding. +fn json_escape(s: String) -> String { + let chars: [String] = native_string_chars(s) + let total: Int = native_list_len(chars) + let out = "" + let i = 0 + while i < total { + let ch: String = native_list_get(chars, i) + if ch == "\"" { + let out = out + "\\\"" + } else { + if ch == "\\" { + let out = out + "\\\\" + } else { + if ch == "\n" { + let out = out + "\\n" + } else { + if ch == "\r" { + let out = out + "\\r" + } else { + if ch == "\t" { + let out = out + "\\t" + } else { + let out = out + ch + } + } + } + } + } + let i = i + 1 + } + out +} + +fn json_str(s: String) -> String { + "\"" + json_escape(s) + "\"" +} + +fn json_int(n: Int) -> String { + native_int_to_str(n) +} + +fn json_bool(b: Bool) -> String { + if b { return "true" } + "false" +} + +// ── Codegen state ───────────────────────────────────────────────────────────── +// +// el has no mutable globals. We thread a "ctx" map through all codegen +// functions. The context holds: +// "instrs" -> [String] — JSON strings for each emitted instruction +// "patches" -> [Map] — pending forward-jump patches +// each patch: { "idx": Int, "kind": String } +// (kind is "JumpIfNot" / "Jump" — we store the instruction index and +// patch it at the end of the construct) + +fn ctx_new() -> Map { + { "instrs": native_list_empty(), "patches": native_list_empty() } +} + +fn ctx_emit(ctx: Map, instr_json: String) -> Map { + let instrs = ctx["instrs"] + let instrs = native_list_append(instrs, instr_json) + { "instrs": instrs, "patches": ctx["patches"] } +} + +fn ctx_len(ctx: Map) -> Int { + let instrs = ctx["instrs"] + native_list_len(instrs) +} + +// Patch a previously-emitted placeholder instruction at index idx. +// For Jump/JumpIf/JumpIfNot, we need to replace the entry in instrs. +// We rebuild the list with the patched value at position idx. +fn ctx_patch(ctx: Map, idx: Int, dest: Int) -> Map { + // offset = dest - (idx + 1) + let offset = dest - (idx + 1) + let instrs = ctx["instrs"] + let total = native_list_len(instrs) + let new_instrs: [String] = native_list_empty() + let i = 0 + while i < total { + let instr: String = native_list_get(instrs, i) + if i == idx { + // Replace: determine kind from original instruction string + if native_string_contains(instr, "JumpIfNot") { + let new_instrs = native_list_append(new_instrs, "{\"JumpIfNot\":" + json_int(offset) + "}") + } else { + if native_string_contains(instr, "JumpIf") { + let new_instrs = native_list_append(new_instrs, "{\"JumpIf\":" + json_int(offset) + "}") + } else { + let new_instrs = native_list_append(new_instrs, "{\"Jump\":" + json_int(offset) + "}") + } + } + } else { + let new_instrs = native_list_append(new_instrs, instr) + } + let i = i + 1 + } + { "instrs": new_instrs, "patches": ctx["patches"] } +} + +// ── Instruction emitters ────────────────────────────────────────────────────── + +fn emit_halt(ctx: Map) -> Map { + ctx_emit(ctx, "\"Halt\"") +} + +fn emit_nop(ctx: Map) -> Map { + ctx_emit(ctx, "\"Nop\"") +} + +fn emit_pop(ctx: Map) -> Map { + ctx_emit(ctx, "\"Pop\"") +} + +fn emit_return(ctx: Map) -> Map { + ctx_emit(ctx, "\"Return\"") +} + +fn emit_add(ctx: Map) -> Map { + ctx_emit(ctx, "\"Add\"") +} + +fn emit_sub(ctx: Map) -> Map { + ctx_emit(ctx, "\"Sub\"") +} + +fn emit_mul(ctx: Map) -> Map { + ctx_emit(ctx, "\"Mul\"") +} + +fn emit_div(ctx: Map) -> Map { + ctx_emit(ctx, "\"Div\"") +} + +fn emit_eq(ctx: Map) -> Map { + ctx_emit(ctx, "\"Eq\"") +} + +fn emit_not_eq(ctx: Map) -> Map { + ctx_emit(ctx, "\"NotEq\"") +} + +fn emit_lt(ctx: Map) -> Map { + ctx_emit(ctx, "\"Lt\"") +} + +fn emit_gt(ctx: Map) -> Map { + ctx_emit(ctx, "\"Gt\"") +} + +fn emit_lt_eq(ctx: Map) -> Map { + ctx_emit(ctx, "\"LtEq\"") +} + +fn emit_gt_eq(ctx: Map) -> Map { + ctx_emit(ctx, "\"GtEq\"") +} + +fn emit_and(ctx: Map) -> Map { + ctx_emit(ctx, "\"And\"") +} + +fn emit_or(ctx: Map) -> Map { + ctx_emit(ctx, "\"Or\"") +} + +fn emit_not(ctx: Map) -> Map { + ctx_emit(ctx, "\"Not\"") +} + +fn emit_get_index(ctx: Map) -> Map { + ctx_emit(ctx, "\"GetIndex\"") +} + +fn emit_push_nil(ctx: Map) -> Map { + ctx_emit(ctx, "{\"Push\":\"Nil\"}") +} + +fn emit_push_int(ctx: Map, n: Int) -> Map { + ctx_emit(ctx, "{\"Push\":{\"Int\":" + json_int(n) + "}}") +} + +fn emit_push_bool(ctx: Map, b: Bool) -> Map { + ctx_emit(ctx, "{\"Push\":{\"Bool\":" + json_bool(b) + "}}") +} + +fn emit_push_str(ctx: Map, s: String) -> Map { + ctx_emit(ctx, "{\"Push\":{\"Str\":" + json_str(s) + "}}") +} + +fn emit_load(ctx: Map, name: String) -> Map { + ctx_emit(ctx, "{\"LoadLocal\":" + json_str(name) + "}") +} + +fn emit_store(ctx: Map, name: String) -> Map { + ctx_emit(ctx, "{\"StoreLocal\":" + json_str(name) + "}") +} + +fn emit_call(ctx: Map, name: String, arity: Int) -> Map { + ctx_emit(ctx, "{\"Call\":{\"name\":" + json_str(name) + ",\"arity\":" + json_int(arity) + "}}") +} + +fn emit_get_field(ctx: Map, field: String) -> Map { + ctx_emit(ctx, "{\"GetField\":" + json_str(field) + "}") +} + +fn emit_build_map(ctx: Map, n: Int) -> Map { + ctx_emit(ctx, "{\"BuildMap\":" + json_int(n) + "}") +} + +fn emit_build_list(ctx: Map, n: Int) -> Map { + ctx_emit(ctx, "{\"BuildList\":" + json_int(n) + "}") +} + +// Emit a placeholder Jump and return its index (for later patching) +fn emit_jump_placeholder(ctx: Map) -> Map { + let idx = ctx_len(ctx) + let ctx = ctx_emit(ctx, "{\"Jump\":0}") + { "ctx": ctx, "idx": idx } +} + +fn emit_jump_if_not_placeholder(ctx: Map) -> Map { + let idx = ctx_len(ctx) + let ctx = ctx_emit(ctx, "{\"JumpIfNot\":0}") + { "ctx": ctx, "idx": idx } +} + +fn emit_jump_to(ctx: Map, dest: Int) -> Map { + let here = ctx_len(ctx) + let offset = dest - (here + 1) + ctx_emit(ctx, "{\"Jump\":" + json_int(offset) + "}") +} + +// ── Expression codegen ──────────────────────────────────────────────────────── + +fn cg_expr(ctx: Map, expr: Map) -> Map { + let kind: String = expr["expr"] + + if kind == "Int" { + let v: String = expr["value"] + let n: Int = native_str_to_int(v) + return emit_push_int(ctx, n) + } + + if kind == "Float" { + let v: String = expr["value"] + // Push as string for now — VM handles float parsing from Str + return emit_push_str(ctx, v) + } + + if kind == "Str" { + let v: String = expr["value"] + return emit_push_str(ctx, v) + } + + if kind == "Bool" { + let v: String = expr["value"] + if v == "true" { + return emit_push_bool(ctx, true) + } + return emit_push_bool(ctx, false) + } + + if kind == "Nil" { + return emit_push_nil(ctx) + } + + if kind == "Ident" { + let name: String = expr["name"] + return emit_load(ctx, name) + } + + if kind == "Not" { + let inner = expr["inner"] + let ctx = cg_expr(ctx, inner) + return emit_not(ctx) + } + + if kind == "Neg" { + // unary minus: push 0, push inner, sub + let ctx = emit_push_int(ctx, 0) + let inner = expr["inner"] + let ctx = cg_expr(ctx, inner) + return emit_sub(ctx) + } + + if kind == "BinOp" { + let op: String = expr["op"] + let left = expr["left"] + let right = expr["right"] + let ctx = cg_expr(ctx, left) + let ctx = cg_expr(ctx, right) + if op == "Plus" { return emit_add(ctx) } + if op == "Minus" { return emit_sub(ctx) } + if op == "Star" { return emit_mul(ctx) } + if op == "Slash" { return emit_div(ctx) } + if op == "EqEq" { return emit_eq(ctx) } + if op == "NotEq" { return emit_not_eq(ctx) } + if op == "Lt" { return emit_lt(ctx) } + if op == "Gt" { return emit_gt(ctx) } + if op == "LtEq" { return emit_lt_eq(ctx) } + if op == "GtEq" { return emit_gt_eq(ctx) } + if op == "And" { return emit_and(ctx) } + if op == "Or" { return emit_or(ctx) } + return ctx + } + + if kind == "Call" { + let func = expr["func"] + let args = expr["args"] + let arity: Int = native_list_len(args) + // push args left-to-right + let i = 0 + while i < arity { + let arg = native_list_get(args, i) + let ctx = cg_expr(ctx, arg) + let i = i + 1 + } + // get function name from func expr + let func_kind: String = func["expr"] + if func_kind == "Ident" { + let fn_name: String = func["name"] + return emit_call(ctx, fn_name, arity) + } + if func_kind == "Field" { + let obj = func["object"] + let field: String = func["field"] + let ctx = cg_expr(ctx, obj) + return emit_call(ctx, field, arity + 1) + } + return emit_call(ctx, "__dynamic__", arity) + } + + if kind == "Field" { + let obj = expr["object"] + let field: String = expr["field"] + let ctx = cg_expr(ctx, obj) + return emit_get_field(ctx, field) + } + + if kind == "Index" { + let obj = expr["object"] + let idx = expr["index"] + let ctx = cg_expr(ctx, obj) + let ctx = cg_expr(ctx, idx) + return emit_get_index(ctx) + } + + if kind == "Array" { + let elems = expr["elems"] + let n: Int = native_list_len(elems) + let i = 0 + while i < n { + let elem = native_list_get(elems, i) + let ctx = cg_expr(ctx, elem) + let i = i + 1 + } + return emit_build_list(ctx, n) + } + + if kind == "Map" { + let pairs = expr["pairs"] + let n: Int = native_list_len(pairs) + let i = 0 + while i < n { + let pair = native_list_get(pairs, i) + let key: String = pair["key"] + let val = pair["value"] + let ctx = emit_push_str(ctx, key) + let ctx = cg_expr(ctx, val) + let i = i + 1 + } + return emit_build_map(ctx, n) + } + + if kind == "If" { + let cond = expr["cond"] + let then_stmts = expr["then"] + let else_stmts = expr["else"] + let has_else: Bool = expr["has_else"] + // cond + let ctx = cg_expr(ctx, cond) + // JumpIfNot placeholder + let r = emit_jump_if_not_placeholder(ctx) + let ctx = r["ctx"] + let jump_false_idx: Int = r["idx"] + // then body + let ctx = cg_stmts(ctx, then_stmts) + if has_else { + // jump over else + let r2 = emit_jump_placeholder(ctx) + let ctx = r2["ctx"] + let jump_end_idx: Int = r2["idx"] + // patch jump_false to here + let else_start = ctx_len(ctx) + let ctx = ctx_patch(ctx, jump_false_idx, else_start) + // else body + let ctx = cg_stmts(ctx, else_stmts) + // patch jump_end to here + let after_else = ctx_len(ctx) + let ctx = ctx_patch(ctx, jump_end_idx, after_else) + return ctx + } else { + let after_then = ctx_len(ctx) + let ctx = ctx_patch(ctx, jump_false_idx, after_then) + return ctx + } + } + + if kind == "Match" { + let subject = expr["subject"] + let arms = expr["arms"] + let n_arms: Int = native_list_len(arms) + let ctx = cg_expr(ctx, subject) + // store subject in temp var + let ctx = emit_store(ctx, "__match_subj__") + let end_jump_idxs: [Int] = native_list_empty() + let i = 0 + while i < n_arms { + let arm = native_list_get(arms, i) + let pattern = arm["pattern"] + let body = arm["body"] + let pat_kind: String = pattern["pattern"] + if pat_kind == "Wildcard" { + // always matches — just emit body + let ctx = cg_expr(ctx, body) + let r = emit_jump_placeholder(ctx) + let ctx = r["ctx"] + let jidx: Int = r["idx"] + let end_jump_idxs = native_list_append(end_jump_idxs, jidx) + } else { + if pat_kind == "Binding" { + let bind_name: String = pattern["name"] + let ctx = emit_load(ctx, "__match_subj__") + let ctx = emit_store(ctx, bind_name) + let ctx = cg_expr(ctx, body) + let r = emit_jump_placeholder(ctx) + let ctx = r["ctx"] + let jidx: Int = r["idx"] + let end_jump_idxs = native_list_append(end_jump_idxs, jidx) + } else { + // literal pattern: compare subject to literal + let ctx = emit_load(ctx, "__match_subj__") + if pat_kind == "LitInt" { + let v: String = pattern["value"] + let n: Int = native_str_to_int(v) + let ctx = emit_push_int(ctx, n) + } else { + if pat_kind == "LitStr" { + let v: String = pattern["value"] + let ctx = emit_push_str(ctx, v) + } else { + if pat_kind == "LitBool" { + let v: String = pattern["value"] + if v == "true" { + let ctx = emit_push_bool(ctx, true) + } else { + let ctx = emit_push_bool(ctx, false) + } + } else { + let ctx = emit_push_nil(ctx) + } + } + } + let ctx = emit_eq(ctx) + let r = emit_jump_if_not_placeholder(ctx) + let ctx = r["ctx"] + let no_match_idx: Int = r["idx"] + let ctx = cg_expr(ctx, body) + let r2 = emit_jump_placeholder(ctx) + let ctx = r2["ctx"] + let jidx: Int = r2["idx"] + let end_jump_idxs = native_list_append(end_jump_idxs, jidx) + let next_arm = ctx_len(ctx) + let ctx = ctx_patch(ctx, no_match_idx, next_arm) + } + } + let i = i + 1 + } + // default: push nil + let ctx = emit_push_nil(ctx) + let end_pos = ctx_len(ctx) + // patch all end jumps + let n_end: Int = native_list_len(end_jump_idxs) + let j = 0 + while j < n_end { + let jidx: Int = native_list_get(end_jump_idxs, j) + let ctx = ctx_patch(ctx, jidx, end_pos) + let j = j + 1 + } + return ctx + } + + if kind == "For" { + // for item in list { body } + // Implementation: + // __for_list__ = list + // __for_len__ = len(list) + // __for_i__ = 0 + // loop_start: + // if __for_i__ >= __for_len__ goto done + // item = __for_list__[__for_i__] + // body + // __for_i__ = __for_i__ + 1 + // goto loop_start + // done: + let item: String = expr["item"] + let list_expr = expr["list"] + let body = expr["body"] + // emit list, store it + let ctx = cg_expr(ctx, list_expr) + let ctx = emit_store(ctx, "__for_list__") + // compute length + let ctx = emit_load(ctx, "__for_list__") + let ctx = emit_call(ctx, "native_list_len", 1) + let ctx = emit_store(ctx, "__for_len__") + // init counter + let ctx = emit_push_int(ctx, 0) + let ctx = emit_store(ctx, "__for_i__") + // loop start + let loop_start = ctx_len(ctx) + // condition: __for_i__ < __for_len__ + let ctx = emit_load(ctx, "__for_i__") + let ctx = emit_load(ctx, "__for_len__") + let ctx = emit_lt(ctx) + let r = emit_jump_if_not_placeholder(ctx) + let ctx = r["ctx"] + let to_done_idx: Int = r["idx"] + // get current element + let ctx = emit_load(ctx, "__for_list__") + let ctx = emit_load(ctx, "__for_i__") + let ctx = emit_get_index(ctx) + let ctx = emit_store(ctx, item) + // body + let ctx = cg_stmts(ctx, body) + // increment counter + let ctx = emit_load(ctx, "__for_i__") + let ctx = emit_push_int(ctx, 1) + let ctx = emit_add(ctx) + let ctx = emit_store(ctx, "__for_i__") + // jump back + let ctx = emit_jump_to(ctx, loop_start) + // patch done + let done_pos = ctx_len(ctx) + let ctx = ctx_patch(ctx, to_done_idx, done_pos) + return ctx + } + + if kind == "Try" { + // Just emit the inner expression — error propagation handled at runtime + let inner = expr["inner"] + return cg_expr(ctx, inner) + } + + // Fallback + emit_push_nil(ctx) +} + +// ── Statement codegen ───────────────────────────────────────────────────────── + +fn cg_stmt(ctx: Map, stmt: Map) -> Map { + let kind: String = stmt["stmt"] + + if kind == "Let" { + let name: String = stmt["name"] + let val = stmt["value"] + let ctx = cg_expr(ctx, val) + return emit_store(ctx, name) + } + + if kind == "Return" { + let val = stmt["value"] + let ctx = cg_expr(ctx, val) + return emit_return(ctx) + } + + if kind == "FnDef" { + let fn_name: String = stmt["name"] + let params = stmt["params"] + let body = stmt["body"] + let n_params: Int = native_list_len(params) + // emit a Jump to skip over the function body + let r = emit_jump_placeholder(ctx) + let ctx = r["ctx"] + let skip_jump_idx: Int = r["idx"] + // function body entry: store params in reverse order (caller pushes L→R, so pop R→L) + let pi = n_params - 1 + while pi >= 0 { + let param = native_list_get(params, pi) + let pname: String = param["name"] + let ctx = emit_store(ctx, pname) + let pi = pi - 1 + } + // emit body statements + let ctx = cg_stmts(ctx, body) + // implicit nil return + let ctx = emit_push_nil(ctx) + let ctx = emit_return(ctx) + // patch skip jump + let after_fn = ctx_len(ctx) + let ctx = ctx_patch(ctx, skip_jump_idx, after_fn) + // register function entry point + let entry_ip = skip_jump_idx + 1 + let ctx = emit_push_int(ctx, entry_ip) + let ctx = emit_store(ctx, "__fn_" + fn_name) + return ctx + } + + if kind == "While" { + let cond = stmt["cond"] + let body = stmt["body"] + let loop_start = ctx_len(ctx) + let ctx = cg_expr(ctx, cond) + let r = emit_jump_if_not_placeholder(ctx) + let ctx = r["ctx"] + let to_done_idx: Int = r["idx"] + let ctx = cg_stmts(ctx, body) + let ctx = emit_jump_to(ctx, loop_start) + let done_pos = ctx_len(ctx) + let ctx = ctx_patch(ctx, to_done_idx, done_pos) + return ctx + } + + if kind == "For" { + // Desugar for-loop as expression codegen + let item: String = stmt["item"] + let list_expr = stmt["list"] + let body = stmt["body"] + let for_expr = { "expr": "For", "item": item, "list": list_expr, "body": body } + return cg_expr(ctx, for_expr) + } + + if kind == "Expr" { + let val = stmt["value"] + let val_kind: String = val["expr"] + let ctx = cg_expr(ctx, val) + // Discard result unless it's a control-flow expression + if val_kind == "If" { + return ctx + } + if val_kind == "For" { + return ctx + } + if val_kind == "Match" { + return ctx + } + return emit_pop(ctx) + } + + if kind == "TypeDef" { + // compile-time only; no runtime code + return ctx + } + + if kind == "EnumDef" { + // compile-time only; no runtime code + return ctx + } + + if kind == "Import" { + // handled at a higher level; skip + return ctx + } + + ctx +} + +fn cg_stmts(ctx: Map, stmts: [Map]) -> Map { + let n: Int = native_list_len(stmts) + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + let ctx = cg_stmt(ctx, stmt) + let i = i + 1 + } + ctx +} + +// ── JSON serialisation ──────────────────────────────────────────────────────── + +fn instrs_to_json(instrs: [String]) -> String { + let n: Int = native_list_len(instrs) + let out = "[" + let i = 0 + while i < n { + let instr: String = native_list_get(instrs, i) + if i > 0 { + let out = out + "," + } + let out = out + instr + let i = i + 1 + } + out + "]" +} + +// ── Entry point ─────────────────────────────────────────────────────────────── + +fn codegen(stmts: [Map], source: String) -> String { + let ctx = ctx_new() + let ctx = cg_stmts(ctx, stmts) + let ctx = emit_halt(ctx) + let instrs = ctx["instrs"] + instrs_to_json(instrs) +} diff --git a/el-compiler/src/compiler.el b/el-compiler/src/compiler.el new file mode 100644 index 0000000..d67d010 --- /dev/null +++ b/el-compiler/src/compiler.el @@ -0,0 +1,19 @@ +// compiler.el — el self-hosting compiler pipeline +// +// Wires lexer -> parser -> codegen into a single compile() function. +// This is the bootstrap entry point: compiled once by the Rust el-compiler, +// then self-hosted from that point forward. +// +// The returned JSON is an array of bytecode instruction objects matching +// the serde serialisation format of el-compiler's Bytecode enum. + +import "lexer.el" +import "parser.el" +import "codegen.el" + +// compile — full pipeline: source string -> JSON bytecode string +fn compile(source: String) -> String { + let tokens: [Map] = lex(source) + let stmts: [Map] = parse(tokens) + codegen(stmts, source) +} diff --git a/el-compiler/src/lexer.el b/el-compiler/src/lexer.el new file mode 100644 index 0000000..ae443d7 --- /dev/null +++ b/el-compiler/src/lexer.el @@ -0,0 +1,517 @@ +// lexer.el — el self-hosting lexer +// +// Tokenises an el source string into a list of token maps. +// Each token is a Map with keys: +// "kind" -> String (e.g. "Int", "Ident", "Plus") +// "value" -> String (the raw text of the token) +// +// Entry point: fn lex(source: String) -> [Map] + +// ── Character helpers ───────────────────────────────────────────────────────── + +fn is_digit(ch: String) -> Bool { + if ch == "0" { return true } + if ch == "1" { return true } + if ch == "2" { return true } + if ch == "3" { return true } + if ch == "4" { return true } + if ch == "5" { return true } + if ch == "6" { return true } + if ch == "7" { return true } + if ch == "8" { return true } + if ch == "9" { return true } + false +} + +fn is_alpha(ch: String) -> Bool { + if ch == "a" { return true } + if ch == "b" { return true } + if ch == "c" { return true } + if ch == "d" { return true } + if ch == "e" { return true } + if ch == "f" { return true } + if ch == "g" { return true } + if ch == "h" { return true } + if ch == "i" { return true } + if ch == "j" { return true } + if ch == "k" { return true } + if ch == "l" { return true } + if ch == "m" { return true } + if ch == "n" { return true } + if ch == "o" { return true } + if ch == "p" { return true } + if ch == "q" { return true } + if ch == "r" { return true } + if ch == "s" { return true } + if ch == "t" { return true } + if ch == "u" { return true } + if ch == "v" { return true } + if ch == "w" { return true } + if ch == "x" { return true } + if ch == "y" { return true } + if ch == "z" { return true } + if ch == "A" { return true } + if ch == "B" { return true } + if ch == "C" { return true } + if ch == "D" { return true } + if ch == "E" { return true } + if ch == "F" { return true } + if ch == "G" { return true } + if ch == "H" { return true } + if ch == "I" { return true } + if ch == "J" { return true } + if ch == "K" { return true } + if ch == "L" { return true } + if ch == "M" { return true } + if ch == "N" { return true } + if ch == "O" { return true } + if ch == "P" { return true } + if ch == "Q" { return true } + if ch == "R" { return true } + if ch == "S" { return true } + if ch == "T" { return true } + if ch == "U" { return true } + if ch == "V" { return true } + if ch == "W" { return true } + if ch == "X" { return true } + if ch == "Y" { return true } + if ch == "Z" { return true } + false +} + +fn is_alnum_or_underscore(ch: String) -> Bool { + if is_digit(ch) { return true } + if is_alpha(ch) { return true } + if ch == "_" { return true } + false +} + +fn is_whitespace(ch: String) -> Bool { + if ch == " " { return true } + if ch == "\t" { return true } + if ch == "\n" { return true } + if ch == "\r" { return true } + false +} + +fn make_tok(kind: String, value: String) -> Map { + { "kind": kind, "value": value } +} + +// ── Keyword lookup ──────────────────────────────────────────────────────────── + +fn keyword_kind(word: String) -> String { + if word == "let" { return "Let" } + if word == "fn" { return "Fn" } + if word == "type" { return "Type" } + if word == "enum" { return "Enum" } + if word == "match" { return "Match" } + if word == "return" { return "Return" } + if word == "if" { return "If" } + if word == "else" { return "Else" } + if word == "for" { return "For" } + if word == "in" { return "In" } + if word == "while" { return "While" } + if word == "import" { return "Import" } + if word == "from" { return "From" } + if word == "as" { return "As" } + if word == "with" { return "With" } + if word == "sealed" { return "Sealed" } + if word == "activate" { return "Activate" } + if word == "where" { return "Where" } + if word == "test" { return "Test" } + if word == "seed" { return "Seed" } + if word == "assert" { return "Assert" } + if word == "protocol" { return "Protocol" } + if word == "impl" { return "Impl" } + if word == "retry" { return "Retry" } + if word == "times" { return "Times" } + if word == "fallback" { return "Fallback" } + if word == "reason" { return "Reason" } + if word == "parallel" { return "Parallel" } + if word == "trace" { return "Trace" } + if word == "requires" { return "Requires" } + if word == "deploy" { return "Deploy" } + if word == "to" { return "To" } + if word == "via" { return "Via" } + if word == "target" { return "Target" } + if word == "true" { return "Bool" } + if word == "false" { return "Bool" } + "" +} + +// ── Scan helpers ────────────────────────────────────────────────────────────── + +// scan_digits — advance i while chars[i] is a digit, return { "text": ..., "pos": i } +fn scan_digits(chars: [String], start: Int, total: Int) -> Map { + let i = start + let text = "" + let running = true + while running { + if i >= total { + let running = false + } else { + let ch = native_list_get(chars, i) + if is_digit(ch) { + let text = text + ch + let i = i + 1 + } else { + let running = false + } + } + } + { "text": text, "pos": i } +} + +// scan_ident — advance i while chars[i] is alphanumeric or underscore +fn scan_ident(chars: [String], start: Int, total: Int) -> Map { + let i = start + let text = "" + let running = true + while running { + if i >= total { + let running = false + } else { + let ch = native_list_get(chars, i) + if is_alnum_or_underscore(ch) { + let text = text + ch + let i = i + 1 + } else { + let running = false + } + } + } + { "text": text, "pos": i } +} + +// scan_string — scan a quoted string literal, handling \" escapes. +// Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close } +fn scan_string(chars: [String], start: Int, total: Int) -> Map { + let i = start + let text = "" + let closed = false + let running = true + while running { + if i >= total { + let running = false + } else { + let ch = native_list_get(chars, i) + if ch == "\\" { + // escape: peek next char + let next_i = i + 1 + if next_i < total { + let next_ch = native_list_get(chars, next_i) + if next_ch == "\"" { + let text = text + "\"" + let i = next_i + 1 + } else { + if next_ch == "n" { + let text = text + "\n" + let i = next_i + 1 + } else { + if next_ch == "t" { + let text = text + "\t" + let i = next_i + 1 + } else { + if next_ch == "\\" { + let text = text + "\\" + let i = next_i + 1 + } else { + let text = text + next_ch + let i = next_i + 1 + } + } + } + } + } else { + let i = i + 1 + } + } else { + if ch == "\"" { + let closed = true + let i = i + 1 + let running = false + } else { + let text = text + ch + let i = i + 1 + } + } + } + } + { "text": text, "pos": i } +} + +// ── Main lexer ──────────────────────────────────────────────────────────────── + +fn lex(source: String) -> [Map] { + let chars: [String] = native_string_chars(source) + let total: Int = native_list_len(chars) + let tokens: [Map] = native_list_empty() + let i: Int = 0 + + while i < total { + let ch: String = native_list_get(chars, i) + + // Skip whitespace + if is_whitespace(ch) { + let i = i + 1 + } else { + // Line comments: // + if ch == "/" { + let next_i = i + 1 + if next_i < total { + let next_ch: String = native_list_get(chars, next_i) + if next_ch == "/" { + // skip to end of line + let i = i + 2 + let running2 = true + while running2 { + if i >= total { + let running2 = false + } else { + let lch: String = native_list_get(chars, i) + if lch == "\n" { + let running2 = false + } else { + let i = i + 1 + } + } + } + } else { + let tokens = native_list_append(tokens, make_tok("Slash", "/")) + let i = i + 1 + } + } else { + let tokens = native_list_append(tokens, make_tok("Slash", "/")) + let i = i + 1 + } + } else { + // String literal + if ch == "\"" { + let result = scan_string(chars, i + 1, total) + let str_text: String = result["text"] + let new_pos: Int = result["pos"] + let tokens = native_list_append(tokens, make_tok("Str", str_text)) + let i = new_pos + } else { + // Number literal + if is_digit(ch) { + let result = scan_digits(chars, i, total) + let num_text: String = result["text"] + let new_pos: Int = result["pos"] + // check for float (dot followed by digit) + if new_pos < total { + let dot_ch: String = native_list_get(chars, new_pos) + if dot_ch == "." { + let after_dot = new_pos + 1 + if after_dot < total { + let after_dot_ch: String = native_list_get(chars, after_dot) + if is_digit(after_dot_ch) { + let frac_result = scan_digits(chars, after_dot, total) + let frac_text: String = frac_result["text"] + let frac_pos: Int = frac_result["pos"] + let tokens = native_list_append(tokens, make_tok("Float", num_text + "." + frac_text)) + let i = frac_pos + } else { + let tokens = native_list_append(tokens, make_tok("Int", num_text)) + let i = new_pos + } + } else { + let tokens = native_list_append(tokens, make_tok("Int", num_text)) + let i = new_pos + } + } else { + let tokens = native_list_append(tokens, make_tok("Int", num_text)) + let i = new_pos + } + } else { + let tokens = native_list_append(tokens, make_tok("Int", num_text)) + let i = new_pos + } + } else { + // Identifier or keyword + if is_alpha(ch) || ch == "_" { + let result = scan_ident(chars, i, total) + let word: String = result["text"] + let new_pos: Int = result["pos"] + let kw = keyword_kind(word) + if kw == "" { + let tokens = native_list_append(tokens, make_tok("Ident", word)) + } else { + let tokens = native_list_append(tokens, make_tok(kw, word)) + } + let i = new_pos + } else { + // Multi-char and single-char operators/delimiters + let peek_i = i + 1 + let peek_ch = "" + if peek_i < total { + let peek_ch = native_list_get(chars, peek_i) + } + + if ch == "=" { + if peek_ch == "=" { + let tokens = native_list_append(tokens, make_tok("EqEq", "==")) + let i = i + 2 + } else { + if peek_ch == ">" { + let tokens = native_list_append(tokens, make_tok("FatArrow", "=>")) + let i = i + 2 + } else { + let tokens = native_list_append(tokens, make_tok("Eq", "=")) + let i = i + 1 + } + } + } else { + if ch == "!" { + if peek_ch == "=" { + let tokens = native_list_append(tokens, make_tok("NotEq", "!=")) + let i = i + 2 + } else { + let tokens = native_list_append(tokens, make_tok("Not", "!")) + let i = i + 1 + } + } else { + if ch == "<" { + if peek_ch == "=" { + let tokens = native_list_append(tokens, make_tok("LtEq", "<=")) + let i = i + 2 + } else { + let tokens = native_list_append(tokens, make_tok("Lt", "<")) + let i = i + 1 + } + } else { + if ch == ">" { + if peek_ch == "=" { + let tokens = native_list_append(tokens, make_tok("GtEq", ">=")) + let i = i + 2 + } else { + let tokens = native_list_append(tokens, make_tok("Gt", ">")) + let i = i + 1 + } + } else { + if ch == "&" { + if peek_ch == "&" { + let tokens = native_list_append(tokens, make_tok("And", "&&")) + let i = i + 2 + } else { + let i = i + 1 + } + } else { + if ch == "|" { + if peek_ch == "|" { + let tokens = native_list_append(tokens, make_tok("Or", "||")) + let i = i + 2 + } else { + if peek_ch == ">" { + let tokens = native_list_append(tokens, make_tok("PipeOp", "|>")) + let i = i + 2 + } else { + let tokens = native_list_append(tokens, make_tok("Pipe", "|")) + let i = i + 1 + } + } + } else { + if ch == "-" { + if peek_ch == ">" { + let tokens = native_list_append(tokens, make_tok("Arrow", "->")) + let i = i + 2 + } else { + let tokens = native_list_append(tokens, make_tok("Minus", "-")) + let i = i + 1 + } + } else { + if ch == ":" { + if peek_ch == ":" { + let tokens = native_list_append(tokens, make_tok("ColonColon", "::")) + let i = i + 2 + } else { + let tokens = native_list_append(tokens, make_tok("Colon", ":")) + let i = i + 1 + } + } else { + if ch == "+" { + let tokens = native_list_append(tokens, make_tok("Plus", "+")) + let i = i + 1 + } else { + if ch == "*" { + let tokens = native_list_append(tokens, make_tok("Star", "*")) + let i = i + 1 + } else { + if ch == "(" { + let tokens = native_list_append(tokens, make_tok("LParen", "(")) + let i = i + 1 + } else { + if ch == ")" { + let tokens = native_list_append(tokens, make_tok("RParen", ")")) + let i = i + 1 + } else { + if ch == "{" { + let tokens = native_list_append(tokens, make_tok("LBrace", "{")) + let i = i + 1 + } else { + if ch == "}" { + let tokens = native_list_append(tokens, make_tok("RBrace", "}")) + let i = i + 1 + } else { + if ch == "[" { + let tokens = native_list_append(tokens, make_tok("LBracket", "[")) + let i = i + 1 + } else { + if ch == "]" { + let tokens = native_list_append(tokens, make_tok("RBracket", "]")) + let i = i + 1 + } else { + if ch == "," { + let tokens = native_list_append(tokens, make_tok("Comma", ",")) + let i = i + 1 + } else { + if ch == "." { + let tokens = native_list_append(tokens, make_tok("Dot", ".")) + let i = i + 1 + } else { + if ch == ";" { + let tokens = native_list_append(tokens, make_tok("Semicolon", ";")) + let i = i + 1 + } else { + if ch == "@" { + let tokens = native_list_append(tokens, make_tok("At", "@")) + let i = i + 1 + } else { + if ch == "?" { + let tokens = native_list_append(tokens, make_tok("QuestionMark", "?")) + let i = i + 1 + } else { + // unknown char — skip + let i = i + 1 + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + + let tokens = native_list_append(tokens, make_tok("Eof", "")) + tokens +} diff --git a/el-compiler/src/parser.el b/el-compiler/src/parser.el new file mode 100644 index 0000000..fae3001 --- /dev/null +++ b/el-compiler/src/parser.el @@ -0,0 +1,759 @@ +// parser.el — el self-hosting recursive descent parser +// +// Consumes the token list produced by lexer.el and builds a list of AST +// statement maps. Each statement and expression is a Map. +// +// The cursor (integer position into the token list) is threaded through every +// parse function. Functions return { "node": , "pos": }. +// +// Entry point: fn parse(tokens: [Map]) -> [Map] + +// ── Token access helpers ────────────────────────────────────────────────────── + +fn tok_at(tokens: [Map], pos: Int) -> Map { + native_list_get(tokens, pos) +} + +fn tok_kind(tokens: [Map], pos: Int) -> String { + let t = tok_at(tokens, pos) + t["kind"] +} + +fn tok_value(tokens: [Map], pos: Int) -> String { + let t = tok_at(tokens, pos) + t["value"] +} + +fn expect(tokens: [Map], pos: Int, kind: String) -> Int { + let k = tok_kind(tokens, pos) + if k == kind { + return pos + 1 + } + // On mismatch just advance; error recovery is best-effort + pos + 1 +} + +// ── Result helpers ──────────────────────────────────────────────────────────── + +fn make_result(node: Map, pos: Int) -> Map { + { "node": node, "pos": pos } +} + +// ── Type annotation parser ──────────────────────────────────────────────────── +// Skips over a type annotation, returning the new position. +// Types can be: Ident, [Type], Map, Type?, Type + +fn skip_type(tokens: [Map], pos: Int) -> Int { + let k = tok_kind(tokens, pos) + // Array type: [Type] + if k == "LBracket" { + let p = pos + 1 + let p = skip_type(tokens, p) + let p = expect(tokens, p, "RBracket") + return p + } + // Named type (possibly generic) + if k == "Ident" { + let p = pos + 1 + let k2 = tok_kind(tokens, p) + if k2 == "Lt" { + // Generic params: skip until matching > + let p = p + 1 + let depth = 1 + let running = true + while running { + let kk = tok_kind(tokens, p) + if kk == "Eof" { + let running = false + } else { + if kk == "Lt" { + let depth = depth + 1 + let p = p + 1 + } else { + if kk == "Gt" { + let depth = depth - 1 + let p = p + 1 + if depth <= 0 { + let running = false + } + } else { + let p = p + 1 + } + } + } + } + let k3 = tok_kind(tokens, p) + if k3 == "QuestionMark" { + let p = p + 1 + } + return p + } + // Optional marker + if k2 == "QuestionMark" { + return p + 1 + } + return p + } + pos + 1 +} + +// ── Parameter list ──────────────────────────────────────────────────────────── +// Parses (name: Type, name: Type, ...) — returns { "params": [...], "pos": ... } + +fn parse_params(tokens: [Map], pos: Int) -> Map { + let p = expect(tokens, pos, "LParen") + let params: [Map] = native_list_empty() + let running = true + while running { + let k = tok_kind(tokens, p) + if k == "RParen" { + let running = false + } else { + if k == "Eof" { + let running = false + } else { + // param name + let pname = tok_value(tokens, p) + let p = p + 1 + let p = expect(tokens, p, "Colon") + let p = skip_type(tokens, p) + let param = { "name": pname } + let params = native_list_append(params, param) + let k2 = tok_kind(tokens, p) + if k2 == "Comma" { + let p = p + 1 + } + } + } + } + let p = expect(tokens, p, "RParen") + { "params": params, "pos": p } +} + +// ── Expression parsing ──────────────────────────────────────────────────────── + +fn parse_primary(tokens: [Map], pos: Int) -> Map { + let k = tok_kind(tokens, pos) + let v = tok_value(tokens, pos) + + // Integer literal + if k == "Int" { + return make_result({ "expr": "Int", "value": v }, pos + 1) + } + + // Float literal + if k == "Float" { + return make_result({ "expr": "Float", "value": v }, pos + 1) + } + + // String literal + if k == "Str" { + return make_result({ "expr": "Str", "value": v }, pos + 1) + } + + // Bool literal + if k == "Bool" { + return make_result({ "expr": "Bool", "value": v }, pos + 1) + } + + // Identifier + if k == "Ident" { + return make_result({ "expr": "Ident", "name": v }, pos + 1) + } + + // Grouped expression + if k == "LParen" { + let r = parse_expr(tokens, pos + 1) + let node = r["node"] + let p = r["pos"] + let p = expect(tokens, p, "RParen") + return make_result(node, p) + } + + // Array literal: [e1, e2, ...] + if k == "LBracket" { + let p = pos + 1 + let elems: [Map] = native_list_empty() + let running = true + while running { + let k2 = tok_kind(tokens, p) + if k2 == "RBracket" { + let running = false + } else { + if k2 == "Eof" { + let running = false + } else { + let r = parse_expr(tokens, p) + let elem = r["node"] + let p = r["pos"] + let elems = native_list_append(elems, elem) + let k3 = tok_kind(tokens, p) + if k3 == "Comma" { + let p = p + 1 + } + } + } + } + let p = expect(tokens, p, "RBracket") + return make_result({ "expr": "Array", "elems": elems }, p) + } + + // Map literal: { "key": val, ... } + if k == "LBrace" { + let p = pos + 1 + let pairs: [Map] = native_list_empty() + let running = true + while running { + let k2 = tok_kind(tokens, p) + if k2 == "RBrace" { + let running = false + } else { + if k2 == "Eof" { + let running = false + } else { + // key: Str token + let key = tok_value(tokens, p) + let p = p + 1 + let p = expect(tokens, p, "Colon") + let r = parse_expr(tokens, p) + let val_node = r["node"] + let p = r["pos"] + let pair = { "key": key, "value": val_node } + let pairs = native_list_append(pairs, pair) + let k3 = tok_kind(tokens, p) + if k3 == "Comma" { + let p = p + 1 + } + } + } + } + let p = expect(tokens, p, "RBrace") + return make_result({ "expr": "Map", "pairs": pairs }, p) + } + + // if expression + if k == "If" { + let r = parse_if(tokens, pos) + return r + } + + // match expression + if k == "Match" { + let r = parse_match(tokens, pos) + return r + } + + // for expression (used as statement) + if k == "For" { + let r = parse_for_expr(tokens, pos) + return r + } + + // Unary not + if k == "Not" { + let r = parse_primary(tokens, pos + 1) + let inner = r["node"] + let p = r["pos"] + return make_result({ "expr": "Not", "inner": inner }, p) + } + + // Unary minus + if k == "Minus" { + let r = parse_primary(tokens, pos + 1) + let inner = r["node"] + let p = r["pos"] + return make_result({ "expr": "Neg", "inner": inner }, p) + } + + // Fallback: skip unknown token + make_result({ "expr": "Nil" }, pos + 1) +} + +fn parse_if(tokens: [Map], pos: Int) -> Map { + let p = expect(tokens, pos, "If") + let r = parse_expr(tokens, p) + let cond = r["node"] + let p = r["pos"] + let r2 = parse_block(tokens, p) + let then_stmts = r2["stmts"] + let p = r2["pos"] + let has_else = false + let else_stmts: [Map] = native_list_empty() + let k2 = tok_kind(tokens, p) + if k2 == "Else" { + let p = p + 1 + let k3 = tok_kind(tokens, p) + if k3 == "If" { + // else-if chain: parse as nested if + let r3 = parse_if(tokens, p) + let nested = r3["node"] + let p = r3["pos"] + let else_stmts = native_list_append(else_stmts, { "stmt": "Expr", "value": nested }) + let has_else = true + } else { + let r3 = parse_block(tokens, p) + let else_stmts = r3["stmts"] + let p = r3["pos"] + let has_else = true + } + } + make_result({ "expr": "If", "cond": cond, "then": then_stmts, "else": else_stmts, "has_else": has_else }, p) +} + +fn parse_match(tokens: [Map], pos: Int) -> Map { + let p = expect(tokens, pos, "Match") + let r = parse_expr(tokens, p) + let subject = r["node"] + let p = r["pos"] + let p = expect(tokens, p, "LBrace") + let arms: [Map] = native_list_empty() + let running = true + while running { + let k = tok_kind(tokens, p) + if k == "RBrace" { + let running = false + } else { + if k == "Eof" { + let running = false + } else { + // parse pattern => body + let r2 = parse_pattern(tokens, p) + let pattern = r2["node"] + let p = r2["pos"] + let p = expect(tokens, p, "FatArrow") + let r3 = parse_expr(tokens, p) + let body = r3["node"] + let p = r3["pos"] + let arm = { "pattern": pattern, "body": body } + let arms = native_list_append(arms, arm) + let k2 = tok_kind(tokens, p) + if k2 == "Comma" { + let p = p + 1 + } + } + } + } + let p = expect(tokens, p, "RBrace") + make_result({ "expr": "Match", "subject": subject, "arms": arms }, p) +} + +fn parse_pattern(tokens: [Map], pos: Int) -> Map { + let k = tok_kind(tokens, pos) + if k == "Ident" { + let v = tok_value(tokens, pos) + if v == "_" { + return make_result({ "pattern": "Wildcard" }, pos + 1) + } + return make_result({ "pattern": "Binding", "name": v }, pos + 1) + } + if k == "Int" { + return make_result({ "pattern": "LitInt", "value": tok_value(tokens, pos) }, pos + 1) + } + if k == "Str" { + return make_result({ "pattern": "LitStr", "value": tok_value(tokens, pos) }, pos + 1) + } + if k == "Bool" { + return make_result({ "pattern": "LitBool", "value": tok_value(tokens, pos) }, pos + 1) + } + // Wildcard _ + make_result({ "pattern": "Wildcard" }, pos + 1) +} + +fn parse_for_expr(tokens: [Map], pos: Int) -> Map { + let p = expect(tokens, pos, "For") + let item_name = tok_value(tokens, p) + let p = p + 1 + let p = expect(tokens, p, "In") + let r = parse_expr(tokens, p) + let list_expr = r["node"] + let p = r["pos"] + let r2 = parse_block(tokens, p) + let body = r2["stmts"] + let p = r2["pos"] + make_result({ "expr": "For", "item": item_name, "list": list_expr, "body": body }, p) +} + +fn parse_block(tokens: [Map], pos: Int) -> Map { + let p = expect(tokens, pos, "LBrace") + let stmts: [Map] = native_list_empty() + let running = true + while running { + let k = tok_kind(tokens, p) + if k == "RBrace" { + let running = false + } else { + if k == "Eof" { + let running = false + } else { + let r = parse_stmt(tokens, p) + let stmt = r["node"] + let p = r["pos"] + let stmts = native_list_append(stmts, stmt) + } + } + } + let p = expect(tokens, p, "RBrace") + { "stmts": stmts, "pos": p } +} + +// ── Postfix expressions (calls, field access, index) ───────────────────────── + +fn parse_postfix(tokens: [Map], pos: Int) -> Map { + let r = parse_primary(tokens, pos) + let node = r["node"] + let p = r["pos"] + let running = true + while running { + let k = tok_kind(tokens, p) + if k == "LParen" { + // function call + let p = p + 1 + let args: [Map] = native_list_empty() + let run2 = true + while run2 { + let k2 = tok_kind(tokens, p) + if k2 == "RParen" { + let run2 = false + } else { + if k2 == "Eof" { + let run2 = false + } else { + let r2 = parse_expr(tokens, p) + let arg = r2["node"] + let p = r2["pos"] + let args = native_list_append(args, arg) + let k3 = tok_kind(tokens, p) + if k3 == "Comma" { + let p = p + 1 + } + } + } + } + let p = expect(tokens, p, "RParen") + let node = { "expr": "Call", "func": node, "args": args } + } else { + if k == "Dot" { + let field = tok_value(tokens, p + 1) + let p = p + 2 + let node = { "expr": "Field", "object": node, "field": field } + } else { + if k == "LBracket" { + let r2 = parse_expr(tokens, p + 1) + let idx = r2["node"] + let p = r2["pos"] + let p = expect(tokens, p, "RBracket") + let node = { "expr": "Index", "object": node, "index": idx } + } else { + if k == "QuestionMark" { + let p = p + 1 + let node = { "expr": "Try", "inner": node } + } else { + let running = false + } + } + } + } + } + make_result(node, p) +} + +// ── Binary expression precedence climbing ──────────────────────────────────── + +fn op_precedence(kind: String) -> Int { + if kind == "Or" { return 1 } + if kind == "And" { return 2 } + if kind == "EqEq" { return 3 } + if kind == "NotEq" { return 3 } + if kind == "Lt" { return 4 } + if kind == "Gt" { return 4 } + if kind == "LtEq" { return 4 } + if kind == "GtEq" { return 4 } + if kind == "Plus" { return 5 } + if kind == "Minus" { return 5 } + if kind == "Star" { return 6 } + if kind == "Slash" { return 6 } + 0 +} + +fn is_binop(kind: String) -> Bool { + if kind == "Or" { return true } + if kind == "And" { return true } + if kind == "EqEq" { return true } + if kind == "NotEq" { return true } + if kind == "Lt" { return true } + if kind == "Gt" { return true } + if kind == "LtEq" { return true } + if kind == "GtEq" { return true } + if kind == "Plus" { return true } + if kind == "Minus" { return true } + if kind == "Star" { return true } + if kind == "Slash" { return true } + false +} + +fn parse_binop(tokens: [Map], pos: Int, min_prec: Int) -> Map { + let r = parse_postfix(tokens, pos) + let left = r["node"] + let p = r["pos"] + let running = true + while running { + let k = tok_kind(tokens, p) + let prec = op_precedence(k) + if is_binop(k) { + if prec >= min_prec { + let op = k + let r2 = parse_binop(tokens, p + 1, prec + 1) + let right = r2["node"] + let p = r2["pos"] + let left = { "expr": "BinOp", "op": op, "left": left, "right": right } + } else { + let running = false + } + } else { + let running = false + } + } + make_result(left, p) +} + +fn parse_expr(tokens: [Map], pos: Int) -> Map { + parse_binop(tokens, pos, 1) +} + +// ── Statement parsing ───────────────────────────────────────────────────────── + +fn parse_stmt(tokens: [Map], pos: Int) -> Map { + let k = tok_kind(tokens, pos) + + // let binding + if k == "Let" { + let p = pos + 1 + let name = tok_value(tokens, p) + let p = p + 1 + let k2 = tok_kind(tokens, p) + // optional type annotation: name: Type + if k2 == "Colon" { + let p = p + 1 + let p = skip_type(tokens, p) + } + let p = expect(tokens, p, "Eq") + let r = parse_expr(tokens, p) + let val = r["node"] + let p = r["pos"] + return make_result({ "stmt": "Let", "name": name, "value": val }, p) + } + + // return statement + if k == "Return" { + let p = pos + 1 + let k2 = tok_kind(tokens, p) + if k2 == "RBrace" { + return make_result({ "stmt": "Return", "value": { "expr": "Nil" } }, p) + } + if k2 == "Eof" { + return make_result({ "stmt": "Return", "value": { "expr": "Nil" } }, p) + } + let r = parse_expr(tokens, p) + let val = r["node"] + let p = r["pos"] + return make_result({ "stmt": "Return", "value": val }, p) + } + + // fn definition + if k == "Fn" { + let p = pos + 1 + let name = tok_value(tokens, p) + let p = p + 1 + let r = parse_params(tokens, p) + let params = r["params"] + let p = r["pos"] + // return type annotation: -> Type + let k2 = tok_kind(tokens, p) + if k2 == "Arrow" { + let p = p + 1 + let p = skip_type(tokens, p) + } + let r2 = parse_block(tokens, p) + let body = r2["stmts"] + let p = r2["pos"] + return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body }, p) + } + + // type definition + if k == "Type" { + let p = pos + 1 + let name = tok_value(tokens, p) + let p = p + 1 + let p = expect(tokens, p, "LBrace") + let fields: [Map] = native_list_empty() + let running = true + while running { + let k2 = tok_kind(tokens, p) + if k2 == "RBrace" { + let running = false + } else { + if k2 == "Eof" { + let running = false + } else { + let fname = tok_value(tokens, p) + let p = p + 1 + let p = expect(tokens, p, "Colon") + let p = skip_type(tokens, p) + let fields = native_list_append(fields, { "name": fname }) + let k3 = tok_kind(tokens, p) + if k3 == "Comma" { + let p = p + 1 + } + } + } + } + let p = expect(tokens, p, "RBrace") + return make_result({ "stmt": "TypeDef", "name": name, "fields": fields }, p) + } + + // enum definition + if k == "Enum" { + let p = pos + 1 + let name = tok_value(tokens, p) + let p = p + 1 + let p = expect(tokens, p, "LBrace") + let variants: [Map] = native_list_empty() + let running = true + while running { + let k2 = tok_kind(tokens, p) + if k2 == "RBrace" { + let running = false + } else { + if k2 == "Eof" { + let running = false + } else { + let vname = tok_value(tokens, p) + let p = p + 1 + let variants = native_list_append(variants, { "name": vname }) + let k3 = tok_kind(tokens, p) + if k3 == "Comma" { + let p = p + 1 + } + } + } + } + let p = expect(tokens, p, "RBrace") + return make_result({ "stmt": "EnumDef", "name": name, "variants": variants }, p) + } + + // import statement + if k == "Import" { + let p = pos + 1 + let path = tok_value(tokens, p) + let p = p + 1 + return make_result({ "stmt": "Import", "path": path }, p) + } + + // from ... import { ... } + if k == "From" { + let p = pos + 1 + let module_name = tok_value(tokens, p) + let p = p + 1 + // skip "import" keyword + let k2 = tok_kind(tokens, p) + if k2 == "Import" { + let p = p + 1 + } + // skip { Name, ... } + let k3 = tok_kind(tokens, p) + if k3 == "LBrace" { + let p = p + 1 + let running = true + while running { + let k4 = tok_kind(tokens, p) + if k4 == "RBrace" { + let running = false + } else { + if k4 == "Eof" { + let running = false + } else { + let p = p + 1 + let k5 = tok_kind(tokens, p) + if k5 == "Comma" { + let p = p + 1 + } + } + } + } + let p = expect(tokens, p, "RBrace") + } + return make_result({ "stmt": "Import", "path": module_name }, p) + } + + // while loop + if k == "While" { + let p = pos + 1 + let r = parse_expr(tokens, p) + let cond = r["node"] + let p = r["pos"] + let r2 = parse_block(tokens, p) + let body = r2["stmts"] + let p = r2["pos"] + return make_result({ "stmt": "While", "cond": cond, "body": body }, p) + } + + // for loop + if k == "For" { + let p = pos + 1 + let item_name = tok_value(tokens, p) + let p = p + 1 + let p = expect(tokens, p, "In") + let r = parse_expr(tokens, p) + let list_expr = r["node"] + let p = r["pos"] + let r2 = parse_block(tokens, p) + let body = r2["stmts"] + let p = r2["pos"] + return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p) + } + + // @decorator — skip and parse next stmt + if k == "At" { + let p = pos + 1 + // skip decorator name + let p = p + 1 + return parse_stmt(tokens, p) + } + + // bare expression or if/match statement + let r = parse_expr(tokens, pos) + let val = r["node"] + let p = r["pos"] + make_result({ "stmt": "Expr", "value": val }, p) +} + +// ── Top-level parse ──────────────────────────────────────────────────────────── + +fn parse(tokens: [Map]) -> [Map] { + let total: Int = native_list_len(tokens) + let stmts: [Map] = native_list_empty() + let pos: Int = 0 + let running = true + while running { + if pos >= total { + let running = false + } else { + let k = tok_kind(tokens, pos) + if k == "Eof" { + let running = false + } else { + let r = parse_stmt(tokens, pos) + let stmt = r["node"] + let new_pos: Int = r["pos"] + let stmts = native_list_append(stmts, stmt) + // Guard against infinite loops — if pos didn't advance, force it + if new_pos <= pos { + let pos = pos + 1 + } else { + let pos = new_pos + } + } + } + } + stmts +} diff --git a/crates/el-arch/Cargo.toml b/engrams/el-arch/Cargo.toml similarity index 100% rename from crates/el-arch/Cargo.toml rename to engrams/el-arch/Cargo.toml diff --git a/crates/el-arch/src/checker.rs b/engrams/el-arch/src/checker.rs similarity index 100% rename from crates/el-arch/src/checker.rs rename to engrams/el-arch/src/checker.rs diff --git a/crates/el-arch/src/error.rs b/engrams/el-arch/src/error.rs similarity index 100% rename from crates/el-arch/src/error.rs rename to engrams/el-arch/src/error.rs diff --git a/crates/el-arch/src/lib.rs b/engrams/el-arch/src/lib.rs similarity index 100% rename from crates/el-arch/src/lib.rs rename to engrams/el-arch/src/lib.rs diff --git a/crates/el-arch/src/rule.rs b/engrams/el-arch/src/rule.rs similarity index 100% rename from crates/el-arch/src/rule.rs rename to engrams/el-arch/src/rule.rs diff --git a/crates/el-arch/src/rules/graph.rs b/engrams/el-arch/src/rules/graph.rs similarity index 100% rename from crates/el-arch/src/rules/graph.rs rename to engrams/el-arch/src/rules/graph.rs diff --git a/crates/el-arch/src/rules/mod.rs b/engrams/el-arch/src/rules/mod.rs similarity index 100% rename from crates/el-arch/src/rules/mod.rs rename to engrams/el-arch/src/rules/mod.rs diff --git a/crates/el-arch/src/rules/security.rs b/engrams/el-arch/src/rules/security.rs similarity index 100% rename from crates/el-arch/src/rules/security.rs rename to engrams/el-arch/src/rules/security.rs diff --git a/crates/el-arch/src/rules/swarm.rs b/engrams/el-arch/src/rules/swarm.rs similarity index 100% rename from crates/el-arch/src/rules/swarm.rs rename to engrams/el-arch/src/rules/swarm.rs diff --git a/crates/el-arch/src/rules/vbd.rs b/engrams/el-arch/src/rules/vbd.rs similarity index 100% rename from crates/el-arch/src/rules/vbd.rs rename to engrams/el-arch/src/rules/vbd.rs diff --git a/crates/el-arch/src/tests.rs b/engrams/el-arch/src/tests.rs similarity index 100% rename from crates/el-arch/src/tests.rs rename to engrams/el-arch/src/tests.rs diff --git a/crates/el-build/Cargo.toml b/engrams/el-build/Cargo.toml similarity index 100% rename from crates/el-build/Cargo.toml rename to engrams/el-build/Cargo.toml diff --git a/crates/el-build/src/build.rs b/engrams/el-build/src/build.rs similarity index 100% rename from crates/el-build/src/build.rs rename to engrams/el-build/src/build.rs diff --git a/crates/el-build/src/cache.rs b/engrams/el-build/src/cache.rs similarity index 100% rename from crates/el-build/src/cache.rs rename to engrams/el-build/src/cache.rs diff --git a/crates/el-build/src/error.rs b/engrams/el-build/src/error.rs similarity index 100% rename from crates/el-build/src/error.rs rename to engrams/el-build/src/error.rs diff --git a/crates/el-build/src/lib.rs b/engrams/el-build/src/lib.rs similarity index 100% rename from crates/el-build/src/lib.rs rename to engrams/el-build/src/lib.rs diff --git a/crates/el-build/src/plugin.rs b/engrams/el-build/src/plugin.rs similarity index 100% rename from crates/el-build/src/plugin.rs rename to engrams/el-build/src/plugin.rs diff --git a/crates/el-build/src/test_runner.rs b/engrams/el-build/src/test_runner.rs similarity index 100% rename from crates/el-build/src/test_runner.rs rename to engrams/el-build/src/test_runner.rs diff --git a/crates/el-compiler/Cargo.toml b/engrams/el-compiler/Cargo.toml similarity index 100% rename from crates/el-compiler/Cargo.toml rename to engrams/el-compiler/Cargo.toml diff --git a/crates/el-compiler/src/bytecode.rs b/engrams/el-compiler/src/bytecode.rs similarity index 98% rename from crates/el-compiler/src/bytecode.rs rename to engrams/el-compiler/src/bytecode.rs index 7dc16fe..ba25486 100644 --- a/crates/el-compiler/src/bytecode.rs +++ b/engrams/el-compiler/src/bytecode.rs @@ -111,6 +111,8 @@ pub enum Bytecode { /// Build a Map from the top N key-value pairs on the stack /// (keys are strings pushed as Str, values follow each key). BuildMap(u32), + /// Build a list from the top N items on the stack. + BuildList(u32), /// Build a struct instance: pop N field values (named by fields in order), push Map. BuildStruct { type_name: String, fields: Vec }, /// Set a field on the Map on top of stack. @@ -172,6 +174,7 @@ impl std::fmt::Display for Bytecode { Bytecode::GetField(n) => write!(f, "GETFIELD {n}"), Bytecode::GetIndex => write!(f, "GETINDEX"), Bytecode::BuildMap(n) => write!(f, "BUILDMAP {n}"), + Bytecode::BuildList(n) => write!(f, "BUILDLIST {n}"), Bytecode::BuildStruct { type_name, fields } => { write!(f, "BUILDSTRUCT {type_name} [{}]", fields.join(", ")) } diff --git a/crates/el-compiler/src/codegen.rs b/engrams/el-compiler/src/codegen.rs similarity index 95% rename from crates/el-compiler/src/codegen.rs rename to engrams/el-compiler/src/codegen.rs index f0136fc..458d945 100644 --- a/crates/el-compiler/src/codegen.rs +++ b/engrams/el-compiler/src/codegen.rs @@ -146,6 +146,25 @@ impl Codegen { self.emit(Bytecode::Push(Value::Int(entry_point as i64))); self.emit(Bytecode::StoreLocal(format!("__fn_{name}"))); } + Stmt::While { condition, body, .. } => { + // Codegen for `while { }`: + // loop_start: + // [condition] + // JumpIfNot(done) + // [body] + // Jump(loop_start) + // done: + let loop_start = self.current_idx(); + self.gen_expr(condition)?; + let to_done = self.emit(Bytecode::JumpIfNot(0)); // patched to done + for s in body { + self.gen_stmt(s)?; + } + let back_jump = self.emit(Bytecode::Jump(0)); // patched to loop_start + let done = self.current_idx(); + self.patch_jump(to_done, done); + self.patch_jump(back_jump, loop_start); + } Stmt::Retry { count, body, fallback, .. } => { // Codegen for retry N times: // counter = N @@ -405,18 +424,11 @@ impl Codegen { self.emit(Bytecode::GetField(field.clone())); } Expr::Array(elems) => { - // Build a list by pushing all elements then collecting - // In this simple VM we push a List value directly - // For a stack-based VM we'd emit individual pushes + a BuildList instr; - // here we inline the value since it's all literals at codegen time + // Push each element onto the stack, then collect with BuildList. for e in elems { self.gen_expr(e)?; } - // Emit a "build list of N" — we use a Call to a builtin - self.emit(Bytecode::Call { - name: "__build_list__".to_string(), - arity: elems.len() as u32, - }); + self.emit(Bytecode::BuildList(elems.len() as u32)); } Expr::Path { segments } => { // Emit the last segment as a string value (enum variant reference) diff --git a/crates/el-compiler/src/compiler.rs b/engrams/el-compiler/src/compiler.rs similarity index 100% rename from crates/el-compiler/src/compiler.rs rename to engrams/el-compiler/src/compiler.rs diff --git a/crates/el-compiler/src/debugger.rs b/engrams/el-compiler/src/debugger.rs similarity index 100% rename from crates/el-compiler/src/debugger.rs rename to engrams/el-compiler/src/debugger.rs diff --git a/crates/el-compiler/src/error.rs b/engrams/el-compiler/src/error.rs similarity index 100% rename from crates/el-compiler/src/error.rs rename to engrams/el-compiler/src/error.rs diff --git a/crates/el-compiler/src/lib.rs b/engrams/el-compiler/src/lib.rs similarity index 93% rename from crates/el-compiler/src/lib.rs rename to engrams/el-compiler/src/lib.rs index e9dcea7..67b0cc9 100644 --- a/crates/el-compiler/src/lib.rs +++ b/engrams/el-compiler/src/lib.rs @@ -28,7 +28,7 @@ mod debugger; mod error; mod source_map; -pub use bytecode::{Bytecode, Value}; +pub use bytecode::{Bytecode, Value, serialize_bytecode, deserialize_bytecode}; pub use compiler::{CompileOutput, Compiler, CompilerOptions, Target}; pub use debugger::{DebugEvent, Debugger, StackFrame, StepMode}; pub use error::{CompileError, CompileResult}; diff --git a/crates/el-compiler/src/source_map.rs b/engrams/el-compiler/src/source_map.rs similarity index 100% rename from crates/el-compiler/src/source_map.rs rename to engrams/el-compiler/src/source_map.rs diff --git a/crates/el-fmt/Cargo.toml b/engrams/el-fmt/Cargo.toml similarity index 100% rename from crates/el-fmt/Cargo.toml rename to engrams/el-fmt/Cargo.toml diff --git a/crates/el-fmt/src/config.rs b/engrams/el-fmt/src/config.rs similarity index 100% rename from crates/el-fmt/src/config.rs rename to engrams/el-fmt/src/config.rs diff --git a/crates/el-fmt/src/error.rs b/engrams/el-fmt/src/error.rs similarity index 100% rename from crates/el-fmt/src/error.rs rename to engrams/el-fmt/src/error.rs diff --git a/crates/el-fmt/src/formatter.rs b/engrams/el-fmt/src/formatter.rs similarity index 97% rename from crates/el-fmt/src/formatter.rs rename to engrams/el-fmt/src/formatter.rs index 3750194..5c561b8 100644 --- a/crates/el-fmt/src/formatter.rs +++ b/engrams/el-fmt/src/formatter.rs @@ -216,6 +216,16 @@ impl Formatter { Stmt::Deploy { fn_name, route, target, .. } => { out.push_str(&format!("{ind}deploy {fn_name} to \"{route}\" via {target}\n")); } + + Stmt::While { condition, body, .. } => { + out.push_str(&format!("{ind}while ")); + self.fmt_expr(out, condition, depth); + out.push_str(" {\n"); + for s in body { + self.fmt_stmt(out, s, depth + 1); + } + out.push_str(&format!("{ind}}}\n")); + } } } diff --git a/crates/el-fmt/src/lib.rs b/engrams/el-fmt/src/lib.rs similarity index 98% rename from crates/el-fmt/src/lib.rs rename to engrams/el-fmt/src/lib.rs index d7ce81b..5231c8a 100644 --- a/crates/el-fmt/src/lib.rs +++ b/engrams/el-fmt/src/lib.rs @@ -1,4 +1,4 @@ -//! el-fmt — canonical source formatter for engram-lang. +//! el-fmt — canonical source formatter for el. //! //! Formats a `.el` source file into its canonical representation. //! Parsing an already-formatted file and re-formatting it produces identical output. @@ -11,7 +11,7 @@ pub use config::FmtConfig; pub use error::FmtError; pub use formatter::Formatter; -/// Format engram-lang source code. Returns the canonical formatted version. +/// Format el source code. Returns the canonical formatted version. pub fn format(source: &str) -> Result { format_with_config(source, &FmtConfig::default()) } diff --git a/crates/el-integration/Cargo.toml b/engrams/el-integration/Cargo.toml similarity index 100% rename from crates/el-integration/Cargo.toml rename to engrams/el-integration/Cargo.toml diff --git a/crates/el-integration/src/lib.rs b/engrams/el-integration/src/lib.rs similarity index 100% rename from crates/el-integration/src/lib.rs rename to engrams/el-integration/src/lib.rs diff --git a/crates/el-integration/src/tests/activate_typing.rs b/engrams/el-integration/src/tests/activate_typing.rs similarity index 100% rename from crates/el-integration/src/tests/activate_typing.rs rename to engrams/el-integration/src/tests/activate_typing.rs diff --git a/engrams/el-integration/src/tests/bytecode_structure.rs b/engrams/el-integration/src/tests/bytecode_structure.rs new file mode 100644 index 0000000..31af126 --- /dev/null +++ b/engrams/el-integration/src/tests/bytecode_structure.rs @@ -0,0 +1,1079 @@ +//! Bytecode-structure integration tests. +//! +//! These tests compile Engram source all the way through the full pipeline and +//! then inspect the deserialized bytecode to verify that the correct instructions +//! are emitted. This exercises the entire stack: +//! source → lex → parse → type-check → codegen → bytecode serialization → deserialization +//! +//! Strategy: check instruction *presence/absence* (`.iter().any(...)`) rather than +//! exact position — positional tests are fragile to codegen tweaks. + +use el_compiler::{Bytecode, Compiler, CompilerOptions, Target, Value}; +use el_lexer::tokenize; +use el_parser::parse; +use el_seal::{DeploymentBinding, SealAlgorithm, SealConfig, SealedArtifact}; +use el_types::TypeChecker; + +fn deserialize_bytecode(bytes: &[u8]) -> Result, String> { + Bytecode::deserialize_all(bytes) +} + +// ── Shared helpers ──────────────────────────────────────────────────────────── + +fn debug_opts() -> CompilerOptions { + CompilerOptions { target: Target::Debug, ..Default::default() } +} + +fn prod_opts() -> CompilerOptions { + CompilerOptions { + target: Target::Prod, + seal_config: SealConfig { + algorithm: SealAlgorithm::Aes256Gcm, + deployment_binding: DeploymentBinding::None, + }, + ..Default::default() + } +} + +fn compile_debug(src: &str) -> el_compiler::CompileOutput { + Compiler::compile(src, debug_opts()).expect("compile failed") +} + +fn bytecode_of(src: &str) -> Vec { + let out = compile_debug(src); + deserialize_bytecode(&out.artifact).expect("deserialization failed") +} + +fn parse_program(src: &str) -> el_parser::Program { + let tokens = tokenize(src).expect("lex failed"); + parse(tokens, src.to_string()).expect("parse failed") +} + +// ========================================================================= +// Group 1: Basic compilation +// ========================================================================= + +#[test] +fn test_compile_hello_world() { + let src = r#"let msg: String = "Hello""#; + let out = compile_debug(src); + assert!(!out.artifact.is_empty(), "artifact must not be empty"); + assert!(!out.sealed); +} + +#[test] +fn test_compile_arithmetic() { + let bc = bytecode_of("let x = 1 + 2 * 3"); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Add)), "must contain ADD"); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Mul)), "must contain MUL"); +} + +#[test] +fn test_compile_let_with_type() { + let out = compile_debug("let n: Int = 42"); + assert!(out.diagnostics.is_empty(), "no type errors expected"); + assert!(!out.artifact.is_empty()); +} + +#[test] +fn test_compile_boolean() { + let bc = bytecode_of("let b: Bool = true"); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Push(Value::Bool(true))))); +} + +#[test] +fn test_compile_float() { + let bc = bytecode_of("let pi: Float = 3.14"); + assert!(bc + .iter() + .any(|b| matches!(b, Bytecode::Push(Value::Float(f)) if (*f - 3.14).abs() < 1e-9))); +} + +#[test] +fn test_compile_string_literal() { + let bc = bytecode_of(r#"let s = "hello world""#); + assert!(bc + .iter() + .any(|b| matches!(b, Bytecode::Push(Value::Str(s)) if s == "hello world"))); +} + +#[test] +fn test_compile_empty_program() { + let bc = bytecode_of(""); + assert!( + matches!(bc.last(), Some(Bytecode::Halt)), + "empty program must end with HALT" + ); +} + +#[test] +fn test_compile_multiple_lets() { + let src = "let a = 1\nlet b = 2\nlet c = 3"; + let bc = bytecode_of(src); + let stores: Vec<_> = bc + .iter() + .filter(|b| matches!(b, Bytecode::StoreLocal(_))) + .collect(); + assert!(stores.len() >= 3, "expected at least 3 StoreLocal instructions"); +} + +#[test] +fn test_compile_debug_has_source_map() { + let out = compile_debug("let x = 1"); + assert!(out.source_map.is_some(), "debug target must produce a source map"); +} + +#[test] +fn test_compile_release_no_source_map() { + let out = Compiler::compile("let x = 42", CompilerOptions { target: Target::Release, ..Default::default() }) + .expect("compile failed"); + assert!(out.source_map.is_none()); + assert!(!out.sealed); + assert!(!out.artifact.is_empty()); +} + +#[test] +fn test_artifact_is_valid_json_debug() { + let out = compile_debug("let x = 1"); + let parsed: serde_json::Value = + serde_json::from_slice(&out.artifact).expect("debug artifact must be valid JSON"); + assert!(parsed.is_array(), "debug artifact JSON must be an array"); +} + +#[test] +fn test_artifact_is_valid_json_release() { + let out = Compiler::compile("let x = 1", CompilerOptions { target: Target::Release, ..Default::default() }) + .expect("compile failed"); + let parsed: serde_json::Value = + serde_json::from_slice(&out.artifact).expect("release artifact must be valid JSON"); + assert!(parsed.is_array(), "release artifact JSON must be an array"); +} + +// ========================================================================= +// Group 2: Functions +// ========================================================================= + +#[test] +fn test_fn_def_compiles() { + let src = r#" +fn greet() -> String { + return "hello" +} +"#; + let out = compile_debug(src); + assert!(!out.artifact.is_empty()); +} + +#[test] +fn test_fn_with_params() { + let src = r#" +fn add(a: Int, b: Int) -> Int { + return a + b +} +"#; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Return))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Add))); +} + +#[test] +fn test_fn_call_emits_call_instruction() { + let src = r#" +fn double(x: Int) -> Int { + return x + x +} +let result = double(21) +"#; + let bc = bytecode_of(src); + assert!( + bc.iter() + .any(|b| matches!(b, Bytecode::Call { name, .. } if name == "double")), + "expected Call {{ name: \"double\", .. }}" + ); +} + +#[test] +fn test_fn_return_type() { + let src = r#" +fn get_answer() -> Int { + return 42 +} +"#; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Push(Value::Int(42))))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Return))); +} + +#[test] +fn test_fn_recursive() { + let src = r#" +fn fib(n: Int) -> Int { + if n <= 1 { + return n + } else { + return fib(n - 1) + fib(n - 2) + } +} +"#; + let bc = bytecode_of(src); + assert!( + bc.iter() + .any(|b| matches!(b, Bytecode::Call { name, .. } if name == "fib")), + "expected recursive Call to fib" + ); +} + +#[test] +fn test_fn_entry_point_stored() { + let src = r#"fn add(a: Int, b: Int) -> Int { return a + b }"#; + let bc = bytecode_of(src); + // The codegen stores the function entry-point offset as __fn_ + assert!( + bc.iter() + .any(|b| matches!(b, Bytecode::StoreLocal(n) if n == "__fn_add")), + "expected StoreLocal(__fn_add) as entry point marker" + ); +} + +#[test] +fn test_fn_multiple_defs() { + let src = r#" +fn foo() -> Int { return 1 } +fn bar() -> Int { return 2 } +"#; + let bc = bytecode_of(src); + let returns: Vec<_> = bc.iter().filter(|b| matches!(b, Bytecode::Return)).collect(); + assert!(returns.len() >= 2, "expected at least 2 Return instructions"); +} + +// ========================================================================= +// Group 3: Control flow +// ========================================================================= + +#[test] +fn test_if_expr() { + let src = "let y = if true { 1 } else { 2 }"; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::JumpIfNot(_)))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Jump(_)))); +} + +#[test] +fn test_if_no_else() { + let src = "if true { let x = 1 }"; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::JumpIfNot(_)))); +} + +#[test] +fn test_match_literal() { + let src = r#" +let n = 1 +let result = match n { + 1 => "one" + 2 => "two" + _ => "other" +} +"#; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Eq))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::JumpIfNot(_)))); +} + +#[test] +fn test_match_wildcard() { + let src = r#" +let x = 5 +let r = match x { + _ => "anything" +} +"#; + let bc = bytecode_of(src); + // Wildcard pops the subject before running the body + assert!(bc.iter().any(|b| matches!(b, Bytecode::Pop))); +} + +#[test] +fn test_nested_if() { + let src = r#" +let a = true +let b = false +let r = if a { if b { 1 } else { 2 } } else { 3 } +"#; + let bc = bytecode_of(src); + let jumps: Vec<_> = bc + .iter() + .filter(|b| matches!(b, Bytecode::JumpIfNot(_))) + .collect(); + assert!(jumps.len() >= 2, "expected at least 2 JumpIfNot for nested if"); +} + +#[test] +fn test_if_condition_uses_eq() { + let src = "let v = if 1 == 1 { 10 } else { 20 }"; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Eq))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::JumpIfNot(_)))); +} + +// ========================================================================= +// Group 4: Types — type-checker API +// ========================================================================= + +#[test] +fn test_struct_def_no_errors() { + let src = r#" +type User { + name: String + age: Int +} +"#; + let out = compile_debug(src); + assert!(out.diagnostics.is_empty(), "struct def should have no type errors"); +} + +#[test] +fn test_enum_def_no_errors() { + let src = r#" +enum Status { + Active + Inactive + Pending(String) +} +"#; + let out = compile_debug(src); + assert!(out.diagnostics.is_empty(), "enum def should have no type errors"); +} + +#[test] +fn test_type_mismatch_diagnostic() { + let src = r#"let x: Int = "not an int""#; + let out = compile_debug(src); + assert!( + !out.diagnostics.is_empty(), + "type mismatch should produce at least one diagnostic" + ); +} + +#[test] +fn test_named_type_in_fn_param() { + let src = r#" +type Point { + x: Int + y: Int +} +fn get_x(p: Point) -> Int { + return p.x +} +"#; + let out = compile_debug(src); + assert!(!out.artifact.is_empty()); +} + +#[test] +fn test_array_type() { + let src = "let xs: [Int] = [1, 2, 3]"; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Push(Value::Int(1))))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Push(Value::Int(2))))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Push(Value::Int(3))))); + assert!(bc + .iter() + .any(|b| matches!(b, Bytecode::Call { name, .. } if name == "__build_list__"))); +} + +#[test] +fn test_optional_type_annotation_accepted() { + let src = "let maybe: Int? = 0"; + let out = compile_debug(src); + assert!(!out.artifact.is_empty()); +} + +#[test] +fn test_type_checker_ok_on_valid_program() { + let src = "let x: Int = 10"; + let prog = parse_program(src); + let mut checker = TypeChecker::with_builtins(); + checker.check(&prog); + assert!(checker.ok(), "valid program should type-check without errors"); +} + +#[test] +fn test_type_checker_error_on_mismatch() { + let src = r#"let x: Int = "oops""#; + let prog = parse_program(src); + let mut checker = TypeChecker::with_builtins(); + checker.check(&prog); + assert!(!checker.ok(), "type mismatch should be an error"); + assert!(!checker.diagnostics.is_empty()); +} + +#[test] +fn test_typecheck_struct_fields_resolved() { + let src = r#" +type Person { + name: String + age: Int +} +"#; + let prog = parse_program(src); + let mut checker = TypeChecker::with_builtins(); + checker.check(&prog); + assert!( + !checker.diagnostics.iter().any(|d| d.is_error), + "struct with known field types should have no errors" + ); +} + +#[test] +fn test_typecheck_enum_variants_resolved() { + let src = r#" +enum Mood { + Happy + Sad + Neutral +} +"#; + let prog = parse_program(src); + let mut checker = TypeChecker::with_builtins(); + checker.check(&prog); + assert!( + !checker.diagnostics.iter().any(|d| d.is_error), + "simple enum def should have no errors" + ); +} + +// ========================================================================= +// Group 5: Expressions +// ========================================================================= + +#[test] +fn test_binary_ops_all() { + let cases: &[(&str, fn(&Bytecode) -> bool)] = &[ + ("1 + 2", |b| matches!(b, Bytecode::Add)), + ("3 - 1", |b| matches!(b, Bytecode::Sub)), + ("2 * 3", |b| matches!(b, Bytecode::Mul)), + ("6 / 2", |b| matches!(b, Bytecode::Div)), + ]; + for (src, pred) in cases { + let bc = bytecode_of(src); + assert!(bc.iter().any(pred), "bytecode for `{src}` missing expected op"); + } +} + +#[test] +fn test_comparison_ops() { + let cases: &[(&str, fn(&Bytecode) -> bool)] = &[ + ("1 == 1", |b| matches!(b, Bytecode::Eq)), + ("1 != 2", |b| matches!(b, Bytecode::NotEq)), + ("1 < 2", |b| matches!(b, Bytecode::Lt)), + ("2 > 1", |b| matches!(b, Bytecode::Gt)), + ("1 <= 1", |b| matches!(b, Bytecode::LtEq)), + ("1 >= 1", |b| matches!(b, Bytecode::GtEq)), + ]; + for (src, pred) in cases { + let bc = bytecode_of(src); + assert!(bc.iter().any(pred), "bytecode for `{src}` missing comparison op"); + } +} + +#[test] +fn test_logical_ops() { + let bc_and = bytecode_of("true && false"); + assert!(bc_and.iter().any(|b| matches!(b, Bytecode::And))); + + let bc_or = bytecode_of("true || false"); + assert!(bc_or.iter().any(|b| matches!(b, Bytecode::Or))); +} + +#[test] +fn test_unary_not() { + let bc = bytecode_of("!true"); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Not))); +} + +#[test] +fn test_array_literal_arity() { + let bc = bytecode_of("[1, 2, 3]"); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Push(Value::Int(1))))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Push(Value::Int(2))))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Push(Value::Int(3))))); + assert!(bc + .iter() + .any(|b| matches!(b, Bytecode::Call { name, arity } if name == "__build_list__" && *arity == 3))); +} + +#[test] +fn test_empty_array_literal() { + let bc = bytecode_of("let empty = []"); + assert!(bc + .iter() + .any(|b| matches!(b, Bytecode::Call { name, arity } if name == "__build_list__" && *arity == 0))); +} + +#[test] +fn test_block_expr() { + let src = r#" +let result = { + let a = 1 + let b = 2 + a + b +} +"#; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Add))); +} + +#[test] +fn test_field_access() { + let src = r#" +type Point { x: Int y: Int } +fn get_x(p: Point) -> Int { + return p.x +} +"#; + let bc = bytecode_of(src); + assert!( + bc.iter() + .any(|b| matches!(b, Bytecode::GetField(f) if f == "x")), + "expected GetField(\"x\")" + ); +} + +#[test] +fn test_index_expr() { + let src = r#" +fn first(xs: [Int]) -> Int { + return xs[0] +} +"#; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::GetIndex))); +} + +#[test] +fn test_path_expr_emits_str_push() { + let src = r#" +enum Color { Red Green Blue } +let c = Color::Red +"#; + let bc = bytecode_of(src); + assert!( + bc.iter() + .any(|b| matches!(b, Bytecode::Push(Value::Str(s)) if s == "Red")), + "expected Push(Str(\"Red\")) for path expr" + ); +} + +#[test] +fn test_chained_arithmetic() { + let bc = bytecode_of("1 + 2 + 3 + 4"); + let add_count = bc.iter().filter(|b| matches!(b, Bytecode::Add)).count(); + assert!(add_count >= 3, "expected at least 3 ADD instructions for 1+2+3+4"); +} + +#[test] +fn test_subtraction_emits_sub() { + let bc = bytecode_of("10 - 3"); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Sub))); +} + +#[test] +fn test_division_emits_div() { + let bc = bytecode_of("100 / 4"); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Div))); +} + +// ========================================================================= +// Group 6: Activate construct +// ========================================================================= + +#[test] +fn test_activate_emits_activate_instruction() { + let src = r#"activate User where "find admins""#; + let bc = bytecode_of(src); + assert!( + bc.iter().any(|b| matches!( + b, + Bytecode::Activate { type_name, query } + if type_name == "User" && query == "find admins" + )), + "expected Activate {{ type_name: \"User\", query: \"find admins\" }}" + ); +} + +#[test] +fn test_activate_type_name_preserved() { + let src = r#"activate Document where "recent docs""#; + let bc = bytecode_of(src); + assert!(bc + .iter() + .any(|b| matches!(b, Bytecode::Activate { type_name, .. } if type_name == "Document"))); +} + +#[test] +fn test_activate_query_preserved() { + let src = r#"activate Node where "high importance nodes""#; + let bc = bytecode_of(src); + assert!(bc.iter().any( + |b| matches!(b, Bytecode::Activate { query, .. } if query == "high importance nodes") + )); +} + +#[test] +fn test_activate_in_fn_body() { + let src = r#" +fn fetch_users() -> String { + let result = activate User where "all users" + return "done" +} +"#; + let bc = bytecode_of(src); + assert!(bc + .iter() + .any(|b| matches!(b, Bytecode::Activate { type_name, .. } if type_name == "User"))); +} + +#[test] +fn test_activate_assigned_to_let() { + let src = r#"let result = activate Node where "query""#; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Activate { .. }))); + assert!(bc + .iter() + .any(|b| matches!(b, Bytecode::StoreLocal(n) if n == "result"))); +} + +// ========================================================================= +// Group 7: Sealed blocks +// ========================================================================= + +#[test] +fn test_sealed_emits_markers() { + let src = "sealed { let x = 1 }"; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::SealedBegin))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::SealedEnd))); +} + +#[test] +fn test_sealed_begin_before_end() { + let src = "sealed { let secret = 42 }"; + let bc = bytecode_of(src); + let begin_pos = bc + .iter() + .position(|b| matches!(b, Bytecode::SealedBegin)) + .expect("SealedBegin not found"); + let end_pos = bc + .iter() + .position(|b| matches!(b, Bytecode::SealedEnd)) + .expect("SealedEnd not found"); + assert!(begin_pos < end_pos, "SealedBegin must appear before SealedEnd"); +} + +#[test] +fn test_sealed_in_fn() { + let src = r#" +fn secure() -> String { + sealed { let key = "secret" } + return "done" +} +"#; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::SealedBegin))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::SealedEnd))); +} + +#[test] +fn test_prod_sealed_encryption() { + let src = "let x = 1"; + let out = Compiler::compile(src, prod_opts()).expect("prod compile failed"); + assert!(out.sealed); + assert_eq!(&out.artifact[..8], b"ENGRAM01", "artifact must start with ENGRAM01 magic"); +} + +#[test] +fn test_sealed_body_content_compiled() { + // The sealed block body should still be compiled — the int 10 is pushed inside the sealed region + let src = "sealed { let a = 10 }"; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Push(Value::Int(10))))); +} + +// ========================================================================= +// Group 8: Full pipeline type checking +// ========================================================================= + +#[test] +fn test_typecheck_fn_return_mismatch_debug_compiles() { + // In debug mode, type errors are non-fatal — we get diagnostics but still compile + let src = r#" +fn bad() -> Int { + return "not an int" +} +"#; + let out = compile_debug(src); + assert!(!out.artifact.is_empty(), "should still produce artifact in debug mode"); +} + +#[test] +fn test_typecheck_undefined_type_produces_diagnostic() { + let src = r#"let x: NonExistentType = 0"#; + let prog = parse_program(src); + let mut checker = TypeChecker::with_builtins(); + checker.check(&prog); + assert!( + !checker.diagnostics.is_empty() || !checker.ok(), + "undefined type should produce diagnostics" + ); +} + +#[test] +fn test_typecheck_array_homogeneous_accepted() { + let src = "let xs: [Int] = [1, 2, 3]"; + let prog = parse_program(src); + let mut checker = TypeChecker::with_builtins(); + checker.check(&prog); + assert!(!prog.stmts.is_empty()); +} + +#[test] +fn test_typecheck_fn_signature_registered() { + let src = r#" +fn square(n: Int) -> Int { + return n * n +} +let r = square(5) +"#; + let prog = parse_program(src); + let mut checker = TypeChecker::with_builtins(); + checker.check(&prog); + assert!(!prog.stmts.is_empty()); +} + +// ========================================================================= +// Group 9: Real programs — multi-statement programs +// ========================================================================= + +#[test] +fn test_program_fibonacci() { + let src = r#" +fn fib(n: Int) -> Int { + if n <= 1 { + return n + } else { + return fib(n - 1) + fib(n - 2) + } +} +let answer = fib(10) +"#; + let out = compile_debug(src); + assert!(out.diagnostics.is_empty(), "fibonacci should have no type errors"); + let bc = deserialize_bytecode(&out.artifact).unwrap(); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Call { name, .. } if name == "fib"))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::LtEq))); +} + +#[test] +fn test_program_http_handler() { + let src = r#" +fn handle_request(path: String) -> String { + if path == "/" { + return "index" + } else { + return "not found" + } +} +fn http_serve() -> String { + return handle_request("/") +} +"#; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Call { name, .. } if name == "handle_request"))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Eq))); +} + +#[test] +fn test_program_types_and_activate() { + let src = r#" +type User { + name: String + age: Int +} +let users = activate User where "all active users" +"#; + let bc = bytecode_of(src); + assert!(bc + .iter() + .any(|b| matches!(b, Bytecode::Activate { type_name, .. } if type_name == "User"))); +} + +#[test] +fn test_program_enum_match() { + let src = r#" +enum Direction { + North + South + East + West +} +let d = Direction::North +let label = match d { + "North" => "up" + "South" => "down" + _ => "side" +} +"#; + let bc = bytecode_of(src); + // Path expr pushes the variant name as a Str + assert!(bc.iter().any(|b| matches!(b, Bytecode::Push(Value::Str(s)) if s == "North"))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Eq))); +} + +#[test] +fn test_program_multi_function_call_chain() { + let src = r#" +fn add_one(n: Int) -> Int { return n + 1 } +fn double(n: Int) -> Int { return n + n } +fn transform(n: Int) -> Int { return double(add_one(n)) } +let result = transform(5) +"#; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Call { name, .. } if name == "transform"))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Call { name, .. } if name == "double"))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Call { name, .. } if name == "add_one"))); +} + +#[test] +fn test_program_sealed_with_activate() { + let src = r#" +sealed { + let secret_query = activate SecretNode where "classified" +} +"#; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::SealedBegin))); + assert!(bc + .iter() + .any(|b| matches!(b, Bytecode::Activate { type_name, .. } if type_name == "SecretNode"))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::SealedEnd))); +} + +// ========================================================================= +// Group 10: Prod target and sealing +// ========================================================================= + +#[test] +fn test_prod_magic_bytes() { + let out = Compiler::compile("let x = 1", prod_opts()).expect("prod compile failed"); + assert!(out.artifact.len() >= 8, "prod artifact must be at least 8 bytes"); + assert_eq!(&out.artifact[..8], b"ENGRAM01", "prod artifact must start with ENGRAM01"); +} + +#[test] +fn test_prod_roundtrip() { + let src = "let answer: Int = 42"; + let out = Compiler::compile(src, prod_opts()).expect("prod compile failed"); + let sealed = SealedArtifact::from_bytes(&out.artifact).expect("from_bytes failed"); + let bytecode_bytes = el_seal::unseal(&sealed, &[]).expect("unseal failed"); + let instructions = deserialize_bytecode(&bytecode_bytes).expect("deserialization failed"); + assert!( + instructions + .iter() + .any(|b| matches!(b, Bytecode::Push(Value::Int(42)))), + "roundtripped bytecode must contain Push(42)" + ); +} + +#[test] +fn test_prod_type_error_is_fatal() { + let src = r#"let x: Int = "not an int""#; + let result = Compiler::compile(src, prod_opts()); + assert!(result.is_err(), "prod mode must fail when there are type errors"); +} + +#[test] +fn test_prod_sealed_flag() { + let out = Compiler::compile("let x = 0", prod_opts()).expect("prod compile failed"); + assert!(out.sealed, "prod output must be marked as sealed"); +} + +#[test] +fn test_prod_no_source_map() { + let out = Compiler::compile("let x = 1", prod_opts()).expect("prod compile failed"); + assert!(out.source_map.is_none(), "prod output must not contain a source map"); +} + +#[test] +fn test_prod_roundtrip_fn_def() { + let src = r#" +fn square(n: Int) -> Int { + return n * n +} +"#; + let out = Compiler::compile(src, prod_opts()).expect("prod compile failed"); + let sealed = SealedArtifact::from_bytes(&out.artifact).expect("from_bytes failed"); + let bc_bytes = el_seal::unseal(&sealed, &[]).expect("unseal failed"); + let instructions = deserialize_bytecode(&bc_bytes).expect("deserialization failed"); + assert!(instructions.iter().any(|b| matches!(b, Bytecode::Mul))); + assert!(instructions.iter().any(|b| matches!(b, Bytecode::Return))); +} + +#[test] +fn test_prod_roundtrip_sealed_block() { + let src = "sealed { let key = 99 }"; + let out = Compiler::compile(src, prod_opts()).expect("prod compile failed"); + let sealed = SealedArtifact::from_bytes(&out.artifact).expect("from_bytes failed"); + let bc_bytes = el_seal::unseal(&sealed, &[]).expect("unseal failed"); + let instructions = deserialize_bytecode(&bc_bytes).expect("deserialization failed"); + assert!(instructions.iter().any(|b| matches!(b, Bytecode::SealedBegin))); + assert!(instructions + .iter() + .any(|b| matches!(b, Bytecode::Push(Value::Int(99))))); + assert!(instructions.iter().any(|b| matches!(b, Bytecode::SealedEnd))); +} + +// ========================================================================= +// Group 11: Bytecode structure invariants +// ========================================================================= + +#[test] +fn test_halt_always_last() { + let cases = [ + "let x = 1", + "42", + "fn f() -> Int { return 1 }", + r#"let s = "hi""#, + ]; + for src in &cases { + let bc = bytecode_of(src); + assert!( + matches!(bc.last(), Some(Bytecode::Halt)), + "bytecode for `{src}` must end with HALT" + ); + } +} + +#[test] +fn test_store_load_roundtrip_in_bytecode() { + let src = "let x = 5\nlet y = x"; + let bc = bytecode_of(src); + assert!(bc.iter().any(|b| matches!(b, Bytecode::StoreLocal(n) if n == "x"))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::LoadLocal(n) if n == "x"))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::StoreLocal(n) if n == "y"))); +} + +#[test] +fn test_not_eq_emits_noteq() { + let bc = bytecode_of("1 != 2"); + assert!(bc.iter().any(|b| matches!(b, Bytecode::NotEq))); +} + +#[test] +fn test_lt_emits_lt() { + let bc = bytecode_of("1 < 2"); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Lt))); +} + +#[test] +fn test_gt_emits_gt() { + let bc = bytecode_of("2 > 1"); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Gt))); +} + +#[test] +fn test_lteq_emits_lteq() { + let bc = bytecode_of("1 <= 1"); + assert!(bc.iter().any(|b| matches!(b, Bytecode::LtEq))); +} + +#[test] +fn test_gteq_emits_gteq() { + let bc = bytecode_of("2 >= 1"); + assert!(bc.iter().any(|b| matches!(b, Bytecode::GtEq))); +} + +#[test] +fn test_and_emits_and() { + let bc = bytecode_of("true && false"); + assert!(bc.iter().any(|b| matches!(b, Bytecode::And))); +} + +#[test] +fn test_or_emits_or() { + let bc = bytecode_of("true || false"); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Or))); +} + +#[test] +fn test_diagnostics_are_non_empty_strings() { + let src = r#"let x: Int = "bad""#; + let out = compile_debug(src); + for d in &out.diagnostics { + assert!(!d.is_empty(), "diagnostic messages must not be empty strings"); + } +} + +#[test] +fn test_parse_produces_correct_stmt_count() { + let src = "let x = 1"; + let prog = parse_program(src); + assert_eq!(prog.stmts.len(), 1); + assert_eq!(prog.source, src); +} + +#[test] +fn test_parse_fn_stmt_is_fn_def() { + let src = r#"fn f() -> Int { return 1 }"#; + let prog = parse_program(src); + assert_eq!(prog.stmts.len(), 1); + assert!(matches!(&prog.stmts[0], el_parser::Stmt::FnDef { name, .. } if name == "f")); +} + +#[test] +fn test_parse_multiple_stmts_count() { + let src = "let a = 1\nlet b = 2\nlet c = 3"; + let prog = parse_program(src); + assert_eq!(prog.stmts.len(), 3); +} + +#[test] +fn test_compile_multiple_lets_is_ok() { + let src = r#" +let a: Int = 1 +let b: Int = 2 +let c: Int = a + b +"#; + let out = compile_debug(src); + assert!(!out.artifact.is_empty()); + assert!(out.diagnostics.is_empty()); +} + +#[test] +fn test_jump_if_not_offset_is_nonzero_for_if() { + // A real if expression must jump past at least one instruction + let src = "if true { 1 } else { 2 }"; + let bc = bytecode_of(src); + let has_nonzero_jump = bc.iter().any(|b| match b { + Bytecode::JumpIfNot(off) => *off != 0, + _ => false, + }); + assert!(has_nonzero_jump, "JumpIfNot offset must be non-zero for real if"); +} + +#[test] +fn test_match_string_pattern_emits_str_push() { + let src = r#" +let x = "hello" +let r = match x { + "hello" => 1 + _ => 0 +} +"#; + let bc = bytecode_of(src); + // Pattern literal "hello" is pushed for comparison + assert!(bc + .iter() + .any(|b| matches!(b, Bytecode::Push(Value::Str(s)) if s == "hello"))); + assert!(bc.iter().any(|b| matches!(b, Bytecode::Eq))); +} diff --git a/crates/el-integration/src/tests/compiler_pipeline.rs b/engrams/el-integration/src/tests/compiler_pipeline.rs similarity index 100% rename from crates/el-integration/src/tests/compiler_pipeline.rs rename to engrams/el-integration/src/tests/compiler_pipeline.rs diff --git a/crates/el-integration/src/tests/decorator_codegen.rs b/engrams/el-integration/src/tests/decorator_codegen.rs similarity index 100% rename from crates/el-integration/src/tests/decorator_codegen.rs rename to engrams/el-integration/src/tests/decorator_codegen.rs diff --git a/crates/el-integration/src/tests/error_propagation.rs b/engrams/el-integration/src/tests/error_propagation.rs similarity index 100% rename from crates/el-integration/src/tests/error_propagation.rs rename to engrams/el-integration/src/tests/error_propagation.rs diff --git a/crates/el-integration/src/tests/mod.rs b/engrams/el-integration/src/tests/mod.rs similarity index 100% rename from crates/el-integration/src/tests/mod.rs rename to engrams/el-integration/src/tests/mod.rs diff --git a/crates/el-integration/src/tests/protocol_conformance.rs b/engrams/el-integration/src/tests/protocol_conformance.rs similarity index 100% rename from crates/el-integration/src/tests/protocol_conformance.rs rename to engrams/el-integration/src/tests/protocol_conformance.rs diff --git a/crates/el-integration/src/tests/stdlib_usage.rs b/engrams/el-integration/src/tests/stdlib_usage.rs similarity index 100% rename from crates/el-integration/src/tests/stdlib_usage.rs rename to engrams/el-integration/src/tests/stdlib_usage.rs diff --git a/crates/el-integration/src/tests/test_framework.rs b/engrams/el-integration/src/tests/test_framework.rs similarity index 100% rename from crates/el-integration/src/tests/test_framework.rs rename to engrams/el-integration/src/tests/test_framework.rs diff --git a/crates/el-lexer/Cargo.toml b/engrams/el-lexer/Cargo.toml similarity index 100% rename from crates/el-lexer/Cargo.toml rename to engrams/el-lexer/Cargo.toml diff --git a/crates/el-lexer/src/error.rs b/engrams/el-lexer/src/error.rs similarity index 100% rename from crates/el-lexer/src/error.rs rename to engrams/el-lexer/src/error.rs diff --git a/crates/el-lexer/src/lexer.rs b/engrams/el-lexer/src/lexer.rs similarity index 99% rename from crates/el-lexer/src/lexer.rs rename to engrams/el-lexer/src/lexer.rs index e3dda6b..6aacbb3 100644 --- a/crates/el-lexer/src/lexer.rs +++ b/engrams/el-lexer/src/lexer.rs @@ -348,6 +348,7 @@ fn keyword_or_ident(s: String) -> Token { "else" => Token::Else, "for" => Token::For, "in" => Token::In, + "while" => Token::While, "test" => Token::Test, "seed" => Token::Seed, "assert" => Token::Assert, diff --git a/crates/el-lexer/src/lib.rs b/engrams/el-lexer/src/lib.rs similarity index 100% rename from crates/el-lexer/src/lib.rs rename to engrams/el-lexer/src/lib.rs diff --git a/crates/el-lexer/src/token.rs b/engrams/el-lexer/src/token.rs similarity index 99% rename from crates/el-lexer/src/token.rs rename to engrams/el-lexer/src/token.rs index 1165c39..40bdd2e 100644 --- a/crates/el-lexer/src/token.rs +++ b/engrams/el-lexer/src/token.rs @@ -73,6 +73,8 @@ pub enum Token { For, /// `in` In, + /// `while` + While, /// `test` — test block definition Test, /// `seed` — graph seeding statement inside a test @@ -212,6 +214,7 @@ impl std::fmt::Display for Token { Token::If => write!(f, "if"), Token::Else => write!(f, "else"), Token::For => write!(f, "for"), + Token::While => write!(f, "while"), Token::In => write!(f, "in"), Token::Test => write!(f, "test"), Token::Seed => write!(f, "seed"), diff --git a/crates/el-lint/Cargo.toml b/engrams/el-lint/Cargo.toml similarity index 100% rename from crates/el-lint/Cargo.toml rename to engrams/el-lint/Cargo.toml diff --git a/crates/el-lint/src/error.rs b/engrams/el-lint/src/error.rs similarity index 100% rename from crates/el-lint/src/error.rs rename to engrams/el-lint/src/error.rs diff --git a/crates/el-lint/src/lib.rs b/engrams/el-lint/src/lib.rs similarity index 98% rename from crates/el-lint/src/lib.rs rename to engrams/el-lint/src/lib.rs index d256ddd..5853007 100644 --- a/crates/el-lint/src/lib.rs +++ b/engrams/el-lint/src/lib.rs @@ -1,4 +1,4 @@ -//! el-lint — linter for engram-lang source files. +//! el-lint — linter for el source files. //! //! Combines: //! - `el-arch` architectural rule violations (VBD, EBD, swarm, security, graph) @@ -14,7 +14,7 @@ pub use error::LintError; pub use linter::Linter; pub use report::{LintDiagnostic, LintReport, LintSeverity}; -/// Lint engram-lang source code. Returns a report with all diagnostics. +/// Lint el source code. Returns a report with all diagnostics. pub fn lint(source: &str) -> Result { Linter::new().lint(source) } diff --git a/crates/el-lint/src/linter.rs b/engrams/el-lint/src/linter.rs similarity index 100% rename from crates/el-lint/src/linter.rs rename to engrams/el-lint/src/linter.rs diff --git a/crates/el-lint/src/report.rs b/engrams/el-lint/src/report.rs similarity index 100% rename from crates/el-lint/src/report.rs rename to engrams/el-lint/src/report.rs diff --git a/crates/el-lint/src/rules.rs b/engrams/el-lint/src/rules.rs similarity index 100% rename from crates/el-lint/src/rules.rs rename to engrams/el-lint/src/rules.rs diff --git a/crates/el-manifest/Cargo.toml b/engrams/el-manifest/Cargo.toml similarity index 100% rename from crates/el-manifest/Cargo.toml rename to engrams/el-manifest/Cargo.toml diff --git a/crates/el-manifest/src/error.rs b/engrams/el-manifest/src/error.rs similarity index 100% rename from crates/el-manifest/src/error.rs rename to engrams/el-manifest/src/error.rs diff --git a/crates/el-manifest/src/lib.rs b/engrams/el-manifest/src/lib.rs similarity index 100% rename from crates/el-manifest/src/lib.rs rename to engrams/el-manifest/src/lib.rs diff --git a/crates/el-manifest/src/manifest.rs b/engrams/el-manifest/src/manifest.rs similarity index 100% rename from crates/el-manifest/src/manifest.rs rename to engrams/el-manifest/src/manifest.rs diff --git a/crates/el-manifest/src/parse.rs b/engrams/el-manifest/src/parse.rs similarity index 100% rename from crates/el-manifest/src/parse.rs rename to engrams/el-manifest/src/parse.rs diff --git a/crates/el-parser/Cargo.toml b/engrams/el-parser/Cargo.toml similarity index 100% rename from crates/el-parser/Cargo.toml rename to engrams/el-parser/Cargo.toml diff --git a/crates/el-parser/src/ast.rs b/engrams/el-parser/src/ast.rs similarity index 98% rename from crates/el-parser/src/ast.rs rename to engrams/el-parser/src/ast.rs index e66c937..3433e31 100644 --- a/crates/el-parser/src/ast.rs +++ b/engrams/el-parser/src/ast.rs @@ -272,6 +272,12 @@ pub enum Stmt { methods: Vec, span: Span, }, + /// `while { }` + While { + condition: Expr, + body: Vec, + span: Span, + }, /// `retry N times { ... } fallback { ... }` Retry { count: Expr, diff --git a/crates/el-parser/src/error.rs b/engrams/el-parser/src/error.rs similarity index 100% rename from crates/el-parser/src/error.rs rename to engrams/el-parser/src/error.rs diff --git a/crates/el-parser/src/lib.rs b/engrams/el-parser/src/lib.rs similarity index 100% rename from crates/el-parser/src/lib.rs rename to engrams/el-parser/src/lib.rs diff --git a/crates/el-parser/src/parser.rs b/engrams/el-parser/src/parser.rs similarity index 99% rename from crates/el-parser/src/parser.rs rename to engrams/el-parser/src/parser.rs index 4db8149..eb39735 100644 --- a/crates/el-parser/src/parser.rs +++ b/engrams/el-parser/src/parser.rs @@ -113,6 +113,7 @@ impl Parser { Token::From => "from".to_string(), Token::As => "as".to_string(), Token::With => "with".to_string(), + Token::While => "while".to_string(), Token::Retry => "retry".to_string(), Token::Times => "times".to_string(), Token::Fallback => "fallback".to_string(), @@ -173,6 +174,7 @@ impl Parser { Token::From => self.parse_from_import(start), Token::Protocol => self.parse_protocol_def(start), Token::Impl => self.parse_impl_def(start), + Token::While => self.parse_while(start), Token::Retry => self.parse_retry(start), Token::Deploy => self.parse_deploy(start), Token::Return => { @@ -516,6 +518,16 @@ impl Parser { Ok(Stmt::ImplDef { protocol_name, type_name, methods, span: start }) } + /// Parse `while { }` + fn parse_while(&mut self, start: Span) -> Result { + self.expect(&Token::While)?; + let condition = self.parse_expr()?; + self.expect(&Token::LBrace)?; + let body = self.parse_block_body()?; + self.expect(&Token::RBrace)?; + Ok(Stmt::While { condition, body, span: start }) + } + /// Parse `retry N times { ... } fallback { ... }` fn parse_retry(&mut self, start: Span) -> Result { self.expect(&Token::Retry)?; diff --git a/crates/el-registry/Cargo.toml b/engrams/el-registry/Cargo.toml similarity index 100% rename from crates/el-registry/Cargo.toml rename to engrams/el-registry/Cargo.toml diff --git a/crates/el-registry/src/client.rs b/engrams/el-registry/src/client.rs similarity index 100% rename from crates/el-registry/src/client.rs rename to engrams/el-registry/src/client.rs diff --git a/crates/el-registry/src/error.rs b/engrams/el-registry/src/error.rs similarity index 100% rename from crates/el-registry/src/error.rs rename to engrams/el-registry/src/error.rs diff --git a/crates/el-registry/src/lib.rs b/engrams/el-registry/src/lib.rs similarity index 100% rename from crates/el-registry/src/lib.rs rename to engrams/el-registry/src/lib.rs diff --git a/crates/el-registry/src/resolve.rs b/engrams/el-registry/src/resolve.rs similarity index 100% rename from crates/el-registry/src/resolve.rs rename to engrams/el-registry/src/resolve.rs diff --git a/crates/el-seal/Cargo.toml b/engrams/el-seal/Cargo.toml similarity index 100% rename from crates/el-seal/Cargo.toml rename to engrams/el-seal/Cargo.toml diff --git a/crates/el-seal/src/artifact.rs b/engrams/el-seal/src/artifact.rs similarity index 100% rename from crates/el-seal/src/artifact.rs rename to engrams/el-seal/src/artifact.rs diff --git a/crates/el-seal/src/error.rs b/engrams/el-seal/src/error.rs similarity index 100% rename from crates/el-seal/src/error.rs rename to engrams/el-seal/src/error.rs diff --git a/crates/el-seal/src/lib.rs b/engrams/el-seal/src/lib.rs similarity index 100% rename from crates/el-seal/src/lib.rs rename to engrams/el-seal/src/lib.rs diff --git a/crates/el-seal/src/seal.rs b/engrams/el-seal/src/seal.rs similarity index 100% rename from crates/el-seal/src/seal.rs rename to engrams/el-seal/src/seal.rs diff --git a/crates/el-stdlib/Cargo.toml b/engrams/el-stdlib/Cargo.toml similarity index 100% rename from crates/el-stdlib/Cargo.toml rename to engrams/el-stdlib/Cargo.toml diff --git a/crates/el-stdlib/src/array.rs b/engrams/el-stdlib/src/array.rs similarity index 100% rename from crates/el-stdlib/src/array.rs rename to engrams/el-stdlib/src/array.rs diff --git a/crates/el-stdlib/src/engram.rs b/engrams/el-stdlib/src/engram.rs similarity index 100% rename from crates/el-stdlib/src/engram.rs rename to engrams/el-stdlib/src/engram.rs diff --git a/crates/el-stdlib/src/lib.rs b/engrams/el-stdlib/src/lib.rs similarity index 100% rename from crates/el-stdlib/src/lib.rs rename to engrams/el-stdlib/src/lib.rs diff --git a/crates/el-stdlib/src/map.rs b/engrams/el-stdlib/src/map.rs similarity index 100% rename from crates/el-stdlib/src/map.rs rename to engrams/el-stdlib/src/map.rs diff --git a/crates/el-stdlib/src/math.rs b/engrams/el-stdlib/src/math.rs similarity index 100% rename from crates/el-stdlib/src/math.rs rename to engrams/el-stdlib/src/math.rs diff --git a/crates/el-stdlib/src/optional.rs b/engrams/el-stdlib/src/optional.rs similarity index 100% rename from crates/el-stdlib/src/optional.rs rename to engrams/el-stdlib/src/optional.rs diff --git a/crates/el-stdlib/src/result.rs b/engrams/el-stdlib/src/result.rs similarity index 100% rename from crates/el-stdlib/src/result.rs rename to engrams/el-stdlib/src/result.rs diff --git a/crates/el-stdlib/src/string.rs b/engrams/el-stdlib/src/string.rs similarity index 100% rename from crates/el-stdlib/src/string.rs rename to engrams/el-stdlib/src/string.rs diff --git a/crates/el-test/Cargo.toml b/engrams/el-test/Cargo.toml similarity index 100% rename from crates/el-test/Cargo.toml rename to engrams/el-test/Cargo.toml diff --git a/crates/el-test/src/discovery.rs b/engrams/el-test/src/discovery.rs similarity index 100% rename from crates/el-test/src/discovery.rs rename to engrams/el-test/src/discovery.rs diff --git a/crates/el-test/src/eval.rs b/engrams/el-test/src/eval.rs similarity index 100% rename from crates/el-test/src/eval.rs rename to engrams/el-test/src/eval.rs diff --git a/crates/el-test/src/graph.rs b/engrams/el-test/src/graph.rs similarity index 100% rename from crates/el-test/src/graph.rs rename to engrams/el-test/src/graph.rs diff --git a/crates/el-test/src/lib.rs b/engrams/el-test/src/lib.rs similarity index 100% rename from crates/el-test/src/lib.rs rename to engrams/el-test/src/lib.rs diff --git a/crates/el-test/src/report.rs b/engrams/el-test/src/report.rs similarity index 98% rename from crates/el-test/src/report.rs rename to engrams/el-test/src/report.rs index 1353482..759b2a2 100644 --- a/crates/el-test/src/report.rs +++ b/engrams/el-test/src/report.rs @@ -113,7 +113,7 @@ impl TestReport { let mut xml = String::new(); xml.push_str("\n"); xml.push_str(&format!( - "\n", + "\n", self.total, self.failed, self.errors, diff --git a/crates/el-test/src/runner.rs b/engrams/el-test/src/runner.rs similarity index 100% rename from crates/el-test/src/runner.rs rename to engrams/el-test/src/runner.rs diff --git a/crates/el-test/src/types.rs b/engrams/el-test/src/types.rs similarity index 100% rename from crates/el-test/src/types.rs rename to engrams/el-test/src/types.rs diff --git a/crates/el-types/Cargo.toml b/engrams/el-types/Cargo.toml similarity index 100% rename from crates/el-types/Cargo.toml rename to engrams/el-types/Cargo.toml diff --git a/crates/el-types/src/checker.rs b/engrams/el-types/src/checker.rs similarity index 99% rename from crates/el-types/src/checker.rs rename to engrams/el-types/src/checker.rs index 1473092..d0c58e1 100644 --- a/crates/el-types/src/checker.rs +++ b/engrams/el-types/src/checker.rs @@ -198,6 +198,10 @@ impl TypeChecker { } } Stmt::Deploy { .. } => {} + Stmt::While { condition, body, .. } => { + self.infer_expr(condition); + for s in body { self.check_stmt(s); } + } } } diff --git a/crates/el-types/src/error.rs b/engrams/el-types/src/error.rs similarity index 100% rename from crates/el-types/src/error.rs rename to engrams/el-types/src/error.rs diff --git a/crates/el-types/src/lib.rs b/engrams/el-types/src/lib.rs similarity index 100% rename from crates/el-types/src/lib.rs rename to engrams/el-types/src/lib.rs diff --git a/crates/el-types/src/types.rs b/engrams/el-types/src/types.rs similarity index 100% rename from crates/el-types/src/types.rs rename to engrams/el-types/src/types.rs diff --git a/crates/el-wasm/Cargo.toml b/engrams/el-wasm/Cargo.toml similarity index 100% rename from crates/el-wasm/Cargo.toml rename to engrams/el-wasm/Cargo.toml diff --git a/crates/el-wasm/build.sh b/engrams/el-wasm/build.sh similarity index 86% rename from crates/el-wasm/build.sh rename to engrams/el-wasm/build.sh index cc9b7b1..653f7ed 100755 --- a/crates/el-wasm/build.sh +++ b/engrams/el-wasm/build.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Build the engram-lang WASM package. +# Build the el WASM package. # # Requires wasm-pack: # cargo install wasm-pack @@ -13,7 +13,7 @@ set -euo pipefail cd "$(dirname "$0")" -echo "Building engram-lang WASM runtime..." +echo "Building el WASM runtime..." wasm-pack build --target web --out-dir pkg -- --features wasm echo "" diff --git a/crates/el-wasm/js/pwa-manifest.json b/engrams/el-wasm/js/pwa-manifest.json similarity index 88% rename from crates/el-wasm/js/pwa-manifest.json rename to engrams/el-wasm/js/pwa-manifest.json index a9e26d3..ffd8452 100644 --- a/crates/el-wasm/js/pwa-manifest.json +++ b/engrams/el-wasm/js/pwa-manifest.json @@ -1,7 +1,7 @@ { "name": "Neuron", "short_name": "Neuron", - "description": "Neuron — AI companion powered by engram-lang", + "description": "Neuron — AI companion powered by el", "start_url": "/", "display": "standalone", "background_color": "#0a0a0f", diff --git a/crates/el-wasm/js/runtime.js b/engrams/el-wasm/js/runtime.js similarity index 88% rename from crates/el-wasm/js/runtime.js rename to engrams/el-wasm/js/runtime.js index 3431320..b76ac65 100644 --- a/crates/el-wasm/js/runtime.js +++ b/engrams/el-wasm/js/runtime.js @@ -10,7 +10,7 @@ let wasmModule = null; /** - * Initialise the engram-lang WASM runtime. Safe to call multiple times — + * Initialise the el WASM runtime. Safe to call multiple times — * subsequent calls return the already-loaded module immediately. * * @param {string} wasmUrl - Path to the .wasm file (default: /pkg/el_wasm_bg.wasm) @@ -25,7 +25,7 @@ export async function initRuntime(wasmUrl = '/pkg/el_wasm_bg.wasm') { await init(wasmUrl); wasmModule = { compile_source, load_and_run, eval: elEval, version }; - console.log(`engram-lang WASM runtime v${version()} loaded`); + console.log(`el WASM runtime v${version()} loaded`); return wasmModule; } @@ -48,7 +48,7 @@ export async function runElc(elcUrl) { } /** - * Compile engram-lang source and run it immediately. Useful for REPL and + * Compile el source and run it immediately. Useful for REPL and * developer-mode execution where source is available at runtime. * * @param {string} source - Engram-lang source code @@ -60,7 +60,7 @@ export async function evalSource(source) { } /** - * Compile engram-lang source to bytecode bytes without running it. + * Compile el source to bytecode bytes without running it. * The returned Uint8Array can be stored or uploaded as a .elc file. * * @param {string} source - Engram-lang source code diff --git a/crates/el-wasm/js/service-worker.js b/engrams/el-wasm/js/service-worker.js similarity index 97% rename from crates/el-wasm/js/service-worker.js rename to engrams/el-wasm/js/service-worker.js index fa9d576..e7647e3 100644 --- a/crates/el-wasm/js/service-worker.js +++ b/engrams/el-wasm/js/service-worker.js @@ -1,4 +1,4 @@ -// Service worker for the Neuron PWA powered by engram-lang. +// Service worker for the Neuron PWA powered by el. // // Caching strategy: // /pkg/ — WASM runtime files, cache-first (content-addressed, change on version bump) diff --git a/crates/el-wasm/src/lib.rs b/engrams/el-wasm/src/lib.rs similarity index 99% rename from crates/el-wasm/src/lib.rs rename to engrams/el-wasm/src/lib.rs index 4b21b86..0c39187 100644 --- a/crates/el-wasm/src/lib.rs +++ b/engrams/el-wasm/src/lib.rs @@ -1,6 +1,6 @@ //! el-wasm — Engram language WebAssembly runtime. //! -//! Compiles the engram-lang compiler and (optionally) execution pipeline to +//! Compiles the el compiler and (optionally) execution pipeline to //! `wasm32-unknown-unknown`, exposing a JavaScript API via `wasm-bindgen`. //! //! # Build for browsers @@ -64,7 +64,7 @@ extern "C" { fn web_sys_log(s: &str); } -/// Compile engram-lang source code to bytecode bytes (`.elc` format). +/// Compile el source code to bytecode bytes (`.elc` format). /// /// Returns the raw bytecode bytes on success, or throws a JS error string /// describing the first compilation error. @@ -87,7 +87,7 @@ pub fn load_and_run(bytecode_bytes: &[u8]) -> Result { load_and_run_inner(bytecode_bytes).map_err(|e| JsValue::from_str(&e)) } -/// Compile and run engram-lang source in one step. +/// Compile and run el source in one step. /// /// Equivalent to `load_and_run(compile_source(source))`. Useful for REPL /// and developer-mode execution where the source is available at runtime. @@ -100,7 +100,7 @@ pub fn eval(source: &str) -> Result { load_and_run_inner(&bytes).map_err(|e| JsValue::from_str(&e)) } -/// Return the engram-lang runtime version string. +/// Return the el runtime version string. #[cfg(feature = "wasm")] #[wasm_bindgen] pub fn version() -> String { diff --git a/target/debug/libel_compiler.d b/target/debug/libel_compiler.d index dfa6a85..84ed300 100644 --- a/target/debug/libel_compiler.d +++ b/target/debug/libel_compiler.d @@ -1 +1 @@ -/Users/will/Development/neuron-technologies/engram-lang/target/debug/libel_compiler.rlib: /Users/will/Development/neuron-technologies/engram/crates/engram-crypto/src/algorithm.rs /Users/will/Development/neuron-technologies/engram/crates/engram-crypto/src/engine.rs /Users/will/Development/neuron-technologies/engram/crates/engram-crypto/src/error.rs /Users/will/Development/neuron-technologies/engram/crates/engram-crypto/src/lib.rs /Users/will/Development/neuron-technologies/engram/crates/engram-crypto/src/registry.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-compiler/src/bytecode.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-compiler/src/codegen.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-compiler/src/compiler.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-compiler/src/error.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-compiler/src/lib.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-compiler/src/source_map.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/token.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-parser/src/ast.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-parser/src/error.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-parser/src/lib.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-parser/src/parser.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-seal/src/artifact.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-seal/src/error.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-seal/src/lib.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-seal/src/seal.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-types/src/checker.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-types/src/error.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-types/src/lib.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-types/src/types.rs +/Users/will/Development/neuron-technologies/foundation/engram-lang/target/debug/libel_compiler.rlib: /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/algorithm.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/engine.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/registry.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-compiler/src/bytecode.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-compiler/src/codegen.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-compiler/src/compiler.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-compiler/src/debugger.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-compiler/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-compiler/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-compiler/src/source_map.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/token.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/ast.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/parser.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/artifact.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/seal.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/checker.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/types.rs diff --git a/target/debug/libel_compiler.rlib b/target/debug/libel_compiler.rlib index be197ca..2335d03 100644 Binary files a/target/debug/libel_compiler.rlib and b/target/debug/libel_compiler.rlib differ diff --git a/target/debug/libel_lexer.d b/target/debug/libel_lexer.d index 68c2b2d..df76e13 100644 --- a/target/debug/libel_lexer.d +++ b/target/debug/libel_lexer.d @@ -1 +1 @@ -/Users/will/Development/neuron-technologies/engram-lang/target/debug/libel_lexer.rlib: /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/token.rs +/Users/will/Development/neuron-technologies/foundation/engram-lang/target/debug/libel_lexer.rlib: /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/token.rs diff --git a/target/debug/libel_lexer.rlib b/target/debug/libel_lexer.rlib index 95e4a2d..e83a970 100644 Binary files a/target/debug/libel_lexer.rlib and b/target/debug/libel_lexer.rlib differ diff --git a/target/debug/libel_parser.d b/target/debug/libel_parser.d index 95ef9ab..cc53d02 100644 --- a/target/debug/libel_parser.d +++ b/target/debug/libel_parser.d @@ -1 +1 @@ -/Users/will/Development/neuron-technologies/engram-lang/target/debug/libel_parser.rlib: /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/token.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-parser/src/ast.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-parser/src/error.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-parser/src/lib.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-parser/src/parser.rs +/Users/will/Development/neuron-technologies/foundation/engram-lang/target/debug/libel_parser.rlib: /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/token.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/ast.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/parser.rs diff --git a/target/debug/libel_parser.rlib b/target/debug/libel_parser.rlib index 7601a94..294dfa1 100644 Binary files a/target/debug/libel_parser.rlib and b/target/debug/libel_parser.rlib differ diff --git a/target/debug/libel_seal.d b/target/debug/libel_seal.d index 9573009..28fc99c 100644 --- a/target/debug/libel_seal.d +++ b/target/debug/libel_seal.d @@ -1 +1 @@ -/Users/will/Development/neuron-technologies/engram-lang/target/debug/libel_seal.rlib: /Users/will/Development/neuron-technologies/engram/crates/engram-crypto/src/algorithm.rs /Users/will/Development/neuron-technologies/engram/crates/engram-crypto/src/engine.rs /Users/will/Development/neuron-technologies/engram/crates/engram-crypto/src/error.rs /Users/will/Development/neuron-technologies/engram/crates/engram-crypto/src/lib.rs /Users/will/Development/neuron-technologies/engram/crates/engram-crypto/src/registry.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-seal/src/artifact.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-seal/src/error.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-seal/src/lib.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-seal/src/seal.rs +/Users/will/Development/neuron-technologies/foundation/engram-lang/target/debug/libel_seal.rlib: /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/algorithm.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/engine.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/registry.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/artifact.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/seal.rs diff --git a/target/debug/libel_seal.rlib b/target/debug/libel_seal.rlib index bbf1195..087130c 100644 Binary files a/target/debug/libel_seal.rlib and b/target/debug/libel_seal.rlib differ diff --git a/target/debug/libel_types.d b/target/debug/libel_types.d index 79ccbe0..a9a6f2a 100644 --- a/target/debug/libel_types.d +++ b/target/debug/libel_types.d @@ -1 +1 @@ -/Users/will/Development/neuron-technologies/engram-lang/target/debug/libel_types.rlib: /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-lexer/src/token.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-parser/src/ast.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-parser/src/error.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-parser/src/lib.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-parser/src/parser.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-types/src/checker.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-types/src/error.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-types/src/lib.rs /Users/will/Development/neuron-technologies/engram-lang/crates/el-types/src/types.rs +/Users/will/Development/neuron-technologies/foundation/engram-lang/target/debug/libel_types.rlib: /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/token.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/ast.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/parser.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/checker.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/types.rs diff --git a/target/debug/libel_types.rlib b/target/debug/libel_types.rlib index 175cf21..40fac8d 100644 Binary files a/target/debug/libel_types.rlib and b/target/debug/libel_types.rlib differ