use workshop_rs::catalog::Kind;
use workshop_rs::{Action, ModifyOp, Value};
use super::Compiler;
use super::string_format;
use crate::compile_time::round_half_up;
pub(crate) const NUMBER_LIMIT: f64 = 1e7;
const RANDOM_CALLS: [&str; 4] = [
"randomInteger",
"randomReal",
"randomValueInArray",
"randomizedArray",
];
pub(super) struct OperatorOptimizer<'a> {
compiler: &'a Compiler,
strict: bool,
}
impl<'a> OperatorOptimizer<'a> {
pub(super) fn new(compiler: &'a Compiler, strict: bool) -> Self {
Self { compiler, strict }
}
pub(super) fn constant_truth(&self, value: &Value) -> Option<bool> {
if falsy(value) {
Some(false)
} else if self.truthy(value) {
Some(true)
} else {
None
}
}
pub(super) fn wrap_condition(&self, value: Value) -> Value {
match value {
Value::Call { name, args }
if matches!(name.as_str(), "==" | "!=" | "<" | "<=" | ">" | ">=") =>
{
Value::Call { name, args }
}
Value::Call { name, mut args } if name == "not" && args.len() == 1 => call(
"==",
vec![args.pop().expect("one argument"), Value::Bool(false)],
),
value if self.boolean(&value) => call("==", vec![value, Value::Bool(true)]),
value => call("!=", vec![value, Value::Bool(false)]),
}
}
pub(super) fn node(&self, value: Value) -> Value {
let value = literal_array(value);
let head = head_of(&value);
let rewritten = match self.rewrite(value) {
Rewrite::Same(value) => return canonical(value),
Rewrite::Changed(value) => value,
};
if head_of(&rewritten) == head {
canonical(rewritten)
} else {
self.node(rewritten)
}
}
fn rewrite(&self, value: Value) -> Rewrite {
let Value::Call { name, args } = value else {
return Rewrite::Same(value);
};
match (name.as_str(), args.len()) {
("==", 2) => self.equals(args, true),
("!=", 2) => self.equals(args, false),
("<", 2) => Self::ordering("<", args, |a, b| a < b, false),
("<=", 2) => Self::ordering("<=", args, |a, b| a <= b, true),
(">", 2) => Self::ordering(">", args, |a, b| a > b, false),
(">=", 2) => Self::ordering(">=", args, |a, b| a >= b, true),
("not", 1) => self.not(args),
("and", 2) | ("or", 2) => self.logical(args, &name),
("ifThenElse", 3) => self.if_then_else(args),
("add", 2) => self.add(args),
("subtract", 2) => self.subtract(args),
("multiply", 2) => self.multiply(args),
("divide", 2) => self.divide(args),
("modulo", 2) => self.modulo(args),
("raiseToPower", 2) => self.power(args),
("-", 1) => self.negate(args),
("roundToInteger", 2) => Self::round(args),
("absoluteValue", 1) => Self::unary("absoluteValue", args, f64::abs),
("sin", 1) => Self::unary("sin", args, f64::sin),
("cos", 1) => Self::unary("cos", args, f64::cos),
("sinDeg", 1) => Self::unary("sinDeg", args, |degrees| {
(degrees * (std::f64::consts::PI / 180.0)).sin()
}),
("cosDeg", 1) => Self::unary("cosDeg", args, |degrees| {
(degrees * (std::f64::consts::PI / 180.0)).cos()
}),
("tan", 1) => Self::unary("tan", args, f64::tan),
("tanDeg", 1) => Self::unary("tanDeg", args, |degrees| {
(degrees * (std::f64::consts::PI / 180.0)).tan()
}),
("acos", 1) => Self::unary("acos", args, |value| value.clamp(-1.0, 1.0).acos()),
("acosDeg", 1) => Self::unary("acosDeg", args, |value| {
value.clamp(-1.0, 1.0).acos() * (std::f64::consts::PI / 180.0)
}),
("asin", 1) => Self::unary("asin", args, |value| value.clamp(-1.0, 1.0).asin()),
("asinDeg", 1) => Self::unary("asinDeg", args, |value| {
value.clamp(-1.0, 1.0).asin() * (std::f64::consts::PI / 180.0)
}),
("atan2", 2) => Self::binary("atan2", args, f64::atan2),
("atan2Deg", 2) => Self::binary("atan2Deg", args, |numerator, denominator| {
numerator.atan2(denominator) * (std::f64::consts::PI / 180.0)
}),
("crossProduct", 2) => Self::cross_product(args),
("normalize", 1) => Self::normalize(args),
("strLen", 1) => Self::string_length(args),
("squareRoot", 1) => self.square_root(args),
("min", 2) => Self::extremum("min", args, f64::min),
("max", 2) => Self::extremum("max", args, f64::max),
("magnitude", 1) => Self::magnitude(args),
("distance", 2) => Self::distance(args),
("dotProduct", 2) => Self::dot_product(args),
("directionTowards", 2) => Self::direction_towards(args),
("vectorTowards", 2) => Self::vector_towards(args),
("oppositeTeamOf", 1) => Self::opposite_team(args),
("strContains", 2) => Self::string_contains(args),
("stringSlice", 3) => Self::string_slice(args),
("stringReplace", 3) => Self::string_replace(args),
("indexOfArrayValue", 2) => self.index_of(args),
("customString", _) if !args.is_empty() => Self::custom_string(args),
("slice", 3) => Self::slice(args),
("charAt", 2) => Self::char_at(args),
("lastOf", 1) => {
let [array] = one(args);
match literal_array(array) {
Value::Array(mut elements) if !elements.is_empty() => {
Rewrite::Changed(elements.pop().expect("non-empty"))
}
other => Rewrite::Same(call("lastOf", vec![other])),
}
}
("appendToArray", 2) => self.concat(args),
("removeFromArray", 2) => self.exclude(args),
("mappedArray", 2) => Self::mapped(args),
("filteredArray", 2) => self.filtered(args),
("arrayContains", 2) => self.array_contains(args),
("valueInArray", 2) => self.value_in_array(args),
("firstOf", 1) => {
let [array] = one(args);
if matches!(&array, Value::Array(elements) if elements.is_empty()) && self.strict {
return Rewrite::Same(call("firstOf", vec![array]));
}
self.value_in_array(vec![array, Value::Number(0.0)])
}
("__xComponentOf__", 1) => Self::component(args, 0),
("__yComponentOf__", 1) => Self::component(args, 1),
("__zComponentOf__", 1) => Self::component(args, 2),
("log", 1) | ("log", 2) => Rewrite::Changed(expand_log(args, true)),
_ => Rewrite::Same(Value::Call { name, args }),
}
}
fn equals(&self, args: Vec<Value>, equal: bool) -> Rewrite {
let name = if equal { "==" } else { "!=" };
let [left, right] = two(args);
if let (Value::Number(a), Value::Number(b)) = (&left, &right) {
return Rewrite::Changed(Value::Bool((a == b) == equal));
}
if same(&left, &right) {
return Rewrite::Changed(Value::Bool(equal));
}
if self.literal(&left) && self.literal(&right) {
return Rewrite::Changed(Value::Bool(!equal));
}
if falsy(&right) && self.boolean(&left) {
return Rewrite::Changed(if equal { not(left) } else { left });
}
if falsy(&left) && self.boolean(&right) {
return Rewrite::Changed(if equal { not(right) } else { right });
}
if matches!(right, Value::Bool(true)) && self.boolean(&left) {
return Rewrite::Changed(if equal { left } else { not(left) });
}
if matches!(left, Value::Bool(true)) && self.boolean(&right) {
return Rewrite::Changed(if equal { right } else { not(right) });
}
Rewrite::Same(call(name, vec![left, right]))
}
fn ordering(
name: &str,
args: Vec<Value>,
compare: fn(f64, f64) -> bool,
reflexive: bool,
) -> Rewrite {
let [left, right] = two(args);
if let (Value::Number(a), Value::Number(b)) = (&left, &right) {
return Rewrite::Changed(Value::Bool(compare(*a, *b)));
}
if same(&left, &right) {
return Rewrite::Changed(Value::Bool(reflexive));
}
Rewrite::Same(call(name, vec![left, right]))
}
fn not(&self, args: Vec<Value>) -> Rewrite {
let [operand] = one(args);
if falsy(&operand) {
return Rewrite::Changed(Value::Bool(true));
}
if self.truthy(&operand) {
return Rewrite::Changed(Value::Bool(false));
}
if let Value::Call { name, args } = operand {
if name == "not" && args.len() == 1 && self.boolean(&args[0]) {
return Rewrite::Changed(args.into_iter().next().expect("one argument"));
}
let inverse = match name.as_str() {
"isAlive" if args.len() == 1 => Some("isDead"),
"isDead" if args.len() == 1 => Some("isAlive"),
"==" => Some("!="),
"!=" => Some("=="),
">" => Some("<="),
">=" => Some("<"),
"<" => Some(">="),
"<=" => Some(">"),
_ => None,
};
if let Some(inverse) = inverse {
return Rewrite::Changed(call(inverse, args));
}
return Rewrite::Same(not(Value::Call { name, args }));
}
Rewrite::Same(not(operand))
}
fn logical(&self, args: Vec<Value>, name: &str) -> Rewrite {
let conjunction = name == "and";
let opposite = if conjunction { "or" } else { "and" };
let [left, right] = two(args);
if falsy(&left) {
return Rewrite::Changed(if conjunction { left } else { right });
}
if !self.strict && falsy(&right) {
return Rewrite::Changed(if conjunction { right } else { left });
}
if self.truthy(&left) {
return Rewrite::Changed(if conjunction { right } else { left });
}
if !self.strict && self.truthy(&right) {
return Rewrite::Changed(if conjunction { left } else { right });
}
if same(&left, &right) {
return Rewrite::Changed(left);
}
if !self.strict && (negates(&right, &left) || negates(&left, &right)) {
return Rewrite::Changed(Value::Bool(!conjunction));
}
if let (Some(a), Some(b)) = (negated(&left), negated(&right)) {
return Rewrite::Changed(not(call(opposite, vec![a.clone(), b.clone()])));
}
Rewrite::Same(call(name, vec![left, right]))
}
fn if_then_else(&self, args: Vec<Value>) -> Rewrite {
let [condition, then_value, else_value] = three(args);
if self.truthy(&condition) {
return Rewrite::Changed(then_value);
}
if falsy(&condition) {
return Rewrite::Changed(else_value);
}
if same(&then_value, &else_value) {
return Rewrite::Changed(then_value);
}
if let Value::Call { name, args } = condition {
if name == "not" && args.len() == 1 {
let inner = args.into_iter().next().expect("one argument");
return Rewrite::Changed(call("ifThenElse", vec![inner, else_value, then_value]));
}
return Rewrite::Same(call(
"ifThenElse",
vec![Value::Call { name, args }, then_value, else_value],
));
}
Rewrite::Same(call("ifThenElse", vec![condition, then_value, else_value]))
}
fn add(&self, args: Vec<Value>) -> Rewrite {
let [left, right] = two(args);
if let Some(sum) = fold(&left, &right, |a, b| a + b) {
return Rewrite::Changed(sum);
}
if !self.strict {
if is_number(&left, 0.0) {
return Rewrite::Changed(right);
}
if is_number(&right, 0.0) {
return Rewrite::Changed(left);
}
if zero_vector(&left) {
return Rewrite::Changed(right);
}
if zero_vector(&right) {
return Rewrite::Changed(left);
}
}
if same(&left, &right) {
return Rewrite::Changed(call("multiply", vec![Value::Number(2.0), left]));
}
if let Some(sum) = vector_fold(&left, &right, |a, b| a + b) {
return Rewrite::Changed(sum);
}
Rewrite::Same(call("add", vec![left, right]))
}
fn subtract(&self, args: Vec<Value>) -> Rewrite {
let [left, right] = two(args);
if let Some(difference) = fold(&left, &right, |a, b| a - b) {
return Rewrite::Changed(difference);
}
if is_number(&right, 0.0) || zero_vector(&right) {
return Rewrite::Changed(left);
}
if same(&left, &right) {
return Rewrite::Changed(call("multiply", vec![left, Value::Number(0.0)]));
}
if let Some(difference) = vector_fold(&left, &right, |a, b| a - b) {
return Rewrite::Changed(difference);
}
Rewrite::Same(call("subtract", vec![left, right]))
}
fn multiply(&self, args: Vec<Value>) -> Rewrite {
let [left, right] = two(args);
if let Some(product) = fold(&left, &right, |a, b| a * b) {
return Rewrite::Changed(product);
}
if !self.strict {
if is_number(&left, 1.0) {
return Rewrite::Changed(right);
}
if is_number(&right, 1.0) {
return Rewrite::Changed(left);
}
}
if (is_number(&left, 0.0) && (!self.strict || self.float(&right)))
|| (is_number(&right, 0.0) && (!self.strict || self.float(&left)))
{
return Rewrite::Changed(Value::Number(0.0));
}
if let Some(product) = vector_fold(&left, &right, |a, b| a * b) {
return Rewrite::Changed(product);
}
if let (Value::Number(scale), Some(components)) = (&left, number_components(&right)) {
return Rewrite::Changed(vector(components.map(|c| scale * c)));
}
if let (Some(components), Value::Number(scale)) = (number_components(&left), &right) {
return Rewrite::Changed(vector(components.map(|c| c * scale)));
}
Rewrite::Same(call("multiply", vec![left, right]))
}
fn divide(&self, args: Vec<Value>) -> Rewrite {
let [left, right] = two(args);
if let Some(quotient) = fold(&left, &right, |a, b| a / b) {
return Rewrite::Changed(quotient);
}
if !self.strict {
if is_number(&right, 1.0) {
return Rewrite::Changed(left);
}
if is_number(&left, 0.0) || is_number(&right, 0.0) {
return Rewrite::Changed(Value::Number(0.0));
}
}
if let Some(quotient) = vector_fold(&left, &right, |a, b| a / b) {
return Rewrite::Changed(quotient);
}
if let (Some(components), Value::Number(divisor)) = (number_components(&left), &right) {
return Rewrite::Changed(vector(components.map(|c| c / divisor)));
}
Rewrite::Same(call("divide", vec![left, right]))
}
fn modulo(&self, args: Vec<Value>) -> Rewrite {
let [left, right] = two(args);
if let (Value::Number(a), Value::Number(b)) = (&left, &right) {
return Rewrite::Changed(Value::Number(a % b.abs()));
}
if same(&left, &right) || is_number(&left, 0.0) || is_number(&right, 0.0) {
return Rewrite::Changed(Value::Number(0.0));
}
Rewrite::Same(call("modulo", vec![left, right]))
}
fn power(&self, args: Vec<Value>) -> Rewrite {
let [base, exponent] = two(args);
if let (Value::Number(a), Value::Number(b)) = (&base, &exponent) {
if *a < 0.0 {
return Rewrite::Changed(Value::Number(0.0));
}
let result = a.powf(*b);
if result.abs() <= NUMBER_LIMIT {
return Rewrite::Changed(Value::Number(result));
}
}
if is_number(&exponent, 1.0) || is_number(&base, 1.0) {
return Rewrite::Changed(base);
}
if is_number(&base, 0.0) {
return Rewrite::Changed(Value::Number(0.0));
}
Rewrite::Same(call("raiseToPower", vec![base, exponent]))
}
fn negate(&self, args: Vec<Value>) -> Rewrite {
let [operand] = one(args);
if let Value::Number(number) = operand {
return Rewrite::Changed(Value::Number(-number));
}
match operand {
Value::Call { name, mut args }
if matches!(name.as_str(), "multiply" | "divide" | "modulo")
&& args.len() == 2
&& matches!(args[0], Value::Number(_)) =>
{
if let Value::Number(number) = args[0] {
args[0] = Value::Number(-number);
}
Rewrite::Changed(Value::Call { name, args })
}
other => {
if let Some(components) = number_components(&other) {
return Rewrite::Changed(vector(components.map(|c| -c)));
}
Rewrite::Changed(call("multiply", vec![Value::Number(-1.0), other]))
}
}
}
fn round(args: Vec<Value>) -> Rewrite {
let [number, direction] = two(args);
if let (Value::Number(number), Value::Enum { value, .. }) = (&number, &direction) {
match value.as_str() {
"NEAREST" => return Rewrite::Changed(Value::Number(round_half_up(*number))),
"UP" => return Rewrite::Changed(Value::Number(number.ceil())),
"DOWN" => return Rewrite::Changed(Value::Number(number.floor())),
_ => {}
}
}
Rewrite::Same(call("roundToInteger", vec![number, direction]))
}
fn unary(name: &str, args: Vec<Value>, apply: fn(f64) -> f64) -> Rewrite {
let [number] = one(args);
match number {
Value::Number(number) => Rewrite::Changed(Value::Number(apply(number))),
other => Rewrite::Same(call(name, vec![other])),
}
}
fn binary(name: &str, args: Vec<Value>, apply: fn(f64, f64) -> f64) -> Rewrite {
match two(args) {
[Value::Number(left), Value::Number(right)] => {
Rewrite::Changed(Value::Number(apply(left, right)))
}
[left, right] => Rewrite::Same(call(name, vec![left, right])),
}
}
fn cross_product(args: Vec<Value>) -> Rewrite {
let [left, right] = two(args);
match (number_components(&left), number_components(&right)) {
(Some(a), Some(b)) => Rewrite::Changed(vector([
a[1] * b[2] - a[2] * b[1] + 0.0,
a[2] * b[0] - a[0] * b[2] + 0.0,
a[0] * b[1] - a[1] * b[0] + 0.0,
])),
_ => Rewrite::Same(call("crossProduct", vec![left, right])),
}
}
fn normalize(args: Vec<Value>) -> Rewrite {
let [operand] = one(args);
if let Value::Call { name, args } = &operand
&& name == "vectorTowards"
&& args.len() == 2
{
let Value::Call { args, .. } = operand else {
unreachable!()
};
return Rewrite::Changed(call("directionTowards", args));
}
let Some(components) = number_components(&operand) else {
return Rewrite::Same(call("normalize", vec![operand]));
};
let length = components
.iter()
.map(|part| part * part)
.sum::<f64>()
.sqrt();
Rewrite::Changed(vector(if length == 0.0 {
[0.0; 3]
} else {
components.map(|part| part / length + 0.0)
}))
}
fn string_length(args: Vec<Value>) -> Rewrite {
let [text] = one(args);
match literal_text(&text) {
Some(text) => Rewrite::Changed(Value::Number(text.chars().count() as f64)),
None => Rewrite::Same(call("strLen", vec![text])),
}
}
fn square_root(&self, args: Vec<Value>) -> Rewrite {
let [operand] = one(args);
match operand {
Value::Number(number) => {
let root = number.sqrt();
Rewrite::Changed(Value::Number(if root.is_nan() { 0.0 } else { root + 0.0 }))
}
Value::Call { name, mut args }
if name == "dotProduct" && args.len() == 2 && same(&args[0], &args[1]) =>
{
Rewrite::Changed(call("magnitude", vec![args.swap_remove(0)]))
}
other => Rewrite::Same(call("squareRoot", vec![other])),
}
}
fn extremum(name: &str, args: Vec<Value>, apply: fn(f64, f64) -> f64) -> Rewrite {
Self::binary(name, args, apply)
}
fn magnitude(args: Vec<Value>) -> Rewrite {
let [vector] = one(args);
match number_components(&vector) {
Some([x, y, z]) => Rewrite::Changed(Value::Number((x * x + y * y + z * z).sqrt())),
None => Rewrite::Same(call("magnitude", vec![vector])),
}
}
fn distance(args: Vec<Value>) -> Rewrite {
let [left, right] = two(args);
if let (Some(a), Some(b)) = (number_components(&left), number_components(&right)) {
let squares: f64 = (0..3).map(|axis| (a[axis] - b[axis]).powi(2)).sum();
return Rewrite::Changed(Value::Number(squares.sqrt()));
}
if zero_vector(&left) {
return Rewrite::Changed(call("magnitude", vec![right]));
}
if zero_vector(&right) {
return Rewrite::Changed(call("magnitude", vec![left]));
}
Rewrite::Same(call("distance", vec![left, right]))
}
fn dot_product(args: Vec<Value>) -> Rewrite {
let [left, right] = two(args);
match (number_components(&left), number_components(&right)) {
(Some(a), Some(b)) => {
Rewrite::Changed(Value::Number(a[0] * b[0] + a[1] * b[1] + a[2] * b[2]))
}
_ => Rewrite::Same(call("dotProduct", vec![left, right])),
}
}
fn vector_towards(args: Vec<Value>) -> Rewrite {
let [start, end] = two(args);
let (Some(a), Some(b)) = (number_components(&start), number_components(&end)) else {
return Rewrite::Same(call("vectorTowards", vec![start, end]));
};
Rewrite::Changed(vector([
b[0] - a[0] + 0.0,
b[1] - a[1] + 0.0,
b[2] - a[2] + 0.0,
]))
}
fn direction_towards(args: Vec<Value>) -> Rewrite {
let [start, end] = two(args);
let (Some(a), Some(b)) = (number_components(&start), number_components(&end)) else {
return Rewrite::Same(call("directionTowards", vec![start, end]));
};
let offset = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let length = (offset[0] * offset[0] + offset[1] * offset[1] + offset[2] * offset[2]).sqrt();
Rewrite::Changed(if length == 0.0 {
vector([0.0, 0.0, 0.0])
} else {
vector(offset.map(|component| component / length))
})
}
fn opposite_team(args: Vec<Value>) -> Rewrite {
let [team] = one(args);
let opposite = match &team {
Value::Call { name, args } if name == "team" && args.len() == 1 => {
opposite_of(&args[0]).map(|member| call("team", vec![member]))
}
other => opposite_of(other),
};
match opposite {
Some(opposite) => Rewrite::Changed(opposite),
None => Rewrite::Same(call("oppositeTeamOf", vec![team])),
}
}
fn string_contains(args: Vec<Value>) -> Rewrite {
let [text, search] = two(args);
match (literal_text(&text), literal_text(&search)) {
(Some(text), Some(search)) => Rewrite::Changed(Value::Bool(text.contains(search))),
_ => Rewrite::Same(call("strContains", vec![text, search])),
}
}
fn string_slice(args: Vec<Value>) -> Rewrite {
let [text, start, length] = three(args);
if let (Some(literal), Value::Number(start), Value::Number(length)) =
(literal_text(&text), &start, &length)
{
let units: Vec<u16> = literal.encode_utf16().collect();
let from = (start.max(0.0).trunc() as usize).min(units.len());
let to = ((start.max(0.0) + length.max(0.0)).trunc() as usize).min(units.len());
let slice = String::from_utf16_lossy(&units[from..to.max(from)]);
return Rewrite::Changed(Value::String(slice));
}
Rewrite::Same(call("stringSlice", vec![text, start, length]))
}
fn string_replace(args: Vec<Value>) -> Rewrite {
let [text, search, replacement] = three(args);
if let (Some(text), Some(search), Some(replacement)) = (
literal_text(&text),
literal_text(&search),
literal_text(&replacement),
) {
return Rewrite::Changed(Value::String(text.replace(search, replacement)));
}
Rewrite::Same(call("stringReplace", vec![text, search, replacement]))
}
fn index_of(&self, args: Vec<Value>) -> Rewrite {
let [array, needle] = two(args);
let mut elements = match literal_array(array.clone()) {
Value::Array(elements) => elements,
Value::Call { name, args } if name == "emptyArray" && args.is_empty() => Vec::new(),
_ => return Rewrite::Same(call("indexOfArrayValue", vec![array, needle])),
};
if elements.is_empty() {
return Rewrite::Changed(Value::Number(-1.0));
}
let needle_literal = self.constant(&needle);
let mut all_literal = true;
let mut kept = elements.len();
for (position, element) in elements.iter().enumerate() {
all_literal &= self.constant(element);
if same(element, &needle) {
if needle_literal && all_literal {
return Rewrite::Changed(Value::Number(position as f64));
}
kept = position + 1;
break;
}
}
elements.truncate(kept);
if needle_literal && all_literal {
return Rewrite::Changed(Value::Number(-1.0));
}
Rewrite::Same(call(
"indexOfArrayValue",
vec![Value::Array(elements), needle],
))
}
fn custom_string(args: Vec<Value>) -> Rewrite {
let value = call("customString", args);
let Some(tokens) = string_format::tokens(&value) else {
return Rewrite::Same(value);
};
let (merged, changed) = string_format::merge(tokens);
let merged = string_format::unsplit(merged);
if changed {
Rewrite::Changed(merged)
} else {
Rewrite::Same(merged)
}
}
fn slice(args: Vec<Value>) -> Rewrite {
let [array, start, length] = three(args);
let array = literal_array(array);
let mut length_value = None;
if let Value::Number(length) = &length {
let length = length.round();
if length <= 0.0 {
return Rewrite::Changed(call("emptyArray", Vec::new()));
}
length_value = Some(length);
}
if let (Value::Array(elements), Value::Number(start), Some(mut length)) =
(&array, &start, length_value)
{
let mut start = start.round();
if start < 0.0 {
length += start;
start = 0.0;
}
let from = (start as usize).min(elements.len());
let to = ((start + length).max(0.0) as usize).min(elements.len());
return Rewrite::Changed(Value::Array(elements[from..to.max(from)].to_vec()));
}
Rewrite::Same(call("slice", vec![array, start, length]))
}
fn char_at(args: Vec<Value>) -> Rewrite {
let [text, index] = two(args);
if let (Some(text), Value::Number(index)) = (literal_text(&text), &index) {
let position = index.max(0.0) as usize;
let character = text
.chars()
.nth(position)
.map(String::from)
.unwrap_or_default();
return Rewrite::Changed(Value::String(character));
}
Rewrite::Same(call("charAt", vec![text, index]))
}
fn concat(&self, args: Vec<Value>) -> Rewrite {
let [array, addition] = two(args);
let array = literal_array(array);
if let Value::Array(mut elements) = array {
let addition = literal_array(addition);
if let Value::Array(more) = addition {
elements.extend(more);
return Rewrite::Changed(Value::Array(elements));
}
if self.literal(&addition) {
elements.push(addition);
return Rewrite::Changed(Value::Array(elements));
}
return Rewrite::Same(call(
"appendToArray",
vec![Value::Array(elements), addition],
));
}
Rewrite::Same(call("appendToArray", vec![array, addition]))
}
fn exclude(&self, args: Vec<Value>) -> Rewrite {
let [array, removed] = two(args);
let array = literal_array(array);
let removed = literal_array(removed);
let Value::Array(mut elements) = array else {
return Rewrite::Same(call("removeFromArray", vec![array, removed]));
};
if self.literal(&removed) && !matches!(removed, Value::Array(_)) {
elements.retain(|element| !same(element, &removed));
if elements.iter().all(|element| self.literal(element)) {
return Rewrite::Changed(Value::Array(elements));
}
}
if let Value::Array(mut removals) = removed {
elements.retain(|element| !removals.iter().any(|other| same(element, other)));
if elements.iter().all(|element| self.literal(element))
&& removals.iter().all(|element| self.literal(element))
{
return Rewrite::Changed(Value::Array(elements));
}
removals.retain(|element| !self.literal(element));
return Rewrite::Same(call(
"removeFromArray",
vec![Value::Array(elements), Value::Array(removals)],
));
}
Rewrite::Same(call(
"removeFromArray",
vec![Value::Array(elements), removed],
))
}
fn mapped(args: Vec<Value>) -> Rewrite {
let [array, mapping] = two(args);
if matches!(&mapping, Value::Call { name, .. } if name == "currentArrayElement") {
return Rewrite::Changed(array);
}
Rewrite::Same(call("mappedArray", vec![array, mapping]))
}
fn filtered(&self, args: Vec<Value>) -> Rewrite {
let [array, predicate] = two(args);
let element_free = |value: &Value| !mentions_element(value);
if let Value::Call {
name,
args: operands,
} = &predicate
{
if name == "!=" && operands.len() == 2 {
for (element, other) in [(0, 1), (1, 0)] {
if matches!(&operands[element], Value::Call { name, .. } if name == "currentArrayElement")
&& element_free(&operands[other])
{
return Rewrite::Changed(call(
"removeFromArray",
vec![array, operands[other].clone()],
));
}
}
}
}
Rewrite::Same(call("filteredArray", vec![array, predicate]))
}
fn array_contains(&self, args: Vec<Value>) -> Rewrite {
let [array, needle] = two(args);
let array = literal_array(array);
let Value::Array(mut elements) = array else {
return Rewrite::Same(call("arrayContains", vec![array, needle]));
};
if elements.iter().any(|element| same(element, &needle)) {
return Rewrite::Changed(Value::Bool(true));
}
if self.literal(&needle) {
elements.retain(|element| !self.literal(element));
}
match elements.len() {
0 => Rewrite::Changed(Value::Bool(false)),
1 => Rewrite::Changed(call(
"==",
vec![needle, elements.pop().expect("one element")],
)),
_ => Rewrite::Same(call("arrayContains", vec![Value::Array(elements), needle])),
}
}
fn value_in_array(&self, args: Vec<Value>) -> Rewrite {
let [array, index] = two(args);
let array = literal_array(array);
if let Value::Number(position) = index {
if position < 0.0 {
return Rewrite::Changed(Value::Null);
}
let position = position.round();
if let Value::Array(mut elements) = array {
if (position as usize) < elements.len() {
return Rewrite::Changed(elements.swap_remove(position as usize));
}
if !elements.is_empty() || !self.strict {
return Rewrite::Changed(Value::Null);
}
return Rewrite::Same(call(
"valueInArray",
vec![Value::Array(elements), Value::Number(position)],
));
}
if position == 0.0 {
return Rewrite::Changed(call("firstOf", vec![array]));
}
return Rewrite::Same(call("valueInArray", vec![array, Value::Number(position)]));
}
Rewrite::Same(call("valueInArray", vec![array, index]))
}
fn component(args: Vec<Value>, axis: usize) -> Rewrite {
let [operand] = one(args);
match operand {
Value::Vector { x, y, z } => Rewrite::Changed([*x, *y, *z][axis].clone()),
Value::Call { name, mut args } if name == "vector" && args.len() == 3 => {
Rewrite::Changed(args.swap_remove(axis))
}
other => match number_components(&other) {
Some(components) => Rewrite::Changed(Value::Number(components[axis])),
None => Rewrite::Same(call(
["__xComponentOf__", "__yComponentOf__", "__zComponentOf__"][axis],
vec![other],
)),
},
}
}
pub(super) fn boolean(&self, value: &Value) -> bool {
match value {
Value::Bool(_) => true,
Value::Call { name, .. } => {
matches!(
name.as_str(),
"==" | "!=" | "<" | "<=" | ">" | ">=" | "and" | "or" | "not"
) || self
.compiler
.catalog
.entry(Kind::Value, name)
.and_then(|entry| entry.return_type())
.is_some_and(|kind| matches!(kind, "Boolean" | "BoolLiteral"))
}
_ => false,
}
}
fn float(&self, value: &Value) -> bool {
match value {
Value::Number(_) => true,
Value::Call { name, .. } => self
.compiler
.catalog
.entry(Kind::Value, name)
.and_then(|entry| entry.return_type())
.is_some_and(|kind| kind == "Number"),
_ => false,
}
}
fn truthy(&self, value: &Value) -> bool {
match value {
Value::Bool(value) => *value,
Value::Number(number) => *number != 0.0,
Value::Vector { x, y, z } => self.truthy(x) || self.truthy(y) || self.truthy(z),
Value::Array(elements) => elements.first().is_some_and(|first| self.truthy(first)),
Value::Enum { value_type, .. } => matches!(
value_type.as_str(),
"Hero" | "Map" | "Gamemode" | "Team" | "Button" | "Color"
),
Value::String(text) => has_literal_text(text),
Value::Call { name, args } if name == "customString" => match args.first() {
Some(Value::String(text)) => has_literal_text(text),
_ => false,
},
_ => false,
}
}
fn constant(&self, value: &Value) -> bool {
match value {
Value::Number(_) | Value::Enum { .. } => true,
Value::Vector { x, y, z } => self.constant(x) && self.constant(y) && self.constant(z),
Value::String(_) => !self.strict,
Value::Call { name, args } if name == "customString" => args.len() == 1 && !self.strict,
Value::Call { name, args } if matches!(name.as_str(), "vector" | "customColor") => {
args.iter().all(|arg| self.constant(arg))
}
_ => false,
}
}
fn literal(&self, value: &Value) -> bool {
match value {
Value::Number(_) | Value::Bool(_) | Value::Null | Value::Enum { .. } => true,
Value::Array(elements) => elements.iter().all(|element| self.literal(element)),
Value::Vector { x, y, z } => self.literal(x) && self.literal(y) && self.literal(z),
Value::String(_) => !self.strict,
Value::Call { name, args } if name == "customString" => args.len() == 1 && !self.strict,
_ => false,
}
}
}
enum Rewrite {
Same(Value),
Changed(Value),
}
fn head_of(value: &Value) -> String {
match value {
Value::Call { name, .. } => name.clone(),
Value::Number(_) => "#number".to_string(),
Value::Bool(_) => "#bool".to_string(),
Value::Null => "#null".to_string(),
Value::Array(_) => "#array".to_string(),
Value::Vector { .. } => "#vector".to_string(),
_ => "#other".to_string(),
}
}
fn call(name: &str, args: Vec<Value>) -> Value {
Value::Call {
name: name.to_string(),
args: args.into_iter().map(array_call).collect(),
}
}
fn not(value: Value) -> Value {
call("not", vec![value])
}
fn one(args: Vec<Value>) -> [Value; 1] {
args.try_into().expect("operator arity")
}
fn two(args: Vec<Value>) -> [Value; 2] {
args.try_into().expect("operator arity")
}
fn three(args: Vec<Value>) -> [Value; 3] {
args.try_into().expect("operator arity")
}
pub(super) fn literal_text(value: &Value) -> Option<&str> {
match value {
Value::String(text) => Some(text),
Value::Call { name, args } if name == "customString" && args.len() == 1 => match &args[0] {
Value::String(text) => Some(text),
_ => None,
},
_ => None,
}
}
fn opposite_of(team: &Value) -> Option<Value> {
let Value::Enum { value_type, value } = team else {
return None;
};
let value = match value.as_str() {
"TEAM_1" => "TEAM_2",
"TEAM_2" => "TEAM_1",
"ALL" => "ALL",
_ => return None,
};
(value_type == "Team").then(|| Value::Enum {
value_type: value_type.clone(),
value: value.to_string(),
})
}
fn is_number(value: &Value, expected: f64) -> bool {
matches!(value, Value::Number(number) if *number == expected)
}
fn zero_vector(value: &Value) -> bool {
number_components(value).is_some_and(|components| components == [0.0, 0.0, 0.0])
}
pub(super) fn number_components(value: &Value) -> Option<[f64; 3]> {
match value {
Value::Call { name, args } if name == "vector" => match args.as_slice() {
[Value::Number(x), Value::Number(y), Value::Number(z)] => Some([*x, *y, *z]),
_ => None,
},
Value::Vector { x, y, z } => match (&**x, &**y, &**z) {
(Value::Number(x), Value::Number(y), Value::Number(z)) => Some([*x, *y, *z]),
_ => None,
},
Value::Enum { value_type, value } if value_type == "Vector" => match value.as_str() {
"LEFT" => Some([1.0, 0.0, 0.0]),
"RIGHT" => Some([-1.0, 0.0, 0.0]),
"UP" => Some([0.0, 1.0, 0.0]),
"DOWN" => Some([0.0, -1.0, 0.0]),
"FORWARD" => Some([0.0, 0.0, 1.0]),
"BACKWARD" => Some([0.0, 0.0, -1.0]),
_ => None,
},
_ => None,
}
}
fn vector(components: [f64; 3]) -> Value {
let name = match components {
[1.0, 0.0, 0.0] => Some("LEFT"),
[-1.0, 0.0, 0.0] => Some("RIGHT"),
[0.0, 1.0, 0.0] => Some("UP"),
[0.0, -1.0, 0.0] => Some("DOWN"),
[0.0, 0.0, 1.0] => Some("FORWARD"),
[0.0, 0.0, -1.0] => Some("BACKWARD"),
_ => None,
};
if let Some(name) = name {
return Value::Enum {
value_type: "Vector".to_string(),
value: name.to_string(),
};
}
let [x, y, z] = components;
Value::Vector {
x: Box::new(Value::Number(x)),
y: Box::new(Value::Number(y)),
z: Box::new(Value::Number(z)),
}
}
pub(super) fn expand_log(args: Vec<Value>, fold_constants: bool) -> Value {
fn approximated(value: Value) -> Value {
call(
"multiply",
vec![
Value::Number(10000.0),
call(
"subtract",
vec![
call("raiseToPower", vec![value, Value::Number(0.0001)]),
Value::Number(1.0),
],
),
],
)
}
fn resolve(value: Value, fold_constants: bool) -> Value {
match (fold_constants, value) {
(true, Value::Number(number)) if number.ln().is_finite() => Value::Number(number.ln()),
(_, value) => approximated(value),
}
}
let mut args = args.into_iter();
let Some(number) = args.next() else {
unreachable!("log lowering enforces one or two arguments");
};
match args.next() {
None => resolve(number, fold_constants),
Some(Value::Number(base)) if base == std::f64::consts::E => resolve(number, fold_constants),
Some(base) => {
if fold_constants && let (Value::Number(a), Value::Number(b)) = (&number, &base) {
let folded = a.ln() / b.ln();
if folded.is_finite() && folded.abs() <= NUMBER_LIMIT {
return Value::Number(if folded == 0.0 { 0.0 } else { folded });
}
if !a.ln().is_finite() || !b.ln().is_finite() {
return call(
"divide",
vec![approximated(number.clone()), approximated(base.clone())],
);
}
}
call(
"divide",
vec![
resolve(number, fold_constants),
resolve(base, fold_constants),
],
)
}
}
}
fn fold(left: &Value, right: &Value, apply: fn(f64, f64) -> f64) -> Option<Value> {
let (Value::Number(a), Value::Number(b)) = (left, right) else {
return None;
};
let result = apply(*a, *b);
(result.abs() <= NUMBER_LIMIT).then_some(Value::Number(result))
}
fn vector_fold(left: &Value, right: &Value, apply: fn(f64, f64) -> f64) -> Option<Value> {
let (a, b) = (number_components(left)?, number_components(right)?);
Some(vector([
apply(a[0], b[0]),
apply(a[1], b[1]),
apply(a[2], b[2]),
]))
}
pub(super) fn falsy(value: &Value) -> bool {
match value {
Value::Null | Value::Bool(false) => true,
Value::Number(number) => *number == 0.0,
Value::Array(elements) => elements.first().is_none_or(falsy),
Value::String(text) => text.is_empty(),
Value::Call { name, args } => match name.as_str() {
"emptyArray" => true,
"customString" => matches!(args.first(), Some(Value::String(text)) if text.is_empty()),
_ => false,
},
_ => false,
}
}
fn has_literal_text(text: &str) -> bool {
let mut depth = 0;
for character in text.chars() {
match character {
'{' => depth += 1,
'}' if depth > 0 => depth -= 1,
_ if depth == 0 => return true,
_ => {}
}
}
false
}
fn negated(value: &Value) -> Option<&Value> {
match value {
Value::Call { name, args } if name == "not" && args.len() == 1 => args.first(),
_ => None,
}
}
fn negates(value: &Value, other: &Value) -> bool {
negated(value).is_some_and(|inner| same(inner, other))
}
pub(super) fn same(left: &Value, right: &Value) -> bool {
match (left, right) {
(Value::Number(a), Value::Number(b)) => a == b,
(Value::String(a), Value::String(b))
| (Value::LocalizedString(a), Value::LocalizedString(b))
| (Value::GlobalVariable(a), Value::GlobalVariable(b))
| (Value::Subroutine(a), Value::Subroutine(b)) => a == b,
(Value::Bool(a), Value::Bool(b)) => a == b,
(Value::Null, Value::Null) | (Value::EventPlayer, Value::EventPlayer) => true,
(Value::Array(a), Value::Array(b)) => {
a.len() == b.len() && a.iter().zip(b).all(|(a, b)| same(a, b))
}
(
Value::Vector {
x: ax,
y: ay,
z: az,
},
Value::Vector {
x: bx,
y: by,
z: bz,
},
) => same(ax, bx) && same(ay, by) && same(az, bz),
(
Value::Enum {
value_type: at,
value: av,
},
Value::Enum {
value_type: bt,
value: bv,
},
) => at == bt && av == bv,
(
Value::PlayerVariable {
player: ap,
variable: av,
},
Value::PlayerVariable {
player: bp,
variable: bv,
},
) => av == bv && same(ap, bp),
(Value::Call { name: an, args: aa }, Value::Call { name: bn, args: ba }) => {
an == bn
&& !RANDOM_CALLS.contains(&an.as_str())
&& aa.len() == ba.len()
&& aa.iter().zip(ba).all(|(a, b)| same(a, b))
}
_ => false,
}
}
fn mentions_element(value: &Value) -> bool {
match value {
Value::Call { name, args } => {
matches!(name.as_str(), "currentArrayElement" | "currentArrayIndex")
|| args.iter().any(mentions_element)
}
Value::Array(elements) => elements.iter().any(mentions_element),
Value::Vector { x, y, z } => [x, y, z].into_iter().any(|v| mentions_element(v)),
Value::PlayerVariable { player, .. } => mentions_element(player),
_ => false,
}
}
fn literal_array(value: Value) -> Value {
match value {
Value::Call { name, args } if name == "array" => Value::Array(args),
other => other,
}
}
fn modify_operation(name: &str) -> Option<ModifyOp> {
Some(match name {
"add" => ModifyOp::Add,
"subtract" => ModifyOp::Subtract,
"multiply" => ModifyOp::Multiply,
"divide" => ModifyOp::Divide,
"modulo" => ModifyOp::Modulo,
"raiseToPower" => ModifyOp::RaiseToPower,
"min" => ModifyOp::Min,
"max" => ModifyOp::Max,
"appendToArray" => ModifyOp::AppendToArray,
"removeFromArray" => ModifyOp::RemoveFromArrayByValue,
_ => return None,
})
}
fn self_operand(
name: &str,
args: &[Value],
is_target: impl FnOnce(&Value) -> bool,
) -> Option<(ModifyOp, Value)> {
let op = modify_operation(name)?;
if args.len() != 2 || !is_target(&args[0]) {
return None;
}
Some((op, args[1].clone()))
}
pub(super) fn self_modification(action: &Action) -> Option<Action> {
match action {
Action::SetGlobalVariable {
variable,
value: Value::Call { name, args },
} => {
let (op, value) = self_operand(
name,
args,
|target| matches!(target, Value::GlobalVariable(other) if other == variable),
)?;
Some(Action::ModifyGlobalVariable {
variable: variable.clone(),
op,
value,
})
}
Action::SetPlayerVariable {
player,
variable,
value: Value::Call { name, args },
} => {
let (op, value) = self_operand(name, args, |target| {
matches!(target, Value::PlayerVariable { player: other, variable: other_variable }
if other_variable == variable && same(player, other))
})?;
Some(Action::ModifyPlayerVariable {
player: player.clone(),
variable: variable.clone(),
op,
value,
})
}
Action::Call { name, args }
if matches!(
name.as_str(),
"setGlobalVariableAtIndex" | "setPlayerVariableAtIndex"
) && args.len() == 3 =>
{
let Value::Call {
name: operation,
args: operands,
} = &args[2]
else {
return None;
};
let (variable, index) = (&args[0], &args[1]);
let (_, value) = self_operand(operation, operands, |read| match read {
Value::Call { name, args } if name == "valueInArray" && args.len() == 2 => {
same(&args[0], variable) && same(&args[1], index)
}
Value::Call { name, args } if name == "firstOf" && args.len() == 1 => {
same(&args[0], variable) && is_number(index, 0.0)
}
_ => false,
})?;
Some(Action::Call {
name: name.replace("set", "modify"),
args: vec![
variable.clone(),
index.clone(),
call(operation, Vec::new()),
value,
],
})
}
_ => None,
}
}
fn array_call(value: Value) -> Value {
match value {
Value::Array(elements) if elements.is_empty() => Value::Call {
name: "emptyArray".to_string(),
args: elements,
},
Value::Array(elements) => Value::Call {
name: "array".to_string(),
args: elements,
},
other => other,
}
}
fn canonical(value: Value) -> Value {
match array_call(value) {
Value::Call { name, args } => Value::Call {
name,
args: args.into_iter().map(canonical).collect(),
},
other => other,
}
}