//! The expression transpiler. use std::{fmt::Display, string::ToString}; use super::util::MacroString; #[cfg(feature = "shulkerbox")] use enum_as_inner::EnumAsInner; #[cfg(feature = "shulkerbox")] use shulkerbox::prelude::{Command, Condition, Execute}; #[cfg(feature = "shulkerbox")] use super::{ error::{ IllegalIndexing, IllegalIndexingReason, MismatchedTypes, NotComptime, UnknownIdentifier, }, Scope, TranspileResult, Transpiler, VariableData, }; #[cfg(feature = "shulkerbox")] use crate::{ base::{self, source_file::SourceElement, Handler, VoidHandler}, lexical::token::MacroStringLiteralPart, syntax::syntax_tree::expression::{ Binary, BinaryOperator, Expression, PrefixOperator, Primary, }, transpile::{ error::{FunctionArgumentsNotAllowed, MissingValue}, TranspileError, }, }; #[cfg(feature = "shulkerbox")] use std::sync::Arc; /// Compile-time evaluated value #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq)] pub enum ComptimeValue { Boolean(bool), Integer(i64), String(String), MacroString(MacroString), } impl ComptimeValue { /// Returns the value as a string not containing a macro. #[must_use] pub fn to_string_no_macro(&self) -> Option { match self { Self::Boolean(boolean) => Some(boolean.to_string()), Self::Integer(int) => Some(int.to_string()), Self::String(string) => Some(string.clone()), Self::MacroString(_) => None, } } /// Returns the value as a [`MacroString`]. #[must_use] pub fn to_macro_string(&self) -> MacroString { match self { Self::Boolean(boolean) => MacroString::String(boolean.to_string()), Self::Integer(int) => MacroString::String(int.to_string()), Self::String(string) => MacroString::String(string.clone()), Self::MacroString(macro_string) => macro_string.clone(), } } } /// The type of an expression. #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub enum ValueType { Boolean, Integer, String, } impl Display for ValueType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Boolean => write!(f, "boolean"), Self::Integer => write!(f, "integer"), Self::String => write!(f, "string"), } } } #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ExpectedType { Boolean, Integer, String, Any, AnyOf(Vec), } impl ExpectedType { /// Add another expected type to the list of expected types. #[must_use] pub fn or(self, or: Self) -> Self { match self { Self::Boolean | Self::Integer | Self::String => match or { Self::Boolean | Self::Integer | Self::String => Self::AnyOf(vec![self, or]), Self::Any => Self::Any, Self::AnyOf(mut types) => { types.push(self); Self::AnyOf(types) } }, Self::Any => Self::Any, Self::AnyOf(mut types) => { types.push(or); Self::AnyOf(types) } } } } impl Display for ExpectedType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Boolean => write!(f, "boolean"), Self::Integer => write!(f, "integer"), Self::String => write!(f, "string"), Self::Any => write!(f, "any"), Self::AnyOf(types) => { write!(f, "any of [")?; for (i, r#type) in types.iter().enumerate() { if i != 0 { write!(f, ", ")?; } write!(f, "{type}")?; } write!(f, "]") } } } } impl From for ExpectedType { fn from(value: ValueType) -> Self { match value { ValueType::Boolean => Self::Boolean, ValueType::Integer => Self::Integer, ValueType::String => Self::String, } } } /// Location of data #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq)] pub enum DataLocation { ScoreboardValue { objective: String, target: String, }, Tag { tag_name: String, entity: String, }, Storage { storage_name: String, path: String, r#type: StorageType, }, } impl DataLocation { /// Returns the type of the data location. #[must_use] pub fn value_type(&self) -> ValueType { match self { Self::ScoreboardValue { .. } => ValueType::Integer, Self::Tag { .. } => ValueType::Boolean, Self::Storage { r#type, .. } => match r#type { StorageType::Boolean => ValueType::Boolean, StorageType::Byte | StorageType::Int | StorageType::Long => ValueType::Integer, StorageType::Double => todo!("Double storage type"), }, } } } /// The type of a storage. #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub enum StorageType { Boolean, Byte, Int, Long, Double, } impl StorageType { /// Returns the suffix of the storage type. #[must_use] pub fn suffix(&self) -> &'static str { match self { Self::Boolean | Self::Byte => "b", Self::Int => "", Self::Long => "l", Self::Double => "d", } } /// Returns the string representation of the storage type. #[must_use] pub fn as_str(&self) -> &'static str { match self { Self::Boolean | Self::Byte => "byte", Self::Int => "int", Self::Long => "long", Self::Double => "double", } } } /// Condition #[cfg(feature = "shulkerbox")] #[derive(Debug, Clone, PartialEq, Eq, EnumAsInner)] pub enum ExtendedCondition { /// Runtime condition Runtime(Condition), /// Compile-time condition Comptime(bool), } #[cfg(feature = "shulkerbox")] impl Expression { /// Returns whether the expression can yield a certain type. #[must_use] pub fn can_yield_type(&self, r#type: ValueType, scope: &Arc) -> bool { match self { Self::Primary(primary) => primary.can_yield_type(r#type, scope), Self::Binary(binary) => binary.can_yield_type(r#type, scope), } } /// Evaluate at compile-time. /// /// # Errors /// - If the expression is not compile-time evaluatable. pub fn comptime_eval( &self, scope: &Arc, handler: &impl Handler, ) -> Result { match self { Self::Primary(primary) => primary.comptime_eval(scope, handler), Self::Binary(binary) => binary.comptime_eval(scope, handler), } } } #[cfg(feature = "shulkerbox")] impl Primary { /// Returns whether the primary can yield a certain type. #[must_use] pub fn can_yield_type(&self, r#type: ValueType, scope: &Arc) -> bool { match self { Self::Boolean(_) => matches!(r#type, ValueType::Boolean), Self::Integer(_) => matches!(r#type, ValueType::Integer), Self::FunctionCall(_) => matches!(r#type, ValueType::Integer | ValueType::Boolean), Self::Identifier(ident) => { scope .get_variable(ident.span.str()) .is_some_and(|variable| match r#type { ValueType::Boolean => { matches!(variable.as_ref(), VariableData::BooleanStorage { .. }) } ValueType::Integer => { matches!(variable.as_ref(), VariableData::ScoreboardValue { .. }) } ValueType::String => false, }) } Self::Parenthesized(parenthesized) => { parenthesized.expression().can_yield_type(r#type, scope) } Self::Prefix(prefix) => match prefix.operator() { PrefixOperator::LogicalNot(_) => { matches!(r#type, ValueType::Boolean) && prefix.operand().can_yield_type(r#type, scope) } PrefixOperator::Negate(_) => { matches!(r#type, ValueType::Integer) && prefix.operand().can_yield_type(r#type, scope) } }, Self::Indexed(indexed) => { if let Self::Identifier(ident) = indexed.object().as_ref() { scope .get_variable(ident.span.str()) .is_some_and(|variable| match r#type { ValueType::Boolean => { matches!( variable.as_ref(), VariableData::Tag { .. } | VariableData::BooleanStorageArray { .. } ) } ValueType::Integer => { matches!( variable.as_ref(), VariableData::Scoreboard { .. } | VariableData::ScoreboardArray { .. } ) } ValueType::String => false, }) } else { false } } #[cfg_attr(not(feature = "lua"), expect(unused_variables))] Self::Lua(lua) => { cfg_if::cfg_if! { if #[cfg(feature = "lua")] { lua.eval(scope, &VoidHandler).is_ok_and(|(value, _)| match value { mlua::Value::Boolean(_) => matches!(r#type, ValueType::Boolean), mlua::Value::Integer(_) => matches!(r#type, ValueType::Integer), mlua::Value::String(_) => matches!(r#type, ValueType::String), _ => false, }) } else { false } } } Self::StringLiteral(_) | Self::MacroStringLiteral(_) => { matches!(r#type, ValueType::String | ValueType::Boolean) } } } /// Evaluate at compile-time. /// /// # Errors /// - If the expression is not compile-time evaluatable. pub fn comptime_eval( &self, scope: &Arc, handler: &impl Handler, ) -> Result { match self { Self::Boolean(boolean) => Ok(ComptimeValue::Boolean(boolean.value())), Self::Integer(int) => Ok(ComptimeValue::Integer(int.as_i64())), Self::StringLiteral(string_literal) => Ok(ComptimeValue::String( string_literal.str_content().to_string(), )), Self::Identifier(ident) => scope.get_variable(ident.span.str()).map_or_else( || { Err(NotComptime { expression: self.span(), }) }, |var| match var.as_ref() { VariableData::ComptimeValue { value, read_only: _, } => value.read().unwrap().clone().ok_or_else(|| NotComptime { expression: ident.span.clone(), }), _ => Err(NotComptime { expression: self.span(), }), }, ), Self::FunctionCall(_) | Self::Indexed(_) => Err(NotComptime { expression: self.span(), }), Self::Parenthesized(parenthesized) => { parenthesized.expression().comptime_eval(scope, handler) } Self::Prefix(prefix) => { prefix .operand() .comptime_eval(scope, handler) .and_then(|val| match (prefix.operator(), val) { (PrefixOperator::LogicalNot(_), ComptimeValue::Boolean(boolean)) => { Ok(ComptimeValue::Boolean(!boolean)) } (PrefixOperator::Negate(_), ComptimeValue::Integer(int)) => { Ok(ComptimeValue::Integer(-int)) } _ => Err(NotComptime { expression: prefix.span(), }), }) } Self::Lua(lua) => lua .eval_comptime(scope, &VoidHandler) .inspect_err(|err| { handler.receive(err.clone()); }) .map_err(|_| NotComptime { expression: lua.span(), }) .and_then(|val| val), Self::MacroStringLiteral(macro_string_literal) => { if macro_string_literal .parts() .iter() .any(|part| matches!(part, MacroStringLiteralPart::MacroUsage { .. })) { Ok(ComptimeValue::MacroString( macro_string_literal.clone().into(), )) } else { Ok(ComptimeValue::String(macro_string_literal.str_content())) } } } } } #[cfg(feature = "shulkerbox")] impl Binary { /// Returns whether the binary can yield a certain type. #[must_use] pub fn can_yield_type(&self, r#type: ValueType, scope: &Arc) -> bool { match self.operator() { BinaryOperator::Add(_) => { matches!(r#type, ValueType::Integer | ValueType::String) && self.left_operand().can_yield_type(r#type, scope) && self.right_operand().can_yield_type(r#type, scope) } BinaryOperator::Subtract(_) | BinaryOperator::Multiply(_) | BinaryOperator::Divide(_) | BinaryOperator::Modulo(_) => { matches!(r#type, ValueType::Integer) && self.left_operand().can_yield_type(r#type, scope) && self.right_operand().can_yield_type(r#type, scope) } BinaryOperator::Equal(..) | BinaryOperator::NotEqual(..) => { matches!(r#type, ValueType::Boolean) } BinaryOperator::GreaterThan(_) | BinaryOperator::GreaterThanOrEqual(..) | BinaryOperator::LessThan(_) | BinaryOperator::LessThanOrEqual(..) => { matches!(r#type, ValueType::Boolean) && self .left_operand() .can_yield_type(ValueType::Integer, scope) && self .right_operand() .can_yield_type(ValueType::Integer, scope) } BinaryOperator::LogicalAnd(..) | BinaryOperator::LogicalOr(..) => { matches!(r#type, ValueType::Boolean) && self.left_operand().can_yield_type(r#type, scope) && self.right_operand().can_yield_type(r#type, scope) } } } /// Evaluate at compile-time. /// /// # Errors /// - If the expression is not compile-time evaluatable. pub fn comptime_eval( &self, scope: &Arc, handler: &impl Handler, ) -> Result { let left = self.left_operand().comptime_eval(scope, handler)?; let right = self.right_operand().comptime_eval(scope, handler)?; match (left, right) { (ComptimeValue::Boolean(true), _) | (_, ComptimeValue::Boolean(true)) if matches!(self.operator(), BinaryOperator::LogicalOr(..)) && self .left_operand() .can_yield_type(ValueType::Boolean, scope) && self .right_operand() .can_yield_type(ValueType::Boolean, scope) => { Ok(ComptimeValue::Boolean(true)) } (ComptimeValue::Boolean(false), _) | (_, ComptimeValue::Boolean(false)) if matches!(self.operator(), BinaryOperator::LogicalAnd(..)) && self .left_operand() .can_yield_type(ValueType::Boolean, scope) && self .right_operand() .can_yield_type(ValueType::Boolean, scope) => { Ok(ComptimeValue::Boolean(false)) } (ComptimeValue::Boolean(left), ComptimeValue::Boolean(right)) => { match self.operator() { BinaryOperator::Equal(..) => Ok(ComptimeValue::Boolean(left == right)), BinaryOperator::NotEqual(..) => Ok(ComptimeValue::Boolean(left != right)), BinaryOperator::LogicalAnd(..) => Ok(ComptimeValue::Boolean(left && right)), BinaryOperator::LogicalOr(..) => Ok(ComptimeValue::Boolean(left || right)), _ => Err(NotComptime { expression: self.span(), }), } } (ComptimeValue::Integer(left), ComptimeValue::Integer(right)) => { match self.operator() { BinaryOperator::Add(..) => left.checked_add(right).map(ComptimeValue::Integer), BinaryOperator::Subtract(..) => { left.checked_sub(right).map(ComptimeValue::Integer) } BinaryOperator::Multiply(..) => { left.checked_mul(right).map(ComptimeValue::Integer) } BinaryOperator::Divide(..) => { left.checked_div(right).map(ComptimeValue::Integer) } BinaryOperator::Modulo(..) => { left.checked_rem(right).map(ComptimeValue::Integer) } BinaryOperator::Equal(..) => Some(ComptimeValue::Boolean(left == right)), BinaryOperator::NotEqual(..) => Some(ComptimeValue::Boolean(left != right)), BinaryOperator::LessThan(..) => Some(ComptimeValue::Boolean(left < right)), BinaryOperator::LessThanOrEqual(..) => { Some(ComptimeValue::Boolean(left <= right)) } BinaryOperator::GreaterThan(..) => Some(ComptimeValue::Boolean(left > right)), BinaryOperator::GreaterThanOrEqual(..) => { Some(ComptimeValue::Boolean(left >= right)) } _ => None, } .ok_or_else(|| NotComptime { expression: self.span(), }) } (ComptimeValue::String(left), ComptimeValue::String(right)) => match self.operator() { BinaryOperator::Add(..) => Ok(ComptimeValue::String(left + &right)), BinaryOperator::Equal(..) => Ok(ComptimeValue::Boolean(left == right)), BinaryOperator::NotEqual(..) => Ok(ComptimeValue::Boolean(left != right)), _ => Err(NotComptime { expression: self.span(), }), }, // TODO: also allow macro strings ( left @ ComptimeValue::String(_), right @ (ComptimeValue::Boolean(_) | ComptimeValue::Integer(_)), ) | ( left @ (ComptimeValue::Boolean(_) | ComptimeValue::Integer(_)), right @ ComptimeValue::String(_), ) => match self.operator() { BinaryOperator::Add(_) => Ok(ComptimeValue::String( left.to_string_no_macro().unwrap() + &right.to_string_no_macro().unwrap(), )), _ => Err(NotComptime { expression: self.span(), }), }, _ => Err(NotComptime { expression: self.span(), }), } } } #[cfg(feature = "shulkerbox")] impl Transpiler { /// Initializes a constant score. pub(crate) fn initialize_constant_score(&mut self, constant: i64) { if self.initialized_constant_scores.is_empty() { self.datapack .register_scoreboard("shu_constants", None::<&str>, None::<&str>); } if self.initialized_constant_scores.insert(constant) { self.setup_cmds.push(Command::Raw(format!( "scoreboard players set {constant} shu_constants {constant}" ))); } } /// Transpiles an expression. pub(super) fn transpile_expression( &mut self, expression: &Expression, target: &DataLocation, scope: &Arc, handler: &impl Handler, ) -> TranspileResult> { match expression { Expression::Primary(primary) => { self.transpile_primary_expression(primary, target, scope, handler) } Expression::Binary(binary) => { self.transpile_binary_expression(binary, target, scope, handler) } } } #[expect(clippy::too_many_lines)] pub(super) fn transpile_primary_expression( &mut self, primary: &Primary, target: &DataLocation, scope: &Arc, handler: &impl Handler, ) -> TranspileResult> { match primary { Primary::Boolean(boolean) => self.store_comptime_value( &ComptimeValue::Boolean(boolean.value()), target, boolean, handler, ), Primary::FunctionCall(func) => match target { DataLocation::ScoreboardValue { objective, target } => { let mut cmds = self.transpile_function_call(func, scope, handler)?; if let Some(call_cmd) = cmds.pop() { let modified = Command::Execute(Execute::Store( format!("result score {target} {objective}").into(), Box::new(Execute::Run(Box::new(call_cmd))), )); cmds.push(modified); } Ok(cmds) } DataLocation::Storage { storage_name, path, r#type, } => { let mut cmds = self.transpile_function_call(func, scope, handler)?; if let Some(call_cmd) = cmds.pop() { let result_success = if matches!(r#type, StorageType::Boolean) { "success" } else { "result" }; let modified = Command::Execute(Execute::Store( format!( "{result_success} storage {storage_name} {path} {type} 1.0d", r#type = r#type.as_str() ) .into(), Box::new(Execute::Run(Box::new(call_cmd))), )); cmds.push(modified); } Ok(cmds) } DataLocation::Tag { tag_name, entity } => { if func .arguments() .as_ref() .is_some_and(|args| !args.is_empty()) { let err = TranspileError::FunctionArgumentsNotAllowed( FunctionArgumentsNotAllowed { arguments: func.arguments().as_ref().unwrap().span(), message: "Assigning results to a tag does not support arguments." .into(), }, ); handler.receive(err.clone()); Err(err) } else { let prepare_cmd = Command::Raw(format!("tag {entity} remove {tag_name}")); let success_cmd = Command::Raw(format!("tag {entity} add {tag_name}")); let (function_location, _) = self.get_or_transpile_function( &func.identifier().span, None, scope, handler, )?; let if_cmd = Command::Execute(Execute::If( Condition::Atom(format!("function {function_location}").into()), Box::new(Execute::Run(Box::new(success_cmd))), None, )); Ok(vec![prepare_cmd, if_cmd]) } } }, Primary::Integer(int) => self.store_comptime_value( &ComptimeValue::Integer(int.as_i64()), target, int, handler, ), Primary::Parenthesized(parenthesized) => { self.transpile_expression(parenthesized.expression(), target, scope, handler) } Primary::Lua(lua) => { #[expect(clippy::option_if_let_else)] if let Ok(value) = lua.eval_comptime(scope, handler)? { self.store_comptime_value(&value, target, lua, handler) } else { let err = TranspileError::MissingValue(MissingValue { expression: lua.span(), }); handler.receive(err.clone()); Err(err) } } Primary::StringLiteral(_) | Primary::MacroStringLiteral(_) => { if matches!( target, DataLocation::Storage { r#type: StorageType::Boolean, .. } | DataLocation::Tag { .. } ) { let (mut cmds, cond) = self.transpile_primary_expression_as_condition(primary, scope, handler)?; let store_cmds = self.store_condition_success(cond, target, primary, handler)?; cmds.extend(store_cmds); Ok(cmds) } else { let err = TranspileError::MismatchedTypes(MismatchedTypes { expected_type: target.value_type().into(), expression: primary.span(), }); handler.receive(err.clone()); Err(err) } } Primary::Prefix(prefix) => match prefix.operator() { PrefixOperator::Negate(_) => match target { DataLocation::ScoreboardValue { objective, target: score_target, } => { let mut expr_cmds = self.transpile_primary_expression( prefix.operand(), target, scope, handler, )?; self.initialize_constant_score(-1); let negate_cmd = Command::Raw(format!("scoreboard players operation {score_target} {objective} *= -1 shu_constants")); expr_cmds.push(negate_cmd); Ok(expr_cmds) } DataLocation::Storage { storage_name, path, r#type, } if matches!( r#type, StorageType::Byte | StorageType::Int | StorageType::Long ) => { let (target_objective, mut targets) = self.get_temp_scoreboard_locations(1); let target_ident = targets.pop().expect("at least size 1"); let score_to_storage_cmd = Command::Execute(Execute::Store( format!( "result storage {storage_name} {path} {t} 1.0", t = r#type.as_str() ) .into(), Box::new(Execute::Run(Box::new(Command::Raw(format!( "scoreboard players get {target_ident} {target_objective} " ))))), )); let mut scoreboard_cmds = self.transpile_primary_expression( primary, &DataLocation::ScoreboardValue { objective: target_objective, target: target_ident, }, scope, handler, )?; scoreboard_cmds.push(score_to_storage_cmd); Ok(scoreboard_cmds) } _ => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expression: prefix.span(), expected_type: target.value_type().into(), }); handler.receive(err.clone()); Err(err) } }, PrefixOperator::LogicalNot(_) => { let (mut cmds, cond) = self.transpile_primary_expression_as_condition(primary, scope, handler)?; let store_cmds = self.store_condition_success(cond, target, primary, handler)?; cmds.extend(store_cmds); Ok(cmds) } }, Primary::Identifier(ident) => { let variable = scope.get_variable(ident.span.str()); if let Some(variable) = variable.as_deref() { let from = match variable { VariableData::BooleanStorage { storage_name, path } => { Ok(DataLocation::Storage { storage_name: storage_name.to_string(), path: path.to_string(), r#type: StorageType::Boolean, }) } VariableData::ScoreboardValue { objective, target } => { Ok(DataLocation::ScoreboardValue { objective: objective.to_string(), target: target.to_string(), }) } _ => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expected_type: target.value_type().into(), expression: primary.span(), }); handler.receive(err.clone()); Err(err) } }?; self.move_data(&from, target, primary, handler) } else { let err = TranspileError::UnknownIdentifier(UnknownIdentifier { identifier: ident.span.clone(), }); handler.receive(err.clone()); Err(err) } } Primary::Indexed(indexed) => { let ident = if let Primary::Identifier(ident) = indexed.object().as_ref() { Ok(ident) } else { let err = TranspileError::IllegalIndexing(IllegalIndexing { reason: IllegalIndexingReason::NotIdentifier, expression: indexed.object().span(), }); handler.receive(err.clone()); Err(err) }?; let variable = scope.get_variable(ident.span.str()); #[expect(clippy::option_if_let_else)] if let Some(variable) = variable.as_deref() { let from = match variable { VariableData::Scoreboard { objective } => { if let Ok(ComptimeValue::String(target)) = indexed.index().comptime_eval(scope, handler) { Ok(DataLocation::ScoreboardValue { objective: objective.to_string(), target, }) } else { let err = TranspileError::IllegalIndexing(IllegalIndexing { reason: IllegalIndexingReason::InvalidComptimeType { expected: ExpectedType::String, }, expression: indexed.index().span(), }); handler.receive(err.clone()); Err(err) } } VariableData::ScoreboardArray { objective, targets } => { if let Ok(ComptimeValue::Integer(index)) = indexed.index().comptime_eval(scope, handler) { if let Some(target) = usize::try_from(index) .ok() .and_then(|index| targets.get(index)) .map(ToString::to_string) { Ok(DataLocation::ScoreboardValue { objective: objective.to_string(), target, }) } else { let err = TranspileError::IllegalIndexing(IllegalIndexing { reason: IllegalIndexingReason::IndexOutOfBounds { length: targets.len(), index: usize::try_from(index).unwrap_or(usize::MAX), }, expression: indexed.index().span(), }); handler.receive(err.clone()); Err(err) } } else { let err = TranspileError::IllegalIndexing(IllegalIndexing { reason: IllegalIndexingReason::InvalidComptimeType { expected: ExpectedType::Integer, }, expression: indexed.index().span(), }); handler.receive(err.clone()); Err(err) } } VariableData::BooleanStorageArray { storage_name, paths, } => { if let Ok(ComptimeValue::Integer(index)) = indexed.index().comptime_eval(scope, handler) { if let Some(path) = usize::try_from(index) .ok() .and_then(|index| paths.get(index)) .map(ToString::to_string) { Ok(DataLocation::Storage { storage_name: storage_name.to_string(), path, r#type: StorageType::Boolean, }) } else { let err = TranspileError::IllegalIndexing(IllegalIndexing { reason: IllegalIndexingReason::IndexOutOfBounds { length: paths.len(), index: usize::try_from(index).unwrap_or(usize::MAX), }, expression: indexed.index().span(), }); handler.receive(err.clone()); Err(err) } } else { let err = TranspileError::IllegalIndexing(IllegalIndexing { reason: IllegalIndexingReason::InvalidComptimeType { expected: ExpectedType::Integer, }, expression: indexed.index().span(), }); handler.receive(err.clone()); Err(err) } } _ => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expected_type: target.value_type().into(), expression: primary.span(), }); handler.receive(err.clone()); Err(err) } }?; self.move_data(&from, target, primary, handler) } else { let err = TranspileError::UnknownIdentifier(UnknownIdentifier { identifier: ident.span.clone(), }); handler.receive(err.clone()); Err(err) } } } } pub(super) fn transpile_binary_expression( &mut self, binary: &Binary, target: &DataLocation, scope: &Arc, handler: &impl Handler, ) -> TranspileResult> { if let Ok(value) = binary.comptime_eval(scope, handler) { self.store_comptime_value(&value, target, binary, handler) } else { match binary.operator() { BinaryOperator::Add(_) | BinaryOperator::Subtract(_) | BinaryOperator::Multiply(_) | BinaryOperator::Divide(_) | BinaryOperator::Modulo(_) => { self.transpile_scoreboard_operation(binary, target, scope, handler) } BinaryOperator::Equal(..) | BinaryOperator::GreaterThan(_) | BinaryOperator::GreaterThanOrEqual(..) | BinaryOperator::LessThan(_) | BinaryOperator::LessThanOrEqual(..) | BinaryOperator::NotEqual(..) | BinaryOperator::LogicalAnd(..) | BinaryOperator::LogicalOr(..) => { let (mut cmds, cond) = self.transpile_binary_expression_as_condition(binary, scope, handler)?; let store_cmds = self.store_condition_success(cond, target, binary, handler)?; cmds.extend(store_cmds); Ok(cmds) } } } } pub(super) fn transpile_expression_as_condition( &mut self, expression: &Expression, scope: &Arc, handler: &impl Handler, ) -> TranspileResult<(Vec, ExtendedCondition)> { match expression { Expression::Primary(primary) => { self.transpile_primary_expression_as_condition(primary, scope, handler) } Expression::Binary(binary) => { self.transpile_binary_expression_as_condition(binary, scope, handler) } } } #[expect(clippy::too_many_lines)] fn transpile_primary_expression_as_condition( &mut self, primary: &Primary, scope: &Arc, handler: &impl Handler, ) -> TranspileResult<(Vec, ExtendedCondition)> { match primary { Primary::Boolean(boolean) => { Ok((Vec::new(), ExtendedCondition::Comptime(boolean.value()))) } Primary::Integer(_) => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expected_type: ExpectedType::Boolean, expression: primary.span(), }); handler.receive(err.clone()); Err(err) } Primary::StringLiteral(s) => Ok(( Vec::new(), ExtendedCondition::Runtime(Condition::Atom(s.str_content().to_string().into())), )), Primary::MacroStringLiteral(macro_string) => Ok(( Vec::new(), ExtendedCondition::Runtime(Condition::Atom(macro_string.into())), )), Primary::FunctionCall(func) => { if func .arguments() .as_ref() .is_some_and(|args| !args.is_empty()) { let err = TranspileError::FunctionArgumentsNotAllowed(FunctionArgumentsNotAllowed { arguments: func.arguments().as_ref().unwrap().span(), message: "Function calls as conditions do not support arguments." .into(), }); handler.receive(err.clone()); Err(err) } else { let (func_location, _) = self.get_or_transpile_function( &func.identifier().span, None, scope, handler, )?; Ok(( Vec::new(), ExtendedCondition::Runtime(Condition::Atom( format!("function {func_location}").into(), )), )) } } Primary::Identifier(ident) => { #[expect(clippy::option_if_let_else)] if let Some(variable) = scope.get_variable(ident.span.str()).as_deref() { match variable { VariableData::BooleanStorage { storage_name, path } => Ok(( Vec::new(), ExtendedCondition::Runtime(Condition::Atom( format!("data storage {storage_name} {{{path}: 1b}}").into(), )), )), VariableData::MacroParameter { macro_name, .. } => Ok(( Vec::new(), ExtendedCondition::Runtime(Condition::Atom( shulkerbox::util::MacroString::MacroString(vec![ shulkerbox::util::MacroStringPart::MacroUsage( macro_name.clone(), ), ]), )), )), _ => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expected_type: ExpectedType::Boolean, expression: primary.span(), }); handler.receive(err.clone()); Err(err) } } } else { let err = TranspileError::UnknownIdentifier(UnknownIdentifier { identifier: ident.span.clone(), }); handler.receive(err.clone()); Err(err) } } Primary::Parenthesized(parenthesized) => { self.transpile_expression_as_condition(parenthesized.expression(), scope, handler) } Primary::Indexed(indexed) => { let ident = if let Primary::Identifier(ident) = indexed.object().as_ref() { Ok(ident) } else { let err = TranspileError::IllegalIndexing(IllegalIndexing { reason: IllegalIndexingReason::NotIdentifier, expression: indexed.object().span(), }); handler.receive(err.clone()); Err(err) }?; #[expect(clippy::option_if_let_else)] if let Some(variable) = scope.get_variable(ident.span.str()).as_deref() { #[expect(clippy::single_match_else)] match variable { VariableData::BooleanStorageArray { storage_name, paths, } => { if let Ok(ComptimeValue::Integer(index)) = indexed.index().comptime_eval(scope, handler) { if let Some(path) = usize::try_from(index) .ok() .and_then(|index| paths.get(index)) .map(ToString::to_string) { Ok(( Vec::new(), ExtendedCondition::Runtime(Condition::Atom( format!("data storage {storage_name} {{{path}: 1b}}") .into(), )), )) } else { let err = TranspileError::IllegalIndexing(IllegalIndexing { reason: IllegalIndexingReason::IndexOutOfBounds { length: paths.len(), index: usize::try_from(index).unwrap_or(usize::MAX), }, expression: indexed.index().span(), }); handler.receive(err.clone()); Err(err) } } else { let err = TranspileError::IllegalIndexing(IllegalIndexing { reason: IllegalIndexingReason::InvalidComptimeType { expected: ExpectedType::Integer, }, expression: indexed.index().span(), }); handler.receive(err.clone()); Err(err) } } _ => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expected_type: ExpectedType::Boolean, expression: primary.span(), }); handler.receive(err.clone()); Err(err) } } } else { let err = TranspileError::UnknownIdentifier(UnknownIdentifier { identifier: ident.span.clone(), }); handler.receive(err.clone()); Err(err) } } Primary::Prefix(prefix) => match prefix.operator() { PrefixOperator::LogicalNot(_) => { let (cmds, cond) = self.transpile_primary_expression_as_condition( prefix.operand(), scope, handler, )?; Ok(( cmds, match cond { ExtendedCondition::Runtime(cond) => { ExtendedCondition::Runtime(Condition::Not(Box::new(cond))) } ExtendedCondition::Comptime(cond) => ExtendedCondition::Comptime(!cond), }, )) } PrefixOperator::Negate(_) => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expected_type: ExpectedType::Boolean, expression: primary.span(), }); handler.receive(err.clone()); Err(err) } }, Primary::Lua(lua) => match lua.eval_comptime(scope, handler)? { Ok(ComptimeValue::String(value)) => Ok(( Vec::new(), ExtendedCondition::Runtime(Condition::Atom(value.into())), )), Ok(ComptimeValue::MacroString(value)) => Ok(( Vec::new(), ExtendedCondition::Runtime(Condition::Atom(value.into())), )), Ok(ComptimeValue::Boolean(boolean)) => { Ok((Vec::new(), ExtendedCondition::Comptime(boolean))) } _ => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expected_type: ExpectedType::Boolean, expression: primary.span(), }); handler.receive(err.clone()); Err(err) } }, } } fn transpile_binary_expression_as_condition( &mut self, binary: &Binary, scope: &Arc, handler: &impl Handler, ) -> TranspileResult<(Vec, ExtendedCondition)> { match binary.operator() { BinaryOperator::Equal(..) | BinaryOperator::NotEqual(..) | BinaryOperator::GreaterThan(_) | BinaryOperator::GreaterThanOrEqual(..) | BinaryOperator::LessThan(_) | BinaryOperator::LessThanOrEqual(..) => self .transpile_comparison_operator(binary, scope, handler) .map(|(cmds, cond)| (cmds, ExtendedCondition::Runtime(cond))), BinaryOperator::LogicalAnd(..) | BinaryOperator::LogicalOr(..) => { self.transpile_logic_operator(binary, scope, handler) } _ => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expected_type: ExpectedType::Boolean, expression: binary.span(), }); handler.receive(err.clone()); Err(err) } } } #[expect(clippy::too_many_lines)] fn transpile_scoreboard_operation( &mut self, binary: &Binary, target: &DataLocation, scope: &Arc, handler: &impl Handler, ) -> TranspileResult> { let left = binary.left_operand(); let right = binary.right_operand(); let operator = binary.operator(); let (temp_objective, temp_locations) = self.get_temp_scoreboard_locations(2); let score_target_location = match target { DataLocation::ScoreboardValue { objective, target } => (objective, target), _ => (&temp_objective, &temp_locations[0]), }; let left_cmds = self.transpile_expression( left, &DataLocation::ScoreboardValue { objective: score_target_location.0.clone(), target: score_target_location.1.clone(), }, scope, handler, )?; let (right_cmds, rhs_score) = if let Ok(ComptimeValue::Integer(int)) = right.comptime_eval(scope, handler) { self.initialize_constant_score(int); ( Vec::new(), ("shu_constants", std::borrow::Cow::Owned(int.to_string())), ) } else { let right_cmds = self.transpile_expression( right, &DataLocation::ScoreboardValue { objective: temp_objective.clone(), target: temp_locations[1].clone(), }, scope, handler, )?; ( right_cmds, ( temp_objective.as_str(), std::borrow::Cow::Borrowed(&temp_locations[1]), ), ) }; let calc_cmds = { let (target_objective, target) = score_target_location; let source = rhs_score.1.as_ref(); let source_objective = rhs_score.0; let operation = match operator { BinaryOperator::Add(_) => "+=", BinaryOperator::Subtract(_) => "-=", BinaryOperator::Multiply(_) => "*=", BinaryOperator::Divide(_) => "/=", BinaryOperator::Modulo(_) => "%=", _ => unreachable!("This operator should not be handled here."), }; vec![Command::Raw(format!( "scoreboard players operation {target} {target_objective} {operation} {source} {source_objective}" ))] }; let transfer_cmd = match target { DataLocation::ScoreboardValue { .. } => None, DataLocation::Tag { .. } => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expected_type: ExpectedType::Boolean, expression: binary.span(), }); handler.receive(err.clone()); return Err(err); } DataLocation::Storage { storage_name, path, r#type, } => match r#type { StorageType::Byte | StorageType::Double | StorageType::Int | StorageType::Long => { Some(Command::Execute(Execute::Store( format!( "result storage {storage_name} {path} {t} 1.0", t = r#type.as_str() ) .into(), Box::new(Execute::Run(Box::new(Command::Raw(format!( "scoreboard players get {target} {objective}", objective = score_target_location.0, target = score_target_location.1 ))))), ))) } StorageType::Boolean => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expected_type: ExpectedType::Boolean, expression: binary.span(), }); handler.receive(err.clone()); return Err(err); } }, }; Ok(left_cmds .into_iter() .chain(right_cmds) .chain(calc_cmds) .chain(transfer_cmd) .collect()) } fn transpile_comparison_operator( &mut self, binary: &Binary, scope: &Arc, handler: &impl Handler, ) -> TranspileResult<(Vec, Condition)> { let invert = matches!(binary.operator(), BinaryOperator::NotEqual(..)); // TODO: evaluate comptime values and compare using `matches` and integer ranges let operator = match binary.operator() { BinaryOperator::Equal(..) | BinaryOperator::NotEqual(..) => "=", BinaryOperator::GreaterThan(_) => ">", BinaryOperator::GreaterThanOrEqual(..) => ">=", BinaryOperator::LessThan(_) => "<", BinaryOperator::LessThanOrEqual(..) => "<=", _ => unreachable!("This function should only be called for comparison operators."), }; let (temp_objective, mut temp_locations) = self.get_temp_scoreboard_locations(2); let condition = Condition::Atom( format!( "score {target} {temp_objective} {operator} {source} {temp_objective}", target = temp_locations[0], source = temp_locations[1] ) .into(), ); let left_cmds = self.transpile_expression( binary.left_operand(), &DataLocation::ScoreboardValue { objective: temp_objective.clone(), target: std::mem::take(&mut temp_locations[0]), }, scope, handler, )?; let right_cmds = self.transpile_expression( binary.right_operand(), &DataLocation::ScoreboardValue { objective: temp_objective, target: std::mem::take(&mut temp_locations[1]), }, scope, handler, )?; Ok(( left_cmds.into_iter().chain(right_cmds).collect(), if invert { Condition::Not(Box::new(condition)) } else { condition }, )) } fn transpile_logic_operator( &mut self, binary: &Binary, scope: &Arc, handler: &impl Handler, ) -> TranspileResult<(Vec, ExtendedCondition)> { let left = binary.left_operand().as_ref(); let right = binary.right_operand().as_ref(); let (left_cmds, left_cond) = self.transpile_expression_as_condition(left, scope, handler)?; let (right_cmds, right_cond) = self.transpile_expression_as_condition(right, scope, handler)?; match (binary.operator(), left_cond, right_cond) { (BinaryOperator::LogicalAnd(..), ExtendedCondition::Comptime(true), other) | (BinaryOperator::LogicalOr(..), ExtendedCondition::Comptime(false), other) => { Ok((right_cmds, other)) } (BinaryOperator::LogicalAnd(..), other, ExtendedCondition::Comptime(true)) | (BinaryOperator::LogicalOr(..), other, ExtendedCondition::Comptime(false)) => { Ok((left_cmds, other)) } (BinaryOperator::LogicalAnd(..), ExtendedCondition::Comptime(false), _) | (BinaryOperator::LogicalAnd(..), _, ExtendedCondition::Comptime(false)) => { Ok((Vec::new(), ExtendedCondition::Comptime(false))) } (BinaryOperator::LogicalOr(..), ExtendedCondition::Comptime(true), _) | (BinaryOperator::LogicalOr(..), _, ExtendedCondition::Comptime(true)) => { Ok((Vec::new(), ExtendedCondition::Comptime(true))) } ( BinaryOperator::LogicalAnd(..), ExtendedCondition::Runtime(left_cond), ExtendedCondition::Runtime(right_cond), ) => Ok(( left_cmds.into_iter().chain(right_cmds).collect(), ExtendedCondition::Runtime(Condition::And( Box::new(left_cond), Box::new(right_cond), )), )), ( BinaryOperator::LogicalOr(..), ExtendedCondition::Runtime(left_cond), ExtendedCondition::Runtime(right_cond), ) => Ok(( left_cmds.into_iter().chain(right_cmds).collect(), ExtendedCondition::Runtime(Condition::Or( Box::new(left_cond), Box::new(right_cond), )), )), _ => unreachable!("This function should only be called for logical operators."), } } fn store_comptime_value( &mut self, value: &ComptimeValue, target: &DataLocation, source: &impl SourceElement, handler: &impl Handler, ) -> TranspileResult> { match value { ComptimeValue::Integer(value) => match target { DataLocation::ScoreboardValue { objective, target } => Ok(vec![Command::Raw( format!("scoreboard players set {target} {objective} {value}"), )]), DataLocation::Tag { .. } => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expected_type: ExpectedType::Boolean, expression: source.span(), }); handler.receive(err.clone()); Err(err) } DataLocation::Storage { storage_name, path, r#type, } => { if matches!( r#type, StorageType::Byte | StorageType::Double | StorageType::Int | StorageType::Long ) { Ok(vec![Command::Raw(format!( "data modify storage {storage_name} {path} set value {value}{suffix}", suffix = r#type.suffix(), ))]) } else { let err = TranspileError::MismatchedTypes(MismatchedTypes { expression: source.span(), expected_type: target.value_type().into(), }); handler.receive(err.clone()); Err(err) } } }, &ComptimeValue::Boolean(value) => match target { DataLocation::Tag { tag_name, entity } => Ok(vec![Command::Raw(format!( "tag {entity} {op} {tag_name}", op = if value { "add" } else { "remove" } ))]), DataLocation::Storage { storage_name, path, r#type, } => { if matches!(r#type, StorageType::Boolean) { Ok(vec![Command::Raw(format!( "data modify storage {storage_name} {path} set value {value}{suffix}", value = u8::from(value), suffix = r#type.suffix(), ))]) } else { let err = TranspileError::MismatchedTypes(MismatchedTypes { expression: source.span(), expected_type: target.value_type().into(), }); handler.receive(err.clone()); Err(err) } } DataLocation::ScoreboardValue { objective, target } => { Ok(vec![Command::Raw(format!( "scoreboard players set {target} {objective} {value}", value = u8::from(value) ))]) } }, ComptimeValue::String(value) => self.store_comptime_value( &ComptimeValue::MacroString(value.clone().into()), target, source, handler, ), ComptimeValue::MacroString(value) => { match target { DataLocation::Storage { r#type: StorageType::Boolean, .. } | DataLocation::Tag { .. } => self.store_condition_success( ExtendedCondition::Runtime(Condition::Atom(value.clone().into())), target, source, handler, ), // DataLocation::Storage { storage_name, path, r#type: StorageType::String } => todo!("implement storage string") _ => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expected_type: target.value_type().into(), expression: source.span(), }); handler.receive(err.clone()); Err(err) } } } } } #[expect(clippy::unused_self, clippy::needless_pass_by_ref_mut)] fn store_condition_success( &mut self, cond: ExtendedCondition, target: &DataLocation, source: &impl SourceElement, handler: &impl Handler, ) -> TranspileResult> { let (success_cmd, else_cmd) = match target { DataLocation::ScoreboardValue { objective, target } => ( format!("scoreboard players set {target} {objective} 1"), format!("scoreboard players set {target} {objective} 0"), ), DataLocation::Storage { storage_name, path, r#type, } => { if matches!(r#type, StorageType::Boolean) { ( format!("data modify storage {storage_name} {path} set value 1b"), format!("data modify storage {storage_name} {path} set value 0b"), ) } else { let err = TranspileError::MismatchedTypes(MismatchedTypes { expected_type: ExpectedType::Boolean, expression: source.span(), }); handler.receive(err.clone()); return Err(err); } } DataLocation::Tag { tag_name, entity } => ( format!("tag {entity} add {tag_name}"), format!("tag {entity} remove {tag_name}"), ), }; let cmd = match cond { ExtendedCondition::Runtime(cond) => Command::Execute(Execute::If( cond, Box::new(Execute::Run(Box::new(Command::Raw(success_cmd)))), Some(Box::new(Execute::Run(Box::new(Command::Raw(else_cmd))))), )), ExtendedCondition::Comptime(cond) => { if cond { Command::Raw(success_cmd) } else { Command::Raw(else_cmd) } } }; Ok(vec![cmd]) } /// Get temporary scoreboard locations. pub(super) fn get_temp_scoreboard_locations(&mut self, amount: usize) -> (String, Vec) { let objective = "shu_temp_".to_string() + &chksum_md5::hash(&self.main_namespace_name).to_hex_lowercase(); self.datapack .register_scoreboard(&objective, None::<&str>, None::<&str>); let targets = (0..amount) .map(|i| { chksum_md5::hash(format!( "{namespace}\0{j}", namespace = self.main_namespace_name, j = i + self.temp_counter )) .to_hex_lowercase() .split_off(16) }) .collect(); self.temp_counter = self.temp_counter.wrapping_add(amount); (objective, targets) } /// Get temporary storage locations. pub(super) fn get_temp_storage_locations(&mut self, amount: usize) -> (String, Vec) { let storage_name = "shulkerscript:temp_".to_string() + &chksum_md5::hash(&self.main_namespace_name).to_hex_lowercase(); let paths = (0..amount) .map(|i| { chksum_md5::hash(format!( "{namespace}\0{j}", namespace = self.main_namespace_name, j = i + self.temp_counter )) .to_hex_lowercase() .split_off(16) }) .collect(); self.temp_counter = self.temp_counter.wrapping_add(amount); (storage_name, paths) } }