use alloc::collections::BTreeMap;
use alloc::string::String;
use core::cmp::Ordering;
use crate::syntax::{SyntaxGraph, SyntaxNode};
use crate::NodeId;
#[derive(Clone, PartialEq, Debug)]
pub enum Folded {
Int(i64),
Float(f64),
Text(String),
Truth(bool),
}
#[must_use]
pub fn strip_quotes(value: &str) -> &str {
value.trim_matches(['"', '\''])
}
fn has_ambiguous_leading_zero(value: &str) -> bool {
let unsigned = value.strip_prefix('-').unwrap_or(value);
unsigned.len() > 1
&& unsigned.starts_with('0')
&& unsigned.chars().all(|character| character.is_ascii_digit())
}
fn parse_number(value: &str) -> Option<Folded> {
let normalized: String = value.chars().filter(|c| *c != '_').collect();
let trimmed = normalized.trim_end_matches(['l', 'L']);
if let Some(digits) = trimmed
.strip_prefix("0x")
.or_else(|| trimmed.strip_prefix("0X"))
{
return i64::from_str_radix(digits, 16).ok().map(Folded::Int);
}
if let Some(digits) = trimmed
.strip_prefix("0b")
.or_else(|| trimmed.strip_prefix("0B"))
{
return i64::from_str_radix(digits, 2).ok().map(Folded::Int);
}
if has_ambiguous_leading_zero(trimmed) {
return None;
}
if let Ok(int) = trimmed.parse::<i64>() {
return Some(Folded::Int(int));
}
trimmed
.trim_end_matches(['f', 'F', 'd', 'D'])
.parse::<f64>()
.ok()
.map(Folded::Float)
}
#[must_use]
pub fn fold_leaf(value: &str, value_type: &str) -> Option<Folded> {
match value_type {
"number" => parse_number(value),
"bool" | "boolean" => match value {
"true" => Some(Folded::Truth(true)),
"false" => Some(Folded::Truth(false)),
_ => None,
},
_ => Some(Folded::Text(String::from(strip_quotes(value)))),
}
}
fn fold_reserved(value: &str) -> Folded {
match value {
"true" => Folded::Truth(true),
"false" => Folded::Truth(false),
_ => Folded::Text(String::from(strip_quotes(value))),
}
}
fn compare(operator: &str, ordering: Ordering) -> Option<bool> {
match operator {
"==" => Some(ordering == Ordering::Equal),
"!=" => Some(ordering != Ordering::Equal),
"<" => Some(ordering == Ordering::Less),
"<=" => Some(ordering != Ordering::Greater),
">" => Some(ordering == Ordering::Greater),
">=" => Some(ordering != Ordering::Less),
_ => None,
}
}
fn fold_operation(operator: &str, left: &Folded, right: &Folded) -> Option<Folded> {
match (left, right) {
(Folded::Int(l), Folded::Int(r)) => match operator {
"+" => l.checked_add(*r).map(Folded::Int),
"-" => l.checked_sub(*r).map(Folded::Int),
"*" => l.checked_mul(*r).map(Folded::Int),
"/" => l.checked_div(*r).map(Folded::Int),
_ => compare(operator, l.cmp(r)).map(Folded::Truth),
},
(Folded::Float(l), Folded::Float(r)) => match operator {
"+" => Some(Folded::Float(l + r)),
"-" => Some(Folded::Float(l - r)),
"*" => Some(Folded::Float(l * r)),
"/" => Some(Folded::Float(l / r)),
_ => l
.partial_cmp(r)
.and_then(|ordering| compare(operator, ordering))
.map(Folded::Truth),
},
(Folded::Text(l), Folded::Text(r)) => match operator {
"==" => Some(Folded::Truth(l == r)),
"!=" => Some(Folded::Truth(l != r)),
_ => None,
},
(Folded::Truth(l), Folded::Truth(r)) => match operator {
"==" => Some(Folded::Truth(l == r)),
"!=" => Some(Folded::Truth(l != r)),
"&" | "&&" => Some(Folded::Truth(*l && *r)),
"|" | "||" => Some(Folded::Truth(*l || *r)),
_ => None,
},
_ => None,
}
}
#[must_use]
pub fn fold(
graph: &SyntaxGraph,
resolved: &BTreeMap<NodeId, Folded>,
n_id: NodeId,
) -> Option<Folded> {
match graph.nodes.get(&n_id)? {
SyntaxNode::BinaryOperation {
operator,
left_id,
right_id,
} => {
let left = fold(graph, resolved, (*left_id)?)?;
let right = fold(graph, resolved, (*right_id)?)?;
fold_operation(operator, &left, &right)
}
SyntaxNode::Literal { value, value_type } => fold_leaf(value, value_type),
SyntaxNode::ReservedWord { value } => Some(fold_reserved(value)),
SyntaxNode::SymbolLookup { .. } => resolved.get(&n_id).cloned(),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{fold, Folded};
use crate::syntax::{SyntaxGraph, SyntaxNode};
use crate::NodeId;
use alloc::borrow::ToOwned;
use alloc::collections::BTreeMap;
use alloc::string::String;
fn literal(value: &str, value_type: &str) -> SyntaxNode {
SyntaxNode::Literal {
value: value.to_owned(),
value_type: value_type.to_owned(),
}
}
fn binary_operation(operator: &str, left_id: NodeId, right_id: NodeId) -> SyntaxNode {
SyntaxNode::BinaryOperation {
operator: operator.to_owned(),
left_id: Some(left_id),
right_id: Some(right_id),
}
}
fn folded(operator: &str, left: (&str, &str), right: (&str, &str)) -> Option<Folded> {
let mut graph = SyntaxGraph::new();
graph.add_node(NodeId(1), binary_operation(operator, NodeId(2), NodeId(3)));
graph.add_node(NodeId(2), literal(left.0, left.1));
graph.add_node(NodeId(3), literal(right.0, right.1));
fold(&graph, &BTreeMap::new(), NodeId(1))
}
#[test]
fn folds_integer_comparisons() {
assert_eq!(
folded(">", ("5", "number"), ("3", "number")),
Some(Folded::Truth(true))
);
assert_eq!(
folded("<", ("5", "number"), ("3", "number")),
Some(Folded::Truth(false))
);
assert_eq!(
folded("<=", ("3", "number"), ("3", "number")),
Some(Folded::Truth(true))
);
assert_eq!(
folded(">=", ("2", "number"), ("3", "number")),
Some(Folded::Truth(false))
);
assert_eq!(
folded("==", ("3", "number"), ("3", "number")),
Some(Folded::Truth(true))
);
assert_eq!(
folded("!=", ("3", "number"), ("3", "number")),
Some(Folded::Truth(false))
);
}
#[test]
fn folds_numeric_literal_notations() {
assert_eq!(
folded("==", ("0x1A", "number"), ("26", "number")),
Some(Folded::Truth(true))
);
assert_eq!(
folded("==", ("1_000", "number"), ("1000", "number")),
Some(Folded::Truth(true))
);
assert_eq!(
folded("==", ("10L", "number"), ("10", "number")),
Some(Folded::Truth(true))
);
assert_eq!(
folded("==", ("2.5f", "number"), ("2.5", "number")),
Some(Folded::Truth(true))
);
assert_eq!(
folded("==", ("0b101", "number"), ("5", "number")),
Some(Folded::Truth(true))
);
assert_eq!(
folded("<", ("-5", "number"), ("3", "number")),
Some(Folded::Truth(true))
);
assert_eq!(folded("==", ("017", "number"), ("17", "number")), None);
}
#[test]
fn folds_float_comparisons_with_ieee_semantics() {
assert_eq!(
folded("<", ("2.5", "number"), ("2.6", "number")),
Some(Folded::Truth(true))
);
assert_eq!(
folded("==", ("-0.0", "number"), ("0.0", "number")),
Some(Folded::Truth(true))
);
}
#[test]
fn folds_string_equality_by_declared_type() {
assert_eq!(
folded("==", ("abc", "string"), ("abc", "string")),
Some(Folded::Truth(true))
);
assert_eq!(
folded("!=", ("abc", "string"), ("abd", "string")),
Some(Folded::Truth(true))
);
assert_eq!(
folded("==", ("a bc", "string"), ("a bc", "string")),
Some(Folded::Truth(true))
);
assert_eq!(
folded("==", ("5", "string"), ("05", "string")),
Some(Folded::Truth(false))
);
assert_eq!(folded("<", ("abc", "string"), ("abd", "string")), None);
}
#[test]
fn folds_boolean_operators() {
assert_eq!(
folded("&&", ("true", "boolean"), ("false", "boolean")),
Some(Folded::Truth(false))
);
assert_eq!(
folded("||", ("true", "boolean"), ("false", "boolean")),
Some(Folded::Truth(true))
);
assert_eq!(
folded("&", ("true", "boolean"), ("true", "boolean")),
Some(Folded::Truth(true))
);
assert_eq!(
folded("|", ("false", "boolean"), ("false", "boolean")),
Some(Folded::Truth(false))
);
assert_eq!(
folded("==", ("true", "boolean"), ("true", "boolean")),
Some(Folded::Truth(true))
);
assert_eq!(
folded("!=", ("false", "boolean"), ("false", "boolean")),
Some(Folded::Truth(false))
);
assert_eq!(folded("&&", ("yes", "boolean"), ("true", "boolean")), None);
}
#[test]
fn rejects_mixed_or_unsupported_operands() {
assert_eq!(folded("==", ("5", "string"), ("5", "number")), None);
assert_eq!(folded("==", ("1", "number"), ("true", "boolean")), None);
assert_eq!(folded("%", ("5", "number"), ("3", "number")), None);
}
#[test]
fn folds_integer_arithmetic_with_java_division() {
let mut graph = SyntaxGraph::new();
graph.add_node(NodeId(1), binary_operation("==", NodeId(2), NodeId(3)));
graph.add_node(NodeId(2), binary_operation("+", NodeId(4), NodeId(5)));
graph.add_node(NodeId(3), binary_operation("/", NodeId(8), NodeId(9)));
graph.add_node(NodeId(4), binary_operation("/", NodeId(6), NodeId(7)));
graph.add_node(NodeId(5), binary_operation("/", NodeId(10), NodeId(11)));
graph.add_node(NodeId(6), literal("1", "number"));
graph.add_node(NodeId(7), literal("10", "number"));
graph.add_node(NodeId(8), literal("3", "number"));
graph.add_node(NodeId(9), literal("10", "number"));
graph.add_node(NodeId(10), literal("2", "number"));
graph.add_node(NodeId(11), literal("10", "number"));
assert_eq!(
fold(&graph, &BTreeMap::new(), NodeId(1)),
Some(Folded::Truth(true))
);
}
#[test]
fn folds_integer_subtraction_and_multiplication() {
let mut graph = SyntaxGraph::new();
graph.add_node(NodeId(1), binary_operation("==", NodeId(2), NodeId(3)));
graph.add_node(NodeId(2), binary_operation("*", NodeId(4), NodeId(5)));
graph.add_node(NodeId(4), binary_operation("-", NodeId(6), NodeId(7)));
graph.add_node(NodeId(6), literal("7", "number"));
graph.add_node(NodeId(7), literal("2", "number"));
graph.add_node(NodeId(5), literal("3", "number"));
graph.add_node(NodeId(3), literal("15", "number"));
assert_eq!(
fold(&graph, &BTreeMap::new(), NodeId(1)),
Some(Folded::Truth(true))
);
}
#[test]
fn integer_overflow_abstains() {
let mut graph = SyntaxGraph::new();
graph.add_node(NodeId(1), binary_operation("==", NodeId(2), NodeId(5)));
graph.add_node(NodeId(2), binary_operation("+", NodeId(3), NodeId(4)));
graph.add_node(NodeId(3), literal("9223372036854775807", "number"));
graph.add_node(NodeId(4), literal("1", "number"));
graph.add_node(NodeId(5), literal("0", "number"));
assert_eq!(fold(&graph, &BTreeMap::new(), NodeId(1)), None);
}
#[test]
fn zero_division_abstains() {
let mut graph = SyntaxGraph::new();
graph.add_node(NodeId(1), binary_operation("==", NodeId(2), NodeId(5)));
graph.add_node(NodeId(2), binary_operation("/", NodeId(3), NodeId(4)));
graph.add_node(NodeId(3), literal("1", "number"));
graph.add_node(NodeId(4), literal("0", "number"));
graph.add_node(NodeId(5), literal("0", "number"));
assert_eq!(fold(&graph, &BTreeMap::new(), NodeId(1)), None);
}
#[test]
fn nan_comparisons_abstain() {
let mut graph = SyntaxGraph::new();
graph.add_node(NodeId(1), binary_operation("==", NodeId(2), NodeId(5)));
graph.add_node(NodeId(2), binary_operation("/", NodeId(3), NodeId(4)));
graph.add_node(NodeId(3), literal("0.0", "number"));
graph.add_node(NodeId(4), literal("0.0", "number"));
graph.add_node(NodeId(5), literal("0.0", "number"));
assert_eq!(fold(&graph, &BTreeMap::new(), NodeId(1)), None);
}
#[test]
fn folds_the_tree_structure_not_a_flat_expression() {
let mut graph = SyntaxGraph::new();
graph.add_node(NodeId(1), binary_operation("&", NodeId(2), NodeId(5)));
graph.add_node(NodeId(2), binary_operation("|", NodeId(3), NodeId(4)));
graph.add_node(NodeId(3), literal("true", "boolean"));
graph.add_node(NodeId(4), literal("false", "boolean"));
graph.add_node(NodeId(5), literal("false", "boolean"));
assert_eq!(
fold(&graph, &BTreeMap::new(), NodeId(1)),
Some(Folded::Truth(false))
);
}
#[test]
fn reserved_words_fold_type_blind() {
let mut graph = SyntaxGraph::new();
graph.add_node(NodeId(1), binary_operation("==", NodeId(2), NodeId(3)));
graph.add_node(
NodeId(2),
SyntaxNode::ReservedWord {
value: "null".to_owned(),
},
);
graph.add_node(
NodeId(3),
SyntaxNode::ReservedWord {
value: "null".to_owned(),
},
);
assert_eq!(
fold(&graph, &BTreeMap::new(), NodeId(1)),
Some(Folded::Truth(true))
);
}
#[test]
fn a_symbol_leaf_reads_the_resolved_map_only() {
let mut graph = SyntaxGraph::new();
graph.add_node(NodeId(1), binary_operation("==", NodeId(2), NodeId(3)));
graph.add_node(
NodeId(2),
SyntaxNode::SymbolLookup {
symbol: "x".to_owned(),
symbol_scope: None,
value: Some(String::from("1")),
},
);
graph.add_node(NodeId(3), literal("1", "number"));
assert_eq!(fold(&graph, &BTreeMap::new(), NodeId(1)), None);
let mut resolved = BTreeMap::new();
resolved.insert(NodeId(2), Folded::Int(1));
assert_eq!(
fold(&graph, &resolved, NodeId(1)),
Some(Folded::Truth(true))
);
}
}