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"
)
}
+3
View File
@@ -265,6 +265,7 @@ fn extract_calls_from_expr(expr: &Expr, in_loop: bool, out: &mut Vec<CallInfo>)
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<String>
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),
}
}
+14
View File
@@ -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"),
+25 -2
View File
@@ -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)?;
+11
View File
@@ -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 => ">>",
}
}
+10 -6
View File
@@ -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
}
}
'|' => {
+18
View File
@@ -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}"),
+16
View File
@@ -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<Expr>, right: Box<Expr> },
UnaryNot(Box<Expr>),
UnaryBitNot(Box<Expr>),
Call { func: Box<Expr>, args: Vec<Expr> },
Block(Vec<Stmt>),
Match { subject: Box<Expr>, arms: Vec<MatchArm> },
+1 -1
View File
@@ -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};
+48 -1
View File
@@ -717,6 +717,48 @@ impl Parser {
self.parse_pipe_expr()
}
fn parse_bitwise_or_expr(&mut self) -> Result<Expr, ParseError> {
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<Expr, ParseError> {
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<Expr, ParseError> {
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<Expr, ParseError> {
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<Expr, ParseError> {
@@ -766,7 +808,7 @@ impl Parser {
}
fn parse_comparison(&mut self) -> Result<Expr, ParseError> {
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()
}
+22
View File
@@ -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)]
+39
View File
@@ -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)]
+8
View File
@@ -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)]
+26
View File
@@ -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))
}
+6
View File
@@ -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,
}
}