From 8ae065f5822a0bffb7bdd34379c34726f7adaf2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20H=C3=B6lting?= <87192362+moritz-hoelting@users.noreply.github.com> Date: Thu, 6 Mar 2025 14:40:26 +0100 Subject: [PATCH] implement basic assignment transpilation --- src/semantic/mod.rs | 4 +- src/transpile/error.rs | 55 ++++++- src/transpile/expression.rs | 304 ++++++++++++++++++++++++++++++++++++ src/transpile/mod.rs | 15 +- src/transpile/transpiler.rs | 276 ++++++++------------------------ src/transpile/variables.rs | 271 ++++++++++++++++++++++++++++++-- 6 files changed, 690 insertions(+), 235 deletions(-) create mode 100644 src/transpile/expression.rs diff --git a/src/semantic/mod.rs b/src/semantic/mod.rs index ff0d10c..91f6e4f 100644 --- a/src/semantic/mod.rs +++ b/src/semantic/mod.rs @@ -545,7 +545,9 @@ impl VariableDeclaration { ), KeywordKind::Bool => !matches!( assignment.expression(), - Expression::Primary(Primary::Boolean(_) | Primary::Lua(_)) + Expression::Primary( + Primary::Boolean(_) | Primary::Lua(_) | Primary::FunctionCall(_) + ) ), _ => false, }; diff --git a/src/transpile/error.rs b/src/transpile/error.rs index 4b01afc..0a26c21 100644 --- a/src/transpile/error.rs +++ b/src/transpile/error.rs @@ -12,7 +12,7 @@ use crate::{ semantic::error::{ConflictingFunctionNames, InvalidFunctionArguments, UnexpectedExpression}, }; -use super::FunctionData; +use super::{expression::ValueType, FunctionData}; /// Errors that can occur during transpilation. #[allow(clippy::module_name_repetitions, missing_docs)] @@ -32,6 +32,10 @@ pub enum TranspileError { InvalidFunctionArguments(#[from] InvalidFunctionArguments), #[error(transparent)] IllegalAnnotationContent(#[from] IllegalAnnotationContent), + #[error(transparent)] + MismatchedTypes(#[from] MismatchedTypes), + #[error(transparent)] + FunctionArgumentsNotAllowed(#[from] FunctionArgumentsNotAllowed), } /// The result of a transpilation operation. @@ -56,12 +60,10 @@ impl MissingFunctionDeclaration { let own_name = identifier_span.str(); let alternatives = scope - .get_variables() - .read() - .unwrap() + .get_all_variables() .iter() .filter_map(|(name, value)| { - let super::variables::VariableType::Function { + let super::variables::VariableData::Function { function_data: data, .. } = value.as_ref() @@ -182,3 +184,46 @@ impl Display for IllegalAnnotationContent { } impl std::error::Error for IllegalAnnotationContent {} + +/// An error that occurs when an expression can not evaluate to the wanted type. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MismatchedTypes { + pub expression: Span, + pub expected_type: ValueType, +} + +impl Display for MismatchedTypes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let message = format!("expression can not evaluate to type {}", self.expected_type); + write!(f, "{}", Message::new(Severity::Error, message))?; + + write!( + f, + "\n{}", + SourceCodeDisplay::new(&self.expression, Option::::None) + ) + } +} + +impl std::error::Error for MismatchedTypes {} + +/// An error that occurs when an expression can not evaluate to the wanted type. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FunctionArgumentsNotAllowed { + pub arguments: Span, + pub message: String, +} + +impl Display for FunctionArgumentsNotAllowed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", Message::new(Severity::Error, &self.message))?; + + write!( + f, + "\n{}", + SourceCodeDisplay::new(&self.arguments, Option::::None) + ) + } +} + +impl std::error::Error for FunctionArgumentsNotAllowed {} diff --git a/src/transpile/expression.rs b/src/transpile/expression.rs new file mode 100644 index 0000000..37eadc1 --- /dev/null +++ b/src/transpile/expression.rs @@ -0,0 +1,304 @@ +//! The expression transpiler. + +use std::fmt::Display; + +use crate::syntax::syntax_tree::expression::{Expression, Primary}; + +#[cfg(feature = "shulkerbox")] +use std::sync::Arc; + +#[cfg(feature = "shulkerbox")] +use shulkerbox::prelude::{Command, Condition, Execute}; + +#[cfg(feature = "shulkerbox")] +use super::{error::MismatchedTypes, TranspileResult, Transpiler}; +#[cfg(feature = "shulkerbox")] +use crate::{ + base::{self, source_file::SourceElement, Handler}, + semantic::error::UnexpectedExpression, + transpile::{error::FunctionArgumentsNotAllowed, TranspileError}, +}; + +/// The type of an expression. +#[allow(missing_docs)] +#[derive(Debug, Clone, PartialEq, Eq, Copy)] +pub enum ValueType { + ScoreboardValue, + Tag, + NumberStorage, + BooleanStorage, +} + +impl Display for ValueType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ScoreboardValue => write!(f, "scoreboard value"), + Self::Tag => write!(f, "tag"), + Self::BooleanStorage => write!(f, "boolean storage"), + Self::NumberStorage => write!(f, "number storage"), + } + } +} + +/// 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, + }, +} + +/// 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", + } + } +} + +impl Expression { + /// Returns whether the expression can yield a certain type. + #[must_use] + pub fn can_yield_type(&self, r#type: ValueType) -> bool { + match self { + Self::Primary(primary) => primary.can_yield_type(r#type), + } + } +} + +impl Primary { + /// Returns whether the primary can yield a certain type. + #[must_use] + pub fn can_yield_type(&self, r#type: ValueType) -> bool { + match self { + Self::Boolean(_) => matches!(r#type, ValueType::Tag | ValueType::BooleanStorage), + Self::Integer(_) => matches!(r#type, ValueType::ScoreboardValue), + Self::FunctionCall(_) => matches!( + r#type, + ValueType::ScoreboardValue | ValueType::Tag | ValueType::BooleanStorage + ), + // TODO: Add support for Lua. + #[expect(clippy::match_same_arms)] + Self::Lua(_) => false, + Self::StringLiteral(_) | Self::MacroStringLiteral(_) => false, + } + } +} + +#[cfg(feature = "shulkerbox")] +impl Transpiler { + /// 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) + } + } + } + + #[expect(clippy::too_many_lines)] + fn transpile_primary_expression( + &mut self, + primary: &Primary, + target: &DataLocation, + scope: &Arc, + handler: &impl Handler, + ) -> TranspileResult> { + match primary { + Primary::Boolean(boolean) => match target { + DataLocation::Tag { tag_name, entity } => { + let cmd = format!( + "tag {target} {op} {tag}", + target = entity, + op = if boolean.value() { "add" } else { "remove" }, + tag = tag_name + ); + Ok(vec![shulkerbox::prelude::Command::Raw(cmd)]) + } + DataLocation::Storage { + storage_name, + path, + r#type, + } => { + let cmd = format!( + "data modify storage {storage} {path} set value {value}{suffix}", + storage = storage_name, + path = path, + value = if boolean.value() { "1" } else { "0" }, + suffix = r#type.suffix() + ); + Ok(vec![shulkerbox::prelude::Command::Raw(cmd)]) + } + DataLocation::ScoreboardValue { objective, target } => { + let cmd = format!( + "scoreboard players set {target} {objective} {value}", + target = target, + objective = objective, + value = if boolean.value() { "1" } else { "0" } + ); + Ok(vec![shulkerbox::prelude::Command::Raw(cmd)]) + } + }, + Primary::FunctionCall(func) => match target { + DataLocation::ScoreboardValue { objective, target } => { + let call_cmd = self.transpile_function_call(func, scope, handler)?; + Ok(vec![Command::Execute(Execute::Store( + format!("result score {target} {objective}").into(), + Box::new(Execute::Run(Box::new(call_cmd))), + ))]) + } + DataLocation::Storage { + storage_name, + path, + r#type, + } => { + let call_cmd = self.transpile_function_call(func, scope, handler)?; + let result_success = if matches!(r#type, StorageType::Boolean) { + "success" + } else { + "result" + }; + Ok(vec![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))), + ))]) + } + DataLocation::Tag { tag_name, entity } => { + if func + .arguments() + .as_ref() + .is_some_and(|args| !args.is_empty()) + { + Err(TranspileError::FunctionArgumentsNotAllowed( + FunctionArgumentsNotAllowed { + arguments: func.arguments().as_ref().unwrap().span(), + message: "Assigning results to a tag does not support arguments." + .into(), + }, + )) + } 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) => match target { + DataLocation::ScoreboardValue { objective, target } => { + Ok(vec![Command::Raw(format!( + "scoreboard players set {target} {objective} {value}", + target = target, + objective = objective, + value = int.as_i64() + ))]) + } + DataLocation::Tag { .. } => Err(TranspileError::MismatchedTypes(MismatchedTypes { + expected_type: ValueType::Tag, + expression: primary.span(), + })), + 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} {path} set value {value}{suffix}", + storage = storage_name, + path = path, + value = int.as_i64(), + suffix = r#type.suffix() + ))]) + } else { + Err(TranspileError::MismatchedTypes(MismatchedTypes { + expression: primary.span(), + expected_type: ValueType::NumberStorage, + })) + } + } + }, + Primary::Lua(_) => { + // TODO: Add support for Lua. + Err(TranspileError::UnexpectedExpression(UnexpectedExpression( + Expression::Primary(primary.clone()), + ))) + } + Primary::StringLiteral(_) | Primary::MacroStringLiteral(_) => { + Err(TranspileError::MismatchedTypes(MismatchedTypes { + expected_type: match target { + DataLocation::ScoreboardValue { .. } => ValueType::ScoreboardValue, + DataLocation::Tag { .. } => ValueType::Tag, + DataLocation::Storage { .. } => ValueType::NumberStorage, + }, + expression: primary.span(), + })) + } + } + } +} diff --git a/src/transpile/mod.rs b/src/transpile/mod.rs index ac21ba0..3d899db 100644 --- a/src/transpile/mod.rs +++ b/src/transpile/mod.rs @@ -15,6 +15,8 @@ use crate::{ pub mod conversions; mod error; +pub mod expression; + #[doc(inline)] #[allow(clippy::module_name_repetitions)] pub use error::{TranspileError, TranspileResult}; @@ -26,13 +28,17 @@ use strum::EnumIs; #[cfg(feature = "shulkerbox")] #[cfg_attr(feature = "shulkerbox", doc(inline))] pub use transpiler::Transpiler; + #[cfg(feature = "shulkerbox")] mod variables; +#[cfg(feature = "shulkerbox")] +pub use variables::{Scope, VariableData}; pub mod util; +/// Data of a function. #[derive(Clone, PartialEq, Eq)] -pub(super) struct FunctionData { +pub struct FunctionData { pub(super) namespace: String, pub(super) identifier_span: Span, pub(super) parameters: Vec, @@ -75,13 +81,12 @@ impl From> for TranspileAnnotationValue { impl Debug for FunctionData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut s = f.debug_struct("FunctionData"); - struct HiddenList; impl Debug for HiddenList { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut list = f.debug_list(); - list.finish_non_exhaustive() + list.entry(&..); + list.finish() } } @@ -114,6 +119,8 @@ impl Debug for FunctionData { } } + let mut s = f.debug_struct("FunctionData"); + s.field("namespace", &self.namespace); s.field("identifier", &self.identifier_span.str()); s.field("public", &self.public); diff --git a/src/transpile/transpiler.rs b/src/transpile/transpiler.rs index 80b0bfe..a0fc9c5 100644 --- a/src/transpile/transpiler.rs +++ b/src/transpile/transpiler.rs @@ -15,7 +15,6 @@ use crate::{ source_file::{SourceElement, Span}, Handler, }, - lexical::token::KeywordKind, semantic::error::{ConflictingFunctionNames, InvalidFunctionArguments, UnexpectedExpression}, syntax::syntax_tree::{ declaration::{Declaration, ImportItems}, @@ -23,7 +22,7 @@ use crate::{ program::{Namespace, ProgramFile}, statement::{ execute_block::{Conditional, Else, ExecuteBlock, ExecuteBlockHead, ExecuteBlockTail}, - SemicolonStatement, SingleVariableDeclaration, Statement, VariableDeclaration, + SemicolonStatement, Statement, }, AnnotationAssignment, }, @@ -32,14 +31,14 @@ use crate::{ use super::{ error::{TranspileError, TranspileResult}, - variables::{Scope, VariableType}, + variables::{Scope, VariableData}, FunctionData, TranspileAnnotationValue, }; /// A transpiler for `Shulkerscript`. #[derive(Debug)] pub struct Transpiler { - datapack: shulkerbox::datapack::Datapack, + pub(super) datapack: shulkerbox::datapack::Datapack, /// Top-level [`Scope`] for each program identifier scopes: BTreeMap>>, /// Key: (program identifier, function name) @@ -184,7 +183,7 @@ impl Transpiler { }; scope.set_variable( &name, - VariableType::Function { + VariableData::Function { function_data: function_data.clone(), path: OnceLock::new(), }, @@ -240,7 +239,7 @@ impl Transpiler { /// Returns the location of the function or None if the function does not exist. #[allow(clippy::significant_drop_tightening)] #[tracing::instrument(level = "trace", skip(self, handler))] - fn get_or_transpile_function( + pub(super) fn get_or_transpile_function( &mut self, identifier_span: &Span, arguments: Option<&[&Expression]>, @@ -258,7 +257,7 @@ impl Transpiler { .expect("called function should be in scope") .as_ref() { - VariableType::Function { path, .. } => Some(path.get().is_some()), + VariableData::Function { path, .. } => Some(path.get().is_some()), _ => None, } .expect("called variable should be of type function"); @@ -282,7 +281,7 @@ impl Transpiler { error })?; - let VariableType::Function { + let VariableData::Function { function_data, path: function_path, } = function_data.as_ref() @@ -296,7 +295,7 @@ impl Transpiler { let function_scope = Scope::with_parent(scope); for (i, param) in function_data.parameters.iter().enumerate() { - function_scope.set_variable(param, VariableType::FunctionArgument { index: i }); + function_scope.set_variable(param, VariableData::FunctionArgument { index: i }); } let statements = function_data.statements.clone(); @@ -441,11 +440,11 @@ impl Transpiler { let mut errors = Vec::new(); let commands = statements .iter() - .filter_map(|statement| { + .flat_map(|statement| { self.transpile_statement(statement, program_identifier, scope, handler) .unwrap_or_else(|err| { errors.push(err); - None + Vec::new() }) }) .collect(); @@ -463,15 +462,15 @@ impl Transpiler { program_identifier: &str, scope: &Arc, handler: &impl Handler, - ) -> TranspileResult> { + ) -> TranspileResult> { match statement { Statement::LiteralCommand(literal_command) => { - Ok(Some(literal_command.clean_command().into())) + Ok(vec![literal_command.clean_command().into()]) } Statement::Run(run) => match run.expression() { - Expression::Primary(Primary::FunctionCall(func)) => { - self.transpile_function_call(func, scope, handler).map(Some) - } + Expression::Primary(Primary::FunctionCall(func)) => self + .transpile_function_call(func, scope, handler) + .map(|cmd| vec![cmd]), Expression::Primary(Primary::Integer(num)) => { let error = TranspileError::UnexpectedExpression(UnexpectedExpression( Expression::Primary(Primary::Integer(num.clone())), @@ -487,25 +486,27 @@ impl Transpiler { Err(error) } Expression::Primary(Primary::StringLiteral(string)) => { - Ok(Some(Command::Raw(string.str_content().to_string()))) + Ok(vec![Command::Raw(string.str_content().to_string())]) } Expression::Primary(Primary::MacroStringLiteral(string)) => { - Ok(Some(Command::UsesMacro(string.into()))) - } - Expression::Primary(Primary::Lua(code)) => { - Ok(code.eval_string(handler)?.map(Command::Raw)) + Ok(vec![Command::UsesMacro(string.into())]) } + Expression::Primary(Primary::Lua(code)) => Ok(code + .eval_string(handler)? + .map_or_else(Vec::new, |cmd| vec![Command::Raw(cmd)])), }, Statement::Block(_) => { unreachable!("Only literal commands are allowed in functions at this time.") } Statement::ExecuteBlock(execute) => { let child_scope = Scope::with_parent(scope); - self.transpile_execute_block(execute, program_identifier, &child_scope, handler) + Ok(self + .transpile_execute_block(execute, program_identifier, &child_scope, handler)? + .map_or_else(Vec::new, |cmd| vec![cmd])) } Statement::DocComment(doccomment) => { let content = doccomment.content(); - Ok(Some(Command::Comment(content.to_string()))) + Ok(vec![Command::Comment(content.to_string())]) } Statement::Grouping(group) => { let child_scope = Scope::with_parent(scope); @@ -513,7 +514,7 @@ impl Transpiler { let mut errors = Vec::new(); let commands = statements .iter() - .filter_map(|statement| { + .flat_map(|statement| { self.transpile_statement( statement, program_identifier, @@ -522,7 +523,7 @@ impl Transpiler { ) .unwrap_or_else(|err| { errors.push(err); - None + Vec::new() }) }) .collect::>(); @@ -530,17 +531,17 @@ impl Transpiler { return Err(errors.remove(0)); } if commands.is_empty() { - Ok(None) + Ok(Vec::new()) } else { - Ok(Some(Command::Group(commands))) + Ok(vec![Command::Group(commands)]) } } Statement::Semicolon(semi) => match semi.statement() { #[expect(clippy::match_wildcard_for_single_variants)] SemicolonStatement::Expression(expr) => match expr { - Expression::Primary(Primary::FunctionCall(func)) => { - self.transpile_function_call(func, scope, handler).map(Some) - } + Expression::Primary(Primary::FunctionCall(func)) => self + .transpile_function_call(func, scope, handler) + .map(|cmd| vec![cmd]), unexpected => { let error = TranspileError::UnexpectedExpression(UnexpectedExpression( unexpected.clone(), @@ -556,7 +557,7 @@ impl Transpiler { } } - fn transpile_function_call( + pub(super) fn transpile_function_call( &mut self, func: &FunctionCall, scope: &Arc, @@ -591,158 +592,6 @@ impl Transpiler { Ok(Command::Raw(function_call)) } - fn transpile_variable_declaration( - &mut self, - declaration: &VariableDeclaration, - program_identifier: &str, - scope: &Arc, - handler: &impl Handler, - ) -> TranspileResult> { - match declaration { - VariableDeclaration::Single(single) => self.transpile_single_variable_declaration( - single, - program_identifier, - scope, - handler, - ), - _ => todo!("declarations not supported yet: {declaration:?}"), - } - } - - #[expect(clippy::too_many_lines)] - fn transpile_single_variable_declaration( - &mut self, - single: &SingleVariableDeclaration, - program_identifier: &str, - scope: &Arc, - handler: &impl Handler, - ) -> TranspileResult> { - let mut deobfuscate_annotations = single - .annotations() - .iter() - .filter(|a| a.has_identifier("deobfuscate")); - - let variable_type = single.variable_type().keyword; - - let deobfuscate_annotation = deobfuscate_annotations.next(); - - if let Some(duplicate) = deobfuscate_annotations.next() { - let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: duplicate.span(), - message: "Multiple deobfuscate annotations are not allowed.".to_string(), - }); - handler.receive(error.clone()); - return Err(error); - } - let (name, target) = if let Some(deobfuscate_annotation) = deobfuscate_annotation { - let deobfuscate_annotation_value = - TranspileAnnotationValue::from(deobfuscate_annotation.assignment().value.clone()); - - if let TranspileAnnotationValue::Map(map) = deobfuscate_annotation_value { - if map.len() > 2 { - let error = - TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: deobfuscate_annotation.span(), - message: "Deobfuscate annotation must have at most 2 key-value pairs." - .to_string(), - }); - handler.receive(error.clone()); - return Err(error); - } - if let (Some(name), Some(target)) = (map.get("name"), map.get("target")) { - if let ( - TranspileAnnotationValue::Expression(objective), - TranspileAnnotationValue::Expression(target), - ) = (name, target) - { - if let (Some(name_eval), Some(target_eval)) = - (objective.comptime_eval(), target.comptime_eval()) - { - // TODO: change invalid criteria if boolean - if !crate::util::is_valid_scoreboard_name(&name_eval) { - let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: deobfuscate_annotation.span(), - message: "Deobfuscate annotation 'name' must be a valid scoreboard name.".to_string() - }); - handler.receive(error.clone()); - return Err(error); - } - if !crate::util::is_valid_player_name(&target_eval) { - let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: deobfuscate_annotation.span(), - message: "Deobfuscate annotation 'target' must be a valid player name.".to_string() - }); - handler.receive(error.clone()); - return Err(error); - } - (name_eval, target_eval) - } else { - let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: deobfuscate_annotation.span(), - message: "Deobfuscate annotation 'name' or 'target' could not have been evaluated at compile time.".to_string() - }); - handler.receive(error.clone()); - return Err(error); - } - } else { - let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: deobfuscate_annotation.span(), - message: "Deobfuscate annotation 'name' and 'target' must be compile time expressions.".to_string() - }); - handler.receive(error.clone()); - return Err(error); - } - } else { - let error = - TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: deobfuscate_annotation.span(), - message: - "Deobfuscate annotation must have both 'name' and 'target' keys." - .to_string(), - }); - handler.receive(error.clone()); - return Err(error); - } - } else { - let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: deobfuscate_annotation.span(), - message: "Deobfuscate annotation must be a map.".to_string(), - }); - handler.receive(error.clone()); - return Err(error); - } - } else { - let name = - "shu_values_".to_string() + &md5::hash(program_identifier).to_hex_lowercase(); - let target = md5::hash((Arc::as_ptr(scope) as usize).to_le_bytes()) - .to_hex_lowercase() - .split_off(16); - - (name, target) - }; - - if variable_type == KeywordKind::Int { - if !self.datapack.scoreboards().contains_key(&name) { - self.datapack.register_scoreboard(&name, None, None); - } - - scope.set_variable( - single.identifier().span.str(), - VariableType::ScoreboardValue { - objective: name.clone(), - target, - }, - ); - } else { - todo!("implement other variable types") - } - - Ok(single - .assignment() - .is_some() - .then(|| todo!("transpile assignment"))) - } - fn transpile_execute_block( &mut self, execute: &ExecuteBlock, @@ -769,11 +618,11 @@ impl Transpiler { let commands = block .statements() .iter() - .filter_map(|s| { + .flat_map(|s| { self.transpile_statement(s, program_identifier, scope, handler) .unwrap_or_else(|err| { errors.push(err); - None + Vec::new() }) }) .collect::>(); @@ -806,11 +655,11 @@ impl Transpiler { let mut errors = Vec::new(); let commands = statements .iter() - .filter_map(|statement| { + .flat_map(|statement| { self.transpile_statement(statement, program_identifier, scope, handler) .unwrap_or_else(|err| { errors.push(err); - None + Vec::new() }) }) .collect(); @@ -819,8 +668,19 @@ impl Transpiler { } Some(Execute::Runs(commands)) } else { - self.transpile_statement(&statements[0], program_identifier, scope, handler)? - .map(|cmd| Execute::Run(Box::new(cmd))) + let cmds = self.transpile_statement( + &statements[0], + program_identifier, + scope, + handler, + )?; + if cmds.len() > 1 { + Some(Execute::Runs(cmds)) + } else { + cmds.into_iter() + .next() + .map(|cmd| Execute::Run(Box::new(cmd))) + } }; then.map_or_else( @@ -857,27 +717,23 @@ impl Transpiler { .and_then(|el| { let (_, block) = el.clone().dissolve(); let statements = block.statements(); - if statements.is_empty() { - None - } else if statements.len() == 1 { - self.transpile_statement(&statements[0], program_identifier, scope, handler) - .unwrap_or_else(|err| { - errors.push(err); - None - }) - .map(|cmd| Execute::Run(Box::new(cmd))) - } else { - let commands = statements - .iter() - .filter_map(|statement| { - self.transpile_statement(statement, program_identifier, scope, handler) - .unwrap_or_else(|err| { - errors.push(err); - None - }) - }) - .collect(); - Some(Execute::Runs(commands)) + let cmds = statements + .iter() + .flat_map(|statement| { + self.transpile_statement(statement, program_identifier, scope, handler) + .unwrap_or_else(|err| { + errors.push(err); + Vec::new() + }) + }) + .collect::>(); + + match cmds.len() { + 0 => None, + 1 => Some(Execute::Run(Box::new( + cmds.into_iter().next().expect("length is 1"), + ))), + _ => Some(Execute::Runs(cmds)), } }) .map(Box::new); diff --git a/src/transpile/variables.rs b/src/transpile/variables.rs index 4aefbc4..ee7304e 100644 --- a/src/transpile/variables.rs +++ b/src/transpile/variables.rs @@ -7,54 +7,97 @@ use std::{ sync::{Arc, OnceLock, RwLock}, }; +use chksum_md5 as md5; +use shulkerbox::prelude::Command; use strum::EnumIs; -use super::{FunctionData, Transpiler}; +use crate::{ + base::{self, source_file::SourceElement as _, Handler}, + lexical::token::KeywordKind, + syntax::syntax_tree::{ + expression::{Expression, Primary}, + statement::{SingleVariableDeclaration, VariableDeclaration}, + }, +}; +use super::{ + error::IllegalAnnotationContent, expression::DataLocation, FunctionData, + TranspileAnnotationValue, TranspileError, TranspileResult, Transpiler, +}; + +/// Stores the data required to access a variable. #[derive(Debug, Clone, EnumIs)] -pub enum VariableType { +pub enum VariableData { + /// A function. Function { + /// The function data. function_data: FunctionData, + /// The path to the function once it is generated. path: OnceLock, }, + /// A function argument/parameter. FunctionArgument { + /// The index of the argument. index: usize, }, + /// A scoreboard. Scoreboard { + /// The objective name. objective: String, }, + /// A scoreboard value. ScoreboardValue { + /// The objective name. objective: String, + /// The target. target: String, }, + /// Multiple values stored in scoreboard. ScoreboardArray { + /// The objective name. objective: String, + /// The targets. targets: Vec, }, + /// A tag applied to entities. Tag { + /// The tag name. tag_name: String, }, + /// A boolean stored in a data storage. BooleanStorage { + /// The storage name. storage_name: String, + /// The path to the boolean. path: String, }, + /// Multiple booleans stored in a data storage array. BooleanStorageArray { + /// The storage name. storage_name: String, + /// The paths to the booleans. paths: Vec, }, } +/// A scope that stores variables. #[derive(Default)] pub struct Scope<'a> { + /// Parent scope where variables are inherited from. parent: Option<&'a Arc>, - variables: RwLock>>, + /// Variables stored in the scope. + variables: RwLock>>, } impl<'a> Scope<'a> { + /// Creates a new scope. + #[must_use] pub fn new() -> Arc { Arc::new(Self::default()) } + /// Creates a new scope with a parent. + #[must_use] pub fn with_parent(parent: &'a Arc) -> Arc { Arc::new(Self { parent: Some(parent), @@ -62,7 +105,8 @@ impl<'a> Scope<'a> { }) } - pub fn get_variable(&self, name: &str) -> Option> { + /// Gets a variable from the scope. + pub fn get_variable(&self, name: &str) -> Option> { let var = self.variables.read().unwrap().get(name).cloned(); if var.is_some() { var @@ -73,17 +117,31 @@ impl<'a> Scope<'a> { } } - pub fn set_variable(&self, name: &str, var: VariableType) { + /// Sets a variable in the scope. + pub fn set_variable(&self, name: &str, var: VariableData) { self.variables .write() .unwrap() .insert(name.to_string(), Arc::new(var)); } - pub fn get_variables(&self) -> &RwLock>> { + /// Gets the variables stored in the current scope. + pub fn get_local_variables(&self) -> &RwLock>> { &self.variables } + /// Gets all variables stored in the scope. + /// + /// This function does not return a reference to the variables, but clones them. + pub fn get_all_variables(&self) -> HashMap> { + let mut variables = self.variables.read().unwrap().clone(); + if let Some(parent) = self.parent.as_ref() { + variables.extend(parent.get_all_variables()); + } + variables + } + + /// Gets the parent scope. pub fn get_parent(&self) -> Option> { self.parent.cloned() } @@ -91,10 +149,7 @@ impl<'a> Scope<'a> { impl<'a> Debug for Scope<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut s = f.debug_struct("Scope"); - s.field("parent", &self.parent); - - struct VariableWrapper<'a>(&'a RwLock>>); + struct VariableWrapper<'a>(&'a RwLock>>); impl<'a> Debug for VariableWrapper<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = self.0.read().unwrap(); @@ -102,12 +157,198 @@ impl<'a> Debug for Scope<'a> { } } + let mut s = f.debug_struct("Scope"); + s.field("parent", &self.parent); + s.field("variables", &VariableWrapper(&self.variables)); s.finish() } } -impl Transpiler {} +impl Transpiler { + pub(super) fn transpile_variable_declaration( + &mut self, + declaration: &VariableDeclaration, + program_identifier: &str, + scope: &Arc, + handler: &impl Handler, + ) -> TranspileResult> { + match declaration { + VariableDeclaration::Single(single) => self.transpile_single_variable_declaration( + single, + program_identifier, + scope, + handler, + ), + _ => todo!("declarations not supported yet: {declaration:?}"), + } + } + + #[expect(clippy::too_many_lines)] + fn transpile_single_variable_declaration( + &mut self, + single: &SingleVariableDeclaration, + program_identifier: &str, + scope: &Arc, + handler: &impl Handler, + ) -> TranspileResult> { + let mut deobfuscate_annotations = single + .annotations() + .iter() + .filter(|a| a.has_identifier("deobfuscate")); + + let variable_type = single.variable_type().keyword; + + let deobfuscate_annotation = deobfuscate_annotations.next(); + + if let Some(duplicate) = deobfuscate_annotations.next() { + let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { + annotation: duplicate.span(), + message: "Multiple deobfuscate annotations are not allowed.".to_string(), + }); + handler.receive(error.clone()); + return Err(error); + } + let (name, target) = if let Some(deobfuscate_annotation) = deobfuscate_annotation { + let deobfuscate_annotation_value = + TranspileAnnotationValue::from(deobfuscate_annotation.assignment().value.clone()); + + if let TranspileAnnotationValue::Map(map) = deobfuscate_annotation_value { + if map.len() > 2 { + let error = + TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { + annotation: deobfuscate_annotation.span(), + message: "Deobfuscate annotation must have at most 2 key-value pairs." + .to_string(), + }); + handler.receive(error.clone()); + return Err(error); + } + if let (Some(name), Some(target)) = (map.get("name"), map.get("target")) { + if let ( + TranspileAnnotationValue::Expression(objective), + TranspileAnnotationValue::Expression(target), + ) = (name, target) + { + if let (Some(name_eval), Some(target_eval)) = + (objective.comptime_eval(), target.comptime_eval()) + { + // TODO: change invalid criteria if boolean + if !crate::util::is_valid_scoreboard_name(&name_eval) { + let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { + annotation: deobfuscate_annotation.span(), + message: "Deobfuscate annotation 'name' must be a valid scoreboard name.".to_string() + }); + handler.receive(error.clone()); + return Err(error); + } + if !crate::util::is_valid_player_name(&target_eval) { + let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { + annotation: deobfuscate_annotation.span(), + message: "Deobfuscate annotation 'target' must be a valid player name.".to_string() + }); + handler.receive(error.clone()); + return Err(error); + } + (name_eval, target_eval) + } else { + let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { + annotation: deobfuscate_annotation.span(), + message: "Deobfuscate annotation 'name' or 'target' could not have been evaluated at compile time.".to_string() + }); + handler.receive(error.clone()); + return Err(error); + } + } else { + let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { + annotation: deobfuscate_annotation.span(), + message: "Deobfuscate annotation 'name' and 'target' must be compile time expressions.".to_string() + }); + handler.receive(error.clone()); + return Err(error); + } + } else { + let error = + TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { + annotation: deobfuscate_annotation.span(), + message: + "Deobfuscate annotation must have both 'name' and 'target' keys." + .to_string(), + }); + handler.receive(error.clone()); + return Err(error); + } + } else { + let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { + annotation: deobfuscate_annotation.span(), + message: "Deobfuscate annotation must be a map.".to_string(), + }); + handler.receive(error.clone()); + return Err(error); + } + } else { + let name = + "shu_values_".to_string() + &md5::hash(program_identifier).to_hex_lowercase(); + let target = md5::hash((Arc::as_ptr(scope) as usize).to_le_bytes()) + .to_hex_lowercase() + .split_off(16); + + (name, target) + }; + + match variable_type { + KeywordKind::Int => { + if !self.datapack.scoreboards().contains_key(&name) { + self.datapack.register_scoreboard(&name, None, None); + } + + scope.set_variable( + single.identifier().span.str(), + VariableData::ScoreboardValue { + objective: name.clone(), + target, + }, + ); + } + _ => todo!("implement other variable types"), + } + + single.assignment().as_ref().map_or_else( + || Ok(Vec::new()), + |assignment| { + self.transpile_assignment( + single.identifier().span.str(), + assignment.expression(), + scope, + handler, + ) + }, + ) + } + + fn transpile_assignment( + &mut self, + name: &str, + expression: &crate::syntax::syntax_tree::expression::Expression, + scope: &Arc, + handler: &impl Handler, + ) -> TranspileResult> { + let target = scope.get_variable(name).unwrap(); + let data_location = match target.as_ref() { + VariableData::BooleanStorage { storage_name, path } => DataLocation::Storage { + storage_name: storage_name.to_owned(), + path: path.to_owned(), + r#type: super::expression::StorageType::Boolean, + }, + VariableData::ScoreboardValue { objective, target } => DataLocation::ScoreboardValue { + objective: objective.to_owned(), + target: target.to_owned(), + }, + _ => todo!("implement other variable types"), + }; + self.transpile_expression(expression, &data_location, scope, handler) + } +} #[cfg(test)] mod tests { @@ -118,13 +359,13 @@ mod tests { let scope = Scope::new(); scope.set_variable( "test", - VariableType::Scoreboard { + VariableData::Scoreboard { objective: "test".to_string(), }, ); if let Some(var) = scope.get_variable("test") { match var.as_ref() { - VariableType::Scoreboard { objective } => assert_eq!(objective, "test"), + VariableData::Scoreboard { objective } => assert_eq!(objective, "test"), _ => panic!("Incorrect Variable"), } } else { @@ -137,14 +378,14 @@ mod tests { let scope = Scope::new(); scope.set_variable( "test", - VariableType::Scoreboard { + VariableData::Scoreboard { objective: "test".to_string(), }, ); let child = Scope::with_parent(&scope); if let Some(var) = child.get_variable("test") { match var.as_ref() { - VariableType::Scoreboard { objective } => assert_eq!(objective, "test"), + VariableData::Scoreboard { objective } => assert_eq!(objective, "test"), _ => panic!("Incorrect Variable"), } } else {