From 8d4d9ed78606aba6fe5774ad4eedcf76a462091e Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Wed, 29 Apr 2026 03:58:16 -0500 Subject: [PATCH] Add operators, math, time, string, collection, and HOF builtins to El --- bin/el/src/main.rs | 734 +++++++++++++++++++++++++++- engrams/el-arch/src/checker.rs | 3 + engrams/el-compiler/src/bytecode.rs | 14 + engrams/el-compiler/src/codegen.rs | 27 +- engrams/el-fmt/src/formatter.rs | 11 + engrams/el-lexer/src/lexer.rs | 16 +- engrams/el-lexer/src/token.rs | 18 + engrams/el-parser/src/ast.rs | 16 + engrams/el-parser/src/lib.rs | 2 +- engrams/el-parser/src/parser.rs | 49 +- engrams/el-stdlib/src/array.rs | 22 + engrams/el-stdlib/src/math.rs | 39 ++ engrams/el-stdlib/src/string.rs | 8 + engrams/el-test/src/eval.rs | 26 + engrams/el-types/src/checker.rs | 6 + 15 files changed, 972 insertions(+), 19 deletions(-) diff --git a/bin/el/src/main.rs b/bin/el/src/main.rs index 49014b9..d9933db 100644 --- a/bin/el/src/main.rs +++ b/bin/el/src/main.rs @@ -1152,6 +1152,40 @@ fn run_sub_interpreter_with_stack( _ => Value::Nil, }); } + Bytecode::Mod => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(1)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => if y == 0 { Value::Int(0) } else { Value::Int(x % y) }, + (Value::Float(x), Value::Float(y)) => Value::Float(x % y), + (Value::Int(x), Value::Float(y)) => Value::Float(x as f64 % y), + (Value::Float(x), Value::Int(y)) => Value::Float(x % y as f64), + _ => Value::Int(0), + }); + } + Bytecode::BitAnd => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { (Value::Int(x), Value::Int(y)) => Value::Int(x & y), _ => Value::Int(0) }); + } + Bytecode::BitOr => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { (Value::Int(x), Value::Int(y)) => Value::Int(x | y), _ => Value::Int(0) }); + } + Bytecode::BitXor => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { (Value::Int(x), Value::Int(y)) => Value::Int(x ^ y), _ => Value::Int(0) }); + } + Bytecode::BitNot => { + let a = stack.pop().unwrap_or(Value::Int(0)); + stack.push(match a { Value::Int(x) => Value::Int(!x), _ => Value::Int(0) }); + } + Bytecode::Shl => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { (Value::Int(x), Value::Int(y)) => Value::Int(x << (y as u32)), _ => Value::Int(0) }); + } + Bytecode::Shr => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { (Value::Int(x), Value::Int(y)) => Value::Int(x >> (y as u32)), _ => Value::Int(0) }); + } Bytecode::Eq => { let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); stack.push(Value::Bool(a == b)); @@ -1473,6 +1507,58 @@ fn run_interpreter_with_args(instructions: &[el_compiler::Bytecode], program_arg _ => Value::Nil, }); } + Bytecode::Mod => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(1)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => if y == 0 { Value::Int(0) } else { Value::Int(x % y) }, + (Value::Float(x), Value::Float(y)) => Value::Float(x % y), + (Value::Int(x), Value::Float(y)) => Value::Float(x as f64 % y), + (Value::Float(x), Value::Int(y)) => Value::Float(x % y as f64), + _ => Value::Int(0), + }); + } + Bytecode::BitAnd => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Int(x & y), + _ => Value::Int(0), + }); + } + Bytecode::BitOr => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Int(x | y), + _ => Value::Int(0), + }); + } + Bytecode::BitXor => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Int(x ^ y), + _ => Value::Int(0), + }); + } + Bytecode::BitNot => { + let a = stack.pop().unwrap_or(Value::Int(0)); + stack.push(match a { + Value::Int(x) => Value::Int(!x), + _ => Value::Int(0), + }); + } + Bytecode::Shl => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Int(x << (y as u32)), + _ => Value::Int(0), + }); + } + Bytecode::Shr => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Int(x >> (y as u32)), + _ => Value::Int(0), + }); + } Bytecode::Eq => { let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); stack.push(Value::Bool(a == b)); @@ -1543,16 +1629,84 @@ fn run_interpreter_with_args(instructions: &[el_compiler::Bytecode], program_arg BuiltinResult::Handled | BuiltinResult::HttpServe => {} BuiltinResult::Exit(code) => std::process::exit(code), BuiltinResult::NotBuiltin => { - // Try user-defined function - if let Some(&entry) = fn_table.get(name.as_str()) { - // Save current locals and return address - let saved = locals.clone(); - call_stack.push((ip + 1, saved)); - ip = entry; - continue; + // Handle HOFs that need fn_table access + match name.as_str() { + "list_map" => { + let fn_name = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let list = match stack.pop().unwrap_or(Value::List(vec![])) { + Value::List(l) => l, + _ => vec![], + }; + let mut result = Vec::new(); + if let Some(&entry) = fn_table.get(&fn_name) { + for item in list { + let mapped = run_sub_interpreter_with_stack(instructions, &fn_table, entry, vec![item]); + result.push(mapped); + } + } + stack.push(Value::List(result)); + } + "list_filter" => { + let fn_name = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let list = match stack.pop().unwrap_or(Value::List(vec![])) { + Value::List(l) => l, + _ => vec![], + }; + let mut result = Vec::new(); + if let Some(&entry) = fn_table.get(&fn_name) { + for item in list { + let keep = run_sub_interpreter_with_stack(instructions, &fn_table, entry, vec![item.clone()]); + if matches!(keep, Value::Bool(true)) { + result.push(item); + } + } + } + stack.push(Value::List(result)); + } + "list_reduce" => { + let fn_name = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let init = stack.pop().unwrap_or(Value::Nil); + let list = match stack.pop().unwrap_or(Value::List(vec![])) { + Value::List(l) => l, + _ => vec![], + }; + let mut acc = init; + if let Some(&entry) = fn_table.get(&fn_name) { + for item in list { + acc = run_sub_interpreter_with_stack(instructions, &fn_table, entry, vec![acc, item]); + } + } + stack.push(acc); + } + "fn_ref" => { + let name_val = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + stack.push(Value::Str(name_val)); + } + _ => { + // Try user-defined function + if let Some(&entry) = fn_table.get(name.as_str()) { + // Save current locals and return address + let saved = locals.clone(); + call_stack.push((ip + 1, saved)); + ip = entry; + continue; + } + // Unknown — push Nil + stack.push(Value::Nil); + } } - // Unknown — push Nil - stack.push(Value::Nil); } } } @@ -5368,6 +5522,480 @@ fn dispatch_builtin( BuiltinResult::Handled } + // ── Math additions ──────────────────────────────────────────────────── + "math_sin" => { + let a = stack.pop().unwrap_or(Value::Float(0.0)); + let v = match a { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 0.0 }; + stack.push(Value::Float(v.sin())); BuiltinResult::Handled + } + "math_cos" => { + let a = stack.pop().unwrap_or(Value::Float(0.0)); + let v = match a { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 0.0 }; + stack.push(Value::Float(v.cos())); BuiltinResult::Handled + } + "math_tan" => { + let a = stack.pop().unwrap_or(Value::Float(0.0)); + let v = match a { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 0.0 }; + stack.push(Value::Float(v.tan())); BuiltinResult::Handled + } + "math_asin" => { + let a = stack.pop().unwrap_or(Value::Float(0.0)); + let v = match a { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 0.0 }; + stack.push(Value::Float(v.asin())); BuiltinResult::Handled + } + "math_acos" => { + let a = stack.pop().unwrap_or(Value::Float(0.0)); + let v = match a { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 0.0 }; + stack.push(Value::Float(v.acos())); BuiltinResult::Handled + } + "math_atan2" => { + let x = match stack.pop().unwrap_or(Value::Float(1.0)) { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 1.0 }; + let y = match stack.pop().unwrap_or(Value::Float(0.0)) { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 0.0 }; + stack.push(Value::Float(y.atan2(x))); BuiltinResult::Handled + } + "math_exp" => { + let a = stack.pop().unwrap_or(Value::Float(0.0)); + let v = match a { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 0.0 }; + stack.push(Value::Float(v.exp())); BuiltinResult::Handled + } + "math_ln" => { + let a = stack.pop().unwrap_or(Value::Float(1.0)); + let v = match a { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 1.0 }; + stack.push(Value::Float(v.ln())); BuiltinResult::Handled + } + "math_log2" => { + let a = stack.pop().unwrap_or(Value::Float(1.0)); + let v = match a { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 1.0 }; + stack.push(Value::Float(v.log2())); BuiltinResult::Handled + } + "math_log10" => { + let a = stack.pop().unwrap_or(Value::Float(1.0)); + let v = match a { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 1.0 }; + stack.push(Value::Float(v.log10())); BuiltinResult::Handled + } + "math_mod" => { + let b = match stack.pop().unwrap_or(Value::Int(1)) { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 1.0 }; + let a = match stack.pop().unwrap_or(Value::Int(0)) { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 0.0 }; + stack.push(Value::Float(a % b)); BuiltinResult::Handled + } + "math_pi" => { stack.push(Value::Float(std::f64::consts::PI)); BuiltinResult::Handled } + "math_e" => { stack.push(Value::Float(std::f64::consts::E)); BuiltinResult::Handled } + "int_to_float" => { + let a = stack.pop().unwrap_or(Value::Int(0)); + let v = match a { Value::Int(i) => i as f64, Value::Float(f) => f, _ => 0.0 }; + stack.push(Value::Float(v)); BuiltinResult::Handled + } + "float_to_int" => { + let a = stack.pop().unwrap_or(Value::Float(0.0)); + let v = match a { Value::Float(f) => f as i64, Value::Int(i) => i, _ => 0 }; + stack.push(Value::Int(v)); BuiltinResult::Handled + } + "is_nil" => { + let a = stack.pop().unwrap_or(Value::Nil); + stack.push(Value::Bool(matches!(a, Value::Nil))); BuiltinResult::Handled + } + "unwrap_or" => { + let default_val = stack.pop().unwrap_or(Value::Nil); + let val = stack.pop().unwrap_or(Value::Nil); + stack.push(match val { Value::Nil => default_val, other => other }); + BuiltinResult::Handled + } + "decimal_add" => { + let b = match stack.pop().unwrap_or(Value::Float(0.0)) { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 0.0 }; + let a = match stack.pop().unwrap_or(Value::Float(0.0)) { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 0.0 }; + stack.push(Value::Float(a + b)); BuiltinResult::Handled + } + "decimal_sub" => { + let b = match stack.pop().unwrap_or(Value::Float(0.0)) { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 0.0 }; + let a = match stack.pop().unwrap_or(Value::Float(0.0)) { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 0.0 }; + stack.push(Value::Float(a - b)); BuiltinResult::Handled + } + "decimal_mul" => { + let b = match stack.pop().unwrap_or(Value::Float(1.0)) { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 1.0 }; + let a = match stack.pop().unwrap_or(Value::Float(1.0)) { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 1.0 }; + stack.push(Value::Float(a * b)); BuiltinResult::Handled + } + "decimal_div" => { + let b = match stack.pop().unwrap_or(Value::Float(1.0)) { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 1.0 }; + let a = match stack.pop().unwrap_or(Value::Float(0.0)) { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 0.0 }; + stack.push(Value::Float(if b == 0.0 { 0.0 } else { a / b })); BuiltinResult::Handled + } + "decimal_round" => { + let decimals = match stack.pop().unwrap_or(Value::Int(2)) { Value::Int(i) => i, _ => 2 }; + let f = match stack.pop().unwrap_or(Value::Float(0.0)) { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 0.0 }; + let factor = 10f64.powi(decimals as i32); + stack.push(Value::Float((f * factor).round() / factor)); BuiltinResult::Handled + } + + // ── String additions ────────────────────────────────────────────────── + "str_char_at" => { + let i = match stack.pop().unwrap_or(Value::Int(0)) { Value::Int(i) => i as usize, _ => 0 }; + let s = match stack.pop().unwrap_or(Value::Str(String::new())) { Value::Str(s) => s, _ => String::new() }; + let ch = s.chars().nth(i).map(|c| c.to_string()).unwrap_or_default(); + stack.push(Value::Str(ch)); BuiltinResult::Handled + } + "str_char_code" => { + let i = match stack.pop().unwrap_or(Value::Int(0)) { Value::Int(i) => i as usize, _ => 0 }; + let s = match stack.pop().unwrap_or(Value::Str(String::new())) { Value::Str(s) => s, _ => String::new() }; + let code = s.chars().nth(i).map(|c| c as i64).unwrap_or(0); + stack.push(Value::Int(code)); BuiltinResult::Handled + } + "str_from_char_code" => { + let n = match stack.pop().unwrap_or(Value::Int(0)) { Value::Int(i) => i as u32, _ => 0 }; + let ch = char::from_u32(n).map(|c| c.to_string()).unwrap_or_default(); + stack.push(Value::Str(ch)); BuiltinResult::Handled + } + "str_pad_left" => { + let pad = match stack.pop().unwrap_or(Value::Str(" ".to_string())) { Value::Str(s) => s, _ => " ".to_string() }; + let width = match stack.pop().unwrap_or(Value::Int(0)) { Value::Int(i) => i as usize, _ => 0 }; + let s = match stack.pop().unwrap_or(Value::Str(String::new())) { Value::Str(s) => s, _ => String::new() }; + let pad_char = pad.chars().next().unwrap_or(' '); + let cur_len = s.chars().count(); + let result = if cur_len >= width { s } else { + let pad_count = width - cur_len; + let mut r = String::new(); + for _ in 0..pad_count { r.push(pad_char); } + r.push_str(&s); + r + }; + stack.push(Value::Str(result)); BuiltinResult::Handled + } + "str_pad_right" => { + let pad = match stack.pop().unwrap_or(Value::Str(" ".to_string())) { Value::Str(s) => s, _ => " ".to_string() }; + let width = match stack.pop().unwrap_or(Value::Int(0)) { Value::Int(i) => i as usize, _ => 0 }; + let s = match stack.pop().unwrap_or(Value::Str(String::new())) { Value::Str(s) => s, _ => String::new() }; + let pad_char = pad.chars().next().unwrap_or(' '); + let cur_len = s.chars().count(); + let result = if cur_len >= width { s } else { + let mut r = s; + let pad_count = width - cur_len; + for _ in 0..pad_count { r.push(pad_char); } + r + }; + stack.push(Value::Str(result)); BuiltinResult::Handled + } + "format_float" => { + let decimals = match stack.pop().unwrap_or(Value::Int(2)) { Value::Int(i) => i, _ => 2 }; + let f = match stack.pop().unwrap_or(Value::Float(0.0)) { Value::Float(f) => f, Value::Int(i) => i as f64, _ => 0.0 }; + stack.push(Value::Str(format!("{:.prec$}", f, prec = decimals as usize))); + BuiltinResult::Handled + } + "str_format" => { + let map = match stack.pop().unwrap_or(Value::Map(vec![])) { Value::Map(m) => m, _ => vec![] }; + let template = match stack.pop().unwrap_or(Value::Str(String::new())) { Value::Str(s) => s, _ => String::new() }; + let mut result = template; + for (key, val) in &map { + let placeholder = format!("{{{}}}", key); + let replacement = match val { + Value::Str(s) => s.clone(), + Value::Int(i) => i.to_string(), + Value::Float(f) => f.to_string(), + Value::Bool(b) => b.to_string(), + _ => String::new(), + }; + result = result.replace(&placeholder, &replacement); + } + stack.push(Value::Str(result)); BuiltinResult::Handled + } + + // ── List additions ──────────────────────────────────────────────────── + "list_push" => { + let item = stack.pop().unwrap_or(Value::Nil); + let list = match stack.pop().unwrap_or(Value::List(vec![])) { Value::List(l) => l, _ => vec![] }; + let mut new_list = list; + new_list.push(item); + stack.push(Value::List(new_list)); BuiltinResult::Handled + } + "list_pop" => { + let list = match stack.pop().unwrap_or(Value::List(vec![])) { Value::List(l) => l, _ => vec![] }; + let mut new_list = list; + new_list.pop(); + stack.push(Value::List(new_list)); BuiltinResult::Handled + } + "list_pop_front" => { + let list = match stack.pop().unwrap_or(Value::List(vec![])) { Value::List(l) => l, _ => vec![] }; + let new_list = if list.is_empty() { vec![] } else { list[1..].to_vec() }; + stack.push(Value::List(new_list)); BuiltinResult::Handled + } + "list_peek_last" => { + let list = match stack.pop().unwrap_or(Value::List(vec![])) { Value::List(l) => l, _ => vec![] }; + let val = list.last().cloned().unwrap_or(Value::Nil); + stack.push(val); BuiltinResult::Handled + } + "list_range" => { + let end = match stack.pop().unwrap_or(Value::Int(0)) { Value::Int(i) => i, _ => 0 }; + let start = match stack.pop().unwrap_or(Value::Int(0)) { Value::Int(i) => i, _ => 0 }; + let list: Vec = (start..end).map(Value::Int).collect(); + stack.push(Value::List(list)); BuiltinResult::Handled + } + "list_new" => { stack.push(Value::List(vec![])); BuiltinResult::Handled } + "list_empty" => { + let list = match stack.pop().unwrap_or(Value::List(vec![])) { Value::List(l) => l, _ => vec![] }; + stack.push(Value::Bool(list.is_empty())); BuiltinResult::Handled + } + "list_map" | "list_filter" | "list_reduce" | "fn_ref" => { + // These require fn_table access; handled in NotBuiltin branch of main loop. + BuiltinResult::NotBuiltin + } + + // ── Stack aliases ───────────────────────────────────────────────────── + "stack_push" => { + let item = stack.pop().unwrap_or(Value::Nil); + let list = match stack.pop().unwrap_or(Value::List(vec![])) { Value::List(l) => l, _ => vec![] }; + let mut new_list = list; new_list.push(item); + stack.push(Value::List(new_list)); BuiltinResult::Handled + } + "stack_pop" => { + let list = match stack.pop().unwrap_or(Value::List(vec![])) { Value::List(l) => l, _ => vec![] }; + let mut new_list = list; new_list.pop(); + stack.push(Value::List(new_list)); BuiltinResult::Handled + } + "stack_peek" => { + let list = match stack.pop().unwrap_or(Value::List(vec![])) { Value::List(l) => l, _ => vec![] }; + let val = list.last().cloned().unwrap_or(Value::Nil); + stack.push(val); BuiltinResult::Handled + } + "stack_new" => { stack.push(Value::List(vec![])); BuiltinResult::Handled } + + // ── Queue aliases ───────────────────────────────────────────────────── + "queue_enqueue" => { + let item = stack.pop().unwrap_or(Value::Nil); + let list = match stack.pop().unwrap_or(Value::List(vec![])) { Value::List(l) => l, _ => vec![] }; + let mut new_list = list; new_list.push(item); + stack.push(Value::List(new_list)); BuiltinResult::Handled + } + "queue_dequeue" => { + let list = match stack.pop().unwrap_or(Value::List(vec![])) { Value::List(l) => l, _ => vec![] }; + let new_list = if list.is_empty() { vec![] } else { list[1..].to_vec() }; + stack.push(Value::List(new_list)); BuiltinResult::Handled + } + "queue_peek" => { + let list = match stack.pop().unwrap_or(Value::List(vec![])) { Value::List(l) => l, _ => vec![] }; + let val = list.first().cloned().unwrap_or(Value::Nil); + stack.push(val); BuiltinResult::Handled + } + "queue_new" => { stack.push(Value::List(vec![])); BuiltinResult::Handled } + + // ── Time builtins ───────────────────────────────────────────────────── + "time_now_utc" => { + use std::time::{SystemTime, UNIX_EPOCH}; + let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as i64; + stack.push(Value::Int(ms)); BuiltinResult::Handled + } + "time_to_parts" => { + let ts = match stack.pop().unwrap_or(Value::Int(0)) { Value::Int(i) => i, _ => 0 }; + let (year, month, day, hour, min, sec, ms) = epoch_ms_to_parts(ts); + let days_since_epoch = ts / 86_400_000; + let weekday_num = ((days_since_epoch % 7 + 4) % 7 + 7) % 7; + let weekday_names = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]; + let weekday_name = weekday_names[weekday_num as usize].to_string(); + let pairs = vec![ + ("year".to_string(), Value::Int(year as i64)), + ("month".to_string(), Value::Int(month as i64)), + ("day".to_string(), Value::Int(day as i64)), + ("hour".to_string(), Value::Int(hour as i64)), + ("minute".to_string(), Value::Int(min as i64)), + ("second".to_string(), Value::Int(sec as i64)), + ("ms".to_string(), Value::Int(ms as i64)), + ("weekday_num".to_string(), Value::Int(weekday_num)), + ("weekday_name".to_string(), Value::Str(weekday_name)), + ("tz".to_string(), Value::Str("UTC".to_string())), + ]; + stack.push(Value::Map(pairs)); BuiltinResult::Handled + } + "time_from_parts" => { + let parts = match stack.pop().unwrap_or(Value::Map(vec![])) { Value::Map(m) => m, _ => vec![] }; + let get_i = |m: &Vec<(String,Value)>, k: &str, def: i64| -> i64 { + m.iter().find(|(key,_)| key == k).and_then(|(_,v)| if let Value::Int(i) = v { Some(*i) } else { None }).unwrap_or(def) + }; + let year = get_i(&parts, "year", 1970) as i32; + let month = get_i(&parts, "month", 1) as u32; + let day = get_i(&parts, "day", 1) as u32; + let hour = get_i(&parts, "hour", 0) as u32; + let min = get_i(&parts, "minute", 0) as u32; + let sec = get_i(&parts, "second", 0) as u32; + let ms = get_i(&parts, "ms", 0) as u32; + stack.push(Value::Int(parts_to_epoch_ms(year, month, day, hour, min, sec, ms))); + BuiltinResult::Handled + } + "time_format" => { + let fmt = match stack.pop().unwrap_or(Value::Str("ISO".to_string())) { Value::Str(s) => s, _ => "ISO".to_string() }; + let ts = match stack.pop().unwrap_or(Value::Int(0)) { Value::Int(i) => i, _ => 0 }; + let (year, month, day, hour, min, sec, _ms) = epoch_ms_to_parts(ts); + let result = if fmt == "ISO" || fmt == "iso" { + format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", year, month, day, hour, min, sec) + } else { + fmt.replace("{YYYY}", &format!("{:04}", year)) + .replace("{MM}", &format!("{:02}", month)) + .replace("{DD}", &format!("{:02}", day)) + .replace("{HH}", &format!("{:02}", hour)) + .replace("{mm}", &format!("{:02}", min)) + .replace("{ss}", &format!("{:02}", sec)) + }; + stack.push(Value::Str(result)); BuiltinResult::Handled + } + "time_parse" => { + let s = match stack.pop().unwrap_or(Value::Str(String::new())) { Value::Str(s) => s, _ => String::new() }; + let result = (|| -> Option { + let s = s.trim().trim_end_matches('Z'); + let (date_part, time_part) = if let Some(t_pos) = s.find('T') { + (&s[..t_pos], Some(&s[t_pos+1..])) + } else { (s, None) }; + let dp: Vec<&str> = date_part.split('-').collect(); + if dp.len() < 3 { return None; } + let year = dp[0].parse::().ok()?; + let month = dp[1].parse::().ok()?; + let day = dp[2].parse::().ok()?; + let (hour, min, sec) = if let Some(tp) = time_part { + let tp = tp.trim_end_matches('Z'); + let tp_parts: Vec<&str> = tp.split(':').collect(); + let h = tp_parts.get(0).and_then(|s| s.parse::().ok()).unwrap_or(0); + let m = tp_parts.get(1).and_then(|s| s.parse::().ok()).unwrap_or(0); + let sv = tp_parts.get(2).and_then(|s| s.parse::().ok()).unwrap_or(0.0); + (h, m, sv as u32) + } else { (0, 0, 0) }; + Some(parts_to_epoch_ms(year, month, day, hour, min, sec, 0)) + })().unwrap_or(0); + stack.push(Value::Int(result)); BuiltinResult::Handled + } + "time_add" => { + let unit = match stack.pop().unwrap_or(Value::Str("ms".to_string())) { Value::Str(s) => s, _ => "ms".to_string() }; + let amt = match stack.pop().unwrap_or(Value::Int(0)) { Value::Int(i) => i, _ => 0 }; + let ts = match stack.pop().unwrap_or(Value::Int(0)) { Value::Int(i) => i, _ => 0 }; + let result = match unit.as_str() { + "ms" => ts + amt, + "sec" | "second" | "seconds" => ts + amt * 1_000, + "min" | "minute" | "minutes" => ts + amt * 60_000, + "hour" | "hours" => ts + amt * 3_600_000, + "day" | "days" => ts + amt * 86_400_000, + "week" | "weeks" => ts + amt * 604_800_000, + "month" | "months" => { + let (y, m, d, h, mn, s, ms) = epoch_ms_to_parts(ts); + let total_months = (y as i64) * 12 + (m as i64 - 1) + amt; + let ny = (total_months / 12) as i32; + let nm = (total_months % 12 + 1) as u32; + let nd = d.min(days_in_month(ny, nm)); + parts_to_epoch_ms(ny, nm, nd, h, mn, s, ms) + } + "year" | "years" => { + let (y, m, d, h, mn, s, ms) = epoch_ms_to_parts(ts); + let ny = y + amt as i32; + let nd = d.min(days_in_month(ny, m)); + parts_to_epoch_ms(ny, m, nd, h, mn, s, ms) + } + _ => ts + amt, + }; + stack.push(Value::Int(result)); BuiltinResult::Handled + } + "time_diff" => { + let unit = match stack.pop().unwrap_or(Value::Str("ms".to_string())) { Value::Str(s) => s, _ => "ms".to_string() }; + let ts2 = match stack.pop().unwrap_or(Value::Int(0)) { Value::Int(i) => i, _ => 0 }; + let ts1 = match stack.pop().unwrap_or(Value::Int(0)) { Value::Int(i) => i, _ => 0 }; + let diff_ms = ts2 - ts1; + let result = match unit.as_str() { + "ms" => diff_ms, + "sec" | "second" | "seconds" => diff_ms / 1_000, + "min" | "minute" | "minutes" => diff_ms / 60_000, + "hour" | "hours" => diff_ms / 3_600_000, + "day" | "days" => diff_ms / 86_400_000, + "week" | "weeks" => diff_ms / 604_800_000, + _ => diff_ms, + }; + stack.push(Value::Int(result)); BuiltinResult::Handled + } + "time_start_of" => { + let unit = match stack.pop().unwrap_or(Value::Str("day".to_string())) { Value::Str(s) => s, _ => "day".to_string() }; + let ts = match stack.pop().unwrap_or(Value::Int(0)) { Value::Int(i) => i, _ => 0 }; + let (year, month, day, hour, _min, _sec, _ms) = epoch_ms_to_parts(ts); + let result = match unit.as_str() { + "minute" => { let (y,mo,d,h,mn,_,_) = epoch_ms_to_parts(ts); parts_to_epoch_ms(y,mo,d,h,mn,0,0) } + "hour" => parts_to_epoch_ms(year, month, day, hour, 0, 0, 0), + "day" => parts_to_epoch_ms(year, month, day, 0, 0, 0, 0), + "week" => { + let days_since_epoch = ts / 86_400_000; + let weekday = ((days_since_epoch % 7 + 4) % 7 + 7) % 7; + let monday_offset = if weekday == 0 { 6 } else { weekday - 1 }; + (days_since_epoch - monday_offset) * 86_400_000 + } + "month" => parts_to_epoch_ms(year, month, 1, 0, 0, 0, 0), + "year" => parts_to_epoch_ms(year, 1, 1, 0, 0, 0, 0), + _ => ts, + }; + stack.push(Value::Int(result)); BuiltinResult::Handled + } + "time_tz_offset" => { + let tz = match stack.pop().unwrap_or(Value::Str("UTC".to_string())) { Value::Str(s) => s, _ => "UTC".to_string() }; + let offset: i64 = match tz.as_str() { + "UTC" | "GMT" => 0, + "EST" | "America/New_York" => -300, + "CST" | "America/Chicago" => -360, + "MST" | "America/Denver" => -420, + "PST" | "America/Los_Angeles" => -480, + "EDT" => -240, "CDT" => -300, "MDT" => -360, "PDT" => -420, + "CET" => 60, "CEST" => 120, "EET" => 120, "EEST" => 180, + "IST" => 330, "JST" => 540, "KST" => 540, "CST_CHINA" => 480, + "AEST" => 600, "AEDT" => 660, "NZST" => 720, "BRT" => -180, "ART" => -180, + _ => 0, + }; + stack.push(Value::Int(offset)); BuiltinResult::Handled + } + "time_to_tz" => { + let tz = match stack.pop().unwrap_or(Value::Str("UTC".to_string())) { Value::Str(s) => s, _ => "UTC".to_string() }; + let ts = match stack.pop().unwrap_or(Value::Int(0)) { Value::Int(i) => i, _ => 0 }; + let offset_minutes: i64 = match tz.as_str() { + "UTC" | "GMT" => 0, + "EST" | "America/New_York" => -300, + "CST" | "America/Chicago" => -360, + "MST" | "America/Denver" => -420, + "PST" | "America/Los_Angeles" => -480, + "EDT" => -240, "CDT" => -300, "MDT" => -360, "PDT" => -420, + "CET" => 60, "CEST" => 120, "IST" => 330, "JST" => 540, + "CST_CHINA" => 480, "AEST" => 600, "BRT" => -180, + _ => 0, + }; + let local_ms = ts + offset_minutes * 60_000; + let (year, month, day, hour, min, sec, ms) = epoch_ms_to_parts(local_ms); + let days_since_epoch = local_ms / 86_400_000; + let weekday_num = ((days_since_epoch % 7 + 4) % 7 + 7) % 7; + let weekday_names = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]; + let weekday_name = weekday_names[weekday_num as usize].to_string(); + let pairs = vec![ + ("year".to_string(), Value::Int(year as i64)), + ("month".to_string(), Value::Int(month as i64)), + ("day".to_string(), Value::Int(day as i64)), + ("hour".to_string(), Value::Int(hour as i64)), + ("minute".to_string(), Value::Int(min as i64)), + ("second".to_string(), Value::Int(sec as i64)), + ("ms".to_string(), Value::Int(ms as i64)), + ("weekday_num".to_string(), Value::Int(weekday_num)), + ("weekday_name".to_string(), Value::Str(weekday_name)), + ("offset_minutes".to_string(), Value::Int(offset_minutes)), + ("tz".to_string(), Value::Str(tz)), + ]; + stack.push(Value::Map(pairs)); BuiltinResult::Handled + } + + // ── Observer system ─────────────────────────────────────────────────── + "observe" => { + let fn_name = match stack.pop().unwrap_or(Value::Str(String::new())) { Value::Str(s) => s, _ => String::new() }; + let key = match stack.pop().unwrap_or(Value::Str(String::new())) { Value::Str(s) => s, _ => String::new() }; + let handle = OBSERVER_ID_COUNTER.with(|c| { let id = c.get(); c.set(id + 1); id }); + OBSERVER_REGISTRY.with(|r| { + r.borrow_mut().entry(key).or_default().push((handle, fn_name)); + }); + stack.push(Value::Int(handle)); BuiltinResult::Handled + } + "unobserve" => { + let handle = match stack.pop().unwrap_or(Value::Int(-1)) { Value::Int(i) => i, _ => -1 }; + OBSERVER_REGISTRY.with(|r| { + let mut registry = r.borrow_mut(); + for observers in registry.values_mut() { + observers.retain(|(id, _)| *id != handle); + } + }); + stack.push(Value::Nil); BuiltinResult::Handled + } + _ => BuiltinResult::NotBuiltin, } } @@ -5435,6 +6063,75 @@ fn is_leap(year: u64) -> bool { (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) } +fn is_leap_i32(year: i32) -> bool { + (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) +} + +fn days_in_month(year: i32, month: u32) -> u32 { + match month { + 1|3|5|7|8|10|12 => 31, + 4|6|9|11 => 30, + 2 => if is_leap_i32(year) { 29 } else { 28 }, + _ => 30, + } +} + +fn epoch_ms_to_parts(ms: i64) -> (i32, u32, u32, u32, u32, u32, u32) { + let secs = ms / 1000; + let ms_frac = (ms % 1000).unsigned_abs() as u32; + let mut days = secs / 86400; + let mut time_of_day = secs % 86400; + if time_of_day < 0 { time_of_day += 86400; days -= 1; } + let secs_in_day = time_of_day as u32; + let hour = secs_in_day / 3600; + let minute = (secs_in_day % 3600) / 60; + let second = secs_in_day % 60; + let mut year = 1970i32; + let mut remaining = days; + loop { + let days_in_year = if is_leap_i32(year) { 366 } else { 365 }; + if remaining < days_in_year { break; } + remaining -= days_in_year; + year += 1; + } + let leap = is_leap_i32(year); + let month_days = [31u32, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + let mut month = 0u32; + let mut day_of_month = remaining as u32; + for (m, &d) in month_days.iter().enumerate() { + if day_of_month < d { month = m as u32 + 1; break; } + day_of_month -= d; + } + (year, month, day_of_month + 1, hour, minute, second, ms_frac) +} + +fn parts_to_epoch_ms(year: i32, month: u32, day: u32, hour: u32, min: u32, sec: u32, ms: u32) -> i64 { + let mut days: i64 = 0; + if year >= 1970 { + for y in 1970..year { + days += if is_leap_i32(y) { 366 } else { 365 }; + } + } else { + for y in year..1970 { + days -= if is_leap_i32(y) { 366 } else { 365 }; + } + } + let leap = is_leap_i32(year); + let month_days = [31u32, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + for m in 0..(month as usize - 1) { + days += month_days[m] as i64; + } + days += (day as i64) - 1; + let secs = days * 86400 + (hour as i64) * 3600 + (min as i64) * 60 + (sec as i64); + secs * 1000 + ms as i64 +} + +thread_local! { + static OBSERVER_REGISTRY: std::cell::RefCell>> = + std::cell::RefCell::new(std::collections::HashMap::new()); + static OBSERVER_ID_COUNTER: std::cell::Cell = std::cell::Cell::new(1); +} + /// Compare two runtime values for ordering (used by Lt/Gt/LtEq/GtEq). fn cmp_values(a: &el_compiler::Value, b: &el_compiler::Value) -> std::cmp::Ordering { use el_compiler::Value; @@ -5476,6 +6173,25 @@ fn is_builtin(name: &str) -> bool { | "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" + // Math additions + | "math_sin" | "math_cos" | "math_tan" | "math_asin" | "math_acos" | "math_atan2" + | "math_exp" | "math_ln" | "math_log2" | "math_log10" | "math_mod" | "math_pi" | "math_e" + | "int_to_float" | "float_to_int" | "is_nil" | "unwrap_or" + | "decimal_add" | "decimal_sub" | "decimal_mul" | "decimal_div" | "decimal_round" + // String additions + | "str_char_at" | "str_char_code" | "str_from_char_code" + | "str_pad_left" | "str_pad_right" | "format_float" | "str_format" + // List additions + | "list_push" | "list_pop" | "list_pop_front" | "list_peek_last" | "list_range" + | "list_new" | "list_empty" + | "stack_push" | "stack_pop" | "stack_peek" | "stack_new" + | "queue_enqueue" | "queue_dequeue" | "queue_peek" | "queue_new" + | "list_map" | "list_filter" | "list_reduce" | "fn_ref" + // Time + | "time_now_utc" | "time_to_parts" | "time_from_parts" | "time_format" | "time_parse" + | "time_add" | "time_diff" | "time_start_of" | "time_tz_offset" | "time_to_tz" + // Observer + | "observe" | "unobserve" ) } diff --git a/engrams/el-arch/src/checker.rs b/engrams/el-arch/src/checker.rs index 4c9fb15..2e0532d 100644 --- a/engrams/el-arch/src/checker.rs +++ b/engrams/el-arch/src/checker.rs @@ -265,6 +265,7 @@ fn extract_calls_from_expr(expr: &Expr, in_loop: bool, out: &mut Vec) extract_calls_from_stmt(s, in_loop, out); } } + Expr::UnaryBitNot(inner) => extract_calls_from_expr(inner, in_loop, out), } } @@ -386,6 +387,7 @@ fn extract_activate_types_expr(expr: &Expr, in_loop: bool, out: &mut Vec extract_activate_types_stmt(s, in_loop, out); } } + Expr::UnaryBitNot(inner) => extract_activate_types_expr(inner, in_loop, out), } } @@ -449,6 +451,7 @@ fn has_sealed_in_loop_expr(expr: &Expr, in_loop: bool) -> bool { Expr::Reason { .. } => false, Expr::Parallel { entries } => entries.iter().any(|(_, e)| has_sealed_in_loop_expr(e, in_loop)), Expr::Trace { body, .. } => body.iter().any(|s| has_sealed_in_loop_stmt(s, in_loop)), + Expr::UnaryBitNot(inner) => has_sealed_in_loop_expr(inner, in_loop), } } diff --git a/engrams/el-compiler/src/bytecode.rs b/engrams/el-compiler/src/bytecode.rs index ba25486..8c2b411 100644 --- a/engrams/el-compiler/src/bytecode.rs +++ b/engrams/el-compiler/src/bytecode.rs @@ -69,6 +69,13 @@ pub enum Bytecode { Sub, Mul, Div, + Mod, + BitAnd, + BitOr, + BitXor, + BitNot, + Shl, + Shr, // ── Comparison ──────────────────────────────────────────────────────────── Eq, @@ -155,6 +162,13 @@ impl std::fmt::Display for Bytecode { Bytecode::Sub => write!(f, "SUB"), Bytecode::Mul => write!(f, "MUL"), Bytecode::Div => write!(f, "DIV"), + Bytecode::Mod => write!(f, "MOD"), + Bytecode::BitAnd => write!(f, "BITAND"), + Bytecode::BitOr => write!(f, "BITOR"), + Bytecode::BitXor => write!(f, "BITXOR"), + Bytecode::BitNot => write!(f, "BITNOT"), + Bytecode::Shl => write!(f, "SHL"), + Bytecode::Shr => write!(f, "SHR"), Bytecode::Eq => write!(f, "EQ"), Bytecode::NotEq => write!(f, "NEQ"), Bytecode::Lt => write!(f, "LT"), diff --git a/engrams/el-compiler/src/codegen.rs b/engrams/el-compiler/src/codegen.rs index 458d945..de44c83 100644 --- a/engrams/el-compiler/src/codegen.rs +++ b/engrams/el-compiler/src/codegen.rs @@ -273,6 +273,12 @@ impl Codegen { BinOp::GtEq => Bytecode::GtEq, BinOp::And => Bytecode::And, BinOp::Or => Bytecode::Or, + BinOp::Mod => Bytecode::Mod, + BinOp::BitAnd => Bytecode::BitAnd, + BinOp::BitOr => Bytecode::BitOr, + BinOp::BitXor => Bytecode::BitXor, + BinOp::Shl => Bytecode::Shl, + BinOp::Shr => Bytecode::Shr, }; self.emit(instr); } @@ -280,6 +286,10 @@ impl Codegen { self.gen_expr(inner)?; self.emit(Bytecode::Not); } + Expr::UnaryBitNot(inner) => { + self.gen_expr(inner)?; + self.emit(Bytecode::BitNot); + } Expr::Call { func, args } => { // Push arguments left-to-right for arg in args { @@ -326,8 +336,12 @@ impl Codegen { let after_else = self.current_idx(); self.patch_jump(jump_end, after_else); } else { - let after_then = self.current_idx(); - self.patch_jump(jump_false, after_then); + // No else branch: if-without-else is a statement, not a value + // expression. Discard the then-block's pushed value so both + // paths leave the stack at the same height (net zero change). + self.emit(Bytecode::Pop); + let after_pop = self.current_idx(); + self.patch_jump(jump_false, after_pop); // false path skips Pop too } } Expr::Match { subject, arms } => { @@ -451,6 +465,15 @@ impl Codegen { fields: field_names, }); } + Expr::MapLiteral(pairs) => { + // Push each key-value pair, then collect with BuildMap. + let n = pairs.len() as u32; + for (key_expr, val_expr) in pairs { + self.gen_expr(key_expr)?; + self.gen_expr(val_expr)?; + } + self.emit(Bytecode::BuildMap(n)); + } Expr::With { base, updates } => { // Generate base struct clone then apply updates self.gen_expr(base)?; diff --git a/engrams/el-fmt/src/formatter.rs b/engrams/el-fmt/src/formatter.rs index 5c561b8..ed07454 100644 --- a/engrams/el-fmt/src/formatter.rs +++ b/engrams/el-fmt/src/formatter.rs @@ -248,6 +248,11 @@ impl Formatter { self.fmt_expr(out, inner, depth); } + Expr::UnaryBitNot(inner) => { + out.push('~'); + self.fmt_expr(out, inner, depth); + } + Expr::Try(inner) => { self.fmt_expr(out, inner, depth); out.push('?'); @@ -427,6 +432,12 @@ impl Formatter { BinOp::GtEq => ">=", BinOp::And => "&&", BinOp::Or => "||", + BinOp::Mod => "%", + BinOp::BitAnd => "&", + BinOp::BitOr => "|", + BinOp::BitXor => "^", + BinOp::Shl => "<<", + BinOp::Shr => ">>", } } diff --git a/engrams/el-lexer/src/lexer.rs b/engrams/el-lexer/src/lexer.rs index 6aacbb3..1d3af12 100644 --- a/engrams/el-lexer/src/lexer.rs +++ b/engrams/el-lexer/src/lexer.rs @@ -139,6 +139,9 @@ impl<'src> Lexer<'src> { // ── Operators that may be multi-char ──────────────────────────── '+' => Token::Plus, '*' => Token::Star, + '%' => Token::Percent, + '^' => Token::Caret, + '~' => Token::Tilde, '-' => { if self.eat('>') { Token::Arrow @@ -164,14 +167,18 @@ impl<'src> Lexer<'src> { } } '<' => { - if self.eat('=') { + if self.eat('<') { + Token::Shl + } else if self.eat('=') { Token::LtEq } else { Token::Lt } } '>' => { - if self.eat('=') { + if self.eat('>') { + Token::Shr + } else if self.eat('=') { Token::GtEq } else { Token::Gt @@ -181,10 +188,7 @@ impl<'src> Lexer<'src> { if self.eat('&') { Token::And } else { - return Err(LexError::new( - LexErrorKind::UnexpectedChar('&'), - self.span_from(start), - )); + Token::Ampersand } } '|' => { diff --git a/engrams/el-lexer/src/token.rs b/engrams/el-lexer/src/token.rs index 40bdd2e..cfe4d74 100644 --- a/engrams/el-lexer/src/token.rs +++ b/engrams/el-lexer/src/token.rs @@ -194,6 +194,18 @@ pub enum Token { Pipe, /// `?` — used both for Optional types and the Try operator QuestionMark, + /// `%` — modulo + Percent, + /// `&` — bitwise AND + Ampersand, + /// `^` — bitwise XOR + Caret, + /// `~` — bitwise NOT + Tilde, + /// `<<` — left shift + Shl, + /// `>>` — right shift + Shr, // ── Special ─────────────────────────────────────────────────────────────── Eof, @@ -240,6 +252,12 @@ impl std::fmt::Display for Token { Token::At => write!(f, "@"), Token::Pipe => write!(f, "|"), Token::QuestionMark => write!(f, "?"), + Token::Percent => write!(f, "%"), + Token::Ampersand => write!(f, "&"), + Token::Caret => write!(f, "^"), + Token::Tilde => write!(f, "~"), + Token::Shl => write!(f, "<<"), + Token::Shr => write!(f, ">>"), Token::BoolLiteral(b) => write!(f, "{b}"), Token::IntLiteral(n) => write!(f, "{n}"), Token::FloatLiteral(n) => write!(f, "{n}"), diff --git a/engrams/el-parser/src/ast.rs b/engrams/el-parser/src/ast.rs index 3433e31..68da41e 100644 --- a/engrams/el-parser/src/ast.rs +++ b/engrams/el-parser/src/ast.rs @@ -84,6 +84,21 @@ pub enum BinOp { Add, Sub, Mul, Div, Eq, NotEq, Lt, Gt, LtEq, GtEq, And, Or, + Mod, // % + BitAnd, // & + BitOr, // | (single pipe) + BitXor, // ^ + Shl, // << + Shr, // >> +} + +// ── Unary operators ─────────────────────────────────────────────────────────── + +#[derive(Debug, Clone, PartialEq)] +pub enum UnaryOp { + Neg, // - (unary minus) + Not, // ! (logical not) + BitNot, // ~ } // ── Expressions ─────────────────────────────────────────────────────────────── @@ -94,6 +109,7 @@ pub enum Expr { Ident(String), BinOp { op: BinOp, left: Box, right: Box }, UnaryNot(Box), + UnaryBitNot(Box), Call { func: Box, args: Vec }, Block(Vec), Match { subject: Box, arms: Vec }, diff --git a/engrams/el-parser/src/lib.rs b/engrams/el-parser/src/lib.rs index 8e3789d..a12ee4c 100644 --- a/engrams/el-parser/src/lib.rs +++ b/engrams/el-parser/src/lib.rs @@ -11,7 +11,7 @@ mod error; mod parser; pub use ast::{ - BinOp, Decorator, Expr, Field, Literal, MatchArm, Param, Pattern, Program, ProtocolMethod, + BinOp, UnaryOp, Decorator, Expr, Field, Literal, MatchArm, Param, Pattern, Program, ProtocolMethod, SeedStmt, Stmt, TestTarget, TypeExpr, Variant, }; pub use error::{ParseError, ParseErrorKind}; diff --git a/engrams/el-parser/src/parser.rs b/engrams/el-parser/src/parser.rs index eb39735..089215b 100644 --- a/engrams/el-parser/src/parser.rs +++ b/engrams/el-parser/src/parser.rs @@ -717,6 +717,48 @@ impl Parser { self.parse_pipe_expr() } + fn parse_bitwise_or_expr(&mut self) -> Result { + let mut left = self.parse_bitwise_xor_expr()?; + while self.eat(&Token::Pipe) { + let right = self.parse_bitwise_xor_expr()?; + left = Expr::BinOp { op: BinOp::BitOr, left: Box::new(left), right: Box::new(right) }; + } + Ok(left) + } + + fn parse_bitwise_xor_expr(&mut self) -> Result { + let mut left = self.parse_bitwise_and_expr()?; + while self.eat(&Token::Caret) { + let right = self.parse_bitwise_and_expr()?; + left = Expr::BinOp { op: BinOp::BitXor, left: Box::new(left), right: Box::new(right) }; + } + Ok(left) + } + + fn parse_bitwise_and_expr(&mut self) -> Result { + let mut left = self.parse_shift_expr()?; + while self.eat(&Token::Ampersand) { + let right = self.parse_shift_expr()?; + left = Expr::BinOp { op: BinOp::BitAnd, left: Box::new(left), right: Box::new(right) }; + } + Ok(left) + } + + fn parse_shift_expr(&mut self) -> Result { + let mut left = self.parse_additive()?; + loop { + let op = match self.peek() { + Token::Shl => BinOp::Shl, + Token::Shr => BinOp::Shr, + _ => break, + }; + self.advance(); + let right = self.parse_additive()?; + left = Expr::BinOp { op, left: Box::new(left), right: Box::new(right) }; + } + Ok(left) + } + /// pipe_expr = or_expr (|> ident)* /// `a |> f` desugars to `Call(f, [a])` fn parse_pipe_expr(&mut self) -> Result { @@ -766,7 +808,7 @@ impl Parser { } fn parse_comparison(&mut self) -> Result { - let mut left = self.parse_additive()?; + let mut left = self.parse_bitwise_or_expr()?; loop { let op = match self.peek() { Token::Lt => BinOp::Lt, @@ -803,6 +845,7 @@ impl Parser { let op = match self.peek() { Token::Star => BinOp::Mul, Token::Slash => BinOp::Div, + Token::Percent => BinOp::Mod, _ => break, }; self.advance(); @@ -817,6 +860,10 @@ impl Parser { let inner = self.parse_unary()?; return Ok(Expr::UnaryNot(Box::new(inner))); } + if self.eat(&Token::Tilde) { + let inner = self.parse_unary()?; + return Ok(Expr::UnaryBitNot(Box::new(inner))); + } self.parse_postfix() } diff --git a/engrams/el-stdlib/src/array.rs b/engrams/el-stdlib/src/array.rs index f1073a0..fc8f2bb 100644 --- a/engrams/el-stdlib/src/array.rs +++ b/engrams/el-stdlib/src/array.rs @@ -50,6 +50,28 @@ pub fn register(env: &mut TypeEnv) { env.register_fn("array_last", fn_type(vec![arr_int.clone()], Type::Optional(Box::new(Type::Int)))); // array_contains([String], String) -> Bool env.register_fn("array_contains", fn_type(vec![arr_str, Type::String], Type::Bool)); + // New list/stack/queue builtins + env.register_fn("list_push", fn_type(vec![arr_unk.clone(), Type::Unknown], arr_unk.clone())); + env.register_fn("list_pop", fn_type(vec![arr_unk.clone()], arr_unk.clone())); + env.register_fn("list_pop_front", fn_type(vec![arr_unk.clone()], arr_unk.clone())); + env.register_fn("list_peek_last", fn_type(vec![arr_unk.clone()], Type::Unknown)); + env.register_fn("list_range", fn_type(vec![Type::Int, Type::Int], arr_unk.clone())); + env.register_fn("list_new", fn_type(vec![], arr_unk.clone())); + env.register_fn("list_empty", fn_type(vec![arr_unk.clone()], Type::Bool)); + env.register_fn("list_map", fn_type(vec![arr_unk.clone(), Type::String], arr_unk.clone())); + env.register_fn("list_filter", fn_type(vec![arr_unk.clone(), Type::String], arr_unk.clone())); + env.register_fn("list_reduce", fn_type(vec![arr_unk.clone(), Type::Unknown, Type::String], Type::Unknown)); + env.register_fn("fn_ref", fn_type(vec![Type::String], Type::String)); + // Stack + env.register_fn("stack_push", fn_type(vec![arr_unk.clone(), Type::Unknown], arr_unk.clone())); + env.register_fn("stack_pop", fn_type(vec![arr_unk.clone()], arr_unk.clone())); + env.register_fn("stack_peek", fn_type(vec![arr_unk.clone()], Type::Unknown)); + env.register_fn("stack_new", fn_type(vec![], arr_unk.clone())); + // Queue + env.register_fn("queue_enqueue", fn_type(vec![arr_unk.clone(), Type::Unknown], arr_unk.clone())); + env.register_fn("queue_dequeue", fn_type(vec![arr_unk.clone()], arr_unk.clone())); + env.register_fn("queue_peek", fn_type(vec![arr_unk.clone()], Type::Unknown)); + env.register_fn("queue_new", fn_type(vec![], arr_unk)); } #[cfg(test)] diff --git a/engrams/el-stdlib/src/math.rs b/engrams/el-stdlib/src/math.rs index c0d54cb..faeb472 100644 --- a/engrams/el-stdlib/src/math.rs +++ b/engrams/el-stdlib/src/math.rs @@ -16,6 +16,45 @@ pub fn register(env: &mut TypeEnv) { env.register_fn("math_abs_int", fn_type(vec![Type::Int], Type::Int)); env.register_fn("math_max_int", fn_type(vec![Type::Int, Type::Int], Type::Int)); env.register_fn("math_min_int", fn_type(vec![Type::Int, Type::Int], Type::Int)); + // Trig + env.register_fn("math_sin", fn_type(vec![Type::Float], Type::Float)); + env.register_fn("math_cos", fn_type(vec![Type::Float], Type::Float)); + env.register_fn("math_tan", fn_type(vec![Type::Float], Type::Float)); + env.register_fn("math_asin", fn_type(vec![Type::Float], Type::Float)); + env.register_fn("math_acos", fn_type(vec![Type::Float], Type::Float)); + env.register_fn("math_atan2", fn_type(vec![Type::Float, Type::Float], Type::Float)); + env.register_fn("math_exp", fn_type(vec![Type::Float], Type::Float)); + env.register_fn("math_ln", fn_type(vec![Type::Float], Type::Float)); + env.register_fn("math_log2", fn_type(vec![Type::Float], Type::Float)); + env.register_fn("math_log10", fn_type(vec![Type::Float], Type::Float)); + env.register_fn("math_mod", fn_type(vec![Type::Float, Type::Float], Type::Float)); + env.register_fn("math_pi", fn_type(vec![], Type::Float)); + env.register_fn("math_e", fn_type(vec![], Type::Float)); + // Conversion + env.register_fn("int_to_float", fn_type(vec![Type::Int], Type::Float)); + env.register_fn("float_to_int", fn_type(vec![Type::Float], Type::Int)); + env.register_fn("is_nil", fn_type(vec![Type::Unknown], Type::Bool)); + env.register_fn("unwrap_or", fn_type(vec![Type::Unknown, Type::Unknown], Type::Unknown)); + // Decimal + env.register_fn("decimal_add", fn_type(vec![Type::Float, Type::Float], Type::Float)); + env.register_fn("decimal_sub", fn_type(vec![Type::Float, Type::Float], Type::Float)); + env.register_fn("decimal_mul", fn_type(vec![Type::Float, Type::Float], Type::Float)); + env.register_fn("decimal_div", fn_type(vec![Type::Float, Type::Float], Type::Float)); + env.register_fn("decimal_round", fn_type(vec![Type::Float, Type::Int], Type::Float)); + // Time + env.register_fn("time_now_utc", fn_type(vec![], Type::Int)); + env.register_fn("time_to_parts", fn_type(vec![Type::Int], Type::Unknown)); + env.register_fn("time_from_parts", fn_type(vec![Type::Unknown], Type::Int)); + env.register_fn("time_format", fn_type(vec![Type::Int, Type::String], Type::String)); + env.register_fn("time_parse", fn_type(vec![Type::String], Type::Int)); + env.register_fn("time_add", fn_type(vec![Type::Int, Type::Int, Type::String], Type::Int)); + env.register_fn("time_diff", fn_type(vec![Type::Int, Type::Int, Type::String], Type::Int)); + env.register_fn("time_start_of", fn_type(vec![Type::Int, Type::String], Type::Int)); + env.register_fn("time_tz_offset", fn_type(vec![Type::String], Type::Int)); + env.register_fn("time_to_tz", fn_type(vec![Type::Int, Type::String], Type::Unknown)); + // Observer + env.register_fn("observe", fn_type(vec![Type::String, Type::String], Type::Int)); + env.register_fn("unobserve", fn_type(vec![Type::Int], Type::Unknown)); } #[cfg(test)] diff --git a/engrams/el-stdlib/src/string.rs b/engrams/el-stdlib/src/string.rs index 9b2686b..2d23061 100644 --- a/engrams/el-stdlib/src/string.rs +++ b/engrams/el-stdlib/src/string.rs @@ -27,6 +27,14 @@ pub fn register(env: &mut TypeEnv) { env.register_fn("string_from_float", fn_type(vec![Type::Float], Type::String)); env.register_fn("string_is_empty", fn_type(vec![Type::String], Type::Bool)); env.register_fn("string_concat", fn_type(vec![Type::String, Type::String], Type::String)); + // New string builtins + env.register_fn("str_char_at", fn_type(vec![Type::String, Type::Int], Type::String)); + env.register_fn("str_char_code", fn_type(vec![Type::String, Type::Int], Type::Int)); + env.register_fn("str_from_char_code", fn_type(vec![Type::Int], Type::String)); + env.register_fn("str_pad_left", fn_type(vec![Type::String, Type::Int, Type::String], Type::String)); + env.register_fn("str_pad_right", fn_type(vec![Type::String, Type::Int, Type::String], Type::String)); + env.register_fn("format_float", fn_type(vec![Type::Float, Type::Int], Type::String)); + env.register_fn("str_format", fn_type(vec![Type::String, Type::Unknown], Type::String)); } #[cfg(test)] diff --git a/engrams/el-test/src/eval.rs b/engrams/el-test/src/eval.rs index bd46360..74e42a6 100644 --- a/engrams/el-test/src/eval.rs +++ b/engrams/el-test/src/eval.rs @@ -354,6 +354,30 @@ impl<'g> Evaluator<'g> { BinOp::GtEq => self.compare_ord(lv, rv, |o| o != std::cmp::Ordering::Less), BinOp::And => Ok(EvalValue::Bool(lv.as_bool() && rv.as_bool())), BinOp::Or => Ok(EvalValue::Bool(lv.as_bool() || rv.as_bool())), + BinOp::Mod => match (lv, rv) { + (EvalValue::Int(a), EvalValue::Int(b)) if b != 0 => Ok(EvalValue::Int(a % b)), + _ => Ok(EvalValue::Int(0)), + }, + BinOp::BitAnd => match (lv, rv) { + (EvalValue::Int(a), EvalValue::Int(b)) => Ok(EvalValue::Int(a & b)), + _ => Ok(EvalValue::Int(0)), + }, + BinOp::BitOr => match (lv, rv) { + (EvalValue::Int(a), EvalValue::Int(b)) => Ok(EvalValue::Int(a | b)), + _ => Ok(EvalValue::Int(0)), + }, + BinOp::BitXor => match (lv, rv) { + (EvalValue::Int(a), EvalValue::Int(b)) => Ok(EvalValue::Int(a ^ b)), + _ => Ok(EvalValue::Int(0)), + }, + BinOp::Shl => match (lv, rv) { + (EvalValue::Int(a), EvalValue::Int(b)) => Ok(EvalValue::Int(a << (b as u32))), + _ => Ok(EvalValue::Int(0)), + }, + BinOp::Shr => match (lv, rv) { + (EvalValue::Int(a), EvalValue::Int(b)) => Ok(EvalValue::Int(a >> (b as u32))), + _ => Ok(EvalValue::Int(0)), + }, } } @@ -414,6 +438,8 @@ pub fn expr_to_text(expr: &Expr) -> String { BinOp::Eq => "==", BinOp::NotEq => "!=", BinOp::Lt => "<", BinOp::Gt => ">", BinOp::LtEq => "<=", BinOp::GtEq => ">=", BinOp::And => "&&", BinOp::Or => "||", + BinOp::Mod => "%", BinOp::BitAnd => "&", BinOp::BitOr => "|", + BinOp::BitXor => "^", BinOp::Shl => "<<", BinOp::Shr => ">>", }; format!("{} {op_str} {}", expr_to_text(left), expr_to_text(right)) } diff --git a/engrams/el-types/src/checker.rs b/engrams/el-types/src/checker.rs index d0c58e1..c04ad52 100644 --- a/engrams/el-types/src/checker.rs +++ b/engrams/el-types/src/checker.rs @@ -493,6 +493,10 @@ impl TypeChecker { Expr::Reason { .. } => Type::String, Expr::Parallel { .. } => Type::Unknown, Expr::Trace { .. } => Type::Unknown, + Expr::UnaryBitNot(inner) => { + self.infer_expr(inner); + Type::Int + } } } @@ -549,6 +553,8 @@ impl TypeChecker { } Type::Bool } + BinOp::Mod | BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor + | BinOp::Shl | BinOp::Shr => Type::Int, } }