From f3b3d5d3b68f3fdfb49030bd873004656c2092a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20H=C3=B6lting?= <87192362+moritz-hoelting@users.noreply.github.com> Date: Sun, 30 Mar 2025 19:38:08 +0200 Subject: [PATCH] implement integer and boolean function arguments --- src/lexical/token.rs | 2 + src/semantic/mod.rs | 2 +- src/syntax/syntax_tree/declaration.rs | 97 ++++- src/transpile/function.rs | 565 ++++++++++++++++++++++++++ src/transpile/internal_functions.rs | 2 +- src/transpile/mod.rs | 12 +- src/transpile/transpiler.rs | 447 +------------------- src/transpile/util.rs | 33 ++ src/util.rs | 42 ++ 9 files changed, 758 insertions(+), 444 deletions(-) create mode 100644 src/transpile/function.rs diff --git a/src/lexical/token.rs b/src/lexical/token.rs index 162cfa1..931d067 100644 --- a/src/lexical/token.rs +++ b/src/lexical/token.rs @@ -52,6 +52,7 @@ pub enum KeywordKind { Replace, Int, Bool, + Macro, } impl Display for KeywordKind { @@ -117,6 +118,7 @@ impl KeywordKind { Self::Replace => "replace", Self::Int => "int", Self::Bool => "bool", + Self::Macro => "macro", } } diff --git a/src/semantic/mod.rs b/src/semantic/mod.rs index ccef284..2bd9222 100644 --- a/src/semantic/mod.rs +++ b/src/semantic/mod.rs @@ -165,7 +165,7 @@ impl Function { parameters .elements() - .map(|el| el.span.str().to_string()) + .map(|el| el.identifier().span.str().to_string()) .collect() } else { HashSet::new() diff --git a/src/syntax/syntax_tree/declaration.rs b/src/syntax/syntax_tree/declaration.rs index 19ea859..a671274 100644 --- a/src/syntax/syntax_tree/declaration.rs +++ b/src/syntax/syntax_tree/declaration.rs @@ -4,6 +4,7 @@ use std::collections::VecDeque; +use enum_as_inner::EnumAsInner; use getset::Getters; use crate::{ @@ -87,7 +88,7 @@ impl Declaration { /// ; /// /// ParameterList: -/// Identifier (',' Identifier)* ','? +/// FunctionArgument (',' FunctionArgument)* ','? /// ; /// ``` #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -104,7 +105,7 @@ pub struct Function { #[get = "pub"] open_paren: Punctuation, #[get = "pub"] - parameters: Option>, + parameters: Option>, #[get = "pub"] close_paren: Punctuation, #[get = "pub"] @@ -123,7 +124,7 @@ impl Function { Keyword, Identifier, Punctuation, - Option>, + Option>, Punctuation, Block, ) { @@ -156,6 +157,41 @@ impl SourceElement for Function { } } +// Represents a variable type keyword for function arguments. +/// +/// Syntax Synopsis: +/// +/// ``` ebnf +/// FunctionVariableType: +/// 'macro' | 'int' | 'bool' +/// ; +/// ``` +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, EnumAsInner)] +pub enum FunctionVariableType { + Macro(Keyword), + Integer(Keyword), + Boolean(Keyword), +} + +/// Represents a function argument in the syntax tree. +/// +/// Syntax Synopsis: +/// +/// ``` ebnf +/// FunctionArgument: +/// FunctionVariableType Identifier +/// ; +/// ``` +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Getters)] +pub struct FunctionParameter { + #[get = "pub"] + variable_type: FunctionVariableType, + #[get = "pub"] + identifier: Identifier, +} + /// Represents an import declaration in the syntax tree. /// /// Syntax Synopsis: @@ -451,7 +487,7 @@ impl Parser<'_> { let delimited_tree = self.parse_enclosed_list( Delimiter::Parenthesis, ',', - |parser: &mut Parser<'_>| parser.parse_identifier(handler), + |parser: &mut Parser<'_>| parser.parse_function_parameter(handler), handler, )?; @@ -479,4 +515,57 @@ impl Parser<'_> { } } } + + fn parse_function_parameter( + &mut self, + handler: &impl Handler, + ) -> ParseResult { + match self.stop_at_significant() { + Reading::Atomic(Token::Keyword(keyword)) if keyword.keyword == KeywordKind::Int => { + let variable_type = FunctionVariableType::Integer(keyword); + self.forward(); + + let identifier = self.parse_identifier(handler)?; + + Ok(FunctionParameter { + variable_type, + identifier, + }) + } + Reading::Atomic(Token::Keyword(keyword)) if keyword.keyword == KeywordKind::Bool => { + let variable_type = FunctionVariableType::Boolean(keyword); + self.forward(); + + let identifier = self.parse_identifier(handler)?; + + Ok(FunctionParameter { + variable_type, + identifier, + }) + } + Reading::Atomic(Token::Keyword(keyword)) if keyword.keyword == KeywordKind::Macro => { + let variable_type = FunctionVariableType::Macro(keyword); + self.forward(); + + let identifier = self.parse_identifier(handler)?; + + Ok(FunctionParameter { + variable_type, + identifier, + }) + } + unexpected => { + let err = Error::UnexpectedSyntax(UnexpectedSyntax { + expected: SyntaxKind::Either(&[ + SyntaxKind::Keyword(KeywordKind::Int), + SyntaxKind::Keyword(KeywordKind::Bool), + SyntaxKind::Keyword(KeywordKind::Macro), + ]), + found: unexpected.into_token(), + }); + handler.receive(err.clone()); + Err(err) + } + } + } } diff --git a/src/transpile/function.rs b/src/transpile/function.rs new file mode 100644 index 0000000..6e90a90 --- /dev/null +++ b/src/transpile/function.rs @@ -0,0 +1,565 @@ +use chksum_md5 as md5; +use enum_as_inner::EnumAsInner; +use std::{collections::BTreeMap, sync::Arc}; + +use shulkerbox::datapack::{Command, Execute}; + +use crate::{ + base::{ + self, + source_file::{SourceElement, Span}, + Handler, + }, + semantic::error::{ConflictingFunctionNames, InvalidFunctionArguments}, + syntax::syntax_tree::{ + declaration::FunctionVariableType, + expression::{Expression, Primary}, + statement::Statement, + }, + transpile::{ + error::{IllegalAnnotationContent, MissingFunctionDeclaration}, + util::{MacroString, MacroStringPart}, + }, +}; + +use super::{ + error::{MismatchedTypes, TranspileError, TranspileResult, UnknownIdentifier}, + expression::{ComptimeValue, ExpectedType, StorageType}, + variables::{Scope, VariableData}, + FunctionData, TranspileAnnotationValue, Transpiler, +}; + +#[derive(Debug, Clone)] +pub enum TranspiledFunctionArguments { + None, + Static(BTreeMap), + Dynamic(Vec), +} + +impl Transpiler { + /// Gets the function at the given path, or transpiles it if it hasn't been transpiled yet. + /// Returns the location of the function or None if the function does not exist. + #[tracing::instrument(level = "trace", skip(self, handler))] + pub(super) fn get_or_transpile_function( + &mut self, + identifier_span: &Span, + arguments: Option<&[&Expression]>, + scope: &Arc, + handler: &impl Handler, + ) -> TranspileResult<(String, TranspiledFunctionArguments)> { + let program_identifier = identifier_span.source_file().identifier(); + let program_query = ( + program_identifier.to_string(), + identifier_span.str().to_string(), + ); + let alias_query = self.aliases.get(&program_query).cloned(); + let already_transpiled = scope + .get_variable(identifier_span.str()) + .expect("called function should be in scope") + .as_ref() + .as_function() + .map(|(_, path)| path.get().is_some()) + .expect("called variable should be of type function"); + + let function_data = scope + .get_variable(identifier_span.str()) + .or_else(|| { + alias_query + .clone() + .and_then(|(alias_program_identifier, alias_function_name)| { + self.scopes + .get(&alias_program_identifier) + .and_then(|s| s.get_variable(&alias_function_name)) + }) + }) + .ok_or_else(|| { + let error = TranspileError::MissingFunctionDeclaration( + MissingFunctionDeclaration::from_scope(identifier_span.clone(), scope), + ); + handler.receive(error.clone()); + error + })?; + + let VariableData::Function { + function_data, + path: function_path, + } = function_data.as_ref() + else { + unreachable!("must be of correct type, otherwise errored out before"); + }; + + if !already_transpiled { + tracing::trace!("Function not transpiled yet, transpiling."); + + let statements = function_data.statements.clone(); + + let modified_name = function_data.annotations.get("deobfuscate").map_or_else( + || { + let hash_data = program_identifier.to_string() + "\0" + identifier_span.str(); + Ok("shu/".to_string() + &md5::hash(hash_data).to_hex_lowercase()) + }, + |val| match val { + TranspileAnnotationValue::None => Ok(identifier_span.str().to_string()), + TranspileAnnotationValue::Expression(expr) => expr + .comptime_eval(scope, handler) + .and_then(|val| val.to_string_no_macro()) + .ok_or_else(|| { + let err = TranspileError::IllegalAnnotationContent( + IllegalAnnotationContent { + annotation: identifier_span.clone(), + message: "Cannot evaluate annotation at compile time" + .to_string(), + }, + ); + handler.receive(err.clone()); + err + }), + TranspileAnnotationValue::Map(_) => { + let err = + TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { + annotation: identifier_span.clone(), + message: "Deobfuscate annotation cannot be a map.".to_string(), + }); + handler.receive(err.clone()); + Err(err) + } + }, + )?; + + let function_location = format!( + "{namespace}:{modified_name}", + namespace = function_data.namespace + ); + + function_path.set(function_location.clone()).unwrap(); + + let function_scope = Scope::with_parent(scope); + + for (i, param) in function_data.parameters.iter().enumerate() { + let param_str = param.identifier().span.str(); + match param.variable_type() { + FunctionVariableType::Macro(_) => { + function_scope.set_variable( + param_str, + VariableData::MacroParameter { + index: i, + macro_name: crate::util::identifier_to_macro(param_str).to_string(), + }, + ); + } + FunctionVariableType::Integer(_) => { + let objective = format!( + "shu_arguments_{}", + function_location.replace(['/', ':'], "_") + ); + function_scope.set_variable( + param_str, + VariableData::ScoreboardValue { + objective: objective.clone(), + target: crate::util::identifier_to_scoreboard_target(param_str) + .into_owned(), + }, + ); + } + FunctionVariableType::Boolean(_) => { + let storage_name = format!( + "shulkerscript:arguments_{}", + function_location.replace(['/', ':'], "_") + ); + // TODO: replace with proper path + function_scope.set_variable( + param_str, + VariableData::BooleanStorage { + storage_name, + path: crate::util::identifier_to_scoreboard_target(param_str) + .into_owned(), + }, + ); + } + } + } + + let commands = + self.transpile_function(&statements, program_identifier, &function_scope, handler)?; + + let namespace = self.datapack.namespace_mut(&function_data.namespace); + + if namespace.function(&modified_name).is_some() { + let err = TranspileError::ConflictingFunctionNames(ConflictingFunctionNames { + name: modified_name, + definition: identifier_span.clone(), + }); + handler.receive(err.clone()); + return Err(err); + } + + let function = namespace.function_mut(&modified_name); + function.get_commands_mut().extend(commands); + + if function_data.annotations.contains_key("tick") { + self.datapack.add_tick(&function_location); + } + if function_data.annotations.contains_key("load") { + self.datapack.add_load(&function_location); + } + } + + let function_location = function_path + .get() + .ok_or_else(|| { + let error = TranspileError::MissingFunctionDeclaration( + MissingFunctionDeclaration::from_scope(identifier_span.clone(), scope), + ); + handler.receive(error.clone()); + error + }) + .map(String::to_owned)?; + + let args = self.transpile_function_arguments( + function_data, + &function_location, + arguments, + scope, + handler, + )?; + + Ok((function_location, args)) + } + + fn transpile_function( + &mut self, + statements: &[Statement], + program_identifier: &str, + scope: &Arc, + handler: &impl Handler, + ) -> TranspileResult> { + let mut errors = Vec::new(); + let commands = statements + .iter() + .flat_map(|statement| { + self.transpile_statement(statement, program_identifier, scope, handler) + .unwrap_or_else(|err| { + errors.push(err); + Vec::new() + }) + }) + .collect(); + + if !errors.is_empty() { + return Err(errors.remove(0)); + } + + Ok(commands) + } + + #[expect(clippy::too_many_lines)] + fn transpile_function_arguments( + &mut self, + function_data: &FunctionData, + function_location: &str, + arguments: Option<&[&Expression]>, + scope: &Arc, + handler: &impl Handler, + ) -> TranspileResult { + let parameters = &function_data.parameters; + let identifier_span = &function_data.identifier_span; + let arg_count = arguments.map(<[&Expression]>::len); + + match arg_count { + Some(arg_count) if arg_count != parameters.len() => { + let err = TranspileError::InvalidFunctionArguments(InvalidFunctionArguments { + expected: parameters.len(), + actual: arg_count, + span: identifier_span.clone(), + }); + handler.receive(err.clone()); + Err(err) + } + Some(arg_count) if arg_count > 0 => { + #[derive(Debug, Clone, EnumAsInner)] + enum Parameter { + Static(MacroString), + Storage { + prepare_cmds: Vec, + storage_name: String, + path: String, + }, + } + + let mut compiled_args = Vec::::new(); + let mut errs = Vec::new(); + + for expression in arguments.iter().flat_map(|expressions| expressions.iter()) { + let value = match expression { + Expression::Primary(Primary::Lua(lua)) => { + lua.eval_comptime(scope, handler).and_then(|val| match val { + Some(ComptimeValue::MacroString(s)) => Ok(Parameter::Static(s)), + Some(val) => Ok(Parameter::Static(val.to_macro_string())), + None => { + let err = TranspileError::MismatchedTypes(MismatchedTypes { + expression: expression.span(), + expected_type: ExpectedType::String, + }); + handler.receive(err.clone()); + Err(err) + } + }) + } + Expression::Primary(Primary::Integer(num)) => { + Ok(Parameter::Static(num.span.str().to_string().into())) + } + Expression::Primary(Primary::Boolean(bool)) => { + Ok(Parameter::Static(bool.span.str().to_string().into())) + } + Expression::Primary(Primary::StringLiteral(string)) => { + Ok(Parameter::Static(string.str_content().to_string().into())) + } + Expression::Primary(Primary::MacroStringLiteral(literal)) => { + Ok(Parameter::Static(literal.into())) + } + Expression::Primary(primary @ Primary::Identifier(ident)) => { + let var = scope.get_variable(ident.span.str()).ok_or_else(|| { + let err = TranspileError::UnknownIdentifier(UnknownIdentifier { + identifier: ident.span(), + }); + handler.receive(err.clone()); + err + })?; + match var.as_ref() { + VariableData::MacroParameter { macro_name, .. } => { + Ok(Parameter::Static(MacroString::MacroString(vec![ + MacroStringPart::MacroUsage(macro_name.clone()), + ]))) + } + + VariableData::BooleanStorage { .. } + | VariableData::ScoreboardValue { .. } => { + let (temp_storage, mut temp_path) = + self.get_temp_storage_locations(1); + let prepare_cmds = self.transpile_primary_expression( + primary, + &super::expression::DataLocation::Storage { + storage_name: temp_storage.clone(), + path: temp_path[0].clone(), + r#type: match var.as_ref() { + VariableData::BooleanStorage { .. } => { + StorageType::Boolean + } + VariableData::ScoreboardValue { .. } => { + StorageType::Int + } + _ => unreachable!("checked in parent match"), + }, + }, + scope, + handler, + )?; + + Ok(Parameter::Storage { + prepare_cmds, + storage_name: temp_storage, + path: std::mem::take(&mut temp_path[0]), + }) + } + _ => todo!("other variable types"), + } + } + Expression::Primary( + Primary::Parenthesized(_) + | Primary::Prefix(_) + | Primary::Indexed(_) + | Primary::FunctionCall(_), + ) + | Expression::Binary(_) => { + let (temp_storage, mut temp_path) = self.get_temp_storage_locations(1); + let prepare_cmds = self.transpile_expression( + expression, + &super::expression::DataLocation::Storage { + storage_name: temp_storage.clone(), + path: temp_path[0].clone(), + r#type: StorageType::Int, + }, + scope, + handler, + )?; + + Ok(Parameter::Storage { + prepare_cmds, + storage_name: temp_storage, + path: std::mem::take(&mut temp_path[0]), + }) + } + }; + + match value { + Ok(value) => { + compiled_args.push(value); + } + Err(err) => { + compiled_args + .push(Parameter::Static(MacroString::String(String::new()))); + errs.push(err.clone()); + } + } + } + if let Some(err) = errs.first() { + return Err(err.clone()); + } + + if compiled_args.iter().any(|arg| !arg.is_static()) { + let (mut setup_cmds, move_cmds, static_params) = parameters.clone().into_iter().zip(compiled_args).fold( + (Vec::new(), Vec::new(), BTreeMap::new()), + |(mut acc_setup, mut acc_move, mut statics), (param, data)| { + match param.variable_type() { + FunctionVariableType::Macro(_) => { + let arg_name = crate::util::identifier_to_macro(param.identifier().span.str()); + match data { + Parameter::Static(s) => { + match s { + MacroString::String(value) => statics.insert( + arg_name.to_string(), + MacroString::String(crate::util::escape_str(&value).to_string()) + ), + MacroString::MacroString(parts) => { + let parts = parts.into_iter().map(|part| { + match part { + MacroStringPart::String(s) => MacroStringPart::String(crate::util::escape_str(&s).to_string()), + MacroStringPart::MacroUsage(m) => MacroStringPart::MacroUsage(m), + } + }).collect(); + statics.insert(arg_name.to_string(), MacroString::MacroString(parts)) + } + }; + } + Parameter::Storage { prepare_cmds, storage_name, path } => { + acc_setup.extend(prepare_cmds); + acc_move.push(Command::Raw( + format!(r"data modify storage shulkerscript:function_arguments {arg_name} set from storage {storage_name} {path}") + )); + } + } + } + FunctionVariableType::Integer(_) => { + let objective = format!("shu_arguments_{}", function_location.replace(['/', ':'], "_")); + let param_str = param.identifier().span.str(); + let target = crate::util::identifier_to_scoreboard_target(param_str); + + match data { + Parameter::Static(s) => { + match s.as_str() { + Ok(s) => { + if s.parse::().is_ok() { + acc_move.push(Command::Raw(format!(r"scoreboard players set {target} {objective} {s}"))); + } else { + panic!("non-integer static argument") + } + } + Err(parts) => { + acc_move.push(Command::UsesMacro(MacroString::MacroString( + std::iter::once(MacroStringPart::String(format!("scoreboard players set {target} {objective} "))) + .chain(parts.iter().cloned()).collect() + ).into())); + } + } + } + Parameter::Storage { prepare_cmds, storage_name, path } => { + acc_setup.extend(prepare_cmds); + acc_move.push(Command::Execute(Execute::Store( + format!("result score {target} {objective}").into(), + Box::new(Execute::Run(Box::new(Command::Raw(format!("data get storage {storage_name} {path}"))))) + ))); + } + } + }, + FunctionVariableType::Boolean(_) => { + let target_storage_name = format!("shulkerscript:arguments_{}", function_location.replace(['/', ':'], "_")); + let param_str = param.identifier().span.str(); + let target_path = crate::util::identifier_to_scoreboard_target(param_str); + + match data { + Parameter::Static(s) => { + match s.as_str() { + Ok(s) => { + if let Ok(b) = s.parse::() { + acc_move.push(Command::Raw(format!("data modify storage {target_storage_name} {target_path} set value {}", if b { "1b" } else { "0b" }))); + } else { + panic!("non-integer static argument") + } + } + Err(parts) => { + acc_move.push(Command::UsesMacro(MacroString::MacroString( + std::iter::once(MacroStringPart::String(format!("data modify storage {target_storage_name} {target_path} set value "))) + .chain(parts.iter().cloned()).collect() + ).into())); + } + } + } + Parameter::Storage { prepare_cmds, storage_name, path } => { + acc_setup.extend(prepare_cmds); + acc_move.push(Command::Raw(format!("data modify storage {target_storage_name} {target_path} set from storage {storage_name} {path}"))); + } + } + }, + } + (acc_setup, acc_move, statics)}, + ); + let statics_len = static_params.len(); + let joined_statics = + super::util::join_macro_strings(static_params.into_iter().enumerate().map( + |(i, (k, v))| match v { + MacroString::String(s) => { + let mut s = format!(r#"{k}:"{s}""#); + if i < statics_len - 1 { + s.push(','); + } + MacroString::String(s) + } + MacroString::MacroString(mut parts) => { + parts.insert(0, MacroStringPart::String(format!(r#"{k}:""#))); + let mut ending = '"'.to_string(); + if i < statics_len - 1 { + ending.push(','); + } + parts.push(MacroStringPart::String(ending)); + MacroString::MacroString(parts) + } + }, + )); + let statics_cmd = match joined_statics { + MacroString::String(s) => Command::Raw(format!( + r"data merge storage shulkerscript:function_arguments {{{s}}}" + )), + MacroString::MacroString(_) => Command::UsesMacro( + super::util::join_macro_strings([ + MacroString::String( + "data merge storage shulkerscript:function_arguments {" + .to_string(), + ), + joined_statics, + MacroString::String("}".to_string()), + ]) + .into(), + ), + }; + setup_cmds.push(statics_cmd); + setup_cmds.extend(move_cmds); + + Ok(TranspiledFunctionArguments::Dynamic(setup_cmds)) + } else { + let function_args = parameters + .clone() + .into_iter() + .zip( + compiled_args + .into_iter() + .map(|arg| arg.into_static().expect("checked in if condition")), + ) + .map(|(k, v)| (k.identifier().span.str().to_string(), v)) + .collect(); + Ok(TranspiledFunctionArguments::Static(function_args)) + } + } + _ => Ok(TranspiledFunctionArguments::None), + } + } +} diff --git a/src/transpile/internal_functions.rs b/src/transpile/internal_functions.rs index 0dd4baf..09dc0db 100644 --- a/src/transpile/internal_functions.rs +++ b/src/transpile/internal_functions.rs @@ -311,7 +311,7 @@ fn print_function( todo!("throw error when index is not constant integer") } } - _ => todo!(), + _ => todo!("catch illegal indexing"), } } _ => Err(TranspileError::IllegalIndexing(IllegalIndexing { diff --git a/src/transpile/mod.rs b/src/transpile/mod.rs index fba3ef3..0f72685 100644 --- a/src/transpile/mod.rs +++ b/src/transpile/mod.rs @@ -7,7 +7,10 @@ use std::{ use crate::{ base::source_file::{SourceElement, Span}, - syntax::syntax_tree::{expression::Expression, statement::Statement, AnnotationValue}, + syntax::syntax_tree::{ + declaration::FunctionParameter, expression::Expression, statement::Statement, + AnnotationValue, + }, }; #[doc(hidden)] @@ -32,6 +35,11 @@ pub use transpiler::Transpiler; #[cfg(feature = "shulkerbox")] pub mod internal_functions; +#[doc(hidden)] +#[cfg(feature = "shulkerbox")] +pub mod function; +pub use function::TranspiledFunctionArguments; + mod variables; pub use variables::{Scope, VariableData}; @@ -42,7 +50,7 @@ pub mod util; pub struct FunctionData { pub(super) namespace: String, pub(super) identifier_span: Span, - pub(super) parameters: Vec, + pub(super) parameters: Vec, pub(super) statements: Vec, pub(super) public: bool, pub(super) annotations: HashMap, diff --git a/src/transpile/transpiler.rs b/src/transpile/transpiler.rs index b5161a7..69c95af 100644 --- a/src/transpile/transpiler.rs +++ b/src/transpile/transpiler.rs @@ -1,7 +1,5 @@ //! Transpiler for `Shulkerscript` -use chksum_md5 as md5; -use enum_as_inner::EnumAsInner; use std::{ collections::{BTreeMap, HashMap, HashSet}, ops::Deref, @@ -11,12 +9,8 @@ use std::{ use shulkerbox::datapack::{self, Command, Datapack, Execute}; use crate::{ - base::{ - self, - source_file::{SourceElement, Span}, - Handler, - }, - semantic::error::{ConflictingFunctionNames, InvalidFunctionArguments, UnexpectedExpression}, + base::{self, source_file::SourceElement, Handler}, + semantic::error::UnexpectedExpression, syntax::syntax_tree::{ declaration::{Declaration, ImportItems}, expression::{Expression, FunctionCall, Primary}, @@ -27,17 +21,14 @@ use crate::{ }, AnnotationAssignment, }, - transpile::{ - error::{IllegalAnnotationContent, MissingFunctionDeclaration}, - util::{MacroString, MacroStringPart}, - }, + transpile::util::{MacroString, MacroStringPart}, }; use super::{ - error::{MismatchedTypes, TranspileError, TranspileResult, UnknownIdentifier}, - expression::{ComptimeValue, ExpectedType, ExtendedCondition, StorageType}, + error::{MismatchedTypes, TranspileError, TranspileResult}, + expression::{ComptimeValue, ExpectedType, ExtendedCondition}, variables::{Scope, TranspileAssignmentTarget, VariableData}, - FunctionData, TranspileAnnotationValue, + FunctionData, TranspileAnnotationValue, TranspiledFunctionArguments, }; /// A transpiler for `Shulkerscript`. @@ -49,18 +40,11 @@ pub struct Transpiler { pub(super) initialized_constant_scores: HashSet, pub(super) temp_counter: usize, /// Top-level [`Scope`] for each program identifier - scopes: BTreeMap>>, + pub(super) scopes: BTreeMap>>, /// Key: (program identifier, function name) - functions: BTreeMap<(String, String), FunctionData>, + pub(super) functions: BTreeMap<(String, String), FunctionData>, /// Key: alias, Value: target - aliases: HashMap<(String, String), (String, String)>, -} - -#[derive(Debug, Clone)] -pub enum TranspiledFunctionArguments { - None, - Static(BTreeMap), - Dynamic(Vec), + pub(super) aliases: HashMap<(String, String), (String, String)>, } impl Transpiler { @@ -202,11 +186,7 @@ impl Transpiler { parameters: function .parameters() .as_ref() - .map(|l| { - l.elements() - .map(|i| i.span.str().to_string()) - .collect::>() - }) + .map(|l| l.elements().cloned().collect::>()) .unwrap_or_default(), statements, public: function.is_public(), @@ -266,412 +246,7 @@ impl Transpiler { }; } - /// Gets the function at the given path, or transpiles it if it hasn't been transpiled yet. - /// Returns the location of the function or None if the function does not exist. - #[tracing::instrument(level = "trace", skip(self, handler))] - pub(super) fn get_or_transpile_function( - &mut self, - identifier_span: &Span, - arguments: Option<&[&Expression]>, - scope: &Arc, - handler: &impl Handler, - ) -> TranspileResult<(String, TranspiledFunctionArguments)> { - let program_identifier = identifier_span.source_file().identifier(); - let program_query = ( - program_identifier.to_string(), - identifier_span.str().to_string(), - ); - let alias_query = self.aliases.get(&program_query).cloned(); - let already_transpiled = scope - .get_variable(identifier_span.str()) - .expect("called function should be in scope") - .as_ref() - .as_function() - .map(|(_, path)| path.get().is_some()) - .expect("called variable should be of type function"); - - let function_data = scope - .get_variable(identifier_span.str()) - .or_else(|| { - alias_query - .clone() - .and_then(|(alias_program_identifier, alias_function_name)| { - self.scopes - .get(&alias_program_identifier) - .and_then(|s| s.get_variable(&alias_function_name)) - }) - }) - .ok_or_else(|| { - let error = TranspileError::MissingFunctionDeclaration( - MissingFunctionDeclaration::from_scope(identifier_span.clone(), scope), - ); - handler.receive(error.clone()); - error - })?; - - let VariableData::Function { - function_data, - path: function_path, - } = function_data.as_ref() - else { - unreachable!("must be of correct type, otherwise errored out before"); - }; - - if !already_transpiled { - tracing::trace!("Function not transpiled yet, transpiling."); - - let function_scope = Scope::with_parent(scope); - - for (i, param) in function_data.parameters.iter().enumerate() { - function_scope.set_variable( - param, - VariableData::MacroParameter { - index: i, - macro_name: crate::util::identifier_to_macro(param).to_string(), - }, - ); - } - - let statements = function_data.statements.clone(); - - let modified_name = function_data.annotations.get("deobfuscate").map_or_else( - || { - let hash_data = program_identifier.to_string() + "\0" + identifier_span.str(); - Ok("shu/".to_string() + &md5::hash(hash_data).to_hex_lowercase()) - }, - |val| match val { - TranspileAnnotationValue::None => Ok(identifier_span.str().to_string()), - TranspileAnnotationValue::Expression(expr) => expr - .comptime_eval(scope, handler) - .and_then(|val| val.to_string_no_macro()) - .ok_or_else(|| { - let err = TranspileError::IllegalAnnotationContent( - IllegalAnnotationContent { - annotation: identifier_span.clone(), - message: "Cannot evaluate annotation at compile time" - .to_string(), - }, - ); - handler.receive(err.clone()); - err - }), - TranspileAnnotationValue::Map(_) => { - let err = - TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: identifier_span.clone(), - message: "Deobfuscate annotation cannot be a map.".to_string(), - }); - handler.receive(err.clone()); - Err(err) - } - }, - )?; - - let function_location = format!( - "{namespace}:{modified_name}", - namespace = function_data.namespace - ); - - function_path.set(function_location.clone()).unwrap(); - - let commands = - self.transpile_function(&statements, program_identifier, &function_scope, handler)?; - - let namespace = self.datapack.namespace_mut(&function_data.namespace); - - if namespace.function(&modified_name).is_some() { - let err = TranspileError::ConflictingFunctionNames(ConflictingFunctionNames { - name: modified_name, - definition: identifier_span.clone(), - }); - handler.receive(err.clone()); - return Err(err); - } - - let function = namespace.function_mut(&modified_name); - function.get_commands_mut().extend(commands); - - if function_data.annotations.contains_key("tick") { - self.datapack.add_tick(&function_location); - } - if function_data.annotations.contains_key("load") { - self.datapack.add_load(&function_location); - } - } - - let parameters = &function_data.parameters; - - let function_location = function_path - .get() - .ok_or_else(|| { - let error = TranspileError::MissingFunctionDeclaration( - MissingFunctionDeclaration::from_scope(identifier_span.clone(), scope), - ); - handler.receive(error.clone()); - error - }) - .map(String::to_owned)?; - - let arg_count = arguments.map(<[&Expression]>::len); - if arg_count.is_some_and(|arg_count| arg_count != parameters.len()) { - let err = TranspileError::InvalidFunctionArguments(InvalidFunctionArguments { - expected: parameters.len(), - actual: arg_count.unwrap_or_default(), - span: identifier_span.clone(), - }); - handler.receive(err.clone()); - Err(err) - } else if arg_count.is_some_and(|arg_count| arg_count > 0) { - { - #[derive(Debug, Clone, EnumAsInner)] - enum Parameter { - Static(MacroString), - Dynamic { - prepare_cmds: Vec, - storage_name: String, - path: String, - }, - } - - let mut compiled_args = Vec::new(); - let mut errs = Vec::new(); - - for expression in arguments.iter().flat_map(|x| x.iter()) { - let value = match expression { - Expression::Primary(Primary::Lua(lua)) => { - lua.eval_comptime(scope, handler).and_then(|val| match val { - Some(ComptimeValue::MacroString(s)) => Ok(Parameter::Static(s)), - Some(val) => Ok(Parameter::Static(val.to_macro_string())), - None => { - let err = TranspileError::MismatchedTypes(MismatchedTypes { - expression: expression.span(), - expected_type: ExpectedType::String, - }); - handler.receive(err.clone()); - Err(err) - } - }) - } - Expression::Primary(Primary::Integer(num)) => { - Ok(Parameter::Static(num.span.str().to_string().into())) - } - Expression::Primary(Primary::Boolean(bool)) => { - Ok(Parameter::Static(bool.span.str().to_string().into())) - } - Expression::Primary(Primary::StringLiteral(string)) => { - Ok(Parameter::Static(string.str_content().to_string().into())) - } - Expression::Primary(Primary::MacroStringLiteral(literal)) => { - Ok(Parameter::Static(literal.into())) - } - Expression::Primary(primary @ Primary::Identifier(ident)) => { - let var = scope.get_variable(ident.span.str()).ok_or_else(|| { - let err = TranspileError::UnknownIdentifier(UnknownIdentifier { - identifier: ident.span(), - }); - handler.receive(err.clone()); - err - })?; - match var.as_ref() { - VariableData::MacroParameter { macro_name, .. } => { - Ok(Parameter::Static(MacroString::MacroString(vec![ - MacroStringPart::MacroUsage(macro_name.clone()), - ]))) - } - - VariableData::BooleanStorage { .. } - | VariableData::ScoreboardValue { .. } => { - let (temp_storage, mut temp_path) = - self.get_temp_storage_locations(1); - let prepare_cmds = self.transpile_primary_expression( - primary, - &super::expression::DataLocation::Storage { - storage_name: temp_storage.clone(), - path: temp_path[0].clone(), - r#type: match var.as_ref() { - VariableData::BooleanStorage { .. } => { - StorageType::Boolean - } - VariableData::ScoreboardValue { .. } => { - StorageType::Int - } - _ => unreachable!("checked in parent match"), - }, - }, - scope, - handler, - )?; - - Ok(Parameter::Dynamic { - prepare_cmds, - storage_name: temp_storage, - path: std::mem::take(&mut temp_path[0]), - }) - } - _ => todo!("other variable types"), - } - } - Expression::Primary( - Primary::Parenthesized(_) - | Primary::Prefix(_) - | Primary::Indexed(_) - | Primary::FunctionCall(_), - ) - | Expression::Binary(_) => { - let (temp_storage, mut temp_path) = self.get_temp_storage_locations(1); - let prepare_cmds = self.transpile_expression( - expression, - &super::expression::DataLocation::Storage { - storage_name: temp_storage.clone(), - path: temp_path[0].clone(), - r#type: StorageType::Int, - }, - scope, - handler, - )?; - - Ok(Parameter::Dynamic { - prepare_cmds, - storage_name: temp_storage, - path: std::mem::take(&mut temp_path[0]), - }) - } - }; - - match value { - Ok(value) => { - compiled_args.push(value); - } - Err(err) => { - compiled_args - .push(Parameter::Static(MacroString::String(String::new()))); - errs.push(err.clone()); - } - } - } - if let Some(err) = errs.first() { - return Err(err.clone()); - } - if compiled_args.iter().any(|arg| !arg.is_static()) { - let (mut setup_cmds, move_cmds, static_params) = parameters.clone().into_iter().zip(compiled_args).fold( - (Vec::new(), Vec::new(), BTreeMap::new()), - |(mut acc_setup, mut acc_move, mut statics), (arg_name, data)| { - let arg_name = crate::util::identifier_to_macro(&arg_name); - match data { - Parameter::Static(s) => { - match s { - MacroString::String(value) => statics.insert(arg_name.to_string(), MacroString::String(crate::util::escape_str(&value).to_string())), - MacroString::MacroString(parts) => { - let parts = parts.into_iter().map(|part| { - match part { - MacroStringPart::String(s) => MacroStringPart::String(crate::util::escape_str(&s).to_string()), - MacroStringPart::MacroUsage(m) => MacroStringPart::MacroUsage(m), - } - }).collect(); - statics.insert(arg_name.to_string(), MacroString::MacroString(parts)) - } - }; - } - Parameter::Dynamic { prepare_cmds, storage_name, path } => { - acc_setup.extend(prepare_cmds); - acc_move.push(Command::Raw(format!(r"data modify storage shulkerscript:function_arguments {arg_name} set from storage {storage_name} {path}"))); - } - } - (acc_setup, acc_move, statics)}, - ); - let statics_len = static_params.len(); - let joined_statics = - super::util::join_macro_strings(static_params.into_iter().enumerate().map( - |(i, (k, v))| match v { - MacroString::String(s) => { - let mut s = format!(r#"{k}:"{s}""#); - if i < statics_len - 1 { - s.push(','); - } - MacroString::String(s) - } - MacroString::MacroString(mut parts) => { - parts.insert(0, MacroStringPart::String(format!(r#"{k}:""#))); - let mut ending = '"'.to_string(); - if i < statics_len - 1 { - ending.push(','); - } - parts.push(MacroStringPart::String(ending)); - MacroString::MacroString(parts) - } - }, - )); - let statics_cmd = match joined_statics { - MacroString::String(s) => Command::Raw(format!( - r"data merge storage shulkerscript:function_arguments {{{s}}}" - )), - MacroString::MacroString(_) => Command::UsesMacro( - super::util::join_macro_strings([ - MacroString::String( - "data merge storage shulkerscript:function_arguments {" - .to_string(), - ), - joined_statics, - MacroString::String("}".to_string()), - ]) - .into(), - ), - }; - setup_cmds.push(statics_cmd); - setup_cmds.extend(move_cmds); - - Ok(( - function_location, - TranspiledFunctionArguments::Dynamic(setup_cmds), - )) - } else { - let function_args = parameters - .clone() - .into_iter() - .zip( - compiled_args - .into_iter() - .map(|arg| arg.into_static().expect("checked in if condition")), - ) - .collect(); - Ok(( - function_location, - TranspiledFunctionArguments::Static(function_args), - )) - } - } - } else { - Ok((function_location, TranspiledFunctionArguments::None)) - } - } - - fn transpile_function( - &mut self, - statements: &[Statement], - program_identifier: &str, - scope: &Arc, - handler: &impl Handler, - ) -> TranspileResult> { - let mut errors = Vec::new(); - let commands = statements - .iter() - .flat_map(|statement| { - self.transpile_statement(statement, program_identifier, scope, handler) - .unwrap_or_else(|err| { - errors.push(err); - Vec::new() - }) - }) - .collect(); - - if !errors.is_empty() { - return Err(errors.remove(0)); - } - - Ok(commands) - } - - fn transpile_statement( + pub(super) fn transpile_statement( &mut self, statement: &Statement, program_identifier: &str, diff --git a/src/transpile/util.rs b/src/transpile/util.rs index 4a19061..22ff0f1 100644 --- a/src/transpile/util.rs +++ b/src/transpile/util.rs @@ -44,6 +44,39 @@ impl Display for MacroString { } } +impl MacroString { + /// Check if the macro string contains any macros + #[must_use] + pub fn contains_macros(&self) -> bool { + match self { + Self::String(_) => false, + Self::MacroString(parts) => parts + .iter() + .any(|p| matches!(p, MacroStringPart::MacroUsage(_))), + } + } + + /// Get the string representation of the macro string or the parts if it contains macros + /// + /// # Errors + /// - If the macro string contains macros + pub fn as_str(&self) -> Result, &[MacroStringPart]> { + match self { + Self::String(s) => Ok(std::borrow::Cow::Borrowed(s)), + Self::MacroString(parts) if self.contains_macros() => Err(parts), + Self::MacroString(parts) => Ok(std::borrow::Cow::Owned( + parts + .iter() + .map(|p| match p { + MacroStringPart::String(s) => s.clone(), + MacroStringPart::MacroUsage(m) => format!("$({m})"), + }) + .collect::(), + )), + } + } +} + fn normalize_program_identifier(identifier: S) -> String where S: AsRef, diff --git a/src/util.rs b/src/util.rs index 85b32d8..8ec9a87 100644 --- a/src/util.rs +++ b/src/util.rs @@ -79,6 +79,48 @@ pub fn identifier_to_macro(ident: &str) -> std::borrow::Cow { } } +/// Transforms an identifier to a macro name that only contains `a-zA-Z0-9_`. +#[cfg(feature = "shulkerbox")] +#[must_use] +pub fn identifier_to_scoreboard_target(ident: &str) -> std::borrow::Cow { + if !(..=16).contains(&ident.len()) + || ident + .chars() + .any(|c| c != '_' || !c.is_ascii_alphanumeric()) + { + std::borrow::Cow::Owned(chksum_md5::hash(ident).to_hex_lowercase().split_off(16)) + } else { + std::borrow::Cow::Borrowed(ident) + } +} + +/// Transforms an identifier to a name that only contains `a-zA-Z0-9_`. +/// Does only strip invalid characters if the `shulkerbox` feature is not enabled. +#[cfg(not(feature = "shulkerbox"))] +#[must_use] +pub fn identifier_to_scoreboard_target(ident: &str) -> std::borrow::Cow { + if !(..=16).contains(&ident.len()) + || ident + .chars() + .any(|c| c != '_' || !c.is_ascii_alphanumeric()) + { + let new_ident = ident + .chars() + .map(|c| { + if *c != '_' && !c.is_ascii_alphanumeric() { + '_' + } else { + c + } + }) + .collect::(); + + std::borrow::Cow::Owned(new_ident) + } else { + std::borrow::Cow::Borrowed(ident) + } +} + /// Returns whether a string is a valid scoreboard name. #[must_use] pub fn is_valid_scoreboard_objective_name(name: &str) -> bool {