Add operators, math, time, string, collection, and HOF builtins to El

This commit is contained in:
Will Anderson
2026-04-29 03:58:16 -05:00
parent a42429012e
commit 8d4d9ed786
15 changed files with 972 additions and 19 deletions
+725 -9
View File
@@ -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<Value> = (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<i64> {
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::<i32>().ok()?;
let month = dp[1].parse::<u32>().ok()?;
let day = dp[2].parse::<u32>().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::<u32>().ok()).unwrap_or(0);
let m = tp_parts.get(1).and_then(|s| s.parse::<u32>().ok()).unwrap_or(0);
let sv = tp_parts.get(2).and_then(|s| s.parse::<f64>().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::collections::HashMap<String, Vec<(i64, String)>>> =
std::cell::RefCell::new(std::collections::HashMap::new());
static OBSERVER_ID_COUNTER: std::cell::Cell<i64> = 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"
)
}