use std::collections::HashMap;
use std::fmt;
use crate::ast::{BinaryOp, ExpressionKind, ExpressionNode, LiteralNode, UnaryOp};
#[derive(Debug, Clone, PartialEq)]
pub enum ConstValue {
Int(i64),
Float(f64),
Bool(bool),
Str(String),
Char(char),
}
impl fmt::Display for ConstValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConstValue::Int(v) => write!(f, "{}", v),
ConstValue::Float(v) => write!(f, "{}", v),
ConstValue::Bool(v) => write!(f, "{}", v),
ConstValue::Str(v) => write!(f, "{}", v),
ConstValue::Char(v) => write!(f, "{}", v),
}
}
}
pub fn fold(expr: &ExpressionNode) -> Option<ConstValue> {
fold_with_env(expr, &HashMap::new())
}
pub fn fold_with_env(
expr: &ExpressionNode,
env: &HashMap<String, ConstValue>,
) -> Option<ConstValue> {
match &expr.kind {
ExpressionKind::Literal(lit) => Some(match lit {
LiteralNode::Integer(v) => ConstValue::Int(*v),
LiteralNode::Float(v) => ConstValue::Float(v.into_inner()),
LiteralNode::Boolean(v) => ConstValue::Bool(*v),
LiteralNode::String(v) => ConstValue::Str(v.clone()),
LiteralNode::Char(v) => ConstValue::Char(*v),
}),
ExpressionKind::Identifier(name) => env.get(name).cloned(),
ExpressionKind::Unary { op, expr, .. } => {
let value = fold_with_env(expr, env)?;
fold_unary(op, value)
}
ExpressionKind::Binary {
left, op, right, ..
} => {
let l = fold_with_env(left, env)?;
let r = fold_with_env(right, env)?;
fold_binary(l, op, r)
}
_ => None,
}
}
fn fold_unary(op: &UnaryOp, value: ConstValue) -> Option<ConstValue> {
match (op, value) {
(UnaryOp::Neg, ConstValue::Int(v)) => v.checked_neg().map(ConstValue::Int),
(UnaryOp::Neg, ConstValue::Float(v)) => Some(ConstValue::Float(-v)),
(UnaryOp::Not, ConstValue::Bool(v)) => Some(ConstValue::Bool(!v)),
_ => None,
}
}
fn fold_binary(left: ConstValue, op: &BinaryOp, right: ConstValue) -> Option<ConstValue> {
use ConstValue::*;
match (left, right) {
(Int(l), Int(r)) => fold_int_op(l, op, r),
(Float(l), Float(r)) => fold_float_op(l, op, r),
(Bool(l), Bool(r)) => match op {
BinaryOp::Equal => Some(Bool(l == r)),
BinaryOp::NotEqual => Some(Bool(l != r)),
BinaryOp::LogicalAnd => Some(Bool(l && r)),
BinaryOp::LogicalOr => Some(Bool(l || r)),
_ => None,
},
(Str(l), Str(r)) => match op {
BinaryOp::Add => Some(Str(l + &r)),
BinaryOp::Equal => Some(Bool(l == r)),
BinaryOp::NotEqual => Some(Bool(l != r)),
_ => None,
},
(Char(l), Char(r)) => match op {
BinaryOp::Equal => Some(Bool(l == r)),
BinaryOp::NotEqual => Some(Bool(l != r)),
_ => None,
},
_ => None,
}
}
fn fold_int_op(l: i64, op: &BinaryOp, r: i64) -> Option<ConstValue> {
use ConstValue::{Bool, Int};
match op {
BinaryOp::Add => l.checked_add(r).map(Int),
BinaryOp::Subtract => l.checked_sub(r).map(Int),
BinaryOp::Multiply => l.checked_mul(r).map(Int),
BinaryOp::Divide => l.checked_div(r).map(Int),
BinaryOp::Modulo => l.checked_rem(r).map(Int),
BinaryOp::Equal => Some(Bool(l == r)),
BinaryOp::NotEqual => Some(Bool(l != r)),
BinaryOp::Less => Some(Bool(l < r)),
BinaryOp::LessEqual => Some(Bool(l <= r)),
BinaryOp::Greater => Some(Bool(l > r)),
BinaryOp::GreaterEqual => Some(Bool(l >= r)),
_ => None,
}
}
fn fold_float_op(l: f64, op: &BinaryOp, r: f64) -> Option<ConstValue> {
use ConstValue::{Bool, Float};
let arith = |v: f64| {
if v.is_nan() { None } else { Some(Float(v)) }
};
match op {
BinaryOp::Add => arith(l + r),
BinaryOp::Subtract => arith(l - r),
BinaryOp::Multiply => arith(l * r),
BinaryOp::Divide => arith(l / r),
BinaryOp::Modulo => arith(l % r),
BinaryOp::Equal => Some(Bool(l == r)),
BinaryOp::NotEqual => Some(Bool(l != r)),
BinaryOp::Less => Some(Bool(l < r)),
BinaryOp::LessEqual => Some(Bool(l <= r)),
BinaryOp::Greater => Some(Bool(l > r)),
BinaryOp::GreaterEqual => Some(Bool(l >= r)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{BinaryOp, ExpressionKind, ExpressionNode, LiteralNode, UnaryOp};
use crate::lexer::Span;
fn lit(l: LiteralNode) -> ExpressionNode {
l.into()
}
fn int(v: i64) -> ExpressionNode {
lit(LiteralNode::Integer(v))
}
fn ident(name: &str) -> ExpressionNode {
name.into()
}
fn binary(left: ExpressionNode, op: BinaryOp, right: ExpressionNode) -> ExpressionNode {
ExpressionNode {
kind: ExpressionKind::Binary {
left: Box::new(left),
op,
op_span: Span::new(0, 0),
right: Box::new(right),
},
span: Span::new(0, 0),
}
}
fn unary(op: UnaryOp, expr: ExpressionNode) -> ExpressionNode {
ExpressionNode {
kind: ExpressionKind::Unary {
op,
op_span: Span::new(0, 0),
expr: Box::new(expr),
postfix: false,
},
span: Span::new(0, 0),
}
}
#[test]
fn folds_int_arithmetic_and_comparisons() {
let e = binary(int(2), BinaryOp::Add, int(3));
assert_eq!(fold(&e), Some(ConstValue::Int(5)));
let e = binary(int(1), BinaryOp::Greater, int(2));
assert_eq!(fold(&e), Some(ConstValue::Bool(false)));
}
#[test]
fn bails_on_int_overflow_and_zero_division() {
let e = binary(int(i64::MAX), BinaryOp::Add, int(1));
assert_eq!(fold(&e), None);
let e = binary(int(i64::MIN), BinaryOp::Divide, int(-1));
assert_eq!(fold(&e), None);
let e = binary(int(10), BinaryOp::Divide, int(0));
assert_eq!(fold(&e), None);
let e = binary(int(10), BinaryOp::Modulo, int(0));
assert_eq!(fold(&e), None);
}
#[test]
fn bails_on_exponent() {
let e = binary(int(2), BinaryOp::Exponent, int(3));
assert_eq!(fold(&e), None);
}
#[test]
fn folds_float_ops_and_bails_on_nan() {
use ordered_float::OrderedFloat;
let f = |v: f64| lit(LiteralNode::Float(OrderedFloat(v)));
let e = binary(f(1.0), BinaryOp::NotEqual, f(0.0));
assert_eq!(fold(&e), Some(ConstValue::Bool(true)));
let e = binary(f(0.0), BinaryOp::Divide, f(0.0));
assert_eq!(fold(&e), None);
let e = binary(f(1.0), BinaryOp::Divide, f(0.0));
assert_eq!(fold(&e), Some(ConstValue::Float(f64::INFINITY)));
}
#[test]
fn folds_logical_string_char_ops() {
let e = binary(lit(true.into()), BinaryOp::LogicalAnd, lit(false.into()));
assert_eq!(fold(&e), Some(ConstValue::Bool(false)));
let e = binary(lit("a".into()), BinaryOp::Add, lit("b".into()));
assert_eq!(fold(&e), Some(ConstValue::Str("ab".to_string())));
let e = binary(lit('x'.into()), BinaryOp::Equal, lit('x'.into()));
assert_eq!(fold(&e), Some(ConstValue::Bool(true)));
}
#[test]
fn folds_unary_and_env_identifiers() {
let e = unary(UnaryOp::Neg, int(5));
assert_eq!(fold(&e), Some(ConstValue::Int(-5)));
let e = unary(UnaryOp::Not, lit(true.into()));
assert_eq!(fold(&e), Some(ConstValue::Bool(false)));
let mut env = HashMap::new();
env.insert("b".to_string(), ConstValue::Int(0));
let e = binary(ident("b"), BinaryOp::NotEqual, int(0));
assert_eq!(fold_with_env(&e, &env), Some(ConstValue::Bool(false)));
let e = binary(ident("unknown"), BinaryOp::NotEqual, int(0));
assert_eq!(fold_with_env(&e, &env), None);
}
}