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
+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,
}
}