use std::collections::HashMap;
use crate::hir::Expr;
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Value {
Number(f64),
String(String),
Bool(bool),
Array(Vec<Value>),
Object(Vec<(Value, Value)>),
Vector(Vec<Value>),
Null,
}
pub(crate) fn evaluate(
expression: &Expr,
constants: &HashMap<String, &Expr>,
bindings: &HashMap<String, Value>,
stack: &mut Vec<String>,
) -> Option<Value> {
eval(expression, constants, bindings, stack, false)
}
const NUMBER_LIMIT: f64 = 10_000_000.0;
pub(crate) fn evaluate_settings(
expression: &Expr,
constants: &HashMap<String, &Expr>,
bindings: &HashMap<String, Value>,
stack: &mut Vec<String>,
) -> Option<Value> {
eval(expression, constants, bindings, stack, true)
}
fn eval(
expression: &Expr,
constants: &HashMap<String, &Expr>,
bindings: &HashMap<String, Value>,
stack: &mut Vec<String>,
settings: bool,
) -> Option<Value> {
match expression {
Expr::Number { value, text, .. } => {
if settings
&& ["e-", "e+", "E-", "E+"]
.iter()
.any(|mark| text.contains(mark))
{
return None;
}
Some(Value::Number(*value))
}
Expr::String { value, .. } => {
if settings && has_unbound_formatters(value) {
return None;
}
Some(Value::String(value.clone()))
}
Expr::Bool { value, .. } => Some(Value::Bool(*value)),
Expr::Null { .. } => settings.then_some(Value::Null),
Expr::Array { elements, .. } => Some(Value::Array(
elements
.iter()
.map(|element| eval(element, constants, bindings, stack, settings))
.collect::<Option<Vec<_>>>()?,
)),
Expr::Vector { x, y, z, .. } => Some(Value::Vector(vec![
eval(x, constants, bindings, stack, settings)?,
eval(y, constants, bindings, stack, settings)?,
eval(z, constants, bindings, stack, settings)?,
])),
Expr::Dict { entries, .. } => Some(Value::Object(
entries
.iter()
.map(|entry| {
let key = eval(&entry.key, constants, bindings, stack, settings)?;
if settings && !matches!(key, Value::String(_)) {
return None;
}
Some((
key,
eval(&entry.value, constants, bindings, stack, settings)?,
))
})
.collect::<Option<Vec<_>>>()?,
)),
Expr::Constant { name, .. } => {
if let Some(value) = bindings.get(name) {
return Some(value.clone());
}
let Some(&value) = constants.get(name) else {
return (settings && name == "Infinity").then_some(Value::Number(f64::INFINITY));
};
if stack.iter().any(|active| active == name) {
return None;
}
stack.push(name.clone());
let result = eval(value, constants, bindings, stack, settings);
stack.pop();
result
}
Expr::Format { text, args, .. } => {
let values = args
.iter()
.map(|arg| eval(arg, constants, bindings, stack, settings))
.collect::<Option<Vec<_>>>()?;
evaluate_format(text, &values)
}
Expr::Binary {
op, left, right, ..
} => evaluate_binary(
op,
eval(left, constants, bindings, stack, settings)?,
eval(right, constants, bindings, stack, settings)?,
settings,
),
Expr::Conditional {
then_value: then_expr,
condition,
else_value: else_expr,
..
} => {
let condition = eval(condition, constants, bindings, stack, settings)?;
let then_value = eval(then_expr, constants, bindings, stack, settings)?;
let else_value = eval(else_expr, constants, bindings, stack, settings)?;
if matches!(&condition, Value::Array(values) if values.is_empty()) {
return None;
}
if same_spelling(then_expr, else_expr) && values_equal(&then_value, &else_value) {
return Some(then_value);
}
if matches!(condition, Value::Object(_)) {
return None;
}
match truthiness(&condition) {
Some(true) => Some(then_value),
Some(false) => Some(else_value),
None => None,
}
}
Expr::Unary { op, operand, .. } => {
let operand = eval(operand, constants, bindings, stack, settings)?;
match (op.as_str(), operand) {
("-", Value::Number(value)) => Some(Value::Number(-value)),
("+", value) => Some(value),
("not", value) => truthiness(&value).map(|value| Value::Bool(!value)),
_ => None,
}
}
Expr::Index { array, index, .. } => evaluate_index(
eval(array, constants, bindings, stack, settings)?,
eval(index, constants, bindings, stack, settings)?,
),
Expr::Member {
receiver, member, ..
} => {
match (
eval(receiver, constants, bindings, stack, settings)?,
member.as_str(),
) {
(Value::Vector(values), "x") => values.into_iter().next(),
(Value::Vector(values), "y") => values.into_iter().nth(1),
(Value::Vector(values), "z") => values.into_iter().nth(2),
_ => None,
}
}
Expr::ReceiverCall {
receiver,
name,
args,
..
} => {
let mut values = vec![eval(receiver, constants, bindings, stack, settings)?];
for arg in args {
values.push(eval(arg, constants, bindings, stack, settings)?);
}
evaluate_builtin(name, &values)
}
Expr::Call { name, args, .. } => {
let values = args
.iter()
.map(|arg| eval(arg, constants, bindings, stack, settings))
.collect::<Option<Vec<_>>>()?;
evaluate_builtin(name, &values)
}
_ => None,
}
}
fn evaluate_builtin(name: &str, values: &[Value]) -> Option<Value> {
let degrees = std::f64::consts::PI / 180.0;
let number = |value: &Value| match value {
Value::Number(value) => Some(*value),
_ => None,
};
fn vector(value: &Value) -> Option<&[Value]> {
match value {
Value::Vector(components) => Some(components.as_slice()),
_ => None,
}
}
let unary = |function: &dyn Fn(f64) -> f64| match values {
[value] => number(value).map(function).map(Value::Number),
_ => None,
};
match name {
"abs" => unary(&f64::abs),
"acos" => unary(&|value| libm::acos(js_clamp(value))),
"acosDeg" => unary(&|value| libm::acos(js_clamp(value)) * degrees),
"asin" => unary(&|value| libm::asin(js_clamp(value))),
"asinDeg" => unary(&|value| libm::asin(js_clamp(value)) * degrees),
"atan2" => match values {
[y, x] => Some(Value::Number(libm::atan2(number(y)?, number(x)?))),
_ => None,
},
"atan2Deg" => match values {
[y, x] => Some(Value::Number(libm::atan2(number(y)?, number(x)?) * degrees)),
_ => None,
},
"ceil" => unary(&f64::ceil),
"charAt" => match values {
[Value::String(text), index] => {
let index = js_max(0.0, number(index)?);
if index.is_nan() || index >= text.encode_utf16().count() as f64 {
return Some(Value::String(String::new()));
}
let unit = text.encode_utf16().nth(index as usize)?;
Some(Value::String(String::from_utf16_lossy(&[unit])))
}
_ => None,
},
"cos" => unary(&libm::cos),
"cosDeg" => unary(&|value| libm::cos(value * degrees)),
"concat" => match values {
[Value::Array(elements), Value::Array(extension)] => {
let mut elements = elements.clone();
elements.extend(extension.iter().cloned());
Some(Value::Array(elements))
}
[Value::Array(elements), value] => {
let mut elements = elements.clone();
elements.push(value.clone());
Some(Value::Array(elements))
}
_ => None,
},
"crossProduct" => match values {
[a, b] => {
let [ax, ay, az] = vector(a)? else {
return None;
};
let [bx, by, bz] = vector(b)? else {
return None;
};
let (ax, ay, az) = (number(ax)?, number(ay)?, number(az)?);
let (bx, by, bz) = (number(bx)?, number(by)?, number(bz)?);
Some(Value::Vector(vec![
Value::Number(ay * bz - az * by),
Value::Number(az * bx - ax * bz),
Value::Number(ax * by - ay * bx),
]))
}
_ => None,
},
"distance" => match values {
[a, b] => {
let [x1, y1, z1] = vector(a)? else {
return None;
};
let [x2, y2, z2] = vector(b)? else {
return None;
};
Some(Value::Number(libm::sqrt(
libm::pow(number(x1)? - number(x2)?, 2.0)
+ libm::pow(number(y1)? - number(y2)?, 2.0)
+ libm::pow(number(z1)? - number(z2)?, 2.0),
)))
}
_ => None,
},
"dotProduct" => match values {
[a, b] => {
let [x1, y1, z1] = vector(a)? else {
return None;
};
let [x2, y2, z2] = vector(b)? else {
return None;
};
Some(Value::Number(
number(x1)? * number(x2)?
+ number(y1)? * number(y2)?
+ number(z1)? * number(z2)?,
))
}
_ => None,
},
"exclude" => match values {
[Value::Array(elements), Value::Array(excluded)] => Some(Value::Array(
elements
.iter()
.filter(|element| !excluded.iter().any(|other| values_equal(element, other)))
.cloned()
.collect(),
)),
[Value::Array(elements), excluded] => Some(Value::Array(
elements
.iter()
.filter(|element| !values_equal(element, excluded))
.cloned()
.collect(),
)),
_ => None,
},
"floor" => unary(&f64::floor),
"index" => match values {
[Value::Array(elements), searched] => Some(Value::Number(
elements
.iter()
.position(|element| values_equal(element, searched))
.map(|index| index as f64)
.unwrap_or(-1.0),
)),
_ => None,
},
"last" => match values {
[Value::Array(elements)] if !elements.is_empty() => {
Some(elements[elements.len() - 1].clone())
}
_ => None,
},
"len" => match values {
[Value::Array(values)] => Some(Value::Number(values.len() as f64)),
_ => None,
},
"log" => match values {
[value] => number(value).map(libm::log).map(Value::Number),
[value, base] => Some(Value::Number(
libm::log(number(value)?) / libm::log(number(base)?),
)),
_ => None,
},
"magnitude" => match values {
[arg] => {
let [x, y, z] = vector(arg)? else { return None };
Some(Value::Number(libm::sqrt(
libm::pow(number(x)?, 2.0)
+ libm::pow(number(y)?, 2.0)
+ libm::pow(number(z)?, 2.0),
)))
}
_ => None,
},
"min" | "max" => {
let numbers = values.iter().map(&number).collect::<Option<Vec<_>>>()?;
if numbers.len() < 2 {
return None;
}
let result = numbers
.into_iter()
.reduce(if name == "min" { js_min } else { js_max })?;
Some(Value::Number(result))
}
"replace" => match values {
[
Value::String(text),
Value::String(pattern),
Value::String(replacement),
] => Some(Value::String(text.replace(pattern.as_str(), replacement))),
_ => None,
},
"round" => unary(&round_half_up),
"sin" => unary(&libm::sin),
"sinDeg" => unary(&|value| libm::sin(value * degrees)),
"slice" => match values {
[Value::Array(elements), start, count] => {
let mut count = round_half_up(number(count)?);
if count <= 0.0 {
return Some(Value::Array(Vec::new()));
}
let mut start = round_half_up(number(start)?);
if start.is_nan() || count.is_nan() {
return Some(Value::Array(Vec::new()));
}
if start < 0.0 {
count += start;
start = 0.0;
}
let start = start as usize;
let end = (start as f64 + count).min(elements.len() as f64) as usize;
Some(Value::Array(
elements
.get(start.min(elements.len())..end.min(elements.len()))
.unwrap_or(&[])
.to_vec(),
))
}
_ => None,
},
"sqrt" => unary(&|value| {
let result = value.sqrt();
if result.is_nan() { 0.0 } else { result }
}),
"strContains" => match values {
[Value::String(text), Value::String(member)] => {
Some(Value::Bool(text.contains(member.as_str())))
}
_ => None,
},
"strLen" => match values {
[Value::String(text)] => Some(Value::Number(text.chars().count() as f64)),
_ => None,
},
"substring" => match values {
[Value::String(text), start, length] => {
let units: Vec<u16> = text.encode_utf16().collect();
let start = js_max(0.0, number(start)?);
let end = start + js_max(0.0, number(length)?);
let clamp = |index: f64| {
if index.is_nan() {
0.0
} else {
index.min(units.len() as f64)
}
};
let (mut lo, mut hi) = (clamp(start), clamp(end));
if lo > hi {
std::mem::swap(&mut lo, &mut hi);
}
Some(Value::String(String::from_utf16_lossy(
&units[lo as usize..hi as usize],
)))
}
_ => None,
},
"tan" => unary(&libm::tan),
"tanDeg" => unary(&|value| libm::tan(value * degrees)),
"vectorTowards" => match values {
[a, b] => {
let [x1, y1, z1] = vector(a)? else {
return None;
};
let [x2, y2, z2] = vector(b)? else {
return None;
};
Some(Value::Vector(vec![
Value::Number(number(x2)? - number(x1)?),
Value::Number(number(y2)? - number(y1)?),
Value::Number(number(z2)? - number(z1)?),
]))
}
_ => None,
},
_ => None,
}
}
fn js_min(left: f64, right: f64) -> f64 {
if left.is_nan() || right.is_nan() {
f64::NAN
} else {
left.min(right)
}
}
fn js_max(left: f64, right: f64) -> f64 {
if left.is_nan() || right.is_nan() {
f64::NAN
} else {
left.max(right)
}
}
fn js_clamp(value: f64) -> f64 {
if value.is_nan() {
f64::NAN
} else {
value.clamp(-1.0, 1.0)
}
}
fn evaluate_binary(op: &str, left: Value, right: Value, settings: bool) -> Option<Value> {
let is_zero = |value: &Value| matches!(value, Value::Number(number) if *number == 0.0);
let is_one = |value: &Value| matches!(value, Value::Number(number) if *number == 1.0);
let is_bool = |value: &Value| matches!(value, Value::Bool(_));
let is_literal = |value: &Value| match value {
Value::Number(_) | Value::String(_) => true,
Value::Vector(components) => components
.iter()
.all(|component| matches!(component, Value::Number(_) | Value::String(_))),
_ => false,
};
let is_falsy = |value: &Value| truthiness(value) == Some(false);
let folded = |result: f64| (result.abs() <= NUMBER_LIMIT).then_some(Value::Number(result));
match op {
"+" => {
if let (Value::Number(left), Value::Number(right)) = (&left, &right) {
if let result @ Some(_) = folded(left + right) {
return result;
}
}
if is_zero(&left) {
return Some(right);
}
if is_zero(&right) {
return Some(left);
}
if !settings && let (Value::String(left), Value::String(right)) = (&left, &right) {
return Some(Value::String(left.clone() + right));
}
None
}
"-" => {
if let (Value::Number(left), Value::Number(right)) = (&left, &right)
&& let result @ Some(_) = folded(left - right)
{
return result;
}
if is_zero(&right) {
return Some(left);
}
if values_equal(&left, &right) {
return Some(Value::Number(0.0));
}
None
}
"*" => {
if let (Value::Number(left), Value::Number(right)) = (&left, &right)
&& let result @ Some(_) = folded(left * right)
{
return result;
}
if is_one(&left) {
return Some(right);
}
if is_one(&right) {
return Some(left);
}
if is_zero(&left) || is_zero(&right) {
return Some(Value::Number(0.0));
}
None
}
"/" => {
if let (Value::Number(left), Value::Number(right)) = (&left, &right)
&& let result @ Some(_) = folded(left / right)
{
return result;
}
if is_one(&right) {
return Some(left);
}
if is_zero(&left) || is_zero(&right) {
return Some(Value::Number(0.0));
}
None
}
"%" => {
if let (Value::Number(left), Value::Number(right)) = (&left, &right) {
return Some(Value::Number(left % right.abs()));
}
if values_equal(&left, &right) || is_zero(&left) || is_zero(&right) {
return Some(Value::Number(0.0));
}
None
}
"**" => {
if let (Value::Number(base), Value::Number(exponent)) = (&left, &right) {
if *base < 0.0 {
return Some(Value::Number(0.0));
}
if let result @ Some(_) = folded(base.powf(*exponent)) {
return result;
}
}
if is_one(&right) {
return Some(left);
}
if is_zero(&left) {
return Some(Value::Number(0.0));
}
if is_one(&left) {
return Some(left);
}
None
}
"and" => {
if is_falsy(&left) {
Some(left)
} else if is_falsy(&right) || truthiness(&left) == Some(true) {
Some(right)
} else if truthiness(&right) == Some(true) || values_equal(&left, &right) {
Some(left)
} else {
None
}
}
"or" => {
if is_falsy(&left) {
Some(right)
} else if is_falsy(&right) || truthiness(&left) == Some(true) {
Some(left)
} else if truthiness(&right) == Some(true) {
Some(right)
} else if values_equal(&left, &right) {
Some(left)
} else {
None
}
}
"==" => {
if let (Value::Number(left), Value::Number(right)) = (&left, &right) {
return Some(Value::Bool(left == right));
}
if values_equal(&left, &right) {
return Some(Value::Bool(true));
}
if is_literal(&left) && is_literal(&right) {
return Some(Value::Bool(false));
}
match (&left, &right) {
(Value::Bool(left), right) if is_falsy(right) => {
return Some(Value::Bool(!left));
}
(left, Value::Bool(right)) if is_falsy(left) => {
return Some(Value::Bool(!right));
}
(left, right) if is_bool(left) && *right == Value::Bool(true) => {
return Some(left.clone());
}
(left, right) if is_bool(right) && *left == Value::Bool(true) => {
return Some(right.clone());
}
_ => {}
}
None
}
"!=" => {
if let (Value::Number(left), Value::Number(right)) = (&left, &right) {
return Some(Value::Bool(left != right));
}
if values_equal(&left, &right) {
return Some(Value::Bool(false));
}
if is_literal(&left) && is_literal(&right) {
return Some(Value::Bool(true));
}
match (&left, &right) {
(left, right) if is_bool(left) && is_falsy(right) => {
return Some(left.clone());
}
(left, right) if is_bool(right) && is_falsy(left) => {
return Some(right.clone());
}
(Value::Bool(left), right) if *right == Value::Bool(true) => {
return Some(Value::Bool(!left));
}
(left, Value::Bool(right)) if *left == Value::Bool(true) => {
return Some(Value::Bool(!right));
}
_ => {}
}
None
}
"<" | "<=" | ">" | ">=" => {
if let (Value::Number(left), Value::Number(right)) = (&left, &right) {
return Some(Value::Bool(match op {
"<" => left < right,
"<=" => left <= right,
">" => left > right,
_ => left >= right,
}));
}
if values_equal(&left, &right) {
return Some(Value::Bool(matches!(op, "<=" | ">=")));
}
None
}
"in" => match right {
Value::Array(values) => Some(Value::Bool(
values.iter().any(|value| values_equal(value, &left)),
)),
_ => None,
},
"not in" => match right {
Value::Array(values) => Some(Value::Bool(
!values.iter().any(|value| values_equal(value, &left)),
)),
_ => None,
},
_ => None,
}
}
fn truthiness(value: &Value) -> Option<bool> {
match value {
Value::Bool(value) => Some(*value),
Value::Number(value) => Some(*value != 0.0),
Value::String(value) => Some(!value.is_empty()),
Value::Array(values) if values.is_empty() => Some(false),
Value::Array(values) => truthiness(&values[0]),
Value::Null => Some(false),
Value::Object(_) => None,
Value::Vector(components) => components
.iter()
.any(|component| truthiness(component) == Some(true))
.then_some(true),
}
}
fn same_spelling(left: &Expr, right: &Expr) -> bool {
match (left, right) {
(Expr::Number { text: a, .. }, Expr::Number { text: b, .. }) => a == b,
(Expr::Array { elements: a, .. }, Expr::Array { elements: b, .. }) => {
a.len() == b.len() && a.iter().zip(b.iter()).all(|(a, b)| same_spelling(a, b))
}
(Expr::Dict { entries: a, .. }, Expr::Dict { entries: b, .. }) => {
a.len() == b.len()
&& a.iter().zip(b.iter()).all(|(a, b)| {
same_spelling(&a.key, &b.key) && same_spelling(&a.value, &b.value)
})
}
(
Expr::Vector {
x: ax,
y: ay,
z: az,
..
},
Expr::Vector {
x: bx,
y: by,
z: bz,
..
},
) => same_spelling(ax, bx) && same_spelling(ay, by) && same_spelling(az, bz),
_ => true,
}
}
fn values_equal(left: &Value, right: &Value) -> bool {
match (left, right) {
(Value::Number(left), Value::Number(right)) => {
crate::compiler::number_format::javascript_text(*left)
== crate::compiler::number_format::javascript_text(*right)
}
(Value::String(left), Value::String(right)) => left == right,
(Value::Bool(left), Value::Bool(right)) => left == right,
(Value::Null, Value::Null) => true,
(Value::Array(left), Value::Array(right)) | (Value::Vector(left), Value::Vector(right)) => {
left.len() == right.len()
&& left
.iter()
.zip(right.iter())
.all(|(left, right)| values_equal(left, right))
}
(Value::Object(left), Value::Object(right)) => {
left.len() == right.len()
&& left
.iter()
.zip(right.iter())
.all(|((lk, lv), (rk, rv))| values_equal(lk, rk) && values_equal(lv, rv))
}
_ => false,
}
}
fn evaluate_index(collection: Value, index: Value) -> Option<Value> {
match collection {
Value::Array(values) => {
let Value::Number(index) = index else {
return None;
};
let index = round_half_up(index);
if index.is_nan() || index < 0.0 || index >= values.len() as f64 {
return None;
}
values.into_iter().nth(index as usize)
}
Value::Object(entries) => entries
.into_iter()
.find(|(key, _)| values_equal(key, &index))
.map(|(_, value)| value),
_ => None,
}
}
fn evaluate_format(text: &str, args: &[Value]) -> Option<Value> {
let chars: Vec<char> = text.chars().collect();
let mut output = String::new();
let mut unnumbered = 0usize;
let mut numbered = false;
let mut index = 0;
while index < chars.len() {
if chars[index] == '{' {
if let Some(length) = brace_escape_len(&chars[index..]) {
output.extend(&chars[index..index + length]);
index += length;
continue;
}
if let Some((digits, length)) = format_arg_len(&chars[index..]) {
index += length;
if let Some(digits) = digits {
numbered = true;
let argument = digits.parse::<usize>().ok()?;
output.push_str(&format_substitute(args.get(argument)?)?);
} else {
unnumbered += 1;
output.push_str(&format_substitute(args.get(unnumbered - 1)?)?);
}
continue;
}
}
output.push(chars[index]);
index += 1;
}
if numbered && unnumbered > 0 || unnumbered > 0 && unnumbered != args.len() {
return None;
}
Some(Value::String(output))
}
fn brace_escape_len(chars: &[char]) -> Option<usize> {
if chars.len() < 4 || chars[0] != '{' || chars[1] != '{' {
return None;
}
let mut index = 2;
while index < chars.len() && chars[index].is_ascii_digit() {
index += 1;
}
(index + 1 < chars.len() && chars[index] == '}' && chars[index + 1] == '}').then_some(index + 2)
}
fn format_arg_len(chars: &[char]) -> Option<(Option<String>, usize)> {
let mut index = 1;
while index < chars.len() && chars[index].is_ascii_digit() {
index += 1;
}
if index >= chars.len() || chars[index] != '}' {
return None;
}
let digits: String = chars[1..index].iter().collect();
Some(((!digits.is_empty()).then_some(digits), index + 1))
}
pub(crate) fn has_unbound_formatters(text: &str) -> bool {
evaluate_format(text, &[]).is_none()
}
fn format_substitute(value: &Value) -> Option<String> {
match value {
Value::Number(value) if value.abs() <= NUMBER_LIMIT => Some(workshop_number_text(*value)),
Value::String(value) => Some(value.clone()),
Value::Null => Some("0".to_string()),
Value::Array(values) if values.is_empty() => Some(String::new()),
Value::Array(values) => format_substitute(&values[0]),
_ => None,
}
}
pub(crate) fn display(value: Value) -> Option<String> {
match value {
Value::Number(value) if value.is_finite() => Some(workshop_number_text(value)),
Value::String(value) => Some(value),
Value::Bool(value) => Some(value.to_string()),
_ => None,
}
}
pub(crate) fn round_half_up(value: f64) -> f64 {
let floor = value.floor();
if value - floor >= 0.5 {
floor + 1.0
} else {
floor
}
}
fn workshop_number_text(value: f64) -> String {
let rounded = (value * 100.0).round() / 100.0;
if rounded.fract() == 0.0 {
format!("{}", rounded as i64)
} else {
format!("{rounded:.2}")
}
}